@chainlink/ccip-sdk 0.91.0 → 0.92.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 (273) hide show
  1. package/README.md +127 -80
  2. package/dist/aptos/hasher.d.ts.map +1 -1
  3. package/dist/aptos/hasher.js +7 -6
  4. package/dist/aptos/hasher.js.map +1 -1
  5. package/dist/aptos/index.d.ts +7 -2
  6. package/dist/aptos/index.d.ts.map +1 -1
  7. package/dist/aptos/index.js +29 -20
  8. package/dist/aptos/index.js.map +1 -1
  9. package/dist/aptos/logs.d.ts +5 -3
  10. package/dist/aptos/logs.d.ts.map +1 -1
  11. package/dist/aptos/logs.js +64 -27
  12. package/dist/aptos/logs.js.map +1 -1
  13. package/dist/aptos/token.d.ts.map +1 -1
  14. package/dist/aptos/token.js +2 -1
  15. package/dist/aptos/token.js.map +1 -1
  16. package/dist/aptos/types.js +6 -6
  17. package/dist/aptos/types.js.map +1 -1
  18. package/dist/chain.d.ts +36 -11
  19. package/dist/chain.d.ts.map +1 -1
  20. package/dist/chain.js +34 -2
  21. package/dist/chain.js.map +1 -1
  22. package/dist/commits.d.ts +2 -3
  23. package/dist/commits.d.ts.map +1 -1
  24. package/dist/commits.js +19 -8
  25. package/dist/commits.js.map +1 -1
  26. package/dist/errors/CCIPError.d.ts +48 -0
  27. package/dist/errors/CCIPError.d.ts.map +1 -0
  28. package/dist/errors/CCIPError.js +65 -0
  29. package/dist/errors/CCIPError.js.map +1 -0
  30. package/dist/errors/codes.d.ts +120 -0
  31. package/dist/errors/codes.d.ts.map +1 -0
  32. package/dist/errors/codes.js +156 -0
  33. package/dist/errors/codes.js.map +1 -0
  34. package/dist/errors/index.d.ts +26 -0
  35. package/dist/errors/index.d.ts.map +1 -0
  36. package/dist/errors/index.js +51 -0
  37. package/dist/errors/index.js.map +1 -0
  38. package/dist/errors/recovery.d.ts +6 -0
  39. package/dist/errors/recovery.d.ts.map +1 -0
  40. package/dist/errors/recovery.js +118 -0
  41. package/dist/errors/recovery.js.map +1 -0
  42. package/dist/errors/specialized.d.ts +637 -0
  43. package/dist/errors/specialized.d.ts.map +1 -0
  44. package/dist/errors/specialized.js +1298 -0
  45. package/dist/errors/specialized.js.map +1 -0
  46. package/dist/errors/utils.d.ts +11 -0
  47. package/dist/errors/utils.d.ts.map +1 -0
  48. package/dist/errors/utils.js +61 -0
  49. package/dist/errors/utils.js.map +1 -0
  50. package/dist/evm/abi/CommitStore_1_5.js +1 -1
  51. package/dist/evm/abi/LockReleaseTokenPool_1_5.js +1 -1
  52. package/dist/evm/abi/OffRamp_1_5.js +1 -1
  53. package/dist/evm/abi/OnRamp_1_5.js +1 -1
  54. package/dist/evm/abi/PriceRegistry_1_2.d.ts +443 -0
  55. package/dist/evm/abi/PriceRegistry_1_2.d.ts.map +1 -0
  56. package/dist/evm/abi/PriceRegistry_1_2.js +439 -0
  57. package/dist/evm/abi/PriceRegistry_1_2.js.map +1 -0
  58. package/dist/evm/const.d.ts +1 -0
  59. package/dist/evm/const.d.ts.map +1 -1
  60. package/dist/evm/const.js +2 -0
  61. package/dist/evm/const.js.map +1 -1
  62. package/dist/evm/hasher.d.ts.map +1 -1
  63. package/dist/evm/hasher.js +7 -6
  64. package/dist/evm/hasher.js.map +1 -1
  65. package/dist/evm/index.d.ts +9 -13
  66. package/dist/evm/index.d.ts.map +1 -1
  67. package/dist/evm/index.js +85 -68
  68. package/dist/evm/index.js.map +1 -1
  69. package/dist/evm/logs.d.ts.map +1 -1
  70. package/dist/evm/logs.js +47 -16
  71. package/dist/evm/logs.js.map +1 -1
  72. package/dist/evm/messages.d.ts +7 -6
  73. package/dist/evm/messages.d.ts.map +1 -1
  74. package/dist/evm/offchain.js +1 -1
  75. package/dist/evm/offchain.js.map +1 -1
  76. package/dist/evm/types.d.ts +10 -0
  77. package/dist/evm/types.d.ts.map +1 -0
  78. package/dist/evm/types.js +2 -0
  79. package/dist/evm/types.js.map +1 -0
  80. package/dist/execution.d.ts.map +1 -1
  81. package/dist/execution.js +9 -5
  82. package/dist/execution.js.map +1 -1
  83. package/dist/extra-args.d.ts.map +1 -1
  84. package/dist/extra-args.js +4 -3
  85. package/dist/extra-args.js.map +1 -1
  86. package/dist/gas.d.ts.map +1 -1
  87. package/dist/gas.js +3 -2
  88. package/dist/gas.js.map +1 -1
  89. package/dist/hasher/hasher.d.ts.map +1 -1
  90. package/dist/hasher/hasher.js +2 -1
  91. package/dist/hasher/hasher.js.map +1 -1
  92. package/dist/hasher/merklemulti.d.ts.map +1 -1
  93. package/dist/hasher/merklemulti.js +9 -8
  94. package/dist/hasher/merklemulti.js.map +1 -1
  95. package/dist/index.d.ts +5 -2
  96. package/dist/index.d.ts.map +1 -1
  97. package/dist/index.js +6 -2
  98. package/dist/index.js.map +1 -1
  99. package/dist/offchain.d.ts.map +1 -1
  100. package/dist/offchain.js +5 -8
  101. package/dist/offchain.js.map +1 -1
  102. package/dist/requests.d.ts +1 -1
  103. package/dist/requests.d.ts.map +1 -1
  104. package/dist/requests.js +37 -43
  105. package/dist/requests.js.map +1 -1
  106. package/dist/selectors.d.ts.map +1 -1
  107. package/dist/selectors.js +22 -0
  108. package/dist/selectors.js.map +1 -1
  109. package/dist/solana/cleanup.d.ts +2 -2
  110. package/dist/solana/cleanup.d.ts.map +1 -1
  111. package/dist/solana/cleanup.js +2 -3
  112. package/dist/solana/cleanup.js.map +1 -1
  113. package/dist/solana/exec.d.ts.map +1 -1
  114. package/dist/solana/exec.js +12 -12
  115. package/dist/solana/exec.js.map +1 -1
  116. package/dist/solana/hasher.d.ts.map +1 -1
  117. package/dist/solana/hasher.js +6 -5
  118. package/dist/solana/hasher.js.map +1 -1
  119. package/dist/solana/index.d.ts +30 -13
  120. package/dist/solana/index.d.ts.map +1 -1
  121. package/dist/solana/index.js +96 -143
  122. package/dist/solana/index.js.map +1 -1
  123. package/dist/solana/logs.d.ts +15 -0
  124. package/dist/solana/logs.d.ts.map +1 -0
  125. package/dist/solana/logs.js +106 -0
  126. package/dist/solana/logs.js.map +1 -0
  127. package/dist/solana/offchain.d.ts.map +1 -1
  128. package/dist/solana/offchain.js +6 -5
  129. package/dist/solana/offchain.js.map +1 -1
  130. package/dist/solana/patchBorsh.d.ts.map +1 -1
  131. package/dist/solana/patchBorsh.js +3 -2
  132. package/dist/solana/patchBorsh.js.map +1 -1
  133. package/dist/solana/send.d.ts.map +1 -1
  134. package/dist/solana/send.js +8 -7
  135. package/dist/solana/send.js.map +1 -1
  136. package/dist/solana/utils.d.ts +7 -8
  137. package/dist/solana/utils.d.ts.map +1 -1
  138. package/dist/solana/utils.js +23 -11
  139. package/dist/solana/utils.js.map +1 -1
  140. package/dist/sui/discovery.d.ts +18 -0
  141. package/dist/sui/discovery.d.ts.map +1 -0
  142. package/dist/sui/discovery.js +116 -0
  143. package/dist/sui/discovery.js.map +1 -0
  144. package/dist/sui/events.d.ts +36 -0
  145. package/dist/sui/events.d.ts.map +1 -0
  146. package/dist/sui/events.js +179 -0
  147. package/dist/sui/events.js.map +1 -0
  148. package/dist/sui/hasher.d.ts.map +1 -1
  149. package/dist/sui/hasher.js +6 -5
  150. package/dist/sui/hasher.js.map +1 -1
  151. package/dist/sui/index.d.ts +69 -41
  152. package/dist/sui/index.d.ts.map +1 -1
  153. package/dist/sui/index.js +402 -65
  154. package/dist/sui/index.js.map +1 -1
  155. package/dist/sui/manuallyExec/encoder.d.ts +8 -0
  156. package/dist/sui/manuallyExec/encoder.d.ts.map +1 -0
  157. package/dist/sui/manuallyExec/encoder.js +76 -0
  158. package/dist/sui/manuallyExec/encoder.js.map +1 -0
  159. package/dist/sui/manuallyExec/index.d.ts +37 -0
  160. package/dist/sui/manuallyExec/index.d.ts.map +1 -0
  161. package/dist/sui/manuallyExec/index.js +81 -0
  162. package/dist/sui/manuallyExec/index.js.map +1 -0
  163. package/dist/sui/objects.d.ts +46 -0
  164. package/dist/sui/objects.d.ts.map +1 -0
  165. package/dist/sui/objects.js +259 -0
  166. package/dist/sui/objects.js.map +1 -0
  167. package/dist/ton/bindings/offramp.d.ts +48 -0
  168. package/dist/ton/bindings/offramp.d.ts.map +1 -0
  169. package/dist/ton/bindings/offramp.js +63 -0
  170. package/dist/ton/bindings/offramp.js.map +1 -0
  171. package/dist/ton/bindings/onramp.d.ts +40 -0
  172. package/dist/ton/bindings/onramp.d.ts.map +1 -0
  173. package/dist/ton/bindings/onramp.js +51 -0
  174. package/dist/ton/bindings/onramp.js.map +1 -0
  175. package/dist/ton/bindings/router.d.ts +47 -0
  176. package/dist/ton/bindings/router.d.ts.map +1 -0
  177. package/dist/ton/bindings/router.js +51 -0
  178. package/dist/ton/bindings/router.js.map +1 -0
  179. package/dist/ton/exec.d.ts +18 -0
  180. package/dist/ton/exec.d.ts.map +1 -0
  181. package/dist/ton/exec.js +28 -0
  182. package/dist/ton/exec.js.map +1 -0
  183. package/dist/ton/hasher.d.ts +27 -0
  184. package/dist/ton/hasher.d.ts.map +1 -0
  185. package/dist/ton/hasher.js +134 -0
  186. package/dist/ton/hasher.js.map +1 -0
  187. package/dist/ton/index.d.ts +247 -0
  188. package/dist/ton/index.d.ts.map +1 -0
  189. package/dist/ton/index.js +781 -0
  190. package/dist/ton/index.js.map +1 -0
  191. package/dist/ton/logs.d.ts +26 -0
  192. package/dist/ton/logs.d.ts.map +1 -0
  193. package/dist/ton/logs.js +126 -0
  194. package/dist/ton/logs.js.map +1 -0
  195. package/dist/ton/types.d.ts +37 -0
  196. package/dist/ton/types.d.ts.map +1 -0
  197. package/dist/ton/types.js +92 -0
  198. package/dist/ton/types.js.map +1 -0
  199. package/dist/ton/utils.d.ts +67 -0
  200. package/dist/ton/utils.d.ts.map +1 -0
  201. package/dist/ton/utils.js +425 -0
  202. package/dist/ton/utils.js.map +1 -0
  203. package/dist/types.d.ts +4 -2
  204. package/dist/types.d.ts.map +1 -1
  205. package/dist/types.js +1 -0
  206. package/dist/types.js.map +1 -1
  207. package/dist/utils.d.ts +10 -0
  208. package/dist/utils.d.ts.map +1 -1
  209. package/dist/utils.js +52 -17
  210. package/dist/utils.js.map +1 -1
  211. package/package.json +12 -10
  212. package/src/aptos/hasher.ts +10 -6
  213. package/src/aptos/index.ts +50 -31
  214. package/src/aptos/logs.ts +85 -29
  215. package/src/aptos/token.ts +5 -1
  216. package/src/aptos/types.ts +6 -6
  217. package/src/chain.ts +83 -12
  218. package/src/commits.ts +23 -11
  219. package/src/errors/CCIPError.ts +86 -0
  220. package/src/errors/codes.ts +179 -0
  221. package/src/errors/index.ts +175 -0
  222. package/src/errors/recovery.ts +170 -0
  223. package/src/errors/specialized.ts +1655 -0
  224. package/src/errors/utils.ts +73 -0
  225. package/src/evm/abi/CommitStore_1_5.ts +1 -1
  226. package/src/evm/abi/LockReleaseTokenPool_1_5.ts +1 -1
  227. package/src/evm/abi/OffRamp_1_5.ts +1 -1
  228. package/src/evm/abi/OnRamp_1_5.ts +1 -1
  229. package/src/evm/abi/PriceRegistry_1_2.ts +438 -0
  230. package/src/evm/const.ts +2 -0
  231. package/src/evm/hasher.ts +7 -6
  232. package/src/evm/index.ts +104 -86
  233. package/src/evm/logs.ts +64 -16
  234. package/src/evm/messages.ts +14 -14
  235. package/src/evm/offchain.ts +1 -1
  236. package/src/evm/types.ts +11 -0
  237. package/src/execution.ts +13 -9
  238. package/src/extra-args.ts +4 -3
  239. package/src/gas.ts +10 -3
  240. package/src/hasher/hasher.ts +2 -1
  241. package/src/hasher/merklemulti.ts +18 -8
  242. package/src/index.ts +14 -2
  243. package/src/offchain.ts +10 -14
  244. package/src/requests.ts +51 -53
  245. package/src/selectors.ts +23 -0
  246. package/src/solana/cleanup.ts +2 -4
  247. package/src/solana/exec.ts +13 -13
  248. package/src/solana/hasher.ts +9 -5
  249. package/src/solana/index.ts +126 -200
  250. package/src/solana/logs.ts +155 -0
  251. package/src/solana/offchain.ts +10 -7
  252. package/src/solana/patchBorsh.ts +3 -2
  253. package/src/solana/send.ts +14 -7
  254. package/src/solana/utils.ts +31 -17
  255. package/src/sui/discovery.ts +163 -0
  256. package/src/sui/events.ts +328 -0
  257. package/src/sui/hasher.ts +6 -5
  258. package/src/sui/index.ts +528 -80
  259. package/src/sui/manuallyExec/encoder.ts +88 -0
  260. package/src/sui/manuallyExec/index.ts +137 -0
  261. package/src/sui/objects.ts +358 -0
  262. package/src/ton/bindings/offramp.ts +96 -0
  263. package/src/ton/bindings/onramp.ts +72 -0
  264. package/src/ton/bindings/router.ts +65 -0
  265. package/src/ton/exec.ts +44 -0
  266. package/src/ton/hasher.ts +184 -0
  267. package/src/ton/index.ts +989 -0
  268. package/src/ton/logs.ts +157 -0
  269. package/src/ton/types.ts +143 -0
  270. package/src/ton/utils.ts +514 -0
  271. package/src/types.ts +6 -2
  272. package/src/utils.ts +58 -23
  273. package/tsconfig.json +2 -1
@@ -0,0 +1,514 @@
1
+ import { type Address, Cell, Dictionary, beginCell } from '@ton/core'
2
+ import type { TonClient4 } from '@ton/ton'
3
+
4
+ import {
5
+ CCIPTransactionNotFinalizedError,
6
+ CCIPTransactionNotFoundError,
7
+ } from '../errors/specialized.ts'
8
+ import type { WithLogger } from '../types.ts'
9
+ import { bytesToBuffer, sleep } from '../utils.ts'
10
+
11
+ /**
12
+ * Converts hex string to Buffer, handling 0x prefix normalization
13
+ * Returns empty buffer for empty input
14
+ */
15
+ export const hexToBuffer = (value: string): Buffer => {
16
+ if (!value || value === '0x' || value === '0X') return Buffer.alloc(0)
17
+ // Normalize to lowercase 0x prefix for bytesToBuffer/getDataBytes
18
+ let normalized: string
19
+ if (value.startsWith('0x')) {
20
+ normalized = value
21
+ } else if (value.startsWith('0X')) {
22
+ normalized = `0x${value.slice(2)}`
23
+ } else {
24
+ normalized = `0x${value}`
25
+ }
26
+ return bytesToBuffer(normalized)
27
+ }
28
+
29
+ /**
30
+ * Attempts to parse hex string as TON BOC (Bag of Cells) format
31
+ * Falls back to storing raw bytes as cell data if BOC parsing fails
32
+ * Used for parsing message data, extra data, and other hex-encoded fields
33
+ */
34
+ export const tryParseCell = (hex: string): Cell => {
35
+ const bytes = hexToBuffer(hex)
36
+ if (bytes.length === 0) return beginCell().endCell()
37
+ try {
38
+ return Cell.fromBoc(bytes)[0]
39
+ } catch {
40
+ return beginCell().storeBuffer(bytes).endCell()
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Extracts the 32-bit magic tag from a BOC-encoded cell
46
+ * Magic tags identify the type of TON structures (e.g., extra args types)
47
+ * Used for type detection and validation when decoding CCIP extra args
48
+ * Returns tag as 0x-prefixed hex string for easy comparison
49
+ */
50
+ export function extractMagicTag(bocHex: string): string {
51
+ const cell = Cell.fromBoc(hexToBuffer(bocHex))[0]
52
+ const tag = cell.beginParse().loadUint(32)
53
+ return `0x${tag.toString(16).padStart(8, '0')}`
54
+ }
55
+
56
+ /**
57
+ * Waits for a transaction to be confirmed by polling until the wallet's seqno advances.
58
+ * Once seqno advances past expectedSeqno, fetches the latest transaction details.
59
+ *
60
+ * @param client - TON V4 client
61
+ * @param walletAddress - Address of the wallet that sent the transaction
62
+ * @param expectedSeqno - The seqno used when sending the transaction
63
+ * @param expectedDestination - Optional destination address to verify (e.g., offRamp)
64
+ * @param maxAttempts - Maximum polling attempts (default: 25)
65
+ * @param intervalMs - Polling interval in ms (default: 1000)
66
+ * @returns Transaction info with lt and hash
67
+ */
68
+ export async function waitForTransaction(
69
+ client: TonClient4,
70
+ walletAddress: Address,
71
+ expectedSeqno: number,
72
+ expectedDestination?: Address,
73
+ maxAttempts = 25,
74
+ intervalMs = 1000,
75
+ ): Promise<{ lt: string; hash: string; timestamp: number }> {
76
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
77
+ try {
78
+ // Get latest block for state lookup (V4 API requires block seqno)
79
+ const lastBlock = await client.getLastBlock()
80
+
81
+ // Check current seqno by running the getter
82
+ const seqnoResult = await client.runMethod(lastBlock.last.seqno, walletAddress, 'seqno')
83
+ const currentSeqno = seqnoResult.reader.readNumber()
84
+
85
+ const seqnoAdvanced = currentSeqno > expectedSeqno
86
+
87
+ if (seqnoAdvanced) {
88
+ // Get account state to find latest transaction
89
+ const account = await client.getAccountLite(lastBlock.last.seqno, walletAddress)
90
+ if (!account.account.last) {
91
+ await sleep(intervalMs)
92
+ continue
93
+ }
94
+
95
+ // Get recent transactions using V4 API
96
+ const txs = await client.getAccountTransactions(
97
+ walletAddress,
98
+ BigInt(account.account.last.lt),
99
+ Buffer.from(account.account.last.hash, 'base64'),
100
+ )
101
+
102
+ for (const { tx } of txs) {
103
+ // If destination verification requested, check outgoing messages
104
+ if (expectedDestination) {
105
+ const outMessages = tx.outMessages.values()
106
+ let destinationMatch = false
107
+
108
+ for (const msg of outMessages) {
109
+ if (msg.info.type === 'internal' && msg.info.dest.equals(expectedDestination)) {
110
+ destinationMatch = true
111
+ break
112
+ }
113
+ }
114
+
115
+ if (!destinationMatch) continue
116
+ }
117
+
118
+ return {
119
+ lt: tx.lt.toString(),
120
+ hash: tx.hash().toString('hex'),
121
+ timestamp: tx.now,
122
+ }
123
+ }
124
+ }
125
+
126
+ // Handle case where contract was just deployed (seqno 0 -> 1)
127
+ if (expectedSeqno === 0 && attempt > 0) {
128
+ const account = await client.getAccountLite(lastBlock.last.seqno, walletAddress)
129
+ if (account.account.last) {
130
+ const txs = await client.getAccountTransactions(
131
+ walletAddress,
132
+ BigInt(account.account.last.lt),
133
+ Buffer.from(account.account.last.hash, 'base64'),
134
+ )
135
+ if (txs.length > 0) {
136
+ const { tx } = txs[0]
137
+ return {
138
+ lt: tx.lt.toString(),
139
+ hash: tx.hash().toString('hex'),
140
+ timestamp: tx.now,
141
+ }
142
+ }
143
+ }
144
+ }
145
+ } catch {
146
+ // Contract might not be initialized yet, or network error - retry
147
+ }
148
+
149
+ await sleep(intervalMs)
150
+ }
151
+
152
+ throw new CCIPTransactionNotFinalizedError(String(expectedSeqno))
153
+ }
154
+
155
+ /**
156
+ * Parses snake format data from a cell.
157
+ * Snake format: first byte indicates format (0x00), followed by string data that may span multiple cells.
158
+ */
159
+ function parseSnakeData(cell: Cell): string {
160
+ const slice = cell.beginParse()
161
+
162
+ // Check first byte. Should be 0x00 for snake format
163
+ if (slice.remainingBits >= 8) {
164
+ const firstByte = slice.preloadUint(8)
165
+ if (firstByte === 0x00) {
166
+ // Standard snake format. skip the indicator byte
167
+ slice.loadUint(8)
168
+ }
169
+ // If not 0x00, the data might be stored directly without indicator
170
+ }
171
+
172
+ // Load the string, following references if needed
173
+ let result = ''
174
+
175
+ // Load available bits as string
176
+ const bits = slice.remainingBits
177
+ if (bits > 0) {
178
+ // Round down to nearest byte
179
+ const bytes = Math.floor(bits / 8)
180
+ if (bytes > 0) {
181
+ const buffer = slice.loadBuffer(bytes)
182
+ result = buffer.toString('utf-8')
183
+ }
184
+ }
185
+
186
+ // Follow references for continuation (snake format can span multiple cells)
187
+ while (slice.remainingRefs > 0) {
188
+ const refCell = slice.loadRef()
189
+ const refSlice = refCell.beginParse()
190
+ const refBits = refSlice.remainingBits
191
+ if (refBits > 0) {
192
+ const refBytes = Math.floor(refBits / 8)
193
+ if (refBytes > 0) {
194
+ const buffer = refSlice.loadBuffer(refBytes)
195
+ result += buffer.toString('utf-8')
196
+ }
197
+ }
198
+ break
199
+ }
200
+
201
+ return result
202
+ }
203
+
204
+ /**
205
+ * Fetches Jetton metadata from an external URI.
206
+ * Handles IPFS and HTTP(S) URIs.
207
+ */
208
+ async function fetchOffchainJettonMetadata(
209
+ uri: string,
210
+ rateLimitedFetch: typeof fetch,
211
+ logger?: { debug?: (...args: unknown[]) => void },
212
+ ): Promise<{ symbol: string; decimals: number }> {
213
+ // Default values
214
+ let symbol = 'JETTON'
215
+ let decimals = 9
216
+
217
+ try {
218
+ // Normalize URI
219
+ let normalizedUri = uri
220
+ if (uri.startsWith('ipfs://')) {
221
+ normalizedUri = 'https://ipfs.io/ipfs/' + uri.slice(7)
222
+ } else if (uri.startsWith('Qm') && uri.length >= 46) {
223
+ normalizedUri = 'https://ipfs.io/ipfs/' + uri
224
+ }
225
+
226
+ if (!normalizedUri.startsWith('http://') && !normalizedUri.startsWith('https://')) {
227
+ return { symbol, decimals }
228
+ }
229
+
230
+ const response = await rateLimitedFetch(normalizedUri, {
231
+ headers: { Accept: 'application/json' },
232
+ })
233
+
234
+ if (!response.ok) {
235
+ return { symbol, decimals }
236
+ }
237
+
238
+ const metadata = (await response.json()) as {
239
+ symbol?: string
240
+ decimals?: number | string
241
+ }
242
+
243
+ if (metadata.symbol && typeof metadata.symbol === 'string') {
244
+ symbol = metadata.symbol
245
+ }
246
+
247
+ if (metadata.decimals !== undefined) {
248
+ const dec =
249
+ typeof metadata.decimals === 'string' ? parseInt(metadata.decimals, 10) : metadata.decimals
250
+ if (!isNaN(dec) && dec >= 0 && dec <= 255) {
251
+ decimals = dec
252
+ }
253
+ }
254
+ } catch (error) {
255
+ logger?.debug?.(`Failed to fetch offchain jetton metadata from ${uri}:`, error)
256
+ }
257
+
258
+ return { symbol, decimals }
259
+ }
260
+
261
+ /** SHA256 hashes of known TEP-64 attribute names */
262
+ const TEP64_HASHES = {
263
+ symbol: BigInt('0xb76a7ca153c24671658335bbd08946350ffc621fa1c516e7123095d4ffd5c581'),
264
+ decimals: BigInt('0xee80fd2f1e03480e2282363596ee752d7bb27f50776b95086a0279189675923e'),
265
+ uri: BigInt('0x70e5d7b6a29b392f85076fe15ca2f2053c56c2338728c4e33c9e8ddb1ee827cc'),
266
+ } as const
267
+
268
+ /**
269
+ * Parses onchain metadata dictionary to extract symbol and decimals.
270
+ * If symbol is not found, checks for URI key to fetch offchain metadata.
271
+ */
272
+ async function parseOnchainDict(
273
+ dict: Dictionary<bigint, Cell>,
274
+ rateLimitedFetch: typeof fetch,
275
+ logger?: { debug?: (...args: unknown[]) => void },
276
+ ): Promise<{ symbol: string; decimals: number }> {
277
+ let symbol = 'JETTON'
278
+ let decimals = 9
279
+
280
+ // Try to get symbol from dict
281
+ const symbolCell = dict.get(TEP64_HASHES.symbol)
282
+ if (symbolCell) {
283
+ const parsed = parseSnakeData(symbolCell)
284
+ if (parsed) {
285
+ symbol = parsed
286
+ }
287
+ }
288
+
289
+ // Try to get decimals from dict
290
+ const decimalsCell = dict.get(TEP64_HASHES.decimals)
291
+ if (decimalsCell) {
292
+ const decStr = parseSnakeData(decimalsCell)
293
+ const parsed = parseInt(decStr, 10)
294
+ if (!isNaN(parsed) && parsed >= 0 && parsed <= 255) {
295
+ decimals = parsed
296
+ }
297
+ }
298
+
299
+ // If symbol not found in dict, check for URI key and fetch offchain
300
+ if (symbol === 'JETTON') {
301
+ const uriCell = dict.get(TEP64_HASHES.uri)
302
+ if (uriCell) {
303
+ const uri = parseSnakeData(uriCell)
304
+ if (uri && (uri.startsWith('http') || uri.startsWith('ipfs://') || uri.startsWith('Qm'))) {
305
+ const offchain = await fetchOffchainJettonMetadata(uri, rateLimitedFetch, logger)
306
+ symbol = offchain.symbol
307
+ // Only use offchain decimals if we didn't get it from onchain
308
+ if (decimals === 9) {
309
+ decimals = offchain.decimals
310
+ }
311
+ }
312
+ }
313
+ }
314
+
315
+ return { symbol, decimals }
316
+ }
317
+
318
+ /**
319
+ * Parses Jetton content cell to extract metadata.
320
+ * Supports onchain (0x00), offchain (0x01), and semichain (0x02) formats per TEP-64.
321
+ */
322
+ export async function parseJettonContent(
323
+ contentCell: Cell,
324
+ rateLimitedFetch: typeof fetch,
325
+ logger?: { debug?: (...args: unknown[]) => void },
326
+ ): Promise<{ symbol: string; decimals: number }> {
327
+ const slice = contentCell.beginParse()
328
+
329
+ // Default values
330
+ const symbol = 'JETTON'
331
+ const decimals = 9
332
+
333
+ try {
334
+ // Check content type (first byte)
335
+ const contentType = slice.loadUint(8)
336
+
337
+ if (contentType === 0x00) {
338
+ // Onchain metadata - dictionary may be inline or in a reference
339
+ let dict: Dictionary<bigint, Cell> | undefined
340
+
341
+ // Check if there's remaining data for inline dict
342
+ if (slice.remainingBits > 1) {
343
+ try {
344
+ dict = slice.loadDict(Dictionary.Keys.BigUint(256), Dictionary.Values.Cell())
345
+ } catch {
346
+ // Failed, will try from ref below
347
+ }
348
+ }
349
+
350
+ // If no inline dict, check for Maybe ^Cell pattern (1 bit + ref)
351
+ if (!dict && slice.remainingBits >= 1 && slice.remainingRefs > 0) {
352
+ const hasDict = slice.loadBit()
353
+ if (hasDict) {
354
+ const dictCell = slice.loadRef()
355
+ try {
356
+ dict = dictCell
357
+ .beginParse()
358
+ .loadDictDirect(Dictionary.Keys.BigUint(256), Dictionary.Values.Cell())
359
+ } catch {
360
+ try {
361
+ dict = Dictionary.loadDirect(
362
+ Dictionary.Keys.BigUint(256),
363
+ Dictionary.Values.Cell(),
364
+ dictCell.beginParse(),
365
+ )
366
+ } catch {
367
+ logger?.debug?.('Onchain: failed to load dict from ref')
368
+ }
369
+ }
370
+ }
371
+ }
372
+
373
+ // If still no dict, try loading directly from first ref
374
+ if (!dict && contentCell.refs.length > 0) {
375
+ try {
376
+ const refSlice = contentCell.refs[0].beginParse()
377
+ dict = refSlice.loadDictDirect(Dictionary.Keys.BigUint(256), Dictionary.Values.Cell())
378
+ } catch {
379
+ logger?.debug?.('Onchain: failed to load dict directly from ref')
380
+ }
381
+ }
382
+
383
+ if (dict) {
384
+ return await parseOnchainDict(dict, rateLimitedFetch, logger)
385
+ }
386
+
387
+ return { symbol, decimals }
388
+ } else if (contentType === 0x01) {
389
+ // Offchain metadata: URI stored in remaining bits
390
+ const uri = slice.loadStringTail()
391
+ return fetchOffchainJettonMetadata(uri, rateLimitedFetch, logger)
392
+ } else if (contentType === 0x02) {
393
+ // Semichain metadata per TEP-64
394
+ let onchainResult = { symbol: 'JETTON', decimals: 9 }
395
+ let uri = ''
396
+
397
+ // Load dictionary directly from remaining slice data
398
+ try {
399
+ const dict = slice.loadDictDirect(Dictionary.Keys.BigUint(256), Dictionary.Values.Cell())
400
+ onchainResult = await parseOnchainDict(dict, rateLimitedFetch, logger)
401
+ } catch (e) {
402
+ logger?.debug?.('Semichain: failed to load dict directly:', e)
403
+ }
404
+
405
+ // After dictionary, there may be a URI in remaining bits or refs
406
+ if (slice.remainingBits > 0) {
407
+ try {
408
+ uri = slice.loadStringTail()
409
+ } catch {
410
+ logger?.debug?.('Semichain: failed to load URI from remaining bits')
411
+ }
412
+ }
413
+
414
+ // If no URI in bits, try from cell reference
415
+ if (!uri && slice.remainingRefs > 0) {
416
+ try {
417
+ const uriCell = slice.loadRef()
418
+ const uriSlice = uriCell.beginParse()
419
+
420
+ if (uriSlice.remainingBits >= 8) {
421
+ const firstByte = uriSlice.preloadUint(8)
422
+ if (firstByte === 0x01) {
423
+ uriSlice.loadUint(8)
424
+ }
425
+ }
426
+ uri = uriSlice.loadStringTail()
427
+ } catch {
428
+ logger?.debug?.('Semichain: failed to load URI from ref')
429
+ }
430
+ }
431
+
432
+ // If we got valid symbol from onchain dict, use it
433
+ if (onchainResult.symbol !== 'JETTON') {
434
+ return onchainResult
435
+ }
436
+
437
+ // Otherwise try fetching from URI
438
+ if (uri && (uri.startsWith('http') || uri.startsWith('ipfs://') || uri.startsWith('Qm'))) {
439
+ const offchainResult = await fetchOffchainJettonMetadata(uri, rateLimitedFetch, logger)
440
+ return {
441
+ symbol: offchainResult.symbol,
442
+ decimals: onchainResult.decimals !== 9 ? onchainResult.decimals : offchainResult.decimals,
443
+ }
444
+ }
445
+
446
+ return onchainResult
447
+ }
448
+ } catch (error) {
449
+ logger?.debug?.('Failed to parse jetton content:', error)
450
+ }
451
+
452
+ return { symbol, decimals }
453
+ }
454
+
455
+ /**
456
+ * Looks up a transaction by raw hash using the TonCenter V3 API.
457
+ *
458
+ * This is necessary because TON's V4 API requires (address, lt, hash) for lookups,
459
+ * but users typically only have the raw transaction hash from explorers.
460
+ * TonCenter V3 provides an index that allows hash-only lookups.
461
+ *
462
+ * @param hash - Raw 64-char hex transaction hash
463
+ * @param isTestnet - Whether to use testnet API
464
+ * @param rateLimitedFetch - Rate-limited fetch function
465
+ * @param logger - Logger instance
466
+ * @returns Transaction identifier components needed for V4 API lookup
467
+ */
468
+ export async function lookupTxByRawHash(
469
+ hash: string,
470
+ isTestnet: boolean,
471
+ rateLimitedFetch: typeof fetch,
472
+ logger: WithLogger['logger'],
473
+ ): Promise<{
474
+ account: string
475
+ lt: string
476
+ hash: string
477
+ }> {
478
+ const baseUrl = isTestnet
479
+ ? 'https://testnet.toncenter.com/api/v3/transactions'
480
+ : 'https://toncenter.com/api/v3/transactions'
481
+
482
+ // TonCenter V3 accepts hex directly
483
+ const cleanHash = hash.startsWith('0x') ? hash.slice(2) : hash
484
+
485
+ const url = `${baseUrl}?hash=${cleanHash}`
486
+ logger?.debug?.(`TonCenter V3 lookup: ${url}`)
487
+
488
+ let response: Response
489
+ try {
490
+ response = await rateLimitedFetch(url, {
491
+ headers: { Accept: 'application/json' },
492
+ })
493
+ } catch (error) {
494
+ logger?.error?.(`TonCenter V3 fetch failed:`, error)
495
+ throw new CCIPTransactionNotFoundError(hash, { cause: error as Error })
496
+ }
497
+
498
+ let data: { transactions?: Array<{ account: string; lt: string; hash: string }> }
499
+ try {
500
+ data = (await response.json()) as typeof data
501
+ } catch (error) {
502
+ logger?.error?.(`TonCenter V3 JSON parse failed:`, error)
503
+ throw new CCIPTransactionNotFoundError(hash, { cause: error as Error })
504
+ }
505
+
506
+ logger?.debug?.(`TonCenter V3 response:`, data)
507
+
508
+ if (!data.transactions || data.transactions.length === 0) {
509
+ logger?.debug?.(`TonCenter V3: no transactions found for hash ${cleanHash}`)
510
+ throw new CCIPTransactionNotFoundError(hash)
511
+ }
512
+
513
+ return data.transactions[0]
514
+ }
package/src/types.ts CHANGED
@@ -6,6 +6,7 @@ import type { CCIPMessage_EVM, CCIPMessage_V1_6_EVM } from './evm/messages.ts'
6
6
  import type { ExtraArgs } from './extra-args.ts'
7
7
  import type { CCIPMessage_V1_6_Solana } from './solana/types.ts'
8
8
  import type { CCIPMessage_V1_6_Sui } from './sui/types.ts'
9
+ import type { CCIPMessage_V1_6_TON } from './ton/types.ts'
9
10
  // v1.6 Base type from EVM contains the intersection of all other CCIPMessage v1.6 types
10
11
  export type { CCIPMessage_V1_6 } from './evm/messages.ts'
11
12
 
@@ -63,6 +64,7 @@ export const ChainFamily = {
63
64
  Solana: 'solana',
64
65
  Aptos: 'aptos',
65
66
  Sui: 'sui',
67
+ TON: 'ton',
66
68
  } as const
67
69
  /** Type representing one of the supported chain families. */
68
70
  export type ChainFamily = (typeof ChainFamily)[keyof typeof ChainFamily]
@@ -79,7 +81,9 @@ export const CCIPVersion = {
79
81
  export type CCIPVersion = (typeof CCIPVersion)[keyof typeof CCIPVersion]
80
82
 
81
83
  /** Helper type that maps chain family to its chain ID format. */
82
- type ChainFamilyWithId<F extends ChainFamily> = F extends typeof ChainFamily.EVM
84
+ type ChainFamilyWithId<F extends ChainFamily> = F extends
85
+ | typeof ChainFamily.EVM
86
+ | typeof ChainFamily.TON
83
87
  ? { readonly family: F; readonly chainId: number }
84
88
  : F extends typeof ChainFamily.Solana
85
89
  ? { readonly family: F; readonly chainId: string }
@@ -120,7 +124,7 @@ export type CCIPMessage<V extends CCIPVersion = CCIPVersion> = V extends
120
124
  | typeof CCIPVersion.V1_2
121
125
  | typeof CCIPVersion.V1_5
122
126
  ? CCIPMessage_EVM<V>
123
- : CCIPMessage_V1_6_EVM | CCIPMessage_V1_6_Solana | CCIPMessage_V1_6_Sui
127
+ : CCIPMessage_V1_6_EVM | CCIPMessage_V1_6_Solana | CCIPMessage_V1_6_Sui | CCIPMessage_V1_6_TON
124
128
 
125
129
  /**
126
130
  * Generic log structure compatible across chain families.