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
Draft
Create a new type TokenAmount and use it to support multiple precision in the fungible contract#6606ma2bd wants to merge 25 commits into
TokenAmount and use it to support multiple precision in the fungible contract#6606ma2bd wants to merge 25 commits into
Conversation
# Conflicts: # linera-base/src/data_types.rs
TokenAmountTokenAmount and use it to support multiple precision in the fungible contract
…n-workspace fallout
ma2bd
marked this pull request as ready for review
July 14, 2026 03:09
ma2bd
marked this pull request as draft
July 14, 2026 06:25
deuszx
reviewed
Jul 14, 2026
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. |
Contributor
There was a problem hiding this comment.
I think this comment (and the one below lines 164-167) is unnecessary. Anyone that has worked with application IDs has used this trick.
deuszx
reviewed
Jul 14, 2026
|
|
||
| // 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"); |
Contributor
There was a problem hiding this comment.
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.
Contributor
There was a problem hiding this comment.
Especially that down below you define a FungibleAmount type alias.
deuszx
reviewed
Jul 14, 2026
deuszx
reviewed
Jul 14, 2026
| /// 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); |
Contributor
There was a problem hiding this comment.
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.
deuszx
reviewed
Jul 14, 2026
…with a fixed-precision brand
…us T: Default bound
…ive them instead of hand-writing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
Amountis hard-coded to 18 decimal places, so an application that wants a differentprecision — or that juggles several tokens with different precisions — cannot reuse it. This
PR introduces a generic, precision-parametric amount type, re-bases
Amounton top of it, anddemonstrates the capability by giving the
fungibleexample a configurabledecimalsparameter.
Proposal
Introduce
TokenAmount<T>: au128fixed-point amount "branded" by a marker typeT: Tokenthat supplies the precision and a serde/GraphQL name.New type (
linera-base)TokenAmount<T>carries the full arithmetic /Display/FromStr/ serde / GraphQLsurface of the old
Amount, parameterized by precision.Tokentrait providesNAME,decimals(), aDECIMAL_DISPLAYflag (decimal vs.raw-
u128form), and apow10helper. Precision that overflowsu128panics instead ofsilently wrapping; units finer than the token supports truncate instead of panicking.
Tokenmarker needs no derives, andDebugis tagged with the token's name (e.g.
Amount(…)).to_inner/from_innerplusFrom/Intoconversions withU128make crossing thetyped/wire boundary idiomatic.
AmountbecomesTokenAmount<NativeToken>Amountis now a type alias.ONE/MAX/ZERO/DECIMAL_PLACESremain inherent consts andthe native-only helpers (
to_attos,U256/f64conversions) are preserved, so the ~230existing call sites compile unchanged. BCS bytes and the
AmountGraphQL scalar areunchanged.
amount→token-amount. This is a rename only: asingle-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)branded_token!factory macro declares a brand together with its precision — either fixedat compile time (a
const), or configured exactly once at runtime from the applicationparameters (via a process-global
OnceLock; reading before configuration panics).FungibleTokenAbi<T = NativeToken>,with
FungibleOperation<T>/FungibleResponse<T>/InitialState<T>carrying brandedTokenAmount<T>amounts), and the two sides use it differently:fungibleexample instantiates the ABI with its own runtime brandFungible, whose precision is read once from thedecimalsparameter inContract::loadand
Service::new.default
NativeToken(aconst18-decimals brand) — so they never have to configuredecimals. The example re-exports the generic ABI for exactly this, while using
<Fungible>only in its own contract/service.
T = NativeTokenmatches the pre-PRAmount-based ABI, the shared ABI isbyte- 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 thewire with a consumer using another.
StableEnumandGraphQLMutationRootderives to accept generic enums; the code generated for the existing non-generic types is
unchanged.
fungibleexample gains adecimalsparameter (default 18 in JSON) and usesFungiblefor decimal-aware state and service display. The dependent examples (amm, matching-engine,
crowd-funding, rfq) and
native-fungibleuse the ABI at the default fixed brand, so they needno 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-enumStableEnum/GraphQLMutationRootderives 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-serviceGraphQL schema, and the fungible / native-fungible format snapshots wereregenerated and committed;
CLI.mdandbinary_formatsare verified unaffected.Release Plan
Nothing to do / These changes follow the usual release cycle.
The WIT type rename (
amount→token-amount) does not change the wire layout, and theprotocol 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