Skip to Content
Symmio Trading-SDK — the SDK surface for builders on Arbitrum
CoreGaslesswaitForGaslessRequest

waitForGaslessRequest

Poll one gasless request until it broadcasts or reaches a terminal status — the framework-agnostic awaited form of the getGaslessRequest poll loop.

This is the raw driver: it reports whatever terminal it reaches. To assert an outcome — resolve only on succeeded, optionally after the receipt lands on your own client — use confirmGaslessRequest instead. Reach for this one when you want until: "broadcast", or when a non-succeeded terminal is a normal branch rather than an error.

import { waitForGaslessRequest, GaslessRequestStatus } from "@symmio/trading-core"; const record = await waitForGaslessRequest(config, { requestId, until: "broadcast" }); if (record.txHash) { await config.getClient().waitForTransactionReceipt({ hash: record.txHash }); }

The cadence follows the service recommendation — GASLESS_QUEUED_POLL_MS (1 500 ms) while queued, GASLESS_SUBMITTED_POLL_MS (3 000 ms) after submitted — bounded by an overall timeoutMs budget (GASLESS_WAIT_TIMEOUT_MS, 120 000 ms). Up to three consecutive 404s are tolerated right after a submit (the accepted record may not be readable yet); a fourth is rethrown.

Every cadence and window below falls back to the deployment’s gasless.execution block before its constant, so a gateway’s own tuning reaches every wait — including the ones you never call directly, like the one a relayed write runs in its receipt tail.

The stream leads; HTTP is the fallback

Where the deployment serves a status stream, the wait subscribes to it and polls only while it is not delivering. A healthy workflow therefore costs no status reads at all: the stream carries the same records the reads return, so a caller cannot tell which transport produced one.

SituationWhat the wait does
Subscribing, first record not in yetWaits up to streamSettleMs (1 000 ms) before reading — the answer is coming
The stream says it cannot deliver (unreachable, not on this instance, over the subscription cap, disabled)Reads at once; the settle window does not run out first
DeliveringPolling stands down entirely
Delivering, but silent about this workflow for streamStaleMs (15 000 ms)Takes one read anyway, then re-arms the window
No stream configured, or transport: "poll"Polls at the cadence above, exactly as before

The stale window is not redundant with the socket’s own liveness check: heartbeats arrive about every 20 s and prove the socket is alive, saying nothing about whether this workflow is still being reported. Without it a live-but-silent stream could blind a wait until the socket’s 60 s liveness timer fired.

The settle window is deliberately shorter than the poll cadence it defers, so a stream that never answers costs one delayed read rather than an extra one.

A failed read is not a failed request

Once the service has returned a 202 the workflow is running whether or not you can see it, so the loop absorbs every transient transport failure instead of reporting one:

FailureWhat the loop does
No response, 408, 429, 500, 502, 503, 504Reports it through onTransportIssue, then retries after max(Retry-After, jittered 1 s → 30 s backoff)
404, up to three in a row right after a submitRetries — the accepted record may not be readable yet
A fourth consecutive 404Throws: the id is not a request this service knows
401, 403, 422, a service error codeThrows: the service gave a definitive answer

Every retry is capped by whatever is left of timeoutMs, so a 30-second backoff can never hide a 5-second budget. Use onTransportIssue to render “status unavailable, still running” — never a failure, and never a reason to re-submit an intent that is already executing.

const record = await waitForGaslessRequest(config, { requestId, onTransportIssue: () => setDegraded(true), onUpdate: () => setDegraded(false), });

When the budget does run out, the thrown timeout carries the last transient failure as its cause, which is how you tell “the relayer is slow” from “we went blind”:

try { await waitForGaslessRequest(config, { requestId }); } catch (err) { if (err instanceof SymmError && err.code === "GASLESS_TERMINAL_TIMEOUT") { const wentBlind = err.cause !== undefined; } }

The wait resolves with the record for reverted / failed / rejected terminals — those are workflow outcomes, not transport failures. Branch on status; only succeeded is success. It throws only for transport-level problems: an exhausted budget, an abort, or a persistent fetch failure. On a timeout the request keeps running server-side — keep the requestId and keep watching; never re-submit the same intent through the wallet.

Parameters

requestIdstringrequired

Stable service tracking id returned by a gasless submit.

until"broadcast" | "terminal"default "terminal"optional

"broadcast" resolves as soon as a transaction hash exists; "terminal" waits for succeeded | reverted | failed | rejected.

service"operations" | "deposits"default "operations"optional

Which sub-service stores the request.

timeoutMsnumberdefault 120_000optional

Overall wait budget.

queuedPollMsnumberdefault 1_500optional

Poll cadence while queued.

submittedPollMsnumberdefault 3_000optional

Poll cadence after submitted.

streamSettleMsnumberdefault 1_000optional

How long to let the status stream settle before the first HTTP read. Ends early the moment the stream delivers, or reports that it will not. Ignored under transport: "poll".

streamStaleMsnumberdefault 15_000optional

How long a delivering stream may report nothing about the workflow before one HTTP read is taken anyway.

transport"auto" | "poll"default "auto"optional

"auto" prefers the status stream where the deployment enables it; "poll" forces HTTP polling. The outcome is identical either way.

signalAbortSignaloptional

Abort the wait (e.g. on unmount). The request itself keeps running server-side.

onUpdate(request: GaslessRequest) => voidoptional

Observer invoked with every fetched record, including the final one.

onTransportIssue(error: SymmError) => voidoptional

Observer invoked for every transient read failure the loop absorbed. Nothing is wrong with the workflow when it fires — only your view of it is stale.

chainIdnumberoptional

Target chain id. Defaults to the config’s defaultChainId.

Returns

Promise<GaslessRequest>

The record at broadcast (until: "broadcast") or at the terminal status — the same shape getGaslessRequest returns.

Throws

  • SymmError("api", "GASLESS_BROADCAST_TIMEOUT" | "GASLESS_TERMINAL_TIMEOUT") — the budget ran out before the wait target. cause is the last transient read failure, when the budget was spent going blind rather than waiting.
  • SymmError("api", "GASLESS_WAIT_ABORTED") — signal aborted.
  • Everything getGaslessRequest throws for a definitive answer, and after the 404 tolerance is exhausted. Transient transport failures are absorbed rather than thrown.
Last updated on