@getomnichain/omnichain 0.2.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 (296) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +66 -0
  3. package/dist/abstract_gas_pricing.d.ts +31 -0
  4. package/dist/abstract_gas_pricing.d.ts.map +1 -0
  5. package/dist/abstract_gas_pricing.js +22 -0
  6. package/dist/abstract_gas_pricing.js.map +1 -0
  7. package/dist/address.d.ts +10 -0
  8. package/dist/address.d.ts.map +1 -0
  9. package/dist/address.factory.d.ts +26 -0
  10. package/dist/address.factory.d.ts.map +1 -0
  11. package/dist/address.factory.js +75 -0
  12. package/dist/address.factory.js.map +1 -0
  13. package/dist/address.js +13 -0
  14. package/dist/address.js.map +1 -0
  15. package/dist/address_field.decorator.d.ts +17 -0
  16. package/dist/address_field.decorator.d.ts.map +1 -0
  17. package/dist/address_field.decorator.js +34 -0
  18. package/dist/address_field.decorator.js.map +1 -0
  19. package/dist/chain.base.d.ts +79 -0
  20. package/dist/chain.base.d.ts.map +1 -0
  21. package/dist/chain.base.js +113 -0
  22. package/dist/chain.base.js.map +1 -0
  23. package/dist/chain_ids.d.ts +122 -0
  24. package/dist/chain_ids.d.ts.map +1 -0
  25. package/dist/chain_ids.js +193 -0
  26. package/dist/chain_ids.js.map +1 -0
  27. package/dist/errors.d.ts +42 -0
  28. package/dist/errors.d.ts.map +1 -0
  29. package/dist/errors.js +62 -0
  30. package/dist/errors.js.map +1 -0
  31. package/dist/evm/evm_address.d.ts +11 -0
  32. package/dist/evm/evm_address.d.ts.map +1 -0
  33. package/dist/evm/evm_address.js +56 -0
  34. package/dist/evm/evm_address.js.map +1 -0
  35. package/dist/evm/evm_chain.d.ts +123 -0
  36. package/dist/evm/evm_chain.d.ts.map +1 -0
  37. package/dist/evm/evm_chain.js +685 -0
  38. package/dist/evm/evm_chain.js.map +1 -0
  39. package/dist/evm/evm_chains.d.ts +83 -0
  40. package/dist/evm/evm_chains.d.ts.map +1 -0
  41. package/dist/evm/evm_chains.js +493 -0
  42. package/dist/evm/evm_chains.js.map +1 -0
  43. package/dist/evm/evm_gas_estimate.d.ts +45 -0
  44. package/dist/evm/evm_gas_estimate.d.ts.map +1 -0
  45. package/dist/evm/evm_gas_estimate.js +70 -0
  46. package/dist/evm/evm_gas_estimate.js.map +1 -0
  47. package/dist/evm/evm_gas_pricing.d.ts +40 -0
  48. package/dist/evm/evm_gas_pricing.d.ts.map +1 -0
  49. package/dist/evm/evm_gas_pricing.js +56 -0
  50. package/dist/evm/evm_gas_pricing.js.map +1 -0
  51. package/dist/evm/evm_token.d.ts +7 -0
  52. package/dist/evm/evm_token.d.ts.map +1 -0
  53. package/dist/evm/evm_token.js +22 -0
  54. package/dist/evm/evm_token.js.map +1 -0
  55. package/dist/evm/evm_tokens.d.ts +81 -0
  56. package/dist/evm/evm_tokens.d.ts.map +1 -0
  57. package/dist/evm/evm_tokens.js +104 -0
  58. package/dist/evm/evm_tokens.js.map +1 -0
  59. package/dist/evm/evm_transaction_status.d.ts +108 -0
  60. package/dist/evm/evm_transaction_status.d.ts.map +1 -0
  61. package/dist/evm/evm_transaction_status.js +177 -0
  62. package/dist/evm/evm_transaction_status.js.map +1 -0
  63. package/dist/evm/index.d.ts +10 -0
  64. package/dist/evm/index.d.ts.map +1 -0
  65. package/dist/evm/index.js +10 -0
  66. package/dist/evm/index.js.map +1 -0
  67. package/dist/evm/unsigned_evm_transaction.d.ts +19 -0
  68. package/dist/evm/unsigned_evm_transaction.d.ts.map +1 -0
  69. package/dist/evm/unsigned_evm_transaction.js +18 -0
  70. package/dist/evm/unsigned_evm_transaction.js.map +1 -0
  71. package/dist/index.d.ts +18 -0
  72. package/dist/index.d.ts.map +1 -0
  73. package/dist/index.js +18 -0
  74. package/dist/index.js.map +1 -0
  75. package/dist/is_address.decorator.d.ts +3 -0
  76. package/dist/is_address.decorator.d.ts.map +1 -0
  77. package/dist/is_address.decorator.js +51 -0
  78. package/dist/is_address.decorator.js.map +1 -0
  79. package/dist/network_type.d.ts +73 -0
  80. package/dist/network_type.d.ts.map +1 -0
  81. package/dist/network_type.js +144 -0
  82. package/dist/network_type.js.map +1 -0
  83. package/dist/priority.d.ts +23 -0
  84. package/dist/priority.d.ts.map +1 -0
  85. package/dist/priority.js +23 -0
  86. package/dist/priority.js.map +1 -0
  87. package/dist/solana/index.d.ts +9 -0
  88. package/dist/solana/index.d.ts.map +1 -0
  89. package/dist/solana/index.js +9 -0
  90. package/dist/solana/index.js.map +1 -0
  91. package/dist/solana/solana_address.d.ts +20 -0
  92. package/dist/solana/solana_address.d.ts.map +1 -0
  93. package/dist/solana/solana_address.js +45 -0
  94. package/dist/solana/solana_address.js.map +1 -0
  95. package/dist/solana/solana_chain.d.ts +213 -0
  96. package/dist/solana/solana_chain.d.ts.map +1 -0
  97. package/dist/solana/solana_chain.js +704 -0
  98. package/dist/solana/solana_chain.js.map +1 -0
  99. package/dist/solana/solana_chains.d.ts +24 -0
  100. package/dist/solana/solana_chains.d.ts.map +1 -0
  101. package/dist/solana/solana_chains.js +84 -0
  102. package/dist/solana/solana_chains.js.map +1 -0
  103. package/dist/solana/solana_gas_pricing.d.ts +30 -0
  104. package/dist/solana/solana_gas_pricing.d.ts.map +1 -0
  105. package/dist/solana/solana_gas_pricing.js +26 -0
  106. package/dist/solana/solana_gas_pricing.js.map +1 -0
  107. package/dist/solana/solana_token.d.ts +19 -0
  108. package/dist/solana/solana_token.d.ts.map +1 -0
  109. package/dist/solana/solana_token.js +34 -0
  110. package/dist/solana/solana_token.js.map +1 -0
  111. package/dist/solana/solana_tokens.d.ts +22 -0
  112. package/dist/solana/solana_tokens.d.ts.map +1 -0
  113. package/dist/solana/solana_tokens.js +26 -0
  114. package/dist/solana/solana_tokens.js.map +1 -0
  115. package/dist/solana/solana_transaction_status.d.ts +59 -0
  116. package/dist/solana/solana_transaction_status.d.ts.map +1 -0
  117. package/dist/solana/solana_transaction_status.js +144 -0
  118. package/dist/solana/solana_transaction_status.js.map +1 -0
  119. package/dist/solana/unsigned_solana_transaction.d.ts +31 -0
  120. package/dist/solana/unsigned_solana_transaction.d.ts.map +1 -0
  121. package/dist/solana/unsigned_solana_transaction.js +26 -0
  122. package/dist/solana/unsigned_solana_transaction.js.map +1 -0
  123. package/dist/token.d.ts +35 -0
  124. package/dist/token.d.ts.map +1 -0
  125. package/dist/token.js +62 -0
  126. package/dist/token.js.map +1 -0
  127. package/dist/ton/index.d.ts +2 -0
  128. package/dist/ton/index.d.ts.map +1 -0
  129. package/dist/ton/index.js +2 -0
  130. package/dist/ton/index.js.map +1 -0
  131. package/dist/ton/ton_address.d.ts +10 -0
  132. package/dist/ton/ton_address.d.ts.map +1 -0
  133. package/dist/ton/ton_address.js +53 -0
  134. package/dist/ton/ton_address.js.map +1 -0
  135. package/dist/transaction_status.d.ts +140 -0
  136. package/dist/transaction_status.d.ts.map +1 -0
  137. package/dist/transaction_status.js +271 -0
  138. package/dist/transaction_status.js.map +1 -0
  139. package/dist/unsigned_transaction.d.ts +7 -0
  140. package/dist/unsigned_transaction.d.ts.map +1 -0
  141. package/dist/unsigned_transaction.js +9 -0
  142. package/dist/unsigned_transaction.js.map +1 -0
  143. package/dist/utxo/btc/btc_address.d.ts +15 -0
  144. package/dist/utxo/btc/btc_address.d.ts.map +1 -0
  145. package/dist/utxo/btc/btc_address.js +39 -0
  146. package/dist/utxo/btc/btc_address.js.map +1 -0
  147. package/dist/utxo/btc/btc_chain.d.ts +34 -0
  148. package/dist/utxo/btc/btc_chain.d.ts.map +1 -0
  149. package/dist/utxo/btc/btc_chain.js +62 -0
  150. package/dist/utxo/btc/btc_chain.js.map +1 -0
  151. package/dist/utxo/btc/btc_chains.d.ts +25 -0
  152. package/dist/utxo/btc/btc_chains.d.ts.map +1 -0
  153. package/dist/utxo/btc/btc_chains.js +43 -0
  154. package/dist/utxo/btc/btc_chains.js.map +1 -0
  155. package/dist/utxo/btc/btc_token.d.ts +6 -0
  156. package/dist/utxo/btc/btc_token.d.ts.map +1 -0
  157. package/dist/utxo/btc/btc_token.js +11 -0
  158. package/dist/utxo/btc/btc_token.js.map +1 -0
  159. package/dist/utxo/btc/index.d.ts +14 -0
  160. package/dist/utxo/btc/index.d.ts.map +1 -0
  161. package/dist/utxo/btc/index.js +14 -0
  162. package/dist/utxo/btc/index.js.map +1 -0
  163. package/dist/utxo/btc/network_params.d.ts +50 -0
  164. package/dist/utxo/btc/network_params.d.ts.map +1 -0
  165. package/dist/utxo/btc/network_params.js +188 -0
  166. package/dist/utxo/btc/network_params.js.map +1 -0
  167. package/dist/utxo/btc/tools/asset_outpoint.d.ts +7 -0
  168. package/dist/utxo/btc/tools/asset_outpoint.d.ts.map +1 -0
  169. package/dist/utxo/btc/tools/asset_outpoint.js +12 -0
  170. package/dist/utxo/btc/tools/asset_outpoint.js.map +1 -0
  171. package/dist/utxo/btc/tools/bitcoin_core.tool.d.ts +41 -0
  172. package/dist/utxo/btc/tools/bitcoin_core.tool.d.ts.map +1 -0
  173. package/dist/utxo/btc/tools/bitcoin_core.tool.js +172 -0
  174. package/dist/utxo/btc/tools/bitcoin_core.tool.js.map +1 -0
  175. package/dist/utxo/btc/tools/esplora.tool.d.ts +27 -0
  176. package/dist/utxo/btc/tools/esplora.tool.d.ts.map +1 -0
  177. package/dist/utxo/btc/tools/esplora.tool.js +113 -0
  178. package/dist/utxo/btc/tools/esplora.tool.js.map +1 -0
  179. package/dist/utxo/btc/tools/inscription_index.d.ts +6 -0
  180. package/dist/utxo/btc/tools/inscription_index.d.ts.map +1 -0
  181. package/dist/utxo/btc/tools/inscription_index.js +2 -0
  182. package/dist/utxo/btc/tools/inscription_index.js.map +1 -0
  183. package/dist/utxo/btc/tools/ordiscan.tool.d.ts +19 -0
  184. package/dist/utxo/btc/tools/ordiscan.tool.d.ts.map +1 -0
  185. package/dist/utxo/btc/tools/ordiscan.tool.js +53 -0
  186. package/dist/utxo/btc/tools/ordiscan.tool.js.map +1 -0
  187. package/dist/utxo/btc/tools/rare_sat_index.d.ts +6 -0
  188. package/dist/utxo/btc/tools/rare_sat_index.d.ts.map +1 -0
  189. package/dist/utxo/btc/tools/rare_sat_index.js +2 -0
  190. package/dist/utxo/btc/tools/rare_sat_index.js.map +1 -0
  191. package/dist/utxo/btc/tools/rune_index.d.ts +6 -0
  192. package/dist/utxo/btc/tools/rune_index.d.ts.map +1 -0
  193. package/dist/utxo/btc/tools/rune_index.js +2 -0
  194. package/dist/utxo/btc/tools/rune_index.js.map +1 -0
  195. package/dist/utxo/btc/tools/unisat.tool.d.ts +18 -0
  196. package/dist/utxo/btc/tools/unisat.tool.d.ts.map +1 -0
  197. package/dist/utxo/btc/tools/unisat.tool.js +49 -0
  198. package/dist/utxo/btc/tools/unisat.tool.js.map +1 -0
  199. package/dist/utxo/coin_selection.d.ts +29 -0
  200. package/dist/utxo/coin_selection.d.ts.map +1 -0
  201. package/dist/utxo/coin_selection.js +156 -0
  202. package/dist/utxo/coin_selection.js.map +1 -0
  203. package/dist/utxo/doge/doge_chains.d.ts +17 -0
  204. package/dist/utxo/doge/doge_chains.d.ts.map +1 -0
  205. package/dist/utxo/doge/doge_chains.js +28 -0
  206. package/dist/utxo/doge/doge_chains.js.map +1 -0
  207. package/dist/utxo/doge/index.d.ts +3 -0
  208. package/dist/utxo/doge/index.d.ts.map +1 -0
  209. package/dist/utxo/doge/index.js +3 -0
  210. package/dist/utxo/doge/index.js.map +1 -0
  211. package/dist/utxo/doge/network_params.d.ts +4 -0
  212. package/dist/utxo/doge/network_params.d.ts.map +1 -0
  213. package/dist/utxo/doge/network_params.js +24 -0
  214. package/dist/utxo/doge/network_params.js.map +1 -0
  215. package/dist/utxo/ecc.d.ts +2 -0
  216. package/dist/utxo/ecc.d.ts.map +1 -0
  217. package/dist/utxo/ecc.js +11 -0
  218. package/dist/utxo/ecc.js.map +1 -0
  219. package/dist/utxo/fee.d.ts +12 -0
  220. package/dist/utxo/fee.d.ts.map +1 -0
  221. package/dist/utxo/fee.js +51 -0
  222. package/dist/utxo/fee.js.map +1 -0
  223. package/dist/utxo/index.d.ts +20 -0
  224. package/dist/utxo/index.d.ts.map +1 -0
  225. package/dist/utxo/index.js +20 -0
  226. package/dist/utxo/index.js.map +1 -0
  227. package/dist/utxo/ltc/index.d.ts +3 -0
  228. package/dist/utxo/ltc/index.d.ts.map +1 -0
  229. package/dist/utxo/ltc/index.js +3 -0
  230. package/dist/utxo/ltc/index.js.map +1 -0
  231. package/dist/utxo/ltc/ltc_chains.d.ts +17 -0
  232. package/dist/utxo/ltc/ltc_chains.d.ts.map +1 -0
  233. package/dist/utxo/ltc/ltc_chains.js +28 -0
  234. package/dist/utxo/ltc/ltc_chains.js.map +1 -0
  235. package/dist/utxo/ltc/network_params.d.ts +4 -0
  236. package/dist/utxo/ltc/network_params.d.ts.map +1 -0
  237. package/dist/utxo/ltc/network_params.js +27 -0
  238. package/dist/utxo/ltc/network_params.js.map +1 -0
  239. package/dist/utxo/script.d.ts +16 -0
  240. package/dist/utxo/script.d.ts.map +1 -0
  241. package/dist/utxo/script.js +52 -0
  242. package/dist/utxo/script.js.map +1 -0
  243. package/dist/utxo/tools/broadcaster.d.ts +6 -0
  244. package/dist/utxo/tools/broadcaster.d.ts.map +1 -0
  245. package/dist/utxo/tools/broadcaster.js +2 -0
  246. package/dist/utxo/tools/broadcaster.js.map +1 -0
  247. package/dist/utxo/tools/chain_tip_provider.d.ts +5 -0
  248. package/dist/utxo/tools/chain_tip_provider.d.ts.map +1 -0
  249. package/dist/utxo/tools/chain_tip_provider.js +2 -0
  250. package/dist/utxo/tools/chain_tip_provider.js.map +1 -0
  251. package/dist/utxo/tools/fee_estimator.d.ts +6 -0
  252. package/dist/utxo/tools/fee_estimator.d.ts.map +1 -0
  253. package/dist/utxo/tools/fee_estimator.js +2 -0
  254. package/dist/utxo/tools/fee_estimator.js.map +1 -0
  255. package/dist/utxo/tools/raw_transaction_provider.d.ts +8 -0
  256. package/dist/utxo/tools/raw_transaction_provider.d.ts.map +1 -0
  257. package/dist/utxo/tools/raw_transaction_provider.js +2 -0
  258. package/dist/utxo/tools/raw_transaction_provider.js.map +1 -0
  259. package/dist/utxo/tools/utxo_provider.d.ts +7 -0
  260. package/dist/utxo/tools/utxo_provider.d.ts.map +1 -0
  261. package/dist/utxo/tools/utxo_provider.js +2 -0
  262. package/dist/utxo/tools/utxo_provider.js.map +1 -0
  263. package/dist/utxo/unsigned_utxo_transaction.d.ts +32 -0
  264. package/dist/utxo/unsigned_utxo_transaction.d.ts.map +1 -0
  265. package/dist/utxo/unsigned_utxo_transaction.js +32 -0
  266. package/dist/utxo/unsigned_utxo_transaction.js.map +1 -0
  267. package/dist/utxo/utxo.d.ts +43 -0
  268. package/dist/utxo/utxo.d.ts.map +1 -0
  269. package/dist/utxo/utxo.js +2 -0
  270. package/dist/utxo/utxo.js.map +1 -0
  271. package/dist/utxo/utxo_chain.d.ts +102 -0
  272. package/dist/utxo/utxo_chain.d.ts.map +1 -0
  273. package/dist/utxo/utxo_chain.js +524 -0
  274. package/dist/utxo/utxo_chain.js.map +1 -0
  275. package/dist/utxo/utxo_chains.d.ts +29 -0
  276. package/dist/utxo/utxo_chains.d.ts.map +1 -0
  277. package/dist/utxo/utxo_chains.js +63 -0
  278. package/dist/utxo/utxo_chains.js.map +1 -0
  279. package/dist/utxo/utxo_gas_pricing.d.ts +22 -0
  280. package/dist/utxo/utxo_gas_pricing.d.ts.map +1 -0
  281. package/dist/utxo/utxo_gas_pricing.js +20 -0
  282. package/dist/utxo/utxo_gas_pricing.js.map +1 -0
  283. package/dist/utxo/utxo_network_params.d.ts +25 -0
  284. package/dist/utxo/utxo_network_params.d.ts.map +1 -0
  285. package/dist/utxo/utxo_network_params.js +15 -0
  286. package/dist/utxo/utxo_network_params.js.map +1 -0
  287. package/dist/utxo/utxo_transaction_status.d.ts +73 -0
  288. package/dist/utxo/utxo_transaction_status.d.ts.map +1 -0
  289. package/dist/utxo/utxo_transaction_status.js +89 -0
  290. package/dist/utxo/utxo_transaction_status.js.map +1 -0
  291. package/docs/PUBLISHING.md +119 -0
  292. package/docs/README.md +273 -0
  293. package/docs/evm.md +205 -0
  294. package/docs/solana.md +220 -0
  295. package/docs/utxo.md +364 -0
  296. package/package.json +94 -0
@@ -0,0 +1,704 @@
1
+ import { ComputeBudgetProgram, Connection, MessageV0, PublicKey, SystemProgram, VersionedTransaction, } from '@solana/web3.js';
2
+ import { TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID, createAssociatedTokenAccountIdempotentInstruction, createAssociatedTokenAccountInstruction, createTransferCheckedInstruction, getAccount, getAssociatedTokenAddressSync, getMint, } from '@solana/spl-token';
3
+ import { Chain, resolveTransferAmount } from "../chain.base.js";
4
+ import { ChainError, ChainErrorKinds, sanitizeCause, sanitizeMessage } from "../errors.js";
5
+ import { NetworkType, registerNonEvmChain } from "../network_type.js";
6
+ import { Priority } from "../priority.js";
7
+ import { AssetBalanceChange, } from "../transaction_status.js";
8
+ import { SolanaTransactionFees, SolanaTransactionStatus, } from "./solana_transaction_status.js";
9
+ import { SolanaAddress } from "./solana_address.js";
10
+ import { SolanaToken } from "./solana_token.js";
11
+ import { UnsignedSolanaTransaction } from "./unsigned_solana_transaction.js";
12
+ const DEFAULT_COMPUTE_UNIT_LIMIT = 200_000;
13
+ /** Percentile (0..1) of recent prioritization fees picked per Priority. */
14
+ const SOL_PRIORITY_PERCENTILE = {
15
+ [Priority.SLOW]: 0.25,
16
+ [Priority.NORMAL]: 0.5,
17
+ [Priority.FAST]: 0.9,
18
+ };
19
+ /**
20
+ * Solana is account-based but very different from EVM (see docs/solana.md). Key choices made
21
+ * for depositron:
22
+ * - Transfers always build a `VersionedTransaction` (MessageV0) — legacy transactions are
23
+ * deprecated and can't address account-lookup tables for fee reduction.
24
+ * - The fee payer is always the sender (no relayer pattern here).
25
+ * - SPL transfers use `transferChecked` (which asserts the recipient mint matches the
26
+ * declared mint, defeating the entire class of "wrong-token-decimals" attacks).
27
+ * - When the recipient doesn't have an ATA for the mint, we *prepend* a create-ATA
28
+ * instruction paid by the sender. This costs ~2039 lamports of rent-exempt deposit.
29
+ * - We do NOT consult a curated registry to determine SPL vs Token-2022 —
30
+ * instead we read the mint account owner at build time. That's one RPC per transfer but
31
+ * it's authoritative.
32
+ */
33
+ export class SolanaChain extends Chain {
34
+ defaultRpcUrl;
35
+ rpcUrl;
36
+ legacyRpcEnvNames;
37
+ explorerClusterSuffix;
38
+ chainAgnosticGenesisHash;
39
+ _nativeToken;
40
+ _connection = null;
41
+ constructor(init) {
42
+ super(init.chainId, init.name, NetworkType.SOLANA, init.blockTimeSeconds, init.nativeSymbol, init.explorerBaseUrl);
43
+ this.defaultRpcUrl = init.defaultRpcUrl;
44
+ this.rpcUrl = init.rpcUrl;
45
+ this.legacyRpcEnvNames = init.legacyRpcEnvNames ?? [];
46
+ this.explorerClusterSuffix = init.explorerClusterSuffix ?? '';
47
+ this.chainAgnosticGenesisHash = init.chainAgnosticGenesisHash;
48
+ this._nativeToken = SolanaToken.native(init.chainId, init.nativeSymbol, init.nativeDecimals ?? 9);
49
+ registerNonEvmChain(init.chainId, NetworkType.SOLANA);
50
+ }
51
+ get nativeToken() {
52
+ return this._nativeToken;
53
+ }
54
+ getConnection() {
55
+ if (this._connection)
56
+ return this._connection;
57
+ const rpcUrl = this.readRpcUrl();
58
+ this._connection = new Connection(rpcUrl, { commitment: 'confirmed' });
59
+ return this._connection;
60
+ }
61
+ /**
62
+ * Network-aware priority fee suggestion in microlamports per compute unit.
63
+ * Samples `getRecentPrioritizationFees({ lockedWritableAccounts: [] })` (cluster-wide, ~150 most recent slots) and
64
+ * picks a percentile per priority:
65
+ * - SLOW = p25
66
+ * - NORMAL = p50
67
+ * - FAST = p90
68
+ * Returns 0 if the cluster has been so quiet that the chosen percentile is 0 (legitimate
69
+ * outcome on devnet / off-peak mainnet). Caller plugs this into
70
+ * `ComputeBudgetProgram.setComputeUnitPrice({ microLamports })`.
71
+ */
72
+ async suggestPriorityFeeMicroLamports(priority) {
73
+ const samples = await this.getConnection().getRecentPrioritizationFees({
74
+ lockedWritableAccounts: [],
75
+ });
76
+ const fees = samples
77
+ .map((s) => s.prioritizationFee)
78
+ .filter((f) => Number.isFinite(f) && f >= 0)
79
+ .sort((a, b) => a - b);
80
+ if (fees.length === 0)
81
+ return 0;
82
+ const percentile = SOL_PRIORITY_PERCENTILE[priority];
83
+ const idx = Math.min(fees.length - 1, Math.floor((fees.length - 1) * percentile));
84
+ return fees[idx];
85
+ }
86
+ /**
87
+ * Env var name for a Solana chain by chain id — mirrors Python's
88
+ * `SolanaChain.rpc_url_env_for_chain_id` (impl/solana/base.py:421-423)
89
+ * exactly, including the signed chainId. Shell operators must set via
90
+ * dotenv / docker / k8s since `SOLANA_-2000_RPC_URL` is not a settable
91
+ * name in sh/bash/zsh.
92
+ */
93
+ static rpcUrlEnvForChainId(chainId) {
94
+ return `SOLANA_${chainId}_RPC_URL`;
95
+ }
96
+ /**
97
+ * Resolution precedence, mirrors omnichain-py/impl/solana/base.py:424-434
98
+ * plus a TS-side legacy fallback for consumers with pre-v0 env-var names:
99
+ * 1. constructor `rpcUrl`
100
+ * 2. env `<NAME_UPPERCASE_UNDERSCORED>_RPC_URL`
101
+ * 3. env `SOLANA_<chainId>_RPC_URL` (Python parity — signed)
102
+ * 4. env from `legacyRpcEnvNames` (if configured — e.g. mainnet checks
103
+ * the pre-rename `SOLANA_RPC_URL` here)
104
+ * 5. `defaultRpcUrl` (never throws — public cluster)
105
+ */
106
+ readRpcUrl() {
107
+ if (this.rpcUrl && this.rpcUrl.trim().length > 0)
108
+ return this.rpcUrl.trim();
109
+ const env = globalThis.process?.env;
110
+ const candidates = [
111
+ this.name.replace(/ /g, '_').toUpperCase() + '_RPC_URL',
112
+ SolanaChain.rpcUrlEnvForChainId(this.chainId),
113
+ ...this.legacyRpcEnvNames,
114
+ ];
115
+ for (const key of candidates) {
116
+ const v = env?.[key];
117
+ if (v && v.trim().length > 0)
118
+ return v.trim();
119
+ }
120
+ return this.defaultRpcUrl;
121
+ }
122
+ getWalletExplorerUrl(address) {
123
+ return `${this.explorerBaseUrl}/account/${address}${this.explorerClusterSuffix}`;
124
+ }
125
+ getTokenExplorerUrl(tokenIdentifier) {
126
+ if (!tokenIdentifier) {
127
+ return this.explorerBaseUrl + this.explorerClusterSuffix;
128
+ }
129
+ return `${this.explorerBaseUrl}/token/${tokenIdentifier}${this.explorerClusterSuffix}`;
130
+ }
131
+ getTransactionExplorerUrl(txHash) {
132
+ return `${this.explorerBaseUrl}/tx/${txHash}${this.explorerClusterSuffix}`;
133
+ }
134
+ validateAddress(raw) {
135
+ try {
136
+ new SolanaAddress(raw);
137
+ return true;
138
+ }
139
+ catch {
140
+ return false;
141
+ }
142
+ }
143
+ validateTokenIdentifier(raw) {
144
+ if (raw === undefined)
145
+ return true; // native SOL
146
+ return this.validateAddress(raw);
147
+ }
148
+ async getBalance(owner, tokenIdentifier) {
149
+ if (!this.validateAddress(owner)) {
150
+ throw new ChainError(ChainErrorKinds.InvalidAddress, `Invalid Solana address: ${owner}`, {
151
+ chainId: this.chainId,
152
+ address: owner,
153
+ });
154
+ }
155
+ const connection = this.getConnection();
156
+ const ownerPk = new PublicKey(owner);
157
+ try {
158
+ if (tokenIdentifier === undefined) {
159
+ return BigInt(await connection.getBalance(ownerPk));
160
+ }
161
+ if (!this.validateAddress(tokenIdentifier)) {
162
+ throw new ChainError(ChainErrorKinds.InvalidTokenIdentifier, `Invalid Solana mint: ${tokenIdentifier}`, { chainId: this.chainId, identifier: tokenIdentifier });
163
+ }
164
+ const mintPk = new PublicKey(tokenIdentifier);
165
+ const programId = await this.resolveTokenProgramId(mintPk);
166
+ const ata = getAssociatedTokenAddressSync(mintPk, ownerPk, false, programId);
167
+ try {
168
+ const acct = await getAccount(connection, ata, undefined, programId);
169
+ return acct.amount;
170
+ }
171
+ catch {
172
+ // No ATA → balance 0.
173
+ return 0n;
174
+ }
175
+ }
176
+ catch (err) {
177
+ if (err instanceof ChainError)
178
+ throw err;
179
+ throw new ChainError(ChainErrorKinds.RpcError, `Failed to read Solana balance for ${owner}/${tokenIdentifier ?? 'NATIVE'}: ${err.message}`, { chainId: this.chainId, address: owner, identifier: tokenIdentifier }, err);
180
+ }
181
+ }
182
+ async createTransferUnsignedTransaction(req) {
183
+ if (!req.from) {
184
+ throw new ChainError(ChainErrorKinds.InvalidArgument, 'Solana transfers require an explicit `from` (no implicit signer)', { chainId: this.chainId });
185
+ }
186
+ if (!this.validateAddress(req.from)) {
187
+ throw new ChainError(ChainErrorKinds.InvalidAddress, `Invalid sender: ${req.from}`, {
188
+ chainId: this.chainId,
189
+ address: req.from,
190
+ });
191
+ }
192
+ if (!this.validateAddress(req.to)) {
193
+ throw new ChainError(ChainErrorKinds.InvalidAddress, `Invalid recipient: ${req.to}`, {
194
+ chainId: this.chainId,
195
+ address: req.to,
196
+ });
197
+ }
198
+ if (!this.validateTokenIdentifier(req.tokenIdentifier)) {
199
+ throw new ChainError(ChainErrorKinds.InvalidTokenIdentifier, `Invalid token identifier: ${req.tokenIdentifier}`, { chainId: this.chainId, identifier: req.tokenIdentifier });
200
+ }
201
+ // gasPricing not yet consumed by Solana builder (Phase 2 follow-up);
202
+ // reject rather than silently ignore.
203
+ if (req.gasPricing !== undefined) {
204
+ throw new ChainError(ChainErrorKinds.InvalidArgument, 'CreateTransferRequest.gasPricing is not yet consumed by SolanaChain (Phase 2 follow-up). Use `priorityFeeMicroLamportsPerCu` / `computeUnitLimit` on SolanaTransferOptions for now.', { chainId: this.chainId });
205
+ }
206
+ // isFullBalance requires a base-fee + rent-exempt reserve before
207
+ // it can be safely wired — sending all lamports leaves 0 for the
208
+ // 5000-lamport base fee (and ATA rent when feePayer === from on
209
+ // the SPL path). Deferred to a follow-up card.
210
+ if (req.isFullBalance === true) {
211
+ throw new ChainError(ChainErrorKinds.InvalidArgument, 'CreateTransferRequest.isFullBalance is not yet supported on SolanaChain — pass `amount` or `amountHr` explicitly.', { chainId: this.chainId });
212
+ }
213
+ // Only fetch decimals when amountHr is the input form — avoids the
214
+ // extra getMint RPC on every bigint-amount transfer. Native path
215
+ // uses SDK-declared decimals; SPL path uses resolveMintDecimals
216
+ // (which already throws on RPC failure — no defensive fallback).
217
+ const tokenDecimals = req.amountHr !== undefined
218
+ ? req.tokenIdentifier === undefined
219
+ ? this._nativeToken.decimals
220
+ : await this.resolveMintDecimals(new PublicKey(req.tokenIdentifier))
221
+ : 0;
222
+ const resolvedAmount = resolveTransferAmount(req, tokenDecimals);
223
+ const amountMr = resolvedAmount.kind === 'exact' ? resolvedAmount.amountMr : 0n;
224
+ if (amountMr <= 0n) {
225
+ throw new ChainError(ChainErrorKinds.InvalidArgument, `Transfer amount must be positive (got ${amountMr})`, { chainId: this.chainId });
226
+ }
227
+ if (req.feePayer !== undefined && !this.validateAddress(req.feePayer)) {
228
+ throw new ChainError(ChainErrorKinds.InvalidAddress, `Invalid feePayer: ${req.feePayer}`, {
229
+ chainId: this.chainId,
230
+ address: req.feePayer,
231
+ });
232
+ }
233
+ const connection = this.getConnection();
234
+ const fromPk = new PublicKey(req.from);
235
+ const toPk = new PublicKey(req.to);
236
+ const feePayerAddress = req.feePayer ?? req.from;
237
+ const feePayerPk = new PublicKey(feePayerAddress);
238
+ const instructions = [];
239
+ if (req.computeUnitLimit !== undefined || req.priorityFeeMicroLamportsPerCu !== undefined) {
240
+ instructions.push(ComputeBudgetProgram.setComputeUnitLimit({
241
+ units: req.computeUnitLimit ?? DEFAULT_COMPUTE_UNIT_LIMIT,
242
+ }));
243
+ if (req.priorityFeeMicroLamportsPerCu !== undefined) {
244
+ instructions.push(ComputeBudgetProgram.setComputeUnitPrice({
245
+ microLamports: req.priorityFeeMicroLamportsPerCu,
246
+ }));
247
+ }
248
+ }
249
+ if (req.tokenIdentifier === undefined) {
250
+ instructions.push(SystemProgram.transfer({
251
+ fromPubkey: fromPk,
252
+ toPubkey: toPk,
253
+ lamports: amountMr,
254
+ }));
255
+ }
256
+ else {
257
+ const mintPk = new PublicKey(req.tokenIdentifier);
258
+ const programId = await this.resolveTokenProgramId(mintPk);
259
+ // If amountHr wasn't the input form, tokenDecimals is still 0 and
260
+ // we haven't fetched the mint yet — do it now for the transfer-
261
+ // checked instruction (which requires the real decimals).
262
+ const decimals = tokenDecimals > 0 ? tokenDecimals : await this.resolveMintDecimals(mintPk);
263
+ const sourceAta = getAssociatedTokenAddressSync(mintPk, fromPk, false, programId);
264
+ const destAta = getAssociatedTokenAddressSync(mintPk, toPk, false, programId);
265
+ // Create the destination ATA when missing — the fee payer pays rent.
266
+ const destInfo = await connection.getAccountInfo(destAta);
267
+ if (!destInfo) {
268
+ instructions.push(createAssociatedTokenAccountInstruction(feePayerPk, destAta, toPk, mintPk, programId));
269
+ }
270
+ instructions.push(createTransferCheckedInstruction(sourceAta, mintPk, destAta, fromPk, amountMr, decimals, [], programId));
271
+ }
272
+ const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash('finalized');
273
+ const message = MessageV0.compile({
274
+ payerKey: feePayerPk,
275
+ instructions,
276
+ recentBlockhash: blockhash,
277
+ });
278
+ return new UnsignedSolanaTransaction({
279
+ chainId: this.chainId,
280
+ transaction: new VersionedTransaction(message),
281
+ feePayer: feePayerAddress,
282
+ recentBlockhash: blockhash,
283
+ lastValidBlockHeight,
284
+ instructions,
285
+ });
286
+ }
287
+ /**
288
+ * Wraps a caller-supplied list of program instructions into a fully-formed
289
+ * UnsignedSolanaTransaction. Used by depositron's SOL_INSTRUCTIONS action type to support
290
+ * arbitrary program calls (staking, swaps, custom programs) without the SDK having to
291
+ * understand the instruction semantics. The SDK still owns the envelope: CU limit + price,
292
+ * MessageV0 compilation, fresh blockhash, VersionedTransaction wrapping.
293
+ *
294
+ * Caller is responsible for: instruction correctness, signer-account constraints (only the
295
+ * single key the caller will sign with — typically the vault key — can be flagged as signer),
296
+ * ATA creation (if any instruction touches an ATA that doesn't exist yet), and decimals
297
+ * validation (no automatic `transferChecked` fallback here).
298
+ */
299
+ async createInstructionsUnsignedTransaction(req) {
300
+ if (!req.from || !this.validateAddress(req.from)) {
301
+ throw new ChainError(ChainErrorKinds.InvalidAddress, `Invalid sender: ${req.from}`, {
302
+ chainId: this.chainId,
303
+ address: req.from,
304
+ });
305
+ }
306
+ if (req.feePayer !== undefined && !this.validateAddress(req.feePayer)) {
307
+ throw new ChainError(ChainErrorKinds.InvalidAddress, `Invalid feePayer: ${req.feePayer}`, {
308
+ chainId: this.chainId,
309
+ address: req.feePayer,
310
+ });
311
+ }
312
+ if (!Array.isArray(req.instructions) || req.instructions.length === 0) {
313
+ throw new ChainError(ChainErrorKinds.InvalidArgument, 'SolanaChain.createInstructionsUnsignedTransaction requires at least one instruction', { chainId: this.chainId });
314
+ }
315
+ const feePayerAddress = req.feePayer ?? req.from;
316
+ const feePayerPk = new PublicKey(feePayerAddress);
317
+ const allIxs = [];
318
+ if (req.computeUnitLimit !== undefined || req.priorityFeeMicroLamportsPerCu !== undefined) {
319
+ allIxs.push(ComputeBudgetProgram.setComputeUnitLimit({
320
+ units: req.computeUnitLimit ?? DEFAULT_COMPUTE_UNIT_LIMIT,
321
+ }));
322
+ if (req.priorityFeeMicroLamportsPerCu !== undefined) {
323
+ allIxs.push(ComputeBudgetProgram.setComputeUnitPrice({
324
+ microLamports: req.priorityFeeMicroLamportsPerCu,
325
+ }));
326
+ }
327
+ }
328
+ for (const ix of req.instructions)
329
+ allIxs.push(ix);
330
+ const connection = this.getConnection();
331
+ const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash('finalized');
332
+ const message = MessageV0.compile({
333
+ payerKey: feePayerPk,
334
+ instructions: allIxs,
335
+ recentBlockhash: blockhash,
336
+ });
337
+ return new UnsignedSolanaTransaction({
338
+ chainId: this.chainId,
339
+ transaction: new VersionedTransaction(message),
340
+ feePayer: feePayerAddress,
341
+ recentBlockhash: blockhash,
342
+ lastValidBlockHeight,
343
+ instructions: allIxs,
344
+ });
345
+ }
346
+ /**
347
+ * Returns a fresh UnsignedSolanaTransaction with the same instructions but a newly-fetched
348
+ * blockhash. Use when a previously-built tx couldn't broadcast inside its blockhash window
349
+ * (~60s on mainnet) — Solana drops txs whose blockhash has expired. Caller must re-sign:
350
+ * the new signed message has a different recent_blockhash byte sequence, so any existing
351
+ * signature is invalid.
352
+ */
353
+ async refreshBlockhash(unsigned) {
354
+ const connection = this.getConnection();
355
+ const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash('finalized');
356
+ if (blockhash === unsigned.recentBlockhash)
357
+ return unsigned;
358
+ const message = MessageV0.compile({
359
+ payerKey: unsigned.feePayerPubkey,
360
+ instructions: unsigned.instructions,
361
+ recentBlockhash: blockhash,
362
+ });
363
+ return new UnsignedSolanaTransaction({
364
+ chainId: unsigned.chainId,
365
+ transaction: new VersionedTransaction(message),
366
+ feePayer: unsigned.feePayer,
367
+ recentBlockhash: blockhash,
368
+ lastValidBlockHeight,
369
+ instructions: unsigned.instructions,
370
+ });
371
+ }
372
+ /**
373
+ * Simulates the tx and right-sizes its setComputeUnitLimit instruction (10% headroom).
374
+ * Lowers priority-fee cost because the fee = CU price × CU limit; oversized limits pay for
375
+ * unused compute. Returns the unsigned untouched if it has no CU-limit ix or simulation didn't
376
+ * report unitsConsumed. Throws ChainError(SimulationFailed) when the sim returns `err`.
377
+ */
378
+ async estimateAndApplyCu(unsigned) {
379
+ const connection = this.getConnection();
380
+ const sim = await connection.simulateTransaction(unsigned.transaction, {
381
+ sigVerify: false,
382
+ replaceRecentBlockhash: true,
383
+ commitment: 'confirmed',
384
+ });
385
+ if (sim.value.err) {
386
+ throw new ChainError(ChainErrorKinds.InvalidArgument, `Solana simulation rejected the tx: ${JSON.stringify(sim.value.err)}`, { chainId: this.chainId });
387
+ }
388
+ const consumed = sim.value.unitsConsumed;
389
+ if (!consumed)
390
+ return unsigned;
391
+ const newLimit = Math.ceil(consumed * 1.1);
392
+ const setLimitIxIndex = unsigned.instructions.findIndex((ix) => ix.programId.equals(ComputeBudgetProgram.programId) &&
393
+ // setComputeUnitLimit ix discriminator is 0x02 in the first data byte.
394
+ ix.data.length >= 1 &&
395
+ ix.data[0] === 0x02);
396
+ if (setLimitIxIndex === -1)
397
+ return unsigned;
398
+ const newInstructions = [...unsigned.instructions];
399
+ newInstructions[setLimitIxIndex] = ComputeBudgetProgram.setComputeUnitLimit({
400
+ units: newLimit,
401
+ });
402
+ const message = MessageV0.compile({
403
+ payerKey: unsigned.feePayerPubkey,
404
+ instructions: newInstructions,
405
+ recentBlockhash: unsigned.recentBlockhash,
406
+ });
407
+ return new UnsignedSolanaTransaction({
408
+ chainId: unsigned.chainId,
409
+ transaction: new VersionedTransaction(message),
410
+ feePayer: unsigned.feePayer,
411
+ recentBlockhash: unsigned.recentBlockhash,
412
+ lastValidBlockHeight: unsigned.lastValidBlockHeight,
413
+ instructions: newInstructions,
414
+ });
415
+ }
416
+ /** Whether the given error came from a preflight simulation rejection (vs network/transport). */
417
+ static isSimulationError(err) {
418
+ if (err instanceof ChainError && err.kind === ChainErrorKinds.InvalidArgument)
419
+ return true;
420
+ const msg = err?.message ?? '';
421
+ return /simulation failed|transaction simulation failed/i.test(msg);
422
+ }
423
+ /** Whether the given error indicates the tx's blockhash has expired and a refresh is needed. */
424
+ static isBlockhashExpiredError(err) {
425
+ const msg = err?.message ?? '';
426
+ return /blockhash not found|expired blockhash|blockhash.*expired/i.test(msg);
427
+ }
428
+ async getTransactionStatus(txHash) {
429
+ const connection = this.getConnection();
430
+ // Best-effort read of the current rpc URL for message/cause scrubbing.
431
+ // readRpcUrl throws when nothing is configured; there'd be no
432
+ // network call in that case, but defensive null keeps the sanitizer
433
+ // total.
434
+ let scrubUrl = null;
435
+ try {
436
+ scrubUrl = this.readRpcUrl();
437
+ }
438
+ catch {
439
+ scrubUrl = null;
440
+ }
441
+ let tx;
442
+ try {
443
+ tx = await connection.getTransaction(txHash, {
444
+ commitment: 'confirmed',
445
+ maxSupportedTransactionVersion: 0,
446
+ });
447
+ }
448
+ catch (err) {
449
+ const rawMsg = err instanceof Error ? err.message : String(err);
450
+ throw new ChainError(ChainErrorKinds.RpcError, sanitizeMessage(`Failed to read Solana tx ${txHash}: ${rawMsg}`, scrubUrl), { chainId: this.chainId, txHash }, sanitizeCause(err, scrubUrl));
451
+ }
452
+ if (!tx) {
453
+ // Full tx not fetchable; fall back to signature status. Consume BOTH
454
+ // fields (`confirmationStatus` + `err`) — the previous fix that
455
+ // always returned Pending on settled-but-unfetchable let a settled-
456
+ // FAILED tx poll indefinitely.
457
+ let sig;
458
+ try {
459
+ sig = await connection.getSignatureStatus(txHash, { searchTransactionHistory: true });
460
+ }
461
+ catch (err) {
462
+ const rawMsg = err instanceof Error ? err.message : String(err);
463
+ throw new ChainError(ChainErrorKinds.RpcError, sanitizeMessage(`Failed to read Solana signature status ${txHash}: ${rawMsg}`, scrubUrl), { chainId: this.chainId, txHash }, sanitizeCause(err, scrubUrl));
464
+ }
465
+ if (!sig || !sig.value)
466
+ return SolanaTransactionStatus.notFound(this.chainId);
467
+ const settled = sig.value.confirmationStatus === 'finalized' ||
468
+ sig.value.confirmationStatus === 'confirmed';
469
+ if (settled && sig.value.err) {
470
+ // Fees are not reconstructable from a sig-status only — factory
471
+ // now accepts fees: null for exactly this case.
472
+ return SolanaTransactionStatus.failed({
473
+ chainId: this.chainId,
474
+ inclusionAt: null,
475
+ error: { code: 'REVERTED', reason: JSON.stringify(sig.value.err) },
476
+ fees: null,
477
+ });
478
+ }
479
+ // settled-Success without a fetchable body OR still propagating —
480
+ // keep polling. NotFound here would misreport a settled deposit.
481
+ return SolanaTransactionStatus.pending(this.chainId);
482
+ }
483
+ if (!tx.meta) {
484
+ // A returned tx without meta is a valid RPC response shape (some
485
+ // very old txs or unusual nodes). Report Pending so consumers keep
486
+ // polling rather than throwing from a status read; matches the
487
+ // _decodeBalanceChanges guard at the same file.
488
+ return SolanaTransactionStatus.pending(this.chainId);
489
+ }
490
+ const accounts = tx.transaction.message.staticAccountKeys.map((k) => k.toBase58());
491
+ const feePayer = accounts[0] ?? '';
492
+ if (feePayer.length === 0) {
493
+ throw new ChainError(ChainErrorKinds.TransactionDecodeFailed, `Solana tx ${txHash} has no fee_payer account`, { chainId: this.chainId, txHash });
494
+ }
495
+ const feeLamports = BigInt(tx.meta.fee);
496
+ const rawCu = tx.meta.computeUnitsConsumed;
497
+ const computeUnitsConsumed = rawCu === undefined || rawCu === null ? null : BigInt(rawCu);
498
+ const preLamports = BigInt(tx.meta.preBalances?.[0] ?? 0);
499
+ const postLamports = BigInt(tx.meta.postBalances?.[0] ?? 0);
500
+ const fees = new SolanaTransactionFees({
501
+ feePayer,
502
+ feeLamports,
503
+ computeUnitsConsumed,
504
+ netLamportsChangeByFeePayer: postLamports - preLamports,
505
+ });
506
+ const inclusionAt = tx.blockTime ? new Date(tx.blockTime * 1000) : null;
507
+ if (tx.meta.err) {
508
+ return SolanaTransactionStatus.failed({
509
+ chainId: this.chainId,
510
+ inclusionAt,
511
+ error: { code: 'REVERTED', reason: JSON.stringify(tx.meta.err) },
512
+ fees,
513
+ });
514
+ }
515
+ // Wrap the decoder so a malformed uiTokenAmount.amount / pre/postBalance
516
+ // (BigInt(...) throws SyntaxError on non-numeric strings) surfaces as
517
+ // ChainError(TransactionDecodeFailed) rather than a raw SyntaxError.
518
+ // Mirrors evm_chain.ts:decodeBalanceChanges catch.
519
+ let balanceChanges;
520
+ try {
521
+ balanceChanges = this._decodeBalanceChanges(tx);
522
+ }
523
+ catch (err) {
524
+ if (err instanceof ChainError)
525
+ throw err;
526
+ throw new ChainError(ChainErrorKinds.TransactionDecodeFailed, `Failed to decode Solana tx ${txHash}: ${err instanceof Error ? err.message : String(err)}`, { chainId: this.chainId, txHash }, err instanceof Error ? err : undefined);
527
+ }
528
+ return SolanaTransactionStatus.successful({
529
+ chainId: this.chainId,
530
+ inclusionAt,
531
+ balanceChanges,
532
+ fees,
533
+ });
534
+ }
535
+ async getChainTipHeight() {
536
+ return this.getConnection().getSlot('confirmed');
537
+ }
538
+ /**
539
+ * Resolves the token program owner for a given mint. Token-2022 mints are owned by
540
+ * TOKEN_2022_PROGRAM_ID; classic SPL by TOKEN_PROGRAM_ID. Required for `transferChecked`
541
+ * and ATA derivation.
542
+ */
543
+ async resolveTokenProgramId(mint) {
544
+ const connection = this.getConnection();
545
+ const info = await connection.getAccountInfo(mint);
546
+ if (!info) {
547
+ throw new ChainError(ChainErrorKinds.InvalidTokenIdentifier, `Mint ${mint.toBase58()} not found on chain`, { chainId: this.chainId, identifier: mint.toBase58() });
548
+ }
549
+ const owner = info.owner.toBase58();
550
+ if (owner === TOKEN_PROGRAM_ID.toBase58())
551
+ return TOKEN_PROGRAM_ID;
552
+ if (owner === TOKEN_2022_PROGRAM_ID.toBase58())
553
+ return TOKEN_2022_PROGRAM_ID;
554
+ throw new ChainError(ChainErrorKinds.InvalidTokenIdentifier, `Mint ${mint.toBase58()} is owned by ${owner}, not an SPL Token program`, { chainId: this.chainId, identifier: mint.toBase58() });
555
+ }
556
+ async resolveMintDecimals(mint) {
557
+ const programId = await this.resolveTokenProgramId(mint);
558
+ const mintInfo = await getMint(this.getConnection(), mint, undefined, programId);
559
+ return mintInfo.decimals;
560
+ }
561
+ /**
562
+ * Raw-instruction helper for callers that will compile the transaction downstream
563
+ * (e.g. depositron's SOL_INSTRUCTIONS action type) instead of using
564
+ * createTransferUnsignedTransaction's compiled-tx surface.
565
+ *
566
+ * Synchronous — no RPC.
567
+ */
568
+ buildNativeTransferInstruction(from, to, lamports) {
569
+ if (lamports <= 0n) {
570
+ throw new ChainError(ChainErrorKinds.InvalidArgument, `Native transfer lamports must be positive (got ${lamports})`, { chainId: this.chainId });
571
+ }
572
+ return SystemProgram.transfer({
573
+ fromPubkey: from,
574
+ toPubkey: to,
575
+ lamports,
576
+ });
577
+ }
578
+ /**
579
+ * Raw-instruction helper for SPL / Token-2022 transfers where the caller will compile
580
+ * the transaction downstream. Returns [createATA-idempotent?, transferChecked].
581
+ *
582
+ * Idempotent ATA (vs the probe-then-create pattern in createTransferUnsignedTransaction)
583
+ * is deliberate: when a downstream compiler runs the emitted instructions blindly,
584
+ * a probe done here can race with execution — the idempotent form is a no-op when the
585
+ * ATA exists and a create when it doesn't, without needing a probe RPC.
586
+ *
587
+ * `allowOwnerOffCurve: true` applies to BOTH source and destination ATA derivation —
588
+ * needed for PDA-owned vaults / program-owned recipients. Defaults to false to match
589
+ * createTransferUnsignedTransaction.
590
+ */
591
+ async buildSplTransferInstructions(req) {
592
+ if (req.amount <= 0n) {
593
+ throw new ChainError(ChainErrorKinds.InvalidArgument, `SPL transfer amount must be positive (got ${req.amount})`, { chainId: this.chainId, identifier: req.mint.toBase58() });
594
+ }
595
+ const includeCreateAta = req.includeCreateAta ?? true;
596
+ const allowOwnerOffCurve = req.allowOwnerOffCurve ?? false;
597
+ const programId = await this.resolveTokenProgramId(req.mint);
598
+ const decimals = await this.resolveMintDecimals(req.mint);
599
+ const deriveAta = (owner, role) => {
600
+ try {
601
+ return getAssociatedTokenAddressSync(req.mint, owner, allowOwnerOffCurve, programId);
602
+ }
603
+ catch (err) {
604
+ // spl-token's TokenOwnerOffCurveError ships with an empty message. Rethrow as
605
+ // ChainError so consumers see the SDK's declared error surface, and so the
606
+ // reason is discoverable (pass `allowOwnerOffCurve: true` for PDA-owned wallets).
607
+ throw new ChainError(ChainErrorKinds.InvalidAddress, `${role} owner is off-curve; pass allowOwnerOffCurve:true for PDA-owned wallets`, { chainId: this.chainId, address: owner.toBase58() });
608
+ }
609
+ };
610
+ const sourceAta = deriveAta(req.from, 'source');
611
+ const destAta = deriveAta(req.to, 'destination');
612
+ const instructions = [];
613
+ if (includeCreateAta) {
614
+ instructions.push(createAssociatedTokenAccountIdempotentInstruction(req.from, destAta, req.to, req.mint, programId));
615
+ }
616
+ instructions.push(createTransferCheckedInstruction(sourceAta, req.mint, destAta, req.from, req.amount, decimals, [], programId));
617
+ return instructions;
618
+ }
619
+ /**
620
+ * Decodes per-account lamport deltas + SPL pre/post token-balance diffs into
621
+ * a `NestedBalanceChanges` map. Marked internal via naming rather than TS
622
+ * `private` so chain-agnostic unit tests can exercise it directly without
623
+ * spinning up a Connection.
624
+ */
625
+ _decodeBalanceChanges(tx) {
626
+ const result = new Map();
627
+ if (!tx.meta)
628
+ return result;
629
+ // For v0 messages, meta.preBalances/postBalances are indexed over
630
+ // staticAccountKeys ++ loadedAddresses.writable ++ loadedAddresses.readonly.
631
+ // Iterating only staticAccountKeys silently drops lamport deltas for
632
+ // any LUT-resolved account — a SOL deposit to a wallet that only
633
+ // appears via a lookup table produces no balanceChanges row at all.
634
+ // getTransaction() is called with maxSupportedTransactionVersion: 0
635
+ // so we always see the resolved addresses.
636
+ const staticKeys = tx.transaction.message.staticAccountKeys.map((k) => k.toBase58());
637
+ const loaded = tx.meta.loadedAddresses;
638
+ const loadedWritable = (loaded?.writable ?? []).map((k) => k.toBase58());
639
+ const loadedReadonly = (loaded?.readonly ?? []).map((k) => k.toBase58());
640
+ const accounts = [...staticKeys, ...loadedWritable, ...loadedReadonly];
641
+ const pre = tx.meta.preBalances ?? [];
642
+ const post = tx.meta.postBalances ?? [];
643
+ for (let i = 0; i < accounts.length; i++) {
644
+ const delta = BigInt(post[i] ?? 0) - BigInt(pre[i] ?? 0);
645
+ if (delta === 0n)
646
+ continue;
647
+ AssetBalanceChange.upsert(result, accounts[i], this._nativeToken, AssetBalanceChange.fromMr(delta, this._nativeToken.decimals));
648
+ }
649
+ const preTok = tx.meta.preTokenBalances ?? [];
650
+ const postTok = tx.meta.postTokenBalances ?? [];
651
+ const tokenKey = (b) => `${b.accountIndex}|${b.mint}`;
652
+ // Track the token account's owner (a wallet) and the mint's decimals,
653
+ // both surfaced verbatim by getTransaction's parsed token-balance meta.
654
+ const byKey = new Map();
655
+ for (const b of preTok) {
656
+ byKey.set(tokenKey(b), {
657
+ pre: BigInt(b.uiTokenAmount.amount),
658
+ post: 0n,
659
+ mint: b.mint,
660
+ owner: b.owner ?? null,
661
+ decimals: b.uiTokenAmount.decimals,
662
+ });
663
+ }
664
+ for (const b of postTok) {
665
+ const k = tokenKey(b);
666
+ const cur = byKey.get(k);
667
+ if (cur) {
668
+ cur.post = BigInt(b.uiTokenAmount.amount);
669
+ // Prefer the post-owner if the pre entry lacked one (account
670
+ // freshly initialized inside the tx).
671
+ if (cur.owner === null && b.owner)
672
+ cur.owner = b.owner;
673
+ }
674
+ else {
675
+ byKey.set(k, {
676
+ pre: 0n,
677
+ post: BigInt(b.uiTokenAmount.amount),
678
+ mint: b.mint,
679
+ owner: b.owner ?? null,
680
+ decimals: b.uiTokenAmount.decimals,
681
+ });
682
+ }
683
+ }
684
+ for (const { pre: p, post: q, mint, owner, decimals } of byKey.values()) {
685
+ const delta = q - p;
686
+ if (delta === 0n)
687
+ continue;
688
+ // Skip entries where the token-account owner is unknown — filling in
689
+ // the mint as if it were a wallet address would be actively wrong.
690
+ // Matches Python's behavior in impl/solana/base.py:704-706.
691
+ if (owner === null)
692
+ continue;
693
+ // Symbol is not surfaced by getTransaction; use an UNKNOWN placeholder
694
+ // built from the first four mint characters. Mirrors the EVM decoder's
695
+ // UNKNOWN_<hex-slice> shape (evm_chain.ts:480) so consumers can spot
696
+ // unresolved tokens uniformly.
697
+ const symbol = `UNKNOWN_${mint.slice(0, 4)}`;
698
+ const splToken = new SolanaToken(this.chainId, symbol, mint, decimals);
699
+ AssetBalanceChange.upsert(result, owner, splToken, AssetBalanceChange.fromMr(delta, decimals));
700
+ }
701
+ return result;
702
+ }
703
+ }
704
+ //# sourceMappingURL=solana_chain.js.map