@sip-protocol/sdk 0.7.3 → 0.8.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 (263) hide show
  1. package/README.md +267 -0
  2. package/dist/{TransportWebUSB-TQ7WZ4LE.mjs → TransportWebUSB-YQMAGJAJ.mjs} +12 -9
  3. package/dist/browser.d.mts +10 -4
  4. package/dist/browser.d.ts +10 -4
  5. package/dist/browser.js +47556 -19603
  6. package/dist/browser.mjs +628 -48
  7. package/dist/chunk-4GRJ5MAW.mjs +152 -0
  8. package/dist/chunk-5D7A3L3W.mjs +717 -0
  9. package/dist/chunk-64AYA5F5.mjs +7834 -0
  10. package/dist/chunk-GMDGB22A.mjs +379 -0
  11. package/dist/chunk-I534WKN7.mjs +328 -0
  12. package/dist/chunk-IBZVA5Y7.mjs +1003 -0
  13. package/dist/chunk-PRRZAWJE.mjs +223 -0
  14. package/dist/{chunk-UJCSKKID.mjs → chunk-XGB3TDIC.mjs} +13 -1
  15. package/dist/{chunk-3M3HNQCW.mjs → chunk-YWGJ77A2.mjs} +28656 -13103
  16. package/dist/{chunk-6WGN57S2.mjs → chunk-Z3K7W5S3.mjs} +48 -0
  17. package/dist/constants-LHAAUC2T.mjs +51 -0
  18. package/dist/dist-2OGQ7FED.mjs +3957 -0
  19. package/dist/dist-IFHPYLDX.mjs +254 -0
  20. package/dist/fulfillment_proof-ANHVPKTB.mjs +21 -0
  21. package/dist/funding_proof-ICFZ5LHY.mjs +21 -0
  22. package/dist/{index-DIBZHOOQ.d.ts → index-DXh2IGkz.d.ts} +21239 -10304
  23. package/dist/{index-8MQz13eJ.d.mts → index-DeE1ZzA4.d.mts} +21239 -10304
  24. package/dist/index.d.mts +9 -3
  25. package/dist/index.d.ts +9 -3
  26. package/dist/index.js +48396 -19623
  27. package/dist/index.mjs +537 -19
  28. package/dist/interface-Bf7w1PLW.d.mts +679 -0
  29. package/dist/interface-Bf7w1PLW.d.ts +679 -0
  30. package/dist/{noir-DKfEzWy9.d.mts → noir-kzbLVTei.d.mts} +31 -21
  31. package/dist/{noir-DKfEzWy9.d.ts → noir-kzbLVTei.d.ts} +31 -21
  32. package/dist/proofs/halo2.d.mts +151 -0
  33. package/dist/proofs/halo2.d.ts +151 -0
  34. package/dist/proofs/halo2.js +350 -0
  35. package/dist/proofs/halo2.mjs +11 -0
  36. package/dist/proofs/kimchi.d.mts +160 -0
  37. package/dist/proofs/kimchi.d.ts +160 -0
  38. package/dist/proofs/kimchi.js +431 -0
  39. package/dist/proofs/kimchi.mjs +13 -0
  40. package/dist/proofs/noir.d.mts +1 -1
  41. package/dist/proofs/noir.d.ts +1 -1
  42. package/dist/proofs/noir.js +74 -18
  43. package/dist/proofs/noir.mjs +84 -24
  44. package/dist/solana-U3MEGU7W.mjs +280 -0
  45. package/dist/validity_proof-3POXLPNY.mjs +21 -0
  46. package/package.json +44 -11
  47. package/src/adapters/index.ts +41 -0
  48. package/src/adapters/jupiter.ts +571 -0
  49. package/src/adapters/near-intents.ts +135 -0
  50. package/src/advisor/advisor.ts +653 -0
  51. package/src/advisor/index.ts +54 -0
  52. package/src/advisor/tools.ts +303 -0
  53. package/src/advisor/types.ts +164 -0
  54. package/src/chains/ethereum/announcement.ts +536 -0
  55. package/src/chains/ethereum/bnb-optimizations.ts +474 -0
  56. package/src/chains/ethereum/commitment.ts +522 -0
  57. package/src/chains/ethereum/constants.ts +462 -0
  58. package/src/chains/ethereum/deployment.ts +596 -0
  59. package/src/chains/ethereum/gas-estimation.ts +538 -0
  60. package/src/chains/ethereum/index.ts +268 -0
  61. package/src/chains/ethereum/optimizations.ts +614 -0
  62. package/src/chains/ethereum/privacy-adapter.ts +855 -0
  63. package/src/chains/ethereum/registry.ts +584 -0
  64. package/src/chains/ethereum/rpc.ts +905 -0
  65. package/src/chains/ethereum/stealth.ts +491 -0
  66. package/src/chains/ethereum/token.ts +790 -0
  67. package/src/chains/ethereum/transfer.ts +637 -0
  68. package/src/chains/ethereum/types.ts +456 -0
  69. package/src/chains/ethereum/viewing-key.ts +455 -0
  70. package/src/chains/near/commitment.ts +608 -0
  71. package/src/chains/near/constants.ts +284 -0
  72. package/src/chains/near/function-call.ts +871 -0
  73. package/src/chains/near/history.ts +654 -0
  74. package/src/chains/near/implicit-account.ts +840 -0
  75. package/src/chains/near/index.ts +393 -0
  76. package/src/chains/near/native-transfer.ts +658 -0
  77. package/src/chains/near/nep141.ts +775 -0
  78. package/src/chains/near/privacy-adapter.ts +889 -0
  79. package/src/chains/near/resolver.ts +971 -0
  80. package/src/chains/near/rpc.ts +1016 -0
  81. package/src/chains/near/stealth.ts +419 -0
  82. package/src/chains/near/types.ts +317 -0
  83. package/src/chains/near/viewing-key.ts +876 -0
  84. package/src/chains/solana/anchor-transfer.ts +386 -0
  85. package/src/chains/solana/commitment.ts +577 -0
  86. package/src/chains/solana/constants.ts +126 -12
  87. package/src/chains/solana/ephemeral-keys.ts +543 -0
  88. package/src/chains/solana/index.ts +252 -1
  89. package/src/chains/solana/key-derivation.ts +418 -0
  90. package/src/chains/solana/kit-compat.ts +334 -0
  91. package/src/chains/solana/optimizations.ts +560 -0
  92. package/src/chains/solana/privacy-adapter.ts +605 -0
  93. package/src/chains/solana/providers/generic.ts +47 -6
  94. package/src/chains/solana/providers/helius-enhanced-types.ts +336 -0
  95. package/src/chains/solana/providers/helius-enhanced.ts +623 -0
  96. package/src/chains/solana/providers/helius.ts +186 -33
  97. package/src/chains/solana/providers/index.ts +31 -0
  98. package/src/chains/solana/providers/interface.ts +61 -18
  99. package/src/chains/solana/providers/quicknode.ts +409 -0
  100. package/src/chains/solana/providers/triton.ts +426 -0
  101. package/src/chains/solana/providers/webhook.ts +338 -67
  102. package/src/chains/solana/rpc-client.ts +1150 -0
  103. package/src/chains/solana/scan.ts +83 -66
  104. package/src/chains/solana/sol-transfer.ts +732 -0
  105. package/src/chains/solana/spl-transfer.ts +886 -0
  106. package/src/chains/solana/stealth-scanner.ts +703 -0
  107. package/src/chains/solana/sunspot-verifier.ts +453 -0
  108. package/src/chains/solana/transaction-builder.ts +755 -0
  109. package/src/chains/solana/transfer.ts +74 -5
  110. package/src/chains/solana/types.ts +57 -6
  111. package/src/chains/solana/utils.ts +110 -0
  112. package/src/chains/solana/viewing-key.ts +807 -0
  113. package/src/compliance/fireblocks.ts +921 -0
  114. package/src/compliance/index.ts +23 -0
  115. package/src/compliance/range-sas.ts +398 -33
  116. package/src/config/endpoints.ts +100 -0
  117. package/src/crypto.ts +11 -8
  118. package/src/errors.ts +82 -0
  119. package/src/evm/erc4337-relayer.ts +830 -0
  120. package/src/evm/index.ts +47 -0
  121. package/src/fees/calculator.ts +396 -0
  122. package/src/fees/index.ts +87 -0
  123. package/src/fees/near-contract.ts +429 -0
  124. package/src/fees/types.ts +268 -0
  125. package/src/index.ts +686 -1
  126. package/src/intent.ts +6 -3
  127. package/src/logger.ts +324 -0
  128. package/src/network/index.ts +80 -0
  129. package/src/network/proxy.ts +691 -0
  130. package/src/optimizations/index.ts +541 -0
  131. package/src/oracle/types.ts +1 -0
  132. package/src/privacy-backends/arcium-types.ts +727 -0
  133. package/src/privacy-backends/arcium.ts +719 -0
  134. package/src/privacy-backends/combined-privacy.ts +866 -0
  135. package/src/privacy-backends/cspl-token.ts +595 -0
  136. package/src/privacy-backends/cspl-types.ts +512 -0
  137. package/src/privacy-backends/cspl.ts +907 -0
  138. package/src/privacy-backends/health.ts +488 -0
  139. package/src/privacy-backends/inco-types.ts +323 -0
  140. package/src/privacy-backends/inco.ts +616 -0
  141. package/src/privacy-backends/index.ts +254 -4
  142. package/src/privacy-backends/interface.ts +649 -6
  143. package/src/privacy-backends/lru-cache.ts +343 -0
  144. package/src/privacy-backends/magicblock.ts +458 -0
  145. package/src/privacy-backends/mock.ts +258 -0
  146. package/src/privacy-backends/privacycash.ts +13 -17
  147. package/src/privacy-backends/private-swap.ts +570 -0
  148. package/src/privacy-backends/rate-limiter.ts +683 -0
  149. package/src/privacy-backends/registry.ts +414 -2
  150. package/src/privacy-backends/router.ts +283 -3
  151. package/src/privacy-backends/shadowwire.ts +449 -0
  152. package/src/privacy-backends/sip-native.ts +3 -0
  153. package/src/privacy-logger.ts +191 -0
  154. package/src/production-safety.ts +373 -0
  155. package/src/proofs/aggregator.ts +1029 -0
  156. package/src/proofs/browser-composer.ts +1150 -0
  157. package/src/proofs/browser.ts +113 -25
  158. package/src/proofs/cache/index.ts +127 -0
  159. package/src/proofs/cache/interface.ts +545 -0
  160. package/src/proofs/cache/key-generator.ts +188 -0
  161. package/src/proofs/cache/lru-cache.ts +481 -0
  162. package/src/proofs/cache/multi-tier-cache.ts +575 -0
  163. package/src/proofs/cache/persistent-cache.ts +788 -0
  164. package/src/proofs/compliance-proof.ts +872 -0
  165. package/src/proofs/composer/base.ts +923 -0
  166. package/src/proofs/composer/index.ts +25 -0
  167. package/src/proofs/composer/interface.ts +518 -0
  168. package/src/proofs/composer/types.ts +383 -0
  169. package/src/proofs/converters/halo2.ts +452 -0
  170. package/src/proofs/converters/index.ts +208 -0
  171. package/src/proofs/converters/interface.ts +363 -0
  172. package/src/proofs/converters/kimchi.ts +462 -0
  173. package/src/proofs/converters/noir.ts +451 -0
  174. package/src/proofs/fallback.ts +888 -0
  175. package/src/proofs/halo2.ts +42 -0
  176. package/src/proofs/index.ts +471 -0
  177. package/src/proofs/interface.ts +13 -0
  178. package/src/proofs/kimchi.ts +42 -0
  179. package/src/proofs/lazy.ts +1004 -0
  180. package/src/proofs/mock.ts +25 -1
  181. package/src/proofs/noir.ts +110 -29
  182. package/src/proofs/orchestrator.ts +960 -0
  183. package/src/proofs/parallel/concurrency.ts +297 -0
  184. package/src/proofs/parallel/dependency-graph.ts +602 -0
  185. package/src/proofs/parallel/executor.ts +420 -0
  186. package/src/proofs/parallel/index.ts +131 -0
  187. package/src/proofs/parallel/interface.ts +685 -0
  188. package/src/proofs/parallel/worker-pool.ts +644 -0
  189. package/src/proofs/providers/halo2.ts +560 -0
  190. package/src/proofs/providers/index.ts +34 -0
  191. package/src/proofs/providers/kimchi.ts +641 -0
  192. package/src/proofs/validator.ts +881 -0
  193. package/src/proofs/verifier.ts +867 -0
  194. package/src/quantum/index.ts +112 -0
  195. package/src/quantum/winternitz-vault.ts +639 -0
  196. package/src/quantum/wots.ts +611 -0
  197. package/src/settlement/backends/direct-chain.ts +1 -0
  198. package/src/settlement/index.ts +9 -0
  199. package/src/settlement/router.ts +732 -46
  200. package/src/solana/index.ts +72 -0
  201. package/src/solana/jito-relayer.ts +687 -0
  202. package/src/solana/noir-verifier-types.ts +430 -0
  203. package/src/solana/noir-verifier.ts +816 -0
  204. package/src/stealth/address-derivation.ts +193 -0
  205. package/src/stealth/ed25519.ts +431 -0
  206. package/src/stealth/index.ts +233 -0
  207. package/src/stealth/meta-address.ts +221 -0
  208. package/src/stealth/secp256k1.ts +368 -0
  209. package/src/stealth/utils.ts +194 -0
  210. package/src/stealth.ts +50 -1504
  211. package/src/sync/index.ts +106 -0
  212. package/src/sync/manager.ts +504 -0
  213. package/src/sync/mock-provider.ts +318 -0
  214. package/src/sync/oblivious.ts +625 -0
  215. package/src/tokens/index.ts +15 -0
  216. package/src/tokens/registry.ts +301 -0
  217. package/src/utils/deprecation.ts +94 -0
  218. package/src/utils/index.ts +9 -0
  219. package/src/wallet/ethereum/index.ts +68 -0
  220. package/src/wallet/ethereum/metamask-privacy.ts +420 -0
  221. package/src/wallet/ethereum/multi-wallet.ts +646 -0
  222. package/src/wallet/ethereum/privacy-adapter.ts +700 -0
  223. package/src/wallet/ethereum/types.ts +3 -1
  224. package/src/wallet/ethereum/walletconnect-adapter.ts +675 -0
  225. package/src/wallet/hardware/index.ts +10 -0
  226. package/src/wallet/hardware/ledger-privacy.ts +414 -0
  227. package/src/wallet/index.ts +71 -0
  228. package/src/wallet/near/adapter.ts +626 -0
  229. package/src/wallet/near/index.ts +86 -0
  230. package/src/wallet/near/meteor-wallet.ts +1153 -0
  231. package/src/wallet/near/my-near-wallet.ts +790 -0
  232. package/src/wallet/near/wallet-selector.ts +702 -0
  233. package/src/wallet/solana/adapter.ts +6 -4
  234. package/src/wallet/solana/index.ts +13 -0
  235. package/src/wallet/solana/privacy-adapter.ts +567 -0
  236. package/src/wallet/sui/types.ts +6 -4
  237. package/src/zcash/rpc-client.ts +13 -6
  238. package/dist/chunk-2XIVXWHA.mjs +0 -1930
  239. package/dist/chunk-3INS3PR5.mjs +0 -884
  240. package/dist/chunk-3OVABDRH.mjs +0 -17096
  241. package/dist/chunk-7RFRWDCW.mjs +0 -1504
  242. package/dist/chunk-DLDWZFYC.mjs +0 -1495
  243. package/dist/chunk-E6SZWREQ.mjs +0 -57
  244. package/dist/chunk-F6F73W35.mjs +0 -16166
  245. package/dist/chunk-G33LB27A.mjs +0 -16166
  246. package/dist/chunk-HGU6HZRC.mjs +0 -231
  247. package/dist/chunk-L2K34JCU.mjs +0 -1496
  248. package/dist/chunk-OFDBEIEK.mjs +0 -16166
  249. package/dist/chunk-SF7YSLF5.mjs +0 -1515
  250. package/dist/chunk-SN4ZDTVW.mjs +0 -16166
  251. package/dist/chunk-WWUSGOXE.mjs +0 -17129
  252. package/dist/constants-VOI7BSLK.mjs +0 -27
  253. package/dist/index-B71aXVzk.d.ts +0 -13264
  254. package/dist/index-BYZbDjal.d.ts +0 -11390
  255. package/dist/index-CHB3KuOB.d.mts +0 -11859
  256. package/dist/index-CzWPI6Le.d.ts +0 -11859
  257. package/dist/index-pOIIuwfV.d.mts +0 -13264
  258. package/dist/index-xbWjohNq.d.mts +0 -11390
  259. package/dist/solana-4O4K45VU.mjs +0 -46
  260. package/dist/solana-5EMCTPTS.mjs +0 -46
  261. package/dist/solana-NDABAZ6P.mjs +0 -56
  262. package/dist/solana-Q4NAVBTS.mjs +0 -46
  263. package/dist/solana-ZYO63LY5.mjs +0 -46
@@ -0,0 +1,886 @@
1
+ /**
2
+ * Enhanced SPL Token Transfer with Privacy Wrapping
3
+ *
4
+ * Provides advanced SPL token transfer functionality with:
5
+ * - Token metadata resolution for UI display
6
+ * - Token balance validation before transfer
7
+ * - Batch transfer support
8
+ * - Enhanced error handling
9
+ *
10
+ * @module chains/solana/spl-transfer
11
+ */
12
+
13
+ import {
14
+ Connection,
15
+ PublicKey,
16
+ Transaction,
17
+ TransactionInstruction,
18
+ type Commitment,
19
+ } from '@solana/web3.js'
20
+ import {
21
+ getAssociatedTokenAddress,
22
+ createAssociatedTokenAccountInstruction,
23
+ createTransferInstruction,
24
+ TOKEN_PROGRAM_ID,
25
+ ASSOCIATED_TOKEN_PROGRAM_ID,
26
+ getAccount,
27
+ getMint,
28
+ } from '@solana/spl-token'
29
+ import {
30
+ generateEd25519StealthAddress,
31
+ ed25519PublicKeyToSolanaAddress,
32
+ } from '../../stealth'
33
+ import { ValidationError } from '../../errors'
34
+ import type { StealthMetaAddress } from '@sip-protocol/types'
35
+ import { createAnnouncementMemo } from './types'
36
+ import {
37
+ MEMO_PROGRAM_ID,
38
+ getExplorerUrl,
39
+ ESTIMATED_TX_FEE_LAMPORTS,
40
+ type SolanaCluster,
41
+ } from './constants'
42
+
43
+ // ─── Types ────────────────────────────────────────────────────────────────────
44
+
45
+ /**
46
+ * Token metadata for UI display
47
+ */
48
+ export interface TokenMetadata {
49
+ /** Token mint address */
50
+ mint: string
51
+ /** Token name (e.g., "USD Coin") */
52
+ name: string
53
+ /** Token symbol (e.g., "USDC") */
54
+ symbol: string
55
+ /** Number of decimals */
56
+ decimals: number
57
+ /** Token logo URI */
58
+ logoUri?: string
59
+ /** Token supply */
60
+ supply?: bigint
61
+ /** Is token frozen */
62
+ isFrozen?: boolean
63
+ }
64
+
65
+ /**
66
+ * Token balance information
67
+ */
68
+ export interface TokenBalance {
69
+ /** Token mint address */
70
+ mint: string
71
+ /** Balance in smallest unit (raw) */
72
+ amount: bigint
73
+ /** Balance formatted with decimals */
74
+ uiAmount: number
75
+ /** Token decimals */
76
+ decimals: number
77
+ /** Associated token account address */
78
+ tokenAccount: string
79
+ }
80
+
81
+ /**
82
+ * Enhanced transfer parameters with validation options
83
+ */
84
+ export interface EnhancedSPLTransferParams {
85
+ /** Solana RPC connection */
86
+ connection: Connection
87
+ /** Sender's public key */
88
+ sender: PublicKey
89
+ /** Sender's token account (ATA) - auto-detected if not provided */
90
+ senderTokenAccount?: PublicKey
91
+ /** Recipient's stealth meta-address */
92
+ recipientMetaAddress: StealthMetaAddress
93
+ /** SPL token mint address */
94
+ mint: PublicKey
95
+ /** Amount to transfer (in token's smallest unit) */
96
+ amount: bigint
97
+ /** Function to sign the transaction */
98
+ signTransaction: <T extends Transaction>(tx: T) => Promise<T>
99
+ /** Skip balance validation (default: false) */
100
+ skipBalanceCheck?: boolean
101
+ /** Transaction commitment level */
102
+ commitment?: Commitment
103
+ /** Custom memo to append (will be added after SIP announcement) */
104
+ customMemo?: string
105
+ }
106
+
107
+ /**
108
+ * Enhanced transfer result with metadata
109
+ */
110
+ export interface EnhancedSPLTransferResult {
111
+ /** Transaction signature */
112
+ txSignature: string
113
+ /** Stealth address (base58 Solana address) */
114
+ stealthAddress: string
115
+ /** Ephemeral public key (base58) for recipient scanning */
116
+ ephemeralPublicKey: string
117
+ /** View tag for efficient scanning */
118
+ viewTag: string
119
+ /** Explorer URL for the transaction */
120
+ explorerUrl: string
121
+ /** Cluster the transaction was sent on */
122
+ cluster: SolanaCluster
123
+ /** Token metadata */
124
+ tokenMetadata: TokenMetadata
125
+ /** Amount transferred */
126
+ amount: bigint
127
+ /** UI amount (formatted with decimals) */
128
+ uiAmount: number
129
+ /** Whether ATA was created */
130
+ ataCreated: boolean
131
+ /** Estimated fee paid */
132
+ estimatedFee: bigint
133
+ }
134
+
135
+ /**
136
+ * Batch transfer item
137
+ */
138
+ export interface BatchTransferItem {
139
+ /** Recipient's stealth meta-address */
140
+ recipientMetaAddress: StealthMetaAddress
141
+ /** Amount to transfer (in token's smallest unit) */
142
+ amount: bigint
143
+ /** Custom memo for this transfer */
144
+ customMemo?: string
145
+ }
146
+
147
+ /**
148
+ * Batch transfer result
149
+ */
150
+ export interface BatchTransferResult {
151
+ /** Transaction signature */
152
+ txSignature: string
153
+ /** Individual transfer results */
154
+ transfers: Array<{
155
+ stealthAddress: string
156
+ ephemeralPublicKey: string
157
+ viewTag: string
158
+ amount: bigint
159
+ uiAmount: number
160
+ }>
161
+ /** Explorer URL */
162
+ explorerUrl: string
163
+ /** Cluster */
164
+ cluster: SolanaCluster
165
+ /** Total amount transferred */
166
+ totalAmount: bigint
167
+ /** Token metadata */
168
+ tokenMetadata: TokenMetadata
169
+ }
170
+
171
+ /**
172
+ * Transfer validation result
173
+ */
174
+ export interface TransferValidation {
175
+ /** Whether transfer is valid */
176
+ isValid: boolean
177
+ /** Validation errors (if any) */
178
+ errors: string[]
179
+ /** Sender's token balance */
180
+ senderBalance?: TokenBalance
181
+ /** Estimated total fee */
182
+ estimatedFee: bigint
183
+ /** Whether ATA needs creation */
184
+ needsAtaCreation: boolean
185
+ /** Token metadata */
186
+ tokenMetadata?: TokenMetadata
187
+ }
188
+
189
+ // ─── Token Metadata ───────────────────────────────────────────────────────────
190
+
191
+ /**
192
+ * Well-known token metadata (mainnet)
193
+ * Used as fallback when metadata cannot be fetched
194
+ */
195
+ const KNOWN_TOKENS: Record<string, Omit<TokenMetadata, 'mint' | 'supply' | 'isFrozen'>> = {
196
+ // USDC
197
+ 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v': {
198
+ name: 'USD Coin',
199
+ symbol: 'USDC',
200
+ decimals: 6,
201
+ logoUri: 'https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v/logo.png',
202
+ },
203
+ // USDT
204
+ 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB': {
205
+ name: 'Tether USD',
206
+ symbol: 'USDT',
207
+ decimals: 6,
208
+ logoUri: 'https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB/logo.png',
209
+ },
210
+ // SOL (wrapped)
211
+ 'So11111111111111111111111111111111111111112': {
212
+ name: 'Wrapped SOL',
213
+ symbol: 'SOL',
214
+ decimals: 9,
215
+ logoUri: 'https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/So11111111111111111111111111111111111111112/logo.png',
216
+ },
217
+ // BONK
218
+ 'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263': {
219
+ name: 'Bonk',
220
+ symbol: 'BONK',
221
+ decimals: 5,
222
+ logoUri: 'https://arweave.net/hQiPZOsRZXGXBJd_82PhVdlM_hACsT_q6wqwf5cSY7I',
223
+ },
224
+ // JUP
225
+ 'JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN': {
226
+ name: 'Jupiter',
227
+ symbol: 'JUP',
228
+ decimals: 6,
229
+ logoUri: 'https://static.jup.ag/jup/icon.png',
230
+ },
231
+ // RAY
232
+ '4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R': {
233
+ name: 'Raydium',
234
+ symbol: 'RAY',
235
+ decimals: 6,
236
+ logoUri: 'https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R/logo.png',
237
+ },
238
+ // PYTH
239
+ 'HZ1JovNiVvGrGNiiYvEozEVgZ58xaU3RKwX8eACQBCt3': {
240
+ name: 'Pyth Network',
241
+ symbol: 'PYTH',
242
+ decimals: 6,
243
+ logoUri: 'https://pyth.network/token.svg',
244
+ },
245
+ }
246
+
247
+ /**
248
+ * Resolve token metadata from mint address
249
+ *
250
+ * Attempts to fetch metadata from on-chain, falls back to known tokens.
251
+ *
252
+ * @param connection - Solana RPC connection
253
+ * @param mint - Token mint address
254
+ * @returns Token metadata
255
+ */
256
+ export async function resolveTokenMetadata(
257
+ connection: Connection,
258
+ mint: PublicKey
259
+ ): Promise<TokenMetadata> {
260
+ const mintAddress = mint.toBase58()
261
+
262
+ // Try known tokens first
263
+ const known = KNOWN_TOKENS[mintAddress]
264
+
265
+ try {
266
+ // Fetch on-chain mint data
267
+ const mintInfo = await getMint(connection, mint)
268
+
269
+ if (known) {
270
+ return {
271
+ mint: mintAddress,
272
+ ...known,
273
+ supply: mintInfo.supply,
274
+ isFrozen: mintInfo.freezeAuthority !== null,
275
+ }
276
+ }
277
+
278
+ // Unknown token - return basic info
279
+ return {
280
+ mint: mintAddress,
281
+ name: `Token ${mintAddress.slice(0, 8)}...`,
282
+ symbol: mintAddress.slice(0, 4).toUpperCase(),
283
+ decimals: mintInfo.decimals,
284
+ supply: mintInfo.supply,
285
+ isFrozen: mintInfo.freezeAuthority !== null,
286
+ }
287
+ } catch (error) {
288
+ // Fallback if mint fetch fails
289
+ if (known) {
290
+ return {
291
+ mint: mintAddress,
292
+ ...known,
293
+ }
294
+ }
295
+
296
+ throw new ValidationError(
297
+ `Failed to resolve token metadata for ${mintAddress}: ${error instanceof Error ? error.message : 'Unknown error'}`,
298
+ 'mint'
299
+ )
300
+ }
301
+ }
302
+
303
+ /**
304
+ * Get multiple token metadata at once
305
+ *
306
+ * @param connection - Solana RPC connection
307
+ * @param mints - Array of token mint addresses
308
+ * @returns Array of token metadata
309
+ */
310
+ export async function batchResolveTokenMetadata(
311
+ connection: Connection,
312
+ mints: PublicKey[]
313
+ ): Promise<TokenMetadata[]> {
314
+ return Promise.all(mints.map(mint => resolveTokenMetadata(connection, mint)))
315
+ }
316
+
317
+ // ─── Token Balance ────────────────────────────────────────────────────────────
318
+
319
+ /**
320
+ * Get token balance for an address
321
+ *
322
+ * @param connection - Solana RPC connection
323
+ * @param owner - Owner public key
324
+ * @param mint - Token mint address
325
+ * @returns Token balance or null if no account
326
+ */
327
+ export async function getTokenBalance(
328
+ connection: Connection,
329
+ owner: PublicKey,
330
+ mint: PublicKey
331
+ ): Promise<TokenBalance | null> {
332
+ try {
333
+ const ata = await getAssociatedTokenAddress(mint, owner, true)
334
+ const account = await getAccount(connection, ata)
335
+ const mintInfo = await getMint(connection, mint)
336
+
337
+ return {
338
+ mint: mint.toBase58(),
339
+ amount: account.amount,
340
+ uiAmount: Number(account.amount) / Math.pow(10, mintInfo.decimals),
341
+ decimals: mintInfo.decimals,
342
+ tokenAccount: ata.toBase58(),
343
+ }
344
+ } catch {
345
+ return null
346
+ }
347
+ }
348
+
349
+ /**
350
+ * Get multiple token balances for an address
351
+ *
352
+ * @param connection - Solana RPC connection
353
+ * @param owner - Owner public key
354
+ * @param mints - Array of token mint addresses
355
+ * @returns Array of token balances (null for missing accounts)
356
+ */
357
+ export async function batchGetTokenBalances(
358
+ connection: Connection,
359
+ owner: PublicKey,
360
+ mints: PublicKey[]
361
+ ): Promise<(TokenBalance | null)[]> {
362
+ return Promise.all(mints.map(mint => getTokenBalance(connection, owner, mint)))
363
+ }
364
+
365
+ // ─── Transfer Validation ──────────────────────────────────────────────────────
366
+
367
+ /**
368
+ * Validate a transfer before execution
369
+ *
370
+ * Checks:
371
+ * - Sender has sufficient balance
372
+ * - Meta-address is valid for Solana
373
+ * - Amount is valid
374
+ * - ATA creation requirements
375
+ *
376
+ * @param params - Transfer parameters to validate
377
+ * @returns Validation result
378
+ */
379
+ export async function validateTransfer(
380
+ params: Omit<EnhancedSPLTransferParams, 'signTransaction'>
381
+ ): Promise<TransferValidation> {
382
+ const errors: string[] = []
383
+ let senderBalance: TokenBalance | undefined
384
+ let tokenMetadata: TokenMetadata | undefined
385
+ let needsAtaCreation = false
386
+ let estimatedFee = ESTIMATED_TX_FEE_LAMPORTS
387
+
388
+ // Validate meta-address
389
+ if (!params.recipientMetaAddress) {
390
+ errors.push('Recipient meta-address is required')
391
+ } else if (params.recipientMetaAddress.chain !== 'solana') {
392
+ errors.push(`Invalid chain: expected 'solana', got '${params.recipientMetaAddress.chain}'`)
393
+ }
394
+
395
+ // Validate amount
396
+ if (params.amount <= 0n) {
397
+ errors.push('Amount must be greater than 0')
398
+ }
399
+ if (params.amount > 2n ** 64n - 1n) {
400
+ errors.push('Amount exceeds maximum SPL token amount')
401
+ }
402
+
403
+ // Get sender balance
404
+ try {
405
+ const balance = await getTokenBalance(params.connection, params.sender, params.mint)
406
+ if (balance) {
407
+ senderBalance = balance
408
+ if (balance.amount < params.amount) {
409
+ errors.push(
410
+ `Insufficient balance: have ${balance.uiAmount}, need ${Number(params.amount) / Math.pow(10, balance.decimals)}`
411
+ )
412
+ }
413
+ } else {
414
+ errors.push('Sender does not have a token account for this mint')
415
+ }
416
+ } catch (error) {
417
+ errors.push(`Failed to check sender balance: ${error instanceof Error ? error.message : 'Unknown error'}`)
418
+ }
419
+
420
+ // Get token metadata
421
+ try {
422
+ tokenMetadata = await resolveTokenMetadata(params.connection, params.mint)
423
+ } catch (error) {
424
+ errors.push(`Failed to resolve token metadata: ${error instanceof Error ? error.message : 'Unknown error'}`)
425
+ }
426
+
427
+ // Check if stealth ATA needs creation
428
+ if (params.recipientMetaAddress && params.recipientMetaAddress.chain === 'solana') {
429
+ try {
430
+ const { stealthAddress } = generateEd25519StealthAddress(params.recipientMetaAddress)
431
+ const stealthAddressBase58 = ed25519PublicKeyToSolanaAddress(stealthAddress.address)
432
+ const stealthPubkey = new PublicKey(stealthAddressBase58)
433
+ const stealthATA = await getAssociatedTokenAddress(params.mint, stealthPubkey, true)
434
+
435
+ try {
436
+ await getAccount(params.connection, stealthATA)
437
+ } catch {
438
+ needsAtaCreation = true
439
+ const rentExemption = await params.connection.getMinimumBalanceForRentExemption(165)
440
+ estimatedFee += BigInt(rentExemption)
441
+ }
442
+ } catch {
443
+ // Cannot generate stealth address - error already captured
444
+ }
445
+ }
446
+
447
+ return {
448
+ isValid: errors.length === 0,
449
+ errors,
450
+ senderBalance,
451
+ estimatedFee,
452
+ needsAtaCreation,
453
+ tokenMetadata,
454
+ }
455
+ }
456
+
457
+ // ─── Enhanced Transfer ────────────────────────────────────────────────────────
458
+
459
+ /**
460
+ * Send SPL tokens privately with enhanced features
461
+ *
462
+ * Improvements over base transfer:
463
+ * - Auto-detects sender token account if not provided
464
+ * - Validates balance before transfer
465
+ * - Includes token metadata in result
466
+ * - Better error messages
467
+ *
468
+ * @param params - Enhanced transfer parameters
469
+ * @returns Enhanced transfer result
470
+ *
471
+ * @example
472
+ * ```typescript
473
+ * const result = await sendEnhancedSPLTransfer({
474
+ * connection,
475
+ * sender: wallet.publicKey,
476
+ * recipientMetaAddress: recipientMeta,
477
+ * mint: USDC_MINT,
478
+ * amount: 5_000_000n,
479
+ * signTransaction: wallet.signTransaction,
480
+ * })
481
+ *
482
+ * console.log(`Sent ${result.uiAmount} ${result.tokenMetadata.symbol}`)
483
+ * console.log(`To stealth address: ${result.stealthAddress}`)
484
+ * ```
485
+ */
486
+ export async function sendEnhancedSPLTransfer(
487
+ params: EnhancedSPLTransferParams
488
+ ): Promise<EnhancedSPLTransferResult> {
489
+ const {
490
+ connection,
491
+ sender,
492
+ recipientMetaAddress,
493
+ mint,
494
+ amount,
495
+ signTransaction,
496
+ skipBalanceCheck = false,
497
+ commitment = 'confirmed',
498
+ customMemo,
499
+ } = params
500
+
501
+ // Validate meta-address
502
+ if (!recipientMetaAddress) {
503
+ throw new ValidationError('recipientMetaAddress is required', 'recipientMetaAddress')
504
+ }
505
+ if (recipientMetaAddress.chain !== 'solana') {
506
+ throw new ValidationError(
507
+ `Invalid chain: expected 'solana', got '${recipientMetaAddress.chain}'`,
508
+ 'recipientMetaAddress.chain'
509
+ )
510
+ }
511
+
512
+ // Validate amount
513
+ if (amount <= 0n) {
514
+ throw new ValidationError('amount must be greater than 0', 'amount')
515
+ }
516
+ if (amount > 2n ** 64n - 1n) {
517
+ throw new ValidationError('amount exceeds maximum SPL token amount', 'amount')
518
+ }
519
+
520
+ // Resolve token metadata
521
+ const tokenMetadata = await resolveTokenMetadata(connection, mint)
522
+
523
+ // Auto-detect sender token account if not provided
524
+ let senderTokenAccount = params.senderTokenAccount
525
+ if (!senderTokenAccount) {
526
+ senderTokenAccount = await getAssociatedTokenAddress(mint, sender, false)
527
+ }
528
+
529
+ // Check balance (unless skipped)
530
+ if (!skipBalanceCheck) {
531
+ const balance = await getTokenBalance(connection, sender, mint)
532
+ if (!balance) {
533
+ throw new ValidationError(
534
+ `Sender does not have a ${tokenMetadata.symbol} token account`,
535
+ 'sender'
536
+ )
537
+ }
538
+ if (balance.amount < amount) {
539
+ const uiAmount = Number(amount) / Math.pow(10, tokenMetadata.decimals)
540
+ throw new ValidationError(
541
+ `Insufficient ${tokenMetadata.symbol} balance: have ${balance.uiAmount}, need ${uiAmount}`,
542
+ 'amount'
543
+ )
544
+ }
545
+ }
546
+
547
+ // Generate stealth address
548
+ const { stealthAddress } = generateEd25519StealthAddress(recipientMetaAddress)
549
+ const stealthAddressBase58 = ed25519PublicKeyToSolanaAddress(stealthAddress.address)
550
+ const stealthPubkey = new PublicKey(stealthAddressBase58)
551
+ const ephemeralPubkeyBase58 = ed25519PublicKeyToSolanaAddress(stealthAddress.ephemeralPublicKey)
552
+
553
+ // Get or create stealth ATA
554
+ const stealthATA = await getAssociatedTokenAddress(mint, stealthPubkey, true)
555
+
556
+ // Build transaction
557
+ const transaction = new Transaction()
558
+ let ataCreated = false
559
+ let estimatedFee = ESTIMATED_TX_FEE_LAMPORTS
560
+
561
+ // Check if stealth ATA exists
562
+ try {
563
+ await getAccount(connection, stealthATA)
564
+ } catch {
565
+ // Create ATA
566
+ transaction.add(
567
+ createAssociatedTokenAccountInstruction(
568
+ sender,
569
+ stealthATA,
570
+ stealthPubkey,
571
+ mint,
572
+ TOKEN_PROGRAM_ID,
573
+ ASSOCIATED_TOKEN_PROGRAM_ID
574
+ )
575
+ )
576
+ ataCreated = true
577
+ const rentExemption = await connection.getMinimumBalanceForRentExemption(165)
578
+ estimatedFee += BigInt(rentExemption)
579
+ }
580
+
581
+ // Add transfer instruction
582
+ transaction.add(
583
+ createTransferInstruction(
584
+ senderTokenAccount,
585
+ stealthATA,
586
+ sender,
587
+ amount
588
+ )
589
+ )
590
+
591
+ // Add SIP announcement memo
592
+ const viewTagHex = stealthAddress.viewTag.toString(16).padStart(2, '0')
593
+ const memoContent = createAnnouncementMemo(
594
+ ephemeralPubkeyBase58,
595
+ viewTagHex,
596
+ stealthAddressBase58
597
+ )
598
+
599
+ transaction.add(
600
+ new TransactionInstruction({
601
+ keys: [],
602
+ programId: new PublicKey(MEMO_PROGRAM_ID),
603
+ data: Buffer.from(memoContent, 'utf-8'),
604
+ })
605
+ )
606
+
607
+ // Add custom memo if provided
608
+ if (customMemo) {
609
+ transaction.add(
610
+ new TransactionInstruction({
611
+ keys: [],
612
+ programId: new PublicKey(MEMO_PROGRAM_ID),
613
+ data: Buffer.from(customMemo, 'utf-8'),
614
+ })
615
+ )
616
+ }
617
+
618
+ // Get blockhash and sign
619
+ const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash(commitment)
620
+ transaction.recentBlockhash = blockhash
621
+ transaction.lastValidBlockHeight = lastValidBlockHeight
622
+ transaction.feePayer = sender
623
+
624
+ const signedTx = await signTransaction(transaction)
625
+
626
+ // Send and confirm
627
+ const txSignature = await connection.sendRawTransaction(signedTx.serialize(), {
628
+ skipPreflight: false,
629
+ preflightCommitment: commitment,
630
+ })
631
+
632
+ await connection.confirmTransaction(
633
+ { signature: txSignature, blockhash, lastValidBlockHeight },
634
+ commitment
635
+ )
636
+
637
+ // Detect cluster
638
+ const cluster = detectCluster(connection.rpcEndpoint)
639
+
640
+ return {
641
+ txSignature,
642
+ stealthAddress: stealthAddressBase58,
643
+ ephemeralPublicKey: ephemeralPubkeyBase58,
644
+ viewTag: viewTagHex,
645
+ explorerUrl: getExplorerUrl(txSignature, cluster),
646
+ cluster,
647
+ tokenMetadata,
648
+ amount,
649
+ uiAmount: Number(amount) / Math.pow(10, tokenMetadata.decimals),
650
+ ataCreated,
651
+ estimatedFee,
652
+ }
653
+ }
654
+
655
+ // ─── Batch Transfer ───────────────────────────────────────────────────────────
656
+
657
+ /**
658
+ * Send SPL tokens to multiple stealth addresses in a single transaction
659
+ *
660
+ * Note: Solana transactions have size limits (~1232 bytes).
661
+ * Batch size is limited to prevent failures.
662
+ *
663
+ * @param connection - Solana RPC connection
664
+ * @param sender - Sender's public key
665
+ * @param senderTokenAccount - Sender's token account (optional, auto-detected)
666
+ * @param mint - Token mint address
667
+ * @param transfers - Array of transfer items
668
+ * @param signTransaction - Transaction signing function
669
+ * @returns Batch transfer result
670
+ */
671
+ export async function sendBatchSPLTransfer(
672
+ connection: Connection,
673
+ sender: PublicKey,
674
+ senderTokenAccount: PublicKey | undefined,
675
+ mint: PublicKey,
676
+ transfers: BatchTransferItem[],
677
+ signTransaction: <T extends Transaction>(tx: T) => Promise<T>
678
+ ): Promise<BatchTransferResult> {
679
+ // Validate batch size (max ~5-6 transfers per transaction due to size limits)
680
+ const MAX_BATCH_SIZE = 5
681
+ if (transfers.length > MAX_BATCH_SIZE) {
682
+ throw new ValidationError(
683
+ `Batch size ${transfers.length} exceeds maximum ${MAX_BATCH_SIZE}. Split into multiple transactions.`,
684
+ 'transfers'
685
+ )
686
+ }
687
+
688
+ if (transfers.length === 0) {
689
+ throw new ValidationError('At least one transfer is required', 'transfers')
690
+ }
691
+
692
+ // Resolve token metadata
693
+ const tokenMetadata = await resolveTokenMetadata(connection, mint)
694
+
695
+ // Auto-detect sender token account
696
+ if (!senderTokenAccount) {
697
+ senderTokenAccount = await getAssociatedTokenAddress(mint, sender, false)
698
+ }
699
+
700
+ // Calculate total amount
701
+ const totalAmount = transfers.reduce((sum, t) => sum + t.amount, 0n)
702
+
703
+ // Check balance
704
+ const balance = await getTokenBalance(connection, sender, mint)
705
+ if (!balance || balance.amount < totalAmount) {
706
+ const uiAmount = Number(totalAmount) / Math.pow(10, tokenMetadata.decimals)
707
+ throw new ValidationError(
708
+ `Insufficient ${tokenMetadata.symbol} balance for batch: need ${uiAmount}`,
709
+ 'amount'
710
+ )
711
+ }
712
+
713
+ // Build transaction
714
+ const transaction = new Transaction()
715
+ const transferResults: BatchTransferResult['transfers'] = []
716
+
717
+ for (const transfer of transfers) {
718
+ // Validate meta-address
719
+ if (transfer.recipientMetaAddress.chain !== 'solana') {
720
+ throw new ValidationError(
721
+ `Invalid chain for recipient: expected 'solana', got '${transfer.recipientMetaAddress.chain}'`,
722
+ 'recipientMetaAddress'
723
+ )
724
+ }
725
+
726
+ // Generate stealth address
727
+ const { stealthAddress } = generateEd25519StealthAddress(transfer.recipientMetaAddress)
728
+ const stealthAddressBase58 = ed25519PublicKeyToSolanaAddress(stealthAddress.address)
729
+ const stealthPubkey = new PublicKey(stealthAddressBase58)
730
+ const ephemeralPubkeyBase58 = ed25519PublicKeyToSolanaAddress(stealthAddress.ephemeralPublicKey)
731
+
732
+ // Get stealth ATA
733
+ const stealthATA = await getAssociatedTokenAddress(mint, stealthPubkey, true)
734
+
735
+ // Create ATA if needed
736
+ try {
737
+ await getAccount(connection, stealthATA)
738
+ } catch {
739
+ transaction.add(
740
+ createAssociatedTokenAccountInstruction(
741
+ sender,
742
+ stealthATA,
743
+ stealthPubkey,
744
+ mint,
745
+ TOKEN_PROGRAM_ID,
746
+ ASSOCIATED_TOKEN_PROGRAM_ID
747
+ )
748
+ )
749
+ }
750
+
751
+ // Add transfer
752
+ transaction.add(
753
+ createTransferInstruction(senderTokenAccount, stealthATA, sender, transfer.amount)
754
+ )
755
+
756
+ // Add announcement memo
757
+ const viewTagHex = stealthAddress.viewTag.toString(16).padStart(2, '0')
758
+ const memoContent = createAnnouncementMemo(
759
+ ephemeralPubkeyBase58,
760
+ viewTagHex,
761
+ stealthAddressBase58
762
+ )
763
+
764
+ transaction.add(
765
+ new TransactionInstruction({
766
+ keys: [],
767
+ programId: new PublicKey(MEMO_PROGRAM_ID),
768
+ data: Buffer.from(memoContent, 'utf-8'),
769
+ })
770
+ )
771
+
772
+ // Add custom memo if provided
773
+ if (transfer.customMemo) {
774
+ transaction.add(
775
+ new TransactionInstruction({
776
+ keys: [],
777
+ programId: new PublicKey(MEMO_PROGRAM_ID),
778
+ data: Buffer.from(transfer.customMemo, 'utf-8'),
779
+ })
780
+ )
781
+ }
782
+
783
+ transferResults.push({
784
+ stealthAddress: stealthAddressBase58,
785
+ ephemeralPublicKey: ephemeralPubkeyBase58,
786
+ viewTag: viewTagHex,
787
+ amount: transfer.amount,
788
+ uiAmount: Number(transfer.amount) / Math.pow(10, tokenMetadata.decimals),
789
+ })
790
+ }
791
+
792
+ // Get blockhash and sign
793
+ const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash()
794
+ transaction.recentBlockhash = blockhash
795
+ transaction.lastValidBlockHeight = lastValidBlockHeight
796
+ transaction.feePayer = sender
797
+
798
+ const signedTx = await signTransaction(transaction)
799
+
800
+ // Send and confirm
801
+ const txSignature = await connection.sendRawTransaction(signedTx.serialize(), {
802
+ skipPreflight: false,
803
+ preflightCommitment: 'confirmed',
804
+ })
805
+
806
+ await connection.confirmTransaction(
807
+ { signature: txSignature, blockhash, lastValidBlockHeight },
808
+ 'confirmed'
809
+ )
810
+
811
+ const cluster = detectCluster(connection.rpcEndpoint)
812
+
813
+ return {
814
+ txSignature,
815
+ transfers: transferResults,
816
+ explorerUrl: getExplorerUrl(txSignature, cluster),
817
+ cluster,
818
+ totalAmount,
819
+ tokenMetadata,
820
+ }
821
+ }
822
+
823
+ // ─── Utilities ────────────────────────────────────────────────────────────────
824
+
825
+ /**
826
+ * Format token amount for display
827
+ *
828
+ * @param amount - Raw token amount
829
+ * @param decimals - Token decimals
830
+ * @param maxDecimals - Maximum decimal places to show (default: 4)
831
+ * @returns Formatted string
832
+ */
833
+ export function formatTokenAmount(
834
+ amount: bigint,
835
+ decimals: number,
836
+ maxDecimals: number = 4
837
+ ): string {
838
+ const value = Number(amount) / Math.pow(10, decimals)
839
+
840
+ if (value === 0) return '0'
841
+
842
+ // For very small amounts, use scientific notation
843
+ if (value < Math.pow(10, -maxDecimals) && value > 0) {
844
+ return value.toExponential(2)
845
+ }
846
+
847
+ // Format with appropriate decimals
848
+ const formatted = value.toFixed(maxDecimals)
849
+
850
+ // Remove trailing zeros
851
+ return formatted.replace(/\.?0+$/, '')
852
+ }
853
+
854
+ /**
855
+ * Parse token amount from user input
856
+ *
857
+ * @param input - User input string (e.g., "1.5", "100")
858
+ * @param decimals - Token decimals
859
+ * @returns Raw token amount as bigint
860
+ */
861
+ export function parseTokenAmount(input: string, decimals: number): bigint {
862
+ // Remove commas and whitespace
863
+ const cleaned = input.replace(/[,\s]/g, '')
864
+
865
+ // Parse as float
866
+ const value = parseFloat(cleaned)
867
+ if (isNaN(value) || value < 0) {
868
+ throw new ValidationError(`Invalid amount: ${input}`, 'amount')
869
+ }
870
+
871
+ // Convert to smallest unit
872
+ const multiplier = Math.pow(10, decimals)
873
+ const raw = Math.round(value * multiplier)
874
+
875
+ return BigInt(raw)
876
+ }
877
+
878
+ /**
879
+ * Detect Solana cluster from RPC endpoint
880
+ */
881
+ function detectCluster(endpoint: string): SolanaCluster {
882
+ if (endpoint.includes('devnet')) return 'devnet'
883
+ if (endpoint.includes('testnet')) return 'testnet'
884
+ if (endpoint.includes('localhost') || endpoint.includes('127.0.0.1')) return 'localnet'
885
+ return 'mainnet-beta'
886
+ }