@chainlink/ccip-sdk 0.0.0 → 0.90.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 (319) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +109 -0
  3. package/dist/aptos/exec.d.ts +18 -0
  4. package/dist/aptos/exec.d.ts.map +1 -0
  5. package/dist/aptos/exec.js +55 -0
  6. package/dist/aptos/exec.js.map +1 -0
  7. package/dist/aptos/hasher.d.ts +11 -0
  8. package/dist/aptos/hasher.d.ts.map +1 -0
  9. package/dist/aptos/hasher.js +62 -0
  10. package/dist/aptos/hasher.js.map +1 -0
  11. package/dist/aptos/index.d.ts +92 -0
  12. package/dist/aptos/index.d.ts.map +1 -0
  13. package/dist/aptos/index.js +482 -0
  14. package/dist/aptos/index.js.map +1 -0
  15. package/dist/aptos/logs.d.ts +9 -0
  16. package/dist/aptos/logs.d.ts.map +1 -0
  17. package/dist/aptos/logs.js +167 -0
  18. package/dist/aptos/logs.js.map +1 -0
  19. package/dist/aptos/send.d.ts +11 -0
  20. package/dist/aptos/send.d.ts.map +1 -0
  21. package/dist/aptos/send.js +78 -0
  22. package/dist/aptos/send.js.map +1 -0
  23. package/dist/aptos/token.d.ts +4 -0
  24. package/dist/aptos/token.d.ts.map +1 -0
  25. package/dist/aptos/token.js +134 -0
  26. package/dist/aptos/token.js.map +1 -0
  27. package/dist/aptos/types.d.ts +78 -0
  28. package/dist/aptos/types.d.ts.map +1 -0
  29. package/dist/aptos/types.js +60 -0
  30. package/dist/aptos/types.js.map +1 -0
  31. package/dist/aptos/utils.d.ts +12 -0
  32. package/dist/aptos/utils.d.ts.map +1 -0
  33. package/dist/aptos/utils.js +15 -0
  34. package/dist/aptos/utils.js.map +1 -0
  35. package/dist/chain.d.ts +344 -0
  36. package/dist/chain.d.ts.map +1 -0
  37. package/dist/chain.js +41 -0
  38. package/dist/chain.js.map +1 -0
  39. package/dist/commits.d.ts +25 -0
  40. package/dist/commits.d.ts.map +1 -0
  41. package/dist/commits.js +29 -0
  42. package/dist/commits.js.map +1 -0
  43. package/dist/evm/abi/BurnMintERC677Token.d.ts +602 -0
  44. package/dist/evm/abi/BurnMintERC677Token.d.ts.map +1 -0
  45. package/dist/evm/abi/BurnMintERC677Token.js +488 -0
  46. package/dist/evm/abi/BurnMintERC677Token.js.map +1 -0
  47. package/dist/evm/abi/CommitStore_1_2.d.ts +688 -0
  48. package/dist/evm/abi/CommitStore_1_2.d.ts.map +1 -0
  49. package/dist/evm/abi/CommitStore_1_2.js +638 -0
  50. package/dist/evm/abi/CommitStore_1_2.js.map +1 -0
  51. package/dist/evm/abi/CommitStore_1_5.d.ts +708 -0
  52. package/dist/evm/abi/CommitStore_1_5.d.ts.map +1 -0
  53. package/dist/evm/abi/CommitStore_1_5.js +675 -0
  54. package/dist/evm/abi/CommitStore_1_5.js.map +1 -0
  55. package/dist/evm/abi/FeeQuoter_1_6.d.ts +1770 -0
  56. package/dist/evm/abi/FeeQuoter_1_6.d.ts.map +1 -0
  57. package/dist/evm/abi/FeeQuoter_1_6.js +1904 -0
  58. package/dist/evm/abi/FeeQuoter_1_6.js.map +1 -0
  59. package/dist/evm/abi/LockReleaseTokenPool_1_5.d.ts +1116 -0
  60. package/dist/evm/abi/LockReleaseTokenPool_1_5.d.ts.map +1 -0
  61. package/dist/evm/abi/LockReleaseTokenPool_1_5.js +1096 -0
  62. package/dist/evm/abi/LockReleaseTokenPool_1_5.js.map +1 -0
  63. package/dist/evm/abi/LockReleaseTokenPool_1_5_1.d.ts +1306 -0
  64. package/dist/evm/abi/LockReleaseTokenPool_1_5_1.d.ts.map +1 -0
  65. package/dist/evm/abi/LockReleaseTokenPool_1_5_1.js +1278 -0
  66. package/dist/evm/abi/LockReleaseTokenPool_1_5_1.js.map +1 -0
  67. package/dist/evm/abi/LockReleaseTokenPool_1_6_1.d.ts +1290 -0
  68. package/dist/evm/abi/LockReleaseTokenPool_1_6_1.d.ts.map +1 -0
  69. package/dist/evm/abi/LockReleaseTokenPool_1_6_1.js +1288 -0
  70. package/dist/evm/abi/LockReleaseTokenPool_1_6_1.js.map +1 -0
  71. package/dist/evm/abi/OffRamp_1_2.d.ts +1217 -0
  72. package/dist/evm/abi/OffRamp_1_2.d.ts.map +1 -0
  73. package/dist/evm/abi/OffRamp_1_2.js +1204 -0
  74. package/dist/evm/abi/OffRamp_1_2.js.map +1 -0
  75. package/dist/evm/abi/OffRamp_1_5.d.ts +1271 -0
  76. package/dist/evm/abi/OffRamp_1_5.d.ts.map +1 -0
  77. package/dist/evm/abi/OffRamp_1_5.js +1273 -0
  78. package/dist/evm/abi/OffRamp_1_5.js.map +1 -0
  79. package/dist/evm/abi/OffRamp_1_6.d.ts +1472 -0
  80. package/dist/evm/abi/OffRamp_1_6.d.ts.map +1 -0
  81. package/dist/evm/abi/OffRamp_1_6.js +1529 -0
  82. package/dist/evm/abi/OffRamp_1_6.js.map +1 -0
  83. package/dist/evm/abi/OnRamp_1_2.d.ts +1391 -0
  84. package/dist/evm/abi/OnRamp_1_2.d.ts.map +1 -0
  85. package/dist/evm/abi/OnRamp_1_2.js +1343 -0
  86. package/dist/evm/abi/OnRamp_1_2.js.map +1 -0
  87. package/dist/evm/abi/OnRamp_1_5.d.ts +1443 -0
  88. package/dist/evm/abi/OnRamp_1_5.d.ts.map +1 -0
  89. package/dist/evm/abi/OnRamp_1_5.js +1427 -0
  90. package/dist/evm/abi/OnRamp_1_5.js.map +1 -0
  91. package/dist/evm/abi/OnRamp_1_6.d.ts +796 -0
  92. package/dist/evm/abi/OnRamp_1_6.d.ts.map +1 -0
  93. package/dist/evm/abi/OnRamp_1_6.js +880 -0
  94. package/dist/evm/abi/OnRamp_1_6.js.map +1 -0
  95. package/dist/evm/abi/Router.d.ts +541 -0
  96. package/dist/evm/abi/Router.d.ts.map +1 -0
  97. package/dist/evm/abi/Router.js +508 -0
  98. package/dist/evm/abi/Router.js.map +1 -0
  99. package/dist/evm/abi/TokenAdminRegistry_1_5.d.ts +373 -0
  100. package/dist/evm/abi/TokenAdminRegistry_1_5.d.ts.map +1 -0
  101. package/dist/evm/abi/TokenAdminRegistry_1_5.js +333 -0
  102. package/dist/evm/abi/TokenAdminRegistry_1_5.js.map +1 -0
  103. package/dist/evm/const.d.ts +27 -0
  104. package/dist/evm/const.d.ts.map +1 -0
  105. package/dist/evm/const.js +63 -0
  106. package/dist/evm/const.js.map +1 -0
  107. package/dist/evm/errors.d.ts +36 -0
  108. package/dist/evm/errors.d.ts.map +1 -0
  109. package/dist/evm/errors.js +192 -0
  110. package/dist/evm/errors.js.map +1 -0
  111. package/dist/evm/hasher.d.ts +5 -0
  112. package/dist/evm/hasher.d.ts.map +1 -0
  113. package/dist/evm/hasher.js +116 -0
  114. package/dist/evm/hasher.js.map +1 -0
  115. package/dist/evm/index.d.ts +121 -0
  116. package/dist/evm/index.d.ts.map +1 -0
  117. package/dist/evm/index.js +904 -0
  118. package/dist/evm/index.js.map +1 -0
  119. package/dist/evm/messages.d.ts +35 -0
  120. package/dist/evm/messages.d.ts.map +1 -0
  121. package/dist/evm/messages.js +11 -0
  122. package/dist/evm/messages.js.map +1 -0
  123. package/dist/evm/offchain.d.ts +16 -0
  124. package/dist/evm/offchain.d.ts.map +1 -0
  125. package/dist/evm/offchain.js +142 -0
  126. package/dist/evm/offchain.js.map +1 -0
  127. package/dist/execution.d.ts +80 -0
  128. package/dist/execution.d.ts.map +1 -0
  129. package/dist/execution.js +91 -0
  130. package/dist/execution.js.map +1 -0
  131. package/dist/extra-args.d.ts +45 -0
  132. package/dist/extra-args.d.ts.map +1 -0
  133. package/dist/extra-args.js +44 -0
  134. package/dist/extra-args.js.map +1 -0
  135. package/dist/gas.d.ts +27 -0
  136. package/dist/gas.d.ts.map +1 -0
  137. package/dist/gas.js +80 -0
  138. package/dist/gas.js.map +1 -0
  139. package/dist/hasher/common.d.ts +12 -0
  140. package/dist/hasher/common.d.ts.map +1 -0
  141. package/dist/hasher/common.js +19 -0
  142. package/dist/hasher/common.js.map +1 -0
  143. package/dist/hasher/hasher.d.ts +4 -0
  144. package/dist/hasher/hasher.d.ts.map +1 -0
  145. package/dist/hasher/hasher.js +11 -0
  146. package/dist/hasher/hasher.js.map +1 -0
  147. package/dist/hasher/index.d.ts +4 -0
  148. package/dist/hasher/index.d.ts.map +1 -0
  149. package/dist/hasher/index.js +4 -0
  150. package/dist/hasher/index.js.map +1 -0
  151. package/dist/hasher/merklemulti.d.ts +58 -0
  152. package/dist/hasher/merklemulti.d.ts.map +1 -0
  153. package/dist/hasher/merklemulti.js +257 -0
  154. package/dist/hasher/merklemulti.js.map +1 -0
  155. package/dist/index.d.ts +13 -0
  156. package/dist/index.d.ts.map +1 -0
  157. package/dist/index.js +13 -0
  158. package/dist/index.js.map +1 -0
  159. package/dist/offchain.d.ts +20 -0
  160. package/dist/offchain.d.ts.map +1 -0
  161. package/dist/offchain.js +59 -0
  162. package/dist/offchain.js.map +1 -0
  163. package/dist/requests.d.ts +48 -0
  164. package/dist/requests.d.ts.map +1 -0
  165. package/dist/requests.js +286 -0
  166. package/dist/requests.js.map +1 -0
  167. package/dist/selectors.d.ts +9 -0
  168. package/dist/selectors.d.ts.map +1 -0
  169. package/dist/selectors.js +1330 -0
  170. package/dist/selectors.js.map +1 -0
  171. package/dist/solana/cleanup.d.ts +15 -0
  172. package/dist/solana/cleanup.d.ts.map +1 -0
  173. package/dist/solana/cleanup.js +159 -0
  174. package/dist/solana/cleanup.js.map +1 -0
  175. package/dist/solana/exec.d.ts +15 -0
  176. package/dist/solana/exec.d.ts.map +1 -0
  177. package/dist/solana/exec.js +417 -0
  178. package/dist/solana/exec.js.map +1 -0
  179. package/dist/solana/hasher.d.ts +4 -0
  180. package/dist/solana/hasher.d.ts.map +1 -0
  181. package/dist/solana/hasher.js +81 -0
  182. package/dist/solana/hasher.js.map +1 -0
  183. package/dist/solana/idl/1.6.0/BASE_TOKEN_POOL.d.ts +866 -0
  184. package/dist/solana/idl/1.6.0/BASE_TOKEN_POOL.d.ts.map +1 -0
  185. package/dist/solana/idl/1.6.0/BASE_TOKEN_POOL.js +866 -0
  186. package/dist/solana/idl/1.6.0/BASE_TOKEN_POOL.js.map +1 -0
  187. package/dist/solana/idl/1.6.0/BURN_MINT_TOKEN_POOL.d.ts +949 -0
  188. package/dist/solana/idl/1.6.0/BURN_MINT_TOKEN_POOL.d.ts.map +1 -0
  189. package/dist/solana/idl/1.6.0/BURN_MINT_TOKEN_POOL.js +949 -0
  190. package/dist/solana/idl/1.6.0/BURN_MINT_TOKEN_POOL.js.map +1 -0
  191. package/dist/solana/idl/1.6.0/CCIP_CCTP_TOKEN_POOL.d.ts +1374 -0
  192. package/dist/solana/idl/1.6.0/CCIP_CCTP_TOKEN_POOL.d.ts.map +1 -0
  193. package/dist/solana/idl/1.6.0/CCIP_CCTP_TOKEN_POOL.js +1374 -0
  194. package/dist/solana/idl/1.6.0/CCIP_CCTP_TOKEN_POOL.js.map +1 -0
  195. package/dist/solana/idl/1.6.0/CCIP_COMMON.d.ts +104 -0
  196. package/dist/solana/idl/1.6.0/CCIP_COMMON.d.ts.map +1 -0
  197. package/dist/solana/idl/1.6.0/CCIP_COMMON.js +104 -0
  198. package/dist/solana/idl/1.6.0/CCIP_COMMON.js.map +1 -0
  199. package/dist/solana/idl/1.6.0/CCIP_OFFRAMP.d.ts +2746 -0
  200. package/dist/solana/idl/1.6.0/CCIP_OFFRAMP.d.ts.map +1 -0
  201. package/dist/solana/idl/1.6.0/CCIP_OFFRAMP.js +2746 -0
  202. package/dist/solana/idl/1.6.0/CCIP_OFFRAMP.js.map +1 -0
  203. package/dist/solana/idl/1.6.0/CCIP_ROUTER.d.ts +2332 -0
  204. package/dist/solana/idl/1.6.0/CCIP_ROUTER.d.ts.map +1 -0
  205. package/dist/solana/idl/1.6.0/CCIP_ROUTER.js +2332 -0
  206. package/dist/solana/idl/1.6.0/CCIP_ROUTER.js.map +1 -0
  207. package/dist/solana/index.d.ts +205 -0
  208. package/dist/solana/index.d.ts.map +1 -0
  209. package/dist/solana/index.js +1085 -0
  210. package/dist/solana/index.js.map +1 -0
  211. package/dist/solana/offchain.d.ts +31 -0
  212. package/dist/solana/offchain.d.ts.map +1 -0
  213. package/dist/solana/offchain.js +152 -0
  214. package/dist/solana/offchain.js.map +1 -0
  215. package/dist/solana/patchBorsh.d.ts +2 -0
  216. package/dist/solana/patchBorsh.d.ts.map +1 -0
  217. package/dist/solana/patchBorsh.js +60 -0
  218. package/dist/solana/patchBorsh.js.map +1 -0
  219. package/dist/solana/send.d.ts +14 -0
  220. package/dist/solana/send.d.ts.map +1 -0
  221. package/dist/solana/send.js +272 -0
  222. package/dist/solana/send.js.map +1 -0
  223. package/dist/solana/types.d.ts +4 -0
  224. package/dist/solana/types.d.ts.map +1 -0
  225. package/dist/solana/types.js +2 -0
  226. package/dist/solana/types.js.map +1 -0
  227. package/dist/solana/utils.d.ts +58 -0
  228. package/dist/solana/utils.d.ts.map +1 -0
  229. package/dist/solana/utils.js +211 -0
  230. package/dist/solana/utils.js.map +1 -0
  231. package/dist/sui/hasher.d.ts +12 -0
  232. package/dist/sui/hasher.d.ts.map +1 -0
  233. package/dist/sui/hasher.js +63 -0
  234. package/dist/sui/hasher.js.map +1 -0
  235. package/dist/sui/index.d.ts +72 -0
  236. package/dist/sui/index.d.ts.map +1 -0
  237. package/dist/sui/index.js +128 -0
  238. package/dist/sui/index.js.map +1 -0
  239. package/dist/sui/types.d.ts +17 -0
  240. package/dist/sui/types.d.ts.map +1 -0
  241. package/dist/sui/types.js +17 -0
  242. package/dist/sui/types.js.map +1 -0
  243. package/dist/supported-chains.d.ts +5 -0
  244. package/dist/supported-chains.d.ts.map +1 -0
  245. package/dist/supported-chains.js +3 -0
  246. package/dist/supported-chains.js.map +1 -0
  247. package/dist/types.d.ts +118 -0
  248. package/dist/types.d.ts.map +1 -0
  249. package/dist/types.js +11 -0
  250. package/dist/types.js.map +1 -0
  251. package/dist/utils.d.ts +117 -0
  252. package/dist/utils.d.ts.map +1 -0
  253. package/dist/utils.js +336 -0
  254. package/dist/utils.js.map +1 -0
  255. package/package.json +66 -8
  256. package/src/aptos/exec.ts +69 -0
  257. package/src/aptos/hasher.ts +92 -0
  258. package/src/aptos/index.ts +660 -0
  259. package/src/aptos/logs.ts +210 -0
  260. package/src/aptos/send.ts +120 -0
  261. package/src/aptos/token.ts +150 -0
  262. package/src/aptos/types.ts +85 -0
  263. package/src/aptos/utils.ts +24 -0
  264. package/src/chain.ts +398 -0
  265. package/src/commits.ts +44 -0
  266. package/src/evm/abi/BurnMintERC677Token.ts +487 -0
  267. package/src/evm/abi/CommitStore_1_2.ts +637 -0
  268. package/src/evm/abi/CommitStore_1_5.ts +674 -0
  269. package/src/evm/abi/FeeQuoter_1_6.ts +1903 -0
  270. package/src/evm/abi/LockReleaseTokenPool_1_5.ts +1095 -0
  271. package/src/evm/abi/LockReleaseTokenPool_1_5_1.ts +1277 -0
  272. package/src/evm/abi/LockReleaseTokenPool_1_6_1.ts +1287 -0
  273. package/src/evm/abi/OffRamp_1_2.ts +1203 -0
  274. package/src/evm/abi/OffRamp_1_5.ts +1272 -0
  275. package/src/evm/abi/OffRamp_1_6.ts +1528 -0
  276. package/src/evm/abi/OnRamp_1_2.ts +1342 -0
  277. package/src/evm/abi/OnRamp_1_5.ts +1426 -0
  278. package/src/evm/abi/OnRamp_1_6.ts +879 -0
  279. package/src/evm/abi/Router.ts +507 -0
  280. package/src/evm/abi/TokenAdminRegistry_1_5.ts +332 -0
  281. package/src/evm/const.ts +69 -0
  282. package/src/evm/errors.ts +212 -0
  283. package/src/evm/hasher.ts +166 -0
  284. package/src/evm/index.ts +1262 -0
  285. package/src/evm/messages.ts +73 -0
  286. package/src/evm/offchain.ts +189 -0
  287. package/src/execution.ts +131 -0
  288. package/src/extra-args.ts +71 -0
  289. package/src/gas.ts +135 -0
  290. package/src/hasher/common.ts +23 -0
  291. package/src/hasher/hasher.ts +12 -0
  292. package/src/hasher/index.ts +3 -0
  293. package/src/hasher/merklemulti.ts +309 -0
  294. package/src/index.ts +51 -0
  295. package/src/offchain.ts +86 -0
  296. package/src/requests.ts +339 -0
  297. package/src/selectors.ts +1340 -0
  298. package/src/solana/cleanup.ts +216 -0
  299. package/src/solana/exec.ts +645 -0
  300. package/src/solana/hasher.ts +104 -0
  301. package/src/solana/idl/1.6.0/BASE_TOKEN_POOL.ts +1734 -0
  302. package/src/solana/idl/1.6.0/BURN_MINT_TOKEN_POOL.ts +1900 -0
  303. package/src/solana/idl/1.6.0/CCIP_CCTP_TOKEN_POOL.ts +2750 -0
  304. package/src/solana/idl/1.6.0/CCIP_COMMON.ts +210 -0
  305. package/src/solana/idl/1.6.0/CCIP_OFFRAMP.ts +5494 -0
  306. package/src/solana/idl/1.6.0/CCIP_ROUTER.ts +4671 -0
  307. package/src/solana/index.ts +1454 -0
  308. package/src/solana/offchain.ts +209 -0
  309. package/src/solana/patchBorsh.ts +67 -0
  310. package/src/solana/send.ts +436 -0
  311. package/src/solana/types.ts +6 -0
  312. package/src/solana/utils.ts +272 -0
  313. package/src/sui/hasher.ts +90 -0
  314. package/src/sui/index.ts +198 -0
  315. package/src/sui/types.ts +22 -0
  316. package/src/supported-chains.ts +4 -0
  317. package/src/types.ts +153 -0
  318. package/src/utils.ts +405 -0
  319. package/tsconfig.json +18 -0
@@ -0,0 +1,1454 @@
1
+ import util from 'util'
2
+
3
+ import {
4
+ type Idl,
5
+ type IdlTypes,
6
+ AnchorProvider,
7
+ BorshAccountsCoder,
8
+ BorshCoder,
9
+ Program,
10
+ Wallet,
11
+ eventDiscriminator,
12
+ } from '@coral-xyz/anchor'
13
+ import { NATIVE_MINT } from '@solana/spl-token'
14
+ import {
15
+ type Commitment,
16
+ type ConfirmedSignatureInfo,
17
+ type ConnectionConfig,
18
+ type VersionedTransactionResponse,
19
+ Connection,
20
+ Keypair,
21
+ PublicKey,
22
+ SYSVAR_CLOCK_PUBKEY,
23
+ SystemProgram,
24
+ } from '@solana/web3.js'
25
+ import type BN from 'bn.js'
26
+ import bs58 from 'bs58'
27
+ import {
28
+ type BytesLike,
29
+ concat,
30
+ dataLength,
31
+ dataSlice,
32
+ encodeBase58,
33
+ encodeBase64,
34
+ getBytes,
35
+ hexlify,
36
+ isHexString,
37
+ toBigInt,
38
+ } from 'ethers'
39
+ import moize, { type Moized } from 'moize'
40
+
41
+ import {
42
+ type ChainTransaction,
43
+ type LogFilter,
44
+ type RateLimiterState,
45
+ type TokenInfo,
46
+ type TokenPoolRemote,
47
+ Chain,
48
+ ChainFamily,
49
+ } from '../chain.ts'
50
+ import { type EVMExtraArgsV2, type ExtraArgs, EVMExtraArgsV2Tag } from '../extra-args.ts'
51
+ import type { LeafHasher } from '../hasher/common.ts'
52
+ import SELECTORS from '../selectors.ts'
53
+ import { supportedChains } from '../supported-chains.ts'
54
+ import {
55
+ type AnyMessage,
56
+ type CCIPCommit,
57
+ type CCIPExecution,
58
+ type CCIPMessage,
59
+ type CCIPRequest,
60
+ type CommitReport,
61
+ type ExecutionReceipt,
62
+ type ExecutionReport,
63
+ type Lane,
64
+ type Log_,
65
+ type NetworkInfo,
66
+ type OffchainTokenData,
67
+ CCIPVersion,
68
+ ExecutionState,
69
+ } from '../types.ts'
70
+ import {
71
+ createRateLimitedFetch,
72
+ decodeAddress,
73
+ decodeOnRampAddress,
74
+ getDataBytes,
75
+ leToBigInt,
76
+ networkInfo,
77
+ parseTypeAndVersion,
78
+ toLeArray,
79
+ } from '../utils.ts'
80
+ import { cleanUpBuffers } from './cleanup.ts'
81
+ import { executeReport } from './exec.ts'
82
+ import { getV16SolanaLeafHasher } from './hasher.ts'
83
+ import { IDL as BASE_TOKEN_POOL } from './idl/1.6.0/BASE_TOKEN_POOL.ts'
84
+ import { IDL as BURN_MINT_TOKEN_POOL } from './idl/1.6.0/BURN_MINT_TOKEN_POOL.ts'
85
+ import { IDL as CCIP_CCTP_TOKEN_POOL } from './idl/1.6.0/CCIP_CCTP_TOKEN_POOL.ts'
86
+ import { IDL as CCIP_OFFRAMP_IDL } from './idl/1.6.0/CCIP_OFFRAMP.ts'
87
+ import { IDL as CCIP_ROUTER_IDL } from './idl/1.6.0/CCIP_ROUTER.ts'
88
+ import { fetchSolanaOffchainTokenData } from './offchain.ts'
89
+ import { ccipSend, getFee } from './send.ts'
90
+ import type { CCIPMessage_V1_6_Solana } from './types.ts'
91
+ import { bytesToBuffer, getErrorFromLogs, parseSolanaLogs, simulationProvider } from './utils.ts'
92
+
93
+ const routerCoder = new BorshCoder(CCIP_ROUTER_IDL)
94
+ const offrampCoder = new BorshCoder(CCIP_OFFRAMP_IDL)
95
+ const tokenPoolCoder = new BorshCoder({
96
+ ...BURN_MINT_TOKEN_POOL,
97
+ types: BASE_TOKEN_POOL.types,
98
+ events: BASE_TOKEN_POOL.events,
99
+ errors: [...BASE_TOKEN_POOL.errors, ...BURN_MINT_TOKEN_POOL.errors],
100
+ })
101
+ const cctpTokenPoolCoder = new BorshCoder({
102
+ ...CCIP_CCTP_TOKEN_POOL,
103
+ types: [...BASE_TOKEN_POOL.types, ...CCIP_CCTP_TOKEN_POOL.types],
104
+ events: [...BASE_TOKEN_POOL.events, ...CCIP_CCTP_TOKEN_POOL.events],
105
+ errors: [...BASE_TOKEN_POOL.errors, ...CCIP_CCTP_TOKEN_POOL.errors],
106
+ })
107
+ // const commonCoder = new BorshCoder(CCIP_COMMON_IDL)
108
+
109
+ interface ParsedTokenInfo {
110
+ name?: string
111
+ symbol?: string
112
+ decimals: number
113
+ }
114
+
115
+ // hardcoded symbols for tokens without metadata
116
+ const unknownTokens: { [mint: string]: string } = {
117
+ '4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU': 'USDC', // devnet
118
+ }
119
+
120
+ // some circular specialized types, but all good with proper references
121
+ type SolanaLog = Log_ & { tx: SolanaTransaction }
122
+ type SolanaTransaction = ChainTransaction & {
123
+ tx: VersionedTransactionResponse
124
+ logs: SolanaLog[]
125
+ }
126
+
127
+ function hexDiscriminator(eventName: string): string {
128
+ return hexlify(eventDiscriminator(eventName))
129
+ }
130
+
131
+ export class SolanaChain extends Chain<typeof ChainFamily.Solana> {
132
+ static readonly family = ChainFamily.Solana
133
+ static readonly decimals = 9
134
+
135
+ readonly network: NetworkInfo<typeof ChainFamily.Solana>
136
+ readonly connection: Connection
137
+ readonly commitment: Commitment = 'confirmed'
138
+
139
+ _getSignaturesForAddress: (
140
+ programId: string,
141
+ before?: string,
142
+ ) => Promise<ConfirmedSignatureInfo[]>
143
+
144
+ constructor(connection: Connection, network: NetworkInfo) {
145
+ super()
146
+
147
+ if (network.family !== ChainFamily.Solana) {
148
+ throw new Error(`Invalid network family for SolanaChain: ${network.family}`)
149
+ }
150
+ this.network = network
151
+ this.connection = connection
152
+
153
+ // Memoize expensive operations
154
+ this.typeAndVersion = moize.default(this.typeAndVersion.bind(this), {
155
+ maxArgs: 1,
156
+ isPromise: true,
157
+ })
158
+ this.getBlockTimestamp = moize.default(this.getBlockTimestamp.bind(this), {
159
+ isPromise: true,
160
+ maxSize: 100,
161
+ updateCacheForKey: (key) => typeof key[key.length - 1] !== 'number',
162
+ })
163
+ this.getTransaction = moize.default(this.getTransaction.bind(this), {
164
+ maxSize: 100,
165
+ maxArgs: 1,
166
+ })
167
+ this.getWallet = moize.default(this.getWallet.bind(this), { maxSize: 1, maxArgs: 0 })
168
+ this.getTokenForTokenPool = moize.default(this.getTokenForTokenPool.bind(this))
169
+ this.getTokenInfo = moize.default(this.getTokenInfo.bind(this))
170
+ this._getSignaturesForAddress = moize.default(
171
+ (programId: string, before?: string) =>
172
+ this.connection.getSignaturesForAddress(
173
+ new PublicKey(programId),
174
+ { limit: 1000, before },
175
+ 'confirmed',
176
+ ),
177
+ {
178
+ maxSize: 100,
179
+ maxAge: 60000,
180
+ maxArgs: 2,
181
+ isPromise: true,
182
+ updateExpire: true,
183
+ // only expire undefined before (i.e. recent getSignaturesForAddress calls)
184
+ onExpire: ([, before]) => !before,
185
+ },
186
+ )
187
+ // cache account info for 30 seconds
188
+ this.connection.getAccountInfo = moize.default(
189
+ this.connection.getAccountInfo.bind(this.connection),
190
+ {
191
+ maxSize: 100,
192
+ maxArgs: 2,
193
+ maxAge: 30e3,
194
+ transformArgs: ([address, commitment]) =>
195
+ [(address as PublicKey).toString(), commitment] as const,
196
+ },
197
+ )
198
+
199
+ this._getRouterConfig = moize.default(this._getRouterConfig.bind(this), {
200
+ maxArgs: 1,
201
+ })
202
+
203
+ this.listFeeTokens = moize.default(this.listFeeTokens.bind(this), {
204
+ maxArgs: 1,
205
+ })
206
+ }
207
+
208
+ static _getConnection(url: string): Connection {
209
+ if (!url.startsWith('http') && !url.startsWith('ws')) {
210
+ throw new Error(
211
+ `Invalid Solana RPC URL format (should be https://, http://, wss://, or ws://): ${url}`,
212
+ )
213
+ }
214
+
215
+ const config: ConnectionConfig = { commitment: 'confirmed' }
216
+ if (url.includes('.solana.com')) {
217
+ config.fetch = createRateLimitedFetch({
218
+ maxRequests: 10,
219
+ maxRetries: 3,
220
+ windowMs: 11e3,
221
+ }) // public nodes
222
+ console.warn('Using rate-limited fetch for public solana nodes, commands may be slow')
223
+ }
224
+
225
+ return new Connection(url, config)
226
+ }
227
+
228
+ static async fromConnection(connection: Connection): Promise<SolanaChain> {
229
+ // Get genesis hash to use as chainId
230
+ return new SolanaChain(connection, networkInfo(await connection.getGenesisHash()))
231
+ }
232
+
233
+ static async fromUrl(url: string): Promise<SolanaChain> {
234
+ const connection = this._getConnection(url)
235
+ return this.fromConnection(connection)
236
+ }
237
+
238
+ async destroy(): Promise<void> {
239
+ // Solana Connection doesn't have an explicit destroy method
240
+ // The memoized functions will be garbage collected when the instance is destroyed
241
+ }
242
+
243
+ static getWallet(_opts?: { wallet?: unknown }): Promise<Wallet> {
244
+ throw new Error('Wallet not implemented')
245
+ }
246
+
247
+ /**
248
+ * Load wallet
249
+ * @param opts - options to load wallet
250
+ * @param opts.wallet - private key as 0x or base58 string, or async getter function resolving to
251
+ * Wallet instance
252
+ * @returns Wallet, after caching in instance
253
+ */
254
+ async getWallet(opts: { wallet?: unknown } = {}): Promise<Wallet> {
255
+ try {
256
+ if (typeof opts.wallet === 'string')
257
+ return new Wallet(
258
+ Keypair.fromSecretKey(
259
+ opts.wallet.startsWith('0x') ? getBytes(opts.wallet) : bs58.decode(opts.wallet),
260
+ ),
261
+ )
262
+ } catch (_) {
263
+ // pass
264
+ }
265
+ return (this.constructor as typeof SolanaChain).getWallet(opts)
266
+ }
267
+
268
+ async getWalletAddress(opts?: { wallet?: unknown }): Promise<string> {
269
+ return (await this.getWallet(opts)).publicKey.toBase58()
270
+ }
271
+
272
+ // cached
273
+ async getBlockTimestamp(block: number | 'finalized'): Promise<number> {
274
+ if (block === 'finalized') {
275
+ const slot = await this.connection.getSlot('finalized')
276
+ const blockTime = await this.connection.getBlockTime(slot)
277
+ if (blockTime === null) {
278
+ throw new Error(`Could not get block time for finalized slot ${slot}`)
279
+ }
280
+ return blockTime
281
+ }
282
+
283
+ const blockTime = await this.connection.getBlockTime(block)
284
+ if (blockTime === null) {
285
+ throw new Error(`Could not get block time for slot ${block}`)
286
+ }
287
+ return blockTime
288
+ }
289
+
290
+ // cached
291
+ async getTransaction(hash: string): Promise<SolanaTransaction> {
292
+ const tx = await this.connection.getTransaction(hash, {
293
+ commitment: 'confirmed',
294
+ maxSupportedTransactionVersion: 0,
295
+ })
296
+ if (!tx) throw new Error(`Transaction not found: ${hash}`)
297
+ if (tx.blockTime) {
298
+ ;(this.getBlockTimestamp as Moized<typeof this.getBlockTimestamp>).set(
299
+ [tx.slot],
300
+ Promise.resolve(tx.blockTime),
301
+ )
302
+ } else {
303
+ tx.blockTime = await this.getBlockTimestamp(tx.slot)
304
+ }
305
+
306
+ // Parse logs from transaction using helper function
307
+ const logs_: Log_[] = tx.meta?.logMessages?.length
308
+ ? parseSolanaLogs(tx.meta?.logMessages).map((l) => ({
309
+ ...l,
310
+ transactionHash: hash,
311
+ blockNumber: tx.slot,
312
+ }))
313
+ : []
314
+
315
+ const chainTx: SolanaTransaction = {
316
+ chain: this,
317
+ hash,
318
+ logs: [] as SolanaLog[],
319
+ blockNumber: tx.slot,
320
+ timestamp: tx.blockTime,
321
+ from: tx.transaction.message.staticAccountKeys[0].toString(),
322
+ error: tx.meta?.err,
323
+ tx, // specialized solana transaction
324
+ }
325
+ // solana logs include circular reference to tx
326
+ chainTx.logs = logs_.map((l) => Object.assign(l, { tx: chainTx }))
327
+ return chainTx
328
+ }
329
+
330
+ // implements inner paging logic for this.getLogs
331
+ async *_getTransactionsForAddress(
332
+ opts: Omit<LogFilter, 'topics'>,
333
+ ): AsyncGenerator<SolanaTransaction> {
334
+ if (!opts.address) throw new Error('Program address is required for Solana log filtering')
335
+
336
+ let allSignatures
337
+ if (opts.startBlock || opts.startTime) {
338
+ // forward collect all matching sigs in array
339
+ const allSigs: { signature: string; slot: number; blockTime?: number | null }[] = []
340
+ let batch: Awaited<ReturnType<typeof this.connection.getSignaturesForAddress>> | undefined,
341
+ popped = false
342
+ while (!popped && (batch?.length ?? true)) {
343
+ batch = await this._getSignaturesForAddress(
344
+ opts.address,
345
+ allSigs[allSigs.length - 1]?.signature,
346
+ )
347
+ while (
348
+ batch.length > 0 &&
349
+ (batch[batch.length - 1].slot < (opts.startBlock || 0) ||
350
+ (batch[batch.length - 1].blockTime || -1) < (opts.startTime || 0))
351
+ ) {
352
+ batch.pop() // pop tail of txs which are older than requested start
353
+ popped = true
354
+ }
355
+ allSigs.push(...batch)
356
+ }
357
+ allSigs.reverse()
358
+ while (
359
+ opts.endBlock &&
360
+ allSigs.length > 0 &&
361
+ allSigs[allSigs.length - 1].slot > opts.endBlock
362
+ ) {
363
+ allSigs.pop() // pop head (after reverse) of txs which are newer than requested end
364
+ }
365
+ allSignatures = allSigs
366
+ } else {
367
+ allSignatures = async function* (this: SolanaChain) {
368
+ let batch: { signature: string; slot: number; blockTime?: number | null }[] | undefined
369
+ while (batch?.length ?? true) {
370
+ batch = await this._getSignaturesForAddress(
371
+ opts.address!,
372
+ batch?.length
373
+ ? batch[batch.length - 1].signature
374
+ : opts.endBefore
375
+ ? opts.endBefore
376
+ : undefined,
377
+ )
378
+ for (const sig of batch) {
379
+ if (opts.endBlock && sig.slot > opts.endBlock) continue
380
+ yield sig
381
+ }
382
+ }
383
+ }.call(this) // generate backwards until depleting getSignaturesForAddress
384
+ }
385
+
386
+ // Process signatures
387
+ for await (const signatureInfo of allSignatures) {
388
+ yield await this.getTransaction(signatureInfo.signature)
389
+ }
390
+ }
391
+
392
+ /**
393
+ * Retrieves logs from Solana transactions with enhanced chronological ordering.
394
+ *
395
+ * Behavior:
396
+ * - If opts.startBlock or opts.startTime is provided:
397
+ * * Fetches ALL signatures for the address going back in time
398
+ * * Continues fetching until finding signatures older than the start target
399
+ * * Filters out signatures older than start criteria
400
+ * * Returns logs in chronological order (oldest first)
401
+ *
402
+ * - If opts.startBlock and opts.startTime are omitted:
403
+ * * Fetches signatures in reverse chronological order (newest first)
404
+ * * Returns logs in reverse chronological order (newest first)
405
+ *
406
+ * @param opts - Log filter options
407
+ * @param opts.startBlock - Starting slot number (inclusive)
408
+ * @param opts.startTime - Starting Unix timestamp (inclusive)
409
+ * @param opts.endBlock - Ending slot number (inclusive)
410
+ * @param opts.address - Program address to filter logs by (required for Solana)
411
+ * @param opts.topics - Array of topics to filter logs by (optional);
412
+ * either 0x-8B discriminants or event names
413
+ * @param.opts.programs - a special option to allow querying by address of interest, but
414
+ * yielding matching logs from specific (string address) program or any (true)
415
+ * @param opts.commit - Special param for fetching ExecutionReceipts, to narrow down the search
416
+ * @returns AsyncIterableIterator of parsed Log_ objects
417
+ */
418
+ async *getLogs(
419
+ opts: LogFilter & { sender?: string; programs?: string[] | true; commit?: CommitReport },
420
+ ): AsyncGenerator<Log_ & { tx: SolanaTransaction }> {
421
+ let programs: true | string[]
422
+ if (opts.sender && !opts.address) {
423
+ // specialization for fetching txs/requests for a given account of interest without a programID
424
+ opts.address = opts.sender
425
+ programs = true
426
+ } else if (!opts.address) {
427
+ throw new Error('Program address is required for Solana log filtering')
428
+ } else if (!opts.programs) {
429
+ programs = [opts.address]
430
+ } else {
431
+ programs = opts.programs
432
+ }
433
+ if (opts.topics?.length) {
434
+ if (!opts.topics.every((topic) => typeof topic === 'string'))
435
+ throw new Error('Topics must be strings')
436
+ // append events discriminants (if not 0x-8B already), but keep OG topics
437
+ opts.topics.push(
438
+ ...opts.topics.filter((t) => !isHexString(t, 8)).map((t) => hexDiscriminator(t)),
439
+ )
440
+ }
441
+
442
+ // Process signatures and yield logs
443
+ for await (const tx of this._getTransactionsForAddress(opts)) {
444
+ for (const log of tx.logs) {
445
+ // Filter and yield logs from the specified program, and which match event discriminant or log prefix
446
+ if (
447
+ (programs !== true && !programs.includes(log.address)) ||
448
+ (opts.topics?.length &&
449
+ !(opts.topics as string[]).some(
450
+ (t) =>
451
+ t === log.topics[0] || (typeof log.data === 'string' && log.data.startsWith(t)),
452
+ ))
453
+ )
454
+ continue
455
+ yield Object.assign(log, { timestamp: new Date(tx.timestamp * 1000) })
456
+ }
457
+ }
458
+ }
459
+
460
+ async typeAndVersion(address: string) {
461
+ const program = new Program(
462
+ CCIP_OFFRAMP_IDL, // `typeVersion` schema should be the same
463
+ new PublicKey(address),
464
+ simulationProvider(this.connection),
465
+ )
466
+
467
+ // Create the typeVersion instruction
468
+ const returnDataString = (await program.methods
469
+ .typeVersion()
470
+ .accounts({ clock: SYSVAR_CLOCK_PUBKEY })
471
+ .view()) as string
472
+ const res = parseTypeAndVersion(returnDataString.trim())
473
+ if (res[1].startsWith('0.1.')) res[1] = CCIPVersion.V1_6
474
+ return res
475
+ }
476
+
477
+ getRouterForOnRamp(onRamp: string, _destChainSelector: bigint): Promise<string> {
478
+ return Promise.resolve(onRamp) // Solana's router is also the onRamp
479
+ }
480
+
481
+ async getRouterForOffRamp(offRamp: string, _sourceChainSelector: bigint): Promise<string> {
482
+ const offRamp_ = new PublicKey(offRamp)
483
+ const program = new Program(CCIP_OFFRAMP_IDL as Idl, offRamp_, {
484
+ connection: this.connection,
485
+ })
486
+
487
+ const [referenceAddressesAddr] = PublicKey.findProgramAddressSync(
488
+ [Buffer.from('reference_addresses')],
489
+ offRamp_,
490
+ )
491
+ const referenceAddressesPda = await this.connection.getAccountInfo(referenceAddressesAddr)
492
+ if (!referenceAddressesPda)
493
+ throw new Error(`referenceAddresses account not found for offRamp=${offRamp}`)
494
+
495
+ // Decode the config account using the program's coder
496
+ const { router }: { router: PublicKey } = program.coder.accounts.decode(
497
+ 'referenceAddresses',
498
+ referenceAddressesPda.data,
499
+ )
500
+ return router.toBase58()
501
+ }
502
+
503
+ getNativeTokenForRouter(_router: string): Promise<string> {
504
+ return Promise.resolve(NATIVE_MINT.toBase58())
505
+ }
506
+
507
+ async getOffRampsForRouter(router: string, sourceChainSelector: bigint): Promise<string[]> {
508
+ // feeQuoter is present in router's config, and has a DestChainState account which is updated by
509
+ // the offramps, so we can use it to narrow the search for the offramp
510
+ const { feeQuoter } = await this._getRouterConfig(router)
511
+
512
+ const [feeQuoterDestChainStateAccountAddress] = PublicKey.findProgramAddressSync(
513
+ [Buffer.from('dest_chain'), toLeArray(sourceChainSelector, 8)],
514
+ feeQuoter,
515
+ )
516
+
517
+ for await (const log of this.getLogs({
518
+ programs: true,
519
+ address: feeQuoterDestChainStateAccountAddress.toBase58(),
520
+ topics: ['ExecutionStateChanged', 'CommitReportAccepted', 'Transmitted'],
521
+ })) {
522
+ return [log.address] // assume single offramp per router/deployment on Solana
523
+ }
524
+ throw new Error(`Could not find OffRamp events in feeQuoter=${feeQuoter.toString()} txs`)
525
+ }
526
+
527
+ getOnRampForRouter(router: string, _destChainSelector: bigint): Promise<string> {
528
+ return Promise.resolve(router) // solana's Router is also the OnRamp
529
+ }
530
+
531
+ async getOnRampForOffRamp(offRamp: string, sourceChainSelector: bigint): Promise<string> {
532
+ const program = new Program(CCIP_OFFRAMP_IDL, new PublicKey(offRamp), {
533
+ connection: this.connection,
534
+ })
535
+
536
+ const [statePda] = PublicKey.findProgramAddressSync(
537
+ [Buffer.from('source_chain_state'), toLeArray(sourceChainSelector, 8)],
538
+ program.programId,
539
+ )
540
+
541
+ // Decode the config account using the program's coder
542
+ const {
543
+ config: { onRamp },
544
+ } = await program.account.sourceChain.fetch(statePda)
545
+ return decodeAddress(
546
+ new Uint8Array(onRamp.bytes.slice(0, onRamp.len)),
547
+ networkInfo(sourceChainSelector).family,
548
+ )
549
+ }
550
+
551
+ getCommitStoreForOffRamp(offRamp: string): Promise<string> {
552
+ return Promise.resolve(offRamp) // Solana supports only CCIP>=1.6, for which OffRamp and CommitStore are the same
553
+ }
554
+
555
+ async getTokenForTokenPool(tokenPool: string): Promise<string> {
556
+ const tokenPoolInfo = await this.connection.getAccountInfo(new PublicKey(tokenPool))
557
+ if (!tokenPoolInfo) throw new Error(`TokenPool info not found: ${tokenPool}`)
558
+ const { config }: { config: { mint: PublicKey } } = tokenPoolCoder.accounts.decode(
559
+ 'state',
560
+ tokenPoolInfo.data,
561
+ )
562
+ return config.mint.toString()
563
+ }
564
+
565
+ async getTokenInfo(token: string): Promise<TokenInfo> {
566
+ const mint = new PublicKey(token)
567
+ const mintInfo = await this.connection.getParsedAccountInfo(mint)
568
+
569
+ if (
570
+ !mintInfo.value ||
571
+ !mintInfo.value.data ||
572
+ (typeof mintInfo.value.data === 'object' &&
573
+ 'program' in mintInfo.value.data &&
574
+ mintInfo.value.data.program !== 'spl-token' &&
575
+ mintInfo.value.data.program !== 'spl-token-2022')
576
+ ) {
577
+ throw new Error(`Invalid SPL token or Token-2022: ${token}`)
578
+ }
579
+
580
+ if (typeof mintInfo.value.data === 'object' && 'parsed' in mintInfo.value.data) {
581
+ const parsed = mintInfo.value.data.parsed as { info: ParsedTokenInfo }
582
+ const data = parsed.info
583
+ let symbol = data.symbol || unknownTokens[token] || 'UNKNOWN'
584
+ let name = data.name
585
+
586
+ // If symbol or name is missing, try to fetch from Metaplex metadata
587
+ if (!data.symbol || symbol === 'UNKNOWN' || !data.name) {
588
+ try {
589
+ const metadata = await this._fetchTokenMetadata(mint)
590
+ if (metadata) {
591
+ if (metadata.symbol && (!data.symbol || symbol === 'UNKNOWN')) {
592
+ symbol = metadata.symbol
593
+ }
594
+ if (metadata.name && !name) {
595
+ name = metadata.name
596
+ }
597
+ }
598
+ } catch (error) {
599
+ // Metaplex metadata fetch failed, keep the default values
600
+ console.debug(`Failed to fetch Metaplex metadata for token ${token}:`, error)
601
+ }
602
+ }
603
+
604
+ return {
605
+ name,
606
+ symbol,
607
+ decimals: data.decimals,
608
+ }
609
+ } else {
610
+ throw new Error(`Unable to parse token data for ${token}`)
611
+ }
612
+ }
613
+
614
+ async _fetchTokenMetadata(
615
+ mintPublicKey: PublicKey,
616
+ ): Promise<{ name: string; symbol: string } | null> {
617
+ try {
618
+ // Token Metadata Program ID
619
+ const TOKEN_METADATA_PROGRAM_ID = new PublicKey('metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s')
620
+
621
+ // Derive metadata account address
622
+ const [metadataPDA] = PublicKey.findProgramAddressSync(
623
+ [Buffer.from('metadata'), TOKEN_METADATA_PROGRAM_ID.toBuffer(), mintPublicKey.toBuffer()],
624
+ TOKEN_METADATA_PROGRAM_ID,
625
+ )
626
+
627
+ // Fetch metadata account
628
+ const metadataAccount = await this.connection.getAccountInfo(metadataPDA)
629
+ if (!metadataAccount) {
630
+ return null
631
+ }
632
+
633
+ // Parse Metaplex Token Metadata according to the actual format
634
+ // Reference: https://docs.metaplex.com/programs/token-metadata/accounts#metadata
635
+ const data = metadataAccount.data
636
+ if (data.length < 100) {
637
+ return null
638
+ }
639
+
640
+ let offset = 0
641
+
642
+ // Skip key (1 byte) - discriminator for account type
643
+ offset += 1
644
+
645
+ // Skip update_authority (32 bytes)
646
+ offset += 32
647
+
648
+ // Skip mint (32 bytes)
649
+ offset += 32
650
+
651
+ // Parse name (variable length string)
652
+ if (offset + 4 > data.length) return null
653
+ const nameLength = data.readUInt32LE(offset)
654
+ offset += 4
655
+ if (nameLength > 200 || offset + nameLength > data.length) return null
656
+ const nameBytes = data.subarray(offset, offset + nameLength)
657
+ const name = nameBytes.toString('utf8').replace(/\0/g, '').trim()
658
+ offset += nameLength
659
+
660
+ // Parse symbol (variable length string)
661
+ if (offset + 4 > data.length) return null
662
+ const symbolLength = data.readUInt32LE(offset)
663
+ offset += 4
664
+ if (symbolLength > 50 || offset + symbolLength > data.length) return null
665
+
666
+ const symbolBytes = data.subarray(offset, offset + symbolLength)
667
+ const symbol = symbolBytes.toString('utf8').replace(/\0/g, '').trim()
668
+
669
+ return name || symbol ? { name, symbol } : null
670
+ } catch (error) {
671
+ console.debug('Error fetching token metadata:', error)
672
+ return null
673
+ }
674
+ }
675
+
676
+ static decodeMessage({ data }: { data: unknown }): CCIPMessage | undefined {
677
+ if (!data || typeof data !== 'string') return undefined
678
+ let eventDataBuffer
679
+ try {
680
+ eventDataBuffer = bytesToBuffer(data)
681
+ } catch (_) {
682
+ return
683
+ }
684
+
685
+ const disc = dataSlice(eventDataBuffer, 0, 8)
686
+ if (disc !== hexDiscriminator('CCIPMessageSent')) return
687
+
688
+ // Use module-level BorshCoder for decoding structs
689
+
690
+ // Manually parse event header (discriminator + event-level fields)
691
+ let offset = 8
692
+
693
+ // Parse event-level fields
694
+ const _destChainSelector = eventDataBuffer.readBigUInt64LE(offset)
695
+ offset += 8
696
+
697
+ const _sequenceNumber = eventDataBuffer.readBigUInt64LE(offset)
698
+ offset += 8
699
+
700
+ // Now decode the SVM2AnyRampMessage struct using BorshCoder
701
+ const messageBytes = eventDataBuffer.subarray(offset)
702
+
703
+ const message: IdlTypes<typeof CCIP_ROUTER_IDL>['SVM2AnyRampMessage'] =
704
+ routerCoder.types.decode('SVM2AnyRampMessage', messageBytes)
705
+
706
+ // Convert BN/number types to bigints
707
+ const sourceChainSelector = BigInt(message.header.sourceChainSelector.toString())
708
+ const destChainSelector = BigInt(message.header.destChainSelector.toString())
709
+ const sequenceNumber = BigInt(message.header.sequenceNumber.toString())
710
+ const nonce = BigInt(message.header.nonce.toString())
711
+ const destNetwork = networkInfo(destChainSelector)
712
+
713
+ // Convert message fields to expected format
714
+ const messageId = hexlify(new Uint8Array(message.header.messageId))
715
+ const sender = message.sender.toString()
716
+ const data_ = getDataBytes(message.data)
717
+ // TODO: extract this into a proper normalize/decode/reencode data utility
718
+ const msgData = destNetwork.family === ChainFamily.Solana ? encodeBase64(data_) : hexlify(data_)
719
+ const receiver = decodeAddress(message.receiver, destNetwork.family)
720
+ const feeToken = message.feeToken.toString()
721
+
722
+ // Process token amounts
723
+ const tokenAmounts = message.tokenAmounts.map((ta) => ({
724
+ sourcePoolAddress: ta.sourcePoolAddress.toBase58(),
725
+ destTokenAddress: decodeAddress(ta.destTokenAddress, destNetwork.family),
726
+ extraData: hexlify(ta.extraData),
727
+ amount: leToBigInt(ta.amount.leBytes),
728
+ destExecData: hexlify(ta.destExecData),
729
+ // destGasAmount is encoded as BE uint32;
730
+ destGasAmount: toBigInt(ta.destExecData),
731
+ }))
732
+
733
+ // Convert fee amounts from CrossChainAmount format
734
+ const feeTokenAmount = leToBigInt(message.feeTokenAmount.leBytes)
735
+ const feeValueJuels = leToBigInt(message.feeValueJuels.leBytes)
736
+
737
+ // Parse gas limit from extraArgs
738
+ const extraArgs = hexlify(message.extraArgs)
739
+ const parsed = this.decodeExtraArgs(extraArgs)
740
+ if (!parsed) throw new Error('Invalid extraArgs: ' + extraArgs)
741
+ const { _tag, ...rest } = parsed
742
+
743
+ return {
744
+ header: {
745
+ messageId,
746
+ sourceChainSelector,
747
+ destChainSelector: destChainSelector,
748
+ sequenceNumber: sequenceNumber,
749
+ nonce,
750
+ },
751
+ sender,
752
+ receiver,
753
+ data: msgData,
754
+ tokenAmounts,
755
+ feeToken,
756
+ feeTokenAmount,
757
+ feeValueJuels,
758
+ extraArgs,
759
+ ...rest,
760
+ } as CCIPMessage<typeof CCIPVersion.V1_6>
761
+ }
762
+
763
+ static decodeExtraArgs(
764
+ extraArgs: BytesLike,
765
+ ): (EVMExtraArgsV2 & { _tag: 'EVMExtraArgsV2' }) | undefined {
766
+ const data = getDataBytes(extraArgs),
767
+ tag = dataSlice(data, 0, 4)
768
+ switch (tag) {
769
+ case EVMExtraArgsV2Tag: {
770
+ if (dataLength(data) === 4 + 16 + 1) {
771
+ // Solana-generated EVMExtraArgsV2 (21 bytes total)
772
+ return {
773
+ _tag: 'EVMExtraArgsV2',
774
+ gasLimit: leToBigInt(dataSlice(data, 4, 4 + 16)), // from Uint128LE
775
+ allowOutOfOrderExecution: data[4 + 16] == 1,
776
+ }
777
+ }
778
+ throw new Error(`Unsupported EVMExtraArgsV2 length: ${dataLength(data)}`)
779
+ }
780
+ default:
781
+ return
782
+ }
783
+ }
784
+
785
+ static encodeExtraArgs(args: ExtraArgs): string {
786
+ if ('computeUnits' in args) throw new Error('Solana can only encode EVMExtraArgsV2')
787
+ const gasLimitUint128Le = toLeArray(args.gasLimit, 16)
788
+ return concat([
789
+ EVMExtraArgsV2Tag,
790
+ gasLimitUint128Le,
791
+ 'allowOutOfOrderExecution' in args && args.allowOutOfOrderExecution ? '0x01' : '0x00',
792
+ ])
793
+ }
794
+
795
+ static decodeCommits(
796
+ log: Pick<Log_, 'data'>,
797
+ lane?: Omit<Lane, 'destChainSelector'>,
798
+ ): CommitReport[] | undefined {
799
+ // Check if this is a CommitReportAccepted event by looking at the discriminant
800
+ if (!log.data || typeof log.data !== 'string') {
801
+ throw new Error('Log data is missing or not a string')
802
+ }
803
+
804
+ const eventDataBuffer = bytesToBuffer(log.data)
805
+
806
+ // Verify the discriminant matches CommitReportAccepted
807
+ const expectedDiscriminant = hexDiscriminator('CommitReportAccepted')
808
+ const actualDiscriminant = hexlify(eventDataBuffer.subarray(0, 8))
809
+ if (actualDiscriminant !== expectedDiscriminant) return
810
+
811
+ // Skip the 8-byte discriminant and decode the event data manually
812
+ let offset = 8
813
+
814
+ // Decode Option<MerkleRoot> - first byte indicates Some(1) or None(0)
815
+ const hasValue = eventDataBuffer.readUInt8(offset)
816
+ offset += 1
817
+ if (!hasValue) return []
818
+
819
+ // Decode MerkleRoot struct using the types decoder
820
+ // We need to read the remaining bytes as a MerkleRoot struct
821
+ const merkleRootBytes = eventDataBuffer.subarray(offset)
822
+
823
+ type MerkleRootData = {
824
+ sourceChainSelector: BN
825
+ onRampAddress: Buffer
826
+ minSeqNr: BN
827
+ maxSeqNr: BN
828
+ merkleRoot: number[]
829
+ }
830
+
831
+ const merkleRootData: MerkleRootData = offrampCoder.types.decode('MerkleRoot', merkleRootBytes)
832
+
833
+ if (!merkleRootData) {
834
+ throw new Error('Failed to decode MerkleRoot data')
835
+ }
836
+
837
+ // Verify the source chain selector matches our lane
838
+ const sourceChainSelector = BigInt(merkleRootData.sourceChainSelector.toString())
839
+
840
+ // Convert the onRampAddress from bytes to the proper format
841
+ const onRampAddress = decodeOnRampAddress(
842
+ merkleRootData.onRampAddress,
843
+ networkInfo(sourceChainSelector).family,
844
+ )
845
+ if (lane) {
846
+ if (sourceChainSelector !== lane.sourceChainSelector) return
847
+ // Verify the onRampAddress matches our lane
848
+ if (onRampAddress !== lane.onRamp) return
849
+ }
850
+
851
+ return [
852
+ {
853
+ sourceChainSelector,
854
+ onRampAddress,
855
+ minSeqNr: BigInt(merkleRootData.minSeqNr.toString()),
856
+ maxSeqNr: BigInt(merkleRootData.maxSeqNr.toString()),
857
+ merkleRoot: hexlify(new Uint8Array(merkleRootData.merkleRoot)),
858
+ },
859
+ ]
860
+ }
861
+
862
+ static decodeReceipt(log: Pick<Log_, 'data' | 'tx' | 'index'>): ExecutionReceipt | undefined {
863
+ // Check if this is a ExecutionStateChanged event by looking at the discriminant
864
+ if (!log.data || typeof log.data !== 'string') {
865
+ throw new Error('Log data is missing or not a string')
866
+ }
867
+
868
+ // Verify the discriminant matches ExecutionStateChanged
869
+ if (dataSlice(getDataBytes(log.data), 0, 8) !== hexDiscriminator('ExecutionStateChanged'))
870
+ return
871
+ const eventDataBuffer = bytesToBuffer(log.data)
872
+
873
+ // Note: We manually decode the event fields rather than using BorshCoder
874
+ // since ExecutionStateChanged is an event, not a defined type
875
+
876
+ // Skip the 8-byte discriminant and manually decode the event fields
877
+ let offset = 8
878
+
879
+ // Decode sourceChainSelector (u64)
880
+ const sourceChainSelector = eventDataBuffer.readBigUInt64LE(offset)
881
+ offset += 8
882
+
883
+ // Decode sequenceNumber (u64)
884
+ const sequenceNumber = eventDataBuffer.readBigUInt64LE(offset)
885
+ offset += 8
886
+
887
+ // Decode messageId ([u8; 32])
888
+ const messageId = hexlify(eventDataBuffer.subarray(offset, offset + 32))
889
+ offset += 32
890
+
891
+ // Decode messageHash ([u8; 32])
892
+ const messageHash = hexlify(eventDataBuffer.subarray(offset, offset + 32))
893
+ offset += 32
894
+
895
+ // Decode state enum (MessageExecutionState)
896
+ // Enum discriminant is a single byte: Untouched=0, InProgress=1, Success=2, Failure=3
897
+ let state = eventDataBuffer.readUInt8(offset) as ExecutionState
898
+ let returnData
899
+ if (log.tx) {
900
+ // use only last receipt per tx+message (i.e. skip intermediary InProgress=1 states for Solana)
901
+ const laterReceiptLog = log.tx.logs
902
+ .filter((l) => l.index > log.index)
903
+ .findLast((l) => {
904
+ const lastReceipt = this.decodeReceipt(l)
905
+ return lastReceipt && lastReceipt.messageId === messageId
906
+ })
907
+ if (laterReceiptLog) {
908
+ return // ignore intermediary state (InProgress=1) if we can find a later receipt
909
+ } else if (state !== ExecutionState.Success) {
910
+ returnData = getErrorFromLogs(log.tx.logs)
911
+ } else if (log.tx.error) {
912
+ returnData = util.inspect(log.tx.error)
913
+ state = ExecutionState.Failed
914
+ }
915
+ }
916
+
917
+ return {
918
+ sourceChainSelector,
919
+ sequenceNumber,
920
+ messageId,
921
+ messageHash,
922
+ state,
923
+ returnData,
924
+ }
925
+ }
926
+
927
+ static getAddress(bytes: BytesLike): string {
928
+ try {
929
+ if (typeof bytes === 'string' && bs58.decode(bytes).length === 32) return bytes
930
+ } catch (_) {
931
+ // pass
932
+ }
933
+ return encodeBase58(getDataBytes(bytes))
934
+ }
935
+
936
+ static getDestLeafHasher(lane: Lane): LeafHasher<typeof CCIPVersion.V1_6> {
937
+ return getV16SolanaLeafHasher(lane)
938
+ }
939
+
940
+ async getTokenAdminRegistryFor(address: string): Promise<string> {
941
+ const [type] = await this.typeAndVersion(address)
942
+ if (!type.includes('Router')) throw new Error(`Not a Router: ${address} is ${type}`)
943
+ // Solana implements TokenAdminRegistry in the Router/OnRamp program
944
+ return address
945
+ }
946
+
947
+ /**
948
+ * Get the fee required to send a CCIP message from the Solana router.
949
+ */
950
+ getFee(router: string, destChainSelector: bigint, message: AnyMessage): Promise<bigint> {
951
+ return getFee(this.connection, router, destChainSelector, message)
952
+ }
953
+
954
+ async sendMessage(
955
+ router_: string,
956
+ destChainSelector: bigint,
957
+ message: AnyMessage & { fee: bigint },
958
+ opts?: { wallet?: unknown; approveMax?: boolean },
959
+ ): Promise<ChainTransaction> {
960
+ const wallet = await this.getWallet(opts)
961
+
962
+ const router = new Program(
963
+ CCIP_ROUTER_IDL,
964
+ new PublicKey(router_),
965
+ new AnchorProvider(this.connection, wallet, { commitment: this.commitment }),
966
+ )
967
+ const { hash } = await ccipSend(router, destChainSelector, message, opts)
968
+ return this.getTransaction(hash)
969
+ }
970
+
971
+ async fetchOffchainTokenData(request: CCIPRequest): Promise<OffchainTokenData[]> {
972
+ return fetchSolanaOffchainTokenData(this.connection, request)
973
+ }
974
+
975
+ async executeReport(
976
+ offRamp: string,
977
+ execReport_: ExecutionReport,
978
+ opts?: {
979
+ wallet?: string
980
+ gasLimit?: number
981
+ forceLookupTable?: boolean
982
+ forceBuffer?: boolean
983
+ clearLeftoverAccounts?: boolean
984
+ dontWait?: boolean
985
+ },
986
+ ): Promise<ChainTransaction> {
987
+ if (!('computeUnits' in execReport_.message))
988
+ throw new Error("ExecutionReport's message not for Solana")
989
+ const execReport = execReport_ as ExecutionReport<CCIPMessage_V1_6_Solana>
990
+
991
+ const wallet = await this.getWallet(opts)
992
+ const provider = new AnchorProvider(this.connection, wallet, { commitment: this.commitment })
993
+ const offrampProgram = new Program(CCIP_OFFRAMP_IDL, new PublicKey(offRamp), provider)
994
+
995
+ const rep = await executeReport({ offrampProgram, execReport, ...opts })
996
+ if (opts?.clearLeftoverAccounts) {
997
+ try {
998
+ await this.cleanUpBuffers(opts)
999
+ } catch (err) {
1000
+ console.warn('Error while trying to clean up buffers:', err)
1001
+ }
1002
+ }
1003
+ return this.getTransaction(rep.hash)
1004
+ }
1005
+
1006
+ /**
1007
+ * Clean up and recycle buffers and address lookup tables owned by wallet
1008
+ * CAUTION: this will close ANY lookup table owned by this wallet
1009
+ * @param wallet - wallet options
1010
+ * @param dontWait - Whether to skip waiting for lookup table deactivation cool down period
1011
+ * (513 slots) to pass before closing; by default, we deactivate (if needed) and wait to close
1012
+ * before returning from this method
1013
+ */
1014
+ async cleanUpBuffers(opts?: { wallet?: string; dontWait?: boolean }): Promise<void> {
1015
+ const wallet = await this.getWallet(opts)
1016
+ const provider = new AnchorProvider(this.connection, wallet, { commitment: this.commitment })
1017
+ await cleanUpBuffers(provider, this.getLogs.bind(this), opts)
1018
+ }
1019
+
1020
+ static parse(data: unknown) {
1021
+ if (!data) return
1022
+ try {
1023
+ if (Array.isArray(data)) {
1024
+ if (data.every((e) => typeof e === 'string')) return getErrorFromLogs(data)
1025
+ else if (data.every((e) => typeof e === 'object' && 'data' in e && 'address' in e))
1026
+ return getErrorFromLogs(data as Log_[])
1027
+ } else if (typeof data === 'object') {
1028
+ if ('transactionLogs' in data && 'transactionMessage' in data) {
1029
+ const parsed = getErrorFromLogs(data.transactionLogs as Log_[] | string[])
1030
+ if (parsed) return { message: data.transactionMessage, ...parsed }
1031
+ }
1032
+ if ('logs' in data) return getErrorFromLogs(data.logs as Log_[] | string[])
1033
+ } else if (typeof data === 'string') {
1034
+ const parsedExtraArgs = this.decodeExtraArgs(getDataBytes(data))
1035
+ if (parsedExtraArgs) return parsedExtraArgs
1036
+ const parsedMessage = this.decodeMessage({ data })
1037
+ if (parsedMessage) return parsedMessage
1038
+ }
1039
+ } catch (_) {
1040
+ // Ignore errors during parsing
1041
+ }
1042
+ }
1043
+
1044
+ /**
1045
+ * Solana optimization: we use getProgramAccounts with
1046
+ */
1047
+ async fetchCommitReport(
1048
+ commitStore: string,
1049
+ request: {
1050
+ lane: Lane
1051
+ message: { header: { sequenceNumber: bigint } }
1052
+ timestamp?: number
1053
+ },
1054
+ hints?: { startBlock?: number; page?: number },
1055
+ ): Promise<CCIPCommit> {
1056
+ const commitsAroundSeqNum = await this.connection.getProgramAccounts(
1057
+ new PublicKey(commitStore),
1058
+ {
1059
+ filters: [
1060
+ {
1061
+ memcmp: {
1062
+ offset: 0,
1063
+ bytes: encodeBase58(BorshAccountsCoder.accountDiscriminator('CommitReport')),
1064
+ },
1065
+ },
1066
+ {
1067
+ memcmp: {
1068
+ offset: 8 + 1,
1069
+ bytes: encodeBase58(toLeArray(request.lane.sourceChainSelector, 8)),
1070
+ },
1071
+ },
1072
+ // dirty trick: memcmp report.min with msg.sequenceNumber's without least-significant byte;
1073
+ // this should be ~256 around seqNum, i.e. big chance of a match
1074
+ {
1075
+ memcmp: {
1076
+ offset: 8 + 1 + 8 + 32 + 8 + 1,
1077
+ bytes: encodeBase58(toLeArray(request.message.header.sequenceNumber, 8).slice(1)),
1078
+ },
1079
+ },
1080
+ ],
1081
+ },
1082
+ )
1083
+ for (const acc of commitsAroundSeqNum) {
1084
+ // const merkleRoot = acc.account.data.subarray(8 + 1 + 8, 8 + 1 + 8 + 32)
1085
+ const minSeqNr = acc.account.data.readBigUInt64LE(8 + 1 + 8 + 32 + 8)
1086
+ const maxSeqNr = acc.account.data.readBigUInt64LE(8 + 1 + 8 + 32 + 8 + 8)
1087
+ if (
1088
+ minSeqNr > request.message.header.sequenceNumber ||
1089
+ maxSeqNr < request.message.header.sequenceNumber
1090
+ )
1091
+ continue
1092
+ // we have all the commit report info, but we also need log details (txHash, etc)
1093
+ for await (const log of this.getLogs({
1094
+ startTime: 1, // just to force getting the oldest log first
1095
+ programs: [commitStore],
1096
+ address: acc.pubkey.toBase58(),
1097
+ topics: ['CommitReportAccepted'],
1098
+ })) {
1099
+ // first yielded log should be commit (which created this PDA)
1100
+ const report = (this.constructor as typeof SolanaChain).decodeCommits(
1101
+ log,
1102
+ request.lane,
1103
+ )?.[0]
1104
+ if (report) return { report, log }
1105
+ }
1106
+ }
1107
+ // in case we can't find it, fallback to generic iterating txs
1108
+ return super.fetchCommitReport(commitStore, request, hints)
1109
+ }
1110
+
1111
+ // specialized override with stricter address-of-interest
1112
+ async *fetchExecutionReceipts(
1113
+ offRamp: string,
1114
+ messageIds: Set<string>,
1115
+ hints?: { startBlock?: number; startTime?: number; page?: number; commit?: CommitReport },
1116
+ ): AsyncGenerator<CCIPExecution> {
1117
+ if (!hints?.commit) {
1118
+ // if no commit, fall back to generic implementation
1119
+ yield* super.fetchExecutionReceipts(offRamp, messageIds, hints)
1120
+ return
1121
+ }
1122
+ // otherwise, use `commit_report` PDA as more specialized address
1123
+ const [commitReportPda] = PublicKey.findProgramAddressSync(
1124
+ [
1125
+ Buffer.from('commit_report'),
1126
+ toLeArray(hints.commit.sourceChainSelector, 8),
1127
+ bytesToBuffer(hints.commit.merkleRoot),
1128
+ ],
1129
+ new PublicKey(offRamp),
1130
+ )
1131
+ // rest is similar to generic implemenetation
1132
+ const onlyLast = !hints?.startBlock && !hints?.startTime // backwards
1133
+ for await (const log of this.getLogs({
1134
+ ...hints,
1135
+ programs: [offRamp],
1136
+ address: commitReportPda.toBase58(),
1137
+ topics: ['ExecutionStateChanged'],
1138
+ })) {
1139
+ const receipt = (this.constructor as typeof SolanaChain).decodeReceipt(log)
1140
+ if (!receipt || !messageIds.has(receipt.messageId)) continue
1141
+ if (onlyLast || receipt.state === ExecutionState.Success) messageIds.delete(receipt.messageId)
1142
+
1143
+ const timestamp = await this.getBlockTimestamp(log.blockNumber)
1144
+ yield { receipt, log, timestamp }
1145
+ if (!messageIds.size) break
1146
+ }
1147
+ }
1148
+
1149
+ async getRegistryTokenConfig(
1150
+ registry: string,
1151
+ token: string,
1152
+ ): Promise<{
1153
+ administrator: string
1154
+ pendingAdministrator?: string
1155
+ tokenPool?: string
1156
+ }> {
1157
+ const registry_ = new PublicKey(registry)
1158
+ const tokenMint = new PublicKey(token)
1159
+
1160
+ const [tokenAdminRegistryAddr] = PublicKey.findProgramAddressSync(
1161
+ [Buffer.from('token_admin_registry'), tokenMint.toBuffer()],
1162
+ registry_,
1163
+ )
1164
+
1165
+ const tokenAdminRegistry = await this.connection.getAccountInfo(tokenAdminRegistryAddr)
1166
+ if (!tokenAdminRegistry)
1167
+ throw new Error(`Token ${token} is not configured in registry ${registry}`)
1168
+
1169
+ const config: {
1170
+ administrator: string
1171
+ pendingAdministrator?: string
1172
+ tokenPool?: string
1173
+ } = {
1174
+ administrator: encodeBase58(tokenAdminRegistry.data.subarray(9, 9 + 32)),
1175
+ }
1176
+ const pendingAdministrator = new PublicKey(tokenAdminRegistry.data.subarray(41, 41 + 32))
1177
+
1178
+ // Check if pendingAdministrator is set (not system program address)
1179
+ if (
1180
+ pendingAdministrator &&
1181
+ !pendingAdministrator.equals(SystemProgram.programId) &&
1182
+ !pendingAdministrator.equals(PublicKey.default)
1183
+ ) {
1184
+ config.pendingAdministrator = pendingAdministrator.toBase58()
1185
+ }
1186
+
1187
+ // Get token pool from lookup table if available
1188
+ try {
1189
+ const lookupTableAddr = new PublicKey(tokenAdminRegistry.data.subarray(73, 73 + 32))
1190
+ const lookupTable = await this.connection.getAddressLookupTable(lookupTableAddr)
1191
+ if (lookupTable?.value) {
1192
+ // tokenPool state PDA is at index [3]
1193
+ const tokenPoolAddress = lookupTable.value.state.addresses[3]
1194
+ if (tokenPoolAddress && !tokenPoolAddress.equals(PublicKey.default)) {
1195
+ config.tokenPool = tokenPoolAddress.toBase58()
1196
+ }
1197
+ }
1198
+ } catch (_err) {
1199
+ // Token pool may not be configured yet
1200
+ }
1201
+ return config
1202
+ }
1203
+
1204
+ async getTokenPoolConfigs(tokenPool: string): Promise<{
1205
+ token: string
1206
+ router: string
1207
+ tokenPoolProgram: string
1208
+ typeAndVersion?: string
1209
+ }> {
1210
+ // `tokenPool` is actually a State PDA in the tokenPoolProgram
1211
+ const tokenPoolState = await this.connection.getAccountInfo(new PublicKey(tokenPool))
1212
+ if (!tokenPoolState) throw new Error(`TokenPool State PDA not found at ${tokenPool}`)
1213
+
1214
+ const { config }: { config: { mint: PublicKey; router: PublicKey } } =
1215
+ tokenPoolCoder.accounts.decode('state', tokenPoolState.data)
1216
+ const tokenPoolProgram = tokenPoolState.owner.toBase58()
1217
+
1218
+ let typeAndVersion
1219
+ try {
1220
+ ;[, , typeAndVersion] = await this.typeAndVersion(tokenPoolProgram)
1221
+ } catch (_) {
1222
+ // TokenPool may not have a typeAndVersion
1223
+ }
1224
+
1225
+ return {
1226
+ token: config.mint.toBase58(),
1227
+ router: config.router.toBase58(),
1228
+ tokenPoolProgram,
1229
+ typeAndVersion,
1230
+ }
1231
+ }
1232
+
1233
+ async getTokenPoolRemotes(
1234
+ tokenPool: string,
1235
+ remoteChainSelector?: bigint,
1236
+ ): Promise<Record<string, TokenPoolRemote>> {
1237
+ // `tokenPool` is actually a State PDA in the tokenPoolProgram
1238
+ const tokenPoolState = await this.connection.getAccountInfo(new PublicKey(tokenPool))
1239
+ if (!tokenPoolState) throw new Error(`TokenPool State PDA not found at ${tokenPool}`)
1240
+
1241
+ const tokenPoolProgram = tokenPoolState.owner
1242
+
1243
+ const { config }: { config: { mint: PublicKey; router: PublicKey } } =
1244
+ tokenPoolCoder.accounts.decode('state', tokenPoolState.data)
1245
+
1246
+ // Get all supported chains by fetching ChainConfig PDAs
1247
+ // We need to scan for all ChainConfig accounts owned by this token pool program
1248
+ const remotes: Record<string, TokenPoolRemote> = {}
1249
+
1250
+ // Fetch all ChainConfig accounts for this token pool
1251
+ let selectors: { selector: bigint }[] = Object.values(SELECTORS)
1252
+ let accounts
1253
+ if (remoteChainSelector) {
1254
+ selectors = [{ selector: remoteChainSelector }]
1255
+ const [chainConfigAddr] = PublicKey.findProgramAddressSync(
1256
+ [
1257
+ Buffer.from('ccip_tokenpool_chainconfig'),
1258
+ toLeArray(remoteChainSelector, 8),
1259
+ config.mint.toBuffer(),
1260
+ ],
1261
+ tokenPoolProgram,
1262
+ )
1263
+ const chainConfigAcc = await this.connection.getAccountInfo(chainConfigAddr)
1264
+ if (!chainConfigAcc)
1265
+ throw new Error(
1266
+ `ChainConfig not found at ${chainConfigAddr.toBase58()} for tokenPool=${tokenPool} and remoteNetwork=${networkInfo(remoteChainSelector).name}`,
1267
+ )
1268
+ accounts = [
1269
+ {
1270
+ pubkey: chainConfigAddr,
1271
+ account: chainConfigAcc,
1272
+ },
1273
+ ]
1274
+ } else
1275
+ accounts = await this.connection.getProgramAccounts(tokenPoolProgram, {
1276
+ filters: [
1277
+ {
1278
+ memcmp: {
1279
+ offset: 0,
1280
+ bytes: encodeBase58(BorshAccountsCoder.accountDiscriminator('ChainConfig')),
1281
+ },
1282
+ },
1283
+ ],
1284
+ })
1285
+
1286
+ for (const acc of accounts) {
1287
+ try {
1288
+ let base: {
1289
+ remote: {
1290
+ poolAddresses: { address: Buffer }[]
1291
+ tokenAddress: { address: Buffer }
1292
+ decimals: number
1293
+ }
1294
+ inboundRateLimit: {
1295
+ tokens: BN
1296
+ lastUpdated: BN
1297
+ cfg: {
1298
+ enabled: boolean
1299
+ capacity: BN
1300
+ rate: BN
1301
+ }
1302
+ }
1303
+ outboundRateLimit: {
1304
+ tokens: BN
1305
+ lastUpdated: BN
1306
+ cfg: {
1307
+ enabled: boolean
1308
+ capacity: BN
1309
+ rate: BN
1310
+ }
1311
+ }
1312
+ }
1313
+ try {
1314
+ ;({ base } = tokenPoolCoder.accounts.decode('chainConfig', acc.account.data))
1315
+ } catch (_) {
1316
+ ;({ base } = cctpTokenPoolCoder.accounts.decode('chainConfig', acc.account.data))
1317
+ }
1318
+
1319
+ let remoteChainSelector
1320
+ // test all selectors, to find the correct seed
1321
+ for (const { selector } of Object.values(selectors)) {
1322
+ const [chainConfigAddr] = PublicKey.findProgramAddressSync(
1323
+ [
1324
+ Buffer.from('ccip_tokenpool_chainconfig'),
1325
+ toLeArray(selector, 8),
1326
+ config.mint.toBuffer(),
1327
+ ],
1328
+ tokenPoolProgram,
1329
+ )
1330
+ if (chainConfigAddr.equals(acc.pubkey)) {
1331
+ remoteChainSelector = selector
1332
+ break
1333
+ }
1334
+ }
1335
+ if (!remoteChainSelector) continue
1336
+
1337
+ const remoteNetwork = networkInfo(remoteChainSelector)
1338
+
1339
+ const remoteToken = decodeAddress(base.remote.tokenAddress.address, remoteNetwork.family)
1340
+
1341
+ const remotePools = base.remote.poolAddresses.map((pool) =>
1342
+ decodeAddress(pool.address, remoteNetwork.family),
1343
+ )
1344
+
1345
+ let inboundRateLimiterState: RateLimiterState = null
1346
+ if (base.inboundRateLimit.cfg.enabled) {
1347
+ inboundRateLimiterState = {
1348
+ tokens: BigInt(base.inboundRateLimit.tokens.toString()),
1349
+ capacity: BigInt(base.inboundRateLimit.cfg.capacity.toString()),
1350
+ rate: BigInt(base.inboundRateLimit.cfg.rate.toString()),
1351
+ }
1352
+ const cur =
1353
+ inboundRateLimiterState.tokens +
1354
+ inboundRateLimiterState.rate *
1355
+ BigInt(Math.floor(Date.now() / 1000) - base.inboundRateLimit.lastUpdated.toNumber())
1356
+ if (cur < inboundRateLimiterState.capacity) inboundRateLimiterState.tokens = cur
1357
+ else inboundRateLimiterState.tokens = inboundRateLimiterState.capacity
1358
+ }
1359
+
1360
+ let outboundRateLimiterState: RateLimiterState = null
1361
+ if (base.outboundRateLimit.cfg.enabled) {
1362
+ outboundRateLimiterState = {
1363
+ tokens: BigInt(base.outboundRateLimit.tokens.toString()),
1364
+ capacity: BigInt(base.outboundRateLimit.cfg.capacity.toString()),
1365
+ rate: BigInt(base.outboundRateLimit.cfg.rate.toString()),
1366
+ }
1367
+ const cur =
1368
+ outboundRateLimiterState.tokens +
1369
+ outboundRateLimiterState.rate *
1370
+ BigInt(Math.floor(Date.now() / 1000) - base.outboundRateLimit.lastUpdated.toNumber())
1371
+ if (cur < outboundRateLimiterState.capacity) outboundRateLimiterState.tokens = cur
1372
+ else outboundRateLimiterState.tokens = outboundRateLimiterState.capacity
1373
+ }
1374
+
1375
+ remotes[remoteNetwork.name] = {
1376
+ remoteToken,
1377
+ remotePools,
1378
+ inboundRateLimiterState,
1379
+ outboundRateLimiterState,
1380
+ }
1381
+ } catch (err) {
1382
+ console.warn('Failed to decode ChainConfig account:', err)
1383
+ }
1384
+ }
1385
+
1386
+ return remotes
1387
+ }
1388
+
1389
+ async getSupportedTokens(router: string): Promise<string[]> {
1390
+ // `mint` offset in TokenAdminRegistry account data; more robust against changes in layout
1391
+ const mintOffset = 8 + 1 + 32 + 32 + 32 + 16 * 2 // = 137
1392
+ const router_ = new PublicKey(router)
1393
+ const res = []
1394
+ for (const acc of await this.connection.getProgramAccounts(router_, {
1395
+ filters: [
1396
+ {
1397
+ memcmp: {
1398
+ offset: 0,
1399
+ bytes: encodeBase58(BorshAccountsCoder.accountDiscriminator('TokenAdminRegistry')),
1400
+ },
1401
+ },
1402
+ ],
1403
+ })) {
1404
+ if (!acc.account.data || acc.account.data.length < mintOffset + 32) continue
1405
+ const mint = new PublicKey(acc.account.data.subarray(mintOffset, mintOffset + 32))
1406
+ const [derivedPda] = PublicKey.findProgramAddressSync(
1407
+ [Buffer.from('token_admin_registry'), mint.toBuffer()],
1408
+ router_,
1409
+ )
1410
+ if (!acc.pubkey.equals(derivedPda)) continue
1411
+ res.push(mint.toBase58())
1412
+ }
1413
+ return res
1414
+ }
1415
+
1416
+ async listFeeTokens(router: string): Promise<Record<string, TokenInfo>> {
1417
+ const { feeQuoter } = await this._getRouterConfig(router)
1418
+ const tokenConfigs = await this.connection.getProgramAccounts(feeQuoter, {
1419
+ filters: [
1420
+ {
1421
+ memcmp: {
1422
+ offset: 0,
1423
+ bytes: encodeBase58(
1424
+ BorshAccountsCoder.accountDiscriminator('BillingTokenConfigWrapper'),
1425
+ ),
1426
+ },
1427
+ },
1428
+ ],
1429
+ })
1430
+ return Object.fromEntries(
1431
+ await Promise.all(
1432
+ tokenConfigs.map(async (acc) => {
1433
+ const token = new PublicKey(acc.account.data.subarray(10, 10 + 32))
1434
+ return [token.toBase58(), await this.getTokenInfo(token.toBase58())] as const
1435
+ }),
1436
+ ),
1437
+ )
1438
+ }
1439
+
1440
+ // cached
1441
+ async _getRouterConfig(router: string) {
1442
+ const program = new Program(CCIP_ROUTER_IDL, new PublicKey(router), {
1443
+ connection: this.connection,
1444
+ })
1445
+
1446
+ const [configPda] = PublicKey.findProgramAddressSync([Buffer.from('config')], program.programId)
1447
+
1448
+ // feeQuoter is present in router's config, and has a DestChainState account which is updated by
1449
+ // the offramps, so we can use it to narrow the search for the offramp
1450
+ return program.account.config.fetch(configPda)
1451
+ }
1452
+ }
1453
+
1454
+ supportedChains[ChainFamily.Solana] = SolanaChain