index.mjs 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  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. // 等待5秒再复制流动性
  160. await new Promise((resolve) => setTimeout(resolve, 5000))
  161. await addLiquidity(position, symbol, maxUsdValue)
  162. console.log(`✅ 已复制流动性: ${position.positionAddress}`)
  163. } else {
  164. console.log(`❌ 未复制流动性: ${position.positionAddress}`)
  165. }
  166. return true
  167. } catch (error) {
  168. console.error('发送到 Discord 失败:', error)
  169. return false
  170. }
  171. }
  172. /**
  173. * 发送消息到 Discord 交易对
  174. */
  175. async function sendToDiscordTradePair(pair) {
  176. const embed = {
  177. title: '🆕 新交易对发现',
  178. color: 0x00b098, // 绿色
  179. fields: [
  180. {
  181. name: '交易对',
  182. value: `${pair.mintA.mintInfo.symbol}/${pair.mintB.mintInfo.symbol}`,
  183. inline: true,
  184. },
  185. ],
  186. timestamp: new Date().toISOString(),
  187. url: `https://www.byreal.io/en/portfolio?userAddress=${pair.poolAddress}&tab=current&positionAddress=${pair.poolAddress}`,
  188. }
  189. try {
  190. const response = await fetch(DISCORD_WEBHOOK_URL_SMART_WALLET, {
  191. method: 'POST',
  192. headers: {
  193. 'content-type': 'application/json',
  194. },
  195. body: JSON.stringify({
  196. embeds: [embed],
  197. }),
  198. })
  199. if (!response.ok) {
  200. throw new Error(`Discord webhook 失败: ${response.status}`)
  201. }
  202. console.log(`✅ 已发送到 Discord: ${pair.poolAddress}`)
  203. return true
  204. } catch (error) {
  205. console.error('发送到 Discord 交易对失败:', error)
  206. return false
  207. }
  208. }
  209. /**
  210. * 计算 APR
  211. */
  212. function calculateAPR(position) {
  213. const earnedPerSecond = Number(position.earnedUsd) / (Number(position.positionAgeMs) / 1000)
  214. const apr = (earnedPerSecond * 60 * 60 * 24 * 365) / Number(position.liquidityUsd)
  215. return `${(apr * 100).toFixed(2)}%`
  216. }
  217. /**
  218. * 处理新仓位
  219. */
  220. async function processPositions(token_symbol, maxUsdValue) {
  221. console.log(`\n[${new Date().toLocaleString('zh-CN')}] 开始检查新仓位...`)
  222. const { records, symbol, tokenList } = await fetchTopPositions(token_symbol)
  223. if (!records) {
  224. console.log('获取数据失败')
  225. return
  226. }
  227. // 过滤出10分钟以内的仓位
  228. const newPositions = records.filter((record) => {
  229. const ageMs = Number(record.positionAgeMs)
  230. return ageMs < MAX_AGE_MS && ageMs >= 0
  231. })
  232. console.log(`找到 ${newPositions.length} 个10分钟内的仓位`)
  233. // 过滤出未发送过的仓位
  234. const unsentPositions = newPositions.filter((position) => {
  235. const address = position.positionAddress
  236. return address && !sentPositions.has(address)
  237. })
  238. console.log(`其中 ${unsentPositions.length} 个未发送过`)
  239. // 发送到 Discord
  240. for (const position of unsentPositions) {
  241. const address = position.positionAddress
  242. if (address) {
  243. const success = await sendToDiscord(position, symbol, tokenList, maxUsdValue)
  244. if (success) {
  245. sentPositions.add(address)
  246. }
  247. // 避免发送过快,稍微延迟
  248. await new Promise((resolve) => setTimeout(resolve, 500))
  249. }
  250. }
  251. console.log(`处理完成,已发送 ${unsentPositions.length} 个新仓位`)
  252. }
  253. /**
  254. * 处理交易对数据
  255. */
  256. async function processTradePair() {
  257. console.log(`\n[${new Date().toLocaleString('zh-CN')}] 开始检查交易对...`)
  258. const tradePair = await fetchTradePair()
  259. if (!tradePair) {
  260. console.log('获取交易对数据失败')
  261. return
  262. }
  263. console.log(`找到 ${tradePair.length} 个交易对`)
  264. // 过滤出未发送过的交易对
  265. const unsentTradePair = tradePair.filter((pair) => {
  266. return pair.poolAddress && !sentTradePair.has(pair.poolAddress)
  267. })
  268. console.log(`其中 ${unsentTradePair.length} 个未发送过`)
  269. if (!firstRun) {
  270. // 发送到 Discord
  271. for (const pair of unsentTradePair) {
  272. const success = await sendToDiscordTradePair(pair)
  273. if (success) {
  274. sentTradePair.add(pair.poolAddress)
  275. }
  276. // 避免发送过快,稍微延迟
  277. await new Promise((resolve) => setTimeout(resolve, 500))
  278. }
  279. console.log(`处理完成,已发送 ${unsentTradePair.length} 个交易对`)
  280. } else {
  281. firstRun = false
  282. for (const pair of tradePair) {
  283. sentTradePair.add(pair.poolAddress)
  284. }
  285. console.log('第一次运行,不发送交易对')
  286. }
  287. }
  288. // 启动定时任务
  289. console.log('🚀 监控程序已启动')
  290. console.log(`- 检查间隔: ${INTERVAL_MS / 1000 / 60} 分钟`)
  291. console.log(`- 监控池地址: ${DEFAULT_POOL_ADDRESS}`)
  292. console.log(`- 最大年龄: ${MAX_AGE_MS / 1000 / 60} 分钟`)
  293. console.log(`- Discord Webhook: ${DISCORD_WEBHOOK_URL}`)
  294. // 立即执行一次
  295. processPositions('MON')
  296. processPositions('WET')
  297. processTradePair()
  298. // 每5分钟执行一次
  299. setInterval(() => {
  300. // processPositions('MON')
  301. // processPositions('WET')
  302. processPositions('WhiteWhale', 3)
  303. // processPositions('fish', 1)
  304. }, INTERVAL_MS)
  305. setInterval(processTradePair, MAX_AGE_MS)