@aptos-labs/cross-chain-core 5.8.2 → 5.9.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 (48) hide show
  1. package/README.md +26 -0
  2. package/dist/CrossChainCore.d.ts +20 -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 +580 -275
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +583 -274
  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 +1 -1
  13. package/dist/providers/wormhole/signers/AptosLocalSigner.d.ts.map +1 -1
  14. package/dist/providers/wormhole/signers/Signer.d.ts +1 -1
  15. package/dist/providers/wormhole/signers/Signer.d.ts.map +1 -1
  16. package/dist/providers/wormhole/signers/SolanaLocalSigner.d.ts +65 -0
  17. package/dist/providers/wormhole/signers/SolanaLocalSigner.d.ts.map +1 -0
  18. package/dist/providers/wormhole/signers/SolanaSigner.d.ts +12 -20
  19. package/dist/providers/wormhole/signers/SolanaSigner.d.ts.map +1 -1
  20. package/dist/providers/wormhole/signers/solanaUtils.d.ts +68 -0
  21. package/dist/providers/wormhole/signers/solanaUtils.d.ts.map +1 -0
  22. package/dist/providers/wormhole/types.d.ts +43 -0
  23. package/dist/providers/wormhole/types.d.ts.map +1 -1
  24. package/dist/providers/wormhole/utils.d.ts +26 -0
  25. package/dist/providers/wormhole/utils.d.ts.map +1 -0
  26. package/dist/providers/wormhole/wormhole.d.ts +36 -6
  27. package/dist/providers/wormhole/wormhole.d.ts.map +1 -1
  28. package/dist/utils/receiptSerialization.d.ts +38 -0
  29. package/dist/utils/receiptSerialization.d.ts.map +1 -0
  30. package/dist/version.d.ts +1 -1
  31. package/package.json +2 -2
  32. package/src/CrossChainCore.ts +20 -0
  33. package/src/config/mainnet/chains.ts +2 -2
  34. package/src/config/testnet/chains.ts +2 -2
  35. package/src/index.ts +1 -0
  36. package/src/providers/wormhole/index.ts +2 -0
  37. package/src/providers/wormhole/signers/AptosLocalSigner.ts +4 -4
  38. package/src/providers/wormhole/signers/AptosSigner.ts +1 -1
  39. package/src/providers/wormhole/signers/EthereumSigner.ts +3 -3
  40. package/src/providers/wormhole/signers/Signer.ts +4 -4
  41. package/src/providers/wormhole/signers/SolanaLocalSigner.ts +243 -0
  42. package/src/providers/wormhole/signers/SolanaSigner.ts +45 -337
  43. package/src/providers/wormhole/signers/solanaUtils.ts +422 -0
  44. package/src/providers/wormhole/types.ts +68 -0
  45. package/src/providers/wormhole/utils.ts +72 -0
  46. package/src/providers/wormhole/wormhole.ts +182 -120
  47. package/src/utils/receiptSerialization.ts +141 -0
  48. package/src/version.ts +1 -1
@@ -0,0 +1,243 @@
1
+ import {
2
+ Connection,
3
+ Keypair,
4
+ Transaction,
5
+ type Commitment,
6
+ } from "@solana/web3.js";
7
+ import {
8
+ Chain,
9
+ Network,
10
+ SignAndSendSigner,
11
+ TxHash,
12
+ UnsignedTransaction,
13
+ } from "@wormhole-foundation/sdk";
14
+ import {
15
+ addPriorityFeeInstructions,
16
+ PriorityFeeConfig,
17
+ sendAndConfirmTransaction,
18
+ } from "./solanaUtils";
19
+
20
+ export interface SolanaLocalSignerConfig {
21
+ /** The Solana keypair to sign transactions with */
22
+ keypair: Keypair;
23
+ /** The Solana RPC connection */
24
+ connection: Connection;
25
+ /** Transaction confirmation commitment level (default: "finalized") */
26
+ commitment?: Commitment;
27
+ /** Retry interval in ms when transaction is not confirmed (default: 5000) */
28
+ retryIntervalMs?: number;
29
+ /** Priority fee configuration for faster transaction landing */
30
+ priorityFeeConfig?: PriorityFeeConfig;
31
+ /** Enable verbose logging (default: false) */
32
+ verbose?: boolean;
33
+ }
34
+
35
+ /**
36
+ * Server-side Solana signer for programmatic transaction signing.
37
+ * Use this when you want to sign Solana transactions without user wallet interaction,
38
+ * such as for server-side claim operations.
39
+ *
40
+ * @example
41
+ * ```typescript
42
+ * import { SolanaLocalSigner } from "@aptos-labs/cross-chain-core";
43
+ * import { Connection, Keypair } from "@solana/web3.js";
44
+ * import bs58 from "bs58";
45
+ *
46
+ * const keypair = Keypair.fromSecretKey(bs58.decode(process.env.SOLANA_CLAIM_SIGNER_KEY));
47
+ * const connection = new Connection("https://api.mainnet-beta.solana.com");
48
+ *
49
+ * const signer = new SolanaLocalSigner({
50
+ * keypair,
51
+ * connection,
52
+ * // Optional: configure priority fees for faster landing
53
+ * priorityFeeConfig: {
54
+ * percentile: 0.9,
55
+ * min: 100_000,
56
+ * max: 1_000_000,
57
+ * },
58
+ * });
59
+ * await cctpRoute.complete(signer, receipt);
60
+ * ```
61
+ */
62
+ export class SolanaLocalSigner<N extends Network, C extends Chain>
63
+ implements SignAndSendSigner<N, C>
64
+ {
65
+ private keypair: Keypair;
66
+ private connection: Connection;
67
+ private commitment: Commitment;
68
+ private retryIntervalMs: number;
69
+ private priorityFeeConfig?: PriorityFeeConfig;
70
+ private verbose: boolean;
71
+ private _claimedTransactionHashes: string[] = [];
72
+
73
+ constructor(config: SolanaLocalSignerConfig) {
74
+ this.keypair = config.keypair;
75
+ this.connection = config.connection;
76
+ this.commitment = config.commitment ?? "finalized";
77
+ this.retryIntervalMs = config.retryIntervalMs ?? 5000;
78
+ this.priorityFeeConfig = config.priorityFeeConfig;
79
+ this.verbose = config.verbose ?? false;
80
+ }
81
+
82
+ chain(): C {
83
+ return "Solana" as C;
84
+ }
85
+
86
+ address(): string {
87
+ return this.keypair.publicKey.toBase58();
88
+ }
89
+
90
+ /**
91
+ * Returns all transaction hashes from the most recent signAndSend call,
92
+ * joined by comma. If only one transaction was signed, returns a single hash string.
93
+ */
94
+ claimedTransactionHashes(): string {
95
+ return this._claimedTransactionHashes.join(",");
96
+ }
97
+
98
+ async signAndSend(txs: UnsignedTransaction<N, C>[]): Promise<TxHash[]> {
99
+ const txHashes: TxHash[] = [];
100
+ this._claimedTransactionHashes = [];
101
+
102
+ for (const tx of txs) {
103
+ const txId = await this.signAndSendTransaction(tx);
104
+ txHashes.push(txId);
105
+ this._claimedTransactionHashes.push(txId);
106
+ }
107
+ return txHashes;
108
+ }
109
+
110
+ private async signAndSendTransaction(request: any): Promise<string> {
111
+ const { blockhash, lastValidBlockHeight } =
112
+ await this.connection.getLatestBlockhash(this.commitment);
113
+
114
+ // Wormhole SDK wraps transactions in SolanaUnsignedTransaction
115
+ // The actual transaction is in the .transaction property
116
+ // Sometimes it's nested: request.transaction.transaction
117
+ let unsignedTx = request.transaction ?? request;
118
+
119
+ // Capture additional signers before unwrapping.
120
+ // SolanaUnsignedTransaction nests them at request.transaction.signers
121
+ // (see SolanaSigner.ts for the client-side equivalent).
122
+ const additionalSigners = request.transaction?.signers;
123
+
124
+ // Unwrap nested transaction wrappers (Wormhole SDK's SolanaUnsignedTransaction)
125
+ const MAX_UNWRAP_DEPTH = 10;
126
+ let unwrapDepth = 0;
127
+ while (
128
+ unsignedTx &&
129
+ typeof unsignedTx === "object" &&
130
+ "transaction" in unsignedTx &&
131
+ !(unsignedTx instanceof Transaction) &&
132
+ !("signatures" in unsignedTx && "message" in unsignedTx)
133
+ ) {
134
+ if (++unwrapDepth > MAX_UNWRAP_DEPTH) {
135
+ throw new Error(
136
+ "Transaction unwrapping exceeded maximum depth — possible circular nesting",
137
+ );
138
+ }
139
+ unsignedTx = unsignedTx.transaction;
140
+ }
141
+
142
+ // Check if this is a versioned transaction using duck typing
143
+ // (VersionedTransaction has .message and .signatures properties)
144
+ const isVersioned =
145
+ unsignedTx.message !== undefined &&
146
+ unsignedTx.signatures !== undefined &&
147
+ typeof unsignedTx.message.recentBlockhash !== "undefined";
148
+
149
+ if (isVersioned) {
150
+ // For versioned transactions, we need to update the blockhash and sign
151
+ unsignedTx.message.recentBlockhash = blockhash;
152
+
153
+ // Note: Priority fees for versioned transactions would require rebuilding
154
+ // the message, which is more complex. For now, we skip priority fees for versioned txs.
155
+ if (this.verbose || this.priorityFeeConfig) {
156
+ console.warn(
157
+ "SolanaLocalSigner: Versioned transaction detected — priority fees are not applied. " +
158
+ "Consider using legacy transactions if priority fees are required.",
159
+ );
160
+ }
161
+ unsignedTx.sign([this.keypair]);
162
+
163
+ // Also sign with any additional signers from Wormhole SDK
164
+ if (additionalSigners && additionalSigners.length > 0) {
165
+ unsignedTx.sign(additionalSigners);
166
+ }
167
+ } else if (unsignedTx instanceof Transaction) {
168
+ // Legacy transaction handling
169
+ unsignedTx.recentBlockhash = blockhash;
170
+ unsignedTx.lastValidBlockHeight = lastValidBlockHeight;
171
+
172
+ // Add priority fee instructions if configured
173
+ if (this.priorityFeeConfig) {
174
+ await addPriorityFeeInstructions(
175
+ this.connection,
176
+ unsignedTx,
177
+ this.priorityFeeConfig,
178
+ this.verbose,
179
+ );
180
+ }
181
+
182
+ // Sign with the local keypair (and any additional signers in a single call).
183
+ // Transaction.sign() resets the signatures array to only the provided
184
+ // signers, so calling partialSign() afterwards for extra signers would
185
+ // throw "unknown signer". Passing everyone to sign() at once avoids this.
186
+ if (additionalSigners && additionalSigners.length > 0) {
187
+ unsignedTx.sign(this.keypair, ...additionalSigners);
188
+ } else {
189
+ unsignedTx.sign(this.keypair);
190
+ }
191
+ } else if (
192
+ unsignedTx.recentBlockhash !== undefined ||
193
+ unsignedTx.feePayer !== undefined
194
+ ) {
195
+ // Looks like a legacy transaction but instanceof check failed
196
+ // This can happen with different module instances
197
+ unsignedTx.recentBlockhash = blockhash;
198
+ unsignedTx.lastValidBlockHeight = lastValidBlockHeight;
199
+
200
+ // Add priority fee instructions if configured
201
+ if (this.priorityFeeConfig) {
202
+ await addPriorityFeeInstructions(
203
+ this.connection,
204
+ unsignedTx,
205
+ this.priorityFeeConfig,
206
+ this.verbose,
207
+ );
208
+ }
209
+
210
+ // Same rationale as the instanceof Transaction branch above:
211
+ // include all signers in a single sign() call to avoid "unknown signer".
212
+ if (additionalSigners && additionalSigners.length > 0) {
213
+ unsignedTx.sign(this.keypair, ...additionalSigners);
214
+ } else {
215
+ unsignedTx.sign(this.keypair);
216
+ }
217
+ } else {
218
+ throw new Error(
219
+ `Unsupported transaction type: ${unsignedTx?.constructor?.name}`,
220
+ );
221
+ }
222
+
223
+ const serializedTx = unsignedTx.serialize();
224
+
225
+ // Use shared utility for sending and confirming
226
+ const signature = await sendAndConfirmTransaction(
227
+ serializedTx,
228
+ blockhash,
229
+ lastValidBlockHeight,
230
+ {
231
+ connection: this.connection,
232
+ commitment: this.commitment,
233
+ retryIntervalMs: this.retryIntervalMs,
234
+ verbose: this.verbose,
235
+ },
236
+ );
237
+
238
+ return signature;
239
+ }
240
+ }
241
+
242
+ // Re-export PriorityFeeConfig for convenience
243
+ export type { PriorityFeeConfig };
@@ -1,30 +1,34 @@
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
1
+ /**
2
+ * Client-side Solana signer for wallet-based transaction signing.
3
+ * This function signs and sends the transaction while constantly checking for confirmation
4
+ * and resending the transaction if it hasn't been confirmed after the specified interval.
5
+ */
3
6
 
4
7
  import {
5
- ComputeBudgetProgram,
6
8
  ConfirmOptions,
7
- LAMPORTS_PER_SOL,
8
- SimulatedTransactionResponse,
9
- TransactionInstruction,
9
+ Transaction,
10
10
  VersionedTransaction,
11
11
  } from "@solana/web3.js";
12
12
 
13
- import { Transaction } from "@solana/web3.js";
14
- import { RpcResponseAndContext, SignatureResult } from "@solana/web3.js";
15
- import {
16
- determinePriorityFee,
17
- determinePriorityFeeTritonOne,
18
- SolanaUnsignedTransaction,
19
- } from "@wormhole-foundation/sdk-solana";
20
-
21
13
  import { Connection } from "@solana/web3.js";
22
14
  import { Network } from "@wormhole-foundation/sdk";
15
+ import { SolanaUnsignedTransaction } from "@wormhole-foundation/sdk-solana";
23
16
  import { AdapterWallet } from "@aptos-labs/wallet-adapter-core";
24
17
  import { CrossChainCore } from "../../../CrossChainCore";
25
18
  import { SolanaDerivedWallet } from "@aptos-labs/derived-wallet-solana";
26
-
27
- export type SolanaRpcProvider = "triton" | "helius" | "ankr" | "unknown";
19
+ import {
20
+ addPriorityFeeInstructions,
21
+ sendAndConfirmTransaction,
22
+ PriorityFeeConfig,
23
+ } from "./solanaUtils";
24
+
25
+ // Re-export types for backwards compatibility
26
+ export type { SolanaRpcProvider, PriorityFeeConfig } from "./solanaUtils";
27
+ export {
28
+ sleep,
29
+ isEmptyObject,
30
+ determineRpcProvider,
31
+ } from "./solanaUtils";
28
32
 
29
33
  // See https://docs.triton.one/chains/solana/sending-txs for more information
30
34
  export async function signAndSendTransaction(
@@ -34,7 +38,7 @@ export async function signAndSendTransaction(
34
38
  crossChainCore?: CrossChainCore,
35
39
  ) {
36
40
  if (!wallet || !(wallet instanceof SolanaDerivedWallet)) {
37
- throw new Error("Invalid wallet type or missing Solana wallet").message;
41
+ throw new Error("Invalid wallet type or missing Solana wallet");
38
42
  }
39
43
 
40
44
  const commitment = options?.commitment ?? "finalized";
@@ -47,31 +51,22 @@ export async function signAndSendTransaction(
47
51
  const { blockhash, lastValidBlockHeight } =
48
52
  await connection.getLatestBlockhash(commitment);
49
53
 
50
- /**
51
- * TODO: Priority Fee is supported, but needs to come from dapp config
52
- */
54
+ // Add priority fee instructions
53
55
  const unsignedTx = await setPriorityFeeInstructions(
54
56
  connection,
55
57
  blockhash,
56
58
  lastValidBlockHeight,
57
59
  request,
58
- crossChainCore,
60
+ crossChainCore?._dappConfig?.solanaConfig?.priorityFeeConfig,
59
61
  );
60
62
 
61
- let confirmTransactionPromise: Promise<
62
- RpcResponseAndContext<SignatureResult>
63
- > | null = null;
64
- let confirmedTx: RpcResponseAndContext<SignatureResult> | null = null;
65
- let txSendAttempts = 1;
66
- let signature = "";
67
-
68
63
  if (!wallet.solanaWallet.signTransaction) {
69
- throw new Error("Wallet does not support signing transactions").message;
64
+ throw new Error("Wallet does not support signing transactions");
70
65
  }
71
66
 
72
67
  const tx = await wallet.solanaWallet.signTransaction(unsignedTx);
73
68
 
74
- if (!tx) throw new Error("Failed to sign transaction").message;
69
+ if (!tx) throw new Error("Failed to sign transaction");
75
70
 
76
71
  // Order matters. Phantom's Lighthouse security requires wallet to sign first,
77
72
  // then additional signers sign afterward
@@ -80,332 +75,45 @@ export async function signAndSendTransaction(
80
75
  }
81
76
 
82
77
  const serializedTx = tx.serialize();
83
- const sendOptions = {
84
- skipPreflight: true,
85
- maxRetries: 0,
86
- preFlightCommitment: commitment, // See PR and linked issue for why setting this matters: https://github.com/anza-xyz/agave/pull/483
87
- };
88
- signature = await connection.sendRawTransaction(serializedTx, sendOptions);
89
- confirmTransactionPromise = connection.confirmTransaction(
78
+
79
+ // Use shared utility for sending and confirming
80
+ const signature = await sendAndConfirmTransaction(
81
+ serializedTx,
82
+ blockhash,
83
+ lastValidBlockHeight,
90
84
  {
91
- signature,
92
- blockhash,
93
- lastValidBlockHeight,
85
+ connection,
86
+ commitment,
87
+ retryIntervalMs: 5000,
88
+ verbose: false,
94
89
  },
95
- commitment,
96
90
  );
97
91
 
98
- // This loop will break once the transaction has been confirmed or the block height is exceeded.
99
- // An exception will be thrown if the block height is exceeded by the confirmTransactionPromise.
100
- // The transaction will be resent if it hasn't been confirmed after the interval.
101
- const txRetryInterval = 5000;
102
- while (!confirmedTx) {
103
- confirmedTx = await Promise.race([
104
- confirmTransactionPromise,
105
- new Promise<null>((resolve) =>
106
- setTimeout(() => {
107
- resolve(null);
108
- }, txRetryInterval),
109
- ),
110
- ]);
111
- if (confirmedTx) {
112
- break;
113
- }
114
- console.log(
115
- `Tx not confirmed after ${
116
- txRetryInterval * txSendAttempts++
117
- }ms, resending`,
118
- );
119
- try {
120
- await connection.sendRawTransaction(serializedTx, sendOptions);
121
- } catch (e) {
122
- console.error("Failed to resend transaction:", e);
123
- }
124
- }
125
-
126
- if (confirmedTx.value.err) {
127
- let errorMessage = `Transaction failed: ${confirmedTx.value.err}`;
128
- if (typeof confirmedTx.value.err === "object") {
129
- try {
130
- errorMessage = `Transaction failed: ${JSON.stringify(
131
- confirmedTx.value.err,
132
- (_key, value) =>
133
- typeof value === "bigint" ? value.toString() : value, // Handle bigint props
134
- )}`;
135
- } catch (e: unknown) {
136
- // Most likely a circular reference error, we can't stringify this error object.
137
- // See for more details:
138
- // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#exceptions
139
- errorMessage = `Transaction failed: Unknown error`;
140
- }
141
- }
142
- throw new Error(`Transaction failed: ${errorMessage}`).message;
143
- }
144
-
145
92
  return signature;
146
93
  }
147
94
 
95
+ /**
96
+ * Prepares a transaction with priority fee instructions.
97
+ */
148
98
  export async function setPriorityFeeInstructions(
149
99
  connection: Connection,
150
100
  blockhash: string,
151
101
  lastValidBlockHeight: number,
152
102
  request: SolanaUnsignedTransaction<Network>,
153
- crossChainCore?: CrossChainCore,
103
+ priorityFeeConfig?: PriorityFeeConfig,
154
104
  ): Promise<Transaction | VersionedTransaction> {
155
105
  const unsignedTx = request.transaction.transaction as Transaction;
156
106
 
157
- const computeBudgetIxFilter = (ix: TransactionInstruction) =>
158
- ix.programId.toString() !== "ComputeBudget111111111111111111111111111111";
159
-
160
107
  unsignedTx.recentBlockhash = blockhash;
161
108
  unsignedTx.lastValidBlockHeight = lastValidBlockHeight;
162
109
 
163
- // Remove existing compute budget instructions if they were added by the SDK
164
- unsignedTx.instructions = unsignedTx.instructions.filter(
165
- computeBudgetIxFilter,
166
- );
167
- unsignedTx.add(
168
- ...(await createPriorityFeeInstructions(
169
- connection,
170
- unsignedTx,
171
- crossChainCore,
172
- )),
110
+ // Add priority fee instructions using shared utility
111
+ await addPriorityFeeInstructions(
112
+ connection,
113
+ unsignedTx,
114
+ priorityFeeConfig,
115
+ false,
173
116
  );
174
117
 
175
118
  return unsignedTx;
176
119
  }
177
-
178
- // This will throw if the simulation fails
179
- async function createPriorityFeeInstructions(
180
- connection: Connection,
181
- transaction: Transaction | VersionedTransaction,
182
- crossChainCore?: CrossChainCore,
183
- ) {
184
- let unitsUsed = 200_000;
185
- let simulationAttempts = 0;
186
-
187
- simulationLoop: while (true) {
188
- const response = await connection.simulateTransaction(
189
- transaction as Transaction,
190
- );
191
-
192
- if (response.value.err) {
193
- if (checkKnownSimulationError(response.value)) {
194
- // Number of attempts will be at most 5 for known errors
195
- if (simulationAttempts < 5) {
196
- simulationAttempts++;
197
- await sleep(1000);
198
- continue simulationLoop;
199
- }
200
- } else if (simulationAttempts < 3) {
201
- // Number of attempts will be at most 3 for unknown errors
202
- simulationAttempts++;
203
- await sleep(1000);
204
- continue simulationLoop;
205
- }
206
-
207
- // Still failing after multiple attempts for both known and unknown errors
208
- // We should throw in that case
209
- throw new Error(
210
- `Simulation failed: ${JSON.stringify(response.value.err)}\nLogs:\n${(
211
- response.value.logs || []
212
- ).join("\n ")}`,
213
- ).message;
214
- } else {
215
- // Simulation was successful
216
- if (response.value.unitsConsumed) {
217
- unitsUsed = response.value.unitsConsumed;
218
- }
219
- break;
220
- }
221
- }
222
-
223
- const unitBudget = Math.floor(unitsUsed * 1.2); // Budget in 20% headroom
224
-
225
- const instructions: TransactionInstruction[] = [];
226
- instructions.push(
227
- ComputeBudgetProgram.setComputeUnitLimit({
228
- // Set compute budget to 120% of the units used in the simulated transaction
229
- units: unitBudget,
230
- }),
231
- );
232
-
233
- const {
234
- percentile = 0.9,
235
- percentileMultiple = 1,
236
- min = 100_000,
237
- max = 100_000_000,
238
- } = crossChainCore?._dappConfig?.solanaConfig?.priorityFeeConfig ?? {};
239
-
240
- const calculateFee = async (
241
- rpcProvider?: SolanaRpcProvider,
242
- ): Promise<{ fee: number; methodUsed: "triton" | "default" | "minimum" }> => {
243
- if (rpcProvider === "triton") {
244
- // Triton has an experimental RPC method that accepts a percentile paramater
245
- // and usually gives more accurate fee numbers.
246
- try {
247
- const fee = await determinePriorityFeeTritonOne(
248
- connection,
249
- transaction,
250
- percentile,
251
- percentileMultiple,
252
- min,
253
- max,
254
- );
255
-
256
- return {
257
- fee,
258
- methodUsed: "triton",
259
- };
260
- } catch (e) {
261
- console.warn(`Failed to determine priority fee using Triton RPC:`, e);
262
- }
263
- }
264
-
265
- try {
266
- // By default, use generic Solana RPC method
267
- const fee = await determinePriorityFee(
268
- connection,
269
- transaction,
270
- percentile,
271
- percentileMultiple,
272
- min,
273
- max,
274
- );
275
-
276
- return {
277
- fee,
278
- methodUsed: "default",
279
- };
280
- } catch (e) {
281
- console.warn(`Failed to determine priority fee using Triton RPC:`, e);
282
-
283
- return {
284
- fee: min,
285
- methodUsed: "minimum",
286
- };
287
- }
288
- };
289
-
290
- const rpcProvider = determineRpcProvider(connection.rpcEndpoint);
291
-
292
- const { fee, methodUsed } = await calculateFee(rpcProvider);
293
-
294
- const maxFeeInSol =
295
- (fee /
296
- // convert microlamports to lamports
297
- 1e6 /
298
- // convert lamports to SOL
299
- LAMPORTS_PER_SOL) *
300
- // multiply by maximum compute units used
301
- unitBudget;
302
-
303
- console.table({
304
- "RPC Provider": rpcProvider,
305
- "Method used": methodUsed,
306
- "Percentile used": percentile,
307
- "Multiple used": percentileMultiple,
308
- "Compute budget": unitBudget,
309
- "Priority fee": fee,
310
- "Max fee in SOL": maxFeeInSol,
311
- });
312
-
313
- instructions.push(
314
- ComputeBudgetProgram.setComputeUnitPrice({ microLamports: fee }),
315
- );
316
- return instructions;
317
- }
318
-
319
- // Checks response logs for known errors.
320
- // Returns when the first error is encountered.
321
- function checkKnownSimulationError(
322
- response: SimulatedTransactionResponse,
323
- ): boolean {
324
- const errors = {} as any;
325
-
326
- // This error occur when the blockhash included in a transaction is not deemed to be valid
327
- // when a validator processes a transaction. We can retry the simulation to get a valid blockhash.
328
- if (response.err === "BlockhashNotFound") {
329
- errors["BlockhashNotFound"] =
330
- "Blockhash not found during simulation. Trying again.";
331
- }
332
-
333
- // Check the response logs for any known errors
334
- if (response.logs) {
335
- for (const line of response.logs) {
336
- // In some cases which aren't deterministic, like a slippage error, we can retry the
337
- // simulation a few times to get a successful response.
338
- if (line.includes("SlippageToleranceExceeded")) {
339
- errors["SlippageToleranceExceeded"] =
340
- "Slippage failure during simulation. Trying again.";
341
- }
342
-
343
- // In this case a require_gte expression was violated during a Swap instruction.
344
- // We can retry the simulation to get a successful response.
345
- if (line.includes("RequireGteViolated")) {
346
- errors["RequireGteViolated"] =
347
- "Swap instruction failure during simulation. Trying again.";
348
- }
349
- }
350
- }
351
-
352
- // No known simulation errors found
353
- if (isEmptyObject(errors)) {
354
- return false;
355
- }
356
-
357
- console.table(errors);
358
- return true;
359
- }
360
-
361
- export async function sleep(timeout: number) {
362
- return new Promise((resolve) => setTimeout(resolve, timeout));
363
- }
364
-
365
- /**
366
- * Checks whether an object is empty.
367
- *
368
- * isEmptyObject(null)
369
- * // => true
370
- *
371
- * isEmptyObject(undefined)
372
- * // => true
373
- *
374
- * isEmptyObject({})
375
- * // => true
376
- *
377
- * isEmptyObject({ 'a': 1 })
378
- * // => false
379
- */
380
- export const isEmptyObject = (value: object | null | undefined) => {
381
- if (value === null || value === undefined) {
382
- return true;
383
- }
384
-
385
- // Check all property keys for any own prop
386
- for (const key in value) {
387
- if (value.hasOwnProperty.call(value, key)) {
388
- return false;
389
- }
390
- }
391
-
392
- return true;
393
- };
394
-
395
- function determineRpcProvider(endpoint: string): SolanaRpcProvider {
396
- try {
397
- const url = new URL(endpoint);
398
- const hostname = url.hostname;
399
- if (hostname === "rpcpool.com") {
400
- return "triton";
401
- } else if (hostname === "helius-rpc.com") {
402
- return "helius";
403
- } else if (hostname === "rpc.ankr.com") {
404
- return "ankr";
405
- } else {
406
- return "unknown";
407
- }
408
- } catch (e) {
409
- return "unknown";
410
- }
411
- }