@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,18 +1,18 @@
1
- import util from 'util';
2
- import { AnchorProvider, BorshAccountsCoder, BorshCoder, Program, Wallet, eventDiscriminator, } from '@coral-xyz/anchor';
1
+ import { Buffer } from 'buffer';
2
+ import { BorshAccountsCoder, BorshCoder, Program } from '@coral-xyz/anchor';
3
3
  import { NATIVE_MINT } from '@solana/spl-token';
4
- import { Connection, Keypair, PublicKey, SYSVAR_CLOCK_PUBKEY, SystemProgram, } from '@solana/web3.js';
4
+ import { Connection, PublicKey, SYSVAR_CLOCK_PUBKEY, SystemProgram, } from '@solana/web3.js';
5
5
  import bs58 from 'bs58';
6
- import { concat, dataLength, dataSlice, encodeBase58, encodeBase64, getBytes, hexlify, isHexString, toBigInt, } from 'ethers';
7
- import moize, {} from 'moize';
8
- import { Chain, ChainFamily, } from "../chain.js";
6
+ import { concat, dataLength, dataSlice, encodeBase58, encodeBase64, hexlify, isHexString, toBigInt, } from 'ethers';
7
+ import { memoize } from 'micro-memoize';
8
+ import { Chain, } from "../chain.js";
9
9
  import { EVMExtraArgsV2Tag } from "../extra-args.js";
10
10
  import SELECTORS from "../selectors.js";
11
11
  import { supportedChains } from "../supported-chains.js";
12
- import { CCIPVersion, ExecutionState, } from "../types.js";
13
- import { createRateLimitedFetch, decodeAddress, decodeOnRampAddress, getDataBytes, leToBigInt, networkInfo, parseTypeAndVersion, toLeArray, } from "../utils.js";
12
+ import { CCIPVersion, ChainFamily, ExecutionState, } from "../types.js";
13
+ import { createRateLimitedFetch, decodeAddress, decodeOnRampAddress, getDataBytes, leToBigInt, networkInfo, parseTypeAndVersion, toLeArray, util, } from "../utils.js";
14
14
  import { cleanUpBuffers } from "./cleanup.js";
15
- import { executeReport } from "./exec.js";
15
+ import { generateUnsignedExecuteReport } from "./exec.js";
16
16
  import { getV16SolanaLeafHasher } from "./hasher.js";
17
17
  import { IDL as BASE_TOKEN_POOL } from "./idl/1.6.0/BASE_TOKEN_POOL.js";
18
18
  import { IDL as BURN_MINT_TOKEN_POOL } from "./idl/1.6.0/BURN_MINT_TOKEN_POOL.js";
@@ -20,8 +20,11 @@ import { IDL as CCIP_CCTP_TOKEN_POOL } from "./idl/1.6.0/CCIP_CCTP_TOKEN_POOL.js
20
20
  import { IDL as CCIP_OFFRAMP_IDL } from "./idl/1.6.0/CCIP_OFFRAMP.js";
21
21
  import { IDL as CCIP_ROUTER_IDL } from "./idl/1.6.0/CCIP_ROUTER.js";
22
22
  import { fetchSolanaOffchainTokenData } from "./offchain.js";
23
- import { ccipSend, getFee } from "./send.js";
24
- import { bytesToBuffer, getErrorFromLogs, parseSolanaLogs, simulationProvider } from "./utils.js";
23
+ import { generateUnsignedCcipSend, getFee } from "./send.js";
24
+ import { isWallet } from "./types.js";
25
+ import { bytesToBuffer, getErrorFromLogs, hexDiscriminator, parseSolanaLogs, simulateAndSendTxs, simulationProvider, } from "./utils.js";
26
+ import { fetchAllMessagesInBatch, fetchCCIPRequestById, fetchCCIPRequestsInTx, } from "../requests.js";
27
+ import { patchBorsh } from "./patchBorsh.js";
25
28
  const routerCoder = new BorshCoder(CCIP_ROUTER_IDL);
26
29
  const offrampCoder = new BorshCoder(CCIP_OFFRAMP_IDL);
27
30
  const tokenPoolCoder = new BorshCoder({
@@ -40,114 +43,106 @@ const cctpTokenPoolCoder = new BorshCoder({
40
43
  const unknownTokens = {
41
44
  '4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU': 'USDC', // devnet
42
45
  };
43
- function hexDiscriminator(eventName) {
44
- return hexlify(eventDiscriminator(eventName));
45
- }
46
+ /**
47
+ * Solana chain implementation supporting Solana networks.
48
+ */
46
49
  export class SolanaChain extends Chain {
50
+ static {
51
+ patchBorsh();
52
+ supportedChains[ChainFamily.Solana] = SolanaChain;
53
+ }
47
54
  static family = ChainFamily.Solana;
48
55
  static decimals = 9;
49
- network;
50
56
  connection;
51
57
  commitment = 'confirmed';
52
- _getSignaturesForAddress;
53
- constructor(connection, network) {
54
- super();
55
- if (network.family !== ChainFamily.Solana) {
56
- throw new Error(`Invalid network family for SolanaChain: ${network.family}`);
57
- }
58
- this.network = network;
58
+ /**
59
+ * Creates a new SolanaChain instance.
60
+ * @param connection - Solana connection instance.
61
+ * @param network - Network information for this chain.
62
+ */
63
+ constructor(connection, network, ctx) {
64
+ super(network, ctx);
59
65
  this.connection = connection;
60
66
  // Memoize expensive operations
61
- this.typeAndVersion = moize.default(this.typeAndVersion.bind(this), {
67
+ this.typeAndVersion = memoize(this.typeAndVersion.bind(this), {
62
68
  maxArgs: 1,
63
- isPromise: true,
69
+ async: true,
64
70
  });
65
- this.getBlockTimestamp = moize.default(this.getBlockTimestamp.bind(this), {
66
- isPromise: true,
71
+ this.getBlockTimestamp = memoize(this.getBlockTimestamp.bind(this), {
72
+ async: true,
67
73
  maxSize: 100,
68
- updateCacheForKey: (key) => typeof key[key.length - 1] !== 'number',
74
+ forceUpdate: (key) => typeof key[key.length - 1] !== 'number',
69
75
  });
70
- this.getTransaction = moize.default(this.getTransaction.bind(this), {
76
+ this.getTransaction = memoize(this.getTransaction.bind(this), {
71
77
  maxSize: 100,
72
78
  maxArgs: 1,
73
79
  });
74
- this.getWallet = moize.default(this.getWallet.bind(this), { maxSize: 1, maxArgs: 0 });
75
- this.getTokenForTokenPool = moize.default(this.getTokenForTokenPool.bind(this));
76
- this.getTokenInfo = moize.default(this.getTokenInfo.bind(this));
77
- this._getSignaturesForAddress = moize.default((programId, before) => this.connection.getSignaturesForAddress(new PublicKey(programId), { limit: 1000, before }, 'confirmed'), {
80
+ this.getTokenForTokenPool = memoize(this.getTokenForTokenPool.bind(this));
81
+ this.getTokenInfo = memoize(this.getTokenInfo.bind(this));
82
+ this.connection.getSignaturesForAddress = memoize(this.connection.getSignaturesForAddress.bind(this.connection), {
78
83
  maxSize: 100,
79
- maxAge: 60000,
80
- maxArgs: 2,
81
- isPromise: true,
82
- updateExpire: true,
83
- // only expire undefined before (i.e. recent getSignaturesForAddress calls)
84
- onExpire: ([, before]) => !before,
84
+ async: true,
85
+ // if options.before is defined, caches for long, otherwise for short (recent signatures)
86
+ expires: (key) => (key[1] ? 2 ** 31 - 1 : 5e3),
87
+ transformKey: ([address, options, commitment]) => [
88
+ address.toBase58(),
89
+ options?.before,
90
+ options?.until,
91
+ options?.limit,
92
+ commitment,
93
+ ],
85
94
  });
86
95
  // cache account info for 30 seconds
87
- this.connection.getAccountInfo = moize.default(this.connection.getAccountInfo.bind(this.connection), {
96
+ this.connection.getAccountInfo = memoize(this.connection.getAccountInfo.bind(this.connection), {
88
97
  maxSize: 100,
89
98
  maxArgs: 2,
90
- maxAge: 30e3,
91
- transformArgs: ([address, commitment]) => [address.toString(), commitment],
92
- });
93
- this._getRouterConfig = moize.default(this._getRouterConfig.bind(this), {
94
- maxArgs: 1,
95
- });
96
- this.listFeeTokens = moize.default(this.listFeeTokens.bind(this), {
97
- maxArgs: 1,
99
+ expires: 30e3,
100
+ transformKey: ([address, commitment]) => [address.toString(), commitment],
98
101
  });
102
+ this._getRouterConfig = memoize(this._getRouterConfig.bind(this), { maxArgs: 1 });
103
+ this.getFeeTokens = memoize(this.getFeeTokens.bind(this), { maxArgs: 1 });
104
+ this.getOffRampsForRouter = memoize(this.getOffRampsForRouter.bind(this), { maxArgs: 1 });
99
105
  }
100
- static _getConnection(url) {
106
+ /**
107
+ * Creates a Solana connection from a URL.
108
+ * @param url - RPC endpoint URL (https://, http://, wss://, or ws://).
109
+ * @param ctx - context containing logger.
110
+ * @returns Solana Connection instance.
111
+ */
112
+ static _getConnection(url, ctx) {
113
+ const { logger = console } = ctx ?? {};
101
114
  if (!url.startsWith('http') && !url.startsWith('ws')) {
102
115
  throw new Error(`Invalid Solana RPC URL format (should be https://, http://, wss://, or ws://): ${url}`);
103
116
  }
104
117
  const config = { commitment: 'confirmed' };
105
118
  if (url.includes('.solana.com')) {
106
- config.fetch = createRateLimitedFetch({
107
- maxRequests: 10,
108
- maxRetries: 3,
109
- windowMs: 11e3,
110
- }); // public nodes
111
- console.warn('Using rate-limited fetch for public solana nodes, commands may be slow');
119
+ config.fetch = createRateLimitedFetch(undefined, ctx); // public nodes
120
+ logger.warn('Using rate-limited fetch for public solana nodes, commands may be slow');
112
121
  }
113
122
  return new Connection(url, config);
114
123
  }
115
- static async fromConnection(connection) {
124
+ /**
125
+ * Creates a SolanaChain instance from an existing connection.
126
+ * @param connection - Solana Connection instance.
127
+ * @param ctx - context containing logger.
128
+ * @returns A new SolanaChain instance.
129
+ */
130
+ static async fromConnection(connection, ctx) {
116
131
  // Get genesis hash to use as chainId
117
- return new SolanaChain(connection, networkInfo(await connection.getGenesisHash()));
118
- }
119
- static async fromUrl(url) {
120
- const connection = this._getConnection(url);
121
- return this.fromConnection(connection);
122
- }
123
- async destroy() {
124
- // Solana Connection doesn't have an explicit destroy method
125
- // The memoized functions will be garbage collected when the instance is destroyed
126
- }
127
- static getWallet(_opts) {
128
- throw new Error('Wallet not implemented');
132
+ return new SolanaChain(connection, networkInfo(await connection.getGenesisHash()), ctx);
129
133
  }
130
134
  /**
131
- * Load wallet
132
- * @param opts - options to load wallet
133
- * @param opts.wallet - private key as 0x or base58 string, or async getter function resolving to
134
- * Wallet instance
135
- * @returns Wallet, after caching in instance
135
+ * Creates a SolanaChain instance from an RPC URL.
136
+ * @param url - RPC endpoint URL.
137
+ * @param ctx - context containing logger.
138
+ * @returns A new SolanaChain instance.
136
139
  */
137
- async getWallet(opts = {}) {
138
- try {
139
- if (typeof opts.wallet === 'string')
140
- return new Wallet(Keypair.fromSecretKey(opts.wallet.startsWith('0x') ? getBytes(opts.wallet) : bs58.decode(opts.wallet)));
141
- }
142
- catch (_) {
143
- // pass
144
- }
145
- return this.constructor.getWallet(opts);
146
- }
147
- async getWalletAddress(opts) {
148
- return (await this.getWallet(opts)).publicKey.toBase58();
140
+ static async fromUrl(url, ctx) {
141
+ const connection = this._getConnection(url, ctx);
142
+ return this.fromConnection(connection, ctx);
149
143
  }
150
144
  // cached
145
+ /** {@inheritDoc Chain.getBlockTimestamp} */
151
146
  async getBlockTimestamp(block) {
152
147
  if (block === 'finalized') {
153
148
  const slot = await this.connection.getSlot('finalized');
@@ -163,7 +158,7 @@ export class SolanaChain extends Chain {
163
158
  }
164
159
  return blockTime;
165
160
  }
166
- // cached
161
+ /** {@inheritDoc Chain.getTransaction} */
167
162
  async getTransaction(hash) {
168
163
  const tx = await this.connection.getTransaction(hash, {
169
164
  commitment: 'confirmed',
@@ -173,7 +168,7 @@ export class SolanaChain extends Chain {
173
168
  throw new Error(`Transaction not found: ${hash}`);
174
169
  if (tx.blockTime) {
175
170
  ;
176
- this.getBlockTimestamp.set([tx.slot], Promise.resolve(tx.blockTime));
171
+ this.getBlockTimestamp.cache.set([tx.slot], Promise.resolve(tx.blockTime));
177
172
  }
178
173
  else {
179
174
  tx.blockTime = await this.getBlockTimestamp(tx.slot);
@@ -187,7 +182,6 @@ export class SolanaChain extends Chain {
187
182
  }))
188
183
  : [];
189
184
  const chainTx = {
190
- chain: this,
191
185
  hash,
192
186
  logs: [],
193
187
  blockNumber: tx.slot,
@@ -200,48 +194,54 @@ export class SolanaChain extends Chain {
200
194
  chainTx.logs = logs_.map((l) => Object.assign(l, { tx: chainTx }));
201
195
  return chainTx;
202
196
  }
203
- // implements inner paging logic for this.getLogs
204
- async *_getTransactionsForAddress(opts) {
197
+ /**
198
+ * Internal method to get transactions for an address with pagination.
199
+ * @param opts - Log filter options.
200
+ * @returns Async generator of Solana transactions.
201
+ */
202
+ async *getTransactionsForAddress(opts) {
205
203
  if (!opts.address)
206
204
  throw new Error('Program address is required for Solana log filtering');
207
205
  let allSignatures;
206
+ const limit = Math.min(opts?.page || 1000, 1000);
208
207
  if (opts.startBlock || opts.startTime) {
209
208
  // forward collect all matching sigs in array
210
- const allSigs = [];
211
- let batch, popped = false;
212
- while (!popped && (batch?.length ?? true)) {
213
- batch = await this._getSignaturesForAddress(opts.address, allSigs[allSigs.length - 1]?.signature);
209
+ allSignatures = [];
210
+ let batch;
211
+ do {
212
+ batch = await this.connection.getSignaturesForAddress(new PublicKey(opts.address), { limit, before: allSignatures[allSignatures.length - 1]?.signature }, 'confirmed');
214
213
  while (batch.length > 0 &&
215
214
  (batch[batch.length - 1].slot < (opts.startBlock || 0) ||
216
215
  (batch[batch.length - 1].blockTime || -1) < (opts.startTime || 0))) {
217
- batch.pop(); // pop tail of txs which are older than requested start
218
- popped = true;
216
+ batch.length--; // truncate tail of txs which are older than requested start
219
217
  }
220
- allSigs.push(...batch);
221
- }
222
- allSigs.reverse();
218
+ allSignatures.push(...batch); // concat in descending order
219
+ } while (batch.length >= limit);
220
+ allSignatures.reverse();
223
221
  while (opts.endBlock &&
224
- allSigs.length > 0 &&
225
- allSigs[allSigs.length - 1].slot > opts.endBlock) {
226
- allSigs.pop(); // pop head (after reverse) of txs which are newer than requested end
222
+ allSignatures.length > 0 &&
223
+ allSignatures[allSignatures.length - 1].slot > opts.endBlock) {
224
+ allSignatures.length--; // truncate head (after reverse) of txs newer than requested end
227
225
  }
228
- allSignatures = allSigs;
229
226
  }
230
227
  else {
231
228
  allSignatures = async function* () {
232
229
  let batch;
233
- while (batch?.length ?? true) {
234
- batch = await this._getSignaturesForAddress(opts.address, batch?.length
235
- ? batch[batch.length - 1].signature
236
- : opts.endBefore
237
- ? opts.endBefore
238
- : undefined);
230
+ do {
231
+ batch = await this.connection.getSignaturesForAddress(new PublicKey(opts.address), {
232
+ limit,
233
+ before: batch?.length
234
+ ? batch[batch.length - 1].signature
235
+ : opts.endBefore
236
+ ? opts.endBefore
237
+ : undefined,
238
+ }, 'confirmed');
239
239
  for (const sig of batch) {
240
240
  if (opts.endBlock && sig.slot > opts.endBlock)
241
241
  continue;
242
242
  yield sig;
243
243
  }
244
- }
244
+ } while (batch.length >= limit);
245
245
  }.call(this); // generate backwards until depleting getSignaturesForAddress
246
246
  }
247
247
  // Process signatures
@@ -263,17 +263,16 @@ export class SolanaChain extends Chain {
263
263
  * * Fetches signatures in reverse chronological order (newest first)
264
264
  * * Returns logs in reverse chronological order (newest first)
265
265
  *
266
- * @param opts - Log filter options
267
- * @param opts.startBlock - Starting slot number (inclusive)
268
- * @param opts.startTime - Starting Unix timestamp (inclusive)
269
- * @param opts.endBlock - Ending slot number (inclusive)
270
- * @param opts.address - Program address to filter logs by (required for Solana)
271
- * @param opts.topics - Array of topics to filter logs by (optional);
272
- * either 0x-8B discriminants or event names
273
- * @param.opts.programs - a special option to allow querying by address of interest, but
274
- * yielding matching logs from specific (string address) program or any (true)
275
- * @param opts.commit - Special param for fetching ExecutionReceipts, to narrow down the search
276
- * @returns AsyncIterableIterator of parsed Log_ objects
266
+ * @param opts - Log filter options containing:
267
+ * - `startBlock`: Starting slot number (inclusive)
268
+ * - `startTime`: Starting Unix timestamp (inclusive)
269
+ * - `endBlock`: Ending slot number (inclusive)
270
+ * - `address`: Program address to filter logs by (required for Solana)
271
+ * - `topics`: Array of topics to filter logs by (optional); either 0x-8B discriminants or event names
272
+ * - `programs`: Special option to allow querying by address of interest, but yielding matching
273
+ * logs from specific (string address) program or any (true)
274
+ * - `commit`: Special param for fetching ExecutionReceipts, to narrow down the search
275
+ * @returns AsyncIterableIterator of parsed Log_ objects.
277
276
  */
278
277
  async *getLogs(opts) {
279
278
  let programs;
@@ -298,20 +297,43 @@ export class SolanaChain extends Chain {
298
297
  opts.topics.push(...opts.topics.filter((t) => !isHexString(t, 8)).map((t) => hexDiscriminator(t)));
299
298
  }
300
299
  // Process signatures and yield logs
301
- for await (const tx of this._getTransactionsForAddress(opts)) {
300
+ for await (const tx of this.getTransactionsForAddress(opts)) {
302
301
  for (const log of tx.logs) {
303
302
  // Filter and yield logs from the specified program, and which match event discriminant or log prefix
304
303
  if ((programs !== true && !programs.includes(log.address)) ||
305
304
  (opts.topics?.length &&
306
305
  !opts.topics.some((t) => t === log.topics[0] || (typeof log.data === 'string' && log.data.startsWith(t)))))
307
306
  continue;
308
- yield Object.assign(log, { timestamp: new Date(tx.timestamp * 1000) });
307
+ yield log;
309
308
  }
310
309
  }
311
310
  }
311
+ /** {@inheritDoc Chain.fetchRequestsInTx} */
312
+ async fetchRequestsInTx(tx) {
313
+ return fetchCCIPRequestsInTx(this, typeof tx === 'string' ? await this.getTransaction(tx) : tx);
314
+ }
315
+ /** {@inheritDoc Chain.fetchRequestById} */
316
+ fetchRequestById(messageId, onRamp, opts) {
317
+ if (!onRamp)
318
+ throw new Error('onRamp is required');
319
+ return fetchCCIPRequestById(this, messageId, { address: onRamp, ...opts });
320
+ }
321
+ /** {@inheritDoc Chain.fetchAllMessagesInBatch} */
322
+ async fetchAllMessagesInBatch(request, commit, opts) {
323
+ const [destChainStatePda] = PublicKey.findProgramAddressSync([Buffer.from('dest_chain_state'), toLeArray(request.lane.destChainSelector, 8)], new PublicKey(request.log.address));
324
+ // fetchAllMessagesInBatch pass opts back to getLogs; use it to narrow getLogs filter only to
325
+ // txs touching destChainStatePda
326
+ const opts_ = {
327
+ ...opts,
328
+ programs: [request.log.address],
329
+ address: destChainStatePda.toBase58(),
330
+ };
331
+ return fetchAllMessagesInBatch(this, request, commit, opts_);
332
+ }
333
+ /** {@inheritDoc Chain.typeAndVersion} */
312
334
  async typeAndVersion(address) {
313
335
  const program = new Program(CCIP_OFFRAMP_IDL, // `typeVersion` schema should be the same
314
- new PublicKey(address), simulationProvider(this.connection));
336
+ new PublicKey(address), simulationProvider(this));
315
337
  // Create the typeVersion instruction
316
338
  const returnDataString = (await program.methods
317
339
  .typeVersion()
@@ -322,9 +344,11 @@ export class SolanaChain extends Chain {
322
344
  res[1] = CCIPVersion.V1_6;
323
345
  return res;
324
346
  }
347
+ /** {@inheritDoc Chain.getRouterForOnRamp} */
325
348
  getRouterForOnRamp(onRamp, _destChainSelector) {
326
349
  return Promise.resolve(onRamp); // Solana's router is also the onRamp
327
350
  }
351
+ /** {@inheritDoc Chain.getRouterForOffRamp} */
328
352
  async getRouterForOffRamp(offRamp, _sourceChainSelector) {
329
353
  const offRamp_ = new PublicKey(offRamp);
330
354
  const program = new Program(CCIP_OFFRAMP_IDL, offRamp_, {
@@ -338,9 +362,11 @@ export class SolanaChain extends Chain {
338
362
  const { router } = program.coder.accounts.decode('referenceAddresses', referenceAddressesPda.data);
339
363
  return router.toBase58();
340
364
  }
365
+ /** {@inheritDoc Chain.getNativeTokenForRouter} */
341
366
  getNativeTokenForRouter(_router) {
342
367
  return Promise.resolve(NATIVE_MINT.toBase58());
343
368
  }
369
+ /** {@inheritDoc Chain.getOffRampsForRouter} */
344
370
  async getOffRampsForRouter(router, sourceChainSelector) {
345
371
  // feeQuoter is present in router's config, and has a DestChainState account which is updated by
346
372
  // the offramps, so we can use it to narrow the search for the offramp
@@ -355,9 +381,11 @@ export class SolanaChain extends Chain {
355
381
  }
356
382
  throw new Error(`Could not find OffRamp events in feeQuoter=${feeQuoter.toString()} txs`);
357
383
  }
384
+ /** {@inheritDoc Chain.getOnRampForRouter} */
358
385
  getOnRampForRouter(router, _destChainSelector) {
359
386
  return Promise.resolve(router); // solana's Router is also the OnRamp
360
387
  }
388
+ /** {@inheritDoc Chain.getOnRampForOffRamp} */
361
389
  async getOnRampForOffRamp(offRamp, sourceChainSelector) {
362
390
  const program = new Program(CCIP_OFFRAMP_IDL, new PublicKey(offRamp), {
363
391
  connection: this.connection,
@@ -367,9 +395,11 @@ export class SolanaChain extends Chain {
367
395
  const { config: { onRamp }, } = await program.account.sourceChain.fetch(statePda);
368
396
  return decodeAddress(new Uint8Array(onRamp.bytes.slice(0, onRamp.len)), networkInfo(sourceChainSelector).family);
369
397
  }
398
+ /** {@inheritDoc Chain.getCommitStoreForOffRamp} */
370
399
  getCommitStoreForOffRamp(offRamp) {
371
400
  return Promise.resolve(offRamp); // Solana supports only CCIP>=1.6, for which OffRamp and CommitStore are the same
372
401
  }
402
+ /** {@inheritDoc Chain.getTokenForTokenPool} */
373
403
  async getTokenForTokenPool(tokenPool) {
374
404
  const tokenPoolInfo = await this.connection.getAccountInfo(new PublicKey(tokenPool));
375
405
  if (!tokenPoolInfo)
@@ -377,6 +407,7 @@ export class SolanaChain extends Chain {
377
407
  const { config } = tokenPoolCoder.accounts.decode('state', tokenPoolInfo.data);
378
408
  return config.mint.toString();
379
409
  }
410
+ /** {@inheritDoc Chain.getTokenInfo} */
380
411
  async getTokenInfo(token) {
381
412
  const mint = new PublicKey(token);
382
413
  const mintInfo = await this.connection.getParsedAccountInfo(mint);
@@ -408,7 +439,7 @@ export class SolanaChain extends Chain {
408
439
  }
409
440
  catch (error) {
410
441
  // Metaplex metadata fetch failed, keep the default values
411
- console.debug(`Failed to fetch Metaplex metadata for token ${token}:`, error);
442
+ this.logger.debug(`Failed to fetch Metaplex metadata for token ${token}:`, error);
412
443
  }
413
444
  }
414
445
  return {
@@ -421,6 +452,11 @@ export class SolanaChain extends Chain {
421
452
  throw new Error(`Unable to parse token data for ${token}`);
422
453
  }
423
454
  }
455
+ /**
456
+ * Fetches token metadata from Metaplex.
457
+ * @param mintPublicKey - Token mint public key.
458
+ * @returns Token name and symbol, or null if not found.
459
+ */
424
460
  async _fetchTokenMetadata(mintPublicKey) {
425
461
  try {
426
462
  // Token Metadata Program ID
@@ -467,34 +503,30 @@ export class SolanaChain extends Chain {
467
503
  return name || symbol ? { name, symbol } : null;
468
504
  }
469
505
  catch (error) {
470
- console.debug('Error fetching token metadata:', error);
506
+ this.logger.debug('Error fetching token metadata:', error);
471
507
  return null;
472
508
  }
473
509
  }
510
+ /**
511
+ * Decodes a CCIP message from a Solana log event.
512
+ * @param log - Log with data field.
513
+ * @returns Decoded CCIPMessage or undefined if not valid.
514
+ */
474
515
  static decodeMessage({ data }) {
475
516
  if (!data || typeof data !== 'string')
476
517
  return undefined;
477
- let eventDataBuffer;
518
+ // Verify the discriminant matches CCIPMessageSent
478
519
  try {
479
- eventDataBuffer = bytesToBuffer(data);
520
+ if (dataSlice(getDataBytes(data), 0, 8) !== hexDiscriminator('CCIPMessageSent'))
521
+ return;
480
522
  }
481
523
  catch (_) {
482
524
  return;
483
525
  }
484
- const disc = dataSlice(eventDataBuffer, 0, 8);
485
- if (disc !== hexDiscriminator('CCIPMessageSent'))
526
+ const decoded = routerCoder.events.decode(data);
527
+ if (decoded?.name !== 'CCIPMessageSent')
486
528
  return;
487
- // Use module-level BorshCoder for decoding structs
488
- // Manually parse event header (discriminator + event-level fields)
489
- let offset = 8;
490
- // Parse event-level fields
491
- const _destChainSelector = eventDataBuffer.readBigUInt64LE(offset);
492
- offset += 8;
493
- const _sequenceNumber = eventDataBuffer.readBigUInt64LE(offset);
494
- offset += 8;
495
- // Now decode the SVM2AnyRampMessage struct using BorshCoder
496
- const messageBytes = eventDataBuffer.subarray(offset);
497
- const message = routerCoder.types.decode('SVM2AnyRampMessage', messageBytes);
529
+ const message = decoded.data.message;
498
530
  // Convert BN/number types to bigints
499
531
  const sourceChainSelector = BigInt(message.header.sourceChainSelector.toString());
500
532
  const destChainSelector = BigInt(message.header.destChainSelector.toString());
@@ -547,6 +579,11 @@ export class SolanaChain extends Chain {
547
579
  ...rest,
548
580
  };
549
581
  }
582
+ /**
583
+ * Decodes extra arguments from Solana CCIP messages.
584
+ * @param extraArgs - Encoded extra arguments bytes.
585
+ * @returns Decoded EVMExtraArgsV2 or undefined if unknown format.
586
+ */
550
587
  static decodeExtraArgs(extraArgs) {
551
588
  const data = getDataBytes(extraArgs), tag = dataSlice(data, 0, 4);
552
589
  switch (tag) {
@@ -565,6 +602,11 @@ export class SolanaChain extends Chain {
565
602
  return;
566
603
  }
567
604
  }
605
+ /**
606
+ * Encodes extra arguments for Solana CCIP messages.
607
+ * @param args - Extra arguments to encode.
608
+ * @returns Encoded extra arguments as hex string.
609
+ */
568
610
  static encodeExtraArgs(args) {
569
611
  if ('computeUnits' in args)
570
612
  throw new Error('Solana can only encode EVMExtraArgsV2');
@@ -575,35 +617,33 @@ export class SolanaChain extends Chain {
575
617
  'allowOutOfOrderExecution' in args && args.allowOutOfOrderExecution ? '0x01' : '0x00',
576
618
  ]);
577
619
  }
620
+ /**
621
+ * Decodes commit reports from a Solana log event.
622
+ * @param log - Log with data field.
623
+ * @param lane - Lane info for filtering.
624
+ * @returns Array of CommitReport or undefined if not valid.
625
+ */
578
626
  static decodeCommits(log, lane) {
579
627
  // Check if this is a CommitReportAccepted event by looking at the discriminant
580
628
  if (!log.data || typeof log.data !== 'string') {
581
629
  throw new Error('Log data is missing or not a string');
582
630
  }
583
- const eventDataBuffer = bytesToBuffer(log.data);
584
- // Verify the discriminant matches CommitReportAccepted
585
- const expectedDiscriminant = hexDiscriminator('CommitReportAccepted');
586
- const actualDiscriminant = hexlify(eventDataBuffer.subarray(0, 8));
587
- if (actualDiscriminant !== expectedDiscriminant)
631
+ try {
632
+ // Verify the discriminant matches CommitReportAccepted
633
+ if (dataSlice(getDataBytes(log.data), 0, 8) !== hexDiscriminator('CommitReportAccepted'))
634
+ return;
635
+ }
636
+ catch (_) {
588
637
  return;
589
- // Skip the 8-byte discriminant and decode the event data manually
590
- let offset = 8;
591
- // Decode Option<MerkleRoot> - first byte indicates Some(1) or None(0)
592
- const hasValue = eventDataBuffer.readUInt8(offset);
593
- offset += 1;
594
- if (!hasValue)
595
- return [];
596
- // Decode MerkleRoot struct using the types decoder
597
- // We need to read the remaining bytes as a MerkleRoot struct
598
- const merkleRootBytes = eventDataBuffer.subarray(offset);
599
- const merkleRootData = offrampCoder.types.decode('MerkleRoot', merkleRootBytes);
600
- if (!merkleRootData) {
601
- throw new Error('Failed to decode MerkleRoot data');
602
638
  }
639
+ const decoded = offrampCoder.events.decode(log.data);
640
+ if (decoded?.name !== 'CommitReportAccepted' || !decoded.data?.merkleRoot)
641
+ return;
642
+ const merkleRoot = decoded.data.merkleRoot;
603
643
  // Verify the source chain selector matches our lane
604
- const sourceChainSelector = BigInt(merkleRootData.sourceChainSelector.toString());
644
+ const sourceChainSelector = BigInt(merkleRoot.sourceChainSelector.toString());
605
645
  // Convert the onRampAddress from bytes to the proper format
606
- const onRampAddress = decodeOnRampAddress(merkleRootData.onRampAddress, networkInfo(sourceChainSelector).family);
646
+ const onRampAddress = decodeOnRampAddress(merkleRoot.onRampAddress, networkInfo(sourceChainSelector).family);
607
647
  if (lane) {
608
648
  if (sourceChainSelector !== lane.sourceChainSelector)
609
649
  return;
@@ -615,40 +655,48 @@ export class SolanaChain extends Chain {
615
655
  {
616
656
  sourceChainSelector,
617
657
  onRampAddress,
618
- minSeqNr: BigInt(merkleRootData.minSeqNr.toString()),
619
- maxSeqNr: BigInt(merkleRootData.maxSeqNr.toString()),
620
- merkleRoot: hexlify(new Uint8Array(merkleRootData.merkleRoot)),
658
+ minSeqNr: BigInt(merkleRoot.minSeqNr.toString()),
659
+ maxSeqNr: BigInt(merkleRoot.maxSeqNr.toString()),
660
+ merkleRoot: hexlify(getDataBytes(merkleRoot.merkleRoot)),
621
661
  },
622
662
  ];
623
663
  }
664
+ /**
665
+ * Decodes an execution receipt from a Solana log event.
666
+ * @param log - Log with data, tx, and index fields.
667
+ * @returns ExecutionReceipt or undefined if not valid.
668
+ */
624
669
  static decodeReceipt(log) {
625
670
  // Check if this is a ExecutionStateChanged event by looking at the discriminant
626
671
  if (!log.data || typeof log.data !== 'string') {
627
672
  throw new Error('Log data is missing or not a string');
628
673
  }
629
- // Verify the discriminant matches ExecutionStateChanged
630
- if (dataSlice(getDataBytes(log.data), 0, 8) !== hexDiscriminator('ExecutionStateChanged'))
674
+ try {
675
+ // Verify the discriminant matches ExecutionStateChanged
676
+ if (dataSlice(getDataBytes(log.data), 0, 8) !== hexDiscriminator('ExecutionStateChanged'))
677
+ return;
678
+ }
679
+ catch (_) {
680
+ return;
681
+ }
682
+ const decoded = offrampCoder.events.decode(log.data);
683
+ if (decoded?.name !== 'ExecutionStateChanged')
631
684
  return;
632
- const eventDataBuffer = bytesToBuffer(log.data);
633
- // Note: We manually decode the event fields rather than using BorshCoder
634
- // since ExecutionStateChanged is an event, not a defined type
635
- // Skip the 8-byte discriminant and manually decode the event fields
636
- let offset = 8;
637
- // Decode sourceChainSelector (u64)
638
- const sourceChainSelector = eventDataBuffer.readBigUInt64LE(offset);
639
- offset += 8;
640
- // Decode sequenceNumber (u64)
641
- const sequenceNumber = eventDataBuffer.readBigUInt64LE(offset);
642
- offset += 8;
643
- // Decode messageId ([u8; 32])
644
- const messageId = hexlify(eventDataBuffer.subarray(offset, offset + 32));
645
- offset += 32;
646
- // Decode messageHash ([u8; 32])
647
- const messageHash = hexlify(eventDataBuffer.subarray(offset, offset + 32));
648
- offset += 32;
685
+ const messageId = hexlify(getDataBytes(decoded.data.messageId));
649
686
  // Decode state enum (MessageExecutionState)
650
687
  // Enum discriminant is a single byte: Untouched=0, InProgress=1, Success=2, Failure=3
651
- let state = eventDataBuffer.readUInt8(offset);
688
+ let state;
689
+ if (decoded.data.state.inProgress) {
690
+ state = ExecutionState.InProgress;
691
+ }
692
+ else if (decoded.data.state.success) {
693
+ state = ExecutionState.Success;
694
+ }
695
+ else if (decoded.data.state.failure) {
696
+ state = ExecutionState.Failed;
697
+ }
698
+ else
699
+ throw new Error(`Invalid ExecutionState: ${util.inspect(decoded.data.state)}`);
652
700
  let returnData;
653
701
  if (log.tx) {
654
702
  // use only last receipt per tx+message (i.e. skip intermediary InProgress=1 states for Solana)
@@ -670,14 +718,19 @@ export class SolanaChain extends Chain {
670
718
  }
671
719
  }
672
720
  return {
673
- sourceChainSelector,
674
- sequenceNumber,
721
+ sourceChainSelector: BigInt(decoded.data.sourceChainSelector.toString()),
722
+ sequenceNumber: BigInt(decoded.data.sequenceNumber.toString()),
675
723
  messageId,
676
- messageHash,
724
+ messageHash: hexlify(getDataBytes(decoded.data.messageHash)),
677
725
  state,
678
726
  returnData,
679
727
  };
680
728
  }
729
+ /**
730
+ * Converts bytes to a Solana address (Base58).
731
+ * @param bytes - Bytes to convert.
732
+ * @returns Base58-encoded Solana address.
733
+ */
681
734
  static getAddress(bytes) {
682
735
  try {
683
736
  if (typeof bytes === 'string' && bs58.decode(bytes).length === 32)
@@ -688,9 +741,15 @@ export class SolanaChain extends Chain {
688
741
  }
689
742
  return encodeBase58(getDataBytes(bytes));
690
743
  }
691
- static getDestLeafHasher(lane) {
692
- return getV16SolanaLeafHasher(lane);
744
+ /**
745
+ * Gets the leaf hasher for Solana destination chains.
746
+ * @param lane - Lane configuration.
747
+ * @returns Leaf hasher function.
748
+ */
749
+ static getDestLeafHasher(lane, ctx) {
750
+ return getV16SolanaLeafHasher(lane, ctx);
693
751
  }
752
+ /** {@inheritDoc Chain.getTokenAdminRegistryFor} */
694
753
  async getTokenAdminRegistryFor(address) {
695
754
  const [type] = await this.typeAndVersion(address);
696
755
  if (!type.includes('Router'))
@@ -698,52 +757,114 @@ export class SolanaChain extends Chain {
698
757
  // Solana implements TokenAdminRegistry in the Router/OnRamp program
699
758
  return address;
700
759
  }
760
+ /** {@inheritDoc Chain.getFee} */
761
+ getFee(router, destChainSelector, message) {
762
+ return getFee(this, router, destChainSelector, message);
763
+ }
701
764
  /**
702
- * Get the fee required to send a CCIP message from the Solana router.
765
+ * Raw/unsigned version of [[sendMessage]]
766
+ *
767
+ * @param sender - sender/feePayer address
768
+ * @param router - router address
769
+ * @param destChainSelector - destination chain selector
770
+ * @param message - AnyMessage to send (with or without fee)
771
+ * @param approveMax - approve max amount of tokens if needed, instead of only what's needed
772
+ * @returns instructions - array of instructions; `ccipSend` is last, after any approval
773
+ * lookupTables - array of lookup tables for `ccipSend` call
774
+ * mainIndex - instructions.length - 1
703
775
  */
704
- getFee(router, destChainSelector, message) {
705
- return getFee(this.connection, router, destChainSelector, message);
776
+ async generateUnsignedSendMessage(sender, router, destChainSelector, message, opts) {
777
+ if (!message.fee)
778
+ message.fee = await this.getFee(router, destChainSelector, message);
779
+ return generateUnsignedCcipSend(this, new PublicKey(sender), new PublicKey(router), destChainSelector, message, opts);
706
780
  }
707
- async sendMessage(router_, destChainSelector, message, opts) {
708
- const wallet = await this.getWallet(opts);
709
- const router = new Program(CCIP_ROUTER_IDL, new PublicKey(router_), new AnchorProvider(this.connection, wallet, { commitment: this.commitment }));
710
- const { hash } = await ccipSend(router, destChainSelector, message, opts);
711
- return this.getTransaction(hash);
781
+ /** {@inheritDoc Chain.sendMessage} */
782
+ async sendMessage(router, destChainSelector, message, opts) {
783
+ const wallet = opts.wallet;
784
+ if (!isWallet(wallet))
785
+ throw new Error(`Expected Wallet, got=${util.inspect(wallet)}`);
786
+ const unsigned = await this.generateUnsignedSendMessage(wallet.publicKey.toBase58(), router, destChainSelector, message, opts);
787
+ const hash = await simulateAndSendTxs(this, wallet, unsigned);
788
+ return (await this.fetchRequestsInTx(await this.getTransaction(hash)))[0];
712
789
  }
790
+ /** {@inheritDoc Chain.fetchOffchainTokenData} */
713
791
  async fetchOffchainTokenData(request) {
714
- return fetchSolanaOffchainTokenData(this.connection, request);
792
+ return fetchSolanaOffchainTokenData(request, this);
715
793
  }
716
- async executeReport(offRamp, execReport_, opts) {
794
+ /**
795
+ * Raw/unsigned version of [[executeReport]]
796
+ * @param payer - payer address of the execution transaction
797
+ * @param offRamp - OffRamp contract address
798
+ * @param execReport_ - ExecutionReport of a dest=Solana message
799
+ * @param opts - execute report options
800
+ * - forceBuffer - Whether to force the use of a buffer account
801
+ * - forceLookupTable - Whether to force creation of a lookup table for the call
802
+ * @returns instructions - array of instructions to execute the report
803
+ * lookupTables - array of lookup tables for `manuallyExecute` call
804
+ * mainIndex - index of the `manuallyExecute` instruction in the array; last unless
805
+ * forceLookupTable is set, in which case last is ALT deactivation tx, and manuallyExecute is
806
+ * second to last
807
+ */
808
+ async generateUnsignedExecuteReport(payer, offRamp, execReport_, opts) {
717
809
  if (!('computeUnits' in execReport_.message))
718
810
  throw new Error("ExecutionReport's message not for Solana");
719
811
  const execReport = execReport_;
720
- const wallet = await this.getWallet(opts);
721
- const provider = new AnchorProvider(this.connection, wallet, { commitment: this.commitment });
722
- const offrampProgram = new Program(CCIP_OFFRAMP_IDL, new PublicKey(offRamp), provider);
723
- const rep = await executeReport({ offrampProgram, execReport, ...opts });
724
- if (opts?.clearLeftoverAccounts) {
812
+ const offRamp_ = new PublicKey(offRamp);
813
+ return generateUnsignedExecuteReport(this, new PublicKey(payer), offRamp_, execReport, opts);
814
+ }
815
+ /** {@inheritDoc Chain.executeReport} */
816
+ async executeReport(offRamp, execReport, opts) {
817
+ const wallet = opts.wallet;
818
+ if (!isWallet(wallet))
819
+ throw new Error(`Expected Wallet, got=${util.inspect(wallet)}`);
820
+ let hash;
821
+ do {
725
822
  try {
726
- await this.cleanUpBuffers(opts);
823
+ const unsigned = await this.generateUnsignedExecuteReport(wallet.publicKey.toBase58(), offRamp, execReport, opts);
824
+ hash = await simulateAndSendTxs(this, wallet, unsigned, opts?.gasLimit);
727
825
  }
728
826
  catch (err) {
729
- console.warn('Error while trying to clean up buffers:', err);
827
+ if (!(err instanceof Error) ||
828
+ !['encoding overruns Uint8Array', 'too large'].some((e) => err.message.includes(e)))
829
+ throw err;
830
+ // in case of failure to serialize a report, first try buffering (because it gets
831
+ // auto-closed upon successful execution), then ALTs (need a grace period ~3min after
832
+ // deactivation before they can be closed/recycled)
833
+ if (!opts?.forceBuffer)
834
+ opts = { ...opts, forceBuffer: true };
835
+ else if (!opts?.forceLookupTable)
836
+ opts = { ...opts, forceLookupTable: true };
837
+ else
838
+ throw err;
730
839
  }
840
+ } while (!hash);
841
+ try {
842
+ await this.cleanUpBuffers(opts);
731
843
  }
732
- return this.getTransaction(rep.hash);
844
+ catch (err) {
845
+ this.logger.warn('Error while trying to clean up buffers:', err);
846
+ }
847
+ return this.getTransaction(hash);
733
848
  }
734
849
  /**
735
850
  * Clean up and recycle buffers and address lookup tables owned by wallet
736
- * CAUTION: this will close ANY lookup table owned by this wallet
737
- * @param wallet - wallet options
738
- * @param dontWait - Whether to skip waiting for lookup table deactivation cool down period
739
- * (513 slots) to pass before closing; by default, we deactivate (if needed) and wait to close
740
- * before returning from this method
851
+ * @param opts - cleanUp options
852
+ * - wallet - wallet instance to sign txs
853
+ * - waitDeactivation - Whether to wait for lookup table deactivation cool down period
854
+ * (513 slots) to pass before closing; by default, we deactivate (if needed) and move on, to
855
+ * close other ready ALTs
741
856
  */
742
857
  async cleanUpBuffers(opts) {
743
- const wallet = await this.getWallet(opts);
744
- const provider = new AnchorProvider(this.connection, wallet, { commitment: this.commitment });
745
- await cleanUpBuffers(provider, this.getLogs.bind(this), opts);
858
+ const wallet = opts.wallet;
859
+ if (!isWallet(wallet))
860
+ throw new Error(`Expected Wallet, got=${util.inspect(wallet)}`);
861
+ await cleanUpBuffers(this, wallet, this.getLogs.bind(this), opts);
746
862
  }
863
+ /**
864
+ * Parses raw Solana data into typed structures.
865
+ * @param data - Raw data to parse.
866
+ * @returns Parsed data or undefined.
867
+ */
747
868
  static parse(data) {
748
869
  if (!data)
749
870
  return;
@@ -794,8 +915,8 @@ export class SolanaChain extends Chain {
794
915
  bytes: encodeBase58(toLeArray(request.lane.sourceChainSelector, 8)),
795
916
  },
796
917
  },
797
- // dirty trick: memcmp report.min with msg.sequenceNumber's without least-significant byte;
798
- // this should be ~256 around seqNum, i.e. big chance of a match
918
+ // hack: memcmp report.min with msg.sequenceNumber's without least-significant byte;
919
+ // this should be ~256 around seqNum, i.e. big chance of a match; requires PDAs to be alive
799
920
  {
800
921
  memcmp: {
801
922
  offset: 8 + 1 + 8 + 32 + 8 + 1,
@@ -827,38 +948,25 @@ export class SolanaChain extends Chain {
827
948
  // in case we can't find it, fallback to generic iterating txs
828
949
  return super.fetchCommitReport(commitStore, request, hints);
829
950
  }
830
- // specialized override with stricter address-of-interest
831
- async *fetchExecutionReceipts(offRamp, messageIds, hints) {
832
- if (!hints?.commit) {
833
- // if no commit, fall back to generic implementation
834
- yield* super.fetchExecutionReceipts(offRamp, messageIds, hints);
835
- return;
836
- }
837
- // otherwise, use `commit_report` PDA as more specialized address
838
- const [commitReportPda] = PublicKey.findProgramAddressSync([
839
- Buffer.from('commit_report'),
840
- toLeArray(hints.commit.sourceChainSelector, 8),
841
- bytesToBuffer(hints.commit.merkleRoot),
842
- ], new PublicKey(offRamp));
843
- // rest is similar to generic implemenetation
844
- const onlyLast = !hints?.startBlock && !hints?.startTime; // backwards
845
- for await (const log of this.getLogs({
846
- ...hints,
847
- programs: [offRamp],
848
- address: commitReportPda.toBase58(),
849
- topics: ['ExecutionStateChanged'],
850
- })) {
851
- const receipt = this.constructor.decodeReceipt(log);
852
- if (!receipt || !messageIds.has(receipt.messageId))
853
- continue;
854
- if (onlyLast || receipt.state === ExecutionState.Success)
855
- messageIds.delete(receipt.messageId);
856
- const timestamp = await this.getBlockTimestamp(log.blockNumber);
857
- yield { receipt, log, timestamp };
858
- if (!messageIds.size)
859
- break;
951
+ /** {@inheritDoc Chain.fetchExecutionReceipts} */
952
+ async *fetchExecutionReceipts(offRamp, request, commit, opts) {
953
+ let opts_ = opts;
954
+ if (commit) {
955
+ // if we know of commit, use `commit_report` PDA as more specialized address
956
+ const [commitReportPda] = PublicKey.findProgramAddressSync([
957
+ Buffer.from('commit_report'),
958
+ toLeArray(request.lane.sourceChainSelector, 8),
959
+ bytesToBuffer(commit.report.merkleRoot),
960
+ ], new PublicKey(offRamp));
961
+ opts_ = {
962
+ ...opts,
963
+ programs: [offRamp],
964
+ address: commitReportPda.toBase58(),
965
+ };
860
966
  }
967
+ yield* super.fetchExecutionReceipts(offRamp, request, commit, opts_);
861
968
  }
969
+ /** {@inheritDoc Chain.getRegistryTokenConfig} */
862
970
  async getRegistryTokenConfig(registry, token) {
863
971
  const registry_ = new PublicKey(registry);
864
972
  const tokenMint = new PublicKey(token);
@@ -893,6 +1001,7 @@ export class SolanaChain extends Chain {
893
1001
  }
894
1002
  return config;
895
1003
  }
1004
+ /** {@inheritDoc Chain.getTokenPoolConfigs} */
896
1005
  async getTokenPoolConfigs(tokenPool) {
897
1006
  // `tokenPool` is actually a State PDA in the tokenPoolProgram
898
1007
  const tokenPoolState = await this.connection.getAccountInfo(new PublicKey(tokenPool));
@@ -915,6 +1024,7 @@ export class SolanaChain extends Chain {
915
1024
  typeAndVersion,
916
1025
  };
917
1026
  }
1027
+ /** {@inheritDoc Chain.getTokenPoolRemotes} */
918
1028
  async getTokenPoolRemotes(tokenPool, remoteChainSelector) {
919
1029
  // `tokenPool` is actually a State PDA in the tokenPoolProgram
920
1030
  const tokenPoolState = await this.connection.getAccountInfo(new PublicKey(tokenPool));
@@ -1023,11 +1133,12 @@ export class SolanaChain extends Chain {
1023
1133
  };
1024
1134
  }
1025
1135
  catch (err) {
1026
- console.warn('Failed to decode ChainConfig account:', err);
1136
+ this.logger.warn('Failed to decode ChainConfig account:', err);
1027
1137
  }
1028
1138
  }
1029
1139
  return remotes;
1030
1140
  }
1141
+ /** {@inheritDoc Chain.getSupportedTokens} */
1031
1142
  async getSupportedTokens(router) {
1032
1143
  // `mint` offset in TokenAdminRegistry account data; more robust against changes in layout
1033
1144
  const mintOffset = 8 + 1 + 32 + 32 + 32 + 16 * 2; // = 137
@@ -1053,7 +1164,8 @@ export class SolanaChain extends Chain {
1053
1164
  }
1054
1165
  return res;
1055
1166
  }
1056
- async listFeeTokens(router) {
1167
+ /** {@inheritDoc Chain.getFeeTokens} */
1168
+ async getFeeTokens(router) {
1057
1169
  const { feeQuoter } = await this._getRouterConfig(router);
1058
1170
  const tokenConfigs = await this.connection.getProgramAccounts(feeQuoter, {
1059
1171
  filters: [
@@ -1066,11 +1178,15 @@ export class SolanaChain extends Chain {
1066
1178
  ],
1067
1179
  });
1068
1180
  return Object.fromEntries(await Promise.all(tokenConfigs.map(async (acc) => {
1069
- const token = new PublicKey(acc.account.data.subarray(10, 10 + 32));
1070
- return [token.toBase58(), await this.getTokenInfo(token.toBase58())];
1181
+ const token = new PublicKey(acc.account.data.subarray(10, 10 + 32)).toBase58();
1182
+ return [token, await this.getTokenInfo(token)];
1071
1183
  })));
1072
1184
  }
1073
- // cached
1185
+ /**
1186
+ * Gets the router configuration from the Config PDA.
1187
+ * @param router - Router program address.
1188
+ * @returns Router configuration including feeQuoter.
1189
+ */
1074
1190
  async _getRouterConfig(router) {
1075
1191
  const program = new Program(CCIP_ROUTER_IDL, new PublicKey(router), {
1076
1192
  connection: this.connection,
@@ -1081,5 +1197,4 @@ export class SolanaChain extends Chain {
1081
1197
  return program.account.config.fetch(configPda);
1082
1198
  }
1083
1199
  }
1084
- supportedChains[ChainFamily.Solana] = SolanaChain;
1085
1200
  //# sourceMappingURL=index.js.map