From c117aa3778863c248e20a9e613d45d6b33279378 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 8 May 2025 12:19:48 +0000 Subject: [PATCH 1/2] feat: support multiple JITO endpoints with round-robin retry Co-Authored-By: Ali Behjati --- apps/price_pusher/src/solana/command.ts | 17 +++++-- apps/price_pusher/src/solana/solana.ts | 50 +++++++++++-------- .../solana/sdk/js/solana_utils/package.json | 2 +- .../solana/sdk/js/solana_utils/src/jito.ts | 37 ++++++++++++-- 4 files changed, 77 insertions(+), 29 deletions(-) diff --git a/apps/price_pusher/src/solana/command.ts b/apps/price_pusher/src/solana/command.ts index 93420ef79d..e9a56f17b3 100644 --- a/apps/price_pusher/src/solana/command.ts +++ b/apps/price_pusher/src/solana/command.ts @@ -50,7 +50,7 @@ export default { default: 50000, } as Options, "jito-endpoint": { - description: "Jito endpoint", + description: "Jito endpoint(s) - comma-separated list of endpoints", type: "string", optional: true, } as Options, @@ -209,7 +209,13 @@ export default { Uint8Array.from(JSON.parse(fs.readFileSync(jitoKeypairFile, "ascii"))), ); - const jitoClient = searcherClient(jitoEndpoint, jitoKeypair); + const jitoEndpoints = jitoEndpoint + .split(",") + .map((endpoint: string) => endpoint.trim()); + const jitoClients: SearcherClient[] = jitoEndpoints.map( + (endpoint: string) => searcherClient(endpoint, jitoKeypair), + ); + solanaPricePusher = new SolanaPricePusherJito( pythSolanaReceiver, hermesClient, @@ -218,13 +224,16 @@ export default { jitoTipLamports, dynamicJitoTips, maxJitoTipLamports, - jitoClient, + jitoClients, jitoBundleSize, updatesPerJitoBundle, + 60000, // Default max retry time of 60 seconds lookupTableAccount, ); - onBundleResult(jitoClient, logger.child({ module: "JitoClient" })); + jitoClients.forEach((client, index) => { + onBundleResult(client, logger.child({ module: `JitoClient-${index}` })); + }); } else { solanaPricePusher = new SolanaPricePusher( pythSolanaReceiver, diff --git a/apps/price_pusher/src/solana/solana.ts b/apps/price_pusher/src/solana/solana.ts index e907283496..ff64a7fc17 100644 --- a/apps/price_pusher/src/solana/solana.ts +++ b/apps/price_pusher/src/solana/solana.ts @@ -165,9 +165,10 @@ export class SolanaPricePusherJito implements IPricePusher { private defaultJitoTipLamports: number, private dynamicJitoTips: boolean, private maxJitoTipLamports: number, - private searcherClient: SearcherClient, + private searcherClients: SearcherClient[], private jitoBundleSize: number, private updatesPerJitoBundle: number, + private maxRetryTimeMs: number = 60000, // Default to 60 seconds max retry time private addressLookupTableAccount?: AddressLookupTableAccount, ) {} @@ -242,27 +243,34 @@ export class SolanaPricePusherJito implements IPricePusher { jitoBundleSize: this.jitoBundleSize, }); - let retries = 60; - while (retries > 0) { - try { - await sendTransactionsJito( - transactions, - this.searcherClient, - this.pythSolanaReceiver.wallet, - ); - break; - } catch (err: any) { - if (err.code === 8 && err.details?.includes("Rate limit exceeded")) { - this.logger.warn("Rate limit hit, waiting before retry..."); - await this.sleep(1100); // Wait slightly more than 1 second - retries--; - if (retries === 0) { - this.logger.error("Max retries reached for rate limit"); - throw err; - } - } else { - throw err; + try { + await sendTransactionsJito( + transactions, + this.searcherClients, + this.pythSolanaReceiver.wallet, + { maxRetryTimeMs: this.maxRetryTimeMs }, + ); + } catch (err: any) { + if (err.code === 8 && err.details?.includes("Rate limit exceeded")) { + this.logger.warn("Rate limit hit, waiting before retry..."); + await this.sleep(1100); // Wait slightly more than 1 second + try { + await sendTransactionsJito( + transactions, + this.searcherClients, + this.pythSolanaReceiver.wallet, + { maxRetryTimeMs: this.maxRetryTimeMs }, + ); + } catch (retryErr: any) { + this.logger.error("Failed after rate limit retry"); + throw retryErr; } + } else { + this.logger.error( + { err }, + "Failed to send transactions via all JITO endpoints", + ); + throw err; } } diff --git a/target_chains/solana/sdk/js/solana_utils/package.json b/target_chains/solana/sdk/js/solana_utils/package.json index d647e92b53..20fea33bb9 100644 --- a/target_chains/solana/sdk/js/solana_utils/package.json +++ b/target_chains/solana/sdk/js/solana_utils/package.json @@ -1,6 +1,6 @@ { "name": "@pythnetwork/solana-utils", - "version": "0.4.4", + "version": "0.4.5", "description": "Utility functions for Solana", "homepage": "https://pyth.network", "main": "lib/index.js", diff --git a/target_chains/solana/sdk/js/solana_utils/src/jito.ts b/target_chains/solana/sdk/js/solana_utils/src/jito.ts index 2a6a5c290e..5db14c1315 100644 --- a/target_chains/solana/sdk/js/solana_utils/src/jito.ts +++ b/target_chains/solana/sdk/js/solana_utils/src/jito.ts @@ -42,9 +42,23 @@ export async function sendTransactionsJito( tx: VersionedTransaction; signers?: Signer[] | undefined; }[], - searcherClient: SearcherClient, + searcherClients: SearcherClient | SearcherClient[], wallet: Wallet, + options: { + maxRetryTimeMs?: number; + } = {}, ): Promise { + const clients = Array.isArray(searcherClients) + ? searcherClients + : [searcherClients]; + + if (clients.length === 0) { + throw new Error("No searcher clients provided"); + } + + const maxRetryTimeMs = options.maxRetryTimeMs || 60000; // Default to 60 seconds + const startTime = Date.now(); + const signedTransactions = []; for (const transaction of transactions) { @@ -64,7 +78,24 @@ export async function sendTransactionsJito( ); const bundle = new Bundle(signedTransactions, 2); - await searcherClient.sendBundle(bundle); - return firstTransactionSignature; + let lastError: Error | null = null; + let clientIndex = 0; + + while (Date.now() - startTime < maxRetryTimeMs) { + const currentClient = clients[clientIndex]; + try { + await currentClient.sendBundle(bundle); + return firstTransactionSignature; + } catch (err: any) { + lastError = err; + clientIndex = (clientIndex + 1) % clients.length; + await new Promise((resolve) => setTimeout(resolve, 500)); + } + } + + throw ( + lastError || + new Error("Failed to send transactions via JITO after maximum retry time") + ); } From e6d98c2e7e62f822cafc719bf1307e722645baef Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 9 May 2025 18:23:37 +0000 Subject: [PATCH 2/2] fix: rename jito-endpoint to jito-endpoints Co-Authored-By: Ali Behjati --- apps/price_pusher/src/solana/command.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/price_pusher/src/solana/command.ts b/apps/price_pusher/src/solana/command.ts index e9a56f17b3..0b6353cefa 100644 --- a/apps/price_pusher/src/solana/command.ts +++ b/apps/price_pusher/src/solana/command.ts @@ -49,7 +49,7 @@ export default { type: "number", default: 50000, } as Options, - "jito-endpoint": { + "jito-endpoints": { description: "Jito endpoint(s) - comma-separated list of endpoints", type: "string", optional: true, @@ -117,7 +117,7 @@ export default { pythContractAddress, pushingFrequency, pollingFrequency, - jitoEndpoint, + jitoEndpoints, jitoKeypairFile, jitoTipLamports, dynamicJitoTips, @@ -209,10 +209,10 @@ export default { Uint8Array.from(JSON.parse(fs.readFileSync(jitoKeypairFile, "ascii"))), ); - const jitoEndpoints = jitoEndpoint + const jitoEndpointsList = jitoEndpoints .split(",") .map((endpoint: string) => endpoint.trim()); - const jitoClients: SearcherClient[] = jitoEndpoints.map( + const jitoClients: SearcherClient[] = jitoEndpointsList.map( (endpoint: string) => searcherClient(endpoint, jitoKeypair), );