Repository navigation
Bound Soroban RPC calls, add an explicit rail failover policy, and propagate request ids - #1915
Merged
yusuftomilola merged 2 commits intoSep 25, 2026
Conversation
…currency math to integers Soroban RPC calls are now bounded. Every getAccount, simulate, send and status read runs inside the existing withTimeout helper with a configurable SOROBAN_RPC_TIMEOUT_MS (default 10000, validated at construction). A timeout is treated as transient: it retries within that endpoint's budget and then fails over to the next endpoint exactly like a connection outage, because a hung endpoint is not a terminal provider verdict. Rail failover is now an explicit, opt-in policy rather than an implicit failure. PaymentRailRegistry gains an availability-aware resolve() that follows ordered PAYMENT_RAIL_FAILOVER edges, and it is used at initiation only. The Payment row stores the rail that was actually used and the requested rail is retained in metadata, so webhook, reconciliation and refund lookups keep resolving the same adapter that initiated the payment. The strict get() path is unchanged for every post-initiation caller. See the new payments/adapters/README.md for the policy and its documented limits. Request correlation is propagated to outbound calls through a single outboundRequestHeaders() helper that returns both accepted inbound header names and never emits a placeholder id. It is applied to the manual-review SMTP alert, and to the Soroban RPC transport through the SDK seam the pinned version exposes, with an explicit note where a provider cannot accept custom headers. Currency arithmetic was audited across the backend. payments/utils contained no money math, and the existing basis-point allocation already used guarded integer arithmetic, so no correct code was rewritten. Instead payments/utils/minor-units.ts adds a strict integer-only primitive for future payment code, and regression assertions lock in the existing allocation's exactness. Closes DistinctCodes#1785 Closes DistinctCodes#1784 Closes DistinctCodes#1783 Closes DistinctCodes#1782
|
@Maryermarh is attempting to deploy a commit to the naijabuz's projects Team on Vercel. A member of the Team first needs to authorize it. |
|
@Maryermarh Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
…ger-math-rail-failover # Conflicts: # backend/.env.example # backend/src/payments/payments.module.ts # backend/src/payments/payments.service.spec.ts # backend/src/payments/payments.service.ts # backend/src/payments/reconciliation.service.spec.ts # backend/src/payments/reconciliation.service.ts
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
Closes four issues assigned to this account, all in the payments module.
Soroban RPC timeout (#1784)
Every RPC call —
getAccount,simulateTransaction,sendTransaction,getTransaction— is now bounded, so a hung testnet/mainnet endpoint can no longer hold a request open indefinitely. The call is wrapped in the existingwithTimeouthelper and the budget comes fromSOROBAN_RPC_TIMEOUT_MS(default 10000), validated at construction so a malformed value fails fast instead of creating an unbounded client.A timeout is deliberately treated as transient, not terminal: a hung endpoint is an outage, not a verdict that the payment failed, so it retries within that endpoint's budget and then fails over to the next configured endpoint exactly like a connection error.
SorobanRpcTimeoutErrorextendsTransientRpcErrorso it flows through the existingisRetryablepredicate. The docs and the log line both say so explicitly.Rail failover policy (#1783)
The issue's framing was accurate: the registry threw, and the request just failed. Failover is now an explicit, opt-in policy rather than an implicit behaviour.
The important constraint is that a
Paymentstores its rail, and both the webhook and reconciliation paths resolve the adapter from that stored value. Switching rails for a payment that already exists would send its confirmation to the wrong adapter. So failover applies at initiation only:PaymentRailRegistrygainsisAvailable(rail)andresolve(rail), which follows orderedPAYMENT_RAIL_FAILOVERedges (FROM=TO, comma-separated) and returns the first available rail.Paymentrow stores the rail that was actually used; the requested rail is retained inmetadata.requestedRail, and a WARN is logged when they differ.get(rail)path is untouched and is still what webhook, reconciliation and refund use, so those always resolve the adapter that initiated the payment.getPaymentsServicealso compares the requested rail on an idempotency replay, so a replayed request is not mistaken for a different payload.payments/adapters/README.mddocuments the policy: what failover means, when it applies, how to configure it, the degraded behaviour when no fallback is available, and what is deliberately not covered.Request ID propagation (#1782)
Added
outboundRequestHeaders()as the single source of truth. It returns both header names the middleware already accepts on inbound (x-request-idandx-correlation-id), so an id survives a full round trip, and returns an empty object when no request context is active — a placeholder id is never manufactured for background work. It is applied to:headers(a genuinely verifiable outbound propagation);The JSDoc is explicit that a third-party provider which does not accept custom headers cannot be made traceable by adding fields to that object.
Currency math audit (#1785)
The honest finding, rather than a manufactured bug:
payments/utilscontained no currency math (onlywith-timeoutandretry-with-backoff), and the real basis-point allocation incredits/split-allocation.tsalready used guarded integer arithmetic (Math.floor/%with aMAX_SAFE_INTEGERcheck). So no correct code was rewritten.Instead:
parseFloat,Number(),toFixed,Math.round, percentage and money arithmetic. No floating-point currency defect was found; the remainingNumber(...)sites convert database integers or an enum, andtoFixed/retry-jitter/TTL math are non-currency.payments/utils/minor-units.tsadds a strict, integer-only primitive so future payment code has a safe, audited helper instead of reaching for float math: it rejects non-integer/negative/NaN/unsafe values with a typed error, rejects an amount that cannot be scaled exactly rather than silently losing precision, and apportions by largest remainder so allocations always sum exactly to the input.credits/split-allocation.spec.tsgained regression assertions locking in the existing integer guarantee.minor-units.tsis intentionally a separate primitive fromsplit-allocation.ts— the latter is a configurable, tie-broken allocation with sort-order semantics and is left alone.Configuration
SOROBAN_RPC_TIMEOUT_MS10000PAYMENT_RAIL_FAILOVERFROM=TOfailover edges, initiation onlyTesting
Per the task constraints, no install, build, lint, or test run was performed — the maintainer runs those manually. No new dependencies were added, so
package-lock.jsonis unaffected.New specs:
payments/utils/minor-units.spec.ts,common/request-context.spec.ts. Extended:payment-rail-registry.spec.ts,payments.service.spec.ts,reconciliation.service.spec.ts,soroban-rpc-client.spec.ts,credits/split-allocation.spec.ts.Reviewer notes
Closing issues
Closes #1785
Closes #1784
Closes #1783
Closes #1782