diff --git a/Dockerfile b/Dockerfile
index 7a0c8c022..78e119877 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -52,7 +52,7 @@ RUN set -o xtrace \
# for x86 emulators
libatk-bridge2.0-0 libgdk-pixbuf-xlib-2.0-0 libgtk-3-0 libnspr4 libnss3-dev libsqlite3-dev libxtst6 libxss1 lftp sqlite3 xxd \
# Linux desktop dependencies
- clang cmake libgtk-3-dev ninja-build pkg-config libsecret-1-0 libsecret-1-dev gir1.2-secret-1 \
+ clang lld cmake libgtk-3-dev ninja-build pkg-config libsecret-1-0 libsecret-1-dev gir1.2-secret-1 \
# monero_c dependencies
autoconf automake build-essential ccache gperf libtool llvm \
# extra stuff for KVM
diff --git a/assets/images/radar.png b/assets/images/radar.png
new file mode 100644
index 000000000..386511418
Binary files /dev/null and b/assets/images/radar.png differ
diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart
index a2b2f798f..a0d88ce9a 100644
--- a/cw_bitcoin/lib/bitcoin_wallet.dart
+++ b/cw_bitcoin/lib/bitcoin_wallet.dart
@@ -345,12 +345,16 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
return balance;
}
+ try {
final lBalance = await lightningWallet!.getBalance();
this.balance[CryptoCurrency.btcln] = ElectrumBalance(
confirmed: lBalance,
unconfirmed: Money.zero(CryptoCurrency.btcln),
frozen: Money.zero(CryptoCurrency.btcln));
+ } catch (e) {
+ printV("Error fetching lightning balance: $e");
+ }
return ElectrumBalance(
confirmed: balance.confirmed,
@@ -551,8 +555,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
return tx;
}
- List getUtxoWithPrivateKeys() => unspentCoins
- .where((e) => (e.isSending && !e.isFrozen))
+ List getUtxoWithPrivateKeys({bool confirmedOnly = false}) => unspentCoins
+ .where((e) => e.isSending && !e.isFrozen && (!confirmedOnly || (e.confirmations ?? 0) > 0))
.map((unspent) => UtxoWithPrivateKey.fromUnspent(unspent, this))
.toList();
diff --git a/cw_bitcoin/lib/bitcoin_wallet_service.dart b/cw_bitcoin/lib/bitcoin_wallet_service.dart
index 6163a8a82..8a034d8cf 100644
--- a/cw_bitcoin/lib/bitcoin_wallet_service.dart
+++ b/cw_bitcoin/lib/bitcoin_wallet_service.dart
@@ -8,7 +8,6 @@ import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_core/payjoin_session.dart';
import 'package:cw_core/unspent_coins_info.dart';
import 'package:cw_core/utils/zpub.dart';
-import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_service.dart';
import 'package:cw_bitcoin/bitcoin_wallet.dart';
import 'package:cw_core/pathForWallet.dart';
@@ -130,31 +129,6 @@ class BitcoinWalletService extends WalletService<
}
}
- @override
- Future rename(String currentName, String password, String newName) async {
- final currentWalletInfo = await WalletInfo.get(currentName, getType());
- if (currentWalletInfo == null) {
- throw Exception('Wallet not found');
- }
- final currentWallet = await BitcoinWalletBase.open(
- password: password,
- name: currentName,
- walletInfo: currentWalletInfo,
- unspentCoinsInfo: unspentCoinsInfoSource,
- payjoinBox: payjoinSessionSource,
- encryptionFileUtils: encryptionFileUtilsFor(isDirect),
- );
-
- await currentWallet.renameWalletFiles(newName);
- await saveBackup(newName);
-
- final newWalletInfo = currentWalletInfo;
- newWalletInfo.id = WalletBase.idFor(newName, getType());
- newWalletInfo.name = newName;
-
- await newWalletInfo.save();
- }
-
@override
Future restoreFromHardwareWallet(BitcoinRestoreWalletFromHardware credentials,
{bool? isTestnet}) async {
diff --git a/cw_bitcoin/lib/electrum_transaction_info.dart b/cw_bitcoin/lib/electrum_transaction_info.dart
index 9e18d0e60..a58829e00 100644
--- a/cw_bitcoin/lib/electrum_transaction_info.dart
+++ b/cw_bitcoin/lib/electrum_transaction_info.dart
@@ -18,7 +18,7 @@ class ElectrumTransactionBundle {
{required this.ins, required this.confirmations, this.time});
final BtcTransaction originalTransaction;
- final List ins;
+ final List ins;
final int? time;
final int confirmations;
}
@@ -127,9 +127,14 @@ class ElectrumTransactionInfo extends TransactionInfo {
List inputAddresses = [];
List outputAddresses = [];
+ var hasMissingInputTx = false;
for (var i = 0; i < bundle.originalTransaction.inputs.length; i++) {
final input = bundle.originalTransaction.inputs[i];
- final inputTransaction = bundle.ins[i];
+ final inputTransaction = i < bundle.ins.length ? bundle.ins[i] : null;
+ if (inputTransaction == null || input.txIndex >= inputTransaction.outputs.length) {
+ hasMissingInputTx = true;
+ continue;
+ }
final outTransaction = inputTransaction.outputs[input.txIndex];
inputAmount += outTransaction.amount.toInt();
if (addresses.contains(addressFromOutputScript(outTransaction.scriptPubKey, network))) {
@@ -184,9 +189,14 @@ class ElectrumTransactionInfo extends TransactionInfo {
final b = tx.outputs.first.scriptPubKey.toBytes();
return b.length == 34 && b[0] == 88 && b[1] == 32;
};
- final isHogEx = isHogExTx(bundle.originalTransaction) && isHogExTx(bundle.ins.first);
+ final firstInput = bundle.ins.isNotEmpty ? bundle.ins.first : null;
+ final isHogEx = firstInput != null &&
+ isHogExTx(bundle.originalTransaction) &&
+ isHogExTx(firstInput);
- final fee = inputAmount - totalOutAmount;
+ final fee = hasMissingInputTx ? null : inputAmount - totalOutAmount;
+ final walletCurrency = walletTypeToCryptoCurrency(type);
+ final feeMoney = fee != null ? Money.fromInt(fee, walletCurrency) : null;
return ElectrumTransactionInfo(type,
id: bundle.originalTransaction.txId(),
height: height,
@@ -194,11 +204,12 @@ class ElectrumTransactionInfo extends TransactionInfo {
isReplaced: false,
inputAddresses: inputAddresses,
outputAddresses: outputAddresses,
- fee: Money.fromInt(fee, walletTypeToCryptoCurrency(type)),
+ fee: feeMoney,
direction: direction,
- amount: Money.fromInt(amount, walletTypeToCryptoCurrency(type)),
+ amount: Money.fromInt(amount, walletCurrency),
date: date,
isHogEx: isHogEx,
+ additionalInfo: {'hasMissingInputTx': hasMissingInputTx},
confirmations: bundle.confirmations);
}
diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart
index 095d1a45e..3dbecad96 100644
--- a/cw_bitcoin/lib/electrum_wallet.dart
+++ b/cw_bitcoin/lib/electrum_wallet.dart
@@ -1,6 +1,5 @@
import 'dart:async';
import 'dart:convert';
-import 'dart:io';
import 'dart:isolate';
import 'package:bitcoin_base/bitcoin_base.dart';
@@ -38,7 +37,6 @@ import 'package:cw_core/get_height_by_date.dart';
import 'package:cw_core/hardware/hardware_wallet_service.dart';
import 'package:cw_core/node.dart';
import 'package:cw_core/output_info.dart';
-import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/pending_transaction.dart';
import 'package:cw_core/root_dir.dart';
import 'package:cw_core/sync_status.dart';
@@ -1736,37 +1734,6 @@ abstract class ElectrumWalletBase
await transactionHistory.save();
}
- @override
- Future renameWalletFiles(String newWalletName) async {
- final currentWalletPath = await pathForWallet(name: walletInfo.name, type: type);
- final currentWalletFile = File(currentWalletPath);
-
- final currentDirPath = await pathForWalletDir(name: walletInfo.name, type: type);
- final currentTransactionsFile = File('$currentDirPath/$transactionsHistoryFileName');
-
- // Copies current wallet files into new wallet name's dir and files
- if (currentWalletFile.existsSync()) {
- final newWalletPath = await pathForWallet(name: newWalletName, type: type);
- await currentWalletFile.copy(newWalletPath);
- }
- if (currentTransactionsFile.existsSync()) {
- final newDirPath = await pathForWalletDir(name: newWalletName, type: type);
- await currentTransactionsFile.copy('$newDirPath/$transactionsHistoryFileName');
- }
-
- // Delete old name's dir and files
- final dir = Directory(currentDirPath);
- for (var attempt = 0; attempt < 3; attempt++) {
- try {
- await dir.delete(recursive: true);
- break;
- } on FileSystemException {
- if (attempt == 2) rethrow;
- await Future.delayed(const Duration(milliseconds: 200));
- }
- }
- }
-
@override
Future changePassword(String password) async {
_password = password;
@@ -2118,6 +2085,9 @@ abstract class ElectrumWalletBase
for (int i = 0; i < bundle.originalTransaction.inputs.length; i++) {
final input = bundle.originalTransaction.inputs[i];
final inputTransaction = bundle.ins[i];
+ if (inputTransaction == null) {
+ throw Exception("Missing input transaction for fee calculation");
+ }
final vout = input.txIndex;
final outTransaction = inputTransaction.outputs[vout];
allInputsAmount += outTransaction.amount.toInt();
@@ -2146,6 +2116,9 @@ abstract class ElectrumWalletBase
for (var i = 0; i < bundle.originalTransaction.inputs.length; i++) {
final input = bundle.originalTransaction.inputs[i];
final inputTransaction = bundle.ins[i];
+ if (inputTransaction == null) {
+ throw Exception("Missing input transaction for replace-by-fee");
+ }
final vout = input.txIndex;
final outTransaction = inputTransaction.outputs[vout];
final address = addressFromOutputScript(outTransaction.scriptPubKey, network);
@@ -2434,20 +2407,24 @@ abstract class ElectrumWalletBase
}
final original = BtcTransaction.fromRaw(transactionHex);
- final ins = [];
+ final ins = [];
for (final vin in original.inputs) {
- final verboseTransaction = await electrumClient.getTransactionVerbose(hash: vin.txId);
+ try {
+ final verboseTransaction = await electrumClient.getTransactionVerbose(hash: vin.txId);
- final String inputTransactionHex;
+ final String inputTransactionHex;
- if (verboseTransaction.isEmpty) {
- inputTransactionHex = await electrumClient.getTransactionHex(hash: hash);
- } else {
- inputTransactionHex = verboseTransaction['hex'] as String;
+ if (verboseTransaction.isEmpty) {
+ inputTransactionHex = await electrumClient.getTransactionHex(hash: vin.txId);
+ } else {
+ inputTransactionHex = verboseTransaction['hex'] as String;
+ }
+
+ ins.add(inputTransactionHex.isEmpty ? null : BtcTransaction.fromRaw(inputTransactionHex));
+ } catch (_) {
+ ins.add(null);
}
-
- ins.add(BtcTransaction.fromRaw(inputTransactionHex));
}
return ElectrumTransactionBundle(
@@ -3204,22 +3181,11 @@ abstract class ElectrumWalletBase
}
}
- final ins = [];
final inputTxids = inputTxidsByHash[txid] ?? const [];
- bool allInputsPresent = true;
- for (final inputTxid in inputTxids) {
- final inTx = parsedInputTxById[inputTxid];
- if (inTx == null) {
- allInputsPresent = false;
- break;
- }
- ins.add(inTx);
- }
-
- if (!allInputsPresent || ins.length != original.inputs.length) {
- continue;
- }
+ final ins = [
+ for (final inputTxid in inputTxids) parsedInputTxById[inputTxid],
+ ];
bundles[txid] = ElectrumTransactionBundle(
original,
@@ -3796,6 +3762,7 @@ abstract class ElectrumWalletBase
for (int i = 0; i < bundle.originalTransaction.inputs.length; i++) {
final input = bundle.originalTransaction.inputs[i];
final inputTransaction = bundle.ins[i];
+ if (inputTransaction == null) continue;
final vout = input.txIndex;
final outTransaction = inputTransaction.outputs[vout];
final address = addressFromOutputScript(outTransaction.scriptPubKey, network);
diff --git a/cw_bitcoin/lib/lightning/lightning_wallet.dart b/cw_bitcoin/lib/lightning/lightning_wallet.dart
index d78c88d13..42dda7cb2 100644
--- a/cw_bitcoin/lib/lightning/lightning_wallet.dart
+++ b/cw_bitcoin/lib/lightning/lightning_wallet.dart
@@ -123,7 +123,9 @@ class LightningWallet {
Future close() async {
_eventSubscription?.cancel();
- await _sdk?.disconnect();
+ try {
+ await _sdk?.disconnect();
+ } catch (_) {}
_logSubscription?.cancel();
}
@@ -439,6 +441,11 @@ class LightningWallet {
direction = TransactionDirection.incoming;
}
+ String? preimage;
+ if (payment.details != null && payment.details is PaymentDetails_Lightning) {
+ preimage = (payment.details as PaymentDetails_Lightning).htlcDetails.preimage;
+ }
+
return ElectrumTransactionInfo(
WalletType.bitcoin,
id: payment.id,
@@ -448,7 +455,10 @@ class LightningWallet {
fee: Money(payment.fees, currency),
date: DateTime.fromMillisecondsSinceEpoch(payment.timestamp.toInt() * 1000),
confirmations: payment.status == PaymentStatus.pending ? 0 : 10,
- additionalInfo: {"isLightning": true},
+ additionalInfo: {
+ "isLightning": true,
+ if (preimage != null) "preimage": preimage,
+ },
);
}
diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart
index 51cc8a4d8..2f9ae0b3c 100644
--- a/cw_bitcoin/lib/litecoin_wallet.dart
+++ b/cw_bitcoin/lib/litecoin_wallet.dart
@@ -497,22 +497,25 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
mwebUtxosBox = await CakeHive.openBox(boxName);
}
- @override
- Future renameWalletFiles(String newWalletName) async {
- // rename the hive box:
- final oldBoxName = "${walletInfo.name.replaceAll(" ", "_")}_${MwebUtxo.boxName}";
- final newBoxName = "${newWalletName.replaceAll(" ", "_")}_${MwebUtxo.boxName}";
+ static Future copyMwebBox({
+ required String fromName,
+ required String toName,
+ }) async {
+ final oldBoxName = "${fromName.replaceAll(" ", "_")}_${MwebUtxo.boxName}";
+ final newBoxName = "${toName.replaceAll(" ", "_")}_${MwebUtxo.boxName}";
+ if (oldBoxName == newBoxName) return;
final oldBox = await CakeHive.openBox(oldBoxName);
- mwebUtxosBox = await CakeHive.openBox(newBoxName);
+ final newBox = await CakeHive.openBox(newBoxName);
for (final key in oldBox.keys) {
- final value = oldBox.get(key);
- await oldBox.delete(key);
- await mwebUtxosBox.put(key, value!);
+ await newBox.put(key, oldBox.get(key)!);
}
- oldBox.deleteFromDisk();
+ }
- await super.renameWalletFiles(newWalletName);
+ static Future deleteMwebBox(String name) async {
+ final boxName = "${name.replaceAll(" ", "_")}_${MwebUtxo.boxName}";
+ final box = await CakeHive.openBox(boxName);
+ await box.deleteFromDisk();
}
@action
diff --git a/cw_bitcoin/lib/litecoin_wallet_service.dart b/cw_bitcoin/lib/litecoin_wallet_service.dart
index 739602e24..0156ee672 100644
--- a/cw_bitcoin/lib/litecoin_wallet_service.dart
+++ b/cw_bitcoin/lib/litecoin_wallet_service.dart
@@ -12,8 +12,6 @@ import 'package:cw_core/wallet_service.dart';
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cw_core/wallet_info.dart';
-import 'package:cw_core/wallet_base.dart';
-import 'package:collection/collection.dart';
import 'package:bip39/bip39.dart' as bip39;
import 'package:path_provider/path_provider.dart';
@@ -139,26 +137,11 @@ class LitecoinWalletService extends WalletService<
@override
Future rename(String currentName, String password, String newName) async {
- final currentWalletInfo = await WalletInfo.get(currentName, getType());
- if (currentWalletInfo == null) {
- throw Exception('Wallet not found');
- }
- final currentWallet = await LitecoinWalletBase.open(
- password: password,
- name: currentName,
- walletInfo: currentWalletInfo,
- unspentCoinsInfo: unspentCoinsInfoSource,
- encryptionFileUtils: encryptionFileUtilsFor(isDirect),
- );
+ if (currentName == newName) return;
- await currentWallet.renameWalletFiles(newName);
- await saveBackup(newName);
-
- final newWalletInfo = currentWalletInfo;
- newWalletInfo.id = WalletBase.idFor(newName, getType());
- newWalletInfo.name = newName;
-
- await newWalletInfo.save();
+ await LitecoinWalletBase.copyMwebBox(fromName: currentName, toName: newName);
+ await super.rename(currentName, password, newName);
+ await LitecoinWalletBase.deleteMwebBox(currentName);
}
@override
diff --git a/cw_bitcoin/lib/payjoin/manager.dart b/cw_bitcoin/lib/payjoin/manager.dart
index 437fed3ff..4fa2e2a63 100644
--- a/cw_bitcoin/lib/payjoin/manager.dart
+++ b/cw_bitcoin/lib/payjoin/manager.dart
@@ -297,10 +297,10 @@ class PayjoinManager {
break;
case PayjoinReceiverRequestTypes.getCandidateInputs:
- utxos = _wallet.getUtxoWithPrivateKeys();
+ utxos = _wallet.getUtxoWithPrivateKeys(confirmedOnly: true);
if (utxos.isEmpty) {
await _wallet.updateAllUnspents();
- utxos = _wallet.getUtxoWithPrivateKeys();
+ utxos = _wallet.getUtxoWithPrivateKeys(confirmedOnly: true);
}
mainToIsolateSendPort?.send({
'requestId': message['requestId'],
diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart
index 1861b21f4..c7316ec4c 100644
--- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart
+++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart
@@ -1,13 +1,11 @@
import 'dart:io';
import 'package:bip39/bip39.dart';
-import 'package:collection/collection.dart';
import 'package:cw_bitcoin/bitcoin_mnemonics_bip39.dart';
import 'package:cw_bitcoin_cash/cw_bitcoin_cash.dart';
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/unspent_coins_info.dart';
-import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_service.dart';
import 'package:cw_core/wallet_type.dart';
@@ -99,29 +97,6 @@ class BitcoinCashWalletService extends WalletService<
}
}
- @override
- Future rename(String currentName, String password, String newName) async {
- final currentWalletInfo = await WalletInfo.get(currentName, getType());
- if (currentWalletInfo == null) {
- throw Exception('Wallet not found');
- }
- final currentWallet = await BitcoinCashWalletBase.open(
- password: password,
- name: currentName,
- walletInfo: currentWalletInfo,
- unspentCoinsInfo: unspentCoinsInfoSource,
- encryptionFileUtils: encryptionFileUtilsFor(isDirect));
-
- await currentWallet.renameWalletFiles(newName);
- await saveBackup(newName);
-
- final newWalletInfo = currentWalletInfo;
- newWalletInfo.id = WalletBase.idFor(newName, getType());
- newWalletInfo.name = newName;
-
- await newWalletInfo.save();
- }
-
@override
Future restoreFromHardwareWallet(BitcoinCashNewWalletCredentials credentials) {
throw UnimplementedError(
diff --git a/cw_core/lib/amount/money_double.dart b/cw_core/lib/amount/money_double.dart
new file mode 100644
index 000000000..b3331c44b
--- /dev/null
+++ b/cw_core/lib/amount/money_double.dart
@@ -0,0 +1,13 @@
+import 'dart:math';
+import 'package:cw_core/amount/money.dart';
+import 'package:cw_core/currency.dart';
+
+extension ToMoney on double {
+ /// Turn a double representation of a currency amount to a proper Money representation
+ /// truncating currencies with more than 20 decimals because double can not handle more
+ Money? tryToMoney(Currency currency) =>
+ Money.tryParse(toStringAsFixed(min(currency.decimals, 20)), currency);
+
+ Money toMoney(Currency currency) =>
+ Money.parse(toStringAsFixed(min(currency.decimals, 20)), currency);
+}
diff --git a/cw_core/lib/parse_fixed.dart b/cw_core/lib/parse_fixed.dart
index fede4df09..51a6a7a58 100644
--- a/cw_core/lib/parse_fixed.dart
+++ b/cw_core/lib/parse_fixed.dart
@@ -29,6 +29,13 @@ BigInt? tryParseFixed(String value, int decimals) {
BigInt parseFixed(String value, int decimals) {
final multiplier = getMultiplier(decimals);
+ /// handle weird cases where users enter spaces and currency after the amount
+ /// This should be handled from UI field to prevent non numerical values
+ /// but will be in the refactoring
+ if (value.contains(" ")) {
+ value = value.split(" ").first;
+ }
+
final negative = value.startsWith("-");
if (negative) value = value.substring(1);
diff --git a/cw_core/lib/pathForWallet.dart b/cw_core/lib/pathForWallet.dart
index cb5a4fe78..9fe5f717a 100644
--- a/cw_core/lib/pathForWallet.dart
+++ b/cw_core/lib/pathForWallet.dart
@@ -1,6 +1,7 @@
import 'dart:io';
import 'package:cw_core/root_dir.dart';
import 'package:cw_core/wallet_type.dart';
+import 'package:path/path.dart' as p;
Future pathForWalletTypeDir({required WalletType type}) async {
final root = await getAppDir();
@@ -36,3 +37,43 @@ Future outdatedAndroidPathForWalletDir({required String name}) async {
return pathDir;
}
+
+Future copyWalletFilesTo({
+ required String fromName,
+ required String toName,
+ required WalletType type,
+}) async {
+ if (fromName == toName) return;
+
+ final typeRoot = await pathForWalletTypeDir(type: type);
+ final sourceDir = Directory(p.join(typeRoot, fromName));
+ if (!sourceDir.existsSync()) {
+ throw "Source wallet not found: $fromName $type";
+ }
+
+ if (Directory(p.join(typeRoot, toName)).existsSync()) {
+ throw Exception('Cannot rename wallet: "$toName" already exists');
+ }
+
+ final destinationDir = Directory(p.join(typeRoot, toName));
+ await _copyDirectory(sourceDir, destinationDir);
+
+ for (final suffix in const ['', '.keys', '.keys.backup']) {
+ final file = File(p.join(destinationDir.path, '$fromName$suffix'));
+ if (file.existsSync()) {
+ await file.rename(p.join(destinationDir.path, '$toName$suffix'));
+ }
+ }
+}
+
+Future _copyDirectory(Directory source, Directory destination) async {
+ await destination.create(recursive: true);
+ await for (final entity in source.list(followLinks: false)) {
+ final name = p.basename(entity.path);
+ if (entity is File) {
+ await entity.copy(p.join(destination.path, name));
+ } else if (entity is Directory) {
+ await _copyDirectory(entity, Directory(p.join(destination.path, name)));
+ }
+ }
+}
diff --git a/cw_core/lib/payment_uris.dart b/cw_core/lib/payment_uris.dart
index daf56af2f..9e4191072 100644
--- a/cw_core/lib/payment_uris.dart
+++ b/cw_core/lib/payment_uris.dart
@@ -7,6 +7,13 @@ abstract class PaymentURI {
final String address;
}
+class ExternalAddressURI extends PaymentURI {
+ ExternalAddressURI({required super.amount, required super.address});
+
+ @override
+ String toString() => address;
+}
+
class MoneroURI extends PaymentURI {
MoneroURI({required super.amount, required super.address});
diff --git a/cw_core/lib/wallet_base.dart b/cw_core/lib/wallet_base.dart
index 917d86ef4..89d6356da 100644
--- a/cw_core/lib/wallet_base.dart
+++ b/cw_core/lib/wallet_base.dart
@@ -12,6 +12,7 @@ import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/sync_status.dart';
import 'package:cw_core/node.dart';
import 'package:cw_core/wallet_type.dart';
+import 'package:cw_core/pathForWallet.dart';
abstract class WalletBase {
@@ -115,7 +116,8 @@ abstract class WalletBase null;
- Future renameWalletFiles(String newWalletName);
+ Future renameWalletFiles(String newWalletName) =>
+ copyWalletFilesTo(fromName: walletInfo.name, toName: newWalletName, type: type);
Future signMessage(String message, {String? address = null});
diff --git a/cw_core/lib/wallet_service.dart b/cw_core/lib/wallet_service.dart
index 016f3cc68..8d19a9f8c 100644
--- a/cw_core/lib/wallet_service.dart
+++ b/cw_core/lib/wallet_service.dart
@@ -3,9 +3,12 @@ import 'dart:io';
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/utils/file.dart';
+import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_credentials.dart';
+import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart';
+import 'package:path/path.dart' as p;
abstract class WalletService {
@@ -25,7 +28,30 @@ abstract class WalletService remove(String wallet);
- Future rename(String currentName, String password, String newName);
+ Future rename(String currentName, String password, String newName) async {
+ if (currentName == newName) return;
+
+ final currentWalletInfo = await WalletInfo.get(currentName, getType());
+ if (currentWalletInfo == null) {
+ throw Exception('Wallet not found');
+ }
+
+ await copyWalletFilesTo(fromName: currentName, toName: newName, type: getType());
+ await saveBackup(newName);
+
+ currentWalletInfo.id = WalletBase.idFor(newName, getType());
+ currentWalletInfo.name = newName;
+ await currentWalletInfo.save();
+
+ final oldDir = Directory(p.join(await pathForWalletTypeDir(type: getType()), currentName));
+ if (oldDir.existsSync()) {
+ try {
+ await oldDir.delete(recursive: true);
+ } catch (e) {
+ printV('rename: failed to delete old wallet dir "$currentName": $e');
+ }
+ }
+ }
Future restoreWalletFilesFromBackup(String name) async {
final backupWalletDirPath = await pathForWalletDir(name: "$name.backup", type: getType());
diff --git a/cw_dogecoin/lib/src/dogecoin_wallet_service.dart b/cw_dogecoin/lib/src/dogecoin_wallet_service.dart
index 988abd2e7..1a78e14ff 100644
--- a/cw_dogecoin/lib/src/dogecoin_wallet_service.dart
+++ b/cw_dogecoin/lib/src/dogecoin_wallet_service.dart
@@ -1,12 +1,10 @@
import 'dart:io';
import 'package:bip39/bip39.dart';
-import 'package:collection/collection.dart';
import 'package:cw_bitcoin/bitcoin_mnemonics_bip39.dart';
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/unspent_coins_info.dart';
-import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_service.dart';
import 'package:cw_core/wallet_type.dart';
@@ -100,29 +98,6 @@ class DogeCoinWalletService extends WalletService<
}
}
- @override
- Future rename(String currentName, String password, String newName) async {
- final currentWalletInfo = await WalletInfo.get(currentName, getType());
- if (currentWalletInfo == null) {
- throw Exception('Wallet not found');
- }
- final currentWallet = await DogeCoinWalletBase.open(
- password: password,
- name: currentName,
- walletInfo: currentWalletInfo,
- unspentCoinsInfo: unspentCoinsInfoSource,
- encryptionFileUtils: encryptionFileUtilsFor(isDirect));
-
- await currentWallet.renameWalletFiles(newName);
- await saveBackup(newName);
-
- final newWalletInfo = currentWalletInfo;
- newWalletInfo.id = WalletBase.idFor(newName, getType());
- newWalletInfo.name = newName;
-
- await newWalletInfo.save();
- }
-
@override
Future restoreFromHardwareWallet(DogeCoinNewWalletCredentials credentials) {
throw UnimplementedError(
diff --git a/cw_evm/lib/clients/evm_chain_client.dart b/cw_evm/lib/clients/evm_chain_client.dart
index 2751e94bb..21973e30c 100644
--- a/cw_evm/lib/clients/evm_chain_client.dart
+++ b/cw_evm/lib/clients/evm_chain_client.dart
@@ -618,74 +618,117 @@ class EVMChainClient {
return [];
}
- final uri = Uri.https(
- 'deep-index.moralis.io',
- '/api/v2.2/$address/erc20',
- {
- "chain": chainName,
- },
- );
-
- final response = await client.get(
- uri,
- headers: {
- "Accept": "application/json",
- "X-API-Key": secrets.moralisApiKey,
- },
- );
-
- if (response.statusCode < 200 || response.statusCode >= 300) {
- printV('Moralis API returned invalid status code: ${response.statusCode}');
- return [];
- }
-
- final decodedResponse = jsonDecode(response.body) as List;
-
+ const maxPages = 3;
+ String? cursor;
+ int pageCount = 0;
final List tokens = [];
- for (final item in decodedResponse) {
- final tokenData = item as Map;
+ do {
+ final params = {
+ "chain": chainName,
+ if (cursor != null && cursor.isNotEmpty) "cursor": cursor,
+ };
- final balanceStr = tokenData['balance'] as String? ?? '0';
- final balanceWei = BigInt.tryParse(balanceStr) ?? BigInt.zero;
- if (balanceWei == BigInt.zero) continue;
-
- final contractAddress = (tokenData['token_address'] as String? ?? '').toLowerCase();
- final name = (tokenData['name'] as String? ?? '').toString();
- final symbol = (tokenData['symbol'] as String? ?? '').toString();
- final symbolFiltered = symbol.replaceFirst(RegExp('^\\\$'), '');
-
- final decimalsRaw = tokenData['decimals'];
- final decimals =
- decimalsRaw is int ? decimalsRaw : int.tryParse(decimalsRaw.toString()) ?? 18;
-
- final logo = tokenData['logo'] as String?;
- final thumbnail = tokenData['thumbnail'] as String?;
- final iconUrl = logo ?? thumbnail;
-
- final possibleSpamRaw = tokenData['possible_spam'];
- final possibleSpam = possibleSpamRaw is bool
- ? possibleSpamRaw
- : (possibleSpamRaw.toString().toLowerCase() == 'true');
-
- final verifiedContractRaw = tokenData['verified_contract'];
- final verifiedContract = verifiedContractRaw is bool
- ? verifiedContractRaw
- : (verifiedContractRaw.toString().toLowerCase() == 'true');
-
- tokens.add(
- MoralisWalletTokenBalance(
- contractAddress: contractAddress,
- name: name,
- symbol: symbolFiltered,
- decimals: decimals,
- iconUrl: iconUrl,
- balanceWei: balanceWei,
- possibleSpam: possibleSpam,
- verifiedContract: verifiedContract,
- ),
+ final uri = Uri.https(
+ 'deep-index.moralis.io',
+ '/api/v2.2/wallets/$address/tokens',
+ params,
);
- }
+
+ final response = await client.get(
+ uri,
+ headers: {
+ "Accept": "application/json",
+ "X-API-Key": secrets.moralisApiKey,
+ },
+ );
+
+ if (response.statusCode < 200 || response.statusCode >= 300) {
+ printV('Moralis API returned invalid status code: ${response.statusCode}');
+ return tokens;
+ }
+
+ final decoded = jsonDecode(response.body);
+ if (decoded is! Map) return tokens;
+
+ final result = decoded['result'];
+ if (result is! List) return tokens;
+
+ for (final item in result) {
+ if (item is! Map) continue;
+ final tokenData = item;
+
+ final nativeRaw = tokenData['native_token'];
+ final nativeToken = nativeRaw is bool
+ ? nativeRaw
+ : (nativeRaw?.toString().toLowerCase() == 'true');
+ if (nativeToken) continue;
+
+ final balanceStr = tokenData['balance'] as String? ?? '0';
+ final balanceWei = BigInt.tryParse(balanceStr) ?? BigInt.zero;
+ if (balanceWei == BigInt.zero) continue;
+
+ final contractAddress = (tokenData['token_address'] as String? ?? '').toLowerCase();
+ final name = (tokenData['name'] as String? ?? '').toString();
+ final symbol = (tokenData['symbol'] as String? ?? '').toString();
+ final symbolFiltered = symbol.replaceFirst(RegExp('^\\\$'), '');
+
+ final decimalsRaw = tokenData['decimals'];
+ final decimals =
+ decimalsRaw is int ? decimalsRaw : int.tryParse(decimalsRaw.toString()) ?? 18;
+
+ final logo = tokenData['logo'] as String?;
+ final thumbnail = tokenData['thumbnail'] as String?;
+ final iconUrl = logo ?? thumbnail;
+
+ final possibleSpamRaw = tokenData['possible_spam'];
+ final possibleSpam = possibleSpamRaw is bool
+ ? possibleSpamRaw
+ : (possibleSpamRaw?.toString().toLowerCase() == 'true');
+
+ final verifiedContractRaw = tokenData['verified_contract'];
+ final verifiedContract = verifiedContractRaw is bool
+ ? verifiedContractRaw
+ : (verifiedContractRaw?.toString().toLowerCase() == 'true');
+
+ final usdPriceRaw = tokenData['usd_price'];
+ final double? usdPrice = usdPriceRaw is num
+ ? usdPriceRaw.toDouble()
+ : (usdPriceRaw is String ? double.tryParse(usdPriceRaw) : null);
+
+ final usdValueRaw = tokenData['usd_value'];
+ final double? usdValue = usdValueRaw is num
+ ? usdValueRaw.toDouble()
+ : (usdValueRaw is String ? double.tryParse(usdValueRaw) : null);
+
+ final securityRaw = tokenData['security_score'];
+ final int? securityScore = securityRaw is int
+ ? securityRaw
+ : (securityRaw is num
+ ? securityRaw.toInt()
+ : (securityRaw is String ? int.tryParse(securityRaw) : null));
+
+ tokens.add(
+ MoralisWalletTokenBalance(
+ contractAddress: contractAddress,
+ name: name,
+ symbol: symbolFiltered,
+ decimals: decimals,
+ iconUrl: iconUrl,
+ balanceWei: balanceWei,
+ possibleSpam: possibleSpam,
+ verifiedContract: verifiedContract,
+ usdPrice: usdPrice,
+ usdValue: usdValue,
+ securityScore: securityScore,
+ ),
+ );
+ }
+
+ final nextCursor = decoded['cursor'];
+ cursor = nextCursor is String && nextCursor.isNotEmpty ? nextCursor : null;
+ pageCount++;
+ } while (cursor != null && pageCount < maxPages);
return tokens;
} catch (e, stackTrace) {
@@ -737,6 +780,9 @@ class MoralisWalletTokenBalance {
final BigInt balanceWei;
final bool possibleSpam;
final bool verifiedContract;
+ final double? usdPrice;
+ final double? usdValue;
+ final int? securityScore;
MoralisWalletTokenBalance({
required this.contractAddress,
@@ -747,5 +793,8 @@ class MoralisWalletTokenBalance {
required this.balanceWei,
required this.possibleSpam,
required this.verifiedContract,
+ this.usdPrice,
+ this.usdValue,
+ this.securityScore,
});
}
diff --git a/cw_evm/lib/evm_chain_wallet.dart b/cw_evm/lib/evm_chain_wallet.dart
index 4b002e396..d404013fb 100644
--- a/cw_evm/lib/evm_chain_wallet.dart
+++ b/cw_evm/lib/evm_chain_wallet.dart
@@ -1,6 +1,5 @@
import 'dart:async';
import 'dart:convert';
-import 'dart:io';
import 'dart:typed_data';
import 'package:bip32/bip32.dart' as bip32;
@@ -545,67 +544,112 @@ abstract class EVMChainWalletBase
await save();
}
- bool isTokenPropertiesSuspicious(Erc20Token token) {
- bool isTokenWhitelisted = getDefaultTokenContractAddresses
- .any((element) => element.toLowerCase() == token.contractAddress.toLowerCase());
+ static const _urlLikeSuspiciousMarkers = [
+ 't.me',
+ '.me',
+ 'telegram',
+ 'http',
+ 'https',
+ '.com',
+ '.org',
+ '.top',
+ '.live',
+ '.xyz',
+ 'www',
+ 'π',
+ 'airdrop',
+ 'distribution',
+ ];
- final defaultTokenSymbols = EVMChainDefaultTokens.getDefaultTokenSymbols(selectedChainId);
+ static final _suspiciousWordPattern =
+ RegExp(r'\b(bot|claim|reward)\b', caseSensitive: false);
+
+ static const _knownNonEvmNativeSymbols = {
+ 'ICP',
+ 'SOL',
+ 'TRX',
+ 'ATOM',
+ 'DOT',
+ 'ADA',
+ 'XRP',
+ 'XLM',
+ 'XMR',
+ 'ALGO',
+ 'NEAR',
+ 'TON',
+ 'HBAR',
+ 'APT',
+ 'SUI',
+ 'KAS',
+ };
+
+ static bool _hasSuspiciousData(String normalized) {
+ final lower = normalized.toLowerCase();
+ if (_urlLikeSuspiciousMarkers.any(lower.contains)) return true;
+ return _suspiciousWordPattern.hasMatch(lower);
+ }
+
+ bool isTokenPropertiesSuspicious(
+ Erc20Token token, {
+ Set? cachedWhitelistLower,
+ Set? cachedDefaultSymbolsUpper,
+ }) {
+ final whitelistLower = cachedWhitelistLower ??
+ getDefaultTokenContractAddresses.map((a) => a.toLowerCase()).toSet();
+ final defaultSymbolsUpper = cachedDefaultSymbolsUpper ??
+ EVMChainDefaultTokens.getDefaultTokenSymbols(selectedChainId).toSet();
+
+ final isTokenWhitelisted = whitelistLower.contains(token.contractAddress.toLowerCase());
- // Normalize the token data to check for homoglyph spoofing attack, characters that look like ASCII (Cyrillic, Greek, etc.)
final normalizedName = normalizeHomoglyphs(token.name.trim().toUpperCase());
final normalizedSymbol = normalizeHomoglyphs(token.symbol.trim().toUpperCase());
final normalizedTitle = normalizeHomoglyphs(token.title.trim().toUpperCase());
- final suspiciousStrings = [
- 't.me',
- '.me',
- 'telegram',
- 'http',
- 'https',
- '.com',
- '.org',
- '.top',
- '.live',
- 'airdrop',
- 'reward',
- 'distribution',
- 'www',
- '.xyz',
- 'π',
- 'bot',
- 'claim',
- 'reward',
- ];
-
- final hasSuspiciousData = suspiciousStrings.any(
- (element) =>
- normalizedName.toLowerCase().contains(element) ||
- normalizedSymbol.toLowerCase().contains(element) ||
- normalizedTitle.toLowerCase().contains(element),
- );
+ final hasSuspiciousData = _hasSuspiciousData(normalizedName) ||
+ _hasSuspiciousData(normalizedSymbol) ||
+ _hasSuspiciousData(normalizedTitle);
final nativeSymbol = currency.title.toUpperCase();
final hasSuspiciousNativeSymbol = normalizedSymbol == nativeSymbol && !isTokenWhitelisted;
final hasSuspiciousDefaultTokenSymbol =
- defaultTokenSymbols.contains(normalizedSymbol) && !isTokenWhitelisted;
+ defaultSymbolsUpper.contains(normalizedSymbol) && !isTokenWhitelisted;
- return hasSuspiciousData || hasSuspiciousNativeSymbol || hasSuspiciousDefaultTokenSymbol;
+ final hasSuspiciousNonEvmNativeSymbol =
+ _knownNonEvmNativeSymbols.contains(normalizedSymbol) && !isTokenWhitelisted;
+
+ return hasSuspiciousData ||
+ hasSuspiciousNativeSymbol ||
+ hasSuspiciousDefaultTokenSymbol ||
+ hasSuspiciousNonEvmNativeSymbol;
}
- Future _checkForExistingScamTokens() async {
- for (var token in erc20Currencies) {
- bool isPotentialScam = false;
+ String get _scamCheckDoneKey => 'evm_scam_check_v2_done_${walletInfo.name}';
- if (isTokenPropertiesSuspicious(token)) {
- isPotentialScam = true;
+ Future _checkForExistingScamTokens() async {
+ final prefs = await sharedPrefs.future;
+ if (prefs.getBool(_scamCheckDoneKey) == true) return;
+
+ final whitelistLower =
+ getDefaultTokenContractAddresses.map((a) => a.toLowerCase()).toSet();
+ final defaultSymbolsUpper =
+ EVMChainDefaultTokens.getDefaultTokenSymbols(selectedChainId).toSet();
+
+ for (var token in erc20Currencies) {
+ final suspicious = isTokenPropertiesSuspicious(
+ token,
+ cachedWhitelistLower: whitelistLower,
+ cachedDefaultSymbolsUpper: defaultSymbolsUpper,
+ );
+
+ if (suspicious && !token.isPotentialScam) {
token.isPotentialScam = true;
token.iconPath = null;
await token.save();
+ continue;
}
- // For fixing wrongly classified tokens
- if (!isPotentialScam && token.isPotentialScam) {
+ if (!suspicious && token.isPotentialScam) {
token.isPotentialScam = false;
if (token.iconPath == null || token.iconPath!.isEmpty) {
@@ -621,6 +665,8 @@ abstract class EVMChainWalletBase
await token.save();
}
}
+
+ await prefs.setBool(_scamCheckDoneKey, true);
}
Future discoverTokensFromMoralis() async {
@@ -672,6 +718,9 @@ abstract class EVMChainWalletBase
DiscoveredToken(
token: newToken,
balanceWei: token.balanceWei,
+ verifiedContract: token.verifiedContract,
+ moralisUsdPrice: token.usdPrice,
+ moralisUsdValue: token.usdValue,
),
);
}
@@ -1653,30 +1702,6 @@ abstract class EVMChainWalletBase
);
}
- @override
- Future renameWalletFiles(String newWalletName) async {
- final transactionHistoryFileNameForWallet = getTransactionHistoryFileName();
-
- final currentWalletPath = await pathForWallet(name: walletInfo.name, type: type);
- final currentWalletFile = File(currentWalletPath);
-
- final currentDirPath = await pathForWalletDir(name: walletInfo.name, type: type);
- final currentTransactionsFile = File('$currentDirPath/$transactionHistoryFileNameForWallet');
-
- // Copies current wallet files into new wallet name's dir and files
- if (currentWalletFile.existsSync()) {
- final newWalletPath = await pathForWallet(name: newWalletName, type: type);
- await currentWalletFile.copy(newWalletPath);
- }
- if (currentTransactionsFile.existsSync()) {
- final newDirPath = await pathForWalletDir(name: newWalletName, type: type);
- await currentTransactionsFile.copy('$newDirPath/$transactionHistoryFileNameForWallet');
- }
-
- // Delete old name's dir and files
- await Directory(currentDirPath).delete(recursive: true);
- }
-
void _setTransactionUpdateTimer() {
if (_transactionsUpdateTimer?.isActive ?? false) {
_transactionsUpdateTimer!.cancel();
@@ -1757,10 +1782,16 @@ class GasParamsHandler {
class DiscoveredToken {
final Erc20Token token;
final BigInt balanceWei;
+ final bool verifiedContract;
+ final double? moralisUsdPrice;
+ final double? moralisUsdValue;
const DiscoveredToken({
required this.token,
required this.balanceWei,
+ required this.verifiedContract,
+ this.moralisUsdPrice,
+ this.moralisUsdValue,
});
}
diff --git a/cw_evm/lib/evm_chain_wallet_service.dart b/cw_evm/lib/evm_chain_wallet_service.dart
index d63686225..425bdceb8 100644
--- a/cw_evm/lib/evm_chain_wallet_service.dart
+++ b/cw_evm/lib/evm_chain_wallet_service.dart
@@ -3,10 +3,12 @@ import 'dart:io';
import 'package:bip39/bip39.dart' as bip39;
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_core/pathForWallet.dart';
+import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_service.dart';
import 'package:cw_core/wallet_type.dart';
+import 'package:path/path.dart' as p;
import 'package:cw_evm/clients/evm_chain_client.dart';
import 'package:cw_evm/evm_chain_client_factory.dart';
import 'package:cw_evm/evm_chain_registry.dart';
@@ -166,29 +168,30 @@ class EVMChainWalletService extends WalletService<
@override
Future rename(String currentName, String password, String newName) async {
+ if (currentName == newName) return;
+
final currentWalletInfo = await _findWalletByName(currentName);
if (currentWalletInfo == null) {
throw Exception('Wallet not found');
}
- final currentWallet = await _openWalletInstance(
- password: password,
- name: currentName,
- walletInfo: currentWalletInfo,
- encryptionFileUtils: encryptionFileUtilsFor(isDirect),
- );
+ final type = currentWalletInfo.type;
- await currentWallet.renameWalletFiles(newName);
+ await copyWalletFilesTo(fromName: currentName, toName: newName, type: type);
+ await saveBackup(newName, walletInfo: currentWalletInfo);
- // Update walletInfo with new name before saving backup
- final newWalletInfo = currentWalletInfo;
- newWalletInfo.id = WalletBase.idFor(newName, currentWalletInfo.type);
- newWalletInfo.name = newName;
+ currentWalletInfo.id = WalletBase.idFor(newName, type);
+ currentWalletInfo.name = newName;
+ await currentWalletInfo.save();
- // Pass walletInfo to saveBackup to avoid lookup (since WalletInfo not saved yet)
- await saveBackup(newName, walletInfo: newWalletInfo);
-
- await newWalletInfo.save();
+ final oldDir = Directory(p.join(await pathForWalletTypeDir(type: type), currentName));
+ if (oldDir.existsSync()) {
+ try {
+ await oldDir.delete(recursive: true);
+ } catch (e) {
+ printV('rename: failed to delete old wallet dir "$currentName": $e');
+ }
+ }
}
@override
diff --git a/cw_evm/pubspec.yaml b/cw_evm/pubspec.yaml
index 0186841c5..5cc563e6a 100644
--- a/cw_evm/pubspec.yaml
+++ b/cw_evm/pubspec.yaml
@@ -21,6 +21,7 @@ dependencies:
http: ^1.1.0
hive: ^2.2.3
collection: ^1.17.1
+ path: ^1.8.0
shared_preferences: ^2.0.15
mobx: ^2.0.7+4
cw_core:
diff --git a/cw_solana/lib/solana_client.dart b/cw_solana/lib/solana_client.dart
index ad72c74a9..3bea89e76 100644
--- a/cw_solana/lib/solana_client.dart
+++ b/cw_solana/lib/solana_client.dart
@@ -3,6 +3,7 @@ import 'dart:convert';
import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:cw_core/amount/money.dart';
+import 'package:cw_core/amount/money_double.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/currency.dart';
import 'package:cw_core/node.dart';
@@ -654,7 +655,7 @@ class SolanaWalletClient {
from: outgoingFrom,
to: outgoingTo,
id: outgoingId,
- amount: Money.parse(outgoingAmount.toStringAsFixed(outgoingToken.decimals), outgoingToken),
+ amount: outgoingAmount.toMoney(outgoingToken),
programId: outgoingMintAddress == null
? SystemProgramConst.programId.address
: SPLTokenProgramConst.tokenProgramId.address,
@@ -672,7 +673,7 @@ class SolanaWalletClient {
from: incomingFrom,
to: incomingTo,
id: incomingId,
- amount: Money.parse(incomingAmount.toStringAsFixed(incomingToken.decimals), incomingToken),
+ amount: incomingAmount.toMoney(incomingToken),
programId: incomingMintAddress == null
? SystemProgramConst.programId.address
: SPLTokenProgramConst.tokenProgramId.address,
@@ -869,8 +870,7 @@ class SolanaWalletClient {
from: sender,
to: receiver,
id: signature,
- amount: Money.parse(amount.toStringAsFixed((splToken ?? CryptoCurrency.sol).decimals),
- splToken ?? CryptoCurrency.sol),
+ amount: amount.toMoney(splToken ?? CryptoCurrency.sol),
programId: SPLTokenProgramConst.tokenProgramId.address,
blockTimeInInt: blockTime?.toInt() ?? 0,
fee: Money.fromInt(fee, CryptoCurrency.sol),
diff --git a/cw_solana/lib/solana_wallet.dart b/cw_solana/lib/solana_wallet.dart
index 48a1b0e71..b5b9309fb 100644
--- a/cw_solana/lib/solana_wallet.dart
+++ b/cw_solana/lib/solana_wallet.dart
@@ -1,6 +1,5 @@
import 'dart:async';
import 'dart:convert';
-import 'dart:io';
import 'package:cw_core/amount/money.dart';
import 'package:cw_core/cake_hive.dart';
@@ -12,6 +11,7 @@ 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_priority.dart';
+import 'package:cw_core/utils/homoglyph_normalizer.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_addresses.dart';
import 'package:cw_core/wallet_base.dart';
@@ -135,6 +135,8 @@ abstract class SolanaWalletBase
splTokensBox = await CakeHive.openBox(boxName);
+ await _checkForExistingScamTokens();
+
// Create the privatekey using either the mnemonic or the privateKey
_solanaPrivateKey = await getPrivateKey(
mnemonic: _mnemonic,
@@ -153,6 +155,36 @@ abstract class SolanaWalletBase
await save();
}
+ String get _scamCheckDoneKey => 'solana_scam_check_v2_done_${walletInfo.name}';
+
+ Future _checkForExistingScamTokens() async {
+ if (!splTokensBox.isOpen) return;
+
+ final prefs = await _sharedPrefs.future;
+ if (prefs.getBool(_scamCheckDoneKey) == true) return;
+
+ final defaultMints =
+ DefaultSPLTokens().initialSPLTokens.map((t) => t.mintAddress).toSet();
+ final defaultSymbolsUpper = DefaultSPLTokens()
+ .initialSPLTokens
+ .map((t) => t.symbol.toUpperCase())
+ .toSet();
+
+ for (final token in splTokensBox.values) {
+ final suspicious = isTokenPropertiesSuspicious(
+ token,
+ cachedDefaultMints: defaultMints,
+ cachedDefaultSymbolsUpper: defaultSymbolsUpper,
+ );
+ if (suspicious && !token.isPotentialScam) {
+ token.isPotentialScam = true;
+ await token.save();
+ }
+ }
+
+ await prefs.setBool(_scamCheckDoneKey, true);
+ }
+
Future getPrivateKey({
String? mnemonic,
String? privateKey,
@@ -671,7 +703,98 @@ abstract class SolanaWalletBase
}
}
+ static const _urlLikeSuspiciousMarkers = [
+ 't.me',
+ '.me',
+ 'telegram',
+ 'http',
+ 'https',
+ '.com',
+ '.org',
+ '.top',
+ '.live',
+ '.xyz',
+ 'www',
+ 'π',
+ 'airdrop',
+ 'distribution',
+ ];
+
+ static final _suspiciousWordPattern =
+ RegExp(r'\b(bot|claim|reward)\b', caseSensitive: false);
+
+ static const _knownNonSolanaNativeSymbols = {
+ 'BTC',
+ 'ETH',
+ 'BNB',
+ 'AVAX',
+ 'MATIC',
+ 'POL',
+ 'ICP',
+ 'TRX',
+ 'ATOM',
+ 'DOT',
+ 'ADA',
+ 'XRP',
+ 'XLM',
+ 'XMR',
+ 'ALGO',
+ 'NEAR',
+ 'TON',
+ 'HBAR',
+ 'APT',
+ 'SUI',
+ 'KAS',
+ };
+
+ static bool _hasSuspiciousData(String normalized) {
+ final lower = normalized.toLowerCase();
+ if (_urlLikeSuspiciousMarkers.any(lower.contains)) return true;
+ return _suspiciousWordPattern.hasMatch(lower);
+ }
+
+ bool isTokenPropertiesSuspicious(
+ SPLToken token, {
+ Set? cachedDefaultMints,
+ Set? cachedDefaultSymbolsUpper,
+ }) {
+ final defaultMints = cachedDefaultMints ??
+ DefaultSPLTokens().initialSPLTokens.map((t) => t.mintAddress).toSet();
+ final defaultSymbolsUpper = cachedDefaultSymbolsUpper ??
+ DefaultSPLTokens()
+ .initialSPLTokens
+ .map((t) => t.symbol.toUpperCase())
+ .toSet();
+
+ final isTokenWhitelisted = defaultMints.contains(token.mintAddress);
+
+ final normalizedName = normalizeHomoglyphs(token.name.trim().toUpperCase());
+ final normalizedSymbol = normalizeHomoglyphs(token.symbol.trim().toUpperCase());
+ final normalizedTitle = normalizeHomoglyphs(token.title.trim().toUpperCase());
+
+ final hasSuspiciousData = _hasSuspiciousData(normalizedName) ||
+ _hasSuspiciousData(normalizedSymbol) ||
+ _hasSuspiciousData(normalizedTitle);
+
+ const nativeSymbol = 'SOL';
+ final hasSuspiciousNativeSymbol = normalizedSymbol == nativeSymbol && !isTokenWhitelisted;
+
+ final hasSuspiciousDefaultTokenSymbol =
+ defaultSymbolsUpper.contains(normalizedSymbol) && !isTokenWhitelisted;
+
+ final hasSuspiciousNonSolanaNativeSymbol =
+ _knownNonSolanaNativeSymbols.contains(normalizedSymbol) && !isTokenWhitelisted;
+
+ return hasSuspiciousData ||
+ hasSuspiciousNativeSymbol ||
+ hasSuspiciousDefaultTokenSymbol ||
+ hasSuspiciousNonSolanaNativeSymbol;
+ }
+
Future addSPLToken(SPLToken token) async {
+ final isSuspicious = isTokenPropertiesSuspicious(token);
+ token.isPotentialScam = token.isPotentialScam || isSuspicious;
+
await splTokensBox.put(token.mintAddress, token);
if (token.enabled) {
@@ -709,28 +832,6 @@ abstract class SolanaWalletBase
}
}
- @override
- Future renameWalletFiles(String newWalletName) async {
- final currentWalletPath = await pathForWallet(name: walletInfo.name, type: type);
- final currentWalletFile = File(currentWalletPath);
-
- final currentDirPath = await pathForWalletDir(name: walletInfo.name, type: type);
- final currentTransactionsFile = File('$currentDirPath/$transactionsHistoryFileName');
-
- // Copies current wallet files into new wallet name's dir and files
- if (currentWalletFile.existsSync()) {
- final newWalletPath = await pathForWallet(name: newWalletName, type: type);
- await currentWalletFile.copy(newWalletPath);
- }
- if (currentTransactionsFile.existsSync()) {
- final newDirPath = await pathForWalletDir(name: newWalletName, type: type);
- await currentTransactionsFile.copy('$newDirPath/$transactionsHistoryFileName');
- }
-
- // Delete old name's dir and files
- await Directory(currentDirPath).delete(recursive: true);
- }
-
void _setTransactionUpdateTimer() {
if (_transactionsUpdateTimer?.isActive ?? false) {
_transactionsUpdateTimer!.cancel();
diff --git a/cw_solana/lib/solana_wallet_service.dart b/cw_solana/lib/solana_wallet_service.dart
index a16f3ee6f..d1fce3b20 100644
--- a/cw_solana/lib/solana_wallet_service.dart
+++ b/cw_solana/lib/solana_wallet_service.dart
@@ -1,7 +1,6 @@
import 'dart:io';
import 'package:bip39/bip39.dart' as bip39;
-import 'package:collection/collection.dart';
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_core/balance.dart';
import 'package:cw_core/pathForWallet.dart';
@@ -14,7 +13,6 @@ import 'package:cw_core/wallet_type.dart';
import 'package:cw_solana/solana_mnemonics.dart';
import 'package:cw_solana/solana_wallet.dart';
import 'package:cw_solana/solana_wallet_creation_credentials.dart';
-import 'package:hive/hive.dart';
class SolanaWalletService extends WalletService {
@@ -138,29 +136,6 @@ class SolanaWalletService extends WalletService rename(String currentName, String password, String newName) async {
- final currentWalletInfo = await WalletInfo.get(currentName, getType());
- if (currentWalletInfo == null) {
- throw Exception('Wallet not found');
- }
- final currentWallet = await SolanaWalletBase.open(
- password: password,
- name: currentName,
- walletInfo: currentWalletInfo,
- encryptionFileUtils: encryptionFileUtilsFor(isDirect),
- );
-
- await currentWallet.renameWalletFiles(newName);
- await saveBackup(newName);
-
- final newWalletInfo = currentWalletInfo;
- newWalletInfo.id = WalletBase.idFor(newName, getType());
- newWalletInfo.name = newName;
-
- await newWalletInfo.save();
- }
-
@override
Future, TransactionInfo>> restoreFromHardwareWallet(SolanaNewWalletCredentials credentials) {
// TODO: implement restoreFromHardwareWallet
diff --git a/cw_tron/lib/tron_wallet.dart b/cw_tron/lib/tron_wallet.dart
index 1a4d98b48..24fa83f15 100644
--- a/cw_tron/lib/tron_wallet.dart
+++ b/cw_tron/lib/tron_wallet.dart
@@ -1,7 +1,6 @@
import 'dart:async';
import 'dart:convert';
import 'dart:developer';
-import 'dart:io';
import 'package:bip39/bip39.dart' as bip39;
import 'package:blockchain_utils/blockchain_utils.dart';
@@ -607,30 +606,6 @@ abstract class TronWalletBase
Future getTronToken(String contractAddress) async =>
await _client.getTronToken(contractAddress, _tronAddress);
- @override
- Future renameWalletFiles(String newWalletName) async {
- const transactionHistoryFileNameForWallet = 'tron_transactions.json';
-
- final currentWalletPath = await pathForWallet(name: walletInfo.name, type: type);
- final currentWalletFile = File(currentWalletPath);
-
- final currentDirPath = await pathForWalletDir(name: walletInfo.name, type: type);
- final currentTransactionsFile = File('$currentDirPath/$transactionHistoryFileNameForWallet');
-
- // Copies current wallet files into new wallet name's dir and files
- if (currentWalletFile.existsSync()) {
- final newWalletPath = await pathForWallet(name: newWalletName, type: type);
- await currentWalletFile.copy(newWalletPath);
- }
- if (currentTransactionsFile.existsSync()) {
- final newDirPath = await pathForWalletDir(name: newWalletName, type: type);
- await currentTransactionsFile.copy('$newDirPath/$transactionHistoryFileNameForWallet');
- }
-
- // Delete old name's dir and files
- await Directory(currentDirPath).delete(recursive: true);
- }
-
void _setTransactionUpdateTimer() {
if (_transactionsUpdateTimer?.isActive ?? false) {
_transactionsUpdateTimer!.cancel();
diff --git a/cw_tron/lib/tron_wallet_service.dart b/cw_tron/lib/tron_wallet_service.dart
index cf83b9158..9b81a9a57 100644
--- a/cw_tron/lib/tron_wallet_service.dart
+++ b/cw_tron/lib/tron_wallet_service.dart
@@ -14,7 +14,6 @@ import 'package:cw_tron/tron_client.dart';
import 'package:cw_tron/tron_exception.dart';
import 'package:cw_tron/tron_wallet.dart';
import 'package:cw_tron/tron_wallet_creation_credentials.dart';
-import 'package:hive/hive.dart';
class TronWalletService extends WalletService<
TronNewWalletCredentials,
@@ -134,29 +133,6 @@ class TronWalletService extends WalletService<
return wallet;
}
- @override
- Future rename(String currentName, String password, String newName) async {
- final currentWalletInfo = await WalletInfo.get(currentName, getType());
- if (currentWalletInfo == null) {
- throw Exception('Wallet not found');
- }
- final currentWallet = await TronWalletBase.open(
- password: password,
- name: currentName,
- walletInfo: currentWalletInfo,
- encryptionFileUtils: encryptionFileUtilsFor(isDirect),
- );
-
- await currentWallet.renameWalletFiles(newName);
- await saveBackup(newName);
-
- final newWalletInfo = currentWalletInfo;
- newWalletInfo.id = WalletBase.idFor(newName, getType());
- newWalletInfo.name = newName;
-
- await newWalletInfo.save();
- }
-
@override
Future isWalletExit(String name) async =>
File(await pathForWallet(name: name, type: getType())).existsSync();
diff --git a/lib/cake_pay/src/cards/cake_pay_buy_card_page.dart b/lib/cake_pay/src/cards/cake_pay_buy_card_page.dart
index 5442dcff1..8380897b9 100644
--- a/lib/cake_pay/src/cards/cake_pay_buy_card_page.dart
+++ b/lib/cake_pay/src/cards/cake_pay_buy_card_page.dart
@@ -11,8 +11,8 @@ import 'package:cake_wallet/cake_pay/src/widgets/link_extractor.dart';
import 'package:cake_wallet/cake_pay/src/widgets/rounded_overlay_cards_widget.dart';
import 'package:cake_wallet/cake_pay/src/widgets/text_icon_button.dart';
import 'package:cake_wallet/cake_pay/src/widgets/three_checkbox_alert_content_widget.dart';
+import 'package:cake_wallet/core/address_resolver/parsed_address.dart';
import 'package:cake_wallet/core/execution_state.dart';
-import 'package:cake_wallet/entities/parsed_address.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/keyboard_hide_overlay.dart';
import 'package:cake_wallet/routes.dart';
@@ -584,8 +584,11 @@ class CakePayBuyCardPage extends BasePage {
final displayingOutputs = _sendViewModel.outputs
.map((o) => o.OutputCopyWithParsedAddress(
parsedAddress: ParsedAddress(
- addresses: [o.address],
- name: 'Cake Pay',
+ parsedAddressByCurrencyMap: {
+ cakePayBuyCardViewModel.sendViewModel.selectedCryptoCurrency:
+ o.address,
+ },
+ handle: 'Cake Pay',
profileName: order?.cards.first.cardName ?? 'Cake Pay',
profileImageUrl: order?.cards.first.cardImagePath ?? '',
),
@@ -653,8 +656,11 @@ class CakePayBuyCardPage extends BasePage {
final displayingOutputs = outputsCopy
.map((o) => o.OutputCopyWithParsedAddress(
parsedAddress: ParsedAddress(
- addresses: [o.address],
- name: 'Cake Pay',
+ parsedAddressByCurrencyMap: {
+ cakePayBuyCardViewModel.sendViewModel.selectedCryptoCurrency:
+ o.address,
+ },
+ handle: 'Cake Pay',
profileName: order?.cards.first.cardName ?? 'Cake Pay',
profileImageUrl: order?.cards.first.cardImagePath ?? '',
),
diff --git a/lib/core/address_resolver/address_lookup_provider.dart b/lib/core/address_resolver/address_lookup_provider.dart
new file mode 100644
index 000000000..9feff5610
--- /dev/null
+++ b/lib/core/address_resolver/address_lookup_provider.dart
@@ -0,0 +1,22 @@
+import 'package:cake_wallet/core/address_resolver/address_sources.dart';
+import 'package:cake_wallet/core/address_resolver/parsed_address.dart';
+import 'package:cake_wallet/store/settings_store.dart';
+import 'package:cw_core/crypto_currency.dart';
+import 'package:cw_core/wallet_base.dart';
+
+abstract class AddressLookupProvider {
+
+ AddressSource get source;
+
+ List get supportedCurrencies;
+
+ bool isEnabled(SettingsStore settingsStore);
+
+ bool canHandle(String query);
+
+ Future> resolve({
+ required String query,
+ required List currencies,
+ required WalletBase wallet,
+ });
+}
diff --git a/lib/core/address_resolver/address_resolver_service.dart b/lib/core/address_resolver/address_resolver_service.dart
new file mode 100644
index 000000000..8592c53c6
--- /dev/null
+++ b/lib/core/address_resolver/address_resolver_service.dart
@@ -0,0 +1,143 @@
+import 'package:cake_wallet/core/address_resolver/address_lookup_provider.dart';
+import 'package:cake_wallet/core/address_resolver/address_sources.dart';
+import 'package:cake_wallet/core/address_resolver/bip_353/bip_353_address_provider.dart';
+import 'package:cake_wallet/core/address_resolver/ens/ens_address_provider.dart';
+import 'package:cake_wallet/core/address_resolver/fio/fio_address_provider.dart';
+import 'package:cake_wallet/core/address_resolver/lnurl_pay/lnurl_pay_address_provider.dart';
+import 'package:cake_wallet/core/address_resolver/mastodon/mastodon_address_provider.dart';
+import 'package:cake_wallet/core/address_resolver/nostr/nostr_address_provider.dart';
+import 'package:cake_wallet/core/address_resolver/openalias/openalias_address_provider.dart';
+import 'package:cake_wallet/core/address_resolver/parsed_address.dart';
+import 'package:cake_wallet/core/address_resolver/thorchain/thorchain_address_provider.dart';
+import 'package:cake_wallet/core/address_resolver/twitter/twitter_address_provider.dart';
+import 'package:cake_wallet/core/address_resolver/unstoppable/unstoppable_address_provider.dart';
+import 'package:cake_wallet/core/address_resolver/wellknown/wellknown_address_provider.dart';
+import 'package:cake_wallet/core/address_resolver/yat/yat_address_provider.dart';
+import 'package:cake_wallet/core/address_resolver/zano/zano_alias_address_provider.dart';
+import 'package:cake_wallet/core/address_resolver/zcash/zcash_address_provider.dart';
+import 'package:cake_wallet/core/address_resolver/zcash/zcash_me_address_provider.dart';
+import 'package:cake_wallet/store/settings_store.dart';
+import 'package:cw_core/crypto_currency.dart';
+import 'package:cw_core/utils/print_verbose.dart';
+import 'package:cw_core/wallet_base.dart';
+
+class AddressResolverService {
+ AddressResolverService({required this.settingsStore});
+
+ final SettingsStore settingsStore;
+
+// Sources available for address resolution and lookup settings.
+ static const supportedSources = [
+ AddressSource.lnurlPay,
+ AddressSource.twitter,
+ AddressSource.mastodon,
+ AddressSource.yatRecord,
+ AddressSource.unstoppableDomains,
+ AddressSource.openAlias,
+ AddressSource.ens,
+ AddressSource.zcashName,
+ AddressSource.zcashAddress,
+ AddressSource.wellKnown,
+ AddressSource.zanoAlias,
+ AddressSource.bip353,
+ AddressSource.fio,
+ AddressSource.thorChain,
+ AddressSource.nostr,
+ ];
+
+ List _buildProviders() {
+ final allProviders = [
+ LNUrlPayAddressProvider(),
+ TwitterAddressProvider(),
+ MastodonAddressProvider(),
+ YatAddressProvider(),
+ UnstoppableAddressProvider(),
+ OpenaliasAddressProvider(),
+ EnsAddressProvider(),
+ ZcashNameAddressProvider(),
+ ZcashMeAddressProvider(),
+ WellKnownAddressProvider(),
+ ZanoAliasAddressProvider(),
+ Bip353AddressProvider(),
+ FioAddressProvider(),
+ ThorchainAddressProvider(),
+ NostrAddressProvider(),
+ ];
+
+ return allProviders.where((provider) => supportedSources.contains(provider.source)).toList();
+ }
+
+ Future> resolve({
+ required String query,
+ required WalletBase wallet,
+ required CryptoCurrency currency,
+ }) async {
+ try {
+ final providers = _buildProviders();
+ // return first entry if currency is specified and provider supports it
+ if (currency != null) {
+ for (final provider in providers) {
+ final cur = currency.title;
+ final src = provider.source.label;
+
+ printV('[address resolver service] checking $src - $cur - $query');
+ if (!provider.isEnabled(settingsStore)) {
+ printV('[address resolver service] ...skipping $src (disabled)');
+ continue;
+ }
+ if (!provider.canHandle(query)) {
+ printV('[address resolver service] ...skipping $src (cannot handle)');
+ continue;
+ }
+ if (!provider.supportedCurrencies.contains(currency)) {
+ printV('[address resolver service] ...skipping $src (unsupported currency)');
+ continue;
+ }
+
+ try {
+ printV('[address resolver service] ...resolving $src - $cur - $query');
+ final results = await provider.resolve(
+ query: query,
+ currencies: [currency],
+ wallet: wallet,
+ );
+
+ if (results.isNotEmpty) {
+ final parsedAddress = results.first.parsedAddressByCurrencyMap[currency];
+ final parsedAddressStr = parsedAddress == null || parsedAddress.isEmpty ? 'N/A' : parsedAddress;
+ printV('[address resolver service] resolved $src - $cur - $parsedAddressStr');
+ return [results.first];
+ }
+ } catch (e) {
+ printV('[address resolver service] Error resolving $src: $e');
+ }
+ }
+
+ return [];
+ }
+
+ return []; // TODO: Implement multi-currency resolution if no specific currency is provided.
+
+ final tasks = >>[];
+
+ for (final provider in providers) {
+ if (!provider.isEnabled(settingsStore)) continue;
+ if (!provider.canHandle(query)) continue;
+
+ tasks.add(
+ provider.resolve(
+ query: query,
+ currencies: provider.supportedCurrencies,
+ wallet: wallet,
+ ),
+ );
+ }
+
+ final results = await Future.wait(tasks);
+ return results.expand((items) => items).toList();
+ } catch (e) {
+ printV('Error resolving address: $e');
+ return [];
+ }
+ }
+}
diff --git a/lib/core/address_resolver/address_resolver_utils.dart b/lib/core/address_resolver/address_resolver_utils.dart
new file mode 100644
index 000000000..f9f59240c
--- /dev/null
+++ b/lib/core/address_resolver/address_resolver_utils.dart
@@ -0,0 +1,104 @@
+import 'package:cake_wallet/core/address_resolver/unstoppable/unstoppable_address_provider.dart';
+import 'package:cake_wallet/core/address_validator.dart';
+import 'package:cw_core/crypto_currency.dart';
+import 'package:cw_core/utils/print_verbose.dart';
+
+class AddressResolverUtils {
+ static Future
', caseSensitive: false), '\n')
+ .replaceAll(RegExp(r'<[^>]*>'), ' ')
+ .trim();
+ }
+}
diff --git a/lib/core/address_resolver/address_sources.dart b/lib/core/address_resolver/address_sources.dart
new file mode 100644
index 000000000..729ef478a
--- /dev/null
+++ b/lib/core/address_resolver/address_sources.dart
@@ -0,0 +1,87 @@
+import 'package:cake_wallet/core/address_validator.dart';
+import 'package:cw_core/crypto_currency.dart';
+
+enum AddressSource {
+ twitter(label: 'X', iconPath: 'assets/new-ui/address_sources/x.svg', alias: '@username'),
+ unstoppableDomains(
+ label: 'Unstoppable Domains',
+ iconPath: 'assets/new-ui/address_sources/unstoppable.svg',
+ alias: 'domain.tld'),
+ openAlias(
+ label: 'OpenAlias',
+ iconPath: 'assets/new-ui/address_sources/openalias.svg',
+ alias: 'name.domain.tld'),
+ yatRecord(label: 'Yat', iconPath: 'assets/new-ui/address_sources/yat.svg', alias: 'πππ'),
+ fio(label: 'FIO', iconPath: 'assets/new-ui/address_sources/fio.svg', alias: 'user@domain'),
+ ens(
+ label: 'Ethereum Name Service',
+ iconPath: 'assets/new-ui/address_sources/ens.svg',
+ alias: 'domain.eth'),
+ mastodon(
+ label: 'Mastodon',
+ iconPath: 'assets/new-ui/address_sources/mastodon.svg',
+ alias: '@username@domain.tld'),
+ nostr(
+ label: 'Nostr',
+ iconPath: 'assets/new-ui/address_sources/nostr.svg',
+ alias: 'user@domain.tld'),
+ thorChain(
+ label: 'ThorChain',
+ iconPath: 'assets/new-ui/address_sources/icon-thorchain.svg',
+ alias: 'name'),
+ wellKnown(
+ label: '.wellknown',
+ iconPath: 'assets/new-ui/address_sources/wellknown.svg',
+ alias: 'domain.tld'),
+ zanoAlias(
+ label: 'Zano Alias', iconPath: 'assets/new-ui/address_sources/zano.svg', alias: '@alias'),
+ bip353(
+ label: 'BIP353',
+ iconPath: 'assets/new-ui/address_sources/bip353.svg',
+ alias: 'user@domain.com'),
+ zcashAddress(
+ label: 'Zcash.me',
+ iconPath: 'assets/new-ui/address_sources/icon-zcash-me.svg',
+ alias: 'zcash.me/username'),
+ zcashName(
+ label: 'Zcash Names',
+ iconPath: 'assets/new-ui/address_sources/icon-zcashnames.svg',
+ alias: 'name.zec'),
+ lnurlPay(
+ label: 'LNURL Pay',
+ iconPath: 'assets/new-ui/address_sources/icon-lnurl-pay.svg',
+ alias: 'user@domain.com'),
+ contact(label: 'Contact', iconPath: ''),
+ notParsed(label: 'Unknown', iconPath: ''),
+ ;
+
+ const AddressSource({
+ required this.label,
+ required this.iconPath,
+ this.alias = '',
+ });
+
+ final String label;
+ final String iconPath;
+ final String alias;
+}
+
+extension AddressSourceIndex on AddressSource {
+ int get raw => index;
+
+ static AddressSource fromRaw(int raw) =>
+ AddressSource.values[raw.clamp(0, AddressSource.values.length - 1)];
+}
+
+extension AddressSourceNameParser on AddressSource {
+ static AddressSource fromLabel(String? text) {
+ if (text == null || text.trim().isEmpty) {
+ return AddressSource.notParsed;
+ }
+ final needle = text.trim().toLowerCase();
+ return AddressSource.values.firstWhere(
+ (src) => src.label.toLowerCase() == needle,
+ orElse: () => AddressSource.notParsed,
+ );
+ }
+}
diff --git a/lib/core/address_resolver/bip_353/bip_353_address_provider.dart b/lib/core/address_resolver/bip_353/bip_353_address_provider.dart
new file mode 100644
index 000000000..3a46f87a4
--- /dev/null
+++ b/lib/core/address_resolver/bip_353/bip_353_address_provider.dart
@@ -0,0 +1,68 @@
+import 'package:cake_wallet/core/address_resolver/address_lookup_provider.dart';
+import 'package:cake_wallet/core/address_resolver/address_sources.dart';
+import 'package:cake_wallet/core/address_resolver/bip_353/bip_353_record.dart';
+import 'package:cake_wallet/core/address_resolver/parsed_address.dart';
+import 'package:cake_wallet/store/settings_store.dart';
+import 'package:cw_core/crypto_currency.dart';
+import 'package:cw_core/utils/print_verbose.dart';
+import 'package:cw_core/wallet_base.dart';
+
+class Bip353AddressProvider extends AddressLookupProvider {
+ @override
+ AddressSource get source => AddressSource.bip353;
+
+ @override
+ List get supportedCurrencies => [CryptoCurrency.btc];
+
+ @override
+ bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsBip353;
+
+ @override
+ bool canHandle(String query) {
+ final value = query.trim();
+ return value.contains('@') && value.contains('.') && !value.startsWith('@');
+ }
+
+ @override
+ Future> resolve({
+ required String query,
+ required List currencies,
+ required WalletBase wallet,
+ }) async {
+ try {
+ final result = {};
+ String? dnsProof;
+
+ for (final cur in currencies) {
+ final bip353AddressMap = await Bip353Record.fetchUriByCryptoCurrency(query, cur.title);
+ if (bip353AddressMap == null || bip353AddressMap.isEmpty) continue;
+
+ final spAddress = bip353AddressMap['sp'];
+ final address = bip353AddressMap['address'];
+ final chosenAddress = spAddress?.isNotEmpty == true ? spAddress : address;
+
+ if (chosenAddress != null && chosenAddress.isNotEmpty) {
+ result[cur] = chosenAddress;
+ }
+ }
+
+ if (result.isEmpty) return [];
+
+ try {
+ dnsProof = await Bip353Record.fetchDnsProof(query);
+ } catch (e) {
+ printV('Bip353Record.fetchDnsProof error: $e');
+ }
+
+ return [ParsedAddress(
+ parsedAddressByCurrencyMap: result,
+ addressSource: AddressSource.bip353,
+ handle: query,
+ bip353DnsProof: dnsProof ?? '',
+ )];
+ } catch (e) {
+ printV('[address resolver] Error resolving BIP353 address: $e');
+ return [];
+ }
+ }
+}
\ No newline at end of file
diff --git a/lib/entities/bip_353_record.dart b/lib/core/address_resolver/bip_353/bip_353_record.dart
similarity index 59%
rename from lib/entities/bip_353_record.dart
rename to lib/core/address_resolver/bip_353/bip_353_record.dart
index 9a2a50e0a..257d76737 100644
--- a/lib/entities/bip_353_record.dart
+++ b/lib/core/address_resolver/bip_353/bip_353_record.dart
@@ -2,13 +2,9 @@ import 'dart:convert';
import 'dart:isolate';
import 'package:basic_utils/basic_utils.dart';
-import 'package:cake_wallet/generated/i18n.dart';
-import 'package:cake_wallet/src/widgets/alert_with_picker_option.dart';
-import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:dnssec_proof/dnssec_proof.dart';
-import 'package:flutter/material.dart';
class Bip353Record {
Bip353Record({
@@ -104,69 +100,4 @@ class Bip353Record {
}
}
- static Future pickBip353AddressChoice(
- BuildContext context,
- String bip353Name,
- Map addressMap,
- ) async {
- if (addressMap.length == 1) {
- return addressMap.values.first;
- }
-
- final chosenAddress = await _showAddressChoiceDialog(context, bip353Name, addressMap);
-
- return chosenAddress;
- }
-
- static Future _showAddressChoiceDialog(
- BuildContext context,
- String bip353Name,
- Map addressMap,
- ) async {
- final entriesList = addressMap.entries.toList();
- final List