Migrate EVM/Sol/TRX tokens to SQL (#3450)
What changed, and why it matters
This commit replaces the way Cake Wallet stores EVM, Solana, and Tron token lists: it moves them from Hive (a local key-value store) into SQLite (a local SQL database). It also adds migration code that copies existing token data from the old Hive boxes into the new SQLite tables when the app starts or restores a backup. The change is a large data-layer refactor, not a clear security fix or vulnerability. There are some migration edge cases (duplicate wallet names, interrupted migrations, case-insensitive EVM contract addresses) that are explicitly handled and tested.
Treat as a regular data-layer refactor. Review the migration order and exception handling in production, ensure backups are taken before the schema upgrade, and verify that ConflictAlgorithm.replace does not unintentionally clobber user-disabled scam tokens during future default-token updates. No immediate security patch is indicated by the diff alone.
Security signals we found
Large data migration touching token metadata and enabled toggles
Use of ConflictAlgorithm.replace for token upserts could overwrite user-modified fields if default token metadata changes later
Migration swallows exceptions and continues, which may hide corruption but avoids startup crashes
EVM contract addresses are normalized to lowercase; Solana/TRX mint/contract addresses preserve casing
Rename logic deletes rows under the target walletName before updating, which could destroy unrelated token data if a wallet is renamed to an existing name
No parameterized query issues visible; sqflite helpers are used consistently
Evidence from the diff
The patch migrates Erc20Token, SPLToken, and TronToken persistence from Hive boxes to SQLite tables. New model classes implement toMap/fromMap, CRUD helpers, and upsert via ConflictAlgorithm.replace. Legacy Hive adapters are moved to _legacy.dart files and invoked by performTokenHiveMigration() during startup and backup verification. The schema bump from v9 to v10 creates Erc20Token, SPLToken, and TronToken tables with unique indexes keyed by walletName (+ chainId for EVM) and contract/mint address. Wallet rename/remove paths are updated to move or delete associated token rows. Tests cover duplicate-name merging, re-running migrations, interrupted migrations, and rename/delete behavior.
Changed components
cw_core/lib/db/sqlite.dartcw_core/lib/erc20_token.dartcw_core/lib/erc20_token_legacy.dartcw_core/lib/spl_token.dartcw_core/lib/spl_token_legacy.dartcw_core/lib/tron_token.dartcw_core/lib/tron_token_legacy.dartcw_core/lib/wallet_service.dartcw_evm/lib/evm_chain_wallet.dartcw_evm/lib/evm_chain_wallet_service.dartcw_solana/lib/solana_wallet.dartcw_solana/lib/solana_wallet_service.dartcw_tron/lib/tron_wallet.dartcw_tron/lib/tron_wallet_service.dartlib/core/backup_service.dartlib/entities/default_settings_migration.dartlib/main.dartlib/utils/token_utilities.dartInspect captured patch +1888 / −857
diff --git a/cw_core/lib/db/sqlite.dart b/cw_core/lib/db/sqlite.dart
index 43bef5f3..0deffab8 100644
--- a/cw_core/lib/db/sqlite.dart
+++ b/cw_core/lib/db/sqlite.dart
@@ -63,7 +63,7 @@ Future<void> _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<void> _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<void> _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<void> _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<void> _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 d5fde7aa..a0c14173 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<String, Object?> 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<String, dynamic> 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<int> 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<List<Erc20Token>> selectList(
+ String where,
+ List<dynamic> 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<List<Erc20Token>> getAllForWallet(String walletName, int chainId) =>
+ selectList("walletName = ? AND chainId = ?", [walletName, chainId]);
+
+ static Future<Erc20Token?> 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<int> deleteForWallet(String walletName, int chainId, String contractAddress) =>
+ db!.delete(
+ tableName,
+ where: "walletName = ? AND chainId = ? AND contractAddress = ?",
+ whereArgs: [walletName, chainId, contractAddress.toLowerCase()],
+ );
+
+ static Future<int> deleteAllForWallet(String walletName) =>
+ db!.delete(tableName, where: "walletName = ?", whereArgs: [walletName]);
+
+ static Future<void> 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.part.dart b/cw_core/lib/erc20_token.part.dart
deleted file mode 100644
index e19b4b52..00000000
--- a/cw_core/lib/erc20_token.part.dart
+++ /dev/null
@@ -1,59 +0,0 @@
-// GENERATED CODE - DO NOT MODIFY BY HAND
-
-part of 'erc20_token.dart';
-
-// **************************************************************************
-// TypeAdapterGenerator
-// **************************************************************************
-
-class Erc20TokenAdapter extends TypeAdapter<Erc20Token> {
- @override
- final int typeId = 12;
-
- @override
- Erc20Token read(BinaryReader reader) {
- final numOfFields = reader.readByte();
- final fields = <int, dynamic>{
- for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
- };
- return Erc20Token(
- name: fields[0] as String,
- symbol: fields[1] as String,
- contractAddress: fields[2] as String,
- decimal: fields[3] as int,
- iconPath: fields[5] as String?,
- tag: fields[6] as String?,
- isPotentialScam: fields[7] == null ? false : fields[7] as bool,
- ).._enabled = fields[4] == null ? true : fields[4] as bool;
- }
-
- @override
- void write(BinaryWriter writer, Erc20Token obj) {
- writer
- ..writeByte(8)
- ..writeByte(0)
- ..write(obj.name)
- ..writeByte(1)
- ..write(obj.symbol)
- ..writeByte(2)
- ..write(obj.contractAddress)
- ..writeByte(3)
- ..write(obj.decimal)
- ..writeByte(4)
- ..write(obj._enabled)
- ..writeByte(5)
- ..write(obj.iconPath)
- ..writeByte(6)
- ..write(obj.tag)
- ..writeByte(7)
- ..write(obj.isPotentialScam);
- }
-
- @override
- int get hashCode => typeId.hashCode;
-
- @override
- bool operator ==(Object other) =>
- identical(this, other) ||
- other is Erc20TokenAdapter && runtimeType == other.runtimeType && typeId == other.typeId;
-}
diff --git a/cw_core/lib/erc20_token_legacy.dart b/cw_core/lib/erc20_token_legacy.dart
new file mode 100644
index 00000000..c965899c
--- /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<void> 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<void> migrateAllToSqlite(List<WalletInfo> wallets) async {
+ await _migrateLegacyGlobalBox(wallets);
+
+ final sanitizedToRawNames = <String, Set<String>>{};
+ for (final wallet in wallets) {
+ if (!evmWalletTypes.contains(wallet.type)) {
+ continue;
+ }
+
+ sanitizedToRawNames
+ .putIfAbsent(wallet.name.replaceAll(" ", "_"), () => <String>{})
+ .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<Erc20Token>(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<void> _migrateLegacyGlobalBox(List<WalletInfo> wallets) async {
+ try {
+ if (!await CakeHive.boxExists(boxName)) {
+ return;
+ }
+
+ if (!wallets.any((wallet) => wallet.type == WalletType.ethereum)) {
+ return;
+ }
+
+ final box = await CakeHive.openBox<Erc20Token>(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<Erc20Token> box) {
+ final groups = <String, _MergedTokenGroup>{};
+
+ 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<void> 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<dynamic> sourceKeys;
+}
diff --git a/cw_core/lib/erc20_token_legacy.part.dart b/cw_core/lib/erc20_token_legacy.part.dart
new file mode 100644
index 00000000..9614c0c1
--- /dev/null
+++ b/cw_core/lib/erc20_token_legacy.part.dart
@@ -0,0 +1,59 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of "erc20_token_legacy.dart";
+
+// **************************************************************************
+// TypeAdapterGenerator
+// **************************************************************************
+
+class Erc20TokenAdapter extends TypeAdapter<Erc20Token> {
+ @override
+ final int typeId = 12;
+
+ @override
+ Erc20Token read(BinaryReader reader) {
+ final numOfFields = reader.readByte();
+ final fields = <int, dynamic>{
+ for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
+ };
+ return Erc20Token(
+ name: fields[0] as String,
+ symbol: fields[1] as String,
+ contractAddress: fields[2] as String,
+ decimal: fields[3] as int,
+ iconPath: fields[5] as String?,
+ tag: fields[6] as String?,
+ isPotentialScam: fields[7] == null ? false : fields[7] as bool,
+ ).._enabled = fields[4] == null ? true : fields[4] as bool;
+ }
+
+ @override
+ void write(BinaryWriter writer, Erc20Token obj) {
+ writer
+ ..writeByte(8)
+ ..writeByte(0)
+ ..write(obj.name)
+ ..writeByte(1)
+ ..write(obj.symbol)
+ ..writeByte(2)
+ ..write(obj.contractAddress)
+ ..writeByte(3)
+ ..write(obj.decimal)
+ ..writeByte(4)
+ ..write(obj._enabled)
+ ..writeByte(5)
+ ..write(obj.iconPath)
+ ..writeByte(6)
+ ..write(obj.tag)
+ ..writeByte(7)
+ ..write(obj.isPotentialScam);
+ }
+
+ @override
+ int get hashCode => typeId.hashCode;
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ other is Erc20TokenAdapter && runtimeType == other.runtimeType && typeId == other.typeId;
+}
diff --git a/cw_core/lib/spl_token.dart b/cw_core/lib/spl_token.dart
index 50043531..5ac9330f 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<String> 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<String, Object?> 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<String, dynamic> 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<int> 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<List<SPLToken>> selectList(String where, List<dynamic> 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<List<SPLToken>> getAllForWallet(String walletName) async => selectList("walletName = ?", [walletName]);
+
+ static Future<SPLToken?> getByMint(String walletName, String mintAddress) async {
+ final list = await selectList("walletName = ? AND mintAddress = ?", [walletName, mintAddress]);
+ return list.isEmpty ? null : list.first;
+ }
+
+ static Future<int> deleteForWallet(String walletName, String mintAddress) => db!.delete(
+ tableName,
+ where: "walletName = ? AND mintAddress = ?",
+ whereArgs: [walletName, mintAddress],
+ );
+
+ static Future<int> deleteAllForWallet(String walletName) => db!.delete(tableName, where: "walletName = ?", whereArgs: [walletName]);
+
+ static Future<void> 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.part.dart b/cw_core/lib/spl_token.part.dart
deleted file mode 100644
index cede960c..00000000
--- a/cw_core/lib/spl_token.part.dart
+++ /dev/null
@@ -1,62 +0,0 @@
-// GENERATED CODE - DO NOT MODIFY BY HAND
-
-part of 'spl_token.dart';
-
-// **************************************************************************
-// TypeAdapterGenerator
-// **************************************************************************
-
-class SPLTokenAdapter extends TypeAdapter<SPLToken> {
- @override
- final int typeId = 16;
-
- @override
- SPLToken read(BinaryReader reader) {
- final numOfFields = reader.readByte();
- final fields = <int, dynamic>{
- for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
- };
- return SPLToken(
- name: fields[0] as String,
- symbol: fields[1] as String,
- mintAddress: fields[2] as String,
- decimal: fields[3] as int,
- mint: fields[5] as String,
- iconPath: fields[6] as String?,
- tag: fields[7] as String?,
- isPotentialScam: fields[8] == null ? false : fields[8] as bool,
- ).._enabled = fields[4] == null ? true : fields[4] as bool;
- }
-
- @override
- void write(BinaryWriter writer, SPLToken obj) {
- writer
- ..writeByte(9)
- ..writeByte(0)
- ..write(obj.name)
- ..writeByte(1)
- ..write(obj.symbol)
- ..writeByte(2)
- ..write(obj.mintAddress)
- ..writeByte(3)
- ..write(obj.decimal)
- ..writeByte(4)
- ..write(obj._enabled)
- ..writeByte(5)
- ..write(obj.mint)
- ..writeByte(6)
- ..write(obj.iconPath)
- ..writeByte(7)
- ..write(obj.tag)
- ..writeByte(8)
- ..write(obj.isPotentialScam);
- }
-
- @override
- int get hashCode => typeId.hashCode;
-
- @override
- bool operator ==(Object other) =>
- identical(this, other) ||
- other is SPLTokenAdapter && runtimeType == other.runtimeType && typeId == other.typeId;
-}
diff --git a/cw_core/lib/spl_token_legacy.dart b/cw_core/lib/spl_token_legacy.dart
new file mode 100644
index 00000000..aea75191
--- /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<void> 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<void> migrateAllToSqlite(List<WalletInfo> wallets) async {
+ final sanitizedToRawNames = <String, Set<String>>{};
+ for (final wallet in wallets) {
+ if (wallet.type != WalletType.solana) {
+ continue;
+ }
+
+ sanitizedToRawNames
+ .putIfAbsent(wallet.name.replaceAll(" ", "_"), () => <String>{})
+ .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<SPLToken>(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<void> 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_legacy.part.dart b/cw_core/lib/spl_token_legacy.part.dart
new file mode 100644
index 00000000..ca228a65
--- /dev/null
+++ b/cw_core/lib/spl_token_legacy.part.dart
@@ -0,0 +1,62 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of "spl_token_legacy.dart";
+
+// **************************************************************************
+// TypeAdapterGenerator
+// **************************************************************************
+
+class SPLTokenAdapter extends TypeAdapter<SPLToken> {
+ @override
+ final int typeId = 16;
+
+ @override
+ SPLToken read(BinaryReader reader) {
+ final numOfFields = reader.readByte();
+ final fields = <int, dynamic>{
+ for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
+ };
+ return SPLToken(
+ name: fields[0] as String,
+ symbol: fields[1] as String,
+ mintAddress: fields[2] as String,
+ decimal: fields[3] as int,
+ mint: fields[5] as String,
+ iconPath: fields[6] as String?,
+ tag: fields[7] as String?,
+ isPotentialScam: fields[8] == null ? false : fields[8] as bool,
+ ).._enabled = fields[4] == null ? true : fields[4] as bool;
+ }
+
+ @override
+ void write(BinaryWriter writer, SPLToken obj) {
+ writer
+ ..writeByte(9)
+ ..writeByte(0)
+ ..write(obj.name)
+ ..writeByte(1)
+ ..write(obj.symbol)
+ ..writeByte(2)
+ ..write(obj.mintAddress)
+ ..writeByte(3)
+ ..write(obj.decimal)
+ ..writeByte(4)
+ ..write(obj._enabled)
+ ..writeByte(5)
+ ..write(obj.mint)
+ ..writeByte(6)
+ ..write(obj.iconPath)
+ ..writeByte(7)
+ ..write(obj.tag)
+ ..writeByte(8)
+ ..write(obj.isPotentialScam);
+ }
+
+ @override
+ int get hashCode => typeId.hashCode;
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ other is SPLTokenAdapter && runtimeType == other.runtimeType && typeId == other.typeId;
+}
diff --git a/cw_core/lib/tron_token.dart b/cw_core/lib/tron_token.dart
index c4b27ed2..2e1b7ed1 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<String, Object?> 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;
+
+ @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 const typeId = TRON_TOKEN_TYPE_ID;
- static const boxName = 'TronTokens';
+ 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<String, dynamic> 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<int> 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<List<TronToken>> selectList(
+ String where,
+ List<dynamic> 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<List<TronToken>> getAllForWallet(String walletName) =>
+ selectList("walletName = ?", [walletName]);
+
+ static Future<TronToken?> getByContract(String walletName, String contractAddress) async {
+ final list =
+ await selectList("walletName = ? AND contractAddress = ?", [walletName, contractAddress]);
+ return list.isEmpty ? null : list.first;
+ }
+
+ static Future<int> deleteForWallet(String walletName, String contractAddress) => db!.delete(
+ tableName,
+ where: "walletName = ? AND contractAddress = ?",
+ whereArgs: [walletName, contractAddress],
+ );
+
+ static Future<int> deleteAllForWallet(String walletName) =>
+ db!.delete(tableName, where: "walletName = ?", whereArgs: [walletName]);
+
+ static Future<void> 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.part.dart b/cw_core/lib/tron_token.part.dart
deleted file mode 100644
index c8486534..00000000
--- a/cw_core/lib/tron_token.part.dart
+++ /dev/null
@@ -1,59 +0,0 @@
-// GENERATED CODE - DO NOT MODIFY BY HAND
-
-part of 'tron_token.dart';
-
-// **************************************************************************
-// TypeAdapterGenerator
-// **************************************************************************
-
-class TronTokenAdapter extends TypeAdapter<TronToken> {
- @override
- final int typeId = 18;
-
- @override
- TronToken read(BinaryReader reader) {
- final numOfFields = reader.readByte();
- final fields = <int, dynamic>{
- for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
- };
- return TronToken(
- name: fields[0] as String,
- symbol: fields[1] as String,
- contractAddress: fields[2] as String,
- decimal: fields[3] as int,
- iconPath: fields[5] as String?,
- tag: fields[6] as String?,
- isPotentialScam: fields[7] == null ? false : fields[7] as bool,
- ).._enabled = fields[4] == null ? true : fields[4] as bool;
- }
-
- @override
- void write(BinaryWriter writer, TronToken obj) {
- writer
- ..writeByte(8)
- ..writeByte(0)
- ..write(obj.name)
- ..writeByte(1)
- ..write(obj.symbol)
- ..writeByte(2)
- ..write(obj.contractAddress)
- ..writeByte(3)
- ..write(obj.decimal)
- ..writeByte(4)
- ..write(obj._enabled)
- ..writeByte(5)
- ..write(obj.iconPath)
- ..writeByte(6)
- ..write(obj.tag)
- ..writeByte(7)
- ..write(obj.isPotentialScam);
- }
-
- @override
- int get hashCode => typeId.hashCode;
-
- @override
- bool operator ==(Object other) =>
- identical(this, other) ||
- other is TronTokenAdapter && runtimeType == other.runtimeType && typeId == other.typeId;
-}
diff --git a/cw_core/lib/tron_token_legacy.dart b/cw_core/lib/tron_token_legacy.dart
new file mode 100644
index 00000000..c7fd8c90
--- /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<void> 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<void> migrateAllToSqlite(List<WalletInfo> wallets) async {
+ final sanitizedToRawNames = <String, Set<String>>{};
+ for (final wallet in wallets) {
+ if (wallet.type != WalletType.tron) {
+ continue;
+ }
+
+ sanitizedToRawNames
+ .putIfAbsent(wallet.name.replaceAll(" ", "_"), () => <String>{})
+ .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<TronToken>(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<void> 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_legacy.part.dart b/cw_core/lib/tron_token_legacy.part.dart
new file mode 100644
index 00000000..0884d918
--- /dev/null
+++ b/cw_core/lib/tron_token_legacy.part.dart
@@ -0,0 +1,59 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of "tron_token_legacy.dart";
+
+// **************************************************************************
+// TypeAdapterGenerator
+// **************************************************************************
+
+class TronTokenAdapter extends TypeAdapter<TronToken> {
+ @override
+ final int typeId = 18;
+
+ @override
+ TronToken read(BinaryReader reader) {
+ final numOfFields = reader.readByte();
+ final fields = <int, dynamic>{
+ for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
+ };
+ return TronToken(
+ name: fields[0] as String,
+ symbol: fields[1] as String,
+ contractAddress: fields[2] as String,
+ decimal: fields[3] as int,
+ iconPath: fields[5] as String?,
+ tag: fields[6] as String?,
+ isPotentialScam: fields[7] == null ? false : fields[7] as bool,
+ ).._enabled = fields[4] == null ? true : fields[4] as bool;
+ }
+
+ @override
+ void write(BinaryWriter writer, TronToken obj) {
+ writer
+ ..writeByte(8)
+ ..writeByte(0)
+ ..write(obj.name)
+ ..writeByte(1)
+ ..write(obj.symbol)
+ ..writeByte(2)
+ ..write(obj.contractAddress)
+ ..writeByte(3)
+ ..write(obj.decimal)
+ ..writeByte(4)
+ ..write(obj._enabled)
+ ..writeByte(5)
+ ..write(obj.iconPath)
+ ..writeByte(6)
+ ..write(obj.tag)
+ ..writeByte(7)
+ ..write(obj.isPotentialScam);
+ }
+
+ @override
+ int get hashCode => typeId.hashCode;
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ other is TronTokenAdapter && runtimeType == other.runtimeType && typeId == other.typeId;
+}
diff --git a/cw_core/lib/wallet_service.dart b/cw_core/lib/wallet_service.dart
index 8d19a9f8..12219364 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 '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 "dart:convert";
+import "dart:io";
+
+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<N extends WalletCredentials, RFS extends WalletCredentials,
RFK extends WalletCredentials, RFH extends WalletCredentials> {
@@ -29,11 +31,13 @@ abstract class WalletService<N extends WalletCredentials, RFS extends WalletCred
Future<void> remove(String wallet);
Future<void> 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<N extends WalletCredentials, RFS extends WalletCred
currentWalletInfo.name = newName;
await currentWalletInfo.save();
+ await _renameTokenRows(currentName, newName);
+
final oldDir = Directory(p.join(await pathForWalletTypeDir(type: getType()), currentName));
if (oldDir.existsSync()) {
try {
@@ -53,6 +59,16 @@ abstract class WalletService<N extends WalletCredentials, RFS extends WalletCred
}
}
+ Future<void> _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<void> 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<N extends WalletCredentials, RFS extends WalletCred
final jsonSource = await read(path: path, password: password);
try {
final data = json.decode(jsonSource) as Map;
- return data['mnemonic'] as String? ?? '';
+ return data["mnemonic"] as String? ?? "";
} catch (_) {
// if not a valid json
return jsonSource.substring(0, 200);
}
} catch (_) {
// if the file couldn't be opened or read
- return '';
+ return "";
}
}
diff --git a/cw_core/test/token_sqlite_migration_test.dart b/cw_core/test/token_sqlite_migration_test.dart
new file mode 100644
index 00000000..c81c1618
--- /dev/null
+++ b/cw_core/test/token_sqlite_migration_test.dart
@@ -0,0 +1,365 @@
+import "dart:io";
+
+import "package:cw_core/cake_hive.dart";
+import "package:cw_core/db/sqlite.dart";
+import "package:cw_core/erc20_token.dart" as erc20_sql;
+import "package:cw_core/erc20_token_legacy.dart" as erc20_legacy;
+import "package:cw_core/root_dir.dart";
+import "package:cw_core/spl_token.dart" as spl_sql;
+import "package:cw_core/spl_token_legacy.dart" as spl_legacy;
+import "package:cw_core/tron_token.dart" as tron_sql;
+import "package:cw_core/tron_token_legacy.dart" as tron_legacy;
+import "package:cw_core/wallet_type.dart";
+import "package:flutter_test/flutter_test.dart";
+import "package:path_provider_platform_interface/path_provider_platform_interface.dart";
+import "package:sqflite_common_ffi/sqflite_ffi.dart";
+
+// Faking the documents dir keeps getAppDir() off the platform channel, so the
+// test runs with a plain `flutter test` on any host and in CI.
+class _FakePathProviderPlatform extends PathProviderPlatform {
+ _FakePathProviderPlatform(this.root);
+
+ final String root;
+
+ @override
+ Future<String?> getApplicationDocumentsPath() async => root;
+
+ @override
+ Future<String?> getApplicationSupportPath() async => root;
+}
+
+Future<void> main() async {
+ final dataRoot = Directory("./test/data/token_migration");
+
+ Future<void> 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<erc20_legacy.Erc20Token>("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<erc20_legacy.Erc20Token>("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<erc20_legacy.Erc20Token>("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<spl_legacy.SPLToken>("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<tron_legacy.TronToken>("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<spl_legacy.SPLToken>("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 f66c3431..c9e07c4a 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<Erc20Token> erc20TokensBox;
-
- late Box<Erc20Token> evmChainErc20TokensBox;
+ List<Erc20Token> _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<void> 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<String> get getDefaultTokenContractAddresses =>
EVMChainDefaultTokens.getDefaultTokenAddresses(selectedChainId);
- Future<void> 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;
- }
- }
-
- final chainId = selectedChainId;
+ Future<void> initErc20Tokens() async {
+ _erc20Tokens = await Erc20Token.getAllForWallet(walletInfo.name, 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<Erc20Token>(boxName);
- } else {
- evmChainErc20TokensBox = await CakeHive.openBox<Erc20Token>(boxName);
- }
-
- await _normalizeEvmChainErc20TokensBoxKeys();
-
- addInitialTokens();
+ await addInitialTokens();
}
- /// Ethereum-specific initialization with backward compatibility
- Future<void> _initEthereumErc20TokensBox() async {
- // Opens a box specific to this wallet
- evmChainErc20TokensBox = await CakeHive.openBox<Erc20Token>(
- "${walletInfo.name.replaceAll(" ", "_")}_${Erc20Token.ethereumBoxName}",
- );
-
- erc20TokensBox = await CakeHive.openBox<Erc20Token>(Erc20Token.boxName);
+ Erc20Token? _findCachedToken(String contractAddress) {
+ final lowerAddress = contractAddress.toLowerCase();
- 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();
-
- // Clear and delete the old token box
- await erc20TokensBox.clear();
- await erc20TokensBox.deleteFromDisk();
+ void _upsertCachedToken(Erc20Token token) {
+ final lowerAddress = token.contractAddress.toLowerCase();
- // 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<void> _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<String, Erc20Token> 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<bool> 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<void> init() async {
- await initErc20TokensBox();
+ await initErc20Tokens();
await walletAddresses.init();
await transactionHistory.init();
@@ -669,8 +536,6 @@ abstract class EVMChainWalletBase
Future<MoralisDiscoveryResult> 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<void> _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<Erc20Token>().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<void> updateTransactionsHistory() async => await _updateTransactions();
- List<Erc20Token> get erc20Currencies {
- try {
- if (!evmChainErc20TokensBox.isOpen) return [];
-
- return evmChainErc20TokensBox.values.toList();
- } catch (_) {
- return [];
- }
- }
+ List<Erc20Token> get erc20Currencies => _erc20Tokens.toList();
Future<void> addErc20Token(Erc20Token token) async {
final isSuspicious = isTokenPropertiesSuspicious(token);
@@ -1561,33 +1406,27 @@ 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<void> 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 token.delete();
+ await Erc20Token.deleteForWallet(walletInfo.name, selectedChainId, token.contractAddress);
} catch (e) {
- // Token might be from a closed box, just remove from balance
- printV('Error deleting token from box: $e');
+ printV('Error deleting token: $e');
}
+ _erc20Tokens
+ .removeWhere((t) => t.contractAddress.toLowerCase() == token.contractAddress.toLowerCase());
+
balance.remove(token);
await removeTokenTransactionsInHistory(token);
if (shouldUpdateBalance) {
diff --git a/cw_evm/lib/evm_chain_wallet_service.dart b/cw_evm/lib/evm_chain_wallet_service.dart
index 5a293001..ec52c519 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<void> 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<void> 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<EVMChainWallet> 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<void> 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 60355cab..969ffc7b 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 31a1bc3d..b41ee421 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<void>? _currentRefresh;
- late final Box<SPLToken> splTokensBox;
+ List<SPLToken> _splTokens = [];
@override
WalletAddresses walletAddresses;
@@ -133,9 +127,7 @@ abstract class SolanaWalletBase
);
Future<void> init() async {
- final boxName = "${walletInfo.name.replaceAll(" ", "_")}_${SPLToken.boxName}";
-
- splTokensBox = await CakeHive.openBox<SPLToken>(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<void> _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<void> updateSPLTokenTransactions({List<String>? 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<String>? 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<SPLToken> get splTokenCurrencies => splTokensBox.values.toList();
+ List<SPLToken> 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<void> 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<SolanaMoralisDiscoveryResult> 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 1ed55c41..a557507f 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 6da85fe1..32ff8c75 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<TronToken> tronTokensBox;
+ List<TronToken> _tronTokens = [];
late final TronPrivateKey _tronPrivateKey;
@@ -107,7 +101,7 @@ abstract class TronWalletBase
late ObservableMap<CryptoCurrency, TronBalance> balance;
Future<void> init() async {
- await initTronTokensBox();
+ await initTronTokens();
await walletAddresses.init();
await transactionHistory.init();
@@ -179,25 +173,38 @@ abstract class TronWalletBase
);
}
- void addInitialTokens() {
+ Future<void> 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<void> initTronTokensBox() async {
- final boxName = "${walletInfo.name.replaceAll(" ", "_")}_${TronToken.boxName}";
+ Future<void> initTronTokens() async {
+ _tronTokens = await TronToken.getAllForWallet(walletInfo.name);
+ }
+
+ TronToken? _findCachedToken(String contractAddress) {
+ for (final token in _tronTokens) {
+ if (token.contractAddress == contractAddress) return token;
+ }
- tronTokensBox = await CakeHive.openBox<TronToken>(boxName);
+ 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<void> _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<TronToken> get tronTokenCurrencies => tronTokensBox.values.toList();
+ List<TronToken> get tronTokenCurrencies => _tronTokens.toList();
Future<void> 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<void> 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 9b81a9a5..5c2e30a8 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 f195f2ac..e85622af 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<void> 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 3934b633..ae94b47e 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<void> _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}';
-
- Box<Erc20Token> tokenBox;
- if (CakeHive.isBoxOpen(boxName)) {
- tokenBox = CakeHive.box<Erc20Token>(boxName);
- } else {
- tokenBox = await CakeHive.openBox<Erc20Token>(boxName);
- }
+ final existingToken = await Erc20Token.getByContract(
+ walletInfo.name,
+ ethereumChainId,
+ xautToken.contractAddress,
+ );
- final xautAddress = xautToken.contractAddress;
- if (!tokenBox.containsKey(xautAddress)) {
- await tokenBox.put(xautAddress, xautToken);
- }
+ if (existingToken != null) continue;
+
+ await Erc20Token.copyWith(
+ xautToken,
+ walletName: walletInfo.name,
+ chainId: ethereumChainId,
+ ).save();
}
} catch (e) {
printV('Error in XAUT migration: $e');
@@ -1309,20 +1308,11 @@ Future<void> _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}';
-
- Box<SPLToken> tokenBox;
- if (CakeHive.isBoxOpen(boxName)) {
- tokenBox = CakeHive.box<SPLToken>(boxName);
- } else {
- tokenBox = await CakeHive.openBox<SPLToken>(boxName);
- }
+ final existingToken = await SPLToken.getByMint(walletInfo.name, xaut0Token.mintAddress);
- final xaut0Address = xaut0Token.mintAddress;
- if (!tokenBox.containsKey(xaut0Address)) {
- await tokenBox.put(xaut0Address, xaut0Token);
- }
+ if (existingToken != null) continue;
+
+ await SPLToken.copyWith(xaut0Token, walletName: walletInfo.name).save();
}
} catch (e) {
printV('Error in XAUT0 migration: $e');
@@ -1346,20 +1336,13 @@ Future<void> _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}";
-
- Box<SPLToken> tokenBox;
- if (CakeHive.isBoxOpen(boxName)) {
- tokenBox = CakeHive.box<SPLToken>(boxName);
- } else {
- tokenBox = await CakeHive.openBox<SPLToken>(boxName);
- }
+ final existingToken = await SPLToken.getByMint(walletInfo.name, tbbToken.mintAddress);
- final tbbAddress = tbbToken.mintAddress;
- if (!tokenBox.containsKey(tbbAddress)) {
- await tokenBox.put(tbbAddress, tbbToken);
+ if (existingToken != null) {
+ continue;
}
+
+ 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 2f248858..02229c80 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<void> 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 b70a061a..efb7b684 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 = <SPLToken>[];
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 = <String>{};
final unique = <SPLToken>[];
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 = <String>{};
final unique = <TronToken>[];
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<Erc20Token> loadDefaultEvmTokensForSwap() {
- if (evm == null) return [];
+ if (evm == null) {
+ return [];
+ }
final tokens = <Erc20Token>[];
final seen = <String>{};
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<int?> findEvmChainIdForContract(
String contractAddress, {
int? excludingChainId,
@@ -195,45 +226,10 @@ class TokenUtilities {
return null;
}
- static Future<Box<Erc20Token>> _openEvmTokensBoxFor(WalletInfo walletInfo) async {
- final walletKey = walletInfo.name.replaceAll(' ', '_');
- final boxName = _getErc20TokensBoxName(walletKey, walletInfo.type);
-
- if (CakeHive.isBoxOpen(boxName)) {
- return CakeHive.box<Erc20Token>(boxName);
- }
- return CakeHive.openBox<Erc20Token>(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<Box<SPLToken>> _openSolTokensBoxFor(WalletInfo wallet) async {
- final boxName = '${wallet.name.replaceAll(' ', '_')}_${SPLToken.boxName}';
- if (CakeHive.isBoxOpen(boxName)) {
- return CakeHive.box<SPLToken>(boxName);
- }
- return CakeHive.openBox<SPLToken>(boxName);
- }
-
- static Future<Box<TronToken>> _openTronTokensBoxFor(WalletInfo walletInfo) async {
- final boxName = '${walletInfo.name.replaceAll(' ', '_')}_${TronToken.boxName}';
- if (CakeHive.isBoxOpen(boxName)) {
- return CakeHive.box<TronToken>(boxName);
- }
- return CakeHive.openBox<TronToken>(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<List<CryptoCurrency>> _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 [];
Why this scored 27/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.