@w3lib/solana-batch-call 0.5.0 → 0.7.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 +47 -0
- package/README.md +129 -9
- package/dist/accountReader.d.ts +57 -0
- package/dist/accountReader.d.ts.map +1 -0
- package/dist/accountReader.js +149 -0
- package/dist/accountReader.js.map +1 -0
- package/dist/address.d.ts +9 -0
- package/dist/address.d.ts.map +1 -1
- package/dist/address.js +36 -6
- package/dist/address.js.map +1 -1
- package/dist/approvals.d.ts +5 -3
- package/dist/approvals.d.ts.map +1 -1
- package/dist/approvals.js +25 -36
- package/dist/approvals.js.map +1 -1
- package/dist/balances.d.ts +178 -0
- package/dist/balances.d.ts.map +1 -0
- package/dist/balances.js +524 -0
- package/dist/balances.js.map +1 -0
- package/dist/batchPlan.d.ts +52 -0
- package/dist/batchPlan.d.ts.map +1 -0
- package/dist/batchPlan.js +64 -0
- package/dist/batchPlan.js.map +1 -0
- package/dist/bytes.d.ts +31 -0
- package/dist/bytes.d.ts.map +1 -0
- package/dist/bytes.js +74 -0
- package/dist/bytes.js.map +1 -0
- package/dist/claim.d.ts +163 -0
- package/dist/claim.d.ts.map +1 -0
- package/dist/claim.js +197 -0
- package/dist/claim.js.map +1 -0
- package/dist/client.d.ts +26 -41
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +41 -986
- package/dist/client.js.map +1 -1
- package/dist/clientConfig.d.ts +11 -0
- package/dist/clientConfig.d.ts.map +1 -0
- package/dist/clientConfig.js +3 -0
- package/dist/clientConfig.js.map +1 -0
- package/dist/decode.d.ts +22 -0
- package/dist/decode.d.ts.map +1 -0
- package/dist/decode.js +160 -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.map +1 -1
- package/dist/fees.js +2 -12
- package/dist/fees.js.map +1 -1
- package/dist/history.d.ts +125 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +220 -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/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/layout.d.ts +21 -0
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +50 -69
- package/dist/layout.js.map +1 -1
- package/dist/liquidity/common.js +2 -2
- package/dist/liquidity/common.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 -12
- package/dist/lp.d.ts.map +1 -1
- package/dist/lp.js +158 -70
- 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/portfolio.d.ts.map +1 -1
- package/dist/portfolio.js +3 -8
- package/dist/portfolio.js.map +1 -1
- 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 +192 -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.js +2 -2
- package/dist/risk.js.map +1 -1
- package/dist/rpc.d.ts +30 -7
- package/dist/rpc.d.ts.map +1 -1
- package/dist/rpc.js +404 -111
- 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 +395 -0
- package/dist/simulate.js.map +1 -0
- package/dist/source.d.ts +8 -4
- package/dist/source.d.ts.map +1 -1
- package/dist/source.js +11 -4
- package/dist/source.js.map +1 -1
- package/dist/subpaths/approvals.d.ts +3 -3
- package/dist/subpaths/approvals.d.ts.map +1 -1
- package/dist/subpaths/approvals.js.map +1 -1
- 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/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/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/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/swap/index.js +3 -3
- package/dist/swap/index.js.map +1 -1
- package/dist/tokenDetails.d.ts +12 -0
- package/dist/tokenDetails.d.ts.map +1 -0
- package/dist/tokenDetails.js +196 -0
- package/dist/tokenDetails.js.map +1 -0
- package/dist/tokenMeta.d.ts +63 -0
- package/dist/tokenMeta.d.ts.map +1 -0
- package/dist/tokenMeta.js +226 -0
- package/dist/tokenMeta.js.map +1 -0
- package/dist/tradeCheck.js +3 -3
- package/dist/tradeCheck.js.map +1 -1
- package/dist/util.d.ts +22 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +107 -0
- package/dist/util.js.map +1 -0
- package/dist/write/chain.d.ts +51 -0
- package/dist/write/chain.d.ts.map +1 -0
- package/dist/write/chain.js +184 -0
- package/dist/write/chain.js.map +1 -0
- package/dist/write/collect.js +2 -2
- package/dist/write/collect.js.map +1 -1
- package/dist/write/errors.d.ts +10 -0
- package/dist/write/errors.d.ts.map +1 -0
- package/dist/write/errors.js +77 -0
- package/dist/write/errors.js.map +1 -0
- package/dist/write/instructions.d.ts +2 -0
- package/dist/write/instructions.d.ts.map +1 -1
- package/dist/write/instructions.js +16 -0
- package/dist/write/instructions.js.map +1 -1
- package/dist/write/pack.d.ts +28 -0
- package/dist/write/pack.d.ts.map +1 -0
- package/dist/write/pack.js +125 -0
- package/dist/write/pack.js.map +1 -0
- package/dist/write/send.d.ts +23 -9
- package/dist/write/send.d.ts.map +1 -1
- package/dist/write/send.js +53 -351
- package/dist/write/send.js.map +1 -1
- package/dist/write/tracker.d.ts +24 -0
- package/dist/write/tracker.d.ts.map +1 -0
- package/dist/write/tracker.js +129 -0
- package/dist/write/tracker.js.map +1 -0
- package/package.json +21 -1
- package/src/accountReader.ts +187 -0
- package/src/address.ts +41 -6
- package/src/approvals.ts +36 -44
- package/src/balances.ts +678 -0
- package/src/batchPlan.ts +117 -0
- package/src/bytes.ts +89 -0
- package/src/claim.ts +327 -0
- package/src/client.ts +59 -1239
- package/src/clientConfig.ts +12 -0
- package/src/decode.ts +176 -0
- package/src/describe.ts +77 -0
- package/src/fees.ts +2 -9
- package/src/history.ts +325 -0
- package/src/historyHelius.ts +136 -0
- package/src/index.ts +13 -1
- package/src/layout.ts +63 -82
- package/src/liquidity/common.ts +2 -2
- package/src/liquidity/orca.ts +15 -5
- package/src/liquidity/raydiumClmm.ts +13 -5
- package/src/lp.ts +226 -75
- package/src/lpMath.ts +38 -0
- package/src/portfolio.ts +8 -11
- package/src/programs.ts +216 -0
- package/src/reclaim.ts +550 -0
- package/src/risk.ts +2 -2
- package/src/rpc.ts +476 -118
- package/src/simulate.ts +517 -0
- package/src/source.ts +14 -5
- package/src/subpaths/approvals.ts +3 -2
- package/src/subpaths/claim.ts +54 -0
- package/src/subpaths/history.ts +38 -0
- package/src/subpaths/lp.ts +5 -1
- package/src/subpaths/reclaim.ts +58 -0
- package/src/subpaths/simulate.ts +37 -0
- package/src/swap/index.ts +3 -3
- package/src/tokenDetails.ts +240 -0
- package/src/tokenMeta.ts +270 -0
- package/src/tradeCheck.ts +3 -3
- package/src/util.ts +110 -0
- package/src/write/chain.ts +218 -0
- package/src/write/collect.ts +2 -2
- package/src/write/errors.ts +82 -0
- package/src/write/instructions.ts +17 -0
- package/src/write/pack.ts +142 -0
- package/src/write/send.ts +74 -407
- package/src/write/tracker.ts +152 -0
package/src/lp.ts
CHANGED
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
import { sha256 } from '@noble/hashes/sha2.js'
|
|
2
2
|
import { base64 } from '@scure/base'
|
|
3
3
|
|
|
4
|
-
import { decodeAddress,
|
|
4
|
+
import { decodeAddress, findProgramAddress, InvalidAddressError, isAddress } from './address.js'
|
|
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 { boundedSet, mapPool } from './util.js'
|
|
9
|
+
import { Bytes } from './bytes.js'
|
|
10
|
+
import {
|
|
11
|
+
getAmountsForLiquidity,
|
|
12
|
+
getFeeGrowthInside,
|
|
13
|
+
getFeesOwed,
|
|
14
|
+
getRewardGrowthInside,
|
|
15
|
+
orcaSqrtPriceAtTick,
|
|
16
|
+
raydiumSqrtPriceAtTick,
|
|
17
|
+
rewardGrowthGlobal,
|
|
18
|
+
sqrtPriceToPrice,
|
|
19
|
+
tickToPrice,
|
|
20
|
+
type RewardEmission,
|
|
21
|
+
type TickFeeData,
|
|
22
|
+
} from './lpMath.js'
|
|
9
23
|
import { defillamaPrices, type SolanaPriceSource } from './price.js'
|
|
10
24
|
|
|
11
25
|
export const RAYDIUM_CLMM_PROGRAM_ID = 'CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK'
|
|
@@ -97,6 +111,24 @@ export interface SolanaLpClmmPosition extends SolanaLpPositionBase {
|
|
|
97
111
|
fees1Formatted: string | null
|
|
98
112
|
/** 开启 prices 时:未领取手续费的美元价值(手续费为 null 或缺价格时为 null) */
|
|
99
113
|
feesUsd?: number | null
|
|
114
|
+
/** 开启 rewards 时:未领取的挖矿奖励(池子里已初始化的每个奖励一项,数量为 0 的也在) */
|
|
115
|
+
rewards?: SolanaLpReward[]
|
|
116
|
+
/** 开启 rewards 和 prices 时:未领取挖矿奖励的美元价值(有数量的奖励缺价格或读不到时为 null) */
|
|
117
|
+
rewardsUsd?: number | null
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** 集中流动性仓位未领取的挖矿奖励(Raydium CLMM reward_infos / Orca Whirlpool reward_infos) */
|
|
121
|
+
export interface SolanaLpReward {
|
|
122
|
+
/** 奖励在池子里的位置(0~2) */
|
|
123
|
+
index: number
|
|
124
|
+
mint: string
|
|
125
|
+
symbol: string | null
|
|
126
|
+
decimals: number | null
|
|
127
|
+
/** 未领取的数量(最小单位):已记账的 + 按池子最新状态结算的新增;读不到两端的 tick 数据时为 null */
|
|
128
|
+
amount: string | null
|
|
129
|
+
formatted: string | null
|
|
130
|
+
/** 开启 prices 时:美元价值(没有价格时为 null) */
|
|
131
|
+
usd?: number | null
|
|
100
132
|
}
|
|
101
133
|
|
|
102
134
|
export type SolanaLpPosition = SolanaLpAmmPosition | SolanaLpClmmPosition
|
|
@@ -116,6 +148,13 @@ export interface LpPositionsOptions extends RequestOptions {
|
|
|
116
148
|
pools?: readonly string[]
|
|
117
149
|
/** 返回已清空的仓位(流动性为 0、手续费也领完,NFT 还在)。默认 false */
|
|
118
150
|
includeClosed?: boolean
|
|
151
|
+
/**
|
|
152
|
+
* 集中流动性仓位同时算未领取的挖矿奖励(Raydium CLMM 农场奖励、Orca 的奖励 slot),放在 rewards 字段,照搬程序的结算:
|
|
153
|
+
* 池子的奖励增长按时间摊给当前流动性,再按仓位区间内的增长计算。默认 false。有奖励没领的仓位不算“已清空”
|
|
154
|
+
*/
|
|
155
|
+
rewards?: boolean
|
|
156
|
+
/** rewards 结算到哪个时刻(unix 秒),默认当前时间;链上用的是区块时间,差几秒影响很小 */
|
|
157
|
+
now?: number
|
|
119
158
|
/** 查美元价值:true 用 DefiLlama,也可以传自己的 SolanaPriceSource(如 /price 的 combinePrices([defillamaPrices(), onchainPrices()]))。默认不查 */
|
|
120
159
|
prices?: boolean | SolanaPriceSource
|
|
121
160
|
/**
|
|
@@ -131,35 +170,10 @@ export interface LpPositionsOptions extends RequestOptions {
|
|
|
131
170
|
// 账户布局(各程序的 Anchor / 原生账户,偏移量含 8 字节判别符)
|
|
132
171
|
// ---------------------------------------------------------------------------
|
|
133
172
|
|
|
134
|
-
|
|
135
|
-
export
|
|
136
|
-
readonly #view: DataView
|
|
137
|
-
constructor(readonly data: Uint8Array) {
|
|
138
|
-
this.#view = new DataView(data.buffer, data.byteOffset, data.byteLength)
|
|
139
|
-
}
|
|
140
|
-
u8(o: number): number {
|
|
141
|
-
return this.#view.getUint8(o)
|
|
142
|
-
}
|
|
143
|
-
u16(o: number): number {
|
|
144
|
-
return this.#view.getUint16(o, true)
|
|
145
|
-
}
|
|
146
|
-
u32(o: number): number {
|
|
147
|
-
return this.#view.getUint32(o, true)
|
|
148
|
-
}
|
|
149
|
-
i32(o: number): number {
|
|
150
|
-
return this.#view.getInt32(o, true)
|
|
151
|
-
}
|
|
152
|
-
u64(o: number): bigint {
|
|
153
|
-
return this.#view.getBigUint64(o, true)
|
|
154
|
-
}
|
|
155
|
-
u128(o: number): bigint {
|
|
156
|
-
return (this.#view.getBigUint64(o + 8, true) << 64n) | this.#view.getBigUint64(o, true)
|
|
157
|
-
}
|
|
158
|
-
key(o: number): string {
|
|
159
|
-
return encodeAddress(this.data.subarray(o, o + 32))
|
|
160
|
-
}
|
|
161
|
-
}
|
|
173
|
+
// 小端读取账户数据:公共的 Bytes(添加 / 移除流动性也从这里引用)
|
|
174
|
+
export { Bytes }
|
|
162
175
|
|
|
176
|
+
const ZERO_ADDRESS = '11111111111111111111111111111111'
|
|
163
177
|
const RAYDIUM_POSITION_SIZE = 281
|
|
164
178
|
const ORCA_POSITION_SIZE = 216
|
|
165
179
|
/** 池子账户至少要读到的长度(Raydium PoolState 实际 1544 字节,Orca Whirlpool 653 字节) */
|
|
@@ -172,7 +186,7 @@ const RAYDIUM_TICK_SIZE = 168
|
|
|
172
186
|
const ORCA_TICK_ARRAY_SIZE = 88
|
|
173
187
|
const ORCA_TICK_SIZE = 113
|
|
174
188
|
|
|
175
|
-
interface ClmmPositionState {
|
|
189
|
+
export interface ClmmPositionState {
|
|
176
190
|
protocol: 'raydium-clmm' | 'orca-whirlpool'
|
|
177
191
|
address: string
|
|
178
192
|
mint: string
|
|
@@ -184,8 +198,12 @@ interface ClmmPositionState {
|
|
|
184
198
|
growthLast1: bigint
|
|
185
199
|
owed0: bigint
|
|
186
200
|
owed1: bigint
|
|
201
|
+
/** 三个挖矿奖励:上次记录的区间内增长、已记账未领取的数量 */
|
|
202
|
+
rewards: Array<{ growthLast: bigint; owed: bigint }>
|
|
187
203
|
}
|
|
188
204
|
|
|
205
|
+
const positionRewards = (b: Bytes, offset: number) => [0, 1, 2].map((i) => ({ growthLast: b.u128(offset + i * 24), owed: b.u64(offset + i * 24 + 16) }))
|
|
206
|
+
|
|
189
207
|
function raydiumPosition(address: string, b: Bytes): ClmmPositionState {
|
|
190
208
|
return {
|
|
191
209
|
protocol: 'raydium-clmm',
|
|
@@ -199,6 +217,7 @@ function raydiumPosition(address: string, b: Bytes): ClmmPositionState {
|
|
|
199
217
|
growthLast1: b.u128(113),
|
|
200
218
|
owed0: b.u64(129),
|
|
201
219
|
owed1: b.u64(137),
|
|
220
|
+
rewards: positionRewards(b, 145),
|
|
202
221
|
}
|
|
203
222
|
}
|
|
204
223
|
|
|
@@ -215,7 +234,19 @@ function orcaPosition(address: string, b: Bytes): ClmmPositionState {
|
|
|
215
234
|
owed0: b.u64(112),
|
|
216
235
|
growthLast1: b.u128(120),
|
|
217
236
|
owed1: b.u64(136),
|
|
237
|
+
rewards: positionRewards(b, 144),
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** 按程序和账户大小解析仓位账户(Raydium PersonalPositionState 281 字节、Orca Position 216 字节);不是仓位账户时为 null */
|
|
242
|
+
export function parseClmmPosition(protocol: 'raydium-clmm' | 'orca-whirlpool', address: string, account: AccountInfo): ClmmPositionState | null {
|
|
243
|
+
if (protocol === 'raydium-clmm' && account.owner === RAYDIUM_CLMM_PROGRAM_ID && account.data.length === RAYDIUM_POSITION_SIZE) {
|
|
244
|
+
return raydiumPosition(address, new Bytes(account.data))
|
|
218
245
|
}
|
|
246
|
+
if (protocol === 'orca-whirlpool' && account.owner === ORCA_WHIRLPOOL_PROGRAM_ID && account.data.length === ORCA_POSITION_SIZE) {
|
|
247
|
+
return orcaPosition(address, new Bytes(account.data))
|
|
248
|
+
}
|
|
249
|
+
return null
|
|
219
250
|
}
|
|
220
251
|
|
|
221
252
|
interface ClmmPoolState {
|
|
@@ -231,6 +262,41 @@ interface ClmmPoolState {
|
|
|
231
262
|
/** Orca:池子里的费率;Raydium:在 AmmConfig 里,ammConfig 为它的地址 */
|
|
232
263
|
fee: number | null
|
|
233
264
|
ammConfig: string | null
|
|
265
|
+
/** 当前区间内的流动性 */
|
|
266
|
+
liquidity: bigint
|
|
267
|
+
/** 已初始化的挖矿奖励(index 是在池子里的位置) */
|
|
268
|
+
rewards: Array<{ index: number; mint: string; emission: RewardEmission }>
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const U64_MAX_VALUE = (1n << 64n) - 1n
|
|
272
|
+
|
|
273
|
+
/** 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 */
|
|
274
|
+
function raydiumRewards(b: Bytes): ClmmPoolState['rewards'] {
|
|
275
|
+
if (b.data.length < 397 + 3 * 169) {
|
|
276
|
+
return []
|
|
277
|
+
}
|
|
278
|
+
return [0, 1, 2].flatMap((index) => {
|
|
279
|
+
const o = 397 + index * 169
|
|
280
|
+
const mint = b.key(o + 57)
|
|
281
|
+
return mint === ZERO_ADDRESS
|
|
282
|
+
? []
|
|
283
|
+
: [{ 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) } }]
|
|
284
|
+
})
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** Orca reward_last_updated_timestamp(261)和 reward_infos(每个 128 字节,从 269 开始):mint +0、emissions_per_second_x64 +96、growth_global +112 */
|
|
288
|
+
function orcaRewards(b: Bytes): ClmmPoolState['rewards'] {
|
|
289
|
+
if (b.data.length < 269 + 3 * 128) {
|
|
290
|
+
return []
|
|
291
|
+
}
|
|
292
|
+
const lastUpdated = b.u64(261)
|
|
293
|
+
return [0, 1, 2].flatMap((index) => {
|
|
294
|
+
const o = 269 + index * 128
|
|
295
|
+
const mint = b.key(o)
|
|
296
|
+
return mint === ZERO_ADDRESS
|
|
297
|
+
? []
|
|
298
|
+
: [{ index, mint, emission: { initialized: true, openTime: 0n, endTime: U64_MAX_VALUE, lastUpdated, emissionsPerSecondX64: b.u128(o + 96), growthGlobalX64: b.u128(o + 112) } }]
|
|
299
|
+
})
|
|
234
300
|
}
|
|
235
301
|
|
|
236
302
|
function raydiumPool(b: Bytes): ClmmPoolState {
|
|
@@ -246,6 +312,8 @@ function raydiumPool(b: Bytes): ClmmPoolState {
|
|
|
246
312
|
global0: b.u128(277),
|
|
247
313
|
global1: b.u128(293),
|
|
248
314
|
fee: null,
|
|
315
|
+
liquidity: b.u128(237),
|
|
316
|
+
rewards: raydiumRewards(b),
|
|
249
317
|
}
|
|
250
318
|
}
|
|
251
319
|
|
|
@@ -262,6 +330,8 @@ function orcaPool(b: Bytes): ClmmPoolState {
|
|
|
262
330
|
global1: b.u128(245),
|
|
263
331
|
decimals0: null,
|
|
264
332
|
decimals1: null,
|
|
333
|
+
liquidity: b.u128(49),
|
|
334
|
+
rewards: orcaRewards(b),
|
|
265
335
|
}
|
|
266
336
|
}
|
|
267
337
|
|
|
@@ -346,7 +416,9 @@ export function orcaTickArray(pool: string, tick: number, spacing: number): { ad
|
|
|
346
416
|
return { address: findProgramAddress([utf8('tick_array'), decodeAddress(pool), utf8(String(start))], ORCA_WHIRLPOOL_PROGRAM_ID), start }
|
|
347
417
|
}
|
|
348
418
|
|
|
349
|
-
|
|
419
|
+
const rewardOutside = (b: Bytes, offset: number) => [b.u128(offset), b.u128(offset + 16), b.u128(offset + 32)]
|
|
420
|
+
|
|
421
|
+
export function raydiumTick(account: AccountInfo, start: number, tick: number, spacing: number): TickFeeData | null {
|
|
350
422
|
raydiumDiscriminator ??= discriminator('TickArrayState')
|
|
351
423
|
const offset = 44 + ((tick - start) / spacing) * RAYDIUM_TICK_SIZE
|
|
352
424
|
// 先认判别符、查长度再读字段(PDA 上可能是别的账户,如被转过 SOL 的系统账户)
|
|
@@ -358,7 +430,7 @@ function raydiumTick(account: AccountInfo, start: number, tick: number, spacing:
|
|
|
358
430
|
return null
|
|
359
431
|
}
|
|
360
432
|
// Raydium 不区分是否初始化:没用过的 tick 各项都是 0,按程序的公式算出来就是对的
|
|
361
|
-
return { initialized: true, feeGrowthOutside0: b.u128(offset + 36), feeGrowthOutside1: b.u128(offset + 52) }
|
|
433
|
+
return { initialized: true, feeGrowthOutside0: b.u128(offset + 36), feeGrowthOutside1: b.u128(offset + 52), rewardGrowthsOutside: rewardOutside(b, offset + 68) }
|
|
362
434
|
}
|
|
363
435
|
|
|
364
436
|
/** Anchor 账户判别符:sha256("account:<名称>") 的前 8 字节 */
|
|
@@ -367,7 +439,7 @@ let orcaDiscriminators: { fixed: Uint8Array; dynamic: Uint8Array } | undefined
|
|
|
367
439
|
let raydiumDiscriminator: Uint8Array | undefined
|
|
368
440
|
const startsWith = (data: Uint8Array, prefix: Uint8Array) => data.length >= prefix.length && prefix.every((byte, i) => data[i] === byte)
|
|
369
441
|
|
|
370
|
-
function orcaTick(account: AccountInfo, start: number, tick: number, spacing: number): TickFeeData | null {
|
|
442
|
+
export function orcaTick(account: AccountInfo, start: number, tick: number, spacing: number): TickFeeData | null {
|
|
371
443
|
orcaDiscriminators ??= { fixed: discriminator('TickArray'), dynamic: discriminator('DynamicTickArray') }
|
|
372
444
|
if (account.data.length < 60) {
|
|
373
445
|
return null // 两种格式的头部(判别符 + start + …)都放不下
|
|
@@ -380,7 +452,7 @@ function orcaTick(account: AccountInfo, start: number, tick: number, spacing: nu
|
|
|
380
452
|
if (b.i32(8) !== start || offset + ORCA_TICK_SIZE > account.data.length) {
|
|
381
453
|
return null
|
|
382
454
|
}
|
|
383
|
-
return { initialized: b.u8(offset) === 1, feeGrowthOutside0: b.u128(offset + 33), feeGrowthOutside1: b.u128(offset + 49) }
|
|
455
|
+
return { initialized: b.u8(offset) === 1, feeGrowthOutside0: b.u128(offset + 33), feeGrowthOutside1: b.u128(offset + 49), rewardGrowthsOutside: rewardOutside(b, offset + 65) }
|
|
384
456
|
}
|
|
385
457
|
if (startsWith(account.data, orcaDiscriminators.dynamic)) {
|
|
386
458
|
// 可变大小:start i32 | whirlpool | tick_bitmap u128 | ticks(未初始化 1 字节 0;初始化 1 字节 1 + 112 字节数据)
|
|
@@ -403,7 +475,7 @@ function orcaTick(account: AccountInfo, start: number, tick: number, spacing: nu
|
|
|
403
475
|
if (offset + 113 > account.data.length) {
|
|
404
476
|
return null
|
|
405
477
|
}
|
|
406
|
-
return { initialized: true, feeGrowthOutside0: b.u128(offset + 33), feeGrowthOutside1: b.u128(offset + 49) }
|
|
478
|
+
return { initialized: true, feeGrowthOutside0: b.u128(offset + 33), feeGrowthOutside1: b.u128(offset + 49), rewardGrowthsOutside: rewardOutside(b, offset + 65) }
|
|
407
479
|
}
|
|
408
480
|
return null
|
|
409
481
|
}
|
|
@@ -426,6 +498,8 @@ interface RawClmm {
|
|
|
426
498
|
amount1: bigint
|
|
427
499
|
fees0: bigint | null
|
|
428
500
|
fees1: bigint | null
|
|
501
|
+
/** rewards 选项开启时:每个已初始化的奖励的未领取数量(读不到 tick 时为 null) */
|
|
502
|
+
rewards?: Array<{ index: number; mint: string; amount: bigint | null }>
|
|
429
503
|
}
|
|
430
504
|
|
|
431
505
|
interface RawAmm {
|
|
@@ -446,7 +520,7 @@ interface RawAmm {
|
|
|
446
520
|
*/
|
|
447
521
|
export async function lpPositions(client: SolanaClient, owner: string, options: LpPositionsOptions = {}): Promise<SolanaLpPosition[]> {
|
|
448
522
|
if (!isAddress(owner)) {
|
|
449
|
-
throw new
|
|
523
|
+
throw new InvalidAddressError(`Invalid owner address: ${owner}`, owner)
|
|
450
524
|
}
|
|
451
525
|
const protocols = new Set<SolanaLpProtocol>(options.protocols ?? (Object.keys(SOLANA_LP_PROTOCOLS) as SolanaLpProtocol[]))
|
|
452
526
|
for (const p of protocols) {
|
|
@@ -456,7 +530,7 @@ export async function lpPositions(client: SolanaClient, owner: string, options:
|
|
|
456
530
|
}
|
|
457
531
|
for (const address of [...(options.mints ?? []), ...(options.pools ?? [])]) {
|
|
458
532
|
if (!isAddress(address)) {
|
|
459
|
-
throw new
|
|
533
|
+
throw new InvalidAddressError(`Invalid address: ${address}`, address)
|
|
460
534
|
}
|
|
461
535
|
}
|
|
462
536
|
const { signal } = options
|
|
@@ -498,11 +572,9 @@ export async function lpPositions(client: SolanaClient, owner: string, options:
|
|
|
498
572
|
if (!account) {
|
|
499
573
|
return
|
|
500
574
|
}
|
|
501
|
-
const
|
|
502
|
-
if (
|
|
503
|
-
positions.push(
|
|
504
|
-
} else if (c.protocol === 'orca-whirlpool' && account.owner === program && account.data.length === ORCA_POSITION_SIZE) {
|
|
505
|
-
positions.push(orcaPosition(c.address, new Bytes(account.data)))
|
|
575
|
+
const parsed = parseClmmPosition(c.protocol, c.address, account)
|
|
576
|
+
if (parsed) {
|
|
577
|
+
positions.push(parsed)
|
|
506
578
|
}
|
|
507
579
|
})
|
|
508
580
|
// LP mint 的权限是 AMM v4 / CPMM 的固定权限地址时才去找池子
|
|
@@ -525,13 +597,30 @@ export async function lpPositions(client: SolanaClient, owner: string, options:
|
|
|
525
597
|
}
|
|
526
598
|
})
|
|
527
599
|
|
|
528
|
-
// 3. 池子:集中流动性的池子账户;LP 代币按 LP mint
|
|
600
|
+
// 3. 池子:集中流动性的池子账户;LP 代币按 LP mint 找池子(之前找到过的直接读池子账户,和集中流动性的池子一起读)
|
|
529
601
|
const poolAddresses = [...new Set(positions.map((p) => p.pool))]
|
|
602
|
+
const cached = lookups.flatMap((l) => {
|
|
603
|
+
const address = ammPoolByLpMint.get(lpPoolKey(l.protocol, l.lpMint))
|
|
604
|
+
return address ? [{ ...l, address }] : []
|
|
605
|
+
})
|
|
606
|
+
const uncached = lookups.filter((l) => !ammPoolByLpMint.has(lpPoolKey(l.protocol, l.lpMint)))
|
|
530
607
|
const [poolAccounts, found] = await Promise.all([
|
|
531
|
-
client.accounts(poolAddresses, { signal }),
|
|
532
|
-
|
|
608
|
+
client.accounts([...poolAddresses, ...cached.map((c) => c.address)], { signal }),
|
|
609
|
+
findAmmPools(client, uncached, signal),
|
|
533
610
|
])
|
|
534
|
-
|
|
611
|
+
// 缓存的池子核对一下(地址不对、账户读不到时重新找)
|
|
612
|
+
const stale: typeof lookups = []
|
|
613
|
+
cached.forEach((c, i) => {
|
|
614
|
+
const account = poolAccounts[poolAddresses.length + i]
|
|
615
|
+
const pool = account ? parseAmmPool(account) : null
|
|
616
|
+
if (pool && pool.protocol === c.protocol && pool.lpMint === c.lpMint) {
|
|
617
|
+
poolByLp.set(pool.lpMint, pool)
|
|
618
|
+
} else {
|
|
619
|
+
ammPoolByLpMint.delete(lpPoolKey(c.protocol, c.lpMint))
|
|
620
|
+
stale.push({ protocol: c.protocol, lpMint: c.lpMint })
|
|
621
|
+
}
|
|
622
|
+
})
|
|
623
|
+
for (const pool of [...found, ...(stale.length ? await findAmmPools(client, stale, signal) : [])]) {
|
|
535
624
|
if (pool) {
|
|
536
625
|
poolByLp.set(pool.lpMint, pool)
|
|
537
626
|
}
|
|
@@ -605,10 +694,32 @@ export async function lpPositions(client: SolanaClient, owner: string, options:
|
|
|
605
694
|
fees1 = null
|
|
606
695
|
}
|
|
607
696
|
}
|
|
608
|
-
|
|
697
|
+
// 未领取的挖矿奖励:没有流动性时只有已记账的;有流动性时先把池子的奖励增长结算到 now,再按区间内的增长算新增
|
|
698
|
+
let rewards: RawClmm['rewards']
|
|
699
|
+
if (options.rewards) {
|
|
700
|
+
const now = BigInt(Math.floor(options.now ?? Date.now() / 1000))
|
|
701
|
+
const ticks = (() => {
|
|
702
|
+
if (!arrays) return null
|
|
703
|
+
const read = position.protocol === 'raydium-clmm' ? raydiumTick : orcaTick
|
|
704
|
+
const lowerAccount = byAddress.get(arrays.lower.address)
|
|
705
|
+
const upperAccount = byAddress.get(arrays.upper.address)
|
|
706
|
+
const lower = lowerAccount ? read(lowerAccount, arrays.lower.start, position.tickLower, pool.tickSpacing) : null
|
|
707
|
+
const upper = upperAccount ? read(upperAccount, arrays.upper.start, position.tickUpper, pool.tickSpacing) : null
|
|
708
|
+
return lower && upper ? { lower, upper } : null
|
|
709
|
+
})()
|
|
710
|
+
rewards = pool.rewards.map(({ index, mint, emission }) => {
|
|
711
|
+
const state = position.rewards[index] as { growthLast: bigint; owed: bigint }
|
|
712
|
+
if (!arrays) return { index, mint, amount: state.owed }
|
|
713
|
+
if (!ticks) return { index, mint, amount: null }
|
|
714
|
+
const global = rewardGrowthGlobal(pool.liquidity, emission, now)
|
|
715
|
+
const inside = getRewardGrowthInside(pool.tick, position.tickLower, ticks.lower, position.tickUpper, ticks.upper, index, global)
|
|
716
|
+
return { index, mint, amount: getFeesOwed(state.owed, position.liquidity, inside, state.growthLast) }
|
|
717
|
+
})
|
|
718
|
+
}
|
|
719
|
+
if (!options.includeClosed && position.liquidity === 0n && fees0 === 0n && fees1 === 0n && !rewards?.some((x) => x.amount !== 0n)) {
|
|
609
720
|
continue
|
|
610
721
|
}
|
|
611
|
-
raw.push({ type: 'clmm', position, pool: { ...pool, fee: pool.fee ?? (pool.ammConfig ? (feeRates.get(pool.ammConfig) ?? null) : null) }, amount0, amount1, fees0, fees1 })
|
|
722
|
+
raw.push({ type: 'clmm', position, pool: { ...pool, fee: pool.fee ?? (pool.ammConfig ? (feeRates.get(pool.ammConfig) ?? null) : null) }, amount0, amount1, fees0, fees1, rewards })
|
|
612
723
|
}
|
|
613
724
|
for (const pool of ammHeld) {
|
|
614
725
|
const balance = held.get(pool.lpMint)?.amount ?? 0n
|
|
@@ -633,6 +744,19 @@ export async function lpPositions(client: SolanaClient, owner: string, options:
|
|
|
633
744
|
return finalize(client, raw, options)
|
|
634
745
|
}
|
|
635
746
|
|
|
747
|
+
/**
|
|
748
|
+
* 方法不可用(见 methodUnavailable)时把错误换成说明怎么绕过的 Error(cause 为原错误,hint 拿到原错误的 message);
|
|
749
|
+
* 取消、限频、超时等原样抛出(保留错误类型)。/lp、/approvals、资产总值共用
|
|
750
|
+
*/
|
|
751
|
+
export function withMethodHint<T>(promise: Promise<T>, signal: AbortSignal | undefined, hint: (detail: string) => string): Promise<T> {
|
|
752
|
+
return promise.catch((err: unknown) => {
|
|
753
|
+
if (signal?.aborted || !methodUnavailable(err)) {
|
|
754
|
+
throw err
|
|
755
|
+
}
|
|
756
|
+
throw new Error(hint(err instanceof Error ? err.message : String(err)), { cause: err })
|
|
757
|
+
})
|
|
758
|
+
}
|
|
759
|
+
|
|
636
760
|
/**
|
|
637
761
|
* 节点不提供这个方法(方法不存在、被拦截、需要付费 / Token、403 等):这时报错里说明怎么绕过。
|
|
638
762
|
* 多节点时所有节点都是这个原因才算;限频、超时、网络不对等原样抛出(资产总值 portfolio 也用)
|
|
@@ -661,43 +785,63 @@ async function listHoldings(client: SolanaClient, owner: string, mints: readonly
|
|
|
661
785
|
const balances = list.length ? await client.balances(owner, list, { signal }) : []
|
|
662
786
|
return balances.filter((b) => b.success && !b.native && b.balance !== '0').map((b) => ({ mint: b.token, amount: BigInt(b.balance), decimals: b.decimals }))
|
|
663
787
|
}
|
|
664
|
-
const tokens = await
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
{ cause: err },
|
|
672
|
-
)
|
|
673
|
-
})
|
|
788
|
+
const tokens = await withMethodHint(
|
|
789
|
+
client.ownerTokens(owner, { signal, metadata: false, includeNative: false, includeNfts: true }),
|
|
790
|
+
signal,
|
|
791
|
+
(detail) =>
|
|
792
|
+
`Scanning the token accounts of ${owner} failed (${detail}). ` +
|
|
793
|
+
'Scanning needs getTokenAccountsByOwner (most free public nodes do not support it): pass the position NFT / LP mints in `mints`, or use a node that supports it',
|
|
794
|
+
)
|
|
674
795
|
return tokens.map((t) => ({ mint: t.token, amount: BigInt(t.balance), decimals: t.decimals }))
|
|
675
796
|
}
|
|
676
797
|
|
|
677
|
-
/**
|
|
798
|
+
/**
|
|
799
|
+
* LP mint → 池子地址(AMM v4 / CPMM):池子地址不会变,找到后永久缓存(条目数有上限,超出淘汰最早写入的),
|
|
800
|
+
* 之后不再 getProgramAccounts,用时读池子账户核对
|
|
801
|
+
*/
|
|
802
|
+
const ammPoolByLpMint = new Map<string, string>()
|
|
803
|
+
const MAX_CACHED_LP_POOLS = 10_000
|
|
804
|
+
/** 同时进行的 getProgramAccounts 数(每个都要节点扫描整个程序的账户,很重) */
|
|
805
|
+
const MAX_PARALLEL_POOL_LOOKUPS = 4
|
|
806
|
+
|
|
807
|
+
function lpPoolKey(protocol: 'raydium-amm' | 'raydium-cpmm', lpMint: string): string {
|
|
808
|
+
return `${protocol}:${lpMint}`
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/** 测试用:清空 LP mint → 池子地址的缓存 */
|
|
812
|
+
export function resetLpPoolCache(): void {
|
|
813
|
+
ammPoolByLpMint.clear()
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/** 按 LP mint 找池子,限制并发;结果与 lookups 一一对应 */
|
|
817
|
+
function findAmmPools(
|
|
818
|
+
client: SolanaClient,
|
|
819
|
+
lookups: ReadonlyArray<{ protocol: 'raydium-amm' | 'raydium-cpmm'; lpMint: string }>,
|
|
820
|
+
signal: AbortSignal | undefined,
|
|
821
|
+
): Promise<Array<AmmPoolState | null>> {
|
|
822
|
+
return mapPool(lookups, MAX_PARALLEL_POOL_LOOKUPS, (l) => findAmmPool(client, l.protocol, l.lpMint, signal))
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/** 按 LP mint 找 AMM v4 / CPMM 的池子(getProgramAccounts + memcmp),找不到时为 null;找到的池子地址写入缓存 */
|
|
678
826
|
async function findAmmPool(client: SolanaClient, protocol: 'raydium-amm' | 'raydium-cpmm', lpMint: string, signal: AbortSignal | undefined): Promise<AmmPoolState | null> {
|
|
679
827
|
const [programId, size, offset] = protocol === 'raydium-amm' ? [RAYDIUM_AMM_PROGRAM_ID, AMM_POOL_SIZE, 464] : [RAYDIUM_CPMM_PROGRAM_ID, CPMM_POOL_SIZE, 136]
|
|
680
|
-
|
|
681
|
-
|
|
828
|
+
// 免费公共节点大多不支持 getProgramAccounts:说明怎么绕过
|
|
829
|
+
const res = await withMethodHint(
|
|
830
|
+
client.request<{ pubkey: string; account: { data: [string, string]; owner: string } }[]>(
|
|
682
831
|
'getProgramAccounts',
|
|
683
832
|
[programId, { encoding: 'base64', commitment: client.commitment, filters: [{ dataSize: size }, { memcmp: { offset, bytes: lpMint } }] }],
|
|
684
833
|
{ signal },
|
|
685
|
-
)
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
throw new Error(
|
|
692
|
-
`Finding the ${SOLANA_LP_PROTOCOLS[protocol].name} pool of LP token ${lpMint} uses getProgramAccounts, which failed (${err instanceof Error ? err.message : String(err)}; most free public nodes do not support it). ` +
|
|
693
|
-
"Pass the pool address in `pools`, use a node that supports getProgramAccounts, or skip LP tokens with `protocols: ['raydium-clmm', 'orca-whirlpool']`",
|
|
694
|
-
{ cause: err },
|
|
695
|
-
)
|
|
696
|
-
})
|
|
834
|
+
),
|
|
835
|
+
signal,
|
|
836
|
+
(detail) =>
|
|
837
|
+
`Finding the ${SOLANA_LP_PROTOCOLS[protocol].name} pool of LP token ${lpMint} uses getProgramAccounts, which failed (${detail}; most free public nodes do not support it). ` +
|
|
838
|
+
"Pass the pool address in `pools`, use a node that supports getProgramAccounts, or skip LP tokens with `protocols: ['raydium-clmm', 'orca-whirlpool']`",
|
|
839
|
+
)
|
|
697
840
|
for (const item of res ?? []) {
|
|
698
841
|
const data = base64.decode(item.account.data[0])
|
|
699
842
|
const pool = parseAmmPool({ address: item.pubkey, owner: item.account.owner, data, lamports: 0n, executable: false })
|
|
700
843
|
if (pool?.lpMint === lpMint) {
|
|
844
|
+
boundedSet(ammPoolByLpMint, lpPoolKey(protocol, lpMint), pool.address, MAX_CACHED_LP_POOLS)
|
|
701
845
|
return pool
|
|
702
846
|
}
|
|
703
847
|
}
|
|
@@ -706,7 +850,7 @@ async function findAmmPool(client: SolanaClient, protocol: 'raydium-amm' | 'rayd
|
|
|
706
850
|
|
|
707
851
|
async function finalize(client: SolanaClient, raw: readonly (RawClmm | RawAmm)[], options: LpPositionsOptions): Promise<SolanaLpPosition[]> {
|
|
708
852
|
const { signal } = options
|
|
709
|
-
const mints = [...new Set(raw.flatMap((r) => [r.pool.mint0, r.pool.mint1]))]
|
|
853
|
+
const mints = [...new Set(raw.flatMap((r) => [r.pool.mint0, r.pool.mint1, ...(r.type === 'clmm' ? (r.rewards ?? []).map((x) => x.mint) : [])]))]
|
|
710
854
|
const priceSource = options.prices === true ? defillamaPrices() : options.prices || null
|
|
711
855
|
const fetchFn = options.fetch ?? ((...args: Parameters<typeof fetch>) => fetch(...args))
|
|
712
856
|
// 价格源出错(服务不可用、被拦截、同步抛错等)时照样返回仓位,只是不带价格;取消查询时照常 reject
|
|
@@ -784,6 +928,11 @@ async function finalize(client: SolanaClient, raw: readonly (RawClmm | RawAmm)[]
|
|
|
784
928
|
const d1 = token1.decimals
|
|
785
929
|
const fees0Formatted = r.fees0 === null ? null : fmt(r.fees0, token0)
|
|
786
930
|
const fees1Formatted = r.fees1 === null ? null : fmt(r.fees1, token1)
|
|
931
|
+
const rewards = r.rewards?.map((x): SolanaLpReward => {
|
|
932
|
+
const t = token(x.mint, null)
|
|
933
|
+
const formatted = x.amount === null ? null : fmt(x.amount, t)
|
|
934
|
+
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) } : {}) }
|
|
935
|
+
})
|
|
787
936
|
return {
|
|
788
937
|
...base,
|
|
789
938
|
type: 'clmm',
|
|
@@ -804,6 +953,8 @@ async function finalize(client: SolanaClient, raw: readonly (RawClmm | RawAmm)[]
|
|
|
804
953
|
fees0Formatted,
|
|
805
954
|
fees1Formatted,
|
|
806
955
|
...(priceSource ? { feesUsd: r.fees0 === null || r.fees1 === null ? null : sum(usd(token0, fees0Formatted), usd(token1, fees1Formatted), r.fees0, r.fees1) } : {}),
|
|
956
|
+
...(rewards ? { rewards } : {}),
|
|
957
|
+
...(rewards && priceSource ? { rewardsUsd: rewards.some((x) => x.usd === null || x.usd === undefined) ? null : rewards.reduce((acc, x) => acc + (x.usd ?? 0), 0) } : {}),
|
|
807
958
|
}
|
|
808
959
|
})
|
|
809
960
|
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 级别的整数转换丢精度后再平方
|
package/src/portfolio.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* 资产总值(子路径 `@w3lib/solana-batch-call/price`):代币余额 × 价格,可选加上 LP 仓位的价值
|
|
3
3
|
*/
|
|
4
4
|
import type { RequestOptions, SolanaClient } from './client.js'
|
|
5
|
-
import { lpPositions,
|
|
5
|
+
import { lpPositions, withMethodHint, type LpPositionsOptions, type SolanaLpPosition } from './lp.js'
|
|
6
6
|
import { combinePrices, defillamaPrices, onchainPrices, tokenPrices, type SolanaPriceSource } from './price.js'
|
|
7
7
|
|
|
8
8
|
export interface PortfolioOptions extends RequestOptions {
|
|
@@ -110,15 +110,12 @@ async function holdings(client: SolanaClient, owner: string, tokens: readonly st
|
|
|
110
110
|
const balances = tokens.length ? await client.balances(owner, tokens, { symbol: true, signal }) : []
|
|
111
111
|
return balances.filter((b) => b.success).map((b) => ({ token: b.token, native: b.native, symbol: b.symbol ?? null, balance: b.balance, decimals: b.decimals, formatted: b.formatted }))
|
|
112
112
|
}
|
|
113
|
-
const owned = await
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
{ cause: err },
|
|
121
|
-
)
|
|
122
|
-
})
|
|
113
|
+
const owned = await withMethodHint(
|
|
114
|
+
client.ownerTokens(owner, { signal }),
|
|
115
|
+
signal,
|
|
116
|
+
(detail) =>
|
|
117
|
+
`Listing the tokens of ${owner} failed (${detail}). ` +
|
|
118
|
+
'Listing needs getTokenAccountsByOwner (most free public nodes do not support it): pass the mints in `tokens`, or use a node that supports it',
|
|
119
|
+
)
|
|
123
120
|
return owned.map((t) => ({ token: t.token, native: t.native, symbol: t.symbol ?? null, balance: t.balance, decimals: t.decimals, formatted: t.formatted }))
|
|
124
121
|
}
|