zwallet/lib/store.dart

386 lines
10 KiB
Dart

import 'dart:isolate';
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';
import 'main.dart';
part 'store.g.dart';
class Settings = _Settings with _$Settings;
abstract class _Settings with Store {
@observable
ThemeMode mode;
@observable
String ldUrl;
@observable
String ldUrlChoice;
@observable
int anchorOffset;
@observable
bool getTx;
nextMode() {
return mode == ThemeMode.light ? "Dark Mode" : "Light Mode";
}
@action
Future<void> restore() async {
final prefs = await SharedPreferences.getInstance();
final prefMode = prefs.getString('theme') ?? "light";
if (prefMode == "light")
mode = ThemeMode.light;
else
mode = ThemeMode.dark;
ldUrlChoice = prefs.getString('lightwalletd_choice') ?? "lightwalletd";
ldUrl = prefs.getString('lightwalletd_custom') ?? "";
prefs.setString('lightwalletd_choice', ldUrlChoice);
prefs.setString('lightwalletd_custom', ldUrl);
anchorOffset = prefs.getInt('anchor_offset') ?? 3;
getTx = prefs.getBool('get_txinfo') ?? true;
}
@action
Future<void> toggle() async {
mode = mode == ThemeMode.light ? ThemeMode.dark : ThemeMode.light;
final prefs = await SharedPreferences.getInstance();
prefs.setString('theme', mode == ThemeMode.light ? "light" : "dark");
}
@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);
}
String getLWD() {
switch (ldUrlChoice) {
case "custom": return ldUrl;
case "lightwalletd": return "https://mainnet.lightwalletd.com:9067";
case "zecwallet": return "https://lwdv3.zecwallet.co";
}
}
void updateLWD() {
WarpApi.updateLWD(getLWD());
}
@action
Future<void> updateGetTx(bool v) async {
final prefs = await SharedPreferences.getInstance();
getTx = v;
prefs.setBool('get_txinfo', v);
}
}
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;
@observable
String taddress = "";
@observable
int tbalance = 0;
@observable
List<Note> notes = [];
@observable
List<Tx> txs = [];
@observable
List<Account> accounts = [];
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 {
if (account == null) return;
final prefs = await SharedPreferences.getInstance();
prefs.setInt('account', account.id);
final List<Map> res1 = await db.rawQuery(
"SELECT address FROM taddrs WHERE account = ?1", [account.id]);
if (res1.isNotEmpty) taddress = res1[0]['address'];
WarpApi.setMempoolAccount(account.id);
final List<Map> res2 = await db.rawQuery(
"SELECT sk FROM accounts WHERE id_account = ?1", [account.id]);
canPay = res2.isNotEmpty && res2[0]['sk'] != null;
await _fetchNotesAndHistory(account.id);
active = account;
print("Active account = ${account.id}");
}
@action
Future<void> setActiveAccountId(int idAccount) async {
final account = accounts.firstWhere((account) => account.id == idAccount,
orElse: () => accounts.isNotEmpty ? accounts[0] : null);
await setActiveAccount(account);
}
String newAddress() {
return WarpApi.newAddress(active.id);
}
Future<String> getBackup() async {
final List<Map> res = await db.rawQuery(
"SELECT seed, sk, ivk FROM accounts WHERE id_account = ?1",
[active.id]);
if (res.isEmpty) return null;
final row = res[0];
final backup = row['seed'] ?? row['sk'] ?? row['ivk'];
return backup;
}
Future<int> _getBalance(int accountId) async {
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)",
[accountId]);
if (res.isEmpty) return 0;
return res[0]['value'] ?? 0;
}
Future<int> getBalanceSpendable(int height) async {
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) AND height <= ?2",
[active.id, height]);
if (res.isEmpty) return 0;
return res[0]['value'] ?? 0;
}
@action
Future<void> updateUnconfirmedBalance() async {
unconfirmedBalance = await WarpApi.mempoolSync();
}
isEmpty() async {
final List<Map> res = await db.rawQuery("SELECT name FROM accounts", []);
return res.isEmpty;
}
Future<List<Account>> _list() async {
final List<Map> res = await db.rawQuery(
"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();
}
@action
Future<void> delete(int account) async {
await db.rawDelete("DELETE FROM accounts WHERE id_account = ?1", [account]);
await db.rawDelete("DELETE FROM taddrs WHERE account = ?1", [account]);
}
@action
Future<void> updateBalance() async {
if (active == null) return;
balance = await _getBalance(active.id);
}
@action
Future<void> fetchNotesAndHistory() async {
if (active == null) return;
await _fetchNotesAndHistory(active.id);
}
final DateFormat dateFormat = DateFormat("yyyy-MM-dd HH:mm:ss");
Future<void> _updateBalance(int accountId) async {
balance = await _getBalance(accountId);
}
@action
Future<void> _fetchNotesAndHistory(int accountId) async {
await _updateBalance(accountId);
final List<Map> res = await db.rawQuery(
"SELECT n.height, n.value, t.timestamp FROM received_notes n, transactions t WHERE n.account = ?1 AND (n.spent IS NULL OR n.spent = 0) AND n.tx = t.id_tx",
[accountId]);
notes = res.map((row) {
final height = row['height'];
final timestamp = dateFormat
.format(DateTime.fromMillisecondsSinceEpoch(row['timestamp'] * 1000));
return Note(height, timestamp, row['value'] / ZECUNIT);
}).toList();
final List<Map> res2 = await db.rawQuery(
"SELECT id_tx, txid, height, timestamp, address, value, memo FROM transactions WHERE account = ?1",
[accountId]);
txs = res2.map((row) {
final fullTxId = hex.encode(row['txid']);
final txid = fullTxId.substring(0, 8);
final timestamp = dateFormat
.format(DateTime.fromMillisecondsSinceEpoch(row['timestamp'] * 1000));
return Tx(row['id_tx'], row['height'], timestamp, txid, fullTxId, row['value'] / ZECUNIT, row['address'], row['memo']);
}).toList();
}
@action
Future<void> convertToWatchOnly() async {
await db.rawUpdate(
"UPDATE accounts SET seed = NULL, sk = NULL WHERE id_account = ?1",
[active.id]);
canPay = false;
}
void updateTBalance() {
if (active == null) return;
int balance = WarpApi.getTBalance(active.id);
if (balance != tbalance) tbalance = balance;
}
}
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;
@action
Future<void> fetchZecPrice() async {
final base = "api.binance.com";
final uri = Uri.https(base, '/api/v3/avgPrice', {'symbol': 'ZECUSDT'});
final rep = await http.get(uri);
if (rep.statusCode == 200) {
final json = convert.jsonDecode(rep.body) as Map<String, dynamic>;
final price = double.parse(json['price']);
zecPrice = price;
}
}
}
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) {
syncedHeight = height;
}
@action
Future<bool> update() async {
final _syncedHeight = Sqflite.firstIntValue(
await _db.rawQuery("SELECT MAX(height) FROM blocks")) ??
0;
if (_syncedHeight > 0) syncedHeight = _syncedHeight;
latestHeight = await WarpApi.getLatestHeight();
print("$syncedHeight / $latestHeight");
return syncedHeight == latestHeight;
}
}
var progressPort = ReceivePort();
var progressStream = progressPort.asBroadcastStream();
class Note {
int height;
String timestamp;
double value;
Note(this.height, this.timestamp, this.value);
}
class Tx {
int id;
int height;
String timestamp;
String txid;
String fullTxId;
double value;
String address;
String memo;
Tx(this.id, this.height, this.timestamp, this.txid, this.fullTxId, this.value, this.address, this.memo);
}