Skip to content

Spike: escrow on-chain write path — recommend client-signed, prototype release builder - #186

Merged
meshackyaro merged 2 commits into
trustflow-protocol:mainfrom
kris-nana:spike/escrow-write-path-180
Aug 16, 2026
Merged

Spike: escrow on-chain write path — recommend client-signed, prototype release builder#186
meshackyaro merged 2 commits into
trustflow-protocol:mainfrom
kris-nana:spike/escrow-write-path-180

Conversation

@kris-nana

Copy link
Copy Markdown
Contributor

Summary

Closes #180

EscrowService.create/release/raiseDispute are purely in-memory today — none of them ever builds, signs, or submits a Soroban transaction, even though event-ingestion already reads chain state back into EscrowService. This spike:

  • Maps every escrow action against whether it touches chain today (none do) and whether event-ingestion would observe it (all three would).
  • Evaluates server-side signing/custody vs. client-signed-only architectures and recommends client-signed: no key-custody infrastructure exists anywhere in this repo, and server custody wouldn't even skip the existing event-ingestion round-trip that EscrowService already depends on.
  • Prototypes the recommendation for the release action: EscrowReleaseTransactionBuilderService builds and simulates an unsigned Soroban release invocation (GET /escrows/:id/release/transaction), mirroring how SorobanEscrowChainStateClient already reads chain state. The backend never signs or submits — the caller's wallet does, and the resulting event flows back through the existing, unmodified event-ingestion pipeline.
  • Documents how this reconciles with stellar.service.ts / soroban.helper.ts.
  • Flags blocking unknowns explicitly (unverified contract entrypoint shape, no live testnet run possible in this environment, unknown auth requirements on release).

Full write-up: backend/ESCROW_WRITE_PATH_SPIKE.md.

Follow-up implementation issues filed and linked from #180.

Test plan

  • npm run lint:check — clean (pre-existing warnings only, unrelated to this change)
  • npm run format:check — clean
  • npx tsc --noEmit — clean
  • npm run test:ci — 337/337 tests passing (5 new tests for the prototype, plus updated EscrowController tests)
  • npm run build — clean

…ned release builder

EscrowService.release()/raiseDispute()/create() only ever mutate an
in-memory map — none of them has ever built, signed, or submitted a
Soroban transaction, even though event-ingestion already reads chain
state back into EscrowService. This resolves the ambiguity by
recommending a client-signed design (backend builds/simulates
unsigned XDR, never holds a signing key) over server-side custody,
since no key-custody infrastructure exists anywhere in this repo and
custody wouldn't even skip the existing event-ingestion round-trip.

Prototypes the recommendation for the release action:
EscrowReleaseTransactionBuilderService builds and simulates an
unsigned release invocation via GET /escrows/:id/release/transaction,
mirroring how SorobanEscrowChainStateClient already reads chain state.

Closes trustflow-protocol#180
@kris-nana
kris-nana requested a review from meshackyaro as a code owner August 16, 2026 15:51
@meshackyaro

Copy link
Copy Markdown
Contributor

Summary

Closes #180

EscrowService.create/release/raiseDispute are purely in-memory today — none of them ever builds, signs, or submits a Soroban transaction, even though event-ingestion already reads chain state back into EscrowService. This spike:

  • Maps every escrow action against whether it touches chain today (none do) and whether event-ingestion would observe it (all three would).
  • Evaluates server-side signing/custody vs. client-signed-only architectures and recommends client-signed: no key-custody infrastructure exists anywhere in this repo, and server custody wouldn't even skip the existing event-ingestion round-trip that EscrowService already depends on.
  • Prototypes the recommendation for the release action: EscrowReleaseTransactionBuilderService builds and simulates an unsigned Soroban release invocation (GET /escrows/:id/release/transaction), mirroring how SorobanEscrowChainStateClient already reads chain state. The backend never signs or submits — the caller's wallet does, and the resulting event flows back through the existing, unmodified event-ingestion pipeline.
  • Documents how this reconciles with stellar.service.ts / soroban.helper.ts.
  • Flags blocking unknowns explicitly (unverified contract entrypoint shape, no live testnet run possible in this environment, unknown auth requirements on release).

Full write-up: backend/ESCROW_WRITE_PATH_SPIKE.md.

Follow-up implementation issues filed and linked from #180.

Test plan

  • npm run lint:check — clean (pre-existing warnings only, unrelated to this change)
  • npm run format:check — clean
  • npx tsc --noEmit — clean
  • npm run test:ci — 337/337 tests passing (5 new tests for the prototype, plus updated EscrowController tests)
  • npm run build — clean

Reviewed the spike, the new transaction-builder prototype, tests, controller wiring, and the spike doc. This is a thoughtful, low-risk addition that correctly recommends client-signed transactions and provides a focused, well-tested prototype for the release flow. Merge after the small, targeted changes below (mostly testability/DI and a couple of validation/nit fixes).

Before I can approve and merge, please address the following

backend/src/escrow-write/escrow-release-transaction-builder.service.ts — The service instantiates SorobanRpc.Server(STELLAR_CONFIG.sorobanRpcUrl) inside its constructor, which forces module-level config/side-effect coupling and complicates testing. Make the rpc server (or a Soroban client abstraction) injectable via the constructor or accept an optional rpcServer parameter. Prefer a Nest provider so tests can supply a mocked rpcServer without jest.resetModules/process.env manipulation.
backend/src/escrow-write/escrow-release-transaction-builder.service.spec.ts — The tests currently mutate process.env and use jest.resetModules() + require(...) to exercise configured vs unconfigured behavior. Update tests to construct the service with injected config and a mocked rpcServer instead of module reloading. This will make tests more straightforward and avoid global state flakiness.
backend/src/escrow/escrow.controller.ts — The STELLAR_ADDRESS_REGEX is defined inline. Reuse a shared validator/util or use Nest’s Pipes + class-validator to validate Stellar addresses in a central place (avoids duplication and keeps validation consistent across endpoints).
backend/src/escrow/escrow-release-transaction-builder.service.ts — The method returns prepared.toXDR() and a minimal set of metadata. Consider (in a follow-up) including simulation results/footprint summary or exposing simulation errors clearly so clients/SDKs can present useful UI messages before signing. At minimum, document the exact contract/entrypoint assumptions in code near the contract.call(...) line (there is already a note in the spike doc).
backend/src/escrow/escrow.controller.spec.ts — The new controller tests mock the builder; ensure they also add a test to assert that invalid sourceAccount triggers BadRequest before any service calls (I see one test — ensure it remains stable after refactoring validation into a pipe/validator).

Other Possible improvements, although non-blocking, but recommended:

backend/src/escrow-write/escrow-release-transaction-builder.service.ts — Centralize the contract entrypoint name and expected argument shape into a single constant or config area (with a TODO to update when the contract repo is available). This makes it easier to update once the contract signature is confirmed.
Logging: be mindful of PII/sensitivity for escrow IDs in logs — consider redaction or structured logs indexed by a non-sensitive identifier if needed.
API docs: add a tiny OpenAPI description/example showing the intended client flow (build unsigned XDR -> sign -> submit to Soroban RPC). This will help SDK/integrator teams and is quick to add to the controller’s docstrings.

…lize address validation

Addresses maintainer review on trustflow-protocol#186:
- Inject the Soroban RPC server and Stellar config into
  EscrowReleaseTransactionBuilderService via Nest provider tokens instead
  of constructing them in the constructor, so tests supply mocks directly
  instead of mutating process.env and reloading modules.
- Validate sourceAccount with a class-validator DTO
  (BuildReleaseTransactionQueryDto) reusing a shared STELLAR_ADDRESS_REGEX
  exported from escrow.dto.ts, run through the app's global ValidationPipe,
  instead of a controller-local regex check. Covered by a Supertest-driven
  integration test that boots a real Nest app, matching the pattern already
  used in reputation.controller.spec.ts.
- Pull the assumed `release` entrypoint name into a documented constant
  next to the contract.call() site.
- Flesh out the endpoint's OpenAPI docs with a response schema and the
  intended sign/submit client flow.
@kris-nana

Copy link
Copy Markdown
Contributor Author

Thanks for the review — pushed a follow-up commit addressing the blocking items:

  • DI for the RPC server/config: EscrowReleaseTransactionBuilderService now takes the Soroban RPC server and Stellar config via injected Nest provider tokens (SOROBAN_RPC_SERVER, ESCROW_WRITE_STELLAR_CONFIG, wired in escrow-write.module.ts) instead of constructing them inline.
  • Tests no longer mutate process.env: the service spec now constructs the service directly with a mocked RPC server and an arbitrary config object — no more jest.resetModules()/require().
  • Centralized address validation: added STELLAR_ADDRESS_REGEX as a shared export from escrow.dto.ts, and the new endpoint now validates sourceAccount via a class-validator DTO (BuildReleaseTransactionQueryDto) running through the app's existing global ValidationPipe, rather than an inline regex check in the controller.
  • Added a Supertest-driven integration test block (matching the existing pattern in reputation.controller.spec.ts) that boots a real Nest app so the 400-on-invalid-sourceAccount behavior is actually verified end-to-end through the pipe, not just at the controller-method level.
  • Pulled the assumed release entrypoint name into a documented constant right next to the contract.call(...) line, in addition to the existing note in the spike doc.
  • Fleshed out the endpoint's OpenAPI docs with a response schema and an explicit build → sign → submit client-flow description.

Left the logging as-is — the only thing logged is the internal contractEscrowId, never the caller's wallet address, so I don't think there's PII exposure there, but happy to add redaction if you'd like it more conservative.

All local checks pass (lint, format, typecheck, 339/339 tests, build) and CI is green.

@meshackyaro meshackyaro left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent follow-up commit — all five blocking items are now cleanly addressed. This PR is ready to merge.

What I verified:

  1. DI for RPC/config: SOROBAN_RPC_SERVER and ESCROW_WRITE_STELLAR_CONFIG are now properly injected via Nest providers in escrow-write.module.ts. Tests supply mocked RPC directly — no jest.resetModules() or process.env mutation.

  2. Test refactoring: Both the unit tests in escrow-release-transaction-builder.service.spec.ts and controller tests in escrow.controller.spec.ts are now clean. The new Supertest integration test matches the pattern in reputation.controller.spec.ts and properly verifies that invalid sourceAccount is rejected by the ValidationPipe before any service call.

  3. Centralized address validation: STELLAR_ADDRESS_REGEX is now exported from escrow.dto.ts and reused in BuildReleaseTransactionQueryDto. Validation runs through the global ValidationPipe consistently.

  4. Entrypoint constant & docs: RELEASE_ENTRYPOINT is pulled into a documented constant with the caveat that it's unverified. The OpenAPI docs are comprehensive — they show the intended client flow (build → sign → submit) and clearly explain why the backend never signs.

  5. Simulation/error handling: The current approach (return XDR + metadata, let caller handle simulation errors) is pragmatic for a spike. Consider (in a follow-up) exposing simulation result summaries if SDKs need to show clients resource costs before signing.

Architecture confidence:

The spike write-up is thorough and the recommendation for client-signed over server-side custody is sound. No custody infrastructure in the repo today, and server custody wouldn't even skip the existing event-ingestion round-trip — so the lower-blast-radius client-signed path is the right call.

Code quality:

  • 339/339 tests passing, lint/format/typecheck all clean
  • Follow-up issues are scoped and linked from #180
  • Blocking unknowns (unverified contract shape, no live testnet, unknown auth) are explicitly flagged

Great job. This sets the pattern for the remaining actions (create, raiseDispute) and leaves the repository in a much clearer state on the escrow write-path question. Well done @kris-nana!

@meshackyaro
meshackyaro merged commit 3ad0d29 into trustflow-protocol:main Aug 16, 2026
1 check 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.

Spike: Define the on-chain write path for escrow actions (create/release/dispute)

2 participants