mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-28 06:28:41 +01:00
Merge branch 'cake-tech:dev' into dev
This commit is contained in:
+1
-1
@@ -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
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 76 KiB |
@@ -551,8 +551,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
||||
return tx;
|
||||
}
|
||||
|
||||
List<UtxoWithPrivateKey> getUtxoWithPrivateKeys() => unspentCoins
|
||||
.where((e) => (e.isSending && !e.isFrozen))
|
||||
List<UtxoWithPrivateKey> getUtxoWithPrivateKeys({bool confirmedOnly = false}) => unspentCoins
|
||||
.where((e) => e.isSending && !e.isFrozen && (!confirmedOnly || (e.confirmations ?? 0) > 0))
|
||||
.map((unspent) => UtxoWithPrivateKey.fromUnspent(unspent, this))
|
||||
.toList();
|
||||
|
||||
|
||||
@@ -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<void> 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<BitcoinWallet> restoreFromHardwareWallet(BitcoinRestoreWalletFromHardware credentials,
|
||||
{bool? isTestnet}) async {
|
||||
|
||||
@@ -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<void> 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<void>.delayed(const Duration(milliseconds: 200));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> changePassword(String password) async {
|
||||
_password = password;
|
||||
|
||||
@@ -441,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,
|
||||
@@ -450,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,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -497,22 +497,25 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
||||
mwebUtxosBox = await CakeHive.openBox<MwebUtxo>(boxName);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> renameWalletFiles(String newWalletName) async {
|
||||
// rename the hive box:
|
||||
final oldBoxName = "${walletInfo.name.replaceAll(" ", "_")}_${MwebUtxo.boxName}";
|
||||
final newBoxName = "${newWalletName.replaceAll(" ", "_")}_${MwebUtxo.boxName}";
|
||||
static Future<void> 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<MwebUtxo>(oldBoxName);
|
||||
mwebUtxosBox = await CakeHive.openBox<MwebUtxo>(newBoxName);
|
||||
final newBox = await CakeHive.openBox<MwebUtxo>(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<void> deleteMwebBox(String name) async {
|
||||
final boxName = "${name.replaceAll(" ", "_")}_${MwebUtxo.boxName}";
|
||||
final box = await CakeHive.openBox<MwebUtxo>(boxName);
|
||||
await box.deleteFromDisk();
|
||||
}
|
||||
|
||||
@action
|
||||
|
||||
@@ -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<void> 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
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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<void> 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<BitcoinCashWallet> restoreFromHardwareWallet(BitcoinCashNewWalletCredentials credentials) {
|
||||
throw UnimplementedError(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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<String> pathForWalletTypeDir({required WalletType type}) async {
|
||||
final root = await getAppDir();
|
||||
@@ -36,3 +37,43 @@ Future<String> outdatedAndroidPathForWalletDir({required String name}) async {
|
||||
|
||||
return pathDir;
|
||||
}
|
||||
|
||||
Future<void> 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<void> _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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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});
|
||||
|
||||
|
||||
@@ -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<BalanceType extends Balance, HistoryType extends TransactionHistoryBase,
|
||||
TransactionType extends TransactionInfo> {
|
||||
@@ -115,7 +116,8 @@ abstract class WalletBase<BalanceType extends Balance, HistoryType extends Trans
|
||||
|
||||
void setExceptionHandler(void Function(FlutterErrorDetails) onError) => null;
|
||||
|
||||
Future<void> renameWalletFiles(String newWalletName);
|
||||
Future<void> renameWalletFiles(String newWalletName) =>
|
||||
copyWalletFilesTo(fromName: walletInfo.name, toName: newWalletName, type: type);
|
||||
|
||||
Future<String> signMessage(String message, {String? address = null});
|
||||
|
||||
|
||||
@@ -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<N extends WalletCredentials, RFS extends WalletCredentials,
|
||||
RFK extends WalletCredentials, RFH extends WalletCredentials> {
|
||||
@@ -25,7 +28,30 @@ abstract class WalletService<N extends WalletCredentials, RFS extends WalletCred
|
||||
|
||||
Future<void> remove(String wallet);
|
||||
|
||||
Future<void> rename(String currentName, String password, String newName);
|
||||
Future<void> 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<void> restoreWalletFilesFromBackup(String name) async {
|
||||
final backupWalletDirPath = await pathForWalletDir(name: "$name.backup", type: getType());
|
||||
|
||||
@@ -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<void> 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<DogeCoinWallet> restoreFromHardwareWallet(DogeCoinNewWalletCredentials credentials) {
|
||||
throw UnimplementedError(
|
||||
|
||||
@@ -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<MoralisWalletTokenBalance> tokens = [];
|
||||
|
||||
for (final item in decodedResponse) {
|
||||
final tokenData = item as Map<String, dynamic>;
|
||||
do {
|
||||
final params = <String, String>{
|
||||
"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<String, dynamic>) return tokens;
|
||||
|
||||
final result = decoded['result'];
|
||||
if (result is! List) return tokens;
|
||||
|
||||
for (final item in result) {
|
||||
if (item is! Map<String, dynamic>) 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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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<String>? cachedWhitelistLower,
|
||||
Set<String>? 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<void> _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<void> _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<MoralisDiscoveryResult> 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<void> 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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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<void> 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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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<SPLToken>(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<void> _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<SolanaPrivateKey> 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<String>? cachedDefaultMints,
|
||||
Set<String>? 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<void> 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<void> 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();
|
||||
|
||||
@@ -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<SolanaNewWalletCredentials,
|
||||
SolanaRestoreWalletFromSeedCredentials, SolanaRestoreWalletFromPrivateKey, SolanaNewWalletCredentials> {
|
||||
@@ -138,29 +136,6 @@ class SolanaWalletService extends WalletService<SolanaNewWalletCredentials,
|
||||
return wallet;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> 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<WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo>> restoreFromHardwareWallet(SolanaNewWalletCredentials credentials) {
|
||||
// TODO: implement restoreFromHardwareWallet
|
||||
|
||||
@@ -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<TronToken?> getTronToken(String contractAddress) async =>
|
||||
await _client.getTronToken(contractAddress, _tronAddress);
|
||||
|
||||
@override
|
||||
Future<void> 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();
|
||||
|
||||
@@ -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<void> 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<bool> isWalletExit(String name) async =>
|
||||
File(await pathForWallet(name: name, type: getType())).existsSync();
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_core/utils/proxy_wrapper.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cake_wallet/entities/fiat_currency.dart';
|
||||
import 'dart:convert';
|
||||
import 'package:cake_wallet/.secrets.g.dart' as secrets;
|
||||
|
||||
const _fiatApiClearNetAuthority = 'fiat-api.cakewallet.com';
|
||||
const _fiatApiOnionAuthority = 'kfkyguqtz5vcnbvar5pjgddkaeawbo4j3r4fj3e22k3tzqageplosiid.onion';
|
||||
const _fiatApiClearNetAuthority = 'prices.cakewallet.com';
|
||||
const _fiatApiOnionAuthority = '46wisoe2uwipcj2j4og6smiq7hbmj34fkkrquwlzbbsqtgat7bf3erid.onion';
|
||||
const _fiatApiPath = '/v2/rates';
|
||||
|
||||
Future<double> _fetchPrice(String crypto, String fiat, bool torOnly) async {
|
||||
|
||||
+24
-22
@@ -692,29 +692,18 @@ class CWEVM extends EVM {
|
||||
);
|
||||
}
|
||||
|
||||
Future<({double usdValue, bool hasValidFiatPrice})> _getTokenUsdValueAndFiatCheck(
|
||||
Erc20Token token,
|
||||
BigInt balanceWei,
|
||||
) async {
|
||||
Future<double> _fetchFiatApiPriceForToken(Erc20Token token) async {
|
||||
try {
|
||||
final settingsStore = getIt.get<SettingsStore>();
|
||||
final torOnly = settingsStore.fiatApiMode == FiatApiMode.torOnly;
|
||||
|
||||
final price = await FiatConversionService.fetchPrice(
|
||||
return await FiatConversionService.fetchPrice(
|
||||
crypto: token,
|
||||
fiat: FiatCurrency.usd,
|
||||
torOnly: torOnly,
|
||||
);
|
||||
|
||||
final hasValidFiatPrice = price > 0;
|
||||
|
||||
final decimals = token.decimal;
|
||||
final balance = balanceWei.toDouble() / math.pow(10, decimals);
|
||||
final usdValue = balance * price;
|
||||
|
||||
return (usdValue: usdValue, hasValidFiatPrice: hasValidFiatPrice);
|
||||
} catch (e) {
|
||||
return (usdValue: 0.0, hasValidFiatPrice: false);
|
||||
} catch (_) {
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -730,21 +719,34 @@ class CWEVM extends EVM {
|
||||
|
||||
final List<Future<void>> tokenChecks = [];
|
||||
|
||||
final whitelistedContracts =
|
||||
wallet.getDefaultTokenContractAddresses.map((a) => a.toLowerCase()).toSet();
|
||||
|
||||
for (final item in result.newTokens) {
|
||||
tokenChecks.add((() async {
|
||||
final token = item.token;
|
||||
|
||||
final isPropertiesSuspicious = wallet.isTokenPropertiesSuspicious(token);
|
||||
final isWhitelisted = whitelistedContracts.contains(token.contractAddress.toLowerCase());
|
||||
|
||||
final fiatResult = await _getTokenUsdValueAndFiatCheck(
|
||||
token,
|
||||
item.balanceWei,
|
||||
);
|
||||
final isSpam = isPropertiesSuspicious || !fiatResult.hasValidFiatPrice;
|
||||
final moralisPrice = item.moralisUsdPrice;
|
||||
final moralisValue = item.moralisUsdValue ?? 0.0;
|
||||
final hasMoralisPrice = moralisPrice != null && moralisPrice > 0;
|
||||
|
||||
final fiatApiPrice = await _fetchFiatApiPriceForToken(token);
|
||||
final hasFiatApiPrice = fiatApiPrice > 0;
|
||||
|
||||
final isImpersonator =
|
||||
hasFiatApiPrice && !hasMoralisPrice && !isWhitelisted && !item.verifiedContract;
|
||||
|
||||
final isSpam = isPropertiesSuspicious ||
|
||||
token.isPotentialScam ||
|
||||
isImpersonator ||
|
||||
(!hasMoralisPrice && !hasFiatApiPrice);
|
||||
|
||||
token.isPotentialScam = isSpam;
|
||||
|
||||
token.enabled = (fiatResult.usdValue >= _minTokenUsdValue) && !isSpam;
|
||||
token.enabled =
|
||||
hasMoralisPrice && hasFiatApiPrice && (moralisValue >= _minTokenUsdValue) && !isSpam;
|
||||
|
||||
await wallet.addErc20Token(token);
|
||||
})());
|
||||
|
||||
@@ -5,13 +5,12 @@ import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:cake_wallet/src/screens/receive/widgets/qr_image.dart';
|
||||
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
|
||||
import 'package:cake_wallet/utils/address_formatter.dart';
|
||||
import 'package:cake_wallet/utils/clipboard_util.dart';
|
||||
import 'package:cake_wallet/view_model/send/send_view_model.dart';
|
||||
import 'package:cw_core/payment_uris.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
class L2SendExternalModal extends StatefulWidget {
|
||||
const L2SendExternalModal({super.key, required this.sendViewModel});
|
||||
@@ -180,11 +179,7 @@ class _L2SendExternalModalState extends State<L2SendExternalModal> {
|
||||
children: [
|
||||
Flexible(
|
||||
child: NewPrimaryButton(
|
||||
onPressed:(){
|
||||
Clipboard.setData(
|
||||
ClipboardData(text: output.address),
|
||||
);
|
||||
},
|
||||
onPressed: () => ClipboardUtil.copyToClipboard(context, output.address),
|
||||
text: S.of(context).copy,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary),
|
||||
|
||||
@@ -4,11 +4,11 @@ import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/swap_page/swap_modal_header.dart';
|
||||
import 'package:cake_wallet/src/screens/receive/widgets/qr_image.dart';
|
||||
import 'package:cake_wallet/utils/address_formatter.dart';
|
||||
import 'package:cake_wallet/utils/clipboard_util.dart';
|
||||
import 'package:cake_wallet/view_model/exchange/exchange_trade_view_model.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/payment_uris.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class SwapSendExternalModal extends StatefulWidget {
|
||||
const SwapSendExternalModal(
|
||||
@@ -45,7 +45,7 @@ class _SwapSendExternalModalState extends State<SwapSendExternalModal> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (uri == null) return SizedBox.shrink();
|
||||
if (widget.address.isEmpty) return SizedBox.shrink();
|
||||
final resolvedSize = MediaQuery.of(context).size.width * (largeQrMode ? 0.8 : 0.54);
|
||||
|
||||
return PopScope(
|
||||
@@ -139,7 +139,7 @@ class _SwapSendExternalModalState extends State<SwapSendExternalModal> {
|
||||
child: QrImage(
|
||||
size: animatedSize,
|
||||
embeddedImagePath: widget.from.iconPath,
|
||||
data: uri.toString(),
|
||||
data: uri?.toString() ?? widget.address,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -175,7 +175,7 @@ class _SwapSendExternalModalState extends State<SwapSendExternalModal> {
|
||||
children: [
|
||||
Flexible(
|
||||
child: NewPrimaryButton(
|
||||
onPressed: ()=> Clipboard.setData(ClipboardData(text:widget.address)),
|
||||
onPressed: () => ClipboardUtil.copyToClipboard(context, widget.address),
|
||||
text: S.of(context).copy,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary),
|
||||
|
||||
@@ -387,9 +387,11 @@ class CWSolana extends Solana {
|
||||
tokenChecks.add((() async {
|
||||
final token = item.token;
|
||||
|
||||
final isPropertiesSuspicious = wallet.isTokenPropertiesSuspicious(token);
|
||||
|
||||
final fiatResult = await _getTokenUsdValueAndFiatCheck(token, item.balance);
|
||||
|
||||
final isSpam = !fiatResult.hasValidFiatPrice;
|
||||
final isSpam = isPropertiesSuspicious || !fiatResult.hasValidFiatPrice;
|
||||
|
||||
token.isPotentialScam = isSpam;
|
||||
token.enabled = (fiatResult.usdValue >= _minTokenUsdValue) && !isSpam;
|
||||
|
||||
@@ -148,6 +148,15 @@ class CakeFeaturesPage extends StatelessWidget {
|
||||
subTitle: "Turn your old phone into your new hardware wallet with our new app",
|
||||
image: 'assets/images/cupcake.png',
|
||||
),
|
||||
AppsWidget(
|
||||
isWide: true,
|
||||
isLink: true,
|
||||
isCake: true,
|
||||
onTap: () => _launchUrl("radar.chat"),
|
||||
title: "Radar",
|
||||
subTitle: "Your messages. Your Bitcoin. Together, at last",
|
||||
image: 'assets/images/radar.png',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 24, top: 16, bottom: 8),
|
||||
|
||||
@@ -139,68 +139,71 @@ class WalletTypeFormState extends State<WalletTypeForm> {
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 18, right: 18, bottom: 12),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
),
|
||||
child: ListView.separated(
|
||||
key: const ValueKey('new_wallet_type_scrollable_key'),
|
||||
itemCount: filteredTypes.length,
|
||||
shrinkWrap: true,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
padding: EdgeInsets.zero,
|
||||
itemBuilder: (context, index) {
|
||||
final item = filteredTypes[index];
|
||||
|
||||
return GestureDetector(
|
||||
key: ValueKey('new_wallet_type_${item.name}_button_key'),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
onTypeSelected(item);
|
||||
},
|
||||
child: SizedBox(
|
||||
height: 48,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
CakeImageWidget(
|
||||
height: 24,
|
||||
width: 24,
|
||||
imageUrl: getCryptoCurrencyIconForWalletListItem(item),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(walletTypeToDisplayName(item)),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
walletTypeToDisplayTicker(item),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
),
|
||||
child: ListView.separated(
|
||||
key: const ValueKey('new_wallet_type_scrollable_key'),
|
||||
itemCount: filteredTypes.length,
|
||||
shrinkWrap: true,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
padding: EdgeInsets.zero,
|
||||
itemBuilder: (context, index) {
|
||||
final item = filteredTypes[index];
|
||||
|
||||
return GestureDetector(
|
||||
key: ValueKey('new_wallet_type_${item.name}_button_key'),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
onTypeSelected(item);
|
||||
},
|
||||
child: SizedBox(
|
||||
height: 50,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
CakeImageWidget(
|
||||
height: 24,
|
||||
width: 24,
|
||||
imageUrl: getCryptoCurrencyIconForWalletListItem(item),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/arrow_forward.svg",
|
||||
height: 16,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
BlendMode.srcIn,
|
||||
const SizedBox(width: 12),
|
||||
Text(walletTypeToDisplayName(item)),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
walletTypeToDisplayTicker(item),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/arrow_forward.svg",
|
||||
height: 16,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) => Container(
|
||||
height: 1,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) => Container(
|
||||
height: 1,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/utils/device_info.dart';
|
||||
import 'package:cake_wallet/utils/show_bar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:sensitive_clipboard/sensitive_clipboard.dart';
|
||||
|
||||
class ClipboardUtil {
|
||||
@@ -12,4 +14,18 @@ class ClipboardUtil {
|
||||
|
||||
return Clipboard.setData(data);
|
||||
}
|
||||
|
||||
/// Copies [text] to the clipboard and shows a "Copied to Clipboard"
|
||||
/// confirmation toast. Prefer this for any user-facing copy action so the
|
||||
/// feedback stays consistent across the app.
|
||||
static Future<void> copyToClipboard(
|
||||
BuildContext context,
|
||||
String text, {
|
||||
bool isSensitive = false,
|
||||
}) async {
|
||||
await setSensitiveDataToClipboard(ClipboardData(text: text), isSensitive: isSensitive);
|
||||
if (context.mounted) {
|
||||
showBar<void>(context, S.of(context).copied_to_clipboard);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,14 +299,15 @@ class ExceptionHandler {
|
||||
"FocusScopeNode was used after being disposed",
|
||||
"_getDismissibleFlushbar",
|
||||
"_QueuedFuture.execute (package:universal_ble/src/queue.dart:65)",
|
||||
"Pending Request Canceled | RequestQueue disposed",
|
||||
"reown_core/relay_client/websocket/websocket_handler.dart",
|
||||
"Image upload failed due to loss of GPU access",
|
||||
"transport error",
|
||||
"SdkError.sparkError(field0: Operator RPC error: Connection error: status: Unavailable, message: \"dns error\", details: []",
|
||||
"the timeout of the request was reached",
|
||||
|
||||
"support for coin removed, your seedphrase:"
|
||||
"Exception: Invalid image data"
|
||||
"support for coin removed, your seedphrase:",
|
||||
"Exception: Invalid image data",
|
||||
];
|
||||
|
||||
static Future<void> _addDeviceInfo(File file) async {
|
||||
@@ -441,8 +442,20 @@ class ExceptionHandler {
|
||||
}
|
||||
|
||||
static bool _flutterErrorIgnore(FlutterErrorDetails errorDetails) {
|
||||
// most probably a flutter context error so just ignore it if there is no stack we can debug with
|
||||
return errorDetails.exception.toString().contains("Null check operator used on a null value") &&
|
||||
errorDetails.stack == null;
|
||||
if (errorDetails.exception.toString().contains("Null check operator used on a null value")) {
|
||||
// Most probably a flutter context error so just ignore it if there is no
|
||||
// stack we can debug with.
|
||||
if (errorDetails.stack == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final stack = errorDetails.stack.toString();
|
||||
if (stack.contains("handleFocusHighlightModeChange") ||
|
||||
stack.contains("_HighlightModeManager")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -415,9 +415,13 @@ abstract class ExchangeTradeViewModelBase with Store {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Using trade currency here so the external-send QR encodes the correct scheme
|
||||
final uriWalletType =
|
||||
cryptoCurrencyOrTokenToWalletType(fromCurrency) ?? wallet.type;
|
||||
// Using the trade's `from` currency so the external-send QR encodes the correct scheme.
|
||||
final uriWalletType = cryptoCurrencyOrTokenToWalletType(fromCurrency);
|
||||
|
||||
// for other currencies that we don't have wallets for
|
||||
if (uriWalletType == null) {
|
||||
return ExternalAddressURI(address: inputAddress, amount: amount);
|
||||
}
|
||||
|
||||
printV(uriWalletType);
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ import 'package:cake_wallet/store/app_store.dart';
|
||||
import 'package:cake_wallet/utils/exchange_provider_logger.dart';
|
||||
import 'package:cw_core/amount/amount_sanitizer.dart';
|
||||
import 'package:cw_core/amount/money.dart';
|
||||
import 'package:cw_core/amount/money_double.dart';
|
||||
import "package:cw_core/wallet_info.dart";
|
||||
import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
|
||||
import 'package:cake_wallet/store/dashboard/trades_store.dart';
|
||||
@@ -804,7 +805,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
|
||||
}
|
||||
|
||||
final amount_ = _enteredAmount / (forcedProvider == null ? bestRate : forcedProviderRate);
|
||||
_depositAmount = Money.tryParse(amount_.toStringAsFixed(depositCurrency.decimals), depositCurrency);
|
||||
_depositAmount = amount_.tryToMoney(depositCurrency);
|
||||
}
|
||||
|
||||
@action
|
||||
@@ -863,8 +864,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
|
||||
}
|
||||
|
||||
final amount_ = _enteredAmount * (forcedProvider == null ? bestRate : forcedProviderRate);
|
||||
_receiveAmount =
|
||||
Money.tryParse(amount_.toStringAsFixed(min(20, receiveCurrency.decimals)), receiveCurrency);
|
||||
_receiveAmount = amount_.tryToMoney(receiveCurrency);
|
||||
}
|
||||
|
||||
bool checkIfInputMeetsMinOrMaxCondition(String input) {
|
||||
|
||||
@@ -9,7 +9,6 @@ import 'package:cake_wallet/zano/zano.dart';
|
||||
import 'package:cw_core/crypto_amount_format.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/currency_for_wallet_type.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
import 'package:cw_core/transaction_info.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
@@ -50,10 +49,9 @@ String _moneroRecipientAddressForDisplay(String raw, WalletType walletType) {
|
||||
return match?.group(0) ?? raw.trim();
|
||||
}
|
||||
|
||||
bool isLightning(TransactionInfo tx) {
|
||||
printV(tx.additionalInfo);
|
||||
return (tx.additionalInfo["isLightning"] as bool?) ?? false;
|
||||
}
|
||||
bool isLightning(TransactionInfo tx) => (tx.additionalInfo["isLightning"] as bool?) ?? false;
|
||||
|
||||
bool hasLightningPreimage(TransactionInfo tx) => (tx.additionalInfo["preimage"] as String?) != null;
|
||||
|
||||
class TxDetailRowDefinition {
|
||||
final String keyString;
|
||||
@@ -188,6 +186,13 @@ class TxDetailRowDefinition {
|
||||
},
|
||||
applicable: (vm) => vm.wallet.type == WalletType.monero),
|
||||
|
||||
TxDetailRowDefinition(
|
||||
keyString: "standard_list_item_lightning_preimage",
|
||||
title: S.current.transaction_preimage,
|
||||
valueGetter: (vm) => vm.transactionInfo.additionalInfo['preimage'] as String? ?? "",
|
||||
applicable: (vm) =>
|
||||
hasLightningPreimage(vm.transactionInfo) && isLightning(vm.transactionInfo),
|
||||
),
|
||||
|
||||
TxDetailRowDefinition(
|
||||
keyString: "standard_list_item_transaction_confirmed_key",
|
||||
@@ -316,10 +321,9 @@ abstract class TransactionDetailsViewModelBase with Store {
|
||||
TransactionPriority? transactionPriority;
|
||||
|
||||
CryptoCurrency get transactionAsset {
|
||||
if (isEVMCompatibleChain(wallet.type)) return evm!.assetOfTransaction(wallet, transactionInfo);
|
||||
|
||||
if (isEVMCompatibleChain(wallet.type)) {
|
||||
return evm!.assetOfTransaction(wallet, transactionInfo);
|
||||
}
|
||||
if (isLightning(transactionInfo)) return CryptoCurrency.btcln;
|
||||
|
||||
return switch (wallet.type) {
|
||||
WalletType.solana => solana!.assetOfTransaction(wallet, transactionInfo),
|
||||
@@ -329,7 +333,6 @@ abstract class TransactionDetailsViewModelBase with Store {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// TODO integrate these getters with the TransactionInfo object
|
||||
String get formattedPendingStatus {
|
||||
switch (wallet.type) {
|
||||
|
||||
+4
-2
@@ -205,8 +205,10 @@ dependency_overrides:
|
||||
ref: 7bcb00380783f8611f68f5cd63fec96261fea2c2
|
||||
ledger_usb_plus:
|
||||
git:
|
||||
url: https://github.com/konstantinullrich/ledger-usb-plus
|
||||
ref: dad482c847c0d0eedceabbb40d3478e64b65e630
|
||||
# url: https://github.com/konstantinullrich/ledger-usb-plus
|
||||
# ref: dad482c847c0d0eedceabbb40d3478e64b65e630
|
||||
url: https://github.com/cake-tech/ledger-usb-plus
|
||||
ref: 734c3d6f8267ba52ad73b6e1e78e570c07298890
|
||||
socks5_proxy:
|
||||
git:
|
||||
url: https://github.com/LacticWhale/socks_dart.git
|
||||
|
||||
@@ -1253,6 +1253,7 @@
|
||||
"transaction_error": "Transaction Error",
|
||||
"transaction_key": "Transaction Key",
|
||||
"transaction_memo_optional": "Transaction Memo (optional)",
|
||||
"transaction_preimage": "Preimage",
|
||||
"transaction_priority_fast": "Fast",
|
||||
"transaction_priority_fastest": "Fastest",
|
||||
"transaction_priority_medium": "Medium",
|
||||
@@ -1452,4 +1453,4 @@
|
||||
"zcash_card_missing_funds": "Missing funds?",
|
||||
"zcash_card_scan": "Scan",
|
||||
"zcash_card_warning": "Do not close the app until the procedure completes, if you do so this process will need to restart from scratch."
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user