Init
This commit is contained in:
233
wallet.py
Normal file
233
wallet.py
Normal file
@@ -0,0 +1,233 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user