@chainlink/ccip-sdk 0.0.0 → 0.90.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.
- package/LICENSE +21 -0
- package/README.md +109 -0
- package/dist/aptos/exec.d.ts +18 -0
- package/dist/aptos/exec.d.ts.map +1 -0
- package/dist/aptos/exec.js +55 -0
- package/dist/aptos/exec.js.map +1 -0
- package/dist/aptos/hasher.d.ts +11 -0
- package/dist/aptos/hasher.d.ts.map +1 -0
- package/dist/aptos/hasher.js +62 -0
- package/dist/aptos/hasher.js.map +1 -0
- package/dist/aptos/index.d.ts +92 -0
- package/dist/aptos/index.d.ts.map +1 -0
- package/dist/aptos/index.js +482 -0
- package/dist/aptos/index.js.map +1 -0
- package/dist/aptos/logs.d.ts +9 -0
- package/dist/aptos/logs.d.ts.map +1 -0
- package/dist/aptos/logs.js +167 -0
- package/dist/aptos/logs.js.map +1 -0
- package/dist/aptos/send.d.ts +11 -0
- package/dist/aptos/send.d.ts.map +1 -0
- package/dist/aptos/send.js +78 -0
- package/dist/aptos/send.js.map +1 -0
- package/dist/aptos/token.d.ts +4 -0
- package/dist/aptos/token.d.ts.map +1 -0
- package/dist/aptos/token.js +134 -0
- package/dist/aptos/token.js.map +1 -0
- package/dist/aptos/types.d.ts +78 -0
- package/dist/aptos/types.d.ts.map +1 -0
- package/dist/aptos/types.js +60 -0
- package/dist/aptos/types.js.map +1 -0
- package/dist/aptos/utils.d.ts +12 -0
- package/dist/aptos/utils.d.ts.map +1 -0
- package/dist/aptos/utils.js +15 -0
- package/dist/aptos/utils.js.map +1 -0
- package/dist/chain.d.ts +344 -0
- package/dist/chain.d.ts.map +1 -0
- package/dist/chain.js +41 -0
- package/dist/chain.js.map +1 -0
- package/dist/commits.d.ts +25 -0
- package/dist/commits.d.ts.map +1 -0
- package/dist/commits.js +29 -0
- package/dist/commits.js.map +1 -0
- package/dist/evm/abi/BurnMintERC677Token.d.ts +602 -0
- package/dist/evm/abi/BurnMintERC677Token.d.ts.map +1 -0
- package/dist/evm/abi/BurnMintERC677Token.js +488 -0
- package/dist/evm/abi/BurnMintERC677Token.js.map +1 -0
- package/dist/evm/abi/CommitStore_1_2.d.ts +688 -0
- package/dist/evm/abi/CommitStore_1_2.d.ts.map +1 -0
- package/dist/evm/abi/CommitStore_1_2.js +638 -0
- package/dist/evm/abi/CommitStore_1_2.js.map +1 -0
- package/dist/evm/abi/CommitStore_1_5.d.ts +708 -0
- package/dist/evm/abi/CommitStore_1_5.d.ts.map +1 -0
- package/dist/evm/abi/CommitStore_1_5.js +675 -0
- package/dist/evm/abi/CommitStore_1_5.js.map +1 -0
- package/dist/evm/abi/FeeQuoter_1_6.d.ts +1770 -0
- package/dist/evm/abi/FeeQuoter_1_6.d.ts.map +1 -0
- package/dist/evm/abi/FeeQuoter_1_6.js +1904 -0
- package/dist/evm/abi/FeeQuoter_1_6.js.map +1 -0
- package/dist/evm/abi/LockReleaseTokenPool_1_5.d.ts +1116 -0
- package/dist/evm/abi/LockReleaseTokenPool_1_5.d.ts.map +1 -0
- package/dist/evm/abi/LockReleaseTokenPool_1_5.js +1096 -0
- package/dist/evm/abi/LockReleaseTokenPool_1_5.js.map +1 -0
- package/dist/evm/abi/LockReleaseTokenPool_1_5_1.d.ts +1306 -0
- package/dist/evm/abi/LockReleaseTokenPool_1_5_1.d.ts.map +1 -0
- package/dist/evm/abi/LockReleaseTokenPool_1_5_1.js +1278 -0
- package/dist/evm/abi/LockReleaseTokenPool_1_5_1.js.map +1 -0
- package/dist/evm/abi/LockReleaseTokenPool_1_6_1.d.ts +1290 -0
- package/dist/evm/abi/LockReleaseTokenPool_1_6_1.d.ts.map +1 -0
- package/dist/evm/abi/LockReleaseTokenPool_1_6_1.js +1288 -0
- package/dist/evm/abi/LockReleaseTokenPool_1_6_1.js.map +1 -0
- package/dist/evm/abi/OffRamp_1_2.d.ts +1217 -0
- package/dist/evm/abi/OffRamp_1_2.d.ts.map +1 -0
- package/dist/evm/abi/OffRamp_1_2.js +1204 -0
- package/dist/evm/abi/OffRamp_1_2.js.map +1 -0
- package/dist/evm/abi/OffRamp_1_5.d.ts +1271 -0
- package/dist/evm/abi/OffRamp_1_5.d.ts.map +1 -0
- package/dist/evm/abi/OffRamp_1_5.js +1273 -0
- package/dist/evm/abi/OffRamp_1_5.js.map +1 -0
- package/dist/evm/abi/OffRamp_1_6.d.ts +1472 -0
- package/dist/evm/abi/OffRamp_1_6.d.ts.map +1 -0
- package/dist/evm/abi/OffRamp_1_6.js +1529 -0
- package/dist/evm/abi/OffRamp_1_6.js.map +1 -0
- package/dist/evm/abi/OnRamp_1_2.d.ts +1391 -0
- package/dist/evm/abi/OnRamp_1_2.d.ts.map +1 -0
- package/dist/evm/abi/OnRamp_1_2.js +1343 -0
- package/dist/evm/abi/OnRamp_1_2.js.map +1 -0
- package/dist/evm/abi/OnRamp_1_5.d.ts +1443 -0
- package/dist/evm/abi/OnRamp_1_5.d.ts.map +1 -0
- package/dist/evm/abi/OnRamp_1_5.js +1427 -0
- package/dist/evm/abi/OnRamp_1_5.js.map +1 -0
- package/dist/evm/abi/OnRamp_1_6.d.ts +796 -0
- package/dist/evm/abi/OnRamp_1_6.d.ts.map +1 -0
- package/dist/evm/abi/OnRamp_1_6.js +880 -0
- package/dist/evm/abi/OnRamp_1_6.js.map +1 -0
- package/dist/evm/abi/Router.d.ts +541 -0
- package/dist/evm/abi/Router.d.ts.map +1 -0
- package/dist/evm/abi/Router.js +508 -0
- package/dist/evm/abi/Router.js.map +1 -0
- package/dist/evm/abi/TokenAdminRegistry_1_5.d.ts +373 -0
- package/dist/evm/abi/TokenAdminRegistry_1_5.d.ts.map +1 -0
- package/dist/evm/abi/TokenAdminRegistry_1_5.js +333 -0
- package/dist/evm/abi/TokenAdminRegistry_1_5.js.map +1 -0
- package/dist/evm/const.d.ts +27 -0
- package/dist/evm/const.d.ts.map +1 -0
- package/dist/evm/const.js +63 -0
- package/dist/evm/const.js.map +1 -0
- package/dist/evm/errors.d.ts +36 -0
- package/dist/evm/errors.d.ts.map +1 -0
- package/dist/evm/errors.js +192 -0
- package/dist/evm/errors.js.map +1 -0
- package/dist/evm/hasher.d.ts +5 -0
- package/dist/evm/hasher.d.ts.map +1 -0
- package/dist/evm/hasher.js +116 -0
- package/dist/evm/hasher.js.map +1 -0
- package/dist/evm/index.d.ts +121 -0
- package/dist/evm/index.d.ts.map +1 -0
- package/dist/evm/index.js +904 -0
- package/dist/evm/index.js.map +1 -0
- package/dist/evm/messages.d.ts +35 -0
- package/dist/evm/messages.d.ts.map +1 -0
- package/dist/evm/messages.js +11 -0
- package/dist/evm/messages.js.map +1 -0
- package/dist/evm/offchain.d.ts +16 -0
- package/dist/evm/offchain.d.ts.map +1 -0
- package/dist/evm/offchain.js +142 -0
- package/dist/evm/offchain.js.map +1 -0
- package/dist/execution.d.ts +80 -0
- package/dist/execution.d.ts.map +1 -0
- package/dist/execution.js +91 -0
- package/dist/execution.js.map +1 -0
- package/dist/extra-args.d.ts +45 -0
- package/dist/extra-args.d.ts.map +1 -0
- package/dist/extra-args.js +44 -0
- package/dist/extra-args.js.map +1 -0
- package/dist/gas.d.ts +27 -0
- package/dist/gas.d.ts.map +1 -0
- package/dist/gas.js +80 -0
- package/dist/gas.js.map +1 -0
- package/dist/hasher/common.d.ts +12 -0
- package/dist/hasher/common.d.ts.map +1 -0
- package/dist/hasher/common.js +19 -0
- package/dist/hasher/common.js.map +1 -0
- package/dist/hasher/hasher.d.ts +4 -0
- package/dist/hasher/hasher.d.ts.map +1 -0
- package/dist/hasher/hasher.js +11 -0
- package/dist/hasher/hasher.js.map +1 -0
- package/dist/hasher/index.d.ts +4 -0
- package/dist/hasher/index.d.ts.map +1 -0
- package/dist/hasher/index.js +4 -0
- package/dist/hasher/index.js.map +1 -0
- package/dist/hasher/merklemulti.d.ts +58 -0
- package/dist/hasher/merklemulti.d.ts.map +1 -0
- package/dist/hasher/merklemulti.js +257 -0
- package/dist/hasher/merklemulti.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/offchain.d.ts +20 -0
- package/dist/offchain.d.ts.map +1 -0
- package/dist/offchain.js +59 -0
- package/dist/offchain.js.map +1 -0
- package/dist/requests.d.ts +48 -0
- package/dist/requests.d.ts.map +1 -0
- package/dist/requests.js +286 -0
- package/dist/requests.js.map +1 -0
- package/dist/selectors.d.ts +9 -0
- package/dist/selectors.d.ts.map +1 -0
- package/dist/selectors.js +1330 -0
- package/dist/selectors.js.map +1 -0
- package/dist/solana/cleanup.d.ts +15 -0
- package/dist/solana/cleanup.d.ts.map +1 -0
- package/dist/solana/cleanup.js +159 -0
- package/dist/solana/cleanup.js.map +1 -0
- package/dist/solana/exec.d.ts +15 -0
- package/dist/solana/exec.d.ts.map +1 -0
- package/dist/solana/exec.js +417 -0
- package/dist/solana/exec.js.map +1 -0
- package/dist/solana/hasher.d.ts +4 -0
- package/dist/solana/hasher.d.ts.map +1 -0
- package/dist/solana/hasher.js +81 -0
- package/dist/solana/hasher.js.map +1 -0
- package/dist/solana/idl/1.6.0/BASE_TOKEN_POOL.d.ts +866 -0
- package/dist/solana/idl/1.6.0/BASE_TOKEN_POOL.d.ts.map +1 -0
- package/dist/solana/idl/1.6.0/BASE_TOKEN_POOL.js +866 -0
- package/dist/solana/idl/1.6.0/BASE_TOKEN_POOL.js.map +1 -0
- package/dist/solana/idl/1.6.0/BURN_MINT_TOKEN_POOL.d.ts +949 -0
- package/dist/solana/idl/1.6.0/BURN_MINT_TOKEN_POOL.d.ts.map +1 -0
- package/dist/solana/idl/1.6.0/BURN_MINT_TOKEN_POOL.js +949 -0
- package/dist/solana/idl/1.6.0/BURN_MINT_TOKEN_POOL.js.map +1 -0
- package/dist/solana/idl/1.6.0/CCIP_CCTP_TOKEN_POOL.d.ts +1374 -0
- package/dist/solana/idl/1.6.0/CCIP_CCTP_TOKEN_POOL.d.ts.map +1 -0
- package/dist/solana/idl/1.6.0/CCIP_CCTP_TOKEN_POOL.js +1374 -0
- package/dist/solana/idl/1.6.0/CCIP_CCTP_TOKEN_POOL.js.map +1 -0
- package/dist/solana/idl/1.6.0/CCIP_COMMON.d.ts +104 -0
- package/dist/solana/idl/1.6.0/CCIP_COMMON.d.ts.map +1 -0
- package/dist/solana/idl/1.6.0/CCIP_COMMON.js +104 -0
- package/dist/solana/idl/1.6.0/CCIP_COMMON.js.map +1 -0
- package/dist/solana/idl/1.6.0/CCIP_OFFRAMP.d.ts +2746 -0
- package/dist/solana/idl/1.6.0/CCIP_OFFRAMP.d.ts.map +1 -0
- package/dist/solana/idl/1.6.0/CCIP_OFFRAMP.js +2746 -0
- package/dist/solana/idl/1.6.0/CCIP_OFFRAMP.js.map +1 -0
- package/dist/solana/idl/1.6.0/CCIP_ROUTER.d.ts +2332 -0
- package/dist/solana/idl/1.6.0/CCIP_ROUTER.d.ts.map +1 -0
- package/dist/solana/idl/1.6.0/CCIP_ROUTER.js +2332 -0
- package/dist/solana/idl/1.6.0/CCIP_ROUTER.js.map +1 -0
- package/dist/solana/index.d.ts +205 -0
- package/dist/solana/index.d.ts.map +1 -0
- package/dist/solana/index.js +1085 -0
- package/dist/solana/index.js.map +1 -0
- package/dist/solana/offchain.d.ts +31 -0
- package/dist/solana/offchain.d.ts.map +1 -0
- package/dist/solana/offchain.js +152 -0
- package/dist/solana/offchain.js.map +1 -0
- package/dist/solana/patchBorsh.d.ts +2 -0
- package/dist/solana/patchBorsh.d.ts.map +1 -0
- package/dist/solana/patchBorsh.js +60 -0
- package/dist/solana/patchBorsh.js.map +1 -0
- package/dist/solana/send.d.ts +14 -0
- package/dist/solana/send.d.ts.map +1 -0
- package/dist/solana/send.js +272 -0
- package/dist/solana/send.js.map +1 -0
- package/dist/solana/types.d.ts +4 -0
- package/dist/solana/types.d.ts.map +1 -0
- package/dist/solana/types.js +2 -0
- package/dist/solana/types.js.map +1 -0
- package/dist/solana/utils.d.ts +58 -0
- package/dist/solana/utils.d.ts.map +1 -0
- package/dist/solana/utils.js +211 -0
- package/dist/solana/utils.js.map +1 -0
- package/dist/sui/hasher.d.ts +12 -0
- package/dist/sui/hasher.d.ts.map +1 -0
- package/dist/sui/hasher.js +63 -0
- package/dist/sui/hasher.js.map +1 -0
- package/dist/sui/index.d.ts +72 -0
- package/dist/sui/index.d.ts.map +1 -0
- package/dist/sui/index.js +128 -0
- package/dist/sui/index.js.map +1 -0
- package/dist/sui/types.d.ts +17 -0
- package/dist/sui/types.d.ts.map +1 -0
- package/dist/sui/types.js +17 -0
- package/dist/sui/types.js.map +1 -0
- package/dist/supported-chains.d.ts +5 -0
- package/dist/supported-chains.d.ts.map +1 -0
- package/dist/supported-chains.js +3 -0
- package/dist/supported-chains.js.map +1 -0
- package/dist/types.d.ts +118 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +11 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +117 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +336 -0
- package/dist/utils.js.map +1 -0
- package/package.json +66 -8
- package/src/aptos/exec.ts +69 -0
- package/src/aptos/hasher.ts +92 -0
- package/src/aptos/index.ts +660 -0
- package/src/aptos/logs.ts +210 -0
- package/src/aptos/send.ts +120 -0
- package/src/aptos/token.ts +150 -0
- package/src/aptos/types.ts +85 -0
- package/src/aptos/utils.ts +24 -0
- package/src/chain.ts +398 -0
- package/src/commits.ts +44 -0
- package/src/evm/abi/BurnMintERC677Token.ts +487 -0
- package/src/evm/abi/CommitStore_1_2.ts +637 -0
- package/src/evm/abi/CommitStore_1_5.ts +674 -0
- package/src/evm/abi/FeeQuoter_1_6.ts +1903 -0
- package/src/evm/abi/LockReleaseTokenPool_1_5.ts +1095 -0
- package/src/evm/abi/LockReleaseTokenPool_1_5_1.ts +1277 -0
- package/src/evm/abi/LockReleaseTokenPool_1_6_1.ts +1287 -0
- package/src/evm/abi/OffRamp_1_2.ts +1203 -0
- package/src/evm/abi/OffRamp_1_5.ts +1272 -0
- package/src/evm/abi/OffRamp_1_6.ts +1528 -0
- package/src/evm/abi/OnRamp_1_2.ts +1342 -0
- package/src/evm/abi/OnRamp_1_5.ts +1426 -0
- package/src/evm/abi/OnRamp_1_6.ts +879 -0
- package/src/evm/abi/Router.ts +507 -0
- package/src/evm/abi/TokenAdminRegistry_1_5.ts +332 -0
- package/src/evm/const.ts +69 -0
- package/src/evm/errors.ts +212 -0
- package/src/evm/hasher.ts +166 -0
- package/src/evm/index.ts +1262 -0
- package/src/evm/messages.ts +73 -0
- package/src/evm/offchain.ts +189 -0
- package/src/execution.ts +131 -0
- package/src/extra-args.ts +71 -0
- package/src/gas.ts +135 -0
- package/src/hasher/common.ts +23 -0
- package/src/hasher/hasher.ts +12 -0
- package/src/hasher/index.ts +3 -0
- package/src/hasher/merklemulti.ts +309 -0
- package/src/index.ts +51 -0
- package/src/offchain.ts +86 -0
- package/src/requests.ts +339 -0
- package/src/selectors.ts +1340 -0
- package/src/solana/cleanup.ts +216 -0
- package/src/solana/exec.ts +645 -0
- package/src/solana/hasher.ts +104 -0
- package/src/solana/idl/1.6.0/BASE_TOKEN_POOL.ts +1734 -0
- package/src/solana/idl/1.6.0/BURN_MINT_TOKEN_POOL.ts +1900 -0
- package/src/solana/idl/1.6.0/CCIP_CCTP_TOKEN_POOL.ts +2750 -0
- package/src/solana/idl/1.6.0/CCIP_COMMON.ts +210 -0
- package/src/solana/idl/1.6.0/CCIP_OFFRAMP.ts +5494 -0
- package/src/solana/idl/1.6.0/CCIP_ROUTER.ts +4671 -0
- package/src/solana/index.ts +1454 -0
- package/src/solana/offchain.ts +209 -0
- package/src/solana/patchBorsh.ts +67 -0
- package/src/solana/send.ts +436 -0
- package/src/solana/types.ts +6 -0
- package/src/solana/utils.ts +272 -0
- package/src/sui/hasher.ts +90 -0
- package/src/sui/index.ts +198 -0
- package/src/sui/types.ts +22 -0
- package/src/supported-chains.ts +4 -0
- package/src/types.ts +153 -0
- package/src/utils.ts +405 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { type BN, BorshCoder, EventParser } from '@coral-xyz/anchor'
|
|
2
|
+
import { type Connection, PublicKey } from '@solana/web3.js'
|
|
3
|
+
import { hexlify } from 'ethers'
|
|
4
|
+
|
|
5
|
+
import { getUsdcAttestation } from '../offchain.ts'
|
|
6
|
+
import type { CCIPMessage, CCIPRequest, OffchainTokenData } from '../types.ts'
|
|
7
|
+
import { networkInfo } from '../utils.ts'
|
|
8
|
+
import { IDL as BASE_TOKEN_POOL } from './idl/1.6.0/BASE_TOKEN_POOL.ts'
|
|
9
|
+
import { IDL as CCTP_TOKEN_POOL } from './idl/1.6.0/CCIP_CCTP_TOKEN_POOL.ts'
|
|
10
|
+
import { bytesToBuffer } from './utils.ts'
|
|
11
|
+
|
|
12
|
+
interface CcipCctpMessageSentEvent {
|
|
13
|
+
originalSender: PublicKey
|
|
14
|
+
remoteChainSelector: BN
|
|
15
|
+
msgTotalNonce: BN
|
|
16
|
+
eventAddress: PublicKey
|
|
17
|
+
sourceDomain: number
|
|
18
|
+
cctpNonce: BN
|
|
19
|
+
messageSentBytes: Uint8Array
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface CcipCctpMessageAndAttestation {
|
|
23
|
+
message: {
|
|
24
|
+
data: Uint8Array
|
|
25
|
+
}
|
|
26
|
+
attestation: Uint8Array
|
|
27
|
+
}
|
|
28
|
+
const cctpTokenPoolCoder = new BorshCoder({
|
|
29
|
+
...CCTP_TOKEN_POOL,
|
|
30
|
+
types: [...BASE_TOKEN_POOL.types, ...CCTP_TOKEN_POOL.types],
|
|
31
|
+
events: BASE_TOKEN_POOL.events,
|
|
32
|
+
errors: [...BASE_TOKEN_POOL.errors, ...CCTP_TOKEN_POOL.errors],
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Analyzes a Solana transaction to extract CcipCctpMessageSentEvent, fetch Circle attestation,
|
|
37
|
+
* 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
|
+
* })
|
|
51
|
+
*/
|
|
52
|
+
export async function fetchSolanaOffchainTokenData(
|
|
53
|
+
connection: Connection,
|
|
54
|
+
request: Pick<CCIPRequest, 'tx' | 'lane'> & {
|
|
55
|
+
message: CCIPMessage
|
|
56
|
+
log: Pick<CCIPRequest['log'], 'topics' | 'index' | 'transactionHash'>
|
|
57
|
+
},
|
|
58
|
+
): Promise<OffchainTokenData[]> {
|
|
59
|
+
if (request.message.tokenAmounts === undefined || request.message.tokenAmounts.length === 0) {
|
|
60
|
+
return []
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (request.message.tokenAmounts.length > 1) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`Expected at most 1 token transfer, found ${request.message.tokenAmounts?.length}`,
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const { isTestnet } = networkInfo(request.lane.sourceChainSelector)
|
|
70
|
+
const txSignature = request.log.transactionHash
|
|
71
|
+
if (!txSignature) {
|
|
72
|
+
throw new Error('Transaction hash not found for OffchainTokenData parsing')
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Parse Solana transaction to find CCTP event
|
|
76
|
+
const cctpEvents = await parseCcipCctpEvents(connection, txSignature)
|
|
77
|
+
const offchainTokenData: OffchainTokenData[] = request.message.tokenAmounts.map(() => undefined)
|
|
78
|
+
|
|
79
|
+
// If no CcipCctpMessageSentEvent found, return defaults so we don't block execution
|
|
80
|
+
if (cctpEvents.length === 0) {
|
|
81
|
+
console.debug('No events')
|
|
82
|
+
return offchainTokenData
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Currently, we only support ONE token per transfer
|
|
86
|
+
if (cctpEvents.length > 1) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`Expected only 1 CcipCctpMessageSentEvent, found ${cctpEvents.length} in transaction ${txSignature}.`,
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// NOTE: assuming USDC token is the first (and only) token in the CCIP message, we will process the CCTP event.
|
|
93
|
+
// If later multi-token transfers support is added, we need to add more info in order to match each token with it's event and offchainTokenData.
|
|
94
|
+
const cctpEvent = cctpEvents[0]
|
|
95
|
+
if (cctpEvent) {
|
|
96
|
+
const message = hexlify(cctpEvent.messageSentBytes)
|
|
97
|
+
try {
|
|
98
|
+
// Extract message bytes to fetch circle's attestation and then encode offchainTokenData.
|
|
99
|
+
const attestation = await getUsdcAttestation(message, isTestnet)
|
|
100
|
+
|
|
101
|
+
offchainTokenData[0] = { _tag: 'usdc', message, attestation }
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.warn(
|
|
104
|
+
`❌ Solana CCTP: Failed to fetch attestation for ${txSignature}:`,
|
|
105
|
+
message,
|
|
106
|
+
error,
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
console.debug('Got Solana offchain token data', offchainTokenData)
|
|
112
|
+
|
|
113
|
+
return offchainTokenData
|
|
114
|
+
}
|
|
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
|
+
/**
|
|
187
|
+
* Encodes CCTP message and attestation
|
|
188
|
+
*
|
|
189
|
+
* @param data - OffchainTokenData (_tag="usdc")
|
|
190
|
+
* @returns Encoded data - Borsh-encoded attestation for Solana
|
|
191
|
+
*/
|
|
192
|
+
export function encodeSolanaOffchainTokenData(data: OffchainTokenData): string {
|
|
193
|
+
if (data?._tag === 'usdc') {
|
|
194
|
+
const messageBuffer = bytesToBuffer(data.message)
|
|
195
|
+
const attestationBuffer = bytesToBuffer(data.attestation)
|
|
196
|
+
|
|
197
|
+
// Solana destination: use Borsh encoding
|
|
198
|
+
const messageAndAttestation: CcipCctpMessageAndAttestation = {
|
|
199
|
+
message: {
|
|
200
|
+
data: messageBuffer, // u8 array
|
|
201
|
+
},
|
|
202
|
+
attestation: attestationBuffer, // u8 array
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const encoded = cctpTokenPoolCoder.types.encode('MessageAndAttestation', messageAndAttestation)
|
|
206
|
+
return hexlify(encoded)
|
|
207
|
+
}
|
|
208
|
+
return '0x'
|
|
209
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { BorshInstructionCoder } from '@coral-xyz/anchor'
|
|
2
|
+
import { BorshTypesCoder } from '@coral-xyz/anchor/dist/cjs/coder/borsh/types.js'
|
|
3
|
+
import { sha256, toUtf8Bytes } from 'ethers'
|
|
4
|
+
|
|
5
|
+
import { snakeToCamel } from '../utils.ts'
|
|
6
|
+
import { camelToSnakeCase } from './utils.ts'
|
|
7
|
+
|
|
8
|
+
type Layout_<T = unknown> = { encode: (type: T, buffer: Buffer) => number }
|
|
9
|
+
|
|
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
|
+
function sighash(nameSpace: string, ixName: string): Buffer {
|
|
35
|
+
const name = camelToSnakeCase(ixName)
|
|
36
|
+
const preimage = `${nameSpace}:${name}`
|
|
37
|
+
return Buffer.from(sha256(toUtf8Bytes(preimage)).slice(2, 18), 'hex')
|
|
38
|
+
}
|
|
39
|
+
|
|
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)
|
|
59
|
+
len = layout.encode(ix, buffer)
|
|
60
|
+
} else {
|
|
61
|
+
throw err
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const data = buffer.subarray(0, len)
|
|
65
|
+
return Buffer.concat([sighash(nameSpace, ixName), data])
|
|
66
|
+
},
|
|
67
|
+
})
|
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
import util from 'util'
|
|
2
|
+
|
|
3
|
+
import { type AnchorProvider, type IdlTypes, Program } from '@coral-xyz/anchor'
|
|
4
|
+
import {
|
|
5
|
+
NATIVE_MINT,
|
|
6
|
+
createApproveInstruction,
|
|
7
|
+
getAccount,
|
|
8
|
+
getAssociatedTokenAddressSync,
|
|
9
|
+
} from '@solana/spl-token'
|
|
10
|
+
import {
|
|
11
|
+
type AccountMeta,
|
|
12
|
+
type AddressLookupTableAccount,
|
|
13
|
+
type Connection,
|
|
14
|
+
type TransactionInstruction,
|
|
15
|
+
ComputeBudgetProgram,
|
|
16
|
+
PublicKey,
|
|
17
|
+
TransactionMessage,
|
|
18
|
+
VersionedTransaction,
|
|
19
|
+
} from '@solana/web3.js'
|
|
20
|
+
import BN from 'bn.js'
|
|
21
|
+
import { zeroPadValue } from 'ethers'
|
|
22
|
+
|
|
23
|
+
import { SolanaChain } from './index.ts'
|
|
24
|
+
import type { AnyMessage } from '../types.ts'
|
|
25
|
+
import { toLeArray } from '../utils.ts'
|
|
26
|
+
import { IDL as CCIP_ROUTER_IDL } from './idl/1.6.0/CCIP_ROUTER.ts'
|
|
27
|
+
import { bytesToBuffer, simulateTransaction, simulationProvider } from './utils.ts'
|
|
28
|
+
|
|
29
|
+
function anyToSvmMessage(message: AnyMessage): IdlTypes<typeof CCIP_ROUTER_IDL>['SVM2AnyMessage'] {
|
|
30
|
+
const feeTokenPubkey = message.feeToken ? new PublicKey(message.feeToken) : PublicKey.default
|
|
31
|
+
|
|
32
|
+
const svmMessage: IdlTypes<typeof CCIP_ROUTER_IDL>['SVM2AnyMessage'] = {
|
|
33
|
+
receiver: bytesToBuffer(zeroPadValue(message.receiver, 32)),
|
|
34
|
+
data: bytesToBuffer(message.data || '0x'),
|
|
35
|
+
tokenAmounts: (message.tokenAmounts || []).map((ta) => {
|
|
36
|
+
if (!ta.token || ta.amount < 0n) {
|
|
37
|
+
throw new Error('Invalid token amount: token address and positive amount required')
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
token: new PublicKey(ta.token),
|
|
41
|
+
amount: new BN(ta.amount),
|
|
42
|
+
}
|
|
43
|
+
}),
|
|
44
|
+
feeToken: feeTokenPubkey,
|
|
45
|
+
extraArgs: bytesToBuffer(SolanaChain.encodeExtraArgs(message.extraArgs)),
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return svmMessage
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function getFee(
|
|
52
|
+
connection: Connection,
|
|
53
|
+
router: string,
|
|
54
|
+
destChainSelector: bigint,
|
|
55
|
+
message: AnyMessage,
|
|
56
|
+
): Promise<bigint> {
|
|
57
|
+
const program = new Program(
|
|
58
|
+
CCIP_ROUTER_IDL,
|
|
59
|
+
new PublicKey(router),
|
|
60
|
+
simulationProvider(connection),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
// Get router config to find feeQuoter
|
|
64
|
+
const [configPda] = PublicKey.findProgramAddressSync([Buffer.from('config')], program.programId)
|
|
65
|
+
const configAccount = await connection.getAccountInfo(configPda)
|
|
66
|
+
if (!configAccount) throw new Error(`Router config not found at ${configPda.toBase58()}`)
|
|
67
|
+
|
|
68
|
+
const { feeQuoter, linkTokenMint }: { feeQuoter: PublicKey; linkTokenMint: PublicKey } =
|
|
69
|
+
program.coder.accounts.decode('config', configAccount.data)
|
|
70
|
+
|
|
71
|
+
// Derive fee-related PDAs
|
|
72
|
+
const [destChainStatePda] = PublicKey.findProgramAddressSync(
|
|
73
|
+
[Buffer.from('dest_chain_state'), toLeArray(destChainSelector, 8)],
|
|
74
|
+
program.programId,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
const [feeQuoterConfigPda] = PublicKey.findProgramAddressSync([Buffer.from('config')], feeQuoter)
|
|
78
|
+
|
|
79
|
+
const [feeQuoterDestChainPda] = PublicKey.findProgramAddressSync(
|
|
80
|
+
[Buffer.from('dest_chain'), toLeArray(destChainSelector, 8)],
|
|
81
|
+
feeQuoter,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
if (
|
|
85
|
+
message.feeToken &&
|
|
86
|
+
message.feeToken !== PublicKey.default.toBase58() &&
|
|
87
|
+
message.feeToken !== linkTokenMint.toBase58()
|
|
88
|
+
) {
|
|
89
|
+
console.warn('feeToken is not default nor link =', linkTokenMint.toBase58())
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Convert feeToken to PublicKey (default to native SOL if not specified)
|
|
93
|
+
const feeTokenPubkey =
|
|
94
|
+
message.feeToken && message.feeToken !== PublicKey.default.toBase58()
|
|
95
|
+
? new PublicKey(message.feeToken)
|
|
96
|
+
: NATIVE_MINT
|
|
97
|
+
|
|
98
|
+
const [feeQuoterBillingTokenConfigPda] = PublicKey.findProgramAddressSync(
|
|
99
|
+
[Buffer.from('fee_billing_token_config'), feeTokenPubkey.toBuffer()],
|
|
100
|
+
feeQuoter,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
// LINK token config (assuming default LINK token for now)
|
|
104
|
+
const [feeQuoterLinkTokenConfigPda] = PublicKey.findProgramAddressSync(
|
|
105
|
+
[Buffer.from('fee_billing_token_config'), linkTokenMint.toBuffer()],
|
|
106
|
+
feeQuoter,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
// Convert AnyMessage to SVM2AnyMessage format
|
|
110
|
+
const svmMessage = anyToSvmMessage(message)
|
|
111
|
+
|
|
112
|
+
// 2 feeQuoter PDAs per token
|
|
113
|
+
const remainingAccounts = svmMessage.tokenAmounts
|
|
114
|
+
.map((ta) => [
|
|
115
|
+
PublicKey.findProgramAddressSync(
|
|
116
|
+
[Buffer.from('fee_billing_token_config'), ta.token.toBuffer()],
|
|
117
|
+
feeQuoter,
|
|
118
|
+
)[0],
|
|
119
|
+
PublicKey.findProgramAddressSync(
|
|
120
|
+
[
|
|
121
|
+
Buffer.from('per_chain_per_token_config'),
|
|
122
|
+
toLeArray(destChainSelector, 8),
|
|
123
|
+
ta.token.toBuffer(),
|
|
124
|
+
],
|
|
125
|
+
feeQuoter,
|
|
126
|
+
)[0],
|
|
127
|
+
])
|
|
128
|
+
.flat()
|
|
129
|
+
.map((pubkey) => ({ pubkey, isWritable: false, isSigner: false }))
|
|
130
|
+
|
|
131
|
+
// Call getFee method
|
|
132
|
+
const result: unknown = await program.methods
|
|
133
|
+
.getFee(new BN(destChainSelector), svmMessage)
|
|
134
|
+
.accounts({
|
|
135
|
+
config: configPda,
|
|
136
|
+
destChainState: destChainStatePda,
|
|
137
|
+
feeQuoter: feeQuoter,
|
|
138
|
+
feeQuoterConfig: feeQuoterConfigPda,
|
|
139
|
+
feeQuoterDestChain: feeQuoterDestChainPda,
|
|
140
|
+
feeQuoterBillingTokenConfig: feeQuoterBillingTokenConfigPda,
|
|
141
|
+
feeQuoterLinkTokenConfig: feeQuoterLinkTokenConfigPda,
|
|
142
|
+
})
|
|
143
|
+
.remainingAccounts(remainingAccounts)
|
|
144
|
+
.view()
|
|
145
|
+
|
|
146
|
+
if (!(result as { amount?: BN })?.amount) {
|
|
147
|
+
throw new Error(`Invalid fee result from router: ${util.inspect(result)}`)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return BigInt((result as { amount: BN }).amount.toString())
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function deriveAccountsCcipSend({
|
|
154
|
+
router,
|
|
155
|
+
destChainSelector,
|
|
156
|
+
message,
|
|
157
|
+
sender,
|
|
158
|
+
}: {
|
|
159
|
+
router: Program<typeof CCIP_ROUTER_IDL>
|
|
160
|
+
destChainSelector: bigint
|
|
161
|
+
message: IdlTypes<typeof CCIP_ROUTER_IDL>['SVM2AnyMessage']
|
|
162
|
+
sender: PublicKey
|
|
163
|
+
}) {
|
|
164
|
+
const connection = router.provider.connection
|
|
165
|
+
const derivedAccounts: AccountMeta[] = []
|
|
166
|
+
const addressLookupTableAccounts: AddressLookupTableAccount[] = []
|
|
167
|
+
const tokenIndices: number[] = []
|
|
168
|
+
let askWith: AccountMeta[] = []
|
|
169
|
+
let stage = 'Start'
|
|
170
|
+
let tokenIndex = 0
|
|
171
|
+
|
|
172
|
+
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
|
+
do {
|
|
181
|
+
// Create the transaction instruction for the deriveAccountsCcipSend method
|
|
182
|
+
const response = (await roProgram.methods
|
|
183
|
+
.deriveAccountsCcipSend(
|
|
184
|
+
{
|
|
185
|
+
destChainSelector: new BN(destChainSelector.toString()),
|
|
186
|
+
ccipSendCaller: sender,
|
|
187
|
+
message,
|
|
188
|
+
},
|
|
189
|
+
stage,
|
|
190
|
+
)
|
|
191
|
+
.accounts({
|
|
192
|
+
config: configPDA,
|
|
193
|
+
})
|
|
194
|
+
.remainingAccounts(askWith)
|
|
195
|
+
.view()) as IdlTypes<typeof CCIP_ROUTER_IDL>['DeriveAccountsResponse']
|
|
196
|
+
|
|
197
|
+
// Check if it is the start of a token transfer
|
|
198
|
+
const isStartOfToken = /^TokenTransferStaticAccounts\/\d+\/0$/.test(response.currentStage)
|
|
199
|
+
if (isStartOfToken) {
|
|
200
|
+
// From CCIP_ROUTER IDL, ccipSend has 18 static accounts before remaining_accounts
|
|
201
|
+
const numStaticCcipSendAccounts = 18
|
|
202
|
+
tokenIndices.push(tokenIndex - numStaticCcipSendAccounts)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Update token index
|
|
206
|
+
tokenIndex += response.accountsToSave.length
|
|
207
|
+
|
|
208
|
+
// Collect the derived accounts
|
|
209
|
+
for (const meta of response.accountsToSave) {
|
|
210
|
+
derivedAccounts.push({
|
|
211
|
+
pubkey: meta.pubkey,
|
|
212
|
+
isWritable: meta.isWritable,
|
|
213
|
+
isSigner: meta.isSigner,
|
|
214
|
+
})
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Prepare askWith for next iteration
|
|
218
|
+
askWith = response.askAgainWith.map(
|
|
219
|
+
(meta: IdlTypes<typeof CCIP_ROUTER_IDL>['CcipAccountMeta']) => ({
|
|
220
|
+
pubkey: meta.pubkey,
|
|
221
|
+
isWritable: meta.isWritable,
|
|
222
|
+
isSigner: meta.isSigner,
|
|
223
|
+
}),
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
const lookupTableAccounts = await Promise.all(
|
|
227
|
+
response.lookUpTablesToSave.map(async (table) => {
|
|
228
|
+
const lookupTableAccountInfo = await connection.getAddressLookupTable(table)
|
|
229
|
+
|
|
230
|
+
if (!lookupTableAccountInfo.value) {
|
|
231
|
+
throw new Error(`Lookup table account not found: ${table.toBase58()}`)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return lookupTableAccountInfo.value
|
|
235
|
+
}),
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
// Collect lookup tables
|
|
239
|
+
addressLookupTableAccounts.push(...lookupTableAccounts)
|
|
240
|
+
|
|
241
|
+
stage = response.nextStage
|
|
242
|
+
} while (stage?.length)
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
accounts: derivedAccounts,
|
|
246
|
+
addressLookupTableAccounts,
|
|
247
|
+
tokenIndexes: Buffer.from(tokenIndices),
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
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>,
|
|
298
|
+
destChainSelector: bigint,
|
|
299
|
+
message: AnyMessage & { fee: bigint },
|
|
300
|
+
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
|
+
|
|
308
|
+
const amountsToApprove = (message.tokenAmounts ?? []).reduce(
|
|
309
|
+
(acc, { token, amount }) => ({ ...acc, [token]: (acc[token] ?? 0n) + amount }),
|
|
310
|
+
{} as Record<string, bigint>,
|
|
311
|
+
)
|
|
312
|
+
if (message.feeToken && message.feeToken !== PublicKey.default.toBase58()) {
|
|
313
|
+
amountsToApprove[message.feeToken] = (amountsToApprove[message.feeToken] ?? 0n) + message.fee
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const approveIxs = []
|
|
317
|
+
for (const [token, amount] of Object.entries(amountsToApprove)) {
|
|
318
|
+
const approveIx = await approveRouterSpender(
|
|
319
|
+
connection,
|
|
320
|
+
wallet.publicKey,
|
|
321
|
+
new PublicKey(token),
|
|
322
|
+
router.programId,
|
|
323
|
+
opts?.approveMax ? undefined : amount,
|
|
324
|
+
)
|
|
325
|
+
if (approveIx) approveIxs.push(approveIx)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const svmMessage = anyToSvmMessage(message)
|
|
329
|
+
const { addressLookupTableAccounts, accounts, tokenIndexes } = await deriveAccountsCcipSend({
|
|
330
|
+
router,
|
|
331
|
+
destChainSelector,
|
|
332
|
+
sender: wallet.publicKey,
|
|
333
|
+
message: svmMessage,
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
const sendIx = await router.methods
|
|
337
|
+
.ccipSend(new BN(destChainSelector), svmMessage, tokenIndexes)
|
|
338
|
+
.accountsStrict({
|
|
339
|
+
config: accounts[0].pubkey,
|
|
340
|
+
destChainState: accounts[1].pubkey,
|
|
341
|
+
nonce: accounts[2].pubkey,
|
|
342
|
+
authority: accounts[3].pubkey,
|
|
343
|
+
systemProgram: accounts[4].pubkey,
|
|
344
|
+
feeTokenProgram: accounts[5].pubkey,
|
|
345
|
+
feeTokenMint: accounts[6].pubkey,
|
|
346
|
+
feeTokenUserAssociatedAccount: accounts[7].pubkey,
|
|
347
|
+
feeTokenReceiver: accounts[8].pubkey,
|
|
348
|
+
feeBillingSigner: accounts[9].pubkey,
|
|
349
|
+
feeQuoter: accounts[10].pubkey,
|
|
350
|
+
feeQuoterConfig: accounts[11].pubkey,
|
|
351
|
+
feeQuoterDestChain: accounts[12].pubkey,
|
|
352
|
+
feeQuoterBillingTokenConfig: accounts[13].pubkey,
|
|
353
|
+
feeQuoterLinkTokenConfig: accounts[14].pubkey,
|
|
354
|
+
rmnRemote: accounts[15].pubkey,
|
|
355
|
+
rmnRemoteCurses: accounts[16].pubkey,
|
|
356
|
+
rmnRemoteConfig: accounts[17].pubkey,
|
|
357
|
+
})
|
|
358
|
+
.remainingAccounts(accounts.slice(18))
|
|
359
|
+
.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)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return { hash }
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async function approveRouterSpender(
|
|
386
|
+
connection: Connection,
|
|
387
|
+
owner: PublicKey,
|
|
388
|
+
token: PublicKey,
|
|
389
|
+
router: PublicKey,
|
|
390
|
+
amount?: bigint,
|
|
391
|
+
): Promise<TransactionInstruction | undefined> {
|
|
392
|
+
// Get the current account info to check existing delegation (or create if needed)
|
|
393
|
+
const mintInfo = await connection.getAccountInfo(token)
|
|
394
|
+
if (!mintInfo) throw new Error(`Mint ${token.toBase58()} not found`)
|
|
395
|
+
const associatedTokenAccount = getAssociatedTokenAddressSync(
|
|
396
|
+
token,
|
|
397
|
+
owner,
|
|
398
|
+
undefined,
|
|
399
|
+
mintInfo.owner,
|
|
400
|
+
)
|
|
401
|
+
const accountInfo = await getAccount(
|
|
402
|
+
connection,
|
|
403
|
+
associatedTokenAccount,
|
|
404
|
+
undefined,
|
|
405
|
+
mintInfo.owner,
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
// spender is a Router PDA
|
|
409
|
+
const [spender] = PublicKey.findProgramAddressSync([Buffer.from('fee_billing_signer')], router)
|
|
410
|
+
|
|
411
|
+
// Check if we need to approve
|
|
412
|
+
const needsApproval =
|
|
413
|
+
!accountInfo.delegate ||
|
|
414
|
+
!accountInfo.delegate.equals(spender) ||
|
|
415
|
+
(amount != null && accountInfo.delegatedAmount < amount)
|
|
416
|
+
|
|
417
|
+
if (!needsApproval) return
|
|
418
|
+
// Approve the spender to use tokens from the user's account
|
|
419
|
+
const approveIx = createApproveInstruction(
|
|
420
|
+
accountInfo.address,
|
|
421
|
+
spender,
|
|
422
|
+
owner,
|
|
423
|
+
amount ?? BigInt(Number.MAX_SAFE_INTEGER),
|
|
424
|
+
undefined,
|
|
425
|
+
mintInfo.owner,
|
|
426
|
+
)
|
|
427
|
+
console.info(
|
|
428
|
+
'Approving',
|
|
429
|
+
amount ?? BigInt(Number.MAX_SAFE_INTEGER),
|
|
430
|
+
'of',
|
|
431
|
+
token.toBase58(),
|
|
432
|
+
'tokens to router',
|
|
433
|
+
router.toBase58(),
|
|
434
|
+
)
|
|
435
|
+
return approveIx
|
|
436
|
+
}
|