mirror of
https://github.com/labrinyang/lp-terminal.git
synced 2026-07-27 13:17: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>
519 lines
17 KiB
TypeScript
519 lines
17 KiB
TypeScript
import { useQuery } from '@tanstack/react-query'
|
|
import { usePublicClient } from 'wagmi'
|
|
import type { Address, PublicClient } from 'viem'
|
|
import { clGaugeAbi, clPmAbi, erc20Abi, uniV2PairAbi, uniV3FactoryAbi, uniV3PmAbi, uniV3PoolAbi, v2GaugeAbi, v2PoolAbi } from '../abi'
|
|
import { ADDR, CHAIN_ID, EXPLORER, UNI } from '../config/addresses'
|
|
import { MAX_UINT128, getAmountsForLiquidity, getSqrtRatioAtTick } from '../lib/clmath'
|
|
import { previewV2ClaimFees } from '../lib/v2Fees'
|
|
import type { ClPool, ClPosition, PoolsData, PositionsData, TokenInfo, V2Pool, V2Position } from '../types'
|
|
import { usePools } from './usePools'
|
|
|
|
type McRes = { status: 'success' | 'failure'; result?: unknown }
|
|
async function mc(pc: PublicClient, contracts: unknown[]): Promise<McRes[]> {
|
|
if (contracts.length === 0) return []
|
|
return (await pc.multicall({ contracts: contracts as never })) as McRes[]
|
|
}
|
|
function ok<T>(r: McRes | undefined): T | undefined {
|
|
return r && r.status === 'success' ? (r.result as T) : undefined
|
|
}
|
|
|
|
type RawPos = readonly [
|
|
bigint, // nonce
|
|
Address, // operator
|
|
Address, // token0
|
|
Address, // token1
|
|
number, // tickSpacing
|
|
number, // tickLower
|
|
number, // tickUpper
|
|
bigint, // liquidity
|
|
bigint,
|
|
bigint,
|
|
bigint, // tokensOwed0
|
|
bigint, // tokensOwed1
|
|
]
|
|
|
|
// same tuple shape as RawPos, but index 4 is the uint24 fee tier (univ3 NPMs
|
|
// are fee-keyed where Slipstream is tickSpacing-keyed)
|
|
type RawUniPos = RawPos
|
|
|
|
/**
|
|
* Uniswap v2 wallet positions. V2 LP is a plain ERC-20 — there is no NFT
|
|
* enumeration like the NPMs, and the official factory holds 15k+ pairs
|
|
* (mostly dust), so pair-side sweeps are out. Discover from the WALLET
|
|
* instead: Blockscout lists the address's ERC-20 holdings in one call, and
|
|
* every UNI-V2 entry is verified on-chain (factory() must be the official
|
|
* deployment — a spoofed "Uniswap V2" token fails this) with balance,
|
|
* reserves and supply read fresh. Blockscout being down hides univ2
|
|
* positions for that refresh only — same degrade contract as the univ3
|
|
* fetch below.
|
|
*/
|
|
async function fetchUniV2Positions(
|
|
pc: PublicClient,
|
|
user: Address,
|
|
): Promise<{ v2: V2Position[]; tokens: Record<string, TokenInfo> }> {
|
|
const res = await fetch(`${EXPLORER}/api/v2/addresses/${user}/tokens?type=ERC-20`, {
|
|
signal: AbortSignal.timeout(10_000),
|
|
})
|
|
if (!res.ok) throw new Error(`blockscout ${res.status}`)
|
|
type Held = { token?: { symbol?: string | null; address?: string; address_hash?: string }; value?: string }
|
|
const body = (await res.json()) as { items?: Held[] }
|
|
const pairs = [
|
|
...new Set(
|
|
(body.items ?? [])
|
|
.filter((it) => it.token?.symbol === 'UNI-V2' && BigInt(it.value ?? '0') > 0n)
|
|
.map((it) => (it.token?.address_hash ?? it.token?.address)?.toLowerCase())
|
|
.filter((a): a is string => !!a),
|
|
),
|
|
] as Address[]
|
|
if (pairs.length === 0) return { v2: [], tokens: {} }
|
|
|
|
const det = await mc(
|
|
pc,
|
|
pairs.flatMap((p) => [
|
|
{ abi: uniV2PairAbi, address: p, functionName: 'factory' },
|
|
{ abi: uniV2PairAbi, address: p, functionName: 'token0' },
|
|
{ abi: uniV2PairAbi, address: p, functionName: 'token1' },
|
|
{ abi: uniV2PairAbi, address: p, functionName: 'getReserves' },
|
|
{ abi: uniV2PairAbi, address: p, functionName: 'totalSupply' },
|
|
{ abi: uniV2PairAbi, address: p, functionName: 'balanceOf', args: [user] },
|
|
]),
|
|
)
|
|
|
|
const v2: V2Position[] = []
|
|
pairs.forEach((p, j) => {
|
|
const base = j * 6
|
|
const factory = ok<Address>(det[base])
|
|
const token0 = ok<Address>(det[base + 1])
|
|
const token1 = ok<Address>(det[base + 2])
|
|
const reserves = ok<readonly [bigint, bigint, number]>(det[base + 3])
|
|
const totalSupply = ok<bigint>(det[base + 4]) ?? 0n
|
|
const walletLp = ok<bigint>(det[base + 5]) ?? 0n
|
|
if (factory?.toLowerCase() !== UNI.V2_FACTORY.toLowerCase()) return
|
|
if (!token0 || !token1 || !reserves || walletLp === 0n || totalSupply === 0n) return
|
|
const pool: V2Pool = {
|
|
kind: 'v2',
|
|
protocol: 'univ2',
|
|
address: p,
|
|
token0,
|
|
token1,
|
|
stable: false,
|
|
reserve0: reserves[0],
|
|
reserve1: reserves[1],
|
|
totalSupply,
|
|
gaugeTotalSupply: 0n,
|
|
feeBps: 30, // uniswap v2 flat 0.30%, rolled into reserves
|
|
gauge: null,
|
|
gaugeAlive: false,
|
|
weight: 0n,
|
|
rewardRate: 0n,
|
|
periodFinish: 0n,
|
|
}
|
|
v2.push({
|
|
pool,
|
|
walletLp,
|
|
stakedLp: 0n,
|
|
earned: 0n,
|
|
claimable0: 0n,
|
|
claimable1: 0n,
|
|
amount0: (walletLp * reserves[0]) / totalSupply,
|
|
amount1: (walletLp * reserves[1]) / totalSupply,
|
|
})
|
|
})
|
|
|
|
// erc20 metadata for pair tokens outside the UP33 registry, so any pair
|
|
// renders with real symbols/decimals
|
|
const tokens: Record<string, TokenInfo> = {}
|
|
const uniq = [...new Set(v2.flatMap((r) => [r.pool.token0, r.pool.token1]))]
|
|
const meta = await mc(
|
|
pc,
|
|
uniq.flatMap((a) => [
|
|
{ abi: erc20Abi, address: a, functionName: 'symbol' },
|
|
{ abi: erc20Abi, address: a, functionName: 'decimals' },
|
|
]),
|
|
)
|
|
uniq.forEach((a, j) => {
|
|
tokens[a.toLowerCase()] = {
|
|
address: a,
|
|
symbol: ok<string>(meta[j * 2]) ?? a.slice(0, 6) + '…',
|
|
decimals: ok<number>(meta[j * 2 + 1]) ?? 18,
|
|
}
|
|
})
|
|
return { v2, tokens }
|
|
}
|
|
|
|
/**
|
|
* Uniswap v3 wallet positions (official Robinhood Chain deployment). Pools are
|
|
* discovered per position via factory.getPool and read fresh (slot0/liquidity/
|
|
* tickSpacing); tokens outside the UP33 registry get erc20 metadata fetched so
|
|
* any pair renders correctly. No gauges here — positions are never staked.
|
|
*/
|
|
async function fetchUniPositions(
|
|
pc: PublicClient,
|
|
user: Address,
|
|
pools: PoolsData,
|
|
): Promise<{ cl: ClPosition[]; tokens: Record<string, TokenInfo> }> {
|
|
const none = { cl: [], tokens: {} }
|
|
const cntRes = await mc(pc, [
|
|
{ abi: uniV3PmAbi, address: UNI.V3_NPM, functionName: 'balanceOf', args: [user] },
|
|
])
|
|
const count = Number(ok<bigint>(cntRes[0]) ?? 0n)
|
|
if (count === 0) return none
|
|
|
|
const idRes = await mc(
|
|
pc,
|
|
Array.from({ length: Math.min(count, 100) }, (_, i) => ({
|
|
abi: uniV3PmAbi,
|
|
address: UNI.V3_NPM,
|
|
functionName: 'tokenOfOwnerByIndex',
|
|
args: [user, BigInt(i)],
|
|
})),
|
|
)
|
|
const ids = idRes.map((r) => ok<bigint>(r)).filter((x): x is bigint => x !== undefined)
|
|
if (ids.length === 0) return none
|
|
|
|
const posRes = await mc(
|
|
pc,
|
|
ids.map((id) => ({ abi: uniV3PmAbi, address: UNI.V3_NPM, functionName: 'positions', args: [id] })),
|
|
)
|
|
const raws = ids
|
|
.map((id, j) => ({ id, raw: ok<RawUniPos>(posRes[j]) }))
|
|
.filter((x): x is { id: bigint; raw: RawUniPos } => !!x.raw)
|
|
// drop empty NFTs (closed positions linger in wallets)
|
|
.filter(({ raw }) => raw[7] > 0n || raw[10] > 0n || raw[11] > 0n)
|
|
if (raws.length === 0) return none
|
|
|
|
// resolve each distinct (token0, token1, fee) to its pool address
|
|
const poolKeys = new Map<string, { token0: Address; token1: Address; fee: number }>()
|
|
for (const { raw } of raws) {
|
|
poolKeys.set(`${raw[2].toLowerCase()}|${raw[3].toLowerCase()}|${raw[4]}`, {
|
|
token0: raw[2],
|
|
token1: raw[3],
|
|
fee: raw[4],
|
|
})
|
|
}
|
|
const keys = [...poolKeys.entries()]
|
|
const addrRes = await mc(
|
|
pc,
|
|
keys.map(([, k]) => ({
|
|
abi: uniV3FactoryAbi,
|
|
address: UNI.V3_FACTORY,
|
|
functionName: 'getPool',
|
|
args: [k.token0, k.token1, k.fee],
|
|
})),
|
|
)
|
|
|
|
// pool state + erc20 metadata for tokens the UP33 registry doesn't know
|
|
const unknownTokens = new Set<string>()
|
|
for (const [, k] of keys) {
|
|
for (const t of [k.token0, k.token1]) {
|
|
if (!pools.tokens[t.toLowerCase()]) unknownTokens.add(t.toLowerCase())
|
|
}
|
|
}
|
|
const tokenList = [...unknownTokens] as Address[]
|
|
const poolAddrs = keys.map(([, ], i) => ok<Address>(addrRes[i]))
|
|
const stateCalls: unknown[] = poolAddrs.flatMap((a) =>
|
|
a
|
|
? [
|
|
{ abi: uniV3PoolAbi, address: a, functionName: 'slot0' },
|
|
{ abi: uniV3PoolAbi, address: a, functionName: 'liquidity' },
|
|
{ abi: uniV3PoolAbi, address: a, functionName: 'tickSpacing' },
|
|
]
|
|
: [],
|
|
)
|
|
const metaCalls: unknown[] = tokenList.flatMap((t) => [
|
|
{ abi: erc20Abi, address: t, functionName: 'symbol' },
|
|
{ abi: erc20Abi, address: t, functionName: 'decimals' },
|
|
])
|
|
const r = await mc(pc, [...stateCalls, ...metaCalls])
|
|
|
|
const poolByKey = new Map<string, ClPool>()
|
|
let ri = 0
|
|
keys.forEach(([key, k], i) => {
|
|
const address = poolAddrs[i]
|
|
if (!address) return
|
|
const s0 = ok<readonly [bigint, number, number, number, number, number, boolean]>(r[ri])
|
|
const liquidity = ok<bigint>(r[ri + 1]) ?? 0n
|
|
const tickSpacing = ok<number>(r[ri + 2]) ?? 0
|
|
ri += 3
|
|
if (!s0) return
|
|
poolByKey.set(key, {
|
|
kind: 'cl',
|
|
protocol: 'univ3',
|
|
address,
|
|
token0: k.token0,
|
|
token1: k.token1,
|
|
tickSpacing,
|
|
feePpm: k.fee, // univ3 fee unit (hundredths of a bip) == ppm
|
|
unstakedFeePpm: 0,
|
|
sqrtPriceX96: s0[0],
|
|
tick: s0[1],
|
|
liquidity,
|
|
stakedLiquidity: 0n,
|
|
gauge: null,
|
|
gaugeAlive: false,
|
|
weight: 0n,
|
|
rewardRate: 0n,
|
|
periodFinish: 0n,
|
|
})
|
|
})
|
|
const tokens: Record<string, TokenInfo> = {}
|
|
tokenList.forEach((t, i) => {
|
|
const base = stateCalls.length + i * 2
|
|
tokens[t.toLowerCase()] = {
|
|
address: t,
|
|
symbol: ok<string>(r[base]) ?? t.slice(0, 6) + '…',
|
|
decimals: ok<number>(r[base + 1]) ?? 18,
|
|
}
|
|
})
|
|
|
|
const cl: ClPosition[] = []
|
|
for (const { id, raw } of raws) {
|
|
const pool = poolByKey.get(`${raw[2].toLowerCase()}|${raw[3].toLowerCase()}|${raw[4]}`)
|
|
if (!pool) continue
|
|
const { amount0, amount1 } = getAmountsForLiquidity(
|
|
pool.sqrtPriceX96,
|
|
getSqrtRatioAtTick(raw[5]),
|
|
getSqrtRatioAtTick(raw[6]),
|
|
raw[7],
|
|
)
|
|
cl.push({
|
|
tokenId: id,
|
|
pool,
|
|
tickLower: raw[5],
|
|
tickUpper: raw[6],
|
|
liquidity: raw[7],
|
|
staked: false,
|
|
amount0,
|
|
amount1,
|
|
fees0: raw[10],
|
|
fees1: raw[11],
|
|
earned: 0n,
|
|
})
|
|
}
|
|
return { cl, tokens }
|
|
}
|
|
|
|
async function fetchPositions(
|
|
pc: PublicClient,
|
|
user: Address,
|
|
pools: PoolsData,
|
|
): Promise<PositionsData> {
|
|
// univ3 discovery runs concurrently with the UP33 passes below
|
|
const uniP = fetchUniPositions(pc, user, pools).catch(() => ({ cl: [], tokens: {} }))
|
|
const uniV2P = fetchUniV2Positions(pc, user).catch(() => ({ v2: [], tokens: {} }))
|
|
const clPools = pools.pools.filter((p): p is ClPool => p.kind === 'cl')
|
|
const v2Pools = pools.pools.filter((p): p is V2Pool => p.kind === 'v2')
|
|
const clGauges = clPools.filter((p) => p.gauge)
|
|
|
|
// pass 1: counts + per-pool balances
|
|
const pass1: unknown[] = [
|
|
{ abi: clPmAbi, address: ADDR.CL_PM, functionName: 'balanceOf', args: [user] },
|
|
...clGauges.map((p) => ({
|
|
abi: clGaugeAbi,
|
|
address: p.gauge!,
|
|
functionName: 'stakedValues',
|
|
args: [user],
|
|
})),
|
|
...v2Pools.flatMap((p) => [
|
|
{ abi: v2PoolAbi, address: p.address, functionName: 'balanceOf', args: [user] },
|
|
{ abi: v2PoolAbi, address: p.address, functionName: 'claimable0', args: [user] },
|
|
{ abi: v2PoolAbi, address: p.address, functionName: 'claimable1', args: [user] },
|
|
...(p.gauge
|
|
? [
|
|
{ abi: v2GaugeAbi, address: p.gauge, functionName: 'balanceOf', args: [user] },
|
|
{ abi: v2GaugeAbi, address: p.gauge, functionName: 'earned', args: [user] },
|
|
]
|
|
: []),
|
|
]),
|
|
]
|
|
const r1 = await mc(pc, pass1)
|
|
let idx = 0
|
|
const walletCount = Number(ok<bigint>(r1[idx++]) ?? 0n)
|
|
const stakedIdsByGauge: { pool: ClPool; ids: bigint[] }[] = []
|
|
for (const p of clGauges) {
|
|
const ids = ok<readonly bigint[]>(r1[idx++]) ?? []
|
|
if (ids.length) stakedIdsByGauge.push({ pool: p, ids: [...ids] })
|
|
}
|
|
const v2Raw: {
|
|
pool: V2Pool
|
|
walletLp: bigint
|
|
claimable0: bigint
|
|
claimable1: bigint
|
|
stakedLp: bigint
|
|
earned: bigint
|
|
}[] = []
|
|
for (const p of v2Pools) {
|
|
const walletLp = ok<bigint>(r1[idx++]) ?? 0n
|
|
const claimable0 = ok<bigint>(r1[idx++]) ?? 0n
|
|
const claimable1 = ok<bigint>(r1[idx++]) ?? 0n
|
|
let stakedLp = 0n
|
|
let earned = 0n
|
|
if (p.gauge) {
|
|
stakedLp = ok<bigint>(r1[idx++]) ?? 0n
|
|
earned = ok<bigint>(r1[idx++]) ?? 0n
|
|
}
|
|
if (walletLp > 0n || stakedLp > 0n || claimable0 > 0n || claimable1 > 0n || earned > 0n) {
|
|
v2Raw.push({ pool: p, walletLp, claimable0, claimable1, stakedLp, earned })
|
|
}
|
|
}
|
|
|
|
// pass 2: wallet tokenIds
|
|
const idRes = await mc(
|
|
pc,
|
|
Array.from({ length: Math.min(walletCount, 100) }, (_, i) => ({
|
|
abi: clPmAbi,
|
|
address: ADDR.CL_PM,
|
|
functionName: 'tokenOfOwnerByIndex',
|
|
args: [user, BigInt(i)],
|
|
})),
|
|
)
|
|
const walletIds = idRes.map((r) => ok<bigint>(r)).filter((x): x is bigint => x !== undefined)
|
|
|
|
// pass 3: position structs (+ earned for staked)
|
|
const stakedFlat = stakedIdsByGauge.flatMap(({ pool, ids }) => ids.map((id) => ({ pool, id })))
|
|
const pass3: unknown[] = [
|
|
...walletIds.map((id) => ({
|
|
abi: clPmAbi,
|
|
address: ADDR.CL_PM,
|
|
functionName: 'positions',
|
|
args: [id],
|
|
})),
|
|
...stakedFlat.flatMap(({ pool, id }) => [
|
|
{ abi: clPmAbi, address: ADDR.CL_PM, functionName: 'positions', args: [id] },
|
|
{ abi: clGaugeAbi, address: pool.gauge!, functionName: 'earned', args: [user, id] },
|
|
]),
|
|
]
|
|
const r3 = await mc(pc, pass3)
|
|
|
|
const poolByKey = new Map<string, ClPool>()
|
|
for (const p of clPools) {
|
|
poolByKey.set(`${p.token0.toLowerCase()}|${p.token1.toLowerCase()}|${p.tickSpacing}`, p)
|
|
}
|
|
const findPool = (raw: RawPos): ClPool | undefined =>
|
|
poolByKey.get(`${raw[2].toLowerCase()}|${raw[3].toLowerCase()}|${raw[4]}`)
|
|
|
|
const cl: ClPosition[] = []
|
|
|
|
const buildPos = (
|
|
id: bigint,
|
|
raw: RawPos,
|
|
staked: boolean,
|
|
earned: bigint,
|
|
): ClPosition | null => {
|
|
const pool = findPool(raw)
|
|
if (!pool) return null
|
|
const liquidity = raw[7]
|
|
const { amount0, amount1 } = getAmountsForLiquidity(
|
|
pool.sqrtPriceX96,
|
|
getSqrtRatioAtTick(raw[5]),
|
|
getSqrtRatioAtTick(raw[6]),
|
|
liquidity,
|
|
)
|
|
return {
|
|
tokenId: id,
|
|
pool,
|
|
tickLower: raw[5],
|
|
tickUpper: raw[6],
|
|
liquidity,
|
|
staked,
|
|
amount0,
|
|
amount1,
|
|
fees0: raw[10],
|
|
fees1: raw[11],
|
|
earned,
|
|
}
|
|
}
|
|
|
|
walletIds.forEach((id, j) => {
|
|
const raw = ok<RawPos>(r3[j])
|
|
if (!raw) return
|
|
const pos = buildPos(id, raw, false, 0n)
|
|
if (pos && (pos.liquidity > 0n || pos.fees0 > 0n || pos.fees1 > 0n)) cl.push(pos)
|
|
})
|
|
stakedFlat.forEach(({ id }, j) => {
|
|
const base = walletIds.length + j * 2
|
|
const raw = ok<RawPos>(r3[base])
|
|
const earned = ok<bigint>(r3[base + 1]) ?? 0n
|
|
if (!raw) return
|
|
const pos = buildPos(id, raw, true, earned)
|
|
if (pos) cl.push(pos)
|
|
})
|
|
|
|
// univ3 wallet positions join here so the fee simulation below covers them
|
|
const uni = await uniP
|
|
cl.push(...uni.cl)
|
|
|
|
// pass 4: exact uncollected fees for wallet positions via collect() simulation
|
|
// (collect is signature-identical on both NPMs — only the address differs)
|
|
await Promise.all(
|
|
cl
|
|
.filter((p) => !p.staked)
|
|
.map(async (p) => {
|
|
try {
|
|
const sim = await pc.simulateContract({
|
|
abi: clPmAbi,
|
|
address: p.pool.protocol === 'univ3' ? UNI.V3_NPM : ADDR.CL_PM,
|
|
functionName: 'collect',
|
|
args: [
|
|
{
|
|
tokenId: p.tokenId,
|
|
recipient: user,
|
|
amount0Max: MAX_UINT128,
|
|
amount1Max: MAX_UINT128,
|
|
},
|
|
],
|
|
account: user,
|
|
})
|
|
const [f0, f1] = sim.result as readonly [bigint, bigint]
|
|
p.fees0 = f0
|
|
p.fees1 = f1
|
|
} catch {
|
|
/* keep tokensOwed fallback */
|
|
}
|
|
}),
|
|
)
|
|
|
|
// Solidly claimable getters only expose fees materialized by a prior pool
|
|
// interaction. Simulating claimFees as the owner includes the latest index.
|
|
await Promise.all(
|
|
v2Raw
|
|
.filter((r) => r.pool.protocol === 'up33' && r.walletLp > 0n)
|
|
.map(async (r) => {
|
|
const [fee0, fee1] = await previewV2ClaimFees(pc, r.pool.address, user, [r.claimable0, r.claimable1])
|
|
r.claimable0 = fee0
|
|
r.claimable1 = fee1
|
|
}),
|
|
)
|
|
|
|
const v2: V2Position[] = v2Raw.map((r) => {
|
|
const lp = r.walletLp + r.stakedLp
|
|
const ts = r.pool.totalSupply
|
|
return {
|
|
pool: r.pool,
|
|
walletLp: r.walletLp,
|
|
stakedLp: r.stakedLp,
|
|
earned: r.earned,
|
|
claimable0: r.claimable0,
|
|
claimable1: r.claimable1,
|
|
amount0: ts > 0n ? (lp * r.pool.reserve0) / ts : 0n,
|
|
amount1: ts > 0n ? (lp * r.pool.reserve1) / ts : 0n,
|
|
}
|
|
})
|
|
|
|
const uniV2 = await uniV2P
|
|
v2.push(...uniV2.v2)
|
|
|
|
return { cl, v2, tokens: { ...uni.tokens, ...uniV2.tokens } }
|
|
}
|
|
|
|
export function usePositions(user?: Address) {
|
|
const pc = usePublicClient({ chainId: CHAIN_ID })
|
|
const pools = usePools()
|
|
return useQuery({
|
|
queryKey: ['positions', user],
|
|
enabled: !!pc && !!user && !!pools.data,
|
|
refetchInterval: 15_000,
|
|
queryFn: () => fetchPositions(pc as PublicClient, user!, pools.data!),
|
|
})
|
|
}
|