SwapCard
The swap composer over the V2 router — an honest getAmountsOut quote with its exact minimum received, and the approval and the swap kept as the two transactions they are.
import { SwapCard } from '@flarekit-dev/react-ui'
SwapCard is one currency context over a Uniswap V2 router — BlazeSwap on
Coston2, SparkDEX's V2 router on Flare. The price is a getAmountsOut read, the
floor is the quote's own minReceived, and the approval and the swap stay two
transactions, because that is what they are.
Live#
The preview runs the gallery's own states. The switcher walks the states the card was verified against, each built from the real swap state machine, so nothing here shows a state the card never reaches.
Swap
No limit-order venue is wired; Uniswap V2 has no native limit order.
import { dexFor } from '@flarekit-dev/contracts'
import { applyQuote, createSwap, quoteSwap, readAllowance, startQuoting } from '@flarekit-dev/core'
import { SwapCard } from '@flarekit-dev/react-ui'
import '@flarekit-dev/react-ui/styles.css'
const CHAIN = 114 // Coston2
const dex = dexFor(CHAIN) // the router, factory and swappable tokens
// reader is a viem PublicClient; owner is the address that pays and receives.
export async function quoteFxrpToUsdt(reader, owner) {
const now = Date.now()
const intent = {
fromKey: 'FXRP',
toKey: 'USDT0',
amountIn: 100_000000n, // raw units of the pay token
slippageBips: 50,
recipient: owner,
deadline: Math.floor(now / 1000) + 1200,
}
const { fromKey, toKey, amountIn, slippageBips } = intent
const result = await quoteSwap({ reader, chainId: CHAIN, fromKey, toKey, amountIn, slippageBips, now })
const allowance = await readAllowance(reader, CHAIN, fromKey, owner)
const quoting = startQuoting(createSwap({ chainId: CHAIN, intent, now }), now).record
// ready when the allowance covers the trade, awaiting_approval when it does not.
return applyQuote(quoting, { result, allowance, now }).record
}
export function Swap({ operation, result }) {
return (
<SwapCard
operation={operation}
quoteResult={result}
fromToken={dex.tokens.FXRP}
toToken={dex.tokens.USDT0}
networkLabel="Coston2"
onSubmit={signNextStep}
/>
)
}Usage#
The card is prop-driven and reads nothing itself. You quote, you read the
allowance, and the state machine turns that pair into the state the card
renders — ready when the allowance covers the trade, awaiting_approval when
it does not.
import { dexFor } from '@flarekit-dev/contracts'
import { applyQuote, createSwap, quoteSwap, readAllowance, startQuoting } from '@flarekit-dev/core'
import { SwapCard } from '@flarekit-dev/react-ui'
import '@flarekit-dev/react-ui/styles.css'
const dex = dexFor(114) // Coston2
export async function quoted(reader, intent, owner, now) {
const { fromKey, toKey, amountIn, slippageBips } = intent
const result = await quoteSwap({ reader, chainId: 114, fromKey, toKey, amountIn, slippageBips, now })
const allowance = await readAllowance(reader, 114, fromKey, owner)
const quoting = startQuoting(createSwap({ chainId: 114, intent, now }), now).record
return { operation: applyQuote(quoting, { result, allowance, now }).record, result }
}
export function Swap({ operation, result }) {
return (
<SwapCard
operation={operation}
quoteResult={result}
fromToken={dex.tokens.FXRP}
toToken={dex.tokens.USDT0}
networkLabel="Coston2"
onSubmit={signNextStep}
/>
)
}Props#
| Prop | Type | Default | Description |
|---|---|---|---|
| operationrequired | SwapOperation | — | The swap record. Its state drives the CTA, the note, the spine and whether the quote is still shown as live. |
| fromTokenrequired | DexToken | — | The pay token. It comes from the network's dex registry in @flarekit-dev/contracts, never a literal. |
| toTokenrequired | DexToken | — | The receive token, from the same registry. |
| quoteResult | SwapQuoteResult | — | The latest reading — a quote, a no_route with its reason, or an unavailable read. Drives the receive amount and the detail rows. |
| priceImpactBips | number | null | — | Price impact in bips, measured against a reference quote. Null or omitted renders an em dash, because an uncomputed impact is not zero impact. |
| amountInText | string | — | The pay leg's shown text while the reader is typing. Omit it and the leg shows the intent's own amount. |
| fromBalance | Amount | — | The holder's balance of the pay token, when read. Absence is unknown, so the row is simply not shown. |
| toBalance | Amount | — | The holder's balance of the receive token, when read. |
| mockLabel | string | — | A labelled mock kit's name, rendered as an explicit mock badge. Mock mode is declared on screen, never inferred. |
| networkLabel | string | — | The network the quote was read on, named in the header. |
| onAmountInChange | (text: string) => void | — | Called as the pay amount is typed. Omit it and the field is inert. |
| onFlip | () => void | — | Called when the reader switches the pay and receive sides. |
| onSelectFrom | () => void | — | Called when the pay token's picker is opened — this is where you mount a TokenSelector. |
| onSelectTo | () => void | — | Called when the receive token's picker is opened. |
| onMax | () => void | — | Called by the MAX affordance on the pay leg. The button appears only when this is given, so it never offers a fill the host cannot compute. |
| onSubmit | () => void | — | Called for the CTA — approve, review or re-quote, depending on state. The card signs nothing and broadcasts nothing itself. |
| 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: the order tabs, a pay leg and a receive leg with the flip control
between them, the note for the current state, and the quote details — rate,
minimum received, price impact, max slippage and route. Once the swap is in
flight the two steps render on the shared OperationTimeline spine, and each
transaction hash appears as its own labelled evidence chip, so the approval is
never folded into the swap.
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:
- quote — a real
getAmountsOutreading, with the rate, the exactminReceivedfloor and the measured price impact on screen. - needs approval — the allowance is short of the trade, so the plan carries an approve step and the CTA names the token it will approve.
- approving — the approval transaction is in flight; the spine shows it as the first of two steps.
- swapping — the approval is done and its hash is on the card; the swap transaction is submitted and its outcome is not known yet.
- success —
Swapped, with the approval and swap hashes as separate evidence. The receive amount reads—: the exact fill is recorded on the swap transaction, and the pre-trade quote is never redressed as a receipt. - no route — no pool pairs these tokens, stated with the reason. The output
is
—, never0, and there is nothing to sign. - slippage exceeded — the pool moved past the minimum received before the swap confirmed. The revert is atomic, so nothing moved; the state is distinct and re-quotable, not a failure of the kit.
- Limit — the tab is shown, disabled, and carries its reason: no limit-order venue is wired, and Uniswap V2 has no native limit order.
Mock to live#
The card never quotes; the reader does. createMockSwapReader() is a labelled
SwapReader that the real quoteSwap and readAllowance run against, priced
with the V2 constant-product curve over the actual Coston2 pool reserves — so
moving to live swaps the reader, not the screen.
// From this…
const reader = createMockSwapReader({ allowance: 0n })
// …to this. The component does not change.
const reader = createPublicClient({ chain: coston2, transport: http() })What it will not do#
It will not price a pair with no pool, and it will not show the quoted output as the amount received once the trade concludes. It will not collapse the approval into the swap, and it will not present a slippage revert as a failed swap — the revert is atomic, so it says so and offers a re-quote.