diff --git a/electrum/interface.py b/electrum/interface.py index bdd14e019892..54d14fa415e7 100644 --- a/electrum/interface.py +++ b/electrum/interface.py @@ -35,7 +35,7 @@ import itertools import logging import hashlib - +from electrum import bitcoin import aiorpcx from aiorpcx import TaskGroup from aiorpcx import RPCSession, Notification, NetAddress, NewlineFramer @@ -57,7 +57,6 @@ from .i18n import _ from .logging import Logger from .transaction import Transaction - if TYPE_CHECKING: from .network import Network from .simple_config import SimpleConfig @@ -923,6 +922,7 @@ async def get_history_for_scripthash(self, sh: str) -> List[dict]: assert_non_negative_integer(tx_item['fee']) return res + async def listunspent_for_scripthash(self, sh: str) -> List[dict]: if not is_hash256_str(sh): raise Exception(f"{repr(sh)} is not a scripthash") diff --git a/electrum_gui/android/console.py b/electrum_gui/android/console.py index 80c826d04c9f..1de5a9b7f327 100755 --- a/electrum_gui/android/console.py +++ b/electrum_gui/android/console.py @@ -5345,6 +5345,19 @@ def network_list(self, params): return ret + def find_accounts( + self, password, chain_code, start_index, searching_count, search_count_as_requested_by_user=True, hw=None + ): + mnemonic = self._get_hd_wallet().get_seed(password) + data = wallet_manager.search_existing_wallets( + chain_code, + mnemonic, + start_index=start_index, + searching_count=searching_count, + search_count_as_requested_by_user=search_count_as_requested_by_user, + ) + return data + all_commands = commands.known_commands.copy() for name, func in vars(AndroidCommands).items(): diff --git a/electrum_gui/common/basic/bip44.py b/electrum_gui/common/basic/bip44.py index 1f055fc38b05..5a7d6f30ca2e 100644 --- a/electrum_gui/common/basic/bip44.py +++ b/electrum_gui/common/basic/bip44.py @@ -100,9 +100,14 @@ def from_bip44_path(cls, path: str) -> "BIP44Path": return cls(*levels, last_hardened_level=last_hardened_level) - def next_sibling(self, gap: int = 1) -> "BIP44Path": + def next_sibling(self, gap: int = 1, level: BIP44Level = None) -> "BIP44Path": + if level is not None: + require(len(self._levels) >= level) + increase_index = level - 1 + else: + increase_index = -1 next_levels = self._levels.copy() - next_levels[-1] += gap + next_levels[increase_index] += gap return self.__class__(*next_levels, last_hardened_level=self._last_hardened_level) def to_target_level(self, target_level: BIP44Level, value_filling_if_none: int = 0) -> "BIP44Path": diff --git a/electrum_gui/common/tests/unit/wallet/test_manager.py b/electrum_gui/common/tests/unit/wallet/test_manager.py index 8a6292d1eac7..1656c8b0b2d6 100644 --- a/electrum_gui/common/tests/unit/wallet/test_manager.py +++ b/electrum_gui/common/tests/unit/wallet/test_manager.py @@ -437,36 +437,6 @@ def test_export_mnemonic__standalone_mnemonic_wallet(self): (self.mnemonic, self.passphrase), wallet_manager.export_mnemonic(wallet_info["wallet_id"], self.password) ) - @patch("electrum_gui.common.wallet.manager.provider_manager.get_address") - def test_search_existing_wallets(self, fake_get_address): - fake_get_address.side_effect = ( - lambda chain_code, address: provider_data.Address(address=address, balance=18888, existing=True) - if address == "0xa0331fcfa308e488833de1fe16370b529fa7c720" - else provider_data.Address(address=address, balance=0, existing=False) - ) - - self.assertEqual( - [ - { - 'address': '3Nu7tDXHbqtuMfMi3DMVrnLFabTvaY2FyF', - 'address_encoding': 'P2WPKH-P2SH', - 'balance': 0, - 'bip44_path': "m/49'/0'/0'/0/0", - 'chain_code': 'btc', - 'name': 'BTC-1', - }, - { - 'address': '0xa0331fcfa308e488833de1fe16370b529fa7c720', - 'address_encoding': None, - 'balance': 18888, - 'bip44_path': "m/44'/60'/0'/0/11", - 'chain_code': 'eth', - 'name': 'ETH-1', - }, - ], - wallet_manager.search_existing_wallets(["btc", "eth"], self.mnemonic, passphrase=self.passphrase), - ) - def test_update_wallet_password(self): wallet_info = wallet_manager.import_standalone_wallet_by_mnemonic( "ETH-1", @@ -1188,3 +1158,124 @@ def test_verify_message__software(self, fake_provider_manager): fake_provider_manager.verify_message.assert_called_once_with( "eth", "fake_address", "Hello OneKey", "fake_signature" ) + + def test_generate_searching_bip44_address_paths__eth_base(self): + chain_info = Mock( + bip44_purpose_options=None, + default_address_encoding=None, + bip44_coin_type=60, + bip44_last_hardened_level=bip44.BIP44Level.ACCOUNT, + bip44_auto_increment_level=bip44.BIP44Level.ADDRESS_INDEX, + bip44_target_level=bip44.BIP44Level.ADDRESS_INDEX, + chain_affinity="eth", + ) + + self.assertEqual( + [(None, f"m/44'/60'/0'/0/{i}") for i in range(20)], + list(wallet_manager.generate_searching_bip44_address_paths(chain_info)), + ) + + self.assertEqual( + [(None, f"m/44'/60'/0'/0/{i}") for i in range(5, 20)], + list(wallet_manager.generate_searching_bip44_address_paths(chain_info, start_index=5, searching_count=15)), + ) + + def test_generate_searching_bip44_address_paths__btc_base(self): + bip44_purpose_options = {"P2WPKH-P2SH": 49, "P2PKH": 44, "P2WPKH": 84} + chain_info = Mock( + bip44_purpose_options=bip44_purpose_options, + default_address_encoding="P2WPKH-P2SH", + bip44_coin_type=0, + bip44_last_hardened_level=bip44.BIP44Level.ACCOUNT, + bip44_auto_increment_level=bip44.BIP44Level.ACCOUNT, + bip44_target_level=bip44.BIP44Level.ADDRESS_INDEX, + chain_affinity="btc", + ) + + def _searching_paths(start, end): + for encoding, purpose in bip44_purpose_options.items(): + for account in range(start, end): + for change in (0, 1): + for address in range(20): + yield encoding, f"m/{purpose}'/0'/{account}'/{change}/{address}" + + self.assertEqual( + list(_searching_paths(0, 20)), + list(wallet_manager.generate_searching_bip44_address_paths(chain_info)), + ) + self.assertEqual( + list(_searching_paths(5, 20)), + list(wallet_manager.generate_searching_bip44_address_paths(chain_info, start_index=5, searching_count=15)), + ) + + @patch("electrum_gui.common.wallet.manager.provider_manager.batch_get_address") + def test_search_existing_wallets__eth_base(self, fake_batch_get_address): + fake_batch_get_address.return_value = [ + provider_data.Address(address="0x9858effd232b4033e47d90003d41ec34ecaeda94", balance=0, existing=True), + provider_data.Address(address="0x6fac4d18c912343bf86fa7049364dd4e424ab9c0", balance=10000, existing=True), + provider_data.Address(address="0xb6716976a3ebe8d39aceb04372f22ff8e6802d7a", balance=0, existing=False), + ] + + self.assertEqual( + { + 'chain_code': 'eth', + 'next': 5, + 'found_wallet_info': { + "m/44'/60'/0'/0/0": { + 'sub_path': [ + { + 'path': "m/44'/60'/0'/0/0", + 'address': '0x9858effd232b4033e47d90003d41ec34ecaeda94', + 'balance': 0, + } + ] + }, + "m/44'/60'/0'/0/1": { + 'sub_path': [ + { + 'path': "m/44'/60'/0'/0/1", + 'address': '0x6fac4d18c912343bf86fa7049364dd4e424ab9c0', + 'balance': 10000, + } + ] + }, + }, + }, + wallet_manager.search_existing_wallets("eth", self.mnemonic, start_index=0, searching_count=5), + ) + + @patch("electrum_gui.common.wallet.manager.provider_manager.batch_get_address") + def test_search_existing_wallets__btc_base(self, fake_batch_get_address): + fake_batch_get_address.side_effect = ( + lambda chain_code, addresses: [ + provider_data.Address(address="37VucYSaXLCAsxYyAPfbSi9eh4iEcbShgf", balance=0, existing=True), + provider_data.Address(address="3LtMnn87fqUeHBUG414p9CWwnoV6E2pNKS", balance=10000, existing=True), + provider_data.Address(address="3B4cvWGR8X6Xs8nvTxVUoMJV77E4f7oaia", balance=0, existing=False), + ] + if "37VucYSaXLCAsxYyAPfbSi9eh4iEcbShgf" in addresses + else [ + provider_data.Address(address="37mbeJptxfQC6SNNLJ9a8efCY4BwBh5Kak", balance=20, existing=True), + provider_data.Address(address="3QrMAP4ZG3a7Y1qFF5A4sY8MeSUxZ8Yxjy", balance=0, existing=False), + ] + ) + + self.assertEqual( + { + 'chain_code': 'btc', + 'next': 1, + 'found_wallet_info': { + "m/49'/0'/0'": { + 'sub_path': [ + {'path': "m/49'/0'/0'/0/0", 'address': '37VucYSaXLCAsxYyAPfbSi9eh4iEcbShgf', 'balance': 0}, + { + 'path': "m/49'/0'/0'/0/1", + 'address': '3LtMnn87fqUeHBUG414p9CWwnoV6E2pNKS', + 'balance': 10000, + }, + {'path': "m/49'/0'/0'/0/4", 'address': '37mbeJptxfQC6SNNLJ9a8efCY4BwBh5Kak', 'balance': 20}, + ] + } + }, + }, + wallet_manager.search_existing_wallets("btc", self.mnemonic, start_index=0, searching_count=1), + ) diff --git a/electrum_gui/common/wallet/manager.py b/electrum_gui/common/wallet/manager.py index 88e355771f75..6fb92bc1e85e 100644 --- a/electrum_gui/common/wallet/manager.py +++ b/electrum_gui/common/wallet/manager.py @@ -8,6 +8,7 @@ from typing import Iterable, List, Tuple, Union import eth_account +from peewee import chunked from electrum_gui.common.basic import bip44 from electrum_gui.common.basic import exceptions as basic_exceptions @@ -217,64 +218,81 @@ def create_primary_wallets( @timing_logger("search_existing_wallets") def search_existing_wallets( - chain_codes: List[str], + chain_code: str, mnemonic: str, passphrase: str = None, - bip44_max_searching_address_index: int = 20, + start_index: int = 0, + searching_count: int = 5, + search_count_as_requested_by_user: bool = True, ) -> List[dict]: - require(0 < bip44_max_searching_address_index <= 20) + if search_count_as_requested_by_user: + require(0 < searching_count <= 20) + else: + searching_count = 20 - result = [] master_seed = secret_manager.mnemonic_to_seed(mnemonic, passphrase=passphrase) - for chain_code in chain_codes: - chain_info = coin_manager.get_chain_info(chain_code) - candidates: List[dict] = [] - - with timing_logger(f"search_existing_{chain_code}_wallets"): - for address_encoding, path in _generate_searching_bip44_address_paths( - chain_info, bip44_max_searching_address_index=bip44_max_searching_address_index - ): - verifier = secret_manager.raw_create_key_by_master_seed(chain_info.curve, master_seed, path) - address = provider_manager.pubkey_to_address(chain_code, verifier, encoding=address_encoding) - candidates.append( - { - "chain_code": chain_code, - "bip44_path": path, - "address_encoding": address_encoding, - "address": address, - } - ) - - existing_wallets = [] - for candidate in candidates: - try: - address_info = provider_manager.get_address(candidate["chain_code"], candidate["address"]) - candidate["balance"] = address_info.balance - if address_info.existing: - existing_wallets.append(candidate) - except Exception as e: - logger.exception(f"Error in get address. chain_code: {chain_code}, address: {address}, error: {e}") + chain_info = coin_manager.get_chain_info(chain_code) + candidates: List[dict] = [] + + with timing_logger(f"search_existing_{chain_code}_wallets"): + for address_encoding, path in generate_searching_bip44_address_paths( + chain_info, start_index=start_index, searching_count=searching_count + ): + verifier = secret_manager.raw_create_key_by_master_seed(chain_info.curve, master_seed, path) + address = provider_manager.pubkey_to_address(chain_code, verifier, encoding=address_encoding) + candidates.append( + { + "chain_code": chain_code, + "bip44_path": path, + "address_encoding": address_encoding, + "address": address, + } + ) - if existing_wallets: - existing_wallets = [ - { - "name": f"{wallet['chain_code'].upper()}-{index + 1}", - **wallet, - } - for index, wallet in enumerate(existing_wallets) - ] - result.extend(existing_wallets) - else: - first_wallet = candidates[0] - first_wallet["name"] = f"{first_wallet['chain_code'].upper()}-1" - result.append(first_wallet) + addresses = [] + addresses_path_map = {} + found_info = [] + for candidate in candidates: + addresses.append(candidate["address"]) + addresses_path_map[candidate["address"]] = candidate["bip44_path"] + + try: + for batch in chunked(addresses, 100): + found_info += provider_manager.batch_get_address(candidate["chain_code"], batch) + except Exception as e: + logger.exception(f"Error in batch get address. chain_code: {chain_code}, error: {e}") + + result = {"chain_code": chain_code} + result.update({"next": start_index + searching_count}) + found_wallet_info = {} + for info in found_info: + if info.existing: + if chain_code == "btc": + find_path = ( + bip44.BIP44Path.from_bip44_path(addresses_path_map[info.address]) + .to_target_level(bip44.BIP44Level.ACCOUNT) + .to_bip44_path() + ) + else: + find_path = addresses_path_map[info.address] + sub_path_info = { + "path": addresses_path_map[info.address], + "address": info.address, + "balance": info.balance, + } + if find_path in found_wallet_info: + found_wallet_info[find_path]["sub_path"].append(sub_path_info) + else: + found_wallet_info[find_path] = {"sub_path": [sub_path_info]} + + result.update({"found_wallet_info": found_wallet_info}) return result -def _generate_searching_bip44_address_paths( - chain_info: coin_data.ChainInfo, bip44_account: int = 0, bip44_max_searching_address_index: int = 20 +def generate_searching_bip44_address_paths( + chain_info: coin_data.ChainInfo, start_index: int = 0, searching_count: int = 20 ) -> Iterable[Union[str, str]]: options = chain_info.bip44_purpose_options or {} default_address_encoding = chain_info.default_address_encoding @@ -284,19 +302,30 @@ def _generate_searching_bip44_address_paths( elif default_address_encoding in options: options = {default_address_encoding: options.pop(default_address_encoding), **options} - last_hardened_level = chain_info.bip44_last_hardened_level - target_level = chain_info.bip44_target_level for encoding, purpose in options.items(): - ins = bip44.BIP44Path( - purpose=purpose, - coin_type=chain_info.bip44_coin_type, - account=bip44_account, - last_hardened_level=last_hardened_level, - ).to_target_level(target_level) - - for _ in range(bip44_max_searching_address_index): - yield encoding, ins.to_bip44_path() - ins = ins.next_sibling() + path = ( + bip44.BIP44Path( + purpose=purpose, + coin_type=chain_info.bip44_coin_type, + account=0, + last_hardened_level=chain_info.bip44_last_hardened_level, + ) + .to_target_level(chain_info.bip44_target_level) + .next_sibling(start_index, level=chain_info.bip44_auto_increment_level) + ) + + for _ in range(searching_count): + yield encoding, path.to_bip44_path() + if chain_info.chain_affinity == codes.BTC: + sub_address = path + for _ in range(19): + sub_address = sub_address.next_sibling() + yield encoding, sub_address.to_bip44_path() + sub_change = path.next_sibling(level=bip44.BIP44Level.CHANGE) + for _ in range(20): + yield encoding, sub_change.to_bip44_path() + sub_change = sub_change.next_sibling() + path = path.next_sibling(level=chain_info.bip44_auto_increment_level) @_require_primary_wallet_not_exists()