index.ts 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. import { loadConfig } from '../config.js'
  2. import type { OpenPositionEvent } from '../solana/openPositionListener.js'
  3. const cfg = loadConfig()
  4. export async function copyPosition(positionDetails: OpenPositionEvent['positionDetails']) {
  5. if (!positionDetails) return
  6. const mintA = positionDetails?.mintA || undefined
  7. const mintB = positionDetails?.mintB || undefined
  8. const mintASymbol = mintA?.symbol || undefined
  9. const mintBSymbol = mintB?.symbol || undefined
  10. const totalDeposit = positionDetails?.totalDeposit || undefined
  11. // const upperTick = positionDetails?.upperTick || undefined
  12. // const lowerTick = positionDetails?.lowerTick || undefined
  13. const positionAddress = positionDetails?.positionAddress || ''
  14. const providerAddress = positionDetails?.providerAddress || ''
  15. const nftMintAddress = positionDetails?.nftMintAddress || ''
  16. const copyConfig = cfg?.positionCopy?.config?.[`${mintASymbol}/${mintBSymbol}`] || undefined
  17. const blacklist = cfg?.blacklist || []
  18. if (blacklist.includes(providerAddress)) {
  19. return 'blacklist'
  20. }
  21. let maxUsdValue = 0.2
  22. if (!copyConfig) {
  23. return 'no copy config'
  24. }
  25. const minimumDeposit = copyConfig?.minimumDeposit || 0
  26. if (Number(totalDeposit) < minimumDeposit) {
  27. return
  28. }
  29. if (Number(totalDeposit) > 1000) {
  30. maxUsdValue = copyConfig?.copyMount?.[1000] || 2
  31. } else if (Number(totalDeposit) > 500) {
  32. maxUsdValue = copyConfig?.copyMount?.[500] || 1
  33. } else if (Number(totalDeposit) > minimumDeposit) {
  34. maxUsdValue = copyConfig?.copyMount?.[minimumDeposit] || 0.2
  35. }
  36. console.log(`[${Date.now()}] 复制仓位: ${providerAddress.slice(0, 4)}...${providerAddress.slice(-4)} ${mintASymbol}/${mintBSymbol} ${maxUsdValue}$`)
  37. return fetch(cfg?.positionCopy?.url, {
  38. method: 'POST',
  39. headers: {
  40. 'Content-Type': 'application/json',
  41. 'Authorization': 'Basic YWRtaW46YzU4ODk5Njc='
  42. },
  43. body: JSON.stringify({
  44. nftMintAddress,
  45. positionAddress,
  46. maxUsdValue,
  47. }),
  48. }).then(res => res.json()).then((response: unknown) => {
  49. const data = response as { success?: boolean, error?: string }
  50. if (!data.success) {
  51. throw new Error(data.error ?? 'Failed to copy position')
  52. }
  53. console.log(`[${Date.now()}] 复制仓位成功`)
  54. // 发送Discord通知,使用embeds
  55. const solscanBaseUrl = 'https://solscan.io'
  56. const embed = {
  57. title: '✅ 仓位复制成功',
  58. description: `成功复制仓位:**${mintASymbol}/${mintBSymbol}**`,
  59. color: 0x00ff00, // 绿色
  60. fields: [
  61. {
  62. name: '💰 币对',
  63. value: `${mintASymbol}/${mintBSymbol}`,
  64. inline: true
  65. },
  66. {
  67. name: '💵 总存款',
  68. value: `$${Number(totalDeposit).toFixed(2)}`,
  69. inline: true
  70. },
  71. {
  72. name: '📊 复制金额',
  73. value: `$${maxUsdValue.toFixed(2)}`,
  74. inline: true
  75. },
  76. {
  77. name: '👤 提供者地址',
  78. value: `[${providerAddress.slice(0, 4)}...${providerAddress.slice(-4)}](${solscanBaseUrl}/account/${providerAddress})`,
  79. inline: false
  80. },
  81. {
  82. name: '📍 仓位地址',
  83. value: `[${positionAddress.slice(0, 8)}...${positionAddress.slice(-8)}](${solscanBaseUrl}/account/${positionAddress})`,
  84. inline: false
  85. }
  86. ],
  87. timestamp: new Date().toISOString(),
  88. url: `https://www.byreal.io/en/portfolio?userAddress=${providerAddress}&tab=current&positionAddress=${positionAddress}`,
  89. footer: {
  90. text: 'ByReal Auto Trading'
  91. }
  92. }
  93. // 如果有价格信息,添加到字段中
  94. if (mintA?.price && mintB?.price) {
  95. embed.fields.push({
  96. name: '💹 价格信息',
  97. value: `${mintASymbol}: $${Number(mintA.price).toFixed(6)}\n${mintBSymbol}: $${Number(mintB.price).toFixed(6)}`,
  98. inline: false
  99. })
  100. }
  101. return fetch(cfg?.discordWebhookUrl, {
  102. method: 'POST',
  103. headers: {
  104. 'Content-Type': 'application/json',
  105. },
  106. body: JSON.stringify({
  107. embeds: [embed]
  108. }),
  109. })
  110. }).catch(err => {
  111. // 发送失败通知
  112. const errorEmbed = {
  113. title: '❌ 仓位复制失败',
  114. description: `复制仓位失败:**${mintASymbol}/${mintBSymbol}**`,
  115. url: `https://www.byreal.io/en/portfolio?userAddress=${providerAddress}&tab=current&positionAddress=${positionAddress}`,
  116. color: 0xff0000, // 红色
  117. fields: [
  118. {
  119. name: '💰 币对',
  120. value: `${mintASymbol}/${mintBSymbol}`,
  121. inline: true
  122. },
  123. {
  124. name: '💵 总存款',
  125. value: `$${Number(totalDeposit).toFixed(2)}`,
  126. inline: true
  127. },
  128. {
  129. name: '📍 仓位地址',
  130. value: `[${positionAddress.slice(0, 8)}...${positionAddress.slice(-8)}](https://solscan.io/account/${positionAddress})`,
  131. inline: false
  132. },
  133. {
  134. name: '⚠️ 错误信息',
  135. value: `\`\`\`${err instanceof Error ? err.message.slice(0, 100) : String(err).slice(0, 100)}\`\`\``,
  136. inline: false
  137. }
  138. ],
  139. timestamp: new Date().toISOString(),
  140. footer: {
  141. text: 'ByReal Auto Trading'
  142. }
  143. }
  144. return fetch(cfg?.discordWebhookUrl, {
  145. method: 'POST',
  146. headers: {
  147. 'Content-Type': 'application/json',
  148. },
  149. body: JSON.stringify({
  150. embeds: [errorEmbed]
  151. }),
  152. }).catch(notifyErr => {
  153. console.error(`[${Date.now()}] 发送Discord通知失败: ${notifyErr}`)
  154. })
  155. })
  156. }