@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.
- package/README.md +26 -0
- package/dist/CrossChainCore.d.ts +20 -0
- package/dist/CrossChainCore.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +580 -275
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +583 -274
- package/dist/index.mjs.map +1 -1
- package/dist/providers/wormhole/index.d.ts +2 -0
- package/dist/providers/wormhole/index.d.ts.map +1 -1
- package/dist/providers/wormhole/signers/AptosLocalSigner.d.ts +1 -1
- package/dist/providers/wormhole/signers/AptosLocalSigner.d.ts.map +1 -1
- package/dist/providers/wormhole/signers/Signer.d.ts +1 -1
- package/dist/providers/wormhole/signers/Signer.d.ts.map +1 -1
- package/dist/providers/wormhole/signers/SolanaLocalSigner.d.ts +65 -0
- package/dist/providers/wormhole/signers/SolanaLocalSigner.d.ts.map +1 -0
- package/dist/providers/wormhole/signers/SolanaSigner.d.ts +12 -20
- package/dist/providers/wormhole/signers/SolanaSigner.d.ts.map +1 -1
- package/dist/providers/wormhole/signers/solanaUtils.d.ts +68 -0
- package/dist/providers/wormhole/signers/solanaUtils.d.ts.map +1 -0
- package/dist/providers/wormhole/types.d.ts +43 -0
- package/dist/providers/wormhole/types.d.ts.map +1 -1
- package/dist/providers/wormhole/utils.d.ts +26 -0
- package/dist/providers/wormhole/utils.d.ts.map +1 -0
- package/dist/providers/wormhole/wormhole.d.ts +36 -6
- package/dist/providers/wormhole/wormhole.d.ts.map +1 -1
- package/dist/utils/receiptSerialization.d.ts +38 -0
- package/dist/utils/receiptSerialization.d.ts.map +1 -0
- package/dist/version.d.ts +1 -1
- package/package.json +2 -2
- package/src/CrossChainCore.ts +20 -0
- package/src/config/mainnet/chains.ts +2 -2
- package/src/config/testnet/chains.ts +2 -2
- package/src/index.ts +1 -0
- package/src/providers/wormhole/index.ts +2 -0
- package/src/providers/wormhole/signers/AptosLocalSigner.ts +4 -4
- package/src/providers/wormhole/signers/AptosSigner.ts +1 -1
- package/src/providers/wormhole/signers/EthereumSigner.ts +3 -3
- package/src/providers/wormhole/signers/Signer.ts +4 -4
- package/src/providers/wormhole/signers/SolanaLocalSigner.ts +243 -0
- package/src/providers/wormhole/signers/SolanaSigner.ts +45 -337
- package/src/providers/wormhole/signers/solanaUtils.ts +422 -0
- package/src/providers/wormhole/types.ts +68 -0
- package/src/providers/wormhole/utils.ts +72 -0
- package/src/providers/wormhole/wormhole.ts +182 -120
- package/src/utils/receiptSerialization.ts +141 -0
- 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
|
-
|
|
2
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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")
|
|
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")
|
|
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")
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
}
|