Skip to content

fix(log): match failed Scrypt deposits in financial log receiver set#3775

Closed
TaprootFreak wants to merge 1 commit into
developfrom
fix/financial-log-scrypt-deposit-match
Closed

fix(log): match failed Scrypt deposits in financial log receiver set#3775
TaprootFreak wants to merge 1 commit into
developfrom
fix/financial-log-scrypt-deposit-match

Conversation

@TaprootFreak
Copy link
Copy Markdown
Collaborator

Problem

Production FinancialDataLog has shown totalBalanceChf ~100k CHF too high since 2026-05-27 08:46 UTC. Root cause traced to the Scrypt DEPOSIT receiver-filter in log-job.service.ts:

// before
const eurReceiverScryptExchangeTx = recentScryptExchangeTx.filter(
  (k) => k.type === ExchangeTxType.DEPOSIT && k.status === 'ok' && k.currency === 'EUR',
);

Scrypt marks a deposit failed when its initial reconciliation attempt does not complete, even if the funds later arrive on the account (manual reconciliation at Scrypt, FIX BALANCE stream updates without flipping the deposit row back to ok). With status === 'ok', those failed rows are excluded from the receiver set, the matching bank_tx (e.g. OlkyPay → Scrypt SEPA) stays unmatched, and is kept in pending.toScrypt — while the same amount is already reflected in liquidity via Asset.balance.amount.

Concrete trace (observed)

bank_tx Sender IBAN Amount remittanceInfo matched exchange_tx status
200493 OlkyPay LU11606… 30'006.71 EUR DFX Payout 81090 138339 (DEPOSIT-81090) failed
200504 OlkyPay LU11606… 80'006.76 EUR DFX Payout 81111 138379 (DEPOSIT-81111) failed
200655 OlkyPay LU11606… 30'006.86 EUR DFX Payout 81147 138487 (DEPOSIT-81147) ok

The first two never match → pending.toScrypt = 110'013.47 EUR even though Scrypt's FIX stream reports the corresponding 110k EUR as live balance. At 0.9152 CHF/EUR that is 100'680.78 CHF doubled, matching the observed +100'900.59 CHF jump (residual ~220 CHF explained by USDT price drift across the comparison window).

Fix

  1. Change Scrypt DEPOSIT receiver filter from status === 'ok' to status !== 'pending' — accept both ok and failed. This mirrors the existing Kraken DEPOSIT filters (Lines 372 and 384), which were historically tightened to the same predicate in PR fix: exclude pending deposits from Kraken balance calculation #2929 / commit cd8afc0. After the change, all four cross-system DEPOSIT receiver filters use the same predicate.

  2. Extract the predicate into a public method isMatchableScryptDeposit(currency) so the production code path is directly testable.

  3. Add regression tests pinning the predicate semantics (failed → matchable, pending → excluded, for both EUR and CHF).

Trade-off

A failed deposit where the funds did not arrive (true reject) will now briefly disappear from pending.toScrypt until the corresponding bank_tx_return is recorded — pendingBankTxReturn then accounts for it via minusBalance.pending.bankTxReturn. Net: a short-lived under-count instead of a persistent over-count, which is preferable for the safety-mode threshold.

Risk

  • Withdrawal filters (status !== 'failed', Lines 429/437) are intentionally not changed — different semantics (a failed withdrawal never left the account).
  • Kraken filters already use status !== 'pending' — untouched.
  • No DB migrations, no entity changes, no API surface changes.

Test plan

  • npm run lint
  • npm run format:check
  • npx tsc --noEmit
  • npx jest --testPathPattern="log-job.service.spec" — 20/20 (17 existing + 3 new regression tests)
  • CI green
  • Verify on DEV after merge: re-run financial-log cron, confirm pending.toScrypt for assetId=401 no longer contains funds already reflected in liquidity
  • Production verification once deployed: totalBalanceChf should drop by ~100k CHF on the next cron run

The Scrypt DEPOSIT receiver filter narrowed to status === 'ok', which
left bank_tx unmatched when Scrypt marked a deposit as failed even
though the funds actually arrived (Scrypt FIX balance updated). The
unmatched bank_tx kept counting in pending.toScrypt while the same
amount also showed up in liquidity from the FIX stream — observed as
a ~100k CHF overstatement in totalBalanceChf.

Align the Scrypt filter with the existing Kraken filter (status !==
'pending', see PR #2929) and extract it into a public predicate so it
is directly testable.
@TaprootFreak
Copy link
Copy Markdown
Collaborator Author

Closing in favor of a cleaner approach.

Why

This PR loosened the financial-log receiver filter (status === 'ok'status !== 'pending') to work around the symptom that failed Scrypt deposits with funds-actually-arrived stayed unmatched.

The real cause is one layer up: ExchangeTxService.cleanupStalePendingDeposits overwrites Scrypt deposit status with 'failed' after 24h, regardless of whether the funds eventually arrive. Scrypt SEPA inbound has no historical chargebacks, so a stale pending row is a Scrypt-side reconciliation delay — not a true reject. We should not synthesize a failed status for Scrypt from inside our API.

The replacement PR removes that synthesis entirely (Scrypt is excluded from the cleanup, status comes solely from Scrypt-API via syncExchanges). Once that lands, the existing filter status === 'ok' is semantically correct again — a Scrypt deposit with status === 'failed' would then be a real Scrypt-side reject, which legitimately should not match.

→ Closing without merge. Branch left intact in case the discussion needs to be reopened.

@davidleomay
Copy link
Copy Markdown
Member

Superseded by #3778 which fixes the root cause: the 24h stale deposit cleanup was racing against Scrypt's reconciliation window (can take >24h). Scrypt deposits now get 7 days before cleanup, allowing the sync to pick up the completed version.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants