@chainlink/ccip-sdk 0.93.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 (164) 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 +26 -25
  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/aptos/send.js +1 -1
  14. package/dist/aptos/send.js.map +1 -1
  15. package/dist/chain.d.ts +96 -10
  16. package/dist/chain.d.ts.map +1 -1
  17. package/dist/chain.js +77 -2
  18. package/dist/chain.js.map +1 -1
  19. package/dist/errors/codes.d.ts +7 -3
  20. package/dist/errors/codes.d.ts.map +1 -1
  21. package/dist/errors/codes.js +8 -3
  22. package/dist/errors/codes.js.map +1 -1
  23. package/dist/errors/index.d.ts +7 -7
  24. package/dist/errors/index.d.ts.map +1 -1
  25. package/dist/errors/index.js +7 -7
  26. package/dist/errors/index.js.map +1 -1
  27. package/dist/errors/recovery.d.ts.map +1 -1
  28. package/dist/errors/recovery.js +8 -4
  29. package/dist/errors/recovery.js.map +1 -1
  30. package/dist/errors/specialized.d.ts +53 -18
  31. package/dist/errors/specialized.d.ts.map +1 -1
  32. package/dist/errors/specialized.js +112 -37
  33. package/dist/errors/specialized.js.map +1 -1
  34. package/dist/evm/gas.d.ts +14 -0
  35. package/dist/evm/gas.d.ts.map +1 -0
  36. package/dist/evm/gas.js +97 -0
  37. package/dist/evm/gas.js.map +1 -0
  38. package/dist/evm/index.d.ts +6 -8
  39. package/dist/evm/index.d.ts.map +1 -1
  40. package/dist/evm/index.js +36 -23
  41. package/dist/evm/index.js.map +1 -1
  42. package/dist/evm/offchain.d.ts.map +1 -1
  43. package/dist/evm/offchain.js +8 -8
  44. package/dist/evm/offchain.js.map +1 -1
  45. package/dist/execution.d.ts.map +1 -1
  46. package/dist/execution.js +8 -1
  47. package/dist/execution.js.map +1 -1
  48. package/dist/gas.d.ts +43 -19
  49. package/dist/gas.d.ts.map +1 -1
  50. package/dist/gas.js +48 -68
  51. package/dist/gas.js.map +1 -1
  52. package/dist/index.d.ts +15 -13
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +6 -5
  55. package/dist/index.js.map +1 -1
  56. package/dist/offchain.d.ts +5 -4
  57. package/dist/offchain.d.ts.map +1 -1
  58. package/dist/offchain.js +7 -6
  59. package/dist/offchain.js.map +1 -1
  60. package/dist/requests.d.ts +21 -13
  61. package/dist/requests.d.ts.map +1 -1
  62. package/dist/requests.js +79 -47
  63. package/dist/requests.js.map +1 -1
  64. package/dist/selectors.d.ts +2 -1
  65. package/dist/selectors.d.ts.map +1 -1
  66. package/dist/selectors.js +629 -274
  67. package/dist/selectors.js.map +1 -1
  68. package/dist/solana/exec.d.ts.map +1 -1
  69. package/dist/solana/exec.js +2 -1
  70. package/dist/solana/exec.js.map +1 -1
  71. package/dist/solana/index.d.ts +10 -10
  72. package/dist/solana/index.d.ts.map +1 -1
  73. package/dist/solana/index.js +82 -18
  74. package/dist/solana/index.js.map +1 -1
  75. package/dist/solana/offchain.js +2 -2
  76. package/dist/solana/offchain.js.map +1 -1
  77. package/dist/solana/send.d.ts.map +1 -1
  78. package/dist/solana/send.js +6 -9
  79. package/dist/solana/send.js.map +1 -1
  80. package/dist/solana/utils.d.ts +29 -1
  81. package/dist/solana/utils.d.ts.map +1 -1
  82. package/dist/solana/utils.js +39 -1
  83. package/dist/solana/utils.js.map +1 -1
  84. package/dist/sui/discovery.d.ts +7 -4
  85. package/dist/sui/discovery.d.ts.map +1 -1
  86. package/dist/sui/discovery.js +66 -19
  87. package/dist/sui/discovery.js.map +1 -1
  88. package/dist/sui/events.d.ts +23 -12
  89. package/dist/sui/events.d.ts.map +1 -1
  90. package/dist/sui/events.js +267 -128
  91. package/dist/sui/events.js.map +1 -1
  92. package/dist/sui/index.d.ts +32 -39
  93. package/dist/sui/index.d.ts.map +1 -1
  94. package/dist/sui/index.js +289 -163
  95. package/dist/sui/index.js.map +1 -1
  96. package/dist/sui/manuallyExec/encoder.d.ts.map +1 -1
  97. package/dist/sui/manuallyExec/encoder.js +1 -0
  98. package/dist/sui/manuallyExec/encoder.js.map +1 -1
  99. package/dist/sui/manuallyExec/index.d.ts.map +1 -1
  100. package/dist/sui/manuallyExec/index.js +1 -0
  101. package/dist/sui/manuallyExec/index.js.map +1 -1
  102. package/dist/sui/objects.d.ts +14 -4
  103. package/dist/sui/objects.d.ts.map +1 -1
  104. package/dist/sui/objects.js +63 -69
  105. package/dist/sui/objects.js.map +1 -1
  106. package/dist/sui/types.d.ts +33 -0
  107. package/dist/sui/types.d.ts.map +1 -1
  108. package/dist/sui/types.js.map +1 -1
  109. package/dist/ton/hasher.d.ts.map +1 -1
  110. package/dist/ton/hasher.js +1 -0
  111. package/dist/ton/hasher.js.map +1 -1
  112. package/dist/ton/index.d.ts +4 -4
  113. package/dist/ton/index.d.ts.map +1 -1
  114. package/dist/ton/index.js +8 -8
  115. package/dist/ton/index.js.map +1 -1
  116. package/dist/ton/utils.d.ts +3 -3
  117. package/dist/ton/utils.d.ts.map +1 -1
  118. package/dist/ton/utils.js +6 -5
  119. package/dist/ton/utils.js.map +1 -1
  120. package/dist/types.d.ts +34 -10
  121. package/dist/types.d.ts.map +1 -1
  122. package/dist/types.js +19 -5
  123. package/dist/types.js.map +1 -1
  124. package/dist/utils.d.ts +53 -1
  125. package/dist/utils.d.ts.map +1 -1
  126. package/dist/utils.js +109 -12
  127. package/dist/utils.js.map +1 -1
  128. package/package.json +17 -11
  129. package/src/api/index.ts +343 -9
  130. package/src/api/types.ts +165 -13
  131. package/src/aptos/index.ts +32 -32
  132. package/src/aptos/logs.ts +3 -3
  133. package/src/aptos/send.ts +1 -1
  134. package/src/chain.ts +165 -12
  135. package/src/errors/codes.ts +8 -3
  136. package/src/errors/index.ts +7 -4
  137. package/src/errors/recovery.ts +16 -5
  138. package/src/errors/specialized.ts +147 -45
  139. package/src/evm/gas.ts +149 -0
  140. package/src/evm/index.ts +66 -33
  141. package/src/evm/offchain.ts +15 -9
  142. package/src/execution.ts +8 -1
  143. package/src/gas.ts +95 -116
  144. package/src/index.ts +16 -6
  145. package/src/offchain.ts +12 -6
  146. package/src/requests.ts +113 -59
  147. package/src/selectors.ts +636 -276
  148. package/src/solana/exec.ts +3 -1
  149. package/src/solana/index.ts +119 -23
  150. package/src/solana/offchain.ts +2 -2
  151. package/src/solana/send.ts +5 -23
  152. package/src/solana/utils.ts +66 -0
  153. package/src/sui/discovery.ts +92 -31
  154. package/src/sui/events.ts +346 -239
  155. package/src/sui/index.ts +381 -224
  156. package/src/sui/manuallyExec/encoder.ts +2 -0
  157. package/src/sui/manuallyExec/index.ts +2 -0
  158. package/src/sui/objects.ts +77 -99
  159. package/src/sui/types.ts +35 -0
  160. package/src/ton/hasher.ts +2 -0
  161. package/src/ton/index.ts +12 -11
  162. package/src/ton/utils.ts +7 -6
  163. package/src/types.ts +36 -10
  164. package/src/utils.ts +153 -16
package/src/sui/index.ts CHANGED
@@ -1,29 +1,40 @@
1
- import { toHex } from '@mysten/bcs'
1
+ import { bcs } from '@mysten/sui/bcs'
2
2
  import { type SuiTransactionBlockResponse, SuiClient } from '@mysten/sui/client'
3
3
  import type { Keypair } from '@mysten/sui/cryptography'
4
4
  import { SuiGraphQLClient } from '@mysten/sui/graphql'
5
5
  import { Transaction } from '@mysten/sui/transactions'
6
- import { type BytesLike, AbiCoder, hexlify, isBytesLike } from 'ethers'
7
- 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'
8
9
 
9
10
  import { AptosChain } from '../aptos/index.ts'
10
- import { type ChainContext, 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'
11
18
  import {
12
19
  CCIPContractNotRouterError,
13
20
  CCIPDataFormatUnsupportedError,
14
21
  CCIPError,
15
22
  CCIPErrorCode,
16
23
  CCIPExecTxRevertedError,
17
- CCIPExtraArgsInvalidError,
18
24
  CCIPNotImplementedError,
19
- CCIPSuiMessageVersionInvalidError,
20
- CCIPVersionFeatureUnavailableError,
21
25
  } from '../errors/index.ts'
22
- import type { ExtraArgs, SuiExtraArgsV1 } from '../extra-args.ts'
23
- import { getSuiLeafHasher } from './hasher.ts'
26
+ import {
27
+ CCIPLogsAddressRequiredError,
28
+ CCIPSuiLogInvalidError,
29
+ CCIPTopicsInvalidError,
30
+ } from '../errors/specialized.ts'
31
+ import type { EVMExtraArgsV2, ExtraArgs, SVMExtraArgsV1, SuiExtraArgsV1 } from '../extra-args.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 {
37
+ type AnyMessage,
27
38
  type CCIPExecution,
28
39
  type CCIPMessage,
29
40
  type CCIPRequest,
@@ -40,30 +51,31 @@ import {
40
51
  type WithLogger,
41
52
  ChainFamily,
42
53
  } from '../types.ts'
43
- import { discoverCCIP, discoverOfframp } from './discovery.ts'
44
- import type { CCIPMessage_V1_6_Sui } from './types.ts'
45
- import { bytesToBuffer, decodeAddress, getDataBytes, networkInfo } from '../utils.ts'
46
- 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'
47
64
  import {
48
65
  type SuiManuallyExecuteInput,
49
66
  type TokenConfig,
50
67
  buildManualExecutionPTB,
51
68
  } from './manuallyExec/index.ts'
52
69
  import {
70
+ deriveObjectID,
53
71
  fetchTokenConfigs,
54
- getCcipObjectRef,
55
- getOffRampStateObject,
72
+ getLatestPackageId,
73
+ getObjectRef,
56
74
  getReceiverModule,
57
75
  } from './objects.ts'
76
+ import type { CCIPMessage_V1_6_Sui } from './types.ts'
58
77
 
59
- export const SUI_EXTRA_ARGS_V1_TAG = '21ea4ca9' as const
60
-
61
- type SuiContractDir = {
62
- ccip?: string
63
- onRamp?: string
64
- offRamp?: string
65
- router?: string
66
- }
78
+ const DEFAULT_GAS_LIMIT = 1000000n
67
79
 
68
80
  /**
69
81
  * Sui chain implementation supporting Sui networks.
@@ -80,9 +92,6 @@ export class SuiChain extends Chain<typeof ChainFamily.Sui> {
80
92
  readonly client: SuiClient
81
93
  readonly graphqlClient: SuiGraphQLClient
82
94
 
83
- // contracts dir <chainSelectorName, SuiContractDir>
84
- readonly contractsDir: SuiContractDir
85
-
86
95
  /**
87
96
  * Creates a new SuiChain instance.
88
97
  * @param client - Sui client for interacting with the Sui network.
@@ -93,7 +102,6 @@ export class SuiChain extends Chain<typeof ChainFamily.Sui> {
93
102
 
94
103
  this.client = client
95
104
  this.network = network
96
- this.contractsDir = {}
97
105
 
98
106
  // TODO: Graphql client should come from config
99
107
  let graphqlUrl: string
@@ -138,17 +146,19 @@ export class SuiChain extends Chain<typeof ChainFamily.Sui> {
138
146
  chainId = 'sui:4' // devnet
139
147
  } else {
140
148
  throw new CCIPError(
141
- CCIPErrorCode.NETWORK_FAMILY_UNSUPPORTED,
149
+ CCIPErrorCode.CHAIN_FAMILY_UNSUPPORTED,
142
150
  `Unsupported Sui chain identifier: ${rawChainId}`,
143
151
  )
144
152
  }
145
153
 
146
154
  const network = networkInfo(chainId) as NetworkInfo<typeof ChainFamily.Sui>
147
- return new SuiChain(client, network, ctx)
155
+ const chain = new SuiChain(client, network, ctx)
156
+ return Object.assign(chain, { url })
148
157
  }
149
158
 
150
159
  /** {@inheritDoc Chain.getBlockTimestamp} */
151
- 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)
152
162
  const checkpoint = await this.client.getCheckpoint({
153
163
  id: String(block),
154
164
  })
@@ -174,12 +184,12 @@ export class SuiChain extends Chain<typeof ChainFamily.Sui> {
174
184
  if (txResponse.events?.length) {
175
185
  for (const [i, event] of txResponse.events.entries()) {
176
186
  const eventType = event.type
177
- const packageId = eventType.split('::')[0]
178
- const moduleName = eventType.split('::')[1]
179
- const eventName = eventType.split('::')[2]!
187
+ const splitIdx = eventType.lastIndexOf('::')
188
+ const address = eventType.substring(0, splitIdx)
189
+ const eventName = eventType.substring(splitIdx + 2)
180
190
 
181
191
  events.push({
182
- address: `${packageId}::${moduleName}`,
192
+ address: address,
183
193
  transactionHash: digest,
184
194
  index: i,
185
195
  blockNumber: Number(txResponse.checkpoint || 0),
@@ -200,41 +210,21 @@ export class SuiChain extends Chain<typeof ChainFamily.Sui> {
200
210
 
201
211
  /** {@inheritDoc Chain.getLogs} */
202
212
  async *getLogs(opts: LogFilter & { versionAsHash?: boolean }) {
203
- if (!this.contractsDir.offRamp) {
204
- throw new CCIPContractNotRouterError('OffRamp address not set in contracts directory', 'Sui')
205
- }
213
+ if (!opts.address) throw new CCIPLogsAddressRequiredError()
214
+
206
215
  // Extract the event type from topics
207
- const topic = Array.isArray(opts.topics?.[0]) ? opts.topics[0][0] : opts.topics?.[0] || ''
208
- if (!topic || topic !== 'CommitReportAccepted') {
209
- throw new CCIPVersionFeatureUnavailableError(
210
- 'Event type',
211
- topic || 'unknown',
212
- 'CommitReportAccepted',
213
- )
216
+ if (opts.topics?.length !== 1 || typeof opts.topics[0] !== 'string') {
217
+ throw new CCIPTopicsInvalidError(opts.topics!)
214
218
  }
219
+ const topic = opts.topics[0]
215
220
 
216
- const startTime = opts.startTime ? new Date(opts.startTime * 1000) : new Date(0)
217
- const endTime = opts.endBlock
218
- ? new Date(opts.endBlock)
219
- : new Date(startTime.getTime() + 1 * 24 * 60 * 60 * 1000) // default to +24h
220
-
221
- this.logger.info(
222
- `Fetching Sui events of type ${topic} from ${startTime.toISOString()} to ${endTime.toISOString()}`,
223
- )
224
- const events = await getSuiEventsInTimeRange<CommitEvent>(
225
- this.client,
226
- this.graphqlClient,
227
- `${this.contractsDir.offRamp}::offramp::CommitReportAccepted`,
228
- startTime,
229
- endTime,
230
- )
231
-
232
- for (const event of events) {
233
- 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
234
224
  yield {
235
- address: this.contractsDir.offRamp,
236
- transactionHash: event.transaction?.digest || '',
237
- 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,
238
228
  blockNumber: Number(event.transaction?.effects.checkpoint.sequenceNumber || 0),
239
229
  data: eventData,
240
230
  topics: [topic],
@@ -242,11 +232,6 @@ export class SuiChain extends Chain<typeof ChainFamily.Sui> {
242
232
  }
243
233
  }
244
234
 
245
- /** {@inheritDoc Chain.getMessagesInTx} */
246
- override async getMessagesInTx(_tx: string | ChainTransaction): Promise<CCIPRequest[]> {
247
- return Promise.reject(new CCIPNotImplementedError('SuiChain.getMessagesInTx'))
248
- }
249
-
250
235
  /** {@inheritDoc Chain.getMessagesInBatch} */
251
236
  override async getMessagesInBatch<
252
237
  R extends PickDeep<
@@ -254,25 +239,45 @@ export class SuiChain extends Chain<typeof ChainFamily.Sui> {
254
239
  'lane' | `log.${'topics' | 'address' | 'blockNumber'}` | 'message.sequenceNumber'
255
240
  >,
256
241
  >(
257
- _request: R,
258
- _commit: Pick<CommitReport, 'minSeqNr' | 'maxSeqNr'>,
259
- _opts?: { page?: number },
242
+ request: R,
243
+ commit: Pick<CommitReport, 'minSeqNr' | 'maxSeqNr'>,
244
+ opts?: { page?: number },
260
245
  ): Promise<R['message'][]> {
261
- return Promise.reject(new CCIPNotImplementedError('SuiChain.getMessagesInBatch'))
246
+ return getMessagesInBatch(this, request, commit, opts)
262
247
  }
263
248
 
264
249
  /** {@inheritDoc Chain.typeAndVersion} */
265
- async typeAndVersion(_address: string) {
266
- 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)
267
275
  }
268
276
 
269
277
  /** {@inheritDoc Chain.getRouterForOnRamp} */
270
278
  async getRouterForOnRamp(onRamp: string, _destChainSelector: bigint): Promise<string> {
271
- this.contractsDir.onRamp = onRamp
272
- if (onRamp !== this.contractsDir.onRamp) {
273
- this.contractsDir.onRamp = onRamp
274
- }
275
- return Promise.resolve(this.contractsDir.onRamp)
279
+ // In Sui, the router is the onRamp package itself
280
+ return Promise.resolve(onRamp)
276
281
  }
277
282
 
278
283
  /** {@inheritDoc Chain.getRouterForOffRamp} */
@@ -281,40 +286,40 @@ export class SuiChain extends Chain<typeof ChainFamily.Sui> {
281
286
  }
282
287
 
283
288
  /** {@inheritDoc Chain.getNativeTokenForRouter} */
284
- getNativeTokenForRouter(_router: string): Promise<string> {
289
+ getNativeTokenForRouter(): Promise<string> {
285
290
  // SUI native token is always 0x2::sui::SUI
286
291
  return Promise.resolve('0x2::sui::SUI')
287
292
  }
288
293
 
289
294
  /** {@inheritDoc Chain.getOffRampsForRouter} */
290
295
  async getOffRampsForRouter(router: string, _sourceChainSelector: bigint): Promise<string[]> {
291
- const ccip = await discoverCCIP(this.client, router)
292
- const offramp = await discoverOfframp(this.client, ccip)
293
- this.contractsDir.offRamp = offramp
294
- 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)
295
299
  return [offramp]
296
300
  }
297
301
 
298
302
  /** {@inheritDoc Chain.getOnRampForRouter} */
299
- getOnRampForRouter(_router: string, _destChainSelector: bigint): Promise<string> {
300
- if (!this.contractsDir.onRamp) {
301
- throw new CCIPContractNotRouterError('OnRamp address not set in contracts directory', 'Sui')
302
- }
303
- 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)
304
306
  }
305
307
 
306
308
  /** {@inheritDoc Chain.getOnRampForOffRamp} */
307
309
  async getOnRampForOffRamp(offRamp: string, sourceChainSelector: bigint): Promise<string> {
308
- if (!this.contractsDir.ccip) {
309
- throw new CCIPError(CCIPErrorCode.UNKNOWN, 'CCIP address not set in contracts directory')
310
- }
311
- const offrampPackageId = offRamp
310
+ offRamp = await getLatestPackageId(offRamp, this.client)
312
311
  const functionName = 'get_source_chain_config'
313
- 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)
314
319
 
315
320
  // Get the OffRampState object
316
- const offrampStateObject = await getOffRampStateObject(this.client, offrampPackageId)
317
- 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)
318
323
  // Use the Transaction builder to create a move call
319
324
  const tx = new Transaction()
320
325
 
@@ -379,43 +384,170 @@ export class SuiChain extends Chain<typeof ChainFamily.Sui> {
379
384
  }
380
385
 
381
386
  /** {@inheritDoc Chain.getTokenForTokenPool} */
382
- getTokenForTokenPool(_tokenPool: string): Promise<string> {
383
- 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
384
482
  }
385
483
 
386
484
  /** {@inheritDoc Chain.getTokenInfo} */
387
485
  async getTokenInfo(token: string): Promise<{ symbol: string; decimals: number }> {
388
- // Handle native SUI token
389
- if (token === '0x2::sui::SUI' || token.includes('::sui::SUI')) {
390
- 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')
391
489
  }
392
490
 
393
- try {
394
- // For Coin types, try to fetch metadata from the coin metadata object
395
- // Format: 0xPACKAGE::module::TYPE
396
- const coinMetadata = await this.client.getCoinMetadata({ coinType: token })
491
+ const objectResponse = await this.client.getObject({
492
+ id: normalizedTokenAddress,
493
+ options: { showType: true },
494
+ })
397
495
 
398
- if (coinMetadata) {
399
- return {
400
- symbol: coinMetadata.symbol || 'UNKNOWN',
401
- decimals: coinMetadata.decimals,
402
- }
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}`)
403
502
  }
404
- } catch (error) {
405
- console.log(`Failed to fetch coin metadata for ${token}:`, error)
503
+
504
+ return match[1]
406
505
  }
407
506
 
408
- // Fallback: parse from token type string if possible
409
- const parts = token.split('::')
410
- const symbol = parts[parts.length - 1] || 'UNKNOWN'
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')
534
+ }
535
+
536
+ if (!metadata) {
537
+ throw new CCIPError(CCIPErrorCode.UNKNOWN, 'Error loading Sui token metadata')
538
+ }
411
539
 
412
540
  return {
413
- symbol: symbol.toUpperCase(),
414
- decimals: 9, // Default to 9 decimals (SUI standard)
541
+ symbol: metadata.symbol,
542
+ decimals: metadata.decimals,
415
543
  }
416
544
  }
417
545
 
418
- /** {@inheritDoc Chain.getTokenAdminRegistryFor} */
546
+ /** {@inheritDoc Chain.getBalance} */
547
+ async getBalance(_opts: GetBalanceOpts): Promise<bigint> {
548
+ return Promise.reject(new CCIPNotImplementedError('SuiChain.getBalance'))
549
+ }
550
+
419
551
  /** {@inheritDoc Chain.getTokenAdminRegistryFor} */
420
552
  getTokenAdminRegistryFor(_address: string): Promise<string> {
421
553
  return Promise.reject(new CCIPNotImplementedError())
@@ -424,11 +556,22 @@ export class SuiChain extends Chain<typeof ChainFamily.Sui> {
424
556
  // Static methods for decoding
425
557
  /**
426
558
  * Decodes a CCIP message from a Sui log event.
427
- * @param _log - Log event data.
559
+ * @param log - Log event data.
428
560
  * @returns Decoded CCIPMessage or undefined if not valid.
429
561
  */
430
- static decodeMessage(_log: Log_): CCIPMessage_V1_6_Sui | undefined {
431
- 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
+ }
432
575
  }
433
576
 
434
577
  /**
@@ -438,28 +581,11 @@ export class SuiChain extends Chain<typeof ChainFamily.Sui> {
438
581
  */
439
582
  static decodeExtraArgs(
440
583
  extraArgs: BytesLike,
441
- ): (SuiExtraArgsV1 & { _tag: 'SuiExtraArgsV1' }) | undefined {
442
- const data = getDataBytes(extraArgs)
443
- const hexBytes = toHex(data)
444
- if (!hexBytes.startsWith(SUI_EXTRA_ARGS_V1_TAG)) {
445
- throw new CCIPExtraArgsInvalidError('Sui', hexBytes)
446
- }
447
-
448
- const abiData = '0x' + hexBytes.slice(8)
449
- const decoded = AbiCoder.defaultAbiCoder().decode(
450
- ['tuple(uint256,bool,bytes32,bytes32[])'],
451
- abiData,
452
- )
453
-
454
- const tuple = decoded[0] as readonly [bigint, boolean, string, string[]]
455
-
456
- return {
457
- gasLimit: tuple[0],
458
- allowOutOfOrderExecution: tuple[1],
459
- tokenReceiver: tuple[2],
460
- receiverObjectIds: tuple[3], // Already an array of hex strings
461
- _tag: 'SuiExtraArgsV1',
462
- }
584
+ ):
585
+ | (EVMExtraArgsV2 & { _tag: 'EVMExtraArgsV2' })
586
+ | (SVMExtraArgsV1 & { _tag: 'SVMExtraArgsV1' })
587
+ | undefined {
588
+ return AptosChain.decodeExtraArgs(extraArgs)
463
589
  }
464
590
 
465
591
  /**
@@ -474,30 +600,36 @@ export class SuiChain extends Chain<typeof ChainFamily.Sui> {
474
600
  /**
475
601
  * Decodes commit reports from a log entry.
476
602
  * @param log - The log entry to decode.
477
- * @param _lane - Optional lane information.
603
+ * @param lane - Optional lane information.
478
604
  * @returns Array of decoded commit reports or undefined.
479
605
  */
480
- static decodeCommits(log: Log_, _lane?: Lane): CommitReport[] | undefined {
481
- if (!log.data || typeof log.data !== 'object' || !('unblessed_merkle_roots' in log.data)) {
482
- return
483
- }
484
- const toHexFromBase64 = (b64: string) => '0x' + Buffer.from(b64, 'base64').toString('hex')
485
-
486
- const eventData = log.data as CommitEvent
487
- const unblessedRoots = eventData.unblessed_merkle_roots
488
- if (!Array.isArray(unblessedRoots) || unblessedRoots.length === 0) {
489
- return
490
- }
491
-
492
- return unblessedRoots.map((root) => {
493
- return {
494
- sourceChainSelector: BigInt(root.source_chain_selector),
495
- onRampAddress: toHexFromBase64(root.on_ramp_address),
496
- minSeqNr: BigInt(root.min_seq_nr),
497
- maxSeqNr: BigInt(root.max_seq_nr),
498
- merkleRoot: toHexFromBase64(root.merkle_root),
499
- }
500
- })
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
+ )
501
633
  }
502
634
 
503
635
  /**
@@ -505,52 +637,32 @@ export class SuiChain extends Chain<typeof ChainFamily.Sui> {
505
637
  * @param log - The log entry to decode.
506
638
  * @returns Decoded execution receipt or undefined.
507
639
  */
508
- static decodeReceipt(log: Log_): ExecutionReceipt | undefined {
640
+ static decodeReceipt({
641
+ data,
642
+ topics,
643
+ }: SetOptional<Pick<Log_, 'data' | 'topics'>, 'topics'>): ExecutionReceipt | undefined {
509
644
  // Check if this is an ExecutionStateChanged event
510
- const topic = (Array.isArray(log.topics) ? log.topics[0] : log.topics) as string
511
- if (topic !== 'ExecutionStateChanged') {
512
- return undefined
513
- }
514
-
515
- // Validate log data structure
516
- if (!log.data || typeof log.data !== 'object') {
517
- return undefined
518
- }
645
+ if (topics?.[0] && topics[0] !== 'ExecutionStateChanged') return
519
646
 
520
- const eventData = log.data as {
521
- message_hash?: number[]
522
- message_id?: number[]
523
- sequence_number?: string
524
- source_chain_selector?: string
525
- state?: number
647
+ // Basic log data structure validation
648
+ if (!data || typeof data !== 'object' || !('message_id' in data) || !('state' in data)) {
649
+ return
526
650
  }
527
651
 
528
- // Verify required fields exist
529
- if (
530
- !eventData.message_id ||
531
- !Array.isArray(eventData.message_id) ||
532
- eventData.sequence_number === undefined ||
533
- eventData.state === undefined
534
- ) {
535
- 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
536
658
  }
537
659
 
538
- const toHex = (bytes: BytesLike | number[]) => hexlify(bytesToBuffer(bytes))
539
-
540
- // Convert message_id bytes array to hex string
541
- const messageId = toHex(eventData.message_id)
542
-
543
- // Convert message_hash bytes array to hex string (if present)
544
- const messageHash = eventData.message_hash ? toHex(eventData.message_hash) : undefined
545
-
546
660
  return {
547
- messageId,
661
+ messageId: hexlify(getDataBytes(eventData.message_id)),
548
662
  sequenceNumber: BigInt(eventData.sequence_number),
549
- state: eventData.state as ExecutionState,
550
- sourceChainSelector: eventData.source_chain_selector
551
- ? BigInt(eventData.source_chain_selector)
552
- : undefined,
553
- messageHash,
663
+ state: Number(eventData.state) as ExecutionState,
664
+ sourceChainSelector: BigInt(eventData.source_chain_selector),
665
+ messageHash: hexlify(getDataBytes(eventData.message_hash)),
554
666
  }
555
667
  }
556
668
 
@@ -559,15 +671,17 @@ export class SuiChain extends Chain<typeof ChainFamily.Sui> {
559
671
  * @param bytes - Bytes to convert.
560
672
  * @returns Sui address.
561
673
  */
562
- static getAddress(bytes: BytesLike): string {
674
+ static getAddress(bytes: BytesLike | readonly number[]): string {
563
675
  return AptosChain.getAddress(bytes)
564
676
  }
565
677
 
566
678
  /**
567
679
  * Validates a transaction hash format for Sui
568
680
  */
569
- static isTxHash(_v: unknown): _v is string {
570
- 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)
571
685
  }
572
686
 
573
687
  /**
@@ -598,9 +712,6 @@ export class SuiChain extends Chain<typeof ChainFamily.Sui> {
598
712
 
599
713
  /** {@inheritDoc Chain.getOffchainTokenData} */
600
714
  getOffchainTokenData(request: CCIPRequest): Promise<OffchainTokenData[]> {
601
- if (!('receiverObjectIds' in request.message)) {
602
- throw new CCIPSuiMessageVersionInvalidError()
603
- }
604
715
  // default offchain token data
605
716
  return Promise.resolve(request.message.tokenAmounts.map(() => undefined))
606
717
  }
@@ -618,19 +729,17 @@ export class SuiChain extends Chain<typeof ChainFamily.Sui> {
618
729
  receiverObjectIds?: string[]
619
730
  },
620
731
  ): Promise<CCIPExecution> {
621
- const { execReport } = opts
622
- if (!this.contractsDir.offRamp || !this.contractsDir.ccip) {
623
- throw new CCIPContractNotRouterError(
624
- 'OffRamp or CCIP address not set in contracts directory',
625
- 'Sui',
626
- )
627
- }
732
+ const { execReport, offRamp } = opts
628
733
  const wallet = opts.wallet as Keypair
629
- const ccipObjectRef = await getCcipObjectRef(this.client, this.contractsDir.ccip)
630
- 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)
631
740
  const receiverConfig = await getReceiverModule(
632
741
  this.client,
633
- this.contractsDir.ccip,
742
+ ccip,
634
743
  ccipObjectRef,
635
744
  execReport.message.receiver,
636
745
  )
@@ -638,7 +747,7 @@ export class SuiChain extends Chain<typeof ChainFamily.Sui> {
638
747
  if (execReport.message.tokenAmounts.length !== 0) {
639
748
  tokenConfigs = await fetchTokenConfigs(
640
749
  this.client,
641
- this.contractsDir.ccip,
750
+ ccip,
642
751
  ccipObjectRef,
643
752
  execReport.message.tokenAmounts as CCIPMessage<typeof CCIPVersion.V1_6>['tokenAmounts'],
644
753
  )
@@ -646,8 +755,8 @@ export class SuiChain extends Chain<typeof ChainFamily.Sui> {
646
755
 
647
756
  const input: SuiManuallyExecuteInput = {
648
757
  executionReport: execReport as ExecutionReport<CCIPMessage_V1_6_Sui>,
649
- offrampAddress: this.contractsDir.offRamp,
650
- ccipAddress: this.contractsDir.ccip,
758
+ offrampAddress: offRamp,
759
+ ccipAddress: ccip,
651
760
  ccipObjectRef,
652
761
  offrampStateObject,
653
762
  receiverConfig,
@@ -743,4 +852,52 @@ export class SuiChain extends Chain<typeof ChainFamily.Sui> {
743
852
  async getFeeTokens(_router: string): Promise<never> {
744
853
  return Promise.reject(new CCIPNotImplementedError('SuiChain.getFeeTokens'))
745
854
  }
855
+
856
+ /** {@inheritDoc ChainStatic.buildMessageForDest} */
857
+ static override buildMessageForDest(
858
+ message: Parameters<ChainStatic['buildMessageForDest']>[0],
859
+ ): AnyMessage & { extraArgs: SuiExtraArgsV1 } {
860
+ const gasLimit =
861
+ message.extraArgs && 'gasLimit' in message.extraArgs && message.extraArgs.gasLimit != null
862
+ ? message.extraArgs.gasLimit
863
+ : message.data && dataLength(message.data)
864
+ ? DEFAULT_GAS_LIMIT
865
+ : 0n
866
+ const allowOutOfOrderExecution =
867
+ message.extraArgs &&
868
+ 'allowOutOfOrderExecution' in message.extraArgs &&
869
+ message.extraArgs.allowOutOfOrderExecution != null
870
+ ? message.extraArgs.allowOutOfOrderExecution
871
+ : true
872
+ const tokenReceiver =
873
+ message.extraArgs &&
874
+ 'tokenReceiver' in message.extraArgs &&
875
+ message.extraArgs.tokenReceiver != null
876
+ ? message.extraArgs.tokenReceiver
877
+ : message.tokenAmounts?.length
878
+ ? this.getAddress(message.receiver)
879
+ : '0x0000000000000000000000000000000000000000000000000000000000000000'
880
+ const receiverObjectIds =
881
+ message.extraArgs &&
882
+ 'receiverObjectIds' in message.extraArgs &&
883
+ message.extraArgs.receiverObjectIds?.length
884
+ ? message.extraArgs.receiverObjectIds
885
+ : message.extraArgs && 'accounts' in message.extraArgs && message.extraArgs.accounts?.length
886
+ ? message.extraArgs.accounts // populates receiverObjectIds from accounts
887
+ : []
888
+ const extraArgs: SuiExtraArgsV1 = {
889
+ gasLimit,
890
+ allowOutOfOrderExecution,
891
+ tokenReceiver,
892
+ receiverObjectIds,
893
+ }
894
+ return {
895
+ ...message,
896
+ extraArgs,
897
+ // if tokenReceiver, then message.receiver can (must?) be default
898
+ ...(!!message.tokenAmounts?.length && {
899
+ receiver: '0x0000000000000000000000000000000000000000000000000000000000000000',
900
+ }),
901
+ }
902
+ }
746
903
  }