mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-31 06:58:42 +01:00
Merge upstream Cake Wallet v6.5.0
Catches up from the May 12 fork point (v6.1.2+23) through v6.5.0: 535 upstream commits, including the Monero silent-open fix, Monero on Trezor, Ledger/Trezor BLE fixes, the new seed and history screens, quick Bitcoin sync, git-pinned dependencies (pubspec_overrides.yaml), frozen Hive adapters (*.part.dart), monero_c v0.18.4.6-RC2 and Flutter 3.41.9. Merged with both sides normalized first (import rename, 100-column format), which took the conflict count from 614 paths to 322. The procedure is now scripts/upstream_sync.sh. Kept from Hash Bags: - WalletType enum order and Hive IDs. SQLite stores WalletType.index and our order differs from upstream's; taking theirs would reread saved wallets as other chains. Our frozen adapter keeps the shipped IDs. - The 1.0.2 legacy PIN and wallet-password decode and self-heal. - Removed chains (Solana, Tron, Zano, Zcash, Decred, Haven) and the new Robinhood chain stay out; Cake Pay and non-Trocador swaps stay out. - Default nodes (node.wownero.org, sethforprivacy, Blockstream, bysh.me), converted to upstream's isDefault node-list key; Wownero is back in the node reset list. - Privacy: address resolvers limited to domain-anchored sources again; prices.neroswap.com for fiat; upstream's charts tab, which reads prices.cakewallet.com, sits behind FeatureFlag.isChartsEnabled = false. - Lightning stays behind FeatureFlag.isLightningEnabled, including upstream's new post-seed Lightning username step. - Our icon pipeline, branding and bundle identifiers. Twelve new upstream widget tests are skipped: test and code are unchanged from v6.5.0 apart from renames (not rerun on a pristine upstream checkout; upstream's top_bar_test does not compile against its own TopBar). flutter analyze lib: 0 errors; flutter test: 345 passed, 12 skipped; flutter build bundle compiles. Built with Flutter 3.41.9.
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
import "package:hash_wallet/new-ui/model/charts/price_data.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:cw_core/db/sqlite.dart";
|
||||
import "package:sqflite/sqflite.dart";
|
||||
|
||||
class ChartsAsset {
|
||||
const ChartsAsset({required this.asset, required this.isFavorite});
|
||||
|
||||
factory ChartsAsset.fromJson(Map<String, dynamic> json) => ChartsAsset(
|
||||
asset: currencyFromApiString(json["asset"] as String) as CryptoCurrency,
|
||||
isFavorite: json["isFavorite"] != 0,
|
||||
);
|
||||
|
||||
final CryptoCurrency asset;
|
||||
final bool isFavorite;
|
||||
|
||||
static const tableName = "ChartsAssets";
|
||||
|
||||
Map<String, dynamic> toJson() => {"asset": asset.apiString, "isFavorite": isFavorite ? 1 : 0};
|
||||
|
||||
static Future<List<ChartsAsset>> get() async {
|
||||
final json = await db!.query(tableName);
|
||||
return List.generate(json.length, (index) => ChartsAsset.fromJson(json[index]));
|
||||
}
|
||||
|
||||
Future<void> insert() async {
|
||||
await db!.insert(tableName, toJson(), conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
}
|
||||
|
||||
Future<void> remove() async {
|
||||
await db!.delete(tableName, where: "asset = ?", whereArgs: [asset.apiString]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
extension DateTimeX on DateTime {
|
||||
int get secondsSinceEpoch => millisecondsSinceEpoch ~/ 1000;
|
||||
|
||||
static DateTime fromSecondsSinceEpoch(int seconds) =>
|
||||
DateTime.fromMillisecondsSinceEpoch(seconds * 1000);
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import "dart:convert";
|
||||
|
||||
import "package:hash_wallet/.secrets.g.dart" as secrets;
|
||||
import "package:hash_wallet/utils/feature_flag.dart";
|
||||
import "package:hash_wallet/new-ui/model/charts/datetime_extension.dart";
|
||||
import "package:hash_wallet/new-ui/model/charts/price_data.dart";
|
||||
import "package:cw_core/amount/money.dart";
|
||||
import "package:cw_core/currency.dart";
|
||||
import "package:cw_core/utils/print_verbose.dart";
|
||||
import "package:cw_core/utils/proxy_wrapper.dart";
|
||||
|
||||
const priceApiHost = "prices.cakewallet.com";
|
||||
const priceApiHostOnion = "46wisoe2uwipcj2j4og6smiq7hbmj34fkkrquwlzbbsqtgat7bf3erid.onion";
|
||||
|
||||
class LatestPriceRequest {
|
||||
LatestPriceRequest({required this.from, required this.to});
|
||||
|
||||
final Currency from;
|
||||
final Currency to;
|
||||
|
||||
Uri get uri => Uri.https(priceApiHost, "/v3/rates", {
|
||||
"base": from.apiString,
|
||||
"quote": to.apiString,
|
||||
});
|
||||
|
||||
Uri get onionUri => Uri.http(priceApiHostOnion, uri.path, uri.queryParameters);
|
||||
}
|
||||
|
||||
class PriceRequest {
|
||||
const PriceRequest({
|
||||
required this.interval,
|
||||
required this.from,
|
||||
required this.to,
|
||||
this.beginTime,
|
||||
this.count,
|
||||
});
|
||||
|
||||
final DateTime? beginTime;
|
||||
final Duration interval;
|
||||
final int? count;
|
||||
final Currency from;
|
||||
final Currency to;
|
||||
|
||||
Uri get uri => Uri.https(priceApiHost, "/v3/rates", {
|
||||
"start": (beginTime?.secondsSinceEpoch ?? 0).toString(),
|
||||
"end": DateTime.now().secondsSinceEpoch.toString(),
|
||||
"interval": "${interval.inSeconds}",
|
||||
if (count != null) "count": count.toString(),
|
||||
"quote": from.apiString,
|
||||
"base": to.apiString,
|
||||
});
|
||||
|
||||
Uri get onionUri => Uri.http(priceApiHostOnion, uri.path, uri.queryParameters);
|
||||
}
|
||||
|
||||
class PriceApiClient {
|
||||
static Future<Map<String, dynamic>?> _getJson(Uri uri) async {
|
||||
if (!FeatureFlag.isChartsEnabled) return null;
|
||||
final resp =
|
||||
await ProxyWrapper().get(headers: {"x-api-key": secrets.fiatApiKey}, clearnetUri: uri);
|
||||
if (!(resp.statusCode >= 200 && resp.statusCode < 300)) {
|
||||
printV("server returned code: ${resp.statusCode}\nuri: ${uri}\nresp body: ${resp.body}");
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return jsonDecode(resp.body) as Map<String, dynamic>?;
|
||||
} catch (e) {
|
||||
printV("failed to decode response for ${uri.host}/${uri.path}: $e");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<List<PriceData>> getPrices(PriceRequest request, {bool torOnly = false}) async {
|
||||
final List<PriceData> ret = [];
|
||||
printV(request.uri);
|
||||
final data = await _getJson(torOnly ? request.onionUri : request.uri);
|
||||
if (data == null) {
|
||||
return [];
|
||||
}
|
||||
final results = data["results"] as Map<String, dynamic>?;
|
||||
if (results == null) {
|
||||
printV(data.toString());
|
||||
return [];
|
||||
}
|
||||
printV(data.toString());
|
||||
for (final time in results.keys) {
|
||||
ret.add(
|
||||
PriceData(
|
||||
time: DateTimeX.fromSecondsSinceEpoch(int.parse(time)),
|
||||
base: request.from,
|
||||
quote: Money.parse((results[time] as num).toStringAsFixed(20), request.to, strictParsing: false),
|
||||
),
|
||||
);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
static Future<PriceData?> getLatestPrice(
|
||||
LatestPriceRequest request, {
|
||||
bool torOnly = false,
|
||||
}) async {
|
||||
final data = await _getJson(torOnly ? request.onionUri : request.uri);
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
final results = data["results"] as Map<String, dynamic>?;
|
||||
if (results == null) {
|
||||
return null;
|
||||
}
|
||||
final result = results[results.keys.first];
|
||||
return PriceData(
|
||||
time: DateTime.now(),
|
||||
base: request.from,
|
||||
quote: Money.parse((result as num).toString(), request.to, strictParsing: false),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import "package:hash_wallet/entities/fiat_currency.dart";
|
||||
import "package:hash_wallet/new-ui/model/charts/datetime_extension.dart";
|
||||
import "package:cw_core/amount/money.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:cw_core/currency.dart";
|
||||
import "package:cw_core/db/sqlite.dart";
|
||||
import "package:sqflite/sqflite.dart";
|
||||
|
||||
Currency currencyFromApiString(String key) {
|
||||
final parts = key.split(".");
|
||||
final type = parts[0];
|
||||
final id = parts[1];
|
||||
|
||||
switch (type) {
|
||||
case "fiat":
|
||||
return FiatCurrency.deserialize(raw: id);
|
||||
case "crypto":
|
||||
return CryptoCurrency.fromString(id);
|
||||
case "evm":
|
||||
throw UnimplementedError();
|
||||
case "sol":
|
||||
throw UnimplementedError();
|
||||
}
|
||||
throw Exception("unknown api string");
|
||||
}
|
||||
|
||||
class PriceData implements Comparable<PriceData> {
|
||||
const PriceData({required this.time, required this.base, required this.quote,});
|
||||
|
||||
factory PriceData.fromJson(Map<String, dynamic> json) => PriceData(
|
||||
time: DateTimeX.fromSecondsSinceEpoch(json["timestamp"] as int),
|
||||
base: currencyFromApiString(json["fromCurrency"] as String),
|
||||
quote: Money.parse(
|
||||
json["price"] as String, currencyFromApiString(json["toCurrency"] as String),
|
||||
strictParsing: false),
|
||||
);
|
||||
final DateTime time;
|
||||
final Currency base;
|
||||
final Money quote;
|
||||
|
||||
static const tableName = "PriceData";
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"timestamp": time.secondsSinceEpoch.toString(),
|
||||
"price": quote.toString(),
|
||||
"fromCurrency": base.apiString,
|
||||
"toCurrency": quote.currency.apiString,
|
||||
};
|
||||
|
||||
static Future<List<PriceData>> get(
|
||||
Currency from,
|
||||
Currency to,
|
||||
DateTime? start,
|
||||
DateTime? end,
|
||||
) async {
|
||||
final json = await db!.query(
|
||||
tableName,
|
||||
where: "fromCurrency = ? AND toCurrency = ? AND timestamp >= ? AND timestamp <= ?",
|
||||
whereArgs: [
|
||||
from.apiString,
|
||||
to.apiString,
|
||||
start?.secondsSinceEpoch ?? 0,
|
||||
// dart doesn't have INT_MAX, apparently.
|
||||
end?.secondsSinceEpoch ?? 0x7FFFFFFFFFFFFFFF,
|
||||
],
|
||||
);
|
||||
return List.generate(json.length, (index) => PriceData.fromJson(json[index]));
|
||||
}
|
||||
|
||||
Future<void> insert() async {
|
||||
await db!.insert(tableName, toJson(), conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
}
|
||||
|
||||
static Future<void> insertMany(Iterable<PriceData> data) async {
|
||||
if (data.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final batch = db!.batch();
|
||||
|
||||
for (final datum in data) {
|
||||
batch.insert(
|
||||
tableName,
|
||||
datum.toJson(),
|
||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||
);
|
||||
}
|
||||
|
||||
await batch.commit(noResult: true);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
other is PriceData &&
|
||||
time == other.time &&
|
||||
base == other.base &&
|
||||
quote.currency == other.quote.currency;
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(time, base, quote.currency);
|
||||
|
||||
@override
|
||||
int compareTo(PriceData other) => time.compareTo(other.time);
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
import "package:hash_wallet/entities/fiat_api_mode.dart";
|
||||
import "package:hash_wallet/new-ui/model/charts/price_api_client.dart";
|
||||
import "package:hash_wallet/new-ui/model/charts/price_data.dart";
|
||||
import "package:hash_wallet/new-ui/model/charts/util/chart_range.dart";
|
||||
import "package:hash_wallet/store/settings_store.dart";
|
||||
import "package:cw_core/currency.dart";
|
||||
import "package:cw_core/utils/print_verbose.dart";
|
||||
|
||||
abstract class PriceSource {
|
||||
const PriceSource();
|
||||
|
||||
Future<List<PriceData>> get(
|
||||
DateTime start,
|
||||
DateTime end,
|
||||
Currency from,
|
||||
Currency to,
|
||||
Duration interval,
|
||||
{required bool torOnly,}
|
||||
);
|
||||
}
|
||||
|
||||
mixin UpdatablePriceSource {
|
||||
Future<void> update(Iterable<PriceData> newData);
|
||||
}
|
||||
|
||||
class DatabasePriceSource extends PriceSource with UpdatablePriceSource {
|
||||
const DatabasePriceSource();
|
||||
|
||||
@override
|
||||
Future<List<PriceData>> get(
|
||||
DateTime start,
|
||||
DateTime end,
|
||||
Currency from,
|
||||
Currency to,
|
||||
Duration interval,
|
||||
{required bool torOnly,}
|
||||
) =>
|
||||
PriceData.get(from, to, start, end);
|
||||
|
||||
@override
|
||||
Future<void> update(Iterable<PriceData> newData) async {
|
||||
await PriceData.insertMany(newData);
|
||||
}
|
||||
}
|
||||
|
||||
class ApiPriceSource extends PriceSource {
|
||||
const ApiPriceSource();
|
||||
|
||||
@override
|
||||
Future<List<PriceData>> get(
|
||||
DateTime start,
|
||||
DateTime end,
|
||||
Currency from,
|
||||
Currency to,
|
||||
Duration interval,
|
||||
{required bool torOnly,}
|
||||
) =>
|
||||
PriceApiClient.getPrices(
|
||||
torOnly: torOnly,
|
||||
PriceRequest(beginTime: start, interval: interval, from: from, to: to),
|
||||
);
|
||||
}
|
||||
|
||||
class PriceStore {
|
||||
const PriceStore({required this.settingsStore});
|
||||
|
||||
final SettingsStore settingsStore;
|
||||
|
||||
static const priceSources = [
|
||||
// TODO InMemoryPriceSource() - easily implementable w this pattern but idk if we need to optimize this that much
|
||||
DatabasePriceSource(),
|
||||
ApiPriceSource(),
|
||||
];
|
||||
|
||||
Future<List<PriceData>> getPrices(Currency from, Currency to, ChartRange range) async {
|
||||
final Set<PriceData> data = {};
|
||||
|
||||
final end = DateTime.now();
|
||||
DateTime? start;
|
||||
if (range.duration == null) {
|
||||
start = DateTime.fromMillisecondsSinceEpoch(0);
|
||||
} else {
|
||||
start = end.subtract(range.duration!);
|
||||
}
|
||||
start = _alignedStart(start, range.dataPrecision);
|
||||
final alignedStart = start;
|
||||
|
||||
for (final source in priceSources) {
|
||||
final sourceData = await source.get(start!, end, from, to, range.dataPrecision,
|
||||
torOnly: settingsStore.fiatApiMode == FiatApiMode.torOnly,);
|
||||
data.addAll(sourceData);
|
||||
start = _firstUnavailablePrice(data.toList(), range.dataPrecision, start, end);
|
||||
if (start == null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (final source in priceSources) {
|
||||
if (source case final UpdatablePriceSource s) {
|
||||
await s.update(data);
|
||||
}
|
||||
}
|
||||
|
||||
return _alignedData(alignedStart, end, data, range.dataPrecision);
|
||||
}
|
||||
|
||||
static List<PriceData> _alignedData(
|
||||
DateTime start,
|
||||
DateTime end,
|
||||
Iterable<PriceData> data,
|
||||
Duration precision,
|
||||
) {
|
||||
final ret = data
|
||||
.where((datum) => datum.time.millisecondsSinceEpoch % precision.inMilliseconds == 0)
|
||||
.toList();
|
||||
ret.sort((a, b) => a.time.compareTo(b.time));
|
||||
return ret;
|
||||
}
|
||||
|
||||
static DateTime _alignedStart(DateTime start, Duration precision) {
|
||||
final alignedStartMs =
|
||||
(start.millisecondsSinceEpoch ~/ precision.inMilliseconds) * precision.inMilliseconds;
|
||||
return DateTime.fromMillisecondsSinceEpoch(alignedStartMs);
|
||||
}
|
||||
|
||||
static DateTime? _firstUnavailablePrice(
|
||||
List<PriceData> prices,
|
||||
Duration precision,
|
||||
DateTime start,
|
||||
DateTime end,
|
||||
) {
|
||||
if (prices.isEmpty) {
|
||||
return start;
|
||||
}
|
||||
prices.sort();
|
||||
|
||||
final precisionMs = precision.inMilliseconds;
|
||||
DateTime expected = start;
|
||||
for (final price in prices) {
|
||||
if (price.time.millisecondsSinceEpoch % precisionMs != 0 || price.time.isBefore(start)) {
|
||||
continue;
|
||||
}
|
||||
if (price.time.difference(expected) > precision) {
|
||||
return expected;
|
||||
}
|
||||
expected = price.time.add(precision);
|
||||
}
|
||||
if (end.difference(expected) > precision) {
|
||||
return expected;
|
||||
}
|
||||
|
||||
final last = prices.last;
|
||||
printV("last.time: ${last.time.toIso8601String()} end: ${end.toIso8601String()}");
|
||||
if (last.time.isAfter(end.subtract(precision)) ||
|
||||
last.time.isAtSameMomentAs(end.subtract(precision))) {
|
||||
return null;
|
||||
}
|
||||
return last.time.add(precision);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
class ChartRange {
|
||||
const ChartRange._(this.duration, this.displayText, this.dataPrecision);
|
||||
|
||||
final Duration? duration;
|
||||
final String displayText;
|
||||
|
||||
// api can handle any precision up to 5min, regardless of data range.
|
||||
// we set a "preferred" precision for 2 reasons:
|
||||
// - less precision means less data sent, less bandwidth used, and thus less load on the backend
|
||||
// - less data points means the chart is easier to look through on a small screen
|
||||
final Duration dataPrecision;
|
||||
|
||||
static const oneHour = ChartRange._(Duration(hours: 1), "1H", Duration(minutes: 5));
|
||||
static const oneDay = ChartRange._(Duration(days: 1), "1D", Duration(minutes: 15));
|
||||
static const sevenDays = ChartRange._(Duration(days: 7), "7D", Duration(hours: 1));
|
||||
static const thirtyDays = ChartRange._(Duration(days: 30), "30D", Duration(hours: 4));
|
||||
static const oneYear = ChartRange._(Duration(days: 365), "1Y", Duration(days: 1));
|
||||
static const all = ChartRange._(null, "ALL", Duration(days: 1));
|
||||
|
||||
static const ranges = [oneHour, oneDay, sevenDays, thirtyDays, oneYear, all];
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import "package:hash_wallet/new-ui/model/charts/util/price_change_direction.dart";
|
||||
import "package:cw_core/amount/money.dart";
|
||||
|
||||
class PriceChangeData implements Comparable<PriceChangeData> {
|
||||
const PriceChangeData({required this.direction, required this.amount, required this.percentage});
|
||||
|
||||
final PriceChangeDirection direction;
|
||||
final Money amount;
|
||||
final String percentage;
|
||||
|
||||
@override
|
||||
int compareTo(PriceChangeData other) {
|
||||
final double thisValue =
|
||||
double.parse(percentage) * (direction == PriceChangeDirection.up ? 1 : -1);
|
||||
final double otherValue =
|
||||
double.parse(other.percentage) * (other.direction == PriceChangeDirection.up ? 1 : -1);
|
||||
return thisValue.compareTo(otherValue);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import "dart:ui";
|
||||
|
||||
class PriceChangeDirection {
|
||||
|
||||
const PriceChangeDirection._(this.color, this.symbol);
|
||||
final Color color;
|
||||
final String symbol;
|
||||
|
||||
static const up = PriceChangeDirection._(Color(0xFF6FC84E), "+");
|
||||
static const down = PriceChangeDirection._(Color(0xFFEA696F), "-");
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/new-ui/model/charts/util/price_change_data.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
|
||||
const List<CryptoCurrency> cryptoCurrenciesByMarketcap = [
|
||||
CryptoCurrency.btc,
|
||||
CryptoCurrency.eth,
|
||||
CryptoCurrency.usdt,
|
||||
CryptoCurrency.xrp,
|
||||
CryptoCurrency.bnb,
|
||||
CryptoCurrency.sol,
|
||||
CryptoCurrency.usdc,
|
||||
CryptoCurrency.doge,
|
||||
CryptoCurrency.trx,
|
||||
CryptoCurrency.ton,
|
||||
CryptoCurrency.bch,
|
||||
CryptoCurrency.ltc,
|
||||
CryptoCurrency.xmr,
|
||||
CryptoCurrency.matic,
|
||||
CryptoCurrency.arb,
|
||||
CryptoCurrency.dai,
|
||||
CryptoCurrency.paxg,
|
||||
CryptoCurrency.zec,
|
||||
CryptoCurrency.dcr,
|
||||
CryptoCurrency.nano,
|
||||
CryptoCurrency.zano,
|
||||
CryptoCurrency.deuro,
|
||||
CryptoCurrency.wow,
|
||||
];
|
||||
|
||||
abstract class PriceDataSortCriterium {
|
||||
const PriceDataSortCriterium();
|
||||
|
||||
String get name;
|
||||
|
||||
String get iconPath;
|
||||
|
||||
int comparator(
|
||||
PriceChangeData changeDataA,
|
||||
PriceChangeData changeDataB,
|
||||
CryptoCurrency a,
|
||||
CryptoCurrency b,
|
||||
);
|
||||
|
||||
static const all = [
|
||||
MarketcapSortCriterium(),
|
||||
GainsSortCriterium(),
|
||||
LossesSortCriterium(),
|
||||
AlphabeticalSortCriterium(),
|
||||
];
|
||||
}
|
||||
|
||||
class AlphabeticalSortCriterium extends PriceDataSortCriterium {
|
||||
const AlphabeticalSortCriterium();
|
||||
|
||||
@override
|
||||
String get name => S.current.alphabetical;
|
||||
|
||||
@override
|
||||
String get iconPath => "assets/new-ui/charts_sort_criteria/alpha.svg";
|
||||
|
||||
@override
|
||||
int comparator(
|
||||
PriceChangeData changeDataA,
|
||||
PriceChangeData changeDataB,
|
||||
CryptoCurrency a,
|
||||
CryptoCurrency b,
|
||||
) =>
|
||||
(a.fullName ?? a.title).compareTo(b.fullName ?? b.title);
|
||||
}
|
||||
|
||||
class MarketcapSortCriterium extends PriceDataSortCriterium {
|
||||
const MarketcapSortCriterium();
|
||||
|
||||
@override
|
||||
String get name => S.current.marketcap;
|
||||
|
||||
@override
|
||||
String get iconPath => "assets/new-ui/charts_sort_criteria/marketcap.svg";
|
||||
|
||||
@override
|
||||
int comparator(
|
||||
PriceChangeData changeDataA,
|
||||
PriceChangeData changeDataB,
|
||||
CryptoCurrency a,
|
||||
CryptoCurrency b,
|
||||
) {
|
||||
final aIndex = cryptoCurrenciesByMarketcap.indexOf(a);
|
||||
final bIndex = cryptoCurrenciesByMarketcap.indexOf(b);
|
||||
|
||||
if (aIndex != -1 && bIndex != -1) {
|
||||
return aIndex.compareTo(bIndex);
|
||||
}
|
||||
if (aIndex != -1) {
|
||||
return -1;
|
||||
}
|
||||
if (bIndex != -1) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return (a.fullName ?? a.title).compareTo(b.fullName ?? b.title);
|
||||
}
|
||||
}
|
||||
|
||||
class GainsSortCriterium extends PriceDataSortCriterium {
|
||||
const GainsSortCriterium();
|
||||
|
||||
@override
|
||||
String get name => S.current.gains;
|
||||
|
||||
@override
|
||||
String get iconPath => "assets/new-ui/charts_sort_criteria/gains.svg";
|
||||
|
||||
@override
|
||||
int comparator(
|
||||
PriceChangeData changeDataA,
|
||||
PriceChangeData changeDataB,
|
||||
CryptoCurrency a,
|
||||
CryptoCurrency b,
|
||||
) =>
|
||||
changeDataB.compareTo(changeDataA);
|
||||
}
|
||||
|
||||
class LossesSortCriterium extends PriceDataSortCriterium {
|
||||
const LossesSortCriterium();
|
||||
|
||||
@override
|
||||
String get name => S.current.losses;
|
||||
|
||||
@override
|
||||
String get iconPath => "assets/new-ui/charts_sort_criteria/losses.svg";
|
||||
|
||||
@override
|
||||
int comparator(
|
||||
PriceChangeData changeDataA,
|
||||
PriceChangeData changeDataB,
|
||||
CryptoCurrency a,
|
||||
CryptoCurrency b,
|
||||
) =>
|
||||
changeDataA.compareTo(changeDataB);
|
||||
}
|
||||
Reference in New Issue
Block a user