v1.0.1
This commit is contained in:
parent
b2b5b0072f
commit
2eb76f8232
|
@ -45,6 +45,8 @@ app.*.map.json
|
|||
/android/app/profile
|
||||
/android/app/release
|
||||
|
||||
.gradle/
|
||||
|
||||
target/
|
||||
jniLibs/
|
||||
assets/
|
||||
|
|
|
@ -45,9 +45,9 @@ checksum = "739f4a8db6605981345c5654f3a85b056ce52f37a39d34da03f25bf2151ea16e"
|
|||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "0.7.15"
|
||||
version = "0.7.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5"
|
||||
checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
@ -198,9 +198,9 @@ checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
|
|||
|
||||
[[package]]
|
||||
name = "bitvec"
|
||||
version = "0.19.5"
|
||||
version = "0.19.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8942c8d352ae1838c9dda0b0ca2ab657696ef2232a20147cf1b30ae1a9cb4321"
|
||||
checksum = "a7ba35e9565969edb811639dbebfe34edc0368e472c5018474c8eb2543397f81"
|
||||
dependencies = [
|
||||
"funty",
|
||||
"radium 0.5.3",
|
||||
|
@ -291,9 +291,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "bstr"
|
||||
version = "0.2.15"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a40b47ad93e1a5404e6c18dec46b628214fee441c70f4ab5d6942142cc268a3d"
|
||||
checksum = "90682c8d613ad3373e66de8c6411e0ae2ab2571e879d2efbf73558cc66f21279"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"memchr",
|
||||
|
@ -321,11 +321,11 @@ checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040"
|
|||
|
||||
[[package]]
|
||||
name = "cast"
|
||||
version = "0.2.6"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57cdfa5d50aad6cb4d44dcab6101a7f79925bd59d82ca42f38a9856a28865374"
|
||||
checksum = "4c24dab4283a142afa2fdca129b80ad2c6284e073930f964c3a1293c225ee39a"
|
||||
dependencies = [
|
||||
"rustc_version 0.3.3",
|
||||
"rustc_version 0.4.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -992,13 +992,19 @@ dependencies = [
|
|||
"ahash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
|
||||
|
||||
[[package]]
|
||||
name = "hashlink"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d99cf782f0dc4372d26846bec3de7804ceb5df083c2d4462c0b8d2330e894fa8"
|
||||
dependencies = [
|
||||
"hashbrown",
|
||||
"hashbrown 0.9.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1101,12 +1107,12 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.6.2"
|
||||
version = "1.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "824845a0bf897a9042383849b02c1bc219c2383772efcd5c6f9766fa4b81aef3"
|
||||
checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"hashbrown",
|
||||
"hashbrown 0.11.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1227,9 +1233,9 @@ checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00"
|
|||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.3.4"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525"
|
||||
checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc"
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
|
@ -1289,12 +1295,11 @@ checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a"
|
|||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "6.2.1"
|
||||
version = "6.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c5c51b9083a3c620fa67a2a635d1ce7d95b897e957d6b28ff9a5da960a103a6"
|
||||
checksum = "3d521ee2250f619dd5e06515ba405858d249edc8fae9ddee2dba0695e57db01b"
|
||||
dependencies = [
|
||||
"bitvec 0.19.5",
|
||||
"funty",
|
||||
"bitvec 0.19.4",
|
||||
"lexical-core",
|
||||
"memchr",
|
||||
"version_check",
|
||||
|
@ -1426,15 +1431,6 @@ version = "2.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "petgraph"
|
||||
version = "0.5.1"
|
||||
|
@ -1467,9 +1463,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.6"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc0e1f259c92177c30a4c9d177246edd0a3568b25756a977d0632cf8fa37e905"
|
||||
checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443"
|
||||
|
||||
[[package]]
|
||||
name = "pin-utils"
|
||||
|
@ -1498,15 +1494,15 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "plotters-backend"
|
||||
version = "0.3.0"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b07fffcddc1cb3a1de753caa4e4df03b79922ba43cf882acc1bdd7e8df9f4590"
|
||||
checksum = "d88417318da0eaf0fdcdb51a0ee6c3bed624333bff8f946733049380be67ac1c"
|
||||
|
||||
[[package]]
|
||||
name = "plotters-svg"
|
||||
version = "0.3.0"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b38a02e23bd9604b842a812063aec4ef702b57989c37b655254bb61c471ad211"
|
||||
checksum = "521fa9638fa597e1dc53e9412a4f9cefb01187ee1f7413076f9e6749e2885ba9"
|
||||
dependencies = [
|
||||
"plotters-backend",
|
||||
]
|
||||
|
@ -1762,9 +1758,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.4.6"
|
||||
version = "1.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a26af418b574bd56588335b3a3659a65725d4e636eb1016c2f9e3b38c7cc759"
|
||||
checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
|
@ -1839,11 +1835,11 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.3.3"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee"
|
||||
checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
|
||||
dependencies = [
|
||||
"semver 0.11.0",
|
||||
"semver 1.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1941,17 +1937,14 @@ version = "0.9.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
|
||||
dependencies = [
|
||||
"semver-parser 0.7.0",
|
||||
"semver-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "0.11.0"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6"
|
||||
dependencies = [
|
||||
"semver-parser 0.10.2",
|
||||
]
|
||||
checksum = "5f3aac57ee7f3272d8395c6e4f502f434f0e289fcd62876f70daa008c20dcabe"
|
||||
|
||||
[[package]]
|
||||
name = "semver-parser"
|
||||
|
@ -1959,15 +1952,6 @@ version = "0.7.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "serde"
|
||||
version = "1.0.126"
|
||||
|
@ -2150,7 +2134,6 @@ dependencies = [
|
|||
"anyhow",
|
||||
"bls12_381",
|
||||
"byteorder",
|
||||
"bytes",
|
||||
"criterion",
|
||||
"dotenv",
|
||||
"env_logger",
|
||||
|
@ -2231,18 +2214,18 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.25"
|
||||
version = "1.0.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa6f76457f59514c7eeb4e59d891395fab0b2fd1d40723ae737d64153392e9c6"
|
||||
checksum = "93119e4feac1cbe6c798c34d3a53ea0026b0b1de6a120deef895137c0529bfe2"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.25"
|
||||
version = "1.0.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a36768c0fbf1bb15eca10defa29526bda730a2376c2ab4393ccfa16fb1a318d"
|
||||
checksum = "060d69a0afe7796bf42e9e2ff91f5ee691fb15c53d38b4b62a9a53eb23164745"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -2342,9 +2325,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
|
|||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.7.1"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5fb2ed024293bb19f7a5dc54fe83bf86532a44c12a2bb8ba40d64a4509395ca2"
|
||||
checksum = "570c2eb13b3ab38208130eccd41be92520388791207fde783bda7c1e8ace28d4"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"bytes",
|
||||
|
@ -2543,12 +2526,6 @@ version = "1.13.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06"
|
||||
|
||||
[[package]]
|
||||
name = "ucd-trie"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
version = "0.1.19"
|
||||
|
@ -2560,9 +2537,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.7.1"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796"
|
||||
checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
org.gradle.jvmargs=-Xmx1536M
|
||||
org.gradle.jvmargs=-Xmx4608m
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
|
|
|
@ -2,20 +2,30 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter/services.dart' show rootBundle;
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
|
||||
Future<void> showAbout(BuildContext context) async {
|
||||
final content = await rootBundle.loadString('assets/about.md');
|
||||
showDialog(context: context, barrierDismissible: false,
|
||||
var content = await rootBundle.loadString('assets/about.md');
|
||||
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(
|
||||
title: Text('About ZWallet'),
|
||||
content: Container(width: double.maxFinite, child: Markdown(data: content)),
|
||||
actions: [
|
||||
ElevatedButton(onPressed: () { Navigator.of(context).pop(); }, child: Text('OK'))
|
||||
]
|
||||
|
||||
));
|
||||
|
||||
|
||||
title: Text('About ZWallet'),
|
||||
content: Container(
|
||||
width: double.maxFinite,
|
||||
child: Markdown(data: content),
|
||||
),
|
||||
actions: [
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text('OK'))
|
||||
]));
|
||||
}
|
||||
|
||||
Future<void> showAboutOnce(BuildContext context) async {
|
||||
|
@ -25,4 +35,4 @@ Future<void> showAboutOnce(BuildContext context) async {
|
|||
await showAbout(context);
|
||||
prefs.setBool('about', true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
173
lib/account.dart
173
lib/account.dart
|
@ -1,12 +1,12 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'dart:math' as math;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:local_auth/local_auth.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:warp/store.dart';
|
||||
import 'package:warp_api/warp_api.dart';
|
||||
|
||||
import 'about.dart';
|
||||
|
@ -20,29 +20,33 @@ class AccountPage extends StatefulWidget {
|
|||
class _AccountPageState extends State<AccountPage>
|
||||
with WidgetsBindingObserver, AutomaticKeepAliveClientMixin {
|
||||
Timer _timerSync;
|
||||
int _progress = 0;
|
||||
StreamSubscription _sub;
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
@override
|
||||
initState() {
|
||||
super.initState();
|
||||
print("INITSTATE");
|
||||
Future.microtask(() async {
|
||||
if (await accountManager.isEmpty())
|
||||
Navigator.of(this.context).pushReplacementNamed('/accounts');
|
||||
else {
|
||||
await accountManager.updateUnconfirmedBalance();
|
||||
await accountManager.fetchNotesAndHistory();
|
||||
_sync();
|
||||
_setupTimer();
|
||||
}
|
||||
await accountManager.updateUnconfirmedBalance();
|
||||
await accountManager.fetchNotesAndHistory();
|
||||
_setupTimer();
|
||||
await showAboutOnce(this.context);
|
||||
});
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
super.initState();
|
||||
progressStream.listen((percent) {
|
||||
setState(() {
|
||||
_progress = percent;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
print("DISPOSE");
|
||||
_timerSync?.cancel();
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
|
@ -73,25 +77,31 @@ class _AccountPageState extends State<AccountPage>
|
|||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Observer(
|
||||
builder: (context) =>
|
||||
Text("\u24E9 Wallet - ${accountManager.active.name}")),
|
||||
builder: (context) => Text(
|
||||
"\u24E9 Wallet - ${accountManager.active.name}")),
|
||||
bottom: TabBar(tabs: [
|
||||
Tab(text: "Account"),
|
||||
Tab(text: "Notes"),
|
||||
Tab(text: "History"),
|
||||
]),
|
||||
actions: [
|
||||
PopupMenuButton<String>(
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(child: Text("Accounts"), value: "Accounts"),
|
||||
PopupMenuItem(child: Text("Backup"), value: "Backup"),
|
||||
PopupMenuItem(child: Text("Rescan"), value: "Rescan"),
|
||||
PopupMenuItem(
|
||||
child: Text(settings.nextMode()), value: "Theme"),
|
||||
PopupMenuItem(child: Text("About"), value: "About"),
|
||||
],
|
||||
onSelected: _onMenu,
|
||||
)
|
||||
Observer(builder: (context) {
|
||||
accountManager.canPay;
|
||||
return PopupMenuButton<String>(
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(child: Text("Accounts"), value: "Accounts"),
|
||||
PopupMenuItem(child: Text("Backup"), value: "Backup"),
|
||||
PopupMenuItem(child: Text("Rescan"), value: "Rescan"),
|
||||
if (accountManager.canPay)
|
||||
PopupMenuItem(
|
||||
child: Text("Cold Storage"), value: "Cold"),
|
||||
PopupMenuItem(
|
||||
child: Text(settings.nextMode()), value: "Theme"),
|
||||
PopupMenuItem(child: Text("About"), value: "About"),
|
||||
],
|
||||
onSelected: _onMenu,
|
||||
);
|
||||
})
|
||||
],
|
||||
),
|
||||
body: TabBarView(children: [
|
||||
|
@ -100,10 +110,15 @@ class _AccountPageState extends State<AccountPage>
|
|||
child: Center(
|
||||
child: Column(children: [
|
||||
Observer(
|
||||
builder: (context) => syncStatus.isSynced()
|
||||
? Text('${syncStatus.syncedHeight}', style: Theme.of(this.context).textTheme.caption)
|
||||
: Text(
|
||||
'${syncStatus.syncedHeight} / ${syncStatus.latestHeight}')),
|
||||
builder: (context) => syncStatus.syncedHeight <= 0
|
||||
? Text('Synching')
|
||||
: syncStatus.isSynced()
|
||||
? Text('${syncStatus.syncedHeight}',
|
||||
style: Theme.of(this.context)
|
||||
.textTheme
|
||||
.caption)
|
||||
: Text(
|
||||
'${syncStatus.syncedHeight} / ${syncStatus.latestHeight}')),
|
||||
Padding(padding: EdgeInsets.symmetric(vertical: 8)),
|
||||
Observer(builder: (context) {
|
||||
return Column(children: [
|
||||
|
@ -115,16 +130,20 @@ class _AccountPageState extends State<AccountPage>
|
|||
SelectableText('${accountManager.active.address}'),
|
||||
]);
|
||||
}),
|
||||
Observer(builder: (context) => Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||
textBaseline: TextBaseline.ideographic,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'\u24E9 ${_getBalance_hi(accountManager.balance)}',
|
||||
style: Theme.of(context).textTheme.headline2),
|
||||
Text('${_getBalance_lo(accountManager.balance)}'),
|
||||
])),
|
||||
Observer(
|
||||
builder: (context) => Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||
textBaseline: TextBaseline.ideographic,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'\u24E9 ${_getBalance_hi(accountManager.balance)}',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.headline2),
|
||||
Text(
|
||||
'${_getBalance_lo(accountManager.balance)}'),
|
||||
])),
|
||||
Observer(builder: (context) {
|
||||
final zecPrice = priceStore.zecPrice;
|
||||
final balanceUSD =
|
||||
|
@ -139,23 +158,28 @@ class _AccountPageState extends State<AccountPage>
|
|||
]);
|
||||
}),
|
||||
Padding(padding: EdgeInsets.symmetric(vertical: 8)),
|
||||
Observer(builder: (context) =>
|
||||
(accountManager.unconfirmedBalance != 0) ?
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||
textBaseline: TextBaseline.ideographic,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'${_sign(accountManager.unconfirmedBalance)} ${_getBalance_hi(accountManager.unconfirmedBalance)}',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.headline4
|
||||
?.merge(_unconfirmedStyle())),
|
||||
Text(
|
||||
'${_getBalance_lo(accountManager.unconfirmedBalance)}',
|
||||
style: _unconfirmedStyle()),
|
||||
]) : Container()),
|
||||
Observer(
|
||||
builder: (context) =>
|
||||
(accountManager.unconfirmedBalance != 0)
|
||||
? Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.baseline,
|
||||
textBaseline: TextBaseline.ideographic,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'${_sign(accountManager.unconfirmedBalance)} ${_getBalance_hi(accountManager.unconfirmedBalance)}',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.headline4
|
||||
?.merge(_unconfirmedStyle())),
|
||||
Text(
|
||||
'${_getBalance_lo(accountManager.unconfirmedBalance)}',
|
||||
style: _unconfirmedStyle()),
|
||||
])
|
||||
: Container()),
|
||||
if (_progress > 0)
|
||||
LinearProgressIndicator(value: _progress / 100.0),
|
||||
]))),
|
||||
NoteWidget(),
|
||||
HistoryWidget(),
|
||||
|
@ -190,6 +214,7 @@ class _AccountPageState extends State<AccountPage>
|
|||
}
|
||||
|
||||
_setupTimer() {
|
||||
_sync();
|
||||
_timerSync = Timer.periodic(Duration(seconds: 15), (Timer t) {
|
||||
_trySync();
|
||||
});
|
||||
|
@ -199,6 +224,7 @@ class _AccountPageState extends State<AccountPage>
|
|||
WarpApi.warpSync((int height) async {
|
||||
setState(() {
|
||||
syncStatus.setSyncHeight(height);
|
||||
if (syncStatus.isSynced()) accountManager.fetchNotesAndHistory();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -218,8 +244,9 @@ class _AccountPageState extends State<AccountPage>
|
|||
} else if (res == 0) {
|
||||
syncStatus.setSyncHeight(syncStatus.latestHeight);
|
||||
}
|
||||
await accountManager.fetchNotesAndHistory();
|
||||
}
|
||||
await accountManager.fetchNotesAndHistory();
|
||||
await accountManager.updateBalance();
|
||||
await accountManager.updateUnconfirmedBalance();
|
||||
}
|
||||
|
||||
|
@ -238,6 +265,9 @@ class _AccountPageState extends State<AccountPage>
|
|||
case "Rescan":
|
||||
_rescan();
|
||||
break;
|
||||
case "Cold":
|
||||
_cold();
|
||||
break;
|
||||
case "Theme":
|
||||
settings.toggle();
|
||||
break;
|
||||
|
@ -250,8 +280,7 @@ class _AccountPageState extends State<AccountPage>
|
|||
_backup() async {
|
||||
final localAuth = LocalAuthentication();
|
||||
final didAuthenticate = await localAuth.authenticate(
|
||||
localizedReason: "Please authenticate to show account seed",
|
||||
biometricOnly: true);
|
||||
localizedReason: "Please authenticate to show account seed");
|
||||
if (didAuthenticate) {
|
||||
final seed = await accountManager.getBackup();
|
||||
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 {
|
||||
|
@ -309,7 +363,6 @@ class NoteWidget extends StatefulWidget {
|
|||
|
||||
class _NoteState extends State<NoteWidget> with AutomaticKeepAliveClientMixin {
|
||||
final DateFormat dateFormat = DateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
final latestHeight = syncStatus.latestHeight;
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true; //Set to true
|
||||
|
@ -330,7 +383,7 @@ class _NoteState extends State<NoteWidget> with AutomaticKeepAliveClientMixin {
|
|||
.toList();
|
||||
|
||||
bool _confirmed(int height) {
|
||||
return latestHeight - height >= 10;
|
||||
return syncStatus.latestHeight - height >= 10;
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -338,6 +391,7 @@ class _NoteState extends State<NoteWidget> with AutomaticKeepAliveClientMixin {
|
|||
super.build(context);
|
||||
return SingleChildScrollView(
|
||||
scrollDirection: Axis.vertical,
|
||||
padding: EdgeInsets.only(bottom: 32),
|
||||
child: Observer(
|
||||
builder: (context) => DataTable(
|
||||
columns: [
|
||||
|
@ -376,6 +430,7 @@ class _HistoryState extends State<HistoryWidget>
|
|||
super.build(context);
|
||||
return SingleChildScrollView(
|
||||
scrollDirection: Axis.vertical,
|
||||
padding: EdgeInsets.only(bottom: 64),
|
||||
child: Observer(
|
||||
builder: (context) => DataTable(
|
||||
columns: [
|
||||
|
|
|
@ -54,12 +54,13 @@ class AccountManagerState extends State<AccountManagerPage> {
|
|||
}
|
||||
|
||||
Future<bool> _onAccountDelete(DismissDirection _direction) async {
|
||||
if (accountManager.accounts.length == 1) return false;
|
||||
final confirm = showDialog<bool>(
|
||||
context: this.context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => AlertDialog(
|
||||
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: [
|
||||
TextButton(
|
||||
child: Text('Cancel'),
|
||||
|
@ -68,7 +69,7 @@ class AccountManagerState extends State<AccountManagerPage> {
|
|||
},
|
||||
),
|
||||
TextButton(
|
||||
child: Text('OK'),
|
||||
child: Text('DELETE'),
|
||||
onPressed: () {
|
||||
Navigator.of(this.context).pop(true);
|
||||
},
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:decimal/decimal.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:path/path.dart';
|
||||
|
@ -12,6 +13,9 @@ import 'send.dart';
|
|||
import 'store.dart';
|
||||
|
||||
const ZECUNIT = 100000000.0;
|
||||
var ZECUNIT_DECIMAL = Decimal.parse('100000000');
|
||||
const mZECUNIT = 100000;
|
||||
const DEFAULT_FEE = 1000;
|
||||
|
||||
var accountManager = AccountManager();
|
||||
var priceStore = PriceStore();
|
||||
|
@ -58,7 +62,8 @@ class _ZWalletState extends State<ZWalletApp> {
|
|||
await accountManager.init();
|
||||
await syncStatus.init();
|
||||
await settings.restore();
|
||||
return Future.value(AccountPage());
|
||||
return Future.value(
|
||||
accountManager.accounts.isNotEmpty ? AccountPage() : AccountManagerPage());
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
@ -66,8 +66,11 @@ class _RestorePageState extends State<RestorePage> {
|
|||
if (accountManager.accounts.length == 1) {
|
||||
if (_keyController.text == "")
|
||||
WarpApi.skipToLastHeight(); // single new account -> quick sync
|
||||
else
|
||||
else {
|
||||
final snackBar = SnackBar(content: Text("Scan starting momentarily"));
|
||||
rootScaffoldMessengerKey.currentState.showSnackBar(snackBar);
|
||||
WarpApi.rewindToHeight(0);
|
||||
}
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(context).pushReplacementNamed('/account');
|
||||
|
|
255
lib/send.dart
255
lib/send.dart
|
@ -1,11 +1,16 @@
|
|||
import 'dart:isolate';
|
||||
|
||||
import 'package:barcode_scan/barcode_scan.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_masked_text/flutter_masked_text.dart';
|
||||
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
|
||||
import 'package:warp_api/warp_api.dart';
|
||||
import 'package:decimal/decimal.dart';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'main.dart';
|
||||
import 'store.dart';
|
||||
|
||||
class SendPage extends StatefulWidget {
|
||||
SendPage();
|
||||
|
@ -17,77 +22,112 @@ class SendPage extends StatefulWidget {
|
|||
class SendState extends State<SendPage> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
var _address = "";
|
||||
var _amount = 0.0;
|
||||
var _amount = Decimal.zero;
|
||||
var _maxAmountPerNote = Decimal.zero;
|
||||
var _balance = 0;
|
||||
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
|
||||
initState() {
|
||||
Future.microtask(() async {
|
||||
final balance = await accountManager.getBalanceSpendable(syncStatus.latestHeight - 10);
|
||||
final balance = await accountManager
|
||||
.getBalanceSpendable(syncStatus.latestHeight - 10);
|
||||
setState(() {
|
||||
_balance = balance;
|
||||
_balance = math.max(balance - DEFAULT_FEE, 0);
|
||||
});
|
||||
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
debugPrint(_address);
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text('Send ZEC')),
|
||||
body: Form(
|
||||
key: _formKey,
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(20),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Expanded(child: TextFormField(
|
||||
decoration: InputDecoration(labelText: 'Send ZEC to...'),
|
||||
minLines: 4, maxLines: null,
|
||||
keyboardType: TextInputType.multiline,
|
||||
controller: _addressController,
|
||||
onSaved: onAddress,
|
||||
validator: checkAddress,
|
||||
appBar: AppBar(title: Text('Send ZEC')),
|
||||
body: Form(
|
||||
key: _formKey,
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.all(20),
|
||||
child: Column(children: <Widget>[
|
||||
Row(children: <Widget>[
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
decoration:
|
||||
InputDecoration(labelText: 'Send ZEC to...'),
|
||||
minLines: 4,
|
||||
maxLines: null,
|
||||
keyboardType: TextInputType.multiline,
|
||||
controller: _addressController,
|
||||
onSaved: _onAddress,
|
||||
validator: _checkAddress,
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(icon: new Icon(MdiIcons.qrcodeScan), onPressed: onScan)
|
||||
]
|
||||
),
|
||||
TextFormField(
|
||||
decoration: InputDecoration(labelText: 'Amount'),
|
||||
keyboardType: TextInputType.number,
|
||||
controller: _currencyController,
|
||||
validator: checkAmount,
|
||||
onSaved: onAmount
|
||||
),
|
||||
Padding(padding: EdgeInsets.all(8)),
|
||||
Text("Spendable: ${_balance / ZECUNIT } ZEC"),
|
||||
ButtonBar(
|
||||
children: [
|
||||
IconButton(icon: new Icon(MdiIcons.send), onPressed: onSend),
|
||||
IconButton(icon: new Icon(MdiIcons.cancel), onPressed: onCancel)
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
IconButton(
|
||||
icon: new Icon(MdiIcons.qrcodeScan), onPressed: _onScan)
|
||||
]),
|
||||
Row(children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
decoration: InputDecoration(labelText: 'Amount'),
|
||||
keyboardType: TextInputType.number,
|
||||
controller: _currencyController,
|
||||
validator: _checkAmount,
|
||||
onSaved: _onAmount)),
|
||||
TextButton(child: Text('MAX'), onPressed: _onMax),
|
||||
]),
|
||||
ExpansionPanelList(
|
||||
expansionCallback: (_, isExpanded) {
|
||||
setState(() {
|
||||
_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 (!WarpApi.validAddress(v)) return 'Invalid Address';
|
||||
return null;
|
||||
}
|
||||
|
||||
String checkAmount(String vs) {
|
||||
String _checkAmount(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';
|
||||
|
@ -95,11 +135,44 @@ class SendState extends State<SendPage> {
|
|||
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();
|
||||
}
|
||||
|
||||
void onScan() async {
|
||||
void _onScan() async {
|
||||
var code = await BarcodeScanner.scan();
|
||||
setState(() {
|
||||
_address = code.rawContent;
|
||||
|
@ -107,58 +180,90 @@ class SendState extends State<SendPage> {
|
|||
});
|
||||
}
|
||||
|
||||
void onAmount(v) {
|
||||
_amount = double.parse(v);
|
||||
void _onAmount(v) {
|
||||
_amount = Decimal.parse(v);
|
||||
}
|
||||
|
||||
void onAddress(v) {
|
||||
void _onAddress(v) {
|
||||
_address = v;
|
||||
}
|
||||
|
||||
void onSend() async {
|
||||
void _onSavedMaxAmountPerNote(v) {
|
||||
_maxAmountPerNote = Decimal.parse(v);
|
||||
}
|
||||
|
||||
void _onSend() async {
|
||||
final form = _formKey.currentState;
|
||||
if (form == null) return;
|
||||
|
||||
if (form.validate()) {
|
||||
form.save();
|
||||
final approved = await showDialog(context: context, barrierDismissible: false,
|
||||
builder: (BuildContext context) =>
|
||||
AlertDialog(title: Text('Please Confirm'),
|
||||
content: SingleChildScrollView(
|
||||
child: Text("Sending $_amount ZEC to $_address")
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(child: Text('Cancel'), onPressed: () {Navigator.of(context).pop(false);}),
|
||||
TextButton(child: Text('Approve'), onPressed: () {Navigator.of(context).pop(true);}),
|
||||
],
|
||||
)
|
||||
);
|
||||
final approved = await showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (BuildContext context) => AlertDialog(
|
||||
title: Text('Please Confirm'),
|
||||
content: SingleChildScrollView(
|
||||
child: Text("Sending $_amount ZEC to $_address")),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
child: Text('Cancel'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(false);
|
||||
}),
|
||||
TextButton(
|
||||
child: Text('Approve'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(true);
|
||||
}),
|
||||
],
|
||||
));
|
||||
if (approved) {
|
||||
Navigator.of(context).pop();
|
||||
final snackBar1 = SnackBar(
|
||||
content: Text("Preparing transaction..."));
|
||||
final snackBar1 = SnackBar(content: Text("Preparing transaction..."));
|
||||
rootScaffoldMessengerKey.currentState.showSnackBar(snackBar1);
|
||||
|
||||
final amount = (_amount * ZECUNIT).toInt();
|
||||
final tx = await compute(sendPayment, SendPaymentParam(accountManager.active.id, _address, amount));
|
||||
int amount = (_amount * ZECUNIT_DECIMAL).toInt();
|
||||
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(
|
||||
content: Text("TX ID: $tx"));
|
||||
final snackBar2 = SnackBar(content: Text("TX ID: $tx"));
|
||||
rootScaffoldMessengerKey.currentState.showSnackBar(snackBar2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static MoneyMaskedTextController _makeMoneyMaskedTextController(bool mZEC) =>
|
||||
MoneyMaskedTextController(
|
||||
decimalSeparator: '.',
|
||||
thousandSeparator: ',',
|
||||
precision: mZEC ? 3 : 8);
|
||||
}
|
||||
|
||||
class SendPaymentParam {
|
||||
int account;
|
||||
String address;
|
||||
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) {
|
||||
final tx = WarpApi.sendPayment(param.account, param.address, param.amount);
|
||||
sendPayment(SendPaymentParam param) async {
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:isolate';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
|
@ -26,7 +28,7 @@ abstract class _Settings with Store {
|
|||
@action
|
||||
Future<void> restore() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final prefMode = await prefs.getString('theme') ?? "light";
|
||||
final prefMode = prefs.getString('theme') ?? "light";
|
||||
if (prefMode == "light")
|
||||
mode = ThemeMode.light;
|
||||
else
|
||||
|
@ -65,7 +67,7 @@ abstract class _AccountManager with Store {
|
|||
List<Tx> txs = [];
|
||||
|
||||
@observable
|
||||
List<Account> accounts;
|
||||
List<Account> accounts = [];
|
||||
|
||||
Future<void> init() async {
|
||||
db = await getDatabase();
|
||||
|
@ -87,11 +89,13 @@ abstract class _AccountManager with Store {
|
|||
|
||||
@action
|
||||
Future<void> setActiveAccount(Account account) async {
|
||||
if (account == null) return;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
prefs.setInt('account', account.id);
|
||||
this.active = account;
|
||||
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;
|
||||
await fetchNotesAndHistory();
|
||||
}
|
||||
|
@ -99,12 +103,14 @@ abstract class _AccountManager with Store {
|
|||
@action
|
||||
setActiveAccountId(int idAccount) {
|
||||
final account = accounts.firstWhere((account) => account.id == idAccount,
|
||||
orElse: () => accounts[0]);
|
||||
orElse: () => accounts.isNotEmpty ? accounts[0] : null);
|
||||
setActiveAccount(account);
|
||||
}
|
||||
|
||||
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;
|
||||
final row = res[0];
|
||||
final backup = row['seed'] ?? row['sk'] ?? row['ivk'];
|
||||
|
@ -112,13 +118,17 @@ abstract class _AccountManager with Store {
|
|||
}
|
||||
|
||||
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;
|
||||
return res[0]['value'] ?? 0;
|
||||
}
|
||||
|
||||
Future<int> getBalanceSpendable(int height) async {
|
||||
final List<Map> res = await db.rawQuery("SELECT SUM(value) AS value FROM received_notes WHERE account = ?1 AND (spent IS NULL OR spent = 0) AND height <= ?2", [active.id, height]);
|
||||
final List<Map> res = await db.rawQuery(
|
||||
"SELECT SUM(value) AS value FROM received_notes WHERE account = ?1 AND (spent IS NULL OR spent = 0) AND height <= ?2",
|
||||
[active.id, height]);
|
||||
if (res.isEmpty) return 0;
|
||||
return res[0]['value'] ?? 0;
|
||||
}
|
||||
|
@ -129,16 +139,19 @@ abstract class _AccountManager with Store {
|
|||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
Future<List<Account>> _list() async {
|
||||
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 "
|
||||
"n.spent IS NULL OR n.spent = 0 "
|
||||
"GROUP BY name", []);
|
||||
return res.map((r) => Account(r['account'], r['name'], r['address'], r['balance'])).toList();
|
||||
"WITH notes AS (SELECT a.id_account, a.name, a.address, CASE WHEN r.spent IS NULL THEN r.value ELSE 0 END AS nv FROM accounts a LEFT JOIN received_notes r ON a.id_account = r.account) "
|
||||
"SELECT id_account, name, address, COALESCE(sum(nv), 0) AS balance FROM notes GROUP by id_account",
|
||||
[]);
|
||||
return res
|
||||
.map((r) =>
|
||||
Account(r['id_account'], r['name'], r['address'], r['balance']))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@action
|
||||
|
@ -146,17 +159,25 @@ abstract class _AccountManager with Store {
|
|||
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");
|
||||
|
||||
@action Future<void> fetchNotesAndHistory() async {
|
||||
balance = await _getBalance();
|
||||
@action
|
||||
Future<void> fetchNotesAndHistory() async {
|
||||
if (active == null) return;
|
||||
await updateBalance();
|
||||
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",
|
||||
[active.id]);
|
||||
notes = res.map((row) {
|
||||
final height = row['height'];
|
||||
final timestamp = dateFormat.format(
|
||||
DateTime.fromMillisecondsSinceEpoch(row['timestamp'] * 1000));
|
||||
final timestamp = dateFormat
|
||||
.format(DateTime.fromMillisecondsSinceEpoch(row['timestamp'] * 1000));
|
||||
return Note(height, timestamp, row['value'] / ZECUNIT);
|
||||
}).toList();
|
||||
|
||||
|
@ -165,11 +186,17 @@ abstract class _AccountManager with Store {
|
|||
[active.id]);
|
||||
txs = res2.map((row) {
|
||||
final txid = hex.encode(row['txid']).substring(0, 8);
|
||||
final timestamp = dateFormat.format(
|
||||
DateTime.fromMillisecondsSinceEpoch(row['timestamp'] * 1000));
|
||||
final timestamp = dateFormat
|
||||
.format(DateTime.fromMillisecondsSinceEpoch(row['timestamp'] * 1000));
|
||||
return Tx(row['height'], timestamp, txid, row['value'] / ZECUNIT);
|
||||
}).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 {
|
||||
|
@ -186,11 +213,11 @@ class PriceStore = _PriceStore with _$PriceStore;
|
|||
abstract class _PriceStore with Store {
|
||||
@observable
|
||||
double zecPrice = 0.0;
|
||||
|
||||
|
||||
@action
|
||||
Future<void> fetchZecPrice() async {
|
||||
final base = "api.binance.com";
|
||||
final uri = Uri.https(base, '/api/v3/avgPrice', { 'symbol': 'ZECUSDT' });
|
||||
final uri = Uri.https(base, '/api/v3/avgPrice', {'symbol': 'ZECUSDT'});
|
||||
final rep = await http.get(uri);
|
||||
if (rep.statusCode == 200) {
|
||||
final json = convert.jsonDecode(rep.body) as Map<String, dynamic>;
|
||||
|
@ -224,13 +251,13 @@ abstract class _SyncStatus with Store {
|
|||
|
||||
@action
|
||||
setSyncHeight(int height) {
|
||||
syncedHeight = height;
|
||||
syncedHeight = height;
|
||||
}
|
||||
|
||||
@action
|
||||
Future<bool> update() async {
|
||||
final _syncedHeight = Sqflite.firstIntValue(
|
||||
await _db.rawQuery("SELECT MAX(height) FROM blocks")) ??
|
||||
await _db.rawQuery("SELECT MAX(height) FROM blocks")) ??
|
||||
0;
|
||||
if (_syncedHeight > 0) syncedHeight = _syncedHeight;
|
||||
latestHeight = await WarpApi.getLatestHeight();
|
||||
|
@ -239,6 +266,9 @@ abstract class _SyncStatus with Store {
|
|||
}
|
||||
}
|
||||
|
||||
var progressPort = ReceivePort();
|
||||
var progressStream = progressPort.asBroadcastStream();
|
||||
|
||||
class Note {
|
||||
int height;
|
||||
String timestamp;
|
||||
|
|
|
@ -24,7 +24,7 @@ uint32_t new_account(char *name, char *data);
|
|||
|
||||
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);
|
||||
|
||||
|
|
|
@ -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();
|
||||
r.block_on(async {
|
||||
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 {
|
||||
Err(err) => {
|
||||
log::error!("{}", err);
|
||||
"".to_string()
|
||||
err.to_string()
|
||||
}
|
||||
Ok(tx_id) => tx_id,
|
||||
}
|
||||
|
|
|
@ -61,9 +61,11 @@ pub unsafe extern "C" fn send_payment(
|
|||
account: u32,
|
||||
address: *mut c_char,
|
||||
amount: u64,
|
||||
max_amount_per_note: u64,
|
||||
port: i64,
|
||||
) -> *const c_char {
|
||||
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()
|
||||
}
|
||||
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit cb44cb2438f1239bb494ea7e82801e4135598420
|
||||
Subproject commit 4548e888a1e5c67d98a31f16be7a0323ef3e318c
|
|
@ -0,0 +1,2 @@
|
|||
*.params
|
||||
.idea/
|
|
@ -0,0 +1,2 @@
|
|||
This package just bundles the Zcash sapling circuit parameters
|
||||
as a crate. To build, copy the `*.params` files into `src`.
|
|
@ -17,6 +17,16 @@ class SyncParams {
|
|||
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;
|
||||
|
||||
final warp_api_lib = init();
|
||||
|
@ -98,12 +108,21 @@ class WarpApi {
|
|||
return warp_api_lib.get_mempool_balance();
|
||||
}
|
||||
|
||||
static String sendPayment(int account, String address, int amount) {
|
||||
final txId = warp_api_lib.send_payment(account, address.toNativeUtf8().cast<Int8>(), amount);
|
||||
return txId.cast<Utf8>().toDartString();
|
||||
static Future<String> sendPayment(int account, String address, int amount, int maxAmountPerNote, void Function(int) f) async {
|
||||
var receivePort = ReceivePort();
|
||||
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) {
|
||||
warp_api_lib.warp_sync(params.port.nativePort);
|
||||
}
|
||||
|
|
|
@ -135,11 +135,15 @@ class NativeLibrary {
|
|||
int account,
|
||||
ffi.Pointer<ffi.Int8> address,
|
||||
int amount,
|
||||
int max_amount_per_note,
|
||||
int port,
|
||||
) {
|
||||
return _send_payment(
|
||||
account,
|
||||
address,
|
||||
amount,
|
||||
max_amount_per_note,
|
||||
port,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -260,12 +264,16 @@ typedef _c_send_payment = ffi.Pointer<ffi.Int8> Function(
|
|||
ffi.Uint32 account,
|
||||
ffi.Pointer<ffi.Int8> address,
|
||||
ffi.Uint64 amount,
|
||||
ffi.Uint64 max_amount_per_note,
|
||||
ffi.Int64 port,
|
||||
);
|
||||
|
||||
typedef _dart_send_payment = ffi.Pointer<ffi.Int8> Function(
|
||||
int account,
|
||||
ffi.Pointer<ffi.Int8> address,
|
||||
int amount,
|
||||
int max_amount_per_note,
|
||||
int port,
|
||||
);
|
||||
|
||||
typedef _c_try_warp_sync = ffi.Int8 Function();
|
||||
|
|
79
pubspec.lock
79
pubspec.lock
|
@ -36,6 +36,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -133,7 +140,7 @@ packages:
|
|||
name: cli_util
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.3.1"
|
||||
version: "0.3.2"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -183,6 +190,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.12"
|
||||
decimal:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: decimal
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -408,6 +422,48 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -520,6 +576,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.5"
|
||||
rational:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: rational
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
rflutter_alert:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -700,6 +763,20 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
|
@ -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.
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
version: 1.0.0+5
|
||||
version: 1.0.1+14
|
||||
|
||||
environment:
|
||||
sdk: ">=2.9.0 <3.0.0"
|
||||
|
@ -40,6 +40,9 @@ dependencies:
|
|||
shared_preferences: any
|
||||
splashscreen: 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.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
|
|
Loading…
Reference in New Issue