@aptos-labs/cross-chain-core 4.23.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 (66) hide show
  1. package/LICENSE +201 -0
  2. package/dist/CrossChainCore.d.ts +36 -0
  3. package/dist/CrossChainCore.d.ts.map +1 -0
  4. package/dist/config/index.d.ts +4 -0
  5. package/dist/config/index.d.ts.map +1 -0
  6. package/dist/config/mainnet/chains.d.ts +17 -0
  7. package/dist/config/mainnet/chains.d.ts.map +1 -0
  8. package/dist/config/mainnet/index.d.ts +3 -0
  9. package/dist/config/mainnet/index.d.ts.map +1 -0
  10. package/dist/config/mainnet/tokens.d.ts +4 -0
  11. package/dist/config/mainnet/tokens.d.ts.map +1 -0
  12. package/dist/config/testnet/chains.d.ts +19 -0
  13. package/dist/config/testnet/chains.d.ts.map +1 -0
  14. package/dist/config/testnet/index.d.ts +3 -0
  15. package/dist/config/testnet/index.d.ts.map +1 -0
  16. package/dist/config/testnet/tokens.d.ts +4 -0
  17. package/dist/config/testnet/tokens.d.ts.map +1 -0
  18. package/dist/config/types.d.ts +41 -0
  19. package/dist/config/types.d.ts.map +1 -0
  20. package/dist/index.d.ts +6 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.js +989 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/index.mjs +957 -0
  25. package/dist/index.mjs.map +1 -0
  26. package/dist/providers/wormhole/index.d.ts +5 -0
  27. package/dist/providers/wormhole/index.d.ts.map +1 -0
  28. package/dist/providers/wormhole/signers/AptosLocalSigner.d.ts +18 -0
  29. package/dist/providers/wormhole/signers/AptosLocalSigner.d.ts.map +1 -0
  30. package/dist/providers/wormhole/signers/EthereumSigner.d.ts +5 -0
  31. package/dist/providers/wormhole/signers/EthereumSigner.d.ts.map +1 -0
  32. package/dist/providers/wormhole/signers/Signer.d.ts +17 -0
  33. package/dist/providers/wormhole/signers/Signer.d.ts.map +1 -0
  34. package/dist/providers/wormhole/signers/SolanaSigner.d.ts +28 -0
  35. package/dist/providers/wormhole/signers/SolanaSigner.d.ts.map +1 -0
  36. package/dist/providers/wormhole/types.d.ts +39 -0
  37. package/dist/providers/wormhole/types.d.ts.map +1 -0
  38. package/dist/providers/wormhole/wormhole.d.ts +35 -0
  39. package/dist/providers/wormhole/wormhole.d.ts.map +1 -0
  40. package/dist/utils/getUsdcBalance.d.ts +5 -0
  41. package/dist/utils/getUsdcBalance.d.ts.map +1 -0
  42. package/dist/utils/logger.d.ts +6 -0
  43. package/dist/utils/logger.d.ts.map +1 -0
  44. package/dist/version.d.ts +2 -0
  45. package/dist/version.d.ts.map +1 -0
  46. package/package.json +83 -0
  47. package/src/CrossChainCore.ts +130 -0
  48. package/src/config/index.ts +3 -0
  49. package/src/config/mainnet/chains.ts +64 -0
  50. package/src/config/mainnet/index.ts +2 -0
  51. package/src/config/mainnet/tokens.ts +43 -0
  52. package/src/config/testnet/chains.ts +69 -0
  53. package/src/config/testnet/index.ts +2 -0
  54. package/src/config/testnet/tokens.ts +43 -0
  55. package/src/config/types.ts +45 -0
  56. package/src/index.ts +5 -0
  57. package/src/providers/wormhole/index.ts +4 -0
  58. package/src/providers/wormhole/signers/AptosLocalSigner.ts +136 -0
  59. package/src/providers/wormhole/signers/EthereumSigner.ts +49 -0
  60. package/src/providers/wormhole/signers/Signer.ts +102 -0
  61. package/src/providers/wormhole/signers/SolanaSigner.ts +418 -0
  62. package/src/providers/wormhole/types.ts +59 -0
  63. package/src/providers/wormhole/wormhole.ts +320 -0
  64. package/src/utils/getUsdcBalance.ts +82 -0
  65. package/src/utils/logger.ts +17 -0
  66. package/src/version.ts +1 -0
@@ -0,0 +1,418 @@
1
+ // This function signs and sends the transaction while constantly checking for confirmation
2
+ // and resending the transaction if it hasn't been confirmed after the specified interval
3
+
4
+ import {
5
+ AddressLookupTableAccount,
6
+ Commitment,
7
+ ComputeBudgetProgram,
8
+ ConfirmOptions,
9
+ LAMPORTS_PER_SOL,
10
+ SimulatedTransactionResponse,
11
+ TransactionInstruction,
12
+ TransactionMessage,
13
+ VersionedTransaction,
14
+ } from "@solana/web3.js";
15
+
16
+ import { Transaction } from "@solana/web3.js";
17
+ import { RpcResponseAndContext, SignatureResult } from "@solana/web3.js";
18
+ import {
19
+ determinePriorityFee,
20
+ determinePriorityFeeTritonOne,
21
+ SolanaUnsignedTransaction,
22
+ } from "@wormhole-foundation/sdk-solana";
23
+
24
+ import { Connection } from "@solana/web3.js";
25
+ import { Network } from "@wormhole-foundation/sdk";
26
+ import { AdapterWallet } from "@aptos-labs/wallet-adapter-core";
27
+ import { CrossChainCore } from "../../../CrossChainCore";
28
+ import { SolanaDerivedWallet } from "@aptos-labs/derived-wallet-solana";
29
+
30
+ export type SolanaRpcProvider = "triton" | "helius" | "ankr" | "unknown";
31
+
32
+ // See https://docs.triton.one/chains/solana/sending-txs for more information
33
+ export async function signAndSendTransaction(
34
+ request: SolanaUnsignedTransaction<Network>,
35
+ wallet: AdapterWallet | undefined,
36
+ options?: ConfirmOptions,
37
+ crossChainCore?: CrossChainCore
38
+ ) {
39
+ if (!wallet || !(wallet instanceof SolanaDerivedWallet)) {
40
+ throw new Error("Invalid wallet type or missing Solana wallet").message;
41
+ }
42
+
43
+ const commitment = options?.commitment ?? "finalized";
44
+ // Solana rpc should come from dapp config
45
+ const connection = new Connection(
46
+ crossChainCore?._dappConfig?.solanaConfig?.rpc ??
47
+ "https://api.devnet.solana.com"
48
+ );
49
+ const { blockhash, lastValidBlockHeight } =
50
+ await connection.getLatestBlockhash(commitment);
51
+
52
+ // Circle Manual CCTP on Wormhole is always of a Trnasaction type
53
+ // https://github.com/wormhole-foundation/wormhole-sdk-ts/blob/f7d992e04f844edcc4128659f12f75ade3553717/platforms/solana/protocols/cctp/src/circleBridge.ts#L173
54
+ const transaction = request.transaction.transaction as Transaction;
55
+
56
+ /**
57
+ * TODO: Priority Fee is supported, but needs to come from dapp config
58
+ */
59
+ const unsignedTx = await setPriorityFeeInstructions(
60
+ connection,
61
+ blockhash,
62
+ lastValidBlockHeight,
63
+ request,
64
+ crossChainCore
65
+ );
66
+
67
+ let confirmTransactionPromise: Promise<
68
+ RpcResponseAndContext<SignatureResult>
69
+ > | null = null;
70
+ let confirmedTx: RpcResponseAndContext<SignatureResult> | null = null;
71
+ let txSendAttempts = 1;
72
+ let signature = "";
73
+
74
+ // transaction.recentBlockhash = blockhash;
75
+ // if (request.transaction.signers) {
76
+ // transaction.partialSign(...request.transaction.signers);
77
+ // }
78
+
79
+ if (!wallet.solanaWallet.signTransaction) {
80
+ throw new Error("Wallet does not support signing transactions").message;
81
+ }
82
+
83
+ const tx = await wallet.solanaWallet.signTransaction(unsignedTx);
84
+
85
+ if (!tx) throw new Error("Failed to sign transaction").message;
86
+ const serializedTx = tx.serialize();
87
+ const sendOptions = {
88
+ skipPreflight: true,
89
+ maxRetries: 0,
90
+ preFlightCommitment: commitment, // See PR and linked issue for why setting this matters: https://github.com/anza-xyz/agave/pull/483
91
+ };
92
+ signature = await connection.sendRawTransaction(serializedTx, sendOptions);
93
+ confirmTransactionPromise = connection.confirmTransaction(
94
+ {
95
+ signature,
96
+ blockhash,
97
+ lastValidBlockHeight,
98
+ },
99
+ commitment
100
+ );
101
+
102
+ // This loop will break once the transaction has been confirmed or the block height is exceeded.
103
+ // An exception will be thrown if the block height is exceeded by the confirmTransactionPromise.
104
+ // The transaction will be resent if it hasn't been confirmed after the interval.
105
+ const txRetryInterval = 5000;
106
+ while (!confirmedTx) {
107
+ confirmedTx = await Promise.race([
108
+ confirmTransactionPromise,
109
+ new Promise<null>((resolve) =>
110
+ setTimeout(() => {
111
+ resolve(null);
112
+ }, txRetryInterval)
113
+ ),
114
+ ]);
115
+ if (confirmedTx) {
116
+ break;
117
+ }
118
+ console.log(
119
+ `Tx not confirmed after ${
120
+ txRetryInterval * txSendAttempts++
121
+ }ms, resending`
122
+ );
123
+ try {
124
+ await connection.sendRawTransaction(serializedTx, sendOptions);
125
+ } catch (e) {
126
+ console.error("Failed to resend transaction:", e);
127
+ }
128
+ }
129
+
130
+ if (confirmedTx.value.err) {
131
+ let errorMessage = `Transaction failed: ${confirmedTx.value.err}`;
132
+ if (typeof confirmedTx.value.err === "object") {
133
+ try {
134
+ errorMessage = `Transaction failed: ${JSON.stringify(
135
+ confirmedTx.value.err,
136
+ (_key, value) =>
137
+ typeof value === "bigint" ? value.toString() : value // Handle bigint props
138
+ )}`;
139
+ } catch (e: unknown) {
140
+ // Most likely a circular reference error, we can't stringify this error object.
141
+ // See for more details:
142
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#exceptions
143
+ errorMessage = `Transaction failed: Unknown error`;
144
+ }
145
+ }
146
+ throw new Error(`Transaction failed: ${errorMessage}`).message;
147
+ }
148
+
149
+ return signature;
150
+ }
151
+
152
+ export async function setPriorityFeeInstructions(
153
+ connection: Connection,
154
+ blockhash: string,
155
+ lastValidBlockHeight: number,
156
+ request: SolanaUnsignedTransaction<Network>,
157
+ crossChainCore?: CrossChainCore
158
+ ): Promise<Transaction | VersionedTransaction> {
159
+ const unsignedTx = request.transaction.transaction as Transaction;
160
+
161
+ const computeBudgetIxFilter = (ix: TransactionInstruction) =>
162
+ ix.programId.toString() !== "ComputeBudget111111111111111111111111111111";
163
+
164
+ unsignedTx.recentBlockhash = blockhash;
165
+ unsignedTx.lastValidBlockHeight = lastValidBlockHeight;
166
+
167
+ // Remove existing compute budget instructions if they were added by the SDK
168
+ unsignedTx.instructions = unsignedTx.instructions.filter(
169
+ computeBudgetIxFilter
170
+ );
171
+ unsignedTx.add(
172
+ ...(await createPriorityFeeInstructions(
173
+ connection,
174
+ unsignedTx,
175
+ crossChainCore
176
+ ))
177
+ );
178
+ if (request.transaction.signers) {
179
+ unsignedTx.partialSign(...request.transaction.signers);
180
+ }
181
+
182
+ return unsignedTx;
183
+ }
184
+
185
+ // This will throw if the simulation fails
186
+ async function createPriorityFeeInstructions(
187
+ connection: Connection,
188
+ transaction: Transaction | VersionedTransaction,
189
+ crossChainCore?: CrossChainCore
190
+ ) {
191
+ let unitsUsed = 200_000;
192
+ let simulationAttempts = 0;
193
+
194
+ simulationLoop: while (true) {
195
+ const response = await connection.simulateTransaction(
196
+ transaction as Transaction
197
+ );
198
+
199
+ if (response.value.err) {
200
+ if (checkKnownSimulationError(response.value)) {
201
+ // Number of attempts will be at most 5 for known errors
202
+ if (simulationAttempts < 5) {
203
+ simulationAttempts++;
204
+ await sleep(1000);
205
+ continue simulationLoop;
206
+ }
207
+ } else if (simulationAttempts < 3) {
208
+ // Number of attempts will be at most 3 for unknown errors
209
+ simulationAttempts++;
210
+ await sleep(1000);
211
+ continue simulationLoop;
212
+ }
213
+
214
+ // Still failing after multiple attempts for both known and unknown errors
215
+ // We should throw in that case
216
+ throw new Error(
217
+ `Simulation failed: ${JSON.stringify(response.value.err)}\nLogs:\n${(
218
+ response.value.logs || []
219
+ ).join("\n ")}`
220
+ ).message;
221
+ } else {
222
+ // Simulation was successful
223
+ if (response.value.unitsConsumed) {
224
+ unitsUsed = response.value.unitsConsumed;
225
+ }
226
+ break;
227
+ }
228
+ }
229
+
230
+ const unitBudget = Math.floor(unitsUsed * 1.2); // Budget in 20% headroom
231
+
232
+ const instructions: TransactionInstruction[] = [];
233
+ instructions.push(
234
+ ComputeBudgetProgram.setComputeUnitLimit({
235
+ // Set compute budget to 120% of the units used in the simulated transaction
236
+ units: unitBudget,
237
+ })
238
+ );
239
+
240
+ const {
241
+ percentile = 0.9,
242
+ percentileMultiple = 1,
243
+ min = 100_000,
244
+ max = 100_000_000,
245
+ } = crossChainCore?._dappConfig?.solanaConfig?.priorityFeeConfig ?? {};
246
+
247
+ const calculateFee = async (
248
+ rpcProvider?: SolanaRpcProvider
249
+ ): Promise<{ fee: number; methodUsed: "triton" | "default" | "minimum" }> => {
250
+ if (rpcProvider === "triton") {
251
+ // Triton has an experimental RPC method that accepts a percentile paramater
252
+ // and usually gives more accurate fee numbers.
253
+ try {
254
+ const fee = await determinePriorityFeeTritonOne(
255
+ connection,
256
+ transaction,
257
+ percentile,
258
+ percentileMultiple,
259
+ min,
260
+ max
261
+ );
262
+
263
+ return {
264
+ fee,
265
+ methodUsed: "triton",
266
+ };
267
+ } catch (e) {
268
+ console.warn(`Failed to determine priority fee using Triton RPC:`, e);
269
+ }
270
+ }
271
+
272
+ try {
273
+ // By default, use generic Solana RPC method
274
+ const fee = await determinePriorityFee(
275
+ connection,
276
+ transaction,
277
+ percentile,
278
+ percentileMultiple,
279
+ min,
280
+ max
281
+ );
282
+
283
+ return {
284
+ fee,
285
+ methodUsed: "default",
286
+ };
287
+ } catch (e) {
288
+ console.warn(`Failed to determine priority fee using Triton RPC:`, e);
289
+
290
+ return {
291
+ fee: min,
292
+ methodUsed: "minimum",
293
+ };
294
+ }
295
+ };
296
+
297
+ const rpcProvider = determineRpcProvider(connection.rpcEndpoint);
298
+
299
+ const { fee, methodUsed } = await calculateFee(rpcProvider);
300
+
301
+ const maxFeeInSol =
302
+ (fee /
303
+ // convert microlamports to lamports
304
+ 1e6 /
305
+ // convert lamports to SOL
306
+ LAMPORTS_PER_SOL) *
307
+ // multiply by maximum compute units used
308
+ unitBudget;
309
+
310
+ console.table({
311
+ "RPC Provider": rpcProvider,
312
+ "Method used": methodUsed,
313
+ "Percentile used": percentile,
314
+ "Multiple used": percentileMultiple,
315
+ "Compute budget": unitBudget,
316
+ "Priority fee": fee,
317
+ "Max fee in SOL": maxFeeInSol,
318
+ });
319
+
320
+ instructions.push(
321
+ ComputeBudgetProgram.setComputeUnitPrice({ microLamports: fee })
322
+ );
323
+ return instructions;
324
+ }
325
+
326
+ // Checks response logs for known errors.
327
+ // Returns when the first error is encountered.
328
+ function checkKnownSimulationError(
329
+ response: SimulatedTransactionResponse
330
+ ): boolean {
331
+ const errors = {} as any;
332
+
333
+ // This error occur when the blockhash included in a transaction is not deemed to be valid
334
+ // when a validator processes a transaction. We can retry the simulation to get a valid blockhash.
335
+ if (response.err === "BlockhashNotFound") {
336
+ errors["BlockhashNotFound"] =
337
+ "Blockhash not found during simulation. Trying again.";
338
+ }
339
+
340
+ // Check the response logs for any known errors
341
+ if (response.logs) {
342
+ for (const line of response.logs) {
343
+ // In some cases which aren't deterministic, like a slippage error, we can retry the
344
+ // simulation a few times to get a successful response.
345
+ if (line.includes("SlippageToleranceExceeded")) {
346
+ errors["SlippageToleranceExceeded"] =
347
+ "Slippage failure during simulation. Trying again.";
348
+ }
349
+
350
+ // In this case a require_gte expression was violated during a Swap instruction.
351
+ // We can retry the simulation to get a successful response.
352
+ if (line.includes("RequireGteViolated")) {
353
+ errors["RequireGteViolated"] =
354
+ "Swap instruction failure during simulation. Trying again.";
355
+ }
356
+ }
357
+ }
358
+
359
+ // No known simulation errors found
360
+ if (isEmptyObject(errors)) {
361
+ return false;
362
+ }
363
+
364
+ console.table(errors);
365
+ return true;
366
+ }
367
+
368
+ export async function sleep(timeout: number) {
369
+ return new Promise((resolve) => setTimeout(resolve, timeout));
370
+ }
371
+
372
+ /**
373
+ * Checks whether an object is empty.
374
+ *
375
+ * isEmptyObject(null)
376
+ * // => true
377
+ *
378
+ * isEmptyObject(undefined)
379
+ * // => true
380
+ *
381
+ * isEmptyObject({})
382
+ * // => true
383
+ *
384
+ * isEmptyObject({ 'a': 1 })
385
+ * // => false
386
+ */
387
+ export const isEmptyObject = (value: object | null | undefined) => {
388
+ if (value === null || value === undefined) {
389
+ return true;
390
+ }
391
+
392
+ // Check all property keys for any own prop
393
+ for (const key in value) {
394
+ if (value.hasOwnProperty.call(value, key)) {
395
+ return false;
396
+ }
397
+ }
398
+
399
+ return true;
400
+ };
401
+
402
+ function determineRpcProvider(endpoint: string): SolanaRpcProvider {
403
+ try {
404
+ const url = new URL(endpoint);
405
+ const hostname = url.hostname;
406
+ if (hostname === "rpcpool.com") {
407
+ return "triton";
408
+ } else if (hostname === "helius-rpc.com") {
409
+ return "helius";
410
+ } else if (hostname === "rpc.ankr.com") {
411
+ return "ankr";
412
+ } else {
413
+ return "unknown";
414
+ }
415
+ } catch (e) {
416
+ return "unknown";
417
+ }
418
+ }
@@ -0,0 +1,59 @@
1
+ import { AccountAddressInput, Account } from "@aptos-labs/ts-sdk";
2
+ import { AdapterWallet } from "@aptos-labs/wallet-adapter-core";
3
+ import { routes, AttestationReceipt } from "@wormhole-foundation/sdk/dist/cjs";
4
+ import { Chain, AptosAccount } from "../..";
5
+
6
+ export type WormholeRouteResponse = routes.Route<
7
+ "Mainnet" | "Testnet",
8
+ routes.Options,
9
+ routes.ValidatedTransferParams<routes.Options>,
10
+ routes.Receipt
11
+ >;
12
+
13
+ export type WormholeRequest = routes.RouteTransferRequest<
14
+ "Mainnet" | "Testnet"
15
+ >;
16
+
17
+ export type WormholeQuoteResponse = routes.Quote<
18
+ routes.Options,
19
+ routes.ValidatedTransferParams<routes.Options>,
20
+ any
21
+ >;
22
+
23
+ export interface WormholeQuoteRequest {
24
+ amount: string;
25
+ sourceChain: Chain;
26
+ }
27
+
28
+ export type GasStationApiKey = string;
29
+
30
+ export interface WormholeInitiateTransferRequest {
31
+ sourceChain: Chain;
32
+ wallet: AdapterWallet;
33
+ destinationAddress: AccountAddressInput;
34
+ mainSigner: Account;
35
+ amount?: string;
36
+ sponsorAccount?: Account | GasStationApiKey;
37
+ }
38
+
39
+ export interface WormholeSubmitTransferRequest {
40
+ sourceChain: Chain;
41
+ wallet: AdapterWallet;
42
+ destinationAddress: AccountAddressInput;
43
+ }
44
+
45
+ export interface WormholeClaimTransferRequest {
46
+ receipt: routes.Receipt<AttestationReceipt>;
47
+ mainSigner: AptosAccount;
48
+ sponsorAccount?: AptosAccount | GasStationApiKey;
49
+ }
50
+
51
+ export interface WormholeInitiateTransferResponse {
52
+ destinationChainTxnId: string;
53
+ originChainTxnId: string;
54
+ }
55
+
56
+ export interface WormholeStartTransferResponse {
57
+ originChainTxnId: string;
58
+ receipt: routes.Receipt<AttestationReceipt>;
59
+ }