@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,989 @@
1
+ import { Address, Cell, beginCell, toNano } from '@ton/core'
2
+ import { TonClient4, internal } from '@ton/ton'
3
+ import { type BytesLike, getAddress as checksumAddress, isBytesLike } from 'ethers'
4
+ import { memoize } from 'micro-memoize'
5
+ import type { PickDeep } from 'type-fest'
6
+
7
+ import { type LogDecoders, fetchLogs } from './logs.ts'
8
+ import { type LogFilter, Chain } from '../chain.ts'
9
+ import {
10
+ CCIPArgumentInvalidError,
11
+ CCIPExtraArgsInvalidError,
12
+ CCIPHttpError,
13
+ CCIPNotImplementedError,
14
+ CCIPSourceChainUnsupportedError,
15
+ CCIPTransactionNotFoundError,
16
+ CCIPWalletInvalidError,
17
+ } from '../errors/specialized.ts'
18
+ import { type EVMExtraArgsV2, type ExtraArgs, EVMExtraArgsV2Tag } from '../extra-args.ts'
19
+ import { fetchCCIPRequestsInTx } from '../requests.ts'
20
+ import { supportedChains } from '../supported-chains.ts'
21
+ import {
22
+ type AnyMessage,
23
+ type CCIPRequest,
24
+ type ChainTransaction,
25
+ type CommitReport,
26
+ type ExecutionReceipt,
27
+ type ExecutionReport,
28
+ type Lane,
29
+ type Log_,
30
+ type NetworkInfo,
31
+ type OffchainTokenData,
32
+ type WithLogger,
33
+ ChainFamily,
34
+ } from '../types.ts'
35
+ import {
36
+ bytesToBuffer,
37
+ createRateLimitedFetch,
38
+ decodeAddress,
39
+ getDataBytes,
40
+ networkInfo,
41
+ parseTypeAndVersion,
42
+ } from '../utils.ts'
43
+ import { OffRamp } from './bindings/offramp.ts'
44
+ import { OnRamp } from './bindings/onramp.ts'
45
+ import { Router } from './bindings/router.ts'
46
+ import { generateUnsignedExecuteReport as generateUnsignedExecuteReportImpl } from './exec.ts'
47
+ import { getTONLeafHasher } from './hasher.ts'
48
+ import { type CCIPMessage_V1_6_TON, type UnsignedTONTx, isTONWallet } from './types.ts'
49
+ import { lookupTxByRawHash, parseJettonContent, waitForTransaction } from './utils.ts'
50
+ import type { LeafHasher } from '../hasher/common.ts'
51
+
52
+ /**
53
+ * Type guard to check if an error is a TVM error with an exit code.
54
+ * TON VM errors include an exitCode property indicating the error type.
55
+ */
56
+ function isTvmError(error: unknown): error is Error & { exitCode: number } {
57
+ return error instanceof Error && 'exitCode' in error && typeof error.exitCode === 'number'
58
+ }
59
+
60
+ /**
61
+ * TON chain implementation supporting TON networks.
62
+ *
63
+ * TON uses two different ordering concepts:
64
+ * - `seqno` (sequence number): The actual block number in the blockchain
65
+ * - `lt` (logical time): A per-account transaction ordering timestamp
66
+ *
67
+ * This implementation uses `lt` for the `blockNumber` field in logs and transactions
68
+ * because TON's transaction APIs are indexed by `lt`, not `seqno`. The `lt` is
69
+ * monotonically increasing per account and suitable for pagination and ordering.
70
+ */
71
+ export class TONChain extends Chain<typeof ChainFamily.TON> {
72
+ static {
73
+ supportedChains[ChainFamily.TON] = TONChain
74
+ }
75
+ static readonly family = ChainFamily.TON
76
+ static readonly decimals = 9 // TON uses 9 decimals (nanotons)
77
+ private readonly rateLimitedFetch: typeof fetch
78
+ readonly provider: TonClient4
79
+ /**
80
+ * Cache mapping logical time (lt) to Unix timestamp.
81
+ * Populated during getLogs iteration for later getBlockTimestamp lookups.
82
+ */
83
+ private readonly ltTimestampCache: Map<number, number> = new Map()
84
+
85
+ /**
86
+ * Creates a new TONChain instance.
87
+ * @param client - TonClient instance.
88
+ * @param network - Network information for this chain.
89
+ * @param ctx - Context containing logger.
90
+ */
91
+ constructor(client: TonClient4, network: NetworkInfo, ctx?: WithLogger) {
92
+ super(network, ctx)
93
+ this.provider = client
94
+
95
+ // Rate-limited fetch for TonCenter API (public tier: ~1 req/sec)
96
+ const rateLimitedFetch = createRateLimitedFetch(
97
+ { maxRequests: 1, windowMs: 1500, maxRetries: 5 },
98
+ ctx,
99
+ )
100
+ this.rateLimitedFetch = (input, init) => {
101
+ this.logger.warn?.(
102
+ 'Public TONCenter API calls are rate-limited to ~1 req/sec, some commands may be slow',
103
+ )
104
+ return rateLimitedFetch(input, init)
105
+ }
106
+
107
+ this.getTransaction = memoize(this.getTransaction.bind(this), {
108
+ maxSize: 100,
109
+ })
110
+ }
111
+
112
+ /**
113
+ * Creates a TONChain instance from an RPC URL.
114
+ * Verifies the connection and detects the network.
115
+ *
116
+ * @param url - RPC endpoint URL for TonClient4.
117
+ * @param ctx - Context containing logger.
118
+ * @returns A new TONChain instance.
119
+ */
120
+ static async fromUrl(url: string, ctx?: WithLogger): Promise<TONChain> {
121
+ const { logger = console } = ctx ?? {}
122
+
123
+ // Parse URL for validation
124
+ let parsedUrl: URL
125
+ try {
126
+ parsedUrl = new URL(url)
127
+ } catch {
128
+ throw new CCIPArgumentInvalidError('url', `Invalid URL format: ${url}`)
129
+ }
130
+
131
+ const hostname = parsedUrl.hostname.toLowerCase()
132
+ const client = new TonClient4({ endpoint: url })
133
+
134
+ // Verify connection by getting the latest block
135
+ try {
136
+ await client.getLastBlock()
137
+ logger.debug?.(`Connected to TON V4 endpoint: ${url}`)
138
+ } catch (error) {
139
+ const message = error instanceof Error ? error.message : String(error)
140
+ throw new CCIPHttpError(0, `Failed to connect to TON V4 endpoint ${url}: ${message}`)
141
+ }
142
+
143
+ // Detect network from hostname
144
+ let networkId: string
145
+ if (hostname.includes('testnet')) {
146
+ networkId = 'ton-testnet'
147
+ } else if (
148
+ hostname === 'localhost' ||
149
+ hostname === '127.0.0.1' ||
150
+ hostname.includes('sandbox')
151
+ ) {
152
+ networkId = 'ton-localnet'
153
+ } else {
154
+ // Default to mainnet for production endpoints
155
+ networkId = 'ton-mainnet'
156
+ }
157
+
158
+ return new TONChain(client, networkInfo(networkId), ctx)
159
+ }
160
+
161
+ /**
162
+ * Fetch the timestamp for a given logical time (lt) or finalized block.
163
+ *
164
+ * Note: For TON, the `block` parameter represents logical time (lt), not block seqno.
165
+ * This is because TON transaction APIs are indexed by lt. The lt must have been
166
+ * previously cached via getLogs or getTransaction calls.
167
+ *
168
+ * @param block - Logical time (lt) as number, or 'finalized' for latest block timestamp
169
+ * @returns Unix timestamp in seconds
170
+ */
171
+ async getBlockTimestamp(block: number | 'finalized'): Promise<number> {
172
+ if (block === 'finalized') {
173
+ // Get the latest block timestamp from V4 API
174
+ const lastBlock = await this.provider.getLastBlock()
175
+ return lastBlock.now
176
+ }
177
+
178
+ // Check lt → timestamp cache
179
+ const cached = this.ltTimestampCache.get(block)
180
+ if (cached !== undefined) {
181
+ return cached
182
+ }
183
+
184
+ // For TON, we cannot look up timestamp by lt alone without the account address.
185
+ // The lt must have been cached during a previous getLogs or getTransaction call.
186
+ throw new CCIPNotImplementedError(
187
+ `getBlockTimestamp: lt ${block} not in cache. ` +
188
+ `TON requires lt to be cached from getLogs or getTransaction calls first.`,
189
+ )
190
+ }
191
+
192
+ /**
193
+ * Fetches a transaction by its hash.
194
+ *
195
+ * Supports two formats:
196
+ * 1. Composite format: "workchain:address:lt:hash" (e.g., "0:abc123...def:12345:abc123...def")
197
+ * 2. Raw hash format: 64-character hex string resolved via TonCenter V3 API
198
+ *
199
+ * Note: TON's V4 API requires (address, lt, hash) for lookups. Raw hash lookups
200
+ * use TonCenter's V3 index API to resolve the hash to a full identifier first.
201
+ *
202
+ * @param hash - Transaction identifier in either format
203
+ * @returns ChainTransaction with transaction details
204
+ * Note: `blockNumber` contains logical time (lt), not block seqno
205
+ */
206
+ async getTransaction(hash: string): Promise<ChainTransaction> {
207
+ const parts = hash.split(':')
208
+
209
+ // If not composite format (4 parts), check if it's a raw 64-char hex hash
210
+ if (parts.length !== 4) {
211
+ const cleanHash = hash.startsWith('0x') || hash.startsWith('0X') ? hash.slice(2) : hash
212
+
213
+ if (/^[a-fA-F0-9]{64}$/.test(cleanHash)) {
214
+ const isTestnet = this.network.name?.includes('testnet') ?? false
215
+ const txInfo = await lookupTxByRawHash(
216
+ cleanHash,
217
+ isTestnet,
218
+ this.rateLimitedFetch,
219
+ this.logger,
220
+ )
221
+
222
+ const compositeHash = `${txInfo.account}:${txInfo.lt}:${cleanHash}`
223
+ this.logger.debug?.(`Resolved raw hash to composite: ${compositeHash}`)
224
+
225
+ return this.getTransaction(compositeHash)
226
+ }
227
+
228
+ throw new CCIPArgumentInvalidError(
229
+ 'hash',
230
+ `Invalid TON transaction hash format: "${hash}". Expected "workchain:address:lt:hash" or 64-char hex hash`,
231
+ )
232
+ }
233
+
234
+ // Parse composite format: workchain:address:lt:hash
235
+ const address = Address.parseRaw(`${parts[0]}:${parts[1]}`)
236
+ const lt = parts[2]
237
+ const txHash = parts[3]
238
+
239
+ // Get the latest block to use as reference
240
+ const lastBlock = await this.provider.getLastBlock()
241
+
242
+ // Get account transactions using V4 API
243
+ const account = await this.provider.getAccountLite(lastBlock.last.seqno, address)
244
+ if (!account.account.last) {
245
+ throw new CCIPTransactionNotFoundError(hash)
246
+ }
247
+
248
+ // Fetch transactions and find the one we're looking for
249
+ const txs = await this.provider.getAccountTransactions(
250
+ address,
251
+ BigInt(lt),
252
+ Buffer.from(txHash, 'hex'),
253
+ )
254
+
255
+ if (!txs || txs.length === 0) {
256
+ throw new CCIPTransactionNotFoundError(hash)
257
+ }
258
+
259
+ const tx = txs[0].tx
260
+ const txLt = Number(tx.lt)
261
+
262
+ // Cache lt → timestamp for later getBlockTimestamp lookups
263
+ this.ltTimestampCache.set(txLt, tx.now)
264
+
265
+ // Extract logs from outgoing external messages
266
+ const logs: Log_[] = []
267
+ const outMessages = tx.outMessages.values()
268
+ let index = 0
269
+ for (const msg of outMessages) {
270
+ if (msg.info.type === 'external-out') {
271
+ logs.push({
272
+ address: address.toRawString(),
273
+ topics: [],
274
+ data: msg.body.toBoc().toString('base64'),
275
+ blockNumber: txLt, // Note: This is lt (logical time), not block seqno
276
+ transactionHash: hash,
277
+ index: index,
278
+ })
279
+ }
280
+ index++
281
+ }
282
+
283
+ return {
284
+ hash,
285
+ logs,
286
+ blockNumber: txLt, // Note: This is lt (logical time), not block seqno
287
+ timestamp: tx.now,
288
+ from: address.toRawString(),
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Async generator that yields logs from TON transactions.
294
+ *
295
+ * Note: For TON, `startBlock` and `endBlock` in opts represent logical time (lt),
296
+ * not block sequence numbers. This is because TON transaction APIs are indexed by lt.
297
+ *
298
+ * @param opts - Log filter options (startBlock/endBlock are interpreted as lt values)
299
+ */
300
+ async *getLogs(opts: LogFilter & { versionAsHash?: boolean }): AsyncIterableIterator<Log_> {
301
+ const decoders: LogDecoders = {
302
+ tryDecodeAsMessage: (log) => TONChain.decodeMessage(log),
303
+ tryDecodeAsCommit: (log) => TONChain.decodeCommits(log as Log_),
304
+ }
305
+ yield* fetchLogs(this.provider, opts, this.ltTimestampCache, decoders)
306
+ }
307
+
308
+ /** {@inheritDoc Chain.fetchRequestsInTx} */
309
+ override async fetchRequestsInTx(tx: string | ChainTransaction): Promise<CCIPRequest[]> {
310
+ return fetchCCIPRequestsInTx(this, typeof tx === 'string' ? await this.getTransaction(tx) : tx)
311
+ }
312
+
313
+ /** {@inheritDoc Chain.fetchAllMessagesInBatch} */
314
+ override async fetchAllMessagesInBatch<
315
+ R extends PickDeep<
316
+ CCIPRequest,
317
+ 'lane' | `log.${'topics' | 'address' | 'blockNumber'}` | 'message.sequenceNumber'
318
+ >,
319
+ >(
320
+ _request: R,
321
+ _commit: Pick<CommitReport, 'minSeqNr' | 'maxSeqNr'>,
322
+ _opts?: { page?: number },
323
+ ): Promise<R['message'][]> {
324
+ return Promise.reject(new CCIPNotImplementedError('fetchAllMessagesInBatch'))
325
+ }
326
+
327
+ /** {@inheritDoc Chain.typeAndVersion} */
328
+ async typeAndVersion(
329
+ address: string,
330
+ ): Promise<
331
+ | [type_: string, version: string, typeAndVersion: string]
332
+ | [type_: string, version: string, typeAndVersion: string, suffix: string]
333
+ > {
334
+ const tonAddress = Address.parse(address)
335
+
336
+ // Get current block for state lookup
337
+ const lastBlock = await this.provider.getLastBlock()
338
+
339
+ // Call the typeAndVersion getter method on the contract
340
+ const result = await this.provider.runMethod(lastBlock.last.seqno, tonAddress, 'typeAndVersion')
341
+
342
+ // Parse the two string slices returned by the contract
343
+ // TON contracts return strings as cells with snake format encoding
344
+ const typeCell = result.reader.readCell()
345
+ const versionCell = result.reader.readCell()
346
+
347
+ // Load strings from cells using snake format
348
+ const contractType = typeCell.beginParse().loadStringTail()
349
+ const version = versionCell.beginParse().loadStringTail()
350
+
351
+ // Extract just the last part of the type (e.g., "OffRamp" from "com.chainlink.ton.ccip.OffRamp")
352
+ const typeParts = contractType.split('.')
353
+ const shortType = typeParts[typeParts.length - 1]
354
+
355
+ // Format as "Type Version" and use the common parser
356
+ const typeAndVersionStr = `${shortType} ${version}`
357
+
358
+ return parseTypeAndVersion(typeAndVersionStr) as
359
+ | [type_: string, version: string, typeAndVersion: string]
360
+ | [type_: string, version: string, typeAndVersion: string, suffix: string]
361
+ }
362
+
363
+ /** {@inheritDoc Chain.getRouterForOnRamp} */
364
+ async getRouterForOnRamp(onRamp: string, destChainSelector: bigint): Promise<string> {
365
+ const rawAddress = TONChain.getAddress(onRamp)
366
+ const onRampAddress = Address.parseRaw(rawAddress)
367
+
368
+ const onRampContract = OnRamp.createFromAddress(onRampAddress)
369
+ const openedContract = this.provider.open(onRampContract)
370
+ const destConfig = await openedContract.getDestChainConfig(destChainSelector)
371
+
372
+ return destConfig.router.toString()
373
+ }
374
+
375
+ /** {@inheritDoc Chain.getRouterForOffRamp} */
376
+ async getRouterForOffRamp(offRamp: string, sourceChainSelector: bigint): Promise<string> {
377
+ const offRampAddress = Address.parse(offRamp)
378
+ const offRampContract = OffRamp.createFromAddress(offRampAddress)
379
+ const openedContract = this.provider.open(offRampContract)
380
+
381
+ const sourceConfig = await openedContract.getSourceChainConfig(sourceChainSelector)
382
+ return sourceConfig.router.toString()
383
+ }
384
+
385
+ /** {@inheritDoc Chain.getNativeTokenForRouter} */
386
+ getNativeTokenForRouter(_router: string): Promise<string> {
387
+ return Promise.reject(new CCIPNotImplementedError('getNativeTokenForRouter'))
388
+ }
389
+
390
+ /** {@inheritDoc Chain.getOffRampsForRouter} */
391
+ async getOffRampsForRouter(router: string, sourceChainSelector: bigint): Promise<string[]> {
392
+ const routerAddress = Address.parse(router)
393
+ const routerContract = Router.createFromAddress(routerAddress)
394
+ const openedContract = this.provider.open(routerContract)
395
+
396
+ try {
397
+ // Get the specific OffRamp for the source chain selector
398
+ const offRamp = await openedContract.getOffRamp(sourceChainSelector)
399
+ return [offRamp.toString()]
400
+ } catch (error) {
401
+ if (isTvmError(error) && error.exitCode === 261) {
402
+ return [] // Return empty array if no OffRamp configured for this source chain
403
+ }
404
+ throw error
405
+ }
406
+ }
407
+
408
+ /** {@inheritDoc Chain.getOnRampForRouter} */
409
+ async getOnRampForRouter(router: string, destChainSelector: bigint): Promise<string> {
410
+ const routerAddress = Address.parse(router)
411
+ const routerContract = Router.createFromAddress(routerAddress)
412
+ const openedContract = this.provider.open(routerContract)
413
+
414
+ const onRamp = await openedContract.getOnRamp(destChainSelector)
415
+ return onRamp.toString()
416
+ }
417
+
418
+ /** {@inheritDoc Chain.getOnRampForOffRamp} */
419
+ async getOnRampForOffRamp(offRamp: string, sourceChainSelector: bigint): Promise<string> {
420
+ const offRampAddress = Address.parse(offRamp)
421
+ const offRampContract = OffRamp.createFromAddress(offRampAddress)
422
+ const openedContract = this.provider.open(offRampContract)
423
+
424
+ try {
425
+ const sourceConfig = await openedContract.getSourceChainConfig(sourceChainSelector)
426
+ // Convert CrossChainAddress (buffer) to checksummed EVM address
427
+ return checksumAddress('0x' + sourceConfig.onRamp.toString('hex'))
428
+ } catch (error) {
429
+ if (isTvmError(error) && error.exitCode === 266) {
430
+ throw new CCIPSourceChainUnsupportedError(sourceChainSelector, {
431
+ context: { offRamp },
432
+ })
433
+ }
434
+ throw error
435
+ }
436
+ }
437
+
438
+ /** {@inheritDoc Chain.getCommitStoreForOffRamp} */
439
+ async getCommitStoreForOffRamp(offRamp: string): Promise<string> {
440
+ // TODO: FIXME: check assumption
441
+ return Promise.resolve(offRamp)
442
+ }
443
+
444
+ /** {@inheritDoc Chain.getTokenForTokenPool} */
445
+ async getTokenForTokenPool(_tokenPool: string): Promise<string> {
446
+ return Promise.reject(new CCIPNotImplementedError('getTokenForTokenPool'))
447
+ }
448
+
449
+ /** {@inheritDoc Chain.getTokenInfo} */
450
+ async getTokenInfo(token: string): Promise<{ symbol: string; decimals: number }> {
451
+ const tokenAddress = Address.parse(token)
452
+ const lastBlock = await this.provider.getLastBlock()
453
+
454
+ try {
455
+ const result = await this.provider.runMethod(
456
+ lastBlock.last.seqno,
457
+ tokenAddress,
458
+ 'get_jetton_data',
459
+ )
460
+
461
+ // skips
462
+ result.reader.readBigNumber() // total_supply
463
+ result.reader.readBigNumber() // mintable
464
+ result.reader.readAddress() // admin_address
465
+
466
+ const contentCell = result.reader.readCell()
467
+ return parseJettonContent(contentCell, this.rateLimitedFetch, this.logger)
468
+ } catch (error) {
469
+ this.logger.debug?.(`Failed to get jetton data for ${token}:`, error)
470
+ return { symbol: '', decimals: 9 }
471
+ }
472
+ }
473
+
474
+ /** {@inheritDoc Chain.getTokenAdminRegistryFor} */
475
+ getTokenAdminRegistryFor(_address: string): Promise<string> {
476
+ return Promise.reject(new CCIPNotImplementedError('getTokenAdminRegistryFor'))
477
+ }
478
+
479
+ /**
480
+ * Decodes a CCIP message from a TON log event.
481
+ * @param log - Log with data field.
482
+ * @returns Decoded CCIPMessage or undefined if not valid.
483
+ */
484
+ static decodeMessage(log: Pick<Log_, 'data'>): CCIPMessage_V1_6_TON | undefined {
485
+ if (!log.data || typeof log.data !== 'string') return undefined
486
+
487
+ try {
488
+ // Parse BOC from base64
489
+ const boc = Buffer.from(log.data, 'base64')
490
+ const cell = Cell.fromBoc(boc)[0]
491
+ const slice = cell.beginParse()
492
+
493
+ // Load header fields directly (no topic prefix)
494
+ // Structure from TVM2AnyRampMessage:
495
+ // header: RampMessageHeader + sender: address + body: Cell + feeValueJuels: uint96
496
+ const header = {
497
+ messageId: '0x' + slice.loadUintBig(256).toString(16).padStart(64, '0'),
498
+ sourceChainSelector: slice.loadUintBig(64),
499
+ destChainSelector: slice.loadUintBig(64),
500
+ sequenceNumber: slice.loadUintBig(64),
501
+ nonce: slice.loadUintBig(64),
502
+ }
503
+
504
+ // Load sender address
505
+ const sender = slice.loadAddress()?.toString() ?? ''
506
+
507
+ // Load body cell ref
508
+ const bodyCell = slice.loadRef()
509
+
510
+ // Load feeValueJuels (96 bits) at message level, after body ref
511
+ const feeValueJuels = slice.loadUintBig(96)
512
+
513
+ // Parse body cell: TVM2AnyRampMessageBody
514
+ // Order: receiver (ref) + data (ref) + extraArgs (ref) + tokenAmounts (ref) + feeToken (inline) + feeTokenAmount (256 bits)
515
+ const bodySlice = bodyCell.beginParse()
516
+
517
+ // Load receiver from ref 0 (CrossChainAddress: length(8 bits) + bytes)
518
+ const receiverSlice = bodySlice.loadRef().beginParse()
519
+ const receiverLength = receiverSlice.loadUint(8)
520
+ const receiverBytes = receiverSlice.loadBuffer(receiverLength)
521
+
522
+ // Decode receiver address using destination chain's format
523
+ let receiver: string
524
+ try {
525
+ const destFamily = networkInfo(header.destChainSelector).family
526
+ receiver = decodeAddress(receiverBytes, destFamily)
527
+ } catch {
528
+ // Fallback to raw hex if chain not registered or decoding fails
529
+ receiver = '0x' + receiverBytes.toString('hex')
530
+ }
531
+
532
+ // Load data from ref 1
533
+ const dataSlice = bodySlice.loadRef().beginParse()
534
+ const dataBytes = dataSlice.loadBuffer(dataSlice.remainingBits / 8)
535
+ const data = '0x' + dataBytes.toString('hex')
536
+
537
+ // Load extraArgs from ref 2
538
+ const extraArgsCell = bodySlice.loadRef()
539
+ const extraArgsSlice = extraArgsCell.beginParse()
540
+
541
+ // Read tag (32 bits)
542
+ const extraArgsTag = extraArgsSlice.loadUint(32)
543
+ if (extraArgsTag !== Number(EVMExtraArgsV2Tag)) return undefined
544
+
545
+ // Read gasLimit (maybe uint256): 1 bit flag + 256 bits if present
546
+ const hasGasLimit = extraArgsSlice.loadBit()
547
+ const gasLimit = hasGasLimit ? extraArgsSlice.loadUintBig(256) : 0n
548
+
549
+ // Read allowOutOfOrderExecution (1 bit)
550
+ const allowOutOfOrderExecution = extraArgsSlice.loadBit()
551
+
552
+ // Build extraArgs as raw hex matching reference format
553
+ const tagHex = extraArgsTag.toString(16).padStart(8, '0')
554
+ const gasLimitHex = (hasGasLimit ? '8' : '0') + gasLimit.toString(16).padStart(63, '0')
555
+ const oooByte = allowOutOfOrderExecution ? '40' : '00'
556
+ const extraArgs = '0x' + tagHex + gasLimitHex + oooByte
557
+
558
+ // Load tokenAmounts from ref 3
559
+ const _tokenAmountsCell = bodySlice.loadRef()
560
+ const tokenAmounts: CCIPMessage_V1_6_TON['tokenAmounts'] = [] // TODO: FIXME: parse when implemented
561
+
562
+ // Load feeToken (inline address in body)
563
+ const feeToken = bodySlice.loadMaybeAddress()?.toString() ?? ''
564
+
565
+ // Load feeTokenAmount (256 bits)
566
+ const feeTokenAmount = bodySlice.loadUintBig(256)
567
+
568
+ return {
569
+ ...header,
570
+ sender,
571
+ receiver,
572
+ data,
573
+ tokenAmounts,
574
+ feeToken,
575
+ feeTokenAmount,
576
+ feeValueJuels,
577
+ extraArgs,
578
+ gasLimit,
579
+ allowOutOfOrderExecution,
580
+ }
581
+ } catch {
582
+ return undefined
583
+ }
584
+ }
585
+
586
+ /**
587
+ * Encodes extra args from TON messages into BOC serialization format.
588
+ *
589
+ * Currently only supports GenericExtraArgsV2 (EVMExtraArgsV2) encoding since TON
590
+ * lanes are only connected to EVM chains. When new lanes are planned to be added,
591
+ * this should be extended to support them (eg. Solana and SVMExtraArgsV1)
592
+ *
593
+ * @param args - Extra arguments containing gas limit and execution flags
594
+ * @returns Hex string of BOC-encoded extra args (0x-prefixed)
595
+ */
596
+ static encodeExtraArgs(args: ExtraArgs): string {
597
+ if (!args) return '0x'
598
+ if ('gasLimit' in args && 'allowOutOfOrderExecution' in args) {
599
+ const cell = beginCell()
600
+ .storeUint(Number(EVMExtraArgsV2Tag), 32) // magic tag
601
+ .storeUint(args.gasLimit, 256) // gasLimit
602
+ .storeBit(args.allowOutOfOrderExecution) // bool
603
+ .endCell()
604
+
605
+ // Return full BOC including headers
606
+ return '0x' + cell.toBoc().toString('hex')
607
+ }
608
+ return '0x'
609
+ }
610
+
611
+ /**
612
+ * Decodes BOC-encoded extra arguments from TON messages.
613
+ * Parses the BOC format and extracts extra args, validating the magic tag
614
+ * to ensure correct type. Returns undefined if parsing fails or tag doesn't match.
615
+ *
616
+ * Currently only supports EVMExtraArgsV2 (GenericExtraArgsV2) encoding since TON
617
+ * lanes are only connected to EVM chains. When new lanes are planned to be added,
618
+ * this should be extended to support them (eg. Solana and SVMExtraArgsV1)
619
+ *
620
+ * @param extraArgs - BOC-encoded extra args as hex string or bytes
621
+ * @returns Decoded EVMExtraArgsV2 (GenericExtraArgsV2) object or undefined if invalid
622
+ */
623
+ static decodeExtraArgs(
624
+ extraArgs: BytesLike,
625
+ ): (EVMExtraArgsV2 & { _tag: 'EVMExtraArgsV2' }) | undefined {
626
+ const data = Buffer.from(getDataBytes(extraArgs))
627
+
628
+ try {
629
+ // Parse BOC format to extract cell data
630
+ const cell = Cell.fromBoc(data)[0]
631
+ const slice = cell.beginParse()
632
+
633
+ // Load and verify magic tag to ensure correct extra args type
634
+ const magicTag = slice.loadUint(32)
635
+ if (magicTag !== Number(EVMExtraArgsV2Tag)) return undefined
636
+
637
+ return {
638
+ _tag: 'EVMExtraArgsV2',
639
+ gasLimit: slice.loadUintBig(256),
640
+ allowOutOfOrderExecution: slice.loadBit(),
641
+ }
642
+ } catch {
643
+ // Return undefined for any parsing errors (invalid BOC, malformed data, etc.)
644
+ return undefined
645
+ }
646
+ }
647
+
648
+ /**
649
+ * Decodes commit reports from a TON log event (CommitReportAccepted).
650
+ *
651
+ * @param log - Log with data field (base64-encoded BOC).
652
+ * @param lane - Optional lane info for filtering.
653
+ * @returns Array of CommitReport or undefined if not a valid commit event.
654
+ */
655
+ static decodeCommits(log: Log_, lane?: Lane): CommitReport[] | undefined {
656
+ if (!log.data || typeof log.data !== 'string') return undefined
657
+
658
+ try {
659
+ const boc = Buffer.from(log.data, 'base64')
660
+ const cell = Cell.fromBoc(boc)[0]
661
+ const slice = cell.beginParse()
662
+
663
+ // Cell body starts directly with hasMerkleRoot (topic is in message header)
664
+ const hasMerkleRoot = slice.loadBit()
665
+
666
+ if (!hasMerkleRoot) {
667
+ // No merkle root: could be price-only update, skip for now
668
+ return undefined
669
+ }
670
+
671
+ // Read MerkleRoot fields inline
672
+ const sourceChainSelector = slice.loadUintBig(64)
673
+ const onRampLen = slice.loadUint(8)
674
+
675
+ if (onRampLen === 0 || onRampLen > 32) {
676
+ // Invalid onRamp length
677
+ return undefined
678
+ }
679
+
680
+ const onRampBytes = slice.loadBuffer(onRampLen)
681
+ const minSeqNr = slice.loadUintBig(64)
682
+ const maxSeqNr = slice.loadUintBig(64)
683
+ const merkleRoot = '0x' + slice.loadUintBig(256).toString(16).padStart(64, '0')
684
+
685
+ // Read hasPriceUpdates (1 bit): we don't need the data but should consume it
686
+ if (slice.remainingBits >= 1) {
687
+ const hasPriceUpdates = slice.loadBit()
688
+ if (hasPriceUpdates && slice.remainingRefs > 0) {
689
+ slice.loadRef() // Skip price updates ref
690
+ }
691
+ }
692
+
693
+ const report: CommitReport = {
694
+ sourceChainSelector,
695
+ onRampAddress: '0x' + onRampBytes.toString('hex'),
696
+ minSeqNr,
697
+ maxSeqNr,
698
+ merkleRoot,
699
+ }
700
+
701
+ // Filter by lane if provided
702
+ if (lane) {
703
+ if (report.sourceChainSelector !== lane.sourceChainSelector) return undefined
704
+ if (report.onRampAddress?.toLowerCase() !== lane.onRamp?.toLowerCase()) return undefined
705
+ }
706
+
707
+ return [report]
708
+ } catch {
709
+ return undefined
710
+ }
711
+ }
712
+
713
+ /**
714
+ * Decodes an execution receipt from a TON log event.
715
+ * @param _log - Log with data field.
716
+ * @returns ExecutionReceipt or undefined if not valid.
717
+ */
718
+ static decodeReceipt(_log: Log_): ExecutionReceipt | undefined {
719
+ throw new CCIPNotImplementedError('decodeReceipt')
720
+ }
721
+
722
+ /**
723
+ * Converts bytes to a TON address.
724
+ * Handles:
725
+ * - 36-byte CCIP format: workchain(4 bytes, big-endian) + hash(32 bytes)
726
+ * - 33-byte format: workchain(1 byte) + hash(32 bytes)
727
+ * - 32-byte format: hash only (assumes workchain 0)
728
+ * Also handles user-friendly format strings (e.g., "EQ...", "UQ...", "kQ...", "0Q...")
729
+ * and raw format strings ("workchain:hash").
730
+ * @param bytes - Bytes or string to convert.
731
+ * @returns TON raw address string in format "workchain:hash".
732
+ */
733
+ static getAddress(bytes: BytesLike): string {
734
+ // If it's already a string address, try to parse and return raw format
735
+ if (typeof bytes === 'string') {
736
+ // Handle raw format "workchain:hash"
737
+ if (bytes.includes(':') && !bytes.startsWith('0x')) {
738
+ return bytes
739
+ }
740
+ // Handle user-friendly format (EQ..., UQ..., etc.)
741
+ if (
742
+ bytes.startsWith('EQ') ||
743
+ bytes.startsWith('UQ') ||
744
+ bytes.startsWith('kQ') ||
745
+ bytes.startsWith('0Q')
746
+ ) {
747
+ return Address.parse(bytes).toRawString()
748
+ }
749
+ }
750
+
751
+ const data = bytesToBuffer(bytes)
752
+
753
+ if (data.length === 36) {
754
+ // CCIP cross-chain format: workchain(4 bytes, big-endian) + hash(32 bytes)
755
+ const workchain = data.readInt32BE(0)
756
+ const hash = data.subarray(4).toString('hex')
757
+ return `${workchain}:${hash}`
758
+ } else if (data.length === 33) {
759
+ // workchain (1 byte) + hash (32 bytes)
760
+ const workchain = data[0] === 0xff ? -1 : data[0]
761
+ const hash = data.subarray(1).toString('hex')
762
+ return `${workchain}:${hash}`
763
+ } else if (data.length === 32) {
764
+ // hash only, assume workchain 0
765
+ return `0:${data.toString('hex')}`
766
+ } else {
767
+ throw new CCIPArgumentInvalidError(
768
+ 'bytes',
769
+ `Invalid TON address bytes length: ${data.length}. Expected 32, 33, or 36 bytes.`,
770
+ )
771
+ }
772
+ }
773
+
774
+ /**
775
+ * Formats a TON address for human-friendly display.
776
+ * Converts raw format (workchain:hash) to user-friendly format (EQ..., UQ..., etc.)
777
+ * @param address - Address in any recognized format
778
+ * @returns User-friendly TON address string
779
+ */
780
+ static formatAddress(address: string): string {
781
+ try {
782
+ // Parse the address (handles both raw and friendly formats)
783
+ const parsed = Address.parse(address)
784
+ // Return user-friendly format (bounceable by default)
785
+ return parsed.toString()
786
+ } catch {
787
+ // If parsing fails, return original
788
+ return address
789
+ }
790
+ }
791
+
792
+ /**
793
+ * Formats a TON transaction hash for human-friendly display.
794
+ * Extracts the raw 64-char hash from composite format for cleaner display.
795
+ * @param hash - Transaction hash in composite or raw format
796
+ * @returns The raw 64-char hex hash for display
797
+ */
798
+ static formatTxHash(hash: string): string {
799
+ const parts = hash.split(':')
800
+ if (parts.length === 4) {
801
+ // Composite format: workchain:address:lt:hash - return just the hash part
802
+ return parts[3]
803
+ }
804
+ // Already raw format or unknown - return as-is
805
+ return hash
806
+ }
807
+
808
+ /**
809
+ * Validates a transaction hash format for TON.
810
+ * Supports:
811
+ * - Raw 64-char hex hash (with or without 0x prefix)
812
+ * - Composite format: "workchain:address:lt:hash"
813
+ */
814
+ static isTxHash(v: unknown): v is string {
815
+ if (typeof v !== 'string') return false
816
+
817
+ // Check for raw 64-char hex hash (with or without 0x prefix)
818
+ const cleanHash = v.startsWith('0x') || v.startsWith('0X') ? v.slice(2) : v
819
+ if (/^[a-fA-F0-9]{64}$/.test(cleanHash)) {
820
+ return true
821
+ }
822
+
823
+ // Check for composite format: workchain:address:lt:hash
824
+ const parts = v.split(':')
825
+ if (parts.length === 4) {
826
+ const [workchain, address, lt, hash] = parts
827
+ // workchain should be a number (typically 0 or -1)
828
+ if (!/^-?\d+$/.test(workchain)) return false
829
+ // address should be 64-char hex
830
+ if (!/^[a-fA-F0-9]{64}$/.test(address)) return false
831
+ // lt should be a number
832
+ if (!/^\d+$/.test(lt)) return false
833
+ // hash should be 64-char hex
834
+ if (!/^[a-fA-F0-9]{64}$/.test(hash)) return false
835
+ return true
836
+ }
837
+
838
+ return false
839
+ }
840
+ /**
841
+ * Gets the leaf hasher for TON destination chains.
842
+ * @param lane - Lane configuration.
843
+ * @param _ctx - Context containing logger.
844
+ * @returns Leaf hasher function.
845
+ */
846
+ static getDestLeafHasher(lane: Lane, _ctx?: WithLogger): LeafHasher {
847
+ return getTONLeafHasher(lane)
848
+ }
849
+
850
+ /** {@inheritDoc Chain.getFee} */
851
+ async getFee(_router: string, _destChainSelector: bigint, _message: AnyMessage): Promise<bigint> {
852
+ return Promise.reject(new CCIPNotImplementedError('getFee'))
853
+ }
854
+
855
+ /** {@inheritDoc Chain.generateUnsignedSendMessage} */
856
+ generateUnsignedSendMessage(
857
+ _sender: string,
858
+ _router: string,
859
+ _destChainSelector: bigint,
860
+ _message: AnyMessage & { fee?: bigint },
861
+ _opts?: { approveMax?: boolean },
862
+ ): Promise<never> {
863
+ return Promise.reject(new CCIPNotImplementedError('generateUnsignedSendMessage'))
864
+ }
865
+
866
+ /** {@inheritDoc Chain.sendMessage} */
867
+ async sendMessage(
868
+ _router: string,
869
+ _destChainSelector: bigint,
870
+ _message: AnyMessage & { fee: bigint },
871
+ _opts?: { wallet?: unknown; approveMax?: boolean },
872
+ ): Promise<CCIPRequest> {
873
+ return Promise.reject(new CCIPNotImplementedError('sendMessage'))
874
+ }
875
+
876
+ /** {@inheritDoc Chain.fetchOffchainTokenData} */
877
+ fetchOffchainTokenData(request: CCIPRequest): Promise<OffchainTokenData[]> {
878
+ return Promise.resolve(request.message.tokenAmounts.map(() => undefined))
879
+ }
880
+
881
+ /** {@inheritDoc Chain.generateUnsignedExecuteReport} */
882
+ generateUnsignedExecuteReport(
883
+ _payer: string,
884
+ offRamp: string,
885
+ execReport: ExecutionReport,
886
+ opts?: { gasLimit?: number },
887
+ ): Promise<UnsignedTONTx> {
888
+ if (!('allowOutOfOrderExecution' in execReport.message && 'gasLimit' in execReport.message)) {
889
+ throw new CCIPExtraArgsInvalidError('TON')
890
+ }
891
+
892
+ const unsigned = generateUnsignedExecuteReportImpl(
893
+ offRamp,
894
+ execReport as ExecutionReport<CCIPMessage_V1_6_TON>,
895
+ opts,
896
+ )
897
+
898
+ return Promise.resolve({
899
+ family: ChainFamily.TON,
900
+ to: unsigned.to,
901
+ body: unsigned.body,
902
+ })
903
+ }
904
+
905
+ /** {@inheritDoc Chain.executeReport} */
906
+ async executeReport(
907
+ offRamp: string,
908
+ execReport: ExecutionReport,
909
+ opts: { wallet: unknown; gasLimit?: number },
910
+ ): Promise<ChainTransaction> {
911
+ const wallet = opts.wallet
912
+ if (!isTONWallet(wallet)) {
913
+ throw new CCIPWalletInvalidError(wallet)
914
+ }
915
+
916
+ const unsigned = await this.generateUnsignedExecuteReport(
917
+ wallet.contract.address.toString(),
918
+ offRamp,
919
+ execReport as ExecutionReport<CCIPMessage_V1_6_TON>,
920
+ opts,
921
+ )
922
+
923
+ // Open wallet and send transaction using the unsigned data
924
+ const openedWallet = this.provider.open(wallet.contract)
925
+ const seqno = await openedWallet.getSeqno()
926
+
927
+ await openedWallet.sendTransfer({
928
+ seqno,
929
+ secretKey: wallet.keyPair.secretKey,
930
+ messages: [
931
+ internal({
932
+ to: unsigned.to,
933
+ value: toNano('0.2'), // TODO: FIXME: estimate proper value for execution costs instead of hardcoding.
934
+ body: unsigned.body,
935
+ }),
936
+ ],
937
+ })
938
+
939
+ // Wait for transaction to be confirmed
940
+ const offRampAddress = Address.parse(offRamp)
941
+ const txInfo = await waitForTransaction(
942
+ this.provider,
943
+ wallet.contract.address,
944
+ seqno,
945
+ offRampAddress,
946
+ )
947
+
948
+ // Return composite hash in format "workchain:address:lt:hash"
949
+ const hash = `${wallet.contract.address.toRawString()}:${txInfo.lt}:${txInfo.hash}`
950
+ return this.getTransaction(hash)
951
+ }
952
+
953
+ /**
954
+ * Parses raw TON data into typed structures.
955
+ * @param data - Raw data to parse.
956
+ * @returns Parsed data or undefined.
957
+ */
958
+ static parse(data: unknown) {
959
+ if (isBytesLike(data)) {
960
+ const parsedExtraArgs = this.decodeExtraArgs(data)
961
+ if (parsedExtraArgs) return parsedExtraArgs
962
+ }
963
+ }
964
+
965
+ /** {@inheritDoc Chain.getSupportedTokens} */
966
+ async getSupportedTokens(_address: string): Promise<string[]> {
967
+ return Promise.reject(new CCIPNotImplementedError('getSupportedTokens'))
968
+ }
969
+
970
+ /** {@inheritDoc Chain.getRegistryTokenConfig} */
971
+ async getRegistryTokenConfig(_address: string, _tokenName: string): Promise<never> {
972
+ return Promise.reject(new CCIPNotImplementedError('getRegistryTokenConfig'))
973
+ }
974
+
975
+ /** {@inheritDoc Chain.getTokenPoolConfigs} */
976
+ async getTokenPoolConfigs(_tokenPool: string): Promise<never> {
977
+ return Promise.reject(new CCIPNotImplementedError('getTokenPoolConfigs'))
978
+ }
979
+
980
+ /** {@inheritDoc Chain.getTokenPoolRemotes} */
981
+ async getTokenPoolRemotes(_tokenPool: string): Promise<never> {
982
+ return Promise.reject(new CCIPNotImplementedError('getTokenPoolRemotes'))
983
+ }
984
+
985
+ /** {@inheritDoc Chain.getFeeTokens} */
986
+ async getFeeTokens(_router: string): Promise<never> {
987
+ return Promise.reject(new CCIPNotImplementedError('getFeeTokens'))
988
+ }
989
+ }