@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.
Files changed (223) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +343 -3
  3. package/dist/approvals.d.ts +129 -0
  4. package/dist/approvals.d.ts.map +1 -0
  5. package/dist/approvals.js +221 -0
  6. package/dist/approvals.js.map +1 -0
  7. package/dist/batchPlan.d.ts +52 -0
  8. package/dist/batchPlan.d.ts.map +1 -0
  9. package/dist/batchPlan.js +85 -0
  10. package/dist/batchPlan.js.map +1 -0
  11. package/dist/claim.d.ts +161 -0
  12. package/dist/claim.d.ts.map +1 -0
  13. package/dist/claim.js +197 -0
  14. package/dist/claim.js.map +1 -0
  15. package/dist/decode.d.ts +22 -0
  16. package/dist/decode.d.ts.map +1 -0
  17. package/dist/decode.js +187 -0
  18. package/dist/decode.js.map +1 -0
  19. package/dist/describe.d.ts +26 -0
  20. package/dist/describe.d.ts.map +1 -0
  21. package/dist/describe.js +58 -0
  22. package/dist/describe.js.map +1 -0
  23. package/dist/fees.d.ts +89 -0
  24. package/dist/fees.d.ts.map +1 -0
  25. package/dist/fees.js +166 -0
  26. package/dist/fees.js.map +1 -0
  27. package/dist/history.d.ts +125 -0
  28. package/dist/history.d.ts.map +1 -0
  29. package/dist/history.js +231 -0
  30. package/dist/history.js.map +1 -0
  31. package/dist/historyHelius.d.ts +52 -0
  32. package/dist/historyHelius.d.ts.map +1 -0
  33. package/dist/historyHelius.js +125 -0
  34. package/dist/historyHelius.js.map +1 -0
  35. package/dist/layout.d.ts +2 -0
  36. package/dist/layout.d.ts.map +1 -1
  37. package/dist/layout.js +2 -2
  38. package/dist/layout.js.map +1 -1
  39. package/dist/liquidity/orca.d.ts +6 -0
  40. package/dist/liquidity/orca.d.ts.map +1 -1
  41. package/dist/liquidity/orca.js +14 -3
  42. package/dist/liquidity/orca.js.map +1 -1
  43. package/dist/liquidity/raydiumClmm.d.ts +6 -0
  44. package/dist/liquidity/raydiumClmm.d.ts.map +1 -1
  45. package/dist/liquidity/raydiumClmm.js +12 -5
  46. package/dist/liquidity/raydiumClmm.js.map +1 -1
  47. package/dist/lp.d.ts +55 -15
  48. package/dist/lp.d.ts.map +1 -1
  49. package/dist/lp.js +98 -46
  50. package/dist/lp.js.map +1 -1
  51. package/dist/lpMath.d.ts +22 -0
  52. package/dist/lpMath.d.ts.map +1 -1
  53. package/dist/lpMath.js +21 -0
  54. package/dist/lpMath.js.map +1 -1
  55. package/dist/names.d.ts +68 -0
  56. package/dist/names.d.ts.map +1 -0
  57. package/dist/names.js +257 -0
  58. package/dist/names.js.map +1 -0
  59. package/dist/pools.d.ts +9 -0
  60. package/dist/pools.d.ts.map +1 -0
  61. package/dist/pools.js +45 -0
  62. package/dist/pools.js.map +1 -0
  63. package/dist/portfolio.d.ts +64 -0
  64. package/dist/portfolio.d.ts.map +1 -0
  65. package/dist/portfolio.js +57 -0
  66. package/dist/portfolio.js.map +1 -0
  67. package/dist/price.d.ts +90 -0
  68. package/dist/price.d.ts.map +1 -0
  69. package/dist/price.js +287 -0
  70. package/dist/price.js.map +1 -0
  71. package/dist/programs.d.ts +19 -0
  72. package/dist/programs.d.ts.map +1 -0
  73. package/dist/programs.js +175 -0
  74. package/dist/programs.js.map +1 -0
  75. package/dist/reclaim.d.ts +190 -0
  76. package/dist/reclaim.d.ts.map +1 -0
  77. package/dist/reclaim.js +399 -0
  78. package/dist/reclaim.js.map +1 -0
  79. package/dist/risk.d.ts +84 -0
  80. package/dist/risk.d.ts.map +1 -0
  81. package/dist/risk.js +211 -0
  82. package/dist/risk.js.map +1 -0
  83. package/dist/rpc.d.ts +2 -0
  84. package/dist/rpc.d.ts.map +1 -1
  85. package/dist/rpc.js +36 -10
  86. package/dist/rpc.js.map +1 -1
  87. package/dist/simulate.d.ts +106 -0
  88. package/dist/simulate.d.ts.map +1 -0
  89. package/dist/simulate.js +418 -0
  90. package/dist/simulate.js.map +1 -0
  91. package/dist/subpaths/approvals.d.ts +29 -0
  92. package/dist/subpaths/approvals.d.ts.map +1 -0
  93. package/dist/subpaths/approvals.js +30 -0
  94. package/dist/subpaths/approvals.js.map +1 -0
  95. package/dist/subpaths/claim.d.ts +31 -0
  96. package/dist/subpaths/claim.d.ts.map +1 -0
  97. package/dist/subpaths/claim.js +31 -0
  98. package/dist/subpaths/claim.js.map +1 -0
  99. package/dist/subpaths/fees.d.ts +23 -0
  100. package/dist/subpaths/fees.d.ts.map +1 -0
  101. package/dist/subpaths/fees.js +25 -0
  102. package/dist/subpaths/fees.js.map +1 -0
  103. package/dist/subpaths/history.d.ts +21 -0
  104. package/dist/subpaths/history.d.ts.map +1 -0
  105. package/dist/subpaths/history.js +22 -0
  106. package/dist/subpaths/history.js.map +1 -0
  107. package/dist/subpaths/lp.d.ts +2 -2
  108. package/dist/subpaths/lp.d.ts.map +1 -1
  109. package/dist/subpaths/lp.js +2 -2
  110. package/dist/subpaths/lp.js.map +1 -1
  111. package/dist/subpaths/names.d.ts +37 -0
  112. package/dist/subpaths/names.d.ts.map +1 -0
  113. package/dist/subpaths/names.js +27 -0
  114. package/dist/subpaths/names.js.map +1 -0
  115. package/dist/subpaths/price.d.ts +40 -0
  116. package/dist/subpaths/price.d.ts.map +1 -0
  117. package/dist/subpaths/price.js +43 -0
  118. package/dist/subpaths/price.js.map +1 -0
  119. package/dist/subpaths/reclaim.d.ts +34 -0
  120. package/dist/subpaths/reclaim.d.ts.map +1 -0
  121. package/dist/subpaths/reclaim.js +34 -0
  122. package/dist/subpaths/reclaim.js.map +1 -0
  123. package/dist/subpaths/risk.d.ts +28 -0
  124. package/dist/subpaths/risk.d.ts.map +1 -0
  125. package/dist/subpaths/risk.js +30 -0
  126. package/dist/subpaths/risk.js.map +1 -0
  127. package/dist/subpaths/simulate.d.ts +22 -0
  128. package/dist/subpaths/simulate.d.ts.map +1 -0
  129. package/dist/subpaths/simulate.js +23 -0
  130. package/dist/subpaths/simulate.js.map +1 -0
  131. package/dist/subpaths/swap.d.ts +22 -0
  132. package/dist/subpaths/swap.d.ts.map +1 -0
  133. package/dist/subpaths/swap.js +21 -0
  134. package/dist/subpaths/swap.js.map +1 -0
  135. package/dist/subpaths/write.d.ts +1 -1
  136. package/dist/subpaths/write.d.ts.map +1 -1
  137. package/dist/subpaths/write.js +1 -1
  138. package/dist/subpaths/write.js.map +1 -1
  139. package/dist/swap/amm.d.ts +20 -0
  140. package/dist/swap/amm.d.ts.map +1 -0
  141. package/dist/swap/amm.js +77 -0
  142. package/dist/swap/amm.js.map +1 -0
  143. package/dist/swap/clmmMath.d.ts +24 -0
  144. package/dist/swap/clmmMath.d.ts.map +1 -0
  145. package/dist/swap/clmmMath.js +82 -0
  146. package/dist/swap/clmmMath.js.map +1 -0
  147. package/dist/swap/common.d.ts +68 -0
  148. package/dist/swap/common.d.ts.map +1 -0
  149. package/dist/swap/common.js +65 -0
  150. package/dist/swap/common.js.map +1 -0
  151. package/dist/swap/cpmm.d.ts +28 -0
  152. package/dist/swap/cpmm.d.ts.map +1 -0
  153. package/dist/swap/cpmm.js +113 -0
  154. package/dist/swap/cpmm.js.map +1 -0
  155. package/dist/swap/index.d.ts +145 -0
  156. package/dist/swap/index.d.ts.map +1 -0
  157. package/dist/swap/index.js +349 -0
  158. package/dist/swap/index.js.map +1 -0
  159. package/dist/swap/orca.d.ts +50 -0
  160. package/dist/swap/orca.d.ts.map +1 -0
  161. package/dist/swap/orca.js +382 -0
  162. package/dist/swap/orca.js.map +1 -0
  163. package/dist/swap/raydiumClmm.d.ts +62 -0
  164. package/dist/swap/raydiumClmm.d.ts.map +1 -0
  165. package/dist/swap/raydiumClmm.js +447 -0
  166. package/dist/swap/raydiumClmm.js.map +1 -0
  167. package/dist/tradeCheck.d.ts +45 -0
  168. package/dist/tradeCheck.d.ts.map +1 -0
  169. package/dist/tradeCheck.js +131 -0
  170. package/dist/tradeCheck.js.map +1 -0
  171. package/dist/write/instructions.d.ts +9 -0
  172. package/dist/write/instructions.d.ts.map +1 -1
  173. package/dist/write/instructions.js +40 -1
  174. package/dist/write/instructions.js.map +1 -1
  175. package/dist/write/transaction.d.ts +23 -0
  176. package/dist/write/transaction.d.ts.map +1 -1
  177. package/dist/write/transaction.js +50 -0
  178. package/dist/write/transaction.js.map +1 -1
  179. package/package.json +66 -14
  180. package/src/approvals.ts +358 -0
  181. package/src/batchPlan.ts +137 -0
  182. package/src/claim.ts +323 -0
  183. package/src/decode.ts +202 -0
  184. package/src/describe.ts +77 -0
  185. package/src/fees.ts +259 -0
  186. package/src/history.ts +336 -0
  187. package/src/historyHelius.ts +136 -0
  188. package/src/layout.ts +2 -2
  189. package/src/liquidity/orca.ts +15 -5
  190. package/src/liquidity/raydiumClmm.ts +13 -5
  191. package/src/lp.ts +148 -59
  192. package/src/lpMath.ts +38 -0
  193. package/src/names.ts +329 -0
  194. package/src/pools.ts +43 -0
  195. package/src/portfolio.ts +124 -0
  196. package/src/price.ts +366 -0
  197. package/src/programs.ts +216 -0
  198. package/src/reclaim.ts +547 -0
  199. package/src/risk.ts +243 -0
  200. package/src/rpc.ts +39 -10
  201. package/src/simulate.ts +535 -0
  202. package/src/subpaths/approvals.ts +47 -0
  203. package/src/subpaths/claim.ts +53 -0
  204. package/src/subpaths/fees.ts +41 -0
  205. package/src/subpaths/history.ts +38 -0
  206. package/src/subpaths/lp.ts +5 -1
  207. package/src/subpaths/names.ts +45 -0
  208. package/src/subpaths/price.ts +62 -0
  209. package/src/subpaths/reclaim.ts +57 -0
  210. package/src/subpaths/risk.ts +36 -0
  211. package/src/subpaths/simulate.ts +37 -0
  212. package/src/subpaths/swap.ts +41 -0
  213. package/src/subpaths/write.ts +2 -0
  214. package/src/swap/amm.ts +88 -0
  215. package/src/swap/clmmMath.ts +96 -0
  216. package/src/swap/common.ts +113 -0
  217. package/src/swap/cpmm.ts +132 -0
  218. package/src/swap/index.ts +467 -0
  219. package/src/swap/orca.ts +397 -0
  220. package/src/swap/raydiumClmm.ts +464 -0
  221. package/src/tradeCheck.ts +166 -0
  222. package/src/write/instructions.ts +43 -1
  223. package/src/write/transaction.ts +62 -0
package/src/fees.ts ADDED
@@ -0,0 +1,259 @@
1
+ /**
2
+ * 手续费估算(子路径 `@w3lib/solana-batch-call/fees`):三档优先费(getRecentPrioritizationFees 的百分位,可以只看交易会写的账户),
3
+ * 一笔交易的花费:基础费(每个签名 5000 lamports)+ 优先费(模拟得到的计算单元上限 × 单价)+ 新建账户的免租押金
4
+ */
5
+ import { base64 } from '@scure/base'
6
+
7
+ import { encodeAddress } from './address.js'
8
+ import type { RequestOptions, SolanaClient } from './client.js'
9
+ import { SOL_DECIMALS, SYSTEM_PROGRAM_ID } from './constants.js'
10
+ import { formatUnits } from './layout.js'
11
+ import type { InstructionInput } from './write/transaction.js'
12
+
13
+ export type FeeLevel = 'slow' | 'normal' | 'fast'
14
+
15
+ /** 三档优先费单价(每个计算单元多少 micro-lamports,十进制字符串) */
16
+ export interface PriorityFeeEstimate {
17
+ slow: string
18
+ normal: string
19
+ fast: string
20
+ /** 统计了多少个 slot(节点返回最近 150 个) */
21
+ slots: number
22
+ }
23
+
24
+ export interface EstimateFeesOptions extends RequestOptions {
25
+ /** 只看会写这些账户的交易(如交易的可写账户,最多 128 个);不传时看全网 */
26
+ accounts?: readonly string[]
27
+ /** 三档取各 slot 最低落块费用的百分位,默认 [50, 75, 90](normal 与 /write 的 priorityFee: 'auto' 相同) */
28
+ percentiles?: readonly [number, number, number]
29
+ }
30
+
31
+ interface PrioritizationFee {
32
+ slot: number
33
+ prioritizationFee: number
34
+ }
35
+
36
+ /**
37
+ * 当前的优先费建议:getRecentPrioritizationFees 返回最近 150 个 slot 里(会写这些账户的)落块交易的最低单价,三档取它们的百分位。
38
+ * 不传 accounts 时大多是 0(每个区块总有不加优先费的交易),要估某笔交易时传它的可写账户(estimateCost 自动这样做)
39
+ *
40
+ * ```ts
41
+ * await estimateFees(client, { accounts: [poolAddress] }) // { slow: '0', normal: '12000', fast: '150000', slots: 150 }
42
+ * ```
43
+ */
44
+ export async function estimateFees(client: SolanaClient, options: EstimateFeesOptions = {}): Promise<PriorityFeeEstimate> {
45
+ const [p1, p2, p3] = options.percentiles ?? [50, 75, 90]
46
+ const accounts = [...new Set(options.accounts ?? [])].slice(0, 128)
47
+ const list = await client.request<PrioritizationFee[]>('getRecentPrioritizationFees', accounts.length ? [accounts] : [], { signal: options.signal })
48
+ const fees = list.map((f) => BigInt(f.prioritizationFee)).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))
49
+ // 与 /write 的 'auto' 相同的取法:排序后第 ⌊n × p⌋ 个
50
+ const pick = (p: number) => (fees.length ? (fees[Math.min(fees.length - 1, Math.max(0, Math.floor((fees.length * p) / 100)))] as bigint) : 0n)
51
+ const slow = pick(p1)
52
+ const normal = pick(p2) > slow ? pick(p2) : slow
53
+ const fast = pick(p3) > normal ? pick(p3) : normal
54
+ return { slow: slow.toString(), normal: normal.toString(), fast: fast.toString(), slots: list.length }
55
+ }
56
+
57
+ /**
58
+ * 要估算的交易:
59
+ * - 指令 + 手续费支付者:按 /write 的方式发送时的花费(计算单元上限 = 模拟值 × 1.1 + 1000,单价按这一档)。指令里自带的计算预算指令会被去掉
60
+ * - 已经构建好的交易(序列化的字节或 base64,legacy / v0,签名可以是空的):按交易自带的计算预算指令算(没有 SetComputeUnitPrice 时优先费为 0)
61
+ */
62
+ export type CostInput = { feePayer: string; instructions: InstructionInput | readonly InstructionInput[] } | { transaction: Uint8Array | string }
63
+
64
+ export interface EstimateCostOptions extends Omit<EstimateFeesOptions, 'accounts'> {
65
+ /** 用哪一档优先费,默认 'normal'(只对指令有效) */
66
+ level?: FeeLevel
67
+ /** 直接指定优先费单价(micro-lamports / 计算单元),不查建议(只对指令有效) */
68
+ priorityFee?: number | bigint
69
+ /** 已经查好的费用建议(多笔交易共用);不传时按交易的可写账户查 */
70
+ fees?: PriorityFeeEstimate
71
+ }
72
+
73
+ export interface CreatedAccount {
74
+ address: string
75
+ /** 所属程序 */
76
+ owner: string
77
+ /** 数据大小(字节) */
78
+ space: number
79
+ /** 免租押金(lamports) */
80
+ rent: string
81
+ }
82
+
83
+ export interface CostEstimate {
84
+ /** 模拟消耗的计算单元 */
85
+ computeUnits: number
86
+ /** 计算单元上限(优先费按它收) */
87
+ computeUnitLimit: number
88
+ /** 优先费单价(micro-lamports / 计算单元) */
89
+ priorityFee: string
90
+ signatures: number
91
+ /** 基础费:每个签名 5000 lamports */
92
+ baseFee: string
93
+ /** 优先费:单价 × 上限(lamports,向上取整) */
94
+ priorityFeeLamports: string
95
+ /** 手续费:baseFee + priorityFeeLamports(lamports) */
96
+ fee: string
97
+ /** 新建账户的免租押金合计(lamports,关闭账户时退回) */
98
+ rent: string
99
+ createdAccounts: CreatedAccount[]
100
+ /** fee + rent(lamports)和按 SOL 换算的值 */
101
+ total: string
102
+ totalFormatted: string
103
+ symbol: 'SOL'
104
+ }
105
+
106
+ interface SimulatedAccount {
107
+ lamports: number
108
+ owner: string
109
+ data: [string, string]
110
+ }
111
+
112
+ interface Simulation {
113
+ err: unknown
114
+ logs?: string[] | null
115
+ unitsConsumed?: number
116
+ accounts?: Array<SimulatedAccount | null> | null
117
+ }
118
+
119
+ /** 没有 SetComputeUnitLimit 时每条(非计算预算)指令的默认上限 */
120
+ const DEFAULT_UNITS_PER_INSTRUCTION = 200_000
121
+ /** 地址查找表账户的头部大小,之后每 32 字节一个地址 */
122
+ const LOOKUP_TABLE_META_SIZE = 56
123
+
124
+ /**
125
+ * 估算一笔交易的花费(模拟一次,sigVerify: false,不需要签名、不会发送)。模拟失败时抛出(带程序日志里的原因)。
126
+ * 新建账户:交易前不存在(lamports 为 0)、模拟后存在、有数据或不归 System Program 的可写账户,押金按 getMinimumBalanceForRentExemption 算;
127
+ * 普通转账给新地址(System 账户、没有数据)不算押金
128
+ *
129
+ * ```ts
130
+ * await estimateCost(client, { feePayer: owner, instructions: [ix1, ix2] })
131
+ * // { computeUnits: 41230, computeUnitLimit: 46353, priorityFee: '12000', fee: '5557', rent: '1488440', totalFormatted: '0.001493997', … }
132
+ * await estimateCost(client, { transaction: base64FromJupiter })
133
+ * ```
134
+ */
135
+ export async function estimateCost(client: SolanaClient, input: CostInput, options: EstimateCostOptions = {}): Promise<CostEstimate> {
136
+ const { signal } = options
137
+ // 构建交易的代码按需加载:只查费用建议时打包不会带上
138
+ const [{ MAX_COMPUTE_UNITS, COMPUTE_BUDGET_PROGRAM_ID, setComputeUnitLimit, setComputeUnitPrice }, tx, { transactionErrorText, transactionFee }] = await Promise.all([
139
+ import('./write/instructions.js'),
140
+ import('./write/transaction.js'),
141
+ import('./write/send.js'),
142
+ ])
143
+
144
+ let raw: string
145
+ let signatures: number
146
+ let writable: string[]
147
+ /** 交易自带的计算预算(只对构建好的交易) */
148
+ let budget: { limit: number; price: bigint } | null = null
149
+ if ('transaction' in input) {
150
+ const bytes = typeof input.transaction === 'string' ? base64.decode(input.transaction) : input.transaction
151
+ // legacy / v0 的交易以签名数(compact-u16,不会超过 127)开头;v1 等新格式以版本字节(0x80 | 版本)开头,布局不同
152
+ if (((bytes[0] ?? 0) & 0x80) !== 0) {
153
+ throw new Error(`Unsupported transaction format (version ${(bytes[0] as number) & 0x7f}); only legacy and v0 transactions can be estimated, pass the instructions instead`)
154
+ }
155
+ const message = tx.parseMessage(tx.splitTransaction(bytes).message)
156
+ raw = base64.encode(bytes)
157
+ signatures = message.signatures
158
+ writable = [...message.writable, ...(await lookupWritable(client, message.lookups, signal))]
159
+ let limit: number | null = null
160
+ let price = 0n
161
+ for (const ix of message.instructions) {
162
+ if (ix.programId !== COMPUTE_BUDGET_PROGRAM_ID) continue
163
+ const view = new DataView(ix.data.buffer, ix.data.byteOffset, ix.data.byteLength)
164
+ if (ix.data[0] === 2 && ix.data.length >= 5) limit = view.getUint32(1, true)
165
+ if (ix.data[0] === 3 && ix.data.length >= 9) price = view.getBigUint64(1, true)
166
+ }
167
+ const others = message.instructions.filter((ix) => ix.programId !== COMPUTE_BUDGET_PROGRAM_ID).length
168
+ budget = { limit: Math.min(MAX_COMPUTE_UNITS, limit ?? DEFAULT_UNITS_PER_INSTRUCTION * others), price }
169
+ } else {
170
+ const list = Array.isArray(input.instructions) ? (input.instructions as readonly InstructionInput[]) : [input.instructions as InstructionInput]
171
+ // 和 batchCall 一样:去掉自带的计算单元上限 / 单价指令,模拟时上限取最大、单价为 0
172
+ const instructions = list.map(tx.normalizeInstruction).filter((ix) => !(ix.programId === COMPUTE_BUDGET_PROGRAM_ID && (ix.data[0] === 2 || ix.data[0] === 3)))
173
+ const message = tx.compileMessage(input.feePayer, [setComputeUnitLimit(MAX_COMPUTE_UNITS), setComputeUnitPrice(0n), ...instructions], SYSTEM_PROGRAM_ID)
174
+ raw = base64.encode(tx.serializeTransaction(message, message.signers.map(() => null)))
175
+ signatures = message.signers.length
176
+ writable = message.writable
177
+ }
178
+
179
+ // 交易前不存在的可写账户:模拟后读回它们的状态,判断是否新建
180
+ const before = writable.length ? await client.solBalances(writable, { signal }) : []
181
+ const fresh = writable.filter((_, i) => before[i]?.balance === '0')
182
+ const priceFor = async () => {
183
+ if (budget) return budget.price
184
+ if (options.priorityFee !== undefined) return BigInt(options.priorityFee)
185
+ const fees = options.fees ?? (await estimateFees(client, { accounts: writable, percentiles: options.percentiles, signal }))
186
+ return BigInt(fees[options.level ?? 'normal'])
187
+ }
188
+ const [simulation, price] = await Promise.all([
189
+ client.request<{ value: Simulation }>(
190
+ 'simulateTransaction',
191
+ [
192
+ raw,
193
+ {
194
+ encoding: 'base64',
195
+ sigVerify: false,
196
+ replaceRecentBlockhash: true,
197
+ commitment: 'confirmed',
198
+ ...(fresh.length ? { accounts: { encoding: 'base64', addresses: fresh } } : {}),
199
+ },
200
+ ],
201
+ { signal },
202
+ ),
203
+ priceFor(),
204
+ ])
205
+ const sim = simulation.value
206
+ if (sim.err) {
207
+ throw new Error(`Simulation failed: ${transactionErrorText(sim.err, sim.logs)}`)
208
+ }
209
+ const consumed = sim.unitsConsumed ?? 0
210
+ const limit = budget?.limit ?? Math.min(MAX_COMPUTE_UNITS, Math.ceil(consumed * 1.1) + 1000)
211
+
212
+ const created = fresh.flatMap((address, i) => {
213
+ const a = sim.accounts?.[i]
214
+ if (!a || !(a.lamports > 0)) return []
215
+ const space = base64.decode(a.data[0]).length
216
+ return space > 0 || a.owner !== SYSTEM_PROGRAM_ID ? [{ address, owner: a.owner, space }] : []
217
+ })
218
+ const sizes = [...new Set(created.map((c) => c.space))]
219
+ const rents = await Promise.all(sizes.map((size) => client.request<number>('getMinimumBalanceForRentExemption', [size], { signal })))
220
+ const rentOf = new Map(sizes.map((size, i) => [size, BigInt(rents[i] as number)]))
221
+ const createdAccounts = created.map((c) => ({ ...c, rent: (rentOf.get(c.space) as bigint).toString() }))
222
+ const rent = createdAccounts.reduce((sum, c) => sum + BigInt(c.rent), 0n)
223
+
224
+ const fee = transactionFee(signatures, limit, price)
225
+ const baseFee = 5000n * BigInt(signatures)
226
+ const total = fee + rent
227
+ return {
228
+ computeUnits: consumed,
229
+ computeUnitLimit: limit,
230
+ priorityFee: price.toString(),
231
+ signatures,
232
+ baseFee: baseFee.toString(),
233
+ priorityFeeLamports: (fee - baseFee).toString(),
234
+ fee: fee.toString(),
235
+ rent: rent.toString(),
236
+ createdAccounts,
237
+ total: total.toString(),
238
+ totalFormatted: formatUnits(total, SOL_DECIMALS),
239
+ symbol: 'SOL',
240
+ }
241
+ }
242
+
243
+ /** v0 交易的地址查找表里要写的账户(读查找表账户:56 字节头部之后每 32 字节一个地址);表读不到时跳过 */
244
+ async function lookupWritable(client: SolanaClient, lookups: ReadonlyArray<{ table: string; writable: number[] }>, signal: AbortSignal | undefined): Promise<string[]> {
245
+ const used = lookups.filter((l) => l.writable.length)
246
+ if (!used.length) return []
247
+ const tables = await client.accounts(
248
+ used.map((l) => l.table),
249
+ { signal },
250
+ )
251
+ return used.flatMap((l, i) => {
252
+ const data = tables[i]?.data
253
+ if (!data) return []
254
+ return l.writable.flatMap((index) => {
255
+ const start = LOOKUP_TABLE_META_SIZE + 32 * index
256
+ return start + 32 <= data.length ? [encodeAddress(data.slice(start, start + 32))] : []
257
+ })
258
+ })
259
+ }
package/src/history.ts ADDED
@@ -0,0 +1,336 @@
1
+ /**
2
+ * 交易历史(子路径 `@w3lib/solana-batch-call/history`):把地址的交易整理成统一的 Activity(类型、方向、对手方、资产变化、手续费)。
3
+ * 数据源可以换:默认 rpcHistory(getSignaturesForAddress 翻页 + getTransaction,免费节点就能用,节点失败自动切换),
4
+ * 也可以传 heliusHistory 等;传多个时按顺序尝试。游标就是最后一条签名,换数据源也能接着翻
5
+ */
6
+ import { base58 } from '@scure/base'
7
+
8
+ import { isAddress } from './address.js'
9
+ import type { RequestOptions, SolanaClient } from './client.js'
10
+ import { NATIVE_MINT, SOL_DECIMALS, SYSTEM_PROGRAM_ID } from './constants.js'
11
+ import { decodeInstruction, type DecodedInstruction } from './decode.js'
12
+ import { classify, describe, type AssetDelta, type Classified } from './describe.js'
13
+ import { formatUnits } from './layout.js'
14
+
15
+ export type ActivityType = 'transfer' | 'swap' | 'approve' | 'liquidity' | 'mint' | 'contract' | 'other'
16
+
17
+ export interface ActivityChange extends AssetDelta {}
18
+
19
+ export interface Activity {
20
+ /** 交易签名 */
21
+ hash: string
22
+ /** 区块时间(unix 秒);节点没有给时为 0 */
23
+ time: number
24
+ /** slot */
25
+ block?: number
26
+ status: 'success' | 'failed'
27
+ type: ActivityType
28
+ /** 对这个地址来说:资产只增加 in、只减少 out、转给自己 self;兑换等有增有减时不填 */
29
+ direction?: 'in' | 'out' | 'self'
30
+ /** 转账的对手方 */
31
+ counterparty?: string
32
+ /** 一句话说明,如 “Swap 1 SOL for 150 USDC on Jupiter” */
33
+ label?: string
34
+ /** 这个地址的资产变化(SOL 不含手续费,Wrapped SOL 并入 SOL;asset 原生为 'native',其他是 mint) */
35
+ changes: ActivityChange[]
36
+ /** 这个地址付的手续费(lamports,十进制字符串);不是手续费支付者时不填 */
37
+ fee?: string
38
+ protocol?: string
39
+ }
40
+
41
+ export interface HistoryPage {
42
+ items: Activity[]
43
+ /** 还有更多时:下一页的游标(最后一条处理过的签名);没有更多时不返回 */
44
+ nextCursor?: string
45
+ }
46
+
47
+ export interface HistoryContext {
48
+ client: SolanaClient
49
+ address: string
50
+ limit: number
51
+ cursor?: string
52
+ /** 只要这个时间(unix 秒)之前 / 之后的 */
53
+ before?: number
54
+ after?: number
55
+ types?: readonly ActivityType[]
56
+ fetch: typeof fetch
57
+ signal?: AbortSignal
58
+ }
59
+
60
+ /** 历史数据源:返回统一格式的 Activity(代币的 symbol 先用缩写的 mint,getHistory 统一补全);游标是签名 */
61
+ export interface HistorySource {
62
+ readonly name: string
63
+ getHistory(ctx: HistoryContext): Promise<HistoryPage>
64
+ }
65
+
66
+ export interface HistoryOptions extends RequestOptions {
67
+ /** 每页条数,默认 20,最多 100 */
68
+ limit?: number
69
+ /** 上一页的 nextCursor */
70
+ cursor?: string
71
+ /** 区块时间上限(不含)/ 下限(含),unix 秒 */
72
+ before?: number
73
+ after?: number
74
+ /** 只要这些类型(不够 limit 时会往后多找,最多看 limit × 5 笔) */
75
+ types?: readonly ActivityType[]
76
+ /** 数据源,默认 rpcHistory();传数组时按顺序尝试,前一个失败换下一个 */
77
+ source?: HistorySource | readonly HistorySource[]
78
+ /** 补全代币符号(读 mint 的元数据),默认 true */
79
+ symbols?: boolean
80
+ fetch?: typeof fetch
81
+ }
82
+
83
+ export const MAX_HISTORY_LIMIT = 100
84
+ /** getTransaction 支持的交易版本:主网已经有 v1 格式的交易(2026-10),只给 0 时遇到它们整个请求报错(-32015);json 编码下 v1 的账户和指令字段与 v0 相同 */
85
+ const MAX_TX_VERSION = 1
86
+
87
+ const short = (a: string) => `${a.slice(0, 4)}…${a.slice(-4)}`
88
+
89
+ /**
90
+ * 查地址的交易历史。
91
+ *
92
+ * ```ts
93
+ * const page = await history(client, address, { limit: 20 })
94
+ * const next = await history(client, address, { cursor: page.nextCursor })
95
+ * ```
96
+ */
97
+ export async function history(client: SolanaClient, address: string, options: HistoryOptions = {}): Promise<HistoryPage> {
98
+ if (!isAddress(address)) throw new Error(`Invalid address: ${address}`)
99
+ const limit = options.limit ?? 20
100
+ if (!Number.isInteger(limit) || limit < 1 || limit > MAX_HISTORY_LIMIT) throw new RangeError(`limit must be an integer between 1 and ${MAX_HISTORY_LIMIT}`)
101
+ const sources = options.source === undefined ? [rpcHistory()] : Array.isArray(options.source) ? options.source : [options.source as HistorySource]
102
+ if (!sources.length) throw new Error('No history source')
103
+ const ctx: HistoryContext = {
104
+ client,
105
+ address,
106
+ limit,
107
+ cursor: options.cursor,
108
+ before: options.before,
109
+ after: options.after,
110
+ types: options.types?.length ? options.types : undefined,
111
+ fetch: options.fetch ?? ((...args) => fetch(...args)),
112
+ signal: options.signal,
113
+ }
114
+ const errors: string[] = []
115
+ for (const source of sources) {
116
+ try {
117
+ const page = await (source as HistorySource).getHistory(ctx)
118
+ if (options.symbols !== false) await fillSymbols(client, page.items, options.signal)
119
+ return page
120
+ } catch (err) {
121
+ if (options.signal?.aborted) throw err
122
+ errors.push(`${(source as HistorySource).name}: ${err instanceof Error ? err.message : String(err)}`)
123
+ }
124
+ }
125
+ throw new Error(`All history sources failed (${errors.join('; ')})`)
126
+ }
127
+
128
+ /** 补全还是缩写 mint 的 symbol(一次 tokens 查询,label 里的同步替换);读不到的用缩写的 mint */
129
+ async function fillSymbols(client: SolanaClient, items: Activity[], signal: AbortSignal | undefined): Promise<void> {
130
+ const pending = (c: ActivityChange) => c.symbol === short(c.asset)
131
+ const missing = [...new Set(items.flatMap((a) => a.changes.filter(pending).map((c) => c.asset)))]
132
+ if (!missing.length) return
133
+ const details = await client.tokens(missing, { fields: ['symbol'], signal }).catch(() => null)
134
+ const symbols = new Map(missing.map((m, i) => [m, details?.[i]?.symbol ?? short(m)]))
135
+ for (const a of items) {
136
+ for (const c of a.changes) {
137
+ if (!pending(c)) continue
138
+ const symbol = symbols.get(c.asset) as string
139
+ if (a.label) a.label = a.label.split(c.symbol).join(symbol)
140
+ c.symbol = symbol
141
+ }
142
+ }
143
+ }
144
+
145
+ // ---------------------------------------------------------------------------
146
+ // RPC 数据源
147
+ // ---------------------------------------------------------------------------
148
+
149
+ export interface RpcHistoryOptions {
150
+ /** 同时取多少笔交易的详情,默认 6(公共节点限频很严,节点自己会对 429 退避重试) */
151
+ concurrency?: number
152
+ /** 一次翻页最多检查多少笔签名(含被 types / 时间过滤掉的),默认 limit × 5,最多 500 */
153
+ maxScan?: number
154
+ }
155
+
156
+ interface RawTransaction {
157
+ slot: number
158
+ blockTime: number | null
159
+ meta: {
160
+ err: unknown
161
+ fee: number
162
+ preBalances: number[]
163
+ postBalances: number[]
164
+ preTokenBalances?: RawTokenBalance[]
165
+ postTokenBalances?: RawTokenBalance[]
166
+ loadedAddresses?: { writable: string[]; readonly: string[] }
167
+ } | null
168
+ transaction: {
169
+ signatures: string[]
170
+ message: { accountKeys: string[]; instructions: Array<{ programIdIndex: number; accounts: number[]; data: string }> }
171
+ }
172
+ }
173
+
174
+ interface RawTokenBalance {
175
+ accountIndex: number
176
+ mint: string
177
+ owner?: string
178
+ uiTokenAmount: { amount: string; decimals: number }
179
+ }
180
+
181
+ interface SignatureInfo {
182
+ signature: string
183
+ slot: number
184
+ err: unknown
185
+ blockTime: number | null
186
+ }
187
+
188
+ async function pool<T, R>(items: readonly T[], limit: number, fn: (item: T) => Promise<R>): Promise<R[]> {
189
+ const results = new Array<R>(items.length)
190
+ let next = 0
191
+ const worker = async () => {
192
+ while (next < items.length) {
193
+ const i = next++
194
+ results[i] = await fn(items[i] as T)
195
+ }
196
+ }
197
+ await Promise.all(Array.from({ length: Math.min(Math.max(1, limit), items.length) }, worker))
198
+ return results
199
+ }
200
+
201
+ /**
202
+ * 默认数据源:getSignaturesForAddress(用 before 翻页)+ getTransaction(逐笔,json 编码、支持 v0)。
203
+ * 先按区块时间过滤签名再取详情,只取需要的笔数;失败的交易也列出(status 为 failed)。每页的请求数 ≈ limit + 1
204
+ */
205
+ export function rpcHistory(options: RpcHistoryOptions = {}): HistorySource {
206
+ const concurrency = options.concurrency ?? 6
207
+ return {
208
+ name: 'rpc',
209
+ async getHistory(ctx) {
210
+ const { client, address, limit, signal } = ctx
211
+ const maxScan = Math.min(options.maxScan ?? limit * 5, 500)
212
+ const items: Activity[] = []
213
+ let cursor = ctx.cursor
214
+ let scanned = 0
215
+ let more = true
216
+ while (items.length < limit && scanned < maxScan && more) {
217
+ const pageSize = Math.min(1000, Math.max(limit, 20) * (ctx.types ? 3 : 1))
218
+ const commitment = client.commitment === 'processed' ? 'confirmed' : client.commitment
219
+ const sigs = await client.request<SignatureInfo[]>('getSignaturesForAddress', [address, { limit: pageSize, ...(cursor ? { before: cursor } : {}), commitment }], { signal })
220
+ if (!sigs.length) {
221
+ more = false
222
+ break
223
+ }
224
+ // 按时间过滤:比 before 新的跳过;比 after 旧的之后都不用看了(签名按从新到旧排列)
225
+ let rows = sigs.filter((s) => ctx.before === undefined || s.blockTime === null || s.blockTime < ctx.before)
226
+ const old = ctx.after === undefined ? -1 : rows.findIndex((s) => s.blockTime !== null && s.blockTime < (ctx.after as number))
227
+ if (old >= 0) rows = rows.slice(0, old)
228
+ let i = 0
229
+ while (i < rows.length && items.length < limit && scanned < maxScan) {
230
+ const chunk = rows.slice(i, i + Math.max(1, Math.min(limit - items.length, maxScan - scanned)))
231
+ const txs = await pool(chunk, concurrency, (row) => client.request<RawTransaction | null>('getTransaction', [row.signature, { encoding: 'json', maxSupportedTransactionVersion: MAX_TX_VERSION, commitment }], { signal }))
232
+ for (const [j, tx] of txs.entries()) {
233
+ i++
234
+ scanned++
235
+ cursor = (chunk[j] as SignatureInfo).signature
236
+ const activity = tx ? activityFromTransaction(address, tx) : null
237
+ if (activity && (!ctx.types || ctx.types.includes(activity.type))) items.push(activity)
238
+ if (items.length >= limit) break
239
+ }
240
+ }
241
+ if (i >= rows.length) {
242
+ // 这一页看完了:接着从这一页最后一条往后翻;没有更多(不足一页、或已经到 after)时结束
243
+ cursor = (sigs[sigs.length - 1] as SignatureInfo).signature
244
+ more = sigs.length >= pageSize && old < 0
245
+ }
246
+ }
247
+ return { items, ...(more && cursor ? { nextCursor: cursor } : {}) }
248
+ },
249
+ }
250
+ }
251
+
252
+ const SOL: Pick<AssetDelta, 'asset' | 'symbol' | 'decimals'> = { asset: 'native', symbol: 'SOL', decimals: SOL_DECIMALS }
253
+
254
+ /** 合并资产变化:同一资产相加,去掉 0 */
255
+ function toChanges(native: bigint, tokens: Map<string, { delta: bigint; decimals: number }>): ActivityChange[] {
256
+ const out: ActivityChange[] = []
257
+ if (native !== 0n) out.push({ ...SOL, delta: native.toString(), formatted: formatUnits(native, SOL_DECIMALS) })
258
+ for (const [asset, { delta, decimals }] of tokens) {
259
+ if (delta !== 0n) out.push({ asset, symbol: short(asset), decimals, delta: delta.toString(), formatted: formatUnits(delta, decimals) })
260
+ }
261
+ return out
262
+ }
263
+
264
+ function directionOf(changes: readonly ActivityChange[], counterparty: string | undefined, address: string): Activity['direction'] {
265
+ if (!changes.length) return counterparty === address ? 'self' : undefined
266
+ const negative = changes.filter((c) => c.delta.startsWith('-')).length
267
+ if (negative === 0) return 'in'
268
+ return negative === changes.length ? 'out' : undefined
269
+ }
270
+
271
+ const toActivityType = (kind: Classified | null): ActivityType => (kind ? (kind.type === 'unknown' ? 'contract' : kind.type) : 'other')
272
+
273
+ /** 一笔 getTransaction(json 编码)整理成 Activity,address 的角度;导出供测试和自己取到交易的场景使用 */
274
+ export function activityFromTransaction(address: string, tx: RawTransaction): Activity | null {
275
+ const meta = tx.meta
276
+ const hash = tx.transaction.signatures[0]
277
+ if (!meta || !hash) return null
278
+ const message = tx.transaction.message
279
+ const keys = [...message.accountKeys, ...(meta.loadedAddresses?.writable ?? []), ...(meta.loadedAddresses?.readonly ?? [])]
280
+ const failed = meta.err !== null && meta.err !== undefined
281
+ const ixs: DecodedInstruction[] = message.instructions.map((ix, i) => {
282
+ const programId = keys[ix.programIdIndex] as string
283
+ return decodeInstruction(i, programId, ix.accounts.map((a) => keys[a] as string), base58.decode(ix.data))
284
+ })
285
+
286
+ const index = keys.indexOf(address)
287
+ const isPayer = index === 0
288
+ const fee = BigInt(meta.fee)
289
+ let native = index >= 0 ? BigInt(meta.postBalances[index] ?? 0) - BigInt(meta.preBalances[index] ?? 0) + (isPayer ? fee : 0n) : 0n
290
+ const tokens = new Map<string, { delta: bigint; decimals: number }>()
291
+ const ownerOf = new Map<number, string>()
292
+ for (const [list, sign] of [[meta.preTokenBalances ?? [], -1n], [meta.postTokenBalances ?? [], 1n]] as const) {
293
+ for (const b of list) {
294
+ if (b.owner) ownerOf.set(b.accountIndex, b.owner)
295
+ if (b.owner !== address) continue
296
+ const prev = tokens.get(b.mint)
297
+ tokens.set(b.mint, { delta: (prev?.delta ?? 0n) + sign * BigInt(b.uiTokenAmount.amount), decimals: b.uiTokenAmount.decimals })
298
+ }
299
+ }
300
+ // Wrapped SOL 并入 SOL
301
+ const wsol = tokens.get(NATIVE_MINT)
302
+ if (wsol) {
303
+ native += wsol.delta
304
+ tokens.delete(NATIVE_MINT)
305
+ }
306
+ const changes = failed ? [] : toChanges(native, tokens)
307
+
308
+ // 对手方:第一笔转账的另一头(代币转账取代币账户的 owner)
309
+ let counterparty: string | undefined
310
+ for (const ix of ixs) {
311
+ if (ix.type !== 'transfer') continue
312
+ const args = ix.args as Record<string, string> | undefined
313
+ const owner = (account: string | undefined) => (account ? (ownerOf.get(keys.indexOf(account)) ?? (ix.programId === SYSTEM_PROGRAM_ID ? account : undefined)) : undefined)
314
+ const from = owner(args?.from ?? args?.source)
315
+ const to = owner(args?.to ?? args?.destination)
316
+ counterparty = from === address ? to : from
317
+ if (counterparty) break
318
+ }
319
+
320
+ const kind = classify(ixs, changes)
321
+ const type = toActivityType(kind)
322
+ const label = describe(kind, ixs, changes, counterparty)
323
+ return {
324
+ hash,
325
+ time: tx.blockTime ?? 0,
326
+ block: tx.slot,
327
+ status: failed ? 'failed' : 'success',
328
+ type,
329
+ direction: type === 'transfer' || type === 'other' ? directionOf(changes, counterparty, address) : undefined,
330
+ counterparty,
331
+ label: failed ? `Failed: ${label}` : label,
332
+ changes,
333
+ ...(isPayer ? { fee: fee.toString() } : {}),
334
+ protocol: kind?.protocol,
335
+ }
336
+ }