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