|
@@ -0,0 +1,198 @@
|
|
|
|
|
+// watch.mjs - 监控 top-positions 并发送到 Discord
|
|
|
|
|
+
|
|
|
|
|
+const API_URL = 'https://app-byreal-table.trrlzk.easypanel.host/api/top-positions'
|
|
|
|
|
+const DISCORD_WEBHOOK_URL = 'https://discord.com/api/webhooks/1449354624225120378/7gOkPXzZkaoQF7XWKFZRsevVSKXcUOuim2Rhtx_gWzBd9b7wphFMQiAIqBnRT3Gmkog4'
|
|
|
|
|
+const DEFAULT_POOL_ADDRESS = 'FPBW9dtVRoUug2BeUKZAzaknd6iiet9jHM8RcTvwUkyC'
|
|
|
|
|
+const INTERVAL_MS = 5 * 60 * 1000 // 5分钟
|
|
|
|
|
+const MAX_AGE_MS = 10 * 60 * 1000 // 10分钟
|
|
|
|
|
+
|
|
|
|
|
+// 存储已发送的 positionAddress
|
|
|
|
|
+const sentPositions = new Set()
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 获取 top-positions 数据
|
|
|
|
|
+ */
|
|
|
|
|
+async function fetchTopPositions() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const response = await fetch(API_URL, {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ 'content-type': 'application/json',
|
|
|
|
|
+ },
|
|
|
|
|
+ body: JSON.stringify({
|
|
|
|
|
+ poolAddress: DEFAULT_POOL_ADDRESS,
|
|
|
|
|
+ page: 1,
|
|
|
|
|
+ pageSize: 200,
|
|
|
|
|
+ sortField: 'liquidity',
|
|
|
|
|
+ status: 0,
|
|
|
|
|
+ }),
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ if (!response.ok) {
|
|
|
|
|
+ throw new Error(`HTTP error! status: ${response.status}`)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const result = await response.json()
|
|
|
|
|
+
|
|
|
|
|
+ if (result.retCode !== 0 || !result.result?.data?.records) {
|
|
|
|
|
+ console.error('API 返回错误:', result.retMsg || '未知错误')
|
|
|
|
|
+ return []
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return result.result.data.records
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('获取数据失败:', error)
|
|
|
|
|
+ return []
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 发送消息到 Discord
|
|
|
|
|
+ */
|
|
|
|
|
+async function sendToDiscord(position) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const ageMinutes = Math.floor(position.positionAgeMs / 1000 / 60)
|
|
|
|
|
+ const ageSeconds = Math.floor((position.positionAgeMs / 1000) % 60)
|
|
|
|
|
+
|
|
|
|
|
+ const embed = {
|
|
|
|
|
+ title: '🆕 新仓位发现',
|
|
|
|
|
+ color: 0x00b098, // 绿色
|
|
|
|
|
+ fields: [
|
|
|
|
|
+ {
|
|
|
|
|
+ name: '创建地址',
|
|
|
|
|
+ value: `\`${position.walletAddress}\``,
|
|
|
|
|
+ inline: true,
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ name: '仓位地址',
|
|
|
|
|
+ value: `\`${position.positionAddress}\``,
|
|
|
|
|
+ inline: false,
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ name: '流动性',
|
|
|
|
|
+ value: `$${Number(position.liquidityUsd).toFixed(2)}`,
|
|
|
|
|
+ inline: true,
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ name: '复制数',
|
|
|
|
|
+ value: `${position.copies}`,
|
|
|
|
|
+ inline: true,
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ name: '奖励',
|
|
|
|
|
+ value: `$${Number(position.bonusUsd).toFixed(2)}`,
|
|
|
|
|
+ inline: true,
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ name: 'FEE',
|
|
|
|
|
+ value: `$${Number(position.earnedUsd).toFixed(2)}`,
|
|
|
|
|
+ inline: true,
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ name: 'PNL',
|
|
|
|
|
+ value: `$${Number(position.pnlUsd).toFixed(2)}`,
|
|
|
|
|
+ inline: true,
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ name: '创建时间',
|
|
|
|
|
+ value: `${ageMinutes}分${ageSeconds}秒`,
|
|
|
|
|
+ inline: true,
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ name: 'APR',
|
|
|
|
|
+ value: calculateAPR(position),
|
|
|
|
|
+ inline: true,
|
|
|
|
|
+ },
|
|
|
|
|
+ ],
|
|
|
|
|
+ timestamp: new Date().toISOString(),
|
|
|
|
|
+ url: `https://www.byreal.io/en/portfolio?userAddress=${position.walletAddress}&tab=current&positionAddress=${position.positionAddress}`,
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const response = await fetch(DISCORD_WEBHOOK_URL, {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ 'content-type': 'application/json',
|
|
|
|
|
+ },
|
|
|
|
|
+ body: JSON.stringify({
|
|
|
|
|
+ embeds: [embed],
|
|
|
|
|
+ }),
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ if (!response.ok) {
|
|
|
|
|
+ throw new Error(`Discord webhook 失败: ${response.status}`)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ console.log(`✅ 已发送到 Discord: ${position.positionAddress}`)
|
|
|
|
|
+ return true
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('发送到 Discord 失败:', error)
|
|
|
|
|
+ return false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 计算 APR
|
|
|
|
|
+ */
|
|
|
|
|
+function calculateAPR(position) {
|
|
|
|
|
+ const earnedPerSecond = Number(position.earnedUsd) / (Number(position.positionAgeMs) / 1000)
|
|
|
|
|
+ const apr = (earnedPerSecond * 60 * 60 * 24 * 365) / Number(position.liquidityUsd)
|
|
|
|
|
+ return `${(apr * 100).toFixed(2)}%`
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 处理新仓位
|
|
|
|
|
+ */
|
|
|
|
|
+async function processPositions() {
|
|
|
|
|
+ console.log(`\n[${new Date().toLocaleString('zh-CN')}] 开始检查新仓位...`)
|
|
|
|
|
+
|
|
|
|
|
+ const records = await fetchTopPositions()
|
|
|
|
|
+
|
|
|
|
|
+ if (records.length === 0) {
|
|
|
|
|
+ console.log('未获取到数据')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 过滤出10分钟以内的仓位
|
|
|
|
|
+ const newPositions = records.filter((record) => {
|
|
|
|
|
+ const ageMs = Number(record.positionAgeMs)
|
|
|
|
|
+ return ageMs < MAX_AGE_MS && ageMs >= 0
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ console.log(`找到 ${newPositions.length} 个10分钟内的仓位`)
|
|
|
|
|
+
|
|
|
|
|
+ // 过滤出未发送过的仓位
|
|
|
|
|
+ const unsentPositions = newPositions.filter((position) => {
|
|
|
|
|
+ const address = position.positionAddress
|
|
|
|
|
+ return address && !sentPositions.has(address)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ console.log(`其中 ${unsentPositions.length} 个未发送过`)
|
|
|
|
|
+
|
|
|
|
|
+ // 发送到 Discord
|
|
|
|
|
+ for (const position of unsentPositions) {
|
|
|
|
|
+ const address = position.positionAddress
|
|
|
|
|
+ if (address) {
|
|
|
|
|
+ const success = await sendToDiscord(position)
|
|
|
|
|
+ if (success) {
|
|
|
|
|
+ sentPositions.add(address)
|
|
|
|
|
+ }
|
|
|
|
|
+ // 避免发送过快,稍微延迟
|
|
|
|
|
+ await new Promise((resolve) => setTimeout(resolve, 500))
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ console.log(`处理完成,已发送 ${unsentPositions.length} 个新仓位`)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 启动定时任务
|
|
|
|
|
+console.log('🚀 监控程序已启动')
|
|
|
|
|
+console.log(`- 检查间隔: ${INTERVAL_MS / 1000 / 60} 分钟`)
|
|
|
|
|
+console.log(`- 监控池地址: ${DEFAULT_POOL_ADDRESS}`)
|
|
|
|
|
+console.log(`- 最大年龄: ${MAX_AGE_MS / 1000 / 60} 分钟`)
|
|
|
|
|
+console.log(`- Discord Webhook: ${DISCORD_WEBHOOK_URL}`)
|
|
|
|
|
+
|
|
|
|
|
+// 立即执行一次
|
|
|
|
|
+processPositions()
|
|
|
|
|
+
|
|
|
|
|
+// 每5分钟执行一次
|
|
|
|
|
+setInterval(processPositions, INTERVAL_MS)
|