diff --git a/migration/1768315830503-UpdateXtDeuroLiquidityMinimum.js b/migration/1768315830503-UpdateXtDeuroLiquidityMinimum.js
new file mode 100644
index 0000000000..deb2000733
--- /dev/null
+++ b/migration/1768315830503-UpdateXtDeuroLiquidityMinimum.js
@@ -0,0 +1,36 @@
+/**
+ * @typedef {import('typeorm').MigrationInterface} MigrationInterface
+ * @typedef {import('typeorm').QueryRunner} QueryRunner
+ */
+
+/**
+ * @class
+ * @implements {MigrationInterface}
+ */
+module.exports = class UpdateXtDeuroLiquidityMinimum1768315830503 {
+ name = 'UpdateXtDeuroLiquidityMinimum1768315830503'
+
+ /**
+ * @param {QueryRunner} queryRunner
+ */
+ async up(queryRunner) {
+ // Update XT/DEURO liquidity rule minimum from 4300 to 10000
+ await queryRunner.query(`
+ UPDATE "dbo"."liquidity_management_rule"
+ SET "minimal" = 10000
+ WHERE "id" = 295
+ `);
+ }
+
+ /**
+ * @param {QueryRunner} queryRunner
+ */
+ async down(queryRunner) {
+ // Revert XT/DEURO liquidity rule minimum back to 4300
+ await queryRunner.query(`
+ UPDATE "dbo"."liquidity_management_rule"
+ SET "minimal" = 4300
+ WHERE "id" = 295
+ `);
+ }
+}
diff --git a/src/integration/blockchain/shared/evm/delegation/eip7702-delegation.service.ts b/src/integration/blockchain/shared/evm/delegation/eip7702-delegation.service.ts
index 928f40c28b..77e57cf827 100644
--- a/src/integration/blockchain/shared/evm/delegation/eip7702-delegation.service.ts
+++ b/src/integration/blockchain/shared/evm/delegation/eip7702-delegation.service.ts
@@ -1,5 +1,5 @@
import { Injectable } from '@nestjs/common';
-import { GetConfig } from 'src/config/config';
+import { Config, Environment, GetConfig } from 'src/config/config';
import { Blockchain } from 'src/integration/blockchain/shared/enums/blockchain.enum';
import { Asset } from 'src/shared/models/asset/asset.entity';
import { DfxLogger } from 'src/shared/services/dfx-logger';
@@ -84,7 +84,10 @@ export class Eip7702DelegationService {
* RealUnit app supports eth_sign (unlike MetaMask), so EIP-7702 works
*/
isDelegationSupportedForRealUnit(blockchain: Blockchain): boolean {
- return blockchain === Blockchain.ETHEREUM && CHAIN_CONFIG[blockchain] !== undefined;
+ const expectedBlockchain = [Environment.DEV, Environment.LOC].includes(Config.environment)
+ ? Blockchain.SEPOLIA
+ : Blockchain.ETHEREUM;
+ return blockchain === expectedBlockchain && CHAIN_CONFIG[blockchain] !== undefined;
}
/**
diff --git a/src/integration/sift/dto/sift.dto.ts b/src/integration/sift/dto/sift.dto.ts
index bc26b98b54..843a139f0b 100644
--- a/src/integration/sift/dto/sift.dto.ts
+++ b/src/integration/sift/dto/sift.dto.ts
@@ -1037,6 +1037,7 @@ export const SiftAmlDeclineMap: { [method in AmlReason]: DeclineCategory } = {
[AmlReason.BANK_RELEASE_PENDING]: DeclineCategory.OTHER,
[AmlReason.VIRTUAL_IBAN_USER_MISMATCH]: DeclineCategory.RISKY,
[AmlReason.INTERMEDIARY_WITHOUT_SENDER]: DeclineCategory.RISKY,
+ [AmlReason.NAME_TOO_SHORT]: DeclineCategory.OTHER,
};
export interface ScoreRsponse {
diff --git a/src/shared/i18n/de/mail.json b/src/shared/i18n/de/mail.json
index 180c054df2..aaa27b0fe0 100644
--- a/src/shared/i18n/de/mail.json
+++ b/src/shared/i18n/de/mail.json
@@ -95,7 +95,8 @@
"manual_check_ip_phone": "Wir konnten dich unter deiner angegebenen Telefonnummer nicht erreichen",
"manual_check_ip_country_phone": "Wir konnten dich unter deiner angegebenen Telefonnummer nicht erreichen",
"merge_incomplete": "Die Email Bestätigung wurde nicht akzeptiert",
- "intermediary_without_sender": "Die Absenderbank (Wise/Revolut) hat nur den Banknamen übermittelt, nicht aber den Namen des Kontoinhabers. DFX kann daher den tatsächlichen Absender nicht verifizieren und die Transaktion nicht verarbeiten."
+ "intermediary_without_sender": "Die Absenderbank (Wise/Revolut) hat nur den Banknamen übermittelt, nicht aber den Namen des Kontoinhabers. DFX kann daher den tatsächlichen Absender nicht verifizieren und die Transaktion nicht verarbeiten.",
+ "name_too_short": "Dein Name ist zu kurz für die Bankverarbeitung. Banken benötigen mindestens 4 Buchstaben im Namen des Kontoinhabers."
},
"kyc_start": "Du kannst den KYC Prozess hier starten:
[url:{urlText}]"
},
diff --git a/src/shared/i18n/en/mail.json b/src/shared/i18n/en/mail.json
index c969d23a13..db40214c22 100644
--- a/src/shared/i18n/en/mail.json
+++ b/src/shared/i18n/en/mail.json
@@ -95,7 +95,8 @@
"manual_check_ip_phone": "We were unable to reach you at the phone number you provided",
"manual_check_ip_country_phone": "We were unable to reach you at the phone number you provided",
"merge_incomplete": "The email confirmation was not accepted",
- "intermediary_without_sender": "The sender bank (Wise/Revolut) only transmitted the bank name, not the account holder's name. DFX is therefore unable to verify the actual sender and cannot process the transaction."
+ "intermediary_without_sender": "The sender bank (Wise/Revolut) only transmitted the bank name, not the account holder's name. DFX is therefore unable to verify the actual sender and cannot process the transaction.",
+ "name_too_short": "Your name is too short for bank processing. Banks require at least 4 letters in the account holder name."
},
"kyc_start": "You can start the KYC process here:
[url:{urlText}]"
},
diff --git a/src/shared/i18n/es/mail.json b/src/shared/i18n/es/mail.json
index d9e74c2f34..d1a3aa8c6e 100644
--- a/src/shared/i18n/es/mail.json
+++ b/src/shared/i18n/es/mail.json
@@ -95,7 +95,8 @@
"manual_check_ip_phone": "No hemos podido contactar con usted al número de teléfono que nos facilitó",
"manual_check_ip_country_phone": "No hemos podido contactar con usted al número de teléfono que nos facilitó",
"merge_incomplete": "El correo electrónico de confirmación no fue aceptado",
- "intermediary_without_sender": "El banco emisor (Wise/Revolut) solo transmitió el nombre del banco, no el nombre del titular de la cuenta. Por lo tanto, DFX no puede verificar el remitente real y no puede procesar la transacción."
+ "intermediary_without_sender": "El banco emisor (Wise/Revolut) solo transmitió el nombre del banco, no el nombre del titular de la cuenta. Por lo tanto, DFX no puede verificar el remitente real y no puede procesar la transacción.",
+ "name_too_short": "Tu nombre es demasiado corto para el procesamiento bancario. Los bancos requieren al menos 4 letras en el nombre del titular de la cuenta."
},
"kyc_start": "Puede iniciar el proceso KYC aquí:
[url:{urlText}]"
},
diff --git a/src/shared/i18n/fr/mail.json b/src/shared/i18n/fr/mail.json
index 35470ecc1d..a4c7ef800d 100644
--- a/src/shared/i18n/fr/mail.json
+++ b/src/shared/i18n/fr/mail.json
@@ -95,7 +95,8 @@
"manual_check_ip_phone": "Nous n'avons pas réussi à vous joindre au numéro de téléphone que vous avez fourni",
"manual_check_ip_country_phone": "Nous n'avons pas réussi à vous joindre au numéro de téléphone que vous avez fourni",
"merge_incomplete": "L'e-mail de confirmation n'a pas été accepté",
- "intermediary_without_sender": "La banque émettrice (Wise/Revolut) n'a transmis que le nom de la banque, et non le nom du titulaire du compte. DFX ne peut donc pas vérifier l'expéditeur réel et ne peut pas traiter la transaction."
+ "intermediary_without_sender": "La banque émettrice (Wise/Revolut) n'a transmis que le nom de la banque, et non le nom du titulaire du compte. DFX ne peut donc pas vérifier l'expéditeur réel et ne peut pas traiter la transaction.",
+ "name_too_short": "Votre nom est trop court pour le traitement bancaire. Les banques exigent au moins 4 lettres dans le nom du titulaire du compte."
},
"kyc_start": "Vous pouvez commencer le processus KYC ici:
[url:{urlText}]"
},
diff --git a/src/shared/i18n/it/mail.json b/src/shared/i18n/it/mail.json
index 6cc9c6a18b..1f341d742b 100644
--- a/src/shared/i18n/it/mail.json
+++ b/src/shared/i18n/it/mail.json
@@ -95,7 +95,8 @@
"manual_check_ip_phone": "Non siamo riusciti a contattarti al numero di telefono che ci hai fornito",
"manual_check_ip_country_phone": "Non siamo riusciti a contattarti al numero di telefono che ci hai fornito",
"merge_incomplete": "L'e-mail di conferma non è stata accettata",
- "intermediary_without_sender": "La banca mittente (Wise/Revolut) ha trasmesso solo il nome della banca, non il nome del titolare del conto. DFX non può quindi verificare il mittente effettivo e non può elaborare la transazione."
+ "intermediary_without_sender": "La banca mittente (Wise/Revolut) ha trasmesso solo il nome della banca, non il nome del titolare del conto. DFX non può quindi verificare il mittente effettivo e non può elaborare la transazione.",
+ "name_too_short": "Il tuo nome è troppo corto per l'elaborazione bancaria. Le banche richiedono almeno 4 lettere nel nome del titolare del conto."
},
"kyc_start": "Potete iniziare il processo KYC qui:
[url:{urlText}]"
},
diff --git a/src/shared/i18n/pt/mail.json b/src/shared/i18n/pt/mail.json
index 08346bf21b..192b00f9a9 100644
--- a/src/shared/i18n/pt/mail.json
+++ b/src/shared/i18n/pt/mail.json
@@ -95,7 +95,8 @@
"manual_check_ip_phone": "We were unable to reach you at the phone number you provided",
"manual_check_ip_country_phone": "We were unable to reach you at the phone number you provided",
"merge_incomplete": "The email confirmation was not accepted",
- "intermediary_without_sender": "O banco remetente (Wise/Revolut) transmitiu apenas o nome do banco, não o nome do titular da conta. Portanto, a DFX não pode verificar o remetente real e não pode processar a transação."
+ "intermediary_without_sender": "O banco remetente (Wise/Revolut) transmitiu apenas o nome do banco, não o nome do titular da conta. Portanto, a DFX não pode verificar o remetente real e não pode processar a transação.",
+ "name_too_short": "O seu nome é muito curto para o processamento bancário. Os bancos exigem pelo menos 4 letras no nome do titular da conta."
},
"kyc_start": "You can start the KYC process here:
[url:{urlText}]"
},
diff --git a/src/subdomains/core/aml/enums/aml-error.enum.ts b/src/subdomains/core/aml/enums/aml-error.enum.ts
index 533cdf83a1..8c75ff38a5 100644
--- a/src/subdomains/core/aml/enums/aml-error.enum.ts
+++ b/src/subdomains/core/aml/enums/aml-error.enum.ts
@@ -26,6 +26,7 @@ export enum AmlError {
INVALID_KYC_TYPE = 'InvalidKycType',
NO_VERIFIED_NAME = 'NoVerifiedName',
NAME_MISSING = 'NameMissing',
+ NAME_TOO_SHORT = 'NameTooShort',
VERIFIED_COUNTRY_NOT_ALLOWED = 'VerifiedCountryNotAllowed',
IBAN_COUNTRY_FATF_NOT_ALLOWED = 'IbanCountryFatfNotAllowed',
TX_COUNTRY_NOT_ALLOWED = 'TxCountryNotAllowed',
@@ -151,6 +152,11 @@ export const AmlErrorResult: {
amlCheck: CheckStatus.PENDING,
amlReason: AmlReason.KYC_DATA_NEEDED,
},
+ [AmlError.NAME_TOO_SHORT]: {
+ type: AmlErrorType.CRUCIAL,
+ amlCheck: CheckStatus.FAIL,
+ amlReason: AmlReason.NAME_TOO_SHORT,
+ },
[AmlError.VERIFIED_COUNTRY_NOT_ALLOWED]: {
type: AmlErrorType.CRUCIAL,
amlCheck: CheckStatus.GSHEET,
diff --git a/src/subdomains/core/aml/enums/aml-reason.enum.ts b/src/subdomains/core/aml/enums/aml-reason.enum.ts
index b7773a74de..5f55419276 100644
--- a/src/subdomains/core/aml/enums/aml-reason.enum.ts
+++ b/src/subdomains/core/aml/enums/aml-reason.enum.ts
@@ -39,6 +39,7 @@ export enum AmlReason {
BANK_RELEASE_PENDING = 'BankReleasePending',
VIRTUAL_IBAN_USER_MISMATCH = 'VirtualIbanUserMismatch',
INTERMEDIARY_WITHOUT_SENDER = 'IntermediaryWithoutSender',
+ NAME_TOO_SHORT = 'NameTooShort',
}
export const KycAmlReasons = [
diff --git a/src/subdomains/core/aml/services/aml-helper.service.ts b/src/subdomains/core/aml/services/aml-helper.service.ts
index e8cd420bc0..2a021e9ee2 100644
--- a/src/subdomains/core/aml/services/aml-helper.service.ts
+++ b/src/subdomains/core/aml/services/aml-helper.service.ts
@@ -69,6 +69,13 @@ export class AmlHelperService {
if (!entity.userData.verifiedName) errors.push(AmlError.NO_VERIFIED_NAME);
if (!entity.userData.verifiedName && !bankData?.name && !entity.userData.completeName)
errors.push(AmlError.NAME_MISSING);
+
+ // Check name length (min 4 letters for bank processing)
+ const completeName = entity.userData.verifiedName ?? bankData?.name ?? entity.userData.completeName;
+ if (completeName && this.countLetters(completeName) < 4) {
+ errors.push(AmlError.NAME_TOO_SHORT);
+ }
+
if (entity.userData.verifiedCountry && !entity.userData.verifiedCountry.fatfEnable)
errors.push(AmlError.VERIFIED_COUNTRY_NOT_ALLOWED);
if (ibanCountry && !ibanCountry.fatfEnable) errors.push(AmlError.IBAN_COUNTRY_FATF_NOT_ALLOWED);
@@ -606,4 +613,8 @@ export class AmlHelperService {
// No Result - only comment
return { bankData, comment };
}
+
+ private static countLetters(str: string): number {
+ return str.replace(/[^\p{L}]/gu, '').length;
+ }
}
diff --git a/src/subdomains/core/liquidity-management/adapters/actions/dfx-dex.adapter.ts b/src/subdomains/core/liquidity-management/adapters/actions/dfx-dex.adapter.ts
index 552fe04110..83c796df25 100644
--- a/src/subdomains/core/liquidity-management/adapters/actions/dfx-dex.adapter.ts
+++ b/src/subdomains/core/liquidity-management/adapters/actions/dfx-dex.adapter.ts
@@ -1,5 +1,7 @@
import { Injectable } from '@nestjs/common';
+import { Blockchain } from 'src/integration/blockchain/shared/enums/blockchain.enum';
import { ExchangeRegistryService } from 'src/integration/exchange/services/exchange-registry.service';
+import { Asset, AssetType } from 'src/shared/models/asset/asset.entity';
import { AssetService } from 'src/shared/models/asset/asset.service';
import { LiquidityOrderContext } from 'src/subdomains/supporting/dex/entities/liquidity-order.entity';
import { ReserveLiquidityRequest } from 'src/subdomains/supporting/dex/interfaces';
@@ -40,10 +42,8 @@ export class DfxDexAdapter extends LiquidityActionAdapter {
async checkCompletion(order: LiquidityManagementOrder): Promise {
switch (order.action.command) {
case DfxDexAdapterCommands.PURCHASE:
- return this.checkSellPurchaseCompletion(order);
-
case DfxDexAdapterCommands.SELL:
- return this.checkSellPurchaseCompletion(order);
+ return this.checkSwapCompletion(order);
case DfxDexAdapterCommands.WITHDRAW:
return this.checkWithdrawCompletion(order);
@@ -59,10 +59,8 @@ export class DfxDexAdapter extends LiquidityActionAdapter {
return this.validateWithdrawParams(params);
case DfxDexAdapterCommands.PURCHASE:
- return true;
-
case DfxDexAdapterCommands.SELL:
- return true;
+ return this.validateSwapParams(params);
default:
throw new Error(`Command ${command} not supported by DfxDexAdapter`);
@@ -73,23 +71,43 @@ export class DfxDexAdapter extends LiquidityActionAdapter {
/**
* @note
- * correlationId is the orderId and set by liquidity management
+ * correlationId is the orderId and set by liquidity management.
+ * targetAsset (from rule) is what we're buying, swapAsset is what we spend.
+ * Amount is in target asset (targetAsset).
*/
private async purchase(order: LiquidityManagementOrder): Promise {
const {
pipeline: {
- rule: { targetAsset: asset },
+ rule: { targetAsset },
},
- maxAmount: amount,
id: correlationId,
+ minAmount,
+ maxAmount,
} = order;
+ const { swapAsset: swapAssetName } = this.parseSwapParams(order.action.paramMap);
+ const swapAsset = await this.getSwapAsset(targetAsset.blockchain, swapAssetName);
+
+ const price = await this.dexService.calculatePrice(swapAsset, targetAsset);
+ const minSwapAmount = minAmount * price;
+ const maxSwapAmount = maxAmount * price;
+
+ const swapLiquidity = await this.resolveSwapLiquidity(
+ correlationId.toString(),
+ swapAsset,
+ minSwapAmount,
+ maxSwapAmount,
+ );
+
+ order.inputAmount = swapLiquidity.amount;
+ order.inputAsset = swapLiquidity.asset.name;
+
const request = {
context: LiquidityOrderContext.LIQUIDITY_MANAGEMENT,
correlationId: correlationId.toString(),
- referenceAsset: asset,
- referenceAmount: amount,
- targetAsset: asset,
+ referenceAsset: swapLiquidity.asset,
+ referenceAmount: swapLiquidity.amount,
+ targetAsset,
};
await this.dexService.purchaseLiquidity(request);
@@ -99,25 +117,37 @@ export class DfxDexAdapter extends LiquidityActionAdapter {
/**
* @note
- * correlationId is the orderId and set by liquidity management
+ * correlationId is the orderId and set by liquidity management.
+ * targetAsset (from rule) is what we're selling, swapAsset is what we receive.
+ * Amount is in source asset (targetAsset).
*/
private async sell(order: LiquidityManagementOrder): Promise {
const {
pipeline: {
- rule: { targetAsset: asset },
+ rule: { targetAsset },
},
- maxAmount: amount,
id: correlationId,
+ minAmount,
+ maxAmount,
} = order;
+ const { swapAsset: swapAssetName } = this.parseSwapParams(order.action.paramMap);
+ const swapAsset = await this.getSwapAsset(targetAsset.blockchain, swapAssetName);
+
+ const sellLiquidity = await this.resolveSwapLiquidity(correlationId.toString(), targetAsset, minAmount, maxAmount);
+
+ order.inputAmount = sellLiquidity.amount;
+ order.inputAsset = sellLiquidity.asset.name;
+
const request = {
context: LiquidityOrderContext.LIQUIDITY_MANAGEMENT,
correlationId: correlationId.toString(),
- sellAsset: asset,
- sellAmount: amount,
+ referenceAsset: sellLiquidity.asset,
+ referenceAmount: sellLiquidity.amount,
+ targetAsset: swapAsset,
};
- await this.dexService.sellLiquidity(request);
+ await this.dexService.purchaseLiquidity(request);
return correlationId.toString();
}
@@ -163,7 +193,7 @@ export class DfxDexAdapter extends LiquidityActionAdapter {
// --- COMPLETION CHECKS --- //
- private async checkSellPurchaseCompletion(order: LiquidityManagementOrder): Promise {
+ private async checkSwapCompletion(order: LiquidityManagementOrder): Promise {
try {
const result = await this.dexService.checkOrderReady(
LiquidityOrderContext.LIQUIDITY_MANAGEMENT,
@@ -172,6 +202,9 @@ export class DfxDexAdapter extends LiquidityActionAdapter {
if (result.isReady) {
await this.dexService.completeOrders(LiquidityOrderContext.LIQUIDITY_MANAGEMENT, order.correlationId);
+
+ order.outputAmount = result.targetAmount;
+ order.outputAsset = result.targetAsset;
}
return result.isReady;
@@ -235,4 +268,68 @@ export class DfxDexAdapter extends LiquidityActionAdapter {
this.exchangeRegistry.get(system)
);
}
+
+ private validateSwapParams(params: Record): boolean {
+ try {
+ this.parseSwapParams(params);
+ return true;
+ } catch {
+ return false;
+ }
+ }
+
+ private parseSwapParams(params: Record): { swapAsset: string } {
+ const swapAsset = params?.tradeAsset as string | undefined;
+
+ if (!(typeof swapAsset === 'string' && swapAsset.length > 0))
+ throw new Error('Params provided to DfxDexAdapter swap command are invalid.');
+
+ return { swapAsset };
+ }
+
+ // --- SWAP HELPERS --- //
+
+ private async resolveSwapLiquidity(
+ correlationId: string,
+ liquidityAsset: Asset,
+ minAmount: number,
+ maxAmount: number,
+ ): Promise<{ asset: Asset; amount: number }> {
+ // Check available liquidity
+ const checkRequest = {
+ context: LiquidityOrderContext.LIQUIDITY_MANAGEMENT,
+ correlationId,
+ referenceAsset: liquidityAsset,
+ referenceAmount: minAmount,
+ targetAsset: liquidityAsset,
+ };
+
+ const {
+ reference: { availableAmount },
+ } = await this.dexService.checkLiquidity(checkRequest);
+
+ if (availableAmount < minAmount) {
+ throw new OrderNotProcessableException(
+ `Not enough ${liquidityAsset.name} liquidity (balance: ${availableAmount}, min. requested: ${minAmount}, max. requested: ${maxAmount})`,
+ );
+ }
+
+ const amount = Math.min(maxAmount, availableAmount);
+
+ return { asset: liquidityAsset, amount };
+ }
+
+ private async getSwapAsset(blockchain: Blockchain, swapAssetName: string): Promise {
+ const swapAsset = await this.assetService.getAssetByQuery({
+ name: swapAssetName,
+ blockchain,
+ type: AssetType.TOKEN,
+ });
+
+ if (!swapAsset) {
+ throw new OrderNotProcessableException(`Swap asset ${swapAssetName} not found on ${blockchain}`);
+ }
+
+ return swapAsset;
+ }
}
diff --git a/src/subdomains/core/sell-crypto/process/__tests__/buy-fiat.service.spec.ts b/src/subdomains/core/sell-crypto/process/__tests__/buy-fiat.service.spec.ts
index a836ab9768..b7dcf8fc6a 100644
--- a/src/subdomains/core/sell-crypto/process/__tests__/buy-fiat.service.spec.ts
+++ b/src/subdomains/core/sell-crypto/process/__tests__/buy-fiat.service.spec.ts
@@ -15,6 +15,7 @@ import { createCustomFiatOutput } from 'src/subdomains/supporting/fiat-output/__
import { FiatOutputService } from 'src/subdomains/supporting/fiat-output/fiat-output.service';
import { createCustomCryptoInput } from 'src/subdomains/supporting/payin/entities/__mocks__/crypto-input.entity.mock';
import { PayInService } from 'src/subdomains/supporting/payin/services/payin.service';
+import { PayoutService } from 'src/subdomains/supporting/payout/services/payout.service';
import { TransactionHelper } from 'src/subdomains/supporting/payment/services/transaction-helper';
import { TransactionRequestService } from 'src/subdomains/supporting/payment/services/transaction-request.service';
import { TransactionService } from 'src/subdomains/supporting/payment/services/transaction.service';
@@ -57,6 +58,7 @@ describe('BuyFiatService', () => {
let transactionHelper: TransactionHelper;
let custodyOrderService: CustodyOrderService;
let supportLogService: SupportLogService;
+ let payoutService: PayoutService;
beforeEach(async () => {
buyFiatRepo = createMock();
@@ -78,6 +80,7 @@ describe('BuyFiatService', () => {
transactionHelper = createMock();
custodyOrderService = createMock();
supportLogService = createMock();
+ payoutService = createMock();
const module: TestingModule = await Test.createTestingModule({
imports: [TestSharedModule],
@@ -102,6 +105,7 @@ describe('BuyFiatService', () => {
{ provide: TransactionHelper, useValue: transactionHelper },
{ provide: CustodyOrderService, useValue: custodyOrderService },
{ provide: SupportLogService, useValue: supportLogService },
+ { provide: PayoutService, useValue: payoutService },
],
}).compile();
diff --git a/src/subdomains/core/sell-crypto/process/services/buy-fiat-registration.service.ts b/src/subdomains/core/sell-crypto/process/services/buy-fiat-registration.service.ts
index 89f8d71124..0b1ee1cece 100644
--- a/src/subdomains/core/sell-crypto/process/services/buy-fiat-registration.service.ts
+++ b/src/subdomains/core/sell-crypto/process/services/buy-fiat-registration.service.ts
@@ -3,6 +3,8 @@ import { DfxLogger } from 'src/shared/services/dfx-logger';
import { CryptoInput, PayInPurpose, PayInStatus } from 'src/subdomains/supporting/payin/entities/crypto-input.entity';
import { PayInService } from 'src/subdomains/supporting/payin/services/payin.service';
import { TransactionHelper } from 'src/subdomains/supporting/payment/services/transaction-helper';
+import { PayoutOrderContext } from 'src/subdomains/supporting/payout/entities/payout-order.entity';
+import { PayoutService } from 'src/subdomains/supporting/payout/services/payout.service';
import { IsNull, Not } from 'typeorm';
import { SellRepository } from '../../route/sell.repository';
import { BuyFiatRepository } from '../buy-fiat.repository';
@@ -24,29 +26,53 @@ export class BuyFiatRegistrationService {
private readonly sellRepository: SellRepository,
private readonly payInService: PayInService,
private readonly transactionHelper: TransactionHelper,
+ private readonly payoutService: PayoutService,
) {}
async syncReturnTxId(): Promise {
+ const baseWhere = { chargebackAllowedDate: Not(IsNull()), chargebackTxId: IsNull() };
+
const entities = await this.buyFiatRepo.find({
- where: {
- cryptoInput: { returnTxId: Not(IsNull()), status: PayInStatus.RETURN_CONFIRMED },
- chargebackTxId: IsNull(),
- },
+ where: [
+ // PayIn returned
+ { ...baseWhere, cryptoInput: { status: PayInStatus.RETURN_CONFIRMED, returnTxId: Not(IsNull()) } },
+ // Payout forwarded
+ { ...baseWhere, cryptoInput: { status: PayInStatus.FORWARD_CONFIRMED } },
+ ],
relations: { cryptoInput: true, sell: true, transaction: { user: { wallet: true }, userData: true } },
});
for (const entity of entities) {
try {
- await this.buyFiatRepo.update(entity.id, { chargebackTxId: entity.cryptoInput.returnTxId, isComplete: true });
+ const txId = await this.getReturnTxId(entity);
+ if (!txId) continue;
- // send webhook
+ await this.buyFiatRepo.update(entity.id, { chargebackTxId: txId, isComplete: true });
await this.buyFiatService.triggerWebhook(entity);
} catch (e) {
- this.logger.error(`Error during buyFiat payIn returnTxId sync (${entity.id}):`, e);
+ this.logger.error(`Error during buyFiat returnTxId sync (${entity.id}):`, e);
}
}
}
+ private async getReturnTxId(entity: { id: number; cryptoInput: CryptoInput }): Promise {
+ // PayIn return (funds were on deposit address)
+ if (entity.cryptoInput.status === PayInStatus.RETURN_CONFIRMED && entity.cryptoInput.returnTxId) {
+ return entity.cryptoInput.returnTxId;
+ }
+
+ // Payout return (funds were forwarded to liquidity)
+ if (entity.cryptoInput.status === PayInStatus.FORWARD_CONFIRMED) {
+ const { isComplete, payoutTxId } = await this.payoutService.checkOrderCompletion(
+ PayoutOrderContext.BUY_FIAT_RETURN,
+ `${entity.id}`,
+ );
+ return isComplete ? payoutTxId : undefined;
+ }
+
+ return undefined;
+ }
+
async registerSellPayIn(): Promise {
const newPayIns = await this.payInService.getNewPayIns();
diff --git a/src/subdomains/core/sell-crypto/process/services/buy-fiat.service.ts b/src/subdomains/core/sell-crypto/process/services/buy-fiat.service.ts
index c83829a118..ccbbeaeca4 100644
--- a/src/subdomains/core/sell-crypto/process/services/buy-fiat.service.ts
+++ b/src/subdomains/core/sell-crypto/process/services/buy-fiat.service.ts
@@ -14,13 +14,15 @@ import { CreateBankDataDto } from 'src/subdomains/generic/user/models/bank-data/
import { UserService } from 'src/subdomains/generic/user/models/user/user.service';
import { WebhookService } from 'src/subdomains/generic/user/services/webhook/webhook.service';
import { BankTxService } from 'src/subdomains/supporting/bank-tx/bank-tx/services/bank-tx.service';
-import { CryptoInput } from 'src/subdomains/supporting/payin/entities/crypto-input.entity';
+import { CryptoInput, PayInStatus } from 'src/subdomains/supporting/payin/entities/crypto-input.entity';
import { PayInService } from 'src/subdomains/supporting/payin/services/payin.service';
import { TransactionRequest } from 'src/subdomains/supporting/payment/entities/transaction-request.entity';
import { TransactionTypeInternal } from 'src/subdomains/supporting/payment/entities/transaction.entity';
import { TransactionHelper } from 'src/subdomains/supporting/payment/services/transaction-helper';
import { TransactionRequestService } from 'src/subdomains/supporting/payment/services/transaction-request.service';
import { TransactionService } from 'src/subdomains/supporting/payment/services/transaction.service';
+import { PayoutOrderContext } from 'src/subdomains/supporting/payout/entities/payout-order.entity';
+import { PayoutService } from 'src/subdomains/supporting/payout/services/payout.service';
import { SupportLogType } from 'src/subdomains/supporting/support-issue/enums/support-log.enum';
import { SupportLogService } from 'src/subdomains/supporting/support-issue/services/support-log.service';
import { Between, FindOptionsRelations, In, MoreThan } from 'typeorm';
@@ -68,6 +70,7 @@ export class BuyFiatService {
@Inject(forwardRef(() => CustodyOrderService))
private readonly custodyOrderService: CustodyOrderService,
private readonly supportLogService: SupportLogService,
+ private readonly payoutService: PayoutService,
) {}
async createFromCryptoInput(cryptoInput: CryptoInput, sell: Sell, request?: TransactionRequest): Promise {
@@ -301,11 +304,22 @@ export class BuyFiatService {
let blockchainFee: number;
if (dto.chargebackAllowedDate && chargebackAmount) {
blockchainFee = await this.transactionHelper.getBlockchainFee(buyFiat.cryptoInput.asset, true);
- await this.payInService.returnPayIn(
- buyFiat.cryptoInput,
- refundUser.address ?? buyFiat.chargebackAddress,
- chargebackAmount,
- );
+
+ const returnAddress = refundUser.address ?? buyFiat.chargebackAddress;
+
+ if (buyFiat.cryptoInput.status === PayInStatus.FORWARD_CONFIRMED) {
+ // Funds already forwarded to liquidity - use PayoutOrder to return
+ await this.payoutService.doPayout({
+ context: PayoutOrderContext.BUY_FIAT_RETURN,
+ correlationId: `${buyFiat.id}`,
+ asset: buyFiat.cryptoInput.asset,
+ amount: chargebackAmount,
+ destinationAddress: returnAddress,
+ });
+ } else {
+ // Funds still on deposit address - use PayIn return
+ await this.payInService.returnPayIn(buyFiat.cryptoInput, returnAddress, chargebackAmount);
+ }
}
await this.buyFiatRepo.update(
diff --git a/src/subdomains/core/sell-crypto/sell-crypto.module.ts b/src/subdomains/core/sell-crypto/sell-crypto.module.ts
index a30334ba74..dd188325c3 100644
--- a/src/subdomains/core/sell-crypto/sell-crypto.module.ts
+++ b/src/subdomains/core/sell-crypto/sell-crypto.module.ts
@@ -9,6 +9,7 @@ import { BankModule } from 'src/subdomains/supporting/bank/bank.module';
import { FiatOutputModule } from 'src/subdomains/supporting/fiat-output/fiat-output.module';
import { NotificationModule } from 'src/subdomains/supporting/notification/notification.module';
import { PayInModule } from 'src/subdomains/supporting/payin/payin.module';
+import { PayoutModule } from 'src/subdomains/supporting/payout/payout.module';
import { PaymentModule } from 'src/subdomains/supporting/payment/payment.module';
import { TransactionModule } from 'src/subdomains/supporting/payment/transaction.module';
import { PricingModule } from 'src/subdomains/supporting/pricing/pricing.module';
@@ -42,6 +43,7 @@ import { SellService } from './route/sell.service';
forwardRef(() => BankModule),
forwardRef(() => BankTxModule),
forwardRef(() => PayInModule),
+ PayoutModule,
forwardRef(() => BuyCryptoModule),
forwardRef(() => AddressPoolModule),
FiatOutputModule,
diff --git a/src/subdomains/generic/user/models/bank-data/dto/__tests__/bank-data-dto.spec.ts b/src/subdomains/generic/user/models/bank-data/dto/__tests__/bank-data-dto.spec.ts
new file mode 100644
index 0000000000..a8c058c4fb
--- /dev/null
+++ b/src/subdomains/generic/user/models/bank-data/dto/__tests__/bank-data-dto.spec.ts
@@ -0,0 +1,73 @@
+import { plainToInstance } from 'class-transformer';
+import { CreateBankDataDto } from '../create-bank-data.dto';
+import { UpdateBankDataDto } from '../update-bank-data.dto';
+
+describe('BankDataDto', () => {
+ describe('CreateBankDataDto', () => {
+ describe('name transform', () => {
+ it('should keep valid name unchanged', () => {
+ const dto = plainToInstance(CreateBankDataDto, { iban: 'CH123', name: 'Max Mustermann' });
+ expect(dto.name).toBe('Max Mustermann');
+ });
+
+ it('should trim whitespace from name', () => {
+ const dto = plainToInstance(CreateBankDataDto, { iban: 'CH123', name: ' Max Mustermann ' });
+ expect(dto.name).toBe('Max Mustermann');
+ });
+
+ it('should transform empty string to null', () => {
+ const dto = plainToInstance(CreateBankDataDto, { iban: 'CH123', name: '' });
+ expect(dto.name).toBeUndefined();
+ });
+
+ it('should transform whitespace-only string to null', () => {
+ const dto = plainToInstance(CreateBankDataDto, { iban: 'CH123', name: ' ' });
+ expect(dto.name).toBeUndefined();
+ });
+
+ it('should transform undefined to null', () => {
+ const dto = plainToInstance(CreateBankDataDto, { iban: 'CH123', name: undefined });
+ expect(dto.name).toBeUndefined();
+ });
+
+ it('should transform null to null', () => {
+ const dto = plainToInstance(CreateBankDataDto, { iban: 'CH123', name: null });
+ expect(dto.name).toBeUndefined();
+ });
+ });
+ });
+
+ describe('UpdateBankDataDto', () => {
+ describe('name transform', () => {
+ it('should keep valid name unchanged', () => {
+ const dto = plainToInstance(UpdateBankDataDto, { name: 'Max Mustermann' });
+ expect(dto.name).toBe('Max Mustermann');
+ });
+
+ it('should trim whitespace from name', () => {
+ const dto = plainToInstance(UpdateBankDataDto, { name: ' Max Mustermann ' });
+ expect(dto.name).toBe('Max Mustermann');
+ });
+
+ it('should transform empty string to null', () => {
+ const dto = plainToInstance(UpdateBankDataDto, { name: '' });
+ expect(dto.name).toBeUndefined();
+ });
+
+ it('should transform whitespace-only string to null', () => {
+ const dto = plainToInstance(UpdateBankDataDto, { name: ' ' });
+ expect(dto.name).toBeUndefined();
+ });
+
+ it('should transform undefined to null', () => {
+ const dto = plainToInstance(UpdateBankDataDto, { name: undefined });
+ expect(dto.name).toBeUndefined();
+ });
+
+ it('should transform null to null', () => {
+ const dto = plainToInstance(UpdateBankDataDto, { name: null });
+ expect(dto.name).toBeUndefined();
+ });
+ });
+ });
+});
diff --git a/src/subdomains/generic/user/models/bank-data/dto/create-bank-data.dto.ts b/src/subdomains/generic/user/models/bank-data/dto/create-bank-data.dto.ts
index ec32ae40e5..3f6d6aa905 100644
--- a/src/subdomains/generic/user/models/bank-data/dto/create-bank-data.dto.ts
+++ b/src/subdomains/generic/user/models/bank-data/dto/create-bank-data.dto.ts
@@ -17,6 +17,7 @@ export class CreateBankDataDto {
@IsOptional()
@IsString()
+ @Transform(({ value }) => value?.trim() || undefined)
name?: string;
@IsOptional()
diff --git a/src/subdomains/generic/user/models/bank-data/dto/update-bank-data.dto.ts b/src/subdomains/generic/user/models/bank-data/dto/update-bank-data.dto.ts
index 0dfc6102fb..d4c13117d5 100644
--- a/src/subdomains/generic/user/models/bank-data/dto/update-bank-data.dto.ts
+++ b/src/subdomains/generic/user/models/bank-data/dto/update-bank-data.dto.ts
@@ -1,3 +1,4 @@
+import { Transform } from 'class-transformer';
import { IsBoolean, IsEnum, IsOptional, IsString } from 'class-validator';
import { ReviewStatus } from 'src/subdomains/generic/kyc/enums/review-status.enum';
import { UpdateBankAccountDto } from 'src/subdomains/supporting/bank/bank-account/dto/update-bank-account.dto';
@@ -6,6 +7,7 @@ import { BankDataType } from '../bank-data.entity';
export class UpdateBankDataDto extends UpdateBankAccountDto {
@IsOptional()
@IsString()
+ @Transform(({ value }) => value?.trim() || undefined)
name?: string;
@IsOptional()
diff --git a/src/subdomains/supporting/dex/services/dex.service.ts b/src/subdomains/supporting/dex/services/dex.service.ts
index a657657ba6..5d910cb424 100644
--- a/src/subdomains/supporting/dex/services/dex.service.ts
+++ b/src/subdomains/supporting/dex/services/dex.service.ts
@@ -170,13 +170,15 @@ export class DexService {
async checkOrderReady(
context: LiquidityOrderContext,
correlationId: string,
- ): Promise<{ isReady: boolean; purchaseTxId: string }> {
+ ): Promise<{ isReady: boolean; purchaseTxId: string; targetAmount: number; targetAsset: string }> {
const order = await this.liquidityOrderRepo.findOneBy({ context, correlationId });
- const purchaseTxId = order && order.txId;
- const isReady = order && order.isReady;
+ const purchaseTxId = order?.txId;
+ const isReady = order?.isReady ?? false;
+ const targetAmount = order?.targetAmount;
+ const targetAsset = order?.targetAsset?.name;
- return { isReady, purchaseTxId };
+ return { isReady, purchaseTxId, targetAmount, targetAsset };
}
async checkOrderCompletion(
diff --git a/src/subdomains/supporting/payment/dto/transaction.dto.ts b/src/subdomains/supporting/payment/dto/transaction.dto.ts
index 6dd3d3a86d..2f7876c026 100644
--- a/src/subdomains/supporting/payment/dto/transaction.dto.ts
+++ b/src/subdomains/supporting/payment/dto/transaction.dto.ts
@@ -114,6 +114,7 @@ export const TransactionReasonMapper: {
[AmlReason.BANK_RELEASE_PENDING]: TransactionReason.BANK_RELEASE_PENDING,
[AmlReason.VIRTUAL_IBAN_USER_MISMATCH]: TransactionReason.UNKNOWN,
[AmlReason.INTERMEDIARY_WITHOUT_SENDER]: TransactionReason.BANK_NOT_ALLOWED,
+ [AmlReason.NAME_TOO_SHORT]: TransactionReason.KYC_DATA_NEEDED,
};
export class UnassignedTransactionDto {
diff --git a/src/subdomains/supporting/realunit/__tests__/realunit-dev.service.spec.ts b/src/subdomains/supporting/realunit/__tests__/realunit-dev.service.spec.ts
index 652abe997b..6df4881e3e 100644
--- a/src/subdomains/supporting/realunit/__tests__/realunit-dev.service.spec.ts
+++ b/src/subdomains/supporting/realunit/__tests__/realunit-dev.service.spec.ts
@@ -100,14 +100,6 @@ describe('RealUnitDevService', () => {
let transactionService: jest.Mocked;
let buyCryptoRepo: jest.Mocked;
- const mainnetRealuAsset = createCustomAsset({
- id: 399,
- name: 'REALU',
- blockchain: Blockchain.ETHEREUM,
- type: AssetType.TOKEN,
- decimals: 0,
- });
-
const sepoliaRealuAsset = createCustomAsset({
id: 408,
name: 'REALU',
@@ -144,7 +136,7 @@ describe('RealUnitDevService', () => {
id: 7,
amount: 100,
sourceId: 1,
- targetId: 399,
+ targetId: 408, // Sepolia REALU asset ID
routeId: 1,
status: TransactionRequestStatus.WAITING_FOR_PAYMENT,
type: TransactionRequestType.BUY,
@@ -244,37 +236,25 @@ describe('RealUnitDevService', () => {
it('should execute on DEV environment', async () => {
(global as any).__mockEnvironment = 'dev';
- assetService.getAssetByQuery.mockResolvedValueOnce(mainnetRealuAsset);
assetService.getAssetByQuery.mockResolvedValueOnce(sepoliaRealuAsset);
transactionRequestRepo.find.mockResolvedValue([]);
await service.simulateRealuPayments();
- expect(assetService.getAssetByQuery).toHaveBeenCalledTimes(2);
+ expect(assetService.getAssetByQuery).toHaveBeenCalledTimes(1);
});
it('should execute on LOC environment', async () => {
(global as any).__mockEnvironment = 'loc';
- assetService.getAssetByQuery.mockResolvedValueOnce(mainnetRealuAsset);
assetService.getAssetByQuery.mockResolvedValueOnce(sepoliaRealuAsset);
transactionRequestRepo.find.mockResolvedValue([]);
await service.simulateRealuPayments();
- expect(assetService.getAssetByQuery).toHaveBeenCalledTimes(2);
- });
-
- it('should skip if mainnet REALU asset not found', async () => {
- assetService.getAssetByQuery.mockResolvedValueOnce(null);
- assetService.getAssetByQuery.mockResolvedValueOnce(sepoliaRealuAsset);
-
- await service.simulateRealuPayments();
-
- expect(transactionRequestRepo.find).not.toHaveBeenCalled();
+ expect(assetService.getAssetByQuery).toHaveBeenCalledTimes(1);
});
it('should skip if sepolia REALU asset not found', async () => {
- assetService.getAssetByQuery.mockResolvedValueOnce(mainnetRealuAsset);
assetService.getAssetByQuery.mockResolvedValueOnce(null);
await service.simulateRealuPayments();
@@ -283,7 +263,6 @@ describe('RealUnitDevService', () => {
});
it('should skip if no waiting requests', async () => {
- assetService.getAssetByQuery.mockResolvedValueOnce(mainnetRealuAsset);
assetService.getAssetByQuery.mockResolvedValueOnce(sepoliaRealuAsset);
transactionRequestRepo.find.mockResolvedValue([]);
@@ -292,8 +271,7 @@ describe('RealUnitDevService', () => {
expect(buyService.getBuyByKey).not.toHaveBeenCalled();
});
- it('should query for WAITING_FOR_PAYMENT requests with mainnet REALU targetId', async () => {
- assetService.getAssetByQuery.mockResolvedValueOnce(mainnetRealuAsset);
+ it('should query for WAITING_FOR_PAYMENT requests with sepolia REALU targetId', async () => {
assetService.getAssetByQuery.mockResolvedValueOnce(sepoliaRealuAsset);
transactionRequestRepo.find.mockResolvedValue([]);
@@ -303,7 +281,7 @@ describe('RealUnitDevService', () => {
where: {
status: TransactionRequestStatus.WAITING_FOR_PAYMENT,
type: TransactionRequestType.BUY,
- targetId: 399,
+ targetId: 408,
},
});
});
@@ -311,7 +289,6 @@ describe('RealUnitDevService', () => {
describe('simulatePaymentForRequest', () => {
beforeEach(() => {
- assetService.getAssetByQuery.mockResolvedValueOnce(mainnetRealuAsset);
assetService.getAssetByQuery.mockResolvedValueOnce(sepoliaRealuAsset);
});
diff --git a/src/subdomains/supporting/realunit/realunit-dev.service.ts b/src/subdomains/supporting/realunit/realunit-dev.service.ts
index 174ecc52d2..9025c16cca 100644
--- a/src/subdomains/supporting/realunit/realunit-dev.service.ts
+++ b/src/subdomains/supporting/realunit/realunit-dev.service.ts
@@ -28,6 +28,12 @@ import { TransactionService } from '../payment/services/transaction.service';
@Injectable()
export class RealUnitDevService {
private readonly logger = new DfxLogger(RealUnitDevService);
+ private readonly tokenName = 'REALU';
+ private readonly tokenBlockchain = Blockchain.SEPOLIA;
+
+ private get isDevEnvironment(): boolean {
+ return [Environment.DEV, Environment.LOC].includes(Config.environment);
+ }
constructor(
private readonly transactionRequestRepo: TransactionRequestRepository,
@@ -44,7 +50,7 @@ export class RealUnitDevService {
@Cron(CronExpression.EVERY_MINUTE)
@Lock(60)
async simulateRealuPayments(): Promise {
- if (![Environment.DEV, Environment.LOC].includes(Config.environment)) return;
+ if (!this.isDevEnvironment) return;
try {
await this.processWaitingRealuRequests();
@@ -54,22 +60,14 @@ export class RealUnitDevService {
}
private async processWaitingRealuRequests(): Promise {
- // TransactionRequests are created with Mainnet REALU (via realunit.service.ts)
- const mainnetRealuAsset = await this.assetService.getAssetByQuery({
- name: 'REALU',
- blockchain: Blockchain.ETHEREUM,
- type: AssetType.TOKEN,
- });
-
- // But payouts go to Sepolia in DEV environment
- const sepoliaRealuAsset = await this.assetService.getAssetByQuery({
- name: 'REALU',
- blockchain: Blockchain.SEPOLIA,
+ const realuAsset = await this.assetService.getAssetByQuery({
+ name: this.tokenName,
+ blockchain: this.tokenBlockchain,
type: AssetType.TOKEN,
});
- if (!mainnetRealuAsset || !sepoliaRealuAsset) {
- this.logger.warn('REALU asset not found (mainnet or sepolia) - skipping simulation');
+ if (!realuAsset) {
+ this.logger.warn('REALU asset not found - skipping buy simulation');
return;
}
@@ -77,7 +75,7 @@ export class RealUnitDevService {
where: {
status: TransactionRequestStatus.WAITING_FOR_PAYMENT,
type: TransactionRequestType.BUY,
- targetId: mainnetRealuAsset.id,
+ targetId: realuAsset.id,
},
});
@@ -87,7 +85,7 @@ export class RealUnitDevService {
for (const request of waitingRequests) {
try {
- await this.simulatePaymentForRequest(request, sepoliaRealuAsset);
+ await this.simulatePaymentForRequest(request, realuAsset);
} catch (e) {
this.logger.error(`Failed to simulate payment for TransactionRequest ${request.id}:`, e);
}
diff --git a/src/subdomains/supporting/realunit/realunit.service.ts b/src/subdomains/supporting/realunit/realunit.service.ts
index 33b0abaf93..3d539bf539 100644
--- a/src/subdomains/supporting/realunit/realunit.service.ts
+++ b/src/subdomains/supporting/realunit/realunit.service.ts
@@ -8,7 +8,7 @@ import {
} from '@nestjs/common';
import { verifyTypedData } from 'ethers/lib/utils';
import { request } from 'graphql-request';
-import { Config, GetConfig } from 'src/config/config';
+import { Config, Environment, GetConfig } from 'src/config/config';
import {
BrokerbotBuyPriceDto,
BrokerbotInfoDto,
@@ -75,7 +75,9 @@ export class RealUnitService {
private readonly ponderUrl: string;
private readonly genesisDate = new Date('2022-04-12 07:46:41.000');
private readonly tokenName = 'REALU';
- private readonly tokenBlockchain = Blockchain.ETHEREUM;
+ private readonly tokenBlockchain = [Environment.DEV, Environment.LOC].includes(Config.environment)
+ ? Blockchain.SEPOLIA
+ : Blockchain.ETHEREUM;
private readonly historicalPriceCache = new AsyncCache(CacheItemResetPeriod.EVERY_6_HOURS);
constructor(