mirror of
https://github.com/cake-tech/cake_wallet.git
synced 2025-06-28 20:39:51 +00:00
CW-969-Add-DFX-s-Open-CryptoPay (#2122)
* feat: Add Open CryptoPay for sending * bug: Fix sending Open CryptoPay for eth * feat: add transaction dismissal * style: remove print statements from OCP * feat: add support for Monero over OCP * bug: Hide "Add to Address Book" on OCP transactions * style: revert code style in send_view_model.dart * style: revert code style in send_view_model.dart * feat: set failure state if it fails to create an OCP transaction * Update lib/view_model/send/send_view_model.dart --------- Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
This commit is contained in:
parent
2b020c0c1b
commit
c1e9668b1e
11 changed files with 458 additions and 69 deletions
|
@ -190,7 +190,7 @@ abstract class EVMChainClient {
|
||||||
_sendTransaction = () async => await sendTransaction(signedTransaction);
|
_sendTransaction = () async => await sendTransaction(signedTransaction);
|
||||||
|
|
||||||
return PendingEVMChainTransaction(
|
return PendingEVMChainTransaction(
|
||||||
signedTransaction: signedTransaction,
|
signedTransaction: prepareSignedTransactionForSending(signedTransaction),
|
||||||
amount: amount.toString(),
|
amount: amount.toString(),
|
||||||
fee: gasFee,
|
fee: gasFee,
|
||||||
sendTransaction: _sendTransaction,
|
sendTransaction: _sendTransaction,
|
||||||
|
|
|
@ -185,13 +185,14 @@ Future<PendingTransactionDescription> createTransactionSync(
|
||||||
final rAmt = monero.PendingTransaction_amount(pendingTx);
|
final rAmt = monero.PendingTransaction_amount(pendingTx);
|
||||||
final rFee = monero.PendingTransaction_fee(pendingTx);
|
final rFee = monero.PendingTransaction_fee(pendingTx);
|
||||||
final rHash = monero.PendingTransaction_txid(pendingTx, '');
|
final rHash = monero.PendingTransaction_txid(pendingTx, '');
|
||||||
|
final rHex = monero.PendingTransaction_hex(pendingTx, '');
|
||||||
final rTxKey = rHash;
|
final rTxKey = rHash;
|
||||||
|
|
||||||
return PendingTransactionDescription(
|
return PendingTransactionDescription(
|
||||||
amount: rAmt,
|
amount: rAmt,
|
||||||
fee: rFee,
|
fee: rFee,
|
||||||
hash: rHash,
|
hash: rHash,
|
||||||
hex: '',
|
hex: rHex,
|
||||||
txKey: rTxKey,
|
txKey: rTxKey,
|
||||||
pointerAddress: pendingTx.address,
|
pointerAddress: pendingTx.address,
|
||||||
);
|
);
|
||||||
|
@ -234,7 +235,7 @@ Future<PendingTransactionDescription> createTransactionMultDest(
|
||||||
amount: monero.PendingTransaction_amount(txptr),
|
amount: monero.PendingTransaction_amount(txptr),
|
||||||
fee: monero.PendingTransaction_fee(txptr),
|
fee: monero.PendingTransaction_fee(txptr),
|
||||||
hash: monero.PendingTransaction_txid(txptr, ''),
|
hash: monero.PendingTransaction_txid(txptr, ''),
|
||||||
hex: monero.PendingTransaction_txid(txptr, ''),
|
hex: monero.PendingTransaction_hex(txptr, ''),
|
||||||
txKey: monero.PendingTransaction_txid(txptr, ''),
|
txKey: monero.PendingTransaction_txid(txptr, ''),
|
||||||
pointerAddress: txptr.address,
|
pointerAddress: txptr.address,
|
||||||
);
|
);
|
||||||
|
|
18
lib/core/open_crypto_pay/exceptions.dart
Normal file
18
lib/core/open_crypto_pay/exceptions.dart
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
class OpenCryptoPayException implements Exception {
|
||||||
|
final String message;
|
||||||
|
|
||||||
|
OpenCryptoPayException([this.message = '']);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() =>
|
||||||
|
'OpenCryptoPayException${message.isNotEmpty ? ': $message' : ''}';
|
||||||
|
}
|
||||||
|
|
||||||
|
class OpenCryptoPayNotSupportedException extends OpenCryptoPayException {
|
||||||
|
final String provider;
|
||||||
|
|
||||||
|
OpenCryptoPayNotSupportedException(this.provider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get message => "$provider does not support Open CryptoPay";
|
||||||
|
}
|
78
lib/core/open_crypto_pay/lnurl.dart
Normal file
78
lib/core/open_crypto_pay/lnurl.dart
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:bech32/bech32.dart';
|
||||||
|
|
||||||
|
Uri decodeLNURL(String encodedUrl) {
|
||||||
|
Uri decodedUri;
|
||||||
|
|
||||||
|
/// The URL doesn't have to be encoded at all as per LUD-17: Protocol schemes and raw (non bech32-encoded) URLs.
|
||||||
|
/// https://github.com/lnurl/luds/blob/luds/17.md
|
||||||
|
/// Handle non bech32-encoded LNURL
|
||||||
|
final lud17prefixes = ['lnurlw', 'lnurlc', 'lnurlp', 'keyauth'];
|
||||||
|
decodedUri = Uri.parse(encodedUrl);
|
||||||
|
for (final prefix in lud17prefixes) {
|
||||||
|
if (decodedUri.scheme.contains(prefix)) {
|
||||||
|
decodedUri = decodedUri.replace(scheme: prefix);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (lud17prefixes.contains(decodedUri.scheme)) {
|
||||||
|
/// If the non-bech32 LNURL is a Tor address, the port has to be http instead of https for the clearnet LNURL so check if the host ends with '.onion' or '.onion.'
|
||||||
|
decodedUri = decodedUri.replace(
|
||||||
|
scheme: decodedUri.host.endsWith('onion') ||
|
||||||
|
decodedUri.host.endsWith('onion.')
|
||||||
|
? 'http'
|
||||||
|
: 'https');
|
||||||
|
} else {
|
||||||
|
/// Try to parse the input as a lnUrl. Will throw an error if it fails.
|
||||||
|
final lnUrl = _findLnUrl(encodedUrl);
|
||||||
|
|
||||||
|
/// Decode the lnurl using bech32
|
||||||
|
final bech32 = const Bech32Codec().decode(lnUrl, lnUrl.length);
|
||||||
|
decodedUri = Uri.parse(utf8.decode(_convert(bech32.data, 5, 8, false)));
|
||||||
|
}
|
||||||
|
return decodedUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse and return a given lnurl string if it's valid. Will remove
|
||||||
|
/// `lightning:` from the beginning of it if present.
|
||||||
|
String _findLnUrl(String input) {
|
||||||
|
final res = RegExp(
|
||||||
|
r',*?((lnurl)([0-9]+[a-z0-9]+))',
|
||||||
|
).allMatches(input.toLowerCase());
|
||||||
|
|
||||||
|
if (res.length == 1) {
|
||||||
|
return res.first.group(0)!;
|
||||||
|
} else {
|
||||||
|
throw ArgumentError('Not a valid lnurl string');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Taken from bech32 (bitcoinjs): https://github.com/bitcoinjs/bech32
|
||||||
|
List<int> _convert(List<int> data, int inBits, int outBits, bool pad) {
|
||||||
|
var value = 0;
|
||||||
|
var bits = 0;
|
||||||
|
final maxV = (1 << outBits) - 1;
|
||||||
|
|
||||||
|
final result = <int>[];
|
||||||
|
for (final dataValue in data) {
|
||||||
|
value = (value << inBits) | dataValue;
|
||||||
|
bits += inBits;
|
||||||
|
|
||||||
|
while (bits >= outBits) {
|
||||||
|
bits -= outBits;
|
||||||
|
result.add((value >> bits) & maxV);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pad) {
|
||||||
|
if (bits > 0) result.add((value << (outBits - bits)) & maxV);
|
||||||
|
} else {
|
||||||
|
if (bits >= inBits) throw Exception('Excess padding');
|
||||||
|
|
||||||
|
if ((value << (outBits - bits)) & maxV > 0) {
|
||||||
|
throw Exception('Non-zero padding');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
26
lib/core/open_crypto_pay/models.dart
Normal file
26
lib/core/open_crypto_pay/models.dart
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
class OpenCryptoPayRequest {
|
||||||
|
final String receiverName;
|
||||||
|
final int expiry;
|
||||||
|
final String callbackUrl;
|
||||||
|
final String quote;
|
||||||
|
final Map<String, List<OpenCryptoPayQuoteAsset>> methods;
|
||||||
|
|
||||||
|
OpenCryptoPayRequest({
|
||||||
|
required this.receiverName,
|
||||||
|
required this.expiry,
|
||||||
|
required this.callbackUrl,
|
||||||
|
required this.quote,
|
||||||
|
required this.methods,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class OpenCryptoPayQuoteAsset {
|
||||||
|
final String symbol;
|
||||||
|
final String amount;
|
||||||
|
|
||||||
|
const OpenCryptoPayQuoteAsset(this.symbol, this.amount);
|
||||||
|
|
||||||
|
OpenCryptoPayQuoteAsset.fromJson(Map<String, dynamic> json)
|
||||||
|
: symbol = json['asset'] as String,
|
||||||
|
amount = json['amount'] as String;
|
||||||
|
}
|
184
lib/core/open_crypto_pay/open_cryptopay_service.dart
Normal file
184
lib/core/open_crypto_pay/open_cryptopay_service.dart
Normal file
|
@ -0,0 +1,184 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:developer';
|
||||||
|
|
||||||
|
import 'package:cake_wallet/core/open_crypto_pay/exceptions.dart';
|
||||||
|
import 'package:cake_wallet/core/open_crypto_pay/lnurl.dart';
|
||||||
|
import 'package:cake_wallet/core/open_crypto_pay/models.dart';
|
||||||
|
import 'package:cw_core/crypto_currency.dart';
|
||||||
|
import 'package:http/http.dart';
|
||||||
|
|
||||||
|
class OpenCryptoPayService {
|
||||||
|
static bool isOpenCryptoPayQR(String value) =>
|
||||||
|
value.toLowerCase().contains("lightning=lnurl") ||
|
||||||
|
value.toLowerCase().startsWith("lnurl");
|
||||||
|
|
||||||
|
final Client _httpClient = Client();
|
||||||
|
|
||||||
|
Future<String> commitOpenCryptoPayRequest(
|
||||||
|
String txHex, {
|
||||||
|
required String txId,
|
||||||
|
required OpenCryptoPayRequest request,
|
||||||
|
required CryptoCurrency asset,
|
||||||
|
}) async {
|
||||||
|
final uri = Uri.parse(request.callbackUrl.replaceAll("/cb/", "/tx/"));
|
||||||
|
|
||||||
|
final queryParams = Map.of(uri.queryParameters);
|
||||||
|
|
||||||
|
queryParams['quote'] = request.quote;
|
||||||
|
queryParams['asset'] = asset.title;
|
||||||
|
queryParams['method'] = _getMethod(asset);
|
||||||
|
queryParams['hex'] = txHex;
|
||||||
|
queryParams['tx'] = txId;
|
||||||
|
|
||||||
|
final response =
|
||||||
|
await _httpClient.get(Uri.https(uri.authority, uri.path, queryParams));
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final body = jsonDecode(response.body) as Map;
|
||||||
|
|
||||||
|
if (body.keys.contains("txId")) return body["txId"] as String;
|
||||||
|
throw OpenCryptoPayException(body.toString());
|
||||||
|
}
|
||||||
|
throw OpenCryptoPayException(
|
||||||
|
"Unexpected status code ${response.statusCode} ${response.body}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> cancelOpenCryptoPayRequest(OpenCryptoPayRequest request) async {
|
||||||
|
final uri = Uri.parse(request.callbackUrl.replaceAll("/cb/", "/cancel/"));
|
||||||
|
|
||||||
|
await _httpClient.delete(uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<OpenCryptoPayRequest> getOpenCryptoPayInvoice(String lnUrl) async {
|
||||||
|
if (lnUrl.toLowerCase().startsWith("http")) {
|
||||||
|
final uri = Uri.parse(lnUrl);
|
||||||
|
final params = uri.queryParameters;
|
||||||
|
if (!params.containsKey("lightning")) {
|
||||||
|
throw OpenCryptoPayNotSupportedException(uri.authority);
|
||||||
|
}
|
||||||
|
|
||||||
|
lnUrl = params["lightning"] as String;
|
||||||
|
}
|
||||||
|
final url = decodeLNURL(lnUrl);
|
||||||
|
final params = await _getOpenCryptoPayParams(url);
|
||||||
|
|
||||||
|
return OpenCryptoPayRequest(
|
||||||
|
receiverName: params.$1.displayName ?? "Unknown",
|
||||||
|
expiry: params.$1.expiration.difference(DateTime.now()).inSeconds,
|
||||||
|
callbackUrl: params.$1.callbackUrl,
|
||||||
|
quote: params.$1.id,
|
||||||
|
methods: params.$2,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<(_OpenCryptoPayQuote, Map<String, List<OpenCryptoPayQuoteAsset>>)>
|
||||||
|
_getOpenCryptoPayParams(Uri uri) async {
|
||||||
|
final response = await _httpClient.get(uri);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final responseBody = jsonDecode(response.body) as Map;
|
||||||
|
|
||||||
|
for (final key in ['callback', 'transferAmounts', 'quote']) {
|
||||||
|
if (!responseBody.keys.contains(key)) {
|
||||||
|
throw OpenCryptoPayNotSupportedException(uri.authority);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final methods = <String, List<OpenCryptoPayQuoteAsset>>{};
|
||||||
|
for (final transferAmountRaw in responseBody['transferAmounts'] as List) {
|
||||||
|
final transferAmount = transferAmountRaw as Map;
|
||||||
|
final method = transferAmount['method'] as String;
|
||||||
|
methods[method] = [];
|
||||||
|
for (final assetJson in transferAmount['assets'] as List) {
|
||||||
|
final asset = OpenCryptoPayQuoteAsset.fromJson(
|
||||||
|
assetJson as Map<String, dynamic>);
|
||||||
|
methods[method]?.add(asset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log(responseBody.toString());
|
||||||
|
|
||||||
|
final quote = _OpenCryptoPayQuote.fromJson(
|
||||||
|
responseBody['callback'] as String,
|
||||||
|
responseBody['displayName'] as String?,
|
||||||
|
responseBody['quote'] as Map<String, dynamic>);
|
||||||
|
|
||||||
|
return (quote, methods);
|
||||||
|
} else {
|
||||||
|
throw OpenCryptoPayException(
|
||||||
|
'Failed to get Open CryptoPay Request. Status: ${response.statusCode} ${response.body}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Uri> getOpenCryptoPayAddress(
|
||||||
|
OpenCryptoPayRequest request, CryptoCurrency asset) async {
|
||||||
|
final uri = Uri.parse(request.callbackUrl);
|
||||||
|
final queryParams = Map.of(uri.queryParameters);
|
||||||
|
|
||||||
|
queryParams['quote'] = request.quote;
|
||||||
|
queryParams['asset'] = asset.title;
|
||||||
|
queryParams['method'] = _getMethod(asset);
|
||||||
|
|
||||||
|
final response =
|
||||||
|
await _httpClient.get(Uri.https(uri.authority, uri.path, queryParams));
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final responseBody = jsonDecode(response.body) as Map;
|
||||||
|
|
||||||
|
for (final key in ['expiryDate', 'uri']) {
|
||||||
|
if (!responseBody.keys.contains(key)) {
|
||||||
|
throw OpenCryptoPayNotSupportedException(uri.authority);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = Uri.parse(responseBody['uri'] as String);
|
||||||
|
|
||||||
|
if (result.queryParameters['amount'] != null) return result;
|
||||||
|
|
||||||
|
final newQueryParameters =
|
||||||
|
Map<String, dynamic>.from(result.queryParameters);
|
||||||
|
|
||||||
|
newQueryParameters['amount'] = _getAmountByAsset(request, asset);
|
||||||
|
return Uri(
|
||||||
|
scheme: result.scheme,
|
||||||
|
path: result.path.split("@").first,
|
||||||
|
queryParameters: newQueryParameters);
|
||||||
|
} else {
|
||||||
|
throw OpenCryptoPayException(
|
||||||
|
'Failed to create Open CryptoPay Request. Status: ${response.statusCode} ${response.body}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getAmountByAsset(OpenCryptoPayRequest request, CryptoCurrency asset) {
|
||||||
|
final method = _getMethod(asset);
|
||||||
|
return request.methods[method]!
|
||||||
|
.firstWhere((e) => e.symbol == asset.title)
|
||||||
|
.amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getMethod(CryptoCurrency asset) {
|
||||||
|
switch (asset.tag) {
|
||||||
|
case "ETH":
|
||||||
|
return "Ethereum";
|
||||||
|
case "POL":
|
||||||
|
return "Polygon";
|
||||||
|
default:
|
||||||
|
return asset.fullName!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _OpenCryptoPayQuote {
|
||||||
|
final String callbackUrl;
|
||||||
|
final String? displayName;
|
||||||
|
final String id;
|
||||||
|
final DateTime expiration;
|
||||||
|
|
||||||
|
_OpenCryptoPayQuote(
|
||||||
|
this.callbackUrl, this.displayName, this.id, this.expiration);
|
||||||
|
|
||||||
|
_OpenCryptoPayQuote.fromJson(
|
||||||
|
this.callbackUrl, this.displayName, Map<String, dynamic> json)
|
||||||
|
: id = json['id'] as String,
|
||||||
|
expiration = DateTime.parse(json['expiration'] as String);
|
||||||
|
}
|
|
@ -547,9 +547,9 @@ class SendPage extends BasePage {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state is ExecutedSuccessfullyState) {
|
if (state is ExecutedSuccessfullyState) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
showModalBottomSheet<void>(
|
final result = await showModalBottomSheet<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
isDismissible: false,
|
isDismissible: false,
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
|
@ -570,13 +570,16 @@ class SendPage extends BasePage {
|
||||||
feeFiatAmount: sendViewModel.pendingTransactionFeeFiatAmountFormatted,
|
feeFiatAmount: sendViewModel.pendingTransactionFeeFiatAmountFormatted,
|
||||||
outputs: sendViewModel.outputs,
|
outputs: sendViewModel.outputs,
|
||||||
onSlideComplete: () async {
|
onSlideComplete: () async {
|
||||||
Navigator.of(bottomSheetContext).pop();
|
Navigator.of(bottomSheetContext).pop(true);
|
||||||
sendViewModel.commitTransaction(context);
|
sendViewModel.commitTransaction(context);
|
||||||
},
|
},
|
||||||
change: sendViewModel.pendingTransaction!.change,
|
change: sendViewModel.pendingTransaction!.change,
|
||||||
|
isOpenCryptoPay: sendViewModel.ocpRequest != null,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (result == null) sendViewModel.dismissTransaction();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -600,7 +603,8 @@ class SendPage extends BasePage {
|
||||||
context: context,
|
context: context,
|
||||||
isDismissible: false,
|
isDismissible: false,
|
||||||
builder: (BuildContext bottomSheetContext) {
|
builder: (BuildContext bottomSheetContext) {
|
||||||
return showContactSheet
|
return showContactSheet &&
|
||||||
|
sendViewModel.ocpRequest == null
|
||||||
? InfoBottomSheet(
|
? InfoBottomSheet(
|
||||||
currentTheme: currentTheme,
|
currentTheme: currentTheme,
|
||||||
showDontAskMeCheckbox: true,
|
showDontAskMeCheckbox: true,
|
||||||
|
|
|
@ -1,16 +1,17 @@
|
||||||
|
import 'package:cake_wallet/core/open_crypto_pay/open_cryptopay_service.dart';
|
||||||
import 'package:cake_wallet/entities/priority_for_wallet_type.dart';
|
import 'package:cake_wallet/entities/priority_for_wallet_type.dart';
|
||||||
import 'package:cake_wallet/src/screens/receive/widgets/currency_input_field.dart';
|
import 'package:cake_wallet/src/screens/receive/widgets/currency_input_field.dart';
|
||||||
import 'package:cake_wallet/src/widgets/picker.dart';
|
import 'package:cake_wallet/src/widgets/picker.dart';
|
||||||
import 'package:cake_wallet/src/widgets/standard_checkbox.dart';
|
import 'package:cake_wallet/src/widgets/standard_checkbox.dart';
|
||||||
import 'package:cake_wallet/themes/extensions/keyboard_theme.dart';
|
|
||||||
import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart';
|
import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart';
|
||||||
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
|
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
|
||||||
|
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
|
||||||
|
import 'package:cake_wallet/themes/theme_base.dart';
|
||||||
import 'package:cake_wallet/utils/payment_request.dart';
|
import 'package:cake_wallet/utils/payment_request.dart';
|
||||||
import 'package:cake_wallet/utils/responsive_layout_util.dart';
|
import 'package:cake_wallet/utils/responsive_layout_util.dart';
|
||||||
import 'package:cw_core/crypto_currency.dart';
|
import 'package:cw_core/crypto_currency.dart';
|
||||||
import 'package:cw_core/currency.dart';
|
import 'package:cw_core/currency.dart';
|
||||||
import 'package:cake_wallet/routes.dart';
|
import 'package:cake_wallet/routes.dart';
|
||||||
import 'package:cake_wallet/src/widgets/keyboard_done_button.dart';
|
|
||||||
import 'package:cake_wallet/view_model/send/output.dart';
|
import 'package:cake_wallet/view_model/send/output.dart';
|
||||||
import 'package:cw_core/transaction_priority.dart';
|
import 'package:cw_core/transaction_priority.dart';
|
||||||
import 'package:cw_core/unspent_coin_type.dart';
|
import 'package:cw_core/unspent_coin_type.dart';
|
||||||
|
@ -18,7 +19,6 @@ import 'package:cw_core/wallet_type.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||||
import 'package:mobx/mobx.dart';
|
import 'package:mobx/mobx.dart';
|
||||||
import 'package:keyboard_actions/keyboard_actions.dart';
|
|
||||||
import 'package:cake_wallet/view_model/send/send_view_model.dart';
|
import 'package:cake_wallet/view_model/send/send_view_model.dart';
|
||||||
import 'package:cake_wallet/utils/show_pop_up.dart';
|
import 'package:cake_wallet/utils/show_pop_up.dart';
|
||||||
import 'package:cake_wallet/src/widgets/address_text_field.dart';
|
import 'package:cake_wallet/src/widgets/address_text_field.dart';
|
||||||
|
@ -26,9 +26,6 @@ import 'package:cake_wallet/generated/i18n.dart';
|
||||||
import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
|
import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
|
||||||
import 'package:cake_wallet/themes/extensions/send_page_theme.dart';
|
import 'package:cake_wallet/themes/extensions/send_page_theme.dart';
|
||||||
|
|
||||||
import '../../../../themes/extensions/cake_text_theme.dart';
|
|
||||||
import '../../../../themes/theme_base.dart';
|
|
||||||
|
|
||||||
class SendCard extends StatefulWidget {
|
class SendCard extends StatefulWidget {
|
||||||
SendCard({
|
SendCard({
|
||||||
Key? key,
|
Key? key,
|
||||||
|
@ -185,11 +182,16 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
|
||||||
addressKey: ValueKey('send_page_address_textfield_key'),
|
addressKey: ValueKey('send_page_address_textfield_key'),
|
||||||
focusNode: addressFocusNode,
|
focusNode: addressFocusNode,
|
||||||
controller: addressController,
|
controller: addressController,
|
||||||
onURIScanned: (uri) {
|
onURIScanned: (uri) async {
|
||||||
final paymentRequest = PaymentRequest.fromUri(uri);
|
if (OpenCryptoPayService.isOpenCryptoPayQR(
|
||||||
addressController.text = paymentRequest.address;
|
uri.toString())) {
|
||||||
cryptoAmountController.text = paymentRequest.amount;
|
sendViewModel.createOpenCryptoPayTransaction(uri.toString());
|
||||||
noteController.text = paymentRequest.note;
|
} else {
|
||||||
|
final paymentRequest = PaymentRequest.fromUri(uri);
|
||||||
|
addressController.text = paymentRequest.address;
|
||||||
|
cryptoAmountController.text = paymentRequest.amount;
|
||||||
|
noteController.text = paymentRequest.note;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
options: [
|
options: [
|
||||||
AddressTextFieldOption.paste,
|
AddressTextFieldOption.paste,
|
||||||
|
|
|
@ -28,6 +28,7 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
|
||||||
final List<Output> outputs;
|
final List<Output> outputs;
|
||||||
final VoidCallback onSlideComplete;
|
final VoidCallback onSlideComplete;
|
||||||
final PendingChange? change;
|
final PendingChange? change;
|
||||||
|
final bool isOpenCryptoPay;
|
||||||
|
|
||||||
ConfirmSendingBottomSheet({
|
ConfirmSendingBottomSheet({
|
||||||
required String titleText,
|
required String titleText,
|
||||||
|
@ -46,6 +47,7 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
|
||||||
required this.outputs,
|
required this.outputs,
|
||||||
required this.onSlideComplete,
|
required this.onSlideComplete,
|
||||||
this.change,
|
this.change,
|
||||||
|
this.isOpenCryptoPay = false,
|
||||||
Key? key,
|
Key? key,
|
||||||
}) : showScrollbar = outputs.length > 3,
|
}) : showScrollbar = outputs.length > 3,
|
||||||
super(titleText: titleText, titleIconPath: titleIconPath);
|
super(titleText: titleText, titleIconPath: titleIconPath);
|
||||||
|
@ -132,7 +134,7 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
|
||||||
final _amount = item.cryptoAmount.replaceAll(',', '.') + ' ${currency.title}';
|
final _amount = item.cryptoAmount.replaceAll(',', '.') + ' ${currency.title}';
|
||||||
return isBatchSending || contactName.isNotEmpty
|
return isBatchSending || contactName.isNotEmpty
|
||||||
? AddressExpansionTile(
|
? AddressExpansionTile(
|
||||||
contactType: 'Contact',
|
contactType: isOpenCryptoPay ? 'Open CryptoPay' : S.of(context).contact,
|
||||||
currentTheme: currentTheme,
|
currentTheme: currentTheme,
|
||||||
name: isBatchSending ? batchContactTitle : contactName,
|
name: isBatchSending ? batchContactTitle : contactName,
|
||||||
address: _address,
|
address: _address,
|
||||||
|
@ -143,7 +145,7 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
|
||||||
tileBackgroundColor: tileBackgroundColor,
|
tileBackgroundColor: tileBackgroundColor,
|
||||||
)
|
)
|
||||||
: AddressTile(
|
: AddressTile(
|
||||||
itemTitle: 'Address',
|
itemTitle: S.of(context).address,
|
||||||
currentTheme: currentTheme,
|
currentTheme: currentTheme,
|
||||||
itemTitleTextStyle: itemTitleTextStyle,
|
itemTitleTextStyle: itemTitleTextStyle,
|
||||||
isBatchSending: isBatchSending,
|
isBatchSending: isBatchSending,
|
||||||
|
|
|
@ -1,53 +1,57 @@
|
||||||
|
import 'package:cake_wallet/bitcoin/bitcoin.dart';
|
||||||
|
import 'package:cake_wallet/core/address_validator.dart';
|
||||||
|
import 'package:cake_wallet/core/amount_validator.dart';
|
||||||
|
import 'package:cake_wallet/core/execution_state.dart';
|
||||||
|
import 'package:cake_wallet/core/open_crypto_pay/models.dart';
|
||||||
|
import 'package:cake_wallet/core/open_crypto_pay/open_cryptopay_service.dart';
|
||||||
|
import 'package:cake_wallet/core/validator.dart';
|
||||||
|
import 'package:cake_wallet/core/wallet_change_listener_view_model.dart';
|
||||||
|
import 'package:cake_wallet/decred/decred.dart';
|
||||||
|
import 'package:cake_wallet/entities/calculate_fiat_amount.dart';
|
||||||
import 'package:cake_wallet/entities/contact.dart';
|
import 'package:cake_wallet/entities/contact.dart';
|
||||||
|
import 'package:cake_wallet/entities/contact_record.dart';
|
||||||
import 'package:cake_wallet/entities/evm_transaction_error_fees_handler.dart';
|
import 'package:cake_wallet/entities/evm_transaction_error_fees_handler.dart';
|
||||||
|
import 'package:cake_wallet/entities/fiat_currency.dart';
|
||||||
|
import 'package:cake_wallet/entities/parsed_address.dart';
|
||||||
|
import 'package:cake_wallet/entities/template.dart';
|
||||||
import 'package:cake_wallet/entities/transaction_description.dart';
|
import 'package:cake_wallet/entities/transaction_description.dart';
|
||||||
|
import 'package:cake_wallet/entities/wallet_contact.dart';
|
||||||
import 'package:cake_wallet/ethereum/ethereum.dart';
|
import 'package:cake_wallet/ethereum/ethereum.dart';
|
||||||
import 'package:cake_wallet/exchange/provider/exchange_provider.dart';
|
import 'package:cake_wallet/exchange/provider/exchange_provider.dart';
|
||||||
import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart';
|
import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart';
|
||||||
|
import 'package:cake_wallet/generated/i18n.dart';
|
||||||
|
import 'package:cake_wallet/monero/monero.dart';
|
||||||
import 'package:cake_wallet/nano/nano.dart';
|
import 'package:cake_wallet/nano/nano.dart';
|
||||||
import 'package:cake_wallet/decred/decred.dart';
|
|
||||||
import 'package:cake_wallet/core/wallet_change_listener_view_model.dart';
|
|
||||||
import 'package:cake_wallet/entities/contact_record.dart';
|
|
||||||
import 'package:cake_wallet/entities/wallet_contact.dart';
|
|
||||||
import 'package:cake_wallet/polygon/polygon.dart';
|
import 'package:cake_wallet/polygon/polygon.dart';
|
||||||
import 'package:cake_wallet/reactions/wallet_connect.dart';
|
import 'package:cake_wallet/reactions/wallet_connect.dart';
|
||||||
import 'package:cake_wallet/routes.dart';
|
import 'package:cake_wallet/routes.dart';
|
||||||
import 'package:cake_wallet/solana/solana.dart';
|
import 'package:cake_wallet/solana/solana.dart';
|
||||||
import 'package:cake_wallet/store/app_store.dart';
|
import 'package:cake_wallet/store/app_store.dart';
|
||||||
|
import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
|
||||||
|
import 'package:cake_wallet/store/settings_store.dart';
|
||||||
import 'package:cake_wallet/tron/tron.dart';
|
import 'package:cake_wallet/tron/tron.dart';
|
||||||
|
import 'package:cake_wallet/utils/payment_request.dart';
|
||||||
import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart';
|
import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart';
|
||||||
import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart';
|
import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart';
|
||||||
import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart';
|
import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart';
|
||||||
import 'package:cake_wallet/view_model/send/fees_view_model.dart';
|
import 'package:cake_wallet/view_model/send/fees_view_model.dart';
|
||||||
|
import 'package:cake_wallet/view_model/send/output.dart';
|
||||||
|
import 'package:cake_wallet/view_model/send/send_template_view_model.dart';
|
||||||
|
import 'package:cake_wallet/view_model/send/send_view_model_state.dart';
|
||||||
import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_list_view_model.dart';
|
import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_list_view_model.dart';
|
||||||
import 'package:cake_wallet/wownero/wownero.dart';
|
import 'package:cake_wallet/wownero/wownero.dart';
|
||||||
import 'package:cake_wallet/zano/zano.dart';
|
import 'package:cake_wallet/zano/zano.dart';
|
||||||
|
import 'package:cw_core/crypto_currency.dart';
|
||||||
import 'package:cw_core/exceptions.dart';
|
import 'package:cw_core/exceptions.dart';
|
||||||
|
import 'package:cw_core/pending_transaction.dart';
|
||||||
|
import 'package:cw_core/sync_status.dart';
|
||||||
import 'package:cw_core/transaction_info.dart';
|
import 'package:cw_core/transaction_info.dart';
|
||||||
import 'package:cw_core/unspent_coin_type.dart';
|
import 'package:cw_core/unspent_coin_type.dart';
|
||||||
import 'package:cake_wallet/view_model/send/output.dart';
|
import 'package:cw_core/utils/print_verbose.dart';
|
||||||
import 'package:cake_wallet/view_model/send/send_template_view_model.dart';
|
import 'package:cw_core/wallet_type.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
import 'package:mobx/mobx.dart';
|
import 'package:mobx/mobx.dart';
|
||||||
import 'package:cake_wallet/entities/template.dart';
|
|
||||||
import 'package:cake_wallet/core/address_validator.dart';
|
|
||||||
import 'package:cake_wallet/core/amount_validator.dart';
|
|
||||||
import 'package:cw_core/pending_transaction.dart';
|
|
||||||
import 'package:cake_wallet/core/validator.dart';
|
|
||||||
import 'package:cake_wallet/core/execution_state.dart';
|
|
||||||
import 'package:cake_wallet/monero/monero.dart';
|
|
||||||
import 'package:cw_core/sync_status.dart';
|
|
||||||
import 'package:cw_core/crypto_currency.dart';
|
|
||||||
import 'package:cake_wallet/entities/fiat_currency.dart';
|
|
||||||
import 'package:cake_wallet/entities/calculate_fiat_amount.dart';
|
|
||||||
import 'package:cw_core/wallet_type.dart';
|
|
||||||
import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
|
|
||||||
import 'package:cake_wallet/store/settings_store.dart';
|
|
||||||
import 'package:cake_wallet/view_model/send/send_view_model_state.dart';
|
|
||||||
import 'package:cake_wallet/entities/parsed_address.dart';
|
|
||||||
import 'package:cake_wallet/bitcoin/bitcoin.dart';
|
|
||||||
import 'package:cake_wallet/generated/i18n.dart';
|
|
||||||
|
|
||||||
part 'send_view_model.g.dart';
|
part 'send_view_model.g.dart';
|
||||||
|
|
||||||
|
@ -390,15 +394,62 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||||
return conditionsList.contains(true);
|
return conditionsList.contains(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
final _ocpService = OpenCryptoPayService();
|
||||||
Future<PendingTransaction?> createTransaction({ExchangeProvider? provider}) async {
|
|
||||||
try {
|
|
||||||
if (wallet.isHardwareWallet)
|
|
||||||
state = IsAwaitingDeviceResponseState();
|
|
||||||
else
|
|
||||||
state = IsExecutingState();
|
|
||||||
|
|
||||||
pendingTransaction = await wallet.createTransaction(_credentials(provider));
|
@observable
|
||||||
|
OpenCryptoPayRequest? ocpRequest;
|
||||||
|
|
||||||
|
@action
|
||||||
|
Future<void> dismissTransaction() async {
|
||||||
|
state = InitialExecutionState();
|
||||||
|
if (ocpRequest != null) {
|
||||||
|
clearOutputs();
|
||||||
|
_ocpService.cancelOpenCryptoPayRequest(ocpRequest!);
|
||||||
|
ocpRequest = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
Future<PendingTransaction?> createOpenCryptoPayTransaction(String uri) async {
|
||||||
|
state = IsExecutingState();
|
||||||
|
|
||||||
|
try {
|
||||||
|
final originalOCPRequest =
|
||||||
|
await _ocpService.getOpenCryptoPayInvoice(uri.toString());
|
||||||
|
final paymentUri = await _ocpService.getOpenCryptoPayAddress(
|
||||||
|
originalOCPRequest,
|
||||||
|
selectedCryptoCurrency,
|
||||||
|
);
|
||||||
|
|
||||||
|
ocpRequest = originalOCPRequest;
|
||||||
|
|
||||||
|
final paymentRequest = PaymentRequest.fromUri(paymentUri);
|
||||||
|
clearOutputs();
|
||||||
|
|
||||||
|
outputs.first.address = paymentRequest.address;
|
||||||
|
outputs.first.parsedAddress = ParsedAddress(
|
||||||
|
addresses: [paymentRequest.address], name: ocpRequest!.receiverName);
|
||||||
|
outputs.first.setCryptoAmount(paymentRequest.amount);
|
||||||
|
outputs.first.note = ocpRequest!.receiverName;
|
||||||
|
|
||||||
|
return createTransaction();
|
||||||
|
} catch (e) {
|
||||||
|
printV(e);
|
||||||
|
state = FailureState(translateErrorMessage(e, walletType, currency));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
Future<PendingTransaction?> createTransaction(
|
||||||
|
{ExchangeProvider? provider}) async {
|
||||||
|
try {
|
||||||
|
if (!(state is IsExecutingState)) state = IsExecutingState();
|
||||||
|
|
||||||
|
if (wallet.isHardwareWallet) state = IsAwaitingDeviceResponseState();
|
||||||
|
|
||||||
|
pendingTransaction =
|
||||||
|
await wallet.createTransaction(_credentials(provider));
|
||||||
|
|
||||||
if (provider is ThorChainExchangeProvider) {
|
if (provider is ThorChainExchangeProvider) {
|
||||||
final outputCount = pendingTransaction?.outputCount ?? 0;
|
final outputCount = pendingTransaction?.outputCount ?? 0;
|
||||||
|
@ -412,7 +463,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||||
}
|
}
|
||||||
|
|
||||||
if (wallet.type == WalletType.bitcoin) {
|
if (wallet.type == WalletType.bitcoin) {
|
||||||
final updatedOutputs = bitcoin!.updateOutputs(pendingTransaction!, outputs);
|
final updatedOutputs =
|
||||||
|
bitcoin!.updateOutputs(pendingTransaction!, outputs);
|
||||||
|
|
||||||
if (outputs.length == updatedOutputs.length) {
|
if (outputs.length == updatedOutputs.length) {
|
||||||
outputs.replaceRange(0, outputs.length, updatedOutputs);
|
outputs.replaceRange(0, outputs.length, updatedOutputs);
|
||||||
|
@ -473,7 +525,26 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||||
@action
|
@action
|
||||||
Future<void> commitTransaction(BuildContext context) async {
|
Future<void> commitTransaction(BuildContext context) async {
|
||||||
if (pendingTransaction == null) {
|
if (pendingTransaction == null) {
|
||||||
throw Exception("Pending transaction doesn't exist. It should not be happened.");
|
throw Exception(
|
||||||
|
"Pending transaction doesn't exist. It should not be happened.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ocpRequest != null) {
|
||||||
|
state = TransactionCommitting();
|
||||||
|
if (selectedCryptoCurrency == CryptoCurrency.xmr) {
|
||||||
|
await pendingTransaction!.commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
await _ocpService.commitOpenCryptoPayRequest(
|
||||||
|
pendingTransaction!.hex,
|
||||||
|
txId: pendingTransaction!.id,
|
||||||
|
request: ocpRequest!,
|
||||||
|
asset: selectedCryptoCurrency,
|
||||||
|
);
|
||||||
|
|
||||||
|
state = TransactionCommitted();
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String address = outputs.fold('', (acc, value) {
|
String address = outputs.fold('', (acc, value) {
|
||||||
|
@ -493,8 +564,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||||
|
|
||||||
if (pendingTransaction!.shouldCommitUR()) {
|
if (pendingTransaction!.shouldCommitUR()) {
|
||||||
final urstr = await pendingTransaction!.commitUR();
|
final urstr = await pendingTransaction!.commitUR();
|
||||||
final result =
|
final result = await Navigator.of(context)
|
||||||
await Navigator.of(context).pushNamed(Routes.urqrAnimatedPage, arguments: urstr);
|
.pushNamed(Routes.urqrAnimatedPage, arguments: urstr);
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
state = FailureState("Canceled by user");
|
state = FailureState("Canceled by user");
|
||||||
return;
|
return;
|
||||||
|
@ -508,12 +579,18 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pendingTransaction!.id.isNotEmpty) {
|
if (pendingTransaction!.id.isNotEmpty) {
|
||||||
final descriptionKey = '${pendingTransaction!.id}_${wallet.walletAddresses.primaryAddress}';
|
final descriptionKey =
|
||||||
|
'${pendingTransaction!.id}_${wallet.walletAddresses.primaryAddress}';
|
||||||
_settingsStore.shouldSaveRecipientAddress
|
_settingsStore.shouldSaveRecipientAddress
|
||||||
? await transactionDescriptionBox.add(TransactionDescription(
|
? await transactionDescriptionBox.add(TransactionDescription(
|
||||||
id: descriptionKey, recipientAddress: address, transactionNote: note))
|
id: descriptionKey,
|
||||||
: await transactionDescriptionBox
|
recipientAddress: address,
|
||||||
.add(TransactionDescription(id: descriptionKey, transactionNote: note));
|
transactionNote: note,
|
||||||
|
))
|
||||||
|
: await transactionDescriptionBox.add(TransactionDescription(
|
||||||
|
id: descriptionKey,
|
||||||
|
transactionNote: note,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
state = TransactionCommitted();
|
state = TransactionCommitted();
|
||||||
|
@ -552,12 +629,10 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||||
);
|
);
|
||||||
|
|
||||||
case WalletType.monero:
|
case WalletType.monero:
|
||||||
return monero!
|
return monero!.createMoneroTransactionCreationCredentials(outputs: outputs, priority: priority!);
|
||||||
.createMoneroTransactionCreationCredentials(outputs: outputs, priority: priority!);
|
|
||||||
|
|
||||||
case WalletType.wownero:
|
case WalletType.wownero:
|
||||||
return wownero!
|
return wownero!.createWowneroTransactionCreationCredentials(outputs: outputs, priority: priority!);
|
||||||
.createWowneroTransactionCreationCredentials(outputs: outputs, priority: priority!);
|
|
||||||
|
|
||||||
case WalletType.ethereum:
|
case WalletType.ethereum:
|
||||||
return ethereum!.createEthereumTransactionCredentials(outputs,
|
return ethereum!.createEthereumTransactionCredentials(outputs,
|
||||||
|
@ -568,8 +643,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||||
return polygon!.createPolygonTransactionCredentials(outputs,
|
return polygon!.createPolygonTransactionCredentials(outputs,
|
||||||
priority: priority!, currency: selectedCryptoCurrency);
|
priority: priority!, currency: selectedCryptoCurrency);
|
||||||
case WalletType.solana:
|
case WalletType.solana:
|
||||||
return solana!
|
return solana!.createSolanaTransactionCredentials(outputs, currency: selectedCryptoCurrency);
|
||||||
.createSolanaTransactionCredentials(outputs, currency: selectedCryptoCurrency);
|
|
||||||
case WalletType.tron:
|
case WalletType.tron:
|
||||||
return tron!.createTronTransactionCredentials(outputs, currency: selectedCryptoCurrency);
|
return tron!.createTronTransactionCredentials(outputs, currency: selectedCryptoCurrency);
|
||||||
case WalletType.zano:
|
case WalletType.zano:
|
||||||
|
@ -603,8 +677,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||||
|
|
||||||
ContactRecord? newContactAddress() {
|
ContactRecord? newContactAddress() {
|
||||||
final Set<String> contactAddresses =
|
final Set<String> contactAddresses =
|
||||||
Set.from(contactListViewModel.contacts.map((contact) => contact.address))
|
Set.from(contactListViewModel.contacts.map((contact) => contact.address))
|
||||||
..addAll(contactListViewModel.walletContacts.map((contact) => contact.address));
|
..addAll(contactListViewModel.walletContacts.map((contact) => contact.address));
|
||||||
|
|
||||||
for (var output in outputs) {
|
for (var output in outputs) {
|
||||||
String address;
|
String address;
|
||||||
|
|
|
@ -697,7 +697,7 @@
|
||||||
"send": "Senden",
|
"send": "Senden",
|
||||||
"send_address": "${cryptoCurrency}-Adresse",
|
"send_address": "${cryptoCurrency}-Adresse",
|
||||||
"send_amount": "Betrag:",
|
"send_amount": "Betrag:",
|
||||||
"send_change_to_you": "Verändere dich zu dir:",
|
"send_change_to_you": "Rückgeld:",
|
||||||
"send_creating_transaction": "Erstelle Transaktion",
|
"send_creating_transaction": "Erstelle Transaktion",
|
||||||
"send_error_currency": "Die Währung darf nur Zahlen enthalten",
|
"send_error_currency": "Die Währung darf nur Zahlen enthalten",
|
||||||
"send_error_minimum_value": "Der Mindestbetrag ist 0,01",
|
"send_error_minimum_value": "Der Mindestbetrag ist 0,01",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue