@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,209 @@
1
+ import { type BN, BorshCoder, EventParser } from '@coral-xyz/anchor'
2
+ import { type Connection, PublicKey } from '@solana/web3.js'
3
+ import { hexlify } from 'ethers'
4
+
5
+ import { getUsdcAttestation } from '../offchain.ts'
6
+ import type { CCIPMessage, CCIPRequest, OffchainTokenData } from '../types.ts'
7
+ import { networkInfo } from '../utils.ts'
8
+ import { IDL as BASE_TOKEN_POOL } from './idl/1.6.0/BASE_TOKEN_POOL.ts'
9
+ import { IDL as CCTP_TOKEN_POOL } from './idl/1.6.0/CCIP_CCTP_TOKEN_POOL.ts'
10
+ import { bytesToBuffer } from './utils.ts'
11
+
12
+ interface CcipCctpMessageSentEvent {
13
+ originalSender: PublicKey
14
+ remoteChainSelector: BN
15
+ msgTotalNonce: BN
16
+ eventAddress: PublicKey
17
+ sourceDomain: number
18
+ cctpNonce: BN
19
+ messageSentBytes: Uint8Array
20
+ }
21
+
22
+ interface CcipCctpMessageAndAttestation {
23
+ message: {
24
+ data: Uint8Array
25
+ }
26
+ attestation: Uint8Array
27
+ }
28
+ const cctpTokenPoolCoder = new BorshCoder({
29
+ ...CCTP_TOKEN_POOL,
30
+ types: [...BASE_TOKEN_POOL.types, ...CCTP_TOKEN_POOL.types],
31
+ events: BASE_TOKEN_POOL.events,
32
+ errors: [...BASE_TOKEN_POOL.errors, ...CCTP_TOKEN_POOL.errors],
33
+ })
34
+
35
+ /**
36
+ * Analyzes a Solana transaction to extract CcipCctpMessageSentEvent, fetch Circle attestation,
37
+ * and encode the data in the format required by the destination chain.
38
+ *
39
+ * @param request - CCIP request containing transaction data and chain routing info
40
+ * @returns Array of encoded offchain token data (only one supported for Solana right now)
41
+ *
42
+ * @throws Error if transaction hash is missing or CcipCctpMessageSentEvent parsing fails
43
+ *
44
+ * @example
45
+ * const tokenData = await fetchSolanaOffchainTokenData({
46
+ * lane: { sourceChainSelector: ..., destChainSelector: ... },
47
+ * message: { ... },
48
+ * log: { transactionHash: "3k81TLhJuhwB8fvurCwyMPHXR3k9Tmtqe2ZrUQ8e3rMxk9fWFJT2xVHGgKJg1785FkJcaiQkthY4m86JrESGPhMY" },
49
+ * tx: { logs: [...] }
50
+ * })
51
+ */
52
+ export async function fetchSolanaOffchainTokenData(
53
+ connection: Connection,
54
+ request: Pick<CCIPRequest, 'tx' | 'lane'> & {
55
+ message: CCIPMessage
56
+ log: Pick<CCIPRequest['log'], 'topics' | 'index' | 'transactionHash'>
57
+ },
58
+ ): Promise<OffchainTokenData[]> {
59
+ if (request.message.tokenAmounts === undefined || request.message.tokenAmounts.length === 0) {
60
+ return []
61
+ }
62
+
63
+ if (request.message.tokenAmounts.length > 1) {
64
+ throw new Error(
65
+ `Expected at most 1 token transfer, found ${request.message.tokenAmounts?.length}`,
66
+ )
67
+ }
68
+
69
+ const { isTestnet } = networkInfo(request.lane.sourceChainSelector)
70
+ const txSignature = request.log.transactionHash
71
+ if (!txSignature) {
72
+ throw new Error('Transaction hash not found for OffchainTokenData parsing')
73
+ }
74
+
75
+ // Parse Solana transaction to find CCTP event
76
+ const cctpEvents = await parseCcipCctpEvents(connection, txSignature)
77
+ const offchainTokenData: OffchainTokenData[] = request.message.tokenAmounts.map(() => undefined)
78
+
79
+ // If no CcipCctpMessageSentEvent found, return defaults so we don't block execution
80
+ if (cctpEvents.length === 0) {
81
+ console.debug('No events')
82
+ return offchainTokenData
83
+ }
84
+
85
+ // Currently, we only support ONE token per transfer
86
+ if (cctpEvents.length > 1) {
87
+ throw new Error(
88
+ `Expected only 1 CcipCctpMessageSentEvent, found ${cctpEvents.length} in transaction ${txSignature}.`,
89
+ )
90
+ }
91
+
92
+ // NOTE: assuming USDC token is the first (and only) token in the CCIP message, we will process the CCTP event.
93
+ // If later multi-token transfers support is added, we need to add more info in order to match each token with it's event and offchainTokenData.
94
+ const cctpEvent = cctpEvents[0]
95
+ if (cctpEvent) {
96
+ const message = hexlify(cctpEvent.messageSentBytes)
97
+ try {
98
+ // Extract message bytes to fetch circle's attestation and then encode offchainTokenData.
99
+ const attestation = await getUsdcAttestation(message, isTestnet)
100
+
101
+ offchainTokenData[0] = { _tag: 'usdc', message, attestation }
102
+ } catch (error) {
103
+ console.warn(
104
+ `❌ Solana CCTP: Failed to fetch attestation for ${txSignature}:`,
105
+ message,
106
+ error,
107
+ )
108
+ }
109
+ }
110
+
111
+ console.debug('Got Solana offchain token data', offchainTokenData)
112
+
113
+ return offchainTokenData
114
+ }
115
+
116
+ /**
117
+ * Parses CcipCctpMessageSentEvent from a Solana transaction by analyzing program logs
118
+ *
119
+ * @param txSignature - Solana transaction signature to analyze
120
+ * @param sourceChainSelector - Source chain selector to determine RPC endpoint
121
+ * @returns Array of parsed CcipCctpMessageSentEvent found in the transaction (only 1 supported though)
122
+ *
123
+ * @throws Error if transaction is not found or RPC fails
124
+ *
125
+ * @example
126
+ * const events = await parseSolanaCctpEvents(
127
+ * '3k81TLhJuhwB8fvurCwyMPHXR3k9Tmtqe2ZrUQ8e3rMxk9fWFJT2xVHGgKJg1785FkJcaiQkthY4m86JrESGPhMY',
128
+ * 16423721717087811551n // Solana Devnet
129
+ * )
130
+ */
131
+ async function parseCcipCctpEvents(
132
+ connection: Connection,
133
+ txSignature: string,
134
+ ): Promise<CcipCctpMessageSentEvent[]> {
135
+ // Fetch transaction details using Solana RPC
136
+ const tx = await connection.getTransaction(txSignature, {
137
+ commitment: 'finalized',
138
+ maxSupportedTransactionVersion: 0,
139
+ })
140
+ if (!tx || !tx.meta) {
141
+ throw new Error(`Transaction not found: ${txSignature}`)
142
+ }
143
+
144
+ if (!tx.meta.logMessages?.length) {
145
+ throw new Error(`Transaction has no logs: ${txSignature}`)
146
+ }
147
+
148
+ const cctpPoolAddress = getCctpPoolAddress(tx.meta.logMessages)
149
+ if (!cctpPoolAddress) {
150
+ return []
151
+ }
152
+
153
+ const eventParser = new EventParser(new PublicKey(cctpPoolAddress), cctpTokenPoolCoder)
154
+
155
+ const events: CcipCctpMessageSentEvent[] = Array.from(eventParser.parseLogs(tx.meta.logMessages))
156
+ .filter((event) => event.name === 'CcipCctpMessageSentEvent')
157
+ .map((event) => event.data as unknown as CcipCctpMessageSentEvent)
158
+ return events
159
+ }
160
+
161
+ function getCctpPoolAddress(logs: string[]): string | null {
162
+ // Example logs include lines like the following (though the indexes of the "invoke [1]" are unreliable):
163
+ // "Program <POOL ADDRESS HERE, THIS IS WHAT WE'RE LOOKING FOR> invoke [1]",
164
+ // "Program log: Instruction: LockOrBurnTokens",
165
+ const candidateIx = logs.indexOf('Program log: Instruction: LockOrBurnTokens')
166
+ if (candidateIx < 1) {
167
+ return null
168
+ }
169
+
170
+ const candidateAddress = logs[candidateIx - 1].split(' ')[1]
171
+
172
+ if (!candidateAddress.toLowerCase().startsWith('ccitp')) {
173
+ // The vanity address of the pool includes "ccitp" (case-insensitive) as a prefix
174
+ return null
175
+ }
176
+
177
+ // basic sanity check that we have the pool address: The pool returns a value, so the logs should show that
178
+ const sanityCheck = logs.find((log) => log.startsWith(`Program return: ${candidateAddress} `))
179
+ if (!sanityCheck) {
180
+ return null
181
+ }
182
+
183
+ return candidateAddress
184
+ }
185
+
186
+ /**
187
+ * Encodes CCTP message and attestation
188
+ *
189
+ * @param data - OffchainTokenData (_tag="usdc")
190
+ * @returns Encoded data - Borsh-encoded attestation for Solana
191
+ */
192
+ export function encodeSolanaOffchainTokenData(data: OffchainTokenData): string {
193
+ if (data?._tag === 'usdc') {
194
+ const messageBuffer = bytesToBuffer(data.message)
195
+ const attestationBuffer = bytesToBuffer(data.attestation)
196
+
197
+ // Solana destination: use Borsh encoding
198
+ const messageAndAttestation: CcipCctpMessageAndAttestation = {
199
+ message: {
200
+ data: messageBuffer, // u8 array
201
+ },
202
+ attestation: attestationBuffer, // u8 array
203
+ }
204
+
205
+ const encoded = cctpTokenPoolCoder.types.encode('MessageAndAttestation', messageAndAttestation)
206
+ return hexlify(encoded)
207
+ }
208
+ return '0x'
209
+ }
@@ -0,0 +1,67 @@
1
+ import { BorshInstructionCoder } from '@coral-xyz/anchor'
2
+ import { BorshTypesCoder } from '@coral-xyz/anchor/dist/cjs/coder/borsh/types.js'
3
+ import { sha256, toUtf8Bytes } from 'ethers'
4
+
5
+ import { snakeToCamel } from '../utils.ts'
6
+ import { camelToSnakeCase } from './utils.ts'
7
+
8
+ type Layout_<T = unknown> = { encode: (type: T, buffer: Buffer) => number }
9
+
10
+ // monkey patch some functions to ensure correct buffer allocation (usually, hardcoded 1000B)
11
+ Object.assign(BorshTypesCoder.prototype, {
12
+ encode: function <T>(this: BorshTypesCoder, name: string, type: T): Buffer {
13
+ const layout = (this as unknown as { typeLayouts: Map<string, Layout_> }).typeLayouts.get(name)
14
+ if (!layout) {
15
+ throw new Error(`Unknown type: ${name}`)
16
+ }
17
+ let buffer = Buffer.alloc(512)
18
+ let len
19
+ try {
20
+ len = layout.encode(type, buffer)
21
+ } catch (err) {
22
+ if (err instanceof RangeError) {
23
+ buffer = Buffer.alloc(32000)
24
+ len = layout.encode(type, buffer)
25
+ } else {
26
+ throw err
27
+ }
28
+ }
29
+
30
+ return buffer.subarray(0, len)
31
+ },
32
+ })
33
+
34
+ function sighash(nameSpace: string, ixName: string): Buffer {
35
+ const name = camelToSnakeCase(ixName)
36
+ const preimage = `${nameSpace}:${name}`
37
+ return Buffer.from(sha256(toUtf8Bytes(preimage)).slice(2, 18), 'hex')
38
+ }
39
+
40
+ Object.assign(BorshInstructionCoder.prototype, {
41
+ _encode: function (
42
+ this: BorshInstructionCoder,
43
+ nameSpace: string,
44
+ ixName: string,
45
+ ix: any, // eslint-disable-line @typescript-eslint/no-explicit-any
46
+ ): Buffer {
47
+ const methodName = snakeToCamel(ixName)
48
+ const layout = (this as unknown as { ixLayout: Map<string, Layout_> }).ixLayout.get(methodName)
49
+ if (!layout) {
50
+ throw new Error(`Unknown method: ${methodName}`)
51
+ }
52
+ let buffer = Buffer.alloc(512)
53
+ let len
54
+ try {
55
+ len = layout.encode(ix, buffer)
56
+ } catch (err) {
57
+ if (err instanceof RangeError) {
58
+ buffer = Buffer.alloc(32000)
59
+ len = layout.encode(ix, buffer)
60
+ } else {
61
+ throw err
62
+ }
63
+ }
64
+ const data = buffer.subarray(0, len)
65
+ return Buffer.concat([sighash(nameSpace, ixName), data])
66
+ },
67
+ })
@@ -0,0 +1,436 @@
1
+ import util from 'util'
2
+
3
+ import { type AnchorProvider, type IdlTypes, Program } from '@coral-xyz/anchor'
4
+ import {
5
+ NATIVE_MINT,
6
+ createApproveInstruction,
7
+ getAccount,
8
+ getAssociatedTokenAddressSync,
9
+ } from '@solana/spl-token'
10
+ import {
11
+ type AccountMeta,
12
+ type AddressLookupTableAccount,
13
+ type Connection,
14
+ type TransactionInstruction,
15
+ ComputeBudgetProgram,
16
+ PublicKey,
17
+ TransactionMessage,
18
+ VersionedTransaction,
19
+ } from '@solana/web3.js'
20
+ import BN from 'bn.js'
21
+ import { zeroPadValue } from 'ethers'
22
+
23
+ import { SolanaChain } from './index.ts'
24
+ import type { AnyMessage } from '../types.ts'
25
+ import { toLeArray } from '../utils.ts'
26
+ import { IDL as CCIP_ROUTER_IDL } from './idl/1.6.0/CCIP_ROUTER.ts'
27
+ import { bytesToBuffer, simulateTransaction, simulationProvider } from './utils.ts'
28
+
29
+ function anyToSvmMessage(message: AnyMessage): IdlTypes<typeof CCIP_ROUTER_IDL>['SVM2AnyMessage'] {
30
+ const feeTokenPubkey = message.feeToken ? new PublicKey(message.feeToken) : PublicKey.default
31
+
32
+ const svmMessage: IdlTypes<typeof CCIP_ROUTER_IDL>['SVM2AnyMessage'] = {
33
+ receiver: bytesToBuffer(zeroPadValue(message.receiver, 32)),
34
+ data: bytesToBuffer(message.data || '0x'),
35
+ tokenAmounts: (message.tokenAmounts || []).map((ta) => {
36
+ if (!ta.token || ta.amount < 0n) {
37
+ throw new Error('Invalid token amount: token address and positive amount required')
38
+ }
39
+ return {
40
+ token: new PublicKey(ta.token),
41
+ amount: new BN(ta.amount),
42
+ }
43
+ }),
44
+ feeToken: feeTokenPubkey,
45
+ extraArgs: bytesToBuffer(SolanaChain.encodeExtraArgs(message.extraArgs)),
46
+ }
47
+
48
+ return svmMessage
49
+ }
50
+
51
+ export async function getFee(
52
+ connection: Connection,
53
+ router: string,
54
+ destChainSelector: bigint,
55
+ message: AnyMessage,
56
+ ): Promise<bigint> {
57
+ const program = new Program(
58
+ CCIP_ROUTER_IDL,
59
+ new PublicKey(router),
60
+ simulationProvider(connection),
61
+ )
62
+
63
+ // Get router config to find feeQuoter
64
+ const [configPda] = PublicKey.findProgramAddressSync([Buffer.from('config')], program.programId)
65
+ const configAccount = await connection.getAccountInfo(configPda)
66
+ if (!configAccount) throw new Error(`Router config not found at ${configPda.toBase58()}`)
67
+
68
+ const { feeQuoter, linkTokenMint }: { feeQuoter: PublicKey; linkTokenMint: PublicKey } =
69
+ program.coder.accounts.decode('config', configAccount.data)
70
+
71
+ // Derive fee-related PDAs
72
+ const [destChainStatePda] = PublicKey.findProgramAddressSync(
73
+ [Buffer.from('dest_chain_state'), toLeArray(destChainSelector, 8)],
74
+ program.programId,
75
+ )
76
+
77
+ const [feeQuoterConfigPda] = PublicKey.findProgramAddressSync([Buffer.from('config')], feeQuoter)
78
+
79
+ const [feeQuoterDestChainPda] = PublicKey.findProgramAddressSync(
80
+ [Buffer.from('dest_chain'), toLeArray(destChainSelector, 8)],
81
+ feeQuoter,
82
+ )
83
+
84
+ if (
85
+ message.feeToken &&
86
+ message.feeToken !== PublicKey.default.toBase58() &&
87
+ message.feeToken !== linkTokenMint.toBase58()
88
+ ) {
89
+ console.warn('feeToken is not default nor link =', linkTokenMint.toBase58())
90
+ }
91
+
92
+ // Convert feeToken to PublicKey (default to native SOL if not specified)
93
+ const feeTokenPubkey =
94
+ message.feeToken && message.feeToken !== PublicKey.default.toBase58()
95
+ ? new PublicKey(message.feeToken)
96
+ : NATIVE_MINT
97
+
98
+ const [feeQuoterBillingTokenConfigPda] = PublicKey.findProgramAddressSync(
99
+ [Buffer.from('fee_billing_token_config'), feeTokenPubkey.toBuffer()],
100
+ feeQuoter,
101
+ )
102
+
103
+ // LINK token config (assuming default LINK token for now)
104
+ const [feeQuoterLinkTokenConfigPda] = PublicKey.findProgramAddressSync(
105
+ [Buffer.from('fee_billing_token_config'), linkTokenMint.toBuffer()],
106
+ feeQuoter,
107
+ )
108
+
109
+ // Convert AnyMessage to SVM2AnyMessage format
110
+ const svmMessage = anyToSvmMessage(message)
111
+
112
+ // 2 feeQuoter PDAs per token
113
+ const remainingAccounts = svmMessage.tokenAmounts
114
+ .map((ta) => [
115
+ PublicKey.findProgramAddressSync(
116
+ [Buffer.from('fee_billing_token_config'), ta.token.toBuffer()],
117
+ feeQuoter,
118
+ )[0],
119
+ PublicKey.findProgramAddressSync(
120
+ [
121
+ Buffer.from('per_chain_per_token_config'),
122
+ toLeArray(destChainSelector, 8),
123
+ ta.token.toBuffer(),
124
+ ],
125
+ feeQuoter,
126
+ )[0],
127
+ ])
128
+ .flat()
129
+ .map((pubkey) => ({ pubkey, isWritable: false, isSigner: false }))
130
+
131
+ // Call getFee method
132
+ const result: unknown = await program.methods
133
+ .getFee(new BN(destChainSelector), svmMessage)
134
+ .accounts({
135
+ config: configPda,
136
+ destChainState: destChainStatePda,
137
+ feeQuoter: feeQuoter,
138
+ feeQuoterConfig: feeQuoterConfigPda,
139
+ feeQuoterDestChain: feeQuoterDestChainPda,
140
+ feeQuoterBillingTokenConfig: feeQuoterBillingTokenConfigPda,
141
+ feeQuoterLinkTokenConfig: feeQuoterLinkTokenConfigPda,
142
+ })
143
+ .remainingAccounts(remainingAccounts)
144
+ .view()
145
+
146
+ if (!(result as { amount?: BN })?.amount) {
147
+ throw new Error(`Invalid fee result from router: ${util.inspect(result)}`)
148
+ }
149
+
150
+ return BigInt((result as { amount: BN }).amount.toString())
151
+ }
152
+
153
+ async function deriveAccountsCcipSend({
154
+ router,
155
+ destChainSelector,
156
+ message,
157
+ sender,
158
+ }: {
159
+ router: Program<typeof CCIP_ROUTER_IDL>
160
+ destChainSelector: bigint
161
+ message: IdlTypes<typeof CCIP_ROUTER_IDL>['SVM2AnyMessage']
162
+ sender: PublicKey
163
+ }) {
164
+ const connection = router.provider.connection
165
+ const derivedAccounts: AccountMeta[] = []
166
+ const addressLookupTableAccounts: AddressLookupTableAccount[] = []
167
+ const tokenIndices: number[] = []
168
+ let askWith: AccountMeta[] = []
169
+ let stage = 'Start'
170
+ let tokenIndex = 0
171
+
172
+ const [configPDA] = PublicKey.findProgramAddressSync([Buffer.from('config')], router.programId)
173
+
174
+ // read-only copy of router which avoids signing every simulation
175
+ const roProgram = new Program(
176
+ router.idl,
177
+ router.programId,
178
+ simulationProvider(connection, sender),
179
+ )
180
+ do {
181
+ // Create the transaction instruction for the deriveAccountsCcipSend method
182
+ const response = (await roProgram.methods
183
+ .deriveAccountsCcipSend(
184
+ {
185
+ destChainSelector: new BN(destChainSelector.toString()),
186
+ ccipSendCaller: sender,
187
+ message,
188
+ },
189
+ stage,
190
+ )
191
+ .accounts({
192
+ config: configPDA,
193
+ })
194
+ .remainingAccounts(askWith)
195
+ .view()) as IdlTypes<typeof CCIP_ROUTER_IDL>['DeriveAccountsResponse']
196
+
197
+ // Check if it is the start of a token transfer
198
+ const isStartOfToken = /^TokenTransferStaticAccounts\/\d+\/0$/.test(response.currentStage)
199
+ if (isStartOfToken) {
200
+ // From CCIP_ROUTER IDL, ccipSend has 18 static accounts before remaining_accounts
201
+ const numStaticCcipSendAccounts = 18
202
+ tokenIndices.push(tokenIndex - numStaticCcipSendAccounts)
203
+ }
204
+
205
+ // Update token index
206
+ tokenIndex += response.accountsToSave.length
207
+
208
+ // Collect the derived accounts
209
+ for (const meta of response.accountsToSave) {
210
+ derivedAccounts.push({
211
+ pubkey: meta.pubkey,
212
+ isWritable: meta.isWritable,
213
+ isSigner: meta.isSigner,
214
+ })
215
+ }
216
+
217
+ // Prepare askWith for next iteration
218
+ askWith = response.askAgainWith.map(
219
+ (meta: IdlTypes<typeof CCIP_ROUTER_IDL>['CcipAccountMeta']) => ({
220
+ pubkey: meta.pubkey,
221
+ isWritable: meta.isWritable,
222
+ isSigner: meta.isSigner,
223
+ }),
224
+ )
225
+
226
+ const lookupTableAccounts = await Promise.all(
227
+ response.lookUpTablesToSave.map(async (table) => {
228
+ const lookupTableAccountInfo = await connection.getAddressLookupTable(table)
229
+
230
+ if (!lookupTableAccountInfo.value) {
231
+ throw new Error(`Lookup table account not found: ${table.toBase58()}`)
232
+ }
233
+
234
+ return lookupTableAccountInfo.value
235
+ }),
236
+ )
237
+
238
+ // Collect lookup tables
239
+ addressLookupTableAccounts.push(...lookupTableAccounts)
240
+
241
+ stage = response.nextStage
242
+ } while (stage?.length)
243
+
244
+ return {
245
+ accounts: derivedAccounts,
246
+ addressLookupTableAccounts,
247
+ tokenIndexes: Buffer.from(tokenIndices),
248
+ }
249
+ }
250
+
251
+ export async function simulateAndSendTxs(
252
+ connection: Connection,
253
+ feePayer: AnchorProvider['wallet'],
254
+ instructions: TransactionInstruction[],
255
+ addressLookupTableAccounts?: AddressLookupTableAccount[],
256
+ ) {
257
+ let computeUnitLimit
258
+ const simulated =
259
+ (
260
+ await simulateTransaction({
261
+ connection,
262
+ payerKey: feePayer.publicKey,
263
+ instructions,
264
+ addressLookupTableAccounts,
265
+ })
266
+ ).unitsConsumed || 0
267
+ if (simulated > 200000) computeUnitLimit = Math.ceil(simulated * 1.1)
268
+
269
+ const txMsg = new TransactionMessage({
270
+ payerKey: feePayer.publicKey,
271
+ recentBlockhash: (await connection.getLatestBlockhash('confirmed')).blockhash,
272
+ instructions: [
273
+ ...(computeUnitLimit
274
+ ? [ComputeBudgetProgram.setComputeUnitLimit({ units: computeUnitLimit })]
275
+ : []),
276
+ ...instructions,
277
+ ],
278
+ })
279
+ const messageV0 = txMsg.compileToV0Message(addressLookupTableAccounts)
280
+ const tx = new VersionedTransaction(messageV0)
281
+
282
+ const signed = await feePayer.signTransaction(tx)
283
+ let hash
284
+ for (let attempt = 0; ; attempt++) {
285
+ try {
286
+ hash = await connection.sendTransaction(signed)
287
+ await connection.confirmTransaction(hash, 'confirmed')
288
+ return hash
289
+ } catch (error) {
290
+ if (attempt >= 3) throw error
291
+ console.error(`sendTransaction failed attempt=${attempt + 1}/3:`, error)
292
+ }
293
+ }
294
+ }
295
+
296
+ export async function ccipSend(
297
+ router: Program<typeof CCIP_ROUTER_IDL>,
298
+ destChainSelector: bigint,
299
+ message: AnyMessage & { fee: bigint },
300
+ opts?: { approveMax?: boolean },
301
+ ) {
302
+ const connection = router.provider.connection
303
+ let wallet
304
+ if (!(wallet = (router.provider as AnchorProvider).wallet)) {
305
+ throw new Error('ccipSend called without signer wallet')
306
+ }
307
+
308
+ const amountsToApprove = (message.tokenAmounts ?? []).reduce(
309
+ (acc, { token, amount }) => ({ ...acc, [token]: (acc[token] ?? 0n) + amount }),
310
+ {} as Record<string, bigint>,
311
+ )
312
+ if (message.feeToken && message.feeToken !== PublicKey.default.toBase58()) {
313
+ amountsToApprove[message.feeToken] = (amountsToApprove[message.feeToken] ?? 0n) + message.fee
314
+ }
315
+
316
+ const approveIxs = []
317
+ for (const [token, amount] of Object.entries(amountsToApprove)) {
318
+ const approveIx = await approveRouterSpender(
319
+ connection,
320
+ wallet.publicKey,
321
+ new PublicKey(token),
322
+ router.programId,
323
+ opts?.approveMax ? undefined : amount,
324
+ )
325
+ if (approveIx) approveIxs.push(approveIx)
326
+ }
327
+
328
+ const svmMessage = anyToSvmMessage(message)
329
+ const { addressLookupTableAccounts, accounts, tokenIndexes } = await deriveAccountsCcipSend({
330
+ router,
331
+ destChainSelector,
332
+ sender: wallet.publicKey,
333
+ message: svmMessage,
334
+ })
335
+
336
+ const sendIx = await router.methods
337
+ .ccipSend(new BN(destChainSelector), svmMessage, tokenIndexes)
338
+ .accountsStrict({
339
+ config: accounts[0].pubkey,
340
+ destChainState: accounts[1].pubkey,
341
+ nonce: accounts[2].pubkey,
342
+ authority: accounts[3].pubkey,
343
+ systemProgram: accounts[4].pubkey,
344
+ feeTokenProgram: accounts[5].pubkey,
345
+ feeTokenMint: accounts[6].pubkey,
346
+ feeTokenUserAssociatedAccount: accounts[7].pubkey,
347
+ feeTokenReceiver: accounts[8].pubkey,
348
+ feeBillingSigner: accounts[9].pubkey,
349
+ feeQuoter: accounts[10].pubkey,
350
+ feeQuoterConfig: accounts[11].pubkey,
351
+ feeQuoterDestChain: accounts[12].pubkey,
352
+ feeQuoterBillingTokenConfig: accounts[13].pubkey,
353
+ feeQuoterLinkTokenConfig: accounts[14].pubkey,
354
+ rmnRemote: accounts[15].pubkey,
355
+ rmnRemoteCurses: accounts[16].pubkey,
356
+ rmnRemoteConfig: accounts[17].pubkey,
357
+ })
358
+ .remainingAccounts(accounts.slice(18))
359
+ .instruction()
360
+
361
+ let hash
362
+ try {
363
+ // first try to serialize and send a single tx containing approve and send ixs
364
+ hash = await simulateAndSendTxs(
365
+ connection,
366
+ wallet,
367
+ [...approveIxs, sendIx],
368
+ addressLookupTableAccounts,
369
+ )
370
+ } catch (err) {
371
+ if (
372
+ !approveIxs.length ||
373
+ !(err instanceof Error) ||
374
+ !['encoding overruns Uint8Array', 'too large'].some((e) => err.message.includes(e))
375
+ )
376
+ throw err
377
+ // if serialization fails, send approve txs separately
378
+ for (const approveIx of approveIxs) await simulateAndSendTxs(connection, wallet, [approveIx])
379
+ hash = await simulateAndSendTxs(connection, wallet, [sendIx], addressLookupTableAccounts)
380
+ }
381
+
382
+ return { hash }
383
+ }
384
+
385
+ async function approveRouterSpender(
386
+ connection: Connection,
387
+ owner: PublicKey,
388
+ token: PublicKey,
389
+ router: PublicKey,
390
+ amount?: bigint,
391
+ ): Promise<TransactionInstruction | undefined> {
392
+ // Get the current account info to check existing delegation (or create if needed)
393
+ const mintInfo = await connection.getAccountInfo(token)
394
+ if (!mintInfo) throw new Error(`Mint ${token.toBase58()} not found`)
395
+ const associatedTokenAccount = getAssociatedTokenAddressSync(
396
+ token,
397
+ owner,
398
+ undefined,
399
+ mintInfo.owner,
400
+ )
401
+ const accountInfo = await getAccount(
402
+ connection,
403
+ associatedTokenAccount,
404
+ undefined,
405
+ mintInfo.owner,
406
+ )
407
+
408
+ // spender is a Router PDA
409
+ const [spender] = PublicKey.findProgramAddressSync([Buffer.from('fee_billing_signer')], router)
410
+
411
+ // Check if we need to approve
412
+ const needsApproval =
413
+ !accountInfo.delegate ||
414
+ !accountInfo.delegate.equals(spender) ||
415
+ (amount != null && accountInfo.delegatedAmount < amount)
416
+
417
+ if (!needsApproval) return
418
+ // Approve the spender to use tokens from the user's account
419
+ const approveIx = createApproveInstruction(
420
+ accountInfo.address,
421
+ spender,
422
+ owner,
423
+ amount ?? BigInt(Number.MAX_SAFE_INTEGER),
424
+ undefined,
425
+ mintInfo.owner,
426
+ )
427
+ console.info(
428
+ 'Approving',
429
+ amount ?? BigInt(Number.MAX_SAFE_INTEGER),
430
+ 'of',
431
+ token.toBase58(),
432
+ 'tokens to router',
433
+ router.toBase58(),
434
+ )
435
+ return approveIx
436
+ }