Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions bitcoin_safe/gui/icons/hardware_signers/metrovault-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
39 changes: 37 additions & 2 deletions bitcoin_safe/gui/qt/new_wallet_welcome_screen.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@
from bitcoin_safe.gui.qt.sidebar.sidebar_tree import SidebarNode, SidebarTree
from bitcoin_safe.gui.qt.styled_card_frame import BaseBorderCardFrame
from bitcoin_safe.gui.qt.util import (
Message,
MessageType,
color_with_alpha,
get_neutral_surface_colors,
is_theme_change_event,
Expand All @@ -66,6 +68,8 @@

logger = logging.getLogger(__name__)

METROVAULT_SIGNER_URL = "https://bitcoin-safe.org/en/library/supported-hardware-signers/metrovault/"


class _ConfigWithWalletDir(Protocol):
wallet_dir: str
Expand Down Expand Up @@ -104,6 +108,7 @@ def __init__(self, icon_name: str, parent: QWidget | None = None) -> None:
self.label_description.setTextFormat(Qt.TextFormat.RichText)
self._opacity_effect = QGraphicsOpacityEffect(self)
self.setGraphicsEffect(self._opacity_effect)
self._available = True
self.updateUi()

def set_content(self, title: str, description: str) -> None:
Expand All @@ -116,9 +121,21 @@ def updateUi(self) -> None:
self._apply_visual_state()
self._refresh_theme_dependent_ui()

def set_available(self, available: bool) -> None:
"""Toggle between actionable and greyed-out states without disabling child links."""
self._available = available
self.set_header_clickable(available)
self._apply_visual_state()

def _is_header_activatable(self) -> bool:
"""Allow unavailable cards to intercept clicks and explain why they are disabled."""
return not self._available or super()._is_header_activatable()

def _apply_visual_state(self) -> None:
self.background_color = get_neutral_surface_colors().panel_background
self._opacity_effect.setOpacity(self._disabled_opacity if not self.isEnabled() else 1.0)
self._opacity_effect.setOpacity(
self._disabled_opacity if not self.isEnabled() or not self._available else 1.0
)

def changeEvent(self, a0: QEvent | None) -> None:
"""Refresh card styling when the theme or enabled state changes."""
Expand Down Expand Up @@ -481,6 +498,15 @@ def remove_me(self) -> None:

def on_click_hot_wallet(self) -> None:
"""Create a hot wallet."""
if self.network == bdk.Network.BITCOIN:
Message(
self.tr(
"Hot wallets are disabled on Bitcoin Mainnet.\nYou can switch to Testnet to test Bitcoin Safe without using real Bitcoin."
),
type=MessageType.Warning,
parent=self,
)
return
self.signal_onclick_hot_wallet.emit()
self.signal_remove_me.emit(self)

Expand Down Expand Up @@ -556,6 +582,9 @@ def create_ui(self) -> None:
"hardware_signers/generic-hardware-wallet-icon.svg", self.left_column
)
self.card_custom_wallet = WelcomeActionCard("material-symbols--signature.svg", self.left_column)
self.hot_wallet_help_label = IconLabel(parent=self.card_hot_wallet.header_widget)
self.hot_wallet_help_label.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
self.card_hot_wallet.header_right_layout.addWidget(self.hot_wallet_help_label)
self.connect_devices_help_label = IconLabel(parent=self.card_connect_devices.header_widget)
self.connect_devices_help_label.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
self.card_connect_devices.header_right_layout.addWidget(self.connect_devices_help_label)
Expand Down Expand Up @@ -586,7 +615,7 @@ def updateUi(self) -> None:
on_testnet = not on_mainnet

self.card_demo_wallet.setVisible(on_testnet)
self.card_hot_wallet.setEnabled(on_testnet)
self.card_hot_wallet.set_available(on_testnet)
self.card_demo_wallet.updateUi()
self.card_hot_wallet.updateUi()
self.card_connect_devices.updateUi()
Expand All @@ -612,6 +641,12 @@ def updateUi(self) -> None:
)
),
)
self.hot_wallet_help_label.setVisible(on_mainnet)
self.hot_wallet_help_label.setText(self.tr("No signer available?"))
self.hot_wallet_help_label.set_icon_as_help(
self.tr("Learn how to turn an Android phone into a dedicated bitcoin signer."),
METROVAULT_SIGNER_URL,
)
self.card_connect_devices.set_content(
title=self.tr("Connect Device(s)"),
description=self.tr("Guided setup for your self-custody wallet."),
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 17 additions & 0 deletions bitcoin_safe/hardware_signers.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
logger = logging.getLogger(__name__)

SUPPORTED_HARDWARE_SIGNERS_URL = "https://bitcoin-safe.org/en/knowledge/supported-hardware-signers/"
METROVAULT_REPOSITORY_URL = "https://github.com/gorunjinian/MetroVault"


def _signer_info_url(path: str | None = None) -> str:
Expand Down Expand Up @@ -252,6 +253,22 @@ class HardwareSigners:
icon_filename="jade-plus-icon.svg",
screenshot_name="jade",
)
metrovault = HardwareSigner(
id="metrovault",
brand_name="MetroVault",
display_name="MetroVault",
qr_types=[
QrExportTypes.bbqr,
QrExportTypes.ur,
DescriptorQrExportTypes.default,
DescriptorQrExportTypes.text,
SignMessageRequestQrExportTypes.text,
],
descriptor_export_types=[DescriptorExportTypes.default, DescriptorExportTypes.text],
usb=FeatureLevel.not_capable,
info_url=METROVAULT_REPOSITORY_URL,
icon_filename="metrovault-icon.svg",
)
passport = HardwareSigner(
id="passport",
brand_name="Foundation",
Expand Down
4 changes: 3 additions & 1 deletion release-notes/2.1.0.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
Highlights

- Light/Dark theme switching
- Added MetroVault support
- Upgrade Bitcoin Dev Kit (BDK) to 3.0
- Reenable custom account xpub fetching for USB signers
- Show the connected Bitcoin nodes for compact block filter fetching on the map
- Re-store custom account xpub fetching for USB signers
- Bug fixes
31 changes: 31 additions & 0 deletions tests/gui/qt/test_card_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@

from __future__ import annotations

from unittest.mock import Mock

import bdkpython as bdk
from PyQt6.QtCore import QEvent, Qt
from PyQt6.QtGui import QColor, QPalette
Expand All @@ -37,6 +39,7 @@

from bitcoin_safe.gui.qt.card_base import CardBase, CardExpansionMode, CardList
from bitcoin_safe.gui.qt.new_wallet_welcome_screen import (
METROVAULT_SIGNER_URL,
NetworkChoiceCard,
NetworkChoiceWelcomeScreen,
NewWalletWelcomeScreen,
Expand Down Expand Up @@ -425,3 +428,31 @@ def apply_palette(window: str, text: str) -> str:
assert to_color_name(screen.card_secure_wallet.cta_panel.background_color) == to_color_name(
get_neutral_surface_colors().panel_background
)


def test_mainnet_hot_wallet_card_keeps_help_link_clickable(qtbot: QtBot, monkeypatch) -> None:
screen = NewWalletWelcomeScreen(network=bdk.Network.BITCOIN, signals=Signals())
qtbot.addWidget(screen)
screen.show()
qtbot.waitExposed(screen)

hot_wallet_clicked = Mock()
message_mock = Mock()
open_mock = Mock()
screen.signal_onclick_hot_wallet.connect(hot_wallet_clicked)
monkeypatch.setattr("bitcoin_safe.gui.qt.new_wallet_welcome_screen.Message", message_mock)
monkeypatch.setattr("bitcoin_safe.gui.qt.icon_label.webopen", open_mock)

assert screen.hot_wallet_help_label.isVisible()
assert screen.hot_wallet_help_label.textLabel.text() == "No signer available?"
assert screen.card_hot_wallet.graphicsEffect() is not None
assert screen.card_hot_wallet.header_title.cursor().shape() == Qt.CursorShape.ArrowCursor

qtbot.mouseClick(screen.card_hot_wallet.header_title, Qt.MouseButton.LeftButton)
hot_wallet_clicked.assert_not_called()
message_mock.assert_called_once()
assert "Hot wallets are disabled on Bitcoin Mainnet." in message_mock.call_args.args[0]
assert message_mock.call_args.kwargs["type"].name == "Warning"

qtbot.mouseClick(screen.hot_wallet_help_label.textLabel, Qt.MouseButton.LeftButton)
open_mock.assert_called_once_with(METROVAULT_SIGNER_URL)
14 changes: 14 additions & 0 deletions tests/non_gui/test_hardware_signers.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,25 @@ def test_q_supports_usb() -> None:
assert HardwareSigners.q.usb == FeatureLevel.supported


def test_metrovault_brand_exposes_single_matching_model() -> None:
"""Test MetroVault appears as a single-model mobile signer brand."""
assert [signer.id for signer in HardwareSigners.models_for_brand("MetroVault")] == [
HardwareSigners.metrovault.id
]
assert HardwareSigners.metrovault.display_name == "MetroVault"
assert HardwareSigners.metrovault.icon_name == "metrovault-icon.svg"


def test_infer_from_text_matches_display_name() -> None:
"""Test free-form text inference resolves a known display name."""
assert HardwareSigners.infer_from_text("Krux App backup") == HardwareSigners.krux_diy


def test_infer_from_text_matches_metrovault() -> None:
"""Test free-form text inference resolves MetroVault by name."""
assert HardwareSigners.infer_from_text("MetroVault signer backup") == HardwareSigners.metrovault


def test_signer_ids_are_unique() -> None:
"""Test each configured signer has a unique persisted identifier."""
signer_ids = [signer.id for signer in HardwareSigners.as_list()]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,11 @@
<description>
<p>Highlights</p>
<ul>
<li>Light/Dark theme switching</li>
<li>Added MetroVault support</li>
<li>Upgrade Bitcoin Dev Kit (BDK) to 3.0</li>
<li>Reenable custom account xpub fetching for USB signers</li>
<li>Show the connected Bitcoin nodes for compact block filter fetching on the map</li>
<li>Re-store custom account xpub fetching for USB signers</li>
<li>Bug fixes</li>
</ul>
</description>
Expand Down
Loading