Files
btc-wallet-cli/wallet.py
2025-08-12 11:18:39 +03:00

234 lines
8.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import os
import json
import getpass
import gc
from pathlib import Path
from Crypto.Cipher import AES
from Crypto.Protocol.KDF import PBKDF2
from Crypto.Random import get_random_bytes
from rich import print
from rich.prompt import Prompt
from bit import Key
import requests
WALLETS_DIR = "wallets"
API_BASE_DEFAULT = "https://blockstream.info/api"
if not os.path.exists(WALLETS_DIR):
os.makedirs(WALLETS_DIR)
def encrypt_wallet(secret, password):
salt = get_random_bytes(16)
key = PBKDF2(password, salt, dkLen=32)
cipher = AES.new(key, AES.MODE_GCM)
ciphertext, tag = cipher.encrypt_and_digest(secret.encode())
return {
'ciphertext': ciphertext.hex(),
'salt': salt.hex(),
'nonce': cipher.nonce.hex(),
'tag': tag.hex()
}
def decrypt_wallet(data, password):
salt = bytes.fromhex(data['salt'])
key = PBKDF2(password, salt, dkLen=32)
cipher = AES.new(key, AES.MODE_GCM, nonce=bytes.fromhex(data['nonce']))
return cipher.decrypt_and_verify(bytes.fromhex(data['ciphertext']), bytes.fromhex(data['tag'])).decode()
api_base = API_BASE_DEFAULT
selected_wallet = None
def connect_to_network():
global api_base
url = Prompt.ask("[bold cyan]Введите REST-URL узла/провайдера (Blockstream REST по умолчанию)[/]", default=API_BASE_DEFAULT)
try:
r = requests.get(f"{url.rstrip('/')}/blocks/tip/height", timeout=6)
if r.status_code == 200:
api_base = url.rstrip('/')
print("[green1]Подключено! Используется API:[/]", api_base)
else:
print(f"[red1]Ошибка соединения: HTTP {r.status_code}[/]")
except Exception as e:
print(f"[red1]Ошибка подключения: {e}[/]")
def create_wallet():
k = Key()
save_wallet(k)
show_private_key = Prompt.ask("[bold cyan]Показать приватный ключ (WIF)?[/]", choices=["y","n"], default="n")
if show_private_key == 'y':
print(f"[orange1]{k.to_wif()}[/]")
def import_wallet():
print("[bold cyan]Введите приватный ключ (WIF): [/]", end='')
wif = getpass.getpass(prompt="")
try:
k = Key(wif)
save_wallet(k)
except Exception as e:
print(f"[red1]Не удалось импортировать ключ: {e}[/]")
def save_wallet(key_obj):
print("[bold cyan]Введите пароль для шифрования кошелька: [/]", end='')
password = getpass.getpass(prompt="")
encrypted = encrypt_wallet(key_obj.to_wif(), password)
filename = os.path.join(WALLETS_DIR, f"{key_obj.address}.json")
with open(filename, 'w') as f:
json.dump({"address": key_obj.address, "data": encrypted}, f)
print(f"[green1]Кошелек сохранен: {key_obj.address}[/]")
def select_wallet():
global selected_wallet
files = os.listdir(WALLETS_DIR)
if not files:
print("[red1]Нет доступных кошельков.[/]")
return
for i, fname in enumerate(files):
print(f"[orange1][{i+1}] {Path(fname).stem}[/]")
try:
choice = int(Prompt.ask("[bold cyan]Выберите номер кошелька[/]"))
except:
print("[red1]Неверный номер.[/]")
return
with open(os.path.join(WALLETS_DIR, files[choice-1])) as f:
selected_wallet = json.load(f)
def check_password():
return get_private_key() is not None
def get_private_key():
if not selected_wallet:
print("[red1]Кошелек не выбран.[/]")
return None
print("[bold cyan]Введите пароль от кошелька: [/]", end='')
password = getpass.getpass(prompt="")
try:
wif = decrypt_wallet(selected_wallet['data'], password)
del password
return wif
except Exception:
print("[red1]Неверный пароль![/]")
return None
def delete_wallet():
global selected_wallet
while not check_password():
pass
address = Prompt.ask("[red1]При удалении кошелька доступ к средствам будет потерян, введите адрес кошелька для подтверждения операции[/]")
if address != selected_wallet['address']:
print("[red1]Операция отменена[/]")
return
file_path = os.path.join(WALLETS_DIR, selected_wallet['address'] + '.json')
if os.path.exists(file_path):
os.remove(file_path)
selected_wallet = None
print(f"[green1]Кошелек удален[/]")
def get_balance():
if not selected_wallet:
print("[red1]Кошелек не выбран.[/]")
return
addr = selected_wallet['address']
try:
r = requests.get(f"{api_base}/address/{addr}/utxo", timeout=8)
if r.status_code != 200:
print(f"[red1]Ошибка получения UTXO: HTTP {r.status_code}[/]")
return
utxos = r.json()
balance_sats = sum([u['value'] for u in utxos])
print(f"[orange1]Баланс: {balance_sats / 1e8} BTC ({balance_sats} сатоши)[/]")
except Exception as e:
print(f"[red1]Ошибка при получении баланса: {e}[/]")
def send_transaction():
if not selected_wallet:
print("[red1]Кошелек не выбран.[/]")
return
to = Prompt.ask("[bold cyan]Введите адрес получателя[/]")
amount = float(Prompt.ask("[bold cyan]Введите сумму в BTC[/]"))
fee_per_byte = int(Prompt.ask("[bold cyan]Введите комиссию (sat/byte)[/]", default="10"))
estimated_size = int(Prompt.ask("[bold cyan]Оценочный размер транзакции в байтах[/]", default="200"))
fee_sats = fee_per_byte * estimated_size
print(f"[yellow]Ориентировочная комиссия: {fee_sats} сатоши ({fee_sats/1e8} BTC)[/]")
confirm = Prompt.ask("[bold cyan]Отправить транзакцию?[/]", choices=["y","n"], default="n")
if confirm != 'y':
print("[red1]Операция отменена[/]")
return
wif = None
while not wif:
wif = get_private_key()
try:
key = Key(wif)
outputs = [(to, amount, 'btc')]
# bit.Key.send автоматически выбирает UTXO и подписывает.
# Параметр fee в send ожидает целую сумму в сатоши (absolute fee)
tx_hash = key.send(outputs, fee=fee_sats)
print(f"[green1]Транзакция отправлена! TXID: {tx_hash}[/]")
except Exception as e:
print(f"[red1]Ошибка при отправке: {e}[/]")
finally:
del wif
gc.collect()
def main():
menu = """
[bold blue]Меню:[/]
a. Подключение к сети
c. Создание кошелька
w. Выбор кошелька
b. Просмотр баланса
s. Отправка средств
i. Импорт существующего кошелька (WIF)
d. Удаление кошелька
q. Выход
"""
print(menu)
while True:
try:
choice = Prompt.ask("[slate_blue1]Выберите действие[/]").lower()
if choice == 'a':
connect_to_network()
elif choice == 'c':
create_wallet()
elif choice == 'w':
select_wallet()
elif choice == 'b':
if selected_wallet:
get_balance()
else:
print("[red1]Необходимо выбрать кошелек[/]")
elif choice == 's':
if selected_wallet:
send_transaction()
else:
print("[red1]Необходимо выбрать кошелек[/]")
elif choice == 'i':
import_wallet()
elif choice == 'd':
if not selected_wallet:
select_wallet()
if selected_wallet:
delete_wallet()
else:
print("[red1]Необходимо выбрать кошелек[/]")
elif choice == 'q':
break
elif choice == "clear":
if os.name == 'nt':
_ = os.system('cls')
else:
_ = os.system('clear')
print(menu)
except KeyboardInterrupt:
print()
continue
if __name__ == "__main__":
main()