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.

mock kit

Delegate vote power

Coston2
Wrap · C2FLR
Wrapped balance5.000000000000000000 WC2FLR
Vote power0.000000000000000000 VP
0xB63C…8B5D10000 bips · 100.00%
Delegate toPERCENTAGE

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#

PropTypeDefaultDescription
positionrequiredDelegationPositionViewThe 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.
nativeTokenrequiredDexTokenThe gas token being wrapped — `C2FLR` on Coston2. Its symbol names the wrap leg and the wrap-first note.
wrappedTokenrequiredDexTokenThe WNat token vote power lives on — `WC2FLR` on Coston2. Its decimals set the precision every balance renders at.
operationDelegationOperationThe in-flight wrap or delegate. Its state owns the card: while it is in flight the composer locks and the lifecycle spine appears.
planResultDelegationPlanResultThe 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.
amountTextstringThe wrap amount as typed. Controlled by the host, so the field survives a re-render mid-entry.
nativeBalanceAmountThe payer's native balance, when the host knows it. Shown against the wrap leg.
providersreadonly 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.
mockLabelstringThe mock-mode label, shown as a chip on the panel header. Present only when the host is in mock mode; never a fallback.
networkLabelstringThe 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') => voidCalled when the reader switches the wrap leg. Ignored while an operation is in flight.
onAmountChange(text: string) => voidCalled 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>) => voidCalled with the row index and the changed field. Omit it and the provider rows are read-only.
onAddProvider() => voidCalled 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() => voidCalled 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.
classNamestringExtra 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 WC2FLR delegated 10000 bips (100%) to one provider, mode PERCENTAGE.
  • 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 delegatesOf read is still pending, so this is not success.
  • awaiting — waiting for Flare to record the delegation.
  • succeeded — reached only once delegatesOf reflects the target.
  • unavailable — the last read did not land. Every value renders : the position is unknown, not zero, and this is never collapsed into no-balance.
  • too-many-delegates — the two-provider cap, refused before a call is built.
  • bips-over-100 — the shares sum past 10000 bips, 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 — delegationModeOf never 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.