Files
lp-terminal_github/src/lib/swapGate.ts
T
labrinyang bca538e7e3 feat: SHEEP CHOICE routing, cross-chain deposits, and any-token ZAP
One commit because the pieces do not compile apart: the shared copy, the tab
shell and the data layer all changed together, and splitting them further would
mean inventing intermediate states that never existed.

SHEEP CHOICE — the terminal's own swap. Quotes come from a solver that splits
one trade across several pools instead of forcing it down a single path, and
returns a ready-to-sign Settler transaction; the UI draws the split leg by leg
and scores every venue against one shared fee-free baseline, so the card that
says it pays most actually does. The Kyber transaction path is gone — Kyber is
read-only USD valuation now, and there is no Kyber calldata to sign.

BRIDGE — deposits from other chains over Relay, Across and the native portal,
priced side by side and sorted by what actually reaches you. No fee on any of
them. In-flight transfers get a countdown and survive a reload.

ZAP — add liquidity holding neither side of the pair; whatever needs swapping
is done in the same flow, with an optional stake-after step. Uniswap V2
liquidity now shows up under POSITIONS.

Swaps in flight are persisted, so a refresh mid-swap no longer loses the
transaction. Pair labels copy their token and pool addresses, and jump to
DexScreener.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 00:01:52 +08:00

83 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
type SlippageTone = 'green' | 'amber' | 'red'
export const SLIPPAGE_CHOICES = [50, 100, 300] as const
export type SlippageBps = (typeof SLIPPAGE_CHOICES)[number]
export type AutoSlippage = { bps: number; tone: SlippageTone }
// bounds for ANY slippage value — auto-derived, preset, or hand-typed
export const MIN_SLIPPAGE_BPS = 10 // 0.1% floor
export const MAX_SLIPPAGE_BPS = 5000 // 50% fat-finger ceiling
/** clamp + round an arbitrary bps into the allowed slippage band */
export function clampSlippageBps(bps: number): number {
if (!Number.isFinite(bps)) return MIN_SLIPPAGE_BPS
return Math.min(MAX_SLIPPAGE_BPS, Math.max(MIN_SLIPPAGE_BPS, Math.round(bps)))
}
/** a user-typed percent ("2.5") → clamped bps, or null when not a positive number */
export function slippagePctToBps(pct: string): number | null {
const n = Number(pct.trim())
if (!Number.isFinite(n) || n <= 0) return null
return clampSlippageBps(Math.round(n * 100))
}
/** shared warning tone for any slippage magnitude */
export function slippageTone(bps: number): SlippageTone {
if (bps <= 100) return 'green'
if (bps <= 300) return 'amber'
return 'red'
}
// past this measured impact AUTO refuses to guess: the trade is eating so much
// of the pool that a derived tolerance is meaningless (it balloons toward the
// 50% ceiling), so the user must set the number by hand — same forced-choice
// path as an unavailable impact probe.
export const AUTO_IMPACT_LIMIT_BPS = 1000 // 10%
/** AUTO floor after a slippage-caused halt: 1.5× the tolerance that just
* failed, rounded up to 0.1% — re-offering the number that failed would just
* fail again, while an unbounded jump would be a blank check */
export function retrySlippage(failedBps: number): number {
return clampSlippageBps(Math.ceil((failedBps * 1.5) / 10) * 10)
}
/**
* Quote-derived AUTO policy for MARKET + ZAP: the tolerance absorbs price drift
* between quote and execution. Cover the quote's measured impact, plus the
* pool's fee tier as a volatility prior (1% tiers exist because their pairs
* move between blocks; 0.05% tiers barely do — the fee itself is already paid
* inside the quote, it is NOT a cost here), plus a 0.3% pad — or half the
* impact when that is larger. Rounded up to 0.1%, no fixed ceiling inside the
* sane range. Returns null past AUTO_IMPACT_LIMIT_BPS so the caller forces an
* explicit choice. Override anytime via a preset or typed value.
*/
export function autoSlippage(impactBps: number, poolFeeBps: number): AutoSlippage | null {
const safe = Number.isFinite(impactBps) && impactBps > 0 ? impactBps : 0
if (safe > AUTO_IMPACT_LIMIT_BPS) return null
const fee = Number.isFinite(poolFeeBps) && poolFeeBps > 0 ? poolFeeBps : 0
const raw = safe + fee + Math.max(30, safe * 0.5) // impact + pool fee + max(0.3%, half the impact)
const bps = clampSlippageBps(Math.ceil(raw / 10) * 10)
return { bps, tone: slippageTone(bps) }
}
/**
* A quote's all-in cost in bps — price move, pool fees, modeled transfer taxes
* and terminal fee at once. `netOut` is what the user actually receives;
* `midOut` is the fee-free price for this size, so everything execution took is
* the gap between them and nothing has to be summed (or double-counted).
*
* Every row on screen divides by the SAME `midOut`, whichever quote source
* produced it, so the ordering matches delivered output by construction and the
* cost can never contradict the "behind best" chip beside it. Scoring each row
* against its own probe did exactly that: measured on-chain, the solver
* delivered +26.66 bps more CASHCAT than the best direct route while its card
* read 0.55% against that row's 0.30%, ranking the two backwards.
*
* A missing baseline, or one below the delivered output because the quotes are
* out of sync, makes the cost unavailable.
*/
export function allInCostBps(netOut: bigint, midOut: bigint | null): number | null {
if (midOut === null) return null
if (netOut > midOut) return null
return Number(((midOut - netOut) * 10_000n + midOut / 2n) / midOut) // nearest, not floor
}