frost transaction sending ui update

This commit is contained in:
julian 2024-05-02 16:43:02 -06:00
parent 1be51a666b
commit 832be89cab
6 changed files with 377 additions and 362 deletions

View file

@ -82,6 +82,9 @@ class _FrostSendStep1aState extends ConsumerState<FrostSendStep1a> {
) as BitcoinFrostWallet;
_threshold = wallet.frostInfo.threshold;
WidgetsBinding.instance.addPostFrameCallback((_) {
ref.read(pFrostMyName.state).state = wallet.frostInfo.myName;
});
super.initState();
}
@ -116,7 +119,7 @@ class _FrostSendStep1aState extends ConsumerState<FrostSendStep1a> {
TextSpan(
text:
"Share this config with the group members. ",
style: STextStyles.w600_12(context),
style: STextStyles.w500_12(context),
),
TextSpan(
text:
@ -159,7 +162,7 @@ class _FrostSendStep1aState extends ConsumerState<FrostSendStep1a> {
),
),
SizedBox(
height: Util.isDesktop ? 20 : 16,
height: Util.isDesktop ? 20 : 12,
),
SizedBox(
height: qrImageSize,
@ -179,7 +182,7 @@ class _FrostSendStep1aState extends ConsumerState<FrostSendStep1a> {
),
),
SizedBox(
height: Util.isDesktop ? 20 : 16,
height: Util.isDesktop ? 20 : 12,
),
DetailItem(
title: "Encoded transaction config",
@ -193,7 +196,7 @@ class _FrostSendStep1aState extends ConsumerState<FrostSendStep1a> {
),
),
SizedBox(
height: Util.isDesktop ? 20 : 16,
height: Util.isDesktop ? 20 : 12,
),
DetailItem(
title: "Threshold",
@ -201,7 +204,7 @@ class _FrostSendStep1aState extends ConsumerState<FrostSendStep1a> {
horizontal: true,
),
SizedBox(
height: Util.isDesktop ? 20 : 16,
height: Util.isDesktop ? 20 : 12,
),
if (!Util.isDesktop)
const Spacer(
@ -217,7 +220,7 @@ class _FrostSendStep1aState extends ConsumerState<FrostSendStep1a> {
},
),
SizedBox(
height: Util.isDesktop ? 20 : 16,
height: Util.isDesktop ? 20 : 12,
),
PrimaryButton(
label: "Attempt sign",

View file

@ -12,6 +12,7 @@ import 'package:stackwallet/utilities/logger.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/custom_buttons/checkbox_text_button.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/frost_step_user_steps.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
@ -41,7 +42,7 @@ class _FrostSendStep1bState extends ConsumerState<FrostSendStep1b> {
late final TextEditingController configFieldController;
late final FocusNode configFocusNode;
bool _configEmpty = true;
bool _configEmpty = true, _userVerifyContinue = false;
bool _attemptSignLock = false;
@ -125,6 +126,12 @@ class _FrostSendStep1bState extends ConsumerState<FrostSendStep1b> {
void initState() {
configFieldController = TextEditingController();
configFocusNode = FocusNode();
final wallet = ref.read(pWallets).getWallet(
ref.read(pFrostScaffoldArgs)!.walletId!,
) as BitcoinFrostWallet;
WidgetsBinding.instance.addPostFrameCallback((_) {
ref.read(pFrostMyName.state).state = wallet.frostInfo.myName;
});
super.initState();
}
@ -146,7 +153,7 @@ class _FrostSendStep1bState extends ConsumerState<FrostSendStep1b> {
const FrostStepUserSteps(
userSteps: info,
),
const SizedBox(height: 12),
const SizedBox(height: 20),
FrostStepField(
controller: configFieldController,
focusNode: configFocusNode,
@ -159,16 +166,25 @@ class _FrostSendStep1bState extends ConsumerState<FrostSendStep1b> {
});
},
),
const SizedBox(
height: 16,
),
if (!Util.isDesktop) const Spacer(),
const SizedBox(
height: 16,
height: 12,
),
CheckboxTextButton(
label: "I have verified that everyone has imported he config and"
" is ready to sign",
onChanged: (value) {
setState(() {
_userVerifyContinue = value;
});
},
),
const SizedBox(
height: 12,
),
PrimaryButton(
label: "Start signing",
enabled: !_configEmpty,
enabled: !_configEmpty && _userVerifyContinue,
onPressed: () {
_attemptSign();
},

View file

@ -1,6 +1,4 @@
import 'package:barcode_scan2/barcode_scan2.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/frost_route_generator.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
@ -8,7 +6,6 @@ import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/services/frost.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
@ -17,13 +14,9 @@ import 'package:stackwallet/widgets/custom_buttons/frost_qr_dialog_button.dart';
import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.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/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:stackwallet/widgets/textfields/frost_step_field.dart';
class FrostSendStep2 extends ConsumerStatefulWidget {
const FrostSendStep2({super.key});
@ -47,7 +40,7 @@ class _FrostSendStep2State extends ConsumerState<FrostSendStep2> {
final List<bool> fieldIsEmptyFlags = [];
bool hasEnoughPreprocesses() {
int countPreprocesses() {
// own preprocess is not included in controllers and must be set here
int count = 1;
@ -57,7 +50,7 @@ class _FrostSendStep2State extends ConsumerState<FrostSendStep2> {
}
}
return count >= threshold;
return count;
}
@override
@ -124,11 +117,14 @@ class _FrostSendStep2State extends ConsumerState<FrostSendStep2> {
),
],
),
const SizedBox(
height: 4,
),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"1.",
"2.",
style: STextStyles.w500_12(context),
),
const SizedBox(
@ -141,7 +137,7 @@ class _FrostSendStep2State extends ConsumerState<FrostSendStep2> {
TextSpan(
text:
"Enter their preprocesses into the corresponding fields. ",
style: STextStyles.w600_12(context),
style: STextStyles.w500_12(context),
),
TextSpan(
text: "You must have the threshold number of "
@ -164,9 +160,24 @@ class _FrostSendStep2State extends ConsumerState<FrostSendStep2> {
const SizedBox(
height: 12,
),
DetailItem(
title: "Threshold",
detail: "$threshold signatures",
horizontal: true,
),
const SizedBox(
height: 12,
),
DetailItem(
title: "My name",
detail: myName,
button: Util.isDesktop
? IconCopyButton(
data: myName,
)
: SimpleCopyButton(
data: myName,
),
),
const SizedBox(
height: 12,
@ -189,141 +200,46 @@ class _FrostSendStep2State extends ConsumerState<FrostSendStep2> {
const SizedBox(
height: 12,
),
RoundedWhiteContainer(
child: Text(
"You need to obtain ${threshold - 1} preprocess from signing members to send this transaction.",
style: STextStyles.label(context),
),
),
const SizedBox(
height: 12,
),
Builder(builder: (context) {
final count = countPreprocesses();
final colors = Theme.of(context).extension<StackColors>()!;
return DetailItem(
title: "Required preprocesses",
detail: "$count of $threshold",
horizontal: true,
overrideDetailTextColor: count >= threshold
? colors.accentColorGreen
: colors.accentColorRed,
);
}),
const SizedBox(
height: 12,
),
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(),
)
],
),
),
),
),
),
),
),
],
FrostStepField(
label: participantsWithoutMe[i],
hint: "Enter ${participantsWithoutMe[i]}'s preprocess",
controller: controllers[i],
focusNode: focusNodes[i],
onChanged: (_) {
setState(() {
fieldIsEmptyFlags[i] = controllers[i].text.isEmpty;
});
},
showQrScanOption: true,
),
],
),
@ -332,8 +248,8 @@ class _FrostSendStep2State extends ConsumerState<FrostSendStep2> {
height: 12,
),
PrimaryButton(
label: "Continue signing",
enabled: hasEnoughPreprocesses(),
label: "Generate shares",
enabled: countPreprocesses() >= threshold,
onPressed: () async {
// collect Preprocess strings (not including my own)
final preprocesses = controllers.map((e) => e.text).toList();

View file

@ -1,27 +1,23 @@
import 'package:barcode_scan2/barcode_scan2.dart';
import 'package:coinlib_flutter/coinlib_flutter.dart' as cl;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/frost_route_generator.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/services/frost.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/amount/amount.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/custom_buttons/checkbox_text_button.dart';
import 'package:stackwallet/widgets/custom_buttons/frost_qr_dialog_button.dart';
import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.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/frost_step_user_steps.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:stackwallet/widgets/textfields/frost_step_field.dart';
class FrostSendStep3 extends ConsumerStatefulWidget {
const FrostSendStep3({super.key});
@ -34,6 +30,11 @@ class FrostSendStep3 extends ConsumerStatefulWidget {
}
class _FrostSendStep3State extends ConsumerState<FrostSendStep3> {
static const info = [
"Send your share to other signing group members.",
"Enter their shares into the corresponding fields.",
];
final List<TextEditingController> controllers = [];
final List<FocusNode> focusNodes = [];
@ -45,6 +46,8 @@ class _FrostSendStep3State extends ConsumerState<FrostSendStep3> {
final List<bool> fieldIsEmptyFlags = [];
bool _userVerifyContinue = false;
@override
void initState() {
final wallet = ref.read(pWallets).getWallet(
@ -93,15 +96,28 @@ class _FrostSendStep3State extends ConsumerState<FrostSendStep3> {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
DetailItem(
title: "My name",
detail: myName,
const FrostStepUserSteps(
userSteps: info,
),
const SizedBox(
height: 12,
),
DetailItem(
title: "My shares",
title: "My name",
detail: myName,
button: Util.isDesktop
? IconCopyButton(
data: myName,
)
: SimpleCopyButton(
data: myName,
),
),
const SizedBox(
height: 12,
),
DetailItem(
title: "My share",
detail: myShare,
button: Util.isDesktop
? IconCopyButton(
@ -125,133 +141,17 @@ class _FrostSendStep3State extends ConsumerState<FrostSendStep3> {
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(),
)
],
),
),
),
),
),
),
),
],
FrostStepField(
label: participantsWithoutMe[i],
hint: "Enter ${participantsWithoutMe[i]}'s share",
controller: controllers[i],
focusNode: focusNodes[i],
onChanged: (_) {
setState(() {
fieldIsEmptyFlags[i] = controllers[i].text.isEmpty;
});
},
showQrScanOption: true,
),
],
),
@ -259,22 +159,22 @@ class _FrostSendStep3State extends ConsumerState<FrostSendStep3> {
const SizedBox(
height: 12,
),
CheckboxTextButton(
label: "I have verified that everyone has my share",
onChanged: (value) {
setState(() {
_userVerifyContinue = value;
});
},
),
const SizedBox(
height: 12,
),
PrimaryButton(
label: "Complete signing",
label: "Generate transaction",
enabled: _userVerifyContinue &&
!fieldIsEmptyFlags.reduce((v, e) => v |= e),
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();
@ -295,10 +195,32 @@ class _FrostSendStep3State extends ConsumerState<FrostSendStep3> {
shares: shares,
);
ref.read(pFrostTxData.state).state =
ref.read(pFrostTxData.state).state!.copyWith(
raw: rawTx,
);
final tx = cl.Transaction.fromHex(rawTx);
final txData = ref.read(pFrostTxData)!;
final fractionDigits =
txData.recipients!.first.amount.fractionDigits;
final inputTotal = Amount(
rawValue: txData.utxos!
.map((e) => BigInt.from(e.value))
.reduce((v, e) => v += e),
fractionDigits: fractionDigits,
);
final outputTotal = Amount(
rawValue:
tx.outputs.map((e) => e.value).reduce((v, e) => v += e),
fractionDigits: fractionDigits,
);
ref.read(pFrostTxData.state).state = txData.copyWith(
raw: rawTx,
fee: inputTotal - outputTotal,
frostSigners: [
myName,
...participantsWithoutMe,
],
);
ref.read(pFrostCreateCurrentStep.state).state = 4;
await Navigator.of(context).pushNamed(
@ -313,13 +235,15 @@ class _FrostSendStep3State extends ConsumerState<FrostSendStep3> {
level: LogLevel.Fatal,
);
return await showDialog<void>(
context: context,
builder: (_) => StackOkDialog(
title: "Failed to complete signing process",
desktopPopRootNavigator: Util.isDesktop,
),
);
if (context.mounted) {
return await showDialog<void>(
context: context,
builder: (_) => StackOkDialog(
title: "Failed to complete signing process",
desktopPopRootNavigator: Util.isDesktop,
),
);
}
}
},
),

View file

@ -1,19 +1,27 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:stackwallet/frost_route_generator.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/tx_v2/transaction_v2_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/amount/amount_formatter.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/show_loading.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart';
import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart';
import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/detail_item.dart';
import 'package:stackwallet/widgets/expandable.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
class FrostSendStep4 extends ConsumerStatefulWidget {
@ -27,46 +35,154 @@ class FrostSendStep4 extends ConsumerStatefulWidget {
}
class _FrostSendStep4State extends ConsumerState<FrostSendStep4> {
final List<bool> _expandedStates = [];
bool _broadcastLock = false;
late final CryptoCurrency cryptoCurrency;
@override
void initState() {
final wallet = ref.read(pWallets).getWallet(
ref.read(pFrostScaffoldArgs)!.walletId!,
) as BitcoinFrostWallet;
cryptoCurrency = wallet.cryptoCurrency;
for (final _ in ref.read(pFrostTxData)!.recipients!) {
_expandedStates.add(false);
}
super.initState();
}
@override
Widget build(BuildContext context) {
final signerNames = ref.watch(pFrostTxData)!.frostSigners!;
final recipients = ref.watch(pFrostTxData)!.recipients!;
final String signers;
if (signerNames.length > 1) {
signers = signerNames
.sublist(1)
.fold(signerNames.first, (pv, e) => pv += ", $e");
} else {
signers = signerNames.first;
}
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
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,
),
],
if (kDebugMode)
DetailItem(
title: "Tx hex (debug mode only)",
detail: ref.watch(pFrostTxData)!.raw!,
button: Util.isDesktop
? IconCopyButton(
data: ref.watch(pFrostTxData)!.raw!,
)
: SimpleCopyButton(
data: ref.watch(pFrostTxData)!.raw!,
),
),
if (kDebugMode)
const SizedBox(
height: 12,
),
Text(
"Send ${cryptoCurrency.coin.ticker}",
style: STextStyles.w600_20(context),
),
const SizedBox(
height: 12,
),
recipients.length == 1
? _Recipient(
address: recipients[0].address,
amount: ref
.watch(pAmountFormatter(cryptoCurrency.coin))
.format(recipients[0].amount),
)
: Column(
children: [
for (int i = 0; i < recipients.length; i++)
Padding(
padding: const EdgeInsets.only(top: 10),
child: Expandable(
onExpandChanged: (state) {
setState(() {
_expandedStates[i] =
state == ExpandableState.expanded;
});
},
header: Padding(
padding: const EdgeInsets.only(top: 12, bottom: 6),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Recipient ${i + 1}",
style: STextStyles.itemSubtitle(context),
),
SvgPicture.asset(
_expandedStates[i]
? Assets.svg.chevronUp
: Assets.svg.chevronDown,
width: 12,
height: 6,
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
],
),
),
body: _Recipient(
address: recipients[i].address,
amount: ref
.watch(pAmountFormatter(cryptoCurrency.coin))
.format(recipients[i].amount),
),
),
),
],
),
const SizedBox(
height: 12,
),
DetailItem(
title: "Transaction fee",
detail: ref
.watch(pAmountFormatter(cryptoCurrency.coin))
.format(ref.watch(pFrostTxData)!.fee!),
horizontal: true,
),
const SizedBox(
height: 12,
),
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!,
),
title: "Total",
detail: ref.watch(pAmountFormatter(cryptoCurrency.coin)).format(
ref.watch(pFrostTxData)!.fee! +
recipients.map((e) => e.amount).reduce((v, e) => v += e)),
horizontal: true,
),
const SizedBox(
height: 12,
),
DetailItem(
title: "Note",
detail: ref.watch(pFrostTxData)!.note ?? "",
),
const SizedBox(
height: 12,
),
DetailItem(
title: "Signers",
detail: signers,
),
const SizedBox(
height: 12,
@ -76,7 +192,7 @@ class _FrostSendStep4State extends ConsumerState<FrostSendStep4> {
height: 12,
),
PrimaryButton(
label: "Broadcast Transaction",
label: "Approve transaction",
onPressed: () async {
if (_broadcastLock) {
return;
@ -92,11 +208,11 @@ class _FrostSendStep4State extends ConsumerState<FrostSendStep4> {
ref.read(pFrostScaffoldArgs)!.walletId!,
)
.confirmSend(
txData: ref.read(pFrostTxData.state).state!,
txData: ref.read(pFrostTxData)!,
),
context: context,
message: "Broadcasting transaction to network",
isDesktop: Util.isDesktop,
isDesktop: true, // used to pop using root nav
onException: (e) {
ex = e;
},
@ -106,7 +222,7 @@ class _FrostSendStep4State extends ConsumerState<FrostSendStep4> {
throw ex!;
}
if (mounted) {
if (context.mounted) {
if (txData != null) {
ref.read(pFrostTxData.state).state = txData;
Navigator.of(context).popUntil(
@ -123,15 +239,18 @@ class _FrostSendStep4State extends ConsumerState<FrostSendStep4> {
"$e\n$s",
level: LogLevel.Fatal,
);
return await showDialog<void>(
context: context,
builder: (_) => StackOkDialog(
title: "Broadcast error",
message: e.toString(),
desktopPopRootNavigator: Util.isDesktop,
),
);
if (context.mounted) {
return await showDialog<void>(
context: context,
builder: (_) => StackOkDialog(
title: "Broadcast error",
message: e.toString(),
desktopPopRootNavigator: Util.isDesktop,
onOkPressed:
Navigator.of(context, rootNavigator: true).pop,
),
);
}
} finally {
_broadcastLock = false;
}
@ -142,3 +261,35 @@ class _FrostSendStep4State extends ConsumerState<FrostSendStep4> {
);
}
}
class _Recipient extends StatelessWidget {
const _Recipient({
super.key,
required this.address,
required this.amount,
});
final String address;
final String amount;
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
DetailItem(
title: "Address",
detail: address,
),
const SizedBox(
height: 6,
),
DetailItem(
title: "Amount",
detail: amount,
horizontal: true,
),
],
);
}
}

View file

@ -33,7 +33,9 @@ class TxData {
final String? changeAddress;
// frost specific
final String? frostMSConfig;
final List<String>? frostSigners;
// paynym specific
final PaynymAccountLite? paynymAccountLite;
@ -91,6 +93,7 @@ class TxData {
this.usedUTXOs,
this.changeAddress,
this.frostMSConfig,
this.frostSigners,
this.paynymAccountLite,
this.web3dartTransaction,
this.nonce,
@ -166,6 +169,7 @@ class TxData {
})>?
recipients,
String? frostMSConfig,
List<String>? frostSigners,
String? changeAddress,
PaynymAccountLite? paynymAccountLite,
web3dart.Transaction? web3dartTransaction,
@ -209,6 +213,7 @@ class TxData {
usedUTXOs: usedUTXOs ?? this.usedUTXOs,
recipients: recipients ?? this.recipients,
frostMSConfig: frostMSConfig ?? this.frostMSConfig,
frostSigners: frostSigners ?? this.frostSigners,
changeAddress: changeAddress ?? this.changeAddress,
paynymAccountLite: paynymAccountLite ?? this.paynymAccountLite,
web3dartTransaction: web3dartTransaction ?? this.web3dartTransaction,
@ -249,7 +254,7 @@ class TxData {
'recipients: $recipients, '
'utxos: $utxos, '
'usedUTXOs: $usedUTXOs, '
'frostMSConfig: $frostMSConfig, '
'frostSigners: $frostSigners, '
'changeAddress: $changeAddress, '
'paynymAccountLite: $paynymAccountLite, '
'web3dartTransaction: $web3dartTransaction, '