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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions