From c1a72bd13156095bc99e41b010047618c8450d5c Mon Sep 17 00:00:00 2001 From: Nexory Date: Wed, 10 Jun 2026 11:26:30 +0200 Subject: [PATCH] fix(utils): allow integer prices written with trailing decimal zeros in formatPrice `formatPrice` documents that "integer prices are always allowed regardless of significant figures", and bypasses the 5-sig-fig truncation when the raw input matches `/^-?\d+$/`. That check only runs on the raw input, so a price that is integer-valued but written with a trailing decimal (e.g. "100001.0") fails the regex, falls through to `toFixedTruncate` (which strips the ".0", yielding "100001"), and is then truncated by the significant-figure step to "100000". Result: the same integer price produces different output depending on formatting: formatPrice("100001", 0) === "100001" // ok formatPrice("100001.0", 0) === "100000" // wrong, off by 1 formatPrice("123456.0", 0) === "123450" // wrong formatPrice("-123456.0",0) === "-123450" // wrong A float-derived price (e.g. `n.toFixed(1)` -> "100001.0") therefore submits an order at a different price than intended. Re-check the integer-bypass after decimal truncation, so a value that became an integer is treated the same as a raw integer. A value that truncated all the way to zero is excluded so the existing "too small" RangeError still fires. Adds regression cases to tests/utils/format.test.ts. The full formatPrice suite (including the reference-asset validation) passes; deno fmt and deno lint are clean. --- src/utils/_format.ts | 8 ++++++++ tests/utils/format.test.ts | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/src/utils/_format.ts b/src/utils/_format.ts index 1d088c7e..e382fe83 100644 --- a/src/utils/_format.ts +++ b/src/utils/_format.ts @@ -29,6 +29,14 @@ export function formatPrice(price: string | number, szDecimals: number, type: "p const maxDecimals = Math.max((type === "perp" ? 6 : 8) - szDecimals, 0); price = StringMath.toFixedTruncate(price, maxDecimals); + // Integer prices are always allowed, including values that become integers + // after decimal truncation (e.g. "100001.0" -> "100001"). Without this + // re-check the significant-figure step below would truncate them (-> "100000"). + // A value that truncated all the way to zero is left for the zero check below. + if (/^-?\d+$/.test(price) && !/^-?0+$/.test(price)) { + return formatDecimalString(price); + } + // Apply sig figs limit: max 5 significant figures price = StringMath.toPrecisionTruncate(price, 5); diff --git a/tests/utils/format.test.ts b/tests/utils/format.test.ts index 9a6d939b..975bc442 100644 --- a/tests/utils/format.test.ts +++ b/tests/utils/format.test.ts @@ -32,6 +32,14 @@ Deno.test("formatPrice", async (t) => { assertEquals(formatPrice("1234567", 0), "1234567"); }); + await t.step("integer value with trailing decimal zeros bypasses limit", () => { + assertEquals(formatPrice("100001.0", 0), "100001"); + assertEquals(formatPrice("123456.0", 0), "123456"); + assertEquals(formatPrice("100001.00", 0), "100001"); + assertEquals(formatPrice("-123456.0", 0), "-123456"); + assertEquals(formatPrice("100001.0", 0, "spot"), "100001"); + }); + await t.step("truncates to 5 sig figs", () => { assertEquals(formatPrice("12345.6", 0), "12345"); assertEquals(formatPrice("0.00123456", 0), "0.001234");