From 5099fa3b9cf5911c5e4cc49e045ca10e2cc19e75 Mon Sep 17 00:00:00 2001 From: malik1004x Date: Thu, 4 Jun 2026 11:32:19 +0200 Subject: [PATCH] tx-confirmation-options-on-dev [cw-1463] (#3289) * add note and contact options to tx confirmation screen * fix closing animation for confirm sheet * fix note button for lightning * sizing fix * add local tx info for evm/sol/trx * fix for xmr/zec * merge * fixes --- analysis_options.yaml | 13 -- .../lib/lightning/lightning_wallet.dart | 5 +- .../pending_lightning_transaction.dart | 6 +- cw_evm/lib/evm_chain_wallet.dart | 4 + cw_tron/lib/pending_tron_transaction.dart | 3 +- cw_tron/lib/tron_client.dart | 1 + lib/di.dart | 10 +- lib/evm/cw_evm.dart | 40 ++++ lib/new-ui/pages/send_page.dart | 2 +- .../transaction_details_modal.dart | 7 +- .../widgets/send_page/send_confirm_sheet.dart | 218 +++++++++++++++--- lib/solana/cw_solana.dart | 29 +++ lib/tron/cw_tron.dart | 28 +++ .../dashboard/dashboard_view_model.dart | 23 +- .../dashboard/transaction_list_item.dart | 2 +- lib/view_model/send/send_view_model.dart | 63 +++++ .../transaction_details_view_model.dart | 2 + res/pictures/add_note.svg | 3 + res/pictures/birthday_cake.svg | 1 + res/pictures/save_contact.svg | 3 + res/values/strings_en.arb | 1 + tool/configure.dart | 59 +++++ 22 files changed, 458 insertions(+), 65 deletions(-) create mode 100644 res/pictures/add_note.svg create mode 100644 res/pictures/birthday_cake.svg create mode 100644 res/pictures/save_contact.svg diff --git a/analysis_options.yaml b/analysis_options.yaml index 7f76d2721..98dfe6ae4 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -13,19 +13,6 @@ analyzer: cw_monero/ios/External/**, cw_shared_external/**, shared_external/**, - lib/bitcoin/cw_bitcoin.dart, - lib/bitcoin_cash/cw_bitcoin_cash.dart, - lib/ethereum/cw_ethereum.dart, - lib/haven/cw_haven.dart, - lib/monero/cw_monero.dart, - lib/nano/cw_nano.dart, - lib/polygon/cw_polygon.dart, - lib/solana/cw_solana.dart, - lib/tron/cw_tron.dart, - lib/wownero/cw_wownero.dart, - lib/zano/cw_zano.dart, - lib/base/cw_base.dart, - lib/arbitrum/cw_arbitrum.dart, ] language: strict-casts: true diff --git a/cw_bitcoin/lib/lightning/lightning_wallet.dart b/cw_bitcoin/lib/lightning/lightning_wallet.dart index deef7204b..31ba4b60a 100644 --- a/cw_bitcoin/lib/lightning/lightning_wallet.dart +++ b/cw_bitcoin/lib/lightning/lightning_wallet.dart @@ -198,6 +198,7 @@ class LightningWallet { final res = await sdk.sendPayment( request: SendPaymentRequest(prepareResponse: prepareResponse)); printV(res.payment.status.name); + return res.payment.id; } on SdkError_SparkError catch (e) { if (e.field0.contains("AlreadyExists")) { throw Exception("Invoice already paid"); @@ -239,6 +240,7 @@ class LightningWallet { final res = await sdk.lnurlPay(request: LnurlPayRequest(prepareResponse: prepareResponse)); printV(res.payment.status.name); + return res.payment.id; }, ); } else if (inputType is InputType_BitcoinAddress) { @@ -278,8 +280,9 @@ class LightningWallet { commitOverride: () async { final options = SendPaymentOptions.bitcoinAddress(confirmationSpeed: onchainConfirmationSpeed); - await sdk.sendPayment( + final res = await sdk.sendPayment( request: SendPaymentRequest(prepareResponse: prepareResponse, options: options)); + return res.payment.id; }, ); } diff --git a/cw_bitcoin/lib/lightning/pending_lightning_transaction.dart b/cw_bitcoin/lib/lightning/pending_lightning_transaction.dart index cb75b7d2b..d389949f9 100644 --- a/cw_bitcoin/lib/lightning/pending_lightning_transaction.dart +++ b/cw_bitcoin/lib/lightning/pending_lightning_transaction.dart @@ -13,11 +13,11 @@ class PendingLightningTransaction with PendingTransaction { final int amount; final int fee; final bool isSendAll; - Future Function() commitOverride; + Future Function() commitOverride; final List _listeners =[]; @override - final String id; + String id; @override String get hex => ""; @@ -36,7 +36,7 @@ class PendingLightningTransaction with PendingTransaction { @override Future commit() async { - await commitOverride.call(); + id = await commitOverride.call(); _listeners.forEach((e) => e.call()); } diff --git a/cw_evm/lib/evm_chain_wallet.dart b/cw_evm/lib/evm_chain_wallet.dart index e2e97a356..66ec7577c 100644 --- a/cw_evm/lib/evm_chain_wallet.dart +++ b/cw_evm/lib/evm_chain_wallet.dart @@ -1289,6 +1289,10 @@ abstract class EVMChainWalletBase !_hasEvmTokenContractAddress(existingTxInfo)) { result[transactionModel.hash] = newTxInfo; } + + else if (existingTxInfo.isPending) { + result[transactionModel.hash] = newTxInfo; + } } return result; diff --git a/cw_tron/lib/pending_tron_transaction.dart b/cw_tron/lib/pending_tron_transaction.dart index b1d601f8e..5a1afc527 100644 --- a/cw_tron/lib/pending_tron_transaction.dart +++ b/cw_tron/lib/pending_tron_transaction.dart @@ -14,6 +14,7 @@ class PendingTronTransaction with PendingTransaction { required this.signedTransaction, required this.fee, required this.amount, + required this.id, }); @override @@ -32,7 +33,7 @@ class PendingTronTransaction with PendingTransaction { String get hex => bytesToHex(signedTransaction); @override - String get id => ''; + String id; @override Future> commitUR() { diff --git a/cw_tron/lib/tron_client.dart b/cw_tron/lib/tron_client.dart index 60431674f..18d797655 100644 --- a/cw_tron/lib/tron_client.dart +++ b/cw_tron/lib/tron_client.dart @@ -341,6 +341,7 @@ class TronClient { amount: totalAmount, fee: TronHelper.fromSun(rawTransaction.feeLimit ?? BigInt.zero), sendTransaction: sendTx, + id: rawTransaction.txID ); } diff --git a/lib/di.dart b/lib/di.dart index cd7ee62d1..0043c4e04 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -1417,10 +1417,12 @@ Future setup({ } ); - getIt.registerFactoryParam( - (transactionInfo, _) => TransactionDetailsModal(transactionDetailsViewModel: getIt.get( - param1: [transactionInfo, false])) - ); + getIt.registerFactoryParam( + (transactionInfo, highlightNoteField) => TransactionDetailsModal( + transactionDetailsViewModel: + getIt.get(param1: [transactionInfo, false]), + highlightNoteField: highlightNoteField ?? false, + )); getIt.registerFactoryParam( (TransactionInfo transactionInfo, _) => TransactionDetailsPage( diff --git a/lib/evm/cw_evm.dart b/lib/evm/cw_evm.dart index 7dce83c7b..44ed4bfae 100644 --- a/lib/evm/cw_evm.dart +++ b/lib/evm/cw_evm.dart @@ -143,6 +143,39 @@ class CWEVM extends EVM { ); } + @override + TransactionInfo getTransactionInfo( + + { + required String id, + required int height, + required BigInt ethAmount, + required BigInt ethFee, + required String tokenSymbol, + int exponent = 18, + required TransactionDirection direction, + required bool isPending, + required DateTime date, + required int confirmations, + String? to, + String? from, + String? evmSignatureName, + String? contractAddress, + required int chainId, + }) => + EVMChainTransactionInfo(id: id, + height: height, + ethAmount: ethAmount, + ethFee: ethFee, + tokenSymbol: tokenSymbol, + direction: direction, + isPending: isPending, + date: date, + confirmations: confirmations, + to: to, + from: from, + chainId: chainId); + @override int formatterEVMParseAmount(String amount) => EVMChainFormatter.parseEVMChainAmount(amount); @@ -432,6 +465,13 @@ class CWEVM extends EVM { return null; } + + @override + BigInt getPendingTransactionFee(PendingTransaction tx) => (tx as PendingEVMChainTransaction).fee; + + @override + String getPendingTransactionAmount(PendingTransaction tx) => (tx as PendingEVMChainTransaction).amount; + // Registry helper methods static final EvmChainRegistry _registry = EvmChainRegistry(); diff --git a/lib/new-ui/pages/send_page.dart b/lib/new-ui/pages/send_page.dart index 2511d66c0..af09b68ab 100644 --- a/lib/new-ui/pages/send_page.dart +++ b/lib/new-ui/pages/send_page.dart @@ -1315,7 +1315,7 @@ class _NewSendPageState extends State { // height: MediaQuery.of(context).size.height*0.4, child: ModalNavigator( parentContext: modalContext, - heightMode: ModalHeightModes.autoLock, + heightMode: ModalHeightModes.natural, rootPage: Material( child: NewPicker( title: S.of(context).set_fees, diff --git a/lib/new-ui/widgets/coins_page/assets_history/transaction_details_modal.dart b/lib/new-ui/widgets/coins_page/assets_history/transaction_details_modal.dart index 6c959c20b..aa9b522a5 100644 --- a/lib/new-ui/widgets/coins_page/assets_history/transaction_details_modal.dart +++ b/lib/new-ui/widgets/coins_page/assets_history/transaction_details_modal.dart @@ -16,9 +16,10 @@ import 'package:flutter/services.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; class TransactionDetailsModal extends StatefulWidget { - const TransactionDetailsModal({super.key, required this.transactionDetailsViewModel}); + const TransactionDetailsModal({super.key, required this.transactionDetailsViewModel, this.highlightNoteField = false}); final TransactionDetailsViewModel transactionDetailsViewModel; + final bool highlightNoteField; @override State createState() => _TransactionDetailsModalState(); @@ -38,6 +39,10 @@ class _TransactionDetailsModalState extends State { widget.transactionDetailsViewModel.updateNote(noteController.text); } }); + + if(widget.highlightNoteField) { + noteFocusNode.requestFocus(); + } } @override diff --git a/lib/new-ui/widgets/send_page/send_confirm_sheet.dart b/lib/new-ui/widgets/send_page/send_confirm_sheet.dart index 65e6f8d6c..0b3a867d6 100644 --- a/lib/new-ui/widgets/send_page/send_confirm_sheet.dart +++ b/lib/new-ui/widgets/send_page/send_confirm_sheet.dart @@ -2,17 +2,25 @@ import 'dart:io'; import 'package:cake_wallet/core/address_validator.dart'; import 'package:cake_wallet/core/utilities.dart'; +import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/entities/contact.dart'; +import 'package:cake_wallet/entities/contact_record.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/new-ui/widgets/animated_dropdown.dart'; +import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/transaction_details_modal.dart'; +import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart'; import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; import 'package:cake_wallet/new-ui/widgets/send_page/send_confirm_bottom_widget.dart'; +import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; import 'package:cake_wallet/utils/address_formatter.dart'; import 'package:cake_wallet/view_model/send/send_view_model.dart'; import 'package:cake_wallet/view_model/send/send_view_model_state.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/crypto_amount_format.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -31,12 +39,14 @@ class SendConfirmSheet extends StatefulWidget { } class _SendConfirmSheetState extends State { + bool _committed = false; + void initState() { super.initState(); - reaction((context) => widget.sendViewModel.state, (state) { - if (state is TransactionCommitted) { - Future.delayed(const Duration(seconds: 2), () { - if (mounted) Navigator.of(context).maybePop(); + reaction((_)=>widget.sendViewModel.state, (state){ + if(state is TransactionCommitted) { + setState(() { + _committed = true; }); } }); @@ -60,33 +70,48 @@ class _SendConfirmSheetState extends State { borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), child: SafeArea( - child: Observer( + child: Observer( builder: (_) { - final commited = widget.sendViewModel.state is TransactionCommitted; - return Stack( - fit: StackFit.loose, - children: [ - Positioned.fill( + return AnimatedSize( + duration: const Duration(milliseconds: 300), + curve: Curves.easeOutCubic, + alignment: Alignment.topCenter, + clipBehavior: Clip.hardEdge, + child: Stack( + clipBehavior: Clip.none, + children: [ + Align( + alignment: Alignment.topCenter, + heightFactor: _committed ? 0.0 : 1.0, child: AnimatedSlide( - offset: commited ? Offset.zero : const Offset(1, 0), - duration: const Duration(milliseconds: 300), - curve: Curves.easeOutCubic, - child: const TransactionCommitedScreen(), - )), - AnimatedSlide( - offset: commited ? const Offset(-1, 0) : Offset.zero, - duration: const Duration(milliseconds: 300), - curve: Curves.easeOutCubic, - child: SendTransactionDetails( - sendViewModel: widget.sendViewModel, - isPage: widget.isPage, - title: widget.title, - iconPath: widget.iconPath), - ), - ], + offset: _committed ? const Offset(-1, 0) : Offset.zero, + duration: const Duration(milliseconds: 300), + curve: Curves.easeOutCubic, + child: SendTransactionDetails( + sendViewModel: widget.sendViewModel, + isPage: widget.isPage, + title: widget.title, + iconPath: widget.iconPath, + ), + ), + ), + Align( + alignment: Alignment.topCenter, + heightFactor: _committed ? 1.0 : 0.0, + child: AnimatedSlide( + offset: _committed ? Offset.zero : const Offset(1, 0), + duration: const Duration(milliseconds: 300), + curve: Curves.easeOutCubic, + child: TransactionCommitedScreen( + sendViewModel: widget.sendViewModel, + ), + ), + ), + ], + ), ); }, - ), + ) ), ), ), @@ -178,7 +203,7 @@ class SendTransactionDetails extends StatelessWidget { }), sendViewModel.selectedCryptoCurrency) : sendViewModel.amountParsingProxy.getDisplayCryptoAmount( - formatAmount(transaction.amountFormatted), sendViewModel.selectedCryptoCurrency); + formatAmount(transaction.amountFormatted), sendViewModel.selectedCryptoCurrency).withMaxDecimals(8); final fee = "${(transaction == null) ? sendViewModel.amountParsingProxy.getDisplayCryptoStringFromBigInt(sumByBigInt( @@ -404,29 +429,150 @@ class SendTransactionDetails extends StatelessWidget { } } -class TransactionCommitedScreen extends StatelessWidget { - const TransactionCommitedScreen({super.key}); +class TransactionCommitedScreen extends StatefulWidget { + const TransactionCommitedScreen({super.key, this.sendViewModel}); + + final SendViewModel? sendViewModel; + + @override + State createState() => _TransactionCommitedScreenState(); +} + +class _TransactionCommitedScreenState extends State { + bool _isNoteButtonLoading = false; @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 24.0), - child: Column( + return Observer( + builder: (_) => Column( spacing: 12, - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ + SizedBox(height: 12,), Text( S.of(context).transaction_sent_new, style: TextStyle(fontSize: 24, fontWeight: FontWeight.w600), ), - Image.asset(width: 256, height: 256, "assets/images/birthday_cake.png") + SizedBox(), + CakeImageWidget(width: 200, height: 200, imageUrl: "assets/new-ui/birthday_cake.svg"), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Column( + spacing: 12, + children: [ + if (widget.sendViewModel != null) + Row( + spacing: 8, + children: [ + if (!(widget.sendViewModel!.checkIfAddressIsAContact( + widget.sendViewModel!.outputs.first.address)) && + !(widget.sendViewModel!.outputs.first.isParsedAddress)) + TransactionCommittedScreenActionButton( + text: S.of(context).save_contact, + iconPath: "assets/new-ui/save_contact.svg", + onTap: () { + Navigator.of(context).pushNamed(Routes.addressBookAddContact, + arguments: ContactRecord( + CakeHive.box(Contact.boxName), + Contact( + name: "", + address: widget.sendViewModel!.outputs.first.address, + type: widget.sendViewModel!.wallet.currency))); + }), + // lightning has to be hacked in here as it doesn't get added to tx history for a few secs after committing. + if (widget.sendViewModel!.transactionInfo != null || + widget.sendViewModel!.currency == CryptoCurrency.btcln) + TransactionCommittedScreenActionButton( + text: S.of(context).add_a_note, + iconPath: "assets/new-ui/add_note.svg", + isLoading: _isNoteButtonLoading, + onTap: () async { + setState(() { + _isNoteButtonLoading = true; + }); + + // for ln, we want to show the button and just have it wait until it appears in tx history + // for other currs this is instant + await asyncWhen((_) => widget.sendViewModel!.transactionInfo != null); + + setState(() { + _isNoteButtonLoading = false; + }); + + final page = getIt.get( + param1: widget.sendViewModel!.transactionInfo!, param2: true); + showModalBottomSheet( + isScrollControlled: true, + context: context, + builder: (context) => page); + }), + ], + ), + NewPrimaryButton( + onPressed: Navigator.of(context).maybePop, + text: S.of(context).done, + color: Theme.of(context).colorScheme.primary, + textColor: Theme.of(context).colorScheme.onPrimary), + SizedBox(height: 12,) + ], + ), + ), ], ), ); } } +class TransactionCommittedScreenActionButton extends StatelessWidget { + const TransactionCommittedScreenActionButton( + {super.key, + required this.text, + required this.iconPath, + required this.onTap, + this.isLoading = false}); + + final String text; + final String iconPath; + final VoidCallback onTap; + final bool isLoading; + + @override + Widget build(BuildContext context) { + return Flexible( + child: GestureDetector( + onTap: onTap, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + color: Theme.of(context).colorScheme.surfaceContainer), + child: Padding( + padding: EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 10, + children: [ + isLoading + ? CupertinoActivityIndicator() + : CakeImageWidget( + imageUrl: iconPath, + width: 24, + height: 24, + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.primary, BlendMode.srcIn), + ), + Text( + text, + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.w500), + ) + ], + ), + ), + ))); + } +} class MultiSendAddressPreview extends StatefulWidget { const MultiSendAddressPreview( diff --git a/lib/solana/cw_solana.dart b/lib/solana/cw_solana.dart index 76b2591be..7ca41a2c6 100644 --- a/lib/solana/cw_solana.dart +++ b/lib/solana/cw_solana.dart @@ -409,4 +409,33 @@ class CWSolana extends Solana { await Future.wait(tokenChecks); } catch (_) {} } + + @override + TransactionInfo getTransactionInfo({ + required String id, + required DateTime blockTime, + required String to, + required String from, + String? tokenSymbol, + required TransactionDirection direction, + required double solAmount, + required bool isPending, + required double txFee, + }) => + SolanaTransactionInfo( + id: id, + blockTime: blockTime, + to: to, + from: from, + tokenSymbol: tokenSymbol ?? "SOL", + direction: direction, + solAmount: solAmount, + isPending: isPending, + txFee: txFee); + + @override + double getPendingTransactionAmount(PendingTransaction tx) => (tx as PendingSolanaTransaction).amount; + + @override + double getPendingTransactionFee(PendingTransaction tx) => (tx as PendingSolanaTransaction).fee; } diff --git a/lib/tron/cw_tron.dart b/lib/tron/cw_tron.dart index 0b6dd8cba..42f22db31 100644 --- a/lib/tron/cw_tron.dart +++ b/lib/tron/cw_tron.dart @@ -153,4 +153,32 @@ class CWTron extends Tron { return tronWallet.tronTokenCurrencies .any((element) => element.contractAddress == contractAddress); } + + @override + TransactionInfo getTransactionInfo({ + required String id, + required BigInt tronAmount, + int? txFee, + String? tokenSymbol, + required TransactionDirection direction, + required DateTime blockTime, + String? to, + String? from, + required bool isPending, + }) => + TronTransactionInfo(id: id, + tronAmount: tronAmount, + tokenSymbol: tokenSymbol ?? "TRX", + txFee: txFee, + direction: direction, + blockTime: blockTime, + to: to, + from: from, + isPending: isPending); + + @override + String getPendingTransactionAmount(PendingTransaction tx) => (tx as PendingTronTransaction).amount; + + @override + String getPendingTransactionFee(PendingTransaction tx) => (tx as PendingTronTransaction).amount; } diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 8b249c8cf..d97d119a4 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -326,6 +326,10 @@ abstract class DashboardViewModelBase with Store { appStore.wallet!.transactionHistory.transactions.values.last.confirmations + 1; } catch (_) {} + } else { + confirmations = appStore.wallet!.transactionHistory.transactions.values + .map((item) => item.isPending) + .fold(0, (val, pending) => pending ? val + 1 : val); } return length * confirmations; }, _transactionDisposerCallback, delay: 300); @@ -490,16 +494,23 @@ abstract class DashboardViewModelBase with Store { } // printV("Transaction disposer callback (relevantTxs: ${relevantTxs.length} current: ${transactions.length})"); + // TODO(malik) update this in a saner way during the vm refactor String _txIdentityString(String txHash, TransactionDirection direction) => "${txHash}_$direction"; - String _txIdentityStringConfirmations(String txHash, TransactionDirection direction, int confirmations) => "${txHash}_${direction}_$confirmations"; - + String _txIdentityStringConfirmations( + String txHash, TransactionDirection direction, int confirmations, bool isPending) => + "${txHash}_${direction}_${confirmations}_$isPending"; final existingKeys = transactions - .map((item) => _txIdentityStringConfirmations(item.transaction.txHash, item.transaction.direction, item.transaction.confirmations)) + .map((item) => _txIdentityStringConfirmations( + item.transaction.txHash, + item.transaction.direction, + item.transaction.confirmations, + item.transaction.isPending)) .toSet(); final newTransactions = relevantTxs - .where((tx) => !existingKeys.contains(_txIdentityStringConfirmations(tx.txHash, tx.direction, tx.confirmations))) + .where((tx) => !existingKeys.contains(_txIdentityStringConfirmations( + tx.txHash, tx.direction, tx.confirmations, tx.isPending))) .map((tx) => TransactionListItem( transaction: tx, balanceViewModel: balanceViewModel, @@ -1291,6 +1302,10 @@ abstract class DashboardViewModelBase with Store { appStore.wallet!.transactionHistory.transactions.values.last.confirmations + 1; } catch (_) {} + } else { + confirmations = appStore.wallet!.transactionHistory.transactions.values + .map((item) => item.isPending) + .fold(0, (val, pending) => pending ? val + 1 : val); } return length * confirmations; }, _transactionDisposerCallback, delay: 300); diff --git a/lib/view_model/dashboard/transaction_list_item.dart b/lib/view_model/dashboard/transaction_list_item.dart index 1fb3f3043..4cbd27633 100644 --- a/lib/view_model/dashboard/transaction_list_item.dart +++ b/lib/view_model/dashboard/transaction_list_item.dart @@ -218,7 +218,7 @@ class TransactionListItem extends ActionListItem with Keyable { case WalletType.base: case WalletType.arbitrum: case WalletType.bsc: - final asset = evm!.assetOfTransaction(balanceViewModel.wallet, transaction); + final asset = assetOfTransaction; final price = balanceViewModel.fiatConversionStore.prices[asset]; amount = calculateFiatAmountRaw( cryptoAmount: evm!.formatterEVMAmountToDouble(transaction: transaction), diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index a51423ec5..6c001c5b6 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -8,6 +8,7 @@ import 'package:cake_wallet/core/execution_state.dart'; import 'package:cake_wallet/core/open_crypto_pay/exceptions.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/utilities.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'; @@ -59,6 +60,7 @@ import 'package:cw_core/exceptions.dart'; import 'package:cw_core/lnurl.dart'; import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/sync_status.dart'; +import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/unspent_coin_type.dart'; @@ -488,6 +490,25 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor bool get shouldDisplayTOTP2FAForSendsToInternalWallet => _settingsStore.shouldRequireTOTP2FAForSendsToInternalWallets; + @computed + TransactionInfo? get transactionInfo { + if(isEVMCompatibleChain(walletType)) { + return wallet.transactionHistory.transactions[pendingTransaction?.evmTxHashFromRawHex]; + } + if (walletType == WalletType.monero) { + // monero tx history keys are in format txhash_amount_accountindex_addressindex + return wallet.transactionHistory.transactions[wallet.transactionHistory.transactions.keys + .firstWhereOrNull((item) => item.split("_").first == pendingTransaction?.id)]; + } + if (walletType == WalletType.zcash) { + // zcash has a txHash getter, but the pendingTransaction id contains quotation marks for whatever reason? + final txIdNoQuotes = pendingTransaction?.id.replaceAll("\"", ""); + return wallet.transactionHistory.transactions.values + .firstWhereOrNull((item) => item.txHash == txIdNoQuotes); + } + return wallet.transactionHistory.transactions[pendingTransaction?.id]; + } + //* Still open to further optimize these checks //* It works but can be made better @action @@ -1078,6 +1099,48 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor }); } + // FIXME(malik) ideally, this should be done wallet-side. + // it is required because evm, solana and tron don't actually save the transaction info when you send something. + // instead, they rely on the tx to eventually get fetched at sync time, which can take a while + if(isEVMWallet) { + wallet.transactionHistory.addOne(evm!.getTransactionInfo( + id: pendingTransaction!.evmTxHashFromRawHex!, + height: 0, + // FIXME(malik) this is even more critical given all the issues we had with decimal point parsing. with money it'll be easy + ethAmount: selectedCryptoCurrency.parseAmount(outputs.first.cryptoAmount), + ethFee: CryptoCurrency.eth.parseAmount(pendingTransaction!.feeFormattedValue), + tokenSymbol: selectedCryptoCurrency.title, + direction: TransactionDirection.outgoing, + isPending: true, + date: DateTime.now(), + confirmations: 0, + chainId: wallet.chainId ?? 0)); + } + + if(walletType == WalletType.solana) { + wallet.transactionHistory.addOne(solana!.getTransactionInfo( + id: pendingTransaction!.id, + blockTime: DateTime.now(), + tokenSymbol: selectedCryptoCurrency.title, + to: "", + from: "", + direction: TransactionDirection.outgoing, + solAmount: solana!.getPendingTransactionAmount(pendingTransaction!), + isPending: true, + txFee: solana!.getPendingTransactionFee(pendingTransaction!))); + } + + if (walletType == WalletType.tron) { + wallet.transactionHistory.addOne(tron!.getTransactionInfo( + id: pendingTransaction!.id, + blockTime: DateTime.now(), + tokenSymbol: selectedCryptoCurrency.title, + direction: TransactionDirection.outgoing, + tronAmount: selectedCryptoCurrency.parseAmount(outputs.first.cryptoAmount), + isPending: true, + txFee: CryptoCurrency.trx.parseAmount(outputs.first.estimatedFee).toInt())); + } + if (pendingTransaction!.id.isNotEmpty) { _addTransactionDescription(); } diff --git a/lib/view_model/transaction_details_view_model.dart b/lib/view_model/transaction_details_view_model.dart index 1032ac90a..6a38a45bb 100644 --- a/lib/view_model/transaction_details_view_model.dart +++ b/lib/view_model/transaction_details_view_model.dart @@ -127,6 +127,7 @@ class TxDetailRowDefinition { .firstOrNull ?? ""; case WalletType.tron: + if(vm.transactionInfo.to != null) ret = tron!.getTronBase58Address(vm.transactionInfo.to!, vm.wallet); default: break; @@ -315,6 +316,7 @@ abstract class TransactionDetailsViewModelBase with Store { TransactionPriority? transactionPriority; CryptoCurrency get transactionAsset { + if (isEVMCompatibleChain(wallet.type)) { return evm!.assetOfTransaction(wallet, transactionInfo); } diff --git a/res/pictures/add_note.svg b/res/pictures/add_note.svg new file mode 100644 index 000000000..99fd2b2b8 --- /dev/null +++ b/res/pictures/add_note.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/pictures/birthday_cake.svg b/res/pictures/birthday_cake.svg new file mode 100644 index 000000000..0c11098b5 --- /dev/null +++ b/res/pictures/birthday_cake.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/res/pictures/save_contact.svg b/res/pictures/save_contact.svg new file mode 100644 index 000000000..b474ef5b3 --- /dev/null +++ b/res/pictures/save_contact.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index d6e4d09dd..190093452 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -910,6 +910,7 @@ "save": "Save", "save_backup_password": "Please make sure that you have saved your backup password. You will not be able to import your backup files without it.", "save_backup_password_alert": "Save backup password", + "save_contact": "Save Contact", "save_to_downloads": "Save to Downloads", "saved_the_trade_id": "I've saved the trade ID", "savings": "Savings", diff --git a/tool/configure.dart b/tool/configure.dart index 6a5385f86..a16d88f21 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -938,6 +938,7 @@ import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/spl_token.dart'; +import 'package:cw_core/transaction_direction.dart'; """; const solanaCWHeaders = """ @@ -1033,6 +1034,23 @@ abstract class Solana { }); Future discoverAndAddWalletTokens(WalletBase wallet); + + TransactionInfo getTransactionInfo( +{ + required String id, + required DateTime blockTime, + required String to, + required String from, + String? tokenSymbol, + required TransactionDirection direction, + required double solAmount, + required bool isPending, + required double txFee, + } + ); + + double getPendingTransactionAmount(PendingTransaction tx); + double getPendingTransactionFee(PendingTransaction tx); } class JupiterSwapFailedException implements Exception { @@ -1076,6 +1094,7 @@ Future generateTron(bool hasImplementation) async { const tronCommonHeaders = """ import 'package:cake_wallet/view_model/send/output.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/output_info.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/wallet_base.dart'; @@ -1084,6 +1103,7 @@ import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/tron_token.dart'; import 'package:hive/hive.dart'; +import 'package:cw_core/transaction_direction.dart'; """; const tronCWHeaders = """ @@ -1091,6 +1111,7 @@ import 'package:cw_evm/evm_chain_mnemonics.dart'; import 'package:cw_tron/tron_transaction_credentials.dart'; import 'package:cw_tron/tron_transaction_info.dart'; import 'package:cw_tron/tron_wallet_creation_credentials.dart'; +import 'package:cw_tron/pending_tron_transaction.dart'; import 'package:cw_tron/tron_client.dart'; import 'package:cw_tron/tron_wallet.dart'; @@ -1131,6 +1152,19 @@ abstract class Tron { List getDefaultTokenContractAddresses(); List getDefaultTokenSymbols(); bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress); + TransactionInfo getTransactionInfo({ + required String id, + required BigInt tronAmount, + int? txFee, + String? tokenSymbol, + required TransactionDirection direction, + required DateTime blockTime, + String? to, + String? from, + required bool isPending, + }); + String getPendingTransactionFee(PendingTransaction tx); + String getPendingTransactionAmount(PendingTransaction tx); } """; @@ -1361,6 +1395,7 @@ Future generateEVM(bool hasImplementation) async { import 'dart:math' as math; import 'package:cake_wallet/core/utilities.dart'; import 'package:cake_wallet/view_model/send/output.dart'; +import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/erc20_token.dart'; import 'package:cw_core/hardware/hardware_account_data.dart'; @@ -1379,6 +1414,7 @@ import 'package:ledger_flutter_plus/ledger_flutter_plus.dart' as ledger; import 'package:bitbox_flutter/bitbox_flutter.dart' as bitbox; import 'package:trezor_connect/trezor_connect.dart' as trezor; import 'package:web3dart/web3dart.dart'; +import 'package:cw_core/transaction_direction.dart'; """; const evmCWHeaders = """ @@ -1389,6 +1425,7 @@ import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cw_evm/utils/evm_chain_formatter.dart'; import 'package:cw_evm/evm_chain_mnemonics.dart'; +import 'package:cw_evm/pending_evm_chain_transaction.dart'; import 'package:cw_evm/evm_chain_registry.dart'; import 'package:cw_evm/evm_erc20_balance.dart'; import 'package:cw_evm/evm_chain_transaction_credentials.dart'; @@ -1608,6 +1645,28 @@ abstract class EVM { WalletBase wallet, TransactionPriority priority, ); + + TransactionInfo getTransactionInfo( + { + required String id, + required int height, + required BigInt ethAmount, + required BigInt ethFee, + required String tokenSymbol, + int exponent = 18, + required TransactionDirection direction, + required bool isPending, + required DateTime date, + required int confirmations, + String? to, + String? from, + String? evmSignatureName, + String? contractAddress, + required int chainId, + } + ); + BigInt getPendingTransactionFee(PendingTransaction tx); + String getPendingTransactionAmount(PendingTransaction tx); Future discoverAndAddWalletTokens(WalletBase wallet); }