@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,1085 @@
|
|
|
1
|
+
import util from 'util';
|
|
2
|
+
import { AnchorProvider, BorshAccountsCoder, BorshCoder, Program, Wallet, eventDiscriminator, } from '@coral-xyz/anchor';
|
|
3
|
+
import { NATIVE_MINT } from '@solana/spl-token';
|
|
4
|
+
import { Connection, Keypair, PublicKey, SYSVAR_CLOCK_PUBKEY, SystemProgram, } from '@solana/web3.js';
|
|
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";
|
|
9
|
+
import { EVMExtraArgsV2Tag } from "../extra-args.js";
|
|
10
|
+
import SELECTORS from "../selectors.js";
|
|
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";
|
|
14
|
+
import { cleanUpBuffers } from "./cleanup.js";
|
|
15
|
+
import { executeReport } from "./exec.js";
|
|
16
|
+
import { getV16SolanaLeafHasher } from "./hasher.js";
|
|
17
|
+
import { IDL as BASE_TOKEN_POOL } from "./idl/1.6.0/BASE_TOKEN_POOL.js";
|
|
18
|
+
import { IDL as BURN_MINT_TOKEN_POOL } from "./idl/1.6.0/BURN_MINT_TOKEN_POOL.js";
|
|
19
|
+
import { IDL as CCIP_CCTP_TOKEN_POOL } from "./idl/1.6.0/CCIP_CCTP_TOKEN_POOL.js";
|
|
20
|
+
import { IDL as CCIP_OFFRAMP_IDL } from "./idl/1.6.0/CCIP_OFFRAMP.js";
|
|
21
|
+
import { IDL as CCIP_ROUTER_IDL } from "./idl/1.6.0/CCIP_ROUTER.js";
|
|
22
|
+
import { fetchSolanaOffchainTokenData } from "./offchain.js";
|
|
23
|
+
import { ccipSend, getFee } from "./send.js";
|
|
24
|
+
import { bytesToBuffer, getErrorFromLogs, parseSolanaLogs, simulationProvider } from "./utils.js";
|
|
25
|
+
const routerCoder = new BorshCoder(CCIP_ROUTER_IDL);
|
|
26
|
+
const offrampCoder = new BorshCoder(CCIP_OFFRAMP_IDL);
|
|
27
|
+
const tokenPoolCoder = new BorshCoder({
|
|
28
|
+
...BURN_MINT_TOKEN_POOL,
|
|
29
|
+
types: BASE_TOKEN_POOL.types,
|
|
30
|
+
events: BASE_TOKEN_POOL.events,
|
|
31
|
+
errors: [...BASE_TOKEN_POOL.errors, ...BURN_MINT_TOKEN_POOL.errors],
|
|
32
|
+
});
|
|
33
|
+
const cctpTokenPoolCoder = new BorshCoder({
|
|
34
|
+
...CCIP_CCTP_TOKEN_POOL,
|
|
35
|
+
types: [...BASE_TOKEN_POOL.types, ...CCIP_CCTP_TOKEN_POOL.types],
|
|
36
|
+
events: [...BASE_TOKEN_POOL.events, ...CCIP_CCTP_TOKEN_POOL.events],
|
|
37
|
+
errors: [...BASE_TOKEN_POOL.errors, ...CCIP_CCTP_TOKEN_POOL.errors],
|
|
38
|
+
});
|
|
39
|
+
// hardcoded symbols for tokens without metadata
|
|
40
|
+
const unknownTokens = {
|
|
41
|
+
'4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU': 'USDC', // devnet
|
|
42
|
+
};
|
|
43
|
+
function hexDiscriminator(eventName) {
|
|
44
|
+
return hexlify(eventDiscriminator(eventName));
|
|
45
|
+
}
|
|
46
|
+
export class SolanaChain extends Chain {
|
|
47
|
+
static family = ChainFamily.Solana;
|
|
48
|
+
static decimals = 9;
|
|
49
|
+
network;
|
|
50
|
+
connection;
|
|
51
|
+
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;
|
|
59
|
+
this.connection = connection;
|
|
60
|
+
// Memoize expensive operations
|
|
61
|
+
this.typeAndVersion = moize.default(this.typeAndVersion.bind(this), {
|
|
62
|
+
maxArgs: 1,
|
|
63
|
+
isPromise: true,
|
|
64
|
+
});
|
|
65
|
+
this.getBlockTimestamp = moize.default(this.getBlockTimestamp.bind(this), {
|
|
66
|
+
isPromise: true,
|
|
67
|
+
maxSize: 100,
|
|
68
|
+
updateCacheForKey: (key) => typeof key[key.length - 1] !== 'number',
|
|
69
|
+
});
|
|
70
|
+
this.getTransaction = moize.default(this.getTransaction.bind(this), {
|
|
71
|
+
maxSize: 100,
|
|
72
|
+
maxArgs: 1,
|
|
73
|
+
});
|
|
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'), {
|
|
78
|
+
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,
|
|
85
|
+
});
|
|
86
|
+
// cache account info for 30 seconds
|
|
87
|
+
this.connection.getAccountInfo = moize.default(this.connection.getAccountInfo.bind(this.connection), {
|
|
88
|
+
maxSize: 100,
|
|
89
|
+
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,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
static _getConnection(url) {
|
|
101
|
+
if (!url.startsWith('http') && !url.startsWith('ws')) {
|
|
102
|
+
throw new Error(`Invalid Solana RPC URL format (should be https://, http://, wss://, or ws://): ${url}`);
|
|
103
|
+
}
|
|
104
|
+
const config = { commitment: 'confirmed' };
|
|
105
|
+
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');
|
|
112
|
+
}
|
|
113
|
+
return new Connection(url, config);
|
|
114
|
+
}
|
|
115
|
+
static async fromConnection(connection) {
|
|
116
|
+
// 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');
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
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
|
|
136
|
+
*/
|
|
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();
|
|
149
|
+
}
|
|
150
|
+
// cached
|
|
151
|
+
async getBlockTimestamp(block) {
|
|
152
|
+
if (block === 'finalized') {
|
|
153
|
+
const slot = await this.connection.getSlot('finalized');
|
|
154
|
+
const blockTime = await this.connection.getBlockTime(slot);
|
|
155
|
+
if (blockTime === null) {
|
|
156
|
+
throw new Error(`Could not get block time for finalized slot ${slot}`);
|
|
157
|
+
}
|
|
158
|
+
return blockTime;
|
|
159
|
+
}
|
|
160
|
+
const blockTime = await this.connection.getBlockTime(block);
|
|
161
|
+
if (blockTime === null) {
|
|
162
|
+
throw new Error(`Could not get block time for slot ${block}`);
|
|
163
|
+
}
|
|
164
|
+
return blockTime;
|
|
165
|
+
}
|
|
166
|
+
// cached
|
|
167
|
+
async getTransaction(hash) {
|
|
168
|
+
const tx = await this.connection.getTransaction(hash, {
|
|
169
|
+
commitment: 'confirmed',
|
|
170
|
+
maxSupportedTransactionVersion: 0,
|
|
171
|
+
});
|
|
172
|
+
if (!tx)
|
|
173
|
+
throw new Error(`Transaction not found: ${hash}`);
|
|
174
|
+
if (tx.blockTime) {
|
|
175
|
+
;
|
|
176
|
+
this.getBlockTimestamp.set([tx.slot], Promise.resolve(tx.blockTime));
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
tx.blockTime = await this.getBlockTimestamp(tx.slot);
|
|
180
|
+
}
|
|
181
|
+
// Parse logs from transaction using helper function
|
|
182
|
+
const logs_ = tx.meta?.logMessages?.length
|
|
183
|
+
? parseSolanaLogs(tx.meta?.logMessages).map((l) => ({
|
|
184
|
+
...l,
|
|
185
|
+
transactionHash: hash,
|
|
186
|
+
blockNumber: tx.slot,
|
|
187
|
+
}))
|
|
188
|
+
: [];
|
|
189
|
+
const chainTx = {
|
|
190
|
+
chain: this,
|
|
191
|
+
hash,
|
|
192
|
+
logs: [],
|
|
193
|
+
blockNumber: tx.slot,
|
|
194
|
+
timestamp: tx.blockTime,
|
|
195
|
+
from: tx.transaction.message.staticAccountKeys[0].toString(),
|
|
196
|
+
error: tx.meta?.err,
|
|
197
|
+
tx, // specialized solana transaction
|
|
198
|
+
};
|
|
199
|
+
// solana logs include circular reference to tx
|
|
200
|
+
chainTx.logs = logs_.map((l) => Object.assign(l, { tx: chainTx }));
|
|
201
|
+
return chainTx;
|
|
202
|
+
}
|
|
203
|
+
// implements inner paging logic for this.getLogs
|
|
204
|
+
async *_getTransactionsForAddress(opts) {
|
|
205
|
+
if (!opts.address)
|
|
206
|
+
throw new Error('Program address is required for Solana log filtering');
|
|
207
|
+
let allSignatures;
|
|
208
|
+
if (opts.startBlock || opts.startTime) {
|
|
209
|
+
// 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);
|
|
214
|
+
while (batch.length > 0 &&
|
|
215
|
+
(batch[batch.length - 1].slot < (opts.startBlock || 0) ||
|
|
216
|
+
(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;
|
|
219
|
+
}
|
|
220
|
+
allSigs.push(...batch);
|
|
221
|
+
}
|
|
222
|
+
allSigs.reverse();
|
|
223
|
+
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
|
|
227
|
+
}
|
|
228
|
+
allSignatures = allSigs;
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
allSignatures = async function* () {
|
|
232
|
+
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);
|
|
239
|
+
for (const sig of batch) {
|
|
240
|
+
if (opts.endBlock && sig.slot > opts.endBlock)
|
|
241
|
+
continue;
|
|
242
|
+
yield sig;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}.call(this); // generate backwards until depleting getSignaturesForAddress
|
|
246
|
+
}
|
|
247
|
+
// Process signatures
|
|
248
|
+
for await (const signatureInfo of allSignatures) {
|
|
249
|
+
yield await this.getTransaction(signatureInfo.signature);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Retrieves logs from Solana transactions with enhanced chronological ordering.
|
|
254
|
+
*
|
|
255
|
+
* Behavior:
|
|
256
|
+
* - If opts.startBlock or opts.startTime is provided:
|
|
257
|
+
* * Fetches ALL signatures for the address going back in time
|
|
258
|
+
* * Continues fetching until finding signatures older than the start target
|
|
259
|
+
* * Filters out signatures older than start criteria
|
|
260
|
+
* * Returns logs in chronological order (oldest first)
|
|
261
|
+
*
|
|
262
|
+
* - If opts.startBlock and opts.startTime are omitted:
|
|
263
|
+
* * Fetches signatures in reverse chronological order (newest first)
|
|
264
|
+
* * Returns logs in reverse chronological order (newest first)
|
|
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
|
|
277
|
+
*/
|
|
278
|
+
async *getLogs(opts) {
|
|
279
|
+
let programs;
|
|
280
|
+
if (opts.sender && !opts.address) {
|
|
281
|
+
// specialization for fetching txs/requests for a given account of interest without a programID
|
|
282
|
+
opts.address = opts.sender;
|
|
283
|
+
programs = true;
|
|
284
|
+
}
|
|
285
|
+
else if (!opts.address) {
|
|
286
|
+
throw new Error('Program address is required for Solana log filtering');
|
|
287
|
+
}
|
|
288
|
+
else if (!opts.programs) {
|
|
289
|
+
programs = [opts.address];
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
programs = opts.programs;
|
|
293
|
+
}
|
|
294
|
+
if (opts.topics?.length) {
|
|
295
|
+
if (!opts.topics.every((topic) => typeof topic === 'string'))
|
|
296
|
+
throw new Error('Topics must be strings');
|
|
297
|
+
// append events discriminants (if not 0x-8B already), but keep OG topics
|
|
298
|
+
opts.topics.push(...opts.topics.filter((t) => !isHexString(t, 8)).map((t) => hexDiscriminator(t)));
|
|
299
|
+
}
|
|
300
|
+
// Process signatures and yield logs
|
|
301
|
+
for await (const tx of this._getTransactionsForAddress(opts)) {
|
|
302
|
+
for (const log of tx.logs) {
|
|
303
|
+
// Filter and yield logs from the specified program, and which match event discriminant or log prefix
|
|
304
|
+
if ((programs !== true && !programs.includes(log.address)) ||
|
|
305
|
+
(opts.topics?.length &&
|
|
306
|
+
!opts.topics.some((t) => t === log.topics[0] || (typeof log.data === 'string' && log.data.startsWith(t)))))
|
|
307
|
+
continue;
|
|
308
|
+
yield Object.assign(log, { timestamp: new Date(tx.timestamp * 1000) });
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
async typeAndVersion(address) {
|
|
313
|
+
const program = new Program(CCIP_OFFRAMP_IDL, // `typeVersion` schema should be the same
|
|
314
|
+
new PublicKey(address), simulationProvider(this.connection));
|
|
315
|
+
// Create the typeVersion instruction
|
|
316
|
+
const returnDataString = (await program.methods
|
|
317
|
+
.typeVersion()
|
|
318
|
+
.accounts({ clock: SYSVAR_CLOCK_PUBKEY })
|
|
319
|
+
.view());
|
|
320
|
+
const res = parseTypeAndVersion(returnDataString.trim());
|
|
321
|
+
if (res[1].startsWith('0.1.'))
|
|
322
|
+
res[1] = CCIPVersion.V1_6;
|
|
323
|
+
return res;
|
|
324
|
+
}
|
|
325
|
+
getRouterForOnRamp(onRamp, _destChainSelector) {
|
|
326
|
+
return Promise.resolve(onRamp); // Solana's router is also the onRamp
|
|
327
|
+
}
|
|
328
|
+
async getRouterForOffRamp(offRamp, _sourceChainSelector) {
|
|
329
|
+
const offRamp_ = new PublicKey(offRamp);
|
|
330
|
+
const program = new Program(CCIP_OFFRAMP_IDL, offRamp_, {
|
|
331
|
+
connection: this.connection,
|
|
332
|
+
});
|
|
333
|
+
const [referenceAddressesAddr] = PublicKey.findProgramAddressSync([Buffer.from('reference_addresses')], offRamp_);
|
|
334
|
+
const referenceAddressesPda = await this.connection.getAccountInfo(referenceAddressesAddr);
|
|
335
|
+
if (!referenceAddressesPda)
|
|
336
|
+
throw new Error(`referenceAddresses account not found for offRamp=${offRamp}`);
|
|
337
|
+
// Decode the config account using the program's coder
|
|
338
|
+
const { router } = program.coder.accounts.decode('referenceAddresses', referenceAddressesPda.data);
|
|
339
|
+
return router.toBase58();
|
|
340
|
+
}
|
|
341
|
+
getNativeTokenForRouter(_router) {
|
|
342
|
+
return Promise.resolve(NATIVE_MINT.toBase58());
|
|
343
|
+
}
|
|
344
|
+
async getOffRampsForRouter(router, sourceChainSelector) {
|
|
345
|
+
// feeQuoter is present in router's config, and has a DestChainState account which is updated by
|
|
346
|
+
// the offramps, so we can use it to narrow the search for the offramp
|
|
347
|
+
const { feeQuoter } = await this._getRouterConfig(router);
|
|
348
|
+
const [feeQuoterDestChainStateAccountAddress] = PublicKey.findProgramAddressSync([Buffer.from('dest_chain'), toLeArray(sourceChainSelector, 8)], feeQuoter);
|
|
349
|
+
for await (const log of this.getLogs({
|
|
350
|
+
programs: true,
|
|
351
|
+
address: feeQuoterDestChainStateAccountAddress.toBase58(),
|
|
352
|
+
topics: ['ExecutionStateChanged', 'CommitReportAccepted', 'Transmitted'],
|
|
353
|
+
})) {
|
|
354
|
+
return [log.address]; // assume single offramp per router/deployment on Solana
|
|
355
|
+
}
|
|
356
|
+
throw new Error(`Could not find OffRamp events in feeQuoter=${feeQuoter.toString()} txs`);
|
|
357
|
+
}
|
|
358
|
+
getOnRampForRouter(router, _destChainSelector) {
|
|
359
|
+
return Promise.resolve(router); // solana's Router is also the OnRamp
|
|
360
|
+
}
|
|
361
|
+
async getOnRampForOffRamp(offRamp, sourceChainSelector) {
|
|
362
|
+
const program = new Program(CCIP_OFFRAMP_IDL, new PublicKey(offRamp), {
|
|
363
|
+
connection: this.connection,
|
|
364
|
+
});
|
|
365
|
+
const [statePda] = PublicKey.findProgramAddressSync([Buffer.from('source_chain_state'), toLeArray(sourceChainSelector, 8)], program.programId);
|
|
366
|
+
// Decode the config account using the program's coder
|
|
367
|
+
const { config: { onRamp }, } = await program.account.sourceChain.fetch(statePda);
|
|
368
|
+
return decodeAddress(new Uint8Array(onRamp.bytes.slice(0, onRamp.len)), networkInfo(sourceChainSelector).family);
|
|
369
|
+
}
|
|
370
|
+
getCommitStoreForOffRamp(offRamp) {
|
|
371
|
+
return Promise.resolve(offRamp); // Solana supports only CCIP>=1.6, for which OffRamp and CommitStore are the same
|
|
372
|
+
}
|
|
373
|
+
async getTokenForTokenPool(tokenPool) {
|
|
374
|
+
const tokenPoolInfo = await this.connection.getAccountInfo(new PublicKey(tokenPool));
|
|
375
|
+
if (!tokenPoolInfo)
|
|
376
|
+
throw new Error(`TokenPool info not found: ${tokenPool}`);
|
|
377
|
+
const { config } = tokenPoolCoder.accounts.decode('state', tokenPoolInfo.data);
|
|
378
|
+
return config.mint.toString();
|
|
379
|
+
}
|
|
380
|
+
async getTokenInfo(token) {
|
|
381
|
+
const mint = new PublicKey(token);
|
|
382
|
+
const mintInfo = await this.connection.getParsedAccountInfo(mint);
|
|
383
|
+
if (!mintInfo.value ||
|
|
384
|
+
!mintInfo.value.data ||
|
|
385
|
+
(typeof mintInfo.value.data === 'object' &&
|
|
386
|
+
'program' in mintInfo.value.data &&
|
|
387
|
+
mintInfo.value.data.program !== 'spl-token' &&
|
|
388
|
+
mintInfo.value.data.program !== 'spl-token-2022')) {
|
|
389
|
+
throw new Error(`Invalid SPL token or Token-2022: ${token}`);
|
|
390
|
+
}
|
|
391
|
+
if (typeof mintInfo.value.data === 'object' && 'parsed' in mintInfo.value.data) {
|
|
392
|
+
const parsed = mintInfo.value.data.parsed;
|
|
393
|
+
const data = parsed.info;
|
|
394
|
+
let symbol = data.symbol || unknownTokens[token] || 'UNKNOWN';
|
|
395
|
+
let name = data.name;
|
|
396
|
+
// If symbol or name is missing, try to fetch from Metaplex metadata
|
|
397
|
+
if (!data.symbol || symbol === 'UNKNOWN' || !data.name) {
|
|
398
|
+
try {
|
|
399
|
+
const metadata = await this._fetchTokenMetadata(mint);
|
|
400
|
+
if (metadata) {
|
|
401
|
+
if (metadata.symbol && (!data.symbol || symbol === 'UNKNOWN')) {
|
|
402
|
+
symbol = metadata.symbol;
|
|
403
|
+
}
|
|
404
|
+
if (metadata.name && !name) {
|
|
405
|
+
name = metadata.name;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
catch (error) {
|
|
410
|
+
// Metaplex metadata fetch failed, keep the default values
|
|
411
|
+
console.debug(`Failed to fetch Metaplex metadata for token ${token}:`, error);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return {
|
|
415
|
+
name,
|
|
416
|
+
symbol,
|
|
417
|
+
decimals: data.decimals,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
throw new Error(`Unable to parse token data for ${token}`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
async _fetchTokenMetadata(mintPublicKey) {
|
|
425
|
+
try {
|
|
426
|
+
// Token Metadata Program ID
|
|
427
|
+
const TOKEN_METADATA_PROGRAM_ID = new PublicKey('metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s');
|
|
428
|
+
// Derive metadata account address
|
|
429
|
+
const [metadataPDA] = PublicKey.findProgramAddressSync([Buffer.from('metadata'), TOKEN_METADATA_PROGRAM_ID.toBuffer(), mintPublicKey.toBuffer()], TOKEN_METADATA_PROGRAM_ID);
|
|
430
|
+
// Fetch metadata account
|
|
431
|
+
const metadataAccount = await this.connection.getAccountInfo(metadataPDA);
|
|
432
|
+
if (!metadataAccount) {
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
// Parse Metaplex Token Metadata according to the actual format
|
|
436
|
+
// Reference: https://docs.metaplex.com/programs/token-metadata/accounts#metadata
|
|
437
|
+
const data = metadataAccount.data;
|
|
438
|
+
if (data.length < 100) {
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
let offset = 0;
|
|
442
|
+
// Skip key (1 byte) - discriminator for account type
|
|
443
|
+
offset += 1;
|
|
444
|
+
// Skip update_authority (32 bytes)
|
|
445
|
+
offset += 32;
|
|
446
|
+
// Skip mint (32 bytes)
|
|
447
|
+
offset += 32;
|
|
448
|
+
// Parse name (variable length string)
|
|
449
|
+
if (offset + 4 > data.length)
|
|
450
|
+
return null;
|
|
451
|
+
const nameLength = data.readUInt32LE(offset);
|
|
452
|
+
offset += 4;
|
|
453
|
+
if (nameLength > 200 || offset + nameLength > data.length)
|
|
454
|
+
return null;
|
|
455
|
+
const nameBytes = data.subarray(offset, offset + nameLength);
|
|
456
|
+
const name = nameBytes.toString('utf8').replace(/\0/g, '').trim();
|
|
457
|
+
offset += nameLength;
|
|
458
|
+
// Parse symbol (variable length string)
|
|
459
|
+
if (offset + 4 > data.length)
|
|
460
|
+
return null;
|
|
461
|
+
const symbolLength = data.readUInt32LE(offset);
|
|
462
|
+
offset += 4;
|
|
463
|
+
if (symbolLength > 50 || offset + symbolLength > data.length)
|
|
464
|
+
return null;
|
|
465
|
+
const symbolBytes = data.subarray(offset, offset + symbolLength);
|
|
466
|
+
const symbol = symbolBytes.toString('utf8').replace(/\0/g, '').trim();
|
|
467
|
+
return name || symbol ? { name, symbol } : null;
|
|
468
|
+
}
|
|
469
|
+
catch (error) {
|
|
470
|
+
console.debug('Error fetching token metadata:', error);
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
static decodeMessage({ data }) {
|
|
475
|
+
if (!data || typeof data !== 'string')
|
|
476
|
+
return undefined;
|
|
477
|
+
let eventDataBuffer;
|
|
478
|
+
try {
|
|
479
|
+
eventDataBuffer = bytesToBuffer(data);
|
|
480
|
+
}
|
|
481
|
+
catch (_) {
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
const disc = dataSlice(eventDataBuffer, 0, 8);
|
|
485
|
+
if (disc !== hexDiscriminator('CCIPMessageSent'))
|
|
486
|
+
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);
|
|
498
|
+
// Convert BN/number types to bigints
|
|
499
|
+
const sourceChainSelector = BigInt(message.header.sourceChainSelector.toString());
|
|
500
|
+
const destChainSelector = BigInt(message.header.destChainSelector.toString());
|
|
501
|
+
const sequenceNumber = BigInt(message.header.sequenceNumber.toString());
|
|
502
|
+
const nonce = BigInt(message.header.nonce.toString());
|
|
503
|
+
const destNetwork = networkInfo(destChainSelector);
|
|
504
|
+
// Convert message fields to expected format
|
|
505
|
+
const messageId = hexlify(new Uint8Array(message.header.messageId));
|
|
506
|
+
const sender = message.sender.toString();
|
|
507
|
+
const data_ = getDataBytes(message.data);
|
|
508
|
+
// TODO: extract this into a proper normalize/decode/reencode data utility
|
|
509
|
+
const msgData = destNetwork.family === ChainFamily.Solana ? encodeBase64(data_) : hexlify(data_);
|
|
510
|
+
const receiver = decodeAddress(message.receiver, destNetwork.family);
|
|
511
|
+
const feeToken = message.feeToken.toString();
|
|
512
|
+
// Process token amounts
|
|
513
|
+
const tokenAmounts = message.tokenAmounts.map((ta) => ({
|
|
514
|
+
sourcePoolAddress: ta.sourcePoolAddress.toBase58(),
|
|
515
|
+
destTokenAddress: decodeAddress(ta.destTokenAddress, destNetwork.family),
|
|
516
|
+
extraData: hexlify(ta.extraData),
|
|
517
|
+
amount: leToBigInt(ta.amount.leBytes),
|
|
518
|
+
destExecData: hexlify(ta.destExecData),
|
|
519
|
+
// destGasAmount is encoded as BE uint32;
|
|
520
|
+
destGasAmount: toBigInt(ta.destExecData),
|
|
521
|
+
}));
|
|
522
|
+
// Convert fee amounts from CrossChainAmount format
|
|
523
|
+
const feeTokenAmount = leToBigInt(message.feeTokenAmount.leBytes);
|
|
524
|
+
const feeValueJuels = leToBigInt(message.feeValueJuels.leBytes);
|
|
525
|
+
// Parse gas limit from extraArgs
|
|
526
|
+
const extraArgs = hexlify(message.extraArgs);
|
|
527
|
+
const parsed = this.decodeExtraArgs(extraArgs);
|
|
528
|
+
if (!parsed)
|
|
529
|
+
throw new Error('Invalid extraArgs: ' + extraArgs);
|
|
530
|
+
const { _tag, ...rest } = parsed;
|
|
531
|
+
return {
|
|
532
|
+
header: {
|
|
533
|
+
messageId,
|
|
534
|
+
sourceChainSelector,
|
|
535
|
+
destChainSelector: destChainSelector,
|
|
536
|
+
sequenceNumber: sequenceNumber,
|
|
537
|
+
nonce,
|
|
538
|
+
},
|
|
539
|
+
sender,
|
|
540
|
+
receiver,
|
|
541
|
+
data: msgData,
|
|
542
|
+
tokenAmounts,
|
|
543
|
+
feeToken,
|
|
544
|
+
feeTokenAmount,
|
|
545
|
+
feeValueJuels,
|
|
546
|
+
extraArgs,
|
|
547
|
+
...rest,
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
static decodeExtraArgs(extraArgs) {
|
|
551
|
+
const data = getDataBytes(extraArgs), tag = dataSlice(data, 0, 4);
|
|
552
|
+
switch (tag) {
|
|
553
|
+
case EVMExtraArgsV2Tag: {
|
|
554
|
+
if (dataLength(data) === 4 + 16 + 1) {
|
|
555
|
+
// Solana-generated EVMExtraArgsV2 (21 bytes total)
|
|
556
|
+
return {
|
|
557
|
+
_tag: 'EVMExtraArgsV2',
|
|
558
|
+
gasLimit: leToBigInt(dataSlice(data, 4, 4 + 16)), // from Uint128LE
|
|
559
|
+
allowOutOfOrderExecution: data[4 + 16] == 1,
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
throw new Error(`Unsupported EVMExtraArgsV2 length: ${dataLength(data)}`);
|
|
563
|
+
}
|
|
564
|
+
default:
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
static encodeExtraArgs(args) {
|
|
569
|
+
if ('computeUnits' in args)
|
|
570
|
+
throw new Error('Solana can only encode EVMExtraArgsV2');
|
|
571
|
+
const gasLimitUint128Le = toLeArray(args.gasLimit, 16);
|
|
572
|
+
return concat([
|
|
573
|
+
EVMExtraArgsV2Tag,
|
|
574
|
+
gasLimitUint128Le,
|
|
575
|
+
'allowOutOfOrderExecution' in args && args.allowOutOfOrderExecution ? '0x01' : '0x00',
|
|
576
|
+
]);
|
|
577
|
+
}
|
|
578
|
+
static decodeCommits(log, lane) {
|
|
579
|
+
// Check if this is a CommitReportAccepted event by looking at the discriminant
|
|
580
|
+
if (!log.data || typeof log.data !== 'string') {
|
|
581
|
+
throw new Error('Log data is missing or not a string');
|
|
582
|
+
}
|
|
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)
|
|
588
|
+
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
|
+
}
|
|
603
|
+
// Verify the source chain selector matches our lane
|
|
604
|
+
const sourceChainSelector = BigInt(merkleRootData.sourceChainSelector.toString());
|
|
605
|
+
// Convert the onRampAddress from bytes to the proper format
|
|
606
|
+
const onRampAddress = decodeOnRampAddress(merkleRootData.onRampAddress, networkInfo(sourceChainSelector).family);
|
|
607
|
+
if (lane) {
|
|
608
|
+
if (sourceChainSelector !== lane.sourceChainSelector)
|
|
609
|
+
return;
|
|
610
|
+
// Verify the onRampAddress matches our lane
|
|
611
|
+
if (onRampAddress !== lane.onRamp)
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
return [
|
|
615
|
+
{
|
|
616
|
+
sourceChainSelector,
|
|
617
|
+
onRampAddress,
|
|
618
|
+
minSeqNr: BigInt(merkleRootData.minSeqNr.toString()),
|
|
619
|
+
maxSeqNr: BigInt(merkleRootData.maxSeqNr.toString()),
|
|
620
|
+
merkleRoot: hexlify(new Uint8Array(merkleRootData.merkleRoot)),
|
|
621
|
+
},
|
|
622
|
+
];
|
|
623
|
+
}
|
|
624
|
+
static decodeReceipt(log) {
|
|
625
|
+
// Check if this is a ExecutionStateChanged event by looking at the discriminant
|
|
626
|
+
if (!log.data || typeof log.data !== 'string') {
|
|
627
|
+
throw new Error('Log data is missing or not a string');
|
|
628
|
+
}
|
|
629
|
+
// Verify the discriminant matches ExecutionStateChanged
|
|
630
|
+
if (dataSlice(getDataBytes(log.data), 0, 8) !== hexDiscriminator('ExecutionStateChanged'))
|
|
631
|
+
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;
|
|
649
|
+
// Decode state enum (MessageExecutionState)
|
|
650
|
+
// Enum discriminant is a single byte: Untouched=0, InProgress=1, Success=2, Failure=3
|
|
651
|
+
let state = eventDataBuffer.readUInt8(offset);
|
|
652
|
+
let returnData;
|
|
653
|
+
if (log.tx) {
|
|
654
|
+
// use only last receipt per tx+message (i.e. skip intermediary InProgress=1 states for Solana)
|
|
655
|
+
const laterReceiptLog = log.tx.logs
|
|
656
|
+
.filter((l) => l.index > log.index)
|
|
657
|
+
.findLast((l) => {
|
|
658
|
+
const lastReceipt = this.decodeReceipt(l);
|
|
659
|
+
return lastReceipt && lastReceipt.messageId === messageId;
|
|
660
|
+
});
|
|
661
|
+
if (laterReceiptLog) {
|
|
662
|
+
return; // ignore intermediary state (InProgress=1) if we can find a later receipt
|
|
663
|
+
}
|
|
664
|
+
else if (state !== ExecutionState.Success) {
|
|
665
|
+
returnData = getErrorFromLogs(log.tx.logs);
|
|
666
|
+
}
|
|
667
|
+
else if (log.tx.error) {
|
|
668
|
+
returnData = util.inspect(log.tx.error);
|
|
669
|
+
state = ExecutionState.Failed;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
return {
|
|
673
|
+
sourceChainSelector,
|
|
674
|
+
sequenceNumber,
|
|
675
|
+
messageId,
|
|
676
|
+
messageHash,
|
|
677
|
+
state,
|
|
678
|
+
returnData,
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
static getAddress(bytes) {
|
|
682
|
+
try {
|
|
683
|
+
if (typeof bytes === 'string' && bs58.decode(bytes).length === 32)
|
|
684
|
+
return bytes;
|
|
685
|
+
}
|
|
686
|
+
catch (_) {
|
|
687
|
+
// pass
|
|
688
|
+
}
|
|
689
|
+
return encodeBase58(getDataBytes(bytes));
|
|
690
|
+
}
|
|
691
|
+
static getDestLeafHasher(lane) {
|
|
692
|
+
return getV16SolanaLeafHasher(lane);
|
|
693
|
+
}
|
|
694
|
+
async getTokenAdminRegistryFor(address) {
|
|
695
|
+
const [type] = await this.typeAndVersion(address);
|
|
696
|
+
if (!type.includes('Router'))
|
|
697
|
+
throw new Error(`Not a Router: ${address} is ${type}`);
|
|
698
|
+
// Solana implements TokenAdminRegistry in the Router/OnRamp program
|
|
699
|
+
return address;
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* Get the fee required to send a CCIP message from the Solana router.
|
|
703
|
+
*/
|
|
704
|
+
getFee(router, destChainSelector, message) {
|
|
705
|
+
return getFee(this.connection, router, destChainSelector, message);
|
|
706
|
+
}
|
|
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);
|
|
712
|
+
}
|
|
713
|
+
async fetchOffchainTokenData(request) {
|
|
714
|
+
return fetchSolanaOffchainTokenData(this.connection, request);
|
|
715
|
+
}
|
|
716
|
+
async executeReport(offRamp, execReport_, opts) {
|
|
717
|
+
if (!('computeUnits' in execReport_.message))
|
|
718
|
+
throw new Error("ExecutionReport's message not for Solana");
|
|
719
|
+
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) {
|
|
725
|
+
try {
|
|
726
|
+
await this.cleanUpBuffers(opts);
|
|
727
|
+
}
|
|
728
|
+
catch (err) {
|
|
729
|
+
console.warn('Error while trying to clean up buffers:', err);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
return this.getTransaction(rep.hash);
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* 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
|
|
741
|
+
*/
|
|
742
|
+
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);
|
|
746
|
+
}
|
|
747
|
+
static parse(data) {
|
|
748
|
+
if (!data)
|
|
749
|
+
return;
|
|
750
|
+
try {
|
|
751
|
+
if (Array.isArray(data)) {
|
|
752
|
+
if (data.every((e) => typeof e === 'string'))
|
|
753
|
+
return getErrorFromLogs(data);
|
|
754
|
+
else if (data.every((e) => typeof e === 'object' && 'data' in e && 'address' in e))
|
|
755
|
+
return getErrorFromLogs(data);
|
|
756
|
+
}
|
|
757
|
+
else if (typeof data === 'object') {
|
|
758
|
+
if ('transactionLogs' in data && 'transactionMessage' in data) {
|
|
759
|
+
const parsed = getErrorFromLogs(data.transactionLogs);
|
|
760
|
+
if (parsed)
|
|
761
|
+
return { message: data.transactionMessage, ...parsed };
|
|
762
|
+
}
|
|
763
|
+
if ('logs' in data)
|
|
764
|
+
return getErrorFromLogs(data.logs);
|
|
765
|
+
}
|
|
766
|
+
else if (typeof data === 'string') {
|
|
767
|
+
const parsedExtraArgs = this.decodeExtraArgs(getDataBytes(data));
|
|
768
|
+
if (parsedExtraArgs)
|
|
769
|
+
return parsedExtraArgs;
|
|
770
|
+
const parsedMessage = this.decodeMessage({ data });
|
|
771
|
+
if (parsedMessage)
|
|
772
|
+
return parsedMessage;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
catch (_) {
|
|
776
|
+
// Ignore errors during parsing
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* Solana optimization: we use getProgramAccounts with
|
|
781
|
+
*/
|
|
782
|
+
async fetchCommitReport(commitStore, request, hints) {
|
|
783
|
+
const commitsAroundSeqNum = await this.connection.getProgramAccounts(new PublicKey(commitStore), {
|
|
784
|
+
filters: [
|
|
785
|
+
{
|
|
786
|
+
memcmp: {
|
|
787
|
+
offset: 0,
|
|
788
|
+
bytes: encodeBase58(BorshAccountsCoder.accountDiscriminator('CommitReport')),
|
|
789
|
+
},
|
|
790
|
+
},
|
|
791
|
+
{
|
|
792
|
+
memcmp: {
|
|
793
|
+
offset: 8 + 1,
|
|
794
|
+
bytes: encodeBase58(toLeArray(request.lane.sourceChainSelector, 8)),
|
|
795
|
+
},
|
|
796
|
+
},
|
|
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
|
|
799
|
+
{
|
|
800
|
+
memcmp: {
|
|
801
|
+
offset: 8 + 1 + 8 + 32 + 8 + 1,
|
|
802
|
+
bytes: encodeBase58(toLeArray(request.message.header.sequenceNumber, 8).slice(1)),
|
|
803
|
+
},
|
|
804
|
+
},
|
|
805
|
+
],
|
|
806
|
+
});
|
|
807
|
+
for (const acc of commitsAroundSeqNum) {
|
|
808
|
+
// const merkleRoot = acc.account.data.subarray(8 + 1 + 8, 8 + 1 + 8 + 32)
|
|
809
|
+
const minSeqNr = acc.account.data.readBigUInt64LE(8 + 1 + 8 + 32 + 8);
|
|
810
|
+
const maxSeqNr = acc.account.data.readBigUInt64LE(8 + 1 + 8 + 32 + 8 + 8);
|
|
811
|
+
if (minSeqNr > request.message.header.sequenceNumber ||
|
|
812
|
+
maxSeqNr < request.message.header.sequenceNumber)
|
|
813
|
+
continue;
|
|
814
|
+
// we have all the commit report info, but we also need log details (txHash, etc)
|
|
815
|
+
for await (const log of this.getLogs({
|
|
816
|
+
startTime: 1, // just to force getting the oldest log first
|
|
817
|
+
programs: [commitStore],
|
|
818
|
+
address: acc.pubkey.toBase58(),
|
|
819
|
+
topics: ['CommitReportAccepted'],
|
|
820
|
+
})) {
|
|
821
|
+
// first yielded log should be commit (which created this PDA)
|
|
822
|
+
const report = this.constructor.decodeCommits(log, request.lane)?.[0];
|
|
823
|
+
if (report)
|
|
824
|
+
return { report, log };
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
// in case we can't find it, fallback to generic iterating txs
|
|
828
|
+
return super.fetchCommitReport(commitStore, request, hints);
|
|
829
|
+
}
|
|
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;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
async getRegistryTokenConfig(registry, token) {
|
|
863
|
+
const registry_ = new PublicKey(registry);
|
|
864
|
+
const tokenMint = new PublicKey(token);
|
|
865
|
+
const [tokenAdminRegistryAddr] = PublicKey.findProgramAddressSync([Buffer.from('token_admin_registry'), tokenMint.toBuffer()], registry_);
|
|
866
|
+
const tokenAdminRegistry = await this.connection.getAccountInfo(tokenAdminRegistryAddr);
|
|
867
|
+
if (!tokenAdminRegistry)
|
|
868
|
+
throw new Error(`Token ${token} is not configured in registry ${registry}`);
|
|
869
|
+
const config = {
|
|
870
|
+
administrator: encodeBase58(tokenAdminRegistry.data.subarray(9, 9 + 32)),
|
|
871
|
+
};
|
|
872
|
+
const pendingAdministrator = new PublicKey(tokenAdminRegistry.data.subarray(41, 41 + 32));
|
|
873
|
+
// Check if pendingAdministrator is set (not system program address)
|
|
874
|
+
if (pendingAdministrator &&
|
|
875
|
+
!pendingAdministrator.equals(SystemProgram.programId) &&
|
|
876
|
+
!pendingAdministrator.equals(PublicKey.default)) {
|
|
877
|
+
config.pendingAdministrator = pendingAdministrator.toBase58();
|
|
878
|
+
}
|
|
879
|
+
// Get token pool from lookup table if available
|
|
880
|
+
try {
|
|
881
|
+
const lookupTableAddr = new PublicKey(tokenAdminRegistry.data.subarray(73, 73 + 32));
|
|
882
|
+
const lookupTable = await this.connection.getAddressLookupTable(lookupTableAddr);
|
|
883
|
+
if (lookupTable?.value) {
|
|
884
|
+
// tokenPool state PDA is at index [3]
|
|
885
|
+
const tokenPoolAddress = lookupTable.value.state.addresses[3];
|
|
886
|
+
if (tokenPoolAddress && !tokenPoolAddress.equals(PublicKey.default)) {
|
|
887
|
+
config.tokenPool = tokenPoolAddress.toBase58();
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
catch (_err) {
|
|
892
|
+
// Token pool may not be configured yet
|
|
893
|
+
}
|
|
894
|
+
return config;
|
|
895
|
+
}
|
|
896
|
+
async getTokenPoolConfigs(tokenPool) {
|
|
897
|
+
// `tokenPool` is actually a State PDA in the tokenPoolProgram
|
|
898
|
+
const tokenPoolState = await this.connection.getAccountInfo(new PublicKey(tokenPool));
|
|
899
|
+
if (!tokenPoolState)
|
|
900
|
+
throw new Error(`TokenPool State PDA not found at ${tokenPool}`);
|
|
901
|
+
const { config } = tokenPoolCoder.accounts.decode('state', tokenPoolState.data);
|
|
902
|
+
const tokenPoolProgram = tokenPoolState.owner.toBase58();
|
|
903
|
+
let typeAndVersion;
|
|
904
|
+
try {
|
|
905
|
+
;
|
|
906
|
+
[, , typeAndVersion] = await this.typeAndVersion(tokenPoolProgram);
|
|
907
|
+
}
|
|
908
|
+
catch (_) {
|
|
909
|
+
// TokenPool may not have a typeAndVersion
|
|
910
|
+
}
|
|
911
|
+
return {
|
|
912
|
+
token: config.mint.toBase58(),
|
|
913
|
+
router: config.router.toBase58(),
|
|
914
|
+
tokenPoolProgram,
|
|
915
|
+
typeAndVersion,
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
async getTokenPoolRemotes(tokenPool, remoteChainSelector) {
|
|
919
|
+
// `tokenPool` is actually a State PDA in the tokenPoolProgram
|
|
920
|
+
const tokenPoolState = await this.connection.getAccountInfo(new PublicKey(tokenPool));
|
|
921
|
+
if (!tokenPoolState)
|
|
922
|
+
throw new Error(`TokenPool State PDA not found at ${tokenPool}`);
|
|
923
|
+
const tokenPoolProgram = tokenPoolState.owner;
|
|
924
|
+
const { config } = tokenPoolCoder.accounts.decode('state', tokenPoolState.data);
|
|
925
|
+
// Get all supported chains by fetching ChainConfig PDAs
|
|
926
|
+
// We need to scan for all ChainConfig accounts owned by this token pool program
|
|
927
|
+
const remotes = {};
|
|
928
|
+
// Fetch all ChainConfig accounts for this token pool
|
|
929
|
+
let selectors = Object.values(SELECTORS);
|
|
930
|
+
let accounts;
|
|
931
|
+
if (remoteChainSelector) {
|
|
932
|
+
selectors = [{ selector: remoteChainSelector }];
|
|
933
|
+
const [chainConfigAddr] = PublicKey.findProgramAddressSync([
|
|
934
|
+
Buffer.from('ccip_tokenpool_chainconfig'),
|
|
935
|
+
toLeArray(remoteChainSelector, 8),
|
|
936
|
+
config.mint.toBuffer(),
|
|
937
|
+
], tokenPoolProgram);
|
|
938
|
+
const chainConfigAcc = await this.connection.getAccountInfo(chainConfigAddr);
|
|
939
|
+
if (!chainConfigAcc)
|
|
940
|
+
throw new Error(`ChainConfig not found at ${chainConfigAddr.toBase58()} for tokenPool=${tokenPool} and remoteNetwork=${networkInfo(remoteChainSelector).name}`);
|
|
941
|
+
accounts = [
|
|
942
|
+
{
|
|
943
|
+
pubkey: chainConfigAddr,
|
|
944
|
+
account: chainConfigAcc,
|
|
945
|
+
},
|
|
946
|
+
];
|
|
947
|
+
}
|
|
948
|
+
else
|
|
949
|
+
accounts = await this.connection.getProgramAccounts(tokenPoolProgram, {
|
|
950
|
+
filters: [
|
|
951
|
+
{
|
|
952
|
+
memcmp: {
|
|
953
|
+
offset: 0,
|
|
954
|
+
bytes: encodeBase58(BorshAccountsCoder.accountDiscriminator('ChainConfig')),
|
|
955
|
+
},
|
|
956
|
+
},
|
|
957
|
+
],
|
|
958
|
+
});
|
|
959
|
+
for (const acc of accounts) {
|
|
960
|
+
try {
|
|
961
|
+
let base;
|
|
962
|
+
try {
|
|
963
|
+
;
|
|
964
|
+
({ base } = tokenPoolCoder.accounts.decode('chainConfig', acc.account.data));
|
|
965
|
+
}
|
|
966
|
+
catch (_) {
|
|
967
|
+
;
|
|
968
|
+
({ base } = cctpTokenPoolCoder.accounts.decode('chainConfig', acc.account.data));
|
|
969
|
+
}
|
|
970
|
+
let remoteChainSelector;
|
|
971
|
+
// test all selectors, to find the correct seed
|
|
972
|
+
for (const { selector } of Object.values(selectors)) {
|
|
973
|
+
const [chainConfigAddr] = PublicKey.findProgramAddressSync([
|
|
974
|
+
Buffer.from('ccip_tokenpool_chainconfig'),
|
|
975
|
+
toLeArray(selector, 8),
|
|
976
|
+
config.mint.toBuffer(),
|
|
977
|
+
], tokenPoolProgram);
|
|
978
|
+
if (chainConfigAddr.equals(acc.pubkey)) {
|
|
979
|
+
remoteChainSelector = selector;
|
|
980
|
+
break;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
if (!remoteChainSelector)
|
|
984
|
+
continue;
|
|
985
|
+
const remoteNetwork = networkInfo(remoteChainSelector);
|
|
986
|
+
const remoteToken = decodeAddress(base.remote.tokenAddress.address, remoteNetwork.family);
|
|
987
|
+
const remotePools = base.remote.poolAddresses.map((pool) => decodeAddress(pool.address, remoteNetwork.family));
|
|
988
|
+
let inboundRateLimiterState = null;
|
|
989
|
+
if (base.inboundRateLimit.cfg.enabled) {
|
|
990
|
+
inboundRateLimiterState = {
|
|
991
|
+
tokens: BigInt(base.inboundRateLimit.tokens.toString()),
|
|
992
|
+
capacity: BigInt(base.inboundRateLimit.cfg.capacity.toString()),
|
|
993
|
+
rate: BigInt(base.inboundRateLimit.cfg.rate.toString()),
|
|
994
|
+
};
|
|
995
|
+
const cur = inboundRateLimiterState.tokens +
|
|
996
|
+
inboundRateLimiterState.rate *
|
|
997
|
+
BigInt(Math.floor(Date.now() / 1000) - base.inboundRateLimit.lastUpdated.toNumber());
|
|
998
|
+
if (cur < inboundRateLimiterState.capacity)
|
|
999
|
+
inboundRateLimiterState.tokens = cur;
|
|
1000
|
+
else
|
|
1001
|
+
inboundRateLimiterState.tokens = inboundRateLimiterState.capacity;
|
|
1002
|
+
}
|
|
1003
|
+
let outboundRateLimiterState = null;
|
|
1004
|
+
if (base.outboundRateLimit.cfg.enabled) {
|
|
1005
|
+
outboundRateLimiterState = {
|
|
1006
|
+
tokens: BigInt(base.outboundRateLimit.tokens.toString()),
|
|
1007
|
+
capacity: BigInt(base.outboundRateLimit.cfg.capacity.toString()),
|
|
1008
|
+
rate: BigInt(base.outboundRateLimit.cfg.rate.toString()),
|
|
1009
|
+
};
|
|
1010
|
+
const cur = outboundRateLimiterState.tokens +
|
|
1011
|
+
outboundRateLimiterState.rate *
|
|
1012
|
+
BigInt(Math.floor(Date.now() / 1000) - base.outboundRateLimit.lastUpdated.toNumber());
|
|
1013
|
+
if (cur < outboundRateLimiterState.capacity)
|
|
1014
|
+
outboundRateLimiterState.tokens = cur;
|
|
1015
|
+
else
|
|
1016
|
+
outboundRateLimiterState.tokens = outboundRateLimiterState.capacity;
|
|
1017
|
+
}
|
|
1018
|
+
remotes[remoteNetwork.name] = {
|
|
1019
|
+
remoteToken,
|
|
1020
|
+
remotePools,
|
|
1021
|
+
inboundRateLimiterState,
|
|
1022
|
+
outboundRateLimiterState,
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
catch (err) {
|
|
1026
|
+
console.warn('Failed to decode ChainConfig account:', err);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
return remotes;
|
|
1030
|
+
}
|
|
1031
|
+
async getSupportedTokens(router) {
|
|
1032
|
+
// `mint` offset in TokenAdminRegistry account data; more robust against changes in layout
|
|
1033
|
+
const mintOffset = 8 + 1 + 32 + 32 + 32 + 16 * 2; // = 137
|
|
1034
|
+
const router_ = new PublicKey(router);
|
|
1035
|
+
const res = [];
|
|
1036
|
+
for (const acc of await this.connection.getProgramAccounts(router_, {
|
|
1037
|
+
filters: [
|
|
1038
|
+
{
|
|
1039
|
+
memcmp: {
|
|
1040
|
+
offset: 0,
|
|
1041
|
+
bytes: encodeBase58(BorshAccountsCoder.accountDiscriminator('TokenAdminRegistry')),
|
|
1042
|
+
},
|
|
1043
|
+
},
|
|
1044
|
+
],
|
|
1045
|
+
})) {
|
|
1046
|
+
if (!acc.account.data || acc.account.data.length < mintOffset + 32)
|
|
1047
|
+
continue;
|
|
1048
|
+
const mint = new PublicKey(acc.account.data.subarray(mintOffset, mintOffset + 32));
|
|
1049
|
+
const [derivedPda] = PublicKey.findProgramAddressSync([Buffer.from('token_admin_registry'), mint.toBuffer()], router_);
|
|
1050
|
+
if (!acc.pubkey.equals(derivedPda))
|
|
1051
|
+
continue;
|
|
1052
|
+
res.push(mint.toBase58());
|
|
1053
|
+
}
|
|
1054
|
+
return res;
|
|
1055
|
+
}
|
|
1056
|
+
async listFeeTokens(router) {
|
|
1057
|
+
const { feeQuoter } = await this._getRouterConfig(router);
|
|
1058
|
+
const tokenConfigs = await this.connection.getProgramAccounts(feeQuoter, {
|
|
1059
|
+
filters: [
|
|
1060
|
+
{
|
|
1061
|
+
memcmp: {
|
|
1062
|
+
offset: 0,
|
|
1063
|
+
bytes: encodeBase58(BorshAccountsCoder.accountDiscriminator('BillingTokenConfigWrapper')),
|
|
1064
|
+
},
|
|
1065
|
+
},
|
|
1066
|
+
],
|
|
1067
|
+
});
|
|
1068
|
+
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())];
|
|
1071
|
+
})));
|
|
1072
|
+
}
|
|
1073
|
+
// cached
|
|
1074
|
+
async _getRouterConfig(router) {
|
|
1075
|
+
const program = new Program(CCIP_ROUTER_IDL, new PublicKey(router), {
|
|
1076
|
+
connection: this.connection,
|
|
1077
|
+
});
|
|
1078
|
+
const [configPda] = PublicKey.findProgramAddressSync([Buffer.from('config')], program.programId);
|
|
1079
|
+
// feeQuoter is present in router's config, and has a DestChainState account which is updated by
|
|
1080
|
+
// the offramps, so we can use it to narrow the search for the offramp
|
|
1081
|
+
return program.account.config.fetch(configPda);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
supportedChains[ChainFamily.Solana] = SolanaChain;
|
|
1085
|
+
//# sourceMappingURL=index.js.map
|