index.mjs 6.3 KB

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