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

relayGaslessBatch

Relay several gasless actions as one request: one relay-instant submit, one atomic transaction the relayer broadcasts and pays gas for. Describe each action by name — the SDK encodes it, resolves its contract, sequences the nonces, prices the batch and asks for the signatures.

import { relayGaslessBatch, waitForGaslessRequest } from "@symmio/trading-core"; import { parseUnits } from "viem"; // Approve the gateway's fee budget and use it in the same request. const receipt = await relayGaslessBatch(config, { account: subAccount, calls: [ { functionName: "approveOperationalFeeWithMultiplier", args: [[gaslessLayerAddress], [parseUnits("5", 18)], [10_000n]], }, { functionName: "allocate", args: [parseUnits("100", 18)] }, ], }); const record = await waitForGaslessRequest(config, { requestId: receipt.requestId });

Each entry is one of three forms, freely mixed:

  • A relayable write by name — { functionName, args }. The SDK already knows every relayable write, so no ABI is needed; pass abi (the SDK’s symmioAbi, accountLayerAbi or instantLayerAbi) only if you hold it anyway. GASLESS_RELAYABLE_FUNCTIONS lists the names and their inputs.
  • Raw calldata of a relayable write — { data }, for arguments you encoded yourself with compile-time checks.
  • A GaslessWallet call list — { walletCalls, walletId }: calls the owner’s gasless wallet makes, as one operation. They run as the wallet contract, not as the SYMMIO account.

The target contract of a write is never passed: it is resolved from the encoded selector — the core diamond, the AccountLayer or the InstantLayer. A call whose selector is not a relayable write throws GASLESS_NOT_RELAYABLE before any network call; run anything else as a walletCalls entry.

What happens

  1. Every call is encoded and checked — nothing touches the network or the wallet yet.
  2. A session key’s delegations are checked before any prompt (execution.preflightDelegation, on by default). A missing selector throws GASLESS_SIGNER_NOT_DELEGATED, naming what to grant.
  3. Fresh nonces are read and sequenced per stream: the account’s InstantLayer nonces for the writes, each wallet id’s own nonces for its wallet entries.
  4. The batch is priced on-chain (execution.preflightFee, on by default) — the same preview getGaslessBatchFeeQuote returns. A quote the contract refuses throws before any prompt; a quote that fails for transport reasons does not block, because the relayer simulates the full transaction anyway.
  5. Each operation is signed, in order.
  6. One request carries them all, with one wallet id per operation — 0 for every write.

One signature per call. The protocol has no batch signature: the InstantLayer verifies each operation’s signature against that operation alone. A browser wallet therefore shows one prompt per call, in order; a session key passed as from signs them silently. Nothing is submitted until every prompt is signed, so rejecting one abandons the batch and consumes no nonce.

What a batch buys is shared fate — one revert rolls back every call and every fee — and one request to track. It also changes when the fee is due: the GaslessLayer collects fees after the whole batch executes, so an operational-fee approval early in the batch already covers the calls after it.

The account

account is the sub-account every operation runs under. It is each write’s signerAccount — whose InstantLayer nonces the batch consumes and whose delegations authorize a session key — and the billing account: a virtual account bills its parent. For a wallet entry it also selects the wallet: the GaslessLayer derives it from ownerOf(account), so a sub-account resolves to its owner’s wallets.

Margin writes run under the virtual account’s parent: pass the parent as account and the virtual account as the call’s argument, exactly as the transparent mode does.

Session keys

A session key signs a batch when it holds a delegation for every selector the batch runs, granted on account. getGaslessBatchSelectors builds that set from the calls — for a wallet entry it includes the wallet-execution sentinel.

await grantDelegation(config, { account: { addr: subAccount, isPartyB: false }, delegatedSigner: sessionKey, selectors: getGaslessBatchSelectors(calls), expiryTimestamp, }); await relayGaslessBatch(config, { account: subAccount, calls, from: sessionKey });

A batch that contains grantDelegation must be signed by the account’s owner. The InstantLayer rejects a delegate-signed grant, so no delegation makes it relayable by a session key.

Nonces and concurrency

The batch takes every nonce stream it signs on at once, so a concurrent relay on any of them — a transparent write, a gaslessWalletExecute, another batch — waits for it instead of signing the same nonce. As with every relay, the streams are released at the 202, and the next caller waits for the signed nonces lazily. See nonce streams.

The idempotency key is minted per call, because every call signs fresh operations. Replay a submit whose response was lost with resubmitGaslessRequest, never by signing the batch again.

A 202 is the point of no return: the workflow exists server-side and may execute even if polling later fails. Persist the receipt’s requestId, owner, walletIds and idempotencyKey immediately. React consumers use useRelayGaslessBatch, which confirms the batch before it resolves and refreshes what its calls changed.

Parameters

accountAddressrequired

The sub-account every operation runs under and is billed to. For margin writes, the virtual account’s parent.

callsreadonly GaslessBatchCall[]required

The calls, in execution order. An empty list throws GASLESS_EMPTY_BATCH.

calls[].functionNamestring

A relayable write’s name, with args and an optional abi.

calls[].dataHex

A relayable write as raw calldata.

calls[].walletCallsreadonly GaslessWalletCall[]

Calls the owner’s GaslessWallet makes, each raw { target, data } or { target, abi, functionName, args }.

calls[].walletIdbigintdefault 0noptional

Which of the owner’s GaslessWallets runs a walletCalls entry. Range 0n through 2^256 - 1.

operationTypestringoptional

Free-form label (1–128 characters) stored with the request. Defaults to the calls’ own names joined with + ("approveOperationalFee+allocate"), or "gaslessBatch" when that is too long. Fees are not keyed from it.

metadataRecord<string, unknown>optional

Non-secret client correlation data stored with the request.

fromAddressoptional

The signer to resolve the wallet client with — a session key, for example.

chainIdnumberoptional

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

Returns

Promise<GaslessSubmitReceipt>

The 202 acceptance, with walletIds holding one id per operation (0n for every write). The fields are the same as relayInstantOperations’ — persist requestId, owner, walletIds and idempotencyKey together.

Throws

  • SymmError("config", "GASLESS_NOT_CONFIGURED") / ("config", "GASLESS_UNSUPPORTED_CONTRACTS_VERSION").
  • SymmError("validation", "GASLESS_EMPTY_BATCH"), ("validation", "GASLESS_NOT_RELAYABLE") or ("validation", "GASLESS_WALLET_ID_INVALID"): thrown before any network call.
  • Viem’s encoding error when an ABI-level call’s arguments do not encode.
  • SymmError("config", "GASLESS_CONFIG_INCOHERENT"): the GaslessLayer verifies another InstantLayer than the chain config signs for.
  • SymmError("validation", "GASLESS_SIGNER_NOT_DELEGATED"): a session key is missing a selector. Grant getGaslessBatchSelectors(calls) on account.
  • SymmError("api", "GASLESS_FREE_QUOTA_EXHAUSTED"), ("validation", "GASLESS_FEE_QUOTE_REVERTED") or ("config", "GASLESS_LAYER_INTERFACE_UNSUPPORTED") from the fee pre-flight, before any prompt.
  • SymmError("api", "GASLESS_NONCE_STREAM_BUSY"): an earlier relay on one of the batch’s nonce streams is still unaccounted for.
  • SymmApiError with code GASLESS_RELAY_SUBMIT_FAILED on HTTP failure, and GASLESS_SUBMIT_UNCONFIRMED when the outcome could not be established — replay it with resubmitGaslessRequest.
  • The wallet’s own error when the user rejects a prompt. Nothing is submitted.
Last updated on