Skip to content

Commit

Permalink
Add: rgb-lib parsing for Bitcoin address and RGB invoice
Browse files Browse the repository at this point in the history
  • Loading branch information
rahilmansuri1 committed Feb 27, 2025
1 parent c6e888a commit df788e6
Show file tree
Hide file tree
Showing 8 changed files with 221 additions and 6 deletions.
19 changes: 17 additions & 2 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
16 changes: 16 additions & 0 deletions src/views/components/send_asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
2 changes: 1 addition & 1 deletion src/views/qss/q_label.qss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
46 changes: 44 additions & 2 deletions src/views/ui_send_bitcoin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
)
Expand Down Expand Up @@ -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(),
)
Expand All @@ -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',
),
)
35 changes: 34 additions & 1 deletion src/views/ui_send_rgb_asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
)
Expand Down Expand Up @@ -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())
)
Expand Down Expand Up @@ -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',
),
)
64 changes: 64 additions & 0 deletions unit_tests/tests/ui_tests/ui_send_bitcoin_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -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',
),
)
44 changes: 44 additions & 0 deletions unit_tests/tests/ui_tests/ui_send_rgb_asset_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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('')
Expand Down Expand Up @@ -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()

0 comments on commit df788e6

Please sign in to comment.