From ae51ce61c34765fd9c47e735dd5282f9ca23a736 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 6 Apr 2023 17:27:42 -0600 Subject: [PATCH] amount unit + tests --- lib/utilities/amount/amount_unit.dart | 120 ++++++++++++++++ test/utilities/amount/amount_unit_test.dart | 144 ++++++++++++++++++++ 2 files changed, 264 insertions(+) create mode 100644 lib/utilities/amount/amount_unit.dart create mode 100644 test/utilities/amount/amount_unit_test.dart diff --git a/lib/utilities/amount/amount_unit.dart b/lib/utilities/amount/amount_unit.dart new file mode 100644 index 000000000..2dd64202c --- /dev/null +++ b/lib/utilities/amount/amount_unit.dart @@ -0,0 +1,120 @@ +import 'dart:math' as math; + +import 'package:decimal/decimal.dart'; +import 'package:intl/number_symbols.dart'; +import 'package:intl/number_symbols_data.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; + +enum AmountUnit { + normal(0), + milli(3), + micro(6), + nano(9), + pico(12), + femto(15), + atto(18), + ; + + const AmountUnit(this.shift); + final int shift; +} + +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) { + return "n${coin.ticker}"; + } else { + return "sats"; + } + case AmountUnit.pico: + if (coin == Coin.ethereum) { + return "mwei"; + } else if (coin == Coin.wownero || coin == Coin.monero) { + return "p${coin.ticker}"; + } else { + return "invalid"; + } + case AmountUnit.femto: + if (coin == Coin.ethereum) { + return "kwei"; + } else { + return "invalid"; + } + case AmountUnit.atto: + if (coin == Coin.ethereum) { + return "wei"; + } else { + return "invalid"; + } + } + } + + String displayAmount({ + required Amount amount, + required String locale, + required Coin coin, + required int maxDecimalPlaces, + }) { + 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(); + + // 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 + final int actualDecimalPlaces = math.min(places, maxDecimalPlaces); + + // get remainder string without the prepending "0." + String remainder = fraction.toString().substring(2); + + if (remainder.length > actualDecimalPlaces) { + // 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 = + (numberFormatSymbols[locale] as NumberSymbols?)?.DECIMAL_SEP ?? + (numberFormatSymbols[locale.substring(0, 2)] as NumberSymbols?) + ?.DECIMAL_SEP ?? + "."; + + // append separator and fractional amount + returnValue += "$separator$remainder"; + } + + // return the value with the proper unit symbol + return "$returnValue ${unitForCoin(coin)}"; + } +} diff --git a/test/utilities/amount/amount_unit_test.dart b/test/utilities/amount/amount_unit_test.dart new file mode 100644 index 000000000..2dcd5125c --- /dev/null +++ b/test/utilities/amount/amount_unit_test.dart @@ -0,0 +1,144 @@ +import 'package:decimal/decimal.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/amount/amount_unit.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; + +void main() { + test("displayAmount BTC", () { + final Amount amount = Amount( + rawValue: BigInt.from(1012345678), + fractionDigits: 8, + ); + + expect( + AmountUnit.normal.displayAmount( + amount: amount, + locale: "en_US", + coin: Coin.bitcoin, + maxDecimalPlaces: 8, + ), + "10.12345678 BTC", + ); + + expect( + AmountUnit.milli.displayAmount( + amount: amount, + locale: "en_US", + coin: Coin.bitcoin, + maxDecimalPlaces: 8, + ), + "10123.45678 mBTC", + ); + + expect( + AmountUnit.micro.displayAmount( + amount: amount, + locale: "en_US", + coin: Coin.bitcoin, + maxDecimalPlaces: 8, + ), + "10123456.78 µBTC", + ); + + expect( + AmountUnit.nano.displayAmount( + amount: amount, + locale: "en_US", + coin: Coin.bitcoin, + maxDecimalPlaces: 8, + ), + "1012345678 sats", + ); + final dec = Decimal.parse("10.123456789123456789"); + + expect(dec.toString(), "10.123456789123456789"); + }); + + test("displayAmount ETH", () { + final Amount amount = Amount.fromDecimal( + Decimal.parse("10.123456789123456789"), + fractionDigits: Coin.ethereum.decimals, + ); + + expect( + AmountUnit.normal.displayAmount( + amount: amount, + locale: "en_US", + coin: Coin.ethereum, + maxDecimalPlaces: 8, + ), + "10.12345678 ETH", + ); + + expect( + AmountUnit.normal.displayAmount( + amount: amount, + locale: "en_US", + coin: Coin.ethereum, + maxDecimalPlaces: 18, + ), + "10.123456789123456789 ETH", + ); + + expect( + AmountUnit.milli.displayAmount( + amount: amount, + locale: "en_US", + coin: Coin.ethereum, + maxDecimalPlaces: 9, + ), + "10123.456789123 mETH", + ); + + expect( + AmountUnit.micro.displayAmount( + amount: amount, + locale: "en_US", + coin: Coin.ethereum, + maxDecimalPlaces: 8, + ), + "10123456.78912345 µETH", + ); + + expect( + AmountUnit.nano.displayAmount( + amount: amount, + locale: "en_US", + coin: Coin.ethereum, + maxDecimalPlaces: 1, + ), + "10123456789.1 gwei", + ); + + expect( + AmountUnit.pico.displayAmount( + amount: amount, + locale: "en_US", + coin: Coin.ethereum, + maxDecimalPlaces: 18, + ), + "10123456789123.456789 mwei", + ); + + expect( + AmountUnit.femto.displayAmount( + amount: amount, + locale: "en_US", + coin: Coin.ethereum, + maxDecimalPlaces: 4, + ), + "10123456789123456.789 kwei", + ); + + expect( + AmountUnit.atto.displayAmount( + amount: amount, + locale: "en_US", + coin: Coin.ethereum, + maxDecimalPlaces: 1, + ), + "10123456789123456789 wei", + ); + }); +}