@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/names.ts ADDED
@@ -0,0 +1,329 @@
1
+ /**
2
+ * 域名解析(子路径 `@w3lib/solana-batch-call/names`):SNS(Solana Name Service,Bonfida),名字 → 地址、地址 → 主域名(反查后核对)。
3
+ * 账户地址都在本地按 PDA 推导,读取合成 getMultipleAccounts,不需要索引节点(代币化的域名除外)
4
+ */
5
+ import { sha256 } from '@noble/hashes/sha2.js'
6
+
7
+ import { decodeAddress, encodeAddress, findProgramAddress, getAssociatedTokenAddress, isAddress, isOnCurve } from './address.js'
8
+ import type { RequestOptions, SolanaClient } from './client.js'
9
+ import { TOKEN_PROGRAM_ID } from './constants.js'
10
+ import type { AccountInfo } from './layout.js'
11
+
12
+ /** SPL Name Service 程序 */
13
+ export const NAME_PROGRAM_ID = 'namesLPneVptA9Z5rqUDD9tMTWEJwofgaYwp8cawRkX'
14
+ /** SNS 顶级域(`.sns`,2026-10 之前叫 `.sol`,链上是同一批账户)的根账户 */
15
+ export const SNS_ROOT_DOMAIN = '58PwtjSDuFHuUkYjH9BYnnQKHfwo9reZhC2zMJv9JPkx'
16
+ /** 反查记录的 class(同时是 SNS 注册程序的中心状态账户) */
17
+ const REVERSE_LOOKUP_CLASS = '33m47vH6Eav6jr5Ry86XjhRft2jRBLDnDgPSHoquXi2Z'
18
+ /** V2 记录的 class:sns-records 程序(HP3D4D1Z…)的中心状态 PDA */
19
+ const RECORDS_V2_CLASS = '2pMnqHvei2N5oDcVGCRdZx48gqti199wr5CsyTTafsbo'
20
+ /** 主域名(favourite domain)所在的程序 */
21
+ const PRIMARY_DOMAIN_PROGRAM = '85iDfUvr3HJyLM2zcq5BXSiDvUWfw6cSE1FfNBo8Ap29'
22
+ /** 域名代币化(NFT)程序 */
23
+ const NAME_TOKENIZER_ID = 'nftD3vbNkNqfj2Sd3HZwbpw4BxxKWr4AjGb9X38JeZk'
24
+ /** 名字账户的头部:parent(32)+ owner(32)+ class(32),之后是数据 */
25
+ const HEADER_LEN = 96
26
+
27
+ const encoder = new TextEncoder()
28
+ const ZERO = new Uint8Array(32)
29
+
30
+ export interface NamesOptions extends RequestOptions {
31
+ /**
32
+ * 也按 SNS 解析 `.sol` 名字(当作同名的 `.sns`)。默认 false:`.sol` 正在迁到另一个注册服务(SRS),
33
+ * SNS 官方 SDK 从 slot 452,825,395(约 2026-10-15)起不再解析 `.sol`,以后同一个 `.sol` 名字可能属于别人
34
+ */
35
+ legacySol?: boolean
36
+ /**
37
+ * 域名的 owner 是 PDA(程序账户,不是钱包)时也作为地址返回,默认 false(与官方 SDK 一致:转给 PDA 的资产可能取不出来,address 为 null)
38
+ */
39
+ allowPda?: boolean
40
+ }
41
+
42
+ /**
43
+ * 地址的来源(与 SNS 官方 SDK 的解析顺序相同):
44
+ * - nft:域名代币化成了 NFT,地址是 NFT 的持有人(要用 getTokenLargestAccounts 找,节点不支持时 address 为 null)
45
+ * - record-v2 / record-v1:域名设置的 SOL 记录(V2 要求 owner 验证过、没有过期;V1 要求 owner 的签名)
46
+ * - owner:域名的 owner
47
+ */
48
+ export type NameSource = 'nft' | 'record-v2' | 'record-v1' | 'owner'
49
+
50
+ export interface ResolvedName {
51
+ /** 传入的名字(原样) */
52
+ name: string
53
+ /** 规范化后的名字(小写、去掉空格,后缀为 .sns);名字不合法时为 null */
54
+ normalized: string | null
55
+ /** 解析出的地址(SOL 记录,没有时为 owner);没有注册、名字不合法、记录不合法时为 null */
56
+ address: string | null
57
+ /** 域名的 owner(代币化的域名是 NFT 的持有人) */
58
+ owner: string | null
59
+ source: NameSource | null
60
+ /** 解析这个名字的服务('sns');名字不归 SNS 管时为 null */
61
+ service: 'sns' | null
62
+ }
63
+
64
+ export interface AddressName {
65
+ /** 传入的地址(原样) */
66
+ address: string
67
+ /** 主域名(如 'bonfida.sns'),核对过属于这个地址;没有设置或已经转给别人时为 null */
68
+ name: string | null
69
+ service: 'sns' | null
70
+ }
71
+
72
+ const hashName = (name: string) => sha256(encoder.encode(`SPL Name Service${name}`))
73
+
74
+ /** 名字账户:sha256('SPL Name Service' + name)、class、parent 推导的 PDA(没有 class / parent 时用 32 个 0) */
75
+ function nameAccount(name: string, nameClass?: string, parent?: string): string {
76
+ return findProgramAddress([hashName(name), nameClass ? decodeAddress(nameClass) : ZERO, parent ? decodeAddress(parent) : ZERO], NAME_PROGRAM_ID)
77
+ }
78
+
79
+ /** 域名对应的名字账户:'bonfida' 或子域名 'sub.bonfida'(不带后缀);子域名的种子是 '\0' + 子域名,parent 是父域名 */
80
+ export function snsDomainKey(domain: string): { key: string; parent: string | null } {
81
+ const labels = domain.split('.')
82
+ if (labels.length === 2) {
83
+ const parent = nameAccount(labels[1] as string, undefined, SNS_ROOT_DOMAIN)
84
+ return { key: nameAccount(`\0${labels[0]}`, undefined, parent), parent }
85
+ }
86
+ if (labels.length !== 1) {
87
+ throw new Error(`Invalid SNS domain: ${domain}`)
88
+ }
89
+ return { key: nameAccount(domain, undefined, SNS_ROOT_DOMAIN), parent: null }
90
+ }
91
+
92
+ /** 反查记录:sha256 的是名字账户地址的 base58,class 是 REVERSE_LOOKUP_CLASS,子域名的 parent 是父域名 */
93
+ function reverseKey(domainKey: string, parent?: string | null): string {
94
+ return nameAccount(domainKey, REVERSE_LOOKUP_CLASS, parent ?? undefined)
95
+ }
96
+
97
+ /** 规范化:小写、去掉首尾空格;只认 name.sns / sub.name.sns(legacySol 时也认 .sol),返回不带后缀的部分 */
98
+ function parseName(name: string, legacySol: boolean): { domain: string; normalized: string } | null {
99
+ const n = name.trim().toLowerCase()
100
+ const suffix = n.endsWith('.sns') ? '.sns' : legacySol && n.endsWith('.sol') ? '.sol' : null
101
+ if (!suffix) {
102
+ return null
103
+ }
104
+ const domain = n.slice(0, -suffix.length)
105
+ const labels = domain.split('.')
106
+ if (labels.length > 2 || labels.some((l) => !l || l.includes('\0'))) {
107
+ return null
108
+ }
109
+ return { domain, normalized: `${domain}.sns` }
110
+ }
111
+
112
+ interface Registry {
113
+ parent: string
114
+ owner: string
115
+ data: Uint8Array
116
+ }
117
+
118
+ function registryOf(account: AccountInfo | null | undefined): Registry | null {
119
+ if (!account || account.owner !== NAME_PROGRAM_ID || account.data.length < HEADER_LEN) {
120
+ return null
121
+ }
122
+ return { parent: encodeAddress(account.data.slice(0, 32)), owner: encodeAddress(account.data.slice(32, 64)), data: account.data.slice(HEADER_LEN) }
123
+ }
124
+
125
+ /** NftRecord:tag(1)+ nonce(1)+ 名字账户(32)+ owner(32)+ NFT mint(32);tag 2 表示域名正在代币化 */
126
+ function tokenized(account: AccountInfo | null | undefined): boolean {
127
+ return !!account && account.owner === NAME_TOKENIZER_ID && account.data[0] === 2
128
+ }
129
+
130
+ const nftRecordKey = (domainKey: string) => findProgramAddress([encoder.encode('nft_record'), decodeAddress(domainKey)], NAME_TOKENIZER_ID)
131
+ const nftMint = (domainKey: string) => findProgramAddress([encoder.encode('tokenized_name'), decodeAddress(domainKey)], NAME_TOKENIZER_ID)
132
+
133
+ /** 记录账户:V1 的种子是 '\x01' + 记录名,parent 是域名;V2 是 '\x02' + 记录名,class 是 RECORDS_V2_CLASS */
134
+ const recordV1Key = (domainKey: string, record: string) => nameAccount(`\x01${record}`, undefined, domainKey)
135
+ const recordV2Key = (domainKey: string, record: string) => nameAccount(`\x02${record}`, RECORDS_V2_CLASS, domainKey)
136
+
137
+ /** V2 记录的验证方式和对应 id 的长度:0 None,1 Solana(32),2 Ethereum(20),3 UnverifiedSolana(32) */
138
+ const VALIDATION_LENGTH = [0, 32, 20, 32]
139
+ const VALIDATION_SOLANA = 1
140
+
141
+ /**
142
+ * V2 的 SOL 记录(与官方 SDK 相同):数据是 u16 过期验证方式 + u16 关联验证方式 + u32 内容长度,然后是两个验证 id 和内容。
143
+ * 返回记录的地址;记录过期(过期验证的 id 不是当前 owner)时为 undefined(继续看 V1 / owner);记录不合法时为 null(不解析)
144
+ */
145
+ function solRecordV2(registry: Registry | null, owner: string): string | null | undefined {
146
+ if (!registry || registry.data.length < 8) {
147
+ return undefined
148
+ }
149
+ const view = new DataView(registry.data.buffer, registry.data.byteOffset, registry.data.byteLength)
150
+ const staleness = view.getUint16(0, true)
151
+ const roa = view.getUint16(2, true)
152
+ const stalenessLength = VALIDATION_LENGTH[staleness]
153
+ const roaLength = VALIDATION_LENGTH[roa]
154
+ if (stalenessLength === undefined || roaLength === undefined) {
155
+ return null
156
+ }
157
+ const body = registry.data.slice(8)
158
+ const content = body.slice(stalenessLength + roaLength)
159
+ if (content.length !== 32 || staleness !== VALIDATION_SOLANA || roa !== VALIDATION_SOLANA) {
160
+ return null
161
+ }
162
+ if (encodeAddress(body.slice(0, 32)) !== owner) {
163
+ return undefined // owner 换了:记录过期
164
+ }
165
+ // 关联验证的 id 必须就是内容本身(内容地址签过名)
166
+ return encodeAddress(body.slice(32, 64)) === encodeAddress(content) ? encodeAddress(content) : null
167
+ }
168
+
169
+ /** V1 的 SOL 记录:32 字节地址 + 64 字节 owner 的签名(签的是 hex(地址 ‖ 记录账户地址) 的 UTF-8);签名不对时为 null */
170
+ async function solRecordV1(registry: Registry | null, recordKey: string, owner: string): Promise<string | null> {
171
+ if (!registry || registry.data.length < 96) {
172
+ return null
173
+ }
174
+ const target = registry.data.slice(0, 32)
175
+ const signature = registry.data.slice(32, 96)
176
+ const hex = [...target, ...decodeAddress(recordKey)].map((b) => b.toString(16).padStart(2, '0')).join('')
177
+ return (await verifyEd25519(decodeAddress(owner), signature, encoder.encode(hex))) ? encodeAddress(target) : null
178
+ }
179
+
180
+ /** Ed25519 验签(WebCrypto);运行环境不支持时当作验签失败 */
181
+ async function verifyEd25519(publicKey: Uint8Array, signature: Uint8Array, message: Uint8Array): Promise<boolean> {
182
+ const subtle = globalThis.crypto?.subtle
183
+ if (!subtle) return false
184
+ try {
185
+ const key = await subtle.importKey('raw', publicKey as Uint8Array<ArrayBuffer>, { name: 'Ed25519' }, false, ['verify'])
186
+ return await subtle.verify('Ed25519', key, signature as Uint8Array<ArrayBuffer>, message as Uint8Array<ArrayBuffer>)
187
+ } catch {
188
+ return false
189
+ }
190
+ }
191
+
192
+ /**
193
+ * 代币化域名的 NFT 持有人:getTokenLargestAccounts 找到唯一的持有账户,再读出它的 owner(与官方 SDK 相同)。
194
+ * 免费公共节点大多不支持 getTokenLargestAccounts,这时为 null
195
+ */
196
+ async function nftHolders(client: SolanaClient, domainKeys: readonly string[], signal: AbortSignal | undefined): Promise<Map<string, string | null>> {
197
+ const largest = await Promise.all(
198
+ domainKeys.map((key) =>
199
+ client
200
+ .request<{ value: Array<{ address: string; amount: string }> }>('getTokenLargestAccounts', [nftMint(key), { commitment: client.commitment }], { signal })
201
+ .then((res) => res.value.find((a) => a.amount === '1')?.address ?? null)
202
+ .catch((err: unknown) => {
203
+ if (signal?.aborted) throw err
204
+ return null
205
+ }),
206
+ ),
207
+ )
208
+ const holders = largest.filter((a): a is string => a !== null)
209
+ const accounts = holders.length ? await client.accounts(holders, { signal }) : []
210
+ const ownerOf = new Map(holders.map((a, i) => [a, accounts[i]?.owner === TOKEN_PROGRAM_ID ? encodeAddress((accounts[i] as AccountInfo).data.slice(32, 64)) : null]))
211
+ return new Map(domainKeys.map((key, i) => [key, largest[i] ? (ownerOf.get(largest[i] as string) ?? null) : null]))
212
+ }
213
+
214
+ /**
215
+ * 名字 → 地址(一次 getMultipleAccounts 读域名、NFT 记录、两版 SOL 记录;代币化的域名另外找 NFT 持有人)。
216
+ * 解析顺序同官方 SDK:代币化 → NFT 持有人;V2 SOL 记录(验证过、没过期);V1 SOL 记录(owner 签过名);owner(是 PDA 时默认不返回)
217
+ *
218
+ * ```ts
219
+ * await resolveNames(client, ['bonfida.sns', 'toly.sns'])
220
+ * // [{ name: 'bonfida.sns', normalized: 'bonfida.sns', address: 'Fw1E…Lb1v', owner: 'Fw1E…Lb1v', source: 'owner', service: 'sns' }, …]
221
+ * ```
222
+ */
223
+ export async function resolveNames(client: SolanaClient, names: readonly string[], options: NamesOptions = {}): Promise<ResolvedName[]> {
224
+ const { signal, legacySol = false, allowPda = false } = options
225
+ const items = names.map((name) => {
226
+ const parsed = parseName(name, legacySol)
227
+ if (!parsed) return { name, parsed: null, key: null }
228
+ const { key } = snsDomainKey(parsed.domain)
229
+ return { name, parsed, key, nft: nftRecordKey(key), v1: recordV1Key(key, 'SOL'), v2: recordV2Key(key, 'SOL') }
230
+ })
231
+ const keys = items.flatMap((x) => (x.key ? [x.key, x.nft, x.v1, x.v2] : [])) as string[]
232
+ const accounts = keys.length ? await client.accounts(keys, { signal }) : []
233
+ const byKey = new Map(keys.map((k, i) => [k, accounts[i] ?? null]))
234
+ const nfts = items.filter((x) => x.key && registryOf(byKey.get(x.key)) && tokenized(byKey.get(x.nft as string))).map((x) => x.key as string)
235
+ const holders = nfts.length ? await nftHolders(client, nfts, signal) : new Map<string, string | null>()
236
+
237
+ return Promise.all(
238
+ items.map(async (x): Promise<ResolvedName> => {
239
+ const base = { name: x.name, normalized: x.parsed?.normalized ?? null, service: x.parsed ? ('sns' as const) : null }
240
+ const registry = x.key ? registryOf(byKey.get(x.key)) : null
241
+ if (!x.key || !registry) {
242
+ return { ...base, address: null, owner: null, source: null }
243
+ }
244
+ if (holders.has(x.key)) {
245
+ const holder = holders.get(x.key) ?? null
246
+ return { ...base, address: holder, owner: holder, source: 'nft' }
247
+ }
248
+ const owner = registry.owner
249
+ const v2 = solRecordV2(registryOf(byKey.get(x.v2 as string)), owner)
250
+ if (v2 !== undefined) {
251
+ return { ...base, address: v2, owner, source: 'record-v2' }
252
+ }
253
+ const v1 = await solRecordV1(registryOf(byKey.get(x.v1 as string)), x.v1 as string, owner)
254
+ if (v1) {
255
+ return { ...base, address: v1, owner, source: 'record-v1' }
256
+ }
257
+ return { ...base, address: allowPda || isOnCurve(decodeAddress(owner)) ? owner : null, owner, source: 'owner' }
258
+ }),
259
+ )
260
+ }
261
+
262
+ /** 反查记录的数据:u32 长度 + 名字(子域名的以 \0 开头) */
263
+ function reverseName(account: AccountInfo | null | undefined): string | null {
264
+ const registry = registryOf(account)
265
+ if (!registry || registry.data.length < 4) {
266
+ return null
267
+ }
268
+ const length = new DataView(registry.data.buffer, registry.data.byteOffset).getUint32(0, true)
269
+ if (registry.data.length < 4 + length) {
270
+ return null
271
+ }
272
+ return new TextDecoder().decode(registry.data.slice(4, 4 + length)).replace(/^\0/, '') || null
273
+ }
274
+
275
+ /**
276
+ * 地址 → 主域名:读地址设置的主域名(favourite domain),再读域名和反查记录拼出名字,核对名字确实推导出这个域名账户、
277
+ * 并且域名属于这个地址(owner 是它,或代币化后它持有 NFT)——换了 owner 的旧设置不算。通常两次 getMultipleAccounts(子域名三次)
278
+ *
279
+ * ```ts
280
+ * await lookupAddresses(client, [wallet]) // [{ address: wallet, name: 'bonfida.sns', service: 'sns' }]
281
+ * ```
282
+ */
283
+ export async function lookupAddresses(client: SolanaClient, addresses: readonly string[], options: RequestOptions = {}): Promise<AddressName[]> {
284
+ const { signal } = options
285
+ const wallets = [...new Set(addresses.filter((a) => isAddress(a)))]
286
+ const primaryKeys = wallets.map((w) => findProgramAddress([encoder.encode('favourite_domain'), decodeAddress(w)], PRIMARY_DOMAIN_PROGRAM))
287
+ const primaries = primaryKeys.length ? await client.accounts(primaryKeys, { signal }) : []
288
+ // 主域名账户:tag(1)+ 名字账户(32)
289
+ const chosen = wallets.flatMap((wallet, i) => {
290
+ const a = primaries[i]
291
+ return a && a.owner === PRIMARY_DOMAIN_PROGRAM && a.data.length >= 33 ? [{ wallet, domain: encodeAddress(a.data.slice(1, 33)) }] : []
292
+ })
293
+ // 先按顶级域名读反查记录(大多数情况);是子域名时再读一次
294
+ const round2 = chosen.flatMap((c) => [c.domain, reverseKey(c.domain), getAssociatedTokenAddress(c.wallet, nftMint(c.domain), TOKEN_PROGRAM_ID)])
295
+ const accounts2 = round2.length ? await client.accounts(round2, { signal }) : []
296
+ const found = chosen.map((c, i) => ({ ...c, registry: registryOf(accounts2[3 * i]), reverse: accounts2[3 * i + 1] ?? null, nft: accounts2[3 * i + 2] ?? null }))
297
+ const subs = found.filter((f) => f.registry && f.registry.parent !== SNS_ROOT_DOMAIN)
298
+ const round3 = subs.flatMap((f) => [reverseKey(f.domain, f.registry?.parent), reverseKey(f.registry?.parent as string)])
299
+ const accounts3 = round3.length ? await client.accounts(round3, { signal }) : []
300
+ const subNames = new Map(
301
+ subs.map((f, i) => {
302
+ const [sub, parent] = [reverseName(accounts3[2 * i]), reverseName(accounts3[2 * i + 1])]
303
+ return [f, sub && parent ? `${sub}.${parent}` : null]
304
+ }),
305
+ )
306
+
307
+ const names = new Map<string, string>()
308
+ for (const f of found) {
309
+ if (!f.registry) continue
310
+ const domain = f.registry.parent === SNS_ROOT_DOMAIN ? reverseName(f.reverse) : (subNames.get(f) ?? null)
311
+ if (!domain) continue
312
+ // 正查核对:名字推导出的账户就是主域名指向的账户(反查记录的内容格式不对时不会拼出别的名字)
313
+ let key: string
314
+ try {
315
+ key = snsDomainKey(domain).key
316
+ } catch {
317
+ continue
318
+ }
319
+ // 代币化的域名:钱包的 ATA 里有这个域名的 NFT(数量 1)
320
+ const holdsNft = !!f.nft && f.nft.owner === TOKEN_PROGRAM_ID && f.nft.data.length >= 72 && new DataView(f.nft.data.buffer, f.nft.data.byteOffset).getBigUint64(64, true) === 1n
321
+ if (key === f.domain && (f.registry.owner === f.wallet || holdsNft)) {
322
+ names.set(f.wallet, `${domain}.sns`)
323
+ }
324
+ }
325
+ return addresses.map((address) => {
326
+ const name = names.get(address) ?? null
327
+ return { address, name, service: name ? 'sns' : null }
328
+ })
329
+ }
package/src/pools.ts ADDED
@@ -0,0 +1,43 @@
1
+ /**
2
+ * 按两个 mint 推导池子地址(PDA,不需要 getProgramAccounts):Raydium CLMM / CPMM 的每个 AmmConfig、Orca 的每个费率档位。
3
+ * 链上价格(/price)和兑换(/swap)共用;Raydium AMM v4 的池子地址要用 OpenBook 市场推导,不在这里
4
+ */
5
+ import { decodeAddress, findProgramAddress } from './address.js'
6
+ import { ORCA_WHIRLPOOL_PROGRAM_ID, RAYDIUM_CLMM_PROGRAM_ID, RAYDIUM_CPMM_PROGRAM_ID, utf8 } from './lp.js'
7
+
8
+ export type PdaPoolProtocol = 'raydium-clmm' | 'orca-whirlpool' | 'raydium-cpmm'
9
+
10
+ /** Orca 默认的 WhirlpoolsConfig */
11
+ export const ORCA_WHIRLPOOLS_CONFIG = '2LecshUwdy9xi7meFgHtFJQNSKk4KdTrcpvaB56dP2NQ'
12
+ /**
13
+ * 池子 PDA 用到的配置(2026-10 主网实测存在的全部):
14
+ * - Raydium CLMM / CPMM 的 AmmConfig:PDA ["amm_config", index(u16 大端)],index 0~20
15
+ * - Orca 的费率档位:普通档位的种子是 tick spacing(1、2、4、8、16、64、96、128、256,32896 为 Splash 池),动态费率档位是 1025~1036
16
+ * Raydium CLMM 用 seed_index 建的自定义池子不在推导范围内
17
+ */
18
+ const RAYDIUM_CONFIG_COUNT = 21
19
+ const ORCA_FEE_TIERS = [1, 2, 4, 8, 16, 64, 96, 128, 256, 32896, ...Array.from({ length: 12 }, (_, i) => 1025 + i)]
20
+
21
+ let configs: { clmm: Uint8Array[]; cpmm: Uint8Array[]; orca: Uint8Array } | undefined
22
+ function poolConfigs() {
23
+ const derive = (program: string) =>
24
+ Array.from({ length: RAYDIUM_CONFIG_COUNT }, (_, i) => decodeAddress(findProgramAddress([utf8('amm_config'), new Uint8Array([i >> 8, i & 0xff])], program)))
25
+ configs ??= { clmm: derive(RAYDIUM_CLMM_PROGRAM_ID), cpmm: derive(RAYDIUM_CPMM_PROGRAM_ID), orca: decodeAddress(ORCA_WHIRLPOOLS_CONFIG) }
26
+ return configs
27
+ }
28
+
29
+ /** 两个 mint 在这些协议里可能的池子地址(三个程序都要求 mint0 < mint1,按字节比较);两个 mint 相同时为空 */
30
+ export function poolAddresses(mintA: string, mintB: string, protocols: ReadonlySet<PdaPoolProtocol>): Array<{ protocol: PdaPoolProtocol; address: string }> {
31
+ const a = decodeAddress(mintA)
32
+ const b = decodeAddress(mintB)
33
+ const i = a.findIndex((byte, k) => byte !== b[k])
34
+ if (i < 0) return []
35
+ const [m0, m1] = (a[i] as number) < (b[i] as number) ? [a, b] : [b, a]
36
+ const { clmm, cpmm, orca } = poolConfigs()
37
+ const out: Array<{ protocol: PdaPoolProtocol; address: string }> = []
38
+ const add = (protocol: PdaPoolProtocol, seeds: Uint8Array[], program: string) => out.push({ protocol, address: findProgramAddress(seeds, program) })
39
+ if (protocols.has('raydium-clmm')) for (const c of clmm) add('raydium-clmm', [utf8('pool'), c, m0, m1], RAYDIUM_CLMM_PROGRAM_ID)
40
+ if (protocols.has('raydium-cpmm')) for (const c of cpmm) add('raydium-cpmm', [utf8('pool'), c, m0, m1], RAYDIUM_CPMM_PROGRAM_ID)
41
+ if (protocols.has('orca-whirlpool')) for (const t of ORCA_FEE_TIERS) add('orca-whirlpool', [utf8('whirlpool'), orca, m0, m1, new Uint8Array([t & 0xff, t >> 8])], ORCA_WHIRLPOOL_PROGRAM_ID)
42
+ return out
43
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * 资产总值(子路径 `@w3lib/solana-batch-call/price`):代币余额 × 价格,可选加上 LP 仓位的价值
3
+ */
4
+ import type { RequestOptions, SolanaClient } from './client.js'
5
+ import { lpPositions, methodUnavailable, type LpPositionsOptions, type SolanaLpPosition } from './lp.js'
6
+ import { combinePrices, defillamaPrices, onchainPrices, tokenPrices, type SolanaPriceSource } from './price.js'
7
+
8
+ export interface PortfolioOptions extends RequestOptions {
9
+ /**
10
+ * 要计入的代币(SOL 用 NATIVE_MINT),按关联代币账户(ATA)读余额,免费公共节点也能用。
11
+ * 不传时列出地址持有的全部代币(同 getOwnerTokens,含 SOL,不含 NFT),需要支持 getTokenAccountsByOwner 的节点
12
+ */
13
+ tokens?: readonly string[]
14
+ /** 价格源,依次使用,默认 [defillamaPrices(), onchainPrices()] */
15
+ sources?: readonly SolanaPriceSource[]
16
+ /**
17
+ * 同时计入 LP 仓位(参数同 lpPositions,价格源用上面的 sources),默认不计入。
18
+ * 注意 lpPositions 不传 mints 时也要扫描代币账户:免费公共节点上传 { mints: [仓位 NFT / LP mint], pools: [AMM 池子] }
19
+ */
20
+ lp?: boolean | Omit<LpPositionsOptions, 'prices' | 'onPriceError' | 'fetch' | 'signal'>
21
+ /** 价值低于它的代币不列出(不影响合计),默认 0 */
22
+ minUsd?: number
23
+ /** 某个价格源出错时调用;回调本身出错会被忽略 */
24
+ onPriceError?: (err: unknown) => void
25
+ /** 查价格用的 fetch,默认全局 fetch */
26
+ fetch?: typeof fetch
27
+ }
28
+
29
+ export interface PortfolioToken {
30
+ /** 代币的 mint;SOL 为 NATIVE_MINT(或传入的 nativeMints 里的地址) */
31
+ token: string
32
+ /** 原生 SOL;Wrapped SOL 的代币账户单独列出(native: false),价格同 SOL */
33
+ native: boolean
34
+ symbol: string | null
35
+ balance: string
36
+ decimals: number
37
+ formatted: string
38
+ price: number | null
39
+ /** 美元价值;没有价格时为 null */
40
+ value: number | null
41
+ /** 给出价格的价格源 */
42
+ priceSource: string | null
43
+ }
44
+
45
+ export interface Portfolio {
46
+ /** 合计(代币 + LP;没有价格的不计入) */
47
+ totalUsd: number
48
+ tokensUsd: number
49
+ lpUsd: number
50
+ /** 有余额的代币,按价值从高到低 */
51
+ tokens: PortfolioToken[]
52
+ /** LP 仓位(开启 lp 时) */
53
+ lp: SolanaLpPosition[]
54
+ /** 有余额但没有价格的代币数(LP 仓位里缺价格的也算) */
55
+ unpriced: number
56
+ }
57
+
58
+ interface Held {
59
+ token: string
60
+ native: boolean
61
+ symbol: string | null
62
+ balance: string
63
+ decimals: number
64
+ formatted: string
65
+ }
66
+
67
+ /**
68
+ * 一个地址的资产总值:代币余额 × 价格,可选加上 LP 仓位。
69
+ *
70
+ * ```ts
71
+ * const p = await portfolio(client, owner, { tokens: [NATIVE_MINT, USDC, JUP] }) // 指定代币:免费公共节点也能用
72
+ * const all = await portfolio(new SolanaClient('https://my-rpc…'), owner, { lp: true }) // 全部持仓 + LP:需要支持 getTokenAccountsByOwner 的节点
73
+ * p.totalUsd // 1234.5
74
+ * ```
75
+ */
76
+ export async function portfolio(client: SolanaClient, owner: string, options: PortfolioOptions = {}): Promise<Portfolio> {
77
+ const { tokens, sources = [defillamaPrices(), onchainPrices()], lp, minUsd = 0, onPriceError, fetch: fetchFn, signal } = options
78
+ // 回调本身出错由 combinePrices / lpPositions 忽略
79
+ const onError = onPriceError ? (_: SolanaPriceSource, err: unknown) => onPriceError(err) : undefined
80
+ const [balances, positions] = await Promise.all([
81
+ holdings(client, owner, tokens, signal),
82
+ lp
83
+ ? lpPositions(client, owner, { ...(lp === true ? {} : lp), prices: combinePrices(sources, { onError }), fetch: fetchFn, onPriceError, signal })
84
+ : Promise.resolve([]),
85
+ ])
86
+ // 列出全部持仓时,LP 代币已经按仓位计入,不再作为代币重复计算
87
+ const lpMints = new Set(positions.flatMap((p) => (p.type === 'amm' ? [p.lpMint] : [])))
88
+ const held = balances.filter((b) => BigInt(b.balance) > 0n && !(tokens === undefined && lpMints.has(b.token)))
89
+ const prices = held.length ? await tokenPrices(client, held.map((b) => b.token), { sources, onError, fetch: fetchFn, signal }) : []
90
+ const list: PortfolioToken[] = held.map((b, i) => {
91
+ const p = prices[i]
92
+ const price = p?.price ?? null
93
+ return { ...b, price, value: price === null ? null : Number(b.formatted) * price, priceSource: p?.source ?? null }
94
+ })
95
+ const tokensUsd = list.reduce((sum, t) => sum + (t.value ?? 0), 0)
96
+ const lpUsd = positions.reduce((sum, p) => sum + (p.valueUsd ?? 0), 0)
97
+ return {
98
+ totalUsd: tokensUsd + lpUsd,
99
+ tokensUsd,
100
+ lpUsd,
101
+ tokens: list.filter((t) => (t.value ?? 0) >= minUsd || (minUsd === 0 && t.value === null)).sort((a, b) => (b.value ?? -1) - (a.value ?? -1)),
102
+ lp: positions,
103
+ unpriced: list.filter((t) => t.price === null).length + positions.filter((p) => p.valueUsd === null).length,
104
+ }
105
+ }
106
+
107
+ /** 余额:传 tokens 时读这些代币的 ATA,否则列出全部持仓 */
108
+ async function holdings(client: SolanaClient, owner: string, tokens: readonly string[] | undefined, signal: AbortSignal | undefined): Promise<Held[]> {
109
+ if (tokens) {
110
+ const balances = tokens.length ? await client.balances(owner, tokens, { symbol: true, signal }) : []
111
+ return balances.filter((b) => b.success).map((b) => ({ token: b.token, native: b.native, symbol: b.symbol ?? null, balance: b.balance, decimals: b.decimals, formatted: b.formatted }))
112
+ }
113
+ const owned = await client.ownerTokens(owner, { signal }).catch((err: unknown) => {
114
+ if (signal?.aborted || !methodUnavailable(err)) {
115
+ throw err // 取消、限频、超时等原样抛出(保留错误类型)
116
+ }
117
+ throw new Error(
118
+ `Listing the tokens of ${owner} failed (${err instanceof Error ? err.message : String(err)}). ` +
119
+ 'Listing needs getTokenAccountsByOwner (most free public nodes do not support it): pass the mints in `tokens`, or use a node that supports it',
120
+ { cause: err },
121
+ )
122
+ })
123
+ return owned.map((t) => ({ token: t.token, native: t.native, symbol: t.symbol ?? null, balance: t.balance, decimals: t.decimals, formatted: t.formatted }))
124
+ }