Context
VolunChain connects organizations with volunteers, and rewarded projects need to lock funds upfront so volunteers trust they'll be paid on completion. Trustless Work provides a Stellar/Soroban smart-escrow REST API that handles the on-chain escrow lifecycle (initialize → fund → approve milestone → release / dispute). This issue covers the initialize + fund half of the integration; the approve / release / dispute half is filed as a follow-up.
The repo already has a stub Prisma model that is unused and not aligned with TW's data shape:
// prisma/schema.prisma:122
model escrows {
id Int @id @default(autoincrement())
user_id Int
amount Decimal @db.Decimal(10, 2)
status String @db.VarChar(50)
created_at DateTime? @default(now()) @db.Timestamp(6)
users users @relation(fields: [user_id], references: [id])
}
It needs to be replaced with a model that captures the on-chain escrow identity (Soroban contract ID), TW engagement ID, parties (issuer / receiver / approver / dispute resolver / platform), asset, amount, status, and a link to Project.
Proposed Solution
1. New module: src/modules/escrow/
Follow the same DDD-ish layout used by src/modules/photo/ and src/modules/volunteer/:
src/modules/escrow/
domain/
escrow.entity.ts // Escrow aggregate
escrow-status.enum.ts // PENDING | FUNDED | RELEASED | DISPUTED | RESOLVED | CANCELLED
application/
initialize-escrow.use-case.ts
fund-escrow.use-case.ts
infrastructure/
trustless-work.client.ts // thin REST client around TW API
escrow.repository.ts // Prisma-backed
presentation/
escrow.controller.ts
escrow.routes.ts
2. Replace the stub Prisma model
Migrate escrows → a proper Escrow model:
model Escrow {
id String @id @default(uuid())
projectId String @unique
project Project @relation(fields: [projectId], references: [id])
engagementId String @unique // Trustless Work engagement ID
contractId String? @unique // Soroban contract ID once deployed
asset String // e.g. "USDC:GA5Z..."
amount Decimal @db.Decimal(20, 7)
issuerAddress String // org's Stellar address
receiverAddress String // volunteer's Stellar address (set on assignment)
approverAddress String // who can approve milestones (defaults to issuer)
disputeResolver String // platform-controlled
platformAddress String
status EscrowStatus @default(PENDING)
fundingTxHash String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
enum EscrowStatus {
PENDING
FUNDED
RELEASED
DISPUTED
RESOLVED
CANCELLED
}
Provide a Prisma migration that drops the legacy escrows table (it has no production data) and creates the new one. Update User / Project relations accordingly.
3. Trustless Work client
src/modules/escrow/infrastructure/trustless-work.client.ts should:
- Read
TRUSTLESS_WORK_API_URL, TRUSTLESS_WORK_API_KEY, STELLAR_NETWORK (testnet | mainnet), PLATFORM_STELLAR_ADDRESS from env.
- Expose typed methods:
initializeEscrow(params) → calls TW deploy/initialize endpoint, returns { engagementId, contractId, unsignedTx }.
fundEscrow(engagementId, signedTx) → submits the funding transaction.
- Use
axios (already a dependency) with a 10s timeout and a single retry on 5xx.
- All errors wrapped in a typed
TrustlessWorkError.
4. New endpoint
POST /projects/:projectId/escrow
- Auth: organization that owns the project.
- Body:
{ asset, amount, approverAddress?, disputeResolver? }.
- Behavior: validates project belongs to org → calls
initializeEscrow → persists Escrow row with status = PENDING and engagementId → returns the unsigned funding transaction XDR.
- A second call
POST /projects/:projectId/escrow/fund accepts the signed XDR, calls fundEscrow, transitions the row to FUNDED, stores fundingTxHash.
5. Tests
- Unit tests for
TrustlessWorkClient with axios mocked: success, 4xx (auth), 5xx (retry then fail).
- Use-case tests: cannot initialize when an escrow already exists for that project; cannot fund a PENDING escrow that does not belong to the caller.
- Integration test against a TW testnet sandbox key (gated behind
TRUSTLESS_WORK_E2E=1 env so CI does not require it by default).
Acceptance Criteria
Out of Scope
- Approve / release / dispute flows — covered in the follow-up issue.
- A frontend signing UI — backend returns unsigned XDR; the client signs.
- Asset onboarding / trustline management for receivers (assume volunteers already have the trustline).
Suggested Labels
enhancement, feature, stellar, trustless-work
Context
VolunChain connects organizations with volunteers, and rewarded projects need to lock funds upfront so volunteers trust they'll be paid on completion. Trustless Work provides a Stellar/Soroban smart-escrow REST API that handles the on-chain escrow lifecycle (initialize → fund → approve milestone → release / dispute). This issue covers the initialize + fund half of the integration; the approve / release / dispute half is filed as a follow-up.
The repo already has a stub Prisma model that is unused and not aligned with TW's data shape:
It needs to be replaced with a model that captures the on-chain escrow identity (Soroban contract ID), TW engagement ID, parties (issuer / receiver / approver / dispute resolver / platform), asset, amount, status, and a link to
Project.Proposed Solution
1. New module:
src/modules/escrow/Follow the same DDD-ish layout used by
src/modules/photo/andsrc/modules/volunteer/:2. Replace the stub Prisma model
Migrate
escrows→ a properEscrowmodel:Provide a Prisma migration that drops the legacy
escrowstable (it has no production data) and creates the new one. UpdateUser/Projectrelations accordingly.3. Trustless Work client
src/modules/escrow/infrastructure/trustless-work.client.tsshould:TRUSTLESS_WORK_API_URL,TRUSTLESS_WORK_API_KEY,STELLAR_NETWORK(testnet|mainnet),PLATFORM_STELLAR_ADDRESSfrom env.initializeEscrow(params)→ calls TW deploy/initialize endpoint, returns{ engagementId, contractId, unsignedTx }.fundEscrow(engagementId, signedTx)→ submits the funding transaction.axios(already a dependency) with a 10s timeout and a single retry on 5xx.TrustlessWorkError.4. New endpoint
POST /projects/:projectId/escrow{ asset, amount, approverAddress?, disputeResolver? }.initializeEscrow→ persistsEscrowrow withstatus = PENDINGandengagementId→ returns the unsigned funding transaction XDR.POST /projects/:projectId/escrow/fundaccepts the signed XDR, callsfundEscrow, transitions the row toFUNDED, storesfundingTxHash.5. Tests
TrustlessWorkClientwithaxiosmocked: success, 4xx (auth), 5xx (retry then fail).TRUSTLESS_WORK_E2E=1env so CI does not require it by default).Acceptance Criteria
src/modules/escrow/exists with domain / application / infrastructure / presentation layers.escrowsmodel removed; newEscrowmodel +EscrowStatusenum migrated.TrustlessWorkClientis fully typed and covered by unit tests with mockedaxios.POST /projects/:projectId/escrowinitializes an escrow on TW testnet and persistsengagementId.POST /projects/:projectId/escrow/fundaccepts a signed XDR, submits it via TW, and transitions status toFUNDED.TRUSTLESS_WORK_API_URL,TRUSTLESS_WORK_API_KEY,STELLAR_NETWORK,PLATFORM_STELLAR_ADDRESS) are documented in.env.exampleandreadme.md.openapi.yaml) is updated with both endpoints and theEscrowschema.Out of Scope
Suggested Labels
enhancement,feature,stellar,trustless-work