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-09-05 21:41:32 -07:00
import ' package:flutter/foundation.dart ' ;
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
2021-09-05 21:41:32 -07:00
import ' generated/l10n.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-08-23 05:47:48 -07:00
@ observable
bool shieldBalance = false ;
2021-08-23 08:44:41 -07:00
@ observable
double autoShieldThreshold = 0.0 ;
2021-08-27 02:51:34 -07:00
@ observable
bool useUA = false ;
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-08-23 05:47:48 -07:00
shieldBalance = prefs . getBool ( ' shield_balance ' ) ? ? false ;
2021-08-23 08:44:41 -07:00
autoShieldThreshold = prefs . getDouble ( ' autoshield_threshold ' ) ? ? 0.0 ;
2021-08-27 02:51:34 -07:00
useUA = prefs . getBool ( ' use_ua ' ) ? ? false ;
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-08-23 05:47:48 -07:00
@ action
Future < void > setShieldBalance ( bool v ) async {
final prefs = await SharedPreferences . getInstance ( ) ;
shieldBalance = v ;
prefs . setBool ( ' shield_balance ' , shieldBalance ) ;
}
2021-08-23 08:44:41 -07:00
@ action
Future < void > setAutoShieldThreshold ( double v ) async {
final prefs = await SharedPreferences . getInstance ( ) ;
autoShieldThreshold = v ;
prefs . setDouble ( ' autoshield_threshold ' , autoShieldThreshold ) ;
}
2021-08-27 02:51:34 -07:00
@ action
Future < void > setUseUA ( bool v ) async {
final prefs = await SharedPreferences . getInstance ( ) ;
useUA = v ;
prefs . setBool ( ' use_ua ' , useUA ) ;
}
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-08-23 19:04:45 -07:00
@ observable
bool pnlDesc = false ;
2021-09-08 07:14:19 -07:00
Future < void > init ( Database db ) async {
this . db = db ;
2021-06-26 07:30:12 -07:00
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 ;
2021-08-22 19:22:38 -07:00
await _fetchData ( account . id , true ) ;
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-09-05 21:41:32 -07:00
if ( account = = active ? . id )
resetToDefaultAccount ( ) ;
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-09-05 06:06:54 -07:00
Future < void > fetchAccountData ( bool force ) async {
2021-07-07 08:40:05 -07:00
if ( active = = null ) return ;
2021-09-05 06:06:54 -07:00
await _fetchData ( active . id , force ) ;
2021-07-12 04:32:49 -07:00
}
2021-08-07 00:32:10 -07:00
@ action
void toggleShowTAddr ( ) {
showTAddr = ! showTAddr ;
}
2021-08-22 19:22:38 -07:00
Future < void > _fetchData ( int accountId , bool force ) async {
2021-08-16 06:07:16 -07:00
await _updateBalance ( accountId ) ;
2021-08-23 08:44:41 -07:00
2021-08-22 19:22:38 -07:00
final hasNewTx = await _fetchNotesAndHistory ( accountId , force ) ;
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 ) ;
}
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-22 19:22:38 -07:00
Future < bool > _fetchNotesAndHistory ( int accountId , bool force ) async {
2021-08-16 06:07:16 -07:00
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 ;
2021-08-22 19:22:38 -07:00
if ( ! force & & lastTxHeight = = _lastTxHeight ) return false ;
2021-08-16 06:07:16 -07:00
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-08-23 19:04:45 -07:00
@ action
void togglePnlDesc ( ) {
pnlDesc = ! pnlDesc ;
}
@ computed
List < PnL > get pnlSorted {
if ( pnlDesc ) {
var _pnls = [ . . . pnls . reversed ] ;
return _pnls ;
}
return 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-08-23 19:04:45 -07:00
if ( settings . autoShieldThreshold ! = 0.0 & & tbalance / ZECUNIT > = settings . autoShieldThreshold ) {
2021-08-23 08:44:41 -07:00
WarpApi . shieldTAddr ( active . id ) ;
}
2021-07-09 06:33:39 -07:00
}
2021-07-18 08:59:02 -07:00
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 ( ) ;
}
2021-09-05 21:41:32 -07:00
@ observable
bool accountRestored = false ;
@ observable
bool syncing = false ;
2021-06-26 07:30:12 -07:00
@ 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-09-05 21:41:32 -07:00
@ action
Future < void > sync ( BuildContext context ) async {
syncing = true ;
final snackBar =
SnackBar ( content: Text ( S
. of ( context )
. rescanRequested ) ) ;
rootScaffoldMessengerKey . currentState . showSnackBar ( snackBar ) ;
syncStatus . setSyncHeight ( 0 ) ;
WarpApi . rewindToHeight ( 0 ) ;
2021-09-08 07:14:19 -07:00
WarpApi . truncateData ( ) ;
contacts . markContactsDirty ( false ) ;
2021-09-05 21:41:32 -07:00
await syncStatus . update ( ) ;
final params = SyncParams ( settings . getTx , settings . anchorOffset , syncPort . sendPort ) ;
await compute ( WarpApi . warpSync , params ) ;
syncing = false ;
}
@ action
void setAccountRestored ( bool v ) {
accountRestored = v ;
}
2021-06-26 07:30:12 -07:00
}
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-09-05 06:06:54 -07:00
class ETAStore = _ETAStore with _ $ETAStore ;
abstract class _ETAStore with Store {
@ observable
ETACheckpoint prev ;
@ observable
ETACheckpoint current ;
@ action
void checkpoint ( int height , DateTime timestamp ) {
prev = current ;
current = ETACheckpoint ( height , timestamp ) ;
}
@ computed
String get eta {
if ( prev = = null | | current = = null ) return " " ;
2021-09-05 20:13:08 -07:00
if ( current . timestamp . millisecondsSinceEpoch = = prev . timestamp . millisecondsSinceEpoch ) return " " ;
2021-09-05 06:06:54 -07:00
final speed = ( current . height - prev . height ) / ( current . timestamp . millisecondsSinceEpoch - prev . timestamp . millisecondsSinceEpoch ) ;
if ( speed = = 0 ) return " " ;
2021-09-05 20:13:08 -07:00
final eta = ( syncStatus . latestHeight - current . height ) / speed ;
2021-09-05 06:06:54 -07:00
if ( eta < = 0 ) return " " ;
final duration = Duration ( milliseconds: eta . floor ( ) ) . toString ( ) . split ( ' . ' ) [ 0 ] ;
return " (ETA: $ duration ) " ;
}
}
2021-09-08 07:14:19 -07:00
class ContactStore = _ContactStore with _ $ContactStore ;
abstract class _ContactStore with Store {
Database db ;
@ observable
bool dirty = false ;
@ observable
ObservableList < Contact > contacts = ObservableList < Contact > . of ( [ ] ) ;
void init ( Database db ) async {
this . db = db ;
final prefs = await SharedPreferences . getInstance ( ) ;
dirty = prefs . getBool ( ' contacts_dirty ' ) ? ? false ;
}
@ action
Future < void > fetchContacts ( ) async {
await _fetchContacts ( ) ;
}
Future < void > _fetchContacts ( ) async {
List < Map > 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 < void > markContactsDirty ( bool v ) async {
final prefs = await SharedPreferences . getInstance ( ) ;
dirty = v ;
prefs . setBool ( ' contacts_dirty ' , dirty ) ;
}
@ action
Future < void > add ( Contact c ) async {
WarpApi . storeContact ( c . id , c . name , c . address , true ) ;
await markContactsDirty ( true ) ;
await _fetchContacts ( ) ;
}
@ action
Future < void > remove ( Contact c ) async {
contacts . removeWhere ( ( contact ) = > contact . id = = c . id ) ;
WarpApi . storeContact ( c . id , c . name , " " , true ) ;
await markContactsDirty ( true ) ;
await _fetchContacts ( ) ;
}
}
2021-09-05 06:06:54 -07:00
class ETACheckpoint {
int height ;
DateTime timestamp ;
ETACheckpoint ( this . height , this . timestamp ) ;
}
2021-07-07 08:40:05 -07:00
var progressPort = ReceivePort ( ) ;
var progressStream = progressPort . asBroadcastStream ( ) ;
2021-08-31 08:02:56 -07:00
var syncPort = ReceivePort ( ) ;
var syncStream = syncPort . 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 {
2021-09-08 07:14:19 -07:00
final int id ;
2021-07-18 08:59:02 -07:00
final String name ;
final String address ;
2021-09-08 07:14:19 -07:00
Contact ( this . id , this . name , this . address ) ;
factory Contact . empty ( ) = > Contact ( 0 , " " , " " ) ;
2021-07-18 08:59:02 -07:00
}
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
}