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
| Name | Type | Default | Notes |
|---|---|---|---|
account | Address | required | Account that owns the delegation. |
delegate | Address | required | Delegated signer being checked. |
selector | Hex | required | Function selector as bytes4. |
chainId | number? | config default | Optional chain override. |
query | object? | undefined | TanStack Query overrides. |
config | Config? | provider value | Optional 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
| Name | Type | Default | Notes |
|---|---|---|---|
account | Address | required | Account that owns the delegation. |
delegate | Address | required | Delegated signer being checked. |
selector | Hex | required | Function selector as bytes4. |
chainId | number? | config default | Optional chain override. |
query | object? | undefined | TanStack Query overrides. |
config | Config? | provider value | Optional 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.closeSolverFeeParameters 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:
getDelegationExpiryQueryKeygetIsDelegationActiveQueryKey
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
accountInstantLayerAccountrequiredThe 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.
delegateAddressrequiredThe delegated signer, typically the session key’s address.
selectorsreadonly Hex[]requiredThe selectors the key must hold. Normally the output of useSessionKeySelectors.
chainIdnumberoptionalOptional chain override.
queryQueryParameteroptionalTanStack overrides for the underlying read — use enabled while addresses are still resolving.
Return type
allActivebooleantrue 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 | undefinedEarliest 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.
isLoadingbooleanWhether 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
accountInstantLayerAccountrequiredThe 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.
delegateAddressrequiredThe delegated signer, typically the session key’s address.
selectorsreadonly Hex[]requiredSelectors to probe — the granted set.
chainIdnumberoptionalOptional chain override.
queryQueryParameteroptionalTanStack overrides for the underlying read — use enabled while addresses are still resolving.
Return type
isRevokingbooleantrue 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 | undefinedThe 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 | undefinedWhole 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.
isFinalizablebooleantrue 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.
isLoadingbooleanWhether 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.