Skip to content

Integrate Trustless Work: initialize and fund escrow when an organization publishes a rewarded project #208

@grantfox-development

Description

@grantfox-development

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

  • src/modules/escrow/ exists with domain / application / infrastructure / presentation layers.
  • Legacy escrows model removed; new Escrow model + EscrowStatus enum migrated.
  • TrustlessWorkClient is fully typed and covered by unit tests with mocked axios.
  • POST /projects/:projectId/escrow initializes an escrow on TW testnet and persists engagementId.
  • POST /projects/:projectId/escrow/fund accepts a signed XDR, submits it via TW, and transitions status to FUNDED.
  • Required env vars (TRUSTLESS_WORK_API_URL, TRUSTLESS_WORK_API_KEY, STELLAR_NETWORK, PLATFORM_STELLAR_ADDRESS) are documented in .env.example and readme.md.
  • Only the project's owning organization can call these endpoints; tests cover the negative case.
  • OpenAPI spec (openapi.yaml) is updated with both endpoints and the Escrow schema.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions