@aptos-labs/cross-chain-core 5.8.2 → 6.0.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 +95 -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 +908 -404
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +914 -409
- 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 +9 -7
- package/dist/providers/wormhole/signers/AptosLocalSigner.d.ts.map +1 -1
- package/dist/providers/wormhole/signers/AptosSigner.d.ts +2 -1
- package/dist/providers/wormhole/signers/AptosSigner.d.ts.map +1 -1
- package/dist/providers/wormhole/signers/EthereumSigner.d.ts +1 -1
- package/dist/providers/wormhole/signers/EthereumSigner.d.ts.map +1 -1
- package/dist/providers/wormhole/signers/Signer.d.ts +11 -3
- package/dist/providers/wormhole/signers/Signer.d.ts.map +1 -1
- package/dist/providers/wormhole/signers/SolanaLocalSigner.d.ts +69 -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 +120 -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 +62 -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 +110 -3
- 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 +31 -18
- package/src/providers/wormhole/signers/AptosSigner.ts +11 -2
- package/src/providers/wormhole/signers/EthereumSigner.ts +59 -8
- package/src/providers/wormhole/signers/Signer.ts +23 -6
- package/src/providers/wormhole/signers/SolanaLocalSigner.ts +250 -0
- package/src/providers/wormhole/signers/SolanaSigner.ts +49 -338
- package/src/providers/wormhole/signers/solanaUtils.ts +446 -0
- package/src/providers/wormhole/types.ts +167 -0
- package/src/providers/wormhole/utils.ts +72 -0
- package/src/providers/wormhole/wormhole.ts +309 -137
- package/src/utils/receiptSerialization.ts +141 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,446 @@
|
|
|
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
|
+
// The confirmation promise can reject with "block height exceeded" when the
|
|
87
|
+
// blockhash expires before confirmation completes. Because the transaction was
|
|
88
|
+
// already sent (sendRawTransaction succeeded), it may still land on-chain.
|
|
89
|
+
// In that case we return the signature so the caller can track it, rather than
|
|
90
|
+
// throwing and losing the transaction reference.
|
|
91
|
+
let confirmedTx: RpcResponseAndContext<SignatureResult> | null = null;
|
|
92
|
+
let txSendAttempts = 1;
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
while (!confirmedTx) {
|
|
96
|
+
confirmedTx = await Promise.race([
|
|
97
|
+
confirmTransactionPromise,
|
|
98
|
+
new Promise<null>((resolve) =>
|
|
99
|
+
setTimeout(() => resolve(null), retryIntervalMs),
|
|
100
|
+
),
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
if (confirmedTx) break;
|
|
104
|
+
|
|
105
|
+
if (verbose) {
|
|
106
|
+
console.log(
|
|
107
|
+
`Tx not confirmed after ${retryIntervalMs * txSendAttempts++}ms, resending`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
await connection.sendRawTransaction(serializedTx, sendOptions);
|
|
113
|
+
} catch (e) {
|
|
114
|
+
if (verbose) {
|
|
115
|
+
console.error("Failed to resend transaction:", e);
|
|
116
|
+
}
|
|
117
|
+
// Ignore resend errors, confirmation will handle success/failure
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} catch (e) {
|
|
121
|
+
const message = e instanceof Error ? e.message.toLowerCase() : "";
|
|
122
|
+
if (
|
|
123
|
+
message.includes("block height exceeded") ||
|
|
124
|
+
message.includes("blockheightexceeded")
|
|
125
|
+
) {
|
|
126
|
+
if (verbose) {
|
|
127
|
+
console.warn(
|
|
128
|
+
"Block height exceeded but tx was already sent, returning signature:",
|
|
129
|
+
signature,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
// Transaction was already sent — return the signature so the caller can
|
|
133
|
+
// track confirmation asynchronously instead of losing the tx reference.
|
|
134
|
+
return signature;
|
|
135
|
+
}
|
|
136
|
+
throw e;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (confirmedTx.value.err) {
|
|
140
|
+
const errorMessage = formatTransactionError(confirmedTx.value.err);
|
|
141
|
+
throw new Error(errorMessage);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return signature;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Formats a transaction error into a readable string.
|
|
149
|
+
*/
|
|
150
|
+
export function formatTransactionError(err: unknown): string {
|
|
151
|
+
if (typeof err === "object" && err !== null) {
|
|
152
|
+
try {
|
|
153
|
+
return `Transaction failed: ${JSON.stringify(
|
|
154
|
+
err,
|
|
155
|
+
(_key, value) => (typeof value === "bigint" ? value.toString() : value),
|
|
156
|
+
)}`;
|
|
157
|
+
} catch {
|
|
158
|
+
// Circular reference or other stringify error
|
|
159
|
+
return "Transaction failed: Unknown error";
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return `Transaction failed: ${err}`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ============================================================================
|
|
166
|
+
// Priority Fees
|
|
167
|
+
// ============================================================================
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Adds priority fee instructions to a transaction.
|
|
171
|
+
* Simulates the transaction to determine compute units and calculates optimal fees.
|
|
172
|
+
*
|
|
173
|
+
* @param connection - Solana RPC connection
|
|
174
|
+
* @param transaction - The transaction to add priority fees to
|
|
175
|
+
* @param priorityFeeConfig - Configuration for priority fees
|
|
176
|
+
* @param verbose - Enable verbose logging
|
|
177
|
+
* @returns The transaction with priority fee instructions added
|
|
178
|
+
*/
|
|
179
|
+
export async function addPriorityFeeInstructions(
|
|
180
|
+
connection: Connection,
|
|
181
|
+
transaction: Transaction,
|
|
182
|
+
priorityFeeConfig?: PriorityFeeConfig,
|
|
183
|
+
verbose: boolean = false,
|
|
184
|
+
): Promise<Transaction> {
|
|
185
|
+
const computeBudgetIxFilter = (ix: TransactionInstruction) =>
|
|
186
|
+
ix.programId.toString() !== "ComputeBudget111111111111111111111111111111";
|
|
187
|
+
|
|
188
|
+
// Remove existing compute budget instructions if they were added by the SDK
|
|
189
|
+
transaction.instructions = transaction.instructions.filter(computeBudgetIxFilter);
|
|
190
|
+
|
|
191
|
+
const instructions = await createPriorityFeeInstructions(
|
|
192
|
+
connection,
|
|
193
|
+
transaction,
|
|
194
|
+
priorityFeeConfig,
|
|
195
|
+
verbose,
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
transaction.add(...instructions);
|
|
199
|
+
|
|
200
|
+
return transaction;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Creates priority fee instructions based on simulation and fee estimation.
|
|
205
|
+
*/
|
|
206
|
+
export async function createPriorityFeeInstructions(
|
|
207
|
+
connection: Connection,
|
|
208
|
+
transaction: Transaction | VersionedTransaction,
|
|
209
|
+
priorityFeeConfig?: PriorityFeeConfig,
|
|
210
|
+
verbose: boolean = false,
|
|
211
|
+
): Promise<TransactionInstruction[]> {
|
|
212
|
+
// Simulate to get compute units
|
|
213
|
+
const unitsUsed = await simulateAndGetComputeUnits(connection, transaction);
|
|
214
|
+
const unitBudget = Math.floor(unitsUsed * 1.2); // Budget in 20% headroom
|
|
215
|
+
|
|
216
|
+
const instructions: TransactionInstruction[] = [];
|
|
217
|
+
instructions.push(
|
|
218
|
+
ComputeBudgetProgram.setComputeUnitLimit({
|
|
219
|
+
units: unitBudget,
|
|
220
|
+
}),
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
// Calculate priority fee
|
|
224
|
+
const {
|
|
225
|
+
percentile = 0.9,
|
|
226
|
+
percentileMultiple = 1,
|
|
227
|
+
min = 100_000,
|
|
228
|
+
max = 100_000_000,
|
|
229
|
+
} = priorityFeeConfig ?? {};
|
|
230
|
+
|
|
231
|
+
const rpcProvider = determineRpcProvider(connection.rpcEndpoint);
|
|
232
|
+
const { fee, methodUsed } = await calculatePriorityFee(
|
|
233
|
+
connection,
|
|
234
|
+
transaction,
|
|
235
|
+
rpcProvider,
|
|
236
|
+
{ percentile, percentileMultiple, min, max },
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
if (verbose) {
|
|
240
|
+
const maxFeeInSol = (fee / 1e6 / LAMPORTS_PER_SOL) * unitBudget;
|
|
241
|
+
console.table({
|
|
242
|
+
"RPC Provider": rpcProvider,
|
|
243
|
+
"Method used": methodUsed,
|
|
244
|
+
"Percentile used": percentile,
|
|
245
|
+
"Multiple used": percentileMultiple,
|
|
246
|
+
"Compute budget": unitBudget,
|
|
247
|
+
"Priority fee": fee,
|
|
248
|
+
"Max fee in SOL": maxFeeInSol,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
instructions.push(
|
|
253
|
+
ComputeBudgetProgram.setComputeUnitPrice({ microLamports: fee }),
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
return instructions;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Simulates a transaction and returns the compute units consumed.
|
|
261
|
+
*/
|
|
262
|
+
async function simulateAndGetComputeUnits(
|
|
263
|
+
connection: Connection,
|
|
264
|
+
transaction: Transaction | VersionedTransaction,
|
|
265
|
+
): Promise<number> {
|
|
266
|
+
let unitsUsed = 200_000;
|
|
267
|
+
let simulationAttempts = 0;
|
|
268
|
+
|
|
269
|
+
simulationLoop: while (true) {
|
|
270
|
+
const response = await connection.simulateTransaction(transaction as Transaction);
|
|
271
|
+
|
|
272
|
+
if (response.value.err) {
|
|
273
|
+
if (checkKnownSimulationError(response.value)) {
|
|
274
|
+
// Number of attempts will be at most 5 for known errors
|
|
275
|
+
if (simulationAttempts < 5) {
|
|
276
|
+
simulationAttempts++;
|
|
277
|
+
await sleep(1000);
|
|
278
|
+
continue simulationLoop;
|
|
279
|
+
}
|
|
280
|
+
} else if (simulationAttempts < 3) {
|
|
281
|
+
// Number of attempts will be at most 3 for unknown errors
|
|
282
|
+
simulationAttempts++;
|
|
283
|
+
await sleep(1000);
|
|
284
|
+
continue simulationLoop;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Still failing after multiple attempts
|
|
288
|
+
throw new Error(
|
|
289
|
+
`Simulation failed: ${JSON.stringify(response.value.err)}\nLogs:\n${(
|
|
290
|
+
response.value.logs || []
|
|
291
|
+
).join("\n ")}`,
|
|
292
|
+
);
|
|
293
|
+
} else {
|
|
294
|
+
// Simulation was successful
|
|
295
|
+
if (response.value.unitsConsumed) {
|
|
296
|
+
unitsUsed = response.value.unitsConsumed;
|
|
297
|
+
}
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return unitsUsed;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Calculates the priority fee based on RPC provider and configuration.
|
|
307
|
+
*/
|
|
308
|
+
async function calculatePriorityFee(
|
|
309
|
+
connection: Connection,
|
|
310
|
+
transaction: Transaction | VersionedTransaction,
|
|
311
|
+
rpcProvider: SolanaRpcProvider,
|
|
312
|
+
config: Required<PriorityFeeConfig>,
|
|
313
|
+
): Promise<{ fee: number; methodUsed: "triton" | "default" | "minimum" }> {
|
|
314
|
+
const { percentile, percentileMultiple, min, max } = config;
|
|
315
|
+
|
|
316
|
+
if (rpcProvider === "triton") {
|
|
317
|
+
// Triton has an experimental RPC method that accepts a percentile parameter
|
|
318
|
+
// and usually gives more accurate fee numbers.
|
|
319
|
+
try {
|
|
320
|
+
const fee = await determinePriorityFeeTritonOne(
|
|
321
|
+
connection,
|
|
322
|
+
transaction,
|
|
323
|
+
percentile,
|
|
324
|
+
percentileMultiple,
|
|
325
|
+
min,
|
|
326
|
+
max,
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
return { fee, methodUsed: "triton" };
|
|
330
|
+
} catch (e) {
|
|
331
|
+
console.warn(`Failed to determine priority fee using Triton RPC:`, e);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
try {
|
|
336
|
+
// By default, use generic Solana RPC method
|
|
337
|
+
const fee = await determinePriorityFee(
|
|
338
|
+
connection,
|
|
339
|
+
transaction,
|
|
340
|
+
percentile,
|
|
341
|
+
percentileMultiple,
|
|
342
|
+
min,
|
|
343
|
+
max,
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
return { fee, methodUsed: "default" };
|
|
347
|
+
} catch (e) {
|
|
348
|
+
console.warn(`Failed to determine priority fee:`, e);
|
|
349
|
+
|
|
350
|
+
return { fee: min, methodUsed: "minimum" };
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// ============================================================================
|
|
355
|
+
// Helpers
|
|
356
|
+
// ============================================================================
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Checks response logs for known simulation errors that can be retried.
|
|
360
|
+
*/
|
|
361
|
+
function checkKnownSimulationError(response: SimulatedTransactionResponse): boolean {
|
|
362
|
+
const errors: Record<string, string> = {};
|
|
363
|
+
|
|
364
|
+
// This error occurs when the blockhash included in a transaction is not deemed to be valid
|
|
365
|
+
if (response.err === "BlockhashNotFound") {
|
|
366
|
+
errors["BlockhashNotFound"] =
|
|
367
|
+
"Blockhash not found during simulation. Trying again.";
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Check the response logs for any known errors
|
|
371
|
+
if (response.logs) {
|
|
372
|
+
for (const line of response.logs) {
|
|
373
|
+
if (line.includes("SlippageToleranceExceeded")) {
|
|
374
|
+
errors["SlippageToleranceExceeded"] =
|
|
375
|
+
"Slippage failure during simulation. Trying again.";
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (line.includes("RequireGteViolated")) {
|
|
379
|
+
errors["RequireGteViolated"] =
|
|
380
|
+
"Swap instruction failure during simulation. Trying again.";
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (Object.keys(errors).length === 0) {
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
console.table(errors);
|
|
390
|
+
return true;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Checks whether a hostname is exactly the given domain or a subdomain of it.
|
|
395
|
+
* e.g. isHostOrSubdomainOf("api.triton.one", "triton.one") => true
|
|
396
|
+
* isHostOrSubdomainOf("not-triton.com", "triton.one") => false
|
|
397
|
+
*/
|
|
398
|
+
function isHostOrSubdomainOf(hostname: string, base: string): boolean {
|
|
399
|
+
return hostname === base || hostname.endsWith(`.${base}`);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Determines the RPC provider from the endpoint URL.
|
|
404
|
+
*/
|
|
405
|
+
export function determineRpcProvider(endpoint: string): SolanaRpcProvider {
|
|
406
|
+
try {
|
|
407
|
+
const url = new URL(endpoint);
|
|
408
|
+
const hostname = url.hostname;
|
|
409
|
+
if (isHostOrSubdomainOf(hostname, "rpcpool.com") || isHostOrSubdomainOf(hostname, "triton.one")) {
|
|
410
|
+
return "triton";
|
|
411
|
+
} else if (isHostOrSubdomainOf(hostname, "helius-rpc.com") || isHostOrSubdomainOf(hostname, "helius.xyz")) {
|
|
412
|
+
return "helius";
|
|
413
|
+
} else if (isHostOrSubdomainOf(hostname, "ankr.com")) {
|
|
414
|
+
return "ankr";
|
|
415
|
+
} else {
|
|
416
|
+
return "unknown";
|
|
417
|
+
}
|
|
418
|
+
} catch {
|
|
419
|
+
return "unknown";
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Sleep for a specified duration.
|
|
425
|
+
*/
|
|
426
|
+
export async function sleep(timeout: number): Promise<void> {
|
|
427
|
+
return new Promise((resolve) => setTimeout(resolve, timeout));
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Checks whether an object is empty.
|
|
432
|
+
*/
|
|
433
|
+
export const isEmptyObject = (value: object | null | undefined): boolean => {
|
|
434
|
+
if (value === null || value === undefined) {
|
|
435
|
+
return true;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
for (const key in value) {
|
|
439
|
+
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return true;
|
|
445
|
+
};
|
|
446
|
+
|
|
@@ -35,25 +35,60 @@ export interface WormholeTransferRequest {
|
|
|
35
35
|
mainSigner: Account;
|
|
36
36
|
amount?: string;
|
|
37
37
|
sponsorAccount?: Account;
|
|
38
|
+
/** Optional callback fired before and after each individual transaction is signed. */
|
|
39
|
+
onTransactionSigned?: OnTransactionSigned;
|
|
38
40
|
}
|
|
39
41
|
|
|
42
|
+
export type WithdrawPhase =
|
|
43
|
+
| "initiating" // User signing Aptos burn transaction
|
|
44
|
+
| "tracking" // Waiting for Wormhole attestation (~60s)
|
|
45
|
+
| "claiming"; // Claiming on destination chain
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Callback fired before and after each individual transaction is signed
|
|
49
|
+
* and submitted during a bridge flow.
|
|
50
|
+
*
|
|
51
|
+
* @param description - A human-readable description of the transaction
|
|
52
|
+
* (e.g. "Approving USDC transfer"). Comes from the Wormhole SDK's
|
|
53
|
+
* `UnsignedTransaction.description`.
|
|
54
|
+
* @param txId - `null` when called *before* signing; the on-chain
|
|
55
|
+
* transaction hash when called *after* signing.
|
|
56
|
+
*/
|
|
57
|
+
export type OnTransactionSigned = (description: string, txId: string | null) => void;
|
|
58
|
+
|
|
40
59
|
export interface WormholeWithdrawRequest {
|
|
60
|
+
/**
|
|
61
|
+
* The non-Aptos chain involved in the withdrawal. For a withdrawal from
|
|
62
|
+
* Aptos → Solana, this is `"Solana"`.
|
|
63
|
+
*
|
|
64
|
+
* Note: despite the name, this is the *destination* of the bridge transfer
|
|
65
|
+
* (where USDC will be claimed), not the chain that burns USDC (which is
|
|
66
|
+
* always Aptos for withdrawals).
|
|
67
|
+
*/
|
|
41
68
|
sourceChain: Chain;
|
|
42
69
|
wallet: AdapterWallet;
|
|
43
70
|
destinationAddress: AccountAddressInput;
|
|
44
71
|
sponsorAccount?: Account | GasStationApiKey;
|
|
72
|
+
/** Optional callback fired when the withdraw progresses to a new phase. */
|
|
73
|
+
onPhaseChange?: (phase: WithdrawPhase) => void;
|
|
74
|
+
/** Optional callback fired before and after each individual transaction is signed. */
|
|
75
|
+
onTransactionSigned?: OnTransactionSigned;
|
|
45
76
|
}
|
|
46
77
|
|
|
47
78
|
export interface WormholeSubmitTransferRequest {
|
|
48
79
|
sourceChain: Chain;
|
|
49
80
|
wallet: AdapterWallet;
|
|
50
81
|
destinationAddress: AccountAddressInput;
|
|
82
|
+
/** Optional callback fired before and after each individual transaction is signed. */
|
|
83
|
+
onTransactionSigned?: OnTransactionSigned;
|
|
51
84
|
}
|
|
52
85
|
|
|
53
86
|
export interface WormholeClaimTransferRequest {
|
|
54
87
|
receipt: routes.Receipt<AttestationReceipt>;
|
|
55
88
|
mainSigner: AptosAccount;
|
|
56
89
|
sponsorAccount?: AptosAccount | GasStationApiKey;
|
|
90
|
+
/** Optional callback fired before and after each individual transaction is signed. */
|
|
91
|
+
onTransactionSigned?: OnTransactionSigned;
|
|
57
92
|
}
|
|
58
93
|
|
|
59
94
|
export interface WormholeTransferResponse {
|
|
@@ -70,3 +105,135 @@ export interface WormholeStartTransferResponse {
|
|
|
70
105
|
originChainTxnId: string;
|
|
71
106
|
receipt: routes.Receipt<AttestationReceipt>;
|
|
72
107
|
}
|
|
108
|
+
|
|
109
|
+
// --- Split withdraw flow types ---
|
|
110
|
+
|
|
111
|
+
export interface WormholeInitiateWithdrawRequest {
|
|
112
|
+
wallet: AdapterWallet;
|
|
113
|
+
destinationAddress: AccountAddressInput;
|
|
114
|
+
sponsorAccount?: Account | GasStationApiKey;
|
|
115
|
+
/** Optional callback fired before and after each individual transaction is signed. */
|
|
116
|
+
onTransactionSigned?: OnTransactionSigned;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface WormholeInitiateWithdrawResponse {
|
|
120
|
+
originChainTxnId: string;
|
|
121
|
+
receipt: routes.Receipt<AttestationReceipt>;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface WormholeClaimWithdrawRequest {
|
|
125
|
+
/**
|
|
126
|
+
* The chain on which the claim transaction will be executed (the destination
|
|
127
|
+
* chain of the withdrawal).
|
|
128
|
+
*
|
|
129
|
+
* For example, when withdrawing from Aptos → Solana, `claimChain` is
|
|
130
|
+
* `"Solana"` because that's where the USDC is minted/claimed.
|
|
131
|
+
*/
|
|
132
|
+
claimChain: Chain;
|
|
133
|
+
destinationAddress: string;
|
|
134
|
+
receipt: routes.Receipt<AttestationReceipt>;
|
|
135
|
+
// Required for wallet-based claim (non-Solana chains, or Solana without serverClaimUrl).
|
|
136
|
+
// Not needed when the SDK uses the configured serverClaimUrl for Solana claims.
|
|
137
|
+
wallet?: AdapterWallet;
|
|
138
|
+
/** Optional callback fired before and after each individual transaction is signed. */
|
|
139
|
+
onTransactionSigned?: OnTransactionSigned;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface WormholeClaimWithdrawResponse {
|
|
143
|
+
destinationChainTxnId: string;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface RetryWithdrawClaimRequest extends WormholeClaimWithdrawRequest {
|
|
147
|
+
/** Maximum number of retry attempts (default: 5). */
|
|
148
|
+
maxRetries?: number;
|
|
149
|
+
/** Initial delay in ms before the first retry (default: 2000). */
|
|
150
|
+
initialDelayMs?: number;
|
|
151
|
+
/** Multiplier applied to the delay after each failed attempt (default: 2). */
|
|
152
|
+
backoffMultiplier?: number;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export interface RetryWithdrawClaimResponse extends WormholeClaimWithdrawResponse {
|
|
156
|
+
/** Number of retry attempts that were needed (0 means first attempt succeeded). */
|
|
157
|
+
retriesUsed: number;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Validates that a value returned by `getExpireTimestamp` is a non-negative
|
|
162
|
+
* integer suitable for use as an epoch-second expiration timestamp.
|
|
163
|
+
* Throws immediately for NaN, Infinity, negative values, or floats so that
|
|
164
|
+
* misconfigured callbacks fail fast instead of producing silent misbehaviour.
|
|
165
|
+
*/
|
|
166
|
+
export function validateExpireTimestamp(value: number): void {
|
|
167
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
168
|
+
throw new Error(
|
|
169
|
+
`getExpireTimestamp returned an invalid value (${value}). ` +
|
|
170
|
+
"Expected a non-negative integer (epoch seconds).",
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Error thrown when the transfer (deposit) flow fails *after* the source-chain
|
|
177
|
+
* burn transaction has already been submitted (i.e. during attestation tracking
|
|
178
|
+
* or Aptos claiming).
|
|
179
|
+
*
|
|
180
|
+
* Consumers should check `instanceof TransferError` in their catch block
|
|
181
|
+
* to recover the `originChainTxnId` and display an explorer link so the
|
|
182
|
+
* user can verify their burn on-chain.
|
|
183
|
+
*/
|
|
184
|
+
export class TransferError extends Error {
|
|
185
|
+
/** Source-chain burn transaction hash — available when the burn succeeded before the failure. */
|
|
186
|
+
readonly originChainTxnId: string;
|
|
187
|
+
/**
|
|
188
|
+
* The underlying error that caused this failure.
|
|
189
|
+
* Mirrors ES2022 Error.cause — declared explicitly because the project's
|
|
190
|
+
* TypeScript lib target does not include ES2022 ErrorOptions.
|
|
191
|
+
*/
|
|
192
|
+
readonly cause?: unknown;
|
|
193
|
+
|
|
194
|
+
constructor(
|
|
195
|
+
message: string,
|
|
196
|
+
originChainTxnId: string,
|
|
197
|
+
cause?: unknown,
|
|
198
|
+
) {
|
|
199
|
+
super(message);
|
|
200
|
+
this.name = "TransferError";
|
|
201
|
+
this.originChainTxnId = originChainTxnId;
|
|
202
|
+
this.cause = cause;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Error thrown when the withdraw flow fails *after* the Aptos burn
|
|
208
|
+
* transaction has already been submitted (i.e. during attestation tracking
|
|
209
|
+
* or destination-chain claiming).
|
|
210
|
+
*
|
|
211
|
+
* Consumers should check `instanceof WithdrawError` in their catch block
|
|
212
|
+
* to recover the `originChainTxnId` and display an explorer link so the
|
|
213
|
+
* user can verify their burn on-chain.
|
|
214
|
+
*/
|
|
215
|
+
export class WithdrawError extends Error {
|
|
216
|
+
/** Aptos burn transaction hash — always available when this error is thrown. */
|
|
217
|
+
readonly originChainTxnId: string;
|
|
218
|
+
/** The withdraw phase that failed ("tracking" or "claiming"). */
|
|
219
|
+
readonly phase: WithdrawPhase;
|
|
220
|
+
/**
|
|
221
|
+
* The underlying error that caused this failure.
|
|
222
|
+
* Mirrors ES2022 Error.cause — declared explicitly because the project's
|
|
223
|
+
* TypeScript lib target does not include ES2022 ErrorOptions.
|
|
224
|
+
*/
|
|
225
|
+
readonly cause?: unknown;
|
|
226
|
+
|
|
227
|
+
constructor(
|
|
228
|
+
message: string,
|
|
229
|
+
originChainTxnId: string,
|
|
230
|
+
phase: WithdrawPhase,
|
|
231
|
+
cause?: unknown,
|
|
232
|
+
) {
|
|
233
|
+
super(message);
|
|
234
|
+
this.name = "WithdrawError";
|
|
235
|
+
this.originChainTxnId = originChainTxnId;
|
|
236
|
+
this.phase = phase;
|
|
237
|
+
this.cause = cause;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
@@ -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
|
+
|