fix: Already Known Error (#2606)
What changed, and why it matters
This commit fixes a user-facing bug where sending cryptocurrency through EVM chains (Ethereum, Polygon, Base), Solana, or Tron could produce an 'Already Known' error or fail to show the new transaction promptly. It adds a new method to refresh transaction history and calls it a few seconds after a successful send. The code also includes unrelated formatting cleanups and a small refactor of how EVM wallet types are detected. There is no direct evidence in the commit of a security vulnerability being patched.
Treat as a routine bug-fix/UX improvement. Review in normal development workflow; no urgent security response required based on this commit alone. If the 'Already Known' error had security implications (e.g., transaction replay or fee manipulation), additional vendor or incident references would be needed to justify a security classification.
Security signals we found
No security-relevant keywords in commit title or message
No changes to cryptography, key handling, authentication, or authorization
No input validation or boundary checks added
Adds delayed async transaction-history refresh after send
Refactors EVM wallet-type detection helper
No vendor security disclosure or CVE references present
Evidence from the diff
The patch introduces updateTransactionsHistory() on WalletBase with concrete implementations for EVM, Solana, and Tron wallets. After a successful send, SendViewModel schedules a 4-second delayed refresh of transaction history for those chains. The stated goal is to avoid an ‘Already Known’ error and ensure the sent transaction appears quickly. The diff also refactors isEVMWallet to use isEVMCompatibleChain(walletType) and reformats several unrelated code blocks. No cryptographic, authorization, or input-validation changes are present.
Changed components
cw_core/lib/wallet_base.dartcw_evm/lib/evm_chain_wallet.dartcw_solana/lib/solana_wallet.dartcw_tron/lib/tron_wallet.dartlib/view_model/send/send_view_model.dartInspect captured patch +80 / −44
diff --git a/cw_core/lib/wallet_base.dart b/cw_core/lib/wallet_base.dart
index 769b14c3..5ac3e9b4 100644
--- a/cw_core/lib/wallet_base.dart
+++ b/cw_core/lib/wallet_base.dart
@@ -102,6 +102,7 @@ abstract class WalletBase<BalanceType extends Balance, HistoryType extends Trans
String get password;
Future<void>? updateBalance();
+ Future<void> updateTransactionsHistory() async {}
void setExceptionHandler(void Function(FlutterErrorDetails) onError) => null;
@@ -119,7 +120,7 @@ abstract class WalletBase<BalanceType extends Balance, HistoryType extends Trans
/// Returns true if the connection is alive, false otherwise.
/// Default implementation returns true (no-op for wallets without socket connections).
Future<bool> checkSocketHealth() async => true;
-
+
/// This is used to check if the current node is healthy by making a lightweight RPC call
/// Each wallet implementation should override this to make a single, efficient call
/// Returns true if the node is healthy, false otherwise
diff --git a/cw_evm/lib/evm_chain_wallet.dart b/cw_evm/lib/evm_chain_wallet.dart
index c1c37548..3bf066c9 100644
--- a/cw_evm/lib/evm_chain_wallet.dart
+++ b/cw_evm/lib/evm_chain_wallet.dart
@@ -894,6 +894,9 @@ abstract class EVMChainWalletBase
@override
Future<void>? updateBalance() async => await _updateBalance();
+ @override
+ Future<void> updateTransactionsHistory() async => await _updateTransactions();
+
List<Erc20Token> get erc20Currencies => evmChainErc20TokensBox.values.toList();
diff --git a/cw_solana/lib/solana_wallet.dart b/cw_solana/lib/solana_wallet.dart
index 3085dbb3..581ba562 100644
--- a/cw_solana/lib/solana_wallet.dart
+++ b/cw_solana/lib/solana_wallet.dart
@@ -292,6 +292,14 @@ abstract class SolanaWalletBase
@override
Future<Map<String, SolanaTransactionInfo>> fetchTransactions() async => {};
+ @override
+ Future<void> updateTransactionsHistory() async {
+ await Future.wait([
+ _updateNativeSOLTransactions(),
+ _updateSPLTokenTransactions(),
+ ]);
+ }
+
void updateTransactions(List<SolanaTransactionModel> updatedTx) {
_addTransactionsToTransactionHistory(updatedTx);
}
diff --git a/cw_tron/lib/tron_wallet.dart b/cw_tron/lib/tron_wallet.dart
index 8bd1f97e..6a6b55ac 100644
--- a/cw_tron/lib/tron_wallet.dart
+++ b/cw_tron/lib/tron_wallet.dart
@@ -180,9 +180,11 @@ abstract class TronWalletBase
for (var token in initialTronTokens) {
if (!tronTokensBox.containsKey(token.contractAddress)) {
tronTokensBox.put(token.contractAddress, token);
- } else { // update existing token
+ } else {
+ // update existing token
final existingToken = tronTokensBox.get(token.contractAddress);
- tronTokensBox.put(token.contractAddress, TronToken.copyWith(token, enabled: existingToken!.enabled));
+ tronTokensBox.put(
+ token.contractAddress, TronToken.copyWith(token, enabled: existingToken!.enabled));
}
}
}
@@ -281,7 +283,7 @@ abstract class TronWalletBase
Future<void> startSync() async {
try {
syncStatus = AttemptingSyncStatus();
-
+
// Verify node health before attempting to sync
final isHealthy = await checkNodeHealth();
if (!isHealthy) {
@@ -359,6 +361,14 @@ abstract class TronWalletBase
return pendingTransaction;
}
+ @override
+ Future<void> updateTransactionsHistory() async {
+ await Future.wait([
+ fetchTransactions(),
+ fetchTrc20ExcludedTransactions(),
+ ]);
+ }
+
@override
Future<Map<String, TronTransactionInfo>> fetchTransactions() async {
final address = _tronAddress;
@@ -522,11 +532,11 @@ abstract class TronWalletBase
try {
// Check native balance
await _client.getBalance(_tronPublicKey.toAddress(), throwOnError: true);
-
+
// Check USDT token balance
const usdtContractAddress = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t";
await _client.fetchTronTokenBalances(_tronAddress, usdtContractAddress, throwOnError: true);
-
+
return true;
} catch (e) {
return false;
diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart
index 4ba87f97..83480cfe 100644
--- a/lib/view_model/send/send_view_model.dart
+++ b/lib/view_model/send/send_view_model.dart
@@ -139,8 +139,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
bool get isMwebEnabled => balanceViewModel.mwebEnabled;
- bool get isEVMWallet => walletType == WalletType.ethereum || walletType == WalletType.polygon ||
- walletType == WalletType.base;
+ bool get isEVMWallet => isEVMCompatibleChain(walletType);
@action
void setShowAddressBookPopup(bool value) {
@@ -514,7 +513,6 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
});
}
-
// Swaps.xyz (EVM) path
if (isEVMWallet && trade != null && provider is SwapsXyzExchangeProvider) {
@@ -524,7 +522,6 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
BigInt.tryParse((trade.routerValue ?? '0').toString()) ?? BigInt.zero;
if (routerTo?.isNotEmpty == true && routerData?.isNotEmpty == true) {
-
// detect prepared ERC-20 transfer(...) (alt-vm deposit pattern)
String _selector(String s) =>
(s.startsWith('0x') && s.length >= 10) ? s.substring(0, 10) : '';
@@ -538,11 +535,13 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
// Optionally prebuild approval (SKIP for prepared transfer)
final tokenContract = trade.sourceTokenAddress ?? '';
final requiredAmount = BigInt.tryParse(
- (trade.sourceTokenAmountRaw ?? '0').replaceAll('n', ''),
- ) ?? BigInt.zero;
+ (trade.sourceTokenAmountRaw ?? '0').replaceAll('n', ''),
+ ) ??
+ BigInt.zero;
// Only do approval when NOT a prepared transfer, and only if the API hinted we might need it
- final requiresTokenApproval = (trade.requiresTokenApproval ?? false) && !_isPreparedTransfer;
+ final requiresTokenApproval =
+ (trade.requiresTokenApproval ?? false) && !_isPreparedTransfer;
if (requiresTokenApproval && tokenContract.isNotEmpty && requiredAmount > BigInt.zero) {
if (walletType == WalletType.ethereum) {
@@ -656,7 +655,6 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
}
}
-
// Regular flow
pendingTransaction = await wallet.createTransaction(_credentials(provider));
@@ -772,21 +770,6 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
return; // skip the regular flow below
}
- // Regular flow (non-Swaps)
- if (pendingTransaction!.shouldCommitUR()) {
- final urstr = await pendingTransaction!.commitUR();
- final result = await Navigator.of(context).pushNamed(
- Routes.urqrAnimatedPage,
- arguments: urstr,
- );
- if (result == null) {
- state = FailureState("Canceled by user");
- return;
- }
- } else {
- await pendingTransaction!.commit();
- }
-
String address = outputs.fold('', (acc, value) {
return value.isParsedAddress
? '$acc${value.address}\n${value.extractedAddress}\n\n'
@@ -818,6 +801,17 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
nano!.updateTransactions(wallet);
}
+ // Immediate transaction update for EVM chains, Solana, and Tron
+ if (isEVMWallet || walletType == WalletType.solana || walletType == WalletType.tron) {
+ Future.delayed(Duration(seconds: 4), () async {
+ try {
+ await wallet.updateTransactionsHistory();
+ } catch (e) {
+ printV('Failed to update transactions after send: $e');
+ }
+ });
+ }
+
if (pendingTransaction!.id.isNotEmpty) {
TransactionInfo? tx;
if (walletType == WalletType.monero) {
@@ -1124,7 +1118,6 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
required BigInt requiredAmount,
int? sourceTokenDecimals,
}) async {
-
// Only EVM chains support ERC20 approvals
if (!isEVMWallet) return null;
@@ -1138,15 +1131,24 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
bool needsApproval = false;
if (walletType == WalletType.ethereum) {
needsApproval = await ethereum!.isApprovalRequired(
- wallet, tokenContract, spender, requiredAmount,
+ wallet,
+ tokenContract,
+ spender,
+ requiredAmount,
);
} else if (walletType == WalletType.polygon) {
needsApproval = await polygon!.isApprovalRequired(
- wallet, tokenContract, spender, requiredAmount,
+ wallet,
+ tokenContract,
+ spender,
+ requiredAmount,
);
} else if (walletType == WalletType.base) {
needsApproval = await base!.isApprovalRequired(
- wallet, tokenContract, spender, requiredAmount,
+ wallet,
+ tokenContract,
+ spender,
+ requiredAmount,
);
}
@@ -1154,29 +1156,41 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
final erc20Token = wallet.balance.keys.whereType<Erc20Token>().firstWhere(
(t) => t.contractAddress.toLowerCase() == tokenLc,
- orElse: () => Erc20Token(
- name: '',
- symbol: '',
- contractAddress: tokenContract,
- decimal: sourceTokenDecimals ?? 18,
- enabled: true,
- ),
- );
+ orElse: () => Erc20Token(
+ name: '',
+ symbol: '',
+ contractAddress: tokenContract,
+ decimal: sourceTokenDecimals ?? 18,
+ enabled: true,
+ ),
+ );
if (walletType == WalletType.ethereum) {
final priority = _settingsStore.priority[WalletType.ethereum]!;
return await ethereum!.createTokenApproval(
- wallet, requiredAmount, spender, erc20Token, priority,
+ wallet,
+ requiredAmount,
+ spender,
+ erc20Token,
+ priority,
);
} else if (walletType == WalletType.polygon) {
final priority = _settingsStore.priority[WalletType.polygon]!;
return await polygon!.createTokenApproval(
- wallet, requiredAmount, spender, erc20Token, priority,
+ wallet,
+ requiredAmount,
+ spender,
+ erc20Token,
+ priority,
);
} else if (walletType == WalletType.base) {
final priority = _settingsStore.priority[WalletType.base]!;
return await base!.createTokenApproval(
- wallet, requiredAmount, spender, erc20Token, priority,
+ wallet,
+ requiredAmount,
+ spender,
+ erc20Token,
+ priority,
);
}
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.