@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
|
@@ -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
|
-
//
|
|
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
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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 {
|
|
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}`)
|
|
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}`)
|
|
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 } =
|
|
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
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
-
|
|
380
|
+
wallet,
|
|
362
381
|
this.crossChainCore,
|
|
363
382
|
sponsorAccount,
|
|
383
|
+
onTransactionSigned,
|
|
364
384
|
);
|
|
365
385
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
-
|
|
391
|
+
const receipt = await this.wormholeRoute.initiate(
|
|
375
392
|
this.wormholeRequest,
|
|
376
393
|
signer,
|
|
377
394
|
this.wormholeQuote,
|
|
378
|
-
|
|
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;
|
|
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.
|
|
395
|
-
logger.log("
|
|
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
|
|
430
|
+
`Error tracking withdraw (attempt ${retries + 1} / ${maxRetries}):`,
|
|
435
431
|
e,
|
|
436
432
|
);
|
|
437
|
-
const delay = baseDelay * Math.pow(2, retries);
|
|
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
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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
|
}
|