diff --git a/cw_core/lib/db/sqlite.dart b/cw_core/lib/db/sqlite.dart index 43bef5f38..0deffab8e 100644 --- a/cw_core/lib/db/sqlite.dart +++ b/cw_core/lib/db/sqlite.dart @@ -63,7 +63,7 @@ Future _initDb({String? pathOverride}) async { } } await db?.close(); - db = await openDatabase(dbFile.path, version: 9, + db = await openDatabase(dbFile.path, version: 10, onUpgrade: (Database db, int oldVersion, int newVersion) async { printV("migrating: $oldVersion, $newVersion"); if (oldVersion <= 1) { @@ -147,6 +147,11 @@ CREATE TABLE IF NOT EXISTS BalanceCardStyleSettings ( definition: 'BOOLEAN DEFAULT FALSE', ); } + if (oldVersion <= 9) { + await _createErc20TokenTable(db); + await _createSplTokenTable(db); + await _createTronTokenTable(db); + } }, onCreate: (Database db, int version) async { await db.execute(''' CREATE TABLE WalletInfo ( @@ -242,6 +247,9 @@ CREATE TABLE BalanceCardStyleSettings ( await _createBridgeTransferTable(db); await _createNodeTable(db); await _createTradeTable(db); + await _createErc20TokenTable(db); + await _createSplTokenTable(db); + await _createTronTokenTable(db); }); } @@ -370,6 +378,71 @@ ON BridgeTransfer(wallet_id); '''); } +Future _createErc20TokenTable(Database db) async { + await db.execute(""" +CREATE TABLE IF NOT EXISTS Erc20Token ( + Erc20TokenId INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + walletName TEXT NOT NULL, + chainId INTEGER NOT NULL, + name TEXT NOT NULL DEFAULT '', + symbol TEXT NOT NULL DEFAULT '', + contractAddress TEXT NOT NULL, + decimal INTEGER NOT NULL DEFAULT 0, + enabled INTEGER NOT NULL DEFAULT 1, + iconPath TEXT, + tag TEXT, + isPotentialScam INTEGER NOT NULL DEFAULT 0 +); +"""); + await db.execute(""" +CREATE UNIQUE INDEX IF NOT EXISTS idx_erc20token_wallet_chain_contract +ON Erc20Token (walletName, chainId, contractAddress); +"""); +} + +Future _createSplTokenTable(Database db) async { + await db.execute(""" +CREATE TABLE IF NOT EXISTS SPLToken ( + SPLTokenId INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + walletName TEXT NOT NULL, + name TEXT NOT NULL DEFAULT '', + symbol TEXT NOT NULL DEFAULT '', + mintAddress TEXT NOT NULL, + decimal INTEGER NOT NULL DEFAULT 0, + mint TEXT NOT NULL DEFAULT '', + enabled INTEGER NOT NULL DEFAULT 1, + iconPath TEXT, + tag TEXT, + isPotentialScam INTEGER NOT NULL DEFAULT 0 +); +"""); + await db.execute(""" +CREATE UNIQUE INDEX IF NOT EXISTS idx_spltoken_wallet_mint +ON SPLToken (walletName, mintAddress); +"""); +} + +Future _createTronTokenTable(Database db) async { + await db.execute(""" +CREATE TABLE IF NOT EXISTS TronToken ( + TronTokenId INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + walletName TEXT NOT NULL, + name TEXT NOT NULL DEFAULT '', + symbol TEXT NOT NULL DEFAULT '', + contractAddress TEXT NOT NULL, + decimal INTEGER NOT NULL DEFAULT 0, + enabled INTEGER NOT NULL DEFAULT 1, + iconPath TEXT, + tag TEXT, + isPotentialScam INTEGER NOT NULL DEFAULT 0 +); +"""); + await db.execute(""" +CREATE UNIQUE INDEX IF NOT EXISTS idx_trontoken_wallet_contract +ON TronToken (walletName, contractAddress); +"""); +} + Future _createNodeTable(Database db) async { db.execute(""" CREATE TABLE Node ( diff --git a/cw_core/lib/erc20_token.dart b/cw_core/lib/erc20_token.dart index d5fde7aa2..a0c14173c 100644 --- a/cw_core/lib/erc20_token.dart +++ b/cw_core/lib/erc20_token.dart @@ -1,32 +1,8 @@ -import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/hive_type_ids.dart'; -import 'package:hive/hive.dart'; - -part 'erc20_token.part.dart'; - -// @HiveType(typeId: Erc20Token.typeId) -class Erc20Token extends CryptoCurrency with HiveObjectMixin { - // @HiveField(0) - final String name; - // @HiveField(1) - final String symbol; - // @HiveField(2) - final String contractAddress; - // @HiveField(3) - final int decimal; - // @HiveField(4, defaultValue: true) - bool _enabled; - // @HiveField(5) - String? iconPath; - // @HiveField(6) - final String? tag; - // @HiveField(7, defaultValue: false) - bool isPotentialScam; - - bool get enabled => _enabled; - - set enabled(bool value) => _enabled = value; +import "package:cw_core/crypto_currency.dart"; +import "package:cw_core/db/sqlite.dart"; +import "package:sqflite/sqflite.dart"; +class Erc20Token extends CryptoCurrency { Erc20Token({ required this.name, required this.symbol, @@ -36,6 +12,9 @@ class Erc20Token extends CryptoCurrency with HiveObjectMixin { this.iconPath, this.tag, this.isPotentialScam = false, + this.id = 0, + this.walletName, + this.chainId, }) : _enabled = enabled, super( name: symbol.toLowerCase(), @@ -47,32 +26,161 @@ class Erc20Token extends CryptoCurrency with HiveObjectMixin { isPotentialScam: isPotentialScam, ); - Erc20Token.copyWith(Erc20Token other, {String? icon, String? tag, bool? enabled}) - : this.name = other.name, - this.symbol = other.symbol, - this.contractAddress = other.contractAddress, - this.decimal = other.decimal, - this._enabled = enabled ?? other.enabled, - this.tag = tag ?? other.tag, - this.iconPath = icon ?? other.iconPath, - this.isPotentialScam = other.isPotentialScam, + Erc20Token.copyWith( + Erc20Token other, { + String? icon, + super.tag, + bool? enabled, + String? walletName, + int? chainId, + }) : name = other.name, + symbol = other.symbol, + contractAddress = other.contractAddress, + decimal = other.decimal, + _enabled = enabled ?? other.enabled, + tag = tag ?? other.tag, + iconPath = icon ?? other.iconPath, + isPotentialScam = other.isPotentialScam, + id = 0, + walletName = walletName ?? other.walletName, + chainId = chainId ?? other.chainId, super( name: other.name, title: other.symbol.toUpperCase(), fullName: other.name, - tag: tag, iconPath: icon, decimals: other.decimal, isPotentialScam: other.isPotentialScam, ); - static const typeId = ERC20_TOKEN_TYPE_ID; - static const boxName = 'Erc20Tokens'; - static const ethereumBoxName = 'EthereumErc20Tokens'; - static const polygonBoxName = 'PolygonErc20Tokens'; - static const baseBoxName = 'BaseErc20Tokens'; - static const arbitrumBoxName = 'ArbitrumErc20Tokens'; - static const bscBoxName = 'BscErc20Tokens'; + Erc20Token.fromMap(Map map) + : this( + name: map["name"] as String? ?? "", + symbol: map["symbol"] as String? ?? "", + contractAddress: map["contractAddress"] as String? ?? "", + decimal: (map["decimal"] ?? 0) as int, + enabled: _getBoolFromDB(map["enabled"], defaultValue: true), + iconPath: map["iconPath"] as String?, + tag: map["tag"] as String?, + isPotentialScam: _getBoolFromDB(map["isPotentialScam"]), + id: (map[selfIdColumn] ?? 0) as int, + walletName: map["walletName"] as String?, + chainId: map["chainId"] as int?, + ); + + @override + final String name; + + @override + final String symbol; + + @override + String? iconPath; + + @override + final String? tag; + + @override + bool isPotentialScam; + + @override + bool get enabled => _enabled; + + @override + set enabled(bool value) => _enabled = value; + + int id; + int? chainId; + bool _enabled; + final int decimal; + String? walletName; + final String contractAddress; + + static bool _getBoolFromDB(value, {bool? defaultValue}) { + if (value is bool) { + return value; + } else if (value is int) { + return value == 1; + } else { + return defaultValue ?? false; + } + } + + Map toMap() => { + selfIdColumn: id, + "walletName": walletName, + "chainId": chainId, + "name": name, + "symbol": symbol, + "contractAddress": contractAddress.toLowerCase(), + "decimal": decimal, + "enabled": _enabled ? 1 : 0, + "iconPath": iconPath, + "tag": tag, + "isPotentialScam": isPotentialScam ? 1 : 0, + }; + + static String get tableName => "Erc20Token"; + static String get selfIdColumn => "${tableName}Id"; + + Future save() async { + if (walletName == null || chainId == null) { + throw StateError("Erc20Token.save() requires walletName and chainId to be set"); + } + + final json = toMap(); + if (json[selfIdColumn] == 0) { + json[selfIdColumn] = null; + } + id = await db!.insert(tableName, json, conflictAlgorithm: ConflictAlgorithm.replace); + return id; + } + + static Future> selectList( + String where, + List whereArgs, { + String? orderBy, + }) async { + orderBy ??= selfIdColumn; + final list = await db!.query( + tableName, + where: where.isNotEmpty ? where : "1 = 1", + whereArgs: whereArgs.isNotEmpty ? whereArgs : null, + orderBy: orderBy, + ); + return List.generate(list.length, (index) => Erc20Token.fromMap(list[index])); + } + + static Future> getAllForWallet(String walletName, int chainId) => + selectList("walletName = ? AND chainId = ?", [walletName, chainId]); + + static Future getByContract( + String walletName, + int chainId, + String contractAddress, + ) async { + final list = await selectList( + "walletName = ? AND chainId = ? AND contractAddress = ?", + [walletName, chainId, contractAddress.toLowerCase()], + ); + return list.isEmpty ? null : list.first; + } + + static Future deleteForWallet(String walletName, int chainId, String contractAddress) => + db!.delete( + tableName, + where: "walletName = ? AND chainId = ? AND contractAddress = ?", + whereArgs: [walletName, chainId, contractAddress.toLowerCase()], + ); + + static Future deleteAllForWallet(String walletName) => + db!.delete(tableName, where: "walletName = ?", whereArgs: [walletName]); + + static Future renameWallet(String oldName, String newName) async { + await db!.delete(tableName, where: "walletName = ?", whereArgs: [newName]); + await db! + .update(tableName, {"walletName": newName}, where: "walletName = ?", whereArgs: [oldName]); + } @override bool operator ==(Object other) => other is Erc20Token && other.contractAddress == contractAddress; diff --git a/cw_core/lib/erc20_token_legacy.dart b/cw_core/lib/erc20_token_legacy.dart new file mode 100644 index 000000000..c965899cd --- /dev/null +++ b/cw_core/lib/erc20_token_legacy.dart @@ -0,0 +1,213 @@ +import "package:cw_core/cake_hive.dart"; +import "package:cw_core/db/sqlite.dart"; +import "package:cw_core/erc20_token.dart" as erc20_new; +import "package:cw_core/hive_type_ids.dart"; +import "package:cw_core/utils/print_verbose.dart"; +import "package:cw_core/wallet_info.dart"; +import "package:cw_core/wallet_type.dart"; +import "package:hive/hive.dart"; +import "package:sqflite/sqflite.dart"; + +part "erc20_token_legacy.part.dart"; + +Future performErc20TokenHiveMigration() async { + try { + if (!CakeHive.isAdapterRegistered(Erc20Token.typeId)) { + CakeHive.registerAdapter(Erc20TokenAdapter()); + } + + final wallets = await WalletInfo.getAll(); + await Erc20Token.migrateAllToSqlite(wallets); + } catch (e) { + printV("Error performing Erc20Token Hive migration: $e, continuing anyway"); + } +} + +// @HiveType(typeId: Erc20Token.typeId) +class Erc20Token extends HiveObject { + Erc20Token({ + required this.name, + required this.symbol, + required this.contractAddress, + required this.decimal, + bool enabled = true, + this.iconPath, + this.tag, + this.isPotentialScam = false, + }) : _enabled = enabled; + // @HiveField(0) + final String name; + // @HiveField(1) + final String symbol; + // @HiveField(2) + final String contractAddress; + // @HiveField(3) + final int decimal; + // @HiveField(4, defaultValue: true) + bool _enabled; + // @HiveField(5) + String? iconPath; + // @HiveField(6) + final String? tag; + // @HiveField(7, defaultValue: false) + bool isPotentialScam; + + bool get enabled => _enabled; + + set enabled(bool value) => _enabled = value; + + static const typeId = ERC20_TOKEN_TYPE_ID; + static const boxName = "Erc20Tokens"; + static const ethereumBoxName = "EthereumErc20Tokens"; + static const polygonBoxName = "PolygonErc20Tokens"; + static const baseBoxName = "BaseErc20Tokens"; + static const arbitrumBoxName = "ArbitrumErc20Tokens"; + static const bscBoxName = "BscErc20Tokens"; + + static const chainIdToBoxSuffix = { + 1: ethereumBoxName, + 137: polygonBoxName, + 8453: baseBoxName, + 42161: arbitrumBoxName, + 56: bscBoxName, + }; + + static const evmWalletTypes = [ + WalletType.ethereum, + WalletType.polygon, + WalletType.base, + WalletType.arbitrum, + WalletType.bsc, + ]; + + static Future migrateAllToSqlite(List wallets) async { + await _migrateLegacyGlobalBox(wallets); + + final sanitizedToRawNames = >{}; + for (final wallet in wallets) { + if (!evmWalletTypes.contains(wallet.type)) { + continue; + } + + sanitizedToRawNames + .putIfAbsent(wallet.name.replaceAll(" ", "_"), () => {}) + .add(wallet.name); + } + + for (final entry in sanitizedToRawNames.entries) { + for (final chainEntry in chainIdToBoxSuffix.entries) { + final tokenBoxName = "${entry.key}_${chainEntry.value}"; + try { + if (!await CakeHive.boxExists(tokenBoxName)) { + continue; + } + + final box = await CakeHive.openBox(tokenBoxName); + + for (final group in _mergeByLowercaseContract(box)) { + for (final rawName in entry.value) { + await group.token.migrateToSqlite(walletName: rawName, chainId: chainEntry.key); + } + await box.deleteAll(group.sourceKeys); + } + + await box.deleteFromDisk(); + } catch (e) { + printV("Error migrating erc20 token box $tokenBoxName: $e, continuing anyway"); + } + } + } + } + + static Future _migrateLegacyGlobalBox(List wallets) async { + try { + if (!await CakeHive.boxExists(boxName)) { + return; + } + + if (!wallets.any((wallet) => wallet.type == WalletType.ethereum)) { + return; + } + + final box = await CakeHive.openBox(boxName); + final ethereumWallets = + wallets.where((wallet) => wallet.type == WalletType.ethereum).toList(); + + for (final group in _mergeByLowercaseContract(box)) { + for (final wallet in ethereumWallets) { + await group.token.migrateToSqlite(walletName: wallet.name, chainId: 1); + } + await box.deleteAll(group.sourceKeys); + } + + await box.deleteFromDisk(); + } catch (e) { + printV("Error migrating legacy global erc20 token box: $e, continuing anyway"); + } + } + + static List<_MergedTokenGroup> _mergeByLowercaseContract(Box box) { + final groups = {}; + + for (final key in box.keys) { + final token = box.get(key); + if (token == null) { + continue; + } + + final lowerKey = token.contractAddress.toLowerCase(); + final existing = groups[lowerKey]; + + if (existing == null) { + groups[lowerKey] = _MergedTokenGroup(token: token, sourceKeys: [key]); + continue; + } + + groups[lowerKey] = _MergedTokenGroup( + token: Erc20Token( + name: token.name, + symbol: token.symbol, + contractAddress: lowerKey, + decimal: token.decimal, + enabled: token.enabled || existing.token.enabled, + iconPath: + (token.iconPath?.isNotEmpty ?? false) ? token.iconPath : existing.token.iconPath, + tag: token.tag ?? existing.token.tag, + isPotentialScam: token.isPotentialScam || existing.token.isPotentialScam, + ), + sourceKeys: [...existing.sourceKeys, key], + ); + } + + return groups.values.toList(); + } + + Future migrateToSqlite({required String walletName, required int chainId}) async { + final row = erc20_new.Erc20Token( + name: name, + symbol: symbol, + contractAddress: contractAddress, + decimal: decimal, + enabled: _enabled, + iconPath: iconPath, + tag: tag, + isPotentialScam: isPotentialScam, + walletName: walletName, + chainId: chainId, + ).toMap(); + row[erc20_new.Erc20Token.selfIdColumn] = null; + + await db!.insert( + erc20_new.Erc20Token.tableName, + row, + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } +} + +class _MergedTokenGroup { + _MergedTokenGroup({required this.token, required this.sourceKeys}); + + final Erc20Token token; + final List sourceKeys; +} diff --git a/cw_core/lib/erc20_token.part.dart b/cw_core/lib/erc20_token_legacy.part.dart similarity index 97% rename from cw_core/lib/erc20_token.part.dart rename to cw_core/lib/erc20_token_legacy.part.dart index e19b4b522..9614c0c1a 100644 --- a/cw_core/lib/erc20_token.part.dart +++ b/cw_core/lib/erc20_token_legacy.part.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'erc20_token.dart'; +part of "erc20_token_legacy.dart"; // ************************************************************************** // TypeAdapterGenerator diff --git a/cw_core/lib/spl_token.dart b/cw_core/lib/spl_token.dart index 500435319..5ac9330fb 100644 --- a/cw_core/lib/spl_token.dart +++ b/cw_core/lib/spl_token.dart @@ -1,41 +1,8 @@ -import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/hive_type_ids.dart'; -import 'package:hive/hive.dart'; +import "package:cw_core/crypto_currency.dart"; +import "package:cw_core/db/sqlite.dart"; +import "package:sqflite/sqflite.dart"; -part 'spl_token.part.dart'; - -// @HiveType(typeId: SPLToken.typeId) -class SPLToken extends CryptoCurrency with HiveObjectMixin { - @override - // @HiveField(0) - final String name; - - // @HiveField(1) - final String symbol; - - // @HiveField(2) - final String mintAddress; - - // @HiveField(3) - final int decimal; - - // @HiveField(4, defaultValue: true) - bool _enabled; - - // @HiveField(5) - final String mint; - - @override - // @HiveField(6) - final String? iconPath; - - @override - // @HiveField(7) - final String? tag; - - @override - // @HiveField(8, defaultValue: false) - bool isPotentialScam; +class SPLToken extends CryptoCurrency { SPLToken({ required this.name, @@ -44,10 +11,12 @@ class SPLToken extends CryptoCurrency with HiveObjectMixin { required this.decimal, required this.mint, this.iconPath, - this.tag = 'SOL', + this.tag = "SOL", bool enabled = true, this.isPotentialScam = false, - Set groups = const {}, + super.groups, + this.id = 0, + this.walletName, }) : _enabled = enabled, super( name: mint.toLowerCase(), @@ -57,7 +26,6 @@ class SPLToken extends CryptoCurrency with HiveObjectMixin { iconPath: iconPath, decimals: decimal, isPotentialScam: isPotentialScam, - groups: groups, ); factory SPLToken.fromMetadata({ @@ -67,8 +35,7 @@ class SPLToken extends CryptoCurrency with HiveObjectMixin { required String mintAddress, String? iconPath, bool isPotentialScam = false, - }) { - return SPLToken( + }) => SPLToken( name: name, symbol: symbol, mintAddress: mintAddress, @@ -77,15 +44,8 @@ class SPLToken extends CryptoCurrency with HiveObjectMixin { iconPath: iconPath, isPotentialScam: isPotentialScam, ); - } - @override - bool get enabled => _enabled; - - @override - set enabled(bool value) => _enabled = value; - - SPLToken.copyWith(SPLToken other, {String? icon, String? tag, bool? enabled}) + SPLToken.copyWith(SPLToken other, {String? icon, String? tag, bool? enabled, String? walletName}) : name = other.name, symbol = other.symbol, mintAddress = other.mintAddress, @@ -95,6 +55,8 @@ class SPLToken extends CryptoCurrency with HiveObjectMixin { tag = tag ?? other.tag, iconPath = icon ?? other.iconPath, isPotentialScam = other.isPotentialScam, + id = 0, + walletName = walletName ?? other.walletName, super( title: other.symbol.toUpperCase(), name: other.symbol.toLowerCase(), @@ -106,8 +68,124 @@ class SPLToken extends CryptoCurrency with HiveObjectMixin { groups: other.groups, ); - static const typeId = SPL_TOKEN_TYPE_ID; - static const boxName = 'SPLTokens'; + SPLToken.fromMap(Map map) + : this( + name: map["name"] as String? ?? "", + symbol: map["symbol"] as String? ?? "", + mintAddress: map["mintAddress"] as String? ?? "", + decimal: (map["decimal"] ?? 0) as int, + mint: map["mint"] as String? ?? "", + enabled: _getBoolFromDB(map["enabled"], defaultValue: true), + iconPath: map["iconPath"] as String?, + tag: map["tag"] as String?, + isPotentialScam: _getBoolFromDB(map["isPotentialScam"]), + id: (map[selfIdColumn] ?? 0) as int, + walletName: map["walletName"] as String?, + ); + @override + final String name; + + @override + final String symbol; + + final String mintAddress; + + final int decimal; + + bool _enabled; + + final String mint; + + @override + final String? iconPath; + + @override + final String? tag; + + @override + bool isPotentialScam; + + int id; + String? walletName; + + @override + bool get enabled => _enabled; + + @override + set enabled(bool value) => _enabled = value; + + static bool _getBoolFromDB(value, {bool? defaultValue}) { + if (value is bool) { + return value; + } else if (value is int) { + return value == 1; + } else { + return defaultValue ?? false; + } + } + + Map toMap() => { + selfIdColumn: id, + "walletName": walletName, + "name": name, + "symbol": symbol, + "mintAddress": mintAddress, + "decimal": decimal, + "mint": mint, + "enabled": _enabled ? 1 : 0, + "iconPath": iconPath, + "tag": tag, + "isPotentialScam": isPotentialScam ? 1 : 0, + }; + + static String get tableName => "SPLToken"; + static String get selfIdColumn => "${tableName}Id"; + + Future save() async { + if (walletName == null) { + throw StateError("SPLToken.save() requires walletName to be set"); + } + + final json = toMap(); + if (json[selfIdColumn] == 0) { + json[selfIdColumn] = null; + } + id = await db!.insert(tableName, json, conflictAlgorithm: ConflictAlgorithm.replace); + return id; + } + + static Future> selectList(String where, List whereArgs, + {String? orderBy}) async { + orderBy ??= selfIdColumn; + final list = await db!.query( + tableName, + where: where.isNotEmpty ? where : "1 = 1", + whereArgs: whereArgs.isNotEmpty ? whereArgs : null, + orderBy: orderBy, + ); + return List.generate(list.length, (index) => SPLToken.fromMap(list[index])); + } + + static Future> getAllForWallet(String walletName) async => selectList("walletName = ?", [walletName]); + + static Future getByMint(String walletName, String mintAddress) async { + final list = await selectList("walletName = ? AND mintAddress = ?", [walletName, mintAddress]); + return list.isEmpty ? null : list.first; + } + + static Future deleteForWallet(String walletName, String mintAddress) => db!.delete( + tableName, + where: "walletName = ? AND mintAddress = ?", + whereArgs: [walletName, mintAddress], + ); + + static Future deleteAllForWallet(String walletName) => db!.delete(tableName, where: "walletName = ?", whereArgs: [walletName]); + + static Future renameWallet(String oldName, String newName) async { + await db!.delete(tableName, where: "walletName = ?", whereArgs: [newName]); + await db! + .update(tableName, {"walletName": newName}, where: "walletName = ?", whereArgs: [oldName]); + } @override bool operator ==(other) => diff --git a/cw_core/lib/spl_token_legacy.dart b/cw_core/lib/spl_token_legacy.dart new file mode 100644 index 000000000..aea751915 --- /dev/null +++ b/cw_core/lib/spl_token_legacy.dart @@ -0,0 +1,134 @@ +import "package:cw_core/cake_hive.dart"; +import "package:cw_core/db/sqlite.dart"; +import "package:cw_core/hive_type_ids.dart"; +import "package:cw_core/spl_token.dart" as spl_new; +import "package:cw_core/utils/print_verbose.dart"; +import "package:cw_core/wallet_info.dart"; +import "package:cw_core/wallet_type.dart"; +import "package:hive/hive.dart"; +import "package:sqflite/sqflite.dart"; + +part "spl_token_legacy.part.dart"; + +Future performSplTokenHiveMigration() async { + try { + if (!CakeHive.isAdapterRegistered(SPLToken.typeId)) { + CakeHive.registerAdapter(SPLTokenAdapter()); + } + + final wallets = await WalletInfo.getAll(); + await SPLToken.migrateAllToSqlite(wallets); + } catch (e) { + printV("Error performing SPLToken Hive migration: $e, continuing anyway"); + } +} + +// @HiveType(typeId: SPLToken.typeId) +class SPLToken extends HiveObject { + SPLToken({ + required this.name, + required this.symbol, + required this.mintAddress, + required this.decimal, + required this.mint, + this.iconPath, + this.tag = "SOL", + bool enabled = true, + this.isPotentialScam = false, + }) : _enabled = enabled; + // @HiveField(0) + final String name; + + // @HiveField(1) + final String symbol; + + // @HiveField(2) + final String mintAddress; + + // @HiveField(3) + final int decimal; + + // @HiveField(4, defaultValue: true) + bool _enabled; + + // @HiveField(5) + final String mint; + + // @HiveField(6) + final String? iconPath; + + // @HiveField(7) + final String? tag; + + // @HiveField(8, defaultValue: false) + bool isPotentialScam; + + bool get enabled => _enabled; + + set enabled(bool value) => _enabled = value; + + static const typeId = SPL_TOKEN_TYPE_ID; + static const boxName = "SPLTokens"; + + static Future migrateAllToSqlite(List wallets) async { + final sanitizedToRawNames = >{}; + for (final wallet in wallets) { + if (wallet.type != WalletType.solana) { + continue; + } + + sanitizedToRawNames + .putIfAbsent(wallet.name.replaceAll(" ", "_"), () => {}) + .add(wallet.name); + } + + for (final entry in sanitizedToRawNames.entries) { + final tokenBoxName = "${entry.key}_$boxName"; + try { + if (!await CakeHive.boxExists(tokenBoxName)) { + continue; + } + + final box = await CakeHive.openBox(tokenBoxName); + + for (final key in box.keys.toList()) { + final token = box.get(key); + if (token == null) { + continue; + } + + for (final rawName in entry.value) { + await token.migrateToSqlite(walletName: rawName); + } + await box.delete(key); + } + + await box.deleteFromDisk(); + } catch (e) { + printV("Error migrating spl token box $tokenBoxName: $e, continuing anyway"); + } + } + } + + Future migrateToSqlite({required String walletName}) async { + final row = spl_new.SPLToken( + name: name, + symbol: symbol, + mintAddress: mintAddress, + decimal: decimal, + mint: mint, + enabled: _enabled, + iconPath: iconPath, + tag: tag, + isPotentialScam: isPotentialScam, + walletName: walletName, + ).toMap(); + row[spl_new.SPLToken.selfIdColumn] = null; + + await db!.insert( + spl_new.SPLToken.tableName, + row, + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } +} diff --git a/cw_core/lib/spl_token.part.dart b/cw_core/lib/spl_token_legacy.part.dart similarity index 98% rename from cw_core/lib/spl_token.part.dart rename to cw_core/lib/spl_token_legacy.part.dart index cede960c9..ca228a65e 100644 --- a/cw_core/lib/spl_token.part.dart +++ b/cw_core/lib/spl_token_legacy.part.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'spl_token.dart'; +part of "spl_token_legacy.dart"; // ************************************************************************** // TypeAdapterGenerator diff --git a/cw_core/lib/tron_token.dart b/cw_core/lib/tron_token.dart index c4b27ed21..2e1b7ed1d 100644 --- a/cw_core/lib/tron_token.dart +++ b/cw_core/lib/tron_token.dart @@ -1,41 +1,8 @@ -// ignore_for_file: annotate_overrides, overridden_fields - -import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/hive_type_ids.dart'; -import 'package:hive/hive.dart'; - -part 'tron_token.part.dart'; - -// @HiveType(typeId: TronToken.typeId) -class TronToken extends CryptoCurrency with HiveObjectMixin { - // @HiveField(0) - final String name; - - // @HiveField(1) - final String symbol; - - // @HiveField(2) - final String contractAddress; - - // @HiveField(3) - final int decimal; - - // @HiveField(4, defaultValue: true) - bool _enabled; - - // @HiveField(5) - final String? iconPath; - - // @HiveField(6) - final String? tag; - - // @HiveField(7, defaultValue: false) - final bool isPotentialScam; - - bool get enabled => _enabled; - - set enabled(bool value) => _enabled = value; +import "package:cw_core/crypto_currency.dart"; +import "package:cw_core/db/sqlite.dart"; +import "package:sqflite/sqflite.dart"; +class TronToken extends CryptoCurrency { TronToken({ required this.name, required this.symbol, @@ -43,8 +10,10 @@ class TronToken extends CryptoCurrency with HiveObjectMixin { required this.decimal, bool enabled = true, this.iconPath, - this.tag = 'TRX', + this.tag = "TRX", this.isPotentialScam = false, + this.id = 0, + this.walletName, }) : _enabled = enabled, super( name: symbol.toLowerCase(), @@ -55,9 +24,27 @@ class TronToken extends CryptoCurrency with HiveObjectMixin { decimals: decimal, isPotentialScam: isPotentialScam, ); + TronToken.fromMap(Map map) + : this( + name: map["name"] as String? ?? "", + symbol: map["symbol"] as String? ?? "", + contractAddress: map["contractAddress"] as String? ?? "", + decimal: (map["decimal"] ?? 0) as int, + enabled: _getBoolFromDB(map["enabled"], defaultValue: true), + iconPath: map["iconPath"] as String?, + tag: map["tag"] as String?, + isPotentialScam: _getBoolFromDB(map["isPotentialScam"]), + id: (map[selfIdColumn] ?? 0) as int, + walletName: map["walletName"] as String?, + ); - TronToken.copyWith(TronToken other, {String? icon, String? tag, bool? enabled}) - : name = other.name, + TronToken.copyWith( + TronToken other, { + String? icon, + String? tag, + bool? enabled, + String? walletName, + }) : name = other.name, symbol = other.symbol, contractAddress = other.contractAddress, decimal = other.decimal, @@ -65,6 +52,8 @@ class TronToken extends CryptoCurrency with HiveObjectMixin { tag = tag ?? other.tag, iconPath = icon ?? other.iconPath, isPotentialScam = other.isPotentialScam, + id = 0, + walletName = walletName ?? other.walletName, super( name: other.name, title: other.symbol.toUpperCase(), @@ -74,9 +63,110 @@ class TronToken extends CryptoCurrency with HiveObjectMixin { decimals: other.decimal, isPotentialScam: other.isPotentialScam, ); + @override + final String name; - static const typeId = TRON_TOKEN_TYPE_ID; - static const boxName = 'TronTokens'; + @override + final String symbol; + + @override + final String? iconPath; + + @override + final String? tag; + + @override + final bool isPotentialScam; + + @override + bool get enabled => _enabled; + + @override + set enabled(bool value) => _enabled = value; + + int id; + bool _enabled; + final int decimal; + String? walletName; + final String contractAddress; + + static bool _getBoolFromDB(value, {bool? defaultValue}) { + if (value is bool) { + return value; + } else if (value is int) { + return value == 1; + } else { + return defaultValue ?? false; + } + } + + Map toMap() => { + selfIdColumn: id, + "walletName": walletName, + "name": name, + "symbol": symbol, + "contractAddress": contractAddress, + "decimal": decimal, + "enabled": _enabled ? 1 : 0, + "iconPath": iconPath, + "tag": tag, + "isPotentialScam": isPotentialScam ? 1 : 0, + }; + + static String get tableName => "TronToken"; + static String get selfIdColumn => "${tableName}Id"; + + Future save() async { + if (walletName == null) { + throw StateError("TronToken.save() requires walletName to be set"); + } + + final json = toMap(); + if (json[selfIdColumn] == 0) { + json[selfIdColumn] = null; + } + id = await db!.insert(tableName, json, conflictAlgorithm: ConflictAlgorithm.replace); + return id; + } + + static Future> selectList( + String where, + List whereArgs, { + String? orderBy, + }) async { + orderBy ??= selfIdColumn; + final list = await db!.query( + tableName, + where: where.isNotEmpty ? where : "1 = 1", + whereArgs: whereArgs.isNotEmpty ? whereArgs : null, + orderBy: orderBy, + ); + return List.generate(list.length, (index) => TronToken.fromMap(list[index])); + } + + static Future> getAllForWallet(String walletName) => + selectList("walletName = ?", [walletName]); + + static Future getByContract(String walletName, String contractAddress) async { + final list = + await selectList("walletName = ? AND contractAddress = ?", [walletName, contractAddress]); + return list.isEmpty ? null : list.first; + } + + static Future deleteForWallet(String walletName, String contractAddress) => db!.delete( + tableName, + where: "walletName = ? AND contractAddress = ?", + whereArgs: [walletName, contractAddress], + ); + + static Future deleteAllForWallet(String walletName) => + db!.delete(tableName, where: "walletName = ?", whereArgs: [walletName]); + + static Future renameWallet(String oldName, String newName) async { + await db!.delete(tableName, where: "walletName = ?", whereArgs: [newName]); + await db! + .update(tableName, {"walletName": newName}, where: "walletName = ?", whereArgs: [oldName]); + } @override bool operator ==(other) => diff --git a/cw_core/lib/tron_token_legacy.dart b/cw_core/lib/tron_token_legacy.dart new file mode 100644 index 000000000..c7fd8c907 --- /dev/null +++ b/cw_core/lib/tron_token_legacy.dart @@ -0,0 +1,129 @@ +import "package:cw_core/cake_hive.dart"; +import "package:cw_core/db/sqlite.dart"; +import "package:cw_core/hive_type_ids.dart"; +import "package:cw_core/tron_token.dart" as tron_new; +import "package:cw_core/utils/print_verbose.dart"; +import "package:cw_core/wallet_info.dart"; +import "package:cw_core/wallet_type.dart"; +import "package:hive/hive.dart"; +import "package:sqflite/sqflite.dart"; + +part "tron_token_legacy.part.dart"; + +Future performTronTokenHiveMigration() async { + try { + if (!CakeHive.isAdapterRegistered(TronToken.typeId)) { + CakeHive.registerAdapter(TronTokenAdapter()); + } + + final wallets = await WalletInfo.getAll(); + await TronToken.migrateAllToSqlite(wallets); + } catch (e) { + printV("Error performing TronToken Hive migration: $e, continuing anyway"); + } +} + +// @HiveType(typeId: TronToken.typeId) +class TronToken extends HiveObject { + TronToken({ + required this.name, + required this.symbol, + required this.contractAddress, + required this.decimal, + bool enabled = true, + this.iconPath, + this.tag = "TRX", + this.isPotentialScam = false, + }) : _enabled = enabled; + // @HiveField(0) + final String name; + + // @HiveField(1) + final String symbol; + + // @HiveField(2) + final String contractAddress; + + // @HiveField(3) + final int decimal; + + // @HiveField(4, defaultValue: true) + bool _enabled; + + // @HiveField(5) + final String? iconPath; + + // @HiveField(6) + final String? tag; + + // @HiveField(7, defaultValue: false) + final bool isPotentialScam; + + bool get enabled => _enabled; + + set enabled(bool value) => _enabled = value; + + static const typeId = TRON_TOKEN_TYPE_ID; + static const boxName = "TronTokens"; + + static Future migrateAllToSqlite(List wallets) async { + final sanitizedToRawNames = >{}; + for (final wallet in wallets) { + if (wallet.type != WalletType.tron) { + continue; + } + + sanitizedToRawNames + .putIfAbsent(wallet.name.replaceAll(" ", "_"), () => {}) + .add(wallet.name); + } + + for (final entry in sanitizedToRawNames.entries) { + final tokenBoxName = "${entry.key}_$boxName"; + try { + if (!await CakeHive.boxExists(tokenBoxName)) { + continue; + } + + final box = await CakeHive.openBox(tokenBoxName); + + for (final key in box.keys.toList()) { + final token = box.get(key); + if (token == null) { + continue; + } + + for (final rawName in entry.value) { + await token.migrateToSqlite(walletName: rawName); + } + await box.delete(key); + } + + await box.deleteFromDisk(); + } catch (e) { + printV("Error migrating tron token box $tokenBoxName: $e, continuing anyway"); + } + } + } + + Future migrateToSqlite({required String walletName}) async { + final row = tron_new.TronToken( + name: name, + symbol: symbol, + contractAddress: contractAddress, + decimal: decimal, + enabled: _enabled, + iconPath: iconPath, + tag: tag, + isPotentialScam: isPotentialScam, + walletName: walletName, + ).toMap(); + row[tron_new.TronToken.selfIdColumn] = null; + + await db!.insert( + tron_new.TronToken.tableName, + row, + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } +} diff --git a/cw_core/lib/tron_token.part.dart b/cw_core/lib/tron_token_legacy.part.dart similarity index 97% rename from cw_core/lib/tron_token.part.dart rename to cw_core/lib/tron_token_legacy.part.dart index c84865349..0884d9184 100644 --- a/cw_core/lib/tron_token.part.dart +++ b/cw_core/lib/tron_token_legacy.part.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'tron_token.dart'; +part of "tron_token_legacy.dart"; // ************************************************************************** // TypeAdapterGenerator diff --git a/cw_core/lib/wallet_service.dart b/cw_core/lib/wallet_service.dart index 8d19a9f8c..122193649 100644 --- a/cw_core/lib/wallet_service.dart +++ b/cw_core/lib/wallet_service.dart @@ -1,14 +1,16 @@ -import 'dart:convert'; -import 'dart:io'; +import "dart:convert"; +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; +import "package:cw_core/pathForWallet.dart"; +import "package:cw_core/spl_token.dart"; +import "package:cw_core/tron_token.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 { @@ -29,11 +31,13 @@ abstract class WalletService remove(String wallet); Future rename(String currentName, String password, String newName) async { - if (currentName == newName) return; + if (currentName == newName) { + return; + } final currentWalletInfo = await WalletInfo.get(currentName, getType()); if (currentWalletInfo == null) { - throw Exception('Wallet not found'); + throw Exception("Wallet not found"); } await copyWalletFilesTo(fromName: currentName, toName: newName, type: getType()); @@ -43,6 +47,8 @@ abstract class WalletService _renameTokenRows(String currentName, String newName) async { + if (getType() == WalletType.solana) { + await SPLToken.renameWallet(currentName, newName); + } + + if (getType() == WalletType.tron) { + await TronToken.renameWallet(currentName, newName); + } + } + Future restoreWalletFilesFromBackup(String name) async { final backupWalletDirPath = await pathForWalletDir(name: "$name.backup", type: getType()); final walletDirPath = await pathForWalletDir(name: name, type: getType()); @@ -77,14 +93,14 @@ abstract class WalletService getApplicationDocumentsPath() async => root; + + @override + Future getApplicationSupportPath() async => root; +} + +Future main() async { + final dataRoot = Directory("./test/data/token_migration"); + + Future insertWalletInfoRow(String name, WalletType type) async { + await db!.insert("WalletInfo", { + "id": "${walletTypeToString(type).toLowerCase()}_$name", + "name": name, + "type": type.index, + "isRecovery": 0, + "restoreHeight": 0, + "timestamp": 0, + "dirPath": "", + "path": "", + "address": "", + "showIntroCakePayCard": 0, + "walletInfoDerivationInfoId": 0, + "isNonSeedWallet": 0, + "sortOrder": 0, + "receiveInfoboxDismissed": 0, + "showCombinedBalance": 1, + }); + } + + group( + "token sqlite migration", + () { + setUpAll(() async { + if (dataRoot.existsSync()) { + dataRoot.deleteSync(recursive: true); + } + dataRoot.createSync(recursive: true); + + PathProviderPlatform.instance = _FakePathProviderPlatform(dataRoot.absolute.path); + + // On linux getAppDir() appends /cake_wallet to the documents dir and picks the + // first existing candidate, so create it up front to keep CI on the faked path. + Directory("${dataRoot.path}/cake_wallet").createSync(recursive: true); + + sqfliteFfiInit(); + databaseFactory = databaseFactoryFfi; + await initDb(); + + // Everything must share the dir initDb resolved so boxExists finds the boxes + final appDir = await getAppDir(); + CakeHive.init(appDir.path); + + if (!CakeHive.isAdapterRegistered(erc20_legacy.Erc20Token.typeId)) { + CakeHive.registerAdapter(erc20_legacy.Erc20TokenAdapter()); + } + if (!CakeHive.isAdapterRegistered(spl_legacy.SPLToken.typeId)) { + CakeHive.registerAdapter(spl_legacy.SPLTokenAdapter()); + } + if (!CakeHive.isAdapterRegistered(tron_legacy.TronToken.typeId)) { + CakeHive.registerAdapter(tron_legacy.TronTokenAdapter()); + } + + // Two ethereum wallets whose names sanitize to the same box name, plus sol and tron + await insertWalletInfoRow("My Wallet", WalletType.ethereum); + await insertWalletInfoRow("My_Wallet", WalletType.ethereum); + await insertWalletInfoRow("sol wallet", WalletType.solana); + await insertWalletInfoRow("tron1", WalletType.tron); + + // Legacy global box shared by every ethereum wallet in the pre per-wallet era + final globalBox = await CakeHive.openBox("Erc20Tokens"); + await globalBox.put( + "0xGlobalTokenAAA", + erc20_legacy.Erc20Token( + name: "Global Legacy", + symbol: "GLB", + contractAddress: "0xGlobalTokenAAA", + decimal: 18, + enabled: true, + ), + ); + + // Per-wallet ethereum box with a duplicate contract in two casings and a disabled token + final ethBox = + await CakeHive.openBox("My_Wallet_EthereumErc20Tokens"); + await ethBox.put( + "0xDupCASE01", + erc20_legacy.Erc20Token( + name: "Dup Token", + symbol: "DUP", + contractAddress: "0xDupCASE01", + decimal: 18, + enabled: false, + iconPath: "", + ), + ); + await ethBox.put( + "0xdupcase01", + erc20_legacy.Erc20Token( + name: "Dup Token", + symbol: "DUP", + contractAddress: "0xdupcase01", + decimal: 18, + enabled: true, + iconPath: "assets/images/dup.png", + ), + ); + await ethBox.put( + "0xkeepmedisabled", + erc20_legacy.Erc20Token( + name: "Keep Me", + symbol: "KEEP", + contractAddress: "0xkeepmedisabled", + decimal: 6, + enabled: false, + ), + ); + + // A polygon chain box for the same wallet name + final polyBox = + await CakeHive.openBox("My_Wallet_PolygonErc20Tokens"); + await polyBox.put( + "0xpolytoken01", + erc20_legacy.Erc20Token( + name: "Poly Token", + symbol: "PLY", + contractAddress: "0xpolytoken01", + decimal: 18, + enabled: true, + tag: "POL", + ), + ); + + // SPL and Tron boxes, base58 keys must keep their exact casing + final splBox = await CakeHive.openBox("sol_wallet_SPLTokens"); + await splBox.put( + "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + spl_legacy.SPLToken( + name: "USD Coin", + symbol: "USDC", + mintAddress: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + decimal: 6, + mint: "usdc", + enabled: false, + ), + ); + + final tronBox = await CakeHive.openBox("tron1_TronTokens"); + await tronBox.put( + "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", + tron_legacy.TronToken( + name: "Tether USD", + symbol: "USDT", + contractAddress: "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", + decimal: 6, + enabled: true, + ), + ); + + await globalBox.close(); + await ethBox.close(); + await polyBox.close(); + await splBox.close(); + await tronBox.close(); + }); + + tearDownAll(() async { + await db?.close(); + db = null; + if (dataRoot.existsSync()) { + dataRoot.deleteSync(recursive: true); + } + }); + + test("migrates every token box into sqlite", () async { + await erc20_legacy.performErc20TokenHiveMigration(); + await spl_legacy.performSplTokenHiveMigration(); + await tron_legacy.performTronTokenHiveMigration(); + + // Both ethereum wallets share the sanitized box name, so each gets the rows + for (final walletName in ["My Wallet", "My_Wallet"]) { + final ethTokens = await erc20_sql.Erc20Token.getAllForWallet(walletName, 1); + final addresses = ethTokens.map((t) => t.contractAddress).toSet(); + + expect( + addresses, + {"0xglobaltokenaaa", "0xdupcase01", "0xkeepmedisabled"}, + reason: "wallet $walletName should have the global, merged dup and disabled tokens", + ); + + final dup = ethTokens.firstWhere((t) => t.contractAddress == "0xdupcase01"); + expect(dup.enabled, true, reason: "dup merge ORs the enabled flags"); + expect(dup.iconPath, "assets/images/dup.png", reason: "dup merge prefers non-empty icon"); + + final keep = ethTokens.firstWhere((t) => t.contractAddress == "0xkeepmedisabled"); + expect(keep.enabled, false, reason: "disabled toggle must survive the migration"); + + final polyTokens = await erc20_sql.Erc20Token.getAllForWallet(walletName, 137); + expect(polyTokens.map((t) => t.contractAddress), ["0xpolytoken01"]); + } + + final splTokens = await spl_sql.SPLToken.getAllForWallet("sol wallet"); + expect(splTokens.length, 1); + expect( + splTokens.first.mintAddress, + "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + reason: "mint address casing must be preserved", + ); + expect(splTokens.first.enabled, false); + + final tronTokens = await tron_sql.TronToken.getAllForWallet("tron1"); + expect(tronTokens.length, 1); + expect( + tronTokens.first.contractAddress, + "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", + reason: "tron contract address casing must be preserved", + ); + + // Drained boxes are removed from disk + expect(await CakeHive.boxExists("Erc20Tokens"), false); + expect(await CakeHive.boxExists("My_Wallet_EthereumErc20Tokens"), false); + expect(await CakeHive.boxExists("My_Wallet_PolygonErc20Tokens"), false); + expect(await CakeHive.boxExists("sol_wallet_SPLTokens"), false); + expect(await CakeHive.boxExists("tron1_TronTokens"), false); + }); + + test("re-running the migrations is a no-op", () async { + final before = (await erc20_sql.Erc20Token.selectList("", [])).length + + (await spl_sql.SPLToken.selectList("", [])).length + + (await tron_sql.TronToken.selectList("", [])).length; + + await erc20_legacy.performErc20TokenHiveMigration(); + await spl_legacy.performSplTokenHiveMigration(); + await tron_legacy.performTronTokenHiveMigration(); + + final after = (await erc20_sql.Erc20Token.selectList("", [])).length + + (await spl_sql.SPLToken.selectList("", [])).length + + (await tron_sql.TronToken.selectList("", [])).length; + + expect(after, before); + }); + + test("an interrupted migration re-drains over existing rows without duplicating", () async { + // Simulates a run that inserted rows into sqlite but died before the box was + // emptied: the box reappears holding a token that already has a row (USDC, + // re-enabled in the box copy) plus one the interrupted run never reached (BONK) + final box = await CakeHive.openBox("sol_wallet_SPLTokens"); + await box.put( + "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + spl_legacy.SPLToken( + name: "USD Coin", + symbol: "USDC", + mintAddress: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + decimal: 6, + mint: "usdc", + enabled: true, + ), + ); + await box.put( + "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263", + spl_legacy.SPLToken( + name: "Bonk", + symbol: "BONK", + mintAddress: "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263", + decimal: 5, + mint: "bonk", + enabled: true, + ), + ); + await box.close(); + + await spl_legacy.performSplTokenHiveMigration(); + + final tokens = await spl_sql.SPLToken.getAllForWallet("sol wallet"); + expect(tokens.length, 2, reason: "the re-drain must not duplicate the existing USDC row"); + + final usdc = tokens.firstWhere((t) => t.symbol == "USDC"); + expect(usdc.enabled, true, reason: "the box copy wins over the stale row on re-drain"); + + expect(await CakeHive.boxExists("sol_wallet_SPLTokens"), false); + }); + + test("save without wallet context throws", () { + final token = erc20_sql.Erc20Token( + name: "No Context", + symbol: "NOC", + contractAddress: "0xnocontext", + decimal: 18, + ); + + expect(token.save, throwsStateError); + }); + + test("seeding preserves the enabled toggle without clobbering", () async { + // Same shape addInitialTokens uses: read existing, copyWith preserving enabled + final existing = + await erc20_sql.Erc20Token.getByContract("My Wallet", 1, "0xKEEPMEDISABLED"); + expect(existing, isNotNull); + expect(existing!.enabled, false); + + final refreshedDefault = erc20_sql.Erc20Token( + name: "Keep Me Renamed By Defaults", + symbol: "KEEP", + contractAddress: "0xkeepmedisabled", + decimal: 6, + enabled: true, + ); + final toSave = erc20_sql.Erc20Token.copyWith( + refreshedDefault, + enabled: existing.enabled, + walletName: "My Wallet", + chainId: 1, + ); + await toSave.save(); + + final reloaded = + await erc20_sql.Erc20Token.getByContract("My Wallet", 1, "0xkeepmedisabled"); + expect(reloaded!.enabled, false, reason: "metadata refresh must not re-enable the token"); + expect(reloaded.name, "Keep Me Renamed By Defaults"); + + final rowCount = (await erc20_sql.Erc20Token.getAllForWallet("My Wallet", 1)).length; + expect(rowCount, 3, reason: "upsert must replace, not duplicate"); + }); + + test("rename moves rows and delete removes them", () async { + await erc20_sql.Erc20Token.renameWallet("My Wallet", "Renamed Wallet"); + + expect(await erc20_sql.Erc20Token.getAllForWallet("My Wallet", 1), isEmpty); + expect((await erc20_sql.Erc20Token.getAllForWallet("Renamed Wallet", 1)).length, 3); + expect((await erc20_sql.Erc20Token.getAllForWallet("Renamed Wallet", 137)).length, 1); + + await erc20_sql.Erc20Token.deleteAllForWallet("My_Wallet"); + expect(await erc20_sql.Erc20Token.getAllForWallet("My_Wallet", 1), isEmpty); + expect(await erc20_sql.Erc20Token.getAllForWallet("My_Wallet", 137), isEmpty); + + // Rename into a name that has orphaned rows must not trip the unique index + await erc20_sql.Erc20Token.renameWallet("Renamed Wallet", "My_Wallet"); + expect((await erc20_sql.Erc20Token.getAllForWallet("My_Wallet", 1)).length, 3); + }); + }, + ); +} diff --git a/cw_evm/lib/evm_chain_wallet.dart b/cw_evm/lib/evm_chain_wallet.dart index f66c34319..c9e07c4a7 100644 --- a/cw_evm/lib/evm_chain_wallet.dart +++ b/cw_evm/lib/evm_chain_wallet.dart @@ -5,7 +5,6 @@ import 'dart:typed_data'; import 'package:bip32/bip32.dart' as bip32; import 'package:bip39/bip39.dart' as bip39; import 'package:cw_core/amount/money.dart'; -import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/erc20_token.dart'; @@ -38,7 +37,6 @@ import 'package:cw_evm/hardware/evm_chain_bitbox_credentials.dart'; import 'package:cw_evm/hardware/evm_chain_ledger_credentials.dart'; import 'package:cw_evm/hardware/evm_chain_trezor_credentials.dart'; import 'package:hex/hex.dart'; -import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:web3dart/crypto.dart'; @@ -109,10 +107,6 @@ abstract class EVMChainWalletBase this.walletInfo = walletInfo; transactionHistory = setUpTransactionHistory(walletInfo, password, encryptionFileUtils); - if (!CakeHive.isAdapterRegistered(Erc20Token.typeId)) { - CakeHive.registerAdapter(Erc20TokenAdapter()); - } - sharedPrefs.complete(SharedPreferences.getInstance()); } @@ -121,9 +115,7 @@ abstract class EVMChainWalletBase final String _password; final EncryptionFileUtils encryptionFileUtils; - late final Box erc20TokensBox; - - late Box evmChainErc20TokensBox; + List _erc20Tokens = []; late final Credentials _evmChainPrivateKey; @@ -217,8 +209,8 @@ abstract class EVMChainWalletBase // Automatically connect to node for the selected chain await connectToNode(node: node); - // Reload ERC20 tokens box for the new chain - await initErc20TokensBox(); + // Reload ERC20 tokens for the new chain + await initErc20Tokens(); // Reload transaction history from the new chain's file await transactionHistory.init(); @@ -228,179 +220,52 @@ abstract class EVMChainWalletBase await startSync(); } - void addInitialTokens() { + Future addInitialTokens() async { final initialErc20Tokens = EVMChainDefaultTokens.getDefaultTokensByChainId(selectedChainId); for (final token in initialErc20Tokens) { - if (!evmChainErc20TokensBox.containsKey(token.contractAddress)) { - evmChainErc20TokensBox.put(token.contractAddress, token); - } else { - // update existing token - final existingToken = evmChainErc20TokensBox.get(token.contractAddress); - evmChainErc20TokensBox.put( - token.contractAddress, - Erc20Token.copyWith(token, enabled: existingToken!.enabled), - ); - } + final existingToken = _findCachedToken(token.contractAddress); + + final newToken = Erc20Token.copyWith( + token, + enabled: existingToken?.enabled ?? token.enabled, + walletName: walletInfo.name, + chainId: selectedChainId, + ); + + await newToken.save(); + _upsertCachedToken(newToken); } } List get getDefaultTokenContractAddresses => EVMChainDefaultTokens.getDefaultTokenAddresses(selectedChainId); - Future initErc20TokensBox() async { - // Migration for old WalletType.ethereum wallets: - // Old wallets used a global erc20TokensBox (shared across all wallets). - // New system uses wallet-specific, chain-specific boxes. - // This checks if migration is needed and runs it once. - if (walletInfo.type == WalletType.ethereum) { - try { - // Try to access erc20TokensBox - if it exists, migration already ran - final _ = erc20TokensBox; - // Migration done, proceed with normal chain-specific logic below - } catch (_) { - // erc20TokensBox doesn't exist yet, run migration from global box - await _initEthereumErc20TokensBox(); - await _normalizeEvmChainErc20TokensBoxKeys(); - return; - } - } + Future initErc20Tokens() async { + _erc20Tokens = await Erc20Token.getAllForWallet(walletInfo.name, selectedChainId); - final chainId = selectedChainId; - - final boxName = EVMChainUtils.getErc20TokensBoxName(walletInfo.name, chainId); - - // Close existing box if it's already open (for chain switching) - try { - if (evmChainErc20TokensBox.isOpen) { - await evmChainErc20TokensBox.close(); - } - } catch (_) { - // Box might not be initialized yet, ignore - } - - // Check if box is already open, if so use it, otherwise open it - if (CakeHive.isBoxOpen(boxName)) { - evmChainErc20TokensBox = CakeHive.box(boxName); - } else { - evmChainErc20TokensBox = await CakeHive.openBox(boxName); - } - - await _normalizeEvmChainErc20TokensBoxKeys(); - - addInitialTokens(); + await addInitialTokens(); } - /// Ethereum-specific initialization with backward compatibility - Future _initEthereumErc20TokensBox() async { - // Opens a box specific to this wallet - evmChainErc20TokensBox = await CakeHive.openBox( - "${walletInfo.name.replaceAll(" ", "_")}_${Erc20Token.ethereumBoxName}", - ); + Erc20Token? _findCachedToken(String contractAddress) { + final lowerAddress = contractAddress.toLowerCase(); - erc20TokensBox = await CakeHive.openBox(Erc20Token.boxName); - - if (erc20TokensBox.isEmpty) { - if (evmChainErc20TokensBox.isEmpty) addInitialTokens(); - return; + for (final token in _erc20Tokens) { + if (token.contractAddress.toLowerCase() == lowerAddress) return token; } + return null; + } - final allValues = erc20TokensBox.values.toList(); + void _upsertCachedToken(Erc20Token token) { + final lowerAddress = token.contractAddress.toLowerCase(); - // Clear and delete the old token box - await erc20TokensBox.clear(); - await erc20TokensBox.deleteFromDisk(); - - // Add all the previous tokens with configs to the new box - await evmChainErc20TokensBox.addAll(allValues); + _erc20Tokens.removeWhere((t) => t.contractAddress.toLowerCase() == lowerAddress); + _erc20Tokens.add(token); } String getTransactionHistoryFileName() => EVMChainUtils.getTransactionHistoryFileName(selectedChainId); - /// Ensures all ERC20 token entries use lowercase contract addresses as - /// their Hive keys to avoid duplicates caused by case differences. - Future _normalizeEvmChainErc20TokensBoxKeys() async { - if (!evmChainErc20TokensBox.isOpen) return; - - final prefs = await sharedPrefs.future; - final migrationKey = 'erc20_box_normalized_${walletInfo.name}_$selectedChainId'; - - if (prefs.getBool(migrationKey) ?? false) return; - - final box = evmChainErc20TokensBox; - final keys = box.keys.toList(); - - if (keys.isEmpty) { - await prefs.setBool(migrationKey, true); - return; - } - - final Map normalizedTokens = {}; - var needsRewrite = false; - - for (final key in keys) { - final token = box.get(key); - - if (token == null) { - needsRewrite = true; - continue; - } - - final lowerKey = key is String ? key.toLowerCase() : token.contractAddress.toLowerCase(); - if (key is int) needsRewrite = true; - - final Erc20Token normalizedToken = - token.contractAddress == token.contractAddress.toLowerCase() - ? token - : Erc20Token( - name: token.name, - symbol: token.symbol, - contractAddress: token.contractAddress.toLowerCase(), - decimal: token.decimal, - enabled: token.enabled, - iconPath: token.iconPath, - tag: token.tag, - isPotentialScam: token.isPotentialScam, - ); - - if (!needsRewrite && (lowerKey != key || identical(normalizedToken, token) == false)) { - needsRewrite = true; - } - - final existing = normalizedTokens[lowerKey]; - - if (existing == null) { - normalizedTokens[lowerKey] = normalizedToken; - continue; - } - - final merged = Erc20Token( - name: normalizedToken.name, - symbol: normalizedToken.symbol, - contractAddress: lowerKey, - decimal: normalizedToken.decimal, - enabled: normalizedToken.enabled || existing.enabled, - iconPath: (normalizedToken.iconPath?.isNotEmpty ?? false) - ? normalizedToken.iconPath - : existing.iconPath, - tag: normalizedToken.tag ?? existing.tag, - isPotentialScam: normalizedToken.isPotentialScam || existing.isPotentialScam, - ); - - normalizedTokens[lowerKey] = merged; - } - - if (needsRewrite) { - await box.clear(); - for (final entry in normalizedTokens.entries) { - await box.put(entry.key, entry.value); - } - } - - await prefs.setBool(migrationKey, true); - } - Future checkIfScanProviderIsEnabled() async { final key = EVMChainUtils.getScanProviderPreferenceKey(selectedChainId); return (await sharedPrefs.future).getBool(key) ?? true; @@ -451,6 +316,8 @@ abstract class EVMChainWalletBase tag: token.tag ?? EVMChainUtils.getDefaultTokenTag(selectedChainId), iconPath: iconPath, isPotentialScam: token.isPotentialScam, + walletName: walletInfo.name, + chainId: selectedChainId, ); } @@ -499,7 +366,7 @@ abstract class EVMChainWalletBase String idFor(String name, WalletType type) => '${walletTypeToString(type).toLowerCase()}_$name'; Future init() async { - await initErc20TokensBox(); + await initErc20Tokens(); await walletAddresses.init(); await transactionHistory.init(); @@ -669,8 +536,6 @@ abstract class EVMChainWalletBase Future discoverTokensFromMoralis() async { try { - if (!evmChainErc20TokensBox.isOpen) return MoralisDiscoveryResult.empty; - final address = walletAddresses.address; if (address.isEmpty) return MoralisDiscoveryResult.empty; @@ -680,8 +545,7 @@ abstract class EVMChainWalletBase if (walletTokens.isEmpty) return MoralisDiscoveryResult.empty; final existingTokenAddresses = { - for (final token in evmChainErc20TokensBox.values) - token.contractAddress.toLowerCase(): token, + for (final token in _erc20Tokens) token.contractAddress.toLowerCase(): token, }; final whitelistedTokenAddresses = @@ -1419,32 +1283,21 @@ abstract class EVMChainWalletBase } Future _fetchErc20Balances() async { - // Check if box is open before accessing it - if (!evmChainErc20TokensBox.isOpen) { - return; - } - // First, clean up any tokens in balance map that don't belong to current chain // This handles tokens from previous chains that might still be in the balance map final tokensInBalance = balance.keys.whereType().toList(); - final tokensInBox = evmChainErc20TokensBox.values.toList(); - final boxTokenAddresses = tokensInBox.map((t) => t.contractAddress.toLowerCase()).toSet(); + final tokens = _erc20Tokens.toList(); + final cachedTokenAddresses = tokens.map((t) => t.contractAddress.toLowerCase()).toSet(); for (var token in tokensInBalance) { - // Remove token if it's not in the current box or doesn't match current chain - if (!boxTokenAddresses.contains(token.contractAddress.toLowerCase()) || + // Remove token if it's not in the current token list or doesn't match current chain + if (!cachedTokenAddresses.contains(token.contractAddress.toLowerCase()) || !_isTokenMatchingChain(token)) { balance.remove(token); } } - // Get a snapshot of tokens from current box to avoid issues if box is closed during iteration - final tokens = tokensInBox; - for (var token in tokens) { - // Check if box is still open before operating on tokens - if (!evmChainErc20TokensBox.isOpen) break; - if (!_isTokenMatchingChain(token)) { printV('NOTEE!!!: Token ${token.title} is not matching the currency ${currency.title}'); try { @@ -1532,15 +1385,7 @@ abstract class EVMChainWalletBase @override Future updateTransactionsHistory() async => await _updateTransactions(); - List get erc20Currencies { - try { - if (!evmChainErc20TokensBox.isOpen) return []; - - return evmChainErc20TokensBox.values.toList(); - } catch (_) { - return []; - } - } + List get erc20Currencies => _erc20Tokens.toList(); Future addErc20Token(Erc20Token token) async { final isSuspicious = isTokenPropertiesSuspicious(token); @@ -1561,32 +1406,26 @@ abstract class EVMChainWalletBase final newToken = createNewErc20TokenObject(token, iconPath); if (newToken.enabled) { - try { - balance[newToken] = await _client.fetchERC20Balances(_evmChainPrivateKey.address, newToken); + balance[newToken] = await _client.fetchERC20Balances(_evmChainPrivateKey.address, newToken); - await evmChainErc20TokensBox.put(newToken.contractAddress, newToken); - } on Exception catch (_) { - rethrow; - } + await newToken.save(); } else { - await evmChainErc20TokensBox.put(newToken.contractAddress, newToken); + await newToken.save(); balance.remove(newToken); } + + _upsertCachedToken(newToken); } Future deleteErc20Token(Erc20Token token, {bool shouldUpdateBalance = true}) async { - // Check if box is open before trying to delete - if (!evmChainErc20TokensBox.isOpen) { - balance.remove(token); - return; + try { + await Erc20Token.deleteForWallet(walletInfo.name, selectedChainId, token.contractAddress); + } catch (e) { + printV('Error deleting token: $e'); } - try { - await token.delete(); - } catch (e) { - // Token might be from a closed box, just remove from balance - printV('Error deleting token from box: $e'); - } + _erc20Tokens + .removeWhere((t) => t.contractAddress.toLowerCase() == token.contractAddress.toLowerCase()); balance.remove(token); await removeTokenTransactionsInHistory(token); diff --git a/cw_evm/lib/evm_chain_wallet_service.dart b/cw_evm/lib/evm_chain_wallet_service.dart index 5a2930012..ec52c5194 100644 --- a/cw_evm/lib/evm_chain_wallet_service.dart +++ b/cw_evm/lib/evm_chain_wallet_service.dart @@ -1,7 +1,8 @@ -import 'dart:io'; +import "dart:io"; import 'package:bip39/bip39.dart' as bip39; import 'package:cw_core/encryption_file_utils.dart'; +import "package:cw_core/erc20_token.dart"; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_base.dart'; @@ -49,8 +50,8 @@ class EVMChainWalletService extends WalletService< @override WalletType getType() { throw UnsupportedError( - 'EVMChainWalletService is unified and does not have a single type. ' - 'Use walletInfo.type instead.', + "EVMChainWalletService is unified and does not have a single type. " + "Use walletInfo.type instead.", ); } @@ -60,7 +61,7 @@ class EVMChainWalletService extends WalletService< Future saveBackup(String name, {WalletInfo? walletInfo}) async { final info = walletInfo ?? await _findWalletByName(name); if (info == null) { - throw Exception('Wallet not found: $name'); + throw Exception("Wallet not found: $name"); } final backupWalletDirPath = await pathForWalletDir(name: "$name.backup", type: info.type); @@ -76,7 +77,7 @@ class EVMChainWalletService extends WalletService< Future restoreWalletFilesFromBackup(String name) async { final walletInfo = await _findWalletByName(name); if (walletInfo == null) { - throw Exception('Wallet not found: $name'); + throw Exception("Wallet not found: $name"); } final backupWalletDirPath = await pathForWalletDir(name: "$name.backup", type: walletInfo.type); @@ -97,7 +98,7 @@ class EVMChainWalletService extends WalletService< // Get chainId from wallet type final chainConfig = _registry.getChainConfigByWalletType(walletInfo.type); if (chainConfig == null) { - throw Exception('Chain config not found for wallet type: ${walletInfo.type}'); + throw Exception("Chain config not found for wallet type: ${walletInfo.type}"); } final initialChainId = chainConfig.chainId; @@ -125,7 +126,7 @@ class EVMChainWalletService extends WalletService< ); await wallet.init(); - wallet.addInitialTokens(); + await wallet.addInitialTokens(); await wallet.save(); return wallet; } @@ -134,7 +135,7 @@ class EVMChainWalletService extends WalletService< Future openWallet(String name, String password) async { final walletInfo = await _findWalletByName(name); if (walletInfo == null) { - throw Exception('Wallet not found'); + throw Exception("Wallet not found"); } try { @@ -146,7 +147,7 @@ class EVMChainWalletService extends WalletService< ); await wallet.init(); - wallet.addInitialTokens(); + await wallet.addInitialTokens(); await wallet.save(); await saveBackup(name); return wallet; @@ -161,7 +162,7 @@ class EVMChainWalletService extends WalletService< ); await wallet.init(); - wallet.addInitialTokens(); + await wallet.addInitialTokens(); await wallet.save(); return wallet; } @@ -173,7 +174,7 @@ class EVMChainWalletService extends WalletService< final currentWalletInfo = await _findWalletByName(currentName); if (currentWalletInfo == null) { - throw Exception('Wallet not found'); + throw Exception("Wallet not found"); } final type = currentWalletInfo.type; @@ -185,6 +186,16 @@ class EVMChainWalletService extends WalletService< currentWalletInfo.name = newName; await currentWalletInfo.save(); + final oldNameStillUsed = (await _findWalletByName(currentName)) != null; + if (oldNameStillUsed) { + for (final token in await Erc20Token.selectList("walletName = ?", [currentName])) { + final copiedToken = Erc20Token.copyWith(token, walletName: newName); + await copiedToken.save(); + } + } else { + await Erc20Token.renameWallet(currentName, newName); + } + final oldDir = Directory(p.join(await pathForWalletTypeDir(type: type), currentName)); if (oldDir.existsSync()) { try { @@ -209,7 +220,7 @@ class EVMChainWalletService extends WalletService< // Get chainId from wallet type final chainConfig = _registry.getChainConfigByWalletType(walletInfo.type); if (chainConfig == null) { - throw Exception('Chain config not found for wallet type: ${walletInfo.type}'); + throw Exception("Chain config not found for wallet type: ${walletInfo.type}"); } final initialChainId = chainConfig.chainId; @@ -235,7 +246,7 @@ class EVMChainWalletService extends WalletService< ); await wallet.init(); - wallet.addInitialTokens(); + await wallet.addInitialTokens(); await wallet.save(); return wallet; } @@ -250,7 +261,7 @@ class EVMChainWalletService extends WalletService< // Get chainId from wallet type final chainConfig = _registry.getChainConfigByWalletType(walletInfo.type); if (chainConfig == null) { - throw Exception('Chain config not found for wallet type: ${walletInfo.type}'); + throw Exception("Chain config not found for wallet type: ${walletInfo.type}"); } final initialChainId = chainConfig.chainId; @@ -275,7 +286,7 @@ class EVMChainWalletService extends WalletService< ); await wallet.init(); - wallet.addInitialTokens(); + await wallet.addInitialTokens(); await wallet.save(); return wallet; } @@ -289,7 +300,7 @@ class EVMChainWalletService extends WalletService< // Get chainId from wallet type final chainConfig = _registry.getChainConfigByWalletType(walletInfo.type); if (chainConfig == null) { - throw Exception('Chain config not found for wallet type: ${walletInfo.type}'); + throw Exception("Chain config not found for wallet type: ${walletInfo.type}"); } final initialChainId = chainConfig.chainId; @@ -313,7 +324,7 @@ class EVMChainWalletService extends WalletService< ); await wallet.init(); - wallet.addInitialTokens(); + await wallet.addInitialTokens(); await wallet.save(); return wallet; } @@ -332,11 +343,15 @@ class EVMChainWalletService extends WalletService< Future remove(String wallet) async { final walletInfo = await _findWalletByName(wallet); if (walletInfo == null) { - throw Exception('Wallet not found'); + throw Exception("Wallet not found"); } File(await pathForWalletDir(name: wallet, type: walletInfo.type)).delete(recursive: true); await WalletInfo.delete(walletInfo); + final nameStillUsed = (await _findWalletByName(wallet)) != null; + if (!nameStillUsed) { + await Erc20Token.deleteAllForWallet(wallet); + } } EVMChainWallet _createWalletInstance({ @@ -354,7 +369,7 @@ class EVMChainWalletService extends WalletService< final chainConfig = _registry.getChainConfigByWalletType(walletType); if (chainConfig == null) { - throw Exception('Chain config not found for wallet type: $walletType'); + throw Exception("Chain config not found for wallet type: $walletType"); } return EVMChainWallet( diff --git a/cw_evm/lib/utils/evm_chain_utils.dart b/cw_evm/lib/utils/evm_chain_utils.dart index 60355cabf..969ffc7b3 100644 --- a/cw_evm/lib/utils/evm_chain_utils.dart +++ b/cw_evm/lib/utils/evm_chain_utils.dart @@ -1,11 +1,9 @@ -import 'package:cw_core/erc20_token.dart'; -import 'package:cw_evm/evm_chain_transaction_priority.dart'; -import 'package:web3dart/web3dart.dart' show EtherAmount, EtherUnit; +import "package:cw_evm/evm_chain_transaction_priority.dart"; +import "package:web3dart/web3dart.dart" show EtherAmount, EtherUnit; /// Utility class for chain-specific EVM chain operations class EVMChainUtils { - static int getTotalPriorityFee(EVMChainTransactionPriority priority, int chainId) { - return switch (chainId) { + static int getTotalPriorityFee(EVMChainTransactionPriority priority, int chainId) => switch (chainId) { 1 => _ethereumPriorityFee(priority), 137 => _polygonPriorityFee(priority), 8453 => _basePriorityFee(priority), @@ -13,14 +11,11 @@ class EVMChainUtils { 42161 => 0, // Arbitrum doesn't use priority fees _ => _ethereumPriorityFee(priority), }; - } - static bool hasPriorityFee(int chainId) { - return switch (chainId) { + static bool hasPriorityFee(int chainId) => switch (chainId) { 42161 => false, // Arbitrum doesn't use priority fees _ => true, }; - } static int computeBufferedMaxFeePerGasWei({ required int? gasBaseFee, @@ -38,78 +33,53 @@ class EVMChainUtils { return gasPrice + priorityFeeWei; } - static String getErc20TokensBoxName(String walletName, int chainId) { - final sanitizedName = walletName.replaceAll(" ", "_"); - - return switch (chainId) { - 1 => "${sanitizedName}_${Erc20Token.ethereumBoxName}", - 137 => "${sanitizedName}_${Erc20Token.polygonBoxName}", - 8453 => "${sanitizedName}_${Erc20Token.baseBoxName}", - 42161 => "${sanitizedName}_${Erc20Token.arbitrumBoxName}", - 56 => "${sanitizedName}_${Erc20Token.bscBoxName}", - _ => "${sanitizedName}_${Erc20Token.ethereumBoxName}", + static String getTransactionHistoryFileName(int chainId) => switch (chainId) { + 1 => "transactions.json", // Ethereum + 137 => "polygon_transactions.json", + 8453 => "base_transactions.json", + 42161 => "arbitrum_transactions.json", + 56 => "bsc_transactions.json", + _ => "transactions_$chainId.json", // Generic format for other chains }; - } - - static String getTransactionHistoryFileName(int chainId) { - return switch (chainId) { - 1 => 'transactions.json', // Ethereum - 137 => 'polygon_transactions.json', - 8453 => 'base_transactions.json', - 42161 => 'arbitrum_transactions.json', - 56 => 'bsc_transactions.json', - _ => 'transactions_$chainId.json', // Generic format for other chains - }; - } /// Get scan provider preference key for a wallet type - static String getScanProviderPreferenceKey(int chainId) { - return switch (chainId) { - 1 => 'use_etherscan', - 137 => 'use_polygonscan', - 8453 => 'use_basescan', - 42161 => 'use_arbiscan', - 56 => 'use_bscscan', - _ => 'use_etherscan', + static String getScanProviderPreferenceKey(int chainId) => switch (chainId) { + 1 => "use_etherscan", + 137 => "use_polygonscan", + 8453 => "use_basescan", + 42161 => "use_arbiscan", + 56 => "use_bscscan", + _ => "use_etherscan", }; - } - static String getDefaultTokenTag(int chainId) { - return switch (chainId) { - 1 => 'ETH', - 137 => 'POL', - 8453 => 'BASE', - 42161 => 'ARB', - 56 => 'BSC', - _ => 'ETH', + static String getDefaultTokenTag(int chainId) => switch (chainId) { + 1 => "ETH", + 137 => "POL", + 8453 => "BASE", + 42161 => "ARB", + 56 => "BSC", + _ => "ETH", }; - } - static String getFeeCurrency(int chainId) { - return switch (chainId) { - 1 => 'ETH', - 137 => 'POL', - 8453 => 'ETH', - 42161 => 'ETH', - 56 => 'BNB', - _ => 'ETH', + static String getFeeCurrency(int chainId) => switch (chainId) { + 1 => "ETH", + 137 => "POL", + 8453 => "ETH", + 42161 => "ETH", + 56 => "BNB", + _ => "ETH", }; - } - static String getDefaultTokenSymbol(int chainId) { - return switch (chainId) { - 1 => 'ETH', - 137 => 'POL', - 8453 => 'BASE', - 42161 => 'ARBITRUM', - 56 => 'BSC', - _ => 'ETH', + static String getDefaultTokenSymbol(int chainId) => switch (chainId) { + 1 => "ETH", + 137 => "POL", + 8453 => "BASE", + 42161 => "ARBITRUM", + 56 => "BSC", + _ => "ETH", }; - } - static int _ethereumPriorityFee(EVMChainTransactionPriority priority) { - return EtherAmount.fromInt(EtherUnit.gwei, priority.tip).getInWei.toInt(); - } + static int _ethereumPriorityFee(EVMChainTransactionPriority priority) => EtherAmount.fromInt(EtherUnit.gwei, priority.tip).getInWei.toInt(); // Polygon priority fee calculation (minimum 25 gwei + additional based on priority) static int _polygonPriorityFee(EVMChainTransactionPriority priority) { @@ -127,12 +97,10 @@ class EVMChainUtils { return minPriorityFeeWei + additionalPriorityFee; } - static int _basePriorityFee(EVMChainTransactionPriority priority) { - return switch (priority) { + static int _basePriorityFee(EVMChainTransactionPriority priority) => switch (priority) { EVMChainTransactionPriority.fast => EtherAmount.fromInt(EtherUnit.mwei, 5).getInWei.toInt(), EVMChainTransactionPriority.medium => EtherAmount.fromInt(EtherUnit.mwei, 3).getInWei.toInt(), EVMChainTransactionPriority.slow => EtherAmount.fromInt(EtherUnit.mwei, 1).getInWei.toInt(), _ => EtherAmount.fromInt(EtherUnit.mwei, 1).getInWei.toInt(), }; - } } diff --git a/cw_solana/lib/solana_wallet.dart b/cw_solana/lib/solana_wallet.dart index 31a1bc3d2..b41ee421c 100644 --- a/cw_solana/lib/solana_wallet.dart +++ b/cw_solana/lib/solana_wallet.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:convert'; import 'package:cw_core/amount/money.dart'; -import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/node.dart'; @@ -28,7 +27,6 @@ import 'package:cw_solana/solana_transaction_model.dart'; import 'package:cw_solana/solana_wallet_addresses.dart'; import 'package:cw_core/spl_token.dart'; import 'package:hex/hex.dart'; -import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:on_chain/solana/solana.dart' hide Store; @@ -67,10 +65,6 @@ abstract class SolanaWalletBase encryptionFileUtils: encryptionFileUtils, ); - if (!CakeHive.isAdapterRegistered(SPLToken.typeId)) { - CakeHive.registerAdapter(SPLTokenAdapter()); - } - _sharedPrefs.complete(SharedPreferences.getInstance()); } @@ -90,7 +84,7 @@ abstract class SolanaWalletBase Future? _currentRefresh; - late final Box splTokensBox; + List _splTokens = []; @override WalletAddresses walletAddresses; @@ -133,9 +127,7 @@ abstract class SolanaWalletBase ); Future init() async { - final boxName = "${walletInfo.name.replaceAll(" ", "_")}_${SPLToken.boxName}"; - - splTokensBox = await CakeHive.openBox(boxName); + _splTokens = await SPLToken.getAllForWallet(walletInfo.name); await _checkForExistingScamTokens(); @@ -160,8 +152,6 @@ abstract class SolanaWalletBase 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; @@ -169,7 +159,7 @@ abstract class SolanaWalletBase final defaultSymbolsUpper = DefaultSPLTokens().initialSPLTokens.map((t) => t.symbol.toUpperCase()).toSet(); - for (final token in splTokensBox.values) { + for (final token in _splTokens) { final suspicious = isTokenPropertiesSuspicious( token, cachedDefaultMints: defaultMints, @@ -421,7 +411,7 @@ abstract class SolanaWalletBase } Future updateSPLTokenTransactions({List? specificMints}) async { - final allTokens = splTokensBox.values.where((t) => t.enabled).toList(growable: false); + final allTokens = _splTokens.where((t) => t.enabled).toList(growable: false); // Filter to specific mints if provided final tokens = specificMints != null @@ -618,11 +608,11 @@ abstract class SolanaWalletBase List? tokenMints, }) async { // Remove disabled tokens first to keep state clean - for (var token in splTokensBox.values.where((t) => !t.enabled)) { + for (var token in _splTokens.where((t) => !t.enabled)) { balance.remove(token); } - final enabledTokens = splTokensBox.values.where((t) => t.enabled).toList(growable: false); + final enabledTokens = _splTokens.where((t) => t.enabled).toList(growable: false); if (enabledTokens.isEmpty) return; final tokens = tokenMints == null || tokenMints.isEmpty @@ -677,35 +667,48 @@ abstract class SolanaWalletBase } } - List get splTokenCurrencies => splTokensBox.values.toList(); + List get splTokenCurrencies => _splTokens.toList(); SPLToken? splTokenBySymbol(String symbol) { - for (final token in splTokensBox.values) { + for (final token in _splTokens) { if (token.symbol == symbol) return token; } return null; } - void addInitialTokens() { + SPLToken? _findCachedToken(String mintAddress) { + for (final token in _splTokens) { + if (token.mintAddress == mintAddress) return token; + } + + return null; + } + + void _upsertCachedToken(SPLToken token) { + _splTokens.removeWhere((t) => t.mintAddress == token.mintAddress); + _splTokens.add(token); + } + + Future addInitialTokens() async { final initialSPLTokens = DefaultSPLTokens().initialSPLTokens; for (var token in initialSPLTokens) { - if (!splTokensBox.containsKey(token.mintAddress)) { - splTokensBox.put(token.mintAddress, token); - } else { - // update existing token - final existingToken = splTokensBox.get(token.mintAddress); - splTokensBox.put( - token.mintAddress, SPLToken.copyWith(token, enabled: existingToken!.enabled)); - } + final existingToken = _findCachedToken(token.mintAddress); + + final newToken = SPLToken.copyWith( + token, + enabled: existingToken?.enabled ?? token.enabled, + walletName: walletInfo.name, + ); + + await newToken.save(); + _upsertCachedToken(newToken); } } Future discoverTokensFromMoralis() async { try { - if (!splTokensBox.isOpen) return SolanaMoralisDiscoveryResult.empty; - final address = walletAddresses.address; if (address.isEmpty) return SolanaMoralisDiscoveryResult.empty; @@ -713,7 +716,7 @@ abstract class SolanaWalletBase if (walletTokens.isEmpty) return SolanaMoralisDiscoveryResult.empty; final existingMints = { - for (final token in splTokensBox.values) token.mintAddress: token, + for (final token in _splTokens) token.mintAddress: token, }; final defaultMints = DefaultSPLTokens().initialSPLTokens.map((t) => t.mintAddress).toSet(); @@ -849,7 +852,9 @@ abstract class SolanaWalletBase final isSuspicious = isTokenPropertiesSuspicious(token); token.isPotentialScam = token.isPotentialScam || isSuspicious; - await splTokensBox.put(token.mintAddress, token); + token.walletName = walletInfo.name; + await token.save(); + _upsertCachedToken(token); if (token.enabled) { final tokenBalance = await _client.getSplTokenBalance(token, solanaAddress) ?? @@ -869,13 +874,10 @@ abstract class SolanaWalletBase sources.add(_nativeSource); } - if (splTokensBox.isOpen) { - sources.addAll(splTokensBox.values - .where((t) => t.symbol == token.symbol) - .map((t) => t.mintAddress)); + sources.addAll(_splTokens.where((t) => t.symbol == token.symbol).map((t) => t.mintAddress)); - await splTokensBox.delete(token.mintAddress); - } + await SPLToken.deleteForWallet(walletInfo.name, token.mintAddress); + _splTokens.removeWhere((t) => t.mintAddress == token.mintAddress); balance.remove(token); await _removeTokenTransactionsInHistory(token); diff --git a/cw_solana/lib/solana_wallet_service.dart b/cw_solana/lib/solana_wallet_service.dart index 1ed55c411..a557507f5 100644 --- a/cw_solana/lib/solana_wallet_service.dart +++ b/cw_solana/lib/solana_wallet_service.dart @@ -4,6 +4,7 @@ import 'package:bip39/bip39.dart' as bip39; import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/balance.dart'; import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/spl_token.dart'; import 'package:cw_core/transaction_history.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/wallet_base.dart'; @@ -40,7 +41,7 @@ class SolanaWalletService extends WalletService< ); await wallet.init(); - wallet.addInitialTokens(); + await wallet.addInitialTokens(); await wallet.save(); return wallet; } @@ -68,7 +69,7 @@ class SolanaWalletService extends WalletService< ); await wallet.init(); - wallet.addInitialTokens(); + await wallet.addInitialTokens(); await wallet.save(); saveBackup(name); return wallet; @@ -83,7 +84,7 @@ class SolanaWalletService extends WalletService< ); await wallet.init(); - wallet.addInitialTokens(); + await wallet.addInitialTokens(); await wallet.save(); return wallet; } @@ -97,6 +98,11 @@ class SolanaWalletService extends WalletService< throw Exception('Wallet not found'); } await WalletInfo.delete(walletInfo); + final nameStillUsed = await WalletInfo.get(wallet, getType()) != null; + if (!nameStillUsed) { + await SPLToken.deleteAllForWallet(wallet); + } + final prefs = await SharedPreferences.getInstance(); for (final key in prefs.getKeys().where( (k) => k.startsWith('solana_last_synced_signature_${wallet}_'))) { @@ -116,7 +122,7 @@ class SolanaWalletService extends WalletService< ); await wallet.init(); - wallet.addInitialTokens(); + await wallet.addInitialTokens(); await wallet.save(); return wallet; @@ -139,7 +145,7 @@ class SolanaWalletService extends WalletService< ); await wallet.init(); - wallet.addInitialTokens(); + await wallet.addInitialTokens(); await wallet.save(); return wallet; diff --git a/cw_tron/lib/tron_wallet.dart b/cw_tron/lib/tron_wallet.dart index 6da85fe15..32ff8c75c 100644 --- a/cw_tron/lib/tron_wallet.dart +++ b/cw_tron/lib/tron_wallet.dart @@ -5,7 +5,6 @@ import 'dart:developer'; import 'package:bip39/bip39.dart' as bip39; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_core/amount/money.dart'; -import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/node.dart'; @@ -29,7 +28,6 @@ import 'package:cw_tron/tron_transaction_credentials.dart'; import 'package:cw_tron/tron_transaction_history.dart'; import 'package:cw_tron/tron_transaction_info.dart'; import 'package:cw_tron/tron_wallet_addresses.dart'; -import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:on_chain/on_chain.dart'; @@ -62,10 +60,6 @@ abstract class TronWalletBase this.walletInfo = walletInfo; transactionHistory = TronTransactionHistory( walletInfo: walletInfo, password: password, encryptionFileUtils: encryptionFileUtils); - - if (!CakeHive.isAdapterRegistered(TronToken.typeId)) { - CakeHive.registerAdapter(TronTokenAdapter()); - } } final String? _mnemonic; @@ -73,7 +67,7 @@ abstract class TronWalletBase final String _password; final EncryptionFileUtils encryptionFileUtils; - late final Box tronTokensBox; + List _tronTokens = []; late final TronPrivateKey _tronPrivateKey; @@ -107,7 +101,7 @@ abstract class TronWalletBase late ObservableMap balance; Future init() async { - await initTronTokensBox(); + await initTronTokens(); await walletAddresses.init(); await transactionHistory.init(); @@ -179,25 +173,38 @@ abstract class TronWalletBase ); } - void addInitialTokens() { + Future addInitialTokens() async { final initialTronTokens = DefaultTronTokens().initialTronTokens; for (var token in initialTronTokens) { - if (!tronTokensBox.containsKey(token.contractAddress)) { - tronTokensBox.put(token.contractAddress, token); - } else { - // update existing token - final existingToken = tronTokensBox.get(token.contractAddress); - tronTokensBox.put( - token.contractAddress, TronToken.copyWith(token, enabled: existingToken!.enabled)); - } + final existingToken = _findCachedToken(token.contractAddress); + + final newToken = TronToken.copyWith( + token, + enabled: existingToken?.enabled ?? token.enabled, + walletName: walletInfo.name, + ); + + await newToken.save(); + _upsertCachedToken(newToken); } } - Future initTronTokensBox() async { - final boxName = "${walletInfo.name.replaceAll(" ", "_")}_${TronToken.boxName}"; + Future initTronTokens() async { + _tronTokens = await TronToken.getAllForWallet(walletInfo.name); + } - tronTokensBox = await CakeHive.openBox(boxName); + TronToken? _findCachedToken(String contractAddress) { + for (final token in _tronTokens) { + if (token.contractAddress == contractAddress) return token; + } + + return null; + } + + void _upsertCachedToken(TronToken token) { + _tronTokens.removeWhere((t) => t.contractAddress == token.contractAddress); + _tronTokens.add(token); } String idFor(String name, WalletType type) => '${walletTypeToString(type).toLowerCase()}_$name'; @@ -516,7 +523,7 @@ abstract class TronWalletBase } Future _fetchTronTokenBalances() async { - for (var token in tronTokensBox.values) { + for (var token in _tronTokens.toList()) { try { if (token.enabled) { balance[token] = await _client.fetchTronTokenBalances( @@ -551,7 +558,7 @@ abstract class TronWalletBase } } - List get tronTokenCurrencies => tronTokensBox.values.toList(); + List get tronTokenCurrencies => _tronTokens.toList(); Future addTronToken(TronToken token) async { String? iconPath; @@ -574,9 +581,11 @@ abstract class TronWalletBase tag: token.tag ?? "TRX", iconPath: iconPath, isPotentialScam: token.isPotentialScam, + walletName: walletInfo.name, ); - await tronTokensBox.put(newToken.contractAddress, newToken); + await newToken.save(); + _upsertCachedToken(newToken); if (newToken.enabled) { balance[newToken] = await _client @@ -587,9 +596,8 @@ abstract class TronWalletBase } Future deleteTronToken(TronToken token) async { - if (tronTokensBox.isOpen) { - await tronTokensBox.delete(token.contractAddress); - } + await TronToken.deleteForWallet(walletInfo.name, token.contractAddress); + _tronTokens.removeWhere((t) => t.contractAddress == token.contractAddress); balance.remove(token); await _removeTokenTransactionsInHistory(token); diff --git a/cw_tron/lib/tron_wallet_service.dart b/cw_tron/lib/tron_wallet_service.dart index 9b81a9a57..5c2e30a82 100644 --- a/cw_tron/lib/tron_wallet_service.dart +++ b/cw_tron/lib/tron_wallet_service.dart @@ -6,6 +6,7 @@ import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/transaction_history.dart'; import 'package:cw_core/transaction_info.dart'; +import 'package:cw_core/tron_token.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_service.dart'; @@ -45,7 +46,7 @@ class TronWalletService extends WalletService< ); await wallet.init(); - wallet.addInitialTokens(); + await wallet.addInitialTokens(); await wallet.save(); return wallet; @@ -67,7 +68,7 @@ class TronWalletService extends WalletService< ); await wallet.init(); - wallet.addInitialTokens(); + await wallet.addInitialTokens(); await wallet.save(); saveBackup(name); return wallet; @@ -82,7 +83,7 @@ class TronWalletService extends WalletService< ); await wallet.init(); - wallet.addInitialTokens(); + await wallet.addInitialTokens(); await wallet.save(); return wallet; } @@ -102,7 +103,7 @@ class TronWalletService extends WalletService< ); await wallet.init(); - wallet.addInitialTokens(); + await wallet.addInitialTokens(); await wallet.save(); return wallet; @@ -127,7 +128,7 @@ class TronWalletService extends WalletService< ); await wallet.init(); - wallet.addInitialTokens(); + await wallet.addInitialTokens(); await wallet.save(); return wallet; @@ -145,6 +146,10 @@ class TronWalletService extends WalletService< throw Exception('Wallet not found'); } await WalletInfo.delete(walletInfo); + final nameStillUsed = await WalletInfo.get(wallet, getType()) != null; + if (!nameStillUsed) { + await TronToken.deleteAllForWallet(wallet); + } } @override diff --git a/lib/core/backup_service.dart b/lib/core/backup_service.dart index f195f2ac8..e85622af5 100644 --- a/lib/core/backup_service.dart +++ b/lib/core/backup_service.dart @@ -18,6 +18,9 @@ import 'package:cake_wallet/core/key_service.dart'; import 'package:cake_wallet/entities/encrypt.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cake_wallet/entities/secret_store_key.dart'; +import 'package:cw_core/erc20_token_legacy.dart' show performErc20TokenHiveMigration; +import 'package:cw_core/spl_token_legacy.dart' show performSplTokenHiveMigration; +import 'package:cw_core/tron_token_legacy.dart' show performTronTokenHiveMigration; import 'package:cw_core/wallet_info.dart'; import 'package:cake_wallet/exchange/trade_legacy.dart'; import 'package:cake_wallet/.secrets.g.dart' as secrets; @@ -113,6 +116,10 @@ class $BackupService { Future verifyWallets() async { await performHiveMigration(); // for backups made before sqlite migration await performTradeHiveMigration(_secureStorage); + await performErc20TokenHiveMigration(); + await performSplTokenHiveMigration(); + await performTronTokenHiveMigration(); + correctWallets = (await WalletInfo.getAll()) .where((info) => availableWalletTypes.contains(info.type)) .toList(); diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index 3934b6330..ae94b47e1 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -27,7 +27,6 @@ import 'package:cw_core/wallet_type.dart'; import 'package:encrypt/encrypt.dart' as encrypt; import 'package:hive/hive.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/erc20_token.dart'; const newCakeWalletMoneroUri = 'xmr-node.cakewallet.com:18081'; @@ -1270,22 +1269,22 @@ Future _addXautTokenToExistingEthereumWallets() async { final ethereumWallets = allWallets.where((wallet) => wallet.type == WalletType.ethereum).toList(); + const ethereumChainId = 1; for (final walletInfo in ethereumWallets) { - final sanitizedName = walletInfo.name.replaceAll(' ', '_'); - final boxName = '${sanitizedName}_${Erc20Token.ethereumBoxName}'; + final existingToken = await Erc20Token.getByContract( + walletInfo.name, + ethereumChainId, + xautToken.contractAddress, + ); - Box tokenBox; - if (CakeHive.isBoxOpen(boxName)) { - tokenBox = CakeHive.box(boxName); - } else { - tokenBox = await CakeHive.openBox(boxName); - } + if (existingToken != null) continue; - final xautAddress = xautToken.contractAddress; - if (!tokenBox.containsKey(xautAddress)) { - await tokenBox.put(xautAddress, xautToken); - } + await Erc20Token.copyWith( + xautToken, + walletName: walletInfo.name, + chainId: ethereumChainId, + ).save(); } } catch (e) { printV('Error in XAUT migration: $e'); @@ -1309,20 +1308,11 @@ Future _addXaut0TokenToExistingSolanaWallets() async { final solanaWallets = allWallets.where((wallet) => wallet.type == WalletType.solana).toList(); for (final walletInfo in solanaWallets) { - final sanitizedName = walletInfo.name.replaceAll(' ', '_'); - final boxName = '${sanitizedName}_${SPLToken.boxName}'; + final existingToken = await SPLToken.getByMint(walletInfo.name, xaut0Token.mintAddress); - Box tokenBox; - if (CakeHive.isBoxOpen(boxName)) { - tokenBox = CakeHive.box(boxName); - } else { - tokenBox = await CakeHive.openBox(boxName); - } + if (existingToken != null) continue; - final xaut0Address = xaut0Token.mintAddress; - if (!tokenBox.containsKey(xaut0Address)) { - await tokenBox.put(xaut0Address, xaut0Token); - } + await SPLToken.copyWith(xaut0Token, walletName: walletInfo.name).save(); } } catch (e) { printV('Error in XAUT0 migration: $e'); @@ -1346,20 +1336,13 @@ Future _addTbbTokenToExistingSolanaWallets() async { final solanaWallets = allWallets.where((wallet) => wallet.type == WalletType.solana).toList(); for (final walletInfo in solanaWallets) { - final sanitizedName = walletInfo.name.replaceAll(" ", "_"); - final boxName = "${sanitizedName}_${SPLToken.boxName}"; + final existingToken = await SPLToken.getByMint(walletInfo.name, tbbToken.mintAddress); - Box tokenBox; - if (CakeHive.isBoxOpen(boxName)) { - tokenBox = CakeHive.box(boxName); - } else { - tokenBox = await CakeHive.openBox(boxName); + if (existingToken != null) { + continue; } - final tbbAddress = tbbToken.mintAddress; - if (!tokenBox.containsKey(tbbAddress)) { - await tokenBox.put(tbbAddress, tbbToken); - } + await SPLToken.copyWith(tbbToken, walletName: walletInfo.name).save(); } } catch (e) { printV("Error in TBB migration: $e"); diff --git a/lib/main.dart b/lib/main.dart index 2f248858e..02229c80a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -43,7 +43,7 @@ import 'package:cake_wallet/zcash/zcash.dart'; import 'package:cw_core/address_info.dart'; import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/db/sqlite.dart'; -import 'package:cw_core/erc20_token.dart'; +import 'package:cw_core/erc20_token_legacy.dart' show performErc20TokenHiveMigration; import 'package:cw_core/hive_type_ids.dart'; import 'package:cw_core/key.dart'; import 'package:cw_core/mweb_utxo.dart'; @@ -51,8 +51,8 @@ import 'package:cw_core/node.dart'; import 'package:cw_core/node_legacy.dart' show performNodeHiveMigration; import 'package:cw_core/payjoin_session.dart'; import 'package:cw_core/root_dir.dart'; -import 'package:cw_core/spl_token.dart'; -import 'package:cw_core/tron_token.dart'; +import 'package:cw_core/spl_token_legacy.dart' show performSplTokenHiveMigration; +import 'package:cw_core/tron_token_legacy.dart' show performTronTokenHiveMigration; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/utils/proxy_logger/memory_proxy_logger.dart'; @@ -265,18 +265,10 @@ Future initializeAppConfigs({bool loadWallet = true}) async { CakeHive.registerAdapter(PayjoinSessionAdapter()); } - if (!CakeHive.isAdapterRegistered(Erc20Token.typeId)) { - CakeHive.registerAdapter(Erc20TokenAdapter()); - } - - if (!CakeHive.isAdapterRegistered(SPLToken.typeId)) { - CakeHive.registerAdapter(SPLTokenAdapter()); - } - - if (!CakeHive.isAdapterRegistered(TronToken.typeId)) { - CakeHive.registerAdapter(TronTokenAdapter()); - } await performHiveMigration(); + await performErc20TokenHiveMigration(); + await performSplTokenHiveMigration(); + await performTronTokenHiveMigration(); final secureStorage = secureStorageShared; final transactionDescriptionsBoxKey = diff --git a/lib/utils/token_utilities.dart b/lib/utils/token_utilities.dart index b70a061a7..efb7b684d 100644 --- a/lib/utils/token_utilities.dart +++ b/lib/utils/token_utilities.dart @@ -26,10 +26,10 @@ class TokenUtilities { for (final wallet in evmWallets) { final chain = getTokenNameBasedOnWalletType(wallet.type); - final box = await _openEvmTokensBoxFor(wallet); + final tokens = await Erc20Token.getAllForWallet(wallet.name, _getDefaultChainId(wallet.type)); - for (final t in box.values.where((t) => t.enabled)) { - final key = '$chain|${t.contractAddress.toLowerCase()}'; + for (final t in tokens.where((t) => t.enabled)) { + final key = "$chain|${t.contractAddress.toLowerCase()}"; if (seen.add(key)) { unique.add(t); } @@ -47,15 +47,17 @@ class TokenUtilities { final tokens = []; for (final wallet in solWallets) { - final box = await _openSolTokensBoxFor(wallet); - tokens.addAll(box.values.where((t) => t.enabled)); + final walletTokens = await SPLToken.getAllForWallet(wallet.name); + tokens.addAll(walletTokens.where((t) => t.enabled)); } final seen = {}; final unique = []; for (final token in tokens) { final key = token.mintAddress.toLowerCase(); - if (seen.add(key)) unique.add(token); + if (seen.add(key)) { + unique.add(token); + } } return unique; } @@ -69,25 +71,31 @@ class TokenUtilities { final seen = {}; final unique = []; for (final wallet in tronWallets) { - final box = await _openTronTokensBoxFor(wallet); - for (final t in box.values.where((t) => t.enabled)) { + final walletTokens = await TronToken.getAllForWallet(wallet.name); + for (final t in walletTokens.where((t) => t.enabled)) { final key = t.contractAddress.toLowerCase(); - if (seen.add(key)) unique.add(t); + if (seen.add(key)) { + unique.add(t); + } } } return unique; } static List loadDefaultEvmTokensForSwap() { - if (evm == null) return []; + if (evm == null) { + return []; + } final tokens = []; final seen = {}; for (final chain in evm!.getAllChains()) { for (final token in evm!.getDefaultTokensByChainId(chain.chainId)) { - final key = '${chain.chainId}|${token.contractAddress.toLowerCase()}'; - if (seen.add(key)) tokens.add(token); + final key = "${chain.chainId}|${token.contractAddress.toLowerCase()}"; + if (seen.add(key)) { + tokens.add(token); + } } } @@ -109,7 +117,9 @@ class TokenUtilities { for (final t in [...defaultTokens, ...userTokens]) { final key = '${t.tag ?? 'ETH'}|${t.contractAddress.toLowerCase()}'; - if (seen.add(key)) result.add(t); + if (seen.add(key)) { + result.add(t); + } } return result; @@ -124,7 +134,9 @@ class TokenUtilities { for (final t in [...defaultTokens, ...userTokens]) { final key = t.mintAddress.toLowerCase(); - if (seen.add(key)) result.add(t); + if (seen.add(key)) { + result.add(t); + } } return result; @@ -139,7 +151,9 @@ class TokenUtilities { for (final t in [...defaultTokens, ...userTokens]) { final key = t.contractAddress.toLowerCase(); - if (seen.add(key)) result.add(t); + if (seen.add(key)) { + result.add(t); + } } return result; @@ -153,17 +167,34 @@ class TokenUtilities { required WalletType walletType, required String address, }) async { - if (address.isEmpty) return null; + if (address.isEmpty) { + return null; + } final lower = address.toLowerCase(); final tokens = await getAvailableTokensForNetwork(walletType); for (final t in tokens) { - if (t is Erc20Token && t.contractAddress.toLowerCase() == lower) return t; - if (t is SPLToken && t.mintAddress.toLowerCase() == lower) return t; - if (t is TronToken && t.contractAddress.toLowerCase() == lower) return t; + if (t is Erc20Token && t.contractAddress.toLowerCase() == lower) { + return t; + } + if (t is SPLToken && t.mintAddress.toLowerCase() == lower) { + return t; + } + if (t is TronToken && t.contractAddress.toLowerCase() == lower) { + return t; + } } return null; } + static int _getDefaultChainId(WalletType walletType) => switch (walletType) { + WalletType.ethereum => 1, + WalletType.polygon => 137, + WalletType.base => 8453, + WalletType.arbitrum => 42161, + WalletType.bsc => 56, + _ => 1, + }; + static Future findEvmChainIdForContract( String contractAddress, { int? excludingChainId, @@ -195,45 +226,10 @@ class TokenUtilities { return null; } - static Future> _openEvmTokensBoxFor(WalletInfo walletInfo) async { - final walletKey = walletInfo.name.replaceAll(' ', '_'); - final boxName = _getErc20TokensBoxName(walletKey, walletInfo.type); - - if (CakeHive.isBoxOpen(boxName)) { - return CakeHive.box(boxName); - } - return CakeHive.openBox(boxName); - } - - static String _getErc20TokensBoxName(String walletKey, WalletType walletType) { - return switch (walletType) { - WalletType.ethereum => '${walletKey}_${Erc20Token.ethereumBoxName}', - WalletType.polygon => '${walletKey}_${Erc20Token.polygonBoxName}', - WalletType.base => '${walletKey}_${Erc20Token.baseBoxName}', - WalletType.arbitrum => '${walletKey}_${Erc20Token.arbitrumBoxName}', - WalletType.bsc => '${walletKey}_${Erc20Token.bscBoxName}', - _ => '${walletKey}_${Erc20Token.ethereumBoxName}', - }; - } - - static Future> _openSolTokensBoxFor(WalletInfo wallet) async { - final boxName = '${wallet.name.replaceAll(' ', '_')}_${SPLToken.boxName}'; - if (CakeHive.isBoxOpen(boxName)) { - return CakeHive.box(boxName); - } - return CakeHive.openBox(boxName); - } - - static Future> _openTronTokensBoxFor(WalletInfo walletInfo) async { - final boxName = '${walletInfo.name.replaceAll(' ', '_')}_${TronToken.boxName}'; - if (CakeHive.isBoxOpen(boxName)) { - return CakeHive.box(boxName); - } - return CakeHive.openBox(boxName); - } - static Erc20Token? findErc20Token(CryptoCurrency currency, WalletBase wallet) { - if (currency is Erc20Token) return currency; + if (currency is Erc20Token) { + return currency; + } // More of a fallback for us for (final balanceCurrency in wallet.balance.keys) { @@ -246,10 +242,14 @@ class TokenUtilities { } static Erc20Token? findErc20TokenForSwap(CryptoCurrency currency) { - if (currency is Erc20Token) return currency; + if (currency is Erc20Token) { + return currency; + } for (final token in loadDefaultEvmTokensForSwap()) { - if (_matchesCurrency(token, currency)) return token; + if (_matchesCurrency(token, currency)) { + return token; + } } return null; } @@ -276,19 +276,19 @@ class TokenUtilities { final title = currency.title.toLowerCase(); final tag = currency.tag?.toLowerCase(); - return title == 'eth' || - title == 'ethereum' || - title == 'matic' || - title == 'polygon' || - title == 'base' || - title == 'arbitrum' || - title == 'bnb' || - title == 'bsc' || - title == 'avax' || - title == 'avalanche' || - tag == 'polygon' || - tag == 'bsc' || - tag == 'avalanche'; + return title == "eth" || + title == "ethereum" || + title == "matic" || + title == "polygon" || + title == "base" || + title == "arbitrum" || + title == "bnb" || + title == "bsc" || + title == "avax" || + title == "avalanche" || + tag == "polygon" || + tag == "bsc" || + tag == "avalanche"; } static int getChainId(CryptoCurrency currency) { @@ -298,37 +298,37 @@ class TokenUtilities { // Only check EVM registry for currencies that might be EVM-related final isPotentialEVM = isNativeToken(currency) || (tag != null && - (tag == 'ETH' || tag == 'POL' || tag == 'BASE' || tag == 'ARB' || tag == 'BSC')); + (tag == "ETH" || tag == "POL" || tag == "BASE" || tag == "ARB" || tag == "BSC")); if (isPotentialEVM) { // Try by tag first if available (e.g., 'POL', 'BASE', 'ARB') if (tag != null) { final chainId = evm?.getChainIdByTag(tag); - if (chainId != null) return chainId; + if (chainId != null) { + return chainId; + } } // Try by title (case-insensitive) final titleChainId = evm?.getChainIdByTitle(title); - if (titleChainId != null) return titleChainId; + if (titleChainId != null) { + return titleChainId; + } } // Fallback to hardcoded values for chains not in registry yet - // Avalanche C-Chain - if (title == 'avalanche' || title == 'avax' || tag == 'AVALANCHE') { + if (title == "avalanche" || title == "avax" || tag == "AVALANCHE") { return 43114; } - // Optimism - if (title == 'optimism' || title == 'op' || tag == 'OPTIMISM') { + if (title == "optimism" || title == "op" || tag == "OPTIMISM") { return 10; } - // Fantom Opera - if (title == 'fantom' || title == 'ftm' || tag == 'FANTOM') { + if (title == "fantom" || title == "ftm" || tag == "FANTOM") { return 250; } - // Default to Ethereum mainnet return 1; } @@ -395,7 +395,7 @@ class TokenUtilities { } for (final currency in CryptoCurrency.all) { - if (currency.tag?.toLowerCase() == 'sol') { + if (currency.tag?.toLowerCase() == "sol") { if (currency is SPLToken) { final mintAddress = currency.mintAddress.toLowerCase(); if (addedAddresses.add(mintAddress)) { @@ -406,10 +406,7 @@ class TokenUtilities { } } } - } - - // Handle Tron network - else if (network == WalletType.tron) { + } else if (network == WalletType.tron) { final userTronTokens = await loadAllUniqueTronTokens(); for (final token in userTronTokens) { final contractAddress = token.contractAddress.toLowerCase(); @@ -426,7 +423,7 @@ class TokenUtilities { } for (final currency in CryptoCurrency.all) { - if (currency.tag?.toLowerCase() == 'trx') { + if (currency.tag?.toLowerCase() == "trx") { if (currency is TronToken) { final contractAddress = currency.contractAddress.toLowerCase(); if (addedAddresses.add(contractAddress)) { @@ -442,31 +439,34 @@ class TokenUtilities { return allTokens; } - static bool _matchesCurrency(CryptoCurrency a, CryptoCurrency b) { - return a.title.toUpperCase() == b.title.toUpperCase() && - (a.tag?.toUpperCase() == b.tag?.toUpperCase()); - } + static bool _matchesCurrency(CryptoCurrency a, CryptoCurrency b) => + a.title.toUpperCase() == b.title.toUpperCase() && + (a.tag?.toUpperCase() == b.tag?.toUpperCase()); static Future> _getUserTokensForNetwork(CryptoCurrency baseCurrency) async { final walletType = cryptoCurrencyOrTokenToWalletType(baseCurrency); - if (walletType == null) return []; + if (walletType == null) { + return []; + } if (isEVMCompatibleChain(walletType)) { final tokens = await TokenUtilities.loadAllUniqueEvmTokens(); return tokens.where((token) { - if (baseCurrency.tag == null) return token.tag == baseCurrency.title; + if (baseCurrency.tag == null) { + return token.tag == baseCurrency.title; + } return token.tag?.toLowerCase() == baseCurrency.tag?.toLowerCase(); }).toList(); } if (walletType == WalletType.solana) { - return await loadAllUniqueSolTokens(); + return loadAllUniqueSolTokens(); } if (walletType == WalletType.tron) { - return await loadAllUniqueTronTokens(); + return loadAllUniqueTronTokens(); } return [];