WIP musig send view, but wallet creation not working correctly ATM

This commit is contained in:
sneurlax 2024-12-30 19:39:38 -06:00
parent 6d9c8f9356
commit d986f27c96
5 changed files with 596 additions and 3 deletions

View file

@ -0,0 +1,581 @@
/*
* 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 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:tuple/tuple.dart';
import '../../../frost_route_generator.dart';
import '../../../models/isar/models/isar_models.dart';
import '../../../providers/frost_wallet/frost_wallet_providers.dart';
import '../../../providers/providers.dart';
import '../../../providers/wallet/public_private_balance_state_provider.dart';
import '../../../themes/coin_icon_provider.dart';
import '../../../themes/stack_colors.dart';
import '../../../utilities/amount/amount.dart';
import '../../../utilities/amount/amount_formatter.dart';
import '../../../utilities/constants.dart';
import '../../../utilities/show_loading.dart';
import '../../../utilities/text_styles.dart';
import '../../../utilities/util.dart';
import '../../../wallets/crypto_currency/crypto_currency.dart';
import '../../../wallets/isar/providers/wallet_info_provider.dart';
import '../../../wallets/models/tx_data.dart';
import '../../../wallets/wallet/impl/bitcoin_frost_wallet.dart';
import '../../../wallets/wallet/wallet_mixin_interfaces/coin_control_interface.dart';
import '../../../widgets/background.dart';
import '../../../widgets/conditional_parent.dart';
import '../../../widgets/custom_buttons/app_bar_icon_button.dart';
import '../../../widgets/custom_buttons/blue_text_button.dart';
import '../../../widgets/desktop/primary_button.dart';
import '../../../widgets/desktop/secondary_button.dart';
import '../../../widgets/fee_slider.dart';
import '../../../widgets/frost_scaffold.dart';
import '../../../widgets/icon_widgets/x_icon.dart';
import '../../../widgets/rounded_white_container.dart';
import '../../../widgets/stack_dialog.dart';
import '../../../widgets/stack_text_field.dart';
import '../../../widgets/textfield_icon_button.dart';
import '../../coin_control/coin_control_view.dart';
import 'recipient.dart';
class MultisigSendView extends ConsumerStatefulWidget {
const MultisigSendView({
super.key,
required this.walletId,
required this.coin,
});
static const String routeName = "/frostSendView";
final String walletId;
final CryptoCurrency coin;
@override
ConsumerState<MultisigSendView> createState() => _MultisigSendViewState();
}
class _MultisigSendViewState extends ConsumerState<MultisigSendView> {
final List<int> recipientWidgetIndexes = [0];
int _greatestWidgetIndex = 0;
late final String walletId;
late final CryptoCurrency coin;
late TextEditingController noteController;
late TextEditingController onChainNoteController;
final _noteFocusNode = FocusNode();
Set<UTXO> selectedUTXOs = {};
bool _createSignLock = false;
Future<TxData> _loadingFuture() async {
final wallet = ref.read(pWallets).getWallet(walletId) as BitcoinFrostWallet;
final recipients = recipientWidgetIndexes
.map((i) => ref.read(pRecipient(i).state).state)
.map((e) => (address: e!.address, amount: e!.amount!, isChange: false))
.toList(growable: false);
final txData = await wallet.frostCreateSignConfig(
txData: TxData(recipients: recipients),
feePerWeight: customFeeRate,
);
return txData;
}
Future<void> _createSignConfig() async {
if (_createSignLock) {
return;
}
_createSignLock = true;
try {
// wait for keyboard to disappear
FocusScope.of(context).unfocus();
await Future<void>.delayed(
const Duration(milliseconds: 100),
);
TxData? txData;
if (mounted) {
txData = await showLoading<TxData>(
whileFuture: _loadingFuture(),
context: context,
message: "Generating sign config",
rootNavigator: Util.isDesktop,
onException: (e) {
throw e;
},
);
}
final wallet =
ref.read(pWallets).getWallet(walletId) as BitcoinFrostWallet;
if (mounted && txData != null) {
ref.read(pFrostTxData.notifier).state = txData;
ref.read(pFrostScaffoldArgs.state).state = (
info: (
walletName: wallet.info.name,
frostCurrency: wallet.cryptoCurrency,
),
walletId: walletId,
stepRoutes: FrostRouteGenerator.sendFrostTxStepRoutes,
parentNav: Navigator.of(context),
frostInterruptionDialogType:
FrostInterruptionDialogType.transactionCreation,
callerRouteName: MultisigSendView.routeName,
);
await Navigator.of(context).pushNamed(
FrostStepScaffold.routeName,
);
}
} catch (e) {
if (mounted) {
unawaited(
showDialog<dynamic>(
context: context,
useSafeArea: false,
barrierDismissible: true,
builder: (context) {
return StackDialog(
title: "Create sign config failed",
message: e.toString(),
rightButton: TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getSecondaryEnabledButtonStyle(context),
child: Text(
"Ok",
style: STextStyles.button(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark,
),
),
onPressed: () {
Navigator.of(context).pop();
},
),
);
},
),
);
}
} finally {
_createSignLock = false;
}
}
int customFeeRate = 1;
bool _buttonEnabled = false;
bool _validateRecipientFormStatesHelper() {
for (final i in recipientWidgetIndexes) {
final state = ref.read(pRecipient(i));
if (state?.amount == null ||
state?.address == null ||
state!.address.isEmpty) {
return false;
}
}
return true;
}
void _validateRecipientFormStates() {
setState(() {
_buttonEnabled = _validateRecipientFormStatesHelper();
});
}
@override
void initState() {
coin = widget.coin;
walletId = widget.walletId;
noteController = TextEditingController();
super.initState();
}
@override
void dispose() {
noteController.dispose();
_noteFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
debugPrint("BUILD: $runtimeType");
final wallet = ref.watch(pWallets).getWallet(walletId);
final showCoinControl = wallet is CoinControlInterface &&
ref.watch(
prefsChangeNotifierProvider.select(
(value) => value.enableCoinControl,
),
) &&
(coin is Firo
? ref.watch(publicPrivateBalanceStateProvider) == FiroType.public
: true);
return ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () async {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(const Duration(milliseconds: 50));
}
if (context.mounted) {
Navigator.of(context).pop();
}
},
),
title: Text(
"Sends ${coin.ticker}",
style: STextStyles.navBarTitle(context),
),
),
body: LayoutBuilder(
builder: (builderContext, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
// subtract top and bottom padding set in parent
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: child,
),
),
),
);
},
),
),
),
child: ConditionalParent(
condition: Util.isDesktop,
builder: (child) => Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 14,
),
child: child,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (!Util.isDesktop)
Container(
decoration: BoxDecoration(
color: Theme.of(context).extension<StackColors>()!.popupBG,
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Row(
children: [
SvgPicture.file(
File(
ref.watch(
coinIconProvider(coin),
),
),
width: 22,
height: 22,
),
const SizedBox(
width: 6,
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
ref.watch(pWalletName(walletId)),
style: STextStyles.titleBold12(context)
.copyWith(fontSize: 14),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
// const SizedBox(
// height: 2,
// ),
Text(
"Available balance",
style: STextStyles.label(context)
.copyWith(fontSize: 10),
),
],
),
Util.isDesktop
? const SizedBox(
height: 24,
)
: const Spacer(),
GestureDetector(
onTap: () {},
child: Container(
color: Colors.transparent,
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
ref.watch(pAmountFormatter(coin)).format(
ref
.watch(pWalletBalance(walletId))
.spendable,
),
style:
STextStyles.titleBold12(context).copyWith(
fontSize: 10,
),
textAlign: TextAlign.right,
),
],
),
),
),
],
),
),
),
SizedBox(
height: recipientWidgetIndexes.length > 1 ? 8 : 16,
),
Column(
children: [
for (int i = 0; i < recipientWidgetIndexes.length; i++)
ConditionalParent(
condition: recipientWidgetIndexes.length > 1,
builder: (child) => Padding(
padding: const EdgeInsets.only(top: 8),
child: child,
),
child: Recipient(
key: Key(
"recipientKey_${recipientWidgetIndexes[i]}",
),
index: recipientWidgetIndexes[i],
displayNumber: i + 1,
coin: coin,
onChanged: () {
_validateRecipientFormStates();
},
remove: i == 0 && recipientWidgetIndexes.length == 1
? null
: () {
ref
.read(
pRecipient(recipientWidgetIndexes[i])
.notifier,
)
.state = null;
recipientWidgetIndexes.removeAt(i);
setState(() {});
_validateRecipientFormStates();
},
addAnotherRecipientTapped: () {
// used for tracking recipient forms
_greatestWidgetIndex++;
recipientWidgetIndexes.add(_greatestWidgetIndex);
setState(() {});
_validateRecipientFormStates();
},
sendAllTapped: () {
return ref.read(pAmountFormatter(coin)).format(
ref.read(pWalletBalance(walletId)).spendable,
withUnitName: false,
);
},
),
),
],
),
if (recipientWidgetIndexes.length > 1)
const SizedBox(
height: 12,
),
if (recipientWidgetIndexes.length > 1)
SecondaryButton(
width: double.infinity,
label: "Add recipient",
onPressed: () {
// used for tracking recipient forms
_greatestWidgetIndex++;
recipientWidgetIndexes.add(_greatestWidgetIndex);
setState(() {});
},
),
if (showCoinControl)
const SizedBox(
height: 8,
),
if (showCoinControl)
RoundedWhiteContainer(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Coin control",
style: STextStyles.w500_14(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
),
CustomTextButton(
text: selectedUTXOs.isEmpty
? "Select coins"
: "Selected coins (${selectedUTXOs.length})",
onTap: () async {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(
const Duration(milliseconds: 100),
);
}
if (mounted) {
// TODO: [prio=high] make sure this coincontrol works correctly
Amount? amount;
final result = await Navigator.of(context).pushNamed(
CoinControlView.routeName,
arguments: Tuple4(
walletId,
CoinControlViewType.use,
amount,
selectedUTXOs,
),
);
if (result is Set<UTXO>) {
setState(() {
selectedUTXOs = result;
});
}
}
},
),
],
),
),
const SizedBox(
height: 12,
),
Text(
"Note (optional)",
style: STextStyles.smallMed12(context),
textAlign: TextAlign.left,
),
const SizedBox(
height: 8,
),
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
controller: noteController,
focusNode: _noteFocusNode,
style: STextStyles.field(context),
onChanged: (_) => setState(() {}),
decoration: standardInputDecoration(
"Type something...",
_noteFocusNode,
context,
).copyWith(
suffixIcon: noteController.text.isNotEmpty
? Padding(
padding: const EdgeInsets.only(right: 0),
child: UnconstrainedBox(
child: Row(
children: [
TextFieldIconButton(
child: const XIcon(),
onTap: () async {
setState(() {
noteController.text = "";
});
},
),
],
),
),
)
: null,
),
),
),
const SizedBox(
height: 12,
),
Padding(
padding: const EdgeInsets.only(
bottom: 12,
top: 16,
),
child: FeeSlider(
coin: coin,
showWU: true,
onSatVByteChanged: (rate) {
customFeeRate = rate;
},
),
),
Util.isDesktop
? const SizedBox(
height: 12,
)
: const Spacer(),
const SizedBox(
height: 12,
),
PrimaryButton(
label: "Create multisig transaction",
enabled: _buttonEnabled,
onPressed: _createSignConfig,
),
const SizedBox(
height: 16,
),
],
),
),
);
}
}

View file

@ -484,8 +484,10 @@ class _MultisigSetupViewState extends ConsumerState<MultisigCoordinatorView> {
'cosignerXpubs': xpubControllers.map((e) => e.text).toList(),
});
final newCoin = BIP48Bitcoin(parentWallet.cryptoCurrency.network);
final info = WalletInfo.createNew(
coin: BIP48Bitcoin(parentWallet.cryptoCurrency.network),
coin: newCoin,
name:
'widget.walletName', // TODO [prio=high]: Add wallet name input field to multisig setup view and pass it to the coordinator view here.
restoreHeight: await parentWallet.chainHeight,
@ -524,7 +526,7 @@ class _MultisigSetupViewState extends ConsumerState<MultisigCoordinatorView> {
node ??= parentWallet.cryptoCurrency.defaultNode;
await ref
.read(nodeServiceChangeNotifierProvider)
.setPrimaryNodeFor(coin: parentWallet.cryptoCurrency, node: node);
.setPrimaryNodeFor(coin: newCoin, node: node);
final txTracker = TransactionNotificationTracker(walletId: info.walletId);

View file

@ -45,10 +45,12 @@ import '../../utilities/enums/sync_type_enum.dart';
import '../../utilities/logger.dart';
import '../../utilities/show_loading.dart';
import '../../utilities/text_styles.dart';
import '../../wallets/crypto_currency/coins/bip48_bitcoin.dart';
import '../../wallets/crypto_currency/crypto_currency.dart';
import '../../wallets/crypto_currency/interfaces/bip48_currency_interface.dart';
import '../../wallets/crypto_currency/intermediate/frost_currency.dart';
import '../../wallets/isar/providers/wallet_info_provider.dart';
import '../../wallets/wallet/impl/bip48_bitcoin_wallet.dart';
import '../../wallets/wallet/impl/bitcoin_frost_wallet.dart';
import '../../wallets/wallet/impl/firo_wallet.dart';
import '../../wallets/wallet/intermediate/lib_monero_wallet.dart';
@ -93,6 +95,7 @@ import '../paynym/paynym_claim_view.dart';
import '../paynym/paynym_home_view.dart';
import '../receive_view/receive_view.dart';
import '../send_view/frost_ms/frost_send_view.dart';
import '../send_view/multisig/multisig_send_view.dart';
import '../send_view/send_view.dart';
import '../settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart';
import '../settings_views/wallet_settings_view/wallet_settings_view.dart';
@ -1057,7 +1060,9 @@ class _WalletViewState extends ConsumerState<WalletView> {
Navigator.of(context).pushNamed(
wallet is BitcoinFrostWallet
? FrostSendView.routeName
: SendView.routeName,
: wallet is BIP48BitcoinWallet
? MultisigSendView.routeName
: SendView.routeName,
arguments: (
walletId: walletId,
coin: coin,

View file

@ -22,10 +22,12 @@ import '../../utilities/flutter_secure_storage_interface.dart';
import '../../utilities/logger.dart';
import '../../utilities/paynym_is_api.dart';
import '../../utilities/prefs.dart';
import '../crypto_currency/coins/bip48_bitcoin.dart';
import '../crypto_currency/crypto_currency.dart';
import '../isar/models/wallet_info.dart';
import '../models/tx_data.dart';
import 'impl/banano_wallet.dart';
import 'impl/bip48_bitcoin_wallet.dart';
import 'impl/bitcoin_frost_wallet.dart';
import 'impl/bitcoin_wallet.dart';
import 'impl/bitcoincash_wallet.dart';
@ -335,6 +337,9 @@ abstract class Wallet<T extends CryptoCurrency> {
case const (Banano):
return BananoWallet(net);
case const (BIP48Bitcoin):
return BIP48BitcoinWallet(net);
case const (Bitcoin):
return BitcoinWallet(net);