From 96eb4ece2ba09b5a379fbe8e10d961a86dd09023 Mon Sep 17 00:00:00 2001 From: Teslim Ibisomi Date: Thu, 30 Jul 2026 11:54:31 +0100 Subject: [PATCH] fix: reject unquoted numeric JSON amounts to prevent silent rounding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit JSON.parse() applies IEEE-754 rounding before validation, silently altering payment values like 99999999999.9999999 → 100000000000. The parseJSON() function now explicitly rejects numeric amount values with a row-level error directing users to quote amounts as strings. Fixes #714 --- lib/stellar/parser.ts | 8 ++++++++ tests/parser.test.ts | 29 +++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/lib/stellar/parser.ts b/lib/stellar/parser.ts index be693e7..9fa854f 100644 --- a/lib/stellar/parser.ts +++ b/lib/stellar/parser.ts @@ -39,6 +39,14 @@ export function parseJSON(content: string): PaymentInstruction[] { } return rawInstructions.map((item: Record, index: number) => { + // #714: Reject numeric JSON amounts — JSON.parse applies IEEE-754 rounding + // before validation, silently altering values like 99999999999.9999999 → 1e11 + if (typeof item.amount === 'number') { + throw new Error( + `Row ${index + 1}: amount must be a quoted string, not a number (received ${item.amount})` + ); + } + const instruction: PaymentInstruction = { address: sanitizeValue(String(item.address ?? '')), amount: sanitizeValue(String(item.amount ?? '')), diff --git a/tests/parser.test.ts b/tests/parser.test.ts index a7499f0..f75e795 100644 --- a/tests/parser.test.ts +++ b/tests/parser.test.ts @@ -81,6 +81,35 @@ describe('JSON Parser', () => { const result = parseJSON(json); expect(result[0].amount).toBe('123.456789'); }); + + // #714: Reject numeric JSON amounts to prevent silent IEEE-754 rounding + test('rejects unquoted numeric amount that would round silently', () => { + // Using raw JSON string so the amount is a literal number, not a string + const json = `[{"address":"GBBD47UZM2HN7D7XZIZVG4KVAUC36THN5BES6RMNNOK5TUNXAUCVMAKER","amount":99999999999.9999999,"asset":"XLM"}]`; + expect(() => parseJSON(json)).toThrow(/amount must be a quoted string/); + }); + + test('rejects unquoted small numeric amount that becomes scientific notation', () => { + const json = `[{"address":"GBBD47UZM2HN7D7XZIZVG4KVAUC36THN5BES6RMNNOK5TUNXAUCVMAKER","amount":0.0000001,"asset":"XLM"}]`; + expect(() => parseJSON(json)).toThrow(/amount must be a quoted string/); + }); + + test('rejects unquoted integer amount', () => { + const json = `[{"address":"GBBD47UZM2HN7D7XZIZVG4KVAUC36THN5BES6RMNNOK5TUNXAUCVMAKER","amount":100,"asset":"XLM"}]`; + expect(() => parseJSON(json)).toThrow(/amount must be a quoted string/); + }); + + test('accepts quoted string amount for large value', () => { + const json = `[{"address":"GBBD47UZM2HN7D7XZIZVG4KVAUC36THN5BES6RMNNOK5TUNXAUCVMAKER","amount":"99999999999.9999999","asset":"XLM"}]`; + const result = parseJSON(json); + expect(result[0].amount).toBe('99999999999.9999999'); + }); + + test('accepts quoted string amount for tiny value', () => { + const json = `[{"address":"GBBD47UZM2HN7D7XZIZVG4KVAUC36THN5BES6RMNNOK5TUNXAUCVMAKER","amount":"0.0000001","asset":"XLM"}]`; + const result = parseJSON(json); + expect(result[0].amount).toBe('0.0000001'); + }); }); describe('CSV Parser', () => {