Skip to content

Add dispute lifecycle endpoints and related utilities#545

Merged
1nonlypiece merged 2 commits into
Commitlabs-Org:masterfrom
nonchalanttee22-lgtm:feature/dispute-lifecycle-endpoints
May 29, 2026
Merged

Add dispute lifecycle endpoints and related utilities#545
1nonlypiece merged 2 commits into
Commitlabs-Org:masterfrom
nonchalanttee22-lgtm:feature/dispute-lifecycle-endpoints

Conversation

@nonchalanttee22-lgtm
Copy link
Copy Markdown
Contributor

Closes #445


PR Description

feat: add dispute lifecycle endpoints (dispute & resolve-dispute) — closes #445


Summary

The escrow contract under contracts/escrow exposes dispute and resolve_dispute instructions, but the backend had no routes wired to them. This PR adds two new API endpoints under src/app/api/commitments/[id]/, the corresponding service methods in src/lib/backend/services/contracts.ts, admin-gated auth on resolution, audit log entries for both actions, Zod-validated request bodies, and full test coverage.


What changed

New routes

src/app/api/commitments/[id]/dispute/route.tsPOST
Allows a commitment party to open a dispute against an active escrow.

src/app/api/commitments/[id]/dispute/resolve/route.tsPOST
Admin-only endpoint to resolve an open dispute. Gated behind requireAuth with an admin role check.

Service layer (src/lib/backend/services/contracts.ts)

Two new methods added:

openDispute(commitmentId: string, caller: string, reason: string): Promise<DisputeResult>
resolveDispute(commitmentId: string, admin: string, resolution: "release" | "refund"): Promise<ResolveResult>

Each method calls the corresponding Soroban contract instruction via the existing RPC client pattern and returns a typed result.

Audit log (src/lib/backend/auditLog.ts)

Both actions write to the audit log:

  • dispute.opened — records commitmentId, caller, reason, timestamp
  • dispute.resolved — records commitmentId, admin, resolution, timestamp

Zod schemas (colocated in each route file)

// dispute/route.ts
const OpenDisputeSchema = z.object({
  caller: z.string().min(1),
  reason: z.string().min(10).max(1000),
});

// dispute/resolve/route.ts
const ResolveDisputeSchema = z.object({
  resolution: z.enum(["release", "refund"]),
});

Invalid bodies return 400 with a structured Zod error payload.

Auth

requireAuth is applied at the top of resolve/route.ts. Requests without a valid admin session get 401. Non-admin sessions get 403. The dispute/route.ts endpoint requires authentication but no elevated role — any authenticated commitment party may open a dispute.

Security headers

Both route handlers wrap their responses in attachSecurityHeaders() consistent with the existing pattern in the codebase.


File changes

src/
└── app/
    └── api/
        └── commitments/
            └── [id]/
                └── dispute/
                    ├── route.ts          ← new: POST open dispute
                    └── resolve/
                        └── route.ts      ← new: POST resolve dispute (admin)
src/
└── lib/
    └── backend/
        └── services/
            └── contracts.ts              ← updated: openDispute, resolveDispute methods

tests/
└── api/
    └── dispute.test.ts                   ← new: open dispute tests
    └── dispute-resolve.test.ts           ← new: resolve dispute tests (auth, roles, happy/sad paths)

openapi.yaml                              ← updated: dispute + resolve-dispute path definitions
docs/
└── backend-api-reference.md             ← updated: new endpoint docs
└── backend-changelog.md                 ← updated: change entry for #445

API contract

POST /api/commitments/:id/dispute

Request body:

{
  "caller": "GADDRESS...",
  "reason": "Counterparty has not fulfilled their obligation."
}

Responses:

  • 201 — dispute opened, returns { disputeId, status: "open", txHash }
  • 400 — invalid body
  • 401 — unauthenticated
  • 404 — commitment not found
  • 409 — dispute already open on this commitment

POST /api/commitments/:id/dispute/resolve

Request body:

{
  "resolution": "release" | "refund"
}

Responses:

  • 200 — dispute resolved, returns { status: "resolved", resolution, txHash }
  • 400 — invalid body
  • 401 — unauthenticated
  • 403 — not an admin
  • 404 — commitment or dispute not found
  • 409 — dispute already resolved

Tests

Coverage targets met (≥95%). Tests in tests/api/dispute.test.ts and tests/api/dispute-resolve.test.ts cover:

  • Happy path: open dispute, resolve as release, resolve as refund
  • 400 on missing/invalid fields (both routes)
  • 401 when unauthenticated (both routes)
  • 403 when authenticated but not admin (resolve only)
  • 404 when commitment ID doesn't exist
  • 409 when a dispute is already open / already resolved
  • Audit log called with correct arguments for both actions (spy assertion)
  • requireAuth called and short-circuits correctly on resolve

All tests use the mock-request helper pattern from tests/api/helpers.ts — no network requests.


Docs

openapi.yaml has been extended with path definitions for both new routes, including request body schemas, all response codes, and security requirements (bearerAuth).

docs/backend-api-reference.md has a new Dispute Lifecycle section with endpoint descriptions, example curl commands, and example responses.

docs/backend-changelog.md has a new entry under the current version noting the addition of POST .../dispute and POST .../dispute/resolve as new routes (non-breaking).


How to test locally

# Run the full test suite
npm test

# Run with coverage to verify ≥95%
npm run test:coverage

# Manual smoke test (requires running dev server + valid admin session)
curl -X POST http://localhost:3000/api/commitments/<id>/dispute \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"caller":"GADDR...","reason":"Counterparty did not deliver."}'

curl -X POST http://localhost:3000/api/commitments/<id>/dispute/resolve \
  -H "Authorization: Bearer <admin-token>" \
  -H "Content-Type: application/json" \
  -d '{"resolution":"release"}'

@vercel
Copy link
Copy Markdown

vercel Bot commented May 28, 2026

@teethaking is attempting to deploy a commit to the 1nonly's projects Team on Vercel.

A member of the Team first needs to authorize it.

@drips-wave
Copy link
Copy Markdown

drips-wave Bot commented May 28, 2026

@nonchalanttee22-lgtm Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@1nonlypiece 1nonlypiece merged commit 739b0d9 into Commitlabs-Org:master May 29, 2026
0 of 2 checks passed
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.

Backend - Add dispute lifecycle endpoints mirroring the escrow contract dispute flow

3 participants