From b940d05cf9d3a127e275a8bb73f3690d14ab44ff Mon Sep 17 00:00:00 2001 From: darcszn Date: Thu, 25 Jun 2026 15:28:56 +0100 Subject: [PATCH] feat: add rate limiting and retry logic to RPC calls in contract.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes broken duplicate-line artifacts left from a partial migration, and wires up the already-defined retry/timeout infrastructure to every raw fetch and SDK call that hits the Stellar testnet RPC. Changes: - invoke(): replace duplicate `fetch(config.rpcUrl)` + invalid `fetchWithRetry(NETWORK.rpcUrl)` lines with a single correct `fetchWithRetry(config.rpcUrl)` for sendTransaction — gains 30 s per-request timeout and automatic retries on 429 / 503 / network errors. - invoke() poll loop: remove duplicate `await new Promise` and stale `fetch(config.rpcUrl)` lines; the loop now uses `fetchWithRetry` for every getTransaction poll, and the existing 60 s POLL_TIMEOUT_MS deadline prevents infinite waits. - createStream(): fix declaration order bug where currentLedger was computed before server was defined; reorder so getServer() runs first, then withRetry(() => server.getLatestLedger()). - createStream(): pass the missing config.streamContractId argument to the invoke() call for create_stream — without it the contract address was undefined. Retry policy (already defined, now actually used): - Max 3 retries with 1s/2s/4s exponential backoff - Retries on HTTP 429, HTTP 503, and network-level errors (TypeError) - 30s AbortController timeout per individual request - 60s hard deadline on the transaction-confirmation poll loop --- lib/contract.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/lib/contract.ts b/lib/contract.ts index e6ce35e..7ef466f 100644 --- a/lib/contract.ts +++ b/lib/contract.ts @@ -124,8 +124,7 @@ async function invoke( // Submit the signed XDR directly via the RPC JSON-RPC endpoint. // We bypass TransactionBuilder.fromXDR because Freighter may return a // FeeBumpTransaction envelope (type 4) which fromXDR can't handle. - const rpcResponse = await fetchWithRetry(NETWORK.rpcUrl, { - const rpcResponse = await fetch(config.rpcUrl, { + const rpcResponse = await fetchWithRetry(config.rpcUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -157,9 +156,7 @@ async function invoke( while (pollStatus !== 'SUCCESS' && pollStatus !== 'FAILED') { if (Date.now() >= pollDeadline) throw new Error('Transaction confirmation timed out after 60s') await new Promise((r) => setTimeout(r, 2000)) - const pollRes = await fetchWithRetry(NETWORK.rpcUrl, { - await new Promise((r) => setTimeout(r, 2000)) - const pollRes = await fetch(config.rpcUrl, { + const pollRes = await fetchWithRetry(config.rpcUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -311,9 +308,8 @@ export async function createStream( // Step 1: approve the streaming contract to pull `totalAmount` from the sender. // The allowance needs to outlast the simulation ledger — set it to current + 500 ledgers. - const currentLedger = (await withRetry(() => server.getLatestLedger())).sequence const server = getServer(network) - const currentLedger = (await server.getLatestLedger()).sequence + const currentLedger = (await withRetry(() => server.getLatestLedger())).sequence const expirationLedger = currentLedger + 500 await invoke( @@ -352,6 +348,7 @@ export async function createStream( 'create_stream', [new Address(sender).toScVal(), params], sender, + config.streamContractId, ) // SDK v13 can't parse TransactionMetaV4 (protocol 22+) so returnValue is void.