payment-kit 1.13.214 → 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.
Files changed (53) hide show
  1. package/api/src/crons/index.ts +1 -1
  2. package/api/src/index.ts +1 -1
  3. package/api/src/integrations/{blockchain → arcblock}/stake.ts +19 -12
  4. package/api/src/integrations/ethereum/token.ts +141 -0
  5. package/api/src/integrations/ethereum/tx.ts +32 -0
  6. package/api/src/libs/auth.ts +1 -0
  7. package/api/src/libs/payment.ts +76 -30
  8. package/api/src/queues/checkout-session.ts +1 -1
  9. package/api/src/queues/payment.ts +99 -54
  10. package/api/src/queues/refund.ts +84 -44
  11. package/api/src/routes/connect/change-payment.ts +54 -16
  12. package/api/src/routes/connect/change-plan.ts +55 -13
  13. package/api/src/routes/connect/collect-batch.ts +0 -4
  14. package/api/src/routes/connect/collect.ts +77 -30
  15. package/api/src/routes/connect/pay.ts +56 -12
  16. package/api/src/routes/connect/setup.ts +92 -48
  17. package/api/src/routes/connect/shared.ts +107 -79
  18. package/api/src/routes/connect/subscribe.ts +68 -23
  19. package/api/src/routes/customers.ts +1 -1
  20. package/api/src/routes/invoices.ts +14 -8
  21. package/api/src/routes/payment-currencies.ts +112 -1
  22. package/api/src/routes/payment-methods.ts +81 -3
  23. package/api/src/store/migrations/20230911-seeding.ts +2 -2
  24. package/api/src/store/migrations/20240408-payout.ts +0 -2
  25. package/api/src/store/models/customer.ts +1 -0
  26. package/api/src/store/models/payment-intent.ts +1 -0
  27. package/api/src/store/models/payment-link.ts +1 -1
  28. package/api/src/store/models/payment-method.ts +22 -1
  29. package/api/src/store/models/types.ts +7 -3
  30. package/api/third.d.ts +2 -0
  31. package/blocklet.yml +1 -1
  32. package/package.json +7 -4
  33. package/public/methods/ethereum.png +0 -0
  34. package/src/components/drawer-form.tsx +17 -6
  35. package/src/components/invoice/list.tsx +20 -13
  36. package/src/components/payment-currency/add.tsx +60 -0
  37. package/src/components/payment-currency/form.tsx +51 -0
  38. package/src/components/payment-method/bitcoin.tsx +1 -1
  39. package/src/components/payment-method/ethereum.tsx +16 -8
  40. package/src/components/payment-method/form.tsx +1 -3
  41. package/src/components/subscription/items/usage-records.tsx +20 -38
  42. package/src/locales/en.tsx +33 -0
  43. package/src/locales/zh.tsx +33 -0
  44. package/src/pages/admin/billing/subscriptions/detail.tsx +1 -1
  45. package/src/pages/admin/settings/index.tsx +4 -4
  46. package/src/pages/admin/settings/payment-methods/create.tsx +3 -2
  47. package/src/pages/admin/settings/payment-methods/index.tsx +57 -3
  48. package/src/pages/customer/invoice/past-due.tsx +1 -0
  49. package/src/pages/customer/refund/list.tsx +9 -13
  50. package/src/pages/customer/subscription/change-payment.tsx +1 -0
  51. package/src/pages/customer/subscription/change-plan.tsx +1 -0
  52. package/src/pages/customer/subscription/detail.tsx +2 -2
  53. /package/api/src/integrations/{blockchain → arcblock}/nft.ts +0 -0
@@ -1,6 +1,6 @@
1
1
  import Cron from '@abtnode/cron';
2
2
 
3
- import { checkStakeRevokeTx } from '../integrations/blockchain/stake';
3
+ import { checkStakeRevokeTx } from '../integrations/arcblock/stake';
4
4
  import {
5
5
  batchHandleStripeInvoices,
6
6
  batchHandleStripePayments,
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/blockchain/stake';
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: any) => {
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
- const currency = method.payment_currencies.find((c: any) => t.address === c.contract);
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: any) => {
215
- const client = method.getOcapClient();
216
- const { tokens } = await client.getAccountTokens({ address: did });
217
- (tokens || []).forEach((t: any) => {
218
- const currency = method.payment_currencies.find((c: any) => t.address === c.contract);
219
- if (currency) {
220
- results[currency.id] = t.balance;
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
+ }
@@ -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,
@@ -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
- // If we never delegated before
227
- if (!state) {
228
- return [entry];
229
- }
247
+ if (paymentMethod.type === 'arcblock') {
248
+ const client = paymentMethod.getOcapClient();
249
+ const { state } = await client.getDelegateState({ address });
230
250
 
231
- // If we have metered items, we should not limit tx allowance(set to 0)
232
- if (hasMetered) {
233
- return [entry];
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
- const op = (state as DelegateState).ops.find((x) => x.key === OCAP_PAYMENT_TX_TYPE);
237
- if (op && Array.isArray(op.value.limit?.tokens) && op.value.limit.tokens.length > 0) {
238
- const tokenLimits = cloneDeep(op.value.limit.tokens);
239
- const index = op.value.limit.tokens.findIndex((x) => x.address === paymentCurrency.contract);
240
- // we are updating an existing token limit
241
- if (index > -1) {
242
- const limit = op.value.limit.tokens[index] as TokenLimit;
243
- // If we have a previous delegation and the txAllowance is smaller than requested amount
244
- // If txAllowance is 0 (unlimited), we should not update it
245
- if (limit.txAllowance !== '0' && new BN(limit.txAllowance).lt(new BN(amount))) {
246
- tokenLimits[index] = entry;
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
- } else {
249
- // we are adding a new token limit
250
- tokenLimits.push(entry);
277
+
278
+ return tokenLimits;
251
279
  }
252
280
 
253
- return tokenLimits;
281
+ return [entry];
254
282
  }
255
283
 
256
- return [entry];
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 isBalanceSufficientForPayment(args: {
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/blockchain/nft';
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/blockchain/stake';
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
- const client = paymentMethod.getOcapClient();
424
- const payer = paymentSettings?.payment_method_options.arcblock?.payer;
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
- // check balance before capture with transaction
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
- // do the capture
439
- const signed = await client.signTransferV2Tx({
440
- tx: {
441
- itx: {
442
- to: wallet.address,
443
- value: '0',
444
- assets: [],
445
- tokens: [{ address: paymentCurrency.contract, value: paymentIntent.amount }],
446
- data: {
447
- typeUrl: 'json',
448
- // @ts-ignore
449
- value: {
450
- appId: wallet.address,
451
- reason: invoice ? invoice.billing_reason : 'payment',
452
- paymentIntentId: paymentIntent.id,
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
- await handlePaymentSucceed(paymentIntent);
524
+ await handlePaymentSucceed(paymentIntent);
525
+ }
481
526
  } catch (err) {
482
527
  logger.error('PaymentIntent capture failed', { error: err, id: paymentIntent.id });
483
528