'use client' 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, Tooltip, } from 'antd' import { ExportOutlined, CopyOutlined, ThunderboltOutlined, } from '@ant-design/icons' // 黑名单表格不显示(小额/BOT地址) const BLACK_LIST_ADDRESSES = ['LoVe', 'HZEQ', 'mVBk', 'enVr', 'MV9K'] interface TableData { key: string walletAddress: string liquidity: string earnedUsd: string positionAgeMs: number tokenAaddress?: string tokenBaddress?: string priceRange?: string isInrange?: boolean [key: string]: unknown } interface ApiResponse { retCode: number result: { data: { records: TableData[] total: number current: number pageSize: number pages: number poolMap?: Record } } retMsg?: string } interface PoolOption { label: string value: string } interface PoolInfo { poolAddress: string displayReversed?: boolean feeUsd1d?: number feeUsd1h?: number baseMint?: { price?: number mintInfo: { address?: string symbol?: string decimals?: number } } mintA?: { address: string symbol?: string decimals?: number price?: number mintInfo?: { address?: string symbol?: string decimals?: number } } mintB?: { address: string symbol?: string decimals?: number price?: number mintInfo?: { address?: string symbol?: string decimals?: number } } } interface PoolsListResponse { retCode: number result?: { data?: { records?: PoolInfo[] } } retMsg?: string } function DataTableContent() { const { message } = App.useApp() const [data, setData] = useState([]) const [loading, setLoading] = useState(true) const [poolOptions, setPoolOptions] = useState([]) const [quickCopyAmount, setQuickCopyAmount] = useState(1) const [balance, setBalance] = useState(0) const [tokenName, setTokenName] = useState('') const [userAddress, setUserAddress] = useState('') const userAddressRef = useRef('') const poolsListRef = useRef({ retCode: 0, result: { data: { records: [], }, }, }) const [selectedPoolAddress, setSelectedPoolAddress] = useState( 'FPBW9dtVRoUug2BeUKZAzaknd6iiet9jHM8RcTvwUkyC' ) const [pagination, setPagination] = useState({ current: 1, pageSize: 100, total: 0, }) const [sortField] = useState('liquidity') const [expandedRowKeys, setExpandedRowKeys] = useState([]) const [childTableData, setChildTableData] = useState< Record >({}) const [childTableLoading, setChildTableLoading] = useState< Record >({}) const [selectedRowKeys, setSelectedRowKeys] = useState([]) const [batchCopying, setBatchCopying] = useState(false) // const [priceLower, setPriceLower] = useState(0) // const [priceUpper, setPriceUpper] = useState(0) const [balanceUsd, setBalanceUsd] = useState(0) const [currentPrice, setCurrentPrice] = useState(0) const [feeUsd1d, setFeeUsd1d] = useState(0) const [feeUsd1h, setFeeUsd1h] = useState(0) const fetchBalance = async ( tokenAddress: string, userAddress: string, price: number ) => { const response = await fetch( `/api/my-lp/getBalanceByToken?tokenAddress=${tokenAddress}&accountAddress=${userAddress}` ) const result = await response.json() setBalance(result.result.data.balance) setBalanceUsd( Number((Number(result.result.data.balance) * price).toFixed(2)) ) return true } function fetchAddress() { fetch('/api/my-lp/getAddress') .then((res) => res.json()) .then((data) => { if (data.address) { userAddressRef.current = data.address setUserAddress(data.address) } }) .catch((err) => { console.error('Error fetching address:', err) message.error('获取地址失败') }) } const getTokenBalance = async ( result: PoolsListResponse, selectedPoolAddress: string ) => { const poolInfo = result?.result?.data?.records?.find( (option) => option.poolAddress === selectedPoolAddress ) const price = poolInfo?.baseMint?.price || 0 const label = poolInfo?.baseMint?.mintInfo?.symbol || '' const address = poolInfo?.baseMint?.mintInfo?.address || '' setCurrentPrice(price) setTokenName(label) setFeeUsd1d(poolInfo?.feeUsd1d || 0) setFeeUsd1h(poolInfo?.feeUsd1h || 0) return fetchBalance(address, userAddressRef.current, price) } // 获取 pools 列表 const fetchPoolsList = async () => { try { const response = await fetch('/api/pools/list?page=1&pageSize=500') if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`) } const result: PoolsListResponse = await response.json() if (result.result?.data?.records) { const options: PoolOption[] = result.result.data.records.map((pool) => { const symbolA = pool.mintA?.mintInfo?.symbol || '' const symbolB = pool.mintB?.mintInfo?.symbol || '' // 如果 displayReversed 为 true,调换 AB 位置 const label = pool.displayReversed ? `${symbolB}/${symbolA}` : `${symbolA}/${symbolB}` return { label, value: pool.poolAddress, } }) poolsListRef.current = result setPoolOptions(options) await getTokenBalance(result, selectedPoolAddress) } } catch (error) { console.error('Error fetching pools list:', error) message.error('获取 pools 列表失败') } } const fetchData = async ( page: number = 1, pageSize: number = 100, poolAddress?: string, currentSortField?: string ) => { setLoading(true) try { const response = await fetch('/api/top-positions', { method: 'POST', headers: { 'content-type': 'application/json', }, body: JSON.stringify({ poolAddress: poolAddress || selectedPoolAddress, page, pageSize, sortField: currentSortField || sortField, status: 0, }), }) if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`) } const result: ApiResponse = await response.json() if (result.result) { const { records, total, current, pageSize, poolMap } = result.result.data const poolMapData = poolMap ? (Object.values(poolMap)[0] as PoolInfo) : undefined const tokenAaddress = poolMapData?.mintA?.address const tokenBaddress = poolMapData?.mintB?.address const getPriceRange = ( item: Record, poolMapData: PoolInfo | undefined, displayReversed?: boolean ) => { const priceUpper = TickMath.getPriceFromTick({ tick: item.upperTick as number, decimalsA: poolMapData?.mintA?.decimals || 0, decimalsB: poolMapData?.mintB?.decimals || 0, baseIn: !displayReversed, }) const priceLower = TickMath.getPriceFromTick({ tick: item.lowerTick as number, decimalsA: poolMapData?.mintA?.decimals || 0, decimalsB: poolMapData?.mintB?.decimals || 0, baseIn: !displayReversed, }) // 如果 displayReversed 为 true,调换 priceLower 和 priceUpper 位置 if (displayReversed) { return `${priceUpper.toFixed(6)} - ${priceLower.toFixed(6)}` } return `${priceLower.toFixed(6)} - ${priceUpper.toFixed(6)}` } const filteredRecords = records.filter((item) => { return !BLACK_LIST_ADDRESSES.some((address) => item.walletAddress.toLowerCase().includes(address.toLowerCase()) ) }) as TableData[] setData( filteredRecords.map((item, index) => ({ ...item, key: `${item.id || index}`, priceRange: getPriceRange( item, poolMapData, poolMapData?.displayReversed ), tokenAaddress, tokenBaddress, })) as TableData[] ) setExpandedRowKeys([]) setPagination({ current: current, pageSize: pageSize, total: total, }) } else { message.error(result.retMsg || '获取数据失败') } } catch (error) { console.error('Error fetching data:', error) message.error('网络请求失败,请稍后重试') } finally { setLoading(false) } } const init = async () => { await fetchAddress() await fetchPoolsList() await fetchData(1, 100) } const calculateAPR = (record: TableData) => { const useEarnSecond = Number(record.earnedUsd) / (Number(record.positionAgeMs) / 1000) const apr = (useEarnSecond * 60 * 60 * 24 * 365) / Number(record.liquidityUsd) return (apr * 100).toFixed(2) + '%' } const calculateAPRValue = (record: TableData) => { const useEarnSecond = Number(record.earnedUsd) / (Number(record.positionAgeMs) / 1000) const apr = (useEarnSecond * 60 * 60 * 24 * 365) / Number(record.liquidityUsd) return apr * 100 } function handleQuickCopy(record: TableData) { message.loading({ key: 'quickCopy', content: '复制中...', duration: 0, }) fetch('/api/lp-copy', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ positionAddress: record.positionAddress, nftMintAddress: record.nftMintAddress, maxUsdValue: quickCopyAmount, needSwap: true, }), }) .then((res) => res.json()) .then((data) => { message.destroy('quickCopy') if (data.success) { message.success('快速复制成功') } else { message.error(data.error || '快速复制失败') } }) .catch((err) => { console.error('Error quick copying:', err) message.error('快速复制失败') }) } // 批量快速复制 const handleBatchQuickCopy = async () => { if (selectedRowKeys.length === 0) { message.warning('请先选择要复制的行') return } const selectedRecords = data.filter((record) => selectedRowKeys.includes(record.key) ) if (selectedRecords.length === 0) { message.warning('未找到选中的记录') return } setBatchCopying(true) let successCount = 0 let failCount = 0 message.loading({ key: 'batchCopy', content: `批量复制中... (0/${selectedRecords.length})`, duration: 0, }) // 依次处理每条记录 for (let i = 0; i < selectedRecords.length; i++) { const record = selectedRecords[i] try { const response = await fetch('/api/lp-copy', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ positionAddress: record.positionAddress, nftMintAddress: record.nftMintAddress, maxUsdValue: quickCopyAmount, needSwap: true, }), }) const result = await response.json() if (result.success) { successCount++ } else { failCount++ console.error(`复制失败: ${record.positionAddress}`, result.error) } // 更新进度 message.loading({ key: 'batchCopy', content: `批量复制中... (${i + 1}/${selectedRecords.length})`, duration: 0, }) // 添加延迟,避免请求过快 if (i < selectedRecords.length - 1) { await new Promise((resolve) => setTimeout(resolve, 500)) } } catch (error) { failCount++ console.error(`复制失败: ${record.positionAddress}`, error) } } message.destroy('batchCopy') setBatchCopying(false) setSelectedRowKeys([]) if (failCount === 0) { message.success(`批量复制完成!成功 ${successCount} 条`) } else { message.warning( `批量复制完成!成功 ${successCount} 条,失败 ${failCount} 条` ) } } useEffect(() => { init() // eslint-disable-next-line react-hooks/exhaustive-deps }, []) const renderPositionAgeMs = (text: string) => { const rawAgeMs = Number(text) const ageMs = Number.isFinite(rawAgeMs) ? Math.max(0, rawAgeMs) : 0 const days = Math.floor(ageMs / 86400000) const hours = Math.floor((ageMs % 86400000) / 3600000) const minutes = Math.floor((ageMs % 3600000) / 60000) return ( 0 ? '#2a9d61' : '#FF0000', }} > {days > 0 ? `${days}天/` : ''} {hours > 0 || days > 0 ? `${hours}小时/` : ''} {minutes}分钟 ) } // 当地址获取后,设置页面标题为地址后四位字母 useEffect(() => { if (userAddress) { const lastFour = userAddress.slice(-4) document.title = `${lastFour} - All Pools` } }, [userAddress]) const columns = [ { title: '创建地址', dataIndex: 'walletAddress', key: 'walletAddress', render: (text: string) => { return ( {text.slice(0, 6)}...{text.slice(-4)} ) }, }, { title: 'Liquidity', dataIndex: 'liquidityUsd', key: 'liquidityUsd', sorter: (a: TableData, b: TableData) => { return Number(a.liquidityUsd) - Number(b.liquidityUsd) }, render: (text: string) => { return ( ${Number(text).toFixed(2)} ) }, }, { title: '区间', dataIndex: 'priceRange', key: 'priceRange', render: (text: string) => { if ( currentPrice > Number(text.split('-')[0]) && currentPrice < Number(text.split('-')[1]) ) { return ( {text} ) } else { return {text} } }, }, { title: '复制数', dataIndex: 'copies', key: 'copies', render: (text: string) => { return ( {text} ) }, }, { title: '奖励', dataIndex: 'bonusUsd', key: 'bonusUsd', render: (text: string) => { return ( ${Number(text).toFixed(2)} ) }, }, { title: 'APR', dataIndex: 'apr', key: 'apr', sorter: (a: TableData, b: TableData) => { return calculateAPRValue(a) - calculateAPRValue(b) }, render: (_text: string, record: TableData) => { return ( {calculateAPR(record)} ) }, }, { title: 'FEE', dataIndex: 'earnedUsd', key: 'earnedUsd', render: (text: string) => { return ( 0 ? '#00B098' : '#FF0000' }} > ${Number(text).toFixed(2)} ) }, }, { title: 'PNL', dataIndex: 'pnlUsd', key: 'pnlUsd', render: (text: string) => { return ( 0 ? '#00B098' : '#FF0000' }} > ${Number(text).toFixed(2)} ) }, }, { title: '创建时间', dataIndex: 'positionAgeMs', key: 'positionAgeMs', sorter: (a: TableData, b: TableData) => { return Number(a.positionAgeMs) - Number(b.positionAgeMs) }, render: renderPositionAgeMs, }, { title: '操作', dataIndex: 'walletAddress', key: 'walletAddress', width: 100, render: (text: string, record: TableData) => { return (
handleQuickCopy(record)} style={{ color: '#1890ff' }} >
) }, }, ] const handleTableChange = ( newPagination: { current?: number pageSize?: number }, _filters: unknown, sorter: | { field?: string order?: 'ascend' | 'descend' | null } | unknown ) => { let currentSortField = sortField if (newPagination.current && newPagination.current !== pagination.current) { const targetPage = newPagination.current const targetPageSize = newPagination.pageSize || pagination.pageSize setPagination({ ...pagination, current: targetPage, pageSize: targetPageSize, }) fetchData( targetPage, targetPageSize, selectedPoolAddress, currentSortField ) } else { if (sorter && typeof sorter === 'object' && 'field' in sorter) { const sorterObj = sorter as { field?: string order?: 'ascend' | 'descend' | null } if (sorterObj.field && sorterObj.order) { // 映射字段名到 API 的 sortField const fieldMap: Record = { copies: 'copies', earnedUsd: 'earnedUsd', pnlUsd: 'pnlUsd', liquidityUsd: 'liquidity', positionAgeMs: 'positionAgeMs', } const newSortField = fieldMap[sorterObj.field] || sortField if (newSortField !== sortField) { currentSortField = newSortField } } } } } const handlePoolChange = async (value: string) => { setBalance(0) setBalanceUsd(0) setSelectedPoolAddress(value) await getTokenBalance(poolsListRef.current, value) fetchData(1, 100, value) } // 获取子表格数据 const fetchChildData = async (parentPositionAddress: string) => { if (childTableData[parentPositionAddress]) { // 如果已经加载过,直接返回 return } setChildTableLoading((prev) => ({ ...prev, [parentPositionAddress]: true, })) try { const response = await fetch('/api/top-positions', { method: 'POST', headers: { 'content-type': 'application/json', }, body: JSON.stringify({ poolAddress: selectedPoolAddress, parentPositionAddress, page: 1, pageSize: 500, sortField: 'liquidity', }), }) if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`) } const result: ApiResponse = await response.json() if (result.result) { const { records } = result.result.data const childData: TableData[] = records.map((item, index) => ({ ...item, key: `${parentPositionAddress}-${item.id || index}`, })) as TableData[] setChildTableData((prev) => ({ ...prev, [parentPositionAddress]: childData, })) } } catch (error) { console.error('Error fetching child data:', error) message.error('获取子表格数据失败') } finally { setChildTableLoading((prev) => ({ ...prev, [parentPositionAddress]: false, })) } } // 处理展开/收起 const handleExpand = (expanded: boolean, record: TableData) => { const positionAddress = record.positionAddress as string if (expanded && positionAddress) { setExpandedRowKeys((prev) => [...prev, record.key]) fetchChildData(positionAddress) } else { setExpandedRowKeys((prev) => prev.filter((key) => key !== record.key)) } } const handleRefresh = async () => { setLoading(true) await fetchPoolsList() fetchData(1, 100, selectedPoolAddress, sortField) } // function handleReset() { // setPriceLower(0) // setPriceUpper(0) // setQuickCopyAmount(1) // } // 子表格的列定义 const childColumns = [ { title: '创建地址', dataIndex: 'walletAddress', key: 'walletAddress', render: (text: string) => { return ( {text.slice(0, 6)}...{text.slice(-4)} ) }, }, { title: 'Liquidity', dataIndex: 'liquidityUsd', key: 'liquidityUsd', render: (text: string) => { return ( ${Number(text).toFixed(2)} ) }, }, { title: '复制数', dataIndex: 'copies', key: 'copies', render: (text: string) => { return ( {text} ) }, }, { title: '奖励', dataIndex: 'bonusUsd', key: 'bonusUsd', render: (text: string) => { return ( ${Number(text).toFixed(2)} ) }, }, { title: 'FEE', dataIndex: 'earnedUsd', key: 'earnedUsd', render: (text: string) => { return ( 0 ? '#00B098' : '#FF0000' }} > ${Number(text).toFixed(2)} ) }, }, { title: 'PNL', dataIndex: 'pnlUsd', key: 'pnlUsd', render: (text: string) => { return ( 0 ? '#00B098' : '#FF0000' }} > ${Number(text).toFixed(2)} ) }, }, { title: '创建时间', dataIndex: 'positionAgeMs', key: 'positionAgeMs', render: renderPositionAgeMs, }, { title: '状态', dataIndex: 'status', key: 'status', render: (status: number) => { return status === 0 ? ( Active ) : ( Inactive ) }, }, { title: '操作', dataIndex: 'walletAddress', key: 'walletAddress', render: (text: string, record: TableData) => { return ( 复制 ) }, }, ] return (