235 lines
7.9 KiB
Python
235 lines
7.9 KiB
Python
|
|
import sys
|
||
|
|
import os
|
||
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||
|
|
|
||
|
|
from src.hd_wallet import generate_hd_wallet
|
||
|
|
|
||
|
|
SECRETSCAN_URL = "https://secretscan.org/Bitcoin?address={}"
|
||
|
|
|
||
|
|
TEST_MNEMONIC = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
||
|
|
|
||
|
|
BIP_TYPES = ['bip44', 'bip49', 'bip84', 'bip86']
|
||
|
|
|
||
|
|
|
||
|
|
# --- Electrum top-level structure ---
|
||
|
|
|
||
|
|
def test_hd_top_level_fields():
|
||
|
|
result = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC)
|
||
|
|
assert set(result.keys()) == {'keystore', 'wallet_type', 'use_encryption', 'seed_type', 'seed_version', 'addresses'}
|
||
|
|
|
||
|
|
|
||
|
|
def test_hd_electrum_metadata():
|
||
|
|
result = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC)
|
||
|
|
assert result['wallet_type'] == 'standard'
|
||
|
|
assert result['use_encryption'] is False
|
||
|
|
assert result['seed_type'] == 'bip39'
|
||
|
|
assert result['seed_version'] == 17
|
||
|
|
|
||
|
|
|
||
|
|
# --- Keystore structure ---
|
||
|
|
|
||
|
|
def test_hd_keystore_fields():
|
||
|
|
result = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC)
|
||
|
|
ks = result['keystore']
|
||
|
|
assert set(ks.keys()) == {'type', 'xpub', 'xprv', 'seed', 'passphrase', 'derivation', 'root_fingerprint'}
|
||
|
|
|
||
|
|
|
||
|
|
def test_hd_keystore_type():
|
||
|
|
result = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC)
|
||
|
|
assert result['keystore']['type'] == 'bip32'
|
||
|
|
|
||
|
|
|
||
|
|
def test_hd_keystore_seed():
|
||
|
|
result = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC)
|
||
|
|
assert result['keystore']['seed'] == TEST_MNEMONIC
|
||
|
|
|
||
|
|
|
||
|
|
def test_hd_keystore_root_fingerprint_length():
|
||
|
|
result = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC)
|
||
|
|
assert len(result['keystore']['root_fingerprint']) == 8 # 4 bytes hex
|
||
|
|
|
||
|
|
|
||
|
|
def test_hd_keystore_same_root_fingerprint_across_bip_types():
|
||
|
|
# Root fingerprint derives from master key, same for all BIP types with same mnemonic
|
||
|
|
fingerprints = {
|
||
|
|
bip: generate_hd_wallet('mainnet', bip, TEST_MNEMONIC)['keystore']['root_fingerprint']
|
||
|
|
for bip in BIP_TYPES
|
||
|
|
}
|
||
|
|
assert len(set(fingerprints.values())) == 1
|
||
|
|
|
||
|
|
|
||
|
|
# --- xpub/xprv prefixes per BIP type ---
|
||
|
|
|
||
|
|
def test_hd_bip44_xpub_prefix():
|
||
|
|
ks = generate_hd_wallet('mainnet', 'bip44', TEST_MNEMONIC)['keystore']
|
||
|
|
assert ks['xpub'].startswith('xpub')
|
||
|
|
assert ks['xprv'].startswith('xprv')
|
||
|
|
|
||
|
|
|
||
|
|
def test_hd_bip49_xpub_prefix():
|
||
|
|
ks = generate_hd_wallet('mainnet', 'bip49', TEST_MNEMONIC)['keystore']
|
||
|
|
assert ks['xpub'].startswith('ypub')
|
||
|
|
assert ks['xprv'].startswith('yprv')
|
||
|
|
|
||
|
|
|
||
|
|
def test_hd_bip84_xpub_prefix():
|
||
|
|
ks = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC)['keystore']
|
||
|
|
assert ks['xpub'].startswith('zpub')
|
||
|
|
assert ks['xprv'].startswith('zprv')
|
||
|
|
|
||
|
|
|
||
|
|
def test_hd_bip86_xpub_prefix():
|
||
|
|
ks = generate_hd_wallet('mainnet', 'bip86', TEST_MNEMONIC)['keystore']
|
||
|
|
assert ks['xpub'].startswith('xpub')
|
||
|
|
assert ks['xprv'].startswith('xprv')
|
||
|
|
|
||
|
|
|
||
|
|
# --- Derivation paths ---
|
||
|
|
|
||
|
|
def test_hd_derivation_bip44_mainnet():
|
||
|
|
ks = generate_hd_wallet('mainnet', 'bip44', TEST_MNEMONIC)['keystore']
|
||
|
|
assert ks['derivation'] == "m/44'/0'/0'"
|
||
|
|
|
||
|
|
|
||
|
|
def test_hd_derivation_bip49_mainnet():
|
||
|
|
ks = generate_hd_wallet('mainnet', 'bip49', TEST_MNEMONIC)['keystore']
|
||
|
|
assert ks['derivation'] == "m/49'/0'/0'"
|
||
|
|
|
||
|
|
|
||
|
|
def test_hd_derivation_bip84_mainnet():
|
||
|
|
ks = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC)['keystore']
|
||
|
|
assert ks['derivation'] == "m/84'/0'/0'"
|
||
|
|
|
||
|
|
|
||
|
|
def test_hd_derivation_bip86_mainnet():
|
||
|
|
ks = generate_hd_wallet('mainnet', 'bip86', TEST_MNEMONIC)['keystore']
|
||
|
|
assert ks['derivation'] == "m/86'/0'/0'"
|
||
|
|
|
||
|
|
|
||
|
|
def test_hd_derivation_testnet_coin_type():
|
||
|
|
ks = generate_hd_wallet('testnet', 'bip84', TEST_MNEMONIC)['keystore']
|
||
|
|
assert ks['derivation'] == "m/84'/1'/0'"
|
||
|
|
|
||
|
|
|
||
|
|
# --- Addresses structure ---
|
||
|
|
|
||
|
|
def test_hd_addresses_keys():
|
||
|
|
result = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC)
|
||
|
|
assert set(result['addresses'].keys()) == {'receiving', 'change'}
|
||
|
|
assert result['addresses']['change'] == []
|
||
|
|
|
||
|
|
|
||
|
|
def test_hd_address_entry_fields():
|
||
|
|
result = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC, num_addresses=1)
|
||
|
|
entry = result['addresses']['receiving'][0]
|
||
|
|
assert set(entry.keys()) == {'index', 'path', 'address', 'public_key', 'private_key_wif', 'private_key_hex'}
|
||
|
|
|
||
|
|
|
||
|
|
def test_hd_address_path_format():
|
||
|
|
result = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC, num_addresses=3)
|
||
|
|
for i, entry in enumerate(result['addresses']['receiving']):
|
||
|
|
assert entry['path'] == f"m/84'/0'/0'/0/{i}"
|
||
|
|
assert entry['index'] == i
|
||
|
|
|
||
|
|
|
||
|
|
# --- Address prefixes mainnet ---
|
||
|
|
|
||
|
|
def test_hd_bip44_address_mainnet():
|
||
|
|
result = generate_hd_wallet('mainnet', 'bip44', TEST_MNEMONIC, num_addresses=1)
|
||
|
|
assert result['addresses']['receiving'][0]['address'].startswith('1')
|
||
|
|
|
||
|
|
|
||
|
|
def test_hd_bip49_address_mainnet():
|
||
|
|
result = generate_hd_wallet('mainnet', 'bip49', TEST_MNEMONIC, num_addresses=1)
|
||
|
|
assert result['addresses']['receiving'][0]['address'].startswith('3')
|
||
|
|
|
||
|
|
|
||
|
|
def test_hd_bip84_address_mainnet():
|
||
|
|
result = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC, num_addresses=1)
|
||
|
|
assert result['addresses']['receiving'][0]['address'].startswith('bc1q')
|
||
|
|
|
||
|
|
|
||
|
|
def test_hd_bip86_address_mainnet():
|
||
|
|
result = generate_hd_wallet('mainnet', 'bip86', TEST_MNEMONIC, num_addresses=1)
|
||
|
|
assert result['addresses']['receiving'][0]['address'].startswith('bc1p')
|
||
|
|
|
||
|
|
|
||
|
|
# --- Address prefixes testnet ---
|
||
|
|
|
||
|
|
def test_hd_bip44_address_testnet():
|
||
|
|
result = generate_hd_wallet('testnet', 'bip44', TEST_MNEMONIC, num_addresses=1)
|
||
|
|
assert result['addresses']['receiving'][0]['address'][0] in ('m', 'n')
|
||
|
|
|
||
|
|
|
||
|
|
def test_hd_bip84_address_testnet():
|
||
|
|
result = generate_hd_wallet('testnet', 'bip84', TEST_MNEMONIC, num_addresses=1)
|
||
|
|
assert result['addresses']['receiving'][0]['address'].startswith('tb1q')
|
||
|
|
|
||
|
|
|
||
|
|
def test_hd_bip86_address_testnet():
|
||
|
|
result = generate_hd_wallet('testnet', 'bip86', TEST_MNEMONIC, num_addresses=1)
|
||
|
|
assert result['addresses']['receiving'][0]['address'].startswith('tb1p')
|
||
|
|
|
||
|
|
|
||
|
|
# --- Determinism ---
|
||
|
|
|
||
|
|
def test_hd_same_mnemonic_same_addresses():
|
||
|
|
r1 = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC, num_addresses=3)
|
||
|
|
r2 = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC, num_addresses=3)
|
||
|
|
addrs1 = [a['address'] for a in r1['addresses']['receiving']]
|
||
|
|
addrs2 = [a['address'] for a in r2['addresses']['receiving']]
|
||
|
|
assert addrs1 == addrs2
|
||
|
|
|
||
|
|
|
||
|
|
def test_hd_same_mnemonic_same_xpub():
|
||
|
|
r1 = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC)
|
||
|
|
r2 = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC)
|
||
|
|
assert r1['keystore']['xpub'] == r2['keystore']['xpub']
|
||
|
|
|
||
|
|
|
||
|
|
def test_hd_different_mnemonic_different_xpub():
|
||
|
|
r1 = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC)
|
||
|
|
r2 = generate_hd_wallet('mainnet', 'bip84') # random mnemonic
|
||
|
|
assert r1['keystore']['xpub'] != r2['keystore']['xpub']
|
||
|
|
|
||
|
|
|
||
|
|
# --- num_addresses ---
|
||
|
|
|
||
|
|
def test_hd_num_addresses():
|
||
|
|
for n in [1, 3, 10]:
|
||
|
|
result = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC, num_addresses=n)
|
||
|
|
assert len(result['addresses']['receiving']) == n
|
||
|
|
|
||
|
|
|
||
|
|
# --- Key format ---
|
||
|
|
|
||
|
|
def test_hd_private_key_length():
|
||
|
|
result = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC, num_addresses=1)
|
||
|
|
assert len(result['addresses']['receiving'][0]['private_key_hex']) == 64
|
||
|
|
|
||
|
|
|
||
|
|
def test_hd_public_key_compressed():
|
||
|
|
result = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC, num_addresses=1)
|
||
|
|
pub = result['addresses']['receiving'][0]['public_key']
|
||
|
|
assert len(pub) == 66
|
||
|
|
assert pub[:2] in ('02', '03')
|
||
|
|
|
||
|
|
|
||
|
|
# --- Mnemonic generation ---
|
||
|
|
|
||
|
|
def test_hd_generates_mnemonic_when_none():
|
||
|
|
result = generate_hd_wallet('mainnet', 'bip84')
|
||
|
|
assert len(result['keystore']['seed'].split()) == 12
|
||
|
|
|
||
|
|
|
||
|
|
# --- SecretScan ---
|
||
|
|
|
||
|
|
def test_hd_print_secretscan(capsys):
|
||
|
|
result = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC, num_addresses=3)
|
||
|
|
for a in result['addresses']['receiving']:
|
||
|
|
url = SECRETSCAN_URL.format(a['address'])
|
||
|
|
print(f"\n[HD BIP84] [{a['index']}] {a['address']}")
|
||
|
|
print(f"[HD BIP84] Verify on SecretScan: {url}")
|
||
|
|
captured = capsys.readouterr()
|
||
|
|
assert captured.out.count('secretscan.org') == 3
|