@w3lib/solana-batch-call 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -0
- package/README.md +343 -3
- package/dist/approvals.d.ts +129 -0
- package/dist/approvals.d.ts.map +1 -0
- package/dist/approvals.js +221 -0
- package/dist/approvals.js.map +1 -0
- package/dist/batchPlan.d.ts +52 -0
- package/dist/batchPlan.d.ts.map +1 -0
- package/dist/batchPlan.js +85 -0
- package/dist/batchPlan.js.map +1 -0
- package/dist/claim.d.ts +161 -0
- package/dist/claim.d.ts.map +1 -0
- package/dist/claim.js +197 -0
- package/dist/claim.js.map +1 -0
- package/dist/decode.d.ts +22 -0
- package/dist/decode.d.ts.map +1 -0
- package/dist/decode.js +187 -0
- package/dist/decode.js.map +1 -0
- package/dist/describe.d.ts +26 -0
- package/dist/describe.d.ts.map +1 -0
- package/dist/describe.js +58 -0
- package/dist/describe.js.map +1 -0
- package/dist/fees.d.ts +89 -0
- package/dist/fees.d.ts.map +1 -0
- package/dist/fees.js +166 -0
- package/dist/fees.js.map +1 -0
- package/dist/history.d.ts +125 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +231 -0
- package/dist/history.js.map +1 -0
- package/dist/historyHelius.d.ts +52 -0
- package/dist/historyHelius.d.ts.map +1 -0
- package/dist/historyHelius.js +125 -0
- package/dist/historyHelius.js.map +1 -0
- package/dist/layout.d.ts +2 -0
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +2 -2
- package/dist/layout.js.map +1 -1
- package/dist/liquidity/orca.d.ts +6 -0
- package/dist/liquidity/orca.d.ts.map +1 -1
- package/dist/liquidity/orca.js +14 -3
- package/dist/liquidity/orca.js.map +1 -1
- package/dist/liquidity/raydiumClmm.d.ts +6 -0
- package/dist/liquidity/raydiumClmm.d.ts.map +1 -1
- package/dist/liquidity/raydiumClmm.js +12 -5
- package/dist/liquidity/raydiumClmm.js.map +1 -1
- package/dist/lp.d.ts +55 -15
- package/dist/lp.d.ts.map +1 -1
- package/dist/lp.js +98 -46
- package/dist/lp.js.map +1 -1
- package/dist/lpMath.d.ts +22 -0
- package/dist/lpMath.d.ts.map +1 -1
- package/dist/lpMath.js +21 -0
- package/dist/lpMath.js.map +1 -1
- package/dist/names.d.ts +68 -0
- package/dist/names.d.ts.map +1 -0
- package/dist/names.js +257 -0
- package/dist/names.js.map +1 -0
- package/dist/pools.d.ts +9 -0
- package/dist/pools.d.ts.map +1 -0
- package/dist/pools.js +45 -0
- package/dist/pools.js.map +1 -0
- package/dist/portfolio.d.ts +64 -0
- package/dist/portfolio.d.ts.map +1 -0
- package/dist/portfolio.js +57 -0
- package/dist/portfolio.js.map +1 -0
- package/dist/price.d.ts +90 -0
- package/dist/price.d.ts.map +1 -0
- package/dist/price.js +287 -0
- package/dist/price.js.map +1 -0
- package/dist/programs.d.ts +19 -0
- package/dist/programs.d.ts.map +1 -0
- package/dist/programs.js +175 -0
- package/dist/programs.js.map +1 -0
- package/dist/reclaim.d.ts +190 -0
- package/dist/reclaim.d.ts.map +1 -0
- package/dist/reclaim.js +399 -0
- package/dist/reclaim.js.map +1 -0
- package/dist/risk.d.ts +84 -0
- package/dist/risk.d.ts.map +1 -0
- package/dist/risk.js +211 -0
- package/dist/risk.js.map +1 -0
- package/dist/rpc.d.ts +2 -0
- package/dist/rpc.d.ts.map +1 -1
- package/dist/rpc.js +36 -10
- package/dist/rpc.js.map +1 -1
- package/dist/simulate.d.ts +106 -0
- package/dist/simulate.d.ts.map +1 -0
- package/dist/simulate.js +418 -0
- package/dist/simulate.js.map +1 -0
- package/dist/subpaths/approvals.d.ts +29 -0
- package/dist/subpaths/approvals.d.ts.map +1 -0
- package/dist/subpaths/approvals.js +30 -0
- package/dist/subpaths/approvals.js.map +1 -0
- package/dist/subpaths/claim.d.ts +31 -0
- package/dist/subpaths/claim.d.ts.map +1 -0
- package/dist/subpaths/claim.js +31 -0
- package/dist/subpaths/claim.js.map +1 -0
- package/dist/subpaths/fees.d.ts +23 -0
- package/dist/subpaths/fees.d.ts.map +1 -0
- package/dist/subpaths/fees.js +25 -0
- package/dist/subpaths/fees.js.map +1 -0
- package/dist/subpaths/history.d.ts +21 -0
- package/dist/subpaths/history.d.ts.map +1 -0
- package/dist/subpaths/history.js +22 -0
- package/dist/subpaths/history.js.map +1 -0
- package/dist/subpaths/lp.d.ts +2 -2
- package/dist/subpaths/lp.d.ts.map +1 -1
- package/dist/subpaths/lp.js +2 -2
- package/dist/subpaths/lp.js.map +1 -1
- package/dist/subpaths/names.d.ts +37 -0
- package/dist/subpaths/names.d.ts.map +1 -0
- package/dist/subpaths/names.js +27 -0
- package/dist/subpaths/names.js.map +1 -0
- package/dist/subpaths/price.d.ts +40 -0
- package/dist/subpaths/price.d.ts.map +1 -0
- package/dist/subpaths/price.js +43 -0
- package/dist/subpaths/price.js.map +1 -0
- package/dist/subpaths/reclaim.d.ts +34 -0
- package/dist/subpaths/reclaim.d.ts.map +1 -0
- package/dist/subpaths/reclaim.js +34 -0
- package/dist/subpaths/reclaim.js.map +1 -0
- package/dist/subpaths/risk.d.ts +28 -0
- package/dist/subpaths/risk.d.ts.map +1 -0
- package/dist/subpaths/risk.js +30 -0
- package/dist/subpaths/risk.js.map +1 -0
- package/dist/subpaths/simulate.d.ts +22 -0
- package/dist/subpaths/simulate.d.ts.map +1 -0
- package/dist/subpaths/simulate.js +23 -0
- package/dist/subpaths/simulate.js.map +1 -0
- package/dist/subpaths/swap.d.ts +22 -0
- package/dist/subpaths/swap.d.ts.map +1 -0
- package/dist/subpaths/swap.js +21 -0
- package/dist/subpaths/swap.js.map +1 -0
- package/dist/subpaths/write.d.ts +1 -1
- package/dist/subpaths/write.d.ts.map +1 -1
- package/dist/subpaths/write.js +1 -1
- package/dist/subpaths/write.js.map +1 -1
- package/dist/swap/amm.d.ts +20 -0
- package/dist/swap/amm.d.ts.map +1 -0
- package/dist/swap/amm.js +77 -0
- package/dist/swap/amm.js.map +1 -0
- package/dist/swap/clmmMath.d.ts +24 -0
- package/dist/swap/clmmMath.d.ts.map +1 -0
- package/dist/swap/clmmMath.js +82 -0
- package/dist/swap/clmmMath.js.map +1 -0
- package/dist/swap/common.d.ts +68 -0
- package/dist/swap/common.d.ts.map +1 -0
- package/dist/swap/common.js +65 -0
- package/dist/swap/common.js.map +1 -0
- package/dist/swap/cpmm.d.ts +28 -0
- package/dist/swap/cpmm.d.ts.map +1 -0
- package/dist/swap/cpmm.js +113 -0
- package/dist/swap/cpmm.js.map +1 -0
- package/dist/swap/index.d.ts +145 -0
- package/dist/swap/index.d.ts.map +1 -0
- package/dist/swap/index.js +349 -0
- package/dist/swap/index.js.map +1 -0
- package/dist/swap/orca.d.ts +50 -0
- package/dist/swap/orca.d.ts.map +1 -0
- package/dist/swap/orca.js +382 -0
- package/dist/swap/orca.js.map +1 -0
- package/dist/swap/raydiumClmm.d.ts +62 -0
- package/dist/swap/raydiumClmm.d.ts.map +1 -0
- package/dist/swap/raydiumClmm.js +447 -0
- package/dist/swap/raydiumClmm.js.map +1 -0
- package/dist/tradeCheck.d.ts +45 -0
- package/dist/tradeCheck.d.ts.map +1 -0
- package/dist/tradeCheck.js +131 -0
- package/dist/tradeCheck.js.map +1 -0
- package/dist/write/instructions.d.ts +9 -0
- package/dist/write/instructions.d.ts.map +1 -1
- package/dist/write/instructions.js +40 -1
- package/dist/write/instructions.js.map +1 -1
- package/dist/write/transaction.d.ts +23 -0
- package/dist/write/transaction.d.ts.map +1 -1
- package/dist/write/transaction.js +50 -0
- package/dist/write/transaction.js.map +1 -1
- package/package.json +66 -14
- package/src/approvals.ts +358 -0
- package/src/batchPlan.ts +137 -0
- package/src/claim.ts +323 -0
- package/src/decode.ts +202 -0
- package/src/describe.ts +77 -0
- package/src/fees.ts +259 -0
- package/src/history.ts +336 -0
- package/src/historyHelius.ts +136 -0
- package/src/layout.ts +2 -2
- package/src/liquidity/orca.ts +15 -5
- package/src/liquidity/raydiumClmm.ts +13 -5
- package/src/lp.ts +148 -59
- package/src/lpMath.ts +38 -0
- package/src/names.ts +329 -0
- package/src/pools.ts +43 -0
- package/src/portfolio.ts +124 -0
- package/src/price.ts +366 -0
- package/src/programs.ts +216 -0
- package/src/reclaim.ts +547 -0
- package/src/risk.ts +243 -0
- package/src/rpc.ts +39 -10
- package/src/simulate.ts +535 -0
- package/src/subpaths/approvals.ts +47 -0
- package/src/subpaths/claim.ts +53 -0
- package/src/subpaths/fees.ts +41 -0
- package/src/subpaths/history.ts +38 -0
- package/src/subpaths/lp.ts +5 -1
- package/src/subpaths/names.ts +45 -0
- package/src/subpaths/price.ts +62 -0
- package/src/subpaths/reclaim.ts +57 -0
- package/src/subpaths/risk.ts +36 -0
- package/src/subpaths/simulate.ts +37 -0
- package/src/subpaths/swap.ts +41 -0
- package/src/subpaths/write.ts +2 -0
- package/src/swap/amm.ts +88 -0
- package/src/swap/clmmMath.ts +96 -0
- package/src/swap/common.ts +113 -0
- package/src/swap/cpmm.ts +132 -0
- package/src/swap/index.ts +467 -0
- package/src/swap/orca.ts +397 -0
- package/src/swap/raydiumClmm.ts +464 -0
- package/src/tradeCheck.ts +166 -0
- package/src/write/instructions.ts +43 -1
- package/src/write/transaction.ts +62 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/** Helius 增强交易历史数据源(需要应用自己的 API key):把 `/v0/addresses/{address}/transactions` 的结果整理成和 rpcHistory 一样的 Activity */
|
|
2
|
+
import { NATIVE_MINT, SOL_DECIMALS } from './constants.js'
|
|
3
|
+
import { formatUnits } from './layout.js'
|
|
4
|
+
import type { Activity, ActivityChange, ActivityType, HistorySource } from './history.js'
|
|
5
|
+
|
|
6
|
+
export interface HeliusHistoryOptions {
|
|
7
|
+
apiKey: string
|
|
8
|
+
/** 默认 https://api.helius.xyz */
|
|
9
|
+
baseUrl?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface HeliusItem {
|
|
13
|
+
signature: string
|
|
14
|
+
timestamp?: number
|
|
15
|
+
slot?: number
|
|
16
|
+
fee?: number
|
|
17
|
+
feePayer?: string
|
|
18
|
+
type?: string
|
|
19
|
+
source?: string
|
|
20
|
+
description?: string
|
|
21
|
+
transactionError?: unknown
|
|
22
|
+
nativeTransfers?: Array<{ fromUserAccount: string; toUserAccount: string; amount: number }>
|
|
23
|
+
tokenTransfers?: Array<{ fromUserAccount: string | null; toUserAccount: string | null; tokenAmount: number; mint: string }>
|
|
24
|
+
accountData?: Array<{
|
|
25
|
+
account: string
|
|
26
|
+
nativeBalanceChange?: number
|
|
27
|
+
tokenBalanceChanges?: Array<{ userAccount: string; mint: string; rawTokenAmount: { tokenAmount: string; decimals: number } }>
|
|
28
|
+
}>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const short = (a: string) => `${a.slice(0, 4)}…${a.slice(-4)}`
|
|
32
|
+
|
|
33
|
+
/** Helius 的交易类型 → Activity 类型(认不出的按资产变化判断) */
|
|
34
|
+
function typeOf(item: HeliusItem, changes: readonly ActivityChange[]): ActivityType {
|
|
35
|
+
const t = item.type ?? ''
|
|
36
|
+
if (/SWAP/.test(t)) return 'swap'
|
|
37
|
+
if (t === 'TRANSFER') return 'transfer'
|
|
38
|
+
if (/LIQUIDITY|POSITION|POOL/.test(t)) return 'liquidity'
|
|
39
|
+
if (/MINT/.test(t)) return 'mint'
|
|
40
|
+
if (/APPROVE|REVOKE|DELEGATE/.test(t)) return 'approve'
|
|
41
|
+
const out = changes.filter((c) => c.delta.startsWith('-'))
|
|
42
|
+
if (out.length && changes.some((c) => !c.delta.startsWith('-') && out.some((o) => o.asset !== c.asset))) return 'swap'
|
|
43
|
+
return t && t !== 'UNKNOWN' ? 'contract' : 'other'
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function activityFromHelius(address: string, item: HeliusItem): Activity {
|
|
47
|
+
const me = item.accountData?.find((a) => a.account === address)
|
|
48
|
+
const isPayer = item.feePayer === address
|
|
49
|
+
const fee = BigInt(item.fee ?? 0)
|
|
50
|
+
let native = BigInt(me?.nativeBalanceChange ?? 0) + (isPayer ? fee : 0n)
|
|
51
|
+
const tokens = new Map<string, { delta: bigint; decimals: number }>()
|
|
52
|
+
for (const entry of item.accountData ?? []) {
|
|
53
|
+
for (const t of entry.tokenBalanceChanges ?? []) {
|
|
54
|
+
if (t.userAccount !== address) continue
|
|
55
|
+
const prev = tokens.get(t.mint)
|
|
56
|
+
tokens.set(t.mint, { delta: (prev?.delta ?? 0n) + BigInt(t.rawTokenAmount.tokenAmount), decimals: t.rawTokenAmount.decimals })
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const wsol = tokens.get(NATIVE_MINT)
|
|
60
|
+
if (wsol) {
|
|
61
|
+
native += wsol.delta
|
|
62
|
+
tokens.delete(NATIVE_MINT)
|
|
63
|
+
}
|
|
64
|
+
const failed = item.transactionError !== null && item.transactionError !== undefined
|
|
65
|
+
const changes: ActivityChange[] = []
|
|
66
|
+
if (!failed) {
|
|
67
|
+
if (native !== 0n) changes.push({ asset: 'native', symbol: 'SOL', decimals: SOL_DECIMALS, delta: native.toString(), formatted: formatUnits(native, SOL_DECIMALS) })
|
|
68
|
+
for (const [asset, { delta, decimals }] of tokens) {
|
|
69
|
+
if (delta !== 0n) changes.push({ asset, symbol: short(asset), decimals, delta: delta.toString(), formatted: formatUnits(delta, decimals) })
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const type = typeOf(item, changes)
|
|
73
|
+
let counterparty: string | undefined
|
|
74
|
+
for (const t of [...(item.nativeTransfers ?? []), ...(item.tokenTransfers ?? []).map((x) => ({ fromUserAccount: x.fromUserAccount ?? '', toUserAccount: x.toUserAccount ?? '' }))]) {
|
|
75
|
+
if (t.fromUserAccount === address && t.toUserAccount) counterparty = t.toUserAccount
|
|
76
|
+
else if (t.toUserAccount === address && t.fromUserAccount) counterparty = t.fromUserAccount
|
|
77
|
+
if (counterparty) break
|
|
78
|
+
}
|
|
79
|
+
const negative = changes.filter((c) => c.delta.startsWith('-')).length
|
|
80
|
+
const protocol = item.source && !/^(SYSTEM_PROGRAM|UNKNOWN)$/.test(item.source) ? item.source[0] + item.source.slice(1).toLowerCase().replace(/_/g, ' ') : undefined
|
|
81
|
+
return {
|
|
82
|
+
hash: item.signature,
|
|
83
|
+
time: item.timestamp ?? 0,
|
|
84
|
+
block: item.slot,
|
|
85
|
+
status: failed ? 'failed' : 'success',
|
|
86
|
+
type,
|
|
87
|
+
direction: (type === 'transfer' || type === 'other') && changes.length ? (negative === 0 ? 'in' : negative === changes.length ? 'out' : undefined) : undefined,
|
|
88
|
+
counterparty,
|
|
89
|
+
label: item.description || undefined,
|
|
90
|
+
changes,
|
|
91
|
+
...(isPayer ? { fee: fee.toString() } : {}),
|
|
92
|
+
protocol,
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Helius 增强交易历史(`GET /v0/addresses/{address}/transactions`,按 before 签名翻页)。apiKey 由应用传入,库里不带。
|
|
98
|
+
* 按交易类型过滤时翻页最多再取 3 页
|
|
99
|
+
*
|
|
100
|
+
* ```ts
|
|
101
|
+
* await getHistory(address, { source: [heliusHistory({ apiKey }), rpcHistory()] }) // Helius 失败时用节点
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
export function heliusHistory(options: HeliusHistoryOptions): HistorySource {
|
|
105
|
+
if (!options.apiKey) throw new Error('heliusHistory needs an apiKey')
|
|
106
|
+
const base = (options.baseUrl ?? 'https://api.helius.xyz').replace(/\/+$/, '')
|
|
107
|
+
return {
|
|
108
|
+
name: 'helius',
|
|
109
|
+
async getHistory(ctx) {
|
|
110
|
+
const items: Activity[] = []
|
|
111
|
+
let cursor = ctx.cursor
|
|
112
|
+
let more = true
|
|
113
|
+
for (let page = 0; page < (ctx.types ? 4 : 1) && items.length < ctx.limit && more; page++) {
|
|
114
|
+
const url = `${base}/v0/addresses/${ctx.address}/transactions?api-key=${encodeURIComponent(options.apiKey)}&limit=${Math.min(100, ctx.limit)}${cursor ? `&before=${encodeURIComponent(cursor)}` : ''}`
|
|
115
|
+
const res = await ctx.fetch(url, { signal: ctx.signal })
|
|
116
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
117
|
+
const raw = (await res.json()) as HeliusItem[]
|
|
118
|
+
if (!Array.isArray(raw)) throw new Error('Unexpected response')
|
|
119
|
+
more = raw.length >= Math.min(100, ctx.limit)
|
|
120
|
+
for (const item of raw) {
|
|
121
|
+
cursor = item.signature
|
|
122
|
+
const t = item.timestamp ?? 0
|
|
123
|
+
if (ctx.before !== undefined && t >= ctx.before) continue
|
|
124
|
+
if (ctx.after !== undefined && t < ctx.after) {
|
|
125
|
+
more = false
|
|
126
|
+
break
|
|
127
|
+
}
|
|
128
|
+
const activity = activityFromHelius(ctx.address, item)
|
|
129
|
+
if (!ctx.types || ctx.types.includes(activity.type)) items.push(activity)
|
|
130
|
+
if (items.length >= ctx.limit) break
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return { items, ...(more && cursor ? { nextCursor: cursor } : {}) }
|
|
134
|
+
},
|
|
135
|
+
}
|
|
136
|
+
}
|
package/src/layout.ts
CHANGED
|
@@ -134,8 +134,8 @@ export function parseMint(data: Uint8Array): MintInfo {
|
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
/** 在 Token-2022 账户的扩展(TLV:u16 类型 + u16 长度 + 数据)里找指定类型,返回其数据;没有或数据不完整时为 null */
|
|
138
|
-
function findExtension(data: Uint8Array, extensionType: number): Uint8Array | null {
|
|
137
|
+
/** 在 Token-2022 账户的扩展(TLV:u16 类型 + u16 长度 + 数据)里找指定类型,返回其数据;没有或数据不完整时为 null(包内使用,不从主入口导出) */
|
|
138
|
+
export function findExtension(data: Uint8Array, extensionType: number): Uint8Array | null {
|
|
139
139
|
if (data.length <= EXTENSIONS_OFFSET) {
|
|
140
140
|
return null
|
|
141
141
|
}
|
package/src/liquidity/orca.ts
CHANGED
|
@@ -223,6 +223,20 @@ export async function orcaAdd(ctx: Ctx, params: SolanaIncreaseLiquidityParams |
|
|
|
223
223
|
})
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
+
/**
|
|
227
|
+
* 关闭已清空的仓位(流动性、手续费、奖励都为 0):销毁仓位 NFT、关闭仓位账户和 NFT 的代币账户(nftAccount,默认 owner 的 ATA),租金退给 receiver。
|
|
228
|
+
* NFT 是 Token-2022(带元数据扩展)时连 mint 一起关闭,否则(Metaplex 时代的 SPL Token NFT)mint 留着
|
|
229
|
+
*/
|
|
230
|
+
export function orcaClosePosition(owner: string, receiver: string, positionMint: string, nftProgram: string, nftAccount = getAssociatedTokenAddress(owner, positionMint, nftProgram)): Instruction {
|
|
231
|
+
const position = positionAddress(positionMint)
|
|
232
|
+
const token2022 = nftProgram === TOKEN_2022_PROGRAM_ID
|
|
233
|
+
return {
|
|
234
|
+
programId: PROGRAM,
|
|
235
|
+
keys: [r(owner, true), w(receiver), w(position), w(positionMint), w(nftAccount), r(nftProgram)],
|
|
236
|
+
data: anchorIx(token2022 ? 'close_position_with_token_extensions' : 'close_position'),
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
226
240
|
/** 移除(decrease + 领取手续费和奖励)/ 只领取(collectOnly) */
|
|
227
241
|
export async function orcaRemove(ctx: Ctx, params: SolanaRemoveLiquidityParams, collectOnly = false): Promise<SolanaLiquidityPlan> {
|
|
228
242
|
const mint = refMint(params.position)
|
|
@@ -304,11 +318,7 @@ export async function orcaRemove(ctx: Ctx, params: SolanaRemoveLiquidityParams,
|
|
|
304
318
|
})
|
|
305
319
|
})
|
|
306
320
|
if (burn) {
|
|
307
|
-
main.push(
|
|
308
|
-
nftProgram === TOKEN_2022_PROGRAM_ID
|
|
309
|
-
? { programId: PROGRAM, keys: [r(ctx.owner, true), w(ctx.owner), w(positionAccount), w(mint), w(nftAccount), r(TOKEN_2022_PROGRAM_ID)], data: anchorIx('close_position_with_token_extensions') }
|
|
310
|
-
: { programId: PROGRAM, keys: [r(ctx.owner, true), w(ctx.owner), w(positionAccount), w(mint), w(nftAccount), r(TOKEN_PROGRAM_ID)], data: anchorIx('close_position') },
|
|
311
|
-
)
|
|
321
|
+
main.push(orcaClosePosition(ctx.owner, ctx.owner, mint, nftProgram))
|
|
312
322
|
}
|
|
313
323
|
return makePlan(ctx, {
|
|
314
324
|
action: collectOnly ? 'collect' : 'remove',
|
|
@@ -249,6 +249,18 @@ export async function raydiumAdd(ctx: Ctx, params: SolanaIncreaseLiquidityParams
|
|
|
249
249
|
})
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
+
/**
|
|
253
|
+
* 关闭已清空的仓位(流动性、手续费、奖励都为 0):销毁仓位 NFT、关闭 NFT 的代币账户和仓位账户,租金退给 owner(程序固定,不能指定别人)。
|
|
254
|
+
* pool 是仓位所在的池子(官方 SDK 也带着它)
|
|
255
|
+
*/
|
|
256
|
+
export function raydiumClosePosition(owner: string, positionMint: string, nftProgram: string, pool: string, nftAccount = getAssociatedTokenAddress(owner, positionMint, nftProgram)): Instruction {
|
|
257
|
+
return {
|
|
258
|
+
programId: PROGRAM,
|
|
259
|
+
keys: [w(owner, true), w(positionMint), w(nftAccount), w(personalPositionAddress(positionMint)), r(SYSTEM_PROGRAM_ID), r(nftProgram), w(pool)],
|
|
260
|
+
data: anchorIx('close_position'),
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
252
264
|
/** 移除(liquidity > 0)/ 只领取手续费和奖励(collectOnly:流动性为 0 的 decrease) */
|
|
253
265
|
export async function raydiumRemove(ctx: Ctx, params: SolanaRemoveLiquidityParams, collectOnly = false): Promise<SolanaLiquidityPlan> {
|
|
254
266
|
const mint = refMint(params.position)
|
|
@@ -308,11 +320,7 @@ export async function raydiumRemove(ctx: Ctx, params: SolanaRemoveLiquidityParam
|
|
|
308
320
|
}
|
|
309
321
|
const main: Instruction[] = [decrease]
|
|
310
322
|
if (burn) {
|
|
311
|
-
main.push(
|
|
312
|
-
programId: PROGRAM,
|
|
313
|
-
keys: [w(ctx.owner, true), w(mint), w(nftAta), w(personal), r(SYSTEM_PROGRAM_ID), r(nftProgram), w(pool.address)],
|
|
314
|
-
data: anchorIx('close_position'),
|
|
315
|
-
})
|
|
323
|
+
main.push(raydiumClosePosition(ctx.owner, mint, nftProgram, pool.address))
|
|
316
324
|
}
|
|
317
325
|
return makePlan(ctx, {
|
|
318
326
|
action: collectOnly ? 'collect' : 'remove',
|
package/src/lp.ts
CHANGED
|
@@ -5,7 +5,20 @@ import { decodeAddress, encodeAddress, findProgramAddress, isAddress } from './a
|
|
|
5
5
|
import type { RequestOptions, SolanaClient } from './client.js'
|
|
6
6
|
import { formatUnits, parseMint, parseTokenAccount, type AccountInfo } from './layout.js'
|
|
7
7
|
import { AllNodesFailedError, HttpError, RpcError } from './rpc.js'
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
getAmountsForLiquidity,
|
|
10
|
+
getFeeGrowthInside,
|
|
11
|
+
getFeesOwed,
|
|
12
|
+
getRewardGrowthInside,
|
|
13
|
+
orcaSqrtPriceAtTick,
|
|
14
|
+
raydiumSqrtPriceAtTick,
|
|
15
|
+
rewardGrowthGlobal,
|
|
16
|
+
sqrtPriceToPrice,
|
|
17
|
+
tickToPrice,
|
|
18
|
+
type RewardEmission,
|
|
19
|
+
type TickFeeData,
|
|
20
|
+
} from './lpMath.js'
|
|
21
|
+
import { defillamaPrices, type SolanaPriceSource } from './price.js'
|
|
9
22
|
|
|
10
23
|
export const RAYDIUM_CLMM_PROGRAM_ID = 'CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK'
|
|
11
24
|
export const ORCA_WHIRLPOOL_PROGRAM_ID = 'whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc'
|
|
@@ -25,45 +38,8 @@ export const SOLANA_LP_PROTOCOLS: Readonly<Record<SolanaLpProtocol, { name: stri
|
|
|
25
38
|
'raydium-cpmm': { name: 'Raydium CPMM', type: 'amm', programId: RAYDIUM_CPMM_PROGRAM_ID },
|
|
26
39
|
}
|
|
27
40
|
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
// ---------------------------------------------------------------------------
|
|
31
|
-
|
|
32
|
-
export interface SolanaPriceSource {
|
|
33
|
-
readonly name: string
|
|
34
|
-
/** 返回 mint → 美元价格;没有价格的不返回 */
|
|
35
|
-
prices(ctx: { mints: readonly string[]; fetch: typeof fetch; signal?: AbortSignal }): Promise<Map<string, number>>
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** DefiLlama 价格(免费免 Key,按 100 个一批查询);confidence 低于 minConfidence(默认 0.9)的价格不用 */
|
|
39
|
-
export function defillamaPrices(options: { minConfidence?: number } = {}): SolanaPriceSource {
|
|
40
|
-
const minConfidence = options.minConfidence ?? 0.9
|
|
41
|
-
return {
|
|
42
|
-
name: 'defillama',
|
|
43
|
-
async prices({ mints, fetch: fetchFn, signal }) {
|
|
44
|
-
const result = new Map<string, number>()
|
|
45
|
-
const batches: string[][] = []
|
|
46
|
-
for (let i = 0; i < mints.length; i += 100) {
|
|
47
|
-
batches.push(mints.slice(i, i + 100))
|
|
48
|
-
}
|
|
49
|
-
await Promise.all(
|
|
50
|
-
batches.map(async (batch) => {
|
|
51
|
-
const res = await fetchFn(`https://coins.llama.fi/prices/current/${batch.map((m) => `solana:${m}`).join(',')}`, { signal })
|
|
52
|
-
if (!res.ok) {
|
|
53
|
-
throw new Error(`DefiLlama prices: HTTP ${res.status}`)
|
|
54
|
-
}
|
|
55
|
-
const body = (await res.json()) as { coins?: Record<string, { price?: number; confidence?: number }> }
|
|
56
|
-
for (const [key, coin] of Object.entries(body.coins ?? {})) {
|
|
57
|
-
if (typeof coin.price === 'number' && Number.isFinite(coin.price) && (coin.confidence === undefined || coin.confidence >= minConfidence)) {
|
|
58
|
-
result.set(key.slice(key.indexOf(':') + 1), coin.price)
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}),
|
|
62
|
-
)
|
|
63
|
-
return result
|
|
64
|
-
},
|
|
65
|
-
}
|
|
66
|
-
}
|
|
41
|
+
// 价格源在 price.ts(子路径 /price);这里继续导出,旧的 import 不受影响
|
|
42
|
+
export { defillamaPrices, type SolanaPriceSource, type SolanaPriceSourceContext } from './price.js'
|
|
67
43
|
|
|
68
44
|
// ---------------------------------------------------------------------------
|
|
69
45
|
// 结果类型
|
|
@@ -133,6 +109,24 @@ export interface SolanaLpClmmPosition extends SolanaLpPositionBase {
|
|
|
133
109
|
fees1Formatted: string | null
|
|
134
110
|
/** 开启 prices 时:未领取手续费的美元价值(手续费为 null 或缺价格时为 null) */
|
|
135
111
|
feesUsd?: number | null
|
|
112
|
+
/** 开启 rewards 时:未领取的挖矿奖励(池子里已初始化的每个奖励一项,数量为 0 的也在) */
|
|
113
|
+
rewards?: SolanaLpReward[]
|
|
114
|
+
/** 开启 rewards 和 prices 时:未领取挖矿奖励的美元价值(有数量的奖励缺价格或读不到时为 null) */
|
|
115
|
+
rewardsUsd?: number | null
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** 集中流动性仓位未领取的挖矿奖励(Raydium CLMM reward_infos / Orca Whirlpool reward_infos) */
|
|
119
|
+
export interface SolanaLpReward {
|
|
120
|
+
/** 奖励在池子里的位置(0~2) */
|
|
121
|
+
index: number
|
|
122
|
+
mint: string
|
|
123
|
+
symbol: string | null
|
|
124
|
+
decimals: number | null
|
|
125
|
+
/** 未领取的数量(最小单位):已记账的 + 按池子最新状态结算的新增;读不到两端的 tick 数据时为 null */
|
|
126
|
+
amount: string | null
|
|
127
|
+
formatted: string | null
|
|
128
|
+
/** 开启 prices 时:美元价值(没有价格时为 null) */
|
|
129
|
+
usd?: number | null
|
|
136
130
|
}
|
|
137
131
|
|
|
138
132
|
export type SolanaLpPosition = SolanaLpAmmPosition | SolanaLpClmmPosition
|
|
@@ -152,7 +146,14 @@ export interface LpPositionsOptions extends RequestOptions {
|
|
|
152
146
|
pools?: readonly string[]
|
|
153
147
|
/** 返回已清空的仓位(流动性为 0、手续费也领完,NFT 还在)。默认 false */
|
|
154
148
|
includeClosed?: boolean
|
|
155
|
-
/**
|
|
149
|
+
/**
|
|
150
|
+
* 集中流动性仓位同时算未领取的挖矿奖励(Raydium CLMM 农场奖励、Orca 的奖励 slot),放在 rewards 字段,照搬程序的结算:
|
|
151
|
+
* 池子的奖励增长按时间摊给当前流动性,再按仓位区间内的增长计算。默认 false。有奖励没领的仓位不算“已清空”
|
|
152
|
+
*/
|
|
153
|
+
rewards?: boolean
|
|
154
|
+
/** rewards 结算到哪个时刻(unix 秒),默认当前时间;链上用的是区块时间,差几秒影响很小 */
|
|
155
|
+
now?: number
|
|
156
|
+
/** 查美元价值:true 用 DefiLlama,也可以传自己的 SolanaPriceSource(如 /price 的 combinePrices([defillamaPrices(), onchainPrices()]))。默认不查 */
|
|
156
157
|
prices?: boolean | SolanaPriceSource
|
|
157
158
|
/**
|
|
158
159
|
* 价格源出错时调用(服务不可用、被拦截等):这时照样返回仓位,只是 valueUsd / feesUsd 为 null,
|
|
@@ -196,6 +197,7 @@ export class Bytes {
|
|
|
196
197
|
}
|
|
197
198
|
}
|
|
198
199
|
|
|
200
|
+
const ZERO_ADDRESS = '11111111111111111111111111111111'
|
|
199
201
|
const RAYDIUM_POSITION_SIZE = 281
|
|
200
202
|
const ORCA_POSITION_SIZE = 216
|
|
201
203
|
/** 池子账户至少要读到的长度(Raydium PoolState 实际 1544 字节,Orca Whirlpool 653 字节) */
|
|
@@ -208,7 +210,7 @@ const RAYDIUM_TICK_SIZE = 168
|
|
|
208
210
|
const ORCA_TICK_ARRAY_SIZE = 88
|
|
209
211
|
const ORCA_TICK_SIZE = 113
|
|
210
212
|
|
|
211
|
-
interface ClmmPositionState {
|
|
213
|
+
export interface ClmmPositionState {
|
|
212
214
|
protocol: 'raydium-clmm' | 'orca-whirlpool'
|
|
213
215
|
address: string
|
|
214
216
|
mint: string
|
|
@@ -220,8 +222,12 @@ interface ClmmPositionState {
|
|
|
220
222
|
growthLast1: bigint
|
|
221
223
|
owed0: bigint
|
|
222
224
|
owed1: bigint
|
|
225
|
+
/** 三个挖矿奖励:上次记录的区间内增长、已记账未领取的数量 */
|
|
226
|
+
rewards: Array<{ growthLast: bigint; owed: bigint }>
|
|
223
227
|
}
|
|
224
228
|
|
|
229
|
+
const positionRewards = (b: Bytes, offset: number) => [0, 1, 2].map((i) => ({ growthLast: b.u128(offset + i * 24), owed: b.u64(offset + i * 24 + 16) }))
|
|
230
|
+
|
|
225
231
|
function raydiumPosition(address: string, b: Bytes): ClmmPositionState {
|
|
226
232
|
return {
|
|
227
233
|
protocol: 'raydium-clmm',
|
|
@@ -235,6 +241,7 @@ function raydiumPosition(address: string, b: Bytes): ClmmPositionState {
|
|
|
235
241
|
growthLast1: b.u128(113),
|
|
236
242
|
owed0: b.u64(129),
|
|
237
243
|
owed1: b.u64(137),
|
|
244
|
+
rewards: positionRewards(b, 145),
|
|
238
245
|
}
|
|
239
246
|
}
|
|
240
247
|
|
|
@@ -251,9 +258,21 @@ function orcaPosition(address: string, b: Bytes): ClmmPositionState {
|
|
|
251
258
|
owed0: b.u64(112),
|
|
252
259
|
growthLast1: b.u128(120),
|
|
253
260
|
owed1: b.u64(136),
|
|
261
|
+
rewards: positionRewards(b, 144),
|
|
254
262
|
}
|
|
255
263
|
}
|
|
256
264
|
|
|
265
|
+
/** 按程序和账户大小解析仓位账户(Raydium PersonalPositionState 281 字节、Orca Position 216 字节);不是仓位账户时为 null */
|
|
266
|
+
export function parseClmmPosition(protocol: 'raydium-clmm' | 'orca-whirlpool', address: string, account: AccountInfo): ClmmPositionState | null {
|
|
267
|
+
if (protocol === 'raydium-clmm' && account.owner === RAYDIUM_CLMM_PROGRAM_ID && account.data.length === RAYDIUM_POSITION_SIZE) {
|
|
268
|
+
return raydiumPosition(address, new Bytes(account.data))
|
|
269
|
+
}
|
|
270
|
+
if (protocol === 'orca-whirlpool' && account.owner === ORCA_WHIRLPOOL_PROGRAM_ID && account.data.length === ORCA_POSITION_SIZE) {
|
|
271
|
+
return orcaPosition(address, new Bytes(account.data))
|
|
272
|
+
}
|
|
273
|
+
return null
|
|
274
|
+
}
|
|
275
|
+
|
|
257
276
|
interface ClmmPoolState {
|
|
258
277
|
mint0: string
|
|
259
278
|
mint1: string
|
|
@@ -267,6 +286,41 @@ interface ClmmPoolState {
|
|
|
267
286
|
/** Orca:池子里的费率;Raydium:在 AmmConfig 里,ammConfig 为它的地址 */
|
|
268
287
|
fee: number | null
|
|
269
288
|
ammConfig: string | null
|
|
289
|
+
/** 当前区间内的流动性 */
|
|
290
|
+
liquidity: bigint
|
|
291
|
+
/** 已初始化的挖矿奖励(index 是在池子里的位置) */
|
|
292
|
+
rewards: Array<{ index: number; mint: string; emission: RewardEmission }>
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const U64_MAX_VALUE = (1n << 64n) - 1n
|
|
296
|
+
|
|
297
|
+
/** Raydium reward_infos(每个 169 字节,从 397 开始):open_time +1、end_time +9、last_update_time +17、emissions_per_second_x64 +25、mint +57、growth_global +153 */
|
|
298
|
+
function raydiumRewards(b: Bytes): ClmmPoolState['rewards'] {
|
|
299
|
+
if (b.data.length < 397 + 3 * 169) {
|
|
300
|
+
return []
|
|
301
|
+
}
|
|
302
|
+
return [0, 1, 2].flatMap((index) => {
|
|
303
|
+
const o = 397 + index * 169
|
|
304
|
+
const mint = b.key(o + 57)
|
|
305
|
+
return mint === ZERO_ADDRESS
|
|
306
|
+
? []
|
|
307
|
+
: [{ index, mint, emission: { initialized: true, openTime: b.u64(o + 1), endTime: b.u64(o + 9), lastUpdated: b.u64(o + 17), emissionsPerSecondX64: b.u128(o + 25), growthGlobalX64: b.u128(o + 153) } }]
|
|
308
|
+
})
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** Orca reward_last_updated_timestamp(261)和 reward_infos(每个 128 字节,从 269 开始):mint +0、emissions_per_second_x64 +96、growth_global +112 */
|
|
312
|
+
function orcaRewards(b: Bytes): ClmmPoolState['rewards'] {
|
|
313
|
+
if (b.data.length < 269 + 3 * 128) {
|
|
314
|
+
return []
|
|
315
|
+
}
|
|
316
|
+
const lastUpdated = b.u64(261)
|
|
317
|
+
return [0, 1, 2].flatMap((index) => {
|
|
318
|
+
const o = 269 + index * 128
|
|
319
|
+
const mint = b.key(o)
|
|
320
|
+
return mint === ZERO_ADDRESS
|
|
321
|
+
? []
|
|
322
|
+
: [{ index, mint, emission: { initialized: true, openTime: 0n, endTime: U64_MAX_VALUE, lastUpdated, emissionsPerSecondX64: b.u128(o + 96), growthGlobalX64: b.u128(o + 112) } }]
|
|
323
|
+
})
|
|
270
324
|
}
|
|
271
325
|
|
|
272
326
|
function raydiumPool(b: Bytes): ClmmPoolState {
|
|
@@ -282,6 +336,8 @@ function raydiumPool(b: Bytes): ClmmPoolState {
|
|
|
282
336
|
global0: b.u128(277),
|
|
283
337
|
global1: b.u128(293),
|
|
284
338
|
fee: null,
|
|
339
|
+
liquidity: b.u128(237),
|
|
340
|
+
rewards: raydiumRewards(b),
|
|
285
341
|
}
|
|
286
342
|
}
|
|
287
343
|
|
|
@@ -298,6 +354,8 @@ function orcaPool(b: Bytes): ClmmPoolState {
|
|
|
298
354
|
global1: b.u128(245),
|
|
299
355
|
decimals0: null,
|
|
300
356
|
decimals1: null,
|
|
357
|
+
liquidity: b.u128(49),
|
|
358
|
+
rewards: orcaRewards(b),
|
|
301
359
|
}
|
|
302
360
|
}
|
|
303
361
|
|
|
@@ -382,7 +440,9 @@ export function orcaTickArray(pool: string, tick: number, spacing: number): { ad
|
|
|
382
440
|
return { address: findProgramAddress([utf8('tick_array'), decodeAddress(pool), utf8(String(start))], ORCA_WHIRLPOOL_PROGRAM_ID), start }
|
|
383
441
|
}
|
|
384
442
|
|
|
385
|
-
|
|
443
|
+
const rewardOutside = (b: Bytes, offset: number) => [b.u128(offset), b.u128(offset + 16), b.u128(offset + 32)]
|
|
444
|
+
|
|
445
|
+
export function raydiumTick(account: AccountInfo, start: number, tick: number, spacing: number): TickFeeData | null {
|
|
386
446
|
raydiumDiscriminator ??= discriminator('TickArrayState')
|
|
387
447
|
const offset = 44 + ((tick - start) / spacing) * RAYDIUM_TICK_SIZE
|
|
388
448
|
// 先认判别符、查长度再读字段(PDA 上可能是别的账户,如被转过 SOL 的系统账户)
|
|
@@ -394,7 +454,7 @@ function raydiumTick(account: AccountInfo, start: number, tick: number, spacing:
|
|
|
394
454
|
return null
|
|
395
455
|
}
|
|
396
456
|
// Raydium 不区分是否初始化:没用过的 tick 各项都是 0,按程序的公式算出来就是对的
|
|
397
|
-
return { initialized: true, feeGrowthOutside0: b.u128(offset + 36), feeGrowthOutside1: b.u128(offset + 52) }
|
|
457
|
+
return { initialized: true, feeGrowthOutside0: b.u128(offset + 36), feeGrowthOutside1: b.u128(offset + 52), rewardGrowthsOutside: rewardOutside(b, offset + 68) }
|
|
398
458
|
}
|
|
399
459
|
|
|
400
460
|
/** Anchor 账户判别符:sha256("account:<名称>") 的前 8 字节 */
|
|
@@ -403,7 +463,7 @@ let orcaDiscriminators: { fixed: Uint8Array; dynamic: Uint8Array } | undefined
|
|
|
403
463
|
let raydiumDiscriminator: Uint8Array | undefined
|
|
404
464
|
const startsWith = (data: Uint8Array, prefix: Uint8Array) => data.length >= prefix.length && prefix.every((byte, i) => data[i] === byte)
|
|
405
465
|
|
|
406
|
-
function orcaTick(account: AccountInfo, start: number, tick: number, spacing: number): TickFeeData | null {
|
|
466
|
+
export function orcaTick(account: AccountInfo, start: number, tick: number, spacing: number): TickFeeData | null {
|
|
407
467
|
orcaDiscriminators ??= { fixed: discriminator('TickArray'), dynamic: discriminator('DynamicTickArray') }
|
|
408
468
|
if (account.data.length < 60) {
|
|
409
469
|
return null // 两种格式的头部(判别符 + start + …)都放不下
|
|
@@ -416,7 +476,7 @@ function orcaTick(account: AccountInfo, start: number, tick: number, spacing: nu
|
|
|
416
476
|
if (b.i32(8) !== start || offset + ORCA_TICK_SIZE > account.data.length) {
|
|
417
477
|
return null
|
|
418
478
|
}
|
|
419
|
-
return { initialized: b.u8(offset) === 1, feeGrowthOutside0: b.u128(offset + 33), feeGrowthOutside1: b.u128(offset + 49) }
|
|
479
|
+
return { initialized: b.u8(offset) === 1, feeGrowthOutside0: b.u128(offset + 33), feeGrowthOutside1: b.u128(offset + 49), rewardGrowthsOutside: rewardOutside(b, offset + 65) }
|
|
420
480
|
}
|
|
421
481
|
if (startsWith(account.data, orcaDiscriminators.dynamic)) {
|
|
422
482
|
// 可变大小:start i32 | whirlpool | tick_bitmap u128 | ticks(未初始化 1 字节 0;初始化 1 字节 1 + 112 字节数据)
|
|
@@ -439,7 +499,7 @@ function orcaTick(account: AccountInfo, start: number, tick: number, spacing: nu
|
|
|
439
499
|
if (offset + 113 > account.data.length) {
|
|
440
500
|
return null
|
|
441
501
|
}
|
|
442
|
-
return { initialized: true, feeGrowthOutside0: b.u128(offset + 33), feeGrowthOutside1: b.u128(offset + 49) }
|
|
502
|
+
return { initialized: true, feeGrowthOutside0: b.u128(offset + 33), feeGrowthOutside1: b.u128(offset + 49), rewardGrowthsOutside: rewardOutside(b, offset + 65) }
|
|
443
503
|
}
|
|
444
504
|
return null
|
|
445
505
|
}
|
|
@@ -462,6 +522,8 @@ interface RawClmm {
|
|
|
462
522
|
amount1: bigint
|
|
463
523
|
fees0: bigint | null
|
|
464
524
|
fees1: bigint | null
|
|
525
|
+
/** rewards 选项开启时:每个已初始化的奖励的未领取数量(读不到 tick 时为 null) */
|
|
526
|
+
rewards?: Array<{ index: number; mint: string; amount: bigint | null }>
|
|
465
527
|
}
|
|
466
528
|
|
|
467
529
|
interface RawAmm {
|
|
@@ -534,11 +596,9 @@ export async function lpPositions(client: SolanaClient, owner: string, options:
|
|
|
534
596
|
if (!account) {
|
|
535
597
|
return
|
|
536
598
|
}
|
|
537
|
-
const
|
|
538
|
-
if (
|
|
539
|
-
positions.push(
|
|
540
|
-
} else if (c.protocol === 'orca-whirlpool' && account.owner === program && account.data.length === ORCA_POSITION_SIZE) {
|
|
541
|
-
positions.push(orcaPosition(c.address, new Bytes(account.data)))
|
|
599
|
+
const parsed = parseClmmPosition(c.protocol, c.address, account)
|
|
600
|
+
if (parsed) {
|
|
601
|
+
positions.push(parsed)
|
|
542
602
|
}
|
|
543
603
|
})
|
|
544
604
|
// LP mint 的权限是 AMM v4 / CPMM 的固定权限地址时才去找池子
|
|
@@ -641,10 +701,32 @@ export async function lpPositions(client: SolanaClient, owner: string, options:
|
|
|
641
701
|
fees1 = null
|
|
642
702
|
}
|
|
643
703
|
}
|
|
644
|
-
|
|
704
|
+
// 未领取的挖矿奖励:没有流动性时只有已记账的;有流动性时先把池子的奖励增长结算到 now,再按区间内的增长算新增
|
|
705
|
+
let rewards: RawClmm['rewards']
|
|
706
|
+
if (options.rewards) {
|
|
707
|
+
const now = BigInt(Math.floor(options.now ?? Date.now() / 1000))
|
|
708
|
+
const ticks = (() => {
|
|
709
|
+
if (!arrays) return null
|
|
710
|
+
const read = position.protocol === 'raydium-clmm' ? raydiumTick : orcaTick
|
|
711
|
+
const lowerAccount = byAddress.get(arrays.lower.address)
|
|
712
|
+
const upperAccount = byAddress.get(arrays.upper.address)
|
|
713
|
+
const lower = lowerAccount ? read(lowerAccount, arrays.lower.start, position.tickLower, pool.tickSpacing) : null
|
|
714
|
+
const upper = upperAccount ? read(upperAccount, arrays.upper.start, position.tickUpper, pool.tickSpacing) : null
|
|
715
|
+
return lower && upper ? { lower, upper } : null
|
|
716
|
+
})()
|
|
717
|
+
rewards = pool.rewards.map(({ index, mint, emission }) => {
|
|
718
|
+
const state = position.rewards[index] as { growthLast: bigint; owed: bigint }
|
|
719
|
+
if (!arrays) return { index, mint, amount: state.owed }
|
|
720
|
+
if (!ticks) return { index, mint, amount: null }
|
|
721
|
+
const global = rewardGrowthGlobal(pool.liquidity, emission, now)
|
|
722
|
+
const inside = getRewardGrowthInside(pool.tick, position.tickLower, ticks.lower, position.tickUpper, ticks.upper, index, global)
|
|
723
|
+
return { index, mint, amount: getFeesOwed(state.owed, position.liquidity, inside, state.growthLast) }
|
|
724
|
+
})
|
|
725
|
+
}
|
|
726
|
+
if (!options.includeClosed && position.liquidity === 0n && fees0 === 0n && fees1 === 0n && !rewards?.some((x) => x.amount !== 0n)) {
|
|
645
727
|
continue
|
|
646
728
|
}
|
|
647
|
-
raw.push({ type: 'clmm', position, pool: { ...pool, fee: pool.fee ?? (pool.ammConfig ? (feeRates.get(pool.ammConfig) ?? null) : null) }, amount0, amount1, fees0, fees1 })
|
|
729
|
+
raw.push({ type: 'clmm', position, pool: { ...pool, fee: pool.fee ?? (pool.ammConfig ? (feeRates.get(pool.ammConfig) ?? null) : null) }, amount0, amount1, fees0, fees1, rewards })
|
|
648
730
|
}
|
|
649
731
|
for (const pool of ammHeld) {
|
|
650
732
|
const balance = held.get(pool.lpMint)?.amount ?? 0n
|
|
@@ -671,9 +753,9 @@ export async function lpPositions(client: SolanaClient, owner: string, options:
|
|
|
671
753
|
|
|
672
754
|
/**
|
|
673
755
|
* 节点不提供这个方法(方法不存在、被拦截、需要付费 / Token、403 等):这时报错里说明怎么绕过。
|
|
674
|
-
*
|
|
756
|
+
* 多节点时所有节点都是这个原因才算;限频、超时、网络不对等原样抛出(资产总值 portfolio 也用)
|
|
675
757
|
*/
|
|
676
|
-
function methodUnavailable(err: unknown): boolean {
|
|
758
|
+
export function methodUnavailable(err: unknown): boolean {
|
|
677
759
|
if (err instanceof AllNodesFailedError) {
|
|
678
760
|
return err.errors.length > 0 && err.errors.every((e) => methodUnavailable(e.error))
|
|
679
761
|
}
|
|
@@ -742,7 +824,7 @@ async function findAmmPool(client: SolanaClient, protocol: 'raydium-amm' | 'rayd
|
|
|
742
824
|
|
|
743
825
|
async function finalize(client: SolanaClient, raw: readonly (RawClmm | RawAmm)[], options: LpPositionsOptions): Promise<SolanaLpPosition[]> {
|
|
744
826
|
const { signal } = options
|
|
745
|
-
const mints = [...new Set(raw.flatMap((r) => [r.pool.mint0, r.pool.mint1]))]
|
|
827
|
+
const mints = [...new Set(raw.flatMap((r) => [r.pool.mint0, r.pool.mint1, ...(r.type === 'clmm' ? (r.rewards ?? []).map((x) => x.mint) : [])]))]
|
|
746
828
|
const priceSource = options.prices === true ? defillamaPrices() : options.prices || null
|
|
747
829
|
const fetchFn = options.fetch ?? ((...args: Parameters<typeof fetch>) => fetch(...args))
|
|
748
830
|
// 价格源出错(服务不可用、被拦截、同步抛错等)时照样返回仓位,只是不带价格;取消查询时照常 reject
|
|
@@ -751,8 +833,8 @@ async function finalize(client: SolanaClient, raw: readonly (RawClmm | RawAmm)[]
|
|
|
751
833
|
return new Map()
|
|
752
834
|
}
|
|
753
835
|
try {
|
|
754
|
-
// wSOL 的价格就是 SOL 的价格,DefiLlama 用 So111…112
|
|
755
|
-
return await priceSource.prices({ mints, fetch: fetchFn, signal })
|
|
836
|
+
// wSOL 的价格就是 SOL 的价格,DefiLlama 用 So111…112 查得到;client 给链上价格源读池子
|
|
837
|
+
return await priceSource.prices({ mints, fetch: fetchFn, signal, client })
|
|
756
838
|
} catch (err) {
|
|
757
839
|
if (signal?.aborted) {
|
|
758
840
|
throw err
|
|
@@ -820,6 +902,11 @@ async function finalize(client: SolanaClient, raw: readonly (RawClmm | RawAmm)[]
|
|
|
820
902
|
const d1 = token1.decimals
|
|
821
903
|
const fees0Formatted = r.fees0 === null ? null : fmt(r.fees0, token0)
|
|
822
904
|
const fees1Formatted = r.fees1 === null ? null : fmt(r.fees1, token1)
|
|
905
|
+
const rewards = r.rewards?.map((x): SolanaLpReward => {
|
|
906
|
+
const t = token(x.mint, null)
|
|
907
|
+
const formatted = x.amount === null ? null : fmt(x.amount, t)
|
|
908
|
+
return { index: x.index, mint: x.mint, symbol: t.symbol, decimals: t.decimals, amount: x.amount === null ? null : x.amount.toString(), formatted, ...(priceSource ? { usd: x.amount === 0n ? 0 : usd(t, formatted) } : {}) }
|
|
909
|
+
})
|
|
823
910
|
return {
|
|
824
911
|
...base,
|
|
825
912
|
type: 'clmm',
|
|
@@ -840,6 +927,8 @@ async function finalize(client: SolanaClient, raw: readonly (RawClmm | RawAmm)[]
|
|
|
840
927
|
fees0Formatted,
|
|
841
928
|
fees1Formatted,
|
|
842
929
|
...(priceSource ? { feesUsd: r.fees0 === null || r.fees1 === null ? null : sum(usd(token0, fees0Formatted), usd(token1, fees1Formatted), r.fees0, r.fees1) } : {}),
|
|
930
|
+
...(rewards ? { rewards } : {}),
|
|
931
|
+
...(rewards && priceSource ? { rewardsUsd: rewards.some((x) => x.usd === null || x.usd === undefined) ? null : rewards.reduce((acc, x) => acc + (x.usd ?? 0), 0) } : {}),
|
|
843
932
|
}
|
|
844
933
|
})
|
|
845
934
|
if (priceSource) {
|
package/src/lpMath.ts
CHANGED
|
@@ -153,6 +153,8 @@ export interface TickFeeData {
|
|
|
153
153
|
initialized: boolean
|
|
154
154
|
feeGrowthOutside0: bigint
|
|
155
155
|
feeGrowthOutside1: bigint
|
|
156
|
+
/** 三个挖矿奖励的 tick 外增长(读 tick 时带上;只算手续费时可以不给) */
|
|
157
|
+
rewardGrowthsOutside?: readonly bigint[]
|
|
156
158
|
}
|
|
157
159
|
|
|
158
160
|
const wrap = (x: bigint) => ((x % (1n << 128n)) + (1n << 128n)) % (1n << 128n)
|
|
@@ -192,6 +194,42 @@ export function getFeesOwed(owed: bigint, liquidity: bigint, growthInside: bigin
|
|
|
192
194
|
return owed + (delta > U64_MAX ? 0n : delta)
|
|
193
195
|
}
|
|
194
196
|
|
|
197
|
+
/** 池子里一个挖矿奖励的状态(两个程序的 reward_infos 里的同名字段) */
|
|
198
|
+
export interface RewardEmission {
|
|
199
|
+
/** 已初始化:奖励的 mint 不是默认地址 */
|
|
200
|
+
initialized: boolean
|
|
201
|
+
emissionsPerSecondX64: bigint
|
|
202
|
+
growthGlobalX64: bigint
|
|
203
|
+
/** 上次更新的时间(秒)。Raydium 每个奖励自己记;Orca 是池子的 reward_last_updated_timestamp(三个奖励相同) */
|
|
204
|
+
lastUpdated: bigint
|
|
205
|
+
/** 开始 / 结束时间(秒):Raydium 的 open_time / end_time;Orca 没有,传 0n / 2^64 − 1 */
|
|
206
|
+
openTime: bigint
|
|
207
|
+
endTime: bigint
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* 池子在 now 时刻的奖励全局增长:程序每次操作池子(交易、增减流动性、领取)前先把排放按时间摊给当前的流动性,
|
|
212
|
+
* Q64.64:growth += Δt · emissions_per_second / liquidity(向下取整,u128 回绕)。
|
|
213
|
+
* Raydium:now ≤ open_time 不动,Δt 到 end_time 为止;Orca:池子流动性为 0 或 Δt 为 0 不动
|
|
214
|
+
*/
|
|
215
|
+
export function rewardGrowthGlobal(liquidity: bigint, reward: RewardEmission, now: bigint): bigint {
|
|
216
|
+
if (!reward.initialized || liquidity === 0n || now <= reward.openTime) {
|
|
217
|
+
return reward.growthGlobalX64
|
|
218
|
+
}
|
|
219
|
+
const latest = now < reward.endTime ? now : reward.endTime
|
|
220
|
+
const delta = latest > reward.lastUpdated ? latest - reward.lastUpdated : 0n
|
|
221
|
+
return wrap(reward.growthGlobalX64 + (delta * reward.emissionsPerSecondX64) / liquidity)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** 区间内某个奖励的增长(u128 回绕),tick 未初始化的处理同 getFeeGrowthInside */
|
|
225
|
+
export function getRewardGrowthInside(tick: number, tickLower: number, lower: TickFeeData, tickUpper: number, upper: TickFeeData, index: number, global: bigint): bigint {
|
|
226
|
+
const lowerOutside = lower.rewardGrowthsOutside?.[index] ?? 0n
|
|
227
|
+
const upperOutside = upper.rewardGrowthsOutside?.[index] ?? 0n
|
|
228
|
+
const below = !lower.initialized ? global : tick < tickLower ? wrap(global - lowerOutside) : lowerOutside
|
|
229
|
+
const above = !upper.initialized ? 0n : tick < tickUpper ? upperOutside : wrap(global - upperOutside)
|
|
230
|
+
return wrap(global - below - above)
|
|
231
|
+
}
|
|
232
|
+
|
|
195
233
|
/** Q64.64 sqrt 价格 → token1 / token0 的价格(按精度换算,浮点数,只用于展示) */
|
|
196
234
|
export function sqrtPriceToPrice(sqrtPrice: bigint, decimals0: number, decimals1: number): number {
|
|
197
235
|
// 先取高位再转浮点,避免 2^128 级别的整数转换丢精度后再平方
|