Skip to content

[2.x] Add formatters for decoded nodes - #1215

Merged
lorisleiva merged 1 commit into
mainfrom
10-07-add_formatters_for_decoded_nodes
Oct 9, 2026
Merged

lorisleiva merged 1 commit into
mainfrom
10-07-add_formatters_for_decoded_nodes

Conversation

@lorisleiva

@lorisleiva lorisleiva commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

This PR adds formatters that turn decoded nodes into human-readable strings, using the presentation metadata of the nodes that decoded them: units, display nodes, scales and ticks.

// u64 with amountNumberDisplayNode({ decimals: integerValueNode('6'), unit: stringValueNode('USDC') })
formatInteger(decoded); // "1.5 USDC"
formatFixedPoint(decoded); // "123.45%"
formatDateTime(decoded); // "2024-01-01T00:00:00.5Z"
formatDuration(decoded); // "49:00:00"
  • Formatters: formatInteger, formatFloat, formatFixedPoint, formatDateTime, formatDuration and formatString each take the decoded node of their kind.
  • Exact: amounts and fixed points, including 128-bit and binary (Q-format) ones, use the fixed points of @solana/codecs. Date-times are exact ISO 8601 UTC strings for any year, computed with bigint calendar maths.
  • Units: a display node's unit wins over the type's unit, which is the fallback when the former is absent or can't be resolved. Units follow their value after a space, except %, ‰ and °.
  • Amounts with unresolved decimals: formatInteger returns null instead of a wrongly scaled amount, so callers can show the raw value.
  • Options: formatUnit places units, e.g. "USD 40.5"; numberFormat formats for a locale, its options deciding the digits shown, without ever going through a JavaScript float; resolveInjectedValue resolves injected decimals and units from their path through the decoded node.

@changeset-bot

changeset-bot Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: e128735

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@lorisleiva lorisleiva changed the title Add formatters for decoded nodes [2.x] Add formatters for decoded nodes Oct 7, 2026
@lorisleiva

Copy link
Copy Markdown
Member Author

@trevor-cortex

@trevor-cortex trevor-cortex left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Summary

Adds formatInteger, formatFloat, formatFixedPoint, formatDateTime, formatDuration and formatString to @codama/dynamic-codecs. Each takes the decoded node of its kind and renders it using the presentation metadata of the node that decoded it: unit, display (amount / unit / string display nodes), scale/base, and ticksPerSecond. Scaled numbers go through Kit's @solana/fixed-points (rawDecimalFixedPoint / rawBinaryFixedPoint) so 128-bit and Q-format values are exact; date-times use bigint calendar maths (Hinnant's civil_from_days) so any i64 timestamp formats correctly. Options cover unit placement, Intl.NumberFormat and a resolveInjectedValue callback for display-node inputs.

The core logic is solid. I walked through getCivilDate against the reference algorithm (the floorDiv for era and truncating division for the rest is correct since dayOfEra is always non-negative), floorDiv's sign handling, the nanosecond rounding path (half signed to round away from zero, then truncating division — correct), and the unit precedence (display wins, type unit is the fallback when the display unit is absent, injected-and-unresolved, or resolves to ''), which matches the spec text on amountNumberDisplayNode / unitNumberDisplayNode. Tests are thorough on the number and date paths, including the i64 extremes and a cross-check against Date.prototype.toISOString within JS's range.

Things to address / consider

  • Missing changeset. CONTRIBUTING.md asks for one for any user-facing change (npx changeset add --empty), and this adds new public exports to @codama/dynamic-codecs. No .changeset/*.md is in the diff.
  • formatString slices UTF-16 code units while the spec says indices apply to the "decoded character sequence". See inline — if that's meant to be code points, non-BMP characters (emoji, some CJK) will be split.
  • Unbounded decimals from the resolver — see inline on resolveIntegerInput. Low risk, but a cheap guard.
  • Fractional ticksPerSecond. Both formatDateTime and formatDuration return null unless ticksPerSecond is a positive integer. If the spec allows e.g. 1/60 for minute-resolution timestamps, that's a legit value we'd refuse to format. If the validators already restrict it to positive integers, ignore this — just want to make sure the null isn't hiding a supported case.

Notes for subsequent reviewers

  • @solana/codecs@^8.4.0 already re-exports @solana/fixed-points, so the new imports (rawDecimalFixedPoint, formatDecimalFixedPoint, Signedness, …) don't need a dependency bump.
  • The resolveInjectedValue option is intentionally a plain callback rather than reusing the package's ProvidedScope machinery from getValueNodeVisitor — display-node inputs like decimals typically come from external providers (e.g. a mint account), not from the instruction scope. Worth keeping in mind if someone later wants a default resolver built on scope.
  • Test coverage gaps that are minor but would be nice: formatInteger with an amount display on shortU16 (exercises the special-case in getIntegerLayout), a 128-bit binary fixed point, a resolved (not just unresolved) injected unit on formatFixedPoint, and formatDuration with a non-power-of-10 ticksPerSecond.

Comment thread packages/dynamic-codecs/src/format.ts Outdated
Comment thread packages/dynamic-codecs/src/format.ts
Comment thread packages/dynamic-codecs/src/format.ts Outdated
Comment thread packages/dynamic-codecs/src/format.ts
Comment thread packages/dynamic-codecs/src/format.ts Outdated
@lorisleiva
lorisleiva force-pushed the 10-07-add_formatters_for_decoded_nodes branch from 77f82ec to 068e49e Compare October 7, 2026 15:02

@trevor-cortex trevor-cortex left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Re-review

Re-fetched the diff. Everything actionable from my first pass has been addressed:

  • formatString now slices by Unicode code point (Array.from(...).slice(...).join('')), with tests for both a sliceEnd and a sliceStart landing after an emoji. README and JSDoc document the semantics.
  • DecimalFixedPoint / BinaryFixedPoint are imported directly instead of Parameters<typeof …>[0].
  • numberFormat docs (JSDoc + README) now say its options decide the digits shown, with the 3-fraction-digit default called out, and that only amounts and fixed points bypass JS floats. There's a maximumFractionDigits: 2 test on an amount pinning this.
  • README documents the ticksPerSecond → null behaviour.
  • Coverage gaps filled: shortU16 amount (checked dc0b → 1500), 128-bit binary fixed point (verified the (2^128 − 1) / 2^64 expansion by hand — correct), resolved injected unit on formatFixedPoint, and a locale test on fixed points.

Still open (non-blocking)

  • Changeset. There's still no .changeset/*.md in the diff. If it's being added at the top of the Graphite stack along with the rest of the dynamic-codecs work, ignore this — just flagging so it doesn't fall through the cracks before the stack merges.
  • decimals upper bound in resolveIntegerInput is still MAX_SAFE_INTEGER. Purely defensive; happy to leave it as-is if you'd rather trust the resolver.

Approving — the implementation is correct and well tested.

(Apologies for the delay — GitHub's API was returning 500s on my earlier attempts to post this.)

@lorisleiva
lorisleiva force-pushed the 10-07-decode_codama_nodes_into_decoded_node_trees branch from 2e1a5ca to ce304e7 Compare October 9, 2026 09:08
@lorisleiva
lorisleiva force-pushed the 10-07-add_formatters_for_decoded_nodes branch from 068e49e to 7256a9f Compare October 9, 2026 09:08
@lorisleiva
lorisleiva force-pushed the 10-07-decode_codama_nodes_into_decoded_node_trees branch from ce304e7 to 361b8ef Compare October 9, 2026 09:11
@lorisleiva
lorisleiva force-pushed the 10-07-add_formatters_for_decoded_nodes branch 2 times, most recently from 46c664f to cbd7116 Compare October 9, 2026 09:12
@lorisleiva
lorisleiva force-pushed the 10-07-decode_codama_nodes_into_decoded_node_trees branch 2 times, most recently from 9b0fbca to f143ca4 Compare October 9, 2026 09:13
@lorisleiva
lorisleiva force-pushed the 10-07-add_formatters_for_decoded_nodes branch from cbd7116 to 62035f2 Compare October 9, 2026 09:13
@lorisleiva
lorisleiva force-pushed the 10-07-decode_codama_nodes_into_decoded_node_trees branch from f143ca4 to fa8eac0 Compare October 9, 2026 09:14
@lorisleiva
lorisleiva force-pushed the 10-07-add_formatters_for_decoded_nodes branch 2 times, most recently from ebd5a98 to 06c0d0a Compare October 9, 2026 09:15
@lorisleiva
lorisleiva force-pushed the 10-07-decode_codama_nodes_into_decoded_node_trees branch 2 times, most recently from 12782e9 to 1248d65 Compare October 9, 2026 09:16
@lorisleiva
lorisleiva force-pushed the 10-07-add_formatters_for_decoded_nodes branch from 06c0d0a to 9dbd31f Compare October 9, 2026 09:16
@lorisleiva
lorisleiva force-pushed the 10-07-decode_codama_nodes_into_decoded_node_trees branch from 1248d65 to fd9f4ec Compare October 9, 2026 09:17
@lorisleiva
lorisleiva force-pushed the 10-07-add_formatters_for_decoded_nodes branch 2 times, most recently from 4a978e5 to 1d26fb1 Compare October 9, 2026 09:17
@lorisleiva
lorisleiva force-pushed the 10-07-decode_codama_nodes_into_decoded_node_trees branch 2 times, most recently from 0ad1db3 to fcc31ac Compare October 9, 2026 09:18
@lorisleiva
lorisleiva force-pushed the 10-07-add_formatters_for_decoded_nodes branch 2 times, most recently from 8093254 to 239b195 Compare October 9, 2026 09:18
@lorisleiva
lorisleiva force-pushed the 10-07-decode_codama_nodes_into_decoded_node_trees branch from fcc31ac to 123c3f8 Compare October 9, 2026 09:18
Base automatically changed from 10-07-decode_codama_nodes_into_decoded_node_trees to main October 9, 2026 09:19
@lorisleiva
lorisleiva force-pushed the 10-07-add_formatters_for_decoded_nodes branch from 239b195 to e128735 Compare October 9, 2026 09:19
@lorisleiva
lorisleiva marked this pull request as ready for review October 9, 2026 09:19
@lorisleiva
lorisleiva merged commit de0572f into main Oct 9, 2026
0 of 2 checks passed
@lorisleiva
lorisleiva deleted the 10-07-add_formatters_for_decoded_nodes branch October 9, 2026 09:19
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.

2 participants