What changed, and why it matters
This commit adds a new user-facing feature to Stack Wallet: support for Epic Cash 'slatepack' transactions. Slatepacks are a manual, offline-friendly way to exchange transaction data (via QR codes, text, or files) as an alternative to the automatic 'Epicbox' method. The change introduces new UI screens, models, and wallet methods to create, receive, decode, and finalize slatepacks. There is no indication in the commit that this is a security fix or vulnerability patch.
No security action required. This is a feature commit. If reviewing for release, verify the updated flutter_libepiccash submodule separately and ensure slate JSON parsing and FFI error handling are robust, but that is outside the scope of the visible diff.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The commit implements Epic Cash slatepack transaction support across mobile and desktop UI flows. It adds Dart model classes (EpicSlatepackResult, EpicSlatepackDecodeResult, EpicReceiveResult, EpicFinalizeResult), new views/dialogs for creating, importing, and finalizing slates, an EpicTransactionMethod enum, a UI toggle, and corresponding methods in EpiccashWallet that wrap libepiccash FFI calls (createTransaction with returnSlate, txReceive, txFinalize). It also updates button-state logic so slatepack mode does not require address validation. The crypto_plugins/flutter_libepiccash submodule is updated, but its diff is not shown. No security-relevant bug fixes, memory-safety changes, or input-sanitization improvements are visible in the provided diff.
Changed components
lib/models/epic_slatepack_models.dartlib/pages/epic_finalize_view/epic_finalize_view.dartlib/pages/receive_view/receive_view.dartlib/pages/receive_view/sub_widgets/epic_slatepack_entry_dialog.dartlib/pages/receive_view/sub_widgets/epic_slatepack_import_dialog.dartlib/pages/send_view/confirm_transaction_view.dartlib/pages/send_view/send_view.dartlib/pages/send_view/sub_widgets/epic_slatepack_dialog.dartlib/pages/wallet_view/wallet_view.dartlib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dartlib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dartlib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dartlib/providers/ui/preview_tx_button_state_provider.dartlib/route_generator.dartlib/utilities/enums/epic_transaction_method.dartlib/wallets/crypto_currency/coins/epiccash.dartlib/wallets/wallet/impl/epiccash_wallet.dartlib/widgets/epic_txs_method_toggle.dartlib/wl_gen/interfaces/libepiccash_interface.dartcrypto_plugins/flutter_libepiccash (submodule)Inspect captured patch +2223 / −31
diff --git a/lib/models/epic_slatepack_models.dart b/lib/models/epic_slatepack_models.dart
new file mode 100644
index 0000000..6df4cc7
--- /dev/null
+++ b/lib/models/epic_slatepack_models.dart
@@ -0,0 +1,116 @@
+class EpicSlatepackResult {
+ final bool success;
+ final String? error;
+ final String? slatepack;
+ final String? slateJson;
+ final bool? wasEncrypted;
+ final String? recipientAddress;
+
+ EpicSlatepackResult({
+ required this.success,
+ this.error,
+ this.slatepack,
+ this.slateJson,
+ this.wasEncrypted,
+ this.recipientAddress,
+ });
+
+ @override
+ String toString() {
+ return "EpicSlatepackResult("
+ "success: $success, "
+ "error: $error, "
+ "slatepack: $slatepack, "
+ "slateJson: $slateJson, "
+ "wasEncrypted: $wasEncrypted, "
+ "recipientAddress: $recipientAddress"
+ ")";
+ }
+}
+
+class EpicSlatepackDecodeResult {
+ final bool success;
+ final String? error;
+ final String? slateJson;
+ final bool? wasEncrypted;
+ final String? senderAddress;
+ final String? recipientAddress;
+
+ EpicSlatepackDecodeResult({
+ required this.success,
+ this.error,
+ this.slateJson,
+ this.wasEncrypted,
+ this.senderAddress,
+ this.recipientAddress,
+ });
+
+ @override
+ String toString() {
+ return "EpicSlatepackDecodeResult("
+ "success: $success, "
+ "error: $error, "
+ "slateJson: $slateJson, "
+ "wasEncrypted: $wasEncrypted, "
+ "senderAddress: $senderAddress, "
+ "recipientAddress: $recipientAddress"
+ ")";
+ }
+}
+
+class EpicReceiveResult {
+ final bool success;
+ final String? error;
+ final String? slateId;
+ final String? commitId;
+ final String? responseSlatepack;
+ final bool? wasEncrypted;
+ final String? recipientAddress;
+
+ EpicReceiveResult({
+ required this.success,
+ this.error,
+ this.slateId,
+ this.commitId,
+ this.responseSlatepack,
+ this.wasEncrypted,
+ this.recipientAddress,
+ });
+
+ @override
+ String toString() {
+ return "EpicReceiveResult("
+ "success: $success, "
+ "error: $error, "
+ "slateId: $slateId, "
+ "commitId: $commitId, "
+ "responseSlatepack: $responseSlatepack, "
+ "wasEncrypted: $wasEncrypted, "
+ "recipientAddress: $recipientAddress"
+ ")";
+ }
+}
+
+class EpicFinalizeResult {
+ final bool success;
+ final String? error;
+ final String? slateId;
+ final String? commitId;
+
+ EpicFinalizeResult({
+ required this.success,
+ this.error,
+ this.slateId,
+ this.commitId,
+ });
+
+ @override
+ String toString() {
+ return "EpicFinalizeResult("
+ "success: $success, "
+ "error: $error, "
+ "slateId: $slateId, "
+ "commitId: $commitId"
+ ")";
+ }
+}
diff --git a/lib/pages/epic_finalize_view/epic_finalize_view.dart b/lib/pages/epic_finalize_view/epic_finalize_view.dart
new file mode 100644
index 0000000..13abb38
--- /dev/null
+++ b/lib/pages/epic_finalize_view/epic_finalize_view.dart
@@ -0,0 +1,338 @@
+/*
+ * 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 2026-01-12
+ *
+ */
+
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+
+import '../../notifications/show_flush_bar.dart';
+import '../../providers/global/barcode_scanner_provider.dart';
+import '../../providers/global/wallets_provider.dart';
+import '../../themes/stack_colors.dart';
+import '../../utilities/barcode_scanner_interface.dart';
+import '../../utilities/clipboard_interface.dart';
+import '../../utilities/constants.dart';
+import '../../utilities/logger.dart';
+import '../../utilities/show_loading.dart';
+import '../../utilities/text_styles.dart';
+import '../../utilities/util.dart';
+import '../../wallets/wallet/impl/epiccash_wallet.dart';
+import '../../widgets/background.dart';
+import '../../widgets/conditional_parent.dart';
+import '../../widgets/custom_buttons/app_bar_icon_button.dart';
+import '../../widgets/desktop/primary_button.dart';
+import '../../widgets/icon_widgets/clipboard_icon.dart';
+import '../../widgets/icon_widgets/qrcode_icon.dart';
+import '../../widgets/icon_widgets/x_icon.dart';
+import '../../widgets/stack_dialog.dart';
+import '../../widgets/stack_text_field.dart';
+import '../../widgets/textfield_icon_button.dart';
+
+class EpicFinalizeView extends ConsumerStatefulWidget {
+ const EpicFinalizeView({
+ super.key,
+ required this.walletId,
+ this.clipboard = const ClipboardWrapper(),
+ });
+
+ static const String routeName = "/epicFinalizeView";
+
+ final String walletId;
+ final ClipboardInterface clipboard;
+
+ @override
+ ConsumerState<EpicFinalizeView> createState() => _EpicFinalizeViewState();
+}
+
+class _EpicFinalizeViewState extends ConsumerState<EpicFinalizeView> {
+ late final TextEditingController _slateController;
+ late final FocusNode _slateFocusNode;
+
+ bool _slateToggleFlag = false;
+
+ Future<void> _pasteSlatepack() async {
+ final ClipboardData? data = await widget.clipboard.getData(
+ Clipboard.kTextPlain,
+ );
+ if (data?.text != null && data!.text!.isNotEmpty) {
+ _slateController.text = data.text!;
+ setState(() {
+ _slateToggleFlag = _slateController.text.isNotEmpty;
+ });
+ }
+ }
+
+ Future<void> _scanQr() async {
+ try {
+ if (!Util.isDesktop && _slateFocusNode.hasFocus) {
+ _slateFocusNode.unfocus();
+ await Future<void>.delayed(const Duration(milliseconds: 75));
+ }
+
+ if (mounted) {
+ final qrResult = await ref.read(pBarcodeScanner).scan(context: context);
+ if (qrResult.rawContent != null && qrResult.rawContent!.isNotEmpty) {
+ _slateController.text = qrResult.rawContent!;
+ setState(() {
+ _slateToggleFlag = _slateController.text.isNotEmpty;
+ });
+ }
+ }
+ } on PlatformException catch (e, s) {
+ if (mounted) {
+ try {
+ await checkCamPermDeniedMobileAndOpenAppSettings(
+ context,
+ logging: Logging.instance,
+ );
+ } catch (e, s) {
+ Logging.instance.e(
+ "Failed to check cam permissions",
+ error: e,
+ stackTrace: s,
+ );
+ }
+ } else {
+ Logging.instance.e(
+ "Failed to get camera permissions while trying to scan qr code in SendView: ",
+ error: e,
+ stackTrace: s,
+ );
+ }
+ }
+ }
+
+ Future<void> _finalize() async {
+ // add delay for showloading exception catching hack fix
+ await Future<void>.delayed(const Duration(seconds: 1));
+
+ final wallet =
+ ref.read(pWallets).getWallet(widget.walletId) as EpiccashWallet;
+
+ final decoded = await wallet.decodeSlatepack(_slateController.text);
+ if (!decoded.success) {
+ throw Exception(decoded.error ?? "Failed to decode slate");
+ }
+
+ final analysis = await wallet.analyzeSlatepack(_slateController.text);
+ if (analysis.status != "S2") {
+ throw Exception("Invalid slate type: ${analysis.status}");
+ }
+
+ final result = await wallet.finalizeSlatepack(_slateController.text);
+
+ if (!result.success) {
+ throw Exception(
+ result.error ?? "Finalize failed without providing an error???",
+ );
+ }
+ }
+
+ Future<void> _finalizePressed() async {
+ if (!Util.isDesktop && _slateFocusNode.hasFocus) {
+ _slateFocusNode.unfocus();
+ await Future<void>.delayed(const Duration(milliseconds: 75));
+ }
+ if (mounted) {
+ Exception? ex;
+ await showLoading(
+ whileFuture: _finalize(),
+ context: context,
+ message: "Finalizing slate...",
+ rootNavigator: Util.isDesktop,
+ onException: (e) => ex = e,
+ );
+
+ if (mounted) {
+ if (ex != null) {
+ await showDialog<void>(
+ context: context,
+ builder: (context) => StackOkDialog(
+ desktopPopRootNavigator: Util.isDesktop,
+ title: "Slate finalize error",
+ message: ex?.toString() ?? "Unexpected result without exception",
+ maxWidth: Util.isDesktop ? 400 : null,
+ ),
+ );
+ } else {
+ setState(() {
+ _slateController.text = "";
+ });
+ unawaited(
+ showFloatingFlushBar(
+ type: FlushBarType.success,
+ message: "Transaction finalized and broadcast successfully!",
+ context: context,
+ ),
+ );
+ }
+ }
+ }
+ }
+
+ @override
+ void initState() {
+ super.initState();
+ _slateController = TextEditingController();
+ _slateFocusNode = FocusNode();
+ }
+
+ @override
+ void dispose() {
+ _slateController.dispose();
+ _slateFocusNode.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ debugPrint("BUILD: $runtimeType");
+
+ return 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(
+ "Finalize slate",
+ 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: EdgeInsets.symmetric(
+ horizontal: Constants.size.standardPadding,
+ ),
+ child: child,
+ ),
+ ),
+ ),
+ );
+ },
+ ),
+ ),
+ ),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ ClipRRect(
+ borderRadius: BorderRadius.circular(
+ Constants.size.circularBorderRadius,
+ ),
+ child: TextField(
+ minLines: 1,
+ maxLines: 5,
+ key: const Key("epicFinalizeSlateFieldKey"),
+ controller: _slateController,
+ readOnly: false,
+ autocorrect: false,
+ enableSuggestions: false,
+ toolbarOptions: const ToolbarOptions(
+ copy: false,
+ cut: false,
+ paste: true,
+ selectAll: false,
+ ),
+ onChanged: (newValue) {
+ setState(() {
+ _slateToggleFlag = newValue.isNotEmpty;
+ });
+ },
+ focusNode: _slateFocusNode,
+ style: STextStyles.field(context),
+ decoration:
+ standardInputDecoration(
+ "Enter Response Slate JSON",
+ _slateFocusNode,
+ context,
+ desktopMed: true,
+ ).copyWith(
+ contentPadding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical:
+ 12, // Adjust vertical padding for better alignment
+ ),
+ suffixIcon: Padding(
+ padding: _slateController.text.isEmpty
+ ? const EdgeInsets.only(right: 8)
+ : const EdgeInsets.only(right: 0),
+ child: UnconstrainedBox(
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceAround,
+ children: [
+ _slateToggleFlag
+ ? TextFieldIconButton(
+ key: const Key(
+ "epicSlateFinalizeClearFieldButtonKey",
+ ),
+ onTap: () {
+ _slateController.text = "";
+ setState(() {
+ _slateToggleFlag = false;
+ });
+ },
+ child: const XIcon(),
+ )
+ : TextFieldIconButton(
+ key: const Key(
+ "epicSlateFinalizePasteFieldButtonKey",
+ ),
+ onTap: _pasteSlatepack,
+ child: _slateController.text.isEmpty
+ ? const ClipboardIcon()
+ : const XIcon(),
+ ),
+ if (_slateController.text.isEmpty)
+ TextFieldIconButton(
+ semanticsLabel:
+ "Scan QR Button. Opens Camera For Scanning QR Code.",
+ key: const Key("sendViewScanQrButtonKey"),
+ onTap: _scanQr,
+ child: const QrCodeIcon(),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ Util.isDesktop ? const SizedBox(height: 24) : const Spacer(),
+ PrimaryButton(
+ label: "Finalize Slate",
+ enabled: _slateToggleFlag,
+ onPressed: _slateToggleFlag ? _finalizePressed : null,
+ ),
+
+ if (!Util.isDesktop) SizedBox(height: Constants.size.standardPadding),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/pages/receive_view/receive_view.dart b/lib/pages/receive_view/receive_view.dart
index 7ef4f26..61f8ae5 100644
--- a/lib/pages/receive_view/receive_view.dart
+++ b/lib/pages/receive_view/receive_view.dart
@@ -33,6 +33,7 @@ import '../../utilities/text_styles.dart';
import '../../wallets/crypto_currency/crypto_currency.dart';
import '../../wallets/isar/providers/wallet_info_provider.dart';
import '../../wallets/wallet/impl/bitcoin_wallet.dart';
+import '../../wallets/wallet/impl/epiccash_wallet.dart';
import '../../wallets/wallet/impl/mimblewimblecoin_wallet.dart';
import '../../wallets/wallet/intermediate/bip39_hd_wallet.dart';
import '../../wallets/wallet/wallet_mixin_interfaces/bcash_interface.dart';
@@ -54,6 +55,8 @@ import '../../widgets/rounded_white_container.dart';
import '../../widgets/stack_dialog.dart';
import 'addresses/wallet_addresses_view.dart';
import 'generate_receiving_uri_qr_code_view.dart';
+import 'sub_widgets/epic_slatepack_entry_dialog.dart';
+import 'sub_widgets/epic_slatepack_import_dialog.dart';
import 'sub_widgets/mwc_slatepack_import_dialog.dart';
import 'sub_widgets/slatepack_entry_dialog.dart';
@@ -150,6 +153,67 @@ class _ReceiveViewState extends ConsumerState<ReceiveView> {
}
}
+ Future<void> _importEpicSlatepack() async {
+ final slatepackString = await showDialog<String>(
+ context: context,
+ builder: (context) => const EpicSlatepackEntryDialog(),
+ );
+
+ if (slatepackString == null) return;
+ if (mounted) {
+ final wallet =
+ ref.read(pWallets).getWallet(walletId) as EpiccashWallet;
+
+ Exception? ex;
+ final result = await showLoading(
+ whileFuture: wallet.fullDecodeSlatepack(slatepackString),
+ context: context,
+ message: "Decoding slate...",
+ onException: (e) => ex = e,
+ );
+
+ if (result == null || ex != null) {
+ if (mounted) {
+ await showDialog<void>(
+ context: context,
+ builder: (context) => StackOkDialog(
+ title: "Slate receive error",
+ message: ex?.toString() ?? "Unexpected result without exception",
+ ),
+ );
+ }
+ return;
+ }
+
+ if (mounted) {
+ final response =
+ await showDialog<({String responseSlatepack, bool wasEncrypted})>(
+ context: context,
+ builder: (context) => SDialog(
+ child: EpicSlatepackImportDialog(
+ walletId: widget.walletId,
+ clipboard: widget.clipboard,
+ rawSlatepack: result.raw,
+ decoded: result.result,
+ slatepackType: result.type,
+ ),
+ ),
+ );
+
+ if (mounted && response != null) {
+ await showDialog<void>(
+ context: context,
+ barrierDismissible: false,
+ builder: (context) => EpicSlatepackResponseDialog(
+ responseSlatepack: response.responseSlatepack,
+ wasEncrypted: response.wasEncrypted,
+ ),
+ );
+ }
+ }
+ }
+ }
+
Future<void> generateNewAddress() async {
final wallet = ref.read(pWallets).getWallet(walletId);
@@ -764,6 +828,14 @@ class _ReceiveViewState extends ConsumerState<ReceiveView> {
onPressed: _importSlatepack,
),
],
+ // Epic Cash Slate import button.
+ if (coin is Epiccash) ...[
+ const SizedBox(height: 12),
+ SecondaryButton(
+ label: "Import Slate",
+ onPressed: _importEpicSlatepack,
+ ),
+ ],
const SizedBox(height: 30),
RoundedWhiteContainer(
child: Padding(
diff --git a/lib/pages/receive_view/sub_widgets/epic_slatepack_entry_dialog.dart b/lib/pages/receive_view/sub_widgets/epic_slatepack_entry_dialog.dart
new file mode 100644
index 0000000..c3583e7
--- /dev/null
+++ b/lib/pages/receive_view/sub_widgets/epic_slatepack_entry_dialog.dart
@@ -0,0 +1,219 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+
+import '../../../providers/global/barcode_scanner_provider.dart';
+import '../../../themes/stack_colors.dart';
+import '../../../utilities/barcode_scanner_interface.dart';
+import '../../../utilities/clipboard_interface.dart';
+import '../../../utilities/constants.dart';
+import '../../../utilities/logger.dart';
+import '../../../utilities/text_styles.dart';
+import '../../../widgets/desktop/primary_button.dart';
+import '../../../widgets/desktop/secondary_button.dart';
+import '../../../widgets/icon_widgets/clipboard_icon.dart';
+import '../../../widgets/icon_widgets/qrcode_icon.dart';
+import '../../../widgets/icon_widgets/x_icon.dart';
+import '../../../widgets/stack_dialog.dart';
+import '../../../widgets/stack_text_field.dart';
+import '../../../widgets/textfield_icon_button.dart';
+
+class EpicSlatepackEntryDialog extends ConsumerStatefulWidget {
+ const EpicSlatepackEntryDialog({
+ super.key,
+ this.clipboard = const ClipboardWrapper(),
+ });
+
+ final ClipboardInterface clipboard;
+
+ @override
+ ConsumerState<EpicSlatepackEntryDialog> createState() =>
+ _EpicSlatepackEntryDialogState();
+}
+
+class _EpicSlatepackEntryDialogState extends ConsumerState<EpicSlatepackEntryDialog> {
+ final _receiveSlateController = TextEditingController();
+ final _slateFocusNode = FocusNode();
+
+ bool _slateToggleFlag = false;
+
+ Future<void> _pasteSlatepack() async {
+ final ClipboardData? data = await widget.clipboard.getData(
+ Clipboard.kTextPlain,
+ );
+ if (data?.text != null && data!.text!.isNotEmpty) {
+ _receiveSlateController.text = data.text!;
+ setState(() {
+ _slateToggleFlag = _receiveSlateController.text.isNotEmpty;
+ });
+ }
+ }
+
+ Future<void> _scanQr() async {
+ try {
+ if (_slateFocusNode.hasFocus) {
+ _slateFocusNode.unfocus();
+ await Future<void>.delayed(const Duration(milliseconds: 75));
+ }
+
+ if (mounted) {
+ final qrResult = await ref.read(pBarcodeScanner).scan(context: context);
+ if (qrResult.rawContent != null && qrResult.rawContent!.isNotEmpty) {
+ _receiveSlateController.text = qrResult.rawContent!;
+ setState(() {
+ _slateToggleFlag = _receiveSlateController.text.isNotEmpty;
+ });
+ }
+ }
+ } on PlatformException catch (e, s) {
+ if (mounted) {
+ try {
+ await checkCamPermDeniedMobileAndOpenAppSettings(
+ context,
+ logging: Logging.instance,
+ );
+ } catch (e, s) {
+ Logging.instance.e(
+ "Failed to check cam permissions",
+ error: e,
+ stackTrace: s,
+ );
+ }
+ } else {
+ Logging.instance.e(
+ "Failed to get camera permissions while trying to scan qr code in SendView: ",
+ error: e,
+ stackTrace: s,
+ );
+ }
+ }
+ }
+
+ @override
+ void dispose() {
+ _receiveSlateController.dispose();
+ _slateFocusNode.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return StackDialogBase(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Text(
+ "Receive Slate",
+ style: STextStyles.desktopTextExtraSmall(context).copyWith(
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.textFieldActiveSearchIconRight,
+ ),
+ textAlign: TextAlign.left,
+ ),
+ const SizedBox(height: 12),
+ ClipRRect(
+ borderRadius: BorderRadius.circular(
+ Constants.size.circularBorderRadius,
+ ),
+ child: TextField(
+ minLines: 1,
+ maxLines: 5,
+ key: const Key("receiveViewEpicSlateFieldKey"),
+ controller: _receiveSlateController,
+ readOnly: false,
+ autocorrect: false,
+ enableSuggestions: false,
+ toolbarOptions: const ToolbarOptions(
+ copy: false,
+ cut: false,
+ paste: true,
+ selectAll: false,
+ ),
+ onChanged: (newValue) {
+ setState(() {
+ _slateToggleFlag = newValue.isNotEmpty;
+ });
+ },
+ focusNode: _slateFocusNode,
+ style: STextStyles.desktopTextExtraSmall(context).copyWith(
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.textFieldActiveText,
+ height: 1.8,
+ ),
+ decoration:
+ standardInputDecoration(
+ "Enter Slate JSON",
+ _slateFocusNode,
+ context,
+ desktopMed: true,
+ ).copyWith(
+ contentPadding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical:
+ 12, // Adjust vertical padding for better alignment
+ ),
+ suffixIcon: Padding(
+ padding: _receiveSlateController.text.isEmpty
+ ? const EdgeInsets.only(right: 8)
+ : const EdgeInsets.only(right: 0),
+ child: UnconstrainedBox(
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceAround,
+ children: [
+ _slateToggleFlag
+ ? TextFieldIconButton(
+ key: const Key(
+ "receiveViewClearEpicSlateFieldButtonKey",
+ ),
+ onTap: () {
+ _receiveSlateController.text = "";
+ setState(() {
+ _slateToggleFlag = false;
+ });
+ },
+ child: const XIcon(),
+ )
+ : TextFieldIconButton(
+ key: const Key(
+ "receiveViewPasteEpicSlateFieldButtonKey",
+ ),
+ onTap: _pasteSlatepack,
+ child: _receiveSlateController.text.isEmpty
+ ? const ClipboardIcon()
+ : const XIcon(),
+ ),
+ if (_receiveSlateController.text.isEmpty)
+ TextFieldIconButton(
+ semanticsLabel:
+ "Scan QR Button. Opens Camera For Scanning QR Code.",
+ key: const Key("sendViewScanQrButtonKey"),
+ onTap: _scanQr,
+ child: const QrCodeIcon(),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ const SizedBox(height: 16),
+ PrimaryButton(
+ label: "Import",
+ enabled: _slateToggleFlag,
+ onPressed: !_slateToggleFlag
+ ? null
+ : () => Navigator.of(context).pop(_receiveSlateController.text),
+ ),
+ const SizedBox(height: 16),
+ SecondaryButton(
+ label: "Cancel",
+ onPressed: Navigator.of(context).pop,
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/pages/receive_view/sub_widgets/epic_slatepack_import_dialog.dart b/lib/pages/receive_view/sub_widgets/epic_slatepack_import_dialog.dart
new file mode 100644
index 0000000..769a58a
--- /dev/null
+++ b/lib/pages/receive_view/sub_widgets/epic_slatepack_import_dialog.dart
@@ -0,0 +1,316 @@
+import 'dart:convert';
+
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+
+import '../../../models/epic_slatepack_models.dart';
+import '../../../providers/global/wallets_provider.dart';
+import '../../../themes/stack_colors.dart';
+import '../../../utilities/amount/amount.dart';
+import '../../../utilities/amount/amount_formatter.dart';
+import '../../../utilities/clipboard_interface.dart';
+import '../../../utilities/show_loading.dart';
+import '../../../utilities/text_styles.dart';
+import '../../../utilities/util.dart';
+import '../../../wallets/isar/providers/wallet_info_provider.dart';
+import '../../../wallets/wallet/impl/epiccash_wallet.dart';
+import '../../../widgets/conditional_parent.dart';
+import '../../../widgets/custom_buttons/simple_copy_button.dart';
+import '../../../widgets/desktop/desktop_dialog_close_button.dart';
+import '../../../widgets/desktop/primary_button.dart';
+import '../../../widgets/desktop/secondary_button.dart';
+import '../../../widgets/detail_item.dart';
+import '../../../widgets/dialogs/s_dialog.dart';
+import '../../../widgets/rounded_white_container.dart';
+import '../../../widgets/stack_dialog.dart';
+
+class EpicSlatepackImportDialog extends ConsumerStatefulWidget {
+ const EpicSlatepackImportDialog({
+ super.key,
+ required this.walletId,
+ required this.rawSlatepack,
+ required this.decoded,
+ required this.slatepackType,
+ this.clipboard = const ClipboardWrapper(),
+ });
+
+ final String walletId;
+ final String rawSlatepack;
+ final EpicSlatepackDecodeResult decoded;
+ final String slatepackType;
+ final ClipboardInterface clipboard;
+
+ @override
+ ConsumerState<EpicSlatepackImportDialog> createState() =>
+ _EpicSlatepackImportDialogState();
+}
+
+class _EpicSlatepackImportDialogState
+ extends ConsumerState<EpicSlatepackImportDialog> {
+ Future<({String responseSlatepack, bool wasEncrypted})>
+ _processSlatepack() async {
+ // add delay for showloading exception catching hack fix
+ await Future<void>.delayed(const Duration(seconds: 1));
+
+ final wallet =
+ ref.read(pWallets).getWallet(widget.walletId) as EpiccashWallet;
+
+ // Determine action based on slatepack type.
+ if (widget.slatepackType.contains("S1")) {
+ // This is an initial slatepack - receive it and create response.
+ final result = await wallet.receiveSlatepack(widget.rawSlatepack);
+
+ if (result.success && result.responseSlatepack != null) {
+ return (
+ responseSlatepack: result.responseSlatepack!,
+ wasEncrypted: result.wasEncrypted ?? false,
+ );
+ } else {
+ throw Exception(result.error ?? 'Failed to process slatepack');
+ }
+ } else {
+ throw Exception('Unsupported slatepack type: ${widget.slatepackType}');
+ }
+ }
+
+ Future<void> _processPressed() async {
+ Exception? ex;
+ final result = await showLoading(
+ whileFuture: _processSlatepack(),
+ context: context,
+ message: "Processing slate...",
+ onException: (e) => ex = e,
+ );
+
+ if (result == null || ex != null) {
+ if (mounted) {
+ await showDialog<void>(
+ context: context,
+ useRootNavigator: true,
+ builder:
+ (context) => StackOkDialog(
+ desktopPopRootNavigator: true,
+ maxWidth: Util.isDesktop ? 400 : null,
+ title: "Slate receive error",
+ message:
+ ex?.toString() ?? "Unexpected result without exception",
+ ),
+ );
+ }
+ return;
+ }
+
+ if (mounted) {
+ Navigator.of(context).pop(result);
+ }
+ }
+
+ late final Amount? _amount;
+
+ @override
+ void initState() {
+ final map = jsonDecode(widget.decoded.slateJson!) as Map;
+
+ final rawAmount = BigInt.tryParse(map["amount"].toString());
+ _amount =
+ rawAmount == null
+ ? null
+ : Amount(
+ rawValue: rawAmount,
+ fractionDigits:
+ ref.read(pWalletCoin(widget.walletId)).fractionDigits,
+ );
+
+ super.initState();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final isDesktop = Util.isDesktop;
+
+ return Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ if (isDesktop)
+ // Header with title and close button.
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Padding(
+ padding: const EdgeInsets.only(left: 32),
+ child: Text(
+ "Import Slate",
+ style: STextStyles.pageTitleH2(context),
+ ),
+ ),
+ const DesktopDialogCloseButton(),
+ ],
+ ),
+ Padding(
+ padding: EdgeInsets.symmetric(horizontal: isDesktop ? 32 : 24),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ ConditionalParent(
+ condition: isDesktop,
+ builder:
+ (child) => RoundedWhiteContainer(
+ borderColor:
+ isDesktop
+ ? Theme.of(
+ context,
+ ).extension<StackColors>()!.backgroundAppBar
+ : null,
+ padding: const EdgeInsets.all(0),
+ child: child,
+ ),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ if (!isDesktop)
+ Padding(
+ padding: const EdgeInsets.only(top: 24, bottom: 24),
+ child: Text(
+ "Import slate",
+ style: STextStyles.pageTitleH2(context),
+ ),
+ ),
+
+ if (_amount != null)
+ DetailItem(
+ title: "Amount",
+ detail: ref
+ .watch(
+ pAmountFormatter(
+ ref.watch(pWalletCoin(widget.walletId)),
+ ),
+ )
+ .format(_amount),
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(height: 24),
+ ConditionalParent(
+ condition: isDesktop,
+ builder:
+ (child) => Row(
+ mainAxisAlignment: MainAxisAlignment.end,
+ children: [child],
+ ),
+ child: PrimaryButton(
+ width: isDesktop ? 220 : null,
+
+ buttonHeight: isDesktop ? ButtonHeight.l : null,
+ label: "Process",
+ onPressed: _processPressed,
+ ),
+ ),
+ if (!isDesktop) const SizedBox(height: 12),
+ if (!isDesktop)
+ SecondaryButton(
+ label: "Cancel",
+ onPressed: Navigator.of(context).pop,
+ ),
+ ],
+ ),
+ ),
+ isDesktop ? const SizedBox(height: 32) : const SizedBox(height: 24),
+ ],
+ );
+ }
+}
+
+class EpicSlatepackResponseDialog extends StatelessWidget {
+ const EpicSlatepackResponseDialog({
+ super.key,
+ required this.responseSlatepack,
+ required this.wasEncrypted,
+ });
+
+ final String responseSlatepack;
+ final bool wasEncrypted;
+
+ @override
+ Widget build(BuildContext context) {
+ return SDialog(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ // Header with title and close button.
+ if (Util.isDesktop)
+ Padding(
+ padding: const EdgeInsets.only(left: 32),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(
+ "Response Slate",
+ style: STextStyles.pageTitleH2(context),
+ ),
+ const DesktopDialogCloseButton(),
+ ],
+ ),
+ ),
+ Padding(
+ padding:
+ Util.isDesktop
+ ? const EdgeInsets.only(left: 32, right: 32, bottom: 32)
+ : const EdgeInsets.only(left: 24, right: 24, bottom: 24),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ if (!Util.isDesktop) const SizedBox(height: 24),
+ Text(
+ "Return this slate to the sender to complete the transaction.",
+ style: STextStyles.pageTitleH2(context),
+ ),
+ const SizedBox(height: 24),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(
+ "Response slate",
+ style: STextStyles.itemSubtitle(context),
+ ),
+ SimpleCopyButton(data: responseSlatepack),
+ ],
+ ),
+ const SizedBox(height: 8),
+ ConditionalParent(
+ condition: !Util.isDesktop,
+ builder:
+ (child) => SizedBox(
+ height: 220,
+ child: SingleChildScrollView(child: child),
+ ),
+ child: SelectableText(
+ responseSlatepack,
+ style: STextStyles.w500_14(context),
+ ),
+ ),
+ const SizedBox(height: 24),
+ ConditionalParent(
+ condition: Util.isDesktop,
+ builder:
+ (child) => Row(
+ mainAxisAlignment: MainAxisAlignment.end,
+ children: [child],
+ ),
+ child: PrimaryButton(
+ label: "Done",
+ width: Util.isDesktop ? 220 : null,
+ buttonHeight: Util.isDesktop ? ButtonHeight.l : null,
+ onPressed: Navigator.of(context).pop,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/pages/send_view/confirm_transaction_view.dart b/lib/pages/send_view/confirm_transaction_view.dart
index 91570b6..9538719 100644
--- a/lib/pages/send_view/confirm_transaction_view.dart
+++ b/lib/pages/send_view/confirm_transaction_view.dart
@@ -41,6 +41,7 @@ import '../../wallets/isar/providers/eth/current_token_wallet_provider.dart';
import '../../wallets/isar/providers/solana/current_sol_token_wallet_provider.dart';
import '../../wallets/isar/providers/wallet_info_provider.dart';
import '../../wallets/models/tx_data.dart';
+import '../../wallets/wallet/impl/epiccash_wallet.dart';
import '../../wallets/wallet/impl/firo_wallet.dart';
import '../../wallets/wallet/impl/mimblewimblecoin_wallet.dart';
import '../../wallets/wallet/impl/solana_wallet.dart';
@@ -60,6 +61,7 @@ import '../../widgets/textfield_icon_button.dart';
import '../../wl_gen/interfaces/libepiccash_interface.dart';
import '../pinpad_views/lock_screen_view.dart';
import '../wallet_view/wallet_view.dart';
+import 'sub_widgets/epic_slatepack_dialog.dart';
import 'sub_widgets/mwc_slatepack_dialog.dart';
import 'sub_widgets/sending_transaction_dialog.dart';
@@ -188,6 +190,85 @@ class _ConfirmTransactionViewState
}
}
+ /// Handle Epic Cash slate creation for manual exchange.
+ Future<void> _handleEpicSlatepackCreation(
+ BuildContext context,
+ EpiccashWallet wallet,
+ ) async {
+ try {
+ // Close the progress dialog first.
+ Navigator.of(context).pop();
+
+ // Get recipient information from txData.
+ final recipient = widget.txData.recipients?.first;
+ if (recipient == null) {
+ throw Exception('No recipient found in transaction data');
+ }
+
+ // Create slatepack.
+ final slatepackResult = await wallet.createSlatepack(
+ amount: recipient.amount,
+ recipientAddress: recipient.address.isNotEmpty
+ ? recipient.address
+ : null,
+ message: onChainNoteController.text.isNotEmpty
+ ? onChainNoteController.text
+ : null,
+ );
+
+ if (!slatepackResult.success || slatepackResult.slatepack == null) {
+ throw Exception(slatepackResult.error ?? 'Failed to create slate');
+ }
+
+ // Show slatepack dialog.
+ if (context.mounted) {
+ await showDialog<void>(
+ context: context,
+ barrierDismissible: false,
+ builder: (context) =>
+ EpicSlatepackDialog(slatepackResult: slatepackResult),
+ );
+
+ // After slatepack dialog is closed, navigate back to wallet.
+ if (context.mounted) {
+ widget.onSuccess.call();
+ if (widget.onSuccessInsteadOfRouteOnSuccess == null) {
+ Navigator.of(
+ context,
+ ).popUntil(ModalRoute.withName(routeOnSuccessName));
+ } else {
+ widget.onSuccessInsteadOfRouteOnSuccess!.call();
+ }
+ }
+ }
+ } catch (e, s) {
+ Logging.instance.e('Failed to create Epic Cash slate: $e\n$s');
+
+ if (context.mounted) {
+ // Show user-friendly error message.
+ final errorMessage = e.toString().contains('insufficient funds')
+ ? 'Insufficient funds for this transaction'
+ : e.toString().contains('wallet not open')
+ ? 'Wallet not accessible. Please restart the app.'
+ : 'Failed to create slate: ${e.toString()}';
+
+ await showDialog<void>(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: const Text('Slate Creation Failed'),
+ content: Text('Failed to create slate: $e'),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.of(context).pop(),
+ child: const Text('OK'),
+ ),
+ ],
+ ),
+ );
+ }
+ }
+ }
+
Future<void> _attemptSend(BuildContext context) async {
final wallet = ref.read(pWallets).getWallet(walletId);
final coin = wallet.info.coin;
@@ -276,11 +357,28 @@ class _ConfirmTransactionViewState
);
}
} else if (coin is Epiccash) {
- txDataFuture = wallet.confirmSend(
- txData: widget.txData.copyWith(
- noteOnChain: onChainNoteController.text,
- ),
- );
+ // Check if this is a slatepack transaction (manual exchange).
+ final epicOtherDataMap = widget.txData.otherData != null
+ ? jsonDecode(widget.txData.otherData!)
+ : null;
+ final epicTransactionMethod =
+ epicOtherDataMap?['transactionMethod'] as String?;
+
+ if (epicTransactionMethod == 'slatepack') {
+ // Handle slatepack creation instead of direct send.
+ await _handleEpicSlatepackCreation(
+ context,
+ wallet as EpiccashWallet,
+ );
+ return; // Exit early, don't continue with normal transaction flow.
+ } else {
+ // Handle Epicbox transactions normally.
+ txDataFuture = wallet.confirmSend(
+ txData: widget.txData.copyWith(
+ noteOnChain: onChainNoteController.text,
+ ),
+ );
+ }
} else {
txDataFuture = wallet.confirmSend(txData: widget.txData);
}
diff --git a/lib/pages/send_view/send_view.dart b/lib/pages/send_view/send_view.dart
index 07595be..a58ec0b 100644
--- a/lib/pages/send_view/send_view.dart
+++ b/lib/pages/send_view/send_view.dart
@@ -18,6 +18,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:tuple/tuple.dart';
+import '../../models/epic_slatepack_models.dart';
import '../../models/input.dart';
import '../../models/isar/models/isar_models.dart';
import '../../models/mwc_slatepack_models.dart';
@@ -53,6 +54,7 @@ import '../../wallets/crypto_currency/intermediate/cryptonote_currency.dart';
import '../../wallets/crypto_currency/intermediate/nano_currency.dart';
import '../../wallets/isar/providers/wallet_info_provider.dart';
import '../../wallets/models/tx_data.dart';
+import '../../wallets/wallet/impl/epiccash_wallet.dart';
import '../../wallets/wallet/impl/firo_wallet.dart';
import '../../wallets/wallet/impl/mimblewimblecoin_wallet.dart';
import '../../wallets/wallet/intermediate/cryptonote_wallet.dart';
@@ -71,6 +73,7 @@ import '../../widgets/icon_widgets/addressbook_icon.dart';
import '../../widgets/icon_widgets/clipboard_icon.dart';
import '../../widgets/icon_widgets/qrcode_icon.dart';
import '../../widgets/icon_widgets/x_icon.dart';
+import '../../widgets/epic_txs_method_toggle.dart';
import '../../widgets/mwc_txs_method_toggle.dart';
import '../../widgets/rounded_white_container.dart';
import '../../widgets/stack_dialog.dart';
@@ -81,6 +84,7 @@ import '../coin_control/coin_control_view.dart';
import 'confirm_transaction_view.dart';
import 'sub_widgets/building_transaction_dialog.dart';
import 'sub_widgets/dual_balance_selection_sheet.dart';
+import 'sub_widgets/epic_slatepack_dialog.dart';
import 'sub_widgets/mwc_slatepack_dialog.dart';
import 'sub_widgets/transaction_fee_selection_sheet.dart';
@@ -695,6 +699,93 @@ class _SendViewState extends ConsumerState<SendView> {
}
}
+ Future<void> _createEpicSlatepack() async {
+ // wait for keyboard to disappear
+ FocusScope.of(context).unfocus();
+ await Future<void>.delayed(const Duration(milliseconds: 100));
+
+ try {
+ if (mounted) {
+ final wallet =
+ ref.read(pWallets).getWallet(walletId) as EpiccashWallet;
+
+ final amount = ref.read(pSendAmount)!;
+
+ Future<EpicSlatepackResult> wrappedFutureWithDelay() async {
+ await Future<void>.delayed(const Duration(seconds: 1));
+ return wallet.createSlatepack(
+ amount: amount,
+ recipientAddress: null,
+ // No specific recipient for manual slatepack.
+ message: onChainNoteController.text.isNotEmpty == true
+ ? onChainNoteController.text
+ : null,
+ );
+ }
+
+ // Create slatepack.
+ Exception? ex;
+ final slatepackResult = await showLoading(
+ whileFuture: wrappedFutureWithDelay(),
+ context: context,
+ message: "Building slate...",
+ delay: const Duration(seconds: 2),
+ onException: (e) => ex = e,
+ );
+
+ if (slatepackResult == null ||
+ !slatepackResult.success ||
+ slatepackResult.slatepack == null ||
+ ex != null) {
+ String error =
+ ex?.toString() ??
+ slatepackResult?.error ??
+ 'Failed to create slate';
+ if (error.startsWith("Exception:")) {
+ error = error.replaceFirst("Exception:", "").trim();
+ }
+ throw Exception(error);
+ }
+
+ // refresh asap to show the pending slate tx in history
+ unawaited(() async {
+ await Future<void>.delayed(Duration.zero);
+ await wallet.refresh();
+ }());
+
+ // Show slatepack dialog.
+ if (mounted) {
+ await showDialog<void>(
+ context: context,
+ barrierDismissible: false,
+ builder: (context) => StackDialogBase(
+ child: EpicSlatepackDialog(slatepackResult: slatepackResult),
+ ),
+ );
+
+ // Clear form after slatepack dialog is closed.
+ clearSendForm();
+ }
+ }
+ } catch (e, s) {
+ Logging.instance.e(
+ 'Failed to create Epic Cash slate on mobile',
+ error: e,
+ stackTrace: s,
+ );
+
+ if (mounted) {
+ await showDialog<void>(
+ context: context,
+ builder: (context) => StackOkDialog(
+ title: "Slate Creation Failed",
+ message: e.toString(),
+ ),
+ );
+ }
+ }
+ }
+
Future<void> _previewTransaction() async {
// wait for keyboard to disappear
FocusScope.of(context).unfocus();
@@ -1375,6 +1466,9 @@ class _SendViewState extends ConsumerState<SendView> {
final isMwcSlatepack =
coin is Mimblewimblecoin && ref.watch(pIsSlatepack(widget.walletId));
+ final isEpicSlatepack =
+ coin is Epiccash && ref.watch(pIsSlatepack(widget.walletId));
+ final isSlatepackMode = isMwcSlatepack || isEpicSlatepack;
return Background(
child: Scaffold(
@@ -1553,7 +1647,16 @@ class _SendViewState extends ConsumerState<SendView> {
const SizedBox(height: 16),
],
- if (!isMwcSlatepack)
+ // Epic Cash Transaction Method Selector.
+ if (coin is Epiccash) ...[
+ const SizedBox(
+ height: 40,
+ child: EpicTxsMethodToggle(),
+ ),
+ const SizedBox(height: 16),
+ ],
+
+ if (!isSlatepackMode)
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
@@ -1582,7 +1685,7 @@ class _SendViewState extends ConsumerState<SendView> {
// ),
],
),
- if (!isMwcSlatepack) const SizedBox(height: 8),
+ if (!isSlatepackMode) const SizedBox(height: 8),
if (isPaynymSend)
TextField(
key: const Key("sendViewPaynymAddressFieldKey"),
@@ -1591,7 +1694,7 @@ class _SendViewState extends ConsumerState<SendView> {
readOnly: true,
style: STextStyles.fieldLabel(context),
),
- if (!isPaynymSend && !isMwcSlatepack)
+ if (!isPaynymSend && !isSlatepackMode)
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
@@ -1648,7 +1751,7 @@ class _SendViewState extends ConsumerState<SendView> {
style: STextStyles.field(context),
decoration:
standardInputDecoration(
- isMwcSlatepack
+ isSlatepackMode
? "Enter ${coin.ticker} address (optional)"
: "Enter ${coin.ticker} address",
_addressFocusNode,
@@ -2559,9 +2662,11 @@ class _SendViewState extends ConsumerState<SendView> {
TextButton(
onPressed:
ref.watch(pPreviewTxButtonEnabled(coin))
- ? ref.watch(pIsSlatepack(widget.walletId))
+ ? isMwcSlatepack
? _createSlatepack
- : _previewTransaction
+ : isEpicSlatepack
+ ? _createEpicSlatepack
+ : _previewTransaction
: null,
style: ref.watch(pPreviewTxButtonEnabled(coin))
? Theme.of(context)
@@ -2571,8 +2676,8 @@ class _SendViewState extends ConsumerState<SendView> {
.extension<StackColors>()!
.getPrimaryDisabledButtonStyle(context),
child: Text(
- ref.watch(pIsSlatepack(widget.walletId))
- ? "Create slatepack"
+ isSlatepackMode
+ ? "Create slate"
: "Preview",
style: STextStyles.button(context),
),
diff --git a/lib/pages/send_view/sub_widgets/epic_slatepack_dialog.dart b/lib/pages/send_view/sub_widgets/epic_slatepack_dialog.dart
new file mode 100644
index 0000000..7bcf970
--- /dev/null
+++ b/lib/pages/send_view/sub_widgets/epic_slatepack_dialog.dart
@@ -0,0 +1,175 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+
+import '../../../models/epic_slatepack_models.dart';
+import '../../../notifications/show_flush_bar.dart';
+import '../../../themes/stack_colors.dart';
+import '../../../utilities/assets.dart';
+import '../../../utilities/clipboard_interface.dart';
+import '../../../utilities/text_styles.dart';
+import '../../../utilities/util.dart';
+import '../../../widgets/conditional_parent.dart';
+import '../../../widgets/desktop/desktop_dialog_close_button.dart';
+import '../../../widgets/desktop/primary_button.dart';
+import '../../../widgets/qr.dart';
+import '../../../widgets/rounded_container.dart';
+import '../../../widgets/rounded_white_container.dart';
+
+class EpicSlatepackDialog extends ConsumerStatefulWidget {
+ const EpicSlatepackDialog({
+ super.key,
+ required this.slatepackResult,
+ this.clipboard = const ClipboardWrapper(),
+ });
+
+ final EpicSlatepackResult slatepackResult;
+ final ClipboardInterface clipboard;
+
+ @override
+ ConsumerState<EpicSlatepackDialog> createState() => _EpicSlatepackDialogState();
+}
+
+class _EpicSlatepackDialogState extends ConsumerState<EpicSlatepackDialog> {
+ void _copySlatepack() {
+ widget.clipboard.setData(
+ ClipboardData(text: widget.slatepackResult.slatepack!),
+ );
+ showFloatingFlushBar(
+ type: FlushBarType.info,
+ message: "Slate copied to clipboard",
+ iconAsset: Assets.svg.copy,
+ context: context,
+ );
+ }
+
+ void _shareSlatepack() {
+ // TODO: Implement file sharing for desktop platforms.
+ showFloatingFlushBar(
+ type: FlushBarType.info,
+ message: "Share functionality coming soon",
+ context: context,
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return ConditionalParent(
+ condition: Util.isDesktop,
+ builder:
+ (child) => Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ // Header with title and close button.
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Padding(
+ padding: const EdgeInsets.only(left: 32),
+ child: Text(
+ "Send Slate",
+ style: STextStyles.pageTitleH2(context),
+ ),
+ ),
+ const DesktopDialogCloseButton(),
+ ],
+ ),
+ Padding(padding: const EdgeInsets.all(32), child: child),
+ ],
+ ),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ // Instructions.
+ RoundedContainer(
+ color:
+ Theme.of(context).extension<StackColors>()!.textFieldDefaultBG,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ "Next Steps:",
+ style: STextStyles.label(
+ context,
+ ).copyWith(fontWeight: FontWeight.w600),
+ ),
+ const SizedBox(height: 8),
+ Text(
+ "1. Share this slate with the recipient\n"
+ "2. Wait for them to return the response slate\n"
+ "3. Import their response to finalize the transaction",
+ style: STextStyles.w400_14(context),
+ ),
+ ],
+ ),
+ ),
+
+ const SizedBox(height: 12),
+
+ // QR Code view.
+ Center(
+ child: QR(
+ data: widget.slatepackResult.slatepack!,
+ size: 220,
+ ),
+ ),
+
+ const SizedBox(height: 12),
+
+ // Slatepack text view.
+ RoundedWhiteContainer(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ Row(
+ children: [
+ Text("Slate", style: STextStyles.itemSubtitle(context)),
+ const Spacer(),
+ GestureDetector(
+ onTap: _copySlatepack,
+ child: Row(
+ children: [
+ SvgPicture.asset(
+ Assets.svg.copy,
+ width: 10,
+ height: 10,
+ color:
+ Theme.of(
+ context,
+ ).extension<StackColors>()!.infoItemIcons,
+ ),
+ const SizedBox(width: 4),
+ Text("Copy", style: STextStyles.link2(context)),
+ ],
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 8),
+ Container(
+ constraints: const BoxConstraints(
+ maxHeight: 200,
+ minHeight: 100,
+ ),
+ child: SingleChildScrollView(
+ child: SelectableText(
+ widget.slatepackResult.slatepack!,
+ style: STextStyles.w400_14(
+ context,
+ ).copyWith(fontFamily: 'monospace'),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+
+ if (!Util.isDesktop)
+ PrimaryButton(label: "Done", onPressed: Navigator.of(context).pop),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart
index b3a3338..572292e 100644
--- a/lib/pages/wallet_view/wallet_view.dart
+++ b/lib/pages/wallet_view/wallet_view.dart
@@ -50,6 +50,7 @@ import '../../wallets/crypto_currency/crypto_currency.dart';
import '../../wallets/crypto_currency/intermediate/frost_currency.dart';
import '../../wallets/isar/providers/wallet_info_provider.dart';
import '../../wallets/wallet/impl/bitcoin_frost_wallet.dart';
+import '../../wallets/wallet/impl/epiccash_wallet.dart';
import '../../wallets/wallet/impl/firo_wallet.dart';
import '../../wallets/wallet/impl/mimblewimblecoin_wallet.dart';
import '../../wallets/wallet/impl/namecoin_wallet.dart';
@@ -90,6 +91,7 @@ import '../buy_view/buy_in_wallet_view.dart';
import '../cashfusion/cashfusion_view.dart';
import '../churning/churning_view.dart';
import '../coin_control/coin_control_view.dart';
+import '../epic_finalize_view/epic_finalize_view.dart';
import '../exchange_view/wallet_initiated_exchange_view.dart';
import '../finalize_view/finalize_view.dart';
import '../monkey/monkey_view.dart';
@@ -1028,6 +1030,21 @@ class _WalletViewState extends ConsumerState<WalletView> {
}
},
),
+ if (wallet is EpiccashWallet)
+ WalletNavigationBarItemData(
+ label: "Finalize",
+ icon: const FinalizeNavIcon(),
+ onTap: () {
+ if (mounted) {
+ unawaited(
+ Navigator.of(context).pushNamed(
+ EpicFinalizeView.routeName,
+ arguments: walletId,
+ ),
+ );
+ }
+ },
+ ),
if (ref.watch(pWalletCoin(walletId)) is FrostCurrency)
WalletNavigationBarItemData(
label: "Sign",
diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart
index 3cbb944..eb354eb 100644
--- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart
+++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart
@@ -22,6 +22,7 @@ import '../../../../models/isar/models/isar_models.dart';
import '../../../../models/keys/view_only_wallet_data.dart';
import '../../../../notifications/show_flush_bar.dart';
import '../../../../pages/receive_view/generate_receiving_uri_qr_code_view.dart';
+import '../../../../pages/receive_view/sub_widgets/epic_slatepack_import_dialog.dart';
import '../../../../pages/receive_view/sub_widgets/mwc_slatepack_import_dialog.dart';
import '../../../../providers/providers.dart';
import '../../../../providers/ui/preview_tx_button_state_provider.dart';
@@ -39,6 +40,7 @@ import '../../../../wallets/crypto_currency/crypto_currency.dart';
import '../../../../wallets/isar/providers/eth/current_token_wallet_provider.dart';
import '../../../../wallets/isar/providers/wallet_info_provider.dart';
import '../../../../wallets/wallet/impl/bitcoin_wallet.dart';
+import '../../../../wallets/wallet/impl/epiccash_wallet.dart';
import '../../../../wallets/wallet/impl/mimblewimblecoin_wallet.dart';
import '../../../../wallets/wallet/intermediate/bip39_hd_wallet.dart';
import '../../../../wallets/wallet/wallet_mixin_interfaces/bcash_interface.dart';
@@ -56,6 +58,7 @@ import '../../../../widgets/desktop/secondary_button.dart';
import '../../../../widgets/dialogs/s_dialog.dart';
import '../../../../widgets/icon_widgets/clipboard_icon.dart';
import '../../../../widgets/icon_widgets/x_icon.dart';
+import '../../../../widgets/epic_txs_method_toggle.dart';
import '../../../../widgets/mwc_txs_method_toggle.dart';
import '../../../../widgets/qr.dart';
import '../../../../widgets/rounded_white_container.dart';
@@ -87,6 +90,7 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> {
late bool supportsMweb;
late final bool showMultiType;
late final bool isMimblewimblecoin;
+ late final bool isEpiccash;
late TextEditingController _receiveSlateController;
String? _slate;
bool _slateToggleFlag = false;
@@ -169,6 +173,66 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> {
}
}
+ Future<void> _onEpicReceiveSlatePressed() async {
+ final wallet =
+ ref.read(pWallets).getWallet(walletId) as EpiccashWallet;
+
+ Exception? ex;
+ final result = await showLoading(
+ whileFuture: wallet.fullDecodeSlatepack(_receiveSlateController.text),
+ context: context,
+ message: "Decoding slatepack...",
+ rootNavigator: Util.isDesktop,
+ onException: (e) => ex = e,
+ );
+
+ if (result == null || ex != null) {
+ if (mounted) {
+ await showDialog<void>(
+ context: context,
+ useRootNavigator: true,
+ builder: (context) => StackOkDialog(
+ desktopPopRootNavigator: true,
+ title: "Slatepack receive error",
+ message: ex?.toString() ?? "Unexpected result without exception",
+ maxWidth: 400,
+ ),
+ );
+ }
+ return;
+ }
+
+ if (mounted) {
+ final response =
+ await showDialog<({String responseSlatepack, bool wasEncrypted})>(
+ context: context,
+ builder: (context) => SDialog(
+ child: SizedBox(
+ width: 700,
+ child: EpicSlatepackImportDialog(
+ walletId: widget.walletId,
+ clipboard: widget.clipboard,
+ rawSlatepack: result.raw,
+ decoded: result.result,
+ slatepackType: result.type,
+ ),
+ ),
+ ),
+ );
+
+ if (mounted && response != null) {
+ await showDialog<void>(
+ context: context,
+ barrierDismissible: false,
+ builder: (context) => EpicSlatepackResponseDialog(
+ responseSlatepack: response.responseSlatepack,
+ wasEncrypted: response.wasEncrypted,
+ ),
+ );
+ }
+ }
+ }
+
Future<void> generateNewAddress() async {
final wallet = ref.read(pWallets).getWallet(walletId);
if (wallet is MultiAddressInterface) {
@@ -351,6 +415,7 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> {
wallet.info.isMwebEnabled;
isMimblewimblecoin = wallet is MimblewimblecoinWallet;
+ isEpiccash = wallet is EpiccashWallet;
if (wallet is ViewOnlyOptionInterface && wallet.isViewOnly) {
showMultiType = false;
@@ -513,9 +578,36 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> {
),
),
),
- if (!(isMimblewimblecoin && ref.watch(pIsSlatepack(widget.walletId))))
+ if (isEpiccash)
+ Padding(
+ padding: const EdgeInsets.all(0),
+ child: Container(
+ decoration: BoxDecoration(
+ color:
+ Theme.of(
+ context,
+ ).extension<StackColors>()?.textFieldDefaultBG ??
+ Colors.white, // Fallback color
+ borderRadius: BorderRadius.circular(8),
+ border: Border.all(
+ color:
+ Theme.of(
+ context,
+ ).extension<StackColors>()?.backgroundAppBar ??
+ Colors.grey, // Fallback color
+ width: 1,
+ ),
+ ),
+ child: const SizedBox(
+ height:
+ 60, // Provide an explicit height to avoid infinite constraints
+ child: EpicTxsMethodToggle(),
+ ),
+ ),
+ ),
+ if (!((isMimblewimblecoin || isEpiccash) && ref.watch(pIsSlatepack(widget.walletId))))
const SizedBox(height: 20),
- if (!(isMimblewimblecoin && ref.watch(pIsSlatepack(widget.walletId))))
+ if (!((isMimblewimblecoin || isEpiccash) && ref.watch(pIsSlatepack(widget.walletId))))
ConditionalParent(
condition: showMultiType,
builder: (child) => Column(
@@ -686,7 +778,7 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> {
label: "Generate new address",
),
const SizedBox(height: 20),
- if (isMimblewimblecoin && ref.watch(pIsSlatepack(widget.walletId)))
+ if ((isMimblewimblecoin || isEpiccash) && ref.watch(pIsSlatepack(widget.walletId)))
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -817,14 +909,16 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> {
// TODO: create transparent button class to account for hover
// Conditional logic for 'Submit' button or QR code
- if (isMimblewimblecoin && ref.watch(pIsSlatepack(widget.walletId)))
+ if ((isMimblewimblecoin || isEpiccash) && ref.watch(pIsSlatepack(widget.walletId)))
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: PrimaryButton(
buttonHeight: ButtonHeight.l,
label: "Receive Slatepack",
enabled: _slateToggleFlag,
- onPressed: _slateToggleFlag ? _onReceiveSlatePressed : null,
+ onPressed: _slateToggleFlag
+ ? (isEpiccash ? _onEpicReceiveSlatePressed : _onReceiveSlatePressed)
+ : null,
),
)
else
diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart
index c476879..28c1fea 100644
--- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart
+++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart
@@ -17,6 +17,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
+import '../../../../models/epic_slatepack_models.dart';
import '../../../../models/isar/models/blockchain_data/address.dart';
import '../../../../models/isar/models/blockchain_data/utxo.dart';
import '../../../../models/isar/models/contact_entry.dart';
@@ -25,6 +26,7 @@ import '../../../../models/paynym/paynym_account_lite.dart';
import '../../../../models/send_view_auto_fill_data.dart';
import '../../../../pages/send_view/confirm_transaction_view.dart';
import '../../../../pages/send_view/sub_widgets/building_transaction_dialog.dart';
+import '../../../../pages/send_view/sub_widgets/epic_slatepack_dialog.dart';
import '../../../../pages/send_view/sub_widgets/mwc_slatepack_dialog.dart';
import '../../../../pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart';
import '../../../../providers/providers.dart';
@@ -51,6 +53,7 @@ import '../../../../wallets/crypto_currency/crypto_currency.dart';
import '../../../../wallets/crypto_currency/intermediate/nano_currency.dart';
import '../../../../wallets/isar/providers/wallet_info_provider.dart';
import '../../../../wallets/models/tx_data.dart';
+import '../../../../wallets/wallet/impl/epiccash_wallet.dart';
import '../../../../wallets/wallet/impl/firo_wallet.dart';
import '../../../../wallets/wallet/impl/mimblewimblecoin_wallet.dart';
import '../../../../wallets/wallet/wallet_mixin_interfaces/coin_control_interface.dart';
@@ -69,6 +72,7 @@ import '../../../../widgets/icon_widgets/addressbook_icon.dart';
import '../../../../widgets/icon_widgets/clipboard_icon.dart';
import '../../../../widgets/icon_widgets/qrcode_icon.dart';
import '../../../../widgets/icon_widgets/x_icon.dart';
+import '../../../../widgets/epic_txs_method_toggle.dart';
import '../../../../widgets/mwc_txs_method_toggle.dart';
import '../../../../widgets/rounded_container.dart';
import '../../../../widgets/stack_text_field.dart';
@@ -118,6 +122,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
late final bool isStellar;
late final bool isMimblewimblecoin;
+ late final bool isEpiccash;
String? _note;
String? _onChainNote;
@@ -292,6 +297,131 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
}
}
+ /// Handle Epic Cash slate creation for desktop.
+ Future<void> _handleDesktopEpicSlatepackCreation(
+ EpiccashWallet wallet,
+ ) async {
+ try {
+ final amount = ref.read(pSendAmount)!;
+
+ Future<EpicSlatepackResult> wrappedFutureWithDelay() async {
+ await Future<void>.delayed(const Duration(seconds: 1));
+ return wallet.createSlatepack(
+ amount: amount,
+ recipientAddress: null, // No specific recipient for manual slatepack.
+ message: _onChainNote?.isNotEmpty == true ? _onChainNote : null,
+ );
+ }
+
+ // Create slatepack.
+ Exception? ex;
+ final slatepackResult = await showLoading(
+ whileFuture: wrappedFutureWithDelay(),
+ context: context,
+ rootNavigator: true,
+ message: "Building slate...",
+ delay: const Duration(seconds: 2),
+ onException: (e) => ex = e,
+ );
+
+ if (slatepackResult == null ||
+ !slatepackResult.success ||
+ slatepackResult.slatepack == null ||
+ ex != null) {
+ String error =
+ ex?.toString() ??
+ slatepackResult?.error ??
+ 'Failed to create slate';
+ if (error.startsWith("Exception:")) {
+ error = error.replaceFirst("Exception:", "").trim();
+ }
+ throw Exception(error);
+ }
+
+ // refresh asap to show the pending slate tx in history
+ unawaited(() async {
+ await Future<void>.delayed(Duration.zero);
+ await wallet.refresh();
+ }());
+
+ // Show slatepack dialog.
+ if (mounted) {
+ await showDialog<void>(
+ context: context,
+ barrierDismissible: false,
+ builder: (context) => DesktopDialog(
+ maxHeight: double.infinity,
+ maxWidth: 700,
+ child: EpicSlatepackDialog(slatepackResult: slatepackResult),
+ ),
+ );
+
+ // Clear form after slatepack dialog is closed.
+ clearSendForm();
+ }
+ } catch (e, s) {
+ Logging.instance.e(
+ 'Failed to create Epic Cash slate on desktop',
+ error: e,
+ stackTrace: s,
+ );
+
+ if (mounted) {
+ await showDialog<void>(
+ context: context,
+ builder: (context) => DesktopDialog(
+ maxWidth: 450,
+ maxHeight: double.infinity,
+ child: Padding(
+ padding: const EdgeInsets.only(left: 32, bottom: 32),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(
+ 'Slate Creation Failed',
+ style: STextStyles.desktopH3(context),
+ ),
+ const DesktopDialogCloseButton(),
+ ],
+ ),
+ const SizedBox(height: 12),
+ Padding(
+ padding: const EdgeInsets.only(right: 32),
+ child: Text(
+ 'Failed to create slate: $e',
+ textAlign: TextAlign.left,
+ style: STextStyles.desktopTextExtraExtraSmall(
+ context,
+ ).copyWith(fontSize: 18),
+ ),
+ ),
+ const SizedBox(height: 40),
+ Padding(
+ padding: const EdgeInsets.only(right: 32),
+ child: Row(
+ children: [
+ Expanded(
+ child: PrimaryButton(
+ buttonHeight: ButtonHeight.l,
+ label: 'OK',
+ onPressed: () => Navigator.of(context).pop(),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+ }
+ }
+
Future<void> previewSend() async {
final wallet = ref.read(pWallets).getWallet(walletId);
@@ -301,6 +431,12 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
return;
}
+ // Handle Epic Cash slatepack transactions directly.
+ if (isEpiccash && ref.read(pIsSlatepack(widget.walletId))) {
+ await _handleDesktopEpicSlatepackCreation(wallet as EpiccashWallet);
+ return;
+ }
+
final Amount amount = ref.read(pSendAmount)!;
final Amount availableBalance;
if (coin is Firo || ref.read(pWalletInfo(walletId)).isMwebEnabled) {
@@ -1078,6 +1214,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
isStellar = coin is Stellar;
isMimblewimblecoin = coin is Mimblewimblecoin;
+ isEpiccash = coin is Epiccash;
sendToController = TextEditingController();
cryptoAmountController = TextEditingController();
@@ -1249,6 +1386,34 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
),
),
+ if (isEpiccash)
+ Padding(
+ padding: const EdgeInsets.only(bottom: 16),
+ child: Container(
+ decoration: BoxDecoration(
+ color:
+ Theme.of(
+ context,
+ ).extension<StackColors>()?.textFieldDefaultBG ??
+ Colors.white, // Fallback color
+ borderRadius: BorderRadius.circular(8),
+ border: Border.all(
+ color:
+ Theme.of(
+ context,
+ ).extension<StackColors>()?.backgroundAppBar ??
+ Colors.grey, // Fallback color
+ width: 1,
+ ),
+ ),
+ child: const SizedBox(
+ height:
+ 60, // Provide an explicit height to avoid infinite constraints
+ child: EpicTxsMethodToggle(),
+ ),
+ ),
+ ),
+
if (coin is Firo)
Text(
"Send from",
@@ -1540,7 +1705,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
),
const SizedBox(height: 20),
if (!isPaynymSend &&
- !(isMimblewimblecoin && ref.watch(pIsSlatepack(widget.walletId))))
+ !((isMimblewimblecoin || isEpiccash) && ref.watch(pIsSlatepack(widget.walletId))))
Text(
"Send to",
style: STextStyles.desktopTextExtraSmall(context).copyWith(
@@ -1551,10 +1716,10 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
textAlign: TextAlign.left,
),
if (!isPaynymSend &&
- !(isMimblewimblecoin && ref.watch(pIsSlatepack(widget.walletId))))
+ !((isMimblewimblecoin || isEpiccash) && ref.watch(pIsSlatepack(widget.walletId))))
const SizedBox(height: 10),
if (!isPaynymSend &&
- !(isMimblewimblecoin && ref.watch(pIsSlatepack(widget.walletId))))
+ !((isMimblewimblecoin || isEpiccash) && ref.watch(pIsSlatepack(widget.walletId))))
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
@@ -1733,7 +1898,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
),
),
if (!isPaynymSend &&
- !(isMimblewimblecoin && ref.watch(pIsSlatepack(widget.walletId))))
+ !((isMimblewimblecoin || isEpiccash) && ref.watch(pIsSlatepack(widget.walletId))))
Builder(
builder: (_) {
final String? error;
@@ -1752,9 +1917,9 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
} else {
if (_data != null && _data.contactLabel == _address) {
error = null;
- } else if (coin is Mimblewimblecoin &&
+ } else if ((coin is Mimblewimblecoin || coin is Epiccash) &&
ref.watch(pIsSlatepack(widget.walletId))) {
- // For MWC slatepack transactions, address validation is not required.
+ // For MWC/Epic slatepack transactions, address validation is not required.
// TODO: When implementing encrypted slatepacks, address validation will be required.
error = null;
} else if (!ref.watch(pValidSendToAddress)) {
diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart
index 1c490be..7be1146 100644
--- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart
+++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart
@@ -12,6 +12,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../frost_route_generator.dart';
+import '../../../../pages/epic_finalize_view/epic_finalize_view.dart';
import '../../../../pages/finalize_view/finalize_view.dart';
import '../../../../pages/send_view/frost_ms/frost_send_view.dart';
import '../../../../pages/wallet_view/transaction_views/tx_v2/transaction_v2_list.dart';
@@ -49,6 +50,7 @@ class _MyWalletState extends ConsumerState<MyWallet> {
late final CryptoCurrency coin;
late final bool isFrost;
late final bool isMimblewimblecoin;
+ late final bool isEpiccash;
late final bool isViewOnly;
@override
@@ -59,8 +61,9 @@ class _MyWalletState extends ConsumerState<MyWallet> {
isEth = coin is Ethereum;
isSolana = wallet is SolanaWallet;
isMimblewimblecoin = coin is Mimblewimblecoin;
+ isEpiccash = coin is Epiccash;
- if (isMimblewimblecoin) {
+ if (isMimblewimblecoin || isEpiccash) {
titles.add("Finalize");
}
@@ -186,6 +189,11 @@ class _MyWalletState extends ConsumerState<MyWallet> {
padding: const EdgeInsets.all(20),
child: FinalizeView(walletId: widget.walletId),
),
+ if (isEpiccash)
+ Padding(
+ padding: const EdgeInsets.all(20),
+ child: EpicFinalizeView(walletId: widget.walletId),
+ ),
if (isEth && widget.contractAddress == null)
Padding(
diff --git a/lib/providers/ui/preview_tx_button_state_provider.dart b/lib/providers/ui/preview_tx_button_state_provider.dart
index e800869..fcb77fe 100644
--- a/lib/providers/ui/preview_tx_button_state_provider.dart
+++ b/lib/providers/ui/preview_tx_button_state_provider.dart
@@ -11,6 +11,7 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../utilities/amount/amount.dart';
+import '../../utilities/enums/epic_transaction_method.dart';
import '../../utilities/enums/mwc_transaction_method.dart';
import '../../wallets/crypto_currency/crypto_currency.dart';
import '../../wallets/isar/providers/wallet_info_provider.dart';
@@ -27,11 +28,21 @@ final pSelectedMwcTransactionMethod = StateProvider<MwcTransactionMethod>(
(_) => MwcTransactionMethod.slatepack,
);
+// Epic Cash Transaction Method Provider.
+final pSelectedEpicTransactionMethod = StateProvider<EpicTransactionMethod>(
+ (_) => EpicTransactionMethod.epicbox,
+);
+
final pIsSlatepack = Provider.family<bool, String>((ref, walletId) {
- if (ref.watch(pWalletCoin(walletId)) is Mimblewimblecoin) {
+ final coin = ref.watch(pWalletCoin(walletId));
+ if (coin is Mimblewimblecoin) {
return ref.watch(pSelectedMwcTransactionMethod) ==
MwcTransactionMethod.slatepack;
}
+ if (coin is Epiccash) {
+ return ref.watch(pSelectedEpicTransactionMethod) ==
+ EpicTransactionMethod.slatepack;
+ }
return false;
});
@@ -48,6 +59,14 @@ final pPreviewTxButtonEnabled = Provider.autoDispose
}
}
+ // For Epic Cash slatepack transactions, address validation is not required.
+ if (coin is Epiccash) {
+ final selectedMethod = ref.watch(pSelectedEpicTransactionMethod);
+ if (selectedMethod == EpicTransactionMethod.slatepack) {
+ return amount > Amount.zero;
+ }
+ }
+
if (coin is Firo) {
final firoType = ref.watch(publicPrivateBalanceStateProvider);
switch (firoType) {
diff --git a/lib/route_generator.dart b/lib/route_generator.dart
index 35bf76b..8660430 100644
--- a/lib/route_generator.dart
+++ b/lib/route_generator.dart
@@ -72,6 +72,7 @@ import 'pages/exchange_view/exchange_step_views/step_4_view.dart';
import 'pages/exchange_view/send_from_view.dart';
import 'pages/exchange_view/trade_details_view.dart';
import 'pages/exchange_view/wallet_initiated_exchange_view.dart';
+import 'pages/epic_finalize_view/epic_finalize_view.dart';
import 'pages/finalize_view/finalize_view.dart';
import 'pages/generic/single_field_edit_view.dart';
import 'pages/home_view/home_view.dart';
@@ -1769,6 +1770,16 @@ class RouteGenerator {
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
+ case EpicFinalizeView.routeName:
+ if (args is String) {
+ return getRoute(
+ shouldUseMaterialRoute: useMaterialPageRoute,
+ builder: (_) => EpicFinalizeView(walletId: args),
+ settings: RouteSettings(name: settings.name),
+ );
+ }
+ return _routeError("${settings.name} invalid args: ${args.toString()}");
+
case WalletAddressesView.routeName:
if (args is String) {
return getRoute(
diff --git a/lib/utilities/enums/epic_transaction_method.dart b/lib/utilities/enums/epic_transaction_method.dart
new file mode 100644
index 0000000..4ef30af
--- /dev/null
+++ b/lib/utilities/enums/epic_transaction_method.dart
@@ -0,0 +1,48 @@
+/// Enum to represent different Epic Cash transaction methods.
+enum EpicTransactionMethod {
+ /// Manual slate exchange (copy/paste, QR codes, files).
+ slatepack,
+
+ /// Automatic transaction via Epicbox.
+ epicbox;
+
+ /// Human readable name for the transaction method.
+ String get displayName {
+ switch (this) {
+ case EpicTransactionMethod.slatepack:
+ return 'Slatepack';
+ case EpicTransactionMethod.epicbox:
+ return 'Epicbox';
+ }
+ }
+
+ /// Description of how the transaction method works.
+ String get description {
+ switch (this) {
+ case EpicTransactionMethod.slatepack:
+ return 'Manual exchange via text, QR codes, or files';
+ case EpicTransactionMethod.epicbox:
+ return 'Automatic exchange via Epicbox messaging';
+ }
+ }
+
+ /// Whether this method requires manual intervention.
+ bool get isManual {
+ switch (this) {
+ case EpicTransactionMethod.slatepack:
+ return true;
+ case EpicTransactionMethod.epicbox:
+ return false;
+ }
+ }
+
+ /// Whether this method works offline.
+ bool get worksOffline {
+ switch (this) {
+ case EpicTransactionMethod.slatepack:
+ return true;
+ case EpicTransactionMethod.epicbox:
+ return false;
+ }
+ }
+}
diff --git a/lib/wallets/crypto_currency/coins/epiccash.dart b/lib/wallets/crypto_currency/coins/epiccash.dart
index 84fb1b4..42d6749 100644
--- a/lib/wallets/crypto_currency/coins/epiccash.dart
+++ b/lib/wallets/crypto_currency/coins/epiccash.dart
@@ -1,7 +1,10 @@
+import 'dart:convert';
+
import '../../../models/isar/models/blockchain_data/address.dart';
import '../../../models/node_model.dart';
import '../../../utilities/default_nodes.dart';
import '../../../utilities/enums/derive_path_type_enum.dart';
+import '../../../utilities/enums/epic_transaction_method.dart';
import '../../../wl_gen/interfaces/libepiccash_interface.dart';
import '../crypto_currency.dart';
import '../intermediate/bip39_currency.dart';
@@ -128,6 +131,40 @@ class Epiccash extends Bip39Currency {
}
}
+ /// Check if data is a slate JSON.
+ bool isSlateJson(String data) {
+ try {
+ final parsed = jsonDecode(data);
+ // Check for common slate fields.
+ return parsed is Map &&
+ (parsed.containsKey('id') || parsed.containsKey('slate_id')) &&
+ (parsed.containsKey('amount') || parsed.containsKey('participant_data'));
+ } catch (e) {
+ return false;
+ }
+ }
+
+ /// Check if address is Epicbox format.
+ bool isEpicboxAddress(String address) {
+ return address.contains('@');
+ }
+
+ /// Check if address is HTTP format.
+ bool isHttpAddress(String address) {
+ return address.startsWith('http://') || address.startsWith('https://');
+ }
+
+ /// Detect transaction type based on address/data format.
+ EpicTransactionMethod getTransactionMethod(String addressOrData) {
+ if (isSlateJson(addressOrData)) {
+ return EpicTransactionMethod.slatepack;
+ } else if (isEpicboxAddress(addressOrData) || isHttpAddress(addressOrData)) {
+ return EpicTransactionMethod.epicbox;
+ } else {
+ throw Exception("Unknown EpicTransactionMethod found!");
+ }
+ }
+
@override
AddressType? getAddressType(String address) {
if (validateAddress(address)) {
diff --git a/lib/wallets/wallet/impl/epiccash_wallet.dart b/lib/wallets/wallet/impl/epiccash_wallet.dart
index 9a3e611..86ca76a 100644
--- a/lib/wallets/wallet/impl/epiccash_wallet.dart
+++ b/lib/wallets/wallet/impl/epiccash_wallet.dart
@@ -10,6 +10,7 @@ import 'package:web_socket_channel/web_socket_channel.dart';
import '../../../exceptions/wallet/node_tor_mismatch_config_exception.dart';
import '../../../models/balance.dart';
+import '../../../models/epic_slatepack_models.dart';
import '../../../models/epicbox_config_model.dart';
import '../../../models/isar/models/blockchain_data/address.dart';
import '../../../models/isar/models/blockchain_data/transaction.dart';
@@ -141,6 +142,273 @@ class EpiccashWallet extends Bip39Wallet {
);
}
+ // ================= Slatepack Operations ===================================
+
+ Future<String> _ensureWalletOpen() async {
+ final existing = await secureStorageInterface.read(
+ key: '${walletId}_wallet',
+ );
+ if (existing != null && existing.isNotEmpty) return existing;
+
+ final config = await _getRealConfig();
+ final password = await secureStorageInterface.read(
+ key: '${walletId}_password',
+ );
+ if (password == null) {
+ throw Exception('Wallet password not found');
+ }
+ final opened = await libEpic.openWallet(config: config, password: password);
+ await secureStorageInterface.write(
+ key: '${walletId}_wallet',
+ value: opened,
+ );
+ return opened;
+ }
+
+ /// Create a slatepack for sending Epic Cash.
+ Future<EpicSlatepackResult> createSlatepack({
+ required Amount amount,
+ String? recipientAddress,
+ String? message,
+ int? minimumConfirmations,
+ }) async {
+ try {
+ _hackedCheckTorNodePrefs();
+ final handle = await _ensureWalletOpen();
+ final EpicBoxConfigModel epicboxConfig = await getEpicBoxConfig();
+
+ // Create transaction with returnSlate: true for slatepack mode.
+ final result = await libEpic.createTransaction(
+ wallet: handle,
+ amount: amount.raw.toInt(),
+ address: 'slate', // Not used in slate mode.
+ secretKeyIndex: 0,
+ epicboxConfig: epicboxConfig.toString(),
+ minimumConfirmations:
+ minimumConfirmations ?? cryptoCurrency.minConfirms,
+ note: message ?? '',
+ returnSlate: true,
+ );
+
+ return EpicSlatepackResult(
+ success: true,
+ slatepack: result.slateJson,
+ slateJson: result.slateJson,
+ wasEncrypted: false,
+ recipientAddress: recipientAddress,
+ );
+ } catch (e, s) {
+ Logging.instance.e('Failed to create slatepack: $e\n$s');
+ return EpicSlatepackResult(success: false, error: e.toString());
+ }
+ }
+
+ /// Decode a slatepack/slate JSON.
+ Future<EpicSlatepackDecodeResult> decodeSlatepack(String slateJson) async {
+ try {
+ // For Epic Cash, slates are already JSON, so we parse directly.
+ // Validate that the JSON is valid.
+ jsonDecode(slateJson);
+
+ return EpicSlatepackDecodeResult(
+ success: true,
+ slateJson: slateJson,
+ wasEncrypted: false,
+ senderAddress: null,
+ recipientAddress: null,
+ );
+ } catch (e, s) {
+ Logging.instance.e('Failed to decode slatepack: $e\n$s');
+ return EpicSlatepackDecodeResult(success: false, error: e.toString());
+ }
+ }
+
+ /// Full decode of a slatepack including type analysis.
+ Future<({EpicSlatepackDecodeResult result, String type, String raw})?>
+ fullDecodeSlatepack(String slateJson) async {
+ // Add delay for showloading exception catching hack fix.
+ await Future<void>.delayed(const Duration(seconds: 1));
+
+ if (slateJson.isEmpty) {
+ return null;
+ }
+
+ // Attempt to decode.
+ final decoded = await decodeSlatepack(slateJson);
+
+ if (decoded.success) {
+ final analysis = await analyzeSlatepack(slateJson);
+
+ final String slatepackType = switch (analysis.status) {
+ 'S1' => "S1 (Initial Send)",
+ 'S2' => "S2 (Response)",
+ 'S3' => "S3 (Finalized)",
+ _ => "Unknown",
+ };
+
+ return (result: decoded, type: slatepackType, raw: slateJson);
+ } else {
+ throw Exception(decoded.error ?? "Failed to decode slatepack");
+ }
+ }
+
+ /// Receive a slatepack and return response slate JSON.
+ Future<EpicReceiveResult> receiveSlatepack(String slateJson) async {
+ try {
+ _hackedCheckTorNodePrefs();
+ final handle = await _ensureWalletOpen();
+
+ // Receive and get updated slate JSON.
+ final received = await libEpic.txReceive(
+ wallet: handle,
+ slateJson: slateJson,
+ );
+
+ return EpicReceiveResult(
+ success: true,
+ slateId: received.slateId,
+ commitId: received.commitId,
+ responseSlatepack: received.slateJson,
+ wasEncrypted: false,
+ recipientAddress: null,
+ );
+ } catch (e, s) {
+ Logging.instance.e('Failed to receive slatepack: $e\n$s');
+ return EpicReceiveResult(success: false, error: e.toString());
+ }
+ }
+
+ /// Finalize a slatepack (sender step 3).
+ Future<EpicFinalizeResult> finalizeSlatepack(String slateJson) async {
+ try {
+ _hackedCheckTorNodePrefs();
+ final handle = await _ensureWalletOpen();
+
+ // Finalize transaction.
+ final finalized = await libEpic.txFinalize(
+ wallet: handle,
+ slateJson: slateJson,
+ );
+
+ return EpicFinalizeResult(
+ success: true,
+ slateId: finalized.slateId,
+ commitId: finalized.commitId,
+ );
+ } catch (e, s) {
+ Logging.instance.e('Failed to finalize slatepack: $e\n$s');
+ return EpicFinalizeResult(success: false, error: e.toString());
+ }
+ }
+
+ /// Analyze a slatepack and determine transaction type and metadata.
+ Future<
+ ({
+ String type,
+ String status,
+ String? amount,
+ bool wasEncrypted,
+ String? senderAddress,
+ String? recipientAddress,
+ String slateId,
+ })
+ >
+ analyzeSlatepack(String slateJson) async {
+ try {
+ // Parse the slate JSON to extract metadata.
+ final slateData = jsonDecode(slateJson);
+ final String slateId = "${slateData['id'] ?? ''}";
+ final String? amountStr = slateData['amount']?.toString();
+
+ Logging.instance.d('Analyzed slatepack with ID: $slateId');
+
+ // Determine slate status from the slate structure.
+ String status = 'Unknown';
+ String type = 'Unknown';
+
+ // Check participant data to determine slate status.
+ final List<dynamic>? participants =
+ slateData['participant_data'] as List<dynamic>?;
+ if (participants != null && participants.isNotEmpty) {
+ // Count how many participants have signatures.
+ int signedParticipants = 0;
+ for (final participant in participants) {
+ if (participant['part_sig'] != null) {
+ signedParticipants++;
+ }
+ }
+
+ // Determine status based on signatures and participant count.
+ if (signedParticipants == 0) {
+ status = 'S1';
+ type = 'Outgoing'; // Initial send slate - this is outgoing.
+ } else if (signedParticipants == 1) {
+ status = 'S2';
+ type = 'Incoming'; // Response slate - this means we're receiving.
+ } else if (signedParticipants >= participants.length) {
+ status = 'S3';
+ type = 'Outgoing'; // Finalized slate - completed outgoing transaction.
+ }
+ }
+
+ // Fallback: check for explicit 'sta' field (some slates may have this).
+ if (status == 'Unknown' && slateData['sta'] != null) {
+ status = "${slateData['sta']}";
+ if (status == 'S1') {
+ type = 'Outgoing';
+ } else if (status == 'S2') {
+ type = 'Incoming';
+ } else if (status == 'S3') {
+ type = 'Outgoing';
+ }
+ }
+
+ return (
+ type: type,
+ status: status,
+ amount: amountStr,
+ wasEncrypted: false,
+ senderAddress: null,
+ recipientAddress: null,
+ slateId: slateId,
+ );
+ } catch (e) {
+ // If we can't decode it, return unknown.
+ return (
+ type: 'Unknown',
+ status: 'Unknown',
+ amount: null,
+ wasEncrypted: false,
+ senderAddress: null,
+ recipientAddress: null,
+ slateId: '',
+ );
+ }
+ }
+
+ /// Check if data is a slate JSON.
+ bool isSlateJson(String data) {
+ try {
+ final parsed = jsonDecode(data);
+ // Check for common slate fields.
+ return parsed is Map &&
+ (parsed.containsKey('id') || parsed.containsKey('slate_id')) &&
+ (parsed.containsKey('amount') || parsed.containsKey('participant_data'));
+ } catch (e) {
+ return false;
+ }
+ }
+
+ /// Check if address is Epicbox format.
+ bool isEpicboxAddress(String address) {
+ return address.contains('@');
+ }
+
+ /// Check if address is HTTP format.
+ bool isHttpAddress(String address) {
+ return address.startsWith('http://') || address.startsWith('https://');
+ }
+
// ================= Private =================================================
Future<String> _getConfig() async {
@@ -656,11 +924,11 @@ class EpiccashWallet extends Bip39Wallet {
}
}
- ({String commitId, String slateId}) transaction;
+ ({String commitId, String slateId, String slateJson}) transaction;
if (receiverAddress.startsWith("http://") ||
receiverAddress.startsWith("https://")) {
- transaction = await libEpic.txHttpSend(
+ final httpResult = await libEpic.txHttpSend(
wallet: wallet!,
selectionStrategyIsAll: 0,
minimumConfirmations: cryptoCurrency.minConfirms,
@@ -668,6 +936,11 @@ class EpiccashWallet extends Bip39Wallet {
amount: txData.recipients!.first.amount.raw.toInt(),
address: txData.recipients!.first.address,
);
+ transaction = (
+ commitId: httpResult.commitId,
+ slateId: httpResult.slateId,
+ slateJson: '',
+ );
} else {
transaction = await libEpic.createTransaction(
wallet: wallet!,
@@ -683,7 +956,10 @@ class EpiccashWallet extends Bip39Wallet {
final Map<String, String> txAddressInfo = {};
txAddressInfo['from'] = (await getCurrentReceivingAddress())!.value;
txAddressInfo['to'] = txData.recipients!.first.address;
- await _putSendToAddresses(transaction, txAddressInfo);
+ await _putSendToAddresses(
+ (commitId: transaction.commitId, slateId: transaction.slateId),
+ txAddressInfo,
+ );
return txData.copyWith(txid: transaction.slateId);
} catch (e, s) {
diff --git a/lib/widgets/epic_txs_method_toggle.dart b/lib/widgets/epic_txs_method_toggle.dart
new file mode 100644
index 0000000..f08f086
--- /dev/null
+++ b/lib/widgets/epic_txs_method_toggle.dart
@@ -0,0 +1,67 @@
+/*
+ * 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 2026-01-12
+ *
+ */
+
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+
+import '../providers/ui/preview_tx_button_state_provider.dart';
+import '../themes/stack_colors.dart';
+import '../utilities/assets.dart';
+import '../utilities/constants.dart';
+import '../utilities/enums/epic_transaction_method.dart';
+import '../utilities/util.dart';
+import 'toggle.dart';
+
+class EpicTxsMethodToggle extends ConsumerWidget {
+ const EpicTxsMethodToggle({super.key});
+
+ @override
+ Widget build(BuildContext context, WidgetRef ref) {
+ debugPrint("BUILD: $runtimeType");
+ final isDesktop = Util.isDesktop;
+
+ return Toggle(
+ onValueChanged: (value) {
+ ref.read(pSelectedEpicTransactionMethod.notifier).state =
+ value
+ ? EpicTransactionMethod.epicbox
+ : EpicTransactionMethod.slatepack;
+ },
+ isOn:
+ ref.watch(pSelectedEpicTransactionMethod) ==
+ EpicTransactionMethod.epicbox,
+ onColor:
+ isDesktop
+ ? Theme.of(
+ context,
+ ).extension<StackColors>()!.rateTypeToggleDesktopColorOn
+ : Theme.of(
+ context,
+ ).extension<StackColors>()!.rateTypeToggleColorOn,
+ offColor:
+ isDesktop
+ ? Theme.of(
+ context,
+ ).extension<StackColors>()!.rateTypeToggleDesktopColorOff
+ : Theme.of(
+ context,
+ ).extension<StackColors>()!.rateTypeToggleColorOff,
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(
+ Constants.size.circularBorderRadius,
+ ),
+ ),
+ onIcon: Assets.svg.gear,
+ onText: "Slatepack",
+ offIcon: Assets.svg.radioSyncing,
+ offText: "Automatic",
+ );
+ }
+}
diff --git a/lib/wl_gen/interfaces/libepiccash_interface.dart b/lib/wl_gen/interfaces/libepiccash_interface.dart
index 77eecbd..a063520 100644
--- a/lib/wl_gen/interfaces/libepiccash_interface.dart
+++ b/lib/wl_gen/interfaces/libepiccash_interface.dart
@@ -34,7 +34,7 @@ abstract class LibEpicCashInterface {
required String address,
});
- Future<({String commitId, String slateId})> createTransaction({
+ Future<({String commitId, String slateId, String slateJson})> createTransaction({
required String wallet,
required int amount,
required String address,
@@ -42,6 +42,17 @@ abstract class LibEpicCashInterface {
required String epicboxConfig,
required int minimumConfirmations,
required String note,
+ bool returnSlate = false,
+ });
+
+ Future<({String slateId, String commitId, String slateJson})> txReceive({
+ required String wallet,
+ required String slateJson,
+ });
+
+ Future<({String slateId, String commitId})> txFinalize({
+ required String wallet,
+ required String slateJson,
});
Future<String> cancelTransaction({
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.