From 19ff0892d23fb2b14d597c6ff7193e6969adec82 Mon Sep 17 00:00:00 2001 From: David May <85513542+davidleomay@users.noreply.github.com> Date: Wed, 11 Mar 2026 12:04:57 +0100 Subject: [PATCH 1/6] feat: moved chain IDs to transaction DTO (#3389) --- .../history/mappers/transaction-dto.mapper.ts | 28 +++++++++++++++++++ .../webhook/dto/payment-webhook.dto.ts | 17 +---------- .../webhook/mapper/webhook-data.mapper.ts | 28 ------------------- .../supporting/payment/dto/transaction.dto.ts | 15 ++++++++++ 4 files changed, 44 insertions(+), 44 deletions(-) diff --git a/src/subdomains/core/history/mappers/transaction-dto.mapper.ts b/src/subdomains/core/history/mappers/transaction-dto.mapper.ts index bafbfff669..3d6eabb46c 100644 --- a/src/subdomains/core/history/mappers/transaction-dto.mapper.ts +++ b/src/subdomains/core/history/mappers/transaction-dto.mapper.ts @@ -109,10 +109,18 @@ export class TransactionDtoMapper { } static mapBuyCryptoTransactionDetail(buyCrypto: BuyCryptoExtended): TransactionDetailDto { + const inputAsset = isAsset(buyCrypto.inputAssetEntity) ? buyCrypto.inputAssetEntity : null; + const outputAsset = buyCrypto.outputAsset; + return { ...this.mapBuyCryptoTransaction(buyCrypto), sourceAccount: buyCrypto.bankTx?.iban, targetAccount: buyCrypto.user?.address, + sourceChainId: inputAsset?.chainId ?? null, + destinationChainId: outputAsset?.chainId ?? null, + sourceEvmChainId: inputAsset?.evmChainId ?? null, + destinationEvmChainId: outputAsset?.evmChainId ?? null, + depositAddress: buyCrypto.cryptoInput?.address?.address ?? null, }; } @@ -172,10 +180,17 @@ export class TransactionDtoMapper { } static mapBuyFiatTransactionDetail(buyFiat: BuyFiatExtended): TransactionDetailDto { + const inputAsset = isAsset(buyFiat.inputAssetEntity) ? buyFiat.inputAssetEntity : null; + return { ...this.mapBuyFiatTransaction(buyFiat), sourceAccount: null, targetAccount: buyFiat.bankTx?.iban, + sourceChainId: inputAsset?.chainId ?? null, + destinationChainId: null, + sourceEvmChainId: inputAsset?.evmChainId ?? null, + destinationEvmChainId: null, + depositAddress: buyFiat.cryptoInput?.address?.address ?? null, }; } @@ -225,10 +240,18 @@ export class TransactionDtoMapper { } static mapTxRequestTransactionDetail(txRequest: TransactionRequestExtended): TransactionDetailDto { + const sourceAsset = isAsset(txRequest.sourceAssetEntity) ? txRequest.sourceAssetEntity : null; + const targetAsset = isAsset(txRequest.targetAssetEntity) ? txRequest.targetAssetEntity : null; + return { ...this.mapTxRequestTransaction(txRequest), sourceAccount: null, targetAccount: txRequest.route.targetAccount, + sourceChainId: sourceAsset?.chainId ?? null, + destinationChainId: targetAsset?.chainId ?? null, + sourceEvmChainId: sourceAsset?.evmChainId ?? null, + destinationEvmChainId: targetAsset?.evmChainId ?? null, + depositAddress: null, }; } @@ -286,6 +309,11 @@ export class TransactionDtoMapper { ...this.mapReferralReward(refReward), sourceAccount: null, targetAccount: refReward.user?.address, + sourceChainId: null, + destinationChainId: refReward.outputAsset?.chainId ?? null, + sourceEvmChainId: null, + destinationEvmChainId: refReward.outputAsset?.evmChainId ?? null, + depositAddress: null, }; } diff --git a/src/subdomains/generic/user/services/webhook/dto/payment-webhook.dto.ts b/src/subdomains/generic/user/services/webhook/dto/payment-webhook.dto.ts index 9db5e34717..de7a960d90 100644 --- a/src/subdomains/generic/user/services/webhook/dto/payment-webhook.dto.ts +++ b/src/subdomains/generic/user/services/webhook/dto/payment-webhook.dto.ts @@ -1,4 +1,4 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { ApiProperty } from '@nestjs/swagger'; import { TransactionDetailDto } from 'src/subdomains/supporting/payment/dto/transaction.dto'; import { WebhookDto, WebhookType } from './webhook.dto'; @@ -21,21 +21,6 @@ export enum PaymentWebhookState { export class PaymentWebhookData extends TransactionDetailDto { @ApiProperty() dfxReference: number; - - @ApiPropertyOptional({ description: 'Source token contract address' }) - sourceChainId?: string; - - @ApiPropertyOptional({ description: 'Destination token contract address' }) - destinationChainId?: string; - - @ApiPropertyOptional({ description: 'Source EVM chain ID (e.g. 1, 56, 137)' }) - sourceEvmChainId?: number; - - @ApiPropertyOptional({ description: 'Destination EVM chain ID (e.g. 1, 56, 137)' }) - destinationEvmChainId?: number; - - @ApiPropertyOptional({ description: 'Deposit address for crypto inputs' }) - depositAddress?: string; } export class PaymentWebhookDto extends WebhookDto { diff --git a/src/subdomains/generic/user/services/webhook/mapper/webhook-data.mapper.ts b/src/subdomains/generic/user/services/webhook/mapper/webhook-data.mapper.ts index ee020c9a4b..2739439681 100644 --- a/src/subdomains/generic/user/services/webhook/mapper/webhook-data.mapper.ts +++ b/src/subdomains/generic/user/services/webhook/mapper/webhook-data.mapper.ts @@ -1,4 +1,3 @@ -import { Asset } from 'src/shared/models/asset/asset.entity'; import { CountryDtoMapper } from 'src/shared/models/country/dto/country-dto.mapper'; import { BuyCryptoExtended, @@ -33,16 +32,9 @@ export class WebhookDataMapper { } static mapCryptoFiatData(payment: BuyFiatExtended): PaymentWebhookData { - const inputAsset = payment.inputAssetEntity as Asset; - return { ...TransactionDtoMapper.mapBuyFiatTransactionDetail(payment), dfxReference: payment.id, - sourceChainId: inputAsset.chainId, - destinationChainId: null, - sourceEvmChainId: inputAsset.evmChainId ?? null, - destinationEvmChainId: null, - depositAddress: payment.cryptoInput?.address?.address ?? null, }; } @@ -50,40 +42,20 @@ export class WebhookDataMapper { return { ...TransactionDtoMapper.mapBuyFiatTransactionDetail(payment), dfxReference: payment.id, - sourceChainId: null, - destinationChainId: null, - sourceEvmChainId: null, - destinationEvmChainId: null, - depositAddress: null, }; } static mapCryptoCryptoData(payment: BuyCryptoExtended): PaymentWebhookData { - const inputAsset = payment.inputAssetEntity as Asset; - const outputAsset = payment.outputAsset; - return { ...TransactionDtoMapper.mapBuyCryptoTransactionDetail(payment), dfxReference: payment.id, - sourceChainId: inputAsset.chainId, - destinationChainId: outputAsset?.chainId ?? null, - sourceEvmChainId: inputAsset.evmChainId ?? null, - destinationEvmChainId: outputAsset?.evmChainId ?? null, - depositAddress: payment.cryptoInput?.address?.address ?? null, }; } static mapFiatCryptoData(payment: BuyCryptoExtended): PaymentWebhookData { - const outputAsset = payment.outputAsset; - return { ...TransactionDtoMapper.mapBuyCryptoTransactionDetail(payment), dfxReference: payment.id, - sourceChainId: null, - destinationChainId: outputAsset?.chainId ?? null, - sourceEvmChainId: null, - destinationEvmChainId: outputAsset?.evmChainId ?? null, - depositAddress: null, }; } diff --git a/src/subdomains/supporting/payment/dto/transaction.dto.ts b/src/subdomains/supporting/payment/dto/transaction.dto.ts index 72a0fc48e1..dbefea79d2 100644 --- a/src/subdomains/supporting/payment/dto/transaction.dto.ts +++ b/src/subdomains/supporting/payment/dto/transaction.dto.ts @@ -256,6 +256,21 @@ export class TransactionDetailDto extends TransactionDto { @ApiPropertyOptional() targetAccount?: string; + + @ApiPropertyOptional({ description: 'Contract address of the source asset (for tokens)' }) + sourceChainId?: string; + + @ApiPropertyOptional({ description: 'Contract address of the destination asset (for tokens)' }) + destinationChainId?: string; + + @ApiPropertyOptional({ description: 'EVM chain ID of the source asset (e.g. 1 for Ethereum)' }) + sourceEvmChainId?: number; + + @ApiPropertyOptional({ description: 'EVM chain ID of the destination asset (e.g. 1 for Ethereum)' }) + destinationEvmChainId?: number; + + @ApiPropertyOptional({ description: 'Deposit address for crypto input transactions' }) + depositAddress?: string; } export class TransactionTarget { From 9292042dc0b0e4f4b2df5dc9dc1159c8ab44487c Mon Sep 17 00:00:00 2001 From: David May <85513542+davidleomay@users.noreply.github.com> Date: Wed, 11 Mar 2026 17:07:35 +0100 Subject: [PATCH 2/6] feat: moved chain IDs to transaction base DTO (#3394) --- .../history/mappers/transaction-dto.mapper.ts | 59 +++++++++---------- .../supporting/payment/dto/transaction.dto.ts | 16 ++--- 2 files changed, 37 insertions(+), 38 deletions(-) diff --git a/src/subdomains/core/history/mappers/transaction-dto.mapper.ts b/src/subdomains/core/history/mappers/transaction-dto.mapper.ts index 3d6eabb46c..48f500ccd9 100644 --- a/src/subdomains/core/history/mappers/transaction-dto.mapper.ts +++ b/src/subdomains/core/history/mappers/transaction-dto.mapper.ts @@ -45,6 +45,9 @@ export class TransactionRequestExtended extends TransactionRequest { export class TransactionDtoMapper { // BuyCrypto static mapBuyCryptoTransaction(buyCrypto: BuyCryptoExtended): TransactionDto { + const inputAsset = isAsset(buyCrypto.inputAssetEntity) ? buyCrypto.inputAssetEntity : null; + const outputAsset = buyCrypto.outputAsset; + const dto: TransactionDto = { id: buyCrypto.transaction.id, uid: buyCrypto.transaction.uid, @@ -103,24 +106,21 @@ export class TransactionDtoMapper { asset: buyCrypto.networkStartAsset, } : null, + sourceChainId: inputAsset?.chainId ?? null, + destinationChainId: outputAsset?.chainId ?? null, + sourceEvmChainId: inputAsset?.evmChainId ?? null, + destinationEvmChainId: outputAsset?.evmChainId ?? null, + depositAddress: buyCrypto.cryptoInput?.address?.address ?? null, }; return Object.assign(new TransactionDto(), dto); } static mapBuyCryptoTransactionDetail(buyCrypto: BuyCryptoExtended): TransactionDetailDto { - const inputAsset = isAsset(buyCrypto.inputAssetEntity) ? buyCrypto.inputAssetEntity : null; - const outputAsset = buyCrypto.outputAsset; - return { ...this.mapBuyCryptoTransaction(buyCrypto), sourceAccount: buyCrypto.bankTx?.iban, targetAccount: buyCrypto.user?.address, - sourceChainId: inputAsset?.chainId ?? null, - destinationChainId: outputAsset?.chainId ?? null, - sourceEvmChainId: inputAsset?.evmChainId ?? null, - destinationEvmChainId: outputAsset?.evmChainId ?? null, - depositAddress: buyCrypto.cryptoInput?.address?.address ?? null, }; } @@ -130,6 +130,8 @@ export class TransactionDtoMapper { // BuyFiat static mapBuyFiatTransaction(buyFiat: BuyFiatExtended): TransactionDto { + const inputAsset = isAsset(buyFiat.inputAssetEntity) ? buyFiat.inputAssetEntity : null; + const dto: TransactionDto = { id: buyFiat.transaction.id, uid: buyFiat.transaction.uid, @@ -174,23 +176,21 @@ export class TransactionDtoMapper { chargebackDate: buyFiat.chargebackDate, date: buyFiat.transaction.created, externalTransactionId: buyFiat.transaction.externalId, + sourceChainId: inputAsset?.chainId ?? null, + destinationChainId: null, + sourceEvmChainId: inputAsset?.evmChainId ?? null, + destinationEvmChainId: null, + depositAddress: buyFiat.cryptoInput?.address?.address ?? null, }; return Object.assign(new TransactionDto(), dto); } static mapBuyFiatTransactionDetail(buyFiat: BuyFiatExtended): TransactionDetailDto { - const inputAsset = isAsset(buyFiat.inputAssetEntity) ? buyFiat.inputAssetEntity : null; - return { ...this.mapBuyFiatTransaction(buyFiat), sourceAccount: null, targetAccount: buyFiat.bankTx?.iban, - sourceChainId: inputAsset?.chainId ?? null, - destinationChainId: null, - sourceEvmChainId: inputAsset?.evmChainId ?? null, - destinationEvmChainId: null, - depositAddress: buyFiat.cryptoInput?.address?.address ?? null, }; } @@ -201,6 +201,8 @@ export class TransactionDtoMapper { // Waiting TxRequest static mapTxRequestTransaction(txRequest: TransactionRequestExtended): TransactionDto { const fees = TransactionDtoMapper.mapFees(txRequest); + const sourceAsset = isAsset(txRequest.sourceAssetEntity) ? txRequest.sourceAssetEntity : null; + const targetAsset = isAsset(txRequest.targetAssetEntity) ? txRequest.targetAssetEntity : null; const dto: TransactionDto = { id: null, @@ -210,12 +212,12 @@ export class TransactionDtoMapper { inputAmount: Util.roundReadable(txRequest.amount, amountType(txRequest.sourceAssetEntity)), inputAsset: txRequest.sourceAssetEntity.name, inputAssetId: txRequest.sourceAssetEntity.id, - inputBlockchain: isAsset(txRequest.sourceAssetEntity) ? txRequest.sourceAssetEntity.blockchain : null, + inputBlockchain: sourceAsset?.blockchain ?? null, inputPaymentMethod: txRequest.sourcePaymentMethod, outputAmount: null, outputAsset: txRequest.targetAssetEntity?.name, outputAssetId: txRequest.targetAssetEntity?.id, - outputBlockchain: isAsset(txRequest.targetAssetEntity) ? txRequest.targetAssetEntity?.blockchain : null, + outputBlockchain: targetAsset?.blockchain ?? null, outputPaymentMethod: txRequest.targetPaymentMethod, priceSteps: null, feeAmount: fees?.total, @@ -234,24 +236,21 @@ export class TransactionDtoMapper { date: txRequest.created, externalTransactionId: null, networkStartTx: null, + sourceChainId: sourceAsset?.chainId ?? null, + destinationChainId: targetAsset?.chainId ?? null, + sourceEvmChainId: sourceAsset?.evmChainId ?? null, + destinationEvmChainId: targetAsset?.evmChainId ?? null, + depositAddress: null, }; return Object.assign(new TransactionDto(), dto); } static mapTxRequestTransactionDetail(txRequest: TransactionRequestExtended): TransactionDetailDto { - const sourceAsset = isAsset(txRequest.sourceAssetEntity) ? txRequest.sourceAssetEntity : null; - const targetAsset = isAsset(txRequest.targetAssetEntity) ? txRequest.targetAssetEntity : null; - return { ...this.mapTxRequestTransaction(txRequest), sourceAccount: null, targetAccount: txRequest.route.targetAccount, - sourceChainId: sourceAsset?.chainId ?? null, - destinationChainId: targetAsset?.chainId ?? null, - sourceEvmChainId: sourceAsset?.evmChainId ?? null, - destinationEvmChainId: targetAsset?.evmChainId ?? null, - depositAddress: null, }; } @@ -299,6 +298,11 @@ export class TransactionDtoMapper { chargebackTxUrl: undefined, chargebackDate: undefined, date: refReward.transaction.created, + sourceChainId: null, + destinationChainId: refReward.outputAsset?.chainId ?? null, + sourceEvmChainId: null, + destinationEvmChainId: refReward.outputAsset?.evmChainId ?? null, + depositAddress: null, }; return Object.assign(new TransactionDto(), dto); @@ -309,11 +313,6 @@ export class TransactionDtoMapper { ...this.mapReferralReward(refReward), sourceAccount: null, targetAccount: refReward.user?.address, - sourceChainId: null, - destinationChainId: refReward.outputAsset?.chainId ?? null, - sourceEvmChainId: null, - destinationEvmChainId: refReward.outputAsset?.evmChainId ?? null, - depositAddress: null, }; } diff --git a/src/subdomains/supporting/payment/dto/transaction.dto.ts b/src/subdomains/supporting/payment/dto/transaction.dto.ts index dbefea79d2..2964927e19 100644 --- a/src/subdomains/supporting/payment/dto/transaction.dto.ts +++ b/src/subdomains/supporting/payment/dto/transaction.dto.ts @@ -248,14 +248,6 @@ export class TransactionDto extends UnassignedTransactionDto { @ApiPropertyOptional({ type: NetworkStartTxDto }) networkStartTx?: NetworkStartTxDto; -} - -export class TransactionDetailDto extends TransactionDto { - @ApiPropertyOptional() - sourceAccount?: string; - - @ApiPropertyOptional() - targetAccount?: string; @ApiPropertyOptional({ description: 'Contract address of the source asset (for tokens)' }) sourceChainId?: string; @@ -273,6 +265,14 @@ export class TransactionDetailDto extends TransactionDto { depositAddress?: string; } +export class TransactionDetailDto extends TransactionDto { + @ApiPropertyOptional() + sourceAccount?: string; + + @ApiPropertyOptional() + targetAccount?: string; +} + export class TransactionTarget { @ApiProperty() id: number; From 34de4d47d3b72ebbcd9be8e07db1a8b11b6a4c60 Mon Sep 17 00:00:00 2001 From: Lam Nguyen <32935491+xlamn@users.noreply.github.com> Date: Wed, 11 Mar 2026 18:03:56 +0100 Subject: [PATCH 3/6] fix: remove vat and total from multi tx pdf (#3393) * fix: remove vat and total from multi tx pdf. * chore: warning. --- .../payment/services/swiss-qr.service.ts | 40 ------------------- 1 file changed, 40 deletions(-) diff --git a/src/subdomains/supporting/payment/services/swiss-qr.service.ts b/src/subdomains/supporting/payment/services/swiss-qr.service.ts index c46b7bd483..30174e197c 100644 --- a/src/subdomains/supporting/payment/services/swiss-qr.service.ts +++ b/src/subdomains/supporting/payment/services/swiss-qr.service.ts @@ -458,7 +458,6 @@ export class SwissQRService { const sellTransactions = tableDataWithType.filter((t) => t.type === TransactionType.SELL); const buyTotal = buyTransactions.reduce((sum, t) => sum + t.data.fiatAmount, 0); const sellTotal = sellTransactions.reduce((sum, t) => sum + t.data.fiatAmount, 0); - const grandTotal = sellTotal - buyTotal; const rows: PDFRow[] = []; @@ -579,45 +578,6 @@ export class SwissQRService { }); } - rows.push({ - columns: [{ text: '' }], - height: 10, - }); - - rows.push({ - columns: [ - { text: '', width: mm2pt(30) }, - { text: this.translate('invoice.table.vat_row.vat_label', language) }, - { text: '', width: mm2pt(25) }, - { text: '0%', width: mm2pt(30) }, - ], - padding: 5, - }); - - rows.push({ - columns: [ - { text: '', width: mm2pt(30) }, - { text: this.translate('invoice.table.vat_row.vat_amount_label', language) }, - { text: '', width: mm2pt(25) }, - { text: `${billData.currency} 0.00`, width: mm2pt(30) }, - ], - padding: 5, - }); - - rows.push({ - columns: [ - { text: '', width: mm2pt(30) }, - { - fontName: 'Helvetica-Bold', - text: this.translate('invoice.table.invoice_total_row.invoice_total_label', language), - }, - { text: '', width: mm2pt(25) }, - { fontName: 'Helvetica-Bold', text: `${billData.currency} ${grandTotal.toFixed(2)}`, width: mm2pt(30) }, - ], - height: 40, - padding: 5, - }); - if (!skipTermsAndConditions) { rows.push({ columns: [this.getTermsAndConditions(language)] }); } From 0a98d67fd7d23df4ab604b9bb6cbda898738279e Mon Sep 17 00:00:00 2001 From: bernd2022 <104787072+bernd2022@users.noreply.github.com> Date: Wed, 11 Mar 2026 19:33:16 +0100 Subject: [PATCH 4/6] fix: ICRC token scanning improvements (#3375) * fix: ICRC token scanning bugs and robustness improvements - Fix batch loop break condition to use raw transaction count instead of filtered transfers, preventing skipped blocks with mint/burn/approve txs - Pass fromBlock/toBlock through to ICRC token scanning for correct pollAddress behavior - Reject assets with missing decimals instead of silently falling back to 8 - Reduce verbose logging by removing full address list dump every minute * feat: unified global scanning + settings persistence for ICP register strategy Replace per-address Rosetta calls with global query_blocks scan for native ICP transfers. Persist scan progress in settings table per canister ID for both native ICP and ICRC tokens. Unify both code paths with consistent batched scanning, client-side filtering, and cold-start handling. * test: update register strategy test for SettingService dependency * refactor: consolidate ICP scan state into single JSON setting Replace 5 individual setting entries (icpLastScannedBlock + one per canister) with a single icpLastScannedBlocks JSON entry using getObj/setObj. * refactor: use asset ID as scan state key instead of canister ID * refactor: deduplicate native and ICRC scan logic Extract shared fetchTransfersBatched (batch loop) and scanAndCollect (cold-start + persistence) to eliminate duplication between native ICP and ICRC token scanning paths. * chore: refactoring --------- Co-authored-by: David May --- src/integration/blockchain/icp/dto/icp.dto.ts | 1 + src/integration/blockchain/icp/icp-client.ts | 20 ++- .../__tests__/register.registry.spec.ts | 5 +- .../strategies/register/impl/icp.strategy.ts | 160 ++++++++++++------ 4 files changed, 130 insertions(+), 56 deletions(-) diff --git a/src/integration/blockchain/icp/dto/icp.dto.ts b/src/integration/blockchain/icp/dto/icp.dto.ts index 4eeed3cead..83054b47ff 100644 --- a/src/integration/blockchain/icp/dto/icp.dto.ts +++ b/src/integration/blockchain/icp/dto/icp.dto.ts @@ -12,6 +12,7 @@ export interface IcpTransferQueryResult { transfers: IcpTransfer[]; lastBlockIndex: number; chainLength: number; + rawTransactionCount: number; } // --- Candid query_blocks response types (ICP native ledger) --- diff --git a/src/integration/blockchain/icp/icp-client.ts b/src/integration/blockchain/icp/icp-client.ts index 8c66ac0087..42885101db 100644 --- a/src/integration/blockchain/icp/icp-client.ts +++ b/src/integration/blockchain/icp/icp-client.ts @@ -188,7 +188,7 @@ export class InternetComputerClient extends BlockchainClient { lastIndex = start - 1; } - return { transfers, lastBlockIndex: lastIndex, chainLength }; + return { transfers, lastBlockIndex: lastIndex, chainLength, rawTransactionCount: response.blocks.length }; } private mapBlockToTransfer(block: CandidBlock, index: number): IcpTransfer | undefined { @@ -277,9 +277,23 @@ export class InternetComputerClient extends BlockchainClient { if (transfer) transfers.push(transfer); } - const lastIndex = response.transactions.length > 0 ? firstIndex + response.transactions.length - 1 : start - 1; + let lastIndex: number; - return { transfers, lastBlockIndex: lastIndex, chainLength: Number(response.log_length) }; + if (response.transactions.length > 0) { + lastIndex = firstIndex + response.transactions.length - 1; + } else if (firstIndex > start) { + lastIndex = firstIndex - 1; + this.logger.info(`Skipping archived ICRC blocks ${start}-${lastIndex}, next query starts at ${firstIndex}`); + } else { + lastIndex = start - 1; + } + + return { + transfers, + lastBlockIndex: lastIndex, + chainLength: Number(response.log_length), + rawTransactionCount: response.transactions.length, + }; } private mapIcrcTransaction(tx: CandidIcrcTransaction, index: number, decimals: number): IcpTransfer | undefined { diff --git a/src/subdomains/supporting/payin/strategies/register/__tests__/register.registry.spec.ts b/src/subdomains/supporting/payin/strategies/register/__tests__/register.registry.spec.ts index 914b3c34c8..c24112557a 100644 --- a/src/subdomains/supporting/payin/strategies/register/__tests__/register.registry.spec.ts +++ b/src/subdomains/supporting/payin/strategies/register/__tests__/register.registry.spec.ts @@ -7,7 +7,8 @@ import { TronService } from 'src/integration/blockchain/tron/services/tron.servi import { TatumWebhookService } from 'src/integration/tatum/services/tatum-webhook.service'; import { createCustomAsset } from 'src/shared/models/asset/__mocks__/asset.entity.mock'; import { RepositoryFactory } from 'src/shared/repositories/repository.factory'; -import { TransactionRequestService } from 'src/subdomains/supporting/payment/services/transaction-request.service'; +import { SettingService } from 'src/shared/models/setting/setting.service'; +import { DepositService } from 'src/subdomains/supporting/address-pool/deposit/deposit.service'; import { PayInBitcoinService } from '../../../services/payin-bitcoin.service'; import { PayInInternetComputerService } from '../../../services/payin-icp.service'; import { PayInMoneroService } from '../../../services/payin-monero.service'; @@ -82,7 +83,7 @@ describe('RegisterStrategyRegistry', () => { (ConfigModule as Record).Config = { payment: { internetComputerSeed: 'test' } }; jest.spyOn(InternetComputerUtil, 'createWallet').mockReturnValue({ address: 'test-principal' } as never); jest.spyOn(InternetComputerUtil, 'accountIdentifier').mockReturnValue('test-account-id'); - icpStrategy = new IcpStrategy(mock(), mock()); + icpStrategy = new IcpStrategy(mock(), mock(), mock()); registry = new RegisterStrategyRegistryWrapper( bitcoinStrategy, diff --git a/src/subdomains/supporting/payin/strategies/register/impl/icp.strategy.ts b/src/subdomains/supporting/payin/strategies/register/impl/icp.strategy.ts index 07b5eac0d1..d1e299f85b 100644 --- a/src/subdomains/supporting/payin/strategies/register/impl/icp.strategy.ts +++ b/src/subdomains/supporting/payin/strategies/register/impl/icp.strategy.ts @@ -3,18 +3,21 @@ import { CronExpression } from '@nestjs/schedule'; import { Config } from 'src/config/config'; import { InternetComputerUtil } from 'src/integration/blockchain/icp/icp.util'; import { Blockchain } from 'src/integration/blockchain/shared/enums/blockchain.enum'; +import { Asset, AssetType } from 'src/shared/models/asset/asset.entity'; import { BlockchainAddress } from 'src/shared/models/blockchain-address'; +import { SettingService } from 'src/shared/models/setting/setting.service'; import { DfxLogger } from 'src/shared/services/dfx-logger'; import { Process } from 'src/shared/services/process.service'; import { DfxCron } from 'src/shared/utils/cron'; -import { Util } from 'src/shared/utils/util'; -import { TransactionRequestService } from 'src/subdomains/supporting/payment/services/transaction-request.service'; -import { Not, Like } from 'typeorm'; +import { DepositService } from 'src/subdomains/supporting/address-pool/deposit/deposit.service'; import { PayInType } from '../../../entities/crypto-input.entity'; import { PayInEntry } from '../../../interfaces'; import { PayInInternetComputerService } from '../../../services/payin-icp.service'; import { RegisterStrategy } from './base/register.strategy'; +const BATCH_SIZE = 2000; +const SETTING_KEY = 'icpLastScannedBlocks'; + @Injectable() export class InternetComputerStrategy extends RegisterStrategy { protected readonly logger = new DfxLogger(InternetComputerStrategy); @@ -23,7 +26,8 @@ export class InternetComputerStrategy extends RegisterStrategy { constructor( private readonly payInInternetComputerService: PayInInternetComputerService, - private readonly transactionRequestService: TransactionRequestService, + private readonly depositService: DepositService, + private readonly settingService: SettingService, ) { super(); @@ -38,90 +42,144 @@ export class InternetComputerStrategy extends RegisterStrategy { //*** JOBS ***// @DfxCron(CronExpression.EVERY_MINUTE, { process: Process.PAY_IN, timeout: 7200 }) async checkPayInEntries(): Promise { - const activeDepositAddresses = await this.transactionRequestService.getActiveDepositAddresses( - Util.hoursBefore(1), - this.blockchain, - ); + const allDeposits = await this.depositService.getUsedDepositsByBlockchain(this.blockchain); + const allDepositAddresses = allDeposits.map((d) => d.address); - if (this.paymentAddress) activeDepositAddresses.push(this.paymentAddress); + if (this.paymentAddress && !allDepositAddresses.includes(this.paymentAddress)) { + allDepositAddresses.push(this.paymentAddress); + } - await this.processNewPayInEntries(activeDepositAddresses.map((a) => BlockchainAddress.create(a, this.blockchain))); + await this.processNewPayInEntries(allDepositAddresses, true); } async pollAddress(depositAddress: BlockchainAddress, fromBlock?: number, toBlock?: number): Promise { if (depositAddress.blockchain !== this.blockchain) throw new Error(`Invalid blockchain: ${depositAddress.blockchain}`); - return this.processNewPayInEntries([depositAddress], fromBlock, toBlock); + return this.processNewPayInEntries([depositAddress.address], false, fromBlock, toBlock); } //*** HELPER METHODS ***// private async processNewPayInEntries( - depositAddresses: BlockchainAddress[], + depositAddresses: string[], + persistProgress: boolean, fromBlock?: number, toBlock?: number, ): Promise { const log = this.createNewLogObject(); + const scanState: Record = persistProgress ? await this.getScanState() : {}; + const assets = await this.assetService.getAllBlockchainAssets([this.blockchain]); - const newEntries = await this.getNativeEntries(depositAddresses, fromBlock, toBlock); + const newEntries: PayInEntry[] = []; + for (const asset of assets) { + try { + const entries = await this.scanLedger(asset, scanState, depositAddresses, fromBlock, toBlock); + newEntries.push(...entries); + } catch (e) { + this.logger.error(`Failed to scan ${asset.name}:`, e); + } + } if (newEntries.length) { await this.createPayInsAndSave(newEntries, log); } + if (persistProgress && Object.keys(scanState).length) { + await this.saveScanState(scanState); + } + this.printInputLog(log, 'omitted', this.blockchain); } - // --- Native ICP (Rosetta per-address history) --- // - private async getNativeEntries( - depositAddresses: BlockchainAddress[], + // --- Generic ledger scanner --- // + private async scanLedger( + asset: Asset, + scanState: Record, + depositAddresses: string[], fromBlock?: number, toBlock?: number, ): Promise { - const asset = await this.assetService.getNativeAsset(this.blockchain); + const isNative = asset.type === AssetType.COIN; + const canisterId = asset.chainId; + + if (!isNative && !asset.decimals) { + this.logger.error(`Asset ${asset.name} has no decimals configured, skipping`); + return []; + } + + // Build address lookup: maps transfer.to → principal address + const addressLookup = new Map( + depositAddresses.map((p) => [isNative ? InternetComputerUtil.accountIdentifier(p) : p, p]), + ); + + // Determine block range + const stateKey = asset.id.toString(); + const lastScanned = scanState[stateKey]; + + const chainLength = isNative + ? await this.payInInternetComputerService.getBlockHeight() + : await this.payInInternetComputerService.getIcrcBlockHeight(canisterId); + + const endBlock = toBlock ?? chainLength; + const startBlock = fromBlock ?? (lastScanned === undefined ? Math.max(0, endBlock - BATCH_SIZE) : lastScanned + 1); + + if (startBlock >= endBlock) return []; const entries: PayInEntry[] = []; + let cursor = startBlock; + let highestBlock = startBlock; + + while (cursor < endBlock) { + const count = Math.min(BATCH_SIZE, endBlock - cursor); + + const result = isNative + ? await this.payInInternetComputerService.getTransfers(cursor, count) + : await this.payInInternetComputerService.getIcrcTransfers(canisterId, asset.decimals, cursor, count); + + for (const transfer of result.transfers) { + const matchedAddress = addressLookup.get(transfer.to); + if (!matchedAddress) continue; + + entries.push({ + senderAddresses: transfer.from, + receiverAddress: BlockchainAddress.create(matchedAddress, this.blockchain), + txId: isNative ? transfer.blockIndex.toString() : `${canisterId}:${transfer.blockIndex}`, + txType: this.getTxType(matchedAddress), + blockHeight: transfer.blockIndex, + amount: transfer.amount, + asset, + }); + } - for (const da of depositAddresses) { - try { - const accountId = InternetComputerUtil.accountIdentifier(da.address); - const lastBlock = fromBlock ?? (await this.getLastCheckedNativeBlockHeight(da)) + 1; - - const transfers = await this.payInInternetComputerService.getNativeTransfersForAddress(accountId); - - for (const transfer of transfers) { - if (transfer.blockIndex < lastBlock) continue; - if (toBlock !== undefined && transfer.blockIndex > toBlock) continue; - if (transfer.to !== accountId) continue; - - entries.push({ - senderAddresses: transfer.from, - receiverAddress: BlockchainAddress.create(da.address, this.blockchain), - txId: transfer.blockIndex.toString(), - txType: this.getTxType(da.address), - blockHeight: transfer.blockIndex, - amount: transfer.amount, - asset, - }); - } - } catch (e) { - this.logger.error(`Failed to fetch native transfers for ${da.address}:`, e); + highestBlock = Math.max(highestBlock, result.lastBlockIndex); + + // Advance cursor, handling stuck cursor edge case + const nextCursor = result.lastBlockIndex + 1; + if (nextCursor <= cursor) { + this.logger.warn(`${asset.name}: cursor stuck at ${cursor}, skipping batch`); + cursor += BATCH_SIZE; + } else { + cursor = nextCursor; } + + if (result.rawTransactionCount === 0) break; + } + + // Update scan state + if (highestBlock > (lastScanned ?? 0)) { + scanState[stateKey] = highestBlock; } return entries; } - // --- DB-based block height lookups --- // - private async getLastCheckedNativeBlockHeight(depositAddress: BlockchainAddress): Promise { - return this.payInRepository - .findOne({ - select: { id: true, blockHeight: true }, - where: { address: depositAddress, inTxId: Not(Like('%:%')) }, - order: { blockHeight: 'DESC' }, - loadEagerRelations: false, - }) - .then((input) => input?.blockHeight ?? 0); + // --- Settings persistence --- // + private async getScanState(): Promise> { + return (await this.settingService.getObj>(SETTING_KEY)) ?? {}; + } + + private async saveScanState(state: Record): Promise { + await this.settingService.setObj(SETTING_KEY, state); } private getTxType(resolvedAddress: string): PayInType { From 69aae4b82bdbc56cb217b463d8d245d798a9a4e5 Mon Sep 17 00:00:00 2001 From: David May <85513542+davidleomay@users.noreply.github.com> Date: Thu, 12 Mar 2026 01:39:02 +0100 Subject: [PATCH 5/6] fix: field naming/order (#3396) --- .../history/mappers/transaction-dto.mapper.ts | 40 +++++++++---------- .../supporting/payment/dto/transaction.dto.ts | 30 +++++++------- 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/src/subdomains/core/history/mappers/transaction-dto.mapper.ts b/src/subdomains/core/history/mappers/transaction-dto.mapper.ts index 48f500ccd9..f68aa527f6 100644 --- a/src/subdomains/core/history/mappers/transaction-dto.mapper.ts +++ b/src/subdomains/core/history/mappers/transaction-dto.mapper.ts @@ -57,14 +57,18 @@ export class TransactionDtoMapper { inputAmount: Util.roundReadable(buyCrypto.inputAmount, amountType(buyCrypto.inputAssetEntity)), inputAsset: buyCrypto.inputAssetEntity.name, inputAssetId: buyCrypto.inputAssetEntity.id, + inputChainId: inputAsset?.chainId ?? null, inputBlockchain: buyCrypto.cryptoInput?.asset.blockchain, + inputEvmChainId: inputAsset?.evmChainId ?? null, inputPaymentMethod: buyCrypto.paymentMethodIn, ...(buyCrypto.outputAmount ? buyCrypto.exchangeRate : null), outputAmount: buyCrypto.outputAmount != null ? Util.roundReadable(buyCrypto.outputAmount, AmountType.ASSET) : null, outputAsset: buyCrypto.outputAsset?.name, outputAssetId: buyCrypto.outputAsset?.id, + outputChainId: outputAsset?.chainId ?? null, outputBlockchain: buyCrypto.outputAsset?.blockchain, + outputEvmChainId: outputAsset?.evmChainId ?? null, outputPaymentMethod: CryptoPaymentMethod.CRYPTO, priceSteps: buyCrypto.priceStepsObject, feeAmount: buyCrypto.totalFeeAmount @@ -79,6 +83,7 @@ export class TransactionDtoMapper { inputTxUrl: buyCrypto?.cryptoInput ? txExplorerUrl(buyCrypto.cryptoInput.asset.blockchain, buyCrypto.cryptoInput.inTxId) : null, + depositAddress: buyCrypto.cryptoInput?.address?.address ?? null, outputTxId: buyCrypto.txId, outputTxUrl: buyCrypto.txId ? txExplorerUrl(buyCrypto.outputAsset?.blockchain, buyCrypto.txId) : null, outputDate: buyCrypto.outputDate, @@ -106,11 +111,6 @@ export class TransactionDtoMapper { asset: buyCrypto.networkStartAsset, } : null, - sourceChainId: inputAsset?.chainId ?? null, - destinationChainId: outputAsset?.chainId ?? null, - sourceEvmChainId: inputAsset?.evmChainId ?? null, - destinationEvmChainId: outputAsset?.evmChainId ?? null, - depositAddress: buyCrypto.cryptoInput?.address?.address ?? null, }; return Object.assign(new TransactionDto(), dto); @@ -141,13 +141,17 @@ export class TransactionDtoMapper { inputAmount: Util.roundReadable(buyFiat.inputAmount, amountType(buyFiat.inputAssetEntity)), inputAsset: buyFiat.inputAssetEntity.name, inputAssetId: buyFiat.inputAssetEntity.id, + inputChainId: inputAsset?.chainId ?? null, inputBlockchain: buyFiat.cryptoInput?.asset.blockchain, + inputEvmChainId: inputAsset?.evmChainId ?? null, inputPaymentMethod: CryptoPaymentMethod.CRYPTO, ...(buyFiat.outputAmount ? buyFiat.exchangeRate : null), outputAmount: buyFiat.outputAmount != null ? Util.roundReadable(buyFiat.outputAmount, AmountType.FIAT) : null, outputAsset: buyFiat.outputAsset?.name, outputAssetId: buyFiat.outputAsset?.id, + outputChainId: null, outputBlockchain: null, + outputEvmChainId: null, outputPaymentMethod: FiatPaymentMethod.BANK, outputDate: buyFiat.outputDate, priceSteps: buyFiat.priceStepsObject, @@ -163,6 +167,7 @@ export class TransactionDtoMapper { inputTxUrl: buyFiat?.cryptoInput ? txExplorerUrl(buyFiat.cryptoInput.asset.blockchain, buyFiat.cryptoInput.inTxId) : null, + depositAddress: buyFiat.cryptoInput?.address?.address ?? null, outputTxId: buyFiat.bankTx?.remittanceInfo ?? null, outputTxUrl: null, chargebackAmount: buyFiat.chargebackAmount, @@ -176,11 +181,6 @@ export class TransactionDtoMapper { chargebackDate: buyFiat.chargebackDate, date: buyFiat.transaction.created, externalTransactionId: buyFiat.transaction.externalId, - sourceChainId: inputAsset?.chainId ?? null, - destinationChainId: null, - sourceEvmChainId: inputAsset?.evmChainId ?? null, - destinationEvmChainId: null, - depositAddress: buyFiat.cryptoInput?.address?.address ?? null, }; return Object.assign(new TransactionDto(), dto); @@ -212,12 +212,16 @@ export class TransactionDtoMapper { inputAmount: Util.roundReadable(txRequest.amount, amountType(txRequest.sourceAssetEntity)), inputAsset: txRequest.sourceAssetEntity.name, inputAssetId: txRequest.sourceAssetEntity.id, + inputChainId: sourceAsset?.chainId ?? null, inputBlockchain: sourceAsset?.blockchain ?? null, + inputEvmChainId: sourceAsset?.evmChainId ?? null, inputPaymentMethod: txRequest.sourcePaymentMethod, outputAmount: null, outputAsset: txRequest.targetAssetEntity?.name, outputAssetId: txRequest.targetAssetEntity?.id, + outputChainId: targetAsset?.chainId ?? null, outputBlockchain: targetAsset?.blockchain ?? null, + outputEvmChainId: targetAsset?.evmChainId ?? null, outputPaymentMethod: txRequest.targetPaymentMethod, priceSteps: null, feeAmount: fees?.total, @@ -225,6 +229,7 @@ export class TransactionDtoMapper { fees, inputTxId: null, inputTxUrl: null, + depositAddress: null, outputTxId: null, outputTxUrl: null, outputDate: null, @@ -236,11 +241,6 @@ export class TransactionDtoMapper { date: txRequest.created, externalTransactionId: null, networkStartTx: null, - sourceChainId: sourceAsset?.chainId ?? null, - destinationChainId: targetAsset?.chainId ?? null, - sourceEvmChainId: sourceAsset?.evmChainId ?? null, - destinationEvmChainId: targetAsset?.evmChainId ?? null, - depositAddress: null, }; return Object.assign(new TransactionDto(), dto); @@ -269,7 +269,9 @@ export class TransactionDtoMapper { inputAmount: null, inputAsset: null, inputAssetId: null, + inputChainId: null, inputBlockchain: null, + inputEvmChainId: null, inputPaymentMethod: null, exchangeRate: null, rate: null, @@ -279,7 +281,9 @@ export class TransactionDtoMapper { : null, outputAsset: refReward.outputAsset.name, outputAssetId: refReward.outputAsset?.id, + outputChainId: refReward.outputAsset?.chainId ?? null, outputBlockchain: refReward.targetBlockchain, + outputEvmChainId: refReward.outputAsset?.evmChainId ?? null, outputPaymentMethod: CryptoPaymentMethod.CRYPTO, outputDate: refReward.outputDate, priceSteps: null, @@ -288,6 +292,7 @@ export class TransactionDtoMapper { fees: null, inputTxId: null, inputTxUrl: null, + depositAddress: null, outputTxId: refReward.txId, outputTxUrl: refReward.txId ? txExplorerUrl(refReward.targetBlockchain, refReward.txId) : null, chargebackAmount: undefined, @@ -298,11 +303,6 @@ export class TransactionDtoMapper { chargebackTxUrl: undefined, chargebackDate: undefined, date: refReward.transaction.created, - sourceChainId: null, - destinationChainId: refReward.outputAsset?.chainId ?? null, - sourceEvmChainId: null, - destinationEvmChainId: refReward.outputAsset?.evmChainId ?? null, - depositAddress: null, }; return Object.assign(new TransactionDto(), dto); diff --git a/src/subdomains/supporting/payment/dto/transaction.dto.ts b/src/subdomains/supporting/payment/dto/transaction.dto.ts index 2964927e19..ef9f43b675 100644 --- a/src/subdomains/supporting/payment/dto/transaction.dto.ts +++ b/src/subdomains/supporting/payment/dto/transaction.dto.ts @@ -143,9 +143,15 @@ export class UnassignedTransactionDto { @ApiPropertyOptional({ description: 'Fiat ID for buy transactions, asset ID otherwise' }) inputAssetId?: number; + @ApiPropertyOptional({ description: 'Contract address of the input asset (for tokens)' }) + inputChainId?: string; + @ApiPropertyOptional({ enum: Blockchain }) inputBlockchain?: Blockchain; + @ApiPropertyOptional({ description: 'EVM chain ID of the input asset (e.g. 1 for Ethereum)' }) + inputEvmChainId?: number; + @ApiPropertyOptional({ enum: PaymentMethodSwagger }) inputPaymentMethod?: PaymentMethod; @@ -155,6 +161,9 @@ export class UnassignedTransactionDto { @ApiPropertyOptional() inputTxUrl?: string; + @ApiPropertyOptional({ description: 'Deposit address for crypto input transactions' }) + depositAddress?: string; + @ApiPropertyOptional({ description: 'Chargeback address or chargeback IBAN' }) chargebackTarget?: string; @@ -216,9 +225,15 @@ export class TransactionDto extends UnassignedTransactionDto { @ApiPropertyOptional({ description: 'Fiat ID for sell transactions, asset ID otherwise' }) outputAssetId?: number; + @ApiPropertyOptional({ description: 'Contract address of the output asset (for tokens)' }) + outputChainId?: string; + @ApiPropertyOptional({ enum: Blockchain }) outputBlockchain?: Blockchain; + @ApiPropertyOptional({ description: 'EVM chain ID of the output asset (e.g. 1 for Ethereum)' }) + outputEvmChainId?: number; + @ApiPropertyOptional({ enum: PaymentMethodSwagger }) outputPaymentMethod?: PaymentMethod; @@ -248,21 +263,6 @@ export class TransactionDto extends UnassignedTransactionDto { @ApiPropertyOptional({ type: NetworkStartTxDto }) networkStartTx?: NetworkStartTxDto; - - @ApiPropertyOptional({ description: 'Contract address of the source asset (for tokens)' }) - sourceChainId?: string; - - @ApiPropertyOptional({ description: 'Contract address of the destination asset (for tokens)' }) - destinationChainId?: string; - - @ApiPropertyOptional({ description: 'EVM chain ID of the source asset (e.g. 1 for Ethereum)' }) - sourceEvmChainId?: number; - - @ApiPropertyOptional({ description: 'EVM chain ID of the destination asset (e.g. 1 for Ethereum)' }) - destinationEvmChainId?: number; - - @ApiPropertyOptional({ description: 'Deposit address for crypto input transactions' }) - depositAddress?: string; } export class TransactionDetailDto extends TransactionDto { From 5331fcf4e20d2efcf98489c4f62bb652994f1dcd Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 12 Mar 2026 09:14:29 +0100 Subject: [PATCH 6/6] feat: add PhoneChange KYC endpoint (#3397) --- .../generic/kyc/controllers/kyc.controller.ts | 12 ++++++++++++ .../generic/kyc/dto/input/kyc-data.dto.ts | 9 +++++++++ .../generic/kyc/entities/kyc-step.entity.ts | 2 +- src/subdomains/generic/kyc/services/kyc.service.ts | 14 ++++++++++++++ .../user/models/user-data/user-data.service.ts | 12 +++++++----- 5 files changed, 43 insertions(+), 6 deletions(-) diff --git a/src/subdomains/generic/kyc/controllers/kyc.controller.ts b/src/subdomains/generic/kyc/controllers/kyc.controller.ts index 8d2d2aff8d..4086374780 100644 --- a/src/subdomains/generic/kyc/controllers/kyc.controller.ts +++ b/src/subdomains/generic/kyc/controllers/kyc.controller.ts @@ -43,6 +43,7 @@ import { KycBeneficialData, KycChangeAddressData, KycChangeNameData, + KycChangePhoneData, KycContactData, KycFileData, KycLegalEntityData, @@ -266,6 +267,17 @@ export class KycController { return this.kycService.updateNameChangeData(code, +id, data); } + @Put('data/phone/:id') + @ApiOkResponse({ type: KycStepBase }) + @ApiUnauthorizedResponse(MergedResponse) + async updatePhoneChangeData( + @Headers(CodeHeaderName) code: string, + @Param('id') id: string, + @Body() data: KycChangePhoneData, + ): Promise { + return this.kycService.updatePhoneChangeData(code, +id, data); + } + @Put('data/confirmation/:id') @ApiOkResponse({ type: KycStepBase }) @ApiUnauthorizedResponse(MergedResponse) diff --git a/src/subdomains/generic/kyc/dto/input/kyc-data.dto.ts b/src/subdomains/generic/kyc/dto/input/kyc-data.dto.ts index b4bb7aa4ac..f5cca578ac 100644 --- a/src/subdomains/generic/kyc/dto/input/kyc-data.dto.ts +++ b/src/subdomains/generic/kyc/dto/input/kyc-data.dto.ts @@ -114,6 +114,15 @@ export class KycChangeNameData { lastName: string; } +export class KycChangePhoneData { + @ApiProperty({ description: 'New phone number' }) + @IsNotEmpty() + @IsString() + @Transform(DfxPhoneTransform) + @IsDfxPhone() + phone: string; +} + export class KycPersonalData { @ApiProperty({ enum: AccountType }) @IsNotEmpty() diff --git a/src/subdomains/generic/kyc/entities/kyc-step.entity.ts b/src/subdomains/generic/kyc/entities/kyc-step.entity.ts index 8d18a9c747..6ce3b650c3 100644 --- a/src/subdomains/generic/kyc/entities/kyc-step.entity.ts +++ b/src/subdomains/generic/kyc/entities/kyc-step.entity.ts @@ -135,7 +135,7 @@ export class KycStep extends IEntity { return { url: `${apiUrl}/data/payment/${this.id}`, type: UrlType.API }; case KycStepName.PHONE_CHANGE: - return { url: '', type: UrlType.NONE }; + return { url: `${apiUrl}/data/phone/${this.id}`, type: UrlType.API }; case KycStepName.ADDRESS_CHANGE: return { url: `${apiUrl}/data/address/${this.id}`, type: UrlType.API }; diff --git a/src/subdomains/generic/kyc/services/kyc.service.ts b/src/subdomains/generic/kyc/services/kyc.service.ts index 5d58510275..4322a0a24a 100644 --- a/src/subdomains/generic/kyc/services/kyc.service.ts +++ b/src/subdomains/generic/kyc/services/kyc.service.ts @@ -51,6 +51,7 @@ import { KycBeneficialData, KycChangeAddressData, KycChangeNameData, + KycChangePhoneData, KycContactData, KycFileData, KycLegalEntityData, @@ -772,6 +773,19 @@ export class KycService { }); } + async updatePhoneChangeData(kycHash: string, stepId: number, data: KycChangePhoneData): Promise { + const user = await this.getUser(kycHash); + const kycStep = user.getPendingStepOrThrow(stepId); + + await this.userDataService.updatePhone(user, data.phone, false); + + await this.kycStepRepo.update(...kycStep.complete({ phone: data.phone })); + await this.createStepLog(user, kycStep); + await this.updateProgress(user, false); + + return KycStepMapper.toStepBase(kycStep); + } + async getFinancialData(kycHash: string, ip: string, stepId: number, lang?: string): Promise { const user = await this.getUser(kycHash); const kycStep = user.getPendingStepOrThrow(stepId); diff --git a/src/subdomains/generic/user/models/user-data/user-data.service.ts b/src/subdomains/generic/user/models/user-data/user-data.service.ts index a79997bd87..bd42d91d55 100644 --- a/src/subdomains/generic/user/models/user-data/user-data.service.ts +++ b/src/subdomains/generic/user/models/user-data/user-data.service.ts @@ -754,7 +754,7 @@ export class UserDataService { } // --- PHONE UPDATE --- // - async updatePhone(userData: UserData, phone: string): Promise { + async updatePhone(userData: UserData, phone: string, createStep = true): Promise { if (userData.kycLevel !== KycLevel.LEVEL_0 && !phone) throw new BadRequestException('KYC already started, user data deletion not allowed'); @@ -784,10 +784,12 @@ export class UserDataService { } // create KYC step - await this.kycService.createCustomKycStep(userData, KycStepName.PHONE_CHANGE, ReviewStatus.COMPLETED, { - phone, - previousPhone, - }); + if (createStep) { + await this.kycService.createCustomKycStep(userData, KycStepName.PHONE_CHANGE, ReviewStatus.COMPLETED, { + phone, + previousPhone, + }); + } } // --- ADDRESS UPDATE --- //