|
@@ -14,11 +14,13 @@ import {
|
|
|
InputNumber,
|
|
InputNumber,
|
|
|
Select,
|
|
Select,
|
|
|
Tooltip,
|
|
Tooltip,
|
|
|
|
|
+ Slider,
|
|
|
} from 'antd'
|
|
} from 'antd'
|
|
|
import {
|
|
import {
|
|
|
ExportOutlined,
|
|
ExportOutlined,
|
|
|
CloseCircleOutlined,
|
|
CloseCircleOutlined,
|
|
|
PlusCircleOutlined,
|
|
PlusCircleOutlined,
|
|
|
|
|
+ AimOutlined,
|
|
|
} from '@ant-design/icons'
|
|
} from '@ant-design/icons'
|
|
|
import type { ColumnsType, TablePaginationConfig } from 'antd/es/table'
|
|
import type { ColumnsType, TablePaginationConfig } from 'antd/es/table'
|
|
|
import dayjs from 'dayjs'
|
|
import dayjs from 'dayjs'
|
|
@@ -63,6 +65,8 @@ interface RecordInfo {
|
|
|
isParentPositionClosed?: boolean
|
|
isParentPositionClosed?: boolean
|
|
|
parentLiquidityUsd?: string
|
|
parentLiquidityUsd?: string
|
|
|
bonusUsd?: string
|
|
bonusUsd?: string
|
|
|
|
|
+ positionAgeMs?: number
|
|
|
|
|
+ childPositionList?: RecordInfo[]
|
|
|
bonusInfo?: {
|
|
bonusInfo?: {
|
|
|
fromCreatorWallet: string
|
|
fromCreatorWallet: string
|
|
|
fromCreatorPositionStatus: number
|
|
fromCreatorPositionStatus: number
|
|
@@ -88,6 +92,9 @@ function MyLPPageContent() {
|
|
|
const [tokenAddress, setTokenAddress] = useState<string | undefined>(
|
|
const [tokenAddress, setTokenAddress] = useState<string | undefined>(
|
|
|
undefined
|
|
undefined
|
|
|
)
|
|
)
|
|
|
|
|
+ const [snipeChildState, setSnipeChildState] = useState<
|
|
|
|
|
+ Record<string, { amount: number; multiplier: number }>
|
|
|
|
|
+ >({})
|
|
|
|
|
|
|
|
const fetchLPDetail = async (positions: RecordInfo[]) => {
|
|
const fetchLPDetail = async (positions: RecordInfo[]) => {
|
|
|
message.loading(`正在查询当前页面仓位详细信息,请稍等...`)
|
|
message.loading(`正在查询当前页面仓位详细信息,请稍等...`)
|
|
@@ -110,6 +117,7 @@ function MyLPPageContent() {
|
|
|
let allCopys = 0
|
|
let allCopys = 0
|
|
|
let isParentPositionClosed = false
|
|
let isParentPositionClosed = false
|
|
|
let parentLiquidityUsd = 0
|
|
let parentLiquidityUsd = 0
|
|
|
|
|
+ let childPositionList: RecordInfo[] = []
|
|
|
|
|
|
|
|
if (bonusInfo?.fromCreatorPosition) {
|
|
if (bonusInfo?.fromCreatorPosition) {
|
|
|
const copyInfo = await Promise.all([
|
|
const copyInfo = await Promise.all([
|
|
@@ -124,6 +132,7 @@ function MyLPPageContent() {
|
|
|
copyInfo.map((res) => res.json())
|
|
copyInfo.map((res) => res.json())
|
|
|
)
|
|
)
|
|
|
if (copyInfoData.retCode === 0 && copyInfoData.result) {
|
|
if (copyInfoData.retCode === 0 && copyInfoData.result) {
|
|
|
|
|
+ childPositionList = copyInfoData.result.data.records
|
|
|
allCopys = copyInfoData.result.data.total
|
|
allCopys = copyInfoData.result.data.total
|
|
|
allCopyAmount = copyInfoData.result.data.records
|
|
allCopyAmount = copyInfoData.result.data.records
|
|
|
.filter((item: RecordInfo) => item.status === 0)
|
|
.filter((item: RecordInfo) => item.status === 0)
|
|
@@ -138,6 +147,7 @@ function MyLPPageContent() {
|
|
|
}
|
|
}
|
|
|
const newData = {
|
|
const newData = {
|
|
|
...data.result.data,
|
|
...data.result.data,
|
|
|
|
|
+ childPositionList,
|
|
|
isParentPositionClosed,
|
|
isParentPositionClosed,
|
|
|
parentLiquidityUsd,
|
|
parentLiquidityUsd,
|
|
|
allCopyAmount,
|
|
allCopyAmount,
|
|
@@ -350,6 +360,39 @@ function MyLPPageContent() {
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ function handleSnipeAddPosition(record: RecordInfo, addUsdValue: number) {
|
|
|
|
|
+ if (!addUsdValue || addUsdValue <= 0) {
|
|
|
|
|
+ message.warning('请输入有效加仓金额')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ message.loading({
|
|
|
|
|
+ key: 'snipeAdd',
|
|
|
|
|
+ content: '正在狙击加仓...',
|
|
|
|
|
+ duration: 0,
|
|
|
|
|
+ })
|
|
|
|
|
+ fetch('/api/lp-index/lp-add', {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ body: JSON.stringify({
|
|
|
|
|
+ nftMintAddress: record.nftMintAddress,
|
|
|
|
|
+ addUsdValue,
|
|
|
|
|
+ }),
|
|
|
|
|
+ })
|
|
|
|
|
+ .then((res) => res.json())
|
|
|
|
|
+ .then((data) => {
|
|
|
|
|
+ message.destroy('snipeAdd')
|
|
|
|
|
+ if (data.success) {
|
|
|
|
|
+ message.success('加仓成功')
|
|
|
|
|
+ } else {
|
|
|
|
|
+ message.error(data.error || '加仓失败')
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ .catch((err) => {
|
|
|
|
|
+ console.error('Error snipe adding position:', err)
|
|
|
|
|
+ message.destroy('snipeAdd')
|
|
|
|
|
+ message.error('加仓失败')
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
function getColor(text: string, allCopyAmount: string) {
|
|
function getColor(text: string, allCopyAmount: string) {
|
|
|
const percentage = (Number(text) / Number(allCopyAmount)) * 100
|
|
const percentage = (Number(text) / Number(allCopyAmount)) * 100
|
|
|
if (percentage > 50) return 'green'
|
|
if (percentage > 50) return 'green'
|
|
@@ -357,6 +400,151 @@ function MyLPPageContent() {
|
|
|
else return 'red'
|
|
else return 'red'
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ function formatPositionAgeMs(ms: number | undefined, openTime?: number): string {
|
|
|
|
|
+ if (ms != null && !Number.isNaN(ms)) {
|
|
|
|
|
+ const days = Math.floor(ms / (24 * 60 * 60 * 1000))
|
|
|
|
|
+ const hours = Math.floor((ms % (24 * 60 * 60 * 1000)) / (60 * 60 * 1000))
|
|
|
|
|
+ const mins = Math.floor((ms % (60 * 60 * 1000)) / (60 * 1000))
|
|
|
|
|
+ const parts = []
|
|
|
|
|
+ if (days > 0) parts.push(`${days}天`)
|
|
|
|
|
+ if (hours > 0 || days > 0) parts.push(`${hours}小时`)
|
|
|
|
|
+ parts.push(`${mins}分钟`)
|
|
|
|
|
+ return parts.join('/')
|
|
|
|
|
+ }
|
|
|
|
|
+ if (openTime != null) {
|
|
|
|
|
+ const open = dayjs(openTime)
|
|
|
|
|
+ const days = dayjs().diff(open, 'day')
|
|
|
|
|
+ const hours = dayjs().diff(open, 'hour') % 24
|
|
|
|
|
+ const minutes = dayjs().diff(open, 'minute') % 60
|
|
|
|
|
+ const parts = []
|
|
|
|
|
+ if (days > 0) parts.push(`${days}天/`)
|
|
|
|
|
+ if (hours > 0 || days > 0) parts.push(`${hours}小时/`)
|
|
|
|
|
+ parts.push(`${minutes}分钟`)
|
|
|
|
|
+ return parts.join('')
|
|
|
|
|
+ }
|
|
|
|
|
+ return '-'
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const childColumns: ColumnsType<RecordInfo> = [
|
|
|
|
|
+ {
|
|
|
|
|
+ title: 'Bonus USD',
|
|
|
|
|
+ dataIndex: 'bonusUsd',
|
|
|
|
|
+ key: 'bonusUsd',
|
|
|
|
|
+ render: (text: string) => (
|
|
|
|
|
+ <span className="font-mono text-sm text-orange-500">
|
|
|
|
|
+ ${Number(text ?? 0).toFixed(2)}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ ),
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '持仓时长',
|
|
|
|
|
+ dataIndex: 'positionAgeMs',
|
|
|
|
|
+ key: 'positionAgeMs',
|
|
|
|
|
+ render: (_: unknown, record: RecordInfo) => (
|
|
|
|
|
+ <span className="font-mono text-sm">
|
|
|
|
|
+ {formatPositionAgeMs(record.positionAgeMs, record.openTime)}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ ),
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: 'Liquidity USD',
|
|
|
|
|
+ dataIndex: 'liquidityUsd',
|
|
|
|
|
+ key: 'liquidityUsd',
|
|
|
|
|
+ render: (text: string) => (
|
|
|
|
|
+ <span className="font-mono text-sm text-orange-500">
|
|
|
|
|
+ ${Number(text ?? 0).toFixed(2)}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ ),
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: 'PNL USD',
|
|
|
|
|
+ dataIndex: 'pnlUsd',
|
|
|
|
|
+ key: 'pnlUsd',
|
|
|
|
|
+ render: (text: string) => (
|
|
|
|
|
+ <span
|
|
|
|
|
+ className="font-mono text-sm"
|
|
|
|
|
+ style={{ color: Number(text ?? 0) > 0 ? '#00B098' : '#FF0000' }}
|
|
|
|
|
+ >
|
|
|
|
|
+ ${Number(text ?? 0).toFixed(2)}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ ),
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: 'Wallet Address',
|
|
|
|
|
+ dataIndex: 'walletAddress',
|
|
|
|
|
+ key: 'walletAddress',
|
|
|
|
|
+ render: (text: string, record: RecordInfo) => (
|
|
|
|
|
+ <span className="font-mono text-sm">
|
|
|
|
|
+ <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"
|
|
|
|
|
+ >
|
|
|
|
|
+ {String(text ?? '').slice(0, 6)}...{String(text ?? '').slice(-4)}
|
|
|
|
|
+ </Typography.Link>
|
|
|
|
|
+ </span>
|
|
|
|
|
+ ),
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '狙击',
|
|
|
|
|
+ key: 'snipe',
|
|
|
|
|
+ width: 280,
|
|
|
|
|
+ render: (_: unknown, record: RecordInfo) => {
|
|
|
|
|
+ const key = record.positionAddress
|
|
|
|
|
+ const baseAmount = Number(record.liquidityUsd) || 0
|
|
|
|
|
+ const amount = snipeChildState[key]?.amount ?? baseAmount
|
|
|
|
|
+ const multiplier = snipeChildState[key]?.multiplier ?? 1
|
|
|
|
|
+ const finalAmount = amount * multiplier
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div className="flex items-center gap-2 flex-wrap">
|
|
|
|
|
+ <InputNumber
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ className="font-mono w-24"
|
|
|
|
|
+ min={0}
|
|
|
|
|
+ step={0.01}
|
|
|
|
|
+ value={amount}
|
|
|
|
|
+ onChange={(val) =>
|
|
|
|
|
+ setSnipeChildState((prev) => ({
|
|
|
|
|
+ ...prev,
|
|
|
|
|
+ [key]: {
|
|
|
|
|
+ amount: Number(val) ?? 0,
|
|
|
|
|
+ multiplier: prev[key]?.multiplier ?? 1,
|
|
|
|
|
+ },
|
|
|
|
|
+ }))
|
|
|
|
|
+ }
|
|
|
|
|
+ />
|
|
|
|
|
+ <Slider
|
|
|
|
|
+ className="w-20! m-0! shrink-0"
|
|
|
|
|
+ min={1}
|
|
|
|
|
+ max={5}
|
|
|
|
|
+ step={1}
|
|
|
|
|
+ value={multiplier}
|
|
|
|
|
+ onChange={(val) =>
|
|
|
|
|
+ setSnipeChildState((prev) => ({
|
|
|
|
|
+ ...prev,
|
|
|
|
|
+ [key]: {
|
|
|
|
|
+ amount: prev[key]?.amount ?? baseAmount,
|
|
|
|
|
+ multiplier: val ?? 1,
|
|
|
|
|
+ },
|
|
|
|
|
+ }))
|
|
|
|
|
+ }
|
|
|
|
|
+ />
|
|
|
|
|
+ <span className="font-mono text-xs text-gray-500 w-6">
|
|
|
|
|
+ ×{multiplier}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <Tooltip title="狙击">
|
|
|
|
|
+ <Typography.Link
|
|
|
|
|
+ onClick={() => handleSnipeAddPosition(record, finalAmount)}
|
|
|
|
|
+ className="inline-flex items-center"
|
|
|
|
|
+ >
|
|
|
|
|
+ <AimOutlined style={{ fontSize: '18px', color: '#fa8c16' }} />
|
|
|
|
|
+ </Typography.Link>
|
|
|
|
|
+ </Tooltip>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ ]
|
|
|
|
|
+
|
|
|
const columns: ColumnsType<RecordInfo> = [
|
|
const columns: ColumnsType<RecordInfo> = [
|
|
|
{
|
|
{
|
|
|
title: 'LP Token',
|
|
title: 'LP Token',
|
|
@@ -588,7 +776,7 @@ function MyLPPageContent() {
|
|
|
{
|
|
{
|
|
|
title: '仓位来源',
|
|
title: '仓位来源',
|
|
|
dataIndex: 'bonusInfo',
|
|
dataIndex: 'bonusInfo',
|
|
|
- width: 120,
|
|
|
|
|
|
|
+ width: 140,
|
|
|
fixed: 'right',
|
|
fixed: 'right',
|
|
|
key: 'bonusInfoSource',
|
|
key: 'bonusInfoSource',
|
|
|
render: (text: string, record: RecordInfo) => (
|
|
render: (text: string, record: RecordInfo) => (
|
|
@@ -892,6 +1080,26 @@ function MyLPPageContent() {
|
|
|
rowClassName={(record: RecordInfo) => {
|
|
rowClassName={(record: RecordInfo) => {
|
|
|
return record?.hasDetail ? '' : 'bg-green-100'
|
|
return record?.hasDetail ? '' : 'bg-green-100'
|
|
|
}}
|
|
}}
|
|
|
|
|
+ expandable={{
|
|
|
|
|
+ expandedRowRender: (record: RecordInfo) => {
|
|
|
|
|
+ const list = record.childPositionList ?? []
|
|
|
|
|
+ if (list.length === 0) return null
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div className="bg-gray-50/80 py-2 pr-4">
|
|
|
|
|
+ <Table
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ columns={childColumns}
|
|
|
|
|
+ dataSource={list}
|
|
|
|
|
+ rowKey="positionAddress"
|
|
|
|
|
+ pagination={false}
|
|
|
|
|
+ scroll={{ x: 'max-content' }}
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )
|
|
|
|
|
+ },
|
|
|
|
|
+ rowExpandable: (record: RecordInfo) =>
|
|
|
|
|
+ (record.childPositionList?.length ?? 0) > 0,
|
|
|
|
|
+ }}
|
|
|
rowSelection={{
|
|
rowSelection={{
|
|
|
selectedRowKeys,
|
|
selectedRowKeys,
|
|
|
onChange: (newSelectedRowKeys) => {
|
|
onChange: (newSelectedRowKeys) => {
|