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>

Konstantin Ullrich committed Apr 10, 2025 at 04:03 UTC c1e9668b1e0f58c43a5bd56eb2762740ef98ea2d
11 files changed +456 -67
cw_evm/lib/evm_chain_client.dart
+1 -1
@@ -190,7 +190,7 @@ abstract class EVMChainClient {
190 _sendTransaction = () async => await sendTransaction(signedTransaction);
191
192 return PendingEVMChainTransaction(
193 - signedTransaction: signedTransaction,
193 + signedTransaction: prepareSignedTransactionForSending(signedTransaction),
194 amount: amount.toString(),
195 fee: gasFee,
196 sendTransaction: _sendTransaction,
cw_monero/lib/api/transaction_history.dart
+3 -2
@@ -185,13 +185,14 @@ Future<PendingTransactionDescription> createTransactionSync(
185 final rAmt = monero.PendingTransaction_amount(pendingTx);
186 final rFee = monero.PendingTransaction_fee(pendingTx);
187 final rHash = monero.PendingTransaction_txid(pendingTx, '');
188 + final rHex = monero.PendingTransaction_hex(pendingTx, '');
189 final rTxKey = rHash;
190
191 return PendingTransactionDescription(
192 amount: rAmt,
193 fee: rFee,
194 hash: rHash,
194 - hex: '',
195 + hex: rHex,
196 txKey: rTxKey,
197 pointerAddress: pendingTx.address,
198 );
@@ -234,7 +235,7 @@ Future<PendingTransactionDescription> createTransactionMultDest(
235 amount: monero.PendingTransaction_amount(txptr),
236 fee: monero.PendingTransaction_fee(txptr),
237 hash: monero.PendingTransaction_txid(txptr, ''),
237 - hex: monero.PendingTransaction_txid(txptr, ''),
238 + hex: monero.PendingTransaction_hex(txptr, ''),
239 txKey: monero.PendingTransaction_txid(txptr, ''),
240 pointerAddress: txptr.address,
241 );
lib/core/open_crypto_pay/exceptions.dart new
+18
@@ -0,0 +1,18 @@
1 +class OpenCryptoPayException implements Exception {
2 + final String message;
3 +
4 + OpenCryptoPayException([this.message = '']);
5 +
6 + @override
7 + String toString() =>
8 + 'OpenCryptoPayException${message.isNotEmpty ? ': $message' : ''}';
9 +}
10 +
11 +class OpenCryptoPayNotSupportedException extends OpenCryptoPayException {
12 + final String provider;
13 +
14 + OpenCryptoPayNotSupportedException(this.provider);
15 +
16 + @override
17 + String get message => "$provider does not support Open CryptoPay";
18 +}
lib/core/open_crypto_pay/lnurl.dart new
+78
@@ -0,0 +1,78 @@
1 +import 'dart:convert';
2 +
3 +import 'package:bech32/bech32.dart';
4 +
5 +Uri decodeLNURL(String encodedUrl) {
6 + Uri decodedUri;
7 +
8 + /// The URL doesn't have to be encoded at all as per LUD-17: Protocol schemes and raw (non bech32-encoded) URLs.
9 + /// https://github.com/lnurl/luds/blob/luds/17.md
10 + /// Handle non bech32-encoded LNURL
11 + final lud17prefixes = ['lnurlw', 'lnurlc', 'lnurlp', 'keyauth'];
12 + decodedUri = Uri.parse(encodedUrl);
13 + for (final prefix in lud17prefixes) {
14 + if (decodedUri.scheme.contains(prefix)) {
15 + decodedUri = decodedUri.replace(scheme: prefix);
16 + }
17 + }
18 + if (lud17prefixes.contains(decodedUri.scheme)) {
19 + /// 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.'
20 + decodedUri = decodedUri.replace(
21 + scheme: decodedUri.host.endsWith('onion') ||
22 + decodedUri.host.endsWith('onion.')
23 + ? 'http'
24 + : 'https');
25 + } else {
26 + /// Try to parse the input as a lnUrl. Will throw an error if it fails.
27 + final lnUrl = _findLnUrl(encodedUrl);
28 +
29 + /// Decode the lnurl using bech32
30 + final bech32 = const Bech32Codec().decode(lnUrl, lnUrl.length);
31 + decodedUri = Uri.parse(utf8.decode(_convert(bech32.data, 5, 8, false)));
32 + }
33 + return decodedUri;
34 +}
35 +
36 +/// Parse and return a given lnurl string if it's valid. Will remove
37 +/// `lightning:` from the beginning of it if present.
38 +String _findLnUrl(String input) {
39 + final res = RegExp(
40 + r',*?((lnurl)([0-9]+[a-z0-9]+))',
41 + ).allMatches(input.toLowerCase());
42 +
43 + if (res.length == 1) {
44 + return res.first.group(0)!;
45 + } else {
46 + throw ArgumentError('Not a valid lnurl string');
47 + }
48 +}
49 +
50 +/// Taken from bech32 (bitcoinjs): https://github.com/bitcoinjs/bech32
51 +List<int> _convert(List<int> data, int inBits, int outBits, bool pad) {
52 + var value = 0;
53 + var bits = 0;
54 + final maxV = (1 << outBits) - 1;
55 +
56 + final result = <int>[];
57 + for (final dataValue in data) {
58 + value = (value << inBits) | dataValue;
59 + bits += inBits;
60 +
61 + while (bits >= outBits) {
62 + bits -= outBits;
63 + result.add((value >> bits) & maxV);
64 + }
65 + }
66 +
67 + if (pad) {
68 + if (bits > 0) result.add((value << (outBits - bits)) & maxV);
69 + } else {
70 + if (bits >= inBits) throw Exception('Excess padding');
71 +
72 + if ((value << (outBits - bits)) & maxV > 0) {
73 + throw Exception('Non-zero padding');
74 + }
75 + }
76 +
77 + return result;
78 +}
lib/core/open_crypto_pay/models.dart new
+26
@@ -0,0 +1,26 @@
1 +class OpenCryptoPayRequest {
2 + final String receiverName;
3 + final int expiry;
4 + final String callbackUrl;
5 + final String quote;
6 + final Map<String, List<OpenCryptoPayQuoteAsset>> methods;
7 +
8 + OpenCryptoPayRequest({
9 + required this.receiverName,
10 + required this.expiry,
11 + required this.callbackUrl,
12 + required this.quote,
13 + required this.methods,
14 + });
15 +}
16 +
17 +class OpenCryptoPayQuoteAsset {
18 + final String symbol;
19 + final String amount;
20 +
21 + const OpenCryptoPayQuoteAsset(this.symbol, this.amount);
22 +
23 + OpenCryptoPayQuoteAsset.fromJson(Map<String, dynamic> json)
24 + : symbol = json['asset'] as String,
25 + amount = json['amount'] as String;
26 +}
lib/core/open_crypto_pay/open_cryptopay_service.dart new
+184
@@ -0,0 +1,184 @@
1 +import 'dart:convert';
2 +import 'dart:developer';
3 +
4 +import 'package:cake_wallet/core/open_crypto_pay/exceptions.dart';
5 +import 'package:cake_wallet/core/open_crypto_pay/lnurl.dart';
6 +import 'package:cake_wallet/core/open_crypto_pay/models.dart';
7 +import 'package:cw_core/crypto_currency.dart';
8 +import 'package:http/http.dart';
9 +
10 +class OpenCryptoPayService {
11 + static bool isOpenCryptoPayQR(String value) =>
12 + value.toLowerCase().contains("lightning=lnurl") ||
13 + value.toLowerCase().startsWith("lnurl");
14 +
15 + final Client _httpClient = Client();
16 +
17 + Future<String> commitOpenCryptoPayRequest(
18 + String txHex, {
19 + required String txId,
20 + required OpenCryptoPayRequest request,
21 + required CryptoCurrency asset,
22 + }) async {
23 + final uri = Uri.parse(request.callbackUrl.replaceAll("/cb/", "/tx/"));
24 +
25 + final queryParams = Map.of(uri.queryParameters);
26 +
27 + queryParams['quote'] = request.quote;
28 + queryParams['asset'] = asset.title;
29 + queryParams['method'] = _getMethod(asset);
30 + queryParams['hex'] = txHex;
31 + queryParams['tx'] = txId;
32 +
33 + final response =
34 + await _httpClient.get(Uri.https(uri.authority, uri.path, queryParams));
35 +
36 + if (response.statusCode == 200) {
37 + final body = jsonDecode(response.body) as Map;
38 +
39 + if (body.keys.contains("txId")) return body["txId"] as String;
40 + throw OpenCryptoPayException(body.toString());
41 + }
42 + throw OpenCryptoPayException(
43 + "Unexpected status code ${response.statusCode} ${response.body}");
44 + }
45 +
46 + Future<void> cancelOpenCryptoPayRequest(OpenCryptoPayRequest request) async {
47 + final uri = Uri.parse(request.callbackUrl.replaceAll("/cb/", "/cancel/"));
48 +
49 + await _httpClient.delete(uri);
50 + }
51 +
52 + Future<OpenCryptoPayRequest> getOpenCryptoPayInvoice(String lnUrl) async {
53 + if (lnUrl.toLowerCase().startsWith("http")) {
54 + final uri = Uri.parse(lnUrl);
55 + final params = uri.queryParameters;
56 + if (!params.containsKey("lightning")) {
57 + throw OpenCryptoPayNotSupportedException(uri.authority);
58 + }
59 +
60 + lnUrl = params["lightning"] as String;
61 + }
62 + final url = decodeLNURL(lnUrl);
63 + final params = await _getOpenCryptoPayParams(url);
64 +
65 + return OpenCryptoPayRequest(
66 + receiverName: params.$1.displayName ?? "Unknown",
67 + expiry: params.$1.expiration.difference(DateTime.now()).inSeconds,
68 + callbackUrl: params.$1.callbackUrl,
69 + quote: params.$1.id,
70 + methods: params.$2,
71 + );
72 + }
73 +
74 + Future<(_OpenCryptoPayQuote, Map<String, List<OpenCryptoPayQuoteAsset>>)>
75 + _getOpenCryptoPayParams(Uri uri) async {
76 + final response = await _httpClient.get(uri);
77 +
78 + if (response.statusCode == 200) {
79 + final responseBody = jsonDecode(response.body) as Map;
80 +
81 + for (final key in ['callback', 'transferAmounts', 'quote']) {
82 + if (!responseBody.keys.contains(key)) {
83 + throw OpenCryptoPayNotSupportedException(uri.authority);
84 + }
85 + }
86 +
87 + final methods = <String, List<OpenCryptoPayQuoteAsset>>{};
88 + for (final transferAmountRaw in responseBody['transferAmounts'] as List) {
89 + final transferAmount = transferAmountRaw as Map;
90 + final method = transferAmount['method'] as String;
91 + methods[method] = [];
92 + for (final assetJson in transferAmount['assets'] as List) {
93 + final asset = OpenCryptoPayQuoteAsset.fromJson(
94 + assetJson as Map<String, dynamic>);
95 + methods[method]?.add(asset);
96 + }
97 + }
98 +
99 + log(responseBody.toString());
100 +
101 + final quote = _OpenCryptoPayQuote.fromJson(
102 + responseBody['callback'] as String,
103 + responseBody['displayName'] as String?,
104 + responseBody['quote'] as Map<String, dynamic>);
105 +
106 + return (quote, methods);
107 + } else {
108 + throw OpenCryptoPayException(
109 + 'Failed to get Open CryptoPay Request. Status: ${response.statusCode} ${response.body}');
110 + }
111 + }
112 +
113 + Future<Uri> getOpenCryptoPayAddress(
114 + OpenCryptoPayRequest request, CryptoCurrency asset) async {
115 + final uri = Uri.parse(request.callbackUrl);
116 + final queryParams = Map.of(uri.queryParameters);
117 +
118 + queryParams['quote'] = request.quote;
119 + queryParams['asset'] = asset.title;
120 + queryParams['method'] = _getMethod(asset);
121 +
122 + final response =
123 + await _httpClient.get(Uri.https(uri.authority, uri.path, queryParams));
124 +
125 + if (response.statusCode == 200) {
126 + final responseBody = jsonDecode(response.body) as Map;
127 +
128 + for (final key in ['expiryDate', 'uri']) {
129 + if (!responseBody.keys.contains(key)) {
130 + throw OpenCryptoPayNotSupportedException(uri.authority);
131 + }
132 + }
133 +
134 + final result = Uri.parse(responseBody['uri'] as String);
135 +
136 + if (result.queryParameters['amount'] != null) return result;
137 +
138 + final newQueryParameters =
139 + Map<String, dynamic>.from(result.queryParameters);
140 +
141 + newQueryParameters['amount'] = _getAmountByAsset(request, asset);
142 + return Uri(
143 + scheme: result.scheme,
144 + path: result.path.split("@").first,
145 + queryParameters: newQueryParameters);
146 + } else {
147 + throw OpenCryptoPayException(
148 + 'Failed to create Open CryptoPay Request. Status: ${response.statusCode} ${response.body}');
149 + }
150 + }
151 +
152 + String _getAmountByAsset(OpenCryptoPayRequest request, CryptoCurrency asset) {
153 + final method = _getMethod(asset);
154 + return request.methods[method]!
155 + .firstWhere((e) => e.symbol == asset.title)
156 + .amount;
157 + }
158 +
159 + String _getMethod(CryptoCurrency asset) {
160 + switch (asset.tag) {
161 + case "ETH":
162 + return "Ethereum";
163 + case "POL":
164 + return "Polygon";
165 + default:
166 + return asset.fullName!;
167 + }
168 + }
169 +}
170 +
171 +class _OpenCryptoPayQuote {
172 + final String callbackUrl;
173 + final String? displayName;
174 + final String id;
175 + final DateTime expiration;
176 +
177 + _OpenCryptoPayQuote(
178 + this.callbackUrl, this.displayName, this.id, this.expiration);
179 +
180 + _OpenCryptoPayQuote.fromJson(
181 + this.callbackUrl, this.displayName, Map<String, dynamic> json)
182 + : id = json['id'] as String,
183 + expiration = DateTime.parse(json['expiration'] as String);
184 +}
lib/src/screens/send/send_page.dart
+8 -4
@@ -547,9 +547,9 @@ class SendPage extends BasePage {
547 }
548
549 if (state is ExecutedSuccessfullyState) {
550 - WidgetsBinding.instance.addPostFrameCallback((_) {
550 + WidgetsBinding.instance.addPostFrameCallback((_) async {
551 if (context.mounted) {
552 - showModalBottomSheet<void>(
552 + final result = await showModalBottomSheet<bool>(
553 context: context,
554 isDismissible: false,
555 isScrollControlled: true,
@@ -570,13 +570,16 @@ class SendPage extends BasePage {
570 feeFiatAmount: sendViewModel.pendingTransactionFeeFiatAmountFormatted,
571 outputs: sendViewModel.outputs,
572 onSlideComplete: () async {
573 - Navigator.of(bottomSheetContext).pop();
573 + Navigator.of(bottomSheetContext).pop(true);
574 sendViewModel.commitTransaction(context);
575 },
576 change: sendViewModel.pendingTransaction!.change,
577 + isOpenCryptoPay: sendViewModel.ocpRequest != null,
578 );
579 },
580 );
581 +
582 + if (result == null) sendViewModel.dismissTransaction();
583 }
584 });
585 }
@@ -600,7 +603,8 @@ class SendPage extends BasePage {
603 context: context,
604 isDismissible: false,
605 builder: (BuildContext bottomSheetContext) {
603 - return showContactSheet
606 + return showContactSheet &&
607 + sendViewModel.ocpRequest == null
608 ? InfoBottomSheet(
609 currentTheme: currentTheme,
610 showDontAskMeCheckbox: true,
lib/src/screens/send/widgets/send_card.dart
+13 -11
@@ -1,16 +1,17 @@
1 +import 'package:cake_wallet/core/open_crypto_pay/open_cryptopay_service.dart';
2 import 'package:cake_wallet/entities/priority_for_wallet_type.dart';
3 import 'package:cake_wallet/src/screens/receive/widgets/currency_input_field.dart';
4 import 'package:cake_wallet/src/widgets/picker.dart';
5 import 'package:cake_wallet/src/widgets/standard_checkbox.dart';
5 -import 'package:cake_wallet/themes/extensions/keyboard_theme.dart';
6 import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart';
7 import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
8 +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
9 +import 'package:cake_wallet/themes/theme_base.dart';
10 import 'package:cake_wallet/utils/payment_request.dart';
11 import 'package:cake_wallet/utils/responsive_layout_util.dart';
12 import 'package:cw_core/crypto_currency.dart';
13 import 'package:cw_core/currency.dart';
14 import 'package:cake_wallet/routes.dart';
13 -import 'package:cake_wallet/src/widgets/keyboard_done_button.dart';
15 import 'package:cake_wallet/view_model/send/output.dart';
16 import 'package:cw_core/transaction_priority.dart';
17 import 'package:cw_core/unspent_coin_type.dart';
@@ -18,7 +19,6 @@ import 'package:cw_core/wallet_type.dart';
19 import 'package:flutter/material.dart';
20 import 'package:flutter_mobx/flutter_mobx.dart';
21 import 'package:mobx/mobx.dart';
21 -import 'package:keyboard_actions/keyboard_actions.dart';
22 import 'package:cake_wallet/view_model/send/send_view_model.dart';
23 import 'package:cake_wallet/utils/show_pop_up.dart';
24 import 'package:cake_wallet/src/widgets/address_text_field.dart';
@@ -26,9 +26,6 @@ import 'package:cake_wallet/generated/i18n.dart';
26 import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
27 import 'package:cake_wallet/themes/extensions/send_page_theme.dart';
28
29 -import '../../../../themes/extensions/cake_text_theme.dart';
30 -import '../../../../themes/theme_base.dart';
31 -
29 class SendCard extends StatefulWidget {
30 SendCard({
31 Key? key,
@@ -185,11 +182,16 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
182 addressKey: ValueKey('send_page_address_textfield_key'),
183 focusNode: addressFocusNode,
184 controller: addressController,
188 - onURIScanned: (uri) {
189 - final paymentRequest = PaymentRequest.fromUri(uri);
190 - addressController.text = paymentRequest.address;
191 - cryptoAmountController.text = paymentRequest.amount;
192 - noteController.text = paymentRequest.note;
185 + onURIScanned: (uri) async {
186 + if (OpenCryptoPayService.isOpenCryptoPayQR(
187 + uri.toString())) {
188 + sendViewModel.createOpenCryptoPayTransaction(uri.toString());
189 + } else {
190 + final paymentRequest = PaymentRequest.fromUri(uri);
191 + addressController.text = paymentRequest.address;
192 + cryptoAmountController.text = paymentRequest.amount;
193 + noteController.text = paymentRequest.note;
194 + }
195 },
196 options: [
197 AddressTextFieldOption.paste,
lib/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart
+4 -2
@@ -28,6 +28,7 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
28 final List<Output> outputs;
29 final VoidCallback onSlideComplete;
30 final PendingChange? change;
31 + final bool isOpenCryptoPay;
32
33 ConfirmSendingBottomSheet({
34 required String titleText,
@@ -46,6 +47,7 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
47 required this.outputs,
48 required this.onSlideComplete,
49 this.change,
50 + this.isOpenCryptoPay = false,
51 Key? key,
52 }) : showScrollbar = outputs.length > 3,
53 super(titleText: titleText, titleIconPath: titleIconPath);
@@ -132,7 +134,7 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
134 final _amount = item.cryptoAmount.replaceAll(',', '.') + ' ${currency.title}';
135 return isBatchSending || contactName.isNotEmpty
136 ? AddressExpansionTile(
135 - contactType: 'Contact',
137 + contactType: isOpenCryptoPay ? 'Open CryptoPay' : S.of(context).contact,
138 currentTheme: currentTheme,
139 name: isBatchSending ? batchContactTitle : contactName,
140 address: _address,
@@ -143,7 +145,7 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
145 tileBackgroundColor: tileBackgroundColor,
146 )
147 : AddressTile(
146 - itemTitle: 'Address',
148 + itemTitle: S.of(context).address,
149 currentTheme: currentTheme,
150 itemTitleTextStyle: itemTitleTextStyle,
151 isBatchSending: isBatchSending,
lib/view_model/send/send_view_model.dart
+120 -46
@@ -1,53 +1,57 @@
1 +import 'package:cake_wallet/bitcoin/bitcoin.dart';
2 +import 'package:cake_wallet/core/address_validator.dart';
3 +import 'package:cake_wallet/core/amount_validator.dart';
4 +import 'package:cake_wallet/core/execution_state.dart';
5 +import 'package:cake_wallet/core/open_crypto_pay/models.dart';
6 +import 'package:cake_wallet/core/open_crypto_pay/open_cryptopay_service.dart';
7 +import 'package:cake_wallet/core/validator.dart';
8 +import 'package:cake_wallet/core/wallet_change_listener_view_model.dart';
9 +import 'package:cake_wallet/decred/decred.dart';
10 +import 'package:cake_wallet/entities/calculate_fiat_amount.dart';
11 import 'package:cake_wallet/entities/contact.dart';
12 +import 'package:cake_wallet/entities/contact_record.dart';
13 import 'package:cake_wallet/entities/evm_transaction_error_fees_handler.dart';
14 +import 'package:cake_wallet/entities/fiat_currency.dart';
15 +import 'package:cake_wallet/entities/parsed_address.dart';
16 +import 'package:cake_wallet/entities/template.dart';
17 import 'package:cake_wallet/entities/transaction_description.dart';
18 +import 'package:cake_wallet/entities/wallet_contact.dart';
19 import 'package:cake_wallet/ethereum/ethereum.dart';
20 import 'package:cake_wallet/exchange/provider/exchange_provider.dart';
21 import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart';
22 +import 'package:cake_wallet/generated/i18n.dart';
23 +import 'package:cake_wallet/monero/monero.dart';
24 import 'package:cake_wallet/nano/nano.dart';
8 -import 'package:cake_wallet/decred/decred.dart';
9 -import 'package:cake_wallet/core/wallet_change_listener_view_model.dart';
10 -import 'package:cake_wallet/entities/contact_record.dart';
11 -import 'package:cake_wallet/entities/wallet_contact.dart';
25 import 'package:cake_wallet/polygon/polygon.dart';
26 import 'package:cake_wallet/reactions/wallet_connect.dart';
27 import 'package:cake_wallet/routes.dart';
28 import 'package:cake_wallet/solana/solana.dart';
29 import 'package:cake_wallet/store/app_store.dart';
30 +import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
31 +import 'package:cake_wallet/store/settings_store.dart';
32 import 'package:cake_wallet/tron/tron.dart';
33 +import 'package:cake_wallet/utils/payment_request.dart';
34 import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart';
35 import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart';
36 import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart';
37 import 'package:cake_wallet/view_model/send/fees_view_model.dart';
38 +import 'package:cake_wallet/view_model/send/output.dart';
39 +import 'package:cake_wallet/view_model/send/send_template_view_model.dart';
40 +import 'package:cake_wallet/view_model/send/send_view_model_state.dart';
41 import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_list_view_model.dart';
42 import 'package:cake_wallet/wownero/wownero.dart';
43 import 'package:cake_wallet/zano/zano.dart';
44 +import 'package:cw_core/crypto_currency.dart';
45 import 'package:cw_core/exceptions.dart';
46 +import 'package:cw_core/pending_transaction.dart';
47 +import 'package:cw_core/sync_status.dart';
48 import 'package:cw_core/transaction_info.dart';
49 import 'package:cw_core/unspent_coin_type.dart';
28 -import 'package:cake_wallet/view_model/send/output.dart';
29 -import 'package:cake_wallet/view_model/send/send_template_view_model.dart';
50 +import 'package:cw_core/utils/print_verbose.dart';
51 +import 'package:cw_core/wallet_type.dart';
52 import 'package:flutter/material.dart';
53 import 'package:hive/hive.dart';
54 import 'package:mobx/mobx.dart';
33 -import 'package:cake_wallet/entities/template.dart';
34 -import 'package:cake_wallet/core/address_validator.dart';
35 -import 'package:cake_wallet/core/amount_validator.dart';
36 -import 'package:cw_core/pending_transaction.dart';
37 -import 'package:cake_wallet/core/validator.dart';
38 -import 'package:cake_wallet/core/execution_state.dart';
39 -import 'package:cake_wallet/monero/monero.dart';
40 -import 'package:cw_core/sync_status.dart';
41 -import 'package:cw_core/crypto_currency.dart';
42 -import 'package:cake_wallet/entities/fiat_currency.dart';
43 -import 'package:cake_wallet/entities/calculate_fiat_amount.dart';
44 -import 'package:cw_core/wallet_type.dart';
45 -import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
46 -import 'package:cake_wallet/store/settings_store.dart';
47 -import 'package:cake_wallet/view_model/send/send_view_model_state.dart';
48 -import 'package:cake_wallet/entities/parsed_address.dart';
49 -import 'package:cake_wallet/bitcoin/bitcoin.dart';
50 -import 'package:cake_wallet/generated/i18n.dart';
55
56 part 'send_view_model.g.dart';
57
@@ -390,15 +394,62 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
394 return conditionsList.contains(true);
395 }
396
397 + final _ocpService = OpenCryptoPayService();
398 +
399 + @observable
400 + OpenCryptoPayRequest? ocpRequest;
401 +
402 + @action
403 + Future<void> dismissTransaction() async {
404 + state = InitialExecutionState();
405 + if (ocpRequest != null) {
406 + clearOutputs();
407 + _ocpService.cancelOpenCryptoPayRequest(ocpRequest!);
408 + ocpRequest = null;
409 + }
410 + }
411 +
412 + @action
413 + Future<PendingTransaction?> createOpenCryptoPayTransaction(String uri) async {
414 + state = IsExecutingState();
415 +
416 + try {
417 + final originalOCPRequest =
418 + await _ocpService.getOpenCryptoPayInvoice(uri.toString());
419 + final paymentUri = await _ocpService.getOpenCryptoPayAddress(
420 + originalOCPRequest,
421 + selectedCryptoCurrency,
422 + );
423 +
424 + ocpRequest = originalOCPRequest;
425 +
426 + final paymentRequest = PaymentRequest.fromUri(paymentUri);
427 + clearOutputs();
428 +
429 + outputs.first.address = paymentRequest.address;
430 + outputs.first.parsedAddress = ParsedAddress(
431 + addresses: [paymentRequest.address], name: ocpRequest!.receiverName);
432 + outputs.first.setCryptoAmount(paymentRequest.amount);
433 + outputs.first.note = ocpRequest!.receiverName;
434 +
435 + return createTransaction();
436 + } catch (e) {
437 + printV(e);
438 + state = FailureState(translateErrorMessage(e, walletType, currency));
439 + return null;
440 + }
441 + }
442 +
443 @action
394 - Future<PendingTransaction?> createTransaction({ExchangeProvider? provider}) async {
444 + Future<PendingTransaction?> createTransaction(
445 + {ExchangeProvider? provider}) async {
446 try {
396 - if (wallet.isHardwareWallet)
397 - state = IsAwaitingDeviceResponseState();
398 - else
399 - state = IsExecutingState();
447 + if (!(state is IsExecutingState)) state = IsExecutingState();
448 +
449 + if (wallet.isHardwareWallet) state = IsAwaitingDeviceResponseState();
450
401 - pendingTransaction = await wallet.createTransaction(_credentials(provider));
451 + pendingTransaction =
452 + await wallet.createTransaction(_credentials(provider));
453
454 if (provider is ThorChainExchangeProvider) {
455 final outputCount = pendingTransaction?.outputCount ?? 0;
@@ -412,7 +463,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
463 }
464
465 if (wallet.type == WalletType.bitcoin) {
415 - final updatedOutputs = bitcoin!.updateOutputs(pendingTransaction!, outputs);
466 + final updatedOutputs =
467 + bitcoin!.updateOutputs(pendingTransaction!, outputs);
468
469 if (outputs.length == updatedOutputs.length) {
470 outputs.replaceRange(0, outputs.length, updatedOutputs);
@@ -473,7 +525,26 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
525 @action
526 Future<void> commitTransaction(BuildContext context) async {
527 if (pendingTransaction == null) {
476 - throw Exception("Pending transaction doesn't exist. It should not be happened.");
528 + throw Exception(
529 + "Pending transaction doesn't exist. It should not be happened.");
530 + }
531 +
532 + if (ocpRequest != null) {
533 + state = TransactionCommitting();
534 + if (selectedCryptoCurrency == CryptoCurrency.xmr) {
535 + await pendingTransaction!.commit();
536 + }
537 +
538 + await _ocpService.commitOpenCryptoPayRequest(
539 + pendingTransaction!.hex,
540 + txId: pendingTransaction!.id,
541 + request: ocpRequest!,
542 + asset: selectedCryptoCurrency,
543 + );
544 +
545 + state = TransactionCommitted();
546 +
547 + return;
548 }
549
550 String address = outputs.fold('', (acc, value) {
@@ -493,8 +564,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
564
565 if (pendingTransaction!.shouldCommitUR()) {
566 final urstr = await pendingTransaction!.commitUR();
496 - final result =
497 - await Navigator.of(context).pushNamed(Routes.urqrAnimatedPage, arguments: urstr);
567 + final result = await Navigator.of(context)
568 + .pushNamed(Routes.urqrAnimatedPage, arguments: urstr);
569 if (result == null) {
570 state = FailureState("Canceled by user");
571 return;
@@ -508,12 +579,18 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
579 }
580
581 if (pendingTransaction!.id.isNotEmpty) {
511 - final descriptionKey = '${pendingTransaction!.id}_${wallet.walletAddresses.primaryAddress}';
582 + final descriptionKey =
583 + '${pendingTransaction!.id}_${wallet.walletAddresses.primaryAddress}';
584 _settingsStore.shouldSaveRecipientAddress
585 ? await transactionDescriptionBox.add(TransactionDescription(
514 - id: descriptionKey, recipientAddress: address, transactionNote: note))
515 - : await transactionDescriptionBox
516 - .add(TransactionDescription(id: descriptionKey, transactionNote: note));
586 + id: descriptionKey,
587 + recipientAddress: address,
588 + transactionNote: note,
589 + ))
590 + : await transactionDescriptionBox.add(TransactionDescription(
591 + id: descriptionKey,
592 + transactionNote: note,
593 + ));
594 }
595
596 state = TransactionCommitted();
@@ -552,12 +629,10 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
629 );
630
631 case WalletType.monero:
555 - return monero!
556 - .createMoneroTransactionCreationCredentials(outputs: outputs, priority: priority!);
632 + return monero!.createMoneroTransactionCreationCredentials(outputs: outputs, priority: priority!);
633
634 case WalletType.wownero:
559 - return wownero!
560 - .createWowneroTransactionCreationCredentials(outputs: outputs, priority: priority!);
635 + return wownero!.createWowneroTransactionCreationCredentials(outputs: outputs, priority: priority!);
636
637 case WalletType.ethereum:
638 return ethereum!.createEthereumTransactionCredentials(outputs,
@@ -568,8 +643,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
643 return polygon!.createPolygonTransactionCredentials(outputs,
644 priority: priority!, currency: selectedCryptoCurrency);
645 case WalletType.solana:
571 - return solana!
572 - .createSolanaTransactionCredentials(outputs, currency: selectedCryptoCurrency);
646 + return solana!.createSolanaTransactionCredentials(outputs, currency: selectedCryptoCurrency);
647 case WalletType.tron:
648 return tron!.createTronTransactionCredentials(outputs, currency: selectedCryptoCurrency);
649 case WalletType.zano:
@@ -603,8 +677,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
677
678 ContactRecord? newContactAddress() {
679 final Set<String> contactAddresses =
606 - Set.from(contactListViewModel.contacts.map((contact) => contact.address))
607 - ..addAll(contactListViewModel.walletContacts.map((contact) => contact.address));
680 + Set.from(contactListViewModel.contacts.map((contact) => contact.address))
681 + ..addAll(contactListViewModel.walletContacts.map((contact) => contact.address));
682
683 for (var output in outputs) {
684 String address;
res/values/strings_de.arb
+1 -1
@@ -697,7 +697,7 @@
697 "send": "Senden",
698 "send_address": "${cryptoCurrency}-Adresse",
699 "send_amount": "Betrag:",
700 - "send_change_to_you": "Verändere dich zu dir:",
700 + "send_change_to_you": "Rückgeld:",
701 "send_creating_transaction": "Erstelle Transaktion",
702 "send_error_currency": "Die Währung darf nur Zahlen enthalten",
703 "send_error_minimum_value": "Der Mindestbetrag ist 0,01",