feat(stellar): Soroban integration layer for the escrow contract bridge - #1281
Merged
mftee merged 1 commit intoAug 22, 2026
Merged
Conversation
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
|
@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
approved these changes
Aug 22, 2026
mftee
left a comment
Contributor
There was a problem hiding this comment.
Reviewed the Soroban integration layer for the escrow contract bridge (closes #1275).
- Splitting signer responsibility correctly by contract semantics:
fundEscrow/raiseDisputetake an explicitsigner: Keypairbecause the contract requires the acting party's own auth, whilereleasePayment/refundPayment/resolveDisputeare hardcoded to the platform admin key because the contract requiresadmin.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
EscrowErrorvariants pre-submission (rather than discovering them via an opaque RPC rejection) is exactly right for a contract bridge like this. EscrowContractErrorparsing the specific variant (e.g.InvalidStatus) out of Soroban'sError(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
onModuleInitfail-fast admin-key check is a good defensive addition, and it's backed by a genuinely minimal, additive contract change — a read-onlyget_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 fullEscrowRecordand 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 underbackend/src.StellarContractService
backend/src/stellar/stellar-contract.service.ts— one typed method per entrypoint:fundEscrow/raiseDisputetake an explicitsigner: Keypairparameter — 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/resolveDisputealways sign with the platform admin key — the contract requiresadmin.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/getBalancesimulate only (no signing/submission).EscrowErrorvariants pre-submission, per the issue's technical approach.Typed exceptions
SimulationError,SubmissionError,ChainTimeoutError, andEscrowContractError(parses the specificEscrowErrorvariant — e.g.InvalidStatus— out of Soroban'sError(Contract, #N)diagnostic string instead of leaving contract rejections as opaque RPC errors). Submission failures (ERRORstatus) and unconfirmable submissions (TRY_AGAIN_LATER/DUPLICATE) are distinguished from simulation failures, per the issue's edge cases.Startup fail-fast
onModuleInitasserts the loadedPLATFORM_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 (noget_adminexisted), so I added a minimal read-onlyget_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_SECRETare only required whenSOROBAN_ENABLED=true, so CI and local dev work with zero chain calls by default.Tests
SorobanRpc.Server,assembleTransaction) —Address/Keypair/nativeToScVal/scValToNative/xdrencode-decode logic is exercised for real, including decoding a fullEscrowRecord(struct →scvMap,EscrowStatus→scvVec[Symbol(tag)]per how#[contracttype]encodes enums) and parsing a simulatedInvalidStatusrejection into a typedEscrowContractError. Also asserts no test run ever logs the raw secret.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 onceESCROW_CONTRACT_ADDRESSpoints 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— passesnpm run lint— passesnpm run test— 123 passing across the whole backend (18 new, all pre-existing tests still green)cargo fmt/clippy/build/testfor theget_admincontract addition — CI gate, could not run Rust locally in this environment (no Windows SDK for the linker) — reviewed the small addition carefully by handCloses #1275