lushdog@outlook.com 3 днів тому
коміт
0c23fcb02d
3 змінених файлів з 220 додано та 0 видалено
  1. 9 0
      Dockerfile
  2. 198 0
      index.mjs
  3. 13 0
      package.json

+ 9 - 0
Dockerfile

@@ -0,0 +1,9 @@
+FROM node:20-alpine
+
+WORKDIR /app
+
+COPY package.json .
+
+COPY . .
+
+CMD ["npm", "start"]

+ 198 - 0
index.mjs

@@ -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)

+ 13 - 0
package.json

@@ -0,0 +1,13 @@
+{
+  "name": "byreal-watcher",
+  "version": "1.0.0",
+  "description": "",
+  "license": "ISC",
+  "author": "",
+  "type": "module",
+  "main": "index.mjs",
+  "scripts": {
+    "start": "node index.mjs",
+    "test": "echo \"Error: no test specified\" && exit 1"
+  }
+}