@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
@@ -0,0 +1,38 @@
1
+ import { routes, AttestationReceipt } from "@wormhole-foundation/sdk";
2
+ /**
3
+ * Serializes a Wormhole receipt for JSON transport.
4
+ *
5
+ * JSON doesn't natively support BigInt, Uint8Array, or class instances.
6
+ * This function converts these types to a serializable format with type markers
7
+ * that can be reconstructed by `deserializeReceipt`.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * import { serializeReceipt } from "@aptos-labs/cross-chain-core";
12
+ *
13
+ * // On the client side, before sending to server
14
+ * const serialized = serializeReceipt(receipt);
15
+ * await fetch("/api/claim", {
16
+ * method: "POST",
17
+ * body: JSON.stringify({ receipt: serialized }),
18
+ * });
19
+ * ```
20
+ */
21
+ export declare function serializeReceipt(receipt: routes.Receipt<AttestationReceipt>): unknown;
22
+ /**
23
+ * Deserializes a Wormhole receipt from JSON transport format.
24
+ *
25
+ * Reconstructs BigInt, Uint8Array, and UniversalAddress instances from
26
+ * the serialized format produced by `serializeReceipt`.
27
+ *
28
+ * @example
29
+ * ```typescript
30
+ * import { deserializeReceipt, SolanaLocalSigner } from "@aptos-labs/cross-chain-core";
31
+ *
32
+ * // On the server side, after receiving from client
33
+ * const receipt = deserializeReceipt(body.receipt);
34
+ * await cctpRoute.complete(signer, receipt);
35
+ * ```
36
+ */
37
+ export declare function deserializeReceipt(obj: unknown): routes.Receipt<AttestationReceipt>;
38
+ //# sourceMappingURL=receiptSerialization.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"receiptSerialization.d.ts","sourceRoot":"","sources":["../../src/utils/receiptSerialization.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,MAAM,EACN,kBAAkB,EAEnB,MAAM,0BAA0B,CAAC;AAoBlC;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC,kBAAkB,CAAC,GAC1C,OAAO,CAuBT;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,OAAO,GACX,MAAM,CAAC,OAAO,CAAC,kBAAkB,CAAC,CAoDpC"}
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const CROSS_CHAIN_CORE_VERSION = "5.8.2";
1
+ export declare const CROSS_CHAIN_CORE_VERSION = "6.0.0";
2
2
  //# sourceMappingURL=version.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aptos-labs/cross-chain-core",
3
- "version": "5.8.2",
3
+ "version": "6.0.0",
4
4
  "description": "Aptos Cross Chain Core",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -57,9 +57,9 @@
57
57
  "eventemitter3": "^4.0.7",
58
58
  "tweetnacl": "^1.0.3",
59
59
  "@aptos-labs/derived-wallet-ethereum": "0.9.0",
60
- "@aptos-labs/derived-wallet-solana": "0.12.0",
61
60
  "@aptos-labs/derived-wallet-sui": "0.2.0",
62
- "@aptos-labs/wallet-adapter-core": "8.3.0"
61
+ "@aptos-labs/wallet-adapter-core": "8.4.0",
62
+ "@aptos-labs/derived-wallet-solana": "0.12.1"
63
63
  },
64
64
  "peerDependencies": {
65
65
  "@aptos-labs/ts-sdk": "^5.1.1"
@@ -29,6 +29,17 @@ import {
29
29
  export interface CrossChainDappConfig {
30
30
  aptosNetwork: Network;
31
31
  disableTelemetry?: boolean;
32
+ /**
33
+ * Returns an epoch-second timestamp used as `expireTimestamp` when building
34
+ * Aptos transactions. Called at transaction-build time so that each
35
+ * transaction in a multi-step bridge flow gets a fresh expiration window.
36
+ *
37
+ * @example
38
+ * ```ts
39
+ * getExpireTimestamp: () => Math.floor(Date.now() / 1000) + 120 // 2 minutes
40
+ * ```
41
+ */
42
+ getExpireTimestamp?: () => number;
32
43
  solanaConfig?: {
33
44
  rpc?: string;
34
45
  priorityFeeConfig?: {
@@ -37,7 +48,73 @@ export interface CrossChainDappConfig {
37
48
  min?: number;
38
49
  max?: number;
39
50
  };
51
+ /**
52
+ * URL of a server-side API endpoint that claims withdraw transactions on Solana.
53
+ * When set, the SDK will POST the attested receipt to this URL instead of
54
+ * asking the user's wallet to sign the claim transaction.
55
+ *
56
+ * Expected request body: { serializedReceipt: string, destinationAddress: string, sourceChain: string }
57
+ * Expected response: { destinationChainTxnId: string }
58
+ * Check out the SERVERSIDE_SOLANA_SIGNER.md file for more details.
59
+ *
60
+ * @example
61
+ * const crossChainCore = new CrossChainCore({
62
+ * dappConfig: {
63
+ * aptosNetwork: Network.TESTNET,
64
+ * solanaConfig: {
65
+ * serverClaimUrl: "/api/claim-withdraw",
66
+ * },
67
+ * },
68
+ * });
69
+ */
70
+ serverClaimUrl?: string;
71
+ /**
72
+ * Solana transaction confirmation commitment level.
73
+ *
74
+ * - `"finalized"` (default) — waits for supermajority finalization (~30 s).
75
+ * - `"confirmed"` — waits for supermajority confirmation (~0.5 s).
76
+ *
77
+ * For bridge flows `"confirmed"` is usually sufficient because Wormhole
78
+ * guardians independently verify finality before issuing attestations.
79
+ *
80
+ * @default "finalized"
81
+ *
82
+ * @example
83
+ * const crossChainCore = new CrossChainCore({
84
+ * dappConfig: {
85
+ * aptosNetwork: Network.MAINNET,
86
+ * solanaConfig: {
87
+ * commitment: "confirmed", // ~0.5 s vs ~30 s
88
+ * },
89
+ * },
90
+ * });
91
+ */
92
+ commitment?: "confirmed" | "finalized";
40
93
  };
94
+ /**
95
+ * Custom RPC endpoints for EVM chains. When provided, these override the
96
+ * built-in `defaultRpc` values for balance lookups and Wormhole SDK
97
+ * initialization.
98
+ *
99
+ * @example
100
+ * ```ts
101
+ * evmConfig: {
102
+ * Ethereum: { rpc: "https://rpc.ankr.com/eth/MY_KEY" },
103
+ * Base: { rpc: "https://rpc.ankr.com/base/MY_KEY" },
104
+ * }
105
+ * ```
106
+ */
107
+ evmConfig?: Partial<Record<EvmChainName, { rpc: string }>>;
108
+ /**
109
+ * Custom RPC endpoint for the Sui chain. When provided, overrides the
110
+ * built-in `defaultRpc` for balance lookups and Wormhole SDK initialization.
111
+ *
112
+ * @example
113
+ * ```ts
114
+ * suiConfig: { rpc: "https://fullnode.mainnet.sui.io" }
115
+ * ```
116
+ */
117
+ suiConfig?: { rpc?: string };
41
118
  }
42
119
  export type { AccountAddressInput } from "@aptos-labs/ts-sdk";
43
120
  export { NetworkToChainId, NetworkToNodeAPI } from "@aptos-labs/ts-sdk";
@@ -58,6 +135,27 @@ export type Chain =
58
135
  | "Polygon"
59
136
  | "Sui";
60
137
 
138
+ /**
139
+ * EVM chain names supported by the SDK — derived from {@link Chain} by
140
+ * excluding the non-EVM ecosystems. Adding a new EVM chain to `Chain`
141
+ * automatically makes it a valid key in `evmConfig`.
142
+ */
143
+ export type EvmChainName = Exclude<Chain, "Solana" | "Aptos" | "Sui">;
144
+
145
+ // Record ensures every EvmChainName key is present at compile time.
146
+ const _evmChainRecord: Record<EvmChainName, true> = {
147
+ Ethereum: true,
148
+ Sepolia: true,
149
+ BaseSepolia: true,
150
+ ArbitrumSepolia: true,
151
+ Avalanche: true,
152
+ Base: true,
153
+ Arbitrum: true,
154
+ PolygonSepolia: true,
155
+ Polygon: true,
156
+ };
157
+ export const EVM_CHAIN_NAMES = Object.keys(_evmChainRecord) as EvmChainName[];
158
+
61
159
  // Map of Ethereum chain id to testnet chain config
62
160
  export const EthereumChainIdToTestnetChain: Record<string, ChainConfig> = {
63
161
  11155111: testnetChains.Sepolia!,
@@ -99,6 +197,14 @@ export class CrossChainCore {
99
197
  readonly CHAINS: ChainsConfig = testnetChains;
100
198
  readonly TOKENS: Record<string, TokenConfig> = testnetTokens;
101
199
 
200
+ /**
201
+ * Last known source-chain transaction ID, set by signers immediately after
202
+ * a transaction is submitted. Acts as a recovery side-channel so that
203
+ * callers can retrieve the tx hash even when the orchestration layer throws
204
+ * before returning it (e.g. claim failure after a successful burn).
205
+ */
206
+ _lastSourceChainTxId: string | undefined;
207
+
102
208
  constructor(args: { dappConfig: CrossChainDappConfig }) {
103
209
  this._dappConfig = args.dappConfig;
104
210
  if (args.dappConfig?.aptosNetwork === Network.MAINNET) {
@@ -161,14 +267,15 @@ export class CrossChainCore {
161
267
  walletAddress,
162
268
  this._dappConfig.aptosNetwork,
163
269
  sourceChain,
164
- // TODO: maybe let the user config it
165
- this.CHAINS[sourceChain].defaultRpc,
270
+ this._dappConfig?.evmConfig?.[sourceChain]?.rpc ??
271
+ this.CHAINS[sourceChain].defaultRpc,
166
272
  );
167
273
  case "Sui":
168
274
  return await getSuiWalletUSDCBalance(
169
275
  walletAddress,
170
276
  this._dappConfig.aptosNetwork,
171
- this.CHAINS[sourceChain].defaultRpc,
277
+ this._dappConfig?.suiConfig?.rpc ??
278
+ this.CHAINS[sourceChain].defaultRpc,
172
279
  );
173
280
  default:
174
281
  throw new Error(`Unsupported chain: ${sourceChain}`);
@@ -16,8 +16,8 @@ export const mainnetChains: ChainsConfig = {
16
16
  key: "Solana",
17
17
  context: Context.SOLANA,
18
18
  displayName: "Solana",
19
- explorerUrl: "https://explorer.solana.com/",
20
- explorerName: "Solana Explorer",
19
+ explorerUrl: "https://solscan.io",
20
+ explorerName: "Solscan",
21
21
  chainId: 0,
22
22
  icon: "Solana",
23
23
  symbol: "SOL",
@@ -61,8 +61,8 @@ export const testnetChains: ChainsConfig = {
61
61
  key: "Solana",
62
62
  context: Context.SOLANA,
63
63
  displayName: "Solana",
64
- explorerUrl: "https://explorer.solana.com/",
65
- explorerName: "Solana Explorer",
64
+ explorerUrl: "https://solscan.io",
65
+ explorerName: "Solscan",
66
66
  chainId: 0,
67
67
  icon: "Solana",
68
68
  symbol: "SOL",
package/src/index.ts CHANGED
@@ -2,4 +2,5 @@ export * from "./CrossChainCore";
2
2
  export * from "./config";
3
3
  export * from "./providers/wormhole/index";
4
4
  export * from "./providers/wormhole/types";
5
+ export * from "./utils/receiptSerialization";
5
6
  export { Network } from "@aptos-labs/ts-sdk";
@@ -1,4 +1,6 @@
1
1
  export * from "./wormhole";
2
2
  export * from "./types";
3
+ export * from "./utils";
3
4
  export * from "./signers/AptosLocalSigner";
5
+ export * from "./signers/SolanaLocalSigner";
4
6
  export * from "../../config";
@@ -3,7 +3,6 @@ import {
3
3
  AnyRawTransaction,
4
4
  Aptos,
5
5
  AptosConfig,
6
- Network as AptosNetwork,
7
6
  Account,
8
7
  } from "@aptos-labs/ts-sdk";
9
8
 
@@ -18,8 +17,11 @@ import {
18
17
  AptosUnsignedTransaction,
19
18
  AptosChains,
20
19
  } from "@wormhole-foundation/sdk-aptos";
21
- import { GasStationApiKey } from "../types";
22
- import { isAccount } from "./AptosSigner";
20
+ import {
21
+ OnTransactionSigned,
22
+ validateExpireTimestamp,
23
+ } from "../types";
24
+ import { CrossChainCore } from "../../../CrossChainCore";
23
25
 
24
26
  export class AptosLocalSigner<
25
27
  N extends Network,
@@ -28,22 +30,24 @@ export class AptosLocalSigner<
28
30
  _chain: C;
29
31
  _options: any;
30
32
  _wallet: Account;
31
- _sponsorAccount: Account | GasStationApiKey | undefined;
32
- _claimedTransactionHashes: string;
33
- _dappNetwork: AptosNetwork;
33
+ _sponsorAccount: Account | undefined;
34
+ _onTransactionSigned: OnTransactionSigned | undefined;
35
+ _crossChainCore: CrossChainCore;
36
+ _claimedTransactionHashes: string[] = [];
34
37
  constructor(
35
38
  chain: C,
36
39
  options: any,
37
40
  wallet: Account,
38
- feePayerAccount: Account | GasStationApiKey | undefined,
39
- dappNetwork: AptosNetwork,
41
+ feePayerAccount: Account | undefined,
42
+ crossChainCore: CrossChainCore,
43
+ onTransactionSigned?: OnTransactionSigned,
40
44
  ) {
41
45
  this._chain = chain;
42
46
  this._options = options;
43
47
  this._wallet = wallet;
44
48
  this._sponsorAccount = feePayerAccount;
45
- this._claimedTransactionHashes = "";
46
- this._dappNetwork = dappNetwork;
49
+ this._crossChainCore = crossChainCore;
50
+ this._onTransactionSigned = onTransactionSigned;
47
51
  }
48
52
 
49
53
  chain(): C {
@@ -54,21 +58,24 @@ export class AptosLocalSigner<
54
58
  }
55
59
 
56
60
  claimedTransactionHashes(): string {
57
- return this._claimedTransactionHashes;
61
+ return this._claimedTransactionHashes.join(",");
58
62
  }
59
63
 
60
64
  async signAndSend(txs: UnsignedTransaction<N, C>[]): Promise<TxHash[]> {
61
65
  const txHashes: TxHash[] = [];
66
+ this._claimedTransactionHashes = [];
62
67
 
63
68
  for (const tx of txs) {
69
+ this._onTransactionSigned?.(tx.description, null);
64
70
  const txId = await signAndSendTransaction(
65
71
  tx as AptosUnsignedTransaction<Network, AptosChains>,
66
72
  this._wallet,
67
73
  this._sponsorAccount,
68
- this._dappNetwork,
74
+ this._crossChainCore,
69
75
  );
76
+ this._onTransactionSigned?.(tx.description, txId);
70
77
  txHashes.push(txId);
71
- this._claimedTransactionHashes = txId;
78
+ this._claimedTransactionHashes.push(txId);
72
79
  }
73
80
  return txHashes;
74
81
  }
@@ -77,8 +84,8 @@ export class AptosLocalSigner<
77
84
  export async function signAndSendTransaction(
78
85
  request: UnsignedTransaction<Network, AptosChains>,
79
86
  wallet: Account,
80
- sponsorAccount: Account | GasStationApiKey | undefined,
81
- dappNetwork: AptosNetwork,
87
+ sponsorAccount: Account | undefined,
88
+ crossChainCore: CrossChainCore,
82
89
  ) {
83
90
  if (!wallet) {
84
91
  throw new Error("Wallet is undefined");
@@ -96,15 +103,23 @@ export async function signAndSendTransaction(
96
103
  }
97
104
  });
98
105
 
106
+ const dappNetwork = crossChainCore._dappConfig.aptosNetwork;
99
107
  const aptosConfig = new AptosConfig({
100
108
  network: dappNetwork,
101
109
  });
102
110
  const aptos = new Aptos(aptosConfig);
103
111
 
112
+ const expireTimestamp = crossChainCore._dappConfig.getExpireTimestamp?.();
113
+ if (typeof expireTimestamp !== "undefined") {
114
+ validateExpireTimestamp(expireTimestamp);
115
+ }
104
116
  const txnToSign = await aptos.transaction.build.simple({
105
117
  data: payload,
106
118
  sender: wallet.accountAddress.toString(),
107
119
  withFeePayer: sponsorAccount ? true : false,
120
+ ...(typeof expireTimestamp !== "undefined"
121
+ ? { options: { expireTimestamp } }
122
+ : {}),
108
123
  });
109
124
 
110
125
  const senderAuthenticator = await aptos.transaction.sign({
@@ -122,10 +137,8 @@ export async function signAndSendTransaction(
122
137
  };
123
138
 
124
139
  if (sponsorAccount) {
125
- // Gas station is currently impossible to use, since the transactino we get from
126
- // Wormhole is a script transaction and gas station only supports entry function transactions
127
140
  const feePayerSignerAuthenticator = aptos.transaction.signAsFeePayer({
128
- signer: sponsorAccount as Account,
141
+ signer: sponsorAccount,
129
142
  transaction: txnToSign,
130
143
  });
131
144
  txnToSubmit.feePayerAuthenticator = feePayerSignerAuthenticator;
@@ -16,18 +16,20 @@ 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
- throw new Error("wallet.sendTransaction is undefined").message;
32
+ throw new Error("wallet.sendTransaction is undefined");
31
33
  }
32
34
 
33
35
  const payload = request.transaction;
@@ -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,14 +6,14 @@ 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
- throw new Error("wallet.sendTransaction is undefined").message;
16
+ throw new Error("wallet.sendTransaction is undefined");
17
17
  }
18
18
  // Ensure the signer is connected to the correct chain
19
19
  const chainId = await (
@@ -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).message;
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;
@@ -35,15 +34,67 @@ export async function signAndSendTransaction(
35
34
  ) {
36
35
  throw new Error(
37
36
  `Signer is not connected to the right chain. Expected ${expectedChainId}, got ${actualChainId}`,
38
- ).message;
37
+ );
39
38
  }
40
39
 
41
40
  const provider = new ethers.BrowserProvider(
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
- _claimedTransactionHashes: string;
40
+ _onTransactionSigned: OnTransactionSigned | undefined;
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,7 +63,8 @@ export class Signer<
53
63
  this._wallet = wallet;
54
64
  this._crossChainCore = crossChainCore;
55
65
  this._sponsorAccount = sponsorAccount;
56
- this._claimedTransactionHashes = "";
66
+ this._onTransactionSigned = onTransactionSigned;
67
+ this._trackAsSourceChain = trackAsSourceChain;
57
68
  }
58
69
 
59
70
  chain(): C {
@@ -64,13 +75,15 @@ export class Signer<
64
75
  }
65
76
 
66
77
  claimedTransactionHashes(): string {
67
- return this._claimedTransactionHashes;
78
+ return this._claimedTransactionHashes.join(",");
68
79
  }
69
80
 
70
81
  async signAndSend(txs: UnsignedTransaction<N, C>[]): Promise<TxHash[]> {
71
82
  const txHashes: TxHash[] = [];
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,8 +92,12 @@ 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
- this._claimedTransactionHashes = txId;
100
+ this._claimedTransactionHashes.push(txId);
84
101
  }
85
102
  return txHashes;
86
103
  }
@@ -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 {