Explorar o código

添加详细信息

lushdog@outlook.com hai 1 mes
pai
achega
34e77262cb

+ 1 - 0
package.json

@@ -21,6 +21,7 @@
 		"antd": "^6.0.1",
 		"bn.js": "^5.2.2",
 		"bs58": "^6.0.0",
+		"dayjs": "^1.11.19",
 		"decimal.js": "^10.6.0",
 		"ky": "^1.14.2",
 		"lodash-es": "^4.17.22",

+ 5 - 2
pnpm-lock.yaml

@@ -38,6 +38,9 @@ importers:
       bs58:
         specifier: ^6.0.0
         version: 6.0.0
+      dayjs:
+        specifier: ^1.11.19
+        version: 1.11.19
       decimal.js:
         specifier: ^10.6.0
         version: 10.6.0
@@ -986,7 +989,7 @@ packages:
     resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==, tarball: https://pkgs.d.xiaomi.net:443/artifactory/api/npm/mi-npm/@tybys/wasm-util/-/wasm-util-0.10.1.tgz}
 
   '@types/bn.js@5.2.0':
-    resolution: {integrity: sha512-DLbJ1BPqxvQhIGbeu8VbUC1DiAiahHtAYvA0ZEAa4P31F7IaArc8z3C3BRQdWX4mtLQuABG4yzp76ZrS02Ui1Q==, tarball: https://pkgs.d.xiaomi.net:443/artifactory/api/npm/mi-npm/@types/bn.js/-/bn.js-5.2.0.tgz}
+    resolution: {integrity: sha512-DLbJ1BPqxvQhIGbeu8VbUC1DiAiahHtAYvA0ZEAa4P31F7IaArc8z3C3BRQdWX4mtLQuABG4yzp76ZrS02Ui1Q==}
 
   '@types/connect@3.4.38':
     resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
@@ -1417,7 +1420,7 @@ packages:
     engines: {node: '>= 0.4'}
 
   dayjs@1.11.19:
-    resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==}
+    resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==, tarball: https://pkgs.d.xiaomi.net:443/artifactory/api/npm/mi-npm/dayjs/-/dayjs-1.11.19.tgz}
 
   debug@3.2.7:
     resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}

+ 10 - 5
src/app/api/lp-copy/route.ts

@@ -8,7 +8,7 @@ import { chain } from '@/lib/config'
 export async function POST(request: NextRequest) {
 	try {
 		const body = await request.json()
-		const { positionAddress, maxUsdValue } = body
+		const { positionAddress, maxUsdValue, nftMintAddress } = body
 
 		if (!positionAddress) {
 			return NextResponse.json(
@@ -23,12 +23,17 @@ export async function POST(request: NextRequest) {
 				{ status: 400 }
 			)
 		}
+		let nftMint = undefined
 
-		const detailData = await fetch(
-			`https://api2.byreal.io/byreal/api/dex/v2/position/detail?address=${positionAddress}`
-		).then((res) => res.json())
+		if (nftMintAddress) {
+			nftMint = new PublicKey(nftMintAddress)
+		} else {
+			const detailData = await fetch(
+				`https://api2.byreal.io/byreal/api/dex/v2/position/detail?address=${positionAddress}`
+			).then((res) => res.json())
+			nftMint = new PublicKey(detailData.result.data.nftMintAddress)
+		}
 
-		const nftMint = new PublicKey(detailData.result.data.nftMintAddress)
 		// 获取 position 信息
 		const positionInfo = await chain.getPositionInfoByNftMint(nftMint)
 

+ 34 - 0
src/app/api/my-lp/detail/route.ts

@@ -0,0 +1,34 @@
+import { NextRequest, NextResponse } from 'next/server'
+
+export async function GET(request: NextRequest) {
+	try {
+		const searchParams = request.nextUrl.searchParams
+		const address = searchParams.get('address')
+
+		const response = await fetch(
+			`https://api2.byreal.io/byreal/api/dex/v2/position/detail?address=${address}`,
+			{
+				method: 'GET',
+				headers: {
+					accept: 'application/json',
+				},
+			}
+		)
+
+		if (!response.ok) {
+			return NextResponse.json(
+				{ retCode: response.status, retMsg: '外部 API 请求失败' },
+				{ status: response.status }
+			)
+		}
+
+		const data = await response.json()
+		return NextResponse.json(data)
+	} catch (error) {
+		console.error('API Route Error:', error)
+		return NextResponse.json(
+			{ retCode: 500, retMsg: '服务器内部错误' },
+			{ status: 500 }
+		)
+	}
+}

+ 62 - 9
src/app/components/DataTable.tsx

@@ -1,7 +1,7 @@
 'use client'
 
 import { useEffect, useState } from 'react'
-import { Table, message, Select, Typography, Tag, Button } from 'antd'
+import { Table, Select, Typography, Tag, Button, InputNumber, App } from 'antd'
 
 interface TableData {
 	key: string
@@ -61,10 +61,13 @@ interface PoolsListResponse {
 	retMsg?: string
 }
 
-export default function DataTable() {
+function DataTableContent() {
+	const { message } = App.useApp()
 	const [data, setData] = useState<TableData[]>([])
 	const [loading, setLoading] = useState(true)
 	const [poolOptions, setPoolOptions] = useState<PoolOption[]>([])
+	const [quickCopyAmount, setQuickCopyAmount] = useState<number>(0)
+
 	const [selectedPoolAddress, setSelectedPoolAddress] = useState<string>(
 		'FPBW9dtVRoUug2BeUKZAzaknd6iiet9jHM8RcTvwUkyC'
 	)
@@ -191,6 +194,36 @@ export default function DataTable() {
 		return apr * 100
 	}
 
+	function handleQuickCopy(record: TableData) {
+		message.loading('快速复制中...')
+		fetch('/api/lp-copy', {
+			method: 'POST',
+			headers: {
+				'Content-Type': 'application/json',
+			},
+			body: JSON.stringify({
+				positionAddress: record.positionAddress,
+				nftMintAddress: record.nftMintAddress,
+				maxUsdValue: quickCopyAmount,
+			}),
+		})
+			.then((res) => res.json())
+			.then((data) => {
+				if (data.success) {
+					message.success('快速复制成功')
+				} else {
+					message.error(data.error || '快速复制失败')
+				}
+			})
+			.finally(() => {
+				message.destroy()
+			})
+			.catch((err) => {
+				console.error('Error quick copying:', err)
+				message.error('快速复制失败')
+			})
+	}
+
 	useEffect(() => {
 		init()
 		// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -319,12 +352,20 @@ export default function DataTable() {
 			key: 'walletAddress',
 			render: (text: string, record: TableData) => {
 				return (
-					<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>
+					<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>
 				)
 			},
 		},
@@ -593,9 +634,17 @@ export default function DataTable() {
 						(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
 					}
 				/>
-				<Button type="primary" className="ml-2" onClick={handleRefresh}>
+				<Button type="primary" className="ml-2 mr-2" onClick={handleRefresh}>
 					刷新
 				</Button>
+				<span className="mr-2">快速复制金额($):</span>
+				<InputNumber
+					placeholder="快速复制金额($)"
+					value={quickCopyAmount}
+					min={0.01}
+					step={0.1}
+					onChange={(value) => setQuickCopyAmount(value as number)}
+				/>
 			</div>
 			<Table
 				columns={columns}
@@ -638,3 +687,7 @@ export default function DataTable() {
 		</div>
 	)
 }
+
+export default function DataTable() {
+	return <DataTableContent />
+}

+ 2 - 2
src/app/components/ThemeProvider.tsx

@@ -1,6 +1,6 @@
 'use client'
 
-import { ConfigProvider, theme } from 'antd'
+import { ConfigProvider, theme, App } from 'antd'
 import { useEffect, useState, createContext, useContext } from 'react'
 
 type ThemeMode = 'light' | 'dark' | 'system'
@@ -92,7 +92,7 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
 					algorithm: isDark ? theme.darkAlgorithm : theme.defaultAlgorithm,
 				}}
 			>
-				{children}
+				<App>{children}</App>
 			</ConfigProvider>
 		</ThemeContext.Provider>
 	)

+ 8 - 3
src/app/lp-copy/page.tsx

@@ -7,16 +7,17 @@ import {
 	Button,
 	Form,
 	InputNumber,
-	message,
 	Descriptions,
 	Typography,
 	Space,
+	App,
 } from 'antd'
 import { CopyOutlined, LoadingOutlined } from '@ant-design/icons'
 
 const { Title, Text } = Typography
 
-export default function LpCopyPage() {
+function LpCopyPageContent() {
+	const { message } = App.useApp()
 	const [loading, setLoading] = useState(false)
 	const [positionInfo, setPositionInfo] = useState<{
 		poolAddress: string
@@ -114,7 +115,7 @@ export default function LpCopyPage() {
 							},
 						]}
 					>
-						<InputNumber
+						<InputNumber<number>
 							placeholder="e.g., 5, 10, 50"
 							min={0.01}
 							step={1}
@@ -198,3 +199,7 @@ export default function LpCopyPage() {
 		</div>
 	)
 }
+
+export default function LpCopyPage() {
+	return <LpCopyPageContent />
+}

+ 173 - 29
src/app/my-lp/page.tsx

@@ -1,8 +1,9 @@
 'use client'
 
 import { useEffect, useState } from 'react'
-import { Button, Modal, Input, Table, message, Spin, Image } from 'antd'
+import { Button, Modal, Input, Table, Spin, Image, App, Typography } from 'antd'
 import type { ColumnsType } from 'antd/es/table'
+import dayjs from 'dayjs'
 
 interface MintInfo {
 	symbol: string
@@ -17,15 +18,25 @@ interface LPInfo {
 }
 
 interface RecordInfo {
+	walletAddress: string
 	poolAddress: string
 	nftMintAddress: string
+	positionAddress: string
+	pnlUsd: string
+	pnlUsdPercent: string
 	PriceRange: string
-	Token: string
-	TokenA: number
-	TokenB: number
+	totalDeposit: string
+	earnedUsd: string
+	earnedUsdPercent: string
+	openTime: number
+	bonusInfo?: {
+		fromCreatorPositionStatus: number
+		fromCreatorPosition: string
+	}
 }
 
-export default function MyLPPage() {
+function MyLPPageContent() {
+	const { message } = App.useApp()
 	const [userAddress, setUserAddress] = useState<string>('')
 	const [isModalOpen, setIsModalOpen] = useState(false)
 	const [inputValue, setInputValue] = useState<string>('')
@@ -33,23 +44,37 @@ export default function MyLPPage() {
 	const [loading, setLoading] = useState(false)
 	const [total, setTotal] = useState(0)
 	const [page, setPage] = useState(1)
-	const [pageSize, setPageSize] = useState(10)
+	const [pageSize, setPageSize] = useState(50)
 	const [poolMap, setPoolMap] = useState<Record<string, LPInfo>>({})
 
-	useEffect(() => {
-		const userAddress = localStorage.getItem('userAddress')
-		if (userAddress) {
-			setUserAddress(userAddress)
+	const fetchLPDetail = async (positions: RecordInfo[]) => {
+		// const newLpList = [...lpList]
+		message.loading(`正在查询当前页面仓位详细信息,请稍等...`)
+		for (let index = 0; index < positions.length; index++) {
+			const position = positions[index]
+			const response = await fetch(
+				`/api/my-lp/detail?address=${position.positionAddress}`
+			)
+			const data = await response.json()
+			console.log(data, 'data')
+			const newLpList = positions.map((item) => {
+				if (item.positionAddress === position.positionAddress) {
+					return Object.assign(item, data.result.data)
+				}
+				return item
+			})
+			setLpList(newLpList)
 		}
-	}, [])
+		message.destroy()
+	}
 
-	const fetchLPList = async () => {
-		if (!userAddress) return
+	const fetchLPList = async (adddr: string) => {
+		if (!adddr) return
 
 		setLoading(true)
 		try {
 			const response = await fetch(
-				`/api/my-lp?userAddress=${encodeURIComponent(userAddress)}&page=${page}&pageSize=${pageSize}`
+				`/api/my-lp?userAddress=${encodeURIComponent(adddr)}&page=${page}&pageSize=${pageSize}`
 			)
 			const data = await response.json()
 			console.log(data, 'data')
@@ -59,6 +84,7 @@ export default function MyLPPage() {
 				setTotal(total)
 				setPoolMap(poolMap)
 				message.success(`查询成功,找到 ${data.result.data.total} 个 LP token`)
+				fetchLPDetail(positions)
 			} else {
 				message.error(data.retMsg || '查询失败')
 				setLpList([])
@@ -100,17 +126,25 @@ export default function MyLPPage() {
 				lpToken: '',
 				logoURI: [],
 				price: [],
+				tokenAAddress: '',
+				tokenBAddress: '',
 			}
 		}
 		const tokenA = poolInfo.mintA.symbol
 		const tokenB = poolInfo.mintB.symbol
 		return {
+			tokenAAddress: poolInfo.mintA.address,
+			tokenBAddress: poolInfo.mintB.address,
 			lpToken: `${tokenA}/${tokenB}`,
 			logoURI: [poolInfo.mintA.logoURI, poolInfo.mintB.logoURI],
 			price: [poolInfo.mintA.price, poolInfo.mintB.price],
 		}
 	}
 
+	const handleClosePosition = (record: RecordInfo) => {
+		console.log(record, 'record')
+	}
+
 	const columns: ColumnsType<RecordInfo> = [
 		{
 			title: 'LP Token',
@@ -140,6 +174,16 @@ export default function MyLPPage() {
 				</div>
 			),
 		},
+		{
+			title: '当前价格',
+			dataIndex: 'price',
+			key: 'price',
+			render: (text: string, record: RecordInfo) => (
+				<span className="font-mono text-sm">
+					${Number(getPoolInfo(record.poolAddress).price[0]).toFixed(8)}
+				</span>
+			),
+		},
 		{
 			title: 'NFT Token Address',
 			dataIndex: 'nftMintAddress',
@@ -151,27 +195,123 @@ export default function MyLPPage() {
 			),
 		},
 		{
-			title: '价格范围',
-			dataIndex: 'PriceRange',
-			key: 'PriceRange',
+			title: 'Total Deposit',
+			dataIndex: 'totalDeposit',
+			key: 'totalDeposit',
+			render: (text: string) => (
+				<span className="font-mono text-sm">${Number(text).toFixed(2)}</span>
+			),
+		},
+		{
+			title: 'PNL',
+			dataIndex: 'pnlUsd',
+			key: 'pnlUsd',
+			render: (text: string) => (
+				<span className="font-mono text-sm">${Number(text).toFixed(2)}</span>
+			),
+		},
+		{
+			title: 'PNL Percent',
+			dataIndex: 'pnlUsd',
+			key: 'pnlUsdPercent',
+			render: (text: string) => (
+				<span className="font-mono text-sm">{Number(text).toFixed(2)}%</span>
+			),
+		},
+		{
+			title: 'Earned',
+			dataIndex: 'earnedUsd',
+			key: 'pnlUsd',
+			render: (text: string) => (
+				<span className="font-mono text-sm">${Number(text).toFixed(2)}</span>
+			),
+		},
+		{
+			title: 'Earned Percent',
+			dataIndex: 'earnedUsdPercent',
+			key: 'earnedUsdPercent',
+			render: (text: string) => (
+				<span className="font-mono text-sm">{Number(text).toFixed(2)}%</span>
+			),
 		},
 		{
-			title: 'Token',
-			dataIndex: 'Token',
-			key: 'Token',
+			title: 'Earned Percent',
+			dataIndex: 'earnedUsdPercent',
+			key: 'earnedUsdPercent',
+			render: (text: string) => (
+				<span className="font-mono text-sm">{Number(text).toFixed(2)}%</span>
+			),
 		},
 		{
-			title: 'TokenA',
-			dataIndex: 'TokenA',
-			key: 'TokenA',
+			title: 'Bonus',
+			dataIndex: 'bonusUsd',
+			key: 'bonusUsd',
+			render: (text: string) => (
+				<span className="font-mono text-sm">${Number(text).toFixed(2)}</span>
+			),
+		},
+		{
+			title: '开仓时间',
+			dataIndex: 'openTime',
+			key: 'openTime',
+			render: (text: string) => (
+				<span className="font-mono text-sm">
+					{dayjs(text).format('YYYY-MM-DD HH:mm:ss')}
+				</span>
+			),
+		},
+		{
+			title: '仓位来源',
+			dataIndex: 'bonusInfo',
+			key: 'bonusInfo',
+			render: (text: string, record: RecordInfo) => (
+				<span className="font-mono text-sm">
+					{record?.bonusInfo?.fromCreatorPosition ? (
+						<span className="text-blue-500 mr-2">复制</span>
+					) : (
+						<span className="text-green-500 mr-2">新开</span>
+					)}
+					{record?.bonusInfo?.fromCreatorPosition ? (
+						record?.bonusInfo?.fromCreatorPositionStatus === 0 ? (
+							<span className="text-green-500">上级未关仓</span>
+						) : (
+							<span className="text-red-500">上级已关仓</span>
+						)
+					) : (
+						''
+					)}
+				</span>
+			),
 		},
 		{
-			title: 'TokenB',
-			dataIndex: 'TokenB',
-			key: 'TokenB',
+			title: '操作',
+			dataIndex: 'bonusInfo',
+			key: 'bonusInfo',
+			render: (text: string, record: RecordInfo) => (
+				<div className="flex items-center gap-2">
+					<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>
+				</div>
+			),
 		},
 	]
 
+	useEffect(() => {
+		const userAddress = localStorage.getItem('userAddress')
+		if (userAddress) {
+			setUserAddress(userAddress)
+			fetchLPList(userAddress)
+		}
+		// eslint-disable-next-line react-hooks/exhaustive-deps
+	}, [])
+
 	return (
 		<main style={{ padding: '24px' }}>
 			<div className="mb-4">
@@ -181,7 +321,7 @@ export default function MyLPPage() {
 						<Button type="primary" onClick={handleAddressChange}>
 							更换地址
 						</Button>
-						<Button onClick={fetchLPList} loading={loading}>
+						<Button onClick={() => fetchLPList(userAddress)} loading={loading}>
 							刷新
 						</Button>
 					</div>
@@ -206,8 +346,8 @@ export default function MyLPPage() {
 						}}
 						onChange={(pagination) => {
 							setPage(pagination.current || 1)
-							setPageSize(pagination.pageSize || 10)
-							fetchLPList()
+							setPageSize(pagination.pageSize || 50)
+							fetchLPList(userAddress)
 						}}
 					/>
 				</Spin>
@@ -231,3 +371,7 @@ export default function MyLPPage() {
 		</main>
 	)
 }
+
+export default function MyLPPage() {
+	return <MyLPPageContent />
+}