DelegationCard
The delegation composer — wrap the native token into WNat, delegate that vote power to at most two FTSO providers, and read the position the chain actually returned.
import { DelegationCard } from '@flarekit-dev/react-ui'
DelegationCard is the whole FTSO participation flow in one panel: the wrap leg
between the native token and WNat, up to two provider rows with their shares in
bips, and the position the last read observed. It plans through useDelegation
and signs nothing itself.
Live#
The preview runs the gallery's own states, built from the delegation round trip
observed on Coston2 on 2026-08-12. The state switcher walks the cases the
surface was verified against, so nothing here shows a state the composer never
actually reaches.
Delegate vote power
import { useDelegation } from '@flarekit-dev/react'
import { DelegationCard } from '@flarekit-dev/react-ui'
import { createMockDelegationAdapter } from '@flarekit-dev/core'
import '@flarekit-dev/react-ui/styles.css'
const adapter = createMockDelegationAdapter()
export function Delegate({ account, nativeToken, wrappedToken }) {
const [providers, setProviders] = useState([{ to: '', bips: 10000 }])
const { position, operation, buildPlan, submit } = useDelegation({
account,
adapter,
operation: undefined,
reconcile: (owner) => adapter.read(owner),
})
const planResult = buildPlan({ kind: 'delegate', targets: providers })
return (
<DelegationCard
position={position}
operation={operation}
planResult={planResult}
providers={providers}
nativeToken={nativeToken}
wrappedToken={wrappedToken}
networkLabel="Coston2"
onProviderChange={(index, patch) =>
setProviders((rows) => rows.map((row, n) => (n === index ? { ...row, ...patch } : row)))
}
onSubmit={() => planResult?.kind === 'plan' && submit(planResult.plan)}
/>
)
}Usage#
Drive it from useDelegation: the hook's position is the state panel, its
buildPlan is the refusal, and its submit hands the plan to your own wallet.
nativeToken and wrappedToken are DexTokens — the WNat one comes from
@flarekit-dev/contracts (dexFor(chainId).tokens.WNAT), never a literal in your
screen.
import { useDelegation } from '@flarekit-dev/react'
import { DelegationCard } from '@flarekit-dev/react-ui'
import { createMockDelegationAdapter } from '@flarekit-dev/core'
import '@flarekit-dev/react-ui/styles.css'
const adapter = createMockDelegationAdapter()
export function Delegate({ account, nativeToken, wrappedToken }) {
const [providers, setProviders] = useState([{ to: '', bips: 10000 }])
const { position, operation, buildPlan, submit } = useDelegation({
account,
adapter,
operation: undefined,
reconcile: (owner) => adapter.read(owner),
})
const planResult = buildPlan({ kind: 'delegate', targets: providers })
return (
<DelegationCard
position={position}
operation={operation}
planResult={planResult}
providers={providers}
nativeToken={nativeToken}
wrappedToken={wrappedToken}
networkLabel="Coston2"
onProviderChange={(index, patch) =>
setProviders((rows) => rows.map((row, n) => (n === index ? { ...row, ...patch } : row)))
}
onSubmit={() => planResult?.kind === 'plan' && submit(planResult.plan)}
/>
)
}Props#
| Prop | Type | Default | Description |
|---|---|---|---|
| positionrequired | DelegationPositionView | — | The observed wrap/delegate/mode position from `useDelegation`. It carries its own status: `observed` renders the values, `unavailable` renders `—`. There is no way to hand this component a confident zero it did not read. |
| nativeTokenrequired | DexToken | — | The gas token being wrapped — `C2FLR` on Coston2. Its symbol names the wrap leg and the wrap-first note. |
| wrappedTokenrequired | DexToken | — | The WNat token vote power lives on — `WC2FLR` on Coston2. Its decimals set the precision every balance renders at. |
| operation | DelegationOperation | — | The in-flight wrap or delegate. Its state owns the card: while it is in flight the composer locks and the lifecycle spine appears. |
| planResult | DelegationPlanResult | — | The result of `buildPlan`. A plan enables the submit; an error is rendered as the refusal that names it, before any call is built. |
| wrapDirection | 'wrap' | 'unwrap' | 'wrap' | Which leg the composer is on. `unwrap` shows the full wrapped balance and is not editable — the reconciler cannot confirm a partial unwrap without a baseline, so no partial affordance ships. |
| amountText | string | — | The wrap amount as typed. Controlled by the host, so the field survives a re-render mid-entry. |
| nativeBalance | Amount | — | The payer's native balance, when the host knows it. Shown against the wrap leg. |
| providers | readonly DelegationProviderRow[] | [] | The delegate rows — zero, one or two. Each is `{ to, bips? }` in percentage mode, or `{ to, amount? }` in amount mode. Rows present but all empty is never submittable: the card refuses to emit a no-op `batchDelegate([], [])`. |
| delegateMode | 'percentage' | 'amount' | 'percentage' | Which per-provider entry the composer offers. Amount mode renders each share read-only — that path was never driven live, so it displays rather than accepts input. |
| mockLabel | string | — | The mock-mode label, shown as a chip on the panel header. Present only when the host is in mock mode; never a fallback. |
| networkLabel | string | — | The network the position was read on, shown on the header. Network is configuration, so this is a label, not a switch. |
| onDirectionChange | (direction: 'wrap' | 'unwrap') => void | — | Called when the reader switches the wrap leg. Ignored while an operation is in flight. |
| onAmountChange | (text: string) => void | — | Called with the wrap amount as typed. Wired only on the wrap leg; unwrap has no partial amount to change. |
| onProviderChange | (index: number, patch: Partial<DelegationProviderRow>) => void | — | Called with the row index and the changed field. Omit it and the provider rows are read-only. |
| onAddProvider | () => void | — | Called when the reader adds a row. The add affordance disappears at two rows — the protocol cap, enforced in the UI rather than at submit. |
| onSubmit | () => void | — | Called when the reader submits the current plan. The card holds no key and broadcasts nothing; signing is the host's job. |
| theme | 'light' | 'dark' | — | Overrides the inherited theme. Normally left unset — the widget 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: the wrap/unwrap leg, the observed position
(wrapped balance, vote power, and one row per delegate carrying its bips and
percentage), the provider rows with the account's current on-chain delegation
mode, and — once an operation is in flight — the shared OperationTimeline
spine. Every exact value renders in the mono face carrying its asset at full
precision.
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:
- compose — the real observed position:
5.000000000000000000 WC2FLRdelegated10000bips (100%) to one provider, modePERCENTAGE. - no-balance — a real observed-empty holding: nothing wrapped, no delegates. A read that landed and returned nothing.
- needs-wrap — a delegate is being composed with no wrapped balance behind it. Vote power comes from WNat, so the wrap comes first.
- wrapping — the wrap call is in flight.
- delegating — the delegate call is in flight.
- submitted — broadcast, with the transaction hash as evidence. The
delegatesOfread is still pending, so this is not success. - awaiting — waiting for Flare to record the delegation.
- succeeded — reached only once
delegatesOfreflects the target. - unavailable — the last read did not land. Every value renders
—: the position is unknown, not zero, and this is never collapsed intono-balance. - too-many-delegates — the two-provider cap, refused before a call is built.
- bips-over-100 — the shares sum past
10000bips, refused before a call is built. - mode-conflict — the account is already in the other delegation mode, so the
card states the delegation style is fixed for the account rather than
submitting a call the chain ignores —
delegationModeOfnever resets, not even after undelegating. - not-verified — the delegation path is declared unverified on this build and the kit will not sign against it.
Mock to live#
The card is prop-driven, so moving from the mock to a live network swaps the
adapter behind useDelegation, not the screen. The WNat address comes from
@flarekit-dev/contracts; network is configuration.
// From this…
const adapter = createMockDelegationAdapter()
// …to this. The component does not change.
const adapter = makeDelegationAdapter(publicClient, delegationFor('coston2'))What it will not do#
It will not render an unavailable read as zero delegation, and it will not submit a delegate with no provider filled in. It will not offer a partial unwrap, because the reconciler cannot confirm one. And it will not switch you between percentage and amount delegation behind your back — the two are exclusive on chain and the choice never resets, so a conflict says the style is fixed for this account instead of quietly doing nothing.