import 'dart:isolate'; import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:flutter/material.dart'; import 'package:mobx/mobx.dart'; import 'package:sqflite/sqflite.dart'; import 'coin/coins.dart'; import 'package:warp_api/warp_api.dart'; import 'package:warp_api/types.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:http/http.dart' as http; import 'dart:convert' as convert; import 'package:flex_color_scheme/flex_color_scheme.dart'; import 'package:sensors_plus/sensors_plus.dart'; import 'coin/coin.dart'; import 'generated/l10n.dart'; import 'main.dart'; part 'store.g.dart'; class LWDServer { final int coin; final CoinBase coinDef; late String choice; late String customUrl; LWDServer(this.coin, this.coinDef) { choice = coinDef.lwd.first.name; customUrl = coinDef.lwd.first.url; } Future loadPrefs() async { final prefs = await SharedPreferences.getInstance(); final ticker = coinDef.ticker; final _choice = prefs.getString('$ticker.lwd_choice'); final _customUrl = prefs.getString('$ticker.lwd_custom'); if (_choice != null && _customUrl != null) { choice = _choice; customUrl = _customUrl; } else { await savePrefs(choice, customUrl); } return getLWDUrl(); } Future savePrefs(String _choice, String _customUrl) async { choice = _choice; customUrl = _customUrl; final ticker = coinDef.ticker; final prefs = await SharedPreferences.getInstance(); prefs.setString('$ticker.lwd_choice', _choice); prefs.setString('$ticker.lwd_custom', _customUrl); } String getLWDUrl() { final url; if (choice == "custom") url = customUrl; else { final lwd = coinDef.lwd.firstWhere((lwd) => lwd.name == choice, orElse: () => coinDef.lwd.first); url = lwd.url; } return url; } } class CoinData = _CoinData with _$CoinData; abstract class _CoinData with Store { final int coin; int active = 0; int syncedHeight = 0; final CoinBase def; @observable bool contactsSaved = true; _CoinData(this.coin, this.def); } class Settings = _Settings with _$Settings; abstract class _Settings with Store { @observable bool simpleMode = true; List servers = [LWDServer(0, zcash), LWDServer(1, ycash)]; List coins = [CoinData(0, zcash), CoinData(1, ycash)]; @observable int anchorOffset = 10; @observable bool getTx = true; @observable int rowsPerPage = 10; @observable String theme = ""; @observable String themeBrightness = ""; @observable ThemeData themeData = ThemeData.light(); @observable bool showConfirmations = false; @observable String currency = "USD"; @observable List currencies = ["USD"]; @observable String chartRange = '1Y'; @observable bool shieldBalance = false; @observable double autoShieldThreshold = 0.0; @observable bool useUA = false; @observable bool autoHide = true; @observable bool protectSend = false; @observable int primaryColorValue = 0; @observable int primaryVariantColorValue = 0; @observable int secondaryColorValue = 0; @observable int secondaryVariantColorValue = 0; @observable String? memoSignature; @observable bool flat = false; @action Future restore() async { final prefs = await SharedPreferences.getInstance(); simpleMode = prefs.getBool('simple_mode') ?? true; anchorOffset = prefs.getInt('anchor_offset') ?? 3; getTx = prefs.getBool('get_txinfo') ?? true; rowsPerPage = prefs.getInt('rows_per_age') ?? 10; theme = prefs.getString('theme') ?? "gold"; themeBrightness = prefs.getString('theme_brightness') ?? "dark"; showConfirmations = prefs.getBool('show_confirmations') ?? false; currency = prefs.getString('currency') ?? "USD"; chartRange = prefs.getString('chart_range') ?? "1Y"; shieldBalance = prefs.getBool('shield_balance') ?? false; autoShieldThreshold = prefs.getDouble('autoshield_threshold') ?? 0.0; useUA = prefs.getBool('use_ua') ?? false; autoHide = prefs.getBool('auto_hide') ?? true; protectSend = prefs.getBool('protect_send') ?? false; primaryColorValue = prefs.getInt('primary') ?? Colors.blue.value; primaryVariantColorValue = prefs.getInt('primary.variant') ?? Colors.blueAccent.value; secondaryColorValue = prefs.getInt('secondary') ?? Colors.green.value; secondaryVariantColorValue = prefs.getInt('secondary.variant') ?? Colors.greenAccent.value; memoSignature = prefs.getString('memo_signature'); for (var s in servers) { await s.loadPrefs(); } for (var c in coins) { final ticker = c.def.ticker; c.active = prefs.getInt("$ticker.active") ?? 0; c.contactsSaved = prefs.getBool("$ticker.contacts_saved") ?? true; } _updateThemeData(); Future.microtask(_loadCurrencies); // lazily if (isMobile()) accelerometerEvents.listen(_handleAccel); return true; } @action void _handleAccel(event) { final n = sqrt(event.x * event.x + event.y * event.y + event.z * event.z); final inclination = acos(event.z / n) / pi * 180 * event.y.sign; final _flat = inclination < 20; if (flat != _flat) flat = _flat; } @action Future setMode(bool simple) async { final prefs = await SharedPreferences.getInstance(); prefs.setBool('simple_mode', simple); simpleMode = simple; } void updateLWD() async { for (var s in servers) { final url = await s.loadPrefs(); WarpApi.updateLWD(s.coin, url); } } @action Future setAnchorOffset(int offset) async { final prefs = await SharedPreferences.getInstance(); anchorOffset = offset; prefs.setInt('anchor_offset', offset); } @action Future setTheme(String thm) async { final prefs = await SharedPreferences.getInstance(); theme = thm; prefs.setString('theme', thm); _updateThemeData(); } @action Future setThemeBrightness(String brightness) async { final prefs = await SharedPreferences.getInstance(); themeBrightness = brightness; prefs.setString('theme_brightness', brightness); _updateThemeData(); } void _updateThemeData() { if (theme == 'custom') { final colors = FlexSchemeColor( primary: Color(primaryColorValue), primaryVariant: Color(primaryVariantColorValue), secondary: Color(secondaryColorValue), secondaryVariant: Color(secondaryVariantColorValue)); final scheme = FlexSchemeData( name: 'custom', description: 'Custom Theme', light: colors, dark: colors); switch (themeBrightness) { case 'light': themeData = FlexColorScheme.light(colors: scheme.light).toTheme; break; case 'dark': themeData = FlexColorScheme.dark(colors: scheme.dark).toTheme; break; } } else { FlexScheme scheme; switch (theme) { case 'gold': scheme = FlexScheme.mango; break; case 'blue': scheme = FlexScheme.bahamaBlue; break; case 'pink': scheme = FlexScheme.sakura; break; case 'purple': scheme = FlexScheme.deepPurple; break; default: scheme = FlexScheme.mango; } switch (themeBrightness) { case 'light': themeData = FlexColorScheme.light(scheme: scheme).toTheme; break; case 'dark': themeData = FlexColorScheme.dark(scheme: scheme).toTheme; break; } } } @action Future updateCustomThemeColors(Color primary, Color primaryVariant, Color secondary, Color secondaryVariant) async { final prefs = await SharedPreferences.getInstance(); primaryColorValue = primary.value; primaryVariantColorValue = primaryVariant.value; secondaryColorValue = secondary.value; secondaryVariantColorValue = secondaryVariant.value; prefs.setInt('primary', primaryColorValue); prefs.setInt('primary.variant', primaryVariantColorValue); prefs.setInt('secondary', secondaryColorValue); prefs.setInt('secondary.variant', secondaryVariantColorValue); _updateThemeData(); } @action Future setChartRange(String v) async { final prefs = await SharedPreferences.getInstance(); chartRange = v; prefs.setString('chart_range', chartRange); active.fetchChartData(); } @action Future updateGetTx(bool v) async { final prefs = await SharedPreferences.getInstance(); getTx = v; prefs.setBool('get_txinfo', v); } @action Future setRowsPerPage(int v) async { final prefs = await SharedPreferences.getInstance(); rowsPerPage = v; prefs.setInt('rows_per_age', v); } @action Future toggleShowConfirmations() async { final prefs = await SharedPreferences.getInstance(); showConfirmations = !showConfirmations; prefs.setBool('show_confirmations', showConfirmations); } @action Future setCurrency(String newCurrency) async { final prefs = await SharedPreferences.getInstance(); currency = newCurrency; prefs.setString('currency', currency); await priceStore.fetchCoinPrice(active.coin); await active.fetchChartData(); } @action Future _loadCurrencies() async { final base = "api.coingecko.com"; final uri = Uri.https(base, '/api/v3/simple/supported_vs_currencies'); final rep = await http.get(uri); if (rep.statusCode == 200) { final _currencies = convert.jsonDecode(rep.body) as List; final c = _currencies.map((v) => (v as String).toUpperCase()).toList(); c.sort(); currencies = c; } } @action Future setShieldBalance(bool v) async { final prefs = await SharedPreferences.getInstance(); shieldBalance = v; prefs.setBool('shield_balance', shieldBalance); } @action Future setAutoShieldThreshold(double v) async { final prefs = await SharedPreferences.getInstance(); autoShieldThreshold = v; prefs.setDouble('autoshield_threshold', autoShieldThreshold); } @action Future setUseUA(bool v) async { final prefs = await SharedPreferences.getInstance(); useUA = v; prefs.setBool('use_ua', useUA); } @action Future setAutoHide(bool v) async { final prefs = await SharedPreferences.getInstance(); autoHide = v; prefs.setBool('auto_hide', autoHide); } @action Future setProtectSend(bool v) async { final prefs = await SharedPreferences.getInstance(); protectSend = v; prefs.setBool('protect_send', protectSend); } @action Future setMemoSignature(String v) async { final prefs = await SharedPreferences.getInstance(); memoSignature = v; prefs.setString('memo_signature', v); } } class PriceStore = _PriceStore with _$PriceStore; abstract class _PriceStore with Store { @observable double coinPrice = 0.0; int? lastChartUpdateTime; @action Future fetchCoinPrice(int coin) async { final c = settings.coins[coin].def; final base = "api.coingecko.com"; final uri = Uri.https(base, '/api/v3/simple/price', {'ids': c.currency, 'vs_currencies': settings.currency}); final rep = await http.get(uri); if (rep.statusCode == 200) { final json = convert.jsonDecode(rep.body) as Map; final p = json[c.currency][settings.currency.toLowerCase()]; coinPrice = (p is double) ? p : (p as int).toDouble(); } else coinPrice = 0.0; } Future updateChart() async { final _lastChartUpdateTime = lastChartUpdateTime; final now = DateTime.now().millisecondsSinceEpoch ~/ 1000; if (_lastChartUpdateTime == null || now > _lastChartUpdateTime + 5 * 60) { await fetchCoinPrice(active.coin); await WarpApi.syncHistoricalPrices(active.coin, settings.currency); await active.fetchChartData(); lastChartUpdateTime = _lastChartUpdateTime; } } } class SyncStatus = _SyncStatus with _$SyncStatus; abstract class _SyncStatus with Store { @observable int? syncedHeight; @observable int latestHeight = 0; bool accountRestored = false; bool syncing = false; bool isSynced() { final sh = syncedHeight; return sh != null && sh >= latestHeight; } int get confirmHeight { final ch = latestHeight - settings.anchorOffset; return max(ch, 0); } @action setSyncHeight(int? height) { syncedHeight = height; } @action void markAsSynced(int coin) { WarpApi.skipToLastHeight(coin); } Future getDbSyncedHeight() async { final db = active.coinDef.db; final syncedHeight = Sqflite.firstIntValue( await db.rawQuery("SELECT MAX(height) FROM blocks")); return syncedHeight; } @action Future update() async { latestHeight = await WarpApi.getLatestHeight(active.coin); final _syncedHeight = await getDbSyncedHeight(); // if syncedHeight = 0, we just started sync therefore don't set it back to null if (syncedHeight != 0 || _syncedHeight != null) setSyncHeight( _syncedHeight); return latestHeight > 0 && syncedHeight == latestHeight; } @action Future sync() async { if (syncing) return; await syncStatus.update(); if (syncedHeight == null) return; syncing = true; final currentSyncedHeight = syncedHeight; if (!isSynced()) { final params = SyncParams( active.coin, settings.getTx, settings.anchorOffset, syncPort.sendPort); final res = await compute(WarpApi.warpSync, params); if (res == 0) { if (currentSyncedHeight != syncedHeight) { await active.update(); await priceStore.updateChart(); await contacts.fetchContacts(); } } else if (res == 1) { // Reorg final _syncedHeight = await getDbSyncedHeight(); if (_syncedHeight != null) { final rewindHeight = max(_syncedHeight - 20, 0); print("Block reorg detected. Rewind to $rewindHeight"); WarpApi.rewindToHeight(active.coin, rewindHeight); } } } syncing = false; eta.reset(); } @action Future rescan(BuildContext context) async { eta.reset(); final snackBar = SnackBar(content: Text(S.of(context).rescanRequested)); rootScaffoldMessengerKey.currentState?.showSnackBar(snackBar); syncedHeight = 0; WarpApi.rewindToHeight(active.coin, 0); WarpApi.truncateData(active.coin); await sync(); } @action void setAccountRestored(bool v) { accountRestored = v; } @action void setSyncedToLatestHeight() { setSyncHeight(latestHeight); WarpApi.skipToLastHeight(active.coin); } } class MultiPayStore = _MultiPayStore with _$MultiPayStore; abstract class _MultiPayStore with Store { @observable ObservableList recipients = ObservableList.of([]); @action void addRecipient(Recipient recipient) { recipients.add(recipient); } @action void removeRecipient(int index) { recipients.removeAt(index); } @action void clear() { recipients.clear(); } } class ETAStore = _ETAStore with _$ETAStore; abstract class _ETAStore with Store { @observable ETACheckpoint? prev; @observable ETACheckpoint? current; @action void reset() { prev = null; current = null; } @action void checkpoint(int height, DateTime timestamp) { prev = current; current = ETACheckpoint(height, timestamp); } @computed String get eta { final p = prev; final c = current; if (p == null || c == null) return ""; if (c.timestamp.millisecondsSinceEpoch == p.timestamp.millisecondsSinceEpoch) return ""; final speed = (c.height - p.height) / (c.timestamp.millisecondsSinceEpoch - p.timestamp.millisecondsSinceEpoch); if (speed == 0) return ""; final eta = (syncStatus.latestHeight - c.height) / speed; if (eta <= 0) return ""; final duration = Duration(milliseconds: eta.floor()).toString().split('.')[0]; return "(ETA: $duration)"; } } class ContactStore = _ContactStore with _$ContactStore; abstract class _ContactStore with Store { @observable ObservableList contacts = ObservableList.of([]); @action Future fetchContacts() async { final db = active.coinDef.db; List res = await db.rawQuery( "SELECT id, name, address FROM contacts WHERE address <> '' ORDER BY name"); contacts.clear(); for (var c in res) { final contact = Contact(c['id'], c['name'], c['address']); contacts.add(contact); } } @action Future add(Contact c) async { WarpApi.storeContact(active.coin, c.id, c.name, c.address, true); markContactsSaved(active.coin, false); await fetchContacts(); } @action Future remove(Contact c) async { contacts.removeWhere((contact) => contact.id == c.id); WarpApi.storeContact(active.coin, c.id, c.name, "", true); markContactsSaved(active.coin, false); await fetchContacts(); } markContactsSaved(int coin, bool v) { settings.coins[coin].contactsSaved = v; Future.microtask(() async { final prefs = await SharedPreferences.getInstance(); final c = settings.coins[coin].def; prefs.setBool("${c.ticker}.contacts_saved", v); }); } } class ETACheckpoint { int height; DateTime timestamp; ETACheckpoint(this.height, this.timestamp); } var progressPort = ReceivePort(); var progressStream = progressPort.asBroadcastStream(); var syncPort = ReceivePort(); var syncStream = syncPort.asBroadcastStream(); abstract class HasHeight { int height = 0; } class Note extends HasHeight { int id; int height; DateTime timestamp; double value; bool excluded; bool spent; Note(this.id, this.height, this.timestamp, this.value, this.excluded, this.spent); Note get invertExcluded => Note(id, height, timestamp, value, !excluded, spent); } class Tx extends HasHeight { int id; int height; DateTime timestamp; String txid; String fullTxId; double value; String address; String? contact; String memo; Tx(this.id, this.height, this.timestamp, this.txid, this.fullTxId, this.value, this.address, this.contact, this.memo); } class Spending { final String address; final double amount; final String? contact; Spending(this.address, this.amount, this.contact); } class AccountBalance { final DateTime time; final double balance; AccountBalance(this.time, this.balance); } class Backup { final int type; final String name; final String? seed; final int index; final String? sk; final String ivk; final ShareInfo? share; Backup(this.type, this.name, this.seed, this.index, this.sk, this.ivk, this.share); String value() { switch (type) { case 0: return seed!; case 1: return sk!; case 2: return ivk; } return ""; } } class Contact { final int id; final String name; final String address; Contact(this.id, this.name, this.address); factory Contact.empty() => Contact(0, "", ""); } enum SortOrder { Unsorted, Ascending, Descending, } SortOrder nextSortOrder(SortOrder order) => SortOrder.values[(order.index + 1) % 3]; class PnL { final DateTime timestamp; final double price; final double amount; final double realized; final double unrealized; PnL(this.timestamp, this.price, this.amount, this.realized, this.unrealized); @override String toString() { return "$timestamp $price $amount $realized $unrealized"; } } class TimeSeriesPoint { final int day; final V value; TimeSeriesPoint(this.day, this.value); } class Trade { final DateTime dt; final qty; Trade(this.dt, this.qty); } class Portfolio { final DateTime dt; final qty; Portfolio(this.dt, this.qty); } class Quote { final DateTime dt; final price; Quote(this.dt, this.price); } class TimeRange { final int start; final int end; TimeRange(this.start, this.end); } class SortConfig { String field; SortOrder order; SortConfig(this.field, this.order); SortConfig sortOn(String field) { final order = field != this.field ? SortOrder.Ascending : nextSortOrder(this.order); return SortConfig(field, order); } String getIndicator(String field) { if (this.field != field) return ''; if (order == SortOrder.Ascending) return ' \u2191'; if (order == SortOrder.Descending) return ' \u2193'; return ''; } } @JsonSerializable() class DecodedPaymentURI { String address; int amount; String memo; DecodedPaymentURI(this.address, this.amount, this.memo); factory DecodedPaymentURI.fromJson(Map json) => _$DecodedPaymentURIFromJson(json); Map toJson() => _$DecodedPaymentURIToJson(this); } class SendPageArgs { final bool isMulti; final Contact? contact; final String? uri; final List recipients; SendPageArgs({this.isMulti = false, this.contact, this.uri, this.recipients = const[]}); } class ShareInfo { final int index; final int threshold; final int participants; final String value; ShareInfo(this.index, this.threshold, this.participants, this.value); } class TxSummary { final String address; final int amount; final String txJson; TxSummary(this.address, this.amount, this.txJson); }