mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-27 06:18:50 +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);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user