1# Copyright (C) 2020 The Electrum developers
2# Distributed under the MIT software license, see the accompanying
3# file LICENCE or http://www.opensource.org/licenses/mit-license.php
4
5from typing import TYPE_CHECKING
6
7from aiorpcx import TaskGroup
8
9from . import bitcoin
10from .constants import BIP39_WALLET_FORMATS
11from .bip32 import BIP32_PRIME, BIP32Node
12from .bip32 import convert_bip32_path_to_list_of_uint32 as bip32_str_to_ints
13from .bip32 import convert_bip32_intpath_to_strpath as bip32_ints_to_str
14
15if TYPE_CHECKING:
16    from .network import Network
17
18
19async def account_discovery(network: 'Network', get_account_xpub):
20    async with TaskGroup() as group:
21        account_scan_tasks = []
22        for wallet_format in BIP39_WALLET_FORMATS:
23            account_scan = scan_for_active_accounts(network, get_account_xpub, wallet_format)
24            account_scan_tasks.append(await group.spawn(account_scan))
25    active_accounts = []
26    for task in account_scan_tasks:
27        active_accounts.extend(task.result())
28    return active_accounts
29
30
31async def scan_for_active_accounts(network: 'Network', get_account_xpub, wallet_format):
32    active_accounts = []
33    account_path = bip32_str_to_ints(wallet_format["derivation_path"])
34    while True:
35        account_xpub = get_account_xpub(account_path)
36        account_node = BIP32Node.from_xkey(account_xpub)
37        has_history = await account_has_history(network, account_node, wallet_format["script_type"])
38        if has_history:
39            account = format_account(wallet_format, account_path)
40            active_accounts.append(account)
41        if not has_history or not wallet_format["iterate_accounts"]:
42            break
43        account_path[-1] = account_path[-1] + 1
44    return active_accounts
45
46
47async def account_has_history(network: 'Network', account_node: BIP32Node, script_type: str) -> bool:
48    gap_limit = 20
49    async with TaskGroup() as group:
50        get_history_tasks = []
51        for address_index in range(gap_limit):
52            address_node = account_node.subkey_at_public_derivation("0/" + str(address_index))
53            pubkey = address_node.eckey.get_public_key_hex()
54            address = bitcoin.pubkey_to_address(script_type, pubkey)
55            script = bitcoin.address_to_script(address)
56            scripthash = bitcoin.script_to_scripthash(script)
57            get_history = network.get_history_for_scripthash(scripthash)
58            get_history_tasks.append(await group.spawn(get_history))
59    for task in get_history_tasks:
60        history = task.result()
61        if len(history) > 0:
62            return True
63    return False
64
65
66def format_account(wallet_format, account_path):
67    description = wallet_format["description"]
68    if wallet_format["iterate_accounts"]:
69        account_index = account_path[-1] % BIP32_PRIME
70        description = f'{description} (Account {account_index})'
71    return {
72        "description": description,
73        "derivation_path": bip32_ints_to_str(account_path),
74        "script_type": wallet_format["script_type"],
75    }
76