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
This commit is contained in:
2026-04-29 15:05:26 +02:00
parent 8b8d958a45
commit d51076cb0c
9 changed files with 47 additions and 24 deletions
+5
View File
@@ -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
+1 -1
View File
@@ -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
}
}
+1 -1
View File
@@ -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):
+8 -7
View File
@@ -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
+5 -3
View File
@@ -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()
+2 -1
View File
@@ -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)
+2 -1
View File
@@ -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
+2 -2
View File
@@ -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]:
+21 -8
View File
@@ -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