Skip to content

Skip an unreadable history row instead of valuing or throwing on it - #99

Merged
portdeveloper merged 1 commit into
portdeveloper:mainfrom
BeeHiveTeam:fix/history-malformed-rows
Sep 19, 2026
Merged

portdeveloper merged 1 commit into
portdeveloper:mainfrom
BeeHiveTeam:fix/history-malformed-rows

Conversation

@BeeHiveTeam

Copy link
Copy Markdown
Contributor

Closes #98

normalizeHistoryTransaction converted every row with BigInt(tx.value ?? 0). A row the explorer sends without a usable amount took one of two wrong paths, and only one of them was visible.

It threw. The conversion sits inside getHistory's .map(), so one malformed row aborted the entire read — including the valid transactions the other endpoint had already returned. Reproduced on 3e77d03 with the production getHistory and an injected fetch: transactions returns one valid row, internal-transactions returns value: "not-a-number", and the call throws Cannot convert not-a-number to a BigInt instead of returning the one good entry.

Or it invented an amount. BigInt is generous in ways an explorer row is not. Measured on the same commit:

tx.value before after
"42", "0x2a", " 42 ", 42, 42n amount amount, unchanged
"+5", "0b101", "0o17", "\n42\n" amount amount, unchanged — BigInt's own grammar is left alone
missing, null, "", " " 0n → printed in +0.0 MON skipped
false / true 0n / 1n skipped
[] / ["5"] 0n / 5n skipped
{ toString: () => "7" } 7n skipped
"not-a-number", "1.5", "1e3", {}, ["5","6"] throws, whole read dies skipped
1.5, NaN, Infinity throws skipped
"-5", -5, -5n accepted → in +-0.000000000000000005 MON skipped

A fabricated +0.0 MON line is worse than a missing one: it reads as a real zero-value transfer rather than as a row the client could not parse.

What changed

One helper, parseHistoryAmount. It considers only a string, a number or a bigint, refuses an empty or blank string, converts inside a try, and refuses a negative result. BigInt's own numeric grammar is left alone — hex, binary and octal literals, a leading +, surrounding whitespace all still read. Those are whole non-negative integers written oddly, not rows without an amount, and 0x2a is a shape the suite already relied on.

A row whose amount cannot be read returns null, which is the rejection normalizeHistoryTransaction already uses for a missing hash. getHistory's existing .filter(Boolean) isolates it, so no second mechanism appears and the row boundary is where it always was.

Ordering, the result cap and the both-endpoints-failed AggregateError are untouched.

Tests

381, up from 350. 15 consecutive runs, no failure.

  • the shape table above, asserted in both directions — a shape that survives and a shape that is skipped are statements about the same guard, and listing them apart invites one half to drift;
  • a malformed row in the internal endpoint, and separately in the transactions endpoint, each leaving the other endpoint's entry intact;
  • a malformed row mixed with valid rows inside one endpoint;
  • a row with no value at all, skipped rather than reported as zero;
  • malformed rows interleaved with valid ones under a limit, pinning that skipped rows neither disturb newest-first ordering nor consume the cap.

Mutation-checked, each guard reverted on its own with a no-op edit as a control:

reverted result
type check 5 red — true, false, [], ["5"], toString
empty-string refusal 2 red
try/catch 10 red, including both endpoint-isolation tests
negative refusal 3 red
null row rejection 24 red
control: no-op edit 44 pass

Two limits worth naming

A skipped row is invisible. Nothing counts or reports what was dropped, so a page of rows that are all unreadable reaches the REPL as "no recent transactions found" — the same ambiguity as the fabricated zero, seen from the other side. That follows from skipping, which is what the issue asks for; if you want a "n rows skipped" line I'd rather do it as its own issue than widen this one.

Precision is lost before this code runs. An unquoted JSON number above 2^53 is already rounded by JSON.parse: 9007199254740993 arrives as ...992, and the guard faithfully converts the rounded value. Quoted values, which is how explorers in this family send value, are unaffected. Fixing it would mean parsing the response body differently, well outside history normalization.

Note on scope

Skipping absent and negative values changes what /history prints, beyond repairing the crash. Both were confirmed in the claim thread before I wrote any code: absent values skipped rather than zeroed, negatives treated as malformed. The change stays inside history normalization and its tests.

normalizeHistoryTransaction converted every row with BigInt(tx.value ?? 0). A row
the explorer sent without a usable amount took one of two wrong paths: it threw,
which aborted the whole /history read including the other endpoint's valid
transactions, or it converted to a number the chain never saw.

Both paths are now one rejection. parseHistoryAmount considers only a string, a
number or a bigint, refuses an empty one, converts inside a try, and refuses a
negative result. A row whose amount cannot be read returns null, which is the
rejection normalizeHistoryTransaction already uses for a missing hash, so
getHistory's existing filter isolates the row without a second mechanism.

The quiet half matters as much as the crash: "" , [] and false reached BigInt as
0, true as 1, ["5"] as 5, and an object with a toString as whatever it said. Each
printed as a real transfer in /history. Absent now stays absent.

Ordering, the result cap and the both-endpoints-failed error are unchanged.

@portdeveloper portdeveloper left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm, thanks

@portdeveloper
portdeveloper merged commit 0d2c880 into portdeveloper:main Sep 19, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Keep valid history entries when an explorer row has a malformed value

2 participants