index.mjs 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  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 TRADE_PAIR_URL = 'https://app-byreal-table.trrlzk.easypanel.host/api/pools/list?page=1&pageSize=500'
  4. const DISCORD_WEBHOOK_URL = 'https://discord.com/api/webhooks/1449354624225120378/7gOkPXzZkaoQF7XWKFZRsevVSKXcUOuim2Rhtx_gWzBd9b7wphFMQiAIqBnRT3Gmkog4'
  5. const DISCORD_WEBHOOK_URL_SMART_WALLET = 'https://discord.com/api/webhooks/1450301829064954040/zNYc-mnYPDFgnAPLZxY40aicERwdL4TsvXk9VyiqWgyjRqJMXKIBzVHEvBiHbpbJrmtu'
  6. const DEFAULT_POOL_ADDRESS = TOKEN_MAP['MON']
  7. const INTERVAL_MS = 1 * 60 * 1000 // 1分钟
  8. const MAX_AGE_MS = 10 * 60 * 1000 // 10分钟
  9. // 存储已发送的 positionAddress
  10. const sentPositions = new Set()
  11. const sentTradePair = new Set()
  12. let firstRun = true
  13. /**
  14. * 获取 top-positions 数据
  15. */
  16. async function fetchTopPositions(token_symbol) {
  17. try {
  18. const response = await fetch(API_URL, {
  19. method: 'POST',
  20. headers: {
  21. 'content-type': 'application/json',
  22. },
  23. body: JSON.stringify({
  24. poolAddress: TOKEN_MAP[token_symbol],
  25. page: 1,
  26. pageSize: 200,
  27. sortField: 'liquidity',
  28. status: 0,
  29. }),
  30. })
  31. if (!response.ok) {
  32. throw new Error(`HTTP error! status: ${response.status}`)
  33. }
  34. const result = await response.json()
  35. if (result.retCode !== 0 || !result.result?.data?.records) {
  36. console.error('API 返回错误:', result.retMsg || '未知错误')
  37. return []
  38. }
  39. const poolMap = Object.values(result.result.data.poolMap)[0]
  40. return {
  41. records: result.result.data.records,
  42. symbol: `${poolMap.mintA.symbol}/${poolMap.mintB.symbol}`,
  43. }
  44. } catch (error) {
  45. console.error('获取数据失败:', error)
  46. return []
  47. }
  48. }
  49. /**
  50. * 获取交易对数据
  51. */
  52. async function fetchTradePair() {
  53. try {
  54. const response = await fetch(TRADE_PAIR_URL)
  55. if (!response.ok) {
  56. throw new Error(`HTTP error! status: ${response.status}`)
  57. }
  58. const result = await response.json()
  59. if (result.retCode !== 0 || !result.result?.data?.records) {
  60. console.error('API 返回错误:', result.retMsg || '未知错误')
  61. return []
  62. }
  63. return result.result.data.records
  64. } catch (error) {
  65. console.error('获取交易对数据失败:', error)
  66. return []
  67. }
  68. }
  69. /**
  70. * 发送消息到 Discord
  71. */
  72. async function sendToDiscord(position, symbol) {
  73. try {
  74. const ageMinutes = Math.floor(position.positionAgeMs / 1000 / 60)
  75. const ageSeconds = Math.floor((position.positionAgeMs / 1000) % 60)
  76. const embed = {
  77. title: '🆕 新仓位发现',
  78. color: 0x00b098, // 绿色
  79. fields: [
  80. {
  81. name: '创建地址',
  82. value: `\`${position.walletAddress}\``,
  83. inline: true,
  84. },
  85. {
  86. name: '仓位地址',
  87. value: `\`${position.positionAddress}\``,
  88. inline: false,
  89. },
  90. {
  91. name: '地址',
  92. value: `https://www.byreal.io/en/portfolio?userAddress=${position.walletAddress}&tab=current&positionAddress=${position.positionAddress}`,
  93. inline: false,
  94. },
  95. {
  96. name: '交易对',
  97. value: `\`${symbol}\``,
  98. inline: true,
  99. },
  100. {
  101. name: '流动性',
  102. value: `$${Number(position.liquidityUsd).toFixed(2)}`,
  103. inline: true,
  104. },
  105. {
  106. name: '复制数',
  107. value: `${position.copies}`,
  108. inline: true,
  109. },
  110. {
  111. name: '奖励',
  112. value: `$${Number(position.bonusUsd).toFixed(2)}`,
  113. inline: true,
  114. },
  115. {
  116. name: 'FEE',
  117. value: `$${Number(position.earnedUsd).toFixed(2)}`,
  118. inline: true,
  119. },
  120. {
  121. name: 'PNL',
  122. value: `$${Number(position.pnlUsd).toFixed(2)}`,
  123. inline: true,
  124. },
  125. {
  126. name: '创建时间',
  127. value: `${ageMinutes}分${ageSeconds}秒`,
  128. inline: true,
  129. },
  130. {
  131. name: 'APR',
  132. value: calculateAPR(position),
  133. inline: true,
  134. },
  135. ],
  136. timestamp: new Date().toISOString(),
  137. url: `https://www.byreal.io/en/portfolio?userAddress=${position.walletAddress}&tab=current&positionAddress=${position.positionAddress}`,
  138. }
  139. let DISCORD_WEBHOOK = SMART_WALLET.includes(position.walletAddress) ? DISCORD_WEBHOOK_URL_SMART_WALLET : DISCORD_WEBHOOK_URL
  140. if (SMART_WALLET.includes(position.walletAddress)) {
  141. embed.title = '💎 发现重点监控仓位'
  142. }
  143. const response = await fetch(DISCORD_WEBHOOK, {
  144. method: 'POST',
  145. headers: {
  146. 'content-type': 'application/json',
  147. },
  148. body: JSON.stringify({
  149. embeds: [embed],
  150. }),
  151. })
  152. if (!response.ok) {
  153. throw new Error(`Discord webhook 失败: ${response.status}`)
  154. }
  155. console.log(`✅ 已发送到 Discord: ${position.positionAddress}`)
  156. return true
  157. } catch (error) {
  158. console.error('发送到 Discord 失败:', error)
  159. return false
  160. }
  161. }
  162. /**
  163. * 发送消息到 Discord 交易对
  164. */
  165. async function sendToDiscordTradePair(pair) {
  166. const embed = {
  167. title: '🆕 新交易对发现',
  168. color: 0x00b098, // 绿色
  169. fields: [
  170. {
  171. name: '交易对',
  172. value: `${pair.mintA.mintInfo.symbol}/${pair.mintB.mintInfo.symbol}`,
  173. inline: true,
  174. },
  175. ],
  176. timestamp: new Date().toISOString(),
  177. url: `https://www.byreal.io/en/portfolio?userAddress=${pair.poolAddress}&tab=current&positionAddress=${pair.poolAddress}`,
  178. }
  179. try {
  180. const response = await fetch(DISCORD_WEBHOOK_URL_SMART_WALLET, {
  181. method: 'POST',
  182. headers: {
  183. 'content-type': 'application/json',
  184. },
  185. body: JSON.stringify({
  186. embeds: [embed],
  187. }),
  188. })
  189. if (!response.ok) {
  190. throw new Error(`Discord webhook 失败: ${response.status}`)
  191. }
  192. console.log(`✅ 已发送到 Discord: ${pair.poolAddress}`)
  193. } catch (error) {
  194. console.error('发送到 Discord 交易对失败:', error)
  195. return false
  196. }
  197. }
  198. /**
  199. * 计算 APR
  200. */
  201. function calculateAPR(position) {
  202. const earnedPerSecond = Number(position.earnedUsd) / (Number(position.positionAgeMs) / 1000)
  203. const apr = (earnedPerSecond * 60 * 60 * 24 * 365) / Number(position.liquidityUsd)
  204. return `${(apr * 100).toFixed(2)}%`
  205. }
  206. /**
  207. * 处理新仓位
  208. */
  209. async function processPositions(token_symbol) {
  210. console.log(`\n[${new Date().toLocaleString('zh-CN')}] 开始检查新仓位...`)
  211. const { records, symbol } = await fetchTopPositions(token_symbol)
  212. if (!records) {
  213. console.log('获取数据失败')
  214. return
  215. }
  216. // 过滤出10分钟以内的仓位
  217. const newPositions = records.filter((record) => {
  218. const ageMs = Number(record.positionAgeMs)
  219. return ageMs < MAX_AGE_MS && ageMs >= 0
  220. })
  221. console.log(`找到 ${newPositions.length} 个10分钟内的仓位`)
  222. // 过滤出未发送过的仓位
  223. const unsentPositions = newPositions.filter((position) => {
  224. const address = position.positionAddress
  225. return address && !sentPositions.has(address)
  226. })
  227. console.log(`其中 ${unsentPositions.length} 个未发送过`)
  228. // 发送到 Discord
  229. for (const position of unsentPositions) {
  230. const address = position.positionAddress
  231. if (address) {
  232. const success = await sendToDiscord(position, symbol)
  233. if (success) {
  234. sentPositions.add(address)
  235. }
  236. // 避免发送过快,稍微延迟
  237. await new Promise((resolve) => setTimeout(resolve, 500))
  238. }
  239. }
  240. console.log(`处理完成,已发送 ${unsentPositions.length} 个新仓位`)
  241. }
  242. /**
  243. * 处理交易对数据
  244. */
  245. async function processTradePair() {
  246. console.log(`\n[${new Date().toLocaleString('zh-CN')}] 开始检查交易对...`)
  247. const tradePair = await fetchTradePair()
  248. if (!tradePair) {
  249. console.log('获取交易对数据失败')
  250. return
  251. }
  252. console.log(`找到 ${tradePair.length} 个交易对`)
  253. // 过滤出未发送过的交易对
  254. const unsentTradePair = tradePair.filter((pair) => {
  255. return pair.poolAddress && !sentTradePair.has(pair.poolAddress)
  256. })
  257. console.log(`其中 ${unsentTradePair.length} 个未发送过`)
  258. if (!firstRun) {
  259. // 发送到 Discord
  260. for (const pair of unsentTradePair) {
  261. const success = await sendToDiscordTradePair(pair)
  262. if (success) {
  263. sentTradePair.add(pair.poolAddress)
  264. }
  265. // 避免发送过快,稍微延迟
  266. await new Promise((resolve) => setTimeout(resolve, 500))
  267. }
  268. console.log(`处理完成,已发送 ${unsentTradePair.length} 个交易对`)
  269. } else {
  270. firstRun = false
  271. console.log('第一次运行,不发送交易对')
  272. }
  273. }
  274. // 启动定时任务
  275. console.log('🚀 监控程序已启动')
  276. console.log(`- 检查间隔: ${INTERVAL_MS / 1000 / 60} 分钟`)
  277. console.log(`- 监控池地址: ${DEFAULT_POOL_ADDRESS}`)
  278. console.log(`- 最大年龄: ${MAX_AGE_MS / 1000 / 60} 分钟`)
  279. console.log(`- Discord Webhook: ${DISCORD_WEBHOOK_URL}`)
  280. // 立即执行一次
  281. processPositions('MON')
  282. processPositions('WET')
  283. processTradePair()
  284. // 每5分钟执行一次
  285. setInterval(() => {
  286. processPositions('MON')
  287. processPositions('WET')
  288. }, INTERVAL_MS)
  289. setInterval(processTradePair, MAX_AGE_MS)