diff --git a/cw_evm/lib/evm_chain_client.dart b/cw_evm/lib/evm_chain_client.dart index b505577e9..7e6caf374 100644 --- a/cw_evm/lib/evm_chain_client.dart +++ b/cw_evm/lib/evm_chain_client.dart @@ -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, diff --git a/cw_monero/lib/api/transaction_history.dart b/cw_monero/lib/api/transaction_history.dart index a51e415a9..8c7b1e902 100644 --- a/cw_monero/lib/api/transaction_history.dart +++ b/cw_monero/lib/api/transaction_history.dart @@ -185,13 +185,14 @@ Future 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 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, ); diff --git a/lib/core/open_crypto_pay/exceptions.dart b/lib/core/open_crypto_pay/exceptions.dart new file mode 100644 index 000000000..71010482f --- /dev/null +++ b/lib/core/open_crypto_pay/exceptions.dart @@ -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"; +} diff --git a/lib/core/open_crypto_pay/lnurl.dart b/lib/core/open_crypto_pay/lnurl.dart new file mode 100644 index 000000000..0087bab51 --- /dev/null +++ b/lib/core/open_crypto_pay/lnurl.dart @@ -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 _convert(List data, int inBits, int outBits, bool pad) { + var value = 0; + var bits = 0; + final maxV = (1 << outBits) - 1; + + final result = []; + 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; +} diff --git a/lib/core/open_crypto_pay/models.dart b/lib/core/open_crypto_pay/models.dart new file mode 100644 index 000000000..760200f8d --- /dev/null +++ b/lib/core/open_crypto_pay/models.dart @@ -0,0 +1,26 @@ +class OpenCryptoPayRequest { + final String receiverName; + final int expiry; + final String callbackUrl; + final String quote; + final Map> 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 json) + : symbol = json['asset'] as String, + amount = json['amount'] as String; +} diff --git a/lib/core/open_crypto_pay/open_cryptopay_service.dart b/lib/core/open_crypto_pay/open_cryptopay_service.dart new file mode 100644 index 000000000..2591f8903 --- /dev/null +++ b/lib/core/open_crypto_pay/open_cryptopay_service.dart @@ -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 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 cancelOpenCryptoPayRequest(OpenCryptoPayRequest request) async { + final uri = Uri.parse(request.callbackUrl.replaceAll("/cb/", "/cancel/")); + + await _httpClient.delete(uri); + } + + Future 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>)> + _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 = >{}; + 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); + methods[method]?.add(asset); + } + } + + log(responseBody.toString()); + + final quote = _OpenCryptoPayQuote.fromJson( + responseBody['callback'] as String, + responseBody['displayName'] as String?, + responseBody['quote'] as Map); + + return (quote, methods); + } else { + throw OpenCryptoPayException( + 'Failed to get Open CryptoPay Request. Status: ${response.statusCode} ${response.body}'); + } + } + + Future 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.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 json) + : id = json['id'] as String, + expiration = DateTime.parse(json['expiration'] as String); +} diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index 91f94e060..368dac19e 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -547,9 +547,9 @@ class SendPage extends BasePage { } if (state is ExecutedSuccessfullyState) { - WidgetsBinding.instance.addPostFrameCallback((_) { + WidgetsBinding.instance.addPostFrameCallback((_) async { if (context.mounted) { - showModalBottomSheet( + final result = await showModalBottomSheet( 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, diff --git a/lib/src/screens/send/widgets/send_card.dart b/lib/src/screens/send/widgets/send_card.dart index ba2b64f3a..c96992a00 100644 --- a/lib/src/screens/send/widgets/send_card.dart +++ b/lib/src/screens/send/widgets/send_card.dart @@ -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 with AutomaticKeepAliveClientMixin 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, diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 117706228..6b1515bb3 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -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 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 dismissTransaction() async { + state = InitialExecutionState(); + if (ocpRequest != null) { + clearOutputs(); + _ocpService.cancelOpenCryptoPayRequest(ocpRequest!); + ocpRequest = null; + } + } + + @action + Future 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 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 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 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; diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 04f11ff02..699f8306a 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -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",