diff --git a/apps/web/package.json b/apps/web/package.json index 660242c..51f4a7c 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -8,7 +8,7 @@ "dev": "vite", "build": "vite build", "preview": "vite preview", - "test": "node --import tsx --test src/demo/trace-health-check.test.ts src/lib/config.test.ts src/dashboard/fixture-health-check.test.ts src/lib/round-status.test.ts src/components/dashboard/RoundStatusCard.test.tsx src/hooks/useDashboardData.test.ts", + "test": "node --import tsx --test src/demo/trace-health-check.test.ts src/lib/config.test.ts src/lib/chain.test.ts src/dashboard/fixture-health-check.test.ts src/lib/round-status.test.ts src/components/dashboard/RoundStatusCard.test.tsx src/hooks/useDashboardData.test.ts", "typecheck": "tsc --noEmit -p tsconfig.json" }, "dependencies": { diff --git a/apps/web/src/lib/amount.ts b/apps/web/src/lib/amount.ts new file mode 100644 index 0000000..df2e218 --- /dev/null +++ b/apps/web/src/lib/amount.ts @@ -0,0 +1,15 @@ +// Copyright (c) 2026 Sub Rosa contributors + +export function formatEscrowAmount(value: bigint, tokenLabel: string): string { + const SCALE = 10_000_000n; + const QUANTA = 10_000n; + + const whole = value / SCALE; + const remainder = value % SCALE; + const fractional = (remainder * QUANTA + (SCALE / 2n)) / SCALE; + + const normalizedWhole = fractional >= QUANTA ? whole + 1n : whole; + const normalizedFraction = fractional >= QUANTA ? 0n : fractional; + + return `${normalizedWhole}.${normalizedFraction.toString().padStart(4, "0")} ${tokenLabel}`; +} diff --git a/apps/web/src/lib/chain.test.ts b/apps/web/src/lib/chain.test.ts new file mode 100644 index 0000000..ceb3817 --- /dev/null +++ b/apps/web/src/lib/chain.test.ts @@ -0,0 +1,15 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; + +import { formatEscrowAmount } from "./amount"; + +test("formatEscrowAmount preserves exact 4-decimal display above Number.MAX_SAFE_INTEGER", () => { + assert.equal(formatEscrowAmount(9007199254740992n, "token"), "900719925.4741 token"); + assert.equal(formatEscrowAmount(9007199254741500n, "token"), "900719925.4742 token"); +}); + +test("formatEscrowAmount keeps four-decimal display for standard escrow values", () => { + assert.equal(formatEscrowAmount(10_000_000n, "token"), "1.0000 token"); + assert.equal(formatEscrowAmount(1_000_000n, "token"), "0.1000 token"); + assert.equal(formatEscrowAmount(123_456_789n, "token"), "12.3457 token"); +}); diff --git a/apps/web/src/lib/chain.ts b/apps/web/src/lib/chain.ts index c9dc2e7..9122896 100644 --- a/apps/web/src/lib/chain.ts +++ b/apps/web/src/lib/chain.ts @@ -8,6 +8,8 @@ import { import { RoundContract } from "@sub-rosa/sdk"; import { useMemo } from "react"; +import { formatEscrowAmount } from "./amount"; + export const LOGO_SRC = "/sub-rosa-logo.png"; export const RPC_URL = import.meta.env.VITE_RPC_URL ?? "https://soroban-testnet.stellar.org"; export const NETWORK = @@ -70,7 +72,7 @@ export function toDemoEscrowAmount(value: number): bigint { } export function formatDemoAmount(value: bigint): string { - return `${(Number(value) / 10_000_000).toFixed(4)} ${ESCROW_TOKEN_LABEL}`; + return formatEscrowAmount(value, ESCROW_TOKEN_LABEL); } export async function sha256Bytes(text: string): Promise {