mirror of
https://github.com/luxfi/wallet.git
synced 2026-07-27 03:37:41 +00:00
feat(state): zustand swap+bridge slices
Pure data slices for Swap and Bridge state machines. Async lives in the hooks; the store keeps the screen + flow phases in sync. swap : idle → quoting → ready → approving → swapping → done | error bridge : idle → quoting → ready → locking → signing → minting → done | error Quote shapes carry a `ts` timestamp; consumers reject quotes >30s old. Route kinds locked: V2/V3 = AMM, V4 = DEX (rendered verbatim in UI).
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* Bridge-flow state. Drives `screens/bridge/*`.
|
||||
*
|
||||
* idle → editing form, no quote
|
||||
* quoting → fetching route from gateway
|
||||
* ready → quote fresh, "Confirm bridge" enabled
|
||||
* locking → source-chain lock tx in flight
|
||||
* signing → MPC ceremony in progress on M-Chain
|
||||
* minting → destination mint observed, awaiting confirmation
|
||||
* done → mint tx surfaced; user can navigate away
|
||||
* error → any leg failed; `error` populated
|
||||
*
|
||||
* The MPC ceremony is what differentiates Lux Teleport from a vanilla
|
||||
* lock-and-mint: the user signs ONLY the source lock; the destination
|
||||
* mint is signed by the bridge committee via threshold signatures. This
|
||||
* is reflected in the status machine — `signing` belongs to the M-Chain,
|
||||
* not to the user.
|
||||
*/
|
||||
import { create } from "zustand"
|
||||
import type { Asset } from "../lib/asset"
|
||||
|
||||
export type BridgeStatus =
|
||||
| "idle"
|
||||
| "quoting"
|
||||
| "ready"
|
||||
| "locking"
|
||||
| "signing"
|
||||
| "minting"
|
||||
| "done"
|
||||
| "error"
|
||||
|
||||
export interface BridgeQuote {
|
||||
/** Amount the user receives on the destination chain (smallest units). */
|
||||
amountOut: string
|
||||
/** Bridge fee taken from the input amount (smallest units, source asset). */
|
||||
fee: string
|
||||
/** Estimated time to finality in seconds. */
|
||||
etaSeconds: number
|
||||
/** Path of chain ids — usually `[from, "lux-m", to]`. */
|
||||
path: string[]
|
||||
/** MPC threshold notation, e.g. "2/3". Rendered verbatim. */
|
||||
threshold: string
|
||||
/** Destination-chain mint contract address (informational). */
|
||||
mintContract?: `0x${string}`
|
||||
/** Quote freshness timestamp, ms epoch. Stale > 30s discarded before submit. */
|
||||
ts: number
|
||||
}
|
||||
|
||||
export interface BridgeState {
|
||||
fromChainId: string
|
||||
toChainId: string
|
||||
asset: Asset | null
|
||||
amount: string
|
||||
/** Recipient on destination — empty means "same wallet" (caller's address). */
|
||||
recipient: string
|
||||
quote: BridgeQuote | null
|
||||
status: BridgeStatus
|
||||
lockTxHash: string | null
|
||||
mintTxHash: string | null
|
||||
error: string | null
|
||||
|
||||
setFromChainId: (id: string) => void
|
||||
setToChainId: (id: string) => void
|
||||
flipChains: () => void
|
||||
setAsset: (a: Asset | null) => void
|
||||
setAmount: (v: string) => void
|
||||
setRecipient: (v: string) => void
|
||||
setQuote: (q: BridgeQuote | null) => void
|
||||
setStatus: (s: BridgeStatus) => void
|
||||
setLockTxHash: (h: string | null) => void
|
||||
setMintTxHash: (h: string | null) => void
|
||||
setError: (e: string | null) => void
|
||||
reset: () => void
|
||||
}
|
||||
|
||||
const INITIAL = {
|
||||
// Default: bridge from Lux C-Chain → Zoo L1, the canonical "demo" route.
|
||||
fromChainId: "lux-c",
|
||||
toChainId: "zoo-l1",
|
||||
asset: null as Asset | null,
|
||||
amount: "",
|
||||
recipient: "",
|
||||
quote: null as BridgeQuote | null,
|
||||
status: "idle" as BridgeStatus,
|
||||
lockTxHash: null as string | null,
|
||||
mintTxHash: null as string | null,
|
||||
error: null as string | null,
|
||||
}
|
||||
|
||||
export const useBridgeStore = create<BridgeState>((set, get) => ({
|
||||
...INITIAL,
|
||||
setFromChainId: (fromChainId) => set({ fromChainId, quote: null }),
|
||||
setToChainId: (toChainId) => set({ toChainId, quote: null }),
|
||||
flipChains: () => {
|
||||
const { fromChainId, toChainId } = get()
|
||||
set({ fromChainId: toChainId, toChainId: fromChainId, quote: null, asset: null })
|
||||
},
|
||||
setAsset: (asset) => set({ asset, quote: null }),
|
||||
setAmount: (amount) => set({ amount, quote: null }),
|
||||
setRecipient: (recipient) => set({ recipient }),
|
||||
setQuote: (quote) => set({ quote }),
|
||||
setStatus: (status) => set({ status }),
|
||||
setLockTxHash: (lockTxHash) => set({ lockTxHash }),
|
||||
setMintTxHash: (mintTxHash) => set({ mintTxHash }),
|
||||
setError: (error) => set({ error }),
|
||||
reset: () => set({ ...INITIAL }),
|
||||
}))
|
||||
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* Swap-flow state. One store, six explicit phases.
|
||||
*
|
||||
* idle → editing form, no quote yet
|
||||
* quoting → fetching route + price impact
|
||||
* ready → quote fresh, "Confirm swap" enabled
|
||||
* approving → erc20.approve in flight (only if needed)
|
||||
* swapping → router/v4 swap tx in flight
|
||||
* done → tx confirmed
|
||||
* error → quote / approve / swap failed; `error` populated
|
||||
*
|
||||
* Reset is explicit: `/swap` mount clears state so a stale quote can never
|
||||
* replay across sessions.
|
||||
*
|
||||
* Pattern mirrors `store/send.ts` — pure data slice; async lives in hooks.
|
||||
*/
|
||||
import { create } from "zustand"
|
||||
import type { Asset } from "../lib/asset"
|
||||
|
||||
export type SwapStatus =
|
||||
| "idle"
|
||||
| "quoting"
|
||||
| "ready"
|
||||
| "approving"
|
||||
| "swapping"
|
||||
| "done"
|
||||
| "error"
|
||||
|
||||
/**
|
||||
* Naming convention is LOCKED per spec: V2/V3 = AMM, V4 = DEX. The `kind`
|
||||
* field below is rendered verbatim in `QuoteRoute.tsx`.
|
||||
*/
|
||||
export type RouteKind = "amm-v2" | "amm-v3" | "dex-v4"
|
||||
|
||||
export interface SwapQuote {
|
||||
/** Amount in (smallest units, decimal string). */
|
||||
amountIn: string
|
||||
/** Amount out (smallest units, decimal string). */
|
||||
amountOut: string
|
||||
/** Effective price `amountOut / amountIn` as a decimal string. */
|
||||
price: string
|
||||
/** Price impact, basis points (1 bp = 0.01 %). 0–10000. */
|
||||
priceImpactBps: number
|
||||
/** Route shown to the user — chain of token symbols. */
|
||||
path: string[]
|
||||
/** AMM v2/v3 vs DEX v4. */
|
||||
kind: RouteKind
|
||||
/** Pool fee tier in bps for v3/v4 (e.g. 3000 = 0.30 %). 0 if N/A. */
|
||||
feeBps: number
|
||||
/** Router contract that will execute the swap. */
|
||||
router: `0x${string}`
|
||||
/** Calldata produced by the gateway / quoter. Trusted only after slippage
|
||||
* check + signing modal user confirmation. */
|
||||
calldata: `0x${string}`
|
||||
/** Wallet-recipient (zero address means caller). */
|
||||
recipient: `0x${string}`
|
||||
/** Quote freshness timestamp, ms epoch. Stale > 30s discarded. */
|
||||
ts: number
|
||||
}
|
||||
|
||||
export interface SwapState {
|
||||
fromAsset: Asset | null
|
||||
toAsset: Asset | null
|
||||
amount: string
|
||||
/** Slippage tolerance in basis points (e.g. 50 = 0.50 %). */
|
||||
slippageBps: number
|
||||
quote: SwapQuote | null
|
||||
status: SwapStatus
|
||||
approveTxHash: string | null
|
||||
swapTxHash: string | null
|
||||
error: string | null
|
||||
|
||||
setFromAsset: (a: Asset | null) => void
|
||||
setToAsset: (a: Asset | null) => void
|
||||
flipAssets: () => void
|
||||
setAmount: (v: string) => void
|
||||
setSlippageBps: (bps: number) => void
|
||||
setQuote: (q: SwapQuote | null) => void
|
||||
setStatus: (s: SwapStatus) => void
|
||||
setApproveTxHash: (h: string | null) => void
|
||||
setSwapTxHash: (h: string | null) => void
|
||||
setError: (e: string | null) => void
|
||||
reset: () => void
|
||||
}
|
||||
|
||||
const INITIAL = {
|
||||
fromAsset: null as Asset | null,
|
||||
toAsset: null as Asset | null,
|
||||
amount: "",
|
||||
// 0.50 % default per SCREENS.md §3.
|
||||
slippageBps: 50,
|
||||
quote: null as SwapQuote | null,
|
||||
status: "idle" as SwapStatus,
|
||||
approveTxHash: null as string | null,
|
||||
swapTxHash: null as string | null,
|
||||
error: null as string | null,
|
||||
}
|
||||
|
||||
export const useSwapStore = create<SwapState>((set, get) => ({
|
||||
...INITIAL,
|
||||
setFromAsset: (fromAsset) => set({ fromAsset, quote: null }),
|
||||
setToAsset: (toAsset) => set({ toAsset, quote: null }),
|
||||
flipAssets: () => {
|
||||
const { fromAsset, toAsset } = get()
|
||||
set({ fromAsset: toAsset, toAsset: fromAsset, quote: null })
|
||||
},
|
||||
setAmount: (amount) => set({ amount, quote: null }),
|
||||
setSlippageBps: (slippageBps) => set({ slippageBps }),
|
||||
setQuote: (quote) => set({ quote }),
|
||||
setStatus: (status) => set({ status }),
|
||||
setApproveTxHash: (approveTxHash) => set({ approveTxHash }),
|
||||
setSwapTxHash: (swapTxHash) => set({ swapTxHash }),
|
||||
setError: (error) => set({ error }),
|
||||
reset: () => set({ ...INITIAL }),
|
||||
}))
|
||||
Reference in New Issue
Block a user