Migrate EVM/Sol/TRX tokens to SQL (#3450)

* fix android CI * feat: Migrate EVM, Sol and Tron tokens to SQL * handle rename edgecase * refactor: enhance token migration logic and improve database handling

David Adegoke committed Aug 20, 2026 at 02:19 UTC 4cc1495686f662bf2172ef285e63f6d49c570e72
23 files changed +1711 -680
cw_core/lib/db/sqlite.dart
+74 -1
@@ -63,7 +63,7 @@ Future<void> _initDb({String? pathOverride}) async {
63 }
64 }
65 await db?.close();
66 - db = await openDatabase(dbFile.path, version: 9,
66 + db = await openDatabase(dbFile.path, version: 10,
67 onUpgrade: (Database db, int oldVersion, int newVersion) async {
68 printV("migrating: $oldVersion, $newVersion");
69 if (oldVersion <= 1) {
@@ -147,6 +147,11 @@ CREATE TABLE IF NOT EXISTS BalanceCardStyleSettings (
147 definition: 'BOOLEAN DEFAULT FALSE',
148 );
149 }
150 + if (oldVersion <= 9) {
151 + await _createErc20TokenTable(db);
152 + await _createSplTokenTable(db);
153 + await _createTronTokenTable(db);
154 + }
155 }, onCreate: (Database db, int version) async {
156 await db.execute('''
157 CREATE TABLE WalletInfo (
@@ -242,6 +247,9 @@ CREATE TABLE BalanceCardStyleSettings (
247 await _createBridgeTransferTable(db);
248 await _createNodeTable(db);
249 await _createTradeTable(db);
250 + await _createErc20TokenTable(db);
251 + await _createSplTokenTable(db);
252 + await _createTronTokenTable(db);
253 });
254 }
255
@@ -370,6 +378,71 @@ ON BridgeTransfer(wallet_id);
378 ''');
379 }
380
381 +Future<void> _createErc20TokenTable(Database db) async {
382 + await db.execute("""
383 +CREATE TABLE IF NOT EXISTS Erc20Token (
384 + Erc20TokenId INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
385 + walletName TEXT NOT NULL,
386 + chainId INTEGER NOT NULL,
387 + name TEXT NOT NULL DEFAULT '',
388 + symbol TEXT NOT NULL DEFAULT '',
389 + contractAddress TEXT NOT NULL,
390 + decimal INTEGER NOT NULL DEFAULT 0,
391 + enabled INTEGER NOT NULL DEFAULT 1,
392 + iconPath TEXT,
393 + tag TEXT,
394 + isPotentialScam INTEGER NOT NULL DEFAULT 0
395 +);
396 +""");
397 + await db.execute("""
398 +CREATE UNIQUE INDEX IF NOT EXISTS idx_erc20token_wallet_chain_contract
399 +ON Erc20Token (walletName, chainId, contractAddress);
400 +""");
401 +}
402 +
403 +Future<void> _createSplTokenTable(Database db) async {
404 + await db.execute("""
405 +CREATE TABLE IF NOT EXISTS SPLToken (
406 + SPLTokenId INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
407 + walletName TEXT NOT NULL,
408 + name TEXT NOT NULL DEFAULT '',
409 + symbol TEXT NOT NULL DEFAULT '',
410 + mintAddress TEXT NOT NULL,
411 + decimal INTEGER NOT NULL DEFAULT 0,
412 + mint TEXT NOT NULL DEFAULT '',
413 + enabled INTEGER NOT NULL DEFAULT 1,
414 + iconPath TEXT,
415 + tag TEXT,
416 + isPotentialScam INTEGER NOT NULL DEFAULT 0
417 +);
418 +""");
419 + await db.execute("""
420 +CREATE UNIQUE INDEX IF NOT EXISTS idx_spltoken_wallet_mint
421 +ON SPLToken (walletName, mintAddress);
422 +""");
423 +}
424 +
425 +Future<void> _createTronTokenTable(Database db) async {
426 + await db.execute("""
427 +CREATE TABLE IF NOT EXISTS TronToken (
428 + TronTokenId INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
429 + walletName TEXT NOT NULL,
430 + name TEXT NOT NULL DEFAULT '',
431 + symbol TEXT NOT NULL DEFAULT '',
432 + contractAddress TEXT NOT NULL,
433 + decimal INTEGER NOT NULL DEFAULT 0,
434 + enabled INTEGER NOT NULL DEFAULT 1,
435 + iconPath TEXT,
436 + tag TEXT,
437 + isPotentialScam INTEGER NOT NULL DEFAULT 0
438 +);
439 +""");
440 + await db.execute("""
441 +CREATE UNIQUE INDEX IF NOT EXISTS idx_trontoken_wallet_contract
442 +ON TronToken (walletName, contractAddress);
443 +""");
444 +}
445 +
446 Future<void> _createNodeTable(Database db) async {
447 db.execute("""
448 CREATE TABLE Node (
cw_core/lib/erc20_token.dart
+153 -45
@@ -1,32 +1,8 @@
1 -import 'package:cw_core/crypto_currency.dart';
2 -import 'package:cw_core/hive_type_ids.dart';
3 -import 'package:hive/hive.dart';
4 -
5 -part 'erc20_token.part.dart';
6 -
7 -// @HiveType(typeId: Erc20Token.typeId)
8 -class Erc20Token extends CryptoCurrency with HiveObjectMixin {
9 - // @HiveField(0)
10 - final String name;
11 - // @HiveField(1)
12 - final String symbol;
13 - // @HiveField(2)
14 - final String contractAddress;
15 - // @HiveField(3)
16 - final int decimal;
17 - // @HiveField(4, defaultValue: true)
18 - bool _enabled;
19 - // @HiveField(5)
20 - String? iconPath;
21 - // @HiveField(6)
22 - final String? tag;
23 - // @HiveField(7, defaultValue: false)
24 - bool isPotentialScam;
25 -
26 - bool get enabled => _enabled;
27 -
28 - set enabled(bool value) => _enabled = value;
1 +import "package:cw_core/crypto_currency.dart";
2 +import "package:cw_core/db/sqlite.dart";
3 +import "package:sqflite/sqflite.dart";
4
5 +class Erc20Token extends CryptoCurrency {
6 Erc20Token({
7 required this.name,
8 required this.symbol,
@@ -36,6 +12,9 @@ class Erc20Token extends CryptoCurrency with HiveObjectMixin {
12 this.iconPath,
13 this.tag,
14 this.isPotentialScam = false,
15 + this.id = 0,
16 + this.walletName,
17 + this.chainId,
18 }) : _enabled = enabled,
19 super(
20 name: symbol.toLowerCase(),
@@ -47,32 +26,161 @@ class Erc20Token extends CryptoCurrency with HiveObjectMixin {
26 isPotentialScam: isPotentialScam,
27 );
28
50 - Erc20Token.copyWith(Erc20Token other, {String? icon, String? tag, bool? enabled})
51 - : this.name = other.name,
52 - this.symbol = other.symbol,
53 - this.contractAddress = other.contractAddress,
54 - this.decimal = other.decimal,
55 - this._enabled = enabled ?? other.enabled,
56 - this.tag = tag ?? other.tag,
57 - this.iconPath = icon ?? other.iconPath,
58 - this.isPotentialScam = other.isPotentialScam,
29 + Erc20Token.copyWith(
30 + Erc20Token other, {
31 + String? icon,
32 + super.tag,
33 + bool? enabled,
34 + String? walletName,
35 + int? chainId,
36 + }) : name = other.name,
37 + symbol = other.symbol,
38 + contractAddress = other.contractAddress,
39 + decimal = other.decimal,
40 + _enabled = enabled ?? other.enabled,
41 + tag = tag ?? other.tag,
42 + iconPath = icon ?? other.iconPath,
43 + isPotentialScam = other.isPotentialScam,
44 + id = 0,
45 + walletName = walletName ?? other.walletName,
46 + chainId = chainId ?? other.chainId,
47 super(
48 name: other.name,
49 title: other.symbol.toUpperCase(),
50 fullName: other.name,
63 - tag: tag,
51 iconPath: icon,
52 decimals: other.decimal,
53 isPotentialScam: other.isPotentialScam,
54 );
55
69 - static const typeId = ERC20_TOKEN_TYPE_ID;
70 - static const boxName = 'Erc20Tokens';
71 - static const ethereumBoxName = 'EthereumErc20Tokens';
72 - static const polygonBoxName = 'PolygonErc20Tokens';
73 - static const baseBoxName = 'BaseErc20Tokens';
74 - static const arbitrumBoxName = 'ArbitrumErc20Tokens';
75 - static const bscBoxName = 'BscErc20Tokens';
56 + Erc20Token.fromMap(Map<String, Object?> map)
57 + : this(
58 + name: map["name"] as String? ?? "",
59 + symbol: map["symbol"] as String? ?? "",
60 + contractAddress: map["contractAddress"] as String? ?? "",
61 + decimal: (map["decimal"] ?? 0) as int,
62 + enabled: _getBoolFromDB(map["enabled"], defaultValue: true),
63 + iconPath: map["iconPath"] as String?,
64 + tag: map["tag"] as String?,
65 + isPotentialScam: _getBoolFromDB(map["isPotentialScam"]),
66 + id: (map[selfIdColumn] ?? 0) as int,
67 + walletName: map["walletName"] as String?,
68 + chainId: map["chainId"] as int?,
69 + );
70 +
71 + @override
72 + final String name;
73 +
74 + @override
75 + final String symbol;
76 +
77 + @override
78 + String? iconPath;
79 +
80 + @override
81 + final String? tag;
82 +
83 + @override
84 + bool isPotentialScam;
85 +
86 + @override
87 + bool get enabled => _enabled;
88 +
89 + @override
90 + set enabled(bool value) => _enabled = value;
91 +
92 + int id;
93 + int? chainId;
94 + bool _enabled;
95 + final int decimal;
96 + String? walletName;
97 + final String contractAddress;
98 +
99 + static bool _getBoolFromDB(value, {bool? defaultValue}) {
100 + if (value is bool) {
101 + return value;
102 + } else if (value is int) {
103 + return value == 1;
104 + } else {
105 + return defaultValue ?? false;
106 + }
107 + }
108 +
109 + Map<String, dynamic> toMap() => {
110 + selfIdColumn: id,
111 + "walletName": walletName,
112 + "chainId": chainId,
113 + "name": name,
114 + "symbol": symbol,
115 + "contractAddress": contractAddress.toLowerCase(),
116 + "decimal": decimal,
117 + "enabled": _enabled ? 1 : 0,
118 + "iconPath": iconPath,
119 + "tag": tag,
120 + "isPotentialScam": isPotentialScam ? 1 : 0,
121 + };
122 +
123 + static String get tableName => "Erc20Token";
124 + static String get selfIdColumn => "${tableName}Id";
125 +
126 + Future<int> save() async {
127 + if (walletName == null || chainId == null) {
128 + throw StateError("Erc20Token.save() requires walletName and chainId to be set");
129 + }
130 +
131 + final json = toMap();
132 + if (json[selfIdColumn] == 0) {
133 + json[selfIdColumn] = null;
134 + }
135 + id = await db!.insert(tableName, json, conflictAlgorithm: ConflictAlgorithm.replace);
136 + return id;
137 + }
138 +
139 + static Future<List<Erc20Token>> selectList(
140 + String where,
141 + List<dynamic> whereArgs, {
142 + String? orderBy,
143 + }) async {
144 + orderBy ??= selfIdColumn;
145 + final list = await db!.query(
146 + tableName,
147 + where: where.isNotEmpty ? where : "1 = 1",
148 + whereArgs: whereArgs.isNotEmpty ? whereArgs : null,
149 + orderBy: orderBy,
150 + );
151 + return List.generate(list.length, (index) => Erc20Token.fromMap(list[index]));
152 + }
153 +
154 + static Future<List<Erc20Token>> getAllForWallet(String walletName, int chainId) =>
155 + selectList("walletName = ? AND chainId = ?", [walletName, chainId]);
156 +
157 + static Future<Erc20Token?> getByContract(
158 + String walletName,
159 + int chainId,
160 + String contractAddress,
161 + ) async {
162 + final list = await selectList(
163 + "walletName = ? AND chainId = ? AND contractAddress = ?",
164 + [walletName, chainId, contractAddress.toLowerCase()],
165 + );
166 + return list.isEmpty ? null : list.first;
167 + }
168 +
169 + static Future<int> deleteForWallet(String walletName, int chainId, String contractAddress) =>
170 + db!.delete(
171 + tableName,
172 + where: "walletName = ? AND chainId = ? AND contractAddress = ?",
173 + whereArgs: [walletName, chainId, contractAddress.toLowerCase()],
174 + );
175 +
176 + static Future<int> deleteAllForWallet(String walletName) =>
177 + db!.delete(tableName, where: "walletName = ?", whereArgs: [walletName]);
178 +
179 + static Future<void> renameWallet(String oldName, String newName) async {
180 + await db!.delete(tableName, where: "walletName = ?", whereArgs: [newName]);
181 + await db!
182 + .update(tableName, {"walletName": newName}, where: "walletName = ?", whereArgs: [oldName]);
183 + }
184
185 @override
186 bool operator ==(Object other) => other is Erc20Token && other.contractAddress == contractAddress;
cw_core/lib/erc20_token_legacy.dart new
+213
@@ -0,0 +1,213 @@
1 +import "package:cw_core/cake_hive.dart";
2 +import "package:cw_core/db/sqlite.dart";
3 +import "package:cw_core/erc20_token.dart" as erc20_new;
4 +import "package:cw_core/hive_type_ids.dart";
5 +import "package:cw_core/utils/print_verbose.dart";
6 +import "package:cw_core/wallet_info.dart";
7 +import "package:cw_core/wallet_type.dart";
8 +import "package:hive/hive.dart";
9 +import "package:sqflite/sqflite.dart";
10 +
11 +part "erc20_token_legacy.part.dart";
12 +
13 +Future<void> performErc20TokenHiveMigration() async {
14 + try {
15 + if (!CakeHive.isAdapterRegistered(Erc20Token.typeId)) {
16 + CakeHive.registerAdapter(Erc20TokenAdapter());
17 + }
18 +
19 + final wallets = await WalletInfo.getAll();
20 + await Erc20Token.migrateAllToSqlite(wallets);
21 + } catch (e) {
22 + printV("Error performing Erc20Token Hive migration: $e, continuing anyway");
23 + }
24 +}
25 +
26 +// @HiveType(typeId: Erc20Token.typeId)
27 +class Erc20Token extends HiveObject {
28 + Erc20Token({
29 + required this.name,
30 + required this.symbol,
31 + required this.contractAddress,
32 + required this.decimal,
33 + bool enabled = true,
34 + this.iconPath,
35 + this.tag,
36 + this.isPotentialScam = false,
37 + }) : _enabled = enabled;
38 + // @HiveField(0)
39 + final String name;
40 + // @HiveField(1)
41 + final String symbol;
42 + // @HiveField(2)
43 + final String contractAddress;
44 + // @HiveField(3)
45 + final int decimal;
46 + // @HiveField(4, defaultValue: true)
47 + bool _enabled;
48 + // @HiveField(5)
49 + String? iconPath;
50 + // @HiveField(6)
51 + final String? tag;
52 + // @HiveField(7, defaultValue: false)
53 + bool isPotentialScam;
54 +
55 + bool get enabled => _enabled;
56 +
57 + set enabled(bool value) => _enabled = value;
58 +
59 + static const typeId = ERC20_TOKEN_TYPE_ID;
60 + static const boxName = "Erc20Tokens";
61 + static const ethereumBoxName = "EthereumErc20Tokens";
62 + static const polygonBoxName = "PolygonErc20Tokens";
63 + static const baseBoxName = "BaseErc20Tokens";
64 + static const arbitrumBoxName = "ArbitrumErc20Tokens";
65 + static const bscBoxName = "BscErc20Tokens";
66 +
67 + static const chainIdToBoxSuffix = {
68 + 1: ethereumBoxName,
69 + 137: polygonBoxName,
70 + 8453: baseBoxName,
71 + 42161: arbitrumBoxName,
72 + 56: bscBoxName,
73 + };
74 +
75 + static const evmWalletTypes = [
76 + WalletType.ethereum,
77 + WalletType.polygon,
78 + WalletType.base,
79 + WalletType.arbitrum,
80 + WalletType.bsc,
81 + ];
82 +
83 + static Future<void> migrateAllToSqlite(List<WalletInfo> wallets) async {
84 + await _migrateLegacyGlobalBox(wallets);
85 +
86 + final sanitizedToRawNames = <String, Set<String>>{};
87 + for (final wallet in wallets) {
88 + if (!evmWalletTypes.contains(wallet.type)) {
89 + continue;
90 + }
91 +
92 + sanitizedToRawNames
93 + .putIfAbsent(wallet.name.replaceAll(" ", "_"), () => <String>{})
94 + .add(wallet.name);
95 + }
96 +
97 + for (final entry in sanitizedToRawNames.entries) {
98 + for (final chainEntry in chainIdToBoxSuffix.entries) {
99 + final tokenBoxName = "${entry.key}_${chainEntry.value}";
100 + try {
101 + if (!await CakeHive.boxExists(tokenBoxName)) {
102 + continue;
103 + }
104 +
105 + final box = await CakeHive.openBox<Erc20Token>(tokenBoxName);
106 +
107 + for (final group in _mergeByLowercaseContract(box)) {
108 + for (final rawName in entry.value) {
109 + await group.token.migrateToSqlite(walletName: rawName, chainId: chainEntry.key);
110 + }
111 + await box.deleteAll(group.sourceKeys);
112 + }
113 +
114 + await box.deleteFromDisk();
115 + } catch (e) {
116 + printV("Error migrating erc20 token box $tokenBoxName: $e, continuing anyway");
117 + }
118 + }
119 + }
120 + }
121 +
122 + static Future<void> _migrateLegacyGlobalBox(List<WalletInfo> wallets) async {
123 + try {
124 + if (!await CakeHive.boxExists(boxName)) {
125 + return;
126 + }
127 +
128 + if (!wallets.any((wallet) => wallet.type == WalletType.ethereum)) {
129 + return;
130 + }
131 +
132 + final box = await CakeHive.openBox<Erc20Token>(boxName);
133 + final ethereumWallets =
134 + wallets.where((wallet) => wallet.type == WalletType.ethereum).toList();
135 +
136 + for (final group in _mergeByLowercaseContract(box)) {
137 + for (final wallet in ethereumWallets) {
138 + await group.token.migrateToSqlite(walletName: wallet.name, chainId: 1);
139 + }
140 + await box.deleteAll(group.sourceKeys);
141 + }
142 +
143 + await box.deleteFromDisk();
144 + } catch (e) {
145 + printV("Error migrating legacy global erc20 token box: $e, continuing anyway");
146 + }
147 + }
148 +
149 + static List<_MergedTokenGroup> _mergeByLowercaseContract(Box<Erc20Token> box) {
150 + final groups = <String, _MergedTokenGroup>{};
151 +
152 + for (final key in box.keys) {
153 + final token = box.get(key);
154 + if (token == null) {
155 + continue;
156 + }
157 +
158 + final lowerKey = token.contractAddress.toLowerCase();
159 + final existing = groups[lowerKey];
160 +
161 + if (existing == null) {
162 + groups[lowerKey] = _MergedTokenGroup(token: token, sourceKeys: [key]);
163 + continue;
164 + }
165 +
166 + groups[lowerKey] = _MergedTokenGroup(
167 + token: Erc20Token(
168 + name: token.name,
169 + symbol: token.symbol,
170 + contractAddress: lowerKey,
171 + decimal: token.decimal,
172 + enabled: token.enabled || existing.token.enabled,
173 + iconPath:
174 + (token.iconPath?.isNotEmpty ?? false) ? token.iconPath : existing.token.iconPath,
175 + tag: token.tag ?? existing.token.tag,
176 + isPotentialScam: token.isPotentialScam || existing.token.isPotentialScam,
177 + ),
178 + sourceKeys: [...existing.sourceKeys, key],
179 + );
180 + }
181 +
182 + return groups.values.toList();
183 + }
184 +
185 + Future<void> migrateToSqlite({required String walletName, required int chainId}) async {
186 + final row = erc20_new.Erc20Token(
187 + name: name,
188 + symbol: symbol,
189 + contractAddress: contractAddress,
190 + decimal: decimal,
191 + enabled: _enabled,
192 + iconPath: iconPath,
193 + tag: tag,
194 + isPotentialScam: isPotentialScam,
195 + walletName: walletName,
196 + chainId: chainId,
197 + ).toMap();
198 + row[erc20_new.Erc20Token.selfIdColumn] = null;
199 +
200 + await db!.insert(
201 + erc20_new.Erc20Token.tableName,
202 + row,
203 + conflictAlgorithm: ConflictAlgorithm.replace,
204 + );
205 + }
206 +}
207 +
208 +class _MergedTokenGroup {
209 + _MergedTokenGroup({required this.token, required this.sourceKeys});
210 +
211 + final Erc20Token token;
212 + final List<dynamic> sourceKeys;
213 +}
cw_core/lib/erc20_token_legacy.part.dart renamed
+1 -1
@@ -1,6 +1,6 @@
1 // GENERATED CODE - DO NOT MODIFY BY HAND
2
3 -part of 'erc20_token.dart';
3 +part of "erc20_token_legacy.dart";
4
5 // **************************************************************************
6 // TypeAdapterGenerator
cw_core/lib/spl_token.dart
+130 -52
@@ -1,41 +1,8 @@
1 -import 'package:cw_core/crypto_currency.dart';
2 -import 'package:cw_core/hive_type_ids.dart';
3 -import 'package:hive/hive.dart';
1 +import "package:cw_core/crypto_currency.dart";
2 +import "package:cw_core/db/sqlite.dart";
3 +import "package:sqflite/sqflite.dart";
4
5 -part 'spl_token.part.dart';
6 -
7 -// @HiveType(typeId: SPLToken.typeId)
8 -class SPLToken extends CryptoCurrency with HiveObjectMixin {
9 - @override
10 - // @HiveField(0)
11 - final String name;
12 -
13 - // @HiveField(1)
14 - final String symbol;
15 -
16 - // @HiveField(2)
17 - final String mintAddress;
18 -
19 - // @HiveField(3)
20 - final int decimal;
21 -
22 - // @HiveField(4, defaultValue: true)
23 - bool _enabled;
24 -
25 - // @HiveField(5)
26 - final String mint;
27 -
28 - @override
29 - // @HiveField(6)
30 - final String? iconPath;
31 -
32 - @override
33 - // @HiveField(7)
34 - final String? tag;
35 -
36 - @override
37 - // @HiveField(8, defaultValue: false)
38 - bool isPotentialScam;
5 +class SPLToken extends CryptoCurrency {
6
7 SPLToken({
8 required this.name,
@@ -44,10 +11,12 @@ class SPLToken extends CryptoCurrency with HiveObjectMixin {
11 required this.decimal,
12 required this.mint,
13 this.iconPath,
47 - this.tag = 'SOL',
14 + this.tag = "SOL",
15 bool enabled = true,
16 this.isPotentialScam = false,
50 - Set<String> groups = const {},
17 + super.groups,
18 + this.id = 0,
19 + this.walletName,
20 }) : _enabled = enabled,
21 super(
22 name: mint.toLowerCase(),
@@ -57,7 +26,6 @@ class SPLToken extends CryptoCurrency with HiveObjectMixin {
26 iconPath: iconPath,
27 decimals: decimal,
28 isPotentialScam: isPotentialScam,
60 - groups: groups,
29 );
30
31 factory SPLToken.fromMetadata({
@@ -67,8 +35,7 @@ class SPLToken extends CryptoCurrency with HiveObjectMixin {
35 required String mintAddress,
36 String? iconPath,
37 bool isPotentialScam = false,
70 - }) {
71 - return SPLToken(
38 + }) => SPLToken(
39 name: name,
40 symbol: symbol,
41 mintAddress: mintAddress,
@@ -77,15 +44,8 @@ class SPLToken extends CryptoCurrency with HiveObjectMixin {
44 iconPath: iconPath,
45 isPotentialScam: isPotentialScam,
46 );
80 - }
81 -
82 - @override
83 - bool get enabled => _enabled;
84 -
85 - @override
86 - set enabled(bool value) => _enabled = value;
47
88 - SPLToken.copyWith(SPLToken other, {String? icon, String? tag, bool? enabled})
48 + SPLToken.copyWith(SPLToken other, {String? icon, String? tag, bool? enabled, String? walletName})
49 : name = other.name,
50 symbol = other.symbol,
51 mintAddress = other.mintAddress,
@@ -95,6 +55,8 @@ class SPLToken extends CryptoCurrency with HiveObjectMixin {
55 tag = tag ?? other.tag,
56 iconPath = icon ?? other.iconPath,
57 isPotentialScam = other.isPotentialScam,
58 + id = 0,
59 + walletName = walletName ?? other.walletName,
60 super(
61 title: other.symbol.toUpperCase(),
62 name: other.symbol.toLowerCase(),
@@ -106,8 +68,124 @@ class SPLToken extends CryptoCurrency with HiveObjectMixin {
68 groups: other.groups,
69 );
70
109 - static const typeId = SPL_TOKEN_TYPE_ID;
110 - static const boxName = 'SPLTokens';
71 + SPLToken.fromMap(Map<String, Object?> map)
72 + : this(
73 + name: map["name"] as String? ?? "",
74 + symbol: map["symbol"] as String? ?? "",
75 + mintAddress: map["mintAddress"] as String? ?? "",
76 + decimal: (map["decimal"] ?? 0) as int,
77 + mint: map["mint"] as String? ?? "",
78 + enabled: _getBoolFromDB(map["enabled"], defaultValue: true),
79 + iconPath: map["iconPath"] as String?,
80 + tag: map["tag"] as String?,
81 + isPotentialScam: _getBoolFromDB(map["isPotentialScam"]),
82 + id: (map[selfIdColumn] ?? 0) as int,
83 + walletName: map["walletName"] as String?,
84 + );
85 + @override
86 + final String name;
87 +
88 + @override
89 + final String symbol;
90 +
91 + final String mintAddress;
92 +
93 + final int decimal;
94 +
95 + bool _enabled;
96 +
97 + final String mint;
98 +
99 + @override
100 + final String? iconPath;
101 +
102 + @override
103 + final String? tag;
104 +
105 + @override
106 + bool isPotentialScam;
107 +
108 + int id;
109 + String? walletName;
110 +
111 + @override
112 + bool get enabled => _enabled;
113 +
114 + @override
115 + set enabled(bool value) => _enabled = value;
116 +
117 + static bool _getBoolFromDB(value, {bool? defaultValue}) {
118 + if (value is bool) {
119 + return value;
120 + } else if (value is int) {
121 + return value == 1;
122 + } else {
123 + return defaultValue ?? false;
124 + }
125 + }
126 +
127 + Map<String, dynamic> toMap() => {
128 + selfIdColumn: id,
129 + "walletName": walletName,
130 + "name": name,
131 + "symbol": symbol,
132 + "mintAddress": mintAddress,
133 + "decimal": decimal,
134 + "mint": mint,
135 + "enabled": _enabled ? 1 : 0,
136 + "iconPath": iconPath,
137 + "tag": tag,
138 + "isPotentialScam": isPotentialScam ? 1 : 0,
139 + };
140 +
141 + static String get tableName => "SPLToken";
142 + static String get selfIdColumn => "${tableName}Id";
143 +
144 + Future<int> save() async {
145 + if (walletName == null) {
146 + throw StateError("SPLToken.save() requires walletName to be set");
147 + }
148 +
149 + final json = toMap();
150 + if (json[selfIdColumn] == 0) {
151 + json[selfIdColumn] = null;
152 + }
153 + id = await db!.insert(tableName, json, conflictAlgorithm: ConflictAlgorithm.replace);
154 + return id;
155 + }
156 +
157 + static Future<List<SPLToken>> selectList(String where, List<dynamic> whereArgs,
158 + {String? orderBy}) async {
159 + orderBy ??= selfIdColumn;
160 + final list = await db!.query(
161 + tableName,
162 + where: where.isNotEmpty ? where : "1 = 1",
163 + whereArgs: whereArgs.isNotEmpty ? whereArgs : null,
164 + orderBy: orderBy,
165 + );
166 + return List.generate(list.length, (index) => SPLToken.fromMap(list[index]));
167 + }
168 +
169 + static Future<List<SPLToken>> getAllForWallet(String walletName) async => selectList("walletName = ?", [walletName]);
170 +
171 + static Future<SPLToken?> getByMint(String walletName, String mintAddress) async {
172 + final list = await selectList("walletName = ? AND mintAddress = ?", [walletName, mintAddress]);
173 + return list.isEmpty ? null : list.first;
174 + }
175 +
176 + static Future<int> deleteForWallet(String walletName, String mintAddress) => db!.delete(
177 + tableName,
178 + where: "walletName = ? AND mintAddress = ?",
179 + whereArgs: [walletName, mintAddress],
180 + );
181 +
182 + static Future<int> deleteAllForWallet(String walletName) => db!.delete(tableName, where: "walletName = ?", whereArgs: [walletName]);
183 +
184 + static Future<void> renameWallet(String oldName, String newName) async {
185 + await db!.delete(tableName, where: "walletName = ?", whereArgs: [newName]);
186 + await db!
187 + .update(tableName, {"walletName": newName}, where: "walletName = ?", whereArgs: [oldName]);
188 + }
189
190 @override
191 bool operator ==(other) =>
cw_core/lib/spl_token_legacy.dart new
+134
@@ -0,0 +1,134 @@
1 +import "package:cw_core/cake_hive.dart";
2 +import "package:cw_core/db/sqlite.dart";
3 +import "package:cw_core/hive_type_ids.dart";
4 +import "package:cw_core/spl_token.dart" as spl_new;
5 +import "package:cw_core/utils/print_verbose.dart";
6 +import "package:cw_core/wallet_info.dart";
7 +import "package:cw_core/wallet_type.dart";
8 +import "package:hive/hive.dart";
9 +import "package:sqflite/sqflite.dart";
10 +
11 +part "spl_token_legacy.part.dart";
12 +
13 +Future<void> performSplTokenHiveMigration() async {
14 + try {
15 + if (!CakeHive.isAdapterRegistered(SPLToken.typeId)) {
16 + CakeHive.registerAdapter(SPLTokenAdapter());
17 + }
18 +
19 + final wallets = await WalletInfo.getAll();
20 + await SPLToken.migrateAllToSqlite(wallets);
21 + } catch (e) {
22 + printV("Error performing SPLToken Hive migration: $e, continuing anyway");
23 + }
24 +}
25 +
26 +// @HiveType(typeId: SPLToken.typeId)
27 +class SPLToken extends HiveObject {
28 + SPLToken({
29 + required this.name,
30 + required this.symbol,
31 + required this.mintAddress,
32 + required this.decimal,
33 + required this.mint,
34 + this.iconPath,
35 + this.tag = "SOL",
36 + bool enabled = true,
37 + this.isPotentialScam = false,
38 + }) : _enabled = enabled;
39 + // @HiveField(0)
40 + final String name;
41 +
42 + // @HiveField(1)
43 + final String symbol;
44 +
45 + // @HiveField(2)
46 + final String mintAddress;
47 +
48 + // @HiveField(3)
49 + final int decimal;
50 +
51 + // @HiveField(4, defaultValue: true)
52 + bool _enabled;
53 +
54 + // @HiveField(5)
55 + final String mint;
56 +
57 + // @HiveField(6)
58 + final String? iconPath;
59 +
60 + // @HiveField(7)
61 + final String? tag;
62 +
63 + // @HiveField(8, defaultValue: false)
64 + bool isPotentialScam;
65 +
66 + bool get enabled => _enabled;
67 +
68 + set enabled(bool value) => _enabled = value;
69 +
70 + static const typeId = SPL_TOKEN_TYPE_ID;
71 + static const boxName = "SPLTokens";
72 +
73 + static Future<void> migrateAllToSqlite(List<WalletInfo> wallets) async {
74 + final sanitizedToRawNames = <String, Set<String>>{};
75 + for (final wallet in wallets) {
76 + if (wallet.type != WalletType.solana) {
77 + continue;
78 + }
79 +
80 + sanitizedToRawNames
81 + .putIfAbsent(wallet.name.replaceAll(" ", "_"), () => <String>{})
82 + .add(wallet.name);
83 + }
84 +
85 + for (final entry in sanitizedToRawNames.entries) {
86 + final tokenBoxName = "${entry.key}_$boxName";
87 + try {
88 + if (!await CakeHive.boxExists(tokenBoxName)) {
89 + continue;
90 + }
91 +
92 + final box = await CakeHive.openBox<SPLToken>(tokenBoxName);
93 +
94 + for (final key in box.keys.toList()) {
95 + final token = box.get(key);
96 + if (token == null) {
97 + continue;
98 + }
99 +
100 + for (final rawName in entry.value) {
101 + await token.migrateToSqlite(walletName: rawName);
102 + }
103 + await box.delete(key);
104 + }
105 +
106 + await box.deleteFromDisk();
107 + } catch (e) {
108 + printV("Error migrating spl token box $tokenBoxName: $e, continuing anyway");
109 + }
110 + }
111 + }
112 +
113 + Future<void> migrateToSqlite({required String walletName}) async {
114 + final row = spl_new.SPLToken(
115 + name: name,
116 + symbol: symbol,
117 + mintAddress: mintAddress,
118 + decimal: decimal,
119 + mint: mint,
120 + enabled: _enabled,
121 + iconPath: iconPath,
122 + tag: tag,
123 + isPotentialScam: isPotentialScam,
124 + walletName: walletName,
125 + ).toMap();
126 + row[spl_new.SPLToken.selfIdColumn] = null;
127 +
128 + await db!.insert(
129 + spl_new.SPLToken.tableName,
130 + row,
131 + conflictAlgorithm: ConflictAlgorithm.replace,
132 + );
133 + }
134 +}
cw_core/lib/spl_token_legacy.part.dart renamed
+1 -1
@@ -1,6 +1,6 @@
1 // GENERATED CODE - DO NOT MODIFY BY HAND
2
3 -part of 'spl_token.dart';
3 +part of "spl_token_legacy.dart";
4
5 // **************************************************************************
6 // TypeAdapterGenerator
cw_core/lib/tron_token.dart
+132 -42
@@ -1,41 +1,8 @@
1 -// ignore_for_file: annotate_overrides, overridden_fields
2 -
3 -import 'package:cw_core/crypto_currency.dart';
4 -import 'package:cw_core/hive_type_ids.dart';
5 -import 'package:hive/hive.dart';
6 -
7 -part 'tron_token.part.dart';
8 -
9 -// @HiveType(typeId: TronToken.typeId)
10 -class TronToken extends CryptoCurrency with HiveObjectMixin {
11 - // @HiveField(0)
12 - final String name;
13 -
14 - // @HiveField(1)
15 - final String symbol;
16 -
17 - // @HiveField(2)
18 - final String contractAddress;
19 -
20 - // @HiveField(3)
21 - final int decimal;
22 -
23 - // @HiveField(4, defaultValue: true)
24 - bool _enabled;
25 -
26 - // @HiveField(5)
27 - final String? iconPath;
28 -
29 - // @HiveField(6)
30 - final String? tag;
31 -
32 - // @HiveField(7, defaultValue: false)
33 - final bool isPotentialScam;
34 -
35 - bool get enabled => _enabled;
36 -
37 - set enabled(bool value) => _enabled = value;
1 +import "package:cw_core/crypto_currency.dart";
2 +import "package:cw_core/db/sqlite.dart";
3 +import "package:sqflite/sqflite.dart";
4
5 +class TronToken extends CryptoCurrency {
6 TronToken({
7 required this.name,
8 required this.symbol,
@@ -43,8 +10,10 @@ class TronToken extends CryptoCurrency with HiveObjectMixin {
10 required this.decimal,
11 bool enabled = true,
12 this.iconPath,
46 - this.tag = 'TRX',
13 + this.tag = "TRX",
14 this.isPotentialScam = false,
15 + this.id = 0,
16 + this.walletName,
17 }) : _enabled = enabled,
18 super(
19 name: symbol.toLowerCase(),
@@ -55,9 +24,27 @@ class TronToken extends CryptoCurrency with HiveObjectMixin {
24 decimals: decimal,
25 isPotentialScam: isPotentialScam,
26 );
27 + TronToken.fromMap(Map<String, Object?> map)
28 + : this(
29 + name: map["name"] as String? ?? "",
30 + symbol: map["symbol"] as String? ?? "",
31 + contractAddress: map["contractAddress"] as String? ?? "",
32 + decimal: (map["decimal"] ?? 0) as int,
33 + enabled: _getBoolFromDB(map["enabled"], defaultValue: true),
34 + iconPath: map["iconPath"] as String?,
35 + tag: map["tag"] as String?,
36 + isPotentialScam: _getBoolFromDB(map["isPotentialScam"]),
37 + id: (map[selfIdColumn] ?? 0) as int,
38 + walletName: map["walletName"] as String?,
39 + );
40
59 - TronToken.copyWith(TronToken other, {String? icon, String? tag, bool? enabled})
60 - : name = other.name,
41 + TronToken.copyWith(
42 + TronToken other, {
43 + String? icon,
44 + String? tag,
45 + bool? enabled,
46 + String? walletName,
47 + }) : name = other.name,
48 symbol = other.symbol,
49 contractAddress = other.contractAddress,
50 decimal = other.decimal,
@@ -65,6 +52,8 @@ class TronToken extends CryptoCurrency with HiveObjectMixin {
52 tag = tag ?? other.tag,
53 iconPath = icon ?? other.iconPath,
54 isPotentialScam = other.isPotentialScam,
55 + id = 0,
56 + walletName = walletName ?? other.walletName,
57 super(
58 name: other.name,
59 title: other.symbol.toUpperCase(),
@@ -74,9 +63,110 @@ class TronToken extends CryptoCurrency with HiveObjectMixin {
63 decimals: other.decimal,
64 isPotentialScam: other.isPotentialScam,
65 );
66 + @override
67 + final String name;
68 +
69 + @override
70 + final String symbol;
71 +
72 + @override
73 + final String? iconPath;
74 +
75 + @override
76 + final String? tag;
77 +
78 + @override
79 + final bool isPotentialScam;
80 +
81 + @override
82 + bool get enabled => _enabled;
83 +
84 + @override
85 + set enabled(bool value) => _enabled = value;
86 +
87 + int id;
88 + bool _enabled;
89 + final int decimal;
90 + String? walletName;
91 + final String contractAddress;
92
78 - static const typeId = TRON_TOKEN_TYPE_ID;
79 - static const boxName = 'TronTokens';
93 + static bool _getBoolFromDB(value, {bool? defaultValue}) {
94 + if (value is bool) {
95 + return value;
96 + } else if (value is int) {
97 + return value == 1;
98 + } else {
99 + return defaultValue ?? false;
100 + }
101 + }
102 +
103 + Map<String, dynamic> toMap() => {
104 + selfIdColumn: id,
105 + "walletName": walletName,
106 + "name": name,
107 + "symbol": symbol,
108 + "contractAddress": contractAddress,
109 + "decimal": decimal,
110 + "enabled": _enabled ? 1 : 0,
111 + "iconPath": iconPath,
112 + "tag": tag,
113 + "isPotentialScam": isPotentialScam ? 1 : 0,
114 + };
115 +
116 + static String get tableName => "TronToken";
117 + static String get selfIdColumn => "${tableName}Id";
118 +
119 + Future<int> save() async {
120 + if (walletName == null) {
121 + throw StateError("TronToken.save() requires walletName to be set");
122 + }
123 +
124 + final json = toMap();
125 + if (json[selfIdColumn] == 0) {
126 + json[selfIdColumn] = null;
127 + }
128 + id = await db!.insert(tableName, json, conflictAlgorithm: ConflictAlgorithm.replace);
129 + return id;
130 + }
131 +
132 + static Future<List<TronToken>> selectList(
133 + String where,
134 + List<dynamic> whereArgs, {
135 + String? orderBy,
136 + }) async {
137 + orderBy ??= selfIdColumn;
138 + final list = await db!.query(
139 + tableName,
140 + where: where.isNotEmpty ? where : "1 = 1",
141 + whereArgs: whereArgs.isNotEmpty ? whereArgs : null,
142 + orderBy: orderBy,
143 + );
144 + return List.generate(list.length, (index) => TronToken.fromMap(list[index]));
145 + }
146 +
147 + static Future<List<TronToken>> getAllForWallet(String walletName) =>
148 + selectList("walletName = ?", [walletName]);
149 +
150 + static Future<TronToken?> getByContract(String walletName, String contractAddress) async {
151 + final list =
152 + await selectList("walletName = ? AND contractAddress = ?", [walletName, contractAddress]);
153 + return list.isEmpty ? null : list.first;
154 + }
155 +
156 + static Future<int> deleteForWallet(String walletName, String contractAddress) => db!.delete(
157 + tableName,
158 + where: "walletName = ? AND contractAddress = ?",
159 + whereArgs: [walletName, contractAddress],
160 + );
161 +
162 + static Future<int> deleteAllForWallet(String walletName) =>
163 + db!.delete(tableName, where: "walletName = ?", whereArgs: [walletName]);
164 +
165 + static Future<void> renameWallet(String oldName, String newName) async {
166 + await db!.delete(tableName, where: "walletName = ?", whereArgs: [newName]);
167 + await db!
168 + .update(tableName, {"walletName": newName}, where: "walletName = ?", whereArgs: [oldName]);
169 + }
170
171 @override
172 bool operator ==(other) =>
cw_core/lib/tron_token_legacy.dart new
+129
@@ -0,0 +1,129 @@
1 +import "package:cw_core/cake_hive.dart";
2 +import "package:cw_core/db/sqlite.dart";
3 +import "package:cw_core/hive_type_ids.dart";
4 +import "package:cw_core/tron_token.dart" as tron_new;
5 +import "package:cw_core/utils/print_verbose.dart";
6 +import "package:cw_core/wallet_info.dart";
7 +import "package:cw_core/wallet_type.dart";
8 +import "package:hive/hive.dart";
9 +import "package:sqflite/sqflite.dart";
10 +
11 +part "tron_token_legacy.part.dart";
12 +
13 +Future<void> performTronTokenHiveMigration() async {
14 + try {
15 + if (!CakeHive.isAdapterRegistered(TronToken.typeId)) {
16 + CakeHive.registerAdapter(TronTokenAdapter());
17 + }
18 +
19 + final wallets = await WalletInfo.getAll();
20 + await TronToken.migrateAllToSqlite(wallets);
21 + } catch (e) {
22 + printV("Error performing TronToken Hive migration: $e, continuing anyway");
23 + }
24 +}
25 +
26 +// @HiveType(typeId: TronToken.typeId)
27 +class TronToken extends HiveObject {
28 + TronToken({
29 + required this.name,
30 + required this.symbol,
31 + required this.contractAddress,
32 + required this.decimal,
33 + bool enabled = true,
34 + this.iconPath,
35 + this.tag = "TRX",
36 + this.isPotentialScam = false,
37 + }) : _enabled = enabled;
38 + // @HiveField(0)
39 + final String name;
40 +
41 + // @HiveField(1)
42 + final String symbol;
43 +
44 + // @HiveField(2)
45 + final String contractAddress;
46 +
47 + // @HiveField(3)
48 + final int decimal;
49 +
50 + // @HiveField(4, defaultValue: true)
51 + bool _enabled;
52 +
53 + // @HiveField(5)
54 + final String? iconPath;
55 +
56 + // @HiveField(6)
57 + final String? tag;
58 +
59 + // @HiveField(7, defaultValue: false)
60 + final bool isPotentialScam;
61 +
62 + bool get enabled => _enabled;
63 +
64 + set enabled(bool value) => _enabled = value;
65 +
66 + static const typeId = TRON_TOKEN_TYPE_ID;
67 + static const boxName = "TronTokens";
68 +
69 + static Future<void> migrateAllToSqlite(List<WalletInfo> wallets) async {
70 + final sanitizedToRawNames = <String, Set<String>>{};
71 + for (final wallet in wallets) {
72 + if (wallet.type != WalletType.tron) {
73 + continue;
74 + }
75 +
76 + sanitizedToRawNames
77 + .putIfAbsent(wallet.name.replaceAll(" ", "_"), () => <String>{})
78 + .add(wallet.name);
79 + }
80 +
81 + for (final entry in sanitizedToRawNames.entries) {
82 + final tokenBoxName = "${entry.key}_$boxName";
83 + try {
84 + if (!await CakeHive.boxExists(tokenBoxName)) {
85 + continue;
86 + }
87 +
88 + final box = await CakeHive.openBox<TronToken>(tokenBoxName);
89 +
90 + for (final key in box.keys.toList()) {
91 + final token = box.get(key);
92 + if (token == null) {
93 + continue;
94 + }
95 +
96 + for (final rawName in entry.value) {
97 + await token.migrateToSqlite(walletName: rawName);
98 + }
99 + await box.delete(key);
100 + }
101 +
102 + await box.deleteFromDisk();
103 + } catch (e) {
104 + printV("Error migrating tron token box $tokenBoxName: $e, continuing anyway");
105 + }
106 + }
107 + }
108 +
109 + Future<void> migrateToSqlite({required String walletName}) async {
110 + final row = tron_new.TronToken(
111 + name: name,
112 + symbol: symbol,
113 + contractAddress: contractAddress,
114 + decimal: decimal,
115 + enabled: _enabled,
116 + iconPath: iconPath,
117 + tag: tag,
118 + isPotentialScam: isPotentialScam,
119 + walletName: walletName,
120 + ).toMap();
121 + row[tron_new.TronToken.selfIdColumn] = null;
122 +
123 + await db!.insert(
124 + tron_new.TronToken.tableName,
125 + row,
126 + conflictAlgorithm: ConflictAlgorithm.replace,
127 + );
128 + }
129 +}
cw_core/lib/tron_token_legacy.part.dart renamed
+1 -1
@@ -1,6 +1,6 @@
1 // GENERATED CODE - DO NOT MODIFY BY HAND
2
3 -part of 'tron_token.dart';
3 +part of "tron_token_legacy.dart";
4
5 // **************************************************************************
6 // TypeAdapterGenerator
cw_core/lib/wallet_service.dart
+31 -15
@@ -1,14 +1,16 @@
1 -import 'dart:convert';
2 -import 'dart:io';
3 -
4 -import 'package:cw_core/pathForWallet.dart';
5 -import 'package:cw_core/utils/file.dart';
6 -import 'package:cw_core/utils/print_verbose.dart';
7 -import 'package:cw_core/wallet_base.dart';
8 -import 'package:cw_core/wallet_credentials.dart';
9 -import 'package:cw_core/wallet_info.dart';
10 -import 'package:cw_core/wallet_type.dart';
11 -import 'package:path/path.dart' as p;
1 +import "dart:convert";
2 +import "dart:io";
3 +
4 +import "package:cw_core/pathForWallet.dart";
5 +import "package:cw_core/spl_token.dart";
6 +import "package:cw_core/tron_token.dart";
7 +import "package:cw_core/utils/file.dart";
8 +import "package:cw_core/utils/print_verbose.dart";
9 +import "package:cw_core/wallet_base.dart";
10 +import "package:cw_core/wallet_credentials.dart";
11 +import "package:cw_core/wallet_info.dart";
12 +import "package:cw_core/wallet_type.dart";
13 +import "package:path/path.dart" as p;
14
15 abstract class WalletService<N extends WalletCredentials, RFS extends WalletCredentials,
16 RFK extends WalletCredentials, RFH extends WalletCredentials> {
@@ -29,11 +31,13 @@ abstract class WalletService<N extends WalletCredentials, RFS extends WalletCred
31 Future<void> remove(String wallet);
32
33 Future<void> rename(String currentName, String password, String newName) async {
32 - if (currentName == newName) return;
34 + if (currentName == newName) {
35 + return;
36 + }
37
38 final currentWalletInfo = await WalletInfo.get(currentName, getType());
39 if (currentWalletInfo == null) {
36 - throw Exception('Wallet not found');
40 + throw Exception("Wallet not found");
41 }
42
43 await copyWalletFilesTo(fromName: currentName, toName: newName, type: getType());
@@ -43,6 +47,8 @@ abstract class WalletService<N extends WalletCredentials, RFS extends WalletCred
47 currentWalletInfo.name = newName;
48 await currentWalletInfo.save();
49
50 + await _renameTokenRows(currentName, newName);
51 +
52 final oldDir = Directory(p.join(await pathForWalletTypeDir(type: getType()), currentName));
53 if (oldDir.existsSync()) {
54 try {
@@ -53,6 +59,16 @@ abstract class WalletService<N extends WalletCredentials, RFS extends WalletCred
59 }
60 }
61
62 + Future<void> _renameTokenRows(String currentName, String newName) async {
63 + if (getType() == WalletType.solana) {
64 + await SPLToken.renameWallet(currentName, newName);
65 + }
66 +
67 + if (getType() == WalletType.tron) {
68 + await TronToken.renameWallet(currentName, newName);
69 + }
70 + }
71 +
72 Future<void> restoreWalletFilesFromBackup(String name) async {
73 final backupWalletDirPath = await pathForWalletDir(name: "$name.backup", type: getType());
74 final walletDirPath = await pathForWalletDir(name: name, type: getType());
@@ -77,14 +93,14 @@ abstract class WalletService<N extends WalletCredentials, RFS extends WalletCred
93 final jsonSource = await read(path: path, password: password);
94 try {
95 final data = json.decode(jsonSource) as Map;
80 - return data['mnemonic'] as String? ?? '';
96 + return data["mnemonic"] as String? ?? "";
97 } catch (_) {
98 // if not a valid json
99 return jsonSource.substring(0, 200);
100 }
101 } catch (_) {
102 // if the file couldn't be opened or read
87 - return '';
103 + return "";
104 }
105 }
106
cw_core/test/token_sqlite_migration_test.dart new
+365
@@ -0,0 +1,365 @@
1 +import "dart:io";
2 +
3 +import "package:cw_core/cake_hive.dart";
4 +import "package:cw_core/db/sqlite.dart";
5 +import "package:cw_core/erc20_token.dart" as erc20_sql;
6 +import "package:cw_core/erc20_token_legacy.dart" as erc20_legacy;
7 +import "package:cw_core/root_dir.dart";
8 +import "package:cw_core/spl_token.dart" as spl_sql;
9 +import "package:cw_core/spl_token_legacy.dart" as spl_legacy;
10 +import "package:cw_core/tron_token.dart" as tron_sql;
11 +import "package:cw_core/tron_token_legacy.dart" as tron_legacy;
12 +import "package:cw_core/wallet_type.dart";
13 +import "package:flutter_test/flutter_test.dart";
14 +import "package:path_provider_platform_interface/path_provider_platform_interface.dart";
15 +import "package:sqflite_common_ffi/sqflite_ffi.dart";
16 +
17 +// Faking the documents dir keeps getAppDir() off the platform channel, so the
18 +// test runs with a plain `flutter test` on any host and in CI.
19 +class _FakePathProviderPlatform extends PathProviderPlatform {
20 + _FakePathProviderPlatform(this.root);
21 +
22 + final String root;
23 +
24 + @override
25 + Future<String?> getApplicationDocumentsPath() async => root;
26 +
27 + @override
28 + Future<String?> getApplicationSupportPath() async => root;
29 +}
30 +
31 +Future<void> main() async {
32 + final dataRoot = Directory("./test/data/token_migration");
33 +
34 + Future<void> insertWalletInfoRow(String name, WalletType type) async {
35 + await db!.insert("WalletInfo", {
36 + "id": "${walletTypeToString(type).toLowerCase()}_$name",
37 + "name": name,
38 + "type": type.index,
39 + "isRecovery": 0,
40 + "restoreHeight": 0,
41 + "timestamp": 0,
42 + "dirPath": "",
43 + "path": "",
44 + "address": "",
45 + "showIntroCakePayCard": 0,
46 + "walletInfoDerivationInfoId": 0,
47 + "isNonSeedWallet": 0,
48 + "sortOrder": 0,
49 + "receiveInfoboxDismissed": 0,
50 + "showCombinedBalance": 1,
51 + });
52 + }
53 +
54 + group(
55 + "token sqlite migration",
56 + () {
57 + setUpAll(() async {
58 + if (dataRoot.existsSync()) {
59 + dataRoot.deleteSync(recursive: true);
60 + }
61 + dataRoot.createSync(recursive: true);
62 +
63 + PathProviderPlatform.instance = _FakePathProviderPlatform(dataRoot.absolute.path);
64 +
65 + // On linux getAppDir() appends /cake_wallet to the documents dir and picks the
66 + // first existing candidate, so create it up front to keep CI on the faked path.
67 + Directory("${dataRoot.path}/cake_wallet").createSync(recursive: true);
68 +
69 + sqfliteFfiInit();
70 + databaseFactory = databaseFactoryFfi;
71 + await initDb();
72 +
73 + // Everything must share the dir initDb resolved so boxExists finds the boxes
74 + final appDir = await getAppDir();
75 + CakeHive.init(appDir.path);
76 +
77 + if (!CakeHive.isAdapterRegistered(erc20_legacy.Erc20Token.typeId)) {
78 + CakeHive.registerAdapter(erc20_legacy.Erc20TokenAdapter());
79 + }
80 + if (!CakeHive.isAdapterRegistered(spl_legacy.SPLToken.typeId)) {
81 + CakeHive.registerAdapter(spl_legacy.SPLTokenAdapter());
82 + }
83 + if (!CakeHive.isAdapterRegistered(tron_legacy.TronToken.typeId)) {
84 + CakeHive.registerAdapter(tron_legacy.TronTokenAdapter());
85 + }
86 +
87 + // Two ethereum wallets whose names sanitize to the same box name, plus sol and tron
88 + await insertWalletInfoRow("My Wallet", WalletType.ethereum);
89 + await insertWalletInfoRow("My_Wallet", WalletType.ethereum);
90 + await insertWalletInfoRow("sol wallet", WalletType.solana);
91 + await insertWalletInfoRow("tron1", WalletType.tron);
92 +
93 + // Legacy global box shared by every ethereum wallet in the pre per-wallet era
94 + final globalBox = await CakeHive.openBox<erc20_legacy.Erc20Token>("Erc20Tokens");
95 + await globalBox.put(
96 + "0xGlobalTokenAAA",
97 + erc20_legacy.Erc20Token(
98 + name: "Global Legacy",
99 + symbol: "GLB",
100 + contractAddress: "0xGlobalTokenAAA",
101 + decimal: 18,
102 + enabled: true,
103 + ),
104 + );
105 +
106 + // Per-wallet ethereum box with a duplicate contract in two casings and a disabled token
107 + final ethBox =
108 + await CakeHive.openBox<erc20_legacy.Erc20Token>("My_Wallet_EthereumErc20Tokens");
109 + await ethBox.put(
110 + "0xDupCASE01",
111 + erc20_legacy.Erc20Token(
112 + name: "Dup Token",
113 + symbol: "DUP",
114 + contractAddress: "0xDupCASE01",
115 + decimal: 18,
116 + enabled: false,
117 + iconPath: "",
118 + ),
119 + );
120 + await ethBox.put(
121 + "0xdupcase01",
122 + erc20_legacy.Erc20Token(
123 + name: "Dup Token",
124 + symbol: "DUP",
125 + contractAddress: "0xdupcase01",
126 + decimal: 18,
127 + enabled: true,
128 + iconPath: "assets/images/dup.png",
129 + ),
130 + );
131 + await ethBox.put(
132 + "0xkeepmedisabled",
133 + erc20_legacy.Erc20Token(
134 + name: "Keep Me",
135 + symbol: "KEEP",
136 + contractAddress: "0xkeepmedisabled",
137 + decimal: 6,
138 + enabled: false,
139 + ),
140 + );
141 +
142 + // A polygon chain box for the same wallet name
143 + final polyBox =
144 + await CakeHive.openBox<erc20_legacy.Erc20Token>("My_Wallet_PolygonErc20Tokens");
145 + await polyBox.put(
146 + "0xpolytoken01",
147 + erc20_legacy.Erc20Token(
148 + name: "Poly Token",
149 + symbol: "PLY",
150 + contractAddress: "0xpolytoken01",
151 + decimal: 18,
152 + enabled: true,
153 + tag: "POL",
154 + ),
155 + );
156 +
157 + // SPL and Tron boxes, base58 keys must keep their exact casing
158 + final splBox = await CakeHive.openBox<spl_legacy.SPLToken>("sol_wallet_SPLTokens");
159 + await splBox.put(
160 + "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
161 + spl_legacy.SPLToken(
162 + name: "USD Coin",
163 + symbol: "USDC",
164 + mintAddress: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
165 + decimal: 6,
166 + mint: "usdc",
167 + enabled: false,
168 + ),
169 + );
170 +
171 + final tronBox = await CakeHive.openBox<tron_legacy.TronToken>("tron1_TronTokens");
172 + await tronBox.put(
173 + "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
174 + tron_legacy.TronToken(
175 + name: "Tether USD",
176 + symbol: "USDT",
177 + contractAddress: "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
178 + decimal: 6,
179 + enabled: true,
180 + ),
181 + );
182 +
183 + await globalBox.close();
184 + await ethBox.close();
185 + await polyBox.close();
186 + await splBox.close();
187 + await tronBox.close();
188 + });
189 +
190 + tearDownAll(() async {
191 + await db?.close();
192 + db = null;
193 + if (dataRoot.existsSync()) {
194 + dataRoot.deleteSync(recursive: true);
195 + }
196 + });
197 +
198 + test("migrates every token box into sqlite", () async {
199 + await erc20_legacy.performErc20TokenHiveMigration();
200 + await spl_legacy.performSplTokenHiveMigration();
201 + await tron_legacy.performTronTokenHiveMigration();
202 +
203 + // Both ethereum wallets share the sanitized box name, so each gets the rows
204 + for (final walletName in ["My Wallet", "My_Wallet"]) {
205 + final ethTokens = await erc20_sql.Erc20Token.getAllForWallet(walletName, 1);
206 + final addresses = ethTokens.map((t) => t.contractAddress).toSet();
207 +
208 + expect(
209 + addresses,
210 + {"0xglobaltokenaaa", "0xdupcase01", "0xkeepmedisabled"},
211 + reason: "wallet $walletName should have the global, merged dup and disabled tokens",
212 + );
213 +
214 + final dup = ethTokens.firstWhere((t) => t.contractAddress == "0xdupcase01");
215 + expect(dup.enabled, true, reason: "dup merge ORs the enabled flags");
216 + expect(dup.iconPath, "assets/images/dup.png", reason: "dup merge prefers non-empty icon");
217 +
218 + final keep = ethTokens.firstWhere((t) => t.contractAddress == "0xkeepmedisabled");
219 + expect(keep.enabled, false, reason: "disabled toggle must survive the migration");
220 +
221 + final polyTokens = await erc20_sql.Erc20Token.getAllForWallet(walletName, 137);
222 + expect(polyTokens.map((t) => t.contractAddress), ["0xpolytoken01"]);
223 + }
224 +
225 + final splTokens = await spl_sql.SPLToken.getAllForWallet("sol wallet");
226 + expect(splTokens.length, 1);
227 + expect(
228 + splTokens.first.mintAddress,
229 + "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
230 + reason: "mint address casing must be preserved",
231 + );
232 + expect(splTokens.first.enabled, false);
233 +
234 + final tronTokens = await tron_sql.TronToken.getAllForWallet("tron1");
235 + expect(tronTokens.length, 1);
236 + expect(
237 + tronTokens.first.contractAddress,
238 + "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
239 + reason: "tron contract address casing must be preserved",
240 + );
241 +
242 + // Drained boxes are removed from disk
243 + expect(await CakeHive.boxExists("Erc20Tokens"), false);
244 + expect(await CakeHive.boxExists("My_Wallet_EthereumErc20Tokens"), false);
245 + expect(await CakeHive.boxExists("My_Wallet_PolygonErc20Tokens"), false);
246 + expect(await CakeHive.boxExists("sol_wallet_SPLTokens"), false);
247 + expect(await CakeHive.boxExists("tron1_TronTokens"), false);
248 + });
249 +
250 + test("re-running the migrations is a no-op", () async {
251 + final before = (await erc20_sql.Erc20Token.selectList("", [])).length +
252 + (await spl_sql.SPLToken.selectList("", [])).length +
253 + (await tron_sql.TronToken.selectList("", [])).length;
254 +
255 + await erc20_legacy.performErc20TokenHiveMigration();
256 + await spl_legacy.performSplTokenHiveMigration();
257 + await tron_legacy.performTronTokenHiveMigration();
258 +
259 + final after = (await erc20_sql.Erc20Token.selectList("", [])).length +
260 + (await spl_sql.SPLToken.selectList("", [])).length +
261 + (await tron_sql.TronToken.selectList("", [])).length;
262 +
263 + expect(after, before);
264 + });
265 +
266 + test("an interrupted migration re-drains over existing rows without duplicating", () async {
267 + // Simulates a run that inserted rows into sqlite but died before the box was
268 + // emptied: the box reappears holding a token that already has a row (USDC,
269 + // re-enabled in the box copy) plus one the interrupted run never reached (BONK)
270 + final box = await CakeHive.openBox<spl_legacy.SPLToken>("sol_wallet_SPLTokens");
271 + await box.put(
272 + "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
273 + spl_legacy.SPLToken(
274 + name: "USD Coin",
275 + symbol: "USDC",
276 + mintAddress: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
277 + decimal: 6,
278 + mint: "usdc",
279 + enabled: true,
280 + ),
281 + );
282 + await box.put(
283 + "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263",
284 + spl_legacy.SPLToken(
285 + name: "Bonk",
286 + symbol: "BONK",
287 + mintAddress: "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263",
288 + decimal: 5,
289 + mint: "bonk",
290 + enabled: true,
291 + ),
292 + );
293 + await box.close();
294 +
295 + await spl_legacy.performSplTokenHiveMigration();
296 +
297 + final tokens = await spl_sql.SPLToken.getAllForWallet("sol wallet");
298 + expect(tokens.length, 2, reason: "the re-drain must not duplicate the existing USDC row");
299 +
300 + final usdc = tokens.firstWhere((t) => t.symbol == "USDC");
301 + expect(usdc.enabled, true, reason: "the box copy wins over the stale row on re-drain");
302 +
303 + expect(await CakeHive.boxExists("sol_wallet_SPLTokens"), false);
304 + });
305 +
306 + test("save without wallet context throws", () {
307 + final token = erc20_sql.Erc20Token(
308 + name: "No Context",
309 + symbol: "NOC",
310 + contractAddress: "0xnocontext",
311 + decimal: 18,
312 + );
313 +
314 + expect(token.save, throwsStateError);
315 + });
316 +
317 + test("seeding preserves the enabled toggle without clobbering", () async {
318 + // Same shape addInitialTokens uses: read existing, copyWith preserving enabled
319 + final existing =
320 + await erc20_sql.Erc20Token.getByContract("My Wallet", 1, "0xKEEPMEDISABLED");
321 + expect(existing, isNotNull);
322 + expect(existing!.enabled, false);
323 +
324 + final refreshedDefault = erc20_sql.Erc20Token(
325 + name: "Keep Me Renamed By Defaults",
326 + symbol: "KEEP",
327 + contractAddress: "0xkeepmedisabled",
328 + decimal: 6,
329 + enabled: true,
330 + );
331 + final toSave = erc20_sql.Erc20Token.copyWith(
332 + refreshedDefault,
333 + enabled: existing.enabled,
334 + walletName: "My Wallet",
335 + chainId: 1,
336 + );
337 + await toSave.save();
338 +
339 + final reloaded =
340 + await erc20_sql.Erc20Token.getByContract("My Wallet", 1, "0xkeepmedisabled");
341 + expect(reloaded!.enabled, false, reason: "metadata refresh must not re-enable the token");
342 + expect(reloaded.name, "Keep Me Renamed By Defaults");
343 +
344 + final rowCount = (await erc20_sql.Erc20Token.getAllForWallet("My Wallet", 1)).length;
345 + expect(rowCount, 3, reason: "upsert must replace, not duplicate");
346 + });
347 +
348 + test("rename moves rows and delete removes them", () async {
349 + await erc20_sql.Erc20Token.renameWallet("My Wallet", "Renamed Wallet");
350 +
351 + expect(await erc20_sql.Erc20Token.getAllForWallet("My Wallet", 1), isEmpty);
352 + expect((await erc20_sql.Erc20Token.getAllForWallet("Renamed Wallet", 1)).length, 3);
353 + expect((await erc20_sql.Erc20Token.getAllForWallet("Renamed Wallet", 137)).length, 1);
354 +
355 + await erc20_sql.Erc20Token.deleteAllForWallet("My_Wallet");
356 + expect(await erc20_sql.Erc20Token.getAllForWallet("My_Wallet", 1), isEmpty);
357 + expect(await erc20_sql.Erc20Token.getAllForWallet("My_Wallet", 137), isEmpty);
358 +
359 + // Rename into a name that has orphaned rows must not trip the unique index
360 + await erc20_sql.Erc20Token.renameWallet("Renamed Wallet", "My_Wallet");
361 + expect((await erc20_sql.Erc20Token.getAllForWallet("My_Wallet", 1)).length, 3);
362 + });
363 + },
364 + );
365 +}
cw_evm/lib/evm_chain_wallet.dart
+47 -208
@@ -5,7 +5,6 @@ import 'dart:typed_data';
5 import 'package:bip32/bip32.dart' as bip32;
6 import 'package:bip39/bip39.dart' as bip39;
7 import 'package:cw_core/amount/money.dart';
8 -import 'package:cw_core/cake_hive.dart';
8 import 'package:cw_core/crypto_currency.dart';
9 import 'package:cw_core/encryption_file_utils.dart';
10 import 'package:cw_core/erc20_token.dart';
@@ -38,7 +37,6 @@ import 'package:cw_evm/hardware/evm_chain_bitbox_credentials.dart';
37 import 'package:cw_evm/hardware/evm_chain_ledger_credentials.dart';
38 import 'package:cw_evm/hardware/evm_chain_trezor_credentials.dart';
39 import 'package:hex/hex.dart';
41 -import 'package:hive/hive.dart';
40 import 'package:mobx/mobx.dart';
41 import 'package:shared_preferences/shared_preferences.dart';
42 import 'package:web3dart/crypto.dart';
@@ -109,10 +107,6 @@ abstract class EVMChainWalletBase
107 this.walletInfo = walletInfo;
108 transactionHistory = setUpTransactionHistory(walletInfo, password, encryptionFileUtils);
109
112 - if (!CakeHive.isAdapterRegistered(Erc20Token.typeId)) {
113 - CakeHive.registerAdapter(Erc20TokenAdapter());
114 - }
115 -
110 sharedPrefs.complete(SharedPreferences.getInstance());
111 }
112
@@ -121,9 +115,7 @@ abstract class EVMChainWalletBase
115 final String _password;
116 final EncryptionFileUtils encryptionFileUtils;
117
124 - late final Box<Erc20Token> erc20TokensBox;
125 -
126 - late Box<Erc20Token> evmChainErc20TokensBox;
118 + List<Erc20Token> _erc20Tokens = [];
119
120 late final Credentials _evmChainPrivateKey;
121
@@ -217,8 +209,8 @@ abstract class EVMChainWalletBase
209 // Automatically connect to node for the selected chain
210 await connectToNode(node: node);
211
220 - // Reload ERC20 tokens box for the new chain
221 - await initErc20TokensBox();
212 + // Reload ERC20 tokens for the new chain
213 + await initErc20Tokens();
214
215 // Reload transaction history from the new chain's file
216 await transactionHistory.init();
@@ -228,179 +220,52 @@ abstract class EVMChainWalletBase
220 await startSync();
221 }
222
231 - void addInitialTokens() {
223 + Future<void> addInitialTokens() async {
224 final initialErc20Tokens = EVMChainDefaultTokens.getDefaultTokensByChainId(selectedChainId);
225
226 for (final token in initialErc20Tokens) {
235 - if (!evmChainErc20TokensBox.containsKey(token.contractAddress)) {
236 - evmChainErc20TokensBox.put(token.contractAddress, token);
237 - } else {
238 - // update existing token
239 - final existingToken = evmChainErc20TokensBox.get(token.contractAddress);
240 - evmChainErc20TokensBox.put(
241 - token.contractAddress,
242 - Erc20Token.copyWith(token, enabled: existingToken!.enabled),
243 - );
244 - }
227 + final existingToken = _findCachedToken(token.contractAddress);
228 +
229 + final newToken = Erc20Token.copyWith(
230 + token,
231 + enabled: existingToken?.enabled ?? token.enabled,
232 + walletName: walletInfo.name,
233 + chainId: selectedChainId,
234 + );
235 +
236 + await newToken.save();
237 + _upsertCachedToken(newToken);
238 }
239 }
240
241 List<String> get getDefaultTokenContractAddresses =>
242 EVMChainDefaultTokens.getDefaultTokenAddresses(selectedChainId);
243
251 - Future<void> initErc20TokensBox() async {
252 - // Migration for old WalletType.ethereum wallets:
253 - // Old wallets used a global erc20TokensBox (shared across all wallets).
254 - // New system uses wallet-specific, chain-specific boxes.
255 - // This checks if migration is needed and runs it once.
256 - if (walletInfo.type == WalletType.ethereum) {
257 - try {
258 - // Try to access erc20TokensBox - if it exists, migration already ran
259 - final _ = erc20TokensBox;
260 - // Migration done, proceed with normal chain-specific logic below
261 - } catch (_) {
262 - // erc20TokensBox doesn't exist yet, run migration from global box
263 - await _initEthereumErc20TokensBox();
264 - await _normalizeEvmChainErc20TokensBoxKeys();
265 - return;
266 - }
267 - }
268 -
269 - final chainId = selectedChainId;
244 + Future<void> initErc20Tokens() async {
245 + _erc20Tokens = await Erc20Token.getAllForWallet(walletInfo.name, selectedChainId);
246
271 - final boxName = EVMChainUtils.getErc20TokensBoxName(walletInfo.name, chainId);
272 -
273 - // Close existing box if it's already open (for chain switching)
274 - try {
275 - if (evmChainErc20TokensBox.isOpen) {
276 - await evmChainErc20TokensBox.close();
277 - }
278 - } catch (_) {
279 - // Box might not be initialized yet, ignore
280 - }
281 -
282 - // Check if box is already open, if so use it, otherwise open it
283 - if (CakeHive.isBoxOpen(boxName)) {
284 - evmChainErc20TokensBox = CakeHive.box<Erc20Token>(boxName);
285 - } else {
286 - evmChainErc20TokensBox = await CakeHive.openBox<Erc20Token>(boxName);
287 - }
288 -
289 - await _normalizeEvmChainErc20TokensBoxKeys();
290 -
291 - addInitialTokens();
247 + await addInitialTokens();
248 }
249
294 - /// Ethereum-specific initialization with backward compatibility
295 - Future<void> _initEthereumErc20TokensBox() async {
296 - // Opens a box specific to this wallet
297 - evmChainErc20TokensBox = await CakeHive.openBox<Erc20Token>(
298 - "${walletInfo.name.replaceAll(" ", "_")}_${Erc20Token.ethereumBoxName}",
299 - );
300 -
301 - erc20TokensBox = await CakeHive.openBox<Erc20Token>(Erc20Token.boxName);
250 + Erc20Token? _findCachedToken(String contractAddress) {
251 + final lowerAddress = contractAddress.toLowerCase();
252
303 - if (erc20TokensBox.isEmpty) {
304 - if (evmChainErc20TokensBox.isEmpty) addInitialTokens();
305 - return;
253 + for (final token in _erc20Tokens) {
254 + if (token.contractAddress.toLowerCase() == lowerAddress) return token;
255 }
256 + return null;
257 + }
258
308 - final allValues = erc20TokensBox.values.toList();
309 -
310 - // Clear and delete the old token box
311 - await erc20TokensBox.clear();
312 - await erc20TokensBox.deleteFromDisk();
259 + void _upsertCachedToken(Erc20Token token) {
260 + final lowerAddress = token.contractAddress.toLowerCase();
261
314 - // Add all the previous tokens with configs to the new box
315 - await evmChainErc20TokensBox.addAll(allValues);
262 + _erc20Tokens.removeWhere((t) => t.contractAddress.toLowerCase() == lowerAddress);
263 + _erc20Tokens.add(token);
264 }
265
266 String getTransactionHistoryFileName() =>
267 EVMChainUtils.getTransactionHistoryFileName(selectedChainId);
268
321 - /// Ensures all ERC20 token entries use lowercase contract addresses as
322 - /// their Hive keys to avoid duplicates caused by case differences.
323 - Future<void> _normalizeEvmChainErc20TokensBoxKeys() async {
324 - if (!evmChainErc20TokensBox.isOpen) return;
325 -
326 - final prefs = await sharedPrefs.future;
327 - final migrationKey = 'erc20_box_normalized_${walletInfo.name}_$selectedChainId';
328 -
329 - if (prefs.getBool(migrationKey) ?? false) return;
330 -
331 - final box = evmChainErc20TokensBox;
332 - final keys = box.keys.toList();
333 -
334 - if (keys.isEmpty) {
335 - await prefs.setBool(migrationKey, true);
336 - return;
337 - }
338 -
339 - final Map<String, Erc20Token> normalizedTokens = {};
340 - var needsRewrite = false;
341 -
342 - for (final key in keys) {
343 - final token = box.get(key);
344 -
345 - if (token == null) {
346 - needsRewrite = true;
347 - continue;
348 - }
349 -
350 - final lowerKey = key is String ? key.toLowerCase() : token.contractAddress.toLowerCase();
351 - if (key is int) needsRewrite = true;
352 -
353 - final Erc20Token normalizedToken =
354 - token.contractAddress == token.contractAddress.toLowerCase()
355 - ? token
356 - : Erc20Token(
357 - name: token.name,
358 - symbol: token.symbol,
359 - contractAddress: token.contractAddress.toLowerCase(),
360 - decimal: token.decimal,
361 - enabled: token.enabled,
362 - iconPath: token.iconPath,
363 - tag: token.tag,
364 - isPotentialScam: token.isPotentialScam,
365 - );
366 -
367 - if (!needsRewrite && (lowerKey != key || identical(normalizedToken, token) == false)) {
368 - needsRewrite = true;
369 - }
370 -
371 - final existing = normalizedTokens[lowerKey];
372 -
373 - if (existing == null) {
374 - normalizedTokens[lowerKey] = normalizedToken;
375 - continue;
376 - }
377 -
378 - final merged = Erc20Token(
379 - name: normalizedToken.name,
380 - symbol: normalizedToken.symbol,
381 - contractAddress: lowerKey,
382 - decimal: normalizedToken.decimal,
383 - enabled: normalizedToken.enabled || existing.enabled,
384 - iconPath: (normalizedToken.iconPath?.isNotEmpty ?? false)
385 - ? normalizedToken.iconPath
386 - : existing.iconPath,
387 - tag: normalizedToken.tag ?? existing.tag,
388 - isPotentialScam: normalizedToken.isPotentialScam || existing.isPotentialScam,
389 - );
390 -
391 - normalizedTokens[lowerKey] = merged;
392 - }
393 -
394 - if (needsRewrite) {
395 - await box.clear();
396 - for (final entry in normalizedTokens.entries) {
397 - await box.put(entry.key, entry.value);
398 - }
399 - }
400 -
401 - await prefs.setBool(migrationKey, true);
402 - }
403 -
269 Future<bool> checkIfScanProviderIsEnabled() async {
270 final key = EVMChainUtils.getScanProviderPreferenceKey(selectedChainId);
271 return (await sharedPrefs.future).getBool(key) ?? true;
@@ -451,6 +316,8 @@ abstract class EVMChainWalletBase
316 tag: token.tag ?? EVMChainUtils.getDefaultTokenTag(selectedChainId),
317 iconPath: iconPath,
318 isPotentialScam: token.isPotentialScam,
319 + walletName: walletInfo.name,
320 + chainId: selectedChainId,
321 );
322 }
323
@@ -499,7 +366,7 @@ abstract class EVMChainWalletBase
366 String idFor(String name, WalletType type) => '${walletTypeToString(type).toLowerCase()}_$name';
367
368 Future<void> init() async {
502 - await initErc20TokensBox();
369 + await initErc20Tokens();
370
371 await walletAddresses.init();
372 await transactionHistory.init();
@@ -669,8 +536,6 @@ abstract class EVMChainWalletBase
536
537 Future<MoralisDiscoveryResult> discoverTokensFromMoralis() async {
538 try {
672 - if (!evmChainErc20TokensBox.isOpen) return MoralisDiscoveryResult.empty;
673 -
539 final address = walletAddresses.address;
540 if (address.isEmpty) return MoralisDiscoveryResult.empty;
541
@@ -680,8 +545,7 @@ abstract class EVMChainWalletBase
545 if (walletTokens.isEmpty) return MoralisDiscoveryResult.empty;
546
547 final existingTokenAddresses = {
683 - for (final token in evmChainErc20TokensBox.values)
684 - token.contractAddress.toLowerCase(): token,
548 + for (final token in _erc20Tokens) token.contractAddress.toLowerCase(): token,
549 };
550
551 final whitelistedTokenAddresses =
@@ -1419,32 +1283,21 @@ abstract class EVMChainWalletBase
1283 }
1284
1285 Future<void> _fetchErc20Balances() async {
1422 - // Check if box is open before accessing it
1423 - if (!evmChainErc20TokensBox.isOpen) {
1424 - return;
1425 - }
1426 -
1286 // First, clean up any tokens in balance map that don't belong to current chain
1287 // This handles tokens from previous chains that might still be in the balance map
1288 final tokensInBalance = balance.keys.whereType<Erc20Token>().toList();
1430 - final tokensInBox = evmChainErc20TokensBox.values.toList();
1431 - final boxTokenAddresses = tokensInBox.map((t) => t.contractAddress.toLowerCase()).toSet();
1289 + final tokens = _erc20Tokens.toList();
1290 + final cachedTokenAddresses = tokens.map((t) => t.contractAddress.toLowerCase()).toSet();
1291
1292 for (var token in tokensInBalance) {
1434 - // Remove token if it's not in the current box or doesn't match current chain
1435 - if (!boxTokenAddresses.contains(token.contractAddress.toLowerCase()) ||
1293 + // Remove token if it's not in the current token list or doesn't match current chain
1294 + if (!cachedTokenAddresses.contains(token.contractAddress.toLowerCase()) ||
1295 !_isTokenMatchingChain(token)) {
1296 balance.remove(token);
1297 }
1298 }
1299
1441 - // Get a snapshot of tokens from current box to avoid issues if box is closed during iteration
1442 - final tokens = tokensInBox;
1443 -
1300 for (var token in tokens) {
1445 - // Check if box is still open before operating on tokens
1446 - if (!evmChainErc20TokensBox.isOpen) break;
1447 -
1301 if (!_isTokenMatchingChain(token)) {
1302 printV('NOTEE!!!: Token ${token.title} is not matching the currency ${currency.title}');
1303 try {
@@ -1532,15 +1385,7 @@ abstract class EVMChainWalletBase
1385 @override
1386 Future<void> updateTransactionsHistory() async => await _updateTransactions();
1387
1535 - List<Erc20Token> get erc20Currencies {
1536 - try {
1537 - if (!evmChainErc20TokensBox.isOpen) return [];
1538 -
1539 - return evmChainErc20TokensBox.values.toList();
1540 - } catch (_) {
1541 - return [];
1542 - }
1543 - }
1388 + List<Erc20Token> get erc20Currencies => _erc20Tokens.toList();
1389
1390 Future<void> addErc20Token(Erc20Token token) async {
1391 final isSuspicious = isTokenPropertiesSuspicious(token);
@@ -1561,33 +1406,27 @@ abstract class EVMChainWalletBase
1406 final newToken = createNewErc20TokenObject(token, iconPath);
1407
1408 if (newToken.enabled) {
1564 - try {
1565 - balance[newToken] = await _client.fetchERC20Balances(_evmChainPrivateKey.address, newToken);
1409 + balance[newToken] = await _client.fetchERC20Balances(_evmChainPrivateKey.address, newToken);
1410
1567 - await evmChainErc20TokensBox.put(newToken.contractAddress, newToken);
1568 - } on Exception catch (_) {
1569 - rethrow;
1570 - }
1411 + await newToken.save();
1412 } else {
1572 - await evmChainErc20TokensBox.put(newToken.contractAddress, newToken);
1413 + await newToken.save();
1414 balance.remove(newToken);
1415 }
1416 +
1417 + _upsertCachedToken(newToken);
1418 }
1419
1420 Future<void> deleteErc20Token(Erc20Token token, {bool shouldUpdateBalance = true}) async {
1578 - // Check if box is open before trying to delete
1579 - if (!evmChainErc20TokensBox.isOpen) {
1580 - balance.remove(token);
1581 - return;
1582 - }
1583 -
1421 try {
1585 - await token.delete();
1422 + await Erc20Token.deleteForWallet(walletInfo.name, selectedChainId, token.contractAddress);
1423 } catch (e) {
1587 - // Token might be from a closed box, just remove from balance
1588 - printV('Error deleting token from box: $e');
1424 + printV('Error deleting token: $e');
1425 }
1426
1427 + _erc20Tokens
1428 + .removeWhere((t) => t.contractAddress.toLowerCase() == token.contractAddress.toLowerCase());
1429 +
1430 balance.remove(token);
1431 await removeTokenTransactionsInHistory(token);
1432 if (shouldUpdateBalance) {
cw_evm/lib/evm_chain_wallet_service.dart
+34 -19
@@ -1,7 +1,8 @@
1 -import 'dart:io';
1 +import "dart:io";
2
3 import 'package:bip39/bip39.dart' as bip39;
4 import 'package:cw_core/encryption_file_utils.dart';
5 +import "package:cw_core/erc20_token.dart";
6 import 'package:cw_core/pathForWallet.dart';
7 import 'package:cw_core/utils/print_verbose.dart';
8 import 'package:cw_core/wallet_base.dart';
@@ -49,8 +50,8 @@ class EVMChainWalletService extends WalletService<
50 @override
51 WalletType getType() {
52 throw UnsupportedError(
52 - 'EVMChainWalletService is unified and does not have a single type. '
53 - 'Use walletInfo.type instead.',
53 + "EVMChainWalletService is unified and does not have a single type. "
54 + "Use walletInfo.type instead.",
55 );
56 }
57
@@ -60,7 +61,7 @@ class EVMChainWalletService extends WalletService<
61 Future<void> saveBackup(String name, {WalletInfo? walletInfo}) async {
62 final info = walletInfo ?? await _findWalletByName(name);
63 if (info == null) {
63 - throw Exception('Wallet not found: $name');
64 + throw Exception("Wallet not found: $name");
65 }
66
67 final backupWalletDirPath = await pathForWalletDir(name: "$name.backup", type: info.type);
@@ -76,7 +77,7 @@ class EVMChainWalletService extends WalletService<
77 Future<void> restoreWalletFilesFromBackup(String name) async {
78 final walletInfo = await _findWalletByName(name);
79 if (walletInfo == null) {
79 - throw Exception('Wallet not found: $name');
80 + throw Exception("Wallet not found: $name");
81 }
82
83 final backupWalletDirPath = await pathForWalletDir(name: "$name.backup", type: walletInfo.type);
@@ -97,7 +98,7 @@ class EVMChainWalletService extends WalletService<
98 // Get chainId from wallet type
99 final chainConfig = _registry.getChainConfigByWalletType(walletInfo.type);
100 if (chainConfig == null) {
100 - throw Exception('Chain config not found for wallet type: ${walletInfo.type}');
101 + throw Exception("Chain config not found for wallet type: ${walletInfo.type}");
102 }
103 final initialChainId = chainConfig.chainId;
104
@@ -125,7 +126,7 @@ class EVMChainWalletService extends WalletService<
126 );
127
128 await wallet.init();
128 - wallet.addInitialTokens();
129 + await wallet.addInitialTokens();
130 await wallet.save();
131 return wallet;
132 }
@@ -134,7 +135,7 @@ class EVMChainWalletService extends WalletService<
135 Future<EVMChainWallet> openWallet(String name, String password) async {
136 final walletInfo = await _findWalletByName(name);
137 if (walletInfo == null) {
137 - throw Exception('Wallet not found');
138 + throw Exception("Wallet not found");
139 }
140
141 try {
@@ -146,7 +147,7 @@ class EVMChainWalletService extends WalletService<
147 );
148
149 await wallet.init();
149 - wallet.addInitialTokens();
150 + await wallet.addInitialTokens();
151 await wallet.save();
152 await saveBackup(name);
153 return wallet;
@@ -161,7 +162,7 @@ class EVMChainWalletService extends WalletService<
162 );
163
164 await wallet.init();
164 - wallet.addInitialTokens();
165 + await wallet.addInitialTokens();
166 await wallet.save();
167 return wallet;
168 }
@@ -173,7 +174,7 @@ class EVMChainWalletService extends WalletService<
174
175 final currentWalletInfo = await _findWalletByName(currentName);
176 if (currentWalletInfo == null) {
176 - throw Exception('Wallet not found');
177 + throw Exception("Wallet not found");
178 }
179
180 final type = currentWalletInfo.type;
@@ -185,6 +186,16 @@ class EVMChainWalletService extends WalletService<
186 currentWalletInfo.name = newName;
187 await currentWalletInfo.save();
188
189 + final oldNameStillUsed = (await _findWalletByName(currentName)) != null;
190 + if (oldNameStillUsed) {
191 + for (final token in await Erc20Token.selectList("walletName = ?", [currentName])) {
192 + final copiedToken = Erc20Token.copyWith(token, walletName: newName);
193 + await copiedToken.save();
194 + }
195 + } else {
196 + await Erc20Token.renameWallet(currentName, newName);
197 + }
198 +
199 final oldDir = Directory(p.join(await pathForWalletTypeDir(type: type), currentName));
200 if (oldDir.existsSync()) {
201 try {
@@ -209,7 +220,7 @@ class EVMChainWalletService extends WalletService<
220 // Get chainId from wallet type
221 final chainConfig = _registry.getChainConfigByWalletType(walletInfo.type);
222 if (chainConfig == null) {
212 - throw Exception('Chain config not found for wallet type: ${walletInfo.type}');
223 + throw Exception("Chain config not found for wallet type: ${walletInfo.type}");
224 }
225 final initialChainId = chainConfig.chainId;
226
@@ -235,7 +246,7 @@ class EVMChainWalletService extends WalletService<
246 );
247
248 await wallet.init();
238 - wallet.addInitialTokens();
249 + await wallet.addInitialTokens();
250 await wallet.save();
251 return wallet;
252 }
@@ -250,7 +261,7 @@ class EVMChainWalletService extends WalletService<
261 // Get chainId from wallet type
262 final chainConfig = _registry.getChainConfigByWalletType(walletInfo.type);
263 if (chainConfig == null) {
253 - throw Exception('Chain config not found for wallet type: ${walletInfo.type}');
264 + throw Exception("Chain config not found for wallet type: ${walletInfo.type}");
265 }
266 final initialChainId = chainConfig.chainId;
267
@@ -275,7 +286,7 @@ class EVMChainWalletService extends WalletService<
286 );
287
288 await wallet.init();
278 - wallet.addInitialTokens();
289 + await wallet.addInitialTokens();
290 await wallet.save();
291 return wallet;
292 }
@@ -289,7 +300,7 @@ class EVMChainWalletService extends WalletService<
300 // Get chainId from wallet type
301 final chainConfig = _registry.getChainConfigByWalletType(walletInfo.type);
302 if (chainConfig == null) {
292 - throw Exception('Chain config not found for wallet type: ${walletInfo.type}');
303 + throw Exception("Chain config not found for wallet type: ${walletInfo.type}");
304 }
305 final initialChainId = chainConfig.chainId;
306
@@ -313,7 +324,7 @@ class EVMChainWalletService extends WalletService<
324 );
325
326 await wallet.init();
316 - wallet.addInitialTokens();
327 + await wallet.addInitialTokens();
328 await wallet.save();
329 return wallet;
330 }
@@ -332,11 +343,15 @@ class EVMChainWalletService extends WalletService<
343 Future<void> remove(String wallet) async {
344 final walletInfo = await _findWalletByName(wallet);
345 if (walletInfo == null) {
335 - throw Exception('Wallet not found');
346 + throw Exception("Wallet not found");
347 }
348
349 File(await pathForWalletDir(name: wallet, type: walletInfo.type)).delete(recursive: true);
350 await WalletInfo.delete(walletInfo);
351 + final nameStillUsed = (await _findWalletByName(wallet)) != null;
352 + if (!nameStillUsed) {
353 + await Erc20Token.deleteAllForWallet(wallet);
354 + }
355 }
356
357 EVMChainWallet _createWalletInstance({
@@ -354,7 +369,7 @@ class EVMChainWalletService extends WalletService<
369 final chainConfig = _registry.getChainConfigByWalletType(walletType);
370
371 if (chainConfig == null) {
357 - throw Exception('Chain config not found for wallet type: $walletType');
372 + throw Exception("Chain config not found for wallet type: $walletType");
373 }
374
375 return EVMChainWallet(
cw_evm/lib/utils/evm_chain_utils.dart
+41 -73
@@ -1,11 +1,9 @@
1 -import 'package:cw_core/erc20_token.dart';
2 -import 'package:cw_evm/evm_chain_transaction_priority.dart';
3 -import 'package:web3dart/web3dart.dart' show EtherAmount, EtherUnit;
1 +import "package:cw_evm/evm_chain_transaction_priority.dart";
2 +import "package:web3dart/web3dart.dart" show EtherAmount, EtherUnit;
3
4 /// Utility class for chain-specific EVM chain operations
5 class EVMChainUtils {
7 - static int getTotalPriorityFee(EVMChainTransactionPriority priority, int chainId) {
8 - return switch (chainId) {
6 + static int getTotalPriorityFee(EVMChainTransactionPriority priority, int chainId) => switch (chainId) {
7 1 => _ethereumPriorityFee(priority),
8 137 => _polygonPriorityFee(priority),
9 8453 => _basePriorityFee(priority),
@@ -13,14 +11,11 @@ class EVMChainUtils {
11 42161 => 0, // Arbitrum doesn't use priority fees
12 _ => _ethereumPriorityFee(priority),
13 };
16 - }
14
18 - static bool hasPriorityFee(int chainId) {
19 - return switch (chainId) {
15 + static bool hasPriorityFee(int chainId) => switch (chainId) {
16 42161 => false, // Arbitrum doesn't use priority fees
17 _ => true,
18 };
23 - }
19
20 static int computeBufferedMaxFeePerGasWei({
21 required int? gasBaseFee,
@@ -38,78 +33,53 @@ class EVMChainUtils {
33 return gasPrice + priorityFeeWei;
34 }
35
41 - static String getErc20TokensBoxName(String walletName, int chainId) {
42 - final sanitizedName = walletName.replaceAll(" ", "_");
43 -
44 - return switch (chainId) {
45 - 1 => "${sanitizedName}_${Erc20Token.ethereumBoxName}",
46 - 137 => "${sanitizedName}_${Erc20Token.polygonBoxName}",
47 - 8453 => "${sanitizedName}_${Erc20Token.baseBoxName}",
48 - 42161 => "${sanitizedName}_${Erc20Token.arbitrumBoxName}",
49 - 56 => "${sanitizedName}_${Erc20Token.bscBoxName}",
50 - _ => "${sanitizedName}_${Erc20Token.ethereumBoxName}",
36 + static String getTransactionHistoryFileName(int chainId) => switch (chainId) {
37 + 1 => "transactions.json", // Ethereum
38 + 137 => "polygon_transactions.json",
39 + 8453 => "base_transactions.json",
40 + 42161 => "arbitrum_transactions.json",
41 + 56 => "bsc_transactions.json",
42 + _ => "transactions_$chainId.json", // Generic format for other chains
43 };
52 - }
53 -
54 - static String getTransactionHistoryFileName(int chainId) {
55 - return switch (chainId) {
56 - 1 => 'transactions.json', // Ethereum
57 - 137 => 'polygon_transactions.json',
58 - 8453 => 'base_transactions.json',
59 - 42161 => 'arbitrum_transactions.json',
60 - 56 => 'bsc_transactions.json',
61 - _ => 'transactions_$chainId.json', // Generic format for other chains
62 - };
63 - }
44
45 /// Get scan provider preference key for a wallet type
66 - static String getScanProviderPreferenceKey(int chainId) {
67 - return switch (chainId) {
68 - 1 => 'use_etherscan',
69 - 137 => 'use_polygonscan',
70 - 8453 => 'use_basescan',
71 - 42161 => 'use_arbiscan',
72 - 56 => 'use_bscscan',
73 - _ => 'use_etherscan',
46 + static String getScanProviderPreferenceKey(int chainId) => switch (chainId) {
47 + 1 => "use_etherscan",
48 + 137 => "use_polygonscan",
49 + 8453 => "use_basescan",
50 + 42161 => "use_arbiscan",
51 + 56 => "use_bscscan",
52 + _ => "use_etherscan",
53 };
75 - }
54
77 - static String getDefaultTokenTag(int chainId) {
78 - return switch (chainId) {
79 - 1 => 'ETH',
80 - 137 => 'POL',
81 - 8453 => 'BASE',
82 - 42161 => 'ARB',
83 - 56 => 'BSC',
84 - _ => 'ETH',
55 + static String getDefaultTokenTag(int chainId) => switch (chainId) {
56 + 1 => "ETH",
57 + 137 => "POL",
58 + 8453 => "BASE",
59 + 42161 => "ARB",
60 + 56 => "BSC",
61 + _ => "ETH",
62 };
86 - }
63
88 - static String getFeeCurrency(int chainId) {
89 - return switch (chainId) {
90 - 1 => 'ETH',
91 - 137 => 'POL',
92 - 8453 => 'ETH',
93 - 42161 => 'ETH',
94 - 56 => 'BNB',
95 - _ => 'ETH',
64 + static String getFeeCurrency(int chainId) => switch (chainId) {
65 + 1 => "ETH",
66 + 137 => "POL",
67 + 8453 => "ETH",
68 + 42161 => "ETH",
69 + 56 => "BNB",
70 + _ => "ETH",
71 };
97 - }
72
99 - static String getDefaultTokenSymbol(int chainId) {
100 - return switch (chainId) {
101 - 1 => 'ETH',
102 - 137 => 'POL',
103 - 8453 => 'BASE',
104 - 42161 => 'ARBITRUM',
105 - 56 => 'BSC',
106 - _ => 'ETH',
73 + static String getDefaultTokenSymbol(int chainId) => switch (chainId) {
74 + 1 => "ETH",
75 + 137 => "POL",
76 + 8453 => "BASE",
77 + 42161 => "ARBITRUM",
78 + 56 => "BSC",
79 + _ => "ETH",
80 };
108 - }
81
110 - static int _ethereumPriorityFee(EVMChainTransactionPriority priority) {
111 - return EtherAmount.fromInt(EtherUnit.gwei, priority.tip).getInWei.toInt();
112 - }
82 + static int _ethereumPriorityFee(EVMChainTransactionPriority priority) => EtherAmount.fromInt(EtherUnit.gwei, priority.tip).getInWei.toInt();
83
84 // Polygon priority fee calculation (minimum 25 gwei + additional based on priority)
85 static int _polygonPriorityFee(EVMChainTransactionPriority priority) {
@@ -127,12 +97,10 @@ class EVMChainUtils {
97 return minPriorityFeeWei + additionalPriorityFee;
98 }
99
130 - static int _basePriorityFee(EVMChainTransactionPriority priority) {
131 - return switch (priority) {
100 + static int _basePriorityFee(EVMChainTransactionPriority priority) => switch (priority) {
101 EVMChainTransactionPriority.fast => EtherAmount.fromInt(EtherUnit.mwei, 5).getInWei.toInt(),
102 EVMChainTransactionPriority.medium => EtherAmount.fromInt(EtherUnit.mwei, 3).getInWei.toInt(),
103 EVMChainTransactionPriority.slow => EtherAmount.fromInt(EtherUnit.mwei, 1).getInWei.toInt(),
104 _ => EtherAmount.fromInt(EtherUnit.mwei, 1).getInWei.toInt(),
105 };
137 - }
106 }
cw_solana/lib/solana_wallet.dart
+39 -37
@@ -2,7 +2,6 @@ import 'dart:async';
2 import 'dart:convert';
3
4 import 'package:cw_core/amount/money.dart';
5 -import 'package:cw_core/cake_hive.dart';
5 import 'package:cw_core/crypto_currency.dart';
6 import 'package:cw_core/encryption_file_utils.dart';
7 import 'package:cw_core/node.dart';
@@ -28,7 +27,6 @@ import 'package:cw_solana/solana_transaction_model.dart';
27 import 'package:cw_solana/solana_wallet_addresses.dart';
28 import 'package:cw_core/spl_token.dart';
29 import 'package:hex/hex.dart';
31 -import 'package:hive/hive.dart';
30 import 'package:mobx/mobx.dart';
31 import 'package:shared_preferences/shared_preferences.dart';
32 import 'package:on_chain/solana/solana.dart' hide Store;
@@ -67,10 +65,6 @@ abstract class SolanaWalletBase
65 encryptionFileUtils: encryptionFileUtils,
66 );
67
70 - if (!CakeHive.isAdapterRegistered(SPLToken.typeId)) {
71 - CakeHive.registerAdapter(SPLTokenAdapter());
72 - }
73 -
68 _sharedPrefs.complete(SharedPreferences.getInstance());
69 }
70
@@ -90,7 +84,7 @@ abstract class SolanaWalletBase
84
85 Future<void>? _currentRefresh;
86
93 - late final Box<SPLToken> splTokensBox;
87 + List<SPLToken> _splTokens = [];
88
89 @override
90 WalletAddresses walletAddresses;
@@ -133,9 +127,7 @@ abstract class SolanaWalletBase
127 );
128
129 Future<void> init() async {
136 - final boxName = "${walletInfo.name.replaceAll(" ", "_")}_${SPLToken.boxName}";
137 -
138 - splTokensBox = await CakeHive.openBox<SPLToken>(boxName);
130 + _splTokens = await SPLToken.getAllForWallet(walletInfo.name);
131
132 await _checkForExistingScamTokens();
133
@@ -160,8 +152,6 @@ abstract class SolanaWalletBase
152 String get _scamCheckDoneKey => 'solana_scam_check_v2_done_${walletInfo.name}';
153
154 Future<void> _checkForExistingScamTokens() async {
163 - if (!splTokensBox.isOpen) return;
164 -
155 final prefs = await _sharedPrefs.future;
156 if (prefs.getBool(_scamCheckDoneKey) == true) return;
157
@@ -169,7 +159,7 @@ abstract class SolanaWalletBase
159 final defaultSymbolsUpper =
160 DefaultSPLTokens().initialSPLTokens.map((t) => t.symbol.toUpperCase()).toSet();
161
172 - for (final token in splTokensBox.values) {
162 + for (final token in _splTokens) {
163 final suspicious = isTokenPropertiesSuspicious(
164 token,
165 cachedDefaultMints: defaultMints,
@@ -421,7 +411,7 @@ abstract class SolanaWalletBase
411 }
412
413 Future<void> updateSPLTokenTransactions({List<String>? specificMints}) async {
424 - final allTokens = splTokensBox.values.where((t) => t.enabled).toList(growable: false);
414 + final allTokens = _splTokens.where((t) => t.enabled).toList(growable: false);
415
416 // Filter to specific mints if provided
417 final tokens = specificMints != null
@@ -618,11 +608,11 @@ abstract class SolanaWalletBase
608 List<String>? tokenMints,
609 }) async {
610 // Remove disabled tokens first to keep state clean
621 - for (var token in splTokensBox.values.where((t) => !t.enabled)) {
611 + for (var token in _splTokens.where((t) => !t.enabled)) {
612 balance.remove(token);
613 }
614
625 - final enabledTokens = splTokensBox.values.where((t) => t.enabled).toList(growable: false);
615 + final enabledTokens = _splTokens.where((t) => t.enabled).toList(growable: false);
616 if (enabledTokens.isEmpty) return;
617
618 final tokens = tokenMints == null || tokenMints.isEmpty
@@ -677,35 +667,48 @@ abstract class SolanaWalletBase
667 }
668 }
669
680 - List<SPLToken> get splTokenCurrencies => splTokensBox.values.toList();
670 + List<SPLToken> get splTokenCurrencies => _splTokens.toList();
671
672 SPLToken? splTokenBySymbol(String symbol) {
683 - for (final token in splTokensBox.values) {
673 + for (final token in _splTokens) {
674 if (token.symbol == symbol) return token;
675 }
676
677 return null;
678 }
679
690 - void addInitialTokens() {
680 + SPLToken? _findCachedToken(String mintAddress) {
681 + for (final token in _splTokens) {
682 + if (token.mintAddress == mintAddress) return token;
683 + }
684 +
685 + return null;
686 + }
687 +
688 + void _upsertCachedToken(SPLToken token) {
689 + _splTokens.removeWhere((t) => t.mintAddress == token.mintAddress);
690 + _splTokens.add(token);
691 + }
692 +
693 + Future<void> addInitialTokens() async {
694 final initialSPLTokens = DefaultSPLTokens().initialSPLTokens;
695
696 for (var token in initialSPLTokens) {
694 - if (!splTokensBox.containsKey(token.mintAddress)) {
695 - splTokensBox.put(token.mintAddress, token);
696 - } else {
697 - // update existing token
698 - final existingToken = splTokensBox.get(token.mintAddress);
699 - splTokensBox.put(
700 - token.mintAddress, SPLToken.copyWith(token, enabled: existingToken!.enabled));
701 - }
697 + final existingToken = _findCachedToken(token.mintAddress);
698 +
699 + final newToken = SPLToken.copyWith(
700 + token,
701 + enabled: existingToken?.enabled ?? token.enabled,
702 + walletName: walletInfo.name,
703 + );
704 +
705 + await newToken.save();
706 + _upsertCachedToken(newToken);
707 }
708 }
709
710 Future<SolanaMoralisDiscoveryResult> discoverTokensFromMoralis() async {
711 try {
707 - if (!splTokensBox.isOpen) return SolanaMoralisDiscoveryResult.empty;
708 -
712 final address = walletAddresses.address;
713 if (address.isEmpty) return SolanaMoralisDiscoveryResult.empty;
714
@@ -713,7 +716,7 @@ abstract class SolanaWalletBase
716 if (walletTokens.isEmpty) return SolanaMoralisDiscoveryResult.empty;
717
718 final existingMints = {
716 - for (final token in splTokensBox.values) token.mintAddress: token,
719 + for (final token in _splTokens) token.mintAddress: token,
720 };
721
722 final defaultMints = DefaultSPLTokens().initialSPLTokens.map((t) => t.mintAddress).toSet();
@@ -849,7 +852,9 @@ abstract class SolanaWalletBase
852 final isSuspicious = isTokenPropertiesSuspicious(token);
853 token.isPotentialScam = token.isPotentialScam || isSuspicious;
854
852 - await splTokensBox.put(token.mintAddress, token);
855 + token.walletName = walletInfo.name;
856 + await token.save();
857 + _upsertCachedToken(token);
858
859 if (token.enabled) {
860 final tokenBalance = await _client.getSplTokenBalance(token, solanaAddress) ??
@@ -869,13 +874,10 @@ abstract class SolanaWalletBase
874 sources.add(_nativeSource);
875 }
876
872 - if (splTokensBox.isOpen) {
873 - sources.addAll(splTokensBox.values
874 - .where((t) => t.symbol == token.symbol)
875 - .map((t) => t.mintAddress));
877 + sources.addAll(_splTokens.where((t) => t.symbol == token.symbol).map((t) => t.mintAddress));
878
877 - await splTokensBox.delete(token.mintAddress);
878 - }
879 + await SPLToken.deleteForWallet(walletInfo.name, token.mintAddress);
880 + _splTokens.removeWhere((t) => t.mintAddress == token.mintAddress);
881
882 balance.remove(token);
883 await _removeTokenTransactionsInHistory(token);
cw_solana/lib/solana_wallet_service.dart
+11 -5
@@ -4,6 +4,7 @@ import 'package:bip39/bip39.dart' as bip39;
4 import 'package:cw_core/encryption_file_utils.dart';
5 import 'package:cw_core/balance.dart';
6 import 'package:cw_core/pathForWallet.dart';
7 +import 'package:cw_core/spl_token.dart';
8 import 'package:cw_core/transaction_history.dart';
9 import 'package:cw_core/transaction_info.dart';
10 import 'package:cw_core/wallet_base.dart';
@@ -40,7 +41,7 @@ class SolanaWalletService extends WalletService<
41 );
42
43 await wallet.init();
43 - wallet.addInitialTokens();
44 + await wallet.addInitialTokens();
45 await wallet.save();
46 return wallet;
47 }
@@ -68,7 +69,7 @@ class SolanaWalletService extends WalletService<
69 );
70
71 await wallet.init();
71 - wallet.addInitialTokens();
72 + await wallet.addInitialTokens();
73 await wallet.save();
74 saveBackup(name);
75 return wallet;
@@ -83,7 +84,7 @@ class SolanaWalletService extends WalletService<
84 );
85
86 await wallet.init();
86 - wallet.addInitialTokens();
87 + await wallet.addInitialTokens();
88 await wallet.save();
89 return wallet;
90 }
@@ -97,6 +98,11 @@ class SolanaWalletService extends WalletService<
98 throw Exception('Wallet not found');
99 }
100 await WalletInfo.delete(walletInfo);
101 + final nameStillUsed = await WalletInfo.get(wallet, getType()) != null;
102 + if (!nameStillUsed) {
103 + await SPLToken.deleteAllForWallet(wallet);
104 + }
105 +
106 final prefs = await SharedPreferences.getInstance();
107 for (final key in prefs.getKeys().where(
108 (k) => k.startsWith('solana_last_synced_signature_${wallet}_'))) {
@@ -116,7 +122,7 @@ class SolanaWalletService extends WalletService<
122 );
123
124 await wallet.init();
119 - wallet.addInitialTokens();
125 + await wallet.addInitialTokens();
126 await wallet.save();
127
128 return wallet;
@@ -139,7 +145,7 @@ class SolanaWalletService extends WalletService<
145 );
146
147 await wallet.init();
142 - wallet.addInitialTokens();
148 + await wallet.addInitialTokens();
149 await wallet.save();
150
151 return wallet;
cw_tron/lib/tron_wallet.dart
+34 -26
@@ -5,7 +5,6 @@ import 'dart:developer';
5 import 'package:bip39/bip39.dart' as bip39;
6 import 'package:blockchain_utils/blockchain_utils.dart';
7 import 'package:cw_core/amount/money.dart';
8 -import 'package:cw_core/cake_hive.dart';
8 import 'package:cw_core/crypto_currency.dart';
9 import 'package:cw_core/encryption_file_utils.dart';
10 import 'package:cw_core/node.dart';
@@ -29,7 +28,6 @@ import 'package:cw_tron/tron_transaction_credentials.dart';
28 import 'package:cw_tron/tron_transaction_history.dart';
29 import 'package:cw_tron/tron_transaction_info.dart';
30 import 'package:cw_tron/tron_wallet_addresses.dart';
32 -import 'package:hive/hive.dart';
31 import 'package:mobx/mobx.dart';
32 import 'package:on_chain/on_chain.dart';
33
@@ -62,10 +60,6 @@ abstract class TronWalletBase
60 this.walletInfo = walletInfo;
61 transactionHistory = TronTransactionHistory(
62 walletInfo: walletInfo, password: password, encryptionFileUtils: encryptionFileUtils);
65 -
66 - if (!CakeHive.isAdapterRegistered(TronToken.typeId)) {
67 - CakeHive.registerAdapter(TronTokenAdapter());
68 - }
63 }
64
65 final String? _mnemonic;
@@ -73,7 +67,7 @@ abstract class TronWalletBase
67 final String _password;
68 final EncryptionFileUtils encryptionFileUtils;
69
76 - late final Box<TronToken> tronTokensBox;
70 + List<TronToken> _tronTokens = [];
71
72 late final TronPrivateKey _tronPrivateKey;
73
@@ -107,7 +101,7 @@ abstract class TronWalletBase
101 late ObservableMap<CryptoCurrency, TronBalance> balance;
102
103 Future<void> init() async {
110 - await initTronTokensBox();
104 + await initTronTokens();
105
106 await walletAddresses.init();
107 await transactionHistory.init();
@@ -179,25 +173,38 @@ abstract class TronWalletBase
173 );
174 }
175
182 - void addInitialTokens() {
176 + Future<void> addInitialTokens() async {
177 final initialTronTokens = DefaultTronTokens().initialTronTokens;
178
179 for (var token in initialTronTokens) {
186 - if (!tronTokensBox.containsKey(token.contractAddress)) {
187 - tronTokensBox.put(token.contractAddress, token);
188 - } else {
189 - // update existing token
190 - final existingToken = tronTokensBox.get(token.contractAddress);
191 - tronTokensBox.put(
192 - token.contractAddress, TronToken.copyWith(token, enabled: existingToken!.enabled));
193 - }
180 + final existingToken = _findCachedToken(token.contractAddress);
181 +
182 + final newToken = TronToken.copyWith(
183 + token,
184 + enabled: existingToken?.enabled ?? token.enabled,
185 + walletName: walletInfo.name,
186 + );
187 +
188 + await newToken.save();
189 + _upsertCachedToken(newToken);
190 }
191 }
192
197 - Future<void> initTronTokensBox() async {
198 - final boxName = "${walletInfo.name.replaceAll(" ", "_")}_${TronToken.boxName}";
193 + Future<void> initTronTokens() async {
194 + _tronTokens = await TronToken.getAllForWallet(walletInfo.name);
195 + }
196 +
197 + TronToken? _findCachedToken(String contractAddress) {
198 + for (final token in _tronTokens) {
199 + if (token.contractAddress == contractAddress) return token;
200 + }
201
200 - tronTokensBox = await CakeHive.openBox<TronToken>(boxName);
202 + return null;
203 + }
204 +
205 + void _upsertCachedToken(TronToken token) {
206 + _tronTokens.removeWhere((t) => t.contractAddress == token.contractAddress);
207 + _tronTokens.add(token);
208 }
209
210 String idFor(String name, WalletType type) => '${walletTypeToString(type).toLowerCase()}_$name';
@@ -516,7 +523,7 @@ abstract class TronWalletBase
523 }
524
525 Future<void> _fetchTronTokenBalances() async {
519 - for (var token in tronTokensBox.values) {
526 + for (var token in _tronTokens.toList()) {
527 try {
528 if (token.enabled) {
529 balance[token] = await _client.fetchTronTokenBalances(
@@ -551,7 +558,7 @@ abstract class TronWalletBase
558 }
559 }
560
554 - List<TronToken> get tronTokenCurrencies => tronTokensBox.values.toList();
561 + List<TronToken> get tronTokenCurrencies => _tronTokens.toList();
562
563 Future<void> addTronToken(TronToken token) async {
564 String? iconPath;
@@ -574,9 +581,11 @@ abstract class TronWalletBase
581 tag: token.tag ?? "TRX",
582 iconPath: iconPath,
583 isPotentialScam: token.isPotentialScam,
584 + walletName: walletInfo.name,
585 );
586
579 - await tronTokensBox.put(newToken.contractAddress, newToken);
587 + await newToken.save();
588 + _upsertCachedToken(newToken);
589
590 if (newToken.enabled) {
591 balance[newToken] = await _client
@@ -587,9 +596,8 @@ abstract class TronWalletBase
596 }
597
598 Future<void> deleteTronToken(TronToken token) async {
590 - if (tronTokensBox.isOpen) {
591 - await tronTokensBox.delete(token.contractAddress);
592 - }
599 + await TronToken.deleteForWallet(walletInfo.name, token.contractAddress);
600 + _tronTokens.removeWhere((t) => t.contractAddress == token.contractAddress);
601
602 balance.remove(token);
603 await _removeTokenTransactionsInHistory(token);
cw_tron/lib/tron_wallet_service.dart
+10 -5
@@ -6,6 +6,7 @@ import 'package:cw_core/encryption_file_utils.dart';
6 import 'package:cw_core/pathForWallet.dart';
7 import 'package:cw_core/transaction_history.dart';
8 import 'package:cw_core/transaction_info.dart';
9 +import 'package:cw_core/tron_token.dart';
10 import 'package:cw_core/wallet_base.dart';
11 import 'package:cw_core/wallet_info.dart';
12 import 'package:cw_core/wallet_service.dart';
@@ -45,7 +46,7 @@ class TronWalletService extends WalletService<
46 );
47
48 await wallet.init();
48 - wallet.addInitialTokens();
49 + await wallet.addInitialTokens();
50 await wallet.save();
51
52 return wallet;
@@ -67,7 +68,7 @@ class TronWalletService extends WalletService<
68 );
69
70 await wallet.init();
70 - wallet.addInitialTokens();
71 + await wallet.addInitialTokens();
72 await wallet.save();
73 saveBackup(name);
74 return wallet;
@@ -82,7 +83,7 @@ class TronWalletService extends WalletService<
83 );
84
85 await wallet.init();
85 - wallet.addInitialTokens();
86 + await wallet.addInitialTokens();
87 await wallet.save();
88 return wallet;
89 }
@@ -102,7 +103,7 @@ class TronWalletService extends WalletService<
103 );
104
105 await wallet.init();
105 - wallet.addInitialTokens();
106 + await wallet.addInitialTokens();
107 await wallet.save();
108
109 return wallet;
@@ -127,7 +128,7 @@ class TronWalletService extends WalletService<
128 );
129
130 await wallet.init();
130 - wallet.addInitialTokens();
131 + await wallet.addInitialTokens();
132 await wallet.save();
133
134 return wallet;
@@ -145,6 +146,10 @@ class TronWalletService extends WalletService<
146 throw Exception('Wallet not found');
147 }
148 await WalletInfo.delete(walletInfo);
149 + final nameStillUsed = await WalletInfo.get(wallet, getType()) != null;
150 + if (!nameStillUsed) {
151 + await TronToken.deleteAllForWallet(wallet);
152 + }
153 }
154
155 @override
lib/core/backup_service.dart
+7
@@ -18,6 +18,9 @@ import 'package:cake_wallet/core/key_service.dart';
18 import 'package:cake_wallet/entities/encrypt.dart';
19 import 'package:cake_wallet/entities/preferences_key.dart';
20 import 'package:cake_wallet/entities/secret_store_key.dart';
21 +import 'package:cw_core/erc20_token_legacy.dart' show performErc20TokenHiveMigration;
22 +import 'package:cw_core/spl_token_legacy.dart' show performSplTokenHiveMigration;
23 +import 'package:cw_core/tron_token_legacy.dart' show performTronTokenHiveMigration;
24 import 'package:cw_core/wallet_info.dart';
25 import 'package:cake_wallet/exchange/trade_legacy.dart';
26 import 'package:cake_wallet/.secrets.g.dart' as secrets;
@@ -113,6 +116,10 @@ class $BackupService {
116 Future<void> verifyWallets() async {
117 await performHiveMigration(); // for backups made before sqlite migration
118 await performTradeHiveMigration(_secureStorage);
119 + await performErc20TokenHiveMigration();
120 + await performSplTokenHiveMigration();
121 + await performTronTokenHiveMigration();
122 +
123 correctWallets = (await WalletInfo.getAll())
124 .where((info) => availableWalletTypes.contains(info.type))
125 .toList();
lib/entities/default_settings_migration.dart
+22 -39
@@ -27,7 +27,6 @@ import 'package:cw_core/wallet_type.dart';
27 import 'package:encrypt/encrypt.dart' as encrypt;
28 import 'package:hive/hive.dart';
29 import 'package:shared_preferences/shared_preferences.dart';
30 -import 'package:cw_core/cake_hive.dart';
30 import 'package:cw_core/erc20_token.dart';
31
32 const newCakeWalletMoneroUri = 'xmr-node.cakewallet.com:18081';
@@ -1270,22 +1269,22 @@ Future<void> _addXautTokenToExistingEthereumWallets() async {
1269
1270 final ethereumWallets =
1271 allWallets.where((wallet) => wallet.type == WalletType.ethereum).toList();
1272 + const ethereumChainId = 1;
1273
1274 for (final walletInfo in ethereumWallets) {
1275 - final sanitizedName = walletInfo.name.replaceAll(' ', '_');
1276 - final boxName = '${sanitizedName}_${Erc20Token.ethereumBoxName}';
1277 -
1278 - Box<Erc20Token> tokenBox;
1279 - if (CakeHive.isBoxOpen(boxName)) {
1280 - tokenBox = CakeHive.box<Erc20Token>(boxName);
1281 - } else {
1282 - tokenBox = await CakeHive.openBox<Erc20Token>(boxName);
1283 - }
1275 + final existingToken = await Erc20Token.getByContract(
1276 + walletInfo.name,
1277 + ethereumChainId,
1278 + xautToken.contractAddress,
1279 + );
1280
1285 - final xautAddress = xautToken.contractAddress;
1286 - if (!tokenBox.containsKey(xautAddress)) {
1287 - await tokenBox.put(xautAddress, xautToken);
1288 - }
1281 + if (existingToken != null) continue;
1282 +
1283 + await Erc20Token.copyWith(
1284 + xautToken,
1285 + walletName: walletInfo.name,
1286 + chainId: ethereumChainId,
1287 + ).save();
1288 }
1289 } catch (e) {
1290 printV('Error in XAUT migration: $e');
@@ -1309,20 +1308,11 @@ Future<void> _addXaut0TokenToExistingSolanaWallets() async {
1308 final solanaWallets = allWallets.where((wallet) => wallet.type == WalletType.solana).toList();
1309
1310 for (final walletInfo in solanaWallets) {
1312 - final sanitizedName = walletInfo.name.replaceAll(' ', '_');
1313 - final boxName = '${sanitizedName}_${SPLToken.boxName}';
1314 -
1315 - Box<SPLToken> tokenBox;
1316 - if (CakeHive.isBoxOpen(boxName)) {
1317 - tokenBox = CakeHive.box<SPLToken>(boxName);
1318 - } else {
1319 - tokenBox = await CakeHive.openBox<SPLToken>(boxName);
1320 - }
1311 + final existingToken = await SPLToken.getByMint(walletInfo.name, xaut0Token.mintAddress);
1312
1322 - final xaut0Address = xaut0Token.mintAddress;
1323 - if (!tokenBox.containsKey(xaut0Address)) {
1324 - await tokenBox.put(xaut0Address, xaut0Token);
1325 - }
1313 + if (existingToken != null) continue;
1314 +
1315 + await SPLToken.copyWith(xaut0Token, walletName: walletInfo.name).save();
1316 }
1317 } catch (e) {
1318 printV('Error in XAUT0 migration: $e');
@@ -1346,20 +1336,13 @@ Future<void> _addTbbTokenToExistingSolanaWallets() async {
1336 final solanaWallets = allWallets.where((wallet) => wallet.type == WalletType.solana).toList();
1337
1338 for (final walletInfo in solanaWallets) {
1349 - final sanitizedName = walletInfo.name.replaceAll(" ", "_");
1350 - final boxName = "${sanitizedName}_${SPLToken.boxName}";
1351 -
1352 - Box<SPLToken> tokenBox;
1353 - if (CakeHive.isBoxOpen(boxName)) {
1354 - tokenBox = CakeHive.box<SPLToken>(boxName);
1355 - } else {
1356 - tokenBox = await CakeHive.openBox<SPLToken>(boxName);
1357 - }
1339 + final existingToken = await SPLToken.getByMint(walletInfo.name, tbbToken.mintAddress);
1340
1359 - final tbbAddress = tbbToken.mintAddress;
1360 - if (!tokenBox.containsKey(tbbAddress)) {
1361 - await tokenBox.put(tbbAddress, tbbToken);
1341 + if (existingToken != null) {
1342 + continue;
1343 }
1344 +
1345 + await SPLToken.copyWith(tbbToken, walletName: walletInfo.name).save();
1346 }
1347 } catch (e) {
1348 printV("Error in TBB migration: $e");
lib/main.dart
+6 -14
@@ -43,7 +43,7 @@ import 'package:cake_wallet/zcash/zcash.dart';
43 import 'package:cw_core/address_info.dart';
44 import 'package:cw_core/cake_hive.dart';
45 import 'package:cw_core/db/sqlite.dart';
46 -import 'package:cw_core/erc20_token.dart';
46 +import 'package:cw_core/erc20_token_legacy.dart' show performErc20TokenHiveMigration;
47 import 'package:cw_core/hive_type_ids.dart';
48 import 'package:cw_core/key.dart';
49 import 'package:cw_core/mweb_utxo.dart';
@@ -51,8 +51,8 @@ import 'package:cw_core/node.dart';
51 import 'package:cw_core/node_legacy.dart' show performNodeHiveMigration;
52 import 'package:cw_core/payjoin_session.dart';
53 import 'package:cw_core/root_dir.dart';
54 -import 'package:cw_core/spl_token.dart';
55 -import 'package:cw_core/tron_token.dart';
54 +import 'package:cw_core/spl_token_legacy.dart' show performSplTokenHiveMigration;
55 +import 'package:cw_core/tron_token_legacy.dart' show performTronTokenHiveMigration;
56 import 'package:cw_core/unspent_coins_info.dart';
57 import 'package:cw_core/utils/print_verbose.dart';
58 import 'package:cw_core/utils/proxy_logger/memory_proxy_logger.dart';
@@ -265,18 +265,10 @@ Future<void> initializeAppConfigs({bool loadWallet = true}) async {
265 CakeHive.registerAdapter(PayjoinSessionAdapter());
266 }
267
268 - if (!CakeHive.isAdapterRegistered(Erc20Token.typeId)) {
269 - CakeHive.registerAdapter(Erc20TokenAdapter());
270 - }
271 -
272 - if (!CakeHive.isAdapterRegistered(SPLToken.typeId)) {
273 - CakeHive.registerAdapter(SPLTokenAdapter());
274 - }
275 -
276 - if (!CakeHive.isAdapterRegistered(TronToken.typeId)) {
277 - CakeHive.registerAdapter(TronTokenAdapter());
278 - }
268 await performHiveMigration();
269 + await performErc20TokenHiveMigration();
270 + await performSplTokenHiveMigration();
271 + await performTronTokenHiveMigration();
272
273 final secureStorage = secureStorageShared;
274 final transactionDescriptionsBoxKey =
lib/utils/token_utilities.dart
+96 -96
@@ -26,10 +26,10 @@ class TokenUtilities {
26
27 for (final wallet in evmWallets) {
28 final chain = getTokenNameBasedOnWalletType(wallet.type);
29 - final box = await _openEvmTokensBoxFor(wallet);
29 + final tokens = await Erc20Token.getAllForWallet(wallet.name, _getDefaultChainId(wallet.type));
30
31 - for (final t in box.values.where((t) => t.enabled)) {
32 - final key = '$chain|${t.contractAddress.toLowerCase()}';
31 + for (final t in tokens.where((t) => t.enabled)) {
32 + final key = "$chain|${t.contractAddress.toLowerCase()}";
33 if (seen.add(key)) {
34 unique.add(t);
35 }
@@ -47,15 +47,17 @@ class TokenUtilities {
47
48 final tokens = <SPLToken>[];
49 for (final wallet in solWallets) {
50 - final box = await _openSolTokensBoxFor(wallet);
51 - tokens.addAll(box.values.where((t) => t.enabled));
50 + final walletTokens = await SPLToken.getAllForWallet(wallet.name);
51 + tokens.addAll(walletTokens.where((t) => t.enabled));
52 }
53
54 final seen = <String>{};
55 final unique = <SPLToken>[];
56 for (final token in tokens) {
57 final key = token.mintAddress.toLowerCase();
58 - if (seen.add(key)) unique.add(token);
58 + if (seen.add(key)) {
59 + unique.add(token);
60 + }
61 }
62 return unique;
63 }
@@ -69,25 +71,31 @@ class TokenUtilities {
71 final seen = <String>{};
72 final unique = <TronToken>[];
73 for (final wallet in tronWallets) {
72 - final box = await _openTronTokensBoxFor(wallet);
73 - for (final t in box.values.where((t) => t.enabled)) {
74 + final walletTokens = await TronToken.getAllForWallet(wallet.name);
75 + for (final t in walletTokens.where((t) => t.enabled)) {
76 final key = t.contractAddress.toLowerCase();
75 - if (seen.add(key)) unique.add(t);
77 + if (seen.add(key)) {
78 + unique.add(t);
79 + }
80 }
81 }
82 return unique;
83 }
84
85 static List<Erc20Token> loadDefaultEvmTokensForSwap() {
82 - if (evm == null) return [];
86 + if (evm == null) {
87 + return [];
88 + }
89
90 final tokens = <Erc20Token>[];
91 final seen = <String>{};
92
93 for (final chain in evm!.getAllChains()) {
94 for (final token in evm!.getDefaultTokensByChainId(chain.chainId)) {
89 - final key = '${chain.chainId}|${token.contractAddress.toLowerCase()}';
90 - if (seen.add(key)) tokens.add(token);
95 + final key = "${chain.chainId}|${token.contractAddress.toLowerCase()}";
96 + if (seen.add(key)) {
97 + tokens.add(token);
98 + }
99 }
100 }
101
@@ -109,7 +117,9 @@ class TokenUtilities {
117
118 for (final t in [...defaultTokens, ...userTokens]) {
119 final key = '${t.tag ?? 'ETH'}|${t.contractAddress.toLowerCase()}';
112 - if (seen.add(key)) result.add(t);
120 + if (seen.add(key)) {
121 + result.add(t);
122 + }
123 }
124
125 return result;
@@ -124,7 +134,9 @@ class TokenUtilities {
134
135 for (final t in [...defaultTokens, ...userTokens]) {
136 final key = t.mintAddress.toLowerCase();
127 - if (seen.add(key)) result.add(t);
137 + if (seen.add(key)) {
138 + result.add(t);
139 + }
140 }
141
142 return result;
@@ -139,7 +151,9 @@ class TokenUtilities {
151
152 for (final t in [...defaultTokens, ...userTokens]) {
153 final key = t.contractAddress.toLowerCase();
142 - if (seen.add(key)) result.add(t);
154 + if (seen.add(key)) {
155 + result.add(t);
156 + }
157 }
158
159 return result;
@@ -153,17 +167,34 @@ class TokenUtilities {
167 required WalletType walletType,
168 required String address,
169 }) async {
156 - if (address.isEmpty) return null;
170 + if (address.isEmpty) {
171 + return null;
172 + }
173 final lower = address.toLowerCase();
174 final tokens = await getAvailableTokensForNetwork(walletType);
175 for (final t in tokens) {
160 - if (t is Erc20Token && t.contractAddress.toLowerCase() == lower) return t;
161 - if (t is SPLToken && t.mintAddress.toLowerCase() == lower) return t;
162 - if (t is TronToken && t.contractAddress.toLowerCase() == lower) return t;
176 + if (t is Erc20Token && t.contractAddress.toLowerCase() == lower) {
177 + return t;
178 + }
179 + if (t is SPLToken && t.mintAddress.toLowerCase() == lower) {
180 + return t;
181 + }
182 + if (t is TronToken && t.contractAddress.toLowerCase() == lower) {
183 + return t;
184 + }
185 }
186 return null;
187 }
188
189 + static int _getDefaultChainId(WalletType walletType) => switch (walletType) {
190 + WalletType.ethereum => 1,
191 + WalletType.polygon => 137,
192 + WalletType.base => 8453,
193 + WalletType.arbitrum => 42161,
194 + WalletType.bsc => 56,
195 + _ => 1,
196 + };
197 +
198 static Future<int?> findEvmChainIdForContract(
199 String contractAddress, {
200 int? excludingChainId,
@@ -195,45 +226,10 @@ class TokenUtilities {
226 return null;
227 }
228
198 - static Future<Box<Erc20Token>> _openEvmTokensBoxFor(WalletInfo walletInfo) async {
199 - final walletKey = walletInfo.name.replaceAll(' ', '_');
200 - final boxName = _getErc20TokensBoxName(walletKey, walletInfo.type);
201 -
202 - if (CakeHive.isBoxOpen(boxName)) {
203 - return CakeHive.box<Erc20Token>(boxName);
204 - }
205 - return CakeHive.openBox<Erc20Token>(boxName);
206 - }
207 -
208 - static String _getErc20TokensBoxName(String walletKey, WalletType walletType) {
209 - return switch (walletType) {
210 - WalletType.ethereum => '${walletKey}_${Erc20Token.ethereumBoxName}',
211 - WalletType.polygon => '${walletKey}_${Erc20Token.polygonBoxName}',
212 - WalletType.base => '${walletKey}_${Erc20Token.baseBoxName}',
213 - WalletType.arbitrum => '${walletKey}_${Erc20Token.arbitrumBoxName}',
214 - WalletType.bsc => '${walletKey}_${Erc20Token.bscBoxName}',
215 - _ => '${walletKey}_${Erc20Token.ethereumBoxName}',
216 - };
217 - }
218 -
219 - static Future<Box<SPLToken>> _openSolTokensBoxFor(WalletInfo wallet) async {
220 - final boxName = '${wallet.name.replaceAll(' ', '_')}_${SPLToken.boxName}';
221 - if (CakeHive.isBoxOpen(boxName)) {
222 - return CakeHive.box<SPLToken>(boxName);
223 - }
224 - return CakeHive.openBox<SPLToken>(boxName);
225 - }
226 -
227 - static Future<Box<TronToken>> _openTronTokensBoxFor(WalletInfo walletInfo) async {
228 - final boxName = '${walletInfo.name.replaceAll(' ', '_')}_${TronToken.boxName}';
229 - if (CakeHive.isBoxOpen(boxName)) {
230 - return CakeHive.box<TronToken>(boxName);
231 - }
232 - return CakeHive.openBox<TronToken>(boxName);
233 - }
234 -
229 static Erc20Token? findErc20Token(CryptoCurrency currency, WalletBase wallet) {
236 - if (currency is Erc20Token) return currency;
230 + if (currency is Erc20Token) {
231 + return currency;
232 + }
233
234 // More of a fallback for us
235 for (final balanceCurrency in wallet.balance.keys) {
@@ -246,10 +242,14 @@ class TokenUtilities {
242 }
243
244 static Erc20Token? findErc20TokenForSwap(CryptoCurrency currency) {
249 - if (currency is Erc20Token) return currency;
245 + if (currency is Erc20Token) {
246 + return currency;
247 + }
248
249 for (final token in loadDefaultEvmTokensForSwap()) {
252 - if (_matchesCurrency(token, currency)) return token;
250 + if (_matchesCurrency(token, currency)) {
251 + return token;
252 + }
253 }
254 return null;
255 }
@@ -276,19 +276,19 @@ class TokenUtilities {
276 final title = currency.title.toLowerCase();
277 final tag = currency.tag?.toLowerCase();
278
279 - return title == 'eth' ||
280 - title == 'ethereum' ||
281 - title == 'matic' ||
282 - title == 'polygon' ||
283 - title == 'base' ||
284 - title == 'arbitrum' ||
285 - title == 'bnb' ||
286 - title == 'bsc' ||
287 - title == 'avax' ||
288 - title == 'avalanche' ||
289 - tag == 'polygon' ||
290 - tag == 'bsc' ||
291 - tag == 'avalanche';
279 + return title == "eth" ||
280 + title == "ethereum" ||
281 + title == "matic" ||
282 + title == "polygon" ||
283 + title == "base" ||
284 + title == "arbitrum" ||
285 + title == "bnb" ||
286 + title == "bsc" ||
287 + title == "avax" ||
288 + title == "avalanche" ||
289 + tag == "polygon" ||
290 + tag == "bsc" ||
291 + tag == "avalanche";
292 }
293
294 static int getChainId(CryptoCurrency currency) {
@@ -298,37 +298,37 @@ class TokenUtilities {
298 // Only check EVM registry for currencies that might be EVM-related
299 final isPotentialEVM = isNativeToken(currency) ||
300 (tag != null &&
301 - (tag == 'ETH' || tag == 'POL' || tag == 'BASE' || tag == 'ARB' || tag == 'BSC'));
301 + (tag == "ETH" || tag == "POL" || tag == "BASE" || tag == "ARB" || tag == "BSC"));
302
303 if (isPotentialEVM) {
304 // Try by tag first if available (e.g., 'POL', 'BASE', 'ARB')
305 if (tag != null) {
306 final chainId = evm?.getChainIdByTag(tag);
307 - if (chainId != null) return chainId;
307 + if (chainId != null) {
308 + return chainId;
309 + }
310 }
311
312 // Try by title (case-insensitive)
313 final titleChainId = evm?.getChainIdByTitle(title);
312 - if (titleChainId != null) return titleChainId;
314 + if (titleChainId != null) {
315 + return titleChainId;
316 + }
317 }
318
319 // Fallback to hardcoded values for chains not in registry yet
316 - // Avalanche C-Chain
317 - if (title == 'avalanche' || title == 'avax' || tag == 'AVALANCHE') {
320 + if (title == "avalanche" || title == "avax" || tag == "AVALANCHE") {
321 return 43114;
322 }
323
321 - // Optimism
322 - if (title == 'optimism' || title == 'op' || tag == 'OPTIMISM') {
324 + if (title == "optimism" || title == "op" || tag == "OPTIMISM") {
325 return 10;
326 }
327
326 - // Fantom Opera
327 - if (title == 'fantom' || title == 'ftm' || tag == 'FANTOM') {
328 + if (title == "fantom" || title == "ftm" || tag == "FANTOM") {
329 return 250;
330 }
331
331 - // Default to Ethereum mainnet
332 return 1;
333 }
334
@@ -395,7 +395,7 @@ class TokenUtilities {
395 }
396
397 for (final currency in CryptoCurrency.all) {
398 - if (currency.tag?.toLowerCase() == 'sol') {
398 + if (currency.tag?.toLowerCase() == "sol") {
399 if (currency is SPLToken) {
400 final mintAddress = currency.mintAddress.toLowerCase();
401 if (addedAddresses.add(mintAddress)) {
@@ -406,10 +406,7 @@ class TokenUtilities {
406 }
407 }
408 }
409 - }
410 -
411 - // Handle Tron network
412 - else if (network == WalletType.tron) {
409 + } else if (network == WalletType.tron) {
410 final userTronTokens = await loadAllUniqueTronTokens();
411 for (final token in userTronTokens) {
412 final contractAddress = token.contractAddress.toLowerCase();
@@ -426,7 +423,7 @@ class TokenUtilities {
423 }
424
425 for (final currency in CryptoCurrency.all) {
429 - if (currency.tag?.toLowerCase() == 'trx') {
426 + if (currency.tag?.toLowerCase() == "trx") {
427 if (currency is TronToken) {
428 final contractAddress = currency.contractAddress.toLowerCase();
429 if (addedAddresses.add(contractAddress)) {
@@ -442,31 +439,34 @@ class TokenUtilities {
439 return allTokens;
440 }
441
445 - static bool _matchesCurrency(CryptoCurrency a, CryptoCurrency b) {
446 - return a.title.toUpperCase() == b.title.toUpperCase() &&
447 - (a.tag?.toUpperCase() == b.tag?.toUpperCase());
448 - }
442 + static bool _matchesCurrency(CryptoCurrency a, CryptoCurrency b) =>
443 + a.title.toUpperCase() == b.title.toUpperCase() &&
444 + (a.tag?.toUpperCase() == b.tag?.toUpperCase());
445
446 static Future<List<CryptoCurrency>> _getUserTokensForNetwork(CryptoCurrency baseCurrency) async {
447 final walletType = cryptoCurrencyOrTokenToWalletType(baseCurrency);
452 - if (walletType == null) return [];
448 + if (walletType == null) {
449 + return [];
450 + }
451
452 if (isEVMCompatibleChain(walletType)) {
453 final tokens = await TokenUtilities.loadAllUniqueEvmTokens();
454
455 return tokens.where((token) {
458 - if (baseCurrency.tag == null) return token.tag == baseCurrency.title;
456 + if (baseCurrency.tag == null) {
457 + return token.tag == baseCurrency.title;
458 + }
459
460 return token.tag?.toLowerCase() == baseCurrency.tag?.toLowerCase();
461 }).toList();
462 }
463
464 if (walletType == WalletType.solana) {
465 - return await loadAllUniqueSolTokens();
465 + return loadAllUniqueSolTokens();
466 }
467
468 if (walletType == WalletType.tron) {
469 - return await loadAllUniqueTronTokens();
469 + return loadAllUniqueTronTokens();
470 }
471
472 return [];