# Swaps & FX

> The governing rule for this entire family: A rate is a perishable fact. It never appears without a timestamp, and it is never transacted on without a lock. Specs for 9 React components: SwapWidget, RateLine, RateLock, ConversionBreakdown and 5 more.

Source: https://lopifi.com/docs/catalog/04-swaps-fx
Part of: Lopifi — US GAAP books on autopilot, reviewed by a CPA (https://lopifi.com)

---

# Swaps & FX

The governing rule for this entire family:

> **A rate is a perishable fact.** It never appears without a timestamp, and it is never transacted on without a lock.

---

## 38. `SwapWidget`

> The signature component. From/to with the flip.

```
┌─ 24 ──────────────────────────────────┐
│  You send                              │  label
│  [1,000.00]              [USD ▾]       │  figure-lg · currency select
│  Balance $4,120.00              [Max]  │  caption
│                                        │
│         ⟨ ⇅ ⟩  ← flip button           │  centered, overlapping the divider
│                                        │
│  Recipient gets                        │
│  [918.42]                [EUR ▾]       │
│                                        │
│  1 USD = 0.91842 EUR · 12s  [locked]   │  RateLine
│  ├─ ConversionBreakdown ─────────────┤ │
└────────────────────────────────────────┘
```

### The flip
The one place `spring.playful` is used.

```
icon      rotateY? no — rotate(180deg), spring.playful { duration: 0.45, bounce: 0.22 }
fields    cross-translate: top ↕ bottom, transform only, same spring
values    swap INSTANTLY. The amounts do not interpolate. (Money Law 1)
```

The fields move; the numbers don't animate. That distinction is the whole doctrine in one component — motion explains *what happened structurally*, and never renders a false value.

Under reduced motion: instant swap, no rotation.

### Rules
- **Editing either field recomputes the other**, with a 140ms blur-masked crossfade on the computed value — never a count-up.
- **The computed field is visibly derived** — `--text-secondary` until it's the focused one.
- Debounce rate fetches at 300ms; **never show a stale computed value as current.** During refetch, dim the computed value to 0.6.
- Insufficient balance is an inline error under the send field, not a disabled button.
- Fee and rate are always visible without expanding anything.

---

## 39. `RateLine`

```tsx
<RateLine from="USD" to="EUR" rate={0.91842} asOf={t} />
// 1 USD = 0.91842 EUR · 12s ago
```

5 significant figures, `tnum`. **The timestamp is not optional** — the component has no variant without it.

Live variant flashes on update ([value flash](../motion/03-catalog.md#value-flash)), throttled to one flash per 400ms.

## 40. `RateLock`
A locked rate with a countdown. The countdown is a **rule-line progress bar**, `linear`, consistent with the statement grammar rather than a rounded progress pill.

On expiry: the rate refreshes, the new rate crossfades in, and the confirm action is **disabled until the user acknowledges the new rate**. Silently executing at a rate the user didn't see is the worst failure this family can have.

Warning state at 20% remaining — amber, plus a text countdown, never color alone.

## 41. `ConversionBreakdown`

> Wise's honesty pattern, as a component. Every line visible.

```
Amount to convert                    1,000.00 USD
Our fee                                 −4.50 USD   ← itemized, named
Amount we'll convert                   995.50 USD
                                      ──────────    ← single rule
Rate  1 USD = 0.91842 EUR  · 09:12 UTC
Recipient gets                         914.29 EUR
                                      ══════════    ← double rule
```

- **Every fee named and itemized.** Never a lump "Fees".
- The rate line carries its timestamp.
- Uses `Rule` — the same grammar as the statements, which is what makes the library feel like one system.
- Percentage-of-total splits use largest-remainder allocation, so the parts sum to the total exactly.

## 42. `CurrencyPairChart`
Rate history with a period selector. Y-axis **does not start at zero** — it's a rate, not a quantity, and forcing zero destroys the resolution. This is the one sanctioned exception to the zero-baseline rule, and the axis is labeled clearly so it can't mislead.

Draws once on mount. Instant on period change.

## 43. `RateTicker`
Live multi-pair ticker. The value flash is the whole point. Pauses off-screen and on tab hide. **Never a marquee** — a scrolling ticker is continuous large-surface motion that can't be read and can't be paused.

## 44. `SlippageControl`
Tolerance setting that **always states the worst case in absolute money**: "You'll receive at least 910.15 EUR". A percentage alone is not a decision-grade number.

## 45. `SwapRoute`
Multi-hop path (USD → USDC → EUR). Rendered as a horizontal flow, each hop with its own rate and fee. A rare, legitimate use for the [isometric flow-diagram treatment](../foundations/08-imagery-and-isometric.md) — motion explains the mechanism.

## 46. `FxExposure`
Position by currency with unrealized gain/loss. Uses `polarity="standard"` per position, and clearly separates realized from unrealized — they're different facts and conflating them misstates the position.
