Skip to content

feat: implement on-chain synchronization for escrow intents - #95

Open
Mrwicks00 wants to merge 1 commit into
Padi-Pay:mainfrom
Mrwicks00:feat/on-chain-synchronization
Open

feat: implement on-chain synchronization for escrow intents#95
Mrwicks00 wants to merge 1 commit into
Padi-Pay:mainfrom
Mrwicks00:feat/on-chain-synchronization

Conversation

@Mrwicks00

Copy link
Copy Markdown
Contributor

PR title: feat: implement on-chain synchronization for escrow intents

PR description (copy-paste when opening the PR):


Summary

Implements Issue #58 — a synchronization service/webhook handler that bridges the off-chain EscrowIntent DB record with the on-chain truth once a transaction completes.

  • POST /api/relayer/escrow/:id/sync: given a reported transaction outcome (sorobanEscrowId + status), stamps the EscrowIntent with the on-chain escrow id and transitions its status from PENDING to LOCKED when the status is SUCCESS.
  • Idempotent by design: a duplicate delivery of the same successful event (same intent, same on-chain escrow id, already LOCKED) is a safe no-op — no repository write, no error.
  • Guards data integrity: if an intent is already LOCKED with a different on-chain escrow id, or is in some other unexpected lifecycle state, the request is rejected (409) rather than silently overwriting the record.
  • A non-SUCCESS status (FAILED/NOT_FOUND/PENDING) is treated as a legitimate non-event, not a caller error — the intent is returned unchanged with synchronized: false.

Scope note: dependency Issue #17 (stellar.service.js + horizon.service.js + status.parser.js, already in main) handles transaction submission and status normalization, but none of it decodes the Soroban contract's return value (the new on-chain escrow id lives in resultMetaXdr, which nothing in this codebase parses yet). Per the issue's explicit out-of-scope note ("assume a webhook or internal callback triggers this" / no direct polling), this service takes the on-chain escrow id as an already-resolved input — its job is purely the deterministic DB-side bridge, not XDR parsing.

Changes

  • src/services/escrow-sync.service.js (new): syncEscrowOnChain({ escrowIntentId, sorobanEscrowId, status }) — the core state-transition + idempotency/conflict logic described above.
  • src/validation/schemas/escrow.schema.js: added syncEscrowSchema (validates params.id, body.sorobanEscrowId, body.status against the recognized status vocabulary).
  • src/routes/relayer.routes.js: new POST /escrow/:id/sync route. Not gated by the authenticate middleware — this is a system/internal callback reporting a fact about a transaction that already happened, not an action performed on behalf of a logged-in buyer, and there's no webhook-signature-verification infra in this repo yet to gate it with instead. Flagging this as a natural follow-up (shared-secret or signature verification) rather than deciding it silently.

Out of scope

  • Polling the Stellar network directly (per the issue).
  • Decoding the Soroban transaction return value / resultMetaXdr (belongs with the transaction submission/tracking work).

Test plan

  • npm test — 114/114 passing, coverage 97.6% stmts / 92.2% branches (above the 80%/75% thresholds).
  • npm run lint — clean.
  • New unit tests for escrow-sync.service.js: PENDING → LOCKED happy path; 404 on missing intent; non-SUCCESS status → unchanged + synchronized:false; duplicate delivery → idempotent no-op (no repository write); conflicting escrow id on an already-LOCKED intent → 409; unexpected lifecycle state → 409.
  • New schema tests for syncEscrowSchema.
  • New route tests for POST /escrow/:id/sync: validation failure (400), happy path (200), and error propagation through the existing error middleware.

Closes #58

Adds POST /api/relayer/escrow/:id/sync, an internal/webhook callback
that bridges an EscrowIntent's DB record with the outcome of its
on-chain transaction: on a reported SUCCESS it stamps the intent with
the resolved Soroban escrow id and transitions status from PENDING to
LOCKED. Duplicate delivery of the same event is a no-op, and a
conflicting escrow id on an already-locked intent is rejected, so
records are never corrupted or double-updated.
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.

On-Chain Synchronization

1 participant