solana-tx-kit 0.1.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/LICENSE +21 -0
- package/README.md +369 -0
- package/dist/index.cjs +1297 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +675 -0
- package/dist/index.d.ts +675 -0
- package/dist/index.js +1266 -0
- package/dist/index.js.map +1 -0
- package/package.json +60 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1297 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var web3_js = require('@solana/web3.js');
|
|
4
|
+
var events = require('events');
|
|
5
|
+
|
|
6
|
+
// src/sender/transaction-sender.ts
|
|
7
|
+
var JITO_TIP_ACCOUNTS = Object.freeze([
|
|
8
|
+
new web3_js.PublicKey("96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5"),
|
|
9
|
+
new web3_js.PublicKey("HFqU5x63VTqvQss8hp11i4wVV8bD44PvwucfZ2bU7gRe"),
|
|
10
|
+
new web3_js.PublicKey("Cw8CFyM9FkoMi7K7Crf6HNQqf4uEMzpKw6QNghXLvLkY"),
|
|
11
|
+
new web3_js.PublicKey("ADaUMid9yfUytqMBgopwjb2DTLSokTSzL1zt6iGPaS49"),
|
|
12
|
+
new web3_js.PublicKey("DfXygSm4jCyNCybVYYK6DwvWqjKee8pbDmJGcLWNDXjh"),
|
|
13
|
+
new web3_js.PublicKey("ADuUkR4vqLUMWXxW9gh6D6L8pMSawimctcNZ5pGwDcEt"),
|
|
14
|
+
new web3_js.PublicKey("DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL"),
|
|
15
|
+
new web3_js.PublicKey("3AVi9Tg9Uo68tJfuvoKvqKNWKkC5wPdSSdeBnizKZ6jT")
|
|
16
|
+
]);
|
|
17
|
+
var JITO_BLOCK_ENGINE_URL = "https://mainnet.block-engine.jito.wtf";
|
|
18
|
+
var JITO_MIN_TIP_LAMPORTS = 1e3;
|
|
19
|
+
var DEFAULT_RETRY_CONFIG = {
|
|
20
|
+
maxRetries: 3,
|
|
21
|
+
baseDelayMs: 500,
|
|
22
|
+
maxDelayMs: 1e4,
|
|
23
|
+
backoffMultiplier: 2
|
|
24
|
+
};
|
|
25
|
+
var DEFAULT_PRIORITY_FEE_CONFIG = {
|
|
26
|
+
targetPercentile: 75,
|
|
27
|
+
maxMicroLamports: 1e6,
|
|
28
|
+
minMicroLamports: 1e3,
|
|
29
|
+
defaultComputeUnits: 2e5
|
|
30
|
+
};
|
|
31
|
+
var DEFAULT_CONFIRMATION_CONFIG = {
|
|
32
|
+
commitment: "confirmed",
|
|
33
|
+
timeoutMs: 6e4,
|
|
34
|
+
pollIntervalMs: 2e3,
|
|
35
|
+
useWebSocket: true
|
|
36
|
+
};
|
|
37
|
+
var DEFAULT_BLOCKHASH_CONFIG = {
|
|
38
|
+
ttlMs: 6e4,
|
|
39
|
+
refreshIntervalMs: 3e4,
|
|
40
|
+
commitment: "confirmed"
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// src/errors.ts
|
|
44
|
+
var SolTxErrorCode = /* @__PURE__ */ ((SolTxErrorCode2) => {
|
|
45
|
+
SolTxErrorCode2["RETRIES_EXHAUSTED"] = "RETRIES_EXHAUSTED";
|
|
46
|
+
SolTxErrorCode2["NON_RETRYABLE"] = "NON_RETRYABLE";
|
|
47
|
+
SolTxErrorCode2["BLOCKHASH_EXPIRED"] = "BLOCKHASH_EXPIRED";
|
|
48
|
+
SolTxErrorCode2["BLOCKHASH_FETCH_FAILED"] = "BLOCKHASH_FETCH_FAILED";
|
|
49
|
+
SolTxErrorCode2["SIMULATION_FAILED"] = "SIMULATION_FAILED";
|
|
50
|
+
SolTxErrorCode2["INSUFFICIENT_FUNDS"] = "INSUFFICIENT_FUNDS";
|
|
51
|
+
SolTxErrorCode2["CONFIRMATION_TIMEOUT"] = "CONFIRMATION_TIMEOUT";
|
|
52
|
+
SolTxErrorCode2["TRANSACTION_FAILED"] = "TRANSACTION_FAILED";
|
|
53
|
+
SolTxErrorCode2["ALL_ENDPOINTS_UNHEALTHY"] = "ALL_ENDPOINTS_UNHEALTHY";
|
|
54
|
+
SolTxErrorCode2["RATE_LIMITED"] = "RATE_LIMITED";
|
|
55
|
+
SolTxErrorCode2["BUNDLE_FAILED"] = "BUNDLE_FAILED";
|
|
56
|
+
SolTxErrorCode2["BUNDLE_DROPPED"] = "BUNDLE_DROPPED";
|
|
57
|
+
SolTxErrorCode2["TIP_TOO_LOW"] = "TIP_TOO_LOW";
|
|
58
|
+
SolTxErrorCode2["FEE_ESTIMATION_FAILED"] = "FEE_ESTIMATION_FAILED";
|
|
59
|
+
return SolTxErrorCode2;
|
|
60
|
+
})(SolTxErrorCode || {});
|
|
61
|
+
var SolTxError = class extends Error {
|
|
62
|
+
code;
|
|
63
|
+
cause;
|
|
64
|
+
context;
|
|
65
|
+
constructor(code, message, options) {
|
|
66
|
+
super(message);
|
|
67
|
+
this.name = "SolTxError";
|
|
68
|
+
this.code = code;
|
|
69
|
+
this.cause = options?.cause;
|
|
70
|
+
this.context = options?.context;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
var RetryableError = class extends SolTxError {
|
|
74
|
+
retryAfterMs;
|
|
75
|
+
constructor(code, message, options) {
|
|
76
|
+
super(code, message, options);
|
|
77
|
+
this.name = "RetryableError";
|
|
78
|
+
this.retryAfterMs = options?.retryAfterMs;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// src/blockhash/blockhash-manager.ts
|
|
83
|
+
var BlockhashManager = class {
|
|
84
|
+
constructor(connection, config, logger) {
|
|
85
|
+
this.connection = connection;
|
|
86
|
+
this.logger = logger;
|
|
87
|
+
this.config = { ...DEFAULT_BLOCKHASH_CONFIG, ...config };
|
|
88
|
+
}
|
|
89
|
+
cache = null;
|
|
90
|
+
refreshInterval;
|
|
91
|
+
fetchPromise = null;
|
|
92
|
+
config;
|
|
93
|
+
/** Start background refresh loop */
|
|
94
|
+
start() {
|
|
95
|
+
if (this.refreshInterval) return;
|
|
96
|
+
this.refreshInterval = setInterval(() => {
|
|
97
|
+
this.refreshBlockhash().catch((err) => {
|
|
98
|
+
this.logger?.warn("Background blockhash refresh failed", { error: String(err) });
|
|
99
|
+
});
|
|
100
|
+
}, this.config.refreshIntervalMs);
|
|
101
|
+
}
|
|
102
|
+
/** Get a valid blockhash. Fetches fresh one if cache is stale or missing */
|
|
103
|
+
async getBlockhash() {
|
|
104
|
+
if (this.cache && !this.isStale(this.cache)) {
|
|
105
|
+
return this.cache;
|
|
106
|
+
}
|
|
107
|
+
return this.refreshBlockhash();
|
|
108
|
+
}
|
|
109
|
+
/** Force a fresh fetch (used after blockhash expiry during retry) */
|
|
110
|
+
async refreshBlockhash() {
|
|
111
|
+
if (this.fetchPromise) {
|
|
112
|
+
return this.fetchPromise;
|
|
113
|
+
}
|
|
114
|
+
this.fetchPromise = this.fetchBlockhash();
|
|
115
|
+
try {
|
|
116
|
+
const info = await this.fetchPromise;
|
|
117
|
+
this.cache = info;
|
|
118
|
+
return info;
|
|
119
|
+
} finally {
|
|
120
|
+
this.fetchPromise = null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/** Check if the current blockhash is still valid by comparing block heights */
|
|
124
|
+
async isBlockhashValid() {
|
|
125
|
+
if (!this.cache) return false;
|
|
126
|
+
try {
|
|
127
|
+
const currentHeight = await this.connection.getBlockHeight(this.config.commitment);
|
|
128
|
+
return currentHeight < this.cache.lastValidBlockHeight;
|
|
129
|
+
} catch {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/** Get cached info without fetching */
|
|
134
|
+
getCachedBlockhash() {
|
|
135
|
+
if (this.cache && !this.isStale(this.cache)) {
|
|
136
|
+
return this.cache;
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
/** Stop background refresh */
|
|
141
|
+
destroy() {
|
|
142
|
+
if (this.refreshInterval) {
|
|
143
|
+
clearInterval(this.refreshInterval);
|
|
144
|
+
this.refreshInterval = void 0;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
isStale(info) {
|
|
148
|
+
return Date.now() - info.fetchedAt > this.config.ttlMs;
|
|
149
|
+
}
|
|
150
|
+
async fetchBlockhash() {
|
|
151
|
+
try {
|
|
152
|
+
const result = await this.connection.getLatestBlockhash(this.config.commitment);
|
|
153
|
+
const info = {
|
|
154
|
+
blockhash: result.blockhash,
|
|
155
|
+
lastValidBlockHeight: result.lastValidBlockHeight,
|
|
156
|
+
fetchedAt: Date.now()
|
|
157
|
+
};
|
|
158
|
+
this.logger?.debug("Fetched new blockhash", { blockhash: `${info.blockhash.slice(0, 12)}...` });
|
|
159
|
+
return info;
|
|
160
|
+
} catch (err) {
|
|
161
|
+
throw new SolTxError(
|
|
162
|
+
"BLOCKHASH_FETCH_FAILED" /* BLOCKHASH_FETCH_FAILED */,
|
|
163
|
+
"Failed to fetch blockhash. Verify your RPC endpoint is reachable and responding.",
|
|
164
|
+
{ cause: err instanceof Error ? err : new Error(String(err)) }
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
var TxEvent = /* @__PURE__ */ ((TxEvent2) => {
|
|
170
|
+
TxEvent2["SENDING"] = "sending";
|
|
171
|
+
TxEvent2["SIMULATED"] = "simulated";
|
|
172
|
+
TxEvent2["SENT"] = "sent";
|
|
173
|
+
TxEvent2["CONFIRMING"] = "confirming";
|
|
174
|
+
TxEvent2["CONFIRMED"] = "confirmed";
|
|
175
|
+
TxEvent2["RETRYING"] = "retrying";
|
|
176
|
+
TxEvent2["BLOCKHASH_EXPIRED"] = "blockhash_expired";
|
|
177
|
+
TxEvent2["FAILED"] = "failed";
|
|
178
|
+
TxEvent2["BUNDLE_SENT"] = "bundle_sent";
|
|
179
|
+
TxEvent2["BUNDLE_CONFIRMED"] = "bundle_confirmed";
|
|
180
|
+
TxEvent2["BUNDLE_FAILED"] = "bundle_failed";
|
|
181
|
+
return TxEvent2;
|
|
182
|
+
})(TxEvent || {});
|
|
183
|
+
var TypedEventEmitter = class extends events.EventEmitter {
|
|
184
|
+
emit(event, data) {
|
|
185
|
+
return super.emit(event, data);
|
|
186
|
+
}
|
|
187
|
+
on(event, listener) {
|
|
188
|
+
return super.on(event, listener);
|
|
189
|
+
}
|
|
190
|
+
once(event, listener) {
|
|
191
|
+
return super.once(event, listener);
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
// src/confirmation/confirmer.ts
|
|
196
|
+
var TransactionConfirmer = class {
|
|
197
|
+
constructor(logger, events) {
|
|
198
|
+
this.logger = logger;
|
|
199
|
+
this.events = events;
|
|
200
|
+
}
|
|
201
|
+
async confirm(connection, signature, lastValidBlockHeight, config) {
|
|
202
|
+
const resolved = { ...DEFAULT_CONFIRMATION_CONFIG, ...config };
|
|
203
|
+
const startTime = Date.now();
|
|
204
|
+
const latencyMs = () => Date.now() - startTime;
|
|
205
|
+
this.events?.emit("confirming" /* CONFIRMING */, { signature, commitment: resolved.commitment });
|
|
206
|
+
const cleanups = [];
|
|
207
|
+
try {
|
|
208
|
+
const races = [];
|
|
209
|
+
races.push(
|
|
210
|
+
new Promise((resolve) => {
|
|
211
|
+
const timer = setTimeout(() => resolve({ status: "expired", latencyMs: latencyMs() }), resolved.timeoutMs);
|
|
212
|
+
cleanups.push(() => clearTimeout(timer));
|
|
213
|
+
})
|
|
214
|
+
);
|
|
215
|
+
if (resolved.useWebSocket) {
|
|
216
|
+
races.push(this.subscribeWebSocket(connection, signature, resolved, latencyMs, cleanups));
|
|
217
|
+
}
|
|
218
|
+
races.push(this.pollForConfirmation(connection, signature, lastValidBlockHeight, resolved, latencyMs, cleanups));
|
|
219
|
+
return await Promise.race(races);
|
|
220
|
+
} finally {
|
|
221
|
+
for (const cleanup of cleanups) cleanup();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
subscribeWebSocket(connection, signature, config, latencyMs, cleanups) {
|
|
225
|
+
return new Promise((resolve) => {
|
|
226
|
+
try {
|
|
227
|
+
const subId = connection.onSignature(
|
|
228
|
+
signature,
|
|
229
|
+
(result, context) => {
|
|
230
|
+
if (result.err) {
|
|
231
|
+
const errorMsg = typeof result.err === "string" ? result.err : JSON.stringify(result.err);
|
|
232
|
+
resolve({
|
|
233
|
+
status: "failed",
|
|
234
|
+
slot: context.slot,
|
|
235
|
+
error: { code: -1, message: errorMsg },
|
|
236
|
+
latencyMs: latencyMs()
|
|
237
|
+
});
|
|
238
|
+
} else {
|
|
239
|
+
resolve({
|
|
240
|
+
status: config.commitment === "finalized" ? "finalized" : "confirmed",
|
|
241
|
+
slot: context.slot,
|
|
242
|
+
latencyMs: latencyMs()
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
config.commitment
|
|
247
|
+
);
|
|
248
|
+
cleanups.push(() => {
|
|
249
|
+
connection.removeSignatureListener(subId).catch(() => {
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
} catch (err) {
|
|
253
|
+
this.logger?.warn("WebSocket subscription failed, relying on polling", { error: String(err) });
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
pollForConfirmation(connection, signature, lastValidBlockHeight, config, latencyMs, cleanups) {
|
|
258
|
+
return new Promise((resolve) => {
|
|
259
|
+
const timer = setInterval(async () => {
|
|
260
|
+
try {
|
|
261
|
+
const blockHeight = await connection.getBlockHeight(config.commitment);
|
|
262
|
+
if (blockHeight > lastValidBlockHeight) {
|
|
263
|
+
resolve({ status: "expired", latencyMs: latencyMs() });
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const statuses = await connection.getSignatureStatuses([signature]);
|
|
267
|
+
const status = statuses.value[0];
|
|
268
|
+
if (!status) return;
|
|
269
|
+
if (status.err) {
|
|
270
|
+
const errorMsg = typeof status.err === "string" ? status.err : JSON.stringify(status.err);
|
|
271
|
+
resolve({
|
|
272
|
+
status: "failed",
|
|
273
|
+
slot: status.slot,
|
|
274
|
+
error: { code: -1, message: errorMsg },
|
|
275
|
+
latencyMs: latencyMs()
|
|
276
|
+
});
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
if (status.confirmationStatus === "finalized") {
|
|
280
|
+
resolve({ status: "finalized", slot: status.slot, latencyMs: latencyMs() });
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if ((status.confirmationStatus === "confirmed" || status.confirmationStatus === "processed") && config.commitment !== "finalized") {
|
|
284
|
+
resolve({ status: "confirmed", slot: status.slot, latencyMs: latencyMs() });
|
|
285
|
+
}
|
|
286
|
+
} catch (err) {
|
|
287
|
+
this.logger?.warn("Confirmation polling error", { error: String(err) });
|
|
288
|
+
}
|
|
289
|
+
}, config.pollIntervalMs);
|
|
290
|
+
cleanups.push(() => clearInterval(timer));
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
// src/utils.ts
|
|
296
|
+
function isVersionedTransaction(tx) {
|
|
297
|
+
return "version" in tx;
|
|
298
|
+
}
|
|
299
|
+
function isLegacyTransaction(tx) {
|
|
300
|
+
return !isVersionedTransaction(tx);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// src/jito/types.ts
|
|
304
|
+
var BundleStatus = /* @__PURE__ */ ((BundleStatus2) => {
|
|
305
|
+
BundleStatus2["SUBMITTED"] = "submitted";
|
|
306
|
+
BundleStatus2["PENDING"] = "pending";
|
|
307
|
+
BundleStatus2["LANDED"] = "landed";
|
|
308
|
+
BundleStatus2["FAILED"] = "failed";
|
|
309
|
+
BundleStatus2["DROPPED"] = "dropped";
|
|
310
|
+
BundleStatus2["INVALID"] = "invalid";
|
|
311
|
+
return BundleStatus2;
|
|
312
|
+
})(BundleStatus || {});
|
|
313
|
+
|
|
314
|
+
// src/jito/bundle-sender.ts
|
|
315
|
+
function serializeTransaction(tx) {
|
|
316
|
+
if (isVersionedTransaction(tx)) {
|
|
317
|
+
return Buffer.from(tx.serialize()).toString("base64");
|
|
318
|
+
}
|
|
319
|
+
return tx.serialize().toString("base64");
|
|
320
|
+
}
|
|
321
|
+
var JitoBundleSender = class _JitoBundleSender {
|
|
322
|
+
constructor(config, logger, events) {
|
|
323
|
+
this.config = config;
|
|
324
|
+
this.logger = logger;
|
|
325
|
+
this.events = events;
|
|
326
|
+
this.blockEngineUrl = _JitoBundleSender.validateUrl(config.blockEngineUrl ?? JITO_BLOCK_ENGINE_URL);
|
|
327
|
+
this.pollIntervalMs = config.statusPollIntervalMs ?? 2e3;
|
|
328
|
+
this.timeoutMs = config.statusTimeoutMs ?? 6e4;
|
|
329
|
+
}
|
|
330
|
+
blockEngineUrl;
|
|
331
|
+
pollIntervalMs;
|
|
332
|
+
timeoutMs;
|
|
333
|
+
/** Submit a bundle and optionally wait for confirmation */
|
|
334
|
+
async sendBundle(transactions, options) {
|
|
335
|
+
if (transactions.length === 0 || transactions.length > 5) {
|
|
336
|
+
throw new SolTxError(
|
|
337
|
+
"BUNDLE_FAILED" /* BUNDLE_FAILED */,
|
|
338
|
+
`Bundle must contain 1-5 transactions, got ${transactions.length}`
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
const serialized = transactions.map(serializeTransaction);
|
|
342
|
+
const bundleId = await this.rpcCall("sendBundle", [serialized]);
|
|
343
|
+
this.logger?.info("Bundle submitted", { bundleId, txCount: transactions.length });
|
|
344
|
+
this.events?.emit("bundle_sent" /* BUNDLE_SENT */, { bundleId, txCount: transactions.length });
|
|
345
|
+
if (options?.waitForConfirmation) {
|
|
346
|
+
return this.waitForBundleStatus(bundleId);
|
|
347
|
+
}
|
|
348
|
+
return { bundleId, status: "submitted" /* SUBMITTED */ };
|
|
349
|
+
}
|
|
350
|
+
/** Poll getBundleStatuses until landed, failed, or timeout */
|
|
351
|
+
async waitForBundleStatus(bundleId) {
|
|
352
|
+
const startTime = Date.now();
|
|
353
|
+
while (Date.now() - startTime < this.timeoutMs) {
|
|
354
|
+
const statuses = await this.rpcCall("getBundleStatuses", [[bundleId]]);
|
|
355
|
+
const bundleInfo = statuses.value[0];
|
|
356
|
+
if (bundleInfo) {
|
|
357
|
+
if (bundleInfo.confirmation_status === "finalized" || bundleInfo.confirmation_status === "confirmed") {
|
|
358
|
+
const result2 = {
|
|
359
|
+
bundleId,
|
|
360
|
+
status: "landed" /* LANDED */,
|
|
361
|
+
slot: bundleInfo.slot,
|
|
362
|
+
latencyMs: Date.now() - startTime
|
|
363
|
+
};
|
|
364
|
+
this.events?.emit("bundle_confirmed" /* BUNDLE_CONFIRMED */, { bundleId, slot: bundleInfo.slot });
|
|
365
|
+
return result2;
|
|
366
|
+
}
|
|
367
|
+
if (bundleInfo.err?.Err) {
|
|
368
|
+
const result2 = {
|
|
369
|
+
bundleId,
|
|
370
|
+
status: "failed" /* FAILED */,
|
|
371
|
+
latencyMs: Date.now() - startTime
|
|
372
|
+
};
|
|
373
|
+
this.events?.emit("bundle_failed" /* BUNDLE_FAILED */, {
|
|
374
|
+
bundleId,
|
|
375
|
+
error: new Error(JSON.stringify(bundleInfo.err.Err))
|
|
376
|
+
});
|
|
377
|
+
return result2;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
await new Promise((r) => setTimeout(r, this.pollIntervalMs));
|
|
381
|
+
}
|
|
382
|
+
const result = {
|
|
383
|
+
bundleId,
|
|
384
|
+
status: "dropped" /* DROPPED */,
|
|
385
|
+
latencyMs: Date.now() - startTime
|
|
386
|
+
};
|
|
387
|
+
this.events?.emit("bundle_failed" /* BUNDLE_FAILED */, { bundleId, error: new Error("Bundle status polling timed out") });
|
|
388
|
+
return result;
|
|
389
|
+
}
|
|
390
|
+
static validateUrl(url) {
|
|
391
|
+
if (!url.startsWith("https://") && !url.startsWith("http://")) {
|
|
392
|
+
throw new SolTxError(
|
|
393
|
+
"BUNDLE_FAILED" /* BUNDLE_FAILED */,
|
|
394
|
+
`Invalid block engine URL: must start with https:// or http://, got "${url}"`
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
return url.replace(/\/+$/, "");
|
|
398
|
+
}
|
|
399
|
+
/** Raw JSON-RPC call to the block engine */
|
|
400
|
+
async rpcCall(method, params) {
|
|
401
|
+
const url = `${this.blockEngineUrl}/api/v1/bundles`;
|
|
402
|
+
const response = await fetch(url, {
|
|
403
|
+
method: "POST",
|
|
404
|
+
headers: { "Content-Type": "application/json" },
|
|
405
|
+
body: JSON.stringify({
|
|
406
|
+
jsonrpc: "2.0",
|
|
407
|
+
id: 1,
|
|
408
|
+
method,
|
|
409
|
+
params
|
|
410
|
+
})
|
|
411
|
+
});
|
|
412
|
+
if (!response.ok) {
|
|
413
|
+
throw new SolTxError("BUNDLE_FAILED" /* BUNDLE_FAILED */, `Jito block engine returned HTTP ${response.status}`, {
|
|
414
|
+
context: { status: response.status, method }
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
const json = await response.json();
|
|
418
|
+
if (json.error) {
|
|
419
|
+
throw new SolTxError("BUNDLE_FAILED" /* BUNDLE_FAILED */, `Jito RPC error: ${json.error.message}`, {
|
|
420
|
+
context: { code: json.error.code, method }
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
return json.result;
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
var tipIndex = 0;
|
|
427
|
+
function getNextTipAccount() {
|
|
428
|
+
const idx = tipIndex % JITO_TIP_ACCOUNTS.length;
|
|
429
|
+
tipIndex++;
|
|
430
|
+
const account = JITO_TIP_ACCOUNTS[idx];
|
|
431
|
+
if (!account) throw new Error(`Invalid tip account index: ${idx}`);
|
|
432
|
+
return account;
|
|
433
|
+
}
|
|
434
|
+
function resetTipRotation() {
|
|
435
|
+
tipIndex = 0;
|
|
436
|
+
}
|
|
437
|
+
function createTipInstruction(payer, lamports) {
|
|
438
|
+
const tipAccount = getNextTipAccount();
|
|
439
|
+
return web3_js.SystemProgram.transfer({
|
|
440
|
+
fromPubkey: payer,
|
|
441
|
+
toPubkey: tipAccount,
|
|
442
|
+
lamports: Math.max(lamports, JITO_MIN_TIP_LAMPORTS)
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// src/logger.ts
|
|
447
|
+
function createDefaultLogger() {
|
|
448
|
+
return {
|
|
449
|
+
debug(msg, data) {
|
|
450
|
+
console.debug(`[solana-tx-kit] ${msg}`, data ?? "");
|
|
451
|
+
},
|
|
452
|
+
info(msg, data) {
|
|
453
|
+
console.info(`[solana-tx-kit] ${msg}`, data ?? "");
|
|
454
|
+
},
|
|
455
|
+
warn(msg, data) {
|
|
456
|
+
console.warn(`[solana-tx-kit] ${msg}`, data ?? "");
|
|
457
|
+
},
|
|
458
|
+
error(msg, data) {
|
|
459
|
+
console.error(`[solana-tx-kit] ${msg}`, data ?? "");
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
function createComputeBudgetInstructions(config) {
|
|
464
|
+
return [
|
|
465
|
+
web3_js.ComputeBudgetProgram.setComputeUnitLimit({ units: config.computeUnits }),
|
|
466
|
+
web3_js.ComputeBudgetProgram.setComputeUnitPrice({ microLamports: config.microLamports })
|
|
467
|
+
];
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// src/priority-fee/fee-estimator.ts
|
|
471
|
+
function percentile(sorted, p) {
|
|
472
|
+
if (sorted.length === 0) return 0;
|
|
473
|
+
const index = Math.ceil(p / 100 * sorted.length) - 1;
|
|
474
|
+
return sorted[Math.max(0, index)] ?? 0;
|
|
475
|
+
}
|
|
476
|
+
async function estimatePriorityFee(connection, config) {
|
|
477
|
+
const resolved = { ...DEFAULT_PRIORITY_FEE_CONFIG, ...config };
|
|
478
|
+
try {
|
|
479
|
+
const accounts = resolved.writableAccounts?.map((a) => a.toBase58());
|
|
480
|
+
const fees = await connection.getRecentPrioritizationFees(
|
|
481
|
+
accounts ? { lockedWritableAccounts: accounts.map((a) => ({ toBase58: () => a })) } : void 0
|
|
482
|
+
);
|
|
483
|
+
const feeValues = fees.map((f) => f.prioritizationFee).filter((f) => f > 0);
|
|
484
|
+
feeValues.sort((a, b) => a - b);
|
|
485
|
+
const p50 = percentile(feeValues, 50);
|
|
486
|
+
const p75 = percentile(feeValues, 75);
|
|
487
|
+
const p90 = percentile(feeValues, 90);
|
|
488
|
+
let target;
|
|
489
|
+
switch (resolved.targetPercentile) {
|
|
490
|
+
case 50:
|
|
491
|
+
target = p50;
|
|
492
|
+
break;
|
|
493
|
+
case 90:
|
|
494
|
+
target = p90;
|
|
495
|
+
break;
|
|
496
|
+
default:
|
|
497
|
+
target = p75;
|
|
498
|
+
}
|
|
499
|
+
const clamped = Math.min(Math.max(target, resolved.minMicroLamports), resolved.maxMicroLamports);
|
|
500
|
+
return {
|
|
501
|
+
microLamports: clamped,
|
|
502
|
+
percentiles: { p50, p75, p90 },
|
|
503
|
+
sampleCount: feeValues.length
|
|
504
|
+
};
|
|
505
|
+
} catch (err) {
|
|
506
|
+
throw new SolTxError(
|
|
507
|
+
"FEE_ESTIMATION_FAILED" /* FEE_ESTIMATION_FAILED */,
|
|
508
|
+
"Failed to estimate priority fees. Use disablePriorityFees() or pass { priorityFee: { microLamports: N } } as a static override.",
|
|
509
|
+
{ cause: err instanceof Error ? err : new Error(String(err)) }
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// src/retry/error-classifier.ts
|
|
515
|
+
var RETRYABLE_MESSAGES = [
|
|
516
|
+
"blockhash not found",
|
|
517
|
+
"block height exceeded",
|
|
518
|
+
"TransactionExpiredBlockheightExceeded",
|
|
519
|
+
"Node is behind",
|
|
520
|
+
"node is unhealthy",
|
|
521
|
+
"Service unavailable",
|
|
522
|
+
"Too many requests"
|
|
523
|
+
];
|
|
524
|
+
var NEEDS_RESIGN_MESSAGES = ["blockhash not found", "block height exceeded", "TransactionExpiredBlockheightExceeded"];
|
|
525
|
+
var NETWORK_ERROR_CODES = ["ECONNRESET", "ETIMEDOUT", "ENOTFOUND", "ECONNREFUSED", "EAI_AGAIN", "EPIPE"];
|
|
526
|
+
var NON_RETRYABLE_MESSAGES = [
|
|
527
|
+
"insufficient funds",
|
|
528
|
+
"Insufficient funds",
|
|
529
|
+
"invalid account data",
|
|
530
|
+
"Account not found",
|
|
531
|
+
"Signature verification failed",
|
|
532
|
+
"Transaction simulation failed: Error processing Instruction",
|
|
533
|
+
"Program failed to complete",
|
|
534
|
+
"already been processed"
|
|
535
|
+
];
|
|
536
|
+
function classifyError(error) {
|
|
537
|
+
const msg = error.message ?? "";
|
|
538
|
+
const code = error.code;
|
|
539
|
+
for (const pattern of NON_RETRYABLE_MESSAGES) {
|
|
540
|
+
if (msg.includes(pattern)) {
|
|
541
|
+
return { retryable: false, needsResign: false, errorType: pattern };
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
if (code && NETWORK_ERROR_CODES.includes(code)) {
|
|
545
|
+
return { retryable: true, needsResign: false, errorType: code };
|
|
546
|
+
}
|
|
547
|
+
if (msg.includes("429") || msg.includes("Too many requests")) {
|
|
548
|
+
return { retryable: true, needsResign: false, errorType: "RATE_LIMITED" };
|
|
549
|
+
}
|
|
550
|
+
if (msg.includes("503") || msg.includes("Service unavailable")) {
|
|
551
|
+
return { retryable: true, needsResign: false, errorType: "SERVICE_UNAVAILABLE" };
|
|
552
|
+
}
|
|
553
|
+
for (const pattern of RETRYABLE_MESSAGES) {
|
|
554
|
+
if (msg.includes(pattern)) {
|
|
555
|
+
const needsResign = NEEDS_RESIGN_MESSAGES.some((p) => msg.includes(p));
|
|
556
|
+
return { retryable: true, needsResign, errorType: pattern };
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return { retryable: false, needsResign: false, errorType: "UNKNOWN" };
|
|
560
|
+
}
|
|
561
|
+
function isBlockhashExpired(error) {
|
|
562
|
+
const msg = error.message ?? "";
|
|
563
|
+
return NEEDS_RESIGN_MESSAGES.some((pattern) => msg.includes(pattern));
|
|
564
|
+
}
|
|
565
|
+
function isRateLimited(error) {
|
|
566
|
+
const msg = error.message ?? "";
|
|
567
|
+
return msg.includes("429") || msg.includes("Too many requests");
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// src/retry/retry.ts
|
|
571
|
+
function computeDelay(attempt, config) {
|
|
572
|
+
const exponential = config.baseDelayMs * config.backoffMultiplier ** attempt;
|
|
573
|
+
const capped = Math.min(exponential, config.maxDelayMs);
|
|
574
|
+
return Math.random() * capped;
|
|
575
|
+
}
|
|
576
|
+
function sleep(ms) {
|
|
577
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
578
|
+
}
|
|
579
|
+
async function withRetry(fn, config) {
|
|
580
|
+
const resolved = { ...DEFAULT_RETRY_CONFIG, ...config };
|
|
581
|
+
const startTime = Date.now();
|
|
582
|
+
let lastError;
|
|
583
|
+
for (let attempt = 0; attempt <= resolved.maxRetries; attempt++) {
|
|
584
|
+
const context = {
|
|
585
|
+
attempt,
|
|
586
|
+
totalAttempts: resolved.maxRetries + 1,
|
|
587
|
+
elapsed: Date.now() - startTime,
|
|
588
|
+
lastError
|
|
589
|
+
};
|
|
590
|
+
try {
|
|
591
|
+
return await fn(context);
|
|
592
|
+
} catch (err) {
|
|
593
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
594
|
+
lastError = error;
|
|
595
|
+
if (attempt >= resolved.maxRetries) {
|
|
596
|
+
break;
|
|
597
|
+
}
|
|
598
|
+
if (resolved.retryPredicate) {
|
|
599
|
+
if (!resolved.retryPredicate(error, attempt)) {
|
|
600
|
+
throw new SolTxError("NON_RETRYABLE" /* NON_RETRYABLE */, `Non-retryable (custom predicate): ${error.message}`, {
|
|
601
|
+
cause: error,
|
|
602
|
+
context: { attempt }
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
} else {
|
|
606
|
+
const classification = classifyError(error);
|
|
607
|
+
if (!classification.retryable) {
|
|
608
|
+
throw new SolTxError("NON_RETRYABLE" /* NON_RETRYABLE */, `Non-retryable: ${error.message}`, {
|
|
609
|
+
cause: error,
|
|
610
|
+
context: { attempt, errorType: classification.errorType }
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
const delayMs = computeDelay(attempt, resolved);
|
|
615
|
+
if (resolved.onRetry) {
|
|
616
|
+
await resolved.onRetry(error, attempt, delayMs);
|
|
617
|
+
}
|
|
618
|
+
await sleep(delayMs);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
throw new SolTxError("RETRIES_EXHAUSTED" /* RETRIES_EXHAUSTED */, `All ${resolved.maxRetries + 1} attempts failed`, {
|
|
622
|
+
cause: lastError ?? new Error("Unknown error"),
|
|
623
|
+
context: { maxRetries: resolved.maxRetries, elapsed: Date.now() - startTime }
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// src/rpc/types.ts
|
|
628
|
+
var CircuitState = /* @__PURE__ */ ((CircuitState2) => {
|
|
629
|
+
CircuitState2["CLOSED"] = "closed";
|
|
630
|
+
CircuitState2["OPEN"] = "open";
|
|
631
|
+
CircuitState2["HALF_OPEN"] = "half_open";
|
|
632
|
+
return CircuitState2;
|
|
633
|
+
})(CircuitState || {});
|
|
634
|
+
|
|
635
|
+
// src/rpc/circuit-breaker.ts
|
|
636
|
+
var DEFAULT_CIRCUIT_BREAKER_CONFIG = {
|
|
637
|
+
failureThreshold: 5,
|
|
638
|
+
resetTimeoutMs: 3e4,
|
|
639
|
+
windowMs: 6e4
|
|
640
|
+
};
|
|
641
|
+
var CircuitBreaker = class {
|
|
642
|
+
state = "closed" /* CLOSED */;
|
|
643
|
+
failures = [];
|
|
644
|
+
lastOpenedAt = 0;
|
|
645
|
+
config;
|
|
646
|
+
constructor(config) {
|
|
647
|
+
this.config = { ...DEFAULT_CIRCUIT_BREAKER_CONFIG, ...config };
|
|
648
|
+
}
|
|
649
|
+
get currentState() {
|
|
650
|
+
if (this.state === "open" /* OPEN */) {
|
|
651
|
+
if (Date.now() - this.lastOpenedAt >= this.config.resetTimeoutMs) {
|
|
652
|
+
this.state = "half_open" /* HALF_OPEN */;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
return this.state;
|
|
656
|
+
}
|
|
657
|
+
recordSuccess() {
|
|
658
|
+
if (this.state === "half_open" /* HALF_OPEN */) {
|
|
659
|
+
this.state = "closed" /* CLOSED */;
|
|
660
|
+
this.failures = [];
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
recordFailure() {
|
|
664
|
+
const now = Date.now();
|
|
665
|
+
if (this.state === "half_open" /* HALF_OPEN */) {
|
|
666
|
+
this.state = "open" /* OPEN */;
|
|
667
|
+
this.lastOpenedAt = now;
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
this.failures = this.failures.filter((t) => now - t < this.config.windowMs);
|
|
671
|
+
this.failures.push(now);
|
|
672
|
+
if (this.failures.length >= this.config.failureThreshold) {
|
|
673
|
+
this.state = "open" /* OPEN */;
|
|
674
|
+
this.lastOpenedAt = now;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
canExecute() {
|
|
678
|
+
const current = this.currentState;
|
|
679
|
+
return current === "closed" /* CLOSED */ || current === "half_open" /* HALF_OPEN */;
|
|
680
|
+
}
|
|
681
|
+
reset() {
|
|
682
|
+
this.state = "closed" /* CLOSED */;
|
|
683
|
+
this.failures = [];
|
|
684
|
+
this.lastOpenedAt = 0;
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
// src/rpc/health-tracker.ts
|
|
689
|
+
var EMA_ALPHA = 0.3;
|
|
690
|
+
var HealthTracker = class {
|
|
691
|
+
constructor(endpoint, connection, logger, circuitBreakerConfig) {
|
|
692
|
+
this.endpoint = endpoint;
|
|
693
|
+
this.connection = connection;
|
|
694
|
+
this.logger = logger;
|
|
695
|
+
this.breaker = new CircuitBreaker(circuitBreakerConfig);
|
|
696
|
+
}
|
|
697
|
+
breaker;
|
|
698
|
+
metrics = {
|
|
699
|
+
latencyEma: 0,
|
|
700
|
+
errorCount: 0,
|
|
701
|
+
successCount: 0,
|
|
702
|
+
errorRate: 0,
|
|
703
|
+
lastSlot: 0,
|
|
704
|
+
slotLag: 0,
|
|
705
|
+
lastSuccessAt: 0,
|
|
706
|
+
circuitState: "closed" /* CLOSED */
|
|
707
|
+
};
|
|
708
|
+
async healthCheck() {
|
|
709
|
+
const start = Date.now();
|
|
710
|
+
try {
|
|
711
|
+
const slot = await this.connection.getSlot();
|
|
712
|
+
const latency = Date.now() - start;
|
|
713
|
+
this.recordSuccess(latency, slot);
|
|
714
|
+
} catch (err) {
|
|
715
|
+
this.recordFailure(err instanceof Error ? err : new Error(String(err)));
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
recordSuccess(latencyMs, slot) {
|
|
719
|
+
this.metrics.successCount++;
|
|
720
|
+
this.metrics.lastSuccessAt = Date.now();
|
|
721
|
+
if (this.metrics.latencyEma === 0) {
|
|
722
|
+
this.metrics.latencyEma = latencyMs;
|
|
723
|
+
} else {
|
|
724
|
+
this.metrics.latencyEma = EMA_ALPHA * latencyMs + (1 - EMA_ALPHA) * this.metrics.latencyEma;
|
|
725
|
+
}
|
|
726
|
+
if (slot !== void 0) {
|
|
727
|
+
this.metrics.lastSlot = slot;
|
|
728
|
+
}
|
|
729
|
+
this.breaker.recordSuccess();
|
|
730
|
+
this.updateErrorRate();
|
|
731
|
+
this.metrics.circuitState = this.breaker.currentState;
|
|
732
|
+
}
|
|
733
|
+
recordFailure(error) {
|
|
734
|
+
this.metrics.errorCount++;
|
|
735
|
+
this.breaker.recordFailure();
|
|
736
|
+
this.updateErrorRate();
|
|
737
|
+
this.metrics.circuitState = this.breaker.currentState;
|
|
738
|
+
this.logger?.warn(`RPC endpoint ${this.endpoint.label ?? this.endpoint.url} error`, {
|
|
739
|
+
error: error.message,
|
|
740
|
+
circuitState: this.metrics.circuitState
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
isAvailable() {
|
|
744
|
+
return this.breaker.canExecute();
|
|
745
|
+
}
|
|
746
|
+
getMetrics() {
|
|
747
|
+
this.metrics.circuitState = this.breaker.currentState;
|
|
748
|
+
return { ...this.metrics };
|
|
749
|
+
}
|
|
750
|
+
updateSlotLag(highestSlot) {
|
|
751
|
+
this.metrics.slotLag = highestSlot - this.metrics.lastSlot;
|
|
752
|
+
}
|
|
753
|
+
getConnection() {
|
|
754
|
+
return this.connection;
|
|
755
|
+
}
|
|
756
|
+
updateErrorRate() {
|
|
757
|
+
const total = this.metrics.successCount + this.metrics.errorCount;
|
|
758
|
+
this.metrics.errorRate = total > 0 ? this.metrics.errorCount / total : 0;
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
|
|
762
|
+
// src/rpc/connection-pool.ts
|
|
763
|
+
var ConnectionPool = class {
|
|
764
|
+
constructor(config, logger) {
|
|
765
|
+
this.logger = logger;
|
|
766
|
+
this.strategy = config.strategy ?? "weighted-round-robin";
|
|
767
|
+
for (const endpoint of config.endpoints) {
|
|
768
|
+
const connection = new web3_js.Connection(endpoint.url, {
|
|
769
|
+
commitment: config.healthCheckCommitment ?? "confirmed"
|
|
770
|
+
});
|
|
771
|
+
const tracker = new HealthTracker(endpoint, connection, logger, config.circuitBreaker);
|
|
772
|
+
this.trackers.push(tracker);
|
|
773
|
+
}
|
|
774
|
+
const interval = config.healthCheckIntervalMs ?? 1e4;
|
|
775
|
+
this.healthCheckInterval = setInterval(() => {
|
|
776
|
+
this.runHealthChecks();
|
|
777
|
+
}, interval);
|
|
778
|
+
}
|
|
779
|
+
trackers = [];
|
|
780
|
+
healthCheckInterval;
|
|
781
|
+
roundRobinIndex = 0;
|
|
782
|
+
strategy;
|
|
783
|
+
/** Get the best available connection based on strategy */
|
|
784
|
+
getConnection() {
|
|
785
|
+
const available = this.trackers.filter((t) => t.isAvailable());
|
|
786
|
+
if (available.length === 0) {
|
|
787
|
+
this.logger?.warn("All RPC endpoints unhealthy, using first endpoint as fallback");
|
|
788
|
+
const fallback = this.trackers[0];
|
|
789
|
+
if (!fallback) {
|
|
790
|
+
throw new SolTxError(
|
|
791
|
+
"ALL_ENDPOINTS_UNHEALTHY" /* ALL_ENDPOINTS_UNHEALTHY */,
|
|
792
|
+
"No RPC endpoints configured. Call .rpc() or .rpcPool() in the builder."
|
|
793
|
+
);
|
|
794
|
+
}
|
|
795
|
+
return fallback.getConnection();
|
|
796
|
+
}
|
|
797
|
+
if (this.strategy === "latency-based") {
|
|
798
|
+
return this.selectByLatency(available);
|
|
799
|
+
}
|
|
800
|
+
return this.selectByWeight(available);
|
|
801
|
+
}
|
|
802
|
+
/** Get a connection, falling back through endpoints if the first fails */
|
|
803
|
+
async withFallback(fn) {
|
|
804
|
+
const available = this.trackers.filter((t) => t.isAvailable());
|
|
805
|
+
const ordered = available.length > 0 ? available : this.trackers;
|
|
806
|
+
let lastError;
|
|
807
|
+
for (const tracker of ordered) {
|
|
808
|
+
const start = Date.now();
|
|
809
|
+
try {
|
|
810
|
+
const result = await fn(tracker.getConnection());
|
|
811
|
+
tracker.recordSuccess(Date.now() - start);
|
|
812
|
+
return result;
|
|
813
|
+
} catch (err) {
|
|
814
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
815
|
+
tracker.recordFailure(error);
|
|
816
|
+
lastError = error;
|
|
817
|
+
this.logger?.warn(`Failover: ${tracker.endpoint.label ?? tracker.endpoint.url} failed`, {
|
|
818
|
+
error: error.message
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
throw new SolTxError(
|
|
823
|
+
"ALL_ENDPOINTS_UNHEALTHY" /* ALL_ENDPOINTS_UNHEALTHY */,
|
|
824
|
+
"All RPC endpoints failed. Check endpoint health via getHealthReport() or add backup endpoints with .rpcPool().",
|
|
825
|
+
{ cause: lastError ?? new Error("No endpoints available") }
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
/** Get health metrics for all endpoints */
|
|
829
|
+
getHealthReport() {
|
|
830
|
+
const report = /* @__PURE__ */ new Map();
|
|
831
|
+
for (const tracker of this.trackers) {
|
|
832
|
+
const key = tracker.endpoint.label ?? tracker.endpoint.url;
|
|
833
|
+
report.set(key, tracker.getMetrics());
|
|
834
|
+
}
|
|
835
|
+
return report;
|
|
836
|
+
}
|
|
837
|
+
/** Stop background health checks */
|
|
838
|
+
destroy() {
|
|
839
|
+
if (this.healthCheckInterval) {
|
|
840
|
+
clearInterval(this.healthCheckInterval);
|
|
841
|
+
this.healthCheckInterval = void 0;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
selectByLatency(available) {
|
|
845
|
+
const sorted = [...available].sort((a, b) => {
|
|
846
|
+
const aMetrics = a.getMetrics();
|
|
847
|
+
const bMetrics = b.getMetrics();
|
|
848
|
+
return aMetrics.latencyEma - bMetrics.latencyEma;
|
|
849
|
+
});
|
|
850
|
+
const best = sorted[0];
|
|
851
|
+
if (!best) throw new SolTxError("ALL_ENDPOINTS_UNHEALTHY" /* ALL_ENDPOINTS_UNHEALTHY */, "No endpoints available");
|
|
852
|
+
return best.getConnection();
|
|
853
|
+
}
|
|
854
|
+
selectByWeight(available) {
|
|
855
|
+
let totalWeight = 0;
|
|
856
|
+
for (const tracker of available) {
|
|
857
|
+
totalWeight += tracker.endpoint.weight ?? 1;
|
|
858
|
+
}
|
|
859
|
+
const position = this.roundRobinIndex % totalWeight;
|
|
860
|
+
this.roundRobinIndex++;
|
|
861
|
+
let cumulative = 0;
|
|
862
|
+
for (const tracker of available) {
|
|
863
|
+
cumulative += tracker.endpoint.weight ?? 1;
|
|
864
|
+
if (position < cumulative) {
|
|
865
|
+
return tracker.getConnection();
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
throw new SolTxError("ALL_ENDPOINTS_UNHEALTHY" /* ALL_ENDPOINTS_UNHEALTHY */, "No weighted endpoint available");
|
|
869
|
+
}
|
|
870
|
+
runHealthChecks() {
|
|
871
|
+
const promises = this.trackers.map((t) => t.healthCheck());
|
|
872
|
+
Promise.all(promises).then(() => {
|
|
873
|
+
let highestSlot = 0;
|
|
874
|
+
for (const tracker of this.trackers) {
|
|
875
|
+
const metrics = tracker.getMetrics();
|
|
876
|
+
if (metrics.lastSlot > highestSlot) {
|
|
877
|
+
highestSlot = metrics.lastSlot;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
for (const tracker of this.trackers) {
|
|
881
|
+
tracker.updateSlotLag(highestSlot);
|
|
882
|
+
}
|
|
883
|
+
}).catch((err) => {
|
|
884
|
+
this.logger?.warn("Health check round failed", { error: String(err) });
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
};
|
|
888
|
+
|
|
889
|
+
// src/simulation/simulator.ts
|
|
890
|
+
async function simulateTransaction(connection, transaction, config, logger) {
|
|
891
|
+
const commitment = config?.commitment ?? "confirmed";
|
|
892
|
+
const replaceRecentBlockhash = config?.replaceRecentBlockhash ?? true;
|
|
893
|
+
const sigVerify = config?.sigVerify ?? false;
|
|
894
|
+
try {
|
|
895
|
+
let err;
|
|
896
|
+
let logs = null;
|
|
897
|
+
let unitsConsumed;
|
|
898
|
+
if (isVersionedTransaction(transaction)) {
|
|
899
|
+
const result = await connection.simulateTransaction(transaction, {
|
|
900
|
+
commitment,
|
|
901
|
+
replaceRecentBlockhash,
|
|
902
|
+
sigVerify
|
|
903
|
+
});
|
|
904
|
+
err = result.value.err;
|
|
905
|
+
logs = result.value.logs;
|
|
906
|
+
unitsConsumed = result.value.unitsConsumed;
|
|
907
|
+
} else {
|
|
908
|
+
const result = await connection.simulateTransaction(transaction);
|
|
909
|
+
err = result.value.err;
|
|
910
|
+
logs = result.value.logs;
|
|
911
|
+
unitsConsumed = result.value.unitsConsumed;
|
|
912
|
+
}
|
|
913
|
+
if (err) {
|
|
914
|
+
const errorMessage = typeof err === "string" ? err : JSON.stringify(err);
|
|
915
|
+
logger?.warn("Simulation failed", { error: errorMessage });
|
|
916
|
+
let instructionError;
|
|
917
|
+
if (typeof err === "object" && err !== null && "InstructionError" in err) {
|
|
918
|
+
const ie = err.InstructionError;
|
|
919
|
+
instructionError = {
|
|
920
|
+
index: ie[0],
|
|
921
|
+
message: typeof ie[1] === "string" ? ie[1] : JSON.stringify(ie[1])
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
return {
|
|
925
|
+
success: false,
|
|
926
|
+
unitsConsumed: unitsConsumed ?? 0,
|
|
927
|
+
logs: logs ?? [],
|
|
928
|
+
error: {
|
|
929
|
+
code: typeof err === "object" && err !== null && "code" in err ? err.code : -1,
|
|
930
|
+
message: errorMessage,
|
|
931
|
+
...instructionError ? { instructionError } : {}
|
|
932
|
+
}
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
logger?.debug("Simulation succeeded", { unitsConsumed: unitsConsumed ?? 0 });
|
|
936
|
+
return {
|
|
937
|
+
success: true,
|
|
938
|
+
unitsConsumed: unitsConsumed ?? 0,
|
|
939
|
+
logs: logs ?? []
|
|
940
|
+
};
|
|
941
|
+
} catch (err) {
|
|
942
|
+
const cause = err instanceof Error ? err : new Error(String(err));
|
|
943
|
+
throw new SolTxError("SIMULATION_FAILED" /* SIMULATION_FAILED */, `Transaction simulation failed: ${cause.message}`, {
|
|
944
|
+
cause
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// src/sender/builder.ts
|
|
950
|
+
var TransactionSenderBuilder = class {
|
|
951
|
+
config = {};
|
|
952
|
+
/** Set a single RPC endpoint */
|
|
953
|
+
rpc(url) {
|
|
954
|
+
this.config.rpc = { url };
|
|
955
|
+
return this;
|
|
956
|
+
}
|
|
957
|
+
/** Set multiple RPC endpoints with failover */
|
|
958
|
+
rpcPool(endpoints, options) {
|
|
959
|
+
const poolConfig = { endpoints };
|
|
960
|
+
if (options?.strategy) poolConfig.strategy = options.strategy;
|
|
961
|
+
if (options?.healthCheckIntervalMs) poolConfig.healthCheckIntervalMs = options.healthCheckIntervalMs;
|
|
962
|
+
this.config.rpc = poolConfig;
|
|
963
|
+
return this;
|
|
964
|
+
}
|
|
965
|
+
/** Set the transaction signer */
|
|
966
|
+
signer(keypair) {
|
|
967
|
+
this.config.signer = keypair;
|
|
968
|
+
return this;
|
|
969
|
+
}
|
|
970
|
+
/** Configure priority fee estimation */
|
|
971
|
+
withPriorityFees(config) {
|
|
972
|
+
this.config.priorityFee = config ?? {};
|
|
973
|
+
return this;
|
|
974
|
+
}
|
|
975
|
+
/** Disable automatic priority fee estimation */
|
|
976
|
+
disablePriorityFees() {
|
|
977
|
+
this.config.priorityFee = false;
|
|
978
|
+
return this;
|
|
979
|
+
}
|
|
980
|
+
/** Configure Jito bundle submission */
|
|
981
|
+
withJito(config) {
|
|
982
|
+
this.config.jito = config;
|
|
983
|
+
return this;
|
|
984
|
+
}
|
|
985
|
+
/** Configure retry behavior */
|
|
986
|
+
withRetry(config) {
|
|
987
|
+
this.config.retry = config;
|
|
988
|
+
return this;
|
|
989
|
+
}
|
|
990
|
+
/** Configure transaction simulation */
|
|
991
|
+
withSimulation(config) {
|
|
992
|
+
this.config.simulation = config ?? {};
|
|
993
|
+
return this;
|
|
994
|
+
}
|
|
995
|
+
/** Disable pre-flight simulation */
|
|
996
|
+
disableSimulation() {
|
|
997
|
+
this.config.simulation = false;
|
|
998
|
+
return this;
|
|
999
|
+
}
|
|
1000
|
+
/** Configure confirmation tracking */
|
|
1001
|
+
withConfirmation(config) {
|
|
1002
|
+
this.config.confirmation = config;
|
|
1003
|
+
return this;
|
|
1004
|
+
}
|
|
1005
|
+
/** Configure blockhash management */
|
|
1006
|
+
withBlockhash(config) {
|
|
1007
|
+
this.config.blockhash = config;
|
|
1008
|
+
return this;
|
|
1009
|
+
}
|
|
1010
|
+
/** Set the logger */
|
|
1011
|
+
withLogger(logger) {
|
|
1012
|
+
this.config.logger = logger;
|
|
1013
|
+
return this;
|
|
1014
|
+
}
|
|
1015
|
+
/** Set the default commitment */
|
|
1016
|
+
commitment(level) {
|
|
1017
|
+
this.config.commitment = level;
|
|
1018
|
+
return this;
|
|
1019
|
+
}
|
|
1020
|
+
/** Build and return the TransactionSender */
|
|
1021
|
+
build() {
|
|
1022
|
+
if (!this.config.rpc) {
|
|
1023
|
+
throw new Error("TransactionSenderBuilder: at least one RPC endpoint is required. Call .rpc() or .rpcPool()");
|
|
1024
|
+
}
|
|
1025
|
+
if (!this.config.signer) {
|
|
1026
|
+
throw new Error("TransactionSenderBuilder: signer is required. Call .signer()");
|
|
1027
|
+
}
|
|
1028
|
+
return new TransactionSender(this.config);
|
|
1029
|
+
}
|
|
1030
|
+
};
|
|
1031
|
+
|
|
1032
|
+
// src/sender/types.ts
|
|
1033
|
+
function isConnectionPoolConfig(config) {
|
|
1034
|
+
return "endpoints" in config;
|
|
1035
|
+
}
|
|
1036
|
+
function isStaticFee(config) {
|
|
1037
|
+
return "microLamports" in config && !("targetPercentile" in config);
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// src/sender/transaction-sender.ts
|
|
1041
|
+
var TransactionSender = class {
|
|
1042
|
+
events;
|
|
1043
|
+
pool;
|
|
1044
|
+
blockhashManager;
|
|
1045
|
+
confirmer;
|
|
1046
|
+
jitoBundleSender;
|
|
1047
|
+
logger;
|
|
1048
|
+
config;
|
|
1049
|
+
/** @param config - Full sender configuration. Prefer using {@link TransactionSender.builder} for construction. */
|
|
1050
|
+
constructor(config) {
|
|
1051
|
+
this.config = config;
|
|
1052
|
+
this.logger = config.logger ?? createDefaultLogger();
|
|
1053
|
+
this.events = new TypedEventEmitter();
|
|
1054
|
+
if (isConnectionPoolConfig(config.rpc)) {
|
|
1055
|
+
this.pool = new ConnectionPool(config.rpc, this.logger);
|
|
1056
|
+
} else {
|
|
1057
|
+
this.pool = new ConnectionPool({ endpoints: [{ url: config.rpc.url }] }, this.logger);
|
|
1058
|
+
}
|
|
1059
|
+
const primaryConnection = this.pool.getConnection();
|
|
1060
|
+
this.blockhashManager = new BlockhashManager(primaryConnection, config.blockhash, this.logger);
|
|
1061
|
+
this.blockhashManager.start();
|
|
1062
|
+
this.confirmer = new TransactionConfirmer(this.logger, this.events);
|
|
1063
|
+
if (config.jito) {
|
|
1064
|
+
this.jitoBundleSender = new JitoBundleSender(config.jito, this.logger, this.events);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
/** Create a fluent builder for configuring and constructing a TransactionSender */
|
|
1068
|
+
static builder() {
|
|
1069
|
+
return new TransactionSenderBuilder();
|
|
1070
|
+
}
|
|
1071
|
+
/**
|
|
1072
|
+
* Send a single transaction with the full pipeline:
|
|
1073
|
+
* 1. Estimate priority fees (if enabled)
|
|
1074
|
+
* 2. Fetch fresh blockhash
|
|
1075
|
+
* 3. Add compute budget instructions
|
|
1076
|
+
* 4. Sign transaction
|
|
1077
|
+
* 5. Simulate (if enabled)
|
|
1078
|
+
* 6. Send via RPC (with retry + failover)
|
|
1079
|
+
* 7. Confirm (WebSocket + polling)
|
|
1080
|
+
*
|
|
1081
|
+
* @param transaction - A legacy or versioned Solana transaction. Not mutated for legacy transactions.
|
|
1082
|
+
* @param options - Per-send overrides for priority fee, simulation, confirmation, and retry.
|
|
1083
|
+
* @returns The confirmed transaction result including signature, slot, and timing info.
|
|
1084
|
+
* @throws {SolTxError} On simulation failure, non-retryable errors, or exhausted retries.
|
|
1085
|
+
*/
|
|
1086
|
+
async send(transaction, options) {
|
|
1087
|
+
const startTime = Date.now();
|
|
1088
|
+
const commitment = options?.commitment ?? this.config.commitment ?? "confirmed";
|
|
1089
|
+
const { tx, feeAmount } = await this.prepareTransaction(transaction, options);
|
|
1090
|
+
return withRetry(
|
|
1091
|
+
async (ctx) => {
|
|
1092
|
+
const blockhashInfo = await this.blockhashManager.getBlockhash();
|
|
1093
|
+
this.signTransaction(tx, blockhashInfo);
|
|
1094
|
+
const unitsConsumed = await this.runSimulation(tx, options);
|
|
1095
|
+
this.events.emit("sending" /* SENDING */, { transaction: tx, attempt: ctx.attempt });
|
|
1096
|
+
const signature = await this.sendRawTransaction(tx);
|
|
1097
|
+
this.events.emit("sent" /* SENT */, { signature, attempt: ctx.attempt });
|
|
1098
|
+
if (options?.skipConfirmation) {
|
|
1099
|
+
return this.buildResult(signature, 0, commitment, ctx.attempt, startTime, unitsConsumed, feeAmount);
|
|
1100
|
+
}
|
|
1101
|
+
const slot = await this.awaitConfirmation(signature, blockhashInfo.lastValidBlockHeight, commitment);
|
|
1102
|
+
return this.buildResult(signature, slot, commitment, ctx.attempt, startTime, unitsConsumed, feeAmount);
|
|
1103
|
+
},
|
|
1104
|
+
{
|
|
1105
|
+
...this.config.retry,
|
|
1106
|
+
...options?.retry,
|
|
1107
|
+
onRetry: async (error, attempt, delayMs) => {
|
|
1108
|
+
this.events.emit("retrying" /* RETRYING */, {
|
|
1109
|
+
attempt,
|
|
1110
|
+
maxRetries: this.config.retry?.maxRetries ?? 3,
|
|
1111
|
+
error,
|
|
1112
|
+
delayMs
|
|
1113
|
+
});
|
|
1114
|
+
if (isBlockhashExpired(error)) {
|
|
1115
|
+
const oldBlockhash = this.blockhashManager.getCachedBlockhash()?.blockhash ?? "";
|
|
1116
|
+
await this.blockhashManager.refreshBlockhash();
|
|
1117
|
+
const newBlockhash = this.blockhashManager.getCachedBlockhash()?.blockhash ?? "";
|
|
1118
|
+
this.events.emit("blockhash_expired" /* BLOCKHASH_EXPIRED */, { oldBlockhash, newBlockhash });
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1124
|
+
/**
|
|
1125
|
+
* Send a bundle of 1-5 transactions via Jito.
|
|
1126
|
+
* Automatically appends tip instruction to the last transaction.
|
|
1127
|
+
*/
|
|
1128
|
+
async sendJitoBundle(transactions, options) {
|
|
1129
|
+
if (!this.jitoBundleSender) {
|
|
1130
|
+
throw new SolTxError("BUNDLE_FAILED" /* BUNDLE_FAILED */, "Jito is not configured. Use .withJito() in the builder.");
|
|
1131
|
+
}
|
|
1132
|
+
const jitoConfig = this.config.jito;
|
|
1133
|
+
const tipLamports = options?.tipLamports ?? jitoConfig.tipLamports ?? 1e4;
|
|
1134
|
+
const lastTx = transactions[transactions.length - 1];
|
|
1135
|
+
if (lastTx && isLegacyTransaction(lastTx)) {
|
|
1136
|
+
const tipIx = createTipInstruction(jitoConfig.tipPayer.publicKey, tipLamports);
|
|
1137
|
+
lastTx.add(tipIx);
|
|
1138
|
+
}
|
|
1139
|
+
for (const tx of transactions) {
|
|
1140
|
+
const blockhashInfo = await this.blockhashManager.getBlockhash();
|
|
1141
|
+
if (isLegacyTransaction(tx)) {
|
|
1142
|
+
tx.recentBlockhash = blockhashInfo.blockhash;
|
|
1143
|
+
tx.feePayer = this.config.signer.publicKey;
|
|
1144
|
+
tx.sign(this.config.signer);
|
|
1145
|
+
} else {
|
|
1146
|
+
tx.message.recentBlockhash = blockhashInfo.blockhash;
|
|
1147
|
+
tx.sign([this.config.signer]);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
return this.jitoBundleSender.sendBundle(transactions, {
|
|
1151
|
+
waitForConfirmation: options?.waitForConfirmation ?? true
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
/** Get current RPC health metrics */
|
|
1155
|
+
getHealthReport() {
|
|
1156
|
+
return this.pool.getHealthReport();
|
|
1157
|
+
}
|
|
1158
|
+
/** Clean up: stop background tasks, clear intervals */
|
|
1159
|
+
destroy() {
|
|
1160
|
+
this.pool.destroy();
|
|
1161
|
+
this.blockhashManager.destroy();
|
|
1162
|
+
this.events.removeAllListeners();
|
|
1163
|
+
}
|
|
1164
|
+
/** Build a working copy of the transaction with compute budget instructions prepended */
|
|
1165
|
+
async prepareTransaction(transaction, options) {
|
|
1166
|
+
if (this.config.priorityFee !== false && isLegacyTransaction(transaction)) {
|
|
1167
|
+
const feeAmount = await this.resolvePriorityFee(options);
|
|
1168
|
+
const computeUnits = options?.computeUnits ?? DEFAULT_PRIORITY_FEE_CONFIG.defaultComputeUnits;
|
|
1169
|
+
const budgetInstructions = createComputeBudgetInstructions({
|
|
1170
|
+
computeUnits,
|
|
1171
|
+
microLamports: feeAmount
|
|
1172
|
+
});
|
|
1173
|
+
const copy = new web3_js.Transaction();
|
|
1174
|
+
for (const ix of budgetInstructions) copy.add(ix);
|
|
1175
|
+
for (const ix of transaction.instructions) copy.add(ix);
|
|
1176
|
+
return { tx: copy, feeAmount };
|
|
1177
|
+
}
|
|
1178
|
+
return { tx: transaction, feeAmount: void 0 };
|
|
1179
|
+
}
|
|
1180
|
+
/** Set blockhash, fee payer, and sign the transaction */
|
|
1181
|
+
signTransaction(tx, blockhashInfo) {
|
|
1182
|
+
if (isLegacyTransaction(tx)) {
|
|
1183
|
+
tx.recentBlockhash = blockhashInfo.blockhash;
|
|
1184
|
+
tx.feePayer = this.config.signer.publicKey;
|
|
1185
|
+
tx.sign(this.config.signer);
|
|
1186
|
+
} else {
|
|
1187
|
+
tx.message.recentBlockhash = blockhashInfo.blockhash;
|
|
1188
|
+
tx.sign([this.config.signer]);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
/** Run simulation if enabled; returns compute units consumed */
|
|
1192
|
+
async runSimulation(tx, options) {
|
|
1193
|
+
const skip = options?.skipSimulation ?? this.config.simulation === false;
|
|
1194
|
+
if (skip) return void 0;
|
|
1195
|
+
const conn = this.pool.getConnection();
|
|
1196
|
+
const simConfig = this.config.simulation !== false ? this.config.simulation : void 0;
|
|
1197
|
+
const simResult = await simulateTransaction(conn, tx, simConfig, this.logger);
|
|
1198
|
+
this.events.emit("simulated" /* SIMULATED */, {
|
|
1199
|
+
signature: "",
|
|
1200
|
+
unitsConsumed: simResult.unitsConsumed,
|
|
1201
|
+
logs: simResult.logs
|
|
1202
|
+
});
|
|
1203
|
+
if (!simResult.success) {
|
|
1204
|
+
throw new SolTxError(
|
|
1205
|
+
"SIMULATION_FAILED" /* SIMULATION_FAILED */,
|
|
1206
|
+
`Simulation failed: ${simResult.error?.message ?? "unknown error"}`,
|
|
1207
|
+
{ context: { logs: simResult.logs } }
|
|
1208
|
+
);
|
|
1209
|
+
}
|
|
1210
|
+
return simResult.unitsConsumed;
|
|
1211
|
+
}
|
|
1212
|
+
/** Send serialized transaction via the connection pool with fallback */
|
|
1213
|
+
async sendRawTransaction(tx) {
|
|
1214
|
+
return this.pool.withFallback(async (conn) => {
|
|
1215
|
+
const serialized = tx.serialize();
|
|
1216
|
+
return conn.sendRawTransaction(serialized, {
|
|
1217
|
+
skipPreflight: true,
|
|
1218
|
+
maxRetries: 0
|
|
1219
|
+
});
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
/** Confirm a transaction and return its slot, or throw on failure/expiry */
|
|
1223
|
+
async awaitConfirmation(signature, lastValidBlockHeight, commitment) {
|
|
1224
|
+
const conn = this.pool.getConnection();
|
|
1225
|
+
const confirmResult = await this.confirmer.confirm(conn, signature, lastValidBlockHeight, this.config.confirmation);
|
|
1226
|
+
if (confirmResult.status === "failed") {
|
|
1227
|
+
throw new SolTxError(
|
|
1228
|
+
"TRANSACTION_FAILED" /* TRANSACTION_FAILED */,
|
|
1229
|
+
confirmResult.error?.message ?? "Transaction failed on chain"
|
|
1230
|
+
);
|
|
1231
|
+
}
|
|
1232
|
+
if (confirmResult.status === "expired") {
|
|
1233
|
+
throw new SolTxError("BLOCKHASH_EXPIRED" /* BLOCKHASH_EXPIRED */, "Blockhash expired during confirmation");
|
|
1234
|
+
}
|
|
1235
|
+
this.events.emit("confirmed" /* CONFIRMED */, { signature, slot: confirmResult.slot ?? 0, commitment });
|
|
1236
|
+
return confirmResult.slot ?? 0;
|
|
1237
|
+
}
|
|
1238
|
+
/** Construct a SendResult */
|
|
1239
|
+
buildResult(signature, slot, commitment, attempt, startTime, unitsConsumed, priorityFee) {
|
|
1240
|
+
return {
|
|
1241
|
+
signature,
|
|
1242
|
+
slot,
|
|
1243
|
+
commitment,
|
|
1244
|
+
attempts: attempt + 1,
|
|
1245
|
+
totalLatencyMs: Date.now() - startTime,
|
|
1246
|
+
unitsConsumed,
|
|
1247
|
+
priorityFee
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
async resolvePriorityFee(options) {
|
|
1251
|
+
if (options?.priorityFee && isStaticFee(options.priorityFee)) {
|
|
1252
|
+
return options.priorityFee.microLamports;
|
|
1253
|
+
}
|
|
1254
|
+
const conn = this.pool.getConnection();
|
|
1255
|
+
let feeConfig;
|
|
1256
|
+
if (options?.priorityFee && !isStaticFee(options.priorityFee)) {
|
|
1257
|
+
feeConfig = options.priorityFee;
|
|
1258
|
+
} else if (this.config.priorityFee !== false && this.config.priorityFee !== void 0) {
|
|
1259
|
+
feeConfig = this.config.priorityFee;
|
|
1260
|
+
}
|
|
1261
|
+
const result = await estimatePriorityFee(conn, feeConfig);
|
|
1262
|
+
return result.microLamports;
|
|
1263
|
+
}
|
|
1264
|
+
};
|
|
1265
|
+
|
|
1266
|
+
exports.BlockhashManager = BlockhashManager;
|
|
1267
|
+
exports.BundleStatus = BundleStatus;
|
|
1268
|
+
exports.CircuitBreaker = CircuitBreaker;
|
|
1269
|
+
exports.CircuitState = CircuitState;
|
|
1270
|
+
exports.ConnectionPool = ConnectionPool;
|
|
1271
|
+
exports.HealthTracker = HealthTracker;
|
|
1272
|
+
exports.JITO_BLOCK_ENGINE_URL = JITO_BLOCK_ENGINE_URL;
|
|
1273
|
+
exports.JITO_TIP_ACCOUNTS = JITO_TIP_ACCOUNTS;
|
|
1274
|
+
exports.JitoBundleSender = JitoBundleSender;
|
|
1275
|
+
exports.RetryableError = RetryableError;
|
|
1276
|
+
exports.SolTxError = SolTxError;
|
|
1277
|
+
exports.SolTxErrorCode = SolTxErrorCode;
|
|
1278
|
+
exports.TransactionConfirmer = TransactionConfirmer;
|
|
1279
|
+
exports.TransactionSender = TransactionSender;
|
|
1280
|
+
exports.TransactionSenderBuilder = TransactionSenderBuilder;
|
|
1281
|
+
exports.TxEvent = TxEvent;
|
|
1282
|
+
exports.TypedEventEmitter = TypedEventEmitter;
|
|
1283
|
+
exports.classifyError = classifyError;
|
|
1284
|
+
exports.createComputeBudgetInstructions = createComputeBudgetInstructions;
|
|
1285
|
+
exports.createDefaultLogger = createDefaultLogger;
|
|
1286
|
+
exports.createTipInstruction = createTipInstruction;
|
|
1287
|
+
exports.estimatePriorityFee = estimatePriorityFee;
|
|
1288
|
+
exports.getNextTipAccount = getNextTipAccount;
|
|
1289
|
+
exports.isBlockhashExpired = isBlockhashExpired;
|
|
1290
|
+
exports.isLegacyTransaction = isLegacyTransaction;
|
|
1291
|
+
exports.isRateLimited = isRateLimited;
|
|
1292
|
+
exports.isVersionedTransaction = isVersionedTransaction;
|
|
1293
|
+
exports.resetTipRotation = resetTipRotation;
|
|
1294
|
+
exports.simulateTransaction = simulateTransaction;
|
|
1295
|
+
exports.withRetry = withRetry;
|
|
1296
|
+
//# sourceMappingURL=index.cjs.map
|
|
1297
|
+
//# sourceMappingURL=index.cjs.map
|