|
|
@@ -3,8 +3,10 @@ import {
|
|
|
PublicKey,
|
|
|
type Finality,
|
|
|
type LogsCallback,
|
|
|
- type TransactionSignature
|
|
|
+ type TransactionSignature,
|
|
|
+ type ParsedTransactionWithMeta
|
|
|
} from '@solana/web3.js'
|
|
|
+import bs58 from 'bs58'
|
|
|
|
|
|
export type OpenPositionEvent = {
|
|
|
signature: TransactionSignature
|
|
|
@@ -14,7 +16,7 @@ export type OpenPositionEvent = {
|
|
|
accounts?: string[]
|
|
|
tokenMints?: string[]
|
|
|
positionAccount?: string
|
|
|
- logs?: string[] // 添加原始日志
|
|
|
+ logs?: string[]
|
|
|
positionDetails?: {
|
|
|
positionAddress: string
|
|
|
providerAddress: string
|
|
|
@@ -43,37 +45,197 @@ export type OpenPositionListenerOpts = {
|
|
|
commitment?: 'processed' | 'confirmed' | 'finalized'
|
|
|
logIncludes?: string[]
|
|
|
maxSupportedTransactionVersion?: number
|
|
|
+ jupiterApiKey?: string
|
|
|
}
|
|
|
|
|
|
-type PositionApiResponse = {
|
|
|
- retCode: number
|
|
|
- retMsg: string
|
|
|
- result?: {
|
|
|
- success?: boolean
|
|
|
- data?: {
|
|
|
- positionAddress: string
|
|
|
- providerAddress: string
|
|
|
- nftMintAddress: string
|
|
|
- pool: {
|
|
|
- poolAddress: string
|
|
|
- mintA: {
|
|
|
- address: string
|
|
|
- symbol: string
|
|
|
- decimals: number
|
|
|
- price: string
|
|
|
- }
|
|
|
- mintB: {
|
|
|
- address: string
|
|
|
- symbol: string
|
|
|
- decimals: number
|
|
|
- price: string
|
|
|
+// open_position_with_token22_nft instruction discriminator
|
|
|
+const OPEN_POSITION_DISCRIMINATOR = [77, 255, 174, 82, 125, 29, 201, 46]
|
|
|
+
|
|
|
+// CreatePersonalPositionEvent discriminator
|
|
|
+const CREATE_POSITION_EVENT_DISC = [100, 30, 87, 249, 196, 223, 154, 206]
|
|
|
+
|
|
|
+function matchDiscriminator(data: Buffer, expected: number[]): boolean {
|
|
|
+ if (data.length < 8) return false
|
|
|
+ for (let i = 0; i < 8; i++) {
|
|
|
+ if (data[i] !== expected[i]) return false
|
|
|
+ }
|
|
|
+ return true
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Decode CreatePersonalPositionEvent from program logs.
|
|
|
+ * Layout: [8 disc][32 poolState][32 minter][32 nftOwner][4 tickLower][4 tickUpper][16 liquidity][8 amount0][8 amount1]
|
|
|
+ */
|
|
|
+function decodeCreatePositionEvent(logs: string[]): {
|
|
|
+ poolState: string
|
|
|
+ minter: string
|
|
|
+ nftOwner: string
|
|
|
+ tickLower: number
|
|
|
+ tickUpper: number
|
|
|
+ liquidity: string
|
|
|
+ amount0: string
|
|
|
+ amount1: string
|
|
|
+} | null {
|
|
|
+ for (const log of logs) {
|
|
|
+ if (!log.startsWith('Program data:')) continue
|
|
|
+ const b64 = log.replace('Program data: ', '')
|
|
|
+ try {
|
|
|
+ const buf = Buffer.from(b64, 'base64')
|
|
|
+ if (buf.length >= 144 && matchDiscriminator(buf, CREATE_POSITION_EVENT_DISC)) {
|
|
|
+ let off = 8
|
|
|
+ const poolState = new PublicKey(buf.slice(off, off + 32)).toBase58(); off += 32
|
|
|
+ const minter = new PublicKey(buf.slice(off, off + 32)).toBase58(); off += 32
|
|
|
+ const nftOwner = new PublicKey(buf.slice(off, off + 32)).toBase58(); off += 32
|
|
|
+ const tickLower = buf.readInt32LE(off); off += 4
|
|
|
+ const tickUpper = buf.readInt32LE(off); off += 4
|
|
|
+ const lo = buf.readBigUInt64LE(off)
|
|
|
+ const hi = buf.readBigUInt64LE(off + 8)
|
|
|
+ const liquidity = (hi << 64n) | lo; off += 16
|
|
|
+ const amount0 = buf.readBigUInt64LE(off); off += 8
|
|
|
+ const amount1 = buf.readBigUInt64LE(off)
|
|
|
+ return {
|
|
|
+ poolState,
|
|
|
+ minter,
|
|
|
+ nftOwner,
|
|
|
+ tickLower,
|
|
|
+ tickUpper,
|
|
|
+ liquidity: liquidity.toString(),
|
|
|
+ amount0: amount0.toString(),
|
|
|
+ amount1: amount1.toString(),
|
|
|
}
|
|
|
}
|
|
|
- totalDeposit: string
|
|
|
- upperTick: number
|
|
|
- lowerTick: number
|
|
|
+ } catch {
|
|
|
+ // skip unparseable entries
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return null
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Find the open_position_with_token22_nft instruction by matching discriminator.
|
|
|
+ * This is more reliable than just matching programId when a transaction has multiple BYREAL instructions.
|
|
|
+ */
|
|
|
+function findOpenPositionInstruction(tx: ParsedTransactionWithMeta, programId: string) {
|
|
|
+ for (const ix of tx.transaction.message.instructions) {
|
|
|
+ if (!('programId' in ix) || ix.programId.toBase58() !== programId) continue
|
|
|
+ if ('data' in ix && typeof ix.data === 'string') {
|
|
|
+ // getParsedTransaction 返回的 data 是 base58 编码
|
|
|
+ const dataBuf = Buffer.from(bs58.decode(ix.data))
|
|
|
+ if (matchDiscriminator(dataBuf, OPEN_POSITION_DISCRIMINATOR)) return ix
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return null
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Extract account public keys from a parsed instruction.
|
|
|
+ */
|
|
|
+function getInstructionAccounts(ix: ReturnType<typeof findOpenPositionInstruction>): string[] {
|
|
|
+ if (ix && 'accounts' in ix && Array.isArray(ix.accounts)) {
|
|
|
+ return (ix.accounts as PublicKey[]).map((a) => a.toBase58())
|
|
|
+ }
|
|
|
+ return []
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 通过交易的 token balance 信息查找某个 token account 的 mint 地址。
|
|
|
+ * token_account_0 (index 9) 和 token_account_1 (index 10) 是用户的 token 账户,
|
|
|
+ * 它们的 mint 就是池子的 mintA 和 mintB。
|
|
|
+ */
|
|
|
+function findMintForTokenAccount(
|
|
|
+ tx: ParsedTransactionWithMeta,
|
|
|
+ tokenAccountKey: string
|
|
|
+): { mint: string; decimals: number } | undefined {
|
|
|
+ const accountKeys = tx.transaction.message.accountKeys.map(k =>
|
|
|
+ typeof k === 'string' ? k : k.pubkey.toBase58()
|
|
|
+ )
|
|
|
+ const accountIndex = accountKeys.indexOf(tokenAccountKey)
|
|
|
+ if (accountIndex === -1) return undefined
|
|
|
+
|
|
|
+ const allBalances = [
|
|
|
+ ...(tx.meta?.preTokenBalances || []),
|
|
|
+ ...(tx.meta?.postTokenBalances || []),
|
|
|
+ ]
|
|
|
+ for (const bal of allBalances) {
|
|
|
+ if (bal.accountIndex === accountIndex) {
|
|
|
+ return { mint: bal.mint, decimals: bal.uiTokenAmount.decimals }
|
|
|
}
|
|
|
}
|
|
|
+ return undefined
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Get token decimals from transaction's token balances.
|
|
|
+ */
|
|
|
+function getTokenDecimals(
|
|
|
+ tx: ParsedTransactionWithMeta,
|
|
|
+ mintAddress: string
|
|
|
+): number | undefined {
|
|
|
+ const balances = [
|
|
|
+ ...(tx.meta?.preTokenBalances || []),
|
|
|
+ ...(tx.meta?.postTokenBalances || []),
|
|
|
+ ]
|
|
|
+ for (const bal of balances) {
|
|
|
+ if (bal.mint === mintAddress) {
|
|
|
+ return bal.uiTokenAmount.decimals
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return undefined
|
|
|
+}
|
|
|
+
|
|
|
+type TokenInfo = { symbol: string; decimals: number; price: string }
|
|
|
+
|
|
|
+/**
|
|
|
+ * 通过 Jupiter Price API v3 获取价格,通过 Helius DAS API 获取 symbol
|
|
|
+ */
|
|
|
+async function fetchTokensInfo(
|
|
|
+ mintAddresses: string[],
|
|
|
+ rpcUrl: string,
|
|
|
+ apiKey?: string
|
|
|
+): Promise<Record<string, TokenInfo>> {
|
|
|
+ const result: Record<string, TokenInfo> = {}
|
|
|
+ const headers: Record<string, string> = { Accept: 'application/json' }
|
|
|
+ if (apiKey) headers['x-api-key'] = apiKey
|
|
|
+
|
|
|
+ // 并行: Jupiter Price API v3 (价格+decimals) + Helius DAS getAssetBatch (symbol)
|
|
|
+ const [priceRes, assetRes] = await Promise.allSettled([
|
|
|
+ fetch(`https://api.jup.ag/price/v3?ids=${mintAddresses.join(',')}`, { headers, signal: AbortSignal.timeout(8000) })
|
|
|
+ .then(r => r.ok ? r.json() as Promise<Record<string, { usdPrice?: number; decimals?: number }>> : null),
|
|
|
+ fetch(rpcUrl, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ signal: AbortSignal.timeout(8000),
|
|
|
+ body: JSON.stringify({
|
|
|
+ jsonrpc: '2.0', id: 1,
|
|
|
+ method: 'getAssetBatch',
|
|
|
+ params: { ids: mintAddresses }
|
|
|
+ })
|
|
|
+ }).then(r => r.ok ? r.json() as Promise<{
|
|
|
+ result?: Array<{ id: string; content?: { metadata?: { symbol?: string } } }>
|
|
|
+ }> : null)
|
|
|
+ ])
|
|
|
+
|
|
|
+ // 处理 Jupiter 价格
|
|
|
+ if (priceRes.status === 'fulfilled' && priceRes.value) {
|
|
|
+ for (const mint of mintAddresses) {
|
|
|
+ const info = priceRes.value[mint]
|
|
|
+ if (info?.usdPrice) {
|
|
|
+ result[mint] = { symbol: '', decimals: info.decimals ?? 0, price: String(info.usdPrice) }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理 Helius DAS symbol
|
|
|
+ if (assetRes.status === 'fulfilled' && assetRes.value?.result) {
|
|
|
+ for (const asset of assetRes.value.result) {
|
|
|
+ if (!asset?.id) continue
|
|
|
+ const symbol = asset.content?.metadata?.symbol || ''
|
|
|
+ if (!result[asset.id]) result[asset.id] = { symbol: '', decimals: 0, price: '' }
|
|
|
+ if (symbol) result[asset.id].symbol = symbol
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return result
|
|
|
}
|
|
|
|
|
|
export function listenOpenPosition(
|
|
|
@@ -86,124 +248,116 @@ export function listenOpenPosition(
|
|
|
const programKey = new PublicKey(opts.programId)
|
|
|
const logIncludes = opts.logIncludes ?? []
|
|
|
|
|
|
- // 通过 API 获取 position 详细信息
|
|
|
- async function fetchPositionDetails(
|
|
|
- positionAddress: string
|
|
|
- ): Promise<OpenPositionEvent['positionDetails'] | undefined> {
|
|
|
- try {
|
|
|
- const url = `https://api2.byreal.io/byreal/api/dex/v2/position/detail?address=${positionAddress}`
|
|
|
- const response = await fetch(url)
|
|
|
- if (!response.ok) {
|
|
|
- return undefined
|
|
|
- }
|
|
|
+ const cb: LogsCallback = (logs, ctx) => {
|
|
|
+ const lines = logs.logs ?? []
|
|
|
+ if (logIncludes.length && !logIncludes.every((k) => lines.some((l) => l.includes(k)))) return
|
|
|
|
|
|
- const data = (await response.json()) as PositionApiResponse
|
|
|
- if (data.retCode !== 0 || !data.result?.data) {
|
|
|
- return undefined
|
|
|
- }
|
|
|
- const positionData = data.result.data
|
|
|
- if (!positionData) {
|
|
|
- return undefined
|
|
|
- }
|
|
|
+ // 用 void + catch 包裹 async 逻辑,避免未捕获的 Promise rejection 静默吞掉错误
|
|
|
+ void (async () => {
|
|
|
|
|
|
- const pool = positionData.pool
|
|
|
- if (!pool) {
|
|
|
- return undefined
|
|
|
- }
|
|
|
+ // 使用 getParsedTransaction 获取解析后的交易,指令账户已解析为 PublicKey
|
|
|
+ const tx = await conn.getParsedTransaction(logs.signature, {
|
|
|
+ commitment: txFinality,
|
|
|
+ maxSupportedTransactionVersion: opts.maxSupportedTransactionVersion ?? 0
|
|
|
+ })
|
|
|
+ if (!tx?.meta || tx.meta.err) return
|
|
|
|
|
|
- return {
|
|
|
- positionAddress: positionData.positionAddress,
|
|
|
- providerAddress: positionData.providerAddress,
|
|
|
- nftMintAddress: positionData.nftMintAddress,
|
|
|
- poolAddress: pool.poolAddress,
|
|
|
- mintA: {
|
|
|
- address: pool.mintA.address,
|
|
|
- symbol: pool.mintA.symbol,
|
|
|
- decimals: pool.mintA.decimals,
|
|
|
- price: pool.mintA.price
|
|
|
- },
|
|
|
- mintB: {
|
|
|
- address: pool.mintB.address,
|
|
|
- symbol: pool.mintB.symbol,
|
|
|
- decimals: pool.mintB.decimals,
|
|
|
- price: pool.mintB.price
|
|
|
- },
|
|
|
- totalDeposit: positionData.totalDeposit,
|
|
|
- upperTick: positionData.upperTick,
|
|
|
- lowerTick: positionData.lowerTick
|
|
|
- }
|
|
|
- } catch {
|
|
|
- return undefined
|
|
|
- }
|
|
|
- }
|
|
|
+ // 通过 discriminator 精确匹配 open_position_with_token22_nft 指令
|
|
|
+ const byrealIx = findOpenPositionInstruction(tx, opts.programId)
|
|
|
+ if (!byrealIx) return
|
|
|
|
|
|
- const cb: LogsCallback = async (logs, ctx) => {
|
|
|
- const lines = logs.logs ?? []
|
|
|
- if (logIncludes.length && !logIncludes.every((k) => lines.some((l) => l.includes(k)))) return
|
|
|
+ // 从指令账户中按已知索引提取地址 (open_position_with_token22_nft)
|
|
|
+ // 索引来自 IDL 定义:
|
|
|
+ // 2: position_nft_mint (nftMint)
|
|
|
+ // 4: pool_state (poolId)
|
|
|
+ // 8: personal_position (positionAddress)
|
|
|
+ // 9: token_account_0 (用户的 tokenA 账户)
|
|
|
+ // 10: token_account_1 (用户的 tokenB 账户)
|
|
|
+ const ixAccounts = getInstructionAccounts(byrealIx)
|
|
|
+ const nftMintAddress = ixAccounts[2] || ''
|
|
|
+ const poolAddress = ixAccounts[4] || ''
|
|
|
+ const positionAddress = ixAccounts[8] || ''
|
|
|
|
|
|
- // 拉完整交易,方便你后续做“真正的字段解析”
|
|
|
- const tx = await conn.getTransaction(logs.signature, {
|
|
|
- commitment: txFinality,
|
|
|
- maxSupportedTransactionVersion: opts.maxSupportedTransactionVersion ?? 0
|
|
|
- })
|
|
|
+ // 从 token_account_0/1 的 token balance 中提取真正的 mint 地址
|
|
|
+ // 比直接用 ixAccounts[18]/[19] 更可靠,不受额外账户偏移影响
|
|
|
+ const tokenAccount0 = ixAccounts[9] || ''
|
|
|
+ const tokenAccount1 = ixAccounts[10] || ''
|
|
|
+ const mintAResult = findMintForTokenAccount(tx, tokenAccount0)
|
|
|
+ const mintBResult = findMintForTokenAccount(tx, tokenAccount1)
|
|
|
+ const mintAAddress = mintAResult?.mint || ixAccounts[18] || ''
|
|
|
+ const mintBAddress = mintBResult?.mint || ixAccounts[19] || ''
|
|
|
|
|
|
- const accounts = tx
|
|
|
- ? (() => {
|
|
|
- const message = tx.transaction.message as any
|
|
|
- const keys = message.staticAccountKeys ?? message.accountKeys
|
|
|
- return keys?.map((k: any) => k.toBase58())
|
|
|
- })()
|
|
|
- : undefined
|
|
|
-
|
|
|
- // 从 innerInstructions 中获取 positionAccount
|
|
|
- // 直接使用第一个 inner instruction 的第一个指令的 accounts[1]
|
|
|
- let positionAccount: string | undefined
|
|
|
- let positionAccountIndex: number | undefined
|
|
|
-
|
|
|
- if (tx?.meta?.innerInstructions?.[0]?.instructions?.[0]?.accounts?.[1] !== undefined && accounts) {
|
|
|
- const accountIndex = tx.meta.innerInstructions[0].instructions[0].accounts[1]
|
|
|
- if (typeof accountIndex === 'number' && accounts[accountIndex]) {
|
|
|
- positionAccount = accounts[accountIndex]
|
|
|
- positionAccountIndex = accountIndex
|
|
|
+ // 获取 provider (签名者) 地址
|
|
|
+ const accountKeys = tx.transaction.message.accountKeys
|
|
|
+ let providerAddress = ''
|
|
|
+ for (const key of accountKeys) {
|
|
|
+ if (typeof key !== 'string' && 'signer' in key && key.signer) {
|
|
|
+ providerAddress = key.pubkey.toBase58()
|
|
|
+ break
|
|
|
+ }
|
|
|
}
|
|
|
- }
|
|
|
|
|
|
- // 如果没找到,使用默认索引 8 作为后备
|
|
|
- if (!positionAccount && accounts && accounts.length > 7) {
|
|
|
- positionAccount = accounts[8]
|
|
|
- }
|
|
|
+ // 从程序日志中解码 CreatePersonalPositionEvent
|
|
|
+ const txLogs = tx.meta.logMessages || []
|
|
|
+ const createEvent = decodeCreatePositionEvent(txLogs)
|
|
|
|
|
|
- // 如果有 positionAccount,通过 API 获取详细信息
|
|
|
- // 延迟100秒后获取,因为positionAccount可能还没被写入
|
|
|
- await new Promise(resolve => setTimeout(resolve, 100 * 1000))
|
|
|
+ if (!positionAddress) {
|
|
|
+ console.log(`[${new Date().toISOString()}] 无法从交易中解析 positionAddress: ${logs.signature}`)
|
|
|
+ return
|
|
|
+ }
|
|
|
|
|
|
- let positionDetails = positionAccount ? await fetchPositionDetails(positionAccount) : undefined
|
|
|
- // 如果positionDetails为空,则分别尝试accounts[6]和accounts[7] accounts[9]
|
|
|
- if (!positionDetails && accounts && accounts.length > 6) {
|
|
|
- positionDetails = await fetchPositionDetails(accounts[6])
|
|
|
- }
|
|
|
- if (!positionDetails && accounts && accounts.length > 7) {
|
|
|
- positionDetails = await fetchPositionDetails(accounts[7])
|
|
|
- }
|
|
|
- if (!positionDetails && accounts && accounts.length > 9) {
|
|
|
- positionDetails = await fetchPositionDetails(accounts[9])
|
|
|
- }
|
|
|
- if (!positionDetails) {
|
|
|
- console.log('positionAccountIndex', positionAccountIndex)
|
|
|
- // console.log('找不到地址', JSON.stringify(tx, null, 2))
|
|
|
- console.log('找不到地址')
|
|
|
- console.log('accounts', accounts)
|
|
|
- console.log('tx', logs.signature)
|
|
|
+ // 通过 Jupiter Price API + Helius DAS API 获取代币信息
|
|
|
+ const tokensInfo = await fetchTokensInfo([mintAAddress, mintBAddress], conn.rpcEndpoint, opts.jupiterApiKey)
|
|
|
+ const mintAInfo = tokensInfo[mintAAddress]
|
|
|
+ const mintBInfo = tokensInfo[mintBAddress]
|
|
|
|
|
|
- }
|
|
|
+ // 优先使用交易 balance 中的 decimals,其次用 API 返回的
|
|
|
+ const mintADecimals = mintAResult?.decimals ?? mintAInfo?.decimals ?? getTokenDecimals(tx, mintAAddress) ?? 0
|
|
|
+ const mintBDecimals = mintBResult?.decimals ?? mintBInfo?.decimals ?? getTokenDecimals(tx, mintBAddress) ?? 0
|
|
|
+
|
|
|
+ // 通过事件中的 amount + 价格计算 totalDeposit (USD)
|
|
|
+ let totalDeposit = ''
|
|
|
+ if (createEvent && mintAInfo?.price && mintBInfo?.price) {
|
|
|
+ const amountA = Number(createEvent.amount0) / Math.pow(10, mintADecimals)
|
|
|
+ const amountB = Number(createEvent.amount1) / Math.pow(10, mintBDecimals)
|
|
|
+ const valueA = amountA * Number(mintAInfo.price)
|
|
|
+ const valueB = amountB * Number(mintBInfo.price)
|
|
|
+ totalDeposit = (valueA + valueB).toFixed(2)
|
|
|
+ }
|
|
|
|
|
|
- onEvent({
|
|
|
- signature: logs.signature,
|
|
|
- slot: ctx.slot,
|
|
|
- blockTime: tx?.blockTime ?? null,
|
|
|
- programId: opts.programId,
|
|
|
- positionAccount,
|
|
|
- positionDetails
|
|
|
+ console.log(`[${new Date().toISOString()}] 交易解析成功: sig=${logs.signature.slice(0, 8)}... position=${positionAddress.slice(0, 8)}... nftMint=${nftMintAddress.slice(0, 8)}... ${mintAInfo?.symbol || '?'}/${mintBInfo?.symbol || '?'} $${totalDeposit || '?'}`)
|
|
|
+
|
|
|
+ onEvent({
|
|
|
+ signature: logs.signature,
|
|
|
+ slot: ctx.slot,
|
|
|
+ blockTime: tx.blockTime ?? null,
|
|
|
+ programId: opts.programId,
|
|
|
+ positionAccount: positionAddress,
|
|
|
+ positionDetails: {
|
|
|
+ positionAddress,
|
|
|
+ providerAddress,
|
|
|
+ nftMintAddress,
|
|
|
+ poolAddress,
|
|
|
+ mintA: {
|
|
|
+ address: mintAAddress,
|
|
|
+ symbol: mintAInfo?.symbol || '',
|
|
|
+ decimals: mintADecimals,
|
|
|
+ price: mintAInfo?.price || '',
|
|
|
+ },
|
|
|
+ mintB: {
|
|
|
+ address: mintBAddress,
|
|
|
+ symbol: mintBInfo?.symbol || '',
|
|
|
+ decimals: mintBDecimals,
|
|
|
+ price: mintBInfo?.price || '',
|
|
|
+ },
|
|
|
+ totalDeposit,
|
|
|
+ upperTick: createEvent?.tickUpper ?? 0,
|
|
|
+ lowerTick: createEvent?.tickLower ?? 0,
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+ })().catch(err => {
|
|
|
+ console.error(`[${new Date().toISOString()}] openPositionListener 回调异常: ${logs.signature}`, err)
|
|
|
})
|
|
|
}
|
|
|
|
|
|
@@ -215,4 +369,3 @@ export function listenOpenPosition(
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
-
|