@chainlink/ccip-sdk 0.92.0 → 0.93.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 (223) hide show
  1. package/README.md +20 -18
  2. package/dist/api/index.d.ts +103 -0
  3. package/dist/api/index.d.ts.map +1 -0
  4. package/dist/api/index.js +141 -0
  5. package/dist/api/index.js.map +1 -0
  6. package/dist/api/types.d.ts +38 -0
  7. package/dist/api/types.d.ts.map +1 -0
  8. package/dist/api/types.js +2 -0
  9. package/dist/api/types.js.map +1 -0
  10. package/dist/aptos/index.d.ts +20 -33
  11. package/dist/aptos/index.d.ts.map +1 -1
  12. package/dist/aptos/index.js +34 -26
  13. package/dist/aptos/index.js.map +1 -1
  14. package/dist/aptos/logs.js +1 -1
  15. package/dist/aptos/logs.js.map +1 -1
  16. package/dist/aptos/token.js.map +1 -1
  17. package/dist/chain.d.ts +206 -71
  18. package/dist/chain.d.ts.map +1 -1
  19. package/dist/chain.js +89 -20
  20. package/dist/chain.js.map +1 -1
  21. package/dist/commits.d.ts +2 -2
  22. package/dist/commits.d.ts.map +1 -1
  23. package/dist/commits.js +4 -4
  24. package/dist/commits.js.map +1 -1
  25. package/dist/errors/CCIPError.d.ts.map +1 -1
  26. package/dist/errors/CCIPError.js +3 -2
  27. package/dist/errors/CCIPError.js.map +1 -1
  28. package/dist/errors/codes.d.ts +4 -1
  29. package/dist/errors/codes.d.ts.map +1 -1
  30. package/dist/errors/codes.js +6 -1
  31. package/dist/errors/codes.js.map +1 -1
  32. package/dist/errors/index.d.ts +5 -2
  33. package/dist/errors/index.d.ts.map +1 -1
  34. package/dist/errors/index.js +8 -2
  35. package/dist/errors/index.js.map +1 -1
  36. package/dist/errors/recovery.d.ts.map +1 -1
  37. package/dist/errors/recovery.js +4 -1
  38. package/dist/errors/recovery.js.map +1 -1
  39. package/dist/errors/specialized.d.ts +29 -4
  40. package/dist/errors/specialized.d.ts.map +1 -1
  41. package/dist/errors/specialized.js +48 -6
  42. package/dist/errors/specialized.js.map +1 -1
  43. package/dist/evm/errors.js.map +1 -1
  44. package/dist/evm/index.d.ts +24 -48
  45. package/dist/evm/index.d.ts.map +1 -1
  46. package/dist/evm/index.js +71 -59
  47. package/dist/evm/index.js.map +1 -1
  48. package/dist/evm/logs.js.map +1 -1
  49. package/dist/evm/offchain.js +3 -2
  50. package/dist/evm/offchain.js.map +1 -1
  51. package/dist/evm/viem/client-adapter.d.ts +68 -0
  52. package/dist/evm/viem/client-adapter.d.ts.map +1 -0
  53. package/dist/evm/viem/client-adapter.js +104 -0
  54. package/dist/evm/viem/client-adapter.js.map +1 -0
  55. package/dist/evm/viem/index.d.ts +29 -0
  56. package/dist/evm/viem/index.d.ts.map +1 -0
  57. package/dist/evm/viem/index.js +28 -0
  58. package/dist/evm/viem/index.js.map +1 -0
  59. package/dist/evm/viem/types.d.ts +13 -0
  60. package/dist/evm/viem/types.d.ts.map +1 -0
  61. package/dist/evm/viem/types.js +2 -0
  62. package/dist/evm/viem/types.js.map +1 -0
  63. package/dist/evm/viem/wallet-adapter.d.ts +58 -0
  64. package/dist/evm/viem/wallet-adapter.d.ts.map +1 -0
  65. package/dist/evm/viem/wallet-adapter.js +197 -0
  66. package/dist/evm/viem/wallet-adapter.js.map +1 -0
  67. package/dist/execution.d.ts +1 -1
  68. package/dist/execution.d.ts.map +1 -1
  69. package/dist/execution.js +2 -2
  70. package/dist/execution.js.map +1 -1
  71. package/dist/explorer.d.ts +74 -0
  72. package/dist/explorer.d.ts.map +1 -0
  73. package/dist/explorer.js +67 -0
  74. package/dist/explorer.js.map +1 -0
  75. package/dist/gas.js.map +1 -1
  76. package/dist/hasher/merklemulti.js.map +1 -1
  77. package/dist/http-status.d.ts +20 -0
  78. package/dist/http-status.d.ts.map +1 -0
  79. package/dist/http-status.js +25 -0
  80. package/dist/http-status.js.map +1 -0
  81. package/dist/index.d.ts +11 -4
  82. package/dist/index.d.ts.map +1 -1
  83. package/dist/index.js +5 -3
  84. package/dist/index.js.map +1 -1
  85. package/dist/offchain.js.map +1 -1
  86. package/dist/requests.d.ts +4 -4
  87. package/dist/requests.d.ts.map +1 -1
  88. package/dist/requests.js +6 -6
  89. package/dist/requests.js.map +1 -1
  90. package/dist/solana/cleanup.js +2 -2
  91. package/dist/solana/cleanup.js.map +1 -1
  92. package/dist/solana/exec.js +1 -5
  93. package/dist/solana/exec.js.map +1 -1
  94. package/dist/solana/index.d.ts +28 -57
  95. package/dist/solana/index.d.ts.map +1 -1
  96. package/dist/solana/index.js +66 -70
  97. package/dist/solana/index.js.map +1 -1
  98. package/dist/solana/logs.js +2 -2
  99. package/dist/solana/logs.js.map +1 -1
  100. package/dist/solana/offchain.js +3 -3
  101. package/dist/solana/offchain.js.map +1 -1
  102. package/dist/solana/send.js +1 -1
  103. package/dist/solana/send.js.map +1 -1
  104. package/dist/solana/utils.js +1 -1
  105. package/dist/solana/utils.js.map +1 -1
  106. package/dist/sui/discovery.d.ts.map +1 -1
  107. package/dist/sui/discovery.js +2 -5
  108. package/dist/sui/discovery.js.map +1 -1
  109. package/dist/sui/events.d.ts.map +1 -1
  110. package/dist/sui/events.js +2 -8
  111. package/dist/sui/events.js.map +1 -1
  112. package/dist/sui/index.d.ts +18 -29
  113. package/dist/sui/index.d.ts.map +1 -1
  114. package/dist/sui/index.js +24 -26
  115. package/dist/sui/index.js.map +1 -1
  116. package/dist/sui/manuallyExec/encoder.d.ts.map +1 -1
  117. package/dist/sui/manuallyExec/encoder.js +0 -3
  118. package/dist/sui/manuallyExec/encoder.js.map +1 -1
  119. package/dist/sui/manuallyExec/index.d.ts.map +1 -1
  120. package/dist/sui/manuallyExec/index.js +1 -2
  121. package/dist/sui/manuallyExec/index.js.map +1 -1
  122. package/dist/sui/objects.js +4 -4
  123. package/dist/sui/objects.js.map +1 -1
  124. package/dist/ton/exec.d.ts +2 -2
  125. package/dist/ton/exec.d.ts.map +1 -1
  126. package/dist/ton/exec.js.map +1 -1
  127. package/dist/ton/hasher.js +5 -5
  128. package/dist/ton/hasher.js.map +1 -1
  129. package/dist/ton/index.d.ts +54 -49
  130. package/dist/ton/index.d.ts.map +1 -1
  131. package/dist/ton/index.js +331 -236
  132. package/dist/ton/index.js.map +1 -1
  133. package/dist/ton/logs.d.ts +11 -22
  134. package/dist/ton/logs.d.ts.map +1 -1
  135. package/dist/ton/logs.js +95 -118
  136. package/dist/ton/logs.js.map +1 -1
  137. package/dist/ton/types.d.ts +9 -9
  138. package/dist/ton/types.d.ts.map +1 -1
  139. package/dist/ton/types.js +5 -9
  140. package/dist/ton/types.js.map +1 -1
  141. package/dist/ton/utils.d.ts +8 -27
  142. package/dist/ton/utils.d.ts.map +1 -1
  143. package/dist/ton/utils.js +31 -111
  144. package/dist/ton/utils.js.map +1 -1
  145. package/dist/types.d.ts +34 -0
  146. package/dist/types.d.ts.map +1 -1
  147. package/dist/types.js +30 -0
  148. package/dist/types.js.map +1 -1
  149. package/dist/utils.d.ts +6 -5
  150. package/dist/utils.d.ts.map +1 -1
  151. package/dist/utils.js +76 -52
  152. package/dist/utils.js.map +1 -1
  153. package/dist/viem.d.ts +6 -0
  154. package/dist/viem.d.ts.map +1 -0
  155. package/dist/viem.js +6 -0
  156. package/dist/viem.js.map +1 -0
  157. package/package.json +18 -3
  158. package/src/api/index.ts +167 -0
  159. package/src/api/types.ts +39 -0
  160. package/src/aptos/index.ts +57 -64
  161. package/src/aptos/logs.ts +10 -10
  162. package/src/aptos/token.ts +1 -1
  163. package/src/chain.ts +274 -97
  164. package/src/commits.ts +5 -5
  165. package/src/errors/CCIPError.ts +5 -2
  166. package/src/errors/codes.ts +8 -1
  167. package/src/errors/index.ts +15 -2
  168. package/src/errors/recovery.ts +9 -1
  169. package/src/errors/specialized.ts +61 -6
  170. package/src/evm/errors.ts +2 -2
  171. package/src/evm/index.ts +107 -120
  172. package/src/evm/logs.ts +4 -4
  173. package/src/evm/offchain.ts +5 -4
  174. package/src/evm/viem/client-adapter.ts +124 -0
  175. package/src/evm/viem/index.ts +29 -0
  176. package/src/evm/viem/types.ts +14 -0
  177. package/src/evm/viem/wallet-adapter.ts +233 -0
  178. package/src/execution.ts +9 -9
  179. package/src/explorer.ts +90 -0
  180. package/src/gas.ts +2 -2
  181. package/src/hasher/merklemulti.ts +7 -7
  182. package/src/http-status.ts +31 -0
  183. package/src/index.ts +19 -1
  184. package/src/offchain.ts +1 -1
  185. package/src/requests.ts +9 -12
  186. package/src/solana/cleanup.ts +4 -4
  187. package/src/solana/exec.ts +13 -18
  188. package/src/solana/index.ts +92 -117
  189. package/src/solana/logs.ts +8 -8
  190. package/src/solana/offchain.ts +3 -3
  191. package/src/solana/send.ts +20 -20
  192. package/src/solana/utils.ts +4 -4
  193. package/src/sui/discovery.ts +4 -10
  194. package/src/sui/events.ts +5 -12
  195. package/src/sui/index.ts +36 -48
  196. package/src/sui/manuallyExec/encoder.ts +0 -4
  197. package/src/sui/manuallyExec/index.ts +1 -3
  198. package/src/sui/objects.ts +14 -14
  199. package/src/ton/exec.ts +2 -5
  200. package/src/ton/hasher.ts +5 -5
  201. package/src/ton/index.ts +392 -316
  202. package/src/ton/logs.ts +122 -143
  203. package/src/ton/types.ts +17 -21
  204. package/src/ton/utils.ts +39 -145
  205. package/src/types.ts +36 -0
  206. package/src/utils.ts +96 -66
  207. package/src/viem.ts +5 -0
  208. package/tsconfig.json +3 -2
  209. package/dist/ton/bindings/offramp.d.ts +0 -48
  210. package/dist/ton/bindings/offramp.d.ts.map +0 -1
  211. package/dist/ton/bindings/offramp.js +0 -63
  212. package/dist/ton/bindings/offramp.js.map +0 -1
  213. package/dist/ton/bindings/onramp.d.ts +0 -40
  214. package/dist/ton/bindings/onramp.d.ts.map +0 -1
  215. package/dist/ton/bindings/onramp.js +0 -51
  216. package/dist/ton/bindings/onramp.js.map +0 -1
  217. package/dist/ton/bindings/router.d.ts +0 -47
  218. package/dist/ton/bindings/router.d.ts.map +0 -1
  219. package/dist/ton/bindings/router.js +0 -51
  220. package/dist/ton/bindings/router.js.map +0 -1
  221. package/src/ton/bindings/offramp.ts +0 -96
  222. package/src/ton/bindings/onramp.ts +0 -72
  223. package/src/ton/bindings/router.ts +0 -65
package/src/ton/index.ts CHANGED
@@ -1,25 +1,28 @@
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'
1
+ import { type Transaction, Address, Cell, beginCell, toNano } from '@ton/core'
2
+ import { TonClient } from '@ton/ton'
3
+ import { type AxiosAdapter, getAdapter } from 'axios'
4
+ import { type BytesLike, hexlify, isBytesLike, isHexString, toBeArray, toBeHex } from 'ethers'
5
+ import { type Memoized, memoize } from 'micro-memoize'
5
6
  import type { PickDeep } from 'type-fest'
6
7
 
7
- import { type LogDecoders, fetchLogs } from './logs.ts'
8
- import { type LogFilter, Chain } from '../chain.ts'
8
+ import { streamTransactionsForAddress } from './logs.ts'
9
+ import { type ChainContext, type LogFilter, Chain } from '../chain.ts'
9
10
  import {
10
11
  CCIPArgumentInvalidError,
11
12
  CCIPExtraArgsInvalidError,
12
13
  CCIPHttpError,
13
14
  CCIPNotImplementedError,
15
+ CCIPReceiptNotFoundError,
14
16
  CCIPSourceChainUnsupportedError,
17
+ CCIPTopicsInvalidError,
15
18
  CCIPTransactionNotFoundError,
16
19
  CCIPWalletInvalidError,
17
20
  } from '../errors/specialized.ts'
18
21
  import { type EVMExtraArgsV2, type ExtraArgs, EVMExtraArgsV2Tag } from '../extra-args.ts'
19
- import { fetchCCIPRequestsInTx } from '../requests.ts'
22
+ import { getMessagesInTx } from '../requests.ts'
20
23
  import { supportedChains } from '../supported-chains.ts'
21
24
  import {
22
- type AnyMessage,
25
+ type CCIPExecution,
23
26
  type CCIPRequest,
24
27
  type ChainTransaction,
25
28
  type CommitReport,
@@ -31,23 +34,22 @@ import {
31
34
  type OffchainTokenData,
32
35
  type WithLogger,
33
36
  ChainFamily,
37
+ ExecutionState,
34
38
  } from '../types.ts'
35
39
  import {
36
40
  bytesToBuffer,
37
41
  createRateLimitedFetch,
38
42
  decodeAddress,
39
- getDataBytes,
40
43
  networkInfo,
41
44
  parseTypeAndVersion,
45
+ sleep,
42
46
  } 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
47
  import { generateUnsignedExecuteReport as generateUnsignedExecuteReportImpl } from './exec.ts'
47
48
  import { getTONLeafHasher } from './hasher.ts'
48
49
  import { type CCIPMessage_V1_6_TON, type UnsignedTONTx, isTONWallet } from './types.ts'
49
- import { lookupTxByRawHash, parseJettonContent, waitForTransaction } from './utils.ts'
50
+ import { crc32, lookupTxByRawHash, parseJettonContent } from './utils.ts'
50
51
  import type { LeafHasher } from '../hasher/common.ts'
52
+ export type { TONWallet, UnsignedTONTx } from './types.ts'
51
53
 
52
54
  /**
53
55
  * Type guard to check if an error is a TVM error with an exit code.
@@ -74,13 +76,8 @@ export class TONChain extends Chain<typeof ChainFamily.TON> {
74
76
  }
75
77
  static readonly family = ChainFamily.TON
76
78
  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()
79
+ readonly rateLimitedFetch: typeof fetch
80
+ readonly provider: TonClient
84
81
 
85
82
  /**
86
83
  * Creates a new TONChain instance.
@@ -88,74 +85,128 @@ export class TONChain extends Chain<typeof ChainFamily.TON> {
88
85
  * @param network - Network information for this chain.
89
86
  * @param ctx - Context containing logger.
90
87
  */
91
- constructor(client: TonClient4, network: NetworkInfo, ctx?: WithLogger) {
88
+ constructor(
89
+ client: TonClient,
90
+ network: NetworkInfo,
91
+ ctx?: ChainContext & { fetchFn?: typeof fetch },
92
+ ) {
92
93
  super(network, ctx)
93
94
  this.provider = client
94
95
 
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)
96
+ const txCache = new Map<string, Transaction[]>()
97
+ const txDepleted: Record<string, boolean> = {}
98
+ const origGetTransactions = this.provider.getTransactions.bind(this.provider)
99
+ // cached getTransactions, used for getLogs
100
+ this.provider.getTransactions = async (
101
+ address: Address,
102
+ opts: Parameters<typeof this.provider.getTransactions>[1],
103
+ ): Promise<Transaction[]> => {
104
+ const key = address.toString()
105
+ let allTxs
106
+ if (txCache.has(key)) {
107
+ allTxs = txCache.get(key)!
108
+ } else {
109
+ allTxs = [] as Transaction[]
110
+ txCache.set(key, allTxs)
111
+ }
112
+ let txs
113
+ if (!opts.hash) {
114
+ // if no cursor, always fetch most recent transactions
115
+ txs = await origGetTransactions(address, opts)
116
+ } else {
117
+ const hash = opts.hash
118
+ // otherwise, look to see if we have it already cached
119
+ let idx = allTxs.findIndex((tx) => tx.hash().toString('base64') === hash)
120
+ if (idx >= 0 && !opts.inclusive) idx++ // skip first if not inclusive
121
+ // if found, and we have more than requested limit in cache, or we'd previously reached bottom of address
122
+ if (idx >= 0 && (allTxs.length - idx >= opts.limit || txDepleted[key])) {
123
+ return allTxs.slice(idx, idx + opts.limit) // return cached
124
+ }
125
+ // otherwise, fetch after end
126
+ txs = await origGetTransactions(address, opts)
127
+ }
128
+ // add/merge unique/new/unseen txs to allTxs
129
+ const allTxsHashes = new Set(allTxs.map((tx) => tx.hash().toString('base64')))
130
+ allTxs.push(...txs.filter((tx) => !allTxsHashes.has(tx.hash().toString('base64'))))
131
+ allTxs.sort((a, b) => Number(b.lt - a.lt)) // merge sorted inverse order
132
+ if (txs.length < opts.limit) txDepleted[key] = true // bottom reached
133
+ return txs
105
134
  }
106
135
 
136
+ // Rate-limited fetch for TonCenter API (public tier: ~1 req/sec)
137
+ this.rateLimitedFetch =
138
+ ctx?.fetchFn ?? createRateLimitedFetch({ maxRequests: 1, windowMs: 1500, maxRetries: 5 }, ctx)
139
+
107
140
  this.getTransaction = memoize(this.getTransaction.bind(this), {
108
141
  maxSize: 100,
109
142
  })
143
+
144
+ this.getBlockTimestamp = memoize(this.getBlockTimestamp.bind(this), {
145
+ async: true,
146
+ maxArgs: 1,
147
+ maxSize: 100,
148
+ forceUpdate: ([k]) => typeof k !== 'number' || k <= 0,
149
+ })
150
+
151
+ this.typeAndVersion = memoize(this.typeAndVersion.bind(this), {
152
+ maxArgs: 1,
153
+ async: true,
154
+ })
155
+ }
156
+
157
+ /**
158
+ * Detect client network and instantiate a TONChain instance.
159
+ */
160
+ static async fromClient(
161
+ client: TonClient,
162
+ ctx?: ChainContext & { fetchFn?: typeof fetch },
163
+ ): Promise<TONChain> {
164
+ // Verify connection by getting the latest block
165
+ const isTestnet =
166
+ (
167
+ await client.getContractState(
168
+ Address.parse('EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs'), // mainnet USDT
169
+ )
170
+ ).state !== 'active'
171
+ return new TONChain(client, networkInfo(isTestnet ? 'ton-testnet' : 'ton-mainnet'), ctx)
110
172
  }
111
173
 
112
174
  /**
113
175
  * Creates a TONChain instance from an RPC URL.
114
176
  * Verifies the connection and detects the network.
115
177
  *
116
- * @param url - RPC endpoint URL for TonClient4.
178
+ * @param url - RPC endpoint URL for TonClient (v2).
117
179
  * @param ctx - Context containing logger.
118
180
  * @returns A new TONChain instance.
119
181
  */
120
- static async fromUrl(url: string, ctx?: WithLogger): Promise<TONChain> {
182
+ static async fromUrl(url: string, ctx?: ChainContext): Promise<TONChain> {
121
183
  const { logger = console } = ctx ?? {}
184
+ if (!url.endsWith('/jsonRPC')) url += '/jsonRPC'
122
185
 
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}`)
186
+ let fetchFn
187
+ let httpAdapter
188
+ if (['toncenter.com', 'tonapi.io'].some((d) => url.includes(d))) {
189
+ logger.warn(
190
+ 'Public TONCenter API calls are rate-limited to ~1 req/sec, some commands may be slow',
191
+ )
192
+ fetchFn = createRateLimitedFetch({ maxRequests: 1, windowMs: 1500, maxRetries: 5 }, ctx)
193
+ httpAdapter = (getAdapter as (name: string, config: object) => AxiosAdapter)('fetch', {
194
+ env: { fetch: fetchFn },
195
+ })
129
196
  }
130
197
 
131
- const hostname = parsedUrl.hostname.toLowerCase()
132
- const client = new TonClient4({ endpoint: url })
133
-
134
- // Verify connection by getting the latest block
198
+ const client = new TonClient({ endpoint: url, httpAdapter })
135
199
  try {
136
- await client.getLastBlock()
137
- logger.debug?.(`Connected to TON V4 endpoint: ${url}`)
200
+ const chain = await this.fromClient(client, {
201
+ ...ctx,
202
+ fetchFn,
203
+ })
204
+ logger.debug(`Connected to TON V2 endpoint: ${url}`)
205
+ return chain
138
206
  } catch (error) {
139
207
  const message = error instanceof Error ? error.message : String(error)
140
- throw new CCIPHttpError(0, `Failed to connect to TON V4 endpoint ${url}: ${message}`)
208
+ throw new CCIPHttpError(0, `Failed to connect to TONv2 endpoint ${url}: ${message}`)
141
209
  }
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
210
  }
160
211
 
161
212
  /**
@@ -169,16 +220,8 @@ export class TONChain extends Chain<typeof ChainFamily.TON> {
169
220
  * @returns Unix timestamp in seconds
170
221
  */
171
222
  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
223
+ if (typeof block != 'number') {
224
+ return Promise.resolve(Math.floor(Date.now() / 1000))
182
225
  }
183
226
 
184
227
  // For TON, we cannot look up timestamp by lt alone without the account address.
@@ -196,97 +239,97 @@ export class TONChain extends Chain<typeof ChainFamily.TON> {
196
239
  * 1. Composite format: "workchain:address:lt:hash" (e.g., "0:abc123...def:12345:abc123...def")
197
240
  * 2. Raw hash format: 64-character hex string resolved via TonCenter V3 API
198
241
  *
199
- * Note: TON's V4 API requires (address, lt, hash) for lookups. Raw hash lookups
242
+ * Note: TonClient requires (address, lt, hash) for lookups. Raw hash lookups
200
243
  * use TonCenter's V3 index API to resolve the hash to a full identifier first.
201
244
  *
202
- * @param hash - Transaction identifier in either format
245
+ * @param tx - Transaction identifier in either format
203
246
  * @returns ChainTransaction with transaction details
204
247
  * Note: `blockNumber` contains logical time (lt), not block seqno
205
248
  */
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
249
+ async getTransaction(tx: string | Transaction): Promise<ChainTransaction> {
250
+ let address
251
+ if (typeof tx === 'string') {
252
+ let parts = tx.split(':')
253
+
254
+ // If not composite format (4 parts), check if it's a raw 64-char hex hash
255
+ if (parts.length !== 4) {
256
+ const cleanHash = tx.startsWith('0x') || tx.startsWith('0X') ? tx.slice(2) : tx
257
+
258
+ if (!/^[a-fA-F0-9]{64}$/.test(cleanHash))
259
+ throw new CCIPArgumentInvalidError(
260
+ 'hash',
261
+ `Invalid TON transaction hash format: "${tx}". Expected "workchain:address:lt:hash" or 64-char hex hash`,
262
+ )
215
263
  const txInfo = await lookupTxByRawHash(
216
264
  cleanHash,
217
- isTestnet,
265
+ this.network.isTestnet,
218
266
  this.rateLimitedFetch,
219
- this.logger,
267
+ this,
220
268
  )
221
269
 
222
- const compositeHash = `${txInfo.account}:${txInfo.lt}:${cleanHash}`
223
- this.logger.debug?.(`Resolved raw hash to composite: ${compositeHash}`)
224
-
225
- return this.getTransaction(compositeHash)
270
+ tx = `${txInfo.account}:${txInfo.lt}:${cleanHash}`
271
+ this.logger.debug(`Resolved raw hash to composite: ${tx}`)
272
+ parts = tx.split(':')
226
273
  }
227
274
 
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]
275
+ // Parse composite format: workchain:address:lt:hash
276
+ address = Address.parseRaw(`${parts[0]}:${parts[1]}`)
277
+ const [, , lt, txHash] = parts as [string, string, string, string]
238
278
 
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)
279
+ // Fetch transactions and find the one we're looking for
280
+ const tx_ = await this.provider.getTransaction(
281
+ address,
282
+ lt,
283
+ Buffer.from(txHash, 'hex').toString('base64'),
284
+ )
285
+ if (!tx_) throw new CCIPTransactionNotFoundError(tx)
286
+ tx = tx_
287
+ } else {
288
+ address = new Address(0, Buffer.from(toBeArray(tx.address, 32)))
257
289
  }
258
290
 
259
- const tx = txs[0].tx
260
- const txLt = Number(tx.lt)
261
-
262
291
  // Cache lt → timestamp for later getBlockTimestamp lookups
263
- this.ltTimestampCache.set(txLt, tx.now)
292
+ ;(this.getBlockTimestamp as Memoized<typeof this.getBlockTimestamp, { async: true }>).cache.set(
293
+ [Number(tx.lt)],
294
+ Promise.resolve(tx.now),
295
+ )
264
296
 
265
297
  // 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
298
+ // Build composite hash format: workchain:address:lt:hash
299
+ const compositeHash = `${address.toRawString()}:${tx.lt}:${tx.hash().toString('hex')}`
300
+ const res = {
301
+ hash: compositeHash,
302
+ logs: [] as Log_[],
303
+ blockNumber: Number(tx.lt), // Note: This is lt (logical time), not block seqno
287
304
  timestamp: tx.now,
288
305
  from: address.toRawString(),
306
+ tx,
289
307
  }
308
+ const logs: Log_[] = []
309
+ for (const [index, msg] of tx.outMessages) {
310
+ if (msg.info.type !== 'external-out') continue
311
+ const topics = []
312
+ // logs are external messages where dest "address" is the uint32 topic (e.g. crc32("ExecutionStateChanged"))
313
+ if (msg.info.dest && msg.info.dest.value > 0n && msg.info.dest.value < 2n ** 32n)
314
+ topics.push(toBeHex(msg.info.dest.value, 4))
315
+ let data = ''
316
+ try {
317
+ data = msg.body.toBoc().toString('base64')
318
+ } catch (_) {
319
+ // ignore
320
+ }
321
+ logs.push({
322
+ address: msg.info.src.toRawString(),
323
+ topics,
324
+ data,
325
+ blockNumber: res.blockNumber, // Note: This is lt (logical time), not block seqno
326
+ transactionHash: res.hash,
327
+ index,
328
+ tx: res,
329
+ })
330
+ }
331
+ res.logs = logs
332
+ return res
290
333
  }
291
334
 
292
335
  /**
@@ -297,21 +340,34 @@ export class TONChain extends Chain<typeof ChainFamily.TON> {
297
340
  *
298
341
  * @param opts - Log filter options (startBlock/endBlock are interpreted as lt values)
299
342
  */
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_),
343
+ async *getLogs(opts: LogFilter): AsyncIterableIterator<Log_> {
344
+ let topics
345
+ if (opts.topics?.length) {
346
+ if (!opts.topics.every((topic) => typeof topic === 'string'))
347
+ throw new CCIPTopicsInvalidError(opts.topics)
348
+ // append events discriminants (if not 0x-8B already), but keep OG topics
349
+ topics = new Set([
350
+ ...opts.topics,
351
+ ...opts.topics.filter((t) => !isHexString(t, 8)).map((t) => crc32(t)),
352
+ ])
353
+ }
354
+ for await (const tx of streamTransactionsForAddress(opts, this)) {
355
+ const logs =
356
+ opts.startBlock == null && opts.startTime == null ? tx.logs.toReversed() : tx.logs
357
+ for (const log of logs) {
358
+ if (topics && !topics.has(log.topics[0]!)) continue
359
+ yield log
360
+ }
304
361
  }
305
- yield* fetchLogs(this.provider, opts, this.ltTimestampCache, decoders)
306
362
  }
307
363
 
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)
364
+ /** {@inheritDoc Chain.getMessagesInTx} */
365
+ override async getMessagesInTx(tx: string | ChainTransaction): Promise<CCIPRequest[]> {
366
+ return getMessagesInTx(this, typeof tx === 'string' ? await this.getTransaction(tx) : tx)
311
367
  }
312
368
 
313
- /** {@inheritDoc Chain.fetchAllMessagesInBatch} */
314
- override async fetchAllMessagesInBatch<
369
+ /** {@inheritDoc Chain.getMessagesInBatch} */
370
+ override async getMessagesInBatch<
315
371
  R extends PickDeep<
316
372
  CCIPRequest,
317
373
  'lane' | `log.${'topics' | 'address' | 'blockNumber'}` | 'message.sequenceNumber'
@@ -321,28 +377,20 @@ export class TONChain extends Chain<typeof ChainFamily.TON> {
321
377
  _commit: Pick<CommitReport, 'minSeqNr' | 'maxSeqNr'>,
322
378
  _opts?: { page?: number },
323
379
  ): Promise<R['message'][]> {
324
- return Promise.reject(new CCIPNotImplementedError('fetchAllMessagesInBatch'))
380
+ return Promise.reject(new CCIPNotImplementedError('getMessagesInBatch'))
325
381
  }
326
382
 
327
383
  /** {@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
- > {
384
+ async typeAndVersion(address: string) {
334
385
  const tonAddress = Address.parse(address)
335
386
 
336
- // Get current block for state lookup
337
- const lastBlock = await this.provider.getLastBlock()
338
-
339
387
  // Call the typeAndVersion getter method on the contract
340
- const result = await this.provider.runMethod(lastBlock.last.seqno, tonAddress, 'typeAndVersion')
388
+ const result = await this.provider.runMethod(tonAddress, 'typeAndVersion')
341
389
 
342
390
  // Parse the two string slices returned by the contract
343
391
  // TON contracts return strings as cells with snake format encoding
344
- const typeCell = result.reader.readCell()
345
- const versionCell = result.reader.readCell()
392
+ const typeCell = result.stack.readCell()
393
+ const versionCell = result.stack.readCell()
346
394
 
347
395
  // Load strings from cells using snake format
348
396
  const contractType = typeCell.beginParse().loadStringTail()
@@ -355,31 +403,25 @@ export class TONChain extends Chain<typeof ChainFamily.TON> {
355
403
  // Format as "Type Version" and use the common parser
356
404
  const typeAndVersionStr = `${shortType} ${version}`
357
405
 
358
- return parseTypeAndVersion(typeAndVersionStr) as
359
- | [type_: string, version: string, typeAndVersion: string]
360
- | [type_: string, version: string, typeAndVersion: string, suffix: string]
406
+ return parseTypeAndVersion(typeAndVersionStr)
361
407
  }
362
408
 
363
409
  /** {@inheritDoc Chain.getRouterForOnRamp} */
364
410
  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()
411
+ const { stack: destConfig } = await this.provider.runMethod(
412
+ Address.parse(onRamp),
413
+ 'destChainConfig',
414
+ [{ type: 'int', value: destChainSelector }],
415
+ )
416
+ return destConfig.readAddress().toRawString()
373
417
  }
374
418
 
375
419
  /** {@inheritDoc Chain.getRouterForOffRamp} */
376
420
  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()
421
+ const { stack } = await this.provider.runMethod(Address.parse(offRamp), 'sourceChainConfig', [
422
+ { type: 'int', value: sourceChainSelector },
423
+ ])
424
+ return stack.readAddress().toRawString()
383
425
  }
384
426
 
385
427
  /** {@inheritDoc Chain.getNativeTokenForRouter} */
@@ -389,42 +431,54 @@ export class TONChain extends Chain<typeof ChainFamily.TON> {
389
431
 
390
432
  /** {@inheritDoc Chain.getOffRampsForRouter} */
391
433
  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
- }
434
+ const routerContract = this.provider.provider(Address.parse(router))
435
+ // Get the specific OffRamp for the source chain selector
436
+ const { stack } = await routerContract.get('offRamp', [
437
+ { type: 'int', value: sourceChainSelector },
438
+ ])
439
+ return [stack.readAddress().toRawString()]
406
440
  }
407
441
 
408
442
  /** {@inheritDoc Chain.getOnRampForRouter} */
409
443
  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()
444
+ const routerContract = this.provider.provider(Address.parse(router))
445
+ // Get the specific OnRamp for the source chain selector
446
+ const { stack } = await routerContract.get('onRamp', [
447
+ { type: 'int', value: destChainSelector },
448
+ ])
449
+ return stack.readAddress().toRawString()
416
450
  }
417
451
 
418
452
  /** {@inheritDoc Chain.getOnRampForOffRamp} */
419
453
  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
454
  try {
425
- const sourceConfig = await openedContract.getSourceChainConfig(sourceChainSelector)
426
- // Convert CrossChainAddress (buffer) to checksummed EVM address
427
- return checksumAddress('0x' + sourceConfig.onRamp.toString('hex'))
455
+ const offRampContract = this.provider.provider(Address.parse(offRamp))
456
+
457
+ const { stack } = await offRampContract.get('sourceChainConfig', [
458
+ { type: 'int', value: sourceChainSelector },
459
+ ])
460
+ stack.readAddress() // router
461
+ stack.readBoolean() // isEnabled
462
+ stack.readBigNumber() // minSeqNr
463
+ stack.readBoolean() // isRMNVerificationDisabled
464
+
465
+ // onRamp is stored as CrossChainAddress cell
466
+ const onRampCell = stack.readCell()
467
+ const onRampSlice = onRampCell.beginParse()
468
+
469
+ // Check if length-prefixed or raw format based on cell bit length
470
+ const cellBits = onRampCell.bits.length
471
+ let onRamp: Buffer
472
+
473
+ if (cellBits === 160) {
474
+ // Raw 20-byte EVM address (no length prefix)
475
+ onRamp = onRampSlice.loadBuffer(20)
476
+ } else {
477
+ // Length-prefixed format: 8-bit length + data
478
+ const onRampLength = onRampSlice.loadUint(8)
479
+ onRamp = onRampSlice.loadBuffer(onRampLength)
480
+ }
481
+ return decodeAddress(onRamp, networkInfo(sourceChainSelector).family)
428
482
  } catch (error) {
429
483
  if (isTvmError(error) && error.exitCode === 266) {
430
484
  throw new CCIPSourceChainUnsupportedError(sourceChainSelector, {
@@ -449,25 +503,23 @@ export class TONChain extends Chain<typeof ChainFamily.TON> {
449
503
  /** {@inheritDoc Chain.getTokenInfo} */
450
504
  async getTokenInfo(token: string): Promise<{ symbol: string; decimals: number }> {
451
505
  const tokenAddress = Address.parse(token)
452
- const lastBlock = await this.provider.getLastBlock()
506
+ if (tokenAddress.toRawString().match(/^[0:]+1$/)) {
507
+ return { symbol: 'TON', decimals: (this.constructor as typeof TONChain).decimals }
508
+ }
453
509
 
454
510
  try {
455
- const result = await this.provider.runMethod(
456
- lastBlock.last.seqno,
457
- tokenAddress,
458
- 'get_jetton_data',
459
- )
511
+ const { stack } = await this.provider.runMethod(tokenAddress, 'get_jetton_data')
460
512
 
461
513
  // skips
462
- result.reader.readBigNumber() // total_supply
463
- result.reader.readBigNumber() // mintable
464
- result.reader.readAddress() // admin_address
514
+ stack.readBigNumber() // total_supply
515
+ stack.readBigNumber() // mintable
516
+ stack.readAddress() // admin_address
465
517
 
466
- const contentCell = result.reader.readCell()
518
+ const contentCell = stack.readCell()
467
519
  return parseJettonContent(contentCell, this.rateLimitedFetch, this.logger)
468
520
  } catch (error) {
469
- this.logger.debug?.(`Failed to get jetton data for ${token}:`, error)
470
- return { symbol: '', decimals: 9 }
521
+ this.logger.debug(`Failed to get jetton data for ${token}:`, error)
522
+ return { symbol: '', decimals: (this.constructor as typeof TONChain).decimals }
471
523
  }
472
524
  }
473
525
 
@@ -481,20 +533,27 @@ export class TONChain extends Chain<typeof ChainFamily.TON> {
481
533
  * @param log - Log with data field.
482
534
  * @returns Decoded CCIPMessage or undefined if not valid.
483
535
  */
484
- static decodeMessage(log: Pick<Log_, 'data'>): CCIPMessage_V1_6_TON | undefined {
485
- if (!log.data || typeof log.data !== 'string') return undefined
536
+ static decodeMessage({
537
+ data,
538
+ topics,
539
+ }: {
540
+ data: unknown
541
+ topics?: readonly string[]
542
+ }): CCIPMessage_V1_6_TON | undefined {
543
+ if (!data || typeof data !== 'string') return
544
+ if (topics?.length && topics[0] !== crc32('CCIPMessageSent')) return
486
545
 
487
546
  try {
488
547
  // Parse BOC from base64
489
- const boc = Buffer.from(log.data, 'base64')
490
- const cell = Cell.fromBoc(boc)[0]
548
+ const boc = bytesToBuffer(data)
549
+ const cell = Cell.fromBoc(boc)[0]!
491
550
  const slice = cell.beginParse()
492
551
 
493
552
  // Load header fields directly (no topic prefix)
494
553
  // Structure from TVM2AnyRampMessage:
495
554
  // header: RampMessageHeader + sender: address + body: Cell + feeValueJuels: uint96
496
555
  const header = {
497
- messageId: '0x' + slice.loadUintBig(256).toString(16).padStart(64, '0'),
556
+ messageId: toBeHex(slice.loadUintBig(256), 32),
498
557
  sourceChainSelector: slice.loadUintBig(64),
499
558
  destChainSelector: slice.loadUintBig(64),
500
559
  sequenceNumber: slice.loadUintBig(64),
@@ -502,7 +561,7 @@ export class TONChain extends Chain<typeof ChainFamily.TON> {
502
561
  }
503
562
 
504
563
  // Load sender address
505
- const sender = slice.loadAddress()?.toString() ?? ''
564
+ const sender = slice.loadAddress().toString()
506
565
 
507
566
  // Load body cell ref
508
567
  const bodyCell = slice.loadRef()
@@ -532,7 +591,6 @@ export class TONChain extends Chain<typeof ChainFamily.TON> {
532
591
  // Load data from ref 1
533
592
  const dataSlice = bodySlice.loadRef().beginParse()
534
593
  const dataBytes = dataSlice.loadBuffer(dataSlice.remainingBits / 8)
535
- const data = '0x' + dataBytes.toString('hex')
536
594
 
537
595
  // Load extraArgs from ref 2
538
596
  const extraArgsCell = bodySlice.loadRef()
@@ -569,7 +627,7 @@ export class TONChain extends Chain<typeof ChainFamily.TON> {
569
627
  ...header,
570
628
  sender,
571
629
  receiver,
572
- data,
630
+ data: hexlify(dataBytes),
573
631
  tokenAmounts,
574
632
  feeToken,
575
633
  feeTokenAmount,
@@ -594,7 +652,6 @@ export class TONChain extends Chain<typeof ChainFamily.TON> {
594
652
  * @returns Hex string of BOC-encoded extra args (0x-prefixed)
595
653
  */
596
654
  static encodeExtraArgs(args: ExtraArgs): string {
597
- if (!args) return '0x'
598
655
  if ('gasLimit' in args && 'allowOutOfOrderExecution' in args) {
599
656
  const cell = beginCell()
600
657
  .storeUint(Number(EVMExtraArgsV2Tag), 32) // magic tag
@@ -623,11 +680,11 @@ export class TONChain extends Chain<typeof ChainFamily.TON> {
623
680
  static decodeExtraArgs(
624
681
  extraArgs: BytesLike,
625
682
  ): (EVMExtraArgsV2 & { _tag: 'EVMExtraArgsV2' }) | undefined {
626
- const data = Buffer.from(getDataBytes(extraArgs))
683
+ const data = bytesToBuffer(extraArgs)
627
684
 
628
685
  try {
629
686
  // Parse BOC format to extract cell data
630
- const cell = Cell.fromBoc(data)[0]
687
+ const cell = Cell.fromBoc(data)[0]!
631
688
  const slice = cell.beginParse()
632
689
 
633
690
  // Load and verify magic tag to ensure correct extra args type
@@ -652,35 +709,37 @@ export class TONChain extends Chain<typeof ChainFamily.TON> {
652
709
  * @param lane - Optional lane info for filtering.
653
710
  * @returns Array of CommitReport or undefined if not a valid commit event.
654
711
  */
655
- static decodeCommits(log: Log_, lane?: Lane): CommitReport[] | undefined {
656
- if (!log.data || typeof log.data !== 'string') return undefined
657
-
712
+ static decodeCommits(
713
+ { data, topics }: { data: unknown; topics?: readonly string[] },
714
+ lane?: Lane,
715
+ ): CommitReport[] | undefined {
716
+ if (!data || typeof data !== 'string') return
717
+ if (topics?.length && topics[0] !== crc32('CommitReportAccepted')) return
658
718
  try {
659
- const boc = Buffer.from(log.data, 'base64')
660
- const cell = Cell.fromBoc(boc)[0]
719
+ const boc = bytesToBuffer(data)
720
+ const cell = Cell.fromBoc(boc)[0]!
661
721
  const slice = cell.beginParse()
662
722
 
663
723
  // Cell body starts directly with hasMerkleRoot (topic is in message header)
664
724
  const hasMerkleRoot = slice.loadBit()
665
725
 
666
- if (!hasMerkleRoot) {
667
- // No merkle root: could be price-only update, skip for now
668
- return undefined
669
- }
726
+ // No merkle root: could be price-only update, skip for now
727
+ if (!hasMerkleRoot) return
670
728
 
671
729
  // Read MerkleRoot fields inline
672
730
  const sourceChainSelector = slice.loadUintBig(64)
673
731
  const onRampLen = slice.loadUint(8)
674
732
 
675
- if (onRampLen === 0 || onRampLen > 32) {
676
- // Invalid onRamp length
677
- return undefined
678
- }
733
+ // Invalid onRamp length
734
+ if (onRampLen === 0 || onRampLen > 32) return
679
735
 
680
- const onRampBytes = slice.loadBuffer(onRampLen)
736
+ const onRampAddress = decodeAddress(
737
+ slice.loadBuffer(onRampLen),
738
+ networkInfo(sourceChainSelector).family,
739
+ )
681
740
  const minSeqNr = slice.loadUintBig(64)
682
741
  const maxSeqNr = slice.loadUintBig(64)
683
- const merkleRoot = '0x' + slice.loadUintBig(256).toString(16).padStart(64, '0')
742
+ const merkleRoot = hexlify(slice.loadBuffer(32))
684
743
 
685
744
  // Read hasPriceUpdates (1 bit): we don't need the data but should consume it
686
745
  if (slice.remainingBits >= 1) {
@@ -692,7 +751,7 @@ export class TONChain extends Chain<typeof ChainFamily.TON> {
692
751
 
693
752
  const report: CommitReport = {
694
753
  sourceChainSelector,
695
- onRampAddress: '0x' + onRampBytes.toString('hex'),
754
+ onRampAddress,
696
755
  minSeqNr,
697
756
  maxSeqNr,
698
757
  merkleRoot,
@@ -700,23 +759,66 @@ export class TONChain extends Chain<typeof ChainFamily.TON> {
700
759
 
701
760
  // Filter by lane if provided
702
761
  if (lane) {
703
- if (report.sourceChainSelector !== lane.sourceChainSelector) return undefined
704
- if (report.onRampAddress?.toLowerCase() !== lane.onRamp?.toLowerCase()) return undefined
762
+ if (report.sourceChainSelector !== lane.sourceChainSelector) return
763
+ if (report.onRampAddress !== lane.onRamp) return
705
764
  }
706
765
 
707
766
  return [report]
708
767
  } catch {
709
- return undefined
768
+ return
710
769
  }
711
770
  }
712
771
 
713
772
  /**
714
773
  * Decodes an execution receipt from a TON log event.
715
- * @param _log - Log with data field.
774
+ *
775
+ * The ExecutionStateChanged event structure (topic is in message header, not body):
776
+ * - sourceChainSelector: uint64 (8 bytes)
777
+ * - sequenceNumber: uint64 (8 bytes)
778
+ * - messageId: uint256 (32 bytes)
779
+ * - state: uint8 (1 byte) - Untouched=0, InProgress=1, Success=2, Failure=3
780
+ *
781
+ * @param log - Log with data field (base64-encoded BOC).
716
782
  * @returns ExecutionReceipt or undefined if not valid.
717
783
  */
718
- static decodeReceipt(_log: Log_): ExecutionReceipt | undefined {
719
- throw new CCIPNotImplementedError('decodeReceipt')
784
+ static decodeReceipt({
785
+ data,
786
+ topics,
787
+ }: {
788
+ data: unknown
789
+ topics?: readonly string[]
790
+ }): ExecutionReceipt | undefined {
791
+ if (!data || typeof data !== 'string') return
792
+ if (topics?.length && topics[0] !== crc32('ExecutionStateChanged')) return
793
+
794
+ try {
795
+ const boc = bytesToBuffer(data)
796
+ const cell = Cell.fromBoc(boc)[0]!
797
+ const slice = cell.beginParse()
798
+
799
+ // ExecutionStateChanged has no refs
800
+ if (cell.refs.length > 0) return
801
+
802
+ // Cell body contains only the struct fields
803
+ // ExecutionStateChanged: sourceChainSelector(64) + sequenceNumber(64) + messageId(256) + state(8)
804
+ const sourceChainSelector = slice.loadUintBig(64)
805
+ const sequenceNumber = slice.loadUintBig(64)
806
+ const messageId = toBeHex(slice.loadUintBig(256), 32)
807
+ const state = slice.loadUint(8)
808
+
809
+ // Validate state is a valid ExecutionState (2-3)
810
+ // TON has intermediary txs with state 1 (InProgress), but we filter them here
811
+ if (state !== ExecutionState.Success && state !== ExecutionState.Failed) return
812
+
813
+ return {
814
+ messageId,
815
+ sequenceNumber,
816
+ sourceChainSelector,
817
+ state: state as ExecutionState,
818
+ }
819
+ } catch {
820
+ // ignore
821
+ }
720
822
  }
721
823
 
722
824
  /**
@@ -799,7 +901,7 @@ export class TONChain extends Chain<typeof ChainFamily.TON> {
799
901
  const parts = hash.split(':')
800
902
  if (parts.length === 4) {
801
903
  // Composite format: workchain:address:lt:hash - return just the hash part
802
- return parts[3]
904
+ return parts[3]!
803
905
  }
804
906
  // Already raw format or unknown - return as-is
805
907
  return hash
@@ -823,7 +925,7 @@ export class TONChain extends Chain<typeof ChainFamily.TON> {
823
925
  // Check for composite format: workchain:address:lt:hash
824
926
  const parts = v.split(':')
825
927
  if (parts.length === 4) {
826
- const [workchain, address, lt, hash] = parts
928
+ const [workchain, address, lt, hash] = parts as [string, string, string, string]
827
929
  // workchain should be a number (typically 0 or -1)
828
930
  if (!/^-?\d+$/.test(workchain)) return false
829
931
  // address should be 64-char hex
@@ -848,43 +950,33 @@ export class TONChain extends Chain<typeof ChainFamily.TON> {
848
950
  }
849
951
 
850
952
  /** {@inheritDoc Chain.getFee} */
851
- async getFee(_router: string, _destChainSelector: bigint, _message: AnyMessage): Promise<bigint> {
953
+ async getFee(_opts: Parameters<Chain['getFee']>[0]): Promise<bigint> {
852
954
  return Promise.reject(new CCIPNotImplementedError('getFee'))
853
955
  }
854
956
 
855
957
  /** {@inheritDoc Chain.generateUnsignedSendMessage} */
856
958
  generateUnsignedSendMessage(
857
- _sender: string,
858
- _router: string,
859
- _destChainSelector: bigint,
860
- _message: AnyMessage & { fee?: bigint },
861
- _opts?: { approveMax?: boolean },
959
+ _opts: Parameters<Chain['generateUnsignedSendMessage']>[0],
862
960
  ): Promise<never> {
863
961
  return Promise.reject(new CCIPNotImplementedError('generateUnsignedSendMessage'))
864
962
  }
865
963
 
866
964
  /** {@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> {
965
+ async sendMessage(_opts: Parameters<Chain['sendMessage']>[0]): Promise<CCIPRequest> {
873
966
  return Promise.reject(new CCIPNotImplementedError('sendMessage'))
874
967
  }
875
968
 
876
- /** {@inheritDoc Chain.fetchOffchainTokenData} */
877
- fetchOffchainTokenData(request: CCIPRequest): Promise<OffchainTokenData[]> {
969
+ /** {@inheritDoc Chain.getOffchainTokenData} */
970
+ getOffchainTokenData(request: CCIPRequest): Promise<OffchainTokenData[]> {
878
971
  return Promise.resolve(request.message.tokenAmounts.map(() => undefined))
879
972
  }
880
973
 
881
974
  /** {@inheritDoc Chain.generateUnsignedExecuteReport} */
882
- generateUnsignedExecuteReport(
883
- _payer: string,
884
- offRamp: string,
885
- execReport: ExecutionReport,
886
- opts?: { gasLimit?: number },
887
- ): Promise<UnsignedTONTx> {
975
+ generateUnsignedExecuteReport({
976
+ offRamp,
977
+ execReport,
978
+ ...opts
979
+ }: Parameters<Chain['generateUnsignedExecuteReport']>[0]): Promise<UnsignedTONTx> {
888
980
  if (!('allowOutOfOrderExecution' in execReport.message && 'gasLimit' in execReport.message)) {
889
981
  throw new CCIPExtraArgsInvalidError('TON')
890
982
  }
@@ -897,57 +989,41 @@ export class TONChain extends Chain<typeof ChainFamily.TON> {
897
989
 
898
990
  return Promise.resolve({
899
991
  family: ChainFamily.TON,
900
- to: unsigned.to,
901
- body: unsigned.body,
992
+ ...unsigned,
902
993
  })
903
994
  }
904
995
 
905
996
  /** {@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
997
+ async executeReport(opts: Parameters<Chain['executeReport']>[0]): Promise<CCIPExecution> {
998
+ const { offRamp, wallet } = opts
912
999
  if (!isTONWallet(wallet)) {
913
1000
  throw new CCIPWalletInvalidError(wallet)
914
1001
  }
1002
+ const payer = await wallet.getAddress()
915
1003
 
916
- const unsigned = await this.generateUnsignedExecuteReport(
917
- wallet.contract.address.toString(),
918
- offRamp,
919
- execReport as ExecutionReport<CCIPMessage_V1_6_TON>,
920
- opts,
921
- )
1004
+ const { family: _, ...unsigned } = await this.generateUnsignedExecuteReport({
1005
+ ...opts,
1006
+ payer,
1007
+ })
922
1008
 
1009
+ const startTime = Math.floor(Date.now() / 1000)
923
1010
  // 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
- ],
1011
+ const seqno = await wallet.sendTransaction({
1012
+ value: toNano('0.3'),
1013
+ ...unsigned,
937
1014
  })
938
1015
 
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)
1016
+ const message = opts.execReport.message as CCIPMessage_V1_6_TON
1017
+ for await (const exec of this.getExecutionReceipts({
1018
+ offRamp,
1019
+ messageId: message.messageId,
1020
+ sourceChainSelector: message.sourceChainSelector,
1021
+ startTime,
1022
+ watch: sleep(10 * 60e3 /* 10m */),
1023
+ })) {
1024
+ return exec // break and return on first yield
1025
+ }
1026
+ throw new CCIPReceiptNotFoundError(seqno.toString())
951
1027
  }
952
1028
 
953
1029
  /**