diff --git a/infrastructure/bicep/dfx-api.bicep b/infrastructure/bicep/dfx-api.bicep index 22882dda66..3253a2fc72 100644 --- a/infrastructure/bicep/dfx-api.bicep +++ b/infrastructure/bicep/dfx-api.bicep @@ -38,6 +38,7 @@ param intWalletAddress string param stakingWalletAddress string param utxoSpenderAddress string param btcCollectorAddress string +param btcOutWalletAddress string param ethWalletAddress string @secure() @@ -565,6 +566,10 @@ resource apiAppService 'Microsoft.Web/sites@2018-11-01' = { name: 'BTC_COLLECTOR_ADDRESS' value: btcCollectorAddress } + { + name: 'BTC_OUT_WALLET_ADDRESS' + value: btcOutWalletAddress + } { name: 'FTP_HOST' value: '138.201.74.234' diff --git a/infrastructure/bicep/parameters/dev.json b/infrastructure/bicep/parameters/dev.json index a60e92d50f..10180af8bf 100644 --- a/infrastructure/bicep/parameters/dev.json +++ b/infrastructure/bicep/parameters/dev.json @@ -110,6 +110,9 @@ "btcCollectorAddress": { "value": "xxx" }, + "btcOutWalletAddress": { + "value": "xxx" + }, "nodeServicePlanSkuName": { "value": "P1v2" }, diff --git a/infrastructure/bicep/parameters/loc.json b/infrastructure/bicep/parameters/loc.json index f2f99b3a3a..09b97f23fc 100644 --- a/infrastructure/bicep/parameters/loc.json +++ b/infrastructure/bicep/parameters/loc.json @@ -110,6 +110,9 @@ "btcCollectorAddress": { "value": "xxx" }, + "btcOutWalletAddress": { + "value": "xxx" + }, "nodeServicePlanSkuName": { "value": "B1" }, diff --git a/infrastructure/bicep/parameters/prd.json b/infrastructure/bicep/parameters/prd.json index d7772428b4..876fe9cd0f 100644 --- a/infrastructure/bicep/parameters/prd.json +++ b/infrastructure/bicep/parameters/prd.json @@ -110,6 +110,9 @@ "btcCollectorAddress": { "value": "xxx" }, + "btcOutWalletAddress": { + "value": "xxx" + }, "nodeServicePlanSkuName": { "value": "P2v2" }, diff --git a/migration/1666094113435-removeUnusedUserFields.js b/migration/1666094113435-removeUnusedUserFields.js new file mode 100644 index 0000000000..f5ef8b19e9 --- /dev/null +++ b/migration/1666094113435-removeUnusedUserFields.js @@ -0,0 +1,61 @@ +const { MigrationInterface, QueryRunner } = require("typeorm"); + +module.exports = class removeUnusedUserFields1666094113435 { + name = 'removeUnusedUserFields1666094113435' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "dbo"."user" DROP CONSTRAINT "FK_4aaf6d02199282eb8d3931bff31"`); + await queryRunner.query(`ALTER TABLE "dbo"."user" DROP CONSTRAINT "FK_0b294695467ceecc030f95461c1"`); + await queryRunner.query(`ALTER TABLE "dbo"."user" DROP CONSTRAINT "FK_039c54821427d0adca4db8de366"`); + await queryRunner.query(`ALTER TABLE "dbo"."user" DROP CONSTRAINT "FK_19ab0596b1fab6a44be5491ffb4"`); + await queryRunner.query(`ALTER TABLE "dbo"."user_data" DROP CONSTRAINT "DF_45a8d297955d5c3896ea84afa4b"`); + await queryRunner.query(`ALTER TABLE "dbo"."user_data" DROP COLUMN "isMigrated"`); + await queryRunner.query(`ALTER TABLE "dbo"."user" DROP COLUMN "mail"`); + await queryRunner.query(`ALTER TABLE "dbo"."user" DROP COLUMN "firstname"`); + await queryRunner.query(`ALTER TABLE "dbo"."user" DROP COLUMN "surname"`); + await queryRunner.query(`ALTER TABLE "dbo"."user" DROP COLUMN "street"`); + await queryRunner.query(`ALTER TABLE "dbo"."user" DROP COLUMN "houseNumber"`); + await queryRunner.query(`ALTER TABLE "dbo"."user" DROP COLUMN "location"`); + await queryRunner.query(`ALTER TABLE "dbo"."user" DROP COLUMN "zip"`); + await queryRunner.query(`ALTER TABLE "dbo"."user" DROP COLUMN "phone"`); + await queryRunner.query(`ALTER TABLE "dbo"."user" DROP COLUMN "countryId"`); + await queryRunner.query(`ALTER TABLE "dbo"."user" DROP COLUMN "languageId"`); + await queryRunner.query(`ALTER TABLE "dbo"."user" DROP CONSTRAINT "DF_625088799076cba7dffb9947942"`); + await queryRunner.query(`ALTER TABLE "dbo"."user" DROP COLUMN "accountType"`); + await queryRunner.query(`ALTER TABLE "dbo"."user" DROP COLUMN "organizationName"`); + await queryRunner.query(`ALTER TABLE "dbo"."user" DROP COLUMN "organizationStreet"`); + await queryRunner.query(`ALTER TABLE "dbo"."user" DROP COLUMN "organizationHouseNumber"`); + await queryRunner.query(`ALTER TABLE "dbo"."user" DROP COLUMN "organizationLocation"`); + await queryRunner.query(`ALTER TABLE "dbo"."user" DROP COLUMN "organizationZip"`); + await queryRunner.query(`ALTER TABLE "dbo"."user" DROP COLUMN "organizationCountryId"`); + await queryRunner.query(`ALTER TABLE "dbo"."user" DROP COLUMN "currencyId"`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "dbo"."user" ADD "currencyId" int`); + await queryRunner.query(`ALTER TABLE "dbo"."user" ADD "organizationCountryId" int`); + await queryRunner.query(`ALTER TABLE "dbo"."user" ADD "organizationZip" nvarchar(256)`); + await queryRunner.query(`ALTER TABLE "dbo"."user" ADD "organizationLocation" nvarchar(256)`); + await queryRunner.query(`ALTER TABLE "dbo"."user" ADD "organizationHouseNumber" nvarchar(256)`); + await queryRunner.query(`ALTER TABLE "dbo"."user" ADD "organizationStreet" nvarchar(256)`); + await queryRunner.query(`ALTER TABLE "dbo"."user" ADD "organizationName" nvarchar(256)`); + await queryRunner.query(`ALTER TABLE "dbo"."user" ADD "accountType" nvarchar(256) NOT NULL`); + await queryRunner.query(`ALTER TABLE "dbo"."user" ADD CONSTRAINT "DF_625088799076cba7dffb9947942" DEFAULT 'Personal' FOR "accountType"`); + await queryRunner.query(`ALTER TABLE "dbo"."user" ADD "languageId" int`); + await queryRunner.query(`ALTER TABLE "dbo"."user" ADD "countryId" int`); + await queryRunner.query(`ALTER TABLE "dbo"."user" ADD "phone" nvarchar(256)`); + await queryRunner.query(`ALTER TABLE "dbo"."user" ADD "zip" nvarchar(256)`); + await queryRunner.query(`ALTER TABLE "dbo"."user" ADD "location" nvarchar(256)`); + await queryRunner.query(`ALTER TABLE "dbo"."user" ADD "houseNumber" nvarchar(256)`); + await queryRunner.query(`ALTER TABLE "dbo"."user" ADD "street" nvarchar(256)`); + await queryRunner.query(`ALTER TABLE "dbo"."user" ADD "surname" nvarchar(256)`); + await queryRunner.query(`ALTER TABLE "dbo"."user" ADD "firstname" nvarchar(256)`); + await queryRunner.query(`ALTER TABLE "dbo"."user" ADD "mail" nvarchar(256)`); + await queryRunner.query(`ALTER TABLE "dbo"."user_data" ADD "isMigrated" bit NOT NULL`); + await queryRunner.query(`ALTER TABLE "dbo"."user_data" ADD CONSTRAINT "DF_45a8d297955d5c3896ea84afa4b" DEFAULT 1 FOR "isMigrated"`); + await queryRunner.query(`ALTER TABLE "dbo"."user" ADD CONSTRAINT "FK_19ab0596b1fab6a44be5491ffb4" FOREIGN KEY ("currencyId") REFERENCES "fiat"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "dbo"."user" ADD CONSTRAINT "FK_039c54821427d0adca4db8de366" FOREIGN KEY ("organizationCountryId") REFERENCES "country"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "dbo"."user" ADD CONSTRAINT "FK_0b294695467ceecc030f95461c1" FOREIGN KEY ("languageId") REFERENCES "language"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "dbo"."user" ADD CONSTRAINT "FK_4aaf6d02199282eb8d3931bff31" FOREIGN KEY ("countryId") REFERENCES "country"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + } +} diff --git a/src/blockchain/ain/ain.module.ts b/src/blockchain/ain/ain.module.ts index 1dad110dbb..6dc7ae380e 100644 --- a/src/blockchain/ain/ain.module.ts +++ b/src/blockchain/ain/ain.module.ts @@ -5,11 +5,12 @@ import { NodeController } from './node/node.controller'; import { NodeService } from './node/node.service'; import { WhaleService } from './whale/whale.service'; import { DeFiChainUtil } from './utils/defichain.util'; +import { BtcFeeService } from './services/btc-fee.service'; @Module({ imports: [SharedModule], - providers: [CryptoService, NodeService, WhaleService, DeFiChainUtil], - exports: [CryptoService, NodeService, WhaleService, DeFiChainUtil], + providers: [CryptoService, NodeService, WhaleService, DeFiChainUtil, BtcFeeService], + exports: [CryptoService, NodeService, WhaleService, DeFiChainUtil, BtcFeeService], controllers: [NodeController], }) export class AinModule {} diff --git a/src/blockchain/ain/node/btc-client.ts b/src/blockchain/ain/node/btc-client.ts index dc1ceb4bd5..594b828f3f 100644 --- a/src/blockchain/ain/node/btc-client.ts +++ b/src/blockchain/ain/node/btc-client.ts @@ -1,4 +1,5 @@ import { SchedulerRegistry } from '@nestjs/schedule'; +import { Config } from 'src/config/config'; import { HttpService } from 'src/shared/services/http.service'; import { NodeClient, NodeCommand, NodeMode } from './node-client'; @@ -27,4 +28,24 @@ export class BtcClient extends NodeClient { true, ).then((r) => r.txid); } + + async sendMany(payload: { addressTo: string; amount: number }[], feeRate: number): Promise { + const batch = payload.reduce((acc, p) => ({ ...acc, [p.addressTo]: `${p.amount}` }), {}); + + return await this.callNode<{ txid: string }>( + (c) => + c.call( + NodeCommand.SEND, + [ + batch, + null, + 'unset', + null, + { fee_rate: feeRate, replaceable: true, change_address: Config.blockchain.default.btcOutWalletAddress }, + ], + 'number', + ), + true, + ).then((r) => r.txid); + } } diff --git a/src/blockchain/ain/services/btc-fee.service.ts b/src/blockchain/ain/services/btc-fee.service.ts new file mode 100644 index 0000000000..bb2b380545 --- /dev/null +++ b/src/blockchain/ain/services/btc-fee.service.ts @@ -0,0 +1,23 @@ +import { Injectable } from '@nestjs/common'; +import { HttpService } from 'src/shared/services/http.service'; + +@Injectable() +export class BtcFeeService { + private readonly btcFeeUrl = 'https://mempool.space/api/v1/fees/recommended'; + + constructor(private readonly http: HttpService) {} + + async getRecommendedFeeRate(): Promise { + const { fastestFee } = await this.http.get<{ + fastestFee: number; + halfHourFee: number; + hourFee: number; + economyFee: number; + minimumFee: number; + }>(this.btcFeeUrl, { + tryCount: 3, + }); + + return fastestFee; + } +} diff --git a/src/config/config.ts b/src/config/config.ts index 018e4d20d9..c4713ad636 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -72,6 +72,14 @@ export class Configuration { expiresIn: process.env.JWT_EXPIRES_IN ?? 172800, }, }, + company: { + signOptions: { + expiresIn: process.env.JWT_EXPIRES_IN_COMPANY ?? 30, + }, + }, + challenge: { + expiresIn: +process.env.CHALLENGE_EXPIRES_IN ?? 10, + }, signMessage: 'By_signing_this_message,_you_confirm_that_you_are_the_sole_owner_of_the_provided_DeFiChain_address_and_are_in_possession_of_its_private_key._Your_ID:_', signMessageWallet: @@ -119,8 +127,10 @@ export class Configuration { apiKey: process.env.FIXER_API_KEY, }; - lock = { - apiKey: process.env.LOCK_API_KEY, + externalKycServices = { + 'LOCK.space': { + apiKey: process.env.LOCK_API_KEY, + }, }; mail: MailOptions = { @@ -149,6 +159,7 @@ export class Configuration { contact: { supportMail: process.env.SUPPORT_MAIL || 'support@dfx.swiss', monitoringMail: process.env.MONITORING_MAIL || 'monitoring@dfx.swiss', + liqMail: process.env.LIQ_MAIL || 'liq@dfx.swiss', noReplyMail: process.env.NOREPLY_MAIL || 'noreply@dfx.swiss', }, }; @@ -198,6 +209,7 @@ export class Configuration { intWalletAddress: process.env.INT_WALLET_ADDRESS, stakingWalletAddress: process.env.STAKING_WALLET_ADDRESS, btcCollectorAddress: process.env.BTC_COLLECTOR_ADDRESS, + btcOutWalletAddress: process.env.BTC_OUT_WALLET_ADDRESS, minTxAmount: 0.00000297, minDeposit: { Fiat: { diff --git a/src/notification/entities/mail/base/mail.ts b/src/notification/entities/mail/base/mail.ts index 5a1e380ed0..debf920e28 100644 --- a/src/notification/entities/mail/base/mail.ts +++ b/src/notification/entities/mail/base/mail.ts @@ -3,7 +3,7 @@ import { NotificationType } from 'src/notification/enums'; import { Notification, NotificationOptions, NotificationMetadata } from '../../notification.entity'; export interface MailParams { - to: string; + to: string | string[]; subject: string; salutation: string; body: string; @@ -26,7 +26,7 @@ export class Mail extends Notification { name: 'DFX.swiss', address: GetConfig().mail.contact.noReplyMail, }; - readonly #to: string; + readonly #to: string | string[]; readonly #cc: string; readonly #bcc: string; readonly #template: string = GetConfig().mail.defaultMailTemplate; @@ -66,7 +66,7 @@ export class Mail extends Notification { return { name, address }; } - get to(): string { + get to(): string | string[] { return this.#to; } diff --git a/src/notification/entities/mail/error-monitoring-mail.ts b/src/notification/entities/mail/error-monitoring-mail.ts index cf848d6691..6b1e52534e 100644 --- a/src/notification/entities/mail/error-monitoring-mail.ts +++ b/src/notification/entities/mail/error-monitoring-mail.ts @@ -1,4 +1,5 @@ import { GetConfig } from 'src/config/config'; +import { MailContext } from 'src/notification/enums'; import { NotificationMetadata, NotificationOptions } from '../notification.entity'; import { Mail } from './base/mail'; @@ -13,8 +14,11 @@ export interface ErrorMonitoringMailParams { export class ErrorMonitoringMail extends Mail { constructor(params: ErrorMonitoringMailParams) { + const to = [GetConfig().mail.contact.monitoringMail]; + ErrorMonitoringMail.isLiqMail(params) && to.push(GetConfig().mail.contact.liqMail); + const _params = { - to: GetConfig().mail.contact.monitoringMail, + to: to, subject: `${params.subject} (${GetConfig().environment.toUpperCase()})`, salutation: 'Hi DFX Tech Support', body: ErrorMonitoringMail.createBody(params.errors), @@ -25,6 +29,12 @@ export class ErrorMonitoringMail extends Mail { super(_params); } + private static isLiqMail(params: ErrorMonitoringMailParams): boolean { + return [MailContext.BUY_CRYPTO, MailContext.DEX, MailContext.PAYOUT, MailContext.PRICING].includes( + params.metadata?.context, + ); + } + static createBody(errors: string[]): string { const env = GetConfig().environment.toUpperCase(); diff --git a/src/notification/services/mail.service.ts b/src/notification/services/mail.service.ts index 3a00e704e7..8fd64eb521 100644 --- a/src/notification/services/mail.service.ts +++ b/src/notification/services/mail.service.ts @@ -9,6 +9,7 @@ export interface MailOptions { contact: { supportMail: string; monitoringMail: string; + liqMail: string; noReplyMail: string; }; } diff --git a/src/payment/models/buy-crypto/entities/buy-crypto.entity.ts b/src/payment/models/buy-crypto/entities/buy-crypto.entity.ts index b09806def5..b91b19096d 100644 --- a/src/payment/models/buy-crypto/entities/buy-crypto.entity.ts +++ b/src/payment/models/buy-crypto/entities/buy-crypto.entity.ts @@ -213,6 +213,7 @@ export class BuyCrypto extends IEntity { } else if (this.amlCheck === AmlCheck.PENDING) { if (this.amlReason === AmlReason.DAILY_LIMIT) return 'mail.payment.pending.dailyLimit'; if (this.amlReason === AmlReason.ANNUAL_LIMIT) return 'mail.payment.pending.annualLimit'; + if (this.amlReason === AmlReason.OLKY_NO_KYC) return 'mail.payment.pending.olkyNoKyc'; } else if (this.amlCheck === AmlCheck.FAIL) { return 'mail.payment.deposit.paybackInitiated'; } @@ -224,15 +225,19 @@ export class BuyCrypto extends IEntity { return this.buy ? this.buy.user : this.cryptoRoute.user; } - get target(): { address: string; asset: Asset } { + get target(): { address: string; asset: Asset; trimmedReturnAddress: string } { return this.buy ? { address: this.buy.deposit?.address ?? this.buy.user.address, asset: this.buy.asset, + trimmedReturnAddress: this.buy?.iban ? Util.trimIBAN(this.buy.iban) : null, } : { address: this.cryptoRoute.targetDeposit?.address ?? this.cryptoRoute.user.address, asset: this.cryptoRoute.asset, + trimmedReturnAddress: this.cryptoRoute?.user?.address + ? Util.trimBlockchainAddress(this.cryptoRoute.user.address) + : null, }; } } diff --git a/src/payment/models/buy-crypto/enums/aml-reason.enum.ts b/src/payment/models/buy-crypto/enums/aml-reason.enum.ts index 188e017abd..2e692af98f 100644 --- a/src/payment/models/buy-crypto/enums/aml-reason.enum.ts +++ b/src/payment/models/buy-crypto/enums/aml-reason.enum.ts @@ -7,4 +7,5 @@ export enum AmlReason { KYC_REJECTED = 'KycRejected', NAME_CHECK = 'NameCheck', OLKY_NO_KYC = 'OlkyNoKyc', + MIN_DEPOSIT_NOT_REACHED = 'MinDepositNotReached', } diff --git a/src/payment/models/buy-crypto/services/buy-crypto-notification.service.ts b/src/payment/models/buy-crypto/services/buy-crypto-notification.service.ts index 706c33c955..18ffeac6f8 100644 --- a/src/payment/models/buy-crypto/services/buy-crypto-notification.service.ts +++ b/src/payment/models/buy-crypto/services/buy-crypto-notification.service.ts @@ -9,6 +9,7 @@ import { BlockchainExplorerUrls } from 'src/blockchain/shared/enums/blockchain.e import { AmlCheck } from '../enums/aml-check.enum'; import { I18nService } from 'nestjs-i18n'; import { AmlReason } from '../enums/aml-reason.enum'; +import { Config } from 'src/config/config'; @Injectable() export class BuyCryptoNotificationService { @@ -32,6 +33,7 @@ export class BuyCryptoNotificationService { txId: Not(IsNull()), isComplete: true, batch: { status: BuyCryptoBatchStatus.COMPLETE }, + amlCheck: AmlCheck.PASS, }, relations: [ 'bankTx', @@ -137,7 +139,7 @@ export class BuyCryptoNotificationService { returnReason: await this.i18nService.translate(`mail.amlReasonMailText.${entity.amlReason}`, { lang: entity.user.userData.language?.symbol.toLowerCase(), }), - userAddressTrimmed: Util.trimBlockchainAddress(entity.user.address), + userAddressTrimmed: entity.target.trimmedReturnAddress, }, }, }); @@ -157,7 +159,7 @@ export class BuyCryptoNotificationService { outputAmount: IsNull(), chargebackDate: IsNull(), chargebackBankTx: IsNull(), - amlReason: In([AmlReason.DAILY_LIMIT, AmlReason.ANNUAL_LIMIT]), + amlReason: In([AmlReason.DAILY_LIMIT, AmlReason.ANNUAL_LIMIT, AmlReason.OLKY_NO_KYC]), amlCheck: AmlCheck.PENDING, }, relations: [ @@ -182,7 +184,7 @@ export class BuyCryptoNotificationService { userData: entity.user.userData, translationKey: entity.translationKey, translationParams: { - hashLink: `https://payment.dfx.swiss/kyc?code=${entity.user.userData.kycHash}`, + hashLink: `${Config.payment.url}/kyc?code=${entity.user.userData.kycHash}`, }, }, }); diff --git a/src/payment/models/buy-fiat/buy-fiat-notification.service.ts b/src/payment/models/buy-fiat/buy-fiat-notification.service.ts index 0ce657a4ad..8a9bf16da1 100644 --- a/src/payment/models/buy-fiat/buy-fiat-notification.service.ts +++ b/src/payment/models/buy-fiat/buy-fiat-notification.service.ts @@ -2,12 +2,14 @@ import { Injectable } from '@nestjs/common'; import { Interval } from '@nestjs/schedule'; import { I18nService } from 'nestjs-i18n'; import { BlockchainExplorerUrls } from 'src/blockchain/shared/enums/blockchain.enum'; +import { Config } from 'src/config/config'; import { MailType } from 'src/notification/enums'; import { NotificationService } from 'src/notification/services/notification.service'; import { Lock } from 'src/shared/lock'; import { Util } from 'src/shared/util'; -import { IsNull, Not } from 'typeorm'; +import { IsNull, Not, In } from 'typeorm'; import { AmlCheck } from '../buy-crypto/enums/aml-check.enum'; +import { AmlReason } from '../buy-crypto/enums/aml-reason.enum'; import { BuyFiatRepository } from './buy-fiat.repository'; @Injectable() @@ -28,6 +30,7 @@ export class BuyFiatNotificationService { await this.cryptoExchangedToFiat(); await this.fiatToBankTransferInitiated(); await this.paybackToAddressInitiated(); + await this.pendingBuyFiat(); this.lock.release(); } @@ -76,7 +79,7 @@ export class BuyFiatNotificationService { mail1SendDate: Not(IsNull()), mail2SendDate: IsNull(), outputAmount: Not(IsNull()), - amlCheck: Not(AmlCheck.FAIL), + amlCheck: AmlCheck.PASS, }, relations: ['sell', 'sell.user', 'sell.user.userData'], }); @@ -116,7 +119,7 @@ export class BuyFiatNotificationService { mail2SendDate: Not(IsNull()), mail3SendDate: IsNull(), bankTx: Not(IsNull()), - amlCheck: Not(AmlCheck.FAIL), + amlCheck: AmlCheck.PASS, }, relations: ['sell', 'sell.user', 'sell.user.userData'], }); @@ -194,4 +197,42 @@ export class BuyFiatNotificationService { } } } + + async pendingBuyFiat(): Promise { + const entities = await this.buyFiatRepo.find({ + where: { + mail2SendDate: IsNull(), + outputAmount: IsNull(), + amlReason: In([AmlReason.DAILY_LIMIT, AmlReason.ANNUAL_LIMIT]), + amlCheck: AmlCheck.PENDING, + }, + relations: ['sell', 'sell.user', 'sell.user.userData'], + }); + + entities.length > 0 && console.log(`Sending ${entities.length} 'pending' email(s)`); + + for (const entity of entities) { + try { + if (entity.sell.user.userData.mail) { + await this.notificationService.sendMail({ + type: MailType.USER, + input: { + userData: entity.sell.user.userData, + translationKey: + entity.amlReason === AmlReason.DAILY_LIMIT + ? 'mail.payment.pending.dailyLimit' + : 'mail.payment.pending.annualLimit', + translationParams: { + hashLink: `${Config.payment.url}/kyc?code=${entity.sell.user.userData.kycHash}`, + }, + }, + }); + } + + await this.buyFiatRepo.update(...entity.pendingMail()); + } catch (e) { + console.error(e); + } + } + } } diff --git a/src/payment/models/buy-fiat/buy-fiat.entity.ts b/src/payment/models/buy-fiat/buy-fiat.entity.ts index e395ef76a3..ccaf64cd86 100644 --- a/src/payment/models/buy-fiat/buy-fiat.entity.ts +++ b/src/payment/models/buy-fiat/buy-fiat.entity.ts @@ -130,6 +130,13 @@ export class BuyFiat extends IEntity { return [this.id, { recipientMail: this.recipientMail, mail1SendDate: this.mail1SendDate }]; } + pendingMail(): UpdateResult { + this.recipientMail = this.sell.user.userData.mail; + this.mail2SendDate = new Date(); + + return [this.id, { recipientMail: this.recipientMail, mail2SendDate: this.mail2SendDate }]; + } + cryptoExchangedToFiat(): UpdateResult { this.mail2SendDate = new Date(); diff --git a/src/payment/models/crypto-input/btc-input.service.ts b/src/payment/models/crypto-input/btc-input.service.ts index 61e6489cb2..7c7cfcb87e 100644 --- a/src/payment/models/crypto-input/btc-input.service.ts +++ b/src/payment/models/crypto-input/btc-input.service.ts @@ -13,27 +13,26 @@ import { NodeNotAccessibleError } from 'src/payment/exceptions/node-not-accessib import { CryptoInputService } from './crypto-input.service'; import { BtcClient } from 'src/blockchain/ain/node/btc-client'; import { CryptoRouteService } from '../crypto-route/crypto-route.service'; -import { HttpService } from 'src/shared/services/http.service'; import { BuyCryptoService } from '../buy-crypto/services/buy-crypto.service'; import { ChainalysisService } from './chainalysis.service'; import { Blockchain } from 'src/blockchain/shared/enums/blockchain.enum'; import { AmlCheck } from '../buy-crypto/enums/aml-check.enum'; +import { BtcFeeService } from 'src/blockchain/ain/services/btc-fee.service'; @Injectable() export class BtcInputService extends CryptoInputService { private readonly lock = new Lock(7200); - private readonly btcFeeUrl = 'https://mempool.space/api/v1/fees/recommended'; private btcClient: BtcClient; constructor( nodeService: NodeService, cryptoInputRepo: CryptoInputRepository, - private readonly http: HttpService, private readonly assetService: AssetService, private readonly cryptoRouteService: CryptoRouteService, private readonly buyCryptoService: BuyCryptoService, private readonly chainalysisService: ChainalysisService, + private readonly feeService: BtcFeeService, ) { super(cryptoInputRepo); nodeService.getConnectedNode(NodeType.BTC_INPUT).subscribe((bitcoinClient) => (this.btcClient = bitcoinClient)); @@ -177,16 +176,8 @@ export class BtcInputService extends CryptoInputService { } private async getFeeRate(amount: number): Promise { - const { fastestFee } = await this.http.get<{ - fastestFee: number; - halfHourFee: number; - hourFee: number; - economyFee: number; - minimumFee: number; - }>(this.btcFeeUrl, { - tryCount: 3, - }); - return Math.floor(Math.max(Math.min(fastestFee, 500 * amount), 1)); + const feeRate = await this.feeService.getRecommendedFeeRate(); + return Math.floor(Math.max(Math.min(feeRate, 500 * amount), 1)); } // --- CONFIRMATION HANDLING --- // diff --git a/src/payment/models/crypto-staking/crypto-staking.service.ts b/src/payment/models/crypto-staking/crypto-staking.service.ts index c08fc6c17c..674c011110 100644 --- a/src/payment/models/crypto-staking/crypto-staking.service.ts +++ b/src/payment/models/crypto-staking/crypto-staking.service.ts @@ -34,7 +34,7 @@ export class CryptoStakingService { private readonly stakingRefRewardRepo: StakingRefRewardRepository, private readonly stakingRepo: StakingRepository, ) { - nodeService.getConnectedNode(NodeType.INPUT).subscribe((client) => (this.client = client)); + nodeService.getConnectedNode(NodeType.REF).subscribe((client) => (this.client = client)); } // --- CRUD --- // diff --git a/src/payment/models/payout/services/payout-bitcoin.service.ts b/src/payment/models/payout/services/payout-bitcoin.service.ts index a6d2d83ab9..e670ddbfda 100644 --- a/src/payment/models/payout/services/payout-bitcoin.service.ts +++ b/src/payment/models/payout/services/payout-bitcoin.service.ts @@ -1,6 +1,7 @@ import { Injectable } from '@nestjs/common'; import { BtcClient } from 'src/blockchain/ain/node/btc-client'; import { NodeService, NodeType } from 'src/blockchain/ain/node/node.service'; +import { BtcFeeService } from 'src/blockchain/ain/services/btc-fee.service'; import { PayoutOrderContext } from '../entities/payout-order.entity'; import { PayoutGroup, PayoutJellyfishService } from './base/payout-jellyfish.service'; @@ -8,7 +9,7 @@ import { PayoutGroup, PayoutJellyfishService } from './base/payout-jellyfish.ser export class PayoutBitcoinService extends PayoutJellyfishService { #client: BtcClient; - constructor(readonly nodeService: NodeService) { + constructor(readonly nodeService: NodeService, private readonly feeService: BtcFeeService) { super(); nodeService.getConnectedNode(NodeType.BTC_OUTPUT).subscribe((client) => (this.#client = client)); } @@ -22,7 +23,8 @@ export class PayoutBitcoinService extends PayoutJellyfishService { } async sendUtxoToMany(_context: PayoutOrderContext, payout: PayoutGroup): Promise { - return this.#client.sendUtxoToMany(payout); + const feeRate = await this.feeService.getRecommendedFeeRate(); + return this.#client.sendMany(payout, feeRate); } async checkPayoutCompletion(_context: any, payoutTxId: string): Promise { diff --git a/src/shared/i18n/de/mail.json b/src/shared/i18n/de/mail.json index 068bb6c733..c54bafc889 100644 --- a/src/shared/i18n/de/mail.json +++ b/src/shared/i18n/de/mail.json @@ -42,13 +42,18 @@ "pending": { "dailyLimit": { "salutation": "Du hast dein tägliches Volumenlimit überschritten", - "body": "
Bitte verifiziere dich, um dein Limit zu erhöhen.
Nach der vollständigen Verifizierung wird deine Transaktion weiter bearbeitet.
Limit erhöhen mit Verizfierung?
Klicke bitte hier um dich zu verifizieren.
Alternativ kannst du auch über die DFX App auf deiner Paymentseite
bei \"Dein Limit\" auf den roten Pfeil klicken
und die Verifizierung starten.
Herzlichen Dank für dein entgegengebrachtes Vertrauen
Dein freundliches DFX Team
Bitcoiners by heart ♥️
", + "body": "
Bitte verifiziere dich, um dein Limit zu erhöhen.
Nach der vollständigen Verifizierung wird deine Transaktion weiter bearbeitet.
Limit erhöhen mit Verizfierung?
Klicke bitte hier um dich zu verifizieren.
Alternativ kannst du auch über die DFX App auf deiner Paymentseite
bei \"Dein Limit\" auf den roten Pfeil klicken
und die Verifizierung starten.
Bei Fragen stehen wir dir gerne unter support@dfx.swiss zur Seite.
Herzlichen Dank für dein entgegengebrachtes Vertrauen
Dein freundliches DFX Team
Bitcoiners by heart ♥️
", "title": "Tägliches Limit überschritten" }, "annualLimit": { "salutation": "Du hast dein jährliches Volumenlimit überschritten", - "body": "
Bitte beantrage daher ein neues Jahreslimit
Sobald das neue Limit genehmigt wurde,
wird deine Transaktion weiter bearbeitet.
Limit erhöhen?
In der DFX App per \"Staking\" die Paymentseite aufrufen.
Bei \"Dein Limit\" auf den roten Pfeil gehen und
ein neues gewünschtes Limit beantragen.
Fragen? - Kontaktiere bitte support@dfx.swiss
Herzlichen Dank für dein entgegengebrachtes Vertrauen
Dein freundliches DFX Team
Bitcoiners by heart ♥️
", + "body": "
Bitte beantrage daher ein neues Jahreslimit
Sobald das neue Limit genehmigt wurde,
wird deine Transaktion weiter bearbeitet.
Limit erhöhen?
In der DFX App per \"Staking\" die Paymentseite aufrufen.
Bei \"Dein Limit\" auf den roten Pfeil gehen und
ein neues gewünschtes Limit beantragen.
Bei Fragen stehen wir dir gerne unter support@dfx.swiss zur Seite.
Herzlichen Dank für dein entgegengebrachtes Vertrauen
Dein freundliches DFX Team
Bitcoiners by heart ♥️
", "title": "Jährliches Limit überschritten" + }, + "olkyNoKyc": { + "salutation": "Verifizierung für SEPA instant notwendig", + "body": "
Um unser Bankkonto in Luxemburg mit SEPA Instant Funktion zu verwenden, ist eine vollständige Verifizierung leider zwingend erforderlich.
Wir bitten dich daher mit diesem Link mittels Ausweis hier zu verifizieren
Alternativ kannst du auch über die DFX App auf deiner Paymentseite
bei \"Dein Limit\" auf den roten Pfeil klicken
und die Verifizierung starten.
Ohne Verifizierung kannst Du auch unser Bankkonto aus der CH weiterhin bis zu 1'000 EUR pro Tag ohne Verifizierung per Standard Sepa verwenden.
Solltest du die Verifizierung nicht vornehmen wollen, wird das Geld vollautomatisch nach 7 Tagen retourniert.
Bei Fragen stehen wir dir gerne unter support@dfx.swiss zur Seite.
Herzlichen Dank für dein entgegengebrachtes Vertrauen
Dein freundliches DFX Team
Bitcoiners by heart ♥️
", + "title": "Für Nutzung unserer Bank in Luxemburg ist eine Verifzierung notwendig" } }, "deposit": { @@ -64,7 +69,7 @@ }, "paybackInitiated":{ "salutation": "Dein Guthaben wurde an dein Bankkonto zurückerstattet", - "body": "
Erstattet{inputAmount} {inputAsset}
Bank Konto***{userAddressTrimmed}
Verwendungszweck{returnTransactionLink}
Grund, warum wir zurückerstattet haben:
{returnReason}
Herzlichen Dank für dein entgegengebrachtes Vertrauen
Dein freundliches DFX Team
Bitcoiners by heart ♥️
", + "body": "
Erstattet{inputAmount} {inputAsset}
Bank Konto{userAddressTrimmed}
Verwendungszweck{returnTransactionLink}
Grund, warum wir zurückerstattet haben:
{returnReason}
Bei Fragen stehen wir dir gerne unter support@dfx.swiss zur Seite.
Herzlichen Dank für dein entgegengebrachtes Vertrauen
Dein freundliches DFX Team
Bitcoiners by heart ♥️
", "title": "Guthaben wurde erstattet" } }, @@ -86,7 +91,7 @@ }, "paybackToAddressInitiated": { "salutation": "Dein Guthaben wurde an deine Wallet-Adresse zurückerstattet", - "body": "
Erstattet{inputAmount} {inputAsset}
Wallet Adresse{userAddressTrimmed}
Dies ist die Transaktions-ID: Link
Grund, warum wir zurückerstattet haben:
{returnReason}
Herzlichen Dank für dein entgegengebrachtes Vertrauen
Dein freundliches DFX Team
Bitcoiners by heart ♥️
", + "body": "
Erstattet{inputAmount} {inputAsset}
Wallet Adresse{userAddressTrimmed}
Dies ist die Transaktions-ID: Link
Grund, warum wir zurückerstattet haben:
{returnReason}
Bei Fragen stehen wir dir gerne unter support@dfx.swiss zur Seite.
Herzlichen Dank für dein entgegengebrachtes Vertrauen
Dein freundliches DFX Team
Bitcoiners by heart ♥️
", "title": "Guthaben wurde erstattet" } @@ -106,6 +111,7 @@ "KycRejected": "Deine Verifizierung wurde abgelehnt", "NameCheck": "Aus rechtlichen Gründen können dich als Kunden leider nicht bedienen", "AnnualLimit": "Du hast das Jahreslimit überschritten", - "OlkyNoKyc": "Für Nutzung der Olkypay Bank musst du dich vorher vollständig verifzieren (KYC)" + "OlkyNoKyc": "Für Nutzung der Olkypay Bank musst du dich vorher vollständig verifzieren (KYC)", + "MinDepositNotReached": "Die Mindesteinzahlungsgrenze wurde nicht erreicht" } } diff --git a/src/shared/i18n/en/mail.json b/src/shared/i18n/en/mail.json index 43d7573eec..d02e656af2 100644 --- a/src/shared/i18n/en/mail.json +++ b/src/shared/i18n/en/mail.json @@ -42,13 +42,18 @@ "pending": { "dailyLimit": { "salutation": "You have exceeded your daily volume limit", - "body": "
Please verify yourself by KYC (know your customer) to increase your limit.
After a successful KYC we proceed with your transaction.
Increase the limit by KYC?
Click here to start the KYC process.
Alternatively, you can also use the DFX app on your payment page,
click on the red arrow next to \"Your limit\"
and start the verification.
Thank you very much for your trust
Your DFX Team
Bitcoiners by heart ♥️
", + "body": "
Please verify yourself by KYC (know your customer) to increase your limit.
After a successful KYC we proceed with your transaction.
Increase the limit by KYC?
Click here to start the KYC process.
Alternatively, you can also use the DFX app on your payment page,
click on the red arrow next to \"Your limit\"
and start the verification.
If you have any questions, we are happy to help you at support@dfx.swiss.
Thank you very much for your trust
Your DFX Team
Bitcoiners by heart ♥️
", "title": "Daily limit exceeded" }, "annualLimit": { "salutation": "You have exceeded your yearly volume limit", - "body": "
Please apply for a new annual limit.
Once the new limit has been approved,
we proceed with your transaction.
Increase the limit?
Open the payment page via \"Staking\" in the DFX App.
Go to the red arrow under \"Your limit\" and
request a new desired limit.
Questions? - Please contact support@dfx.swiss
Thank you very much for your trust
Your DFX Team
Bitcoiners by heart ♥️
", + "body": "
Please apply for a new annual limit.
Once the new limit has been approved,
we proceed with your transaction.
Increase the limit?
Open the payment page via \"Staking\" in the DFX App.
Go to the red arrow under \"Your limit\" and
request a new desired limit.
If you have any questions, we are happy to help you at support@dfx.swiss.
Thank you very much for your trust
Your DFX Team
Bitcoiners by heart ♥️
", "title": "Annual limit exceeded" + }, + "olkyNoKyc": { + "salutation": "Verification by KYC is required to use our bank in Luxembourg", + "body": "
In order to use our bank account in Luxembourg with the SEPA Instant function, full verification is unfortunately mandatory.
We therefore ask you to use this link here to perform KYC with your ID card
Alternatively, you can also use the DFX app on your payment page
to click on the red arrow next to \"Your limit\"
and start KYC
Without verification, you can continue to use our bank account from Switzerland up to EUR 1,000 per day without KYC via standard Sepa.
If you do not want to carry out the verification, the money will be returned fully automatically after 7 days.
If you have any questions, we are happy to help you at support@dfx.swiss.
Thank you very much for your trust
Your DFX Team
Bitcoiners by heart ♥️
", + "title": "KYC required to use SEPA instant wire transfers" } }, "deposit": { @@ -64,7 +69,7 @@ }, "paybackInitiated":{ "salutation": "Your funds have been reimbursed to your bank account", - "body": "
Reimbursed amount{inputAmount} {inputAsset}
Bank account***{userAddressTrimmed}
Purpose of payment{returnTransactionLink}
Reason why we reimbursed your funds:
{returnReason}
Thank you very much for your trust
Your DFX Team
Bitcoiners by heart ♥️
", + "body": "
Reimbursed amount{inputAmount} {inputAsset}
Bank account{userAddressTrimmed}
Purpose of payment{returnTransactionLink}
Reason why we reimbursed your funds:
{returnReason}
If you have any questions, we are happy to help you at support@dfx.swiss.
Thank you very much for your trust
Your DFX Team
Bitcoiners by heart ♥️
", "title": "Funds have been reimbursed" } }, @@ -86,7 +91,7 @@ }, "paybackToAddressInitiated": { "salutation": "Your funds have been reimbursed to your wallet address", - "body": "
Reimbursed{inputAmount} {inputAsset}
Wallet address{userAddressTrimmed}
Here you can find the transaction ID with your withdrawal: Link
Reason why we reimbursed your funds:
{returnReason}
Thank you very much for your trust
Your DFX Team
Bitcoiners by heart ♥️
", + "body": "
Reimbursed{inputAmount} {inputAsset}
Wallet address{userAddressTrimmed}
Here you can find the transaction ID with your withdrawal: Link
Reason why we reimbursed your funds:
{returnReason}
If you have any questions, we are happy to help you at support@dfx.swiss.
Thank you very much for your trust
Your DFX Team
Bitcoiners by heart ♥️
", "title": "Funds have been reimbursed" } } @@ -105,6 +110,7 @@ "KycRejected": "Your KYC Request was rejected", "NameCheck": "For legal reasons we can not serve you as a customer", "AnnualLimit": "You exceeded your annual limit", - "OlkyNoKyc": "You have to complete KYC for DFX in order to use Olkypay Bank" + "OlkyNoKyc": "You have to complete KYC for DFX in order to use Olkypay Bank", + "MinDepositNotReached": "The minimum deposit limit was not reached" } } diff --git a/src/shared/i18n/es/mail.json b/src/shared/i18n/es/mail.json index a9a27d29b7..8fd7b6f020 100644 --- a/src/shared/i18n/es/mail.json +++ b/src/shared/i18n/es/mail.json @@ -42,13 +42,18 @@ "pending": { "dailyLimit": { "salutation": "Has excedido el límite de volumen diario", - "body": "
Por favor, verificate a través de KYC (conoce a tu cliente) para incrementar tu límite.
Después de que pases el KYC empezaremos con tu transacción.
¿Quieres incrementar el límite a través del KYC?
Aprieta aquí para empezar el proceso KYC.
Alternativamente, puedes usar la app DFX en la página de pago,
apretar la flecha roja al lado de \"Tu límite\"
y empieza la verificación.
Muchas gracias por tu confianza
Tu Equipo DFX
Bitcoiners by heart ♥️
", + "body": "
Por favor, verificate a través de KYC (conoce a tu cliente) para incrementar tu límite.
Después de que pases el KYC empezaremos con tu transacción.
¿Quieres incrementar el límite a través del KYC?
Aprieta aquí para empezar el proceso KYC.
Alternativamente, puedes usar la app DFX en la página de pago,
apretar la flecha roja al lado de \"Tu límite\"
y empieza la verificación.
Si tienes alguna pregunta, nos encantaría ayudarte en support@dfx.swiss.
Muchas gracias por tu confianza
Tu Equipo DFX
Bitcoiners by heart ♥️
", "title": "Límite diario excedido" }, "annualLimit": { "salutation": "Has excedido tu límite de volumen anual", - "body": "
Por favor, registra tu interés para un límite anual nuevo.
Una vez el nuevo límite anual de haya aprobado,
empezaremos con tu transacción.
¿Quieres incrementar el límite?
Abre la aplicación DFX a través de la página de pago en \"Staking\".
Ves a la flecha roja debajo de \"Tu límite\" y
pide un límite nuevo.
¿Alguna pregunta? Por favor ponte en contacto con support@dfx.swiss
Muchas gracias por tu confianza
Tu Equipo DFX
Bitcoiners by heart ♥️
", + "body": "
Por favor, registra tu interés para un límite anual nuevo.
Una vez el nuevo límite anual de haya aprobado,
empezaremos con tu transacción.
¿Quieres incrementar el límite?
Abre la aplicación DFX a través de la página de pago en \"Staking\".
Ves a la flecha roja debajo de \"Tu límite\" y
pide un límite nuevo.
Si tienes alguna pregunta, nos encantaría ayudarte en support@dfx.swiss.
Muchas gracias por tu confianza
Tu Equipo DFX
Bitcoiners by heart ♥️
", "title": "Has excedido el límite anual" + }, + "olkyNoKyc": { + "salutation": "La verificación de KYC es requerida para usar nuestro banco en Luxemburgo", + "body": "
Para poder usar nuestra cuenta bancaria en Luxemburgo con la función instantánea SEPA, la verificación completa es desafortunadamente obligatoria.
Por ello, les pedimos que usen este enlace aquí para ejecutar el KYC con tu tarjeta de identificación
Alternativamente, puedes usar la app DFX en la página de pagos
y apretar en la flecha roja al lado de \"Tu límite\"
y empiezas el KYC
Sin la verificación, puedes continuar usando nuestro banco en Suiza hasta 1,000 euros al día sin el KYC con transferencias SEPA ordinarias.
Si no quieres pasar el proceso de verificación, el dinero se devolverá en su totalidad a partir del séptimo día.
Si tienes alguna pregunta, nos encantaría ayudarte en support@dfx.swiss.
Thank you very much for your trust
Your DFX Team
Bitcoiners by heart ♥️
", + "title": "Se requiere el registro KYC para usar las transferencias SEPA instantáneas" } }, "deposit": { @@ -64,7 +69,7 @@ }, "paybackInitiated":{ "salutation": "Tus fondos se han reembolsado a tu cuenta bancaria", - "body": "
Cantidad reembolsada{inputAmount} {inputAsset}
Cuenta bancaria***{userAddressTrimmed}
Propósito de pago{returnTransactionLink}
Motivo por el cual tus fondos se reembolsaron:
{returnReason}
Muchas gracias por tu confianza
Tu Equipo DFX
Bitcoiners by heart ♥️
", + "body": "
Cantidad reembolsada{inputAmount} {inputAsset}
Cuenta bancaria{userAddressTrimmed}
Propósito de pago{returnTransactionLink}
Motivo por el cual tus fondos se reembolsaron:
{returnReason}
Si tienes alguna pregunta, nos encantaría ayudarte en support@dfx.swiss.
Muchas gracias por tu confianza
Tu Equipo DFX
Bitcoiners by heart ♥️
", "title": "Los fondos han sido reembolsados" } }, @@ -86,7 +91,7 @@ }, "paybackToAddressInitiated": { "salutation": "Tus fondos han sido reembolsados a la dirección de tu billetera", - "body": "
Reembolsados{inputAmount} {inputAsset}
Dirección de la billetera{userAddressTrimmed}
Aquí puedes encontrar el identificador de tu retiro: Link
Motivo por el cual tus fondos se reembolsaron:
{returnReason}
Muchas gracias por tu confianza
Tu Equipo DFX
Bitcoiners by heart ♥️
", + "body": "
Reembolsados{inputAmount} {inputAsset}
Dirección de la billetera{userAddressTrimmed}
Aquí puedes encontrar el identificador de tu retiro: Link
Motivo por el cual tus fondos se reembolsaron:
{returnReason}
Si tienes alguna pregunta, nos encantaría ayudarte en support@dfx.swiss.
Muchas gracias por tu confianza
Tu Equipo DFX
Bitcoiners by heart ♥️
", "title": "Los fondos han sido reembolsados" } } @@ -105,6 +110,7 @@ "KycRejected": "Tu propuesta de registro KYC fue rechazada", "NameCheck": "Por razones legales, no podemos servirle como cliente", "AnnualLimit": "Excedes el límite anual", - "OlkyNoKyc": "Tienes que completar KYC para DFX para poder usar Olkypay Bank" + "OlkyNoKyc": "Tienes que completar KYC para DFX para poder usar Olkypay Bank", + "MinDepositNotReached": "La cantidad de depósito mínima no ha sido alcanzada" } } diff --git a/src/shared/i18n/fr/mail.json b/src/shared/i18n/fr/mail.json index eaac7c3b9c..e78ce04d2f 100644 --- a/src/shared/i18n/fr/mail.json +++ b/src/shared/i18n/fr/mail.json @@ -42,13 +42,18 @@ "pending": { "dailyLimit": { "salutation": "Vous avez dépassé votre volume journalier", - "body": "
Veuillez vous faire vérifier par le KYC (know your customer) pour augmenter votre limite.
Nous traiterons votre transaction après la réussite du KYC.
Augmenter la limite par un KYC ?
Cliquer here pour commencer la procédure de KYC.
Autrement, vous pouvez aussi utiliser l'appli DFX sur votre page de paiement,
cliquer sur la flèche rouge en face de \"Votre limite\"
et commencer la vérification.
Merci beaucoup pour votre confiance
Votre équipe DFX
Bitcoiners by heart ♥️
", + "body": "
Veuillez vous faire vérifier par le KYC (know your customer) pour augmenter votre limite.
Nous traiterons votre transaction après la réussite du KYC.
Augmenter la limite par un KYC ?
Cliquer here pour commencer la procédure de KYC.
Autrement, vous pouvez aussi utiliser l'appli DFX sur votre page de paiement,
cliquer sur la flèche rouge en face de \"Votre limite\"
et commencer la vérification.
Si vous avez des questions, nous sommes ravis de vous aider sur support@dfx.swiss.
Merci beaucoup pour votre confiance
Votre équipe DFX
Bitcoiners by heart ♥️
", "title": "Limite journalière dépassée" }, "annualLimit": { "salutation": "Vous avez dépassé votre volume annuel", - "body": "
Veuillez demander une nouvelle limite annuelle.
Nous traiterons votre transaction après
l'augmentation de votre limite.
Augmenter la limite ?
Ouvrez la page de paiement via le bouton \"Staking\" dans l'appli DFX.
Rendez-vous sur la flèche rouge sous \"Votre limite\" et
demandez une nouvelle limite.
Des questions ? Veuillez contacter support@dfx.swiss
Merci beaucoup pour votre confiance
Votre équipe DFX
Bitcoiners by heart ♥️
", + "body": "
Veuillez demander une nouvelle limite annuelle.
Nous traiterons votre transaction après
l'augmentation de votre limite.
Augmenter la limite ?
Ouvrez la page de paiement via le bouton \"Staking\" dans l'appli DFX.
Rendez-vous sur la flèche rouge sous \"Votre limite\" et
demandez une nouvelle limite.
Si vous avez des questions, nous sommes ravis de vous aider sur support@dfx.swiss.
Merci beaucoup pour votre confiance
Votre équipe DFX
Bitcoiners by heart ♥️
", "title": "Limite annuelle dépassée" + }, + "olkyNoKyc": { + "salutation": "Une vérification KYC est nécessaire pour utiliser notre banque luxembourgeoise", + "body": "
Pour utiliser notre compte bancaire au Luxembourg lors d'un virement instantané, une vérification complète doit être réalisée.
Nous vous demandons donc d'utiliser ce lien ici pour réaliser le KYC avec votre carte d'identité
Autrement, vous pouvez utiliser l'appli DFX et vous rendre sur votre page de paiement
pour cliquer sur la flèche rouge à côté de \"Votre limite\"
et démarrer le KYC
Sans vérification, vous pouvez continuer à utiliser notre compte bancaire suisse jusqu'à 1 000 euros par jour en SEPA standard.
Si vous ne réalisez pas la vérification, l'argent sera retourné automatiquement après 7 jours.
Si vous avez des questions, nous sommes ravis de vous aider sur support@dfx.swiss.
Thank you very much for your trust
Your DFX Team
Bitcoiners by heart ♥️
", + "title": "Le KYC est nécessaire pour utiliser le virement SEPA instantané" } }, "deposit": { @@ -64,7 +69,7 @@ }, "paybackInitiated":{ "salutation": "Vos fonds ont été retournés à votre banque", - "body": "
Montant retourné{inputAmount} {inputAsset}
Compte bancaire***{userAddressTrimmed}
Motif du paiement{returnTransactionLink}
Motif du remboursement:
{returnReason}
Merci beaucoup pour votre confiance
Votre équipe DFX
Bitcoiners by heart ♥️
", + "body": "
Montant retourné{inputAmount} {inputAsset}
Compte bancaire{userAddressTrimmed}
Motif du paiement{returnTransactionLink}
Motif du remboursement:
{returnReason}
Si vous avez des questions, nous sommes ravis de vous aider sur support@dfx.swiss.
Merci beaucoup pour votre confiance
Votre équipe DFX
Bitcoiners by heart ♥️
", "title": "Remboursement des fonds" } }, @@ -86,7 +91,7 @@ }, "paybackToAddressInitiated": { "salutation": "Vos fonds ont été remboursés sur votre adresse de wallet", - "body": "
Remboursés{inputAmount} {inputAsset}
Adresse de wallet{userAddressTrimmed}
Vous trouverez l'ID de la transaction: Link
Motif du remboursement:
{returnReason}
Merci beaucoup pour votre confiance
Votre équipe DFX
Bitcoiners by heart ♥️
", + "body": "
Remboursés{inputAmount} {inputAsset}
Adresse de wallet{userAddressTrimmed}
Vous trouverez l'ID de la transaction: Link
Motif du remboursement:
{returnReason}
Si vous avez des questions, nous sommes ravis de vous aider sur support@dfx.swiss.
Merci beaucoup pour votre confiance
Votre équipe DFX
Bitcoiners by heart ♥️
", "title": "Remboursement des fonds" } } @@ -105,6 +110,7 @@ "KycRejected": "Votre demande KYC a été rejetée", "NameCheck": "Pour des raisons légales, nous ne pouvons vous servir comme client", "AnnualLimit": "Vous avez dépassé votre limite annuelle", - "OlkyNoKyc": "Vous devez terminer le KYC de DFX pour utiliser Olkypay Bank" + "OlkyNoKyc": "Vous devez terminer le KYC de DFX pour utiliser Olkypay Bank", + "MinDepositNotReached": "La limite de dépôt minimum n'a pas été atteinte" } } diff --git a/src/shared/i18n/it/mail.json b/src/shared/i18n/it/mail.json index bee00779fa..0fac9d7594 100644 --- a/src/shared/i18n/it/mail.json +++ b/src/shared/i18n/it/mail.json @@ -42,13 +42,18 @@ "pending": { "dailyLimit": { "salutation": "Hai superato il tuo limite di volume giornaliero", - "body": "
Per favore, completa la verifica KYC (conosci il tuo cliente) per incrementare il tuo limite.
Dopo una verifica KYC eseguita con successo procederemo con le tue transazioni.
Incrementare il limite con la KYC?
Clicca qui per avviare il processo di KYC.
Alternativamente, puoi anche usare l'app DFX nella tua pagina di pagamento,
cliccare sulla freccia rossa vicino a \"Il tuo limite\"
e avviare la verifica.
Grazie mille per la tua fiducia
Tuo DFX team
Bitcoiners by heart ♥️
", + "body": "
Per favore, completa la verifica KYC (conosci il tuo cliente) per incrementare il tuo limite.
Dopo una verifica KYC eseguita con successo procederemo con le tue transazioni.
Incrementare il limite con la KYC?
Clicca qui per avviare il processo di KYC.
Alternativamente, puoi anche usare l'app DFX nella tua pagina di pagamento,
cliccare sulla freccia rossa vicino a \"Il tuo limite\"
e avviare la verifica.
Se hai domande, siamo felici di aiutarti su support@dfx.swiss.
Grazie mille per la tua fiducia
Tuo DFX team
Bitcoiners by heart ♥️
", "title": "Limite giornaliero superato" }, "annualLimit": { "salutation": "Hai superato il tuo limite di volume annuo", - "body": "
Per favore, richiedi un nuovo limite annuale.
Una volta che il nuovo limite è stato approvato,
procederemo con la tua transazione.
Aumentare il limite?
Apri la pagina di pagamento attraverso \"Staking\" nell'app DFX.
Vai sulla freccia rossa sotto \"Il tuo limite\" e
fai richiesta per un nuovo limite desiderato.
Domande? - Per favore, contatta support@dfx.swiss
Grazie mille per la tua fiducia
Tuo DFX team
Bitcoiners by heart ♥️
", + "body": "
Per favore, richiedi un nuovo limite annuale.
Una volta che il nuovo limite è stato approvato,
procederemo con la tua transazione.
Aumentare il limite?
Apri la pagina di pagamento attraverso \"Staking\" nell'app DFX.
Vai sulla freccia rossa sotto \"Il tuo limite\" e
fai richiesta per un nuovo limite desiderato.
Se hai domande, siamo felici di aiutarti su support@dfx.swiss.
Grazie mille per la tua fiducia
Tuo DFX team
Bitcoiners by heart ♥️
", "title": "Limite annuale superato" + }, + "olkyNoKyc": { + "salutation": "La verifica KYC è richiesta per usare la nostra banca in Lussemburgo", + "body": "
Per usare il nostro conto corrente in Lussemburgo con la funzionalità SEPA istantanea, una verifica completa è sfortunatamente obbligatoria.
Ti chiediamo quindi di usare questo link qui per eseguire la KYC col tuo documento d'identità
Alternativamente, puoi anche usare l'app DFX nella tua pagina dei pagamenti
per cliccare sulla freccia rossa vicino a \"Il tuo limite\"
ed avviare la KYC
Senza verifica, puoi continuare a usare il nostro conto corrente svizzero fino a 1000€ al giorno senza KYC utilizzando il SEPA standard.
Se non vuoi eseguire la verifica, i soldi saranno pienamente rimborsati in modo automatico dopo 7 giorni.
Se hai domande, siamo felici di aiutarti su support@dfx.swiss.
Thank you very much for your trust
Your DFX Team
Bitcoiners by heart ♥️
", + "title": "KYC richiesta per utilizzare i trasferimenti immediati SEPA" } }, "deposit": { @@ -64,7 +69,7 @@ }, "paybackInitiated":{ "salutation": "I tuoi fondi sono stati rimborsati sul tuo conto corrente", - "body": "
Importo rimborsato{inputAmount} {inputAsset}
Conto corrente***{userAddressTrimmed}
Causale di pagamento{returnTransactionLink}
Ragioni per cui abbiamo rimborsato i tuoi fondi:
{returnReason}
Grazie mille per la tua fiducia
Tuo DFX team
Bitcoiners by heart ♥️
", + "body": "
Importo rimborsato{inputAmount} {inputAsset}
Conto corrente{userAddressTrimmed}
Causale di pagamento{returnTransactionLink}
Ragioni per cui abbiamo rimborsato i tuoi fondi:
{returnReason}
Se hai domande, siamo felici di aiutarti su support@dfx.swiss.
Grazie mille per la tua fiducia
Tuo DFX team
Bitcoiners by heart ♥️
", "title": "Trasferimento bancario effettuato" } }, @@ -85,8 +90,8 @@ "title": "Trasferimento bancario effettuato" }, "paybackToAddressInitiated": { - "salutation": "I tuoi fondi sono stati rimborsati all’indirizzo del tuo portafoglio", - "body": "
Rimborsato{inputAmount} {inputAsset}
Indirizzo portafoglio{userAddressTrimmed}
Qui trovi l’ID della transazione con il tuo prelievo: Link
Ragioni per cui abbiamo rimborsato i tuoi fondi:
{returnReason}
Grazie mille per la tua fiducia
Tuo DFX team
Bitcoiners by heart ♥️
", + "salutation": "I tuoi fondi sono stati rimborsati all'indirizzo del tuo portafoglio", + "body": "
Rimborsato{inputAmount} {inputAsset}
Indirizzo portafoglio{userAddressTrimmed}
Qui trovi l'ID della transazione con il tuo prelievo: Link
Ragioni per cui abbiamo rimborsato i tuoi fondi:
{returnReason}
Se hai domande, siamo felici di aiutarti su support@dfx.swiss.
Grazie mille per la tua fiducia
Tuo DFX team
Bitcoiners by heart ♥️
", "title": "I tuoi fondi sono stati rimborsati" } } @@ -105,6 +110,7 @@ "KycRejected": "La tua richiesta di KYC è stata rigettata", "NameCheck": "Per ragioni legali non possiamo servirti come cliente", "AnnualLimit": "Hai superato il tuo limite annuale", - "OlkyNoKyc": "Devi completare la KYC per DFX per poter utilizzare Olkypay Bank" + "OlkyNoKyc": "Devi completare la KYC per DFX per poter utilizzare Olkypay Bank", + "MinDepositNotReached": "Il limite di deposito minimo non è stato raggiunto" } } diff --git a/src/shared/i18n/pt/mail.json b/src/shared/i18n/pt/mail.json index 439b362176..4b36b2856e 100644 --- a/src/shared/i18n/pt/mail.json +++ b/src/shared/i18n/pt/mail.json @@ -42,13 +42,18 @@ "pending": { "dailyLimit": { "salutation": "You have exceeded your daily volume limit", - "body": "
Please verify yourself by KYC (know your customer) to increase your limit.
After a successful KYC we proceed with your transaction.
Increase the limit by KYC?
Click here to start the KYC process.
Alternatively, you can also use the DFX app on your payment page,
click on the red arrow next to \"Your limit\"
and start the verification.
Thank you very much for your trust
Your DFX Team
Bitcoiners by heart ♥️
", + "body": "
Please verify yourself by KYC (know your customer) to increase your limit.
After a successful KYC we proceed with your transaction.
Increase the limit by KYC?
Click here to start the KYC process.
Alternatively, you can also use the DFX app on your payment page,
click on the red arrow next to \"Your limit\"
and start the verification.
If you have any questions, we are happy to help you at support@dfx.swiss.
Thank you very much for your trust
Your DFX Team
Bitcoiners by heart ♥️
", "title": "Daily limit exceeded" }, "annualLimit": { "salutation": "You have exceeded your yearly volume limit", - "body": "
Please apply for a new annual limit.
Once the new limit has been approved,
we proceed with your transaction.
Increase the limit?
Open the payment page via \"Staking\" in the DFX App.
Go to the red arrow under \"Your limit\" and
request a new desired limit.
Questions? - Please contact support@dfx.swiss
Thank you very much for your trust
Your DFX Team
Bitcoiners by heart ♥️
", + "body": "
Please apply for a new annual limit.
Once the new limit has been approved,
we proceed with your transaction.
Increase the limit?
Open the payment page via \"Staking\" in the DFX App.
Go to the red arrow under \"Your limit\" and
request a new desired limit.
If you have any questions, we are happy to help you at support@dfx.swiss.
Thank you very much for your trust
Your DFX Team
Bitcoiners by heart ♥️
", "title": "Annual limit exceeded" + }, + "olkyNoKyc": { + "salutation": "Verification by KYC is required to use our bank in Luxembourg", + "body": "
In order to use our bank account in Luxembourg with the SEPA Instant function, full verification is unfortunately mandatory.
We therefore ask you to use this link here to perform KYC with your ID card
Alternatively, you can also use the DFX app on your payment page
to click on the red arrow next to \"Your limit\"
and start KYC
Without verification, you can continue to use our bank account from Switzerland up to EUR 1,000 per day without KYC via standard Sepa.
If you do not want to carry out the verification, the money will be returned fully automatically after 7 days.
If you have any questions, we are happy to help you at support@dfx.swiss.
Thank you very much for your trust
Your DFX Team
Bitcoiners by heart ♥️
", + "title": "KYC required to use SEPA instant wire transfers" } }, "deposit": { @@ -64,7 +69,7 @@ }, "paybackInitiated":{ "salutation": "Your funds have been reimbursed to your bank account", - "body": "
Reimbursed amount{inputAmount} {inputAsset}
Bank account***{userAddressTrimmed}
Purpose of payment{returnTransactionLink}
Reason why we reimbursed your funds:
{returnReason}
Thank you very much for your trust
Your DFX Team
Bitcoiners by heart ♥️
", + "body": "
Reimbursed amount{inputAmount} {inputAsset}
Bank account{userAddressTrimmed}
Purpose of payment{returnTransactionLink}
Reason why we reimbursed your funds:
{returnReason}
If you have any questions, we are happy to help you at support@dfx.swiss.
Thank you very much for your trust
Your DFX Team
Bitcoiners by heart ♥️
", "title": "Funds have been reimbursed" } }, @@ -86,7 +91,7 @@ }, "paybackToAddressInitiated": { "salutation": "Your funds have been reimbursed to your wallet address", - "body": "
Reimbursed{inputAmount} {inputAsset}
Wallet address{userAddressTrimmed}
Here you can find the transaction ID with your withdrawal: Link
Reason why we reimbursed your funds:
{returnReason}
Thank you very much for your trust
Your DFX Team
Bitcoiners by heart ♥️
", + "body": "
Reimbursed{inputAmount} {inputAsset}
Wallet address{userAddressTrimmed}
Here you can find the transaction ID with your withdrawal: Link
Reason why we reimbursed your funds:
{returnReason}
If you have any questions, we are happy to help you at support@dfx.swiss.
Thank you very much for your trust
Your DFX Team
Bitcoiners by heart ♥️
", "title": "Funds have been reimbursed" } } @@ -105,6 +110,7 @@ "KycRejected": "Your KYC Request was rejected", "NameCheck": "For legal reasons we can not serve you as a customer", "AnnualLimit": "You exceeded your annual limit", - "OlkyNoKyc": "You have to complete KYC for DFX in order to use Olkypay Bank" + "OlkyNoKyc": "You have to complete KYC for DFX in order to use Olkypay Bank", + "MinDepositNotReached": "The minimum deposit limit was not reached" } } diff --git a/src/statistic/statistic.service.ts b/src/statistic/statistic.service.ts index 7b8bdcafb1..1b9a59db51 100644 --- a/src/statistic/statistic.service.ts +++ b/src/statistic/statistic.service.ts @@ -1,11 +1,13 @@ import { Injectable } from '@nestjs/common'; import { Interval } from '@nestjs/schedule'; +import { Config } from 'src/config/config'; import { BuyService } from 'src/payment/models/buy/buy.service'; import { MasternodeService } from 'src/payment/models/masternode/masternode.service'; import { SellService } from 'src/payment/models/sell/sell.service'; import { StakingRewardService } from 'src/payment/models/staking-reward/staking-reward.service'; import { StakingService } from 'src/payment/models/staking/staking.service'; import { SettingService } from 'src/shared/models/setting/setting.service'; +import { Util } from 'src/shared/util'; import { UserService } from 'src/user/models/user/user.service'; @Injectable() @@ -29,12 +31,12 @@ export class StatisticService { try { this.statistic = { totalVolume: { - buy: await this.buyService.getTotalVolume(), - sell: await this.sellService.getTotalVolume(), + buy: Util.round(await this.buyService.getTotalVolume(), Config.defaultVolumeDecimal), + sell: Util.round(await this.sellService.getTotalVolume(), Config.defaultVolumeDecimal), }, totalRewards: { - staking: await this.stakingService.getTotalStakingRewards(), - ref: await this.userService.getTotalRefRewards(), + staking: Util.round(await this.stakingService.getTotalStakingRewards(), Config.defaultVolumeDecimal), + ref: Util.round(await this.userService.getTotalRefRewards(), Config.defaultVolumeDecimal), }, staking: { masternodes: await this.masternodeService.getActiveCount(), diff --git a/src/user/models/auth/auth.controller.ts b/src/user/models/auth/auth.controller.ts index b5ee33f577..9bbd735c76 100644 --- a/src/user/models/auth/auth.controller.ts +++ b/src/user/models/auth/auth.controller.ts @@ -30,4 +30,9 @@ export class AuthController { signInCompany(@Body() credentials: AuthCredentialsDto): Promise<{ accessToken: string }> { return this.authService.companySignIn(credentials); } + + @Post('company/challenge') + companyChallenge(@Query('address') address: string): Promise<{ challenge: string }> { + return this.authService.getCompanyChallenge(address); + } } diff --git a/src/user/models/auth/auth.service.ts b/src/user/models/auth/auth.service.ts index 660628bf8f..fda4ce6cb7 100644 --- a/src/user/models/auth/auth.service.ts +++ b/src/user/models/auth/auth.service.ts @@ -21,9 +21,19 @@ import { Blockchain } from 'src/blockchain/shared/enums/blockchain.enum'; import { WalletRepository } from '../wallet/wallet.repository'; import { Wallet } from '../wallet/wallet.entity'; import { UserRole } from 'src/shared/auth/user-role.enum'; +import { Util } from 'src/shared/util'; +import { Interval } from '@nestjs/schedule'; +import { randomUUID } from 'crypto'; + +export interface ChallengeData { + created: Date; + challenge: string; +} @Injectable() export class AuthService { + private challengeList: Map = new Map(); + constructor( private readonly userService: UserService, private readonly userRepo: UserRepository, @@ -33,13 +43,24 @@ export class AuthService { private readonly refService: RefService, ) {} + @Interval(90000) + checkChallengeList() { + for (const [key, challenge] of this.challengeList.entries()) { + if (!this.isChallengeValid(challenge)) { + this.challengeList.delete(key); + } + } + } + + // --- AUTH METHODS --- // async signUp(dto: CreateUserDto, userIp: string): Promise<{ accessToken: string }> { const existingUser = await this.userRepo.getByAddress(dto.address); if (existingUser) { throw new ConflictException('User already exists'); } - if (!this.verifySignature(dto.address, dto.signature)) { + const { message } = this.getSignMessage(dto.address); + if (!this.verifySignature(message, dto.address, dto.signature)) { throw new BadRequestException('Invalid signature'); } @@ -56,7 +77,8 @@ export class AuthService { const user = await this.userRepo.getByAddress(address); if (!user) throw new NotFoundException('User not found'); - const credentialsValid = this.verifySignature(address, signature); + const { message } = this.getSignMessage(address); + const credentialsValid = this.verifySignature(message, address, signature); if (!credentialsValid) throw new UnauthorizedException('Invalid credentials'); // TODO: temporary code to update old wallet signatures @@ -67,17 +89,36 @@ export class AuthService { return { accessToken: this.generateUserToken(user) }; } - async companySignIn({ address, signature }: AuthCredentialsDto): Promise<{ accessToken: string }> { - const wallet = await this.walletRepo.getByAddress(address); + async companySignIn(dto: AuthCredentialsDto): Promise<{ accessToken: string }> { + const wallet = await this.walletRepo.findOne({ where: { address: dto.address } }); if (!wallet || !wallet.isKycClient) throw new NotFoundException('Wallet not found'); - // TODO add challenge response - const credentialsValid = this.verifyCompanySignature(address, signature); + const credentialsValid = this.verifyCompanySignature(dto); if (!credentialsValid) throw new UnauthorizedException('Invalid credentials'); return { accessToken: this.generateCompanyToken(wallet) }; } + async getCompanyChallenge(address: string): Promise<{ challenge: string }> { + const wallet = await this.walletRepo.findOne({ where: { address: address } }); + if (!wallet || !wallet.isKycClient) throw new BadRequestException('Wallet not found/invalid'); + + const challenge = randomUUID(); + + this.challengeList.set(address, { created: new Date(), challenge: challenge }); + + return { challenge: challenge }; + } + + async changeUser(id: number, changeUser: LinkedUserInDto): Promise<{ accessToken: string }> { + const user = await this.getLinkedUser(id, changeUser.address); + if (!user) throw new NotFoundException('User not found'); + if (user.stakingBalance > 0) throw new ForbiddenException('Change user not allowed'); + return { accessToken: this.generateUserToken(user) }; + } + + // --- HELPER METHODS --- // + getSignMessage(address: string): { message: string; blockchains: Blockchain[] } { const blockchains = this.cryptoService.getBlockchainsBasedOn(address); return { @@ -94,13 +135,6 @@ export class AuthService { }; } - async changeUser(id: number, changeUser: LinkedUserInDto): Promise<{ accessToken: string }> { - const user = await this.getLinkedUser(id, changeUser.address); - if (!user) throw new NotFoundException('User not found'); - if (user.stakingBalance > 0) throw new ForbiddenException('Change user not allowed'); - return { accessToken: this.generateUserToken(user) }; - } - private async getLinkedUser(id: number, address: string): Promise { return this.userRepo .createQueryBuilder('user') @@ -112,14 +146,16 @@ export class AuthService { .getRawOne(); } - private verifySignature(address: string, signature: string): boolean { - const signatureMessage = this.getSignMessage(address); - return this.cryptoService.verifySignature(signatureMessage.message, address, signature); + private verifySignature(message: string, address: string, signature: string): boolean { + return this.cryptoService.verifySignature(message, address, signature); } - private verifyCompanySignature(address: string, signature: string): boolean { - const signatureMessage = this.getCompanySignMessage(address); - return this.cryptoService.verifySignature(signatureMessage.message, address, signature); + private verifyCompanySignature(dto: AuthCredentialsDto): boolean { + const challengeData = this.challengeList.get(dto.address); + if (!this.isChallengeValid(challengeData)) throw new UnauthorizedException('Challenge invalid'); + this.challengeList.delete(dto.address); + + return this.verifySignature(challengeData.challenge, dto.address, dto.signature); } private generateUserToken(user: User): string { @@ -138,6 +174,10 @@ export class AuthService { address: wallet.address, role: UserRole.KYC_CLIENT_COMPANY, }; - return this.jwtService.sign(payload); + return this.jwtService.sign(payload, { expiresIn: Config.auth.company.signOptions.expiresIn }); + } + + private isChallengeValid(challenge: ChallengeData): boolean { + return challenge && Util.secondsDiff(challenge.created, new Date()) <= Config.auth.challenge.expiresIn; } } diff --git a/src/user/models/ident/ident.service.ts b/src/user/models/ident/ident.service.ts index 1605b54238..256f733d7c 100644 --- a/src/user/models/ident/ident.service.ts +++ b/src/user/models/ident/ident.service.ts @@ -21,7 +21,7 @@ export class IdentService { spiderData: { identIdentificationIds: Like(`%${result?.identificationprocess?.id}%`) }, }, ], - relations: ['spiderData'], + relations: ['spiderData', 'users', 'users.wallet'], }); if (!user) { diff --git a/src/user/models/kyc/kyc-process.service.ts b/src/user/models/kyc/kyc-process.service.ts index ea96163121..37831ade57 100644 --- a/src/user/models/kyc/kyc-process.service.ts +++ b/src/user/models/kyc/kyc-process.service.ts @@ -24,7 +24,8 @@ export class KycProcessService { // --- GENERAL METHODS --- // async startKycProcess(userData: UserData): Promise { - return await this.goToStatus(userData, KycStatus.CHATBOT); + const lockUser = userData.users.find((e) => e.wallet.name === 'LOCK.space'); + return await this.goToStatus(userData, lockUser ? KycStatus.ONLINE_ID : KycStatus.CHATBOT); } async checkKycProcess(userData: UserData): Promise { @@ -56,7 +57,7 @@ export class KycProcessService { userData.spiderData = await this.updateSpiderData(userData, initiateData); } - if (status === KycStatus.MANUAL) { + if (status === KycStatus.MANUAL && !userData.hasExternalUser) { if (userData.mail) { await this.notificationService.sendMail({ type: MailType.USER, @@ -95,24 +96,29 @@ export class KycProcessService { if (userData.kycStatus === KycStatus.ONLINE_ID) { userData = await this.goToStatus(userData, KycStatus.VIDEO_ID); - await this.notificationService - .sendMail({ - type: MailType.USER, - input: { - userData, - translationKey: 'mail.kyc.failed', - translationParams: { - url: `${Config.payment.url}/kyc?code=${userData.kycHash}`, + if (!userData.hasExternalUser) { + await this.notificationService + .sendMail({ + type: MailType.USER, + input: { + userData, + translationKey: 'mail.kyc.failed', + translationParams: { + url: `${Config.payment.url}/kyc?code=${userData.kycHash}`, + }, }, - }, - }) - .catch(() => null); + }) + .catch(() => null); + } return userData; } // notify support await this.notificationService.sendMail({ type: MailType.KYC_SUPPORT, input: { userData } }); + + //kyc Webhook external Services + await this.kycWebhookService.kycFailed(userData, 'Kyc step failed'); return this.updateKycState(userData, KycState.FAILED); } @@ -167,12 +173,14 @@ export class KycProcessService { async identCompleted(userData: UserData, result: IdentResultDto): Promise { userData = await this.storeIdentResult(userData, result); - await this.notificationService - .sendMail({ - type: MailType.USER, - input: { userData, translationKey: 'mail.kyc.ident', translationParams: {} }, - }) - .catch(() => null); + if (!userData.hasExternalUser) { + await this.notificationService + .sendMail({ + type: MailType.USER, + input: { userData, translationKey: 'mail.kyc.ident', translationParams: {} }, + }) + .catch(() => null); + } return await this.goToStatus(userData, KycStatus.CHECK); } diff --git a/src/user/models/kyc/kyc-webhook.service.ts b/src/user/models/kyc/kyc-webhook.service.ts index 36f24946fa..bf184e0879 100644 --- a/src/user/models/kyc/kyc-webhook.service.ts +++ b/src/user/models/kyc/kyc-webhook.service.ts @@ -1,13 +1,15 @@ import { Injectable } from '@nestjs/common'; import { HttpService } from 'src/shared/services/http.service'; -import { WalletRepository } from '../wallet/wallet.repository'; -import { KycCompleted, UserData } from '../user-data/user-data.entity'; -import { Config } from 'src/config/config'; +import { KycCompleted, KycStatus, UserData } from '../user-data/user-data.entity'; +import { UserRepository } from '../user/user.repository'; +import { SpiderDataRepository } from '../spider-data/spider-data.repository'; +import { WalletService } from '../wallet/wallet.service'; export enum KycWebhookStatus { NA = 'NA', LIGHT = 'Light', FULL = 'Full', + REJECTED = 'Rejected', } export enum KycWebhookResult { @@ -37,7 +39,12 @@ export class KycWebhookDto { @Injectable() export class KycWebhookService { - constructor(private readonly http: HttpService, private readonly walletRepo: WalletRepository) {} + constructor( + private readonly http: HttpService, + private readonly walletService: WalletService, + private readonly userRepo: UserRepository, + private readonly spiderRepo: SpiderDataRepository, + ) {} async kycChanged(userData: UserData): Promise { await this.triggerWebhook(userData, KycWebhookResult.STATUS_CHANGED); @@ -48,19 +55,13 @@ export class KycWebhookService { } private async triggerWebhook(userData: UserData, result: KycWebhookResult, reason?: string): Promise { - if (!userData.users) { - console.info(`Tried to trigger webhook for user ${userData.id}, but users were not loaded`); - return; - } + userData.users = await this.userRepo.find({ where: { userData: { id: userData.id } }, relations: ['wallet'] }); for (const user of userData.users) { try { - if (!user.wallet?.id) { - console.info(`Tried to trigger webhook for user ${userData.id}, but wallet were not loaded`); - continue; - } - const walletUser = await this.walletRepo.findOne({ where: { id: user.wallet.id } }); - if (!walletUser || !walletUser.isKycClient || !walletUser.apiUrl) continue; + if (!user.wallet.isKycClient || !user.wallet.apiUrl) continue; + + const spiderData = await this.spiderRepo.findOne({ where: { userData: { id: userData.id } } }); const data: KycWebhookDto = { id: user.address, @@ -74,19 +75,31 @@ export class KycWebhookService { city: userData.location, zip: userData.zip, phone: userData.phone, - //TODO change for KYC Update v2 - kycStatus: KycCompleted(userData.kycStatus) ? KycWebhookStatus.FULL : KycWebhookStatus.NA, + kycStatus: this.getKycWebhookStatus(userData.kycStatus, spiderData?.chatbotResult), kycHash: userData.kycHash, }, reason: reason, }; - await this.http.post(`${walletUser.apiUrl}/kyc/update`, data, { - headers: { 'x-api-key': Config.lock.apiKey }, + const apiKey = this.walletService.getApiKeyInternal(user.wallet.name); + if (!apiKey) throw new Error(`ApiKey for wallet ${user.wallet.name} not available`); + + await this.http.post(`${user.wallet.apiUrl}/kyc/update`, data, { + headers: { 'x-api-key': apiKey }, }); } catch (error) { console.error(`Exception during KYC webhook (${result}) for user ${userData.id}:`, error); } } } + + getKycWebhookStatus(kycStatus: KycStatus, chatbotResult: string): KycWebhookStatus { + if (KycCompleted(kycStatus)) { + return chatbotResult ? KycWebhookStatus.FULL : KycWebhookStatus.LIGHT; + } else if (kycStatus === KycStatus.REJECTED) { + return KycWebhookStatus.REJECTED; + } else { + return KycWebhookStatus.NA; + } + } } diff --git a/src/user/models/kyc/kyc.service.spec.ts b/src/user/models/kyc/kyc.service.spec.ts index a981bf3209..4bcd57b32a 100644 --- a/src/user/models/kyc/kyc.service.spec.ts +++ b/src/user/models/kyc/kyc.service.spec.ts @@ -19,6 +19,7 @@ import { } from '../user-data/__mocks__/user-data.entity.mock'; import { UserRepository } from '../user/user.repository'; import { WalletRepository } from '../wallet/wallet.repository'; +import { WalletService } from '../wallet/wallet.service'; import { KycUserDataDto } from './dto/kyc-user-data.dto'; import { KycProcessService } from './kyc-process.service'; import { KycInfo, KycService } from './kyc.service'; @@ -36,6 +37,7 @@ describe('KycService', () => { let userRepo: UserRepository; let walletRepo: WalletRepository; let httpService: HttpService; + let walletService: WalletService; const defaultCountry = createDefaultCountry(); @@ -131,6 +133,7 @@ describe('KycService', () => { userRepo = createMock(); walletRepo = createMock(); httpService = createMock(); + walletService = createMock(); const module: TestingModule = await Test.createTestingModule({ providers: [ @@ -145,6 +148,7 @@ describe('KycService', () => { { provide: UserRepository, useValue: userRepo }, { provide: WalletRepository, useValue: walletRepo }, { provide: HttpService, useValue: httpService }, + { provide: WalletService, useValue: walletService }, ], }).compile(); diff --git a/src/user/models/kyc/kyc.service.ts b/src/user/models/kyc/kyc.service.ts index 80fad001b2..1728705832 100644 --- a/src/user/models/kyc/kyc.service.ts +++ b/src/user/models/kyc/kyc.service.ts @@ -28,8 +28,8 @@ import { UpdateKycStatusDto } from '../user-data/dto/update-kyc-status.dto'; import { KycDataTransferDto } from './dto/kyc-data-transfer.dto'; import { WalletRepository } from '../wallet/wallet.repository'; import { HttpService } from 'src/shared/services/http.service'; -import { Config } from 'src/config/config'; import { UserRepository } from '../user/user.repository'; +import { WalletService } from '../wallet/wallet.service'; export interface KycInfo { kycStatus: KycStatus; @@ -51,6 +51,7 @@ export class KycService { private readonly userDataRepo: UserDataRepository, private readonly userRepo: UserRepository, private readonly walletRepo: WalletRepository, + private readonly walletService: WalletService, private readonly spiderService: SpiderService, private readonly spiderSyncService: SpiderSyncService, private readonly countryService: CountryService, @@ -73,7 +74,7 @@ export class KycService { } async updateKycStatus(userDataId: number, dto: UpdateKycStatusDto): Promise { - let userData = await this.userDataRepo.findOne({ where: { id: userDataId } }); + let userData = await this.userDataRepo.findOne({ where: { id: userDataId }, relations: ['users', 'users.wallet'] }); if (!userData) throw new NotFoundException('User data not found'); // update status @@ -137,9 +138,12 @@ export class KycService { if (!user) throw new NotFoundException('DFX user not found'); if (!KycCompleted(user.userData.kycStatus)) throw new ConflictException('KYC required'); + const apiKey = this.walletService.getApiKeyInternal(wallet.name); + if (!apiKey) throw new Error(`ApiKey for wallet ${wallet.name} not available`); + try { result = await this.http.get<{ kycId: string }>(`${wallet.apiUrl}/kyc/check`, { - headers: { 'x-api-key': Config.lock.apiKey }, + headers: { 'x-api-key': apiKey }, params: { address: user.address }, }); @@ -221,7 +225,7 @@ export class KycService { const users = await this.userDataService.getUsersByMail(user.mail); const completedUser = users.find((data) => KycCompleted(data.kycStatus)); - if (completedUser) { + if (completedUser && !user.hasExternalUser) { await this.linkService.createNewLinkAddress(user, completedUser); throw new ConflictException('User already has completed Kyc'); } @@ -287,7 +291,10 @@ export class KycService { } private async getUserByKycCode(code: string): Promise { - const userData = await this.userDataRepo.findOne({ where: { kycHash: code }, relations: ['users', 'spiderData'] }); + const userData = await this.userDataRepo.findOne({ + where: { kycHash: code }, + relations: ['users', 'users.wallet', 'spiderData'], + }); if (!userData) throw new NotFoundException('User not found'); return userData; } diff --git a/src/user/models/user-data/user-data.entity.ts b/src/user/models/user-data/user-data.entity.ts index b16eb62ee9..e919d2c896 100644 --- a/src/user/models/user-data/user-data.entity.ts +++ b/src/user/models/user-data/user-data.entity.ts @@ -40,10 +40,6 @@ export enum BlankType { @Entity() export class UserData extends IEntity { - // TODO: remove - @Column({ default: true }) - isMigrated: boolean; - @Column({ default: AccountType.PERSONAL, length: 256 }) accountType: AccountType; @@ -178,6 +174,10 @@ export class UserData extends IEntity { @OneToOne(() => SpiderData, (c) => c.userData, { nullable: true }) spiderData: SpiderData; + + get hasExternalUser(): boolean { + return !!this.users.find((e) => e.wallet.isKycClient === true); + } } export const KycInProgressStates = [KycStatus.CHATBOT, KycStatus.ONLINE_ID, KycStatus.VIDEO_ID]; diff --git a/src/user/models/user-data/user-data.service.ts b/src/user/models/user-data/user-data.service.ts index ab96f57b06..b1f0222a87 100644 --- a/src/user/models/user-data/user-data.service.ts +++ b/src/user/models/user-data/user-data.service.ts @@ -36,10 +36,11 @@ export class UserDataService { async getUserDataByUser(userId: number): Promise { return this.userDataRepo .createQueryBuilder('userData') - .innerJoin('userData.users', 'user') + .leftJoinAndSelect('userData.users', 'user') .leftJoinAndSelect('userData.country', 'country') .leftJoinAndSelect('userData.organizationCountry', 'organizationCountry') .leftJoinAndSelect('userData.language', 'language') + .leftJoinAndSelect('user.wallet', 'wallet') .where('user.id = :id', { id: userId }) .getOne(); } @@ -69,7 +70,7 @@ export class UserDataService { } async updateUserData(userDataId: number, dto: UpdateUserDataDto): Promise { - let userData = await this.userDataRepo.findOne(userDataId); + let userData = await this.userDataRepo.findOne({ where: { id: userDataId }, relations: ['users', 'users.wallet'] }); if (!userData) throw new NotFoundException('User data not found'); userData = await this.updateSpiderIfNeeded(userData, dto); diff --git a/src/user/models/user/user.entity.ts b/src/user/models/user/user.entity.ts index 257ace17fc..cf42fb3a0b 100644 --- a/src/user/models/user/user.entity.ts +++ b/src/user/models/user/user.entity.ts @@ -1,6 +1,4 @@ import { Buy } from 'src/payment/models/buy/buy.entity'; -import { Country } from 'src/shared/models/country/country.entity'; -import { Language } from 'src/shared/models/language/language.entity'; import { Sell } from 'src/payment/models/sell/sell.entity'; import { UserData } from 'src/user/models/user-data/user-data.entity'; import { Wallet } from 'src/user/models/wallet/wallet.entity'; @@ -8,8 +6,6 @@ import { Entity, Column, OneToMany, ManyToOne, Index } from 'typeorm'; import { UserRole } from 'src/shared/auth/user-role.enum'; import { Staking } from '../../../payment/models/staking/staking.entity'; import { IEntity } from 'src/shared/models/entity'; -import { AccountType } from '../user-data/account-type.enum'; -import { Fiat } from 'src/shared/models/fiat/fiat.entity'; import { RefReward } from 'src/payment/models/ref-reward/ref-reward.entity'; import { StakingRefReward } from 'src/payment/models/staking-ref-reward/staking-ref-reward.entity'; import { BankAccount } from 'src/payment/models/bank-account/bank-account.entity'; @@ -137,59 +133,4 @@ export class User extends IEntity { @OneToMany(() => StakingRefReward, (reward) => reward.user) stakingRefRewards: StakingRefReward[]; - - // --- TO REMOVE --- // - @Column({ default: AccountType.PERSONAL, length: 256 }) - accountType: AccountType; - - @Column({ length: 256, nullable: true }) - mail: string; - - @Column({ length: 256, nullable: true }) - phone: string; - - @ManyToOne(() => Language) - language: Language; - - @ManyToOne(() => Fiat) - currency: Fiat; - - @Column({ length: 256, nullable: true }) - firstname: string; - - @Column({ length: 256, nullable: true }) - surname: string; - - @Column({ length: 256, nullable: true }) - street: string; - - @Column({ length: 256, nullable: true }) - houseNumber: string; - - @Column({ length: 256, nullable: true }) - location: string; - - @Column({ length: 256, nullable: true }) - zip: string; - - @ManyToOne(() => Country) - country: Country; - - @Column({ length: 256, nullable: true }) - organizationName: string; - - @Column({ length: 256, nullable: true }) - organizationStreet: string; - - @Column({ length: 256, nullable: true }) - organizationHouseNumber: string; - - @Column({ length: 256, nullable: true }) - organizationLocation: string; - - @Column({ length: 256, nullable: true }) - organizationZip: string; - - @ManyToOne(() => Country) - organizationCountry: Country; } diff --git a/src/user/models/user/user.service.spec.ts b/src/user/models/user/user.service.spec.ts index a0958e9138..68157a4d48 100644 --- a/src/user/models/user/user.service.spec.ts +++ b/src/user/models/user/user.service.spec.ts @@ -40,7 +40,7 @@ describe('UserService', () => { ) { jest .spyOn(userRepo, 'findOne') - .mockResolvedValue({ accountType, refFeePercent, buyFee, usedRef, cryptoFee } as User); + .mockResolvedValue({ refFeePercent, buyFee, usedRef, cryptoFee, userData: { accountType: accountType } } as User); } beforeEach(async () => { diff --git a/src/user/models/user/user.service.ts b/src/user/models/user/user.service.ts index 236444ec78..ba3221e6ec 100644 --- a/src/user/models/user/user.service.ts +++ b/src/user/models/user/user.service.ts @@ -221,15 +221,16 @@ export class UserService { // --- FEES --- // async getUserBuyFee(userId: number, annualVolume: number): Promise<{ fee: number; refBonus: number }> { - const { usedRef, accountType, buyFee } = await this.userRepo.findOne({ - select: ['id', 'usedRef', 'accountType', 'buyFee'], + const { usedRef, buyFee, userData } = await this.userRepo.findOne({ + select: ['id', 'usedRef', 'buyFee', 'userData'], where: { id: userId }, + relations: ['userData'], }); if (buyFee != null) return { fee: buyFee * 100, refBonus: 0 }; const baseFee = - accountType === AccountType.PERSONAL + userData.accountType === AccountType.PERSONAL ? // personal annualVolume < 5000 ? Config.buy.fee.private.base diff --git a/src/user/models/wallet/dto/kyc-data.dto.ts b/src/user/models/wallet/dto/kyc-data.dto.ts new file mode 100644 index 0000000000..8cf4b8f1ee --- /dev/null +++ b/src/user/models/wallet/dto/kyc-data.dto.ts @@ -0,0 +1,7 @@ +import { KycWebhookStatus } from '../../kyc/kyc-webhook.service'; + +export class KycDataDto { + address: string; + kycStatus: KycWebhookStatus; + kycHash: string; +} diff --git a/src/user/models/wallet/dto/wallet.dto.ts b/src/user/models/wallet/dto/wallet.dto.ts new file mode 100644 index 0000000000..31e84d424e --- /dev/null +++ b/src/user/models/wallet/dto/wallet.dto.ts @@ -0,0 +1,3 @@ +export class WalletDto { + name: string; +} diff --git a/src/user/models/wallet/wallet.controller.ts b/src/user/models/wallet/wallet.controller.ts new file mode 100644 index 0000000000..15d25df04a --- /dev/null +++ b/src/user/models/wallet/wallet.controller.ts @@ -0,0 +1,64 @@ +import { Controller, Get, UseGuards } from '@nestjs/common'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { RoleGuard } from 'src/shared/auth/role.guard'; +import { AuthGuard } from '@nestjs/passport'; +import { UserRole } from 'src/shared/auth/user-role.enum'; +import { WalletService } from './wallet.service'; +import { WalletDto } from './dto/wallet.dto'; +import { Wallet } from './wallet.entity'; +import { GetJwt } from 'src/shared/auth/get-jwt.decorator'; +import { JwtPayload } from 'src/shared/auth/jwt-payload.interface'; +import { User } from '../user/user.entity'; +import { KycDataDto } from './dto/kyc-data.dto'; +import { KycWebhookService } from '../kyc/kyc-webhook.service'; +import { SpiderDataRepository } from '../spider-data/spider-data.repository'; + +@ApiTags('wallet') +@Controller('wallet') +export class WalletController { + constructor( + private readonly walletService: WalletService, + private readonly spiderRepo: SpiderDataRepository, + private readonly kycWebhookService: KycWebhookService, + ) {} + + @Get() + @ApiBearerAuth() + @UseGuards(AuthGuard(), new RoleGuard(UserRole.USER)) + async getAllExternalService(): Promise { + return this.walletService.getAllExternalServices().then((l) => this.toDtoList(l)); + } + + @Get('kycData') + @ApiBearerAuth() + @UseGuards(AuthGuard(), new RoleGuard(UserRole.KYC_CLIENT_COMPANY)) + async getAllKycData(@GetJwt() jwt: JwtPayload): Promise { + return this.walletService.getAllKycData(jwt.id).then((l) => this.toKycDataDtoList(l)); + } + + // --- DTO --- // + private async toDtoList(wallets: Wallet[]): Promise { + return Promise.all(wallets.map((b) => this.toDto(b))); + } + + private async toKycDataDtoList(users: User[]): Promise { + return Promise.all(users.map((b) => this.toKycDataDto(b))); + } + + private async toKycDataDto(user: User): Promise { + return { + address: user.address, + kycStatus: this.kycWebhookService.getKycWebhookStatus( + user.userData.kycStatus, + user.userData.spiderData?.chatbotResult, + ), + kycHash: user.userData.kycHash, + }; + } + + private async toDto(wallet: Wallet): Promise { + return { + name: wallet.name, + }; + } +} diff --git a/src/user/models/wallet/wallet.entity.ts b/src/user/models/wallet/wallet.entity.ts index fdbb4d7e6f..2036f9a579 100644 --- a/src/user/models/wallet/wallet.entity.ts +++ b/src/user/models/wallet/wallet.entity.ts @@ -23,5 +23,5 @@ export class Wallet extends IEntity { apiUrl: string; @OneToMany(() => User, (user) => user.wallet) - logs: User[]; + users: User[]; } diff --git a/src/user/models/wallet/wallet.service.ts b/src/user/models/wallet/wallet.service.ts index f488f41d31..48592d1884 100644 --- a/src/user/models/wallet/wallet.service.ts +++ b/src/user/models/wallet/wallet.service.ts @@ -1,18 +1,34 @@ import { Injectable } from '@nestjs/common'; +import { Config } from 'src/config/config'; import { WalletRepository } from 'src/user/models/wallet/wallet.repository'; +import { User } from '../user/user.entity'; import { Wallet } from './wallet.entity'; @Injectable() export class WalletService { - constructor(private walletRepo: WalletRepository) {} + constructor(private readonly walletRepo: WalletRepository) {} async getWalletOrDefault(id: number): Promise { return (await this.walletRepo.findOne(id)) ?? (await this.walletRepo.findOne(1)); } - // TODO: remove? - // private verifySignature(address: string, signature: string): boolean { - // const signatureMessage = Config.auth.signMessageWallet + address; - // return this.cryptoService.verifySignature(signatureMessage, address, signature); - // } + async getAllExternalServices(): Promise { + return await this.walletRepo.find({ where: { isKycClient: true } }); + } + + async getAllKycData(walletId: number): Promise { + const wallet = await this.walletRepo.findOne({ + where: { id: walletId }, + relations: ['users', 'users.userData', 'users.userData.spiderData'], + }); + return wallet.users; + } + + public getApiKeyInternal(name: string): string { + return ( + Object.entries(Config.externalKycServices) + .filter(([key, _]) => key === name) + .map(([_, value]) => value)[0]?.apiKey ?? undefined + ); + } } diff --git a/src/user/services/spider/spider-sync.service.ts b/src/user/services/spider/spider-sync.service.ts index 926b8ab8cd..882ea1990e 100644 --- a/src/user/services/spider/spider-sync.service.ts +++ b/src/user/services/spider/spider-sync.service.ts @@ -118,7 +118,10 @@ export class SpiderSyncService { } async syncKycUser(userDataId: number, forceSync = false): Promise { - let userData = await this.userDataRepo.findOne({ where: { id: userDataId }, relations: ['spiderData'] }); + let userData = await this.userDataRepo.findOne({ + where: { id: userDataId }, + relations: ['spiderData', 'users', 'users.wallet'], + }); if (!userData) return; // update KYC data @@ -290,31 +293,36 @@ export class SpiderSyncService { userData: UserData, documentType: KycContentType, ): Promise<{ document: KycDocument; version: string; part: DocumentVersionPart }> { - const { document, version } = await this.getCompletedIdentDocument(userData); - if (!version) return null; - - const part = await this.spiderApi - .getDocumentVersionParts(userData.id, false, document, version) - .then((parts) => parts.find((p) => p.contentType === documentType)); - - return { document, version, part }; - } - - private async getCompletedIdentDocument(userData: UserData): Promise<{ document: KycDocument; version: string }> { let document = IdentInProgress(userData.kycStatus) ? KycDocuments[userData.kycStatus].document : KycDocument.ONLINE_IDENTIFICATION; - let version = await this.spiderApi.getDocumentVersion(userData.id, false, document, KycDocumentState.COMPLETED); + let result = await this.getCompletedIdentDocument(userData, document, documentType); - if (!version) { + if (!result) { // fallback to other ident method document = document === KycDocument.ONLINE_IDENTIFICATION ? KycDocument.VIDEO_IDENTIFICATION : KycDocument.ONLINE_IDENTIFICATION; - version = await this.spiderApi.getDocumentVersion(userData.id, false, document, KycDocumentState.COMPLETED); + result = await this.getCompletedIdentDocument(userData, document, documentType); } - return { document, version: version?.name }; + return result; + } + + private async getCompletedIdentDocument( + userData: UserData, + document: KycDocument, + documentType: KycContentType, + ): Promise<{ document: KycDocument; version: string; part: DocumentVersionPart }> { + const version = await this.spiderApi.getDocumentVersion(userData.id, false, document, KycDocumentState.COMPLETED); + if (!version) return null; + + const part = await this.spiderApi + .getDocumentVersionParts(userData.id, false, document, version?.name) + .then((parts) => parts.find((p) => p.contentType === documentType)); + if (!part) return null; + + return { document, version: version.name, part }; } } diff --git a/src/user/services/spider/spider.service.ts b/src/user/services/spider/spider.service.ts index 408bdbbea0..507332f84f 100644 --- a/src/user/services/spider/spider.service.ts +++ b/src/user/services/spider/spider.service.ts @@ -126,6 +126,7 @@ export class SpiderService { organisationType: user.accountType === AccountType.SOLE_PROPRIETORSHIP ? 'SOLE_PROPRIETORSHIP' : 'LEGAL_ENTITY', purposeBusinessRelationship: 'Kauf und Verkauf von DeFiChain Assets', bearerShares: 'NO', + contribution: '0', }; await this.uploadDocument( diff --git a/src/user/user.module.ts b/src/user/user.module.ts index 458d1e0ceb..9237b7c1a6 100644 --- a/src/user/user.module.ts +++ b/src/user/user.module.ts @@ -36,6 +36,7 @@ import { LinkAddressRepository } from './models/link/link-address.repository'; import { NotificationModule } from 'src/notification/notification.module'; import { LimitRequestController } from './models/limit-request/limit-request.controller'; import { KycWebhookService } from './models/kyc/kyc-webhook.service'; +import { WalletController } from './models/wallet/wallet.controller'; @Module({ imports: [ @@ -63,6 +64,7 @@ import { KycWebhookService } from './models/kyc/kyc-webhook.service'; KycController, LinkController, LimitRequestController, + WalletController, ], providers: [ UserService, diff --git a/thunder-tests/thunderCollection.json b/thunder-tests/thunderCollection.json index 80d7aac49f..e48b3e3994 100644 --- a/thunder-tests/thunderCollection.json +++ b/thunder-tests/thunderCollection.json @@ -194,6 +194,13 @@ "created": "2022-09-23T14:03:55.908Z", "sortNum": 300000 }, + { + "_id": "9baf5a17-7886-4959-ab65-eea8e3dae524", + "name": "Wallet", + "containerId": "", + "created": "2022-10-14T16:15:32.185Z", + "sortNum": 275000 + }, { "_id": "af415d12-0498-48a6-a81d-4d3ef4497cdf", "name": "Bank TX Return", diff --git a/thunder-tests/thunderclient.json b/thunder-tests/thunderclient.json index d3e345e18a..b7e313debd 100644 --- a/thunder-tests/thunderclient.json +++ b/thunder-tests/thunderclient.json @@ -1867,6 +1867,64 @@ "tests": [] }, { + "_id": "a0a206f0-13eb-493a-9b07-3a140b0eeedf", + "colId": "62eefab7-84cd-496b-8f93-253c15d4f841", + "containerId": "0bbfaa5e-1c48-4a6b-9dd3-bb28b78af825", + "name": "Company Challenge", + "url": "{{url}}/v1/auth/company/challenge?address=8an2Mi4dCV8GfiqbF2i2pJwDetq87AhFa4", + "method": "POST", + "sortNum": 85000, + "created": "2022-10-14T13:12:37.193Z", + "modified": "2022-10-20T10:15:12.521Z", + "headers": [], + "params": [ + { + "name": "address", + "value": "8an2Mi4dCV8GfiqbF2i2pJwDetq87AhFa4", + "isPath": false + } + ], + "auth": { + "type": "none" + }, + "tests": [] + }, + { + "_id": "8384887d-f0e7-43a4-9a28-1859576d0c1c", + "colId": "62eefab7-84cd-496b-8f93-253c15d4f841", + "containerId": "0bbfaa5e-1c48-4a6b-9dd3-bb28b78af825", + "name": "Company SignIn", + "url": "{{url}}/v1/auth/company/signin", + "method": "POST", + "sortNum": 82500, + "created": "2022-10-14T13:12:39.907Z", + "modified": "2022-10-20T10:28:14.156Z", + "headers": [], + "params": [], + "body": { + "type": "json", + "raw": "{\n \"address\": \"8an2Mi4dCV8GfiqbF2i2pJwDetq87AhFa4\",\n \"signature\": \"HzRZfDlmtAp4ywy7MuGpwCEEGoemH6rB82VNJUaKM7P1PCmiBRo2MyuuYfIBKE/nakafSymO8ep+84AGIzeEm/I=\"\n}", + "form": [] + }, + "auth": { + "type": "none" + }, + "tests": [] + }, + { + "_id": "14441e24-4e57-4e7c-88cc-dff408c5837e", + "colId": "62eefab7-84cd-496b-8f93-253c15d4f841", + "containerId": "9baf5a17-7886-4959-ab65-eea8e3dae524", + "name": "KycData", + "url": "{{url}}/v1/wallet/kycData", + "method": "GET", + "sortNum": 640000, + "created": "2022-10-14T16:15:32.185Z", + "modified": "2022-10-14T16:16:42.670Z", + "headers": [], + "params": [], + "tests": [] + },{ "_id": "fb9bc645-856b-46a8-98c3-445abc95a2c4", "colId": "62eefab7-84cd-496b-8f93-253c15d4f841", "containerId": "d74943e9-e28b-4c36-9df3-b6093cf39800",