@w3lib/solana-batch-call 0.3.0 → 0.5.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.
Files changed (198) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +304 -3
  3. package/dist/address.d.ts +2 -0
  4. package/dist/address.d.ts.map +1 -1
  5. package/dist/address.js +8 -0
  6. package/dist/address.js.map +1 -1
  7. package/dist/approvals.d.ts +129 -0
  8. package/dist/approvals.d.ts.map +1 -0
  9. package/dist/approvals.js +221 -0
  10. package/dist/approvals.js.map +1 -0
  11. package/dist/fees.d.ts +89 -0
  12. package/dist/fees.d.ts.map +1 -0
  13. package/dist/fees.js +166 -0
  14. package/dist/fees.js.map +1 -0
  15. package/dist/index.d.ts +1 -1
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +1 -1
  18. package/dist/index.js.map +1 -1
  19. package/dist/layout.d.ts +2 -0
  20. package/dist/layout.d.ts.map +1 -1
  21. package/dist/layout.js +2 -2
  22. package/dist/layout.js.map +1 -1
  23. package/dist/liquidity/amm.d.ts +7 -0
  24. package/dist/liquidity/amm.d.ts.map +1 -0
  25. package/dist/liquidity/amm.js +307 -0
  26. package/dist/liquidity/amm.js.map +1 -0
  27. package/dist/liquidity/clmm.d.ts +18 -0
  28. package/dist/liquidity/clmm.d.ts.map +1 -0
  29. package/dist/liquidity/clmm.js +85 -0
  30. package/dist/liquidity/clmm.js.map +1 -0
  31. package/dist/liquidity/common.d.ts +94 -0
  32. package/dist/liquidity/common.d.ts.map +1 -0
  33. package/dist/liquidity/common.js +228 -0
  34. package/dist/liquidity/common.js.map +1 -0
  35. package/dist/liquidity/index.d.ts +34 -0
  36. package/dist/liquidity/index.d.ts.map +1 -0
  37. package/dist/liquidity/index.js +157 -0
  38. package/dist/liquidity/index.js.map +1 -0
  39. package/dist/liquidity/orca.d.ts +6 -0
  40. package/dist/liquidity/orca.d.ts.map +1 -0
  41. package/dist/liquidity/orca.js +303 -0
  42. package/dist/liquidity/orca.js.map +1 -0
  43. package/dist/liquidity/raydiumClmm.d.ts +6 -0
  44. package/dist/liquidity/raydiumClmm.d.ts.map +1 -0
  45. package/dist/liquidity/raydiumClmm.js +307 -0
  46. package/dist/liquidity/raydiumClmm.js.map +1 -0
  47. package/dist/liquidity/types.d.ts +150 -0
  48. package/dist/liquidity/types.d.ts.map +1 -0
  49. package/dist/liquidity/types.js +2 -0
  50. package/dist/liquidity/types.js.map +1 -0
  51. package/dist/lp.d.ts +53 -15
  52. package/dist/lp.d.ts.map +1 -1
  53. package/dist/lp.js +15 -38
  54. package/dist/lp.js.map +1 -1
  55. package/dist/lpMath.d.ts +45 -0
  56. package/dist/lpMath.d.ts.map +1 -1
  57. package/dist/lpMath.js +189 -0
  58. package/dist/lpMath.js.map +1 -1
  59. package/dist/names.d.ts +68 -0
  60. package/dist/names.d.ts.map +1 -0
  61. package/dist/names.js +257 -0
  62. package/dist/names.js.map +1 -0
  63. package/dist/pools.d.ts +9 -0
  64. package/dist/pools.d.ts.map +1 -0
  65. package/dist/pools.js +45 -0
  66. package/dist/pools.js.map +1 -0
  67. package/dist/portfolio.d.ts +64 -0
  68. package/dist/portfolio.d.ts.map +1 -0
  69. package/dist/portfolio.js +57 -0
  70. package/dist/portfolio.js.map +1 -0
  71. package/dist/price.d.ts +90 -0
  72. package/dist/price.d.ts.map +1 -0
  73. package/dist/price.js +287 -0
  74. package/dist/price.js.map +1 -0
  75. package/dist/risk.d.ts +84 -0
  76. package/dist/risk.d.ts.map +1 -0
  77. package/dist/risk.js +211 -0
  78. package/dist/risk.js.map +1 -0
  79. package/dist/rpc.d.ts +2 -0
  80. package/dist/rpc.d.ts.map +1 -1
  81. package/dist/rpc.js +33 -9
  82. package/dist/rpc.js.map +1 -1
  83. package/dist/subpaths/approvals.d.ts +29 -0
  84. package/dist/subpaths/approvals.d.ts.map +1 -0
  85. package/dist/subpaths/approvals.js +30 -0
  86. package/dist/subpaths/approvals.js.map +1 -0
  87. package/dist/subpaths/fees.d.ts +23 -0
  88. package/dist/subpaths/fees.d.ts.map +1 -0
  89. package/dist/subpaths/fees.js +25 -0
  90. package/dist/subpaths/fees.js.map +1 -0
  91. package/dist/subpaths/liquidity.d.ts +23 -0
  92. package/dist/subpaths/liquidity.d.ts.map +1 -0
  93. package/dist/subpaths/liquidity.js +19 -0
  94. package/dist/subpaths/liquidity.js.map +1 -0
  95. package/dist/subpaths/lp.d.ts +1 -1
  96. package/dist/subpaths/lp.d.ts.map +1 -1
  97. package/dist/subpaths/lp.js +1 -1
  98. package/dist/subpaths/lp.js.map +1 -1
  99. package/dist/subpaths/names.d.ts +37 -0
  100. package/dist/subpaths/names.d.ts.map +1 -0
  101. package/dist/subpaths/names.js +27 -0
  102. package/dist/subpaths/names.js.map +1 -0
  103. package/dist/subpaths/price.d.ts +40 -0
  104. package/dist/subpaths/price.d.ts.map +1 -0
  105. package/dist/subpaths/price.js +43 -0
  106. package/dist/subpaths/price.js.map +1 -0
  107. package/dist/subpaths/risk.d.ts +28 -0
  108. package/dist/subpaths/risk.d.ts.map +1 -0
  109. package/dist/subpaths/risk.js +30 -0
  110. package/dist/subpaths/risk.js.map +1 -0
  111. package/dist/subpaths/swap.d.ts +22 -0
  112. package/dist/subpaths/swap.d.ts.map +1 -0
  113. package/dist/subpaths/swap.js +21 -0
  114. package/dist/subpaths/swap.js.map +1 -0
  115. package/dist/subpaths/write.d.ts +1 -1
  116. package/dist/subpaths/write.d.ts.map +1 -1
  117. package/dist/subpaths/write.js +1 -1
  118. package/dist/subpaths/write.js.map +1 -1
  119. package/dist/swap/amm.d.ts +20 -0
  120. package/dist/swap/amm.d.ts.map +1 -0
  121. package/dist/swap/amm.js +77 -0
  122. package/dist/swap/amm.js.map +1 -0
  123. package/dist/swap/clmmMath.d.ts +24 -0
  124. package/dist/swap/clmmMath.d.ts.map +1 -0
  125. package/dist/swap/clmmMath.js +82 -0
  126. package/dist/swap/clmmMath.js.map +1 -0
  127. package/dist/swap/common.d.ts +68 -0
  128. package/dist/swap/common.d.ts.map +1 -0
  129. package/dist/swap/common.js +65 -0
  130. package/dist/swap/common.js.map +1 -0
  131. package/dist/swap/cpmm.d.ts +28 -0
  132. package/dist/swap/cpmm.d.ts.map +1 -0
  133. package/dist/swap/cpmm.js +113 -0
  134. package/dist/swap/cpmm.js.map +1 -0
  135. package/dist/swap/index.d.ts +145 -0
  136. package/dist/swap/index.d.ts.map +1 -0
  137. package/dist/swap/index.js +349 -0
  138. package/dist/swap/index.js.map +1 -0
  139. package/dist/swap/orca.d.ts +50 -0
  140. package/dist/swap/orca.d.ts.map +1 -0
  141. package/dist/swap/orca.js +382 -0
  142. package/dist/swap/orca.js.map +1 -0
  143. package/dist/swap/raydiumClmm.d.ts +62 -0
  144. package/dist/swap/raydiumClmm.d.ts.map +1 -0
  145. package/dist/swap/raydiumClmm.js +447 -0
  146. package/dist/swap/raydiumClmm.js.map +1 -0
  147. package/dist/tradeCheck.d.ts +45 -0
  148. package/dist/tradeCheck.d.ts.map +1 -0
  149. package/dist/tradeCheck.js +131 -0
  150. package/dist/tradeCheck.js.map +1 -0
  151. package/dist/write/instructions.d.ts +7 -0
  152. package/dist/write/instructions.d.ts.map +1 -1
  153. package/dist/write/instructions.js +24 -1
  154. package/dist/write/instructions.js.map +1 -1
  155. package/dist/write/transaction.d.ts +23 -0
  156. package/dist/write/transaction.d.ts.map +1 -1
  157. package/dist/write/transaction.js +50 -0
  158. package/dist/write/transaction.js.map +1 -1
  159. package/package.json +41 -2
  160. package/src/address.ts +9 -0
  161. package/src/approvals.ts +358 -0
  162. package/src/fees.ts +259 -0
  163. package/src/index.ts +1 -1
  164. package/src/layout.ts +2 -2
  165. package/src/liquidity/amm.ts +353 -0
  166. package/src/liquidity/clmm.ts +108 -0
  167. package/src/liquidity/common.ts +285 -0
  168. package/src/liquidity/index.ts +177 -0
  169. package/src/liquidity/orca.ts +344 -0
  170. package/src/liquidity/raydiumClmm.ts +348 -0
  171. package/src/liquidity/types.ts +158 -0
  172. package/src/lp.ts +17 -52
  173. package/src/lpMath.ts +215 -0
  174. package/src/names.ts +329 -0
  175. package/src/pools.ts +43 -0
  176. package/src/portfolio.ts +124 -0
  177. package/src/price.ts +366 -0
  178. package/src/risk.ts +243 -0
  179. package/src/rpc.ts +36 -9
  180. package/src/subpaths/approvals.ts +47 -0
  181. package/src/subpaths/fees.ts +41 -0
  182. package/src/subpaths/liquidity.ts +45 -0
  183. package/src/subpaths/lp.ts +9 -0
  184. package/src/subpaths/names.ts +45 -0
  185. package/src/subpaths/price.ts +62 -0
  186. package/src/subpaths/risk.ts +36 -0
  187. package/src/subpaths/swap.ts +41 -0
  188. package/src/subpaths/write.ts +2 -0
  189. package/src/swap/amm.ts +88 -0
  190. package/src/swap/clmmMath.ts +96 -0
  191. package/src/swap/common.ts +113 -0
  192. package/src/swap/cpmm.ts +132 -0
  193. package/src/swap/index.ts +467 -0
  194. package/src/swap/orca.ts +397 -0
  195. package/src/swap/raydiumClmm.ts +464 -0
  196. package/src/tradeCheck.ts +166 -0
  197. package/src/write/instructions.ts +26 -1
  198. package/src/write/transaction.ts +62 -0
@@ -0,0 +1,158 @@
1
+ import type { SolanaLpPosition, SolanaLpProtocol, SolanaLpToken } from '../lp.js'
2
+ import type { SendOptions, WriteResult, WriteStatus } from '../write/send.js'
3
+ import type { Instruction } from '../write/transaction.js'
4
+
5
+ type Amount = bigint | number | string
6
+
7
+ /**
8
+ * 已有的仓位:直接传 getLpPositions 查到的 SolanaLpPosition,或者只传能定位仓位的字段:
9
+ * - Raydium CLMM / Orca Whirlpool:positionMint(仓位 NFT 的 mint)
10
+ * - Raydium AMM v4 / CPMM:pool(池子地址)
11
+ */
12
+ export interface SolanaPositionRef {
13
+ protocol: SolanaLpProtocol
14
+ positionMint?: string
15
+ pool?: string
16
+ }
17
+
18
+ /** 往已有仓位追加:两边数量给一边即可(另一边按当前价格算),也可以都给(按较少的一边) */
19
+ export interface SolanaIncreaseLiquidityParams {
20
+ position: SolanaPositionRef | SolanaLpPosition
21
+ /** token0 的数量:amount0(最小单位)和 amount0Formatted(按精度换算,如 '1.5')二选一 */
22
+ amount0?: Amount
23
+ amount0Formatted?: string
24
+ amount1?: Amount
25
+ amount1Formatted?: string
26
+ }
27
+
28
+ /**
29
+ * 新建仓位(Raydium CLMM / Orca:一个新的仓位 NFT;AMM v4 / CPMM:往池子里加流动性,拿到 LP 代币)。
30
+ * 池子用地址指定(Solana 上同一对代币有很多池子,按地址最明确);价格是“1 个 token0 值多少个 token1”(池子的方向)
31
+ */
32
+ export interface SolanaNewPositionParams {
33
+ protocol: SolanaLpProtocol
34
+ pool: string
35
+ /** 集中流动性:价格区间(token1 / token0,取最近的可用 tick);或直接给 tick;或 fullRange */
36
+ priceLower?: string | number
37
+ priceUpper?: string | number
38
+ tickLower?: number
39
+ tickUpper?: number
40
+ fullRange?: boolean
41
+ amount0?: Amount
42
+ amount0Formatted?: string
43
+ amount1?: Amount
44
+ amount1Formatted?: string
45
+ }
46
+
47
+ export type SolanaAddLiquidityParams = SolanaIncreaseLiquidityParams | SolanaNewPositionParams
48
+
49
+ export interface SolanaRemoveLiquidityParams {
50
+ position: SolanaPositionRef | SolanaLpPosition
51
+ /** 移除的比例(百分数,(0, 100]),默认 100 */
52
+ percent?: number
53
+ /** 或者直接给移除的数量:集中流动性是 L,AMM 是 LP 代币数量(最小单位) */
54
+ liquidity?: Amount
55
+ /** 集中流动性:全部移除后关闭仓位、销毁 NFT(退回租金),默认 false */
56
+ burn?: boolean
57
+ }
58
+
59
+ export interface SolanaCollectFeesParams {
60
+ /** 集中流动性的仓位(AMM 的手续费已经计入 LP 份额) */
61
+ position: SolanaPositionRef | SolanaLpPosition
62
+ }
63
+
64
+ export interface SolanaLiquidityOptions {
65
+ /** 价格滑点(比例,0.005 = 0.5%),默认 0.005 */
66
+ slippage?: number
67
+ /**
68
+ * Wrapped SOL 那一边用 SOL 支付 / 收取(自动包装 / 解包),默认 true。
69
+ * 用的是 wSOL 的关联代币账户:原来没有时创建、用完关闭(剩余的 wSOL 换回 SOL);原来就有时只补差额、不关闭
70
+ */
71
+ native?: boolean
72
+ /** 新建仓位的 NFT 归谁,默认 owner */
73
+ recipient?: string
74
+ /** 取消读取 */
75
+ signal?: AbortSignal
76
+ }
77
+
78
+ /** 一步:要原子执行的一组指令 */
79
+ export interface SolanaLiquidityStep {
80
+ /**
81
+ * - setup:创建代币账户、初始化 tick 数组等准备工作(与主指令能放进同一笔交易时会合并)
82
+ * - add / remove / collect:添加 / 移除流动性、领取手续费
83
+ */
84
+ kind: 'setup' | 'add' | 'remove' | 'collect'
85
+ instructions: Instruction[]
86
+ }
87
+
88
+ /** 添加 / 移除流动性的计划:预计的数量 + 按顺序执行的指令。只读取、不签名,可以当作报价 */
89
+ export interface SolanaLiquidityPlan {
90
+ action: 'add' | 'remove' | 'collect'
91
+ protocol: SolanaLpProtocol
92
+ protocolName: string
93
+ type: 'clmm' | 'amm'
94
+ pool: string
95
+ owner: string
96
+ token0: SolanaLpToken
97
+ token1: SolanaLpToken
98
+ /** 集中流动性:仓位 NFT 的 mint(新建时是新生成的,见 signers)和仓位账户 */
99
+ positionMint: string | null
100
+ position: string | null
101
+ /** AMM:LP 代币的 mint */
102
+ lpMint: string | null
103
+ /** 集中流动性:区间、池子当前的 tick 和价格(token1 / token0,按精度换算,只用于展示) */
104
+ tickLower: number | null
105
+ tickUpper: number | null
106
+ tick: number | null
107
+ price: number | null
108
+ priceLower: number | null
109
+ priceUpper: number | null
110
+ /** 添加 / 移除的流动性:集中流动性是 L;AMM 是 LP 代币数量 */
111
+ liquidity: string
112
+ /** 按当前价格预计存入 / 取出的数量(不含手续费) */
113
+ amount0: string
114
+ amount1: string
115
+ amount0Formatted: string | null
116
+ amount1Formatted: string | null
117
+ /** 添加:最多会用掉的数量(按滑点);移除:null */
118
+ amount0Max: string | null
119
+ amount1Max: string | null
120
+ /** 移除:至少会拿到的数量;添加:null */
121
+ amount0Min: string | null
122
+ amount1Min: string | null
123
+ /** 一并领取的未领取手续费(集中流动性移除 / 领取时);AMM 为 '0' */
124
+ fees0: string
125
+ fees1: string
126
+ fees0Formatted: string | null
127
+ fees1Formatted: string | null
128
+ /** 一并领取的挖矿奖励的代币(集中流动性移除 / 领取时;数量要等交易执行后才知道) */
129
+ rewardMints: string[]
130
+ /** 哪一边用 SOL 支付 / 收取(自动包装 / 解包 wSOL) */
131
+ native: 'token0' | 'token1' | null
132
+ /** 按顺序执行;setup 和主指令能放进同一笔交易时会合并 */
133
+ steps: SolanaLiquidityStep[]
134
+ /**
135
+ * 除 owner 外还需要签名的临时密钥(新建集中流动性仓位时的 NFT mint,32 字节种子)。
136
+ * 自己发送时用它签名主指令所在的交易(如 web3.js 的 Keypair.fromSeed(secretKey))
137
+ */
138
+ signers: Array<{ address: string; secretKey: Uint8Array }>
139
+ }
140
+
141
+ export interface SolanaLiquiditySendOptions extends SolanaLiquidityOptions, Omit<SendOptions, 'onUpdate' | 'signal'> {
142
+ /** 发送前检查余额够不够(添加时),默认 true */
143
+ checkBalance?: boolean
144
+ onUpdate?: (result: SolanaLiquidityStepResult) => void
145
+ }
146
+
147
+ export interface SolanaLiquidityStepResult extends WriteResult {
148
+ kind: SolanaLiquidityStep['kind']
149
+ }
150
+
151
+ export interface SolanaLiquidityResult {
152
+ plan: SolanaLiquidityPlan
153
+ /** 与 plan.steps 一一对应 */
154
+ results: SolanaLiquidityStepResult[]
155
+ /** 主指令(最后一步)的状态和签名 */
156
+ status: WriteStatus
157
+ signature: string | null
158
+ }
package/src/lp.ts CHANGED
@@ -6,15 +6,16 @@ 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
8
  import { getAmountsForLiquidity, getFeeGrowthInside, getFeesOwed, orcaSqrtPriceAtTick, raydiumSqrtPriceAtTick, sqrtPriceToPrice, tickToPrice, type TickFeeData } from './lpMath.js'
9
+ import { defillamaPrices, type SolanaPriceSource } from './price.js'
9
10
 
10
11
  export const RAYDIUM_CLMM_PROGRAM_ID = 'CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK'
11
12
  export const ORCA_WHIRLPOOL_PROGRAM_ID = 'whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc'
12
13
  export const RAYDIUM_AMM_PROGRAM_ID = '675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8'
13
14
  export const RAYDIUM_CPMM_PROGRAM_ID = 'CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C'
14
15
  /** AMM v4 的 LP mint 权限(固定地址) */
15
- const RAYDIUM_AMM_AUTHORITY = '5Q544fKrFoe6tsEbD7S8EmxGTJYAKtTVhAW5Q5pge4j1'
16
+ export const RAYDIUM_AMM_AUTHORITY = '5Q544fKrFoe6tsEbD7S8EmxGTJYAKtTVhAW5Q5pge4j1'
16
17
  /** CPMM 的 LP mint 权限:PDA ["vault_and_lp_mint_auth_seed"] */
17
- const RAYDIUM_CPMM_AUTHORITY = 'GpMZbSM2GgvTKHJirzeGfMFoaZ8UR2X7F4v8vHTvxFbL'
18
+ export const RAYDIUM_CPMM_AUTHORITY = 'GpMZbSM2GgvTKHJirzeGfMFoaZ8UR2X7F4v8vHTvxFbL'
18
19
 
19
20
  export type SolanaLpProtocol = 'raydium-clmm' | 'orca-whirlpool' | 'raydium-amm' | 'raydium-cpmm'
20
21
 
@@ -25,45 +26,8 @@ export const SOLANA_LP_PROTOCOLS: Readonly<Record<SolanaLpProtocol, { name: stri
25
26
  'raydium-cpmm': { name: 'Raydium CPMM', type: 'amm', programId: RAYDIUM_CPMM_PROGRAM_ID },
26
27
  }
27
28
 
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
- }
29
+ // 价格源在 price.ts(子路径 /price);这里继续导出,旧的 import 不受影响
30
+ export { defillamaPrices, type SolanaPriceSource, type SolanaPriceSourceContext } from './price.js'
67
31
 
68
32
  // ---------------------------------------------------------------------------
69
33
  // 结果类型
@@ -152,7 +116,7 @@ export interface LpPositionsOptions extends RequestOptions {
152
116
  pools?: readonly string[]
153
117
  /** 返回已清空的仓位(流动性为 0、手续费也领完,NFT 还在)。默认 false */
154
118
  includeClosed?: boolean
155
- /** 查美元价值:true 用 DefiLlama,也可以传自己的 SolanaPriceSource。默认不查 */
119
+ /** 查美元价值:true 用 DefiLlama,也可以传自己的 SolanaPriceSource(如 /price 的 combinePrices([defillamaPrices(), onchainPrices()]))。默认不查 */
156
120
  prices?: boolean | SolanaPriceSource
157
121
  /**
158
122
  * 价格源出错时调用(服务不可用、被拦截等):这时照样返回仓位,只是 valueUsd / feesUsd 为 null,
@@ -167,7 +131,8 @@ export interface LpPositionsOptions extends RequestOptions {
167
131
  // 账户布局(各程序的 Anchor / 原生账户,偏移量含 8 字节判别符)
168
132
  // ---------------------------------------------------------------------------
169
133
 
170
- class Bytes {
134
+ /** 小端读取账户数据(添加 / 移除流动性也用) */
135
+ export class Bytes {
171
136
  readonly #view: DataView
172
137
  constructor(readonly data: Uint8Array) {
173
138
  this.#view = new DataView(data.buffer, data.byteOffset, data.byteLength)
@@ -300,7 +265,7 @@ function orcaPool(b: Bytes): ClmmPoolState {
300
265
  }
301
266
  }
302
267
 
303
- interface AmmPoolState {
268
+ export interface AmmPoolState {
304
269
  protocol: 'raydium-amm' | 'raydium-cpmm'
305
270
  address: string
306
271
  lpMint: string
@@ -350,7 +315,7 @@ function cpmmPool(address: string, b: Bytes): AmmPoolState {
350
315
  }
351
316
  }
352
317
 
353
- function parseAmmPool(account: AccountInfo): AmmPoolState | null {
318
+ export function parseAmmPool(account: AccountInfo): AmmPoolState | null {
354
319
  const b = new Bytes(account.data)
355
320
  if (account.owner === RAYDIUM_AMM_PROGRAM_ID && account.data.length === AMM_POOL_SIZE) {
356
321
  return ammPool(account.address, b)
@@ -361,14 +326,14 @@ function parseAmmPool(account: AccountInfo): AmmPoolState | null {
361
326
  return null
362
327
  }
363
328
 
364
- const utf8 = (s: string) => new TextEncoder().encode(s)
329
+ export const utf8 = (s: string) => new TextEncoder().encode(s)
365
330
 
366
331
  function floorDiv(a: number, b: number): number {
367
332
  return Math.floor(a / b) * b
368
333
  }
369
334
 
370
335
  /** Raydium tick 数组:PDA ["tick_array", pool, start(i32 大端)],每个数组 60 个 tick */
371
- function raydiumTickArray(pool: string, tick: number, spacing: number): { address: string; start: number } {
336
+ export function raydiumTickArray(pool: string, tick: number, spacing: number): { address: string; start: number } {
372
337
  const start = floorDiv(tick, spacing * RAYDIUM_TICK_ARRAY_SIZE)
373
338
  const be = new Uint8Array(4)
374
339
  new DataView(be.buffer).setInt32(0, start, false)
@@ -376,7 +341,7 @@ function raydiumTickArray(pool: string, tick: number, spacing: number): { addres
376
341
  }
377
342
 
378
343
  /** Orca tick 数组:PDA ["tick_array", whirlpool, start(十进制字符串)],每个数组 88 个 tick */
379
- function orcaTickArray(pool: string, tick: number, spacing: number): { address: string; start: number } {
344
+ export function orcaTickArray(pool: string, tick: number, spacing: number): { address: string; start: number } {
380
345
  const start = floorDiv(tick, spacing * ORCA_TICK_ARRAY_SIZE)
381
346
  return { address: findProgramAddress([utf8('tick_array'), decodeAddress(pool), utf8(String(start))], ORCA_WHIRLPOOL_PROGRAM_ID), start }
382
347
  }
@@ -670,9 +635,9 @@ export async function lpPositions(client: SolanaClient, owner: string, options:
670
635
 
671
636
  /**
672
637
  * 节点不提供这个方法(方法不存在、被拦截、需要付费 / Token、403 等):这时报错里说明怎么绕过。
673
- * 多节点时所有节点都是这个原因才算;限频、超时、网络不对等原样抛出
638
+ * 多节点时所有节点都是这个原因才算;限频、超时、网络不对等原样抛出(资产总值 portfolio 也用)
674
639
  */
675
- function methodUnavailable(err: unknown): boolean {
640
+ export function methodUnavailable(err: unknown): boolean {
676
641
  if (err instanceof AllNodesFailedError) {
677
642
  return err.errors.length > 0 && err.errors.every((e) => methodUnavailable(e.error))
678
643
  }
@@ -750,8 +715,8 @@ async function finalize(client: SolanaClient, raw: readonly (RawClmm | RawAmm)[]
750
715
  return new Map()
751
716
  }
752
717
  try {
753
- // wSOL 的价格就是 SOL 的价格,DefiLlama 用 So111…112 查得到
754
- return await priceSource.prices({ mints, fetch: fetchFn, signal })
718
+ // wSOL 的价格就是 SOL 的价格,DefiLlama 用 So111…112 查得到;client 给链上价格源读池子
719
+ return await priceSource.prices({ mints, fetch: fetchFn, signal, client })
755
720
  } catch (err) {
756
721
  if (signal?.aborted) {
757
722
  throw err
package/src/lpMath.ts CHANGED
@@ -203,3 +203,218 @@ export function sqrtPriceToPrice(sqrtPrice: bigint, decimals0: number, decimals1
203
203
  export function tickToPrice(tick: number, decimals0: number, decimals1: number): number {
204
204
  return 1.0001 ** tick * 10 ** (decimals0 - decimals1)
205
205
  }
206
+
207
+ // ---------------------------------------------------------------------------
208
+ // 添加 / 移除流动性用到的计算(两个程序公式相同,tick ↔ 价格按各自的换算)
209
+ // ---------------------------------------------------------------------------
210
+
211
+ /** 两个程序允许的 sqrt 价格范围(Raydium:MIN / MAX_SQRT_PRICE_X64;Orca:MIN / MAX_SQRT_PRICE) */
212
+ export const RAYDIUM_MIN_SQRT_PRICE = 4295048016n
213
+ export const RAYDIUM_MAX_SQRT_PRICE = 79226673521066979257578248091n
214
+ export const ORCA_MIN_SQRT_PRICE = 4295048016n
215
+ export const ORCA_MAX_SQRT_PRICE = 79226673515401279992447579055n
216
+
217
+ export type ClmmProgram = 'raydium' | 'orca'
218
+
219
+ const sqrtAtTick = (program: ClmmProgram) => (program === 'raydium' ? raydiumSqrtPriceAtTick : orcaSqrtPriceAtTick)
220
+ const sqrtRange = (program: ClmmProgram): [bigint, bigint] => (program === 'raydium' ? [RAYDIUM_MIN_SQRT_PRICE, RAYDIUM_MAX_SQRT_PRICE] : [ORCA_MIN_SQRT_PRICE, ORCA_MAX_SQRT_PRICE])
221
+
222
+ /** tick → sqrt 价格(按程序) */
223
+ export function sqrtPriceAtTick(program: ClmmProgram, tick: number): bigint {
224
+ return sqrtAtTick(program)(tick)
225
+ }
226
+
227
+ /** sqrt 价格 → tick:满足 sqrtPriceAtTick(tick) <= sqrtPrice 的最大 tick(二分查找,与程序的 get_tick_at_sqrt_price 一致),限制在 [MIN_TICK, MAX_TICK] 内 */
228
+ export function tickAtSqrtPrice(program: ClmmProgram, sqrtPrice: bigint): number {
229
+ const at = sqrtAtTick(program)
230
+ if (sqrtPrice < at(MIN_TICK)) {
231
+ return MIN_TICK
232
+ }
233
+ let lo = MIN_TICK
234
+ let hi = MAX_TICK
235
+ while (lo < hi) {
236
+ const mid = Math.floor((lo + hi + 1) / 2)
237
+ if (at(mid) <= sqrtPrice) {
238
+ lo = mid
239
+ } else {
240
+ hi = mid - 1
241
+ }
242
+ }
243
+ return lo
244
+ }
245
+
246
+ /** 向上取整的整数除法 */
247
+ export const ceilDiv = (a: bigint, b: bigint) => (a + b - 1n) / b
248
+
249
+ /** 添加流动性 L 时程序收取的 token0(向上取整):ceil(L · 2^64 · (b − a) / (a · b)) */
250
+ function amount0Up(a: bigint, b: bigint, liquidity: bigint): bigint {
251
+ const [lo, hi] = a < b ? [a, b] : [b, a]
252
+ return ceilDiv((liquidity << 64n) * (hi - lo), lo * hi)
253
+ }
254
+
255
+ /** 添加流动性 L 时程序收取的 token1(向上取整):ceil(L · (b − a) / 2^64) */
256
+ function amount1Up(a: bigint, b: bigint, liquidity: bigint): bigint {
257
+ const [lo, hi] = a < b ? [a, b] : [b, a]
258
+ return ceilDiv(liquidity * (hi - lo), Q64)
259
+ }
260
+
261
+ /** 添加流动性 L 时程序收取的两种代币数量(向上取整;按当前 tick 判断区间,与程序一致) */
262
+ export function getMintAmounts(tick: number, sqrtPrice: bigint, sqrtLower: bigint, sqrtUpper: bigint, liquidity: bigint, tickLower: number, tickUpper: number): [bigint, bigint] {
263
+ if (liquidity === 0n) {
264
+ return [0n, 0n]
265
+ }
266
+ if (tick < tickLower) {
267
+ return [amount0Up(sqrtLower, sqrtUpper, liquidity), 0n]
268
+ }
269
+ if (tick < tickUpper) {
270
+ return [amount0Up(sqrtPrice, sqrtUpper, liquidity), amount1Up(sqrtLower, sqrtPrice, liquidity)]
271
+ }
272
+ return [0n, amount1Up(sqrtLower, sqrtUpper, liquidity)]
273
+ }
274
+
275
+ /** 区间 [a, b] 内 amount0 个 token0 对应的流动性(向下取整):amount0 · (a · b / 2^64) / (b − a) */
276
+ export function getLiquidityForAmount0(a: bigint, b: bigint, amount0: bigint): bigint {
277
+ const [lo, hi] = a < b ? [a, b] : [b, a]
278
+ return (amount0 * ((lo * hi) / Q64)) / (hi - lo)
279
+ }
280
+
281
+ /** 区间 [a, b] 内 amount1 个 token1 对应的流动性(向下取整):amount1 · 2^64 / (b − a) */
282
+ export function getLiquidityForAmount1(a: bigint, b: bigint, amount1: bigint): bigint {
283
+ const [lo, hi] = a < b ? [a, b] : [b, a]
284
+ return (amount1 * Q64) / (hi - lo)
285
+ }
286
+
287
+ /**
288
+ * 最多用 amount0 / amount1 时能添加的流动性:两边都给时取较小的;只给一边时只按这一边算(另一边要多少由 getMintAmounts 算)。
289
+ * 区间在当前价格之外时只需要一种代币,给的是另一种时返回 0
290
+ */
291
+ export function getLiquidityForAmounts(
292
+ tick: number,
293
+ sqrtPrice: bigint,
294
+ sqrtLower: bigint,
295
+ sqrtUpper: bigint,
296
+ amount0: bigint | undefined,
297
+ amount1: bigint | undefined,
298
+ tickLower: number,
299
+ tickUpper: number,
300
+ ): bigint {
301
+ let l0: bigint | null = null
302
+ let l1: bigint | null = null
303
+ // 价格正好在下限(tick == tickLower 且 sqrtPrice == sqrtLower)时也只需要 token0:按下方处理,避免除以 0
304
+ if (tick < tickLower || sqrtPrice <= sqrtLower) {
305
+ l0 = amount0 === undefined ? null : getLiquidityForAmount0(sqrtLower, sqrtUpper, amount0)
306
+ l1 = amount1 === undefined ? null : amount0 === undefined ? 0n : null
307
+ } else if (tick < tickUpper && sqrtPrice < sqrtUpper) {
308
+ // 价格正好在上限(向下穿过 tick 后 tick = tickUpper − 1、sqrtPrice = 上限)时只需要 token1:按上方处理
309
+ l0 = amount0 === undefined ? null : getLiquidityForAmount0(sqrtPrice, sqrtUpper, amount0)
310
+ l1 = amount1 === undefined ? null : getLiquidityForAmount1(sqrtLower, sqrtPrice, amount1)
311
+ } else {
312
+ l1 = amount1 === undefined ? null : getLiquidityForAmount1(sqrtLower, sqrtUpper, amount1)
313
+ l0 = amount0 === undefined ? null : amount1 === undefined ? 0n : null
314
+ }
315
+ if (l0 === null) return l1 ?? 0n
316
+ if (l1 === null) return l0
317
+ return l0 < l1 ? l0 : l1
318
+ }
319
+
320
+ /** 整数平方根(向下取整,牛顿迭代从不小于平方根的值开始) */
321
+ export function sqrtBigInt(value: bigint): bigint {
322
+ if (value < 0n) {
323
+ throw new RangeError('square root of a negative number')
324
+ }
325
+ if (value < 2n) {
326
+ return value
327
+ }
328
+ let x = 1n << BigInt((value.toString(2).length + 1) >> 1)
329
+ for (;;) {
330
+ const y = (x + value / x) >> 1n
331
+ if (y >= x) {
332
+ return x
333
+ }
334
+ x = y
335
+ }
336
+ }
337
+
338
+ /** 十进制数('1.5'、'2e-7'、1.5)→ 分子 / 分母,精确,不经过浮点 */
339
+ export function parseDecimal(value: string | number): { num: bigint; den: bigint } {
340
+ const text = typeof value === 'number' ? (Number.isFinite(value) ? String(value) : '') : value.trim()
341
+ const m = /^\+?(\d*)(?:\.(\d*))?(?:[eE]([+-]?\d+))?$/.exec(text)
342
+ if (!m || (!m[1] && !m[2])) {
343
+ throw new Error(`Invalid number: ${String(value)}`)
344
+ }
345
+ const frac = m[2] ?? ''
346
+ const exp = Number(m[3] ?? 0) - frac.length
347
+ if (Math.abs(exp) > 400) {
348
+ throw new RangeError(`Number out of range: ${String(value)}`)
349
+ }
350
+ const digits = BigInt(`${m[1] ?? ''}${frac}` || '0')
351
+ return exp >= 0 ? { num: digits * 10n ** BigInt(exp), den: 1n } : { num: digits, den: 10n ** BigInt(-exp) }
352
+ }
353
+
354
+ /** 价格(token1 / token0,按精度换算)→ Q64.64 sqrt 价格(向下取整);num / den 是价格的分子分母 */
355
+ export function ratioToSqrtPrice(num: bigint, den: bigint, decimals0: number, decimals1: number): bigint {
356
+ if (num <= 0n || den <= 0n) {
357
+ throw new RangeError('price must be greater than 0')
358
+ }
359
+ const scale = decimals1 - decimals0
360
+ return sqrtBigInt((num * (scale > 0 ? 10n ** BigInt(scale) : 1n) * 2n ** 128n) / (den * (scale < 0 ? 10n ** BigInt(-scale) : 1n)))
361
+ }
362
+
363
+ /** 价格(token1 / token0,按精度换算)→ tick(按程序;满足价格 >= tick 价格的最大 tick) */
364
+ export function priceToTick(program: ClmmProgram, price: string | number, decimals0: number, decimals1: number): number {
365
+ const { num, den } = parseDecimal(price)
366
+ return tickAtSqrtPrice(program, ratioToSqrtPrice(num, den, decimals0, decimals1))
367
+ }
368
+
369
+ /** 最近的可用 tick(tickSpacing 的整数倍),限制在 [MIN_TICK, MAX_TICK] 内 */
370
+ export function nearestUsableTick(tick: number, tickSpacing: number): number {
371
+ if (!Number.isInteger(tickSpacing) || tickSpacing <= 0) {
372
+ throw new RangeError(`invalid tickSpacing: ${tickSpacing}`)
373
+ }
374
+ const rounded = Math.round(tick / tickSpacing) * tickSpacing
375
+ if (rounded < MIN_TICK) {
376
+ return rounded + tickSpacing
377
+ }
378
+ if (rounded > MAX_TICK) {
379
+ return rounded - tickSpacing
380
+ }
381
+ return rounded
382
+ }
383
+
384
+ /** 价格按 slippage 上下浮动后的 sqrt 价格和对应的 tick,限制在程序允许的范围内(slippage 是比例,精确到百万分之一) */
385
+ export function sqrtPriceBounds(program: ClmmProgram, sqrtPrice: bigint, slippage: number): Array<{ sqrtPrice: bigint; tick: number }> {
386
+ if (!(slippage >= 0 && slippage < 1)) {
387
+ throw new RangeError(`slippage must be in [0, 1): ${slippage}`)
388
+ }
389
+ const [min, max] = sqrtRange(program)
390
+ const ppm = BigInt(Math.round(slippage * 1e6))
391
+ const square = sqrtPrice * sqrtPrice
392
+ return [1_000_000n - ppm, 1_000_000n + ppm].map((factor) => {
393
+ let s = sqrtBigInt((square * factor) / 1_000_000n)
394
+ s = s < min ? min : s >= max ? max - 1n : s
395
+ return { sqrtPrice: s, tick: tickAtSqrtPrice(program, s) }
396
+ })
397
+ }
398
+
399
+ /** 价格在 slippage 范围内变动时,流动性 L 至少对应多少代币(移除时的最小数量,向下取整) */
400
+ export function getMinAmounts(program: ClmmProgram, sqrtPrice: bigint, tickLower: number, tickUpper: number, liquidity: bigint, slippage: number): [bigint, bigint] {
401
+ const [lower, upper] = sqrtPriceBounds(program, sqrtPrice, slippage) as [{ sqrtPrice: bigint; tick: number }, { sqrtPrice: bigint; tick: number }]
402
+ const a = sqrtPriceAtTick(program, tickLower)
403
+ const b = sqrtPriceAtTick(program, tickUpper)
404
+ // token0 随价格上涨而减少,取上限价格时的;token1 取下限价格时的
405
+ return [
406
+ getAmountsForLiquidity(upper.tick, upper.sqrtPrice, a, b, liquidity, tickLower, tickUpper)[0],
407
+ getAmountsForLiquidity(lower.tick, lower.sqrtPrice, a, b, liquidity, tickLower, tickUpper)[1],
408
+ ]
409
+ }
410
+
411
+ /** 价格在 slippage 范围内变动时,添加流动性 L 最多需要多少代币(amountMax,向上取整) */
412
+ export function getMaxAmounts(program: ClmmProgram, sqrtPrice: bigint, tickLower: number, tickUpper: number, liquidity: bigint, slippage: number): [bigint, bigint] {
413
+ const [lower, upper] = sqrtPriceBounds(program, sqrtPrice, slippage) as [{ sqrtPrice: bigint; tick: number }, { sqrtPrice: bigint; tick: number }]
414
+ const a = sqrtPriceAtTick(program, tickLower)
415
+ const b = sqrtPriceAtTick(program, tickUpper)
416
+ return [
417
+ getMintAmounts(lower.tick, lower.sqrtPrice, a, b, liquidity, tickLower, tickUpper)[0],
418
+ getMintAmounts(upper.tick, upper.sqrtPrice, a, b, liquidity, tickLower, tickUpper)[1],
419
+ ]
420
+ }