@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,136 @@
1
+ /** Helius 增强交易历史数据源(需要应用自己的 API key):把 `/v0/addresses/{address}/transactions` 的结果整理成和 rpcHistory 一样的 Activity */
2
+ import { NATIVE_MINT, SOL_DECIMALS } from './constants.js'
3
+ import { formatUnits } from './layout.js'
4
+ import type { Activity, ActivityChange, ActivityType, HistorySource } from './history.js'
5
+
6
+ export interface HeliusHistoryOptions {
7
+ apiKey: string
8
+ /** 默认 https://api.helius.xyz */
9
+ baseUrl?: string
10
+ }
11
+
12
+ interface HeliusItem {
13
+ signature: string
14
+ timestamp?: number
15
+ slot?: number
16
+ fee?: number
17
+ feePayer?: string
18
+ type?: string
19
+ source?: string
20
+ description?: string
21
+ transactionError?: unknown
22
+ nativeTransfers?: Array<{ fromUserAccount: string; toUserAccount: string; amount: number }>
23
+ tokenTransfers?: Array<{ fromUserAccount: string | null; toUserAccount: string | null; tokenAmount: number; mint: string }>
24
+ accountData?: Array<{
25
+ account: string
26
+ nativeBalanceChange?: number
27
+ tokenBalanceChanges?: Array<{ userAccount: string; mint: string; rawTokenAmount: { tokenAmount: string; decimals: number } }>
28
+ }>
29
+ }
30
+
31
+ const short = (a: string) => `${a.slice(0, 4)}…${a.slice(-4)}`
32
+
33
+ /** Helius 的交易类型 → Activity 类型(认不出的按资产变化判断) */
34
+ function typeOf(item: HeliusItem, changes: readonly ActivityChange[]): ActivityType {
35
+ const t = item.type ?? ''
36
+ if (/SWAP/.test(t)) return 'swap'
37
+ if (t === 'TRANSFER') return 'transfer'
38
+ if (/LIQUIDITY|POSITION|POOL/.test(t)) return 'liquidity'
39
+ if (/MINT/.test(t)) return 'mint'
40
+ if (/APPROVE|REVOKE|DELEGATE/.test(t)) return 'approve'
41
+ const out = changes.filter((c) => c.delta.startsWith('-'))
42
+ if (out.length && changes.some((c) => !c.delta.startsWith('-') && out.some((o) => o.asset !== c.asset))) return 'swap'
43
+ return t && t !== 'UNKNOWN' ? 'contract' : 'other'
44
+ }
45
+
46
+ export function activityFromHelius(address: string, item: HeliusItem): Activity {
47
+ const me = item.accountData?.find((a) => a.account === address)
48
+ const isPayer = item.feePayer === address
49
+ const fee = BigInt(item.fee ?? 0)
50
+ let native = BigInt(me?.nativeBalanceChange ?? 0) + (isPayer ? fee : 0n)
51
+ const tokens = new Map<string, { delta: bigint; decimals: number }>()
52
+ for (const entry of item.accountData ?? []) {
53
+ for (const t of entry.tokenBalanceChanges ?? []) {
54
+ if (t.userAccount !== address) continue
55
+ const prev = tokens.get(t.mint)
56
+ tokens.set(t.mint, { delta: (prev?.delta ?? 0n) + BigInt(t.rawTokenAmount.tokenAmount), decimals: t.rawTokenAmount.decimals })
57
+ }
58
+ }
59
+ const wsol = tokens.get(NATIVE_MINT)
60
+ if (wsol) {
61
+ native += wsol.delta
62
+ tokens.delete(NATIVE_MINT)
63
+ }
64
+ const failed = item.transactionError !== null && item.transactionError !== undefined
65
+ const changes: ActivityChange[] = []
66
+ if (!failed) {
67
+ if (native !== 0n) changes.push({ asset: 'native', symbol: 'SOL', decimals: SOL_DECIMALS, delta: native.toString(), formatted: formatUnits(native, SOL_DECIMALS) })
68
+ for (const [asset, { delta, decimals }] of tokens) {
69
+ if (delta !== 0n) changes.push({ asset, symbol: short(asset), decimals, delta: delta.toString(), formatted: formatUnits(delta, decimals) })
70
+ }
71
+ }
72
+ const type = typeOf(item, changes)
73
+ let counterparty: string | undefined
74
+ for (const t of [...(item.nativeTransfers ?? []), ...(item.tokenTransfers ?? []).map((x) => ({ fromUserAccount: x.fromUserAccount ?? '', toUserAccount: x.toUserAccount ?? '' }))]) {
75
+ if (t.fromUserAccount === address && t.toUserAccount) counterparty = t.toUserAccount
76
+ else if (t.toUserAccount === address && t.fromUserAccount) counterparty = t.fromUserAccount
77
+ if (counterparty) break
78
+ }
79
+ const negative = changes.filter((c) => c.delta.startsWith('-')).length
80
+ const protocol = item.source && !/^(SYSTEM_PROGRAM|UNKNOWN)$/.test(item.source) ? item.source[0] + item.source.slice(1).toLowerCase().replace(/_/g, ' ') : undefined
81
+ return {
82
+ hash: item.signature,
83
+ time: item.timestamp ?? 0,
84
+ block: item.slot,
85
+ status: failed ? 'failed' : 'success',
86
+ type,
87
+ direction: (type === 'transfer' || type === 'other') && changes.length ? (negative === 0 ? 'in' : negative === changes.length ? 'out' : undefined) : undefined,
88
+ counterparty,
89
+ label: item.description || undefined,
90
+ changes,
91
+ ...(isPayer ? { fee: fee.toString() } : {}),
92
+ protocol,
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Helius 增强交易历史(`GET /v0/addresses/{address}/transactions`,按 before 签名翻页)。apiKey 由应用传入,库里不带。
98
+ * 按交易类型过滤时翻页最多再取 3 页
99
+ *
100
+ * ```ts
101
+ * await getHistory(address, { source: [heliusHistory({ apiKey }), rpcHistory()] }) // Helius 失败时用节点
102
+ * ```
103
+ */
104
+ export function heliusHistory(options: HeliusHistoryOptions): HistorySource {
105
+ if (!options.apiKey) throw new Error('heliusHistory needs an apiKey')
106
+ const base = (options.baseUrl ?? 'https://api.helius.xyz').replace(/\/+$/, '')
107
+ return {
108
+ name: 'helius',
109
+ async getHistory(ctx) {
110
+ const items: Activity[] = []
111
+ let cursor = ctx.cursor
112
+ let more = true
113
+ for (let page = 0; page < (ctx.types ? 4 : 1) && items.length < ctx.limit && more; page++) {
114
+ const url = `${base}/v0/addresses/${ctx.address}/transactions?api-key=${encodeURIComponent(options.apiKey)}&limit=${Math.min(100, ctx.limit)}${cursor ? `&before=${encodeURIComponent(cursor)}` : ''}`
115
+ const res = await ctx.fetch(url, { signal: ctx.signal })
116
+ if (!res.ok) throw new Error(`HTTP ${res.status}`)
117
+ const raw = (await res.json()) as HeliusItem[]
118
+ if (!Array.isArray(raw)) throw new Error('Unexpected response')
119
+ more = raw.length >= Math.min(100, ctx.limit)
120
+ for (const item of raw) {
121
+ cursor = item.signature
122
+ const t = item.timestamp ?? 0
123
+ if (ctx.before !== undefined && t >= ctx.before) continue
124
+ if (ctx.after !== undefined && t < ctx.after) {
125
+ more = false
126
+ break
127
+ }
128
+ const activity = activityFromHelius(ctx.address, item)
129
+ if (!ctx.types || ctx.types.includes(activity.type)) items.push(activity)
130
+ if (items.length >= ctx.limit) break
131
+ }
132
+ }
133
+ return { items, ...(more && cursor ? { nextCursor: cursor } : {}) }
134
+ },
135
+ }
136
+ }
package/src/index.ts CHANGED
@@ -6,6 +6,7 @@ export {
6
6
  type AccountsOptions,
7
7
  type BalanceQuery,
8
8
  type BalancesOptions,
9
+ type BalancesQueryOptions,
9
10
  type ClientConfig,
10
11
  type OwnerTokensOptions,
11
12
  type PersistTokenMetaOptions,
@@ -64,7 +65,17 @@ export {
64
65
  clusterOfGenesis,
65
66
  type Cluster,
66
67
  } from './constants.js'
67
- export { createProgramAddress, decodeAddress, encodeAddress, findProgramAddress, getAssociatedTokenAddress, getMetadataAddress, isAddress, isOnCurve } from './address.js'
68
+ export {
69
+ InvalidAddressError,
70
+ createProgramAddress,
71
+ decodeAddress,
72
+ encodeAddress,
73
+ findProgramAddress,
74
+ getAssociatedTokenAddress,
75
+ getMetadataAddress,
76
+ isAddress,
77
+ isOnCurve,
78
+ } from './address.js'
68
79
  export {
69
80
  formatAmount,
70
81
  formatUnits,
@@ -94,6 +105,7 @@ export {
94
105
  type FallbackOptions,
95
106
  type HttpRpcOptions,
96
107
  type RequestEvent,
108
+ type RpcRequestOptions,
97
109
  type RpcTransport,
98
110
  } from './rpc.js'
99
111
  export { NetworkCheckedRpc, NetworkMismatchError, type RpcSource, type SourceOptions } from './source.js'
package/src/layout.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { encodeAddress } from './address.js'
1
+ import { Bytes } from './bytes.js'
2
2
 
3
3
  /** getMultipleAccounts 解析后的账户 */
4
4
  export interface AccountInfo {
@@ -10,80 +10,6 @@ export interface AccountInfo {
10
10
  executable: boolean
11
11
  }
12
12
 
13
- class Reader {
14
- #offset: number
15
- readonly #view: DataView
16
-
17
- constructor(
18
- readonly bytes: Uint8Array,
19
- offset = 0,
20
- ) {
21
- this.#offset = offset
22
- this.#view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
23
- }
24
-
25
- get offset(): number {
26
- return this.#offset
27
- }
28
-
29
- #need(n: number): void {
30
- if (this.#offset + n > this.bytes.length) {
31
- throw new RangeError('Account data is shorter than expected')
32
- }
33
- }
34
-
35
- skip(n: number): void {
36
- this.#need(n)
37
- this.#offset += n
38
- }
39
-
40
- u8(): number {
41
- this.#need(1)
42
- return this.bytes[this.#offset++] as number
43
- }
44
-
45
- u16(): number {
46
- this.#need(2)
47
- const v = this.#view.getUint16(this.#offset, true)
48
- this.#offset += 2
49
- return v
50
- }
51
-
52
- u32(): number {
53
- this.#need(4)
54
- const v = this.#view.getUint32(this.#offset, true)
55
- this.#offset += 4
56
- return v
57
- }
58
-
59
- u64(): bigint {
60
- this.#need(8)
61
- const v = this.#view.getBigUint64(this.#offset, true)
62
- this.#offset += 8
63
- return v
64
- }
65
-
66
- pubkey(): string {
67
- this.#need(32)
68
- const v = encodeAddress(this.bytes.slice(this.#offset, this.#offset + 32))
69
- this.#offset += 32
70
- return v
71
- }
72
-
73
- /** borsh string:u32 长度 + UTF-8;去掉 Metaplex 用来补齐长度的 \0 */
74
- string(): string {
75
- const len = this.u32()
76
- this.#need(len)
77
- const v = new TextDecoder().decode(this.bytes.slice(this.#offset, this.#offset + len))
78
- this.#offset += len
79
- return v.replace(/\0+$/, '').trim()
80
- }
81
-
82
- option<T>(read: () => T): T | null {
83
- return this.u8() === 1 ? read() : null
84
- }
85
- }
86
-
87
13
  export interface MintInfo {
88
14
  mintAuthority: string | null
89
15
  supply: bigint
@@ -116,7 +42,7 @@ export function parseMint(data: Uint8Array): MintInfo {
116
42
  if (data.length !== MINT_SIZE && !(data.length > ACCOUNT_TYPE_OFFSET && data[ACCOUNT_TYPE_OFFSET] === ACCOUNT_TYPE_MINT)) {
117
43
  throw new RangeError('Not a mint account')
118
44
  }
119
- const r = new Reader(data)
45
+ const r = new Bytes(data)
120
46
  const hasMintAuthority = r.u32() === 1
121
47
  const mintAuthority = r.pubkey()
122
48
  const supply = r.u64()
@@ -140,7 +66,7 @@ export function findExtension(data: Uint8Array, extensionType: number): Uint8Arr
140
66
  return null
141
67
  }
142
68
  try {
143
- const r = new Reader(data, EXTENSIONS_OFFSET)
69
+ const r = new Bytes(data, EXTENSIONS_OFFSET)
144
70
  while (r.offset + 4 <= data.length) {
145
71
  const type = r.u16()
146
72
  const length = r.u16()
@@ -167,7 +93,7 @@ function parseTokenMetadataExtension(data: Uint8Array): TokenMetadata | null {
167
93
  return null
168
94
  }
169
95
  try {
170
- const m = new Reader(ext)
96
+ const m = new Bytes(ext)
171
97
  m.pubkey() // updateAuthority
172
98
  m.pubkey() // mint
173
99
  return { name: m.string(), symbol: m.string(), uri: m.string() }
@@ -212,7 +138,7 @@ export function parseTransferFeeConfig(data: Uint8Array): TransferFeeConfigInfo
212
138
  return null
213
139
  }
214
140
  try {
215
- const r = new Reader(ext)
141
+ const r = new Bytes(ext)
216
142
  // OptionalNonZeroPubkey:全 0 表示没有
217
143
  const configAuthority = r.pubkey()
218
144
  const withdrawAuthority = r.pubkey()
@@ -238,15 +164,70 @@ export interface TokenAccountInfo {
238
164
  amount: bigint
239
165
  }
240
166
 
167
+ function isTokenAccountData(data: Uint8Array): boolean {
168
+ return data.length === ACCOUNT_SIZE || (data.length > ACCOUNT_TYPE_OFFSET && data[ACCOUNT_TYPE_OFFSET] === ACCOUNT_TYPE_ACCOUNT)
169
+ }
170
+
241
171
  /** 解析代币账户(SPL Token / Token-2022 前 72 字节相同) */
242
172
  export function parseTokenAccount(data: Uint8Array): TokenAccountInfo {
243
- if (data.length !== ACCOUNT_SIZE && !(data.length > ACCOUNT_TYPE_OFFSET && data[ACCOUNT_TYPE_OFFSET] === ACCOUNT_TYPE_ACCOUNT)) {
173
+ if (!isTokenAccountData(data)) {
244
174
  throw new RangeError('Not a token account')
245
175
  }
246
- const r = new Reader(data)
176
+ const r = new Bytes(data)
247
177
  return { mint: r.pubkey(), owner: r.pubkey(), amount: r.u64() }
248
178
  }
249
179
 
180
+ /** 代币账户的完整基础布局(前 165 字节,SPL Token / Token-2022 相同) */
181
+ export interface TokenAccountState extends TokenAccountInfo {
182
+ delegate: string | null
183
+ /** 0 未初始化、1 正常、2 冻结 */
184
+ state: number
185
+ frozen: boolean
186
+ /** Wrapped SOL 账户(is_native 有值) */
187
+ native: boolean
188
+ delegatedAmount: bigint
189
+ closeAuthority: string | null
190
+ }
191
+
192
+ /** 解析代币账户的完整基础布局;不是代币账户或未初始化时为 null(包内使用:/approvals、/simulate) */
193
+ export function parseTokenAccountState(data: Uint8Array): TokenAccountState | null {
194
+ if (!isTokenAccountData(data)) {
195
+ return null
196
+ }
197
+ const b = new Bytes(data)
198
+ const state = b.u8(108)
199
+ if (state === 0) {
200
+ return null
201
+ }
202
+ // COption:u32 标记(1 有、0 没有)+ 值
203
+ return {
204
+ mint: b.key(0),
205
+ owner: b.key(32),
206
+ amount: b.u64(64),
207
+ delegate: b.u32(72) === 1 ? b.key(76) : null,
208
+ state,
209
+ frozen: state === 2,
210
+ native: b.u32(109) === 1,
211
+ delegatedAmount: b.u64(121),
212
+ closeAuthority: b.u32(129) === 1 ? b.key(133) : null,
213
+ }
214
+ }
215
+
216
+ /** 计算预算指令里的计算单元上限(SetComputeUnitLimit)和单价(SetComputeUnitPrice,micro-lamports);没有时 limit 为 null、price 为 0 */
217
+ export function computeBudgetOf(instructions: Iterable<{ programId: string; data: Uint8Array }>): { limit: number | null; price: bigint } {
218
+ let limit: number | null = null
219
+ let price = 0n
220
+ for (const ix of instructions) {
221
+ if (ix.programId !== COMPUTE_BUDGET_PROGRAM) continue
222
+ const b = new Bytes(ix.data)
223
+ if (ix.data[0] === 2 && ix.data.length >= 5) limit = b.u32(1)
224
+ if (ix.data[0] === 3 && ix.data.length >= 9) price = b.u64(1)
225
+ }
226
+ return { limit, price }
227
+ }
228
+
229
+ const COMPUTE_BUDGET_PROGRAM = 'ComputeBudget111111111111111111111111111111'
230
+
250
231
  export interface MetaplexMetadata extends TokenMetadata {
251
232
  updateAuthority: string
252
233
  mint: string
@@ -261,7 +242,7 @@ export interface MetaplexMetadata extends TokenMetadata {
261
242
 
262
243
  /** 解析 Metaplex Token Metadata 账户;老版本账户缺少后面的字段时,缺的部分为默认值 */
263
244
  export function parseMetaplexMetadata(data: Uint8Array): MetaplexMetadata {
264
- const r = new Reader(data)
245
+ const r = new Bytes(data)
265
246
  if (r.u8() !== 4) {
266
247
  throw new RangeError('Not a Metaplex metadata account') // Key::MetadataV1 = 4
267
248
  }
@@ -1,6 +1,6 @@
1
1
  import { sha256 } from '@noble/hashes/sha2.js'
2
2
 
3
- import { encodeAddress, getAssociatedTokenAddress, isAddress } from '../address.js'
3
+ import { encodeAddress, getAssociatedTokenAddress, InvalidAddressError, isAddress } from '../address.js'
4
4
  import type { SolanaClient } from '../client.js'
5
5
  import { NATIVE_MINT, SYSTEM_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from '../constants.js'
6
6
  import { formatUnits, parseMint, parseTokenAccount, parseTransferFeeConfig, parseUnits, type AccountInfo } from '../layout.js'
@@ -25,7 +25,7 @@ export interface Ctx {
25
25
  export function createCtx(client: SolanaClient, owner: string, options: SolanaLiquidityOptions): Ctx {
26
26
  for (const address of [owner, options.recipient ?? owner]) {
27
27
  if (!isAddress(address)) {
28
- throw new Error(`Invalid address: ${address}`)
28
+ throw new InvalidAddressError(`Invalid address: ${address}`, address)
29
29
  }
30
30
  }
31
31
  const slippage = options.slippage ?? 0.005
@@ -223,6 +223,20 @@ export async function orcaAdd(ctx: Ctx, params: SolanaIncreaseLiquidityParams |
223
223
  })
224
224
  }
225
225
 
226
+ /**
227
+ * 关闭已清空的仓位(流动性、手续费、奖励都为 0):销毁仓位 NFT、关闭仓位账户和 NFT 的代币账户(nftAccount,默认 owner 的 ATA),租金退给 receiver。
228
+ * NFT 是 Token-2022(带元数据扩展)时连 mint 一起关闭,否则(Metaplex 时代的 SPL Token NFT)mint 留着
229
+ */
230
+ export function orcaClosePosition(owner: string, receiver: string, positionMint: string, nftProgram: string, nftAccount = getAssociatedTokenAddress(owner, positionMint, nftProgram)): Instruction {
231
+ const position = positionAddress(positionMint)
232
+ const token2022 = nftProgram === TOKEN_2022_PROGRAM_ID
233
+ return {
234
+ programId: PROGRAM,
235
+ keys: [r(owner, true), w(receiver), w(position), w(positionMint), w(nftAccount), r(nftProgram)],
236
+ data: anchorIx(token2022 ? 'close_position_with_token_extensions' : 'close_position'),
237
+ }
238
+ }
239
+
226
240
  /** 移除(decrease + 领取手续费和奖励)/ 只领取(collectOnly) */
227
241
  export async function orcaRemove(ctx: Ctx, params: SolanaRemoveLiquidityParams, collectOnly = false): Promise<SolanaLiquidityPlan> {
228
242
  const mint = refMint(params.position)
@@ -304,11 +318,7 @@ export async function orcaRemove(ctx: Ctx, params: SolanaRemoveLiquidityParams,
304
318
  })
305
319
  })
306
320
  if (burn) {
307
- main.push(
308
- nftProgram === TOKEN_2022_PROGRAM_ID
309
- ? { programId: PROGRAM, keys: [r(ctx.owner, true), w(ctx.owner), w(positionAccount), w(mint), w(nftAccount), r(TOKEN_2022_PROGRAM_ID)], data: anchorIx('close_position_with_token_extensions') }
310
- : { programId: PROGRAM, keys: [r(ctx.owner, true), w(ctx.owner), w(positionAccount), w(mint), w(nftAccount), r(TOKEN_PROGRAM_ID)], data: anchorIx('close_position') },
311
- )
321
+ main.push(orcaClosePosition(ctx.owner, ctx.owner, mint, nftProgram))
312
322
  }
313
323
  return makePlan(ctx, {
314
324
  action: collectOnly ? 'collect' : 'remove',
@@ -249,6 +249,18 @@ export async function raydiumAdd(ctx: Ctx, params: SolanaIncreaseLiquidityParams
249
249
  })
250
250
  }
251
251
 
252
+ /**
253
+ * 关闭已清空的仓位(流动性、手续费、奖励都为 0):销毁仓位 NFT、关闭 NFT 的代币账户和仓位账户,租金退给 owner(程序固定,不能指定别人)。
254
+ * pool 是仓位所在的池子(官方 SDK 也带着它)
255
+ */
256
+ export function raydiumClosePosition(owner: string, positionMint: string, nftProgram: string, pool: string, nftAccount = getAssociatedTokenAddress(owner, positionMint, nftProgram)): Instruction {
257
+ return {
258
+ programId: PROGRAM,
259
+ keys: [w(owner, true), w(positionMint), w(nftAccount), w(personalPositionAddress(positionMint)), r(SYSTEM_PROGRAM_ID), r(nftProgram), w(pool)],
260
+ data: anchorIx('close_position'),
261
+ }
262
+ }
263
+
252
264
  /** 移除(liquidity > 0)/ 只领取手续费和奖励(collectOnly:流动性为 0 的 decrease) */
253
265
  export async function raydiumRemove(ctx: Ctx, params: SolanaRemoveLiquidityParams, collectOnly = false): Promise<SolanaLiquidityPlan> {
254
266
  const mint = refMint(params.position)
@@ -308,11 +320,7 @@ export async function raydiumRemove(ctx: Ctx, params: SolanaRemoveLiquidityParam
308
320
  }
309
321
  const main: Instruction[] = [decrease]
310
322
  if (burn) {
311
- main.push({
312
- programId: PROGRAM,
313
- keys: [w(ctx.owner, true), w(mint), w(nftAta), w(personal), r(SYSTEM_PROGRAM_ID), r(nftProgram), w(pool.address)],
314
- data: anchorIx('close_position'),
315
- })
323
+ main.push(raydiumClosePosition(ctx.owner, mint, nftProgram, pool.address))
316
324
  }
317
325
  return makePlan(ctx, {
318
326
  action: collectOnly ? 'collect' : 'remove',