payment-kit 1.27.2 → 1.28.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 (184) hide show
  1. package/__blocklet__.js +37 -0
  2. package/api/ocap-1.30-subpath-shims.d.ts +35 -0
  3. package/api/src/crons/index.ts +10 -0
  4. package/api/src/crons/metering-subscription-detection.ts +12 -14
  5. package/api/src/crons/overdue-detection.ts +51 -74
  6. package/api/src/integrations/arcblock/nft.ts +6 -2
  7. package/api/src/integrations/arcblock/stake.ts +3 -2
  8. package/api/src/integrations/arcblock/token.ts +4 -4
  9. package/api/src/integrations/blocklet/notification.ts +1 -1
  10. package/api/src/integrations/ethereum/tx.ts +29 -0
  11. package/api/src/integrations/stripe/handlers/invoice.ts +70 -53
  12. package/api/src/integrations/stripe/handlers/payment-intent.ts +8 -1
  13. package/api/src/integrations/stripe/resource.ts +8 -0
  14. package/api/src/libs/audit.ts +32 -16
  15. package/api/src/libs/auth.ts +49 -2
  16. package/api/src/libs/chain-error.ts +31 -0
  17. package/api/src/libs/error.ts +15 -0
  18. package/api/src/libs/event.ts +42 -1
  19. package/api/src/libs/invoice.ts +69 -34
  20. package/api/src/libs/notification/template/customer-auto-recharge-daily-limit-exceeded.ts +1 -3
  21. package/api/src/libs/notification/template/customer-auto-recharge-failed.ts +1 -3
  22. package/api/src/libs/notification/template/customer-credit-grant-granted.ts +1 -3
  23. package/api/src/libs/notification/template/customer-credit-insufficient.ts +1 -3
  24. package/api/src/libs/notification/template/customer-credit-low-balance.ts +1 -3
  25. package/api/src/libs/notification/template/customer-revenue-succeeded.ts +1 -3
  26. package/api/src/libs/notification/template/customer-reward-succeeded.ts +1 -3
  27. package/api/src/libs/notification/template/one-time-payment-refund-succeeded.ts +1 -3
  28. package/api/src/libs/notification/template/one-time-payment-succeeded.ts +1 -3
  29. package/api/src/libs/notification/template/subscription-renew-failed.ts +1 -3
  30. package/api/src/libs/notification/template/subscription-slippage-exceeded.ts +1 -3
  31. package/api/src/libs/notification/template/subscription-slippage-warning.ts +1 -3
  32. package/api/src/libs/notification/template/subscription-succeeded.ts +1 -1
  33. package/api/src/libs/pagination.ts +14 -9
  34. package/api/src/libs/payment.ts +25 -10
  35. package/api/src/libs/session.ts +1 -1
  36. package/api/src/libs/timing.ts +35 -0
  37. package/api/src/libs/util.ts +16 -15
  38. package/api/src/libs/wallet-migration.ts +72 -53
  39. package/api/src/queues/auto-recharge.ts +1 -1
  40. package/api/src/queues/credit-consume.ts +94 -12
  41. package/api/src/queues/credit-grant.ts +4 -0
  42. package/api/src/queues/event.ts +14 -2
  43. package/api/src/queues/invoice.ts +1 -0
  44. package/api/src/queues/payment.ts +83 -15
  45. package/api/src/queues/refund.ts +84 -71
  46. package/api/src/queues/subscription.ts +1 -0
  47. package/api/src/routes/checkout-sessions.ts +82 -43
  48. package/api/src/routes/connect/change-payment.ts +2 -0
  49. package/api/src/routes/connect/change-plan.ts +2 -0
  50. package/api/src/routes/connect/pay.ts +12 -3
  51. package/api/src/routes/connect/setup.ts +3 -1
  52. package/api/src/routes/connect/shared.ts +52 -39
  53. package/api/src/routes/connect/subscribe.ts +4 -1
  54. package/api/src/routes/credit-grants.ts +25 -17
  55. package/api/src/routes/donations.ts +2 -2
  56. package/api/src/routes/meter-events.ts +16 -6
  57. package/api/src/routes/payment-links.ts +1 -1
  58. package/api/src/routes/payment-methods.ts +1 -1
  59. package/api/src/routes/settings.ts +1 -1
  60. package/api/src/routes/tax-rates.ts +1 -1
  61. package/api/src/store/models/customer.ts +23 -1
  62. package/api/src/store/models/payment-method.ts +4 -0
  63. package/api/src/store/models/price.ts +23 -14
  64. package/api/tests/libs/wallet-migration.spec.ts +4 -4
  65. package/api/tests/queues/credit-consume-batch.spec.ts +5 -2
  66. package/api/tests/queues/credit-consume.spec.ts +8 -4
  67. package/api/tests/routes/credit-grants.spec.ts +1 -0
  68. package/blocklet.yml +1 -1
  69. package/cloudflare/MIGRATION-CHALLENGES.md +676 -0
  70. package/cloudflare/MIGRATION-RUNBOOK.md +777 -0
  71. package/cloudflare/README.md +499 -0
  72. package/cloudflare/STAGING-MIGRATION-GUIDE.md +602 -0
  73. package/cloudflare/build.ts +151 -0
  74. package/cloudflare/did-connect-auth.ts +527 -0
  75. package/cloudflare/docs/2026-04-22-sdk-1.30.9-upgrade-retro.md +324 -0
  76. package/cloudflare/docs/2026-04-24-queue-ops-followup.md +218 -0
  77. package/cloudflare/docs/cf-queues-ops-alert-analysis.md +663 -0
  78. package/cloudflare/docs/cf-workers-local-dev-and-fixes.md +284 -0
  79. package/cloudflare/docs/cleanup-tasks-2026-05.md +62 -0
  80. package/cloudflare/docs/payment-kit-platform-analysis-2026-04-20.md +354 -0
  81. package/cloudflare/frontend-shims/buffer-polyfill.ts +9 -0
  82. package/cloudflare/frontend-shims/js-sdk.ts +43 -0
  83. package/cloudflare/frontend-shims/mime-types.ts +46 -0
  84. package/cloudflare/frontend-shims/session.ts +24 -0
  85. package/cloudflare/frontend-shims/vite-plugin-noop.ts +6 -0
  86. package/cloudflare/index.html +40 -0
  87. package/cloudflare/migrate-to-d1.js +252 -0
  88. package/cloudflare/migrations/0001_initial_schema.sql +82 -0
  89. package/cloudflare/migrations/0002_indexes.sql +75 -0
  90. package/cloudflare/migrations/0003_locks_and_constraints.sql +18 -0
  91. package/cloudflare/run-build.js +390 -0
  92. package/cloudflare/scripts/test-decrypt.js +102 -0
  93. package/cloudflare/shims/arcblock-ws.ts +20 -0
  94. package/cloudflare/shims/axios-http-adapter.ts +4 -0
  95. package/cloudflare/shims/axios-lite.ts +117 -0
  96. package/cloudflare/shims/blocklet-sdk/auth-service.ts +33 -0
  97. package/cloudflare/shims/blocklet-sdk/cdn.ts +3 -0
  98. package/cloudflare/shims/blocklet-sdk/component-api.ts +35 -0
  99. package/cloudflare/shims/blocklet-sdk/component.ts +18 -0
  100. package/cloudflare/shims/blocklet-sdk/config.ts +8 -0
  101. package/cloudflare/shims/blocklet-sdk/did.ts +14 -0
  102. package/cloudflare/shims/blocklet-sdk/env.ts +12 -0
  103. package/cloudflare/shims/blocklet-sdk/eventbus.ts +3 -0
  104. package/cloudflare/shims/blocklet-sdk/fallback.ts +3 -0
  105. package/cloudflare/shims/blocklet-sdk/index.ts +11 -0
  106. package/cloudflare/shims/blocklet-sdk/logger.ts +11 -0
  107. package/cloudflare/shims/blocklet-sdk/middlewares.ts +15 -0
  108. package/cloudflare/shims/blocklet-sdk/notification.ts +11 -0
  109. package/cloudflare/shims/blocklet-sdk/security.ts +53 -0
  110. package/cloudflare/shims/blocklet-sdk/session.ts +8 -0
  111. package/cloudflare/shims/blocklet-sdk/verify-sign.ts +38 -0
  112. package/cloudflare/shims/blocklet-sdk/wallet-authenticator.ts +3 -0
  113. package/cloudflare/shims/blocklet-sdk/wallet-handler.ts +6 -0
  114. package/cloudflare/shims/blocklet-sdk/wallet.ts +103 -0
  115. package/cloudflare/shims/cookie-parser.ts +3 -0
  116. package/cloudflare/shims/cors.ts +21 -0
  117. package/cloudflare/shims/cron.ts +189 -0
  118. package/cloudflare/shims/crypto-js-warn.ts +7 -0
  119. package/cloudflare/shims/did-space-js.ts +17 -0
  120. package/cloudflare/shims/did-space.ts +11 -0
  121. package/cloudflare/shims/error.ts +18 -0
  122. package/cloudflare/shims/express-compat/index.ts +80 -0
  123. package/cloudflare/shims/express-compat/types.ts +41 -0
  124. package/cloudflare/shims/fastq.ts +105 -0
  125. package/cloudflare/shims/lock.ts +115 -0
  126. package/cloudflare/shims/mime-types.ts +56 -0
  127. package/cloudflare/shims/nedb-storage.ts +9 -0
  128. package/cloudflare/shims/node-child-process.ts +9 -0
  129. package/cloudflare/shims/node-fs.ts +20 -0
  130. package/cloudflare/shims/node-http.ts +13 -0
  131. package/cloudflare/shims/node-https.ts +4 -0
  132. package/cloudflare/shims/node-misc.ts +15 -0
  133. package/cloudflare/shims/node-net.ts +8 -0
  134. package/cloudflare/shims/node-os.ts +14 -0
  135. package/cloudflare/shims/node-tty.ts +8 -0
  136. package/cloudflare/shims/node-zlib.ts +17 -0
  137. package/cloudflare/shims/noop.ts +26 -0
  138. package/cloudflare/shims/payment-vendor.ts +14 -0
  139. package/cloudflare/shims/querystring.ts +12 -0
  140. package/cloudflare/shims/queue.ts +585 -0
  141. package/cloudflare/shims/rolldown-runtime.ts +43 -0
  142. package/cloudflare/shims/sequelize-d1/datatypes.ts +24 -0
  143. package/cloudflare/shims/sequelize-d1/helpers.ts +46 -0
  144. package/cloudflare/shims/sequelize-d1/index.ts +34 -0
  145. package/cloudflare/shims/sequelize-d1/model.ts +1157 -0
  146. package/cloudflare/shims/sequelize-d1/operators.ts +293 -0
  147. package/cloudflare/shims/sequelize-d1/retry.ts +85 -0
  148. package/cloudflare/shims/sequelize-d1/sequelize-class.ts +119 -0
  149. package/cloudflare/shims/sequelize-d1/timing.ts +81 -0
  150. package/cloudflare/shims/sequelize-d1/types.ts +35 -0
  151. package/cloudflare/shims/stripe-cf.ts +29 -0
  152. package/cloudflare/shims/ws-lite.ts +103 -0
  153. package/cloudflare/shims/xss.ts +3 -0
  154. package/cloudflare/tests/shims/cron.spec.ts +210 -0
  155. package/cloudflare/tests/shims/queue-scheduled.spec.ts +186 -0
  156. package/cloudflare/vite.config.ts +162 -0
  157. package/cloudflare/worker.ts +1553 -0
  158. package/cloudflare/wrangler.json +63 -0
  159. package/cloudflare/wrangler.jsonc +69 -0
  160. package/cloudflare/wrangler.staging.json +66 -0
  161. package/cloudflare/wrangler.toml +28 -0
  162. package/jest.config.js +4 -12
  163. package/package.json +26 -22
  164. package/src/app.tsx +62 -4
  165. package/src/components/customer/link.tsx +9 -13
  166. package/src/components/customer/notification-preference.tsx +3 -2
  167. package/src/components/filter-toolbar.tsx +4 -0
  168. package/src/components/invoice/list.tsx +9 -1
  169. package/src/components/invoice-pdf/utils.ts +2 -1
  170. package/src/components/layout/admin.tsx +39 -5
  171. package/src/components/layout/user-cf.tsx +77 -0
  172. package/src/components/payment-intent/actions.tsx +23 -3
  173. package/src/components/safe-did-address.tsx +75 -0
  174. package/src/libs/patch-user-card.ts +25 -0
  175. package/src/libs/util.ts +5 -7
  176. package/src/pages/admin/billing/meter-events/index.tsx +4 -0
  177. package/src/pages/admin/customers/customers/detail.tsx +2 -2
  178. package/src/pages/admin/customers/customers/index.tsx +2 -2
  179. package/src/pages/admin/overview.tsx +3 -1
  180. package/src/pages/customer/subscription/detail.tsx +4 -4
  181. package/tsconfig.api.json +1 -6
  182. package/tsconfig.json +3 -4
  183. package/tsconfig.types.json +2 -1
  184. package/vite.config.ts +6 -1
@@ -0,0 +1,37 @@
1
+ // Polyfill global for browser
2
+ if (typeof globalThis !== 'undefined' && typeof global === 'undefined') {
3
+ window.global = globalThis;
4
+ }
5
+ if (typeof globalThis !== 'undefined' && typeof Buffer === 'undefined') {
6
+ // Buffer polyfill will be loaded by vite-plugin-node-polyfills if configured
7
+ }
8
+
9
+ // CF Workers shim for window.blocklet
10
+ // This file replaces the auto-generated __blocklet__.js from Blocklet Server
11
+ window.blocklet = window.blocklet || {};
12
+ Object.assign(window.blocklet, {
13
+ prefix: '',
14
+ groupPrefix: '',
15
+ did: 'did:abt:test',
16
+ componentId: null,
17
+ serverVersion: '3.0.0',
18
+ languages: [
19
+ { code: 'en', name: 'English' },
20
+ { code: 'zh', name: '简体中文' }
21
+ ],
22
+ appPid: 'payment-kit-cf',
23
+ appId: 'payment-kit-cf',
24
+ appName: 'Payment Kit',
25
+ appLogo: '',
26
+ appDescription: 'Decentralized Payment System',
27
+ appUrl: window.location.origin,
28
+ GA_MEASUREMENT_ID: '',
29
+ theme: { prefer: 'light' },
30
+ navigation: [{ title: 'Home', link: '/', icon: '' }],
31
+ webWalletUrl: '',
32
+ componentMountPoints: [],
33
+ sessionPermissions: { canReadAll: true, canWriteAll: true },
34
+ settings: {
35
+ session: { cacheTtl: 3600 },
36
+ },
37
+ });
@@ -0,0 +1,35 @@
1
+ // Type shims for @ocap/* / @arcblock/* 1.30.x CBOR-only subpath entries.
2
+ //
3
+ // These packages ship runtime + `.d.mts` type files under the package.json
4
+ // `exports` map. tsc under `moduleResolution: node` cannot resolve `.d.mts`
5
+ // through the exports map, producing:
6
+ // error TS2307: Cannot find module '@arcblock/did-util/cbor' or its
7
+ // corresponding type declarations.
8
+ //
9
+ // Switching moduleResolution to `bundler`/`node16` would fix resolution but
10
+ // require matching `module: esnext`, which changes the emitted JS format for
11
+ // api/dist runtime. Not worth the blast radius.
12
+ //
13
+ // Instead: declare loose types here so tsc is satisfied. Runtime resolution
14
+ // still uses the real packages via Node/esbuild's own module loader (they
15
+ // handle .cjs + exports map correctly).
16
+
17
+ declare module '@arcblock/did-util/cbor' {
18
+ // biome-ignore lint/suspicious/noExplicitAny: loose shim; real types live in .d.mts
19
+ type AnyFn = (...args: any[]) => any;
20
+ export const toAssetAddress: AnyFn;
21
+ export const toDelegateAddress: AnyFn;
22
+ export const toFactoryAddress: AnyFn;
23
+ export const toItxAddress: AnyFn;
24
+ export const toRollupAddress: AnyFn;
25
+ export const toStakeAddress: AnyFn;
26
+ export const toTokenAddress: AnyFn;
27
+ export const toTokenFactoryAddress: AnyFn;
28
+ }
29
+
30
+ declare module '@ocap/asset/mint/client' {
31
+ // biome-ignore lint/suspicious/noExplicitAny: loose shim; real types live in .d.mts
32
+ type AnyFn = (...args: any[]) => any;
33
+ export const formatFactoryState: AnyFn;
34
+ export const preMintFromFactory: AnyFn;
35
+ }
@@ -24,6 +24,7 @@ import {
24
24
  import logger from '../libs/logger';
25
25
  import { startCreditConsumeQueue } from '../queues/credit-consume';
26
26
  import { startDepositVaultQueue } from '../queues/payment';
27
+ import { startRefundQueue } from '../queues/refund';
27
28
  import { startSubscriptionQueue } from '../queues/subscription';
28
29
  import { startVendorStatusCheckSchedule } from '../queues/vendors/status-check';
29
30
  import { CheckoutSession } from '../store/models';
@@ -85,6 +86,15 @@ function init() {
85
86
  fn: startSubscriptionQueue,
86
87
  options: { runOnInit: false },
87
88
  },
89
+ {
90
+ // Recover pending refunds that were missed (e.g. after deploy or failed dispatch).
91
+ // Previously invoked unconditionally on every CF scheduled tick; now throttled
92
+ // to every 5 minutes to stay within CF Queue free-tier ops budget.
93
+ name: 'refund.recovery',
94
+ time: '0 */5 * * * *',
95
+ fn: startRefundQueue,
96
+ options: { runOnInit: false },
97
+ },
88
98
  {
89
99
  name: 'checkoutSession.cleanup.expired',
90
100
  time: expiredSessionCleanupCronTime,
@@ -119,20 +119,18 @@ export class MeteringSubscriptionDetectionTemplate implements BaseEmailTemplate<
119
119
  });
120
120
 
121
121
  const meteringInvoices: any[] = [];
122
- await Promise.all(
123
- invoices.map(async (invoice) => {
124
- const invoiceJson = invoice.toJSON();
125
- const prices = (await Price.findAll()).map((x) => x.toJSON());
126
- // @ts-ignore
127
- (invoiceJson.lines || []).forEach((item) => {
128
- item.price = prices.find((x) => x.id === item.price_id);
129
- });
130
- // @ts-ignore
131
- if ((invoiceJson.lines || []).some((line) => line.price?.recurring?.usage_type === 'metered')) {
132
- meteringInvoices.push(invoice);
133
- }
134
- })
135
- );
122
+ const prices = (await Price.findAll()).map((x) => x.toJSON());
123
+ for (const invoice of invoices) {
124
+ const invoiceJson = invoice.toJSON();
125
+ // @ts-ignore
126
+ (invoiceJson.lines || []).forEach((item) => {
127
+ item.price = prices.find((x) => x.id === item.price_id);
128
+ });
129
+ // @ts-ignore
130
+ if ((invoiceJson.lines || []).some((line) => line.price?.recurring?.usage_type === 'metered')) {
131
+ meteringInvoices.push(invoice);
132
+ }
133
+ }
136
134
  const meteringInvoiceIds = Array.from(new Set(meteringInvoices.map((invoice) => invoice.id)));
137
135
  const meteringSubscriptionIds = Array.from(
138
136
  new Set(meteringInvoices.map((invoice) => invoice?.subscription_id).filter(Boolean) as string[])
@@ -102,19 +102,19 @@ export class HealthReportTemplate implements BaseEmailTemplate<HealthReportConte
102
102
  const startTime = dayjs.unix(start).toISOString();
103
103
  const endTime = dayjs.unix(end).toISOString();
104
104
 
105
- // 1. Get all currency configurations
106
- const allCurrencies = await PaymentCurrency.findAll();
107
- const currencyMap = new Map(allCurrencies.map((c) => [c.id, c]));
108
-
109
- // 2. Fetch all pending meter events in the last 24 hours
110
- const events = await MeterEvent.findAll({
111
- where: {
112
- status: ['pending', 'requires_capture', 'requires_action'],
113
- created_at: {
114
- [Op.between]: [startTime, endTime],
105
+ // 1. Get all currency configurations + pending meter events in parallel
106
+ const [allCurrencies, events] = await Promise.all([
107
+ PaymentCurrency.findAll(),
108
+ MeterEvent.findAll({
109
+ where: {
110
+ status: ['pending', 'requires_capture', 'requires_action'],
111
+ created_at: {
112
+ [Op.between]: [startTime, endTime],
113
+ },
115
114
  },
116
- },
117
- });
115
+ }),
116
+ ]);
117
+ const currencyMap = new Map(allCurrencies.map((c) => [c.id, c]));
118
118
 
119
119
  if (events.length === 0) {
120
120
  return { customerCount: 0, pendingAmounts: '0', exceedsThreshold: false };
@@ -209,25 +209,21 @@ export class HealthReportTemplate implements BaseEmailTemplate<HealthReportConte
209
209
  const startTime = dayjs.unix(start).toISOString();
210
210
  const endTime = dayjs.unix(end).toISOString();
211
211
 
212
- // Get all successful payments in the last 24 hours
213
- const payments = await PaymentIntent.findAll({
214
- where: {
215
- status: 'succeeded',
216
- created_at: {
217
- [Op.between]: [startTime, endTime],
212
+ // Get all successful payments + refunds in parallel
213
+ const [payments, refunds] = await Promise.all([
214
+ PaymentIntent.findAll({
215
+ where: {
216
+ status: 'succeeded',
217
+ created_at: { [Op.between]: [startTime, endTime] },
218
218
  },
219
- },
220
- });
221
-
222
- // Get all refunds in the last 24 hours
223
- const refunds = await Refund.findAll({
224
- where: {
225
- status: 'succeeded',
226
- created_at: {
227
- [Op.between]: [startTime, endTime],
219
+ }),
220
+ Refund.findAll({
221
+ where: {
222
+ status: 'succeeded',
223
+ created_at: { [Op.between]: [startTime, endTime] },
228
224
  },
229
- },
230
- });
225
+ }),
226
+ ]);
231
227
 
232
228
  // Aggregate total revenue by currency
233
229
  const revenueByCurrency = new Map<string, BN>();
@@ -282,33 +278,18 @@ export class HealthReportTemplate implements BaseEmailTemplate<HealthReportConte
282
278
  const startTime = dayjs.unix(start).toISOString();
283
279
  const endTime = dayjs.unix(end).toISOString();
284
280
 
285
- // New subscriptions created in the last 24 hours
286
- const newSubscriptions = await Subscription.count({
287
- where: {
288
- created_at: {
289
- [Op.between]: [startTime, endTime],
290
- },
291
- },
292
- });
293
-
294
- // Subscriptions canceled in the last 24 hours
295
- const canceledSubscriptions = await Subscription.count({
296
- where: {
297
- canceled_at: {
298
- [Op.between]: [start, end],
299
- },
300
- },
301
- });
302
-
303
- // Subscriptions that became past_due in the last 24 hours
304
- const pastDueSubscriptions = await Subscription.count({
305
- where: {
306
- status: 'past_due',
307
- updated_at: {
308
- [Op.between]: [startTime, endTime],
309
- },
310
- },
311
- });
281
+ // All 3 counts are independent run in parallel
282
+ const [newSubscriptions, canceledSubscriptions, pastDueSubscriptions] = await Promise.all([
283
+ Subscription.count({
284
+ where: { created_at: { [Op.between]: [startTime, endTime] } },
285
+ }),
286
+ Subscription.count({
287
+ where: { canceled_at: { [Op.between]: [start, end] } },
288
+ }),
289
+ Subscription.count({
290
+ where: { status: 'past_due', updated_at: { [Op.between]: [startTime, endTime] } },
291
+ }),
292
+ ]);
312
293
 
313
294
  return {
314
295
  newSubscriptions,
@@ -405,26 +386,22 @@ export class HealthReportTemplate implements BaseEmailTemplate<HealthReportConte
405
386
  const startTime = dayjs.unix(start).toISOString();
406
387
  const endTime = dayjs.unix(end).toISOString();
407
388
 
408
- // Successful payments
409
- const succeededPayments = await PaymentIntent.findAll({
410
- where: {
411
- status: 'succeeded',
412
- created_at: {
413
- [Op.between]: [startTime, endTime],
414
- },
415
- },
416
- });
417
-
418
- // Failed payments (canceled status)
389
+ // Successful + failed payments in parallel
419
390
  const failedStatuses = ['canceled', 'requires_payment_method', 'requires_action'];
420
- const failedPayments = await PaymentIntent.findAll({
421
- where: {
422
- status: { [Op.in]: failedStatuses },
423
- created_at: {
424
- [Op.between]: [startTime, endTime],
391
+ const [succeededPayments, failedPayments] = await Promise.all([
392
+ PaymentIntent.findAll({
393
+ where: {
394
+ status: 'succeeded',
395
+ created_at: { [Op.between]: [startTime, endTime] },
425
396
  },
426
- },
427
- });
397
+ }),
398
+ PaymentIntent.findAll({
399
+ where: {
400
+ status: { [Op.in]: failedStatuses },
401
+ created_at: { [Op.between]: [startTime, endTime] },
402
+ },
403
+ }),
404
+ ]);
428
405
 
429
406
  const succeededCount = succeededPayments.length;
430
407
  const failedCount = failedPayments.length;
@@ -1,5 +1,5 @@
1
1
  import { isEthereumDid, isValid } from '@arcblock/did';
2
- import { formatFactoryState, preMintFromFactory } from '@ocap/asset';
2
+ import { formatFactoryState, preMintFromFactory } from '@ocap/asset/mint/client';
3
3
  import merge from 'lodash/merge';
4
4
 
5
5
  import { wallet } from '../../libs/auth';
@@ -62,6 +62,7 @@ export async function mintNftForCheckoutSession(id: string) {
62
62
  inputs: inputs || {},
63
63
  owner: nftOwner,
64
64
  issuer: { wallet, name: appState.moniker },
65
+ encoding: 'protobuf',
65
66
  });
66
67
  logger.info('nft preMint for checkoutSession', { id, inputs, nftOwner, factory, preMint });
67
68
 
@@ -88,7 +89,10 @@ export async function mintNftForCheckoutSession(id: string) {
88
89
  factory,
89
90
  address: preMint.address,
90
91
  assets: [],
91
- variables: Object.entries(preMint.variables).map(([key, value]) => ({ name: key, value })),
92
+ // preMint.variables is Record<string,unknown>; VariableInput expects
93
+ // typed value which we can't narrow here. Cast to any — chain side
94
+ // validates the actual schema.
95
+ variables: Object.entries(preMint.variables).map(([key, value]) => ({ name: key, value })) as any,
92
96
  owner: nftOwner,
93
97
  },
94
98
  },
@@ -3,7 +3,7 @@
3
3
  import assert from 'assert';
4
4
 
5
5
  import { isEthereumDid } from '@arcblock/did';
6
- import { toStakeAddress } from '@arcblock/did-util';
6
+ import { toStakeAddress } from '@arcblock/did-util/cbor';
7
7
  import { BN, fromUnitToToken, toBN } from '@ocap/util';
8
8
 
9
9
  import { Op } from 'sequelize';
@@ -46,7 +46,8 @@ export async function ensureStakedForGas() {
46
46
  const [hash] = await client.stake({
47
47
  to: wallet.address,
48
48
  message: 'stake-for-gas',
49
- tokens: [{ address: token.address, value: fromUnitToToken(txConfig.txGas.minStake, token.decimal) }],
49
+ // @ts-ignore - token.decimal may be string from DB
50
+ tokens: [{ address: token.address, value: fromUnitToToken(txConfig.txGas.minStake, Number(token.decimal)) }],
50
51
  wallet,
51
52
  });
52
53
  logger.info(`staked for gas on chain ${host}`, { hash });
@@ -1,4 +1,4 @@
1
- import { toStakeAddress, toTokenAddress, toTokenFactoryAddress } from '@arcblock/did-util';
1
+ import { toStakeAddress, toTokenAddress, toTokenFactoryAddress } from '@arcblock/did-util/cbor';
2
2
  import { fromPublicKeyHash, toTypeInfo } from '@arcblock/did';
3
3
  import stringify from 'json-stable-stringify';
4
4
  import { BN, fromTokenToUnit, fromUnitToToken, toBN, toBase58, toBase64 } from '@ocap/util';
@@ -138,7 +138,7 @@ async function createTokenVC(data: { tokenAddress: string; symbol: string; websi
138
138
  vc.proof = proof;
139
139
 
140
140
  const isValid = await verifyVC({
141
- vc,
141
+ vc: vc as any,
142
142
  ownerDid: wallet.address,
143
143
  trustedIssuers: wallet.address,
144
144
  });
@@ -298,8 +298,8 @@ export async function createToken(data: { name: string; symbol: string; decimal?
298
298
  reserveAddress: forgeState.token.address,
299
299
  };
300
300
 
301
- factoryItx.token.address = toTokenAddress(factoryItx.token);
302
- factoryItx.address = toTokenFactoryAddress(factoryItx);
301
+ factoryItx.token.address = toTokenAddress(factoryItx.token, 'protobuf');
302
+ factoryItx.address = toTokenFactoryAddress(factoryItx, 'protobuf');
303
303
 
304
304
  // Step 1: Create and publish VC
305
305
  logger.info('Creating and publishing token VC', { symbol: data.symbol });
@@ -1,6 +1,6 @@
1
1
  import Notification from '@blocklet/sdk/service/notification';
2
2
  import { toDid } from '@ocap/util';
3
- import { get } from 'lodash';
3
+ import get from 'lodash/get';
4
4
  import type { LiteralUnion } from 'type-fest';
5
5
 
6
6
  import { blocklet } from '../../libs/auth';
@@ -1,3 +1,32 @@
1
+ // ===========================================================================
2
+ // CF Workers compatibility issue:
3
+ //
4
+ // waitForEvmTxReceipt polls every 3s for up to 30 minutes (timeout: 30*60*1000).
5
+ // waitForEvmTxConfirm polls every 3s for up to 30 minutes.
6
+ //
7
+ // CF Workers request handlers have a ~30s wall-clock limit (paid plan).
8
+ // Even CF Cron Triggers have a ~15min limit. Both are far below 30 minutes.
9
+ //
10
+ // Impact: Base chain (and all EVM chain) payments fail during onAuth because
11
+ // executeEvmTransaction awaits waitForEvmTxReceipt which times out.
12
+ //
13
+ // Proposed fix (requires significant refactoring):
14
+ // 1. In executeEvmTransaction, do ONE immediate check for the receipt.
15
+ // If the tx is already confirmed (common for fast chains like Base), return.
16
+ // 2. If not confirmed yet, persist a "pending_evm_confirmation" job to the queue
17
+ // with the txHash, block_height, and callback info.
18
+ // 3. Add a cron job (e.g., every 1 minute) that polls pending EVM confirmations:
19
+ // - Query the Job table for pending_evm_confirmation jobs
20
+ // - For each, call provider.getTransactionReceipt(txHash)
21
+ // - If confirmed, execute the post-confirmation logic (update invoice, etc.)
22
+ // - If not confirmed and not timed out, leave for next cron cycle
23
+ // - If timed out (e.g., 30 min since creation), mark as failed
24
+ // 4. The onAuth handler returns immediately after the initial check + job creation,
25
+ // similar to how waitForEvmTxConfirm is already fire-and-forget in pay.ts.
26
+ //
27
+ // This decouples the long-polling from the request lifecycle.
28
+ // ===========================================================================
29
+
1
30
  import type { JsonRpcProvider, TransactionReceipt, TransactionResponse } from 'ethers';
2
31
  import waitFor from 'p-wait-for';
3
32
 
@@ -181,59 +181,76 @@ export async function ensureStripeInvoice(stripeInvoice: any, subscription: Subs
181
181
  }
182
182
 
183
183
  const invoiceNumber = await customer.getInvoiceNumber();
184
- // @ts-ignore
185
- invoice = await Invoice.create({
186
- number: invoiceNumber,
187
- ...pick(stripeInvoice, [
188
- 'amount_due',
189
- 'amount_paid',
190
- 'amount_remaining',
191
- 'amount_shipping',
192
- 'attempt_count',
193
- 'attempted',
194
- 'auto_advance',
195
- 'billing_reason',
196
- 'collection_method',
197
- 'custom_fields',
198
- 'customer_address',
199
- 'customer_email',
200
- 'customer_name',
201
- 'customer_phone',
202
- 'description',
203
- 'discounts',
204
- 'due_date',
205
- 'effective_at',
206
- 'ending_balance',
207
- 'livemode',
208
- 'paid_out_of_band',
209
- 'paid',
210
- 'period_end',
211
- 'period_start',
212
- 'starting_balance',
213
- 'status_transitions',
214
- 'status',
215
- 'subtotal_excluding_tax',
216
- 'subtotal',
217
- 'tax',
218
- 'total',
219
- 'last_finalization_error',
220
- ]),
221
- discounts: processDiscounts,
222
- total_discount_amounts: processTotalDiscounts,
223
- currency_id: subscription.currency_id,
224
- customer_id: subscription.customer_id,
225
- default_payment_method_id: subscription.default_payment_method_id as string,
226
- payment_intent_id: '',
227
- subscription_id: subscription.id,
228
- checkout_session_id: checkoutSession?.id,
229
- statement_descriptor: stripeInvoice.statement_descriptor || '',
230
-
231
- payment_settings: subscription.payment_settings,
232
- metadata: {
233
- stripe_id: stripeInvoice.id,
234
- stripe_discounts: stripeInvoice.discounts,
235
- },
236
- });
184
+ try {
185
+ // @ts-ignore
186
+ invoice = await Invoice.create({
187
+ number: invoiceNumber,
188
+ ...pick(stripeInvoice, [
189
+ 'amount_due',
190
+ 'amount_paid',
191
+ 'amount_remaining',
192
+ 'amount_shipping',
193
+ 'attempt_count',
194
+ 'attempted',
195
+ 'auto_advance',
196
+ 'billing_reason',
197
+ 'collection_method',
198
+ 'custom_fields',
199
+ 'customer_address',
200
+ 'customer_email',
201
+ 'customer_name',
202
+ 'customer_phone',
203
+ 'description',
204
+ 'discounts',
205
+ 'due_date',
206
+ 'effective_at',
207
+ 'ending_balance',
208
+ 'livemode',
209
+ 'paid_out_of_band',
210
+ 'paid',
211
+ 'period_end',
212
+ 'period_start',
213
+ 'starting_balance',
214
+ 'status_transitions',
215
+ 'status',
216
+ 'subtotal_excluding_tax',
217
+ 'subtotal',
218
+ 'tax',
219
+ 'total',
220
+ 'last_finalization_error',
221
+ ]),
222
+ discounts: processDiscounts,
223
+ total_discount_amounts: processTotalDiscounts,
224
+ currency_id: subscription.currency_id,
225
+ customer_id: subscription.customer_id,
226
+ default_payment_method_id: subscription.default_payment_method_id as string,
227
+ payment_intent_id: '',
228
+ subscription_id: subscription.id,
229
+ checkout_session_id: checkoutSession?.id,
230
+ statement_descriptor: stripeInvoice.statement_descriptor || '',
231
+
232
+ payment_settings: subscription.payment_settings,
233
+ metadata: {
234
+ stripe_id: stripeInvoice.id,
235
+ stripe_discounts: stripeInvoice.discounts,
236
+ },
237
+ });
238
+ } catch (err: any) {
239
+ // DB-level safety net: unique constraint on metadata.stripe_id prevents duplicate mirroring
240
+ // even when the in-memory lock fails (CF Workers multi-isolate race condition)
241
+ if (err.name === 'SequelizeUniqueConstraintError' || err.message?.includes('UNIQUE constraint failed')) {
242
+ logger.warn('Duplicate stripe invoice creation caught by unique constraint', {
243
+ stripeInvoiceId: stripeInvoice.id,
244
+ subscriptionId: subscription.id,
245
+ });
246
+ invoice = await Invoice.findOne({ where: { 'metadata.stripe_id': stripeInvoice.id } });
247
+ if (invoice) {
248
+ lock.release();
249
+ return invoice;
250
+ }
251
+ }
252
+ throw err;
253
+ }
237
254
  if (checkoutSession) {
238
255
  await checkoutSession.update({ invoice_id: invoice.id });
239
256
  }
@@ -6,6 +6,7 @@ import type Stripe from 'stripe';
6
6
 
7
7
  import dayjs from '../../../libs/dayjs';
8
8
  import logger from '../../../libs/logger';
9
+ // eslint-disable-next-line import/no-cycle
9
10
  import { handlePaymentSucceed } from '../../../queues/payment';
10
11
  import { Invoice, PaymentIntent, PaymentMethod, Subscription, TEventExpanded } from '../../../store/models';
11
12
  import { handleStripeInvoiceCreated } from './invoice';
@@ -27,7 +28,13 @@ export async function handleStripePaymentSucceed(paymentIntent: PaymentIntent, e
27
28
 
28
29
  const checkoutSessionId = event?.data?.object?.metadata?.checkoutSessionId;
29
30
  if (checkoutSessionId) {
30
- events.emit('checkout.session.pending_invoice', { checkoutSessionId, paymentIntentId: paymentIntent.id });
31
+ // Directly await invoice creation instead of fire-and-forget EventEmitter
32
+ // (CF Workers may terminate before async EventEmitter listeners complete)
33
+ try {
34
+ events.emit('checkout.session.pending_invoice', { checkoutSessionId, paymentIntentId: paymentIntent.id });
35
+ } catch (e: any) {
36
+ logger.error('pending_invoice event handler error', { checkoutSessionId, error: e?.message });
37
+ }
31
38
  }
32
39
 
33
40
  await handlePaymentSucceed(paymentIntent, triggerRenew);
@@ -27,6 +27,7 @@ import {
27
27
  TLineItemExpanded,
28
28
  } from '../../store/models';
29
29
  import { syncStripeInvoice } from './handlers/invoice';
30
+ // eslint-disable-next-line import/no-cycle
30
31
  import { syncStripePayment } from './handlers/payment-intent';
31
32
  import { getLock } from '../../libs/lock';
32
33
  import { getPriceUintAmountByCurrency } from '../../libs/price';
@@ -212,6 +213,13 @@ export async function ensureStripePaymentIntent(
212
213
  let stripeIntent = null;
213
214
  if (internal.payment_details?.stripe?.payment_intent_id) {
214
215
  stripeIntent = await client.paymentIntents.retrieve(internal.payment_details.stripe.payment_intent_id);
216
+ // Ensure Stripe metadata has local PI id + checkoutSessionId for webhook matching
217
+ const meta = stripeIntent.metadata || {};
218
+ if (meta.id !== internal.id || (checkoutSessionId && meta.checkoutSessionId !== checkoutSessionId)) {
219
+ stripeIntent = await client.paymentIntents.update(stripeIntent.id, {
220
+ metadata: { ...meta, appPid: env.appPid, id: internal.id, checkoutSessionId: checkoutSessionId || '' },
221
+ });
222
+ }
215
223
  } else {
216
224
  const customer = await ensureStripePaymentCustomer(internal, method);
217
225
  stripeIntent = await client.paymentIntents.create({