diff --git a/l10n/messages.pot b/l10n/messages.pot index 9a91fe28..a00f6d56 100644 --- a/l10n/messages.pot +++ b/l10n/messages.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: seedsigner 0.8.6\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-02-16 21:42+0000\n" +"POT-Creation-Date: 2026-02-19 19:13+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -1340,6 +1340,11 @@ msgstr "" msgid "Verify addr" msgstr "" +#. Multisig policy. For a "2 of 3" policy, "threshold" = 2; "n" = 3 +#: src/seedsigner/views/seed_views.py +msgid "{threshold} of {n}" +msgstr "" + #: src/seedsigner/views/seed_views.py msgid "Signing messages for custom derivation paths not supported" msgstr "" @@ -1466,6 +1471,11 @@ msgstr "" msgid "Change addresses" msgstr "" +#. Multisig policy. For a "2 / 3 multisig" policy, "threshold" = 2; "n" = 3 +#: src/seedsigner/views/tools_views.py +msgid "{threshold} / {n} multisig" +msgstr "" + #. a status message that our payment addresses are being calculated #: src/seedsigner/views/tools_views.py msgid "Calculating addrs..." diff --git a/src/seedsigner/gui/screens/tools_screens.py b/src/seedsigner/gui/screens/tools_screens.py index 42642946..f453ba30 100644 --- a/src/seedsigner/gui/screens/tools_screens.py +++ b/src/seedsigner/gui/screens/tools_screens.py @@ -489,7 +489,7 @@ class ToolsAddressExplorerAddressTypeScreen(ButtonListScreen): self.components.append(IconTextLine( # TRANSLATOR_NOTE: a label for a BIP-380-ish Output Descriptor label_text=_("Wallet descriptor"), - value_text=self.wallet_descriptor_display_name, # TODO: English text from embit (e.g. "1 / 2 multisig"); make l10 friendly + value_text=self.wallet_descriptor_display_name, is_text_centered=True, screen_x=GUIConstants.EDGE_PADDING, screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING, diff --git a/src/seedsigner/helpers/embit_utils.py b/src/seedsigner/helpers/embit_utils.py index ed304719..80553799 100644 --- a/src/seedsigner/helpers/embit_utils.py +++ b/src/seedsigner/helpers/embit_utils.py @@ -104,6 +104,14 @@ def get_multisig_address(descriptor: Descriptor, index: int = 0, is_change: bool +def get_multisig_policy(descriptor: Descriptor) -> tuple: + """Extract (threshold, n) from a basic multisig descriptor.""" + if not descriptor.is_basic_multisig: + raise ValueError(f"Expected a basic multisig descriptor, got: {descriptor.brief_policy}") + return (str(descriptor.miniscript.args[0]), str(len(descriptor.keys))) + + + def get_embit_network_name(settings_name): """ Convert SeedSigner SettingsConstants for `network` to embit's NETWORK key """ lookup = { diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 47958965..e938da49 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -2068,8 +2068,10 @@ class MultisigWalletDescriptorView(View): fingerprint = hexlify(key.fingerprint).decode() fingerprints.append(fingerprint) - policy = descriptor.brief_policy.split("multisig")[0].strip() - # policy = " / ".join(policy.split(" of ")) # i18n w/o l10n since coming from non-l10n embit + from seedsigner.helpers.embit_utils import get_multisig_policy + threshold, n = get_multisig_policy(descriptor) + # TRANSLATOR_NOTE: Multisig policy. For a "2 of 3" policy, "threshold" = 2; "n" = 3 + policy = _("{threshold} of {n}").format(threshold=threshold, n=n) button_data = [self.OK] if self.controller.resume_main_flow: diff --git a/src/seedsigner/views/tools_views.py b/src/seedsigner/views/tools_views.py index 92612222..b2c6b9fb 100644 --- a/src/seedsigner/views/tools_views.py +++ b/src/seedsigner/views/tools_views.py @@ -599,8 +599,12 @@ class ToolsAddressExplorerAddressTypeView(View): wallet_descriptor_display_name = None if "wallet_descriptor" in data: - wallet_descriptor_display_name = data["wallet_descriptor"].brief_policy.replace(" (sorted)", "") - wallet_descriptor_display_name = " / ".join(wallet_descriptor_display_name.split(" of ")) # i18n w/o l10n since coming from non-l10n embit + from seedsigner.helpers.embit_utils import get_multisig_policy + threshold, n = get_multisig_policy(data["wallet_descriptor"]) + # TRANSLATOR_NOTE: Multisig policy. For a "2 / 3 multisig" policy, "threshold" = 2; "n" = 3 + wallet_descriptor_display_name = _("{threshold} / {n} multisig").format( + threshold=threshold, n=n + ) script_type = data["script_type"] if "script_type" in data else None diff --git a/tests/test_embit_utils.py b/tests/test_embit_utils.py index bdc57dbc..7eaf1835 100644 --- a/tests/test_embit_utils.py +++ b/tests/test_embit_utils.py @@ -345,6 +345,35 @@ def test_get_multisig_address(): func(descriptor=descriptor, index=args[1], is_change=args[2], embit_network=args[3]) +def test_get_multisig_policy(): + """ + tests seedsigner.helpers.embit_utils.get_multisig_policy() + """ + from embit.descriptor import Descriptor + + # Reuses the same 2-of-3 multisig descriptors from test_get_multisig_address + vectors_descriptor_expected = { + # native segwit 2-of-3 + "wsh(sortedmulti(2,[8d55ff0d/48h/1h/0h/2h]tpubDDxNVWk924RTUhdkVB2uLHw1hGMPNMGufpZefhkkswjbZppVZcuMdjYKQN4ewUog9vbL6RBLFPRWcgTGT7kYP79N6thyJ43ELUs4N2szXMg/{0,1}/*,[73c5da0a/48h/1h/0h/2h]tpubDFH9dgzveyD8zTbPUFuLrGmCydNvxehyNdUXKJAQN8x4aZ4j6UZqGfnqFrD4NqyaTVGKbvEW54tsvPTK2UoSbCC1PJY8iCNiwTL3RWZEheQ/{0,1}/*,[0be174ee/48h/1h/0h/2h]tpubDEsePyLPkbxbrDiZSTTWdsviiNtiQjrvvzZnkLtG72QYLBygEsXePRsTdXi8DeMA7taCuuvoEBjUAfFrsNZeQJqfvG9fFoujYWbFPYUn7ux/{0,1}/*))#zw6cnrlk": ("2", "3"), + # nested segwit 2-of-3 + "sh(wsh(sortedmulti(2,[73c5da0a/48h/1h/1h/0h/1h]tpubDFH9dgzveyD8yHQb8VrpG8FYAuwcLMHMje2CCcbBo1FpaGzYVtJeYYxcYgRqSTta5utUFts8nPPHs9C2bqoxrey5jia6Dwf9mpwrPq7YvcJ/{0,1}/*,[0be174ee/48h/1h/0h/1h]tpubDEsePyLPkbxbnj6XuKvWwdERHaKkikZxaGJ9sJqmM7okbZXgkNSFiGU6GX6qEes6kD8f9Z9FosYB9UEnBSgBEyEwwJhj4uUcFE1WE8VtKoh/{0,1}/*,[8d55ff0d/48h/1h/0h/1h]tpubDDxNVWk924RTT3vyGLHdSDoZ2JUVX7jUsPcwCQ9MrKHAtJrW5zECTF9rFHCvqu526E4PjHp61hBknts2c5aGexvX7hvCZ8TGPvQFdzxxy59/{0,1}/*)))#2ujlfp73": ("2", "3"), + # legacy p2sh 2-of-3 + "sh(sortedmulti(2,[8d55ff0d/45h]tpubDANogJ2yfnizHwX7fSi5kUVzybyuPXDhgHB2TR9TUvkSLZFW73cRq4STKFDpx7qjJJiisyq82tbu4CeiYtmKEmT1xoCq9P8BPvXV31HUh6d/{0,1}/*,[0be174ee/45h]tpubDBkeVF2tDNT1Pz7L47iJeBB6RokU12LX6x4E6Ph8T89hmjQfB77q1AMyGwL8qpREVGq9sCJEbWwmnemwNTxnpxGn1di7BGy8jx9wEi5Vahu/{0,1}/*,[73c5da0a/45h]tpubDBKsGC1UqBDNvx9aivFmxZNgeZTUnmsCFGhWrqkLzucUCDePvbWWm3n8tAaAwMmxBG2ihdKCG9fzBdUnMxKx5PrkiqSZFi6Vkv6msUs9ddN/{0,1}/*))#p5t8sa8c": ("2", "3"), + } + + for desc_str, (expected_threshold, expected_n) in vectors_descriptor_expected.items(): + descriptor = Descriptor.from_string(desc_str) + threshold, n = embit_utils.get_multisig_policy(descriptor) + assert threshold == expected_threshold + assert n == expected_n + + # Non-multisig descriptor should raise ValueError + with pytest.raises(ValueError): + embit_utils.get_multisig_policy(Descriptor.from_string( + "wpkh([73c5da0a/84h/1h/0h]tpubDC5FSnBiZDMmhiuCmWAYsLwgLYrrT9rAqvTySfuCCrgsWz8wxMXUS9Tb9iVMvcRbvFcAHGkMD5Kx8koh4GquNGNTfohfk7pgjhaPCdXpoba/{0,1}/*)#2aj6cvca" + )) + + def test_parse_derivation_path(): # Shouldn't care if input uses "'" or "h" derivation_path = "m/84'/0'/0'/0/0"