From 7cb1ae9c9884edcb0eafa6d67f25d0d0e75805db Mon Sep 17 00:00:00 2001 From: Mykhailo Kremniov Date: Tue, 5 Aug 2025 21:52:32 +0300 Subject: [PATCH 1/4] Trezor firmware versioning --- Cargo.lock | 6 +- Cargo.toml | 5 +- node-gui/backend/Cargo.toml | 2 + node-gui/backend/src/backend_impl.rs | 54 ++++++-- node-gui/backend/src/error.rs | 19 +++ node-gui/backend/src/messages.rs | 3 +- node-gui/src/main_window/main_menu.rs | 61 ++++---- .../main_widget/tabs/cold_wallet.rs | 11 +- .../src/main_window/main_widget/tabs/mod.rs | 28 ++-- .../main_widget/tabs/networking.rs | 11 +- .../main_window/main_widget/tabs/settings.rs | 8 +- .../main_window/main_widget/tabs/summary.rs | 8 +- .../main_widget/tabs/wallet/mod.rs | 28 +++- .../main_widget/tabs/wallet/status_bar.rs | 82 +++++++++++ node-gui/src/main_window/mod.rs | 2 - wallet/Cargo.toml | 1 + wallet/src/signer/mod.rs | 4 +- wallet/src/signer/software_signer/mod.rs | 4 +- wallet/src/signer/trezor_signer/mod.rs | 130 +++++++++++++----- wallet/src/wallet/mod.rs | 13 +- wallet/types/Cargo.toml | 1 + wallet/types/src/hw_data.rs | 38 ++++- .../src/command_handler/mod.rs | 21 ++- wallet/wallet-cli-commands/src/lib.rs | 10 +- wallet/wallet-controller/src/lib.rs | 18 +++ .../wallet-controller/src/runtime_wallet.rs | 9 ++ wallet/wallet-controller/src/types/mod.rs | 17 +++ wallet/wallet-rpc-daemon/docs/RPC.md | 7 +- wallet/wallet-rpc-lib/src/rpc/types.rs | 4 +- 29 files changed, 451 insertions(+), 154 deletions(-) create mode 100644 node-gui/src/main_window/main_widget/tabs/wallet/status_bar.rs diff --git a/Cargo.lock b/Cargo.lock index 2ad5caef89..df7727053f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4992,12 +4992,14 @@ dependencies = [ "common", "crypto", "futures", + "itertools 0.14.0", "logging", "mempool", "node-comm", "node-lib", "p2p", "rstest", + "semver", "serde", "serde_json", "serde_with", @@ -8689,7 +8691,7 @@ dependencies = [ [[package]] name = "trezor-client" version = "0.1.5" -source = "git+https://github.com/mintlayer/mintlayer-trezor-firmware?rev=198346c2f731e7ff34be03b7a16818008eeeae0d#198346c2f731e7ff34be03b7a16818008eeeae0d" +source = "git+https://github.com/mintlayer/mintlayer-trezor-firmware?rev=397155e26660993a044e78447dad893852fa44d1#397155e26660993a044e78447dad893852fa44d1" dependencies = [ "bitcoin", "byteorder", @@ -9096,6 +9098,7 @@ dependencies = [ "randomness", "rpc-description", "rstest", + "semver", "serde", "serde_json", "serial_test", @@ -9430,6 +9433,7 @@ dependencies = [ "randomness", "rpc-description", "rstest", + "semver", "serde", "serialization", "storage", diff --git a/Cargo.toml b/Cargo.toml index 5e6696adba..799a44231a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -211,6 +211,7 @@ rlimit = "0.10" rstest = "0.24" rusqlite = "0.33" schnorrkel = "0.11" +semver = "1.0" serde = "1.0" serde_json = "1.0" serde_test = "1.0" @@ -248,8 +249,8 @@ zeroize = "1.5" [workspace.dependencies.trezor-client] git = "https://github.com/mintlayer/mintlayer-trezor-firmware" -# The commit "Remove destination from MintlayerFillOrderV1; fail if the host asks to sign a FillOrder input" -rev = "198346c2f731e7ff34be03b7a16818008eeeae0d" +# The commit "Mintlayer firmware versioning" +rev = "397155e26660993a044e78447dad893852fa44d1" features = ["bitcoin", "mintlayer"] [workspace.metadata.dist.dependencies.apt] diff --git a/node-gui/backend/Cargo.toml b/node-gui/backend/Cargo.toml index 90f8a6947a..0d27d83476 100644 --- a/node-gui/backend/Cargo.toml +++ b/node-gui/backend/Cargo.toml @@ -31,6 +31,8 @@ wallet-cli-commands = { path = "../../wallet/wallet-cli-commands"} anyhow.workspace = true chrono.workspace = true futures.workspace = true +itertools.workspace = true +semver.workspace = true serde = { workspace = true, features = ["derive"] } serde_json.workspace = true serde_with.workspace = true diff --git a/node-gui/backend/src/backend_impl.rs b/node-gui/backend/src/backend_impl.rs index 00f20519b1..d5f3dc7373 100644 --- a/node-gui/backend/src/backend_impl.rs +++ b/node-gui/backend/src/backend_impl.rs @@ -15,21 +15,22 @@ use std::{collections::BTreeMap, fmt::Debug, path::PathBuf, str::FromStr, sync::Arc}; +use futures::{stream::FuturesOrdered, TryStreamExt}; +use tokio::{ + sync::mpsc::{UnboundedReceiver, UnboundedSender}, + task::JoinHandle, +}; + use common::{ address::{Address, RpcAddress}, chain::{ChainConfig, GenBlock, SignedTransaction}, primitives::{per_thousand::PerThousand, BlockHeight, Id}, }; use crypto::key::hdkd::{child_number::ChildNumber, u31::U31}; -use futures::{stream::FuturesOrdered, TryStreamExt}; use logging::log; use node_comm::rpc_client::ColdWalletClient; use node_lib::node_controller::NodeController; use serialization::hex_encoded::HexEncoded; -use tokio::{ - sync::mpsc::{UnboundedReceiver, UnboundedSender}, - task::JoinHandle, -}; use wallet::{account::transaction_list::TransactionList, wallet::Error, WalletError}; use wallet_cli_commands::{ get_repl_command, parse_input, CommandHandler, ConsoleCommand, ManageableWalletCommand, @@ -37,7 +38,7 @@ use wallet_cli_commands::{ }; use wallet_controller::{ make_cold_wallet_rpc_client, - types::{Balances, WalletCreationOptions, WalletTypeArgs}, + types::{Balances, WalletCreationOptions, WalletExtraInfo, WalletTypeArgs}, ControllerConfig, NodeInterface, UtxoState, WalletHandlesClient, }; use wallet_rpc_client::handles_client::WalletRpcHandlesClient; @@ -361,6 +362,7 @@ impl Backend { }; let encryption = EncryptionState::Disabled; + let wallet_extra_info = Self::get_wallet_extra_info(&wallet_data.controller).await?; let wallet_info = WalletInfo { wallet_id, @@ -369,6 +371,7 @@ impl Backend { accounts: accounts_info, best_block, wallet_type, + extra_info: wallet_extra_info, }; self.wallets.insert(wallet_id, wallet_data); @@ -376,6 +379,23 @@ impl Backend { Ok(wallet_info) } + async fn get_wallet_extra_info( + controller: &GuiHotColdController, + ) -> Result { + let wallet_info_from_rpc = match controller { + GuiHotColdController::Hot(wallet_rpc, _) => wallet_rpc + .wallet_info() + .await + .map_err(|err| BackendError::WalletError(err.to_string()))?, + GuiHotColdController::Cold(wallet_rpc, _) => wallet_rpc + .wallet_info() + .await + .map_err(|err| BackendError::WalletError(err.to_string()))?, + }; + + Ok(wallet_info_from_rpc.extra_info) + } + async fn create_wallet( &mut self, handles_client: N, @@ -414,10 +434,18 @@ impl Backend { overwrite_wallet_file: true, scan_blockchain: import.should_scan_blockchain(), }; - wallet_rpc + let created_wallet = wallet_rpc .create_wallet(file_path, wallet_args, options) .await .map_err(|err| BackendError::WalletError(err.to_string()))?; + match created_wallet { + wallet_controller::types::CreatedWallet::UserProvidedMnemonic + | wallet_controller::types::CreatedWallet::NewlyGeneratedMnemonic(_) => {} + #[cfg(feature = "trezor")] + wallet_controller::types::CreatedWallet::TrezorDeviceSelection(found_devices) => { + return Err(BackendError::MultipleTrezorDevicesFound(found_devices)) + } + } tokio::spawn(forward_events( wallet_events, wallet_service @@ -572,6 +600,8 @@ impl Backend { } }; + let wallet_extra_info = Self::get_wallet_extra_info(&wallet_data.controller).await?; + let wallet_info = WalletInfo { wallet_id, path: file_path, @@ -579,6 +609,7 @@ impl Backend { accounts: accounts_info, best_block, wallet_type, + extra_info: wallet_extra_info, }; self.wallets.insert(wallet_id, wallet_data); @@ -619,7 +650,7 @@ impl Backend { let node_rpc = wallet_service.node_rpc().clone(); let chain_config = wallet_service.chain_config().clone(); let wallet_rpc = WalletRpc::new(wallet_handle, node_rpc.clone(), chain_config.clone()); - wallet_rpc + let opened_wallet = wallet_rpc .open_wallet( file_path, None, @@ -629,6 +660,13 @@ impl Backend { ) .await .map_err(|err| BackendError::WalletError(err.to_string()))?; + match opened_wallet { + | wallet_controller::types::OpenedWallet::Opened => {} + #[cfg(feature = "trezor")] + wallet_controller::types::OpenedWallet::TrezorDeviceSelection(found_devices) => { + return Err(BackendError::MultipleTrezorDevicesFound(found_devices)) + } + } tokio::spawn(forward_events( wallet_events, wallet_service diff --git a/node-gui/backend/src/error.rs b/node-gui/backend/src/error.rs index 0f4d8bdca7..4d170094d1 100644 --- a/node-gui/backend/src/error.rs +++ b/node-gui/backend/src/error.rs @@ -13,6 +13,11 @@ // See the License for the specific language governing permissions and // limitations under the License. +use itertools::Itertools as _; + +#[cfg(feature = "trezor")] +use wallet::signer::trezor_signer; + use super::{account_id::AccountId, messages::WalletId}; #[derive(thiserror::Error, Debug, Clone)] @@ -47,4 +52,18 @@ pub enum BackendError { InvalidConsoleCommand(String), #[error("Empty console command")] EmptyConsoleCommand, + + #[cfg(feature = "trezor")] + #[error( + "Multiple Trezor devices found: {}.\nLeave only one of them connected and try again.", + format_multiple_trezor_devices_err(_0) + )] + MultipleTrezorDevicesFound(Vec), +} + +fn format_multiple_trezor_devices_err(devices: &[trezor_signer::FoundDevice]) -> String { + devices + .iter() + .map(|device| format!("{} (device id = {})", device.device_name, device.device_id)) + .join(", ") } diff --git a/node-gui/backend/src/messages.rs b/node-gui/backend/src/messages.rs index 76d37735ee..911939220b 100644 --- a/node-gui/backend/src/messages.rs +++ b/node-gui/backend/src/messages.rs @@ -33,7 +33,7 @@ use p2p::P2pEvent; use serialization::hex_encoded::hex_encoded_serialization; use wallet::account::transaction_list::TransactionList; use wallet_cli_commands::ConsoleCommand; -use wallet_controller::types::{Balances, WalletTypeArgs}; +use wallet_controller::types::{Balances, WalletExtraInfo, WalletTypeArgs}; use wallet_rpc_lib::types::PoolInfo; use wallet_types::wallet_type::WalletType; @@ -58,6 +58,7 @@ pub struct WalletInfo { pub accounts: BTreeMap, pub best_block: (Id, BlockHeight), pub wallet_type: WalletType, + pub extra_info: WalletExtraInfo, } #[derive(Debug, Clone, Serialize)] diff --git a/node-gui/src/main_window/main_menu.rs b/node-gui/src/main_window/main_menu.rs index bedd2849c3..72e33054e3 100644 --- a/node-gui/src/main_window/main_menu.rs +++ b/node-gui/src/main_window/main_menu.rs @@ -96,7 +96,8 @@ fn labeled_button<'a>(label: &'a str, msg: MenuMessage) -> button::Button<'a, Me } fn menu_item(label: &str, msg: MenuMessage) -> Item<'_, MenuMessage, Theme, iced::Renderer> { - Item::new(labeled_button(label, msg).width(Length::Fixed(230.0))) + // Note: if this width is smaller than the text, the menu item will drop the whole last word. + Item::new(labeled_button(label, msg).width(Length::Fixed(270.0))) } fn make_menu_file<'a>(wallet_mode: WalletMode) -> Item<'a, MenuMessage, Theme, iced::Renderer> { @@ -104,7 +105,7 @@ fn make_menu_file<'a>(wallet_mode: WalletMode) -> Item<'a, MenuMessage, Theme, i labeled_button("File", MenuMessage::NoOp), Menu::new(match wallet_mode { WalletMode::Hot => { - let menu = vec![ + let mut menu = vec![ menu_item( "Create new Software wallet", MenuMessage::CreateNewWallet { @@ -123,43 +124,31 @@ fn make_menu_file<'a>(wallet_mode: WalletMode) -> Item<'a, MenuMessage, Theme, i wallet_type: WalletType::Hot, }, ), - // TODO: enable setting when needed - // menu_item("Settings", MenuMessage::NoOp), - menu_item("Exit", MenuMessage::Exit), ]; #[cfg(feature = "trezor")] { - let mut menu = menu; - menu.insert( - 1, - menu_item( - "Create new Trezor wallet", - MenuMessage::CreateNewWallet { - wallet_type: WalletType::Trezor, - }, - ), - ); - menu.insert( - 3, - menu_item( - "Recover from Trezor wallet", - MenuMessage::RecoverWallet { - wallet_type: WalletType::Trezor, - }, - ), - ); - menu.insert( - 5, - menu_item( - "Open Trezor wallet", - MenuMessage::OpenWallet { - wallet_type: WalletType::Trezor, - }, - ), - ); - menu + menu.push(menu_item( + "(Beta) Create new Trezor wallet", + MenuMessage::CreateNewWallet { + wallet_type: WalletType::Trezor, + }, + )); + menu.push(menu_item( + "(Beta) Recover from Trezor wallet", + MenuMessage::RecoverWallet { + wallet_type: WalletType::Trezor, + }, + )); + menu.push(menu_item( + "(Beta) Open Trezor wallet", + MenuMessage::OpenWallet { + wallet_type: WalletType::Trezor, + }, + )); } - #[cfg(not(feature = "trezor"))] + // TODO: enable setting when needed + // menu.push(menu_item("Settings", MenuMessage::NoOp)); + menu.push(menu_item("Exit", MenuMessage::Exit)); menu } WalletMode::Cold => { @@ -188,7 +177,7 @@ fn make_menu_file<'a>(wallet_mode: WalletMode) -> Item<'a, MenuMessage, Theme, i ] } }) - .width(260), + .width(300), ); root diff --git a/node-gui/src/main_window/main_widget/tabs/cold_wallet.rs b/node-gui/src/main_window/main_widget/tabs/cold_wallet.rs index 4692a0ea20..6c1946042a 100644 --- a/node-gui/src/main_window/main_widget/tabs/cold_wallet.rs +++ b/node-gui/src/main_window/main_widget/tabs/cold_wallet.rs @@ -40,12 +40,11 @@ fn get_network_type_capitalized(chain_config: &ChainConfig) -> String { impl Tab for ColdWalletTab { type Message = TabsMessage; - fn title(&self) -> String { - String::from("Cold wallet summary") - } - - fn tab_label(&self) -> TabLabel { - TabLabel::IconText(iced_fonts::Bootstrap::Info.into(), self.title()) + fn tab_label(&self, _node_state: &NodeState) -> TabLabel { + TabLabel::IconText( + iced_fonts::Bootstrap::Info.into(), + String::from("Cold wallet summary"), + ) } fn content(&self, node_state: &NodeState) -> Element { diff --git a/node-gui/src/main_window/main_widget/tabs/mod.rs b/node-gui/src/main_window/main_widget/tabs/mod.rs index 6e5e014e64..0f3312df10 100644 --- a/node-gui/src/main_window/main_widget/tabs/mod.rs +++ b/node-gui/src/main_window/main_widget/tabs/mod.rs @@ -102,32 +102,32 @@ impl TabsWidget { WalletMode::Hot => tabs .push( TabIndex::Summary as usize, - self.summary_tab.tab_label(), - self.summary_tab.view(node_state), + self.summary_tab.tab_label(node_state), + self.summary_tab.content(node_state), ) .push( TabIndex::Networking as usize, - self.networking_tab.tab_label(), - self.networking_tab.view(node_state), + self.networking_tab.tab_label(node_state), + self.networking_tab.content(node_state), ), WalletMode::Cold => tabs.push( TabIndex::Summary as usize, - self.cold_wallet_tab.tab_label(), - self.cold_wallet_tab.view(node_state), + self.cold_wallet_tab.tab_label(node_state), + self.cold_wallet_tab.content(node_state), ), }; // TODO: enable settings tab when needed //.push( // TabIndex::Settings as usize, - // self.settings_tab.tab_label(), - // self.settings_tab.view(node_state), + // self.settings_tab.tab_label(node_state), + // self.settings_tab.content(node_state), //); for (idx, wallet) in self.wallets.iter().enumerate() { tabs = tabs.push( idx + TabIndex::COUNT, - wallet.tab_label(), - wallet.view(node_state), + wallet.tab_label(node_state), + wallet.content(node_state), ) } @@ -194,13 +194,7 @@ impl TabsWidget { trait Tab { type Message; - fn title(&self) -> String; - - fn tab_label(&self) -> TabLabel; - - fn view(&self, node_state: &NodeState) -> Element { - self.content(node_state) - } + fn tab_label(&self, node_state: &NodeState) -> TabLabel; fn content(&self, node_state: &NodeState) -> Element; } diff --git a/node-gui/src/main_window/main_widget/tabs/networking.rs b/node-gui/src/main_window/main_widget/tabs/networking.rs index 193cd4f40a..c9c3cde61a 100644 --- a/node-gui/src/main_window/main_widget/tabs/networking.rs +++ b/node-gui/src/main_window/main_widget/tabs/networking.rs @@ -43,12 +43,11 @@ impl NetworkingTab { impl Tab for NetworkingTab { type Message = TabsMessage; - fn title(&self) -> String { - String::from("Networking") - } - - fn tab_label(&self) -> TabLabel { - TabLabel::IconText(iced_fonts::Bootstrap::Wifi.into(), self.title()) + fn tab_label(&self, _node_state: &NodeState) -> TabLabel { + TabLabel::IconText( + iced_fonts::Bootstrap::Wifi.into(), + String::from("Networking"), + ) } fn content(&self, node_state: &NodeState) -> Element { diff --git a/node-gui/src/main_window/main_widget/tabs/settings.rs b/node-gui/src/main_window/main_widget/tabs/settings.rs index 4679e9eeb7..a90cf6707c 100644 --- a/node-gui/src/main_window/main_widget/tabs/settings.rs +++ b/node-gui/src/main_window/main_widget/tabs/settings.rs @@ -87,12 +87,8 @@ impl SettingsTab { impl Tab for SettingsTab { type Message = TabsMessage; - fn title(&self) -> String { - String::from("Settings") - } - - fn tab_label(&self) -> TabLabel { - TabLabel::IconText(iced_fonts::Bootstrap::Gear.into(), self.title()) + fn tab_label(&self, _node_state: &NodeState) -> TabLabel { + TabLabel::IconText(iced_fonts::Bootstrap::Gear.into(), String::from("Settings")) } fn content(&self, _node_state: &NodeState) -> Element { diff --git a/node-gui/src/main_window/main_widget/tabs/summary.rs b/node-gui/src/main_window/main_widget/tabs/summary.rs index 3aecfaa412..de69139a0a 100644 --- a/node-gui/src/main_window/main_widget/tabs/summary.rs +++ b/node-gui/src/main_window/main_widget/tabs/summary.rs @@ -53,12 +53,8 @@ fn get_network_type_capitalized(chain_config: &ChainConfig) -> String { impl Tab for SummaryTab { type Message = TabsMessage; - fn title(&self) -> String { - String::from("Summary") - } - - fn tab_label(&self) -> TabLabel { - TabLabel::IconText(iced_fonts::Bootstrap::Info.into(), self.title()) + fn tab_label(&self, _node_state: &NodeState) -> TabLabel { + TabLabel::IconText(iced_fonts::Bootstrap::Info.into(), String::from("Summary")) } fn content(&self, node_state: &NodeState) -> Element { diff --git a/node-gui/src/main_window/main_widget/tabs/wallet/mod.rs b/node-gui/src/main_window/main_widget/tabs/wallet/mod.rs index 524123e39d..0672e9e463 100644 --- a/node-gui/src/main_window/main_widget/tabs/wallet/mod.rs +++ b/node-gui/src/main_window/main_widget/tabs/wallet/mod.rs @@ -18,6 +18,7 @@ mod delegation; mod left_panel; mod send; mod stake; +mod status_bar; mod top_panel; mod transactions; @@ -463,12 +464,16 @@ impl WalletTab { impl Tab for WalletTab { type Message = TabsMessage; - fn title(&self) -> String { - String::from("Wallet") - } + fn tab_label(&self, node_state: &NodeState) -> TabLabel { + let text = match node_state.wallets.get(&self.wallet_id) { + Some(wallet_info) => match wallet_info.extra_info { + wallet_controller::types::WalletExtraInfo::SoftwareWallet => "Software wallet", + wallet_controller::types::WalletExtraInfo::TrezorWallet { .. } => "Trezor wallet", + }, + None => "No wallet", + }; - fn tab_label(&self) -> TabLabel { - TabLabel::IconText(iced_fonts::Bootstrap::Wallet.into(), self.title()) + TabLabel::IconText(iced_fonts::Bootstrap::Wallet.into(), text.to_owned()) } fn content(&self, node_state: &NodeState) -> Element { @@ -568,6 +573,17 @@ impl Tab for WalletTab { .on_resize(10, WalletMessage::Resized) .into(); - pane_grid.map(|msg| TabsMessage::WalletMessage(self.wallet_id, msg)) + let result = if let Some(status_bar) = status_bar::view_status_bar(&wallet_info.extra_info) + { + Element::new(column![ + pane_grid, + horizontal_rule(1), + container(status_bar).width(Length::Fill) + ]) + } else { + pane_grid + }; + + result.map(|msg| TabsMessage::WalletMessage(self.wallet_id, msg)) } } diff --git a/node-gui/src/main_window/main_widget/tabs/wallet/status_bar.rs b/node-gui/src/main_window/main_widget/tabs/wallet/status_bar.rs new file mode 100644 index 0000000000..064af223af --- /dev/null +++ b/node-gui/src/main_window/main_widget/tabs/wallet/status_bar.rs @@ -0,0 +1,82 @@ +// Copyright (c) 2023 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use iced::{ + font, + widget::{container, row, Container}, + Alignment, Element, Font, Length, Padding, Theme, +}; + +use wallet_controller::types::WalletExtraInfo; + +use super::WalletMessage; + +pub fn view_status_bar(wallet_info: &WalletExtraInfo) -> Option> { + let bold_font = Font { + weight: font::Weight::Bold, + ..Font::default() + }; + let text_size = 16; + + let row = match wallet_info { + WalletExtraInfo::SoftwareWallet => { + return None; + } + #[cfg(feature = "trezor")] + WalletExtraInfo::TrezorWallet { + device_id: _, + device_name, + firmware_version, + } => { + use iced::widget::{rich_text, span}; + + row![ + rich_text([span("Device name: ").font(bold_font), span(device_name.clone())]) + .size(text_size), + rich_text([ + span("Firmware version: ").font(bold_font), + span(firmware_version.clone()) + ]) + .size(text_size), + ] + } + }; + + let vertical_padding = 5.; + let horizontal_padding = 10.; + + let status_bar = Container::new( + row.width(Length::Fill) + .padding(Padding { + top: vertical_padding, + right: horizontal_padding, + bottom: vertical_padding, + left: horizontal_padding, + }) + .spacing(horizontal_padding) + .align_y(Alignment::Center), + ) + .style(|theme: &Theme| { + let palette = theme.extended_palette(); + + container::Style { + background: Some(palette.background.weak.color.into()), + ..container::Style::default() + } + }) + .into(); + + Some(status_bar) +} diff --git a/node-gui/src/main_window/mod.rs b/node-gui/src/main_window/mod.rs index f905c747e4..acce53b5dd 100644 --- a/node-gui/src/main_window/mod.rs +++ b/node-gui/src/main_window/mod.rs @@ -832,8 +832,6 @@ impl MainWindow { self.main_widget .view(&self.node_state) .map(MainWindowMessage::MainWidgetMessage), - // TODO: workaround for the tabview component not accounting for the tab labels height - iced::widget::Column::new().height(70), ]; let show_dialog = self.active_dialog != ActiveDialog::None; diff --git a/wallet/Cargo.toml b/wallet/Cargo.toml index 1f6caa0b9d..18cb0173ec 100644 --- a/wallet/Cargo.toml +++ b/wallet/Cargo.toml @@ -34,6 +34,7 @@ bip39 = { workspace = true, default-features = false, features = [ hex.workspace = true itertools.workspace = true parity-scale-codec.workspace = true +semver.workspace = true serde.workspace = true thiserror.workspace = true trezor-client = { workspace = true, optional = true } diff --git a/wallet/src/signer/mod.rs b/wallet/src/signer/mod.rs index 66bf418b2a..814dbc6d1c 100644 --- a/wallet/src/signer/mod.rs +++ b/wallet/src/signer/mod.rs @@ -38,7 +38,7 @@ use wallet_storage::{ WalletStorageReadLocked, WalletStorageReadUnlocked, WalletStorageWriteUnlocked, }; use wallet_types::{ - hw_data::HardwareWalletData, + hw_data::HardwareWalletFullInfo, partially_signed_transaction::{PartiallySignedTransaction, PartiallySignedTransactionError}, signature_status::SignatureStatus, AccountId, @@ -163,5 +163,5 @@ pub trait SignerProvider { id: &AccountId, ) -> WalletResult>; - fn get_hardware_wallet_data(&self) -> Option; + fn get_hardware_wallet_info(&self) -> Option; } diff --git a/wallet/src/signer/software_signer/mod.rs b/wallet/src/signer/software_signer/mod.rs index f5036784d8..37ddadfe61 100644 --- a/wallet/src/signer/software_signer/mod.rs +++ b/wallet/src/signer/software_signer/mod.rs @@ -55,7 +55,7 @@ use wallet_storage::{ WalletStorageWriteUnlocked, }; use wallet_types::{ - hw_data::HardwareWalletData, partially_signed_transaction::PartiallySignedTransaction, + hw_data::HardwareWalletFullInfo, partially_signed_transaction::PartiallySignedTransaction, seed_phrase::StoreSeedPhrase, signature_status::SignatureStatus, AccountId, }; @@ -521,7 +521,7 @@ impl SignerProvider for SoftwareSignerProvider { Account::load_from_database(chain_config, db_tx, id) } - fn get_hardware_wallet_data(&self) -> Option { + fn get_hardware_wallet_info(&self) -> Option { None } } diff --git a/wallet/src/signer/trezor_signer/mod.rs b/wallet/src/signer/trezor_signer/mod.rs index 6b65b87701..55cdad100d 100644 --- a/wallet/src/signer/trezor_signer/mod.rs +++ b/wallet/src/signer/trezor_signer/mod.rs @@ -61,6 +61,7 @@ use crypto::key::{ signature::SignatureKind, PrivateKey, SigAuxDataProvider, Signature, SignatureError, }; +use logging::log; use randomness::make_true_rng; use serialization::Encode; use trezor_client::{ @@ -82,7 +83,7 @@ use trezor_client::{ MintlayerTokenOutputValue, MintlayerTokenTotalSupply, MintlayerTokenTotalSupplyType, MintlayerUnfreezeToken, MintlayerUnmintTokens, MintlayerUtxoType, }, - Model, + Features, Model, }; use trezor_client::{ protos::{MintlayerTransferTxOutput, MintlayerUtxoTxInput}, @@ -94,7 +95,7 @@ use wallet_storage::{ }; use wallet_types::{ account_info::DEFAULT_ACCOUNT_INDEX, - hw_data::{HardwareWalletData, TrezorData}, + hw_data::{HardwareWalletData, HardwareWalletFullInfo, TrezorFullInfo}, partially_signed_transaction::{ OrderAdditionalInfo, PartiallySignedTransaction, TokenAdditionalInfo, TxAdditionalInfo, }, @@ -109,9 +110,11 @@ use crate::{ use super::{Signer, SignerError, SignerProvider, SignerResult}; +const REQUIRED_FIRMWARE_MAJOR_VERSION: u32 = 1; + #[derive(Debug, Clone, Eq, PartialEq)] pub struct FoundDevice { - pub name: String, + pub device_name: String, pub device_id: String, } @@ -147,17 +150,32 @@ pub enum TrezorError { MultisigSignatureReturned, #[error("The file being loaded is a software wallet and does not correspond to the connected hardware wallet")] HardwareWalletDifferentFile, - #[error("Public keys mismatch. Wrong device or passphrase:\nfile device id \"{file_device_id}\", connected device id \"{connected_device_id}\",\nfile label \"{file_label}\" and connected device label \"{connected_device_label}\"")] + #[error( + "Public keys mismatch - wrong device or passphrase.\n\ + Last used device id: \"{file_device_id}\", connected device id: \"{connected_device_id}\".\n\ + Last used device name: \"{file_device_name}\", connected device name: \"{connected_device_name}\".", + )] HardwareWalletDifferentMnemonicOrPassphrase { file_device_id: String, connected_device_id: String, - file_label: String, - connected_device_label: String, + file_device_name: String, + connected_device_name: String, }, #[error("The file being loaded corresponds to the connected hardware wallet, but public keys are different. Maybe a wrong passphrase was entered?")] HardwareWalletDifferentPassphrase, #[error("Missing hardware wallet data in database")] MissingHardwareWalletData, + #[error("Firmware prerelease id parse error: {0}")] + FirmwarePrereleaseIdParseError(String), + #[error("Firmware build metadata parse error: {0}")] + FirmwareBuildMetadataParseError(String), + #[error( + "Wrong firmware version, required: {required_major_version}.x.x, actual: {actual_version}" + )] + WrongFirmwareVersion { + required_major_version: u32, + actual_version: semver::Version, + }, } // Note: @@ -215,12 +233,12 @@ impl TrezorSigner { Err(trezor_client::Error::TransportSendMessage( trezor_client::transport::error::Error::Usb(_), )) => { - let (mut new_client, data, session_id) = find_trezor_device_from_db(db_tx, None)?; + let (mut new_client, info, session_id) = find_trezor_device_from_db(db_tx, None)?; check_public_keys_against_key_chain( db_tx, &mut new_client, - &data, + &info, key_chain, &self.chain_config, )?; @@ -463,7 +481,7 @@ impl TrezorSigner { fn find_trezor_device_from_db( db_tx: &impl WalletStorageReadLocked, selected_device_id: Option, -) -> SignerResult<(Trezor, TrezorData, Vec)> { +) -> SignerResult<(Trezor, TrezorFullInfo, Vec)> { if let Some(device_id) = selected_device_id { return find_trezor_device(Some(SelectedDevice { device_id })) .map_err(SignerError::TrezorError); @@ -1565,7 +1583,7 @@ fn to_trezor_output_lock(lock: &OutputTimeLock) -> trezor_client::protos::Mintla #[derive(Clone)] pub struct TrezorSignerProvider { client: Arc>, - data: TrezorData, + info: TrezorFullInfo, session_id: Vec, } @@ -1577,11 +1595,11 @@ impl std::fmt::Debug for TrezorSignerProvider { impl TrezorSignerProvider { pub fn new(selected: Option) -> Result { - let (client, data, session_id) = find_trezor_device(selected)?; + let (client, info, session_id) = find_trezor_device(selected)?; Ok(Self { client: Arc::new(Mutex::new(client)), - data, + info, session_id, }) } @@ -1591,11 +1609,11 @@ impl TrezorSignerProvider { db_tx: &impl WalletStorageReadLocked, device_id: Option, ) -> WalletResult { - let (client, data, session_id) = find_trezor_device_from_db(db_tx, device_id)?; + let (client, info, session_id) = find_trezor_device_from_db(db_tx, device_id)?; let provider = Self { client: Arc::new(Mutex::new(client)), - data, + info, session_id, }; @@ -1632,7 +1650,7 @@ fn to_trezor_chain_type(chain_config: &ChainConfig) -> MintlayerChainType { fn check_public_keys_against_key_chain( db_tx: &impl WalletStorageReadLocked, client: &mut Trezor, - trezor_data: &TrezorData, + trezor_info: &TrezorFullInfo, key_chain: &impl AccountKeyChains, chain_config: &ChainConfig, ) -> SignerResult<()> { @@ -1647,14 +1665,14 @@ fn check_public_keys_against_key_chain( HardwareWalletData::Trezor(data) => { // If the device_id is the same but public keys are different, maybe a // different passphrase was used - if data.device_id == trezor_data.device_id { + if data.device_id == trezor_info.device_id { return Err(TrezorError::HardwareWalletDifferentPassphrase.into()); } else { return Err(TrezorError::HardwareWalletDifferentMnemonicOrPassphrase { file_device_id: data.device_id, - connected_device_id: trezor_data.device_id.clone(), - file_label: data.label, - connected_device_label: trezor_data.label.clone(), + connected_device_id: trezor_info.device_id.clone(), + file_device_name: data.device_name, + connected_device_name: trezor_info.device_name.clone(), } .into()); } @@ -1708,7 +1726,7 @@ fn check_public_keys_against_db( check_public_keys_against_key_chain( db_tx, &mut provider.client.lock().expect("poisoned lock"), - &provider.data, + &provider.info, loaded_acc.key_chain(), &chain_config, ) @@ -1717,7 +1735,7 @@ fn check_public_keys_against_db( fn find_trezor_device( selected: Option, -) -> Result<(Trezor, TrezorData, Vec), TrezorError> { +) -> Result<(Trezor, TrezorFullInfo, Vec), TrezorError> { let devices = find_devices(false); ensure!(!devices.is_empty(), TrezorError::NoDeviceFound); @@ -1747,7 +1765,7 @@ fn find_trezor_device( .position(|d| d.features().is_some_and(|f| s.device_id == f.device_id())) }); - let client = if let Some(position) = found_selected_device { + let mut client = if let Some(position) = found_selected_device { devices.remove(position) } else { match devices.len() { @@ -1758,12 +1776,7 @@ fn find_trezor_device( .into_iter() .filter_map(|c| { c.features().map(|f| FoundDevice { - name: if !f.label().is_empty() { - f.label() - } else { - f.model() - } - .to_owned(), + device_name: get_trezor_device_name(f), device_id: f.device_id().to_owned(), }) }) @@ -1774,13 +1787,60 @@ fn find_trezor_device( }; let features = client.features().ok_or(TrezorError::CannotGetDeviceFeatures)?; - let data = TrezorData { - label: features.label().to_owned(), - device_id: features.device_id().to_owned(), - }; + log::debug!( + "Found Trezor device: id = {}, label = '{}', model = '{}'", + features.device_id(), + features.label(), + features.model() + ); + let session_id = features.session_id().to_vec(); + let device_name = get_trezor_device_name(features); + let device_id = features.device_id().to_owned(); + + let firmware_version = get_checked_firmware_version(&mut client)?; + let device_info = TrezorFullInfo { + device_name, + device_id, + firmware_version, + }; + + Ok((client, device_info, session_id)) +} + +fn get_trezor_device_name(features: &Features) -> String { + if !features.label().is_empty() { + features.label() + } else { + features.model() + } + .to_owned() +} + +fn get_checked_firmware_version(client: &mut Trezor) -> Result { + let firmware_info = client + .mintlayer_get_firmware_info() + .map_err(|e| TrezorError::DeviceError(e.to_string()))?; + + let version = semver::Version { + major: firmware_info.major_version.into(), + minor: firmware_info.minor_version.into(), + patch: firmware_info.patch_version.into(), + pre: semver::Prerelease::new(&firmware_info.prerelease_id) + .map_err(|err| TrezorError::FirmwarePrereleaseIdParseError(err.to_string()))?, + build: semver::BuildMetadata::new(&firmware_info.build_metadata) + .map_err(|err| TrezorError::FirmwareBuildMetadataParseError(err.to_string()))?, + }; + + ensure!( + firmware_info.major_version == REQUIRED_FIRMWARE_MAJOR_VERSION, + TrezorError::WrongFirmwareVersion { + required_major_version: REQUIRED_FIRMWARE_MAJOR_VERSION, + actual_version: version.clone(), + } + ); - Ok((client, data, session_id)) + Ok(version) } impl SignerProvider for TrezorSignerProvider { @@ -1822,8 +1882,8 @@ impl SignerProvider for TrezorSignerProvider { Account::load_from_database(chain_config, db_tx, id) } - fn get_hardware_wallet_data(&self) -> Option { - Some(HardwareWalletData::Trezor(self.data.clone())) + fn get_hardware_wallet_info(&self) -> Option { + Some(HardwareWalletFullInfo::Trezor(self.info.clone())) } } diff --git a/wallet/src/wallet/mod.rs b/wallet/src/wallet/mod.rs index 5223c7301f..6c7c7e14ba 100644 --- a/wallet/src/wallet/mod.rs +++ b/wallet/src/wallet/mod.rs @@ -78,6 +78,7 @@ use wallet_storage::{ }; use wallet_types::account_info::{StandaloneAddressDetails, StandaloneAddresses}; use wallet_types::chain_info::ChainInfo; +use wallet_types::hw_data::HardwareWalletFullInfo; use wallet_types::partially_signed_transaction::{ PartiallySignedTransaction, PartiallySignedTransactionError, PoolAdditionalInfo, TokenAdditionalInfo, TxAdditionalInfo, @@ -402,8 +403,8 @@ where Err(err) => return Err(err), }; - if let Some(data) = signer_provider.get_hardware_wallet_data() { - db_tx.set_hardware_wallet_data(data)?; + if let Some(info) = signer_provider.get_hardware_wallet_info() { + db_tx.set_hardware_wallet_data(info.into())?; } let default_account = Wallet::::create_next_unused_account( @@ -858,9 +859,9 @@ where // The device id stored in the db may not match the actual device id; // this may happen if the user has reset the device after the wallet file was created. // So we overwrite the hardware wallet data to update the id. - if let Some(data) = signer_provider.get_hardware_wallet_data() { + if let Some(info) = signer_provider.get_hardware_wallet_info() { let mut db_tx = db.transaction_rw(None)?; - db_tx.set_hardware_wallet_data(data)?; + db_tx.set_hardware_wallet_data(info.into())?; db_tx.commit()?; } @@ -980,6 +981,10 @@ where (hash_encoded(&acc_id), names) } + pub fn hardware_wallet_info(&self) -> Option { + self.signer_provider.get_hardware_wallet_info() + } + fn create_next_unused_account( next_account_index: U31, chain_config: Arc, diff --git a/wallet/types/Cargo.toml b/wallet/types/Cargo.toml index a6f7a59601..7c8fac78c5 100644 --- a/wallet/types/Cargo.toml +++ b/wallet/types/Cargo.toml @@ -22,6 +22,7 @@ bip39 = { workspace = true, default-features = false, features = ["std", "zeroiz hex.workspace = true itertools.workspace = true parity-scale-codec.workspace = true +semver.workspace = true serde.workspace = true thiserror.workspace = true zeroize.workspace = true diff --git a/wallet/types/src/hw_data.rs b/wallet/types/src/hw_data.rs index f31b9db7ec..02fc47b8fd 100644 --- a/wallet/types/src/hw_data.rs +++ b/wallet/types/src/hw_data.rs @@ -15,16 +15,52 @@ use serialization::{Decode, Encode}; +/// This is the data that will be stored in the wallet db. #[cfg(feature = "trezor")] #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] pub struct TrezorData { pub device_id: String, - pub label: String, + pub device_name: String, } +/// All the info we may want to know about a Trezor device. +#[cfg(feature = "trezor")] +#[derive(Debug, Clone)] +pub struct TrezorFullInfo { + pub device_id: String, + pub device_name: String, + pub firmware_version: semver::Version, +} + +impl From for TrezorData { + fn from(info: TrezorFullInfo) -> Self { + Self { + device_id: info.device_id, + device_name: info.device_name, + } + } +} + +/// This is the data that will be stored in the wallet db. #[derive(Debug, Clone, Encode, Decode)] pub enum HardwareWalletData { #[cfg(feature = "trezor")] #[codec(index = 0)] Trezor(TrezorData), } + +/// All the info we may want to know about a hardware wallet. +#[derive(Debug, Clone)] +pub enum HardwareWalletFullInfo { + #[cfg(feature = "trezor")] + Trezor(TrezorFullInfo), +} + +impl From for HardwareWalletData { + fn from(info: HardwareWalletFullInfo) -> Self { + match info { + #[cfg(feature = "trezor")] + HardwareWalletFullInfo::Trezor(trezor_data) => Self::Trezor(trezor_data.into()), + } + } +} diff --git a/wallet/wallet-cli-commands/src/command_handler/mod.rs b/wallet/wallet-cli-commands/src/command_handler/mod.rs index 41c18b6ab5..3a9b758cbd 100644 --- a/wallet/wallet-cli-commands/src/command_handler/mod.rs +++ b/wallet/wallet-cli-commands/src/command_handler/mod.rs @@ -36,7 +36,7 @@ use utils::{ qrcode::{QrCode, QrCodeError}, }; use wallet::version::get_version; -use wallet_controller::types::GenericTokenTransfer; +use wallet_controller::types::{GenericTokenTransfer, WalletExtraInfo}; use wallet_rpc_client::wallet_rpc_traits::{PartialOrSignedTx, WalletInterface}; use wallet_rpc_lib::types::{ Balances, ComposedTransaction, ControllerConfig, HardwareWalletType, MnemonicInfo, @@ -323,7 +323,20 @@ where match command { ColdWalletCommand::WalletInfo => { let info = self.non_empty_wallet().await?.wallet_info().await?; - let names = info + let wallet_description = match info.extra_info { + WalletExtraInfo::SoftwareWallet => "This is a software wallet".to_owned(), + WalletExtraInfo::TrezorWallet { + device_name, + device_id, + firmware_version, + } => { + format!( + "This is a Trezor wallet; device name: {}, device id: {}, firmware version: {}", + device_name, device_id, firmware_version + ) + } + }; + let account_names = info .account_names .into_iter() .enumerate() @@ -333,7 +346,9 @@ where }) .join("\n"); - Ok(ConsoleCommand::Print(format!("Wallet Accounts:\n{names}"))) + Ok(ConsoleCommand::Print(format!( + "{wallet_description}\nWallet Accounts:\n{account_names}" + ))) } ColdWalletCommand::EncryptPrivateKeys { password } => { diff --git a/wallet/wallet-cli-commands/src/lib.rs b/wallet/wallet-cli-commands/src/lib.rs index f097461db4..a909b4f749 100644 --- a/wallet/wallet-cli-commands/src/lib.rs +++ b/wallet/wallet-cli-commands/src/lib.rs @@ -68,7 +68,7 @@ pub enum CreateWalletSubCommand { #[arg(long = "passphrase")] passphrase: Option, }, - /// Create a wallet using a connected hardware wallet. Only the public keys will be kept in + /// (Beta) Create a wallet using a connected Trezor hardware wallet. Only the public keys will be kept in /// the software wallet. Cannot specify a mnemonic or passphrase here, /// the former must have been entered on the hardware during the device setup /// and the latter will have to be entered every time the device is connected to the host machine. @@ -135,7 +135,7 @@ pub enum RecoverWalletSubCommand { #[arg(long = "passphrase")] passphrase: Option, }, - /// Recover a wallet using a connected hardware wallet. Only the public keys will be kept in + /// (Beta) Recover a wallet using a connected Trezor hardware wallet. Only the public keys will be kept in /// the software wallet. Cannot specify a mnemonic or passphrase here, /// the former must have been entered on the hardware during the device setup /// and the latter will have to be entered every time the device is connected to the host machine. @@ -193,7 +193,7 @@ pub enum OpenWalletSubCommand { #[arg(long)] force_change_wallet_type: bool, }, - /// Open a wallet file that is connected to a hardware wallet. + /// (Beta) Open a wallet file that is connected to a Trezor hardware wallet. #[command()] Trezor { /// File path of the wallet file @@ -1052,7 +1052,7 @@ impl ChoiceMenu for CreateWalletDeviceSelectMenu { fn choice_list(&self) -> Vec { self.available_devices .iter() - .map(|d| format!("{} (device id: {})", d.name, d.device_id)) + .map(|d| format!("{} (device id: {})", d.device_name, d.device_id)) .collect() } @@ -1109,7 +1109,7 @@ impl ChoiceMenu for OpenWalletDeviceSelectMenu { fn choice_list(&self) -> Vec { self.available_devices .iter() - .map(|d| format!("{} (device id: {})", d.name, d.device_id)) + .map(|d| format!("{} (device id: {})", d.device_name, d.device_id)) .collect() } diff --git a/wallet/wallet-controller/src/lib.rs b/wallet/wallet-controller/src/lib.rs index a8864b5115..262c6f8b9a 100644 --- a/wallet/wallet-controller/src/lib.rs +++ b/wallet/wallet-controller/src/lib.rs @@ -108,6 +108,7 @@ pub use wallet_types::{ utxo_types::{UtxoState, UtxoStates, UtxoType, UtxoTypes}, }; use wallet_types::{ + hw_data::HardwareWalletFullInfo, partially_signed_transaction::{ make_sighash_input_commitments, PartiallySignedTransaction, PartiallySignedTransactionError, SighashInputCommitmentCreationError, TxAdditionalInfo, @@ -118,6 +119,9 @@ use wallet_types::{ Currency, }; +#[cfg(feature = "trezor")] +use crate::types::WalletExtraInfo; + // Note: the standard `Debug` macro is not smart enough and requires N to implement the `Debug` // trait even though only `N::Error` needs it. So we use `derive_more::Debug` instead. #[derive(thiserror::Error, derive_more::Debug)] @@ -544,9 +548,23 @@ where pub fn wallet_info(&self) -> WalletInfo { let (wallet_id, account_names) = self.wallet.wallet_info(); + let hw_wallet_info = self.wallet.hardware_wallet_info(); + let extra_info = match hw_wallet_info { + Some(hw_wallet_info) => match hw_wallet_info { + #[cfg(feature = "trezor")] + HardwareWalletFullInfo::Trezor(trezor_info) => WalletExtraInfo::TrezorWallet { + device_name: trezor_info.device_name, + device_id: trezor_info.device_id, + firmware_version: trezor_info.firmware_version.to_string(), + }, + }, + None => WalletExtraInfo::SoftwareWallet, + }; + WalletInfo { wallet_id, account_names, + extra_info, } } diff --git a/wallet/wallet-controller/src/runtime_wallet.rs b/wallet/wallet-controller/src/runtime_wallet.rs index 74fdd4d7d8..af060246ab 100644 --- a/wallet/wallet-controller/src/runtime_wallet.rs +++ b/wallet/wallet-controller/src/runtime_wallet.rs @@ -52,6 +52,7 @@ use wallet::{ }; use wallet_types::{ account_info::{StandaloneAddressDetails, StandaloneAddresses}, + hw_data::HardwareWalletFullInfo, partially_signed_transaction::{PartiallySignedTransaction, TxAdditionalInfo}, seed_phrase::SerializableSeedPhrase, signature_status::SignatureStatus, @@ -183,6 +184,14 @@ impl RuntimeWallet { } } + pub fn hardware_wallet_info(&self) -> Option { + match self { + RuntimeWallet::Software(w) => w.hardware_wallet_info(), + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => w.hardware_wallet_info(), + } + } + pub fn create_next_account( &mut self, name: Option, diff --git a/wallet/wallet-controller/src/types/mod.rs b/wallet/wallet-controller/src/types/mod.rs index 937b821267..53e310f84c 100644 --- a/wallet/wallet-controller/src/types/mod.rs +++ b/wallet/wallet-controller/src/types/mod.rs @@ -55,6 +55,23 @@ use crate::mnemonic; pub struct WalletInfo { pub wallet_id: H256, pub account_names: Vec>, + pub extra_info: WalletExtraInfo, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub enum WalletExtraInfo { + SoftwareWallet, + #[cfg(feature = "trezor")] + TrezorWallet { + device_name: String, + device_id: String, + // Note: semver::Version is not serializable, so we can't use it here. + firmware_version: String, + }, +} + +impl rpc_description::HasValueHint for WalletExtraInfo { + const HINT_SER: rpc_description::ValueHint = rpc_description::ValueHint::GENERIC_OBJECT; } // A struct that represents sending a particular amount of unspecified currency. diff --git a/wallet/wallet-rpc-daemon/docs/RPC.md b/wallet/wallet-rpc-daemon/docs/RPC.md index 8160bbca84..652c25258e 100644 --- a/wallet/wallet-rpc-daemon/docs/RPC.md +++ b/wallet/wallet-rpc-daemon/docs/RPC.md @@ -3217,7 +3217,7 @@ Returns: 1) { "type": "Trezor", "content": { "devices": [ { - "name": string, + "device_name": string, "device_id": string, }, .. ] }, } @@ -3265,7 +3265,7 @@ Returns: 1) { "type": "Trezor", "content": { "devices": [ { - "name": string, + "device_name": string, "device_id": string, }, .. ] }, } @@ -3308,7 +3308,7 @@ EITHER OF "content": { "available": { "type": "Trezor", "content": { "devices": [ { - "name": string, + "device_name": string, "device_id": string, }, .. ] }, } }, @@ -3347,6 +3347,7 @@ Returns: "account_names": [ EITHER OF 1) string 2) null, .. ], + "extra_info": object, } ``` diff --git a/wallet/wallet-rpc-lib/src/rpc/types.rs b/wallet/wallet-rpc-lib/src/rpc/types.rs index 391beb12d5..a4b92ea720 100644 --- a/wallet/wallet-rpc-lib/src/rpc/types.rs +++ b/wallet/wallet-rpc-lib/src/rpc/types.rs @@ -820,7 +820,7 @@ pub enum MnemonicInfo { #[cfg(feature = "trezor")] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, HasValueHint)] pub struct FoundDevice { - pub name: String, + pub device_name: String, pub device_id: String, } @@ -828,7 +828,7 @@ pub struct FoundDevice { impl From for FoundDevice { fn from(value: wallet::signer::trezor_signer::FoundDevice) -> Self { Self { - name: value.name, + device_name: value.device_name, device_id: value.device_id, } } From c7374c5a7869c15825c0c7a1ee164f081bc3f49f Mon Sep 17 00:00:00 2001 From: Mykhailo Kremniov Date: Thu, 7 Aug 2025 19:01:10 +0300 Subject: [PATCH 2/4] Minor logging improvement --- consensus/src/pos/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/consensus/src/pos/mod.rs b/consensus/src/pos/mod.rs index 1148b7402b..18c845b9cd 100644 --- a/consensus/src/pos/mod.rs +++ b/consensus/src/pos/mod.rs @@ -230,9 +230,9 @@ pub fn stake( let first_timestamp = *block_timestamp; log::debug!( - "Search for a valid block ({}..{}), pool_id: {}", - first_timestamp, - max_block_timestamp, + "Search for a valid block, start: {}, slots count: {}, pool_id: {}", + first_timestamp.into_time(), + max_block_timestamp.as_int_seconds() - first_timestamp.as_int_seconds() + 1, Address::new(chain_config, *pos_data.stake_pool_id()) .expect("Pool id to address cannot fail") ); From 3bccead011e62be41b0730f57926b2803ca19f27 Mon Sep 17 00:00:00 2001 From: Mykhailo Kremniov Date: Thu, 7 Aug 2025 15:54:58 +0300 Subject: [PATCH 3/4] Update readmes --- README.md | 19 ++- api-server/README.md | 2 +- build-tools/linux-systemd-service/README.md | 2 +- rpc/README.md | 2 +- wallet/README.md | 2 +- wallet/TREZOR_SUPPORT.md | 145 ++++++++++++++++++++ wallet/wallet-rpc-daemon/README.md | 8 +- 7 files changed, 165 insertions(+), 15 deletions(-) create mode 100644 wallet/TREZOR_SUPPORT.md diff --git a/README.md b/README.md index d474a44081..ccd04e60c9 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,6 @@ Welcome to the official Github repository for Mintlayer, an innovative, open-source blockchain project. For detailed technical insights, we recommend visiting our [documentation](https://docs.mintlayer.org/). -Please note, the code in this repository is currently under active development. Consequently, it should not be deemed production-ready. Nonetheless, you're invited to test the code in our active testnet environment. - ## Security Discovered a potential security issue? We urge you to contact us directly at security@mintlayer.org. When reporting, please encrypt your report using Ben's GPG key which can be found [here](https://www.mintlayer.org/assets/keys/ben). @@ -54,24 +52,26 @@ You can either keep running the code from source, using `cargo run --release --b ### Running software and how to control logging -The logging of mintlayer-core is configured via the `RUST_LOG` environment variable. All log messages are printed to the terminal screen; we prefer simplicity over complicated log machinery. For example, to see all logs of the `info` level and above (the default level for normal operation), you can run the node with `RUST_LOG=info cargo run --bin node-daemon -- testnet`. If you're facing an issue, it's recommended to use `RUST_LOG=debug` instead. We recommend using these commands that not only print the logs on the screen, but also write them to a file in case you face an issue. On Linux, this can be achieved using `tee` as shown below. +Normally, the logging of mintlayer-core is configured via the `RUST_LOG` environment variable and all log messages are printed to the terminal screen. For example, to see all logs of the `info` level and above (the default level for normal operation), you can run the node with `RUST_LOG=info cargo run --bin node-daemon -- testnet`. If you're facing an issue, it's recommended to use `RUST_LOG=debug` instead. We recommend using the commands that not only print the logs on the screen, but also write them to a file in case you face an issue. On Linux, this can be achieved using `tee` as shown below. If the `RUST_LOG` environment variable is not specified, the log level `info` will be used by default. +Additionally, independent of what is printed to the console, node's logs may also be automatically written to the data directory (on Linux, they will be inside `~/.mintlayer/mainnet/logs/` and `~/.mintlayer/testnet/logs/` for mainnet and testnet respectively); this is controlled by the `--log-to-file` option (e.g. `--log-to-file true`), which is accepted by both node-daemon and node-gui. By default, its value is false for node-daemon and true for node-gui. The log files are rotated based on size, so older logs will be automatically deleted eventually. Also note that the log level used in this case is always `info`, regardless of the value of `RUST_LOG`. + Here are the commands as recommended for different scenarios: #### Assuming you're using the source code -Every release has a tag and a release branch. Make sure you checkout the release you need. For example, if you need v0.5.1, you should first run the following to check out the release branch: +Every release has a tag and a release branch. Make sure you checkout the release you need. For example, if you need v1.0.0, you should first run the following to check out the release branch: ```sh -git checkout release-v0.5.1 +git checkout release-v1.0.0 ``` or to checkout the tag: ```sh -git checkout tags/v0.5.1 +git checkout tags/v1.0.0 ``` Release branches are more recommended than tags, because they get necessary security patches, if any. Tags are just markers. @@ -156,11 +156,16 @@ cargo build --bin wallet-cli --target=aarch64-unknown-linux-gnu --release The artifacts can be found in `target/aarch64-unknown-linux-gnu/release`, or a similar directory name. - ## Wallets For more information about the wallets and their usage, [visit this readme file](wallet/README.md). +## Trezor support + +Mintlayer wallets support Trezor hardware wallets, though it's still in Beta at this moment. + +For details, [visit this readme file](wallet/TREZOR_SUPPORT.md). + ## The API server The API server is a tool for indexing the blockchain. Its source code is contained in this repository and its [readme can be found in its directory](api-server/README.md). diff --git a/api-server/README.md b/api-server/README.md index a3a4e66db8..183c2a19f7 100644 --- a/api-server/README.md +++ b/api-server/README.md @@ -4,7 +4,7 @@ The Mintlayer API server is a tool that scans the blockchain and publishes its data in a database for easy access. Technically speaking, this is done to achieve the trade-off where the blockchain itself contains the minimal required amount of data, while the API server indexes all the data for easy reach. The API server is used by block explorers and web wallets. The API server is made to be usable in many ways, including in exchanges, by people interested in writing tooling for the blockchain, or anything else. -For example to understand what problem the API server solves, the node software only stores blocks of the blockchain, but it does not index the transactions by their id. Meaning: Attempting to find a transaction by its id is virtually impossible without going through all blocks. This same applies to more information that's not directly, minimally, required to operate the blockchain. In that case, using the API server solves all these problems, since the API server is made to index the information and put it in the database. +For example to understand what problem the API server solves, the node software only stores blocks of the blockchain, but it does not index the transactions by their id. Meaning: Attempting to find a transaction by its id is virtually impossible without going through all blocks. The same applies to more information that's not directly, minimally, required to operate the blockchain. In that case, using the API server solves all these problems, since the API server is made to index the information and put it in the database. ## Architecture diff --git a/build-tools/linux-systemd-service/README.md b/build-tools/linux-systemd-service/README.md index 290803dffa..7dbfd8ac51 100644 --- a/build-tools/linux-systemd-service/README.md +++ b/build-tools/linux-systemd-service/README.md @@ -7,7 +7,7 @@ Note: If you're running mintlayer in your home or behind a consumer router, thes Also, there are managed services that do this for you, but they're more expensive. We never had to use any of them, but it's up to you. One that comes to mind is Plesk. It's made to make server management easier. But it's not free. -1. NEVER run mintlayer software as root or a user that has access to root. It's preferable to create a separate user for this +1. NEVER run mintlayer software as root or a user that has access to root. It's preferable to create a separate user for this. 2. NEVER keep all your server ports open. This is a huge security flaw that can endanger both your server and make stealing your coins easy. Mintlayer needs only port 13031 (testnet p2p) or 3031 (mainnet p2p). All other ports (maybe besides ssh) should be blocked by a firewall. 3. DO NOT allow public access to RPC (port 13030 for testnet, 3030 for mainnet). RPC basically gives full control and it's meant for the owner. 4. DO NOT bind RPC to 0.0.0.0 unless you know what you're doing. The correct way to reach your RPC is with an ssh tunnel, not by opening the RPC to the public. diff --git a/rpc/README.md b/rpc/README.md index 4ee0e3b6ce..fd1ce326ee 100644 --- a/rpc/README.md +++ b/rpc/README.md @@ -51,7 +51,7 @@ for example, to get the current state of chainstate as another example, since this is websocket, you can also subscribe to events. So to do that, send the function: ``` -{"jsonrpc": "2.0", "method": "chainstate_subscribe_events", "params":[{}], "id": 1} +{"jsonrpc": "2.0", "method": "chainstate_subscribe_to_events", "params":[{}], "id": 1} ``` which will return a confirmation with a result. Then, the node will notify you for events, like new blocks becoming the chainstate tip. diff --git a/wallet/README.md b/wallet/README.md index 18faba2d36..aca249c3b0 100644 --- a/wallet/README.md +++ b/wallet/README.md @@ -8,7 +8,7 @@ We assume here that you already have a mintlayer node running as a daemon. If yo ### How are wallets stored? -Our wallets use BIP-39 for deriving a master key from seed words. It also uses BIP-32 for deriving child keys, and finally BIP-44 is used for path derivation. The path is `m/44'/'19788/'0/0/0` for mainnet and `m/44'/'1/'0/0/0` for testnet. +Our wallets use BIP-39 for deriving a master key from seed words. They also use BIP-32 for deriving child keys, and finally BIP-44 is used for path derivation. The path is `m/44'/'19788/'0/0/0` for mainnet and `m/44'/'1/'0/0/0` for testnet. Wallets load the 12- or 24-word seed (and possibly the passphrase as well), then follow the above-mentioned standards for key derivation. diff --git a/wallet/TREZOR_SUPPORT.md b/wallet/TREZOR_SUPPORT.md new file mode 100644 index 0000000000..03fe56c91f --- /dev/null +++ b/wallet/TREZOR_SUPPORT.md @@ -0,0 +1,145 @@ +## Using Mintlayer with Trezor + +In order to use a Trezor device with Mintlayer, you'll have to: +- Flash the device with custom firmware provided by Mintlayer. +- Use one of the Mintlayer Core wallets - node-gui or wallet-cli. I.e. Trezor Suite won't be able to see your ML coins. Also, at this moment Mojito wallet doesn't support Trezor either, though there are plans to add the support in the future. + +Note: Core wallets still require you to create a wallet file on your computer. In this case though only public keys will be kept in the wallet file (unless you explicitly add a standalone private key to it). Also note that you have to create a separate wallet file for each device/passphrase combination. + +### Caveats + +From the Trezor perspective, there are two types of firmware: a) the official one, signed by Trezor and perfectly safe to use; b) custom-built one, potentially unsafe to use. + +Since Mintlayer is not officially supported by Trezor, the firmware we provide falls into the latter category. This has certain implications: +- When installing the firmware, the device will show the warning "UNSAFE, DO NOT USE!". +- When installing custom firmware over the official one, **the device will be wiped clean**. Make sure you have a backup of your seed phrase. +- When installing custom firmware over different custom firmware, the device will **not** be wiped clean. This makes it succeptible to the so-called "evil maid attack", +where the attacker has physical access to your device. It goes like this: + * The "maid" temporarily steals your device and installs on it firmware that she has built herself; it looks identical to the previously installed firmware, but also logs the PIN and the passphrase entered by the user. Then she puts the device back. + * You use the device. + * She steals the device again and can now extract the logged PIN and passphrase. Moreover, knowing the PIN and being able to flash arbitrary firmware, she can now extract the seed phrase as well. So now she can either steal your coins right away or + simply put the device back and wait until you accumulate more of them. + + So: + * keep your device in a safe place; + * preferably, use a dedicated device with a dedicated seed phrase and PIN specifically for Mintlayer; +- In order to install custom firmware on a Trezor Safe family device (e.g. Safe 3 or Safe 5), you have to [unlock the bootloader first](https://trezor.io/learn/security-privacy/how-trezor-keeps-you-safe/unlocking-the-bootloader-on-trezor-safe-devices); +this is an irreversible operation after which the device authenticity check will no longer work. This means that every time you use Trezor Suite, you will be presented with a warning "Your device may have been compromised" (unless you disable +the authenticity check in the Trezor Suite's device settings). + +### Building and flashing the firmware + +#### A note about versioning + +Firmware built from the Mintlayer fork has two version numbers: +- A version number assigned by Trezor; this is the original release that we've based our release upon + and this is what is shown to you on the device screen when you flash the firmware. +- An additional version number assigned by us, to which we refer as "Mintlayer firmware version". + It is obtainable via `trezorctl mintlayer get-firmware-info` and it's what + our wallets display in their UI. + +The table of correspondence between the two versions can be found in the [firmware repository](https://github.com/mintlayer/mintlayer-trezor-firmware/blob/mintlayer-master/README.md). + +The Mintlayer firmware version determines the compatibility between the firmware and the Core wallets: + +| Mintlayer Core version | Required Mintlayer firmware version | +| --- | --- | +| 1.1.0 | 1.x.x | + +Note: if you've built Core wallets directly from `master` instead of using a specific release, +you'll probably won't be able to use a specific release for the firmware either. +Instead, you'll have to build it from `mintlayer-master`. + +#### How to build + +##### Get the source code + +Clone the repository and `cd` into it: +```sh +git clone --recurse-submodules https://github.com/mintlayer/mintlayer-trezor-firmware +cd mintlayer-trezor-firmware +``` + +Then checkout the required revision: +- If you want the latest version that is in development, checkout the `mintlayer-master` branch: + ```sh + git checkout --recurse-submodules mintlayer-master + ``` +- If you want a particular release, checkout the tag corresponding to that release. The list of tags + can be found [here](https://github.com/mintlayer/mintlayer-trezor-firmware/tags). + Assuming that you've chosen `mintlayer-v1.0.0`, run: + ```sh + git checkout --recurse-submodules mintlayer-v1.0.0 + ``` + +##### Install `Nix` + +On a Debian-based system you can do this via `sudo apt install nix-bin`. + +Check that `Nix` works by running `nix-shell -p hello --run hello` + +If you're getting the error `getting status of /nix/var/nix/daemon-socket/socket: Permission denied` +on your Linux machine, you may need to add the current user to the `nix-users` group: +```sh +sudo usermod -aG nix-users your_username +``` +You'll also need to re-login after that. + +If you're getting the error `file 'nixpkgs' was not found in the Nix search path`, add +the `nixpkgs` channel by running: +```sh +nix-channel --add https://nixos.org/channels/nixos-25.05 nixpkgs +nix-channel --update +``` + +Run `nix-shell -p hello --run hello` again. If everything is ok, it should print `Hello, world!`. + +##### Install required Python dependencies via `Poetry` + +```sh +nix-shell --run "poetry install" +``` + +##### Build the firmware + +Run: + +```sh +TREZOR_MODEL=T3T1 nix-shell --run "poetry run make -C core vendor build_firmware" +``` + +The value of the `TREZOR_MODEL` env variable determines the target device which the firmware will be built for. +The possible values are: +| TREZOR_MODEL value | Device model | +| --- | --- | +| T2T1 | Model T | +| T2B1 | Safe 3 revision A | +| T3B1 | Safe 3 revision B | +| T3T1 | Safe 5 | + +Note: +- Trezor Safe 3 revision A and B look identical. To determine the revision of your particular device, + first connect the device (which means, both connect it physically and enter the PIN) and then run: + ```sh + nix-shell --run "poetry run trezorctl get-features" + ``` + Look for the `internal_model` value in the output. +- Trezor Model One is not supported. + +##### Flash the firmware + +First you need to put your device into bootloader mode. To do so +- On Safe 3, hold the left button when connecting the USB cable. +- On Model T and Safe 5, swipe across the screen when connecting the USB cable. + +After that the device will present you with an option to install firmware, select that option. + +Now you can flash the firmware by running: +```sh +nix-shell --run "poetry run make -C core upload" +``` + +Note: instead of executing `nix-shell --run "poetry run the_command"` every time, you can enter +the nix-shell by running `nix-shell` +and then inside the nix-shell enter poetry shell by running `poetry shell`. +After this, you can run the commands directly, e.g. `trezorctl get-features`. diff --git a/wallet/wallet-rpc-daemon/README.md b/wallet/wallet-rpc-daemon/README.md index 8ab5a873f8..899d73a484 100644 --- a/wallet/wallet-rpc-daemon/README.md +++ b/wallet/wallet-rpc-daemon/README.md @@ -28,10 +28,10 @@ Using `curl` over HTTP (replace all caps placeholders as appropriate): curl -H 'Content-Type: application/json' -d '{"jsonrpc": "2.0", "id": ID, "method": METHOD, "params": [PARAM1, PARAM2, ...]}' http://USER:PASS@HOST:PORT ``` -for example, to get the balance of account with index 0 from an open wallet, with RPC, assuming authentication is disabled +for example, to get the balance of account with index 0 from an open wallet, counting only confirmed UTXOs, assuming authentication is disabled ```sh -curl -H 'Content-Type: application/json' -d '{"jsonrpc": "2.0", "id": 1, "method": "account_balance", "params": {"account": 0}}' http://127.0.0.1:3034 +curl -H 'Content-Type: application/json' -d '{"jsonrpc": "2.0", "id": 1, "method": "account_balance", "params": {"account": 0, "utxo_states": ["Confirmed"]}}' http://127.0.0.1:3034 ``` For websocket, you can use `websocat` (replace all caps placeholders as appropriate): @@ -49,7 +49,7 @@ and then type in the method invocations one per line in the following format: for example, to get the balance of account with index 0 from an open wallet, with RPC ``` -{"jsonrpc": "2.0", "id": 1, "method": "account_balance", "params": {"account": 0}} +{"jsonrpc": "2.0", "id": 1, "method": "account_balance", "params": {"account": 0, "utxo_states": ["Confirmed"]}} ``` as another example, since this is websocket, you can also subscribe to events. So to do that, send the function: @@ -83,7 +83,7 @@ The mechanism to subscribe to and to deliver events follows the [Ethereum pubsub However, the emitted events take slightly different shape. To see how the events are defined in full detail, see the `Event` type -in [src/service/events.rs](src/service/events.rs). +in [events.rs](/wallet/wallet-rpc-lib/src/service/events.rs). ### NewBlock From 76774a64df87435e0d96ff3ae3902552e17332c6 Mon Sep 17 00:00:00 2001 From: Mykhailo Kremniov Date: Mon, 11 Aug 2025 15:32:09 +0300 Subject: [PATCH 4/4] Fix Trezor device selection in wallet-cli --- wallet/wallet-cli-commands/src/lib.rs | 4 ++-- wallet/wallet-controller/src/lib.rs | 34 ++++++++++++++++++++++----- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/wallet/wallet-cli-commands/src/lib.rs b/wallet/wallet-cli-commands/src/lib.rs index a909b4f749..2b1eeb5ce6 100644 --- a/wallet/wallet-cli-commands/src/lib.rs +++ b/wallet/wallet-cli-commands/src/lib.rs @@ -1046,7 +1046,7 @@ impl CreateWalletDeviceSelectMenu { impl ChoiceMenu for CreateWalletDeviceSelectMenu { fn header(&self) -> &str { - "Please chose one of the available Trezor devices:" + "Please choose one of the available Trezor devices:" } fn choice_list(&self) -> Vec { @@ -1103,7 +1103,7 @@ impl OpenWalletDeviceSelectMenu { impl ChoiceMenu for OpenWalletDeviceSelectMenu { fn header(&self) -> &str { - "Please chose one of the available Trezor devices:" + "Please choose one of the available Trezor devices:" } fn choice_list(&self) -> Vec { diff --git a/wallet/wallet-controller/src/lib.rs b/wallet/wallet-controller/src/lib.rs index 262c6f8b9a..1ca5daddcc 100644 --- a/wallet/wallet-controller/src/lib.rs +++ b/wallet/wallet-controller/src/lib.rs @@ -317,10 +317,7 @@ where .map(|w| w.map_wallet(RuntimeWallet::Trezor)), }; - if res.is_err() { - let _ = fs::remove_file(file_path); - } - + Self::delete_wallet_file_on_wallet_creation_failure(&res, file_path); res } @@ -338,10 +335,10 @@ where ) ); - let db = wallet::wallet::open_or_create_wallet_file(file_path) + let db = wallet::wallet::open_or_create_wallet_file(file_path.as_ref()) .map_err(ControllerError::WalletError)?; - match args { + let res = match args { WalletTypeArgsComputed::Software { mnemonic, passphrase, @@ -382,6 +379,31 @@ where .map_err(ControllerError::WalletError)?; Ok(wallet.map_wallet(RuntimeWallet::Trezor)) } + }; + + Self::delete_wallet_file_on_wallet_creation_failure(&res, file_path); + res + } + + /// If wallet creation/recovery didn't succeed (e.g. due to a hard error, or because + /// user intervention is required), we must delete the wallet file. + fn delete_wallet_file_on_wallet_creation_failure( + result: &Result>, ControllerError>, + file_path: impl AsRef, + ) { + let must_remove_wallet_file = match result { + Err(_) => true, + Ok(wallet_creation) => match wallet_creation { + // Wallet was created successfully. + WalletCreation::Wallet(_) => false, + // Wallet was not created successfully. The caller will need to handle this result + // and either fail or try again. + WalletCreation::MultipleAvailableTrezorDevices(_) => true, + }, + }; + + if must_remove_wallet_file { + let _ = fs::remove_file(file_path); } }