浏览代码

feat: add liquidity management features and UI improvements

- Add new lp-add API endpoint for adding liquidity to existing positions
- Update lp-copy to use ByReal API for accurate USD pricing and add USD1 stablecoin support
- Replace text links with icon buttons in operation columns (ExportOutlined, CloseCircleOutlined, PlusCircleOutlined, ThunderboltOutlined)
- Add ROI column showing liquidity/(PNL+Bonus) percentage
- Improve price range status display with color coding (green/orange/red)
- Move 'View Parent' link to parent address column
- Adjust font sizes for better readability
lushdog@outlook.com 1 月之前
父节点
当前提交
61fa17a453
共有 4 个文件被更改,包括 567 次插入73 次删除
  1. 74 21
      src/app/api/lp-copy/route.ts
  2. 388 0
      src/app/api/lp-index/lp-add/route.ts
  3. 35 14
      src/app/components/DataTable.tsx
  4. 70 38
      src/app/my-lp/page.tsx

+ 74 - 21
src/app/api/lp-copy/route.ts

@@ -87,36 +87,70 @@ async function copyLPPosition(
 		// 稳定币地址
 		const USDC_ADDRESS = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'
 		const USDT_ADDRESS = 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB'
+		const USD1_ADDRESS = 'USD1ttGY1N17NEEHLmELoaybftRBUSErhqYiQzvEmuB'
 
 		const tokenAAddress = poolInfo.mintA.toBase58()
 		const tokenBAddress = poolInfo.mintB.toBase58()
 
 		// 判断哪个 token 是稳定币
 		const isTokenAStable =
-			tokenAAddress === USDC_ADDRESS || tokenAAddress === USDT_ADDRESS
+			tokenAAddress === USDC_ADDRESS ||
+			tokenAAddress === USDT_ADDRESS ||
+			tokenAAddress === USD1_ADDRESS
 		const isTokenBStable =
-			tokenBAddress === USDC_ADDRESS || tokenBAddress === USDT_ADDRESS
+			tokenBAddress === USDC_ADDRESS ||
+			tokenBAddress === USDT_ADDRESS ||
+			tokenBAddress === USD1_ADDRESS
 
 		// 计算 token 的 USD 价格
-		let tokenAPriceUsd: number
-		let tokenBPriceUsd: number
-
-		if (isTokenBStable) {
-			// tokenB 是稳定币,价格为 1 USD
-			tokenBPriceUsd = 1
-			// tokenA 的价格 = currentPrice (A/B) * tokenB 价格
-			tokenAPriceUsd = currentPrice
-		} else if (isTokenAStable) {
-			// tokenA 是稳定币,价格为 1 USD
-			tokenAPriceUsd = 1
-			// tokenB 的价格 = 1 / currentPrice (因为 currentPrice = A/B)
-			tokenBPriceUsd = 1 / currentPrice
-		} else {
-			// 如果都不是稳定币,假设其中一个的价格为 1(简化处理)
-			// 或者可以从价格 API 获取实际价格
-			// 这里假设 tokenB 价格为 1,tokenA 价格为 currentPrice
-			tokenBPriceUsd = 1
-			tokenAPriceUsd = currentPrice
+		let tokenAPriceUsd = 0
+		let tokenBPriceUsd = 0
+		let priceFromApi = false
+
+		// 首先尝试从 ByReal API 获取真实 USD 价格
+		try {
+			const detailData = await fetch(
+				`https://api2.byreal.io/byreal/api/dex/v2/position/detail?address=${positionAddress}`
+			).then((res) => res.json())
+
+			const poolData = detailData.result?.data?.pool
+			if (poolData?.mintA?.price && poolData?.mintB?.price) {
+				tokenAPriceUsd = parseFloat(poolData.mintA.price)
+				tokenBPriceUsd = parseFloat(poolData.mintB.price)
+				priceFromApi = true
+				console.log('Using prices from ByReal API:', {
+					tokenAPriceUsd,
+					tokenBPriceUsd,
+				})
+			}
+		} catch (error) {
+			console.warn('Failed to fetch prices from API:', error)
+		}
+
+		// 如果 API 获取失败,使用稳定币逻辑
+		if (!priceFromApi) {
+			if (isTokenBStable) {
+				// tokenB 是稳定币,价格为 1 USD
+				tokenBPriceUsd = 1
+				// tokenA 的价格 = currentPrice (A/B) * tokenB 价格
+				tokenAPriceUsd = currentPrice
+			} else if (isTokenAStable) {
+				// tokenA 是稳定币,价格为 1 USD
+				tokenAPriceUsd = 1
+				// tokenB 的价格 = 1 / currentPrice (因为 currentPrice = A/B)
+				tokenBPriceUsd = 1 / currentPrice
+			} else {
+				// 如果都不是稳定币且无法获取 API 价格,返回错误
+				return NextResponse.json(
+					{
+						error:
+							'无法获取代币价格:非稳定币交易对且 API 价格获取失败。请确保交易对包含 USDC/USDT 或稍后重试。',
+						tokenA: tokenAAddress,
+						tokenB: tokenBAddress,
+					},
+					{ status: 400 }
+				)
+			}
 		}
 
 		console.log('tokenAPriceUsd', tokenAPriceUsd)
@@ -466,6 +500,25 @@ async function copyLPPosition(
 		console.log('Total Value (USD):', totalValue.toFixed(2))
 		console.log('==========================================\n')
 
+		// 检查代币数量是否为0
+		if (baseAmount.eq(new BN(0)) || otherAmountMax.eq(new BN(0))) {
+			console.error('Error: One of the token amounts is zero')
+			return NextResponse.json(
+				{
+					error:
+						'添加LP失败:其中一个代币数量为0。当前SDK不支持单币添加流动性,请确保两个代币都有数量。',
+					details: {
+						baseAmount: baseAmount.toString(),
+						otherAmountMax: otherAmountMax.toString(),
+						base,
+						tokenAAmount: finalUiAmountA.toString(),
+						tokenBAmount: finalUiAmountB.toString(),
+					},
+				},
+				{ status: 400 }
+			)
+		}
+
 		// 从环境变量读取私钥
 		const secretKey = process.env.SOL_SECRET_KEY
 		if (!secretKey) {

+ 388 - 0
src/app/api/lp-index/lp-add/route.ts

@@ -0,0 +1,388 @@
+import { NextRequest, NextResponse } from 'next/server'
+import { Keypair, PublicKey, VersionedTransaction } from '@solana/web3.js'
+import BN from 'bn.js'
+import { Decimal } from 'decimal.js'
+import bs58 from 'bs58'
+import { chain } from '@/lib/config'
+
+export async function POST(request: NextRequest) {
+	try {
+		const body = await request.json()
+		const { nftMintAddress, addUsdValue } = body
+
+		// 验证输入
+		if (!nftMintAddress) {
+			return NextResponse.json(
+				{ error: 'Position NFT address is required' },
+				{ status: 400 }
+			)
+		}
+
+		if (!addUsdValue || addUsdValue <= 0) {
+			return NextResponse.json(
+				{ error: 'Add USD value must be greater than 0' },
+				{ status: 400 }
+			)
+		}
+
+		const nftMint = new PublicKey(nftMintAddress)
+
+		// 获取仓位信息
+		const positionInfo = await chain.getPositionInfoByNftMint(nftMint)
+		if (!positionInfo) {
+			return NextResponse.json({ error: 'Position not found' }, { status: 404 })
+		}
+
+		const { rawPoolInfo } = positionInfo
+		const poolInfo = rawPoolInfo
+
+		// 使用 currentPrice (A/B 价格) 来计算 token 的 USD 价格
+		const currentPrice = poolInfo.currentPrice
+
+		// 稳定币地址
+		const USDC_ADDRESS = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'
+		const USDT_ADDRESS = 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB'
+		const USD1_ADDRESS = 'USD1ttGY1N17NEEHLmELoaybftRBUSErhqYiQzvEmuB'
+
+		const tokenAAddress = poolInfo.mintA.toBase58()
+		const tokenBAddress = poolInfo.mintB.toBase58()
+
+		// 判断哪个 token 是稳定币
+		const isTokenAStable =
+			tokenAAddress === USDC_ADDRESS ||
+			tokenAAddress === USDT_ADDRESS ||
+			tokenAAddress === USD1_ADDRESS
+		const isTokenBStable =
+			tokenBAddress === USDC_ADDRESS ||
+			tokenBAddress === USDT_ADDRESS ||
+			tokenBAddress === USD1_ADDRESS
+
+		// 计算 token 的 USD 价格
+		let tokenAPriceUsd = 0
+		let tokenBPriceUsd = 0
+		let priceFromApi = false
+
+		// 尝试从 ByReal API 获取真实价格
+		try {
+			const response = await fetch(
+				`https://api2.byreal.io/byreal/api/dex/v2/position/detail?address=${nftMintAddress}`,
+				{
+					method: 'GET',
+					headers: {
+						accept: 'application/json',
+					},
+				}
+			)
+
+			if (response.ok) {
+				const data = await response.json()
+				const poolData = data.result?.data?.pool
+
+				if (poolData?.mintA?.price && poolData?.mintB?.price) {
+					tokenAPriceUsd = parseFloat(poolData.mintA.price)
+					tokenBPriceUsd = parseFloat(poolData.mintB.price)
+					priceFromApi = true
+					console.log('Using prices from ByReal API:', {
+						tokenAPriceUsd,
+						tokenBPriceUsd,
+					})
+				}
+			}
+		} catch (error) {
+			console.warn('Failed to fetch prices from API:', error)
+		}
+
+		// 如果 API 获取失败,使用稳定币逻辑或默认逻辑
+		if (!priceFromApi) {
+			if (isTokenBStable) {
+				tokenBPriceUsd = 1
+				tokenAPriceUsd = currentPrice
+			} else if (isTokenAStable) {
+				tokenAPriceUsd = 1
+				tokenBPriceUsd = 1 / currentPrice
+			} else {
+				// 非稳定币对且无法获取价格时,使用相对价格
+				// 以 tokenB 为基准(价格为1),tokenA 价格为 currentPrice
+				tokenBPriceUsd = 1
+				tokenAPriceUsd = currentPrice
+				console.warn(
+					'Warning: Non-stablecoin pair without API prices. Using relative pricing.'
+				)
+			}
+		}
+
+		// 获取仓位的当前价格范围
+		const priceLower = positionInfo.priceLower
+		const priceUpper = positionInfo.priceUpper
+
+		// 使用较小的 token 作为 base
+		const useTokenAAsBase = tokenAPriceUsd <= tokenBPriceUsd
+		const base = useTokenAAsBase ? 'MintA' : 'MintB'
+
+		// 计算 baseAmount
+		const targetValue = addUsdValue * 0.995 // 99.5%,更接近最大值
+		let baseAmount: BN
+		let otherAmountMax: BN
+
+		if (base === 'MintA') {
+			const estimatedPricePerBase =
+				tokenAPriceUsd +
+				((priceLower.toNumber() + priceUpper.toNumber()) / 2) * tokenBPriceUsd
+
+			// 迭代调整 baseAmount
+			let low = new BN(0)
+			let high = new BN(
+				Math.ceil(
+					(targetValue / estimatedPricePerBase) *
+						10 ** poolInfo.mintDecimalsA *
+						1.5
+				)
+			)
+			let bestBaseAmount = new BN(0)
+			let bestValue = 0
+
+			for (let i = 0; i < 30; i++) {
+				const mid = low.add(high).div(new BN(2))
+				if (mid.eq(low) || mid.eq(high)) break
+
+				const testBaseAmount = mid
+				const testOtherAmount = chain.getAmountBFromAmountA({
+					priceLower,
+					priceUpper,
+					amountA: testBaseAmount,
+					poolInfo,
+				})
+
+				const testUiAmountA = new Decimal(testBaseAmount.toString()).div(
+					10 ** poolInfo.mintDecimalsA
+				)
+				const testUiAmountB = new Decimal(testOtherAmount.toString()).div(
+					10 ** poolInfo.mintDecimalsB
+				)
+				const testValue =
+					testUiAmountA.toNumber() * tokenAPriceUsd +
+					testUiAmountB.toNumber() * tokenBPriceUsd
+
+				if (testValue <= targetValue && testValue > bestValue) {
+					bestValue = testValue
+					bestBaseAmount = testBaseAmount
+				}
+
+				if (testValue > targetValue) {
+					high = mid
+				} else {
+					low = mid
+				}
+			}
+
+			baseAmount = bestBaseAmount.gt(new BN(0)) ? bestBaseAmount : low
+
+			// 计算需要的 tokenB 数量
+			const otherAmountNeeded = chain.getAmountBFromAmountA({
+				priceLower,
+				priceUpper,
+				amountA: baseAmount,
+				poolInfo,
+			})
+
+			// 添加 2% slippage
+			otherAmountMax = otherAmountNeeded.mul(new BN(10200)).div(new BN(10000))
+		} else {
+			const midPrice = priceLower.add(priceUpper).div(2)
+			const estimatedPricePerBase =
+				tokenBPriceUsd + (1 / midPrice.toNumber()) * tokenAPriceUsd
+
+			let low = new BN(0)
+			let high = new BN(
+				Math.ceil(
+					(targetValue / estimatedPricePerBase) *
+						10 ** poolInfo.mintDecimalsB *
+						1.5
+				)
+			)
+			let bestBaseAmount = new BN(0)
+			let bestValue = 0
+
+			for (let i = 0; i < 30; i++) {
+				const mid = low.add(high).div(new BN(2))
+				if (mid.eq(low) || mid.eq(high)) break
+
+				const testBaseAmount = mid
+				const testOtherAmount = chain.getAmountAFromAmountB({
+					priceLower,
+					priceUpper,
+					amountB: testBaseAmount,
+					poolInfo,
+				})
+
+				const testUiAmountA = new Decimal(testOtherAmount.toString()).div(
+					10 ** poolInfo.mintDecimalsA
+				)
+				const testUiAmountB = new Decimal(testBaseAmount.toString()).div(
+					10 ** poolInfo.mintDecimalsB
+				)
+				const testValue =
+					testUiAmountA.toNumber() * tokenAPriceUsd +
+					testUiAmountB.toNumber() * tokenBPriceUsd
+
+				if (testValue <= targetValue && testValue > bestValue) {
+					bestValue = testValue
+					bestBaseAmount = testBaseAmount
+				}
+
+				if (testValue > targetValue) {
+					high = mid
+				} else {
+					low = mid
+				}
+			}
+
+			baseAmount = bestBaseAmount.gt(new BN(0)) ? bestBaseAmount : low
+
+			// 计算需要的 tokenA 数量
+			const otherAmountNeeded = chain.getAmountAFromAmountB({
+				priceLower,
+				priceUpper,
+				amountB: baseAmount,
+				poolInfo,
+			})
+
+			// 添加 2% slippage
+			otherAmountMax = otherAmountNeeded.mul(new BN(10200)).div(new BN(10000))
+		}
+
+		// 检查代币数量是否为0
+		if (baseAmount.eq(new BN(0)) || otherAmountMax.eq(new BN(0))) {
+			console.error('Error: One of the token amounts is zero')
+			return NextResponse.json(
+				{
+					error:
+						'添加流动性失败:其中一个代币数量为0。请确保两个代币都有数量。',
+					details: {
+						baseAmount: baseAmount.toString(),
+						otherAmountMax: otherAmountMax.toString(),
+						base,
+					},
+				},
+				{ status: 400 }
+			)
+		}
+
+		// 计算最终 UI 金额
+		const uiAmountA =
+			base === 'MintA'
+				? new Decimal(baseAmount.toString()).div(10 ** poolInfo.mintDecimalsA)
+				: new Decimal(otherAmountMax.toString()).div(
+						10 ** poolInfo.mintDecimalsA
+					)
+		const uiAmountB =
+			base === 'MintB'
+				? new Decimal(baseAmount.toString()).div(10 ** poolInfo.mintDecimalsB)
+				: new Decimal(otherAmountMax.toString()).div(
+						10 ** poolInfo.mintDecimalsB
+					)
+
+		// 打印所有信息
+		console.log('\n========== Add Liquidity Information ==========')
+		console.log('Position NFT Address:', nftMint.toBase58())
+		console.log('\n--- Pool Information ---')
+		console.log('Pool Address:', poolInfo.poolId.toBase58())
+		console.log('Token A Address:', poolInfo.mintA.toBase58())
+		console.log('Token B Address:', poolInfo.mintB.toBase58())
+		console.log('Token A Decimals:', poolInfo.mintDecimalsA)
+		console.log('Token B Decimals:', poolInfo.mintDecimalsB)
+		console.log('Current Price (A/B):', currentPrice)
+		console.log('Token A Price (USD):', tokenAPriceUsd)
+		console.log('Token B Price (USD):', tokenBPriceUsd)
+		console.log('\n--- Position Range ---')
+		console.log('Price Lower:', priceLower.toString())
+		console.log('Price Upper:', priceUpper.toString())
+		console.log('\n--- Investment Details ---')
+		console.log('Add USD Value:', addUsdValue)
+		console.log('Base Token:', base)
+		console.log('Base Amount (raw):', baseAmount.toString())
+		console.log('Other Amount Max (raw):', otherAmountMax.toString())
+		console.log('Token A Amount (UI):', uiAmountA.toString())
+		console.log('Token B Amount (UI):', uiAmountB.toString())
+		console.log(
+			'Token A Value (USD):',
+			(uiAmountA.toNumber() * tokenAPriceUsd).toFixed(2)
+		)
+		console.log(
+			'Token B Value (USD):',
+			(uiAmountB.toNumber() * tokenBPriceUsd).toFixed(2)
+		)
+		console.log('==========================================\n')
+
+		// 从环境变量读取私钥
+		const secretKey = process.env.SOL_SECRET_KEY
+		if (!secretKey) {
+			return NextResponse.json(
+				{ error: 'SOL_SECRET_KEY not configured' },
+				{ status: 500 }
+			)
+		}
+
+		const userKeypair = Keypair.fromSecretKey(bs58.decode(secretKey))
+		const userAddress = userKeypair.publicKey
+
+		console.log('User Address:', userAddress.toBase58())
+		console.log('\n--- Executing Transaction ---')
+		console.log('Adding liquidity on-chain...')
+
+		// 执行实际上链操作
+		const signerCallback = async (tx: VersionedTransaction) => {
+			tx.sign([userKeypair])
+			return tx
+		}
+
+		const txid = await chain.addLiquidity({
+			userAddress,
+			nftMint,
+			base,
+			baseAmount,
+			otherAmountMax,
+			signerCallback,
+		})
+
+		console.log('Transaction ID:', txid)
+		console.log('Liquidity added successfully!')
+		console.log('==========================================\n')
+
+		return NextResponse.json({
+			success: true,
+			txid,
+			positionInfo: {
+				nftMint: nftMint.toBase58(),
+				poolAddress: poolInfo.poolId.toBase58(),
+				priceLower: priceLower.toString(),
+				priceUpper: priceUpper.toString(),
+				base,
+				baseAmount: baseAmount.toString(),
+				otherAmountMax: otherAmountMax.toString(),
+				tokenA: {
+					address: poolInfo.mintA.toBase58(),
+					amount: uiAmountA.toString(),
+					valueUsd: (uiAmountA.toNumber() * tokenAPriceUsd).toFixed(2),
+				},
+				tokenB: {
+					address: poolInfo.mintB.toBase58(),
+					amount: uiAmountB.toString(),
+					valueUsd: (uiAmountB.toNumber() * tokenBPriceUsd).toFixed(2),
+				},
+				userAddress: userAddress?.toBase58() || 'N/A',
+			},
+		})
+	} catch (error: unknown) {
+		console.error('Add Liquidity Error:', error)
+		const errorMessage =
+			error instanceof Error ? error.message : 'Failed to add liquidity'
+
+		// 如果错误包含 "insufficient funds",返回友好的余额不足提示
+		if (errorMessage.includes('insufficient funds')) {
+			return NextResponse.json({ error: '余额不足' }, { status: 400 })
+		}
+
+		return NextResponse.json({ error: errorMessage }, { status: 500 })
+	}
+}

+ 35 - 14
src/app/components/DataTable.tsx

@@ -2,7 +2,21 @@
 
 import { TickMath } from '@/lib/byreal-clmm-sdk/src/instructions/utils/tickMath'
 import { useEffect, useState, useRef } from 'react'
-import { Table, Select, Typography, Tag, Button, InputNumber, App } from 'antd'
+import {
+	Table,
+	Select,
+	Typography,
+	Tag,
+	Button,
+	InputNumber,
+	App,
+	Tooltip,
+} from 'antd'
+import {
+	ExportOutlined,
+	CopyOutlined,
+	ThunderboltOutlined,
+} from '@ant-design/icons'
 
 // 黑名单表格不显示(小额/BOT地址)
 const BLACK_LIST_ADDRESSES = ['LoVe', 'HZEQ', 'mVBk', 'enVr', 'MV9K']
@@ -606,21 +620,28 @@ function DataTableContent() {
 			title: '操作',
 			dataIndex: 'walletAddress',
 			key: 'walletAddress',
+			width: 100,
 			render: (text: string, record: TableData) => {
 				return (
-					<div className="flex items-center gap-2">
-						<Typography.Link
-							href={`https://www.byreal.io/en/portfolio?userAddress=${text}&tab=current&positionAddress=${record.positionAddress}&tokenAddress=${record.tokenAaddress}&tokenAddress=${record.tokenBaddress}`}
-							target="_blank"
-						>
-							复制
-						</Typography.Link>
-						<Typography.Link
-							onClick={() => handleQuickCopy(record)}
-							target="_blank"
-						>
-							快速复制
-						</Typography.Link>
+					<div className="flex items-center gap-3">
+						<Tooltip title="复制">
+							<Typography.Link
+								href={`https://www.byreal.io/en/portfolio?userAddress=${text}&tab=current&positionAddress=${record.positionAddress}&tokenAddress=${record.tokenAaddress}&tokenAddress=${record.tokenBaddress}`}
+								target="_blank"
+							>
+								<ExportOutlined style={{ fontSize: '18px' }} />
+							</Typography.Link>
+						</Tooltip>
+						<Tooltip title="快速复制">
+							<Typography.Link
+								onClick={() => handleQuickCopy(record)}
+								style={{ color: '#1890ff' }}
+							>
+								<ThunderboltOutlined
+									style={{ fontSize: '18px', color: '#1890ff' }}
+								/>
+							</Typography.Link>
+						</Tooltip>
 					</div>
 				)
 			},

+ 70 - 38
src/app/my-lp/page.tsx

@@ -13,7 +13,13 @@ import {
 	Tag,
 	InputNumber,
 	Select,
+	Tooltip,
 } from 'antd'
+import {
+	ExportOutlined,
+	CloseCircleOutlined,
+	PlusCircleOutlined,
+} from '@ant-design/icons'
 import type { ColumnsType, TablePaginationConfig } from 'antd/es/table'
 import dayjs from 'dayjs'
 import pLimit from 'p-limit'
@@ -314,14 +320,13 @@ function MyLPPageContent() {
 				/>
 			),
 			onOk: async () => {
-				const quickCopyAmount = inputNumberRef.current?.value
+				const addAmount = inputNumberRef.current?.value
 				message.destroy('addPosition')
-				return fetch('/api/lp-copy', {
+				return fetch('/api/lp-index/lp-add', {
 					method: 'POST',
 					body: JSON.stringify({
-						positionAddress: record.positionAddress,
 						nftMintAddress: record.nftMintAddress,
-						maxUsdValue: quickCopyAmount,
+						addUsdValue: addAmount,
 					}),
 				})
 					.then((res) => res.json())
@@ -410,7 +415,7 @@ function MyLPPageContent() {
 			dataIndex: 'allCopyAmount',
 			key: 'allCopyAmount',
 			render: (text: string, record: RecordInfo) => (
-				<span className="font-mono text-sm text-orange-500">
+				<span className="font-mono text-base text-orange-500">
 					${Number(text).toFixed(2)}
 					<span className="text-sm text-blue-500">({record.allCopys})</span>
 				</span>
@@ -431,12 +436,18 @@ function MyLPPageContent() {
 			dataIndex: 'fromCreatorWallet',
 			key: 'fromCreatorWallet',
 			render: (text: string, record: RecordInfo) => (
-				<span className="font-mono text-sm text-blue-500">
-					{record?.bonusInfo?.fromCreatorWallet
-						? record?.bonusInfo?.fromCreatorWallet?.slice(0, 6) +
-							'...' +
-							record?.bonusInfo?.fromCreatorWallet?.slice(-4)
-						: '无上级'}
+				<span className="font-mono text-sm">
+					{record?.bonusInfo?.fromCreatorWallet ? (
+						<Typography.Link
+							href={`https://www.byreal.io/en/portfolio?userAddress=${record.bonusInfo.fromCreatorWallet}&tab=current&positionAddress=${record.bonusInfo.fromCreatorPosition}&tokenAddress=${getPoolInfo(record.poolAddress).tokenAAddress}&tokenAddress=${getPoolInfo(record.poolAddress).tokenBAddress}`}
+							target="_blank"
+						>
+							{record.bonusInfo.fromCreatorWallet.slice(0, 6)}...
+							{record.bonusInfo.fromCreatorWallet.slice(-4)}
+						</Typography.Link>
+					) : (
+						<span className="text-gray-500">无上级</span>
+					)}
 				</span>
 			),
 		},
@@ -459,7 +470,7 @@ function MyLPPageContent() {
 			key: 'pnlUsdPercent',
 			render: (text: string) => (
 				<span
-					className="font-mono text-sm"
+					className="font-mono text-base"
 					style={{ color: Number(text) > 0 ? '#00B098' : '#FF0000' }}
 				>
 					{(Number(text) * 100).toFixed(2)}%
@@ -513,10 +524,10 @@ function MyLPPageContent() {
 				<span className="font-mono text-sm">
 					{text ? (
 						<Tag color="green">区间内</Tag>
+					) : record.priceOutType === 'priceUp' ? (
+						<Tag color="orange">超出上限</Tag>
 					) : (
-						<Tag color="red">
-							{record.priceOutType === 'priceUp' ? '超出上限' : '低于下限'}
-						</Tag>
+						<Tag color="red">低于下限</Tag>
 					)}
 				</span>
 			),
@@ -531,6 +542,26 @@ function MyLPPageContent() {
 				</span>
 			),
 		},
+		{
+			title: 'ROI',
+			dataIndex: 'roi',
+			key: 'roi',
+			render: (_text: string, record: RecordInfo) => {
+				const liquidity = Number(record.liquidityUsd) || 0
+				const pnl = Number(record.pnlUsd) || 0
+				const bonus = Number(record.bonusUsd) || 0
+				const totalReturn = pnl + bonus
+				const roi = liquidity !== 0 ? (totalReturn / liquidity) * 100 : 0
+				return (
+					<span
+						className="font-mono text-base font-bold"
+						style={{ color: roi > 0 ? '#00B098' : '#FF0000' }}
+					>
+						{roi.toFixed(1)}%
+					</span>
+				)
+			},
+		},
 		{
 			title: '开仓时间',
 			dataIndex: 'openTime',
@@ -580,31 +611,32 @@ function MyLPPageContent() {
 		{
 			title: '操作',
 			dataIndex: 'bonusInfo',
-			width: 200,
+			width: 120,
 			key: 'bonusInfo',
 			render: (text: string, record: RecordInfo) => (
-				<div className="flex items-center gap-2">
-					{record?.bonusInfo?.fromCreatorWallet &&
-						record?.bonusInfo?.fromCreatorPosition && (
-							<Typography.Link
-								href={`https://www.byreal.io/en/portfolio?userAddress=${record.bonusInfo.fromCreatorWallet}&tab=current&positionAddress=${record.bonusInfo.fromCreatorPosition}&tokenAddress=${getPoolInfo(record.poolAddress).tokenAAddress}&tokenAddress=${getPoolInfo(record.poolAddress).tokenBAddress}`}
-								target="_blank"
-							>
-								查看上级
-							</Typography.Link>
-						)}
-					<Typography.Link
-						href={`https://www.byreal.io/en/portfolio?userAddress=${record.walletAddress}&tab=current&positionAddress=${record.positionAddress}&tokenAddress=${getPoolInfo(record.poolAddress).tokenAAddress}&tokenAddress=${getPoolInfo(record.poolAddress).tokenBAddress}`}
-						target="_blank"
-					>
-						去关仓
-					</Typography.Link>
-					<Typography.Link onClick={() => handleClosePosition(record)}>
-						快速关仓
-					</Typography.Link>
-					<Typography.Link onClick={() => handleAddPosition(record)}>
-						快速加仓
-					</Typography.Link>
+				<div className="flex items-center gap-3">
+					<Tooltip title="去关仓">
+						<Typography.Link
+							href={`https://www.byreal.io/en/portfolio?userAddress=${record.walletAddress}&tab=current&positionAddress=${record.positionAddress}&tokenAddress=${getPoolInfo(record.poolAddress).tokenAAddress}&tokenAddress=${getPoolInfo(record.poolAddress).tokenBAddress}`}
+							target="_blank"
+						>
+							<ExportOutlined style={{ fontSize: '18px' }} />
+						</Typography.Link>
+					</Tooltip>
+					<Tooltip title="快速关仓">
+						<Typography.Link onClick={() => handleClosePosition(record)}>
+							<CloseCircleOutlined
+								style={{ fontSize: '18px', color: '#ff4d4f' }}
+							/>
+						</Typography.Link>
+					</Tooltip>
+					<Tooltip title="快速加仓">
+						<Typography.Link onClick={() => handleAddPosition(record)}>
+							<PlusCircleOutlined
+								style={{ fontSize: '18px', color: '#52c41a' }}
+							/>
+						</Typography.Link>
+					</Tooltip>
 				</div>
 			),
 		},