zwallet/lib/store.dart

923 lines
24 KiB
Dart
Raw Normal View History

2021-07-07 08:40:05 -07:00
import 'dart:isolate';
2021-07-10 22:20:53 -07:00
import 'dart:typed_data';
2021-07-12 04:32:49 -07:00
import 'dart:math' as math;
2021-08-02 22:58:02 -07:00
import 'package:json_annotation/json_annotation.dart';
2021-07-07 08:40:05 -07:00
2021-07-12 04:32:49 -07:00
import 'package:charts_flutter/flutter.dart' as charts show MaterialPalette;
2021-06-26 07:30:12 -07:00
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:mobx/mobx.dart';
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';
import 'package:warp_api/warp_api.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:http/http.dart' as http;
import 'dart:convert' as convert;
import 'package:convert/convert.dart';
2021-07-10 22:20:53 -07:00
import 'package:flex_color_scheme/flex_color_scheme.dart';
2021-06-26 07:30:12 -07:00
import 'main.dart';
part 'store.g.dart';
class Settings = _Settings with _$Settings;
abstract class _Settings with Store {
2021-07-09 06:33:39 -07:00
@observable
String ldUrl;
@observable
String ldUrlChoice;
@observable
int anchorOffset;
2021-07-09 22:44:34 -07:00
@observable
bool getTx;
2021-07-10 22:20:53 -07:00
@observable
int rowsPerPage;
@observable
String theme;
@observable
String themeBrightness;
@observable
2021-07-12 04:32:49 -07:00
ThemeData themeData = ThemeData.light();
2021-07-30 02:45:43 -07:00
@observable
bool showConfirmations = false;
2021-08-07 00:32:10 -07:00
@observable
String currency = "USD";
@observable
List<String> currencies = ["USD"];
2021-08-16 06:07:16 -07:00
@observable
String chartRange = '1Y';
2021-07-12 04:32:49 -07:00
var palette = charts.MaterialPalette.blue;
2021-06-26 07:30:12 -07:00
@action
2021-07-12 04:32:49 -07:00
Future<bool> restore() async {
2021-06-26 07:30:12 -07:00
final prefs = await SharedPreferences.getInstance();
2021-08-13 20:44:53 -07:00
ldUrlChoice = prefs.getString('lightwalletd_choice') ?? "Lightwalletd";
2021-07-09 06:33:39 -07:00
ldUrl = prefs.getString('lightwalletd_custom') ?? "";
prefs.setString('lightwalletd_choice', ldUrlChoice);
prefs.setString('lightwalletd_custom', ldUrl);
anchorOffset = prefs.getInt('anchor_offset') ?? 3;
2021-07-09 22:44:34 -07:00
getTx = prefs.getBool('get_txinfo') ?? true;
2021-07-10 22:20:53 -07:00
rowsPerPage = prefs.getInt('rows_per_age') ?? 10;
theme = prefs.getString('theme') ?? "zcash";
themeBrightness = prefs.getString('theme_brightness') ?? "dark";
2021-07-30 02:45:43 -07:00
showConfirmations = prefs.getBool('show_confirmations') ?? false;
2021-08-09 07:13:42 -07:00
currency = prefs.getString('currency') ?? "USD";
2021-08-16 06:07:16 -07:00
chartRange = prefs.getString('chart_range') ?? "1Y";
2021-07-10 22:20:53 -07:00
_updateThemeData();
2021-08-07 00:32:10 -07:00
Future.microtask(_loadCurrencies); // lazily
2021-07-12 04:32:49 -07:00
return true;
2021-06-26 07:30:12 -07:00
}
2021-07-09 06:33:39 -07:00
@action
Future<void> setURLChoice(String choice) async {
ldUrlChoice = choice;
final prefs = await SharedPreferences.getInstance();
prefs.setString('lightwalletd_choice', ldUrlChoice);
updateLWD();
}
@action
Future<void> setURL(String url) async {
ldUrl = url;
final prefs = await SharedPreferences.getInstance();
prefs.setString('lightwalletd_custom', ldUrl);
updateLWD();
}
@action
Future<void> setAnchorOffset(int offset) async {
final prefs = await SharedPreferences.getInstance();
anchorOffset = offset;
prefs.setInt('anchor_offset', offset);
}
2021-07-10 22:20:53 -07:00
@action
Future<void> setTheme(String thm) async {
final prefs = await SharedPreferences.getInstance();
theme = thm;
prefs.setString('theme', thm);
_updateThemeData();
}
@action
Future<void> setThemeBrightness(String brightness) async {
final prefs = await SharedPreferences.getInstance();
themeBrightness = brightness;
prefs.setString('theme_brightness', brightness);
_updateThemeData();
}
void _updateThemeData() {
FlexScheme scheme;
switch (theme) {
2021-07-12 04:32:49 -07:00
case 'zcash':
scheme = FlexScheme.mango;
palette = charts.MaterialPalette.gray;
break;
case 'blue':
scheme = FlexScheme.bahamaBlue;
palette = charts.MaterialPalette.blue;
break;
case 'pink':
scheme = FlexScheme.sakura;
palette = charts.MaterialPalette.pink;
break;
case 'coffee':
scheme = FlexScheme.espresso;
palette = charts.MaterialPalette.gray;
break;
2021-07-10 22:20:53 -07:00
}
switch (themeBrightness) {
2021-08-16 06:07:16 -07:00
case 'light':
themeData = FlexColorScheme.light(scheme: scheme).toTheme;
break;
case 'dark':
themeData = FlexColorScheme.dark(scheme: scheme).toTheme;
break;
2021-07-10 22:20:53 -07:00
}
}
2021-08-16 06:07:16 -07:00
@action
Future<void> setChartRange(String v) async {
final prefs = await SharedPreferences.getInstance();
chartRange = v;
prefs.setString('chart_range', chartRange);
accountManager.fetchPNL();
}
2021-07-09 06:33:39 -07:00
String getLWD() {
switch (ldUrlChoice) {
2021-08-16 06:07:16 -07:00
case "custom":
return ldUrl;
default:
return coin.lwd
.firstWhere((lwd) => lwd.name == ldUrlChoice,
orElse: () => coin.lwd.first)
.url;
2021-07-09 06:33:39 -07:00
}
}
void updateLWD() {
WarpApi.updateLWD(getLWD());
}
2021-07-09 22:44:34 -07:00
@action
Future<void> updateGetTx(bool v) async {
final prefs = await SharedPreferences.getInstance();
getTx = v;
prefs.setBool('get_txinfo', v);
}
2021-07-10 22:20:53 -07:00
@action
Future<void> setRowsPerPage(int v) async {
final prefs = await SharedPreferences.getInstance();
rowsPerPage = v;
prefs.setInt('rows_per_age', v);
}
2021-07-30 02:45:43 -07:00
@action
Future<void> toggleShowConfirmations() async {
final prefs = await SharedPreferences.getInstance();
showConfirmations = !showConfirmations;
prefs.setBool('show_confirmations', showConfirmations);
}
2021-08-07 00:32:10 -07:00
@action
Future<void> setCurrency(String newCurrency) async {
2021-08-09 07:13:42 -07:00
final prefs = await SharedPreferences.getInstance();
2021-08-07 00:32:10 -07:00
currency = newCurrency;
2021-08-09 07:13:42 -07:00
prefs.setString('currency', currency);
2021-08-07 00:32:10 -07:00
await priceStore.fetchZecPrice();
2021-08-16 06:07:16 -07:00
await accountManager.fetchPNL();
2021-08-07 00:32:10 -07:00
}
@action
Future<void> _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<dynamic>;
final c = _currencies.map((v) => (v as String).toUpperCase()).toList();
c.sort();
currencies = c;
}
}
2021-06-26 07:30:12 -07:00
}
class AccountManager = _AccountManager with _$AccountManager;
abstract class _AccountManager with Store {
Database db;
@observable
Account active;
@observable
bool canPay = false;
@observable
int balance = 0;
@observable
int unconfirmedBalance = 0;
2021-07-09 06:33:39 -07:00
@observable
String taddress = "";
2021-08-07 00:32:10 -07:00
@observable
bool showTAddr = false;
2021-07-09 06:33:39 -07:00
@observable
int tbalance = 0;
2021-06-26 07:30:12 -07:00
@observable
List<Note> notes = [];
@observable
List<Tx> txs = [];
2021-08-16 06:07:16 -07:00
@observable
int lastTxHeight = 0;
2021-07-12 04:32:49 -07:00
@observable
2021-07-18 08:59:02 -07:00
int dataEpoch = 0;
2021-07-12 04:32:49 -07:00
@observable
List<Spending> spendings = [];
@observable
2021-08-16 06:07:16 -07:00
List<TimeSeriesPoint> accountBalances = [];
2021-07-12 04:32:49 -07:00
2021-08-09 07:13:42 -07:00
@observable
List<PnL> pnls = [];
2021-06-26 07:30:12 -07:00
@observable
2021-07-07 08:40:05 -07:00
List<Account> accounts = [];
2021-06-26 07:30:12 -07:00
2021-07-12 04:32:49 -07:00
@observable
SortOrder noteSortOrder = SortOrder.Unsorted;
@observable
SortOrder txSortOrder = SortOrder.Unsorted;
2021-08-09 07:13:42 -07:00
@observable
int pnlSeriesIndex = 0;
2021-07-18 08:59:02 -07:00
@observable
List<Contact> contacts = [];
2021-06-26 07:30:12 -07:00
Future<void> init() async {
db = await getDatabase();
await resetToDefaultAccount();
}
Future<void> resetToDefaultAccount() async {
await refresh();
if (accounts.isNotEmpty) {
final prefs = await SharedPreferences.getInstance();
final account = prefs.getInt('account') ?? accounts[0].id;
setActiveAccountId(account);
}
}
refresh() async {
accounts = await _list();
}
@action
Future<void> setActiveAccount(Account account) async {
2021-07-07 08:40:05 -07:00
if (account == null) return;
2021-06-26 07:30:12 -07:00
final prefs = await SharedPreferences.getInstance();
prefs.setInt('account', account.id);
2021-07-09 06:33:39 -07:00
final List<Map> res1 = await db.rawQuery(
"SELECT address FROM taddrs WHERE account = ?1", [account.id]);
2021-08-07 00:32:10 -07:00
taddress = res1.isNotEmpty ? res1[0]['address'] : "";
showTAddr = false;
2021-07-09 06:33:39 -07:00
2021-06-26 07:30:12 -07:00
WarpApi.setMempoolAccount(account.id);
2021-07-09 06:33:39 -07:00
final List<Map> res2 = await db.rawQuery(
"SELECT sk FROM accounts WHERE id_account = ?1", [account.id]);
canPay = res2.isNotEmpty && res2[0]['sk'] != null;
active = account;
print("Active account = ${account.id}");
2021-08-09 07:13:42 -07:00
await _fetchData(account.id);
2021-06-26 07:30:12 -07:00
}
@action
2021-07-09 06:33:39 -07:00
Future<void> setActiveAccountId(int idAccount) async {
2021-06-26 07:30:12 -07:00
final account = accounts.firstWhere((account) => account.id == idAccount,
2021-07-07 08:40:05 -07:00
orElse: () => accounts.isNotEmpty ? accounts[0] : null);
2021-07-09 06:33:39 -07:00
await setActiveAccount(account);
2021-06-26 07:30:12 -07:00
}
2021-07-07 21:22:54 -07:00
String newAddress() {
return WarpApi.newAddress(active.id);
}
2021-07-12 04:32:49 -07:00
Future<Backup> getBackup() async {
2021-07-07 08:40:05 -07:00
final List<Map> res = await db.rawQuery(
"SELECT seed, sk, ivk FROM accounts WHERE id_account = ?1",
[active.id]);
2021-06-26 07:30:12 -07:00
if (res.isEmpty) return null;
final row = res[0];
2021-07-12 04:32:49 -07:00
final seed = row['seed'];
final sk = row['sk'];
final ivk = row['ivk'];
int type;
2021-08-16 06:07:16 -07:00
if (seed != null)
type = 0;
else if (sk != null)
type = 1;
2021-07-12 04:32:49 -07:00
else if (ivk != null) type = 2;
return Backup(type, seed, sk, ivk);
2021-06-26 07:30:12 -07:00
}
2021-07-09 06:33:39 -07:00
Future<int> _getBalance(int accountId) async {
2021-07-07 08:40:05 -07:00
final List<Map> res = await db.rawQuery(
"SELECT SUM(value) AS value FROM received_notes WHERE account = ?1 AND (spent IS NULL OR spent = 0)",
2021-07-09 06:33:39 -07:00
[accountId]);
2021-06-26 07:30:12 -07:00
if (res.isEmpty) return 0;
return res[0]['value'] ?? 0;
}
Future<int> getBalanceSpendable(int height) async {
2021-07-07 08:40:05 -07:00
final List<Map> res = await db.rawQuery(
2021-08-14 01:02:30 -07:00
"SELECT SUM(value) AS value FROM received_notes WHERE account = ?1 AND spent IS NULL "
2021-08-16 06:07:16 -07:00
"AND height <= ?2 AND (excluded IS NULL OR NOT excluded)",
2021-07-07 08:40:05 -07:00
[active.id, height]);
2021-06-26 07:30:12 -07:00
if (res.isEmpty) return 0;
return res[0]['value'] ?? 0;
}
@action
Future<void> updateUnconfirmedBalance() async {
unconfirmedBalance = await WarpApi.mempoolSync();
}
isEmpty() async {
2021-07-07 08:40:05 -07:00
final List<Map> res = await db.rawQuery("SELECT name FROM accounts", []);
2021-06-26 07:30:12 -07:00
return res.isEmpty;
}
Future<List<Account>> _list() async {
final List<Map> res = await db.rawQuery(
2021-07-07 08:40:05 -07:00
"WITH notes AS (SELECT a.id_account, a.name, a.address, CASE WHEN r.spent IS NULL THEN r.value ELSE 0 END AS nv FROM accounts a LEFT JOIN received_notes r ON a.id_account = r.account) "
"SELECT id_account, name, address, COALESCE(sum(nv), 0) AS balance FROM notes GROUP by id_account",
[]);
return res
.map((r) =>
Account(r['id_account'], r['name'], r['address'], r['balance']))
.toList();
2021-06-26 07:30:12 -07:00
}
@action
Future<void> delete(int account) async {
await db.rawDelete("DELETE FROM accounts WHERE id_account = ?1", [account]);
2021-07-09 06:33:39 -07:00
await db.rawDelete("DELETE FROM taddrs WHERE account = ?1", [account]);
2021-06-26 07:30:12 -07:00
}
2021-07-19 01:17:23 -07:00
@action
Future<void> changeAccountName(String name) async {
2021-08-16 06:07:16 -07:00
await db.execute("UPDATE accounts SET name = ?2 WHERE id_account = ?1",
[active.id, name]);
2021-07-19 01:17:23 -07:00
await refresh();
await setActiveAccountId(active.id);
}
2021-07-07 08:40:05 -07:00
@action
Future<void> updateBalance() async {
if (active == null) return;
2021-07-09 06:33:39 -07:00
balance = await _getBalance(active.id);
2021-07-07 08:40:05 -07:00
}
@action
2021-07-12 04:32:49 -07:00
Future<void> fetchAccountData() async {
2021-07-07 08:40:05 -07:00
if (active == null) return;
2021-07-12 04:32:49 -07:00
await _fetchData(active.id);
}
2021-08-07 00:32:10 -07:00
@action
void toggleShowTAddr() {
showTAddr = !showTAddr;
}
2021-07-12 04:32:49 -07:00
Future<void> _fetchData(int accountId) async {
2021-08-16 06:07:16 -07:00
await _updateBalance(accountId);
final hasNewTx = await _fetchNotesAndHistory(accountId);
2021-08-09 07:13:42 -07:00
int countNewPrices = await WarpApi.syncHistoricalPrices(settings.currency);
2021-08-16 06:07:16 -07:00
if (hasNewTx) {
await _fetchSpending(accountId);
await _fetchAccountBalanceTimeSeries(accountId);
await _fetchContacts(accountId);
}
if (countNewPrices > 0 || pnls.isEmpty || hasNewTx)
await _fetchPNL(accountId);
2021-07-09 06:33:39 -07:00
}
2021-07-10 22:20:53 -07:00
final DateFormat noteDateFormat = DateFormat("yy-MM-dd HH:mm");
final DateFormat txDateFormat = DateFormat("MM-dd HH:mm");
2021-07-09 06:33:39 -07:00
Future<void> _updateBalance(int accountId) async {
2021-08-16 06:07:16 -07:00
final _balance = await _getBalance(accountId);
if (_balance == balance) return;
balance = _balance;
dataEpoch = DateTime.now().millisecondsSinceEpoch;
2021-07-09 06:33:39 -07:00
}
2021-08-16 06:07:16 -07:00
Future<bool> _fetchNotesAndHistory(int accountId) async {
final List<Map> res0 = await db.rawQuery(
"SELECT MAX(height) as height FROM transactions WHERE account = ?1",
[accountId]);
if (res0.isEmpty) return false;
final _lastTxHeight = res0[0]['height'] ?? 0;
if (lastTxHeight == _lastTxHeight) return false;
lastTxHeight = _lastTxHeight;
2021-06-26 07:30:12 -07:00
final List<Map> res = await db.rawQuery(
2021-08-14 01:02:30 -07:00
"SELECT n.id_note, n.height, n.value, t.timestamp, n.excluded, n.spent FROM received_notes n, transactions t "
2021-08-16 06:07:16 -07:00
"WHERE n.account = ?1 AND (n.spent IS NULL OR n.spent = 0) "
"AND n.tx = t.id_tx",
2021-07-09 06:33:39 -07:00
[accountId]);
2021-06-26 07:30:12 -07:00
notes = res.map((row) {
2021-07-12 04:32:49 -07:00
final id = row['id_note'];
2021-06-26 07:30:12 -07:00
final height = row['height'];
2021-07-10 22:20:53 -07:00
final timestamp = noteDateFormat
2021-07-07 08:40:05 -07:00
.format(DateTime.fromMillisecondsSinceEpoch(row['timestamp'] * 1000));
2021-07-12 04:32:49 -07:00
final excluded = (row['excluded'] ?? 0) != 0;
2021-08-14 01:02:30 -07:00
final spent = row['spent'] == 0;
2021-08-16 06:07:16 -07:00
return Note(
id, height, timestamp, row['value'] / ZECUNIT, excluded, spent);
2021-06-26 07:30:12 -07:00
}).toList();
final List<Map> res2 = await db.rawQuery(
2021-07-09 22:44:34 -07:00
"SELECT id_tx, txid, height, timestamp, address, value, memo FROM transactions WHERE account = ?1",
2021-07-09 06:33:39 -07:00
[accountId]);
2021-06-26 07:30:12 -07:00
txs = res2.map((row) {
2021-07-10 22:20:53 -07:00
Uint8List txid = row['txid'];
final fullTxId = hex.encode(txid.reversed.toList());
final shortTxid = fullTxId.substring(0, 8);
final timestamp = txDateFormat
2021-07-07 08:40:05 -07:00
.format(DateTime.fromMillisecondsSinceEpoch(row['timestamp'] * 1000));
2021-08-16 06:07:16 -07:00
return Tx(row['id_tx'], row['height'], timestamp, shortTxid, fullTxId,
row['value'] / ZECUNIT, row['address'], row['memo']);
2021-06-26 07:30:12 -07:00
}).toList();
2021-08-16 06:07:16 -07:00
dataEpoch = DateTime.now().millisecondsSinceEpoch;
return true;
}
@computed
List<Note> get sortedNotes {
var notes2 = [...notes];
return _sortNoteAmount(notes2, noteSortOrder);
2021-07-12 04:32:49 -07:00
}
@action
Future<void> sortNoteAmount() async {
noteSortOrder = nextSortOrder(noteSortOrder);
}
2021-08-16 06:07:16 -07:00
List<Note> _sortNoteAmount(List<Note> notes, SortOrder order) {
2021-07-12 04:32:49 -07:00
switch (order) {
case SortOrder.Ascending:
notes.sort((a, b) => a.value.compareTo(b.value));
break;
case SortOrder.Descending:
notes.sort((a, b) => -a.value.compareTo(b.value));
break;
case SortOrder.Unsorted:
2021-07-30 02:45:43 -07:00
notes.sort((a, b) => -a.height.compareTo(b.height));
2021-07-12 04:32:49 -07:00
break;
}
2021-08-16 06:07:16 -07:00
return notes;
}
@computed
List<Tx> get sortedTxs {
var txs2 = [...txs];
return _sortTxAmount(txs2, txSortOrder);
2021-07-12 04:32:49 -07:00
}
@action
Future<void> sortTxAmount() async {
txSortOrder = nextSortOrder(txSortOrder);
}
2021-08-16 06:07:16 -07:00
List<Tx> _sortTxAmount(List<Tx> txs, SortOrder order) {
2021-07-12 04:32:49 -07:00
switch (order) {
case SortOrder.Ascending:
txs.sort((a, b) => a.value.compareTo(b.value));
break;
case SortOrder.Descending:
txs.sort((a, b) => -a.value.compareTo(b.value));
break;
case SortOrder.Unsorted:
2021-07-30 02:45:43 -07:00
txs.sort((a, b) => -a.height.compareTo(b.height));
2021-07-12 04:32:49 -07:00
break;
}
2021-08-16 06:07:16 -07:00
return txs;
2021-07-12 04:32:49 -07:00
}
Future<void> _fetchSpending(int accountId) async {
2021-08-16 06:07:16 -07:00
final cutoff =
DateTime.now().add(Duration(days: -30)).millisecondsSinceEpoch / 1000;
2021-07-12 04:32:49 -07:00
final List<Map> res = await db.rawQuery(
"SELECT SUM(value) as v, address FROM transactions WHERE account = ?1 AND timestamp >= ?2 AND value < 0 GROUP BY address ORDER BY v ASC LIMIT 10",
[accountId, cutoff]);
spendings = res.map((row) {
final address = row['address'] ?? "";
final value = -row['v'] / ZECUNIT;
return Spending(addressLeftTrim(address), value);
}).toList();
}
Future<void> _fetchAccountBalanceTimeSeries(int accountId) async {
2021-08-16 06:07:16 -07:00
final now = DateTime.now();
final today = DateTime.utc(now.year, now.month, now.day);
final end = today;
final start = today.add(Duration(days: -30));
final cutoff = start.millisecondsSinceEpoch ~/ 1000;
2021-07-12 04:32:49 -07:00
final List<Map> res = await db.rawQuery(
"SELECT timestamp, value FROM transactions WHERE account = ?1 AND timestamp >= ?2 ORDER BY timestamp DESC",
[accountId, cutoff]);
2021-08-16 06:07:16 -07:00
List<AccountBalance> _accountBalances = [];
2021-07-12 04:32:49 -07:00
var b = balance;
2021-08-16 06:07:16 -07:00
_accountBalances.add(AccountBalance(DateTime.now(), b / ZECUNIT));
2021-07-12 04:32:49 -07:00
for (var row in res) {
2021-08-16 06:07:16 -07:00
final timestamp =
DateTime.fromMillisecondsSinceEpoch(row['timestamp'] * 1000);
2021-07-12 04:32:49 -07:00
final value = row['value'];
final ab = AccountBalance(timestamp, b / ZECUNIT);
2021-08-16 06:07:16 -07:00
_accountBalances.add(ab);
2021-07-12 04:32:49 -07:00
b -= value;
}
2021-08-16 06:07:16 -07:00
_accountBalances.add(AccountBalance(start, b / ZECUNIT));
_accountBalances = _accountBalances.reversed.toList();
accountBalances = sampleDaily<AccountBalance, double, double>(
_accountBalances,
start.millisecondsSinceEpoch,
end.millisecondsSinceEpoch,
(AccountBalance ab) => ab.time.millisecondsSinceEpoch ~/ DAY_MS,
(AccountBalance ab) => ab.balance,
(acc, v) => v,
0.0);
2021-06-26 07:30:12 -07:00
}
2021-07-07 08:40:05 -07:00
2021-08-16 06:07:16 -07:00
@action
Future<void> fetchPNL() async {
if (active == null) return;
await _fetchPNL(active.id);
}
Future<void> _fetchPNL(int accountId) async {
final now = DateTime.now();
final today = DateTime.utc(now.year, now.month, now.day);
var days = 365;
switch (settings.chartRange) {
case '1M': days = 30; break;
case '3M': days = 90; break;
case '6M': days = 180; break;
}
final cutoff = today.add(Duration(days: -days));
final List<Map> res1 = await db.rawQuery(
"SELECT timestamp, value FROM transactions WHERE timestamp >= ?2 AND account = ?1",
[accountId, cutoff.millisecondsSinceEpoch ~/ 1000]);
final List<Trade> trades = [];
for (var row in res1) {
final dt = DateTime.fromMillisecondsSinceEpoch(row['timestamp'] * 1000);
final qty = row['value'] / ZECUNIT;
trades.add(Trade(dt, qty));
}
final portfolioTimeSeries = sampleDaily<Trade, Trade, double>(
trades,
cutoff.millisecondsSinceEpoch,
today.millisecondsSinceEpoch,
(t) => t.dt.millisecondsSinceEpoch ~/ DAY_MS,
(t) => t,
(acc, t) => acc + t.qty,
0.0);
final List<Map> res2 = await db.rawQuery(
"SELECT timestamp, price FROM historical_prices WHERE timestamp >= ?2 AND currency = ?1",
[settings.currency, cutoff.millisecondsSinceEpoch ~/ 1000]);
final List<Quote> quotes = [];
for (var row in res2) {
final dt = DateTime.fromMillisecondsSinceEpoch(row['timestamp'] * 1000);
final price = row['price'];
quotes.add(Quote(dt, price));
}
var prevBalance = 0.0;
2021-08-09 07:13:42 -07:00
var cash = 0.0;
var realized = 0.0;
2021-08-16 06:07:16 -07:00
final List<PnL> _pnls = [];
2021-08-21 05:41:05 -07:00
final len = math.min(quotes.length, portfolioTimeSeries.length);
for (var i = 0; i < len; i++) {
2021-08-16 06:07:16 -07:00
final dt = quotes[i].dt;
final price = quotes[i].price;
final balance = portfolioTimeSeries[i].value;
final qty = balance - prevBalance;
final closeQty = qty * balance < 0
? math.min(qty.abs(), prevBalance.abs()) * qty.sign
: 0.0;
final openQty = qty - closeQty;
final avgPrice = prevBalance != 0 ? cash / prevBalance : 0.0;
2021-08-09 07:13:42 -07:00
cash += openQty * price + closeQty * avgPrice;
2021-08-16 06:07:16 -07:00
realized += closeQty * (avgPrice - price);
2021-08-09 07:13:42 -07:00
final unrealized = price * balance - cash;
2021-08-16 06:07:16 -07:00
final pnl = PnL(dt, price, balance, realized, unrealized);
_pnls.add(pnl);
2021-08-09 07:13:42 -07:00
2021-08-16 06:07:16 -07:00
prevBalance = balance;
}
2021-08-09 07:13:42 -07:00
pnls = _pnls;
}
2021-07-07 08:40:05 -07:00
@action
Future<void> convertToWatchOnly() async {
2021-07-09 06:33:39 -07:00
await db.rawUpdate(
"UPDATE accounts SET seed = NULL, sk = NULL WHERE id_account = ?1",
[active.id]);
2021-07-07 08:40:05 -07:00
canPay = false;
}
2021-07-09 06:33:39 -07:00
2021-07-12 04:32:49 -07:00
@action
Future<void> excludeNote(Note note) async {
await db.execute(
"UPDATE received_notes SET excluded = ?2 WHERE id_note = ?1",
[note.id, note.excluded]);
}
2021-07-09 06:33:39 -07:00
void updateTBalance() {
if (active == null) return;
int balance = WarpApi.getTBalance(active.id);
if (balance != tbalance) tbalance = balance;
}
2021-07-18 08:59:02 -07:00
Future<void> _fetchContacts(int accountId) async {
2021-08-16 06:07:16 -07:00
List<Map> res = await db.rawQuery(
"SELECT name, address FROM contacts WHERE account = ?1 ORDER BY name",
[accountId]);
2021-07-18 08:59:02 -07:00
contacts = [];
for (var c in res) {
final contact = Contact(c['name'], c['address']);
contacts.add(contact);
}
}
2021-08-09 07:13:42 -07:00
@action
void setPnlSeriesIndex(int index) {
pnlSeriesIndex = index;
}
2021-06-26 07:30:12 -07:00
}
class Account {
final int id;
final String name;
final String address;
final int balance;
Account(this.id, this.name, this.address, this.balance);
}
class PriceStore = _PriceStore with _$PriceStore;
abstract class _PriceStore with Store {
@observable
double zecPrice = 0.0;
2021-07-07 08:40:05 -07:00
2021-06-26 07:30:12 -07:00
@action
Future<void> fetchZecPrice() async {
2021-08-07 00:32:10 -07:00
final base = "api.coingecko.com";
2021-08-16 06:07:16 -07:00
final uri = Uri.https(base, '/api/v3/simple/price',
{'ids': coin.currency, 'vs_currencies': settings.currency});
2021-06-26 07:30:12 -07:00
final rep = await http.get(uri);
if (rep.statusCode == 200) {
final json = convert.jsonDecode(rep.body) as Map<String, dynamic>;
2021-08-13 20:44:53 -07:00
final p = json[coin.currency][settings.currency.toLowerCase()];
2021-08-09 07:13:42 -07:00
zecPrice = (p is double) ? p : (p as int).toDouble();
2021-08-16 06:07:16 -07:00
} else
zecPrice = 0.0;
2021-06-26 07:30:12 -07:00
}
}
class SyncStatus = _SyncStatus with _$SyncStatus;
abstract class _SyncStatus with Store {
Database _db;
init() async {
var databasesPath = await getDatabasesPath();
final path = join(databasesPath, 'zec.db');
_db = await openDatabase(path);
await update();
}
@observable
int syncedHeight = -1;
@observable
int latestHeight = 0;
bool isSynced() {
return syncedHeight < 0 || syncedHeight == latestHeight;
}
@action
setSyncHeight(int height) {
2021-07-07 08:40:05 -07:00
syncedHeight = height;
2021-06-26 07:30:12 -07:00
}
@action
Future<bool> update() async {
2021-07-19 01:17:23 -07:00
latestHeight = await WarpApi.getLatestHeight();
2021-06-26 07:30:12 -07:00
final _syncedHeight = Sqflite.firstIntValue(
2021-07-07 08:40:05 -07:00
await _db.rawQuery("SELECT MAX(height) FROM blocks")) ??
2021-06-26 07:30:12 -07:00
0;
if (_syncedHeight > 0) syncedHeight = _syncedHeight;
return syncedHeight == latestHeight;
}
}
2021-08-02 22:58:02 -07:00
class MultiPayStore = _MultiPayStore with _$MultiPayStore;
abstract class _MultiPayStore with Store {
@observable
ObservableList<Recipient> recipients = ObservableList.of([]);
@action
void addRecipient(Recipient recipient) {
recipients.add(recipient);
}
@action
void removeRecipient(int index) {
recipients.removeAt(index);
}
@action
void clear() {
recipients.clear();
}
}
2021-07-07 08:40:05 -07:00
var progressPort = ReceivePort();
var progressStream = progressPort.asBroadcastStream();
2021-06-26 07:30:12 -07:00
class Note {
2021-07-12 04:32:49 -07:00
int id;
2021-06-26 07:30:12 -07:00
int height;
String timestamp;
double value;
2021-07-12 04:32:49 -07:00
bool excluded;
2021-08-14 01:02:30 -07:00
bool spent;
2021-06-26 07:30:12 -07:00
2021-08-16 06:07:16 -07:00
Note(this.id, this.height, this.timestamp, this.value, this.excluded,
this.spent);
2021-06-26 07:30:12 -07:00
}
class Tx {
2021-07-09 22:44:34 -07:00
int id;
2021-06-26 07:30:12 -07:00
int height;
String timestamp;
String txid;
2021-07-09 22:44:34 -07:00
String fullTxId;
2021-06-26 07:30:12 -07:00
double value;
2021-07-09 22:44:34 -07:00
String address;
String memo;
2021-06-26 07:30:12 -07:00
2021-08-16 06:07:16 -07:00
Tx(this.id, this.height, this.timestamp, this.txid, this.fullTxId, this.value,
this.address, this.memo);
2021-06-26 07:30:12 -07:00
}
2021-07-12 04:32:49 -07:00
class Spending {
final String address;
final double amount;
Spending(this.address, this.amount);
}
class AccountBalance {
final DateTime time;
final double balance;
AccountBalance(this.time, this.balance);
}
class Backup {
int type;
final String seed;
final String sk;
final String ivk;
Backup(this.type, this.seed, this.sk, this.ivk);
String value() {
switch (type) {
2021-08-16 06:07:16 -07:00
case 0:
return seed;
case 1:
return sk;
case 2:
return ivk;
2021-07-12 04:32:49 -07:00
}
return "";
}
}
2021-07-18 08:59:02 -07:00
class Contact {
final String name;
final String address;
Contact(this.name, this.address);
}
2021-08-16 06:07:16 -07:00
String addressLeftTrim(String address) =>
"..." + address.substring(math.max(address.length - 6, 0));
2021-07-12 04:32:49 -07:00
enum SortOrder {
Unsorted,
Ascending,
Descending,
}
2021-08-16 06:07:16 -07:00
SortOrder nextSortOrder(SortOrder order) =>
SortOrder.values[(order.index + 1) % 3];
2021-07-18 08:59:02 -07:00
2021-08-02 22:58:02 -07:00
@JsonSerializable()
class Recipient {
final String address;
final int amount;
final String memo;
Recipient(this.address, this.amount, this.memo);
2021-08-16 06:07:16 -07:00
factory Recipient.fromJson(Map<String, dynamic> json) =>
_$RecipientFromJson(json);
2021-08-02 22:58:02 -07:00
Map<String, dynamic> toJson() => _$RecipientToJson(this);
}
2021-08-09 07:13:42 -07:00
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);
2021-08-16 06:07:16 -07:00
@override
String toString() {
return "$timestamp $price $amount $realized $unrealized";
}
}
class TimeSeriesPoint<V> {
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);
2021-08-09 07:13:42 -07:00
}