This commit is contained in:
Hanh 2021-07-07 23:40:05 +08:00
parent b2b5b0072f
commit 2eb76f8232
20 changed files with 581 additions and 253 deletions

2
.gitignore vendored
View File

@ -45,6 +45,8 @@ app.*.map.json
/android/app/profile /android/app/profile
/android/app/release /android/app/release
.gradle/
target/ target/
jniLibs/ jniLibs/
assets/ assets/

115
Cargo.lock generated
View File

@ -45,9 +45,9 @@ checksum = "739f4a8db6605981345c5654f3a85b056ce52f37a39d34da03f25bf2151ea16e"
[[package]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "0.7.15" version = "0.7.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5" checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
@ -198,9 +198,9 @@ checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
[[package]] [[package]]
name = "bitvec" name = "bitvec"
version = "0.19.5" version = "0.19.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8942c8d352ae1838c9dda0b0ca2ab657696ef2232a20147cf1b30ae1a9cb4321" checksum = "a7ba35e9565969edb811639dbebfe34edc0368e472c5018474c8eb2543397f81"
dependencies = [ dependencies = [
"funty", "funty",
"radium 0.5.3", "radium 0.5.3",
@ -291,9 +291,9 @@ dependencies = [
[[package]] [[package]]
name = "bstr" name = "bstr"
version = "0.2.15" version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a40b47ad93e1a5404e6c18dec46b628214fee441c70f4ab5d6942142cc268a3d" checksum = "90682c8d613ad3373e66de8c6411e0ae2ab2571e879d2efbf73558cc66f21279"
dependencies = [ dependencies = [
"lazy_static", "lazy_static",
"memchr", "memchr",
@ -321,11 +321,11 @@ checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040"
[[package]] [[package]]
name = "cast" name = "cast"
version = "0.2.6" version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57cdfa5d50aad6cb4d44dcab6101a7f79925bd59d82ca42f38a9856a28865374" checksum = "4c24dab4283a142afa2fdca129b80ad2c6284e073930f964c3a1293c225ee39a"
dependencies = [ dependencies = [
"rustc_version 0.3.3", "rustc_version 0.4.0",
] ]
[[package]] [[package]]
@ -992,13 +992,19 @@ dependencies = [
"ahash", "ahash",
] ]
[[package]]
name = "hashbrown"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
[[package]] [[package]]
name = "hashlink" name = "hashlink"
version = "0.6.0" version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d99cf782f0dc4372d26846bec3de7804ceb5df083c2d4462c0b8d2330e894fa8" checksum = "d99cf782f0dc4372d26846bec3de7804ceb5df083c2d4462c0b8d2330e894fa8"
dependencies = [ dependencies = [
"hashbrown", "hashbrown 0.9.1",
] ]
[[package]] [[package]]
@ -1101,12 +1107,12 @@ dependencies = [
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "1.6.2" version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824845a0bf897a9042383849b02c1bc219c2383772efcd5c6f9766fa4b81aef3" checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"hashbrown", "hashbrown 0.11.2",
] ]
[[package]] [[package]]
@ -1227,9 +1233,9 @@ checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00"
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.3.4" version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc"
[[package]] [[package]]
name = "memoffset" name = "memoffset"
@ -1289,12 +1295,11 @@ checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a"
[[package]] [[package]]
name = "nom" name = "nom"
version = "6.2.1" version = "6.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c5c51b9083a3c620fa67a2a635d1ce7d95b897e957d6b28ff9a5da960a103a6" checksum = "3d521ee2250f619dd5e06515ba405858d249edc8fae9ddee2dba0695e57db01b"
dependencies = [ dependencies = [
"bitvec 0.19.5", "bitvec 0.19.4",
"funty",
"lexical-core", "lexical-core",
"memchr", "memchr",
"version_check", "version_check",
@ -1426,15 +1431,6 @@ version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
[[package]]
name = "pest"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53"
dependencies = [
"ucd-trie",
]
[[package]] [[package]]
name = "petgraph" name = "petgraph"
version = "0.5.1" version = "0.5.1"
@ -1467,9 +1463,9 @@ dependencies = [
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.6" version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc0e1f259c92177c30a4c9d177246edd0a3568b25756a977d0632cf8fa37e905" checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443"
[[package]] [[package]]
name = "pin-utils" name = "pin-utils"
@ -1498,15 +1494,15 @@ dependencies = [
[[package]] [[package]]
name = "plotters-backend" name = "plotters-backend"
version = "0.3.0" version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b07fffcddc1cb3a1de753caa4e4df03b79922ba43cf882acc1bdd7e8df9f4590" checksum = "d88417318da0eaf0fdcdb51a0ee6c3bed624333bff8f946733049380be67ac1c"
[[package]] [[package]]
name = "plotters-svg" name = "plotters-svg"
version = "0.3.0" version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b38a02e23bd9604b842a812063aec4ef702b57989c37b655254bb61c471ad211" checksum = "521fa9638fa597e1dc53e9412a4f9cefb01187ee1f7413076f9e6749e2885ba9"
dependencies = [ dependencies = [
"plotters-backend", "plotters-backend",
] ]
@ -1762,9 +1758,9 @@ dependencies = [
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.4.6" version = "1.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a26af418b574bd56588335b3a3659a65725d4e636eb1016c2f9e3b38c7cc759" checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
@ -1839,11 +1835,11 @@ dependencies = [
[[package]] [[package]]
name = "rustc_version" name = "rustc_version"
version = "0.3.3" version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
dependencies = [ dependencies = [
"semver 0.11.0", "semver 1.0.3",
] ]
[[package]] [[package]]
@ -1941,17 +1937,14 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
dependencies = [ dependencies = [
"semver-parser 0.7.0", "semver-parser",
] ]
[[package]] [[package]]
name = "semver" name = "semver"
version = "0.11.0" version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" checksum = "5f3aac57ee7f3272d8395c6e4f502f434f0e289fcd62876f70daa008c20dcabe"
dependencies = [
"semver-parser 0.10.2",
]
[[package]] [[package]]
name = "semver-parser" name = "semver-parser"
@ -1959,15 +1952,6 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
[[package]]
name = "semver-parser"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7"
dependencies = [
"pest",
]
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.126" version = "1.0.126"
@ -2150,7 +2134,6 @@ dependencies = [
"anyhow", "anyhow",
"bls12_381", "bls12_381",
"byteorder", "byteorder",
"bytes",
"criterion", "criterion",
"dotenv", "dotenv",
"env_logger", "env_logger",
@ -2231,18 +2214,18 @@ dependencies = [
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.25" version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa6f76457f59514c7eeb4e59d891395fab0b2fd1d40723ae737d64153392e9c6" checksum = "93119e4feac1cbe6c798c34d3a53ea0026b0b1de6a120deef895137c0529bfe2"
dependencies = [ dependencies = [
"thiserror-impl", "thiserror-impl",
] ]
[[package]] [[package]]
name = "thiserror-impl" name = "thiserror-impl"
version = "1.0.25" version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a36768c0fbf1bb15eca10defa29526bda730a2376c2ab4393ccfa16fb1a318d" checksum = "060d69a0afe7796bf42e9e2ff91f5ee691fb15c53d38b4b62a9a53eb23164745"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -2342,9 +2325,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.7.1" version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fb2ed024293bb19f7a5dc54fe83bf86532a44c12a2bb8ba40d64a4509395ca2" checksum = "570c2eb13b3ab38208130eccd41be92520388791207fde783bda7c1e8ace28d4"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"bytes", "bytes",
@ -2543,12 +2526,6 @@ version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06" checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06"
[[package]]
name = "ucd-trie"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c"
[[package]] [[package]]
name = "unicode-normalization" name = "unicode-normalization"
version = "0.1.19" version = "0.1.19"
@ -2560,9 +2537,9 @@ dependencies = [
[[package]] [[package]]
name = "unicode-segmentation" name = "unicode-segmentation"
version = "1.7.1" version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796" checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
[[package]] [[package]]
name = "unicode-width" name = "unicode-width"

View File

@ -1,3 +1,3 @@
org.gradle.jvmargs=-Xmx1536M org.gradle.jvmargs=-Xmx4608m
android.useAndroidX=true android.useAndroidX=true
android.enableJetifier=true android.enableJetifier=true

View File

@ -2,20 +2,30 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle; import 'package:flutter/services.dart' show rootBundle;
import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:package_info_plus/package_info_plus.dart';
Future<void> showAbout(BuildContext context) async { Future<void> showAbout(BuildContext context) async {
final content = await rootBundle.loadString('assets/about.md'); var content = await rootBundle.loadString('assets/about.md');
showDialog(context: context, barrierDismissible: false, PackageInfo packageInfo = await PackageInfo.fromPlatform();
String version = packageInfo.version;
String code = packageInfo.buildNumber;
content += "`Version: $version+$code`";
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: Text('About ZWallet'), title: Text('About ZWallet'),
content: Container(width: double.maxFinite, child: Markdown(data: content)), content: Container(
actions: [ width: double.maxFinite,
ElevatedButton(onPressed: () { Navigator.of(context).pop(); }, child: Text('OK')) child: Markdown(data: content),
] ),
actions: [
)); ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text('OK'))
]));
} }
Future<void> showAboutOnce(BuildContext context) async { Future<void> showAboutOnce(BuildContext context) async {
@ -25,4 +35,4 @@ Future<void> showAboutOnce(BuildContext context) async {
await showAbout(context); await showAbout(context);
prefs.setBool('about', true); prefs.setBool('about', true);
} }
} }

View File

@ -1,12 +1,12 @@
import 'dart:async'; import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:local_auth/local_auth.dart'; import 'package:local_auth/local_auth.dart';
import 'package:qr_flutter/qr_flutter.dart'; import 'package:qr_flutter/qr_flutter.dart';
import 'package:warp/store.dart';
import 'package:warp_api/warp_api.dart'; import 'package:warp_api/warp_api.dart';
import 'about.dart'; import 'about.dart';
@ -20,29 +20,33 @@ class AccountPage extends StatefulWidget {
class _AccountPageState extends State<AccountPage> class _AccountPageState extends State<AccountPage>
with WidgetsBindingObserver, AutomaticKeepAliveClientMixin { with WidgetsBindingObserver, AutomaticKeepAliveClientMixin {
Timer _timerSync; Timer _timerSync;
int _progress = 0;
StreamSubscription _sub;
@override @override
bool get wantKeepAlive => true; bool get wantKeepAlive => true;
@override @override
initState() { initState() {
super.initState();
print("INITSTATE");
Future.microtask(() async { Future.microtask(() async {
if (await accountManager.isEmpty()) await accountManager.updateUnconfirmedBalance();
Navigator.of(this.context).pushReplacementNamed('/accounts'); await accountManager.fetchNotesAndHistory();
else { _setupTimer();
await accountManager.updateUnconfirmedBalance();
await accountManager.fetchNotesAndHistory();
_sync();
_setupTimer();
}
await showAboutOnce(this.context); await showAboutOnce(this.context);
}); });
WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addObserver(this);
super.initState(); progressStream.listen((percent) {
setState(() {
_progress = percent;
});
});
} }
@override @override
void dispose() { void dispose() {
print("DISPOSE");
_timerSync?.cancel(); _timerSync?.cancel();
WidgetsBinding.instance.removeObserver(this); WidgetsBinding.instance.removeObserver(this);
super.dispose(); super.dispose();
@ -73,25 +77,31 @@ class _AccountPageState extends State<AccountPage>
child: Scaffold( child: Scaffold(
appBar: AppBar( appBar: AppBar(
title: Observer( title: Observer(
builder: (context) => builder: (context) => Text(
Text("\u24E9 Wallet - ${accountManager.active.name}")), "\u24E9 Wallet - ${accountManager.active.name}")),
bottom: TabBar(tabs: [ bottom: TabBar(tabs: [
Tab(text: "Account"), Tab(text: "Account"),
Tab(text: "Notes"), Tab(text: "Notes"),
Tab(text: "History"), Tab(text: "History"),
]), ]),
actions: [ actions: [
PopupMenuButton<String>( Observer(builder: (context) {
itemBuilder: (context) => [ accountManager.canPay;
PopupMenuItem(child: Text("Accounts"), value: "Accounts"), return PopupMenuButton<String>(
PopupMenuItem(child: Text("Backup"), value: "Backup"), itemBuilder: (context) => [
PopupMenuItem(child: Text("Rescan"), value: "Rescan"), PopupMenuItem(child: Text("Accounts"), value: "Accounts"),
PopupMenuItem( PopupMenuItem(child: Text("Backup"), value: "Backup"),
child: Text(settings.nextMode()), value: "Theme"), PopupMenuItem(child: Text("Rescan"), value: "Rescan"),
PopupMenuItem(child: Text("About"), value: "About"), if (accountManager.canPay)
], PopupMenuItem(
onSelected: _onMenu, child: Text("Cold Storage"), value: "Cold"),
) PopupMenuItem(
child: Text(settings.nextMode()), value: "Theme"),
PopupMenuItem(child: Text("About"), value: "About"),
],
onSelected: _onMenu,
);
})
], ],
), ),
body: TabBarView(children: [ body: TabBarView(children: [
@ -100,10 +110,15 @@ class _AccountPageState extends State<AccountPage>
child: Center( child: Center(
child: Column(children: [ child: Column(children: [
Observer( Observer(
builder: (context) => syncStatus.isSynced() builder: (context) => syncStatus.syncedHeight <= 0
? Text('${syncStatus.syncedHeight}', style: Theme.of(this.context).textTheme.caption) ? Text('Synching')
: Text( : syncStatus.isSynced()
'${syncStatus.syncedHeight} / ${syncStatus.latestHeight}')), ? Text('${syncStatus.syncedHeight}',
style: Theme.of(this.context)
.textTheme
.caption)
: Text(
'${syncStatus.syncedHeight} / ${syncStatus.latestHeight}')),
Padding(padding: EdgeInsets.symmetric(vertical: 8)), Padding(padding: EdgeInsets.symmetric(vertical: 8)),
Observer(builder: (context) { Observer(builder: (context) {
return Column(children: [ return Column(children: [
@ -115,16 +130,20 @@ class _AccountPageState extends State<AccountPage>
SelectableText('${accountManager.active.address}'), SelectableText('${accountManager.active.address}'),
]); ]);
}), }),
Observer(builder: (context) => Row( Observer(
mainAxisAlignment: MainAxisAlignment.center, builder: (context) => Row(
crossAxisAlignment: CrossAxisAlignment.baseline, mainAxisAlignment: MainAxisAlignment.center,
textBaseline: TextBaseline.ideographic, crossAxisAlignment: CrossAxisAlignment.baseline,
children: <Widget>[ textBaseline: TextBaseline.ideographic,
Text( children: <Widget>[
'\u24E9 ${_getBalance_hi(accountManager.balance)}', Text(
style: Theme.of(context).textTheme.headline2), '\u24E9 ${_getBalance_hi(accountManager.balance)}',
Text('${_getBalance_lo(accountManager.balance)}'), style: Theme.of(context)
])), .textTheme
.headline2),
Text(
'${_getBalance_lo(accountManager.balance)}'),
])),
Observer(builder: (context) { Observer(builder: (context) {
final zecPrice = priceStore.zecPrice; final zecPrice = priceStore.zecPrice;
final balanceUSD = final balanceUSD =
@ -139,23 +158,28 @@ class _AccountPageState extends State<AccountPage>
]); ]);
}), }),
Padding(padding: EdgeInsets.symmetric(vertical: 8)), Padding(padding: EdgeInsets.symmetric(vertical: 8)),
Observer(builder: (context) => Observer(
(accountManager.unconfirmedBalance != 0) ? builder: (context) =>
Row( (accountManager.unconfirmedBalance != 0)
mainAxisAlignment: MainAxisAlignment.center, ? Row(
crossAxisAlignment: CrossAxisAlignment.baseline, mainAxisAlignment: MainAxisAlignment.center,
textBaseline: TextBaseline.ideographic, crossAxisAlignment:
children: <Widget>[ CrossAxisAlignment.baseline,
Text( textBaseline: TextBaseline.ideographic,
'${_sign(accountManager.unconfirmedBalance)} ${_getBalance_hi(accountManager.unconfirmedBalance)}', children: <Widget>[
style: Theme.of(context) Text(
.textTheme '${_sign(accountManager.unconfirmedBalance)} ${_getBalance_hi(accountManager.unconfirmedBalance)}',
.headline4 style: Theme.of(context)
?.merge(_unconfirmedStyle())), .textTheme
Text( .headline4
'${_getBalance_lo(accountManager.unconfirmedBalance)}', ?.merge(_unconfirmedStyle())),
style: _unconfirmedStyle()), Text(
]) : Container()), '${_getBalance_lo(accountManager.unconfirmedBalance)}',
style: _unconfirmedStyle()),
])
: Container()),
if (_progress > 0)
LinearProgressIndicator(value: _progress / 100.0),
]))), ]))),
NoteWidget(), NoteWidget(),
HistoryWidget(), HistoryWidget(),
@ -190,6 +214,7 @@ class _AccountPageState extends State<AccountPage>
} }
_setupTimer() { _setupTimer() {
_sync();
_timerSync = Timer.periodic(Duration(seconds: 15), (Timer t) { _timerSync = Timer.periodic(Duration(seconds: 15), (Timer t) {
_trySync(); _trySync();
}); });
@ -199,6 +224,7 @@ class _AccountPageState extends State<AccountPage>
WarpApi.warpSync((int height) async { WarpApi.warpSync((int height) async {
setState(() { setState(() {
syncStatus.setSyncHeight(height); syncStatus.setSyncHeight(height);
if (syncStatus.isSynced()) accountManager.fetchNotesAndHistory();
}); });
}); });
} }
@ -218,8 +244,9 @@ class _AccountPageState extends State<AccountPage>
} else if (res == 0) { } else if (res == 0) {
syncStatus.setSyncHeight(syncStatus.latestHeight); syncStatus.setSyncHeight(syncStatus.latestHeight);
} }
await accountManager.fetchNotesAndHistory();
} }
await accountManager.fetchNotesAndHistory();
await accountManager.updateBalance();
await accountManager.updateUnconfirmedBalance(); await accountManager.updateUnconfirmedBalance();
} }
@ -238,6 +265,9 @@ class _AccountPageState extends State<AccountPage>
case "Rescan": case "Rescan":
_rescan(); _rescan();
break; break;
case "Cold":
_cold();
break;
case "Theme": case "Theme":
settings.toggle(); settings.toggle();
break; break;
@ -250,8 +280,7 @@ class _AccountPageState extends State<AccountPage>
_backup() async { _backup() async {
final localAuth = LocalAuthentication(); final localAuth = LocalAuthentication();
final didAuthenticate = await localAuth.authenticate( final didAuthenticate = await localAuth.authenticate(
localizedReason: "Please authenticate to show account seed", localizedReason: "Please authenticate to show account seed");
biometricOnly: true);
if (didAuthenticate) { if (didAuthenticate) {
final seed = await accountManager.getBackup(); final seed = await accountManager.getBackup();
showDialog( showDialog(
@ -300,6 +329,31 @@ class _AccountPageState extends State<AccountPage>
]), ]),
); );
} }
_cold() {
showDialog(
context: this.context,
barrierDismissible: false,
builder: (context) => AlertDialog(
title: Text('Cold Storage'),
content: Text(
'Do you want to DELETE the secret key and convert this account to a watch-only account? '
'You will not be able to spend from this device anymore. This operation is NOT reversible.'),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text('Cancel')),
TextButton(
onPressed: _convertToWatchOnly, child: Text('DELETE'))
]));
}
_convertToWatchOnly() {
accountManager.convertToWatchOnly();
Navigator.of(context).pop();
}
} }
class NoteWidget extends StatefulWidget { class NoteWidget extends StatefulWidget {
@ -309,7 +363,6 @@ class NoteWidget extends StatefulWidget {
class _NoteState extends State<NoteWidget> with AutomaticKeepAliveClientMixin { class _NoteState extends State<NoteWidget> with AutomaticKeepAliveClientMixin {
final DateFormat dateFormat = DateFormat("yyyy-MM-dd HH:mm:ss"); final DateFormat dateFormat = DateFormat("yyyy-MM-dd HH:mm:ss");
final latestHeight = syncStatus.latestHeight;
@override @override
bool get wantKeepAlive => true; //Set to true bool get wantKeepAlive => true; //Set to true
@ -330,7 +383,7 @@ class _NoteState extends State<NoteWidget> with AutomaticKeepAliveClientMixin {
.toList(); .toList();
bool _confirmed(int height) { bool _confirmed(int height) {
return latestHeight - height >= 10; return syncStatus.latestHeight - height >= 10;
} }
@override @override
@ -338,6 +391,7 @@ class _NoteState extends State<NoteWidget> with AutomaticKeepAliveClientMixin {
super.build(context); super.build(context);
return SingleChildScrollView( return SingleChildScrollView(
scrollDirection: Axis.vertical, scrollDirection: Axis.vertical,
padding: EdgeInsets.only(bottom: 32),
child: Observer( child: Observer(
builder: (context) => DataTable( builder: (context) => DataTable(
columns: [ columns: [
@ -376,6 +430,7 @@ class _HistoryState extends State<HistoryWidget>
super.build(context); super.build(context);
return SingleChildScrollView( return SingleChildScrollView(
scrollDirection: Axis.vertical, scrollDirection: Axis.vertical,
padding: EdgeInsets.only(bottom: 64),
child: Observer( child: Observer(
builder: (context) => DataTable( builder: (context) => DataTable(
columns: [ columns: [

View File

@ -54,12 +54,13 @@ class AccountManagerState extends State<AccountManagerPage> {
} }
Future<bool> _onAccountDelete(DismissDirection _direction) async { Future<bool> _onAccountDelete(DismissDirection _direction) async {
if (accountManager.accounts.length == 1) return false;
final confirm = showDialog<bool>( final confirm = showDialog<bool>(
context: this.context, context: this.context,
barrierDismissible: false, barrierDismissible: false,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: Text('Seed'), title: Text('Seed'),
content: Text('Are you SURE you want to DELETE this account?'), content: Text('Are you SURE you want to DELETE this account? You MUST have a BACKUP to recover it. This operation is NOT reversible.'),
actions: [ actions: [
TextButton( TextButton(
child: Text('Cancel'), child: Text('Cancel'),
@ -68,7 +69,7 @@ class AccountManagerState extends State<AccountManagerPage> {
}, },
), ),
TextButton( TextButton(
child: Text('OK'), child: Text('DELETE'),
onPressed: () { onPressed: () {
Navigator.of(this.context).pop(true); Navigator.of(this.context).pop(true);
}, },

View File

@ -1,3 +1,4 @@
import 'package:decimal/decimal.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
@ -12,6 +13,9 @@ import 'send.dart';
import 'store.dart'; import 'store.dart';
const ZECUNIT = 100000000.0; const ZECUNIT = 100000000.0;
var ZECUNIT_DECIMAL = Decimal.parse('100000000');
const mZECUNIT = 100000;
const DEFAULT_FEE = 1000;
var accountManager = AccountManager(); var accountManager = AccountManager();
var priceStore = PriceStore(); var priceStore = PriceStore();
@ -58,7 +62,8 @@ class _ZWalletState extends State<ZWalletApp> {
await accountManager.init(); await accountManager.init();
await syncStatus.init(); await syncStatus.init();
await settings.restore(); await settings.restore();
return Future.value(AccountPage()); return Future.value(
accountManager.accounts.isNotEmpty ? AccountPage() : AccountManagerPage());
} }
@override @override

View File

@ -66,8 +66,11 @@ class _RestorePageState extends State<RestorePage> {
if (accountManager.accounts.length == 1) { if (accountManager.accounts.length == 1) {
if (_keyController.text == "") if (_keyController.text == "")
WarpApi.skipToLastHeight(); // single new account -> quick sync WarpApi.skipToLastHeight(); // single new account -> quick sync
else else {
final snackBar = SnackBar(content: Text("Scan starting momentarily"));
rootScaffoldMessengerKey.currentState.showSnackBar(snackBar);
WarpApi.rewindToHeight(0); WarpApi.rewindToHeight(0);
}
} }
Navigator.of(context).pop(); Navigator.of(context).pop();
Navigator.of(context).pushReplacementNamed('/account'); Navigator.of(context).pushReplacementNamed('/account');

View File

@ -1,11 +1,16 @@
import 'dart:isolate';
import 'package:barcode_scan/barcode_scan.dart'; import 'package:barcode_scan/barcode_scan.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_masked_text/flutter_masked_text.dart'; import 'package:flutter_masked_text/flutter_masked_text.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
import 'package:warp_api/warp_api.dart'; import 'package:warp_api/warp_api.dart';
import 'package:decimal/decimal.dart';
import 'dart:math' as math;
import 'main.dart'; import 'main.dart';
import 'store.dart';
class SendPage extends StatefulWidget { class SendPage extends StatefulWidget {
SendPage(); SendPage();
@ -17,77 +22,112 @@ class SendPage extends StatefulWidget {
class SendState extends State<SendPage> { class SendState extends State<SendPage> {
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
var _address = ""; var _address = "";
var _amount = 0.0; var _amount = Decimal.zero;
var _maxAmountPerNote = Decimal.zero;
var _balance = 0; var _balance = 0;
final _addressController = TextEditingController(); final _addressController = TextEditingController();
final _currencyController = MoneyMaskedTextController(decimalSeparator: '.', thousandSeparator: ',', precision: 3); var _mZEC = true;
var _currencyController = _makeMoneyMaskedTextController(true);
var _maxAmountPerNoteController = _makeMoneyMaskedTextController(true);
var _includeFee = false;
var _isExpanded = false;
@override @override
initState() { initState() {
Future.microtask(() async { Future.microtask(() async {
final balance = await accountManager.getBalanceSpendable(syncStatus.latestHeight - 10); final balance = await accountManager
.getBalanceSpendable(syncStatus.latestHeight - 10);
setState(() { setState(() {
_balance = balance; _balance = math.max(balance - DEFAULT_FEE, 0);
}); });
}); });
super.initState(); super.initState();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
debugPrint(_address);
return Scaffold( return Scaffold(
appBar: AppBar(title: Text('Send ZEC')), appBar: AppBar(title: Text('Send ZEC')),
body: Form( body: Form(
key: _formKey, key: _formKey,
child: Container( child: SingleChildScrollView(
padding: EdgeInsets.all(20), padding: EdgeInsets.all(20),
child: Column( child: Column(children: <Widget>[
children: <Widget>[ Row(children: <Widget>[
Row( Expanded(
children: <Widget>[ child: TextFormField(
Expanded(child: TextFormField( decoration:
decoration: InputDecoration(labelText: 'Send ZEC to...'), InputDecoration(labelText: 'Send ZEC to...'),
minLines: 4, maxLines: null, minLines: 4,
keyboardType: TextInputType.multiline, maxLines: null,
controller: _addressController, keyboardType: TextInputType.multiline,
onSaved: onAddress, controller: _addressController,
validator: checkAddress, onSaved: _onAddress,
validator: _checkAddress,
),
), ),
), IconButton(
IconButton(icon: new Icon(MdiIcons.qrcodeScan), onPressed: onScan) icon: new Icon(MdiIcons.qrcodeScan), onPressed: _onScan)
] ]),
), Row(children: [
TextFormField( Expanded(
decoration: InputDecoration(labelText: 'Amount'), child: TextFormField(
keyboardType: TextInputType.number, decoration: InputDecoration(labelText: 'Amount'),
controller: _currencyController, keyboardType: TextInputType.number,
validator: checkAmount, controller: _currencyController,
onSaved: onAmount validator: _checkAmount,
), onSaved: _onAmount)),
Padding(padding: EdgeInsets.all(8)), TextButton(child: Text('MAX'), onPressed: _onMax),
Text("Spendable: ${_balance / ZECUNIT } ZEC"), ]),
ButtonBar( ExpansionPanelList(
children: [ expansionCallback: (_, isExpanded) {
IconButton(icon: new Icon(MdiIcons.send), onPressed: onSend), setState(() {
IconButton(icon: new Icon(MdiIcons.cancel), onPressed: onCancel) _isExpanded = !isExpanded;
] });
) },
] children: [
) ExpansionPanel(
) headerBuilder: (_, __) =>
) ListTile(title: Text('Advanced Options')),
); body: Column(children: [
CheckboxListTile(
title: Text('Round to mZEC'),
value: _mZEC,
onChanged: _onChangedmZEC),
CheckboxListTile(
title: Text('Include Fee in Amount'),
value: _includeFee,
onChanged: _onChangedIncludeFee),
ListTile(
title: TextFormField(
decoration: InputDecoration(
labelText: 'Max Amount per Note'),
keyboardType: TextInputType.number,
controller: _maxAmountPerNoteController,
validator: _checkMaxAmountPerNote,
onSaved: _onSavedMaxAmountPerNote,
))
]),
isExpanded: _isExpanded)
]),
Padding(padding: EdgeInsets.all(8)),
Text("Spendable: ${_balance / ZECUNIT} ZEC"),
ButtonBar(children: [
IconButton(
icon: new Icon(MdiIcons.cancel), onPressed: _onCancel),
IconButton(
icon: new Icon(MdiIcons.send), onPressed: _onSend),
])
]))));
} }
String checkAddress(String v) { String _checkAddress(String v) {
if (v.isEmpty) return 'Address is empty'; if (v.isEmpty) return 'Address is empty';
if (!WarpApi.validAddress(v)) return 'Invalid Address'; if (!WarpApi.validAddress(v)) return 'Invalid Address';
return null; return null;
} }
String checkAmount(String vs) { String _checkAmount(String vs) {
final v = double.tryParse(vs); final v = double.tryParse(vs);
if (v == null) return 'Amount must be a number'; if (v == null) return 'Amount must be a number';
if (v <= 0.0) return 'Amount must be positive'; if (v <= 0.0) return 'Amount must be positive';
@ -95,11 +135,44 @@ class SendState extends State<SendPage> {
return null; return null;
} }
void onCancel() { String _checkMaxAmountPerNote(String vs) {
final v = double.tryParse(vs);
if (v == null) return 'Amount must be a number';
if (v < 0.0) return 'Amount must be positive';
return null;
}
void _onMax() {
setState(() {
_mZEC = false;
_currencyController = _makeMoneyMaskedTextController(false);
_includeFee = false;
_currencyController.updateValue(
(Decimal.fromInt(_balance) / ZECUNIT_DECIMAL).toDouble());
});
}
void _onChangedIncludeFee(bool v) {
setState(() {
_includeFee = v;
});
}
void _onChangedmZEC(bool v) {
setState(() {
_mZEC = v;
final amount = _currencyController.numberValue;
_currencyController = _makeMoneyMaskedTextController(v);
_currencyController.updateValue(amount);
_maxAmountPerNoteController = _makeMoneyMaskedTextController(v);
});
}
void _onCancel() {
Navigator.of(context).pop(); Navigator.of(context).pop();
} }
void onScan() async { void _onScan() async {
var code = await BarcodeScanner.scan(); var code = await BarcodeScanner.scan();
setState(() { setState(() {
_address = code.rawContent; _address = code.rawContent;
@ -107,58 +180,90 @@ class SendState extends State<SendPage> {
}); });
} }
void onAmount(v) { void _onAmount(v) {
_amount = double.parse(v); _amount = Decimal.parse(v);
} }
void onAddress(v) { void _onAddress(v) {
_address = v; _address = v;
} }
void onSend() async { void _onSavedMaxAmountPerNote(v) {
_maxAmountPerNote = Decimal.parse(v);
}
void _onSend() async {
final form = _formKey.currentState; final form = _formKey.currentState;
if (form == null) return; if (form == null) return;
if (form.validate()) { if (form.validate()) {
form.save(); form.save();
final approved = await showDialog(context: context, barrierDismissible: false, final approved = await showDialog(
builder: (BuildContext context) => context: context,
AlertDialog(title: Text('Please Confirm'), barrierDismissible: false,
content: SingleChildScrollView( builder: (BuildContext context) => AlertDialog(
child: Text("Sending $_amount ZEC to $_address") title: Text('Please Confirm'),
), content: SingleChildScrollView(
actions: <Widget>[ child: Text("Sending $_amount ZEC to $_address")),
TextButton(child: Text('Cancel'), onPressed: () {Navigator.of(context).pop(false);}), actions: <Widget>[
TextButton(child: Text('Approve'), onPressed: () {Navigator.of(context).pop(true);}), TextButton(
], child: Text('Cancel'),
) onPressed: () {
); Navigator.of(context).pop(false);
}),
TextButton(
child: Text('Approve'),
onPressed: () {
Navigator.of(context).pop(true);
}),
],
));
if (approved) { if (approved) {
Navigator.of(context).pop(); Navigator.of(context).pop();
final snackBar1 = SnackBar( final snackBar1 = SnackBar(content: Text("Preparing transaction..."));
content: Text("Preparing transaction..."));
rootScaffoldMessengerKey.currentState.showSnackBar(snackBar1); rootScaffoldMessengerKey.currentState.showSnackBar(snackBar1);
final amount = (_amount * ZECUNIT).toInt(); int amount = (_amount * ZECUNIT_DECIMAL).toInt();
final tx = await compute(sendPayment, SendPaymentParam(accountManager.active.id, _address, amount)); if (_includeFee) amount -= DEFAULT_FEE;
int maxAmountPerNote = (_maxAmountPerNote * ZECUNIT_DECIMAL).toInt();
final tx = await compute(
sendPayment,
SendPaymentParam(
accountManager.active.id, _address, amount, maxAmountPerNote,
progressPort.sendPort
));
final snackBar2 = SnackBar( final snackBar2 = SnackBar(content: Text("TX ID: $tx"));
content: Text("TX ID: $tx"));
rootScaffoldMessengerKey.currentState.showSnackBar(snackBar2); rootScaffoldMessengerKey.currentState.showSnackBar(snackBar2);
} }
} }
} }
static MoneyMaskedTextController _makeMoneyMaskedTextController(bool mZEC) =>
MoneyMaskedTextController(
decimalSeparator: '.',
thousandSeparator: ',',
precision: mZEC ? 3 : 8);
} }
class SendPaymentParam { class SendPaymentParam {
int account; int account;
String address; String address;
int amount; int amount;
int maxAmountPerNote;
SendPort sendPort;
SendPaymentParam(this.account, this.address, this.amount); SendPaymentParam(
this.account, this.address, this.amount, this.maxAmountPerNote, this.sendPort);
} }
sendPayment(SendPaymentParam param) { sendPayment(SendPaymentParam param) async {
final tx = WarpApi.sendPayment(param.account, param.address, param.amount); param.sendPort.send(0);
final tx = await WarpApi.sendPayment(
param.account, param.address, param.amount, param.maxAmountPerNote,
(percent) {
param.sendPort.send(percent);
});
param.sendPort.send(0);
return tx; return tx;
} }

View File

@ -1,3 +1,5 @@
import 'dart:isolate';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
@ -26,7 +28,7 @@ abstract class _Settings with Store {
@action @action
Future<void> restore() async { Future<void> restore() async {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
final prefMode = await prefs.getString('theme') ?? "light"; final prefMode = prefs.getString('theme') ?? "light";
if (prefMode == "light") if (prefMode == "light")
mode = ThemeMode.light; mode = ThemeMode.light;
else else
@ -65,7 +67,7 @@ abstract class _AccountManager with Store {
List<Tx> txs = []; List<Tx> txs = [];
@observable @observable
List<Account> accounts; List<Account> accounts = [];
Future<void> init() async { Future<void> init() async {
db = await getDatabase(); db = await getDatabase();
@ -87,11 +89,13 @@ abstract class _AccountManager with Store {
@action @action
Future<void> setActiveAccount(Account account) async { Future<void> setActiveAccount(Account account) async {
if (account == null) return;
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
prefs.setInt('account', account.id); prefs.setInt('account', account.id);
this.active = account; this.active = account;
WarpApi.setMempoolAccount(account.id); WarpApi.setMempoolAccount(account.id);
final List<Map> res = await db.rawQuery("SELECT sk FROM accounts WHERE id_account = ?1", [active.id]); final List<Map> res = await db
.rawQuery("SELECT sk FROM accounts WHERE id_account = ?1", [active.id]);
canPay = res.isNotEmpty && res[0]['sk'] != null; canPay = res.isNotEmpty && res[0]['sk'] != null;
await fetchNotesAndHistory(); await fetchNotesAndHistory();
} }
@ -99,12 +103,14 @@ abstract class _AccountManager with Store {
@action @action
setActiveAccountId(int idAccount) { setActiveAccountId(int idAccount) {
final account = accounts.firstWhere((account) => account.id == idAccount, final account = accounts.firstWhere((account) => account.id == idAccount,
orElse: () => accounts[0]); orElse: () => accounts.isNotEmpty ? accounts[0] : null);
setActiveAccount(account); setActiveAccount(account);
} }
Future<String> getBackup() async { Future<String> getBackup() async {
final List<Map> res = await db.rawQuery("SELECT seed, sk, ivk FROM accounts WHERE id_account = ?1", [active.id]); final List<Map> res = await db.rawQuery(
"SELECT seed, sk, ivk FROM accounts WHERE id_account = ?1",
[active.id]);
if (res.isEmpty) return null; if (res.isEmpty) return null;
final row = res[0]; final row = res[0];
final backup = row['seed'] ?? row['sk'] ?? row['ivk']; final backup = row['seed'] ?? row['sk'] ?? row['ivk'];
@ -112,13 +118,17 @@ abstract class _AccountManager with Store {
} }
Future<int> _getBalance() async { Future<int> _getBalance() 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)", [active.id]); 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)",
[active.id]);
if (res.isEmpty) return 0; if (res.isEmpty) return 0;
return res[0]['value'] ?? 0; return res[0]['value'] ?? 0;
} }
Future<int> getBalanceSpendable(int height) async { 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]); 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; if (res.isEmpty) return 0;
return res[0]['value'] ?? 0; return res[0]['value'] ?? 0;
} }
@ -129,16 +139,19 @@ abstract class _AccountManager with Store {
} }
isEmpty() async { isEmpty() async {
final List<Map> res = await db.rawQuery("SELECT 1 FROM accounts", []); final List<Map> res = await db.rawQuery("SELECT name FROM accounts", []);
return res.isEmpty; return res.isEmpty;
} }
Future<List<Account>> _list() async { Future<List<Account>> _list() async {
final List<Map> res = await db.rawQuery( final List<Map> res = await db.rawQuery(
"SELECT a.id_account AS account, a.name, a.address, COALESCE(SUM(n.value), 0) AS balance FROM accounts a LEFT JOIN received_notes n ON n.account = a.id_account WHERE " "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) "
"n.spent IS NULL OR n.spent = 0 " "SELECT id_account, name, address, COALESCE(sum(nv), 0) AS balance FROM notes GROUP by id_account",
"GROUP BY name", []); []);
return res.map((r) => Account(r['account'], r['name'], r['address'], r['balance'])).toList(); return res
.map((r) =>
Account(r['id_account'], r['name'], r['address'], r['balance']))
.toList();
} }
@action @action
@ -146,17 +159,25 @@ abstract class _AccountManager with Store {
await db.rawDelete("DELETE FROM accounts WHERE id_account = ?1", [account]); await db.rawDelete("DELETE FROM accounts WHERE id_account = ?1", [account]);
} }
@action
Future<void> updateBalance() async {
if (active == null) return;
balance = await _getBalance();
}
final DateFormat dateFormat = DateFormat("yyyy-MM-dd HH:mm:ss"); final DateFormat dateFormat = DateFormat("yyyy-MM-dd HH:mm:ss");
@action Future<void> fetchNotesAndHistory() async { @action
balance = await _getBalance(); Future<void> fetchNotesAndHistory() async {
if (active == null) return;
await updateBalance();
final List<Map> res = await db.rawQuery( 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", "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",
[active.id]); [active.id]);
notes = res.map((row) { notes = res.map((row) {
final height = row['height']; final height = row['height'];
final timestamp = dateFormat.format( final timestamp = dateFormat
DateTime.fromMillisecondsSinceEpoch(row['timestamp'] * 1000)); .format(DateTime.fromMillisecondsSinceEpoch(row['timestamp'] * 1000));
return Note(height, timestamp, row['value'] / ZECUNIT); return Note(height, timestamp, row['value'] / ZECUNIT);
}).toList(); }).toList();
@ -165,11 +186,17 @@ abstract class _AccountManager with Store {
[active.id]); [active.id]);
txs = res2.map((row) { txs = res2.map((row) {
final txid = hex.encode(row['txid']).substring(0, 8); final txid = hex.encode(row['txid']).substring(0, 8);
final timestamp = dateFormat.format( final timestamp = dateFormat
DateTime.fromMillisecondsSinceEpoch(row['timestamp'] * 1000)); .format(DateTime.fromMillisecondsSinceEpoch(row['timestamp'] * 1000));
return Tx(row['height'], timestamp, txid, row['value'] / ZECUNIT); return Tx(row['height'], timestamp, txid, row['value'] / ZECUNIT);
}).toList(); }).toList();
} }
@action
Future<void> convertToWatchOnly() async {
await db.rawUpdate("UPDATE accounts SET seed = NULL, sk = NULL WHERE id_account = ?1", [active.id]);
canPay = false;
}
} }
class Account { class Account {
@ -186,11 +213,11 @@ class PriceStore = _PriceStore with _$PriceStore;
abstract class _PriceStore with Store { abstract class _PriceStore with Store {
@observable @observable
double zecPrice = 0.0; double zecPrice = 0.0;
@action @action
Future<void> fetchZecPrice() async { Future<void> fetchZecPrice() async {
final base = "api.binance.com"; final base = "api.binance.com";
final uri = Uri.https(base, '/api/v3/avgPrice', { 'symbol': 'ZECUSDT' }); final uri = Uri.https(base, '/api/v3/avgPrice', {'symbol': 'ZECUSDT'});
final rep = await http.get(uri); final rep = await http.get(uri);
if (rep.statusCode == 200) { if (rep.statusCode == 200) {
final json = convert.jsonDecode(rep.body) as Map<String, dynamic>; final json = convert.jsonDecode(rep.body) as Map<String, dynamic>;
@ -224,13 +251,13 @@ abstract class _SyncStatus with Store {
@action @action
setSyncHeight(int height) { setSyncHeight(int height) {
syncedHeight = height; syncedHeight = height;
} }
@action @action
Future<bool> update() async { Future<bool> update() async {
final _syncedHeight = Sqflite.firstIntValue( final _syncedHeight = Sqflite.firstIntValue(
await _db.rawQuery("SELECT MAX(height) FROM blocks")) ?? await _db.rawQuery("SELECT MAX(height) FROM blocks")) ??
0; 0;
if (_syncedHeight > 0) syncedHeight = _syncedHeight; if (_syncedHeight > 0) syncedHeight = _syncedHeight;
latestHeight = await WarpApi.getLatestHeight(); latestHeight = await WarpApi.getLatestHeight();
@ -239,6 +266,9 @@ abstract class _SyncStatus with Store {
} }
} }
var progressPort = ReceivePort();
var progressStream = progressPort.asBroadcastStream();
class Note { class Note {
int height; int height;
String timestamp; String timestamp;

View File

@ -24,7 +24,7 @@ uint32_t new_account(char *name, char *data);
int64_t get_mempool_balance(void); int64_t get_mempool_balance(void);
const char *send_payment(uint32_t account, char *address, uint64_t amount); const char *send_payment(uint32_t account, char *address, uint64_t amount, uint64_t max_amount_per_note, int64_t port);
int8_t try_warp_sync(void); int8_t try_warp_sync(void);

View File

@ -122,15 +122,42 @@ pub fn get_latest_height() -> u32 {
}) })
} }
pub fn send_payment(account: u32, address: &str, amount: u64) -> String { pub fn send_payment(
account: u32,
address: &str,
amount: u64,
max_amount_per_note: u64,
port: i64,
) -> String {
let r = Runtime::new().unwrap(); let r = Runtime::new().unwrap();
r.block_on(async { r.block_on(async {
let wallet = WALLET.get().unwrap().lock().unwrap(); let wallet = WALLET.get().unwrap().lock().unwrap();
let res = wallet.send_payment(account, address, amount).await; let res = wallet
.send_payment(
account,
address,
amount,
max_amount_per_note,
move |progress| {
if port != 0 {
let progress = match progress.end() {
Some(end) => (progress.cur() * 100 / end) as i32,
None => -(progress.cur() as i32),
};
let mut progress = progress.into_dart();
unsafe {
POST_COBJ.map(|p| {
p(port, &mut progress);
});
}
}
},
)
.await;
match res { match res {
Err(err) => { Err(err) => {
log::error!("{}", err); log::error!("{}", err);
"".to_string() err.to_string()
} }
Ok(tx_id) => tx_id, Ok(tx_id) => tx_id,
} }

View File

@ -61,9 +61,11 @@ pub unsafe extern "C" fn send_payment(
account: u32, account: u32,
address: *mut c_char, address: *mut c_char,
amount: u64, amount: u64,
max_amount_per_note: u64,
port: i64,
) -> *const c_char { ) -> *const c_char {
let address = CStr::from_ptr(address).to_string_lossy(); let address = CStr::from_ptr(address).to_string_lossy();
let tx_id = api::send_payment(account, &address, amount); let tx_id = api::send_payment(account, &address, amount, max_amount_per_note, port);
CString::new(tx_id).unwrap().into_raw() CString::new(tx_id).unwrap().into_raw()
} }

@ -1 +1 @@
Subproject commit cb44cb2438f1239bb494ea7e82801e4135598420 Subproject commit 4548e888a1e5c67d98a31f16be7a0323ef3e318c

2
native/zcash_params/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*.params
.idea/

View File

@ -0,0 +1,2 @@
This package just bundles the Zcash sapling circuit parameters
as a crate. To build, copy the `*.params` files into `src`.

View File

@ -17,6 +17,16 @@ class SyncParams {
SyncParams(this.port); SyncParams(this.port);
} }
class PaymentParams {
int account;
String address;
int amount;
int maxAmountPerNote;
SendPort port;
PaymentParams(this.account, this.address, this.amount, this.maxAmountPerNote, this.port);
}
const DEFAULT_ACCOUNT = 1; const DEFAULT_ACCOUNT = 1;
final warp_api_lib = init(); final warp_api_lib = init();
@ -98,12 +108,21 @@ class WarpApi {
return warp_api_lib.get_mempool_balance(); return warp_api_lib.get_mempool_balance();
} }
static String sendPayment(int account, String address, int amount) { static Future<String> sendPayment(int account, String address, int amount, int maxAmountPerNote, void Function(int) f) async {
final txId = warp_api_lib.send_payment(account, address.toNativeUtf8().cast<Int8>(), amount); var receivePort = ReceivePort();
return txId.cast<Utf8>().toDartString(); receivePort.listen((progress) {
f(progress);
});
return await compute(sendPaymentIsolateFn, PaymentParams(account, address, amount, maxAmountPerNote, receivePort.sendPort));
} }
} }
String sendPaymentIsolateFn(PaymentParams params) {
final txId = warp_api_lib.send_payment(params.account, params.address.toNativeUtf8().cast<Int8>(), params.amount, params.maxAmountPerNote, params.port.nativePort);
return txId.cast<Utf8>().toDartString();
}
void warpSyncIsolateFn(SyncParams params) { void warpSyncIsolateFn(SyncParams params) {
warp_api_lib.warp_sync(params.port.nativePort); warp_api_lib.warp_sync(params.port.nativePort);
} }

View File

@ -135,11 +135,15 @@ class NativeLibrary {
int account, int account,
ffi.Pointer<ffi.Int8> address, ffi.Pointer<ffi.Int8> address,
int amount, int amount,
int max_amount_per_note,
int port,
) { ) {
return _send_payment( return _send_payment(
account, account,
address, address,
amount, amount,
max_amount_per_note,
port,
); );
} }
@ -260,12 +264,16 @@ typedef _c_send_payment = ffi.Pointer<ffi.Int8> Function(
ffi.Uint32 account, ffi.Uint32 account,
ffi.Pointer<ffi.Int8> address, ffi.Pointer<ffi.Int8> address,
ffi.Uint64 amount, ffi.Uint64 amount,
ffi.Uint64 max_amount_per_note,
ffi.Int64 port,
); );
typedef _dart_send_payment = ffi.Pointer<ffi.Int8> Function( typedef _dart_send_payment = ffi.Pointer<ffi.Int8> Function(
int account, int account,
ffi.Pointer<ffi.Int8> address, ffi.Pointer<ffi.Int8> address,
int amount, int amount,
int max_amount_per_note,
int port,
); );
typedef _c_try_warp_sync = ffi.Int8 Function(); typedef _c_try_warp_sync = ffi.Int8 Function();

View File

@ -36,6 +36,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.6.1" version: "2.6.1"
auto_size_text_pk:
dependency: transitive
description:
name: auto_size_text_pk
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
barcode_scan: barcode_scan:
dependency: "direct main" dependency: "direct main"
description: description:
@ -133,7 +140,7 @@ packages:
name: cli_util name: cli_util
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.3.1" version: "0.3.2"
clock: clock:
dependency: transitive dependency: transitive
description: description:
@ -183,6 +190,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.3.12" version: "1.3.12"
decimal:
dependency: "direct main"
description:
name: decimal
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
fake_async: fake_async:
dependency: transitive dependency: transitive
description: description:
@ -408,6 +422,48 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.9.3" version: "1.9.3"
package_info_plus:
dependency: "direct main"
description:
name: package_info_plus
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.3"
package_info_plus_linux:
dependency: transitive
description:
name: package_info_plus_linux
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
package_info_plus_macos:
dependency: transitive
description:
name: package_info_plus_macos
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.1"
package_info_plus_platform_interface:
dependency: transitive
description:
name: package_info_plus_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
package_info_plus_web:
dependency: transitive
description:
name: package_info_plus_web
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
package_info_plus_windows:
dependency: transitive
description:
name: package_info_plus_windows
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
path: path:
dependency: "direct main" dependency: "direct main"
description: description:
@ -520,6 +576,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.5" version: "2.1.5"
rational:
dependency: transitive
description:
name: rational
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.1"
rflutter_alert: rflutter_alert:
dependency: "direct main" dependency: "direct main"
description: description:
@ -700,6 +763,20 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.0" version: "2.1.0"
velocity_x:
dependency: "direct main"
description:
name: velocity_x
url: "https://pub.dartlang.org"
source: hosted
version: "3.3.0"
vxstate:
dependency: transitive
description:
name: vxstate
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
warp_api: warp_api:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at # Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.0.0+5 version: 1.0.1+14
environment: environment:
sdk: ">=2.9.0 <3.0.0" sdk: ">=2.9.0 <3.0.0"
@ -40,6 +40,9 @@ dependencies:
shared_preferences: any shared_preferences: any
splashscreen: any splashscreen: any
flutter_markdown: any flutter_markdown: any
package_info_plus: any
velocity_x: ^3.3.0
decimal: any
# The following adds the Cupertino Icons font to your application. # The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.