payment-kit 1.27.2 → 1.29.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.
- package/__blocklet__.js +37 -0
- package/api/ocap-1.30-subpath-shims.d.ts +35 -0
- package/api/src/crons/index.ts +32 -0
- package/api/src/crons/metering-subscription-detection.ts +12 -14
- package/api/src/crons/overdue-detection.ts +51 -74
- package/api/src/crons/retry-pending-events.ts +58 -0
- package/api/src/integrations/app-store/apple-root-certs.ts +26 -0
- package/api/src/integrations/app-store/client.ts +369 -0
- package/api/src/integrations/app-store/handlers/index.ts +46 -0
- package/api/src/integrations/app-store/handlers/subscription.ts +635 -0
- package/api/src/integrations/app-store/node-apple-receipt-verify.d.ts +17 -0
- package/api/src/integrations/app-store/notification-routing.ts +18 -0
- package/api/src/integrations/app-store/signed-data-verifier.ts +150 -0
- package/api/src/integrations/arcblock/nft.ts +6 -2
- package/api/src/integrations/arcblock/stake.ts +3 -2
- package/api/src/integrations/arcblock/token.ts +4 -4
- package/api/src/integrations/blocklet/notification.ts +1 -1
- package/api/src/integrations/ethereum/tx.ts +29 -0
- package/api/src/integrations/google-play/client.ts +276 -0
- package/api/src/integrations/google-play/handlers/index.ts +69 -0
- package/api/src/integrations/google-play/handlers/subscription.ts +565 -0
- package/api/src/integrations/google-play/handlers/voided.ts +106 -0
- package/api/src/integrations/google-play/setup.ts +43 -0
- package/api/src/integrations/google-play/verify.ts +251 -0
- package/api/src/integrations/iap-reconcile.ts +415 -0
- package/api/src/integrations/stripe/handlers/invoice.ts +70 -53
- package/api/src/integrations/stripe/handlers/payment-intent.ts +8 -1
- package/api/src/integrations/stripe/resource.ts +8 -0
- package/api/src/libs/audit.ts +70 -24
- package/api/src/libs/auth.ts +49 -2
- package/api/src/libs/chain-error.ts +31 -0
- package/api/src/libs/entitlement.ts +399 -0
- package/api/src/libs/env.ts +2 -0
- package/api/src/libs/error.ts +15 -0
- package/api/src/libs/event.ts +42 -1
- package/api/src/libs/invoice.ts +69 -34
- package/api/src/libs/notification/template/customer-auto-recharge-daily-limit-exceeded.ts +1 -3
- package/api/src/libs/notification/template/customer-auto-recharge-failed.ts +1 -3
- package/api/src/libs/notification/template/customer-credit-grant-granted.ts +1 -3
- package/api/src/libs/notification/template/customer-credit-insufficient.ts +1 -3
- package/api/src/libs/notification/template/customer-credit-low-balance.ts +1 -3
- package/api/src/libs/notification/template/customer-revenue-succeeded.ts +1 -3
- package/api/src/libs/notification/template/customer-reward-succeeded.ts +1 -3
- package/api/src/libs/notification/template/one-time-payment-refund-succeeded.ts +1 -3
- package/api/src/libs/notification/template/one-time-payment-succeeded.ts +1 -3
- package/api/src/libs/notification/template/subscription-renew-failed.ts +1 -3
- package/api/src/libs/notification/template/subscription-slippage-exceeded.ts +1 -3
- package/api/src/libs/notification/template/subscription-slippage-warning.ts +1 -3
- package/api/src/libs/notification/template/subscription-succeeded.ts +1 -1
- package/api/src/libs/pagination.ts +14 -9
- package/api/src/libs/payment.ts +25 -10
- package/api/src/libs/security.ts +51 -0
- package/api/src/libs/session.ts +1 -1
- package/api/src/libs/subscription.ts +13 -1
- package/api/src/libs/timing.ts +35 -0
- package/api/src/libs/util.ts +29 -15
- package/api/src/libs/wallet-migration.ts +72 -53
- package/api/src/queues/auto-recharge.ts +1 -1
- package/api/src/queues/credit-consume.ts +94 -12
- package/api/src/queues/credit-grant.ts +4 -0
- package/api/src/queues/event.ts +39 -21
- package/api/src/queues/invoice.ts +1 -0
- package/api/src/queues/payment.ts +83 -15
- package/api/src/queues/refund.ts +84 -71
- package/api/src/queues/subscription.ts +1 -0
- package/api/src/queues/webhook.ts +12 -2
- package/api/src/routes/checkout-sessions.ts +82 -43
- package/api/src/routes/connect/change-payment.ts +2 -0
- package/api/src/routes/connect/change-plan.ts +2 -0
- package/api/src/routes/connect/pay.ts +12 -3
- package/api/src/routes/connect/setup.ts +3 -1
- package/api/src/routes/connect/shared.ts +52 -39
- package/api/src/routes/connect/subscribe.ts +4 -1
- package/api/src/routes/credit-grants.ts +25 -17
- package/api/src/routes/donations.ts +2 -2
- package/api/src/routes/entitlements.ts +105 -0
- package/api/src/routes/events.ts +2 -2
- package/api/src/routes/index.ts +12 -2
- package/api/src/routes/integrations/app-store.ts +267 -0
- package/api/src/routes/integrations/google-play.ts +324 -0
- package/api/src/routes/meter-events.ts +16 -6
- package/api/src/routes/payment-links.ts +1 -1
- package/api/src/routes/payment-methods.ts +131 -1
- package/api/src/routes/settings.ts +1 -1
- package/api/src/routes/tax-rates.ts +1 -1
- package/api/src/store/migrations/20260526-iap-foundation.ts +105 -0
- package/api/src/store/models/customer.ts +37 -1
- package/api/src/store/models/entitlement-grant.ts +118 -0
- package/api/src/store/models/entitlement-product.ts +48 -0
- package/api/src/store/models/entitlement.ts +86 -0
- package/api/src/store/models/index.ts +9 -0
- package/api/src/store/models/invoice.ts +20 -0
- package/api/src/store/models/payment-method.ts +66 -1
- package/api/src/store/models/price.ts +23 -14
- package/api/src/store/models/refund.ts +10 -0
- package/api/src/store/models/subscription.ts +14 -0
- package/api/src/store/models/types.ts +32 -0
- package/api/tests/integrations/app-store/client.spec.ts +335 -0
- package/api/tests/integrations/app-store/handlers.spec.ts +480 -0
- package/api/tests/integrations/app-store/notifications.spec.ts +381 -0
- package/api/tests/integrations/app-store/signed-data-verifier.spec.ts +72 -0
- package/api/tests/integrations/app-store/webhook-routing.spec.ts +27 -0
- package/api/tests/integrations/google-play/handlers.spec.ts +341 -0
- package/api/tests/integrations/google-play/verify.spec.ts +215 -0
- package/api/tests/integrations/iap-reconcile.spec.ts +237 -0
- package/api/tests/libs/entitlement.spec.ts +347 -0
- package/api/tests/libs/wallet-migration.spec.ts +4 -4
- package/api/tests/queues/credit-consume-batch.spec.ts +5 -2
- package/api/tests/queues/credit-consume.spec.ts +8 -4
- package/api/tests/routes/credit-grants.spec.ts +1 -0
- package/blocklet.yml +1 -1
- package/cloudflare/MIGRATION-CHALLENGES.md +676 -0
- package/cloudflare/MIGRATION-RUNBOOK.md +777 -0
- package/cloudflare/README.md +499 -0
- package/cloudflare/STAGING-MIGRATION-GUIDE.md +602 -0
- package/cloudflare/build.ts +151 -0
- package/cloudflare/did-connect-auth.ts +527 -0
- package/cloudflare/docs/2026-04-22-sdk-1.30.9-upgrade-retro.md +324 -0
- package/cloudflare/docs/2026-04-24-queue-ops-followup.md +218 -0
- package/cloudflare/docs/cf-queues-ops-alert-analysis.md +663 -0
- package/cloudflare/docs/cf-workers-local-dev-and-fixes.md +284 -0
- package/cloudflare/docs/cleanup-tasks-2026-05.md +62 -0
- package/cloudflare/docs/payment-kit-platform-analysis-2026-04-20.md +354 -0
- package/cloudflare/frontend-shims/buffer-polyfill.ts +9 -0
- package/cloudflare/frontend-shims/js-sdk.ts +43 -0
- package/cloudflare/frontend-shims/mime-types.ts +46 -0
- package/cloudflare/frontend-shims/session.ts +24 -0
- package/cloudflare/frontend-shims/vite-plugin-noop.ts +6 -0
- package/cloudflare/index.html +40 -0
- package/cloudflare/migrate-to-d1.js +252 -0
- package/cloudflare/migrations/0001_initial_schema.sql +82 -0
- package/cloudflare/migrations/0002_indexes.sql +75 -0
- package/cloudflare/migrations/0003_locks_and_constraints.sql +18 -0
- package/cloudflare/migrations/0004_iap_foundation.sql +72 -0
- package/cloudflare/migrations/0005_iap_tenant_backfill.sql +112 -0
- package/cloudflare/run-build.js +391 -0
- package/cloudflare/scripts/test-decrypt.js +102 -0
- package/cloudflare/shims/arcblock-ws.ts +20 -0
- package/cloudflare/shims/axios-http-adapter.ts +4 -0
- package/cloudflare/shims/axios-lite.ts +117 -0
- package/cloudflare/shims/blocklet-sdk/auth-service.ts +33 -0
- package/cloudflare/shims/blocklet-sdk/cdn.ts +3 -0
- package/cloudflare/shims/blocklet-sdk/component-api.ts +35 -0
- package/cloudflare/shims/blocklet-sdk/component.ts +18 -0
- package/cloudflare/shims/blocklet-sdk/config.ts +8 -0
- package/cloudflare/shims/blocklet-sdk/did.ts +14 -0
- package/cloudflare/shims/blocklet-sdk/env.ts +12 -0
- package/cloudflare/shims/blocklet-sdk/eventbus.ts +3 -0
- package/cloudflare/shims/blocklet-sdk/fallback.ts +3 -0
- package/cloudflare/shims/blocklet-sdk/index.ts +11 -0
- package/cloudflare/shims/blocklet-sdk/logger.ts +11 -0
- package/cloudflare/shims/blocklet-sdk/middlewares.ts +15 -0
- package/cloudflare/shims/blocklet-sdk/notification.ts +11 -0
- package/cloudflare/shims/blocklet-sdk/security.ts +53 -0
- package/cloudflare/shims/blocklet-sdk/session.ts +8 -0
- package/cloudflare/shims/blocklet-sdk/verify-session.ts +44 -0
- package/cloudflare/shims/blocklet-sdk/verify-sign.ts +38 -0
- package/cloudflare/shims/blocklet-sdk/wallet-authenticator.ts +3 -0
- package/cloudflare/shims/blocklet-sdk/wallet-handler.ts +6 -0
- package/cloudflare/shims/blocklet-sdk/wallet.ts +103 -0
- package/cloudflare/shims/cookie-parser.ts +3 -0
- package/cloudflare/shims/cors.ts +21 -0
- package/cloudflare/shims/cron.ts +189 -0
- package/cloudflare/shims/crypto-js-warn.ts +7 -0
- package/cloudflare/shims/did-space-js.ts +17 -0
- package/cloudflare/shims/did-space.ts +11 -0
- package/cloudflare/shims/error.ts +18 -0
- package/cloudflare/shims/express-compat/index.ts +80 -0
- package/cloudflare/shims/express-compat/types.ts +41 -0
- package/cloudflare/shims/fastq.ts +105 -0
- package/cloudflare/shims/lock.ts +115 -0
- package/cloudflare/shims/mime-types.ts +56 -0
- package/cloudflare/shims/nedb-storage.ts +9 -0
- package/cloudflare/shims/node-child-process.ts +9 -0
- package/cloudflare/shims/node-fs.ts +20 -0
- package/cloudflare/shims/node-http.ts +13 -0
- package/cloudflare/shims/node-https.ts +4 -0
- package/cloudflare/shims/node-misc.ts +15 -0
- package/cloudflare/shims/node-net.ts +8 -0
- package/cloudflare/shims/node-os.ts +14 -0
- package/cloudflare/shims/node-tty.ts +8 -0
- package/cloudflare/shims/node-zlib.ts +17 -0
- package/cloudflare/shims/noop.ts +26 -0
- package/cloudflare/shims/payment-vendor.ts +14 -0
- package/cloudflare/shims/querystring.ts +12 -0
- package/cloudflare/shims/queue.ts +611 -0
- package/cloudflare/shims/rolldown-runtime.ts +43 -0
- package/cloudflare/shims/sequelize-d1/datatypes.ts +24 -0
- package/cloudflare/shims/sequelize-d1/helpers.ts +46 -0
- package/cloudflare/shims/sequelize-d1/index.ts +34 -0
- package/cloudflare/shims/sequelize-d1/model.ts +1176 -0
- package/cloudflare/shims/sequelize-d1/operators.ts +306 -0
- package/cloudflare/shims/sequelize-d1/retry.ts +85 -0
- package/cloudflare/shims/sequelize-d1/sequelize-class.ts +119 -0
- package/cloudflare/shims/sequelize-d1/timing.ts +81 -0
- package/cloudflare/shims/sequelize-d1/types.ts +35 -0
- package/cloudflare/shims/stripe-cf.ts +29 -0
- package/cloudflare/shims/ws-lite.ts +103 -0
- package/cloudflare/shims/xss.ts +3 -0
- package/cloudflare/tests/shims/cron.spec.ts +210 -0
- package/cloudflare/tests/shims/queue-delayed-persist.spec.ts +87 -0
- package/cloudflare/tests/shims/queue-scheduled.spec.ts +186 -0
- package/cloudflare/vite.config.ts +162 -0
- package/cloudflare/worker.ts +1608 -0
- package/cloudflare/wrangler.json +63 -0
- package/cloudflare/wrangler.jsonc +75 -0
- package/cloudflare/wrangler.staging.json +67 -0
- package/cloudflare/wrangler.toml +28 -0
- package/jest.config.js +4 -12
- package/package.json +30 -22
- package/scripts/seed-google-play.ts +79 -0
- package/src/app.tsx +62 -4
- package/src/components/customer/link.tsx +9 -13
- package/src/components/customer/notification-preference.tsx +3 -2
- package/src/components/filter-toolbar.tsx +4 -0
- package/src/components/invoice/list.tsx +9 -1
- package/src/components/invoice-pdf/utils.ts +2 -1
- package/src/components/layout/admin.tsx +39 -5
- package/src/components/layout/user-cf.tsx +77 -0
- package/src/components/payment-intent/actions.tsx +23 -3
- package/src/components/payment-method/app-store.tsx +103 -0
- package/src/components/payment-method/form.tsx +7 -1
- package/src/components/payment-method/google-play.tsx +85 -0
- package/src/components/safe-did-address.tsx +75 -0
- package/src/components/subscription/list.tsx +20 -0
- package/src/libs/patch-user-card.ts +25 -0
- package/src/libs/util.ts +5 -7
- package/src/locales/en.tsx +63 -0
- package/src/locales/zh.tsx +63 -0
- package/src/pages/admin/billing/meter-events/index.tsx +4 -0
- package/src/pages/admin/billing/subscriptions/detail.tsx +80 -0
- package/src/pages/admin/customers/customers/detail.tsx +8 -2
- package/src/pages/admin/customers/customers/index.tsx +2 -2
- package/src/pages/admin/overview.tsx +3 -1
- package/src/pages/admin/settings/payment-methods/create.tsx +12 -0
- package/src/pages/admin/settings/payment-methods/index.tsx +1 -1
- package/src/pages/customer/subscription/detail.tsx +4 -4
- package/tsconfig.api.json +1 -6
- package/tsconfig.json +3 -4
- package/tsconfig.types.json +2 -1
- package/vite.config.ts +6 -1
|
@@ -6,6 +6,7 @@ import { encodeTransferItx } from '../../integrations/ethereum/token';
|
|
|
6
6
|
import { executeEvmTransaction, waitForEvmTxConfirm } from '../../integrations/ethereum/tx';
|
|
7
7
|
import { CallbackArgs, ethWallet } from '../../libs/auth';
|
|
8
8
|
import logger from '../../libs/logger';
|
|
9
|
+
import { parseChainError } from '../../libs/chain-error';
|
|
9
10
|
import { getGasPayerExtra } from '../../libs/payment';
|
|
10
11
|
import { createPaymentOutput } from '../../libs/session';
|
|
11
12
|
import { getTxMetadata } from '../../libs/util';
|
|
@@ -130,6 +131,9 @@ export default {
|
|
|
130
131
|
const client = paymentMethod.getOcapClient();
|
|
131
132
|
const claim = claims.find((x) => x.type === 'prepareTx');
|
|
132
133
|
|
|
134
|
+
// Warm up chain context cache to avoid setTimeout(resolve,0) hang in workerd
|
|
135
|
+
await client.getContext();
|
|
136
|
+
|
|
133
137
|
const tx: Partial<Transaction> = client.decodeTx(claim.finalTx);
|
|
134
138
|
if (claim.delegator && claim.from) {
|
|
135
139
|
tx.delegator = claim.delegator;
|
|
@@ -227,10 +231,15 @@ export default {
|
|
|
227
231
|
|
|
228
232
|
return { hash: txHash };
|
|
229
233
|
} catch (err) {
|
|
230
|
-
|
|
231
|
-
logger.error('Failed to finalize paymentIntent on arcblock', {
|
|
234
|
+
const parsed = parseChainError(err);
|
|
235
|
+
logger.error('Failed to finalize paymentIntent on arcblock', {
|
|
236
|
+
paymentIntentId: paymentIntent.id,
|
|
237
|
+
code: parsed.code,
|
|
238
|
+
details: parsed.details,
|
|
239
|
+
raw: err,
|
|
240
|
+
});
|
|
232
241
|
await paymentIntent.update({ status: 'requires_capture' });
|
|
233
|
-
throw
|
|
242
|
+
throw parsed;
|
|
234
243
|
}
|
|
235
244
|
}
|
|
236
245
|
|
|
@@ -101,6 +101,8 @@ export default {
|
|
|
101
101
|
billingThreshold: Math.max(minStakeAmount, billingThreshold),
|
|
102
102
|
items,
|
|
103
103
|
slippageConfig: subscription?.slippage_config || undefined,
|
|
104
|
+
// Reuse DelegateState fetched above — avoids a duplicate chain RPC on CF cold isolates
|
|
105
|
+
delegateState: delegation.state,
|
|
104
106
|
}),
|
|
105
107
|
});
|
|
106
108
|
}
|
|
@@ -217,7 +219,7 @@ export default {
|
|
|
217
219
|
|
|
218
220
|
await checkoutSession.update({ status: 'complete', payment_status: 'paid' });
|
|
219
221
|
if (invoice) {
|
|
220
|
-
invoiceQueue.pushAndWait({ id: invoice.id, job: { invoiceId: invoice.id, retryOnError: false } });
|
|
222
|
+
await invoiceQueue.pushAndWait({ id: invoice.id, job: { invoiceId: invoice.id, retryOnError: false } });
|
|
221
223
|
}
|
|
222
224
|
await addSubscriptionJob(subscription, 'cycle', false, subscription.trial_end);
|
|
223
225
|
logger.info('CheckoutSession updated on setup done', {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/indent */
|
|
2
2
|
/* eslint-disable prettier/prettier */
|
|
3
3
|
import { toTypeInfo } from '@arcblock/did';
|
|
4
|
-
import { toDelegateAddress } from '@arcblock/did-util';
|
|
5
|
-
import type { Transaction } from '@ocap/client';
|
|
4
|
+
import { toDelegateAddress } from '@arcblock/did-util/cbor';
|
|
5
|
+
import type { DelegateState as DelegateStateType, 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
|
|
9
|
+
import isEmpty from 'lodash/isEmpty';
|
|
10
10
|
import dayjs from '../../libs/dayjs';
|
|
11
11
|
import { estimateMaxGasForTx, hasStakedForGas } from '../../integrations/arcblock/stake';
|
|
12
12
|
import { encodeApproveItx } from '../../integrations/ethereum/token';
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
} from '../../libs/subscription';
|
|
32
32
|
import { getCustomerStakeAddress, OCAP_PAYMENT_TX_TYPE } from '../../libs/util';
|
|
33
33
|
|
|
34
|
+
// eslint-disable-next-line import/no-cycle
|
|
34
35
|
import { invoiceQueue } from '../../queues/invoice';
|
|
35
36
|
import { AutoRechargeConfig, type TLineItemExpanded } from '../../store/models';
|
|
36
37
|
import { CheckoutSession } from '../../store/models/checkout-session';
|
|
@@ -453,8 +454,6 @@ export async function ensureInvoiceForCheckout({
|
|
|
453
454
|
}
|
|
454
455
|
}
|
|
455
456
|
|
|
456
|
-
const currency = await PaymentCurrency.findByPk(checkoutSession.currency_id);
|
|
457
|
-
|
|
458
457
|
const metadata = {
|
|
459
458
|
...(checkoutSession.invoice_creation?.invoice_data?.metadata || {}),
|
|
460
459
|
};
|
|
@@ -467,7 +466,13 @@ export async function ensureInvoiceForCheckout({
|
|
|
467
466
|
const trialInDays = Number(checkoutSession.subscription_data?.trial_period_days || 0);
|
|
468
467
|
const trialEnd = Number(checkoutSession.subscription_data?.trial_end || 0);
|
|
469
468
|
const now = dayjs().unix();
|
|
470
|
-
|
|
469
|
+
|
|
470
|
+
// Parallel: fetch currency + expand line items (independent queries)
|
|
471
|
+
const [currency, expandedItems] = await Promise.all([
|
|
472
|
+
PaymentCurrency.findByPk(checkoutSession.currency_id),
|
|
473
|
+
lineItems ? Promise.resolve(lineItems) : Price.expand(checkoutSession.line_items, { product: true, upsell: true }),
|
|
474
|
+
]);
|
|
475
|
+
let invoiceItems = expandedItems;
|
|
471
476
|
|
|
472
477
|
// For items with quote_id, fetch full quote info and attach to line item metadata
|
|
473
478
|
// This ensures invoice line items have complete quote info for display
|
|
@@ -610,33 +615,21 @@ export async function ensureInvoiceForCheckout({
|
|
|
610
615
|
discountCount: discountInfo.appliedDiscounts.length,
|
|
611
616
|
});
|
|
612
617
|
|
|
613
|
-
//
|
|
614
|
-
if (!isGroupInvoice) {
|
|
615
|
-
await checkoutSession.update({ invoice_id: invoice.id });
|
|
616
|
-
}
|
|
617
|
-
if (paymentIntent) {
|
|
618
|
-
await paymentIntent.update({ invoice_id: invoice.id });
|
|
619
|
-
}
|
|
620
|
-
if (subscription) {
|
|
621
|
-
await subscription.update({ latest_invoice_id: invoice.id });
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
// Update quotes with invoice_id for audit trail
|
|
625
|
-
// Find quote_ids from invoice items and update the quotes
|
|
618
|
+
// Update invoice_id references in parallel — these are independent writes
|
|
626
619
|
const quoteIdsFromItems = items
|
|
627
620
|
.map((item) => item.metadata?.quote_id)
|
|
628
621
|
.filter((id): id is string => typeof id === 'string' && id.length > 0);
|
|
629
622
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
623
|
+
await Promise.all([
|
|
624
|
+
!isGroupInvoice ? checkoutSession.update({ invoice_id: invoice.id }) : Promise.resolve(),
|
|
625
|
+
paymentIntent ? paymentIntent.update({ invoice_id: invoice.id }) : Promise.resolve(),
|
|
626
|
+
subscription ? subscription.update({ latest_invoice_id: invoice.id }) : Promise.resolve(),
|
|
627
|
+
quoteIdsFromItems.length > 0
|
|
628
|
+
? PriceQuote.update({ invoice_id: invoice.id }, { where: { id: quoteIdsFromItems } }).then(() => {
|
|
629
|
+
logger.info('Updated quotes with invoice_id', { invoiceId: invoice.id, quoteIds: quoteIdsFromItems });
|
|
630
|
+
})
|
|
631
|
+
: Promise.resolve(),
|
|
632
|
+
]);
|
|
640
633
|
|
|
641
634
|
return { invoice, items };
|
|
642
635
|
}
|
|
@@ -861,6 +854,7 @@ export async function getDelegationTxClaim({
|
|
|
861
854
|
billingThreshold = 0,
|
|
862
855
|
requiredStake = true,
|
|
863
856
|
slippageConfig,
|
|
857
|
+
delegateState,
|
|
864
858
|
}: {
|
|
865
859
|
userDid: string;
|
|
866
860
|
userPk: string;
|
|
@@ -880,6 +874,10 @@ export async function getDelegationTxClaim({
|
|
|
880
874
|
min_acceptable_rate?: string;
|
|
881
875
|
base_currency?: string;
|
|
882
876
|
};
|
|
877
|
+
// Optional DelegateState passthrough — avoids a duplicate chain RPC in getTokenLimitsForDelegation
|
|
878
|
+
// when the caller already fetched it (e.g. via isDelegationSufficientForPayment). Same-request
|
|
879
|
+
// usage only — do not cache across requests (see delegationCache note in libs/payment.ts).
|
|
880
|
+
delegateState?: DelegateStateType | null;
|
|
883
881
|
}) {
|
|
884
882
|
// Calculate authorization amount
|
|
885
883
|
// When slippage_config with min_acceptable_rate is provided, use it for precise calculation
|
|
@@ -902,16 +900,23 @@ export async function getDelegationTxClaim({
|
|
|
902
900
|
}
|
|
903
901
|
|
|
904
902
|
const address = toDelegateAddress(userDid, wallet.address);
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
903
|
+
// These two helpers each issue 1–2 chain RPCs and don't depend on each other,
|
|
904
|
+
// so fan them out — on CF Workers cold isolates the serial path easily pushed the
|
|
905
|
+
// request over the runtime CPU/real-time budget and tripped a "hung promise" 500
|
|
906
|
+
// (see #1351). Parallelising halves the wall-clock for the delegation tx claim.
|
|
907
|
+
const [tokenLimits, tokenRequirementsResult] = await Promise.all([
|
|
908
|
+
getTokenLimitsForDelegation(items, paymentMethod, paymentCurrency, address, amount, delegateState),
|
|
909
|
+
getTokenRequirements({
|
|
910
|
+
items,
|
|
911
|
+
mode,
|
|
912
|
+
trialing,
|
|
913
|
+
billingThreshold,
|
|
914
|
+
paymentMethod,
|
|
915
|
+
paymentCurrency,
|
|
916
|
+
requiredStake,
|
|
917
|
+
}),
|
|
918
|
+
]);
|
|
919
|
+
let tokenRequirements = tokenRequirementsResult;
|
|
915
920
|
|
|
916
921
|
if (mode === 'delegation') {
|
|
917
922
|
tokenRequirements = [];
|
|
@@ -1049,6 +1054,9 @@ export async function getStakeTxClaim({
|
|
|
1049
1054
|
if (paymentMethod.type === 'arcblock') {
|
|
1050
1055
|
// create staking data
|
|
1051
1056
|
const client = paymentMethod.getOcapClient();
|
|
1057
|
+
// CF Workers: warm up chain context to avoid setTimeout(resolve,0) hang on cold client.
|
|
1058
|
+
// Same pattern as pay.ts; needed because did-connect-js tryWithTimeout bounds this callback at 10s.
|
|
1059
|
+
await client.getContext();
|
|
1052
1060
|
|
|
1053
1061
|
const stakeId = hasGrouping ? `stake-group-${subscription.id}` : subscription.id;
|
|
1054
1062
|
|
|
@@ -1155,6 +1163,8 @@ export async function getOverdraftProtectionStakeTxClaim({
|
|
|
1155
1163
|
if (paymentMethod.type === 'arcblock') {
|
|
1156
1164
|
// create staking data
|
|
1157
1165
|
const client = paymentMethod.getOcapClient();
|
|
1166
|
+
// CF Workers: warm up chain context to avoid setTimeout(resolve,0) hang on cold client.
|
|
1167
|
+
await client.getContext();
|
|
1158
1168
|
const nonce = `overdraft-protection-${subscription.id}`;
|
|
1159
1169
|
const address = await getCustomerStakeAddress(userDid, nonce);
|
|
1160
1170
|
const { state } = await client.getStakeState({ address });
|
|
@@ -1579,6 +1589,9 @@ export async function executeOcapTransactions(
|
|
|
1579
1589
|
nonce?: string
|
|
1580
1590
|
) {
|
|
1581
1591
|
const client = paymentMethod.getOcapClient();
|
|
1592
|
+
// CF Workers: warm up chain context to avoid setTimeout(resolve,0) hang on cold client.
|
|
1593
|
+
// Each subsequent executeSingleTransaction (Delegate + Stake broadcast) needs ready context.
|
|
1594
|
+
await client.getContext();
|
|
1582
1595
|
logger.info('start executeOcapTransactions', { userDid, claims });
|
|
1583
1596
|
|
|
1584
1597
|
const delegation = claims.find((x) => x.type === 'signature' && x.meta?.purpose === 'delegation');
|
|
@@ -133,6 +133,8 @@ export default {
|
|
|
133
133
|
items,
|
|
134
134
|
requiredStake,
|
|
135
135
|
slippageConfig: primarySubscription?.slippage_config || undefined,
|
|
136
|
+
// Reuse DelegateState fetched above — avoids a duplicate chain RPC on CF cold isolates
|
|
137
|
+
delegateState: delegation.state,
|
|
136
138
|
}),
|
|
137
139
|
});
|
|
138
140
|
}
|
|
@@ -243,7 +245,8 @@ export default {
|
|
|
243
245
|
|
|
244
246
|
for (const invoice of invoices) {
|
|
245
247
|
if (invoice) {
|
|
246
|
-
|
|
248
|
+
// eslint-disable-next-line no-await-in-loop
|
|
249
|
+
await invoiceQueue.pushAndWait({ id: invoice.id, job: { invoiceId: invoice.id, retryOnError: false } });
|
|
247
250
|
}
|
|
248
251
|
}
|
|
249
252
|
|
|
@@ -27,6 +27,7 @@ import { blocklet } from '../libs/auth';
|
|
|
27
27
|
import { formatMetadata } from '../libs/util';
|
|
28
28
|
import { getPriceUintAmountByCurrency } from '../libs/price';
|
|
29
29
|
import { checkTokenBalance } from '../libs/payment';
|
|
30
|
+
import { measurePhase } from '../libs/timing';
|
|
30
31
|
import { getExchangeRateService } from '../libs/exchange-rate/service';
|
|
31
32
|
import { getExchangeRateSymbol, hasTokenAddress } from '../libs/exchange-rate/token-address-mapping';
|
|
32
33
|
import { isRateBelowMinAcceptableRate } from '../libs/slippage';
|
|
@@ -290,16 +291,19 @@ router.get('/verify-availability', authMine, async (req, res) => {
|
|
|
290
291
|
const { customer_id: customerId, currency_id: currencyId } = value;
|
|
291
292
|
let pendingAmount = value.pending_amount;
|
|
292
293
|
|
|
293
|
-
|
|
294
|
+
// Parallelize Customer + PaymentCurrency lookups (independent, saves ~85ms RTT)
|
|
295
|
+
const [customer, currency] = await Promise.all([
|
|
296
|
+
Customer.findByPkOrDid(customerId),
|
|
297
|
+
PaymentCurrency.findByPk(currencyId),
|
|
298
|
+
]);
|
|
294
299
|
if (!customer) {
|
|
295
300
|
return res.status(404).json({ error: `Customer ${customerId} not found` });
|
|
296
301
|
}
|
|
297
|
-
|
|
298
|
-
const currency = await PaymentCurrency.findByPk(currencyId);
|
|
299
302
|
if (!currency) {
|
|
300
303
|
return res.status(404).json({ error: `PaymentCurrency ${currencyId} not found` });
|
|
301
304
|
}
|
|
302
305
|
|
|
306
|
+
// AutoRechargeConfig needs customer.id which we now have
|
|
303
307
|
const config = (await AutoRechargeConfig.findOne({
|
|
304
308
|
where: {
|
|
305
309
|
customer_id: customer.id,
|
|
@@ -511,13 +515,15 @@ router.get('/verify-availability', authMine, async (req, res) => {
|
|
|
511
515
|
// This ensures user can pay off pending amount and still have balance for continued usage
|
|
512
516
|
// minimumRequiredBalance = requiredPaymentAmount (to pay off pending) + totalAmount (for one more recharge)
|
|
513
517
|
const minimumRequiredBalance = requiredPaymentAmount.add(totalAmount);
|
|
514
|
-
balanceResult = await
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
518
|
+
balanceResult = await measurePhase('chain', () =>
|
|
519
|
+
checkTokenBalance({
|
|
520
|
+
paymentMethod: config.paymentMethod!,
|
|
521
|
+
paymentCurrency: config.rechargeCurrency!,
|
|
522
|
+
userDid: payer,
|
|
523
|
+
amount: minimumRequiredBalance.toString(),
|
|
524
|
+
skipUserCheck: true,
|
|
525
|
+
})
|
|
526
|
+
);
|
|
521
527
|
|
|
522
528
|
if (!balanceResult.sufficient) {
|
|
523
529
|
return res.json({
|
|
@@ -531,13 +537,15 @@ router.get('/verify-availability', authMine, async (req, res) => {
|
|
|
531
537
|
}
|
|
532
538
|
} else {
|
|
533
539
|
// No pending amount: check if balance can cover at least one recharge
|
|
534
|
-
balanceResult = await
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
540
|
+
balanceResult = await measurePhase('chain', () =>
|
|
541
|
+
checkTokenBalance({
|
|
542
|
+
paymentMethod: config.paymentMethod!,
|
|
543
|
+
paymentCurrency: config.rechargeCurrency!,
|
|
544
|
+
userDid: payer,
|
|
545
|
+
amount: totalAmount.toString(),
|
|
546
|
+
skipUserCheck: true,
|
|
547
|
+
})
|
|
548
|
+
);
|
|
541
549
|
|
|
542
550
|
if (!balanceResult.sufficient) {
|
|
543
551
|
return res.json({
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import Joi from 'joi';
|
|
2
2
|
import { Router } from 'express';
|
|
3
3
|
|
|
4
4
|
import { BN } from '@ocap/util';
|
|
@@ -24,7 +24,7 @@ const donationSchema = Joi.object<DonationSettings>({
|
|
|
24
24
|
beneficiaries: Joi.array()
|
|
25
25
|
.items(
|
|
26
26
|
Joi.object({
|
|
27
|
-
address: Joi.
|
|
27
|
+
address: Joi.string().required(),
|
|
28
28
|
share: Joi.number().positive().required(),
|
|
29
29
|
memo: Joi.string().max(64).optional(),
|
|
30
30
|
})
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// Cross-channel entitlement query API.
|
|
2
|
+
//
|
|
3
|
+
// Endpoints:
|
|
4
|
+
// GET /api/entitlements/check ?customer_did=&product_id=[&livemode=]
|
|
5
|
+
// GET /api/entitlements/list ?customer_did=[&livemode=]
|
|
6
|
+
//
|
|
7
|
+
// Auth model:
|
|
8
|
+
// - Component-to-component calls (other blocklets via @blocklet/payment-client):
|
|
9
|
+
// trusted via the component signature; `roles: ['owner','admin']` is the gate.
|
|
10
|
+
// - Logged-in end users (mobile demo, web SPA): `mine: true` lets them in iff
|
|
11
|
+
// their DID matches the query's customer_did — enforced in the handler.
|
|
12
|
+
|
|
13
|
+
import { Router } from 'express';
|
|
14
|
+
import Joi from 'joi';
|
|
15
|
+
|
|
16
|
+
import { checkEntitlement, listEntitlements } from '../libs/entitlement';
|
|
17
|
+
import logger from '../libs/logger';
|
|
18
|
+
import { authenticate } from '../libs/security';
|
|
19
|
+
import { PaymentMethod } from '../store/models';
|
|
20
|
+
|
|
21
|
+
const router = Router();
|
|
22
|
+
// component+owner/admin for cross-blocklet calls; ensureLogin for end users —
|
|
23
|
+
// handler then enforces that non-admin users can only query their own DID.
|
|
24
|
+
const auth = authenticate<PaymentMethod>({ component: true, roles: ['owner', 'admin'], ensureLogin: true });
|
|
25
|
+
|
|
26
|
+
function isAdminUser(role?: string): boolean {
|
|
27
|
+
return role === 'owner' || role === 'admin';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Strip the `did:abt:` prefix if present so callers can compare DIDs across
|
|
31
|
+
// the two shapes that show up in the codebase (bare base58 vs canonical).
|
|
32
|
+
// The CF Workers AUTH_SERVICE returns bare addresses; clients send canonical.
|
|
33
|
+
function canonicalDid(did: string | undefined | null): string {
|
|
34
|
+
if (!did) return '';
|
|
35
|
+
return did.startsWith('did:abt:') ? did.slice('did:abt:'.length) : did;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isSelf(req: any, customerDid: string): boolean {
|
|
39
|
+
const a = canonicalDid(req.user?.did);
|
|
40
|
+
const b = canonicalDid(customerDid);
|
|
41
|
+
return !!a && a === b;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const checkQuerySchema = Joi.object<{
|
|
45
|
+
customer_did: string;
|
|
46
|
+
product_id: string;
|
|
47
|
+
livemode?: string | boolean;
|
|
48
|
+
}>({
|
|
49
|
+
customer_did: Joi.string().required(),
|
|
50
|
+
product_id: Joi.string().required(),
|
|
51
|
+
livemode: Joi.alternatives(Joi.boolean(), Joi.string().valid('true', 'false')),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const listQuerySchema = Joi.object<{
|
|
55
|
+
customer_did: string;
|
|
56
|
+
livemode?: string | boolean;
|
|
57
|
+
}>({
|
|
58
|
+
customer_did: Joi.string().required(),
|
|
59
|
+
livemode: Joi.alternatives(Joi.boolean(), Joi.string().valid('true', 'false')),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
function parseLivemode(value: string | boolean | undefined, fallback: boolean): boolean {
|
|
63
|
+
if (typeof value === 'boolean') return value;
|
|
64
|
+
if (value === 'true') return true;
|
|
65
|
+
if (value === 'false') return false;
|
|
66
|
+
return fallback;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
router.get('/check', auth, async (req, res) => {
|
|
70
|
+
try {
|
|
71
|
+
const input = await checkQuerySchema.validateAsync(req.query, { stripUnknown: true });
|
|
72
|
+
if (!isAdminUser((req as any).user?.role) && !isSelf(req, input.customer_did)) {
|
|
73
|
+
res.status(403).json({ error: 'Cannot query entitlements for other customers' });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const livemode = parseLivemode(input.livemode, !!req.livemode);
|
|
77
|
+
const result = await checkEntitlement({
|
|
78
|
+
customer_did: input.customer_did,
|
|
79
|
+
product_id: input.product_id,
|
|
80
|
+
livemode,
|
|
81
|
+
});
|
|
82
|
+
res.json(result);
|
|
83
|
+
} catch (err: any) {
|
|
84
|
+
logger.error('entitlements/check failed', { error: err?.message, stack: err?.stack });
|
|
85
|
+
res.status(400).json({ error: err?.message ?? 'check failed' });
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
router.get('/list', auth, async (req, res) => {
|
|
90
|
+
try {
|
|
91
|
+
const input = await listQuerySchema.validateAsync(req.query, { stripUnknown: true });
|
|
92
|
+
if (!isAdminUser((req as any).user?.role) && !isSelf(req, input.customer_did)) {
|
|
93
|
+
res.status(403).json({ error: 'Cannot list entitlements for other customers' });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const livemode = parseLivemode(input.livemode, !!req.livemode);
|
|
97
|
+
const list = await listEntitlements({ customer_did: input.customer_did, livemode });
|
|
98
|
+
res.json({ list });
|
|
99
|
+
} catch (err: any) {
|
|
100
|
+
logger.error('entitlements/list failed', { error: err?.message, stack: err?.stack });
|
|
101
|
+
res.status(400).json({ error: err?.message ?? 'list failed' });
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
export default router;
|
package/api/src/routes/events.ts
CHANGED
|
@@ -186,7 +186,7 @@ router.get('/retry-webhooks', auth, async (req, res) => {
|
|
|
186
186
|
// eslint-disable-next-line no-restricted-syntax
|
|
187
187
|
for (const webhook of eventWebhooks) {
|
|
188
188
|
// eslint-disable-next-line no-await-in-loop
|
|
189
|
-
const added = await addWebhookJob(event.id, webhook.id, { persist:
|
|
189
|
+
const added = await addWebhookJob(event.id, webhook.id, { persist: true });
|
|
190
190
|
if (added) {
|
|
191
191
|
scheduled += 1;
|
|
192
192
|
}
|
|
@@ -255,7 +255,7 @@ router.post('/:id/retry-webhooks', auth, async (req, res) => {
|
|
|
255
255
|
// eslint-disable-next-line no-restricted-syntax
|
|
256
256
|
for (const webhook of eventWebhooks) {
|
|
257
257
|
// eslint-disable-next-line no-await-in-loop
|
|
258
|
-
const added = await addWebhookJob(event.id, webhook.id, { persist:
|
|
258
|
+
const added = await addWebhookJob(event.id, webhook.id, { persist: true });
|
|
259
259
|
if (added) {
|
|
260
260
|
scheduled += 1;
|
|
261
261
|
logger.info('Manually scheduled webhook retry', { eventId: event.id, webhookId: webhook.id });
|
package/api/src/routes/index.ts
CHANGED
|
@@ -10,6 +10,9 @@ import creditTransactions from './credit-transactions';
|
|
|
10
10
|
import customers from './customers';
|
|
11
11
|
import donations from './donations';
|
|
12
12
|
import events from './events';
|
|
13
|
+
import entitlements from './entitlements';
|
|
14
|
+
import appStore from './integrations/app-store';
|
|
15
|
+
import googlePlay from './integrations/google-play';
|
|
13
16
|
import stripe from './integrations/stripe';
|
|
14
17
|
import invoices from './invoices';
|
|
15
18
|
import meterEvents from './meter-events';
|
|
@@ -49,8 +52,12 @@ router.use((req, _, next) => {
|
|
|
49
52
|
} catch {
|
|
50
53
|
req.livemode = true;
|
|
51
54
|
}
|
|
52
|
-
} else {
|
|
53
|
-
req.livemode
|
|
55
|
+
} else if (typeof req.livemode !== 'boolean') {
|
|
56
|
+
// CF Workers' createExpressReq pre-populates req.livemode from the worker's
|
|
57
|
+
// PAYMENT_LIVEMODE env var; honor that when set. Express dev has no
|
|
58
|
+
// upstream, so we fall back to the env var ourselves — defaulting to
|
|
59
|
+
// livemode=true unless explicitly disabled via PAYMENT_LIVEMODE=false.
|
|
60
|
+
req.livemode = process.env.PAYMENT_LIVEMODE !== 'false';
|
|
54
61
|
}
|
|
55
62
|
|
|
56
63
|
next();
|
|
@@ -101,6 +108,9 @@ router.use('/donations', loadBaseCurrency, donations);
|
|
|
101
108
|
router.use('/events', events);
|
|
102
109
|
router.use('/invoices', invoices);
|
|
103
110
|
router.use('/integrations/stripe', stripe);
|
|
111
|
+
router.use('/integrations/google-play', googlePlay);
|
|
112
|
+
router.use('/integrations/app-store', appStore);
|
|
113
|
+
router.use('/entitlements', entitlements);
|
|
104
114
|
router.use('/meter-events', meterEvents);
|
|
105
115
|
router.use('/meters', meters);
|
|
106
116
|
router.use('/passports', passports);
|