From a4b31dc2a0249b6420eb24cbfbb74c4987036e6c Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Wed, 4 Mar 2026 11:33:27 +0100 Subject: [PATCH 1/2] fix: filter payout-sent BuyCrypto from pending balance log Exclude BuyCrypto entries from pending obligations when their PayoutOrder is already in PAYOUT_PENDING or COMPLETE status, preventing double-counting that caused negative totalBalance. --- src/subdomains/supporting/log/log-job.service.ts | 4 +++- .../supporting/payout/services/payout.service.ts | 10 +++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/subdomains/supporting/log/log-job.service.ts b/src/subdomains/supporting/log/log-job.service.ts index ec2a043176..fa518da5ed 100644 --- a/src/subdomains/supporting/log/log-job.service.ts +++ b/src/subdomains/supporting/log/log-job.service.ts @@ -261,6 +261,8 @@ export class LogJobService { const pendingPayIns = await this.payInService.getPendingPayIns(); const pendingBuyFiat = await this.buyFiatService.getPendingTransactions(); const pendingBuyCrypto = await this.buyCryptoService.getPendingTransactions(); + const payoutSentBuyCryptoIds = await this.payoutService.getPayoutSentCorrelationIds(PayoutOrderContext.BUY_CRYPTO); + const filteredPendingBuyCrypto = pendingBuyCrypto.filter((tx) => !payoutSentBuyCryptoIds.has(tx.id.toString())); const pendingBankTx = await this.bankTxService.getPendingTx(); const pendingBankTxRepeat = await this.bankTxRepeatService.getPendingTx(); const pendingBankTxReturn = await this.bankTxReturnService.getPendingTx(); @@ -822,7 +824,7 @@ export class LogJobService { const manualDebtPosition = manualDebtPositions.find((p) => p.assetId === curr.id)?.value ?? 0; const { input: buyFiat, output: buyFiatPass } = this.getPendingAmounts([curr], pendingBuyFiat); - const { input: buyCrypto, output: buyCryptoPass } = this.getPendingAmounts([curr], pendingBuyCrypto); + const { input: buyCrypto, output: buyCryptoPass } = this.getPendingAmounts([curr], filteredPendingBuyCrypto); const bankTxNull = this.getPendingAmounts( [curr], diff --git a/src/subdomains/supporting/payout/services/payout.service.ts b/src/subdomains/supporting/payout/services/payout.service.ts index d96496be56..8fb0e4fcd2 100644 --- a/src/subdomains/supporting/payout/services/payout.service.ts +++ b/src/subdomains/supporting/payout/services/payout.service.ts @@ -7,7 +7,7 @@ import { DfxCron } from 'src/shared/utils/cron'; import { Util } from 'src/shared/utils/util'; import { MailContext, MailType } from 'src/subdomains/supporting/notification/enums'; import { NotificationService } from 'src/subdomains/supporting/notification/services/notification.service'; -import { FindOptionsRelations, IsNull, MoreThan, Not } from 'typeorm'; +import { FindOptionsRelations, In, IsNull, MoreThan, Not } from 'typeorm'; import { MailRequest } from '../../notification/interfaces'; import { PayoutOrder, PayoutOrderContext, PayoutOrderStatus } from '../entities/payout-order.entity'; import { PayoutOrderFactory } from '../factories/payout-order.factory'; @@ -79,6 +79,14 @@ export class PayoutService { }; } + async getPayoutSentCorrelationIds(context: PayoutOrderContext): Promise> { + const orders = await this.payoutOrderRepo.findBy({ + context, + status: In([PayoutOrderStatus.PAYOUT_PENDING, PayoutOrderStatus.COMPLETE]), + }); + return new Set(orders.map((o) => o.correlationId)); + } + async estimateFee(targetAsset: Asset, address: string, amount: number, asset: Asset): Promise { const prepareStrategy = this.prepareStrategyRegistry.getPrepareStrategy(targetAsset); const payoutStrategy = this.payoutStrategyRegistry.getPayoutStrategy(targetAsset); From 517294faed3b5591a2f286f10981f879b6a9f2ba Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Wed, 4 Mar 2026 11:42:53 +0100 Subject: [PATCH 2/2] perf: scope payout query to last hour to avoid loading 86k rows Rename to getRecentPayoutSentCorrelationIds and add MoreThan(1h) filter. The race condition window is ~20min max, so 1h is sufficient. --- src/subdomains/supporting/log/log-job.service.ts | 4 +++- src/subdomains/supporting/payout/services/payout.service.ts | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/subdomains/supporting/log/log-job.service.ts b/src/subdomains/supporting/log/log-job.service.ts index fa518da5ed..989b256596 100644 --- a/src/subdomains/supporting/log/log-job.service.ts +++ b/src/subdomains/supporting/log/log-job.service.ts @@ -261,7 +261,9 @@ export class LogJobService { const pendingPayIns = await this.payInService.getPendingPayIns(); const pendingBuyFiat = await this.buyFiatService.getPendingTransactions(); const pendingBuyCrypto = await this.buyCryptoService.getPendingTransactions(); - const payoutSentBuyCryptoIds = await this.payoutService.getPayoutSentCorrelationIds(PayoutOrderContext.BUY_CRYPTO); + const payoutSentBuyCryptoIds = await this.payoutService.getRecentPayoutSentCorrelationIds( + PayoutOrderContext.BUY_CRYPTO, + ); const filteredPendingBuyCrypto = pendingBuyCrypto.filter((tx) => !payoutSentBuyCryptoIds.has(tx.id.toString())); const pendingBankTx = await this.bankTxService.getPendingTx(); const pendingBankTxRepeat = await this.bankTxRepeatService.getPendingTx(); diff --git a/src/subdomains/supporting/payout/services/payout.service.ts b/src/subdomains/supporting/payout/services/payout.service.ts index 8fb0e4fcd2..da3b82e7c1 100644 --- a/src/subdomains/supporting/payout/services/payout.service.ts +++ b/src/subdomains/supporting/payout/services/payout.service.ts @@ -79,10 +79,12 @@ export class PayoutService { }; } - async getPayoutSentCorrelationIds(context: PayoutOrderContext): Promise> { + async getRecentPayoutSentCorrelationIds(context: PayoutOrderContext): Promise> { + const since = new Date(Date.now() - 3600_000); // 1 hour const orders = await this.payoutOrderRepo.findBy({ context, status: In([PayoutOrderStatus.PAYOUT_PENDING, PayoutOrderStatus.COMPLETE]), + updated: MoreThan(since), }); return new Set(orders.map((o) => o.correlationId)); }