stack_wallet/lib/utilities/amount/amount_unit.dart
2023-08-23 11:29:10 -06:00

326 lines
9.2 KiB
Dart

/*
* This file is part of Stack Wallet.
*
* Copyright (c) 2023 Cypher Stack
* All Rights Reserved.
* The code is distributed under GPLv3 license, see LICENSE file for details.
* Generated by Cypher Stack on 2023-05-26
*
*/
import 'dart:math' as math;
import 'package:decimal/decimal.dart';
import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart';
import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/util.dart';
// preserve index order as index is used to store value in preferences
enum AmountUnit {
normal(0),
milli(3),
micro(6),
nano(9),
pico(12),
femto(15),
atto(18),
zepto(21),
yocto(24),
ronto(27),
quecto(30),
;
const AmountUnit(this.shift);
final int shift;
static List<AmountUnit> valuesForCoin(Coin coin) {
switch (coin) {
case Coin.firo:
case Coin.litecoin:
case Coin.particl:
case Coin.namecoin:
case Coin.bitcoinTestNet:
case Coin.litecoinTestNet:
case Coin.bitcoincashTestnet:
case Coin.dogecoinTestNet:
case Coin.firoTestNet:
case Coin.bitcoin:
case Coin.bitcoincash:
case Coin.dogecoin:
case Coin.eCash:
case Coin.epicCash:
case Coin.stellar: // TODO: check if this is correct
case Coin.stellarTestnet:
case Coin.tezos:
return AmountUnit.values.sublist(0, 4);
case Coin.monero:
case Coin.wownero:
return AmountUnit.values.sublist(0, 5);
case Coin.ethereum:
return AmountUnit.values.sublist(0, 7);
case Coin.nano:
case Coin.banano:
return AmountUnit.values;
}
}
}
extension AmountUnitExt on AmountUnit {
String unitForCoin(Coin coin) {
switch (this) {
case AmountUnit.normal:
return coin.ticker;
case AmountUnit.milli:
return "m${coin.ticker}";
case AmountUnit.micro:
return "µ${coin.ticker}";
case AmountUnit.nano:
if (coin == Coin.ethereum) {
return "gwei";
} else if (coin == Coin.wownero ||
coin == Coin.monero ||
coin == Coin.nano ||
coin == Coin.banano) {
return "n${coin.ticker}";
} else {
return "sats";
}
case AmountUnit.pico:
if (coin == Coin.ethereum) {
return "mwei";
} else if (coin == Coin.wownero ||
coin == Coin.monero ||
coin == Coin.nano ||
coin == Coin.banano) {
return "p${coin.ticker}";
} else {
return "invalid";
}
case AmountUnit.femto:
if (coin == Coin.ethereum) {
return "kwei";
} else if (coin == Coin.nano || coin == Coin.banano) {
return "f${coin.ticker}";
} else {
return "invalid";
}
case AmountUnit.atto:
if (coin == Coin.ethereum) {
return "wei";
} else if (coin == Coin.nano || coin == Coin.banano) {
return "a${coin.ticker}";
} else {
return "invalid";
}
case AmountUnit.zepto:
if (coin == Coin.nano || coin == Coin.banano) {
return "z${coin.ticker}";
} else {
return "invalid";
}
case AmountUnit.yocto:
if (coin == Coin.nano || coin == Coin.banano) {
return "y${coin.ticker}";
} else {
return "invalid";
}
case AmountUnit.ronto:
if (coin == Coin.nano || coin == Coin.banano) {
return "r${coin.ticker}";
} else {
return "invalid";
}
case AmountUnit.quecto:
if (coin == Coin.nano || coin == Coin.banano) {
return "q${coin.ticker}";
} else {
return "invalid";
}
}
}
String unitForContract(EthContract contract) {
switch (this) {
case AmountUnit.normal:
return contract.symbol;
case AmountUnit.milli:
return "m${contract.symbol}";
case AmountUnit.micro:
return "µ${contract.symbol}";
case AmountUnit.nano:
return "gwei";
case AmountUnit.pico:
return "mwei";
case AmountUnit.femto:
return "kwei";
case AmountUnit.atto:
return "wei";
default:
throw ArgumentError(
"Does eth even allow more than 18 decimal places?",
);
}
}
Amount? tryParse(
String value, {
required String locale,
required Coin coin,
EthContract? tokenContract,
bool overrideWithDecimalPlacesFromString = false,
}) {
final precisionLost = value.startsWith("~");
final parts = (precisionLost ? value.substring(1) : value).split(" ");
if (parts.first.isEmpty) {
return null;
}
String str = parts.first;
if (str.startsWith(RegExp(r'[+-]'))) {
str = str.substring(1);
}
if (str.isEmpty) {
return null;
}
// get number symbols for decimal place and group separator
final numberSymbols = Util.getSymbolsFor(locale: locale);
final groupSeparator = numberSymbols?.GROUP_SEP ?? ",";
final decimalSeparator = numberSymbols?.DECIMAL_SEP ?? ".";
str = str.replaceAll(groupSeparator, "");
final decimalString = str.replaceFirst(decimalSeparator, ".");
final Decimal? decimal = Decimal.tryParse(decimalString);
if (decimal == null) {
return null;
}
final decimalPlaces = overrideWithDecimalPlacesFromString
? decimal.scale
: tokenContract?.decimals ?? coin.decimals;
final realShift = math.min(shift, decimalPlaces);
return decimal.shift(0 - realShift).toAmount(fractionDigits: decimalPlaces);
}
String displayAmount({
required Amount amount,
required String locale,
required Coin coin,
required int maxDecimalPlaces,
bool withUnitName = true,
bool indicatePrecisionLoss = true,
String? overrideUnit,
EthContract? tokenContract,
}) {
assert(maxDecimalPlaces >= 0);
// ensure we don't shift past minimum atomic value
final realShift = math.min(shift, amount.fractionDigits);
// shifted to unit
final Decimal shifted = amount.decimal.shift(realShift);
// get shift int value without fractional value
final BigInt wholeNumber = shifted.toBigInt();
// get decimal places to display
final int places = math.max(0, amount.fractionDigits - realShift);
// start building the return value with just the whole value
String returnValue = wholeNumber.toString();
// get number symbols for decimal place and group separator
final numberSymbols = Util.getSymbolsFor(locale: locale);
// insert group separator
final regex = RegExp(r'\B(?=(\d{3})+(?!\d))');
returnValue = returnValue.replaceAllMapped(
regex,
(m) => "${m.group(0)}${numberSymbols?.GROUP_SEP ?? ","}",
);
// if true and withUnitName is true, we will show "~" prepended on amount
bool didLosePrecision = false;
// if any decimal places should be shown continue building the return value
if (places > 0) {
// get the fractional value
final Decimal fraction = shifted - shifted.truncate();
// get final decimal based on max precision wanted while ensuring that
// maxDecimalPlaces doesn't exceed the max per coin
final int updatedMax;
if (tokenContract != null) {
updatedMax = maxDecimalPlaces > tokenContract.decimals
? tokenContract.decimals
: maxDecimalPlaces;
} else {
updatedMax =
maxDecimalPlaces > coin.decimals ? coin.decimals : maxDecimalPlaces;
}
final int actualDecimalPlaces = math.min(places, updatedMax);
// get remainder string without the prepending "0."
final fractionString = fraction.toString();
String remainder;
if (fractionString.length > 2) {
remainder = fraction.toString().substring(2);
} else {
remainder = "0";
}
if (remainder.length > actualDecimalPlaces) {
// check for loss of precision
final remainingRemainder =
BigInt.tryParse(remainder.substring(actualDecimalPlaces));
if (remainingRemainder != null) {
didLosePrecision = remainingRemainder > BigInt.zero;
}
// trim unwanted trailing digits
remainder = remainder.substring(0, actualDecimalPlaces);
} else if (remainder.length < actualDecimalPlaces) {
// pad with zeros to achieve requested precision
for (int i = remainder.length; i < actualDecimalPlaces; i++) {
remainder += "0";
}
}
// get decimal separator based on locale
final String separator = numberSymbols?.DECIMAL_SEP ?? ".";
// append separator and fractional amount
returnValue += "$separator$remainder";
}
if (!withUnitName && !indicatePrecisionLoss) {
return returnValue;
}
if (didLosePrecision && indicatePrecisionLoss) {
returnValue = "~$returnValue";
}
if (!withUnitName && indicatePrecisionLoss) {
return returnValue;
}
// return the value with the proper unit symbol
if (tokenContract != null) {
overrideUnit = unitForContract(tokenContract);
}
return "$returnValue ${overrideUnit ?? unitForCoin(coin)}";
}
}