index.mjs 9.4 KB

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