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.)