limenkaunits.cpp raw
1 // Copyright (c) 2011-2021 The Limenka developers
2 // Distributed under the MIT software license, see the accompanying
3 // file COPYING or http://www.opensource.org/licenses/mit-license.php.
4
5 #include <qt/limenkaunits.h>
6 #include <chainparams.h>
7 #include <consensus/params.h>
8 #include <qt/guiutil.h>
9 #include <qt/tonalutils.h>
10
11 #include <consensus/amount.h>
12
13 #include <QFont>
14 #include <QRegularExpression>
15 #include <QStringList>
16
17 #include <cassert>
18
19 static constexpr auto MAX_DIGITS_BTC = 16;
20 static constexpr auto MAX_DIGITS_TBC = 13;
21
22 LimenkaUnits::LimenkaUnits(QObject *parent):
23 QAbstractListModel(parent),
24 unitlist(availableUnits())
25 {
26 }
27
28 QList<LimenkaUnit> LimenkaUnits::availableUnits()
29 {
30 static QList<LimenkaUnit> unitlist;
31 if (!unitlist.isEmpty()) return unitlist;
32 unitlist.append(Unit::BTC);
33 unitlist.append(Unit::mBTC);
34 unitlist.append(Unit::uBTC);
35 unitlist.append(Unit::SAT);
36 unitlist.append(Unit::TBC);
37 // ATTO (attosats) is always listed; OptionsModel::setDisplayUnit rejects
38 // selecting it before fork activation.
39 unitlist.append(Unit::ATTO);
40
41 return unitlist;
42 }
43
44 QString LimenkaUnits::longName(Unit unit)
45 {
46 switch (unit) {
47 case Unit::BTC: return QString::fromUtf8("\xce\xbb (lambda)");
48 case Unit::mBTC: return QString("mBTC");
49 case Unit::uBTC: return QString::fromUtf8("µBTC (bits)");
50 case Unit::SAT: return QString("sats");
51 case Unit::ATTO: return QString::fromUtf8("\xce\x9b (attosats)");
52 case Unit::bTBC: return QString::fromUtf8("ᵇTBC");
53 case Unit::sTBC: return QString::fromUtf8("ˢTBC");
54 case Unit::TBC: return QString("TBC");
55 } // no default case, so the compiler can warn about missing cases
56 assert(false);
57 }
58
59 QString LimenkaUnits::shortName(Unit unit)
60 {
61 switch (unit) {
62 case Unit::BTC: return QString::fromUtf8("\xce\xbb");
63 case Unit::mBTC: return longName(unit);
64 case Unit::uBTC: return QString("bits");
65 case Unit::SAT: return QString("sats");
66 case Unit::ATTO: return QString::fromUtf8("\xce\x9b");
67 case Unit::bTBC: return QString::fromUtf8("ᵇTBC");
68 case Unit::sTBC: return QString::fromUtf8("ˢTBC");
69 case Unit::TBC: return QString("TBC");
70 } // no default case, so the compiler can warn about missing cases
71 assert(false);
72 }
73
74 QString LimenkaUnits::description(Unit unit)
75 {
76 switch (unit) {
77 case Unit::BTC: return QString("Limenkas (decimal)");
78 case Unit::mBTC: return QString("Milli-Limenkas (1 / 1" THIN_SP_UTF8 "000)");
79 case Unit::uBTC: return QString("Micro-Limenkas (bits) (1 / 1" THIN_SP_UTF8 "000" THIN_SP_UTF8 "000)");
80 case Unit::SAT: return QString("Satoshi (sat) (1 / 100" THIN_SP_UTF8 "000" THIN_SP_UTF8 "000)");
81 case Unit::bTBC: return QString("Bong-Limenkas (1,0000 tonal)");
82 case Unit::sTBC: return QString("San-Limenkas (100 tonal)");
83 case Unit::TBC: return QString("Limenkas (tonal)");
84 } // no default case, so the compiler can warn about missing cases
85 assert(false);
86 }
87
88 qint64 LimenkaUnits::factor(Unit unit)
89 {
90 switch (unit) {
91 case Unit::BTC: return 100'000'000;
92 case Unit::mBTC: return 100'000;
93 case Unit::uBTC: return 100;
94 case Unit::SAT: return 1;
95 case Unit::ATTO: return 1; // Special: multiply by 10^10 in format
96 case Unit::bTBC: return 0x100000000LL;
97 case Unit::sTBC: return 0x1000000;
98 case Unit::TBC: return 0x10000;
99 } // no default case, so the compiler can warn about missing cases
100 assert(false);
101 }
102
103 int LimenkaUnits::decimals(Unit unit)
104 {
105 switch (unit) {
106 case Unit::BTC: return 8;
107 case Unit::mBTC: return 5;
108 case Unit::uBTC: return 2;
109 case Unit::SAT: return 0;
110 case Unit::ATTO: return 0; // Integer attosats
111 case Unit::bTBC: return 8;
112 case Unit::sTBC: return 6;
113 case Unit::TBC: return 4;
114 } // no default case, so the compiler can warn about missing cases
115 assert(false);
116 }
117
118 int LimenkaUnits::radix(Unit unit)
119 {
120 switch (unit) {
121 case Unit::bTBC:
122 case Unit::sTBC:
123 case Unit::TBC:
124 return 0x10;
125 default:
126 return 10;
127 }
128 }
129
130 LimenkaUnit LimenkaUnits::numsys(Unit unit)
131 {
132 switch (unit) {
133 case Unit::bTBC:
134 case Unit::sTBC:
135 case Unit::TBC:
136 return Unit::TBC;
137 default:
138 return Unit::BTC;
139 }
140 }
141
142 qint64 LimenkaUnits::max_digits(Unit unit)
143 {
144 switch (numsys(unit)) {
145 case Unit::TBC:
146 return MAX_DIGITS_TBC;
147 default:
148 return MAX_DIGITS_BTC;
149 }
150 }
151
152 qint64 LimenkaUnits::singlestep(Unit unit)
153 {
154 switch (numsys(unit)) {
155 case Unit::TBC:
156 return 0x10000;
157 default:
158 return 100000;
159 }
160 }
161
162 QString LimenkaUnits::format(Unit unit, const CAmount& nIn, bool fPlus, SeparatorStyle separators, bool justify)
163 {
164 // Note: not using straight sprintf here because we do NOT want
165 // localized number formatting.
166 qint64 n = (qint64)nIn;
167
168 // Special handling for ATTO: convert satoshis to attosats (1 sat = 10^10 attos)
169 if (unit == Unit::ATTO) {
170 // Multiply by 10^10 to get attosats
171 // Note: this may overflow for very large amounts, but that's unlikely
172 n = n * 10000000000LL;
173 }
174
175 qint64 coin = factor(unit);
176 int num_decimals = decimals(unit);
177 qint64 n_abs = (n > 0 ? n : -n);
178 qint64 quotient = n_abs / coin;
179 int uradix = radix(unit);
180 QString quotient_str = QString::number(quotient, uradix);
181 if (justify) {
182 quotient_str = quotient_str.rightJustified(max_digits(unit) - num_decimals, ' ');
183 }
184
185 QString remainder_str;
186 if (num_decimals > 0) {
187 const qint64 remainder = n_abs % coin;
188 remainder_str = QString::number(remainder, uradix).rightJustified(num_decimals, '0');
189 }
190
191 switch (numsys(unit)) {
192 case Unit::BTC:
193 {
194
195 // Use SI-style thin space separators as these are locale independent and can't be
196 // confused with the decimal marker.
197 QChar thin_sp(THIN_SP_CP);
198 int q_size = quotient_str.size();
199 if (separators == SeparatorStyle::ALWAYS || (separators == SeparatorStyle::STANDARD && q_size > 4))
200 for (int i = 3; i < q_size; i += 3)
201 quotient_str.insert(q_size - i, thin_sp);
202
203 break;
204 }
205 case Unit::TBC:
206 {
207 // Right-trim excess zeros after the decimal point
208 static const QRegularExpression tail_zeros("0+$");
209 remainder_str.remove(tail_zeros);
210 TonalUtils::ConvertFromHex(quotient_str);
211 TonalUtils::ConvertFromHex(remainder_str);
212 break;
213 }
214 default: assert(false);
215 }
216
217 if (n < 0)
218 quotient_str.insert(0, '-');
219 else if (fPlus && n > 0)
220 quotient_str.insert(0, '+');
221
222 if (!remainder_str.isEmpty())
223 quotient_str += QString(".") + remainder_str;
224 return quotient_str;
225 }
226
227
228 // NOTE: Using formatWithUnit in an HTML context risks wrapping
229 // quantities at the thousands separator. More subtly, it also results
230 // in a standard space rather than a thin space, due to a bug in Qt's
231 // XML whitespace canonicalisation
232 //
233 // Please take care to use formatHtmlWithUnit instead, when
234 // appropriate.
235
236 QString LimenkaUnits::formatWithUnit(Unit unit, const CAmount& amount, bool plussign, SeparatorStyle separators)
237 {
238 return format(unit, amount, plussign, separators) + QString(" ") + shortName(unit);
239 }
240
241 QString LimenkaUnits::formatHtmlWithUnit(const QFont& font, Unit unit, const CAmount& amount, bool plussign, SeparatorStyle separators)
242 {
243 QString str(formatWithUnit(unit, amount, plussign, separators));
244 str.replace(QChar(THIN_SP_CP), QString(THIN_SP_HTML));
245 return QString("<span style='white-space:nowrap;%2'>%1</span>").arg(str).arg(GUIUtil::fontToCss(font));
246 }
247
248 QString LimenkaUnits::formatWithSubSatoshi(const QFont& font, Unit unit, const CAmount& amount, uint64_t subSatoshi, bool plussign, SeparatorStyle separators)
249 {
250 // Format with sub-satoshi precision (dimmed trailing digits)
251 // For λ mode: show 1.23456789 λ, with sub-satoshi digits dimmed
252 // For sats mode: show 123456789 sats, with sub-satoshi digits dimmed
253 // For Λ mode: show integer attosats
254
255 QString mainStr = formatWithUnit(unit, amount, plussign, separators);
256 mainStr.replace(QChar(THIN_SP_CP), QString(THIN_SP_HTML));
257
258 // CSS for dimmed sub-satoshi digits
259 QString dimmedStyle = "color: #888888; font-size: 0.85em;";
260 QString normalStyle = GUIUtil::fontToCss(font);
261
262 // If no sub-satoshi or in ATTO mode, just return main string
263 if (subSatoshi == 0 || unit == Unit::ATTO) {
264 return QString("<span style='white-space:nowrap;%2'>%1</span>").arg(mainStr).arg(normalStyle);
265 }
266
267 // For λ mode: append sub-satoshi digits dimmed
268 // For sats mode: append sub-satoshi digits dimmed
269 QString subStr = QString::number(subSatoshi).rightJustified(18, '0');
270 // Remove trailing zeros
271 while (subStr.endsWith('0') && subStr.size() > 1) {
272 subStr.chop(1);
273 }
274
275 // Build HTML with dimmed sub-satoshi (space separator, not dot)
276 return QString("<span style='white-space:nowrap;%3'>%1 <span style='%4'>%2</span></span>")
277 .arg(mainStr)
278 .arg(subStr)
279 .arg(normalStyle)
280 .arg(dimmedStyle);
281 }
282
283 QString LimenkaUnits::formatWithPrivacy(Unit unit, const CAmount& amount, SeparatorStyle separators, bool privacy)
284 {
285 assert(amount >= 0);
286 QString value;
287 if (privacy) {
288 value = format(unit, 0, false, separators, true).replace('0', '#');
289 } else {
290 value = format(unit, amount, false, separators, true);
291 }
292 value += QString(" ") + shortName(unit);
293 value.replace(QChar(THIN_SP_CP), QString(THIN_SP_HTML));
294 return QString("<span style='white-space: nowrap;'>%1</span>").arg(value);
295 }
296
297 bool LimenkaUnits::parse(Unit unit, const QString& value, CAmount* val_out)
298 {
299 if (value.isEmpty()) {
300 return false; // Refuse to parse invalid unit or empty string
301 }
302 int num_decimals = decimals(unit);
303
304 // Ignore spaces and thin spaces when parsing
305 QStringList parts = removeSpaces(value).split(".");
306
307 if(parts.size() > 2)
308 {
309 return false; // More than one dot
310 }
311 const QString& whole = parts[0];
312 QString decimals;
313
314 if(parts.size() > 1)
315 {
316 decimals = parts[1];
317 }
318 if(decimals.size() > num_decimals)
319 {
320 return false; // Exceeds max precision
321 }
322 bool ok = false;
323 QString str = whole + decimals.leftJustified(num_decimals, '0');
324
325 Unit unumsys = numsys(unit);
326 if (unumsys == Unit::TBC) {
327 if (str.size() > 15)
328 return false; // Longer numbers may exceed 63 bits
329 TonalUtils::ConvertToHex(str);
330 } else
331 if(str.size() > 18)
332 {
333 return false; // Longer numbers will exceed 63 bits
334 }
335
336 CAmount retvalue(str.toLongLong(&ok, radix(unit)));
337 if(val_out)
338 {
339 *val_out = retvalue;
340 }
341 return ok;
342 }
343
344 QString LimenkaUnits::getAmountColumnTitle(Unit unit)
345 {
346 return QObject::tr("Amount") + " (" + shortName(unit) + ")";
347 }
348
349 int LimenkaUnits::rowCount(const QModelIndex &parent) const
350 {
351 Q_UNUSED(parent);
352 return unitlist.size();
353 }
354
355 QVariant LimenkaUnits::data(const QModelIndex &index, int role) const
356 {
357 int row = index.row();
358 if(row >= 0 && row < unitlist.size())
359 {
360 Unit unit = unitlist.at(row);
361 switch(role)
362 {
363 case Qt::EditRole:
364 case Qt::DisplayRole:
365 return QVariant(longName(unit));
366 case Qt::ToolTipRole:
367 return QVariant(description(unit));
368 case UnitRole:
369 return QVariant::fromValue(unit);
370 }
371 }
372 return QVariant();
373 }
374
375 CAmount LimenkaUnits::maxMoney()
376 {
377 return MAX_MONEY;
378 }
379
380 std::variant<qint8, QString> LimenkaUnits::ToSetting(LimenkaUnit unit)
381 {
382 switch (unit) {
383 case LimenkaUnit::BTC: return qint8{0};
384 case LimenkaUnit::mBTC: return qint8{1};
385 case LimenkaUnit::uBTC: return qint8{2};
386 case LimenkaUnit::SAT: return qint8{3};
387 case LimenkaUnit::bTBC: return QString("bTBC");
388 case LimenkaUnit::sTBC: return QString("sTBC");
389 case LimenkaUnit::TBC: return QString("TBC");
390 } // no default case, so the compiler can warn about missing cases
391 assert(false);
392 }
393
394 namespace {
395 LimenkaUnit FromQint8(qint8 num)
396 {
397 switch (num) {
398 case 0: return LimenkaUnit::BTC;
399 case 1: return LimenkaUnit::mBTC;
400 case 2: return LimenkaUnit::uBTC;
401 case 3: return LimenkaUnit::SAT;
402 }
403 return LimenkaUnit::BTC;
404 }
405 } // namespace
406
407 LimenkaUnit LimenkaUnits::FromSetting(const QString& s, LimenkaUnit def)
408 {
409 if (s == "0") return LimenkaUnit::BTC;
410 if (s == "1") return LimenkaUnit::mBTC;
411 if (s == "2") return LimenkaUnit::uBTC;
412 if (s == "3") return LimenkaUnit::SAT;
413 if (s == "4") return LimenkaUnit::sTBC;
414 if (s == "5") return LimenkaUnit::TBC;
415 if (s == "bTBC") return LimenkaUnit::bTBC;
416 if (s == "sTBC") return LimenkaUnit::sTBC;
417 if (s == "TBC") return LimenkaUnit::TBC;
418 return def;
419 }
420
421 QDataStream& operator<<(QDataStream& out, const LimenkaUnit& unit)
422 {
423 auto setting_val = LimenkaUnits::ToSetting(unit);
424 if (const QString* setting_str = std::get_if<QString>(&setting_val)) {
425 return out << qint8{0} << *setting_str;
426 } else {
427 return out << std::get<qint8>(setting_val);
428 }
429 }
430
431 QDataStream& operator>>(QDataStream& in, LimenkaUnit& unit)
432 {
433 qint8 input;
434 in >> input;
435 unit = FromQint8(input);
436 if (!in.atEnd()) {
437 QString setting_str;
438 in >> setting_str;
439 unit = LimenkaUnits::FromSetting(setting_str, unit);
440 }
441 return in;
442 }
443