StakeCard
The staking composer — live-read validators and bounds, the irreversible stake lock stated in full before you sign, and a position that says "not read" rather than zero.
import { StakeCard } from '@flarekit-dev/react-ui'
StakeCard composes a P-chain stake: pick a validator, enter an amount and a
lock duration, and see the exact commitment before anything is signed. Staking
value-locks the amount for at least 14 days with no early exit, so the card
states the amount, the period and the unlock instant as data above the sign
affordance — not in a tooltip, and not after.
Live#
The preview runs the gallery's own states. The state switcher walks the eleven cases the card was verified against, so nothing here shows a state the composer never actually reaches.
Stake to a validator
import { parseAmount } from '@flarekit-dev/core'
import { useStaking, type UseStakingInput } from '@flarekit-dev/react'
import { StakeCard } from '@flarekit-dev/react-ui'
import '@flarekit-dev/react-ui/styles.css'
import { useState } from 'react'
const NATIVE = { symbol: 'C2FLR', address: '0x…', decimals: 18 }
export function Stake({ input }: { input: UseStakingInput }) {
const staking = useStaking(input)
const [nodeId, setNodeId] = useState('')
const [amountText, setAmountText] = useState('')
const [durationDays, setDurationDays] = useState(14)
const start = BigInt(Math.floor(Date.now() / 1000))
const planResult = nodeId
? staking.buildPlan({
nodeId,
amount: parseAmount(amountText || '0', 18, NATIVE.symbol).value,
startTime: start,
endTime: start + BigInt(durationDays) * 86_400n,
rewardAddress: input.account!,
})
: undefined
return (
<StakeCard
nativeToken={NATIVE}
position={staking.position}
validators={staking.validators}
limits={staking.limits}
planResult={planResult}
selectedNodeId={nodeId}
amountText={amountText}
durationDays={durationDays}
networkLabel="Coston2"
onValidatorChange={setNodeId}
onAmountChange={setAmountText}
onDurationChange={setDurationDays}
onSubmit={() => {
if (planResult?.ok) void staking.submit(planResult.plan)
}}
/>
)
}Usage#
StakeCard is prop-driven: it reads nothing itself. useStaking does the
keyless reads — the validator set, the bounds, the position — and the card
renders them.
import { useStaking } from '@flarekit-dev/react'
import { StakeCard } from '@flarekit-dev/react-ui'
import '@flarekit-dev/react-ui/styles.css'
export function Stake({ input }) {
const staking = useStaking(input)
return (
<StakeCard
nativeToken={{ symbol: 'C2FLR', address: '0x…', decimals: 18 }}
position={staking.position}
validators={staking.validators}
limits={staking.limits}
networkLabel="Coston2"
onSubmit={() => {}}
/>
)
}Props#
| Prop | Type | Default | Description |
|---|---|---|---|
| positionrequired | StakePositionView | — | The observed stake position — `observed` with its stakes and mirrored vote power, or `unavailable`. The two are rendered differently on purpose; an unavailable read is never drawn as a zero position. |
| nativeTokenrequired | DexToken | — | The native token stakes are denominated in — on Coston2, `C2FLR` at 18 decimals. Every amount renders at full stored precision against it. |
| validators | readonly ValidatorInfo[] | — | The validator set, live-read. No NodeID literal lives in the card, so with no validators passed it says none have been read rather than offering one. Each entry shows its NodeID and the end of its active window — the ceiling a stake may not cross. |
| limits | StakeLimits | — | The live-read bounds: minimum and maximum stake, minimum and maximum duration. Shown as soon as they land, so the constraints are visible before an amount is refused against them. |
| planResult | StakePlanResult | — | The result of `planStake` / `useStaking().buildPlan`. An `ok` plan carries the value lock the card discloses and is the only state whose submit button is enabled; a refusal carries the invariant that stopped it, rendered as the note and the button's label. |
| operation | StakeOperation | — | The in-flight or settled stake operation. While it is in flight the composer is read-only and the round-trip legs plus the wallet-signed spine appear beneath it. |
| selectedNodeId | string | — | The NodeID currently picked, matched against `validators`. Uncontrolled selection is not a thing here — the host owns the choice. |
| amountText | string | — | The amount field, as typed. A string, not a number, so the digits a person entered are the digits that are planned against. |
| durationDays | number | — | The lock duration in whole days. |
| nativeBalance | Amount | — | The signer's native balance, when the host knows it. Rendered beside the amount field. |
| networkLabel | string | — | The network spelled out, e.g. `Coston2`. A testnet is a proper noun on screen, never a colour. |
| mockLabel | string | — | Labels the card as running on mock reads. Explicit and host-supplied — mock mode is never entered by falling back. |
| onValidatorChange | (nodeId: string) => void | — | Called with the picked NodeID. Omitted or in flight, the picker is inert. |
| onAmountChange | (text: string) => void | — | Called with the raw text of the amount field. |
| onDurationChange | (days: number) => void | — | Called with the lock duration in days. |
| onSubmit | () => void | — | Called when a reader submits a plan the invariants accepted. The card holds no key and signs nothing — submitting is the host’s job. |
| onAction | (actionId: string) => void | — | Recovery actions from the operation spine, including the return leg once the lock matures. |
| theme | 'light' | 'dark' | — | Overrides the inherited theme. Normally left unset — the card follows `data-theme`. |
| className | string | — | Extra class on the outer element, so a host layout can place the card. |
What it renders#
One panel holding four things in order: the live-read validator picker, the
amount and duration composer with the live bounds beneath it, the value lock
once a plan exists, and the observed position. While an operation is in flight
it also renders the four conceptual legs of the round trip — export C→P, import
onto P, delegate to validator, return P→C after unlock — above the wallet-signed
OperationTimeline spine.
The staking reward is not here. It is the fourth kind on ClaimCard, and it is
the one claim kind that does not expire.
States#
Every state in the switcher above is imported from
packages/react-ui/gallery/, one source of truth for both the gallery and these
docs:
- discovery — the validator set and the bounds have landed, nothing composed
yet. The bounds shown are the ones read on Coston2:
50,000C2FLR minimum,200,000,000maximum, a14–365day window. - compose — a valid plan, so the value lock appears: the exact amount committed, the period, and the instant it unlocks. This preview runs under an explicit verified override, because the shipped build cannot produce it.
- not-verified — the shipped state.
stakeVerifiedisfalse, soplanStakerefuses to emit anything signable and the card shows the configured path with the submit disabled.falsehere means not proven on this build — no live stake round trip has been confirmed — not broken. - amount-below-min — under the live minimum, refused before a plan is built.
- duration-below-min — under the live
14-day floor, refused. - ends-after-validator — the stake would outlive the chosen validator's registration window, refused. The window is the ceiling, and it comes from the read.
- signing — the export leg is active, pre-broadcast. There is no transaction hash yet, so none is shown, and the position is unchanged.
- awaiting — Flare is recording the stake on the P-chain. A submitted
delegate is
submitted, neversucceeded;succeededis entered only whenreadStakesOfshows the matching position. - position blank-slate —
0staked and0mirrored vote power, both real reads. - mirror-unavailable —
0staked, mirrored vote power—. The two are separate reads, and the mirror reverting is an unknown, not a zero. - position-unavailable — the whole position read did not land, so both rows
are
—with a note saying the position is unknown rather than zero.
Those last three are three distinct renderings of what a naive surface would collapse into one. The card also refuses an amount above the maximum and a duration above the maximum; neither has a gallery case, because the live read pass never produced one.
Mock to live#
Nothing changes in the card. StakeCard renders whatever useStaking hands it,
and useStaking reads through the kit — so a mock read pass and a Coston2 read
pass differ in the input, not the screen. Addresses come from
@flarekit-dev/contracts; network is configuration.
// The mock: the reads observed on Coston2, replayed through the real read
// functions against fakes. Explicit, never a fallback.
const reads = await createMockStakeReads()
// Live: the same functions against a real client and P-chain RPC.
const staking = useStaking({ deployment, account, publicClient, rpcBase, executor })The mock carries the same stakeVerified: false the shipped deployment does. It
cannot be configured to hold an active stake position, because no live run
observed one.
What it will not do#
It will not offer a signable stake while the staking path is unverified, and it
will not reach succeeded from a submission — only from the position read. It
will not render an unavailable read as 0, invent a NodeID, or hide the lock
period behind the button that commits to it. It holds no key: signing happens
through the executor your host injects.