From 04b10968d8322e12ef126510d9f37d3d688f0aa3 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Thu, 19 Mar 2026 19:10:00 +0100 Subject: [PATCH 1/4] fix: add timeout to blockchain balance updates to prevent hanging cron (#3468) * fix: add timeout to blockchain balance updates to prevent hanging cron A hanging RPC call (e.g. Firo) would store a never-resolving promise in the updateCalls cache. Subsequent cron runs reused that same promise, permanently blocking checkLiquidityBalances for all providers. This caused all liquidity balances (Olkypay, Yapeal, exchanges, blockchains) to go stale. Wrap the stored promise with a 30s timeout so hanging calls get rejected and the cache entry is cleaned up. * fix: consolidate cleanup to prevent race condition Move updateCalls map cleanup from updateBalancesFor's finally block into the timeout wrapper's finally block. This prevents a late-returning call from deleting a newer map entry after timeout. --- .../adapters/balances/blockchain.adapter.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/subdomains/core/liquidity-management/adapters/balances/blockchain.adapter.ts b/src/subdomains/core/liquidity-management/adapters/balances/blockchain.adapter.ts index 943e6b9beb..7bbd8fe5c8 100644 --- a/src/subdomains/core/liquidity-management/adapters/balances/blockchain.adapter.ts +++ b/src/subdomains/core/liquidity-management/adapters/balances/blockchain.adapter.ts @@ -25,6 +25,7 @@ export class BlockchainAdapter implements LiquidityBalanceIntegration { private readonly logger = new DfxLogger(BlockchainAdapter); private readonly refreshInterval = 45; // seconds + private readonly balanceTimeout = 30000; // ms private readonly balanceCache = new Map(); private readonly updateCalls = new Map>(); @@ -71,7 +72,15 @@ export class BlockchainAdapter implements LiquidityBalanceIntegration { private async updateCacheFor(blockchain: Blockchain, assets: Asset[]): Promise { if (!this.updateCalls.has(blockchain)) { - this.updateCalls.set(blockchain, this.updateBalancesFor(blockchain, assets)); + const call = Util.timeout(this.updateBalancesFor(blockchain, assets), this.balanceTimeout) + .catch((e) => { + this.logger.error(`Timeout updating balances for ${blockchain}:`, e); + this.invalidateCacheFor(assets); + }) + .finally(() => { + this.updateCalls.delete(blockchain); + }); + this.updateCalls.set(blockchain, call); } return this.updateCalls.get(blockchain); @@ -122,8 +131,6 @@ export class BlockchainAdapter implements LiquidityBalanceIntegration { this.updateTimestamps.set(blockchain, updated); } catch (e) { this.logger.error(`Failed to update balances for ${blockchain}:`, e); - } finally { - this.updateCalls.delete(blockchain); } } From 1ccc8ebe013d90d0c99a2bb17bc8c83b66c07cdb Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Thu, 19 Mar 2026 20:59:20 +0100 Subject: [PATCH 2/4] fix: add timeout to custom balance RPC calls in finance log (#3470) The getCustomBalances call in saveTradingLog has no timeout protection. If a blockchain RPC call hangs (e.g. Firo), the entire finance log cron is blocked for the full lock timeout (30 min), causing gaps in the FinancialDataLog. Wrap the call with Util.timeout(30s) to match the timeout added to BlockchainAdapter in #3468. --- src/subdomains/supporting/log/log-job.service.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/subdomains/supporting/log/log-job.service.ts b/src/subdomains/supporting/log/log-job.service.ts index b5ec2bd1ca..8b5b3fd646 100644 --- a/src/subdomains/supporting/log/log-job.service.ts +++ b/src/subdomains/supporting/log/log-job.service.ts @@ -228,8 +228,9 @@ export class LogJobService { return { blockchain: b, balances: [] }; } - const balances = await this.getCustomBalances(client, a, Config.financialLog.customAddresses).then((b) => - b.flat(), + const balances = await Util.timeout( + this.getCustomBalances(client, a, Config.financialLog.customAddresses).then((b) => b.flat()), + 30000, ); return { blockchain: b, balances }; } catch (e) { From 22d090ec1e0198b057b800e7aa96449d641fe9ca Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Thu, 19 Mar 2026 21:19:20 +0100 Subject: [PATCH 3/4] fix: use amount as liquidity base and deduct locked funds from exchangeOrder (#3471) PR #3462 switched to availableAmount globally, but this over-corrected for exchanges like XT where locked funds are trading orders not tracked as exchangeOrder. This caused ~40k CHF to silently drop from the balance. Revert to amount as liquidity base. Instead, deduct the locked portion (amount - availableAmount) from exchangeOrder to avoid the Scrypt double-counting that #3462 originally fixed. --- src/subdomains/supporting/log/log-job.service.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/subdomains/supporting/log/log-job.service.ts b/src/subdomains/supporting/log/log-job.service.ts index 8b5b3fd646..6752f86cbb 100644 --- a/src/subdomains/supporting/log/log-job.service.ts +++ b/src/subdomains/supporting/log/log-job.service.ts @@ -453,13 +453,13 @@ export class LogJobService { const manualLiqPosition = manualLiqPositions.find((p) => p.assetId === curr.id)?.value ?? 0; - // plus (use availableAmount to avoid double-counting with pending exchange orders) - const liquidity = (curr.balance?.availableAmount ?? 0) + (paymentDepositBalance ?? 0) + (manualLiqPosition ?? 0); + // plus + const liquidity = (curr.balance?.amount ?? 0) + (paymentDepositBalance ?? 0) + (manualLiqPosition ?? 0); const cryptoInput = [Blockchain.MONERO, Blockchain.LIGHTNING, Blockchain.ZANO].includes(curr.blockchain) ? 0 : pendingPayIns.reduce((sum, tx) => sum + (tx.asset.id === curr.id ? tx.amount : 0), 0); - const exchangeOrder = pendingExchangeOrders.reduce((sum, tx) => { + const rawExchangeOrder = pendingExchangeOrders.reduce((sum, tx) => { if (tx.pipeline.rule.targetAsset.id !== curr.id) return sum; // for transfer/deposit: only count when action.system matches the target asset's exchange @@ -469,6 +469,13 @@ export class LogJobService { return sum + tx.inputAmount; }, 0); + + // Deduct locked exchange funds from exchangeOrder to avoid double-counting: + // amount includes locked funds (e.g. Scrypt pending withdrawals) that may also + // appear as exchangeOrder. For exchanges without such overlap (e.g. XT trading + // orders), lockedAmount > exchangeOrder so this just clamps to 0. + const lockedAmount = (curr.balance?.amount ?? 0) - (curr.balance?.availableAmount ?? curr.balance?.amount ?? 0); + const exchangeOrder = Math.max(0, rawExchangeOrder - lockedAmount); const bridgeOrder = pendingBridgeOrders.reduce( (sum, tx) => sum + (tx.pipeline.rule.targetAsset.id === curr.id ? tx.inputAmount : 0), 0, From f5fa8dd16918ab9f805a650077d2bf6ffd6a57c2 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Thu, 19 Mar 2026 21:20:40 +0100 Subject: [PATCH 4/4] feat: split Scrypt into Spot and Pending in liquidity chart (#3466) Separate Scrypt balance into "Scrypt Spot" (liquidity + custom) and "Scrypt Pending" (in-transit funds) so the financial dashboard shows actual exchange balance vs pending transfers. --- .../dashboard/dashboard-financial.service.ts | 32 ++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/src/subdomains/supporting/dashboard/dashboard-financial.service.ts b/src/subdomains/supporting/dashboard/dashboard-financial.service.ts index 3b5b6f622a..4099ddac96 100644 --- a/src/subdomains/supporting/dashboard/dashboard-financial.service.ts +++ b/src/subdomains/supporting/dashboard/dashboard-financial.service.ts @@ -138,12 +138,34 @@ export class DashboardFinancialService { const asset = assetMap.get(Number(idStr)); const blockchain = asset?.blockchain ?? 'Unknown'; const assetName = asset?.name ?? 'Unknown'; - const plusChf = (assetData.plusBalance?.total ?? 0) * assetData.priceChf; - if (!blockchainTotals[blockchain]) blockchainTotals[blockchain] = { plus: 0, assets: {} }; - blockchainTotals[blockchain].plus += plusChf; - blockchainTotals[blockchain].assets[assetName] = - (blockchainTotals[blockchain].assets[assetName] ?? 0) + Math.round(plusChf); + if ((blockchain as string) === 'Scrypt') { + const spotTotal = + (assetData.plusBalance?.liquidity?.total ?? 0) + (assetData.plusBalance?.custom?.total ?? 0); + const pendingTotal = assetData.plusBalance?.pending?.total ?? 0; + const spotChf = spotTotal * assetData.priceChf; + const pendingChf = pendingTotal * assetData.priceChf; + + if (spotChf > 0) { + if (!blockchainTotals['Scrypt Spot']) blockchainTotals['Scrypt Spot'] = { plus: 0, assets: {} }; + blockchainTotals['Scrypt Spot'].plus += spotChf; + blockchainTotals['Scrypt Spot'].assets[assetName] = + (blockchainTotals['Scrypt Spot'].assets[assetName] ?? 0) + Math.round(spotChf); + } + if (pendingChf > 0) { + if (!blockchainTotals['Scrypt Pending']) blockchainTotals['Scrypt Pending'] = { plus: 0, assets: {} }; + blockchainTotals['Scrypt Pending'].plus += pendingChf; + blockchainTotals['Scrypt Pending'].assets[assetName] = + (blockchainTotals['Scrypt Pending'].assets[assetName] ?? 0) + Math.round(pendingChf); + } + } else { + const plusChf = (assetData.plusBalance?.total ?? 0) * assetData.priceChf; + + if (!blockchainTotals[blockchain]) blockchainTotals[blockchain] = { plus: 0, assets: {} }; + blockchainTotals[blockchain].plus += plusChf; + blockchainTotals[blockchain].assets[assetName] = + (blockchainTotals[blockchain].assets[assetName] ?? 0) + Math.round(plusChf); + } } }