trezor-monero-passphrase-modal (#3337)
What changed, and why it matters
This commit adds support for using a passphrase with Trezor hardware wallets when restoring a Monero wallet in Cake Wallet. It also fixes a small race condition when refreshing the list of connected USB devices. There is no clear security vulnerability in the diff itself; it is a feature addition with some minor hardening.
Review how the passphrase is persisted: storing a hardware-wallet passphrase in the wallet cache may reduce security if the cache is not encrypted with the same protections as the seed. Verify the new trezor-flutter git ref for any relevant security fixes. No immediate patch is required, but a security review of the passphrase storage model is advisable.
Security signals we found
Passphrase stored in wallet cache attribute 'cakewallet.passphrase' via setCacheAttribute/store
New Trezor passphrase session created via service.client.createChannel(passphrase: ...)
USB refresh race condition mitigated with an _isRefreshingUsb guard and try/catch
Dependency update for trezor-flutter and trezor_usb_transport to a new git ref
Evidence from the diff
The change introduces a passphrase flow for Trezor-based Monero wallet restoration. It stores the user-supplied passphrase as a wallet cache attribute (‘cakewallet.passphrase’), creates a new passphrase session on the Trezor client, and threads the passphrase through wallet creation credentials. It also adds a guard flag to prevent concurrent USB device refresh calls and catches exceptions during refresh. The trezor-flutter dependency is bumped to a newer git ref.
Changed components
cw_monero/lib/api/wallet.dartcw_monero/lib/api/wallet_manager.dartcw_monero/lib/monero_wallet.dartcw_monero/lib/monero_wallet_service.dartcw_monero/lib/trezor.dartlib/src/screens/connect_device/connect_device_page.dartlib/src/screens/connect_device/monero_hardware_wallet_options_page.dartlib/src/screens/connect_device/monero_hardware_wallet_passphrase_input.dartlib/view_model/wallet_hardware_restore_view_model.dartcw_monero/pubspec.yaml / trezor-flutter dependencyInspect captured patch +318 / −171
diff --git a/cw_monero/lib/api/wallet.dart b/cw_monero/lib/api/wallet.dart
index 80924bb..7879332 100644
--- a/cw_monero/lib/api/wallet.dart
+++ b/cw_monero/lib/api/wallet.dart
@@ -257,6 +257,10 @@ void setRecoveringFromSeed({required bool isRecovery}) {
final storeMutex = Mutex();
+void refresh() {
+ currentWallet!.refresh();
+}
+
int lastStorePointer = 0;
int lastStoreHeight = 0;
void storeSync({bool force = false}) async {
diff --git a/cw_monero/lib/api/wallet_manager.dart b/cw_monero/lib/api/wallet_manager.dart
index 2bdcf0b..85bc772 100644
--- a/cw_monero/lib/api/wallet_manager.dart
+++ b/cw_monero/lib/api/wallet_manager.dart
@@ -150,6 +150,11 @@ void restoreWalletFromSeedSync(
_lastOpenedWallet = path;
}
+void storePassphrase({required String path, required String passphrase}) {
+ currentWallet!.setCacheAttribute(key: "cakewallet.passphrase", value: passphrase);
+ currentWallet!.store(path: path);
+}
+
void restoreWalletFromKeys(
{required String path,
required String password,
diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart
index 16ab89a..d0e6539 100644
--- a/cw_monero/lib/monero_wallet.dart
+++ b/cw_monero/lib/monero_wallet.dart
@@ -382,22 +382,30 @@ abstract class MoneroWalletBase
}
MoneroTrezorService? trezorService;
+
+ Future<Trezor> _getTrezor() async {
+ if (trezorService == null) throw Exception("Trezor not connected");
+
+ final trezor = Trezor(trezorService!);
+ await trezor.newPassphraseSession(passphrase);
+ return trezor;
+ }
+
Future<void> syncTrezor() async {
if (trezorService == null) throw Exception("Trezor not connected");
final ptr = Pointer<Void>.fromAddress(currentWallet!.ffiAddress());
final tdis = monero.Wallet_exportTrezorTdis(ptr);
-
- final response = await Trezor(trezorService!).keyImageSync(tdis);
-
+ final trezor = await _getTrezor();
+ final response = await trezor.keyImageSync(tdis);
final success = monero.Wallet_importTrezorEncryptedKeyImagesJson(ptr, response);
if (!success) throw Exception(monero.Wallet_errorString(ptr));
}
Future<String> signTrezorTransaction(String json) async {
- if (trezorService == null) throw Exception("Trezor not connected");
- return Trezor(trezorService!).signTransaction(json);
+ final trezor = await _getTrezor();
+ return trezor.signTransaction(json);
}
@override
diff --git a/cw_monero/lib/monero_wallet_service.dart b/cw_monero/lib/monero_wallet_service.dart
index cb549a8..e774b47 100644
--- a/cw_monero/lib/monero_wallet_service.dart
+++ b/cw_monero/lib/monero_wallet_service.dart
@@ -44,10 +44,15 @@ class MoneroNewWalletCredentials extends WalletCredentials {
}
class MoneroRestoreWalletFromHardwareCredentials extends WalletCredentials {
- MoneroRestoreWalletFromHardwareCredentials(
- {required String name, required this.hardwareWalletService, int height = 0, String? password})
- : super(name: name, password: password, height: height);
- HardwareWalletService hardwareWalletService;
+ MoneroRestoreWalletFromHardwareCredentials({
+ required String name,
+ required this.hardwareWalletService,
+ int height = 0,
+ String? password,
+ String? passphrase,
+ }) : super(name: name, password: password, height: height, passphrase: passphrase);
+
+ final HardwareWalletService hardwareWalletService;
}
class MoneroRestoreWalletFromSeedCredentials extends WalletCredentials {
@@ -306,7 +311,13 @@ class MoneroWalletService extends WalletService<
deviceName: 'Ledger',
);
} else if (credentials.hardwareWalletService case MoneroTrezorService service) {
- final watchCredentials = await Trezor(service).getWatchCredentials();
+ final trezor = Trezor(service);
+
+ if (credentials.passphrase != null) {
+ await trezor.newPassphraseSession(credentials.passphrase);
+ }
+
+ final watchCredentials = await trezor.getWatchCredentials();
monero_wallet_manager.restoreWalletFromKeys(
path: path,
@@ -317,6 +328,10 @@ class MoneroWalletService extends WalletService<
viewKey: watchCredentials.watchKey,
spendKey: "",
);
+
+ if (credentials.passphrase != null) {
+ monero_wallet_manager.storePassphrase(path: path, passphrase: credentials.passphrase!);
+ }
}
final wallet = MoneroWallet(
diff --git a/cw_monero/lib/trezor.dart b/cw_monero/lib/trezor.dart
index eff953d..a9a3879 100644
--- a/cw_monero/lib/trezor.dart
+++ b/cw_monero/lib/trezor.dart
@@ -21,6 +21,15 @@ class Trezor {
Trezor(this.service);
+ String? _sessionPassphrase;
+ Future<void> newPassphraseSession(String? passphrase) async {
+ if (passphrase == null) return;
+
+ if (_sessionPassphrase == passphrase) return;
+ _sessionPassphrase = passphrase;
+ return service.client.createChannel(passphrase: _sessionPassphrase);
+ }
+
Future<MoneroTrezorWatchCredentials> getWatchCredentials() async {
final credentials = await TrezorMonero(service.client).getWatchCredentials();
diff --git a/cw_monero/pubspec.lock b/cw_monero/pubspec.lock
index 79da369..275e7d4 100644
--- a/cw_monero/pubspec.lock
+++ b/cw_monero/pubspec.lock
@@ -921,8 +921,8 @@ packages:
dependency: "direct main"
description:
path: trezor-flutter
- ref: d1289dfa4247a93ee57342dfd249957b339915ef
- resolved-ref: d1289dfa4247a93ee57342dfd249957b339915ef
+ ref: "944080d950a62dd48a199b9736237cbe580351de"
+ resolved-ref: "944080d950a62dd48a199b9736237cbe580351de"
url: "https://github.com/cake-tech/trezor-flutter.git"
source: git
version: "0.1.0"
@@ -930,8 +930,8 @@ packages:
dependency: transitive
description:
path: trezor_usb_transport
- ref: d1289dfa4247a93ee57342dfd249957b339915ef
- resolved-ref: d1289dfa4247a93ee57342dfd249957b339915ef
+ ref: "944080d950a62dd48a199b9736237cbe580351de"
+ resolved-ref: "944080d950a62dd48a199b9736237cbe580351de"
url: "https://github.com/cake-tech/trezor-flutter.git"
source: git
version: "0.0.1"
diff --git a/cw_monero/pubspec.yaml b/cw_monero/pubspec.yaml
index c84f08b..692103e 100644
--- a/cw_monero/pubspec.yaml
+++ b/cw_monero/pubspec.yaml
@@ -33,7 +33,7 @@ dependencies:
trezor_flutter:
git:
url: https://github.com/cake-tech/trezor-flutter.git
- ref: d1289dfa4247a93ee57342dfd249957b339915ef
+ ref: 944080d950a62dd48a199b9736237cbe580351de
path: trezor-flutter
dev_dependencies:
diff --git a/lib/monero/cw_monero.dart b/lib/monero/cw_monero.dart
index 1551b95..7d2b8ec 100644
--- a/lib/monero/cw_monero.dart
+++ b/lib/monero/cw_monero.dart
@@ -236,12 +236,14 @@ class CWMonero extends Monero {
required String password,
required int height,
required HardwareWalletService hardwareWalletService,
+ required String? passphrase,
}) =>
MoneroRestoreWalletFromHardwareCredentials(
name: name,
password: password,
height: height,
hardwareWalletService: hardwareWalletService,
+ passphrase: passphrase,
);
@override
diff --git a/lib/new-ui/widgets/digit_input.dart b/lib/new-ui/widgets/digit_input.dart
index d6889db..2994a75 100644
--- a/lib/new-ui/widgets/digit_input.dart
+++ b/lib/new-ui/widgets/digit_input.dart
@@ -1,5 +1,7 @@
-import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
+import "dart:io";
+
+import "package:flutter/material.dart";
+import "package:flutter/services.dart";
class DigitInputController {
String _text = "";
@@ -19,38 +21,37 @@ class DigitInputController {
}
class DigitInputPill extends StatelessWidget {
- const DigitInputPill({super.key, this.digit, required this.highlighted});
+ const DigitInputPill({required this.highlighted, super.key, this.digit});
final String? digit;
final bool highlighted;
@override
- Widget build(BuildContext context) {
- return Container(
- width: 51,
- height: 84,
- decoration: BoxDecoration(
- color: Theme.of(context).colorScheme.surfaceContainerHigh,
- borderRadius: BorderRadius.circular(16),
- border:
- highlighted ? Border.all(color: Theme.of(context).colorScheme.primary, width: 2) : null,
- ),
- child: Center(
- child: Padding(
- padding: EdgeInsets.symmetric(vertical: 16),
- child: Text(digit ?? " ", style: TextStyle(fontSize: 40)),
+ Widget build(BuildContext context) => Container(
+ width: 51,
+ height: 84,
+ decoration: BoxDecoration(
+ color: Theme.of(context).colorScheme.surfaceContainerHigh,
+ borderRadius: BorderRadius.circular(16),
+ border: highlighted
+ ? Border.all(color: Theme.of(context).colorScheme.primary, width: 2)
+ : null,
),
- ),
- );
- }
+ child: Center(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(vertical: 16),
+ child: Text(digit ?? " ", style: const TextStyle(fontSize: 40)),
+ ),
+ ),
+ );
}
class DigitInput extends StatefulWidget {
const DigitInput({
- super.key,
required this.controller,
required this.desiredLength,
this.breakAt = 3,
+ super.key,
});
final DigitInputController controller;
@@ -74,12 +75,9 @@ class _DigitInputState extends State<DigitInput> implements TextInputClient {
if (_textInputConnection == null || !_textInputConnection!.attached) {
_textInputConnection = TextInput.attach(
this,
- const TextInputConfiguration(
- inputType: TextInputType.numberWithOptions(
- signed: false,
- decimal: false,
- ),
- inputAction: TextInputAction.none,
+ TextInputConfiguration(
+ inputType: TextInputType.number,
+ inputAction: Platform.isIOS ? TextInputAction.done : TextInputAction.none,
),
);
}
@@ -102,30 +100,28 @@ class _DigitInputState extends State<DigitInput> implements TextInputClient {
}
@override
- Widget build(BuildContext context) {
- return GestureDetector(
- onTap: _openVirtualKeyboard,
- child: SizedBox(
- height: 84,
- child: ListView.separated(
- scrollDirection: Axis.horizontal,
- shrinkWrap: true,
- physics: NeverScrollableScrollPhysics(),
- itemBuilder: (context, index) => DigitInputPill(
- digit: widget.controller.text.length > index ? widget.controller.text[index] : null,
- highlighted: widget.controller.text.length == index,
+ Widget build(BuildContext context) => GestureDetector(
+ onTap: _openVirtualKeyboard,
+ child: SizedBox(
+ height: 84,
+ child: ListView.separated(
+ scrollDirection: Axis.horizontal,
+ shrinkWrap: true,
+ physics: const NeverScrollableScrollPhysics(),
+ itemBuilder: (context, index) => DigitInputPill(
+ digit: widget.controller.text.length > index ? widget.controller.text[index] : null,
+ highlighted: widget.controller.text.length == index,
+ ),
+ separatorBuilder: (context, index) =>
+ SizedBox(width: (index + 1) % widget.breakAt == 0 ? 16 : 4),
+ itemCount: widget.desiredLength,
),
- separatorBuilder: (context, index) =>
- SizedBox(width: (index + 1) % widget.breakAt == 0 ? 16 : 4),
- itemCount: widget.desiredLength,
),
- ),
- );
- }
+ );
@override
void updateEditingValue(TextEditingValue value) {
- final RegExp numberRegExp = RegExp(r'^\d{0,6}$');
+ final RegExp numberRegExp = RegExp(r"^\d{0,6}$");
if (numberRegExp.hasMatch(value.text)) {
setState(() => widget.controller.text = value.text);
diff --git a/lib/src/screens/connect_device/connect_device_page.dart b/lib/src/screens/connect_device/connect_device_page.dart
index 21cbd02..ed1ae31 100644
--- a/lib/src/screens/connect_device/connect_device_page.dart
+++ b/lib/src/screens/connect_device/connect_device_page.dart
@@ -142,12 +142,22 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
super.dispose();
}
+ var _isRefreshingUsb = false;
Future<void> _refreshUsbDevices() async {
- final dev = await widget.hardwareWalletVM.getAllUsbDevices();
+ if (_isRefreshingUsb) {
+ return;
+ }
+ _isRefreshingUsb = true;
+ try {
+ final dev = await widget.hardwareWalletVM.getAllUsbDevices();
- if (usbDevices.length != dev.length) {
- setState(() => usbDevices = dev);
+ if (usbDevices.length != dev.length) {
+ setState(() => usbDevices = dev);
+ }
+ } catch(e) {
+ printV(e);
}
+ _isRefreshingUsb = false;
}
Future<void> _refreshBleDevices() async {
diff --git a/lib/src/screens/connect_device/monero_hardware_wallet_options_page.dart b/lib/src/screens/connect_device/monero_hardware_wallet_options_page.dart
index 7e77d43..e6286a8 100644
--- a/lib/src/screens/connect_device/monero_hardware_wallet_options_page.dart
+++ b/lib/src/screens/connect_device/monero_hardware_wallet_options_page.dart
@@ -1,19 +1,22 @@
-import 'package:cake_wallet/core/wallet_name_validator.dart';
-import 'package:cw_core/generate_name.dart';
-import 'package:cake_wallet/generated/i18n.dart';
-import 'package:cake_wallet/src/screens/base_page.dart';
-import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
-import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
-import 'package:cake_wallet/src/widgets/blockchain_height_widget.dart';
-import 'package:cake_wallet/src/widgets/primary_button.dart';
-import 'package:cake_wallet/src/widgets/scrollable_with_bottom_section.dart';
-import 'package:cake_wallet/utils/responsive_layout_util.dart';
-import 'package:cake_wallet/utils/show_pop_up.dart';
-import 'package:cake_wallet/view_model/wallet_hardware_restore_view_model.dart';
-import 'package:cw_core/wallet_type.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_mobx/flutter_mobx.dart';
-import 'package:mobx/mobx.dart';
+import "dart:async";
+
+import "package:cake_wallet/core/wallet_name_validator.dart";
+import "package:cake_wallet/generated/i18n.dart";
+import "package:cake_wallet/src/screens/base_page.dart";
+import "package:cake_wallet/src/screens/connect_device/monero_hardware_wallet_passphrase_input.dart";
+import "package:cake_wallet/src/widgets/alert_with_one_action.dart";
+import "package:cake_wallet/src/widgets/base_text_form_field.dart";
+import "package:cake_wallet/src/widgets/blockchain_height_widget.dart";
+import "package:cake_wallet/src/widgets/primary_button.dart";
+import "package:cake_wallet/src/widgets/scrollable_with_bottom_section.dart";
+import "package:cake_wallet/utils/responsive_layout_util.dart";
+import "package:cake_wallet/utils/show_pop_up.dart";
+import "package:cake_wallet/view_model/wallet_hardware_restore_view_model.dart";
+import "package:cw_core/generate_name.dart";
+import "package:cw_core/wallet_type.dart";
+import "package:flutter/material.dart";
+import "package:flutter_mobx/flutter_mobx.dart";
+import "package:mobx/mobx.dart";
class MoneroHardwareWalletOptionsPage extends BasePage {
MoneroHardwareWalletOptionsPage(this._walletHardwareRestoreVM);
@@ -42,13 +45,15 @@ class _MoneroHardwareWalletOptionsFormState extends State<_MoneroHardwareWalletO
: _formKey = GlobalKey<FormState>(),
_blockchainHeightKey = GlobalKey<BlockchainHeightState>(),
_blockHeightFocusNode = FocusNode(),
- _controller = TextEditingController();
+ _walletNameController = TextEditingController(),
+ _passphraseController = TextEditingController();
final GlobalKey<FormState> _formKey;
final GlobalKey<BlockchainHeightState> _blockchainHeightKey;
final FocusNode _blockHeightFocusNode;
final WalletHardwareRestoreViewModel _walletHardwareRestoreVM;
- final TextEditingController _controller;
+ final TextEditingController _walletNameController;
+ final TextEditingController _passphraseController;
@override
void initState() {
@@ -57,131 +62,159 @@ class _MoneroHardwareWalletOptionsFormState extends State<_MoneroHardwareWalletO
}
@override
- Widget build(BuildContext context) {
- return Padding(
- padding: EdgeInsets.only(top: 24),
- child: ScrollableWithBottomSection(
- contentPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24),
- content: Center(
- child: ConstrainedBox(
- constraints:
- BoxConstraints(maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.center,
- children: [
- Padding(
- padding: EdgeInsets.only(top: 0),
- child: Form(
- key: _formKey,
- child: Stack(
- alignment: Alignment.centerRight,
- children: [
- BaseTextFormField(
- onChanged: (value) => _walletHardwareRestoreVM.name = value,
- controller: _controller,
- textStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
- fontSize: 20.0,
- fontWeight: FontWeight.w600,
- ),
- placeholderTextStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
- fontSize: 18.0,
- fontWeight: FontWeight.w500,
- color: Theme.of(context).colorScheme.onSurfaceVariant,
- ),
- hintText: S.of(context).wallet_name,
- suffixIcon: Semantics(
- label: S.of(context).generate_name,
- child: IconButton(
- onPressed: _onGenerateName,
- icon: Container(
- padding: const EdgeInsets.all(8),
- decoration: BoxDecoration(
- borderRadius: BorderRadius.circular(6.0),
- color: Theme.of(context).colorScheme.surfaceContainerHighest,
+ Widget build(BuildContext context) => Padding(
+ padding: const EdgeInsets.only(top: 24),
+ child: ScrollableWithBottomSection(
+ contentPadding: const EdgeInsets.only(left: 24, right: 24, bottom: 24),
+ content: Center(
+ child: ConstrainedBox(
+ constraints: const BoxConstraints(
+ maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint,
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ Padding(
+ padding: EdgeInsets.zero,
+ child: Form(
+ key: _formKey,
+ child: Stack(
+ alignment: Alignment.centerRight,
+ children: [
+ BaseTextFormField(
+ onChanged: (value) => _walletHardwareRestoreVM.name = value,
+ controller: _walletNameController,
+ textStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
+ fontSize: 20,
+ fontWeight: FontWeight.w600,
),
- width: 34,
- height: 34,
- child: Image.asset(
- 'assets/images/refresh_icon.png',
- color: Theme.of(context).colorScheme.primary,
+ placeholderTextStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
+ fontSize: 18,
+ fontWeight: FontWeight.w500,
+ color: Theme.of(context).colorScheme.onSurfaceVariant,
+ ),
+ hintText: S.of(context).wallet_name,
+ suffixIcon: Semantics(
+ label: S.of(context).generate_name,
+ child: IconButton(
+ onPressed: _onGenerateName,
+ icon: Container(
+ padding: const EdgeInsets.all(8),
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(6),
+ color: Theme.of(context).colorScheme.surfaceContainerHighest,
+ ),
+ width: 34,
+ height: 34,
+ child: Image.asset(
+ "assets/images/refresh_icon.png",
+ color: Theme.of(context).colorScheme.primary,
+ ),
),
),
),
+ validator: WalletNameValidator(),
),
- validator: WalletNameValidator(),
- ),
- ],
+ ],
+ ),
),
),
- ),
- Padding(
- padding: EdgeInsets.only(top: 20),
- child: BlockchainHeightWidget(
- focusNode: _blockHeightFocusNode,
- key: _blockchainHeightKey,
- hasDatePicker: true,
- walletType: WalletType.monero,
+ Padding(
+ padding: const EdgeInsets.only(top: 20),
+ child: BlockchainHeightWidget(
+ focusNode: _blockHeightFocusNode,
+ key: _blockchainHeightKey,
+ hasDatePicker: true,
+ walletType: WalletType.monero,
+ ),
),
+ ],
+ ),
+ ),
+ ),
+ bottomSectionPadding: const EdgeInsets.all(24),
+ bottomSection: Observer(
+ builder: (_) => Column(
+ children: [
+ if (_walletHardwareRestoreVM.passphraseAvailable) ...[
+ TextButton(
+ onPressed: () {
+ showModalBottomSheet(
+ context: context,
+ isScrollControlled: true,
+ backgroundColor: Colors.transparent,
+ builder: (context) => MoneroHardwareWalletPassphraseInputModal(
+ controller: _passphraseController,
+ ),
+ );
+ },
+ child: Text(S.of(context).add_a_passphrase),
+ ),
+ ],
+ LoadingPrimaryButton(
+ onPressed: _confirmForm,
+ text: S.of(context).seed_language_next,
+ color: Theme.of(context).colorScheme.primary,
+ textColor: Theme.of(context).colorScheme.onPrimary,
+ isDisabled: _walletHardwareRestoreVM.name.isEmpty,
),
],
),
),
),
- bottomSectionPadding: EdgeInsets.all(24),
- bottomSection: Observer(
- builder: (context) => LoadingPrimaryButton(
- onPressed: _confirmForm,
- text: S.of(context).seed_language_next,
- color: Theme.of(context).colorScheme.primary,
- textColor: Theme.of(context).colorScheme.onPrimary,
- isDisabled: _walletHardwareRestoreVM.name.isEmpty,
- ),
- ),
- ),
- );
- }
+ );
Future<void> _onGenerateName() async {
final rName = await generateName();
FocusManager.instance.primaryFocus?.unfocus();
setState(() {
- _controller.text = rName;
+ _walletNameController.text = rName;
_walletHardwareRestoreVM.name = rName;
- _controller.selection =
- TextSelection.fromPosition(TextPosition(offset: _controller.text.length));
+ _walletNameController.selection =
+ TextSelection.fromPosition(TextPosition(offset: _walletNameController.text.length));
});
}
Future<void> _confirmForm() async {
- showPopUp<void>(
- context: context,
- builder: (BuildContext context) => AlertWithOneAction(
- alertTitle: S.of(context).proceed_on_device,
- alertContent: S.of(context).proceed_on_device_description,
- buttonText: S.of(context).cancel,
- alertBarrierDismissible: false,
- buttonAction: () => Navigator.of(context).pop(),
+ unawaited(
+ showPopUp<void>(
+ context: context,
+ builder: (context) => AlertWithOneAction(
+ alertTitle: S.of(context).proceed_on_device,
+ alertContent: S.of(context).proceed_on_device_description,
+ buttonText: S.of(context).cancel,
+ alertBarrierDismissible: false,
+ buttonAction: () => Navigator.of(context).pop(),
+ ),
),
);
- final options = {'height': _blockchainHeightKey.currentState?.height ?? -1};
+ final options = <String, dynamic>{"height": _blockchainHeightKey.currentState?.height ?? -1};
+
+ if (_walletHardwareRestoreVM.passphraseAvailable && _passphraseController.text.isNotEmpty) {
+ options["passphrase"] = _passphraseController.text;
+ }
await _walletHardwareRestoreVM.create(options: options);
}
bool _effectsInstalled = false;
void _setEffects(BuildContext context) {
- if (_effectsInstalled) return;
+ if (_effectsInstalled) {
+ return;
+ }
- reaction((_) => _walletHardwareRestoreVM.error, (String? error) {
+ reaction((_) => _walletHardwareRestoreVM.error, (error) {
if (error != null) {
- if (error == S.current.ledger_connection_error) Navigator.of(context).pop();
+ if (error == S.current.ledger_connection_error) {
+ Navigator.of(context).pop();
+ }
WidgetsBinding.instance.addPostFrameCallback((_) {
showPopUp<void>(
context: context,
- builder: (BuildContext context) => AlertWithOneAction(
+ builder: (context) => AlertWithOneAction(
alertTitle: S.of(context).error,
alertContent: error,
buttonText: S.of(context).ok,
diff --git a/lib/src/screens/connect_device/monero_hardware_wallet_passphrase_input.dart b/lib/src/screens/connect_device/monero_hardware_wallet_passphrase_input.dart
new file mode 100644
index 0000000..6dd277e
--- /dev/null
+++ b/lib/src/screens/connect_device/monero_hardware_wallet_passphrase_input.dart
@@ -0,0 +1,54 @@
+import "package:cake_wallet/generated/i18n.dart";
+import "package:cake_wallet/new-ui/widgets/new_primary_button.dart";
+import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
+import "package:cake_wallet/src/widgets/base_text_form_field.dart";
+import "package:flutter/material.dart";
+
+class MoneroHardwareWalletPassphraseInputModal extends StatelessWidget {
+ const MoneroHardwareWalletPassphraseInputModal({required this.controller, super.key});
+
+ final TextEditingController controller;
+
+ @override
+ Widget build(BuildContext context) => Padding(
+ padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
+ child: Container(
+ decoration: BoxDecoration(
+ borderRadius: const BorderRadius.vertical(top: Radius.circular(18)),
+ color: Theme.of(context).colorScheme.surface,
+ ),
+ child: SafeArea(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ ModalTopBar(
+ title: S.of(context).passphrase_entry,
+ leadingIcon: const Icon(Icons.close),
+ onLeadingPressed: Navigator.of(context).pop,
+ ),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 18),
+ child: Column(
+ spacing: 12,
+ children: [
+ BaseTextFormField(
+ controller: controller,
+ hintText: S.of(context).passphrase_raw,
+ ),
+ const SizedBox(),
+ NewPrimaryButton(
+ onPressed: Navigator.of(context).pop,
+ text: S.of(context).restore_next,
+ color: Theme.of(context).colorScheme.primary,
+ textColor: Theme.of(context).colorScheme.onPrimary,
+ ),
+ const SizedBox(),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+}
diff --git a/lib/src/screens/restore/wallet_restore_page.dart b/lib/src/screens/restore/wallet_restore_page.dart
index 2588fd3..c8f795c 100644
--- a/lib/src/screens/restore/wallet_restore_page.dart
+++ b/lib/src/screens/restore/wallet_restore_page.dart
@@ -99,7 +99,7 @@ class WalletRestorePage extends BasePage {
children: [
Observer(
builder: (context) {
- return walletRestoreViewModel.mode == WalletRestoreMode.seed
+ return walletRestoreViewModel.passphraseAvailable
? StandardCheckbox(
captionColor: Theme.of(context).colorScheme.onSecondaryContainer,
value: walletRestoreViewModel.hasPassphrase,
diff --git a/lib/view_model/wallet_hardware_restore_view_model.dart b/lib/view_model/wallet_hardware_restore_view_model.dart
index 37f6467..35de089 100644
--- a/lib/view_model/wallet_hardware_restore_view_model.dart
+++ b/lib/view_model/wallet_hardware_restore_view_model.dart
@@ -6,13 +6,13 @@ import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart';
-import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart';
import 'package:cake_wallet/view_model/seed_settings_view_model.dart';
import 'package:cake_wallet/view_model/wallet_creation_vm.dart';
import 'package:cw_core/hardware/hardware_account_data.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_credentials.dart';
+import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:mobx/mobx.dart';
@@ -43,6 +43,8 @@ abstract class WalletHardwareRestoreViewModelBase extends WalletCreationVM with
@observable
String? error = null;
+ bool get passphraseAvailable => hardwareWalletVM.hardwareWalletType == HardwareWalletType.trezor;
+
// @observable
ObservableList<HardwareAccountData> availableAccounts = ObservableList();
@@ -87,6 +89,7 @@ abstract class WalletHardwareRestoreViewModelBase extends WalletCreationVM with
hardwareWalletService: hardwareWalletVM.getHardwareWalletService(type),
password: password,
height: _options['height'] as int? ?? 0,
+ passphrase: _options['passphrase'] as String?,
);
default:
throw Exception('Unexpected type: ${type.toString()}');
diff --git a/lib/view_model/wallet_restore_view_model.dart b/lib/view_model/wallet_restore_view_model.dart
index 0bc3234..1f95571 100644
--- a/lib/view_model/wallet_restore_view_model.dart
+++ b/lib/view_model/wallet_restore_view_model.dart
@@ -108,6 +108,10 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
@observable
WalletRestoreMode mode;
+ @computed
+ bool get passphraseAvailable =>
+ mode == WalletRestoreMode.seed || hardwareWalletType == HardwareWalletType.trezor;
+
@observable
bool hasPassphrase;
diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb
index e52f356..e5b669c 100644
--- a/res/values/strings_en.arb
+++ b/res/values/strings_en.arb
@@ -14,6 +14,7 @@
"activeConnectionsPrompt": "Active connections will appear here",
"add": "Add",
"add_a_note": "Add a note",
+ "add_a_passphrase": "Add a passphrase",
"add_account": "Add Account",
"add_contact": "Add contact",
"add_contact_to_address_book": "Would you like to add this contact to your address book?",
@@ -402,6 +403,7 @@
"enter_code": "Enter code",
"enter_greater_than": "Please enter an amount greater than",
"enter_less_than": "Please enter an amount less than",
+ "enter_on_device_instead": "Enter on device instead",
"enter_recipient_address": "Please enter recipient address",
"enter_seed_phrase": "Enter your seed phrase",
"enter_totp_code": "Please enter the TOTP Code.",
@@ -745,6 +747,8 @@
"pairing_error": "Pairing Error",
"pairingInvalidEvent": "Pairing Invalid Event",
"passphrase": "Passphrase (Optional)",
+ "passphrase_raw": "Passphrase",
+ "passphrase_entry": "Passphrase entry",
"passphrase_view_keys": "Passphrase",
"passphrases_doesnt_match": "Passphrases do not match, please try again",
"password": "Password",
diff --git a/tool/configure.dart b/tool/configure.dart
index d6b76d7..1628abb 100644
--- a/tool/configure.dart
+++ b/tool/configure.dart
@@ -455,7 +455,7 @@ abstract class Monero {
HardwareWalletType? hardwareWalletType,
required int height});
WalletCredentials createMoneroRestoreWalletFromSeedCredentials({required String name, required String password, required String passphrase, required int height, required String mnemonic});
- WalletCredentials createMoneroRestoreWalletFromHardwareCredentials({required String name, required String password, required int height, required HardwareWalletService hardwareWalletService});
+ WalletCredentials createMoneroRestoreWalletFromHardwareCredentials({required String name, required String password, required int height, required HardwareWalletService hardwareWalletService, required String? passphrase});
WalletCredentials createMoneroNewWalletCredentials({required String name, required String language, required int seedType, required String? passphrase, String? password, String? mnemonic});
Map<String, String> getKeys(Object wallet);
int? getRestoreHeight(Object wallet);
Why this scored 25/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.