mirror of
https://github.com/labrinyang/lp-terminal.git
synced 2026-07-27 21:27:43 +00:00
bca538e7e3
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>
83 lines
4.0 KiB
TypeScript
83 lines
4.0 KiB
TypeScript
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
|
||
}
|