@aptos-labs/cross-chain-core 5.8.1 → 5.9.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 +26 -0
- package/dist/CrossChainCore.d.ts +20 -0
- package/dist/CrossChainCore.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +580 -275
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +583 -274
- package/dist/index.mjs.map +1 -1
- package/dist/providers/wormhole/index.d.ts +2 -0
- package/dist/providers/wormhole/index.d.ts.map +1 -1
- package/dist/providers/wormhole/signers/AptosLocalSigner.d.ts +1 -1
- package/dist/providers/wormhole/signers/AptosLocalSigner.d.ts.map +1 -1
- package/dist/providers/wormhole/signers/Signer.d.ts +1 -1
- package/dist/providers/wormhole/signers/Signer.d.ts.map +1 -1
- package/dist/providers/wormhole/signers/SolanaLocalSigner.d.ts +65 -0
- package/dist/providers/wormhole/signers/SolanaLocalSigner.d.ts.map +1 -0
- package/dist/providers/wormhole/signers/SolanaSigner.d.ts +12 -20
- package/dist/providers/wormhole/signers/SolanaSigner.d.ts.map +1 -1
- package/dist/providers/wormhole/signers/solanaUtils.d.ts +68 -0
- package/dist/providers/wormhole/signers/solanaUtils.d.ts.map +1 -0
- package/dist/providers/wormhole/types.d.ts +43 -0
- package/dist/providers/wormhole/types.d.ts.map +1 -1
- package/dist/providers/wormhole/utils.d.ts +26 -0
- package/dist/providers/wormhole/utils.d.ts.map +1 -0
- package/dist/providers/wormhole/wormhole.d.ts +36 -6
- package/dist/providers/wormhole/wormhole.d.ts.map +1 -1
- package/dist/utils/receiptSerialization.d.ts +38 -0
- package/dist/utils/receiptSerialization.d.ts.map +1 -0
- package/dist/version.d.ts +1 -1
- package/package.json +3 -3
- package/src/CrossChainCore.ts +20 -0
- package/src/config/mainnet/chains.ts +2 -2
- package/src/config/testnet/chains.ts +2 -2
- package/src/index.ts +1 -0
- package/src/providers/wormhole/index.ts +2 -0
- package/src/providers/wormhole/signers/AptosLocalSigner.ts +4 -4
- package/src/providers/wormhole/signers/AptosSigner.ts +1 -1
- package/src/providers/wormhole/signers/EthereumSigner.ts +3 -3
- package/src/providers/wormhole/signers/Signer.ts +4 -4
- package/src/providers/wormhole/signers/SolanaLocalSigner.ts +243 -0
- package/src/providers/wormhole/signers/SolanaSigner.ts +45 -337
- package/src/providers/wormhole/signers/solanaUtils.ts +422 -0
- package/src/providers/wormhole/types.ts +68 -0
- package/src/providers/wormhole/utils.ts +72 -0
- package/src/providers/wormhole/wormhole.ts +182 -120
- package/src/utils/receiptSerialization.ts +141 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for Solana transaction handling.
|
|
3
|
+
* Used by both SolanaLocalSigner (server-side) and SolanaSigner (client-side).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
ComputeBudgetProgram,
|
|
8
|
+
Connection,
|
|
9
|
+
LAMPORTS_PER_SOL,
|
|
10
|
+
RpcResponseAndContext,
|
|
11
|
+
SignatureResult,
|
|
12
|
+
SimulatedTransactionResponse,
|
|
13
|
+
Transaction,
|
|
14
|
+
TransactionInstruction,
|
|
15
|
+
VersionedTransaction,
|
|
16
|
+
type Commitment,
|
|
17
|
+
} from "@solana/web3.js";
|
|
18
|
+
import {
|
|
19
|
+
determinePriorityFee,
|
|
20
|
+
determinePriorityFeeTritonOne,
|
|
21
|
+
} from "@wormhole-foundation/sdk-solana";
|
|
22
|
+
|
|
23
|
+
// ============================================================================
|
|
24
|
+
// Types
|
|
25
|
+
// ============================================================================
|
|
26
|
+
|
|
27
|
+
/** Configuration for priority fees */
|
|
28
|
+
export interface PriorityFeeConfig {
|
|
29
|
+
/** Percentile of recent fees to use (default: 0.9) */
|
|
30
|
+
percentile?: number;
|
|
31
|
+
/** Multiplier for the percentile fee (default: 1) */
|
|
32
|
+
percentileMultiple?: number;
|
|
33
|
+
/** Minimum fee in microlamports (default: 100_000) */
|
|
34
|
+
min?: number;
|
|
35
|
+
/** Maximum fee in microlamports (default: 100_000_000) */
|
|
36
|
+
max?: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Configuration for sending and confirming transactions */
|
|
40
|
+
export interface SendAndConfirmConfig {
|
|
41
|
+
connection: Connection;
|
|
42
|
+
commitment: Commitment;
|
|
43
|
+
retryIntervalMs?: number;
|
|
44
|
+
/** Enable verbose logging (default: false) */
|
|
45
|
+
verbose?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** RPC provider type for priority fee calculation */
|
|
49
|
+
export type SolanaRpcProvider = "triton" | "helius" | "ankr" | "unknown";
|
|
50
|
+
|
|
51
|
+
// ============================================================================
|
|
52
|
+
// Transaction Confirmation
|
|
53
|
+
// ============================================================================
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Sends a serialized transaction and waits for confirmation with automatic retries.
|
|
57
|
+
*
|
|
58
|
+
* @param serializedTx - The serialized transaction bytes
|
|
59
|
+
* @param blockhash - The recent blockhash used in the transaction
|
|
60
|
+
* @param lastValidBlockHeight - The last valid block height for the transaction
|
|
61
|
+
* @param config - Configuration for sending and confirming
|
|
62
|
+
* @returns The transaction signature
|
|
63
|
+
*/
|
|
64
|
+
export async function sendAndConfirmTransaction(
|
|
65
|
+
serializedTx: Buffer | Uint8Array,
|
|
66
|
+
blockhash: string,
|
|
67
|
+
lastValidBlockHeight: number,
|
|
68
|
+
config: SendAndConfirmConfig,
|
|
69
|
+
): Promise<string> {
|
|
70
|
+
const { connection, commitment, retryIntervalMs = 5000, verbose = false } = config;
|
|
71
|
+
|
|
72
|
+
const sendOptions = {
|
|
73
|
+
skipPreflight: true,
|
|
74
|
+
maxRetries: 0,
|
|
75
|
+
preflightCommitment: commitment,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const signature = await connection.sendRawTransaction(serializedTx, sendOptions);
|
|
79
|
+
|
|
80
|
+
const confirmTransactionPromise = connection.confirmTransaction(
|
|
81
|
+
{ signature, blockhash, lastValidBlockHeight },
|
|
82
|
+
commitment,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// Retry loop: resend if not confirmed after interval
|
|
86
|
+
let confirmedTx: RpcResponseAndContext<SignatureResult> | null = null;
|
|
87
|
+
let txSendAttempts = 1;
|
|
88
|
+
|
|
89
|
+
while (!confirmedTx) {
|
|
90
|
+
confirmedTx = await Promise.race([
|
|
91
|
+
confirmTransactionPromise,
|
|
92
|
+
new Promise<null>((resolve) =>
|
|
93
|
+
setTimeout(() => resolve(null), retryIntervalMs),
|
|
94
|
+
),
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
if (confirmedTx) break;
|
|
98
|
+
|
|
99
|
+
if (verbose) {
|
|
100
|
+
console.log(
|
|
101
|
+
`Tx not confirmed after ${retryIntervalMs * txSendAttempts++}ms, resending`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
await connection.sendRawTransaction(serializedTx, sendOptions);
|
|
107
|
+
} catch (e) {
|
|
108
|
+
if (verbose) {
|
|
109
|
+
console.error("Failed to resend transaction:", e);
|
|
110
|
+
}
|
|
111
|
+
// Ignore resend errors, confirmation will handle success/failure
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (confirmedTx.value.err) {
|
|
116
|
+
const errorMessage = formatTransactionError(confirmedTx.value.err);
|
|
117
|
+
throw new Error(errorMessage);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return signature;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Formats a transaction error into a readable string.
|
|
125
|
+
*/
|
|
126
|
+
export function formatTransactionError(err: unknown): string {
|
|
127
|
+
if (typeof err === "object" && err !== null) {
|
|
128
|
+
try {
|
|
129
|
+
return `Transaction failed: ${JSON.stringify(
|
|
130
|
+
err,
|
|
131
|
+
(_key, value) => (typeof value === "bigint" ? value.toString() : value),
|
|
132
|
+
)}`;
|
|
133
|
+
} catch {
|
|
134
|
+
// Circular reference or other stringify error
|
|
135
|
+
return "Transaction failed: Unknown error";
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return `Transaction failed: ${err}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ============================================================================
|
|
142
|
+
// Priority Fees
|
|
143
|
+
// ============================================================================
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Adds priority fee instructions to a transaction.
|
|
147
|
+
* Simulates the transaction to determine compute units and calculates optimal fees.
|
|
148
|
+
*
|
|
149
|
+
* @param connection - Solana RPC connection
|
|
150
|
+
* @param transaction - The transaction to add priority fees to
|
|
151
|
+
* @param priorityFeeConfig - Configuration for priority fees
|
|
152
|
+
* @param verbose - Enable verbose logging
|
|
153
|
+
* @returns The transaction with priority fee instructions added
|
|
154
|
+
*/
|
|
155
|
+
export async function addPriorityFeeInstructions(
|
|
156
|
+
connection: Connection,
|
|
157
|
+
transaction: Transaction,
|
|
158
|
+
priorityFeeConfig?: PriorityFeeConfig,
|
|
159
|
+
verbose: boolean = false,
|
|
160
|
+
): Promise<Transaction> {
|
|
161
|
+
const computeBudgetIxFilter = (ix: TransactionInstruction) =>
|
|
162
|
+
ix.programId.toString() !== "ComputeBudget111111111111111111111111111111";
|
|
163
|
+
|
|
164
|
+
// Remove existing compute budget instructions if they were added by the SDK
|
|
165
|
+
transaction.instructions = transaction.instructions.filter(computeBudgetIxFilter);
|
|
166
|
+
|
|
167
|
+
const instructions = await createPriorityFeeInstructions(
|
|
168
|
+
connection,
|
|
169
|
+
transaction,
|
|
170
|
+
priorityFeeConfig,
|
|
171
|
+
verbose,
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
transaction.add(...instructions);
|
|
175
|
+
|
|
176
|
+
return transaction;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Creates priority fee instructions based on simulation and fee estimation.
|
|
181
|
+
*/
|
|
182
|
+
export async function createPriorityFeeInstructions(
|
|
183
|
+
connection: Connection,
|
|
184
|
+
transaction: Transaction | VersionedTransaction,
|
|
185
|
+
priorityFeeConfig?: PriorityFeeConfig,
|
|
186
|
+
verbose: boolean = false,
|
|
187
|
+
): Promise<TransactionInstruction[]> {
|
|
188
|
+
// Simulate to get compute units
|
|
189
|
+
const unitsUsed = await simulateAndGetComputeUnits(connection, transaction);
|
|
190
|
+
const unitBudget = Math.floor(unitsUsed * 1.2); // Budget in 20% headroom
|
|
191
|
+
|
|
192
|
+
const instructions: TransactionInstruction[] = [];
|
|
193
|
+
instructions.push(
|
|
194
|
+
ComputeBudgetProgram.setComputeUnitLimit({
|
|
195
|
+
units: unitBudget,
|
|
196
|
+
}),
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
// Calculate priority fee
|
|
200
|
+
const {
|
|
201
|
+
percentile = 0.9,
|
|
202
|
+
percentileMultiple = 1,
|
|
203
|
+
min = 100_000,
|
|
204
|
+
max = 100_000_000,
|
|
205
|
+
} = priorityFeeConfig ?? {};
|
|
206
|
+
|
|
207
|
+
const rpcProvider = determineRpcProvider(connection.rpcEndpoint);
|
|
208
|
+
const { fee, methodUsed } = await calculatePriorityFee(
|
|
209
|
+
connection,
|
|
210
|
+
transaction,
|
|
211
|
+
rpcProvider,
|
|
212
|
+
{ percentile, percentileMultiple, min, max },
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
if (verbose) {
|
|
216
|
+
const maxFeeInSol = (fee / 1e6 / LAMPORTS_PER_SOL) * unitBudget;
|
|
217
|
+
console.table({
|
|
218
|
+
"RPC Provider": rpcProvider,
|
|
219
|
+
"Method used": methodUsed,
|
|
220
|
+
"Percentile used": percentile,
|
|
221
|
+
"Multiple used": percentileMultiple,
|
|
222
|
+
"Compute budget": unitBudget,
|
|
223
|
+
"Priority fee": fee,
|
|
224
|
+
"Max fee in SOL": maxFeeInSol,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
instructions.push(
|
|
229
|
+
ComputeBudgetProgram.setComputeUnitPrice({ microLamports: fee }),
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
return instructions;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Simulates a transaction and returns the compute units consumed.
|
|
237
|
+
*/
|
|
238
|
+
async function simulateAndGetComputeUnits(
|
|
239
|
+
connection: Connection,
|
|
240
|
+
transaction: Transaction | VersionedTransaction,
|
|
241
|
+
): Promise<number> {
|
|
242
|
+
let unitsUsed = 200_000;
|
|
243
|
+
let simulationAttempts = 0;
|
|
244
|
+
|
|
245
|
+
simulationLoop: while (true) {
|
|
246
|
+
const response = await connection.simulateTransaction(transaction as Transaction);
|
|
247
|
+
|
|
248
|
+
if (response.value.err) {
|
|
249
|
+
if (checkKnownSimulationError(response.value)) {
|
|
250
|
+
// Number of attempts will be at most 5 for known errors
|
|
251
|
+
if (simulationAttempts < 5) {
|
|
252
|
+
simulationAttempts++;
|
|
253
|
+
await sleep(1000);
|
|
254
|
+
continue simulationLoop;
|
|
255
|
+
}
|
|
256
|
+
} else if (simulationAttempts < 3) {
|
|
257
|
+
// Number of attempts will be at most 3 for unknown errors
|
|
258
|
+
simulationAttempts++;
|
|
259
|
+
await sleep(1000);
|
|
260
|
+
continue simulationLoop;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Still failing after multiple attempts
|
|
264
|
+
throw new Error(
|
|
265
|
+
`Simulation failed: ${JSON.stringify(response.value.err)}\nLogs:\n${(
|
|
266
|
+
response.value.logs || []
|
|
267
|
+
).join("\n ")}`,
|
|
268
|
+
);
|
|
269
|
+
} else {
|
|
270
|
+
// Simulation was successful
|
|
271
|
+
if (response.value.unitsConsumed) {
|
|
272
|
+
unitsUsed = response.value.unitsConsumed;
|
|
273
|
+
}
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return unitsUsed;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Calculates the priority fee based on RPC provider and configuration.
|
|
283
|
+
*/
|
|
284
|
+
async function calculatePriorityFee(
|
|
285
|
+
connection: Connection,
|
|
286
|
+
transaction: Transaction | VersionedTransaction,
|
|
287
|
+
rpcProvider: SolanaRpcProvider,
|
|
288
|
+
config: Required<PriorityFeeConfig>,
|
|
289
|
+
): Promise<{ fee: number; methodUsed: "triton" | "default" | "minimum" }> {
|
|
290
|
+
const { percentile, percentileMultiple, min, max } = config;
|
|
291
|
+
|
|
292
|
+
if (rpcProvider === "triton") {
|
|
293
|
+
// Triton has an experimental RPC method that accepts a percentile parameter
|
|
294
|
+
// and usually gives more accurate fee numbers.
|
|
295
|
+
try {
|
|
296
|
+
const fee = await determinePriorityFeeTritonOne(
|
|
297
|
+
connection,
|
|
298
|
+
transaction,
|
|
299
|
+
percentile,
|
|
300
|
+
percentileMultiple,
|
|
301
|
+
min,
|
|
302
|
+
max,
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
return { fee, methodUsed: "triton" };
|
|
306
|
+
} catch (e) {
|
|
307
|
+
console.warn(`Failed to determine priority fee using Triton RPC:`, e);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
// By default, use generic Solana RPC method
|
|
313
|
+
const fee = await determinePriorityFee(
|
|
314
|
+
connection,
|
|
315
|
+
transaction,
|
|
316
|
+
percentile,
|
|
317
|
+
percentileMultiple,
|
|
318
|
+
min,
|
|
319
|
+
max,
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
return { fee, methodUsed: "default" };
|
|
323
|
+
} catch (e) {
|
|
324
|
+
console.warn(`Failed to determine priority fee:`, e);
|
|
325
|
+
|
|
326
|
+
return { fee: min, methodUsed: "minimum" };
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ============================================================================
|
|
331
|
+
// Helpers
|
|
332
|
+
// ============================================================================
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Checks response logs for known simulation errors that can be retried.
|
|
336
|
+
*/
|
|
337
|
+
function checkKnownSimulationError(response: SimulatedTransactionResponse): boolean {
|
|
338
|
+
const errors: Record<string, string> = {};
|
|
339
|
+
|
|
340
|
+
// This error occurs when the blockhash included in a transaction is not deemed to be valid
|
|
341
|
+
if (response.err === "BlockhashNotFound") {
|
|
342
|
+
errors["BlockhashNotFound"] =
|
|
343
|
+
"Blockhash not found during simulation. Trying again.";
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Check the response logs for any known errors
|
|
347
|
+
if (response.logs) {
|
|
348
|
+
for (const line of response.logs) {
|
|
349
|
+
if (line.includes("SlippageToleranceExceeded")) {
|
|
350
|
+
errors["SlippageToleranceExceeded"] =
|
|
351
|
+
"Slippage failure during simulation. Trying again.";
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (line.includes("RequireGteViolated")) {
|
|
355
|
+
errors["RequireGteViolated"] =
|
|
356
|
+
"Swap instruction failure during simulation. Trying again.";
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (Object.keys(errors).length === 0) {
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
console.table(errors);
|
|
366
|
+
return true;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Checks whether a hostname is exactly the given domain or a subdomain of it.
|
|
371
|
+
* e.g. isHostOrSubdomainOf("api.triton.one", "triton.one") => true
|
|
372
|
+
* isHostOrSubdomainOf("not-triton.com", "triton.one") => false
|
|
373
|
+
*/
|
|
374
|
+
function isHostOrSubdomainOf(hostname: string, base: string): boolean {
|
|
375
|
+
return hostname === base || hostname.endsWith(`.${base}`);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Determines the RPC provider from the endpoint URL.
|
|
380
|
+
*/
|
|
381
|
+
export function determineRpcProvider(endpoint: string): SolanaRpcProvider {
|
|
382
|
+
try {
|
|
383
|
+
const url = new URL(endpoint);
|
|
384
|
+
const hostname = url.hostname;
|
|
385
|
+
if (isHostOrSubdomainOf(hostname, "rpcpool.com") || isHostOrSubdomainOf(hostname, "triton.one")) {
|
|
386
|
+
return "triton";
|
|
387
|
+
} else if (isHostOrSubdomainOf(hostname, "helius-rpc.com") || isHostOrSubdomainOf(hostname, "helius.xyz")) {
|
|
388
|
+
return "helius";
|
|
389
|
+
} else if (isHostOrSubdomainOf(hostname, "ankr.com")) {
|
|
390
|
+
return "ankr";
|
|
391
|
+
} else {
|
|
392
|
+
return "unknown";
|
|
393
|
+
}
|
|
394
|
+
} catch {
|
|
395
|
+
return "unknown";
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Sleep for a specified duration.
|
|
401
|
+
*/
|
|
402
|
+
export async function sleep(timeout: number): Promise<void> {
|
|
403
|
+
return new Promise((resolve) => setTimeout(resolve, timeout));
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Checks whether an object is empty.
|
|
408
|
+
*/
|
|
409
|
+
export const isEmptyObject = (value: object | null | undefined): boolean => {
|
|
410
|
+
if (value === null || value === undefined) {
|
|
411
|
+
return true;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
for (const key in value) {
|
|
415
|
+
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
416
|
+
return false;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return true;
|
|
421
|
+
};
|
|
422
|
+
|
|
@@ -37,11 +37,18 @@ export interface WormholeTransferRequest {
|
|
|
37
37
|
sponsorAccount?: Account;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
export type WithdrawPhase =
|
|
41
|
+
| "initiating" // User signing Aptos burn transaction
|
|
42
|
+
| "tracking" // Waiting for Wormhole attestation (~60s)
|
|
43
|
+
| "claiming"; // Claiming on destination chain
|
|
44
|
+
|
|
40
45
|
export interface WormholeWithdrawRequest {
|
|
41
46
|
sourceChain: Chain;
|
|
42
47
|
wallet: AdapterWallet;
|
|
43
48
|
destinationAddress: AccountAddressInput;
|
|
44
49
|
sponsorAccount?: Account | GasStationApiKey;
|
|
50
|
+
/** Optional callback fired when the withdraw progresses to a new phase. */
|
|
51
|
+
onPhaseChange?: (phase: WithdrawPhase) => void;
|
|
45
52
|
}
|
|
46
53
|
|
|
47
54
|
export interface WormholeSubmitTransferRequest {
|
|
@@ -70,3 +77,64 @@ export interface WormholeStartTransferResponse {
|
|
|
70
77
|
originChainTxnId: string;
|
|
71
78
|
receipt: routes.Receipt<AttestationReceipt>;
|
|
72
79
|
}
|
|
80
|
+
|
|
81
|
+
// --- Split withdraw flow types ---
|
|
82
|
+
|
|
83
|
+
export interface WormholeInitiateWithdrawRequest {
|
|
84
|
+
wallet: AdapterWallet;
|
|
85
|
+
destinationAddress: AccountAddressInput;
|
|
86
|
+
sponsorAccount?: Account | GasStationApiKey;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface WormholeInitiateWithdrawResponse {
|
|
90
|
+
originChainTxnId: string;
|
|
91
|
+
receipt: routes.Receipt<AttestationReceipt>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface WormholeClaimWithdrawRequest {
|
|
95
|
+
sourceChain: Chain;
|
|
96
|
+
destinationAddress: string;
|
|
97
|
+
receipt: routes.Receipt<AttestationReceipt>;
|
|
98
|
+
// Required for wallet-based claim (non-Solana chains, or Solana without serverClaimUrl).
|
|
99
|
+
// Not needed when the SDK uses the configured serverClaimUrl for Solana claims.
|
|
100
|
+
wallet?: AdapterWallet;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface WormholeClaimWithdrawResponse {
|
|
104
|
+
destinationChainTxnId: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Error thrown when the withdraw flow fails *after* the Aptos burn
|
|
109
|
+
* transaction has already been submitted (i.e. during attestation tracking
|
|
110
|
+
* or destination-chain claiming).
|
|
111
|
+
*
|
|
112
|
+
* Consumers should check `instanceof WithdrawError` in their catch block
|
|
113
|
+
* to recover the `originChainTxnId` and display an explorer link so the
|
|
114
|
+
* user can verify their burn on-chain.
|
|
115
|
+
*/
|
|
116
|
+
export class WithdrawError extends Error {
|
|
117
|
+
/** Aptos burn transaction hash — always available when this error is thrown. */
|
|
118
|
+
readonly originChainTxnId: string;
|
|
119
|
+
/** The withdraw phase that failed ("tracking" or "claiming"). */
|
|
120
|
+
readonly phase: WithdrawPhase;
|
|
121
|
+
/**
|
|
122
|
+
* The underlying error that caused this failure.
|
|
123
|
+
* Mirrors ES2022 Error.cause — declared explicitly because the project's
|
|
124
|
+
* TypeScript lib target does not include ES2022 ErrorOptions.
|
|
125
|
+
*/
|
|
126
|
+
readonly cause?: unknown;
|
|
127
|
+
|
|
128
|
+
constructor(
|
|
129
|
+
message: string,
|
|
130
|
+
originChainTxnId: string,
|
|
131
|
+
phase: WithdrawPhase,
|
|
132
|
+
cause?: unknown,
|
|
133
|
+
) {
|
|
134
|
+
super(message);
|
|
135
|
+
this.name = "WithdrawError";
|
|
136
|
+
this.originChainTxnId = originChainTxnId;
|
|
137
|
+
this.phase = phase;
|
|
138
|
+
this.cause = cause;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for Wormhole CCTP route creation.
|
|
3
|
+
* These helpers can be used both by WormholeProvider and server-side claim endpoints.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
chainToPlatform,
|
|
8
|
+
routes,
|
|
9
|
+
Wormhole,
|
|
10
|
+
TokenId,
|
|
11
|
+
} from "@wormhole-foundation/sdk";
|
|
12
|
+
import { Chain } from "../../CrossChainCore";
|
|
13
|
+
import { TokenConfig } from "../../config";
|
|
14
|
+
|
|
15
|
+
export interface CCTPRouteResult {
|
|
16
|
+
route: routes.ManualRoute<"Mainnet" | "Testnet">;
|
|
17
|
+
request: routes.RouteTransferRequest<"Mainnet" | "Testnet">;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Creates a CCTP route for transferring USDC between two chains.
|
|
22
|
+
*
|
|
23
|
+
* This is a standalone helper that can be used both by WormholeProvider
|
|
24
|
+
* (client-side) and server-side claim endpoints to avoid code duplication.
|
|
25
|
+
*
|
|
26
|
+
* @param wh - Initialized Wormhole context
|
|
27
|
+
* @param sourceChain - Source chain (e.g., "Aptos")
|
|
28
|
+
* @param destChain - Destination chain (e.g., "Solana")
|
|
29
|
+
* @param tokens - Token configuration mapping (mainnetTokens or testnetTokens)
|
|
30
|
+
* @returns The CCTP route and request for completing transfers
|
|
31
|
+
* @throws Error if no valid CCTP route is found
|
|
32
|
+
*/
|
|
33
|
+
export async function createCCTPRoute(
|
|
34
|
+
wh: Wormhole<"Mainnet" | "Testnet">,
|
|
35
|
+
sourceChain: Chain,
|
|
36
|
+
destChain: Chain,
|
|
37
|
+
tokens: Record<string, TokenConfig>,
|
|
38
|
+
): Promise<CCTPRouteResult> {
|
|
39
|
+
const sourceToken: TokenId = Wormhole.tokenId(
|
|
40
|
+
sourceChain,
|
|
41
|
+
tokens[sourceChain].tokenId.address,
|
|
42
|
+
);
|
|
43
|
+
const destToken: TokenId = Wormhole.tokenId(
|
|
44
|
+
destChain,
|
|
45
|
+
tokens[destChain].tokenId.address,
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const destContext = wh
|
|
49
|
+
.getPlatform(chainToPlatform(destChain))
|
|
50
|
+
.getChain(destChain);
|
|
51
|
+
const sourceContext = wh
|
|
52
|
+
.getPlatform(chainToPlatform(sourceChain))
|
|
53
|
+
.getChain(sourceChain);
|
|
54
|
+
|
|
55
|
+
const request = await routes.RouteTransferRequest.create(
|
|
56
|
+
wh,
|
|
57
|
+
{ source: sourceToken, destination: destToken },
|
|
58
|
+
sourceContext,
|
|
59
|
+
destContext,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const resolver = wh.resolver([routes.CCTPRoute]);
|
|
63
|
+
const foundRoutes = await resolver.findRoutes(request);
|
|
64
|
+
const cctpRoute = foundRoutes[0];
|
|
65
|
+
|
|
66
|
+
if (!cctpRoute || !routes.isManual(cctpRoute)) {
|
|
67
|
+
throw new Error("Expected manual CCTP route");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { route: cctpRoute, request };
|
|
71
|
+
}
|
|
72
|
+
|