payment-kit 1.13.215 → 1.13.216
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/crons/index.ts +1 -1
- package/api/src/index.ts +1 -1
- package/api/src/integrations/{blockchain → arcblock}/stake.ts +19 -12
- package/api/src/integrations/ethereum/token.ts +141 -0
- package/api/src/integrations/ethereum/tx.ts +32 -0
- package/api/src/libs/auth.ts +1 -0
- package/api/src/libs/payment.ts +76 -30
- package/api/src/queues/checkout-session.ts +1 -1
- package/api/src/queues/payment.ts +99 -54
- package/api/src/queues/refund.ts +84 -44
- package/api/src/routes/connect/change-payment.ts +54 -16
- package/api/src/routes/connect/change-plan.ts +55 -13
- package/api/src/routes/connect/collect-batch.ts +0 -4
- package/api/src/routes/connect/collect.ts +77 -30
- package/api/src/routes/connect/pay.ts +56 -12
- package/api/src/routes/connect/setup.ts +92 -48
- package/api/src/routes/connect/shared.ts +107 -79
- package/api/src/routes/connect/subscribe.ts +68 -23
- package/api/src/routes/customers.ts +1 -1
- package/api/src/routes/invoices.ts +14 -8
- package/api/src/routes/payment-currencies.ts +112 -1
- package/api/src/routes/payment-methods.ts +81 -3
- package/api/src/store/migrations/20230911-seeding.ts +2 -2
- package/api/src/store/models/customer.ts +1 -0
- package/api/src/store/models/payment-method.ts +22 -1
- package/api/src/store/models/types.ts +7 -3
- package/api/third.d.ts +2 -0
- package/blocklet.yml +1 -1
- package/package.json +7 -4
- package/public/methods/ethereum.png +0 -0
- package/src/components/drawer-form.tsx +17 -6
- package/src/components/invoice/list.tsx +20 -13
- package/src/components/payment-currency/add.tsx +60 -0
- package/src/components/payment-currency/form.tsx +51 -0
- package/src/components/payment-method/bitcoin.tsx +1 -1
- package/src/components/payment-method/ethereum.tsx +16 -8
- package/src/components/payment-method/form.tsx +1 -3
- package/src/components/subscription/items/usage-records.tsx +20 -38
- package/src/locales/en.tsx +33 -0
- package/src/locales/zh.tsx +33 -0
- package/src/pages/admin/billing/subscriptions/detail.tsx +1 -1
- package/src/pages/admin/settings/index.tsx +4 -4
- package/src/pages/admin/settings/payment-methods/create.tsx +3 -2
- package/src/pages/admin/settings/payment-methods/index.tsx +57 -3
- package/src/pages/customer/invoice/past-due.tsx +1 -0
- package/src/pages/customer/refund/list.tsx +9 -13
- package/src/pages/customer/subscription/change-payment.tsx +1 -0
- package/src/pages/customer/subscription/change-plan.tsx +1 -0
- package/src/pages/customer/subscription/detail.tsx +2 -2
- /package/api/src/integrations/{blockchain → arcblock}/nft.ts +0 -0
package/api/src/crons/index.ts
CHANGED
package/api/src/index.ts
CHANGED
|
@@ -10,7 +10,7 @@ import express, { ErrorRequestHandler, Request, Response } from 'express';
|
|
|
10
10
|
import morgan from 'morgan';
|
|
11
11
|
|
|
12
12
|
import crons from './crons/index';
|
|
13
|
-
import { ensureStakedForGas } from './integrations/
|
|
13
|
+
import { ensureStakedForGas } from './integrations/arcblock/stake';
|
|
14
14
|
import { initResourceHandler } from './integrations/blocklet/resource';
|
|
15
15
|
import { ensureWebhookRegistered } from './integrations/stripe/setup';
|
|
16
16
|
import { handlers } from './libs/auth';
|
|
@@ -185,11 +185,12 @@ export async function getStakeSummaryByDid(did: string, livemode: boolean): Prom
|
|
|
185
185
|
const address = toStakeAddress(did, wallet.address);
|
|
186
186
|
const results: GroupedBN = {};
|
|
187
187
|
await Promise.all(
|
|
188
|
-
methods.map(async (method:
|
|
188
|
+
methods.map(async (method: PaymentMethod) => {
|
|
189
189
|
const client = method.getOcapClient();
|
|
190
190
|
const { state } = await client.getStakeState({ address });
|
|
191
191
|
(state?.tokens || []).forEach((t: any) => {
|
|
192
|
-
|
|
192
|
+
// @ts-ignore
|
|
193
|
+
const currency = method.payment_currencies.find((c: PaymentCurrency) => t.address === c.contract);
|
|
193
194
|
if (currency) {
|
|
194
195
|
results[currency.id] = t.value;
|
|
195
196
|
}
|
|
@@ -202,7 +203,7 @@ export async function getStakeSummaryByDid(did: string, livemode: boolean): Prom
|
|
|
202
203
|
|
|
203
204
|
export async function getTokenSummaryByDid(did: string, livemode: boolean): Promise<GroupedBN> {
|
|
204
205
|
const methods = await PaymentMethod.findAll({
|
|
205
|
-
where: { type: 'arcblock', livemode },
|
|
206
|
+
where: { type: ['arcblock', 'ethereum'], livemode },
|
|
206
207
|
include: [{ model: PaymentCurrency, as: 'payment_currencies' }],
|
|
207
208
|
});
|
|
208
209
|
if (methods.length === 0) {
|
|
@@ -211,15 +212,21 @@ export async function getTokenSummaryByDid(did: string, livemode: boolean): Prom
|
|
|
211
212
|
|
|
212
213
|
const results: GroupedBN = {};
|
|
213
214
|
await Promise.all(
|
|
214
|
-
methods.map(async (method:
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
215
|
+
methods.map(async (method: PaymentMethod) => {
|
|
216
|
+
if (method.type === 'arcblock') {
|
|
217
|
+
const client = method.getOcapClient();
|
|
218
|
+
const { tokens } = await client.getAccountTokens({ address: did });
|
|
219
|
+
(tokens || []).forEach((t: any) => {
|
|
220
|
+
// @ts-ignore
|
|
221
|
+
const currency = method.payment_currencies.find((c: PaymentCurrency) => t.address === c.contract);
|
|
222
|
+
if (currency) {
|
|
223
|
+
results[currency.id] = t.balance;
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
if (method.type === 'ethereum') {
|
|
228
|
+
// FIXME: how do we get balance for ethereum
|
|
229
|
+
}
|
|
223
230
|
})
|
|
224
231
|
);
|
|
225
232
|
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import erc20Abi from 'erc-20-abi';
|
|
2
|
+
import { JsonRpcProvider, TransactionReceipt, ethers } from 'ethers';
|
|
3
|
+
|
|
4
|
+
import { ethWallet } from '../../libs/auth';
|
|
5
|
+
import type { PaymentMethod } from '../../store/models/payment-method';
|
|
6
|
+
import { waitForEvmTxReceipt } from './tx';
|
|
7
|
+
|
|
8
|
+
export async function fetchErc20Meta(provider: JsonRpcProvider, contractAddress: string) {
|
|
9
|
+
const contract = new ethers.Contract(contractAddress, erc20Abi, provider);
|
|
10
|
+
|
|
11
|
+
// @ts-ignore
|
|
12
|
+
const [name, symbol, decimal] = await Promise.all([contract.name(), contract.symbol(), contract.decimals()]);
|
|
13
|
+
|
|
14
|
+
return { name, symbol, decimal: decimal.toNumber() };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function fetchErc20Balance(provider: JsonRpcProvider, contractAddress: string, account: string) {
|
|
18
|
+
const contract = new ethers.Contract(contractAddress, erc20Abi, provider);
|
|
19
|
+
|
|
20
|
+
// @ts-ignore
|
|
21
|
+
const balance = await contract.balanceOf(account);
|
|
22
|
+
return balance.toString();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function fetchErc20Allowance(
|
|
26
|
+
provider: JsonRpcProvider,
|
|
27
|
+
contractAddress: string,
|
|
28
|
+
owner: string,
|
|
29
|
+
spender: string
|
|
30
|
+
) {
|
|
31
|
+
const contract = new ethers.Contract(contractAddress, erc20Abi, provider);
|
|
32
|
+
|
|
33
|
+
// @ts-ignore
|
|
34
|
+
const allowance = await contract.allowance(owner, spender);
|
|
35
|
+
return allowance.toString();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function fetchEtherBalance(provider: JsonRpcProvider, account: string) {
|
|
39
|
+
const balance = await provider.getBalance(account);
|
|
40
|
+
return balance.toString();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function encodeErc20Transfer(to: string, amount: string) {
|
|
44
|
+
const iface = new ethers.Interface(erc20Abi);
|
|
45
|
+
return iface.encodeFunctionData('transfer', [to, amount]);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function encodeErc20Approve(spender: string, allowance: string) {
|
|
49
|
+
const iface = new ethers.Interface(erc20Abi);
|
|
50
|
+
return iface.encodeFunctionData('approve', [spender, allowance] as any);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function encodeTransferItx(to: string, amount: string, contract?: string) {
|
|
54
|
+
// Sending ERC20
|
|
55
|
+
if (contract) {
|
|
56
|
+
return {
|
|
57
|
+
to: contract,
|
|
58
|
+
value: '0',
|
|
59
|
+
gasLimit: '120000',
|
|
60
|
+
data: encodeErc20Transfer(to, amount),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Sending Ether
|
|
65
|
+
return {
|
|
66
|
+
to,
|
|
67
|
+
value: amount,
|
|
68
|
+
gasLimit: '21000',
|
|
69
|
+
data: '',
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function encodeApproveItx(spender: string, allowance: string, contract: string) {
|
|
74
|
+
return {
|
|
75
|
+
to: contract,
|
|
76
|
+
value: '0',
|
|
77
|
+
gasLimit: '120000', // FIXME: make me dynamic
|
|
78
|
+
data: encodeErc20Approve(spender, allowance),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// capture payments
|
|
83
|
+
export async function transferErc20FromUser(
|
|
84
|
+
provider: JsonRpcProvider,
|
|
85
|
+
contractAddress: string,
|
|
86
|
+
user: string,
|
|
87
|
+
amount: string
|
|
88
|
+
): Promise<TransactionReceipt> {
|
|
89
|
+
const wallet = new ethers.Wallet(ethWallet.secretKey);
|
|
90
|
+
const signer = wallet.connect(provider);
|
|
91
|
+
const contract = new ethers.Contract(contractAddress, erc20Abi, signer);
|
|
92
|
+
|
|
93
|
+
// @ts-ignore
|
|
94
|
+
const res = await contract.transferFrom(user, wallet.address, amount);
|
|
95
|
+
|
|
96
|
+
// Wait for the transaction to be mined
|
|
97
|
+
const receipt = await res.wait();
|
|
98
|
+
return receipt;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// do refunds
|
|
102
|
+
export async function sendErc20ToUser(
|
|
103
|
+
provider: JsonRpcProvider,
|
|
104
|
+
contractAddress: string,
|
|
105
|
+
user: string,
|
|
106
|
+
amount: string
|
|
107
|
+
): Promise<TransactionReceipt> {
|
|
108
|
+
const wallet = new ethers.Wallet(ethWallet.secretKey);
|
|
109
|
+
const signer = wallet.connect(provider);
|
|
110
|
+
const contract = new ethers.Contract(contractAddress, erc20Abi, signer);
|
|
111
|
+
|
|
112
|
+
// @ts-ignore
|
|
113
|
+
const res = await contract.transfer(user, amount);
|
|
114
|
+
|
|
115
|
+
// Wait for the transaction to be mined
|
|
116
|
+
const receipt = await res.wait();
|
|
117
|
+
return receipt;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function executeEvmTransactions(
|
|
121
|
+
type: string,
|
|
122
|
+
userDid: string,
|
|
123
|
+
claims: any[],
|
|
124
|
+
paymentMethod: PaymentMethod
|
|
125
|
+
) {
|
|
126
|
+
const client = paymentMethod.getEvmClient();
|
|
127
|
+
const claim = claims.find((x) => x.type === 'signature');
|
|
128
|
+
const receipt = await waitForEvmTxReceipt(client, claim.hash);
|
|
129
|
+
if (!receipt.status) {
|
|
130
|
+
throw new Error(`EVM Transaction failed: ${claim.hash}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
type,
|
|
135
|
+
tx_hash: claim.hash,
|
|
136
|
+
payer: userDid,
|
|
137
|
+
block_height: receipt.blockNumber.toString(),
|
|
138
|
+
gas_used: receipt.gasUsed.toString(),
|
|
139
|
+
gas_price: receipt.gasPrice.toString(),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { JsonRpcProvider, TransactionReceipt, TransactionResponse } from 'ethers';
|
|
2
|
+
import waitFor from 'p-wait-for';
|
|
3
|
+
|
|
4
|
+
import logger from '../../libs/logger';
|
|
5
|
+
|
|
6
|
+
export async function waitForEvmTxReceipt(provider: JsonRpcProvider, txHash: string) {
|
|
7
|
+
let minted: TransactionResponse;
|
|
8
|
+
let receipt: TransactionReceipt;
|
|
9
|
+
|
|
10
|
+
await waitFor(
|
|
11
|
+
async () => {
|
|
12
|
+
// @ts-ignore
|
|
13
|
+
minted = await provider.getTransaction(txHash);
|
|
14
|
+
logger.info('waitForTxReceipt.mintCheck', minted);
|
|
15
|
+
return !!minted?.blockNumber;
|
|
16
|
+
},
|
|
17
|
+
{ interval: 3000, timeout: 30 * 60 * 1000 }
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
await waitFor(
|
|
21
|
+
async () => {
|
|
22
|
+
// @ts-ignore
|
|
23
|
+
receipt = await provider.getTransactionReceipt(txHash);
|
|
24
|
+
logger.info('waitForTxReceipt.receiptCheck', receipt);
|
|
25
|
+
return !!receipt;
|
|
26
|
+
},
|
|
27
|
+
{ interval: 1000, timeout: 60 * 1000 }
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
// @ts-ignore
|
|
31
|
+
return receipt;
|
|
32
|
+
}
|
package/api/src/libs/auth.ts
CHANGED
|
@@ -11,6 +11,7 @@ import type { LiteralUnion } from 'type-fest';
|
|
|
11
11
|
import env from './env';
|
|
12
12
|
|
|
13
13
|
export const wallet = getWallet();
|
|
14
|
+
export const ethWallet = getWallet('ethereum');
|
|
14
15
|
export const authenticator = new WalletAuthenticator();
|
|
15
16
|
export const handlers = new WalletHandler({
|
|
16
17
|
authenticator,
|
package/api/src/libs/payment.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/indent */
|
|
2
|
+
import { isEthereumDid } from '@arcblock/did';
|
|
2
3
|
import { toDelegateAddress } from '@arcblock/did-util';
|
|
3
4
|
import { sign } from '@arcblock/jwt';
|
|
4
5
|
import { getWalletDid } from '@blocklet/sdk/lib/did';
|
|
@@ -8,9 +9,10 @@ import { BN, fromUnitToToken } from '@ocap/util';
|
|
|
8
9
|
import cloneDeep from 'lodash/cloneDeep';
|
|
9
10
|
import type { LiteralUnion } from 'type-fest';
|
|
10
11
|
|
|
12
|
+
import { fetchErc20Allowance, fetchErc20Balance, fetchEtherBalance } from '../integrations/ethereum/token';
|
|
11
13
|
import { Invoice, PaymentCurrency, PaymentIntent, PaymentMethod, TCustomer, TLineItemExpanded } from '../store/models';
|
|
12
14
|
import type { TPaymentCurrency } from '../store/models/payment-currency';
|
|
13
|
-
import { blocklet, wallet } from './auth';
|
|
15
|
+
import { blocklet, ethWallet, wallet } from './auth';
|
|
14
16
|
import { OCAP_PAYMENT_TX_TYPE } from './util';
|
|
15
17
|
|
|
16
18
|
export interface SufficientForPaymentResult {
|
|
@@ -101,6 +103,28 @@ export async function isDelegationSufficientForPayment(args: {
|
|
|
101
103
|
return { sufficient: false, reason: 'NOT_SUPPORTED' };
|
|
102
104
|
}
|
|
103
105
|
|
|
106
|
+
if (paymentMethod.type === 'ethereum') {
|
|
107
|
+
if (!paymentCurrency.contract) {
|
|
108
|
+
return { sufficient: false, reason: 'NOT_SUPPORTED' };
|
|
109
|
+
}
|
|
110
|
+
if (isEthereumDid(userDid) === false) {
|
|
111
|
+
return { sufficient: false, reason: 'NOT_SUPPORTED' };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const provider = paymentMethod.getEvmClient();
|
|
115
|
+
const balance = await fetchErc20Balance(provider, paymentCurrency.contract, userDid);
|
|
116
|
+
if (new BN(balance).lt(new BN(amount))) {
|
|
117
|
+
return { sufficient: false, reason: 'NO_ENOUGH_TOKEN' };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const allowance = await fetchErc20Allowance(provider, paymentCurrency.contract, userDid, ethWallet.address);
|
|
121
|
+
if (new BN(allowance).lt(new BN(amount))) {
|
|
122
|
+
return { sufficient: false, reason: 'NO_ENOUGH_ALLOWANCE' };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return { sufficient: true };
|
|
126
|
+
}
|
|
127
|
+
|
|
104
128
|
throw new Error(`Payment method ${paymentMethod.type} not supported`);
|
|
105
129
|
}
|
|
106
130
|
|
|
@@ -207,9 +231,6 @@ export async function getTokenLimitsForDelegation(
|
|
|
207
231
|
address: string,
|
|
208
232
|
amount: string
|
|
209
233
|
): Promise<TokenLimit[]> {
|
|
210
|
-
const client = paymentMethod.getOcapClient();
|
|
211
|
-
const { state } = await client.getDelegateState({ address });
|
|
212
|
-
|
|
213
234
|
const hasMetered = items.some((x) => x.price.recurring?.usage_type === 'metered');
|
|
214
235
|
const allowance = hasMetered ? '0' : amount;
|
|
215
236
|
|
|
@@ -223,40 +244,53 @@ export async function getTokenLimitsForDelegation(
|
|
|
223
244
|
validUntil: 0,
|
|
224
245
|
};
|
|
225
246
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
}
|
|
247
|
+
if (paymentMethod.type === 'arcblock') {
|
|
248
|
+
const client = paymentMethod.getOcapClient();
|
|
249
|
+
const { state } = await client.getDelegateState({ address });
|
|
230
250
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
251
|
+
// If we never delegated before
|
|
252
|
+
if (!state) {
|
|
253
|
+
return [entry];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// If we have metered items, we should not limit tx allowance(set to 0)
|
|
257
|
+
if (hasMetered) {
|
|
258
|
+
return [entry];
|
|
259
|
+
}
|
|
235
260
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
261
|
+
const op = (state as DelegateState).ops.find((x) => x.key === OCAP_PAYMENT_TX_TYPE);
|
|
262
|
+
if (op && Array.isArray(op.value.limit?.tokens) && op.value.limit.tokens.length > 0) {
|
|
263
|
+
const tokenLimits = cloneDeep(op.value.limit.tokens);
|
|
264
|
+
const index = op.value.limit.tokens.findIndex((x) => x.address === paymentCurrency.contract);
|
|
265
|
+
// we are updating an existing token limit
|
|
266
|
+
if (index > -1) {
|
|
267
|
+
const limit = op.value.limit.tokens[index] as TokenLimit;
|
|
268
|
+
// If we have a previous delegation and the txAllowance is smaller than requested amount
|
|
269
|
+
// If txAllowance is 0 (unlimited), we should not update it
|
|
270
|
+
if (limit.txAllowance !== '0' && new BN(limit.txAllowance).lt(new BN(amount))) {
|
|
271
|
+
tokenLimits[index] = entry;
|
|
272
|
+
}
|
|
273
|
+
} else {
|
|
274
|
+
// we are adding a new token limit
|
|
275
|
+
tokenLimits.push(entry);
|
|
247
276
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
tokenLimits.push(entry);
|
|
277
|
+
|
|
278
|
+
return tokenLimits;
|
|
251
279
|
}
|
|
252
280
|
|
|
253
|
-
return
|
|
281
|
+
return [entry];
|
|
254
282
|
}
|
|
255
283
|
|
|
256
|
-
|
|
284
|
+
if (paymentMethod.type === 'ethereum') {
|
|
285
|
+
// FIXME: @wangshijun better control from customer.token_limits
|
|
286
|
+
entry.totalAllowance = new BN(amount).mul(new BN(12)).toString();
|
|
287
|
+
return [entry];
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
throw new Error(`getTokenLimitsForDelegation: Payment method ${paymentMethod.type} not supported`);
|
|
257
291
|
}
|
|
258
292
|
|
|
259
|
-
export async function
|
|
293
|
+
export async function isBalanceSufficientForRefund(args: {
|
|
260
294
|
paymentMethod: PaymentMethod;
|
|
261
295
|
paymentCurrency: TPaymentCurrency;
|
|
262
296
|
amount: string;
|
|
@@ -278,9 +312,21 @@ export async function isBalanceSufficientForPayment(args: {
|
|
|
278
312
|
return { sufficient: true, token };
|
|
279
313
|
}
|
|
280
314
|
|
|
315
|
+
if (paymentMethod.type === 'ethereum') {
|
|
316
|
+
const provider = paymentMethod.getEvmClient();
|
|
317
|
+
const balance = paymentCurrency.contract
|
|
318
|
+
? await fetchErc20Balance(provider, paymentCurrency.contract, ethWallet.address)
|
|
319
|
+
: await fetchEtherBalance(provider, ethWallet.address);
|
|
320
|
+
if (new BN(balance).lt(new BN(amount))) {
|
|
321
|
+
return { sufficient: false, reason: 'NO_ENOUGH_TOKEN' };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return { sufficient: true };
|
|
325
|
+
}
|
|
326
|
+
|
|
281
327
|
if (paymentMethod.type === 'stripe') {
|
|
282
328
|
return { sufficient: false, reason: 'NOT_SUPPORTED' };
|
|
283
329
|
}
|
|
284
330
|
|
|
285
|
-
throw new Error(`Payment method ${paymentMethod.type} not supported`);
|
|
331
|
+
throw new Error(`isBalanceSufficientForRefund: Payment method ${paymentMethod.type} not supported`);
|
|
286
332
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* eslint-disable no-await-in-loop */
|
|
2
2
|
import { Op } from 'sequelize';
|
|
3
3
|
|
|
4
|
-
import { mintNftForCheckoutSession } from '../integrations/
|
|
4
|
+
import { mintNftForCheckoutSession } from '../integrations/arcblock/nft';
|
|
5
5
|
import { ensurePassportIssued } from '../integrations/blocklet/passport';
|
|
6
6
|
import dayjs from '../libs/dayjs';
|
|
7
7
|
import { events } from '../libs/event';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import isEmpty from 'lodash/isEmpty';
|
|
2
2
|
|
|
3
|
-
import { ensureStakedForGas } from '../integrations/
|
|
3
|
+
import { ensureStakedForGas } from '../integrations/arcblock/stake';
|
|
4
|
+
import { transferErc20FromUser } from '../integrations/ethereum/token';
|
|
4
5
|
import { createEvent } from '../libs/audit';
|
|
5
6
|
import { blocklet, wallet } from '../libs/auth';
|
|
6
7
|
import dayjs from '../libs/dayjs';
|
|
@@ -415,69 +416,113 @@ export const handlePayment = async (job: PaymentJob) => {
|
|
|
415
416
|
}
|
|
416
417
|
|
|
417
418
|
// try payment capture and reschedule on error
|
|
418
|
-
logger.info('PaymentIntent capture attempt', { id: paymentIntent.id });
|
|
419
|
+
logger.info('PaymentIntent capture attempt', { id: paymentIntent.id, attempt: invoice?.attempt_count });
|
|
419
420
|
let result;
|
|
420
421
|
try {
|
|
421
422
|
await paymentIntent.update({ status: 'processing', last_payment_error: null });
|
|
423
|
+
if (paymentMethod.type === 'arcblock') {
|
|
424
|
+
const client = paymentMethod.getOcapClient();
|
|
425
|
+
const payer = paymentSettings?.payment_method_options.arcblock?.payer;
|
|
426
|
+
|
|
427
|
+
// check balance before capture with transaction
|
|
428
|
+
result = await isDelegationSufficientForPayment({
|
|
429
|
+
paymentMethod,
|
|
430
|
+
paymentCurrency,
|
|
431
|
+
userDid: payer as string,
|
|
432
|
+
amount: paymentIntent.amount,
|
|
433
|
+
});
|
|
434
|
+
if (result.sufficient === false) {
|
|
435
|
+
logger.error('PaymentIntent capture aborted on preCheck', { id: paymentIntent.id, result });
|
|
436
|
+
throw new CustomError(result.reason, 'payer balance or delegation not sufficient for this payment');
|
|
437
|
+
}
|
|
422
438
|
|
|
423
|
-
|
|
424
|
-
|
|
439
|
+
// do the capture
|
|
440
|
+
const signed = await client.signTransferV2Tx({
|
|
441
|
+
tx: {
|
|
442
|
+
itx: {
|
|
443
|
+
to: wallet.address,
|
|
444
|
+
value: '0',
|
|
445
|
+
assets: [],
|
|
446
|
+
tokens: [{ address: paymentCurrency.contract, value: paymentIntent.amount }],
|
|
447
|
+
data: {
|
|
448
|
+
typeUrl: 'json',
|
|
449
|
+
// @ts-ignore
|
|
450
|
+
value: {
|
|
451
|
+
appId: wallet.address,
|
|
452
|
+
reason: invoice ? invoice.billing_reason : 'payment',
|
|
453
|
+
paymentIntentId: paymentIntent.id,
|
|
454
|
+
},
|
|
455
|
+
},
|
|
456
|
+
},
|
|
457
|
+
},
|
|
458
|
+
wallet,
|
|
459
|
+
delegator: payer,
|
|
460
|
+
});
|
|
461
|
+
// @ts-ignore
|
|
462
|
+
const { buffer } = await client.encodeTransferV2Tx({ tx: signed });
|
|
463
|
+
// @ts-ignore
|
|
464
|
+
const txHash = await client.sendTransferV2Tx({ tx: signed, wallet, delegator: payer }, getGasPayerExtra(buffer));
|
|
465
|
+
|
|
466
|
+
logger.info('PaymentIntent capture done', { id: paymentIntent.id, txHash });
|
|
467
|
+
|
|
468
|
+
await paymentIntent.update({
|
|
469
|
+
status: 'succeeded',
|
|
470
|
+
last_payment_error: null,
|
|
471
|
+
amount_received: paymentIntent.amount,
|
|
472
|
+
payment_details: {
|
|
473
|
+
arcblock: {
|
|
474
|
+
tx_hash: txHash,
|
|
475
|
+
payer: payer as string,
|
|
476
|
+
type: 'transfer',
|
|
477
|
+
},
|
|
478
|
+
},
|
|
479
|
+
});
|
|
425
480
|
|
|
426
|
-
|
|
427
|
-
result = await isDelegationSufficientForPayment({
|
|
428
|
-
paymentMethod,
|
|
429
|
-
paymentCurrency,
|
|
430
|
-
userDid: payer as string,
|
|
431
|
-
amount: paymentIntent.amount,
|
|
432
|
-
});
|
|
433
|
-
if (result.sufficient === false) {
|
|
434
|
-
logger.error('PaymentIntent capture aborted on preCheck', { id: paymentIntent.id, result });
|
|
435
|
-
throw new CustomError(result.reason, 'payer balance or delegation not sufficient for this payment');
|
|
481
|
+
await handlePaymentSucceed(paymentIntent);
|
|
436
482
|
}
|
|
437
483
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
484
|
+
if (paymentMethod.type === 'ethereum') {
|
|
485
|
+
if (!paymentCurrency.contract) {
|
|
486
|
+
throw new Error('Payment capture not supported for ethereum payment currencies without contract');
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const client = paymentMethod.getEvmClient();
|
|
490
|
+
const payer = paymentSettings?.payment_method_options.ethereum?.payer as string;
|
|
491
|
+
|
|
492
|
+
// check balance before capture with transaction
|
|
493
|
+
result = await isDelegationSufficientForPayment({
|
|
494
|
+
paymentMethod,
|
|
495
|
+
paymentCurrency,
|
|
496
|
+
userDid: payer as string,
|
|
497
|
+
amount: paymentIntent.amount,
|
|
498
|
+
});
|
|
499
|
+
if (result.sufficient === false) {
|
|
500
|
+
logger.error('PaymentIntent capture aborted on preCheck', { id: paymentIntent.id, result });
|
|
501
|
+
throw new CustomError(result.reason, 'payer balance or delegation not sufficient for this payment');
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// do the capture
|
|
505
|
+
const receipt = await transferErc20FromUser(client, paymentCurrency.contract, payer, paymentIntent.amount);
|
|
506
|
+
logger.info('PaymentIntent capture done', { id: paymentIntent.id, txHash: receipt.hash });
|
|
507
|
+
|
|
508
|
+
await paymentIntent.update({
|
|
509
|
+
status: 'succeeded',
|
|
510
|
+
last_payment_error: null,
|
|
511
|
+
amount_received: paymentIntent.amount,
|
|
512
|
+
payment_details: {
|
|
513
|
+
ethereum: {
|
|
514
|
+
tx_hash: receipt.hash,
|
|
515
|
+
payer: payer as string,
|
|
516
|
+
block_height: receipt.blockNumber.toString(),
|
|
517
|
+
gas_used: receipt.gasUsed.toString(),
|
|
518
|
+
gas_price: receipt.gasPrice.toString(),
|
|
519
|
+
type: 'transfer',
|
|
454
520
|
},
|
|
455
521
|
},
|
|
456
|
-
}
|
|
457
|
-
wallet,
|
|
458
|
-
delegator: payer,
|
|
459
|
-
});
|
|
460
|
-
// @ts-ignore
|
|
461
|
-
const { buffer } = await client.encodeTransferV2Tx({ tx: signed });
|
|
462
|
-
// @ts-ignore
|
|
463
|
-
const txHash = await client.sendTransferV2Tx({ tx: signed, wallet, delegator: payer }, getGasPayerExtra(buffer));
|
|
464
|
-
|
|
465
|
-
logger.info('PaymentIntent capture done', { id: paymentIntent.id, txHash });
|
|
466
|
-
|
|
467
|
-
await paymentIntent.update({
|
|
468
|
-
status: 'succeeded',
|
|
469
|
-
last_payment_error: null,
|
|
470
|
-
amount_received: paymentIntent.amount,
|
|
471
|
-
payment_details: {
|
|
472
|
-
arcblock: {
|
|
473
|
-
tx_hash: txHash,
|
|
474
|
-
payer: payer as string,
|
|
475
|
-
type: 'transfer',
|
|
476
|
-
},
|
|
477
|
-
},
|
|
478
|
-
});
|
|
522
|
+
});
|
|
479
523
|
|
|
480
|
-
|
|
524
|
+
await handlePaymentSucceed(paymentIntent);
|
|
525
|
+
}
|
|
481
526
|
} catch (err) {
|
|
482
527
|
logger.error('PaymentIntent capture failed', { error: err, id: paymentIntent.id });
|
|
483
528
|
|