tx-confirmation-options-on-dev [cw-1463] (#3289)
What changed, and why it matters
This commit adds new UI options to the transaction confirmation screen, letting users save a recipient as a contact or add a note right after sending. It also changes how pending transactions are tracked for EVM, Solana, and Tron so they appear immediately in history instead of waiting for the next sync. There is no clear security fix or vulnerability being patched; it reads as a feature/UI improvement. Some of the new code introduces minor quality concerns (e.g., a likely copy-paste bug in Tron fee handling, FIXME comments about decimal parsing, and a broad removal of analysis exclusions), but nothing in the diff itself demonstrates an exploitable security flaw.
Treat as a normal feature commit, not an emergency security patch. Code-review follow-ups: fix the apparent Tron fee/amount copy-paste bug; resolve the FIXME comments around decimal parsing before relying on the new pending-transaction values for accounting; verify that re-enabling analysis on the previously excluded files does not introduce build-breaking or behavior-changing lint fixes without review; and ensure the new 'Save Contact' and 'Add Note' flows validate addresses and note content to avoid injection or UI spoofing issues.
Security signals we found
Broad removal of analyzer exclusions re-enables static analysis for many chain-specific generated files
New pending-transaction construction for EVM/Solana/Tron uses parsed amounts and fees with FIXME comments warning about decimal-point parsing issues
Likely copy-paste defect in lib/tron/cw_tron.dart: getPendingTransactionFee returns PendingTronTransaction.amount instead of .fee
Lightning commitOverride return type changed from Future<void> to Future<String> and assigned to pending transaction id
New UI post-commit actions navigate to address-book and transaction-details modals using sendViewModel outputs and transactionInfo
Evidence from the diff
The patch is a feature commit (CW-1463) for the transaction-confirmation flow. Key changes: (1) send_confirm_sheet.dart now shows post-send ‘Save Contact’ and ‘Add a Note’ buttons; (2) TransactionDetailsModal gains a highlightNoteField parameter; (3) Lightning commit overrides now return payment IDs; (4) EVM, Solana, and Tron wallets get helper methods to construct local TransactionInfo objects and add pending transactions to history immediately after commit; (5) analysis_options.yaml removes a large list of excluded files, re-enabling static analysis for many generated chain wrappers. Security-relevant observations from the diff only: the Tron helper getPendingTransactionFee casts to PendingTronTransaction but returns .amount instead of .fee, which looks like a copy-paste defect; several FIXME comments warn about decimal/fee parsing; and re-enabling linting on previously excluded files could surface latent issues but is not itself a vulnerability. No CVE, advisory, or vendor security disclosure is present in the supplied materials.
Changed components
lib/new-ui/widgets/send_page/send_confirm_sheet.dartlib/new-ui/widgets/coins_page/assets_history/transaction_details_modal.dartlib/view_model/send/send_view_model.dartlib/evm/cw_evm.dartlib/solana/cw_solana.dartlib/tron/cw_tron.dartcw_evm/lib/evm_chain_wallet.dartcw_bitcoin/lib/lightning/lightning_wallet.dartcw_bitcoin/lib/lightning/pending_lightning_transaction.dartcw_tron/lib/pending_tron_transaction.dartcw_tron/lib/tron_client.dartanalysis_options.yamlInspect captured patch +458 / −65
diff --git a/analysis_options.yaml b/analysis_options.yaml
index 7f76d272..98dfe6ae 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 deef7204..31ba4b60 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 cb75b7d2..d389949f 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<void> Function() commitOverride;
+ Future<String> Function() commitOverride;
final List<void Function()> _listeners =[];
@override
- final String id;
+ String id;
@override
String get hex => "";
@@ -36,7 +36,7 @@ class PendingLightningTransaction with PendingTransaction {
@override
Future<void> 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 e2e97a35..66ec7577 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 b1d601f8..5a1afc52 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<Map<String, String>> commitUR() {
diff --git a/cw_tron/lib/tron_client.dart b/cw_tron/lib/tron_client.dart
index 60431674..18d79765 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 cd7ee62d..0043c4e0 100644
--- a/lib/di.dart
+++ b/lib/di.dart
@@ -1417,10 +1417,12 @@ Future<void> setup({
}
);
- getIt.registerFactoryParam<TransactionDetailsModal, TransactionInfo, void>(
- (transactionInfo, _) => TransactionDetailsModal(transactionDetailsViewModel: getIt.get<TransactionDetailsViewModel>(
- param1: [transactionInfo, false]))
- );
+ getIt.registerFactoryParam<TransactionDetailsModal, TransactionInfo, bool?>(
+ (transactionInfo, highlightNoteField) => TransactionDetailsModal(
+ transactionDetailsViewModel:
+ getIt.get<TransactionDetailsViewModel>(param1: [transactionInfo, false]),
+ highlightNoteField: highlightNoteField ?? false,
+ ));
getIt.registerFactoryParam<TransactionDetailsPage, TransactionInfo, void>(
(TransactionInfo transactionInfo, _) => TransactionDetailsPage(
diff --git a/lib/evm/cw_evm.dart b/lib/evm/cw_evm.dart
index 7dce83c7..44ed4bfa 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 2511d66c..af09b68a 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<NewSendPage> {
// 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 6c959c20..aa9b522a 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<TransactionDetailsModal> createState() => _TransactionDetailsModalState();
@@ -38,6 +39,10 @@ class _TransactionDetailsModalState extends State<TransactionDetailsModal> {
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 65e6f8d6..0b3a867d 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<SendConfirmSheet> {
+ 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<SendConfirmSheet> {
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<TransactionCommitedScreen> createState() => _TransactionCommitedScreenState();
+}
+
+class _TransactionCommitedScreenState extends State<TransactionCommitedScreen> {
+ 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>(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<TransactionDetailsModal>(
+ 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 76b2591b..7ca41a2c 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 0b6dd8cb..42f22db3 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 8b249c8c..d97d119a 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 1fb3f304..4cbd2763 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 a51423ec..6c001c5b 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 1032ac90..6a38a45b 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 00000000..99fd2b2b
--- /dev/null
+++ b/res/pictures/add_note.svg
@@ -0,0 +1,3 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path d="M15.275 12.475L11.525 8.7L14.3 5.95L13.575 5.225L8.1 10.7L6.7 9.3L12.15 3.825C12.55 3.425 13.0208 3.225 13.5625 3.225C14.1042 3.225 14.575 3.425 14.975 3.825L15.7 4.55L16.95 3.3C17.15 3.1 17.3875 3 17.6625 3C17.9375 3 18.175 3.1 18.375 3.3L20.7 5.625C20.9 5.825 21 6.0625 21 6.3375C21 6.6125 20.9 6.85 20.7 7.05L15.275 12.475ZM6.75 21H3V17.25L10.1 10.125L13.875 13.875L6.75 21Z" fill="white"/>
+</svg>
diff --git a/res/pictures/birthday_cake.svg b/res/pictures/birthday_cake.svg
new file mode 100644
index 00000000..0c11098b
--- /dev/null
+++ b/res/pictures/birthday_cake.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="195" height="180" fill="none" viewBox="0 0 195 180"><path stroke="#d0d0f6" stroke-linecap="round" stroke-width="3" d="M63.5 19v11"/><path stroke="#d0d0e5" stroke-linecap="round" stroke-width="3" d="M63.5 19v11"/><path stroke="#e3e3f9" stroke-linecap="round" stroke-width="3" d="M63.5 19v11"/><path stroke="#d0d0f6" stroke-linecap="round" stroke-width="3" d="M97 19v11"/><path stroke="#d0d0e5" stroke-linecap="round" stroke-width="3" d="M97 19v11"/><path stroke="#e3e3f9" stroke-linecap="round" stroke-width="3" d="M97 19v11"/><path stroke="#d0d0f6" stroke-linecap="round" stroke-width="3" d="M131 19v11"/><path stroke="#d0d0e5" stroke-linecap="round" stroke-width="3" d="M131 19v11"/><path stroke="#e3e3f9" stroke-linecap="round" stroke-width="3" d="M131 19v11"/><g filter="url(#a)"><path fill="#14c846" d="M136.67 51.991h-11.485a1.79 1.79 0 0 1-1.79-1.79V22.765c0-.989.801-1.79 1.79-1.79h11.485a1.79 1.79 0 0 1 1.791 1.79v27.436a1.79 1.79 0 0 1-1.791 1.79"/><path fill="url(#b)" d="M136.67 51.991h-11.485a1.79 1.79 0 0 1-1.79-1.79V22.765c0-.989.801-1.79 1.79-1.79h11.485a1.79 1.79 0 0 1 1.791 1.79v27.436a1.79 1.79 0 0 1-1.791 1.79"/><path fill="#14c846" d="M102.994 51.991H91.509a1.79 1.79 0 0 1-1.79-1.79V22.765c0-.989.801-1.79 1.79-1.79h11.485a1.79 1.79 0 0 1 1.791 1.79v27.436a1.79 1.79 0 0 1-1.791 1.79"/><path fill="url(#c)" d="M102.994 51.991H91.509a1.79 1.79 0 0 1-1.79-1.79V22.765c0-.989.801-1.79 1.79-1.79h11.485a1.79 1.79 0 0 1 1.791 1.79v27.436a1.79 1.79 0 0 1-1.791 1.79"/><path fill="#14c846" d="M69.319 51.991H57.832a1.79 1.79 0 0 1-1.79-1.79V22.765c0-.989.802-1.79 1.79-1.79H69.32c.988 0 1.79.801 1.79 1.79v27.436a1.79 1.79 0 0 1-1.79 1.79"/><path fill="url(#d)" d="M69.319 51.991H57.832a1.79 1.79 0 0 1-1.79-1.79V22.765c0-.989.802-1.79 1.79-1.79H69.32c.988 0 1.79.801 1.79 1.79v27.436a1.79 1.79 0 0 1-1.79 1.79"/></g><path fill="#fc914a" d="M135.674 5.237c-1.466-2.953-4.301-7.863-7.557-3.517-2.472 3.3-4.866 9.324-1.034 12.558 2.06 1.738 5.624 1.741 7.688 0 2.73-2.303 2.313-6.202.903-9.04"/><path fill="url(#e)" d="M135.674 5.237c-1.466-2.953-4.301-7.863-7.557-3.517-2.472 3.3-4.866 9.324-1.034 12.558 2.06 1.738 5.624 1.741 7.688 0 2.73-2.303 2.313-6.202.903-9.04"/><path fill="#fc914a" d="M101.999 5.237c-1.467-2.953-4.301-7.863-7.558-3.517-2.472 3.3-4.866 9.324-1.034 12.558 2.06 1.738 5.624 1.741 7.688 0 2.73-2.303 2.313-6.202.904-9.04"/><path fill="url(#f)" d="M101.999 5.237c-1.467-2.953-4.301-7.863-7.558-3.517-2.472 3.3-4.866 9.324-1.034 12.558 2.06 1.738 5.624 1.741 7.688 0 2.73-2.303 2.313-6.202.904-9.04"/><path fill="#fc914a" d="M68.323 5.237c-1.466-2.953-4.301-7.863-7.557-3.517-2.472 3.3-4.867 9.324-1.034 12.558 2.06 1.738 5.623 1.741 7.687 0 2.73-2.303 2.313-6.202.904-9.04"/><path fill="url(#g)" d="M68.323 5.237c-1.466-2.953-4.301-7.863-7.557-3.517-2.472 3.3-4.867 9.324-1.034 12.558 2.06 1.738 5.623 1.741 7.687 0 2.73-2.303 2.313-6.202.904-9.04"/><path fill="url(#h)" d="M66.339 8.361c-.877-1.895-2.572-5.046-4.52-2.257-1.477 2.118-2.91 5.984-.618 8.059 1.232 1.115 3.363 1.117 4.598 0 1.632-1.478 1.382-3.98.54-5.802"/><path fill="url(#i)" d="M100.339 8.361c-.877-1.895-2.572-5.046-4.52-2.257-1.478 2.118-2.91 5.984-.618 8.059 1.232 1.115 3.363 1.117 4.598 0 1.632-1.478 1.382-3.98.54-5.802"/><path fill="url(#j)" d="M133.339 8.361c-.877-1.895-2.572-5.046-4.519-2.257-1.479 2.118-2.91 5.984-.619 8.059 1.232 1.115 3.363 1.117 4.597 0 1.633-1.478 1.383-3.98.541-5.802"/><g filter="url(#k)"><path fill="#a06047" d="M163.293 108.917H31.207V55.203c0-5.728 4.644-10.372 10.372-10.372h111.343c5.728 0 10.371 4.644 10.371 10.372z"/><path fill="url(#l)" d="M163.293 108.917H31.207V55.203c0-5.728 4.644-10.372 10.372-10.372h111.343c5.728 0 10.371 4.644 10.371 10.372z"/></g><path fill="#ffd57b" d="M163.292 78.528H31.207v12.888h132.085z"/><g filter="url(#m)"><path fill="#2194ff" d="M163.293 55.203v5.484c-2.451 2.01-4.795 4.11-9.69 4.11-9.391 0-9.391-7.733-18.783-7.733s-9.392 7.733-18.788 7.733c-9.392 0-9.392-7.733-18.784-7.733s-9.392 7.733-18.784 7.733-9.392-7.733-18.783-7.733-9.392 7.733-18.784 7.733c-4.892 0-7.235-2.1-9.686-4.11v-5.484c0-5.729 4.644-10.373 10.373-10.373H152.92c5.729 0 10.373 4.644 10.373 10.373"/><path fill="url(#n)" d="M163.293 55.203v5.484c-2.451 2.01-4.795 4.11-9.69 4.11-9.391 0-9.391-7.733-18.783-7.733s-9.392 7.733-18.788 7.733c-9.392 0-9.392-7.733-18.784-7.733s-9.392 7.733-18.784 7.733-9.392-7.733-18.783-7.733-9.392 7.733-18.784 7.733c-4.892 0-7.235-2.1-9.686-4.11v-5.484c0-5.729 4.644-10.373 10.373-10.373H152.92c5.729 0 10.373 4.644 10.373 10.373"/><path fill="url(#o)" d="M163.293 55.203v5.484c-2.451 2.01-4.795 4.11-9.69 4.11-9.391 0-9.391-7.733-18.783-7.733s-9.392 7.733-18.788 7.733c-9.392 0-9.392-7.733-18.784-7.733s-9.392 7.733-18.784 7.733-9.392-7.733-18.783-7.733-9.392 7.733-18.784 7.733c-4.892 0-7.235-2.1-9.686-4.11v-5.484c0-5.729 4.644-10.373 10.373-10.373H152.92c5.729 0 10.373 4.644 10.373 10.373"/></g><g filter="url(#p)"><path fill="url(#q)" d="M178.503 172.999H16v-53.713c0-5.728 4.644-10.372 10.372-10.372H168.13c5.728 0 10.371 4.644 10.371 10.372z"/></g><path fill="#ffac82" d="M178.501 142.615H16v12.888h162.501z"/><g filter="url(#r)"><path fill="#2194ff" d="M178.504 120.114v4.657c-3.016 2.01-5.9 4.11-11.92 4.11-11.558 0-11.558-7.733-23.115-7.733-11.553 0-11.553 7.733-23.11 7.733-11.554 0-11.554-7.733-23.11-7.733-11.554 0-11.554 7.733-23.111 7.733-11.554 0-11.554-7.733-23.11-7.733-11.554 0-11.554 7.733-23.111 7.733-6.02 0-8.901-2.099-11.917-4.11v-5.484c0-5.729 4.644-10.373 10.373-10.373h140.932c6.185 0 11.199 3.586 11.199 11.2"/><path fill="url(#s)" d="M178.504 120.114v4.657c-3.016 2.01-5.9 4.11-11.92 4.11-11.558 0-11.558-7.733-23.115-7.733-11.553 0-11.553 7.733-23.11 7.733-11.554 0-11.554-7.733-23.11-7.733-11.554 0-11.554 7.733-23.111 7.733-11.554 0-11.554-7.733-23.11-7.733-11.554 0-11.554 7.733-23.111 7.733-6.02 0-8.901-2.099-11.917-4.11v-5.484c0-5.729 4.644-10.373 10.373-10.373h140.932c6.185 0 11.199 3.586 11.199 11.2"/></g><g filter="url(#t)"><rect width="195" height="6" y="173" fill="#d9d9d9" rx="3"/><rect width="195" height="6" y="173" fill="url(#u)" rx="3"/></g><defs><linearGradient id="b" x1="131.704" x2="126.405" y1="22.935" y2="42.767" gradientUnits="userSpaceOnUse"><stop stop-color="#efeff8"/><stop offset="1" stop-color="#c3c3f6"/></linearGradient><linearGradient id="c" x1="98.028" x2="92.729" y1="22.935" y2="42.767" gradientUnits="userSpaceOnUse"><stop stop-color="#efeff8"/><stop offset="1" stop-color="#c3c3f6"/></linearGradient><linearGradient id="d" x1="64.353" x2="59.053" y1="22.935" y2="42.767" gradientUnits="userSpaceOnUse"><stop stop-color="#efeff8"/><stop offset="1" stop-color="#c3c3f6"/></linearGradient><linearGradient id="e" x1="128.521" x2="121.868" y1="-2" y2="2.666" gradientUnits="userSpaceOnUse"><stop stop-color="#fe9c12"/><stop offset="1" stop-color="#ff6a00"/></linearGradient><linearGradient id="f" x1="94.845" x2="88.193" y1="-2" y2="2.666" gradientUnits="userSpaceOnUse"><stop stop-color="#fe9c12"/><stop offset="1" stop-color="#ff6a00"/></linearGradient><linearGradient id="g" x1="61.169" x2="54.517" y1="-2" y2="2.666" gradientUnits="userSpaceOnUse"><stop stop-color="#fe9c12"/><stop offset="1" stop-color="#ff6a00"/></linearGradient><linearGradient id="h" x1="63.5" x2="63.5" y1="5" y2="15" gradientUnits="userSpaceOnUse"><stop stop-color="#fff" stop-opacity="0"/><stop offset="1" stop-color="#fff"/></linearGradient><linearGradient id="i" x1="97.5" x2="97.5" y1="5" y2="15" gradientUnits="userSpaceOnUse"><stop stop-color="#fff" stop-opacity="0"/><stop offset="1" stop-color="#fff"/></linearGradient><linearGradient id="j" x1="130.5" x2="130.5" y1="5" y2="15" gradientUnits="userSpaceOnUse"><stop stop-color="#fff" stop-opacity="0"/><stop offset="1" stop-color="#fff"/></linearGradient><linearGradient id="l" x1="48.883" x2="169.302" y1="47.846" y2="112.452" gradientUnits="userSpaceOnUse"><stop offset=".354" stop-color="#ffc414"/><stop offset="1" stop-color="#ff9447"/></linearGradient><linearGradient id="n" x1="51.937" x2="136.726" y1="42.916" y2="82.605" gradientUnits="userSpaceOnUse"><stop offset=".128" stop-color="#61c5ff"/><stop offset="1" stop-color="#2a92fa"/></linearGradient><linearGradient id="o" x1="13.998" x2="147.998" y1="45" y2="72" gradientUnits="userSpaceOnUse"><stop offset=".128" stop-color="#61c5ff"/><stop offset="1" stop-color="#2a92fa"/></linearGradient><linearGradient id="q" x1="58.776" x2="156.591" y1="110.019" y2="245.711" gradientUnits="userSpaceOnUse"><stop stop-color="#fe6412"/><stop offset="1" stop-color="#eb4600"/></linearGradient><linearGradient id="s" x1="41.5" x2="137" y1="107" y2="162" gradientUnits="userSpaceOnUse"><stop offset=".128" stop-color="#61c5ff"/><stop offset="1" stop-color="#2a92fa"/></linearGradient><linearGradient id="u" x1="198" x2="84.121" y1="176" y2="273.876" gradientUnits="userSpaceOnUse"><stop stop-color="#c3c3f6"/><stop offset=".553" stop-color="#efeff8"/><stop offset="1" stop-color="#c3c3f6"/></linearGradient><filter id="a" width="82.418" height="34.017" x="56.043" y="17.975" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset dy="-8"/><feGaussianBlur stdDeviation="1.5"/><feComposite in2="hardAlpha" k2="-1" k3="1" operator="arithmetic"/><feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2 0"/><feBlend in2="shape" result="effect1_innerShadow_19634_811896"/></filter><filter id="k" width="132.086" height="64.086" x="31.207" y="44.831" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset dy="-2"/><feComposite in2="hardAlpha" k2="-1" k3="1" operator="arithmetic"/><feColorMatrix values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.3 0"/><feBlend in2="shape" result="effect1_innerShadow_19634_811896"/></filter><filter id="m" width="136.882" height="27.067" x="28.811" y="42.13" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset dy="2"/><feGaussianBlur stdDeviation="1.2"/><feComposite in2="hardAlpha" operator="out"/><feColorMatrix values="0 0 0 0 0.913462 0 0 0 0 0.578452 0 0 0 0 0.0395248 0 0 0 0.4 0"/><feBlend in2="BackgroundImageFix" result="effect1_dropShadow_19634_811896"/><feBlend in="SourceGraphic" in2="effect1_dropShadow_19634_811896" result="shape"/><feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset dy="-3"/><feGaussianBlur stdDeviation="1.35"/><feComposite in2="hardAlpha" k2="-1" k3="1" operator="arithmetic"/><feColorMatrix values="0 0 0 0 0.610577 0 0 0 0 0.83125 0 0 0 0 1 0 0 0 1 0"/><feBlend in2="shape" result="effect2_innerShadow_19634_811896"/><feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset dy="-1"/><feGaussianBlur stdDeviation="1.35"/><feComposite in2="hardAlpha" k2="-1" k3="1" operator="arithmetic"/><feColorMatrix values="0 0 0 0 0.107064 0 0 0 0 0.531767 0 0 0 0 0.927885 0 0 0 1 0"/><feBlend in2="effect2_innerShadow_19634_811896" result="effect3_innerShadow_19634_811896"/><feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset dy="1"/><feComposite in2="hardAlpha" k2="-1" k3="1" operator="arithmetic"/><feColorMatrix values="0 0 0 0 0.663462 0 0 0 0 0.859615 0 0 0 0 1 0 0 0 1 0"/><feBlend in2="effect3_innerShadow_19634_811896" result="effect4_innerShadow_19634_811896"/></filter><filter id="p" width="162.504" height="64.085" x="16" y="108.914" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset dy="-2"/><feComposite in2="hardAlpha" k2="-1" k3="1" operator="arithmetic"/><feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/><feBlend in2="shape" result="effect1_innerShadow_19634_811896"/></filter><filter id="r" width="167.304" height="27.067" x="13.6" y="106.214" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset dy="2"/><feGaussianBlur stdDeviation="1.2"/><feComposite in2="hardAlpha" operator="out"/><feColorMatrix values="0 0 0 0 0.860577 0 0 0 0 0.291016 0 0 0 0 0 0 0 0 0.5 0"/><feBlend in2="BackgroundImageFix" result="effect1_dropShadow_19634_811896"/><feBlend in="SourceGraphic" in2="effect1_dropShadow_19634_811896" result="shape"/><feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset dy="-3"/><feGaussianBlur stdDeviation="1.35"/><feComposite in2="hardAlpha" k2="-1" k3="1" operator="arithmetic"/><feColorMatrix values="0 0 0 0 0.610577 0 0 0 0 0.83125 0 0 0 0 1 0 0 0 1 0"/><feBlend in2="shape" result="effect2_innerShadow_19634_811896"/><feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset dy="-1"/><feGaussianBlur stdDeviation="1.35"/><feComposite in2="hardAlpha" k2="-1" k3="1" operator="arithmetic"/><feColorMatrix values="0 0 0 0 0.107064 0 0 0 0 0.531767 0 0 0 0 0.927885 0 0 0 1 0"/><feBlend in2="effect2_innerShadow_19634_811896" result="effect3_innerShadow_19634_811896"/><feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset dy="1"/><feComposite in2="hardAlpha" k2="-1" k3="1" operator="arithmetic"/><feColorMatrix values="0 0 0 0 0.663462 0 0 0 0 0.859615 0 0 0 0 1 0 0 0 1 0"/><feBlend in2="effect3_innerShadow_19634_811896" result="effect4_innerShadow_19634_811896"/></filter><filter id="t" width="195" height="7" x="0" y="173" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset dy="1"/><feComposite in2="hardAlpha" operator="out"/><feColorMatrix values="0 0 0 0 0.476493 0 0 0 0 0.476493 0 0 0 0 0.639423 0 0 0 1 0"/><feBlend in2="BackgroundImageFix" result="effect1_dropShadow_19634_811896"/><feBlend in="SourceGraphic" in2="effect1_dropShadow_19634_811896" result="shape"/></filter></defs></svg>
\ 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 00000000..b474ef5b
--- /dev/null
+++ b/res/pictures/save_contact.svg
@@ -0,0 +1,3 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path d="M9.175 10.825C8.39167 10.0417 8 9.1 8 8C8 6.9 8.39167 5.95833 9.175 5.175C9.95833 4.39167 10.9 4 12 4C13.1 4 14.0417 4.39167 14.825 5.175C15.6083 5.95833 16 6.9 16 8C16 9.1 15.6083 10.0417 14.825 10.825C14.0417 11.6083 13.1 12 12 12C10.9 12 9.95833 11.6083 9.175 10.825ZM4 20V17.2C4 16.6333 4.14583 16.1125 4.4375 15.6375C4.72917 15.1625 5.11667 14.8 5.6 14.55C6.63333 14.0333 7.68333 13.6458 8.75 13.3875C9.81667 13.1292 10.9 13 12 13C13.1 13 14.1833 13.1292 15.25 13.3875C16.3167 13.6458 17.3667 14.0333 18.4 14.55C18.8833 14.8 19.2708 15.1625 19.5625 15.6375C19.8542 16.1125 20 16.6333 20 17.2V20H4Z" fill="white"/>
+</svg>
diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb
index d6e4d09d..19009345 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 6a5385f8..a16d88f2 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<void> 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<void> 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<String> getDefaultTokenContractAddresses();
List<String> 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<void> 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<void> discoverAndAddWalletTokens(WalletBase wallet);
}
Why this scored 23/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.