From a5b1aec93bf03502d567506be8133beed7da95cd Mon Sep 17 00:00:00 2001 From: Joe Blau Date: Sun, 26 Jul 2026 22:02:40 -0700 Subject: [PATCH] test(coverage): close the coverage tail on shims, signing internals, decimal formatting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Focused offline tests for the remaining uncovered lines: - transport/_polyfills.ts: native pass-throughs, plus the React Native fallbacks via a cache-busting re-import with the platform globals deleted. - transport/_redact.ts: a toJSON returning its own owner or an ancestor closes a cycle immediately. - signing/_abstractWallet.ts: signature length/recovery-value validation and the wrap-vs-rethrow error paths of signTypedData, signRawDigest, getWalletAddress, and getWalletChainId. - signing/_canonicalize.ts: the fast-path variant branch and the union structural matcher's required/optional key handling. - signing/_fastWallet.ts: the deriveAddress guard for a broken WASM module. - signing/_keccak.ts: a hasher throwing during the known-answer self-check, and the real loader's import-failure fallback (CJS entry hidden via require.cache drop + rename, restored afterwards). - utils/_decimal.ts / _format.ts: half-even edge branches (round-up-to-unit, all-nines carry-out) and the subnormal mantissa decomposition, forced through the exact path. _fastWallet.ts keeps 3 uncoverable lines (54-55, 58): the real ESM loader's failure internals cannot be exercised in a shared bun test process — an evaluated ESM module record is immutable, mock.module poisons the specifier process-wide, and a forced resolution failure poisons it permanently, all of which would break the differential suites. --- .../signing/canonicalize_conformance.test.ts | 55 +++++++ tests/signing/fastWallet.test.ts | 30 ++++ tests/signing/keccak.test.ts | 52 ++++++ tests/signing/mod.test.ts | 152 +++++++++++++++++- tests/transport/_polyfills.test.ts | 84 ++++++++++ tests/transport/_redact.test.ts | 30 ++++ tests/utils/format.test.ts | 51 ++++++ 7 files changed, 453 insertions(+), 1 deletion(-) create mode 100644 tests/transport/_polyfills.test.ts diff --git a/tests/signing/canonicalize_conformance.test.ts b/tests/signing/canonicalize_conformance.test.ts index 7c8bdc83..c755d0cf 100644 --- a/tests/signing/canonicalize_conformance.test.ts +++ b/tests/signing/canonicalize_conformance.test.ts @@ -782,3 +782,58 @@ describe("canonicalize() fast path (issue #8)", () => { }); }); }); + +describe("canonicalize() fast-path variant and union checks", () => { + test("a canonical variant child passes by reference", () => { + const schema = v.object({ + t: v.variant("type", [ + v.object({ type: v.literal("a"), x: v.number() }), + v.object({ type: v.literal("b"), y: v.number() }), + ]), + }); + const value = { t: { type: "a" as const, x: 1 } }; + + // Already canonical: the identity check must descend into the variant child and match its option. + expect(canonicalize(schema, value)).toBe(value); + }); + + test("a non-canonical variant child is rebuilt in schema order", () => { + const schema = v.object({ + t: v.variant("type", [ + v.object({ type: v.literal("a"), x: v.number() }), + v.object({ type: v.literal("b"), y: v.number() }), + ]), + }); + const value = { t: { x: 1, type: "a" as const } }; + + const out = canonicalize(schema, value); + expect(out).not.toBe(value); + expect(Object.keys(out.t)).toEqual(["type", "x"]); + expect(out).toEqual(value); + }); + + test("an unmatched variant child takes the slow path and throws CanonicalizeError", () => { + const schema = v.object({ + t: v.variant("type", [v.object({ type: v.literal("a"), x: v.number() })]), + }); + + expect(() => canonicalize(schema, { t: { type: "z" } })).toThrow(CanonicalizeError); + }); + + test("union matching skips an option that is missing a required key", () => { + const options = [v.object({ a: v.string(), b: v.string() }), v.object({ a: v.string() })]; + // Top-level union: `walk` consults the structural matcher directly. + expect(canonicalize(v.union(options), { a: "1" })).toEqual({ a: "1" }); + // Union as a child: the identity check consults it too, and the value passes by reference. + const schema = v.object({ u: v.union(options) }); + const value = { u: { a: "1" } }; + expect(canonicalize(schema, value)).toBe(value); + }); + + test("union matching accepts an option whose only missing key is optional", () => { + const options = [v.object({ a: v.string(), b: v.optional(v.string()) }), v.object({ a: v.string() })]; + // The first option matches despite `b` being absent: an optional key is not required. + const value = { a: "1" }; + expect(canonicalize(v.union(options), value)).toBe(value); + }); +}); diff --git a/tests/signing/fastWallet.test.ts b/tests/signing/fastWallet.test.ts index 70bce3f9..f6e8979b 100644 --- a/tests/signing/fastWallet.test.ts +++ b/tests/signing/fastWallet.test.ts @@ -246,6 +246,36 @@ describe("createFastLocalWallet() fallback", () => { }); }); +// --- WASM module validation ----------------------------------------------------- + +describe("createFastLocalWallet() WASM module validation", () => { + test("throws when the module cannot derive a public key from the private key", async () => { + // `pointFromScalar` returning null: the guard rejects instead of building a broken address. + _setEccLoaderForTests(() => + Promise.resolve({ + isPrivate: () => true, + pointFromScalar: () => null, + signRecoverable: () => { + throw new Error("unreachable"); + }, + }), + ); + await expect(createFastLocalWallet(PRIVATE_KEYS[0])).rejects.toThrow("Failed to derive the public key"); + + // A malformed (wrong-length) point hits the same guard. + _setEccLoaderForTests(() => + Promise.resolve({ + isPrivate: () => true, + pointFromScalar: () => new Uint8Array(33), + signRecoverable: () => { + throw new Error("unreachable"); + }, + }), + ); + await expect(createFastLocalWallet(PRIVATE_KEYS[0])).rejects.toThrow("Failed to derive the public key"); + }); +}); + // --- JSON-RPC wallets stay untouched -------------------------------------------- describe("createFastLocalWallet() alongside JSON-RPC wallets", () => { diff --git a/tests/signing/keccak.test.ts b/tests/signing/keccak.test.ts index 89b9ae2a..eb2bcfd7 100644 --- a/tests/signing/keccak.test.ts +++ b/tests/signing/keccak.test.ts @@ -21,6 +21,8 @@ */ import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import { rename } from "node:fs/promises"; +import { createRequire } from "node:module"; import { keccak_256 } from "@noble/hashes/sha3.js"; import { bytesToHex, hexToBytes } from "@noble/hashes/utils.js"; import { privateKeyToAccount } from "viem/accounts"; @@ -314,6 +316,22 @@ describe("keccak256() load semantics", () => { expect(createL1ActionHash({ action: ORDER_WITH_CLOID, nonce: NONCE })).toBe(ORDER_ACTION_HASH); }); + test("a hasher that throws during the self-check is never trusted", async () => { + _setKeccakLoaderForTests(() => + Promise.resolve({ + init() { + throw new Error("broken WASM build"); + }, + update() {}, + digest: (_outputType: "binary") => new Uint8Array(32), + }), + ); + await preloadWasmKeccak(); + + expect(bytesToHex(keccak256(new Uint8Array(0)))).toBe(KECCAK256_EMPTY); + expect(createL1ActionHash({ action: ORDER_WITH_CLOID, nonce: NONCE })).toBe(ORDER_ACTION_HASH); + }); + test("a partially linked module (missing createKeccak) falls back to noble", async () => { _setKeccakLoaderForTests(() => Promise.resolve(undefined)); await preloadWasmKeccak(); @@ -341,6 +359,40 @@ if (hashWasmAvailable) { expect(bytesToHex(keccak256(new Uint8Array(0)))).toBe(KECCAK256_EMPTY); }); }); + + describe("loadWasmKeccak() with an unreadable hash-wasm entry", () => { + test("the real loader routes a failed import to the noble fallback", async () => { + // Hiding the package's entry file makes the real loader's dynamic import reject — the one + // loader-failure path `_setKeccakLoaderForTests` cannot reach (it replaces the loader), and + // module mocking cannot simulate (a mocked specifier stays poisoned for the process — see + // the module header). hash-wasm ships CJS, so forcing a re-read only takes dropping the + // `require.cache` record; the file is restored in `finally` and the failed load leaves no + // cache entry behind, so the real module loads again on the next import. + const nodeRequire = createRequire(import.meta.url); + const entry = nodeRequire.resolve("hash-wasm"); + const hidden = `${entry}.hidden-by-test`; + delete nodeRequire.cache[entry]; + await rename(entry, hidden); + try { + // The failure must be real — if a future runtime serves the import from an immutable + // module cache instead, this test covers nothing and must say so. + const served = await import("hash-wasm").then( + () => true, + () => false, + ); + expect(served).toBe(false); + + _setKeccakLoaderForTests(undefined); // the real loader — its import now rejects + await preloadWasmKeccak(); + + expect(bytesToHex(keccak256(new Uint8Array(0)))).toBe(KECCAK256_EMPTY); + expect(createL1ActionHash({ action: ORDER_WITH_CLOID, nonce: NONCE })).toBe(ORDER_ACTION_HASH); + } finally { + await rename(hidden, entry); + delete nodeRequire.cache[entry]; // drop any failed-load record so the real module re-loads + } + }); + }); } // --- Cross-provider signature identity ------------------------------------------ diff --git a/tests/signing/mod.test.ts b/tests/signing/mod.test.ts index 566724a2..f0a76df0 100644 --- a/tests/signing/mod.test.ts +++ b/tests/signing/mod.test.ts @@ -4,20 +4,23 @@ * @module */ -import { describe, test } from "bun:test"; +import { describe, expect, test } from "bun:test"; import { assertEquals } from "@jsr/std__assert"; import { createWalletClient, custom } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { arbitrum } from "viem/chains"; import { + AbstractWalletError, createL1ActionHash, + getWalletAddress, getWalletChainId, signL1Action, signMultiSigL1, signMultiSigUserSigned, signUserSignedAction, } from "@bloxwap/hyperliquid/signing"; +import { signRawDigest, signTypedData } from "../../src/signing/_abstractWallet.ts"; // ============================================================ // Test Data @@ -372,3 +375,150 @@ describe("signing", () => { }); }); }); + +// ============================================================ +// Wallet error wrapping +// ============================================================ + +describe("wallet error wrapping (AbstractWalletError)", () => { + const TYPED_DATA_ARGS = { + domain: { + name: "HyperliquidSignTransaction", + version: "1", + chainId: 421614, + verifyingContract: "0x0000000000000000000000000000000000000000", + }, + types: { Agent: [{ name: "source", type: "string" }] }, + primaryType: "Agent", + message: { source: "a" }, + } as const; + const DIGEST = `0x${"11".repeat(32)}` as const; + const VALID_HEX_SIGNATURE = `0x${"1".repeat(64)}${"2".repeat(64)}1b` as const; + + /** Minimal JSON-RPC wallet stub; individual methods are overridden per test. */ + function jsonRpcWallet(overrides: { + signTypedData?: (_params: never) => Promise<`0x${string}`>; + getAddresses?: () => Promise<`0x${string}`[]>; + getChainId?: () => Promise; + }) { + return { + signTypedData: overrides.signTypedData ?? ((_params: never) => Promise.resolve(VALID_HEX_SIGNATURE)), + getAddresses: + overrides.getAddresses ?? (() => Promise.resolve(["0x1111111111111111111111111111111111111111" as const])), + getChainId: overrides.getChainId ?? (() => Promise.resolve(1337)), + }; + } + + /** Awaits a rejection and returns the caught error (fails if the call resolves). */ + async function caught(promise: Promise): Promise { + return promise.then( + () => { + throw new Error("expected the call to reject"); + }, + (error: unknown) => error as Error, + ); + } + + describe("signTypedData()", () => { + test("rejects a wallet signature that is not 65 bytes", async () => { + const wallet = jsonRpcWallet({ signTypedData: (_params: never) => Promise.resolve("0x1234" as const) }); + + const error = await caught(signTypedData({ wallet, ...TYPED_DATA_ARGS })); + expect(error).toBeInstanceOf(AbstractWalletError); + expect(error.message).toBe("Expected 65-byte signature (132 hex chars), got 6"); + }); + + test("rejects a wallet signature with an invalid recovery value", async () => { + const wallet = jsonRpcWallet({ + signTypedData: (_params: never) => Promise.resolve(`0x${"1".repeat(64)}${"2".repeat(64)}1d` as const), + }); + + const error = await caught(signTypedData({ wallet, ...TYPED_DATA_ARGS })); + expect(error).toBeInstanceOf(AbstractWalletError); + expect(error.message).toBe("Invalid signature recovery value: 29, expected 0/1 or 27/28"); + }); + + test("wraps a non-SDK wallet failure, keeping the cause", async () => { + const wallet = jsonRpcWallet({ + signTypedData: (_params: never) => Promise.reject(new Error("user rejected the request")), + }); + + const error = await caught(signTypedData({ wallet, ...TYPED_DATA_ARGS })); + expect(error).toBeInstanceOf(AbstractWalletError); + expect(error.message).toBe("Failed to sign the typed data using the wallet"); + expect(error.cause).toBeInstanceOf(Error); + }); + + test("rethrows an AbstractWalletError unchanged", async () => { + const failure = new AbstractWalletError("already an SDK error"); + const wallet = jsonRpcWallet({ signTypedData: (_params: never) => Promise.reject(failure) }); + + const error = await caught(signTypedData({ wallet, ...TYPED_DATA_ARGS })); + expect(error).toBe(failure); + }); + }); + + describe("signRawDigest()", () => { + /** Minimal viem-local wallet stub whose raw-digest `sign` fails as instructed. */ + function localWallet(failure: Error) { + return { + address: "0x1111111111111111111111111111111111111111" as const, + sign: (_args: { hash: `0x${string}` }) => Promise.reject(failure), + signTypedData: (_params: never) => Promise.resolve(VALID_HEX_SIGNATURE), + }; + } + + test("wraps a non-SDK wallet failure, keeping the cause", async () => { + const error = await caught( + signRawDigest({ wallet: localWallet(new Error("device disconnected")), digest: DIGEST }), + ); + expect(error).toBeInstanceOf(AbstractWalletError); + expect(error.message).toBe("Failed to sign the digest using the wallet"); + expect(error.cause).toBeInstanceOf(Error); + }); + + test("rethrows an AbstractWalletError unchanged", async () => { + const failure = new AbstractWalletError("already an SDK error"); + const error = await caught(signRawDigest({ wallet: localWallet(failure), digest: DIGEST })); + expect(error).toBe(failure); + }); + }); + + describe("getWalletAddress()", () => { + test("wraps a non-SDK lookup failure, keeping the cause", async () => { + const wallet = jsonRpcWallet({ getAddresses: () => Promise.reject(new Error("rpc down")) }); + + const error = await caught(getWalletAddress(wallet)); + expect(error).toBeInstanceOf(AbstractWalletError); + expect(error.message).toBe("Failed to get an address from the wallet"); + expect(error.cause).toBeInstanceOf(Error); + }); + + test("rethrows an AbstractWalletError unchanged", async () => { + const failure = new AbstractWalletError("already an SDK error"); + const wallet = jsonRpcWallet({ getAddresses: () => Promise.reject(failure) }); + + const error = await caught(getWalletAddress(wallet)); + expect(error).toBe(failure); + }); + }); + + describe("getWalletChainId()", () => { + test("wraps a non-SDK lookup failure, keeping the cause", async () => { + const wallet = jsonRpcWallet({ getChainId: () => Promise.reject(new Error("rpc down")) }); + + const error = await caught(getWalletChainId(wallet)); + expect(error).toBeInstanceOf(AbstractWalletError); + expect(error.message).toBe("Failed to get the chain ID from the wallet"); + expect(error.cause).toBeInstanceOf(Error); + }); + + test("rethrows an AbstractWalletError unchanged", async () => { + const failure = new AbstractWalletError("already an SDK error"); + const wallet = jsonRpcWallet({ getChainId: () => Promise.reject(failure) }); + + const error = await caught(getWalletChainId(wallet)); + expect(error).toBe(failure); + }); + }); +}); diff --git a/tests/transport/_polyfills.test.ts b/tests/transport/_polyfills.test.ts new file mode 100644 index 00000000..1ec79c20 --- /dev/null +++ b/tests/transport/_polyfills.test.ts @@ -0,0 +1,84 @@ +/** + * Tests for the platform shims: the native pass-throughs used on Node/Bun/browser, and the + * fallback implementations selected on platforms missing the API (mainly React Native). + * + * The fallbacks are chosen once at module evaluation, so covering them takes a fresh module + * instance: the platform globals are deleted and the module is re-imported through a + * cache-busting query string (`_polyfills.ts?…`), which Bun treats as a distinct module record. + * The globals are restored before the test ends, and the re-import never touches the instance + * the rest of the SDK already holds, so no other test observes the simulated platform. + * @module + */ + +import { describe, expect, test } from "bun:test"; +import { CustomEvent_, DOMException_, Promise_ } from "../../src/transport/_polyfills.ts"; + +describe("platform shims on a full platform", () => { + test("Promise_.withResolvers() delegates to the native Promise.withResolvers", async () => { + const { promise, resolve, reject } = Promise_.withResolvers(); + resolve(42); + expect(await promise).toBe(42); + expect(typeof reject).toBe("function"); + }); + + test("Promise_.withResolvers() exposes a working reject", async () => { + const { promise, reject } = Promise_.withResolvers(); + reject(new Error("nope")); + await expect(promise).rejects.toThrow("nope"); + }); + + test("DOMException_ and CustomEvent_ are the native classes", () => { + expect(DOMException_).toBe(globalThis.DOMException); + expect(CustomEvent_).toBe(globalThis.CustomEvent); + }); +}); + +describe("platform shims on a platform missing the APIs (React Native)", () => { + test("falls back to the bundled implementations", async () => { + const originalWithResolvers = Promise.withResolvers; + const originalDOMException = globalThis.DOMException; + const originalCustomEvent = globalThis.CustomEvent; + delete (Promise as unknown as Record).withResolvers; + delete (globalThis as Record).DOMException; + delete (globalThis as Record).CustomEvent; + try { + // A fresh module instance: the IIFEs re-run and now select the fallback shims. The + // cache-busting query goes through a variable so tsc does not try to resolve it. + const shimmedSpecifier = "../../src/transport/_polyfills.ts?react-native"; + const shimmed: typeof import("../../src/transport/_polyfills.ts") = await import(shimmedSpecifier); + expect(shimmed.Promise_).not.toBe(Promise_); + + // Promise.withResolvers fallback: manual resolver wiring. + const { promise, resolve, reject } = shimmed.Promise_.withResolvers(); + expect(promise).toBeInstanceOf(Promise); + resolve(7); + expect(await promise).toBe(7); + const rejected = shimmed.Promise_.withResolvers(); + rejected.reject(new Error("fallback")); + await expect(rejected.promise).rejects.toThrow("fallback"); + + // DOMException fallback: an Error subclass carrying the name. + const exception = new shimmed.DOMException_("boom", "AbortError"); + expect(exception).toBeInstanceOf(Error); + expect(exception.message).toBe("boom"); + expect(exception.name).toBe("AbortError"); + // Constructor defaults: empty message, "Error" name. + expect(new shimmed.DOMException_().name).toBe("Error"); + + // CustomEvent fallback: an Event subclass carrying the detail. + const event = new shimmed.CustomEvent_("ping", { detail: { a: 1 } }); + expect(event).toBeInstanceOf(Event); + expect(event.type).toBe("ping"); + expect(event.detail).toEqual({ a: 1 }); + expect(new shimmed.CustomEvent_("ping").detail).toBeNull(); + // The deprecated initCustomEvent is a no-op kept for interface parity (the union with the + // native class types it as requiring arguments, so call it through the fallback's shape). + (event as unknown as { initCustomEvent(): void }).initCustomEvent(); + } finally { + Promise.withResolvers = originalWithResolvers; + globalThis.DOMException = originalDOMException; + globalThis.CustomEvent = originalCustomEvent; + } + expect(typeof Promise.withResolvers).toBe("function"); + }); +}); diff --git a/tests/transport/_redact.test.ts b/tests/transport/_redact.test.ts index 646784ad..e83a8a1a 100644 --- a/tests/transport/_redact.test.ts +++ b/tests/transport/_redact.test.ts @@ -166,6 +166,36 @@ describe("redactSignature", () => { assert(serialized.includes("[Circular]")); }); + test("toJSON returning its own owner closes a cycle immediately", () => { + // The serialized form IS the object being walked: without the owner on the path before + // `toJSON` runs, the walk would descend into the same node forever. + const payload = { + signature: SIGNATURE, + wrapper: { + toJSON() { + return this; + }, + }, + }; + + const redacted = redactSignature(payload) as Record; + assertEquals(redacted.wrapper, "[Circular]"); + const serialized = JSON.stringify(redacted); // must not throw or hang + assert(serialized.includes('"signature":"0x"')); + }); + + test("toJSON returning an ancestor closes a cycle immediately", () => { + const root: Record = { name: "root" }; + root.child = { + toJSON() { + return root; + }, + }; + + const redacted = redactSignature({ payload: root }) as Record; + assertEquals((redacted.payload as Record).child, "[Circular]"); + }); + test("toJSON plus an own signature key: the serialized form wins", () => { const real = { r: `0x${"9".repeat(64)}`, s: `0x${"8".repeat(64)}`, v: 27 }; const payload = { diff --git a/tests/utils/format.test.ts b/tests/utils/format.test.ts index 8d2c24f2..5a5a80d5 100644 --- a/tests/utils/format.test.ts +++ b/tests/utils/format.test.ts @@ -8,6 +8,7 @@ import { describe, test } from "bun:test"; import { assertEquals, assertThrows } from "@jsr/std__assert"; import { Decimal } from "decimal.js"; import { FormatError, floatToWire, formatPrice, formatSize } from "@bloxwap/hyperliquid/utils"; +import { type DecimalParts, toDecimalPlacesHalfEven, toSignificantDigitsHalfEven } from "../../src/utils/_decimal.ts"; // ============================================================ // Test Data @@ -638,3 +639,53 @@ describe("floatToWire", () => { }); }); }); + +describe("half-even rounding internals (_decimal.ts)", () => { + const parts = (digits: string, exp: number): DecimalParts => ({ sign: 1, digits, exp }); + + test("toDecimalPlacesHalfEven rounds up to one unit only past half a unit", () => { + // 0.6 at 0 decimal places → 1 (past half a unit). + assertEquals(toDecimalPlacesHalfEven(parts("6", 0), 0), parts("1", 1)); + // 0.5 at 0 decimal places → 0: an exact half ties to the implicit leading 0, which is even. + assertEquals(toDecimalPlacesHalfEven(parts("5", 0), 0), parts("", 0)); + // Everything below the place: 0.009 at 1 decimal place → 0. + assertEquals(toDecimalPlacesHalfEven(parts("9", -2), 1), parts("", -2)); + }); + + test("toDecimalPlacesHalfEven carries a run of nines out front", () => { + // 0.99 at 1 decimal place → 1: the carry reaches the front and comes out as 0.1 × 10^(exp+1). + assertEquals(toDecimalPlacesHalfEven(parts("99", 0), 1), parts("1", 1)); + }); + + test("toSignificantDigitsHalfEven carries a run of nines out front", () => { + // 9.99 to 2 significant digits → 10. + assertEquals(toSignificantDigitsHalfEven(parts("999", 1), 2), parts("1", 2)); + }); + + test("a value past half a unit at the 8th decimal trips the floatToWire guard", () => { + // 5e-9's exact expansion exceeds half a unit at the 8th place, so the exact path rounds it up + // to 0.00000001 — a 5e-9 change, which the 1e-12 guard rejects. + assertThrows(() => floatToWire(0.000000005), FormatError); + }); +}); + +describe("floatToWire exact path on subnormal doubles", () => { + test("subnormals decompose through the subnormal branch and round to zero", () => { + // The fast path never routes a subnormal to `exactDecimalParts` (every subnormal's + // `toFixed(9)` is "0.000000000"), leaving its subnormal-mantissa branch unreachable from the + // public API. Force the exact path by making the tie predicate fire; the exact expansion of + // every subnormal is far below half a unit at the 8th decimal, so the wire form is still "0" — + // the same answer the fast path gives. + const original = Number.prototype.toFixed; + Number.prototype.toFixed = function (this: number, digits?: number): string { + const rendered = original.call(this, digits); + return digits === 9 ? `${rendered}5` : rendered; + }; + try { + assertEquals(floatToWire(5e-324), "0"); // Number.MIN_VALUE: the smallest subnormal + assertEquals(floatToWire(1e-310), "0"); + } finally { + Number.prototype.toFixed = original; + } + }); +});