@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,13 +1,14 @@
1
- import { type BN, BorshCoder, EventParser } from '@coral-xyz/anchor'
2
- import { type Connection, PublicKey } from '@solana/web3.js'
1
+ import { type BN, BorshCoder } from '@coral-xyz/anchor'
2
+ import type { PublicKey } from '@solana/web3.js'
3
3
  import { hexlify } from 'ethers'
4
4
 
5
5
  import { getUsdcAttestation } from '../offchain.ts'
6
- import type { CCIPMessage, CCIPRequest, OffchainTokenData } from '../types.ts'
7
- import { networkInfo } from '../utils.ts'
6
+ import type { CCIPMessage, CCIPRequest, OffchainTokenData, WithLogger } from '../types.ts'
7
+ import { networkInfo, util } from '../utils.ts'
8
8
  import { IDL as BASE_TOKEN_POOL } from './idl/1.6.0/BASE_TOKEN_POOL.ts'
9
9
  import { IDL as CCTP_TOKEN_POOL } from './idl/1.6.0/CCIP_CCTP_TOKEN_POOL.ts'
10
- import { bytesToBuffer } from './utils.ts'
10
+ import type { SolanaLog, SolanaTransaction } from './index.ts'
11
+ import { bytesToBuffer, hexDiscriminator } from './utils.ts'
11
12
 
12
13
  interface CcipCctpMessageSentEvent {
13
14
  originalSender: PublicKey
@@ -28,33 +29,24 @@ interface CcipCctpMessageAndAttestation {
28
29
  const cctpTokenPoolCoder = new BorshCoder({
29
30
  ...CCTP_TOKEN_POOL,
30
31
  types: [...BASE_TOKEN_POOL.types, ...CCTP_TOKEN_POOL.types],
31
- events: BASE_TOKEN_POOL.events,
32
+ events: [...BASE_TOKEN_POOL.events, ...CCTP_TOKEN_POOL.events],
32
33
  errors: [...BASE_TOKEN_POOL.errors, ...CCTP_TOKEN_POOL.errors],
33
34
  })
34
35
 
35
36
  /**
36
37
  * Analyzes a Solana transaction to extract CcipCctpMessageSentEvent, fetch Circle attestation,
37
38
  * and encode the data in the format required by the destination chain.
38
- *
39
- * @param request - CCIP request containing transaction data and chain routing info
40
- * @returns Array of encoded offchain token data (only one supported for Solana right now)
41
- *
42
- * @throws Error if transaction hash is missing or CcipCctpMessageSentEvent parsing fails
43
- *
44
- * @example
45
- * const tokenData = await fetchSolanaOffchainTokenData({
46
- * lane: { sourceChainSelector: ..., destChainSelector: ... },
47
- * message: { ... },
48
- * log: { transactionHash: "3k81TLhJuhwB8fvurCwyMPHXR3k9Tmtqe2ZrUQ8e3rMxk9fWFJT2xVHGgKJg1785FkJcaiQkthY4m86JrESGPhMY" },
49
- * tx: { logs: [...] }
50
- * })
39
+ * @param request - CCIP request containing transaction data and chain routing info.
40
+ * @param logger - Logger instance for logging messages.
41
+ * @returns Array of encoded offchain token data (only one supported for Solana right now).
42
+ * @throws Error if transaction hash is missing or CcipCctpMessageSentEvent parsing fails.
51
43
  */
52
44
  export async function fetchSolanaOffchainTokenData(
53
- connection: Connection,
54
45
  request: Pick<CCIPRequest, 'tx' | 'lane'> & {
55
46
  message: CCIPMessage
56
- log: Pick<CCIPRequest['log'], 'topics' | 'index' | 'transactionHash'>
47
+ log: Pick<CCIPRequest['log'], 'topics' | 'index' | 'transactionHash' | 'address'>
57
48
  },
49
+ { logger = console }: WithLogger = {},
58
50
  ): Promise<OffchainTokenData[]> {
59
51
  if (request.message.tokenAmounts === undefined || request.message.tokenAmounts.length === 0) {
60
52
  return []
@@ -68,17 +60,29 @@ export async function fetchSolanaOffchainTokenData(
68
60
 
69
61
  const { isTestnet } = networkInfo(request.lane.sourceChainSelector)
70
62
  const txSignature = request.log.transactionHash
71
- if (!txSignature) {
72
- throw new Error('Transaction hash not found for OffchainTokenData parsing')
73
- }
74
63
 
75
64
  // Parse Solana transaction to find CCTP event
76
- const cctpEvents = await parseCcipCctpEvents(connection, txSignature)
65
+ const tx = request.tx as SolanaTransaction
66
+ const log = request.log as SolanaLog
67
+ const logMessages = tx.tx.meta!.logMessages!
68
+ // there may have multiple ccipSend calls in same tx;
69
+ // use `invoke [level]` to filter only logs inside this call
70
+ const requestInvokeIdx = logMessages.findLastIndex(
71
+ (l, i) => i < log.index && l === `Program ${request.log.address} invoke [${log.level}]`,
72
+ )
73
+ const cctpEvents = []
74
+ for (const l of tx.logs) {
75
+ if (requestInvokeIdx >= l.index || l.index >= log.index) continue
76
+ if (l.topics[0] !== hexDiscriminator('CcipCctpMessageSentEvent')) continue
77
+ const decoded = cctpTokenPoolCoder.events.decode(l.data)
78
+ if (!decoded) throw new Error(`Failed to decode CCTP event: ${util.inspect(l)}`)
79
+ cctpEvents.push(decoded.data as unknown as CcipCctpMessageSentEvent)
80
+ }
77
81
  const offchainTokenData: OffchainTokenData[] = request.message.tokenAmounts.map(() => undefined)
78
82
 
79
83
  // If no CcipCctpMessageSentEvent found, return defaults so we don't block execution
80
84
  if (cctpEvents.length === 0) {
81
- console.debug('No events')
85
+ logger.debug('No USDC/CCTP events found')
82
86
  return offchainTokenData
83
87
  }
84
88
 
@@ -100,89 +104,15 @@ export async function fetchSolanaOffchainTokenData(
100
104
 
101
105
  offchainTokenData[0] = { _tag: 'usdc', message, attestation }
102
106
  } catch (error) {
103
- console.warn(
104
- `❌ Solana CCTP: Failed to fetch attestation for ${txSignature}:`,
105
- message,
106
- error,
107
- )
107
+ logger.warn(`❌ Solana CCTP: Failed to fetch attestation for ${txSignature}:`, message, error)
108
108
  }
109
109
  }
110
110
 
111
- console.debug('Got Solana offchain token data', offchainTokenData)
111
+ logger.debug('Got Solana offchain token data', offchainTokenData)
112
112
 
113
113
  return offchainTokenData
114
114
  }
115
115
 
116
- /**
117
- * Parses CcipCctpMessageSentEvent from a Solana transaction by analyzing program logs
118
- *
119
- * @param txSignature - Solana transaction signature to analyze
120
- * @param sourceChainSelector - Source chain selector to determine RPC endpoint
121
- * @returns Array of parsed CcipCctpMessageSentEvent found in the transaction (only 1 supported though)
122
- *
123
- * @throws Error if transaction is not found or RPC fails
124
- *
125
- * @example
126
- * const events = await parseSolanaCctpEvents(
127
- * '3k81TLhJuhwB8fvurCwyMPHXR3k9Tmtqe2ZrUQ8e3rMxk9fWFJT2xVHGgKJg1785FkJcaiQkthY4m86JrESGPhMY',
128
- * 16423721717087811551n // Solana Devnet
129
- * )
130
- */
131
- async function parseCcipCctpEvents(
132
- connection: Connection,
133
- txSignature: string,
134
- ): Promise<CcipCctpMessageSentEvent[]> {
135
- // Fetch transaction details using Solana RPC
136
- const tx = await connection.getTransaction(txSignature, {
137
- commitment: 'finalized',
138
- maxSupportedTransactionVersion: 0,
139
- })
140
- if (!tx || !tx.meta) {
141
- throw new Error(`Transaction not found: ${txSignature}`)
142
- }
143
-
144
- if (!tx.meta.logMessages?.length) {
145
- throw new Error(`Transaction has no logs: ${txSignature}`)
146
- }
147
-
148
- const cctpPoolAddress = getCctpPoolAddress(tx.meta.logMessages)
149
- if (!cctpPoolAddress) {
150
- return []
151
- }
152
-
153
- const eventParser = new EventParser(new PublicKey(cctpPoolAddress), cctpTokenPoolCoder)
154
-
155
- const events: CcipCctpMessageSentEvent[] = Array.from(eventParser.parseLogs(tx.meta.logMessages))
156
- .filter((event) => event.name === 'CcipCctpMessageSentEvent')
157
- .map((event) => event.data as unknown as CcipCctpMessageSentEvent)
158
- return events
159
- }
160
-
161
- function getCctpPoolAddress(logs: string[]): string | null {
162
- // Example logs include lines like the following (though the indexes of the "invoke [1]" are unreliable):
163
- // "Program <POOL ADDRESS HERE, THIS IS WHAT WE'RE LOOKING FOR> invoke [1]",
164
- // "Program log: Instruction: LockOrBurnTokens",
165
- const candidateIx = logs.indexOf('Program log: Instruction: LockOrBurnTokens')
166
- if (candidateIx < 1) {
167
- return null
168
- }
169
-
170
- const candidateAddress = logs[candidateIx - 1].split(' ')[1]
171
-
172
- if (!candidateAddress.toLowerCase().startsWith('ccitp')) {
173
- // The vanity address of the pool includes "ccitp" (case-insensitive) as a prefix
174
- return null
175
- }
176
-
177
- // basic sanity check that we have the pool address: The pool returns a value, so the logs should show that
178
- const sanityCheck = logs.find((log) => log.startsWith(`Program return: ${candidateAddress} `))
179
- if (!sanityCheck) {
180
- return null
181
- }
182
-
183
- return candidateAddress
184
- }
185
-
186
116
  /**
187
117
  * Encodes CCTP message and attestation
188
118
  *
@@ -1,3 +1,5 @@
1
+ import { Buffer } from 'buffer'
2
+
1
3
  import { BorshInstructionCoder } from '@coral-xyz/anchor'
2
4
  import { BorshTypesCoder } from '@coral-xyz/anchor/dist/cjs/coder/borsh/types.js'
3
5
  import { sha256, toUtf8Bytes } from 'ethers'
@@ -7,61 +9,74 @@ import { camelToSnakeCase } from './utils.ts'
7
9
 
8
10
  type Layout_<T = unknown> = { encode: (type: T, buffer: Buffer) => number }
9
11
 
10
- // monkey patch some functions to ensure correct buffer allocation (usually, hardcoded 1000B)
11
- Object.assign(BorshTypesCoder.prototype, {
12
- encode: function <T>(this: BorshTypesCoder, name: string, type: T): Buffer {
13
- const layout = (this as unknown as { typeLayouts: Map<string, Layout_> }).typeLayouts.get(name)
14
- if (!layout) {
15
- throw new Error(`Unknown type: ${name}`)
16
- }
17
- let buffer = Buffer.alloc(512)
18
- let len
19
- try {
20
- len = layout.encode(type, buffer)
21
- } catch (err) {
22
- if (err instanceof RangeError) {
23
- buffer = Buffer.alloc(32000)
24
- len = layout.encode(type, buffer)
25
- } else {
26
- throw err
27
- }
28
- }
29
-
30
- return buffer.subarray(0, len)
31
- },
32
- })
33
-
34
12
  function sighash(nameSpace: string, ixName: string): Buffer {
35
13
  const name = camelToSnakeCase(ixName)
36
14
  const preimage = `${nameSpace}:${name}`
37
15
  return Buffer.from(sha256(toUtf8Bytes(preimage)).slice(2, 18), 'hex')
38
16
  }
39
17
 
40
- Object.assign(BorshInstructionCoder.prototype, {
41
- _encode: function (
42
- this: BorshInstructionCoder,
43
- nameSpace: string,
44
- ixName: string,
45
- ix: any, // eslint-disable-line @typescript-eslint/no-explicit-any
46
- ): Buffer {
47
- const methodName = snakeToCamel(ixName)
48
- const layout = (this as unknown as { ixLayout: Map<string, Layout_> }).ixLayout.get(methodName)
49
- if (!layout) {
50
- throw new Error(`Unknown method: ${methodName}`)
51
- }
52
- let buffer = Buffer.alloc(512)
53
- let len
54
- try {
55
- len = layout.encode(ix, buffer)
56
- } catch (err) {
57
- if (err instanceof RangeError) {
58
- buffer = Buffer.alloc(32000)
18
+ let patched = false
19
+ /**
20
+ * Patches BorshTypesCoder to ensure correct buffer allocation for large messages.
21
+ * Should be called before encoding Solana CCIP messages.
22
+ */
23
+ export function patchBorsh() {
24
+ if (patched) return
25
+ patched = true
26
+ // monkey patch some functions to ensure correct buffer allocation (usually, hardcoded 1000B)
27
+ Object.assign(BorshTypesCoder.prototype, {
28
+ encode: function <T>(this: BorshTypesCoder, name: string, type: T): Buffer {
29
+ const layout = (this as unknown as { typeLayouts: Map<string, Layout_> }).typeLayouts.get(
30
+ name,
31
+ )
32
+ if (!layout) {
33
+ throw new Error(`Unknown type: ${name}`)
34
+ }
35
+ let buffer = Buffer.alloc(512)
36
+ let len
37
+ try {
38
+ len = layout.encode(type, buffer)
39
+ } catch (err) {
40
+ if (err instanceof RangeError) {
41
+ buffer = Buffer.alloc(32000)
42
+ len = layout.encode(type, buffer)
43
+ } else {
44
+ throw err
45
+ }
46
+ }
47
+
48
+ return buffer.subarray(0, len)
49
+ },
50
+ })
51
+
52
+ Object.assign(BorshInstructionCoder.prototype, {
53
+ _encode: function (
54
+ this: BorshInstructionCoder,
55
+ nameSpace: string,
56
+ ixName: string,
57
+ ix: any, // eslint-disable-line @typescript-eslint/no-explicit-any
58
+ ): Buffer {
59
+ const methodName = snakeToCamel(ixName)
60
+ const layout = (this as unknown as { ixLayout: Map<string, Layout_> }).ixLayout.get(
61
+ methodName,
62
+ )
63
+ if (!layout) {
64
+ throw new Error(`Unknown method: ${methodName}`)
65
+ }
66
+ let buffer = Buffer.alloc(512)
67
+ let len
68
+ try {
59
69
  len = layout.encode(ix, buffer)
60
- } else {
61
- throw err
70
+ } catch (err) {
71
+ if (err instanceof RangeError) {
72
+ buffer = Buffer.alloc(32000)
73
+ len = layout.encode(ix, buffer)
74
+ } else {
75
+ throw err
76
+ }
62
77
  }
63
- }
64
- const data = buffer.subarray(0, len)
65
- return Buffer.concat([sighash(nameSpace, ixName), data])
66
- },
67
- })
78
+ const data = buffer.subarray(0, len)
79
+ return Buffer.concat([sighash(nameSpace, ixName), data])
80
+ },
81
+ })
82
+ }
@@ -1,6 +1,6 @@
1
- import util from 'util'
1
+ import { Buffer } from 'buffer'
2
2
 
3
- import { type AnchorProvider, type IdlTypes, Program } from '@coral-xyz/anchor'
3
+ import { type IdlTypes, Program } from '@coral-xyz/anchor'
4
4
  import {
5
5
  NATIVE_MINT,
6
6
  createApproveInstruction,
@@ -12,19 +12,17 @@ import {
12
12
  type AddressLookupTableAccount,
13
13
  type Connection,
14
14
  type TransactionInstruction,
15
- ComputeBudgetProgram,
16
15
  PublicKey,
17
- TransactionMessage,
18
- VersionedTransaction,
19
16
  } from '@solana/web3.js'
20
17
  import BN from 'bn.js'
21
18
  import { zeroPadValue } from 'ethers'
22
19
 
23
20
  import { SolanaChain } from './index.ts'
24
- import type { AnyMessage } from '../types.ts'
25
- import { toLeArray } from '../utils.ts'
21
+ import { type AnyMessage, type WithLogger, ChainFamily } from '../types.ts'
22
+ import { toLeArray, util } from '../utils.ts'
26
23
  import { IDL as CCIP_ROUTER_IDL } from './idl/1.6.0/CCIP_ROUTER.ts'
27
- import { bytesToBuffer, simulateTransaction, simulationProvider } from './utils.ts'
24
+ import type { UnsignedSolanaTx } from './types.ts'
25
+ import { bytesToBuffer, simulationProvider } from './utils.ts'
28
26
 
29
27
  function anyToSvmMessage(message: AnyMessage): IdlTypes<typeof CCIP_ROUTER_IDL>['SVM2AnyMessage'] {
30
28
  const feeTokenPubkey = message.feeToken ? new PublicKey(message.feeToken) : PublicKey.default
@@ -48,17 +46,22 @@ function anyToSvmMessage(message: AnyMessage): IdlTypes<typeof CCIP_ROUTER_IDL>[
48
46
  return svmMessage
49
47
  }
50
48
 
49
+ /**
50
+ * Gets the fee for sending a CCIP message on Solana.
51
+ * @param ctx - Context object containing the Solana connection and logger.
52
+ * @param router - Router program address.
53
+ * @param destChainSelector - Destination chain selector.
54
+ * @param message - CCIP message to send.
55
+ * @returns Fee amount in native tokens.
56
+ */
51
57
  export async function getFee(
52
- connection: Connection,
58
+ ctx: { connection: Connection } & WithLogger,
53
59
  router: string,
54
60
  destChainSelector: bigint,
55
61
  message: AnyMessage,
56
62
  ): Promise<bigint> {
57
- const program = new Program(
58
- CCIP_ROUTER_IDL,
59
- new PublicKey(router),
60
- simulationProvider(connection),
61
- )
63
+ const { connection, logger = console } = ctx
64
+ const program = new Program(CCIP_ROUTER_IDL, new PublicKey(router), simulationProvider(ctx))
62
65
 
63
66
  // Get router config to find feeQuoter
64
67
  const [configPda] = PublicKey.findProgramAddressSync([Buffer.from('config')], program.programId)
@@ -86,7 +89,7 @@ export async function getFee(
86
89
  message.feeToken !== PublicKey.default.toBase58() &&
87
90
  message.feeToken !== linkTokenMint.toBase58()
88
91
  ) {
89
- console.warn('feeToken is not default nor link =', linkTokenMint.toBase58())
92
+ logger.warn('feeToken is not default nor link =', linkTokenMint.toBase58())
90
93
  }
91
94
 
92
95
  // Convert feeToken to PublicKey (default to native SOL if not specified)
@@ -129,7 +132,7 @@ export async function getFee(
129
132
  .map((pubkey) => ({ pubkey, isWritable: false, isSigner: false }))
130
133
 
131
134
  // Call getFee method
132
- const result: unknown = await program.methods
135
+ const result = (await program.methods
133
136
  .getFee(new BN(destChainSelector), svmMessage)
134
137
  .accounts({
135
138
  config: configPda,
@@ -141,13 +144,13 @@ export async function getFee(
141
144
  feeQuoterLinkTokenConfig: feeQuoterLinkTokenConfigPda,
142
145
  })
143
146
  .remainingAccounts(remainingAccounts)
144
- .view()
147
+ .view()) as IdlTypes<typeof CCIP_ROUTER_IDL>['GetFeeResult']
145
148
 
146
- if (!(result as { amount?: BN })?.amount) {
149
+ if (!result?.amount) {
147
150
  throw new Error(`Invalid fee result from router: ${util.inspect(result)}`)
148
151
  }
149
152
 
150
- return BigInt((result as { amount: BN }).amount.toString())
153
+ return BigInt(result.amount.toString())
151
154
  }
152
155
 
153
156
  async function deriveAccountsCcipSend({
@@ -170,16 +173,9 @@ async function deriveAccountsCcipSend({
170
173
  let tokenIndex = 0
171
174
 
172
175
  const [configPDA] = PublicKey.findProgramAddressSync([Buffer.from('config')], router.programId)
173
-
174
- // read-only copy of router which avoids signing every simulation
175
- const roProgram = new Program(
176
- router.idl,
177
- router.programId,
178
- simulationProvider(connection, sender),
179
- )
180
176
  do {
181
177
  // Create the transaction instruction for the deriveAccountsCcipSend method
182
- const response = (await roProgram.methods
178
+ const response = (await router.methods
183
179
  .deriveAccountsCcipSend(
184
180
  {
185
181
  destChainSelector: new BN(destChainSelector.toString()),
@@ -248,63 +244,24 @@ async function deriveAccountsCcipSend({
248
244
  }
249
245
  }
250
246
 
251
- export async function simulateAndSendTxs(
252
- connection: Connection,
253
- feePayer: AnchorProvider['wallet'],
254
- instructions: TransactionInstruction[],
255
- addressLookupTableAccounts?: AddressLookupTableAccount[],
256
- ) {
257
- let computeUnitLimit
258
- const simulated =
259
- (
260
- await simulateTransaction({
261
- connection,
262
- payerKey: feePayer.publicKey,
263
- instructions,
264
- addressLookupTableAccounts,
265
- })
266
- ).unitsConsumed || 0
267
- if (simulated > 200000) computeUnitLimit = Math.ceil(simulated * 1.1)
268
-
269
- const txMsg = new TransactionMessage({
270
- payerKey: feePayer.publicKey,
271
- recentBlockhash: (await connection.getLatestBlockhash('confirmed')).blockhash,
272
- instructions: [
273
- ...(computeUnitLimit
274
- ? [ComputeBudgetProgram.setComputeUnitLimit({ units: computeUnitLimit })]
275
- : []),
276
- ...instructions,
277
- ],
278
- })
279
- const messageV0 = txMsg.compileToV0Message(addressLookupTableAccounts)
280
- const tx = new VersionedTransaction(messageV0)
281
-
282
- const signed = await feePayer.signTransaction(tx)
283
- let hash
284
- for (let attempt = 0; ; attempt++) {
285
- try {
286
- hash = await connection.sendTransaction(signed)
287
- await connection.confirmTransaction(hash, 'confirmed')
288
- return hash
289
- } catch (error) {
290
- if (attempt >= 3) throw error
291
- console.error(`sendTransaction failed attempt=${attempt + 1}/3:`, error)
292
- }
293
- }
294
- }
295
-
296
- export async function ccipSend(
297
- router: Program<typeof CCIP_ROUTER_IDL>,
247
+ /**
248
+ * Generates unsigned instructions for sending a message with CCIP on Solana
249
+ * @param ctx - Context containing connection and logger.
250
+ * @param sender - Wallet to pay transaction fees.
251
+ * @param router - Router program instance.
252
+ * @param destChainSelector - Destination chain selector.
253
+ * @param message - CCIP message with fee.
254
+ * @param opts - Optional parameters for approval.
255
+ * @returns Solana unsigned txs (instructions and lookup tables)
256
+ */
257
+ export async function generateUnsignedCcipSend(
258
+ ctx: { connection: Connection } & WithLogger,
259
+ sender: PublicKey,
260
+ router: PublicKey,
298
261
  destChainSelector: bigint,
299
262
  message: AnyMessage & { fee: bigint },
300
263
  opts?: { approveMax?: boolean },
301
- ) {
302
- const connection = router.provider.connection
303
- let wallet
304
- if (!(wallet = (router.provider as AnchorProvider).wallet)) {
305
- throw new Error('ccipSend called without signer wallet')
306
- }
307
-
264
+ ): Promise<UnsignedSolanaTx> {
308
265
  const amountsToApprove = (message.tokenAmounts ?? []).reduce(
309
266
  (acc, { token, amount }) => ({ ...acc, [token]: (acc[token] ?? 0n) + amount }),
310
267
  {} as Record<string, bigint>,
@@ -312,14 +269,15 @@ export async function ccipSend(
312
269
  if (message.feeToken && message.feeToken !== PublicKey.default.toBase58()) {
313
270
  amountsToApprove[message.feeToken] = (amountsToApprove[message.feeToken] ?? 0n) + message.fee
314
271
  }
272
+ const program = new Program(CCIP_ROUTER_IDL, router, simulationProvider(ctx, sender))
315
273
 
316
274
  const approveIxs = []
317
275
  for (const [token, amount] of Object.entries(amountsToApprove)) {
318
276
  const approveIx = await approveRouterSpender(
319
- connection,
320
- wallet.publicKey,
277
+ ctx,
278
+ sender,
321
279
  new PublicKey(token),
322
- router.programId,
280
+ router,
323
281
  opts?.approveMax ? undefined : amount,
324
282
  )
325
283
  if (approveIx) approveIxs.push(approveIx)
@@ -327,13 +285,13 @@ export async function ccipSend(
327
285
 
328
286
  const svmMessage = anyToSvmMessage(message)
329
287
  const { addressLookupTableAccounts, accounts, tokenIndexes } = await deriveAccountsCcipSend({
330
- router,
288
+ router: program,
331
289
  destChainSelector,
332
- sender: wallet.publicKey,
290
+ sender,
333
291
  message: svmMessage,
334
292
  })
335
293
 
336
- const sendIx = await router.methods
294
+ const sendIx = await program.methods
337
295
  .ccipSend(new BN(destChainSelector), svmMessage, tokenIndexes)
338
296
  .accountsStrict({
339
297
  config: accounts[0].pubkey,
@@ -357,33 +315,16 @@ export async function ccipSend(
357
315
  })
358
316
  .remainingAccounts(accounts.slice(18))
359
317
  .instruction()
360
-
361
- let hash
362
- try {
363
- // first try to serialize and send a single tx containing approve and send ixs
364
- hash = await simulateAndSendTxs(
365
- connection,
366
- wallet,
367
- [...approveIxs, sendIx],
368
- addressLookupTableAccounts,
369
- )
370
- } catch (err) {
371
- if (
372
- !approveIxs.length ||
373
- !(err instanceof Error) ||
374
- !['encoding overruns Uint8Array', 'too large'].some((e) => err.message.includes(e))
375
- )
376
- throw err
377
- // if serialization fails, send approve txs separately
378
- for (const approveIx of approveIxs) await simulateAndSendTxs(connection, wallet, [approveIx])
379
- hash = await simulateAndSendTxs(connection, wallet, [sendIx], addressLookupTableAccounts)
318
+ return {
319
+ family: ChainFamily.Solana,
320
+ mainIndex: approveIxs.length,
321
+ instructions: [...approveIxs, sendIx],
322
+ lookupTables: addressLookupTableAccounts,
380
323
  }
381
-
382
- return { hash }
383
324
  }
384
325
 
385
326
  async function approveRouterSpender(
386
- connection: Connection,
327
+ { connection, logger = console }: { connection: Connection } & WithLogger,
387
328
  owner: PublicKey,
388
329
  token: PublicKey,
389
330
  router: PublicKey,
@@ -424,7 +365,7 @@ async function approveRouterSpender(
424
365
  undefined,
425
366
  mintInfo.owner,
426
367
  )
427
- console.info(
368
+ logger.info(
428
369
  'Approving',
429
370
  amount ?? BigInt(Number.MAX_SAFE_INTEGER),
430
371
  'of',
@@ -1,6 +1,47 @@
1
+ import type {
2
+ AddressLookupTableAccount,
3
+ PublicKey,
4
+ Transaction,
5
+ TransactionInstruction,
6
+ VersionedTransaction,
7
+ } from '@solana/web3.js'
8
+
1
9
  import type { SVMExtraArgsV1 } from '../extra-args.ts'
2
- import type { CCIPMessage_V1_6 } from '../types.ts'
10
+ import type { CCIPMessage_V1_6, ChainFamily } from '../types.ts'
3
11
 
4
- // SourceTokenData adds `destGasAmount` (decoded from source's `destExecData`);
5
- // not sure why they kept the "gas" name in Solana, but let's just be keep consistent
12
+ /** Solana-specific CCIP v1.6 message type with SVM extra args. */
6
13
  export type CCIPMessage_V1_6_Solana = CCIPMessage_V1_6 & SVMExtraArgsV1
14
+
15
+ /**
16
+ * Contains unsigned data for a Solana transaction.
17
+ * instructions - array of instructions; may or may not fit in a single transaction
18
+ * mainIndex - index of the main instruction in the array
19
+ * lookupTables - array of lookupTables to be used in *main* transaction
20
+ */
21
+ export type UnsignedSolanaTx = {
22
+ family: typeof ChainFamily.Solana
23
+ instructions: TransactionInstruction[]
24
+ mainIndex?: number
25
+ lookupTables?: AddressLookupTableAccount[]
26
+ }
27
+
28
+ /** Minimal Solana wallet interface (anchor.Wallet=) */
29
+ export type Wallet = {
30
+ readonly publicKey: PublicKey
31
+ signTransaction<T extends Transaction | VersionedTransaction>(tx: T): Promise<T>
32
+ }
33
+
34
+ /** Typeguard for Solana Wallet */
35
+ export function isWallet(wallet: unknown): wallet is Wallet {
36
+ return (
37
+ typeof wallet === 'object' &&
38
+ wallet !== null &&
39
+ 'publicKey' in wallet &&
40
+ 'signTransaction' in wallet &&
41
+ typeof wallet.publicKey === 'object' &&
42
+ wallet.publicKey !== null &&
43
+ 'toBase58' in wallet.publicKey &&
44
+ typeof wallet.publicKey.toBase58 === 'function' &&
45
+ typeof wallet.signTransaction === 'function'
46
+ )
47
+ }