various firo restore/rescan fixes and tweaks

This commit is contained in:
julian 2023-11-27 11:50:35 -06:00
parent e5043dfe90
commit 5b3a998091
7 changed files with 363 additions and 51 deletions

View file

@ -422,8 +422,8 @@ class MainDB {
await isar.transactionV2s.where().walletIdEqualTo(walletId).count();
final addressCount = await getAddresses(walletId).count();
final utxoCount = await getUTXOs(walletId).count();
final lelantusCoinCount =
await isar.lelantusCoins.where().walletIdEqualTo(walletId).count();
// final lelantusCoinCount =
// await isar.lelantusCoins.where().walletIdEqualTo(walletId).count();
await isar.writeTxn(() async {
const paginateLimit = 50;
@ -471,16 +471,17 @@ class MainDB {
}
// lelantusCoins
for (int i = 0; i < lelantusCoinCount; i += paginateLimit) {
final lelantusCoinIds = await isar.lelantusCoins
.where()
.walletIdEqualTo(walletId)
.offset(i)
.limit(paginateLimit)
.idProperty()
.findAll();
await isar.lelantusCoins.deleteAll(lelantusCoinIds);
}
await isar.lelantusCoins.where().walletIdEqualTo(walletId).deleteAll();
// for (int i = 0; i < lelantusCoinCount; i += paginateLimit) {
// final lelantusCoinIds = await isar.lelantusCoins
// .where()
// .walletIdEqualTo(walletId)
// .offset(i)
// .limit(paginateLimit)
// .idProperty()
// .findAll();
// await isar.lelantusCoins.deleteAll(lelantusCoinIds);
// }
});
}

View file

@ -0,0 +1,236 @@
/*
* 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:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:isar/isar.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/providers/db/main_db_provider.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
class LelantusCoinsView extends ConsumerStatefulWidget {
const LelantusCoinsView({
Key? key,
required this.walletId,
}) : super(key: key);
static const String routeName = "/lelantusCoinsView";
final String walletId;
@override
ConsumerState<LelantusCoinsView> createState() => _LelantusCoinsViewState();
}
class _LelantusCoinsViewState extends ConsumerState<LelantusCoinsView> {
List<LelantusCoin> _coins = [];
Stream<List<LelantusCoin>>? lelantusCoinsCollectionWatcher;
void _onLelantusCoinsCollectionWatcherEvent(List<LelantusCoin> coins) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
setState(() {
_coins = coins;
});
}
});
}
@override
void initState() {
lelantusCoinsCollectionWatcher = ref
.read(mainDBProvider)
.isar
.lelantusCoins
.where()
.walletIdEqualTo(widget.walletId)
.sortByMintIndexDesc()
.watch(fireImmediately: true);
lelantusCoinsCollectionWatcher!
.listen((data) => _onLelantusCoinsCollectionWatcherEvent(data));
super.initState();
}
@override
void dispose() {
lelantusCoinsCollectionWatcher = null;
super.dispose();
}
@override
Widget build(BuildContext context) {
return DesktopScaffold(
appBar: DesktopAppBar(
background: Theme.of(context).extension<StackColors>()!.popupBG,
leading: Expanded(
child: Row(
children: [
const SizedBox(
width: 32,
),
AppBarIconButton(
size: 32,
color: Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultBG,
shadows: const [],
icon: SvgPicture.asset(
Assets.svg.arrowLeft,
width: 18,
height: 18,
color: Theme.of(context)
.extension<StackColors>()!
.topNavIconPrimary,
),
onPressed: Navigator.of(context).pop,
),
const SizedBox(
width: 12,
),
Text(
"Lelantus Coins",
style: STextStyles.desktopH3(context),
),
const Spacer(),
],
),
),
useSpacers: false,
isCompactHeight: true,
),
body: Padding(
padding: const EdgeInsets.all(24),
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(4),
child: RoundedWhiteContainer(
child: Row(
children: [
Expanded(
flex: 9,
child: Text(
"TXID",
style: STextStyles.itemSubtitle(context),
textAlign: TextAlign.left,
),
),
Expanded(
flex: 3,
child: Text(
"Value (sats)",
style: STextStyles.itemSubtitle(context),
textAlign: TextAlign.right,
),
),
Expanded(
flex: 2,
child: Text(
"Index",
style: STextStyles.itemSubtitle(context),
textAlign: TextAlign.right,
),
),
Expanded(
flex: 2,
child: Text(
"Is JMint",
style: STextStyles.itemSubtitle(context),
textAlign: TextAlign.right,
),
),
Expanded(
flex: 2,
child: Text(
"Used",
style: STextStyles.itemSubtitle(context),
textAlign: TextAlign.right,
),
),
],
),
),
),
Expanded(
child: ListView.separated(
shrinkWrap: true,
itemCount: _coins.length,
separatorBuilder: (_, __) => Container(
height: 1,
color: Theme.of(context)
.extension<StackColors>()!
.backgroundAppBar,
),
itemBuilder: (_, index) => Padding(
padding: const EdgeInsets.all(4),
child: RoundedWhiteContainer(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Expanded(
flex: 9,
child: SelectableText(
_coins[index].txid,
style: STextStyles.itemSubtitle12(context),
),
),
Expanded(
flex: 3,
child: SelectableText(
_coins[index].value,
style: STextStyles.itemSubtitle12(context),
textAlign: TextAlign.right,
),
),
Expanded(
flex: 2,
child: SelectableText(
_coins[index].mintIndex.toString(),
style: STextStyles.itemSubtitle12(context),
textAlign: TextAlign.right,
),
),
Expanded(
flex: 2,
child: SelectableText(
_coins[index].isJMint.toString(),
style: STextStyles.itemSubtitle12(context),
textAlign: TextAlign.right,
),
),
Expanded(
flex: 2,
child: SelectableText(
_coins[index].isUsed.toString(),
style: STextStyles.itemSubtitle12(context),
textAlign: TextAlign.right,
),
),
],
),
),
),
),
),
],
),
),
);
}
}

View file

@ -10,12 +10,14 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/change_representative_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/xpub_view.dart';
import 'package:stackwallet/pages_desktop_specific/addresses/desktop_wallet_addresses_view.dart';
import 'package:stackwallet/pages_desktop_specific/lelantus_coins/lelantus_coins_view.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_delete_wallet_dialog.dart';
import 'package:stackwallet/route_generator.dart';
import 'package:stackwallet/themes/stack_colors.dart';
@ -29,7 +31,8 @@ enum _WalletOptions {
addressList,
deleteWallet,
changeRepresentative,
showXpub;
showXpub,
lelantusCoins;
String get prettyName {
switch (this) {
@ -41,6 +44,8 @@ enum _WalletOptions {
return "Change representative";
case _WalletOptions.showXpub:
return "Show xPub";
case _WalletOptions.lelantusCoins:
return "Lelantus Coins";
}
}
}
@ -81,6 +86,9 @@ class WalletOptionsButton extends StatelessWidget {
onShowXpubPressed: () async {
Navigator.of(context).pop(_WalletOptions.showXpub);
},
onFiroShowLelantusCoins: () async {
Navigator.of(context).pop(_WalletOptions.lelantusCoins);
},
walletId: walletId,
);
},
@ -174,6 +182,15 @@ class WalletOptionsButton extends StatelessWidget {
}
}
break;
case _WalletOptions.lelantusCoins:
unawaited(
Navigator.of(context).pushNamed(
LelantusCoinsView.routeName,
arguments: walletId,
),
);
break;
}
}
},
@ -206,6 +223,7 @@ class WalletOptionsPopupMenu extends ConsumerWidget {
required this.onAddressListPressed,
required this.onShowXpubPressed,
required this.onChangeRepPressed,
required this.onFiroShowLelantusCoins,
required this.walletId,
}) : super(key: key);
@ -213,12 +231,16 @@ class WalletOptionsPopupMenu extends ConsumerWidget {
final VoidCallback onAddressListPressed;
final VoidCallback onShowXpubPressed;
final VoidCallback onChangeRepPressed;
final VoidCallback onFiroShowLelantusCoins;
final String walletId;
@override
Widget build(BuildContext context, WidgetRef ref) {
final coin = ref.watch(pWalletCoin(walletId));
final firoDebug =
kDebugMode && (coin == Coin.firo || coin == Coin.firoTestNet);
// TODO: [prio=low]
// final bool xpubEnabled = manager.hasXPub;
final bool xpubEnabled = false;
@ -315,6 +337,43 @@ class WalletOptionsPopupMenu extends ConsumerWidget {
),
),
),
if (firoDebug)
const SizedBox(
height: 8,
),
if (firoDebug)
TransparentButton(
onPressed: onFiroShowLelantusCoins,
child: Padding(
padding: const EdgeInsets.all(8),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
SvgPicture.asset(
Assets.svg.eye,
width: 20,
height: 20,
color: Theme.of(context)
.extension<StackColors>()!
.textFieldActiveSearchIconLeft,
),
const SizedBox(width: 14),
Expanded(
child: Text(
_WalletOptions.lelantusCoins.prettyName,
style: STextStyles.desktopTextExtraExtraSmall(
context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
),
),
),
],
),
),
),
if (xpubEnabled)
const SizedBox(
height: 8,

View file

@ -146,6 +146,7 @@ import 'package:stackwallet/pages_desktop_specific/desktop_buy/desktop_buy_view.
import 'package:stackwallet/pages_desktop_specific/desktop_exchange/desktop_all_trades_view.dart';
import 'package:stackwallet/pages_desktop_specific/desktop_exchange/desktop_exchange_view.dart';
import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart';
import 'package:stackwallet/pages_desktop_specific/lelantus_coins/lelantus_coins_view.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/my_stack_view.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/desktop_token_view.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart';
@ -1843,6 +1844,20 @@ class RouteGenerator {
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case LelantusCoinsView.routeName:
if (args is String) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => LelantusCoinsView(
walletId: args,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case DesktopCoinControlView.routeName:
if (args is String) {
return getRoute(

View file

@ -31,7 +31,7 @@ abstract final class LelantusFfiWrapper {
required final Bip39HDCurrency cryptoCurrency,
required final int latestSetId,
required final Map<dynamic, dynamic> setDataMap,
required final List<String> usedSerialNumbers,
required final Set<String> usedSerialNumbers,
required final String walletId,
required final String partialDerivationPath,
}) async {
@ -59,16 +59,17 @@ abstract final class LelantusFfiWrapper {
// partialDerivationPath should be something like "m/$purpose'/$coinType'/$account'/"
static Future<({List<String> spendTxIds, List<LelantusCoin> lelantusCoins})>
_restore(
({
String hexRootPrivateKey,
Uint8List chaincode,
Bip39HDCurrency cryptoCurrency,
int latestSetId,
Map<dynamic, dynamic> setDataMap,
List<String> usedSerialNumbers,
String walletId,
String partialDerivationPath,
}) args) async {
({
String hexRootPrivateKey,
Uint8List chaincode,
Bip39HDCurrency cryptoCurrency,
int latestSetId,
Map<dynamic, dynamic> setDataMap,
Set<String> usedSerialNumbers,
String walletId,
String partialDerivationPath,
}) args,
) async {
List<int> jindexes = [];
List<isar_models.LelantusCoin> lelantusCoins = [];
@ -76,23 +77,20 @@ abstract final class LelantusFfiWrapper {
int lastFoundIndex = 0;
int currentIndex = 0;
Set<String> usedSerialNumbersSet = args.usedSerialNumbers.toSet();
final root = BIP32.fromPrivateKey(
args.hexRootPrivateKey.toUint8ListFromHex,
args.chaincode,
);
while (currentIndex < lastFoundIndex + 50) {
final _derivePath =
"${args.partialDerivationPath}$MINT_INDEX/$currentIndex";
final mintKeyPair = root.derivePath(_derivePath);
final mintKeyPair = root.derivePath(
"${args.partialDerivationPath}$MINT_INDEX/$currentIndex",
);
final String mintTag = lelantus.CreateTag(
mintKeyPair.privateKey!.toHex,
currentIndex,
Format.uint8listToString(mintKeyPair.identifier),
mintKeyPair.identifier.toHex,
isTestnet: args.cryptoCurrency.network == CryptoCurrencyNetwork.test,
);
@ -121,7 +119,7 @@ abstract final class LelantusFfiWrapper {
isTestnet:
args.cryptoCurrency.network == CryptoCurrencyNetwork.test,
);
final bool isUsed = usedSerialNumbersSet.contains(serialNumber);
final bool isUsed = args.usedSerialNumbers.contains(serialNumber);
lelantusCoins.removeWhere((e) =>
e.txid == txId &&
@ -141,14 +139,13 @@ abstract final class LelantusFfiWrapper {
publicCoin, // not really needed but saved just in case
),
);
debugPrint("amount $amount used $isUsed");
debugPrint("serial=$serialNumber amount=$amount used=$isUsed");
} else if (thirdValue is String) {
final int keyPath = lelantus.GetAesKeyPath(publicCoin);
final derivePath =
"${args.partialDerivationPath}$JMINT_INDEX/$keyPath";
final aesKeyPair = root.derivePath(derivePath);
final aesKeyPair = root.derivePath(
"${args.partialDerivationPath}$JMINT_INDEX/$keyPath",
);
try {
final String aesPrivateKey = aesKeyPair.privateKey!.toHex;
@ -165,7 +162,8 @@ abstract final class LelantusFfiWrapper {
isTestnet:
args.cryptoCurrency.network == CryptoCurrencyNetwork.test,
);
bool isUsed = usedSerialNumbersSet.contains(serialNumber);
final bool isUsed = args.usedSerialNumbers.contains(serialNumber);
lelantusCoins.removeWhere((e) =>
e.txid == txId &&
e.mintIndex == currentIndex &&
@ -188,8 +186,7 @@ abstract final class LelantusFfiWrapper {
spendTxIds.add(txId);
} catch (_) {
debugPrint(
"AES keypair derivation issue for derive path: $derivePath");
debugPrint("AES keypair derivation issue for key path: $keyPath");
}
} else {
debugPrint("Unexpected coin found: $foundCoin");
@ -312,9 +309,9 @@ abstract final class LelantusFfiWrapper {
isTestNet: arg.cryptoCurrency.network == CryptoCurrencyNetwork.test,
),
);
var changeToMint = estimateJoinSplitFee.changeToMint;
var fee = estimateJoinSplitFee.fee;
var spendCoinIndexes = estimateJoinSplitFee.spendCoinIndexes;
final changeToMint = estimateJoinSplitFee.changeToMint;
final fee = estimateJoinSplitFee.fee;
final spendCoinIndexes = estimateJoinSplitFee.spendCoinIndexes;
debugPrint("$changeToMint $fee $spendCoinIndexes");
if (spendCoinIndexes.isEmpty) {
throw Exception("Error, Not enough funds.");

View file

@ -796,14 +796,17 @@ class FiroWallet extends Bip39HDWallet
setDataMapFuture,
]);
final usedSerialsSet = (futureResults[0] as List<String>).toSet();
final setDataMap = futureResults[1] as Map<dynamic, dynamic>;
await recoverLelantusWallet(
latestSetId: latestSetId,
setDataMap: futureResults[1] as Map<dynamic, dynamic>,
usedSerialNumbers: futureResults[0] as List<String>,
usedSerialNumbers: usedSerialsSet,
setDataMap: setDataMap,
);
await updateBalance();
});
await refresh();
} catch (e, s) {
Logging.instance.log(
"Exception rethrown from electrumx_mixin recover(): $e\n$s",

View file

@ -559,7 +559,7 @@ mixin LelantusInterface on Bip39HDWallet, ElectrumXInterface {
Future<void> recoverLelantusWallet({
required int latestSetId,
required Map<dynamic, dynamic> setDataMap,
required List<String> usedSerialNumbers,
required Set<String> usedSerialNumbers,
}) async {
final root = await getRootHDNode();
@ -568,6 +568,8 @@ mixin LelantusInterface on Bip39HDWallet, ElectrumXInterface {
chain: 0,
index: 0,
);
// get "m/$purpose'/$coinType'/$account'/" from "m/$purpose'/$coinType'/$account'/0/0"
final partialDerivationPath = derivePath.substring(
0,
derivePath.length - 3,
@ -593,12 +595,11 @@ mixin LelantusInterface on Bip39HDWallet, ElectrumXInterface {
.or()
.isLelantusEqualTo(false)
.findAll();
final lelantusCoins = result.lelantusCoins;
// Edit the receive transactions with the mint fees.
List<Transaction> editedTransactions = [];
for (final coin in lelantusCoins) {
for (final coin in result.lelantusCoins) {
String txid = coin.txid;
Transaction? tx;
try {
@ -675,7 +676,7 @@ mixin LelantusInterface on Bip39HDWallet, ElectrumXInterface {
}
transactionMap.removeWhere((key, value) =>
lelantusCoins.any((element) => element.txid == key) ||
result.lelantusCoins.any((element) => element.txid == key) ||
((value.height == -1 || value.height == null) &&
!value.isConfirmed(currentHeight, cryptoCurrency.minConfirms)));