index.mjs 9.8 KB

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