payment-kit 1.21.16 → 1.22.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/api/src/index.ts +3 -1
- package/api/src/integrations/blocklet/user.ts +2 -2
- package/api/src/integrations/ethereum/token.ts +4 -5
- package/api/src/integrations/stripe/handlers/invoice.ts +31 -26
- package/api/src/integrations/stripe/handlers/payment-intent.ts +1 -1
- package/api/src/integrations/stripe/handlers/setup-intent.ts +231 -0
- package/api/src/integrations/stripe/handlers/subscription.ts +31 -9
- package/api/src/integrations/stripe/resource.ts +30 -1
- package/api/src/integrations/stripe/setup.ts +1 -1
- package/api/src/libs/auth.ts +7 -6
- package/api/src/libs/env.ts +1 -1
- package/api/src/libs/notification/template/subscription-trial-will-end.ts +1 -0
- package/api/src/libs/notification/template/subscription-will-renew.ts +1 -1
- package/api/src/libs/payment.ts +11 -6
- package/api/src/libs/refund.ts +1 -1
- package/api/src/libs/remote-signer.ts +93 -0
- package/api/src/libs/security.ts +1 -1
- package/api/src/libs/subscription.ts +4 -7
- package/api/src/libs/util.ts +18 -1
- package/api/src/libs/vendor-util/adapters/didnames-adapter.ts +17 -9
- package/api/src/libs/vendor-util/adapters/launcher-adapter.ts +11 -6
- package/api/src/queues/payment.ts +2 -2
- package/api/src/queues/payout.ts +1 -1
- package/api/src/queues/refund.ts +2 -2
- package/api/src/queues/subscription.ts +1 -1
- package/api/src/queues/usage-record.ts +1 -1
- package/api/src/queues/vendors/status-check.ts +1 -1
- package/api/src/queues/webhook.ts +1 -1
- package/api/src/routes/auto-recharge-configs.ts +1 -1
- package/api/src/routes/checkout-sessions.ts +4 -6
- package/api/src/routes/connect/change-payer.ts +148 -0
- package/api/src/routes/connect/collect-batch.ts +1 -1
- package/api/src/routes/connect/collect.ts +1 -1
- package/api/src/routes/connect/pay.ts +1 -1
- package/api/src/routes/connect/recharge-account.ts +1 -1
- package/api/src/routes/connect/recharge.ts +1 -1
- package/api/src/routes/connect/shared.ts +62 -23
- package/api/src/routes/customers.ts +1 -1
- package/api/src/routes/integrations/stripe.ts +1 -1
- package/api/src/routes/invoices.ts +141 -2
- package/api/src/routes/meter-events.ts +9 -12
- package/api/src/routes/payment-currencies.ts +1 -1
- package/api/src/routes/payment-intents.ts +2 -2
- package/api/src/routes/payment-links.ts +2 -1
- package/api/src/routes/payouts.ts +1 -1
- package/api/src/routes/products.ts +1 -0
- package/api/src/routes/subscriptions.ts +130 -3
- package/api/src/store/models/types.ts +1 -1
- package/api/tests/setup.ts +11 -0
- package/api/third.d.ts +0 -2
- package/blocklet.yml +1 -1
- package/jest.config.js +2 -2
- package/package.json +26 -26
- package/src/components/invoice/table.tsx +2 -2
- package/src/components/invoice-pdf/template.tsx +30 -0
- package/src/components/subscription/payment-method-info.tsx +222 -0
- package/src/global.css +4 -0
- package/src/libs/util.ts +1 -1
- package/src/locales/en.tsx +13 -0
- package/src/locales/zh.tsx +13 -0
- package/src/pages/admin/billing/invoices/detail.tsx +5 -3
- package/src/pages/admin/billing/subscriptions/detail.tsx +16 -0
- package/src/pages/admin/overview.tsx +14 -14
- package/src/pages/customer/invoice/detail.tsx +59 -17
- package/src/pages/customer/subscription/detail.tsx +21 -2
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { ethers, JsonRpcProvider, TransactionRequest } from 'ethers';
|
|
2
|
+
import type { WalletObject } from '@ocap/wallet';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Remote Signer that uses remote signing service for Ethereum transactions
|
|
6
|
+
* Implements required methods from AbstractSigner
|
|
7
|
+
*/
|
|
8
|
+
export class RemoteSigner extends ethers.AbstractSigner {
|
|
9
|
+
private wallet: WalletObject;
|
|
10
|
+
|
|
11
|
+
constructor(wallet: WalletObject, provider?: JsonRpcProvider) {
|
|
12
|
+
super(provider);
|
|
13
|
+
this.wallet = wallet;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Returns the address of the signer
|
|
18
|
+
* Required by AbstractSigner
|
|
19
|
+
*/
|
|
20
|
+
getAddress(): Promise<string> {
|
|
21
|
+
return Promise.resolve(this.wallet.address);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Signs a transaction and returns the signed serialized transaction
|
|
26
|
+
* Required by AbstractSigner
|
|
27
|
+
*/
|
|
28
|
+
async signTransaction(transaction: TransactionRequest): Promise<string> {
|
|
29
|
+
// Populate transaction fields (nonce, gasLimit, chainId, etc.)
|
|
30
|
+
const populatedTx = await this.populateTransaction(transaction);
|
|
31
|
+
|
|
32
|
+
// Remove 'from' field as it's not allowed in unsigned transactions
|
|
33
|
+
// The 'from' address is derived from the signature
|
|
34
|
+
const { from, ...txWithoutFrom } = populatedTx;
|
|
35
|
+
|
|
36
|
+
// Serialize unsigned transaction
|
|
37
|
+
const unsignedTx = ethers.Transaction.from(txWithoutFrom);
|
|
38
|
+
const serialized = unsignedTx.unsignedSerialized;
|
|
39
|
+
|
|
40
|
+
// Hash and sign via remote service using signETH (Ethereum-specific signing)
|
|
41
|
+
const hash = ethers.keccak256(serialized);
|
|
42
|
+
const signature = await this.wallet.signETH(hash, false); // hashBeforeSign=false because we already hashed
|
|
43
|
+
|
|
44
|
+
// Attach signature and return signed transaction
|
|
45
|
+
unsignedTx.signature = ethers.Signature.from(signature);
|
|
46
|
+
return unsignedTx.serialized;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Signs a message and returns the signature
|
|
51
|
+
* Required for message signing operations
|
|
52
|
+
*/
|
|
53
|
+
async signMessage(message: string | Uint8Array): Promise<string> {
|
|
54
|
+
// Convert message to bytes if it's a string
|
|
55
|
+
const messageBytes = typeof message === 'string' ? ethers.toUtf8Bytes(message) : message;
|
|
56
|
+
|
|
57
|
+
// Calculate the Ethereum signed message hash
|
|
58
|
+
const messageHash = ethers.hashMessage(messageBytes);
|
|
59
|
+
|
|
60
|
+
// Sign via remote service using signETH (Ethereum-specific signing)
|
|
61
|
+
const signature = await this.wallet.signETH(messageHash, false); // hashBeforeSign=false because hashMessage already hashed
|
|
62
|
+
|
|
63
|
+
// Return signature as hex string
|
|
64
|
+
return ethers.hexlify(signature);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Signs typed data (EIP-712)
|
|
69
|
+
* Optional but useful for signing structured data
|
|
70
|
+
*/
|
|
71
|
+
async signTypedData(
|
|
72
|
+
domain: ethers.TypedDataDomain,
|
|
73
|
+
types: Record<string, ethers.TypedDataField[]>,
|
|
74
|
+
value: Record<string, any>
|
|
75
|
+
): Promise<string> {
|
|
76
|
+
// Calculate the EIP-712 hash
|
|
77
|
+
const hash = ethers.TypedDataEncoder.hash(domain, types, value);
|
|
78
|
+
|
|
79
|
+
// Sign via remote service using signETH (Ethereum-specific signing)
|
|
80
|
+
const signature = await this.wallet.signETH(hash, false); // hashBeforeSign=false because hash is already computed
|
|
81
|
+
|
|
82
|
+
// Return signature as hex string
|
|
83
|
+
return ethers.hexlify(signature);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Connects the signer to a new provider
|
|
88
|
+
* Required by AbstractSigner
|
|
89
|
+
*/
|
|
90
|
+
connect(provider: JsonRpcProvider): RemoteSigner {
|
|
91
|
+
return new RemoteSigner(this.wallet, provider);
|
|
92
|
+
}
|
|
93
|
+
}
|
package/api/src/libs/security.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { auth } from '@blocklet/sdk/lib/middlewares';
|
|
2
2
|
import { getVerifyData, verify } from '@blocklet/sdk/lib/util/verify-sign';
|
|
3
|
-
import getWallet from '@blocklet/sdk/lib/wallet';
|
|
3
|
+
import { getWallet } from '@blocklet/sdk/lib/wallet';
|
|
4
4
|
import type { NextFunction, Request, Response } from 'express';
|
|
5
5
|
import type { Model } from 'sequelize';
|
|
6
6
|
|
|
@@ -1043,22 +1043,19 @@ export async function getSubscriptionStakeAmountSetup(
|
|
|
1043
1043
|
logger.info('getSubscriptionStakeAmountSetup failed, no inputs', { txHash, info });
|
|
1044
1044
|
return null;
|
|
1045
1045
|
}
|
|
1046
|
-
const amountRes: { [key: string]:
|
|
1046
|
+
const amountRes: { [key: string]: string } = {};
|
|
1047
1047
|
// calculate stake amount for each address
|
|
1048
1048
|
inputs.forEach((input: any) => {
|
|
1049
1049
|
const { tokens } = input;
|
|
1050
1050
|
tokens.forEach((token: any) => {
|
|
1051
1051
|
const { address, value } = token;
|
|
1052
1052
|
if (amountRes[address]) {
|
|
1053
|
-
amountRes[address] = amountRes[address].add(new BN(value));
|
|
1053
|
+
amountRes[address] = new BN(amountRes[address]).add(new BN(value)).toString();
|
|
1054
1054
|
} else {
|
|
1055
|
-
amountRes[address] = new BN(value);
|
|
1055
|
+
amountRes[address] = new BN(value).toString();
|
|
1056
1056
|
}
|
|
1057
1057
|
});
|
|
1058
1058
|
});
|
|
1059
|
-
Object.keys(amountRes).forEach((address) => {
|
|
1060
|
-
amountRes[address] = amountRes[address].toString();
|
|
1061
|
-
});
|
|
1062
1059
|
logger.info('get subscription stake amount setup success', { txHash, amountRes });
|
|
1063
1060
|
return amountRes;
|
|
1064
1061
|
}
|
|
@@ -1489,7 +1486,7 @@ export async function slashOverdraftProtectionStake(subscription: Subscription,
|
|
|
1489
1486
|
// @ts-ignore
|
|
1490
1487
|
const { buffer } = await client.encodeSlashStakeTx({ tx: signed });
|
|
1491
1488
|
// @ts-ignore
|
|
1492
|
-
const txHash = await client.sendSlashStakeTx({ tx: signed, wallet }, getGasPayerExtra(buffer));
|
|
1489
|
+
const txHash = await client.sendSlashStakeTx({ tx: signed, wallet }, await getGasPayerExtra(buffer));
|
|
1493
1490
|
|
|
1494
1491
|
await paymentIntent.update({
|
|
1495
1492
|
status: 'succeeded',
|
package/api/src/libs/util.ts
CHANGED
|
@@ -2,7 +2,7 @@ import crypto from 'crypto';
|
|
|
2
2
|
import { Readable } from 'stream';
|
|
3
3
|
import { buffer } from 'node:stream/consumers';
|
|
4
4
|
import { getUrl } from '@blocklet/sdk/lib/component';
|
|
5
|
-
import env from '@blocklet/sdk/lib/env';
|
|
5
|
+
import { env } from '@blocklet/sdk/lib/env';
|
|
6
6
|
import { getWalletDid } from '@blocklet/sdk/lib/did';
|
|
7
7
|
import { toStakeAddress } from '@arcblock/did-util';
|
|
8
8
|
import { customAlphabet } from 'nanoid';
|
|
@@ -603,3 +603,20 @@ export function formatNumber(
|
|
|
603
603
|
const [left, right] = result.split('.');
|
|
604
604
|
return right ? [left, trimEnd(right, '0')].filter(Boolean).join('.') : left;
|
|
605
605
|
}
|
|
606
|
+
|
|
607
|
+
export function formatLinkWithLocale(url: string, locale?: string) {
|
|
608
|
+
if (!locale || !url) {
|
|
609
|
+
return url;
|
|
610
|
+
}
|
|
611
|
+
try {
|
|
612
|
+
const urlObj = new URL(url);
|
|
613
|
+
urlObj.searchParams.set('locale', locale);
|
|
614
|
+
return urlObj.toString();
|
|
615
|
+
} catch (error) {
|
|
616
|
+
if (/[?&]locale=[^&]*/.test(url)) {
|
|
617
|
+
return url.replace(/([?&])locale=[^&]*/, `$1locale=${locale}`);
|
|
618
|
+
}
|
|
619
|
+
const separator = url.includes('?') ? '&' : '?';
|
|
620
|
+
return `${url}${separator}locale=${locale}`;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
@@ -94,7 +94,8 @@ export class DidnamesAdapter implements VendorAdapter {
|
|
|
94
94
|
});
|
|
95
95
|
|
|
96
96
|
// Generate bindDomainCap for this domain
|
|
97
|
-
|
|
97
|
+
// eslint-disable-next-line no-await-in-loop
|
|
98
|
+
const bindDomainCap = await this.generateBindCap({
|
|
98
99
|
domain,
|
|
99
100
|
checkoutSessionId: orderData.checkoutSessionId,
|
|
100
101
|
});
|
|
@@ -115,7 +116,8 @@ export class DidnamesAdapter implements VendorAdapter {
|
|
|
115
116
|
};
|
|
116
117
|
|
|
117
118
|
try {
|
|
118
|
-
|
|
119
|
+
// eslint-disable-next-line no-await-in-loop
|
|
120
|
+
const { headers, body } = await VendorAuth.signRequestWithHeaders(updatedOrderData);
|
|
119
121
|
|
|
120
122
|
// eslint-disable-next-line no-await-in-loop
|
|
121
123
|
const response = await fetch(url, {
|
|
@@ -198,7 +200,13 @@ export class DidnamesAdapter implements VendorAdapter {
|
|
|
198
200
|
/**
|
|
199
201
|
* Generate bindDomainCap (binding capability) for domain authorization
|
|
200
202
|
*/
|
|
201
|
-
private generateBindCap({
|
|
203
|
+
private async generateBindCap({
|
|
204
|
+
domain,
|
|
205
|
+
checkoutSessionId,
|
|
206
|
+
}: {
|
|
207
|
+
domain: string;
|
|
208
|
+
checkoutSessionId: string;
|
|
209
|
+
}): Promise<any> {
|
|
202
210
|
const now = Math.floor(Date.now() / 1000);
|
|
203
211
|
const expireInMinutes = 30;
|
|
204
212
|
|
|
@@ -210,7 +218,7 @@ export class DidnamesAdapter implements VendorAdapter {
|
|
|
210
218
|
nonce: uuidV4(),
|
|
211
219
|
};
|
|
212
220
|
|
|
213
|
-
const signature = toBase58(wallet.sign(stableStringify(cap) || ''));
|
|
221
|
+
const signature = toBase58(await wallet.sign(stableStringify(cap) || ''));
|
|
214
222
|
|
|
215
223
|
return {
|
|
216
224
|
cap,
|
|
@@ -345,7 +353,7 @@ export class DidnamesAdapter implements VendorAdapter {
|
|
|
345
353
|
customParams: params.customParams,
|
|
346
354
|
};
|
|
347
355
|
|
|
348
|
-
const { headers, body } = VendorAuth.signRequestWithHeaders(returnRequest);
|
|
356
|
+
const { headers, body } = await VendorAuth.signRequestWithHeaders(returnRequest);
|
|
349
357
|
const url = formatVendorUrl(vendorConfig, '/api/vendor/return');
|
|
350
358
|
logger.info('submitting domain return to DID Names', {
|
|
351
359
|
url,
|
|
@@ -449,7 +457,7 @@ export class DidnamesAdapter implements VendorAdapter {
|
|
|
449
457
|
async getOrder(vendor: ProductVendor, orderId: string): Promise<any> {
|
|
450
458
|
const url = formatVendorUrl(vendor, `/api/vendor/orders/${orderId}`);
|
|
451
459
|
|
|
452
|
-
const { headers } = VendorAuth.signRequestWithHeaders({});
|
|
460
|
+
const { headers } = await VendorAuth.signRequestWithHeaders({});
|
|
453
461
|
const response = await fetch(url, { method: 'GET', headers });
|
|
454
462
|
|
|
455
463
|
if (!response.ok) {
|
|
@@ -474,7 +482,7 @@ export class DidnamesAdapter implements VendorAdapter {
|
|
|
474
482
|
orderId,
|
|
475
483
|
});
|
|
476
484
|
|
|
477
|
-
const { headers } = VendorAuth.signRequestWithHeaders({});
|
|
485
|
+
const { headers } = await VendorAuth.signRequestWithHeaders({});
|
|
478
486
|
const response = await fetch(url, { method: 'GET', headers });
|
|
479
487
|
|
|
480
488
|
logger.info('didnames order status response', {
|
|
@@ -499,9 +507,9 @@ export class DidnamesAdapter implements VendorAdapter {
|
|
|
499
507
|
return data;
|
|
500
508
|
}
|
|
501
509
|
|
|
502
|
-
connectTest(): Promise<any> {
|
|
510
|
+
async connectTest(): Promise<any> {
|
|
503
511
|
const url = formatVendorUrl(this.vendorConfig!, '/api/vendor/health');
|
|
504
|
-
const { headers } = VendorAuth.signRequestWithHeaders({});
|
|
512
|
+
const { headers } = await VendorAuth.signRequestWithHeaders({});
|
|
505
513
|
return fetch(url, { headers }).then((res) => res.json());
|
|
506
514
|
}
|
|
507
515
|
}
|
|
@@ -17,8 +17,13 @@ import {
|
|
|
17
17
|
} from './types';
|
|
18
18
|
import { formatVendorUrl } from './util';
|
|
19
19
|
|
|
20
|
-
const doRequestVendorData = (
|
|
21
|
-
|
|
20
|
+
const doRequestVendorData = async (
|
|
21
|
+
vendor: ProductVendor,
|
|
22
|
+
orderId: string,
|
|
23
|
+
url: string,
|
|
24
|
+
options: { shortUrl: boolean }
|
|
25
|
+
) => {
|
|
26
|
+
const { headers } = await VendorAuth.signRequestWithHeaders({});
|
|
22
27
|
const name = vendor?.name;
|
|
23
28
|
const key = vendor?.vendor_key;
|
|
24
29
|
const { shortUrl } = options;
|
|
@@ -124,7 +129,7 @@ export class LauncherAdapter implements VendorAdapter {
|
|
|
124
129
|
};
|
|
125
130
|
}
|
|
126
131
|
|
|
127
|
-
const { headers, body } = VendorAuth.signRequestWithHeaders(orderData);
|
|
132
|
+
const { headers, body } = await VendorAuth.signRequestWithHeaders(orderData);
|
|
128
133
|
const url = formatVendorUrl(vendorConfig, '/api/vendor/deliveries');
|
|
129
134
|
const response = await fetch(url, {
|
|
130
135
|
method: 'POST',
|
|
@@ -207,7 +212,7 @@ export class LauncherAdapter implements VendorAdapter {
|
|
|
207
212
|
});
|
|
208
213
|
|
|
209
214
|
const vendorConfig = await this.getVendorConfig();
|
|
210
|
-
const { headers, body } = VendorAuth.signRequestWithHeaders(params);
|
|
215
|
+
const { headers, body } = await VendorAuth.signRequestWithHeaders(params);
|
|
211
216
|
|
|
212
217
|
const response = await fetch(formatVendorUrl(vendorConfig, '/api/vendor/return'), {
|
|
213
218
|
method: 'POST',
|
|
@@ -268,9 +273,9 @@ export class LauncherAdapter implements VendorAdapter {
|
|
|
268
273
|
return doRequestVendorData(vendor, orderId, url, options);
|
|
269
274
|
}
|
|
270
275
|
|
|
271
|
-
connectTest(): Promise<any> {
|
|
276
|
+
async connectTest(): Promise<any> {
|
|
272
277
|
const url = formatVendorUrl(this.vendorConfig!, '/api/vendor/health');
|
|
273
|
-
const { headers } = VendorAuth.signRequestWithHeaders({});
|
|
278
|
+
const { headers } = await VendorAuth.signRequestWithHeaders({});
|
|
274
279
|
return fetch(url, { headers }).then((res) => res.json());
|
|
275
280
|
}
|
|
276
281
|
}
|
|
@@ -754,7 +754,7 @@ const handleStakeSlash = async (
|
|
|
754
754
|
// @ts-ignore
|
|
755
755
|
const { buffer } = await client.encodeSlashStakeTx({ tx: signed });
|
|
756
756
|
// @ts-ignore
|
|
757
|
-
const txHash = await client.sendSlashStakeTx({ tx: signed, wallet }, getGasPayerExtra(buffer));
|
|
757
|
+
const txHash = await client.sendSlashStakeTx({ tx: signed, wallet }, await getGasPayerExtra(buffer));
|
|
758
758
|
logger.info('Stake slashing done', {
|
|
759
759
|
subscription: subscription.id,
|
|
760
760
|
amount: slashAmount,
|
|
@@ -973,7 +973,7 @@ export const handlePayment = async (job: PaymentJob) => {
|
|
|
973
973
|
const txHash = await client.sendTransferV2Tx(
|
|
974
974
|
// @ts-ignore
|
|
975
975
|
{ tx: signed, wallet, delegator: result.delegator },
|
|
976
|
-
getGasPayerExtra(buffer)
|
|
976
|
+
await getGasPayerExtra(buffer)
|
|
977
977
|
);
|
|
978
978
|
logger.info('PaymentIntent txHash', { txHash });
|
|
979
979
|
logger.info('PaymentIntent capture done', { id: paymentIntent.id, txHash });
|
package/api/src/queues/payout.ts
CHANGED
|
@@ -82,7 +82,7 @@ async function processArcblockPayout(payout: Payout, paymentMethod: PaymentMetho
|
|
|
82
82
|
// @ts-ignore
|
|
83
83
|
const { buffer } = await client.encodeTransferV2Tx({ tx: signed });
|
|
84
84
|
// @ts-ignore
|
|
85
|
-
const txHash = await client.sendTransferV2Tx({ tx: signed, wallet }, getGasPayerExtra(buffer));
|
|
85
|
+
const txHash = await client.sendTransferV2Tx({ tx: signed, wallet }, await getGasPayerExtra(buffer));
|
|
86
86
|
|
|
87
87
|
logger.info('Payout completed', { id: payout.id, txHash });
|
|
88
88
|
|
package/api/src/queues/refund.ts
CHANGED
|
@@ -167,7 +167,7 @@ const handleRefundJob = async (
|
|
|
167
167
|
// @ts-ignore
|
|
168
168
|
const { buffer } = await client.encodeTransferV2Tx({ tx: signed });
|
|
169
169
|
// @ts-ignore
|
|
170
|
-
const txHash = await client.sendTransferV2Tx({ tx: signed, wallet }, getGasPayerExtra(buffer));
|
|
170
|
+
const txHash = await client.sendTransferV2Tx({ tx: signed, wallet }, await getGasPayerExtra(buffer));
|
|
171
171
|
|
|
172
172
|
logger.info('refund transfer done', { id: refund.id, txHash });
|
|
173
173
|
await refund.update({
|
|
@@ -366,7 +366,7 @@ const handleStakeReturnJob = async (
|
|
|
366
366
|
// @ts-ignore
|
|
367
367
|
const { buffer } = await client.encodeReturnStakeTx({ tx: signed });
|
|
368
368
|
// @ts-ignore
|
|
369
|
-
const txHash = await client.sendReturnStakeTx({ tx: signed, wallet }, getGasPayerExtra(buffer));
|
|
369
|
+
const txHash = await client.sendReturnStakeTx({ tx: signed, wallet }, await getGasPayerExtra(buffer));
|
|
370
370
|
logger.info('stake return done', { id: refund.id, txHash });
|
|
371
371
|
await refund.update({
|
|
372
372
|
status: 'succeeded',
|
|
@@ -743,7 +743,7 @@ export const handleStakeSlashAfterCancel = async (subscription: Subscription, fo
|
|
|
743
743
|
// @ts-ignore
|
|
744
744
|
const { buffer } = await client.encodeSlashStakeTx({ tx: signed });
|
|
745
745
|
// @ts-ignore
|
|
746
|
-
const txHash = await client.sendSlashStakeTx({ tx: signed, wallet }, getGasPayerExtra(buffer));
|
|
746
|
+
const txHash = await client.sendSlashStakeTx({ tx: signed, wallet }, await getGasPayerExtra(buffer));
|
|
747
747
|
logger.info('Stake slashing done', {
|
|
748
748
|
subscription: subscription.id,
|
|
749
749
|
amount: invoice.amount_remaining,
|
|
@@ -108,7 +108,7 @@ export const doHandleUsageRecord = async (job: UsageRecordJob) => {
|
|
|
108
108
|
rawQuantity,
|
|
109
109
|
quantity,
|
|
110
110
|
|
|
111
|
-
unitAmount: fromUnitToToken(unitAmount, currency.decimal),
|
|
111
|
+
unitAmount: fromUnitToToken(unitAmount || '0', currency.decimal),
|
|
112
112
|
totalAmount: fromUnitToToken(totalAmount.toString(), currency.decimal),
|
|
113
113
|
threshold: fromUnitToToken(threshold.toString(), currency.decimal),
|
|
114
114
|
});
|
|
@@ -103,7 +103,7 @@ export const handleVendorStatusCheck = async (job: VendorStatusCheckJob) => {
|
|
|
103
103
|
'/api/vendor/status',
|
|
104
104
|
vendor.order_id
|
|
105
105
|
);
|
|
106
|
-
const { headers } = VendorAuth.signRequestWithHeaders({});
|
|
106
|
+
const { headers } = await VendorAuth.signRequestWithHeaders({});
|
|
107
107
|
|
|
108
108
|
const result = await fetch(serverStatusUrl, { headers });
|
|
109
109
|
const data = await result.json();
|
|
@@ -63,7 +63,7 @@ export const handleWebhook = async (job: WebhookJob) => {
|
|
|
63
63
|
headers: {
|
|
64
64
|
'x-app-id': wallet.address,
|
|
65
65
|
'x-app-pk': wallet.publicKey,
|
|
66
|
-
'x-component-sig': sign(json),
|
|
66
|
+
'x-component-sig': await sign(json),
|
|
67
67
|
'x-component-did': process.env.BLOCKLET_COMPONENT_DID as string,
|
|
68
68
|
},
|
|
69
69
|
});
|
|
@@ -3,7 +3,7 @@ import Joi from 'joi';
|
|
|
3
3
|
|
|
4
4
|
import { CustomError } from '@blocklet/error';
|
|
5
5
|
import { Op } from 'sequelize';
|
|
6
|
-
import sessionMiddleware from '@blocklet/sdk/lib/middlewares/session';
|
|
6
|
+
import { sessionMiddleware } from '@blocklet/sdk/lib/middlewares/session';
|
|
7
7
|
import { BN, fromTokenToUnit, fromUnitToToken } from '@ocap/util';
|
|
8
8
|
import { trimDecimals } from '../libs/math-utils';
|
|
9
9
|
import {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* eslint-disable consistent-return */
|
|
2
2
|
import { isValid } from '@arcblock/did';
|
|
3
3
|
import { getUrl } from '@blocklet/sdk/lib/component';
|
|
4
|
-
import sessionMiddleware from '@blocklet/sdk/lib/middlewares/session';
|
|
4
|
+
import { sessionMiddleware } from '@blocklet/sdk/lib/middlewares/session';
|
|
5
5
|
import { BN, fromUnitToToken } from '@ocap/util';
|
|
6
6
|
import { NextFunction, Request, Response, Router } from 'express';
|
|
7
7
|
import Joi from 'joi';
|
|
@@ -390,7 +390,7 @@ export async function calculateAndUpdateAmount(
|
|
|
390
390
|
amount,
|
|
391
391
|
});
|
|
392
392
|
|
|
393
|
-
if (checkoutSession.mode === 'payment' && amount.total
|
|
393
|
+
if (checkoutSession.mode === 'payment' && new BN(amount.total || '0').lt(new BN('0'))) {
|
|
394
394
|
throw new Error('Payment amount should be greater or equal to 0');
|
|
395
395
|
}
|
|
396
396
|
|
|
@@ -2149,10 +2149,6 @@ router.post('/:id/fast-checkout-confirm', user, ensureCheckoutSessionOpen, async
|
|
|
2149
2149
|
return res.status(400).json({ error: 'Payment method not found' });
|
|
2150
2150
|
}
|
|
2151
2151
|
|
|
2152
|
-
if (paymentMethod.type !== 'arcblock') {
|
|
2153
|
-
return res.status(400).json({ error: 'Payment method not supported for fast checkout' });
|
|
2154
|
-
}
|
|
2155
|
-
|
|
2156
2152
|
// Validate checkout session ownership if it was created for a specific customer
|
|
2157
2153
|
if (checkoutSession.customer_id && checkoutSession.metadata?.createdBy) {
|
|
2158
2154
|
const createdByDid = checkoutSession.metadata.createdBy;
|
|
@@ -2355,6 +2351,8 @@ router.post('/:id/fast-checkout-confirm', user, ensureCheckoutSessionOpen, async
|
|
|
2355
2351
|
};
|
|
2356
2352
|
canFastPay = true;
|
|
2357
2353
|
}
|
|
2354
|
+
} else if (paymentMethod.type !== 'arcblock') {
|
|
2355
|
+
return res.status(400).json({ error: 'Payment method not supported for fast checkout' });
|
|
2358
2356
|
}
|
|
2359
2357
|
|
|
2360
2358
|
logger.info('Checkout session submitted successfully', {
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { executeEvmTransaction, waitForEvmTxConfirm } from '../../integrations/ethereum/tx';
|
|
2
|
+
import type { CallbackArgs } from '../../libs/auth';
|
|
3
|
+
import { getTxMetadata } from '../../libs/util';
|
|
4
|
+
import { type TLineItemExpanded } from '../../store/models';
|
|
5
|
+
import {
|
|
6
|
+
ensurePayerChangeContext,
|
|
7
|
+
executeOcapTransactions,
|
|
8
|
+
getAuthPrincipalClaim,
|
|
9
|
+
getDelegationTxClaim,
|
|
10
|
+
} from './shared';
|
|
11
|
+
import { EVM_CHAIN_TYPES } from '../../libs/constants';
|
|
12
|
+
|
|
13
|
+
export default {
|
|
14
|
+
action: 'change-payer',
|
|
15
|
+
authPrincipal: false,
|
|
16
|
+
persistentDynamicClaims: true,
|
|
17
|
+
claims: {
|
|
18
|
+
authPrincipal: async ({ extraParams }: CallbackArgs) => {
|
|
19
|
+
const { paymentMethod } = await ensurePayerChangeContext(extraParams.subscriptionId);
|
|
20
|
+
return getAuthPrincipalClaim(paymentMethod, 'continue');
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
onConnect: async ({ userDid, userPk, extraParams }: CallbackArgs) => {
|
|
24
|
+
const { subscriptionId } = extraParams;
|
|
25
|
+
const { subscription, paymentMethod, paymentCurrency, payerAddress } =
|
|
26
|
+
await ensurePayerChangeContext(subscriptionId);
|
|
27
|
+
|
|
28
|
+
if (userDid === payerAddress) {
|
|
29
|
+
throw new Error('The current payer is the same as the new payer, please use another account to change payer');
|
|
30
|
+
}
|
|
31
|
+
const claimsList: any[] = [];
|
|
32
|
+
// @ts-ignore
|
|
33
|
+
const items = subscription!.items as TLineItemExpanded[];
|
|
34
|
+
const trialing = true;
|
|
35
|
+
const billingThreshold = Number(subscription.billing_thresholds?.amount_gte || 0);
|
|
36
|
+
|
|
37
|
+
if (paymentMethod.type === 'arcblock') {
|
|
38
|
+
claimsList.push({
|
|
39
|
+
signature: await getDelegationTxClaim({
|
|
40
|
+
mode: 'delegation',
|
|
41
|
+
userDid,
|
|
42
|
+
userPk,
|
|
43
|
+
nonce: subscription.id,
|
|
44
|
+
data: getTxMetadata({ subscriptionId: subscription.id }),
|
|
45
|
+
paymentCurrency,
|
|
46
|
+
paymentMethod,
|
|
47
|
+
trialing,
|
|
48
|
+
billingThreshold,
|
|
49
|
+
items,
|
|
50
|
+
requiredStake: false,
|
|
51
|
+
}),
|
|
52
|
+
});
|
|
53
|
+
return claimsList;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (EVM_CHAIN_TYPES.includes(paymentMethod.type)) {
|
|
57
|
+
if (!paymentCurrency.contract) {
|
|
58
|
+
throw new Error(`Payment currency ${paymentMethod.type}:${paymentCurrency.id} does not support subscription`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
claimsList.push({
|
|
62
|
+
signature: await getDelegationTxClaim({
|
|
63
|
+
mode: 'subscription',
|
|
64
|
+
userDid,
|
|
65
|
+
userPk,
|
|
66
|
+
nonce: `change-payer-${subscription!.id}`,
|
|
67
|
+
data: getTxMetadata({ subscriptionId: subscription!.id }),
|
|
68
|
+
paymentCurrency,
|
|
69
|
+
paymentMethod,
|
|
70
|
+
trialing,
|
|
71
|
+
billingThreshold,
|
|
72
|
+
items,
|
|
73
|
+
}),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return claimsList;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
throw new Error(`ChangePayer: Payment method ${paymentMethod.type} not supported`);
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
onAuth: async ({ request, userDid, userPk, claims, extraParams, step }: CallbackArgs) => {
|
|
83
|
+
const { subscriptionId } = extraParams;
|
|
84
|
+
const { subscription, paymentMethod, paymentCurrency } = await ensurePayerChangeContext(subscriptionId);
|
|
85
|
+
|
|
86
|
+
const result = request?.context?.store?.result || [];
|
|
87
|
+
result.push({
|
|
88
|
+
step,
|
|
89
|
+
claim: claims?.[0],
|
|
90
|
+
stepRequest: {
|
|
91
|
+
headers: request?.headers,
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
const claimsList = result.map((x: any) => x.claim);
|
|
95
|
+
|
|
96
|
+
const afterTxExecution = async (paymentDetails: any) => {
|
|
97
|
+
await subscription?.update({
|
|
98
|
+
payment_settings: {
|
|
99
|
+
payment_method_types: [paymentMethod.type],
|
|
100
|
+
payment_method_options: {
|
|
101
|
+
[paymentMethod.type]: { payer: userDid },
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
payment_details: {
|
|
105
|
+
...subscription.payment_details,
|
|
106
|
+
[paymentMethod.type]: {
|
|
107
|
+
...(subscription.payment_details?.[paymentMethod.type as keyof typeof subscription.payment_details] || {}),
|
|
108
|
+
type: 'delegate',
|
|
109
|
+
payer: userDid,
|
|
110
|
+
tx_hash: paymentDetails.tx_hash,
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
if (paymentMethod.type === 'arcblock') {
|
|
117
|
+
const requestArray = result
|
|
118
|
+
.map((item: { stepRequest?: Request }) => item.stepRequest)
|
|
119
|
+
.filter(Boolean) as Request[];
|
|
120
|
+
const requestSource = requestArray.length > 0 ? requestArray : request;
|
|
121
|
+
|
|
122
|
+
const paymentDetails = await executeOcapTransactions(
|
|
123
|
+
userDid,
|
|
124
|
+
userPk,
|
|
125
|
+
claimsList,
|
|
126
|
+
paymentMethod,
|
|
127
|
+
requestSource,
|
|
128
|
+
subscription?.id,
|
|
129
|
+
paymentCurrency.contract
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
await afterTxExecution(paymentDetails);
|
|
133
|
+
return { hash: paymentDetails.tx_hash };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (EVM_CHAIN_TYPES.includes(paymentMethod.type)) {
|
|
137
|
+
const paymentDetails = await executeEvmTransaction('approve', userDid, claimsList, paymentMethod);
|
|
138
|
+
waitForEvmTxConfirm(paymentMethod.getEvmClient(), +paymentDetails.block_height, paymentMethod.confirmation.block)
|
|
139
|
+
.then(async () => {
|
|
140
|
+
await afterTxExecution(paymentDetails);
|
|
141
|
+
})
|
|
142
|
+
.catch(console.error);
|
|
143
|
+
return { hash: paymentDetails.tx_hash };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
throw new Error(`ChangePayer: Payment method ${paymentMethod.type} not supported`);
|
|
147
|
+
},
|
|
148
|
+
};
|
|
@@ -136,7 +136,7 @@ export default {
|
|
|
136
136
|
const txHash = await client.sendTransferV3Tx(
|
|
137
137
|
// @ts-ignore
|
|
138
138
|
{ tx, wallet: fromAddress(userDid) },
|
|
139
|
-
getGasPayerExtra(buffer, client.pickGasPayerHeaders(request))
|
|
139
|
+
await getGasPayerExtra(buffer, client.pickGasPayerHeaders(request))
|
|
140
140
|
);
|
|
141
141
|
await afterTxExecution({ tx_hash: txHash, payer: userDid, type: 'transfer' });
|
|
142
142
|
|
|
@@ -155,7 +155,7 @@ export default {
|
|
|
155
155
|
const txHash = await client.sendTransferV3Tx(
|
|
156
156
|
// @ts-ignore
|
|
157
157
|
{ tx, wallet: fromAddress(userDid) },
|
|
158
|
-
getGasPayerExtra(buffer, client.pickGasPayerHeaders(request))
|
|
158
|
+
await getGasPayerExtra(buffer, client.pickGasPayerHeaders(request))
|
|
159
159
|
);
|
|
160
160
|
|
|
161
161
|
await afterTxExecution({
|
|
@@ -115,7 +115,7 @@ export default {
|
|
|
115
115
|
const txHash = await client.sendTransferV3Tx(
|
|
116
116
|
// @ts-ignore
|
|
117
117
|
{ tx, wallet: fromAddress(userDid) },
|
|
118
|
-
getGasPayerExtra(buffer, client.pickGasPayerHeaders(request))
|
|
118
|
+
await getGasPayerExtra(buffer, client.pickGasPayerHeaders(request))
|
|
119
119
|
);
|
|
120
120
|
|
|
121
121
|
await paymentIntent.update({
|
|
@@ -149,7 +149,7 @@ export default {
|
|
|
149
149
|
const txHash = await client.sendTransferV3Tx(
|
|
150
150
|
// @ts-ignore
|
|
151
151
|
{ tx, wallet: fromAddress(userDid) },
|
|
152
|
-
getGasPayerExtra(buffer, client.pickGasPayerHeaders(request))
|
|
152
|
+
await getGasPayerExtra(buffer, client.pickGasPayerHeaders(request))
|
|
153
153
|
);
|
|
154
154
|
|
|
155
155
|
await afterTxExecution({
|
|
@@ -128,7 +128,7 @@ export default {
|
|
|
128
128
|
const txHash = await client.sendTransferV3Tx(
|
|
129
129
|
// @ts-ignore
|
|
130
130
|
{ tx, wallet: fromAddress(userDid) },
|
|
131
|
-
getGasPayerExtra(buffer, client.pickGasPayerHeaders(request))
|
|
131
|
+
await getGasPayerExtra(buffer, client.pickGasPayerHeaders(request))
|
|
132
132
|
);
|
|
133
133
|
|
|
134
134
|
logger.info('Recharge successful', {
|