npm package: @liberfi.io/ui-trade
Description: Trade hooks for Liberfi React SDK
Author: liberfi.io
Latest version: 0.1.113

# @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 via `atomWithStorage`.

## 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/client` and `WalletAdapter` abstractions.
- **UI via `@liberfi.io/ui`** — Uses the shared UI library (`Button`, `Input`, `Avatar`, `Modal`, etc.) for consistent styling across the monorepo. No direct `@heroui/react` dependency.
- **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

```bash
pnpm add @liberfi.io/ui-trade
```

Peer dependencies the consumer must provide:

- `react` (>=18)
- `react-dom` (>=18)
- `jotai` (>=2.15.1) — used for persistent preset state via `atomWithStorage`
- `@liberfi.io/react` (provides `DexClientProvider` and `useDexClient`)
- `@liberfi.io/ui` (provides UI components: `Button`, `Input`, `Modal`, `Avatar`, etc.)
- `@liberfi.io/ui-chain-select` (provides `ChainSelectMobileUI` for `MultiChainPresetFormWidget`)
- `@liberfi.io/wallet-connector` (provides `WalletAdapter` type)

## 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.

```tsx
<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`

```typescript
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`

```typescript
interface SwapResult {
  txHash: string;
  extra?: Record<string, unknown>;
}
```

#### `SwapPhase`

```typescript
type SwapPhase = "route" | "sign" | "send";
```

#### `UseSwapOptions`

```typescript
interface UseSwapOptions {
  onSubmitted?: (result: SwapResult) => void;
  onError?: (error: Error, phase: SwapPhase) => void;
}
```

#### `TxConfirmationStatus`

```typescript
type TxConfirmationStatus = "idle" | "pending" | "confirmed" | "failed";
```

#### `UseTxConfirmationOptions`

```typescript
interface UseTxConfirmationOptions {
  onConfirmed?: (txHash: string) => void;
  onFailed?: (txHash: string, error: Error) => void;
  timeout?: number; // default: 60000
}
```

#### `UseSwapRoutePollingOptions`

```typescript
interface UseSwapRoutePollingOptions {
  interval?: number; // default: 12000
  paused?: boolean;
  onError?: (error: Error) => void;
}
```

### Constants

#### `version`

```typescript
const version: string; // e.g. "0.1.0"
```

## Usage Examples

### SwapWidget (Plug-and-Play)

```tsx
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

```tsx
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)

```tsx
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)

```tsx
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.

```tsx
<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.

```tsx
<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`.

```tsx
<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.

```tsx
<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.

```typescript
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.

```typescript
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`

```typescript
interface TradePresetValues {
  slippage: number | null;
  priorityFee: number | null;
  tipFee: number | null;
  autoFee: boolean;
  maxAutoFee: number | null;
  antiMev: "off" | "reduced" | "secure";
  customRPC: string | null;
}
```

#### `InstantTradeSettings`

```typescript
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)

```tsx
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)

```tsx
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)

```tsx
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

```tsx
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

```tsx
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

```tsx
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

```tsx
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 `useSwapQuote` hook for fetching quotes without executing (read-only route info).
- Add ERC20 approval detection and `useTokenApproval` hook for EVM chains that don't support permit.
- Support `ExactOut` swap mode with output amount estimation.
- Add a skeleton loading component (`swap-skeleton.ui.tsx`) for the swap form.
- Support token-select callback props on `SwapWidget` for 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`.