Skip to content

fix(exchange-tx): exclude Scrypt deposits from stale-pending cleanup#3779

Closed
TaprootFreak wants to merge 1 commit into
developfrom
fix/exclude-scrypt-from-stale-deposit-cleanup
Closed

fix(exchange-tx): exclude Scrypt deposits from stale-pending cleanup#3779
TaprootFreak wants to merge 1 commit into
developfrom
fix/exclude-scrypt-from-stale-deposit-cleanup

Conversation

@TaprootFreak
Copy link
Copy Markdown
Collaborator

Problem

ExchangeTxService.cleanupStalePendingDeposits is an hourly cron that flips every DEPOSIT with status='pending' older than one day to 'failed' — exchange-agnostically. For Scrypt this synthesises a status our own DB has no authority over.

Scrypt deposit semantics:

  1. Scrypt API creates a deposit row with status='pending' when the SEPA inbound is announced.
  2. The actual funds can arrive days later (DB evidence: exchange_tx#138339 was created 22.05 07:49, our cron flipped it to 'failed' on 23.05 08:00, but the corresponding 30'006.71 EUR only landed in Asset.balance.amount (Scrypt FIX-stream BALANCE) on 27.05 08:46).
  3. Scrypt SEPA inbound has no chargebacks in our entire history — there is no bank_tx_return row that points at a Scrypt-typed bank_tx.
  4. The authoritative status for Scrypt rows comes from Scrypt itself via syncExchanges (exchange-tx.service.ts:63). When Scrypt confirms receipt, it flips its own row to 'ok'; our Object.assign(entity, transaction) mirrors that on the next sync tick.

Our synthetic 'failed' was therefore breaking the financial-log matching: the OlkyPay bank_tx (200493 / 200504, refs 81090 / 81111) could not pair with their failed Scrypt counterparts (138339 / 138379), so 110'013.47 EUR kept counting in pending.toScrypt even though the FIX stream already reflected the funds in Asset.balance.amount — directly responsible for the ~100k CHF overstatement in totalBalanceChf since 2026-05-27 08:46 UTC.

Fix

Add exchange: Not(ExchangeName.SCRYPT) to the find query so the cleanup leaves Scrypt rows alone:

where: {
  type: ExchangeTxType.DEPOSIT,
  status: 'pending',
  created: LessThan(Util.daysBefore(1)),
  // Scrypt deposit status is owned by Scrypt — funds can arrive days after creation, so don't synthesize 'failed' here.
  exchange: Not(ExchangeName.SCRYPT),
},

That is the entire production change. Other exchanges (Kraken, Binance, MEXC, XT) keep their existing 'failed'-after-24h default, which is appropriate for them — they do produce real rejects.

Why this is the right place to fix it

Two earlier draft PRs explored aggregator-side workarounds (#3775 loosened the financial-log receiver filter; #3777 flipped stale Scrypt rows to 'ok'). Both were closed because they synthesised status from inside our API instead of letting Scrypt own its own truth.

After this PR:

  • A Scrypt deposit with status='pending' means "Scrypt announced the inbound, money not confirmed received yet" — the matching bank_tx correctly stays in pending.toScrypt.
  • A Scrypt deposit with status='ok' means "Scrypt confirmed receipt" — the bank_tx correctly leaves pending.toScrypt.
  • A Scrypt deposit with status='failed' means "Scrypt actively rejected" (in practice never observed, but legitimate) — the bank_tx correctly stays in pending.toScrypt (and a bank_tx_return would eventually neutralise it).

In all three cases the existing financial-log filter status === 'ok' (log-job.service.ts:393, 423) is semantically correct — no aggregator-side change needed.

Risk

  • Other exchanges unaffected.
  • No schema migration, no entity changes, no API surface changes.
  • Existing historically-mis-set 'failed' Scrypt rows (#138339, #138379, plus ~10 older ones from May) will NOT auto-correct: syncExchanges only re-fetches rows whose anchor is status='pending' (getSyncSinceDate, L209-L223). A one-time backfill is needed for those — out of scope for this PR; tracked separately.

Test plan

  • npm run lint
  • npm run format:check
  • npx tsc --noEmit
  • npx jest --testPathPattern="exchange-tx.service.spec" — 4/4 (new spec file: find-query excludes Scrypt; non-Scrypt set to failed; early-return on empty; should be defined)
  • CI green
  • DEV verification after merge: trigger the cron, confirm Scrypt deposits stay pending while Kraken/Binance go to failed
  • Production verification: existing wrong-failed rows (#138339, #138379, …) need a one-time backfill — separate operation

The hourly cleanupStalePendingDeposits cron flips every DEPOSIT row that
stays pending for more than a day to 'failed' — exchange-agnostically.
For Scrypt that synthesises a status our own DB has no authority over:
Scrypt SEPA inbound funds historically can arrive days after the
exchange_tx is created (no chargebacks observed), and any status change
must come from Scrypt itself via syncExchanges.

A wrong 'failed' here left the matching OlkyPay/Yapeal bank_tx unmatched
in pending.toScrypt while the actual funds were already reflected in
Asset.balance.amount via the Scrypt FIX stream — observed as the ~100k
CHF overstatement in totalBalanceChf on 2026-05-27.

Add `exchange: Not(ExchangeName.SCRYPT)` to the find query. Other
exchanges (Kraken, Binance, MEXC, XT) keep the existing 'failed'-after-
24h default.
@davidleomay
Copy link
Copy Markdown
Member

Superseded by #3778 — the root cause was that Scrypt deposit requests were sent at bank transmission time, before money arrived. Moving the notification to after bank_tx confirmation eliminates the pending window entirely, making the cleanup cron unnecessary.

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