@chainlink/ccip-sdk 0.94.0 → 0.95.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 (149) hide show
  1. package/dist/api/index.d.ts +80 -4
  2. package/dist/api/index.d.ts.map +1 -1
  3. package/dist/api/index.js +262 -6
  4. package/dist/api/index.js.map +1 -1
  5. package/dist/api/types.d.ts +138 -13
  6. package/dist/api/types.d.ts.map +1 -1
  7. package/dist/aptos/index.d.ts +5 -9
  8. package/dist/aptos/index.d.ts.map +1 -1
  9. package/dist/aptos/index.js +18 -21
  10. package/dist/aptos/index.js.map +1 -1
  11. package/dist/aptos/logs.js +3 -3
  12. package/dist/aptos/logs.js.map +1 -1
  13. package/dist/chain.d.ts +84 -5
  14. package/dist/chain.d.ts.map +1 -1
  15. package/dist/chain.js +63 -2
  16. package/dist/chain.js.map +1 -1
  17. package/dist/errors/codes.d.ts +7 -3
  18. package/dist/errors/codes.d.ts.map +1 -1
  19. package/dist/errors/codes.js +8 -3
  20. package/dist/errors/codes.js.map +1 -1
  21. package/dist/errors/index.d.ts +7 -7
  22. package/dist/errors/index.d.ts.map +1 -1
  23. package/dist/errors/index.js +7 -7
  24. package/dist/errors/index.js.map +1 -1
  25. package/dist/errors/recovery.d.ts.map +1 -1
  26. package/dist/errors/recovery.js +8 -4
  27. package/dist/errors/recovery.js.map +1 -1
  28. package/dist/errors/specialized.d.ts +53 -18
  29. package/dist/errors/specialized.d.ts.map +1 -1
  30. package/dist/errors/specialized.js +112 -37
  31. package/dist/errors/specialized.js.map +1 -1
  32. package/dist/evm/gas.d.ts +14 -0
  33. package/dist/evm/gas.d.ts.map +1 -0
  34. package/dist/evm/gas.js +97 -0
  35. package/dist/evm/gas.js.map +1 -0
  36. package/dist/evm/index.d.ts +6 -8
  37. package/dist/evm/index.d.ts.map +1 -1
  38. package/dist/evm/index.js +23 -14
  39. package/dist/evm/index.js.map +1 -1
  40. package/dist/evm/offchain.d.ts.map +1 -1
  41. package/dist/evm/offchain.js +8 -8
  42. package/dist/evm/offchain.js.map +1 -1
  43. package/dist/execution.d.ts.map +1 -1
  44. package/dist/execution.js +8 -1
  45. package/dist/execution.js.map +1 -1
  46. package/dist/gas.d.ts +43 -19
  47. package/dist/gas.d.ts.map +1 -1
  48. package/dist/gas.js +48 -68
  49. package/dist/gas.js.map +1 -1
  50. package/dist/index.d.ts +15 -13
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +6 -5
  53. package/dist/index.js.map +1 -1
  54. package/dist/offchain.d.ts +5 -4
  55. package/dist/offchain.d.ts.map +1 -1
  56. package/dist/offchain.js +7 -6
  57. package/dist/offchain.js.map +1 -1
  58. package/dist/requests.d.ts +13 -11
  59. package/dist/requests.d.ts.map +1 -1
  60. package/dist/requests.js +69 -47
  61. package/dist/requests.js.map +1 -1
  62. package/dist/selectors.d.ts +2 -1
  63. package/dist/selectors.d.ts.map +1 -1
  64. package/dist/selectors.js +613 -278
  65. package/dist/selectors.js.map +1 -1
  66. package/dist/solana/exec.d.ts.map +1 -1
  67. package/dist/solana/exec.js +2 -1
  68. package/dist/solana/exec.js.map +1 -1
  69. package/dist/solana/index.d.ts +4 -8
  70. package/dist/solana/index.d.ts.map +1 -1
  71. package/dist/solana/index.js +20 -13
  72. package/dist/solana/index.js.map +1 -1
  73. package/dist/solana/offchain.js +2 -2
  74. package/dist/solana/offchain.js.map +1 -1
  75. package/dist/solana/send.d.ts.map +1 -1
  76. package/dist/solana/send.js +6 -9
  77. package/dist/solana/send.js.map +1 -1
  78. package/dist/solana/utils.d.ts +29 -1
  79. package/dist/solana/utils.d.ts.map +1 -1
  80. package/dist/solana/utils.js +39 -1
  81. package/dist/solana/utils.js.map +1 -1
  82. package/dist/sui/discovery.d.ts +7 -4
  83. package/dist/sui/discovery.d.ts.map +1 -1
  84. package/dist/sui/discovery.js +66 -19
  85. package/dist/sui/discovery.js.map +1 -1
  86. package/dist/sui/events.d.ts +23 -12
  87. package/dist/sui/events.d.ts.map +1 -1
  88. package/dist/sui/events.js +267 -128
  89. package/dist/sui/events.js.map +1 -1
  90. package/dist/sui/index.d.ts +20 -32
  91. package/dist/sui/index.d.ts.map +1 -1
  92. package/dist/sui/index.js +246 -148
  93. package/dist/sui/index.js.map +1 -1
  94. package/dist/sui/objects.d.ts +14 -4
  95. package/dist/sui/objects.d.ts.map +1 -1
  96. package/dist/sui/objects.js +61 -68
  97. package/dist/sui/objects.js.map +1 -1
  98. package/dist/sui/types.d.ts +33 -0
  99. package/dist/sui/types.d.ts.map +1 -1
  100. package/dist/sui/types.js.map +1 -1
  101. package/dist/ton/index.d.ts +4 -4
  102. package/dist/ton/index.d.ts.map +1 -1
  103. package/dist/ton/index.js +7 -8
  104. package/dist/ton/index.js.map +1 -1
  105. package/dist/ton/utils.d.ts +3 -3
  106. package/dist/ton/utils.d.ts.map +1 -1
  107. package/dist/ton/utils.js +6 -5
  108. package/dist/ton/utils.js.map +1 -1
  109. package/dist/types.d.ts +24 -8
  110. package/dist/types.d.ts.map +1 -1
  111. package/dist/types.js +19 -5
  112. package/dist/types.js.map +1 -1
  113. package/dist/utils.d.ts +52 -1
  114. package/dist/utils.d.ts.map +1 -1
  115. package/dist/utils.js +108 -12
  116. package/dist/utils.js.map +1 -1
  117. package/package.json +8 -8
  118. package/src/api/index.ts +343 -9
  119. package/src/api/types.ts +165 -13
  120. package/src/aptos/index.ts +19 -33
  121. package/src/aptos/logs.ts +3 -3
  122. package/src/chain.ts +139 -10
  123. package/src/errors/codes.ts +8 -3
  124. package/src/errors/index.ts +7 -4
  125. package/src/errors/recovery.ts +16 -5
  126. package/src/errors/specialized.ts +147 -45
  127. package/src/evm/gas.ts +149 -0
  128. package/src/evm/index.ts +47 -30
  129. package/src/evm/offchain.ts +15 -9
  130. package/src/execution.ts +8 -1
  131. package/src/gas.ts +95 -116
  132. package/src/index.ts +16 -6
  133. package/src/offchain.ts +12 -6
  134. package/src/requests.ts +100 -58
  135. package/src/selectors.ts +620 -280
  136. package/src/solana/exec.ts +3 -1
  137. package/src/solana/index.ts +26 -22
  138. package/src/solana/offchain.ts +2 -2
  139. package/src/solana/send.ts +5 -23
  140. package/src/solana/utils.ts +66 -0
  141. package/src/sui/discovery.ts +92 -31
  142. package/src/sui/events.ts +346 -239
  143. package/src/sui/index.ts +325 -201
  144. package/src/sui/objects.ts +74 -98
  145. package/src/sui/types.ts +35 -0
  146. package/src/ton/index.ts +10 -11
  147. package/src/ton/utils.ts +7 -6
  148. package/src/types.ts +25 -8
  149. package/src/utils.ts +151 -16
package/src/sui/index.ts CHANGED
@@ -1,14 +1,20 @@
1
- import { Buffer } from 'buffer'
2
-
1
+ import { bcs } from '@mysten/sui/bcs'
3
2
  import { type SuiTransactionBlockResponse, SuiClient } from '@mysten/sui/client'
4
3
  import type { Keypair } from '@mysten/sui/cryptography'
5
4
  import { SuiGraphQLClient } from '@mysten/sui/graphql'
6
5
  import { Transaction } from '@mysten/sui/transactions'
7
- import { type BytesLike, dataLength, hexlify, isBytesLike } from 'ethers'
8
- import type { PickDeep } from 'type-fest'
6
+ import { isValidSuiAddress, isValidTransactionDigest, normalizeSuiAddress } from '@mysten/sui/utils'
7
+ import { type BytesLike, dataLength, hexlify, isBytesLike, isHexString } from 'ethers'
8
+ import type { PickDeep, SetOptional } from 'type-fest'
9
9
 
10
10
  import { AptosChain } from '../aptos/index.ts'
11
- import { type ChainContext, type ChainStatic, type LogFilter, Chain } from '../chain.ts'
11
+ import {
12
+ type ChainContext,
13
+ type ChainStatic,
14
+ type GetBalanceOpts,
15
+ type LogFilter,
16
+ Chain,
17
+ } from '../chain.ts'
12
18
  import {
13
19
  CCIPContractNotRouterError,
14
20
  CCIPDataFormatUnsupportedError,
@@ -16,13 +22,17 @@ import {
16
22
  CCIPErrorCode,
17
23
  CCIPExecTxRevertedError,
18
24
  CCIPNotImplementedError,
19
- CCIPSuiMessageVersionInvalidError,
20
- CCIPVersionFeatureUnavailableError,
21
25
  } from '../errors/index.ts'
26
+ import {
27
+ CCIPLogsAddressRequiredError,
28
+ CCIPSuiLogInvalidError,
29
+ CCIPTopicsInvalidError,
30
+ } from '../errors/specialized.ts'
22
31
  import type { EVMExtraArgsV2, ExtraArgs, SVMExtraArgsV1, SuiExtraArgsV1 } from '../extra-args.ts'
23
- import { getSuiLeafHasher } from './hasher.ts'
24
32
  import type { LeafHasher } from '../hasher/common.ts'
33
+ import { decodeMessage, getMessagesInBatch } from '../requests.ts'
25
34
  import { supportedChains } from '../supported-chains.ts'
35
+ import { getSuiLeafHasher } from './hasher.ts'
26
36
  import {
27
37
  type AnyMessage,
28
38
  type CCIPExecution,
@@ -41,32 +51,32 @@ import {
41
51
  type WithLogger,
42
52
  ChainFamily,
43
53
  } from '../types.ts'
44
- import { discoverCCIP, discoverOfframp } from './discovery.ts'
45
- import type { CCIPMessage_V1_6_Sui } from './types.ts'
46
- import { bytesToBuffer, decodeAddress, networkInfo } from '../utils.ts'
47
- import { type CommitEvent, getSuiEventsInTimeRange } from './events.ts'
54
+ import {
55
+ decodeAddress,
56
+ decodeOnRampAddress,
57
+ getDataBytes,
58
+ networkInfo,
59
+ parseTypeAndVersion,
60
+ util,
61
+ } from '../utils.ts'
62
+ import { getCcipStateAddress, getOffRampForCcip } from './discovery.ts'
63
+ import { type CommitEvent, streamSuiLogs } from './events.ts'
48
64
  import {
49
65
  type SuiManuallyExecuteInput,
50
66
  type TokenConfig,
51
67
  buildManualExecutionPTB,
52
68
  } from './manuallyExec/index.ts'
53
69
  import {
70
+ deriveObjectID,
54
71
  fetchTokenConfigs,
55
- getCcipObjectRef,
56
- getOffRampStateObject,
72
+ getLatestPackageId,
73
+ getObjectRef,
57
74
  getReceiverModule,
58
75
  } from './objects.ts'
76
+ import type { CCIPMessage_V1_6_Sui } from './types.ts'
59
77
 
60
- export const SUI_EXTRA_ARGS_V1_TAG = '21ea4ca9' as const
61
78
  const DEFAULT_GAS_LIMIT = 1000000n
62
79
 
63
- type SuiContractDir = {
64
- ccip?: string
65
- onRamp?: string
66
- offRamp?: string
67
- router?: string
68
- }
69
-
70
80
  /**
71
81
  * Sui chain implementation supporting Sui networks.
72
82
  * Note: This implementation is currently a placeholder.
@@ -82,9 +92,6 @@ export class SuiChain extends Chain<typeof ChainFamily.Sui> {
82
92
  readonly client: SuiClient
83
93
  readonly graphqlClient: SuiGraphQLClient
84
94
 
85
- // contracts dir <chainSelectorName, SuiContractDir>
86
- readonly contractsDir: SuiContractDir
87
-
88
95
  /**
89
96
  * Creates a new SuiChain instance.
90
97
  * @param client - Sui client for interacting with the Sui network.
@@ -95,7 +102,6 @@ export class SuiChain extends Chain<typeof ChainFamily.Sui> {
95
102
 
96
103
  this.client = client
97
104
  this.network = network
98
- this.contractsDir = {}
99
105
 
100
106
  // TODO: Graphql client should come from config
101
107
  let graphqlUrl: string
@@ -140,17 +146,19 @@ export class SuiChain extends Chain<typeof ChainFamily.Sui> {
140
146
  chainId = 'sui:4' // devnet
141
147
  } else {
142
148
  throw new CCIPError(
143
- CCIPErrorCode.NETWORK_FAMILY_UNSUPPORTED,
149
+ CCIPErrorCode.CHAIN_FAMILY_UNSUPPORTED,
144
150
  `Unsupported Sui chain identifier: ${rawChainId}`,
145
151
  )
146
152
  }
147
153
 
148
154
  const network = networkInfo(chainId) as NetworkInfo<typeof ChainFamily.Sui>
149
- return new SuiChain(client, network, ctx)
155
+ const chain = new SuiChain(client, network, ctx)
156
+ return Object.assign(chain, { url })
150
157
  }
151
158
 
152
159
  /** {@inheritDoc Chain.getBlockTimestamp} */
153
- async getBlockTimestamp(block: number): Promise<number> {
160
+ async getBlockTimestamp(block: number | 'finalized'): Promise<number> {
161
+ if (typeof block !== 'number' || block <= 0) return Math.floor(Date.now() / 1000)
154
162
  const checkpoint = await this.client.getCheckpoint({
155
163
  id: String(block),
156
164
  })
@@ -176,12 +184,12 @@ export class SuiChain extends Chain<typeof ChainFamily.Sui> {
176
184
  if (txResponse.events?.length) {
177
185
  for (const [i, event] of txResponse.events.entries()) {
178
186
  const eventType = event.type
179
- const packageId = eventType.split('::')[0]
180
- const moduleName = eventType.split('::')[1]
181
- const eventName = eventType.split('::')[2]!
187
+ const splitIdx = eventType.lastIndexOf('::')
188
+ const address = eventType.substring(0, splitIdx)
189
+ const eventName = eventType.substring(splitIdx + 2)
182
190
 
183
191
  events.push({
184
- address: `${packageId}::${moduleName}`,
192
+ address: address,
185
193
  transactionHash: digest,
186
194
  index: i,
187
195
  blockNumber: Number(txResponse.checkpoint || 0),
@@ -202,41 +210,21 @@ export class SuiChain extends Chain<typeof ChainFamily.Sui> {
202
210
 
203
211
  /** {@inheritDoc Chain.getLogs} */
204
212
  async *getLogs(opts: LogFilter & { versionAsHash?: boolean }) {
205
- if (!this.contractsDir.offRamp) {
206
- throw new CCIPContractNotRouterError('OffRamp address not set in contracts directory', 'Sui')
207
- }
213
+ if (!opts.address) throw new CCIPLogsAddressRequiredError()
214
+
208
215
  // Extract the event type from topics
209
- const topic = Array.isArray(opts.topics?.[0]) ? opts.topics[0][0] : opts.topics?.[0] || ''
210
- if (!topic || topic !== 'CommitReportAccepted') {
211
- throw new CCIPVersionFeatureUnavailableError(
212
- 'Event type',
213
- topic || 'unknown',
214
- 'CommitReportAccepted',
215
- )
216
+ if (opts.topics?.length !== 1 || typeof opts.topics[0] !== 'string') {
217
+ throw new CCIPTopicsInvalidError(opts.topics!)
216
218
  }
219
+ const topic = opts.topics[0]
217
220
 
218
- const startTime = opts.startTime ? new Date(opts.startTime * 1000) : new Date(0)
219
- const endTime = opts.endBlock
220
- ? new Date(opts.endBlock)
221
- : new Date(startTime.getTime() + 1 * 24 * 60 * 60 * 1000) // default to +24h
222
-
223
- this.logger.info(
224
- `Fetching Sui events of type ${topic} from ${startTime.toISOString()} to ${endTime.toISOString()}`,
225
- )
226
- const events = await getSuiEventsInTimeRange<CommitEvent>(
227
- this.client,
228
- this.graphqlClient,
229
- `${this.contractsDir.offRamp}::offramp::CommitReportAccepted`,
230
- startTime,
231
- endTime,
232
- )
233
-
234
- for (const event of events) {
235
- const eventData = event.contents.json
221
+ for await (const event of streamSuiLogs<Record<string, unknown>>(this, opts)) {
222
+ const eventData = event.contents?.json
223
+ if (!eventData) continue
236
224
  yield {
237
- address: this.contractsDir.offRamp,
238
- transactionHash: event.transaction?.digest || '',
239
- index: 0, // Sui events do not have an index, set to 0
225
+ address: opts.address,
226
+ transactionHash: event.transaction!.digest,
227
+ index: Number(event.sequenceNumber) || 0,
240
228
  blockNumber: Number(event.transaction?.effects.checkpoint.sequenceNumber || 0),
241
229
  data: eventData,
242
230
  topics: [topic],
@@ -244,11 +232,6 @@ export class SuiChain extends Chain<typeof ChainFamily.Sui> {
244
232
  }
245
233
  }
246
234
 
247
- /** {@inheritDoc Chain.getMessagesInTx} */
248
- override async getMessagesInTx(_tx: string | ChainTransaction): Promise<CCIPRequest[]> {
249
- return Promise.reject(new CCIPNotImplementedError('SuiChain.getMessagesInTx'))
250
- }
251
-
252
235
  /** {@inheritDoc Chain.getMessagesInBatch} */
253
236
  override async getMessagesInBatch<
254
237
  R extends PickDeep<
@@ -256,25 +239,45 @@ export class SuiChain extends Chain<typeof ChainFamily.Sui> {
256
239
  'lane' | `log.${'topics' | 'address' | 'blockNumber'}` | 'message.sequenceNumber'
257
240
  >,
258
241
  >(
259
- _request: R,
260
- _commit: Pick<CommitReport, 'minSeqNr' | 'maxSeqNr'>,
261
- _opts?: { page?: number },
242
+ request: R,
243
+ commit: Pick<CommitReport, 'minSeqNr' | 'maxSeqNr'>,
244
+ opts?: { page?: number },
262
245
  ): Promise<R['message'][]> {
263
- return Promise.reject(new CCIPNotImplementedError('SuiChain.getMessagesInBatch'))
246
+ return getMessagesInBatch(this, request, commit, opts)
264
247
  }
265
248
 
266
249
  /** {@inheritDoc Chain.typeAndVersion} */
267
- async typeAndVersion(_address: string) {
268
- return Promise.reject(new CCIPNotImplementedError('SuiChain.typeAndVersion'))
250
+ async typeAndVersion(address: string) {
251
+ // requires address to have `::<module>` suffix
252
+ address = await getLatestPackageId(address, this.client)
253
+ const target = `${address}::type_and_version`
254
+
255
+ // Use the Transaction builder to create a move call
256
+ const tx = new Transaction()
257
+ // Add move call to the transaction
258
+ tx.moveCall({ target, arguments: [] })
259
+
260
+ // Execute with devInspectTransactionBlock for read-only call
261
+ const result = await this.client.devInspectTransactionBlock({
262
+ sender: '0x0000000000000000000000000000000000000000000000000000000000000000',
263
+ transactionBlock: tx,
264
+ })
265
+
266
+ if (result.effects.status.status !== 'success' || !result.results?.[0]?.returnValues?.[0]) {
267
+ throw new CCIPDataFormatUnsupportedError(
268
+ `Failed to call ${target}: ${result.effects.status.error || 'No return value'}`,
269
+ )
270
+ }
271
+
272
+ const [data] = result.results[0].returnValues[0]
273
+ const res = bcs.String.parse(getDataBytes(data))
274
+ return parseTypeAndVersion(res)
269
275
  }
270
276
 
271
277
  /** {@inheritDoc Chain.getRouterForOnRamp} */
272
278
  async getRouterForOnRamp(onRamp: string, _destChainSelector: bigint): Promise<string> {
273
- this.contractsDir.onRamp = onRamp
274
- if (onRamp !== this.contractsDir.onRamp) {
275
- this.contractsDir.onRamp = onRamp
276
- }
277
- return Promise.resolve(this.contractsDir.onRamp)
279
+ // In Sui, the router is the onRamp package itself
280
+ return Promise.resolve(onRamp)
278
281
  }
279
282
 
280
283
  /** {@inheritDoc Chain.getRouterForOffRamp} */
@@ -283,40 +286,40 @@ export class SuiChain extends Chain<typeof ChainFamily.Sui> {
283
286
  }
284
287
 
285
288
  /** {@inheritDoc Chain.getNativeTokenForRouter} */
286
- getNativeTokenForRouter(_router: string): Promise<string> {
289
+ getNativeTokenForRouter(): Promise<string> {
287
290
  // SUI native token is always 0x2::sui::SUI
288
291
  return Promise.resolve('0x2::sui::SUI')
289
292
  }
290
293
 
291
294
  /** {@inheritDoc Chain.getOffRampsForRouter} */
292
295
  async getOffRampsForRouter(router: string, _sourceChainSelector: bigint): Promise<string[]> {
293
- const ccip = await discoverCCIP(this.client, router)
294
- const offramp = await discoverOfframp(this.client, ccip)
295
- this.contractsDir.offRamp = offramp
296
- this.contractsDir.ccip = ccip
296
+ router = await getLatestPackageId(router, this.client)
297
+ const ccip = await getCcipStateAddress(router, this.client)
298
+ const offramp = await getOffRampForCcip(ccip, this.client)
297
299
  return [offramp]
298
300
  }
299
301
 
300
302
  /** {@inheritDoc Chain.getOnRampForRouter} */
301
- getOnRampForRouter(_router: string, _destChainSelector: bigint): Promise<string> {
302
- if (!this.contractsDir.onRamp) {
303
- throw new CCIPContractNotRouterError('OnRamp address not set in contracts directory', 'Sui')
304
- }
305
- return Promise.resolve(this.contractsDir.onRamp)
303
+ getOnRampForRouter(router: string, _destChainSelector: bigint): Promise<string> {
304
+ // For Sui, the router is the onramp package address
305
+ return Promise.resolve(router)
306
306
  }
307
307
 
308
308
  /** {@inheritDoc Chain.getOnRampForOffRamp} */
309
309
  async getOnRampForOffRamp(offRamp: string, sourceChainSelector: bigint): Promise<string> {
310
- if (!this.contractsDir.ccip) {
311
- throw new CCIPError(CCIPErrorCode.UNKNOWN, 'CCIP address not set in contracts directory')
312
- }
313
- const offrampPackageId = offRamp
310
+ offRamp = await getLatestPackageId(offRamp, this.client)
314
311
  const functionName = 'get_source_chain_config'
315
- const target = `${offrampPackageId}::offramp::${functionName}`
312
+ // Preserve module suffix if present, otherwise add it
313
+ const target = offRamp.includes('::')
314
+ ? `${offRamp}::${functionName}`
315
+ : `${offRamp}::offramp::${functionName}`
316
+
317
+ // Discover the CCIP package from the offramp
318
+ const ccip = await getCcipStateAddress(offRamp, this.client)
316
319
 
317
320
  // Get the OffRampState object
318
- const offrampStateObject = await getOffRampStateObject(this.client, offrampPackageId)
319
- const ccipObjectRef = await getCcipObjectRef(this.client, this.contractsDir.ccip)
321
+ const offrampStateObject = await getObjectRef(offRamp, this.client)
322
+ const ccipObjectRef = await getObjectRef(ccip, this.client)
320
323
  // Use the Transaction builder to create a move call
321
324
  const tx = new Transaction()
322
325
 
@@ -381,43 +384,170 @@ export class SuiChain extends Chain<typeof ChainFamily.Sui> {
381
384
  }
382
385
 
383
386
  /** {@inheritDoc Chain.getTokenForTokenPool} */
384
- getTokenForTokenPool(_tokenPool: string): Promise<string> {
385
- throw new CCIPNotImplementedError()
387
+ async getTokenForTokenPool(tokenPool: string): Promise<string> {
388
+ const normalizedTokenPool = normalizeSuiAddress(tokenPool)
389
+
390
+ // Get objects owned by this package (looking for state pointers)
391
+ const objects = await this.client.getOwnedObjects({
392
+ owner: normalizedTokenPool,
393
+ options: { showType: true, showContent: true },
394
+ })
395
+
396
+ const tpType = objects.data
397
+ .find((obj) => obj.data?.type?.includes('token_pool::'))
398
+ ?.data?.type?.split('::')[1]
399
+
400
+ const allowedTps = ['managed_token_pool', 'burn_mint_token_pool', 'lock_release_token_pool']
401
+ if (!tpType || !allowedTps.includes(tpType)) {
402
+ throw new CCIPError(CCIPErrorCode.UNKNOWN, `Invalid token pool type: ${tpType}`)
403
+ }
404
+
405
+ // Find the state pointer object
406
+ let stateObjectPointerId: string | undefined
407
+ for (const obj of objects.data) {
408
+ const content = obj.data?.content
409
+ if (content?.dataType !== 'moveObject') continue
410
+
411
+ const fields = content.fields as Record<string, unknown>
412
+ // Look for a pointer field that references the state object
413
+ stateObjectPointerId = fields[`${tpType}_object_id`] as string
414
+ }
415
+
416
+ if (!stateObjectPointerId) {
417
+ throw new CCIPError(
418
+ CCIPErrorCode.UNKNOWN,
419
+ `No token pool state pointer found for ${tokenPool}`,
420
+ )
421
+ }
422
+
423
+ const stateNamesPerTP: Record<string, string> = {
424
+ managed_token_pool: 'ManagedTokenPoolState',
425
+ burn_mint_token_pool: 'BurnMintTokenPoolState',
426
+ lock_release_token_pool: 'LockReleaseTokenPoolState',
427
+ }
428
+
429
+ const poolStateObject = deriveObjectID(
430
+ stateObjectPointerId,
431
+ new TextEncoder().encode(stateNamesPerTP[tpType]),
432
+ )
433
+
434
+ // Get object info to get the coin type
435
+ const info = await this.client.getObject({
436
+ id: poolStateObject,
437
+ options: { showType: true, showContent: true },
438
+ })
439
+
440
+ const type = info.data?.type
441
+ if (!type) {
442
+ throw new CCIPError(CCIPErrorCode.UNKNOWN, 'Error loading token pool state object type')
443
+ }
444
+
445
+ // Extract the type parameter T from ManagedTokenPoolState<T>
446
+ const typeMatch = type.match(/(?:Managed|BurnMint|LockRelease)TokenPoolState<(.+)>$/)
447
+ if (!typeMatch || !typeMatch[1]) {
448
+ throw new CCIPError(CCIPErrorCode.UNKNOWN, `Invalid pool state type format: ${type}`)
449
+ }
450
+ const tokenType = typeMatch[1]
451
+
452
+ // Call get_token function from managed_token_pool contract with the type parameter
453
+ const target = type.split('<')[0]?.split('::').slice(0, 2).join('::') + '::get_token'
454
+ if (!target) {
455
+ throw new CCIPError(CCIPErrorCode.UNKNOWN, `Invalid pool state type format: ${type}`)
456
+ }
457
+ const tx = new Transaction()
458
+ tx.moveCall({
459
+ target,
460
+ typeArguments: [tokenType],
461
+ arguments: [tx.object(poolStateObject)],
462
+ })
463
+
464
+ const result = await this.client.devInspectTransactionBlock({
465
+ sender: '0x0000000000000000000000000000000000000000000000000000000000000000',
466
+ transactionBlock: tx,
467
+ })
468
+
469
+ if (result.effects.status.status !== 'success' || !result.results?.[0]?.returnValues?.[0]) {
470
+ throw new CCIPDataFormatUnsupportedError(
471
+ `Failed to call ${target}: ${result.effects.status.error || 'No return value'}`,
472
+ )
473
+ }
474
+
475
+ // Parse the return value to get the coin metadata address (32 bytes)
476
+ const returnValue = result.results[0].returnValues[0]
477
+ const [data] = returnValue
478
+ const coinMetadataBytes = new Uint8Array(data)
479
+ const coinMetadataAddress = normalizeSuiAddress(hexlify(coinMetadataBytes))
480
+
481
+ return coinMetadataAddress
386
482
  }
387
483
 
388
484
  /** {@inheritDoc Chain.getTokenInfo} */
389
485
  async getTokenInfo(token: string): Promise<{ symbol: string; decimals: number }> {
390
- // Handle native SUI token
391
- if (token === '0x2::sui::SUI' || token.includes('::sui::SUI')) {
392
- return { symbol: 'SUI', decimals: 9 }
486
+ const normalizedTokenAddress = normalizeSuiAddress(token)
487
+ if (!isValidSuiAddress(normalizedTokenAddress)) {
488
+ throw new CCIPError(CCIPErrorCode.UNKNOWN, 'Error loading Sui token metadata')
393
489
  }
394
490
 
395
- try {
396
- // For Coin types, try to fetch metadata from the coin metadata object
397
- // Format: 0xPACKAGE::module::TYPE
398
- const coinMetadata = await this.client.getCoinMetadata({ coinType: token })
491
+ const objectResponse = await this.client.getObject({
492
+ id: normalizedTokenAddress,
493
+ options: { showType: true },
494
+ })
399
495
 
400
- if (coinMetadata) {
401
- return {
402
- symbol: coinMetadata.symbol || 'UNKNOWN',
403
- decimals: coinMetadata.decimals,
404
- }
496
+ const getCoinFromMetadata = (metadata: string) => {
497
+ // Extract the type parameter from CoinMetadata<...>
498
+ const match = metadata.match(/CoinMetadata<(.+)>$/)
499
+
500
+ if (!match || !match[1]) {
501
+ throw new CCIPError(CCIPErrorCode.UNKNOWN, `Invalid metadata format: ${metadata}`)
405
502
  }
406
- } catch (error) {
407
- console.log(`Failed to fetch coin metadata for ${token}:`, error)
503
+
504
+ return match[1]
505
+ }
506
+
507
+ let coinType: string
508
+ const objectType = objectResponse.data?.type
509
+
510
+ // Check if this is a CoinMetadata object or a coin type string
511
+ if (objectType?.includes('CoinMetadata')) {
512
+ coinType = getCoinFromMetadata(objectType)
513
+ } else if (token.includes('::')) {
514
+ // This is a coin type string (e.g., "0xabc::coin::COIN")
515
+ coinType = token
516
+ } else {
517
+ // This is a package address or unknown format
518
+ throw new CCIPError(
519
+ CCIPErrorCode.UNKNOWN,
520
+ `Token address ${token} is not a CoinMetadata object or coin type. Expected format: package::module::Type`,
521
+ )
522
+ }
523
+
524
+ if (coinType.split('::').length < 3) {
525
+ throw new CCIPError(CCIPErrorCode.UNKNOWN, 'Error loading Sui token metadata')
526
+ }
527
+
528
+ let metadata = null
529
+ try {
530
+ metadata = await this.client.getCoinMetadata({ coinType })
531
+ } catch (e) {
532
+ console.error('Error fetching coin metadata:', e)
533
+ throw new CCIPError(CCIPErrorCode.UNKNOWN, 'Error loading Sui token metadata')
408
534
  }
409
535
 
410
- // Fallback: parse from token type string if possible
411
- const parts = token.split('::')
412
- const symbol = parts[parts.length - 1] || 'UNKNOWN'
536
+ if (!metadata) {
537
+ throw new CCIPError(CCIPErrorCode.UNKNOWN, 'Error loading Sui token metadata')
538
+ }
413
539
 
414
540
  return {
415
- symbol: symbol.toUpperCase(),
416
- decimals: 9, // Default to 9 decimals (SUI standard)
541
+ symbol: metadata.symbol,
542
+ decimals: metadata.decimals,
417
543
  }
418
544
  }
419
545
 
420
- /** {@inheritDoc Chain.getTokenAdminRegistryFor} */
546
+ /** {@inheritDoc Chain.getBalance} */
547
+ async getBalance(_opts: GetBalanceOpts): Promise<bigint> {
548
+ return Promise.reject(new CCIPNotImplementedError('SuiChain.getBalance'))
549
+ }
550
+
421
551
  /** {@inheritDoc Chain.getTokenAdminRegistryFor} */
422
552
  getTokenAdminRegistryFor(_address: string): Promise<string> {
423
553
  return Promise.reject(new CCIPNotImplementedError())
@@ -426,11 +556,22 @@ export class SuiChain extends Chain<typeof ChainFamily.Sui> {
426
556
  // Static methods for decoding
427
557
  /**
428
558
  * Decodes a CCIP message from a Sui log event.
429
- * @param _log - Log event data.
559
+ * @param log - Log event data.
430
560
  * @returns Decoded CCIPMessage or undefined if not valid.
431
561
  */
432
- static decodeMessage(_log: Log_): CCIPMessage_V1_6_Sui | undefined {
433
- throw new CCIPNotImplementedError()
562
+ static decodeMessage(log: Log_): CCIPMessage | undefined {
563
+ const { data } = log
564
+ if (
565
+ (typeof data !== 'string' || !data.startsWith('{')) &&
566
+ (typeof data !== 'object' || isBytesLike(data))
567
+ )
568
+ throw new CCIPSuiLogInvalidError(util.inspect(log))
569
+ // offload massaging to generic decodeJsonMessage
570
+ try {
571
+ return decodeMessage(data)
572
+ } catch (_) {
573
+ // return undefined
574
+ }
434
575
  }
435
576
 
436
577
  /**
@@ -459,30 +600,36 @@ export class SuiChain extends Chain<typeof ChainFamily.Sui> {
459
600
  /**
460
601
  * Decodes commit reports from a log entry.
461
602
  * @param log - The log entry to decode.
462
- * @param _lane - Optional lane information.
603
+ * @param lane - Optional lane information.
463
604
  * @returns Array of decoded commit reports or undefined.
464
605
  */
465
- static decodeCommits(log: Log_, _lane?: Lane): CommitReport[] | undefined {
466
- if (!log.data || typeof log.data !== 'object' || !('unblessed_merkle_roots' in log.data)) {
467
- return
468
- }
469
- const toHexFromBase64 = (b64: string) => '0x' + Buffer.from(b64, 'base64').toString('hex')
470
-
471
- const eventData = log.data as CommitEvent
472
- const unblessedRoots = eventData.unblessed_merkle_roots
473
- if (!Array.isArray(unblessedRoots) || unblessedRoots.length === 0) {
474
- return
475
- }
476
-
477
- return unblessedRoots.map((root) => {
478
- return {
479
- sourceChainSelector: BigInt(root.source_chain_selector),
480
- onRampAddress: toHexFromBase64(root.on_ramp_address),
481
- minSeqNr: BigInt(root.min_seq_nr),
482
- maxSeqNr: BigInt(root.max_seq_nr),
483
- merkleRoot: toHexFromBase64(root.merkle_root),
484
- }
485
- })
606
+ static decodeCommits(
607
+ { data, topics }: SetOptional<Pick<Log_, 'data' | 'topics'>, 'topics'>,
608
+ lane?: Lane,
609
+ ): CommitReport[] | undefined {
610
+ // Check if this is an CommitReportAccepted event
611
+ if (topics?.[0] && topics[0] !== 'CommitReportAccepted') return
612
+
613
+ // Basic log data structure validation
614
+ if (!data || typeof data !== 'object' || !('unblessed_merkle_roots' in data)) return
615
+
616
+ const eventData = data as CommitEvent
617
+ const rootsRaw = eventData.blessed_merkle_roots.concat(eventData.unblessed_merkle_roots)
618
+ return rootsRaw
619
+ .map((root) => {
620
+ return {
621
+ sourceChainSelector: BigInt(root.source_chain_selector),
622
+ onRampAddress: decodeOnRampAddress(root.on_ramp_address),
623
+ minSeqNr: BigInt(root.min_seq_nr),
624
+ maxSeqNr: BigInt(root.max_seq_nr),
625
+ merkleRoot: hexlify(getDataBytes(root.merkle_root)),
626
+ }
627
+ })
628
+ .filter((r) =>
629
+ lane
630
+ ? r.sourceChainSelector === lane.sourceChainSelector && r.onRampAddress === lane.onRamp
631
+ : true,
632
+ )
486
633
  }
487
634
 
488
635
  /**
@@ -490,52 +637,32 @@ export class SuiChain extends Chain<typeof ChainFamily.Sui> {
490
637
  * @param log - The log entry to decode.
491
638
  * @returns Decoded execution receipt or undefined.
492
639
  */
493
- static decodeReceipt(log: Log_): ExecutionReceipt | undefined {
640
+ static decodeReceipt({
641
+ data,
642
+ topics,
643
+ }: SetOptional<Pick<Log_, 'data' | 'topics'>, 'topics'>): ExecutionReceipt | undefined {
494
644
  // Check if this is an ExecutionStateChanged event
495
- const topic = (Array.isArray(log.topics) ? log.topics[0] : log.topics) as string
496
- if (topic !== 'ExecutionStateChanged') {
497
- return undefined
498
- }
645
+ if (topics?.[0] && topics[0] !== 'ExecutionStateChanged') return
499
646
 
500
- // Validate log data structure
501
- if (!log.data || typeof log.data !== 'object') {
502
- return undefined
503
- }
504
-
505
- const eventData = log.data as {
506
- message_hash?: number[]
507
- message_id?: number[]
508
- sequence_number?: string
509
- source_chain_selector?: string
510
- state?: number
647
+ // Basic log data structure validation
648
+ if (!data || typeof data !== 'object' || !('message_id' in data) || !('state' in data)) {
649
+ return
511
650
  }
512
651
 
513
- // Verify required fields exist
514
- if (
515
- !eventData.message_id ||
516
- !Array.isArray(eventData.message_id) ||
517
- eventData.sequence_number === undefined ||
518
- eventData.state === undefined
519
- ) {
520
- return undefined
652
+ const eventData = data as {
653
+ message_hash: BytesLike
654
+ message_id: BytesLike
655
+ sequence_number: string
656
+ source_chain_selector: string
657
+ state: number
521
658
  }
522
659
 
523
- const toHex = (bytes: BytesLike | number[]) => hexlify(bytesToBuffer(bytes))
524
-
525
- // Convert message_id bytes array to hex string
526
- const messageId = toHex(eventData.message_id)
527
-
528
- // Convert message_hash bytes array to hex string (if present)
529
- const messageHash = eventData.message_hash ? toHex(eventData.message_hash) : undefined
530
-
531
660
  return {
532
- messageId,
661
+ messageId: hexlify(getDataBytes(eventData.message_id)),
533
662
  sequenceNumber: BigInt(eventData.sequence_number),
534
- state: eventData.state as ExecutionState,
535
- sourceChainSelector: eventData.source_chain_selector
536
- ? BigInt(eventData.source_chain_selector)
537
- : undefined,
538
- messageHash,
663
+ state: Number(eventData.state) as ExecutionState,
664
+ sourceChainSelector: BigInt(eventData.source_chain_selector),
665
+ messageHash: hexlify(getDataBytes(eventData.message_hash)),
539
666
  }
540
667
  }
541
668
 
@@ -544,15 +671,17 @@ export class SuiChain extends Chain<typeof ChainFamily.Sui> {
544
671
  * @param bytes - Bytes to convert.
545
672
  * @returns Sui address.
546
673
  */
547
- static getAddress(bytes: BytesLike): string {
674
+ static getAddress(bytes: BytesLike | readonly number[]): string {
548
675
  return AptosChain.getAddress(bytes)
549
676
  }
550
677
 
551
678
  /**
552
679
  * Validates a transaction hash format for Sui
553
680
  */
554
- static isTxHash(_v: unknown): _v is string {
555
- return false
681
+ static isTxHash(v: unknown): v is string {
682
+ if (typeof v !== 'string') return false
683
+ // check in both hex and base58 formats
684
+ return isHexString(v, 32) || isValidTransactionDigest(v)
556
685
  }
557
686
 
558
687
  /**
@@ -583,9 +712,6 @@ export class SuiChain extends Chain<typeof ChainFamily.Sui> {
583
712
 
584
713
  /** {@inheritDoc Chain.getOffchainTokenData} */
585
714
  getOffchainTokenData(request: CCIPRequest): Promise<OffchainTokenData[]> {
586
- if (!('receiverObjectIds' in request.message)) {
587
- throw new CCIPSuiMessageVersionInvalidError()
588
- }
589
715
  // default offchain token data
590
716
  return Promise.resolve(request.message.tokenAmounts.map(() => undefined))
591
717
  }
@@ -603,19 +729,17 @@ export class SuiChain extends Chain<typeof ChainFamily.Sui> {
603
729
  receiverObjectIds?: string[]
604
730
  },
605
731
  ): Promise<CCIPExecution> {
606
- const { execReport } = opts
607
- if (!this.contractsDir.offRamp || !this.contractsDir.ccip) {
608
- throw new CCIPContractNotRouterError(
609
- 'OffRamp or CCIP address not set in contracts directory',
610
- 'Sui',
611
- )
612
- }
732
+ const { execReport, offRamp } = opts
613
733
  const wallet = opts.wallet as Keypair
614
- const ccipObjectRef = await getCcipObjectRef(this.client, this.contractsDir.ccip)
615
- const offrampStateObject = await getOffRampStateObject(this.client, this.contractsDir.offRamp)
734
+
735
+ // Discover the CCIP package from the offramp
736
+ const ccip = await getCcipStateAddress(offRamp, this.client)
737
+
738
+ const ccipObjectRef = await getObjectRef(ccip, this.client)
739
+ const offrampStateObject = await getObjectRef(offRamp, this.client)
616
740
  const receiverConfig = await getReceiverModule(
617
741
  this.client,
618
- this.contractsDir.ccip,
742
+ ccip,
619
743
  ccipObjectRef,
620
744
  execReport.message.receiver,
621
745
  )
@@ -623,7 +747,7 @@ export class SuiChain extends Chain<typeof ChainFamily.Sui> {
623
747
  if (execReport.message.tokenAmounts.length !== 0) {
624
748
  tokenConfigs = await fetchTokenConfigs(
625
749
  this.client,
626
- this.contractsDir.ccip,
750
+ ccip,
627
751
  ccipObjectRef,
628
752
  execReport.message.tokenAmounts as CCIPMessage<typeof CCIPVersion.V1_6>['tokenAmounts'],
629
753
  )
@@ -631,8 +755,8 @@ export class SuiChain extends Chain<typeof ChainFamily.Sui> {
631
755
 
632
756
  const input: SuiManuallyExecuteInput = {
633
757
  executionReport: execReport as ExecutionReport<CCIPMessage_V1_6_Sui>,
634
- offrampAddress: this.contractsDir.offRamp,
635
- ccipAddress: this.contractsDir.ccip,
758
+ offrampAddress: offRamp,
759
+ ccipAddress: ccip,
636
760
  ccipObjectRef,
637
761
  offrampStateObject,
638
762
  receiverConfig,