index.mjs 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. // watch.mjs - 监控 top-positions 并发送到 Discord
  2. const API_URL = 'https://app-byreal-table.trrlzk.easypanel.host/api/top-positions'
  3. const DISCORD_WEBHOOK_URL = 'https://discord.com/api/webhooks/1449354624225120378/7gOkPXzZkaoQF7XWKFZRsevVSKXcUOuim2Rhtx_gWzBd9b7wphFMQiAIqBnRT3Gmkog4'
  4. const DEFAULT_POOL_ADDRESS = 'FPBW9dtVRoUug2BeUKZAzaknd6iiet9jHM8RcTvwUkyC'
  5. const INTERVAL_MS = 5 * 60 * 1000 // 5分钟
  6. const MAX_AGE_MS = 10 * 60 * 1000 // 10分钟
  7. // 存储已发送的 positionAddress
  8. const sentPositions = new Set()
  9. /**
  10. * 获取 top-positions 数据
  11. */
  12. async function fetchTopPositions() {
  13. try {
  14. const response = await fetch(API_URL, {
  15. method: 'POST',
  16. headers: {
  17. 'content-type': 'application/json',
  18. },
  19. body: JSON.stringify({
  20. poolAddress: DEFAULT_POOL_ADDRESS,
  21. page: 1,
  22. pageSize: 200,
  23. sortField: 'liquidity',
  24. status: 0,
  25. }),
  26. })
  27. if (!response.ok) {
  28. throw new Error(`HTTP error! status: ${response.status}`)
  29. }
  30. const result = await response.json()
  31. if (result.retCode !== 0 || !result.result?.data?.records) {
  32. console.error('API 返回错误:', result.retMsg || '未知错误')
  33. return []
  34. }
  35. return result.result.data.records
  36. } catch (error) {
  37. console.error('获取数据失败:', error)
  38. return []
  39. }
  40. }
  41. /**
  42. * 发送消息到 Discord
  43. */
  44. async function sendToDiscord(position) {
  45. try {
  46. const ageMinutes = Math.floor(position.positionAgeMs / 1000 / 60)
  47. const ageSeconds = Math.floor((position.positionAgeMs / 1000) % 60)
  48. const embed = {
  49. title: '🆕 新仓位发现',
  50. color: 0x00b098, // 绿色
  51. fields: [
  52. {
  53. name: '创建地址',
  54. value: `\`${position.walletAddress}\``,
  55. inline: true,
  56. },
  57. {
  58. name: '仓位地址',
  59. value: `\`${position.positionAddress}\``,
  60. inline: false,
  61. },
  62. {
  63. name: '流动性',
  64. value: `$${Number(position.liquidityUsd).toFixed(2)}`,
  65. inline: true,
  66. },
  67. {
  68. name: '复制数',
  69. value: `${position.copies}`,
  70. inline: true,
  71. },
  72. {
  73. name: '奖励',
  74. value: `$${Number(position.bonusUsd).toFixed(2)}`,
  75. inline: true,
  76. },
  77. {
  78. name: 'FEE',
  79. value: `$${Number(position.earnedUsd).toFixed(2)}`,
  80. inline: true,
  81. },
  82. {
  83. name: 'PNL',
  84. value: `$${Number(position.pnlUsd).toFixed(2)}`,
  85. inline: true,
  86. },
  87. {
  88. name: '创建时间',
  89. value: `${ageMinutes}分${ageSeconds}秒`,
  90. inline: true,
  91. },
  92. {
  93. name: 'APR',
  94. value: calculateAPR(position),
  95. inline: true,
  96. },
  97. ],
  98. timestamp: new Date().toISOString(),
  99. url: `https://www.byreal.io/en/portfolio?userAddress=${position.walletAddress}&tab=current&positionAddress=${position.positionAddress}`,
  100. }
  101. const response = await fetch(DISCORD_WEBHOOK_URL, {
  102. method: 'POST',
  103. headers: {
  104. 'content-type': 'application/json',
  105. },
  106. body: JSON.stringify({
  107. embeds: [embed],
  108. }),
  109. })
  110. if (!response.ok) {
  111. throw new Error(`Discord webhook 失败: ${response.status}`)
  112. }
  113. console.log(`✅ 已发送到 Discord: ${position.positionAddress}`)
  114. return true
  115. } catch (error) {
  116. console.error('发送到 Discord 失败:', error)
  117. return false
  118. }
  119. }
  120. /**
  121. * 计算 APR
  122. */
  123. function calculateAPR(position) {
  124. const earnedPerSecond = Number(position.earnedUsd) / (Number(position.positionAgeMs) / 1000)
  125. const apr = (earnedPerSecond * 60 * 60 * 24 * 365) / Number(position.liquidityUsd)
  126. return `${(apr * 100).toFixed(2)}%`
  127. }
  128. /**
  129. * 处理新仓位
  130. */
  131. async function processPositions() {
  132. console.log(`\n[${new Date().toLocaleString('zh-CN')}] 开始检查新仓位...`)
  133. const records = await fetchTopPositions()
  134. if (records.length === 0) {
  135. console.log('未获取到数据')
  136. return
  137. }
  138. // 过滤出10分钟以内的仓位
  139. const newPositions = records.filter((record) => {
  140. const ageMs = Number(record.positionAgeMs)
  141. return ageMs < MAX_AGE_MS && ageMs >= 0
  142. })
  143. console.log(`找到 ${newPositions.length} 个10分钟内的仓位`)
  144. // 过滤出未发送过的仓位
  145. const unsentPositions = newPositions.filter((position) => {
  146. const address = position.positionAddress
  147. return address && !sentPositions.has(address)
  148. })
  149. console.log(`其中 ${unsentPositions.length} 个未发送过`)
  150. // 发送到 Discord
  151. for (const position of unsentPositions) {
  152. const address = position.positionAddress
  153. if (address) {
  154. const success = await sendToDiscord(position)
  155. if (success) {
  156. sentPositions.add(address)
  157. }
  158. // 避免发送过快,稍微延迟
  159. await new Promise((resolve) => setTimeout(resolve, 500))
  160. }
  161. }
  162. console.log(`处理完成,已发送 ${unsentPositions.length} 个新仓位`)
  163. }
  164. // 启动定时任务
  165. console.log('🚀 监控程序已启动')
  166. console.log(`- 检查间隔: ${INTERVAL_MS / 1000 / 60} 分钟`)
  167. console.log(`- 监控池地址: ${DEFAULT_POOL_ADDRESS}`)
  168. console.log(`- 最大年龄: ${MAX_AGE_MS / 1000 / 60} 分钟`)
  169. console.log(`- Discord Webhook: ${DISCORD_WEBHOOK_URL}`)
  170. // 立即执行一次
  171. processPositions()
  172. // 每5分钟执行一次
  173. setInterval(processPositions, INTERVAL_MS)