Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@

## 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)
- [Tree-shaking](guides/tree-shaking.md)

## Reference

- [Reference](reference/README.md)
- [Known documentation drift](reference/known-drift.md)
1 change: 0 additions & 1 deletion docs/guides/README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
# guides

1 change: 0 additions & 1 deletion docs/reference/README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
# reference

11 changes: 9 additions & 2 deletions src/signing/_fastWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<TinySecp256k1 | undefined> {
export async function loadTinySecp256k1(
importEcc: () => Promise<unknown> = () => import("tiny-secp256k1"),
): Promise<TinySecp256k1 | undefined> {
try {
const ecc = await import("tiny-secp256k1");
const ecc = (await importEcc()) as TinySecp256k1;
if (
typeof ecc.signRecoverable !== "function" ||
typeof ecc.pointFromScalar !== "function" ||
Expand Down
34 changes: 33 additions & 1 deletion tests/signing/fastWallet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) --------------

Expand Down Expand Up @@ -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", () => {
Expand Down
Loading