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 7, 2021
1 parent fed94eb commit bddb673
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 7 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_accounts(
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
105 changes: 100 additions & 5 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 @@ -273,6 +274,87 @@ def search_existing_wallets(
return result


@timing_logger("search_existing_accounts")
def search_existing_accounts(
chain_code: str,
mnemonic: str,
passphrase: str = None,
begin_num: int = 0,
search_count: int = 5,
search_count_as_requested_by_user: bool = False,
network=None,
) -> List[dict]:
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)

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, bip44_max_searching_address_index=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,
}
)

addresses = []
addresses_path_map = {}
found_info = []
for candidate in candidates:
addresses.append(candidate["address"])
addresses_path_map[candidate["address"]] = candidate["bip44_path"]

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:
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 not 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
) -> Iterable[Union[str, str]]:
Expand All @@ -285,18 +367,31 @@ def _generate_searching_bip44_address_paths(
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
target_level = chain_info.bip44_auto_increment_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,
change=0,
address_index=address_index,
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()
if chain_info.chain_affinity == "eth":
for _ in range(bip44_max_searching_address_index):
yield encoding, ins.to_bip44_path()
ins = ins.next_sibling()
elif chain_info.chain_affinity == "btc":
for _ in range(bip44_max_searching_address_index):
for i in range(20):
yield encoding, f"{ins.to_bip44_path()}/0/{i}"
ins = ins.next_sibling()
# ins = None


@_require_primary_wallet_not_exists()
Expand Down

0 comments on commit bddb673

Please sign in to comment.