diff --git a/migration/1768325447128-RefRewardLiqPipeline.js b/migration/1768325447128-RefRewardLiqPipeline.js new file mode 100644 index 0000000000..30a04643e1 --- /dev/null +++ b/migration/1768325447128-RefRewardLiqPipeline.js @@ -0,0 +1,28 @@ +/** + * @typedef {import('typeorm').MigrationInterface} MigrationInterface + * @typedef {import('typeorm').QueryRunner} QueryRunner + */ + +/** + * @class + * @implements {MigrationInterface} + */ +module.exports = class RefRewardLiqPipeline1768325447128 { + name = 'RefRewardLiqPipeline1768325447128' + + /** + * @param {QueryRunner} queryRunner + */ + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "ref_reward" ADD "liquidityPipelineId" int`); + await queryRunner.query(`ALTER TABLE "ref_reward" ADD CONSTRAINT "FK_0bdf973ad618dffd7a7c6c53dc8" FOREIGN KEY ("liquidityPipelineId") REFERENCES "liquidity_management_pipeline"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + } + + /** + * @param {QueryRunner} queryRunner + */ + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "ref_reward" DROP CONSTRAINT "FK_0bdf973ad618dffd7a7c6c53dc8"`); + await queryRunner.query(`ALTER TABLE "ref_reward" DROP COLUMN "liquidityPipelineId"`); + } +} diff --git a/package-lock.json b/package-lock.json index 0e8271d577..5427a0c11f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6193,30 +6193,6 @@ "node": ">= 0.10.0" } }, - "node_modules/@nestjs/platform-express/node_modules/express/node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.1", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, "node_modules/@nestjs/platform-express/node_modules/finalhandler": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", @@ -6298,21 +6274,6 @@ "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", "license": "MIT" }, - "node_modules/@nestjs/platform-express/node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/@nestjs/platform-express/node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -12318,9 +12279,9 @@ "license": "MIT" }, "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "license": "MIT", "dependencies": { "bytes": "3.1.2", @@ -12331,7 +12292,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -16046,22 +16007,6 @@ "node": ">= 0.8" } }, - "node_modules/express/node_modules/qs": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", - "license": "BSD-3-Clause", - "peer": true, - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/express/node_modules/raw-body": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", @@ -24594,12 +24539,12 @@ } }, "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.1.0" }, "engines": { "node": ">=0.6" @@ -24936,15 +24881,6 @@ "node": ">= 0.6" } }, - "node_modules/request/node_modules/qs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", - "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.6" - } - }, "node_modules/request/node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -25904,6 +25840,45 @@ "url": "https://opencollective.com/express" } }, + "node_modules/servify/node_modules/express/node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/servify/node_modules/express/node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/servify/node_modules/finalhandler": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", @@ -25988,21 +25963,6 @@ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "license": "MIT" }, - "node_modules/servify/node_modules/qs": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/servify/node_modules/raw-body": { "version": "2.5.3", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", @@ -27244,22 +27204,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/superagent/node_modules/qs": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/superstruct": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-2.0.2.tgz", diff --git a/package.json b/package.json index afce4d539c..af1158c7c6 100644 --- a/package.json +++ b/package.json @@ -181,6 +181,8 @@ "forceExit": true }, "overrides": { + "body-parser": "1.20.3", + "qs": "^6.14.1", "request": { "form-data": "2.5.5" }, diff --git a/src/main.ts b/src/main.ts index 270fbde53b..8a02d765e5 100644 --- a/src/main.ts +++ b/src/main.ts @@ -42,7 +42,7 @@ async function bootstrap() { AppInsights.start(); } - const app = await NestFactory.create(AppModule); + const app = await NestFactory.create(AppModule, { bodyParser: false }); app.use(morgan('dev')); app.use(helmet()); diff --git a/src/subdomains/core/history/controllers/transaction.controller.ts b/src/subdomains/core/history/controllers/transaction.controller.ts index eb33dcf98e..099182832d 100644 --- a/src/subdomains/core/history/controllers/transaction.controller.ts +++ b/src/subdomains/core/history/controllers/transaction.controller.ts @@ -447,7 +447,7 @@ export class TransactionController { if (!dto.creditorData) throw new BadRequestException('Creditor data is required for bank refunds'); return this.bankTxReturnService.refundBankTx(transaction.targetEntity, { - refundIban: refundData.refundTarget ?? dto.refundTarget, + refundIban: dto.refundTarget ?? refundData.refundTarget, chargebackCurrency, creditorData: dto.creditorData, ...refundDto, @@ -476,7 +476,7 @@ export class TransactionController { if (!dto.creditorData) throw new BadRequestException('Creditor data is required for bank refunds'); return this.buyCryptoService.refundBankTx(transaction.targetEntity, { - refundIban: refundData.refundTarget ?? dto.refundTarget, + refundIban: dto.refundTarget ?? refundData.refundTarget, chargebackCurrency, creditorData: dto.creditorData, ...refundDto, diff --git a/src/subdomains/core/liquidity-management/entities/liquidity-management-pipeline.entity.ts b/src/subdomains/core/liquidity-management/entities/liquidity-management-pipeline.entity.ts index 598c023a1d..f34a5da967 100644 --- a/src/subdomains/core/liquidity-management/entities/liquidity-management-pipeline.entity.ts +++ b/src/subdomains/core/liquidity-management/entities/liquidity-management-pipeline.entity.ts @@ -1,6 +1,7 @@ import { IEntity } from 'src/shared/models/entity'; import { Column, Entity, Index, JoinTable, ManyToOne, OneToMany } from 'typeorm'; import { BuyCrypto } from '../../buy-crypto/process/entities/buy-crypto.entity'; +import { RefReward } from '../../referral/reward/ref-reward.entity'; import { LiquidityManagementExchanges, LiquidityManagementOrderStatus, @@ -28,6 +29,9 @@ export class LiquidityManagementPipeline extends IEntity { @OneToMany(() => BuyCrypto, (buyCrypto) => buyCrypto.liquidityPipeline) buyCryptos: BuyCrypto[]; + @OneToMany(() => RefReward, (refReward) => refReward.liquidityPipeline) + refRewards: RefReward[]; + @OneToMany(() => LiquidityManagementOrder, (orders) => orders.pipeline) orders: LiquidityManagementOrder[]; diff --git a/src/subdomains/core/referral/referral.module.ts b/src/subdomains/core/referral/referral.module.ts index 27546ea109..265bc34db0 100644 --- a/src/subdomains/core/referral/referral.module.ts +++ b/src/subdomains/core/referral/referral.module.ts @@ -2,6 +2,7 @@ import { forwardRef, Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { BlockchainModule } from 'src/integration/blockchain/blockchain.module'; import { SharedModule } from 'src/shared/shared.module'; +import { LiquidityManagementModule } from 'src/subdomains/core/liquidity-management/liquidity-management.module'; import { UserModule } from 'src/subdomains/generic/user/user.module'; import { DexModule } from 'src/subdomains/supporting/dex/dex.module'; import { NotificationModule } from 'src/subdomains/supporting/notification/notification.module'; @@ -32,6 +33,7 @@ import { RefRewardService } from './reward/services/ref-reward.service'; NotificationModule, PricingModule, forwardRef(() => TransactionModule), + LiquidityManagementModule, ], controllers: [RefController, RefRewardController], providers: [ diff --git a/src/subdomains/core/referral/reward/ref-reward.entity.ts b/src/subdomains/core/referral/reward/ref-reward.entity.ts index 6cf21c4926..b82dccd82d 100644 --- a/src/subdomains/core/referral/reward/ref-reward.entity.ts +++ b/src/subdomains/core/referral/reward/ref-reward.entity.ts @@ -1,5 +1,6 @@ import { Blockchain } from 'src/integration/blockchain/shared/enums/blockchain.enum'; import { UpdateResult } from 'src/shared/models/entity'; +import { LiquidityManagementPipeline } from 'src/subdomains/core/liquidity-management/entities/liquidity-management-pipeline.entity'; import { UserData } from 'src/subdomains/generic/user/models/user-data/user-data.entity'; import { User } from 'src/subdomains/generic/user/models/user/user.entity'; import { Transaction } from 'src/subdomains/supporting/payment/entities/transaction.entity'; @@ -40,6 +41,9 @@ export class RefReward extends Reward { @JoinColumn() sourceTransaction?: Transaction; + @ManyToOne(() => LiquidityManagementPipeline, { nullable: true }) + liquidityPipeline?: LiquidityManagementPipeline; + //*** FACTORY METHODS ***// readyToPayout(outputAmount: number): UpdateResult { diff --git a/src/subdomains/core/referral/reward/services/ref-reward-dex.service.ts b/src/subdomains/core/referral/reward/services/ref-reward-dex.service.ts index 110882db41..b2eeeb6f12 100644 --- a/src/subdomains/core/referral/reward/services/ref-reward-dex.service.ts +++ b/src/subdomains/core/referral/reward/services/ref-reward-dex.service.ts @@ -2,8 +2,10 @@ import { Injectable } from '@nestjs/common'; import { Asset } from 'src/shared/models/asset/asset.entity'; import { DfxLogger } from 'src/shared/services/dfx-logger'; import { Util } from 'src/shared/utils/util'; +import { LiquidityManagementService } from 'src/subdomains/core/liquidity-management/services/liquidity-management.service'; import { LiquidityOrderContext } from 'src/subdomains/supporting/dex/entities/liquidity-order.entity'; -import { PurchaseLiquidityRequest, ReserveLiquidityRequest } from 'src/subdomains/supporting/dex/interfaces'; +import { NotEnoughLiquidityException } from 'src/subdomains/supporting/dex/exceptions/not-enough-liquidity.exception'; +import { ReserveLiquidityRequest } from 'src/subdomains/supporting/dex/interfaces'; import { DexService } from 'src/subdomains/supporting/dex/services/dex.service'; import { PriceCurrency, @@ -27,45 +29,78 @@ export class RefRewardDexService { private readonly refRewardRepo: RefRewardRepository, private readonly dexService: DexService, private readonly priceService: PricingService, + private readonly liquidityService: LiquidityManagementService, ) {} async secureLiquidity(): Promise { const newRefRewards = await this.refRewardRepo.find({ where: { status: RewardStatus.PREPARED }, + relations: { liquidityPipeline: true }, }); const groupedRewards = Util.groupByAccessor(newRefRewards, (r) => r.outputAsset.id); for (const rewards of groupedRewards.values()) { + const asset = rewards[0].outputAsset; + + // Skip if any pipeline is running for this asset + if (rewards.some((r) => r.liquidityPipeline && !r.liquidityPipeline.isDone)) { + continue; + } + try { - // payout asset price - const asset = rewards[0].outputAsset; const assetPrice = await this.priceService.getPrice(PriceCurrency.EUR, asset, PriceValidity.VALID_ONLY); for (const reward of rewards) { - const outputAmount = assetPrice.convert(reward.amountInEur, 8); + try { + const outputAmount = assetPrice.convert(reward.amountInEur, 8); + + await this.reserveLiquidity({ + amount: outputAmount, + asset, + rewardId: reward.id.toString(), + }); - await this.checkLiquidity({ - amount: outputAmount, - asset, - rewardId: reward.id.toString(), - }); + await this.refRewardRepo.update(...reward.readyToPayout(outputAmount)); + } catch (e) { + if (e instanceof NotEnoughLiquidityException) { + // Start ONE pipeline for ALL remaining rewards + const remainingRewards = rewards.filter((r) => r.status === RewardStatus.PREPARED); + const totalAmount = Util.round( + remainingRewards.reduce((sum, r) => sum + assetPrice.convert(r.amountInEur, 8), 0), + 8, + ); + await this.startLiquidityPipeline(remainingRewards, asset, totalAmount); + break; + } - await this.refRewardRepo.update(...reward.readyToPayout(outputAmount)); + this.logger.error(`Error in processing ref reward ${reward.id}:`, e); + } } } catch (e) { - this.logger.error(`Error in processing ref rewards for ${rewards[0].outputAsset.uniqueName}:`, e); + this.logger.error(`Error in processing ref rewards for ${asset.uniqueName}:`, e); } } } - private async checkLiquidity(request: RefLiquidityRequest): Promise { - const reserveRequest = this.createLiquidityRequest(request); + private async startLiquidityPipeline(rewards: RefReward[], asset: Asset, amount: number): Promise { + try { + const pipeline = await this.liquidityService.buyLiquidity(asset.id, amount, amount, true); + this.logger.info(`Missing ref-reward liquidity. Liquidity management order created: ${pipeline.id}`); + + for (const reward of rewards) { + await this.refRewardRepo.update(reward.id, { liquidityPipeline: pipeline }); + } + } catch (e) { + this.logger.error(`Failed to start liquidity pipeline for ref rewards (${asset.uniqueName}):`, e); + } + } - return this.dexService.reserveLiquidity(reserveRequest); + private async reserveLiquidity(request: RefLiquidityRequest): Promise { + return this.dexService.reserveLiquidity(this.createReserveLiquidityRequest(request)); } - private createLiquidityRequest(request: RefLiquidityRequest): PurchaseLiquidityRequest | ReserveLiquidityRequest { + private createReserveLiquidityRequest(request: RefLiquidityRequest): ReserveLiquidityRequest { return { context: LiquidityOrderContext.REF_PAYOUT, correlationId: request.rewardId, diff --git a/src/subdomains/supporting/log/log-job.service.ts b/src/subdomains/supporting/log/log-job.service.ts index d7a495d206..aabb57d1c6 100644 --- a/src/subdomains/supporting/log/log-job.service.ts +++ b/src/subdomains/supporting/log/log-job.service.ts @@ -330,6 +330,7 @@ export class LogJobService { const chfReceiverExchangeTx = recentKrakenExchangeTx.filter( (k) => k.type === ExchangeTxType.DEPOSIT && + k.status !== 'pending' && k.method === 'Bank Frick (SIC) International' && k.address === yapealChfBank.bic.padEnd(11, 'XXX'), ); @@ -341,6 +342,7 @@ export class LogJobService { const eurReceiverExchangeTx = recentKrakenExchangeTx.filter( (k) => k.type === ExchangeTxType.DEPOSIT && + k.status !== 'pending' && k.method === 'Bank Frick (SEPA) International' && k.address === yapealEurBank.bic.padEnd(11, 'XXX'), );