Files
hash-wallet/cw_core/lib/amount/money.dart
T
b88fbf32ae charts (#3162)
* charts (wip)

* charts (wip)

* wip

* add asset grid

* split files

* logic wip

* proper getPrices logic

* viewmodel (wip)

* charts

* remove unused import

* remove restricted import...?

* fix background gradient when scrolling

* add safeguards for broken db

* save new pin on removal

* add safeguards for concurrency

* add haptic feedback

* fix haptic feedback

* fix fiat ticker

* reduce chart padding

* add line touch indicator

* improve touch target size on range selector

* add date/time display when viewing past amount

* merge

* remove iconSvgPath reference

* merge

* fix state for date display

* merge

* auto-reformat

* only load charts when page is opened

* merge

* reformat and apply lints

* refactor to use `Money` instead of storing amount in `String`

* add slop comment

* fix exception on division by zero

* ci

* workaround toDouble being removed

* disable strict parsing and rounding

* fix abs()

* use new currency picker sheet

* safeguard for max flutter-supported decimals

* fix amount precision

* adjust to new buy/sell

* Update lib/new-ui/pages/charts_page.dart

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* adjust buy/sell di

* replace -> ignore

* reformat

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
2026-09-22 21:32:38 +03:00

317 lines
9.6 KiB
Dart

import "dart:math";
import "package:cw_core/amount/utils.dart";
import "package:cw_core/crypto_amount_format.dart";
import "package:cw_core/crypto_currency.dart";
import "package:cw_core/currency.dart";
import "package:cw_core/format_fixed.dart";
import "package:cw_core/parse_fixed.dart";
export "money_local.dart";
class Money implements Comparable<Money> {
const Money(this.amount, this.currency, [int? overrideDecimals])
: _overrideDecimals = overrideDecimals;
factory Money.zero(Currency currency) => Money(BigInt.zero, currency);
factory Money.fromInt(int amount, Currency currency) => Money(BigInt.from(amount), currency);
/// Parse the [source] and turn it into [Money] trimming trailing 0s
///
/// Throws a [FormatException] if the [source] is not a valid decimal or
/// not in canonical representation or if it is a decimal when [isBaseUnit]
factory Money.parse(
String source,
Currency currency, {
bool isBaseUnit = false,
bool strictParsing = true,
}) {
if (!isBaseUnit) {
source = trimTrailingFractionZeros(source);
}
final decimals = strictParsing ? currency.decimals : _getActualDecimals(source, currency);
final amount = isBaseUnit ? BigInt.parse(source) : parseFixed(source, decimals);
return Money(amount, currency, decimals);
}
/// Parse the [source] and turn it into [Money] if possible trimming trailing 0s
///
/// As [parse] except that this method returns `null` if the input is not
/// valid or if it is a decimal when [isBaseUnit]
static Money? tryParse(
String source,
Currency currency, {
bool isBaseUnit = false,
bool strictParsing = true,
}) {
try {
if (!isBaseUnit) {
source = trimTrailingFractionZeros(source);
}
final decimals = strictParsing ? currency.decimals : _getActualDecimals(source, currency);
final amount = isBaseUnit ? BigInt.tryParse(source) : tryParseFixed(source, decimals);
return amount != null ? Money(amount, currency, decimals) : null;
} catch (_) {
return null;
}
}
static Money? trySafeParse(source, Currency currency, {bool isBaseUnit = false}) {
final amount = isBaseUnit
? BigInt.tryParse(source.toString())
: tryParseFixed(source.toString().withDecimals(currency.decimals), currency.decimals);
return amount != null ? Money(amount, currency) : null;
}
final BigInt amount;
final Currency currency;
final int? _overrideDecimals;
/// Returns the amount of decimals of [currency]
int get decimals => _overrideDecimals ?? currency.decimals;
/// Returns the sign of this [BigInt] amount.
/// Returns 0 for zero, -1 for values less than zero and +1 for values
/// greater than zero.
int get sign => amount.sign;
/// Returns `true` when amount of this money is zero.
bool get isZero => amount == BigInt.zero;
/// Returns `true` when amount of this money is negative.
bool get isNegative => amount.isNegative;
/// Compares this to [other].
///
/// [other] has to be in same currency, [ArgumentError] will be thrown
/// otherwise.
@override
int compareTo(Money other) {
_assertSameCurrency(other);
final aligned = _align(other);
return aligned.a.compareTo(aligned.b);
}
/// Returns `true` if [other] is the same amount of money in
/// the same currency.
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (other is! Money || other.currency != currency) {
return false;
}
final aligned = _align(other);
return aligned.a == aligned.b;
}
/// Returns `true` when this money is less than [other].
///
/// Both operands have to be in same currency, [ArgumentError] will be thrown
/// otherwise.
bool operator <(Money other) {
_assertSameCurrency(other, "Cannot compare money in different currencies.");
final aligned = _align(other);
return aligned.a < aligned.b;
}
/// Returns `true` when this money is less than or equal to [other].
///
/// Both operands have to be in same currency, [ArgumentError] will be thrown
/// otherwise.
bool operator <=(Money other) {
_assertSameCurrency(other, "Cannot compare money in different currencies.");
final aligned = _align(other);
return aligned.a <= aligned.b;
}
/// Returns `true` when this money is greater than [other].
///
/// Both operands have to be in same currency, [ArgumentError] will be thrown
/// otherwise.
bool operator >(Money other) {
_assertSameCurrency(other, "Cannot compare money in different currencies.");
final aligned = _align(other);
return aligned.a > aligned.b;
}
/// Returns `true` when this money is greater than or equal to [other].
///
/// Both operands have to be in same currency, [ArgumentError] will be thrown
/// otherwise.
bool operator >=(Money other) {
_assertSameCurrency(other, "Cannot compare money in different currencies.");
final aligned = _align(other);
return aligned.a >= aligned.b;
}
/// Adds the amount of [other] to this amount.
///
/// Both operands must be in same currency, [ArgumentError] will be thrown
/// otherwise.
Money operator +(Money other) {
_assertSameCurrency(other);
final aligned = _align(other);
return copyWith(amount: aligned.a + aligned.b, decimals: aligned.decimals);
}
/// unary minus operator.
Money operator -() => copyWith(amount: -amount);
/// Subtracts the amount of [other] from this amount.
///
/// Both operands must be in same currency, [ArgumentError] will be thrown
/// otherwise.
Money operator -(Money other) {
_assertSameCurrency(other);
final aligned = _align(other);
return copyWith(amount: aligned.a - aligned.b, decimals: aligned.decimals);
}
/// Returns [Money] multiplied by [other].
///
/// The result is again [Money].
Money operator *(BigInt other) => copyWith(amount: amount * other);
/// Returns [Money] divided by [other].
///
/// The result is again [Money].
Money operator /(BigInt other) {
if (other == BigInt.zero) {
throw Exception("Division by zero.");
}
final neg = (amount.isNegative) ^ (other.isNegative);
final A = amount.abs();
final B = other.abs();
final q = A ~/ B;
final r = A % B;
// Compare 2*r with B to detect half/up/down
final twiceR = r << 1;
final mag = (twiceR >= B) ? q + BigInt.one : q;
return copyWith(amount: neg ? -mag : mag);
}
/// Creates a copy of this [Money] object with optional new values
/// for [amount], [currency].
///
/// If just [currency] is provided and the decimals missmatch
/// the amount will be transformed to keep its canonical representation
Money copyWith({BigInt? amount, Currency? currency, int? decimals}) {
if (currency != null && amount == null && decimals == null &&
currency.decimals != this.decimals) {
return Money(
_transformAmount(this.amount, this.decimals, currency.decimals),
currency,
currency.decimals,
);
}
return Money(amount ?? this.amount, currency ?? this.currency, decimals ?? this.decimals);
}
void _assertSameCurrency(Money other, [String? message]) {
if (currency != other.currency) {
throw ArgumentError(message ?? "Cannot operate with money values in different currencies.");
}
}
static BigInt _transformAmount(BigInt source, int sourceDecimals, int targetDecimals) {
final diff = targetDecimals - sourceDecimals;
return diff == 0
? source
: diff > 0
? source * BigInt.from(10).pow(diff)
: source ~/ BigInt.from(10).pow(-diff);
}
({BigInt a, BigInt b, int decimals}) _align(Money other) {
final decimals = max(this.decimals, other.decimals);
return (
a: _transformAmount(amount, this.decimals, decimals),
b: _transformAmount(other.amount, other.decimals, decimals),
decimals: decimals,
);
}
static int _getActualDecimals(String value, Currency currency) {
final comps = value.split(".");
if (comps.length > 2) {
throw FormatException("Money._getActualDecimals: too many decimal points, value, $value");
}
return max(comps.length == 2 ? comps[1].length : 0, currency.decimals);
}
@override
int get hashCode {
final value = formatFixed(amount, this.decimals, trimZeros: true);
final decimals = _getActualDecimals(value, currency);
return Object.hash(value, decimals, currency);
}
@override
String toString() => formatFixed(amount, decimals);
String toStringWithSymbol({
int? fractionalDigits,
bool trimZeros = true,
bool useBaseUnit = false,
bool withSymbolPrefix = false,
}) {
final amount = toStringWithPrecision(
fractionalDigits: fractionalDigits,
trimZeros: trimZeros,
useBaseUnit: useBaseUnit,
);
final symbol = getSymbol(useBaseUnit: useBaseUnit);
return withSymbolPrefix ? "$symbol $amount" : "$amount $symbol";
}
String toStringWithPrecision({
int? fractionalDigits,
bool trimZeros = true,
bool useBaseUnit = false,
}) =>
formatFixed(
decimals != currency.decimals
? _transformAmount(amount, decimals, currency.decimals)
: amount,
useBaseUnit ? 0 : currency.decimals,
fractionalDigits: fractionalDigits,
trimZeros: trimZeros,
);
// To Override the symbol with the ticker of the base unit
String getSymbol({required bool useBaseUnit}) {
if (useBaseUnit && [CryptoCurrency.btc, CryptoCurrency.btcln].contains(currency)) {
return "sats";
}
return currency.symbol;
}
/// new [Money] with absolute value of this instance
Money abs() => copyWith(amount: amount.abs());
}