260 lines
8.1 KiB
Python
260 lines
8.1 KiB
Python
|
|
import sys
|
||
|
|
import os
|
||
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
from src.single_wallet import encrypt_single_wallet, decrypt_single_wallet
|
||
|
|
from src.p2pkh import generate_legacy_address
|
||
|
|
from src.p2wpkh import generate_segwit_address
|
||
|
|
from src.p2tr import generate_p2tr_address
|
||
|
|
from src.p2pk import generate_p2pk
|
||
|
|
|
||
|
|
SECRETSCAN_URL = "https://secretscan.org/Bitcoin?address={}"
|
||
|
|
|
||
|
|
PASSWORD = "correct_horse_battery_staple"
|
||
|
|
|
||
|
|
|
||
|
|
# --- Fixtures ---
|
||
|
|
|
||
|
|
def _p2pkh():
|
||
|
|
return generate_legacy_address('mainnet')
|
||
|
|
|
||
|
|
def _p2wpkh():
|
||
|
|
return generate_segwit_address('mainnet')
|
||
|
|
|
||
|
|
def _p2tr():
|
||
|
|
return generate_p2tr_address('mainnet')
|
||
|
|
|
||
|
|
def _p2pk():
|
||
|
|
return generate_p2pk('mainnet', compressed=True)
|
||
|
|
|
||
|
|
|
||
|
|
# --- encrypt_single_wallet: output structure ---
|
||
|
|
|
||
|
|
def test_encrypt_sets_use_encryption_flag():
|
||
|
|
wallet = _p2pkh()
|
||
|
|
encrypted = encrypt_single_wallet(wallet, PASSWORD)
|
||
|
|
assert encrypted['use_encryption'] is True
|
||
|
|
|
||
|
|
|
||
|
|
def test_encrypt_returns_dict():
|
||
|
|
wallet = _p2pkh()
|
||
|
|
result = encrypt_single_wallet(wallet, PASSWORD)
|
||
|
|
assert isinstance(result, dict)
|
||
|
|
|
||
|
|
|
||
|
|
# --- encrypt_single_wallet: sensitive fields are encrypted ---
|
||
|
|
|
||
|
|
def test_encrypt_private_key_hex_changes():
|
||
|
|
wallet = _p2pkh()
|
||
|
|
encrypted = encrypt_single_wallet(wallet, PASSWORD)
|
||
|
|
assert encrypted['private_key_hex'] != wallet['private_key_hex']
|
||
|
|
|
||
|
|
|
||
|
|
def test_encrypt_private_key_wif_changes():
|
||
|
|
wallet = _p2pkh()
|
||
|
|
encrypted = encrypt_single_wallet(wallet, PASSWORD)
|
||
|
|
assert encrypted['private_key_wif'] != wallet['private_key_wif']
|
||
|
|
|
||
|
|
|
||
|
|
# --- encrypt_single_wallet: public fields are preserved ---
|
||
|
|
|
||
|
|
def test_encrypt_preserves_address():
|
||
|
|
wallet = _p2pkh()
|
||
|
|
encrypted = encrypt_single_wallet(wallet, PASSWORD)
|
||
|
|
assert encrypted['address'] == wallet['address']
|
||
|
|
|
||
|
|
|
||
|
|
def test_encrypt_preserves_public_key_hex():
|
||
|
|
wallet = _p2pkh()
|
||
|
|
encrypted = encrypt_single_wallet(wallet, PASSWORD)
|
||
|
|
assert encrypted['public_key_hex'] == wallet['public_key_hex']
|
||
|
|
|
||
|
|
|
||
|
|
def test_encrypt_preserves_network():
|
||
|
|
wallet = _p2pkh()
|
||
|
|
encrypted = encrypt_single_wallet(wallet, PASSWORD)
|
||
|
|
assert encrypted['network'] == wallet['network']
|
||
|
|
|
||
|
|
|
||
|
|
def test_encrypt_preserves_script_type():
|
||
|
|
wallet = _p2pkh()
|
||
|
|
encrypted = encrypt_single_wallet(wallet, PASSWORD)
|
||
|
|
assert encrypted['script_type'] == wallet['script_type']
|
||
|
|
|
||
|
|
|
||
|
|
# --- encrypt_single_wallet: immutability ---
|
||
|
|
|
||
|
|
def test_encrypt_does_not_mutate_original_private_key_hex():
|
||
|
|
wallet = _p2pkh()
|
||
|
|
original_key = wallet['private_key_hex']
|
||
|
|
encrypt_single_wallet(wallet, PASSWORD)
|
||
|
|
assert wallet['private_key_hex'] == original_key
|
||
|
|
|
||
|
|
|
||
|
|
def test_encrypt_does_not_mutate_original_private_key_wif():
|
||
|
|
wallet = _p2pkh()
|
||
|
|
original_wif = wallet['private_key_wif']
|
||
|
|
encrypt_single_wallet(wallet, PASSWORD)
|
||
|
|
assert wallet['private_key_wif'] == original_wif
|
||
|
|
|
||
|
|
|
||
|
|
def test_encrypt_does_not_set_flag_on_original():
|
||
|
|
wallet = _p2pkh()
|
||
|
|
assert 'use_encryption' not in wallet
|
||
|
|
encrypt_single_wallet(wallet, PASSWORD)
|
||
|
|
assert 'use_encryption' not in wallet
|
||
|
|
|
||
|
|
|
||
|
|
# --- encrypt_single_wallet: error cases ---
|
||
|
|
|
||
|
|
def test_encrypt_empty_password_raises_value_error():
|
||
|
|
wallet = _p2pkh()
|
||
|
|
with pytest.raises(ValueError):
|
||
|
|
encrypt_single_wallet(wallet, "")
|
||
|
|
|
||
|
|
|
||
|
|
# --- decrypt_single_wallet: round-trips ---
|
||
|
|
|
||
|
|
def test_decrypt_round_trip_private_key_hex():
|
||
|
|
wallet = _p2pkh()
|
||
|
|
encrypted = encrypt_single_wallet(wallet, PASSWORD)
|
||
|
|
decrypted = decrypt_single_wallet(encrypted, PASSWORD)
|
||
|
|
assert decrypted['private_key_hex'] == wallet['private_key_hex']
|
||
|
|
|
||
|
|
|
||
|
|
def test_decrypt_round_trip_private_key_wif():
|
||
|
|
wallet = _p2pkh()
|
||
|
|
encrypted = encrypt_single_wallet(wallet, PASSWORD)
|
||
|
|
decrypted = decrypt_single_wallet(encrypted, PASSWORD)
|
||
|
|
assert decrypted['private_key_wif'] == wallet['private_key_wif']
|
||
|
|
|
||
|
|
|
||
|
|
def test_decrypt_clears_use_encryption_flag():
|
||
|
|
wallet = _p2pkh()
|
||
|
|
encrypted = encrypt_single_wallet(wallet, PASSWORD)
|
||
|
|
decrypted = decrypt_single_wallet(encrypted, PASSWORD)
|
||
|
|
assert decrypted['use_encryption'] is False
|
||
|
|
|
||
|
|
|
||
|
|
def test_decrypt_preserves_address():
|
||
|
|
wallet = _p2pkh()
|
||
|
|
encrypted = encrypt_single_wallet(wallet, PASSWORD)
|
||
|
|
decrypted = decrypt_single_wallet(encrypted, PASSWORD)
|
||
|
|
assert decrypted['address'] == wallet['address']
|
||
|
|
|
||
|
|
|
||
|
|
def test_decrypt_preserves_public_key():
|
||
|
|
wallet = _p2pkh()
|
||
|
|
encrypted = encrypt_single_wallet(wallet, PASSWORD)
|
||
|
|
decrypted = decrypt_single_wallet(encrypted, PASSWORD)
|
||
|
|
assert decrypted['public_key_hex'] == wallet['public_key_hex']
|
||
|
|
|
||
|
|
|
||
|
|
# --- decrypt_single_wallet: unencrypted wallet is a no-op ---
|
||
|
|
|
||
|
|
def test_decrypt_unencrypted_wallet_noop():
|
||
|
|
wallet = _p2pkh()
|
||
|
|
result = decrypt_single_wallet(wallet, PASSWORD)
|
||
|
|
assert result['private_key_hex'] == wallet['private_key_hex']
|
||
|
|
|
||
|
|
|
||
|
|
def test_decrypt_unencrypted_wallet_returns_copy():
|
||
|
|
wallet = _p2pkh()
|
||
|
|
result = decrypt_single_wallet(wallet, PASSWORD)
|
||
|
|
assert result is not wallet
|
||
|
|
|
||
|
|
|
||
|
|
# --- decrypt_single_wallet: immutability ---
|
||
|
|
|
||
|
|
def test_decrypt_does_not_mutate_encrypted_wallet():
|
||
|
|
wallet = _p2pkh()
|
||
|
|
encrypted = encrypt_single_wallet(wallet, PASSWORD)
|
||
|
|
original_enc_key = encrypted['private_key_hex']
|
||
|
|
decrypt_single_wallet(encrypted, PASSWORD)
|
||
|
|
assert encrypted['private_key_hex'] == original_enc_key
|
||
|
|
|
||
|
|
|
||
|
|
# --- decrypt_single_wallet: error cases ---
|
||
|
|
|
||
|
|
def test_decrypt_wrong_password_raises_value_error():
|
||
|
|
wallet = _p2pkh()
|
||
|
|
encrypted = encrypt_single_wallet(wallet, PASSWORD)
|
||
|
|
with pytest.raises(ValueError):
|
||
|
|
decrypt_single_wallet(encrypted, "wrong_password")
|
||
|
|
|
||
|
|
|
||
|
|
def test_decrypt_empty_password_raises_value_error():
|
||
|
|
wallet = _p2pkh()
|
||
|
|
encrypted = encrypt_single_wallet(wallet, PASSWORD)
|
||
|
|
with pytest.raises(ValueError):
|
||
|
|
decrypt_single_wallet(encrypted, "")
|
||
|
|
|
||
|
|
|
||
|
|
# --- Works with all wallet types ---
|
||
|
|
|
||
|
|
def test_encrypt_decrypt_p2wpkh():
|
||
|
|
wallet = _p2wpkh()
|
||
|
|
encrypted = encrypt_single_wallet(wallet, PASSWORD)
|
||
|
|
decrypted = decrypt_single_wallet(encrypted, PASSWORD)
|
||
|
|
assert decrypted['private_key_hex'] == wallet['private_key_hex']
|
||
|
|
assert decrypted['address'] == wallet['address']
|
||
|
|
|
||
|
|
|
||
|
|
def test_encrypt_decrypt_p2tr():
|
||
|
|
wallet = _p2tr()
|
||
|
|
encrypted = encrypt_single_wallet(wallet, PASSWORD)
|
||
|
|
decrypted = decrypt_single_wallet(encrypted, PASSWORD)
|
||
|
|
assert decrypted['private_key_hex'] == wallet['private_key_hex']
|
||
|
|
assert decrypted['address'] == wallet['address']
|
||
|
|
|
||
|
|
|
||
|
|
def test_encrypt_decrypt_p2pk():
|
||
|
|
# P2PK has no 'address' field; only private keys are sensitive
|
||
|
|
wallet = _p2pk()
|
||
|
|
encrypted = encrypt_single_wallet(wallet, PASSWORD)
|
||
|
|
decrypted = decrypt_single_wallet(encrypted, PASSWORD)
|
||
|
|
assert decrypted['private_key_hex'] == wallet['private_key_hex']
|
||
|
|
assert decrypted['private_key_wif'] == wallet['private_key_wif']
|
||
|
|
assert decrypted['public_key_hex'] == wallet['public_key_hex']
|
||
|
|
|
||
|
|
|
||
|
|
def test_encrypt_all_networks():
|
||
|
|
for network in ('mainnet', 'testnet', 'regtest'):
|
||
|
|
wallet = generate_legacy_address(network)
|
||
|
|
encrypted = encrypt_single_wallet(wallet, PASSWORD)
|
||
|
|
decrypted = decrypt_single_wallet(encrypted, PASSWORD)
|
||
|
|
assert decrypted['private_key_hex'] == wallet['private_key_hex']
|
||
|
|
|
||
|
|
|
||
|
|
# --- Two different passwords produce different ciphertext ---
|
||
|
|
|
||
|
|
def test_different_passwords_different_ciphertext():
|
||
|
|
wallet = _p2pkh()
|
||
|
|
enc1 = encrypt_single_wallet(wallet, "password_one")
|
||
|
|
enc2 = encrypt_single_wallet(wallet, "password_two")
|
||
|
|
assert enc1['private_key_hex'] != enc2['private_key_hex']
|
||
|
|
|
||
|
|
|
||
|
|
# --- SecretScan ---
|
||
|
|
|
||
|
|
def test_single_wallet_print_secretscan(capsys):
|
||
|
|
wallet = _p2pkh()
|
||
|
|
encrypted = encrypt_single_wallet(wallet, PASSWORD)
|
||
|
|
decrypted = decrypt_single_wallet(encrypted, PASSWORD)
|
||
|
|
url = SECRETSCAN_URL.format(wallet['address'])
|
||
|
|
print(f"\n[SingleWallet] P2PKH Address: {wallet['address']}")
|
||
|
|
print(f"[SingleWallet] Verify on SecretScan: {url}")
|
||
|
|
captured = capsys.readouterr()
|
||
|
|
assert 'secretscan.org' in captured.out
|
||
|
|
assert decrypted['private_key_hex'] == wallet['private_key_hex']
|
||
|
|
|
||
|
|
|
||
|
|
def test_single_wallet_p2wpkh_print_secretscan(capsys):
|
||
|
|
wallet = _p2wpkh()
|
||
|
|
url = SECRETSCAN_URL.format(wallet['address'])
|
||
|
|
print(f"\n[SingleWallet] P2WPKH Address: {wallet['address']}")
|
||
|
|
print(f"[SingleWallet] Verify on SecretScan: {url}")
|
||
|
|
captured = capsys.readouterr()
|
||
|
|
assert 'secretscan.org' in captured.out
|