Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions migration/1768315830503-UpdateXtDeuroLiquidityMinimum.js
Original file line number Diff line number Diff line change
@@ -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
`);
}
}
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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;
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/integration/sift/dto/sift.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
3 changes: 2 additions & 1 deletion src/shared/i18n/de/mail.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:<br>[url:{urlText}]"
},
Expand Down
3 changes: 2 additions & 1 deletion src/shared/i18n/en/mail.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:<br>[url:{urlText}]"
},
Expand Down
3 changes: 2 additions & 1 deletion src/shared/i18n/es/mail.json
Original file line number Diff line number Diff line change
Expand Up @@ -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í:<br>[url:{urlText}]"
},
Expand Down
3 changes: 2 additions & 1 deletion src/shared/i18n/fr/mail.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:<br>[url:{urlText}]"
},
Expand Down
3 changes: 2 additions & 1 deletion src/shared/i18n/it/mail.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:<br>[url:{urlText}]"
},
Expand Down
3 changes: 2 additions & 1 deletion src/shared/i18n/pt/mail.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:<br>[url:{urlText}]"
},
Expand Down
6 changes: 6 additions & 0 deletions src/subdomains/core/aml/enums/aml-error.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions src/subdomains/core/aml/enums/aml-reason.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
11 changes: 11 additions & 0 deletions src/subdomains/core/aml/services/aml-helper.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -40,10 +42,8 @@ export class DfxDexAdapter extends LiquidityActionAdapter {
async checkCompletion(order: LiquidityManagementOrder): Promise<boolean> {
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);
Expand All @@ -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`);
Expand All @@ -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<CorrelationId> {
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);
Expand All @@ -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<CorrelationId> {
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();
}
Expand Down Expand Up @@ -163,7 +193,7 @@ export class DfxDexAdapter extends LiquidityActionAdapter {

// --- COMPLETION CHECKS --- //

private async checkSellPurchaseCompletion(order: LiquidityManagementOrder): Promise<boolean> {
private async checkSwapCompletion(order: LiquidityManagementOrder): Promise<boolean> {
try {
const result = await this.dexService.checkOrderReady(
LiquidityOrderContext.LIQUIDITY_MANAGEMENT,
Expand All @@ -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;
Expand Down Expand Up @@ -235,4 +268,68 @@ export class DfxDexAdapter extends LiquidityActionAdapter {
this.exchangeRegistry.get(system)
);
}

private validateSwapParams(params: Record<string, unknown>): boolean {
try {
this.parseSwapParams(params);
return true;
} catch {
return false;
}
}

private parseSwapParams(params: Record<string, unknown>): { 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<Asset> {
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;
}
}
Loading
Loading