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