Repository navigation
LedgerReconciliationService never fetches on-chain data: reconciliation flags every stored entry missing #427
Description
Problem
The "ledger reconciliation" service compares on-chain data against the database but never fetches any on-chain data. LedgerReconciliationService.fetchOnChainData is a placeholder that returns an empty list, so the comparison is structurally vacuous:
// app/backend/src/onchain/ledger-reconciliation.service.ts
private fetchOnChainData(_startLedger: number, _endLedger: number): OnChainLedgerEntry[] {
// Placeholder for actual Horizon API call
// In production, this would query the Stellar Horizon API
return [];
}In processReconciliation, the forward loop over onChainData therefore never runs, and the reverse loop flags every stored BalanceLedger row as a discrepancy with ledger: -1:
for (const storedEntry of storedEntries) {
const onChainEntry = onChainData.find(e => e.id === storedEntry.id); // always undefined
if (!onChainEntry) {
discrepancies.push({ ledger: -1, type: 'missing', expected: null, observed: storedEntry, severity: 'medium' });
}
}Consequence: triggerReconciliation reports a completed job whose discrepancies are a false-positive "missing" row for every DB ledger entry and whose on-chain side was never examined. The feature advertises a trust boundary — "compare on-chain vs stored" — but can neither detect a genuinely missing on-chain entry, an amount mismatch, nor an event-type mismatch. Operators acting on its actionable flag act on noise. Additionally, the amount comparison that would run uses storedEntry.amount.toNumber(), a lossy float conversion for the i128-scale token amounts the contract uses.
Root cause
The service was scaffolded with a simulated data source and shipped before the Horizon/tx-source integration was implemented; the reverse-loop guard makes the missing source produce misleading output instead of an explicit "unimplemented" failure.
Why this is architecturally hard
- The on-chain source is the whole point. A real implementation must page Stellar/Soroban ledger data (Horizon or the RPC's transaction/ledger history) and correlate it with
BalanceLedgerrows, including deciding what constitutes a ledger "entry" for a smart-contract platform whose events (package_created, etc.) are not Horizon balance rows. - The comparison contract is ill-defined.
OnChainLedgerEntryhasid,ledger,amount,eventType, butBalanceLedgerstores different fields; the fix must define the join key and the exact equivalence foramount(raw integer units, nottoNumber()floats). - Failure must be loud, not "completed". If the source is unavailable, the job must surface
failed/degradedwith a clear error rather than a completed report; otherwise the same silent-confidence bug recurs. - It must reuse existing infra. The
onchainBullMQ queue andOnchainProcessoralready exist, andSorobanAdapter.getTransactionStatus/the event stream are the likely data source — the design must integrate with them rather than add a parallel Horizon client.
Proposed design
Replace fetchOnChainData with a real, pageable source (RPC/Horizon) returning the events/entries within [startLedger, endLedger], define the join key, and treat a source failure as a failed job. Use integer/BigInt comparison for amounts. Keep the discrepancy classification (missing/amount_mismatch/count_mismatch) but only emit ledger: -1 when genuinely unknown, and never fabricate a completed summary when the source returned nothing.
Acceptance criteria
Service
-
processReconciliationreads real on-chain data for the requested ledger range; a source error fails the job rather than returning a completed report. - With on-chain data present, a stored row with no on-chain counterpart produces a real
missingdiscrepancy with the correct ledger, and an amount difference abovethresholdPercentproduces anamount_mismatchusing exact integer units. - With zero on-chain entries available, the job does not flag every stored row as
missingwithledger: -1.
Tests
- Unit tests inject a fake on-chain source and assert each discrepancy type and the
actionablecomputation.
Out of scope
Replacing ioredis-mock with testcontainers and the BalanceLedger Float→Decimal work are separate, already-tracked concerns.
Getting started
Files: app/backend/src/onchain/ledger-reconciliation.service.ts, app/backend/src/onchain/onchain.processor.ts, app/backend/src/onchain/ledger-backfill.service.ts.
cd app/backend
npm testGood first files to read: ledger-reconciliation.service.ts (the current, vacuous flow) and onchain.processor.ts (the queue job that would invoke it).
Activity
ugoocreates-pixel commentedon Aug 19, 2026
Hello, I’ve gone through the requirements and I’m confident I can implement a proper solution that matches the acceptance criteria. Kindly assign this issue to me.
snowrugar-beep commentedon Aug 20, 2026
Please assign me this issue, I have 7 years of experience, Thanks.
grantfox-oss commentedon Aug 20, 2026
🦊 GrantFox — @snowrugar-beep has been assigned to this issue as part of the Third Campaign campaign!
Next steps:
Closes #427)Good luck! Track your progress on GrantFox.
grantfox-oss commentedon Aug 20, 2026
🎉 This issue has been marked as completed on GrantFox as part of the Third Campaign campaign!
@snowrugar-beep's PR #451 was approved and merged by @kilodesodiq-arch.
🏆 @snowrugar-beep: You earned 40 FoxPoints for this contribution! Your current tier: Builder (1,185 total points). Track your full progress on GrantFox.
👏 Great work, @snowrugar-beep! Keep contributing to ChainForgee.