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",