mirror of
https://github.com/labrinyang/lp-terminal.git
synced 2026-07-27 21:27:43 +00:00
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>
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
// Live smoke of the bridge layer: token DISCOVERY per remote (relay/across
|
||||
// support surfaces, same-token only), then every discovered route × direction
|
||||
// quoted through each supporting engine, printed as the normalized BridgeQuote
|
||||
// the UI consumes. Read-only (quotes only, placeholder payer) — safe any time.
|
||||
// npm run smoke:bridge
|
||||
import { formatUnits, parseUnits } from 'viem'
|
||||
import { REMOTE_CHAINS, resolveIntent, type BridgeDir } from '../src/config/bridge'
|
||||
import { quoteAcross } from '../src/lib/bridge/across'
|
||||
import { quotePortal } from '../src/lib/bridge/portal'
|
||||
import { quoteRelay } from '../src/lib/bridge/relay'
|
||||
import { fetchBridgeTokens, toTokenOption } from '../src/lib/bridge/tokens'
|
||||
import type { BridgeFee, BridgeProviderId, BridgeQuote } from '../src/lib/bridge/types'
|
||||
|
||||
const FEE: BridgeFee = { bps: 0, receiver: '0x1111111111111111111111111111111111111111' }
|
||||
const REMOTES = [
|
||||
REMOTE_CHAINS.find((r) => r.label === 'ETHEREUM')!,
|
||||
REMOTE_CHAINS.find((r) => r.label === 'BASE')!,
|
||||
]
|
||||
|
||||
let failures = 0
|
||||
for (const remote of REMOTES) {
|
||||
const supports = await fetchBridgeTokens(remote)
|
||||
console.log(`\n== ${remote.label} — discovered same-token routes ==`)
|
||||
for (const s of supports) {
|
||||
console.log(
|
||||
` ${s.symbol.padEnd(5)} dec=${s.decimals} rh=${s.robinhoodToken.slice(0, 10)} remote=${s.remoteToken.slice(0, 10)} ` +
|
||||
`relay=${s.relay} across=${s.across} portal=${s.portal}`,
|
||||
)
|
||||
}
|
||||
for (const s of supports) {
|
||||
for (const dir of ['in', 'out'] as BridgeDir[]) {
|
||||
const token = toTokenOption(s, dir)
|
||||
if (token.providers.length === 0) continue
|
||||
const amount = token.symbol === 'USDG' ? parseUnits('100', token.decimals) : parseUnits('0.05', token.decimals)
|
||||
const leg = resolveIntent({ dir, token, remote, amount })
|
||||
const inNum = Number(formatUnits(amount, leg.inputDecimals))
|
||||
const runners: Partial<Record<BridgeProviderId, () => Promise<BridgeQuote>>> = {
|
||||
portal: () => Promise.resolve(quotePortal(leg, amount)),
|
||||
relay: () => quoteRelay(leg, amount, FEE, null),
|
||||
across: () => quoteAcross(leg, amount, FEE, null),
|
||||
}
|
||||
for (const provider of token.providers) {
|
||||
const t0 = Date.now()
|
||||
try {
|
||||
const q = await runners[provider]!()
|
||||
const outNum = Number(formatUnits(q.outputAmount, leg.outputDecimals))
|
||||
const impact = ((outNum / inNum - 1) * 100).toFixed(2)
|
||||
console.log(
|
||||
` ${token.symbol.padEnd(5)} ${dir.padEnd(3)} ${provider.padEnd(6)} out=${outNum.toFixed(6)} ${leg.outputSymbol.padEnd(4)} ` +
|
||||
`impact=${impact}% eta=${q.etaSec}s steps=${q.steps.map((st) => st.kind).join('+')} ` +
|
||||
`(${Date.now() - t0}ms)`,
|
||||
)
|
||||
} catch (e) {
|
||||
failures++
|
||||
console.log(
|
||||
` ${token.symbol.padEnd(5)} ${dir.padEnd(3)} ${provider.padEnd(6)} FAILED: ${(e as Error).message.slice(0, 90)} (${Date.now() - t0}ms)`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(`\n${failures === 0 ? 'ALL ROUTES QUOTED' : `${failures} route quote(s) failed`}`)
|
||||
+52
-40
@@ -1,5 +1,5 @@
|
||||
// Live-chain smoke test for the read layer: TickMath constants, ABI selectors,
|
||||
// pool discovery, quoter-vs-kyber sanity. Run: npm run smoke
|
||||
// pool discovery and direct Uniswap/UP33 quote sanity. Run: npm run smoke
|
||||
// Never prints the RPC URL.
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
@@ -20,6 +20,8 @@ import {
|
||||
getSqrtRatioAtTick,
|
||||
sqrtPriceToPrice,
|
||||
} from '../src/lib/clmath'
|
||||
import { directRouteLabel, netAfterFee, quoteDirectCandidates } from '../src/lib/directSwap'
|
||||
import type { ClPool } from '../src/types'
|
||||
|
||||
let pass = 0
|
||||
let fail = 0
|
||||
@@ -29,21 +31,9 @@ function check(name: string, cond: boolean, detail = '') {
|
||||
console.log(` ${cond ? 'PASS' : 'FAIL'} ${name}${detail ? ' — ' + detail : ''}`)
|
||||
}
|
||||
|
||||
// duplicated rather than imported: src/config/env.ts reads import.meta.env,
|
||||
// which is vite-only and does not load under node/tsx.
|
||||
const PUBLIC_RPC = 'https://rpc.mainnet.chain.robinhood.com'
|
||||
|
||||
/** repo-root .env `RPC` (SECRET — never print it). No .env / no key: public RPC. */
|
||||
const rpc = (() => {
|
||||
const fromEnv = process.env.RPC?.trim()
|
||||
if (fromEnv) return fromEnv
|
||||
try {
|
||||
const text = readFileSync(fileURLToPath(new URL('../.env', import.meta.url)), 'utf8')
|
||||
return text.match(/^\s*RPC\s*=\s*(\S+)\s*$/m)?.[1] ?? PUBLIC_RPC
|
||||
} catch {
|
||||
return PUBLIC_RPC
|
||||
}
|
||||
})()
|
||||
const envText = readFileSync(fileURLToPath(new URL('../../.env', import.meta.url)), 'utf8')
|
||||
const rpc =
|
||||
envText.match(/^\s*RPC\s*=\s*(\S+)\s*$/m)?.[1] ?? 'https://rpc.mainnet.chain.robinhood.com'
|
||||
|
||||
const chain = defineChain({
|
||||
id: 4663,
|
||||
@@ -114,6 +104,7 @@ async function main() {
|
||||
{ abi: clPoolAbi, address: p, functionName: 'token0' },
|
||||
{ abi: clPoolAbi, address: p, functionName: 'token1' },
|
||||
{ abi: clPoolAbi, address: p, functionName: 'tickSpacing' },
|
||||
{ abi: clPoolAbi, address: p, functionName: 'fee' },
|
||||
{ abi: clPoolAbi, address: p, functionName: 'liquidity' },
|
||||
{ abi: clPoolAbi, address: p, functionName: 'gauge' },
|
||||
]) as never,
|
||||
@@ -127,18 +118,20 @@ async function main() {
|
||||
t0: Address
|
||||
t1: Address
|
||||
ts: number
|
||||
fee: number
|
||||
liq: bigint
|
||||
gauge?: Address
|
||||
}[] = []
|
||||
clAddrs.forEach((p, i) => {
|
||||
const s0 = ok<readonly [bigint, number, number, number, number, boolean]>(slotRes[i * 6])
|
||||
const t0 = ok<Address>(slotRes[i * 6 + 1])
|
||||
const t1 = ok<Address>(slotRes[i * 6 + 2])
|
||||
const ts = ok<number>(slotRes[i * 6 + 3])
|
||||
const liq = ok<bigint>(slotRes[i * 6 + 4]) ?? 0n
|
||||
const gauge = ok<Address>(slotRes[i * 6 + 5])
|
||||
if (!s0 || !t0 || !t1 || ts === undefined) return
|
||||
clPools.push({ addr: p, sqrtP: s0[0], tick: s0[1], t0, t1, ts, liq, gauge })
|
||||
const s0 = ok<readonly [bigint, number, number, number, number, boolean]>(slotRes[i * 7])
|
||||
const t0 = ok<Address>(slotRes[i * 7 + 1])
|
||||
const t1 = ok<Address>(slotRes[i * 7 + 2])
|
||||
const ts = ok<number>(slotRes[i * 7 + 3])
|
||||
const fee = ok<number>(slotRes[i * 7 + 4])
|
||||
const liq = ok<bigint>(slotRes[i * 7 + 5]) ?? 0n
|
||||
const gauge = ok<Address>(slotRes[i * 7 + 6])
|
||||
if (!s0 || !t0 || !t1 || ts === undefined || fee === undefined) return
|
||||
clPools.push({ addr: p, sqrtP: s0[0], tick: s0[1], t0, t1, ts, fee, liq, gauge })
|
||||
if (s0[0] === 0n) return
|
||||
const lo = getSqrtRatioAtTick(s0[1])
|
||||
const hi = getSqrtRatioAtTick(s0[1] + 1)
|
||||
@@ -147,7 +140,7 @@ async function main() {
|
||||
})
|
||||
check('slot0 sqrtPrice within [tick, tick+1) for all CL pools', tickBad === 0, `${tickOk} ok / ${tickBad} bad`)
|
||||
|
||||
// 6. WETH/UP CL pool: our quoter path vs kyber aggregator ballpark
|
||||
// 6. WETH/UP: raw UP33 quoter plus the product's direct comparison module
|
||||
const wethUp = clPools
|
||||
.filter(
|
||||
(p) =>
|
||||
@@ -194,22 +187,41 @@ async function main() {
|
||||
check('quoter ~ spot price (<30% incl. fee+impact)', rel < 0.3, `spot=${upPerWeth.toFixed(1)} quote=${outF.toFixed(1)}`)
|
||||
}
|
||||
|
||||
try {
|
||||
const r = await fetch(
|
||||
`https://aggregator-api.kyberswap.com/robinhood/api/v1/routes?tokenIn=${ADDR.WETH}&tokenOut=${ADDR.UP}&amountIn=${oneWeth}`,
|
||||
{ headers: { 'x-client-id': 'up33-terminal-smoke' } },
|
||||
)
|
||||
const j: any = await r.json()
|
||||
const aggOut = BigInt(j?.data?.routeSummary?.amountOut ?? '0')
|
||||
if (aggOut > 0n && quoterOut) {
|
||||
const rel = Math.abs(Number(aggOut - quoterOut)) / Number(aggOut)
|
||||
check('kyber agg vs our single-pool quote (<25%)', rel < 0.25, `agg=${aggOut} ours=${quoterOut}`)
|
||||
} else {
|
||||
check('kyber aggregator reachable', aggOut > 0n, j?.message ?? 'no data')
|
||||
}
|
||||
} catch (e) {
|
||||
check('kyber aggregator reachable', false, String(e))
|
||||
const smokePool: ClPool = {
|
||||
kind: 'cl',
|
||||
protocol: 'up33',
|
||||
address: wethUp.addr,
|
||||
token0: wethUp.t0,
|
||||
token1: wethUp.t1,
|
||||
tickSpacing: wethUp.ts,
|
||||
feePpm: wethUp.fee,
|
||||
unstakedFeePpm: 0,
|
||||
sqrtPriceX96: wethUp.sqrtP,
|
||||
tick: wethUp.tick,
|
||||
liquidity: wethUp.liq,
|
||||
stakedLiquidity: 0n,
|
||||
gauge: null,
|
||||
gaugeAlive: false,
|
||||
weight: 0n,
|
||||
rewardRate: 0n,
|
||||
periodFinish: 0n,
|
||||
}
|
||||
const direct = await quoteDirectCandidates(pc, [smokePool], ADDR.WETH, ADDR.UP, oneWeth, 9)
|
||||
check(
|
||||
'direct comparison quotes Uniswap and UP33',
|
||||
direct.status.uniswap === 'quoted' && direct.status.up33 === 'quoted',
|
||||
`${direct.status.uniswap}/${direct.status.up33}`,
|
||||
)
|
||||
check(
|
||||
'direct UP33 quote is net of the exact 9 bps fee',
|
||||
!!quoterOut && direct.byProtocol.up33?.amountOut === netAfterFee(quoterOut, 9),
|
||||
direct.byProtocol.up33?.amountOut.toString() ?? 'no quote',
|
||||
)
|
||||
check(
|
||||
'direct comparison selects a concrete route',
|
||||
direct.best !== null,
|
||||
direct.best ? directRouteLabel(direct.best.route) : 'none',
|
||||
)
|
||||
}
|
||||
|
||||
// 7. v2 gauge standard selectors respond (gauge instances are unverified on Blockscout)
|
||||
|
||||
@@ -8,21 +8,8 @@ import { uniV3FactoryAbi, uniV3PmAbi, uniV3PoolAbi } from '../src/abi/index'
|
||||
import { ADDR, UNI } from '../src/config/addresses'
|
||||
import { getLiquidityForAmounts, getSqrtRatioAtTick, minAmountsForLiquidity } from '../src/lib/clmath'
|
||||
|
||||
// duplicated rather than imported: src/config/env.ts reads import.meta.env,
|
||||
// which is vite-only and does not load under node/tsx.
|
||||
const PUBLIC_RPC = 'https://rpc.mainnet.chain.robinhood.com'
|
||||
|
||||
/** repo-root .env `RPC` (SECRET — never print it). No .env / no key: public RPC. */
|
||||
const rpc = (() => {
|
||||
const fromEnv = process.env.RPC?.trim()
|
||||
if (fromEnv) return fromEnv
|
||||
try {
|
||||
const text = readFileSync(new URL('../.env', import.meta.url), 'utf8')
|
||||
return text.match(/^\s*RPC\s*=\s*(\S+)\s*$/m)?.[1] ?? PUBLIC_RPC
|
||||
} catch {
|
||||
return PUBLIC_RPC
|
||||
}
|
||||
})()
|
||||
const envText = readFileSync(new URL('../../.env', import.meta.url), 'utf8')
|
||||
const rpc = envText.match(/^\s*RPC\s*=\s*(\S+)\s*$/m)?.[1] ?? 'https://rpc.mainnet.chain.robinhood.com'
|
||||
const chain = defineChain({
|
||||
id: 4663,
|
||||
name: 'Robinhood Chain',
|
||||
|
||||
Reference in New Issue
Block a user