Every Soroban contract read is a separate HTTP round-trip to the Soroban RPC endpoint. Endpoints that need multiple pieces of vault state (total assets, share price, paused status, etc.) serialise those round-trips:
Sequential (before): total_assets (200ms) → share_price (200ms) → is_paused (200ms) = 600ms
Batched (after): all three fired concurrently = ~210ms
For latency-sensitive API endpoints (e.g. /api/v1/vault/summary) this makes a material difference.
File: backend/src/sorobanBatchClient.ts
The SorobanBatchClient class fires an array of read calls with Promise.all, bounded by a configurable semaphore to avoid overloading the RPC with too many simultaneous connections.
import { createBatchClient } from './sorobanBatchClient';
const client = createBatchClient(); // uses STELLAR_RPC_URL + VAULT_CONTRACT_ID env vars
// Fire 4 reads concurrently
const summary = await client.getVaultSummaryBatched();
// → { totalAssets: '5000000', totalShares: '4800000', sharePrice: '...', isPaused: false }const [assets, shares, price] = await client.batchRead([
{ method: 'total_assets' },
{ method: 'total_shares' },
{ method: 'share_price' },
]);Rejects if any single call fails. Use when all values are required.
const results = await client.batchReadWithFallback(
[{ method: 'total_assets' }, { method: 'apy_snapshot' }],
null, // fallback value for failed calls
);
for (const r of results) {
if (!r.success) console.warn(`${r.method} failed:`, r.error?.message);
else console.log(`${r.method} =`, r.value);
}Failed calls return the fallback value; the rest of the batch still resolves.
Fetches total_assets, total_shares, share_price, and is_paused in one concurrent batch and returns a typed VaultSummary object. Failed individual reads default to '0' / false.
| Option | Default | Description |
|---|---|---|
rpcUrl |
STELLAR_RPC_URL env |
Soroban RPC endpoint |
contractId |
VAULT_CONTRACT_ID env |
Vault contract address |
maxConcurrency |
5 |
Maximum simultaneous RPC calls |
The maxConcurrency setting is important for public RPC nodes that enforce per-IP rate limits. Keep it at 5 or lower for testnet. For a private RPC node you can increase it.
Every batchRead call emits a debug log entry with:
callCount— number of methods fetchedbatchMs— wall-clock time for the whole batchsumIndividualMs— sum of individual call durationslatencySavingMs—sumIndividualMs - batchMs(positive = concurrent wins)
Example output:
[debug] soroban batch read complete { callCount: 4, batchMs: 210, sumIndividualMs: 780, latencySavingMs: 570 }
All unit tests are in backend/src/__tests__/sorobanBatchClient.test.ts.
Tests use an injected RpcReader mock — no real Soroban RPC is called:
cd backend && npm test -- sorobanBatchClientCoverage:
- All calls succeed → results returned in call order
- One call fails →
batchReadWithFallbackreturns fallback for that slot only maxConcurrencylimit is enforced (≤ N simultaneous in-flight calls)- Latency logging asserted
getVaultSummaryBatchedfull-success and full-failure paths- Empty call array handled gracefully (returns
[], no reader invocations) createBatchClientfactory readsSTELLAR_RPC_URL/VAULT_CONTRACT_IDenv vars