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