Payment initiation and idempotent, low-latency transaction submission - #1282
Merged
mftee merged 1 commit intoAug 22, 2026
Conversation
|
@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. |
Adds POST /shipments/:id/payment (builds+simulates an unsigned fund_escrow transaction for the shipper to sign client-side, non-custodial) and POST /shipments/:id/payment/:paymentId/submit (accepts the signed XDR and submits it). A new FUNDING payment status is claimed atomically (PENDING -> FUNDING) before the chain call, so concurrent duplicate submit requests can never both reach the network — the loser gets a structured 409 instead. Stellar/escrow contract failures (insufficient balance, missing token allowance, contract rejection) map to typed 4xx errors instead of a generic 500. Shipment.price converts to the contract's i128 base units via a documented fixed 7-decimal shift (MVP, no live FX). Also adds a test-only shipper-signing path (ALLOW_TEST_SIGNING, disabled by default) so the flow is verifiable end-to-end on testnet before the frontend wallet-signing UI exists. Closes CodeGirlsInc#1276
AbdulmujibOladayo
force-pushed
the
feature/1276-payment-initiation-fund-escrow
branch
from
August 22, 2026 09:22
948f8e1 to
8f9b636
Compare
mftee
approved these changes
Aug 22, 2026
mftee
left a comment
Contributor
There was a problem hiding this comment.
Reviewed the payment initiation and submission PR (closes #1276, building on the Soroban bridge from #1275).
- The two-endpoint split (initiate/build-and-simulate unsigned XDR, then separately submit the shipper-signed XDR) correctly preserves the non-custodial model from the ADR — the platform never touches shipper keys, only ever unsigned or shipper-signed envelopes.
- The atomic
PENDING -> FUNDINGclaim via a conditionalUPDATE ... WHERE status = 'pending'immediately before the chain call is the right way to prevent double-submission — it means the race is resolved at the DB layer before any network call happens, not after. The loser getting a structured 409 instead of a second chain call, and the claim being released back toPENDINGon failure so a legitimate retry isn't permanently blocked, are both the correct behaviors for this kind of guard. - Mapping
StellarContractServicefailures (simulation, submission, typed contract rejection) into structuredPaymentFlowError4xx subclasses instead of letting them surface as generic 500s continues the pattern established in the earlier Stellar bridge work — good consistency. - The stale-carrier-binding refresh (an existing
PENDINGrow gets its carrier/shipper wallet refreshed in place if the shipment's carrier changed since the row was created, instead of reusing stale wallet data) is a real edge case that's easy to miss and is explicitly tested. - The 7-decimal fiat-to-
i128conversion being documented as an MVP simplification, and specifically matching the convention already used in the escrow contract's own tests rather than picking an arbitrary precision, shows the tradeoff was made deliberately rather than by accident. - The test-only signing path is appropriately gated behind
ALLOW_TEST_SIGNINGand disabled by default — a reasonable way to get end-to-end testnet verification before the real wallet-signing frontend exists, without it being reachable in a normal deployment. - Test coverage matches the risk surface: the atomic claim race (including the losing
affected: 0path), claim-release-on-failure, stale-carrier refresh, and simulation-error mapping are all explicitly covered, plus the pre-existing 18StellarContractServicetests still pass after extracting the sharedsubmitPreparedhelper.
CI is green across Backend, Frontend, and Contracts. Vercel's FAILURE is the usual unauthorized deployment integration link, unrelated to the code.
Approving — careful handling of the funding flow's concurrency and failure edges.
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
POST /shipments/:id/payment(shipper only): validates the shipment is ACCEPTED and the requester is its shipper, then builds and simulates (but does not sign) afund_escrowtransaction viaStellarContractService.buildFundEscrowTransaction, returning unsigned XDR for the shipper's own wallet to sign — the platform never holds shipper keys (non-custodial deposit model from [PAY-02] Soroban integration layer — signer, RPC client, and escrow contract bridge #1275/ADR).POST /shipments/:id/payment/:paymentId/submit(shipper only): accepts the shipper-signed XDR and submits it via a newStellarContractService.submitSignedTransaction.FUNDINGpayment status, claimed atomically (PENDING -> FUNDINGvia a conditionalUPDATE ... WHERE status = 'pending') immediately before the chain call insubmit, so two concurrent duplicate submit requests can never both reach the network — the loser gets a structured 409 (PaymentAlreadyInFlightError) instead of a second chain call. On failure the claim is released back toPENDINGso a legitimate retry is possible.StellarContractServicefailures (simulation failure incl. insufficient balance/missing tokenapproveallowance, submission failure, typed escrow-contract rejection) to structured 4xxPaymentFlowErrorsubclasses instead of a generic 500.Shipment.price(fiat decimal) converts to the contract'si128base units via a documented fixed 7-decimal shift (settlement-asset.util.ts) — an MVP simplification (no live FX), matching the 7-decimal convention already used in the escrow contract's own tests.PENDINGpayment row has its carrier/shipper wallet binding refreshed in place rather than reused stale, if the shipment's carrier has changed since the row was created.POST .../test-sign-and-submit, gated byALLOW_TEST_SIGNING, disabled by default) so the funding flow is verifiable end-to-end on testnet before the frontend wallet-signing UI (a later issue) exists.FUNDINGenum value andstellar_tx_hash/failure_reasoncolumns (production relies on migrations;synchronizeis dev/test only).Test plan
npm run buildnpm run lint(scoped to changed files — 0 errors; the full-repo run shows thousands of pre-existing CRLF-as-error results across untouched files, confirmed unrelated to this change)npm run test— all suites pass, including new/updated specs:payments.service.spec.ts(forbidden/not-accepted/missing-wallet validation, first-funding-attempt row creation, already-funded / already-in-flight guards, stale-carrier-binding refresh, simulation-error mapping, the atomic claim race for concurrent submit —affected: 0losing path, claim-release-on-failure, and the test-signing path)stellar-contract.service.spec.ts(newbuildFundEscrowTransaction/submitSignedTransactionmethods, plus all 18 pre-existing tests still passing after extracting the sharedsubmitPreparedhelper frominvoke)settlement-asset.util.spec.tsCloses #1276