@aptos-labs/cross-chain-core 5.9.0 → 6.0.1

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 (35) hide show
  1. package/dist/CrossChainCore.d.ts +75 -0
  2. package/dist/CrossChainCore.d.ts.map +1 -1
  3. package/dist/index.js +360 -161
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +361 -165
  6. package/dist/index.mjs.map +1 -1
  7. package/dist/providers/wormhole/signers/AptosLocalSigner.d.ts +8 -6
  8. package/dist/providers/wormhole/signers/AptosLocalSigner.d.ts.map +1 -1
  9. package/dist/providers/wormhole/signers/AptosSigner.d.ts +2 -1
  10. package/dist/providers/wormhole/signers/AptosSigner.d.ts.map +1 -1
  11. package/dist/providers/wormhole/signers/EthereumSigner.d.ts +1 -1
  12. package/dist/providers/wormhole/signers/EthereumSigner.d.ts.map +1 -1
  13. package/dist/providers/wormhole/signers/Signer.d.ts +10 -2
  14. package/dist/providers/wormhole/signers/Signer.d.ts.map +1 -1
  15. package/dist/providers/wormhole/signers/SolanaLocalSigner.d.ts +4 -0
  16. package/dist/providers/wormhole/signers/SolanaLocalSigner.d.ts.map +1 -1
  17. package/dist/providers/wormhole/signers/SolanaSigner.d.ts.map +1 -1
  18. package/dist/providers/wormhole/signers/solanaUtils.d.ts.map +1 -1
  19. package/dist/providers/wormhole/types.d.ts +78 -1
  20. package/dist/providers/wormhole/types.d.ts.map +1 -1
  21. package/dist/providers/wormhole/wormhole.d.ts +27 -1
  22. package/dist/providers/wormhole/wormhole.d.ts.map +1 -1
  23. package/dist/version.d.ts +1 -1
  24. package/package.json +5 -5
  25. package/src/CrossChainCore.ts +91 -4
  26. package/src/providers/wormhole/signers/AptosLocalSigner.ts +27 -14
  27. package/src/providers/wormhole/signers/AptosSigner.ts +10 -1
  28. package/src/providers/wormhole/signers/EthereumSigner.ts +57 -6
  29. package/src/providers/wormhole/signers/Signer.ts +19 -2
  30. package/src/providers/wormhole/signers/SolanaLocalSigner.ts +7 -0
  31. package/src/providers/wormhole/signers/SolanaSigner.ts +4 -1
  32. package/src/providers/wormhole/signers/solanaUtils.ts +43 -19
  33. package/src/providers/wormhole/types.ts +100 -1
  34. package/src/providers/wormhole/wormhole.ts +138 -28
  35. package/src/version.ts +1 -1
@@ -16,15 +16,17 @@ import {
16
16
  AptosChains,
17
17
  AptosUnsignedTransaction,
18
18
  } from "@wormhole-foundation/sdk-aptos";
19
- import { GasStationApiKey } from "..";
19
+ import { GasStationApiKey, validateExpireTimestamp } from "..";
20
20
  import { UserResponseStatus } from "@aptos-labs/wallet-standard";
21
21
  import { GasStationClient, GasStationTransactionSubmitter } from "@aptos-labs/gas-station-client";
22
+ import { CrossChainCore } from "../../../CrossChainCore";
22
23
 
23
24
  export async function signAndSendTransaction(
24
25
  request: AptosUnsignedTransaction<Network, AptosChains>,
25
26
  wallet: AdapterWallet,
26
27
  sponsorAccount: Account | GasStationApiKey | undefined,
27
28
  dappNetwork: AptosNetwork,
29
+ crossChainCore?: CrossChainCore,
28
30
  ) {
29
31
  if (!wallet) {
30
32
  throw new Error("wallet.sendTransaction is undefined");
@@ -90,12 +92,19 @@ export async function signAndSendTransaction(
90
92
  functionArguments,
91
93
  };
92
94
 
95
+ const expireTimestamp = crossChainCore?._dappConfig?.getExpireTimestamp?.();
96
+ if (typeof expireTimestamp !== "undefined") {
97
+ validateExpireTimestamp(expireTimestamp);
98
+ }
93
99
  const txnToSign = await aptos.transaction.build.simple({
94
100
  data: transactionData,
95
101
  sender: (
96
102
  await wallet.features["aptos:account"]?.account()
97
103
  ).address.toString(),
98
104
  withFeePayer: sponsorAccount ? true : false,
105
+ ...(typeof expireTimestamp !== "undefined"
106
+ ? { options: { expireTimestamp } }
107
+ : {}),
99
108
  });
100
109
 
101
110
  const response =
@@ -6,11 +6,11 @@ import { Network } from "@wormhole-foundation/sdk";
6
6
  import { ethers, getBigInt } from "ethers";
7
7
  import { AdapterWallet } from "@aptos-labs/wallet-adapter-core";
8
8
  import { EIP1193DerivedWallet } from "@aptos-labs/derived-wallet-ethereum";
9
+
9
10
  export async function signAndSendTransaction(
10
11
  request: EvmUnsignedTransaction<Network, EvmChains>,
11
12
  wallet: AdapterWallet,
12
13
  chainName: string,
13
- options: any,
14
14
  ): Promise<string> {
15
15
  if (!wallet) {
16
16
  throw new Error("wallet.sendTransaction is undefined");
@@ -23,8 +23,7 @@ export async function signAndSendTransaction(
23
23
  });
24
24
  const actualChainId = parseInt(chainId, 16);
25
25
 
26
- if (!actualChainId)
27
- throw new Error("No signer found for chain" + chainName);
26
+ if (!actualChainId) throw new Error("No signer found for chain" + chainName);
28
27
  const expectedChainId = request.transaction.chainId
29
28
  ? getBigInt(request.transaction.chainId)
30
29
  : undefined;
@@ -42,8 +41,60 @@ export async function signAndSendTransaction(
42
41
  (wallet as EIP1193DerivedWallet).eip1193Provider,
43
42
  );
44
43
  const signer = await provider.getSigner();
45
- const response = await signer.sendTransaction(request.transaction);
46
- const receipt = await response.wait();
47
44
 
48
- return receipt?.hash || "";
45
+ let response: ethers.TransactionResponse;
46
+ try {
47
+ response = await signer.sendTransaction(request.transaction);
48
+ } catch (e) {
49
+ // Some wallet providers (e.g. MetaMask via injected provider) can throw
50
+ // after the transaction is already broadcast. Try to extract the hash from
51
+ // the error so the caller can track the pending transaction.
52
+ const message = e instanceof Error ? e.message : String(e);
53
+ const hashMatch = message.match(/"hash":\s*"(0x[a-fA-F0-9]{64})"/);
54
+ if (hashMatch) {
55
+ console.warn("Extracted EVM tx hash from error:", hashMatch[1]);
56
+ return hashMatch[1];
57
+ }
58
+ throw e;
59
+ }
60
+
61
+ try {
62
+ const receipt = await response.wait();
63
+ return receipt?.hash || response.hash || "";
64
+ } catch (e: any) {
65
+ // When a user speeds up or cancels a transaction in their wallet, ethers
66
+ // throws a TRANSACTION_REPLACED error. We must handle this specifically
67
+ // to avoid returning the old (now-invalid) hash.
68
+ if (e?.code === "TRANSACTION_REPLACED") {
69
+ // "repriced" means the same transaction data was re-sent with higher
70
+ // gas — the bridge burn still went through with the replacement tx.
71
+ if (e.reason === "repriced") {
72
+ const replacementHash = e.receipt?.hash || e.replacement?.hash;
73
+ if (replacementHash) {
74
+ console.warn(
75
+ "EVM transaction was repriced. Using replacement hash:",
76
+ replacementHash,
77
+ );
78
+ return replacementHash;
79
+ }
80
+ }
81
+ // "cancelled" or "replaced" means the original burn was superseded by
82
+ // a different transaction (e.g. a 0-value self-transfer or an entirely
83
+ // different call). The bridge burn did not happen, so we must not
84
+ // return a hash that implies success.
85
+ throw e;
86
+ }
87
+
88
+ // wait() can fail due to network timeouts or RPC instability, but the
89
+ // transaction was already submitted (sendTransaction returned successfully).
90
+ // Return the hash so the caller can track confirmation asynchronously.
91
+ if (response.hash) {
92
+ console.warn(
93
+ "EVM transaction wait failed but tx was submitted:",
94
+ response.hash,
95
+ );
96
+ return response.hash;
97
+ }
98
+ throw e;
99
+ }
49
100
  }
@@ -25,7 +25,7 @@ import { ChainConfig } from "../../../config";
25
25
  import { CrossChainCore } from "../../../CrossChainCore";
26
26
  import { AptosChains } from "@wormhole-foundation/sdk-aptos/dist/cjs/types";
27
27
  import { AptosUnsignedTransaction } from "@wormhole-foundation/sdk-aptos/dist/cjs/unsignedTransaction";
28
- import { GasStationApiKey } from "../types";
28
+ import { GasStationApiKey, OnTransactionSigned } from "../types";
29
29
  import { Account } from "@aptos-labs/ts-sdk";
30
30
  export class Signer<
31
31
  N extends Network,
@@ -37,7 +37,15 @@ export class Signer<
37
37
  _wallet: AdapterWallet;
38
38
  _crossChainCore: CrossChainCore;
39
39
  _sponsorAccount: Account | GasStationApiKey | undefined;
40
+ _onTransactionSigned: OnTransactionSigned | undefined;
40
41
  _claimedTransactionHashes: string[] = [];
42
+ /**
43
+ * When true, signed tx hashes are written to
44
+ * `_crossChainCore._lastSourceChainTxId` as a recovery side-channel.
45
+ * Set to false for destination-chain claim signers so they don't
46
+ * overwrite the source-chain burn hash.
47
+ */
48
+ _trackAsSourceChain: boolean;
41
49
 
42
50
  constructor(
43
51
  chain: ChainConfig,
@@ -46,6 +54,8 @@ export class Signer<
46
54
  wallet: AdapterWallet,
47
55
  crossChainCore: CrossChainCore,
48
56
  sponsorAccount?: Account | GasStationApiKey | undefined,
57
+ onTransactionSigned?: OnTransactionSigned,
58
+ trackAsSourceChain: boolean = true,
49
59
  ) {
50
60
  this._chain = chain;
51
61
  this._address = address;
@@ -53,6 +63,8 @@ export class Signer<
53
63
  this._wallet = wallet;
54
64
  this._crossChainCore = crossChainCore;
55
65
  this._sponsorAccount = sponsorAccount;
66
+ this._onTransactionSigned = onTransactionSigned;
67
+ this._trackAsSourceChain = trackAsSourceChain;
56
68
  }
57
69
 
58
70
  chain(): C {
@@ -71,6 +83,7 @@ export class Signer<
71
83
  this._claimedTransactionHashes = [];
72
84
 
73
85
  for (const tx of txs) {
86
+ this._onTransactionSigned?.(tx.description, null);
74
87
  const txId = await signAndSendTransaction(
75
88
  this._chain,
76
89
  tx,
@@ -79,6 +92,10 @@ export class Signer<
79
92
  this._crossChainCore,
80
93
  this._sponsorAccount,
81
94
  );
95
+ if (this._trackAsSourceChain) {
96
+ this._crossChainCore._lastSourceChainTxId = txId;
97
+ }
98
+ this._onTransactionSigned?.(tx.description, txId);
82
99
  txHashes.push(txId);
83
100
  this._claimedTransactionHashes.push(txId);
84
101
  }
@@ -113,7 +130,6 @@ export const signAndSendTransaction = async (
113
130
  request as EvmUnsignedTransaction<Network, EvmChains>,
114
131
  wallet,
115
132
  chain.displayName,
116
- options,
117
133
  );
118
134
  return tx;
119
135
  } else if (chain.context === "Sui") {
@@ -128,6 +144,7 @@ export const signAndSendTransaction = async (
128
144
  wallet,
129
145
  sponsorAccount,
130
146
  dappNetwork,
147
+ crossChainCore,
131
148
  );
132
149
  return tx;
133
150
  } else {
@@ -16,6 +16,7 @@ import {
16
16
  PriorityFeeConfig,
17
17
  sendAndConfirmTransaction,
18
18
  } from "./solanaUtils";
19
+ import { OnTransactionSigned } from "../types";
19
20
 
20
21
  export interface SolanaLocalSignerConfig {
21
22
  /** The Solana keypair to sign transactions with */
@@ -30,6 +31,8 @@ export interface SolanaLocalSignerConfig {
30
31
  priorityFeeConfig?: PriorityFeeConfig;
31
32
  /** Enable verbose logging (default: false) */
32
33
  verbose?: boolean;
34
+ /** Optional callback fired before and after each individual transaction is signed. */
35
+ onTransactionSigned?: OnTransactionSigned;
33
36
  }
34
37
 
35
38
  /**
@@ -68,6 +71,7 @@ export class SolanaLocalSigner<N extends Network, C extends Chain>
68
71
  private retryIntervalMs: number;
69
72
  private priorityFeeConfig?: PriorityFeeConfig;
70
73
  private verbose: boolean;
74
+ private _onTransactionSigned?: OnTransactionSigned;
71
75
  private _claimedTransactionHashes: string[] = [];
72
76
 
73
77
  constructor(config: SolanaLocalSignerConfig) {
@@ -77,6 +81,7 @@ export class SolanaLocalSigner<N extends Network, C extends Chain>
77
81
  this.retryIntervalMs = config.retryIntervalMs ?? 5000;
78
82
  this.priorityFeeConfig = config.priorityFeeConfig;
79
83
  this.verbose = config.verbose ?? false;
84
+ this._onTransactionSigned = config.onTransactionSigned;
80
85
  }
81
86
 
82
87
  chain(): C {
@@ -100,7 +105,9 @@ export class SolanaLocalSigner<N extends Network, C extends Chain>
100
105
  this._claimedTransactionHashes = [];
101
106
 
102
107
  for (const tx of txs) {
108
+ this._onTransactionSigned?.(tx.description, null);
103
109
  const txId = await this.signAndSendTransaction(tx);
110
+ this._onTransactionSigned?.(tx.description, txId);
104
111
  txHashes.push(txId);
105
112
  this._claimedTransactionHashes.push(txId);
106
113
  }
@@ -41,7 +41,10 @@ export async function signAndSendTransaction(
41
41
  throw new Error("Invalid wallet type or missing Solana wallet");
42
42
  }
43
43
 
44
- const commitment = options?.commitment ?? "finalized";
44
+ const commitment =
45
+ options?.commitment ??
46
+ crossChainCore?._dappConfig?.solanaConfig?.commitment ??
47
+ "finalized";
45
48
  // Solana rpc should come from dapp config
46
49
  const connection = new Connection(
47
50
  crossChainCore?._dappConfig?.solanaConfig?.rpc ??
@@ -82,34 +82,58 @@ export async function sendAndConfirmTransaction(
82
82
  commitment,
83
83
  );
84
84
 
85
- // Retry loop: resend if not confirmed after interval
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.
86
91
  let confirmedTx: RpcResponseAndContext<SignatureResult> | null = null;
87
92
  let txSendAttempts = 1;
88
93
 
89
- while (!confirmedTx) {
90
- confirmedTx = await Promise.race([
91
- confirmTransactionPromise,
92
- new Promise<null>((resolve) =>
93
- setTimeout(() => resolve(null), retryIntervalMs),
94
- ),
95
- ]);
94
+ try {
95
+ while (!confirmedTx) {
96
+ confirmedTx = await Promise.race([
97
+ confirmTransactionPromise,
98
+ new Promise<null>((resolve) =>
99
+ setTimeout(() => resolve(null), retryIntervalMs),
100
+ ),
101
+ ]);
96
102
 
97
- if (confirmedTx) break;
103
+ if (confirmedTx) break;
98
104
 
99
- if (verbose) {
100
- console.log(
101
- `Tx not confirmed after ${retryIntervalMs * txSendAttempts++}ms, resending`,
102
- );
103
- }
105
+ if (verbose) {
106
+ console.log(
107
+ `Tx not confirmed after ${retryIntervalMs * txSendAttempts++}ms, resending`,
108
+ );
109
+ }
104
110
 
105
- try {
106
- await connection.sendRawTransaction(serializedTx, sendOptions);
107
- } catch (e) {
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
+ ) {
108
126
  if (verbose) {
109
- console.error("Failed to resend transaction:", e);
127
+ console.warn(
128
+ "Block height exceeded but tx was already sent, returning signature:",
129
+ signature,
130
+ );
110
131
  }
111
- // Ignore resend errors, confirmation will handle success/failure
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;
112
135
  }
136
+ throw e;
113
137
  }
114
138
 
115
139
  if (confirmedTx.value.err) {
@@ -35,6 +35,8 @@ 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
 
40
42
  export type WithdrawPhase =
@@ -42,25 +44,51 @@ export type WithdrawPhase =
42
44
  | "tracking" // Waiting for Wormhole attestation (~60s)
43
45
  | "claiming"; // Claiming on destination chain
44
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
+
45
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
+ */
46
68
  sourceChain: Chain;
47
69
  wallet: AdapterWallet;
48
70
  destinationAddress: AccountAddressInput;
49
71
  sponsorAccount?: Account | GasStationApiKey;
50
72
  /** Optional callback fired when the withdraw progresses to a new phase. */
51
73
  onPhaseChange?: (phase: WithdrawPhase) => void;
74
+ /** Optional callback fired before and after each individual transaction is signed. */
75
+ onTransactionSigned?: OnTransactionSigned;
52
76
  }
53
77
 
54
78
  export interface WormholeSubmitTransferRequest {
55
79
  sourceChain: Chain;
56
80
  wallet: AdapterWallet;
57
81
  destinationAddress: AccountAddressInput;
82
+ /** Optional callback fired before and after each individual transaction is signed. */
83
+ onTransactionSigned?: OnTransactionSigned;
58
84
  }
59
85
 
60
86
  export interface WormholeClaimTransferRequest {
61
87
  receipt: routes.Receipt<AttestationReceipt>;
62
88
  mainSigner: AptosAccount;
63
89
  sponsorAccount?: AptosAccount | GasStationApiKey;
90
+ /** Optional callback fired before and after each individual transaction is signed. */
91
+ onTransactionSigned?: OnTransactionSigned;
64
92
  }
65
93
 
66
94
  export interface WormholeTransferResponse {
@@ -84,6 +112,8 @@ export interface WormholeInitiateWithdrawRequest {
84
112
  wallet: AdapterWallet;
85
113
  destinationAddress: AccountAddressInput;
86
114
  sponsorAccount?: Account | GasStationApiKey;
115
+ /** Optional callback fired before and after each individual transaction is signed. */
116
+ onTransactionSigned?: OnTransactionSigned;
87
117
  }
88
118
 
89
119
  export interface WormholeInitiateWithdrawResponse {
@@ -92,18 +122,87 @@ export interface WormholeInitiateWithdrawResponse {
92
122
  }
93
123
 
94
124
  export interface WormholeClaimWithdrawRequest {
95
- sourceChain: Chain;
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;
96
133
  destinationAddress: string;
97
134
  receipt: routes.Receipt<AttestationReceipt>;
98
135
  // Required for wallet-based claim (non-Solana chains, or Solana without serverClaimUrl).
99
136
  // Not needed when the SDK uses the configured serverClaimUrl for Solana claims.
100
137
  wallet?: AdapterWallet;
138
+ /** Optional callback fired before and after each individual transaction is signed. */
139
+ onTransactionSigned?: OnTransactionSigned;
101
140
  }
102
141
 
103
142
  export interface WormholeClaimWithdrawResponse {
104
143
  destinationChainTxnId: string;
105
144
  }
106
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
+
107
206
  /**
108
207
  * Error thrown when the withdraw flow fails *after* the Aptos burn
109
208
  * transaction has already been submitted (i.e. during attestation tracking