mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-26 06:08:40 +01:00
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:
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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) =>
|
||||
|
||||
@@ -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
@@ -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) =>
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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 =
|
||||
|
||||
@@ -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 [];
|
||||
|
||||
Reference in New Issue
Block a user