Skip to content

fix(escrow): waterfall-distribute funds among collaborators on escrow release#273

Open
macan88 wants to merge 2 commits intodavedumto:mainfrom
macan88:felix/fix-269
Open

fix(escrow): waterfall-distribute funds among collaborators on escrow release#273
macan88 wants to merge 2 commits intodavedumto:mainfrom
macan88:felix/fix-269

Conversation

@macan88
Copy link

@macan88 macan88 commented Feb 26, 2026

Problem

When an invoice with revenue-split collaborators had its escrow released, the full escrow amount was sent directly to the freelancer's wallet, completely bypassing collaborator shares. This caused conflicting Stellar transactions, left funds stuck in escrow, and broke the revenue-split contract between the freelancer and their sub-contractors.

Root Cause

The escrow release route (app/api/routes-d/escrow/release/route.ts) queried the invoice without include: { collaborators: true }, so collaborator data was never available. Even if it had been fetched, the code unconditionally called sendStellarPayment once with 100% of the escrow amount to the invoice owner — there was no waterfall logic at all.

Solution

  1. Include collaborators in the initial query — added collaborators: { select: { id, email, walletAddress, revenueSharePercent } } to the findUnique call so split data is always present.
  2. Pre-flight validation before any state mutation — verify every collaborator has a walletAddress and a valid revenueSharePercent, and that the combined share is < 100%. This is checked before the on-chain escrow release and before the DB transaction, so a bad configuration cannot leave the system in a partially-released state.
  3. Waterfall arithmetic — collaborator amounts are computed as floor(percent/100 * total * 1e7) / 1e7 (Stellar's 7-decimal precision). The freelancer receives the exact remainder, ensuring no dust is lost.
  4. Collaborator payments dispatched first — contractually, collaborators are paid from the top of the waterfall before the freelancer receives their net share.
  5. Audit events logged inside the DB transaction — each queued payment is recorded as an escrowEvent row atomically with the status flip so operations can replay any Stellar broadcast failure without re-releasing the escrow.

Edge Cases Handled

  • Collaborator wallet missing: 422 returned before the escrow contract or DB are touched — no partial release.
  • Invalid/zero revenue share percent: validated and rejected with a clear error message during pre-flight.
  • Combined splits ≥ 100%: would produce a zero or negative freelancer payment — caught and rejected before any mutation.
  • No collaborators: code path is identical to the original behaviour — a single Stellar payment of 100% to the freelancer.
  • Double-release race condition: the existing optimistic-concurrency updateMany guard (count !== 1 → throw ESCROW_RELEASE_CONFLICT) is preserved and still protects against concurrent requests.
  • Stellar broadcast failure after DB commit: each payment attempt is wrapped in try/catch; failures are logged and surfaced in the response as paymentErrors so the caller and monitoring systems can react without needing server log access. The queued escrowEvent rows enable manual replay.

Testing

  • Added test: no collaborators → single 100% payment to freelancer — verifies the happy path is not regressed.
  • Added test: 30% collaborator split → $300 to collaborator, $700 to freelancer — verifies the waterfall arithmetic and payment ordering.
  • Added test: collaborator missing wallet → 422, zero DB/Stellar side-effects — verifies pre-flight guard works.
  • Added test: combined shares ≥ 100% → 422, zero DB/Stellar side-effects — verifies the over-allocation guard.
  • Verified: existing escrow release conflict (409) handling is preserved.

Closes #269

@vercel
Copy link

vercel bot commented Feb 26, 2026

@macan88 is attempting to deploy a commit to the david's projects Team on Vercel.

A member of the Team first needs to authorize it.

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.

[BUG] Escrow release fails when invoice has collaborators (waterfall conflict)

1 participant