mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2024-11-17 17:57:40 +00:00
WIP frost send
This commit is contained in:
parent
911837b265
commit
444afb88ae
12 changed files with 2746 additions and 12 deletions
405
lib/pages/send_view/frost_ms/frost_attempt_sign_config_view.dart
Normal file
405
lib/pages/send_view/frost_ms/frost_attempt_sign_config_view.dart
Normal file
|
@ -0,0 +1,405 @@
|
|||
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/coins/bitcoin/frost_wallet.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/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(walletsChangeNotifierProvider)
|
||||
.getManager(widget.walletId)
|
||||
.wallet as FrostWallet;
|
||||
|
||||
myName = wallet.myName;
|
||||
threshold = wallet.threshold;
|
||||
participantsWithoutMe = wallet.participants;
|
||||
myIndex = participantsWithoutMe.indexOf(wallet.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,
|
||||
);
|
||||
}
|
||||
}
|
206
lib/pages/send_view/frost_ms/frost_complete_sign_view.dart
Normal file
206
lib/pages/send_view/frost_ms/frost_complete_sign_view.dart
Normal 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(walletsChangeNotifierProvider)
|
||||
.getManager(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,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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/coins/bitcoin/frost_wallet.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 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(walletsChangeNotifierProvider)
|
||||
.getManager(widget.walletId)
|
||||
.wallet as FrostWallet;
|
||||
|
||||
myName = wallet.myName;
|
||||
participantsAll = wallet.participants;
|
||||
myIndex = wallet.participants.indexOf(wallet.myName);
|
||||
myShare = ref.read(pFrostContinueSignData.state).state!.share;
|
||||
|
||||
participantsWithoutMe = wallet.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,
|
||||
);
|
||||
}
|
||||
}
|
180
lib/pages/send_view/frost_ms/frost_create_sign_config_view.dart
Normal file
180
lib/pages/send_view/frost_ms/frost_create_sign_config_view.dart
Normal file
|
@ -0,0 +1,180 @@
|
|||
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) {
|
||||
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(
|
||||
"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: MediaQuery.of(context).size.width - 32,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
QrImageView(
|
||||
data: ref.watch(pFrostTxData.state).state!.frostMSConfig!,
|
||||
size: MediaQuery.of(context).size.width - 32,
|
||||
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(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 ? 64 : 16,
|
||||
),
|
||||
if (!Util.isDesktop)
|
||||
const Spacer(
|
||||
flex: 2,
|
||||
),
|
||||
PrimaryButton(
|
||||
label: "Attempt sign",
|
||||
onPressed: () {
|
||||
_attemptSign();
|
||||
},
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
330
lib/pages/send_view/frost_ms/frost_import_sign_config_view.dart
Normal file
330
lib/pages/send_view/frost_ms/frost_import_sign_config_view.dart
Normal file
|
@ -0,0 +1,330 @@
|
|||
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,
|
||||
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();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
602
lib/pages/send_view/frost_ms/frost_send_view.dart
Normal file
602
lib/pages/send_view/frost_ms/frost_send_view.dart
Normal file
|
@ -0,0 +1,602 @@
|
|||
/*
|
||||
* 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: 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);
|
501
lib/pages/send_view/frost_ms/recipient.dart
Normal file
501
lib/pages/send_view/frost_ms/recipient.dart
Normal file
|
@ -0,0 +1,501 @@
|
|||
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(
|
||||
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();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
|
@ -63,6 +64,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';
|
||||
|
@ -973,10 +975,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,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
|
|
@ -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,37 @@ 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.end,
|
||||
children: [
|
||||
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(
|
||||
|
|
|
@ -756,6 +756,24 @@ class RouteGenerator {
|
|||
}
|
||||
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 MonkeyLoadedView.routeName:
|
||||
// if (args is Tuple2<String, ChangeNotifierProvider<Manager>>) {
|
||||
// return getRoute(
|
||||
|
|
|
@ -29,6 +29,12 @@ import 'package:stackwallet/wallets/models/tx_data.dart';
|
|||
import 'package:stackwallet/wallets/wallet/wallet.dart';
|
||||
|
||||
class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
|
||||
@override
|
||||
int get isarTransactionVersion => 2;
|
||||
|
||||
@override
|
||||
bool get supportsMultiRecipient => true;
|
||||
|
||||
BitcoinFrostWallet(CryptoCurrencyNetwork network)
|
||||
: super(BitcoinFrost(network) as T);
|
||||
|
||||
|
|
|
@ -55,6 +55,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);
|
||||
|
||||
//============================================================================
|
||||
|
|
Loading…
Reference in a new issue