@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.
Files changed (220) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +129 -9
  3. package/dist/accountReader.d.ts +57 -0
  4. package/dist/accountReader.d.ts.map +1 -0
  5. package/dist/accountReader.js +149 -0
  6. package/dist/accountReader.js.map +1 -0
  7. package/dist/address.d.ts +9 -0
  8. package/dist/address.d.ts.map +1 -1
  9. package/dist/address.js +36 -6
  10. package/dist/address.js.map +1 -1
  11. package/dist/approvals.d.ts +5 -3
  12. package/dist/approvals.d.ts.map +1 -1
  13. package/dist/approvals.js +25 -36
  14. package/dist/approvals.js.map +1 -1
  15. package/dist/balances.d.ts +178 -0
  16. package/dist/balances.d.ts.map +1 -0
  17. package/dist/balances.js +524 -0
  18. package/dist/balances.js.map +1 -0
  19. package/dist/batchPlan.d.ts +52 -0
  20. package/dist/batchPlan.d.ts.map +1 -0
  21. package/dist/batchPlan.js +64 -0
  22. package/dist/batchPlan.js.map +1 -0
  23. package/dist/bytes.d.ts +31 -0
  24. package/dist/bytes.d.ts.map +1 -0
  25. package/dist/bytes.js +74 -0
  26. package/dist/bytes.js.map +1 -0
  27. package/dist/claim.d.ts +163 -0
  28. package/dist/claim.d.ts.map +1 -0
  29. package/dist/claim.js +197 -0
  30. package/dist/claim.js.map +1 -0
  31. package/dist/client.d.ts +26 -41
  32. package/dist/client.d.ts.map +1 -1
  33. package/dist/client.js +41 -986
  34. package/dist/client.js.map +1 -1
  35. package/dist/clientConfig.d.ts +11 -0
  36. package/dist/clientConfig.d.ts.map +1 -0
  37. package/dist/clientConfig.js +3 -0
  38. package/dist/clientConfig.js.map +1 -0
  39. package/dist/decode.d.ts +22 -0
  40. package/dist/decode.d.ts.map +1 -0
  41. package/dist/decode.js +160 -0
  42. package/dist/decode.js.map +1 -0
  43. package/dist/describe.d.ts +26 -0
  44. package/dist/describe.d.ts.map +1 -0
  45. package/dist/describe.js +58 -0
  46. package/dist/describe.js.map +1 -0
  47. package/dist/fees.d.ts.map +1 -1
  48. package/dist/fees.js +2 -12
  49. package/dist/fees.js.map +1 -1
  50. package/dist/history.d.ts +125 -0
  51. package/dist/history.d.ts.map +1 -0
  52. package/dist/history.js +220 -0
  53. package/dist/history.js.map +1 -0
  54. package/dist/historyHelius.d.ts +52 -0
  55. package/dist/historyHelius.d.ts.map +1 -0
  56. package/dist/historyHelius.js +125 -0
  57. package/dist/historyHelius.js.map +1 -0
  58. package/dist/index.d.ts +3 -3
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +1 -1
  61. package/dist/index.js.map +1 -1
  62. package/dist/layout.d.ts +21 -0
  63. package/dist/layout.d.ts.map +1 -1
  64. package/dist/layout.js +50 -69
  65. package/dist/layout.js.map +1 -1
  66. package/dist/liquidity/common.js +2 -2
  67. package/dist/liquidity/common.js.map +1 -1
  68. package/dist/liquidity/orca.d.ts +6 -0
  69. package/dist/liquidity/orca.d.ts.map +1 -1
  70. package/dist/liquidity/orca.js +14 -3
  71. package/dist/liquidity/orca.js.map +1 -1
  72. package/dist/liquidity/raydiumClmm.d.ts +6 -0
  73. package/dist/liquidity/raydiumClmm.d.ts.map +1 -1
  74. package/dist/liquidity/raydiumClmm.js +12 -5
  75. package/dist/liquidity/raydiumClmm.js.map +1 -1
  76. package/dist/lp.d.ts +55 -12
  77. package/dist/lp.d.ts.map +1 -1
  78. package/dist/lp.js +158 -70
  79. package/dist/lp.js.map +1 -1
  80. package/dist/lpMath.d.ts +22 -0
  81. package/dist/lpMath.d.ts.map +1 -1
  82. package/dist/lpMath.js +21 -0
  83. package/dist/lpMath.js.map +1 -1
  84. package/dist/portfolio.d.ts.map +1 -1
  85. package/dist/portfolio.js +3 -8
  86. package/dist/portfolio.js.map +1 -1
  87. package/dist/programs.d.ts +19 -0
  88. package/dist/programs.d.ts.map +1 -0
  89. package/dist/programs.js +175 -0
  90. package/dist/programs.js.map +1 -0
  91. package/dist/reclaim.d.ts +192 -0
  92. package/dist/reclaim.d.ts.map +1 -0
  93. package/dist/reclaim.js +399 -0
  94. package/dist/reclaim.js.map +1 -0
  95. package/dist/risk.js +2 -2
  96. package/dist/risk.js.map +1 -1
  97. package/dist/rpc.d.ts +30 -7
  98. package/dist/rpc.d.ts.map +1 -1
  99. package/dist/rpc.js +404 -111
  100. package/dist/rpc.js.map +1 -1
  101. package/dist/simulate.d.ts +106 -0
  102. package/dist/simulate.d.ts.map +1 -0
  103. package/dist/simulate.js +395 -0
  104. package/dist/simulate.js.map +1 -0
  105. package/dist/source.d.ts +8 -4
  106. package/dist/source.d.ts.map +1 -1
  107. package/dist/source.js +11 -4
  108. package/dist/source.js.map +1 -1
  109. package/dist/subpaths/approvals.d.ts +3 -3
  110. package/dist/subpaths/approvals.d.ts.map +1 -1
  111. package/dist/subpaths/approvals.js.map +1 -1
  112. package/dist/subpaths/claim.d.ts +31 -0
  113. package/dist/subpaths/claim.d.ts.map +1 -0
  114. package/dist/subpaths/claim.js +31 -0
  115. package/dist/subpaths/claim.js.map +1 -0
  116. package/dist/subpaths/history.d.ts +21 -0
  117. package/dist/subpaths/history.d.ts.map +1 -0
  118. package/dist/subpaths/history.js +22 -0
  119. package/dist/subpaths/history.js.map +1 -0
  120. package/dist/subpaths/lp.d.ts +2 -2
  121. package/dist/subpaths/lp.d.ts.map +1 -1
  122. package/dist/subpaths/lp.js +2 -2
  123. package/dist/subpaths/lp.js.map +1 -1
  124. package/dist/subpaths/reclaim.d.ts +34 -0
  125. package/dist/subpaths/reclaim.d.ts.map +1 -0
  126. package/dist/subpaths/reclaim.js +34 -0
  127. package/dist/subpaths/reclaim.js.map +1 -0
  128. package/dist/subpaths/simulate.d.ts +22 -0
  129. package/dist/subpaths/simulate.d.ts.map +1 -0
  130. package/dist/subpaths/simulate.js +23 -0
  131. package/dist/subpaths/simulate.js.map +1 -0
  132. package/dist/swap/index.js +3 -3
  133. package/dist/swap/index.js.map +1 -1
  134. package/dist/tokenDetails.d.ts +12 -0
  135. package/dist/tokenDetails.d.ts.map +1 -0
  136. package/dist/tokenDetails.js +196 -0
  137. package/dist/tokenDetails.js.map +1 -0
  138. package/dist/tokenMeta.d.ts +63 -0
  139. package/dist/tokenMeta.d.ts.map +1 -0
  140. package/dist/tokenMeta.js +226 -0
  141. package/dist/tokenMeta.js.map +1 -0
  142. package/dist/tradeCheck.js +3 -3
  143. package/dist/tradeCheck.js.map +1 -1
  144. package/dist/util.d.ts +22 -0
  145. package/dist/util.d.ts.map +1 -0
  146. package/dist/util.js +107 -0
  147. package/dist/util.js.map +1 -0
  148. package/dist/write/chain.d.ts +51 -0
  149. package/dist/write/chain.d.ts.map +1 -0
  150. package/dist/write/chain.js +184 -0
  151. package/dist/write/chain.js.map +1 -0
  152. package/dist/write/collect.js +2 -2
  153. package/dist/write/collect.js.map +1 -1
  154. package/dist/write/errors.d.ts +10 -0
  155. package/dist/write/errors.d.ts.map +1 -0
  156. package/dist/write/errors.js +77 -0
  157. package/dist/write/errors.js.map +1 -0
  158. package/dist/write/instructions.d.ts +2 -0
  159. package/dist/write/instructions.d.ts.map +1 -1
  160. package/dist/write/instructions.js +16 -0
  161. package/dist/write/instructions.js.map +1 -1
  162. package/dist/write/pack.d.ts +28 -0
  163. package/dist/write/pack.d.ts.map +1 -0
  164. package/dist/write/pack.js +125 -0
  165. package/dist/write/pack.js.map +1 -0
  166. package/dist/write/send.d.ts +23 -9
  167. package/dist/write/send.d.ts.map +1 -1
  168. package/dist/write/send.js +53 -351
  169. package/dist/write/send.js.map +1 -1
  170. package/dist/write/tracker.d.ts +24 -0
  171. package/dist/write/tracker.d.ts.map +1 -0
  172. package/dist/write/tracker.js +129 -0
  173. package/dist/write/tracker.js.map +1 -0
  174. package/package.json +21 -1
  175. package/src/accountReader.ts +187 -0
  176. package/src/address.ts +41 -6
  177. package/src/approvals.ts +36 -44
  178. package/src/balances.ts +678 -0
  179. package/src/batchPlan.ts +117 -0
  180. package/src/bytes.ts +89 -0
  181. package/src/claim.ts +327 -0
  182. package/src/client.ts +59 -1239
  183. package/src/clientConfig.ts +12 -0
  184. package/src/decode.ts +176 -0
  185. package/src/describe.ts +77 -0
  186. package/src/fees.ts +2 -9
  187. package/src/history.ts +325 -0
  188. package/src/historyHelius.ts +136 -0
  189. package/src/index.ts +13 -1
  190. package/src/layout.ts +63 -82
  191. package/src/liquidity/common.ts +2 -2
  192. package/src/liquidity/orca.ts +15 -5
  193. package/src/liquidity/raydiumClmm.ts +13 -5
  194. package/src/lp.ts +226 -75
  195. package/src/lpMath.ts +38 -0
  196. package/src/portfolio.ts +8 -11
  197. package/src/programs.ts +216 -0
  198. package/src/reclaim.ts +550 -0
  199. package/src/risk.ts +2 -2
  200. package/src/rpc.ts +476 -118
  201. package/src/simulate.ts +517 -0
  202. package/src/source.ts +14 -5
  203. package/src/subpaths/approvals.ts +3 -2
  204. package/src/subpaths/claim.ts +54 -0
  205. package/src/subpaths/history.ts +38 -0
  206. package/src/subpaths/lp.ts +5 -1
  207. package/src/subpaths/reclaim.ts +58 -0
  208. package/src/subpaths/simulate.ts +37 -0
  209. package/src/swap/index.ts +3 -3
  210. package/src/tokenDetails.ts +240 -0
  211. package/src/tokenMeta.ts +270 -0
  212. package/src/tradeCheck.ts +3 -3
  213. package/src/util.ts +110 -0
  214. package/src/write/chain.ts +218 -0
  215. package/src/write/collect.ts +2 -2
  216. package/src/write/errors.ts +82 -0
  217. package/src/write/instructions.ts +17 -0
  218. package/src/write/pack.ts +142 -0
  219. package/src/write/send.ts +74 -407
  220. package/src/write/tracker.ts +152 -0
@@ -0,0 +1,12 @@
1
+ import type { Cluster } from './constants.js'
2
+ import type { SourceOptions } from './source.js'
3
+
4
+ /** SolanaClient 的节点配置(网络、节点选项、期望的创世哈希),按客户端登记:写操作的 sendRpc 按同样的配置解析、校验网络 */
5
+ export interface ClientSourceConfig {
6
+ cluster: Cluster | undefined
7
+ options: SourceOptions
8
+ genesis: () => Promise<string>
9
+ }
10
+
11
+ /** 单独成模块:/write 只需查表,不用静态带上 source.ts(sendRpc 解析时才按需加载) */
12
+ export const clientSourceConfigs = new WeakMap<object, ClientSourceConfig>()
package/src/decode.ts ADDED
@@ -0,0 +1,176 @@
1
+ /**
2
+ * 指令解码(/simulate 和 /history 共用):System、SPL Token / Token-2022、关联代币账户、计算预算、Memo、升级程序加载器,
3
+ * 以及 programs.ts 里认得的 DEX / 聚合器(协议 + 指令名 + 大类)。其他程序只给程序名(认得时)和大类 contract / unknown
4
+ */
5
+ import { Bytes } from './bytes.js'
6
+ import { ASSOCIATED_TOKEN_PROGRAM_ID, SYSTEM_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from './constants.js'
7
+ import { BPF_LOADER_UPGRADEABLE, COMPUTE_BUDGET, MEMO_V1, MEMO_V2, PROGRAMS, knownMethod, type IxType } from './programs.js'
8
+
9
+ export interface DecodedInstruction {
10
+ /** 在交易里的下标(顶层指令) */
11
+ index: number
12
+ programId: string
13
+ /** 程序名;不认识时为 null */
14
+ program: string | null
15
+ type: IxType
16
+ protocol?: string
17
+ method?: string
18
+ summary: string
19
+ /** 金额都是最小单位的十进制字符串 */
20
+ args?: Record<string, unknown>
21
+ }
22
+
23
+ /** Token 的 SetAuthority 的权限类型 */
24
+ export const AUTHORITY_TYPES = [
25
+ 'MintTokens', 'FreezeAccount', 'AccountOwner', 'CloseAccount', 'TransferFeeConfig', 'WithheldWithdraw', 'CloseMint', 'InterestRate',
26
+ 'PermanentDelegate', 'ConfidentialTransferMint', 'TransferHookProgramId', 'ConfidentialTransferFeeConfig', 'MetadataPointer', 'GroupPointer', 'GroupMemberPointer',
27
+ ] as const
28
+
29
+ const short = (a: string | undefined) => (a ? `${a.slice(0, 4)}…${a.slice(-4)}` : '?')
30
+
31
+ /**
32
+ * 解码一条指令。accounts 是指令用到的账户地址(已按指令里的下标解析,包含地址查找表里的),data 是指令数据。
33
+ * 数据格式不对时不抛错,按“不认识的指令”返回
34
+ */
35
+ export function decodeInstruction(index: number, programId: string, accounts: readonly string[], data: Uint8Array): DecodedInstruction {
36
+ const info = PROGRAMS[programId]
37
+ const base = { index, programId, program: info?.name ?? null }
38
+ try {
39
+ const decoded = decodeKnown(programId, accounts, data)
40
+ if (decoded) return { ...base, ...decoded }
41
+ const known = knownMethod(programId, data)
42
+ if (known) {
43
+ return { ...base, type: known[1], protocol: info?.protocol, method: known[0], summary: `${info?.protocol ?? info?.name} ${known[0]}` }
44
+ }
45
+ } catch {
46
+ // 数据不符合格式:当作不认识
47
+ }
48
+ if (info) return { ...base, type: 'contract', protocol: info.protocol, summary: `Call ${info.name}` }
49
+ return { ...base, type: 'unknown', summary: `Call unknown program ${short(programId)}` }
50
+ }
51
+
52
+ type Partial_ = Pick<DecodedInstruction, 'type' | 'summary'> & Partial<Pick<DecodedInstruction, 'method' | 'args' | 'protocol'>>
53
+
54
+ function decodeKnown(programId: string, a: readonly string[], data: Uint8Array): Partial_ | null {
55
+ const d = new Bytes(data)
56
+ switch (programId) {
57
+ case SYSTEM_PROGRAM_ID:
58
+ return decodeSystem(d, a)
59
+ case TOKEN_PROGRAM_ID:
60
+ case TOKEN_2022_PROGRAM_ID:
61
+ return decodeToken(d, a)
62
+ case ASSOCIATED_TOKEN_PROGRAM_ID: {
63
+ const method = data.length === 0 || data[0] === 0 ? 'create' : data[0] === 1 ? 'createIdempotent' : 'recoverNested'
64
+ return { type: 'setup', method, summary: `Create token account for ${short(a[2])}`, args: { payer: a[0], account: a[1], owner: a[2], mint: a[3] } }
65
+ }
66
+ case COMPUTE_BUDGET: {
67
+ const tag = d.u8()
68
+ if (tag === 2) return { type: 'budget', method: 'setComputeUnitLimit', summary: 'Set compute unit limit', args: { units: d.u32() } }
69
+ if (tag === 3) return { type: 'budget', method: 'setComputeUnitPrice', summary: 'Set priority fee', args: { microLamports: d.u64().toString() } }
70
+ return { type: 'budget', summary: 'Compute budget' }
71
+ }
72
+ case MEMO_V1:
73
+ case MEMO_V2:
74
+ return { type: 'memo', method: 'memo', summary: 'Memo', args: { text: new TextDecoder().decode(data) } }
75
+ case BPF_LOADER_UPGRADEABLE: {
76
+ const tag = d.u32()
77
+ const method = ['initializeBuffer', 'write', 'deploy', 'upgrade', 'setAuthority', 'close', 'extendProgram', 'setAuthorityChecked'][tag]
78
+ return { type: 'contract', method, summary: `Program loader: ${method ?? `instruction ${tag}`}`, args: { program: a[tag === 3 ? 1 : 0] } }
79
+ }
80
+ default:
81
+ return null
82
+ }
83
+ }
84
+
85
+ function decodeSystem(d: Bytes, a: readonly string[]): Partial_ {
86
+ const tag = d.u32()
87
+ switch (tag) {
88
+ case 0: {
89
+ const lamports = d.u64()
90
+ const space = d.u64()
91
+ return { type: 'setup', method: 'createAccount', summary: `Create account ${short(a[1])}`, args: { from: a[0], account: a[1], lamports: lamports.toString(), space: space.toString(), owner: d.key() } }
92
+ }
93
+ case 1: {
94
+ const owner = d.key()
95
+ return { type: 'approve', method: 'assign', summary: `Assign ${short(a[0])} to program ${short(owner)}`, args: { account: a[0], owner } }
96
+ }
97
+ case 2: {
98
+ const lamports = d.u64()
99
+ return { type: 'transfer', method: 'transfer', summary: `Transfer SOL to ${short(a[1])}`, args: { from: a[0], to: a[1], lamports: lamports.toString() } }
100
+ }
101
+ case 11: {
102
+ const lamports = d.u64()
103
+ return { type: 'transfer', method: 'transferWithSeed', summary: `Transfer SOL to ${short(a[2])}`, args: { from: a[0], to: a[2], lamports: lamports.toString() } }
104
+ }
105
+ case 5: {
106
+ const lamports = d.u64()
107
+ return { type: 'transfer', method: 'withdrawNonce', summary: `Withdraw nonce account to ${short(a[1])}`, args: { from: a[0], to: a[1], lamports: lamports.toString() } }
108
+ }
109
+ case 10:
110
+ return { type: 'approve', method: 'assignWithSeed', summary: `Assign ${short(a[0])} to another program`, args: { account: a[0] } }
111
+ default:
112
+ return { type: 'setup', summary: 'System instruction', method: ['createAccount', 'assign', 'transfer', 'createAccountWithSeed', 'advanceNonce', 'withdrawNonce', 'initializeNonce', 'authorizeNonce', 'allocate', 'allocateWithSeed'][tag] }
113
+ }
114
+ }
115
+
116
+ function decodeToken(d: Bytes, a: readonly string[]): Partial_ {
117
+ const tag = d.u8()
118
+ const amountArgs = (extra: Record<string, unknown>, amount: bigint) => ({ ...extra, amount: amount.toString() })
119
+ switch (tag) {
120
+ case 3: {
121
+ const amount = d.u64()
122
+ return { type: 'transfer', method: 'transfer', summary: `Transfer tokens to ${short(a[1])}`, args: amountArgs({ source: a[0], destination: a[1], authority: a[2] }, amount) }
123
+ }
124
+ case 12: {
125
+ const amount = d.u64()
126
+ return { type: 'transfer', method: 'transferChecked', summary: `Transfer tokens to ${short(a[2])}`, args: amountArgs({ source: a[0], mint: a[1], destination: a[2], authority: a[3] }, amount) }
127
+ }
128
+ case 26: {
129
+ const sub = d.u8()
130
+ if (sub === 1) {
131
+ const amount = d.u64()
132
+ return { type: 'transfer', method: 'transferCheckedWithFee', summary: `Transfer tokens to ${short(a[2])}`, args: amountArgs({ source: a[0], mint: a[1], destination: a[2], authority: a[3] }, amount) }
133
+ }
134
+ return { type: 'setup', method: 'transferFeeExtension', summary: 'Token-2022 transfer fee instruction' }
135
+ }
136
+ case 4: {
137
+ const amount = d.u64()
138
+ return { type: 'approve', method: 'approve', summary: `Approve ${short(a[1])} to spend tokens`, args: amountArgs({ account: a[0], delegate: a[1], owner: a[2] }, amount) }
139
+ }
140
+ case 13: {
141
+ const amount = d.u64()
142
+ return { type: 'approve', method: 'approveChecked', summary: `Approve ${short(a[2])} to spend tokens`, args: amountArgs({ account: a[0], mint: a[1], delegate: a[2], owner: a[3] }, amount) }
143
+ }
144
+ case 5:
145
+ return { type: 'approve', method: 'revoke', summary: 'Revoke token delegate', args: { account: a[0], owner: a[1] } }
146
+ case 6: {
147
+ const authorityType = AUTHORITY_TYPES[d.u8()] ?? 'Unknown'
148
+ const newAuthority = d.u8() === 1 ? d.key() : null
149
+ return {
150
+ type: 'approve',
151
+ method: 'setAuthority',
152
+ summary: newAuthority ? `Set ${authorityType} authority to ${short(newAuthority)}` : `Remove ${authorityType} authority`,
153
+ args: { account: a[0], authorityType, newAuthority },
154
+ }
155
+ }
156
+ case 7:
157
+ case 14: {
158
+ const amount = d.u64()
159
+ return { type: 'mint', method: tag === 7 ? 'mintTo' : 'mintToChecked', summary: `Mint tokens to ${short(a[1])}`, args: amountArgs({ mint: a[0], destination: a[1] }, amount) }
160
+ }
161
+ case 8:
162
+ case 15: {
163
+ const amount = d.u64()
164
+ return { type: 'burn', method: tag === 8 ? 'burn' : 'burnChecked', summary: 'Burn tokens', args: amountArgs({ account: a[0], mint: a[1] }, amount) }
165
+ }
166
+ case 9:
167
+ return { type: 'close', method: 'closeAccount', summary: `Close token account ${short(a[0])}`, args: { account: a[0], destination: a[1], owner: a[2] } }
168
+ case 17:
169
+ return { type: 'setup', method: 'syncNative', summary: 'Sync wrapped SOL', args: { account: a[0] } }
170
+ case 10:
171
+ case 11:
172
+ return { type: 'contract', method: tag === 10 ? 'freezeAccount' : 'thawAccount', summary: tag === 10 ? 'Freeze token account' : 'Thaw token account', args: { account: a[0] } }
173
+ default:
174
+ return { type: 'setup', summary: tag <= 25 ? 'Initialize token account / mint' : 'Token-2022 extension instruction' }
175
+ }
176
+ }
@@ -0,0 +1,77 @@
1
+ /** 交易的类型和一句话说明(/simulate 和 /history 共用):指令的大类 + 钱包的余额变化 */
2
+ import type { DecodedInstruction } from './decode.js'
3
+ import { PROGRAMS } from './programs.js'
4
+
5
+ export interface AssetDelta {
6
+ asset: string
7
+ symbol: string
8
+ decimals: number
9
+ /** 带符号的最小单位十进制字符串 */
10
+ delta: string
11
+ formatted: string
12
+ }
13
+
14
+ export type TxKind = 'transfer' | 'swap' | 'approve' | 'liquidity' | 'mint' | 'contract' | 'unknown'
15
+
16
+ export interface Classified {
17
+ type: TxKind
18
+ protocol?: string
19
+ method?: string
20
+ /** 没有认得的指令、按余额变化(一个资产减少、另一个增加)推断为 swap */
21
+ inferred?: boolean
22
+ }
23
+
24
+ const short = (a: string) => `${a.slice(0, 4)}…${a.slice(-4)}`
25
+
26
+ /**
27
+ * 判断类型:先看认得的指令(流动性 > 兑换 > 授权 > 铸造),再看是不是只有转账;有不认得的程序时,
28
+ * 钱包一个资产减少、另一个增加就推断为兑换,否则是合约调用(程序认得)或未知。没有任何有意义的指令(只有建账户、关闭等)时为 null
29
+ */
30
+ export function classify(ixs: readonly DecodedInstruction[], changes: readonly Pick<AssetDelta, 'asset' | 'delta'>[]): Classified | null {
31
+ const pick = (type: DecodedInstruction['type']) => ixs.find((ix) => ix.type === type)
32
+ const result = (ix: DecodedInstruction, type: TxKind): Classified => ({ type, protocol: ix.protocol, method: ix.method })
33
+ for (const type of ['liquidity', 'swap', 'approve', 'mint'] as const) {
34
+ const ix = pick(type)
35
+ if (ix) return result(ix, type)
36
+ }
37
+ const others = ixs.filter((ix) => ix.type === 'contract' || ix.type === 'unknown')
38
+ const out = changes.filter((c) => c.delta.startsWith('-'))
39
+ const received = changes.filter((c) => !c.delta.startsWith('-') && c.delta !== '0')
40
+ if (others.length) {
41
+ const first = others.find((ix) => ix.protocol) ?? others[0]!
42
+ if (out.length && received.some((r) => out.some((o) => o.asset !== r.asset))) return { type: 'swap', protocol: first.protocol, inferred: true }
43
+ return { type: others.some((ix) => PROGRAMS[ix.programId]) ? 'contract' : 'unknown', protocol: first.protocol, method: first.method }
44
+ }
45
+ const transfer = pick('transfer')
46
+ if (transfer) return result(transfer, 'transfer')
47
+ return null
48
+ }
49
+
50
+ const amount = (c: AssetDelta) => `${c.formatted.replace(/^-/, '')} ${c.symbol}`
51
+ const join = (list: readonly AssetDelta[]) => list.map(amount).join(' + ')
52
+
53
+ /** 一句话说明,如 “Swap 1 SOL for 150.2 USDC on Jupiter” */
54
+ export function describe(kind: Classified | null, ixs: readonly DecodedInstruction[], changes: readonly AssetDelta[], counterparty?: string): string {
55
+ const out = changes.filter((c) => c.delta.startsWith('-'))
56
+ const received = changes.filter((c) => !c.delta.startsWith('-') && c.delta !== '0')
57
+ const via = kind?.protocol ? ` on ${kind.protocol}` : ''
58
+ switch (kind?.type) {
59
+ case 'swap':
60
+ return out.length && received.length ? `Swap ${join(out)} for ${join(received)}${via}` : `Swap${via}`
61
+ case 'transfer':
62
+ if (out.length && !received.length) return `Send ${join(out)}${counterparty ? ` to ${short(counterparty)}` : ''}`
63
+ if (received.length && !out.length) return `Receive ${join(received)}${counterparty ? ` from ${short(counterparty)}` : ''}`
64
+ return ixs.find((ix) => ix.type === 'transfer')?.summary ?? 'Transfer'
65
+ case 'liquidity':
66
+ return `${kind.method ? kind.method.replace(/_/g, ' ') : 'Liquidity'}${via}${out.length ? `: pay ${join(out)}` : ''}${received.length ? `${out.length ? ',' : ':'} receive ${join(received)}` : ''}`
67
+ case 'approve':
68
+ return ixs.find((ix) => ix.type === 'approve')?.summary ?? 'Approve'
69
+ case 'mint':
70
+ return `Mint${received.length ? `: receive ${join(received)}` : ''}${via}`
71
+ case 'contract':
72
+ case 'unknown':
73
+ return `${ixs.find((ix) => ix.type === kind.type)?.summary ?? 'Call program'}${out.length ? `, pay ${join(out)}` : ''}${received.length ? `, receive ${join(received)}` : ''}`
74
+ default:
75
+ return ixs.find((ix) => ix.type === 'close')?.summary ?? (changes.length ? `Balance change: ${join(changes)}` : 'No balance change')
76
+ }
77
+ }
package/src/fees.ts CHANGED
@@ -7,7 +7,7 @@ import { base64 } from '@scure/base'
7
7
  import { encodeAddress } from './address.js'
8
8
  import type { RequestOptions, SolanaClient } from './client.js'
9
9
  import { SOL_DECIMALS, SYSTEM_PROGRAM_ID } from './constants.js'
10
- import { formatUnits } from './layout.js'
10
+ import { computeBudgetOf, formatUnits } from './layout.js'
11
11
  import type { InstructionInput } from './write/transaction.js'
12
12
 
13
13
  export type FeeLevel = 'slow' | 'normal' | 'fast'
@@ -156,14 +156,7 @@ export async function estimateCost(client: SolanaClient, input: CostInput, optio
156
156
  raw = base64.encode(bytes)
157
157
  signatures = message.signatures
158
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
- }
159
+ const { limit, price } = computeBudgetOf(message.instructions)
167
160
  const others = message.instructions.filter((ix) => ix.programId !== COMPUTE_BUDGET_PROGRAM_ID).length
168
161
  budget = { limit: Math.min(MAX_COMPUTE_UNITS, limit ?? DEFAULT_UNITS_PER_INSTRUCTION * others), price }
169
162
  } else {
package/src/history.ts ADDED
@@ -0,0 +1,325 @@
1
+ /**
2
+ * 交易历史(子路径 `@w3lib/solana-batch-call/history`):把地址的交易整理成统一的 Activity(类型、方向、对手方、资产变化、手续费)。
3
+ * 数据源可以换:默认 rpcHistory(getSignaturesForAddress 翻页 + getTransaction,免费节点就能用,节点失败自动切换),
4
+ * 也可以传 heliusHistory 等;传多个时按顺序尝试。游标就是最后一条签名,换数据源也能接着翻
5
+ */
6
+ import { base58 } from '@scure/base'
7
+
8
+ import { InvalidAddressError, 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
+ import { mapPool } from './util.js'
15
+
16
+ export type ActivityType = 'transfer' | 'swap' | 'approve' | 'liquidity' | 'mint' | 'contract' | 'other'
17
+
18
+ export interface ActivityChange extends AssetDelta {}
19
+
20
+ export interface Activity {
21
+ /** 交易签名 */
22
+ hash: string
23
+ /** 区块时间(unix 秒);节点没有给时为 0 */
24
+ time: number
25
+ /** slot */
26
+ block?: number
27
+ status: 'success' | 'failed'
28
+ type: ActivityType
29
+ /** 对这个地址来说:资产只增加 in、只减少 out、转给自己 self;兑换等有增有减时不填 */
30
+ direction?: 'in' | 'out' | 'self'
31
+ /** 转账的对手方 */
32
+ counterparty?: string
33
+ /** 一句话说明,如 “Swap 1 SOL for 150 USDC on Jupiter” */
34
+ label?: string
35
+ /** 这个地址的资产变化(SOL 不含手续费,Wrapped SOL 并入 SOL;asset 原生为 'native',其他是 mint) */
36
+ changes: ActivityChange[]
37
+ /** 这个地址付的手续费(lamports,十进制字符串);不是手续费支付者时不填 */
38
+ fee?: string
39
+ protocol?: string
40
+ }
41
+
42
+ export interface HistoryPage {
43
+ items: Activity[]
44
+ /** 还有更多时:下一页的游标(最后一条处理过的签名);没有更多时不返回 */
45
+ nextCursor?: string
46
+ }
47
+
48
+ export interface HistoryContext {
49
+ client: SolanaClient
50
+ address: string
51
+ limit: number
52
+ cursor?: string
53
+ /** 只要这个时间(unix 秒)之前 / 之后的 */
54
+ before?: number
55
+ after?: number
56
+ types?: readonly ActivityType[]
57
+ fetch: typeof fetch
58
+ signal?: AbortSignal
59
+ }
60
+
61
+ /** 历史数据源:返回统一格式的 Activity(代币的 symbol 先用缩写的 mint,getHistory 统一补全);游标是签名 */
62
+ export interface HistorySource {
63
+ readonly name: string
64
+ getHistory(ctx: HistoryContext): Promise<HistoryPage>
65
+ }
66
+
67
+ export interface HistoryOptions extends RequestOptions {
68
+ /** 每页条数,默认 20,最多 100 */
69
+ limit?: number
70
+ /** 上一页的 nextCursor */
71
+ cursor?: string
72
+ /** 区块时间上限(不含)/ 下限(含),unix 秒 */
73
+ before?: number
74
+ after?: number
75
+ /** 只要这些类型(不够 limit 时会往后多找,最多看 limit × 5 笔) */
76
+ types?: readonly ActivityType[]
77
+ /** 数据源,默认 rpcHistory();传数组时按顺序尝试,前一个失败换下一个 */
78
+ source?: HistorySource | readonly HistorySource[]
79
+ /** 补全代币符号(读 mint 的元数据),默认 true */
80
+ symbols?: boolean
81
+ fetch?: typeof fetch
82
+ }
83
+
84
+ export const MAX_HISTORY_LIMIT = 100
85
+ /** getTransaction 支持的交易版本:主网已经有 v1 格式的交易(2026-10),只给 0 时遇到它们整个请求报错(-32015);json 编码下 v1 的账户和指令字段与 v0 相同 */
86
+ const MAX_TX_VERSION = 1
87
+
88
+ const short = (a: string) => `${a.slice(0, 4)}…${a.slice(-4)}`
89
+
90
+ /**
91
+ * 查地址的交易历史。
92
+ *
93
+ * ```ts
94
+ * const page = await history(client, address, { limit: 20 })
95
+ * const next = await history(client, address, { cursor: page.nextCursor })
96
+ * ```
97
+ */
98
+ export async function history(client: SolanaClient, address: string, options: HistoryOptions = {}): Promise<HistoryPage> {
99
+ if (!isAddress(address)) throw new InvalidAddressError(`Invalid address: ${address}`, address)
100
+ const limit = options.limit ?? 20
101
+ if (!Number.isInteger(limit) || limit < 1 || limit > MAX_HISTORY_LIMIT) throw new RangeError(`limit must be an integer between 1 and ${MAX_HISTORY_LIMIT}`)
102
+ const sources = options.source === undefined ? [rpcHistory()] : Array.isArray(options.source) ? options.source : [options.source as HistorySource]
103
+ if (!sources.length) throw new Error('No history source')
104
+ const ctx: HistoryContext = {
105
+ client,
106
+ address,
107
+ limit,
108
+ cursor: options.cursor,
109
+ before: options.before,
110
+ after: options.after,
111
+ types: options.types?.length ? options.types : undefined,
112
+ fetch: options.fetch ?? ((...args) => fetch(...args)),
113
+ signal: options.signal,
114
+ }
115
+ const errors: string[] = []
116
+ for (const source of sources) {
117
+ try {
118
+ const page = await (source as HistorySource).getHistory(ctx)
119
+ if (options.symbols !== false) await fillSymbols(client, page.items, options.signal)
120
+ return page
121
+ } catch (err) {
122
+ if (options.signal?.aborted) throw err
123
+ errors.push(`${(source as HistorySource).name}: ${err instanceof Error ? err.message : String(err)}`)
124
+ }
125
+ }
126
+ throw new Error(`All history sources failed (${errors.join('; ')})`)
127
+ }
128
+
129
+ /** 补全还是缩写 mint 的 symbol(一次 tokens 查询,label 里的同步替换);读不到的用缩写的 mint */
130
+ async function fillSymbols(client: SolanaClient, items: Activity[], signal: AbortSignal | undefined): Promise<void> {
131
+ const pending = (c: ActivityChange) => c.symbol === short(c.asset)
132
+ const missing = [...new Set(items.flatMap((a) => a.changes.filter(pending).map((c) => c.asset)))]
133
+ if (!missing.length) return
134
+ const details = await client.tokens(missing, { fields: ['symbol'], signal }).catch(() => null)
135
+ const symbols = new Map(missing.map((m, i) => [m, details?.[i]?.symbol ?? short(m)]))
136
+ for (const a of items) {
137
+ for (const c of a.changes) {
138
+ if (!pending(c)) continue
139
+ const symbol = symbols.get(c.asset) as string
140
+ if (a.label) a.label = a.label.split(c.symbol).join(symbol)
141
+ c.symbol = symbol
142
+ }
143
+ }
144
+ }
145
+
146
+ // ---------------------------------------------------------------------------
147
+ // RPC 数据源
148
+ // ---------------------------------------------------------------------------
149
+
150
+ export interface RpcHistoryOptions {
151
+ /** 同时取多少笔交易的详情,默认 6(公共节点限频很严,节点自己会对 429 退避重试) */
152
+ concurrency?: number
153
+ /** 一次翻页最多检查多少笔签名(含被 types / 时间过滤掉的),默认 limit × 5,最多 500 */
154
+ maxScan?: number
155
+ }
156
+
157
+ interface RawTransaction {
158
+ slot: number
159
+ blockTime: number | null
160
+ meta: {
161
+ err: unknown
162
+ fee: number
163
+ preBalances: number[]
164
+ postBalances: number[]
165
+ preTokenBalances?: RawTokenBalance[]
166
+ postTokenBalances?: RawTokenBalance[]
167
+ loadedAddresses?: { writable: string[]; readonly: string[] }
168
+ } | null
169
+ transaction: {
170
+ signatures: string[]
171
+ message: { accountKeys: string[]; instructions: Array<{ programIdIndex: number; accounts: number[]; data: string }> }
172
+ }
173
+ }
174
+
175
+ interface RawTokenBalance {
176
+ accountIndex: number
177
+ mint: string
178
+ owner?: string
179
+ uiTokenAmount: { amount: string; decimals: number }
180
+ }
181
+
182
+ interface SignatureInfo {
183
+ signature: string
184
+ slot: number
185
+ err: unknown
186
+ blockTime: number | null
187
+ }
188
+
189
+
190
+ /**
191
+ * 默认数据源:getSignaturesForAddress(用 before 翻页)+ getTransaction(逐笔,json 编码、支持 v0)。
192
+ * 先按区块时间过滤签名再取详情,只取需要的笔数;失败的交易也列出(status 为 failed)。每页的请求数 ≈ limit + 1
193
+ */
194
+ export function rpcHistory(options: RpcHistoryOptions = {}): HistorySource {
195
+ const concurrency = options.concurrency ?? 6
196
+ return {
197
+ name: 'rpc',
198
+ async getHistory(ctx) {
199
+ const { client, address, limit, signal } = ctx
200
+ const maxScan = Math.min(options.maxScan ?? limit * 5, 500)
201
+ const items: Activity[] = []
202
+ let cursor = ctx.cursor
203
+ let scanned = 0
204
+ let more = true
205
+ while (items.length < limit && scanned < maxScan && more) {
206
+ const pageSize = Math.min(1000, Math.max(limit, 20) * (ctx.types ? 3 : 1))
207
+ const commitment = client.commitment === 'processed' ? 'confirmed' : client.commitment
208
+ const sigs = await client.request<SignatureInfo[]>('getSignaturesForAddress', [address, { limit: pageSize, ...(cursor ? { before: cursor } : {}), commitment }], { signal })
209
+ if (!sigs.length) {
210
+ more = false
211
+ break
212
+ }
213
+ // 按时间过滤:比 before 新的跳过;比 after 旧的之后都不用看了(签名按从新到旧排列)
214
+ let rows = sigs.filter((s) => ctx.before === undefined || s.blockTime === null || s.blockTime < ctx.before)
215
+ const old = ctx.after === undefined ? -1 : rows.findIndex((s) => s.blockTime !== null && s.blockTime < (ctx.after as number))
216
+ if (old >= 0) rows = rows.slice(0, old)
217
+ let i = 0
218
+ while (i < rows.length && items.length < limit && scanned < maxScan) {
219
+ const chunk = rows.slice(i, i + Math.max(1, Math.min(limit - items.length, maxScan - scanned)))
220
+ const txs = await mapPool(chunk, concurrency, (row) => client.request<RawTransaction | null>('getTransaction', [row.signature, { encoding: 'json', maxSupportedTransactionVersion: MAX_TX_VERSION, commitment }], { signal }))
221
+ for (const [j, tx] of txs.entries()) {
222
+ i++
223
+ scanned++
224
+ cursor = (chunk[j] as SignatureInfo).signature
225
+ const activity = tx ? activityFromTransaction(address, tx) : null
226
+ if (activity && (!ctx.types || ctx.types.includes(activity.type))) items.push(activity)
227
+ if (items.length >= limit) break
228
+ }
229
+ }
230
+ if (i >= rows.length) {
231
+ // 这一页看完了:接着从这一页最后一条往后翻;没有更多(不足一页、或已经到 after)时结束
232
+ cursor = (sigs[sigs.length - 1] as SignatureInfo).signature
233
+ more = sigs.length >= pageSize && old < 0
234
+ }
235
+ }
236
+ return { items, ...(more && cursor ? { nextCursor: cursor } : {}) }
237
+ },
238
+ }
239
+ }
240
+
241
+ const SOL: Pick<AssetDelta, 'asset' | 'symbol' | 'decimals'> = { asset: 'native', symbol: 'SOL', decimals: SOL_DECIMALS }
242
+
243
+ /** 合并资产变化:同一资产相加,去掉 0 */
244
+ function toChanges(native: bigint, tokens: Map<string, { delta: bigint; decimals: number }>): ActivityChange[] {
245
+ const out: ActivityChange[] = []
246
+ if (native !== 0n) out.push({ ...SOL, delta: native.toString(), formatted: formatUnits(native, SOL_DECIMALS) })
247
+ for (const [asset, { delta, decimals }] of tokens) {
248
+ if (delta !== 0n) out.push({ asset, symbol: short(asset), decimals, delta: delta.toString(), formatted: formatUnits(delta, decimals) })
249
+ }
250
+ return out
251
+ }
252
+
253
+ function directionOf(changes: readonly ActivityChange[], counterparty: string | undefined, address: string): Activity['direction'] {
254
+ if (!changes.length) return counterparty === address ? 'self' : undefined
255
+ const negative = changes.filter((c) => c.delta.startsWith('-')).length
256
+ if (negative === 0) return 'in'
257
+ return negative === changes.length ? 'out' : undefined
258
+ }
259
+
260
+ const toActivityType = (kind: Classified | null): ActivityType => (kind ? (kind.type === 'unknown' ? 'contract' : kind.type) : 'other')
261
+
262
+ /** 一笔 getTransaction(json 编码)整理成 Activity,address 的角度;导出供测试和自己取到交易的场景使用 */
263
+ export function activityFromTransaction(address: string, tx: RawTransaction): Activity | null {
264
+ const meta = tx.meta
265
+ const hash = tx.transaction.signatures[0]
266
+ if (!meta || !hash) return null
267
+ const message = tx.transaction.message
268
+ const keys = [...message.accountKeys, ...(meta.loadedAddresses?.writable ?? []), ...(meta.loadedAddresses?.readonly ?? [])]
269
+ const failed = meta.err !== null && meta.err !== undefined
270
+ const ixs: DecodedInstruction[] = message.instructions.map((ix, i) => {
271
+ const programId = keys[ix.programIdIndex] as string
272
+ return decodeInstruction(i, programId, ix.accounts.map((a) => keys[a] as string), base58.decode(ix.data))
273
+ })
274
+
275
+ const index = keys.indexOf(address)
276
+ const isPayer = index === 0
277
+ const fee = BigInt(meta.fee)
278
+ let native = index >= 0 ? BigInt(meta.postBalances[index] ?? 0) - BigInt(meta.preBalances[index] ?? 0) + (isPayer ? fee : 0n) : 0n
279
+ const tokens = new Map<string, { delta: bigint; decimals: number }>()
280
+ const ownerOf = new Map<number, string>()
281
+ for (const [list, sign] of [[meta.preTokenBalances ?? [], -1n], [meta.postTokenBalances ?? [], 1n]] as const) {
282
+ for (const b of list) {
283
+ if (b.owner) ownerOf.set(b.accountIndex, b.owner)
284
+ if (b.owner !== address) continue
285
+ const prev = tokens.get(b.mint)
286
+ tokens.set(b.mint, { delta: (prev?.delta ?? 0n) + sign * BigInt(b.uiTokenAmount.amount), decimals: b.uiTokenAmount.decimals })
287
+ }
288
+ }
289
+ // Wrapped SOL 并入 SOL
290
+ const wsol = tokens.get(NATIVE_MINT)
291
+ if (wsol) {
292
+ native += wsol.delta
293
+ tokens.delete(NATIVE_MINT)
294
+ }
295
+ const changes = failed ? [] : toChanges(native, tokens)
296
+
297
+ // 对手方:第一笔转账的另一头(代币转账取代币账户的 owner)
298
+ let counterparty: string | undefined
299
+ for (const ix of ixs) {
300
+ if (ix.type !== 'transfer') continue
301
+ const args = ix.args as Record<string, string> | undefined
302
+ const owner = (account: string | undefined) => (account ? (ownerOf.get(keys.indexOf(account)) ?? (ix.programId === SYSTEM_PROGRAM_ID ? account : undefined)) : undefined)
303
+ const from = owner(args?.from ?? args?.source)
304
+ const to = owner(args?.to ?? args?.destination)
305
+ counterparty = from === address ? to : from
306
+ if (counterparty) break
307
+ }
308
+
309
+ const kind = classify(ixs, changes)
310
+ const type = toActivityType(kind)
311
+ const label = describe(kind, ixs, changes, counterparty)
312
+ return {
313
+ hash,
314
+ time: tx.blockTime ?? 0,
315
+ block: tx.slot,
316
+ status: failed ? 'failed' : 'success',
317
+ type,
318
+ direction: type === 'transfer' || type === 'other' ? directionOf(changes, counterparty, address) : undefined,
319
+ counterparty,
320
+ label: failed ? `Failed: ${label}` : label,
321
+ changes,
322
+ ...(isPayer ? { fee: fee.toString() } : {}),
323
+ protocol: kind?.protocol,
324
+ }
325
+ }