stack_wallet/lib/db/db_version_migration.dart
2024-11-26 09:18:35 -06:00

737 lines
24 KiB
Dart

/*
* This file is part of Stack Wallet.
*
* Copyright (c) 2023 Cypher Stack
* All Rights Reserved.
* The code is distributed under GPLv3 license, see LICENSE file for details.
* Generated by Cypher Stack on 2023-05-26
*
*/
import 'package:isar/isar.dart';
import 'package:tuple/tuple.dart';
import '../app_config.dart';
import '../electrumx_rpc/electrumx_client.dart';
import '../models/contact.dart';
import '../models/exchange/change_now/exchange_transaction.dart';
import '../models/exchange/response_objects/trade.dart';
import '../models/isar/models/blockchain_data/address.dart';
import '../models/isar/models/contact_entry.dart' as isar_contact;
import '../models/isar/models/isar_models.dart' as isar_models;
import '../models/models.dart';
import '../models/node_model.dart';
import '../services/mixins/wallet_db.dart';
import '../services/node_service.dart';
import '../services/wallets_service.dart';
import '../utilities/amount/amount.dart';
import '../utilities/constants.dart';
import '../utilities/flutter_secure_storage_interface.dart';
import '../utilities/logger.dart';
import '../utilities/prefs.dart';
import '../wallets/crypto_currency/crypto_currency.dart';
import 'hive/db.dart';
import 'isar/main_db.dart';
import 'migrate_wallets_to_isar.dart';
class DbVersionMigrator with WalletDB {
Future<void> migrate(
int fromVersion, {
required SecureStorageInterface secureStore,
}) async {
if (AppConfig.appName == "Campfire" && fromVersion < 12) {
// safe to skip to v11 for campfire
fromVersion = 11;
}
Logging.instance.log(
"Running migrate fromVersion $fromVersion",
level: LogLevel.Warning,
);
switch (fromVersion) {
case 0:
await DB.instance.hive.openBox<dynamic>(DB.boxNameAllWalletsData);
await DB.instance.hive.openBox<dynamic>(DB.boxNamePrefs);
final walletsService = WalletsService();
final nodeService = NodeService(secureStorageInterface: secureStore);
final prefs = Prefs.instance;
final walletInfoList = await walletsService.walletNames;
await prefs.init();
ElectrumXClient? client;
int? latestSetId;
final firo = Firo(CryptoCurrencyNetwork.main);
// only instantiate client if there are firo wallets
if (walletInfoList.values
.any((element) => element.coinIdentifier == firo.identifier)) {
await DB.instance.hive.openBox<NodeModel>(DB.boxNameNodeModels);
await DB.instance.hive.openBox<NodeModel>(DB.boxNamePrimaryNodes);
final node =
nodeService.getPrimaryNodeFor(currency: firo) ?? firo.defaultNode;
final List<ElectrumXNode> failovers = nodeService
.failoverNodesFor(currency: firo)
.map(
(e) => ElectrumXNode(
address: e.host,
port: e.port,
name: e.name,
id: e.id,
useSSL: e.useSSL,
torEnabled: e.torEnabled,
clearnetEnabled: e.clearnetEnabled,
),
)
.toList();
client = ElectrumXClient.from(
node: ElectrumXNode(
address: node.host,
port: node.port,
name: node.name,
id: node.id,
useSSL: node.useSSL,
torEnabled: node.torEnabled,
clearnetEnabled: node.clearnetEnabled,
),
prefs: prefs,
failovers: failovers,
cryptoCurrency: Firo(CryptoCurrencyNetwork.main),
);
try {
latestSetId = await client.getLelantusLatestCoinId();
} catch (e) {
// default to 2 for now
latestSetId = 2;
Logging.instance.log(
"Failed to fetch latest coin id during firo db migrate: $e \nUsing a default value of 2",
level: LogLevel.Warning,
);
}
}
for (final walletInfo in walletInfoList.values) {
// migrate each firo wallet's lelantus coins
if (walletInfo.coinIdentifier == firo.identifier) {
await DB.instance.hive.openBox<dynamic>(walletInfo.walletId);
final _lelantusCoins = DB.instance.get<dynamic>(
boxName: walletInfo.walletId,
key: '_lelantus_coins',
) as List?;
final List<Map<dynamic, LelantusCoin>> lelantusCoins = [];
for (final lCoin in _lelantusCoins ?? []) {
lelantusCoins
.add({lCoin.keys.first: lCoin.values.first as LelantusCoin});
}
final List<Map<dynamic, LelantusCoin>> coins = [];
for (final element in lelantusCoins) {
final LelantusCoin coin = element.values.first;
int anonSetId = coin.anonymitySetId;
if (coin.anonymitySetId == 1 &&
(coin.publicCoin == '' ||
coin.publicCoin == "jmintData.publicCoin")) {
anonSetId = latestSetId!;
}
coins.add({
element.keys.first: LelantusCoin(
coin.index,
coin.value,
coin.publicCoin,
coin.txId,
anonSetId,
coin.isUsed,
),
});
}
Logger.print("newcoins $coins", normalLength: false);
await DB.instance.put<dynamic>(
boxName: walletInfo.walletId,
key: '_lelantus_coins',
value: coins,
);
}
}
// update version
await DB.instance.put<dynamic>(
boxName: DB.boxNameDBInfo,
key: "hive_data_version",
value: 1,
);
// try to continue migrating
return await migrate(1, secureStore: secureStore);
case 1:
await DB.instance.hive.openBox<ExchangeTransaction>(DB.boxNameTrades);
await DB.instance.hive.openBox<Trade>(DB.boxNameTradesV2);
final trades =
DB.instance.values<ExchangeTransaction>(boxName: DB.boxNameTrades);
for (final old in trades) {
if (old.statusObject != null) {
final trade = Trade.fromExchangeTransaction(old, false);
await DB.instance.put<Trade>(
boxName: DB.boxNameTradesV2,
key: trade.uuid,
value: trade,
);
}
}
// update version
await DB.instance.put<dynamic>(
boxName: DB.boxNameDBInfo,
key: "hive_data_version",
value: 2,
);
// try to continue migrating
return await migrate(2, secureStore: secureStore);
case 2:
await DB.instance.hive.openBox<dynamic>(DB.boxNamePrefs);
final prefs = Prefs.instance;
await prefs.init();
if (!(await prefs.isExternalCallsSet())) {
prefs.externalCalls = true;
}
// update version
await DB.instance.put<dynamic>(
boxName: DB.boxNameDBInfo,
key: "hive_data_version",
value: 3,
);
return await migrate(3, secureStore: secureStore);
case 3:
// clear possible broken firo cache
await DB.instance.clearSharedTransactionCache(
currency: Firo(
CryptoCurrencyNetwork.test,
),
);
// update version
await DB.instance.put<dynamic>(
boxName: DB.boxNameDBInfo,
key: "hive_data_version",
value: 4,
);
// try to continue migrating
return await migrate(4, secureStore: secureStore);
case 4:
// migrate
await _v4(secureStore);
// update version
await DB.instance.put<dynamic>(
boxName: DB.boxNameDBInfo,
key: "hive_data_version",
value: 5,
);
// try to continue migrating
return await migrate(5, secureStore: secureStore);
case 5:
// migrate
await DB.instance.hive.openBox<dynamic>("theme");
await DB.instance.hive.openBox<dynamic>(DB.boxNamePrefs);
final themeName =
DB.instance.get<dynamic>(boxName: "theme", key: "colorScheme")
as String? ??
"light";
await DB.instance.put<dynamic>(
boxName: DB.boxNamePrefs,
key: "theme",
value: themeName,
);
// update version
await DB.instance.put<dynamic>(
boxName: DB.boxNameDBInfo,
key: "hive_data_version",
value: 6,
);
// try to continue migrating
return await migrate(6, secureStore: secureStore);
case 6:
// migrate
await MainDB.instance.initMainDB();
final count = await MainDB.instance.isar.addresses.count();
// add change/receiving tags to address labels
for (var i = 0; i < count; i += 50) {
final addresses = await MainDB.instance.isar.addresses
.where()
.offset(i)
.limit(50)
.findAll();
final List<isar_models.AddressLabel> labels = [];
for (final address in addresses) {
List<String>? tags;
switch (address.subType) {
case AddressSubType.receiving:
tags = ["receiving"];
break;
case AddressSubType.change:
tags = ["change"];
break;
case AddressSubType.paynymNotification:
tags = ["paynym notification"];
break;
case AddressSubType.paynymSend:
break;
case AddressSubType.paynymReceive:
tags = ["paynym receiving"];
break;
case AddressSubType.unknown:
break;
case AddressSubType.nonWallet:
break;
}
// update/create label if tags is not empty
if (tags != null) {
isar_models.AddressLabel? label = await MainDB
.instance.isar.addressLabels
.where()
.addressStringWalletIdEqualTo(address.value, address.walletId)
.findFirst();
if (label == null) {
label = isar_models.AddressLabel(
walletId: address.walletId,
value: "",
addressString: address.value,
tags: tags,
);
} else if (label.tags == null) {
label = label.copyWith(tags: tags);
}
labels.add(label);
}
}
if (labels.isNotEmpty) {
await MainDB.instance.isar.writeTxn(() async {
await MainDB.instance.isar.addressLabels.putAll(labels);
});
}
}
// update version
await DB.instance.put<dynamic>(
boxName: DB.boxNameDBInfo,
key: "hive_data_version",
value: 7,
);
// try to continue migrating
return await migrate(7, secureStore: secureStore);
case 7:
// migrate
await _v7(secureStore);
// update version
await DB.instance.put<dynamic>(
boxName: DB.boxNameDBInfo,
key: "hive_data_version",
value: 8,
);
// try to continue migrating
return await migrate(8, secureStore: secureStore);
case 8:
// migrate
await DB.instance.hive.openBox<dynamic>(DB.boxNameAllWalletsData);
final walletsService = WalletsService();
final walletInfoList = await walletsService.walletNames;
await MainDB.instance.initMainDB();
for (final walletId in walletInfoList.keys) {
final info = walletInfoList[walletId]!;
if (info.coinIdentifier ==
Bitcoincash(CryptoCurrencyNetwork.main).identifier ||
info.coinIdentifier ==
Bitcoincash(CryptoCurrencyNetwork.test).identifier) {
final ids = await MainDB.instance
.getAddresses(walletId)
.filter()
.typeEqualTo(isar_models.AddressType.p2sh)
.idProperty()
.findAll();
await MainDB.instance.isar.writeTxn(() async {
await MainDB.instance.isar.addresses.deleteAll(ids);
});
}
}
// update version
await DB.instance.put<dynamic>(
boxName: DB.boxNameDBInfo,
key: "hive_data_version",
value: 9,
);
// try to continue migrating
return await migrate(9, secureStore: secureStore);
case 9:
// migrate
await _v9();
// update version
await DB.instance.put<dynamic>(
boxName: DB.boxNameDBInfo,
key: "hive_data_version",
value: 10,
);
// try to continue migrating
return await migrate(10, secureStore: secureStore);
case 10:
// migrate
await _v10(secureStore);
// update version
await DB.instance.put<dynamic>(
boxName: DB.boxNameDBInfo,
key: "hive_data_version",
value: 11,
);
// try to continue migrating
return await migrate(11, secureStore: secureStore);
case 11:
// migrate
await _v11(secureStore);
// update version
await DB.instance.put<dynamic>(
boxName: DB.boxNameDBInfo,
key: "hive_data_version",
value: 12,
);
// try to continue migrating
return await migrate(12, secureStore: secureStore);
case 12:
// migrate
await _v12(secureStore);
// update version
await DB.instance.put<dynamic>(
boxName: DB.boxNameDBInfo,
key: "hive_data_version",
value: 13,
);
// try to continue migrating
return await migrate(13, secureStore: secureStore);
default:
// finally return
return;
}
}
Future<void> _v4(SecureStorageInterface secureStore) async {
await DB.instance.hive.openBox<dynamic>(DB.boxNameAllWalletsData);
await DB.instance.hive.openBox<dynamic>(DB.boxNamePrefs);
final walletsService = WalletsService();
final prefs = Prefs.instance;
final walletInfoList = await walletsService.walletNames;
await prefs.init();
await MainDB.instance.initMainDB();
for (final walletId in walletInfoList.keys) {
final info = walletInfoList[walletId]!;
assert(info.walletId == walletId);
final walletBox = await DB.instance.hive.openBox<dynamic>(info.walletId);
const receiveAddressesPrefix = "receivingAddresses";
const changeAddressesPrefix = "changeAddresses";
// we need to manually migrate epic cash transactions as they are not
// stored on the epic cash blockchain
final epic = Epiccash(CryptoCurrencyNetwork.main);
if (info.coinIdentifier == epic.identifier) {
final txnData = walletBox.get("latest_tx_model") as TransactionData?;
// we ever only used index 0 in the past
const rcvIndex = 0;
final List<Tuple2<isar_models.Transaction, isar_models.Address?>>
transactionsData = [];
if (txnData != null) {
final txns = txnData.getAllTransactions();
for (final tx in txns.values) {
final bool isIncoming = tx.txType == "Received";
final iTx = isar_models.Transaction(
walletId: walletId,
txid: tx.txid,
timestamp: tx.timestamp,
type: isIncoming
? isar_models.TransactionType.incoming
: isar_models.TransactionType.outgoing,
subType: isar_models.TransactionSubType.none,
amount: tx.amount,
amountString: Amount(
rawValue: BigInt.from(tx.amount),
fractionDigits: epic.fractionDigits,
).toJsonString(),
fee: tx.fees,
height: tx.height,
isCancelled: tx.isCancelled,
isLelantus: false,
slateId: tx.slateId,
otherData: tx.otherData,
nonce: null,
inputs: [],
outputs: [],
numberOfMessages: tx.numberOfMessages,
);
if (tx.address.isEmpty) {
transactionsData.add(Tuple2(iTx, null));
} else {
final address = isar_models.Address(
walletId: walletId,
value: tx.address,
publicKey: [],
derivationIndex: isIncoming ? rcvIndex : -1,
derivationPath: null,
type: isIncoming
? isar_models.AddressType.mimbleWimble
: isar_models.AddressType.unknown,
subType: isIncoming
? isar_models.AddressSubType.receiving
: isar_models.AddressSubType.unknown,
);
transactionsData.add(Tuple2(iTx, address));
}
}
}
await MainDB.instance.addNewTransactionData(transactionsData, walletId);
}
// delete data from hive
await walletBox.delete(receiveAddressesPrefix);
await walletBox.delete("${receiveAddressesPrefix}P2PKH");
await walletBox.delete("${receiveAddressesPrefix}P2SH");
await walletBox.delete("${receiveAddressesPrefix}P2WPKH");
await walletBox.delete(changeAddressesPrefix);
await walletBox.delete("${changeAddressesPrefix}P2PKH");
await walletBox.delete("${changeAddressesPrefix}P2SH");
await walletBox.delete("${changeAddressesPrefix}P2WPKH");
await walletBox.delete("latest_tx_model");
await walletBox.delete("latest_lelantus_tx_model");
// set empty mnemonic passphrase as we used that by default before
if ((await secureStore.read(key: '${walletId}_mnemonicPassphrase')) ==
null) {
await secureStore.write(
key: '${walletId}_mnemonicPassphrase',
value: "",
);
}
// doing this for epic cash will delete transaction history as it is not
// stored on the epic cash blockchain
if (info.coinIdentifier != epic.identifier) {
// set flag to initiate full rescan on opening wallet
await DB.instance.put<dynamic>(
boxName: DB.boxNameDBInfo,
key: "rescan_on_open_$walletId",
value: Constants.rescanV1,
);
}
}
}
Future<void> _v7(SecureStorageInterface secureStore) async {
await DB.instance.hive.openBox<dynamic>(DB.boxNameAllWalletsData);
final walletsService = WalletsService();
final walletInfoList = await walletsService.walletNames;
await MainDB.instance.initMainDB();
for (final walletId in walletInfoList.keys) {
final info = walletInfoList[walletId]!;
assert(info.walletId == walletId);
final count = await MainDB.instance.getTransactions(walletId).count();
final crypto = AppConfig.getCryptoCurrencyFor(info.coinIdentifier)!;
for (var i = 0; i < count; i += 50) {
final txns = await MainDB.instance
.getTransactions(walletId)
.offset(i)
.limit(50)
.findAll();
// migrate amount to serialized amount string
final txnsData = txns
.map(
(tx) => Tuple2(
tx
..amountString = Amount(
rawValue: BigInt.from(tx.amount),
fractionDigits: crypto.fractionDigits,
).toJsonString(),
tx.address.value,
),
)
.toList();
// update db records
await MainDB.instance.addNewTransactionData(txnsData, walletId);
}
}
}
Future<void> _v9() async {
final addressBookBox =
await DB.instance.hive.openBox<dynamic>(DB.boxNameAddressBook);
await MainDB.instance.initMainDB();
final keys = List<String>.from(addressBookBox.keys);
final contacts = keys
.map(
(id) => Contact.fromJson(
Map<String, dynamic>.from(
addressBookBox.get(id) as Map,
),
),
)
.toList(growable: false);
final List<isar_contact.ContactEntry> newContacts = [];
for (final contact in contacts) {
final List<isar_contact.ContactAddressEntry> newContactAddressEntries =
[];
for (final entry in contact.addresses) {
newContactAddressEntries.add(
isar_contact.ContactAddressEntry()
..coinName = entry.coin.identifier
..address = entry.address
..label = entry.label
..other = entry.other,
);
}
final newContact = isar_contact.ContactEntry(
name: contact.name,
addresses: newContactAddressEntries,
isFavorite: contact.isFavorite,
customId: contact.id,
);
newContacts.add(newContact);
}
await MainDB.instance.isar.writeTxn(() async {
await MainDB.instance.isar.contactEntrys.putAll(newContacts);
});
await addressBookBox.deleteFromDisk();
}
Future<void> _v10(SecureStorageInterface secureStore) async {
await DB.instance.hive.openBox<dynamic>(DB.boxNameAllWalletsData);
await DB.instance.hive.openBox<dynamic>(DB.boxNamePrefs);
final walletsService = WalletsService();
final prefs = Prefs.instance;
final walletInfoList = await walletsService.walletNames;
await prefs.init();
await MainDB.instance.initMainDB();
final firo = Firo(CryptoCurrencyNetwork.main);
for (final walletId in walletInfoList.keys) {
final info = walletInfoList[walletId]!;
assert(info.walletId == walletId);
if (info.coinIdentifier == firo.identifier &&
MainDB.instance.isar.lelantusCoins
.where()
.walletIdEqualTo(walletId)
.countSync() ==
0) {
final walletBox = await DB.instance.hive.openBox<dynamic>(walletId);
final hiveLCoins = DB.instance.get<dynamic>(
boxName: walletId,
key: "_lelantus_coins",
) as List? ??
[];
final jindexes = (DB.instance
.get<dynamic>(boxName: walletId, key: "jindex") as List? ??
[])
.cast<int>();
final List<isar_models.LelantusCoin> coins = [];
for (final e in hiveLCoins) {
final map = e as Map;
final lcoin = map.values.first as LelantusCoin;
final isJMint = jindexes.contains(lcoin.index);
final coin = isar_models.LelantusCoin(
walletId: walletId,
txid: lcoin.txId,
value: lcoin.value.toString(),
mintIndex: lcoin.index,
anonymitySetId: lcoin.anonymitySetId,
isUsed: lcoin.isUsed,
isJMint: isJMint,
otherData: null,
);
coins.add(coin);
}
if (coins.isNotEmpty) {
await MainDB.instance.isar.writeTxn(() async {
await MainDB.instance.isar.lelantusCoins.putAll(coins);
});
}
}
}
}
Future<void> _v11(SecureStorageInterface secureStore) async {
await migrateWalletsToIsar(secureStore: secureStore);
}
Future<void> _v12(SecureStorageInterface secureStore) async {
for (final identifier in ["firo", "firoTestNet"]) {
await DB.instance.deleteBoxFromDisk(
boxName: "${identifier}_anonymitySetSparkCache",
);
await DB.instance.deleteBoxFromDisk(
boxName: "${identifier}_sparkUsedCoinsTagsCache",
);
}
}
}