diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 82760dee..9bfe6790 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -11,6 +11,7 @@ ## Guides +- [Guides](guides/README.md) - [Market orders](guides/market-orders.md) - [Agent wallets and vaults](guides/agent-wallets-and-vaults.md) - [Browser wallets](guides/browser-wallets.md) @@ -18,4 +19,5 @@ ## Reference +- [Reference](reference/README.md) - [Known documentation drift](reference/known-drift.md) diff --git a/docs/guides/README.md b/docs/guides/README.md index 6ae1da70..6bc39131 100644 --- a/docs/guides/README.md +++ b/docs/guides/README.md @@ -1,2 +1 @@ # guides - diff --git a/docs/reference/README.md b/docs/reference/README.md index d3673bac..c7704085 100644 --- a/docs/reference/README.md +++ b/docs/reference/README.md @@ -1,2 +1 @@ # reference - diff --git a/src/signing/_fastWallet.ts b/src/signing/_fastWallet.ts index 096bb100..b2f06663 100644 --- a/src/signing/_fastWallet.ts +++ b/src/signing/_fastWallet.ts @@ -43,10 +43,17 @@ let fallbackWarningShown = false; * Dynamically imports `tiny-secp256k1`, returning `undefined` when the optional dependency is * missing or unusable. The shape check guards against a partially linked install the same way the * `try` guards against an absent one: both route to the viem/noble fallback. + * + * Exported (never from `mod.ts`) so tests can drive the failure arms through the same code the + * real path uses: ESM module records are immutable, so a rejecting or partially linked real + * import cannot be simulated any other way — see {@linkcode _setEccLoaderForTests} for why module + * mocking is not an option. Production calls always take the default real import. */ -async function loadTinySecp256k1(): Promise { +export async function loadTinySecp256k1( + importEcc: () => Promise = () => import("tiny-secp256k1"), +): Promise { try { - const ecc = await import("tiny-secp256k1"); + const ecc = (await importEcc()) as TinySecp256k1; if ( typeof ecc.signRecoverable !== "function" || typeof ecc.pointFromScalar !== "function" || diff --git a/tests/signing/fastWallet.test.ts b/tests/signing/fastWallet.test.ts index f6e8979b..21d847fe 100644 --- a/tests/signing/fastWallet.test.ts +++ b/tests/signing/fastWallet.test.ts @@ -26,7 +26,7 @@ import { signUserSignedAction, } from "@bloxwap/hyperliquid/signing"; import { ApproveAgentTypes } from "@bloxwap/hyperliquid/api/exchange"; -import { _setEccLoaderForTests } from "../../src/signing/_fastWallet.ts"; +import { _setEccLoaderForTests, loadTinySecp256k1 } from "../../src/signing/_fastWallet.ts"; // --- Fixtures (same shapes as tests/signing/fastDigest.test.ts) -------------- @@ -276,6 +276,38 @@ describe("createFastLocalWallet() WASM module validation", () => { }); }); +// --- Real loader failure arms --------------------------------------------------- + +describe("loadTinySecp256k1() failure arms", () => { + test("returns undefined when the real import rejects (dependency absent)", async () => { + // The same code path the default call takes: a rejecting import lands in the catch arm. + await expect(loadTinySecp256k1(() => Promise.reject(new Error("Cannot find module")))).resolves.toBeUndefined(); + }); + + test("returns undefined when the module is partially linked (shape check)", async () => { + // Missing one of the three required functions — the shape guard routes to the fallback. + await expect( + loadTinySecp256k1(() => + Promise.resolve({ signRecoverable: () => new Uint8Array(65), pointFromScalar: () => new Uint8Array(33) }), + ), + ).resolves.toBeUndefined(); + }); + + test("returns the module when the shape check passes", async () => { + const ecc = { + isPrivate: () => true, + pointFromScalar: () => new Uint8Array(33), + signRecoverable: () => ({ signature: new Uint8Array(64), recoveryId: 0 }), + }; + await expect(loadTinySecp256k1(() => Promise.resolve(ecc))).resolves.toBe(ecc); + }); + + test("the default import resolves to the real tiny-secp256k1", async () => { + const ecc = await loadTinySecp256k1(); + expect(typeof ecc?.signRecoverable).toBe("function"); + }); +}); + // --- JSON-RPC wallets stay untouched -------------------------------------------- describe("createFastLocalWallet() alongside JSON-RPC wallets", () => {