From d51076cb0c202ff4dd5528bafbd25501f43df78b Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Wed, 29 Apr 2026 15:05:26 +0200 Subject: [PATCH] feat: network-aware coin name and unit strings Add COIN_SYMBOL/COIN_NAME to AbstractNet (defaults: BTC/Bitcoin). BitcoinPurple overrides to BTCP/Bitcoin Purple; testnet inherits. Replace module-level base_units/base_units_list in util.py with get_base_units()/get_base_units_list() that read constants.net.COIN_SYMBOL at runtime, producing [BTCP, mBTCP, bits, sat] on BitcoinPurple. Update all UI touch points: Qt window title, watching-only warning, invalid address message, testnet warning, settings unit combo + help text, QML networkName property, and Preferences thousands-separator label --- electrum/constants.py | 5 ++++ electrum/gui/qml/components/Preferences.qml | 2 +- electrum/gui/qml/qenetwork.py | 2 +- electrum/gui/qt/main_window.py | 15 ++++++----- electrum/gui/qt/settings_dialog.py | 8 +++--- electrum/gui/stdio.py | 3 ++- electrum/gui/text.py | 3 ++- electrum/simple_config.py | 4 +-- electrum/util.py | 29 +++++++++++++++------ 9 files changed, 47 insertions(+), 24 deletions(-) diff --git a/electrum/constants.py b/electrum/constants.py index 99c56b588..ae08dc127 100644 --- a/electrum/constants.py +++ b/electrum/constants.py @@ -81,6 +81,9 @@ class AbstractNet: XPUB_HEADERS: Mapping[str, int] XPUB_HEADERS_INV: Mapping[int, str] + COIN_SYMBOL: str = "BTC" + COIN_NAME: str = "Bitcoin" + # PoW difficulty parameters (Bitcoin defaults; override per chain as needed) DIFFICULTY_ADJUSTMENT_INTERVAL: int = 2016 # blocks per retarget window POW_TARGET_TIMESPAN: int = 14 * 24 * 60 * 60 # target seconds per window @@ -273,6 +276,8 @@ class BitcoinPurple(AbstractNet): NET_NAME = "bitcoinpurple" TESTNET = False + COIN_SYMBOL = "BTCP" + COIN_NAME = "Bitcoin Purple" WIF_PREFIX = 0xb7 ADDRTYPE_P2PKH = 56 ADDRTYPE_P2SH = 55 diff --git a/electrum/gui/qml/components/Preferences.qml b/electrum/gui/qml/components/Preferences.qml index 859584087..8b9b94776 100644 --- a/electrum/gui/qml/components/Preferences.qml +++ b/electrum/gui/qml/components/Preferences.qml @@ -89,7 +89,7 @@ Pane { } Label { Layout.fillWidth: true - text: qsTr('Add thousands separators to bitcoin amounts') + text: qsTr('Add thousands separators to %1 amounts').arg(Network.networkName) wrapMode: Text.Wrap } } diff --git a/electrum/gui/qml/qenetwork.py b/electrum/gui/qml/qenetwork.py index 5130bf4f9..870182ce1 100644 --- a/electrum/gui/qml/qenetwork.py +++ b/electrum/gui/qml/qenetwork.py @@ -263,7 +263,7 @@ class QENetwork(QObject, QtEventListener): @pyqtProperty(str, notify=dataChanged) def networkName(self): - return constants.net.__name__.replace('Bitcoin', '') + return constants.net.COIN_NAME @pyqtProperty('QVariantMap', notify=proxyChanged) def proxy(self): diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 6e1c35d3e..8673e886b 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -629,8 +629,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): @classmethod def get_app_name_and_version_str(cls) -> str: name = "Electrum" - if constants.net.TESTNET: - name += " " + constants.net.NET_NAME.capitalize() + if constants.net.NET_NAME != "mainnet": + name += " " + constants.net.COIN_NAME return f"{name} {ELECTRUM_VERSION}" def watching_only_changed(self): @@ -648,10 +648,11 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): def warn_if_watching_only(self): if self.wallet.is_watching_only(): + coin = constants.net.COIN_NAME msg = ' '.join([ _("This wallet is watching-only."), - _("This means you will not be able to spend Bitcoins with it."), - _("Make sure you own the seed phrase or the private keys, before you request Bitcoins to be sent to this wallet.") + _("This means you will not be able to spend {coin} with it.").format(coin=coin), + _("Make sure you own the seed phrase or the private keys, before you request {coin} to be sent to this wallet.").format(coin=coin), ]) self.show_warning(msg, title=_('Watch-only wallet')) @@ -668,7 +669,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): msg = ''.join([ _("You are in testnet mode."), ' ', _("Testnet coins are worthless."), '\n', - _("Testnet is separate from the main Bitcoin network. It is used for testing.") + _("Testnet is separate from the main {coin} network. It is used for testing.").format(coin=constants.net.COIN_NAME) ]) cb = QCheckBox(_("Don't show this again.")) cb_checked = False @@ -2133,7 +2134,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): address = address.text().strip() message = message.toPlainText().strip() if not bitcoin.is_address(address): - self.show_message(_('Invalid Bitcoin address.')) + self.show_message(_(f'Invalid {constants.net.COIN_NAME} address.')) return if self.wallet.is_watching_only(): self.show_message(_('This is a watching-only wallet.')) @@ -2161,7 +2162,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): address = address.text().strip() message = message.toPlainText().strip().encode('utf-8') if not bitcoin.is_address(address): - self.show_message(_('Invalid Bitcoin address.')) + self.show_message(_(f'Invalid {constants.net.COIN_NAME} address.')) return try: # This can throw on invalid base64 diff --git a/electrum/gui/qt/settings_dialog.py b/electrum/gui/qt/settings_dialog.py index 81a7eb4f5..87b49323e 100644 --- a/electrum/gui/qt/settings_dialog.py +++ b/electrum/gui/qt/settings_dialog.py @@ -33,7 +33,8 @@ from PyQt6.QtWidgets import (QComboBox, QTabWidget, QDialog, QSpinBox, QCheckB from electrum.i18n import _, get_gui_lang_names from electrum import util -from electrum.util import base_units_list, event_listener +from electrum.util import get_base_units_list, event_listener +from electrum import constants from electrum.gui.common_qt.util import QtEventListener from electrum.gui import messages @@ -159,9 +160,10 @@ class SettingsDialog(QDialog, QtEventListener): msat_cb.stateChanged.connect(on_msat_checked) # units - units = base_units_list + units = get_base_units_list() + sym = constants.net.COIN_SYMBOL msg = (_('Base unit of your wallet.') - + '\n1 BTC = 1000 mBTC. 1 mBTC = 1000 bits. 1 bit = 100 sat.\n' + + f'\n1 {sym} = 1000 m{sym}. 1 m{sym} = 1000 bits. 1 bit = 100 sat.\n' + _('This setting affects the Send tab, and all balance related fields.')) unit_label = HelpLabel(_('Base unit') + ':', msg) unit_combo = QComboBox() diff --git a/electrum/gui/stdio.py b/electrum/gui/stdio.py index 34eab29c3..138473c20 100644 --- a/electrum/gui/stdio.py +++ b/electrum/gui/stdio.py @@ -6,6 +6,7 @@ from typing import Optional from electrum.gui import BaseElectrumGui from electrum import util +from electrum import constants from electrum import WalletStorage, Wallet from electrum.wallet import Abstract_Wallet from electrum.wallet_db import WalletDB @@ -185,7 +186,7 @@ class ElectrumGui(BaseElectrumGui, EventListener): def do_send(self): if not is_address(self.str_recipient): - print(_('Invalid Bitcoin address')) + print(_(f'Invalid {constants.net.COIN_NAME} address')) return try: amount = int(Decimal(self.str_amount) * COIN) diff --git a/electrum/gui/text.py b/electrum/gui/text.py index 1c8619310..969455cb1 100644 --- a/electrum/gui/text.py +++ b/electrum/gui/text.py @@ -14,6 +14,7 @@ except ImportError: # only use vendored lib as fallback, to allow Linux distros from electrum._vendor import pyperclip from electrum.gui import BaseElectrumGui +from electrum import constants from electrum.bip21 import parse_bip21_URI from electrum.util import format_time from electrum.util import EventListener, event_listener @@ -641,7 +642,7 @@ class ElectrumGui(BaseElectrumGui, EventListener): URI=None, ) else: - self.show_message(_('Invalid Bitcoin address')) + self.show_message(_(f'Invalid {constants.net.COIN_NAME} address')) return None return invoice diff --git a/electrum/simple_config.py b/electrum/simple_config.py index 2dcc8bee3..b8a006118 100644 --- a/electrum/simple_config.py +++ b/electrum/simple_config.py @@ -10,7 +10,7 @@ from copy import deepcopy from . import constants from . import util from . import invoices -from .util import base_units, base_unit_name_to_decimal_point, decimal_point_to_base_unit_name, UnknownBaseUnit, DECIMAL_POINT_DEFAULT +from .util import get_base_units, base_unit_name_to_decimal_point, decimal_point_to_base_unit_name, UnknownBaseUnit, DECIMAL_POINT_DEFAULT from .util import format_satoshis, format_fee_satoshis, os_chmod from .util import user_dir, make_dir from .util import is_valid_websocket_url @@ -547,7 +547,7 @@ class SimpleConfig(Logger): return decimal_point_to_base_unit_name(self.BTC_AMOUNTS_DECIMAL_POINT) def set_base_unit(self, unit): - assert unit in base_units.keys() + assert unit in get_base_units() self.BTC_AMOUNTS_DECIMAL_POINT = base_unit_name_to_decimal_point(unit) def get_nostr_relays(self) -> Sequence[str]: diff --git a/electrum/util.py b/electrum/util.py index 1bf6025cd..12c154d30 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -92,29 +92,42 @@ def all_subclasses(cls) -> Set: ca_path = certifi.where() -base_units = {'BTC':8, 'mBTC':5, 'bits':2, 'sat':0} -base_units_inverse = inv_dict(base_units) -base_units_list = ['BTC', 'mBTC', 'bits', 'sat'] # list(dict) does not guarantee order - DECIMAL_POINT_DEFAULT = 5 # mBTC class UnknownBaseUnit(Exception): pass +# Canonical decimal-point map; keys use 'BTC' as placeholder for any coin symbol. +_BASE_UNITS_DP = {'BTC': 8, 'mBTC': 5, 'bits': 2, 'sat': 0} +_BASE_UNITS_ORDER = ['BTC', 'mBTC', 'bits', 'sat'] + + +def _coin_key(k: str) -> str: + from electrum import constants # avoid circular import at module level + return k.replace('BTC', constants.net.COIN_SYMBOL) + + +def get_base_units() -> dict: + return {_coin_key(k): v for k, v in _BASE_UNITS_DP.items()} + + +def get_base_units_list() -> list: + return [_coin_key(k) for k in _BASE_UNITS_ORDER] + + def decimal_point_to_base_unit_name(dp: int) -> str: - # e.g. 8 -> "BTC" + inv = {v: k for k, v in get_base_units().items()} try: - return base_units_inverse[dp] + return inv[dp] except KeyError: raise UnknownBaseUnit(dp) from None def base_unit_name_to_decimal_point(unit_name: str) -> int: """Returns the max number of digits allowed after the decimal point.""" - # e.g. "BTC" -> 8 try: - return base_units[unit_name] + return get_base_units()[unit_name] except KeyError: raise UnknownBaseUnit(unit_name) from None