Skip to content

Create a new type TokenAmount and use it to support multiple precision in the fungible contract - #6606

Draft
ma2bd wants to merge 25 commits into
linera-io:mainfrom
ma2bd:amount_brand
Draft

Create a new type TokenAmount and use it to support multiple precision in the fungible contract#6606
ma2bd wants to merge 25 commits into
linera-io:mainfrom
ma2bd:amount_brand

Conversation

@ma2bd

@ma2bd ma2bd commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Motivation

Amount is hard-coded to 18 decimal places, so an application that wants a different
precision — or that juggles several tokens with different precisions — cannot reuse it. This
PR introduces a generic, precision-parametric amount type, re-bases Amount on top of it, and
demonstrates the capability by giving the fungible example a configurable decimals
parameter.

Proposal

Introduce TokenAmount<T>: a u128 fixed-point amount "branded" by a marker type
T: Token that supplies the precision and a serde/GraphQL name.

New type (linera-base)

  • TokenAmount<T> carries the full arithmetic / Display / FromStr / serde / GraphQL
    surface of the old Amount, parameterized by precision.
  • The Token trait provides NAME, decimals(), a DECIMAL_DISPLAY flag (decimal vs.
    raw-u128 form), and a pow10 helper. Precision that overflows u128 panics instead of
    silently wrapping; units finer than the token supports truncate instead of panicking.
  • The standard traits are hand-implemented so a Token marker needs no derives, and Debug
    is tagged with the token's name (e.g. Amount(…)).
  • to_inner/from_inner plus From/Into conversions with U128 make crossing the
    typed/wire boundary idiomatic.

Amount becomes TokenAmount<NativeToken>

  • Amount is now a type alias. ONE/MAX/ZERO/DECIMAL_PLACES remain inherent consts and
    the native-only helpers (to_attos, U256/f64 conversions) are preserved, so the ~230
    existing call sites compile unchanged. BCS bytes and the Amount GraphQL scalar are
    unchanged.
  • The WIT runtime-API type is renamed amounttoken-amount. This is a rename only: a
    single-field record has the same flat WIT layout as before and marshalling is positional, so
    the wire format is unchanged and existing contracts stay ABI-compatible.

Configurable-precision tokens (linera-sdk + examples)

  • A branded_token! factory macro declares a brand together with its precision — either fixed
    at compile time (a const), or configured exactly once at runtime from the application
    parameters (via a process-global OnceLock; reading before configuration panics).
  • An application with parametric precision keeps a generic ABI (FungibleTokenAbi<T = NativeToken>,
    with FungibleOperation<T> / FungibleResponse<T> / InitialState<T> carrying branded
    TokenAmount<T> amounts), and the two sides use it differently:
    • Internally, the fungible example instantiates the ABI with its own runtime brand
      Fungible, whose precision is read once from the decimals parameter in Contract::load
      and Service::new.
    • Externally, consumers instantiate the same ABI with a fixed-precision brand — by
      default NativeToken (a const 18-decimals brand) — so they never have to configure
      decimals. The example re-exports the generic ABI for exactly this, while using <Fungible>
      only in its own contract/service.
  • Because the default T = NativeToken matches the pre-PR Amount-based ABI, the shared ABI is
    byte- and GraphQL-identical to before — so this is not an ABI break — and since
    TokenAmount<T> is BCS brand-agnostic, an application built on one brand interoperates on the
    wire with a consumer using another.
  • Supporting a generic ABI required generalizing the StableEnum and GraphQLMutationRoot
    derives to accept generic enums; the code generated for the existing non-generic types is
    unchanged.
  • The fungible example gains a decimals parameter (default 18 in JSON) and uses Fungible
    for decimal-aware state and service display. The dependent examples (amm, matching-engine,
    crowd-funding, rfq) and native-fungible use the ABI at the default fixed brand, so they need
    no precision setup and their test code is unchanged.

Test Plan

CI. New unit tests cover zero-decimal display, precision truncation and overflow, the
decimal/raw display modes, JSON+GraphQL consistency, the generalized conversions, and the
branded_token! macro (fixed, runtime, and fail-fast paths). The generic-enum StableEnum /
GraphQLMutationRoot derives are exercised by the branded fungible example, and the derives'
existing non-generic output is pinned by their round-trip test. Because consumers use the ABI at
the default fixed brand, the integration and example test suites are unchanged. The WIT files,
the linera-service GraphQL schema, and the fungible / native-fungible format snapshots were
regenerated and committed; CLI.md and binary_formats are verified unaffected.

Release Plan

  • Nothing to do / These changes follow the usual release cycle.

    The WIT type rename (amounttoken-amount) does not change the wire layout, and the
    protocol BCS/storage formats are unchanged, so this does not require a new deployment for
    ABI compatibility. (Applications recompiled against the new SDK will pick up the renamed
    WIT type.)

Links

@ma2bd ma2bd changed the title [draft] create a new type TokenAmount Create a new type TokenAmount and use it to support multiple precision in the fungible contract Jul 14, 2026
@ma2bd
ma2bd marked this pull request as ready for review July 14, 2026 03:09
@ma2bd
ma2bd requested review from afck and deuszx July 14, 2026 03:09
@ma2bd
ma2bd marked this pull request as draft July 14, 2026 06:25
Comment on lines +54 to +56
// `create_with_accounts` yields the fungible example's own branded ABI; the crowd-funding
// application refers to the token by the default `FungibleTokenAbi`. The two are wire-identical
// (BCS ignores the brand), so re-tag the application id to the ABI this test expects.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think this comment (and the one below lines 164-167) is unnecessary. Anyone that has worked with application IDs has used this trick.


// The brand for this application's token amounts. Its precision is set at runtime from the
// `decimals` application parameter, in `Contract::load` and `Service::new`.
linera_sdk::branded_token!(pub struct Fungible = "FungibleAmount");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This seems weird – Fungible is (to anyone that has experience iwth blockchains) a very specific thing and here it's followed by an equal sign and FungibleAmount.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Especially that down below you define a FungibleAmount type alias.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

agreed

Comment thread examples/fungible/src/lib.rs Outdated
/// default). The `Fungible` brand reads its precision from a process-global that nothing sets
/// outside a contract, so we configure it on first use; the first configuration wins.
fn fungible_amount(tokens: u128) -> fungible::FungibleAmount {
fungible::Fungible::configure_decimals(18);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Mutating global variables is an anti-pattern that leads to problems. I'd rather see this being a pure function where configure_decimals is part of a constructor below.

Comment thread linera-sdk/src/abis/fungible.rs Outdated
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