From 8f7252761369c58a12c36ba2f80f1a2cd7ed7716 Mon Sep 17 00:00:00 2001 From: KutluhanETH Date: Fri, 18 Sep 2026 23:57:11 +0000 Subject: [PATCH 1/5] fix(utils): format negative bigint token and credit balances Pad absolute fractional digits first, then apply the sign, so values like -5n with 6 decimals become -0.000005 instead of 0.0000-5. Part of #88. --- src/shared/utils.ts | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/shared/utils.ts b/src/shared/utils.ts index 48c0eb2..92b4281 100644 --- a/src/shared/utils.ts +++ b/src/shared/utils.ts @@ -38,20 +38,29 @@ export function formatMicroUnits(value: string) { } export function formatTokenUnits(value: bigint, decimals: number) { + const negative = value < 0n; + const abs = negative ? -value : value; const divisor = 10n ** BigInt(decimals); - const whole = value / divisor; - const fractional = value % divisor; - if (decimals === 0) return whole.toString(); - if (fractional === 0n) return `${whole}.${"0".repeat(decimals)}`; - return `${whole}.${fractional.toString().padStart(decimals, "0")}`; + const whole = abs / divisor; + const fractional = abs % divisor; + const formatted = + decimals === 0 + ? whole.toString() + : fractional === 0n + ? `${whole}.${"0".repeat(decimals)}` + : `${whole}.${fractional.toString().padStart(decimals, "0")}`; + return negative ? `-${formatted}` : formatted; } export function formatCreditBalance(rawBalance: bigint) { + const negative = rawBalance < 0n; + const abs = negative ? -rawBalance : rawBalance; const divisor = 10_000n; - const whole = rawBalance / divisor; - const fractional = rawBalance % divisor; - if (fractional === 0n) return whole.toString(); - return `${whole}.${fractional.toString().padStart(4, "0")}`; + const whole = abs / divisor; + const fractional = abs % divisor; + const formatted = + fractional === 0n ? whole.toString() : `${whole}.${fractional.toString().padStart(4, "0")}`; + return negative ? `-${formatted}` : formatted; } export function isChannelId(value: string) { From e9ee21d0f34c46248c78e86c27bfd0dfc86259fe Mon Sep 17 00:00:00 2001 From: KutluhanETH Date: Fri, 18 Sep 2026 23:57:11 +0000 Subject: [PATCH 2/5] test(utils): cover negative formatTokenUnits and formatCreditBalance Lock the sign-outside-fractional-digits behavior for zero, fractional, and whole negative bigint inputs. Closes #88. --- test/format-units.test.ts | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 test/format-units.test.ts diff --git a/test/format-units.test.ts b/test/format-units.test.ts new file mode 100644 index 0000000..c99548f --- /dev/null +++ b/test/format-units.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from "vitest"; + +import { formatCreditBalance, formatTokenUnits } from "../src/shared/utils.js"; + +describe("formatTokenUnits", () => { + it("formats non-negative values", () => { + expect(formatTokenUnits(0n, 6)).toBe("0.000000"); + expect(formatTokenUnits(1_000_000n, 6)).toBe("1.000000"); + expect(formatTokenUnits(1_234_567n, 6)).toBe("1.234567"); + expect(formatTokenUnits(42n, 0)).toBe("42"); + }); + + it("keeps the sign outside padded fractional digits for negative values", () => { + expect(formatTokenUnits(-5n, 6)).toBe("-0.000005"); + expect(formatTokenUnits(-1_234_567n, 6)).toBe("-1.234567"); + expect(formatTokenUnits(-42n, 0)).toBe("-42"); + }); +}); + +describe("formatCreditBalance", () => { + it("formats non-negative credit balances", () => { + expect(formatCreditBalance(0n)).toBe("0"); + expect(formatCreditBalance(10_000n)).toBe("1"); + expect(formatCreditBalance(12_345n)).toBe("1.2345"); + }); + + it("keeps the sign outside padded fractional digits for negative balances", () => { + expect(formatCreditBalance(-5n)).toBe("-0.0005"); + expect(formatCreditBalance(-12_345n)).toBe("-1.2345"); + }); +}); From e7b56538ded23ce6b9751082ea9839800e4ad6c8 Mon Sep 17 00:00:00 2001 From: KutluhanETH Date: Fri, 18 Sep 2026 23:57:11 +0000 Subject: [PATCH 3/5] fix(sessions): cache nowSeconds for remaining_secs Avoid a clock tick between the grace comparison and subtraction that could yield a negative remaining_secs. Fixes #87. --- src/commands/sessions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/sessions.ts b/src/commands/sessions.ts index b2fbc10..c257e3a 100644 --- a/src/commands/sessions.ts +++ b/src/commands/sessions.ts @@ -654,6 +654,7 @@ function closeTarget(record: ChannelRecord) { } function sessionItem(record: ChannelRecord) { + const now = nowSeconds(); const spent = record.accepted_cumulative > 0n ? record.accepted_cumulative : record.cumulative_amount; const remaining = record.deposit > spent ? record.deposit - spent : 0n; @@ -669,8 +670,7 @@ function sessionItem(record: ChannelRecord) { status, ...(status === "closing" || status === "finalizable" ? { - remaining_secs: - record.grace_ready_at > nowSeconds() ? record.grace_ready_at - nowSeconds() : 0, + remaining_secs: record.grace_ready_at > now ? record.grace_ready_at - now : 0, } : {}), created_at: formatUnixTimestamp(record.created_at), From 016cf7e0486bf8179ad45fe1120e57c0af39060d Mon Sep 17 00:00:00 2001 From: KutluhanETH Date: Fri, 18 Sep 2026 23:57:11 +0000 Subject: [PATCH 4/5] chore(changelog): note format and session remaining_secs fixes Record patch entries for #87 and #88. --- .changelog/negative-format-and-session-now.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changelog/negative-format-and-session-now.md diff --git a/.changelog/negative-format-and-session-now.md b/.changelog/negative-format-and-session-now.md new file mode 100644 index 0000000..0c8ab72 --- /dev/null +++ b/.changelog/negative-format-and-session-now.md @@ -0,0 +1,5 @@ +--- +wallet-cli: patch +--- + +Format negative bigint token and credit balances without embedding the minus sign inside padded fractional digits. Cache `nowSeconds()` in session list remaining-seconds so a clock tick cannot produce a negative grace countdown. From d857d7c9613749a76857a580c053a3ad644e23cb Mon Sep 17 00:00:00 2001 From: KutluhanETH Date: Fri, 18 Sep 2026 23:57:11 +0000 Subject: [PATCH 5/5] docs(sessions): note why remaining_secs caches nowSeconds Document the race the previous fix removes so it is not reintroduced. --- src/commands/sessions.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/commands/sessions.ts b/src/commands/sessions.ts index c257e3a..98731f1 100644 --- a/src/commands/sessions.ts +++ b/src/commands/sessions.ts @@ -654,6 +654,7 @@ function closeTarget(record: ChannelRecord) { } function sessionItem(record: ChannelRecord) { + // Single clock read: comparing and subtracting separate nowSeconds() calls can go negative. const now = nowSeconds(); const spent = record.accepted_cumulative > 0n ? record.accepted_cumulative : record.cumulative_amount;