2021-08-05 06:43:05 -07:00
|
|
|
import 'dart:io';
|
|
|
|
|
2021-06-26 07:30:12 -07:00
|
|
|
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';
|
2021-07-07 08:40:05 -07:00
|
|
|
import 'package:decimal/decimal.dart';
|
2021-08-05 06:43:05 -07:00
|
|
|
import 'package:path_provider/path_provider.dart';
|
|
|
|
import 'package:share_plus/share_plus.dart';
|
2021-07-07 08:40:05 -07:00
|
|
|
import 'dart:math' as math;
|
2021-06-26 07:30:12 -07:00
|
|
|
|
|
|
|
import 'main.dart';
|
2021-07-07 08:40:05 -07:00
|
|
|
import 'store.dart';
|
2021-06-26 07:30:12 -07:00
|
|
|
|
|
|
|
class SendPage extends StatefulWidget {
|
2021-07-18 08:59:02 -07:00
|
|
|
final Contact contact;
|
|
|
|
SendPage(this.contact);
|
2021-06-26 07:30:12 -07:00
|
|
|
|
|
|
|
@override
|
|
|
|
SendState createState() => SendState();
|
|
|
|
}
|
|
|
|
|
|
|
|
class SendState extends State<SendPage> {
|
|
|
|
final _formKey = GlobalKey<FormState>();
|
|
|
|
var _address = "";
|
2021-07-07 08:40:05 -07:00
|
|
|
var _amount = Decimal.zero;
|
|
|
|
var _maxAmountPerNote = Decimal.zero;
|
2021-06-26 07:30:12 -07:00
|
|
|
var _balance = 0;
|
|
|
|
final _addressController = TextEditingController();
|
2021-07-09 22:44:34 -07:00
|
|
|
final _memoController = TextEditingController();
|
2021-07-07 08:40:05 -07:00
|
|
|
var _mZEC = true;
|
|
|
|
var _currencyController = _makeMoneyMaskedTextController(true);
|
|
|
|
var _maxAmountPerNoteController = _makeMoneyMaskedTextController(true);
|
|
|
|
var _includeFee = false;
|
|
|
|
var _isExpanded = false;
|
2021-06-26 07:30:12 -07:00
|
|
|
|
|
|
|
@override
|
|
|
|
initState() {
|
2021-07-18 08:59:02 -07:00
|
|
|
if (widget.contact != null)
|
|
|
|
_addressController.text = widget.contact.address;
|
2021-06-26 07:30:12 -07:00
|
|
|
Future.microtask(() async {
|
2021-07-07 08:40:05 -07:00
|
|
|
final balance = await accountManager
|
2021-07-09 06:33:39 -07:00
|
|
|
.getBalanceSpendable(syncStatus.latestHeight - settings.anchorOffset);
|
2021-06-26 07:30:12 -07:00
|
|
|
setState(() {
|
2021-07-07 08:40:05 -07:00
|
|
|
_balance = math.max(balance - DEFAULT_FEE, 0);
|
2021-06-26 07:30:12 -07:00
|
|
|
});
|
|
|
|
});
|
|
|
|
super.initState();
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
return Scaffold(
|
2021-07-07 08:40:05 -07:00
|
|
|
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,
|
|
|
|
),
|
2021-06-26 07:30:12 -07:00
|
|
|
),
|
2021-07-07 08:40:05 -07:00
|
|
|
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: [
|
2021-07-09 22:44:34 -07:00
|
|
|
ListTile(title: TextFormField(
|
|
|
|
decoration: InputDecoration(labelText: 'Memo'),
|
|
|
|
minLines: 4,
|
|
|
|
maxLines: null,
|
|
|
|
keyboardType: TextInputType.multiline,
|
|
|
|
controller: _memoController,
|
|
|
|
)),
|
2021-07-07 08:40:05 -07:00
|
|
|
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,
|
2021-07-09 22:44:34 -07:00
|
|
|
)),
|
2021-07-07 08:40:05 -07:00
|
|
|
]),
|
|
|
|
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),
|
|
|
|
])
|
|
|
|
]))));
|
2021-06-26 07:30:12 -07:00
|
|
|
}
|
|
|
|
|
2021-07-07 08:40:05 -07:00
|
|
|
String _checkAddress(String v) {
|
2021-06-26 07:30:12 -07:00
|
|
|
if (v.isEmpty) return 'Address is empty';
|
|
|
|
if (!WarpApi.validAddress(v)) return 'Invalid Address';
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2021-07-07 08:40:05 -07:00
|
|
|
String _checkAmount(String vs) {
|
2021-06-26 07:30:12 -07:00
|
|
|
final v = double.tryParse(vs);
|
|
|
|
if (v == null) return 'Amount must be a number';
|
|
|
|
if (v <= 0.0) return 'Amount must be positive';
|
|
|
|
if (v > _balance) return 'Not enough balance';
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2021-07-07 08:40:05 -07:00
|
|
|
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() {
|
2021-06-26 07:30:12 -07:00
|
|
|
Navigator.of(context).pop();
|
|
|
|
}
|
|
|
|
|
2021-07-07 08:40:05 -07:00
|
|
|
void _onScan() async {
|
2021-06-26 07:30:12 -07:00
|
|
|
var code = await BarcodeScanner.scan();
|
|
|
|
setState(() {
|
|
|
|
_address = code.rawContent;
|
|
|
|
_addressController.text = _address;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-08-02 22:58:02 -07:00
|
|
|
void _onAmount(String v) {
|
|
|
|
_amount = Decimal.parse(v.replaceAll(',', ''));
|
2021-06-26 07:30:12 -07:00
|
|
|
}
|
|
|
|
|
2021-07-07 08:40:05 -07:00
|
|
|
void _onAddress(v) {
|
2021-06-26 07:30:12 -07:00
|
|
|
_address = v;
|
|
|
|
}
|
|
|
|
|
2021-07-07 08:40:05 -07:00
|
|
|
void _onSavedMaxAmountPerNote(v) {
|
|
|
|
_maxAmountPerNote = Decimal.parse(v);
|
|
|
|
}
|
|
|
|
|
|
|
|
void _onSend() async {
|
2021-06-26 07:30:12 -07:00
|
|
|
final form = _formKey.currentState;
|
|
|
|
if (form == null) return;
|
|
|
|
|
|
|
|
if (form.validate()) {
|
|
|
|
form.save();
|
2021-07-07 08:40:05 -07:00
|
|
|
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);
|
|
|
|
}),
|
|
|
|
],
|
|
|
|
));
|
2021-06-26 07:30:12 -07:00
|
|
|
if (approved) {
|
|
|
|
Navigator.of(context).pop();
|
2021-07-07 08:40:05 -07:00
|
|
|
final snackBar1 = SnackBar(content: Text("Preparing transaction..."));
|
2021-06-26 07:30:12 -07:00
|
|
|
rootScaffoldMessengerKey.currentState.showSnackBar(snackBar1);
|
|
|
|
|
2021-07-07 08:40:05 -07:00
|
|
|
int amount = (_amount * ZECUNIT_DECIMAL).toInt();
|
|
|
|
if (_includeFee) amount -= DEFAULT_FEE;
|
|
|
|
int maxAmountPerNote = (_maxAmountPerNote * ZECUNIT_DECIMAL).toInt();
|
2021-07-09 22:44:34 -07:00
|
|
|
final memo = _memoController.text;
|
2021-06-26 07:30:12 -07:00
|
|
|
|
2021-08-05 06:43:05 -07:00
|
|
|
if (accountManager.canPay) {
|
|
|
|
final tx = await compute(
|
|
|
|
sendPayment,
|
|
|
|
PaymentParams(
|
|
|
|
accountManager.active.id,
|
|
|
|
_address,
|
|
|
|
amount,
|
|
|
|
memo,
|
|
|
|
maxAmountPerNote,
|
|
|
|
settings.anchorOffset,
|
|
|
|
progressPort.sendPort));
|
|
|
|
|
|
|
|
final snackBar2 = SnackBar(content: Text("TX ID: $tx"));
|
|
|
|
rootScaffoldMessengerKey.currentState.showSnackBar(snackBar2);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
Directory tempDir = await getTemporaryDirectory();
|
|
|
|
String filename = "${tempDir.path}/tx.json";
|
|
|
|
|
|
|
|
WarpApi.prepareTx(accountManager.active.id, _address, amount, memo,
|
|
|
|
maxAmountPerNote, settings.anchorOffset, filename);
|
|
|
|
|
|
|
|
Share.shareFiles([filename], subject: "Unsigned Transaction File");
|
|
|
|
|
|
|
|
final snackBar2 = SnackBar(content: Text("TX saved to: $filename"));
|
|
|
|
rootScaffoldMessengerKey.currentState.showSnackBar(snackBar2);
|
|
|
|
}
|
2021-06-26 07:30:12 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-07-07 08:40:05 -07:00
|
|
|
|
|
|
|
static MoneyMaskedTextController _makeMoneyMaskedTextController(bool mZEC) =>
|
|
|
|
MoneyMaskedTextController(
|
|
|
|
decimalSeparator: '.',
|
|
|
|
thousandSeparator: ',',
|
|
|
|
precision: mZEC ? 3 : 8);
|
2021-06-26 07:30:12 -07:00
|
|
|
}
|
|
|
|
|
2021-07-09 22:44:34 -07:00
|
|
|
sendPayment(PaymentParams param) async {
|
|
|
|
param.port.send(0);
|
|
|
|
final tx = await WarpApi.sendPayment(
|
|
|
|
param.account,
|
|
|
|
param.address,
|
|
|
|
param.amount,
|
|
|
|
param.memo,
|
|
|
|
param.maxAmountPerNote,
|
|
|
|
param.anchorOffset, (percent) {
|
|
|
|
param.port.send(percent);
|
2021-07-07 08:40:05 -07:00
|
|
|
});
|
2021-07-09 22:44:34 -07:00
|
|
|
param.port.send(0);
|
2021-06-26 07:30:12 -07:00
|
|
|
return tx;
|
|
|
|
}
|