payment-kit 1.13.49 → 1.13.50

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.
@@ -13,6 +13,7 @@ import { handleStripeInvoiceCreated } from './invoice';
13
13
  export async function handleStripePaymentSucceed(paymentIntent: PaymentIntent, event?: TEventExpanded) {
14
14
  await paymentIntent.update({
15
15
  status: 'succeeded',
16
+ last_payment_error: null,
16
17
  amount_received: paymentIntent.amount,
17
18
  payment_details: merge(
18
19
  paymentIntent.metadata,
@@ -2,7 +2,7 @@ import { wallet } from '../libs/auth';
2
2
  import dayjs from '../libs/dayjs';
3
3
  import CustomError from '../libs/error';
4
4
  import logger from '../libs/logger';
5
- import { isDelegationSufficientForPayment } from '../libs/payment';
5
+ import { getGasPayerExtra, isDelegationSufficientForPayment } from '../libs/payment';
6
6
  import createQueue from '../libs/queue';
7
7
  import { MAX_RETRY_COUNT, getNextRetry } from '../libs/util';
8
8
  import { CheckoutSession } from '../store/models/checkout-session';
@@ -128,12 +128,14 @@ export const handlePayment = async (job: PaymentJob) => {
128
128
  amount: paymentIntent.amount,
129
129
  });
130
130
  if (result.sufficient === false) {
131
+ logger.error('PaymentIntent capture aborted on preCheck', { id: paymentIntent.id, result });
132
+ // FIXME: send email to customer, pause subscription
131
133
  throw new CustomError(result.reason, 'payer balance or delegation not sufficient for this payment');
132
134
  }
133
135
 
134
136
  // do the capture
135
137
  await paymentIntent.update({ status: 'processing' });
136
- const txHash = await client.sendTransferV2Tx({
138
+ const signed = await client.signTransferV2Tx({
137
139
  tx: {
138
140
  itx: {
139
141
  to: wallet.address,
@@ -150,14 +152,20 @@ export const handlePayment = async (job: PaymentJob) => {
150
152
  },
151
153
  },
152
154
  },
153
- delegator: payer,
154
155
  wallet,
156
+ delegator: payer,
155
157
  });
158
+ // @ts-ignore
159
+ const { buffer } = await client.encodeTransferV2Tx({ tx: signed });
160
+ // @ts-ignore
161
+ const txHash = await client.sendTransferV2Tx({ tx: signed, wallet, delegator: payer }, getGasPayerExtra(buffer));
162
+
156
163
  logger.info(`PaymentIntent capture done: ${paymentIntent.id} with tx ${txHash}`);
157
164
 
158
165
  await paymentIntent.update({
159
166
  status: 'succeeded',
160
167
  amount_received: paymentIntent.amount,
168
+ last_payment_error: null,
161
169
  payment_details: {
162
170
  arcblock: {
163
171
  tx_hash: txHash,
@@ -1,6 +1,8 @@
1
1
  import { toDelegateAddress } from '@arcblock/did-util';
2
+ import { sign } from '@arcblock/jwt';
2
3
  import { getWalletDid } from '@blocklet/sdk/lib/did';
3
4
  import type { DelegateState } from '@ocap/client';
5
+ import { toTxHash } from '@ocap/mcrypto';
4
6
  import { BN } from '@ocap/util';
5
7
 
6
8
  import type { TPaymentCurrency } from '../store/models/payment-currency';
@@ -42,7 +44,7 @@ export async function isDelegationSufficientForPayment(args: {
42
44
  if (!token) {
43
45
  return { sufficient: false, reason: 'NO_TOKEN' };
44
46
  }
45
- if (new BN(token.balance).lte(new BN(amount))) {
47
+ if (new BN(token.balance).lt(new BN(amount))) {
46
48
  return { sufficient: false, reason: 'NO_ENOUGH_TOKEN' };
47
49
  }
48
50
 
@@ -55,3 +57,13 @@ export async function isDelegationSufficientForPayment(args: {
55
57
 
56
58
  throw new Error(`Payment method ${paymentMethod.type} not supported`);
57
59
  }
60
+
61
+ export function getGasPayerExtra(txBuffer: Buffer) {
62
+ const txHash = toTxHash(txBuffer);
63
+ return {
64
+ headers: {
65
+ 'x-gas-payer-sig': sign(wallet.address, wallet.secretKey, { txHash }),
66
+ 'x-gas-payer-pk': wallet.publicKey,
67
+ },
68
+ };
69
+ }
@@ -506,11 +506,17 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
506
506
  return res.status(403).json({ code: 'PAYMENT_PROCESSING', error: 'Checkout session payment processing' });
507
507
  }
508
508
  paymentIntent = await paymentIntent.update({
509
+ status: 'requires_capture',
509
510
  amount: checkoutSession.amount_total,
510
511
  customer_id: customer.id,
511
512
  currency_id: paymentCurrency.id,
512
513
  payment_method_id: paymentMethod.id,
513
514
  receipt_email: customer.email,
515
+ last_payment_error: null,
516
+ });
517
+ logger.info('payment intent for checkout session reset', {
518
+ session: checkoutSession.id,
519
+ intent: paymentIntent.id,
514
520
  });
515
521
  } else {
516
522
  paymentIntent = await PaymentIntent.create({
@@ -547,7 +553,7 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
547
553
  }
548
554
 
549
555
  let setupIntent: SetupIntent | null = null;
550
- if (checkoutSession.mode === 'setup' || paymentMethod.type !== 'stripe') {
556
+ if (checkoutSession.mode === 'setup' && paymentMethod.type !== 'stripe') {
551
557
  if (checkoutSession.setup_intent_id) {
552
558
  setupIntent = await SetupIntent.findByPk(checkoutSession.setup_intent_id);
553
559
  }
@@ -563,16 +569,22 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
563
569
  return res.status(403).json({ code: 'SETUP_PROCESSING', error: 'Checkout session setup processing' });
564
570
  }
565
571
  await setupIntent.update({
572
+ status: 'requires_capture',
566
573
  customer_id: customer.id,
567
574
  currency_id: paymentCurrency.id,
568
575
  payment_method_id: paymentMethod.id,
576
+ last_setup_error: null,
577
+ });
578
+ logger.info('setup intent for checkout session reset', {
579
+ session: checkoutSession.id,
580
+ intent: setupIntent.id,
569
581
  });
570
582
  } else {
571
583
  // ensure payment intent
572
584
  setupIntent = await SetupIntent.create({
573
585
  livemode: !!checkoutSession.livemode,
574
586
  customer_id: customer.id,
575
- description: '',
587
+ description: checkoutSession.payment_intent_data?.description || '',
576
588
  currency_id: paymentCurrency.id,
577
589
  payment_method_id: paymentMethod.id,
578
590
  status: 'requires_payment_method',
@@ -698,20 +710,15 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
698
710
  });
699
711
  }
700
712
  if (checkoutSession.mode === 'payment' && paymentIntent) {
701
- await paymentIntent.update({ status: 'requires_capture' });
702
713
  const { invoice } = await ensureInvoiceForCheckout({ checkoutSession, customer, paymentIntent });
703
714
  if (invoice) {
704
715
  await invoice.update({ auto_advance: true, payment_settings: paymentSettings });
705
716
  invoiceQueue.push({ id: invoice.id, job: { invoiceId: invoice.id, retryOnError: false } });
706
717
  } else {
707
- const job = paymentQueue.push({
718
+ paymentQueue.push({
708
719
  id: paymentIntent.id,
709
720
  job: { paymentIntentId: paymentIntent.id, paymentSettings, retryOnError: false },
710
721
  });
711
- job.on('finished', async () => {
712
- await checkoutSession.update({ status: 'complete', payment_status: 'paid' });
713
- logger.info(`CheckoutSession ${checkoutSession.id} updated on payment done ${paymentIntent?.id}`);
714
- });
715
722
  }
716
723
  }
717
724
  if (checkoutSession.mode === 'setup' && setupIntent && subscription) {
@@ -5,6 +5,7 @@ import { invoiceQueue } from '../../jobs/invoice';
5
5
  import { handlePaymentSucceed, paymentQueue } from '../../jobs/payment';
6
6
  import type { CallbackArgs } from '../../libs/auth';
7
7
  import { wallet } from '../../libs/auth';
8
+ import { getGasPayerExtra } from '../../libs/payment';
8
9
  import { getTxMetadata } from '../../libs/util';
9
10
  import { ensureInvoiceForCollect, getAuthPrincipalClaim } from './shared';
10
11
 
@@ -49,7 +50,7 @@ export default {
49
50
 
50
51
  throw new Error(`Payment method ${paymentMethod.type} not supported`);
51
52
  },
52
- onAuth: async ({ userDid, claims, request, extraParams }: CallbackArgs) => {
53
+ onAuth: async ({ userDid, claims, extraParams }: CallbackArgs) => {
53
54
  const { invoiceId } = extraParams;
54
55
  const { invoice, paymentIntent, paymentMethod } = await ensureInvoiceForCollect(invoiceId);
55
56
 
@@ -64,16 +65,19 @@ export default {
64
65
  tx.from = claim.from;
65
66
  }
66
67
 
68
+ // @ts-ignore
69
+ const { buffer } = await client.encodeTransferV3Tx({ tx });
67
70
  const txHash = await client.sendTransferV3Tx(
68
71
  // @ts-ignore
69
72
  { tx, wallet: fromAddress(userDid) },
70
- { headers: client.pickGasPayerHeaders(request) }
73
+ getGasPayerExtra(buffer)
71
74
  );
72
75
 
73
76
  await paymentIntent.update({
74
77
  status: 'succeeded',
75
78
  amount_received: invoice.amount_due,
76
79
  capture_method: 'manual',
80
+ last_payment_error: null,
77
81
  payment_details: {
78
82
  arcblock: {
79
83
  tx_hash: txHash,
@@ -4,6 +4,7 @@ import { fromAddress } from '@ocap/wallet';
4
4
  import { handlePaymentSucceed } from '../../jobs/payment';
5
5
  import type { CallbackArgs } from '../../libs/auth';
6
6
  import { wallet } from '../../libs/auth';
7
+ import { getGasPayerExtra } from '../../libs/payment';
7
8
  import { getTxMetadata } from '../../libs/util';
8
9
  import { ensureInvoiceForCheckout, ensurePaymentIntent, getAuthPrincipalClaim } from './shared';
9
10
 
@@ -50,7 +51,7 @@ export default {
50
51
 
51
52
  throw new Error(`Payment method ${paymentMethod.type} not supported`);
52
53
  },
53
- onAuth: async ({ userDid, claims, request, extraParams }: CallbackArgs) => {
54
+ onAuth: async ({ userDid, claims, extraParams }: CallbackArgs) => {
54
55
  const { checkoutSessionId } = extraParams;
55
56
  const { checkoutSession, customer, paymentIntent, paymentMethod } = await ensurePaymentIntent(
56
57
  checkoutSessionId,
@@ -73,15 +74,18 @@ export default {
73
74
  tx.from = claim.from;
74
75
  }
75
76
 
77
+ // @ts-ignore
78
+ const { buffer } = await client.encodeTransferV3Tx({ tx });
76
79
  const txHash = await client.sendTransferV3Tx(
77
80
  // @ts-ignore
78
81
  { tx, wallet: fromAddress(userDid) },
79
- { headers: client.pickGasPayerHeaders(request) }
82
+ getGasPayerExtra(buffer)
80
83
  );
81
84
 
82
85
  await paymentIntent.update({
83
86
  status: 'succeeded',
84
87
  amount_received: paymentIntent.amount,
88
+ last_payment_error: null,
85
89
  payment_details: {
86
90
  arcblock: {
87
91
  tx_hash: txHash,
@@ -6,6 +6,7 @@ import { fromPublicKey } from '@ocap/wallet';
6
6
  import { subscriptionQueue } from '../../jobs/subscription';
7
7
  import type { CallbackArgs } from '../../libs/auth';
8
8
  import { wallet } from '../../libs/auth';
9
+ import { getGasPayerExtra } from '../../libs/payment';
9
10
  import { getFastCheckoutAmount } from '../../libs/session';
10
11
  import { getTxMetadata } from '../../libs/util';
11
12
  import type { TLineItemExpanded } from '../../store/models';
@@ -56,7 +57,7 @@ export default {
56
57
  },
57
58
  nonce: checkoutSessionId,
58
59
  requirement: {
59
- tokens: [{ address: paymentCurrency.contract as string, value: amount }],
60
+ tokens: amount === '0' ? [] : [{ address: paymentCurrency.contract as string, value: amount }],
60
61
  },
61
62
  chainInfo: {
62
63
  host: paymentMethod.settings?.arcblock?.api_host as string,
@@ -68,7 +69,7 @@ export default {
68
69
 
69
70
  throw new Error(`Payment method ${paymentMethod.type} not supported`);
70
71
  },
71
- onAuth: async ({ userDid, userPk, claims, request, extraParams }: CallbackArgs) => {
72
+ onAuth: async ({ userDid, userPk, claims, extraParams }: CallbackArgs) => {
72
73
  const { checkoutSessionId } = extraParams;
73
74
  const { setupIntent, checkoutSession, paymentMethod, subscription } = await ensureSetupIntent(
74
75
  checkoutSessionId,
@@ -80,6 +81,7 @@ export default {
80
81
  }
81
82
 
82
83
  if (paymentMethod.type === 'arcblock') {
84
+ await setupIntent.update({ status: 'processing' });
83
85
  await subscription.update({
84
86
  payment_settings: {
85
87
  payment_method_types: ['arcblock'],
@@ -94,15 +96,20 @@ export default {
94
96
 
95
97
  // execute the delegate tx
96
98
  const tx: Partial<Transaction> = client.decodeTx(claim.origin);
99
+ tx.signature = claim.sig;
100
+
101
+ // @ts-ignore
102
+ const { buffer } = await client.encodeDelegateTx({ tx });
97
103
  const txHash = await client.sendDelegateTx(
98
104
  // @ts-ignore
99
- { tx, wallet: fromPublicKey(userPk, toTypeInfo(userDid)), signature: claim.sig },
100
- { headers: client.pickGasPayerHeaders(request) }
105
+ { tx, wallet: fromPublicKey(userPk, toTypeInfo(userDid)) },
106
+ getGasPayerExtra(buffer)
101
107
  );
102
108
 
103
109
  await setupIntent.update({
104
110
  status: 'succeeded',
105
111
  payment_method_types: ['arcblock'],
112
+ last_setup_error: null,
106
113
  payment_method_options: {
107
114
  arcblock: { payer: userDid },
108
115
  },
@@ -226,9 +226,20 @@ export async function ensureInvoiceForCheckout({
226
226
 
227
227
  // Do not create invoice if it's already created
228
228
  if (checkoutSession.invoice_id) {
229
- logger.warn(`Invoice already created for checkout session ${checkoutSession.id}: ${checkoutSession.invoice_id}`);
229
+ logger.info(`Invoice already created for checkout session ${checkoutSession.id}: ${checkoutSession.invoice_id}`);
230
+ const invoice = await Invoice.findByPk(checkoutSession.invoice_id);
231
+ if (invoice) {
232
+ await invoice.update({ status: 'open' });
233
+ logger.info(`Invoice status reset for checkout session ${checkoutSession.id}: ${checkoutSession.invoice_id}`);
234
+ if (invoice.payment_intent_id) {
235
+ await PaymentIntent.update({ status: 'requires_capture' }, { where: { id: invoice.payment_intent_id } });
236
+ logger.info(
237
+ `PaymentIntent status reset for checkout session ${checkoutSession.id}: ${invoice.payment_intent_id}`
238
+ );
239
+ }
240
+ }
230
241
  return {
231
- invoice: await Invoice.findByPk(checkoutSession.invoice_id),
242
+ invoice,
232
243
  items: await InvoiceItem.findAll({ where: { invoice_id: checkoutSession.invoice_id } }),
233
244
  };
234
245
  }
@@ -8,6 +8,7 @@ import { invoiceQueue } from '../../jobs/invoice';
8
8
  import { subscriptionQueue } from '../../jobs/subscription';
9
9
  import type { CallbackArgs } from '../../libs/auth';
10
10
  import { wallet } from '../../libs/auth';
11
+ import { getGasPayerExtra } from '../../libs/payment';
11
12
  import { getFastCheckoutAmount } from '../../libs/session';
12
13
  import { getTxMetadata } from '../../libs/util';
13
14
  import type { TLineItemExpanded } from '../../store/models';
@@ -58,7 +59,7 @@ export default {
58
59
  },
59
60
  nonce: checkoutSessionId,
60
61
  requirement: {
61
- tokens: [{ address: paymentCurrency.contract as string, value: amount }],
62
+ tokens: amount === '0' ? [] : [{ address: paymentCurrency.contract as string, value: amount }],
62
63
  },
63
64
  chainInfo: {
64
65
  host: paymentMethod.settings?.arcblock?.api_host as string,
@@ -70,7 +71,7 @@ export default {
70
71
 
71
72
  throw new Error(`Payment method ${paymentMethod.type} not supported`);
72
73
  },
73
- onAuth: async ({ userDid, userPk, claims, request, extraParams }: CallbackArgs) => {
74
+ onAuth: async ({ userDid, userPk, claims, extraParams }: CallbackArgs) => {
74
75
  const { checkoutSessionId } = extraParams;
75
76
  const { checkoutSession, customer, paymentMethod, paymentCurrency, subscription } = await ensurePaymentIntent(
76
77
  checkoutSessionId,
@@ -109,10 +110,14 @@ export default {
109
110
 
110
111
  // execute the delegate tx
111
112
  const tx: Partial<Transaction> = client.decodeTx(claim.origin);
113
+ tx.signature = claim.sig;
114
+
115
+ // @ts-ignore
116
+ const { buffer } = await client.encodeDelegateTx({ tx });
112
117
  const txHash = await client.sendDelegateTx(
113
118
  // @ts-ignore
114
- { tx, wallet: fromPublicKey(userPk, toTypeInfo(userDid)), signature: claim.sig },
115
- { headers: client.pickGasPayerHeaders(request) }
119
+ { tx, wallet: fromPublicKey(userPk, toTypeInfo(userDid)) },
120
+ getGasPayerExtra(buffer)
116
121
  );
117
122
 
118
123
  await subscription.update({
@@ -35,7 +35,7 @@ export class PaymentIntent extends Model<InferAttributes<PaymentIntent>, InferCr
35
35
  declare last_charge?: string;
36
36
 
37
37
  // The payment error encountered in the previous PaymentIntent confirmation
38
- declare last_payment_error?: PaymentError;
38
+ declare last_payment_error: PaymentError | null;
39
39
 
40
40
  declare metadata?: Record<string, any>;
41
41
 
@@ -22,7 +22,7 @@ export class SetupIntent extends Model<InferAttributes<SetupIntent>, InferCreati
22
22
  declare customer_id: string;
23
23
 
24
24
  // The setup error encountered in the previous SetupIntent confirmation
25
- declare last_setup_error?: PaymentError;
25
+ declare last_setup_error: PaymentError | null;
26
26
 
27
27
  declare metadata?: Record<string, any>;
28
28
 
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.13.49
17
+ version: 1.13.50
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.13.49",
3
+ "version": "1.13.50",
4
4
  "scripts": {
5
5
  "dev": "blocklet dev",
6
6
  "eject": "vite eject",
@@ -44,6 +44,7 @@
44
44
  "@arcblock/did-auth-storage-nedb": "^1.7.1",
45
45
  "@arcblock/did-connect": "^2.8.7",
46
46
  "@arcblock/did-util": "^1.18.95",
47
+ "@arcblock/jwt": "^1.18.95",
47
48
  "@arcblock/ux": "^2.8.7",
48
49
  "@blocklet/logger": "1.16.17",
49
50
  "@blocklet/sdk": "1.16.17",
@@ -103,7 +104,7 @@
103
104
  "@abtnode/types": "1.16.17",
104
105
  "@arcblock/eslint-config": "^0.2.4",
105
106
  "@arcblock/eslint-config-ts": "^0.2.4",
106
- "@did-pay/types": "1.13.49",
107
+ "@did-pay/types": "1.13.50",
107
108
  "@types/cookie-parser": "^1.4.5",
108
109
  "@types/cors": "^2.8.15",
109
110
  "@types/dotenv-flow": "^3.3.2",
@@ -140,5 +141,5 @@
140
141
  "parser": "typescript"
141
142
  }
142
143
  },
143
- "gitHead": "8fd11641c7ee8f092afc35eb9615c27d321d398c"
144
+ "gitHead": "7bdfa2019c557823145e8d262c887b9066c90d04"
144
145
  }
@@ -6,7 +6,7 @@ import { joinURL } from 'ufo';
6
6
  const getTxLink = (method: TPaymentMethod, details: PaymentDetails) => {
7
7
  if (method.type === 'arcblock') {
8
8
  return {
9
- link: joinURL(method.settings.arcblock?.explorer_host as string, '/tx', details.arcblock?.tx_hash as string),
9
+ link: joinURL(method.settings.arcblock?.explorer_host as string, '/txs', details.arcblock?.tx_hash as string),
10
10
  text: details.arcblock?.tx_hash as string,
11
11
  };
12
12
  }
@@ -37,8 +37,16 @@ const getTxLink = (method: TPaymentMethod, details: PaymentDetails) => {
37
37
  return { text: 'N/A', link: '' };
38
38
  };
39
39
 
40
- export default function TxLink(props: { details: PaymentDetails; method: TPaymentMethod }) {
41
- if (!props.details) {
40
+ TxLink.defaultProps = {
41
+ mode: 'dashboard',
42
+ };
43
+
44
+ export default function TxLink(props: {
45
+ details: PaymentDetails;
46
+ method: TPaymentMethod;
47
+ mode?: 'customer' | 'dashboard';
48
+ }) {
49
+ if (!props.details || (props.mode === 'customer' && props.method.type === 'stripe')) {
42
50
  return (
43
51
  <Typography component="small" color="text.secondary">
44
52
  None
@@ -62,7 +62,7 @@ export default function CustomerInvoiceList({ customer_id }: Props) {
62
62
  const grouped = groupByDate(data.list);
63
63
 
64
64
  return (
65
- <Stack direction="column" gap={3} sx={{ mt: 1 }}>
65
+ <Stack direction="column" gap={1} sx={{ mt: 1 }}>
66
66
  {Object.entries(grouped).map(([date, invoices]) => (
67
67
  <Box key={date}>
68
68
  <Typography sx={{ fontWeight: 'bold', color: 'text.secondary', mt: 2, mb: 1 }}>{date}</Typography>
@@ -73,6 +73,7 @@ export default function CustomerInvoiceList({ customer_id }: Props) {
73
73
  xs: 'column',
74
74
  sm: 'row',
75
75
  }}
76
+ sx={{ my: 1 }}
76
77
  gap={{
77
78
  xs: 0.5,
78
79
  sm: 1.5,
@@ -111,7 +111,7 @@ export default function CustomerHome() {
111
111
  label={t('common.txHash')}
112
112
  value={
113
113
  data.paymentIntent?.payment_details ? (
114
- <TxLink details={data.paymentIntent.payment_details} method={data.paymentMethod} />
114
+ <TxLink details={data.paymentIntent.payment_details} method={data.paymentMethod} mode="customer" />
115
115
  ) : (
116
116
  ''
117
117
  )