From 5e45b529a52d8db4b436d081b64495d688ef5281 Mon Sep 17 00:00:00 2001 From: 0xghost42 Date: Thu, 14 May 2026 16:28:48 +0530 Subject: [PATCH] fix(solana-utils): lazy-load jito-ts so non-Jito consumers don't crash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #1838. `jito.ts` previously did top-level value imports of `jito-ts/dist/sdk/block-engine/{searcher,types}`. Pulling those modules also pulls jito-ts's nested `@solana/web3.js@~1.77.3`, which transitively `require`s `rpc-websockets/dist/lib/client` — a path removed in `rpc-websockets@>=7.11`. Because `solana_utils/transaction.ts` imports `buildJitoTipInstruction` from `./jito`, any consumer that loaded a @pythnetwork/solana-utils export — including `PythSolanaReceiver` from `@pythnetwork/pyth-solana-receiver` — eagerly walked the broken chain and crashed at module load with: Error: Cannot find module 'rpc-websockets/dist/lib/client' This change splits the imports so that jito-ts only resolves when the Jito send path is actually exercised: - `SearcherClient` and `Bundle` become `import type` — these were only used as types in function signatures. - `sendTransactionsJito` dynamic-`import`s `Bundle` from `jito-ts/.../types` immediately before constructing the bundle. The function is already async, so this is zero-cost for callers. Non-Jito consumers (the case reported in the issue) no longer trigger the broken require. Jito users still get the same runtime path; the underlying jito-ts/rpc-websockets clash there is a separate problem for jito-ts to solve. Tests - Added `JitoLazyImport.test.ts` asserting that importing either `../transaction` or `../jito` leaves `jito-ts/*` out of `require.cache`. - Existing `TransactionSize.test.ts` still passes (2 cases). - `pnpm --filter @pythnetwork/solana-utils build` succeeds (esm + cjs). Bumped to 0.6.1 (patch — runtime contract preserved, only load order changes). --- .../solana/sdk/js/solana_utils/package.json | 2 +- .../src/__tests__/JitoLazyImport.test.ts | 33 +++++++++++++++++++ .../solana/sdk/js/solana_utils/src/jito.ts | 12 +++++-- 3 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 target_chains/solana/sdk/js/solana_utils/src/__tests__/JitoLazyImport.test.ts diff --git a/target_chains/solana/sdk/js/solana_utils/package.json b/target_chains/solana/sdk/js/solana_utils/package.json index bced0514d9..dd1850cc65 100644 --- a/target_chains/solana/sdk/js/solana_utils/package.json +++ b/target_chains/solana/sdk/js/solana_utils/package.json @@ -81,5 +81,5 @@ }, "type": "module", "types": "./dist/cjs/index.d.ts", - "version": "0.6.0" + "version": "0.6.1" } diff --git a/target_chains/solana/sdk/js/solana_utils/src/__tests__/JitoLazyImport.test.ts b/target_chains/solana/sdk/js/solana_utils/src/__tests__/JitoLazyImport.test.ts new file mode 100644 index 0000000000..12ac1762c7 --- /dev/null +++ b/target_chains/solana/sdk/js/solana_utils/src/__tests__/JitoLazyImport.test.ts @@ -0,0 +1,33 @@ +// Regression test for https://github.com/pyth-network/pyth-crosschain/issues/1838. +// +// `jito.ts` historically eager-imported `jito-ts/dist/sdk/block-engine/{searcher,types}`, +// which transitively required `jito-ts`'s nested `@solana/web3.js@~1.77.3` -> +// `rpc-websockets/dist/lib/client` — a path removed in `rpc-websockets@>=7.11`. +// Consumers that loaded any `@pythnetwork/solana-utils` export therefore +// crashed with `Cannot find module 'rpc-websockets/dist/lib/client'`, even +// when they never used the Jito helpers. +// +// We now type-only-import `SearcherClient`/`Bundle` and dynamic-import +// `Bundle` inside `sendTransactionsJito`. This test guards that contract. + +describe("jito-ts lazy loading", () => { + it("does not load jito-ts when importing transaction helpers", async () => { + await import("../transaction"); + + const cachedJitoPath = Object.keys(require.cache).find((p) => + p.includes(`${"/node_modules/"}jito-ts/`), + ); + + expect(cachedJitoPath).toBeUndefined(); + }); + + it("does not load jito-ts when importing jito.ts itself", async () => { + await import("../jito"); + + const cachedJitoPath = Object.keys(require.cache).find((p) => + p.includes(`${"/node_modules/"}jito-ts/`), + ); + + expect(cachedJitoPath).toBeUndefined(); + }); +}); 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 28d7e6d92d..edcf7dc580 100644 --- a/target_chains/solana/sdk/js/solana_utils/src/jito.ts +++ b/target_chains/solana/sdk/js/solana_utils/src/jito.ts @@ -9,8 +9,13 @@ import type { } from "@solana/web3.js"; import { PublicKey, SystemProgram } from "@solana/web3.js"; import bs58 from "bs58"; +// jito-ts is loaded lazily inside `sendTransactionsJito` so that consumers +// who only use the non-Jito transaction helpers do not pay the cost of +// jito-ts pulling its nested `@solana/web3.js@~1.77.3` (which transitively +// `require`s a rpc-websockets path removed in rpc-websockets@>=7.11). See +// https://github.com/pyth-network/pyth-crosschain/issues/1838. import type { SearcherClient } from "jito-ts/dist/sdk/block-engine/searcher"; -import { Bundle } from "jito-ts/dist/sdk/block-engine/types"; +import type { Bundle as BundleType } from "jito-ts/dist/sdk/block-engine/types"; import type { Logger } from "ts-log"; import { dummyLogger } from "ts-log"; @@ -85,7 +90,10 @@ export async function sendTransactionsJito( signedTransactions[0]?.signatures[0]!, ); - const bundle = new Bundle(signedTransactions, 2); + // Dynamic import so jito-ts (and its old @solana/web3.js transitive dep) + // is only resolved when this code path is actually taken. + const { Bundle } = await import("jito-ts/dist/sdk/block-engine/types"); + const bundle: BundleType = new Bundle(signedTransactions, 2); let lastError: Error | null | undefined; let totalAttempts = 0;