mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2024-12-23 03:49:22 +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_claim_view.dart';
|
||||||
import 'package:stackwallet/pages/paynym/paynym_home_view.dart';
|
import 'package:stackwallet/pages/paynym/paynym_home_view.dart';
|
||||||
import 'package:stackwallet/pages/receive_view/receive_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/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_network_settings_view/wallet_network_settings_view.dart';
|
||||||
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_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/show_loading.dart';
|
||||||
import 'package:stackwallet/utilities/text_styles.dart';
|
import 'package:stackwallet/utilities/text_styles.dart';
|
||||||
import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.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/impl/firo_wallet.dart';
|
||||||
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/cash_fusion_interface.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';
|
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/coin_control_interface.dart';
|
||||||
|
@ -973,10 +975,13 @@ class _WalletViewState extends ConsumerState<WalletView> {
|
||||||
// break;
|
// break;
|
||||||
// }
|
// }
|
||||||
Navigator.of(context).pushNamed(
|
Navigator.of(context).pushNamed(
|
||||||
SendView.routeName,
|
ref.read(pWallets).getWallet(walletId)
|
||||||
arguments: Tuple2(
|
is BitcoinFrostWallet
|
||||||
walletId,
|
? FrostSendView.routeName
|
||||||
coin,
|
: SendView.routeName,
|
||||||
|
arguments: (
|
||||||
|
walletId: walletId,
|
||||||
|
coin: coin,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -10,13 +10,17 @@
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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/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_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_send.dart';
|
||||||
import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_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/providers/global/wallets_provider.dart';
|
||||||
import 'package:stackwallet/utilities/enums/coin_enum.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/custom_tab_view.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
|
||||||
import 'package:stackwallet/widgets/rounded_white_container.dart';
|
import 'package:stackwallet/widgets/rounded_white_container.dart';
|
||||||
|
|
||||||
class MyWallet extends ConsumerStatefulWidget {
|
class MyWallet extends ConsumerStatefulWidget {
|
||||||
|
@ -40,11 +44,15 @@ class _MyWalletState extends ConsumerState<MyWallet> {
|
||||||
];
|
];
|
||||||
|
|
||||||
late final bool isEth;
|
late final bool isEth;
|
||||||
|
late final Coin coin;
|
||||||
|
late final bool isFrost;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
isEth = ref.read(pWallets).getWallet(widget.walletId).info.coin ==
|
final wallet = ref.read(pWallets).getWallet(widget.walletId);
|
||||||
Coin.ethereum;
|
coin = wallet.info.coin;
|
||||||
|
isFrost = wallet is BitcoinFrostWallet;
|
||||||
|
isEth = coin == Coin.ethereum;
|
||||||
|
|
||||||
if (isEth && widget.contractAddress == null) {
|
if (isEth && widget.contractAddress == null) {
|
||||||
titles.add("Transactions");
|
titles.add("Transactions");
|
||||||
|
@ -64,12 +72,37 @@ class _MyWalletState extends ConsumerState<MyWallet> {
|
||||||
titles: titles,
|
titles: titles,
|
||||||
children: [
|
children: [
|
||||||
widget.contractAddress == null
|
widget.contractAddress == null
|
||||||
? Padding(
|
? isFrost
|
||||||
padding: const EdgeInsets.all(20),
|
? Column(
|
||||||
child: DesktopSend(
|
children: [
|
||||||
walletId: widget.walletId,
|
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(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
child: DesktopTokenSend(
|
child: DesktopTokenSend(
|
||||||
|
|
|
@ -756,6 +756,24 @@ class RouteGenerator {
|
||||||
}
|
}
|
||||||
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
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:
|
// case MonkeyLoadedView.routeName:
|
||||||
// if (args is Tuple2<String, ChangeNotifierProvider<Manager>>) {
|
// if (args is Tuple2<String, ChangeNotifierProvider<Manager>>) {
|
||||||
// return getRoute(
|
// return getRoute(
|
||||||
|
|
|
@ -29,6 +29,12 @@ import 'package:stackwallet/wallets/models/tx_data.dart';
|
||||||
import 'package:stackwallet/wallets/wallet/wallet.dart';
|
import 'package:stackwallet/wallets/wallet/wallet.dart';
|
||||||
|
|
||||||
class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
|
class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
|
||||||
|
@override
|
||||||
|
int get isarTransactionVersion => 2;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get supportsMultiRecipient => true;
|
||||||
|
|
||||||
BitcoinFrostWallet(CryptoCurrencyNetwork network)
|
BitcoinFrostWallet(CryptoCurrencyNetwork network)
|
||||||
: super(BitcoinFrost(network) as T);
|
: super(BitcoinFrost(network) as T);
|
||||||
|
|
||||||
|
|
|
@ -55,6 +55,9 @@ abstract class Wallet<T extends CryptoCurrency> {
|
||||||
// default to Transaction class. For TransactionV2 set to 2
|
// default to Transaction class. For TransactionV2 set to 2
|
||||||
int get isarTransactionVersion => 1;
|
int get isarTransactionVersion => 1;
|
||||||
|
|
||||||
|
// whether the wallet currently supports multiple recipients per tx
|
||||||
|
bool get supportsMultiRecipient => false;
|
||||||
|
|
||||||
Wallet(this.cryptoCurrency);
|
Wallet(this.cryptoCurrency);
|
||||||
|
|
||||||
//============================================================================
|
//============================================================================
|
||||||
|
|
Loading…
Reference in a new issue