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:
Konstantin Ullrich 2025-04-10 04:03:06 +02:00 committed by GitHub
parent 2b020c0c1b
commit c1e9668b1e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 458 additions and 69 deletions

View file

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

View file

@ -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,
);

View 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";
}

View 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;
}

View 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;
}

View 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);
}

View file

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

View file

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

View file

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

View file

@ -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;

View file

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