zwallet/lib/send.dart

534 lines
18 KiB
Dart
Raw Normal View History

2021-09-25 02:09:41 -07:00
import 'dart:convert';
import 'dart:ui';
2021-08-05 06:43:05 -07:00
2022-06-16 03:16:32 -07:00
import 'package:audioplayers/audioplayers.dart';
2021-06-26 07:30:12 -07:00
import 'package:flutter/material.dart';
2022-04-15 23:51:13 -07:00
import 'package:flutter_form_builder/flutter_form_builder.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';
2022-03-16 21:03:37 -07:00
import 'accounts.dart';
2022-03-07 06:53:18 -08:00
import 'dualmoneyinput.dart';
import 'package:warp_api/types.dart';
2021-06-26 07:30:12 -07:00
import 'package:warp_api/warp_api.dart';
2021-07-07 08:40:05 -07:00
import 'package:decimal/decimal.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-10-03 08:47:44 -07:00
final SendPageArgs? args;
2021-08-06 00:53:54 -07:00
2021-10-03 08:47:44 -07:00
SendPage(this.args);
2021-06-26 07:30:12 -07:00
@override
SendState createState() => SendState();
bool get isMulti => args?.isMulti ?? false;
2021-06-26 07:30:12 -07:00
}
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>();
2021-10-09 07:17:27 -07:00
final _amountKey = GlobalKey<DualMoneyInputState>();
2021-06-26 07:30:12 -07:00
var _address = "";
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;
var _unconfirmedSpentBalance = 0;
var _unconfirmedBalance = 0;
2021-06-26 07:30:12 -07:00
final _addressController = TextEditingController();
2021-07-09 22:44:34 -07:00
final _memoController = TextEditingController();
2022-04-15 23:51:13 -07:00
final _subjectController = TextEditingController();
2022-03-07 18:47:41 -08:00
var _memoInitialized = false;
2021-09-21 05:52:52 -07:00
final _maxAmountController = TextEditingController(text: zero);
2021-07-07 08:40:05 -07:00
var _isExpanded = false;
2021-10-09 07:17:27 -07:00
var _useMillis = true;
2022-07-07 20:26:20 -07:00
var _useTransparent = settings.shieldBalance || active.showTAddr;
ReactionDisposer? _newBlockAutorunDispose;
final _fee = DEFAULT_FEE;
var _usedBalance = 0;
2022-04-15 23:51:13 -07:00
var _replyTo = settings.includeReplyTo;
2021-06-26 07:30:12 -07:00
@override
initState() {
2021-10-03 08:47:44 -07:00
if (widget.args?.contact != null)
_addressController.text = widget.args!.contact!.address;
2022-04-15 23:51:13 -07:00
if (widget.args?.address != null)
_addressController.text = widget.args!.address!;
if (widget.args?.subject != null)
_subjectController.text = widget.args!.subject!;
final recipients = widget.args?.recipients ?? [];
_usedBalance = recipients.fold(0, (acc, r) => acc + r.amount);
2021-10-03 08:47:44 -07:00
final uri = widget.args?.uri;
if (uri != null)
Future.microtask(() {
_setPaymentURI(uri);
});
2021-06-26 07:30:12 -07:00
super.initState();
2021-08-06 00:53:54 -07:00
_newBlockAutorunDispose = autorun((_) async {
2022-03-07 06:53:18 -08:00
final _ = active.dataEpoch;
final sBalance = active.balances.shieldedBalance;
final tBalance = active.tbalance;
final excludedBalance = active.balances.excludedBalance;
final underConfirmedBalance = active.balances.underConfirmedBalance;
final unconfirmedSpentBalance = active.balances.unconfirmedBalance;
final unconfirmedBalance = active.balances.unconfirmedBalance;
setState(() {
2021-09-27 01:57:55 -07:00
_sBalance = sBalance;
_tBalance = tBalance;
_excludedBalance = excludedBalance;
_underConfirmedBalance = underConfirmedBalance;
_unconfirmedSpentBalance = unconfirmedSpentBalance;
_unconfirmedBalance = unconfirmedBalance;
});
});
2021-06-26 07:30:12 -07:00
}
2021-08-06 19:18:20 -07:00
@override
void dispose() {
_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);
2022-02-22 21:20:45 -08:00
final simpleMode = settings.simpleMode;
2022-03-07 18:47:41 -08:00
if (!_memoInitialized) {
_memoController.text = settings.memoSignature ?? s.sendFrom(APP_NAME);
_memoInitialized = true;
}
2022-02-23 19:36:02 -08:00
2021-06-26 07:30:12 -07:00
return Scaffold(
2022-03-07 06:53:18 -08:00
appBar: AppBar(title: Text(s.sendCointicker(active.coinDef.ticker))),
2021-09-26 11:44:19 -07:00
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(
2022-03-07 06:53:18 -08:00
labelText: s.sendCointickerTo(active.coinDef.ticker)),
2021-09-27 01:57:55 -07:00
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,
2022-03-16 21:03:37 -07:00
onSuggestionSelected: (Suggestion suggestion) {
_addressController.text = suggestion.name;
2021-09-27 01:57:55 -07:00
},
suggestionsCallback: (String pattern) {
2022-03-16 21:03:37 -07:00
final matchingContacts = contacts.contacts.where((c) => c.name
2021-09-27 01:57:55 -07:00
.toLowerCase()
2022-03-16 21:03:37 -07:00
.contains(pattern.toLowerCase())).map((c) => ContactSuggestion(c));
final matchingAccounts = accounts.list
.where((a) => a.coin == active.coin && a.name
.toLowerCase()
.contains(pattern.toLowerCase())).map((a) => AccountSuggestion(a));
return [...matchingContacts, ...matchingAccounts];
2021-09-27 01:57:55 -07:00
},
2022-03-16 21:03:37 -07:00
itemBuilder: (BuildContext context, Suggestion suggestion) =>
ListTile(title: Text(suggestion.name)),
2021-09-27 01:57:55 -07:00
noItemsFoundBuilder: (_) => SizedBox(),
)),
2021-09-26 11:44:19 -07:00
IconButton(
icon: new Icon(MdiIcons.qrcodeScan),
onPressed: _onScan)
]),
DualMoneyInputWidget(
key: _amountKey,
child:
TextButton(child: Text(s.max), onPressed: _onMax),
spendable: spendable),
2022-02-22 21:20:45 -08:00
if (!simpleMode) BalanceTable(_sBalance, _tBalance, _useTransparent,
_excludedBalance, _underConfirmedBalance, change, _usedBalance, _fee),
2022-04-15 23:51:13 -07:00
Container(child: InputDecorator(
decoration: InputDecoration(labelText: s.memo),
child: Column(children: [
FormBuilderCheckbox(
name: 'reply-to',
title: Text(s.includeReplyTo),
initialValue: _replyTo,
onChanged: (_) {
setState(() {
_replyTo = true;
});
},
),
TextFormField(
decoration:
InputDecoration(labelText: s.subject),
controller: _subjectController,
),
TextFormField(
decoration:
InputDecoration(labelText: s.body),
minLines: 4,
maxLines: null,
keyboardType: TextInputType.multiline,
controller: _memoController,
)]))),
2022-02-22 21:20:45 -08:00
if (!simpleMode) ExpansionPanelList(
2021-09-26 11:44:19 -07:00
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: [
CheckboxListTile(
title: Text(s.roundToMillis),
2021-10-09 07:17:27 -07:00
value: _useMillis,
onChanged: _setUseMillis),
2022-03-07 06:53:18 -08:00
if (active.canPay && !widget.isMulti)
2021-09-26 11:44:19 -07:00
CheckboxListTile(
2021-10-11 02:16:35 -07:00
title: Text(s.useTransparentBalance),
value: _useTransparent,
onChanged: _onChangedUseTransparent,
2021-09-26 11:44:19 -07:00
),
ListTile(
title: TextFormField(
2021-09-27 01:57:55 -07:00
decoration: InputDecoration(
labelText: s.maxAmountPerNote),
keyboardType: TextInputType.number,
controller: _maxAmountController,
inputFormatters: [
makeInputFormatter(amountInput?.useMillis)
],
2021-09-27 01:57:55 -07:00
validator: _checkMaxAmountPerNote,
onSaved: _onSavedMaxAmountPerNote,
)),
2021-09-26 11:44:19 -07:00
]),
isExpanded: _isExpanded,
)
]),
Padding(padding: EdgeInsets.all(8)),
ButtonBar(
children: confirmButtons(context, _onSend,
okLabel: widget.isMulti ? s.add : s.send,
okIcon: Icon(MdiIcons.send)))
2021-09-26 11:44:19 -07:00
])))));
2021-06-26 07:30:12 -07:00
}
2022-03-16 21:03:37 -07:00
Suggestion? getSuggestion(String v) {
final c = contacts.contacts.where((c) => c.name == v);
if (c.isNotEmpty) return ContactSuggestion(c.first);
final a = accounts.list.where((a) => a.name == v);
if (a.isNotEmpty) return AccountSuggestion(a.first);
}
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;
2022-03-16 21:03:37 -07:00
final suggestion = getSuggestion(v);
if (suggestion != null) return null;
2022-03-07 06:53:18 -08:00
if (!WarpApi.validAddress(active.coin, v)) return s.invalidAddress;
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(() {
2021-10-09 07:17:27 -07:00
_useMillis = false;
amountInput?.setAmount(spendable);
2021-07-07 08:40:05 -07:00
});
}
2021-10-11 02:16:35 -07:00
void _onChangedUseTransparent(bool? v) {
2021-09-10 02:56:15 -07:00
if (v == null) return;
2021-08-23 05:47:48 -07:00
setState(() {
2021-10-11 02:16:35 -07:00
_useTransparent = v;
2021-08-23 05:47:48 -07:00
});
}
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) {
2021-10-03 08:47:44 -07:00
_setPaymentURI(code); // not an address
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-10-03 08:47:44 -07:00
2021-10-09 07:17:27 -07:00
void _setUseMillis(bool? vv) {
final v = vv ?? false;
amountInput?.setMillis(v);
setState(() {
_useMillis = v;
});
}
2021-10-03 08:47:44 -07:00
void _setPaymentURI(String uri) {
2022-06-07 10:00:08 -07:00
final json = WarpApi.parsePaymentURI(uri);
2021-10-03 08:47:44 -07:00
try {
final payment = DecodedPaymentURI.fromJson(jsonDecode(json));
setState(() {
_address = payment.address;
_addressController.text = _address;
_memoController.text = payment.memo;
2021-10-09 07:17:27 -07:00
amountInput?.setAmount(payment.amount);
2021-10-03 08:47:44 -07:00
});
} on FormatException {}
}
2021-06-26 07:30:12 -07:00
2021-07-07 08:40:05 -07:00
void _onAddress(v) {
2022-03-16 21:03:37 -07:00
final suggestion = getSuggestion(v);
if (suggestion == null)
2021-09-12 04:24:40 -07:00
_address = v;
else {
2022-03-16 21:03:37 -07:00
_address = suggestion.address;
2021-09-12 04:24:40 -07:00
}
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
}
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-10-19 18:22:37 -07:00
final amount = amountInput?.amount ?? 0;
final aZEC = amountToString(amount);
final approved = widget.isMulti ||
await showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) => AlertDialog(
title: Text(s.pleaseConfirm),
content: SingleChildScrollView(
child: Text(s.sendingAzecCointickerToAddress(
2022-03-07 06:53:18 -08:00
aZEC, active.coinDef.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) {
2022-04-08 04:34:24 -07:00
int maxAmountPerNote = (_maxAmountPerNote * ZECUNIT_DECIMAL).toBigInt().toInt();
2021-07-09 22:44:34 -07:00
final memo = _memoController.text;
2022-04-15 23:51:13 -07:00
final subject = _subjectController.text;
final recipient = Recipient(
2022-06-07 10:00:08 -07:00
_address,
amount,
2022-04-15 23:51:13 -07:00
_replyTo,
subject,
memo,
maxAmountPerNote,
);
if (!widget.isMulti)
// send closes the page
await send(context, [recipient], _useTransparent);
else
Navigator.of(context).pop(recipient);
2021-06-26 07:30:12 -07:00
}
}
}
2021-07-07 08:40:05 -07:00
2022-04-08 04:34:24 -07:00
int amountInZAT(Decimal v) => (v * ZECUNIT_DECIMAL).toBigInt().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
get spendable => math.max(
(_useTransparent ? _tBalance : 0) +
_sBalance -
_excludedBalance -
_underConfirmedBalance -
_usedBalance -
_fee,
0);
get change => _unconfirmedSpentBalance + _unconfirmedBalance;
2021-10-09 07:17:27 -07:00
DualMoneyInputState? get amountInput => _amountKey.currentState;
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;
2021-10-11 02:16:35 -07:00
final bool useTBalance;
final int excludedBalance;
final int underConfirmedBalance;
final int change;
final int used;
final int fee;
BalanceTable(this.sBalance, this.tBalance, this.useTBalance,
this.excludedBalance, this.underConfirmedBalance, this.change, this.used, this.fee);
@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)),
2021-09-27 01:57:55 -07:00
child: Column(crossAxisAlignment: CrossAxisAlignment.center, children: [
BalanceRow(Text(S.of(context).totalBalance), totalBalance),
BalanceRow(Text(S.of(context).underConfirmed), -underConfirmed),
BalanceRow(Text(S.of(context).excludedNotes), -excludedBalance),
if (!useTBalance) 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 + change - used - fee;
get underConfirmed => -underConfirmedBalance - change;
2021-09-27 01:57:55 -07:00
get spendable => math.max(
sBalance +
(useTBalance ? tBalance : 0) -
excludedBalance -
underConfirmedBalance -
used -
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));
}
}
Future<void> send(BuildContext context, List<Recipient> recipients, bool useTransparent) async {
final s = S.of(context);
String address = "";
for (var r in recipients) {
if (address.isEmpty)
address = r.address;
else
2021-11-17 20:57:52 -08:00
address = '*';
}
showSnackBar(s.preparingTransaction, autoClose: true);
2021-11-17 20:57:52 -08:00
if (settings.protectSend &&
!await authenticate(context, s.pleaseAuthenticateToSend)) return;
2022-06-20 04:37:31 -07:00
final player = AudioPlayer();
2022-03-07 06:53:18 -08:00
if (active.canPay) {
Navigator.of(context).pop();
2022-06-16 03:16:32 -07:00
active.setBanner(s.paymentInProgress);
final res = await WarpApi.sendPayment(active.coin, active.id, recipients,
useTransparent, settings.anchorOffset, (progress) {
progressPort.sendPort.send(progress);
});
progressPort.sendPort.send(0);
2022-06-16 03:16:32 -07:00
active.setBanner("");
final isError = WarpApi.getError();
final msg = isError ? s.error(res) : s.txId(res);
2022-06-16 18:30:10 -07:00
await player.play(AssetSource(isError ? "fail.mp3" : "success.mp3"));
showSnackBar(msg);
2022-03-07 06:53:18 -08:00
await active.update();
} else {
final txjson = WarpApi.prepareTx(recipients, useTransparent, settings.anchorOffset);
2022-06-20 04:37:31 -07:00
final isError = WarpApi.getError();
if (isError) {
showSnackBar(txjson);
2022-06-20 04:37:31 -07:00
await player.play(AssetSource("fail.mp3"));
Navigator.of(context).pop();
return;
}
2022-06-20 02:05:48 -07:00
if (settings.qrOffline) {
Navigator.pushReplacementNamed(context, '/qroffline', arguments: txjson);
}
else {
2022-06-21 17:22:46 -07:00
await saveFile(txjson, "tx.json", s.unsignedTransactionFile);
2022-03-07 06:53:18 -08:00
showSnackBar(s.fileSaved);
2022-06-20 02:05:48 -07:00
Navigator.of(context).pop();
}
}
2021-06-26 07:30:12 -07:00
}
2022-03-09 05:19:42 -08:00
2022-03-16 21:03:37 -07:00
abstract class Suggestion {
String get name;
String get address;
}
class ContactSuggestion extends Suggestion {
final Contact contact;
ContactSuggestion(this.contact);
String get name => contact.name;
String get address => contact.address;
}
class AccountSuggestion extends Suggestion {
final Account account;
AccountSuggestion(this.account);
String get name => account.name;
String get address => account.address;
}