@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.
Files changed (52) hide show
  1. package/README.md +26 -0
  2. package/dist/CrossChainCore.d.ts +95 -0
  3. package/dist/CrossChainCore.d.ts.map +1 -1
  4. package/dist/index.d.ts +1 -0
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +908 -404
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +914 -409
  9. package/dist/index.mjs.map +1 -1
  10. package/dist/providers/wormhole/index.d.ts +2 -0
  11. package/dist/providers/wormhole/index.d.ts.map +1 -1
  12. package/dist/providers/wormhole/signers/AptosLocalSigner.d.ts +9 -7
  13. package/dist/providers/wormhole/signers/AptosLocalSigner.d.ts.map +1 -1
  14. package/dist/providers/wormhole/signers/AptosSigner.d.ts +2 -1
  15. package/dist/providers/wormhole/signers/AptosSigner.d.ts.map +1 -1
  16. package/dist/providers/wormhole/signers/EthereumSigner.d.ts +1 -1
  17. package/dist/providers/wormhole/signers/EthereumSigner.d.ts.map +1 -1
  18. package/dist/providers/wormhole/signers/Signer.d.ts +11 -3
  19. package/dist/providers/wormhole/signers/Signer.d.ts.map +1 -1
  20. package/dist/providers/wormhole/signers/SolanaLocalSigner.d.ts +69 -0
  21. package/dist/providers/wormhole/signers/SolanaLocalSigner.d.ts.map +1 -0
  22. package/dist/providers/wormhole/signers/SolanaSigner.d.ts +12 -20
  23. package/dist/providers/wormhole/signers/SolanaSigner.d.ts.map +1 -1
  24. package/dist/providers/wormhole/signers/solanaUtils.d.ts +68 -0
  25. package/dist/providers/wormhole/signers/solanaUtils.d.ts.map +1 -0
  26. package/dist/providers/wormhole/types.d.ts +120 -0
  27. package/dist/providers/wormhole/types.d.ts.map +1 -1
  28. package/dist/providers/wormhole/utils.d.ts +26 -0
  29. package/dist/providers/wormhole/utils.d.ts.map +1 -0
  30. package/dist/providers/wormhole/wormhole.d.ts +62 -6
  31. package/dist/providers/wormhole/wormhole.d.ts.map +1 -1
  32. package/dist/utils/receiptSerialization.d.ts +38 -0
  33. package/dist/utils/receiptSerialization.d.ts.map +1 -0
  34. package/dist/version.d.ts +1 -1
  35. package/package.json +3 -3
  36. package/src/CrossChainCore.ts +110 -3
  37. package/src/config/mainnet/chains.ts +2 -2
  38. package/src/config/testnet/chains.ts +2 -2
  39. package/src/index.ts +1 -0
  40. package/src/providers/wormhole/index.ts +2 -0
  41. package/src/providers/wormhole/signers/AptosLocalSigner.ts +31 -18
  42. package/src/providers/wormhole/signers/AptosSigner.ts +11 -2
  43. package/src/providers/wormhole/signers/EthereumSigner.ts +59 -8
  44. package/src/providers/wormhole/signers/Signer.ts +23 -6
  45. package/src/providers/wormhole/signers/SolanaLocalSigner.ts +250 -0
  46. package/src/providers/wormhole/signers/SolanaSigner.ts +49 -338
  47. package/src/providers/wormhole/signers/solanaUtils.ts +446 -0
  48. package/src/providers/wormhole/types.ts +167 -0
  49. package/src/providers/wormhole/utils.ts +72 -0
  50. package/src/providers/wormhole/wormhole.ts +309 -137
  51. package/src/utils/receiptSerialization.ts +141 -0
  52. package/src/version.ts +1 -1
@@ -1,7 +1,5 @@
1
1
  import {
2
- chainToPlatform,
3
2
  routes,
4
- TokenId,
5
3
  Wormhole,
6
4
  wormhole,
7
5
  PlatformLoader,
@@ -17,11 +15,15 @@ import {
17
15
  Chain,
18
16
  CrossChainProvider,
19
17
  CrossChainCore,
18
+ EVM_CHAIN_NAMES,
20
19
  } from "../../CrossChainCore";
21
20
  import { logger } from "../../utils/logger";
21
+ import { serializeReceipt } from "../../utils/receiptSerialization";
22
22
  import { AptosLocalSigner } from "./signers/AptosLocalSigner";
23
+ import { isAccount } from "./signers/AptosSigner";
23
24
  import { Signer } from "./signers/Signer";
24
25
  import { ChainConfig } from "../../config";
26
+ import { createCCTPRoute } from "./utils";
25
27
  import {
26
28
  WormholeQuoteRequest,
27
29
  WormholeQuoteResponse,
@@ -34,6 +36,14 @@ import {
34
36
  WormholeClaimTransferRequest,
35
37
  WormholeWithdrawRequest,
36
38
  WormholeWithdrawResponse,
39
+ WormholeInitiateWithdrawRequest,
40
+ WormholeInitiateWithdrawResponse,
41
+ WormholeClaimWithdrawRequest,
42
+ WormholeClaimWithdrawResponse,
43
+ RetryWithdrawClaimRequest,
44
+ RetryWithdrawClaimResponse,
45
+ WithdrawError,
46
+ TransferError,
37
47
  } from "./types";
38
48
  import { SolanaDerivedWallet } from "@aptos-labs/derived-wallet-solana";
39
49
  import { EIP1193DerivedWallet } from "@aptos-labs/derived-wallet-ethereum";
@@ -54,6 +64,7 @@ export class WormholeProvider implements CrossChainProvider<
54
64
  private wormholeRoute: WormholeRouteResponse | undefined;
55
65
  private wormholeRequest: WormholeRequest | undefined;
56
66
  private wormholeQuote: WormholeQuoteResponse | undefined;
67
+ private destinationChain?: Chain;
57
68
 
58
69
  constructor(core: CrossChainCore) {
59
70
  this.crossChainCore = core;
@@ -74,16 +85,31 @@ export class WormholeProvider implements CrossChainProvider<
74
85
  const isMainnet = dappNetwork === Network.MAINNET;
75
86
  const platforms: PlatformLoader<any>[] = [aptos, solana, evm, sui];
76
87
 
77
- // Get custom RPC endpoints from config
88
+ // RPC resolution order per chain:
89
+ // 1. User-provided config (solanaConfig / suiConfig / evmConfig)
90
+ // 2. Built-in defaultRpc from CHAINS config
91
+ const dappConfig = this.crossChainCore._dappConfig;
92
+ const chains = this.crossChainCore.CHAINS;
93
+
78
94
  const solanaRpc =
79
- this.crossChainCore._dappConfig?.solanaConfig?.rpc ??
80
- this.crossChainCore.CHAINS["Solana"]?.defaultRpc;
95
+ dappConfig?.solanaConfig?.rpc ?? chains["Solana"]?.defaultRpc;
96
+
97
+ const suiRpc = dappConfig?.suiConfig?.rpc ?? chains["Sui"]?.defaultRpc;
98
+
99
+ const evmChainsConfig: Record<string, { rpc: string }> = {};
100
+ for (const name of EVM_CHAIN_NAMES) {
101
+ const rpc =
102
+ dappConfig?.evmConfig?.[name]?.rpc ?? chains[name]?.defaultRpc;
103
+ if (rpc) {
104
+ evmChainsConfig[name] = { rpc };
105
+ }
106
+ }
81
107
 
82
108
  const wh = await wormhole(isMainnet ? "Mainnet" : "Testnet", platforms, {
83
109
  chains: {
84
- Solana: {
85
- rpc: solanaRpc,
86
- },
110
+ ...(solanaRpc ? { Solana: { rpc: solanaRpc } } : {}),
111
+ ...(suiRpc ? { Sui: { rpc: suiRpc } } : {}),
112
+ ...evmChainsConfig,
87
113
  },
88
114
  });
89
115
  this._wormholeContext = wh;
@@ -100,43 +126,16 @@ export class WormholeProvider implements CrossChainProvider<
100
126
  throw new Error("Wormhole context not initialized");
101
127
  }
102
128
 
103
- const { sourceToken, destToken } = this.getTokenInfo(
129
+ const { route: cctpRoute, request } = await createCCTPRoute(
130
+ this._wormholeContext,
104
131
  sourceChain,
105
132
  destinationChain,
133
+ this.crossChainCore.TOKENS,
106
134
  );
107
135
 
108
- const destContext = this._wormholeContext
109
- .getPlatform(chainToPlatform(destinationChain))
110
- .getChain(destinationChain);
111
- const sourceContext = this._wormholeContext
112
- .getPlatform(chainToPlatform(sourceChain))
113
- .getChain(sourceChain);
114
-
115
- logger.log("sourceContext", sourceContext);
116
- logger.log("sourceToken", sourceToken);
117
-
118
- logger.log("destContext", destContext);
119
- logger.log("destToken", destToken);
120
-
121
- const request = await routes.RouteTransferRequest.create(
122
- this._wormholeContext,
123
- {
124
- source: sourceToken,
125
- destination: destToken,
126
- },
127
- sourceContext,
128
- destContext,
129
- );
130
-
131
- const resolver = this._wormholeContext.resolver([
132
- routes.CCTPRoute, // manual CCTP
133
- ]);
134
-
135
- const route = await resolver.findRoutes(request);
136
- const cctpRoute = route[0];
137
-
138
136
  this.wormholeRoute = cctpRoute;
139
137
  this.wormholeRequest = request;
138
+ this.destinationChain = destinationChain;
140
139
 
141
140
  return { route: cctpRoute, request };
142
141
  }
@@ -168,12 +167,12 @@ export class WormholeProvider implements CrossChainProvider<
168
167
  const validated = await route.validate(request, transferParams);
169
168
  if (!validated.valid) {
170
169
  logger.log("invalid", validated.valid);
171
- throw new Error(`Invalid quote: ${validated.error}`).message;
170
+ throw new Error(`Invalid quote: ${validated.error}`);
172
171
  }
173
172
  const quote = await route.quote(request, validated.params);
174
173
  if (!quote.success) {
175
174
  logger.log("quote failed", quote.success);
176
- throw new Error(`Invalid quote: ${quote.error}`).message;
175
+ throw new Error(`Invalid quote: ${quote.error}`);
177
176
  }
178
177
  this.wormholeQuote = quote;
179
178
  logger.log("quote", quote);
@@ -183,7 +182,8 @@ export class WormholeProvider implements CrossChainProvider<
183
182
  async submitCCTPTransfer(
184
183
  input: WormholeSubmitTransferRequest,
185
184
  ): Promise<WormholeStartTransferResponse> {
186
- const { sourceChain, wallet, destinationAddress } = input;
185
+ const { sourceChain, wallet, destinationAddress, onTransactionSigned } =
186
+ input;
187
187
 
188
188
  if (!this._wormholeContext) {
189
189
  await this.setWormholeContext(sourceChain);
@@ -222,6 +222,8 @@ export class WormholeProvider implements CrossChainProvider<
222
222
  {},
223
223
  wallet,
224
224
  this.crossChainCore,
225
+ undefined,
226
+ onTransactionSigned,
225
227
  );
226
228
 
227
229
  logger.log("signer", signer);
@@ -246,10 +248,18 @@ export class WormholeProvider implements CrossChainProvider<
246
248
  async claimCCTPTransfer(
247
249
  input: WormholeClaimTransferRequest,
248
250
  ): Promise<{ destinationChainTxnId: string }> {
249
- let { receipt, mainSigner, sponsorAccount } = input;
251
+ let { receipt, mainSigner, sponsorAccount, onTransactionSigned } = input;
250
252
  if (!this.wormholeRoute) {
251
253
  throw new Error("Wormhole route not initialized");
252
254
  }
255
+ if (sponsorAccount && !isAccount(sponsorAccount)) {
256
+ throw new Error(
257
+ "AptosLocalSigner does not support GasStationApiKey as a sponsor account. " +
258
+ "Wormhole claim transactions are script-based and cannot be submitted " +
259
+ "via the gas station. Please provide an Account instance as the sponsor, " +
260
+ "or omit the sponsor account.",
261
+ );
262
+ }
253
263
 
254
264
  logger.log("mainSigner", mainSigner.accountAddress.toString());
255
265
 
@@ -269,7 +279,8 @@ export class WormholeProvider implements CrossChainProvider<
269
279
  {},
270
280
  mainSigner, // the account that signs the "claim" transaction
271
281
  sponsorAccount, // the fee payer account
272
- this.crossChainCore._dappConfig?.aptosNetwork,
282
+ this.crossChainCore,
283
+ onTransactionSigned,
273
284
  );
274
285
 
275
286
  if (routes.isManual(this.wormholeRoute)) {
@@ -325,26 +336,36 @@ export class WormholeProvider implements CrossChainProvider<
325
336
  // Submit transfer transaction from origin chain
326
337
  let { originChainTxnId, receipt } = await this.submitCCTPTransfer(input);
327
338
  // Claim transfer transaction on destination chain
328
- const { destinationChainTxnId } = await this.claimCCTPTransfer({
329
- receipt,
330
- mainSigner: input.mainSigner,
331
- sponsorAccount: input.sponsorAccount,
332
- });
333
- return { originChainTxnId, destinationChainTxnId };
339
+ try {
340
+ const { destinationChainTxnId } = await this.claimCCTPTransfer({
341
+ receipt,
342
+ mainSigner: input.mainSigner,
343
+ sponsorAccount: input.sponsorAccount,
344
+ onTransactionSigned: input.onTransactionSigned,
345
+ });
346
+ return { originChainTxnId, destinationChainTxnId };
347
+ } catch (error: any) {
348
+ throw new TransferError(
349
+ error?.message ?? "Transfer claim failed after source-chain burn",
350
+ originChainTxnId,
351
+ error,
352
+ );
353
+ }
334
354
  }
335
355
 
336
- async withdraw(
337
- input: WormholeWithdrawRequest,
338
- ): Promise<WormholeWithdrawResponse> {
339
- const { sourceChain, wallet, destinationAddress, sponsorAccount } = input;
340
- logger.log("sourceChain", sourceChain);
341
- logger.log("wallet", wallet);
342
- logger.log("destinationAddress", destinationAddress);
343
- logger.log("sponsorAccount", sponsorAccount);
356
+ // --- Split withdraw flow: initiateWithdraw + trackWithdraw + claimWithdraw ---
357
+
358
+ /**
359
+ * Phase 1: Initiates a withdraw by burning USDC on Aptos.
360
+ * The user signs the Aptos burn transaction via their wallet.
361
+ * Returns a receipt that can be tracked and later claimed.
362
+ */
363
+ async initiateWithdraw(
364
+ input: WormholeInitiateWithdrawRequest,
365
+ ): Promise<WormholeInitiateWithdrawResponse> {
366
+ const { wallet, destinationAddress, sponsorAccount, onTransactionSigned } =
367
+ input;
344
368
 
345
- if (!this._wormholeContext) {
346
- await this.setWormholeContext(sourceChain);
347
- }
348
369
  if (!this._wormholeContext) {
349
370
  throw new Error("Wormhole context not initialized");
350
371
  }
@@ -354,96 +375,267 @@ export class WormholeProvider implements CrossChainProvider<
354
375
 
355
376
  const signer = new Signer(
356
377
  this.getChainConfig("Aptos"),
357
- (
358
- await input.wallet.features["aptos:account"].account()
359
- ).address.toString(),
378
+ (await wallet.features["aptos:account"].account()).address.toString(),
360
379
  {},
361
- input.wallet,
380
+ wallet,
362
381
  this.crossChainCore,
363
382
  sponsorAccount,
383
+ onTransactionSigned,
364
384
  );
365
385
 
366
- logger.log("signer", signer);
367
- logger.log("wormholeRequest", this.wormholeRequest);
368
- logger.log("wormholeQuote", this.wormholeQuote);
369
- logger.log(
370
- "Wormhole.chainAddress",
371
- Wormhole.chainAddress(sourceChain, input.destinationAddress.toString()),
386
+ const wormholeDestAddress = Wormhole.chainAddress(
387
+ this.destinationChain!,
388
+ destinationAddress.toString(),
372
389
  );
373
390
 
374
- let receipt = await this.wormholeRoute.initiate(
391
+ const receipt = await this.wormholeRoute.initiate(
375
392
  this.wormholeRequest,
376
393
  signer,
377
394
  this.wormholeQuote,
378
- Wormhole.chainAddress(sourceChain, input.destinationAddress.toString()),
395
+ wormholeDestAddress,
379
396
  );
380
- logger.log("receipt", receipt);
397
+ logger.log("initiateWithdraw receipt", receipt);
381
398
 
382
399
  const originChainTxnId =
383
400
  "originTxs" in receipt
384
401
  ? receipt.originTxs[receipt.originTxs.length - 1].txid
385
402
  : undefined;
386
403
 
404
+ return { originChainTxnId: originChainTxnId || "", receipt };
405
+ }
406
+
407
+ /**
408
+ * Phase 2: Tracks a withdraw receipt until attestation is ready.
409
+ * This polls Wormhole and returns once the receipt reaches the Attested state.
410
+ */
411
+ async trackWithdraw(receipt: routes.Receipt): Promise<routes.Receipt> {
412
+ if (!this.wormholeRoute) {
413
+ throw new Error("Wormhole route not initialized");
414
+ }
415
+
387
416
  let retries = 0;
388
417
  const maxRetries = 5;
389
- const baseDelay = 1000; // Initial delay of 1 second
418
+ const baseDelay = 1000;
390
419
 
391
420
  while (retries < maxRetries) {
392
421
  try {
393
422
  for await (receipt of this.wormholeRoute.track(receipt, 120 * 1000)) {
394
- if (receipt.state >= TransferState.SourceInitiated) {
395
- logger.log("Receipt is on track ", receipt);
396
-
397
- try {
398
- const signer = new Signer(
399
- this.getChainConfig(sourceChain),
400
- destinationAddress.toString(),
401
- {},
402
- wallet,
403
- this.crossChainCore,
404
- );
405
-
406
- if (routes.isManual(this.wormholeRoute)) {
407
- const circleAttestationReceipt =
408
- await this.wormholeRoute.complete(signer, receipt);
409
- logger.log("Claim receipt: ", circleAttestationReceipt);
410
-
411
- const destinationChainTxnId = signer.claimedTransactionHashes();
412
- return {
413
- originChainTxnId: originChainTxnId || "",
414
- destinationChainTxnId,
415
- };
416
- } else {
417
- // Should be unreachable
418
- return {
419
- originChainTxnId: originChainTxnId || "",
420
- destinationChainTxnId: "",
421
- };
422
- }
423
- } catch (e) {
424
- console.error("Failed to claim", e);
425
- return {
426
- originChainTxnId: originChainTxnId || "",
427
- destinationChainTxnId: "",
428
- };
429
- }
423
+ if (receipt.state >= TransferState.Attested) {
424
+ logger.log("trackWithdraw: receipt attested", receipt);
425
+ return receipt;
430
426
  }
431
427
  }
432
428
  } catch (e) {
433
429
  console.error(
434
- `Error tracking transfer (attempt ${retries + 1} / ${maxRetries}):`,
430
+ `Error tracking withdraw (attempt ${retries + 1} / ${maxRetries}):`,
435
431
  e,
436
432
  );
437
- const delay = baseDelay * Math.pow(2, retries); // Exponential backoff
433
+ const delay = baseDelay * Math.pow(2, retries);
438
434
  await sleep(delay);
439
435
  retries++;
440
436
  }
441
437
  }
438
+ throw new Error("Failed to track withdraw to attested state");
439
+ }
442
440
 
443
- return {
444
- originChainTxnId: originChainTxnId || "",
445
- destinationChainTxnId: "",
446
- };
441
+ /**
442
+ * Phase 3: Claims the withdraw on the destination chain.
443
+ *
444
+ * If the destination is Solana and `solanaConfig.serverClaimUrl` is configured
445
+ * in the dapp config, the SDK automatically POSTs the attested receipt to that
446
+ * URL — no wallet popup required. The dapp's server endpoint handles signing
447
+ * and submitting the claim transaction.
448
+ *
449
+ * Otherwise falls back to the wallet-based Signer (triggers wallet popup).
450
+ */
451
+ async claimWithdraw(
452
+ input: WormholeClaimWithdrawRequest,
453
+ ): Promise<WormholeClaimWithdrawResponse> {
454
+ const { claimChain, destinationAddress, receipt } = input;
455
+
456
+ // Server-side claim path: Solana destination with configured serverClaimUrl
457
+ const serverClaimUrl =
458
+ this.crossChainCore._dappConfig?.solanaConfig?.serverClaimUrl;
459
+
460
+ if (claimChain === "Solana" && serverClaimUrl) {
461
+ logger.log("claimWithdraw: using server-side claim via", serverClaimUrl);
462
+
463
+ const response = await fetch(serverClaimUrl, {
464
+ method: "POST",
465
+ headers: { "Content-Type": "application/json" },
466
+ body: JSON.stringify({
467
+ receipt: serializeReceipt(receipt),
468
+ destinationAddress,
469
+ claimChain,
470
+ }),
471
+ });
472
+
473
+ if (!response.ok) {
474
+ const errorData = await response.json().catch(() => ({}));
475
+ throw new Error(
476
+ errorData.error ||
477
+ `Server-side claim failed with status ${response.status}`,
478
+ );
479
+ }
480
+
481
+ const result = await response.json();
482
+ return { destinationChainTxnId: result.destinationChainTxnId };
483
+ }
484
+
485
+ // Wallet-based claim path
486
+ if (!this.wormholeRoute) {
487
+ throw new Error("Wormhole route not initialized");
488
+ }
489
+
490
+ if (!input.wallet) {
491
+ throw new Error(
492
+ "Wallet is required for claim when serverClaimUrl is not configured",
493
+ );
494
+ }
495
+
496
+ const claimSigner = new Signer(
497
+ this.getChainConfig(claimChain),
498
+ destinationAddress,
499
+ {},
500
+ input.wallet,
501
+ this.crossChainCore,
502
+ undefined,
503
+ input.onTransactionSigned,
504
+ false,
505
+ );
506
+
507
+ if (routes.isManual(this.wormholeRoute)) {
508
+ const circleAttestationReceipt = await this.wormholeRoute.complete(
509
+ claimSigner,
510
+ receipt,
511
+ );
512
+ logger.log("claimWithdraw receipt:", circleAttestationReceipt);
513
+ const destinationChainTxnId = claimSigner.claimedTransactionHashes();
514
+ return { destinationChainTxnId };
515
+ } else {
516
+ throw new Error("Automatic route not supported for manual claim");
517
+ }
518
+ }
519
+
520
+ /**
521
+ * Withdraws USDC from Aptos to a destination chain.
522
+ * Orchestrates all three phases internally:
523
+ * 1. Initiate — user signs the Aptos burn transaction
524
+ * 2. Track — wait for Wormhole attestation
525
+ * 3. Claim — if serverClaimUrl is configured for Solana, delegates to
526
+ * the server; otherwise uses the wallet-based signer.
527
+ *
528
+ * The optional `onPhaseChange` callback lets the dapp update its UI
529
+ * as the flow progresses.
530
+ */
531
+ async withdraw(
532
+ input: WormholeWithdrawRequest,
533
+ ): Promise<WormholeWithdrawResponse> {
534
+ const {
535
+ sourceChain,
536
+ wallet,
537
+ destinationAddress,
538
+ sponsorAccount,
539
+ onPhaseChange,
540
+ onTransactionSigned,
541
+ } = input;
542
+
543
+ // Phase 1: Initiate — user signs Aptos burn
544
+ onPhaseChange?.("initiating");
545
+ const { originChainTxnId, receipt } = await this.initiateWithdraw({
546
+ wallet,
547
+ destinationAddress,
548
+ sponsorAccount,
549
+ onTransactionSigned,
550
+ });
551
+
552
+ // Phases 2 & 3 are wrapped so that, if they fail, the caller still
553
+ // receives the originChainTxnId (the irreversible Aptos burn).
554
+ let currentPhase: "tracking" | "claiming" = "tracking";
555
+ try {
556
+ // Phase 2: Track — wait for attestation
557
+ onPhaseChange?.("tracking");
558
+ const attestedReceipt = await this.trackWithdraw(receipt);
559
+
560
+ // Phase 3: Claim — server-side or wallet-based
561
+ currentPhase = "claiming";
562
+ onPhaseChange?.("claiming");
563
+ const { destinationChainTxnId } = await this.claimWithdraw({
564
+ claimChain: sourceChain,
565
+ destinationAddress: destinationAddress.toString(),
566
+ receipt: attestedReceipt,
567
+ wallet,
568
+ onTransactionSigned,
569
+ });
570
+
571
+ return { originChainTxnId, destinationChainTxnId };
572
+ } catch (error: any) {
573
+ throw new WithdrawError(
574
+ error?.message ?? "Withdraw failed after Aptos burn",
575
+ originChainTxnId,
576
+ currentPhase,
577
+ error,
578
+ );
579
+ }
580
+ }
581
+
582
+ /**
583
+ * Retries a failed `claimWithdraw` call with configurable exponential backoff.
584
+ *
585
+ * Use this when the claim phase of a withdrawal fails (e.g., RPC instability,
586
+ * network congestion) but the Aptos burn transaction has already been submitted.
587
+ * The attested receipt can be recovered from the `WithdrawError` thrown by
588
+ * `withdraw()` and passed directly to this method.
589
+ *
590
+ * @example
591
+ * ```ts
592
+ * try {
593
+ * await provider.withdraw({ ... });
594
+ * } catch (error) {
595
+ * if (error instanceof WithdrawError && error.phase === "claiming") {
596
+ * const result = await provider.retryWithdrawClaim({
597
+ * sourceChain,
598
+ * destinationAddress,
599
+ * receipt: attestedReceipt,
600
+ * wallet,
601
+ * maxRetries: 5,
602
+ * });
603
+ * }
604
+ * }
605
+ * ```
606
+ */
607
+ async retryWithdrawClaim(
608
+ input: RetryWithdrawClaimRequest,
609
+ ): Promise<RetryWithdrawClaimResponse> {
610
+ const {
611
+ maxRetries = 5,
612
+ initialDelayMs = 2000,
613
+ backoffMultiplier = 2,
614
+ ...claimInput
615
+ } = input;
616
+
617
+ let lastError: Error | undefined;
618
+ let delay = initialDelayMs;
619
+
620
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
621
+ try {
622
+ const result = await this.claimWithdraw(claimInput);
623
+ return { ...result, retriesUsed: attempt };
624
+ } catch (error) {
625
+ lastError = error as Error;
626
+ logger.log(
627
+ `retryWithdrawClaim: attempt ${attempt + 1}/${maxRetries + 1} failed: ${lastError.message}`,
628
+ );
629
+ if (attempt < maxRetries) {
630
+ await sleep(delay);
631
+ delay *= backoffMultiplier;
632
+ }
633
+ }
634
+ }
635
+
636
+ throw new Error(
637
+ `Claim failed after ${maxRetries + 1} attempts: ${lastError?.message}`,
638
+ );
447
639
  }
448
640
 
449
641
  getChainConfig(chain: Chain): ChainConfig {
@@ -456,24 +648,4 @@ export class WormholeProvider implements CrossChainProvider<
456
648
  }
457
649
  return chainConfig;
458
650
  }
459
-
460
- getTokenInfo(
461
- sourceChain: Chain,
462
- destinationChain: Chain,
463
- ): {
464
- sourceToken: TokenId;
465
- destToken: TokenId;
466
- } {
467
- const sourceToken: TokenId = Wormhole.tokenId(
468
- this.crossChainCore.TOKENS[sourceChain].tokenId.chain as Chain,
469
- this.crossChainCore.TOKENS[sourceChain].tokenId.address,
470
- );
471
-
472
- const destToken: TokenId = Wormhole.tokenId(
473
- this.crossChainCore.TOKENS[destinationChain].tokenId.chain as Chain,
474
- this.crossChainCore.TOKENS[destinationChain].tokenId.address,
475
- );
476
-
477
- return { sourceToken, destToken };
478
- }
479
651
  }