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. diff --git a/src/commands/sessions.ts b/src/commands/sessions.ts index b2fbc10..98731f1 100644 --- a/src/commands/sessions.ts +++ b/src/commands/sessions.ts @@ -654,6 +654,8 @@ 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; const remaining = record.deposit > spent ? record.deposit - spent : 0n; @@ -669,8 +671,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), 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) { 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"); + }); +});