| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236 |
- import { loadConfig } from '../config.js'
- const LIST_API = 'https://api2.byreal.io/byreal/api/dex/v2/position/list'
- const DETAIL_API = 'https://api2.byreal.io/byreal/api/dex/v2/position/detail'
- // const COPY_INFO_API = 'https://api2.byreal.io/byreal/api/dex/v2/copy/info'
- interface BonusInfo {
- fromCreatorPosition?: string
- [key: string]: unknown
- }
- interface PositionListItem {
- address?: string
- positionAddress?: string
- bonusInfo?: BonusInfo
- [key: string]: unknown
- }
- interface PositionListResult {
- total?: number
- positions?: PositionListItem[]
- [key: string]: unknown
- }
- interface ApiListResponse {
- retCode: number
- result?: {
- success?: boolean
- data?: PositionListResult
- [key: string]: unknown
- }
- }
- interface PositionDetailData {
- positionAddress?: string
- bonusInfo?: BonusInfo
- status?: number
- address?: string
- [key: string]: unknown
- }
- interface ApiDetailResponse {
- retCode: number
- result?: {
- success?: boolean
- data?: PositionDetailData
- [key: string]: unknown
- }
- }
- function isClosedStatus(status: number | undefined): boolean {
- if (status === 1) return true
- return false
- }
- async function fetchPositionList(
- userAddress: string,
- page: number,
- pageSize: number,
- tokenAddress?: string,
- ): Promise<ApiListResponse> {
- const params = new URLSearchParams({
- userAddress,
- page: String(page),
- pageSize: String(pageSize),
- status: String(0)
- })
- if (tokenAddress) params.set('tokenAddress', tokenAddress)
- const url = `${LIST_API}?${params.toString()}`
- const res = await fetch(url)
- return res.json() as Promise<ApiListResponse>
- }
- async function fetchPositionDetail(address: string): Promise<ApiDetailResponse> {
- const url = `${DETAIL_API}?address=${encodeURIComponent(address)}`
- const res = await fetch(url)
- return res.json() as Promise<ApiDetailResponse>
- }
- interface LpCloseApiResponse {
- success: boolean
- txid?: string
- [key: string]: unknown
- }
- async function callLpCloseApi(
- apiUrl: string,
- auth: string,
- nftMintAddress: string
- ): Promise<LpCloseApiResponse> {
- const res = await fetch(apiUrl, {
- method: 'POST',
- body: JSON.stringify({ nftMintAddress }),
- })
- return res.json() as Promise<LpCloseApiResponse>
- }
- async function sendDiscordCloseResultNotification(
- webhookUrl: string,
- childPositionAddress: string,
- parentPositionAddress: string,
- success: boolean,
- txid?: string
- ): Promise<void> {
- const embed = {
- title: success ? '✅ 子仓位关闭成功' : '❌ 子仓位关闭失败',
- description: success
- ? '父仓位已关闭,已通过接口关闭子仓位。'
- : '父仓位已关闭,但调用关闭接口失败,请手动处理子仓位。',
- color: success ? 0x00ff00 : 0xff0000,
- fields: [
- {
- name: '📍 子仓位地址',
- value: `[${childPositionAddress.slice(0, 8)}...${childPositionAddress.slice(-8)}](https://solscan.io/account/${childPositionAddress})`,
- inline: false,
- },
- {
- name: '📍 父仓位地址',
- value: `[${parentPositionAddress.slice(0, 8)}...${parentPositionAddress.slice(-8)}](https://solscan.io/account/${parentPositionAddress})`,
- inline: false,
- },
- ...(success && txid
- ? [
- {
- name: '交易 ID',
- value: `[${txid.slice(0, 16)}...](https://solscan.io/tx/${txid})`,
- inline: false,
- },
- ]
- : []),
- ],
- timestamp: new Date().toISOString(),
- footer: { text: 'ByReal Auto Trading' },
- }
- await fetch(webhookUrl, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ embeds: [embed] }),
- })
- }
- /**
- * 获取全部 LP 仓位(分页),检查每个仓位的 bonusInfo.fromCreatorPosition 对应父仓位是否已关闭,
- * 若已关闭则调用 lp-close 接口关闭子仓位,并根据接口结果发送 Discord 通知。
- */
- export async function checkParentPositionsClosed(): Promise<void> {
- const cfg = loadConfig()
- const closeCfg = cfg.closePosition
- if (!closeCfg) {
- console.log('[closePosition] 未配置 closePosition,跳过')
- return
- }
- const { userAddress, discordWebhookUrl, pageSize, lpCloseApiUrl, lpCloseAuth } = closeCfg
- const PAGE_SIZE = pageSize ?? 50
- let page = 1
- let total = 0
- const parentAddressesByChild = new Map<string, string>() // childAddress -> parentAddress
- do {
- const listRes = await fetchPositionList(userAddress, page, PAGE_SIZE)
- const data = listRes?.result?.data
- const list = data?.positions ?? []
- const totalFromApi = data?.total ?? 0
- if (page === 1) total = totalFromApi
- for (const item of list) {
- const childDetailRes = await fetchPositionDetail(item.positionAddress ?? '')
- const childDetail = childDetailRes?.result?.data as PositionDetailData
- if (!childDetailRes?.result?.success || childDetail == null) {
- console.warn(`[closePosition] 获取子仓位详情失败: ${item.address ?? item.positionAddress ?? ''}`)
- continue
- }
- const childAddress = childDetail.positionAddress ?? ''
- if (childDetail.bonusInfo?.fromCreatorPosition) {
- parentAddressesByChild.set(childAddress, (childDetail.bonusInfo?.fromCreatorPosition as string) ?? '')
- }
- }
- if (list.length < PAGE_SIZE || list.length === 0) break
- page++
- } while (page * PAGE_SIZE < total)
- const parentAddresses = [...new Set(parentAddressesByChild.values())]
- console.log(`[closePosition] 共 ${parentAddressesByChild.size} 条仓位含父仓位,去重后 ${parentAddresses.length} 个父仓位待检查`)
- for (const [childAddress, parentAddress] of parentAddressesByChild) {
- try {
- const detailRes = await fetchPositionDetail(parentAddress)
- const detail = detailRes?.result?.data
- if (!detailRes?.result?.success || detail == null) {
- console.warn(`[closePosition] 获取父仓位详情失败: ${parentAddress}`)
- continue
- }
- if (isClosedStatus(detail.status)) {
- console.log(`[closePosition] 父仓位已关闭: ${parentAddress},调用接口关闭子仓位`)
- try {
- const closeRes = await callLpCloseApi(lpCloseApiUrl, lpCloseAuth, childAddress)
- await sendDiscordCloseResultNotification(
- discordWebhookUrl,
- childAddress,
- parentAddress,
- !!closeRes?.success,
- closeRes?.txid
- )
- if (closeRes?.success) {
- console.log(`[closePosition] 子仓位 ${childAddress} 关闭成功, txid: ${closeRes.txid ?? '-'}`)
- } else {
- console.warn(`[closePosition] 子仓位 ${childAddress} 关闭失败`)
- }
- } catch (e) {
- console.error('[closePosition] 调用关闭接口异常:', e)
- await sendDiscordCloseResultNotification(
- discordWebhookUrl,
- childAddress,
- parentAddress,
- false
- )
- }
- }
- } catch (e) {
- console.error(`[closePosition] 检查父仓位 ${parentAddress} 异常:`, e)
- }
- }
- }
- const INTERVAL_MS = 60 * 60 * 1000 // 每 60 分钟
- function run(): void {
- checkParentPositionsClosed().catch(console.error)
- }
- run()
- setInterval(run, INTERVAL_MS)
- console.log('[closePosition] 已启动,每', INTERVAL_MS / 60000, '分钟检查一次')
|