From 30a646f80abf8756f22c535491ca0e1adfca2435 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 12 Aug 2025 15:41:31 +0000 Subject: [PATCH 1/6] tests: wizard: add test case for "restore from xpub" --- tests/test_wizard.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/test_wizard.py b/tests/test_wizard.py index db2bdd26b..db26a5066 100644 --- a/tests/test_wizard.py +++ b/tests/test_wizard.py @@ -337,6 +337,22 @@ class WalletWizardTestCase(WizardTestCase): v = w.resolve_next(v.view, d) self._set_password_and_check_address(v=v, w=w, recv_addr="bc1qgvx24uzdv4mapfmtlu8azty5fxdcw9ghxu4pr4") + async def test_create_standard_wallet_have_master_key(self): + w = self._wizard_for(wallet_type='standard') + v = w._current + d = v.wizard_data + self.assertEqual('keystore_type', v.view) + + d.update({'keystore_type': 'masterkey'}) + v = w.resolve_next(v.view, d) + self.assertEqual('have_master_key', v.view) + + d.update({ + 'master_key': 'zpub6nAZodjgiMNf9zzX1pTqd6ZVX61ax8azhUDnWRumKVUr1VYATVoqAuqv3qKsb8WJXjxei4wei2p4vnMG9RnpKnen2kmgdhvZUmug2NnHNsr', + 'multisig_master_pubkey': 'zpub6nAZodjgiMNf9zzX1pTqd6ZVX61ax8azhUDnWRumKVUr1VYATVoqAuqv3qKsb8WJXjxei4wei2p4vnMG9RnpKnen2kmgdhvZUmug2NnHNsr'}) + v = w.resolve_next(v.view, d) + self._set_password_and_check_address(v=v, w=w, recv_addr="bc1qq2tmmcngng78nllq2pvrkchcdukemtj56uyue0") + async def test_create_standard_wallet_haveseed_bip39(self): w = self._wizard_for(wallet_type='standard') v = w._current From fe3ebb31ec4f7b615290bbbf631295ffdf1fb2a7 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 12 Aug 2025 15:43:17 +0000 Subject: [PATCH 2/6] tests: wizard: add test case for "restore from slip39" --- tests/test_wizard.py | 64 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/tests/test_wizard.py b/tests/test_wizard.py index db26a5066..fa8b95dfd 100644 --- a/tests/test_wizard.py +++ b/tests/test_wizard.py @@ -8,6 +8,7 @@ from electrum.wizard import ServerConnectWizard, NewWalletWizard, WizardViewStat from electrum.daemon import Daemon from electrum.wallet import Abstract_Wallet from electrum import util +from electrum import slip39 from . import ElectrumTestCase from .test_wallet_vertical import UNICODE_HORROR @@ -127,7 +128,6 @@ class WalletWizardTestCase(WizardTestCase): # TODO imported addresses # TODO imported WIF keys # TODO multisig - # TODO slip39 def _wizard_for( self, @@ -393,6 +393,68 @@ class WalletWizardTestCase(WizardTestCase): v = w.resolve_next(v.view, d) self._set_password_and_check_address(v=v, w=w, recv_addr="bc1qjexrunguxz8rlfuul8h4apafyh3sq5yp9kg98j") + async def test_create_standard_wallet_haveseed_slip39(self): + w = self._wizard_for(wallet_type='standard') + v = w._current + d = v.wizard_data + self.assertEqual('keystore_type', v.view) + + d.update({'keystore_type': 'haveseed'}) + v = w.resolve_next(v.view, d) + self.assertEqual('have_seed', v.view) + + # SLIP39 shares (128 bits, 2 groups from 1 of 1, 1 of 1, 3 of 5, 2 of 6) + mnemonics = [ + "fact else acrobat romp analysis usher havoc vitamins analysis garden prevent romantic silent dramatic adjust priority mailman plains vintage else", + "fact else ceramic round craft lips snake faint adorn square bucket deadline violence guitar greatest academic stadium snake frequent memory", + "fact else ceramic scatter counter remove club forbid busy cause taxi forecast prayer uncover living type training forward software pumps", + "fact else ceramic shaft clock crowd detect cleanup wildlife depict include trip profile isolate express category wealthy advance garden mixture", + ] + encrypted_seed = slip39.recover_ems(mnemonics) + + d.update({'seed': encrypted_seed, 'seed_variant': 'slip39', 'seed_type': 'slip39', 'seed_extend': False}) + v = w.resolve_next(v.view, d) + self.assertEqual('script_and_derivation', v.view) + + d.update({ + 'script_type': 'p2wpkh', 'derivation_path': 'm/84h/0h/0h', + 'multisig_master_pubkey': 'zpub6riQosasrLdM1rmmohyUHtseLYeCBKP55Xe1LTT7jyKFM6dMMZPYVx5ug6zH2gZ6XFGcUYubjbm43vXHecTzNmoMS3yfp6oeZT3GetsGFt4'}) + v = w.resolve_next(v.view, d) + self._set_password_and_check_address(v=v, w=w, recv_addr="bc1q40ksvkl7wvc2l999ppl48swgt3rsl45ykyyrjn") + + async def test_create_standard_wallet_haveseed_slip39_passphrase(self): + w = self._wizard_for(wallet_type='standard') + v = w._current + d = v.wizard_data + self.assertEqual('keystore_type', v.view) + + d.update({'keystore_type': 'haveseed'}) + v = w.resolve_next(v.view, d) + self.assertEqual('have_seed', v.view) + + # SLIP39 shares (128 bits, 2 groups from 1 of 1, 1 of 1, 3 of 5, 2 of 6) + mnemonics = [ + "fact else acrobat romp analysis usher havoc vitamins analysis garden prevent romantic silent dramatic adjust priority mailman plains vintage else", + "fact else ceramic round craft lips snake faint adorn square bucket deadline violence guitar greatest academic stadium snake frequent memory", + "fact else ceramic scatter counter remove club forbid busy cause taxi forecast prayer uncover living type training forward software pumps", + "fact else ceramic shaft clock crowd detect cleanup wildlife depict include trip profile isolate express category wealthy advance garden mixture", + ] + encrypted_seed = slip39.recover_ems(mnemonics) + + d.update({'seed': encrypted_seed, 'seed_variant': 'slip39', 'seed_type': 'slip39', 'seed_extend': True}) + v = w.resolve_next(v.view, d) + self.assertEqual('have_ext', v.view) + + d.update({'seed_extra_words': 'TREZOR'}) + v = w.resolve_next(v.view, d) + self.assertEqual('script_and_derivation', v.view) + + d.update({ + 'script_type': 'p2wpkh', 'derivation_path': 'm/84h/0h/0h', + 'multisig_master_pubkey': 'zpub6s6A9ynh7TT1sPXmQyu8S6g7kxMF6iSZkM3NmgF4w7CtpsGgg56aouYSWHgAoMy186a8FRT8zkmhcwV5SWKFFQfMpvV8C9Ft4woWSzD5sXz'}) + v = w.resolve_next(v.view, d) + self._set_password_and_check_address(v=v, w=w, recv_addr="bc1qs2svwhfz47qv9qju2waa6prxzv5f522fc4p06t") + async def test_2fa_createseed(self): self.assertTrue(self.config.get('enable_plugin_trustedcoin')) w = self._wizard_for(wallet_type='2fa') From d78782c6aea018f2f68ab8ed3bf84b31e18c019a Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 12 Aug 2025 15:44:28 +0000 Subject: [PATCH 3/6] tests: wizard: add test cases for multisig --- tests/test_wizard.py | 149 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 148 insertions(+), 1 deletion(-) diff --git a/tests/test_wizard.py b/tests/test_wizard.py index fa8b95dfd..a8789fb5c 100644 --- a/tests/test_wizard.py +++ b/tests/test_wizard.py @@ -127,7 +127,6 @@ class WalletWizardTestCase(WizardTestCase): # TODO imported addresses # TODO imported WIF keys - # TODO multisig def _wizard_for( self, @@ -624,3 +623,151 @@ class WalletWizardTestCase(WizardTestCase): d.update({'password': '03a580deb85ef85654ed177fc049867ce915a8b392a34a524123870925e48a5b9e'}) self.assertTrue(w.is_last_view(v.view, d)) v = w.resolve_next(v.view, d) + + async def test_create_multisig_wallet_2of2_createseed_cosigner2hasmasterkey(self): + w = self._wizard_for(wallet_type='multisig') + v = w._current + d = v.wizard_data + self.assertEqual('multisig', v.view) + + d.update({'multisig_participants': 2, 'multisig_signatures': 2, 'multisig_cosigner_data': {}}) + v = w.resolve_next(v.view, d) + self.assertEqual('keystore_type', v.view) + + d.update({'keystore_type': 'createseed'}) + v = w.resolve_next(v.view, d) + self.assertEqual('create_seed', v.view) + + d.update({ + 'seed': 'eager divert pigeon dentist punch festival manage smart globe regular adult cash', + 'seed_type': 'segwit', 'seed_extend': False, 'seed_variant': 'electrum'}) + v = w.resolve_next(v.view, d) + self.assertEqual('confirm_seed', v.view) + + d.update({'multisig_master_pubkey': 'Zpub6y7YR1dmZZV4f5rRm6dJCKSqqxZhKUxc8PkssXm84k2bzbGYkL22ugC4aZxVxC1qz4yo53Zwz1c1kiSHmybB4JjCsjCPjzygSsN1UcdCcvB'}) + v = w.resolve_next(v.view, d) + self.assertEqual('multisig_cosigner_keystore', v.view) + + # 2nd cosigner uses Zpub from "9dk" seed + d['multisig_cosigner_data']['2'] = {'keystore_type': 'masterkey'} + d.update({ + 'multisig_current_cosigner': 2, 'cosigner_keystore_type': 'masterkey'}) + v = w.resolve_next(v.view, d) + self.assertEqual('multisig_cosigner_key', v.view) + + d['multisig_cosigner_data']['2'].update({'master_key': 'Zpub6y4evsU8HJw2d7ZH8QNyC6UKWHyxinAuQKkD6btsEZMbamy96UnefnM4sZp2K38rdiUssEhNq9TBpJ8Bh1GZCGTFpnYz8jM9pAdS6vk5VQs'}) + v = w.resolve_next(v.view, d) + self._set_password_and_check_address(v=v, w=w, recv_addr="bc1qg39tkymxwq4tn2ly6c3lmnyvsy94jyw52rdvfqkzdv2slvlj9xcsfy63vc") + + async def test_create_multisig_wallet_3of6_haveseed_passphrase__cs2hasbip39__cs3zpub__cs4trezor__cs5seedandpassphrase__cs6zprv(self): + w = self._wizard_for(wallet_type='multisig') + v = w._current + d = v.wizard_data + self.assertEqual('multisig', v.view) + + d.update({'multisig_participants': 6, 'multisig_signatures': 3, 'multisig_cosigner_data': {}}) + v = w.resolve_next(v.view, d) + self.assertEqual('keystore_type', v.view) + + d.update({'keystore_type': 'haveseed'}) + v = w.resolve_next(v.view, d) + self.assertEqual('have_seed', v.view) + + d.update({ + 'seed': '9dk', + 'seed_variant': 'electrum', 'seed_type': 'segwit', 'seed_extend': True}) + v = w.resolve_next(v.view, d) + self.assertEqual('have_ext', v.view) + + d.update({ + 'seed_extra_words': UNICODE_HORROR, + 'multisig_master_pubkey': 'Zpub6zAYrXzLbLwWFCkahiB3fQz4KMUm68RsoGVHkM5aBjzHBGnQ9orvy7PKuFvMj4gyJXhFW5uFzHBgDDYFEPS75b3ADq3yvtuEJF86ZgLLyeL'}) + v = w.resolve_next(v.view, d) + self.assertEqual('multisig_cosigner_keystore', v.view) + + # 2nd cosigner + d['multisig_cosigner_data']['2'] = {'keystore_type': 'haveseed'} + d.update({ + 'multisig_current_cosigner': 2, 'cosigner_keystore_type': 'haveseed'}) + v = w.resolve_next(v.view, d) + self.assertEqual('multisig_cosigner_seed', v.view) + + d['multisig_cosigner_data']['2'].update({ + 'seed': 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about', + 'seed_variant': 'bip39', 'seed_type': 'bip39', 'seed_extend': False}) + v = w.resolve_next(v.view, d) + self.assertEqual('multisig_cosigner_script_and_derivation', v.view) + + d['multisig_cosigner_data']['2'].update({ + 'script_type': 'p2wsh', 'derivation_path': 'm/48h/0h/0h/2h'}) + v = w.resolve_next(v.view, d) + self.assertEqual('multisig_cosigner_keystore', v.view) + + # 3rd cosigner uses Zpub from "9dk" seed + d['multisig_cosigner_data']['3'] = {'keystore_type': 'masterkey'} + d.update({ + 'multisig_current_cosigner': 3, 'cosigner_keystore_type': 'masterkey'}) + v = w.resolve_next(v.view, d) + self.assertEqual('multisig_cosigner_key', v.view) + + d['multisig_cosigner_data']['3'].update({ + 'master_key': 'Zpub6y4evsU8HJw2d7ZH8QNyC6UKWHyxinAuQKkD6btsEZMbamy96UnefnM4sZp2K38rdiUssEhNq9TBpJ8Bh1GZCGTFpnYz8jM9pAdS6vk5VQs'}) + v = w.resolve_next(v.view, d) + self.assertEqual('multisig_cosigner_keystore', v.view) + + # 4th cosigner + d['multisig_cosigner_data']['4'] = {'keystore_type': 'hardware'} + d.update({ + 'multisig_current_cosigner': 4, 'cosigner_keystore_type': 'hardware'}) + v = w.resolve_next(v.view, d) + self.assertEqual('multisig_cosigner_hardware', v.view) + + d['multisig_cosigner_data']['4'].update({ + 'hardware_device': ( + 'trezor', + DeviceInfo( + device=Device(path='webusb:002:1', interface_number=-1, id_='webusb:002:1', product_key='Trezor', usage_page=0, transport_ui_string='webusb:002:1'), + label='trezor_unittests', initialized=True, exception=None, plugin_name='trezor', soft_device_id='088C3F260B66F60E15DE0FA5', model_name='Trezor T'))}) + v = w.resolve_next(v.view, d) + self.assertEqual('trezor_start', v.view) + + d['multisig_cosigner_data']['4'].update({ + 'script_type': 'p2wsh', 'derivation_path': 'm/48h/0h/0h/2h'}) + v = w.resolve_next(v.view, d) + self.assertEqual('trezor_xpub', v.view) + + d['multisig_cosigner_data']['4'].update({ + 'hw_type': 'trezor', 'master_key': 'Zpub75t8XsK4GVa2EyQtjvT9auayKwonGaQJ149qB9r11o5iikugxJ99hYgbcaTdCGjd4DUdz4z2bqAtmDv2s8UihG1AnbzBufSG82GxjMDfVUn', + 'root_fingerprint': '6306ee35', 'label': 'trezor_unittests', 'soft_device_id': '088C3F260B66F60E15DE0FA5'}) + v = w.resolve_next(v.view, d) + self.assertEqual('multisig_cosigner_keystore', v.view) + + # 5th cosigner + d['multisig_cosigner_data']['5'] = {'keystore_type': 'haveseed'} + d.update({ + 'multisig_current_cosigner': 5, 'cosigner_keystore_type': 'haveseed'}) + v = w.resolve_next(v.view, d) + self.assertEqual('multisig_cosigner_seed', v.view) + + d['multisig_cosigner_data']['5'].update({ + 'seed': 'abandon bike', + 'seed_variant': 'electrum', 'seed_type': 'segwit', 'seed_extend': True}) + v = w.resolve_next(v.view, d) + self.assertEqual('multisig_cosigner_have_ext', v.view) + + d['multisig_cosigner_data']['5'].update({ + 'seed_extra_words': UNICODE_HORROR}) + v = w.resolve_next(v.view, d) + self.assertEqual('multisig_cosigner_keystore', v.view) + + # 6th cosigner uses Zprv from "abandon bike" seed + d['multisig_cosigner_data']['6'] = {'keystore_type': 'masterkey'} + d.update({ + 'multisig_current_cosigner': 6, 'cosigner_keystore_type': 'masterkey'}) + v = w.resolve_next(v.view, d) + self.assertEqual('multisig_cosigner_key', v.view) + + d['multisig_cosigner_data']['6'].update({ + 'master_key': 'ZprvAjWENdvYc1Ctvppxm4Z67U4EoiDy5VXKNvWmVAZshy7UjgKggu1UcAH7MqRqTaHVunuEPZ7o51wCrsZnJXPJtzHnAoxNmMLWFMHC7uvUN5P'}) + v = w.resolve_next(v.view, d) + self._set_password_and_check_address(v=v, w=w, recv_addr="bc1qtuzp7rectyjquax5c3p80eletswhp6cxslye749l47h4m9x92hzs6cmymy") From dad18c030cead690bfae7beb2cb533da6628526e Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 12 Aug 2025 17:47:03 +0000 Subject: [PATCH 4/6] tests: wizard: add test cases for imported wallets --- tests/test_wizard.py | 50 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/tests/test_wizard.py b/tests/test_wizard.py index a8789fb5c..95ba511e5 100644 --- a/tests/test_wizard.py +++ b/tests/test_wizard.py @@ -125,9 +125,6 @@ class ServerConnectWizardTestCase(WizardTestCase): class WalletWizardTestCase(WizardTestCase): - # TODO imported addresses - # TODO imported WIF keys - def _wizard_for( self, *, @@ -154,7 +151,7 @@ class WalletWizardTestCase(WizardTestCase): *, v: WizardViewState, w: NewWalletWizard, - recv_addr: str, + recv_addr: str | None, # "first addr" only makes sense for HD wallets password: str | None = None, encrypt_file: bool = False, ) -> Abstract_Wallet: @@ -170,7 +167,8 @@ class WalletWizardTestCase(WizardTestCase): self.assertTrue(os.path.exists(wallet_path)) wallet = Daemon._load_wallet(wallet_path, password=password, config=self.config) - self.assertEqual(recv_addr, wallet.get_receiving_addresses()[0]) + if recv_addr is not None: + self.assertEqual(recv_addr, wallet.get_receiving_addresses()[0]) self.assertEqual(bool(password), wallet.has_password()) self.assertEqual(encrypt_file, wallet.has_storage_encryption()) return wallet @@ -771,3 +769,45 @@ class WalletWizardTestCase(WizardTestCase): 'master_key': 'ZprvAjWENdvYc1Ctvppxm4Z67U4EoiDy5VXKNvWmVAZshy7UjgKggu1UcAH7MqRqTaHVunuEPZ7o51wCrsZnJXPJtzHnAoxNmMLWFMHC7uvUN5P'}) v = w.resolve_next(v.view, d) self._set_password_and_check_address(v=v, w=w, recv_addr="bc1qtuzp7rectyjquax5c3p80eletswhp6cxslye749l47h4m9x92hzs6cmymy") + + async def test_create_imported_wallet_from_addresses(self): + w = self._wizard_for(wallet_type='imported') + v = w._current + d = v.wizard_data + self.assertEqual('imported', v.view) + + d.update({ + 'address_list': + '14gcRovpkCoGkCNBivQBvw7eso7eiNAbxG\n' + '35ZqQJcBQMZ1rsv8aSuJ2wkC7ohUCQMJbT\n' + 'BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4\n' + 'bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kt5nd6y\n'}) + v = w.resolve_next(v.view, d) + wallet = self._set_password_and_check_address(v=v, w=w, recv_addr=None) + self.assertEqual( + set(wallet.get_receiving_addresses()), + { + "14gcRovpkCoGkCNBivQBvw7eso7eiNAbxG", + "35ZqQJcBQMZ1rsv8aSuJ2wkC7ohUCQMJbT", + "BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4", + "bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kt5nd6y", + }, + ) + + async def test_create_imported_wallet_from_wif_keys(self): + w = self._wizard_for(wallet_type='imported') + v = w._current + d = v.wizard_data + self.assertEqual('imported', v.view) + + d.update({ + 'private_key_list': + 'p2wpkh:L1cgMEnShp73r9iCukoPE3MogLeueNYRD9JVsfT1zVHyPBR3KqBY\n' + 'p2pkh:KyQ2voUQj71P6E9KyDFqQoYMMm3yKKAPMKbfqZccib6xWxbWHCex\n' + 'p2pkh:5JuecQZ1nH4VCQRQJTQjB4yu93BU6NmnAkDoGRdHX2PyH2E8QVX\n'}) + v = w.resolve_next(v.view, d) + wallet = self._set_password_and_check_address(v=v, w=w, recv_addr=None) + self.assertEqual( + set(wallet.get_receiving_addresses()), + {"bc1qq2tmmcngng78nllq2pvrkchcdukemtj56uyue0", "1LNvv5h6QHoYv1nJcqrp13T2TBkD2sUGn1", "1FJEEB8ihPMbzs2SkLmr37dHyRFzakqUmo"}, + ) From 19b2567da83c97e9cc01874e2d04c04edfd90c04 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 12 Aug 2025 18:28:09 +0000 Subject: [PATCH 5/6] tests: bitcoin: merge testnet/mainnet address_to_script tests and add an extra explicit mixed-case bech32 test case --- tests/test_bitcoin.py | 119 +++++++++++++++--------------------------- 1 file changed, 41 insertions(+), 78 deletions(-) diff --git a/tests/test_bitcoin.py b/tests/test_bitcoin.py index f9e28f733..9ce265b52 100644 --- a/tests/test_bitcoin.py +++ b/tests/test_bitcoin.py @@ -481,6 +481,8 @@ class Test_bitcoin(ElectrumTestCase): # test vectors from BIP-0173 # note: the ones that are commented out have been invalidated by BIP-0350 self.assertEqual(address_to_script('BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4').hex(), '0014751e76e8199196d454941c45d1b3a323f1433bd6') + self.assertEqual(address_to_script('tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7', net=constants.BitcoinTestnet).hex(), '00201863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262') + self.assertEqual(address_to_script('tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy', net=constants.BitcoinTestnet).hex(), '0020000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433') # self.assertEqual(address_to_script('bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx'), '5128751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6') # self.assertEqual(address_to_script('BC1SW50QA3JX3S'), '6002751e') # self.assertEqual(address_to_script('bc1zw508d6qejxtdg4y5r3zarvaryvg6kdaj'), '5210751e76e8199196d454941c45d1b3a323') @@ -491,43 +493,57 @@ class Test_bitcoin(ElectrumTestCase): self.assertEqual(address_to_script('BC1SW50QGDZ25J').hex(), '6002751e') self.assertEqual(address_to_script('bc1zw508d6qejxtdg4y5r3zarvaryvaxxpcs').hex(), '5210751e76e8199196d454941c45d1b3a323') self.assertEqual(address_to_script('bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqzk5jj0').hex(), '512079be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798') + self.assertEqual(address_to_script('tb1pqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesf3hn0c', net=constants.BitcoinTestnet).hex(), '5120000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433') # invalid addresses (from BIP-0173) - self.assertFalse(is_address('tc1qw508d6qejxtdg4y5r3zarvary0c5xw7kg3g4ty')) - self.assertFalse(is_address('bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t5')) - self.assertFalse(is_address('BC13W508D6QEJXTDG4Y5R3ZARVARY0C5XW7KN40WF2')) - self.assertFalse(is_address('bc1rw5uspcuh')) - self.assertFalse(is_address('bc10w508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kw5rljs90')) - self.assertFalse(is_address('BC1QR508D6QEJXTDG4Y5R3ZARVARYV98GJ9P')) - self.assertFalse(is_address('tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sL5k7')) - self.assertFalse(is_address('bc1zw508d6qejxtdg4y5r3zarvaryvqyzf3du')) - self.assertFalse(is_address('tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3pjxtptv')) - self.assertFalse(is_address('bc1gmk9yu')) + for net in [constants.BitcoinMainnet, constants.BitcoinTestnet]: + self.assertFalse(is_address('tc1qw508d6qejxtdg4y5r3zarvary0c5xw7kg3g4ty', net=net)) + self.assertFalse(is_address('bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t5', net=net)) + self.assertFalse(is_address('BC13W508D6QEJXTDG4Y5R3ZARVARY0C5XW7KN40WF2', net=net)) + self.assertFalse(is_address('bc1rw5uspcuh', net=net)) + self.assertFalse(is_address('bc10w508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kw5rljs90', net=net)) + self.assertFalse(is_address('BC1QR508D6QEJXTDG4Y5R3ZARVARYV98GJ9P', net=net)) + self.assertFalse(is_address('tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sL5k7', net=net)) + self.assertFalse(is_address('bc1zw508d6qejxtdg4y5r3zarvaryvqyzf3du', net=net)) + self.assertFalse(is_address('tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3pjxtptv', net=net)) + self.assertFalse(is_address('bc1gmk9yu', net=net)) # invalid addresses (from BIP-0350) - self.assertFalse(is_address('tc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vq5zuyut')) - self.assertFalse(is_address('bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqh2y7hd')) - self.assertFalse(is_address('tb1z0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqglt7rf')) - self.assertFalse(is_address('BC1S0XLXVLHEMJA6C4DQV22UAPCTQUPFHLXM9H8Z3K2E72Q4K9HCZ7VQ54WELL')) - self.assertFalse(is_address('bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kemeawh')) - self.assertFalse(is_address('tb1q0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vq24jc47')) - self.assertFalse(is_address('bc1p38j9r5y49hruaue7wxjce0updqjuyyx0kh56v8s25huc6995vvpql3jow4')) - self.assertFalse(is_address('BC130XLXVLHEMJA6C4DQV22UAPCTQUPFHLXM9H8Z3K2E72Q4K9HCZ7VQ7ZWS8R')) - self.assertFalse(is_address('bc1pw5dgrnzv')) - self.assertFalse(is_address('bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7v8n0nx0muaewav253zgeav')) - self.assertFalse(is_address('BC1QR508D6QEJXTDG4Y5R3ZARVARYV98GJ9P')) - self.assertFalse(is_address('tb1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vq47Zagq')) - self.assertFalse(is_address('bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7v07qwwzcrf')) - self.assertFalse(is_address('tb1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vpggkg4j')) - self.assertFalse(is_address('bc1gmk9yu')) + for net in [constants.BitcoinMainnet, constants.BitcoinTestnet]: + self.assertFalse(is_address('tc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vq5zuyut', net=net)) + self.assertFalse(is_address('bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqh2y7hd', net=net)) + self.assertFalse(is_address('tb1z0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqglt7rf', net=net)) + self.assertFalse(is_address('BC1S0XLXVLHEMJA6C4DQV22UAPCTQUPFHLXM9H8Z3K2E72Q4K9HCZ7VQ54WELL', net=net)) + self.assertFalse(is_address('bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kemeawh', net=net)) + self.assertFalse(is_address('tb1q0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vq24jc47', net=net)) + self.assertFalse(is_address('bc1p38j9r5y49hruaue7wxjce0updqjuyyx0kh56v8s25huc6995vvpql3jow4', net=net)) + self.assertFalse(is_address('BC130XLXVLHEMJA6C4DQV22UAPCTQUPFHLXM9H8Z3K2E72Q4K9HCZ7VQ7ZWS8R', net=net)) + self.assertFalse(is_address('bc1pw5dgrnzv', net=net)) + self.assertFalse(is_address('bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7v8n0nx0muaewav253zgeav', net=net)) + self.assertFalse(is_address('BC1QR508D6QEJXTDG4Y5R3ZARVARYV98GJ9P', net=net)) + self.assertFalse(is_address('tb1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vq47Zagq', net=net)) + self.assertFalse(is_address('bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7v07qwwzcrf', net=net)) + self.assertFalse(is_address('tb1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vpggkg4j', net=net)) + self.assertFalse(is_address('bc1gmk9yu', net=net)) + + # bech32(m) mixed case: + bech32_mixed_case1 = 'BC1QW508D6QEJXTDG4Y5R3zarvary0c5xw7kv8f3t4' + self.assertFalse(is_address(bech32_mixed_case1)) + self.assertTrue(is_address(bech32_mixed_case1.lower())) + self.assertTrue(is_address(bech32_mixed_case1.upper())) # base58 P2PKH self.assertEqual(address_to_script('14gcRovpkCoGkCNBivQBvw7eso7eiNAbxG').hex(), '76a91428662c67561b95c79d2257d2a93d9d151c977e9188ac') self.assertEqual(address_to_script('1BEqfzh4Y3zzLosfGhw1AsqbEKVW6e1qHv').hex(), '76a914704f4b81cadb7bf7e68c08cd3657220f680f863c88ac') + self.assertEqual(address_to_script('mutXcGt1CJdkRvXuN2xoz2quAAQYQ59bRX', net=constants.BitcoinTestnet).hex(), '76a9149da64e300c5e4eb4aaffc9c2fd465348d5618ad488ac') + self.assertEqual(address_to_script('miqtaRTkU3U8rzwKbEHx3g8FSz8GJtPS3K', net=constants.BitcoinTestnet).hex(), '76a914247d2d5b6334bdfa2038e85b20fc15264f8e5d2788ac') # base58 P2SH self.assertEqual(address_to_script('35ZqQJcBQMZ1rsv8aSuJ2wkC7ohUCQMJbT').hex(), 'a9142a84cf00d47f699ee7bbc1dea5ec1bdecb4ac15487') self.assertEqual(address_to_script('3PyjzJ3im7f7bcV724GR57edKDqoZvH7Ji').hex(), 'a914f47c8954e421031ad04ecd8e7752c9479206b9d387') + self.assertEqual(address_to_script('2N3LSvr3hv5EVdfcrxg2Yzecf3SRvqyBE4p', net=constants.BitcoinTestnet).hex(), 'a9146eae23d8c4a941316017946fc761a7a6c85561fb87') + self.assertEqual(address_to_script('2NE4ZdmxFmUgwu5wtfoN2gVniyMgRDYq1kk', net=constants.BitcoinTestnet).hex(), 'a914e4567743d378957cd2ee7072da74b1203c1a7a0b87') + def test_address_to_payload(self): # bech32 P2WPKH @@ -644,59 +660,6 @@ class Test_bitcoin(ElectrumTestCase): segwit_addr.bech32_decode('1p2gdwpf')) -class Test_bitcoin_testnet(ElectrumTestCase): - TESTNET = True - - def test_address_to_script(self): - # bech32/bech32m native segwit - # test vectors from BIP-0173 - self.assertEqual(address_to_script('tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7').hex(), '00201863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262') - self.assertEqual(address_to_script('tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy').hex(), '0020000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433') - - # bech32/bech32m native segwit - # test vectors from BIP-0350 - self.assertEqual(address_to_script('tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7').hex(), '00201863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262') - self.assertEqual(address_to_script('tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy').hex(), '0020000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433') - self.assertEqual(address_to_script('tb1pqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesf3hn0c').hex(), '5120000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433') - - # invalid addresses (from BIP-0173) - self.assertFalse(is_address('tc1qw508d6qejxtdg4y5r3zarvary0c5xw7kg3g4ty')) - self.assertFalse(is_address('bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t5')) - self.assertFalse(is_address('BC13W508D6QEJXTDG4Y5R3ZARVARY0C5XW7KN40WF2')) - self.assertFalse(is_address('bc1rw5uspcuh')) - self.assertFalse(is_address('bc10w508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kw5rljs90')) - self.assertFalse(is_address('BC1QR508D6QEJXTDG4Y5R3ZARVARYV98GJ9P')) - self.assertFalse(is_address('tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sL5k7')) - self.assertFalse(is_address('bc1zw508d6qejxtdg4y5r3zarvaryvqyzf3du')) - self.assertFalse(is_address('tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3pjxtptv')) - self.assertFalse(is_address('bc1gmk9yu')) - - # invalid addresses (from BIP-0350) - self.assertFalse(is_address('tc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vq5zuyut')) - self.assertFalse(is_address('bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqh2y7hd')) - self.assertFalse(is_address('tb1z0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqglt7rf')) - self.assertFalse(is_address('BC1S0XLXVLHEMJA6C4DQV22UAPCTQUPFHLXM9H8Z3K2E72Q4K9HCZ7VQ54WELL')) - self.assertFalse(is_address('bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kemeawh')) - self.assertFalse(is_address('tb1q0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vq24jc47')) - self.assertFalse(is_address('bc1p38j9r5y49hruaue7wxjce0updqjuyyx0kh56v8s25huc6995vvpql3jow4')) - self.assertFalse(is_address('BC130XLXVLHEMJA6C4DQV22UAPCTQUPFHLXM9H8Z3K2E72Q4K9HCZ7VQ7ZWS8R')) - self.assertFalse(is_address('bc1pw5dgrnzv')) - self.assertFalse(is_address('bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7v8n0nx0muaewav253zgeav')) - self.assertFalse(is_address('BC1QR508D6QEJXTDG4Y5R3ZARVARYV98GJ9P')) - self.assertFalse(is_address('tb1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vq47Zagq')) - self.assertFalse(is_address('bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7v07qwwzcrf')) - self.assertFalse(is_address('tb1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vpggkg4j')) - self.assertFalse(is_address('bc1gmk9yu')) - - # base58 P2PKH - self.assertEqual(address_to_script('mutXcGt1CJdkRvXuN2xoz2quAAQYQ59bRX').hex(), '76a9149da64e300c5e4eb4aaffc9c2fd465348d5618ad488ac') - self.assertEqual(address_to_script('miqtaRTkU3U8rzwKbEHx3g8FSz8GJtPS3K').hex(), '76a914247d2d5b6334bdfa2038e85b20fc15264f8e5d2788ac') - - # base58 P2SH - self.assertEqual(address_to_script('2N3LSvr3hv5EVdfcrxg2Yzecf3SRvqyBE4p').hex(), 'a9146eae23d8c4a941316017946fc761a7a6c85561fb87') - self.assertEqual(address_to_script('2NE4ZdmxFmUgwu5wtfoN2gVniyMgRDYq1kk').hex(), 'a914e4567743d378957cd2ee7072da74b1203c1a7a0b87') - - class Test_xprv_xpub(ElectrumTestCase): xprv_xpub = ( From acc52e392c2488a2305481c171d9a85dbc8a22e3 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 12 Aug 2025 18:42:35 +0000 Subject: [PATCH 6/6] tests: wizard: imported addrs: validate each addr with "is_address()" This was already done *in the GUIs*, but the backend should definitely do at least sanity-check-level validation like this. --- electrum/wizard.py | 5 +++++ tests/test_wizard.py | 18 +++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/electrum/wizard.py b/electrum/wizard.py index 48a3cee9f..689454974 100644 --- a/electrum/wizard.py +++ b/electrum/wizard.py @@ -679,6 +679,11 @@ class NewWalletWizard(KeystoreWizard): addresses[addr] = {'type': txin_type, 'pubkey': pubkey} elif 'address_list' in data: for addr in data['address_list'].split(): + assert isinstance(addr, str) + assert bitcoin.is_address(addr), f"expected bitcoin addr. got {addr[:5] + '..' + addr[-2:]}" + # note: we do not normalize addresses. :/ + # In particular, bech32 addresses can be either all-lowercase or all-uppercase. + # TODO we should normalize them, but it only makes sense if we also do a walletDB-upgrade. addresses[addr] = {} elif data['keystore_type'] in ['createseed', 'haveseed']: seed_extension = data['seed_extra_words'] if data['seed_extend'] else '' diff --git a/tests/test_wizard.py b/tests/test_wizard.py index 95ba511e5..a494245f2 100644 --- a/tests/test_wizard.py +++ b/tests/test_wizard.py @@ -789,11 +789,27 @@ class WalletWizardTestCase(WizardTestCase): { "14gcRovpkCoGkCNBivQBvw7eso7eiNAbxG", "35ZqQJcBQMZ1rsv8aSuJ2wkC7ohUCQMJbT", - "BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4", + "BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4", # TODO normalize to lowercase? "bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kt5nd6y", }, ) + async def test_create_imported_wallet_from_addresses__invalid_input(self): + w = self._wizard_for(wallet_type='imported') + v = w._current + d = v.wizard_data + self.assertEqual('imported', v.view) + + d.update({ + 'address_list': + 'garbagegarbage\n' + '35ZqQJcBQMZ1rsv8aSuJ2wkC7ohUCQMJbT\n' + }) + v = w.resolve_next(v.view, d) + with self.assertRaises(AssertionError) as ctx: + wallet = self._set_password_and_check_address(v=v, w=w, recv_addr=None) + self.assertTrue("expected bitcoin addr" in ctx.exception.args[0]) + async def test_create_imported_wallet_from_wif_keys(self): w = self._wizard_for(wallet_type='imported') v = w._current