payment-kit 1.14.32 → 1.14.33

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.
@@ -398,7 +398,7 @@ export async function getUpcomingInvoiceAmount(subscriptionId: string) {
398
398
  }
399
399
 
400
400
  if (subscription.isActive() === false) {
401
- throw new Error('Subscription not active, so usage check is skipped');
401
+ throw new Error(`Subscription not active for ${subscriptionId}, so usage check is skipped`);
402
402
  }
403
403
 
404
404
  const currency = await PaymentCurrency.findByPk(subscription.currency_id);
@@ -122,6 +122,7 @@ export const handleInvoice = async (job: InvoiceJob) => {
122
122
  subscription_cycle: 'Subscription cycle',
123
123
  subscription_update: 'Subscription update',
124
124
  subscription_threshold: 'Subscription threshold',
125
+ slash_stake: 'Slash stake',
125
126
  };
126
127
  // TODO: support partial payment from user balance
127
128
  paymentIntent = await PaymentIntent.create({
@@ -1,5 +1,6 @@
1
1
  import isEmpty from 'lodash/isEmpty';
2
2
 
3
+ import { toStakeAddress } from '@arcblock/did-util';
3
4
  import { ensureStakedForGas } from '../integrations/arcblock/stake';
4
5
  import { transferErc20FromUser } from '../integrations/ethereum/token';
5
6
  import { createEvent } from '../libs/audit';
@@ -11,6 +12,7 @@ import logger from '../libs/logger';
11
12
  import { getGasPayerExtra, isDelegationSufficientForPayment } from '../libs/payment';
12
13
  import createQueue from '../libs/queue';
13
14
  import {
15
+ checkRemainingStake,
14
16
  getDaysUntilCancel,
15
17
  getDaysUntilDue,
16
18
  getDueUnit,
@@ -53,10 +55,10 @@ async function updateQuantitySold(checkoutSession: CheckoutSession) {
53
55
  await Promise.all(updatePromises);
54
56
  }
55
57
 
56
- export const handlePaymentSucceed = async (paymentIntent: PaymentIntent) => {
58
+ export const handlePaymentSucceed = async (paymentIntent: PaymentIntent, slashStake: boolean = false) => {
57
59
  // FIXME: @wangshijun we should check stripe payment here before
58
60
 
59
- if (paymentIntent.beneficiaries?.length) {
61
+ if (paymentIntent.beneficiaries?.length && !slashStake) {
60
62
  Promise.all(
61
63
  paymentIntent.beneficiaries.map(async (x) => {
62
64
  let customer = await Customer.findByPkOrDid(x.address);
@@ -119,7 +121,7 @@ export const handlePaymentSucceed = async (paymentIntent: PaymentIntent) => {
119
121
  if (paymentIntent.invoice_id) {
120
122
  invoice = await Invoice.findByPk(paymentIntent.invoice_id);
121
123
  }
122
- if (!invoice) {
124
+ if (!invoice && !slashStake) {
123
125
  const checkoutSession = await CheckoutSession.findOne({ where: { payment_intent_id: paymentIntent.id } });
124
126
  if (checkoutSession && checkoutSession.status === 'open') {
125
127
  updateQuantitySold(checkoutSession).catch((err) => {
@@ -138,7 +140,7 @@ export const handlePaymentSucceed = async (paymentIntent: PaymentIntent) => {
138
140
  return;
139
141
  }
140
142
 
141
- if (invoice.status !== 'paid') {
143
+ if (invoice && invoice?.status !== 'paid') {
142
144
  await invoice.update({
143
145
  paid: true,
144
146
  status: 'paid',
@@ -151,7 +153,7 @@ export const handlePaymentSucceed = async (paymentIntent: PaymentIntent) => {
151
153
  logger.info(`Invoice ${invoice.id} updated on payment done: ${paymentIntent.id}`);
152
154
  }
153
155
 
154
- if (invoice.subscription_id) {
156
+ if (invoice && invoice.subscription_id && !slashStake) {
155
157
  const subscription = await Subscription.findByPk(invoice.subscription_id);
156
158
 
157
159
  // We only update subscription status when the invoice is the latest one
@@ -212,7 +214,7 @@ export const handlePaymentSucceed = async (paymentIntent: PaymentIntent) => {
212
214
  }
213
215
  }
214
216
 
215
- if (invoice.checkout_session_id) {
217
+ if (invoice && invoice.checkout_session_id && !slashStake) {
216
218
  const checkoutSession = await CheckoutSession.findByPk(invoice.checkout_session_id);
217
219
  if (checkoutSession && checkoutSession.status === 'open') {
218
220
  updateQuantitySold(checkoutSession).catch((err) => {
@@ -375,6 +377,105 @@ export const handlePaymentFailed = async (
375
377
  return updates.retry;
376
378
  };
377
379
 
380
+ const handleStakeSlash = async (
381
+ invoice: Invoice,
382
+ paymentIntent: PaymentIntent,
383
+ paymentMethod: PaymentMethod,
384
+ customer: Customer,
385
+ paymentCurrency: PaymentCurrency
386
+ ) => {
387
+ const subscription = await Subscription.findByPk(invoice.subscription_id);
388
+ if (!subscription) {
389
+ logger.warn('Stake slashing skipped because Subscription not found', {
390
+ subscription: invoice.subscription_id,
391
+ paymentIntent: paymentIntent.id,
392
+ invoice: invoice.id,
393
+ });
394
+ return;
395
+ }
396
+ if (!subscription.cancelation_details?.slash_stake || subscription.status !== 'canceled') {
397
+ logger.warn('Stake slashing skipped because subscription not canceled or slash_stake is false', {
398
+ subscription: invoice.subscription_id,
399
+ paymentIntent: paymentIntent.id,
400
+ invoice: invoice.id,
401
+ });
402
+ return;
403
+ }
404
+
405
+ const address = toStakeAddress(customer.did, wallet.address, subscription.id);
406
+ const slashAmount = paymentIntent.amount;
407
+ const stakeEnough = await checkRemainingStake(paymentMethod, paymentCurrency, address, slashAmount);
408
+ if (!stakeEnough.enough) {
409
+ logger.warn('Stake slashing aborted because no enough staking', {
410
+ subscription: subscription.id,
411
+ paymentIntent: paymentIntent.id,
412
+ invoice: invoice.id,
413
+ address,
414
+ staked: stakeEnough.revoked,
415
+ revoked: stakeEnough.revoked,
416
+ });
417
+ return;
418
+ }
419
+ if (slashAmount === '0') {
420
+ logger.warn('Stake slashing aborted because slashAmount is 0', {
421
+ subscription: subscription.id,
422
+ paymentIntent: paymentIntent.id,
423
+ invoice: invoice.id,
424
+ address,
425
+ slashAmount,
426
+ });
427
+ return;
428
+ }
429
+
430
+ // do the slash
431
+ const client = paymentMethod.getOcapClient();
432
+ const signed = await client.signSlashStakeTx({
433
+ tx: {
434
+ itx: {
435
+ address: toStakeAddress(customer.did, wallet.address, subscription.id),
436
+ outputs: [{ owner: wallet.address, tokens: [{ address: paymentCurrency.contract, value: slashAmount }] }],
437
+ message: 'stake_slash_on_subscription_cancel',
438
+ data: {
439
+ typeUrl: 'json',
440
+ // @ts-ignore
441
+ value: {
442
+ appId: wallet.address,
443
+ reason: 'stake_slash_on_subscription_cancel',
444
+ subscriptionId: subscription.id,
445
+ invoiceId: invoice.id,
446
+ paymentIntentId: paymentIntent.id,
447
+ },
448
+ },
449
+ },
450
+ },
451
+ wallet,
452
+ });
453
+ // @ts-ignore
454
+ const { buffer } = await client.encodeSlashStakeTx({ tx: signed });
455
+ // @ts-ignore
456
+ const txHash = await client.sendSlashStakeTx({ tx: signed, wallet }, getGasPayerExtra(buffer));
457
+ logger.info('Stake slashing done', {
458
+ subscription: subscription.id,
459
+ amount: slashAmount,
460
+ paymentIntent: paymentIntent.id,
461
+ invoice: invoice.id,
462
+ address,
463
+ txHash,
464
+ });
465
+ await paymentIntent.update({
466
+ status: 'succeeded',
467
+ amount_received: slashAmount,
468
+ payment_details: {
469
+ arcblock: {
470
+ tx_hash: txHash,
471
+ payer: customer.did,
472
+ type: 'slash',
473
+ },
474
+ },
475
+ });
476
+ await handlePaymentSucceed(paymentIntent, true);
477
+ };
478
+
378
479
  export const handlePayment = async (job: PaymentJob) => {
379
480
  logger.info('handle payment', job);
380
481
 
@@ -450,6 +551,10 @@ export const handlePayment = async (job: PaymentJob) => {
450
551
  try {
451
552
  await paymentIntent.update({ status: 'processing', last_payment_error: null });
452
553
  if (paymentMethod.type === 'arcblock') {
554
+ if (invoice?.billing_reason === 'slash_stake') {
555
+ await handleStakeSlash(invoice, paymentIntent, paymentMethod, customer, paymentCurrency);
556
+ return;
557
+ }
453
558
  const client = paymentMethod.getOcapClient();
454
559
 
455
560
  // check balance before capture with transaction
@@ -529,7 +529,6 @@ const slashStakeOnCancel = async (subscription: Subscription) => {
529
529
  });
530
530
  return;
531
531
  }
532
- const client = paymentMethod.getOcapClient();
533
532
  const address = toStakeAddress(customer.did, wallet.address, subscription.id);
534
533
  const currency = await PaymentCurrency.findByPk(subscription.currency_id);
535
534
  if (!currency) {
@@ -560,99 +559,57 @@ const slashStakeOnCancel = async (subscription: Subscription) => {
560
559
  return;
561
560
  }
562
561
 
563
- // do the slash
564
- const signed = await client.signSlashStakeTx({
565
- tx: {
566
- itx: {
567
- address: toStakeAddress(customer.did, wallet.address, subscription.id),
568
- outputs: [{ owner: wallet.address, tokens: [{ address: currency.contract, value: result.return_amount }] }],
569
- message: 'stake_slash_on_subscription_cancel',
570
- data: {
571
- typeUrl: 'json',
572
- // @ts-ignore
573
- value: {
574
- appId: wallet.address,
575
- reason: 'stake_slash_on_subscription_cancel',
576
- subscriptionId: subscription.id,
577
- invoiceId: subscription.latest_invoice_id,
578
- paymentIntentId: result?.lastInvoice?.payment_intent_id as string,
579
- },
580
- },
581
- },
582
- },
583
- wallet,
584
- });
585
- // @ts-ignore
586
- const { buffer } = await client.encodeSlashStakeTx({ tx: signed });
587
- // @ts-ignore
588
- const txHash = await client.sendSlashStakeTx({ tx: signed, wallet }, getGasPayerExtra(buffer));
589
- logger.info('Stake slashing done', {
590
- subscription: subscription.id,
591
- amount: result.return_amount,
592
- address,
593
- txHash,
594
- });
595
- // create new payment intent
596
- const paymentIntent = await PaymentIntent.create({
597
- livemode: subscription.livemode,
598
- amount: result.return_amount,
599
- amount_received: result.return_amount,
600
- amount_capturable: '0',
601
- currency_id: subscription.currency_id,
602
- customer_id: subscription.customer_id,
603
- payment_method_id: subscription.default_payment_method_id,
604
- status: 'succeeded',
605
- capture_method: 'manual',
606
- last_payment_error: null,
607
- description: 'Stake slash on subscription cancel',
608
- statement_descriptor: result.lastInvoice?.statement_descriptor || getStatementDescriptor([]),
609
- payment_method_types: ['arcblock'],
610
- confirmation_method: '',
611
- payment_details: {
612
- arcblock: {
613
- tx_hash: txHash,
614
- payer: subscription.payment_details?.arcblock?.payer as string,
615
- type: 'slash',
616
- },
562
+ // FIXME: handle exist one more invoices
563
+ const invoice = await Invoice.findOne({
564
+ where: {
565
+ subscription_id: subscription.id,
566
+ billing_reason: 'slash_stake',
617
567
  },
618
568
  });
619
- logger.info('Payment intent created for stake slash', {
620
- paymentIntent: paymentIntent.id,
621
- subscription: subscription.id,
622
- });
623
- // 创建一笔账单关联到该订阅
624
- const { invoice } = await ensureInvoiceAndItems({
625
- customer,
626
- currency,
627
- subscription,
628
- trialing: false,
629
- metered: true,
630
- lineItems: [],
631
- props: {
632
- livemode: subscription.livemode,
633
- description: 'Slash stake',
634
- statement_descriptor: result.lastInvoice?.statement_descriptor,
635
- period_start: subscription.canceled_at,
636
- period_end: subscription.canceled_at,
637
-
638
- auto_advance: true,
639
- status: 'paid',
640
- billing_reason: 'stake_slash_on_subscription_cancel',
641
- currency_id: subscription.currency_id,
642
-
643
- total: result.return_amount,
644
- amount_paid: result.return_amount,
645
- amount_remaining: '0',
646
- amount_due: '0',
647
-
648
- payment_settings: subscription.payment_settings,
649
- default_payment_method_id: subscription.default_payment_method_id,
650
- payment_intent_id: paymentIntent.id,
651
- } as unknown as Invoice,
652
- });
653
- logger.info('Invoice created for stake slash', { invoice: invoice.id, subscription: subscription.id });
654
- paymentIntent.update({ invoice_id: invoice.id });
655
- logger.info('Payment intent updated with invoice', { invoice: invoice.id, paymentIntent: paymentIntent.id });
569
+ if (invoice) {
570
+ invoiceQueue.push({
571
+ id: invoice.id,
572
+ job: { invoiceId: invoice.id, retryOnError: true },
573
+ });
574
+ logger.info('Invoice job scheduled for slash stake', { invoice: invoice.id, subscription: subscription.id });
575
+ } else {
576
+ const { invoice: newInvoice } = await ensureInvoiceAndItems({
577
+ customer,
578
+ currency,
579
+ subscription,
580
+ trialing: false,
581
+ metered: false,
582
+ lineItems: [],
583
+ props: {
584
+ livemode: subscription.livemode,
585
+ description: 'Slash stake',
586
+ statement_descriptor: result.lastInvoice?.statement_descriptor,
587
+ period_start: subscription.canceled_at,
588
+ period_end: subscription.current_period_end as number,
589
+
590
+ auto_advance: true,
591
+ status: 'open',
592
+ billing_reason: 'slash_stake',
593
+ currency_id: subscription.currency_id,
594
+
595
+ total: result.return_amount,
596
+ subtotal: result.return_amount,
597
+ amount_paid: '0',
598
+ amount_remaining: result.return_amount,
599
+ amount_due: result.return_amount,
600
+
601
+ payment_settings: subscription.payment_settings,
602
+ default_payment_method_id: subscription.default_payment_method_id,
603
+ subscription_id: subscription.id,
604
+ } as unknown as Invoice,
605
+ });
606
+ logger.info('Invoice created for stake slash', { invoice: newInvoice.id, subscription: subscription.id });
607
+ invoiceQueue.push({
608
+ id: newInvoice.id,
609
+ job: { invoiceId: newInvoice.id, retryOnError: true },
610
+ });
611
+ logger.info('Invoice job scheduled for slash stake', { invoice: newInvoice.id, subscription: subscription.id });
612
+ }
656
613
  };
657
614
 
658
615
  // generate invoice for subscription periodically
@@ -150,11 +150,21 @@ router.get('/', authMine, async (req, res) => {
150
150
  stakeAmount = state.tokens.find((x: any) => x.address === currency?.contract)?.value;
151
151
  // stakeAmount should not be zero if nonce exist
152
152
  if (!Number(stakeAmount)) {
153
- const refund = await Refund.findOne({
154
- where: { subscription_id: subscription.id, status: 'succeeded', type: 'stake_return' },
155
- });
156
- if (refund) {
157
- stakeAmount = refund.amount;
153
+ if (subscription.cancelation_details?.return_stake) {
154
+ const refund = await Refund.findOne({
155
+ where: { subscription_id: subscription.id, status: 'succeeded', type: 'stake_return' },
156
+ });
157
+ if (refund) {
158
+ stakeAmount = refund.amount;
159
+ }
160
+ }
161
+ if (subscription.cancelation_details?.slash_stake) {
162
+ const invoice = await Invoice.findOne({
163
+ where: { subscription_id: subscription.id, status: 'paid', billing_reason: 'slash_stake' },
164
+ });
165
+ if (invoice) {
166
+ stakeAmount = invoice.total;
167
+ }
158
168
  }
159
169
  }
160
170
  }
@@ -58,7 +58,8 @@ export class Invoice extends Model<InferAttributes<Invoice>, InferCreationAttrib
58
58
  | 'subscription_cancel'
59
59
  | 'subscription'
60
60
  | 'manual'
61
- | 'upcoming',
61
+ | 'upcoming'
62
+ | 'slash_stake',
62
63
  string
63
64
  >;
64
65
 
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.14.32
17
+ version: 1.14.33
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.14.32",
3
+ "version": "1.14.33",
4
4
  "scripts": {
5
5
  "dev": "blocklet dev --open",
6
6
  "eject": "vite eject",
@@ -52,7 +52,7 @@
52
52
  "@arcblock/validator": "^1.18.132",
53
53
  "@blocklet/js-sdk": "1.16.28",
54
54
  "@blocklet/logger": "1.16.28",
55
- "@blocklet/payment-react": "1.14.32",
55
+ "@blocklet/payment-react": "1.14.33",
56
56
  "@blocklet/sdk": "1.16.28",
57
57
  "@blocklet/ui-react": "^2.10.23",
58
58
  "@blocklet/uploader": "^0.1.23",
@@ -119,7 +119,7 @@
119
119
  "devDependencies": {
120
120
  "@abtnode/types": "1.16.28",
121
121
  "@arcblock/eslint-config-ts": "^0.3.2",
122
- "@blocklet/payment-types": "1.14.32",
122
+ "@blocklet/payment-types": "1.14.33",
123
123
  "@types/cookie-parser": "^1.4.7",
124
124
  "@types/cors": "^2.8.17",
125
125
  "@types/debug": "^4.1.12",
@@ -161,5 +161,5 @@
161
161
  "parser": "typescript"
162
162
  }
163
163
  },
164
- "gitHead": "347accd83eb9354f423e06670d53003b4fd40114"
164
+ "gitHead": "e5ae487796f7c104b577dbab2a937013c3fbac12"
165
165
  }
@@ -76,7 +76,11 @@ export default function CustomerForm() {
76
76
  />
77
77
 
78
78
  <FormLabel className="base-label">{t('payment.checkout.billing.required')}</FormLabel>
79
- <Controller name="address.country" control={control} render={({ field }) => <CountrySelect {...field} />} />
79
+ <Controller
80
+ name="address.country"
81
+ control={control}
82
+ render={({ field }) => <CountrySelect {...field} sx={{ pl: '6px' }} />}
83
+ />
80
84
  <FormInput
81
85
  name="address.state"
82
86
  variant="outlined"
@@ -29,7 +29,7 @@ type Props = {
29
29
  changeActive?: (active: boolean) => void;
30
30
  } & Omit<StackProps, 'onChange'>;
31
31
 
32
- const pageSize = 4;
32
+ const pageSize = 5;
33
33
 
34
34
  export default function CurrentSubscriptions({
35
35
  id,
@@ -84,6 +84,7 @@ export default function CurrentSubscriptions({
84
84
  md: '500px',
85
85
  },
86
86
  overflowY: 'auto',
87
+ webkitOverflowScrolling: 'touch',
87
88
  }}>
88
89
  {data.list.map((subscription) => {
89
90
  return (