@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
@@ -5,9 +5,17 @@ import type { SuiClient } from '@mysten/sui/client'
5
5
  import { Transaction } from '@mysten/sui/transactions'
6
6
  import { normalizeSuiAddress } from '@mysten/sui/utils'
7
7
  import { blake2b } from '@noble/hashes/blake2.js'
8
+ import { hexlify, toUtf8Bytes } from 'ethers'
9
+ import { memoize } from 'micro-memoize'
8
10
 
9
11
  import { CCIPDataFormatUnsupportedError } from '../errors/index.ts'
10
12
  import type { CCIPMessage, CCIPVersion } from '../types.ts'
13
+ import { toLeArray } from '../utils.ts'
14
+
15
+ const bcsBytes = (bytes: Uint8Array) => bcs.vector(bcs.u8()).serialize(bytes).toBytes()
16
+
17
+ const HASHING_INTENT_SCOPE_CHILD_OBJECT_ID = 0xf0
18
+ const SUI_FRAMEWORK_ADDRESS = '0x2'
11
19
 
12
20
  /**
13
21
  * Derive a dynamic field object ID using the Sui algorithm
@@ -19,27 +27,24 @@ export function deriveObjectID(parentAddress: string, keyBytes: Uint8Array): str
19
27
  const parentBytes = bcs.Address.serialize(normalizedParent).toBytes()
20
28
 
21
29
  // BCS serialize the key (vector<u8>)
22
- const bcsKeyBytes = bcs.vector(bcs.u8()).serialize(Array.from(keyBytes)).toBytes()
30
+ const bcsKeyBytes = bcsBytes(keyBytes)
31
+ const keyLenBytes = toLeArray(bcsKeyBytes.length, 8) // uint64
23
32
 
24
33
  // Construct TypeTag for DerivedObjectKey<vector<u8>>
25
- const suiFrameworkAddress = bcs.Address.serialize('0x2').toBytes()
34
+ const suiFrameworkAddress = bcs.Address.serialize(SUI_FRAMEWORK_ADDRESS).toBytes()
26
35
  const typeTagBytes = new Uint8Array([
27
36
  0x07, // TypeTag::Struct
28
37
  ...suiFrameworkAddress,
29
- 0x0e, // module length
30
- ...new TextEncoder().encode('derived_object'),
31
- 0x10, // struct name length
32
- ...new TextEncoder().encode('DerivedObjectKey'),
38
+ ...bcsBytes(toUtf8Bytes('derived_object')), //module
39
+ ...bcsBytes(toUtf8Bytes('DerivedObjectKey')), // struct name
33
40
  0x01, // type params count
34
- ...[0x06, 0x01], // vector<u8> TypeTag
41
+ 0x06, // TypeTag::Vector
42
+ 0x01, // TypeTag::U8
35
43
  ])
36
44
 
37
45
  // Build the hash input
38
- const keyLenBytes = new Uint8Array(8)
39
- new DataView(keyLenBytes.buffer).setBigUint64(0, BigInt(bcsKeyBytes.length), true)
40
-
41
46
  const hashInput = new Uint8Array([
42
- 0xf0, // HashingIntentScope::ChildObjectId
47
+ HASHING_INTENT_SCOPE_CHILD_OBJECT_ID,
43
48
  ...parentBytes,
44
49
  ...keyLenBytes,
45
50
  ...bcsKeyBytes,
@@ -49,101 +54,72 @@ export function deriveObjectID(parentAddress: string, keyBytes: Uint8Array): str
49
54
  // Hash with Blake2b-256
50
55
  const hash = blake2b(hashInput, { dkLen: 32 })
51
56
 
52
- // Convert to address string
53
- return normalizeSuiAddress('0x' + Buffer.from(hash).toString('hex'))
57
+ return hexlify(hash)
54
58
  }
55
59
 
56
60
  /**
57
- * Get the CCIPObjectRef ID for a CCIP package
61
+ * Finds the StatePointer object owned by a package.
62
+ * The StatePointer contains a reference to the parent object used for derivation.
58
63
  */
59
- export async function getCcipObjectRef(client: SuiClient, ccipPackageId: string): Promise<string> {
60
- // Get the pointer to find the CCIPObject ID
61
- const pointerResponse = await client.getOwnedObjects({
62
- owner: ccipPackageId,
63
- filter: {
64
- StructType: `${ccipPackageId}::state_object::CCIPObjectRefPointer`,
65
- },
66
- })
67
-
68
- if (!pointerResponse.data.length) {
69
- throw new CCIPDataFormatUnsupportedError(
70
- 'No CCIPObjectRefPointer found for the given packageId',
71
- )
72
- }
73
-
74
- // Get the pointer object to extract ccip_object_id
75
- const pointerId = pointerResponse.data[0]!.data?.objectId
76
- if (!pointerId) {
77
- throw new CCIPDataFormatUnsupportedError('Pointer does not have objectId')
78
- }
79
-
80
- const pointerObject = await client.getObject({
81
- id: pointerId,
82
- options: { showContent: true },
83
- })
84
-
85
- if (pointerObject.data?.content?.dataType !== 'moveObject') {
86
- throw new CCIPDataFormatUnsupportedError('Pointer object is not a Move object')
87
- }
88
-
89
- const ccipObjectId = (pointerObject.data.content.fields as Record<string, unknown>)[
90
- 'ccip_object_id'
91
- ] as string
92
-
93
- if (!ccipObjectId) {
94
- throw new CCIPDataFormatUnsupportedError('Could not find ccip_object_id in pointer')
95
- }
64
+ export const getObjectRef = memoize(
65
+ async function getPackageIds_(address: string, client: SuiClient): Promise<string> {
66
+ let stateObjectName
67
+ if (address.endsWith('::onramp')) stateObjectName = 'OnRampState'
68
+ else if (address.endsWith('::offramp')) stateObjectName = 'OffRampState'
69
+ else stateObjectName = 'CCIPObjectRef'
70
+
71
+ const fullStatePointerType = `${address}::${stateObjectName}Pointer`
72
+
73
+ const ownedObjects = await client.getOwnedObjects({
74
+ owner: address.split('::')[0]!,
75
+ filter: { StructType: fullStatePointerType },
76
+ options: { showContent: true },
77
+ })
96
78
 
97
- // Derive the CCIPObjectRef ID from the parent CCIPObject ID
98
- return deriveObjectID(ccipObjectId, new TextEncoder().encode('CCIPObjectRef'))
99
- }
79
+ const pointer = ownedObjects.data[0]?.data
80
+ if (!pointer?.objectId || pointer.content!.dataType !== 'moveObject')
81
+ throw new CCIPDataFormatUnsupportedError(
82
+ 'No CCIP ObjectRef Pointer found for the given packageId',
83
+ { context: { fullStatePointerType, pointer } },
84
+ )
85
+ // const statePointerObjectId = pointer.objectId
86
+ const parentObjectId = Object.entries(pointer.content!.fields).find(([key]) =>
87
+ key.endsWith('_object_id'),
88
+ )?.[1]
89
+ if (typeof parentObjectId !== 'string')
90
+ throw new CCIPDataFormatUnsupportedError('No parent object id found inthe given pointer', {
91
+ context: { fullStatePointerType, pointer },
92
+ })
93
+ return deriveObjectID(parentObjectId, toUtf8Bytes(stateObjectName))
94
+ },
95
+ { maxArgs: 1, expires: 300e3, async: true },
96
+ )
100
97
 
101
98
  /**
102
- * Get the OffRampState object ID for an offramp package
99
+ * Finds the StatePointer object owned by a package.
100
+ * The StatePointer contains a reference to the parent object used for derivation.
103
101
  */
104
- export async function getOffRampStateObject(
105
- client: SuiClient,
106
- offrampPackageId: string,
107
- ): Promise<string> {
108
- const offrampPointerResponse = await client.getOwnedObjects({
109
- owner: offrampPackageId,
110
- filter: {
111
- StructType: `${offrampPackageId}::offramp::OffRampStatePointer`,
112
- },
113
- })
114
-
115
- if (!offrampPointerResponse.data.length) {
116
- throw new CCIPDataFormatUnsupportedError(
117
- 'No OffRampStatePointer found for the given offramp package',
118
- )
119
- }
120
-
121
- const offrampPointerId = offrampPointerResponse.data[0]!.data?.objectId
122
-
123
- if (!offrampPointerId) {
124
- throw new CCIPDataFormatUnsupportedError('OffRampStatePointer does not have a valid objectId')
125
- }
126
-
127
- const offrampPointerObject = await client.getObject({
128
- id: offrampPointerId,
129
- options: { showContent: true },
130
- })
131
-
132
- if (offrampPointerObject.data?.content?.dataType !== 'moveObject') {
133
- throw new CCIPDataFormatUnsupportedError('OffRamp pointer object is not a Move object')
134
- }
135
-
136
- const offrampObjectId = (offrampPointerObject.data.content.fields as Record<string, unknown>)[
137
- 'off_ramp_object_id'
138
- ] as string
139
-
140
- if (!offrampObjectId) {
141
- throw new CCIPDataFormatUnsupportedError('Could not find off_ramp_object_id in pointer')
142
- }
143
-
144
- // Derive the OffRampState ID from the parent OffRamp Object ID
145
- return deriveObjectID(offrampObjectId, new TextEncoder().encode('OffRampState'))
146
- }
102
+ export const getLatestPackageId = memoize(
103
+ async function getLatestPackageId_(address: string, client: SuiClient): Promise<string> {
104
+ const suffix = address.split('::')[1]
105
+ try {
106
+ const stateObjectId = await getObjectRef(address, client)
107
+ const stateObject = await client.getObject({
108
+ id: stateObjectId,
109
+ options: { showContent: true },
110
+ })
111
+ const stateContent = stateObject.data?.content
112
+ if (stateContent?.dataType !== 'moveObject') return address
113
+ const packageIdsField = (stateContent.fields as Record<string, unknown>)['package_ids']
114
+ if (!Array.isArray(packageIdsField) || packageIdsField.length === 0) return address
115
+ const latest = packageIdsField[packageIdsField.length - 1] as string
116
+ return suffix ? `${latest}::${suffix}` : latest
117
+ } catch {
118
+ return address
119
+ }
120
+ },
121
+ { maxArgs: 1, expires: 60e3, async: true },
122
+ )
147
123
 
148
124
  /**
149
125
  * Get the receiver module configuration from the receiver registry.
package/src/sui/types.ts CHANGED
@@ -15,6 +15,16 @@ export const SuiExtraArgsV1Codec = bcs.struct('SuiExtraArgsV1', {
15
15
  receiverObjectIds: bcs.vector(bcs.vector(bcs.u8())),
16
16
  })
17
17
 
18
+ /** Token amount data structure for Sui CCIP messages. */
19
+ export type SuiTokenAmount = {
20
+ source_pool_address?: string
21
+ dest_token_address?: number[]
22
+ extra_data?: number[]
23
+ amount?: string | number
24
+ dest_exec_data?: number[]
25
+ dest_gas_amount?: string | number
26
+ }
27
+
18
28
  /**
19
29
  * Encodes Sui v1 extra arguments using BCS encoding.
20
30
  * @param args - Sui extra arguments to encode.
@@ -26,3 +36,28 @@ export function encodeSuiExtraArgsV1(args: SuiExtraArgsV1): string {
26
36
  const bcsData = SuiExtraArgsV1Codec.serialize({ ...args, tokenReceiver, receiverObjectIds })
27
37
  return concat([SuiExtraArgsV1Tag, bcsData.toBytes()])
28
38
  }
39
+
40
+ /**
41
+ * Sui-specific CCIP message log structure from events.
42
+ */
43
+ export type SuiCCIPMessageLog = {
44
+ dest_chain_selector: string
45
+ message: {
46
+ data: number[]
47
+ extra_args: number[]
48
+ fee_token: string
49
+ fee_token_amount: string
50
+ fee_value_juels: string
51
+ header: {
52
+ dest_chain_selector: string
53
+ message_id: number[]
54
+ nonce: string
55
+ sequence_number: string
56
+ source_chain_selector: string
57
+ }
58
+ receiver: number[]
59
+ sender: string
60
+ token_amounts: SuiTokenAmount[]
61
+ }
62
+ sequence_number: string
63
+ }
package/src/ton/index.ts CHANGED
@@ -8,7 +8,7 @@ import { type Memoized, memoize } from 'micro-memoize'
8
8
  import type { PickDeep } from 'type-fest'
9
9
 
10
10
  import { streamTransactionsForAddress } from './logs.ts'
11
- import { type ChainContext, type LogFilter, Chain } from '../chain.ts'
11
+ import { type ChainContext, type GetBalanceOpts, type LogFilter, Chain } from '../chain.ts'
12
12
  import {
13
13
  CCIPArgumentInvalidError,
14
14
  CCIPExtraArgsInvalidError,
@@ -21,7 +21,6 @@ import {
21
21
  CCIPWalletInvalidError,
22
22
  } from '../errors/specialized.ts'
23
23
  import { type EVMExtraArgsV2, type ExtraArgs, EVMExtraArgsV2Tag } from '../extra-args.ts'
24
- import { getMessagesInTx } from '../requests.ts'
25
24
  import { supportedChains } from '../supported-chains.ts'
26
25
  import {
27
26
  type CCIPExecution,
@@ -164,13 +163,13 @@ export class TONChain extends Chain<typeof ChainFamily.TON> {
164
163
  ctx?: ChainContext & { fetchFn?: typeof fetch },
165
164
  ): Promise<TONChain> {
166
165
  // Verify connection by getting the latest block
167
- const isTestnet =
166
+ const isMainnet =
168
167
  (
169
168
  await client.getContractState(
170
169
  Address.parse('EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs'), // mainnet USDT
171
170
  )
172
- ).state !== 'active'
173
- return new TONChain(client, networkInfo(isTestnet ? 'ton-testnet' : 'ton-mainnet'), ctx)
171
+ ).state === 'active'
172
+ return new TONChain(client, networkInfo(isMainnet ? 'ton-mainnet' : 'ton-testnet'), ctx)
174
173
  }
175
174
 
176
175
  /**
@@ -264,7 +263,7 @@ export class TONChain extends Chain<typeof ChainFamily.TON> {
264
263
  )
265
264
  const txInfo = await lookupTxByRawHash(
266
265
  cleanHash,
267
- this.network.isTestnet,
266
+ this.network.networkType,
268
267
  this.rateLimitedFetch,
269
268
  this,
270
269
  )
@@ -363,11 +362,6 @@ export class TONChain extends Chain<typeof ChainFamily.TON> {
363
362
  }
364
363
  }
365
364
 
366
- /** {@inheritDoc Chain.getMessagesInTx} */
367
- override async getMessagesInTx(tx: string | ChainTransaction): Promise<CCIPRequest[]> {
368
- return getMessagesInTx(this, typeof tx === 'string' ? await this.getTransaction(tx) : tx)
369
- }
370
-
371
365
  /** {@inheritDoc Chain.getMessagesInBatch} */
372
366
  override async getMessagesInBatch<
373
367
  R extends PickDeep<
@@ -525,6 +519,11 @@ export class TONChain extends Chain<typeof ChainFamily.TON> {
525
519
  }
526
520
  }
527
521
 
522
+ /** {@inheritDoc Chain.getBalance} */
523
+ async getBalance(_opts: GetBalanceOpts): Promise<bigint> {
524
+ return Promise.reject(new CCIPNotImplementedError('TONChain.getBalance'))
525
+ }
526
+
528
527
  /** {@inheritDoc Chain.getTokenAdminRegistryFor} */
529
528
  getTokenAdminRegistryFor(_address: string): Promise<string> {
530
529
  return Promise.reject(new CCIPNotImplementedError('getTokenAdminRegistryFor'))
package/src/ton/utils.ts CHANGED
@@ -2,7 +2,7 @@ import { Cell, Dictionary, beginCell } from '@ton/core'
2
2
  import { hexlify, toBeHex } from 'ethers'
3
3
 
4
4
  import { CCIPTransactionNotFoundError } from '../errors/specialized.ts'
5
- import type { WithLogger } from '../types.ts'
5
+ import { type WithLogger, NetworkType } from '../types.ts'
6
6
  import { bytesToBuffer } from '../utils.ts'
7
7
 
8
8
  /**
@@ -340,14 +340,14 @@ export async function parseJettonContent(
340
340
  * TonCenter V3 provides an index that allows hash-only lookups.
341
341
  *
342
342
  * @param hash - Raw 64-char hex transaction hash
343
- * @param isTestnet - Whether to use testnet API
343
+ * @param networkType - Network type (mainnet or testnet)
344
344
  * @param fetch - Rate-limited fetch function
345
345
  * @param logger - Logger instance
346
346
  * @returns Transaction identifier components needed for V4 API lookup
347
347
  */
348
348
  export async function lookupTxByRawHash(
349
349
  hash: string,
350
- isTestnet: boolean,
350
+ networkType: NetworkType,
351
351
  fetch = globalThis.fetch,
352
352
  { logger = console }: WithLogger = {},
353
353
  ): Promise<{
@@ -355,9 +355,10 @@ export async function lookupTxByRawHash(
355
355
  lt: string
356
356
  hash: string
357
357
  }> {
358
- const baseUrl = isTestnet
359
- ? 'https://testnet.toncenter.com/api/v3/transactions'
360
- : 'https://toncenter.com/api/v3/transactions'
358
+ const baseUrl =
359
+ networkType === NetworkType.Mainnet
360
+ ? 'https://toncenter.com/api/v3/transactions'
361
+ : 'https://testnet.toncenter.com/api/v3/transactions'
361
362
 
362
363
  // TonCenter V3 accepts hex directly
363
364
  const cleanHash = bytesToBuffer(hash).toString('hex')
package/src/types.ts CHANGED
@@ -60,15 +60,26 @@ export type MergeArrayElements<T, U> = {
60
60
  * Enumeration of supported blockchain families.
61
61
  */
62
62
  export const ChainFamily = {
63
- EVM: 'evm',
64
- Solana: 'solana',
65
- Aptos: 'aptos',
66
- Sui: 'sui',
67
- TON: 'ton',
63
+ EVM: 'EVM',
64
+ Solana: 'SVM',
65
+ Aptos: 'APTOS',
66
+ Sui: 'SUI',
67
+ TON: 'TON',
68
+ Unknown: 'UNKNOWN',
68
69
  } as const
69
70
  /** Type representing one of the supported chain families. */
70
71
  export type ChainFamily = (typeof ChainFamily)[keyof typeof ChainFamily]
71
72
 
73
+ /**
74
+ * Enumeration of network types (mainnet vs testnet).
75
+ */
76
+ export const NetworkType = {
77
+ Mainnet: 'MAINNET',
78
+ Testnet: 'TESTNET',
79
+ } as const
80
+ /** Type representing the network environment type. */
81
+ export type NetworkType = (typeof NetworkType)[keyof typeof NetworkType]
82
+
72
83
  /**
73
84
  * Enumeration of supported CCIP protocol versions.
74
85
  */
@@ -88,7 +99,7 @@ type ChainFamilyWithId<F extends ChainFamily> = F extends
88
99
  : F extends typeof ChainFamily.Solana
89
100
  ? { readonly family: F; readonly chainId: string }
90
101
  : F extends typeof ChainFamily.Aptos | typeof ChainFamily.Sui
91
- ? { readonly family: F; readonly chainId: `${F}:${number}` }
102
+ ? { readonly family: F; readonly chainId: `${Lowercase<F>}:${number}` }
92
103
  : never
93
104
 
94
105
  /**
@@ -99,8 +110,8 @@ export type NetworkInfo<F extends ChainFamily = ChainFamily> = {
99
110
  readonly chainSelector: bigint
100
111
  /** Human-readable network name. */
101
112
  readonly name: string
102
- /** Whether this is a testnet. */
103
- readonly isTestnet: boolean
113
+ /** Network environment type. */
114
+ readonly networkType: NetworkType
104
115
  } & ChainFamilyWithId<F>
105
116
 
106
117
  /**
@@ -216,6 +227,12 @@ export const MessageStatus = {
216
227
  Success: 'SUCCESS',
217
228
  /** Message execution failed on destination. */
218
229
  Failed: 'FAILED',
230
+ /** Message is being verified by the CCIP network */
231
+ Verifying: 'VERIFYING',
232
+ /** Message has been verified by the CCIP network */
233
+ Verified: 'VERIFIED',
234
+ /** Unknown status returned by API */
235
+ Unknown: 'UNKNOWN',
219
236
  } as const
220
237
  /** Type representing a CCIP message lifecycle status. */
221
238
  export type MessageStatus = (typeof MessageStatus)[keyof typeof MessageStatus]
package/src/utils.ts CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  toBigInt,
13
13
  } from 'ethers'
14
14
  import { memoize } from 'micro-memoize'
15
+ import yaml from 'yaml'
15
16
 
16
17
  import type { Chain, ChainStatic } from './chain.ts'
17
18
  import {
@@ -24,6 +25,7 @@ import {
24
25
  CCIPTypeVersionInvalidError,
25
26
  HttpStatus,
26
27
  } from './errors/index.ts'
28
+ import { getRetryDelay, shouldRetry } from './errors/utils.ts'
27
29
  import SELECTORS from './selectors.ts'
28
30
  import { supportedChains } from './supported-chains.ts'
29
31
  import { type NetworkInfo, type WithLogger, ChainFamily } from './types.ts'
@@ -103,6 +105,9 @@ export async function getSomeBlockNumberBefore(
103
105
  return beforeBlockNumber
104
106
  }
105
107
 
108
+ /**
109
+ * Checks if a chain is a testnet
110
+ */
106
111
  // memoized so we always output the same object for a given chainId
107
112
  const networkInfoFromChainId = memoize((chainId: NetworkInfo['chainId']): NetworkInfo => {
108
113
  const sel = SELECTORS[chainId]
@@ -112,7 +117,7 @@ const networkInfoFromChainId = memoize((chainId: NetworkInfo['chainId']): Networ
112
117
  chainSelector: sel.selector,
113
118
  name: sel.name,
114
119
  family: sel.family,
115
- isTestnet: !sel.name.includes('-mainnet'),
120
+ networkType: sel.network_type,
116
121
  } as NetworkInfo
117
122
  })
118
123
 
@@ -220,13 +225,23 @@ export function bigIntReviver(_key: string, value: unknown): unknown {
220
225
  return value
221
226
  }
222
227
 
228
+ /**
229
+ * Parses JSON text with BigInt support for large integers.
230
+ * Uses yaml parser which handles integers as BigInt when they exceed safe integer range.
231
+ * @param text - JSON string to parse
232
+ * @returns Parsed object with large integers as BigInt
233
+ */
234
+ export function parseJson<T = unknown>(text: string): T {
235
+ return yaml.parse(text, { intAsBigInt: true }) as T
236
+ }
237
+
223
238
  /**
224
239
  * Decode address from a 32-byte hex string
225
240
  **/
226
241
  export function decodeAddress(address: BytesLike, family: ChainFamily = ChainFamily.EVM): string {
227
242
  const chain = supportedChains[family]
228
243
  if (!chain) throw new CCIPChainFamilyUnsupportedError(family)
229
- return chain.getAddress(getAddressBytes(address))
244
+ return chain.getAddress(address)
230
245
  }
231
246
 
232
247
  /**
@@ -255,7 +270,7 @@ export function decodeOnRampAddress(
255
270
  family: ChainFamily = ChainFamily.EVM,
256
271
  ): string {
257
272
  let decoded = decodeAddress(address, family)
258
- if (family === ChainFamily.Aptos) decoded += '::onramp'
273
+ if (family === ChainFamily.Aptos || family === ChainFamily.Sui) decoded += '::onramp'
259
274
  return decoded
260
275
  }
261
276
 
@@ -299,6 +314,8 @@ export function getDataBytes(data: BytesLike | readonly number[]): Uint8Array {
299
314
  if (Array.isArray(data)) return new Uint8Array(data)
300
315
  if (typeof data === 'string' && data.match(/^[0-9a-f]+[a-f][0-9a-f]+$/i)) data = '0x' + data
301
316
  else if (typeof data === 'string' && data.match(/^0X[0-9a-fA-F]+$/)) data = data.toLowerCase()
317
+ if (typeof data === 'string' && data.startsWith('0x') && data.length % 2)
318
+ data = '0x0' + data.slice(2)
302
319
  if (isBytesLike(data)) {
303
320
  return getBytes(data)
304
321
  } else if (isBase64(data)) {
@@ -322,20 +339,33 @@ export function bytesToBuffer(bytes: BytesLike | readonly number[]): Buffer {
322
339
  * @param address - Address in hex or Base58 format.
323
340
  * @returns Address bytes as Uint8Array.
324
341
  */
325
- export function getAddressBytes(address: BytesLike): Uint8Array {
326
- let bytes: Uint8Array
327
- if (isBytesLike(address)) {
328
- bytes = getBytes(address)
342
+ export function getAddressBytes(address: BytesLike | readonly number[]): Uint8Array {
343
+ let bytes
344
+ if (address instanceof Uint8Array) {
345
+ bytes = address
346
+ } else if (Array.isArray(address)) {
347
+ bytes = new Uint8Array(address)
348
+ } else if (
349
+ typeof address === 'string' &&
350
+ address.match(/^((0x[0-9a-f]*)|[0-9a-f]{40,})(::.*)?$/i)
351
+ ) {
352
+ address = address.split('::')[0]! // discard possible Aptos/Sui module suffix
353
+ // supports with or without (long>=20B) 0x-prefix, odd or even length
354
+ bytes = getBytes(
355
+ address.length % 2
356
+ ? '0x0' + (address.toLowerCase().startsWith('0x') ? address.slice(2) : address)
357
+ : !address.toLowerCase().startsWith('0x')
358
+ ? '0x' + address
359
+ : address,
360
+ )
329
361
  } else {
330
- bytes = bs58.decode(address)
331
- }
332
- if (bytes.length > 20) {
333
- if (
334
- bytes.slice(0, bytes.length - 20).every((b) => b === 0) &&
335
- bytes.slice(-20).some((b) => b !== 0)
336
- ) {
337
- bytes = bytes.slice(-20)
362
+ try {
363
+ const bytes_ = bs58.decode(address as string)
364
+ if (bytes_.length % 32 === 0) bytes = bytes_
365
+ } catch (_) {
366
+ // pass
338
367
  }
368
+ if (!bytes) bytes = decodeBase64(address as string)
339
369
  }
340
370
  return bytes
341
371
  }
@@ -356,7 +386,9 @@ export function convertKeysToCamelCase(
356
386
  mapValues?: (value: unknown, key?: string) => unknown,
357
387
  key?: string,
358
388
  ): unknown {
359
- if (Array.isArray(obj)) {
389
+ if (Array.isArray(obj) && obj.every((v) => typeof v === 'number')) {
390
+ return mapValues ? mapValues(obj, key) : obj
391
+ } else if (Array.isArray(obj)) {
360
392
  return obj.map((v) => convertKeysToCamelCase(v, mapValues, key))
361
393
  }
362
394
 
@@ -379,6 +411,109 @@ export function convertKeysToCamelCase(
379
411
  */
380
412
  export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms).unref())
381
413
 
414
+ /**
415
+ * Configuration for the withRetry utility.
416
+ */
417
+ export type WithRetryConfig = {
418
+ /** Maximum number of retry attempts */
419
+ maxRetries: number
420
+ /** Initial delay in milliseconds before the first retry */
421
+ initialDelayMs: number
422
+ /** Multiplier applied to delay after each retry */
423
+ backoffMultiplier: number
424
+ /** Maximum delay in milliseconds between retries */
425
+ maxDelayMs: number
426
+ /** Whether to respect the error's retryAfterMs hint */
427
+ respectRetryAfterHint: boolean
428
+ /** Optional logger for retry attempts */
429
+ logger?: { debug: (...args: unknown[]) => void; warn: (...args: unknown[]) => void }
430
+ }
431
+
432
+ /**
433
+ * Executes an async operation with retry logic and exponential backoff.
434
+ * Only retries on transient errors (as determined by shouldRetry from errors/utils).
435
+ *
436
+ * @param operation - Async function to execute
437
+ * @param config - Retry configuration
438
+ * @returns Promise resolving to the operation result
439
+ * @throws The last error encountered after all retries are exhausted
440
+ *
441
+ * @example
442
+ * ```typescript
443
+ * const result = await withRetry(
444
+ * () => apiClient.getMessageById(id),
445
+ * {
446
+ * maxRetries: 3,
447
+ * initialDelayMs: 1000,
448
+ * backoffMultiplier: 2,
449
+ * maxDelayMs: 30000,
450
+ * respectRetryAfterHint: true,
451
+ * }
452
+ * )
453
+ * ```
454
+ */
455
+ export async function withRetry<T>(
456
+ operation: () => Promise<T>,
457
+ config: WithRetryConfig,
458
+ ): Promise<T> {
459
+ const {
460
+ maxRetries,
461
+ initialDelayMs,
462
+ backoffMultiplier,
463
+ maxDelayMs,
464
+ respectRetryAfterHint,
465
+ logger,
466
+ } = config
467
+
468
+ let lastError: CCIPError | undefined
469
+ let delay = initialDelayMs
470
+
471
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
472
+ try {
473
+ return await operation()
474
+ } catch (err) {
475
+ lastError = CCIPError.isCCIPError(err) ? err : CCIPError.from(err, 'UNKNOWN')
476
+
477
+ // Only retry on transient errors
478
+ if (!shouldRetry(lastError)) {
479
+ throw lastError
480
+ }
481
+
482
+ // Don't sleep after the last attempt
483
+ if (attempt >= maxRetries) {
484
+ logger?.warn(`All ${maxRetries} retries exhausted:`, lastError.message)
485
+ break
486
+ }
487
+
488
+ // Calculate delay for next retry
489
+ let nextDelay = delay
490
+
491
+ // Respect error's retryAfterMs hint if configured
492
+ if (respectRetryAfterHint) {
493
+ const hintDelay = getRetryDelay(lastError)
494
+ if (hintDelay !== null) {
495
+ nextDelay = Math.max(delay, hintDelay)
496
+ }
497
+ }
498
+
499
+ // Cap at maxDelayMs
500
+ nextDelay = Math.min(nextDelay, maxDelayMs)
501
+
502
+ logger?.debug(
503
+ `Retry attempt ${attempt + 1}/${maxRetries} after ${nextDelay}ms:`,
504
+ lastError.message,
505
+ )
506
+
507
+ await sleep(nextDelay)
508
+
509
+ // Apply exponential backoff for next iteration
510
+ delay = Math.min(delay * backoffMultiplier, maxDelayMs)
511
+ }
512
+ }
513
+
514
+ throw lastError!
515
+ }
516
+
382
517
  /**
383
518
  * Parses a typeAndVersion string into its components.
384
519
  * @param typeAndVersion - String in format "TypeName vX.Y.Z".