diff --git a/poetry.lock b/poetry.lock index cdc7425..baf3d28 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "altgraph" @@ -2148,6 +2148,21 @@ requests = ">=2.0.0" [package.extras] rsa = ["oauthlib[signedtoken] (>=3.0.0)"] +[[package]] +name = "rgb-lib" +version = "0.3.0a12" +description = "RGB Lib Python language bindings." +optional = false +python-versions = ">=3.9.0" +files = [ + {file = "rgb_lib-0.3.0a12-py3-none-macosx_12_0_arm64.whl", hash = "sha256:f4e5a3b2d1f4cb04f2466834e7d8985599df3982299c915ab10c79c9c790cb75"}, + {file = "rgb_lib-0.3.0a12-py3-none-macosx_12_0_x86_64.whl", hash = "sha256:aba59d56bcf28af3da79d9674775c1bd2149841f97b4264b60e71939fa305ae8"}, + {file = "rgb_lib-0.3.0a12-py3-none-manylinux_2_34_aarch64.whl", hash = "sha256:3a24355bf16edd893445084126b7523b42b0a71322deaec53bb498d7192973d6"}, + {file = "rgb_lib-0.3.0a12-py3-none-manylinux_2_34_x86_64.whl", hash = "sha256:40463f14af98327ee02a27feb78b66d46fc5bdd1db5063ff6c9fbf3d790e65f2"}, + {file = "rgb_lib-0.3.0a12-py3-none-win_amd64.whl", hash = "sha256:96dc6942e61a94427e0dd0ff6101f2ea8bc965280e6ff11fcecc7d93207ca28f"}, + {file = "rgb_lib-0.3.0a12.tar.gz", hash = "sha256:ad9bd876f276f9d2e365c19fb16e09ddc1c3aa4d115fba2fad9817c984d120ee"}, +] + [[package]] name = "rsa" version = "4.9" @@ -2392,4 +2407,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.13" -content-hash = "96e4f98958bc387e07cfce7566a54121dc4812375709941a02165517b5deba5c" +content-hash = "8ea492cbd42ea081a534b5b18d44233f7489df30746222b56c912138328283f6" diff --git a/pyproject.toml b/pyproject.toml index c14cbcb..6e45be2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,7 @@ cryptography = "^43.0.0" asyncio = "^3.4.3" requests-cache = "^1.2.1" pytest-xdist = "^3.6.1" +rgb-lib = "0.3.0a12" [tool.poetry.dev-dependencies] black = "24.4.1" bump2version = "1.0.1" diff --git a/src/views/components/send_asset.py b/src/views/components/send_asset.py index c1490d7..4b3f5d0 100644 --- a/src/views/components/send_asset.py +++ b/src/views/components/send_asset.py @@ -204,6 +204,22 @@ def __init__(self, view_model: MainViewModel, address: str): self.send_asset_details_layout.addWidget( self.asset_address_value, 0, Qt.AlignHCenter, ) + self.asset_address_validation_label = QLabel(self) + self.asset_address_validation_label.setObjectName( + 'address_validation_label', + ) + self.asset_address_validation_label.setMinimumSize(QSize(335, 0)) + self.asset_address_validation_label.setMaximumSize( + QSize(335, 16777215), + ) + self.asset_address_validation_label.setWordWrap(True) + self.send_asset_details_layout.addWidget( + self.asset_address_validation_label, 0, Qt.AlignHCenter, + ) + self.asset_address_validation_label.setStyleSheet( + load_stylesheet('views/qss/q_label.qss'), + ) + self.asset_address_validation_label.hide() self.total_supply_label = QLabel(self.send_asset_page) self.total_supply_label.setObjectName('total_supply_label') diff --git a/src/views/qss/q_label.qss b/src/views/qss/q_label.qss index 0dee23d..b1c42e2 100644 --- a/src/views/qss/q_label.qss +++ b/src/views/qss/q_label.qss @@ -58,7 +58,7 @@ font-weight: 600; } /* Error label */ -QLabel#spendable_balance_validation,#asset_amount_validation{ +QLabel#spendable_balance_validation,#asset_amount_validation, #address_validation_label{ color: red; border: none; } diff --git a/src/views/ui_send_bitcoin.py b/src/views/ui_send_bitcoin.py index 21e3674..a65d9d6 100644 --- a/src/views/ui_send_bitcoin.py +++ b/src/views/ui_send_bitcoin.py @@ -4,11 +4,16 @@ """ from __future__ import annotations +from PySide6.QtCore import QCoreApplication from PySide6.QtWidgets import QVBoxLayout from PySide6.QtWidgets import QWidget +from rgb_lib import Address +from rgb_lib import BitcoinNetwork +from rgb_lib import RgbLibError import src.resources_rc from src.data.repository.setting_card_repository import SettingCardRepository +from src.data.repository.setting_repository import SettingRepository from src.model.setting_model import DefaultFeeRate from src.utils.constant import FEE_RATE from src.utils.render_timer import RenderTimer @@ -43,6 +48,9 @@ def __init__(self, view_model): def setup_ui_connection(self): """Set up connections for UI elements.""" self.send_bitcoin_page.send_btn.setDisabled(True) + self.send_bitcoin_page.asset_address_value.textChanged.connect( + self.validate_bitcoin_address, + ) self.send_bitcoin_page.asset_address_value.textChanged.connect( self.handle_button_enabled, ) @@ -162,8 +170,9 @@ def is_valid_value(value): return bool(value) and value != '0' is_address_valid = bool( - self.send_bitcoin_page.asset_address_value.text(), - ) + self.send_bitcoin_page.asset_address_value.text( + ), + ) and not self.send_bitcoin_page.asset_address_validation_label.isVisible() is_amount_valid = is_valid_value( self.send_bitcoin_page.asset_amount_value.text(), ) @@ -184,3 +193,36 @@ def refresh_bitcoin_balance(self): """This method handles the feature for refreshing the Bitcoin balance.""" self.loading_performer = 'REFRESH_BUTTON' self._view_model.bitcoin_view_model.get_transaction_list() + + def validate_bitcoin_address(self): + """ + Validates the Bitcoin address input. + + - Retrieves the wallet network from settings. + - Checks if the entered address is valid for the given network. + - Displays an error message if the address is invalid. + """ + address = self.send_bitcoin_page.asset_address_value.text().strip() + + if not address: + self.send_bitcoin_page.asset_address_validation_label.hide() + return + + try: + network_enum = SettingRepository.get_wallet_network() + network_value = BitcoinNetwork[network_enum.value.upper()] + + # Validate the Bitcoin address + Address(address, network_value) + + # Hide validation label if the address is valid + self.send_bitcoin_page.asset_address_validation_label.hide() + + except RgbLibError.InvalidAddress: + # Show error message if the address is invalid + self.send_bitcoin_page.asset_address_validation_label.show() + self.send_bitcoin_page.asset_address_validation_label.setText( + QCoreApplication.translate( + 'iris_wallet_desktop', 'invalid_address', + ), + ) diff --git a/src/views/ui_send_rgb_asset.py b/src/views/ui_send_rgb_asset.py index 08b26fe..a3c4f62 100644 --- a/src/views/ui_send_rgb_asset.py +++ b/src/views/ui_send_rgb_asset.py @@ -4,9 +4,11 @@ """ from __future__ import annotations -from PySide6.QtCore import QSize +from PySide6.QtCore import QCoreApplication from PySide6.QtWidgets import QVBoxLayout from PySide6.QtWidgets import QWidget +from rgb_lib import Invoice +from rgb_lib import RgbLibError import src.resources_rc from src.data.repository.rgb_repository import RgbRepository @@ -73,6 +75,9 @@ def setup_ui_connection(self): self._view_model.rgb25_view_model.message.connect( self.show_rgb25_message, ) + self.send_rgb_asset_page.asset_address_value.textChanged.connect( + self.validate_rgb_invoice, + ) self.send_rgb_asset_page.asset_amount_value.textChanged.connect( self.handle_button_enabled, ) @@ -178,6 +183,8 @@ def are_fields_valid(): """Checks if required fields are filled and valid.""" return ( is_valid_value(self.send_rgb_asset_page.asset_address_value.text()) and + # Check if the validation label is hidden + not self.send_rgb_asset_page.asset_address_validation_label.isVisible() and is_valid_value(self.send_rgb_asset_page.asset_amount_value.text()) and is_valid_value(self.send_rgb_asset_page.fee_rate_value.text()) ) @@ -296,3 +303,29 @@ def disable_buttons_on_fee_rate_loading(self, button_status: bool): ) self.send_rgb_asset_page.send_btn.setDisabled(update_button_status) self.handle_button_enabled() + + def validate_rgb_invoice(self): + """ + Validates the RGB invoice input. + + - Hides the validation label initially. + - Checks if the entered invoice is valid. + - Displays an error message if the invoice is invalid. + """ + invoice = self.send_rgb_asset_page.asset_address_value.text().strip() + + if not invoice: + self.send_rgb_asset_page.asset_address_validation_label.hide() + return + try: + Invoice(invoice) + self.send_rgb_asset_page.asset_address_validation_label.hide() + + except RgbLibError.InvalidInvoice: + self.send_rgb_asset_page.asset_address_validation_label.show() + self.send_rgb_asset_page.send_btn.setDisabled(True) + self.send_rgb_asset_page.asset_address_validation_label.setText( + QCoreApplication.translate( + 'iris_wallet_desktop', 'invalid_invoice', + ), + ) diff --git a/unit_tests/tests/ui_tests/ui_send_bitcoin_test.py b/unit_tests/tests/ui_tests/ui_send_bitcoin_test.py index a9a7ee4..f2e1680 100644 --- a/unit_tests/tests/ui_tests/ui_send_bitcoin_test.py +++ b/unit_tests/tests/ui_tests/ui_send_bitcoin_test.py @@ -6,6 +6,8 @@ from unittest.mock import patch import pytest +from PySide6.QtCore import QCoreApplication +from rgb_lib import RgbLibError from src.model.setting_model import DefaultFeeRate from src.viewmodels.main_view_model import MainViewModel @@ -101,6 +103,9 @@ def test_handle_button_enabled(send_bitcoin_widget: SendBitcoinWidget): send_bitcoin_widget.send_bitcoin_page.asset_address_value.setText( '1BitcoinAddress', ) + send_bitcoin_widget.send_bitcoin_page.asset_address_validation_label.setVisible( + False, + ) send_bitcoin_widget.send_bitcoin_page.asset_amount_value.setText('0.001') send_bitcoin_widget.send_bitcoin_page.fee_rate_value.setText('0.0001') send_bitcoin_widget.send_bitcoin_page.pay_amount = 1000 @@ -114,6 +119,25 @@ def test_handle_button_enabled(send_bitcoin_widget: SendBitcoinWidget): send_bitcoin_widget.handle_button_enabled() assert not send_bitcoin_widget.send_bitcoin_page.send_btn.isEnabled() + # Test invalid address (validation label visible) + send_bitcoin_widget.send_bitcoin_page.asset_address_value.setText( + '1BitcoinAddress', + ) + send_bitcoin_widget.send_bitcoin_page.asset_address_validation_label.setVisible( + True, + ) + send_bitcoin_widget.handle_button_enabled() + # Since the validation label is visible, the button should be enabled + assert send_bitcoin_widget.send_bitcoin_page.send_btn.isEnabled() + + # Test invalid amount (empty) + send_bitcoin_widget.send_bitcoin_page.asset_address_validation_label.setVisible( + False, + ) + send_bitcoin_widget.send_bitcoin_page.asset_amount_value.clear() + send_bitcoin_widget.handle_button_enabled() + assert not send_bitcoin_widget.send_bitcoin_page.send_btn.isEnabled() + # Test invalid amount (zero) send_bitcoin_widget.send_bitcoin_page.asset_amount_value.setText('0') send_bitcoin_widget.handle_button_enabled() @@ -245,3 +269,43 @@ def test_bitcoin_page_navigation(send_bitcoin_widget: SendBitcoinWidget): # Assert that the bitcoin_page method was called once send_bitcoin_widget._view_model.page_navigation.bitcoin_page.assert_called_once() + + +def test_validate_bitcoin_address(send_bitcoin_widget: SendBitcoinWidget): + """Test the validate_bitcoin_address method.""" + + # Mock the necessary attributes + send_bitcoin_widget.send_bitcoin_page.asset_address_value = MagicMock() + send_bitcoin_widget.send_bitcoin_page.asset_address_validation_label = MagicMock() + + # Test with an empty address + send_bitcoin_widget.send_bitcoin_page.asset_address_value.text.return_value = ' ' + send_bitcoin_widget.validate_bitcoin_address() + send_bitcoin_widget.send_bitcoin_page.asset_address_validation_label.hide.assert_called_once() + send_bitcoin_widget.send_bitcoin_page.asset_address_validation_label.hide.reset_mock() + + # Test with a valid address + # Example valid Bitcoin address + valid_address = '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa' + send_bitcoin_widget.send_bitcoin_page.asset_address_value.text.return_value = valid_address + + # Mock the settings and the address validation + with patch('src.data.repository.setting_repository.SettingRepository.get_wallet_network', return_value=MagicMock(value='MAINNET')), \ + patch('rgb_lib.Address', return_value=None): + send_bitcoin_widget.validate_bitcoin_address() + # Reset the call count for hide before the next assertion + send_bitcoin_widget.send_bitcoin_page.asset_address_validation_label.hide.assert_called_once() + + # Test with an invalid address + invalid_address = 'invalid_address' + send_bitcoin_widget.send_bitcoin_page.asset_address_value.text.return_value = invalid_address + + with patch('src.data.repository.setting_repository.SettingRepository.get_wallet_network', return_value=MagicMock(value='MAINNET')), \ + patch('rgb_lib.Address', side_effect=RgbLibError.InvalidAddress('Invalid address details')): + send_bitcoin_widget.validate_bitcoin_address() + send_bitcoin_widget.send_bitcoin_page.asset_address_validation_label.show.assert_called_once() + send_bitcoin_widget.send_bitcoin_page.asset_address_validation_label.setText.assert_called_once_with( + QCoreApplication.translate( + 'iris_wallet_desktop', 'invalid_address', + ), + ) diff --git a/unit_tests/tests/ui_tests/ui_send_rgb_asset_test.py b/unit_tests/tests/ui_tests/ui_send_rgb_asset_test.py index 3216ff5..f43e6f0 100644 --- a/unit_tests/tests/ui_tests/ui_send_rgb_asset_test.py +++ b/unit_tests/tests/ui_tests/ui_send_rgb_asset_test.py @@ -9,6 +9,7 @@ import pytest from PySide6.QtCore import QCoreApplication +from rgb_lib import RgbLibError from src.model.enums.enums_model import ToastPreset from src.model.rgb_model import Balance @@ -64,6 +65,7 @@ def test_retranslate_ui(send_rgb_asset_widget: SendRGBAssetWidget, qtbot): def test_handle_button_enabled(send_rgb_asset_widget: SendRGBAssetWidget, qtbot): """Test the handle_button_enabled method.""" + # Test with empty address and amount send_rgb_asset_widget.send_rgb_asset_page.asset_address_value.setText('') send_rgb_asset_widget.send_rgb_asset_page.asset_amount_value.setText('') @@ -569,3 +571,45 @@ def test_disable_buttons_on_fee_rate_loading(send_rgb_asset_widget: SendRGBAsset assert not send_rgb_asset_widget.send_rgb_asset_page.fast_checkbox.isEnabled() assert not send_rgb_asset_widget.send_rgb_asset_page.custom_checkbox.isEnabled() assert not send_rgb_asset_widget.send_rgb_asset_page.send_btn.isEnabled() + + +def test_validate_rgb_invoice(send_rgb_asset_widget: SendRGBAssetWidget): + """Test the validate_rgb_invoice method.""" + + # Mock the necessary attributes + send_rgb_asset_widget.send_rgb_asset_page.asset_address_value = MagicMock() + send_rgb_asset_widget.send_rgb_asset_page.asset_address_validation_label = MagicMock() + + # Test with an empty invoice + send_rgb_asset_widget.send_rgb_asset_page.asset_address_value.text.return_value = ' ' + send_rgb_asset_widget.validate_rgb_invoice() + send_rgb_asset_widget.send_rgb_asset_page.asset_address_validation_label.hide.assert_called_once() + send_rgb_asset_widget.send_rgb_asset_page.asset_address_validation_label.hide.reset_mock() + + # Test with a valid invoice + valid_invoice = 'valid_rgb_invoice_string' # Example valid RGB invoice + send_rgb_asset_widget.send_rgb_asset_page.asset_address_value.text.return_value = valid_invoice + +# # Test with a valid invoice + valid_invoice = 'valid_invoice_string' + send_rgb_asset_widget.send_rgb_asset_page.asset_address_value.setText( + valid_invoice, + ) + with patch('src.views.ui_send_rgb_asset.Invoice', return_value=None): + send_rgb_asset_widget.validate_rgb_invoice() + send_rgb_asset_widget.send_rgb_asset_page.asset_address_validation_label.hide.assert_called_once() + send_rgb_asset_widget.send_rgb_asset_page.asset_address_validation_label.hide.reset_mock() + + # Test with an invalid invoice + invalid_invoice = 'invalid_rgb_invoice_string' + send_rgb_asset_widget.send_rgb_asset_page.asset_address_value.text.return_value = invalid_invoice + + with patch('rgb_lib.Invoice', side_effect=RgbLibError.InvalidInvoice('Invalid invoice details')): + send_rgb_asset_widget.validate_rgb_invoice() + send_rgb_asset_widget.send_rgb_asset_page.asset_address_validation_label.show.assert_called_once() + send_rgb_asset_widget.send_rgb_asset_page.asset_address_validation_label.setText.assert_called_once_with( + QCoreApplication.translate( + 'iris_wallet_desktop', 'invalid_invoice', + ), + ) + assert not send_rgb_asset_widget.send_rgb_asset_page.send_btn.isEnabled()