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);
|
||||
|
||||
return PendingEVMChainTransaction(
|
||||
signedTransaction: signedTransaction,
|
||||
signedTransaction: prepareSignedTransactionForSending(signedTransaction),
|
||||
amount: amount.toString(),
|
||||
fee: gasFee,
|
||||
sendTransaction: _sendTransaction,
|
||||
|
|
|
@ -185,13 +185,14 @@ Future<PendingTransactionDescription> createTransactionSync(
|
|||
final rAmt = monero.PendingTransaction_amount(pendingTx);
|
||||
final rFee = monero.PendingTransaction_fee(pendingTx);
|
||||
final rHash = monero.PendingTransaction_txid(pendingTx, '');
|
||||
final rHex = monero.PendingTransaction_hex(pendingTx, '');
|
||||
final rTxKey = rHash;
|
||||
|
||||
return PendingTransactionDescription(
|
||||
amount: rAmt,
|
||||
fee: rFee,
|
||||
hash: rHash,
|
||||
hex: '',
|
||||
hex: rHex,
|
||||
txKey: rTxKey,
|
||||
pointerAddress: pendingTx.address,
|
||||
);
|
||||
|
@ -234,7 +235,7 @@ Future<PendingTransactionDescription> createTransactionMultDest(
|
|||
amount: monero.PendingTransaction_amount(txptr),
|
||||
fee: monero.PendingTransaction_fee(txptr),
|
||||
hash: monero.PendingTransaction_txid(txptr, ''),
|
||||
hex: monero.PendingTransaction_txid(txptr, ''),
|
||||
hex: monero.PendingTransaction_hex(txptr, ''),
|
||||
txKey: monero.PendingTransaction_txid(txptr, ''),
|
||||
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) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
if (context.mounted) {
|
||||
showModalBottomSheet<void>(
|
||||
final result = await showModalBottomSheet<bool>(
|
||||
context: context,
|
||||
isDismissible: false,
|
||||
isScrollControlled: true,
|
||||
|
@ -570,13 +570,16 @@ class SendPage extends BasePage {
|
|||
feeFiatAmount: sendViewModel.pendingTransactionFeeFiatAmountFormatted,
|
||||
outputs: sendViewModel.outputs,
|
||||
onSlideComplete: () async {
|
||||
Navigator.of(bottomSheetContext).pop();
|
||||
Navigator.of(bottomSheetContext).pop(true);
|
||||
sendViewModel.commitTransaction(context);
|
||||
},
|
||||
change: sendViewModel.pendingTransaction!.change,
|
||||
isOpenCryptoPay: sendViewModel.ocpRequest != null,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (result == null) sendViewModel.dismissTransaction();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -600,7 +603,8 @@ class SendPage extends BasePage {
|
|||
context: context,
|
||||
isDismissible: false,
|
||||
builder: (BuildContext bottomSheetContext) {
|
||||
return showContactSheet
|
||||
return showContactSheet &&
|
||||
sendViewModel.ocpRequest == null
|
||||
? InfoBottomSheet(
|
||||
currentTheme: currentTheme,
|
||||
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/src/screens/receive/widgets/currency_input_field.dart';
|
||||
import 'package:cake_wallet/src/widgets/picker.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/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/responsive_layout_util.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/currency.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:cw_core/transaction_priority.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_mobx/flutter_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/utils/show_pop_up.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/themes/extensions/send_page_theme.dart';
|
||||
|
||||
import '../../../../themes/extensions/cake_text_theme.dart';
|
||||
import '../../../../themes/theme_base.dart';
|
||||
|
||||
class SendCard extends StatefulWidget {
|
||||
SendCard({
|
||||
Key? key,
|
||||
|
@ -185,11 +182,16 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
|
|||
addressKey: ValueKey('send_page_address_textfield_key'),
|
||||
focusNode: addressFocusNode,
|
||||
controller: addressController,
|
||||
onURIScanned: (uri) {
|
||||
final paymentRequest = PaymentRequest.fromUri(uri);
|
||||
addressController.text = paymentRequest.address;
|
||||
cryptoAmountController.text = paymentRequest.amount;
|
||||
noteController.text = paymentRequest.note;
|
||||
onURIScanned: (uri) async {
|
||||
if (OpenCryptoPayService.isOpenCryptoPayQR(
|
||||
uri.toString())) {
|
||||
sendViewModel.createOpenCryptoPayTransaction(uri.toString());
|
||||
} else {
|
||||
final paymentRequest = PaymentRequest.fromUri(uri);
|
||||
addressController.text = paymentRequest.address;
|
||||
cryptoAmountController.text = paymentRequest.amount;
|
||||
noteController.text = paymentRequest.note;
|
||||
}
|
||||
},
|
||||
options: [
|
||||
AddressTextFieldOption.paste,
|
||||
|
|
|
@ -28,6 +28,7 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
|
|||
final List<Output> outputs;
|
||||
final VoidCallback onSlideComplete;
|
||||
final PendingChange? change;
|
||||
final bool isOpenCryptoPay;
|
||||
|
||||
ConfirmSendingBottomSheet({
|
||||
required String titleText,
|
||||
|
@ -46,6 +47,7 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
|
|||
required this.outputs,
|
||||
required this.onSlideComplete,
|
||||
this.change,
|
||||
this.isOpenCryptoPay = false,
|
||||
Key? key,
|
||||
}) : showScrollbar = outputs.length > 3,
|
||||
super(titleText: titleText, titleIconPath: titleIconPath);
|
||||
|
@ -132,7 +134,7 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
|
|||
final _amount = item.cryptoAmount.replaceAll(',', '.') + ' ${currency.title}';
|
||||
return isBatchSending || contactName.isNotEmpty
|
||||
? AddressExpansionTile(
|
||||
contactType: 'Contact',
|
||||
contactType: isOpenCryptoPay ? 'Open CryptoPay' : S.of(context).contact,
|
||||
currentTheme: currentTheme,
|
||||
name: isBatchSending ? batchContactTitle : contactName,
|
||||
address: _address,
|
||||
|
@ -143,7 +145,7 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
|
|||
tileBackgroundColor: tileBackgroundColor,
|
||||
)
|
||||
: AddressTile(
|
||||
itemTitle: 'Address',
|
||||
itemTitle: S.of(context).address,
|
||||
currentTheme: currentTheme,
|
||||
itemTitleTextStyle: itemTitleTextStyle,
|
||||
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_record.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/wallet_contact.dart';
|
||||
import 'package:cake_wallet/ethereum/ethereum.dart';
|
||||
import 'package:cake_wallet/exchange/provider/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/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/reactions/wallet_connect.dart';
|
||||
import 'package:cake_wallet/routes.dart';
|
||||
import 'package:cake_wallet/solana/solana.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/utils/payment_request.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/hardware_wallet/ledger_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/wownero/wownero.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/pending_transaction.dart';
|
||||
import 'package:cw_core/sync_status.dart';
|
||||
import 'package:cw_core/transaction_info.dart';
|
||||
import 'package:cw_core/unspent_coin_type.dart';
|
||||
import 'package:cake_wallet/view_model/send/output.dart';
|
||||
import 'package:cake_wallet/view_model/send/send_template_view_model.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hive/hive.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';
|
||||
|
||||
|
@ -390,15 +394,62 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
|||
return conditionsList.contains(true);
|
||||
}
|
||||
|
||||
@action
|
||||
Future<PendingTransaction?> createTransaction({ExchangeProvider? provider}) async {
|
||||
try {
|
||||
if (wallet.isHardwareWallet)
|
||||
state = IsAwaitingDeviceResponseState();
|
||||
else
|
||||
state = IsExecutingState();
|
||||
final _ocpService = OpenCryptoPayService();
|
||||
|
||||
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) {
|
||||
final outputCount = pendingTransaction?.outputCount ?? 0;
|
||||
|
@ -412,7 +463,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
|||
}
|
||||
|
||||
if (wallet.type == WalletType.bitcoin) {
|
||||
final updatedOutputs = bitcoin!.updateOutputs(pendingTransaction!, outputs);
|
||||
final updatedOutputs =
|
||||
bitcoin!.updateOutputs(pendingTransaction!, outputs);
|
||||
|
||||
if (outputs.length == updatedOutputs.length) {
|
||||
outputs.replaceRange(0, outputs.length, updatedOutputs);
|
||||
|
@ -473,7 +525,26 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
|||
@action
|
||||
Future<void> commitTransaction(BuildContext context) async {
|
||||
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) {
|
||||
|
@ -493,8 +564,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
|||
|
||||
if (pendingTransaction!.shouldCommitUR()) {
|
||||
final urstr = await pendingTransaction!.commitUR();
|
||||
final result =
|
||||
await Navigator.of(context).pushNamed(Routes.urqrAnimatedPage, arguments: urstr);
|
||||
final result = await Navigator.of(context)
|
||||
.pushNamed(Routes.urqrAnimatedPage, arguments: urstr);
|
||||
if (result == null) {
|
||||
state = FailureState("Canceled by user");
|
||||
return;
|
||||
|
@ -508,12 +579,18 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
|||
}
|
||||
|
||||
if (pendingTransaction!.id.isNotEmpty) {
|
||||
final descriptionKey = '${pendingTransaction!.id}_${wallet.walletAddresses.primaryAddress}';
|
||||
final descriptionKey =
|
||||
'${pendingTransaction!.id}_${wallet.walletAddresses.primaryAddress}';
|
||||
_settingsStore.shouldSaveRecipientAddress
|
||||
? await transactionDescriptionBox.add(TransactionDescription(
|
||||
id: descriptionKey, recipientAddress: address, transactionNote: note))
|
||||
: await transactionDescriptionBox
|
||||
.add(TransactionDescription(id: descriptionKey, transactionNote: note));
|
||||
id: descriptionKey,
|
||||
recipientAddress: address,
|
||||
transactionNote: note,
|
||||
))
|
||||
: await transactionDescriptionBox.add(TransactionDescription(
|
||||
id: descriptionKey,
|
||||
transactionNote: note,
|
||||
));
|
||||
}
|
||||
|
||||
state = TransactionCommitted();
|
||||
|
@ -552,12 +629,10 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
|||
);
|
||||
|
||||
case WalletType.monero:
|
||||
return monero!
|
||||
.createMoneroTransactionCreationCredentials(outputs: outputs, priority: priority!);
|
||||
return monero!.createMoneroTransactionCreationCredentials(outputs: outputs, priority: priority!);
|
||||
|
||||
case WalletType.wownero:
|
||||
return wownero!
|
||||
.createWowneroTransactionCreationCredentials(outputs: outputs, priority: priority!);
|
||||
return wownero!.createWowneroTransactionCreationCredentials(outputs: outputs, priority: priority!);
|
||||
|
||||
case WalletType.ethereum:
|
||||
return ethereum!.createEthereumTransactionCredentials(outputs,
|
||||
|
@ -568,8 +643,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
|||
return polygon!.createPolygonTransactionCredentials(outputs,
|
||||
priority: priority!, currency: selectedCryptoCurrency);
|
||||
case WalletType.solana:
|
||||
return solana!
|
||||
.createSolanaTransactionCredentials(outputs, currency: selectedCryptoCurrency);
|
||||
return solana!.createSolanaTransactionCredentials(outputs, currency: selectedCryptoCurrency);
|
||||
case WalletType.tron:
|
||||
return tron!.createTronTransactionCredentials(outputs, currency: selectedCryptoCurrency);
|
||||
case WalletType.zano:
|
||||
|
@ -603,8 +677,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
|||
|
||||
ContactRecord? newContactAddress() {
|
||||
final Set<String> contactAddresses =
|
||||
Set.from(contactListViewModel.contacts.map((contact) => contact.address))
|
||||
..addAll(contactListViewModel.walletContacts.map((contact) => contact.address));
|
||||
Set.from(contactListViewModel.contacts.map((contact) => contact.address))
|
||||
..addAll(contactListViewModel.walletContacts.map((contact) => contact.address));
|
||||
|
||||
for (var output in outputs) {
|
||||
String address;
|
||||
|
|
|
@ -697,7 +697,7 @@
|
|||
"send": "Senden",
|
||||
"send_address": "${cryptoCurrency}-Adresse",
|
||||
"send_amount": "Betrag:",
|
||||
"send_change_to_you": "Verändere dich zu dir:",
|
||||
"send_change_to_you": "Rückgeld:",
|
||||
"send_creating_transaction": "Erstelle Transaktion",
|
||||
"send_error_currency": "Die Währung darf nur Zahlen enthalten",
|
||||
"send_error_minimum_value": "Der Mindestbetrag ist 0,01",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue