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