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 [];