taler-docs

Documentation for GNU Taler components, APIs and protocols
Log | Files | Refs | README | LICENSE

051-fractional-digits.rst (11102B)


      1 DD 51: Fractional Digits
      2 #########################
      3 
      4 :Design status: Accepted
      5 :Implementation status: Implemented
      6 :DD shepherd: TBD
      7 :Historical contributors: Sebastian, Marc Stibane, Iván Ávalos, Christian Grothoff
      8 :First published: 2023-10-02
      9 :Last substantive change: 2025-07-03
     10 :Implementation evidence: exchange (2023-10-07), taler-ios (2023-10-24), taler-android (2024-02-12)
     11 :Normative references: ``manpages/frags/currency-spec.rst``, ``core/exchange/get-config.rst``, ``wallet/wallet-core.md``
     12 
     13 Summary
     14 =======
     15 
     16 This design document specifies how an amount's fractional digits should be rendered.
     17 Note that UIs that cannot render amounts as specified (e.g. because the display does
     18 not support super script digits) may ignore the rendering advice provided by the
     19 protocol under this DD.
     20 
     21 
     22 Motivation
     23 ==========
     24 
     25 Since different currencies have different ways to show/render fractionals, the
     26 end-user apps should follow these guidelines.
     27 
     28 Requirements
     29 ============
     30 
     31 There was already a specification for ScopedCurrencyInfo - which got renamed to CurrencySpecification.
     32 
     33 We need three core characteristics for fractional digits for each currency:
     34 
     35   e) the number of fractional digits e in [0..8] the user may 'e'nter in a TextInputField
     36 
     37   n) the number of fractional digits n in [0..8] to be rendered as 'n'ormal characters (same font and size as the integer digits). All additional fractional digits will be rendered as SuperScriptDigits as known from gas filling stations. The UI should never round or truncate any amount, but always render all existing digits (except trailing zeroes, see c).
     38 
     39   z) the number of fractional digits z in [0..8] to be rendered as trailing 'z'eroes (including SuperScript digits). E.g. if z = 2 (and n = 2), then render $5 as ``$ 5.00``. If z = 3 (and n = 2), then render $5 as ``$ 5.00⁰`` with two normal trailing zeroes and one superscript trailing zero.
     40 
     41 The values e, n, and z are independent from each other. Each could be any value
     42 from 0 to 8. However, when a user enters an amount, s/he should be able to input
     43 all normal fractionals. Thus e should never be smaller than n.
     44 
     45 Usually, all these three numbers have the same value (e = n = z), which means
     46 that in case of e.g. '2' (used for €,$,£) the user can enter cent/penny values
     47 (but not a fraction of those), these cents/pennies are always shown (even if
     48 they are 0) as two normal digits after the decimal separator, and fractions of
     49 a cent/penny are rendered as SuperScriptDigits, but appear only if they are not
     50 trailing zeroes.
     51 For japanese ¥, all three values could be 0, which means that the user cannot
     52 enter fractions at all. If there are fractions they would never be rendered as
     53 normal digits but always as SuperScript, and appear only if they are not
     54 trailing zeroes.
     55 
     56 Additionally, some cryptocurrencies have such huge units, that they are
     57 commonly rendered in milli-units, such as mBTC (milliBTC, 1/1000 of a BTC),
     58 Gwei (Giga-WEI), Mwei (Million-WEI), Kwei (Kilo-WEI), or
     59 Mether/Kether/Gether/Tether and more "logical" units such as Szabo and
     60 Finney. See ``https://coinguides.org/ethereum-unit-converter-gwei-ether/`` if
     61 you want a good laugh. Regardless of the self-inflicted insanity here, this
     62 could also make sense for inflated currencies in some cases. So we probably
     63 should also have the ability to ship such a conversion map.
     64 
     65 For the "withdraw" dialog action buttons with the pre-filled amounts in
     66 some wallet GUIs, we also need to provide wallets with an idea of what
     67 good amounts would be.
     68 
     69 
     70 Proposed Solution
     71 =================
     72 
     73 Protocol considerations
     74 -----------------------
     75 
     76 The exchange, bank and merchant backends would need to be configured (via
     77 their configuration files) to return the following CurrencySpecification in their
     78 ``/config`` and/or ``/keys`` endpoints.  The bank returns this so that the
     79 bank SPA can render amounts correctly, the exchange informs the wallets about
     80 the desired way to render the currency, and the merchant backend informs the
     81 merchant SPA --- independently of any particular exchange being used --- how
     82 the merchant SPA should render amounts. Hence, the information will need to be
     83 provisioned by all three services.
     84 
     85   .. code-block:: swift
     86 
     87       public struct CurrencySpecification: Codable, Sendable {
     88           // e.g. “Japanese Yen” or "Bitcoin (Mainnet)"
     89           let name: String
     90           // how many digits the user may enter after the decimal separator
     91           let fractional_input_digits: Int
     92           // €,$,£: 2; some arabic currencies: 3, ¥: 0
     93           let fractional_normal_digits: Int
     94           // usually same as fractionalNormalDigits, but e.g. might be 2 for ¥
     95           let fractional_trailing_zero_digits: Int
     96           // specifies whether the keys in `alt_unit_names' are symbols
     97           // (e.g. €, k€) or names (e.g. BTC, mBTC), so that apps can decide
     98           // how to render it (e.g. EUR 10 vs €10)
     99           let alt_unit_names_are_symbols: Bool
    100           // map of powers of 10 to alternative currency names / symbols,
    101           // must always have an entry under "0" that defines the base name,
    102           // e.g.  "0 : €" or "3 : k€". For BTC, would be "0 : BTC, -3 : mBTC".
    103           // This way, we can also communicate the currency symbol to be used.
    104           let alt_unit_names: [Int : String]
    105 
    106           // An array of common amounts that should be turned into
    107           // display buttons in dialogs where the user might like
    108           // a short-cut.  The array should have four entries, but
    109           // may have fewer or more entries. Wallets may omit
    110           // later entries in the array.
    111           let common_amounts: [Amount]
    112       }
    113 
    114 (Note: decimal_separator, group_separator and is_currency_name_leading were
    115 removed from this struct since they should always be taken from the user's
    116 locale.)
    117 
    118 For very large (2400000) or very tiny amounts (0.000056) the software would
    119 then first represent the number compactly without any fraction (so for our
    120 examples above, 24 * 10^6 and 56 * 10^-6) and then search for the nearest fit
    121 in the alt_unit_names table. The result might then be 24000 KGELD or 0.056
    122 mGELD, assuming the map had entries for 3 and -3 respectively. Depending on
    123 the table, the result could also be 24 MGELD (6 : MGELD), or 5.6 nGELD
    124 (assuming -6 : nGeld).  Fractional rendering rules would still be applied
    125 to the alternative unit name, alas the "fractional_input_digits" would
    126 always apply to the unit currency and may need to be adjusted if amounts
    127 are input using an alternative unit name.
    128 
    129 Configuration syntax
    130 --------------------
    131 
    132 Each currency should be specified in its own subsystem-independent
    133 currency, with the section name prefixed with "currency-". In that
    134 section. The map could be given directly in JSON. For example:
    135 
    136   .. code-block:: ini
    137 
    138     [currency-euro]
    139     ENABLED = YES
    140     name = "Euro"
    141     code = "EUR"
    142     fractional_input_digits = 2
    143     fractional_normal_digits = 2
    144     fractional_trailing_zero_digits = 2
    145     alt_unit_names_are_symbols = YES
    146     alt_unit_names = {"0":"€"}
    147     common_amounts = EUR:10 EUR:25 EUR:50 EUR:100
    148 
    149     [currency-japanese-yen]
    150     ENABLED = YES
    151     name = "Japanese Yen"
    152     code = "JPY"
    153     fractional_input_digits = 2
    154     fractional_normal_digits = 0
    155     fractional_trailing_zero_digits = 2
    156     alt_unit_names_are_symbols = YES
    157     alt_unit_names = {"0":"¥"}
    158     common_amounts = JPY:500 JPY:1000 JPY:5000 JPY:10000
    159 
    160     [currency-bitcoin-mainnet]
    161     ENABLED = NO
    162     name = "Bitcoin (Mainnet)"
    163     code = "BITCOINBTC"
    164     fractional_input_digits = 8
    165     fractional_normal_digits = 3
    166     fractional_trailing_zero_digits = 0
    167     alt_unit_names_are_symbols = NO
    168     alt_unit_names = {"0":"BTC","-3":"mBTC"}
    169     common_amounts = BITCOINBTC:0.001 BITCOINBTC:0.01 BITCOINBTC:0.02 BITCOINBTC:0.025
    170 
    171     [currency-ethereum]
    172     ENABLED = NO
    173     name = "WAI-ETHER (Ethereum)"
    174     code = "EthereumWAI"
    175     fractional_input_digits = 0
    176     fractional_normal_digits = 0
    177     fractional_trailing_zero_digits = 0
    178     alt_unit_names_are_symbols = NO
    179     alt_unit_names = {"0":"WAI","3":"KWAI","6":"MWAI","9":"GWAI","12":"Szabo","15":"Finney","18":"Ether","21":"KEther","24":"MEther"}
    180     common_amounts = EthereumWAI:0.001 EthereumWAI:0.01
    181 
    182 
    183 Implementation considerations
    184 -----------------------------
    185 
    186 iOS has a built-in currency formatter, which can be configured from a locale.
    187 It knows how to deal with group-separators and where to apply them (e.g. India
    188 uses a mixture of thousands and hundreds instead of putting the separator
    189 after each 3 digits like western currencies). Set the formatter's parameter
    190 ``maximumFractionDigits`` to 8, then it will not round the value and thus can
    191 be used for the whole amount. Set its parameter ``minimumFractionDigits`` to
    192 'z' (``fractionalTrailingZeroDigits``) to let it automatically add trailing
    193 zeroes. Then convert all fractional digits after 'n'
    194 (``fractionalNormalDigits``) to SuperScript digits.
    195 
    196 The field ``alt_unit_names_are_symbols`` was introduced in order to help UIs
    197 better decide how to render amounts with unit names (e.g. BTC, mBTC) instead
    198 of unit symbols (e.g. €, k€). Typically, currency symbols (in Android and iOS)
    199 are rendered, depending on the locale, before or after the amount without a
    200 space in between (e.g. €10), however, if the given currency has names instead
    201 of symbols for its units, rendering amounts without a space in between
    202 (e.g. BTC10) is not ideal and results in ugliness and user
    203 dissatisfaction. When this field is set to ``true``, it is expected that the
    204 wallet apps will render the amount with a space in between.
    205 
    206 Definition of Done
    207 ==================
    208 
    209 (Only applicable to design documents that describe a new feature.  While the
    210 DoD is not satisfied yet, a user-facing feature **must** be behind a feature
    211 flag or dev-mode flag.)
    212 
    213   * [x] Configuration (INI) format finalized and documented in the
    214     ``taler.conf`` man page
    215   * [x] Endpoints of libeufin-bank, fakebank, exchange and merchant return the information
    216   * [x] SPAs use the information to render amounts
    217   * [x] Wallet-core passes rendering information to wallet UIs
    218   * [x] Cashier, Android PoS, WebExtension, Android and iOS Wallet render amounts accordingly
    219 
    220 
    221 Alternatives
    222 ============
    223 
    224 None, we cannot confuse users by rendering amounts in ways that break cultural
    225 standards, and we cannot round and have numbers in balances not add up.
    226 
    227 
    228 Drawbacks
    229 =========
    230 
    231 Discussion / Q&A
    232 ================
    233 
    234 We probably should NOT have the decimalSeparator in this definition. Instead that
    235 should be taken from the locale of the user, so they see currency amounts formatted
    236 like they're used to.
    237 If we really keep this, then we would also need the groupSeparator to ensure it is
    238 not identical to the decimalSeparator.
    239 Better to leave this can of worms to the operating system our app runs on, and render
    240 according to the user's preferences (locale)...
    241 
    242 However, instead of decimalSeparator we could specify the locale this currency belongs to.
    243 
    244 
    245 
    246 (This should be filled in with results from discussions on mailing lists / personal communication.)