#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ ================================================================================ УТИЛИТА ВОССТАНОВЛЕНИЯ СТАРОГО КОШЕЛЬКА (blockchain.info / blockchain.com) ================================================================================ ЧТО ДЕЛАЕТ: Вы не знаете точно, что из записанного — seed, а что — пароль или passphrase. Утилита перебирает все правдоподобные варианты сразу: - фраза из 12 слов, или из 15/18/24 слов (берёт нужную часть автоматически); - "лишние" слова и пароль пробуются и как passphrase, и просто отдельно; - все три типа адресов: Legacy (1...), SegWit (3...), Native (bc1...); - несколько аккаунтов (0,1,2...). Для каждого варианта проверяется контрольная сумма BIP39 (отсекает опечатки), затем вычисляются адреса. По желанию утилита сама проверит их балансы. Подсказка для кошелька 2015-2016: ваши монеты тех лет — это Legacy-адреса (на "1"), потому что SegWit появился только в августе 2017. -------------------------------------------------------------------------------- БЕЗОПАСНОСТЬ — КАК ЭТО УСТРОЕНО (прочитайте): - Сид-фраза и пароль НИКУДА не отправляются. Они используются только внутри этой программы на ВАШЕМ компьютере, чтобы вычислить адреса. - В интернет (если включите проверку балансов) уходят ТОЛЬКО сами адреса — а они и так публичны, их видно в блокчейне у кого угодно. Ключи и seed не передаются никогда. - Хотите максимум приватности — поставьте CHECK_BALANCES_ONLINE = False: тогда программа вообще не выходит в сеть, а просто покажет адреса, и вы проверите их сами в блок-эксплорере. - Если впишете слова прямо в файл (см. ниже) — они хранятся на диске в открытом виде. После восстановления удалите файл или сотрите данные. - Никогда не вводите свою фразу на сайтах, в поисковике или в чатах. - Когда найдёте монеты — сразу переведите их на НОВЫЙ кошелёк. КАК ЗАПУСТИТЬ: 1) pip install bip-utils requests 2) Вариант А: впишите слова и пароль в WORDS / PASSWORD ниже в этом файле. Вариант Б: оставьте их пустыми — программа спросит при запуске. 3) python recover_wallet.py ================================================================================ """ import sys import time try: from bip_utils import ( Bip39MnemonicValidator, Bip39SeedGenerator, Bip44, Bip49, Bip84, Bip44Coins, Bip49Coins, Bip84Coins, Bip44Changes, ) except ImportError: sys.exit("Сначала установите библиотеку: pip install bip-utils requests") # =========================================================================== # ВАШИ ДАННЫЕ — можно вписать прямо сюда (между кавычек) # =========================================================================== # Вставьте ВСЕ свои слова одной строкой через пробел (включая "лишние", если # не уверены). Пароль — отдельно. Если оставить WORDS пустым (""), программа # спросит слова и пароль при запуске, как раньше. WORDS = "" # напр.: "word1 word2 word3 ... word12 extra1 extra2" PASSWORD = "" # пароль, если есть; иначе оставьте "" # =========================================================================== # --------------------------- НАСТРОЙКИ -------------------------------------- CHECK_BALANCES_ONLINE = True # False = офлайн, только показать адреса ACCOUNTS = range(0, 3) # проверять аккаунты 0,1,2 (можно расширить) GAP_LIMIT = 20 # сколько пустых адресов подряд до остановки ADDRESSES_OFFLINE = 5 # сколько адресов показать в офлайн-режиме REQUEST_DELAY = 0.3 # пауза между запросами к эксплореру, сек API = "https://blockstream.info/api/address/" # можно заменить на mempool.space # ---------------------------------------------------------------------------- VALID_LENGTHS = (12, 15, 18, 21, 24) # допустимые длины BIP39-фразы SCHEMES = [ ("Legacy (адреса на 1) m/44h/0h/xh", Bip44, Bip44Coins.BITCOIN), ("SegWit (адреса на 3) m/49h/0h/xh", Bip49, Bip49Coins.BITCOIN), ("Native (адреса на bc1) m/84h/0h/xh", Bip84, Bip84Coins.BITCOIN), ] def build_mnemonic_candidates(words): """Из набора слов делает варианты-кандидаты допустимой длины: префиксы (первые N слов) и суффиксы (последние N слов).""" cands = [] L = len(words) for n in VALID_LENGTHS: if n <= L: cands.append(words[:n]) # первые n слов if words[:n] != words[L - n:]: cands.append(words[L - n:]) # последние n слов uniq, seen = [], set() for c in cands: key = tuple(c) if key not in seen: seen.add(key) uniq.append(c) return uniq def build_passphrase_candidates(words, password): """Варианты passphrase: пусто, отдельный пароль, и 'лишние' слова строкой.""" cands = [""] if password: cands.append(password) leftover = words[12:] # слова сверх 12 if leftover: cands.append(" ".join(leftover)) out, seen = [], set() for c in cands: if c not in seen: seen.add(c) out.append(c) return out def derive_address(scheme_cls, coin, seed_bytes, account, index): master = scheme_cls.FromSeed(seed_bytes, coin) chain = (master.Purpose().Coin() .Account(account) .Change(Bip44Changes.CHAIN_EXT)) return chain.AddressIndex(index).PublicKey().ToAddress() def address_activity(addr): """Возвращает (tx_count, balance_sat) для адреса через блок-эксплорер. Передаётся ТОЛЬКО адрес — публичная информация, не ключ.""" import requests for attempt in range(3): try: r = requests.get(API + addr, timeout=20) if r.status_code == 429: # слишком много запросов time.sleep(2 + attempt * 2) continue r.raise_for_status() s = r.json().get("chain_stats", {}) tx = s.get("tx_count", 0) bal = s.get("funded_txo_sum", 0) - s.get("spent_txo_sum", 0) return tx, bal except Exception: time.sleep(1 + attempt) return None, None # не удалось проверить def scan_online(label, scheme_cls, coin, seed_bytes, pf_desc): """Сканирует путь с учётом gap-лимита, печатает найденное.""" found = [] for account in ACCOUNTS: gap, index = 0, 0 while gap < GAP_LIMIT: addr = derive_address(scheme_cls, coin, seed_bytes, account, index) tx, bal = address_activity(addr) time.sleep(REQUEST_DELAY) if tx is None: print(" (не удалось проверить %s — пропускаю)" % addr) elif tx > 0: btc = bal / 1e8 mark = " <<< БАЛАНС!" if bal > 0 else " (был, сейчас пусто)" print(" acct%d[%d] %s tx=%d %.8f BTC%s" % (account, index, addr, tx, btc, mark)) found.append((label, pf_desc, account, index, addr, tx, bal)) gap = 0 else: gap += 1 index += 1 return found def show_offline(label, scheme_cls, coin, seed_bytes): for account in ACCOUNTS: print(" Аккаунт %d:" % account) for i in range(ADDRESSES_OFFLINE): addr = derive_address(scheme_cls, coin, seed_bytes, account, i) print(" [%d] %s" % (i, addr)) def get_inputs(): """Берёт слова и пароль из файла (WORDS/PASSWORD), а если пусто — спрашивает.""" if WORDS.strip(): print("Использую данные, вписанные в файл (WORDS / PASSWORD).") return WORDS.strip(), PASSWORD.strip() raw = input("Вставьте ВСЕ слова (через пробел) и нажмите Enter:\n> ").strip() pwd = input("Пароль (если есть; иначе просто Enter):\n> ").strip() return raw, pwd def main(): print(__doc__) raw, password = get_inputs() words = raw.split() print("\nПолучено слов: %d" % len(words)) mnem_cands = build_mnemonic_candidates(words) pf_cands = build_passphrase_candidates(words, password) valid = [m for m in mnem_cands if Bip39MnemonicValidator().IsValid(" ".join(m))] if not valid: print("\n[X] Ни один вариант фразы не прошёл проверку контрольной суммы.") print(" Возможные причины: опечатка/пропущенное слово, неверный порядок,") print(" либо это не BIP39-сид (например, формат Electrum — он") print(" восстанавливается только в своём родном приложении).") print(" Перепроверьте каждое слово по списку слов BIP39 и порядок.") return print("Корректных вариантов фразы: %d (длины: %s)" % (len(valid), sorted({len(m) for m in valid}))) print("Вариантов passphrase к проверке: %d" % len(pf_cands)) if CHECK_BALANCES_ONLINE: print("\nРежим: ОНЛАЙН — проверяю балансы (в сеть уходят только адреса).") print("Это может занять несколько минут. Покажу только адреса с историей.\n") else: print("\nРежим: ОФЛАЙН — просто показываю адреса для ручной проверки.\n") all_found = [] for mnem in valid: for pf in pf_cands: pf_desc = "без passphrase" if pf == "" else ("passphrase='%s'" % pf) seed = Bip39SeedGenerator(" ".join(mnem)).Generate(pf) print("=== Фраза из %d слов | %s ===" % (len(mnem), pf_desc)) for label, cls, coin in SCHEMES: print(" %s" % label) if CHECK_BALANCES_ONLINE: all_found += scan_online(label, cls, coin, seed, pf_desc) else: show_offline(label, cls, coin, seed) print() print("=" * 78) if CHECK_BALANCES_ONLINE: if all_found: print("[OK] НАЙДЕНЫ адреса с историей. Запишите параметры — они нужны,") print(" чтобы импортировать кошелёк и вывести монеты:") for label, pf_desc, acct, idx, addr, tx, bal in all_found: print(" - %s | аккаунт %d | %s | %.8f BTC | %s" % (label.split("m/")[0].strip(), acct, pf_desc, bal/1e8, addr)) print("\n Импортируйте этот вариант в Sparrow/Electrum (тип адреса +") print(" аккаунт + passphrase как выше) и переведите всё на НОВЫЙ кошелёк.") else: print("Ни на одном адресе не нашлось истории транзакций.") print("Скорее всего, монеты лежат на ИМПОРТИРОВАННОМ адресе, который") print("сид-фраза не восстанавливает. Тогда нужен доступ к исходному") print("кошельку: восстановите Wallet ID по email на blockchain.com и") print("войдите паролем (и вторым паролем, если он был).") else: print("Проверьте показанные адреса в блок-эксплорере (mempool.space,") print("blockstream.info). Где есть баланс — это ваш вариант.") print("=" * 78) if __name__ == "__main__": main()