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