Merge pull request #805 from cypherstack/add_frost

Add FROST
This commit is contained in:
Diego Salazar 2024-03-12 16:52:53 -06:00 committed by GitHub
commit 7b90f2cd57
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
88 changed files with 14359 additions and 420 deletions

5
.gitmodules vendored
View file

@ -6,4 +6,7 @@
url = https://github.com/cypherstack/flutter_libmonero.git
[submodule "crypto_plugins/flutter_liblelantus"]
path = crypto_plugins/flutter_liblelantus
url = https://github.com/cypherstack/flutter_liblelantus.git
url = https://github.com/cypherstack/flutter_liblelantus.git
[submodule "crypto_plugins/frostdart"]
path = crypto_plugins/frostdart
url = https://github.com/cypherstack/frostdart

@ -0,0 +1 @@
Subproject commit 0fbc038a262e3c2d82c7c6e34e194e9a47011d91

View file

@ -21,6 +21,7 @@ import 'package:stackwallet/models/isar/stack_theme.dart';
import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/stack_file_system.dart';
import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart';
import 'package:stackwallet/wallets/isar/models/spark_coin.dart';
import 'package:stackwallet/wallets/isar/models/token_wallet_info.dart';
import 'package:stackwallet/wallets/isar/models/wallet_info.dart';
@ -67,6 +68,7 @@ class MainDB {
SparkCoinSchema,
WalletInfoMetaSchema,
TokenWalletInfoSchema,
FrostWalletInfoSchema,
],
directory: (await StackFileSystem.applicationIsarDirectory()).path,
// inspector: kDebugMode,

View file

@ -163,6 +163,7 @@ enum AddressType {
spark,
stellar,
tezos,
frostMS,
;
String get readableName {
@ -193,6 +194,8 @@ enum AddressType {
return "Stellar";
case AddressType.tezos:
return "Tezos";
case AddressType.frostMS:
return "FrostMS";
}
}
}

View file

@ -266,6 +266,7 @@ const _AddresstypeEnumValueMap = {
'spark': 10,
'stellar': 11,
'tezos': 12,
'frostMS': 13,
};
const _AddresstypeValueEnumMap = {
0: AddressType.p2pkh,
@ -281,6 +282,7 @@ const _AddresstypeValueEnumMap = {
10: AddressType.spark,
11: AddressType.stellar,
12: AddressType.tezos,
13: AddressType.frostMS,
};
Id _addressGetId(Address object) {

View file

@ -0,0 +1,343 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/home_view/home_view.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
import 'package:stackwallet/providers/db/main_db_provider.dart';
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
import 'package:stackwallet/providers/global/node_service_provider.dart';
import 'package:stackwallet/providers/global/prefs_provider.dart';
import 'package:stackwallet/providers/global/secure_store_provider.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/services/frost.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart';
import 'package:stackwallet/wallets/wallet/wallet.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/detail_item.dart';
import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.dart';
import 'package:stackwallet/widgets/loading_indicator.dart';
import '../../../../wallets/isar/models/wallet_info.dart';
class ConfirmNewFrostMSWalletCreationView extends ConsumerStatefulWidget {
const ConfirmNewFrostMSWalletCreationView({
super.key,
required this.walletName,
required this.coin,
});
static const String routeName = "/confirmNewFrostMSWalletCreationView";
final String walletName;
final Coin coin;
@override
ConsumerState<ConfirmNewFrostMSWalletCreationView> createState() =>
_ConfirmNewFrostMSWalletCreationViewState();
}
class _ConfirmNewFrostMSWalletCreationViewState
extends ConsumerState<ConfirmNewFrostMSWalletCreationView> {
late final String seed, recoveryString, serializedKeys, multisigConfig;
late final Uint8List multisigId;
@override
void initState() {
seed = ref.read(pFrostStartKeyGenData.state).state!.seed;
serializedKeys =
ref.read(pFrostCompletedKeyGenData.state).state!.serializedKeys;
recoveryString =
ref.read(pFrostCompletedKeyGenData.state).state!.recoveryString;
multisigId = ref.read(pFrostCompletedKeyGenData.state).state!.multisigId;
multisigConfig = ref.read(pFrostMultisigConfig.state).state!;
super.initState();
}
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
await showDialog<void>(
context: context,
builder: (_) => FrostInterruptionDialog(
type: FrostInterruptionDialogType.walletCreation,
popUntilOnYesRouteName:
Util.isDesktop ? DesktopHomeView.routeName : HomeView.routeName,
),
);
return false;
},
child: ConditionalParent(
condition: Util.isDesktop,
builder: (child) => DesktopScaffold(
background: Theme.of(context).extension<StackColors>()!.background,
appBar: DesktopAppBar(
isCompactHeight: false,
leading: AppBarBackButton(
onPressed: () async {
await showDialog<void>(
context: context,
builder: (_) => const FrostInterruptionDialog(
type: FrostInterruptionDialogType.walletCreation,
popUntilOnYesRouteName: DesktopHomeView.routeName,
),
);
},
),
trailing: ExitToMyStackButton(
onPressed: () async {
await showDialog<void>(
context: context,
builder: (_) => const FrostInterruptionDialog(
type: FrostInterruptionDialogType.walletCreation,
popUntilOnYesRouteName: DesktopHomeView.routeName,
),
);
},
),
),
body: SizedBox(
width: 480,
child: child,
),
),
child: ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () async {
await showDialog<void>(
context: context,
builder: (_) => const FrostInterruptionDialog(
type: FrostInterruptionDialogType.walletCreation,
popUntilOnYesRouteName: HomeView.routeName,
),
);
},
),
title: Text(
"Finalize FROST multisig wallet",
style: STextStyles.navBarTitle(context),
),
),
body: SafeArea(
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.all(16),
child: child,
),
),
),
);
},
),
),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Ensure your multisig ID matches that of each other participant",
style: STextStyles.pageTitleH2(context),
),
const _Div(),
DetailItem(
title: "ID",
detail: multisigId.toString(),
button: Util.isDesktop
? IconCopyButton(
data: multisigId.toString(),
)
: SimpleCopyButton(
data: multisigId.toString(),
),
),
const _Div(),
const _Div(),
Text(
"Back up your keys and config",
style: STextStyles.pageTitleH2(context),
),
const _Div(),
DetailItem(
title: "Multisig Config",
detail: multisigConfig,
button: Util.isDesktop
? IconCopyButton(
data: multisigConfig,
)
: SimpleCopyButton(
data: multisigConfig,
),
),
const _Div(),
DetailItem(
title: "Keys",
detail: serializedKeys,
button: Util.isDesktop
? IconCopyButton(
data: serializedKeys,
)
: SimpleCopyButton(
data: serializedKeys,
),
),
if (!Util.isDesktop) const Spacer(),
const _Div(),
PrimaryButton(
label: "Confirm",
onPressed: () async {
bool progressPopped = false;
try {
unawaited(
showDialog<dynamic>(
context: context,
barrierDismissible: false,
useSafeArea: true,
builder: (ctx) {
return const Center(
child: LoadingIndicator(
width: 50,
height: 50,
),
);
},
),
);
final info = WalletInfo.createNew(
coin: widget.coin,
name: widget.walletName,
);
final wallet = await Wallet.create(
walletInfo: info,
mainDB: ref.read(mainDBProvider),
secureStorageInterface: ref.read(secureStoreProvider),
nodeService: ref.read(nodeServiceChangeNotifierProvider),
prefs: ref.read(prefsChangeNotifierProvider),
);
await (wallet as BitcoinFrostWallet).initializeNewFrost(
mnemonic: seed,
multisigConfig: multisigConfig,
recoveryString: recoveryString,
serializedKeys: serializedKeys,
multisigId: multisigId,
myName: ref.read(pFrostMyName.state).state!,
participants: Frost.getParticipants(
multisigConfig:
ref.read(pFrostMultisigConfig.state).state!,
),
threshold: Frost.getThreshold(
multisigConfig:
ref.read(pFrostMultisigConfig.state).state!,
),
);
await info.setMnemonicVerified(
isar: ref.read(mainDBProvider).isar,
);
ref.read(pWallets).addWallet(wallet);
// pop progress dialog
if (mounted) {
Navigator.pop(context);
progressPopped = true;
}
if (mounted) {
if (Util.isDesktop) {
Navigator.of(context).popUntil(
ModalRoute.withName(
DesktopHomeView.routeName,
),
);
} else {
unawaited(
Navigator.of(context).pushNamedAndRemoveUntil(
HomeView.routeName,
(route) => false,
),
);
}
ref.read(pFrostMultisigConfig.state).state = null;
ref.read(pFrostStartKeyGenData.state).state = null;
ref.read(pFrostSecretSharesData.state).state = null;
unawaited(
showFloatingFlushBar(
type: FlushBarType.success,
message: "Your wallet is set up.",
iconAsset: Assets.svg.check,
context: context,
),
);
}
} catch (e, s) {
Logging.instance.log(
"$e\n$s",
level: LogLevel.Fatal,
);
// pop progress dialog
if (mounted && !progressPopped) {
Navigator.pop(context);
progressPopped = true;
}
// TODO: handle gracefully
rethrow;
}
},
),
],
),
),
),
);
}
}
class _Div extends StatelessWidget {
const _Div({super.key});
@override
Widget build(BuildContext context) {
return const SizedBox(
height: 12,
);
}
}

View file

@ -0,0 +1,285 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/share_new_multisig_config_view.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
import 'package:stackwallet/services/frost.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/conditional_parent.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/desktop/primary_button.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
class CreateNewFrostMsWalletView extends ConsumerStatefulWidget {
const CreateNewFrostMsWalletView({
super.key,
required this.walletName,
required this.coin,
});
static const String routeName = "/createNewFrostMsWalletView";
final String walletName;
final Coin coin;
@override
ConsumerState<CreateNewFrostMsWalletView> createState() =>
_NewFrostMsWalletViewState();
}
class _NewFrostMsWalletViewState
extends ConsumerState<CreateNewFrostMsWalletView> {
final _thresholdController = TextEditingController();
final _participantsController = TextEditingController();
final List<TextEditingController> controllers = [];
int _participantsCount = 0;
String _validateInputData() {
final threshold = int.tryParse(_thresholdController.text);
if (threshold == null) {
return "Choose a threshold";
}
final partsCount = int.tryParse(_participantsController.text);
if (partsCount == null) {
return "Choose total number of participants";
}
if (threshold > partsCount) {
return "Threshold cannot be greater than the number of participants";
}
if (partsCount < 2) {
return "At least two participants required";
}
if (controllers.length != partsCount) {
return "Participants count error";
}
final hasEmptyParticipants = controllers
.map((e) => e.text.isEmpty)
.reduce((value, element) => value |= element);
if (hasEmptyParticipants) {
return "Participants must not be empty";
}
if (controllers.length != controllers.map((e) => e.text).toSet().length) {
return "Duplicate participant name found";
}
return "valid";
}
void _participantsCountChanged(String newValue) {
final count = int.tryParse(newValue);
if (count != null) {
if (count > _participantsCount) {
for (int i = _participantsCount; i < count; i++) {
controllers.add(TextEditingController());
}
_participantsCount = count;
setState(() {});
} else if (count < _participantsCount) {
for (int i = _participantsCount; i > count; i--) {
final last = controllers.removeLast();
last.dispose();
}
_participantsCount = count;
setState(() {});
}
}
}
@override
void dispose() {
_thresholdController.dispose();
_participantsController.dispose();
for (final e in controllers) {
e.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return ConditionalParent(
condition: Util.isDesktop,
builder: (child) => DesktopScaffold(
background: Theme.of(context).extension<StackColors>()!.background,
appBar: const DesktopAppBar(
isCompactHeight: false,
leading: AppBarBackButton(),
trailing: ExitToMyStackButton(),
),
body: SizedBox(
width: 480,
child: child,
),
),
child: ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () {
Navigator.of(context).pop();
},
),
title: Text(
"New FROST multisig config",
style: STextStyles.navBarTitle(context),
),
),
body: SafeArea(
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.all(16),
child: child,
),
),
),
);
},
),
),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Threshold",
style: STextStyles.label(context),
),
const SizedBox(
height: 10,
),
TextField(
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
controller: _thresholdController,
),
const SizedBox(
height: 16,
),
Text(
"Number of participants",
style: STextStyles.label(context),
),
const SizedBox(
height: 10,
),
TextField(
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
controller: _participantsController,
onChanged: _participantsCountChanged,
),
const SizedBox(
height: 16,
),
if (controllers.isNotEmpty)
Text(
"My name",
style: STextStyles.label(context),
),
if (controllers.isNotEmpty)
const SizedBox(
height: 10,
),
if (controllers.isNotEmpty)
TextField(
controller: controllers.first,
),
if (controllers.length > 1)
const SizedBox(
height: 16,
),
if (controllers.length > 1)
Text(
"Remaining participants",
style: STextStyles.label(context),
),
if (controllers.length > 1)
Column(
children: [
for (int i = 1; i < controllers.length; i++)
Padding(
padding: const EdgeInsets.only(
top: 10,
),
child: TextField(
controller: controllers[i],
),
),
],
),
if (!Util.isDesktop) const Spacer(),
const SizedBox(
height: 16,
),
PrimaryButton(
label: "Generate",
onPressed: () async {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
}
final validationMessage = _validateInputData();
if (validationMessage != "valid") {
return await showDialog<void>(
context: context,
builder: (_) => StackOkDialog(
title: validationMessage,
desktopPopRootNavigator: Util.isDesktop,
),
);
}
final config = Frost.createMultisigConfig(
name: controllers.first.text,
threshold: int.parse(_thresholdController.text),
participants: controllers.map((e) => e.text).toList(),
);
ref.read(pFrostMyName.notifier).state = controllers.first.text;
ref.read(pFrostMultisigConfig.notifier).state = config;
await Navigator.of(context).pushNamed(
ShareNewMultisigConfigView.routeName,
arguments: (
walletName: widget.walletName,
coin: widget.coin,
),
);
},
),
],
),
),
);
}
}

View file

@ -0,0 +1,443 @@
import 'package:barcode_scan2/barcode_scan2.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/frost_share_shares_view.dart';
import 'package:stackwallet/pages/home_view/home_view.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
import 'package:stackwallet/services/frost.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/detail_item.dart';
import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.dart';
import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart';
import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart';
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:stackwallet/widgets/textfield_icon_button.dart';
class FrostShareCommitmentsView extends ConsumerStatefulWidget {
const FrostShareCommitmentsView({
super.key,
required this.walletName,
required this.coin,
});
static const String routeName = "/frostShareCommitmentsView";
final String walletName;
final Coin coin;
@override
ConsumerState<FrostShareCommitmentsView> createState() =>
_FrostShareCommitmentsViewState();
}
class _FrostShareCommitmentsViewState
extends ConsumerState<FrostShareCommitmentsView> {
final List<TextEditingController> controllers = [];
final List<FocusNode> focusNodes = [];
late final List<String> participants;
late final String myCommitment;
late final int myIndex;
final List<bool> fieldIsEmptyFlags = [];
@override
void initState() {
participants = Frost.getParticipants(
multisigConfig: ref.read(pFrostMultisigConfig.state).state!,
);
myIndex = participants.indexOf(ref.read(pFrostMyName.state).state!);
myCommitment = ref.read(pFrostStartKeyGenData.state).state!.commitments;
// temporarily remove my name
participants.removeAt(myIndex);
for (int i = 0; i < participants.length; i++) {
controllers.add(TextEditingController());
focusNodes.add(FocusNode());
fieldIsEmptyFlags.add(true);
}
super.initState();
}
@override
void dispose() {
for (int i = 0; i < controllers.length; i++) {
controllers[i].dispose();
}
for (int i = 0; i < focusNodes.length; i++) {
focusNodes[i].dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
await showDialog<void>(
context: context,
builder: (_) => FrostInterruptionDialog(
type: FrostInterruptionDialogType.walletCreation,
popUntilOnYesRouteName:
Util.isDesktop ? DesktopHomeView.routeName : HomeView.routeName,
),
);
return false;
},
child: ConditionalParent(
condition: Util.isDesktop,
builder: (child) => DesktopScaffold(
background: Theme.of(context).extension<StackColors>()!.background,
appBar: DesktopAppBar(
isCompactHeight: false,
leading: AppBarBackButton(
onPressed: () async {
await showDialog<void>(
context: context,
builder: (_) => const FrostInterruptionDialog(
type: FrostInterruptionDialogType.walletCreation,
popUntilOnYesRouteName: DesktopHomeView.routeName,
),
);
},
),
trailing: ExitToMyStackButton(
onPressed: () async {
await showDialog<void>(
context: context,
builder: (_) => const FrostInterruptionDialog(
type: FrostInterruptionDialogType.walletCreation,
popUntilOnYesRouteName: DesktopHomeView.routeName,
),
);
},
),
),
body: SizedBox(
width: 480,
child: child,
),
),
child: ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () async {
await showDialog<void>(
context: context,
builder: (_) => const FrostInterruptionDialog(
type: FrostInterruptionDialogType.walletCreation,
popUntilOnYesRouteName: HomeView.routeName,
),
);
},
),
title: Text(
"Commitments",
style: STextStyles.navBarTitle(context),
),
),
body: SafeArea(
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.all(16),
child: child,
),
),
),
);
},
),
),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: 220,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
QrImageView(
data: myCommitment,
size: 220,
backgroundColor: Theme.of(context)
.extension<StackColors>()!
.background,
foregroundColor: Theme.of(context)
.extension<StackColors>()!
.accentColorDark,
),
],
),
),
const _Div(),
DetailItem(
title: "My name",
detail: ref.watch(pFrostMyName.state).state!,
),
const _Div(),
DetailItem(
title: "My commitment",
detail: myCommitment,
button: Util.isDesktop
? IconCopyButton(
data: myCommitment,
)
: SimpleCopyButton(
data: myCommitment,
),
),
const _Div(),
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (int i = 0; i < participants.length; i++)
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
key: Key("frostCommitmentsTextFieldKey_$i"),
controller: controllers[i],
focusNode: focusNodes[i],
readOnly: false,
autocorrect: false,
enableSuggestions: false,
style: STextStyles.field(context),
onChanged: (_) {
setState(() {
fieldIsEmptyFlags[i] =
controllers[i].text.isEmpty;
});
},
decoration: standardInputDecoration(
"Enter ${participants[i]}'s commitment",
focusNodes[i],
context,
).copyWith(
contentPadding: const EdgeInsets.only(
left: 16,
top: 6,
bottom: 8,
right: 5,
),
suffixIcon: Padding(
padding: fieldIsEmptyFlags[i]
? const EdgeInsets.only(right: 8)
: const EdgeInsets.only(right: 0),
child: UnconstrainedBox(
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceAround,
children: [
!fieldIsEmptyFlags[i]
? TextFieldIconButton(
semanticsLabel:
"Clear Button. Clears The Commitment Field Input.",
key: Key(
"frostCommitmentsClearButtonKey_$i"),
onTap: () {
controllers[i].text = "";
setState(() {
fieldIsEmptyFlags[i] = true;
});
},
child: const XIcon(),
)
: TextFieldIconButton(
semanticsLabel:
"Paste Button. Pastes From Clipboard To Commitment Field Input.",
key: Key(
"frostCommitmentsPasteButtonKey_$i"),
onTap: () async {
final ClipboardData? data =
await Clipboard.getData(
Clipboard.kTextPlain);
if (data?.text != null &&
data!.text!.isNotEmpty) {
controllers[i].text =
data.text!.trim();
}
setState(() {
fieldIsEmptyFlags[i] =
controllers[i]
.text
.isEmpty;
});
},
child: fieldIsEmptyFlags[i]
? const ClipboardIcon()
: const XIcon(),
),
if (fieldIsEmptyFlags[i])
TextFieldIconButton(
semanticsLabel:
"Scan QR Button. Opens Camera For Scanning QR Code.",
key: Key(
"frostCommitmentsScanQrButtonKey_$i"),
onTap: () async {
try {
if (FocusScope.of(context)
.hasFocus) {
FocusScope.of(context)
.unfocus();
await Future<void>.delayed(
const Duration(
milliseconds: 75));
}
final qrResult =
await BarcodeScanner.scan();
controllers[i].text =
qrResult.rawContent;
setState(() {
fieldIsEmptyFlags[i] =
controllers[i]
.text
.isEmpty;
});
} on PlatformException catch (e, s) {
Logging.instance.log(
"Failed to get camera permissions while trying to scan qr code: $e\n$s",
level: LogLevel.Warning,
);
}
},
child: const QrCodeIcon(),
)
],
),
),
),
),
),
),
),
],
),
],
),
if (!Util.isDesktop) const Spacer(),
const _Div(),
PrimaryButton(
label: "Generate shares",
enabled: !fieldIsEmptyFlags.reduce((v, e) => v |= e),
onPressed: () async {
// check for empty commitments
if (controllers
.map((e) => e.text.isEmpty)
.reduce((value, element) => value |= element)) {
return await showDialog<void>(
context: context,
builder: (_) => StackOkDialog(
title: "Missing commitments",
desktopPopRootNavigator: Util.isDesktop,
),
);
}
// collect commitment strings and insert my own at the correct index
final commitments = controllers.map((e) => e.text).toList();
commitments.insert(myIndex, myCommitment);
try {
ref.read(pFrostSecretSharesData.notifier).state =
Frost.generateSecretShares(
multisigConfigWithNamePtr: ref
.read(pFrostStartKeyGenData.state)
.state!
.multisigConfigWithNamePtr,
mySeed: ref.read(pFrostStartKeyGenData.state).state!.seed,
secretShareMachineWrapperPtr: ref
.read(pFrostStartKeyGenData.state)
.state!
.secretShareMachineWrapperPtr,
commitments: commitments,
);
await Navigator.of(context).pushNamed(
FrostShareSharesView.routeName,
arguments: (
walletName: widget.walletName,
coin: widget.coin,
),
);
} catch (e, s) {
Logging.instance.log(
"$e\n$s",
level: LogLevel.Fatal,
);
return await showDialog<void>(
context: context,
builder: (_) => StackOkDialog(
title: "Failed to generate shares",
message: e.toString(),
desktopPopRootNavigator: Util.isDesktop,
),
);
}
},
),
],
),
),
),
);
}
}
class _Div extends StatelessWidget {
const _Div({super.key});
@override
Widget build(BuildContext context) {
return const SizedBox(
height: 12,
);
}
}

View file

@ -0,0 +1,409 @@
import 'package:barcode_scan2/barcode_scan2.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/confirm_new_frost_ms_wallet_creation_view.dart';
import 'package:stackwallet/pages/home_view/home_view.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
import 'package:stackwallet/services/frost.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/detail_item.dart';
import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.dart';
import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart';
import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart';
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:stackwallet/widgets/textfield_icon_button.dart';
class FrostShareSharesView extends ConsumerStatefulWidget {
const FrostShareSharesView({
super.key,
required this.walletName,
required this.coin,
});
static const String routeName = "/frostShareSharesView";
final String walletName;
final Coin coin;
@override
ConsumerState<FrostShareSharesView> createState() =>
_FrostShareSharesViewState();
}
class _FrostShareSharesViewState extends ConsumerState<FrostShareSharesView> {
final List<TextEditingController> controllers = [];
final List<FocusNode> focusNodes = [];
late final List<String> participants;
late final String myShare;
late final int myIndex;
final List<bool> fieldIsEmptyFlags = [];
@override
void initState() {
participants = Frost.getParticipants(
multisigConfig: ref.read(pFrostMultisigConfig.state).state!,
);
myIndex = participants.indexOf(ref.read(pFrostMyName.state).state!);
myShare = ref.read(pFrostSecretSharesData.state).state!.share;
// temporarily remove my name. Added back later
participants.removeAt(myIndex);
for (int i = 0; i < participants.length; i++) {
controllers.add(TextEditingController());
focusNodes.add(FocusNode());
fieldIsEmptyFlags.add(true);
}
super.initState();
}
@override
void dispose() {
for (int i = 0; i < controllers.length; i++) {
controllers[i].dispose();
}
for (int i = 0; i < focusNodes.length; i++) {
focusNodes[i].dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
await showDialog<void>(
context: context,
builder: (_) => FrostInterruptionDialog(
type: FrostInterruptionDialogType.walletCreation,
popUntilOnYesRouteName:
Util.isDesktop ? DesktopHomeView.routeName : HomeView.routeName,
),
);
return false;
},
child: ConditionalParent(
condition: Util.isDesktop,
builder: (child) => DesktopScaffold(
background: Theme.of(context).extension<StackColors>()!.background,
appBar: DesktopAppBar(
isCompactHeight: false,
leading: AppBarBackButton(
onPressed: () async {
await showDialog<void>(
context: context,
builder: (_) => const FrostInterruptionDialog(
type: FrostInterruptionDialogType.walletCreation,
popUntilOnYesRouteName: DesktopHomeView.routeName,
),
);
},
),
trailing: ExitToMyStackButton(
onPressed: () async {
await showDialog<void>(
context: context,
builder: (_) => const FrostInterruptionDialog(
type: FrostInterruptionDialogType.walletCreation,
popUntilOnYesRouteName: DesktopHomeView.routeName,
),
);
},
),
),
body: SizedBox(
width: 480,
child: child,
),
),
child: ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () async {
await showDialog<void>(
context: context,
builder: (_) => const FrostInterruptionDialog(
type: FrostInterruptionDialogType.walletCreation,
popUntilOnYesRouteName: HomeView.routeName,
),
);
},
),
title: Text(
"Generate shares",
style: STextStyles.navBarTitle(context),
),
),
body: SafeArea(
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.all(16),
child: child,
),
),
),
);
},
),
),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: 220,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
QrImageView(
data: myShare,
size: 220,
backgroundColor: Theme.of(context)
.extension<StackColors>()!
.background,
foregroundColor: Theme.of(context)
.extension<StackColors>()!
.accentColorDark,
),
],
),
),
const _Div(),
DetailItem(
title: "My name",
detail: ref.watch(pFrostMyName.state).state!,
),
const _Div(),
DetailItem(
title: "My share",
detail: myShare,
button: Util.isDesktop
? IconCopyButton(
data: myShare,
)
: SimpleCopyButton(
data: myShare,
),
),
const _Div(),
for (int i = 0; i < participants.length; i++)
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
key: Key("frSharesTextFieldKey_$i"),
controller: controllers[i],
focusNode: focusNodes[i],
readOnly: false,
autocorrect: false,
enableSuggestions: false,
style: STextStyles.field(context),
decoration: standardInputDecoration(
"Enter ${participants[i]}'s share",
focusNodes[i],
context,
).copyWith(
contentPadding: const EdgeInsets.only(
left: 16,
top: 6,
bottom: 8,
right: 5,
),
suffixIcon: Padding(
padding: fieldIsEmptyFlags[i]
? const EdgeInsets.only(right: 8)
: const EdgeInsets.only(right: 0),
child: UnconstrainedBox(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
!fieldIsEmptyFlags[i]
? TextFieldIconButton(
semanticsLabel:
"Clear Button. Clears The Share Field Input.",
key: Key("frSharesClearButtonKey_$i"),
onTap: () {
controllers[i].text = "";
setState(() {
fieldIsEmptyFlags[i] = true;
});
},
child: const XIcon(),
)
: TextFieldIconButton(
semanticsLabel:
"Paste Button. Pastes From Clipboard To Share Field Input.",
key: Key("frSharesPasteButtonKey_$i"),
onTap: () async {
final ClipboardData? data =
await Clipboard.getData(
Clipboard.kTextPlain);
if (data?.text != null &&
data!.text!.isNotEmpty) {
controllers[i].text =
data.text!.trim();
}
setState(() {
fieldIsEmptyFlags[i] =
controllers[i].text.isEmpty;
});
},
child: fieldIsEmptyFlags[i]
? const ClipboardIcon()
: const XIcon(),
),
if (fieldIsEmptyFlags[i])
TextFieldIconButton(
semanticsLabel:
"Scan QR Button. Opens Camera For Scanning QR Code.",
key: Key("frSharesScanQrButtonKey_$i"),
onTap: () async {
try {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(
const Duration(milliseconds: 75));
}
final qrResult =
await BarcodeScanner.scan();
controllers[i].text =
qrResult.rawContent;
setState(() {
fieldIsEmptyFlags[i] =
controllers[i].text.isEmpty;
});
} on PlatformException catch (e, s) {
Logging.instance.log(
"Failed to get camera permissions while trying to scan qr code: $e\n$s",
level: LogLevel.Warning,
);
}
},
child: const QrCodeIcon(),
)
],
),
),
),
),
),
),
),
if (!Util.isDesktop) const Spacer(),
const _Div(),
PrimaryButton(
label: "Generate",
onPressed: () async {
// check for empty commitments
if (controllers
.map((e) => e.text.isEmpty)
.reduce((value, element) => value |= element)) {
return await showDialog<void>(
context: context,
builder: (_) => StackOkDialog(
title: "Missing shares",
desktopPopRootNavigator: Util.isDesktop,
),
);
}
// collect commitment strings and insert my own at the correct index
final shares = controllers.map((e) => e.text).toList();
shares.insert(myIndex, myShare);
try {
ref.read(pFrostCompletedKeyGenData.notifier).state =
Frost.completeKeyGeneration(
multisigConfigWithNamePtr: ref
.read(pFrostStartKeyGenData.state)
.state!
.multisigConfigWithNamePtr,
secretSharesResPtr: ref
.read(pFrostSecretSharesData.state)
.state!
.secretSharesResPtr,
shares: shares,
);
await Navigator.of(context).pushNamed(
ConfirmNewFrostMSWalletCreationView.routeName,
arguments: (
walletName: widget.walletName,
coin: widget.coin,
),
);
} catch (e, s) {
Logging.instance.log(
"$e\n$s",
level: LogLevel.Fatal,
);
return await showDialog<void>(
context: context,
builder: (_) => StackOkDialog(
title: "Failed to complete key generation",
desktopPopRootNavigator: Util.isDesktop,
),
);
}
},
),
],
),
),
),
);
}
}
class _Div extends StatelessWidget {
const _Div({super.key});
@override
Widget build(BuildContext context) {
return const SizedBox(
height: 12,
);
}
}

View file

@ -0,0 +1,386 @@
import 'package:barcode_scan2/barcode_scan2.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/frost_share_commitments_view.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
import 'package:stackwallet/services/frost.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/conditional_parent.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/desktop/primary_button.dart';
import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart';
import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart';
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:stackwallet/widgets/textfield_icon_button.dart';
class ImportNewFrostMsWalletView extends ConsumerStatefulWidget {
const ImportNewFrostMsWalletView({
super.key,
required this.walletName,
required this.coin,
});
static const String routeName = "/importNewFrostMsWalletView";
final String walletName;
final Coin coin;
@override
ConsumerState<ImportNewFrostMsWalletView> createState() =>
_ImportNewFrostMsWalletViewState();
}
class _ImportNewFrostMsWalletViewState
extends ConsumerState<ImportNewFrostMsWalletView> {
late final TextEditingController myNameFieldController, configFieldController;
late final FocusNode myNameFocusNode, configFocusNode;
bool _nameEmpty = true, _configEmpty = true;
@override
void initState() {
myNameFieldController = TextEditingController();
configFieldController = TextEditingController();
myNameFocusNode = FocusNode();
configFocusNode = FocusNode();
super.initState();
}
@override
void dispose() {
myNameFieldController.dispose();
configFieldController.dispose();
myNameFocusNode.dispose();
configFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ConditionalParent(
condition: Util.isDesktop,
builder: (child) => DesktopScaffold(
background: Theme.of(context).extension<StackColors>()!.background,
appBar: const DesktopAppBar(
isCompactHeight: false,
leading: AppBarBackButton(),
trailing: ExitToMyStackButton(),
),
body: SizedBox(
width: 480,
child: child,
),
),
child: ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () {
Navigator.of(context).pop();
},
),
title: Text(
"Import FROST multisig config",
style: STextStyles.navBarTitle(context),
),
),
body: SafeArea(
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.all(16),
child: child,
),
),
),
);
},
),
),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 16,
),
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
key: const Key("frMyNameTextFieldKey"),
controller: myNameFieldController,
onChanged: (_) {
setState(() {
_nameEmpty = myNameFieldController.text.isEmpty;
});
},
focusNode: myNameFocusNode,
readOnly: false,
autocorrect: false,
enableSuggestions: false,
style: STextStyles.field(context),
decoration: standardInputDecoration(
"My name",
myNameFocusNode,
context,
).copyWith(
contentPadding: const EdgeInsets.only(
left: 16,
top: 6,
bottom: 8,
right: 5,
),
suffixIcon: Padding(
padding: _nameEmpty
? const EdgeInsets.only(right: 8)
: const EdgeInsets.only(right: 0),
child: UnconstrainedBox(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
!_nameEmpty
? TextFieldIconButton(
semanticsLabel:
"Clear Button. Clears The Config Field.",
key: const Key("frMyNameClearButtonKey"),
onTap: () {
myNameFieldController.text = "";
setState(() {
_nameEmpty = true;
});
},
child: const XIcon(),
)
: TextFieldIconButton(
semanticsLabel:
"Paste Button. Pastes From Clipboard To Name Field.",
key: const Key("frMyNamePasteButtonKey"),
onTap: () async {
final ClipboardData? data =
await Clipboard.getData(
Clipboard.kTextPlain);
if (data?.text != null &&
data!.text!.isNotEmpty) {
myNameFieldController.text =
data.text!.trim();
}
setState(() {
_nameEmpty =
myNameFieldController.text.isEmpty;
});
},
child: _nameEmpty
? const ClipboardIcon()
: const XIcon(),
),
],
),
),
),
),
),
),
const SizedBox(
height: 16,
),
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
key: const Key("frConfigTextFieldKey"),
controller: configFieldController,
onChanged: (_) {
setState(() {
_configEmpty = configFieldController.text.isEmpty;
});
},
focusNode: configFocusNode,
readOnly: false,
autocorrect: false,
enableSuggestions: false,
style: STextStyles.field(context),
decoration: standardInputDecoration(
"Enter config",
configFocusNode,
context,
).copyWith(
contentPadding: const EdgeInsets.only(
left: 16,
top: 6,
bottom: 8,
right: 5,
),
suffixIcon: Padding(
padding: _configEmpty
? const EdgeInsets.only(right: 8)
: const EdgeInsets.only(right: 0),
child: UnconstrainedBox(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
!_configEmpty
? TextFieldIconButton(
semanticsLabel:
"Clear Button. Clears The Config Field.",
key: const Key("frConfigClearButtonKey"),
onTap: () {
configFieldController.text = "";
setState(() {
_configEmpty = true;
});
},
child: const XIcon(),
)
: TextFieldIconButton(
semanticsLabel:
"Paste Button. Pastes From Clipboard To Config Field Input.",
key: const Key("frConfigPasteButtonKey"),
onTap: () async {
final ClipboardData? data =
await Clipboard.getData(
Clipboard.kTextPlain);
if (data?.text != null &&
data!.text!.isNotEmpty) {
configFieldController.text =
data.text!.trim();
}
setState(() {
_configEmpty =
configFieldController.text.isEmpty;
});
},
child: _configEmpty
? const ClipboardIcon()
: const XIcon(),
),
if (_configEmpty)
TextFieldIconButton(
semanticsLabel:
"Scan QR Button. Opens Camera For Scanning QR Code.",
key: const Key("frConfigScanQrButtonKey"),
onTap: () async {
try {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(
const Duration(milliseconds: 75));
}
final qrResult = await BarcodeScanner.scan();
configFieldController.text =
qrResult.rawContent;
setState(() {
_configEmpty =
configFieldController.text.isEmpty;
});
} on PlatformException catch (e, s) {
Logging.instance.log(
"Failed to get camera permissions while trying to scan qr code: $e\n$s",
level: LogLevel.Warning,
);
}
},
child: const QrCodeIcon(),
)
],
),
),
),
),
),
),
const SizedBox(
height: 16,
),
if (!Util.isDesktop) const Spacer(),
const SizedBox(
height: 16,
),
PrimaryButton(
label: "Start key generation",
enabled: !_nameEmpty && !_configEmpty,
onPressed: () async {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
}
final config = configFieldController.text;
if (!Frost.validateEncodedMultisigConfig(
encodedConfig: config)) {
return await showDialog<void>(
context: context,
builder: (_) => StackOkDialog(
title: "Invalid config",
desktopPopRootNavigator: Util.isDesktop,
),
);
}
if (!Frost.getParticipants(multisigConfig: config)
.contains(myNameFieldController.text)) {
return await showDialog<void>(
context: context,
builder: (_) => StackOkDialog(
title: "My name not found in config participants",
desktopPopRootNavigator: Util.isDesktop,
),
);
}
ref.read(pFrostMyName.state).state = myNameFieldController.text;
ref.read(pFrostMultisigConfig.notifier).state = config;
ref.read(pFrostStartKeyGenData.state).state =
Frost.startKeyGeneration(
multisigConfig: ref.read(pFrostMultisigConfig.state).state!,
myName: ref.read(pFrostMyName.state).state!,
);
await Navigator.of(context).pushNamed(
FrostShareCommitmentsView.routeName,
arguments: (
walletName: widget.walletName,
coin: widget.coin,
),
);
},
),
],
),
),
);
}
}

View file

@ -0,0 +1,162 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/frost_share_commitments_view.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
import 'package:stackwallet/services/frost.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/detail_item.dart';
class ShareNewMultisigConfigView extends ConsumerStatefulWidget {
const ShareNewMultisigConfigView({
super.key,
required this.walletName,
required this.coin,
});
static const String routeName = "/shareNewMultisigConfigView";
final String walletName;
final Coin coin;
@override
ConsumerState<ShareNewMultisigConfigView> createState() =>
_ShareNewMultisigConfigViewState();
}
class _ShareNewMultisigConfigViewState
extends ConsumerState<ShareNewMultisigConfigView> {
@override
Widget build(BuildContext context) {
return ConditionalParent(
condition: Util.isDesktop,
builder: (child) => DesktopScaffold(
background: Theme.of(context).extension<StackColors>()!.background,
appBar: const DesktopAppBar(
isCompactHeight: false,
leading: AppBarBackButton(),
trailing: ExitToMyStackButton(),
),
body: SizedBox(
width: 480,
child: child,
),
),
child: ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () {
Navigator.of(context).pop();
},
),
title: Text(
"Multisig config",
style: STextStyles.navBarTitle(context),
),
),
body: SafeArea(
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.all(16),
child: child,
),
),
),
);
},
),
),
),
),
child: Column(
children: [
if (!Util.isDesktop) const Spacer(),
SizedBox(
height: 220,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
QrImageView(
data:
ref.watch(pFrostMultisigConfig.state).state ?? "Error",
size: 220,
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
foregroundColor: Theme.of(context)
.extension<StackColors>()!
.accentColorDark,
),
],
),
),
const SizedBox(
height: 32,
),
DetailItem(
title: "Encoded config",
detail: ref.watch(pFrostMultisigConfig.state).state ?? "Error",
button: Util.isDesktop
? IconCopyButton(
data: ref.watch(pFrostMultisigConfig.state).state ??
"Error",
)
: SimpleCopyButton(
data: ref.watch(pFrostMultisigConfig.state).state ??
"Error",
),
),
SizedBox(
height: Util.isDesktop ? 64 : 16,
),
if (!Util.isDesktop)
const Spacer(
flex: 2,
),
PrimaryButton(
label: "Start key generation",
onPressed: () async {
ref.read(pFrostStartKeyGenData.notifier).state =
Frost.startKeyGeneration(
multisigConfig: ref.watch(pFrostMultisigConfig.state).state!,
myName: ref.read(pFrostMyName.state).state!,
);
await Navigator.of(context).pushNamed(
FrostShareCommitmentsView.routeName,
arguments: (
walletName: widget.walletName,
coin: widget.coin,
),
);
},
),
],
),
),
);
}
}

View file

@ -0,0 +1,480 @@
import 'dart:async';
import 'package:barcode_scan2/barcode_scan2.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:frostdart/frostdart.dart' as frost;
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/home_view/home_view.dart';
import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
import 'package:stackwallet/providers/db/main_db_provider.dart';
import 'package:stackwallet/providers/global/node_service_provider.dart';
import 'package:stackwallet/providers/global/prefs_provider.dart';
import 'package:stackwallet/providers/global/secure_store_provider.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/services/frost.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/show_loading.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart';
import 'package:stackwallet/wallets/isar/models/wallet_info.dart';
import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart';
import 'package:stackwallet/wallets/wallet/wallet.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/conditional_parent.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/desktop/primary_button.dart';
import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart';
import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart';
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:stackwallet/widgets/textfield_icon_button.dart';
class RestoreFrostMsWalletView extends ConsumerStatefulWidget {
const RestoreFrostMsWalletView({
super.key,
required this.walletName,
required this.coin,
});
static const String routeName = "/restoreFrostMsWalletView";
final String walletName;
final Coin coin;
@override
ConsumerState<RestoreFrostMsWalletView> createState() =>
_RestoreFrostMsWalletViewState();
}
class _RestoreFrostMsWalletViewState
extends ConsumerState<RestoreFrostMsWalletView> {
late final TextEditingController keysFieldController, configFieldController;
late final FocusNode keysFocusNode, configFocusNode;
bool _keysEmpty = true, _configEmpty = true;
bool _restoreButtonLock = false;
Future<Wallet> _createWalletAndRecover() async {
final keys = keysFieldController.text;
final config = configFieldController.text;
final myNameIndex = frost.getParticipantIndexFromKeys(serializedKeys: keys);
final participants = Frost.getParticipants(multisigConfig: config);
final myName = participants[myNameIndex];
final info = WalletInfo.createNew(
coin: widget.coin,
name: widget.walletName,
);
final wallet = await Wallet.create(
walletInfo: info,
mainDB: ref.read(mainDBProvider),
secureStorageInterface: ref.read(secureStoreProvider),
nodeService: ref.read(nodeServiceChangeNotifierProvider),
prefs: ref.read(prefsChangeNotifierProvider),
);
final frostInfo = FrostWalletInfo(
walletId: info.walletId,
knownSalts: [],
participants: participants,
myName: myName,
threshold: frost.multisigThreshold(
multisigConfig: config,
),
);
await ref.read(mainDBProvider).isar.writeTxn(() async {
await ref.read(mainDBProvider).isar.frostWalletInfo.put(frostInfo);
});
await (wallet as BitcoinFrostWallet).recover(
serializedKeys: keys,
multisigConfig: config,
isRescan: false,
);
await info.setMnemonicVerified(
isar: ref.read(mainDBProvider).isar,
);
return wallet;
}
Future<void> _restore() async {
if (_restoreButtonLock) {
return;
}
_restoreButtonLock = true;
try {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
}
Exception? ex;
final wallet = await showLoading(
whileFuture: _createWalletAndRecover(),
context: context,
message: "Restoring wallet...",
isDesktop: Util.isDesktop,
onException: (e) {
ex = e;
},
);
if (ex != null) {
throw ex!;
}
ref.read(pWallets).addWallet(wallet!);
if (mounted) {
if (Util.isDesktop) {
Navigator.of(context).popUntil(
ModalRoute.withName(
DesktopHomeView.routeName,
),
);
} else {
unawaited(
Navigator.of(context).pushNamedAndRemoveUntil(
HomeView.routeName,
(route) => false,
),
);
}
unawaited(
showFloatingFlushBar(
type: FlushBarType.success,
message: "Your wallet is set up.",
iconAsset: Assets.svg.check,
context: context,
),
);
}
} catch (e, s) {
Logging.instance.log(
"$e\n$s",
level: LogLevel.Fatal,
);
if (mounted) {
await showDialog<void>(
context: context,
builder: (_) => StackOkDialog(
title: "Failed to restore",
message: e.toString(),
desktopPopRootNavigator: Util.isDesktop,
),
);
}
} finally {
_restoreButtonLock = false;
}
}
@override
void initState() {
keysFieldController = TextEditingController();
configFieldController = TextEditingController();
keysFocusNode = FocusNode();
configFocusNode = FocusNode();
super.initState();
}
@override
void dispose() {
keysFieldController.dispose();
configFieldController.dispose();
keysFocusNode.dispose();
configFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ConditionalParent(
condition: Util.isDesktop,
builder: (child) => DesktopScaffold(
background: Theme.of(context).extension<StackColors>()!.background,
appBar: const DesktopAppBar(
isCompactHeight: false,
leading: AppBarBackButton(),
trailing: ExitToMyStackButton(),
),
body: SizedBox(
width: 480,
child: child,
),
),
child: ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () {
Navigator.of(context).pop();
},
),
title: Text(
"Restore FROST multisig wallet",
style: STextStyles.navBarTitle(context),
),
),
body: SafeArea(
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.all(16),
child: child,
),
),
),
);
},
),
),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 16,
),
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
key: const Key("frMyNameTextFieldKey"),
controller: keysFieldController,
onChanged: (_) {
setState(() {
_keysEmpty = keysFieldController.text.isEmpty;
});
},
focusNode: keysFocusNode,
readOnly: false,
autocorrect: false,
enableSuggestions: false,
style: STextStyles.field(context),
decoration: standardInputDecoration(
"Keys",
keysFocusNode,
context,
).copyWith(
contentPadding: const EdgeInsets.only(
left: 16,
top: 6,
bottom: 8,
right: 5,
),
suffixIcon: Padding(
padding: _keysEmpty
? const EdgeInsets.only(right: 8)
: const EdgeInsets.only(right: 0),
child: UnconstrainedBox(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
!_keysEmpty
? TextFieldIconButton(
semanticsLabel:
"Clear Button. Clears The Keys Field.",
key: const Key("frMyNameClearButtonKey"),
onTap: () {
keysFieldController.text = "";
setState(() {
_keysEmpty = true;
});
},
child: const XIcon(),
)
: TextFieldIconButton(
semanticsLabel:
"Paste Button. Pastes From Clipboard To Keys Field.",
key: const Key("frKeysPasteButtonKey"),
onTap: () async {
final ClipboardData? data =
await Clipboard.getData(
Clipboard.kTextPlain);
if (data?.text != null &&
data!.text!.isNotEmpty) {
keysFieldController.text =
data.text!.trim();
}
setState(() {
_keysEmpty =
keysFieldController.text.isEmpty;
});
},
child: _keysEmpty
? const ClipboardIcon()
: const XIcon(),
),
],
),
),
),
),
),
),
const SizedBox(
height: 16,
),
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
key: const Key("frConfigTextFieldKey"),
controller: configFieldController,
onChanged: (_) {
setState(() {
_configEmpty = configFieldController.text.isEmpty;
});
},
focusNode: configFocusNode,
readOnly: false,
autocorrect: false,
enableSuggestions: false,
style: STextStyles.field(context),
decoration: standardInputDecoration(
"Enter config",
configFocusNode,
context,
).copyWith(
contentPadding: const EdgeInsets.only(
left: 16,
top: 6,
bottom: 8,
right: 5,
),
suffixIcon: Padding(
padding: _configEmpty
? const EdgeInsets.only(right: 8)
: const EdgeInsets.only(right: 0),
child: UnconstrainedBox(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
!_configEmpty
? TextFieldIconButton(
semanticsLabel:
"Clear Button. Clears The Config Field.",
key: const Key("frConfigClearButtonKey"),
onTap: () {
configFieldController.text = "";
setState(() {
_configEmpty = true;
});
},
child: const XIcon(),
)
: TextFieldIconButton(
semanticsLabel:
"Paste Button. Pastes From Clipboard To Config Field Input.",
key: const Key("frConfigPasteButtonKey"),
onTap: () async {
final ClipboardData? data =
await Clipboard.getData(
Clipboard.kTextPlain);
if (data?.text != null &&
data!.text!.isNotEmpty) {
configFieldController.text =
data.text!.trim();
}
setState(() {
_configEmpty =
configFieldController.text.isEmpty;
});
},
child: _configEmpty
? const ClipboardIcon()
: const XIcon(),
),
if (_configEmpty)
TextFieldIconButton(
semanticsLabel:
"Scan QR Button. Opens Camera For Scanning QR Code.",
key: const Key("frConfigScanQrButtonKey"),
onTap: () async {
try {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(
const Duration(milliseconds: 75));
}
final qrResult = await BarcodeScanner.scan();
configFieldController.text =
qrResult.rawContent;
setState(() {
_configEmpty =
configFieldController.text.isEmpty;
});
} on PlatformException catch (e, s) {
Logging.instance.log(
"Failed to get camera permissions while trying to scan qr code: $e\n$s",
level: LogLevel.Warning,
);
}
},
child: const QrCodeIcon(),
)
],
),
),
),
),
),
),
const SizedBox(
height: 16,
),
if (!Util.isDesktop) const Spacer(),
const SizedBox(
height: 16,
),
PrimaryButton(
label: "Restore",
enabled: !_keysEmpty && !_configEmpty,
onPressed: _restore,
),
],
),
),
);
}
}

View file

@ -14,9 +14,13 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:isar/isar.dart';
import 'package:stackwallet/pages/add_wallet_views/create_or_restore_wallet_view/sub_widgets/coin_image.dart';
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart';
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/import_new_frost_ms_wallet_view.dart';
import 'package:stackwallet/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart';
import 'package:stackwallet/pages/add_wallet_views/new_wallet_options/new_wallet_options_view.dart';
import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart';
import 'package:stackwallet/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_import_resharer_config_view.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
import 'package:stackwallet/providers/db/main_db_provider.dart';
import 'package:stackwallet/providers/ui/verify_recovery_phrase/mnemonic_word_count_state_provider.dart';
@ -32,6 +36,8 @@ import 'package:stackwallet/widgets/background.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/desktop/primary_button.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/icon_widgets/dice_icon.dart';
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
@ -77,6 +83,52 @@ class _NameYourWalletViewState extends ConsumerState<NameYourWalletView> {
return name;
}
Future<void> _nextPressed() async {
final name = textEditingController.text;
if (mounted) {
// hide keyboard if has focus
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(const Duration(milliseconds: 50));
}
if (mounted) {
ref.read(mnemonicWordCountStateProvider.state).state =
Constants.possibleLengthsForCoin(coin).last;
ref.read(pNewWalletOptions.notifier).state = null;
switch (widget.addWalletType) {
case AddWalletType.New:
unawaited(
Navigator.of(context).pushNamed(
coin.hasMnemonicPassphraseSupport
? NewWalletOptionsView.routeName
: NewWalletRecoveryPhraseWarningView.routeName,
arguments: Tuple2(
name,
coin,
),
),
);
break;
case AddWalletType.Restore:
unawaited(
Navigator.of(context).pushNamed(
RestoreOptionsView.routeName,
arguments: Tuple2(
name,
coin,
),
),
);
break;
}
}
}
}
@override
void initState() {
isDesktop = Util.isDesktop;
@ -330,78 +382,104 @@ class _NameYourWalletViewState extends ConsumerState<NameYourWalletView> {
const SizedBox(
height: 32,
),
ConstrainedBox(
constraints: BoxConstraints(
minWidth: isDesktop ? 480 : 0,
minHeight: isDesktop ? 70 : 0,
if (widget.coin.isFrost)
if (widget.addWalletType == AddWalletType.Restore)
PrimaryButton(
label: "Next",
enabled: _nextEnabled,
onPressed: () async {
final name = textEditingController.text;
await Navigator.of(context).pushNamed(
RestoreFrostMsWalletView.routeName,
arguments: (
walletName: name,
coin: coin,
),
);
},
),
if (widget.coin.isFrost && widget.addWalletType == AddWalletType.New)
Column(
children: [
PrimaryButton(
label: "Create config",
enabled: _nextEnabled,
onPressed: () async {
final name = textEditingController.text;
await Navigator.of(context).pushNamed(
CreateNewFrostMsWalletView.routeName,
arguments: (
walletName: name,
coin: coin,
),
);
},
),
const SizedBox(
height: 12,
),
SecondaryButton(
label: "Import multisig config",
enabled: _nextEnabled,
onPressed: () async {
final name = textEditingController.text;
await Navigator.of(context).pushNamed(
ImportNewFrostMsWalletView.routeName,
arguments: (
walletName: name,
coin: coin,
),
);
},
),
const SizedBox(
height: 12,
),
SecondaryButton(
label: "Import resharer config",
enabled: _nextEnabled,
onPressed: () async {
final name = textEditingController.text;
await Navigator.of(context).pushNamed(
NewImportResharerConfigView.routeName,
arguments: (
walletName: name,
coin: coin,
),
);
},
),
],
),
child: TextButton(
onPressed: _nextEnabled
? () async {
final name = textEditingController.text;
if (mounted) {
// hide keyboard if has focus
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(
const Duration(milliseconds: 50));
}
if (mounted) {
ref.read(mnemonicWordCountStateProvider.state).state =
Constants.possibleLengthsForCoin(coin).last;
ref.read(pNewWalletOptions.notifier).state = null;
switch (widget.addWalletType) {
case AddWalletType.New:
unawaited(
Navigator.of(context).pushNamed(
coin.hasMnemonicPassphraseSupport
? NewWalletOptionsView.routeName
: NewWalletRecoveryPhraseWarningView
.routeName,
arguments: Tuple2(
name,
coin,
),
),
);
break;
case AddWalletType.Restore:
unawaited(
Navigator.of(context).pushNamed(
RestoreOptionsView.routeName,
arguments: Tuple2(
name,
coin,
),
),
);
break;
}
}
}
}
: null,
style: _nextEnabled
? Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonStyle(context)
: Theme.of(context)
.extension<StackColors>()!
.getPrimaryDisabledButtonStyle(context),
child: Text(
"Next",
style: isDesktop
? _nextEnabled
? STextStyles.desktopButtonEnabled(context)
: STextStyles.desktopButtonDisabled(context)
: STextStyles.button(context),
if (!widget.coin.isFrost)
ConstrainedBox(
constraints: BoxConstraints(
minWidth: isDesktop ? 480 : 0,
minHeight: isDesktop ? 70 : 0,
),
child: TextButton(
onPressed: _nextEnabled ? _nextPressed : null,
style: _nextEnabled
? Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonStyle(context)
: Theme.of(context)
.extension<StackColors>()!
.getPrimaryDisabledButtonStyle(context),
child: Text(
"Next",
style: isDesktop
? _nextEnabled
? STextStyles.desktopButtonEnabled(context)
: STextStyles.desktopButtonDisabled(context)
: STextStyles.button(context),
),
),
),
),
if (isDesktop)
const Spacer(
flex: 15,

View file

@ -51,7 +51,7 @@ class RestoreSucceededDialog extends StatelessWidget {
height: 16,
),
Text(
"You can use your wallet now.",
"You may access your wallet now.",
style: STextStyles.desktopTextMedium(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textDark3,
),
@ -80,7 +80,7 @@ class RestoreSucceededDialog extends StatelessWidget {
} else {
return StackDialog(
title: "Wallet restored",
message: "You can use your wallet now.",
message: "You may access your wallet now.",
icon: SvgPicture.asset(
Assets.svg.checkCircle,
width: 24,

View file

@ -0,0 +1,404 @@
import 'package:barcode_scan2/barcode_scan2.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:stackwallet/pages/send_view/frost_ms/frost_continue_sign_config_view.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/services/frost.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/detail_item.dart';
import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart';
import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart';
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:stackwallet/widgets/textfield_icon_button.dart';
class FrostAttemptSignConfigView extends ConsumerStatefulWidget {
const FrostAttemptSignConfigView({
super.key,
required this.walletId,
});
static const String routeName = "/frostAttemptSignConfigView";
final String walletId;
@override
ConsumerState<FrostAttemptSignConfigView> createState() =>
_FrostAttemptSignConfigViewState();
}
class _FrostAttemptSignConfigViewState
extends ConsumerState<FrostAttemptSignConfigView> {
final List<TextEditingController> controllers = [];
final List<FocusNode> focusNodes = [];
late final String myName;
late final List<String> participantsWithoutMe;
late final String myPreprocess;
late final int myIndex;
late final int threshold;
final List<bool> fieldIsEmptyFlags = [];
bool hasEnoughPreprocesses() {
// own preprocess is not included in controllers and must be set here
int count = 1;
for (final controller in controllers) {
if (controller.text.isNotEmpty) {
count++;
}
}
return count >= threshold;
}
@override
void initState() {
final wallet =
ref.read(pWallets).getWallet(widget.walletId) as BitcoinFrostWallet;
final frostInfo = wallet.frostInfo;
myName = frostInfo.myName;
threshold = frostInfo.threshold;
participantsWithoutMe = List.from(frostInfo.participants); // Copy so it isn't fixed-length.
myIndex = participantsWithoutMe.indexOf(frostInfo.myName);
myPreprocess = ref.read(pFrostAttemptSignData.state).state!.preprocess;
participantsWithoutMe.removeAt(myIndex);
for (int i = 0; i < participantsWithoutMe.length; i++) {
controllers.add(TextEditingController());
focusNodes.add(FocusNode());
fieldIsEmptyFlags.add(true);
}
super.initState();
}
@override
void dispose() {
for (int i = 0; i < controllers.length; i++) {
controllers[i].dispose();
}
for (int i = 0; i < focusNodes.length; i++) {
focusNodes[i].dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return ConditionalParent(
condition: Util.isDesktop,
builder: (child) => DesktopScaffold(
background: Theme.of(context).extension<StackColors>()!.background,
appBar: const DesktopAppBar(
isCompactHeight: false,
leading: AppBarBackButton(),
),
body: SizedBox(
width: 480,
child: child,
),
),
child: ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () {
Navigator.of(context).pop();
},
),
title: Text(
"Preprocesses",
style: STextStyles.navBarTitle(context),
),
),
body: SafeArea(
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.all(16),
child: child,
),
),
),
);
},
),
),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: 220,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
QrImageView(
data: myPreprocess,
size: 220,
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
foregroundColor: Theme.of(context)
.extension<StackColors>()!
.accentColorDark,
),
],
),
),
const _Div(),
DetailItem(
title: "My name",
detail: myName,
),
const _Div(),
DetailItem(
title: "My preprocess",
detail: myPreprocess,
button: Util.isDesktop
? IconCopyButton(
data: myPreprocess,
)
: SimpleCopyButton(
data: myPreprocess,
),
),
const _Div(),
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (int i = 0; i < participantsWithoutMe.length; i++)
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
key: Key("frostPreprocessesTextFieldKey_$i"),
controller: controllers[i],
focusNode: focusNodes[i],
readOnly: false,
autocorrect: false,
enableSuggestions: false,
style: STextStyles.field(context),
onChanged: (_) {
setState(() {
fieldIsEmptyFlags[i] =
controllers[i].text.isEmpty;
});
},
decoration: standardInputDecoration(
"Enter ${participantsWithoutMe[i]}'s preprocess",
focusNodes[i],
context,
).copyWith(
contentPadding: const EdgeInsets.only(
left: 16,
top: 6,
bottom: 8,
right: 5,
),
suffixIcon: Padding(
padding: fieldIsEmptyFlags[i]
? const EdgeInsets.only(right: 8)
: const EdgeInsets.only(right: 0),
child: UnconstrainedBox(
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceAround,
children: [
!fieldIsEmptyFlags[i]
? TextFieldIconButton(
semanticsLabel:
"Clear Button. Clears The Preprocess Field Input.",
key: Key(
"frostPreprocessesClearButtonKey_$i",
),
onTap: () {
controllers[i].text = "";
setState(() {
fieldIsEmptyFlags[i] = true;
});
},
child: const XIcon(),
)
: TextFieldIconButton(
semanticsLabel:
"Paste Button. Pastes From Clipboard To Preprocess Field Input.",
key: Key(
"frostPreprocessesPasteButtonKey_$i",
),
onTap: () async {
final ClipboardData? data =
await Clipboard.getData(
Clipboard.kTextPlain);
if (data?.text != null &&
data!.text!.isNotEmpty) {
controllers[i].text =
data.text!.trim();
}
setState(() {
fieldIsEmptyFlags[i] =
controllers[i]
.text
.isEmpty;
});
},
child: fieldIsEmptyFlags[i]
? const ClipboardIcon()
: const XIcon(),
),
if (fieldIsEmptyFlags[i])
TextFieldIconButton(
semanticsLabel:
"Scan QR Button. Opens Camera For Scanning QR Code.",
key: Key(
"frostPreprocessesScanQrButtonKey_$i",
),
onTap: () async {
try {
if (FocusScope.of(context)
.hasFocus) {
FocusScope.of(context)
.unfocus();
await Future<void>.delayed(
const Duration(
milliseconds: 75));
}
final qrResult =
await BarcodeScanner.scan();
controllers[i].text =
qrResult.rawContent;
setState(() {
fieldIsEmptyFlags[i] =
controllers[i].text.isEmpty;
});
} on PlatformException catch (e, s) {
Logging.instance.log(
"Failed to get camera permissions while trying to scan qr code: $e\n$s",
level: LogLevel.Warning,
);
}
},
child: const QrCodeIcon(),
)
],
),
),
),
),
),
),
),
],
),
],
),
if (!Util.isDesktop) const Spacer(),
const _Div(),
PrimaryButton(
label: "Continue signing",
enabled: hasEnoughPreprocesses(),
onPressed: () async {
// collect Preprocess strings (not including my own)
final preprocesses = controllers.map((e) => e.text).toList();
// collect participants who are involved in this transaction
final List<String> requiredParticipantsUnordered = [];
for (int i = 0; i < participantsWithoutMe.length; i++) {
if (preprocesses[i].isNotEmpty) {
requiredParticipantsUnordered.add(participantsWithoutMe[i]);
}
}
ref.read(pFrostSelectParticipantsUnordered.notifier).state =
requiredParticipantsUnordered;
// insert an empty string at my index
preprocesses.insert(myIndex, "");
try {
ref.read(pFrostContinueSignData.notifier).state =
Frost.continueSigning(
machinePtr:
ref.read(pFrostAttemptSignData.state).state!.machinePtr,
preprocesses: preprocesses,
);
await Navigator.of(context).pushNamed(
FrostContinueSignView.routeName,
arguments: widget.walletId,
);
} catch (e, s) {
Logging.instance.log(
"$e\n$s",
level: LogLevel.Fatal,
);
return await showDialog<void>(
context: context,
builder: (_) => StackOkDialog(
title: "Failed to continue signing",
desktopPopRootNavigator: Util.isDesktop,
),
);
}
},
),
],
),
),
);
}
}
class _Div extends StatelessWidget {
const _Div({super.key});
@override
Widget build(BuildContext context) {
return const SizedBox(
height: 12,
);
}
}

View file

@ -0,0 +1,206 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
import 'package:stackwallet/pages/wallet_view/wallet_view.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/my_stack_view.dart';
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/show_loading.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/detail_item.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
class FrostCompleteSignView extends ConsumerStatefulWidget {
const FrostCompleteSignView({
super.key,
required this.walletId,
});
static const String routeName = "/frostCompleteSignView";
final String walletId;
@override
ConsumerState<FrostCompleteSignView> createState() =>
_FrostCompleteSignViewState();
}
class _FrostCompleteSignViewState extends ConsumerState<FrostCompleteSignView> {
bool _broadcastLock = false;
@override
Widget build(BuildContext context) {
return ConditionalParent(
condition: Util.isDesktop,
builder: (child) => DesktopScaffold(
background: Theme.of(context).extension<StackColors>()!.background,
appBar: const DesktopAppBar(
isCompactHeight: false,
leading: AppBarBackButton(),
),
body: SizedBox(
width: 480,
child: child,
),
),
child: ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () {
Navigator.of(context).pop();
},
),
title: Text(
"Preview transaction",
style: STextStyles.navBarTitle(context),
),
),
body: SafeArea(
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.all(16),
child: child,
),
),
),
);
},
),
),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: 220,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
QrImageView(
data: ref.watch(pFrostTxData.state).state!.raw!,
size: 220,
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
foregroundColor: Theme.of(context)
.extension<StackColors>()!
.accentColorDark,
),
],
),
),
const _Div(),
DetailItem(
title: "Raw transaction hex",
detail: ref.watch(pFrostTxData.state).state!.raw!,
button: Util.isDesktop
? IconCopyButton(
data: ref.watch(pFrostTxData.state).state!.raw!,
)
: SimpleCopyButton(
data: ref.watch(pFrostTxData.state).state!.raw!,
),
),
const _Div(),
if (!Util.isDesktop) const Spacer(),
const _Div(),
PrimaryButton(
label: "Broadcast Transaction",
onPressed: () async {
if (_broadcastLock) {
return;
}
_broadcastLock = true;
try {
Exception? ex;
final txData = await showLoading(
whileFuture: ref
.read(pWallets)
.getWallet(widget.walletId)
.confirmSend(
txData: ref.read(pFrostTxData.state).state!,
),
context: context,
message: "Broadcasting transaction to network",
isDesktop: Util.isDesktop,
onException: (e) {
ex = e;
},
);
if (ex != null) {
throw ex!;
}
if (mounted) {
if (txData != null) {
ref.read(pFrostTxData.state).state = txData;
Navigator.of(context).popUntil(
ModalRoute.withName(
Util.isDesktop
? MyStackView.routeName
: WalletView.routeName,
),
);
}
}
} catch (e, s) {
Logging.instance.log(
"$e\n$s",
level: LogLevel.Fatal,
);
return await showDialog<void>(
context: context,
builder: (_) => StackOkDialog(
title: "Broadcast error",
message: e.toString(),
desktopPopRootNavigator: Util.isDesktop,
),
);
} finally {
_broadcastLock = false;
}
},
),
],
),
),
);
}
}
class _Div extends StatelessWidget {
const _Div({super.key});
@override
Widget build(BuildContext context) {
return const SizedBox(
height: 12,
);
}
}

View file

@ -0,0 +1,445 @@
import 'package:barcode_scan2/barcode_scan2.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:stackwallet/pages/send_view/frost_ms/frost_complete_sign_view.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
import 'package:stackwallet/pages/wallet_view/wallet_view.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart';
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/services/frost.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/detail_item.dart';
import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.dart';
import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart';
import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart';
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:stackwallet/widgets/textfield_icon_button.dart';
class FrostContinueSignView extends ConsumerStatefulWidget {
const FrostContinueSignView({
super.key,
required this.walletId,
});
static const String routeName = "/frostContinueSignView";
final String walletId;
@override
ConsumerState<FrostContinueSignView> createState() =>
_FrostContinueSignViewState();
}
class _FrostContinueSignViewState extends ConsumerState<FrostContinueSignView> {
final List<TextEditingController> controllers = [];
final List<FocusNode> focusNodes = [];
late final String myName;
late final List<String> participantsWithoutMe;
late final List<String> participantsAll;
late final String myShare;
late final int myIndex;
final List<bool> fieldIsEmptyFlags = [];
@override
void initState() {
final wallet =
ref.read(pWallets).getWallet(widget.walletId) as BitcoinFrostWallet;
final frostInfo = wallet.frostInfo;
myName = frostInfo.myName;
participantsAll = frostInfo.participants;
myIndex = frostInfo.participants.indexOf(frostInfo.myName);
myShare = ref.read(pFrostContinueSignData.state).state!.share;
participantsWithoutMe = frostInfo.participants
.toSet()
.intersection(
ref.read(pFrostSelectParticipantsUnordered.state).state!.toSet())
.toList();
participantsWithoutMe.remove(myName);
for (int i = 0; i < participantsWithoutMe.length; i++) {
controllers.add(TextEditingController());
focusNodes.add(FocusNode());
fieldIsEmptyFlags.add(true);
}
super.initState();
}
@override
void dispose() {
for (int i = 0; i < controllers.length; i++) {
controllers[i].dispose();
}
for (int i = 0; i < focusNodes.length; i++) {
focusNodes[i].dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
await showDialog<void>(
context: context,
builder: (_) => FrostInterruptionDialog(
type: FrostInterruptionDialogType.transactionCreation,
popUntilOnYesRouteName: Util.isDesktop
? DesktopWalletView.routeName
: WalletView.routeName,
),
);
return false;
},
child: ConditionalParent(
condition: Util.isDesktop,
builder: (child) => DesktopScaffold(
background: Theme.of(context).extension<StackColors>()!.background,
appBar: DesktopAppBar(
isCompactHeight: false,
leading: AppBarBackButton(
onPressed: () async {
await showDialog<void>(
context: context,
builder: (_) => const FrostInterruptionDialog(
type: FrostInterruptionDialogType.transactionCreation,
popUntilOnYesRouteName: DesktopWalletView.routeName,
),
);
},
),
),
body: SizedBox(
width: 480,
child: child,
),
),
child: ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () async {
await showDialog<void>(
context: context,
builder: (_) => const FrostInterruptionDialog(
type: FrostInterruptionDialogType.transactionCreation,
popUntilOnYesRouteName: WalletView.routeName,
),
);
},
),
title: Text(
"Shares",
style: STextStyles.navBarTitle(context),
),
),
body: SafeArea(
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.all(16),
child: child,
),
),
),
);
},
),
),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: 220,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
QrImageView(
data: myShare,
size: 220,
backgroundColor: Theme.of(context)
.extension<StackColors>()!
.background,
foregroundColor: Theme.of(context)
.extension<StackColors>()!
.accentColorDark,
),
],
),
),
const _Div(),
DetailItem(
title: "My name",
detail: myName,
),
const _Div(),
DetailItem(
title: "My shares",
detail: myShare,
button: Util.isDesktop
? IconCopyButton(
data: myShare,
)
: SimpleCopyButton(
data: myShare,
),
),
const _Div(),
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (int i = 0; i < participantsWithoutMe.length; i++)
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
key: Key("frostSharesTextFieldKey_$i"),
controller: controllers[i],
focusNode: focusNodes[i],
readOnly: false,
autocorrect: false,
enableSuggestions: false,
style: STextStyles.field(context),
decoration: standardInputDecoration(
"Enter ${participantsWithoutMe[i]}'s share",
focusNodes[i],
context,
).copyWith(
contentPadding: const EdgeInsets.only(
left: 16,
top: 6,
bottom: 8,
right: 5,
),
suffixIcon: Padding(
padding: fieldIsEmptyFlags[i]
? const EdgeInsets.only(right: 8)
: const EdgeInsets.only(right: 0),
child: UnconstrainedBox(
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceAround,
children: [
!fieldIsEmptyFlags[i]
? TextFieldIconButton(
semanticsLabel:
"Clear Button. Clears "
"The Share Field Input.",
key: Key(
"frostSharesClearButtonKey_$i",
),
onTap: () {
controllers[i].text = "";
setState(() {
fieldIsEmptyFlags[i] = true;
});
},
child: const XIcon(),
)
: TextFieldIconButton(
semanticsLabel:
"Paste Button. Pastes From "
"Clipboard To Share Field Input.",
key: Key(
"frostSharesPasteButtonKey_$i"),
onTap: () async {
final ClipboardData? data =
await Clipboard.getData(
Clipboard.kTextPlain);
if (data?.text != null &&
data!.text!.isNotEmpty) {
controllers[i].text =
data.text!.trim();
}
setState(() {
fieldIsEmptyFlags[i] =
controllers[i]
.text
.isEmpty;
});
},
child: fieldIsEmptyFlags[i]
? const ClipboardIcon()
: const XIcon(),
),
if (fieldIsEmptyFlags[i])
TextFieldIconButton(
semanticsLabel:
"Scan QR Button. Opens Camera "
"For Scanning QR Code.",
key: Key(
"frostSharesScanQrButtonKey_$i",
),
onTap: () async {
try {
if (FocusScope.of(context)
.hasFocus) {
FocusScope.of(context)
.unfocus();
await Future<void>.delayed(
const Duration(
milliseconds: 75));
}
final qrResult =
await BarcodeScanner.scan();
controllers[i].text =
qrResult.rawContent;
setState(() {
fieldIsEmptyFlags[i] =
controllers[i]
.text
.isEmpty;
});
} on PlatformException catch (e, s) {
Logging.instance.log(
"Failed to get camera permissions "
"while trying to scan qr code: $e\n$s",
level: LogLevel.Warning,
);
}
},
child: const QrCodeIcon(),
)
],
),
),
),
),
),
),
),
],
),
],
),
if (!Util.isDesktop) const Spacer(),
const _Div(),
PrimaryButton(
label: "Complete signing",
onPressed: () async {
// check for empty shares
if (controllers
.map((e) => e.text.isEmpty)
.reduce((value, element) => value |= element)) {
return await showDialog<void>(
context: context,
builder: (_) => StackOkDialog(
title: "Missing Shares",
desktopPopRootNavigator: Util.isDesktop,
),
);
}
// collect Share strings
final sharesCollected =
controllers.map((e) => e.text).toList();
final List<String> shares = [];
for (final participant in participantsAll) {
if (participantsWithoutMe.contains(participant)) {
shares.add(sharesCollected[
participantsWithoutMe.indexOf(participant)]);
} else {
shares.add("");
}
}
try {
final rawTx = Frost.completeSigning(
machinePtr: ref
.read(pFrostContinueSignData.state)
.state!
.machinePtr,
shares: shares,
);
ref.read(pFrostTxData.state).state =
ref.read(pFrostTxData.state).state!.copyWith(
raw: rawTx,
);
await Navigator.of(context).pushNamed(
FrostCompleteSignView.routeName,
arguments: widget.walletId,
);
} catch (e, s) {
Logging.instance.log(
"$e\n$s",
level: LogLevel.Fatal,
);
return await showDialog<void>(
context: context,
builder: (_) => StackOkDialog(
title: "Failed to complete signing process",
desktopPopRootNavigator: Util.isDesktop,
),
);
}
},
),
],
),
),
),
);
}
}
class _Div extends StatelessWidget {
const _Div({super.key});
@override
Widget build(BuildContext context) {
return const SizedBox(
height: 12,
);
}
}

View file

@ -0,0 +1,185 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:stackwallet/pages/send_view/frost_ms/frost_attempt_sign_config_view.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/detail_item.dart';
class FrostCreateSignConfigView extends ConsumerStatefulWidget {
const FrostCreateSignConfigView({
super.key,
required this.walletId,
});
static const String routeName = "/frostCreateSignConfigView";
final String walletId;
@override
ConsumerState<FrostCreateSignConfigView> createState() =>
_FrostCreateSignConfigViewState();
}
class _FrostCreateSignConfigViewState
extends ConsumerState<FrostCreateSignConfigView> {
bool _attemptSignLock = false;
Future<void> _attemptSign() async {
if (_attemptSignLock) {
return;
}
_attemptSignLock = true;
try {
final wallet =
ref.read(pWallets).getWallet(widget.walletId) as BitcoinFrostWallet;
final attemptSignRes = await wallet.frostAttemptSignConfig(
config: ref.read(pFrostTxData.state).state!.frostMSConfig!,
);
ref.read(pFrostAttemptSignData.notifier).state = attemptSignRes;
await Navigator.of(context).pushNamed(
FrostAttemptSignConfigView.routeName,
arguments: widget.walletId,
);
} catch (e, s) {
Logging.instance.log(
"$e\n$s",
level: LogLevel.Error,
);
} finally {
_attemptSignLock = false;
}
}
@override
Widget build(BuildContext context) {
double qrImageSize =
Util.isDesktop ? 360 : MediaQuery.of(context).size.width - 32;
return ConditionalParent(
condition: Util.isDesktop,
builder: (child) => DesktopScaffold(
background: Theme.of(context).extension<StackColors>()!.background,
appBar: const DesktopAppBar(
isCompactHeight: false,
leading: AppBarBackButton(),
),
body: SingleChildScrollView(
child: SizedBox(
width: 600, // Was 480, may look better but overflows the bottom.
child: child,
),
),
),
child: ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () {
Navigator.of(context).pop();
},
),
title: Text(
"Sign config",
style: STextStyles.navBarTitle(context),
),
),
body: SafeArea(
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: child,
),
),
),
);
},
),
),
),
),
child: Column(
children: [
if (!Util.isDesktop) const Spacer(),
SizedBox(
height: qrImageSize,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
QrImageView(
data: ref.watch(pFrostTxData.state).state!.frostMSConfig!,
size: qrImageSize,
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
foregroundColor: Theme.of(context)
.extension<StackColors>()!
.accentColorDark,
),
],
),
),
if (!Util.isDesktop)
const SizedBox(
height: 32,
),
DetailItem(
title: "Encoded config",
detail: ref.watch(pFrostTxData.state).state!.frostMSConfig!,
button: Util.isDesktop
? IconCopyButton(
data: ref.watch(pFrostTxData.state).state!.frostMSConfig!,
)
: SimpleCopyButton(
data: ref.watch(pFrostTxData.state).state!.frostMSConfig!,
),
),
SizedBox(
height: Util.isDesktop ? 20 : 16,
),
if (!Util.isDesktop)
const Spacer(
flex: 2,
),
PrimaryButton(
label: "Attempt sign",
onPressed: () {
_attemptSign();
},
),
const SizedBox(
height: 16,
),
],
),
),
);
}
}

View file

@ -0,0 +1,332 @@
import 'package:barcode_scan2/barcode_scan2.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:isar/isar.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/pages/send_view/frost_ms/frost_attempt_sign_config_view.dart';
import 'package:stackwallet/providers/db/main_db_provider.dart';
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/services/frost.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/format.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/models/tx_data.dart';
import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/conditional_parent.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/desktop/primary_button.dart';
import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart';
import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart';
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:stackwallet/widgets/textfield_icon_button.dart';
class FrostImportSignConfigView extends ConsumerStatefulWidget {
const FrostImportSignConfigView({
super.key,
required this.walletId,
});
static const String routeName = "/frostImportSignConfigView";
final String walletId;
@override
ConsumerState<FrostImportSignConfigView> createState() =>
_FrostImportSignConfigViewState();
}
class _FrostImportSignConfigViewState
extends ConsumerState<FrostImportSignConfigView> {
late final TextEditingController configFieldController;
late final FocusNode configFocusNode;
bool _configEmpty = true;
bool _attemptSignLock = false;
Future<void> _attemptSign() async {
if (_attemptSignLock) {
return;
}
_attemptSignLock = true;
try {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
}
final config = configFieldController.text;
final wallet =
ref.read(pWallets).getWallet(widget.walletId) as BitcoinFrostWallet;
final data = Frost.extractDataFromSignConfig(
signConfig: config,
coin: wallet.cryptoCurrency,
);
final utxos = await ref
.read(mainDBProvider)
.getUTXOs(wallet.walletId)
.filter()
.anyOf(
data.inputs,
(q, e) => q
.txidEqualTo(Format.uint8listToString(e.hash))
.and()
.valueEqualTo(e.value)
.and()
.voutEqualTo(e.vout))
.findAll();
// TODO add more data from 'data' and display to user ?
ref.read(pFrostTxData.notifier).state = TxData(
frostMSConfig: config,
recipients: data.recipients
.map((e) => (address: e.address, amount: e.amount, isChange: false))
.toList(),
utxos: utxos.toSet(),
);
final attemptSignRes = await wallet.frostAttemptSignConfig(
config: ref.read(pFrostTxData.state).state!.frostMSConfig!,
);
ref.read(pFrostAttemptSignData.notifier).state = attemptSignRes;
await Navigator.of(context).pushNamed(
FrostAttemptSignConfigView.routeName,
arguments: widget.walletId,
);
} catch (e, s) {
Logging.instance.log(
"$e\n$s",
level: LogLevel.Error,
);
await showDialog<void>(
context: context,
builder: (_) => StackOkDialog(
title: "Import and attempt sign config failed",
message: e.toString(),
desktopPopRootNavigator: Util.isDesktop,
),
);
} finally {
_attemptSignLock = false;
}
}
@override
void initState() {
configFieldController = TextEditingController();
configFocusNode = FocusNode();
super.initState();
}
@override
void dispose() {
configFieldController.dispose();
configFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ConditionalParent(
condition: Util.isDesktop,
builder: (child) => DesktopScaffold(
background: Theme.of(context).extension<StackColors>()!.background,
appBar: const DesktopAppBar(
isCompactHeight: false,
leading: AppBarBackButton(),
),
body: SizedBox(
width: 480,
child: child,
),
),
child: ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () {
Navigator.of(context).pop();
},
),
title: Text(
"Import FROST sign config",
style: STextStyles.navBarTitle(context),
),
),
body: SafeArea(
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.all(16),
child: child,
),
),
),
);
},
),
),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 16,
),
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
key: const Key("frConfigTextFieldKey"),
controller: configFieldController,
onChanged: (_) {
setState(() {
_configEmpty = configFieldController.text.isEmpty;
});
},
focusNode: configFocusNode,
readOnly: false,
autocorrect: false,
enableSuggestions: false,
style: STextStyles.field(context),
decoration: standardInputDecoration(
"Enter config",
configFocusNode,
context,
).copyWith(
contentPadding: const EdgeInsets.only(
left: 16,
top: 6,
bottom: 8,
right: 5,
),
suffixIcon: Padding(
padding: _configEmpty
? const EdgeInsets.only(right: 8)
: const EdgeInsets.only(right: 0),
child: UnconstrainedBox(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
!_configEmpty
? TextFieldIconButton(
semanticsLabel:
"Clear Button. Clears The Config Field.",
key: const Key("frConfigClearButtonKey"),
onTap: () {
configFieldController.text = "";
setState(() {
_configEmpty = true;
});
},
child: const XIcon(),
)
: TextFieldIconButton(
semanticsLabel:
"Paste Button. Pastes From Clipboard To Config Field Input.",
key: const Key("frConfigPasteButtonKey"),
onTap: () async {
final ClipboardData? data =
await Clipboard.getData(
Clipboard.kTextPlain);
if (data?.text != null &&
data!.text!.isNotEmpty) {
configFieldController.text =
data.text!.trim();
}
setState(() {
_configEmpty =
configFieldController.text.isEmpty;
});
},
child: _configEmpty
? const ClipboardIcon()
: const XIcon(),
),
if (_configEmpty)
TextFieldIconButton(
semanticsLabel:
"Scan QR Button. Opens Camera For Scanning QR Code.",
key: const Key("frConfigScanQrButtonKey"),
onTap: () async {
try {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(
const Duration(milliseconds: 75));
}
final qrResult = await BarcodeScanner.scan();
configFieldController.text =
qrResult.rawContent;
setState(() {
_configEmpty =
configFieldController.text.isEmpty;
});
} on PlatformException catch (e, s) {
Logging.instance.log(
"Failed to get camera permissions while trying to scan qr code: $e\n$s",
level: LogLevel.Warning,
);
}
},
child: const QrCodeIcon(),
)
],
),
),
),
),
),
),
const SizedBox(
height: 16,
),
if (!Util.isDesktop) const Spacer(),
const SizedBox(
height: 16,
),
PrimaryButton(
label: "Start signing",
enabled: !_configEmpty,
onPressed: () {
_attemptSign();
},
),
],
),
),
);
}
}

View file

@ -0,0 +1,613 @@
/*
* 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:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/pages/coin_control/coin_control_view.dart';
import 'package:stackwallet/pages/send_view/frost_ms/frost_create_sign_config_view.dart';
import 'package:stackwallet/pages/send_view/frost_ms/frost_import_sign_config_view.dart';
import 'package:stackwallet/pages/send_view/frost_ms/recipient.dart';
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/themes/coin_icon_provider.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/utilities/amount/amount_formatter.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/show_loading.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart';
import 'package:stackwallet/wallets/models/tx_data.dart';
import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart';
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/coin_control_interface.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart';
import 'package:stackwallet/widgets/fee_slider.dart';
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:stackwallet/widgets/textfield_icon_button.dart';
import 'package:tuple/tuple.dart';
class FrostSendView extends ConsumerStatefulWidget {
const FrostSendView({
Key? key,
required this.walletId,
required this.coin,
}) : super(key: key);
static const String routeName = "/frostSendView";
final String walletId;
final Coin coin;
@override
ConsumerState<FrostSendView> createState() => _FrostSendViewState();
}
class _FrostSendViewState extends ConsumerState<FrostSendView> {
final List<int> recipientWidgetIndexes = [0];
int _greatestWidgetIndex = 0;
late final String walletId;
late final Coin 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),
changeAddress: (await wallet.getCurrentReceivingAddress())!.value,
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",
isDesktop: Util.isDesktop,
onException: (e) {
throw e;
},
);
}
if (mounted && txData != null) {
ref.read(pFrostTxData.notifier).state = txData;
await Navigator.of(context).pushNamed(
FrostCreateSignConfigView.routeName,
arguments: widget.walletId,
);
}
} 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;
void _validateRecipientFormStates() {
for (final i in recipientWidgetIndexes) {
final state = ref.read(pRecipient(i).state).state;
if (state?.amount == null || state?.address == null) {
ref.read(previewTxButtonStateProvider.notifier).state = false;
return;
}
}
ref.read(previewTxButtonStateProvider.notifier).state = true;
return;
}
@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,
),
);
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 (mounted) {
Navigator.of(context).pop();
}
},
),
title: Text(
"Send ${coin.ticker}",
style: STextStyles.navBarTitle(context),
),
actions: [
Padding(
padding: const EdgeInsets.only(
top: 10,
bottom: 10,
right: 10,
),
child: AspectRatio(
aspectRatio: 1,
child: AppBarIconButton(
semanticsLabel: "Import sign config Button.",
key: const Key("importSignConfigButtonKey"),
size: 36,
shadows: const [],
color:
Theme.of(context).extension<StackColors>()!.background,
icon: SvgPicture.asset(
Assets.svg.circlePlus,
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark,
width: 20,
height: 20,
),
onPressed: () {
Navigator.of(context).pushNamed(
FrostImportSignConfigView.routeName,
arguments: walletId,
);
},
),
),
),
],
),
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: () {
// cryptoAmountController.text = ref
// .read(pAmountFormatter(coin))
// .format(
// _cachedBalance!,
// withUnitName: false,
// );
},
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,
),
// Text(
// "${(manager.balance.spendable.decimal * ref.watch(
// priceAnd24hChangeNotifierProvider.select(
// (value) => value.getPrice(coin).item1,
// ),
// )).toAmount(
// fractionDigits: 2,
// ).fiatString(
// locale: locale,
// )} ${ref.watch(
// prefsChangeNotifierProvider
// .select((value) => value.currency),
// )}",
// style: STextStyles.subtitle(context).copyWith(
// fontSize: 8,
// ),
// textAlign: TextAlign.right,
// )
],
),
),
)
],
),
),
),
const SizedBox(
height: 16,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Recipients",
style: STextStyles.smallMed12(context),
textAlign: TextAlign.left,
),
CustomTextButton(
text: "Add",
onTap: () {
// used for tracking recipient forms
_greatestWidgetIndex++;
recipientWidgetIndexes.add(_greatestWidgetIndex);
setState(() {});
},
),
],
),
const SizedBox(
height: 8,
),
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],
coin: coin,
onChanged: () {
_validateRecipientFormStates();
},
remove: i == 0 && recipientWidgetIndexes.length == 1
? null
: () {
recipientWidgetIndexes.removeAt(i);
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) {
// finally spendable = ref
// .read(walletsChangeNotifierProvider)
// .getManager(widget.walletId)
// .balance
// .spendable;
// 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,
onSatVByteChanged: (rate) {
customFeeRate = rate;
},
),
),
Util.isDesktop
? const SizedBox(
height: 12,
)
: const Spacer(),
const SizedBox(
height: 12,
),
TextButton(
onPressed: ref.watch(previewTxButtonStateProvider.state).state
? _createSignConfig
: null,
style: ref.watch(previewTxButtonStateProvider.state).state
? Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonStyle(context)
: Theme.of(context)
.extension<StackColors>()!
.getPrimaryDisabledButtonStyle(context),
child: Text(
"Create config",
style: STextStyles.button(context),
),
),
const SizedBox(
height: 16,
),
],
),
),
);
}
}
final previewTxButtonStateProvider = StateProvider((_) => false);

View file

@ -0,0 +1,502 @@
import 'package:decimal/decimal.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/providers/global/locale_provider.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/address_utils.dart';
import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/utilities/amount/amount_formatter.dart';
import 'package:stackwallet/utilities/amount/amount_input_formatter.dart';
import 'package:stackwallet/utilities/amount/amount_unit.dart';
import 'package:stackwallet/utilities/barcode_scanner_interface.dart';
import 'package:stackwallet/utilities/clipboard_interface.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart';
import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart';
import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart';
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:stackwallet/widgets/textfield_icon_button.dart';
//TODO: move the following two providers elsewhere
final pClipboard =
Provider<ClipboardInterface>((ref) => const ClipboardWrapper());
final pBarcodeScanner =
Provider<BarcodeScannerInterface>((ref) => const BarcodeScannerWrapper());
// final _pPrice = Provider.family<Decimal, Coin>((ref, coin) {
// return ref.watch(
// priceAnd24hChangeNotifierProvider
// .select((value) => value.getPrice(coin).item1),
// );
// });
final pRecipient =
StateProvider.family<({String address, Amount? amount})?, int>(
(ref, index) => null);
class Recipient extends ConsumerStatefulWidget {
const Recipient({
super.key,
required this.index,
required this.coin,
this.remove,
this.onChanged,
});
final int index;
final Coin coin;
final VoidCallback? remove;
final VoidCallback? onChanged;
@override
ConsumerState<Recipient> createState() => _RecipientState();
}
class _RecipientState extends ConsumerState<Recipient> {
late final TextEditingController addressController, amountController;
late final FocusNode addressFocusNode, amountFocusNode;
bool _addressIsEmpty = true;
bool _cryptoAmountChangeLock = false;
void _updateRecipientData() {
final address = addressController.text;
final amount =
ref.read(pAmountFormatter(widget.coin)).tryParse(amountController.text);
ref.read(pRecipient(widget.index).notifier).state = (
address: address,
amount: amount,
);
widget.onChanged?.call();
}
void _cryptoAmountChanged() async {
if (!_cryptoAmountChangeLock) {
Amount? cryptoAmount = ref.read(pAmountFormatter(widget.coin)).tryParse(
amountController.text,
);
if (cryptoAmount != null) {
if (ref.read(pRecipient(widget.index))?.amount != null &&
ref.read(pRecipient(widget.index))?.amount == cryptoAmount) {
return;
}
// final price = ref.read(_pPrice(widget.coin));
//
// if (price > Decimal.zero) {
// baseController.text = (cryptoAmount.decimal * price)
// .toAmount(
// fractionDigits: 2,
// )
// .fiatString(
// locale: ref.read(localeServiceChangeNotifierProvider).locale,
// );
// }
} else {
cryptoAmount = null;
// baseController.text = "";
}
_updateRecipientData();
}
}
@override
void initState() {
addressController = TextEditingController();
amountController = TextEditingController();
// baseController = TextEditingController();
addressFocusNode = FocusNode();
amountFocusNode = FocusNode();
// baseFocusNode = FocusNode();
amountController.addListener(_cryptoAmountChanged);
super.initState();
}
@override
void dispose() {
amountController.removeListener(_cryptoAmountChanged);
addressController.dispose();
amountController.dispose();
// baseController.dispose();
addressFocusNode.dispose();
amountFocusNode.dispose();
// baseFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final String locale = ref.watch(
localeServiceChangeNotifierProvider.select(
(value) => value.locale,
),
);
return RoundedWhiteContainer(
padding: const EdgeInsets.all(0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
key: const Key("sendViewAddressFieldKey"),
controller: addressController,
readOnly: false,
autocorrect: false,
enableSuggestions: false,
focusNode: addressFocusNode,
style: STextStyles.field(context),
onChanged: (_) {
setState(() {
_addressIsEmpty = addressController.text.isEmpty;
});
},
decoration: standardInputDecoration(
"Enter ${widget.coin.ticker} address",
addressFocusNode,
context,
).copyWith(
contentPadding: const EdgeInsets.only(
left: 16,
top: 6,
bottom: 8,
right: 5,
),
suffixIcon: Padding(
padding: _addressIsEmpty
? const EdgeInsets.only(right: 8)
: const EdgeInsets.only(right: 0),
child: UnconstrainedBox(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
!_addressIsEmpty
? TextFieldIconButton(
semanticsLabel:
"Clear Button. Clears The Address Field Input.",
key: const Key(
"sendViewClearAddressFieldButtonKey"),
onTap: () {
addressController.text = "";
setState(() {
_addressIsEmpty = true;
});
_updateRecipientData();
},
child: const XIcon(),
)
: TextFieldIconButton(
semanticsLabel:
"Paste Button. Pastes From Clipboard To Address Field Input.",
key: const Key(
"sendViewPasteAddressFieldButtonKey"),
onTap: () async {
final ClipboardData? data = await ref
.read(pClipboard)
.getData(Clipboard.kTextPlain);
if (data?.text != null &&
data!.text!.isNotEmpty) {
String content = data.text!.trim();
if (content.contains("\n")) {
content = content.substring(
0, content.indexOf("\n"));
}
addressController.text = content.trim();
setState(() {
_addressIsEmpty =
addressController.text.isEmpty;
});
_updateRecipientData();
}
},
child: _addressIsEmpty
? const ClipboardIcon()
: const XIcon(),
),
if (_addressIsEmpty)
TextFieldIconButton(
semanticsLabel: "Scan QR Button. "
"Opens Camera For Scanning QR Code.",
key: const Key(
"sendViewScanQrButtonKey",
),
onTap: () async {
try {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(
const Duration(
milliseconds: 75,
),
);
}
final qrResult =
await ref.read(pBarcodeScanner).scan();
Logging.instance.log(
"qrResult content: ${qrResult.rawContent}",
level: LogLevel.Info,
);
/// TODO: deal with address utils
final results =
AddressUtils.parseUri(qrResult.rawContent);
Logging.instance.log(
"qrResult parsed: $results",
level: LogLevel.Info,
);
if (results.isNotEmpty &&
results["scheme"] ==
widget.coin.uriScheme) {
// auto fill address
addressController.text =
(results["address"] ?? "").trim();
// autofill amount field
if (results["amount"] != null) {
final Amount amount =
Decimal.parse(results["amount"]!)
.toAmount(
fractionDigits: widget.coin.decimals,
);
amountController.text = ref
.read(pAmountFormatter(widget.coin))
.format(
amount,
withUnitName: false,
);
}
} else {
addressController.text =
qrResult.rawContent.trim();
}
setState(() {
_addressIsEmpty =
addressController.text.isEmpty;
});
_updateRecipientData();
} on PlatformException catch (e, s) {
Logging.instance.log(
"Failed to get camera permissions while "
"trying to scan qr code in SendView: $e\n$s",
level: LogLevel.Warning,
);
}
},
child: const QrCodeIcon(),
),
],
),
),
),
),
),
),
const SizedBox(
height: 12,
),
TextField(
autocorrect: false,
enableSuggestions: false,
style: STextStyles.smallMed14(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textDark,
),
key: const Key("amountInputFieldCryptoTextFieldKey"),
controller: amountController,
focusNode: amountFocusNode,
keyboardType: Util.isDesktop
? null
: const TextInputType.numberWithOptions(
signed: false,
decimal: true,
),
textAlign: TextAlign.right,
inputFormatters: [
AmountInputFormatter(
decimals: widget.coin.decimals,
unit: ref.watch(pAmountUnit(widget.coin)),
locale: locale,
),
],
decoration: InputDecoration(
contentPadding: const EdgeInsets.only(
top: 12,
right: 12,
),
hintText: "0",
hintStyle: STextStyles.fieldLabel(context).copyWith(
fontSize: 14,
),
prefixIcon: FittedBox(
fit: BoxFit.scaleDown,
child: Padding(
padding: const EdgeInsets.all(12),
child: Text(
ref
.watch(pAmountUnit(widget.coin))
.unitForCoin(widget.coin),
style: STextStyles.smallMed14(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark),
),
),
),
),
),
// if (ref.watch(prefsChangeNotifierProvider
// .select((value) => value.externalCalls)))
// const SizedBox(
// height: 8,
// ),
// if (ref.watch(prefsChangeNotifierProvider
// .select((value) => value.externalCalls)))
// TextField(
// autocorrect: Util.isDesktop ? false : true,
// enableSuggestions: Util.isDesktop ? false : true,
// style: STextStyles.smallMed14(context).copyWith(
// color: Theme.of(context).extension<StackColors>()!.textDark,
// ),
// key: const Key("amountInputFieldFiatTextFieldKey"),
// controller: baseController,
// focusNode: baseFocusNode,
// keyboardType: Util.isDesktop
// ? null
// : const TextInputType.numberWithOptions(
// signed: false,
// decimal: true,
// ),
// textAlign: TextAlign.right,
// inputFormatters: [
// AmountInputFormatter(
// decimals: 2,
// locale: locale,
// ),
// ],
// onChanged: (baseAmountString) {
// final baseAmount = Amount.tryParseFiatString(
// baseAmountString,
// locale: locale,
// );
// Amount? cryptoAmount;
// final int decimals = widget.coin.decimals;
// if (baseAmount != null) {
// final _price = ref.read(_pPrice(widget.coin));
//
// if (_price == Decimal.zero) {
// cryptoAmount = 0.toAmountAsRaw(
// fractionDigits: decimals,
// );
// } else {
// cryptoAmount = baseAmount <= Amount.zero
// ? 0.toAmountAsRaw(fractionDigits: decimals)
// : (baseAmount.decimal / _price)
// .toDecimal(
// scaleOnInfinitePrecision: decimals,
// )
// .toAmount(fractionDigits: decimals);
// }
// if (ref.read(pRecipient(widget.index))?.amount != null &&
// ref.read(pRecipient(widget.index))?.amount ==
// cryptoAmount) {
// return;
// }
//
// final amountString =
// ref.read(pAmountFormatter(widget.coin)).format(
// cryptoAmount,
// withUnitName: false,
// );
//
// _cryptoAmountChangeLock = true;
// amountController.text = amountString;
// _cryptoAmountChangeLock = false;
// } else {
// cryptoAmount = 0.toAmountAsRaw(
// fractionDigits: decimals,
// );
// _cryptoAmountChangeLock = true;
// amountController.text = "";
// _cryptoAmountChangeLock = false;
// }
//
// _updateRecipientData();
// },
// decoration: InputDecoration(
// contentPadding: const EdgeInsets.only(
// top: 12,
// right: 12,
// ),
// hintText: "0",
// hintStyle: STextStyles.fieldLabel(context).copyWith(
// fontSize: 14,
// ),
// prefixIcon: FittedBox(
// fit: BoxFit.scaleDown,
// child: Padding(
// padding: const EdgeInsets.all(12),
// child: Text(
// ref.watch(prefsChangeNotifierProvider
// .select((value) => value.currency)),
// style: STextStyles.smallMed14(context).copyWith(
// color: Theme.of(context)
// .extension<StackColors>()!
// .accentColorDark),
// ),
// ),
// ),
// ),
// ),
if (widget.remove != null)
const SizedBox(
height: 6,
),
if (widget.remove != null)
Row(
children: [
const Spacer(),
CustomTextButton(
text: "Remove",
onTap: () {
ref.read(pRecipient(widget.index).notifier).state = null;
widget.remove?.call();
},
),
],
),
],
),
);
}
}

View file

@ -26,6 +26,7 @@ import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/wallets/isar/providers/eth/current_token_wallet_provider.dart';
import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart';
import 'package:stackwallet/wallets/wallet/impl/firo_wallet.dart';
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart';
import 'package:stackwallet/widgets/animated_text.dart';
final feeSheetSessionCacheProvider =
@ -697,7 +698,7 @@ class _TransactionFeeSelectionSheetState
const SizedBox(
height: 24,
),
if (coin.isElectrumXCoin)
if (wallet is ElectrumXInterface)
GestureDetector(
onTap: () {
final state =
@ -766,7 +767,7 @@ class _TransactionFeeSelectionSheetState
),
),
),
if (coin.isElectrumXCoin)
if (wallet is ElectrumXInterface)
const SizedBox(
height: 24,
),

View file

@ -166,6 +166,8 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> {
case Coin.firo:
case Coin.namecoin:
case Coin.particl:
case Coin.bitcoinFrost:
case Coin.bitcoinFrostTestNet:
case Coin.bitcoinTestNet:
case Coin.litecoinTestNet:
case Coin.bitcoincashTestnet:
@ -758,6 +760,8 @@ class _NodeFormState extends ConsumerState<NodeForm> {
case Coin.eCash:
case Coin.stellar:
case Coin.stellarTestnet:
case Coin.bitcoinFrost:
case Coin.bitcoinFrostTestNet:
return false;
case Coin.ethereum:

View file

@ -148,6 +148,8 @@ class _NodeDetailsViewState extends ConsumerState<NodeDetailsView> {
case Coin.litecoinTestNet:
case Coin.bitcoincashTestnet:
case Coin.eCash:
case Coin.bitcoinFrost:
case Coin.bitcoinFrostTestNet:
final client = ElectrumXClient(
host: node!.host,
port: node.port,

View file

@ -13,6 +13,7 @@ import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:frostdart/frostdart.dart' as frost;
import 'package:isar/isar.dart';
import 'package:stack_wallet_backup/stack_wallet_backup.dart';
import 'package:stackwallet/db/hive/db.dart';
@ -26,6 +27,7 @@ import 'package:stackwallet/models/stack_restoring_ui_state.dart';
import 'package:stackwallet/models/trade_wallet_lookup.dart';
import 'package:stackwallet/models/wallet_restore_state.dart';
import 'package:stackwallet/services/address_book_service.dart';
import 'package:stackwallet/services/frost.dart';
import 'package:stackwallet/services/node_service.dart';
import 'package:stackwallet/services/trade_notes_service.dart';
import 'package:stackwallet/services/trade_sent_from_stack_service.dart';
@ -41,7 +43,9 @@ import 'package:stackwallet/utilities/format.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/prefs.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart';
import 'package:stackwallet/wallets/isar/models/wallet_info.dart';
import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart';
import 'package:stackwallet/wallets/wallet/impl/epiccash_wallet.dart';
import 'package:stackwallet/wallets/wallet/wallet.dart';
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/cw_based_interface.dart';
@ -302,6 +306,24 @@ abstract class SWB {
await wallet.getMnemonicPassphrase();
} else if (wallet is PrivateKeyInterface) {
backupWallet['privateKey'] = await wallet.getPrivateKey();
} else if (wallet is BitcoinFrostWallet) {
String? keys = await wallet.getSerializedKeys();
String? config = await wallet.getMultisigConfig();
if (keys == null || config == null) {
String err = "${wallet.info.coin.name} wallet ${wallet.info.name} "
"has null keys or config";
Logging.instance.log(err, level: LogLevel.Fatal);
throw Exception(err);
}
//This case should never actually happen in practice unless the whole
// wallet is somehow corrupt
// TODO [prio=low]: solve case in which either keys or config is null.
// Format keys & config as a JSON string and set otherDataJsonString.
Map<String, dynamic> frostData = {};
frostData["keys"] = keys;
frostData["config"] = config;
backupWallet['frostWalletData'] = jsonEncode(frostData);
}
backupWallet['coinName'] = wallet.info.coin.name;
backupWallet['storedChainHeight'] = wallet.info.cachedChainHeight;
@ -384,7 +406,9 @@ abstract class SWB {
if (walletbackup['mnemonic'] == null) {
// probably private key based
privateKey = walletbackup['privateKey'] as String;
if (walletbackup['privateKey'] != null) {
privateKey = walletbackup['privateKey'] as String;
}
} else {
if (walletbackup['mnemonic'] is List) {
List<String> mnemonicList = (walletbackup['mnemonic'] as List<dynamic>)
@ -406,6 +430,37 @@ abstract class SWB {
);
try {
String? serializedKeys;
String? multisigConfig;
if (info.coin.isFrost) {
// Decode info.otherDataJsonString for Frost recovery info.
final frostData = jsonDecode(walletbackup["frostWalletData"] as String);
serializedKeys = frostData["keys"] as String;
multisigConfig = frostData["config"] as String;
final myNameIndex = frost.getParticipantIndexFromKeys(
serializedKeys: serializedKeys,
);
final participants = Frost.getParticipants(
multisigConfig: multisigConfig,
);
final myName = participants[myNameIndex];
final frostInfo = FrostWalletInfo(
walletId: info.walletId,
knownSalts: [],
participants: participants,
myName: myName,
threshold: frost.multisigThreshold(
multisigConfig: multisigConfig,
),
);
await MainDB.instance.isar.writeTxn(() async {
await MainDB.instance.isar.frostWalletInfo.put(frostInfo);
});
}
final wallet = await Wallet.create(
walletInfo: info,
mainDB: MainDB.instance,
@ -427,7 +482,15 @@ abstract class SWB {
Future<void>? restoringFuture;
if (!(wallet is CwBasedInterface || wallet is EpiccashWallet)) {
restoringFuture = wallet.recover(isRescan: false);
if (wallet is BitcoinFrostWallet) {
restoringFuture = wallet.recover(
isRescan: false,
multisigConfig: multisigConfig!,
serializedKeys: serializedKeys!,
);
} else {
restoringFuture = wallet.recover(isRescan: false);
}
}
uiState?.update(

View file

@ -0,0 +1,186 @@
/*
* 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:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/frost_participants_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/begin_reshare_config_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1b/import_reshare_config_view.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
import 'package:stackwallet/providers/db/main_db_provider.dart';
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/conditional_parent.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 FrostMSWalletOptionsView extends ConsumerWidget {
const FrostMSWalletOptionsView({
Key? key,
required this.walletId,
}) : super(key: key);
static const String routeName = "/frostMSWalletOptionsView";
final String walletId;
@override
Widget build(BuildContext context, WidgetRef ref) {
return ConditionalParent(
condition: Util.isDesktop,
builder: (child) => DesktopScaffold(
background: Theme.of(context).extension<StackColors>()!.background,
appBar: const DesktopAppBar(
isCompactHeight: false,
leading: AppBarBackButton(),
trailing: ExitToMyStackButton(),
),
body: SizedBox(
width: 480,
child: child,
),
),
child: ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () {
Navigator.of(context).pop();
},
),
title: Text(
"FROST Multisig options",
style: STextStyles.navBarTitle(context),
),
),
body: child),
),
child: Padding(
padding: const EdgeInsets.only(
top: 12,
left: 16,
right: 16,
),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_OptionButton(
label: "Show participants",
onPressed: () {
Navigator.of(context).pushNamed(
FrostParticipantsView.routeName,
arguments: walletId,
);
},
),
const SizedBox(
height: 8,
),
_OptionButton(
label: "Initiate resharing",
onPressed: () {
// TODO: optimize this by creating watcher providers (similar to normal WalletInfo)
final frostInfo = ref
.read(mainDBProvider)
.isar
.frostWalletInfo
.getByWalletIdSync(walletId)!;
ref.read(pFrostMyName.state).state = frostInfo.myName;
Navigator.of(context).pushNamed(
BeginReshareConfigView.routeName,
arguments: walletId,
);
},
),
const SizedBox(
height: 8,
),
_OptionButton(
label: "Import reshare config",
onPressed: () {
// TODO: optimize this by creating watcher providers (similar to normal WalletInfo)
final frostInfo = ref
.read(mainDBProvider)
.isar
.frostWalletInfo
.getByWalletIdSync(walletId)!;
ref.read(pFrostMyName.state).state = frostInfo.myName;
Navigator.of(context).pushNamed(
ImportReshareConfigView.routeName,
arguments: walletId,
);
},
),
],
),
),
),
),
);
}
}
class _OptionButton extends StatelessWidget {
const _OptionButton({
super.key,
required this.label,
required this.onPressed,
});
final String label;
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
return RoundedWhiteContainer(
padding: const EdgeInsets.all(0),
child: RawMaterialButton(
// splashColor: Theme.of(context).extension<StackColors>()!.highlight,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
onPressed: onPressed,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12.0,
vertical: 20,
),
child: Row(
children: [
Text(
label,
style: STextStyles.titleBold12(context),
),
],
),
),
),
);
}
}

View file

@ -0,0 +1,119 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
import 'package:stackwallet/providers/db/main_db_provider.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/conditional_parent.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';
class FrostParticipantsView extends ConsumerWidget {
const FrostParticipantsView({
super.key,
required this.walletId,
});
static const String routeName = "/frostParticipantsView";
final String walletId;
@override
Widget build(BuildContext context, WidgetRef ref) {
// TODO: optimize this by creating watcher providers (similar to normal WalletInfo)
final frostInfo = ref
.read(mainDBProvider)
.isar
.frostWalletInfo
.getByWalletIdSync(walletId)!;
return ConditionalParent(
condition: Util.isDesktop,
builder: (child) => DesktopScaffold(
background: Theme.of(context).extension<StackColors>()!.background,
appBar: const DesktopAppBar(
isCompactHeight: false,
leading: AppBarBackButton(),
trailing: ExitToMyStackButton(),
),
body: SizedBox(
width: 480,
child: child,
),
),
child: ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () {
Navigator.of(context).pop();
},
),
title: Text(
"Participants",
style: STextStyles.navBarTitle(context),
),
),
body: SafeArea(
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.all(16),
child: child,
),
),
),
);
},
),
),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (int i = 0; i < frostInfo.participants.length; i++)
Padding(
padding: const EdgeInsets.symmetric(
vertical: 8,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Index $i",
style: STextStyles.label(context),
),
const SizedBox(
height: 6,
),
SelectableText(
frostInfo.participants[i] == frostInfo.myName
? "${frostInfo.participants[i]} (me)"
: frostInfo.participants[i],
style: STextStyles.itemSubtitle12(context),
),
],
),
),
],
),
),
);
}
}

View file

@ -0,0 +1,433 @@
import 'dart:ffi';
import 'package:barcode_scan2/barcode_scan2.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/verify_updated_wallet_view.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
import 'package:stackwallet/pages/wallet_view/wallet_view.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart';
import 'package:stackwallet/providers/db/main_db_provider.dart';
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
import 'package:stackwallet/services/frost.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/detail_item.dart';
import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart';
import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart';
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:stackwallet/widgets/textfield_icon_button.dart';
class FinishResharingView extends ConsumerStatefulWidget {
const FinishResharingView({
super.key,
required this.walletId,
});
static const String routeName = "/finishResharingView";
final String walletId;
@override
ConsumerState<FinishResharingView> createState() =>
_FinishResharingViewState();
}
class _FinishResharingViewState extends ConsumerState<FinishResharingView> {
final List<TextEditingController> controllers = [];
final List<FocusNode> focusNodes = [];
late final List<int> resharerIndexes;
late final String myName;
late final int? myResharerIndexIndex;
late final String? myResharerComplete;
late final bool amOutgoingParticipant;
final List<bool> fieldIsEmptyFlags = [];
bool _buttonLock = false;
Future<void> _onPressed() async {
if (_buttonLock) {
return;
}
_buttonLock = true;
try {
if (amOutgoingParticipant) {
ref.read(pFrostResharingData).reset();
Navigator.of(context).popUntil(
ModalRoute.withName(
Util.isDesktop ? DesktopWalletView.routeName : WalletView.routeName,
),
);
} else {
// collect resharer completes strings and insert my own at the correct index
final resharerCompletes = controllers.map((e) => e.text).toList();
if (myResharerIndexIndex != null && myResharerComplete != null) {
resharerCompletes.insert(myResharerIndexIndex!, myResharerComplete!);
}
final data = Frost.finishReshared(
prior: ref.read(pFrostResharingData).startResharedData!.prior.ref,
resharerCompletes: resharerCompletes,
);
ref.read(pFrostResharingData).newWalletData = data;
await Navigator.of(context).pushNamed(
VerifyUpdatedWalletView.routeName,
arguments: widget.walletId,
);
}
} catch (e, s) {
Logging.instance.log(
"$e\n$s",
level: LogLevel.Fatal,
);
if (mounted) {
await showDialog<void>(
context: context,
builder: (_) => StackOkDialog(
title: "Error",
message: e.toString(),
desktopPopRootNavigator: Util.isDesktop,
),
);
}
} finally {
_buttonLock = false;
}
}
@override
void initState() {
final amNewParticipant =
ref.read(pFrostResharingData).startResharerData == null &&
ref.read(pFrostResharingData).incompleteWallet != null &&
ref.read(pFrostResharingData).incompleteWallet?.walletId ==
widget.walletId;
myName = ref.read(pFrostResharingData).myName!;
resharerIndexes = ref.read(pFrostResharingData).configData!.resharers;
if (amNewParticipant) {
myResharerComplete = null;
myResharerIndexIndex = null;
amOutgoingParticipant = false;
} else {
myResharerComplete = ref.read(pFrostResharingData).resharerComplete!;
final frostInfo = ref
.read(mainDBProvider)
.isar
.frostWalletInfo
.getByWalletIdSync(widget.walletId)!;
final myOldIndex =
frostInfo.participants.indexOf(ref.read(pFrostResharingData).myName!);
myResharerIndexIndex = resharerIndexes.indexOf(myOldIndex);
if (myResharerIndexIndex! >= 0) {
// remove my name for now as we don't need a text field for it
resharerIndexes.removeAt(myResharerIndexIndex!);
}
amOutgoingParticipant = !ref
.read(pFrostResharingData)
.configData!
.newParticipants
.contains(ref.read(pFrostResharingData).myName!);
}
for (int i = 0; i < resharerIndexes.length; i++) {
controllers.add(TextEditingController());
focusNodes.add(FocusNode());
fieldIsEmptyFlags.add(true);
}
super.initState();
}
@override
void dispose() {
for (int i = 0; i < controllers.length; i++) {
controllers[i].dispose();
}
for (int i = 0; i < focusNodes.length; i++) {
focusNodes[i].dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return ConditionalParent(
condition: Util.isDesktop,
builder: (child) => DesktopScaffold(
background: Theme.of(context).extension<StackColors>()!.background,
appBar: const DesktopAppBar(
isCompactHeight: false,
leading: AppBarBackButton(),
trailing: ExitToMyStackButton(),
),
body: SizedBox(
width: 480,
child: child,
),
),
child: ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () {
Navigator.of(context).pop();
},
),
title: Text(
"Resharer completes",
style: STextStyles.navBarTitle(context),
),
),
body: SafeArea(
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.all(16),
child: child,
),
),
),
);
},
),
),
),
),
child: Column(
children: [
if (myResharerComplete != null)
SizedBox(
height: 220,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
QrImageView(
data: myResharerComplete!,
size: 220,
backgroundColor: Theme.of(context)
.extension<StackColors>()!
.background,
foregroundColor: Theme.of(context)
.extension<StackColors>()!
.accentColorDark,
),
],
),
),
if (myResharerComplete != null) const _Div(),
if (myResharerComplete != null)
DetailItem(
title: "My resharer complete",
detail: myResharerComplete!,
button: Util.isDesktop
? IconCopyButton(
data: myResharerComplete!,
)
: SimpleCopyButton(
data: myResharerComplete!,
),
),
if (!amOutgoingParticipant) const _Div(),
if (!amOutgoingParticipant)
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (int i = 0; i < resharerIndexes.length; i++)
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
key: Key("frostEncryptionKeyTextFieldKey_$i"),
controller: controllers[i],
focusNode: focusNodes[i],
readOnly: false,
autocorrect: false,
enableSuggestions: false,
style: STextStyles.field(context),
onChanged: (_) {
setState(() {
fieldIsEmptyFlags[i] =
controllers[i].text.isEmpty;
});
},
decoration: standardInputDecoration(
"Enter index "
"${resharerIndexes[i]}"
"'s resharer complete",
focusNodes[i],
context,
).copyWith(
contentPadding: const EdgeInsets.only(
left: 16,
top: 6,
bottom: 8,
right: 5,
),
suffixIcon: Padding(
padding: fieldIsEmptyFlags[i]
? const EdgeInsets.only(right: 8)
: const EdgeInsets.only(right: 0),
child: UnconstrainedBox(
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceAround,
children: [
!fieldIsEmptyFlags[i]
? TextFieldIconButton(
semanticsLabel:
"Clear Button. Clears The Encryption Key Field Input.",
key: Key(
"frostEncryptionKeyClearButtonKey_$i"),
onTap: () {
controllers[i].text = "";
setState(() {
fieldIsEmptyFlags[i] = true;
});
},
child: const XIcon(),
)
: TextFieldIconButton(
semanticsLabel:
"Paste Button. Pastes From Clipboard To Encryption Key Field Input.",
key: Key(
"frostEncryptionKeyPasteButtonKey_$i"),
onTap: () async {
final ClipboardData? data =
await Clipboard.getData(
Clipboard.kTextPlain);
if (data?.text != null &&
data!.text!.isNotEmpty) {
controllers[i].text =
data.text!.trim();
}
setState(() {
fieldIsEmptyFlags[i] =
controllers[i]
.text
.isEmpty;
});
},
child: fieldIsEmptyFlags[i]
? const ClipboardIcon()
: const XIcon(),
),
if (fieldIsEmptyFlags[i])
TextFieldIconButton(
semanticsLabel: "Scan QR Button. "
"Opens Camera For Scanning QR Code.",
key: Key("frostScanQrButtonKey_$i"),
onTap: () async {
try {
if (FocusScope.of(context)
.hasFocus) {
FocusScope.of(context)
.unfocus();
await Future<void>.delayed(
const Duration(
milliseconds: 75));
}
final qrResult =
await BarcodeScanner.scan();
controllers[i].text =
qrResult.rawContent;
setState(() {
fieldIsEmptyFlags[i] =
controllers[i]
.text
.isEmpty;
});
} on PlatformException catch (e, s) {
Logging.instance.log(
"Failed to get camera permissions "
"while trying to scan qr code: $e\n$s",
level: LogLevel.Warning,
);
}
},
child: const QrCodeIcon(),
),
],
),
),
),
),
),
),
),
],
),
],
),
if (!Util.isDesktop) const Spacer(),
const _Div(),
PrimaryButton(
label: amOutgoingParticipant ? "Exit" : "Complete",
enabled: amOutgoingParticipant ||
!fieldIsEmptyFlags.reduce((v, e) => v |= e),
onPressed: _onPressed,
),
],
),
),
);
}
}
class _Div extends StatelessWidget {
const _Div({super.key});
@override
Widget build(BuildContext context) {
return const SizedBox(
height: 12,
);
}
}

View file

@ -0,0 +1,196 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/complete_reshare_config_view.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
import 'package:stackwallet/providers/db/main_db_provider.dart';
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/conditional_parent.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/desktop/primary_button.dart';
final class BeginReshareConfigView extends ConsumerStatefulWidget {
const BeginReshareConfigView({
super.key,
required this.walletId,
});
static const String routeName = "/beginReshareConfigView";
final String walletId;
@override
ConsumerState<BeginReshareConfigView> createState() =>
_BeginReshareConfigViewState();
}
class _BeginReshareConfigViewState
extends ConsumerState<BeginReshareConfigView> {
late final int currentThreshold;
late final List<String> currentParticipants;
final Map<String, int> pFrostResharersMap = {};
@override
void initState() {
ref.read(pFrostResharingData).reset();
// TODO: optimize this by creating watcher providers (similar to normal WalletInfo)
final frostInfo = ref
.read(mainDBProvider)
.isar
.frostWalletInfo
.getByWalletIdSync(widget.walletId)!;
currentThreshold = frostInfo.threshold;
currentParticipants = frostInfo.participants;
super.initState();
}
@override
Widget build(BuildContext context) {
return ConditionalParent(
condition: Util.isDesktop,
builder: (child) => DesktopScaffold(
background: Theme.of(context).extension<StackColors>()!.background,
appBar: const DesktopAppBar(
isCompactHeight: false,
leading: AppBarBackButton(),
trailing: ExitToMyStackButton(),
),
body: SizedBox(
width: 480,
child: child,
),
),
child: ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () {
Navigator.of(context).pop();
},
),
// title: Text(
// "Modify Participants",
// style: STextStyles.navBarTitle(context),
// ),
),
body: SafeArea(
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.all(16),
child: child,
),
),
),
);
},
),
),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Select participants for resharing",
style: STextStyles.label(context),
),
const SizedBox(
height: 16,
),
Column(
children: [
for (int i = 0; i < currentParticipants.length; i++)
Padding(
padding: const EdgeInsets.only(
top: 10,
),
child: RawMaterialButton(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
onPressed: () {
if (pFrostResharersMap[currentParticipants[i]] ==
null) {
pFrostResharersMap[currentParticipants[i]] = i;
} else {
pFrostResharersMap.remove(currentParticipants[i]);
}
setState(() {});
},
child: Container(
color: Colors.transparent,
child: IgnorePointer(
child: Row(
children: [
Checkbox(
value: pFrostResharersMap[
currentParticipants[i]] ==
i,
onChanged: (bool? value) {},
),
const SizedBox(
width: 10,
),
Text(
currentParticipants[i],
style: STextStyles.itemSubtitle12(context),
),
],
),
),
),
),
),
],
),
if (!Util.isDesktop) const Spacer(),
const SizedBox(
height: 16,
),
PrimaryButton(
label: "Continue",
enabled: pFrostResharersMap.length >= currentThreshold,
onPressed: () async {
await Navigator.of(context).pushNamed(
CompleteReshareConfigView.routeName,
arguments: (
walletId: widget.walletId,
resharers:
pFrostResharersMap.values.toList(growable: false),
),
);
},
),
],
),
),
);
}
}

View file

@ -0,0 +1,335 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:frostdart/frostdart.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/display_reshare_config_view.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
import 'package:stackwallet/providers/db/main_db_provider.dart';
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
import 'package:stackwallet/services/frost.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/format.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/conditional_parent.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/desktop/primary_button.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
final class CompleteReshareConfigView extends ConsumerStatefulWidget {
const CompleteReshareConfigView({
super.key,
required this.walletId,
required this.resharers,
});
static const String routeName = "/completeReshareConfigView";
final String walletId;
final List<int> resharers;
@override
ConsumerState<CompleteReshareConfigView> createState() =>
_CompleteReshareConfigViewState();
}
class _CompleteReshareConfigViewState
extends ConsumerState<CompleteReshareConfigView> {
final _newThresholdController = TextEditingController();
final _newParticipantsCountController = TextEditingController();
final List<TextEditingController> controllers = [];
int _participantsCount = 0;
bool _buttonLock = false;
Future<void> _onPressed() async {
if (_buttonLock) {
return;
}
_buttonLock = true;
try {
// TODO: optimize this by creating watcher providers (similar to normal WalletInfo)
final frostInfo = ref
.read(mainDBProvider)
.isar
.frostWalletInfo
.getByWalletIdSync(widget.walletId)!;
final validationMessage = _validateInputData();
if (validationMessage != "valid") {
return await showDialog<void>(
context: context,
builder: (_) => StackOkDialog(
title: validationMessage,
desktopPopRootNavigator: Util.isDesktop,
),
);
}
final config = Frost.createResharerConfig(
newThreshold: int.parse(_newThresholdController.text),
resharers: widget.resharers,
newParticipants: controllers.map((e) => e.text).toList(),
);
final salt = Format.uint8listToString(
resharerSalt(resharerConfig: config),
);
if (frostInfo.knownSalts.contains(salt)) {
return await showDialog<void>(
context: context,
builder: (_) => StackOkDialog(
title: "Duplicate config salt",
desktopPopRootNavigator: Util.isDesktop,
),
);
} else {
final salts = frostInfo.knownSalts; // Fixed length list.
final newSalts = List<String>.from(salts)..add(salt);
final mainDB = ref.read(mainDBProvider);
await mainDB.isar.writeTxn(() async {
final info = frostInfo;
await mainDB.isar.frostWalletInfo.delete(info.id);
await mainDB.isar.frostWalletInfo.put(
info.copyWith(knownSalts: newSalts),
);
});
}
ref.read(pFrostResharingData).myName = frostInfo.myName;
ref.read(pFrostResharingData).resharerConfig = config;
if (mounted) {
await Navigator.of(context).pushNamed(
DisplayReshareConfigView.routeName,
arguments: widget.walletId,
);
}
} catch (e, s) {
Logging.instance.log(
"$e\n$s",
level: LogLevel.Fatal,
);
if (mounted) {
await showDialog<void>(
context: context,
builder: (_) => StackOkDialog(
title: e.toString(),
desktopPopRootNavigator: Util.isDesktop,
),
);
}
} finally {
_buttonLock = false;
}
}
String _validateInputData() {
final threshold = int.tryParse(_newThresholdController.text);
if (threshold == null) {
return "Choose a threshold";
}
final partsCount = int.tryParse(_newParticipantsCountController.text);
if (partsCount == null) {
return "Choose total number of participants";
}
if (threshold > partsCount) {
return "Threshold cannot be greater than the number of participants";
}
if (partsCount < 2) {
return "At least two participants required";
}
if (controllers.length != partsCount) {
return "Participants count error";
}
final hasEmptyParticipants = controllers
.map((e) => e.text.isEmpty)
.reduce((value, element) => value |= element);
if (hasEmptyParticipants) {
return "Participants must not be empty";
}
if (controllers.length != controllers.map((e) => e.text).toSet().length) {
return "Duplicate participant name found";
}
return "valid";
}
void _participantsCountChanged(String newValue) {
final count = int.tryParse(newValue);
if (count != null) {
if (count > _participantsCount) {
for (int i = _participantsCount; i < count; i++) {
controllers.add(TextEditingController());
}
_participantsCount = count;
setState(() {});
} else if (count < _participantsCount) {
for (int i = _participantsCount; i > count; i--) {
final last = controllers.removeLast();
last.dispose();
}
_participantsCount = count;
setState(() {});
}
}
}
@override
void dispose() {
_newThresholdController.dispose();
_newParticipantsCountController.dispose();
for (final e in controllers) {
e.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return ConditionalParent(
condition: Util.isDesktop,
builder: (child) => DesktopScaffold(
background: Theme.of(context).extension<StackColors>()!.background,
appBar: const DesktopAppBar(
isCompactHeight: false,
leading: AppBarBackButton(),
trailing: ExitToMyStackButton(),
),
body: SizedBox(
width: 480,
child: child,
),
),
child: ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () {
Navigator.of(context).pop();
},
),
title: Text(
"Modify Participants",
style: STextStyles.navBarTitle(context),
),
),
body: SafeArea(
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.all(16),
child: child,
),
),
),
);
},
),
),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"New threshold",
style: STextStyles.label(context),
),
const SizedBox(
height: 10,
),
TextField(
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
controller: _newThresholdController,
),
const SizedBox(
height: 16,
),
Text(
"Number of participants",
style: STextStyles.label(context),
),
const SizedBox(
height: 10,
),
TextField(
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
controller: _newParticipantsCountController,
onChanged: _participantsCountChanged,
),
const SizedBox(
height: 16,
),
if (controllers.isNotEmpty)
Text(
"Participants",
style: STextStyles.label(context),
),
if (controllers.isNotEmpty)
const SizedBox(
height: 10,
),
if (controllers.isNotEmpty)
Column(
children: [
for (int i = 0; i < controllers.length; i++)
Padding(
padding: const EdgeInsets.only(
top: 10,
),
child: TextField(
controller: controllers[i],
),
),
],
),
if (!Util.isDesktop) const Spacer(),
const SizedBox(
height: 16,
),
PrimaryButton(
label: "Generate config",
onPressed: () async {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
}
await _onPressed();
},
),
],
),
),
);
}
}

View file

@ -0,0 +1,214 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/begin_resharing_view.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
import 'package:stackwallet/providers/db/main_db_provider.dart';
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/services/frost.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart';
import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/detail_item.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
class DisplayReshareConfigView extends ConsumerStatefulWidget {
const DisplayReshareConfigView({
super.key,
required this.walletId,
});
static const String routeName = "/displayReshareConfigView";
final String walletId;
@override
ConsumerState<DisplayReshareConfigView> createState() =>
_DisplayReshareConfigViewState();
}
class _DisplayReshareConfigViewState
extends ConsumerState<DisplayReshareConfigView> {
late final bool iAmInvolved;
bool _buttonLock = false;
Future<void> _onPressed() async {
if (_buttonLock) {
return;
}
_buttonLock = true;
try {
final wallet =
ref.read(pWallets).getWallet(widget.walletId) as BitcoinFrostWallet;
final serializedKeys = await wallet.getSerializedKeys();
if (mounted) {
final result = Frost.beginResharer(
serializedKeys: serializedKeys!,
config: ref.read(pFrostResharingData).resharerConfig!,
);
ref.read(pFrostResharingData).startResharerData = result;
await Navigator.of(context).pushNamed(
BeginResharingView.routeName,
arguments: widget.walletId,
);
}
} catch (e, s) {
Logging.instance.log(
"$e\n$s",
level: LogLevel.Fatal,
);
if (mounted) {
await showDialog<void>(
context: context,
builder: (_) => StackOkDialog(
title: e.toString(),
desktopPopRootNavigator: Util.isDesktop,
),
);
}
} finally {
_buttonLock = false;
}
}
@override
void initState() {
// TODO: optimize this by creating watcher providers (similar to normal WalletInfo)
final frostInfo = ref
.read(mainDBProvider)
.isar
.frostWalletInfo
.getByWalletIdSync(widget.walletId)!;
final myOldIndex = frostInfo.participants.indexOf(frostInfo.myName);
iAmInvolved = ref
.read(pFrostResharingData)
.configData!
.resharers
.contains(myOldIndex);
super.initState();
}
@override
Widget build(BuildContext context) {
return ConditionalParent(
condition: Util.isDesktop,
builder: (child) => DesktopScaffold(
background: Theme.of(context).extension<StackColors>()!.background,
appBar: const DesktopAppBar(
isCompactHeight: false,
leading: AppBarBackButton(),
),
body: SizedBox(
width: 480,
child: child,
),
),
child: ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () {
Navigator.of(context).pop();
},
),
title: Text(
"Resharer config",
style: STextStyles.navBarTitle(context),
),
),
body: SafeArea(
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.all(16),
child: child,
),
),
),
);
},
),
),
),
),
child: Column(
children: [
if (!Util.isDesktop) const Spacer(),
SizedBox(
height: 220,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
QrImageView(
data: ref.watch(pFrostResharingData).resharerConfig!,
size: 220,
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
foregroundColor: Theme.of(context)
.extension<StackColors>()!
.accentColorDark,
),
],
),
),
const SizedBox(
height: 32,
),
DetailItem(
title: "Config",
detail: ref.watch(pFrostResharingData).resharerConfig!,
button: Util.isDesktop
? IconCopyButton(
data: ref.watch(pFrostResharingData).resharerConfig!,
)
: SimpleCopyButton(
data: ref.watch(pFrostResharingData).resharerConfig!,
),
),
SizedBox(
height: Util.isDesktop ? 64 : 16,
),
if (!Util.isDesktop)
const Spacer(
flex: 2,
),
if (iAmInvolved)
PrimaryButton(
label: "Start resharing",
onPressed: _onPressed,
),
],
),
),
);
}
}

View file

@ -0,0 +1,338 @@
import 'package:barcode_scan2/barcode_scan2.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:frostdart/frostdart.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/begin_resharing_view.dart';
import 'package:stackwallet/providers/db/main_db_provider.dart';
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
import 'package:stackwallet/providers/global/secure_store_provider.dart';
import 'package:stackwallet/services/frost.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/format.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/conditional_parent.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/desktop/primary_button.dart';
import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart';
import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart';
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:stackwallet/widgets/textfield_icon_button.dart';
class ImportReshareConfigView extends ConsumerStatefulWidget {
const ImportReshareConfigView({
super.key,
required this.walletId,
});
static const String routeName = "/importReshareConfigView";
final String walletId;
@override
ConsumerState<ImportReshareConfigView> createState() =>
_ImportReshareConfigViewState();
}
class _ImportReshareConfigViewState
extends ConsumerState<ImportReshareConfigView> {
late final TextEditingController configFieldController;
late final FocusNode configFocusNode;
bool _configEmpty = true;
bool _buttonLock = false;
Future<void> _onPressed() async {
if (_buttonLock) {
return;
}
_buttonLock = true;
try {
// TODO: optimize this by creating watcher providers (similar to normal WalletInfo)
final frostInfo = ref
.read(mainDBProvider)
.isar
.frostWalletInfo
.getByWalletIdSync(widget.walletId)!;
ref.read(pFrostResharingData).reset();
ref.read(pFrostResharingData).myName = frostInfo.myName;
ref.read(pFrostResharingData).resharerConfig = configFieldController.text;
String? salt;
try {
salt = Format.uint8listToString(
resharerSalt(
resharerConfig: ref.read(pFrostResharingData).resharerConfig!,
),
);
} catch (_) {
throw Exception("Bad resharer config");
}
if (frostInfo.knownSalts.contains(salt)) {
throw Exception("Duplicate config salt");
} else {
final salts = frostInfo.knownSalts;
salts.add(salt);
final mainDB = ref.read(mainDBProvider);
await mainDB.isar.writeTxn(() async {
final info = frostInfo;
await mainDB.isar.frostWalletInfo.delete(info.id);
await mainDB.isar.frostWalletInfo.put(
info.copyWith(knownSalts: salts),
);
});
}
final serializedKeys = await ref.read(secureStoreProvider).read(
key: "{${widget.walletId}}_serializedFROSTKeys",
);
if (mounted) {
final result = Frost.beginResharer(
serializedKeys: serializedKeys!,
config: ref.read(pFrostResharingData).resharerConfig!,
);
ref.read(pFrostResharingData).startResharerData = result;
await Navigator.of(context).pushNamed(
BeginResharingView.routeName,
arguments: widget.walletId,
);
}
} catch (e, s) {
Logging.instance.log(
"$e\n$s",
level: LogLevel.Fatal,
);
if (mounted) {
await showDialog<void>(
context: context,
builder: (_) => StackOkDialog(
title: e.toString(),
desktopPopRootNavigator: Util.isDesktop,
),
);
}
} finally {
_buttonLock = false;
}
}
@override
void initState() {
configFieldController = TextEditingController();
configFocusNode = FocusNode();
super.initState();
}
@override
void dispose() {
configFieldController.dispose();
configFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ConditionalParent(
condition: Util.isDesktop,
builder: (child) => DesktopScaffold(
background: Theme.of(context).extension<StackColors>()!.background,
appBar: const DesktopAppBar(
isCompactHeight: false,
leading: AppBarBackButton(),
),
body: SizedBox(
width: 480,
child: child,
),
),
child: ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: const AppBarBackButton(),
title: Text(
"Import FROST reshare config",
style: STextStyles.navBarTitle(context),
),
),
body: SafeArea(
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.all(16),
child: child,
),
),
),
);
},
),
),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 16,
),
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
key: const Key("frConfigTextFieldKey"),
controller: configFieldController,
onChanged: (_) {
setState(() {
_configEmpty = configFieldController.text.isEmpty;
});
},
focusNode: configFocusNode,
readOnly: false,
autocorrect: false,
enableSuggestions: false,
style: STextStyles.field(context),
decoration: standardInputDecoration(
"Enter config",
configFocusNode,
context,
).copyWith(
contentPadding: const EdgeInsets.only(
left: 16,
top: 6,
bottom: 8,
right: 5,
),
suffixIcon: Padding(
padding: _configEmpty
? const EdgeInsets.only(right: 8)
: const EdgeInsets.only(right: 0),
child: UnconstrainedBox(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
!_configEmpty
? TextFieldIconButton(
semanticsLabel:
"Clear Button. Clears The Config Field.",
key: const Key("frConfigClearButtonKey"),
onTap: () {
configFieldController.text = "";
setState(() {
_configEmpty = true;
});
},
child: const XIcon(),
)
: TextFieldIconButton(
semanticsLabel:
"Paste Button. Pastes From Clipboard To Config Field Input.",
key: const Key("frConfigPasteButtonKey"),
onTap: () async {
final ClipboardData? data =
await Clipboard.getData(
Clipboard.kTextPlain);
if (data?.text != null &&
data!.text!.isNotEmpty) {
configFieldController.text =
data.text!.trim();
}
setState(() {
_configEmpty =
configFieldController.text.isEmpty;
});
},
child: _configEmpty
? const ClipboardIcon()
: const XIcon(),
),
if (_configEmpty)
TextFieldIconButton(
semanticsLabel:
"Scan QR Button. Opens Camera For Scanning QR Code.",
key: const Key("frConfigScanQrButtonKey"),
onTap: () async {
try {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(
const Duration(milliseconds: 75));
}
final qrResult = await BarcodeScanner.scan();
configFieldController.text =
qrResult.rawContent;
setState(() {
_configEmpty =
configFieldController.text.isEmpty;
});
} on PlatformException catch (e, s) {
Logging.instance.log(
"Failed to get camera permissions while trying to scan qr code: $e\n$s",
level: LogLevel.Warning,
);
}
},
child: const QrCodeIcon(),
)
],
),
),
),
),
),
),
const SizedBox(
height: 16,
),
if (!Util.isDesktop) const Spacer(),
const SizedBox(
height: 16,
),
PrimaryButton(
label: "Start resharing",
enabled: !_configEmpty,
onPressed: () async {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
}
await _onPressed();
},
),
],
),
),
);
}
}

View file

@ -0,0 +1,439 @@
import 'package:barcode_scan2/barcode_scan2.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/continue_resharing_view.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
import 'package:stackwallet/pages/wallet_view/wallet_view.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart';
import 'package:stackwallet/providers/db/main_db_provider.dart';
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
import 'package:stackwallet/services/frost.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/detail_item.dart';
import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.dart';
import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart';
import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart';
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:stackwallet/widgets/textfield_icon_button.dart';
class BeginResharingView extends ConsumerStatefulWidget {
const BeginResharingView({
super.key,
required this.walletId,
});
static const String routeName = "/beginResharingView";
final String walletId;
@override
ConsumerState<BeginResharingView> createState() => _BeginResharingViewState();
}
class _BeginResharingViewState extends ConsumerState<BeginResharingView> {
final List<TextEditingController> controllers = [];
final List<FocusNode> focusNodes = [];
late final List<int> resharerIndexes;
late final int myResharerIndexIndex;
late final String myResharerStart;
late final bool amOutgoingParticipant;
final List<bool> fieldIsEmptyFlags = [];
bool _buttonLock = false;
Future<void> _onPressed() async {
if (_buttonLock) {
return;
}
_buttonLock = true;
try {
if (!amOutgoingParticipant) {
// collect resharer strings
final resharerStarts = controllers.map((e) => e.text).toList();
if (myResharerIndexIndex >= 0) {
// only insert my own at the correct index if I am a resharer
resharerStarts.insert(myResharerIndexIndex, myResharerStart);
}
final result = Frost.beginReshared(
myName: ref.read(pFrostResharingData).myName!,
resharerConfig: ref.read(pFrostResharingData).resharerConfig!,
resharerStarts: resharerStarts,
);
ref.read(pFrostResharingData).startResharedData = result;
}
await Navigator.of(context).pushNamed(
ContinueResharingView.routeName,
arguments: widget.walletId,
);
} catch (e, s) {
Logging.instance.log(
"$e\n$s",
level: LogLevel.Fatal,
);
if (mounted) {
await showDialog<void>(
context: context,
builder: (_) => StackOkDialog(
title: "Error",
message: e.toString(),
desktopPopRootNavigator: Util.isDesktop,
),
);
}
} finally {
_buttonLock = false;
}
}
@override
void initState() {
// TODO: optimize this by creating watcher providers (similar to normal WalletInfo)
final frostInfo = ref
.read(mainDBProvider)
.isar
.frostWalletInfo
.getByWalletIdSync(widget.walletId)!;
final myOldIndex =
frostInfo.participants.indexOf(ref.read(pFrostResharingData).myName!);
myResharerStart =
ref.read(pFrostResharingData).startResharerData!.resharerStart;
resharerIndexes = ref.read(pFrostResharingData).configData!.resharers;
myResharerIndexIndex = resharerIndexes.indexOf(myOldIndex);
if (myResharerIndexIndex >= 0) {
// remove my name for now as we don't need a text field for it
resharerIndexes.removeAt(myResharerIndexIndex);
}
amOutgoingParticipant = !ref
.read(pFrostResharingData)
.configData!
.newParticipants
.contains(ref.read(pFrostResharingData).myName!);
for (int i = 0; i < resharerIndexes.length; i++) {
controllers.add(TextEditingController());
focusNodes.add(FocusNode());
fieldIsEmptyFlags.add(true);
}
super.initState();
}
@override
void dispose() {
for (int i = 0; i < controllers.length; i++) {
controllers[i].dispose();
}
for (int i = 0; i < focusNodes.length; i++) {
focusNodes[i].dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
await showDialog<void>(
context: context,
builder: (_) => FrostInterruptionDialog(
type: FrostInterruptionDialogType.resharing,
popUntilOnYesRouteName: Util.isDesktop
? DesktopWalletView.routeName
: WalletView.routeName,
),
);
return false;
},
child: ConditionalParent(
condition: Util.isDesktop,
builder: (child) => DesktopScaffold(
background: Theme.of(context).extension<StackColors>()!.background,
appBar: DesktopAppBar(
isCompactHeight: false,
leading: AppBarBackButton(
onPressed: () async {
await showDialog<void>(
context: context,
builder: (_) => const FrostInterruptionDialog(
type: FrostInterruptionDialogType.resharing,
popUntilOnYesRouteName: DesktopWalletView.routeName,
),
);
},
),
),
body: SizedBox(
width: 480,
child: child,
),
),
child: ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () async {
await showDialog<void>(
context: context,
builder: (_) => const FrostInterruptionDialog(
type: FrostInterruptionDialogType.resharing,
popUntilOnYesRouteName: WalletView.routeName,
),
);
},
),
title: Text(
"Resharers",
style: STextStyles.navBarTitle(context),
),
),
body: SafeArea(
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.all(16),
child: child,
),
),
),
);
},
),
),
),
),
child: Column(
children: [
SizedBox(
height: 220,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
QrImageView(
data: myResharerStart,
size: 220,
backgroundColor: Theme.of(context)
.extension<StackColors>()!
.background,
foregroundColor: Theme.of(context)
.extension<StackColors>()!
.accentColorDark,
),
],
),
),
const _Div(),
DetailItem(
title: "My resharer",
detail: myResharerStart,
button: Util.isDesktop
? IconCopyButton(
data: myResharerStart,
)
: SimpleCopyButton(
data: myResharerStart,
),
),
const _Div(),
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (int i = 0; i < resharerIndexes.length; i++)
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
key: Key("frostResharerTextFieldKey_$i"),
controller: controllers[i],
focusNode: focusNodes[i],
readOnly: false,
autocorrect: false,
enableSuggestions: false,
style: STextStyles.field(context),
onChanged: (_) {
setState(() {
fieldIsEmptyFlags[i] =
controllers[i].text.isEmpty;
});
},
decoration: standardInputDecoration(
"Enter index "
"${resharerIndexes[i]}"
"'s resharer",
focusNodes[i],
context,
).copyWith(
contentPadding: const EdgeInsets.only(
left: 16,
top: 6,
bottom: 8,
right: 5,
),
suffixIcon: Padding(
padding: fieldIsEmptyFlags[i]
? const EdgeInsets.only(right: 8)
: const EdgeInsets.only(right: 0),
child: UnconstrainedBox(
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceAround,
children: [
!fieldIsEmptyFlags[i]
? TextFieldIconButton(
semanticsLabel:
"Clear Button. Clears The Resharer Field Input.",
key: Key(
"frostResharerClearButtonKey_$i"),
onTap: () {
controllers[i].text = "";
setState(() {
fieldIsEmptyFlags[i] = true;
});
},
child: const XIcon(),
)
: TextFieldIconButton(
semanticsLabel:
"Paste Button. Pastes From Clipboard To Resharer Field Input.",
key: Key(
"frostResharerPasteButtonKey_$i"),
onTap: () async {
final ClipboardData? data =
await Clipboard.getData(
Clipboard.kTextPlain);
if (data?.text != null &&
data!.text!.isNotEmpty) {
controllers[i].text =
data.text!.trim();
}
setState(() {
fieldIsEmptyFlags[i] =
controllers[i]
.text
.isEmpty;
});
},
child: fieldIsEmptyFlags[i]
? const ClipboardIcon()
: const XIcon(),
),
if (fieldIsEmptyFlags[i])
TextFieldIconButton(
semanticsLabel: "Scan QR Button. "
"Opens Camera For Scanning QR Code.",
key: Key(
"frostCommitmentsScanQrButtonKey_$i"),
onTap: () async {
try {
if (FocusScope.of(context)
.hasFocus) {
FocusScope.of(context)
.unfocus();
await Future<void>.delayed(
const Duration(
milliseconds: 75));
}
final qrResult =
await BarcodeScanner.scan();
controllers[i].text =
qrResult.rawContent;
setState(() {
fieldIsEmptyFlags[i] =
controllers[i]
.text
.isEmpty;
});
} on PlatformException catch (e, s) {
Logging.instance.log(
"Failed to get camera permissions "
"while trying to scan qr code: $e\n$s",
level: LogLevel.Warning,
);
}
},
child: const QrCodeIcon(),
),
],
),
),
),
),
),
),
),
],
),
],
),
if (!Util.isDesktop) const Spacer(),
const _Div(),
PrimaryButton(
label: "Continue",
enabled: amOutgoingParticipant ||
!fieldIsEmptyFlags.reduce((v, e) => v |= e),
onPressed: _onPressed,
),
],
),
),
),
);
}
}
class _Div extends StatelessWidget {
const _Div({super.key});
@override
Widget build(BuildContext context) {
return const SizedBox(
height: 12,
);
}
}

View file

@ -0,0 +1,429 @@
import 'dart:ffi';
import 'package:barcode_scan2/barcode_scan2.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/finish_resharing_view.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
import 'package:stackwallet/pages/wallet_view/wallet_view.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart';
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
import 'package:stackwallet/services/frost.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/detail_item.dart';
import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.dart';
import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart';
import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart';
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:stackwallet/widgets/textfield_icon_button.dart';
class ContinueResharingView extends ConsumerStatefulWidget {
const ContinueResharingView({
super.key,
required this.walletId,
});
static const String routeName = "/continueResharingView";
final String walletId;
@override
ConsumerState<ContinueResharingView> createState() =>
_ContinueResharingViewState();
}
class _ContinueResharingViewState extends ConsumerState<ContinueResharingView> {
final List<TextEditingController> controllers = [];
final List<FocusNode> focusNodes = [];
late final List<String> newParticipants;
late final int myIndex;
late final String? myEncryptionKey;
late final bool amOutgoingParticipant;
final List<bool> fieldIsEmptyFlags = [];
bool _buttonLock = false;
Future<void> _onPressed() async {
if (_buttonLock) {
return;
}
_buttonLock = true;
try {
// collect encryptionKeys strings and insert my own at the correct index
final encryptionKeys = controllers.map((e) => e.text).toList();
if (!amOutgoingParticipant) {
encryptionKeys.insert(myIndex, myEncryptionKey!);
}
final result = Frost.finishResharer(
machine: ref.read(pFrostResharingData).startResharerData!.machine.ref,
encryptionKeysOfResharedTo: encryptionKeys,
);
ref.read(pFrostResharingData).resharerComplete = result;
await Navigator.of(context).pushNamed(
FinishResharingView.routeName,
arguments: widget.walletId,
);
} catch (e, s) {
Logging.instance.log(
"$e\n$s",
level: LogLevel.Fatal,
);
await showDialog<void>(
context: context,
builder: (_) => StackOkDialog(
title: "Error",
message: e.toString(),
desktopPopRootNavigator: Util.isDesktop,
),
);
} finally {
_buttonLock = false;
}
}
@override
void initState() {
myEncryptionKey =
ref.read(pFrostResharingData).startResharedData?.resharedStart;
newParticipants = ref.read(pFrostResharingData).configData!.newParticipants;
myIndex = newParticipants.indexOf(ref.read(pFrostResharingData).myName!);
if (myIndex >= 0) {
// remove my name for now as we don't need a text field for it
newParticipants.removeAt(myIndex);
}
if (myEncryptionKey == null && myIndex == -1) {
amOutgoingParticipant = true;
} else if (myEncryptionKey != null && myIndex >= 0) {
amOutgoingParticipant = false;
} else {
throw Exception("Invalid resharing state");
}
for (int i = 0; i < newParticipants.length; i++) {
controllers.add(TextEditingController());
focusNodes.add(FocusNode());
fieldIsEmptyFlags.add(true);
}
super.initState();
}
@override
void dispose() {
for (int i = 0; i < controllers.length; i++) {
controllers[i].dispose();
}
for (int i = 0; i < focusNodes.length; i++) {
focusNodes[i].dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
await showDialog<void>(
context: context,
builder: (_) => FrostInterruptionDialog(
type: FrostInterruptionDialogType.resharing,
popUntilOnYesRouteName: Util.isDesktop
? DesktopWalletView.routeName
: WalletView.routeName,
),
);
return false;
},
child: ConditionalParent(
condition: Util.isDesktop,
builder: (child) => DesktopScaffold(
background: Theme.of(context).extension<StackColors>()!.background,
appBar: DesktopAppBar(
isCompactHeight: false,
leading: AppBarBackButton(
onPressed: () async {
await showDialog<void>(
context: context,
builder: (_) => const FrostInterruptionDialog(
type: FrostInterruptionDialogType.resharing,
popUntilOnYesRouteName: DesktopWalletView.routeName,
),
);
},
),
),
body: SizedBox(
width: 480,
child: child,
),
),
child: ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () async {
await showDialog<void>(
context: context,
builder: (_) => const FrostInterruptionDialog(
type: FrostInterruptionDialogType.resharing,
popUntilOnYesRouteName: WalletView.routeName,
),
);
},
),
title: Text(
"Encryption keys",
style: STextStyles.navBarTitle(context),
),
),
body: SafeArea(
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.all(16),
child: child,
),
),
),
);
},
),
),
),
),
child: Column(
children: [
if (!amOutgoingParticipant)
SizedBox(
height: 220,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
QrImageView(
data: myEncryptionKey!,
size: 220,
backgroundColor: Theme.of(context)
.extension<StackColors>()!
.background,
foregroundColor: Theme.of(context)
.extension<StackColors>()!
.accentColorDark,
),
],
),
),
if (!amOutgoingParticipant) const _Div(),
if (!amOutgoingParticipant)
DetailItem(
title: "My encryption key",
detail: myEncryptionKey!,
button: Util.isDesktop
? IconCopyButton(
data: myEncryptionKey!,
)
: SimpleCopyButton(
data: myEncryptionKey!,
),
),
if (!amOutgoingParticipant) const _Div(),
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (int i = 0; i < newParticipants.length; i++)
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
key: Key("frostEncryptionKeyTextFieldKey_$i"),
controller: controllers[i],
focusNode: focusNodes[i],
readOnly: false,
autocorrect: false,
enableSuggestions: false,
style: STextStyles.field(context),
onChanged: (_) {
setState(() {
fieldIsEmptyFlags[i] =
controllers[i].text.isEmpty;
});
},
decoration: standardInputDecoration(
"Enter "
"${newParticipants[i]}"
"'s encryption key",
focusNodes[i],
context,
).copyWith(
contentPadding: const EdgeInsets.only(
left: 16,
top: 6,
bottom: 8,
right: 5,
),
suffixIcon: Padding(
padding: fieldIsEmptyFlags[i]
? const EdgeInsets.only(right: 8)
: const EdgeInsets.only(right: 0),
child: UnconstrainedBox(
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceAround,
children: [
!fieldIsEmptyFlags[i]
? TextFieldIconButton(
semanticsLabel:
"Clear Button. Clears The Encryption Key Field Input.",
key: Key(
"frostEncryptionKeyClearButtonKey_$i"),
onTap: () {
controllers[i].text = "";
setState(() {
fieldIsEmptyFlags[i] = true;
});
},
child: const XIcon(),
)
: TextFieldIconButton(
semanticsLabel:
"Paste Button. Pastes From Clipboard To Encryption Key Field Input.",
key: Key(
"frostEncryptionKeyPasteButtonKey_$i"),
onTap: () async {
final ClipboardData? data =
await Clipboard.getData(
Clipboard.kTextPlain);
if (data?.text != null &&
data!.text!.isNotEmpty) {
controllers[i].text =
data.text!.trim();
}
setState(() {
fieldIsEmptyFlags[i] =
controllers[i]
.text
.isEmpty;
});
},
child: fieldIsEmptyFlags[i]
? const ClipboardIcon()
: const XIcon(),
),
if (fieldIsEmptyFlags[i])
TextFieldIconButton(
semanticsLabel: "Scan QR Button. "
"Opens Camera For Scanning QR Code.",
key: Key(
"frostCommitmentsScanQrButtonKey_$i"),
onTap: () async {
try {
if (FocusScope.of(context)
.hasFocus) {
FocusScope.of(context)
.unfocus();
await Future<void>.delayed(
const Duration(
milliseconds: 75));
}
final qrResult =
await BarcodeScanner.scan();
controllers[i].text =
qrResult.rawContent;
setState(() {
fieldIsEmptyFlags[i] =
controllers[i]
.text
.isEmpty;
});
} on PlatformException catch (e, s) {
Logging.instance.log(
"Failed to get camera permissions "
"while trying to scan qr code: $e\n$s",
level: LogLevel.Warning,
);
}
},
child: const QrCodeIcon(),
),
],
),
),
),
),
),
),
),
],
),
],
),
if (!Util.isDesktop) const Spacer(),
const _Div(),
PrimaryButton(
label: "Continue",
enabled: !fieldIsEmptyFlags.reduce((v, e) => v |= e),
onPressed: _onPressed,
),
],
),
),
),
);
}
}
class _Div extends StatelessWidget {
const _Div({super.key});
@override
Widget build(BuildContext context) {
return const SizedBox(
height: 12,
);
}
}

View file

@ -0,0 +1,204 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:stackwallet/pages/home_view/home_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/finish_resharing_view.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/detail_item.dart';
import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.dart';
class NewContinueSharingView extends ConsumerStatefulWidget {
const NewContinueSharingView({
super.key,
required this.walletId,
});
static const String routeName = "/NewContinueSharingView";
final String walletId;
@override
ConsumerState<NewContinueSharingView> createState() =>
_NewContinueSharingViewState();
}
class _NewContinueSharingViewState
extends ConsumerState<NewContinueSharingView> {
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
await showDialog<void>(
context: context,
builder: (_) => FrostInterruptionDialog(
type: FrostInterruptionDialogType.resharing,
popUntilOnYesRouteName:
Util.isDesktop ? DesktopHomeView.routeName : HomeView.routeName,
),
);
return false;
},
child: ConditionalParent(
condition: Util.isDesktop,
builder: (child) => DesktopScaffold(
background: Theme.of(context).extension<StackColors>()!.background,
appBar: DesktopAppBar(
isCompactHeight: false,
leading: AppBarBackButton(
onPressed: () async {
await showDialog<void>(
context: context,
builder: (_) => const FrostInterruptionDialog(
type: FrostInterruptionDialogType.resharing,
popUntilOnYesRouteName: DesktopHomeView.routeName,
),
);
},
),
trailing: ExitToMyStackButton(
onPressed: () async {
await showDialog<void>(
context: context,
builder: (_) => const FrostInterruptionDialog(
type: FrostInterruptionDialogType.resharing,
popUntilOnYesRouteName: DesktopHomeView.routeName,
),
);
},
),
),
body: SizedBox(
width: 480,
child: child,
),
),
child: ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () async {
await showDialog<void>(
context: context,
builder: (_) => const FrostInterruptionDialog(
type: FrostInterruptionDialogType.resharing,
popUntilOnYesRouteName: HomeView.routeName,
),
);
},
),
title: Text(
"Encryption keys",
style: STextStyles.navBarTitle(context),
),
),
body: SafeArea(
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.all(16),
child: child,
),
),
),
);
},
),
),
),
),
child: Column(
children: [
SizedBox(
height: 220,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
QrImageView(
data: ref
.watch(pFrostResharingData)
.startResharedData!
.resharedStart,
size: 220,
backgroundColor: Theme.of(context)
.extension<StackColors>()!
.background,
foregroundColor: Theme.of(context)
.extension<StackColors>()!
.accentColorDark,
),
],
),
),
const _Div(),
DetailItem(
title: "My encryption key",
detail: ref
.watch(pFrostResharingData)
.startResharedData!
.resharedStart,
button: Util.isDesktop
? IconCopyButton(
data: ref
.watch(pFrostResharingData)
.startResharedData!
.resharedStart,
)
: SimpleCopyButton(
data: ref
.watch(pFrostResharingData)
.startResharedData!
.resharedStart,
),
),
if (!Util.isDesktop) const Spacer(),
const _Div(),
PrimaryButton(
label: "Continue",
onPressed: () {
Navigator.of(context).pushNamed(
FinishResharingView.routeName,
arguments: widget.walletId,
);
},
),
],
),
),
),
);
}
}
class _Div extends StatelessWidget {
const _Div({super.key});
@override
Widget build(BuildContext context) {
return const SizedBox(
height: 12,
);
}
}

View file

@ -0,0 +1,426 @@
import 'package:barcode_scan2/barcode_scan2.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_start_resharing_view.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/show_loading.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/isar/models/wallet_info.dart';
import 'package:stackwallet/wallets/models/incomplete_frost_wallet.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/conditional_parent.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/desktop/primary_button.dart';
import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart';
import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart';
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:stackwallet/widgets/textfield_icon_button.dart';
class NewImportResharerConfigView extends ConsumerStatefulWidget {
const NewImportResharerConfigView({
super.key,
required this.walletName,
required this.coin,
});
static const String routeName = "/newImportResharerConfigView";
final String walletName;
final Coin coin;
@override
ConsumerState<NewImportResharerConfigView> createState() =>
_NewImportResharerConfigViewState();
}
class _NewImportResharerConfigViewState
extends ConsumerState<NewImportResharerConfigView> {
late final TextEditingController myNameFieldController, configFieldController;
late final FocusNode myNameFocusNode, configFocusNode;
bool _nameEmpty = true, _configEmpty = true;
bool _buttonLock = false;
Future<IncompleteFrostWallet> _createWallet() async {
final info = WalletInfo.createNew(
name: widget.walletName,
coin: widget.coin,
);
final wallet = IncompleteFrostWallet();
wallet.info = info;
return wallet;
}
@override
void initState() {
myNameFieldController = TextEditingController();
configFieldController = TextEditingController();
myNameFocusNode = FocusNode();
configFocusNode = FocusNode();
super.initState();
}
@override
void dispose() {
myNameFieldController.dispose();
configFieldController.dispose();
myNameFocusNode.dispose();
configFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ConditionalParent(
condition: Util.isDesktop,
builder: (child) => DesktopScaffold(
background: Theme.of(context).extension<StackColors>()!.background,
appBar: const DesktopAppBar(
isCompactHeight: false,
leading: AppBarBackButton(),
trailing: ExitToMyStackButton(),
),
body: SizedBox(
width: 480,
child: child,
),
),
child: ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () {
Navigator.of(context).pop();
},
),
title: Text(
"Import FROST reshare config",
style: STextStyles.navBarTitle(context),
),
),
body: SafeArea(
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.all(16),
child: child,
),
),
),
);
},
),
),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 16,
),
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
key: const Key("frMyNameTextFieldKey"),
controller: myNameFieldController,
onChanged: (_) {
setState(() {
_nameEmpty = myNameFieldController.text.isEmpty;
});
},
focusNode: myNameFocusNode,
readOnly: false,
autocorrect: false,
enableSuggestions: false,
style: STextStyles.field(context),
decoration: standardInputDecoration(
"My name",
myNameFocusNode,
context,
).copyWith(
contentPadding: const EdgeInsets.only(
left: 16,
top: 6,
bottom: 8,
right: 5,
),
suffixIcon: Padding(
padding: _nameEmpty
? const EdgeInsets.only(right: 8)
: const EdgeInsets.only(right: 0),
child: UnconstrainedBox(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
!_nameEmpty
? TextFieldIconButton(
semanticsLabel:
"Clear Button. Clears The Config Field.",
key: const Key("frMyNameClearButtonKey"),
onTap: () {
myNameFieldController.text = "";
setState(() {
_nameEmpty = true;
});
},
child: const XIcon(),
)
: TextFieldIconButton(
semanticsLabel:
"Paste Button. Pastes From Clipboard To Name Field.",
key: const Key("frMyNamePasteButtonKey"),
onTap: () async {
final ClipboardData? data =
await Clipboard.getData(
Clipboard.kTextPlain);
if (data?.text != null &&
data!.text!.isNotEmpty) {
myNameFieldController.text =
data.text!.trim();
}
setState(() {
_nameEmpty =
myNameFieldController.text.isEmpty;
});
},
child: _nameEmpty
? const ClipboardIcon()
: const XIcon(),
),
],
),
),
),
),
),
),
const SizedBox(
height: 16,
),
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
key: const Key("frConfigTextFieldKey"),
controller: configFieldController,
onChanged: (_) {
setState(() {
_configEmpty = configFieldController.text.isEmpty;
});
},
focusNode: configFocusNode,
readOnly: false,
autocorrect: false,
enableSuggestions: false,
style: STextStyles.field(context),
decoration: standardInputDecoration(
"Enter config",
configFocusNode,
context,
).copyWith(
contentPadding: const EdgeInsets.only(
left: 16,
top: 6,
bottom: 8,
right: 5,
),
suffixIcon: Padding(
padding: _configEmpty
? const EdgeInsets.only(right: 8)
: const EdgeInsets.only(right: 0),
child: UnconstrainedBox(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
!_configEmpty
? TextFieldIconButton(
semanticsLabel:
"Clear Button. Clears The Config Field.",
key: const Key("frConfigClearButtonKey"),
onTap: () {
configFieldController.text = "";
setState(() {
_configEmpty = true;
});
},
child: const XIcon(),
)
: TextFieldIconButton(
semanticsLabel:
"Paste Button. Pastes From Clipboard To Config Field Input.",
key: const Key("frConfigPasteButtonKey"),
onTap: () async {
final ClipboardData? data =
await Clipboard.getData(
Clipboard.kTextPlain);
if (data?.text != null &&
data!.text!.isNotEmpty) {
configFieldController.text =
data.text!.trim();
}
setState(() {
_configEmpty =
configFieldController.text.isEmpty;
});
},
child: _configEmpty
? const ClipboardIcon()
: const XIcon(),
),
if (_configEmpty)
TextFieldIconButton(
semanticsLabel:
"Scan QR Button. Opens Camera For Scanning QR Code.",
key: const Key("frConfigScanQrButtonKey"),
onTap: () async {
try {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(
const Duration(milliseconds: 75));
}
final qrResult = await BarcodeScanner.scan();
configFieldController.text =
qrResult.rawContent;
setState(() {
_configEmpty =
configFieldController.text.isEmpty;
});
} on PlatformException catch (e, s) {
Logging.instance.log(
"Failed to get camera permissions while trying to scan qr code: $e\n$s",
level: LogLevel.Warning,
);
}
},
child: const QrCodeIcon(),
)
],
),
),
),
),
),
),
const SizedBox(
height: 16,
),
if (!Util.isDesktop) const Spacer(),
const SizedBox(
height: 16,
),
PrimaryButton(
label: "Start",
enabled: !_nameEmpty && !_configEmpty,
onPressed: () async {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
}
if (_buttonLock) {
return;
}
_buttonLock = true;
try {
ref.read(pFrostResharingData).reset();
ref.read(pFrostResharingData).myName =
myNameFieldController.text;
ref.read(pFrostResharingData).resharerConfig =
configFieldController.text;
if (!ref
.read(pFrostResharingData)
.configData!
.newParticipants
.contains(ref.read(pFrostResharingData).myName!)) {
ref.read(pFrostResharingData).reset();
return await showDialog<void>(
context: context,
builder: (_) => StackOkDialog(
title: "My name not found in config participants",
desktopPopRootNavigator: Util.isDesktop,
),
);
}
Exception? ex;
final wallet = await showLoading(
whileFuture: _createWallet(),
context: context,
message: "Setting up wallet",
isDesktop: Util.isDesktop,
onException: (e) => ex = e,
);
if (ex != null) {
throw ex!;
}
if (mounted) {
ref.read(pFrostResharingData).incompleteWallet = wallet!;
await Navigator.of(context).pushNamed(
NewStartResharingView.routeName,
arguments: wallet.walletId,
);
}
} catch (e, s) {
Logging.instance.log(
"$e\n$s",
level: LogLevel.Fatal,
);
if (mounted) {
await showDialog<void>(
context: context,
builder: (_) => StackOkDialog(
title: e.toString(),
desktopPopRootNavigator: Util.isDesktop,
),
);
}
} finally {
_buttonLock = false;
}
},
),
],
),
),
);
}
}

View file

@ -0,0 +1,379 @@
import 'package:barcode_scan2/barcode_scan2.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/pages/home_view/home_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_continue_sharing_view.dart';
import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
import 'package:stackwallet/services/frost.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/conditional_parent.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/desktop/primary_button.dart';
import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.dart';
import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart';
import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart';
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:stackwallet/widgets/textfield_icon_button.dart';
class NewStartResharingView extends ConsumerStatefulWidget {
const NewStartResharingView({
super.key,
required this.walletId,
});
static const String routeName = "/newStartResharingView";
final String walletId;
@override
ConsumerState<NewStartResharingView> createState() =>
_NewStartResharingViewState();
}
class _NewStartResharingViewState extends ConsumerState<NewStartResharingView> {
final List<TextEditingController> controllers = [];
final List<FocusNode> focusNodes = [];
late final List<int> resharerIndexes;
final List<bool> fieldIsEmptyFlags = [];
bool _buttonLock = false;
Future<void> _onPressed() async {
if (_buttonLock) {
return;
}
_buttonLock = true;
try {
// collect resharer strings
final resharerStarts = controllers.map((e) => e.text).toList();
final result = Frost.beginReshared(
myName: ref.read(pFrostResharingData).myName!,
resharerConfig: ref.read(pFrostResharingData).resharerConfig!,
resharerStarts: resharerStarts,
);
ref.read(pFrostResharingData).startResharedData = result;
await Navigator.of(context).pushNamed(
NewContinueSharingView.routeName,
arguments: widget.walletId,
);
} catch (e, s) {
Logging.instance.log(
"$e\n$s",
level: LogLevel.Fatal,
);
await showDialog<void>(
context: context,
builder: (_) => StackOkDialog(
title: "Error",
message: e.toString(),
desktopPopRootNavigator: Util.isDesktop,
),
);
} finally {
_buttonLock = false;
}
}
@override
void initState() {
resharerIndexes = ref.read(pFrostResharingData).configData!.resharers;
for (int i = 0; i < resharerIndexes.length; i++) {
controllers.add(TextEditingController());
focusNodes.add(FocusNode());
fieldIsEmptyFlags.add(true);
}
super.initState();
}
@override
void dispose() {
for (int i = 0; i < controllers.length; i++) {
controllers[i].dispose();
}
for (int i = 0; i < focusNodes.length; i++) {
focusNodes[i].dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
await showDialog<void>(
context: context,
builder: (_) => FrostInterruptionDialog(
type: FrostInterruptionDialogType.resharing,
popUntilOnYesRouteName:
Util.isDesktop ? DesktopHomeView.routeName : HomeView.routeName,
),
);
return false;
},
child: ConditionalParent(
condition: Util.isDesktop,
builder: (child) => DesktopScaffold(
background: Theme.of(context).extension<StackColors>()!.background,
appBar: DesktopAppBar(
isCompactHeight: false,
leading: AppBarBackButton(
onPressed: () async {
await showDialog<void>(
context: context,
builder: (_) => const FrostInterruptionDialog(
type: FrostInterruptionDialogType.resharing,
popUntilOnYesRouteName: DesktopHomeView.routeName,
),
);
},
),
trailing: ExitToMyStackButton(
onPressed: () async {
await showDialog<void>(
context: context,
builder: (_) => const FrostInterruptionDialog(
type: FrostInterruptionDialogType.resharing,
popUntilOnYesRouteName: DesktopHomeView.routeName,
),
);
},
),
),
body: SizedBox(
width: 480,
child: child,
),
),
child: ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () async {
await showDialog<void>(
context: context,
builder: (_) => const FrostInterruptionDialog(
type: FrostInterruptionDialogType.resharing,
popUntilOnYesRouteName: HomeView.routeName,
),
);
},
),
title: Text(
"Resharers",
style: STextStyles.navBarTitle(context),
),
),
body: SafeArea(
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.all(16),
child: child,
),
),
),
);
},
),
),
),
),
child: Column(
children: [
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (int i = 0; i < resharerIndexes.length; i++)
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
key: Key("frostResharerTextFieldKey_$i"),
controller: controllers[i],
focusNode: focusNodes[i],
readOnly: false,
autocorrect: false,
enableSuggestions: false,
style: STextStyles.field(context),
onChanged: (_) {
setState(() {
fieldIsEmptyFlags[i] =
controllers[i].text.isEmpty;
});
},
decoration: standardInputDecoration(
"Enter index "
"${resharerIndexes[i]}"
"'s resharer",
focusNodes[i],
context,
).copyWith(
contentPadding: const EdgeInsets.only(
left: 16,
top: 6,
bottom: 8,
right: 5,
),
suffixIcon: Padding(
padding: fieldIsEmptyFlags[i]
? const EdgeInsets.only(right: 8)
: const EdgeInsets.only(right: 0),
child: UnconstrainedBox(
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceAround,
children: [
!fieldIsEmptyFlags[i]
? TextFieldIconButton(
semanticsLabel:
"Clear Button. Clears The Resharer Field Input.",
key: Key(
"frostResharerClearButtonKey_$i"),
onTap: () {
controllers[i].text = "";
setState(() {
fieldIsEmptyFlags[i] = true;
});
},
child: const XIcon(),
)
: TextFieldIconButton(
semanticsLabel:
"Paste Button. Pastes From Clipboard To Resharer Field Input.",
key: Key(
"frostResharerPasteButtonKey_$i"),
onTap: () async {
final ClipboardData? data =
await Clipboard.getData(
Clipboard.kTextPlain);
if (data?.text != null &&
data!.text!.isNotEmpty) {
controllers[i].text =
data.text!.trim();
}
setState(() {
fieldIsEmptyFlags[i] =
controllers[i]
.text
.isEmpty;
});
},
child: fieldIsEmptyFlags[i]
? const ClipboardIcon()
: const XIcon(),
),
if (fieldIsEmptyFlags[i])
TextFieldIconButton(
semanticsLabel: "Scan QR Button. "
"Opens Camera For Scanning QR Code.",
key: Key(
"frostCommitmentsScanQrButtonKey_$i"),
onTap: () async {
try {
if (FocusScope.of(context)
.hasFocus) {
FocusScope.of(context)
.unfocus();
await Future<void>.delayed(
const Duration(
milliseconds: 75));
}
final qrResult =
await BarcodeScanner.scan();
controllers[i].text =
qrResult.rawContent;
setState(() {
fieldIsEmptyFlags[i] =
controllers[i]
.text
.isEmpty;
});
} on PlatformException catch (e, s) {
Logging.instance.log(
"Failed to get camera permissions "
"while trying to scan qr code: $e\n$s",
level: LogLevel.Warning,
);
}
},
child: const QrCodeIcon(),
),
],
),
),
),
),
),
),
),
],
),
],
),
if (!Util.isDesktop) const Spacer(),
const _Div(),
PrimaryButton(
label: "Continue",
enabled: !fieldIsEmptyFlags.reduce((v, e) => v |= e),
onPressed: _onPressed,
),
],
),
),
),
);
}
}
class _Div extends StatelessWidget {
const _Div({super.key});
@override
Widget build(BuildContext context) {
return const SizedBox(
height: 12,
);
}
}

View file

@ -0,0 +1,315 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/pages/home_view/home_view.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
import 'package:stackwallet/pages/wallet_view/wallet_view.dart';
import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart';
import 'package:stackwallet/providers/db/main_db_provider.dart';
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
import 'package:stackwallet/providers/global/node_service_provider.dart';
import 'package:stackwallet/providers/global/prefs_provider.dart';
import 'package:stackwallet/providers/global/secure_store_provider.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/show_loading.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/detail_item.dart';
import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
class VerifyUpdatedWalletView extends ConsumerStatefulWidget {
const VerifyUpdatedWalletView({
super.key,
required this.walletId,
});
static const String routeName = "/verifyUpdatedWalletView";
final String walletId;
@override
ConsumerState<VerifyUpdatedWalletView> createState() =>
_VerifyUpdatedWalletViewState();
}
class _VerifyUpdatedWalletViewState
extends ConsumerState<VerifyUpdatedWalletView> {
late final String config;
late final String serializedKeys;
late final String reshareId;
late final bool isNew;
bool _buttonLock = false;
Future<void> _onPressed() async {
if (_buttonLock) {
return;
}
_buttonLock = true;
try {
Exception? ex;
final BitcoinFrostWallet wallet;
if (isNew) {
wallet = await ref
.read(pFrostResharingData)
.incompleteWallet!
.toBitcoinFrostWallet(
mainDB: ref.read(mainDBProvider),
secureStorageInterface: ref.read(secureStoreProvider),
nodeService: ref.read(nodeServiceChangeNotifierProvider),
prefs: ref.read(prefsChangeNotifierProvider),
);
await wallet.info.setMnemonicVerified(
isar: ref.read(mainDBProvider).isar,
);
ref.read(pWallets).addWallet(wallet);
} else {
wallet =
ref.read(pWallets).getWallet(widget.walletId) as BitcoinFrostWallet;
}
if (mounted) {
await showLoading(
whileFuture: wallet.updateWithResharedData(
serializedKeys: serializedKeys,
multisigConfig: config,
isNewWallet: isNew,
),
context: context,
message: isNew ? "Creating wallet" : "Updating wallet data",
isDesktop: Util.isDesktop,
onException: (e) => ex = e,
);
if (ex != null) {
throw ex!;
}
if (mounted) {
ref.read(pFrostResharingData).reset();
Navigator.of(context).popUntil(
ModalRoute.withName(
_popUntilPath,
),
);
}
}
} catch (e, s) {
Logging.instance.log(
"$e\n$s",
level: LogLevel.Fatal,
);
if (mounted) {
await showDialog<void>(
context: context,
builder: (_) => StackOkDialog(
title: "Error",
message: e.toString(),
desktopPopRootNavigator: Util.isDesktop,
),
);
}
} finally {
_buttonLock = false;
}
}
String get _popUntilPath => isNew
? Util.isDesktop
? DesktopHomeView.routeName
: HomeView.routeName
: Util.isDesktop
? DesktopWalletView.routeName
: WalletView.routeName;
@override
void initState() {
config = ref.read(pFrostResharingData).newWalletData!.multisigConfig;
serializedKeys =
ref.read(pFrostResharingData).newWalletData!.serializedKeys;
reshareId = ref.read(pFrostResharingData).newWalletData!.resharedId;
isNew = ref.read(pFrostResharingData).incompleteWallet != null &&
ref.read(pFrostResharingData).incompleteWallet!.walletId ==
widget.walletId;
super.initState();
}
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
await showDialog<void>(
context: context,
builder: (_) => FrostInterruptionDialog(
type: FrostInterruptionDialogType.resharing,
popUntilOnYesRouteName: _popUntilPath,
),
);
return false;
},
child: ConditionalParent(
condition: Util.isDesktop,
builder: (child) => DesktopScaffold(
background: Theme.of(context).extension<StackColors>()!.background,
appBar: DesktopAppBar(
isCompactHeight: false,
leading: AppBarBackButton(
onPressed: () async {
await showDialog<void>(
context: context,
builder: (_) => FrostInterruptionDialog(
type: FrostInterruptionDialogType.resharing,
popUntilOnYesRouteName: _popUntilPath,
),
);
},
),
trailing: ExitToMyStackButton(
onPressed: () async {
await showDialog<void>(
context: context,
builder: (_) => FrostInterruptionDialog(
type: FrostInterruptionDialogType.resharing,
popUntilOnYesRouteName: _popUntilPath,
),
);
},
),
),
body: SizedBox(
width: 480,
child: child,
),
),
child: ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () async {
await showDialog<void>(
context: context,
builder: (_) => FrostInterruptionDialog(
type: FrostInterruptionDialogType.resharing,
popUntilOnYesRouteName: _popUntilPath,
),
);
},
),
),
body: SafeArea(
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.all(16),
child: child,
),
),
),
);
},
),
),
),
),
child: Column(
children: [
Text(
"Ensure your reshare ID matches that of each other participant",
style: STextStyles.pageTitleH2(context),
),
const _Div(),
DetailItem(
title: "ID",
detail: reshareId,
button: Util.isDesktop
? IconCopyButton(
data: reshareId,
)
: SimpleCopyButton(
data: reshareId,
),
),
const _Div(),
const _Div(),
Text(
"Back up your keys and config",
style: STextStyles.pageTitleH2(context),
),
const _Div(),
DetailItem(
title: "Config",
detail: config,
button: Util.isDesktop
? IconCopyButton(
data: config,
)
: SimpleCopyButton(
data: config,
),
),
const _Div(),
DetailItem(
title: "Keys",
detail: serializedKeys,
button: Util.isDesktop
? IconCopyButton(
data: serializedKeys,
)
: SimpleCopyButton(
data: serializedKeys,
),
),
if (!Util.isDesktop) const Spacer(),
const _Div(),
PrimaryButton(
label: "Confirm",
onPressed: _onPressed,
),
],
),
),
),
);
}
}
class _Div extends StatelessWidget {
const _Div({super.key});
@override
Widget build(BuildContext context) {
return const SizedBox(
height: 12,
);
}
}

View file

@ -17,15 +17,20 @@ import 'package:flutter_svg/svg.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/address_utils.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/clipboard_interface.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart';
import 'package:stackwallet/widgets/detail_item.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
class WalletBackupView extends ConsumerWidget {
@ -33,6 +38,7 @@ class WalletBackupView extends ConsumerWidget {
Key? key,
required this.walletId,
required this.mnemonic,
this.frostWalletData,
this.clipboardInterface = const ClipboardWrapper(),
}) : super(key: key);
@ -40,11 +46,21 @@ class WalletBackupView extends ConsumerWidget {
final String walletId;
final List<String> mnemonic;
final ({
String myName,
String config,
String keys,
({String config, String keys})? prevGen,
})? frostWalletData;
final ClipboardInterface clipboardInterface;
@override
Widget build(BuildContext context, WidgetRef ref) {
debugPrint("BUILD: $runtimeType");
final bool frost = frostWalletData != null;
final prevGen = frostWalletData?.prevGen != null;
return Background(
child: Scaffold(
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
@ -91,139 +107,261 @@ class WalletBackupView extends ConsumerWidget {
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(
height: 4,
),
Text(
ref.watch(pWalletName(walletId)),
textAlign: TextAlign.center,
style: STextStyles.label(context).copyWith(
fontSize: 12,
),
),
const SizedBox(
height: 4,
),
Text(
"Recovery Phrase",
textAlign: TextAlign.center,
style: STextStyles.pageTitleH1(context),
),
const SizedBox(
height: 16,
),
Container(
decoration: BoxDecoration(
color: Theme.of(context).extension<StackColors>()!.popupBG,
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Text(
"Please write down your backup key. Keep it safe and never share it with anyone. Your backup key is the only way you can access your funds if you forget your PIN, lose your phone, etc.\n\nStack Wallet does not keep nor is able to restore your backup key. Only you have access to your wallet.",
style: STextStyles.label(context),
),
),
),
const SizedBox(
height: 8,
),
Expanded(
child: SingleChildScrollView(
child: MnemonicTable(
words: mnemonic,
isDesktop: false,
),
),
),
const SizedBox(
height: 12,
),
TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonStyle(context),
onPressed: () {
String data = AddressUtils.encodeQRSeedData(mnemonic);
showDialog<dynamic>(
context: context,
useSafeArea: false,
barrierDismissible: true,
builder: (_) {
final width = MediaQuery.of(context).size.width / 2;
return StackDialogBase(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Center(
child: Text(
"Recovery phrase QR code",
style: STextStyles.pageTitleH2(context),
),
),
const SizedBox(
height: 12,
),
Center(
child: RepaintBoundary(
// key: _qrKey,
child: SizedBox(
width: width + 20,
height: width + 20,
child: QrImageView(
data: data,
size: width,
backgroundColor: Theme.of(context)
.extension<StackColors>()!
.popupBG,
foregroundColor: Theme.of(context)
.extension<StackColors>()!
.accentColorDark),
child: frost
? LayoutBuilder(
builder: (builderContext, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight - 24,
),
child: IntrinsicHeight(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
RoundedWhiteContainer(
child: Text(
"Please write down your backup data. Keep it safe and "
"never share it with anyone. "
"Your backup data is the only way you can access your "
"funds if you forget your PIN, lose your phone, etc."
"\n\n"
"Stack Wallet does not keep nor is able to restore "
"your backup data. "
"Only you have access to your wallet.",
style: STextStyles.label(context),
),
),
),
const SizedBox(
height: 12,
),
Center(
child: SizedBox(
width: width,
child: TextButton(
onPressed: () async {
// await _capturePng(true);
Navigator.of(context).pop();
},
style: Theme.of(context)
.extension<StackColors>()!
.getSecondaryEnabledButtonStyle(context),
const SizedBox(
height: 24,
),
// DetailItem(
// title: "My name",
// detail: frostWalletData!.myName,
// button: Util.isDesktop
// ? IconCopyButton(
// data: frostWalletData!.myName,
// )
// : SimpleCopyButton(
// data: frostWalletData!.myName,
// ),
// ),
// const SizedBox(
// height: 16,
// ),
DetailItem(
title: "Multisig config",
detail: frostWalletData!.config,
button: Util.isDesktop
? IconCopyButton(
data: frostWalletData!.config,
)
: SimpleCopyButton(
data: frostWalletData!.config,
),
),
const SizedBox(
height: 16,
),
DetailItem(
title: "Keys",
detail: frostWalletData!.keys,
button: Util.isDesktop
? IconCopyButton(
data: frostWalletData!.keys,
)
: SimpleCopyButton(
data: frostWalletData!.keys,
),
),
if (prevGen)
const SizedBox(
height: 24,
),
if (prevGen)
RoundedWhiteContainer(
child: Text(
"Cancel",
style: STextStyles.button(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark),
"Previous generation info",
style: STextStyles.label(context),
),
),
),
),
],
if (prevGen)
const SizedBox(
height: 12,
),
if (prevGen)
DetailItem(
title: "Previous multisig config",
detail: frostWalletData!.prevGen!.config,
button: Util.isDesktop
? IconCopyButton(
data:
frostWalletData!.prevGen!.config,
)
: SimpleCopyButton(
data:
frostWalletData!.prevGen!.config,
),
),
if (prevGen)
const SizedBox(
height: 16,
),
if (prevGen)
DetailItem(
title: "Previous keys",
detail: frostWalletData!.prevGen!.keys,
button: Util.isDesktop
? IconCopyButton(
data: frostWalletData!.prevGen!.keys,
)
: SimpleCopyButton(
data: frostWalletData!.prevGen!.keys,
),
),
],
),
),
);
},
);
},
child: Text(
"Show QR Code",
style: STextStyles.button(context),
),
);
},
)
: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(
height: 4,
),
Text(
ref.watch(pWalletName(walletId)),
textAlign: TextAlign.center,
style: STextStyles.label(context).copyWith(
fontSize: 12,
),
),
const SizedBox(
height: 4,
),
Text(
"Recovery Phrase",
textAlign: TextAlign.center,
style: STextStyles.pageTitleH1(context),
),
const SizedBox(
height: 16,
),
Container(
decoration: BoxDecoration(
color:
Theme.of(context).extension<StackColors>()!.popupBG,
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Text(
"Please write down your backup key. Keep it safe and never share it with anyone. Your backup key is the only way you can access your funds if you forget your PIN, lose your phone, etc.\n\nStack Wallet does not keep nor is able to restore your backup key. Only you have access to your wallet.",
style: STextStyles.label(context),
),
),
),
const SizedBox(
height: 8,
),
Expanded(
child: SingleChildScrollView(
child: MnemonicTable(
words: mnemonic,
isDesktop: false,
),
),
),
const SizedBox(
height: 12,
),
TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonStyle(context),
onPressed: () {
String data = AddressUtils.encodeQRSeedData(mnemonic);
showDialog<dynamic>(
context: context,
useSafeArea: false,
barrierDismissible: true,
builder: (_) {
final width = MediaQuery.of(context).size.width / 2;
return StackDialogBase(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Center(
child: Text(
"Recovery phrase QR code",
style: STextStyles.pageTitleH2(context),
),
),
const SizedBox(
height: 12,
),
Center(
child: RepaintBoundary(
// key: _qrKey,
child: SizedBox(
width: width + 20,
height: width + 20,
child: QrImageView(
data: data,
size: width,
backgroundColor: Theme.of(context)
.extension<StackColors>()!
.popupBG,
foregroundColor: Theme.of(context)
.extension<StackColors>()!
.accentColorDark),
),
),
),
const SizedBox(
height: 12,
),
Center(
child: SizedBox(
width: width,
child: TextButton(
onPressed: () async {
// await _capturePng(true);
Navigator.of(context).pop();
},
style: Theme.of(context)
.extension<StackColors>()!
.getSecondaryEnabledButtonStyle(
context),
child: Text(
"Cancel",
style: STextStyles.button(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark),
),
),
),
),
],
),
);
},
);
},
child: Text(
"Show QR Code",
style: STextStyles.button(context),
),
),
],
),
),
],
),
),
),
);

View file

@ -22,6 +22,7 @@ import 'package:stackwallet/pages/pinpad_views/lock_screen_view.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/advanced_views/debug_view.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_preferences_view.dart';
import 'package:stackwallet/pages/settings_views/sub_widgets/settings_list_button.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/change_representative_view.dart';
@ -39,6 +40,7 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/show_loading.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart';
import 'package:stackwallet/wallets/wallet/impl/epiccash_wallet.dart';
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/mnemonic_interface.dart';
import 'package:stackwallet/widgets/background.dart';
@ -193,6 +195,21 @@ class _WalletSettingsViewState extends ConsumerState<WalletSettingsView> {
padding: const EdgeInsets.all(4),
child: Column(
children: [
if (coin == Coin.bitcoinFrost ||
coin == Coin.bitcoinFrostTestNet)
if (coin == Coin.bitcoinFrost ||
coin == Coin.bitcoinFrostTestNet)
SettingsListButton(
iconAssetName: Assets.svg.addressBook2,
iconSize: 16,
title: "FROST Multisig settings",
onPressed: () {
Navigator.of(context).pushNamed(
FrostMSWalletOptionsView.routeName,
arguments: walletId,
);
},
),
SettingsListButton(
iconAssetName: Assets.svg.addressBook,
iconSize: 16,
@ -235,39 +252,83 @@ class _WalletSettingsViewState extends ConsumerState<WalletSettingsView> {
final wallet = ref
.read(pWallets)
.getWallet(widget.walletId);
// TODO: [prio=frost] take wallets that don't have a mnemonic into account
if (wallet is MnemonicInterface) {
final mnemonic =
await wallet.getMnemonicAsWords();
if (mounted) {
await Navigator.push(
context,
RouteGenerator.getRoute(
shouldUseMaterialRoute:
RouteGenerator
.useMaterialPageRoute,
builder: (_) => LockscreenView(
routeOnSuccessArguments:
Tuple2(
walletId, mnemonic),
showBackButton: true,
routeOnSuccess:
WalletBackupView
.routeName,
biometricsCancelButtonString:
"CANCEL",
biometricsLocalizedReason:
"Authenticate to view recovery phrase",
biometricsAuthenticationTitle:
"View recovery phrase",
),
settings: const RouteSettings(
name:
"/viewRecoverPhraseLockscreen"),
),
// TODO: [prio=med] take wallets that don't have a mnemonic into account
List<String>? mnemonic;
({
String myName,
String config,
String keys,
({
String config,
String keys
})? prevGen,
})? frostWalletData;
if (wallet is BitcoinFrostWallet) {
List<Future<dynamic>> futures = [];
futures.addAll(
[
wallet.getSerializedKeys(),
wallet.getMultisigConfig(),
wallet.getSerializedKeysPrevGen(),
wallet.getMultisigConfigPrevGen(),
],
);
final results =
await Future.wait(futures);
if (results.length == 5) {
frostWalletData = (
myName: wallet.frostInfo.myName,
config: results[1],
keys: results[0],
prevGen: results[2] == null ||
results[3] == null
? null
: (
config: results[3],
keys: results[2],
),
);
}
} else if (wallet
is MnemonicInterface) {
mnemonic =
await wallet.getMnemonicAsWords();
}
if (mounted) {
await Navigator.push(
context,
RouteGenerator.getRoute(
shouldUseMaterialRoute:
RouteGenerator
.useMaterialPageRoute,
builder: (_) => LockscreenView(
routeOnSuccessArguments: (
walletId: walletId,
mnemonic: mnemonic ?? [],
frostWalletData:
frostWalletData,
),
showBackButton: true,
routeOnSuccess:
WalletBackupView.routeName,
biometricsCancelButtonString:
"CANCEL",
biometricsLocalizedReason:
"Authenticate to view recovery phrase",
biometricsAuthenticationTitle:
"View recovery phrase",
),
settings: const RouteSettings(
name:
"/viewRecoverPhraseLockscreen"),
),
);
}
},
);

View file

@ -23,7 +23,6 @@ import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
import 'package:tuple/tuple.dart';
enum FiroRescanRecoveryErrorViewOption {
retry,
@ -269,8 +268,10 @@ class _FiroRescanRecoveryErrorViewState
shouldUseMaterialRoute:
RouteGenerator.useMaterialPageRoute,
builder: (_) => LockscreenView(
routeOnSuccessArguments:
Tuple2(widget.walletId, mnemonic),
routeOnSuccessArguments: (
walletId: widget.walletId,
mnemonic: mnemonic,
),
showBackButton: true,
routeOnSuccess: WalletBackupView.routeName,
biometricsCancelButtonString: "CANCEL",

View file

@ -23,6 +23,7 @@ import 'package:stackwallet/providers/db/main_db_provider.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/loading_indicator.dart';
@ -44,6 +45,7 @@ class _TransactionsV2ListState extends ConsumerState<TransactionsV2List> {
late final StreamSubscription<List<TransactionV2>> _subscription;
late final Query<TransactionV2> _query;
late final Coin coin;
BorderRadius get _borderRadiusFirst {
return BorderRadius.only(
@ -69,6 +71,7 @@ class _TransactionsV2ListState extends ConsumerState<TransactionsV2List> {
@override
void initState() {
coin = ref.read(pWallets).getWallet(widget.walletId).info.coin;
_query = ref
.read(mainDBProvider)
.isar
@ -110,8 +113,6 @@ class _TransactionsV2ListState extends ConsumerState<TransactionsV2List> {
@override
Widget build(BuildContext context) {
final coin = ref.watch(pWallets).getWallet(widget.walletId).info.coin;
return FutureBuilder(
future: _query.findAll(),
builder: (fbContext, AsyncSnapshot<List<TransactionV2>> snapshot) {

View file

@ -29,6 +29,7 @@ import 'package:stackwallet/pages/ordinals/ordinals_view.dart';
import 'package:stackwallet/pages/paynym/paynym_claim_view.dart';
import 'package:stackwallet/pages/paynym/paynym_home_view.dart';
import 'package:stackwallet/pages/receive_view/receive_view.dart';
import 'package:stackwallet/pages/send_view/frost_ms/frost_send_view.dart';
import 'package:stackwallet/pages/send_view/send_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_view.dart';
@ -64,6 +65,7 @@ import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/show_loading.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart';
import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart';
import 'package:stackwallet/wallets/wallet/impl/firo_wallet.dart';
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/cash_fusion_interface.dart';
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/coin_control_interface.dart';
@ -994,10 +996,13 @@ class _WalletViewState extends ConsumerState<WalletView> {
// break;
// }
Navigator.of(context).pushNamed(
SendView.routeName,
arguments: Tuple2(
walletId,
coin,
ref.read(pWallets).getWallet(walletId)
is BitcoinFrostWallet
? FrostSendView.routeName
: SendView.routeName,
arguments: (
walletId: walletId,
coin: coin,
),
);
},

View file

@ -52,6 +52,7 @@ import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart';
import 'package:stackwallet/wallets/models/tx_data.dart';
import 'package:stackwallet/wallets/wallet/impl/firo_wallet.dart';
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/coin_control_interface.dart';
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart';
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart';
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart';
import 'package:stackwallet/widgets/animated_text.dart';
@ -1566,7 +1567,8 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
if (!([Coin.nano, Coin.banano, Coin.epicCash, Coin.tezos]
.contains(coin)))
ConditionalParent(
condition: coin.isElectrumXCoin &&
condition: ref.watch(pWallets).getWallet(walletId)
is ElectrumXInterface &&
!(((coin == Coin.firo || coin == Coin.firoTestNet) &&
(ref.watch(publicPrivateBalanceStateProvider.state).state ==
FiroType.lelantus ||

View file

@ -10,13 +10,17 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/pages/send_view/frost_ms/frost_import_sign_config_view.dart';
import 'package:stackwallet/pages/send_view/frost_ms/frost_send_view.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/tx_v2/transaction_v2_list.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart';
import 'package:stackwallet/widgets/custom_tab_view.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
class MyWallet extends ConsumerStatefulWidget {
@ -40,11 +44,15 @@ class _MyWalletState extends ConsumerState<MyWallet> {
];
late final bool isEth;
late final Coin coin;
late final bool isFrost;
@override
void initState() {
isEth = ref.read(pWallets).getWallet(widget.walletId).info.coin ==
Coin.ethereum;
final wallet = ref.read(pWallets).getWallet(widget.walletId);
coin = wallet.info.coin;
isFrost = wallet is BitcoinFrostWallet;
isEth = coin == Coin.ethereum;
if (isEth && widget.contractAddress == null) {
titles.add("Transactions");
@ -64,12 +72,41 @@ class _MyWalletState extends ConsumerState<MyWallet> {
titles: titles,
children: [
widget.contractAddress == null
? Padding(
padding: const EdgeInsets.all(20),
child: DesktopSend(
walletId: widget.walletId,
),
)
? isFrost
? Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding:
const EdgeInsets.fromLTRB(0, 20, 0, 0),
child: SecondaryButton(
width: 200,
buttonHeight: ButtonHeight.l,
label: "Import sign config",
onPressed: () {
Navigator.of(context).pushNamed(
FrostImportSignConfigView.routeName,
arguments: widget.walletId,
);
},
),
)
],
),
FrostSendView(
walletId: widget.walletId,
coin: coin,
),
],
)
: Padding(
padding: const EdgeInsets.all(20),
child: DesktopSend(
walletId: widget.walletId,
),
)
: Padding(
padding: const EdgeInsets.all(20),
child: DesktopTokenSend(

View file

@ -21,6 +21,7 @@ import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart';
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/mnemonic_interface.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart';
@ -80,19 +81,33 @@ class _UnlockWalletKeysDesktopState
Navigator.of(context, rootNavigator: true).pop();
final wallet = ref.read(pWallets).getWallet(widget.walletId);
({String keys, String config})? frostData;
List<String>? words;
// TODO: [prio=med] handle wallets that don't have a mnemonic
// TODO: [prio=low] handle wallets that don't have a mnemonic
// All wallets currently are mnemonic based
if (wallet is! MnemonicInterface) {
throw Exception("FIXME ~= see todo in code");
if (wallet is BitcoinFrostWallet) {
frostData = (
keys: (await wallet.getSerializedKeys())!,
config: (await wallet.getMultisigConfig())!,
);
print(1111111);
print(frostData);
} else {
throw Exception("FIXME ~= see todo in code");
}
} else {
words = await wallet.getMnemonicAsWords();
}
final words = await wallet.getMnemonicAsWords();
if (mounted) {
await Navigator.of(context).pushReplacementNamed(
WalletKeysDesktopPopup.routeName,
arguments: words,
arguments: (
mnemonic: words ?? [],
frostData: frostData,
),
);
}
} else {
@ -301,21 +316,35 @@ class _UnlockWalletKeysDesktopState
if (verified) {
Navigator.of(context, rootNavigator: true).pop();
({String keys, String config})? frostData;
List<String>? words;
final wallet =
ref.read(pWallets).getWallet(widget.walletId);
// TODO: [prio=low] handle wallets that don't have a mnemonic
// All wallets currently are mnemonic based
if (wallet is! MnemonicInterface) {
throw Exception("FIXME ~= see todo in code");
if (wallet is BitcoinFrostWallet) {
frostData = (
keys: (await wallet.getSerializedKeys())!,
config: (await wallet.getMultisigConfig())!,
);
} else {
throw Exception("FIXME ~= see todo in code");
}
} else {
words = await wallet.getMnemonicAsWords();
}
final words = await wallet.getMnemonicAsWords();
if (mounted) {
await Navigator.of(context)
.pushReplacementNamed(
WalletKeysDesktopPopup.routeName,
arguments: words,
arguments: (
mnemonic: words ?? [],
frostData: frostData,
),
);
}
} else {

View file

@ -14,6 +14,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/qr_code_desktop_popup_content.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/address_utils.dart';
@ -24,15 +25,18 @@ import 'package:stackwallet/widgets/desktop/desktop_dialog.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
class WalletKeysDesktopPopup extends StatelessWidget {
const WalletKeysDesktopPopup({
Key? key,
required this.words,
this.frostData,
this.clipboardInterface = const ClipboardWrapper(),
}) : super(key: key);
final List<String> words;
final ({String keys, String config})? frostData;
final ClipboardInterface clipboardInterface;
static const String routeName = "walletKeysDesktopPopup";
@ -66,85 +70,185 @@ class WalletKeysDesktopPopup extends StatelessWidget {
const SizedBox(
height: 28,
),
Text(
"Recovery phrase",
style: STextStyles.desktopTextMedium(context),
),
const SizedBox(
height: 8,
),
Center(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 32,
),
child: Text(
"Please write down your recovery phrase in the correct order and save it to keep your funds secure. You will also be asked to verify the words on the next screen.",
style: STextStyles.desktopTextExtraExtraSmall(context),
textAlign: TextAlign.center,
),
),
),
const SizedBox(
height: 24,
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 32,
),
child: MnemonicTable(
words: words,
isDesktop: true,
itemBorderColor: Theme.of(context)
.extension<StackColors>()!
.buttonBackSecondary,
),
),
const SizedBox(
height: 24,
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 32,
),
child: Row(
children: [
Expanded(
child: SecondaryButton(
label: "Show QR code",
onPressed: () {
final String value = AddressUtils.encodeQRSeedData(words);
Navigator.of(context).pushNamed(
QRCodeDesktopPopupContent.routeName,
arguments: value,
);
},
),
),
const SizedBox(
width: 16,
),
Expanded(
child: PrimaryButton(
label: "Copy",
onPressed: () async {
await clipboardInterface.setData(
ClipboardData(text: words.join(" ")),
);
unawaited(
showFloatingFlushBar(
type: FlushBarType.info,
message: "Copied to clipboard",
iconAsset: Assets.svg.copy,
context: context,
frostData != null
? Column(
children: [
Text(
"Keys",
style: STextStyles.desktopTextMedium(context),
),
const SizedBox(
height: 8,
),
Center(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 32,
),
);
},
),
child: RoundedWhiteContainer(
borderColor: Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultBG,
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 9),
child: Row(
children: [
Flexible(
child: SelectableText(
frostData!.keys,
style: STextStyles.desktopTextExtraExtraSmall(
context),
textAlign: TextAlign.center,
),
),
const SizedBox(
width: 10,
),
IconCopyButton(
data: frostData!.keys,
)
// TODO [prio=low: Add QR code button and dialog.
],
),
),
),
),
const SizedBox(
height: 24,
),
Text(
"Config",
style: STextStyles.desktopTextMedium(context),
),
const SizedBox(
height: 8,
),
Center(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 32,
),
child: RoundedWhiteContainer(
borderColor: Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultBG,
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 9),
child: Row(
children: [
Flexible(
child: SelectableText(
frostData!.config,
style: STextStyles.desktopTextExtraExtraSmall(
context),
textAlign: TextAlign.center,
),
),
const SizedBox(
width: 10,
),
IconCopyButton(
data: frostData!.config,
)
// TODO [prio=low: Add QR code button and dialog.
],
),
),
),
),
const SizedBox(
height: 24,
),
],
)
: Column(
children: [
Text(
"Recovery phrase",
style: STextStyles.desktopTextMedium(context),
),
const SizedBox(
height: 8,
),
Center(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 32,
),
child: Text(
"Please write down your recovery phrase in the correct order and save it to keep your funds secure. You will also be asked to verify the words on the next screen.",
style:
STextStyles.desktopTextExtraExtraSmall(context),
textAlign: TextAlign.center,
),
),
),
const SizedBox(
height: 24,
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 32,
),
child: MnemonicTable(
words: words,
isDesktop: true,
itemBorderColor: Theme.of(context)
.extension<StackColors>()!
.buttonBackSecondary,
),
),
const SizedBox(
height: 24,
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 32,
),
child: Row(
children: [
Expanded(
child: SecondaryButton(
label: "Show QR code",
onPressed: () {
// TODO: address utils
final String value =
AddressUtils.encodeQRSeedData(words);
Navigator.of(context).pushNamed(
QRCodeDesktopPopupContent.routeName,
arguments: value,
);
},
),
),
const SizedBox(
width: 16,
),
Expanded(
child: PrimaryButton(
label: "Copy",
onPressed: () async {
await clipboardInterface.setData(
ClipboardData(text: words.join(" ")),
);
if (context.mounted) {
unawaited(
showFloatingFlushBar(
type: FlushBarType.info,
message: "Copied to clipboard",
iconAsset: Assets.svg.copy,
context: context,
),
);
}
},
),
),
],
),
),
],
),
],
),
),
const SizedBox(
height: 32,
),

View file

@ -14,6 +14,7 @@ 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/frost_ms/frost_ms_options_view.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';
@ -34,7 +35,8 @@ enum _WalletOptions {
changeRepresentative,
showXpub,
lelantusCoins,
sparkCoins;
sparkCoins,
frostOptions;
String get prettyName {
switch (this) {
@ -50,6 +52,8 @@ enum _WalletOptions {
return "Lelantus Coins";
case _WalletOptions.sparkCoins:
return "Spark Coins";
case _WalletOptions.frostOptions:
return "FROST settings";
}
}
}
@ -96,6 +100,9 @@ class WalletOptionsButton extends StatelessWidget {
onFiroShowSparkCoins: () async {
Navigator.of(context).pop(_WalletOptions.sparkCoins);
},
onFrostMSWalletOptionsPressed: () async {
Navigator.of(context).pop(_WalletOptions.frostOptions);
},
walletId: walletId,
);
},
@ -207,6 +214,15 @@ class WalletOptionsButton extends StatelessWidget {
),
);
break;
case _WalletOptions.frostOptions:
unawaited(
Navigator.of(context).pushNamed(
FrostMSWalletOptionsView.routeName,
arguments: walletId,
),
);
break;
}
}
},
@ -241,6 +257,7 @@ class WalletOptionsPopupMenu extends ConsumerWidget {
required this.onChangeRepPressed,
required this.onFiroShowLelantusCoins,
required this.onFiroShowSparkCoins,
required this.onFrostMSWalletOptionsPressed,
required this.walletId,
}) : super(key: key);
@ -250,6 +267,7 @@ class WalletOptionsPopupMenu extends ConsumerWidget {
final VoidCallback onChangeRepPressed;
final VoidCallback onFiroShowLelantusCoins;
final VoidCallback onFiroShowSparkCoins;
final VoidCallback onFrostMSWalletOptionsPressed;
final String walletId;
@override
@ -265,6 +283,9 @@ class WalletOptionsPopupMenu extends ConsumerWidget {
final bool canChangeRep = coin == Coin.nano || coin == Coin.banano;
final bool isFrost =
coin == Coin.bitcoinFrost || coin == Coin.bitcoinFrostTestNet;
return Stack(
children: [
Positioned(
@ -429,6 +450,43 @@ class WalletOptionsPopupMenu extends ConsumerWidget {
),
),
),
if (isFrost)
const SizedBox(
height: 8,
),
if (isFrost)
TransparentButton(
onPressed: onFrostMSWalletOptionsPressed,
child: Padding(
padding: const EdgeInsets.all(8),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
SvgPicture.asset(
Assets.svg.addressBookDesktop,
width: 20,
height: 20,
color: Theme.of(context)
.extension<StackColors>()!
.textFieldActiveSearchIconLeft,
),
const SizedBox(width: 14),
Expanded(
child: Text(
_WalletOptions.frostOptions.prettyName,
style: STextStyles.desktopTextExtraExtraSmall(
context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
),
),
),
],
),
),
),
if (xpubEnabled)
const SizedBox(
height: 8,

View file

@ -0,0 +1,103 @@
import 'dart:ffi';
import 'dart:typed_data';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:frostdart/frostdart_bindings_generated.dart';
import 'package:stackwallet/services/frost.dart';
import 'package:stackwallet/wallets/models/incomplete_frost_wallet.dart';
import 'package:stackwallet/wallets/models/tx_data.dart';
// =================== wallet creation =========================================
final pFrostMultisigConfig = StateProvider<String?>((ref) => null);
final pFrostMyName = StateProvider<String?>((ref) => null);
final pFrostStartKeyGenData = StateProvider<
({
String seed,
String commitments,
Pointer<MultisigConfigWithName> multisigConfigWithNamePtr,
Pointer<SecretShareMachineWrapper> secretShareMachineWrapperPtr,
})?>((_) => null);
final pFrostSecretSharesData = StateProvider<
({
String share,
Pointer<SecretSharesRes> secretSharesResPtr,
})?>((ref) => null);
final pFrostCompletedKeyGenData = StateProvider<
({
Uint8List multisigId,
String recoveryString,
String serializedKeys,
})?>((ref) => null);
// ================= transaction creation ======================================
final pFrostTxData = StateProvider<TxData?>((ref) => null);
final pFrostAttemptSignData = StateProvider<
({
Pointer<TransactionSignMachineWrapper> machinePtr,
String preprocess,
})?>((ref) => null);
final pFrostContinueSignData = StateProvider<
({
Pointer<TransactionSignatureMachineWrapper> machinePtr,
String share,
})?>((ref) => null);
// ===================== shared/util ===========================================
final pFrostSelectParticipantsUnordered =
StateProvider<List<String>?>((ref) => null);
// ========================= resharing =========================================
final pFrostResharingData = Provider((ref) => _ResharingData());
class _ResharingData {
String? myName;
IncompleteFrostWallet? incompleteWallet;
// resharer encoded config string
String? resharerConfig;
({
int newThreshold,
List<int> resharers,
List<String> newParticipants,
})? get configData => resharerConfig != null
? Frost.extractResharerConfigData(resharerConfig: resharerConfig!)
: null;
// resharer start string (for sharing) and machine
({
String resharerStart,
Pointer<StartResharerRes> machine,
})? startResharerData;
// reshared start string (for sharing) and machine
({
String resharedStart,
Pointer<StartResharedRes> prior,
})? startResharedData;
// resharer complete string (for sharing)
String? resharerComplete;
// new keys and config with an ID
({
String multisigConfig,
String serializedKeys,
String resharedId,
})? newWalletData;
// reset/clear all data
void reset() {
resharerConfig = null;
startResharerData = null;
startResharedData = null;
resharerComplete = null;
newWalletData = null;
incompleteWallet = null;
}
}

View file

@ -26,6 +26,13 @@ import 'package:stackwallet/pages/add_wallet_views/add_token_view/add_custom_tok
import 'package:stackwallet/pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dart';
import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart';
import 'package:stackwallet/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart';
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/confirm_new_frost_ms_wallet_creation_view.dart';
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart';
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/frost_share_commitments_view.dart';
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/frost_share_shares_view.dart';
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/import_new_frost_ms_wallet_view.dart';
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/share_new_multisig_config_view.dart';
import 'package:stackwallet/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart';
import 'package:stackwallet/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart';
import 'package:stackwallet/pages/add_wallet_views/new_wallet_options/new_wallet_options_view.dart';
import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_view/new_wallet_recovery_phrase_view.dart';
@ -76,6 +83,10 @@ import 'package:stackwallet/pages/receive_view/addresses/wallet_addresses_view.d
import 'package:stackwallet/pages/receive_view/generate_receiving_uri_qr_code_view.dart';
import 'package:stackwallet/pages/receive_view/receive_view.dart';
import 'package:stackwallet/pages/send_view/confirm_transaction_view.dart';
import 'package:stackwallet/pages/send_view/frost_ms/frost_attempt_sign_config_view.dart';
import 'package:stackwallet/pages/send_view/frost_ms/frost_create_sign_config_view.dart';
import 'package:stackwallet/pages/send_view/frost_ms/frost_import_sign_config_view.dart';
import 'package:stackwallet/pages/send_view/frost_ms/frost_send_view.dart';
import 'package:stackwallet/pages/send_view/send_view.dart';
import 'package:stackwallet/pages/send_view/token_send_view.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/about_view.dart';
@ -113,6 +124,19 @@ import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_pr
import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_preferences_view.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/wallet_syncing_options_view.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/tor_settings/tor_settings_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/frost_participants_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/finish_resharing_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/begin_reshare_config_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/complete_reshare_config_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/display_reshare_config_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1b/import_reshare_config_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/begin_resharing_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/continue_resharing_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_continue_sharing_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_import_resharer_config_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_start_resharing_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/verify_updated_wallet_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_view.dart';
@ -423,6 +447,379 @@ class RouteGenerator {
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case CreateNewFrostMsWalletView.routeName:
if (args is ({
String walletName,
Coin coin,
})) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => CreateNewFrostMsWalletView(
walletName: args.walletName,
coin: args.coin,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case RestoreFrostMsWalletView.routeName:
if (args is ({
String walletName,
Coin coin,
})) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => RestoreFrostMsWalletView(
walletName: args.walletName,
coin: args.coin,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case ShareNewMultisigConfigView.routeName:
if (args is ({
String walletName,
Coin coin,
})) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => ShareNewMultisigConfigView(
walletName: args.walletName,
coin: args.coin,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case ImportNewFrostMsWalletView.routeName:
if (args is ({
String walletName,
Coin coin,
})) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => ImportNewFrostMsWalletView(
walletName: args.walletName,
coin: args.coin,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case NewImportResharerConfigView.routeName:
if (args is ({
String walletName,
Coin coin,
})) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => NewImportResharerConfigView(
walletName: args.walletName,
coin: args.coin,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case NewStartResharingView.routeName:
if (args is String) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => NewStartResharingView(
walletId: args,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case NewContinueSharingView.routeName:
if (args is String) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => NewContinueSharingView(
walletId: args,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case FrostShareCommitmentsView.routeName:
if (args is ({
String walletName,
Coin coin,
})) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => FrostShareCommitmentsView(
walletName: args.walletName,
coin: args.coin,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case FrostShareSharesView.routeName:
if (args is ({
String walletName,
Coin coin,
})) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => FrostShareSharesView(
walletName: args.walletName,
coin: args.coin,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case ConfirmNewFrostMSWalletCreationView.routeName:
if (args is ({
String walletName,
Coin coin,
})) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => ConfirmNewFrostMSWalletCreationView(
walletName: args.walletName,
coin: args.coin,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case FrostMSWalletOptionsView.routeName:
if (args is String) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => FrostMSWalletOptionsView(
walletId: args,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case FrostParticipantsView.routeName:
if (args is String) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => FrostParticipantsView(
walletId: args,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case ImportReshareConfigView.routeName:
if (args is String) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => ImportReshareConfigView(
walletId: args,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case BeginReshareConfigView.routeName:
if (args is String) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => BeginReshareConfigView(
walletId: args,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case CompleteReshareConfigView.routeName:
if (args is ({String walletId, List<int> resharers})) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => CompleteReshareConfigView(
walletId: args.walletId,
resharers: args.resharers,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case DisplayReshareConfigView.routeName:
if (args is String) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => DisplayReshareConfigView(
walletId: args,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case BeginResharingView.routeName:
if (args is String) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => BeginResharingView(
walletId: args,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case ContinueResharingView.routeName:
if (args is String) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => ContinueResharingView(
walletId: args,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case FinishResharingView.routeName:
if (args is String) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => FinishResharingView(
walletId: args,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case VerifyUpdatedWalletView.routeName:
if (args is String) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => VerifyUpdatedWalletView(
walletId: args,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case FrostSendView.routeName:
if (args is ({
String walletId,
Coin coin,
})) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => FrostSendView(
walletId: args.walletId,
coin: args.coin,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case FrostImportSignConfigView.routeName:
if (args is String) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => FrostImportSignConfigView(
walletId: args,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case FrostCreateSignConfigView.routeName:
if (args is String) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => FrostCreateSignConfigView(
walletId: args,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case FrostAttemptSignConfigView.routeName:
if (args is String) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => FrostAttemptSignConfigView(
walletId: args,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
// case MonkeyLoadedView.routeName:
// if (args is Tuple2<String, ChangeNotifierProvider<Manager>>) {
// return getRoute(
@ -1051,12 +1448,33 @@ class RouteGenerator {
return _routeError("${settings.name} invalid args: ${args.toString()}");
case WalletBackupView.routeName:
if (args is Tuple2<String, List<String>>) {
if (args is ({String walletId, List<String> mnemonic})) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => WalletBackupView(
walletId: args.item1,
mnemonic: args.item2,
walletId: args.walletId,
mnemonic: args.mnemonic,
),
settings: RouteSettings(
name: settings.name,
),
);
} else if (args is ({
String walletId,
List<String> mnemonic,
({
String myName,
String config,
String keys,
({String config, String keys})? prevGen,
})? frostWalletData,
})) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => WalletBackupView(
walletId: args.walletId,
mnemonic: args.mnemonic,
frostWalletData: args.frostWalletData,
),
settings: RouteSettings(
name: settings.name,
@ -1961,10 +2379,14 @@ class RouteGenerator {
settings: RouteSettings(name: settings.name));
case WalletKeysDesktopPopup.routeName:
if (args is List<String>) {
if (args is ({
List<String> mnemonic,
({String keys, String config})? frostData
})) {
return FadePageRoute(
WalletKeysDesktopPopup(
words: args,
words: args.mnemonic,
frostData: args.frostData,
),
RouteSettings(
name: settings.name,

613
lib/services/frost.dart Normal file
View file

@ -0,0 +1,613 @@
import 'dart:ffi';
import 'dart:typed_data';
import 'package:frostdart/frostdart.dart';
import 'package:frostdart/frostdart_bindings_generated.dart';
import 'package:frostdart/output.dart';
import 'package:frostdart/util.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart';
import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/utilities/extensions/extensions.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart';
abstract class Frost {
//==================== utility ===============================================
static List<String> getParticipants({
required String multisigConfig,
}) {
try {
final numberOfParticipants = multisigParticipants(
multisigConfig: multisigConfig,
);
final List<String> participants = [];
for (int i = 0; i < numberOfParticipants; i++) {
participants.add(
multisigParticipant(
multisigConfig: multisigConfig,
index: i,
),
);
}
return participants;
} catch (e, s) {
Logging.instance.log(
"getParticipants failed: $e\n$s",
level: LogLevel.Fatal,
);
rethrow;
}
}
static bool validateEncodedMultisigConfig({required String encodedConfig}) {
try {
decodeMultisigConfig(multisigConfig: encodedConfig);
return true;
} catch (e, s) {
Logging.instance.log(
"validateEncodedMultisigConfig failed: $e\n$s",
level: LogLevel.Fatal,
);
return false;
}
}
static int getThreshold({
required String multisigConfig,
}) {
try {
final threshold = multisigThreshold(
multisigConfig: multisigConfig,
);
return threshold;
} catch (e, s) {
Logging.instance.log(
"getThreshold failed: $e\n$s",
level: LogLevel.Fatal,
);
rethrow;
}
}
static ({
List<({String address, Amount amount})> recipients,
String changeAddress,
int feePerWeight,
List<Output> inputs,
}) extractDataFromSignConfig({
required String signConfig,
required CryptoCurrency coin,
}) {
try {
final network = coin.network == CryptoCurrencyNetwork.test
? Network.Testnet
: Network.Mainnet;
final signConfigPointer = decodedSignConfig(
encodedConfig: signConfig,
network: network,
);
// get various data from config
final feePerWeight =
signFeePerWeight(signConfigPointer: signConfigPointer);
final changeAddress = signChange(signConfigPointer: signConfigPointer);
final recipientsCount = signPayments(
signConfigPointer: signConfigPointer,
);
// get tx recipient info
final List<({String address, Amount amount})> recipients = [];
for (int i = 0; i < recipientsCount; i++) {
final String address = signPaymentAddress(
signConfigPointer: signConfigPointer,
index: i,
);
final int amount = signPaymentAmount(
signConfigPointer: signConfigPointer,
index: i,
);
recipients.add(
(
address: address,
amount: Amount(
rawValue: BigInt.from(amount),
fractionDigits: coin.fractionDigits,
),
),
);
}
// get utxos
final count = signInputs(signConfigPointer: signConfigPointer);
final List<Output> outputs = [];
for (int i = 0; i < count; i++) {
final output = signInput(
signConfig: signConfig,
index: i,
network: network,
);
outputs.add(output);
}
return (
recipients: recipients,
changeAddress: changeAddress,
feePerWeight: feePerWeight,
inputs: outputs,
);
} catch (e, s) {
Logging.instance.log(
"extractDataFromSignConfig failed: $e\n$s",
level: LogLevel.Fatal,
);
rethrow;
}
}
//==================== wallet creation =======================================
static String createMultisigConfig({
required String name,
required int threshold,
required List<String> participants,
}) {
try {
final config = newMultisigConfig(
name: name,
threshold: threshold,
participants: participants,
);
return config;
} catch (e, s) {
Logging.instance.log(
"createMultisigConfig failed: $e\n$s",
level: LogLevel.Fatal,
);
rethrow;
}
}
static ({
String seed,
String commitments,
Pointer<MultisigConfigWithName> multisigConfigWithNamePtr,
Pointer<SecretShareMachineWrapper> secretShareMachineWrapperPtr,
}) startKeyGeneration({
required String multisigConfig,
required String myName,
}) {
try {
final startKeyGenResPtr = startKeyGen(
multisigConfig: multisigConfig,
myName: myName,
language: Language.english,
);
final seed = startKeyGenResPtr.ref.seed.toDartString();
final commitments = startKeyGenResPtr.ref.commitments.toDartString();
final configWithNamePtr = startKeyGenResPtr.ref.config;
final machinePtr = startKeyGenResPtr.ref.machine;
return (
seed: seed,
commitments: commitments,
multisigConfigWithNamePtr: configWithNamePtr,
secretShareMachineWrapperPtr: machinePtr,
);
} catch (e, s) {
Logging.instance.log(
"startKeyGeneration failed: $e\n$s",
level: LogLevel.Fatal,
);
rethrow;
}
}
static ({
String share,
Pointer<SecretSharesRes> secretSharesResPtr,
}) generateSecretShares({
required Pointer<MultisigConfigWithName> multisigConfigWithNamePtr,
required String mySeed,
required Pointer<SecretShareMachineWrapper> secretShareMachineWrapperPtr,
required List<String> commitments,
}) {
try {
final secretSharesResPtr = getSecretShares(
multisigConfigWithName: multisigConfigWithNamePtr,
seed: mySeed,
language: Language.english,
machine: secretShareMachineWrapperPtr,
commitments: commitments,
);
final share = secretSharesResPtr.ref.shares.toDartString();
return (share: share, secretSharesResPtr: secretSharesResPtr);
} catch (e, s) {
Logging.instance.log(
"generateSecretShares failed: $e\n$s",
level: LogLevel.Fatal,
);
rethrow;
}
}
static ({
Uint8List multisigId,
String recoveryString,
String serializedKeys,
}) completeKeyGeneration({
required Pointer<MultisigConfigWithName> multisigConfigWithNamePtr,
required Pointer<SecretSharesRes> secretSharesResPtr,
required List<String> shares,
}) {
try {
final keyGenResPtr = completeKeyGen(
multisigConfigWithName: multisigConfigWithNamePtr,
machineAndCommitments: secretSharesResPtr,
shares: shares,
);
final id = Uint8List.fromList(
List<int>.generate(
MULTISIG_ID_LENGTH,
(index) => keyGenResPtr.ref.multisig_id[index],
),
);
final recoveryString = keyGenResPtr.ref.recovery.toDartString();
final serializedKeys = serializeKeys(keys: keyGenResPtr.ref.keys);
return (
multisigId: id,
recoveryString: recoveryString,
serializedKeys: serializedKeys,
);
} catch (e, s) {
Logging.instance.log(
"completeKeyGeneration failed: $e\n$s",
level: LogLevel.Fatal,
);
rethrow;
}
}
//=================== transaction creation ===================================
static String createSignConfig({
required int network,
required List<({UTXO utxo, Uint8List scriptPubKey})> inputs,
required List<({String address, Amount amount, bool isChange})> outputs,
required String changeAddress,
required int feePerWeight,
}) {
try {
final signConfig = newSignConfig(
network: network,
outputs: inputs
.map(
(e) => Output(
hash: e.utxo.txid.toUint8ListFromHex,
vout: e.utxo.vout,
value: e.utxo.value,
scriptPubKey: e.scriptPubKey,
),
)
.toList(),
paymentAddresses: outputs.map((e) => e.address).toList(),
paymentAmounts: outputs.map((e) => e.amount.raw.toInt()).toList(),
change: changeAddress,
feePerWeight: feePerWeight,
);
return signConfig;
} catch (e, s) {
Logging.instance.log(
"createSignConfig failed: $e\n$s",
level: LogLevel.Fatal,
);
rethrow;
}
}
static ({
Pointer<TransactionSignMachineWrapper> machinePtr,
String preprocess,
}) attemptSignConfig({
required int network,
required String config,
required String serializedKeys,
}) {
try {
final keys = deserializeKeys(keys: serializedKeys);
final attemptSignRes = attemptSign(
thresholdKeysWrapperPointer: keys,
network: network,
signConfig: config,
);
return (
preprocess: attemptSignRes.ref.preprocess.toDartString(),
machinePtr: attemptSignRes.ref.machine,
);
} catch (e, s) {
Logging.instance.log(
"attemptSignConfig failed: $e\n$s",
level: LogLevel.Fatal,
);
rethrow;
}
}
static ({
Pointer<TransactionSignatureMachineWrapper> machinePtr,
String share,
}) continueSigning({
required Pointer<TransactionSignMachineWrapper> machinePtr,
required List<String> preprocesses,
}) {
try {
final continueSignRes = continueSign(
machine: machinePtr,
preprocesses: preprocesses,
);
return (
share: continueSignRes.ref.preprocess.toDartString(),
machinePtr: continueSignRes.ref.machine,
);
} catch (e, s) {
Logging.instance.log(
"continueSigning failed: $e\n$s",
level: LogLevel.Fatal,
);
rethrow;
}
}
static String completeSigning({
required Pointer<TransactionSignatureMachineWrapper> machinePtr,
required List<String> shares,
}) {
try {
final rawTransaction = completeSign(
machine: machinePtr,
shares: shares,
);
return rawTransaction;
} catch (e, s) {
Logging.instance.log(
"completeSigning failed: $e\n$s",
level: LogLevel.Fatal,
);
rethrow;
}
}
static Pointer<SignConfig> decodedSignConfig({
required String encodedConfig,
required int network,
}) {
try {
final configPtr =
decodeSignConfig(encodedSignConfig: encodedConfig, network: network);
return configPtr;
} catch (e, s) {
Logging.instance.log(
"decodedSignConfig failed: $e\n$s",
level: LogLevel.Fatal,
);
rethrow;
}
}
//========================== resharing =======================================
static String createResharerConfig({
required int newThreshold,
required List<int> resharers,
required List<String> newParticipants,
}) {
try {
final config = newResharerConfig(
newThreshold: newThreshold,
newParticipants: newParticipants,
resharers: resharers,
);
return config;
} catch (e, s) {
Logging.instance.log(
"createResharerConfig failed: $e\n$s",
level: LogLevel.Fatal,
);
rethrow;
}
}
static ({
String resharerStart,
Pointer<StartResharerRes> machine,
}) beginResharer({
required String serializedKeys,
required String config,
}) {
try {
final result = startResharer(
serializedKeys: serializedKeys,
config: config,
);
return (
resharerStart: result.encoded,
machine: result.machine,
);
} catch (e, s) {
Logging.instance.log(
"beginResharer failed: $e\n$s",
level: LogLevel.Fatal,
);
rethrow;
}
}
/// expects [resharerStarts] of length equal to resharers.
static ({
String resharedStart,
Pointer<StartResharedRes> prior,
}) beginReshared({
required String myName,
required String resharerConfig,
required List<String> resharerStarts,
}) {
try {
final result = startReshared(
newMultisigName: 'unused_property',
myName: myName,
resharerConfig: resharerConfig,
resharerStarts: resharerStarts,
);
return (
resharedStart: result.encoded,
prior: result.machine,
);
} catch (e, s) {
Logging.instance.log(
"beginReshared failed: $e\n$s",
level: LogLevel.Fatal,
);
rethrow;
}
}
/// expects [encryptionKeysOfResharedTo] of length equal to new participants
static String finishResharer({
required StartResharerRes machine,
required List<String> encryptionKeysOfResharedTo,
}) {
try {
final result = completeResharer(
machine: machine,
encryptionKeysOfResharedTo: encryptionKeysOfResharedTo,
);
return result;
} catch (e, s) {
Logging.instance.log(
"finishResharer failed: $e\n$s",
level: LogLevel.Fatal,
);
rethrow;
}
}
/// expects [resharerCompletes] of length equal to resharers
static ({
String multisigConfig,
String serializedKeys,
String resharedId,
}) finishReshared({
required StartResharedRes prior,
required List<String> resharerCompletes,
}) {
try {
final result = completeReshared(
prior: prior,
resharerCompletes: resharerCompletes,
);
return result;
} catch (e, s) {
Logging.instance.log(
"finishReshared failed: $e\n$s",
level: LogLevel.Fatal,
);
rethrow;
}
}
static Pointer<ResharerConfig> decodedResharerConfig({
required String resharerConfig,
}) {
try {
final config = decodeResharerConfig(resharerConfig: resharerConfig);
return config;
} catch (e, s) {
Logging.instance.log(
"decodedResharerConfig failed: $e\n$s",
level: LogLevel.Fatal,
);
rethrow;
}
}
static ({
int newThreshold,
List<int> resharers,
List<String> newParticipants,
}) extractResharerConfigData({
required String resharerConfig,
}) {
try {
final newThreshold = resharerNewThreshold(
resharerConfigPointer: decodedResharerConfig(
resharerConfig: resharerConfig,
),
);
final resharersCount = resharerResharers(
resharerConfigPointer: decodedResharerConfig(
resharerConfig: resharerConfig,
),
);
final List<int> resharers = [];
for (int i = 0; i < resharersCount; i++) {
resharers.add(
resharerResharer(
resharerConfigPointer: decodedResharerConfig(
resharerConfig: resharerConfig,
),
index: i,
),
);
}
final newParticipantsCount = resharerNewParticipants(
resharerConfigPointer: decodedResharerConfig(
resharerConfig: resharerConfig,
),
);
final List<String> newParticipants = [];
for (int i = 0; i < newParticipantsCount; i++) {
newParticipants.add(
resharerNewParticipant(
resharerConfigPointer: decodedResharerConfig(
resharerConfig: resharerConfig,
),
index: i,
),
);
}
return (
newThreshold: newThreshold,
resharers: resharers,
newParticipants: newParticipants,
);
} catch (e, s) {
Logging.instance.log(
"extractResharerConfigData failed: $e\n$s",
level: LogLevel.Fatal,
);
rethrow;
}
}
}

View file

@ -24,6 +24,7 @@ import 'package:stackwallet/services/wallets.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/prefs.dart';
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart';
import 'exchange/exchange.dart';
@ -123,7 +124,7 @@ class NotificationsService extends ChangeNotifier {
final node = nodeService.getPrimaryNodeFor(coin: coin);
if (node != null) {
if (coin.isElectrumXCoin) {
if (wallet is ElectrumXInterface) {
final eNode = ElectrumXNode(
address: node.host,
port: node.port,

View file

@ -16,6 +16,13 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart';
final coinCardProvider = Provider.family<String?, Coin>((ref, coin) {
final assets = ref.watch(themeAssetsProvider);
// TODO: handle this differently by adding proper frost assets to themes
if (coin == Coin.bitcoinFrost) {
coin = Coin.bitcoin;
} else if (coin == Coin.bitcoinFrostTestNet) {
coin = Coin.bitcoinTestNet;
}
if (assets is ThemeAssetsV3) {
return assets.coinCardImages?[coin.mainNetVersion];
} else {
@ -26,6 +33,13 @@ final coinCardProvider = Provider.family<String?, Coin>((ref, coin) {
final coinCardFavoritesProvider = Provider.family<String?, Coin>((ref, coin) {
final assets = ref.watch(themeAssetsProvider);
// TODO: handle this differently by adding proper frost assets to themes
if (coin == Coin.bitcoinFrost) {
coin = Coin.bitcoin;
} else if (coin == Coin.bitcoinFrostTestNet) {
coin = Coin.bitcoinTestNet;
}
if (assets is ThemeAssetsV3) {
return assets.coinCardFavoritesImages?[coin.mainNetVersion] ??
assets.coinCardImages?[coin.mainNetVersion];

View file

@ -16,6 +16,13 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart';
final coinIconProvider = Provider.family<String, Coin>((ref, coin) {
final assets = ref.watch(themeAssetsProvider);
// TODO: handle this differently by adding proper frost assets to themes
if (coin == Coin.bitcoinFrost) {
coin = Coin.bitcoin;
} else if (coin == Coin.bitcoinFrostTestNet) {
coin = Coin.bitcoinTestNet;
}
if (assets is ThemeAssets) {
switch (coin) {
case Coin.bitcoin:

View file

@ -16,6 +16,13 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart';
final coinImageProvider = Provider.family<String, Coin>((ref, coin) {
final assets = ref.watch(themeAssetsProvider);
// TODO: handle this differently by adding proper frost assets to themes
if (coin == Coin.bitcoinFrost) {
coin = Coin.bitcoin;
} else if (coin == Coin.bitcoinFrostTestNet) {
coin = Coin.bitcoinTestNet;
}
if (assets is ThemeAssets) {
switch (coin) {
case Coin.bitcoin:
@ -64,6 +71,13 @@ final coinImageProvider = Provider.family<String, Coin>((ref, coin) {
final coinImageSecondaryProvider = Provider.family<String, Coin>((ref, coin) {
final assets = ref.watch(themeAssetsProvider);
// TODO: handle this differently by adding proper frost assets to themes
if (coin == Coin.bitcoinFrost) {
coin = Coin.bitcoin;
} else if (coin == Coin.bitcoinFrostTestNet) {
coin = Coin.bitcoinTestNet;
}
if (assets is ThemeAssets) {
switch (coin) {
case Coin.bitcoin:

View file

@ -37,6 +37,8 @@ class CoinThemeColorDefault {
switch (coin) {
case Coin.bitcoin:
case Coin.bitcoinTestNet:
case Coin.bitcoinFrost:
case Coin.bitcoinFrostTestNet:
return bitcoin;
case Coin.litecoin:
case Coin.litecoinTestNet:

View file

@ -1680,6 +1680,8 @@ class StackColors extends ThemeExtension<StackColors> {
switch (coin) {
case Coin.bitcoin:
case Coin.bitcoinTestNet:
case Coin.bitcoinFrost:
case Coin.bitcoinFrostTestNet:
return _coin.bitcoin;
case Coin.litecoin:
case Coin.litecoinTestNet:

View file

@ -14,10 +14,9 @@ import 'package:bitcoindart/bitcoindart.dart';
import 'package:crypto/crypto.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/wallets/crypto_currency/coins/bitcoin.dart';
import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart';
import 'package:stackwallet/wallets/crypto_currency/coins/banano.dart';
import 'package:stackwallet/wallets/crypto_currency/coins/bitcoin.dart';
import 'package:stackwallet/wallets/crypto_currency/coins/bitcoin_frost.dart';
import 'package:stackwallet/wallets/crypto_currency/coins/bitcoincash.dart';
import 'package:stackwallet/wallets/crypto_currency/coins/dogecoin.dart';
import 'package:stackwallet/wallets/crypto_currency/coins/ecash.dart';
@ -32,6 +31,7 @@ import 'package:stackwallet/wallets/crypto_currency/coins/particl.dart';
import 'package:stackwallet/wallets/crypto_currency/coins/stellar.dart';
import 'package:stackwallet/wallets/crypto_currency/coins/tezos.dart';
import 'package:stackwallet/wallets/crypto_currency/coins/wownero.dart';
import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart';
class AddressUtils {
static String condenseAddress(String address) {
@ -72,6 +72,9 @@ class AddressUtils {
switch (coin) {
case Coin.bitcoin:
return Bitcoin(CryptoCurrencyNetwork.main).validateAddress(address);
case Coin.bitcoinFrost:
return BitcoinFrost(CryptoCurrencyNetwork.main)
.validateAddress(address);
case Coin.litecoin:
return Litecoin(CryptoCurrencyNetwork.main).validateAddress(address);
case Coin.bitcoincash:
@ -104,6 +107,9 @@ class AddressUtils {
return Tezos(CryptoCurrencyNetwork.main).validateAddress(address);
case Coin.bitcoinTestNet:
return Bitcoin(CryptoCurrencyNetwork.test).validateAddress(address);
case Coin.bitcoinFrostTestNet:
return BitcoinFrost(CryptoCurrencyNetwork.test)
.validateAddress(address);
case Coin.litecoinTestNet:
return Litecoin(CryptoCurrencyNetwork.test).validateAddress(address);
case Coin.bitcoincashTestnet:

View file

@ -40,6 +40,8 @@ enum AmountUnit {
case Coin.litecoin:
case Coin.particl:
case Coin.namecoin:
case Coin.bitcoinFrost:
case Coin.bitcoinFrostTestNet:
case Coin.bitcoinTestNet:
case Coin.litecoinTestNet:
case Coin.bitcoincashTestnet:

View file

@ -18,6 +18,7 @@ Uri getDefaultBlockExplorerUrlFor({
required String txid,
}) {
switch (coin) {
case Coin.bitcoinFrost:
case Coin.bitcoin:
return Uri.parse("https://mempool.space/tx/$txid");
case Coin.litecoin:
@ -25,6 +26,7 @@ Uri getDefaultBlockExplorerUrlFor({
case Coin.litecoinTestNet:
return Uri.parse("https://chain.so/tx/LTCTEST/$txid");
case Coin.bitcoinTestNet:
case Coin.bitcoinFrostTestNet:
return Uri.parse("https://mempool.space/testnet/tx/$txid");
case Coin.dogecoin:
return Uri.parse("https://chain.so/tx/DOGE/$txid");

View file

@ -69,6 +69,7 @@ abstract class Constants {
static BigInt satsPerCoin(Coin coin) {
switch (coin) {
case Coin.bitcoin:
case Coin.bitcoinFrost:
case Coin.litecoin:
case Coin.litecoinTestNet:
case Coin.bitcoincash:
@ -76,6 +77,7 @@ abstract class Constants {
case Coin.dogecoin:
case Coin.firo:
case Coin.bitcoinTestNet:
case Coin.bitcoinFrostTestNet:
case Coin.dogecoinTestNet:
case Coin.firoTestNet:
case Coin.epicCash:
@ -113,6 +115,7 @@ abstract class Constants {
static int decimalPlacesForCoin(Coin coin) {
switch (coin) {
case Coin.bitcoin:
case Coin.bitcoinFrost:
case Coin.litecoin:
case Coin.litecoinTestNet:
case Coin.bitcoincash:
@ -120,6 +123,7 @@ abstract class Constants {
case Coin.dogecoin:
case Coin.firo:
case Coin.bitcoinTestNet:
case Coin.bitcoinFrostTestNet:
case Coin.dogecoinTestNet:
case Coin.firoTestNet:
case Coin.epicCash:
@ -189,6 +193,10 @@ abstract class Constants {
case Coin.wownero:
values.addAll([14, 25]);
break;
case Coin.bitcoinFrost:
case Coin.bitcoinFrostTestNet:
throw ArgumentError("Frost mnemonic lengths unsupported");
}
return values;
}
@ -198,6 +206,8 @@ abstract class Constants {
switch (coin) {
case Coin.bitcoin:
case Coin.bitcoinTestNet:
case Coin.bitcoinFrost:
case Coin.bitcoinFrostTestNet:
case Coin.bitcoincash:
case Coin.bitcoincashTestnet:
case Coin.eCash:
@ -277,6 +287,10 @@ abstract class Constants {
case Coin.monero:
return 25;
case Coin.bitcoinFrost:
case Coin.bitcoinFrostTestNet:
throw ArgumentError("Frost mnemonic length unsupported");
//
// default:
// -1;

View file

@ -312,6 +312,7 @@ abstract class DefaultNodes {
static NodeModel getNodeFor(Coin coin) {
switch (coin) {
case Coin.bitcoin:
case Coin.bitcoinFrost:
return bitcoin;
case Coin.litecoin:
@ -360,6 +361,7 @@ abstract class DefaultNodes {
return tezos;
case Coin.bitcoinTestNet:
case Coin.bitcoinFrostTestNet:
return bitcoinTestnet;
case Coin.litecoinTestNet:

View file

@ -13,6 +13,7 @@ import 'package:stackwallet/utilities/constants.dart';
enum Coin {
bitcoin,
bitcoinFrost,
monero,
banano,
bitcoincash,
@ -35,6 +36,7 @@ enum Coin {
///
bitcoinTestNet,
bitcoinFrostTestNet,
bitcoincashTestnet,
dogecoinTestNet,
firoTestNet,
@ -47,6 +49,8 @@ extension CoinExt on Coin {
switch (this) {
case Coin.bitcoin:
return "Bitcoin";
case Coin.bitcoinFrost:
return "Bitcoin Frost";
case Coin.litecoin:
return "Litecoin";
case Coin.bitcoincash:
@ -79,6 +83,8 @@ extension CoinExt on Coin {
return "Banano";
case Coin.bitcoinTestNet:
return "tBitcoin";
case Coin.bitcoinFrostTestNet:
return "tBitcoin Frost";
case Coin.litecoinTestNet:
return "tLitecoin";
case Coin.bitcoincashTestnet:
@ -95,6 +101,7 @@ extension CoinExt on Coin {
String get ticker {
switch (this) {
case Coin.bitcoin:
case Coin.bitcoinFrost:
return "BTC";
case Coin.litecoin:
return "LTC";
@ -127,6 +134,7 @@ extension CoinExt on Coin {
case Coin.banano:
return "BAN";
case Coin.bitcoinTestNet:
case Coin.bitcoinFrostTestNet:
return "tBTC";
case Coin.litecoinTestNet:
return "tLTC";
@ -144,6 +152,7 @@ extension CoinExt on Coin {
String get uriScheme {
switch (this) {
case Coin.bitcoin:
case Coin.bitcoinFrost:
return "bitcoin";
case Coin.litecoin:
return "litecoin";
@ -177,6 +186,7 @@ extension CoinExt on Coin {
case Coin.banano:
return "ban";
case Coin.bitcoinTestNet:
case Coin.bitcoinFrostTestNet:
return "bitcoin";
case Coin.litecoinTestNet:
return "litecoin";
@ -191,36 +201,6 @@ extension CoinExt on Coin {
}
}
bool get isElectrumXCoin {
switch (this) {
case Coin.bitcoin:
case Coin.litecoin:
case Coin.bitcoincash:
case Coin.dogecoin:
case Coin.firo:
case Coin.namecoin:
case Coin.particl:
case Coin.bitcoinTestNet:
case Coin.litecoinTestNet:
case Coin.bitcoincashTestnet:
case Coin.firoTestNet:
case Coin.dogecoinTestNet:
case Coin.eCash:
return true;
case Coin.epicCash:
case Coin.ethereum:
case Coin.monero:
case Coin.tezos:
case Coin.wownero:
case Coin.nano:
case Coin.banano:
case Coin.stellar:
case Coin.stellarTestnet:
return false;
}
}
bool get hasMnemonicPassphraseSupport {
switch (this) {
case Coin.bitcoin:
@ -241,6 +221,8 @@ extension CoinExt on Coin {
case Coin.stellarTestnet:
return true;
case Coin.bitcoinFrost:
case Coin.bitcoinFrostTestNet:
case Coin.epicCash:
case Coin.monero:
case Coin.wownero:
@ -260,6 +242,8 @@ extension CoinExt on Coin {
case Coin.ethereum:
return true;
case Coin.bitcoinFrost:
case Coin.bitcoinFrostTestNet:
case Coin.firo:
case Coin.namecoin:
case Coin.particl:
@ -284,6 +268,7 @@ extension CoinExt on Coin {
bool get isTestNet {
switch (this) {
case Coin.bitcoin:
case Coin.bitcoinFrost:
case Coin.litecoin:
case Coin.bitcoincash:
case Coin.dogecoin:
@ -303,6 +288,7 @@ extension CoinExt on Coin {
case Coin.dogecoinTestNet:
case Coin.bitcoinTestNet:
case Coin.bitcoinFrostTestNet:
case Coin.litecoinTestNet:
case Coin.bitcoincashTestnet:
case Coin.firoTestNet:
@ -311,9 +297,21 @@ extension CoinExt on Coin {
}
}
bool get isFrost {
switch (this) {
case Coin.bitcoinFrost:
case Coin.bitcoinFrostTestNet:
return true;
default:
return false;
}
}
Coin get mainNetVersion {
switch (this) {
case Coin.bitcoin:
case Coin.bitcoinFrost:
case Coin.litecoin:
case Coin.bitcoincash:
case Coin.dogecoin:
@ -337,6 +335,9 @@ extension CoinExt on Coin {
case Coin.bitcoinTestNet:
return Coin.bitcoin;
case Coin.bitcoinFrostTestNet:
return Coin.bitcoinFrost;
case Coin.litecoinTestNet:
return Coin.litecoin;
@ -364,6 +365,10 @@ extension CoinExt on Coin {
case Coin.particl:
return AddressType.p2wpkh;
case Coin.bitcoinFrost:
case Coin.bitcoinFrostTestNet:
return AddressType.frostMS;
case Coin.eCash:
case Coin.bitcoincash:
case Coin.bitcoincashTestnet:
@ -501,6 +506,15 @@ Coin coinFromPrettyName(String name) {
case "tStellar":
return Coin.stellarTestnet;
case "Bitcoin Frost":
case "bitcoinFrost":
return Coin.bitcoinFrost;
case "Bitcoin Frost Testnet":
case "tBitcoin Frost":
case "bitcoinFrostTestNet":
return Coin.bitcoinFrostTestNet;
default:
throw ArgumentError.value(
name,

View file

@ -44,6 +44,8 @@ extension DerivePathTypeExt on DerivePathType {
case Coin.ethereum: // TODO: do we need something here?
return DerivePathType.eth;
case Coin.bitcoinFrost:
case Coin.bitcoinFrostTestNet:
case Coin.epicCash:
case Coin.monero:
case Coin.wownero:

View file

@ -170,30 +170,10 @@ class Bitcoin extends Bip39HDCurrency with PaynymCurrencyInterface {
NodeModel get defaultNode {
switch (network) {
case CryptoCurrencyNetwork.main:
return NodeModel(
host: "bitcoin.stackwallet.com",
port: 50002,
name: DefaultNodes.defaultName,
id: DefaultNodes.buildId(Coin.bitcoin),
useSSL: true,
enabled: true,
coinName: Coin.bitcoin.name,
isFailover: true,
isDown: false,
);
return DefaultNodes.bitcoin;
case CryptoCurrencyNetwork.test:
return NodeModel(
host: "bitcoin-testnet.stackwallet.com",
port: 51002,
name: DefaultNodes.defaultName,
id: DefaultNodes.buildId(Coin.bitcoinTestNet),
useSSL: true,
enabled: true,
coinName: Coin.bitcoinTestNet.name,
isFailover: true,
isDown: false,
);
return DefaultNodes.bitcoinTestnet;
default:
throw UnimplementedError();

View file

@ -0,0 +1,72 @@
import 'dart:typed_data';
import 'package:stackwallet/models/node_model.dart';
import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/utilities/default_nodes.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart';
import 'package:stackwallet/wallets/crypto_currency/intermediate/bip39_hd_currency.dart';
import 'package:stackwallet/wallets/crypto_currency/intermediate/private_key_currency.dart';
class BitcoinFrost extends FrostCurrency {
BitcoinFrost(super.network) {
switch (network) {
case CryptoCurrencyNetwork.main:
coin = Coin.bitcoin;
case CryptoCurrencyNetwork.test:
coin = Coin.bitcoinTestNet;
default:
throw Exception("Unsupported network: $network");
}
}
@override
int get minConfirms => 1;
@override
NodeModel get defaultNode {
switch (network) {
case CryptoCurrencyNetwork.main:
return DefaultNodes.bitcoin;
case CryptoCurrencyNetwork.test:
return DefaultNodes.bitcoinTestnet;
default:
throw UnimplementedError();
}
}
@override
String get genesisHash {
switch (network) {
case CryptoCurrencyNetwork.main:
return "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f";
case CryptoCurrencyNetwork.test:
return "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943";
default:
throw Exception("Unsupported network: $network");
}
}
@override
Amount get dustLimit => Amount(
rawValue: BigInt.from(294),
fractionDigits: fractionDigits,
);
@override
String pubKeyToScriptHash({required Uint8List pubKey}) {
try {
return Bip39HDCurrency.convertBytesToScriptHash(pubKey);
} catch (e) {
rethrow;
}
}
@override
bool validateAddress(String address) {
// TODO: implement validateAddress for frost addresses
return true;
}
}

View file

@ -0,0 +1,12 @@
import 'dart:typed_data';
import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart';
abstract class FrostCurrency extends CryptoCurrency {
FrostCurrency(super.network);
String pubKeyToScriptHash({required Uint8List pubKey});
Amount get dustLimit;
}

View file

@ -0,0 +1,41 @@
import 'package:isar/isar.dart';
import 'package:stackwallet/wallets/isar/isar_id_interface.dart';
part 'frost_wallet_info.g.dart';
@Collection(accessor: "frostWalletInfo", inheritance: false)
class FrostWalletInfo implements IsarId {
@override
Id id = Isar.autoIncrement;
@Index(unique: true, replace: false)
final String walletId;
final List<String> knownSalts;
final List<String> participants;
final String myName;
final int threshold;
FrostWalletInfo({
required this.walletId,
required this.knownSalts,
required this.participants,
required this.myName,
required this.threshold,
});
FrostWalletInfo copyWith({
List<String>? knownSalts,
List<String>? participants,
String? myName,
int? threshold,
}) {
return FrostWalletInfo(
walletId: walletId,
knownSalts: knownSalts ?? this.knownSalts,
participants: participants ?? this.participants,
myName: myName ?? this.myName,
threshold: threshold ?? this.threshold,
)..id = id;
}
}

File diff suppressed because it is too large Load diff

View file

@ -265,6 +265,7 @@ const _WalletInfomainAddressTypeEnumValueMap = {
'spark': 10,
'stellar': 11,
'tezos': 12,
'frostMS': 13,
};
const _WalletInfomainAddressTypeValueEnumMap = {
0: AddressType.p2pkh,
@ -280,6 +281,7 @@ const _WalletInfomainAddressTypeValueEnumMap = {
10: AddressType.spark,
11: AddressType.stellar,
12: AddressType.tezos,
13: AddressType.frostMS,
};
Id _walletInfoGetId(WalletInfo object) {

View file

@ -0,0 +1,42 @@
import 'package:stackwallet/db/isar/main_db.dart';
import 'package:stackwallet/services/node_service.dart';
import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart';
import 'package:stackwallet/utilities/prefs.dart';
import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart';
import 'package:stackwallet/wallets/isar/models/wallet_info.dart';
import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart';
import 'package:stackwallet/wallets/wallet/wallet.dart';
class IncompleteFrostWallet {
WalletInfo? info;
String? get walletId => info?.walletId;
Future<BitcoinFrostWallet> toBitcoinFrostWallet({
required MainDB mainDB,
required SecureStorageInterface secureStorageInterface,
required NodeService nodeService,
required Prefs prefs,
}) async {
final wallet = await Wallet.create(
walletInfo: info!,
mainDB: mainDB,
secureStorageInterface: secureStorageInterface,
nodeService: nodeService,
prefs: prefs,
);
// dummy entry so updaters work when `wallet.updateWithResharedData` is called
final frostInfo = FrostWalletInfo(
walletId: info!.walletId,
knownSalts: [],
participants: [],
myName: "",
threshold: -1,
);
await mainDB.isar.frostWalletInfo.put(frostInfo);
return wallet as BitcoinFrostWallet;
}
}

File diff suppressed because it is too large Load diff

View file

@ -26,6 +26,7 @@ import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart';
import 'package:stackwallet/wallets/isar/models/wallet_info.dart';
import 'package:stackwallet/wallets/models/tx_data.dart';
import 'package:stackwallet/wallets/wallet/impl/banano_wallet.dart';
import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart';
import 'package:stackwallet/wallets/wallet/impl/bitcoin_wallet.dart';
import 'package:stackwallet/wallets/wallet/impl/bitcoincash_wallet.dart';
import 'package:stackwallet/wallets/wallet/impl/dogecoin_wallet.dart';
@ -55,6 +56,9 @@ abstract class Wallet<T extends CryptoCurrency> {
// default to Transaction class. For TransactionV2 set to 2
int get isarTransactionVersion => 1;
// whether the wallet currently supports multiple recipients per tx
bool get supportsMultiRecipient => false;
Wallet(this.cryptoCurrency);
//============================================================================
@ -289,7 +293,7 @@ abstract class Wallet<T extends CryptoCurrency> {
wallet.prefs = prefs;
wallet.nodeService = nodeService;
if (wallet is ElectrumXInterface) {
if (wallet is ElectrumXInterface || wallet is BitcoinFrostWallet) {
// initialize electrumx instance
await wallet.updateNode();
}
@ -312,6 +316,11 @@ abstract class Wallet<T extends CryptoCurrency> {
case Coin.bitcoinTestNet:
return BitcoinWallet(CryptoCurrencyNetwork.test);
case Coin.bitcoinFrost:
return BitcoinFrostWallet(CryptoCurrencyNetwork.main);
case Coin.bitcoinFrostTestNet:
return BitcoinFrostWallet(CryptoCurrencyNetwork.test);
case Coin.bitcoincash:
return BitcoincashWallet(CryptoCurrencyNetwork.main);
case Coin.bitcoincashTestnet:

View file

@ -865,7 +865,7 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
}
}
Future<ElectrumXNode> getCurrentElectrumXNode() async {
Future<ElectrumXNode> _getCurrentElectrumXNode() async {
final node = getCurrentNode();
return ElectrumXNode(
@ -877,7 +877,7 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
);
}
Future<void> updateElectrumX({required ElectrumXNode newNode}) async {
Future<void> updateElectrumX() async {
final failovers = nodeService
.failoverNodesFor(coin: cryptoCurrency.coin)
.map((e) => ElectrumXNode(
@ -889,7 +889,7 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
))
.toList();
final newNode = await getCurrentElectrumXNode();
final newNode = await _getCurrentElectrumXNode();
try {
await electrumXClient.electrumAdapterClient?.close();
} catch (e, s) {
@ -1241,8 +1241,7 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
@override
Future<void> updateNode() async {
final node = await getCurrentElectrumXNode();
await updateElectrumX(newNode: node);
await updateElectrumX();
}
Future<ElectrumClient> updateClient() async {

View file

@ -0,0 +1,90 @@
import 'package:flutter/material.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
class DetailItem extends StatelessWidget {
const DetailItem({
Key? key,
required this.title,
required this.detail,
this.button,
this.showEmptyDetail = true,
this.disableSelectableText = false,
}) : super(key: key);
final String title;
final String detail;
final Widget? button;
final bool showEmptyDetail;
final bool disableSelectableText;
@override
Widget build(BuildContext context) {
return ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => RoundedWhiteContainer(
child: child,
),
child: ConditionalParent(
condition: Util.isDesktop,
builder: (child) => Padding(
padding: const EdgeInsets.all(16),
child: child,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
disableSelectableText
? Text(
title,
style: STextStyles.itemSubtitle(context),
)
: SelectableText(
title,
style: STextStyles.itemSubtitle(context),
),
button ?? Container(),
],
),
const SizedBox(
height: 5,
),
detail.isEmpty && showEmptyDetail
? disableSelectableText
? Text(
"$title will appear here",
style: STextStyles.w500_14(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle3,
),
)
: SelectableText(
"$title will appear here",
style: STextStyles.w500_14(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle3,
),
)
: disableSelectableText
? Text(
detail,
style: STextStyles.w500_14(context),
)
: SelectableText(
detail,
style: STextStyles.w500_14(context),
),
],
),
),
);
}
}

View file

@ -0,0 +1,70 @@
import 'package:flutter/material.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
enum FrostInterruptionDialogType {
walletCreation,
resharing,
transactionCreation;
}
class FrostInterruptionDialog extends StatelessWidget {
const FrostInterruptionDialog({
super.key,
required this.type,
required this.popUntilOnYesRouteName,
this.onNoPressedOverride,
this.onYesPressedOverride,
});
final FrostInterruptionDialogType type;
final String popUntilOnYesRouteName;
final VoidCallback? onNoPressedOverride;
final VoidCallback? onYesPressedOverride;
String get message {
switch (type) {
case FrostInterruptionDialogType.walletCreation:
return "wallet creation";
case FrostInterruptionDialogType.resharing:
return "resharing";
case FrostInterruptionDialogType.transactionCreation:
return "transaction signing";
}
}
@override
Widget build(BuildContext context) {
return StackDialog(
title: "Cancel $message process",
message: "Are you sure you want to cancel the $message process?",
leftButton: SecondaryButton(
label: "No",
onPressed: onNoPressedOverride ??
Navigator.of(
context,
rootNavigator: Util.isDesktop,
).pop,
),
rightButton: PrimaryButton(
label: "Yes",
onPressed: onYesPressedOverride ??
() {
// pop dialog
Navigator.of(
context,
rootNavigator: Util.isDesktop,
).pop();
Navigator.of(context).popUntil(
ModalRoute.withName(
popUntilOnYesRouteName,
),
);
},
),
);
}
}

View file

@ -169,6 +169,8 @@ class _NodeCardState extends ConsumerState<NodeCard> {
case Coin.namecoin:
case Coin.bitcoincashTestnet:
case Coin.eCash:
case Coin.bitcoinFrost:
case Coin.bitcoinFrostTestNet:
final client = ElectrumXClient(
host: node.host,
port: node.port,

View file

@ -151,6 +151,8 @@ class NodeOptionsSheet extends ConsumerWidget {
case Coin.namecoin:
case Coin.bitcoincashTestnet:
case Coin.eCash:
case Coin.bitcoinFrost:
case Coin.bitcoinFrostTestNet:
final client = ElectrumXClient(
host: node.host,
port: node.port,

View file

@ -147,8 +147,10 @@ class StackOkDialog extends StatelessWidget {
this.icon,
required this.title,
this.message,
this.desktopPopRootNavigator = false,
}) : super(key: key);
final bool desktopPopRootNavigator;
final Widget? leftButton;
final void Function(String)? onOkPressed;
@ -208,9 +210,13 @@ class StackOkDialog extends StatelessWidget {
onOkPressed?.call("OK");
}
: () {
int count = 0;
Navigator.of(context).popUntil((_) => count++ >= 2);
// onOkPressed?.call("OK");
if (desktopPopRootNavigator) {
Navigator.of(context, rootNavigator: true).pop();
} else {
int count = 0;
Navigator.of(context).popUntil((_) => count++ >= 2);
// onOkPressed?.call("OK");
}
},
style: Theme.of(context)
.extension<StackColors>()!

View file

@ -17,6 +17,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
list(APPEND FLUTTER_FFI_PLUGIN_LIST
coinlib_flutter
flutter_libsparkmobile
frostdart
tor_ffi_plugin
)

View file

@ -825,6 +825,13 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.2.0"
frostdart:
dependency: "direct main"
description:
path: "crypto_plugins/frostdart"
relative: true
source: path
version: "0.0.1"
fuchsia_remote_debug_protocol:
dependency: transitive
description: flutter

View file

@ -27,6 +27,9 @@ dependencies:
lelantus:
path: ./crypto_plugins/flutter_liblelantus
frostdart:
path: ./crypto_plugins/frostdart
flutter_libsparkmobile:
git:
url: https://github.com/cypherstack/flutter_libsparkmobile.git

View file

@ -13,10 +13,8 @@ mkdir -p build
(cd ../../crypto_plugins/flutter_liblelantus/scripts/android && ./build_all.sh ) &
(cd ../../crypto_plugins/flutter_libepiccash/scripts/android && ./install_ndk.sh && ./build_opensll.sh && ./build_all.sh ) &
(cd ../../crypto_plugins/flutter_libmonero/scripts/android/ && ./build_all.sh ) &
set_rust_to_1720 &
(cd ../../crypto_plugins/frostdart/scripts/android && ./build_all.sh ) &
wait
echo "Done building"
# set rust (back) to a more recent stable release to allow stack wallet to build tor
set_rust_to_1720

View file

@ -17,13 +17,12 @@ rustup target add x86_64-apple-ios
(cd ../../crypto_plugins/flutter_liblelantus/scripts/ios && ./build_all.sh ) &
(cd ../../crypto_plugins/flutter_libepiccash/scripts/ios && ./build_all.sh ) &
(cd ../../crypto_plugins/flutter_libmonero/scripts/ios/ && ./build_all.sh ) &
set_rust_to_1720 &
(cd ../../crypto_plugins/frostdart/scripts/ios && ./build_all.sh ) &
wait
echo "Done building"
# set rust (back) to a more recent stable release to allow stack wallet to build tor
set_rust_to_1720
# ensure ios rust triples are there
rustup target add aarch64-apple-ios
rustup target add x86_64-apple-ios

View file

@ -15,10 +15,8 @@ mkdir -p build
(cd ../../crypto_plugins/flutter_liblelantus/scripts/linux && ./build_all.sh ) &
(cd ../../crypto_plugins/flutter_libepiccash/scripts/linux && ./build_all.sh ) &
(cd ../../crypto_plugins/flutter_libmonero/scripts/linux && ./build_monero_all.sh && ./build_sharedfile.sh ) &
set_rust_to_1720 &
(cd ../../crypto_plugins/frostdart/scripts/linux && ./build_all.sh ) &
wait
echo "Done building"
# set rust (back) to a more recent stable release to allow stack wallet to build tor
set_rust_to_1720

View file

@ -9,6 +9,8 @@ set_rust_to_1671
(cd ../../crypto_plugins/flutter_liblelantus/scripts/macos && ./build_all.sh ) &
(cd ../../crypto_plugins/flutter_libepiccash/scripts/macos && ./build_all.sh ) &
(cd ../../crypto_plugins/flutter_libmonero/scripts/macos/ && ./build_all.sh ) &
set_rust_to_1720 &
(cd ../../crypto_plugins/frostdart/scripts/macos && ./build_all.sh ) &
wait
echo "Done building"

View file

@ -10,9 +10,8 @@ mkdir -p build
(cd ../../crypto_plugins/flutter_libepiccash/scripts/windows && ./build_all.sh ) &
(cd ../../crypto_plugins/flutter_liblelantus/scripts/windows && ./build_all.sh ) &
(cd ../../crypto_plugins/flutter_libmonero/scripts/windows && ./build_all.sh) &
set_rust_to_1720 &
(cd ../../crypto_plugins/frostdart/scripts/windows && ./build_all.sh ) &
wait
echo "Done building"
# set rust (back) to a more recent stable release to allow stack wallet to build tor
set_rust_to_1720

View file

@ -18,6 +18,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
list(APPEND FLUTTER_FFI_PLUGIN_LIST
coinlib_flutter
flutter_libsparkmobile
frostdart
tor_ffi_plugin
)