Skip to content

Commit c117aa3

Browse files
feat: support multiple JITO endpoints with round-robin retry
Co-Authored-By: Ali Behjati <[email protected]>
1 parent fb15906 commit c117aa3

File tree

4 files changed

+77
-29
lines changed

4 files changed

+77
-29
lines changed

apps/price_pusher/src/solana/command.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export default {
5050
default: 50000,
5151
} as Options,
5252
"jito-endpoint": {
53-
description: "Jito endpoint",
53+
description: "Jito endpoint(s) - comma-separated list of endpoints",
5454
type: "string",
5555
optional: true,
5656
} as Options,
@@ -209,7 +209,13 @@ export default {
209209
Uint8Array.from(JSON.parse(fs.readFileSync(jitoKeypairFile, "ascii"))),
210210
);
211211

212-
const jitoClient = searcherClient(jitoEndpoint, jitoKeypair);
212+
const jitoEndpoints = jitoEndpoint
213+
.split(",")
214+
.map((endpoint: string) => endpoint.trim());
215+
const jitoClients: SearcherClient[] = jitoEndpoints.map(
216+
(endpoint: string) => searcherClient(endpoint, jitoKeypair),
217+
);
218+
213219
solanaPricePusher = new SolanaPricePusherJito(
214220
pythSolanaReceiver,
215221
hermesClient,
@@ -218,13 +224,16 @@ export default {
218224
jitoTipLamports,
219225
dynamicJitoTips,
220226
maxJitoTipLamports,
221-
jitoClient,
227+
jitoClients,
222228
jitoBundleSize,
223229
updatesPerJitoBundle,
230+
60000, // Default max retry time of 60 seconds
224231
lookupTableAccount,
225232
);
226233

227-
onBundleResult(jitoClient, logger.child({ module: "JitoClient" }));
234+
jitoClients.forEach((client, index) => {
235+
onBundleResult(client, logger.child({ module: `JitoClient-${index}` }));
236+
});
228237
} else {
229238
solanaPricePusher = new SolanaPricePusher(
230239
pythSolanaReceiver,

apps/price_pusher/src/solana/solana.ts

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -165,9 +165,10 @@ export class SolanaPricePusherJito implements IPricePusher {
165165
private defaultJitoTipLamports: number,
166166
private dynamicJitoTips: boolean,
167167
private maxJitoTipLamports: number,
168-
private searcherClient: SearcherClient,
168+
private searcherClients: SearcherClient[],
169169
private jitoBundleSize: number,
170170
private updatesPerJitoBundle: number,
171+
private maxRetryTimeMs: number = 60000, // Default to 60 seconds max retry time
171172
private addressLookupTableAccount?: AddressLookupTableAccount,
172173
) {}
173174

@@ -242,27 +243,34 @@ export class SolanaPricePusherJito implements IPricePusher {
242243
jitoBundleSize: this.jitoBundleSize,
243244
});
244245

245-
let retries = 60;
246-
while (retries > 0) {
247-
try {
248-
await sendTransactionsJito(
249-
transactions,
250-
this.searcherClient,
251-
this.pythSolanaReceiver.wallet,
252-
);
253-
break;
254-
} catch (err: any) {
255-
if (err.code === 8 && err.details?.includes("Rate limit exceeded")) {
256-
this.logger.warn("Rate limit hit, waiting before retry...");
257-
await this.sleep(1100); // Wait slightly more than 1 second
258-
retries--;
259-
if (retries === 0) {
260-
this.logger.error("Max retries reached for rate limit");
261-
throw err;
262-
}
263-
} else {
264-
throw err;
246+
try {
247+
await sendTransactionsJito(
248+
transactions,
249+
this.searcherClients,
250+
this.pythSolanaReceiver.wallet,
251+
{ maxRetryTimeMs: this.maxRetryTimeMs },
252+
);
253+
} catch (err: any) {
254+
if (err.code === 8 && err.details?.includes("Rate limit exceeded")) {
255+
this.logger.warn("Rate limit hit, waiting before retry...");
256+
await this.sleep(1100); // Wait slightly more than 1 second
257+
try {
258+
await sendTransactionsJito(
259+
transactions,
260+
this.searcherClients,
261+
this.pythSolanaReceiver.wallet,
262+
{ maxRetryTimeMs: this.maxRetryTimeMs },
263+
);
264+
} catch (retryErr: any) {
265+
this.logger.error("Failed after rate limit retry");
266+
throw retryErr;
265267
}
268+
} else {
269+
this.logger.error(
270+
{ err },
271+
"Failed to send transactions via all JITO endpoints",
272+
);
273+
throw err;
266274
}
267275
}
268276

target_chains/solana/sdk/js/solana_utils/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@pythnetwork/solana-utils",
3-
"version": "0.4.4",
3+
"version": "0.4.5",
44
"description": "Utility functions for Solana",
55
"homepage": "https://pyth.network",
66
"main": "lib/index.js",

target_chains/solana/sdk/js/solana_utils/src/jito.ts

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,23 @@ export async function sendTransactionsJito(
4242
tx: VersionedTransaction;
4343
signers?: Signer[] | undefined;
4444
}[],
45-
searcherClient: SearcherClient,
45+
searcherClients: SearcherClient | SearcherClient[],
4646
wallet: Wallet,
47+
options: {
48+
maxRetryTimeMs?: number;
49+
} = {},
4750
): Promise<string> {
51+
const clients = Array.isArray(searcherClients)
52+
? searcherClients
53+
: [searcherClients];
54+
55+
if (clients.length === 0) {
56+
throw new Error("No searcher clients provided");
57+
}
58+
59+
const maxRetryTimeMs = options.maxRetryTimeMs || 60000; // Default to 60 seconds
60+
const startTime = Date.now();
61+
4862
const signedTransactions = [];
4963

5064
for (const transaction of transactions) {
@@ -64,7 +78,24 @@ export async function sendTransactionsJito(
6478
);
6579

6680
const bundle = new Bundle(signedTransactions, 2);
67-
await searcherClient.sendBundle(bundle);
6881

69-
return firstTransactionSignature;
82+
let lastError: Error | null = null;
83+
let clientIndex = 0;
84+
85+
while (Date.now() - startTime < maxRetryTimeMs) {
86+
const currentClient = clients[clientIndex];
87+
try {
88+
await currentClient.sendBundle(bundle);
89+
return firstTransactionSignature;
90+
} catch (err: any) {
91+
lastError = err;
92+
clientIndex = (clientIndex + 1) % clients.length;
93+
await new Promise((resolve) => setTimeout(resolve, 500));
94+
}
95+
}
96+
97+
throw (
98+
lastError ||
99+
new Error("Failed to send transactions via JITO after maximum retry time")
100+
);
70101
}

0 commit comments

Comments
 (0)