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

InstantLayer hooks

React-query wrappers over @symmio/trading-core’s InstantLayer slice.

The page is split into two groups:

  • Reads — useDelegationExpiry, useIsDelegationActive, useInstantTradeRequiredSelectors, useInstantOpenFees, useInstantCloseFees.
  • Writes — useGrantDelegation.

Delegation reads are intentionally split into two hooks. useDelegationExpiry reads the raw delegations mapping value, and useIsDelegationActive reads the contract’s computed active status.

Reads

useDelegationExpiry

Read the raw expiry timestamp for one account, delegate, and selector.

import { useDelegationExpiry } from "@symmio/trading-react"; const expiryQuery = useDelegationExpiry({ account, delegate, selector: "0x501e891f", }); if (expiryQuery.data) { console.log(expiryQuery.data.toString()); }

Parameters

NameTypeDefaultNotes
accountAddressrequiredAccount that owns the delegation.
delegateAddressrequiredDelegated signer being checked.
selectorHexrequiredFunction selector as bytes4.
chainIdnumber?config defaultOptional chain override.
queryobject?undefinedTanStack Query overrides.
configConfig?provider valueOptional config override.

Return type

UseQueryResult<bigint, SymmioRequestError>.

useIsDelegationActive

Read whether the contract considers a delegation active.

import { useIsDelegationActive } from "@symmio/trading-react"; const activeQuery = useIsDelegationActive({ account, delegate, selector: "0x501e891f", }); if (activeQuery.data === true) { console.log("Delegation is active"); }

Parameters

NameTypeDefaultNotes
accountAddressrequiredAccount that owns the delegation.
delegateAddressrequiredDelegated signer being checked.
selectorHexrequiredFunction selector as bytes4.
chainIdnumber?config defaultOptional chain override.
queryobject?undefinedTanStack Query overrides.
configConfig?provider valueOptional config override.

Return type

UseQueryResult<boolean, SymmioRequestError>.

Conditional UI

Because the read inputs are required, gate the hook-calling component until the UI has valid values.

function DelegationPanel({ account, delegate, selector }: Props) { if (!account || !delegate || !selector) { return <p>Enter account, delegate, and selector.</p>; } return <DelegationReads account={account} delegate={delegate} selector={selector} />; } function DelegationReads({ account, delegate, selector }: Required<Props>) { const expiryQuery = useDelegationExpiry({ account, delegate, selector }); const activeQuery = useIsDelegationActive({ account, delegate, selector }); return ( <> <p>Expiry: {expiryQuery.data?.toString()}</p> <p>Active: {String(activeQuery.data)}</p> </> ); }

useInstantTradeRequiredSelectors

The session-key delegation selector set for the connected (or given) chain, resolved by its contractsVersion: the open leg is sendQuote on a perps-core v0.8.6 chain and the legacy sendQuoteWithAffiliateAndData on v0.8.5, while the close and add-margin selectors are the same on both. Synchronous — it reads config, not the chain.

const selectors = useInstantTradeRequiredSelectors(); // or { chainId } to target another chain const grant = useGrantDelegation(); grant.mutate({ account, delegatedSigner: sessionKey, selectors, expiryTimestamp });

Use it (instead of hardcoding INSTANT_TRADE_REQUIRED_SELECTORS / LEGACY_INSTANT_TRADE_REQUIRED_SELECTORS) in any flow that can point at more than one chain — a hardcoded set grants and checks the wrong open-leg selector on the other generation’s chains. It returns the set in the order grantDelegation expects, and destructures cleanly for per-selector useIsDelegationActive probes: const [addMarginSelector, openLegSelector, closeSelector] = useInstantTradeRequiredSelectors().

useInstantOpenFees

Preview every fee a new instant-open quote pays at open — separated by leg plus the total — before the user submits. Read-only; nothing is signed. Wraps getInstantOpenFees, which mirrors the exact resolution and math prepareInstantOpenParams charges, so the preview equals the real open for the same inputs.

The result is a kind-discriminated union of the open-side legs only. Both kinds carry platformOpenFee, notional, the sized quantity, and totalFee; an "enigma" (lowcap) result adds openSolverFee, staticSolverFeeOpen, and expectedSettlementLoss. totalFee sums the open legs and includes the settlement provision by default — pass includeSettlementInTotalFee: false to sum the fee legs only (the provision stays available as its own field); on majors it is just platformOpenFee. staticSolverFeeOpen is a flat USD amount from the solver’s /info config (useSolverInfo) — size-independent, "0" when the solver publishes none; the hook caches this read internally. Pass solverInfo to override it with already-resolved data. An unavailable /info keeps the existing zero-static-fee fallback. Close fees are charged at close, not funded at open — preview them in the close flow with useInstantCloseFees, where the close notional and the holding-time solver rate are real (see Fees at close).

With fund: { mode: "full-balance", balance } instead of initialMargin (lowcap only), the hook previews a full-balance open: it runs the exact probe-and-rescale sizing the open runs, so the legs and quantity equal what the open will charge and submit. The extra inputs the sizing needs — the solver’s locked-param percents (useLockedParams) and the market’s quote constraints (from the same useMarkets read) — are prefetched through cached queries like every other input, so the query function stays pure math. In this mode data.quantity is the rescaled, lot-snapped size — feed it to TP/SL (see useInstantOpenWithTpSl) instead of re-deriving it from a typed figure.

const { data: fees, validationError, error, isReady, } = useInstantOpenFees({ subAccountAddress, market: { id: symbolId }, positionType, initialMargin, leverage, // slippage — required on majors; omit on lowcap to auto-derive }); // Display validationError ?? error first. // isReady means fees describe the current inputs. // fees?.totalFee — the open legs summed (settlement provision included) if (fees?.kind === "enigma") { // fees.openSolverFee, fees.staticSolverFeeOpen, fees.expectedSettlementLoss }

With initialMargin and availableBalance, the hook previews automatic full-balance fallback. Core compares required funding against the raw balance and returns data.fundingMode alongside the final data.quantity. Locked parameters and market constraints are prefetched for this mode too. Pass the same balance and estimate to preparation, fee preview, and submission; keep the typed input editable. Prefer the resolved mode and quantity over a local typed-margin cap check.

A typed initialMargin above availableBalance produces a synchronous validationError, including while offline, before requesting a price estimate or other prefills. Display validationError ?? error: error and isError describe the calculation query, while validationError describes invalid input. Invalid balance funding disables dependent queries; correcting the input clears validationError and resumes preparation. Explicitly disabled hooks and empty inputs stay idle without validation. Automatic fallback applies only when the typed amount fits but its required funding exceeds the balance.

isReady is true when validation passes, required inputs and the debounced estimate have settled, and the current calculation succeeds. Previous data may remain visible during recalculation; use it for the current order only when isReady is true. Pass estimatedOpenPrice: null for an estimate already attempted but unavailable, or omit it to let the hook request one. Both preparation and fees use the same unavailable-estimate fallback.

The query is disabled until a funding source exists — a non-empty initialMargin or a fund with a balance — and the funding value is part of the query key. Pass the raw input so validation stays immediate; estimate requests are debounced internally. Errors are normalized to SymmioRequestError (SLIPPAGE_REQUIRED on majors without a slippage, INVALID_SLIPPAGE on a malformed one, and in full-balance mode QUOTE_CONSTRAINT_VIOLATED when the sized quantity violates a published market constraint).

useInstantCloseFees

Preview every fee closing a position pays right now — the platform close fee plus, on lowcap (Enigma), the holding-time solver close fee and the flat static close leg. Read-only; nothing is signed. The close-flow counterpart of useInstantOpenFees, which deliberately previews only the open-side legs: the honest close cost exists only in the close flow, where the notional and the decaying solver rate are real. Wraps getInstantCloseFees.

The hook prefetches every input through its own cached queries and passes them in as prefills, so the query function is pure math with zero network hops: market metadata and the close-fee schedule (useMarkets), the on-chain platform fee rates (useFeeForUser), and the solver’s static fees (useSolverInfo, fail-soft) are slow-moving and cached for minutes; the mark price rides the shared price stream. Previous data is kept while inputs move, so a mark-price tick updates the numbers in place instead of flashing a loader.

The holding time is stamped each time the query function runs, so a refetch re-prices the decaying solver close fee as the position ages; pass now (unix seconds) for a deterministic preview. openedAt is typically the position’s createTimestamp; omit it and the schedule prices the worst case (holding 0, the peak rate).

const { data: fees } = useInstantCloseFees({ subAccountAddress: partyA, market: { id: symbolId }, quantity: quantityToClose, openedAt: position.createTimestamp, }); // fees?.totalFee; fees?.kind === "enigma" && fees.closeSolverFee

Parameters mirror the core action — subAccountAddress, market: { id, … } (optional pre-fetched metadata + schedule fields), the leveraged base-asset quantity to close, openedAt?, now?, the prefills markPrice? / feeRates? / solverInfo?, plus chainId / solverId, TanStack query overrides, and an optional config. The result is the same kind-discriminated union: both kinds carry platformCloseFee, notional, and totalFee; narrow on kind === "enigma" to reach closeSolverFee, closeSolverFeeRate, holdingSeconds, and staticSolverFeeClose. The query is disabled until a non-empty quantity exists; errors are normalized to SymmioRequestError (INVALID_TRADE_PARAMETERS on a non-positive quantity or unusable mark price).

Writes

useGrantDelegation

Submit an InstantLayer grantDelegation transaction — or relay it, with no native gas, where the chain runs in gasless execution mode. The transport comes from config, not from a different hook: the result is a transaction hash and its receipt either way, and the same reads are invalidated. Pass gasless: true in the mutation variables to force the relay for one call.

import { useGrantDelegation } from "@symmio/trading-react"; const { mutate, isPending, data, error } = useGrantDelegation(); <button disabled={isPending} onClick={() => mutate({ account: { addr: account, isPartyB: false }, delegatedSigner: delegate, selectors: ["0x501e891f"], expiryTimestamp: 1_766_000_000n, }) } > Grant </button>;

Options

interface UseGrantDelegationParameters { waitForReceipt?: boolean; // default true confirmations?: number; // default 1 }

Cache invalidation

On success, useGrantDelegation invalidates both read caches for the account and delegated signer:

  • getDelegationExpiryQueryKey
  • getIsDelegationActiveQueryKey

Mounted useDelegationExpiry and useIsDelegationActive hooks refetch independently.

useSessionKeySelectors

The selector set to grant a session key: the instant-trade lifecycle unioned with gasless account management, resolved for the connected chain’s contracts generation.

const selectors = useSessionKeySelectors(); const withWithdrawals = useSessionKeySelectors({ withdraw: true });

Returns readonly Hex[] synchronously — it is a memoized config read, not a query.

Parameters

interface UseSessionKeySelectorsParameters { chainId?: number; trade?: boolean; // default true account?: boolean; // default true withdraw?: boolean; // default false }

Requesting account or withdraw on a chain that is not perps-core 0.8.6 throws SESSION_KEY_SELECTORS_UNSUPPORTED_CHAIN, which will crash the rendering component. Gate the scope on useSupportsGaslessService, which tests the same condition:

const supported = useSupportsGaslessService(); const selectors = useSessionKeySelectors({ account: supported, withdraw: false });

withdraw is off by default because initiateWithdraw carries a caller-supplied receiver — see getSessionKeySelectors.

useAreDelegationsActive

Answers “can this key sign?” directly — the hook to reach for when rendering readiness.

const { allActive, missing, isLoading, refetch } = useAreDelegationsActive({ account: { addr: subAccount, isPartyB: false }, delegate: sessionKeyAddress, selectors: useSessionKeySelectors(), });

Parameters

accountInstantLayerAccountrequired

The account the delegation is enforced under. May be a sub-account or a virtual account — the underlying read canonicalizes a VA to its parent the way the contract does.

delegateAddressrequired

The delegated signer, typically the session key’s address.

selectorsreadonly Hex[]required

The selectors the key must hold. Normally the output of useSessionKeySelectors.

chainIdnumberoptional

Optional chain override.

queryQueryParameteroptional

TanStack overrides for the underlying read — use enabled while addresses are still resolving.

Return type

allActiveboolean

true only when every requested selector is active. It is false while loading, and false for an empty selectors list — a key holding no authority is never “ready”, and a vacuous true would green-light a flow that cannot sign.

missingreadonly Hex[]

Requested selectors the contract does not report as active. Until the read resolves this is the full requested set, so gate on isLoading before rendering it as a re-grant list.

activeSelectorsreadonly Hex[]

Requested selectors the contract does report as active.

expiryTimestampbigint | undefined

Earliest expiry across the matched delegations, in Unix seconds. Render as “usable until” — selectors granted separately expire at different times and the contract reports only the soonest.

isLoadingboolean

Whether the underlying read is in flight.

refetch() => Promise<...>

Re-read after a grant or revocation settles.

useActiveDelegations

The raw canonicalizing read, for probing several delegates at once. Prefer useAreDelegationsActive for the single-key readiness question.

const { data } = useActiveDelegations({ delegator: { addr: subAccount, isPartyB: false }, delegates: [sessionKeyAddress], selectors: [selectors], });

delegates and selectors are parallel arrays — see getActiveDelegations.

useRevocationCooldown

const { data: cooldownSeconds } = useRevocationCooldown();

The delay the two-step revocation enforces. Show it before a user starts a revocation, so they learn up front that a compromised key keeps signing for its duration.

It is the size of that window, not a countdown for any particular revocation — for that, use usePendingRevocation.

usePendingRevocation

Answers “is this key being revoked, and until when is it still live?” — the pending half of the two-step revocation, against a clock that ticks.

const { isRevoking, etaTimestamp, isFinalizable } = usePendingRevocation({ account: { addr: subAccount, isPartyB: false }, delegate: sessionKeyAddress, selectors: useSessionKeySelectors(), });

This is the only hook that shows a revocation in flight. useInitiateRevokeDelegation does not clear the grant — it schedules one, and the contract keeps enforcing the delegation until the ETA passes. useAreDelegationsActive therefore keeps reporting the key as ready for the whole cooldown, correctly, because it is. A UI built on readiness alone shows nothing at all after a successful revoke transaction.

Pair the two: useAreDelegationsActive answers “can it sign?”, this one answers “is it on its way out, and when does that land?”.

Probe the granted selectors, not the active ones

Pass the same set you hand to useAreDelegationsActive — the output of useSessionKeySelectors — not that hook’s activeSelectors. Enforcement stops the moment the ETA passes while the schedule survives until someone finalizes, so activeSelectors empties out exactly when isFinalizable should turn true. Feeding it back in here would disable a finalize button at the moment it becomes callable.

Parameters

accountInstantLayerAccountrequired

The account the delegation is enforced under. Pass the same account you revoked with — unlike useAreDelegationsActive, the underlying read does not canonicalize a virtual account to its parent, because neither does the mapping it reads.

delegateAddressrequired

The delegated signer, typically the session key’s address.

selectorsreadonly Hex[]required

Selectors to probe — the granted set.

chainIdnumberoptional

Optional chain override.

queryQueryParameteroptional

TanStack overrides for the underlying read — use enabled while addresses are still resolving.

Return type

isRevokingboolean

true while at least one probed selector has a scheduled revocation whose ETA has not passed — the key is still signing, but on its way out.

etaTimestampbigint | undefined

The latest ETA across the scheduled selectors, in Unix seconds — the moment the key stops being able to sign anything at all. undefined when no revocation is scheduled.

secondsRemainingnumber | undefined

Whole seconds left until etaTimestamp, recomputed every second and clamped at 0 once it lands. Render this as a live countdown — etaTimestamp is a fixed instant, so formatting it relatively ("in 9m") looks frozen for a minute at a time.

revokingSelectorsreadonly Hex[]

Probed selectors with a scheduled revocation, in the order requested. A selector stays in this list after its ETA passes, until someone finalizes — it is scheduled either way, only no longer enforced.

isFinalizableboolean

true when a revocation is scheduled and every scheduled ETA has passed, so useFinalizeRevokeDelegation will go through. Finalizing while any ETA is still in the future reverts with RevocationCooldownNotOver.

isLoadingboolean

Whether the underlying read is in flight.

refetch() => Promise<...>

Re-read after an initiate or finalize settles.

The clock ticks itself

The time comparison runs against a 1-second clock that starts only while something is actually counting down and stops itself once the last ETA passes. isRevoking flips to false and isFinalizable to true on their own, so a finalize button gated on them enables itself without a refresh.

That same clock drives secondsRemaining, so a countdown needs no timer of its own:

const { isRevoking, secondsRemaining } = usePendingRevocation({ account, delegate, selectors }); if (isRevoking) return <p>Still live — authority ends in {formatCountdown(secondsRemaining)}</p>;

The read is skipped for an empty selectors list unless query.enabled says otherwise. The core action is getPendingRevocationEtas.

useInitiateRevokeDelegation

const { mutate } = useInitiateRevokeDelegation(); mutate({ account: { addr: subAccount, isPartyB: false }, delegate: sessionKeyAddress, selectors });

Schedules the revocation. Callable by the owner, the delegate itself, or a REVOKER_ROLE holder.

useFinalizeRevokeDelegation

const { mutate } = useFinalizeRevokeDelegation(); mutate({ account: { addr: subAccount, isPartyB: false }, delegate: sessionKeyAddress, selectors });

Clears the stored grant once the cooldown ETA has passed. Permissionless — anyone may complete it.

It is optional. Authority ends by itself at the ETA, and useGrantDelegation clears any pending schedule for the selectors it grants, so this is never a prerequisite for re-granting. Offer it as cleanup, gated on usePendingRevocation’s isFinalizable, and only while revokingSelectors is non-empty.

Neither revoke hook accepts gasless, and that is contract-enforced, not an omission. Both target the Instant Layer, and _verifyOperation routes any self-targeted operation to _verifyGrantOperation, which accepts only grantDelegation / grantDelegations. A relayed revocation always reverts, so both are gas-paid wallet transactions — including when a session key revokes itself.

Cache invalidation

Both revoke hooks invalidate getDelegationExpiryQueryKey, getIsDelegationActiveQueryKey, getActiveDelegationsQueryKey, and getPendingRevocationEtasQueryKey for the account and delegate, so mounted readiness hooks — and usePendingRevocation — refetch on their own.

Last updated on