Skip to content
This repository has been archived by the owner on Jan 29, 2023. It is now read-only.

Commit

Permalink
New software recovery process (supports btc batch requests)
Browse files Browse the repository at this point in the history
  • Loading branch information
shuaiLiWang committed Sep 8, 2021
1 parent fed94eb commit 40cd797
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 55 deletions.
21 changes: 19 additions & 2 deletions electrum/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -57,7 +57,7 @@
from .i18n import _
from .logging import Logger
from .transaction import Transaction

from electrum_gui.common.provider.data import Address
if TYPE_CHECKING:
from .network import Network
from .simple_config import SimpleConfig
Expand Down Expand Up @@ -923,6 +923,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")
Expand All @@ -941,6 +942,22 @@ async def listunspent_for_scripthash(self, sh: str) -> List[dict]:
assert_hash256_str(utxo_item['tx_hash'])
return res

async def batch_get_wallet_status(self, addresses: List[str]) -> List[dict]:
_call_body = []
async with self.session.send_batch() as batch:
for address in addresses:
batch.add_request("blockchain.scripthash.get_balance", ([bitcoin.address_to_scripthash(address)]))
batch.add_request("blockchain.scripthash.get_history", ([bitcoin.address_to_scripthash(address)]))
_resp_body = []
result_iterator = iter(batch.results)
for _address, _balance_str, _nonce_str in zip(addresses, result_iterator, result_iterator):
_balance = _balance_str["confirmed"]+_balance_str["unconfirmed"]
_nonce = len(_nonce_str)
_resp_body.append(
Address(address=_address, balance=_balance, nonce=_nonce, existing=(bool(_balance) or bool(_nonce)))
)
return _resp_body

async def get_balance_for_scripthash(self, sh: str) -> dict:
if not is_hash256_str(sh):
raise Exception(f"{repr(sh)} is not a scripthash")
Expand Down
5 changes: 5 additions & 0 deletions electrum/network.py
Original file line number Diff line number Diff line change
Expand Up @@ -1035,6 +1035,11 @@ async def get_history_for_scripthash(self, sh: str) -> List[dict]:
async def listunspent_for_scripthash(self, sh: str) -> List[dict]:
return await self.interface.listunspent_for_scripthash(sh)

@best_effort_reliable
@catch_server_exceptions
async def batch_get_wallet_status(self, sh: List[str]) -> List[dict]:
return await self.interface.batch_get_wallet_status(sh)

@best_effort_reliable
@catch_server_exceptions
async def get_balance_for_scripthash(self, sh: str) -> dict:
Expand Down
14 changes: 14 additions & 0 deletions electrum_gui/android/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -5345,6 +5345,20 @@ def network_list(self, params):

return ret

def find_accounts(
self, password, chain_code, begin_num, search_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,
begin_num=begin_num,
search_count=search_count,
search_count_as_requested_by_user=search_count_as_requested_by_user,
network=self.network,
)
return data


all_commands = commands.known_commands.copy()
for name, func in vars(AndroidCommands).items():
Expand Down
9 changes: 7 additions & 2 deletions electrum_gui/common/basic/bip44.py
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down
138 changes: 87 additions & 51 deletions electrum_gui/common/wallet/manager.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import asyncio
import collections
import contextlib
import datetime
Expand Down Expand Up @@ -217,64 +218,86 @@ 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,
begin_num: int = 0,
search_count: int = 5,
search_count_as_requested_by_user: bool = False,
network=None,
) -> List[dict]:
require(0 < bip44_max_searching_address_index <= 20)
if search_count_as_requested_by_user:
require(0 < search_count <= 20)
else:
search_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,
}
)
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_account=begin_num, searching_count=search_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,
}
)

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}")
addresses = []
addresses_path_map = {}
found_info = []
for candidate in candidates:
addresses.append(candidate["address"])
addresses_path_map[candidate["address"]] = candidate["bip44_path"]

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)
try:
if chain_code == "btc":
try:
found_info = asyncio.run_coroutine_threadsafe(
network.batch_get_wallet_status(addresses), network.asyncio_loop
).result()
except BaseException:
found_info = []
else:
first_wallet = candidates[0]
first_wallet["name"] = f"{first_wallet['chain_code'].upper()}-1"
result.append(first_wallet)
found_info = provider_manager.batch_get_address(candidate["chain_code"], addresses)
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": begin_num + search_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]

if find_path in found_wallet_info:
balance = found_wallet_info[find_path] + info.balance
else:
balance = info.balance
found_wallet_info[find_path] = balance

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
chain_info: coin_data.ChainInfo, bip44_account: 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
Expand All @@ -284,19 +307,32 @@ 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
if chain_info.chain_code == "eth":
address_index = bip44_account
bip44_account = 0
elif chain_info.chain_code == "btc":
address_index = 0
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):
change=0,
address_index=address_index,
last_hardened_level=chain_info.bip44_last_hardened_level,
).to_target_level(chain_info.bip44_target_level)
for _ in range(searching_count):
yield encoding, ins.to_bip44_path()
ins = ins.next_sibling()
if chain_info.chain_affinity == codes.BTC:
sub_address = ins
sub_change = ins.next_sibling(level=bip44.BIP44Level.CHANGE)
yield encoding, sub_change.to_bip44_path()
for _ in range(19):
sub_address = sub_address.next_sibling()
sub_change = sub_change.next_sibling()
yield encoding, sub_address.to_bip44_path()
yield encoding, sub_change.to_bip44_path()
ins = ins.next_sibling(level=chain_info.bip44_auto_increment_level)


@_require_primary_wallet_not_exists()
Expand Down

0 comments on commit 40cd797

Please sign in to comment.