Repository navigation
fix(indexer): write milestone releasedAmount when a dispute resolves - #214
Merged
oluwaseyi1996-netizen merged 3 commits intoOct 22, 2026
Conversation
handleResolved flipped every milestone to RELEASED but only wrote status and releasedAt, leaving releasedAmount at its 0 default. The contract sets each on-chain milestone's released_amount to its full amount in escrow.resolve, so the mirror disagreed with the chain and receipt/earnings aggregates read a resolved escrow as having paid out nothing. Update each milestone with its own amount (converted through the job asset's decimals) and add a data migration that repairs rows already indexed before this fix. Covers resolved and dispute_resolved events. Closes vatal-system#168 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
main was red before this branch: receipt fixtures predate the required releasedAmount field, session-aware requireAuth queries prisma.user in route tests whose mocks omit it, and the logout/by-escrow routes were never registered in the OpenAPI document. Update the fixtures and route mocks so authentication can resolve the caller's session without hiding the handler behaviour each test checks. Document both missing routes so the coverage test passes again. 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
WalletProvider kept an unused import and catch binding, and the new auto-scroll and localStorage draft features broke their tests in jsdom: scrollIntoView is not implemented there, and a persisted draft leaked between PostJob cases. Remove the dead symbols and reset the jsdom/localStorage state each test so the suite exercises a blank page, as it did before the drafts. 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
|
@preciousolajide09 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
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.
Summary
handleResolvedsettled the job and dispute for aresolved/dispute_resolvedevent, and flipped every milestone toRELEASED, but never wroteMilestone.releasedAmount. Since that column defaults to0, a resolved escrow was stored as "COMPLETED, all milestones released, but nothing paid out". This PR makes the indexer mirror the contract exactly and backfills the rows that were already indexed with the old handler.Closes #168
Root cause
contracts/escrow/src/lib.rs→resolve()marks every milestone before distributing the shares:The indexer only replayed the two visible side effects:
updateManycannot setreleasedAmountto each row's ownamount, so the column stayed at its0default.handleMilestoneReleasedalready writes the full total for a normal release, which is why only arbitrated/dispute-resolved escrows were wrong.This is not cosmetic: the escrow receipt (
backend/src/lib/receipt.ts) sumsmilestone.releasedAmount, so a resolved job's receipt showedReleased: 0and its full value as still outstanding, and the same column feeds earnings/payment-history/audit projections.Changes
1. Indexer (
backend/src/indexer.ts)After marking the job
COMPLETEDand its disputeRESOLVED,handleResolvednow loads the job with its milestones and updates each milestone with:status: "RELEASED"releasedAt: resolvedAt— the same instant for the job, dispute and every milestone, because the contract changes them in one transactionreleasedAmount: formatAmount(toBaseUnits(milestone.amount, decimals), decimals)— the milestone's full amount in the job asset's human unitsThe per-row update is deliberate:
releasedAmountis the row's ownamount, which a singleupdateManycannot express. The conversion matcheshandleMilestoneReleased, so both release paths write the same format and decimal scale.This applies to both event names that dispatch to the handler (
resolvedanddispute_resolved).2. Data repair migration
backend/prisma/migrations/20261021130000_backfill_resolved_milestone_released_amount/migration.sqlThe indexer cursor has already moved past those events, so fixing the handler alone would leave every previously resolved escrow wrong forever. The migration repairs them:
Milestone.status = 'RELEASED'on aCOMPLETEDjob that has aRESOLVEDdispute — exactly the pathescrow.resolvehandles;releasedAmount = amountwhen the two differ, which also repairs a milestone that had a partial release recorded before the dispute froze the escrow (on-chainresolveoverwrites it with the full amount);handleMilestoneReleased;The migration was applied to a real Postgres 16 database through
prisma migrate deploy, and the backfill was exercised against seeded data: a resolved milestone with0, a resolved milestone with a partial4, and a normally completed milestone with0. The first two became10; the normal one stayed0.3. Regression tests (
backend/src/__tests__/indexer.test.ts)resolvedtest now asserts both milestones end atreleasedAmount: "0.5"as well asRELEASEDand the sharedreleasedAt.releasedAmount: "4"of"10") is overwritten with the full"10"on resolution.dispute_resolvedtest assertsreleasedAmount: "0.5", proving both event names take the same path.4. CI restoration on top of
mainmainwas already red before this branch, so the PR also restores the checks the issue requires:releasedAmountfield.requireAuthnow readsprisma.user(anduser.sessions) on every authenticated request, but route-test mocks either omitteduseror returned a row withoutsessions, so ~100 tests saw401before reaching the handler under test. Mocks now distinguish the auth lookup from handler lookups (and still exercise the original 404/403/assignment branches).POST /api/users/logoutandGET /api/jobs/by-escrow/{escrowId}were registered as routes but not in the document, so the coverage test failed.data:URL, which the newhttp(s)-only validator correctly rejects; it now uses anhttpsURL.ACTIVE_NETWORKimport and an unused catch binding.scrollIntoView(the new auto-scroll effect threw), and the new localStorage draft leaked state betweenPostJobcases; the tests now stub scrolling and clear storage.No handler assertions were removed or weakened.
5. Docs
docs/ARCHITECTURE.mdevent table:resolved/dispute_resolvednow documents that every milestone is released in full.docs/EVENTS.md:escrow.resolveemits onlyresolved, and the indexer writes the full amount to each milestone.Verification
All CI-equivalent checks were run locally.
cd backend && npx tsc --noEmitcd backend && npx vitest runnpx prisma migrate deployagainst Postgres 16npx prisma migrate diff --from-url "$DATABASE_URL" --to-schema-datamodel prisma/schema.prisma --exit-codecd frontend && npm run lintnpx tsc --noEmitnpm testnpm run buildcd contracts && cargo fmt --all -- --checkcargo clippy --all-targets -- -D warningscargo teststellar contract build+ 64 KB gzip checkNote:
backend/package.jsonalso defines alintscript, but it is not part of the CI workflow and currently reports pre-existingno-explicit-any/ unused-symbol findings across older tests and source files. Those are unrelated to this change and were left out of scope; the CI backend job runs typecheck and tests, both of which pass.Risk / rollout
resolved/dispute_resolved, and is idempotent on replay.UPDATEscoped by status and the resolved-dispute relation; it updates no rows on a fresh database and does not change the schema.Checklist
resolvedanddispute_resolvedcargo fmt/cargo clippy -D warnings/cargo testfrontend: lint, typecheck, tests, buildbackend: generate, typecheck, tests, migrations + drift checkdocs/ARCHITECTURE.mdanddocs/EVENTS.md