zwallet/lib/send.dart

499 lines
18 KiB
Dart
Raw Normal View History

2021-09-25 02:09:41 -07:00
import 'dart:convert';
2021-08-05 06:43:05 -07:00
import 'dart:io';
import 'dart:ui';
2021-08-05 06:43:05 -07:00
2021-06-26 07:30:12 -07:00
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
2021-08-06 00:53:54 -07:00
import 'package:mobx/mobx.dart';
2021-06-26 07:30:12 -07:00
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-09-12 04:24:40 -07:00
import 'package:flutter_typeahead/flutter_typeahead.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-08-15 09:18:09 -07:00
import 'generated/l10n.dart';
2021-06-26 07:30:12 -07:00
class SendPage extends StatefulWidget {
2021-09-10 02:56:15 -07:00
final Contact? contact;
2021-08-06 00:53:54 -07:00
2021-07-18 08:59:02 -07:00
SendPage(this.contact);
2021-06-26 07:30:12 -07:00
@override
SendState createState() => SendState();
}
class SendState extends State<SendPage> {
2021-09-21 05:52:52 -07:00
static final zero = decimalFormat(0, 3);
2021-06-26 07:30:12 -07:00
final _formKey = GlobalKey<FormState>();
var _address = "";
2021-08-06 00:53:54 -07:00
var _amount = 0;
2021-07-07 08:40:05 -07:00
var _maxAmountPerNote = Decimal.zero;
2021-09-27 01:57:55 -07:00
var _sBalance = 0;
var _tBalance = 0;
var _excludedBalance = 0;
var _underConfirmedBalance = 0;
2021-06-26 07:30:12 -07:00
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;
2021-09-14 09:08:42 -07:00
var _inputInZEC = true;
2021-09-21 05:52:52 -07:00
var _zecAmountController = TextEditingController(text: zero);
var _fiatAmountController = TextEditingController(text: zero);
final _maxAmountController = TextEditingController(text: zero);
2021-07-07 08:40:05 -07:00
var _isExpanded = false;
2021-08-23 05:47:48 -07:00
var _shieldTransparent = settings.shieldBalance;
2021-09-10 02:56:15 -07:00
ReactionDisposer? _priceAutorunDispose;
ReactionDisposer? _newBlockAutorunDispose;
2021-06-26 07:30:12 -07:00
@override
initState() {
2021-07-18 08:59:02 -07:00
if (widget.contact != null)
2021-09-10 02:56:15 -07:00
_addressController.text = widget.contact!.address;
2021-09-14 09:08:42 -07:00
_updateFiatAmount();
2021-06-26 07:30:12 -07:00
super.initState();
2021-08-06 00:53:54 -07:00
2021-08-06 19:18:20 -07:00
_priceAutorunDispose = autorun((_) {
2021-09-14 09:08:42 -07:00
_updateFiatAmount();
2021-08-06 00:53:54 -07:00
});
_newBlockAutorunDispose = autorun((_) async {
2021-09-27 01:57:55 -07:00
final _ = accountManager.dataEpoch;
final sBalance = await accountManager.getShieldedBalance();
final tBalance = accountManager.tbalance;
final excludedBalance = await accountManager.getExcludedBalance();
2021-09-27 01:57:55 -07:00
final underConfirmedBalance =
await accountManager.getUnderConfirmedBalance();
setState(() {
2021-09-27 01:57:55 -07:00
_sBalance = sBalance;
_tBalance = tBalance;
_excludedBalance = excludedBalance;
_underConfirmedBalance = underConfirmedBalance;
});
});
2021-06-26 07:30:12 -07:00
}
2021-08-06 19:18:20 -07:00
@override
void dispose() {
_priceAutorunDispose?.call();
_newBlockAutorunDispose?.call();
2021-08-06 19:18:20 -07:00
super.dispose();
}
2021-06-26 07:30:12 -07:00
@override
Widget build(BuildContext context) {
2021-09-26 11:44:19 -07:00
final s = S.of(context);
2021-06-26 07:30:12 -07:00
return Scaffold(
2021-09-26 11:44:19 -07:00
appBar: AppBar(title: Text(s.sendCointicker(coin.ticker))),
body: GestureDetector(
onTap: () {
FocusScope.of(context).unfocus();
},
child: Form(
key: _formKey,
child: SingleChildScrollView(
padding: EdgeInsets.all(20),
child: Column(children: <Widget>[
Row(children: <Widget>[
Expanded(
2021-09-27 01:57:55 -07:00
child: TypeAheadFormField(
textFieldConfiguration: TextFieldConfiguration(
controller: _addressController,
decoration: InputDecoration(
labelText: s.sendCointickerTo(coin.ticker)),
minLines: 4,
maxLines: 10,
keyboardType: TextInputType.multiline,
2021-09-26 11:44:19 -07:00
),
2021-09-27 01:57:55 -07:00
onSaved: _onAddress,
validator: _checkAddress,
onSuggestionSelected: (Contact contact) {
_addressController.text = contact.name;
},
suggestionsCallback: (String pattern) {
return contacts.contacts.where((c) => c.name
.toLowerCase()
.contains(pattern.toLowerCase()));
},
itemBuilder: (BuildContext context, Contact c) =>
ListTile(title: Text(c.name)),
noItemsFoundBuilder: (_) => SizedBox(),
)),
2021-09-26 11:44:19 -07:00
IconButton(
icon: new Icon(MdiIcons.qrcodeScan),
onPressed: _onScan)
]),
Row(children: [
Expanded(
child: TextFormField(
2021-09-27 01:57:55 -07:00
style: !_inputInZEC
? TextStyle(fontWeight: FontWeight.w200)
: TextStyle(),
decoration: InputDecoration(
labelText:
s.amountInSettingscurrency(coin.ticker)),
controller: _zecAmountController,
keyboardType: TextInputType.number,
inputFormatters: [makeInputFormatter(_mZEC)],
validator: _checkAmount,
onTap: () => setState(() {
_inputInZEC = true;
}),
onChanged: (_) {
_updateFiatAmount();
},
onSaved: _onAmount,
)),
TextButton(child: Text(s.max), onPressed: _onMax),
2021-09-26 11:44:19 -07:00
]),
Row(children: [
Expanded(
2021-09-27 01:57:55 -07:00
child: TextFormField(
style: _inputInZEC
? TextStyle(fontWeight: FontWeight.w200)
: TextStyle(),
decoration: InputDecoration(
labelText: s.amountInSettingscurrency(
settings.currency)),
controller: _fiatAmountController,
keyboardType: TextInputType.number,
inputFormatters: [makeInputFormatter(_mZEC)],
validator: (v) => _checkAmount(v, isFiat: true),
onTap: () => setState(() {
_inputInZEC = false;
}),
onChanged: (_) {
_updateAmount();
}))
2021-09-26 11:44:19 -07:00
]),
2021-09-27 01:57:55 -07:00
BalanceTable(_sBalance, _tBalance, _excludedBalance,
_underConfirmedBalance),
2021-09-26 11:44:19 -07:00
ExpansionPanelList(
expansionCallback: (_, isExpanded) {
setState(() {
_isExpanded = !isExpanded;
});
},
children: [
ExpansionPanel(
headerBuilder: (_, __) =>
2021-09-27 01:57:55 -07:00
ListTile(title: Text(s.advancedOptions)),
2021-09-26 11:44:19 -07:00
body: Column(children: [
ListTile(
2021-08-06 00:53:54 -07:00
title: TextFormField(
2021-09-27 01:57:55 -07:00
decoration:
InputDecoration(labelText: s.memo),
2021-09-26 11:44:19 -07:00
minLines: 4,
maxLines: null,
keyboardType: TextInputType.multiline,
controller: _memoController,
),
),
CheckboxListTile(
title: Text(s.roundToMillis),
value: _mZEC,
onChanged: _onChangedmZEC),
if (accountManager.canPay)
CheckboxListTile(
2021-09-27 01:57:55 -07:00
title: Text(s.shieldTransparentBalance),
2021-09-12 04:24:40 -07:00
value: _shieldTransparent,
2021-09-26 11:44:19 -07:00
onChanged: _onChangedShieldBalance,
),
ListTile(
title: TextFormField(
2021-09-27 01:57:55 -07:00
decoration: InputDecoration(
labelText: s.maxAmountPerNote),
keyboardType: TextInputType.number,
controller: _maxAmountController,
inputFormatters: [makeInputFormatter(_mZEC)],
validator: _checkMaxAmountPerNote,
onSaved: _onSavedMaxAmountPerNote,
)),
2021-09-26 11:44:19 -07:00
]),
isExpanded: _isExpanded,
)
]),
Padding(padding: EdgeInsets.all(8)),
ButtonBar(
children: confirmButtons(context, _onSend,
2021-09-27 01:57:55 -07:00
okLabel: s.send, okIcon: Icon(MdiIcons.send)))
2021-09-26 11:44:19 -07:00
])))));
2021-06-26 07:30:12 -07:00
}
2021-09-10 02:56:15 -07:00
String? _checkAddress(String? v) {
2021-09-26 11:44:19 -07:00
final s = S.of(context);
if (v == null || v.isEmpty) return s.addressIsEmpty;
2021-09-12 04:24:40 -07:00
final c = contacts.contacts.where((c) => c.name == v);
if (c.isNotEmpty) return null;
2021-08-27 02:51:34 -07:00
final zaddr = WarpApi.getSaplingFromUA(v);
if (zaddr.isNotEmpty) return null;
2021-09-26 11:44:19 -07:00
if (!WarpApi.validAddress(v)) return s.invalidAddress;
2021-06-26 07:30:12 -07:00
return null;
}
2021-09-26 11:44:19 -07:00
String? _checkAmount(String? vs, {bool isFiat: false}) {
final s = S.of(context);
if (vs == null) return s.amountMustBeANumber;
if (!checkNumber(vs)) return s.amountMustBeANumber;
2021-09-10 02:56:15 -07:00
final v = parseNumber(vs);
2021-09-26 11:44:19 -07:00
if (v < 0.0) return s.amountMustBePositive;
if (!isFiat && v == 0.0) return s.amountMustBePositive;
2021-09-27 01:57:55 -07:00
if (!isFiat && amountInZAT(Decimal.parse(v.toString())) > spendable)
2021-09-26 11:44:19 -07:00
return s.notEnoughBalance;
2021-06-26 07:30:12 -07:00
return null;
}
2021-09-10 02:56:15 -07:00
String? _checkMaxAmountPerNote(String? vs) {
2021-09-26 11:44:19 -07:00
final s = S.of(context);
if (vs == null) return s.amountMustBeANumber;
if (!checkNumber(vs)) return s.amountMustBeANumber;
2021-09-10 02:56:15 -07:00
final v = parseNumber(vs);
2021-09-26 11:44:19 -07:00
if (v < 0.0) return s.amountMustBePositive;
2021-07-07 08:40:05 -07:00
return null;
}
void _onMax() {
setState(() {
_mZEC = false;
_zecAmountController.text = amountToString(spendable);
2021-09-14 09:08:42 -07:00
_updateFiatAmount();
2021-07-07 08:40:05 -07:00
});
}
2021-09-10 02:56:15 -07:00
void _onChangedmZEC(bool? v) {
if (v == null) return;
2021-07-07 08:40:05 -07:00
setState(() {
_mZEC = v;
2021-09-14 09:08:42 -07:00
_zecAmountController.text = _trimToPrecision(_zecAmountController.text);
_fiatAmountController.text = _trimToPrecision(_fiatAmountController.text);
2021-07-07 08:40:05 -07:00
});
}
2021-09-10 02:56:15 -07:00
void _onChangedShieldBalance(bool? v) {
if (v == null) return;
2021-08-23 05:47:48 -07:00
setState(() {
_shieldTransparent = v;
});
}
2021-07-07 08:40:05 -07:00
void _onScan() async {
2021-09-10 02:56:15 -07:00
final code = await scanCode(context);
2021-09-25 02:09:41 -07:00
if (code != null) {
if (_checkAddress(code) != null) {
final json = WarpApi.parsePaymentURI(code);
final payment = DecodedPaymentURI.fromJson(jsonDecode(json));
setState(() {
_address = payment.address;
_addressController.text = _address;
_memoController.text = payment.memo;
_amount = payment.amount;
_zecAmountController.text = amountFromZAT(_amount);
});
2021-09-26 11:44:19 -07:00
} else {
2021-09-25 02:09:41 -07:00
setState(() {
_address = code;
_addressController.text = _address;
});
}
}
2021-06-26 07:30:12 -07:00
}
2021-09-10 02:56:15 -07:00
void _onAmount(String? vs) {
2021-09-21 07:16:31 -07:00
final v = parseNumber(vs);
_amount = amountInZAT(Decimal.parse(v.toString()));
2021-06-26 07:30:12 -07:00
}
2021-07-07 08:40:05 -07:00
void _onAddress(v) {
2021-09-12 04:24:40 -07:00
final c = contacts.contacts.where((c) => c.name == v);
if (c.isEmpty)
_address = v;
else {
_address = c.first.address;
}
2021-06-26 07:30:12 -07:00
}
2021-09-21 07:16:31 -07:00
void _onSavedMaxAmountPerNote(vs) {
final v = parseNumber(vs);
_maxAmountPerNote = Decimal.parse(v.toString());
2021-07-07 08:40:05 -07:00
}
2021-09-14 09:08:42 -07:00
void _updateAmount() {
final rate = 1.0 / priceStore.zecPrice;
final amount = parseNumber(_fiatAmountController.text);
final otherAmount = _formatCurrency(amount * rate);
setState(() {
_zecAmountController.text = otherAmount;
});
}
void _updateFiatAmount() {
final rate = priceStore.zecPrice;
final amount = parseNumber(_zecAmountController.text);
final otherAmount = _formatCurrency(amount * rate);
setState(() {
_fiatAmountController.text = otherAmount;
});
2021-08-06 00:53:54 -07:00
}
2021-09-21 05:52:52 -07:00
String _formatCurrency(double v) => decimalFormat(v, precision(_mZEC));
2021-09-14 09:08:42 -07:00
2021-07-07 08:40:05 -07:00
void _onSend() async {
2021-09-26 11:44:19 -07:00
final s = S.of(context);
2021-06-26 07:30:12 -07:00
final form = _formKey.currentState;
if (form == null) return;
if (form.validate()) {
form.save();
2021-08-06 00:53:54 -07:00
final aZEC = (Decimal.fromInt(_amount) / ZECUNIT_DECIMAL).toString();
2021-07-07 08:40:05 -07:00
final approved = await showDialog(
context: context,
barrierDismissible: false,
2021-09-27 01:57:55 -07:00
builder: (BuildContext context) => AlertDialog(
title: Text(s.pleaseConfirm),
content: SingleChildScrollView(
child: Text(s.sendingAzecCointickerToAddress(
aZEC, coin.ticker, _address))),
actions: confirmButtons(
context, () => Navigator.of(context).pop(true),
okLabel: s.approve, cancelValue: false)));
2021-06-26 07:30:12 -07:00
if (approved) {
Navigator.of(context).pop();
final snackBar1 = SnackBar(content: Text(s.preparingTransaction));
2021-09-10 02:56:15 -07:00
rootScaffoldMessengerKey.currentState?.showSnackBar(snackBar1);
2021-06-26 07:30:12 -07:00
2021-07-07 08:40:05 -07:00
int maxAmountPerNote = (_maxAmountPerNote * ZECUNIT_DECIMAL).toInt();
2021-07-09 22:44:34 -07:00
final memo = _memoController.text;
2021-08-27 02:51:34 -07:00
final address = unwrapUA(_address);
2021-06-26 07:30:12 -07:00
2021-08-05 06:43:05 -07:00
if (accountManager.canPay) {
2021-09-26 11:44:19 -07:00
if (settings.protectSend &&
2021-09-27 01:57:55 -07:00
!await authenticate(context, s.pleaseAuthenticateToSend)) return;
2021-08-05 06:43:05 -07:00
final tx = await compute(
sendPayment,
PaymentParams(
accountManager.active.id,
2021-08-27 02:51:34 -07:00
address,
2021-08-06 00:53:54 -07:00
_amount,
2021-08-05 06:43:05 -07:00
memo,
maxAmountPerNote,
settings.anchorOffset,
2021-08-23 05:47:48 -07:00
_shieldTransparent,
2021-08-05 06:43:05 -07:00
progressPort.sendPort));
final snackBar2 = SnackBar(content: Text("${s.txId}: $tx"));
2021-09-10 02:56:15 -07:00
rootScaffoldMessengerKey.currentState?.showSnackBar(snackBar2);
2021-09-05 06:06:54 -07:00
await accountManager.fetchAccountData(true);
2021-08-06 00:53:54 -07:00
} else {
2021-08-05 06:43:05 -07:00
Directory tempDir = await getTemporaryDirectory();
String filename = "${tempDir.path}/tx.json";
2021-09-27 01:57:55 -07:00
final msg = WarpApi.prepareTx(accountManager.active.id, address,
_amount, memo, maxAmountPerNote, settings.anchorOffset, filename);
2021-08-05 06:43:05 -07:00
Share.shareFiles([filename], subject: s.unsignedTransactionFile);
2021-08-05 06:43:05 -07:00
2021-08-07 00:32:10 -07:00
final snackBar2 = SnackBar(content: Text(msg));
2021-09-10 02:56:15 -07:00
rootScaffoldMessengerKey.currentState?.showSnackBar(snackBar2);
2021-08-05 06:43:05 -07:00
}
2021-06-26 07:30:12 -07:00
}
}
}
2021-07-07 08:40:05 -07:00
2021-09-14 09:08:42 -07:00
int amountInZAT(Decimal v) => (v * ZECUNIT_DECIMAL).toInt();
2021-09-26 11:44:19 -07:00
String amountFromZAT(int v) =>
(Decimal.fromInt(v) / ZECUNIT_DECIMAL).toString();
2021-08-06 00:53:54 -07:00
2021-09-14 09:08:42 -07:00
double _fx() {
return priceStore.zecPrice;
}
2021-08-07 00:32:10 -07:00
2021-09-14 09:08:42 -07:00
String _trimToPrecision(String v) {
double vv = parseNumber(v);
2021-09-21 05:52:52 -07:00
return decimalFormat(vv, precision(_mZEC));
2021-09-12 04:24:40 -07:00
}
2021-09-27 01:57:55 -07:00
get spendable => math.max(
_sBalance - _excludedBalance - _underConfirmedBalance - DEFAULT_FEE, 0);
2021-06-26 07:30:12 -07:00
}
class BalanceTable extends StatelessWidget {
2021-09-27 01:57:55 -07:00
final int sBalance;
final int tBalance;
final int excludedBalance;
final int underConfirmedBalance;
2021-09-27 01:57:55 -07:00
BalanceTable(this.sBalance, this.tBalance, this.excludedBalance,
this.underConfirmedBalance);
@override
Widget build(BuildContext context) {
2021-09-27 01:57:55 -07:00
final theme = Theme.of(context);
final tBalanceLabel = Text.rich(TextSpan(children: [
TextSpan(text: S.of(context).unshieldedBalance + ' '),
WidgetSpan(
child: GestureDetector(
child: Icon(Icons.shield_outlined),
onTap: () {
shieldTAddr(context);
},
),
)
2021-09-27 01:57:55 -07:00
]));
return Container(
decoration: BoxDecoration(border: Border.all(color: theme.dividerColor, width: 1),
borderRadius: BorderRadius.circular(8)),
child: Column(crossAxisAlignment: CrossAxisAlignment.center, children: [
BalanceRow(Text(S.of(context).totalBalance), totalBalance),
BalanceRow(Text(S.of(context).underConfirmed), -underConfirmedBalance),
BalanceRow(Text(S.of(context).excludedNotes), -excludedBalance),
BalanceRow(tBalanceLabel, -tBalance),
BalanceRow(Text(S.of(context).spendableBalance), spendable,
style: TextStyle(color: Theme.of(context).primaryColor)),
]));
}
2021-09-27 01:57:55 -07:00
get totalBalance => sBalance + tBalance;
get spendable => math.max(
sBalance - excludedBalance - underConfirmedBalance - DEFAULT_FEE, 0);
}
class BalanceRow extends StatelessWidget {
final label;
final amount;
2021-09-27 01:57:55 -07:00
final style;
BalanceRow(this.label, this.amount, {this.style});
@override
Widget build(BuildContext context) {
2021-09-27 01:57:55 -07:00
return ListTile(
title: label,
trailing: Text(amountToString(amount),
style: TextStyle(fontFeatures: [FontFeature.tabularFigures()]).merge(style)),
visualDensity: VisualDensity(horizontal: 0, vertical: -4));
}
}
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,
2021-08-23 05:47:48 -07:00
param.anchorOffset,
param.shieldBalance, (percent) {
2021-07-09 22:44:34 -07:00
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;
}