@chainlink/ccip-sdk 0.90.2 → 0.91.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 (202) hide show
  1. package/README.md +35 -26
  2. package/dist/aptos/exec.d.ts +4 -5
  3. package/dist/aptos/exec.d.ts.map +1 -1
  4. package/dist/aptos/exec.js +5 -14
  5. package/dist/aptos/exec.js.map +1 -1
  6. package/dist/aptos/hasher.d.ts +18 -0
  7. package/dist/aptos/hasher.d.ts.map +1 -1
  8. package/dist/aptos/hasher.js +18 -0
  9. package/dist/aptos/hasher.js.map +1 -1
  10. package/dist/aptos/index.d.ts +127 -28
  11. package/dist/aptos/index.d.ts.map +1 -1
  12. package/dist/aptos/index.js +199 -70
  13. package/dist/aptos/index.js.map +1 -1
  14. package/dist/aptos/logs.d.ts +18 -0
  15. package/dist/aptos/logs.d.ts.map +1 -1
  16. package/dist/aptos/logs.js +21 -3
  17. package/dist/aptos/logs.js.map +1 -1
  18. package/dist/aptos/send.d.ts +22 -5
  19. package/dist/aptos/send.d.ts.map +1 -1
  20. package/dist/aptos/send.js +23 -15
  21. package/dist/aptos/send.js.map +1 -1
  22. package/dist/aptos/token.d.ts +6 -0
  23. package/dist/aptos/token.d.ts.map +1 -1
  24. package/dist/aptos/token.js +6 -0
  25. package/dist/aptos/token.js.map +1 -1
  26. package/dist/aptos/types.d.ts +16 -1
  27. package/dist/aptos/types.d.ts.map +1 -1
  28. package/dist/aptos/types.js +13 -0
  29. package/dist/aptos/types.js.map +1 -1
  30. package/dist/aptos/utils.d.ts +1 -1
  31. package/dist/aptos/utils.js +1 -1
  32. package/dist/chain.d.ts +185 -99
  33. package/dist/chain.d.ts.map +1 -1
  34. package/dist/chain.js +38 -15
  35. package/dist/chain.js.map +1 -1
  36. package/dist/commits.d.ts +4 -10
  37. package/dist/commits.d.ts.map +1 -1
  38. package/dist/commits.js +2 -1
  39. package/dist/commits.js.map +1 -1
  40. package/dist/evm/const.d.ts +5 -0
  41. package/dist/evm/const.d.ts.map +1 -1
  42. package/dist/evm/const.js +5 -0
  43. package/dist/evm/const.js.map +1 -1
  44. package/dist/evm/errors.d.ts +5 -0
  45. package/dist/evm/errors.d.ts.map +1 -1
  46. package/dist/evm/errors.js +6 -1
  47. package/dist/evm/errors.js.map +1 -1
  48. package/dist/evm/hasher.d.ts +16 -2
  49. package/dist/evm/hasher.d.ts.map +1 -1
  50. package/dist/evm/hasher.js +17 -3
  51. package/dist/evm/hasher.js.map +1 -1
  52. package/dist/evm/index.d.ts +176 -31
  53. package/dist/evm/index.d.ts.map +1 -1
  54. package/dist/evm/index.js +312 -154
  55. package/dist/evm/index.js.map +1 -1
  56. package/dist/evm/logs.d.ts +20 -0
  57. package/dist/evm/logs.d.ts.map +1 -0
  58. package/dist/evm/logs.js +194 -0
  59. package/dist/evm/logs.js.map +1 -0
  60. package/dist/evm/messages.d.ts +11 -2
  61. package/dist/evm/messages.d.ts.map +1 -1
  62. package/dist/evm/messages.js +4 -2
  63. package/dist/evm/messages.js.map +1 -1
  64. package/dist/evm/offchain.d.ts +7 -2
  65. package/dist/evm/offchain.d.ts.map +1 -1
  66. package/dist/evm/offchain.js +12 -7
  67. package/dist/evm/offchain.js.map +1 -1
  68. package/dist/execution.d.ts +19 -62
  69. package/dist/execution.d.ts.map +1 -1
  70. package/dist/execution.js +28 -31
  71. package/dist/execution.js.map +1 -1
  72. package/dist/extra-args.d.ts +35 -5
  73. package/dist/extra-args.d.ts.map +1 -1
  74. package/dist/extra-args.js +10 -5
  75. package/dist/extra-args.js.map +1 -1
  76. package/dist/gas.d.ts +6 -8
  77. package/dist/gas.d.ts.map +1 -1
  78. package/dist/gas.js +7 -9
  79. package/dist/gas.js.map +1 -1
  80. package/dist/hasher/common.d.ts +3 -2
  81. package/dist/hasher/common.d.ts.map +1 -1
  82. package/dist/hasher/common.js +2 -2
  83. package/dist/hasher/common.js.map +1 -1
  84. package/dist/hasher/hasher.d.ts +8 -2
  85. package/dist/hasher/hasher.d.ts.map +1 -1
  86. package/dist/hasher/hasher.js +8 -3
  87. package/dist/hasher/hasher.js.map +1 -1
  88. package/dist/hasher/merklemulti.d.ts +11 -9
  89. package/dist/hasher/merklemulti.d.ts.map +1 -1
  90. package/dist/hasher/merklemulti.js +17 -16
  91. package/dist/hasher/merklemulti.js.map +1 -1
  92. package/dist/index.d.ts +16 -8
  93. package/dist/index.d.ts.map +1 -1
  94. package/dist/index.js +17 -7
  95. package/dist/index.js.map +1 -1
  96. package/dist/requests.d.ts +39 -25
  97. package/dist/requests.d.ts.map +1 -1
  98. package/dist/requests.js +42 -35
  99. package/dist/requests.js.map +1 -1
  100. package/dist/selectors.d.ts +1 -1
  101. package/dist/solana/cleanup.d.ts +14 -10
  102. package/dist/solana/cleanup.d.ts.map +1 -1
  103. package/dist/solana/cleanup.js +35 -33
  104. package/dist/solana/cleanup.js.map +1 -1
  105. package/dist/solana/exec.d.ts +19 -11
  106. package/dist/solana/exec.d.ts.map +1 -1
  107. package/dist/solana/exec.js +86 -163
  108. package/dist/solana/exec.js.map +1 -1
  109. package/dist/solana/hasher.d.ts +7 -2
  110. package/dist/solana/hasher.d.ts.map +1 -1
  111. package/dist/solana/hasher.js +7 -2
  112. package/dist/solana/hasher.js.map +1 -1
  113. package/dist/solana/index.d.ts +202 -84
  114. package/dist/solana/index.d.ts.map +1 -1
  115. package/dist/solana/index.js +367 -252
  116. package/dist/solana/index.js.map +1 -1
  117. package/dist/solana/offchain.d.ts +8 -18
  118. package/dist/solana/offchain.d.ts.map +1 -1
  119. package/dist/solana/offchain.js +29 -83
  120. package/dist/solana/offchain.js.map +1 -1
  121. package/dist/solana/patchBorsh.d.ts +5 -1
  122. package/dist/solana/patchBorsh.d.ts.map +1 -1
  123. package/dist/solana/patchBorsh.js +57 -46
  124. package/dist/solana/patchBorsh.js.map +1 -1
  125. package/dist/solana/send.d.ts +28 -10
  126. package/dist/solana/send.d.ts.map +1 -1
  127. package/dist/solana/send.js +44 -77
  128. package/dist/solana/send.js.map +1 -1
  129. package/dist/solana/types.d.ts +22 -1
  130. package/dist/solana/types.d.ts.map +1 -1
  131. package/dist/solana/types.js +12 -1
  132. package/dist/solana/types.js.map +1 -1
  133. package/dist/solana/utils.d.ts +58 -4
  134. package/dist/solana/utils.d.ts.map +1 -1
  135. package/dist/solana/utils.js +110 -7
  136. package/dist/solana/utils.js.map +1 -1
  137. package/dist/sui/hasher.d.ts +18 -0
  138. package/dist/sui/hasher.d.ts.map +1 -1
  139. package/dist/sui/hasher.js +18 -0
  140. package/dist/sui/hasher.js.map +1 -1
  141. package/dist/sui/index.d.ts +99 -12
  142. package/dist/sui/index.d.ts.map +1 -1
  143. package/dist/sui/index.js +108 -19
  144. package/dist/sui/index.js.map +1 -1
  145. package/dist/sui/types.d.ts +6 -0
  146. package/dist/sui/types.d.ts.map +1 -1
  147. package/dist/sui/types.js +5 -0
  148. package/dist/sui/types.js.map +1 -1
  149. package/dist/supported-chains.d.ts +2 -1
  150. package/dist/supported-chains.d.ts.map +1 -1
  151. package/dist/supported-chains.js.map +1 -1
  152. package/dist/types.d.ts +127 -16
  153. package/dist/types.d.ts.map +1 -1
  154. package/dist/types.js +18 -0
  155. package/dist/types.js.map +1 -1
  156. package/dist/utils.d.ts +67 -46
  157. package/dist/utils.d.ts.map +1 -1
  158. package/dist/utils.js +143 -21
  159. package/dist/utils.js.map +1 -1
  160. package/package.json +13 -9
  161. package/src/aptos/exec.ts +7 -18
  162. package/src/aptos/hasher.ts +18 -0
  163. package/src/aptos/index.ts +288 -110
  164. package/src/aptos/logs.ts +21 -3
  165. package/src/aptos/send.ts +25 -22
  166. package/src/aptos/token.ts +6 -0
  167. package/src/aptos/types.ts +26 -2
  168. package/src/aptos/utils.ts +1 -1
  169. package/src/chain.ts +243 -108
  170. package/src/commits.ts +6 -7
  171. package/src/evm/const.ts +5 -0
  172. package/src/evm/errors.ts +6 -1
  173. package/src/evm/hasher.ts +20 -4
  174. package/src/evm/index.ts +416 -214
  175. package/src/evm/logs.ts +255 -0
  176. package/src/evm/messages.ts +11 -5
  177. package/src/evm/offchain.ts +13 -4
  178. package/src/execution.ts +40 -32
  179. package/src/extra-args.ts +38 -6
  180. package/src/gas.ts +7 -9
  181. package/src/hasher/common.ts +3 -2
  182. package/src/hasher/hasher.ts +12 -4
  183. package/src/hasher/merklemulti.ts +17 -16
  184. package/src/index.ts +29 -23
  185. package/src/requests.ts +64 -46
  186. package/src/selectors.ts +1 -1
  187. package/src/solana/cleanup.ts +49 -34
  188. package/src/solana/exec.ts +128 -272
  189. package/src/solana/hasher.ts +13 -4
  190. package/src/solana/index.ts +483 -356
  191. package/src/solana/offchain.ts +32 -102
  192. package/src/solana/patchBorsh.ts +65 -50
  193. package/src/solana/send.ts +52 -111
  194. package/src/solana/types.ts +44 -3
  195. package/src/solana/utils.ts +143 -19
  196. package/src/sui/hasher.ts +18 -0
  197. package/src/sui/index.ts +143 -31
  198. package/src/sui/types.ts +6 -0
  199. package/src/supported-chains.ts +2 -1
  200. package/src/types.ts +130 -18
  201. package/src/utils.ts +168 -26
  202. package/tsconfig.json +2 -1
@@ -1,23 +1,14 @@
1
- import util from 'util'
1
+ import { Buffer } from 'buffer'
2
2
 
3
- import {
4
- type Idl,
5
- type IdlTypes,
6
- AnchorProvider,
7
- BorshAccountsCoder,
8
- BorshCoder,
9
- Program,
10
- Wallet,
11
- eventDiscriminator,
12
- } from '@coral-xyz/anchor'
3
+ import { type Idl, type IdlTypes, BorshAccountsCoder, BorshCoder, Program } from '@coral-xyz/anchor'
13
4
  import { NATIVE_MINT } from '@solana/spl-token'
14
5
  import {
15
6
  type Commitment,
16
- type ConfirmedSignatureInfo,
17
7
  type ConnectionConfig,
8
+ type Finality,
9
+ type SignaturesForAddressOptions,
18
10
  type VersionedTransactionResponse,
19
11
  Connection,
20
- Keypair,
21
12
  PublicKey,
22
13
  SYSVAR_CLOCK_PUBKEY,
23
14
  SystemProgram,
@@ -31,21 +22,19 @@ import {
31
22
  dataSlice,
32
23
  encodeBase58,
33
24
  encodeBase64,
34
- getBytes,
35
25
  hexlify,
36
26
  isHexString,
37
27
  toBigInt,
38
28
  } from 'ethers'
39
- import moize, { type Moized } from 'moize'
29
+ import { type Memoized, memoize } from 'micro-memoize'
30
+ import type { PickDeep, SetRequired } from 'type-fest'
40
31
 
41
32
  import {
42
- type ChainTransaction,
43
33
  type LogFilter,
44
34
  type RateLimiterState,
45
35
  type TokenInfo,
46
36
  type TokenPoolRemote,
47
37
  Chain,
48
- ChainFamily,
49
38
  } from '../chain.ts'
50
39
  import { type EVMExtraArgsV2, type ExtraArgs, EVMExtraArgsV2Tag } from '../extra-args.ts'
51
40
  import type { LeafHasher } from '../hasher/common.ts'
@@ -57,14 +46,18 @@ import {
57
46
  type CCIPExecution,
58
47
  type CCIPMessage,
59
48
  type CCIPRequest,
49
+ type ChainTransaction,
60
50
  type CommitReport,
61
51
  type ExecutionReceipt,
62
52
  type ExecutionReport,
63
53
  type Lane,
64
54
  type Log_,
55
+ type MergeArrayElements,
65
56
  type NetworkInfo,
66
57
  type OffchainTokenData,
58
+ type WithLogger,
67
59
  CCIPVersion,
60
+ ChainFamily,
68
61
  ExecutionState,
69
62
  } from '../types.ts'
70
63
  import {
@@ -76,9 +69,10 @@ import {
76
69
  networkInfo,
77
70
  parseTypeAndVersion,
78
71
  toLeArray,
72
+ util,
79
73
  } from '../utils.ts'
80
74
  import { cleanUpBuffers } from './cleanup.ts'
81
- import { executeReport } from './exec.ts'
75
+ import { generateUnsignedExecuteReport } from './exec.ts'
82
76
  import { getV16SolanaLeafHasher } from './hasher.ts'
83
77
  import { IDL as BASE_TOKEN_POOL } from './idl/1.6.0/BASE_TOKEN_POOL.ts'
84
78
  import { IDL as BURN_MINT_TOKEN_POOL } from './idl/1.6.0/BURN_MINT_TOKEN_POOL.ts'
@@ -86,9 +80,22 @@ import { IDL as CCIP_CCTP_TOKEN_POOL } from './idl/1.6.0/CCIP_CCTP_TOKEN_POOL.ts
86
80
  import { IDL as CCIP_OFFRAMP_IDL } from './idl/1.6.0/CCIP_OFFRAMP.ts'
87
81
  import { IDL as CCIP_ROUTER_IDL } from './idl/1.6.0/CCIP_ROUTER.ts'
88
82
  import { fetchSolanaOffchainTokenData } from './offchain.ts'
89
- import { ccipSend, getFee } from './send.ts'
90
- import type { CCIPMessage_V1_6_Solana } from './types.ts'
91
- import { bytesToBuffer, getErrorFromLogs, parseSolanaLogs, simulationProvider } from './utils.ts'
83
+ import { generateUnsignedCcipSend, getFee } from './send.ts'
84
+ import { type CCIPMessage_V1_6_Solana, type UnsignedSolanaTx, isWallet } from './types.ts'
85
+ import {
86
+ bytesToBuffer,
87
+ getErrorFromLogs,
88
+ hexDiscriminator,
89
+ parseSolanaLogs,
90
+ simulateAndSendTxs,
91
+ simulationProvider,
92
+ } from './utils.ts'
93
+ import {
94
+ fetchAllMessagesInBatch,
95
+ fetchCCIPRequestById,
96
+ fetchCCIPRequestsInTx,
97
+ } from '../requests.ts'
98
+ import { patchBorsh } from './patchBorsh.ts'
92
99
 
93
100
  const routerCoder = new BorshCoder(CCIP_ROUTER_IDL)
94
101
  const offrampCoder = new BorshCoder(CCIP_OFFRAMP_IDL)
@@ -117,95 +124,101 @@ const unknownTokens: { [mint: string]: string } = {
117
124
  '4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU': 'USDC', // devnet
118
125
  }
119
126
 
120
- // some circular specialized types, but all good with proper references
121
- type SolanaLog = Log_ & { tx: SolanaTransaction }
122
- type SolanaTransaction = ChainTransaction & {
123
- tx: VersionedTransactionResponse
124
- logs: SolanaLog[]
125
- }
126
-
127
- function hexDiscriminator(eventName: string): string {
128
- return hexlify(eventDiscriminator(eventName))
129
- }
127
+ /** Solana-specific log structure with transaction reference and log level. */
128
+ export type SolanaLog = Log_ & { tx: SolanaTransaction; data: string; level: number }
129
+ /** Solana-specific transaction structure with versioned transaction response. */
130
+ export type SolanaTransaction = MergeArrayElements<
131
+ ChainTransaction,
132
+ {
133
+ tx: VersionedTransactionResponse
134
+ logs: readonly SolanaLog[]
135
+ }
136
+ >
130
137
 
138
+ /**
139
+ * Solana chain implementation supporting Solana networks.
140
+ */
131
141
  export class SolanaChain extends Chain<typeof ChainFamily.Solana> {
142
+ static {
143
+ patchBorsh()
144
+ supportedChains[ChainFamily.Solana] = SolanaChain
145
+ }
132
146
  static readonly family = ChainFamily.Solana
133
147
  static readonly decimals = 9
134
148
 
135
- readonly network: NetworkInfo<typeof ChainFamily.Solana>
136
- readonly connection: Connection
137
- readonly commitment: Commitment = 'confirmed'
138
-
139
- _getSignaturesForAddress: (
140
- programId: string,
141
- before?: string,
142
- ) => Promise<ConfirmedSignatureInfo[]>
149
+ connection: Connection
150
+ commitment: Commitment = 'confirmed'
143
151
 
144
- constructor(connection: Connection, network: NetworkInfo) {
145
- super()
152
+ /**
153
+ * Creates a new SolanaChain instance.
154
+ * @param connection - Solana connection instance.
155
+ * @param network - Network information for this chain.
156
+ */
157
+ constructor(connection: Connection, network: NetworkInfo, ctx?: WithLogger) {
158
+ super(network, ctx)
146
159
 
147
- if (network.family !== ChainFamily.Solana) {
148
- throw new Error(`Invalid network family for SolanaChain: ${network.family}`)
149
- }
150
- this.network = network
151
160
  this.connection = connection
152
161
 
153
162
  // Memoize expensive operations
154
- this.typeAndVersion = moize.default(this.typeAndVersion.bind(this), {
163
+ this.typeAndVersion = memoize(this.typeAndVersion.bind(this), {
155
164
  maxArgs: 1,
156
- isPromise: true,
165
+ async: true,
157
166
  })
158
- this.getBlockTimestamp = moize.default(this.getBlockTimestamp.bind(this), {
159
- isPromise: true,
167
+ this.getBlockTimestamp = memoize(this.getBlockTimestamp.bind(this), {
168
+ async: true,
160
169
  maxSize: 100,
161
- updateCacheForKey: (key) => typeof key[key.length - 1] !== 'number',
170
+ forceUpdate: (key) => typeof key[key.length - 1] !== 'number',
162
171
  })
163
- this.getTransaction = moize.default(this.getTransaction.bind(this), {
172
+ this.getTransaction = memoize(this.getTransaction.bind(this), {
164
173
  maxSize: 100,
165
174
  maxArgs: 1,
166
175
  })
167
- this.getWallet = moize.default(this.getWallet.bind(this), { maxSize: 1, maxArgs: 0 })
168
- this.getTokenForTokenPool = moize.default(this.getTokenForTokenPool.bind(this))
169
- this.getTokenInfo = moize.default(this.getTokenInfo.bind(this))
170
- this._getSignaturesForAddress = moize.default(
171
- (programId: string, before?: string) =>
172
- this.connection.getSignaturesForAddress(
173
- new PublicKey(programId),
174
- { limit: 1000, before },
175
- 'confirmed',
176
- ),
176
+ this.getTokenForTokenPool = memoize(this.getTokenForTokenPool.bind(this))
177
+ this.getTokenInfo = memoize(this.getTokenInfo.bind(this))
178
+ this.connection.getSignaturesForAddress = memoize(
179
+ this.connection.getSignaturesForAddress.bind(this.connection),
177
180
  {
178
181
  maxSize: 100,
179
- maxAge: 60000,
180
- maxArgs: 2,
181
- isPromise: true,
182
- updateExpire: true,
183
- // only expire undefined before (i.e. recent getSignaturesForAddress calls)
184
- onExpire: ([, before]) => !before,
182
+ async: true,
183
+ // if options.before is defined, caches for long, otherwise for short (recent signatures)
184
+ expires: (key) => (key[1] ? 2 ** 31 - 1 : 5e3),
185
+ transformKey: ([address, options, commitment]: [
186
+ address: PublicKey,
187
+ options?: SignaturesForAddressOptions,
188
+ commitment?: Finality,
189
+ ]) =>
190
+ [
191
+ address.toBase58(),
192
+ options?.before,
193
+ options?.until,
194
+ options?.limit,
195
+ commitment,
196
+ ] as const,
185
197
  },
186
198
  )
187
199
  // cache account info for 30 seconds
188
- this.connection.getAccountInfo = moize.default(
189
- this.connection.getAccountInfo.bind(this.connection),
190
- {
191
- maxSize: 100,
192
- maxArgs: 2,
193
- maxAge: 30e3,
194
- transformArgs: ([address, commitment]) =>
195
- [(address as PublicKey).toString(), commitment] as const,
196
- },
197
- )
198
-
199
- this._getRouterConfig = moize.default(this._getRouterConfig.bind(this), {
200
- maxArgs: 1,
200
+ this.connection.getAccountInfo = memoize(this.connection.getAccountInfo.bind(this.connection), {
201
+ maxSize: 100,
202
+ maxArgs: 2,
203
+ expires: 30e3,
204
+ transformKey: ([address, commitment]) =>
205
+ [(address as PublicKey).toString(), commitment] as const,
201
206
  })
202
207
 
203
- this.listFeeTokens = moize.default(this.listFeeTokens.bind(this), {
204
- maxArgs: 1,
205
- })
208
+ this._getRouterConfig = memoize(this._getRouterConfig.bind(this), { maxArgs: 1 })
209
+
210
+ this.getFeeTokens = memoize(this.getFeeTokens.bind(this), { maxArgs: 1 })
211
+ this.getOffRampsForRouter = memoize(this.getOffRampsForRouter.bind(this), { maxArgs: 1 })
206
212
  }
207
213
 
208
- static _getConnection(url: string): Connection {
214
+ /**
215
+ * Creates a Solana connection from a URL.
216
+ * @param url - RPC endpoint URL (https://, http://, wss://, or ws://).
217
+ * @param ctx - context containing logger.
218
+ * @returns Solana Connection instance.
219
+ */
220
+ static _getConnection(url: string, ctx?: WithLogger): Connection {
221
+ const { logger = console } = ctx ?? {}
209
222
  if (!url.startsWith('http') && !url.startsWith('ws')) {
210
223
  throw new Error(
211
224
  `Invalid Solana RPC URL format (should be https://, http://, wss://, or ws://): ${url}`,
@@ -214,62 +227,37 @@ export class SolanaChain extends Chain<typeof ChainFamily.Solana> {
214
227
 
215
228
  const config: ConnectionConfig = { commitment: 'confirmed' }
216
229
  if (url.includes('.solana.com')) {
217
- config.fetch = createRateLimitedFetch({
218
- maxRequests: 10,
219
- maxRetries: 3,
220
- windowMs: 11e3,
221
- }) // public nodes
222
- console.warn('Using rate-limited fetch for public solana nodes, commands may be slow')
230
+ config.fetch = createRateLimitedFetch(undefined, ctx) // public nodes
231
+ logger.warn('Using rate-limited fetch for public solana nodes, commands may be slow')
223
232
  }
224
233
 
225
234
  return new Connection(url, config)
226
235
  }
227
236
 
228
- static async fromConnection(connection: Connection): Promise<SolanaChain> {
237
+ /**
238
+ * Creates a SolanaChain instance from an existing connection.
239
+ * @param connection - Solana Connection instance.
240
+ * @param ctx - context containing logger.
241
+ * @returns A new SolanaChain instance.
242
+ */
243
+ static async fromConnection(connection: Connection, ctx?: WithLogger): Promise<SolanaChain> {
229
244
  // Get genesis hash to use as chainId
230
- return new SolanaChain(connection, networkInfo(await connection.getGenesisHash()))
231
- }
232
-
233
- static async fromUrl(url: string): Promise<SolanaChain> {
234
- const connection = this._getConnection(url)
235
- return this.fromConnection(connection)
236
- }
237
-
238
- async destroy(): Promise<void> {
239
- // Solana Connection doesn't have an explicit destroy method
240
- // The memoized functions will be garbage collected when the instance is destroyed
241
- }
242
-
243
- static getWallet(_opts?: { wallet?: unknown }): Promise<Wallet> {
244
- throw new Error('Wallet not implemented')
245
+ return new SolanaChain(connection, networkInfo(await connection.getGenesisHash()), ctx)
245
246
  }
246
247
 
247
248
  /**
248
- * Load wallet
249
- * @param opts - options to load wallet
250
- * @param opts.wallet - private key as 0x or base58 string, or async getter function resolving to
251
- * Wallet instance
252
- * @returns Wallet, after caching in instance
249
+ * Creates a SolanaChain instance from an RPC URL.
250
+ * @param url - RPC endpoint URL.
251
+ * @param ctx - context containing logger.
252
+ * @returns A new SolanaChain instance.
253
253
  */
254
- async getWallet(opts: { wallet?: unknown } = {}): Promise<Wallet> {
255
- try {
256
- if (typeof opts.wallet === 'string')
257
- return new Wallet(
258
- Keypair.fromSecretKey(
259
- opts.wallet.startsWith('0x') ? getBytes(opts.wallet) : bs58.decode(opts.wallet),
260
- ),
261
- )
262
- } catch (_) {
263
- // pass
264
- }
265
- return (this.constructor as typeof SolanaChain).getWallet(opts)
266
- }
267
-
268
- async getWalletAddress(opts?: { wallet?: unknown }): Promise<string> {
269
- return (await this.getWallet(opts)).publicKey.toBase58()
254
+ static async fromUrl(url: string, ctx?: WithLogger): Promise<SolanaChain> {
255
+ const connection = this._getConnection(url, ctx)
256
+ return this.fromConnection(connection, ctx)
270
257
  }
271
258
 
272
259
  // cached
260
+ /** {@inheritDoc Chain.getBlockTimestamp} */
273
261
  async getBlockTimestamp(block: number | 'finalized'): Promise<number> {
274
262
  if (block === 'finalized') {
275
263
  const slot = await this.connection.getSlot('finalized')
@@ -287,7 +275,7 @@ export class SolanaChain extends Chain<typeof ChainFamily.Solana> {
287
275
  return blockTime
288
276
  }
289
277
 
290
- // cached
278
+ /** {@inheritDoc Chain.getTransaction} */
291
279
  async getTransaction(hash: string): Promise<SolanaTransaction> {
292
280
  const tx = await this.connection.getTransaction(hash, {
293
281
  commitment: 'confirmed',
@@ -295,16 +283,15 @@ export class SolanaChain extends Chain<typeof ChainFamily.Solana> {
295
283
  })
296
284
  if (!tx) throw new Error(`Transaction not found: ${hash}`)
297
285
  if (tx.blockTime) {
298
- ;(this.getBlockTimestamp as Moized<typeof this.getBlockTimestamp>).set(
299
- [tx.slot],
300
- Promise.resolve(tx.blockTime),
301
- )
286
+ ;(
287
+ this.getBlockTimestamp as Memoized<typeof this.getBlockTimestamp, { async: true }>
288
+ ).cache.set([tx.slot], Promise.resolve(tx.blockTime))
302
289
  } else {
303
290
  tx.blockTime = await this.getBlockTimestamp(tx.slot)
304
291
  }
305
292
 
306
293
  // Parse logs from transaction using helper function
307
- const logs_: Log_[] = tx.meta?.logMessages?.length
294
+ const logs_ = tx.meta?.logMessages?.length
308
295
  ? parseSolanaLogs(tx.meta?.logMessages).map((l) => ({
309
296
  ...l,
310
297
  transactionHash: hash,
@@ -313,7 +300,6 @@ export class SolanaChain extends Chain<typeof ChainFamily.Solana> {
313
300
  : []
314
301
 
315
302
  const chainTx: SolanaTransaction = {
316
- chain: this,
317
303
  hash,
318
304
  logs: [] as SolanaLog[],
319
305
  blockNumber: tx.slot,
@@ -327,59 +313,70 @@ export class SolanaChain extends Chain<typeof ChainFamily.Solana> {
327
313
  return chainTx
328
314
  }
329
315
 
330
- // implements inner paging logic for this.getLogs
331
- async *_getTransactionsForAddress(
316
+ /**
317
+ * Internal method to get transactions for an address with pagination.
318
+ * @param opts - Log filter options.
319
+ * @returns Async generator of Solana transactions.
320
+ */
321
+ async *getTransactionsForAddress(
332
322
  opts: Omit<LogFilter, 'topics'>,
333
323
  ): AsyncGenerator<SolanaTransaction> {
334
324
  if (!opts.address) throw new Error('Program address is required for Solana log filtering')
335
325
 
336
326
  let allSignatures
327
+ const limit = Math.min(opts?.page || 1000, 1000)
337
328
  if (opts.startBlock || opts.startTime) {
338
329
  // forward collect all matching sigs in array
339
- const allSigs: { signature: string; slot: number; blockTime?: number | null }[] = []
340
- let batch: Awaited<ReturnType<typeof this.connection.getSignaturesForAddress>> | undefined,
341
- popped = false
342
- while (!popped && (batch?.length ?? true)) {
343
- batch = await this._getSignaturesForAddress(
344
- opts.address,
345
- allSigs[allSigs.length - 1]?.signature,
330
+ allSignatures = [] as Awaited<ReturnType<typeof this.connection.getSignaturesForAddress>>
331
+ let batch: typeof allSignatures
332
+ do {
333
+ batch = await this.connection.getSignaturesForAddress(
334
+ new PublicKey(opts.address),
335
+ { limit, before: allSignatures[allSignatures.length - 1]?.signature },
336
+ 'confirmed',
346
337
  )
338
+
347
339
  while (
348
340
  batch.length > 0 &&
349
341
  (batch[batch.length - 1].slot < (opts.startBlock || 0) ||
350
342
  (batch[batch.length - 1].blockTime || -1) < (opts.startTime || 0))
351
343
  ) {
352
- batch.pop() // pop tail of txs which are older than requested start
353
- popped = true
344
+ batch.length-- // truncate tail of txs which are older than requested start
354
345
  }
355
- allSigs.push(...batch)
356
- }
357
- allSigs.reverse()
346
+
347
+ allSignatures.push(...batch) // concat in descending order
348
+ } while (batch.length >= limit)
349
+
350
+ allSignatures.reverse()
351
+
358
352
  while (
359
353
  opts.endBlock &&
360
- allSigs.length > 0 &&
361
- allSigs[allSigs.length - 1].slot > opts.endBlock
354
+ allSignatures.length > 0 &&
355
+ allSignatures[allSignatures.length - 1].slot > opts.endBlock
362
356
  ) {
363
- allSigs.pop() // pop head (after reverse) of txs which are newer than requested end
357
+ allSignatures.length-- // truncate head (after reverse) of txs newer than requested end
364
358
  }
365
- allSignatures = allSigs
366
359
  } else {
367
360
  allSignatures = async function* (this: SolanaChain) {
368
- let batch: { signature: string; slot: number; blockTime?: number | null }[] | undefined
369
- while (batch?.length ?? true) {
370
- batch = await this._getSignaturesForAddress(
371
- opts.address!,
372
- batch?.length
373
- ? batch[batch.length - 1].signature
374
- : opts.endBefore
375
- ? opts.endBefore
376
- : undefined,
361
+ let batch: Awaited<ReturnType<typeof this.connection.getSignaturesForAddress>> | undefined
362
+ do {
363
+ batch = await this.connection.getSignaturesForAddress(
364
+ new PublicKey(opts.address!),
365
+ {
366
+ limit,
367
+ before: batch?.length
368
+ ? batch[batch.length - 1].signature
369
+ : opts.endBefore
370
+ ? opts.endBefore
371
+ : undefined,
372
+ },
373
+ 'confirmed',
377
374
  )
378
375
  for (const sig of batch) {
379
376
  if (opts.endBlock && sig.slot > opts.endBlock) continue
380
377
  yield sig
381
378
  }
382
- }
379
+ } while (batch.length >= limit)
383
380
  }.call(this) // generate backwards until depleting getSignaturesForAddress
384
381
  }
385
382
 
@@ -403,17 +400,16 @@ export class SolanaChain extends Chain<typeof ChainFamily.Solana> {
403
400
  * * Fetches signatures in reverse chronological order (newest first)
404
401
  * * Returns logs in reverse chronological order (newest first)
405
402
  *
406
- * @param opts - Log filter options
407
- * @param opts.startBlock - Starting slot number (inclusive)
408
- * @param opts.startTime - Starting Unix timestamp (inclusive)
409
- * @param opts.endBlock - Ending slot number (inclusive)
410
- * @param opts.address - Program address to filter logs by (required for Solana)
411
- * @param opts.topics - Array of topics to filter logs by (optional);
412
- * either 0x-8B discriminants or event names
413
- * @param.opts.programs - a special option to allow querying by address of interest, but
414
- * yielding matching logs from specific (string address) program or any (true)
415
- * @param opts.commit - Special param for fetching ExecutionReceipts, to narrow down the search
416
- * @returns AsyncIterableIterator of parsed Log_ objects
403
+ * @param opts - Log filter options containing:
404
+ * - `startBlock`: Starting slot number (inclusive)
405
+ * - `startTime`: Starting Unix timestamp (inclusive)
406
+ * - `endBlock`: Ending slot number (inclusive)
407
+ * - `address`: Program address to filter logs by (required for Solana)
408
+ * - `topics`: Array of topics to filter logs by (optional); either 0x-8B discriminants or event names
409
+ * - `programs`: Special option to allow querying by address of interest, but yielding matching
410
+ * logs from specific (string address) program or any (true)
411
+ * - `commit`: Special param for fetching ExecutionReceipts, to narrow down the search
412
+ * @returns AsyncIterableIterator of parsed Log_ objects.
417
413
  */
418
414
  async *getLogs(
419
415
  opts: LogFilter & { sender?: string; programs?: string[] | true; commit?: CommitReport },
@@ -440,7 +436,7 @@ export class SolanaChain extends Chain<typeof ChainFamily.Solana> {
440
436
  }
441
437
 
442
438
  // Process signatures and yield logs
443
- for await (const tx of this._getTransactionsForAddress(opts)) {
439
+ for await (const tx of this.getTransactionsForAddress(opts)) {
444
440
  for (const log of tx.logs) {
445
441
  // Filter and yield logs from the specified program, and which match event discriminant or log prefix
446
442
  if (
@@ -452,16 +448,57 @@ export class SolanaChain extends Chain<typeof ChainFamily.Solana> {
452
448
  ))
453
449
  )
454
450
  continue
455
- yield Object.assign(log, { timestamp: new Date(tx.timestamp * 1000) })
451
+ yield log
456
452
  }
457
453
  }
458
454
  }
459
455
 
456
+ /** {@inheritDoc Chain.fetchRequestsInTx} */
457
+ async fetchRequestsInTx(tx: string | ChainTransaction): Promise<CCIPRequest[]> {
458
+ return fetchCCIPRequestsInTx(this, typeof tx === 'string' ? await this.getTransaction(tx) : tx)
459
+ }
460
+
461
+ /** {@inheritDoc Chain.fetchRequestById} */
462
+ override fetchRequestById(
463
+ messageId: string,
464
+ onRamp?: string,
465
+ opts?: { page?: number },
466
+ ): Promise<CCIPRequest> {
467
+ if (!onRamp) throw new Error('onRamp is required')
468
+ return fetchCCIPRequestById(this, messageId, { address: onRamp, ...opts })
469
+ }
470
+
471
+ /** {@inheritDoc Chain.fetchAllMessagesInBatch} */
472
+ async fetchAllMessagesInBatch<
473
+ R extends PickDeep<
474
+ CCIPRequest,
475
+ 'lane' | `log.${'topics' | 'address' | 'blockNumber'}` | 'message.header.sequenceNumber'
476
+ >,
477
+ >(
478
+ request: R,
479
+ commit: Pick<CommitReport, 'minSeqNr' | 'maxSeqNr'>,
480
+ opts?: { page?: number },
481
+ ): Promise<R['message'][]> {
482
+ const [destChainStatePda] = PublicKey.findProgramAddressSync(
483
+ [Buffer.from('dest_chain_state'), toLeArray(request.lane.destChainSelector, 8)],
484
+ new PublicKey(request.log.address),
485
+ )
486
+ // fetchAllMessagesInBatch pass opts back to getLogs; use it to narrow getLogs filter only to
487
+ // txs touching destChainStatePda
488
+ const opts_: Parameters<SolanaChain['getLogs']>[0] = {
489
+ ...opts,
490
+ programs: [request.log.address],
491
+ address: destChainStatePda.toBase58(),
492
+ }
493
+ return fetchAllMessagesInBatch(this, request, commit, opts_)
494
+ }
495
+
496
+ /** {@inheritDoc Chain.typeAndVersion} */
460
497
  async typeAndVersion(address: string) {
461
498
  const program = new Program(
462
499
  CCIP_OFFRAMP_IDL, // `typeVersion` schema should be the same
463
500
  new PublicKey(address),
464
- simulationProvider(this.connection),
501
+ simulationProvider(this),
465
502
  )
466
503
 
467
504
  // Create the typeVersion instruction
@@ -474,10 +511,12 @@ export class SolanaChain extends Chain<typeof ChainFamily.Solana> {
474
511
  return res
475
512
  }
476
513
 
514
+ /** {@inheritDoc Chain.getRouterForOnRamp} */
477
515
  getRouterForOnRamp(onRamp: string, _destChainSelector: bigint): Promise<string> {
478
516
  return Promise.resolve(onRamp) // Solana's router is also the onRamp
479
517
  }
480
518
 
519
+ /** {@inheritDoc Chain.getRouterForOffRamp} */
481
520
  async getRouterForOffRamp(offRamp: string, _sourceChainSelector: bigint): Promise<string> {
482
521
  const offRamp_ = new PublicKey(offRamp)
483
522
  const program = new Program(CCIP_OFFRAMP_IDL as Idl, offRamp_, {
@@ -500,10 +539,12 @@ export class SolanaChain extends Chain<typeof ChainFamily.Solana> {
500
539
  return router.toBase58()
501
540
  }
502
541
 
542
+ /** {@inheritDoc Chain.getNativeTokenForRouter} */
503
543
  getNativeTokenForRouter(_router: string): Promise<string> {
504
544
  return Promise.resolve(NATIVE_MINT.toBase58())
505
545
  }
506
546
 
547
+ /** {@inheritDoc Chain.getOffRampsForRouter} */
507
548
  async getOffRampsForRouter(router: string, sourceChainSelector: bigint): Promise<string[]> {
508
549
  // feeQuoter is present in router's config, and has a DestChainState account which is updated by
509
550
  // the offramps, so we can use it to narrow the search for the offramp
@@ -524,10 +565,12 @@ export class SolanaChain extends Chain<typeof ChainFamily.Solana> {
524
565
  throw new Error(`Could not find OffRamp events in feeQuoter=${feeQuoter.toString()} txs`)
525
566
  }
526
567
 
568
+ /** {@inheritDoc Chain.getOnRampForRouter} */
527
569
  getOnRampForRouter(router: string, _destChainSelector: bigint): Promise<string> {
528
570
  return Promise.resolve(router) // solana's Router is also the OnRamp
529
571
  }
530
572
 
573
+ /** {@inheritDoc Chain.getOnRampForOffRamp} */
531
574
  async getOnRampForOffRamp(offRamp: string, sourceChainSelector: bigint): Promise<string> {
532
575
  const program = new Program(CCIP_OFFRAMP_IDL, new PublicKey(offRamp), {
533
576
  connection: this.connection,
@@ -548,10 +591,12 @@ export class SolanaChain extends Chain<typeof ChainFamily.Solana> {
548
591
  )
549
592
  }
550
593
 
594
+ /** {@inheritDoc Chain.getCommitStoreForOffRamp} */
551
595
  getCommitStoreForOffRamp(offRamp: string): Promise<string> {
552
596
  return Promise.resolve(offRamp) // Solana supports only CCIP>=1.6, for which OffRamp and CommitStore are the same
553
597
  }
554
598
 
599
+ /** {@inheritDoc Chain.getTokenForTokenPool} */
555
600
  async getTokenForTokenPool(tokenPool: string): Promise<string> {
556
601
  const tokenPoolInfo = await this.connection.getAccountInfo(new PublicKey(tokenPool))
557
602
  if (!tokenPoolInfo) throw new Error(`TokenPool info not found: ${tokenPool}`)
@@ -562,6 +607,7 @@ export class SolanaChain extends Chain<typeof ChainFamily.Solana> {
562
607
  return config.mint.toString()
563
608
  }
564
609
 
610
+ /** {@inheritDoc Chain.getTokenInfo} */
565
611
  async getTokenInfo(token: string): Promise<TokenInfo> {
566
612
  const mint = new PublicKey(token)
567
613
  const mintInfo = await this.connection.getParsedAccountInfo(mint)
@@ -597,7 +643,7 @@ export class SolanaChain extends Chain<typeof ChainFamily.Solana> {
597
643
  }
598
644
  } catch (error) {
599
645
  // Metaplex metadata fetch failed, keep the default values
600
- console.debug(`Failed to fetch Metaplex metadata for token ${token}:`, error)
646
+ this.logger.debug(`Failed to fetch Metaplex metadata for token ${token}:`, error)
601
647
  }
602
648
  }
603
649
 
@@ -611,6 +657,11 @@ export class SolanaChain extends Chain<typeof ChainFamily.Solana> {
611
657
  }
612
658
  }
613
659
 
660
+ /**
661
+ * Fetches token metadata from Metaplex.
662
+ * @param mintPublicKey - Token mint public key.
663
+ * @returns Token name and symbol, or null if not found.
664
+ */
614
665
  async _fetchTokenMetadata(
615
666
  mintPublicKey: PublicKey,
616
667
  ): Promise<{ name: string; symbol: string } | null> {
@@ -668,40 +719,32 @@ export class SolanaChain extends Chain<typeof ChainFamily.Solana> {
668
719
 
669
720
  return name || symbol ? { name, symbol } : null
670
721
  } catch (error) {
671
- console.debug('Error fetching token metadata:', error)
722
+ this.logger.debug('Error fetching token metadata:', error)
672
723
  return null
673
724
  }
674
725
  }
675
726
 
727
+ /**
728
+ * Decodes a CCIP message from a Solana log event.
729
+ * @param log - Log with data field.
730
+ * @returns Decoded CCIPMessage or undefined if not valid.
731
+ */
676
732
  static decodeMessage({ data }: { data: unknown }): CCIPMessage | undefined {
677
733
  if (!data || typeof data !== 'string') return undefined
678
- let eventDataBuffer
734
+
735
+ // Verify the discriminant matches CCIPMessageSent
679
736
  try {
680
- eventDataBuffer = bytesToBuffer(data)
737
+ if (dataSlice(getDataBytes(data), 0, 8) !== hexDiscriminator('CCIPMessageSent')) return
681
738
  } catch (_) {
682
739
  return
683
740
  }
684
741
 
685
- const disc = dataSlice(eventDataBuffer, 0, 8)
686
- if (disc !== hexDiscriminator('CCIPMessageSent')) return
687
-
688
- // Use module-level BorshCoder for decoding structs
689
-
690
- // Manually parse event header (discriminator + event-level fields)
691
- let offset = 8
692
-
693
- // Parse event-level fields
694
- const _destChainSelector = eventDataBuffer.readBigUInt64LE(offset)
695
- offset += 8
696
-
697
- const _sequenceNumber = eventDataBuffer.readBigUInt64LE(offset)
698
- offset += 8
699
-
700
- // Now decode the SVM2AnyRampMessage struct using BorshCoder
701
- const messageBytes = eventDataBuffer.subarray(offset)
702
-
703
- const message: IdlTypes<typeof CCIP_ROUTER_IDL>['SVM2AnyRampMessage'] =
704
- routerCoder.types.decode('SVM2AnyRampMessage', messageBytes)
742
+ const decoded = routerCoder.events.decode<
743
+ (typeof CCIP_ROUTER_IDL)['events'][number] & { name: 'CCIPMessageSent' },
744
+ IdlTypes<typeof CCIP_ROUTER_IDL>
745
+ >(data)
746
+ if (decoded?.name !== 'CCIPMessageSent') return
747
+ const message = decoded.data.message
705
748
 
706
749
  // Convert BN/number types to bigints
707
750
  const sourceChainSelector = BigInt(message.header.sourceChainSelector.toString())
@@ -760,6 +803,11 @@ export class SolanaChain extends Chain<typeof ChainFamily.Solana> {
760
803
  } as CCIPMessage<typeof CCIPVersion.V1_6>
761
804
  }
762
805
 
806
+ /**
807
+ * Decodes extra arguments from Solana CCIP messages.
808
+ * @param extraArgs - Encoded extra arguments bytes.
809
+ * @returns Decoded EVMExtraArgsV2 or undefined if unknown format.
810
+ */
763
811
  static decodeExtraArgs(
764
812
  extraArgs: BytesLike,
765
813
  ): (EVMExtraArgsV2 & { _tag: 'EVMExtraArgsV2' }) | undefined {
@@ -782,6 +830,11 @@ export class SolanaChain extends Chain<typeof ChainFamily.Solana> {
782
830
  }
783
831
  }
784
832
 
833
+ /**
834
+ * Encodes extra arguments for Solana CCIP messages.
835
+ * @param args - Extra arguments to encode.
836
+ * @returns Encoded extra arguments as hex string.
837
+ */
785
838
  static encodeExtraArgs(args: ExtraArgs): string {
786
839
  if ('computeUnits' in args) throw new Error('Solana can only encode EVMExtraArgsV2')
787
840
  const gasLimitUint128Le = toLeArray(args.gasLimit, 16)
@@ -792,6 +845,12 @@ export class SolanaChain extends Chain<typeof ChainFamily.Solana> {
792
845
  ])
793
846
  }
794
847
 
848
+ /**
849
+ * Decodes commit reports from a Solana log event.
850
+ * @param log - Log with data field.
851
+ * @param lane - Lane info for filtering.
852
+ * @returns Array of CommitReport or undefined if not valid.
853
+ */
795
854
  static decodeCommits(
796
855
  log: Pick<Log_, 'data'>,
797
856
  lane?: Omit<Lane, 'destChainSelector'>,
@@ -801,45 +860,27 @@ export class SolanaChain extends Chain<typeof ChainFamily.Solana> {
801
860
  throw new Error('Log data is missing or not a string')
802
861
  }
803
862
 
804
- const eventDataBuffer = bytesToBuffer(log.data)
805
-
806
- // Verify the discriminant matches CommitReportAccepted
807
- const expectedDiscriminant = hexDiscriminator('CommitReportAccepted')
808
- const actualDiscriminant = hexlify(eventDataBuffer.subarray(0, 8))
809
- if (actualDiscriminant !== expectedDiscriminant) return
810
-
811
- // Skip the 8-byte discriminant and decode the event data manually
812
- let offset = 8
813
-
814
- // Decode Option<MerkleRoot> - first byte indicates Some(1) or None(0)
815
- const hasValue = eventDataBuffer.readUInt8(offset)
816
- offset += 1
817
- if (!hasValue) return []
818
-
819
- // Decode MerkleRoot struct using the types decoder
820
- // We need to read the remaining bytes as a MerkleRoot struct
821
- const merkleRootBytes = eventDataBuffer.subarray(offset)
822
-
823
- type MerkleRootData = {
824
- sourceChainSelector: BN
825
- onRampAddress: Buffer
826
- minSeqNr: BN
827
- maxSeqNr: BN
828
- merkleRoot: number[]
863
+ try {
864
+ // Verify the discriminant matches CommitReportAccepted
865
+ if (dataSlice(getDataBytes(log.data), 0, 8) !== hexDiscriminator('CommitReportAccepted'))
866
+ return
867
+ } catch (_) {
868
+ return
829
869
  }
830
870
 
831
- const merkleRootData: MerkleRootData = offrampCoder.types.decode('MerkleRoot', merkleRootBytes)
832
-
833
- if (!merkleRootData) {
834
- throw new Error('Failed to decode MerkleRoot data')
835
- }
871
+ const decoded = offrampCoder.events.decode<
872
+ (typeof CCIP_OFFRAMP_IDL)['events'][number] & { name: 'CommitReportAccepted' },
873
+ IdlTypes<typeof CCIP_OFFRAMP_IDL>
874
+ >(log.data)
875
+ if (decoded?.name !== 'CommitReportAccepted' || !decoded.data?.merkleRoot) return
876
+ const merkleRoot = decoded.data.merkleRoot
836
877
 
837
878
  // Verify the source chain selector matches our lane
838
- const sourceChainSelector = BigInt(merkleRootData.sourceChainSelector.toString())
879
+ const sourceChainSelector = BigInt(merkleRoot.sourceChainSelector.toString())
839
880
 
840
881
  // Convert the onRampAddress from bytes to the proper format
841
882
  const onRampAddress = decodeOnRampAddress(
842
- merkleRootData.onRampAddress,
883
+ merkleRoot.onRampAddress,
843
884
  networkInfo(sourceChainSelector).family,
844
885
  )
845
886
  if (lane) {
@@ -852,49 +893,50 @@ export class SolanaChain extends Chain<typeof ChainFamily.Solana> {
852
893
  {
853
894
  sourceChainSelector,
854
895
  onRampAddress,
855
- minSeqNr: BigInt(merkleRootData.minSeqNr.toString()),
856
- maxSeqNr: BigInt(merkleRootData.maxSeqNr.toString()),
857
- merkleRoot: hexlify(new Uint8Array(merkleRootData.merkleRoot)),
896
+ minSeqNr: BigInt(merkleRoot.minSeqNr.toString()),
897
+ maxSeqNr: BigInt(merkleRoot.maxSeqNr.toString()),
898
+ merkleRoot: hexlify(getDataBytes(merkleRoot.merkleRoot)),
858
899
  },
859
900
  ]
860
901
  }
861
902
 
903
+ /**
904
+ * Decodes an execution receipt from a Solana log event.
905
+ * @param log - Log with data, tx, and index fields.
906
+ * @returns ExecutionReceipt or undefined if not valid.
907
+ */
862
908
  static decodeReceipt(log: Pick<Log_, 'data' | 'tx' | 'index'>): ExecutionReceipt | undefined {
863
909
  // Check if this is a ExecutionStateChanged event by looking at the discriminant
864
910
  if (!log.data || typeof log.data !== 'string') {
865
911
  throw new Error('Log data is missing or not a string')
866
912
  }
867
913
 
868
- // Verify the discriminant matches ExecutionStateChanged
869
- if (dataSlice(getDataBytes(log.data), 0, 8) !== hexDiscriminator('ExecutionStateChanged'))
914
+ try {
915
+ // Verify the discriminant matches ExecutionStateChanged
916
+ if (dataSlice(getDataBytes(log.data), 0, 8) !== hexDiscriminator('ExecutionStateChanged'))
917
+ return
918
+ } catch (_) {
870
919
  return
871
- const eventDataBuffer = bytesToBuffer(log.data)
872
-
873
- // Note: We manually decode the event fields rather than using BorshCoder
874
- // since ExecutionStateChanged is an event, not a defined type
875
-
876
- // Skip the 8-byte discriminant and manually decode the event fields
877
- let offset = 8
878
-
879
- // Decode sourceChainSelector (u64)
880
- const sourceChainSelector = eventDataBuffer.readBigUInt64LE(offset)
881
- offset += 8
882
-
883
- // Decode sequenceNumber (u64)
884
- const sequenceNumber = eventDataBuffer.readBigUInt64LE(offset)
885
- offset += 8
886
-
887
- // Decode messageId ([u8; 32])
888
- const messageId = hexlify(eventDataBuffer.subarray(offset, offset + 32))
889
- offset += 32
920
+ }
890
921
 
891
- // Decode messageHash ([u8; 32])
892
- const messageHash = hexlify(eventDataBuffer.subarray(offset, offset + 32))
893
- offset += 32
922
+ const decoded = offrampCoder.events.decode<
923
+ (typeof CCIP_OFFRAMP_IDL)['events'][number] & { name: 'ExecutionStateChanged' },
924
+ IdlTypes<typeof CCIP_OFFRAMP_IDL>
925
+ >(log.data)
926
+ if (decoded?.name !== 'ExecutionStateChanged') return
927
+ const messageId = hexlify(getDataBytes(decoded.data.messageId))
894
928
 
895
929
  // Decode state enum (MessageExecutionState)
896
930
  // Enum discriminant is a single byte: Untouched=0, InProgress=1, Success=2, Failure=3
897
- let state = eventDataBuffer.readUInt8(offset) as ExecutionState
931
+ let state: ExecutionState
932
+ if (decoded.data.state.inProgress) {
933
+ state = ExecutionState.InProgress
934
+ } else if (decoded.data.state.success) {
935
+ state = ExecutionState.Success
936
+ } else if (decoded.data.state.failure) {
937
+ state = ExecutionState.Failed
938
+ } else throw new Error(`Invalid ExecutionState: ${util.inspect(decoded.data.state)}`)
939
+
898
940
  let returnData
899
941
  if (log.tx) {
900
942
  // use only last receipt per tx+message (i.e. skip intermediary InProgress=1 states for Solana)
@@ -915,15 +957,20 @@ export class SolanaChain extends Chain<typeof ChainFamily.Solana> {
915
957
  }
916
958
 
917
959
  return {
918
- sourceChainSelector,
919
- sequenceNumber,
960
+ sourceChainSelector: BigInt(decoded.data.sourceChainSelector.toString()),
961
+ sequenceNumber: BigInt(decoded.data.sequenceNumber.toString()),
920
962
  messageId,
921
- messageHash,
963
+ messageHash: hexlify(getDataBytes(decoded.data.messageHash)),
922
964
  state,
923
965
  returnData,
924
966
  }
925
967
  }
926
968
 
969
+ /**
970
+ * Converts bytes to a Solana address (Base58).
971
+ * @param bytes - Bytes to convert.
972
+ * @returns Base58-encoded Solana address.
973
+ */
927
974
  static getAddress(bytes: BytesLike): string {
928
975
  try {
929
976
  if (typeof bytes === 'string' && bs58.decode(bytes).length === 32) return bytes
@@ -933,10 +980,16 @@ export class SolanaChain extends Chain<typeof ChainFamily.Solana> {
933
980
  return encodeBase58(getDataBytes(bytes))
934
981
  }
935
982
 
936
- static getDestLeafHasher(lane: Lane): LeafHasher<typeof CCIPVersion.V1_6> {
937
- return getV16SolanaLeafHasher(lane)
983
+ /**
984
+ * Gets the leaf hasher for Solana destination chains.
985
+ * @param lane - Lane configuration.
986
+ * @returns Leaf hasher function.
987
+ */
988
+ static getDestLeafHasher(lane: Lane, ctx?: WithLogger): LeafHasher<typeof CCIPVersion.V1_6> {
989
+ return getV16SolanaLeafHasher(lane, ctx)
938
990
  }
939
991
 
992
+ /** {@inheritDoc Chain.getTokenAdminRegistryFor} */
940
993
  async getTokenAdminRegistryFor(address: string): Promise<string> {
941
994
  const [type] = await this.typeAndVersion(address)
942
995
  if (!type.includes('Router')) throw new Error(`Not a Router: ${address} is ${type}`)
@@ -944,79 +997,161 @@ export class SolanaChain extends Chain<typeof ChainFamily.Solana> {
944
997
  return address
945
998
  }
946
999
 
1000
+ /** {@inheritDoc Chain.getFee} */
1001
+ getFee(router: string, destChainSelector: bigint, message: AnyMessage): Promise<bigint> {
1002
+ return getFee(this, router, destChainSelector, message)
1003
+ }
1004
+
947
1005
  /**
948
- * Get the fee required to send a CCIP message from the Solana router.
1006
+ * Raw/unsigned version of [[sendMessage]]
1007
+ *
1008
+ * @param sender - sender/feePayer address
1009
+ * @param router - router address
1010
+ * @param destChainSelector - destination chain selector
1011
+ * @param message - AnyMessage to send (with or without fee)
1012
+ * @param approveMax - approve max amount of tokens if needed, instead of only what's needed
1013
+ * @returns instructions - array of instructions; `ccipSend` is last, after any approval
1014
+ * lookupTables - array of lookup tables for `ccipSend` call
1015
+ * mainIndex - instructions.length - 1
949
1016
  */
950
- getFee(router: string, destChainSelector: bigint, message: AnyMessage): Promise<bigint> {
951
- return getFee(this.connection, router, destChainSelector, message)
1017
+ async generateUnsignedSendMessage(
1018
+ sender: string,
1019
+ router: string,
1020
+ destChainSelector: bigint,
1021
+ message: AnyMessage & { fee?: bigint },
1022
+ opts?: { approveMax?: boolean },
1023
+ ): Promise<UnsignedSolanaTx> {
1024
+ if (!message.fee) message.fee = await this.getFee(router, destChainSelector, message)
1025
+ return generateUnsignedCcipSend(
1026
+ this,
1027
+ new PublicKey(sender),
1028
+ new PublicKey(router),
1029
+ destChainSelector,
1030
+ message as SetRequired<typeof message, 'fee'>,
1031
+ opts,
1032
+ )
952
1033
  }
953
1034
 
1035
+ /** {@inheritDoc Chain.sendMessage} */
954
1036
  async sendMessage(
955
- router_: string,
1037
+ router: string,
956
1038
  destChainSelector: bigint,
957
- message: AnyMessage & { fee: bigint },
958
- opts?: { wallet?: unknown; approveMax?: boolean },
959
- ): Promise<ChainTransaction> {
960
- const wallet = await this.getWallet(opts)
961
-
962
- const router = new Program(
963
- CCIP_ROUTER_IDL,
964
- new PublicKey(router_),
965
- new AnchorProvider(this.connection, wallet, { commitment: this.commitment }),
1039
+ message: AnyMessage & { fee?: bigint },
1040
+ opts: { wallet: unknown; approveMax?: boolean },
1041
+ ): Promise<CCIPRequest> {
1042
+ const wallet = opts.wallet
1043
+ if (!isWallet(wallet)) throw new Error(`Expected Wallet, got=${util.inspect(wallet)}`)
1044
+ const unsigned = await this.generateUnsignedSendMessage(
1045
+ wallet.publicKey.toBase58(),
1046
+ router,
1047
+ destChainSelector,
1048
+ message,
1049
+ opts,
966
1050
  )
967
- const { hash } = await ccipSend(router, destChainSelector, message, opts)
968
- return this.getTransaction(hash)
1051
+
1052
+ const hash = await simulateAndSendTxs(this, wallet, unsigned)
1053
+ return (await this.fetchRequestsInTx(await this.getTransaction(hash)))[0]
969
1054
  }
970
1055
 
1056
+ /** {@inheritDoc Chain.fetchOffchainTokenData} */
971
1057
  async fetchOffchainTokenData(request: CCIPRequest): Promise<OffchainTokenData[]> {
972
- return fetchSolanaOffchainTokenData(this.connection, request)
1058
+ return fetchSolanaOffchainTokenData(request, this)
973
1059
  }
974
1060
 
975
- async executeReport(
1061
+ /**
1062
+ * Raw/unsigned version of [[executeReport]]
1063
+ * @param payer - payer address of the execution transaction
1064
+ * @param offRamp - OffRamp contract address
1065
+ * @param execReport_ - ExecutionReport of a dest=Solana message
1066
+ * @param opts - execute report options
1067
+ * - forceBuffer - Whether to force the use of a buffer account
1068
+ * - forceLookupTable - Whether to force creation of a lookup table for the call
1069
+ * @returns instructions - array of instructions to execute the report
1070
+ * lookupTables - array of lookup tables for `manuallyExecute` call
1071
+ * mainIndex - index of the `manuallyExecute` instruction in the array; last unless
1072
+ * forceLookupTable is set, in which case last is ALT deactivation tx, and manuallyExecute is
1073
+ * second to last
1074
+ */
1075
+ async generateUnsignedExecuteReport(
1076
+ payer: string,
976
1077
  offRamp: string,
977
1078
  execReport_: ExecutionReport,
978
- opts?: {
979
- wallet?: string
1079
+ opts?: { forceBuffer?: boolean; forceLookupTable?: boolean },
1080
+ ): Promise<UnsignedSolanaTx> {
1081
+ if (!('computeUnits' in execReport_.message))
1082
+ throw new Error("ExecutionReport's message not for Solana")
1083
+ const execReport = execReport_ as ExecutionReport<CCIPMessage_V1_6_Solana>
1084
+ const offRamp_ = new PublicKey(offRamp)
1085
+ return generateUnsignedExecuteReport(this, new PublicKey(payer), offRamp_, execReport, opts)
1086
+ }
1087
+
1088
+ /** {@inheritDoc Chain.executeReport} */
1089
+ async executeReport(
1090
+ offRamp: string,
1091
+ execReport: ExecutionReport,
1092
+ opts: {
1093
+ wallet: unknown
980
1094
  gasLimit?: number
981
1095
  forceLookupTable?: boolean
982
1096
  forceBuffer?: boolean
983
- clearLeftoverAccounts?: boolean
984
- dontWait?: boolean
1097
+ waitDeactivation?: boolean
985
1098
  },
986
1099
  ): Promise<ChainTransaction> {
987
- if (!('computeUnits' in execReport_.message))
988
- throw new Error("ExecutionReport's message not for Solana")
989
- const execReport = execReport_ as ExecutionReport<CCIPMessage_V1_6_Solana>
990
-
991
- const wallet = await this.getWallet(opts)
992
- const provider = new AnchorProvider(this.connection, wallet, { commitment: this.commitment })
993
- const offrampProgram = new Program(CCIP_OFFRAMP_IDL, new PublicKey(offRamp), provider)
1100
+ const wallet = opts.wallet
1101
+ if (!isWallet(wallet)) throw new Error(`Expected Wallet, got=${util.inspect(wallet)}`)
994
1102
 
995
- const rep = await executeReport({ offrampProgram, execReport, ...opts })
996
- if (opts?.clearLeftoverAccounts) {
1103
+ let hash
1104
+ do {
997
1105
  try {
998
- await this.cleanUpBuffers(opts)
1106
+ const unsigned = await this.generateUnsignedExecuteReport(
1107
+ wallet.publicKey.toBase58(),
1108
+ offRamp,
1109
+ execReport,
1110
+ opts,
1111
+ )
1112
+ hash = await simulateAndSendTxs(this, wallet, unsigned, opts?.gasLimit)
999
1113
  } catch (err) {
1000
- console.warn('Error while trying to clean up buffers:', err)
1114
+ if (
1115
+ !(err instanceof Error) ||
1116
+ !['encoding overruns Uint8Array', 'too large'].some((e) => err.message.includes(e))
1117
+ )
1118
+ throw err
1119
+ // in case of failure to serialize a report, first try buffering (because it gets
1120
+ // auto-closed upon successful execution), then ALTs (need a grace period ~3min after
1121
+ // deactivation before they can be closed/recycled)
1122
+ if (!opts?.forceBuffer) opts = { ...opts, forceBuffer: true }
1123
+ else if (!opts?.forceLookupTable) opts = { ...opts, forceLookupTable: true }
1124
+ else throw err
1001
1125
  }
1126
+ } while (!hash)
1127
+
1128
+ try {
1129
+ await this.cleanUpBuffers(opts)
1130
+ } catch (err) {
1131
+ this.logger.warn('Error while trying to clean up buffers:', err)
1002
1132
  }
1003
- return this.getTransaction(rep.hash)
1133
+ return this.getTransaction(hash)
1004
1134
  }
1005
1135
 
1006
1136
  /**
1007
1137
  * Clean up and recycle buffers and address lookup tables owned by wallet
1008
- * CAUTION: this will close ANY lookup table owned by this wallet
1009
- * @param wallet - wallet options
1010
- * @param dontWait - Whether to skip waiting for lookup table deactivation cool down period
1011
- * (513 slots) to pass before closing; by default, we deactivate (if needed) and wait to close
1012
- * before returning from this method
1138
+ * @param opts - cleanUp options
1139
+ * - wallet - wallet instance to sign txs
1140
+ * - waitDeactivation - Whether to wait for lookup table deactivation cool down period
1141
+ * (513 slots) to pass before closing; by default, we deactivate (if needed) and move on, to
1142
+ * close other ready ALTs
1013
1143
  */
1014
- async cleanUpBuffers(opts?: { wallet?: string; dontWait?: boolean }): Promise<void> {
1015
- const wallet = await this.getWallet(opts)
1016
- const provider = new AnchorProvider(this.connection, wallet, { commitment: this.commitment })
1017
- await cleanUpBuffers(provider, this.getLogs.bind(this), opts)
1144
+ async cleanUpBuffers(opts: { wallet: unknown; waitDeactivation?: boolean }): Promise<void> {
1145
+ const wallet = opts.wallet
1146
+ if (!isWallet(wallet)) throw new Error(`Expected Wallet, got=${util.inspect(wallet)}`)
1147
+ await cleanUpBuffers(this, wallet, this.getLogs.bind(this), opts)
1018
1148
  }
1019
1149
 
1150
+ /**
1151
+ * Parses raw Solana data into typed structures.
1152
+ * @param data - Raw data to parse.
1153
+ * @returns Parsed data or undefined.
1154
+ */
1020
1155
  static parse(data: unknown) {
1021
1156
  if (!data) return
1022
1157
  try {
@@ -1044,13 +1179,9 @@ export class SolanaChain extends Chain<typeof ChainFamily.Solana> {
1044
1179
  /**
1045
1180
  * Solana optimization: we use getProgramAccounts with
1046
1181
  */
1047
- async fetchCommitReport(
1182
+ override async fetchCommitReport(
1048
1183
  commitStore: string,
1049
- request: {
1050
- lane: Lane
1051
- message: { header: { sequenceNumber: bigint } }
1052
- timestamp?: number
1053
- },
1184
+ request: PickDeep<CCIPRequest, 'lane' | 'message.header.sequenceNumber' | 'tx.timestamp'>,
1054
1185
  hints?: { startBlock?: number; page?: number },
1055
1186
  ): Promise<CCIPCommit> {
1056
1187
  const commitsAroundSeqNum = await this.connection.getProgramAccounts(
@@ -1069,8 +1200,8 @@ export class SolanaChain extends Chain<typeof ChainFamily.Solana> {
1069
1200
  bytes: encodeBase58(toLeArray(request.lane.sourceChainSelector, 8)),
1070
1201
  },
1071
1202
  },
1072
- // dirty trick: memcmp report.min with msg.sequenceNumber's without least-significant byte;
1073
- // this should be ~256 around seqNum, i.e. big chance of a match
1203
+ // hack: memcmp report.min with msg.sequenceNumber's without least-significant byte;
1204
+ // this should be ~256 around seqNum, i.e. big chance of a match; requires PDAs to be alive
1074
1205
  {
1075
1206
  memcmp: {
1076
1207
  offset: 8 + 1 + 8 + 32 + 8 + 1,
@@ -1108,44 +1239,34 @@ export class SolanaChain extends Chain<typeof ChainFamily.Solana> {
1108
1239
  return super.fetchCommitReport(commitStore, request, hints)
1109
1240
  }
1110
1241
 
1111
- // specialized override with stricter address-of-interest
1112
- async *fetchExecutionReceipts(
1242
+ /** {@inheritDoc Chain.fetchExecutionReceipts} */
1243
+ override async *fetchExecutionReceipts(
1113
1244
  offRamp: string,
1114
- messageIds: Set<string>,
1115
- hints?: { startBlock?: number; startTime?: number; page?: number; commit?: CommitReport },
1116
- ): AsyncGenerator<CCIPExecution> {
1117
- if (!hints?.commit) {
1118
- // if no commit, fall back to generic implementation
1119
- yield* super.fetchExecutionReceipts(offRamp, messageIds, hints)
1120
- return
1121
- }
1122
- // otherwise, use `commit_report` PDA as more specialized address
1123
- const [commitReportPda] = PublicKey.findProgramAddressSync(
1124
- [
1125
- Buffer.from('commit_report'),
1126
- toLeArray(hints.commit.sourceChainSelector, 8),
1127
- bytesToBuffer(hints.commit.merkleRoot),
1128
- ],
1129
- new PublicKey(offRamp),
1130
- )
1131
- // rest is similar to generic implemenetation
1132
- const onlyLast = !hints?.startBlock && !hints?.startTime // backwards
1133
- for await (const log of this.getLogs({
1134
- ...hints,
1135
- programs: [offRamp],
1136
- address: commitReportPda.toBase58(),
1137
- topics: ['ExecutionStateChanged'],
1138
- })) {
1139
- const receipt = (this.constructor as typeof SolanaChain).decodeReceipt(log)
1140
- if (!receipt || !messageIds.has(receipt.messageId)) continue
1141
- if (onlyLast || receipt.state === ExecutionState.Success) messageIds.delete(receipt.messageId)
1142
-
1143
- const timestamp = await this.getBlockTimestamp(log.blockNumber)
1144
- yield { receipt, log, timestamp }
1145
- if (!messageIds.size) break
1245
+ request: PickDeep<CCIPRequest, 'lane' | 'message.header.messageId' | 'tx.timestamp'>,
1246
+ commit?: CCIPCommit,
1247
+ opts?: { page?: number },
1248
+ ): AsyncIterableIterator<CCIPExecution> {
1249
+ let opts_: Parameters<SolanaChain['getLogs']>[0] | undefined = opts
1250
+ if (commit) {
1251
+ // if we know of commit, use `commit_report` PDA as more specialized address
1252
+ const [commitReportPda] = PublicKey.findProgramAddressSync(
1253
+ [
1254
+ Buffer.from('commit_report'),
1255
+ toLeArray(request.lane.sourceChainSelector, 8),
1256
+ bytesToBuffer(commit.report.merkleRoot),
1257
+ ],
1258
+ new PublicKey(offRamp),
1259
+ )
1260
+ opts_ = {
1261
+ ...opts,
1262
+ programs: [offRamp],
1263
+ address: commitReportPda.toBase58(),
1264
+ }
1146
1265
  }
1266
+ yield* super.fetchExecutionReceipts(offRamp, request, commit, opts_)
1147
1267
  }
1148
1268
 
1269
+ /** {@inheritDoc Chain.getRegistryTokenConfig} */
1149
1270
  async getRegistryTokenConfig(
1150
1271
  registry: string,
1151
1272
  token: string,
@@ -1201,6 +1322,7 @@ export class SolanaChain extends Chain<typeof ChainFamily.Solana> {
1201
1322
  return config
1202
1323
  }
1203
1324
 
1325
+ /** {@inheritDoc Chain.getTokenPoolConfigs} */
1204
1326
  async getTokenPoolConfigs(tokenPool: string): Promise<{
1205
1327
  token: string
1206
1328
  router: string
@@ -1230,6 +1352,7 @@ export class SolanaChain extends Chain<typeof ChainFamily.Solana> {
1230
1352
  }
1231
1353
  }
1232
1354
 
1355
+ /** {@inheritDoc Chain.getTokenPoolRemotes} */
1233
1356
  async getTokenPoolRemotes(
1234
1357
  tokenPool: string,
1235
1358
  remoteChainSelector?: bigint,
@@ -1379,13 +1502,14 @@ export class SolanaChain extends Chain<typeof ChainFamily.Solana> {
1379
1502
  outboundRateLimiterState,
1380
1503
  }
1381
1504
  } catch (err) {
1382
- console.warn('Failed to decode ChainConfig account:', err)
1505
+ this.logger.warn('Failed to decode ChainConfig account:', err)
1383
1506
  }
1384
1507
  }
1385
1508
 
1386
1509
  return remotes
1387
1510
  }
1388
1511
 
1512
+ /** {@inheritDoc Chain.getSupportedTokens} */
1389
1513
  async getSupportedTokens(router: string): Promise<string[]> {
1390
1514
  // `mint` offset in TokenAdminRegistry account data; more robust against changes in layout
1391
1515
  const mintOffset = 8 + 1 + 32 + 32 + 32 + 16 * 2 // = 137
@@ -1413,7 +1537,8 @@ export class SolanaChain extends Chain<typeof ChainFamily.Solana> {
1413
1537
  return res
1414
1538
  }
1415
1539
 
1416
- async listFeeTokens(router: string): Promise<Record<string, TokenInfo>> {
1540
+ /** {@inheritDoc Chain.getFeeTokens} */
1541
+ async getFeeTokens(router: string): Promise<Record<string, TokenInfo>> {
1417
1542
  const { feeQuoter } = await this._getRouterConfig(router)
1418
1543
  const tokenConfigs = await this.connection.getProgramAccounts(feeQuoter, {
1419
1544
  filters: [
@@ -1430,14 +1555,18 @@ export class SolanaChain extends Chain<typeof ChainFamily.Solana> {
1430
1555
  return Object.fromEntries(
1431
1556
  await Promise.all(
1432
1557
  tokenConfigs.map(async (acc) => {
1433
- const token = new PublicKey(acc.account.data.subarray(10, 10 + 32))
1434
- return [token.toBase58(), await this.getTokenInfo(token.toBase58())] as const
1558
+ const token = new PublicKey(acc.account.data.subarray(10, 10 + 32)).toBase58()
1559
+ return [token, await this.getTokenInfo(token)] as const
1435
1560
  }),
1436
1561
  ),
1437
1562
  )
1438
1563
  }
1439
1564
 
1440
- // cached
1565
+ /**
1566
+ * Gets the router configuration from the Config PDA.
1567
+ * @param router - Router program address.
1568
+ * @returns Router configuration including feeQuoter.
1569
+ */
1441
1570
  async _getRouterConfig(router: string) {
1442
1571
  const program = new Program(CCIP_ROUTER_IDL, new PublicKey(router), {
1443
1572
  connection: this.connection,
@@ -1450,5 +1579,3 @@ export class SolanaChain extends Chain<typeof ChainFamily.Solana> {
1450
1579
  return program.account.config.fetch(configPda)
1451
1580
  }
1452
1581
  }
1453
-
1454
- supportedChains[ChainFamily.Solana] = SolanaChain