Context
This is the follow-up to the Trustless Work initialize-and-fund issue. Once an Escrow is FUNDED against a Project, VolunChain needs:
- A way to break the project into milestones (volunteer hour targets, deliverables).
- An approval flow that releases the milestone's portion of the escrow to the assigned volunteer.
- A dispute path when the volunteer believes work was completed but the organization refuses approval.
- A webhook receiver so on-chain state changes (release confirmed, dispute opened, dispute resolved) are reflected back into the local DB without polling.
This depends on the new Escrow model and TrustlessWorkClient introduced in the predecessor issue.
Proposed Solution
1. New Milestone Prisma model
model Milestone {
id String @id @default(uuid())
escrowId String
escrow Escrow @relation(fields: [escrowId], references: [id])
title String
description String?
amount Decimal @db.Decimal(20, 7)
order Int
status MilestoneStatus @default(PENDING)
approvedAt DateTime?
releaseTxHash String?
disputedAt DateTime?
resolvedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([escrowId, order])
}
enum MilestoneStatus {
PENDING
APPROVED
RELEASED
DISPUTED
RESOLVED_FOR_RECEIVER
RESOLVED_FOR_ISSUER
}
The sum of all Milestone.amount for a given Escrow must equal Escrow.amount — enforce in the use case, not the DB.
2. Extend TrustlessWorkClient
Add typed methods that wrap the corresponding TW endpoints:
approveMilestone(engagementId, milestoneIndex, signedApproval) → returns { releaseTxHash }.
disputeEscrow(engagementId, signedDispute).
resolveDispute(engagementId, decision, signedResolution) where decision ∈ { 'receiver', 'issuer', 'split' }.
All methods stay symmetric with the initialize/fund pattern: backend builds and returns the unsigned XDR; the caller (org / volunteer / platform) signs and submits the signed XDR back.
3. New endpoints
| Method |
Path |
Auth |
Purpose |
POST |
/projects/:projectId/milestones |
org owner |
Create the full milestone list (one shot, before any approval). |
GET |
/projects/:projectId/milestones |
any authed |
List milestones with status. |
POST |
/milestones/:id/approve/prepare |
org owner |
Build unsigned approval XDR. |
POST |
/milestones/:id/approve/submit |
org owner |
Submit signed XDR; transition to RELEASED. |
POST |
/escrows/:id/dispute/prepare |
assigned volunteer |
Build unsigned dispute XDR. |
POST |
/escrows/:id/dispute/submit |
assigned volunteer |
Submit signed XDR; status DISPUTED. |
POST |
/escrows/:id/dispute/resolve |
platform-only role |
Submit a platform-signed resolution. |
The split into prepare / submit keeps signing client-side and avoids the backend ever holding a signing key for org or volunteer wallets. The platform's resolver key may be held server-side and signed in dispute/resolve.
4. Webhook receiver
POST /webhooks/trustless-work
- Verifies the request via
X-Trustless-Work-Signature HMAC against TRUSTLESS_WORK_WEBHOOK_SECRET.
- Idempotent on TW event ID (store last-seen IDs in Redis with TTL).
- Updates the corresponding
Escrow and/or Milestone rows based on event type:
escrow.funded → Escrow.status = FUNDED, fundingTxHash.
milestone.released → Milestone.status = RELEASED, releaseTxHash.
escrow.disputed → Escrow.status = DISPUTED.
dispute.resolved → Escrow.status = RESOLVED, milestone status set per decision.
- Logs every received event through the project's logger.
5. Authorization rules
- Org owner: can create milestones, approve.
- Assigned volunteer: can dispute. Determined via
Volunteer / UserVolunteer join from the related Project.
- Platform-only: dispute resolution. Gate via a new
PLATFORM role check in authMiddleware.
Add explicit tests for each negative case (volunteer cannot approve, org cannot dispute, random user cannot do anything).
6. Tests
- Unit tests for the new
TrustlessWorkClient methods (mocked axios).
- Use-case tests for milestone creation invariants (amounts sum, ordering).
- Webhook handler tests: valid signature, invalid signature, replayed event ID (must no-op), unknown event type (must 200 + log).
- Authorization matrix integration test.
Acceptance Criteria
Out of Scope
- A user-facing dispute review dashboard (frontend concern).
- Multi-currency / asset conversion.
- Slashing / reputation effects of disputes.
Suggested Labels
enhancement, feature, stellar, trustless-work
Context
This is the follow-up to the Trustless Work initialize-and-fund issue. Once an
EscrowisFUNDEDagainst aProject, VolunChain needs:This depends on the new
Escrowmodel andTrustlessWorkClientintroduced in the predecessor issue.Proposed Solution
1. New
MilestonePrisma modelThe sum of all
Milestone.amountfor a givenEscrowmust equalEscrow.amount— enforce in the use case, not the DB.2. Extend
TrustlessWorkClientAdd typed methods that wrap the corresponding TW endpoints:
approveMilestone(engagementId, milestoneIndex, signedApproval)→ returns{ releaseTxHash }.disputeEscrow(engagementId, signedDispute).resolveDispute(engagementId, decision, signedResolution)wheredecision ∈ { 'receiver', 'issuer', 'split' }.All methods stay symmetric with the initialize/fund pattern: backend builds and returns the unsigned XDR; the caller (org / volunteer / platform) signs and submits the signed XDR back.
3. New endpoints
POST/projects/:projectId/milestonesGET/projects/:projectId/milestonesPOST/milestones/:id/approve/preparePOST/milestones/:id/approve/submitRELEASED.POST/escrows/:id/dispute/preparePOST/escrows/:id/dispute/submitDISPUTED.POST/escrows/:id/dispute/resolveThe split into
prepare/submitkeeps signing client-side and avoids the backend ever holding a signing key for org or volunteer wallets. The platform's resolver key may be held server-side and signed indispute/resolve.4. Webhook receiver
POST /webhooks/trustless-workX-Trustless-Work-SignatureHMAC againstTRUSTLESS_WORK_WEBHOOK_SECRET.Escrowand/orMilestonerows based on event type:escrow.funded→Escrow.status = FUNDED,fundingTxHash.milestone.released→Milestone.status = RELEASED,releaseTxHash.escrow.disputed→Escrow.status = DISPUTED.dispute.resolved→Escrow.status = RESOLVED, milestone status set per decision.5. Authorization rules
Volunteer/UserVolunteerjoin from the relatedProject.PLATFORMrole check inauthMiddleware.Add explicit tests for each negative case (volunteer cannot approve, org cannot dispute, random user cannot do anything).
6. Tests
TrustlessWorkClientmethods (mocked axios).Acceptance Criteria
Milestonemodel +MilestoneStatusenum migrated.TrustlessWorkClientextended withapproveMilestone,disputeEscrow,resolveDisputeand unit-tested.prepare/submitendpoints implemented; backend never signs on behalf of org or volunteer.POST /webhooks/trustless-workvalidates HMAC, is idempotent, and updates DB state correctly for every supported event type.TRUSTLESS_WORK_WEBHOOK_SECRETdocumented in.env.exampleandreadme.md.Milestoneschema.Out of Scope
Suggested Labels
enhancement,feature,stellar,trustless-work