useStaking
The cross-substrate stake lifecycle as React state — live bounds and validators read keylessly, the value lock disclosed before signing, and succeeded only from the P-chain position read.
import { useStaking } from '@flarekit-dev/react'
A Flare stake is a round trip between two substrates: FLR is exported from the
C-chain, imported onto the P-chain, delegated to a validator, and much later
comes back the same way. The outcome lives on the P-chain, not in a transaction
receipt — so a submitted stake is submitted, and nothing else, until
readStakesOf shows the position. useStaking re-reads on an interval and
advances the operation from that read. There is no Resume button.
Reading and planning are keyless. The bounds a stake must satisfy — the minimum
and maximum amount, the minimum and maximum duration — are read from
PChainStakeMirrorVerifier and the live validator set, never typed into the
kit. Signing happens only through the PChainStakeExecutor you inject; the hook
receives it as an argument and never holds a key itself.
Live#
The readout below is the hook's actual return value on this render. This page
has no network, so the hook is handed no viem client, no P-chain RPC base and no
executor — and what it reports is the point. The position is unavailable
rather than a confident zero stake. limits and validators are null rather
than the numbers this page quotes below. buildPlan returns nothing rather than
validate an intent against bounds it has not read. Absence of a reader is never
absence of a stake.
// reads on mountRead from the running hook against the mock kit, on this render.
import { mockStakingDeployment } from '@flarekit-dev/core'
import { useStaking } from '@flarekit-dev/react'
import { StakeCard } from '@flarekit-dev/react-ui'
const deployment = mockStakingDeployment() // live: stakingFor('coston2')
export function Stake({ account, executor, publicClient, intent }) {
const { position, limits, validators, reward, buildPlan, submit } = useStaking({
deployment,
account,
publicClient, // keyless: limits, mirrored vote power, reward
rpcBase: deployment.pChainRpcBase, // keyless: the validator set
executor, // the ONLY signing seam; absent = read and plan only
})
const planResult = buildPlan(intent) // undefined until the reads land
return (
<StakeCard
position={position}
limits={limits}
validators={validators}
planResult={planResult}
onSubmit={() => planResult?.ok && submit(planResult.plan)}
/>
)
}The keyless read pass on Coston2 on 2026-08-12 returned a minimum stake of
50,000 FLR, a maximum of 200,000,000 FLR, and a duration window of 14 to
365 days. Those are values that were read on that day, on that network — the
hook reads them again every poll, and a surface built on this kit should render
what it read, not what is written here.
Usage#
import { stakingFor } from '@flarekit-dev/contracts'
import { useStaking } from '@flarekit-dev/react'
import { StakeCard } from '@flarekit-dev/react-ui'
const deployment = stakingFor('coston2')
function Stake({ account, executor, intent }) {
const { position, limits, validators, reward, operation, buildPlan, submit } = useStaking({
deployment,
account,
publicClient, // keyless: bounds, mirrored vote power, reward
rpcBase: deployment.pChainRpcBase, // keyless: the validator set
executor, // the only signing seam; omit for read and plan only
})
const planResult = buildPlan(intent) // undefined until limits and validators land
return (
<StakeCard
position={position}
limits={limits}
validators={validators}
operation={operation}
planResult={planResult}
onSubmit={() => planResult?.ok && submit(planResult.plan)}
/>
)
}Parameters#
| Prop | Type | Default | Description |
|---|---|---|---|
| deploymentrequired | StakingDeployment | undefined | — | The staking deployment. Carries the EVM addresses, the P-chain RPC base and stakeVerified — the gate planStake runs before any bound. |
| accountrequired | 0x${string} | undefined | — | The C-chain account rewards accrue to and mirrored vote power is read for. |
| publicClientrequired | StakeEvmClient | undefined | — | A keyless viem client for the EVM reads: the verifier bounds, the mirrored vote power and the staking reward. |
| rpcBaserequired | string | undefined | — | The P-chain JSON-RPC base for the keyless validator-set and stake-position reads. |
| executor | PChainStakeExecutor | — | The P-chain signing seam — a wallet or an agent key. Without it the hook reads and plans, but cannot sign and cannot read the position (which needs the executor’s P-address). |
| operation | StakeOperation | — | The current stake-in (staking) or delayed return (staking-return) operation. Only a genuinely new id replaces what the hook is tracking. |
| pollMs | number | 15_000 | Poll cadence in milliseconds. The host owns the clock. |
Return type#
| Prop | Type | Default | Description |
|---|---|---|---|
| operation | StakeOperation | undefined | — | The tracked operation, advanced only by a successful position read. |
| isSettled | boolean | — | True once the operation reaches a terminal state. False when there is no operation at all. |
| position | StakePositionView | — | observed (stakes, mirroredVotePower) or unavailable. An absent P-chain read is never a confident zero stake. |
| limits | StakeLimits | undefined | — | The live bounds read from PChainStakeMirrorVerifier: minAmount and maxAmount in wei, minDuration and maxDuration in seconds. undefined before the read lands. |
| validators | ValidatorInfo[] | undefined | — | The live validator set, each with its node id, active window and weight. undefined before the read lands. |
| reward | StakingRewardState | undefined | — | The NON-EXPIRING staking reward (total, claimed, claimable). undefined before the read lands or when the read was unavailable — never a fabricated zero. |
| error | SerializedError | undefined | — | The last failed READING. It never moves the operation to failed. |
| buildPlan | (intent: StakeIntent) => StakePlanResult | undefined | — | Keyless, pure and synchronous once limits and validators have landed; undefined before that. Returns { ok: true, plan } or { ok: false, error }. |
| submit | (plan: StakePlan) => Promise<StakeOperation | undefined> | — | Signs the two P-chain legs through the injected executor and adopts a SUBMITTED operation. Returns undefined without an executor. |
States#
position.status: 'unavailable'— no P-chain read has landed, or the read threw. It renders as an unknown, never as "staked 0".position.status: 'observed'— a real read, including one that observes no stakes. Inside an observed position,mirroredVotePowermay still beundefined: thePChainStakeMirrorread reverts on Coston2, and that staysundefinedrather than collapsing to zero.- the value lock — a successful plan carries
valueLockwith the amount, the unlock time, the lock duration andirreversible: true. The stake cannot be exited before it unlocks, and that is stated as data so a surface can disclose it before anything is signed. - refusals —
buildPlanreturns{ ok: false }forunverified(checked first, before any bound),amount_below_min,amount_above_max,duration_below_min,duration_above_max— each carrying the live-read bound it failed — andends_after_validator, which carries the validator's own end time. A stake that would outlive its validator is refused rather than stranded. - the two legs — the stake-in operation (
staking) reachessucceededonly oncereadStakesOfshows the matching position. The delayed P→C return is its own operation (staking-return), and its terminalsucceededrequires a successful read showing the stake is gone. unavailableon the return leg — a thrown read isunavailable, neverabsent. Silence from the P-chain leaves the operation where it was, because treating a transport fault as "the stake is gone" would reportsucceededwith funds still on the P-chain.
Mock to live#
The mock is read-shaped rather than adapter-shaped, so the swap is the readers:
// Mock: run every M11 read against the observed Coston2 snapshot, no network.
const { deployment, limits, validators, positions, stakingReward } = await createMockStakeReads()
// Live: the shipped deployment plus your own keyless clients.
useStaking({ deployment: stakingFor('coston2'), publicClient, rpcBase: deployment.pChainRpcBase, account, executor })mockStakingDeployment() returns the shipped deployment unchanged, with
stakeVerified still false — no broadcast ever landed, because the signer held
far less than the read minimum. The mock does not simulate the resulting
refusal; it simply exposes no verified deployment, and the real planStake gate
does the rest.
What it will not do#
It will not sign, and it will not hold a key. Both signed legs go through the
executor you inject: the C→P transfer and the AddPermissionlessDelegator
delegate. Everything else — bounds, validators, reward, position, plan — is
keyless.
It will not report succeeded from a submission. Only the confirmed position
read does that, on either leg, and an unavailable read is never read as an
absent stake.
It will not plan against typed-in bounds. Until the limits and validator reads
land, buildPlan returns undefined rather than validate an amount or a
duration against a constant that may no longer be what the network enforces.