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.

mock kit

Stake to a validator

Coston2
Validator
Stake · C2FLR
Minimum stake50000.000000000000000000 C2FLR
Maximum stake200000000.000000000000000000 C2FLR
Lock window14–365 days
Staked0.000000000000000000 C2FLR
Mirrored vote power0.000000000000000000 VP

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#

PropTypeDefaultDescription
positionrequiredStakePositionViewThe 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.
nativeTokenrequiredDexTokenThe native token stakes are denominated in — on Coston2, `C2FLR` at 18 decimals. Every amount renders at full stored precision against it.
validatorsreadonly 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.
limitsStakeLimitsThe 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.
planResultStakePlanResultThe 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.
operationStakeOperationThe 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.
selectedNodeIdstringThe NodeID currently picked, matched against `validators`. Uncontrolled selection is not a thing here — the host owns the choice.
amountTextstringThe amount field, as typed. A string, not a number, so the digits a person entered are the digits that are planned against.
durationDaysnumberThe lock duration in whole days.
nativeBalanceAmountThe signer's native balance, when the host knows it. Rendered beside the amount field.
networkLabelstringThe network spelled out, e.g. `Coston2`. A testnet is a proper noun on screen, never a colour.
mockLabelstringLabels the card as running on mock reads. Explicit and host-supplied — mock mode is never entered by falling back.
onValidatorChange(nodeId: string) => voidCalled with the picked NodeID. Omitted or in flight, the picker is inert.
onAmountChange(text: string) => voidCalled with the raw text of the amount field.
onDurationChange(days: number) => voidCalled with the lock duration in days.
onSubmit() => voidCalled 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) => voidRecovery 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`.
classNamestringExtra 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,000 C2FLR minimum, 200,000,000 maximum, a 14365 day 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. stakeVerified is false, so planStake refuses to emit anything signable and the card shows the configured path with the submit disabled. false here 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, never succeeded; succeeded is entered only when readStakesOf shows the matching position.
  • position blank-slate0 staked and 0 mirrored vote power, both real reads.
  • mirror-unavailable0 staked, 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.