@liberfi.io/ui-trade
v0.1.26
Published
Trade hooks for Liberfi React SDK
Readme
@liberfi.io/ui-trade
Trade hooks and widgets for the Liberfi React SDK. This package provides chain-agnostic swap logic and ready-to-use trade form components that work across Solana, Ethereum, and BSC.
The package is organized in three layers:
- Hooks (
useSwap,useSwapRoutePolling,useTxConfirmation) — pure-logic building blocks with no UI side-effects, designed for IoC. - Swap Widget (
SwapWidget/useSwapScript/SwapUI/SwapPreviewModal) — a full swap form with preview-before-confirm flow, following the Script/Widget/UI three-layer architecture, composable at any level. - Instant Trade (
InstantTradeWidget/InstantTradeProvider/MultiChainPresetFormWidget/PresetFormWidget) — a configurable quick-trade form with buy/sell tabs, preset management, customizable quick-amount buttons, and trade settings (slippage, priority fee, tip fee, anti-MEV). Includes list-optimised variants (InstantTradeSwapProvider/InstantTradeListButtonWidget) that share a single set of context subscriptions across N buttons. - State atoms (
presetAtomFamily/instantTradeAmountAtomFamily) — Jotai-based persistent state for trade presets and amount/preset selection, storage-backend agnostic viaatomWithStorage.
Design Philosophy
- IoC (Inversion of Control) — Side-effects (toast, analytics, navigation) are injected via callbacks. Hooks never hardcode any feedback mechanism.
- Script/Widget/UI architecture — The swap form follows the monorepo's three-layer pattern:
useSwapScript(data + state),SwapWidget(orchestration),SwapUI(presentation). Consumers can use the widget directly or compose Script + custom UI. - Chain-agnostic — Works across Solana (Jupiter) and EVM chains (KyberSwap). Chain-specific differences (dex selection, tx format) are handled by the underlying
@liberfi.io/clientandWalletAdapterabstractions. - UI via
@liberfi.io/ui— Uses the shared UI library (Button,Input,Avatar,Modal, etc.) for consistent styling across the monorepo. No direct@heroui/reactdependency. - Layered architecture — Builds on
@liberfi.io/react(data layer),@liberfi.io/wallet-connector(signing layer), and@liberfi.io/ui(presentation layer) without duplicating their logic.
Installation
pnpm add @liberfi.io/ui-tradePeer dependencies the consumer must provide:
react(>=18)react-dom(>=18)jotai(>=2.15.1) — used for persistent preset state viaatomWithStorage@liberfi.io/react(providesDexClientProvideranduseDexClient)@liberfi.io/ui(provides UI components:Button,Input,Modal,Avatar, etc.)@liberfi.io/ui-chain-select(providesChainSelectMobileUIforMultiChainPresetFormWidget)@liberfi.io/wallet-connector(providesWalletAdaptertype)
API Reference
Hooks
useSwap(options?: UseSwapOptions)
Orchestrates the full swap flow: route -> sign -> send.
Parameters:
| Name | Type | Description |
| --------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------- |
| options.onSubmitted | (result: SwapResult) => void | Called when the transaction is successfully submitted on-chain. |
| options.onError | (error: Error, phase: SwapPhase) => void | Called when an error occurs. phase indicates where it failed: "route", "sign", or "send". |
Returns: { swap, isSwapping }
| Name | Type | Description |
| ------------ | ------------------------------------------- | ---------------------------------------------------------------------------- |
| swap | (input: SwapInput) => Promise<SwapResult> | Executes the swap. Resolves with SwapResult on success, throws on failure. |
| isSwapping | boolean | Whether a swap is currently in progress. |
useSwapRoutePolling(params, options?)
Polls for swap route quotes at a configurable interval. Built on useSwapRouteQuery (TanStack Query).
Parameters:
| Name | Type | Description |
| ------------------ | ------------------------- | ---------------------------------------------------------------------- |
| params | Partial<API.SwapParams> | Route parameters. Polling starts when all required fields are present. |
| options.interval | number | Polling interval in ms. Defaults to 12000. |
| options.paused | boolean | Pause polling (e.g. during swap execution). |
| options.onError | (error: Error) => void | Called when a route fetch fails. |
Returns: { route, isRouting, error }
| Name | Type | Description |
| ----------- | ---------------------------- | --------------------------------- |
| route | API.SwapRoute \| undefined | Current route quote. |
| isRouting | boolean | Whether a route is being fetched. |
| error | Error \| null | Latest route fetch error. |
useTxConfirmation(options?: UseTxConfirmationOptions)
Tracks transaction confirmation via backend SSE. Designed to compose with useSwap.
Parameters:
| Name | Type | Description |
| --------------------- | ---------------------------------------- | -------------------------------------------------------- |
| options.onConfirmed | (txHash: string) => void | Called when a tracked transaction is confirmed on-chain. |
| options.onFailed | (txHash: string, error: Error) => void | Called when confirmation fails or times out. |
| options.timeout | number | SSE timeout in milliseconds. Defaults to 60000. |
Returns: { track, clear, clearAll, transactions }
| Name | Type | Description |
| -------------- | ---------------------------------------- | --------------------------------------------------- |
| track | (chain: Chain, txHash: string) => void | Start tracking a transaction's confirmation. |
| clear | (txHash: string) => void | Remove a single transaction from tracking. |
| clearAll | () => void | Remove all tracked transactions. |
| transactions | Map<string, TrackedTx> | All tracked transactions with their current status. |
Components (Swap Widget)
The swap widget follows the Script / Widget / UI three-layer architecture.
SwapWidget
Plug-and-play swap form. Calls useSwapScript internally and renders SwapUI with SwapPreviewModal. Clicking the swap button opens a preview modal; confirming in the modal executes the swap.
<SwapWidget
chain={Chain.SOLANA}
from="So11111111111111111111111111111111111111112"
to="EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
onComplete={(result) => console.log(result)}
className="my-swap"
/>Props (SwapWidgetProps):
| Name | Type | Description |
| ------------- | --------------------------------------------------------- | ----------------------------- |
| chain | Chain | Target chain. |
| from? | string | Initial from-token address. |
| to? | string | Initial to-token address. |
| onComplete? | (result: { success: boolean; txHash?: string }) => void | Called when swap completes. |
| className? | string | External style customization. |
useSwapScript(params: UseSwapScriptParams)
Script layer hook. Encapsulates all data fetching, state management, and swap execution. No JSX, no UI imports. Use this to build a custom swap UI.
Parameters (UseSwapScriptParams):
| Name | Type | Description |
| ------------- | --------------------------------------------------------- | --------------------------- |
| chain | Chain | Target chain. |
| from? | string | Initial from-token address. |
| to? | string | Initial to-token address. |
| onComplete? | (result: { success: boolean; txHash?: string }) => void | Called when swap completes. |
Returns (UseSwapScriptResult):
| Name | Type | Description |
| --------------------- | ---------------------------------- | -------------------------------------------- |
| fromTokenAddress | string | Currently selected from-token address. |
| toTokenAddress | string | Currently selected to-token address. |
| setFromTokenAddress | (addr: string) => void | Update from-token address. |
| setToTokenAddress | (addr: string) => void | Update to-token address. |
| fromToken | Token \| null | From-token metadata. |
| toToken | Token \| null | To-token metadata. |
| fromBalance | Portfolio \| null | User's from-token balance. |
| toBalance | Portfolio \| null | User's to-token balance. |
| amount | string \| undefined | Human-readable amount. |
| setAmount | (v: string \| undefined) => void | Update amount. |
| setHalfAmount | () => void | Set amount to half of from-token balance. |
| setMaxAmount | () => void | Set amount to full from-token balance. |
| amountInDecimals | string \| undefined | Amount in smallest unit. |
| amountInUsd | string \| undefined | Amount in USD. |
| outputAmount | string \| undefined | Formatted output amount from route. |
| outputAmountInUsd | string \| undefined | Output amount in USD. |
| route | API.SwapRoute \| undefined | Current route quote. |
| isRouting | boolean | Whether a route is being fetched. |
| routeError | Error \| null | Route fetch error. |
| swap | () => Promise<void> | Execute the swap. |
| isSwapping | boolean | Whether a swap is in progress. |
| txStatus | TxConfirmationStatus | Confirmation status of the last transaction. |
| isLoading | boolean | Whether initial data is loading. |
SwapUI
Pure presentational component for the swap form. Renders the From/To token inputs with token selector buttons, balance display, Half/Max shortcuts, and a preview button. Styled with @liberfi.io/ui components and Tailwind CSS.
Props (SwapUIProps):
| Name | Type | Description |
| ------------------- | ---------------------------------- | --------------------------------- |
| fromToken | Token \| null | From-token metadata. |
| toToken | Token \| null | To-token metadata. |
| fromBalance | Portfolio \| null | User's from-token balance. |
| toBalance | Portfolio \| null | User's to-token balance. |
| amount | string \| undefined | Human-readable input amount. |
| amountInUsd | string \| undefined | Input amount in USD. |
| onAmountChange | (v: string \| undefined) => void | Amount change handler. |
| onHalfAmount? | () => void | Half balance shortcut handler. |
| onMaxAmount? | () => void | Max balance shortcut handler. |
| outputAmount | string \| undefined | Formatted output amount. |
| outputAmountInUsd | string \| undefined | Output amount in USD. |
| onFromTokenSelect | (addr: string) => void | From-token selection handler. |
| onToTokenSelect | (addr: string) => void | To-token selection handler. |
| route | API.SwapRoute \| undefined | Current swap route quote. |
| isRouting | boolean | Whether a route is being fetched. |
| routeError | Error \| null | Route error. |
| onPreview | () => void | Open preview modal handler. |
| isSwapping | boolean | Whether a swap is in progress. |
| className? | string | External style customization. |
SwapPreviewModal
Modal component that previews swap details before confirmation. Shows from/to token cards with amounts, an expandable route plan details section, and a confirm button.
Props (SwapPreviewModalProps):
| Name | Type | Description |
| ------------------- | ---------------------------- | --------------------------------- |
| isOpen | boolean | Whether the modal is open. |
| onOpenChange | (isOpen: boolean) => void | Modal open/close handler. |
| fromToken | Token \| null | From-token metadata. |
| toToken | Token \| null | To-token metadata. |
| fromBalance | Portfolio \| null | User's from-token balance. |
| inputAmount | string \| undefined | Input amount (human-readable). |
| inputAmountInUsd | string \| undefined | Input amount in USD. |
| outputAmount | string \| undefined | Output amount (human-readable). |
| outputAmountInUsd | string \| undefined | Output amount in USD. |
| route | API.SwapRoute \| undefined | Current swap route. |
| isRouting | boolean | Whether a route is being fetched. |
| routeError | Error \| null | Route error. |
| onConfirm | () => void | Confirm swap handler. |
| isSwapping | boolean | Whether a swap is in progress. |
Types
SwapInput
interface SwapInput {
chain: Chain;
wallet: WalletAdapter;
input: string; // input token address
output: string; // output token address
amount: string; // amount in smallest unit (lamports / wei)
mode?: SwapMode; // default: EXACT_IN
slippage?: number; // 0-100, default: 1
// Solana-specific
priorityFee?: string;
tipFee?: string;
isAntiMev?: boolean;
// EVM-specific
permit?: string;
deadline?: number;
}SwapResult
interface SwapResult {
txHash: string;
extra?: Record<string, unknown>;
}SwapPhase
type SwapPhase = "route" | "sign" | "send";UseSwapOptions
interface UseSwapOptions {
onSubmitted?: (result: SwapResult) => void;
onError?: (error: Error, phase: SwapPhase) => void;
}TxConfirmationStatus
type TxConfirmationStatus = "idle" | "pending" | "confirmed" | "failed";UseTxConfirmationOptions
interface UseTxConfirmationOptions {
onConfirmed?: (txHash: string) => void;
onFailed?: (txHash: string, error: Error) => void;
timeout?: number; // default: 60000
}UseSwapRoutePollingOptions
interface UseSwapRoutePollingOptions {
interval?: number; // default: 12000
paused?: boolean;
onError?: (error: Error) => void;
}Constants
version
const version: string; // e.g. "0.1.0"Usage Examples
SwapWidget (Plug-and-Play)
import { Chain } from "@liberfi.io/types";
import { SwapWidget } from "@liberfi.io/ui-trade";
function TradePage() {
return (
<SwapWidget
chain={Chain.SOLANA}
from="So11111111111111111111111111111111111111112"
to="EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
onComplete={(result) => {
if (result.success) {
toast.success(`Swap confirmed: ${result.txHash}`);
} else {
toast.error("Swap failed");
}
}}
/>
);
}Custom UI with useSwapScript + SwapPreviewModal
import { Chain } from "@liberfi.io/types";
import { useDisclosure } from "@liberfi.io/ui";
import { useSwapScript, SwapUI, SwapPreviewModal } from "@liberfi.io/ui-trade";
function MyCustomSwapForm() {
const script = useSwapScript({
chain: Chain.SOLANA,
from: "So11111111111111111111111111111111111111112",
to: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
onComplete: (result) => console.log("Done:", result),
});
const { isOpen, onOpen, onOpenChange } = useDisclosure();
return (
<>
<SwapUI
fromToken={script.fromToken}
toToken={script.toToken}
fromBalance={script.fromBalance}
toBalance={script.toBalance}
amount={script.amount}
amountInUsd={script.amountInUsd}
onAmountChange={script.setAmount}
onHalfAmount={script.setHalfAmount}
onMaxAmount={script.setMaxAmount}
outputAmount={script.outputAmount}
outputAmountInUsd={script.outputAmountInUsd}
onFromTokenSelect={script.setFromTokenAddress}
onToTokenSelect={script.setToTokenAddress}
route={script.route}
isRouting={script.isRouting}
routeError={script.routeError}
onPreview={onOpen}
isSwapping={script.isSwapping}
/>
<SwapPreviewModal
isOpen={isOpen}
onOpenChange={onOpenChange}
fromToken={script.fromToken}
toToken={script.toToken}
fromBalance={script.fromBalance}
inputAmount={script.amount}
inputAmountInUsd={script.amountInUsd}
outputAmount={script.outputAmount}
outputAmountInUsd={script.outputAmountInUsd}
route={script.route}
isRouting={script.isRouting}
routeError={script.routeError}
onConfirm={script.swap}
isSwapping={script.isSwapping}
/>
</>
);
}Swap with Confirmation Tracking (Low-Level Hooks)
import { Chain } from "@liberfi.io/types";
import { useSwap, useTxConfirmation } from "@liberfi.io/ui-trade";
import { useWallets } from "@liberfi.io/wallet-connector";
function SwapWithConfirmation() {
const wallets = useWallets();
const solWallet = wallets.find((w) => w.chainNamespace === "SOL");
const { track, transactions } = useTxConfirmation({
onConfirmed: (txHash) => {
toast.success(`Transaction confirmed: ${txHash}`);
},
onFailed: (txHash, error) => {
toast.error(`Transaction failed: ${error.message}`);
},
});
const { swap, isSwapping } = useSwap({
onSubmitted: (result) => {
toast.info(`Transaction submitted: ${result.txHash}`);
track(Chain.SOLANA, result.txHash);
},
onError: (error, phase) => {
toast.error(`Swap failed at ${phase}: ${error.message}`);
},
});
const handleSwap = async () => {
if (!solWallet) return;
await swap({
chain: Chain.SOLANA,
wallet: solWallet,
input: "So11111111111111111111111111111111111111112",
output: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
amount: "1000000000",
slippage: 1,
});
};
const pendingCount = [...transactions.values()].filter(
(tx) => tx.status === "pending",
).length;
return (
<div>
<button onClick={handleSwap} disabled={isSwapping || !solWallet}>
{isSwapping ? "Swapping..." : "Swap SOL -> USDC"}
</button>
{pendingCount > 0 && <span>Confirming {pendingCount} tx...</span>}
</div>
);
}EVM Swap (ETH/BSC)
import { Chain } from "@liberfi.io/types";
import { useSwap, useTxConfirmation } from "@liberfi.io/ui-trade";
import { useWallets } from "@liberfi.io/wallet-connector";
function EvmSwapButton() {
const wallets = useWallets();
const evmWallet = wallets.find((w) => w.chainNamespace === "EVM");
const { track } = useTxConfirmation({
onConfirmed: (txHash) => console.log("Confirmed:", txHash),
onFailed: (txHash, err) => console.error("Failed:", txHash, err),
});
const { swap, isSwapping } = useSwap({
onSubmitted: (result) => track(Chain.ETHEREUM, result.txHash),
onError: (error, phase) => console.error(`Failed at ${phase}:`, error),
});
const handleSwap = async () => {
if (!evmWallet) return;
await swap({
chain: Chain.ETHEREUM,
wallet: evmWallet,
input: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
output: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
amount: "1000000000000000000",
slippage: 1,
});
};
return (
<button onClick={handleSwap} disabled={isSwapping || !evmWallet}>
{isSwapping ? "Swapping..." : "Swap ETH -> USDC"}
</button>
);
}Components (Instant Trade)
The instant trade module provides a configurable quick-trade form with preset management and customizable quick-amount buttons.
InstantTradeWidget
Full instant-trade form with buy/sell tabs, amount input, quick buttons, preset management, and trade execution.
<InstantTradeWidget
chain={Chain.SOLANA}
tokenAddress="EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
onSwapSubmitted={(result) => console.log(result)}
onSwapError={(error, phase) => console.error(error)}
/>Props (InstantTradeWidgetProps):
| Name | Type | Description |
| ------------------- | ------------------------------------------ | ----------------------------------------------------- |
| chain | Chain | Target chain. |
| tokenAddress | string | Address of the token being traded. |
| onSwapSubmitted? | (result: SwapResult) => void | Called when a swap is submitted. |
| onSwapError? | (error: Error, phase: SwapPhase) => void | Called on swap error. |
| settings? | InstantTradeSettings | Controlled settings (omit for localStorage). |
| onSettingsChange? | (settings: InstantTradeSettings) => void | Called when settings change. |
| headerExtra? | ReactNode | Slot for extra header content (e.g. wallet switcher). |
| className? | string | External style customization. |
InstantTradeProvider
Context provider for instant trade state. Use with InstantTradeButton for compact inline trading.
useInstantTradeScript(params)
Script-layer hook for the instant trade form. Must be used inside an InstantTradeProvider. Encapsulates token queries, balance queries, form state, and swap execution.
AmountPresetInputUI
Pure presentational compact amount input with token icon, lightning icon, and 3 preset buttons (P1/P2/P3) with tooltips. No context dependency — all data is received via props.
Props (AmountPresetInputUIProps):
| Name | Type | Description |
| ----------------- | -------------------------------- | ------------------------------------------------------------------- |
| token | PredefinedToken | Payment token (provides symbol, decimals, and icon). |
| chain | Chain | Target chain — used by preset tooltips to show chain-specific info. |
| amount? | number | Current amount value. |
| onAmountChange | (amount?: number) => void | Called when the amount changes. |
| preset? | number | Currently selected preset index (0–2). Defaults to 0. |
| onPresetChange? | (preset: number) => void | Called when the user selects a different preset. |
| onPresetClick? | (preset: number) => void | Called when the user clicks the already-selected preset. |
| presetValues? | TradePresetValues[] | Preset configurations for tooltip display. Falls back to defaults. |
| size? | "sm" \| "lg" | Controls overall component size. Defaults to "sm". |
| variant? | "default" \| "bordered" | Visual variant. |
| radius? | "full" \| "lg" \| "md" \| "sm" | Border radius. |
| fullWidth? | boolean | Whether the input takes full width. |
| className? | string | External style customization. |
AmountPresetInputWidget
Atom-backed widget that wraps AmountPresetInputUI with atomWithStorage persistence. Amount and preset selection are persisted per id + chain + token.address so different payment tokens and chains have separate saved values.
Props (AmountPresetInputWidgetProps):
| Name | Type | Description |
| ------------------- | -------------------------------- | ---------------------------------------------------------------------------- |
| id | string | Business identifier for storage key (e.g. "token-detail"). |
| chain | Chain | Target chain. |
| token | PredefinedToken | Payment token (provides symbol, decimals, address for storage key). |
| storageKeyPrefix? | string | Storage key prefix. Must match PresetFormWidget. Defaults to "liberfi.". |
| onAmountChange? | (amount?: number) => void | Notification callback (does not control state). |
| onPresetChange? | (preset: number) => void | Notification callback (does not control state). |
| onPresetClick? | (preset: number) => void | Called when the user clicks the already-selected preset. |
| size? | "sm" \| "lg" | Controls overall component size. Defaults to "sm". |
| variant? | "default" \| "bordered" | Visual variant. |
| radius? | "full" \| "lg" \| "md" \| "sm" | Border radius. |
| fullWidth? | boolean | Whether the input takes full width. |
| className? | string | External style customization. |
InstantTradeButton
Trade execution button that reads state from InstantTradeProvider. Handles wallet resolution, amount conversion, and swap execution.
InstantTradeSwapProvider
Context provider that calls useSwap, useConnectedWallet, and useAuthCallback once and exposes a single stable swap function to descendant InstantTradeListButtonWidget components. Use this when rendering many trade buttons in a list to avoid N redundant context subscriptions.
<InstantTradeSwapProvider
chain={Chain.SOLANA}
onSwapSubmitted={(r) => toast.success(`TX: ${r.txHash}`)}
onSwapError={(e, phase) => toast.error(`${phase}: ${e.message}`)}
>
{tokens.map((t) => (
<InstantTradeListButtonWidget key={t.address} ... />
))}
</InstantTradeSwapProvider>Props (InstantTradeSwapProviderProps):
| Name | Type | Description |
| ------------------ | ------------------------------------------ | --------------------------------------------- |
| chain | Chain | Target chain. |
| onSwapSubmitted? | (result: SwapResult) => void | Called when a swap transaction is submitted. |
| onSwapError? | (error: Error, phase: SwapPhase) => void | Called when a swap error occurs at any phase. |
| children | ReactNode | Child components (buttons, inputs, etc.). |
InstantTradeListButtonWidget
Memoized trade button optimised for list rendering. Visually identical to InstantTradeButtonWidget, but reads the swap function from the nearest InstantTradeSwapProvider instead of creating its own useSwap / useConnectedWallet / useAuthCallback subscriptions. Each button only subscribes to its own Jotai atoms (amount + preset) and the stable context function, eliminating unnecessary re-renders when wallet or auth state changes.
Must be used inside an InstantTradeSwapProvider.
Props (InstantTradeListButtonWidgetProps):
| Name | Type | Description |
| ------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| id | string | Business identifier for atom key (must match AmountPresetInputWidget). |
| chain | Chain | Target chain. |
| token | PredefinedToken | Payment token (decimals, symbol, address for atom key and swap input). |
| output | string | Output token address. |
| storageKeyPrefix? | string | Storage key prefix. Must match AmountPresetInputWidget. Default "liberfi.". |
| size? | "xs" \| "sm" \| "md" \| "lg" | Button size. Defaults to "sm". |
| radius? | "full" \| "lg" \| "md" \| "sm" \| "none" | Button border radius. |
| color? | "primary" \| "secondary" \| "success" \| "warning" \| "danger" \| "default" | Button color. Defaults to "primary". |
| className? | string | External style customization. |
useInstantTradeListButtonScript(params)
Lightweight script hook for the list button. Same return type as useInstantTradeButtonScript but without useSwap / useConnectedWallet / useAuthCallback subscriptions. Must be called inside an InstantTradeSwapProvider.
useInstantTradeSwap()
Returns the auth-guarded swap function from the nearest InstantTradeSwapProvider. Throws if called outside the provider. The returned function has type InstantTradeSwapFn.
MultiChainPresetFormWidget
Self-contained multi-chain preset editor. Combines chain switching (ChainSelectMobileUI), preset index tabs (P1/P2/P3), and a persisted PresetFormWidget.
<MultiChainPresetFormWidget
chains={[Chain.SOLANA, Chain.ETHEREUM, Chain.BINANCE]}
onChange={(chain, idx, value) => console.log(chain, idx, value)}
/>Props (MultiChainPresetFormWidgetProps):
| Name | Type | Description |
| ------------------- | ----------------------------------------------------------------------- | ---------------------------------------------------- |
| chains | Chain[] | Available chains to switch between. |
| defaultChain? | Chain | Initial chain. Defaults to first item in chains. |
| storageKeyPrefix? | string | Storage key prefix. Defaults to "liberfi.". |
| onChange? | (chain: Chain, presetIndex: number, value: TradePresetValues) => void | Notification callback with chain and preset context. |
| className? | string | External style customization. |
PresetFormWidget
Atom-backed widget for editing a single trade preset (slippage, priority fee, tip fee, auto fee, anti-MEV, custom RPC). State is persisted via atomWithStorage (keyed by prefix + chain + preset index).
For a pure presentational form without persistence, use PresetFormUI directly.
<PresetFormWidget
chain={Chain.SOLANA}
presetIndex={0}
storageKeyPrefix="myapp."
onChange={(next) => console.log("Preset changed:", next)}
/>Props (PresetFormWidgetProps):
| Name | Type | Description |
| ------------------- | ------------------------------------ | ---------------------------------------------------- |
| chain | Chain | Target chain — determines default values and fields. |
| presetIndex? | number | Preset index (0, 1, or 2). Defaults to 0. |
| storageKeyPrefix? | string | Storage key prefix. Defaults to "liberfi.". |
| onChange? | (value: TradePresetValues) => void | Notification callback (does not control state). |
| className? | string | External style customization. |
PresetFormUI
Pure presentational preset form. Accepts value/onChange props directly. No persistence — the caller owns the state.
useInstantTradeAmount(params)
Read-only hook for the persisted instant-trade amount and preset index. Reads from the same atomWithStorage atom family that AmountPresetInputWidget writes to.
Parameters (UseInstantTradeAmountParams):
| Name | Type | Description |
| ------------------- | -------- | ----------------------------------------------------------------------------- |
| id | string | Business identifier (must match the widget's id). |
| chain | Chain | Target chain. |
| tokenAddress | string | Payment token address. |
| storageKeyPrefix? | string | Storage key prefix. Must match the widget's prefix. Defaults to "liberfi.". |
Returns: AmountPresetState
| Name | Type | Description |
| -------- | --------------------- | ----------------------- |
| amount | number \| undefined | Persisted amount value. |
| preset | number | Persisted preset index. |
State Atoms
presetAtomFamily
Atom family for trade preset values, persisted via atomWithStorage. Each atom is keyed by "{chain}:{index}" (use presetKey to build the key). Default values are chain-specific.
import { presetAtomFamily, presetKey } from "@liberfi.io/ui-trade";
const atom = presetAtomFamily(presetKey(Chain.SOLANA, "buy", 0));presetKey(chain, direction, index, prefix?)
Constructs an atomFamily key string from chain, direction, preset index and optional prefix.
| Name | Type | Description |
| ----------- | ----------------- | --------------------------------------------- |
| chain | Chain | Target chain. |
| direction | "buy" \| "sell" | Trade direction. |
| index | number | Preset index (0, 1, or 2). |
| prefix? | string | Storage key prefix. Defaults to "liberfi.". |
Returns: string — key for use with presetAtomFamily.
instantTradeAmountAtomFamily
Atom family for instant-trade amount and preset index, persisted via atomWithStorage. Each atom is keyed by a string built with instantTradeAmountKey. The storage key includes id, chain, and tokenAddress so the same token on different chains has separate persisted values.
import {
instantTradeAmountAtomFamily,
instantTradeAmountKey,
} from "@liberfi.io/ui-trade";
const atom = instantTradeAmountAtomFamily(
instantTradeAmountKey(
"token-detail",
Chain.SOLANA,
"11111111111111111111111111111111",
),
);instantTradeAmountKey(id, chain, tokenAddress, prefix?)
Constructs an atomFamily key string for instant-trade amount + preset state.
| Name | Type | Description |
| -------------- | -------- | --------------------------------------------- |
| id | string | Business identifier. |
| chain | Chain | Target chain. |
| tokenAddress | string | Payment token address. |
| prefix? | string | Storage key prefix. Defaults to "liberfi.". |
Returns: string — key for use with instantTradeAmountAtomFamily.
Instant Trade Types
TradePresetValues
interface TradePresetValues {
slippage: number | null;
priorityFee: number | null;
tipFee: number | null;
autoFee: boolean;
maxAutoFee: number | null;
antiMev: "off" | "reduced" | "secure";
customRPC: string | null;
}InstantTradeSettings
interface InstantTradeSettings {
buy: BuySettings;
sell: SellSettings;
}
interface BuySettings {
customAmounts: (number | null)[];
presets: TradePresetValues[];
}
interface SellSettings {
customPercentages: (number | null)[];
presets: TradePresetValues[];
}DEFAULT_SOL_TRADE_PRESET
Default preset values: slippage=20, priorityFee=0.001, tipFee=0.001, autoFee=false, maxAutoFee=0.1, antiMev="off".
DEFAULT_INSTANT_TRADE_SETTINGS
Default settings with 4 buy amounts (0.01, 0.1, 1, 10), 4 sell percentages (10, 25, 50, 100), and 3 default presets each.
Usage Examples
InstantTradeWidget (Full Trade Form)
import { Chain } from "@liberfi.io/types";
import { InstantTradeWidget } from "@liberfi.io/ui-trade";
function TokenTradePage({ tokenAddress }: { tokenAddress: string }) {
return (
<InstantTradeWidget
chain={Chain.SOLANA}
tokenAddress={tokenAddress}
onSwapSubmitted={(result) => {
toast.success(`Transaction submitted: ${result.txHash}`);
}}
onSwapError={(error, phase) => {
toast.error(`Swap failed at ${phase}: ${error.message}`);
}}
/>
);
}AmountPresetInputWidget (Persisted Quick-Buy)
import { Chain } from "@liberfi.io/types";
import { AmountPresetInputWidget } from "@liberfi.io/ui-trade";
import { SOLANA_TOKENS } from "@liberfi.io/utils";
function TokenHeader() {
return (
<AmountPresetInputWidget
id="token-detail"
chain={Chain.SOLANA}
token={SOLANA_TOKENS.native}
variant="bordered"
size="sm"
onPresetClick={(p) => console.log(`Open settings for preset ${p}`)}
/>
);
}AmountPresetInputUI (Controlled)
import { useState } from "react";
import { Chain } from "@liberfi.io/types";
import { AmountPresetInputUI } from "@liberfi.io/ui-trade";
import { SOLANA_TOKENS } from "@liberfi.io/utils";
function CustomAmountInput() {
const [amount, setAmount] = useState<number | undefined>();
const [preset, setPreset] = useState(0);
return (
<AmountPresetInputUI
token={SOLANA_TOKENS.native}
chain={Chain.SOLANA}
amount={amount}
onAmountChange={setAmount}
preset={preset}
onPresetChange={setPreset}
variant="bordered"
size="lg"
/>
);
}Reading Persisted Amount/Preset
import { Chain } from "@liberfi.io/types";
import { useInstantTradeAmount } from "@liberfi.io/ui-trade";
function useTokenDetailAmount(tokenAddress: string) {
const { amount, preset } = useInstantTradeAmount({
id: "token-detail",
chain: Chain.SOLANA,
tokenAddress,
});
return { amount, preset };
}Multi-Chain Preset Settings Editor
import { Chain } from "@liberfi.io/types";
import { MultiChainPresetFormWidget } from "@liberfi.io/ui-trade";
function TradeSettings() {
return (
<MultiChainPresetFormWidget
chains={[Chain.SOLANA, Chain.ETHEREUM, Chain.BINANCE]}
storageKeyPrefix="myapp."
onChange={(chain, idx, value) =>
console.log(`chain=${chain}, preset=${idx}`, value)
}
/>
);
}Token List with Shared Swap Provider
import { Chain } from "@liberfi.io/types";
import {
InstantTradeSwapProvider,
AmountPresetInputWidget,
InstantTradeListButtonWidget,
} from "@liberfi.io/ui-trade";
import { SOLANA_TOKENS } from "@liberfi.io/utils";
function TokenList({
tokens,
}: {
tokens: { address: string; symbol: string }[];
}) {
return (
<InstantTradeSwapProvider
chain={Chain.SOLANA}
onSwapSubmitted={(r) => toast.success(`TX: ${r.txHash}`)}
onSwapError={(e, phase) => toast.error(`${phase}: ${e.message}`)}
>
{/* Shared amount input — one for all buttons */}
<AmountPresetInputWidget
id="token-list"
chain={Chain.SOLANA}
token={SOLANA_TOKENS.native}
/>
{/* N buttons — each only subscribes to its own atom + stable context */}
{tokens.map((t) => (
<InstantTradeListButtonWidget
key={t.address}
id="token-list"
chain={Chain.SOLANA}
token={SOLANA_TOKENS.native}
output={t.address}
/>
))}
</InstantTradeSwapProvider>
);
}Reading Preset Values from Atoms
import { Chain } from "@liberfi.io/types";
import { usePresetValues } from "@liberfi.io/ui-trade";
function useCurrentPreset(chain: Chain, index: number) {
return usePresetValues({ chain, direction: "buy", presetIndex: index });
}Future Improvements
- Add
useSwapQuotehook for fetching quotes without executing (read-only route info). - Add ERC20 approval detection and
useTokenApprovalhook for EVM chains that don't support permit. - Support
ExactOutswap mode with output amount estimation. - Add a skeleton loading component (
swap-skeleton.ui.tsx) for the swap form. - Support token-select callback props on
SwapWidgetfor integrating with external token picker UI. - Add limit order and advanced trade modes to
InstantTradeWidget. - Add token price conversion display in the instant trade form.
- Support custom RPC endpoint validation in
PresetFormWidget.
