watchGaslessRequest
Follow one relayer workflow over the gateway’s status WebSocket: the subscribe snapshot, then every stored change, each carrying complete state.
import { watchGaslessRequest } from "@symmio/trading-core";
const unwatch = watchGaslessRequest(config, {
requestId,
onUpdate: ({ request, transactions }) => render(request, transactions),
onStatusChange: (status) => setPolling(status !== "live"),
});It opens a socket and nothing else. The stream is optional and off unless a deployment declares
gasless.statusStream.enabled, so pair it with the polling read and poll whenever the status is not "live". In
React, useGaslessRequest already does both — reach for this only in a non-React layer or a custom
status manager.
Availability
The gateway serves status WebSockets per protocol instance, disabled by default, and a staging confirmation does not cover production. Declare it per deployment:
gasless: {
url: "https://<gateway-origin>",
protocolInstance: "<protocol-instance>",
gaslessLayerAddress: "0x…",
statusStream: { enabled: true },
}When HTTP goes through your own proxy, the stream still needs the gateway itself — a proxy that forwards HTTP does not necessarily forward a WebSocket upgrade, and the stream is anonymous either way:
gasless: {
url: "/api/gasless",
protocolInstance: "<protocol-instance>",
statusStream: { enabled: true, origin: "https://<gateway-origin>" },
}Ask supportsGaslessStatusStream before subscribing; it answers false instead of throwing when the deployment cannot stream.
Parameters
requestIdstringrequiredThe stable id a gasless submit returned. Prefer it over a transaction hash, which may not exist yet and can change across replacement attempts.
service"operations" | "deposits"Which service stores the workflow. Defaults to "operations". An operations stream cannot carry deposits, or
another instance.
chainIdnumberChain override; defaults to the config’s default chain.
onUpdate(update: GaslessRequestStreamUpdate) => voidrequiredCalled with the subscribe snapshot (kind: "snapshot") and every later change (kind: "update"). Both carry the
complete record and its transaction attempts, never a patch — replace your state from each one. request is null
only when the gateway sent a record the SDK could not normalize; the poll fallback then refetches it.
onStatusChange(status: GaslessStreamStatus, detail: GaslessStreamStatusDetail | null) => voidCalled whenever the stream’s health changes. Poll while this is anything but "live".
onError(error: SymmError) => voidCalled on a stream-level problem (GASLESS_STREAM_NOT_FOUND, GASLESS_STREAM_INSTANCE_MISMATCH,
GASLESS_STREAM_DISABLED, …). The subscription stays; your HTTP reads are unaffected.
() => voidAn unwatch function. Call it on teardown; the shared socket closes shortly after its last watcher leaves.
Stream status
| Status | Meaning | Poll? |
|---|---|---|
"idle" | Nothing subscribed yet. | yes |
"connecting" | Dialing, or waiting for the ready handshake and this workflow’s snapshot. | yes |
"live" | Subscribed and delivering. | no |
"degraded" | Temporarily not delivering — reconnecting, throttled, or over the subscription cap. | yes |
"disabled" | Off for this config: streams disabled, unauthorized, misrouted, or a client bug. | yes |
detail.reason names the cause and detail.detail is one sentence naming what would change it, which is what a status badge should show.
What the SDK handles for you
- One socket per service and instance, shared by every watcher, with subscriptions replayed after each reconnect.
- The
readyhandshake is awaited before any command, and every frame is checked against the configured protocol instance — a mismatch disables the stream rather than trusting another deployment’s data. - Commands are paced and single-flight, well under the gateway’s 4 096-byte limit, because they share your request quota. The gateway’s throttle notice requeues the command instead of tearing down the socket.
- Close codes decide the next move: capacity and not-ready codes reconnect with a jittered 1 s → 30 s backoff (reset only after a snapshot), an unexplained close retries a bounded number of times, and
1008,1009,4001,4003and4004disable the stream for the config. - The accept race: a
NOT_FOUNDright after a202resubscribes a few times before giving up, because the gateway does not retain aNOT_FOUNDsubscription. - A terminal status unsubscribes itself, and a lost heartbeat (about every 20 s) forces a reconnect.
- A subscription outlives its last watcher by about two seconds, and a watcher that joins one which has already delivered is handed that record at once — the gateway sends its snapshot per subscribe, not per listener. Together they make the handoff between two waits on the same workflow free: no unsubscribe/subscribe round trip, and nothing to read over HTTP. The grace is given up immediately when another workflow is over the subscription cap and waiting for the slot.
supportsGaslessStatusStream
import { supportsGaslessStatusStream } from "@symmio/trading-core";
if (supportsGaslessStatusStream(config, { service: "deposits" })) {
/* subscribe */
}chainIdnumberChain override; defaults to the config’s default chain.
service"operations" | "deposits"Which service to check. Defaults to "operations".
booleanWhether watchGaslessRequest would resolve an endpoint. false for a chain with no gasless block, a deployment that
has not enabled the stream, or a proxy url with no statusStream.origin.
Related
getGaslessRequest— the polling read this pairs withwaitForGaslessRequest— awaits a terminal status, preferring the stream and polling when it is not liveuseGaslessRequest— the React hook that composes both