feat: reconcile chain and DB escrow state to repair drift - #184
Merged
meshackyaro merged 1 commit intoAug 16, 2026
Conversation
Adds a deterministic reconciler that diffs each DB-tracked escrow
against its on-chain counterpart (read via Soroban RPC contract data,
falling back to an in-memory simulated store when no contract is
configured, mirroring the IPFS pin providers' credential fallback) and
repairs status/amount drift by trusting chain as the source of truth.
A background worker runs it on an interval; a guarded REST endpoint
also triggers it on demand and can backfill DB rows for contract IDs
supplied explicitly, covering escrows created on-chain whose creation
event was missed.
Also fixes a latent collision in EscrowService's id generation
(esc-${Date.now()} could collide and silently overwrite a row when two
escrows were created in the same millisecond) using the same
timestamp+uuid pattern already used for migration run and dispute saga
ids elsewhere in this codebase.
Closes trustflow-protocol#166
meshackyaro
approved these changes
Aug 16, 2026
meshackyaro
left a comment
Contributor
There was a problem hiding this comment.
This PR is well-scoped, adds a deterministic reconciler (chain vs DB) with a worker, controller, Soroban chain client (with a simulated mode), tests, and small EscrowService API additions. Tests are thorough and CI-local commands in the description look good.
Again, this is a well-implemented feature with strong tests and good documentation in code/comments. Great job @CillaSam!
10 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Implements a deterministic reconciler that periodically diffs on-chain escrow state against the DB and repairs drift caused by missed events or partial writes, as requested in the issue.
Closes #166
Type of Change
Component
Changes Made
escrow-reconciliationmodule:EscrowReconciliationServicediffs every DB escrow linked to acontractEscrowIdagainst its on-chain state (status + amount), repairing drift in a single write per escrow by trusting chain as the source of truth.EscrowChainStateClientabstraction +SorobanEscrowChainStateClientimplementation, reading contract storage via Soroban RPCgetContractData. Falls back to an in-memory simulated store whenTRUSTFLOW_CONTRACT_IDis unset (the default in local dev/tests/CI), mirroring how the IPFS pin providers degrade without credentials — keeps the reconciler fully testable without network access.EscrowReconciliationWorkerServiceruns the sweep on an interval (ESCROW_RECONCILIATION_SWEEP_INTERVAL_MS, default 10 min;<= 0disables it), following the same lifecycle pattern asGigExpiryWorkerService.EscrowReconciliationController(JWT-guarded) exposesPOST /escrow-reconciliation/run,GET /escrow-reconciliation/runs, andGET /escrow-reconciliation/runs/:runId, with Swagger docs.contractEscrowIds a caller supplies explicitly (e.g. an ops backfill list) — passed via the run endpoint'scontractEscrowIdsbody field. Found records are backfilled into the DB automatically.EscrowServicewithfindAll,findByContractEscrowId,linkContractEscrowId,applyChainState(the reconciler's escape hatch for writing chain-verified state, bypassing therelease()/raiseDispute()transition guards by design), andcreateFromChainState.EscrowService.create():esc-${Date.now()}could silently overwrite an existing row if two escrows were created within the same millisecond. Now uses the sametimestamp-uuidpattern already used for migration run ids and dispute saga ids elsewhere in this codebase.Architectural decision (documented per the issue's task list)
There's no contract source/ABI in this repo to verify against, so the Soroban client assumes escrows are stored as persistent contract-data entries keyed by
[symbol("Escrow"), string(contractEscrowId)], mirroring the event topic shapeevent-processor.service.tsalready parses. If the deployed contract's actual storage layout differs, onlysoroban-escrow-chain-state.client.tsneeds to change — the rest of the reconciler depends solely on theEscrowChainStateClientinterface.Testing
Automated Testing
Added specs for the new service, worker, controller, chain client (both simulated and mocked-RPC paths), and the new
EscrowServicemethods — 281 tests passing across 31 suites.Manual Testing
npm run lint:check,npm run format:check,npx tsc --noEmit,npm run test:ci,npm run buildall pass clean.Checklist
Additional Notes
EscrowServiceis still an in-memory store (as is the rest of this backend), so "DB" here means the same in-memoryMapthe other modules already use — the reconciliation logic and DI boundaries are written so a real DB-backedEscrowServiceand a real deployed contract could be swapped in without touching the reconciler itself.