diff --git a/lib/models/isar/models/address/address.dart b/lib/models/isar/models/address/address.dart index f8b0195ca..a21d991dd 100644 --- a/lib/models/isar/models/address/address.dart +++ b/lib/models/isar/models/address/address.dart @@ -31,7 +31,7 @@ class Address extends CryptoCurrencyAddress { late List publicKey; @Index() - late int derivationIndex; + late int derivationIndex; // -1 generally means unknown @enumerated late AddressType type; @@ -66,6 +66,7 @@ enum AddressType { p2wpkh, cryptonote, mimbleWimble, + unknown, nonWallet, } diff --git a/lib/utilities/constants.dart b/lib/utilities/constants.dart index 740e40c24..e78d7a76f 100644 --- a/lib/utilities/constants.dart +++ b/lib/utilities/constants.dart @@ -38,7 +38,7 @@ abstract class Constants { // Enable Logger.print statements static const bool disableLogger = false; - static const int currentHiveDbVersion = 4; + static const int currentHiveDbVersion = 5; static int satsPerCoin(Coin coin) { switch (coin) { diff --git a/lib/utilities/db_version_migration.dart b/lib/utilities/db_version_migration.dart index 57c53f385..4abe653bf 100644 --- a/lib/utilities/db_version_migration.dart +++ b/lib/utilities/db_version_migration.dart @@ -1,19 +1,25 @@ +import 'dart:convert'; + import 'package:hive/hive.dart'; import 'package:stackwallet/electrumx_rpc/electrumx.dart'; import 'package:stackwallet/hive/db.dart'; import 'package:stackwallet/models/exchange/change_now/exchange_transaction.dart'; import 'package:stackwallet/models/exchange/response_objects/trade.dart'; -import 'package:stackwallet/models/lelantus_coin.dart'; +import 'package:stackwallet/models/isar/models/isar_models.dart' as isar_models; +import 'package:stackwallet/models/models.dart'; import 'package:stackwallet/models/node_model.dart'; +import 'package:stackwallet/services/mixins/wallet_db.dart'; import 'package:stackwallet/services/node_service.dart'; import 'package:stackwallet/services/wallets_service.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; +import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/prefs.dart'; +import 'package:tuple/tuple.dart'; -class DbVersionMigrator { +class DbVersionMigrator with WalletDB { Future migrate( int fromVersion, { required SecureStorageInterface secureStore, @@ -169,20 +175,396 @@ class DbVersionMigrator { // try to continue migrating return await migrate(4, secureStore: secureStore); - // case 4: - // // TODO: once isar migration is ready - // // 1. Address arrays - // - // // update version - // await DB.instance.put( - // boxName: DB.boxNameDBInfo, key: "hive_data_version", value: 5); - // - // // try to continue migrating - // return await migrate(5, secureStore: secureStore); + case 4: + // migrate + await _v4(secureStore); + + // update version + await DB.instance.put( + boxName: DB.boxNameDBInfo, key: "hive_data_version", value: 5); + + // try to continue migrating + return await migrate(5, secureStore: secureStore); default: // finally return return; } } + + Future _v4(SecureStorageInterface secureStore) async { + await Hive.openBox(DB.boxNameAllWalletsData); + await Hive.openBox(DB.boxNamePrefs); + final walletsService = WalletsService(secureStorageInterface: secureStore); + final prefs = Prefs.instance; + final walletInfoList = await walletsService.walletNames; + await prefs.init(); + + for (final walletId in walletInfoList.keys) { + final info = walletInfoList[walletId]!; + assert(info.walletId == walletId); + + final walletBox = await Hive.openBox(info.walletId); + + final receiveDerivePrefix = "${walletId}_receiveDerivations"; + final changeDerivePrefix = "${walletId}_changeDerivations"; + + const receiveAddressesPrefix = "receivingAddresses"; + const changeAddressesPrefix = "changeAddresses"; + + final p2pkhRcvDerivations = + (await secureStore.read(key: receiveDerivePrefix)) ?? + (await secureStore.read(key: "${receiveDerivePrefix}P2PKH")); + final p2shRcvDerivations = + await secureStore.read(key: "${receiveDerivePrefix}P2SH"); + final p2wpkhRcvDerivations = + await secureStore.read(key: "${receiveDerivePrefix}P2WPKH"); + + final p2pkhCngDerivations = + (await secureStore.read(key: changeDerivePrefix)) ?? + (await secureStore.read(key: "${changeDerivePrefix}P2PKH")); + final p2shCngDerivations = + await secureStore.read(key: "${changeDerivePrefix}P2SH"); + final p2wpkhCngDerivations = + await secureStore.read(key: "${changeDerivePrefix}P2WPKH"); + + // useless? + // const receiveIndexPrefix = "receivingIndex"; + // const changeIndexPrefix = "changeIndex"; + // final p2pkhRcvIndex = walletBox.get(receiveIndexPrefix) as int? ?? + // walletBox.get("${receiveIndexPrefix}P2PKH") as int?; + // final p2shRcvIndex = + // walletBox.get("${receiveIndexPrefix}P2SH") as int?; + // final p2wpkhRcvIndex = + // walletBox.get("${receiveIndexPrefix}P2WPKH") as int?; + // + // final p2pkhCngIndex = walletBox.get(changeIndexPrefix) as int? ?? + // walletBox.get("${changeIndexPrefix}P2PKH") as int?; + // final p2shCngIndex = + // walletBox.get("${changeIndexPrefix}P2SH") as int?; + // final p2wpkhCngIndex = + // walletBox.get("${changeIndexPrefix}P2WPKH") as int?; + + final List newAddresses = []; + + if (p2pkhRcvDerivations != null) { + newAddresses.addAll( + _v4GetAddressesFromDerivationString( + p2pkhRcvDerivations, + isar_models.AddressType.p2pkh, + isar_models.AddressSubType.receiving, + walletId, + ), + ); + } + + if (p2shRcvDerivations != null) { + newAddresses.addAll( + _v4GetAddressesFromDerivationString( + p2shRcvDerivations, + isar_models.AddressType.p2sh, + isar_models.AddressSubType.receiving, + walletId, + ), + ); + } + + if (p2wpkhRcvDerivations != null) { + newAddresses.addAll( + _v4GetAddressesFromDerivationString( + p2wpkhRcvDerivations, + isar_models.AddressType.p2wpkh, + isar_models.AddressSubType.receiving, + walletId, + ), + ); + } + + if (p2pkhCngDerivations != null) { + newAddresses.addAll( + _v4GetAddressesFromDerivationString( + p2pkhCngDerivations, + isar_models.AddressType.p2pkh, + isar_models.AddressSubType.change, + walletId, + ), + ); + } + + if (p2shCngDerivations != null) { + newAddresses.addAll( + _v4GetAddressesFromDerivationString( + p2shCngDerivations, + isar_models.AddressType.p2sh, + isar_models.AddressSubType.change, + walletId, + ), + ); + } + + if (p2wpkhCngDerivations != null) { + newAddresses.addAll( + _v4GetAddressesFromDerivationString( + p2wpkhCngDerivations, + isar_models.AddressType.p2wpkh, + isar_models.AddressSubType.change, + walletId, + ), + ); + } + + final currentNewSet = newAddresses.map((e) => e.value).toSet(); + + final p2pkhRcvAddresses = _v4GetAddressesFromList( + walletBox.get(receiveAddressesPrefix) as List? ?? + walletBox.get("${receiveAddressesPrefix}P2PKH") + as List? ?? + [], + isar_models.AddressType.p2pkh, + isar_models.AddressSubType.receiving, + walletId); + for (final address in p2pkhRcvAddresses) { + if (!currentNewSet.contains(address.value)) { + newAddresses.add(address); + } + } + + final p2shRcvAddresses = _v4GetAddressesFromList( + walletBox.get("${receiveAddressesPrefix}P2SH") as List? ?? [], + isar_models.AddressType.p2sh, + isar_models.AddressSubType.receiving, + walletId); + for (final address in p2shRcvAddresses) { + if (!currentNewSet.contains(address.value)) { + newAddresses.add(address); + } + } + + final p2wpkhRcvAddresses = _v4GetAddressesFromList( + walletBox.get("${receiveAddressesPrefix}P2WPKH") as List? ?? + [], + isar_models.AddressType.p2wpkh, + isar_models.AddressSubType.receiving, + walletId); + for (final address in p2wpkhRcvAddresses) { + if (!currentNewSet.contains(address.value)) { + newAddresses.add(address); + } + } + + final p2pkhCngAddresses = _v4GetAddressesFromList( + walletBox.get(changeAddressesPrefix) as List? ?? + walletBox.get("${changeAddressesPrefix}P2PKH") as List? ?? + [], + isar_models.AddressType.p2wpkh, + isar_models.AddressSubType.change, + walletId); + for (final address in p2pkhCngAddresses) { + if (!currentNewSet.contains(address.value)) { + newAddresses.add(address); + } + } + + final p2shCngAddresses = _v4GetAddressesFromList( + walletBox.get("${changeAddressesPrefix}P2SH") as List? ?? [], + isar_models.AddressType.p2wpkh, + isar_models.AddressSubType.change, + walletId); + for (final address in p2shCngAddresses) { + if (!currentNewSet.contains(address.value)) { + newAddresses.add(address); + } + } + + final p2wpkhCngAddresses = _v4GetAddressesFromList( + walletBox.get("${changeAddressesPrefix}P2WPKH") as List? ?? + [], + isar_models.AddressType.p2wpkh, + isar_models.AddressSubType.change, + walletId); + for (final address in p2wpkhCngAddresses) { + if (!currentNewSet.contains(address.value)) { + newAddresses.add(address); + } + } + + // transactions + final txnData = walletBox.get("latest_tx_model") as TransactionData?; + final txns = txnData?.getAllTransactions().values ?? []; + final txnDataLelantus = + walletBox.get("latest_lelantus_tx_model") as TransactionData?; + final txnsLelantus = txnDataLelantus?.getAllTransactions().values ?? []; + + final List< + Tuple4< + isar_models.Transaction, + List, + List, + isar_models.Address?>> newTransactions = []; + + newTransactions + .addAll(_parseTransactions(txns, walletId, false, newAddresses)); + newTransactions.addAll( + _parseTransactions(txnsLelantus, walletId, true, newAddresses)); + + // store newly parsed data in isar + await isarInit(walletId); + await db.isar.writeTxn(() async { + await db.isar.addresses.putAll(newAddresses); + }); + await addNewTransactionData(newTransactions, 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"); + } + } + + List< + Tuple4, + List, isar_models.Address?>> _parseTransactions( + Iterable txns, + String walletId, + bool isLelantus, + List parsedAddresses, + ) { + List< + Tuple4, + List, isar_models.Address?>> transactions = []; + for (final tx in txns) { + final type = tx.txType.toLowerCase() == "received" + ? isar_models.TransactionType.incoming + : isar_models.TransactionType.outgoing; + final subType = tx.subType.toLowerCase() == "mint" + ? isar_models.TransactionSubType.mint + : tx.subType.toLowerCase() == "join" + ? isar_models.TransactionSubType.join + : isar_models.TransactionSubType.none; + + final transaction = isar_models.Transaction( + walletId: walletId, + txid: tx.txid, + timestamp: tx.timestamp, + type: type, + subType: subType, + amount: tx.amount, + fee: tx.fees, + height: tx.height, + isCancelled: tx.isCancelled, + isLelantus: false, + slateId: tx.slateId, + otherData: tx.otherData, + ); + + final List inputs = []; + final List outputs = []; + + for (final inp in tx.inputs) { + final input = isar_models.Input( + walletId: walletId, + txid: inp.txid, + vout: inp.vout, + scriptSig: inp.scriptsig, + scriptSigAsm: inp.scriptsigAsm, + isCoinbase: inp.isCoinbase, + sequence: inp.sequence, + innerRedeemScriptAsm: inp.innerRedeemscriptAsm, + ); + inputs.add(input); + } + for (final out in tx.outputs) { + final output = isar_models.Output( + walletId: walletId, + scriptPubKey: out.scriptpubkey, + scriptPubKeyAsm: out.scriptpubkeyAsm, + scriptPubKeyType: out.scriptpubkeyType, + scriptPubKeyAddress: out.scriptpubkeyAddress, + value: out.value, + ); + outputs.add(output); + } + + isar_models.Address? address; + if (tx.address.isNotEmpty) { + final addresses = parsedAddresses.where((e) => e.value == tx.address); + if (addresses.isNotEmpty) { + address = addresses.first; + } else { + address = isar_models.Address( + walletId: walletId, + value: tx.address, + publicKey: [], + derivationIndex: -1, + type: isar_models.AddressType.unknown, + subType: type == isar_models.TransactionType.incoming + ? isar_models.AddressSubType.receiving + : isar_models.AddressSubType.change, + ); + } + } + + transactions.add(Tuple4(transaction, outputs, inputs, address)); + } + return transactions; + } + + List _v4GetAddressesFromDerivationString( + String derivationsString, + isar_models.AddressType type, + isar_models.AddressSubType subType, + String walletId, + ) { + final List addresses = []; + + final derivations = + Map.from(jsonDecode(derivationsString) as Map); + + for (final entry in derivations.entries) { + final addr = entry.key; + final pubKey = entry.value["pubKey"] as String; + + final address = isar_models.Address( + walletId: walletId, + value: addr, + publicKey: Format.stringToUint8List(pubKey), + derivationIndex: -1, + type: type, + subType: subType, + ); + addresses.add(address); + } + + return addresses; + } + + List _v4GetAddressesFromList( + List addressStrings, + isar_models.AddressType type, + isar_models.AddressSubType subType, + String walletId, + ) { + final List addresses = []; + + for (final addr in addressStrings) { + final address = isar_models.Address( + walletId: walletId, + value: addr, + publicKey: [], + derivationIndex: -1, + type: type, + subType: subType, + ); + addresses.add(address); + } + + return addresses; + } }