Skip to content

feat(stellar): Soroban integration layer for the escrow contract bridge - #1281

Merged
mftee merged 1 commit into
CodeGirlsInc:mainfrom
wumibals:feature/1275-soroban-integration-layer
Aug 22, 2026
Merged

feat(stellar): Soroban integration layer for the escrow contract bridge#1281
mftee merged 1 commit into
CodeGirlsInc:mainfrom
wumibals:feature/1275-soroban-integration-layer

Conversation

@wumibals

Copy link
Copy Markdown
Contributor

Summary

Gives the backend a real, tested ability to build, simulate, sign, and submit transactions against the escrow contract's (contracts/escrow/src/lib.rs) fund_escrow, release_payment, refund_payment, raise_dispute, resolve_dispute, and read entrypoints. Before this PR, no service anywhere held an RPC URL, network passphrase, contract address, or the platform's signing keypair, and @stellar/stellar-sdk (already a declared dependency) was never imported anywhere under backend/src.

StellarContractService

backend/src/stellar/stellar-contract.service.ts — one typed method per entrypoint:

  • fundEscrow/raiseDispute take an explicit signer: Keypair parameter — the contract requires the shipper's/party's own auth for these (shipper.require_auth()/caller.require_auth()), not the platform's, so the caller supplies whichever keypair is authorized. Who is authorized to call these and how they obtain a signer is a business-logic decision explicitly left to a later issue.
  • releasePayment/refundPayment/resolveDispute always sign with the platform admin key — the contract requires admin.require_auth() for these regardless of caller, which is a fixed technical fact about the contract rather than a business decision, so it's baked into the service.
  • getEscrow/getBalance simulate only (no signing/submission).
  • Every write simulates first to catch EscrowError variants pre-submission, per the issue's technical approach.

Typed exceptions

SimulationError, SubmissionError, ChainTimeoutError, and EscrowContractError (parses the specific EscrowError variant — e.g. InvalidStatus — out of Soroban's Error(Contract, #N) diagnostic string instead of leaving contract rejections as opaque RPC errors). Submission failures (ERROR status) and unconfirmable submissions (TRY_AGAIN_LATER/DUPLICATE) are distinguished from simulation failures, per the issue's edge cases.

Startup fail-fast

onModuleInit asserts the loaded PLATFORM_ADMIN_SECRET's public key matches the contract's stored admin — fails loud at boot instead of on first admin-gated call. This needed a small, additive change to the escrow contract itself: it had no way to expose its stored admin at all (no get_admin existed), so I added a minimal read-only get_admin() -> Result<Address, EscrowError> query. It's purely additive — doesn't touch any existing entrypoint signature or storage layout — with a new Rust unit test (test_get_admin_returns_configured_admin).

SOROBAN_ENABLED

Validated via the existing Joi schema pattern in app.module.ts: SOROBAN_RPC_URL/STELLAR_NETWORK_PASSPHRASE/ESCROW_CONTRACT_ADDRESS/TOKEN_CONTRACT_ADDRESS/PLATFORM_ADMIN_SECRET are only required when SOROBAN_ENABLED=true, so CI and local dev work with zero chain calls by default.

Tests

  • Unit tests mock only network I/O (SorobanRpc.Server, assembleTransaction) — Address/Keypair/nativeToScVal/scValToNative/xdr encode-decode logic is exercised for real, including decoding a full EscrowRecord (struct → scvMap, EscrowStatusscvVec[Symbol(tag)] per how #[contracttype] encodes enums) and parsing a simulated InvalidStatus rejection into a typed EscrowContractError. Also asserts no test run ever logs the raw secret.
  • One gated live-testnet integration test (opt-in via RUN_SOROBAN_INTEGRATION_TESTS=true, so it never affects the hermetic suite CI gates on): verifies real connectivity to Soroban testnet RPC unconditionally (I ran this myself against the actual testnet endpoint), plus a full fund→release→getEscrow round trip that runs once ESCROW_CONTRACT_ADDRESS points at a real deployed instance — no contract is deployed yet, so that half is currently skipped, but the harness is ready for when one is.

Test plan

  • npm run build — passes
  • npm run lint — passes
  • npm run test — 123 passing across the whole backend (18 new, all pre-existing tests still green)
  • Ran the live RPC-connectivity integration test myself against the real Soroban testnet endpoint — passed
  • cargo fmt/clippy/build/test for the get_admin contract addition — CI gate, could not run Rust locally in this environment (no Windows SDK for the linker) — reviewed the small addition carefully by hand

Closes #1275

Gives the backend a real, tested ability to build, simulate, sign, and
submit transactions against the escrow contract's (contracts/escrow/
src/lib.rs) fund_escrow, release_payment, refund_payment, raise_dispute,
resolve_dispute, and read entrypoints. Before this, no service anywhere
held an RPC URL, network passphrase, contract address, or signing
keypair, and @stellar/stellar-sdk was never imported under backend/src.

- StellarContractService (backend/src/stellar/stellar-contract.service.ts):
  one typed method per entrypoint. Every write simulates first (catching
  EscrowError variants pre-submission), signs, then submits; reads
  simulate only. fundEscrow/raiseDispute take an explicit signer (the
  shipper's or party's own keypair, since the contract requires their
  auth, not the platform's) — releasePayment/refundPayment/resolveDispute
  always sign with the platform admin key, since the contract itself
  requires admin auth for those regardless of caller.
- Distinguishable exception types: SimulationError, SubmissionError,
  ChainTimeoutError, and EscrowContractError (parses the specific
  EscrowError variant — e.g. InvalidStatus — out of Soroban's
  `Error(Contract, #N)` diagnostic string instead of leaving contract
  rejections as opaque RPC errors).
- Startup assertion (onModuleInit) that the loaded PLATFORM_ADMIN_SECRET's
  public key matches the contract's stored admin — fails loud at boot
  instead of on first admin-gated call. This needed a new read-only
  get_admin() query on the escrow contract itself, since it had no way to
  expose its stored admin before; purely additive, doesn't touch any
  existing entrypoint or storage layout (new Rust unit test included).
- SOROBAN_ENABLED flag (validated via the existing Joi schema pattern in
  app.module.ts, conditionally requiring SOROBAN_RPC_URL/
  STELLAR_NETWORK_PASSPHRASE/ESCROW_CONTRACT_ADDRESS/
  TOKEN_CONTRACT_ADDRESS/PLATFORM_ADMIN_SECRET only when it's true) so
  CI and local dev work with zero chain calls by default.
- Unit tests mock only network I/O (SorobanRpc.Server, assembleTransaction)
  — Address/Keypair/nativeToScVal/scValToNative/xdr encode-decode logic is
  exercised for real, including decoding a full EscrowRecord (struct →
  scvMap, EscrowStatus → scvVec[Symbol] per #[contracttype]'s enum
  encoding) and parsing a simulated InvalidStatus rejection into a typed
  EscrowContractError. Also asserts no test ever logs the raw secret.
- One gated live-testnet integration test
  (stellar-contract.service.integration.spec.ts, opt-in via
  RUN_SOROBAN_INTEGRATION_TESTS=true so it never affects the hermetic
  suite CI gates on): verifies real connectivity to Soroban testnet RPC
  unconditionally, plus a full fund→release→getEscrow round trip that
  runs once a real escrow instance is deployed (ESCROW_CONTRACT_ADDRESS
  set) — no contract is deployed yet, so that half is currently skipped,
  but the harness is ready for when one is.

Closes CodeGirlsInc#1275
@vercel

vercel Bot commented Aug 21, 2026

Copy link
Copy Markdown

@wumibals is attempting to deploy a commit to the Mftee's projects Team on Vercel.

A member of the Team first needs to authorize it.

@mftee mftee 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.

Reviewed the Soroban integration layer for the escrow contract bridge (closes #1275).

  • Splitting signer responsibility correctly by contract semantics: fundEscrow/raiseDispute take an explicit signer: Keypair because the contract requires the acting party's own auth, while releasePayment/refundPayment/resolveDispute are hardcoded to the platform admin key because the contract requires admin.require_auth() regardless of caller. That's not an arbitrary API choice, it's mirroring a fixed fact about the contract — good call baking it into the service rather than leaving it as a footgun for the caller to get wrong.
  • Simulating every write before submission to surface EscrowError variants pre-submission (rather than discovering them via an opaque RPC rejection) is exactly right for a contract bridge like this.
  • EscrowContractError parsing the specific variant (e.g. InvalidStatus) out of Soroban's Error(Contract, #N) string, and distinguishing submission failures from simulation failures and from unconfirmable states (TRY_AGAIN_LATER/DUPLICATE), gives callers something actionable instead of a generic chain error.
  • The onModuleInit fail-fast admin-key check is a good defensive addition, and it's backed by a genuinely minimal, additive contract change — a read-only get_admin() query that doesn't touch any existing entrypoint signature or storage layout, with its own unit test.
  • SOROBAN_ENABLED-gated env validation keeps CI and local dev chain-call-free by default, which is the right default for a service like this.
  • Test approach is solid: unit tests mock only network I/O while exercising real Address/Keypair/XDR encode-decode logic (including decoding a full EscrowRecord and its enum encoding), plus an explicit assertion that the platform secret never gets logged. The live-testnet integration test is opt-in and was actually run by the author against the real endpoint rather than just described.

CI is green across Backend, Frontend, and Contracts (Rust) — the Contracts job covers the get_admin addition despite the author not being able to run cargo locally. Vercel's FAILURE is the usual unauthorized deployment integration link, unrelated to the code.

Approving — solid, well-scoped bridge layer.

@mftee
mftee merged commit 6e04d80 into CodeGirlsInc:main Aug 22, 2026
3 of 4 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.

[PAY-02] Soroban integration layer — signer, RPC client, and escrow contract bridge

2 participants