mirror of
https://github.com/JoinMarket-Org/joinmarket-clientserver.git
synced 2026-08-16 13:01:17 +02:00
Fixes #1614. Prior to this commit, if data in the persisted cache in the wallet file were wrong (should be a very extraordinary case), then the joinmarket code would have to crash with a cache invalid warning. After this commit, in such an extraordinary case, the option exists to invalidate or remove the cache on startup, so that it can be rebuilt from scratch. This is done with a config var wallet_caching_disabled in the POLICY section.
1065 lines
42 KiB
Python
1065 lines
42 KiB
Python
'''Wallet functionality tests.'''
|
|
import datetime
|
|
import os
|
|
import json
|
|
from binascii import hexlify, unhexlify
|
|
|
|
import pytest
|
|
import jmbitcoin as btc
|
|
from commontest import ensure_bip65_activated
|
|
from jmbase import get_log, hextobin, bintohex
|
|
from jmclient import load_test_config, jm_single, BaseWallet, \
|
|
SegwitLegacyWallet,BIP32Wallet, BIP49Wallet, LegacyWallet,\
|
|
VolatileStorage, get_network, cryptoengine, WalletError,\
|
|
SegwitWallet, WalletService, SegwitWalletFidelityBonds,\
|
|
create_wallet, open_test_wallet_maybe, open_wallet, \
|
|
FidelityBondMixin, FidelityBondWatchonlyWallet,\
|
|
wallet_gettimelockaddress, UnknownAddressForLabel
|
|
from test_blockchaininterface import sync_test_wallet
|
|
from freezegun import freeze_time
|
|
|
|
pytestmark = pytest.mark.usefixtures("setup_regtest_bitcoind")
|
|
|
|
testdir = os.path.dirname(os.path.realpath(__file__))
|
|
|
|
test_create_wallet_filename = "testwallet_for_create_wallet_test"
|
|
test_cache_cleared_filename = "testwallet_for_cache_clear_test"
|
|
log = get_log()
|
|
|
|
|
|
def signed_tx_is_segwit(tx):
|
|
return tx.has_witness()
|
|
|
|
|
|
def assert_segwit(tx):
|
|
assert signed_tx_is_segwit(tx)
|
|
|
|
|
|
def assert_not_segwit(tx):
|
|
assert not signed_tx_is_segwit(tx)
|
|
|
|
|
|
def get_populated_wallet(amount=10**8, num=3):
|
|
storage = VolatileStorage()
|
|
SegwitLegacyWallet.initialize(storage, get_network())
|
|
wallet = SegwitLegacyWallet(storage)
|
|
|
|
# fund three wallet addresses at mixdepth 0
|
|
for i in range(num):
|
|
fund_wallet_addr(wallet, wallet.get_internal_addr(0), amount / 10**8)
|
|
|
|
return wallet
|
|
|
|
|
|
def fund_wallet_addr(wallet, addr, value_btc=1):
|
|
# special case, grab_coins returns hex from rpc:
|
|
txin_id = hextobin(jm_single().bc_interface.grab_coins(addr, value_btc))
|
|
txinfo = jm_single().bc_interface.get_transaction(txin_id)
|
|
txin = btc.CMutableTransaction.deserialize(btc.x(txinfo["hex"]))
|
|
utxo_in = wallet.add_new_utxos(txin, 1)
|
|
assert len(utxo_in) == 1
|
|
return list(utxo_in.keys())[0]
|
|
|
|
|
|
def get_bip39_vectors():
|
|
fh = open(os.path.join(testdir, 'bip39vectors.json'))
|
|
data = json.load(fh)['english']
|
|
fh.close()
|
|
return data
|
|
|
|
|
|
@pytest.mark.parametrize('entropy,mnemonic,key,xpriv', get_bip39_vectors())
|
|
def test_bip39_seeds(monkeypatch, setup_wallet, entropy, mnemonic, key, xpriv):
|
|
jm_single().config.set('BLOCKCHAIN', 'network', 'mainnet')
|
|
created_entropy = SegwitLegacyWallet.entropy_from_mnemonic(mnemonic)
|
|
assert entropy == hexlify(created_entropy).decode('ascii')
|
|
storage = VolatileStorage()
|
|
SegwitLegacyWallet.initialize(
|
|
storage, get_network(), entropy=created_entropy,
|
|
entropy_extension='TREZOR', max_mixdepth=4)
|
|
wallet = SegwitLegacyWallet(storage)
|
|
assert (mnemonic, b'TREZOR') == wallet.get_mnemonic_words()
|
|
assert key == hexlify(wallet._create_master_key()).decode('ascii')
|
|
|
|
# need to monkeypatch this, else we'll default to the BIP-49 path
|
|
monkeypatch.setattr(SegwitLegacyWallet, '_get_bip32_base_path',
|
|
BIP32Wallet._get_bip32_base_path)
|
|
assert xpriv == wallet.get_bip32_priv_export()
|
|
|
|
|
|
def test_bip49_seed(monkeypatch, setup_wallet):
|
|
jm_single().config.set('BLOCKCHAIN', 'network', 'testnet')
|
|
mnemonic = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'
|
|
master_xpriv = 'tprv8ZgxMBicQKsPe5YMU9gHen4Ez3ApihUfykaqUorj9t6FDqy3nP6eoXiAo2ssvpAjoLroQxHqr3R5nE3a5dU3DHTjTgJDd7zrbniJr6nrCzd'
|
|
account0_xpriv = 'tprv8gRrNu65W2Msef2BdBSUgFdRTGzC8EwVXnV7UGS3faeXtuMVtGfEdidVeGbThs4ELEoayCAzZQ4uUji9DUiAs7erdVskqju7hrBcDvDsdbY'
|
|
addr0_script_hash = '336caa13e08b96080a32b5d818d59b4ab3b36742'
|
|
|
|
entropy = SegwitLegacyWallet.entropy_from_mnemonic(mnemonic)
|
|
storage = VolatileStorage()
|
|
SegwitLegacyWallet.initialize(
|
|
storage, get_network(), entropy=entropy, max_mixdepth=0)
|
|
wallet = SegwitLegacyWallet(storage)
|
|
assert (mnemonic, None) == wallet.get_mnemonic_words()
|
|
assert account0_xpriv == wallet.get_bip32_priv_export(0)
|
|
assert addr0_script_hash == hexlify(wallet.get_external_script(0)[2:-1]).decode('ascii')
|
|
|
|
# FIXME: is this desired behaviour? BIP49 wallet will not return xpriv for
|
|
# the root key but only for key after base path
|
|
monkeypatch.setattr(SegwitLegacyWallet, '_get_bip32_base_path',
|
|
BIP32Wallet._get_bip32_base_path)
|
|
assert master_xpriv == wallet.get_bip32_priv_export()
|
|
|
|
|
|
def test_bip32_test_vector_1(monkeypatch, setup_wallet):
|
|
jm_single().config.set('BLOCKCHAIN', 'network', 'mainnet')
|
|
|
|
entropy = unhexlify('000102030405060708090a0b0c0d0e0f')
|
|
storage = VolatileStorage()
|
|
LegacyWallet.initialize(
|
|
storage, get_network(), entropy=entropy, max_mixdepth=0)
|
|
|
|
# test vector 1 is using hardened derivation for the account/mixdepth level
|
|
monkeypatch.setattr(LegacyWallet, '_get_mixdepth_from_path',
|
|
BIP49Wallet._get_mixdepth_from_path)
|
|
monkeypatch.setattr(LegacyWallet, '_get_bip32_mixdepth_path_level',
|
|
BIP49Wallet._get_bip32_mixdepth_path_level)
|
|
monkeypatch.setattr(LegacyWallet, '_get_bip32_base_path',
|
|
BIP32Wallet._get_bip32_base_path)
|
|
monkeypatch.setattr(LegacyWallet, '_create_master_key',
|
|
BIP32Wallet._create_master_key)
|
|
|
|
wallet = LegacyWallet(storage)
|
|
|
|
assert wallet.get_bip32_priv_export() == 'xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi'
|
|
assert wallet.get_bip32_pub_export() == 'xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8'
|
|
assert wallet.get_bip32_priv_export(0) == 'xprv9uHRZZhk6KAJC1avXpDAp4MDc3sQKNxDiPvvkX8Br5ngLNv1TxvUxt4cV1rGL5hj6KCesnDYUhd7oWgT11eZG7XnxHrnYeSvkzY7d2bhkJ7'
|
|
assert wallet.get_bip32_pub_export(0) == 'xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw'
|
|
assert wallet.get_bip32_priv_export(0, 1) == 'xprv9wTYmMFdV23N2TdNG573QoEsfRrWKQgWeibmLntzniatZvR9BmLnvSxqu53Kw1UmYPxLgboyZQaXwTCg8MSY3H2EU4pWcQDnRnrVA1xe8fs'
|
|
assert wallet.get_bip32_pub_export(0, 1) == 'xpub6ASuArnXKPbfEwhqN6e3mwBcDTgzisQN1wXN9BJcM47sSikHjJf3UFHKkNAWbWMiGj7Wf5uMash7SyYq527Hqck2AxYysAA7xmALppuCkwQ'
|
|
# there are more test vectors but those don't match joinmarket's wallet
|
|
# structure, hence they make litte sense to test here
|
|
|
|
|
|
def test_bip32_test_vector_2(monkeypatch, setup_wallet):
|
|
jm_single().config.set('BLOCKCHAIN', 'network', 'mainnet')
|
|
|
|
entropy = unhexlify('fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542')
|
|
storage = VolatileStorage()
|
|
LegacyWallet.initialize(
|
|
storage, get_network(), entropy=entropy, max_mixdepth=0)
|
|
|
|
monkeypatch.setattr(LegacyWallet, '_get_bip32_base_path',
|
|
BIP32Wallet._get_bip32_base_path)
|
|
monkeypatch.setattr(LegacyWallet, '_create_master_key',
|
|
BIP32Wallet._create_master_key)
|
|
|
|
wallet = LegacyWallet(storage)
|
|
|
|
assert wallet.get_bip32_priv_export() == 'xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U'
|
|
assert wallet.get_bip32_pub_export() == 'xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB'
|
|
assert wallet.get_bip32_priv_export(0) == 'xprv9vHkqa6EV4sPZHYqZznhT2NPtPCjKuDKGY38FBWLvgaDx45zo9WQRUT3dKYnjwih2yJD9mkrocEZXo1ex8G81dwSM1fwqWpWkeS3v86pgKt'
|
|
assert wallet.get_bip32_pub_export(0) == 'xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH'
|
|
# there are more test vectors but those don't match joinmarket's wallet
|
|
# structure, hence they make litte sense to test here
|
|
|
|
|
|
def test_bip32_test_vector_3(monkeypatch, setup_wallet):
|
|
jm_single().config.set('BLOCKCHAIN', 'network', 'mainnet')
|
|
|
|
entropy = unhexlify('4b381541583be4423346c643850da4b320e46a87ae3d2a4e6da11eba819cd4acba45d239319ac14f863b8d5ab5a0d0c64d2e8a1e7d1457df2e5a3c51c73235be')
|
|
storage = VolatileStorage()
|
|
LegacyWallet.initialize(
|
|
storage, get_network(), entropy=entropy, max_mixdepth=0)
|
|
|
|
# test vector 3 is using hardened derivation for the account/mixdepth level
|
|
monkeypatch.setattr(LegacyWallet, '_get_mixdepth_from_path',
|
|
BIP49Wallet._get_mixdepth_from_path)
|
|
monkeypatch.setattr(LegacyWallet, '_get_bip32_mixdepth_path_level',
|
|
BIP49Wallet._get_bip32_mixdepth_path_level)
|
|
monkeypatch.setattr(LegacyWallet, '_get_bip32_base_path',
|
|
BIP32Wallet._get_bip32_base_path)
|
|
monkeypatch.setattr(LegacyWallet, '_create_master_key',
|
|
BIP32Wallet._create_master_key)
|
|
|
|
wallet = LegacyWallet(storage)
|
|
|
|
assert wallet.get_bip32_priv_export() == 'xprv9s21ZrQH143K25QhxbucbDDuQ4naNntJRi4KUfWT7xo4EKsHt2QJDu7KXp1A3u7Bi1j8ph3EGsZ9Xvz9dGuVrtHHs7pXeTzjuxBrCmmhgC6'
|
|
assert wallet.get_bip32_pub_export() == 'xpub661MyMwAqRbcEZVB4dScxMAdx6d4nFc9nvyvH3v4gJL378CSRZiYmhRoP7mBy6gSPSCYk6SzXPTf3ND1cZAceL7SfJ1Z3GC8vBgp2epUt13'
|
|
assert wallet.get_bip32_priv_export(0) == 'xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L'
|
|
assert wallet.get_bip32_pub_export(0) == 'xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y'
|
|
|
|
|
|
@pytest.mark.parametrize('mixdepth,internal,index,address,wif', [
|
|
[0, BaseWallet.ADDRESS_TYPE_EXTERNAL, 0, 'mpCX9EbdXpcrKMtjEe1fqFhvzctkfzMYTX', 'cVqtSSoVxFyPqTRGfeESi31uCYfgTF4tGWRtGeVs84fzybiX5TPk'],
|
|
[0, BaseWallet.ADDRESS_TYPE_EXTERNAL, 5, 'mtj85a3pFppRhrxNcFig1k7ECshrZjJ9XC', 'cMsFXc4TRw9PTcCTv7x9mr88rDeGXBTLEV67mKaw2cxCkjkhL32G'],
|
|
[0, BaseWallet.ADDRESS_TYPE_INTERNAL, 3, 'n1EaQuqvTRm719hsSJ7yRsj49JfoG1C86q', 'cUgSTqnAtvYoQRXCYy4wCFfaks2Zrz1d55m6mVhFyVhQbkDi7JGJ'],
|
|
[2, BaseWallet.ADDRESS_TYPE_INTERNAL, 2, 'mfxkBk7uDhmF5PJGS9d1NonGiAxPwJqQP4', 'cPcZXSiXPuS5eiT4oDrDKi1mFumw5D1RcWzK2gkGdEHjEz99eyXn']
|
|
])
|
|
def test_bip32_addresses_p2pkh(monkeypatch, setup_wallet, mixdepth, internal, index, address, wif):
|
|
"""
|
|
Test with a random but fixed entropy
|
|
"""
|
|
jm_single().config.set('BLOCKCHAIN', 'network', 'testnet')
|
|
|
|
entropy = unhexlify('2e0339ba89b4a1272cdf78b27ee62669ee01992a59e836e2807051be128ca817')
|
|
storage = VolatileStorage()
|
|
LegacyWallet.initialize(
|
|
storage, get_network(), entropy=entropy, max_mixdepth=3)
|
|
|
|
monkeypatch.setattr(LegacyWallet, '_get_bip32_base_path',
|
|
BIP32Wallet._get_bip32_base_path)
|
|
monkeypatch.setattr(LegacyWallet, '_create_master_key',
|
|
BIP32Wallet._create_master_key)
|
|
|
|
wallet = LegacyWallet(storage)
|
|
|
|
# wallet needs to know about all intermediate keys
|
|
for i in range(index + 1):
|
|
wallet.get_new_script(mixdepth, internal)
|
|
|
|
assert wif == wallet.get_wif(mixdepth, internal, index)
|
|
assert address == wallet.get_addr(mixdepth, internal, index)
|
|
|
|
|
|
@pytest.mark.parametrize('mixdepth,internal,index,address,wif', [
|
|
[0, 0, 0, '2MzY5yyonUY7zpHspg7jB7WQs1uJxKafQe4', 'cRAGLvPmhpzJNgdMT4W2gVwEW3fusfaDqdQWM2vnWLgXKzCWKtcM'],
|
|
[0, 0, 5, '2MsKvqPGStp3yXT8UivuAaGwfPzT7xYwSWk', 'cSo3h7nRuV4fwhVPXeTDJx6cBCkjAzS9VM8APXViyjoSaMq85ZKn'],
|
|
[0, 1, 3, '2N7k6wiQqkuMaApwGhk3HKrifprUSDydqUv', 'cTwq3UsZa8STVmwZR94dDphgqgdLFeuaRFD1Ea44qjbjFfKEb1n5'],
|
|
[2, 1, 2, '2MtE6gzHgmEXeWzKsmCJFEqkrpNuBDvoRnz', 'cPV8FZuCvrRpk4RhmhpjnSucHhaQZUan4Vbyo1NVQtuAxurW9grb']
|
|
])
|
|
def test_bip32_addresses_p2sh_p2wpkh(setup_wallet, mixdepth, internal, index, address, wif):
|
|
"""
|
|
Test with a random but fixed entropy
|
|
"""
|
|
jm_single().config.set('BLOCKCHAIN', 'network', 'testnet')
|
|
|
|
entropy = unhexlify('2e0339ba89b4a1272cdf78b27ee62669ee01992a59e836e2807051be128ca817')
|
|
storage = VolatileStorage()
|
|
SegwitLegacyWallet.initialize(
|
|
storage, get_network(), entropy=entropy, max_mixdepth=3)
|
|
wallet = SegwitLegacyWallet(storage)
|
|
|
|
# wallet needs to know about all intermediate keys
|
|
for i in range(index + 1):
|
|
wallet.get_new_script(mixdepth, internal)
|
|
|
|
assert wif == wallet.get_wif(mixdepth, internal, index)
|
|
assert address == wallet.get_addr(mixdepth, internal, index)
|
|
|
|
|
|
@pytest.mark.parametrize('timenumber,address,wif', [
|
|
[0, 'bcrt1qgysu2eynn6klarz200ctgev7gqhhp7hwsdaaec3c7h0ltmc3r68q87c2d3', 'cVASAS6bpC5yctGmnsKaDz7D8CxEwccUtpjSNBQzeV2fw8ox8RR9'],
|
|
[50, 'bcrt1q0cnscj0hlf6xqzlqwk7swngd3kmvd6unn49j9h4zgg68kg8fd7gq0r87lf', 'cMtnaLzC2EW3URnmAapRnPQECGwGruxqXJpAnuRjKup3pkWfrxRE'],
|
|
[1, 'bcrt1q26vw0q28rz2r2ktehp8w5yfzkzskrc4fxqdhzjy0f88kzhjvlfrs7fyas6', 'cU8G1YAAxGZMqNsXxApBAahb8pbxhxryDshFdX5eRT9FV4gHNVXT']
|
|
])
|
|
def test_bip32_timelocked_addresses(setup_wallet, timenumber, address, wif):
|
|
jm_single().config.set('BLOCKCHAIN', 'network', 'testnet')
|
|
|
|
entropy = unhexlify('2e0339ba89b4a1272cdf78b27ee62669ee01992a59e836e2807051be128ca817')
|
|
storage = VolatileStorage()
|
|
SegwitWalletFidelityBonds.initialize(
|
|
storage, get_network(), entropy=entropy, max_mixdepth=1)
|
|
wallet = SegwitWalletFidelityBonds(storage)
|
|
mixdepth = FidelityBondMixin.FIDELITY_BOND_MIXDEPTH
|
|
address_type = FidelityBondMixin.BIP32_TIMELOCK_ID
|
|
|
|
assert address == wallet.get_addr(mixdepth, address_type, timenumber)
|
|
assert wif == wallet.get_wif_path(wallet.get_path(mixdepth, address_type, timenumber))
|
|
|
|
|
|
@pytest.mark.parametrize('timenumber,locktime_string', [
|
|
[0, "2020-01"],
|
|
[20, "2021-09"],
|
|
[100, "2028-05"],
|
|
[150, "2032-07"],
|
|
[350, "2049-03"]
|
|
])
|
|
@freeze_time("2019-12")
|
|
def test_gettimelockaddress_method(setup_wallet, timenumber, locktime_string):
|
|
jm_single().config.set("BLOCKCHAIN", "network", "mainnet")
|
|
storage = VolatileStorage()
|
|
SegwitWalletFidelityBonds.initialize(storage, get_network())
|
|
wallet = SegwitWalletFidelityBonds(storage)
|
|
|
|
m = FidelityBondMixin.FIDELITY_BOND_MIXDEPTH
|
|
address_type = FidelityBondMixin.BIP32_TIMELOCK_ID
|
|
script = wallet.get_script(m, address_type, timenumber)
|
|
addr = wallet.script_to_addr(script)
|
|
|
|
addr_from_method = wallet_gettimelockaddress(wallet, locktime_string)
|
|
|
|
assert addr == addr_from_method
|
|
|
|
|
|
@freeze_time("2021-01")
|
|
def test_gettimelockaddress_in_past(setup_wallet):
|
|
jm_single().config.set("BLOCKCHAIN", "network", "mainnet")
|
|
storage = VolatileStorage()
|
|
SegwitWalletFidelityBonds.initialize(storage, get_network())
|
|
wallet = SegwitWalletFidelityBonds(storage)
|
|
|
|
assert wallet_gettimelockaddress(wallet, "2020-01") == ""
|
|
assert wallet_gettimelockaddress(wallet, "2021-01") == ""
|
|
assert wallet_gettimelockaddress(wallet, "2021-02") != ""
|
|
|
|
|
|
@pytest.mark.parametrize('index,wif', [
|
|
[0, 'cVQbz7DB5JQ1TGsg9Dbm32VtJbXBHaj39Yc9QLkaGpRgXcibHTDH'],
|
|
[9, 'cULqe2sYZ4z8jZTGybr2Bzf4EyiT5Ts6wAE3mvCUofRuTVsofR8N'],
|
|
[50, 'cQNp7cQbrwjWuxmbkZF8ax9ogmTuWp3Ykb9LEpainhRTJXYc8Deu']
|
|
])
|
|
def test_bip32_burn_keys(setup_wallet, index, wif):
|
|
jm_single().config.set('BLOCKCHAIN', 'network', 'testnet')
|
|
|
|
entropy = unhexlify('2e0339ba89b4a1272cdf78b27ee62669ee01992a59e836e2807051be128ca817')
|
|
storage = VolatileStorage()
|
|
SegwitWalletFidelityBonds.initialize(
|
|
storage, get_network(), entropy=entropy, max_mixdepth=1)
|
|
wallet = SegwitWalletFidelityBonds(storage)
|
|
mixdepth = FidelityBondMixin.FIDELITY_BOND_MIXDEPTH
|
|
address_type = FidelityBondMixin.BIP32_BURN_ID
|
|
|
|
#advance index_cache enough
|
|
wallet.set_next_index(mixdepth, address_type, index, force=True)
|
|
|
|
assert wif == wallet.get_wif_path(wallet.get_path(mixdepth, address_type, index))
|
|
|
|
|
|
def test_import_key(setup_wallet):
|
|
jm_single().config.set('BLOCKCHAIN', 'network', 'testnet')
|
|
storage = VolatileStorage()
|
|
SegwitLegacyWallet.initialize(storage, get_network())
|
|
wallet = SegwitLegacyWallet(storage)
|
|
|
|
wallet.import_private_key(
|
|
0, 'cRAGLvPmhpzJNgdMT4W2gVwEW3fusfaDqdQWM2vnWLgXKzCWKtcM')
|
|
wallet.import_private_key(
|
|
1, 'cVqtSSoVxFyPqTRGfeESi31uCYfgTF4tGWRtGeVs84fzybiX5TPk')
|
|
|
|
with pytest.raises(WalletError):
|
|
wallet.import_private_key(
|
|
1, 'cRAGLvPmhpzJNgdMT4W2gVwEW3fusfaDqdQWM2vnWLgXKzCWKtcM')
|
|
|
|
# test persist imported keys
|
|
wallet.save()
|
|
data = storage.file_data
|
|
|
|
del wallet
|
|
del storage
|
|
|
|
storage = VolatileStorage(data=data)
|
|
wallet = SegwitLegacyWallet(storage)
|
|
|
|
imported_paths_md0 = list(wallet.yield_imported_paths(0))
|
|
imported_paths_md1 = list(wallet.yield_imported_paths(1))
|
|
assert len(imported_paths_md0) == 1
|
|
assert len(imported_paths_md1) == 1
|
|
|
|
# verify imported addresses
|
|
assert wallet.get_address_from_path(imported_paths_md0[0]) == '2MzY5yyonUY7zpHspg7jB7WQs1uJxKafQe4'
|
|
assert wallet.get_address_from_path(imported_paths_md1[0]) == '2MwbXnJrPP4rnwpgRhvNPP44J6tMokDexZB'
|
|
|
|
# test remove key
|
|
wallet.remove_imported_key(path=imported_paths_md0[0])
|
|
assert not list(wallet.yield_imported_paths(0))
|
|
|
|
assert wallet.get_details(imported_paths_md1[0]) == (1, 'imported', 0)
|
|
|
|
|
|
@pytest.mark.parametrize('wif, type_check', [
|
|
['cRAGLvPmhpzJNgdMT4W2gVwEW3fusfaDqdQWM2vnWLgXKzCWKtcM', assert_segwit]
|
|
])
|
|
def test_signing_imported(setup_wallet, wif, type_check):
|
|
jm_single().config.set('BLOCKCHAIN', 'network', 'testnet')
|
|
storage = VolatileStorage()
|
|
SegwitLegacyWallet.initialize(storage, get_network())
|
|
wallet = SegwitLegacyWallet(storage)
|
|
|
|
MIXDEPTH = 0
|
|
path = wallet.import_private_key(MIXDEPTH, wif)
|
|
utxo = fund_wallet_addr(wallet, wallet.get_address_from_path(path))
|
|
# The dummy output is constructed as an unspendable p2sh:
|
|
tx = btc.mktx([utxo],
|
|
[{"address": str(btc.CCoinAddress.from_scriptPubKey(
|
|
btc.CScript(b"\x00").to_p2sh_scriptPubKey())),
|
|
"value": 10**8 - 9000}])
|
|
script = wallet.get_script_from_path(path)
|
|
success, msg = wallet.sign_tx(tx, {0: (script, 10**8)})
|
|
assert success, msg
|
|
type_check(tx)
|
|
txout = jm_single().bc_interface.pushtx(tx.serialize())
|
|
assert txout
|
|
|
|
|
|
@pytest.mark.parametrize('wallet_cls,type_check', [
|
|
[LegacyWallet, assert_not_segwit],
|
|
[SegwitLegacyWallet, assert_segwit],
|
|
[SegwitWallet, assert_segwit],
|
|
])
|
|
def test_signing_simple(setup_wallet, wallet_cls, type_check):
|
|
jm_single().config.set('BLOCKCHAIN', 'network', 'testnet')
|
|
storage = VolatileStorage()
|
|
wallet_cls.initialize(storage, get_network(), entropy=b"\xaa"*16)
|
|
wallet = wallet_cls(storage)
|
|
utxo = fund_wallet_addr(wallet, wallet.get_internal_addr(0))
|
|
# The dummy output is constructed as an unspendable p2sh:
|
|
tx = btc.mktx([utxo],
|
|
[{"address": str(btc.CCoinAddress.from_scriptPubKey(
|
|
btc.CScript(b"\x00").to_p2sh_scriptPubKey())),
|
|
"value": 10**8 - 9000}])
|
|
script = wallet.get_script(0, BaseWallet.ADDRESS_TYPE_INTERNAL, 0)
|
|
success, msg = wallet.sign_tx(tx, {0: (script, 10**8)})
|
|
assert success, msg
|
|
type_check(tx)
|
|
txout = jm_single().bc_interface.pushtx(tx.serialize())
|
|
assert txout
|
|
|
|
# note that address validation is tested separately;
|
|
# this test functions only to make sure that given a valid
|
|
# taproot address, we can actually spend to it
|
|
@pytest.mark.parametrize('hexspk', [
|
|
"512091b64d5324723a985170e4dc5a0f84c041804f2cd12660fa5dec09fc21783605",
|
|
"5120147c9c57132f6e7ecddba9800bb0c4449251c92a1e60371ee77557b6620f3ea3",
|
|
"5120712447206d7a5238acc7ff53fbe94a3b64539ad291c7cdbc490b7577e4b17df5",
|
|
])
|
|
def test_spend_to_p2traddr(setup_wallet, hexspk):
|
|
storage = VolatileStorage()
|
|
SegwitWallet.initialize(storage, get_network(), entropy=b"\xaa"*16)
|
|
wallet = SegwitWallet(storage)
|
|
utxo = fund_wallet_addr(wallet, wallet.get_internal_addr(0))
|
|
sPK = btc.CScript(hextobin(hexspk))
|
|
tx = btc.mktx([utxo],
|
|
[{"address": str(btc.CCoinAddress.from_scriptPubKey(sPK)),
|
|
"value": 10**8 - 9000}])
|
|
script = wallet.get_script(0, BaseWallet.ADDRESS_TYPE_INTERNAL, 0)
|
|
success, msg = wallet.sign_tx(tx, {0: (script, 10**8)})
|
|
assert success, msg
|
|
txout = jm_single().bc_interface.pushtx(tx.serialize())
|
|
assert txout
|
|
# probably unnecessary, but since we are sanity checking:
|
|
# does the output of the in-mempool tx have the sPK we expect?
|
|
txid = tx.GetTxid()[::-1]
|
|
txres = btc.CTransaction.deserialize(hextobin(jm_single().bc_interface._rpc(
|
|
"getrawtransaction", [bintohex(txid), True])["hex"]))
|
|
assert txres.vout[0].scriptPubKey == sPK
|
|
assert txres.vout[0].nValue == 10**8 - 9000
|
|
|
|
def test_timelocked_output_signing(setup_wallet):
|
|
jm_single().config.set('BLOCKCHAIN', 'network', 'testnet')
|
|
ensure_bip65_activated()
|
|
storage = VolatileStorage()
|
|
SegwitWalletFidelityBonds.initialize(storage, get_network())
|
|
wallet = SegwitWalletFidelityBonds(storage)
|
|
|
|
timenumber = 0
|
|
script = wallet.get_script(
|
|
FidelityBondMixin.FIDELITY_BOND_MIXDEPTH,
|
|
FidelityBondMixin.BIP32_TIMELOCK_ID, timenumber)
|
|
utxo = fund_wallet_addr(wallet, wallet.script_to_addr(script))
|
|
timestamp = wallet._time_number_to_timestamp(timenumber)
|
|
|
|
tx = btc.mktx([utxo], [{"address": str(btc.CCoinAddress.from_scriptPubKey(
|
|
btc.standard_scripthash_scriptpubkey(btc.Hash160(b"\x00")))),
|
|
"value":10**8 - 9000}], locktime=timestamp+1)
|
|
success, msg = wallet.sign_tx(tx, {0: (script, 10**8)})
|
|
assert success, msg
|
|
txout = jm_single().bc_interface.pushtx(tx.serialize())
|
|
assert txout
|
|
|
|
def test_get_bbm(setup_wallet):
|
|
jm_single().config.set('BLOCKCHAIN', 'network', 'testnet')
|
|
amount = 10**8
|
|
num_tx = 3
|
|
wallet = get_populated_wallet(amount, num_tx)
|
|
# disable a utxo and check we can correctly report
|
|
# balance with the disabled flag off:
|
|
utxo_1 = list(wallet._utxos.get_utxos_at_mixdepth(0).keys())[0]
|
|
wallet.disable_utxo(*utxo_1)
|
|
balances = wallet.get_balance_by_mixdepth(include_disabled=True)
|
|
assert balances[0] == num_tx * amount
|
|
balances = wallet.get_balance_by_mixdepth()
|
|
assert balances[0] == (num_tx - 1) * amount
|
|
wallet.toggle_disable_utxo(*utxo_1)
|
|
balances = wallet.get_balance_by_mixdepth()
|
|
assert balances[0] == num_tx * amount
|
|
|
|
def test_add_utxos(setup_wallet):
|
|
jm_single().config.set('BLOCKCHAIN', 'network', 'testnet')
|
|
amount = 10**8
|
|
num_tx = 3
|
|
|
|
wallet = get_populated_wallet(amount, num_tx)
|
|
|
|
balances = wallet.get_balance_by_mixdepth()
|
|
assert balances[0] == num_tx * amount
|
|
for md in range(1, wallet.max_mixdepth + 1):
|
|
assert balances[md] == 0
|
|
|
|
utxos = wallet.get_utxos_by_mixdepth()
|
|
assert len(utxos[0]) == num_tx
|
|
for md in range(1, wallet.max_mixdepth + 1):
|
|
assert not utxos[md]
|
|
|
|
with pytest.raises(Exception):
|
|
# no funds in mixdepth
|
|
wallet.select_utxos(1, amount)
|
|
|
|
with pytest.raises(Exception):
|
|
# not enough funds
|
|
wallet.select_utxos(0, amount * (num_tx + 1))
|
|
|
|
wallet.reset_utxos()
|
|
assert wallet.get_balance_by_mixdepth()[0] == 0
|
|
|
|
|
|
def test_select_utxos(setup_wallet):
|
|
jm_single().config.set('BLOCKCHAIN', 'network', 'testnet')
|
|
amount = 10**8
|
|
|
|
wallet = get_populated_wallet(amount)
|
|
utxos = wallet.select_utxos(0, amount // 2)
|
|
|
|
assert len(utxos) == 1
|
|
utxos = list(utxos.keys())
|
|
|
|
more_utxos = wallet.select_utxos(0, int(amount * 1.5), utxo_filter=utxos)
|
|
assert len(more_utxos) == 2
|
|
assert utxos[0] not in more_utxos
|
|
|
|
|
|
def test_add_new_utxos(setup_wallet):
|
|
jm_single().config.set('BLOCKCHAIN', 'network', 'testnet')
|
|
wallet = get_populated_wallet(num=1)
|
|
|
|
scripts = [wallet.get_new_script(x,
|
|
BaseWallet.ADDRESS_TYPE_INTERNAL) for x in range(3)]
|
|
tx_scripts = list(scripts)
|
|
tx = btc.mktx(
|
|
[(b"\x00"*32, 2)],
|
|
[{"address": wallet.script_to_addr(s),
|
|
"value": 10**8} for s in tx_scripts])
|
|
added = wallet.add_new_utxos(tx, 1)
|
|
assert len(added) == len(scripts)
|
|
|
|
added_scripts = {x['script'] for x in added.values()}
|
|
for s in scripts:
|
|
assert s in added_scripts
|
|
|
|
balances = wallet.get_balance_by_mixdepth()
|
|
assert balances[0] == 2 * 10**8
|
|
assert balances[1] == 10**8
|
|
assert balances[2] == 10**8
|
|
assert len(balances) == wallet.max_mixdepth + 1
|
|
|
|
|
|
def test_remove_old_utxos(setup_wallet):
|
|
jm_single().config.set('BLOCKCHAIN', 'network', 'testnet')
|
|
wallet = get_populated_wallet()
|
|
|
|
# add some more utxos to mixdepth 1
|
|
for i in range(3):
|
|
txin = jm_single().bc_interface.grab_coins(
|
|
wallet.get_internal_addr(1), 1)
|
|
wallet.add_utxo(btc.x(txin), 0, wallet.get_script(1,
|
|
BaseWallet.ADDRESS_TYPE_INTERNAL, i), 10**8, 1)
|
|
|
|
inputs = wallet.select_utxos(0, 10**8)
|
|
inputs.update(wallet.select_utxos(1, 2 * 10**8))
|
|
assert len(inputs) == 3
|
|
|
|
tx_inputs = list(inputs.keys())
|
|
tx_inputs.append((b'\x12'*32, 6))
|
|
|
|
tx = btc.mktx(tx_inputs,
|
|
[{"address": "2N9gfkUsFW7Kkb1Eurue7NzUxUt7aNJiS1U",
|
|
"value": 3 * 10**8 - 1000}])
|
|
|
|
removed = wallet.remove_old_utxos(tx)
|
|
assert len(removed) == len(inputs)
|
|
|
|
for txid in removed:
|
|
assert txid in inputs
|
|
|
|
balances = wallet.get_balance_by_mixdepth()
|
|
assert balances[0] == 2 * 10**8
|
|
assert balances[1] == 10**8
|
|
assert balances[2] == 0
|
|
assert len(balances) == wallet.max_mixdepth + 1
|
|
|
|
|
|
def test_address_labels(setup_wallet):
|
|
wallet = get_populated_wallet(num=2)
|
|
addr1 = wallet.get_internal_addr(0)
|
|
addr2 = wallet.get_internal_addr(1)
|
|
assert wallet.get_address_label(addr2) is None
|
|
assert wallet.get_address_label(addr2) is None
|
|
wallet.set_address_label(addr1, "test")
|
|
# utf-8 characters here are on purpose, to test utf-8 encoding / decoding
|
|
wallet.set_address_label(addr2, "glāžšķūņu rūķīši")
|
|
assert wallet.get_address_label(addr1) == "test"
|
|
assert wallet.get_address_label(addr2) == "glāžšķūņu rūķīši"
|
|
wallet.set_address_label(addr1, "")
|
|
wallet.set_address_label(addr2, None)
|
|
assert wallet.get_address_label(addr2) is None
|
|
assert wallet.get_address_label(addr2) is None
|
|
with pytest.raises(UnknownAddressForLabel):
|
|
wallet.get_address_label("2MzY5yyonUY7zpHspg7jB7WQs1uJxKafQe4")
|
|
wallet.set_address_label("2MzY5yyonUY7zpHspg7jB7WQs1uJxKafQe4",
|
|
"test")
|
|
# we no longer decode addresses just to see if we know about them,
|
|
# so we won't get a CCoinAddressError for invalid addresses
|
|
#with pytest.raises(CCoinAddressError):
|
|
wallet.get_address_label("badaddress")
|
|
wallet.set_address_label("badaddress", "test")
|
|
|
|
|
|
def test_initialize_twice(setup_wallet):
|
|
wallet = get_populated_wallet(num=0)
|
|
storage = wallet._storage
|
|
with pytest.raises(WalletError):
|
|
SegwitLegacyWallet.initialize(storage, get_network())
|
|
|
|
|
|
def test_is_known(setup_wallet):
|
|
wallet = get_populated_wallet(num=0)
|
|
script = wallet.get_new_script(1, BaseWallet.ADDRESS_TYPE_INTERNAL)
|
|
addr = wallet.get_external_addr(2)
|
|
|
|
assert wallet.is_known_script(script)
|
|
assert wallet.is_known_addr(addr)
|
|
assert wallet.is_known_addr(wallet.script_to_addr(script))
|
|
assert wallet.is_known_script(wallet.addr_to_script(addr))
|
|
|
|
assert not wallet.is_known_script(b'\x12' * len(script))
|
|
assert not wallet.is_known_addr('2MzY5yyonUY7zpHspg7jB7WQs1uJxKafQe4')
|
|
|
|
|
|
def test_wallet_save(setup_wallet):
|
|
wallet = get_populated_wallet()
|
|
|
|
script = wallet.get_external_script(1)
|
|
|
|
wallet.save()
|
|
storage = wallet._storage
|
|
data = storage.file_data
|
|
|
|
del wallet
|
|
del storage
|
|
|
|
storage = VolatileStorage(data=data)
|
|
wallet = SegwitLegacyWallet(storage)
|
|
|
|
assert wallet.get_next_unused_index(0, BaseWallet.ADDRESS_TYPE_INTERNAL) == 3
|
|
assert wallet.get_next_unused_index(0, BaseWallet.ADDRESS_TYPE_EXTERNAL) == 0
|
|
assert wallet.get_next_unused_index(1, BaseWallet.ADDRESS_TYPE_INTERNAL) == 0
|
|
assert wallet.get_next_unused_index(1, BaseWallet.ADDRESS_TYPE_EXTERNAL) == 1
|
|
assert wallet.is_known_script(script)
|
|
|
|
|
|
def test_set_next_index(setup_wallet):
|
|
wallet = get_populated_wallet()
|
|
|
|
assert wallet.get_next_unused_index(0,
|
|
BaseWallet.ADDRESS_TYPE_INTERNAL) == 3
|
|
|
|
with pytest.raises(Exception):
|
|
# cannot advance index without force=True
|
|
wallet.set_next_index(0, BaseWallet.ADDRESS_TYPE_INTERNAL, 5)
|
|
|
|
wallet.set_next_index(0, BaseWallet.ADDRESS_TYPE_INTERNAL, 1)
|
|
assert wallet.get_next_unused_index(0, BaseWallet.ADDRESS_TYPE_INTERNAL) == 1
|
|
|
|
wallet.set_next_index(0, BaseWallet.ADDRESS_TYPE_INTERNAL, 20, force=True)
|
|
assert wallet.get_next_unused_index(0, BaseWallet.ADDRESS_TYPE_INTERNAL) == 20
|
|
|
|
script = wallet.get_new_script(0, BaseWallet.ADDRESS_TYPE_INTERNAL)
|
|
path = wallet.script_to_path(script)
|
|
index = wallet.get_details(path)[2]
|
|
assert index == 20
|
|
|
|
|
|
def test_path_repr(setup_wallet):
|
|
wallet = get_populated_wallet()
|
|
path = wallet.get_path(2, BIP32Wallet.ADDRESS_TYPE_EXTERNAL, 0)
|
|
path_repr = wallet.get_path_repr(path)
|
|
path_new = wallet.path_repr_to_path(path_repr)
|
|
|
|
assert path_new == path
|
|
|
|
|
|
def test_path_repr_imported(setup_wallet):
|
|
wallet = get_populated_wallet(num=0)
|
|
path = wallet.import_private_key(
|
|
0, 'cRAGLvPmhpzJNgdMT4W2gVwEW3fusfaDqdQWM2vnWLgXKzCWKtcM')
|
|
path_repr = wallet.get_path_repr(path)
|
|
path_new = wallet.path_repr_to_path(path_repr)
|
|
|
|
assert path_new == path
|
|
|
|
@pytest.mark.parametrize('timenumber,timestamp', [
|
|
[0, 1577836800],
|
|
[50, 1709251200],
|
|
[300, 2366841600],
|
|
[1000, None], #too far in the future
|
|
[-1, None] #before epoch
|
|
])
|
|
def test_timenumber_to_timestamp(setup_wallet, timenumber, timestamp):
|
|
try:
|
|
implied_timestamp = FidelityBondMixin._time_number_to_timestamp(
|
|
timenumber)
|
|
assert implied_timestamp == timestamp
|
|
except ValueError:
|
|
#None means the timenumber is intentionally invalid
|
|
assert timestamp == None
|
|
|
|
@pytest.mark.parametrize('timestamp,timenumber', [
|
|
[1577836800, 0],
|
|
[1709251200, 50],
|
|
[2366841600, 300],
|
|
[1577836801, None], #not exactly midnight on first of month
|
|
[4133980800, None], #too far in future
|
|
[1575158400, None] #before epoch
|
|
])
|
|
def test_timestamp_to_timenumber(setup_wallet, timestamp, timenumber):
|
|
try:
|
|
implied_timenumber = FidelityBondMixin.timestamp_to_time_number(
|
|
timestamp)
|
|
assert implied_timenumber == timenumber
|
|
except ValueError:
|
|
assert timenumber == None
|
|
|
|
def test_wrong_wallet_cls(setup_wallet):
|
|
storage = VolatileStorage()
|
|
SegwitLegacyWallet.initialize(storage, get_network())
|
|
wallet = SegwitLegacyWallet(storage)
|
|
|
|
wallet.save()
|
|
data = storage.file_data
|
|
|
|
del wallet
|
|
del storage
|
|
|
|
storage = VolatileStorage(data=data)
|
|
|
|
with pytest.raises(Exception):
|
|
LegacyWallet(storage)
|
|
|
|
|
|
def test_wallet_id(setup_wallet):
|
|
storage1 = VolatileStorage()
|
|
SegwitLegacyWallet.initialize(storage1, get_network())
|
|
wallet1 = SegwitLegacyWallet(storage1)
|
|
|
|
storage2 = VolatileStorage()
|
|
LegacyWallet.initialize(storage2, get_network(), entropy=wallet1._entropy)
|
|
wallet2 = LegacyWallet(storage2)
|
|
|
|
assert wallet1.get_wallet_id() != wallet2.get_wallet_id()
|
|
|
|
storage2 = VolatileStorage()
|
|
SegwitLegacyWallet.initialize(storage2, get_network(),
|
|
entropy=wallet1._entropy)
|
|
wallet2 = SegwitLegacyWallet(storage2)
|
|
|
|
assert wallet1.get_wallet_id() == wallet2.get_wallet_id()
|
|
|
|
|
|
def test_cache_cleared(setup_wallet):
|
|
# test plan:
|
|
# 1. create a new wallet and sync from scratch
|
|
# 2. read its cache as an object
|
|
# 3. close the wallet, reopen it, sync it.
|
|
# 4. corrupt its cache and save.
|
|
# 5. Re open the wallet with recoversync
|
|
# and check that the corrupted data is not present.
|
|
if os.path.exists(test_cache_cleared_filename):
|
|
os.remove(test_cache_cleared_filename)
|
|
wallet = create_wallet(test_cache_cleared_filename,
|
|
b"hunter2", 2, SegwitWallet)
|
|
# note: we use the WalletService as an encapsulation
|
|
# of the wallet here because we want to be able to sync,
|
|
# but we do not actually start the service and go into
|
|
# the monitoring loop.
|
|
wallet_service = WalletService(wallet)
|
|
# default fast sync, no coins, so no loop
|
|
wallet_service.sync_wallet()
|
|
wallet_service.update_blockheight()
|
|
# to get the cache to save, we need to
|
|
# use an address:
|
|
addr = wallet_service.get_new_addr(0,0)
|
|
jm_single().bc_interface.grab_coins(addr, 1.0)
|
|
wallet_service.transaction_monitor()
|
|
path_to_corrupt = list(wallet._cache.keys())[0]
|
|
# we'll just corrupt the first address and script:
|
|
entry_to_corrupt = wallet._cache[path_to_corrupt][b"84'"][b"1'"][b"0'"][b'0'][b'0']
|
|
entry_to_corrupt[b'A'] = "notanaddress"
|
|
entry_to_corrupt[b'S'] = "notascript"
|
|
wallet_service.wallet.save()
|
|
wallet_service.wallet.close()
|
|
jm_single().config.set("POLICY", "wallet_caching_disabled", "true")
|
|
wallet2 = open_wallet(test_cache_cleared_filename,
|
|
ask_for_password=False,
|
|
password=b"hunter2")
|
|
jm_single().config.set("POLICY", "wallet_caching_disabled", "false")
|
|
wallet_service2 = WalletService(wallet2)
|
|
while not wallet_service2.synced:
|
|
wallet_service2.sync_wallet(fast=False)
|
|
wallet_service.transaction_monitor()
|
|
# we ignored the corrupt cache?
|
|
assert wallet_service2.get_balance_at_mixdepth(0) == 10 ** 8
|
|
|
|
def test_addr_script_conversion(setup_wallet):
|
|
wallet = get_populated_wallet(num=1)
|
|
|
|
path = wallet.get_path(0, BaseWallet.ADDRESS_TYPE_INTERNAL, 0)
|
|
script = wallet.get_script_from_path(path)
|
|
addr = wallet.script_to_addr(script)
|
|
|
|
assert script == wallet.addr_to_script(addr)
|
|
addr_path = wallet.addr_to_path(addr)
|
|
assert path == addr_path
|
|
|
|
|
|
def test_imported_key_removed(setup_wallet):
|
|
wif = 'cRAGLvPmhpzJNgdMT4W2gVwEW3fusfaDqdQWM2vnWLgXKzCWKtcM'
|
|
|
|
storage = VolatileStorage()
|
|
SegwitLegacyWallet.initialize(storage, get_network())
|
|
wallet = SegwitLegacyWallet(storage)
|
|
|
|
path = wallet.import_private_key(1, wif)
|
|
script = wallet.get_script_from_path(path)
|
|
assert wallet.is_known_script(script)
|
|
|
|
wallet.remove_imported_key(path=path)
|
|
assert not wallet.is_known_script(script)
|
|
|
|
with pytest.raises(WalletError):
|
|
wallet.get_script_from_path(path)
|
|
|
|
|
|
def test_wallet_mixdepth_simple(setup_wallet):
|
|
wallet = get_populated_wallet(num=0)
|
|
mixdepth = wallet.mixdepth
|
|
assert wallet.max_mixdepth == mixdepth
|
|
|
|
wallet.close()
|
|
storage_data = wallet._storage.file_data
|
|
|
|
new_wallet = type(wallet)(VolatileStorage(data=storage_data))
|
|
assert new_wallet.mixdepth == mixdepth
|
|
assert new_wallet.max_mixdepth == mixdepth
|
|
|
|
|
|
def test_wallet_mixdepth_increase(setup_wallet):
|
|
wallet = get_populated_wallet(num=0)
|
|
mixdepth = wallet.mixdepth
|
|
|
|
wallet.close()
|
|
storage_data = wallet._storage.file_data
|
|
|
|
new_mixdepth = mixdepth + 2
|
|
new_wallet = type(wallet)(
|
|
VolatileStorage(data=storage_data), mixdepth=new_mixdepth)
|
|
assert new_wallet.mixdepth == new_mixdepth
|
|
assert new_wallet.max_mixdepth == new_mixdepth
|
|
|
|
|
|
def test_wallet_mixdepth_decrease(setup_wallet):
|
|
wallet = get_populated_wallet(num=1)
|
|
|
|
# setup
|
|
max_mixdepth = wallet.max_mixdepth
|
|
assert max_mixdepth >= 1, "bad default value for mixdepth for this test"
|
|
utxo = fund_wallet_addr(wallet, wallet.get_internal_addr(max_mixdepth), 1)
|
|
bci = jm_single().bc_interface
|
|
unspent_list = bci.listunspent(0)
|
|
# filter on label, but note (a) in certain circumstances (in-
|
|
# wallet transfer) it is possible for the utxo to be labeled
|
|
# with the external label, and (b) the wallet will know if it
|
|
# belongs or not anyway (is_known_addr):
|
|
our_unspent_list = [x for x in unspent_list if (
|
|
bci.is_address_labeled(x, wallet.get_wallet_name()))]
|
|
assert wallet.get_balance_by_mixdepth()[max_mixdepth] == 10**8
|
|
wallet.close()
|
|
storage_data = wallet._storage.file_data
|
|
|
|
# actual test
|
|
new_mixdepth = max_mixdepth - 1
|
|
new_wallet = type(wallet)(
|
|
VolatileStorage(data=storage_data), mixdepth=new_mixdepth)
|
|
assert new_wallet.max_mixdepth == max_mixdepth
|
|
assert new_wallet.mixdepth == new_mixdepth
|
|
sync_test_wallet(True, WalletService(new_wallet))
|
|
|
|
assert max_mixdepth not in new_wallet.get_balance_by_mixdepth()
|
|
assert max_mixdepth not in new_wallet.get_utxos_by_mixdepth()
|
|
|
|
# wallet.select_utxos will still return utxos from higher mixdepths
|
|
# because we explicitly ask for a specific mixdepth
|
|
assert utxo in new_wallet.select_utxos(max_mixdepth, 10**7)
|
|
|
|
def test_watchonly_wallet(setup_wallet):
|
|
jm_single().config.set('BLOCKCHAIN', 'network', 'testnet')
|
|
storage = VolatileStorage()
|
|
SegwitWalletFidelityBonds.initialize(storage, get_network())
|
|
wallet = SegwitWalletFidelityBonds(storage)
|
|
|
|
paths = [
|
|
"m/84'/1'/0'/0/0",
|
|
"m/84'/1'/0'/1/0",
|
|
"m/84'/1'/0'/2/0:1577836800",
|
|
"m/84'/1'/0'/2/0:2314051200"
|
|
]
|
|
burn_path = "m/49'/1'/0'/3/0"
|
|
|
|
scripts = [wallet.get_script_from_path(wallet.path_repr_to_path(path))
|
|
for path in paths]
|
|
privkey, engine = wallet._get_key_from_path(wallet.path_repr_to_path(burn_path))
|
|
burn_pubkey = engine.privkey_to_pubkey(privkey)
|
|
|
|
master_pub_key = wallet.get_bip32_pub_export(
|
|
FidelityBondMixin.FIDELITY_BOND_MIXDEPTH)
|
|
watchonly_storage = VolatileStorage()
|
|
entropy = FidelityBondMixin.get_xpub_from_fidelity_bond_master_pub_key(
|
|
master_pub_key).encode()
|
|
FidelityBondWatchonlyWallet.initialize(watchonly_storage, get_network(),
|
|
entropy=entropy)
|
|
watchonly_wallet = FidelityBondWatchonlyWallet(watchonly_storage)
|
|
|
|
watchonly_scripts = [watchonly_wallet.get_script_from_path(
|
|
watchonly_wallet.path_repr_to_path(path)) for path in paths]
|
|
privkey, engine = wallet._get_key_from_path(wallet.path_repr_to_path(burn_path))
|
|
watchonly_burn_pubkey = engine.privkey_to_pubkey(privkey)
|
|
|
|
for script, watchonly_script in zip(scripts, watchonly_scripts):
|
|
assert script == watchonly_script
|
|
assert burn_pubkey == watchonly_burn_pubkey
|
|
|
|
def test_calculate_timelocked_fidelity_bond_value(setup_wallet):
|
|
EPSILON = 0.000001
|
|
YEAR = 60*60*24*356.25
|
|
|
|
#the function should be flat anywhere before the locktime ends
|
|
values = [FidelityBondMixin.calculate_timelocked_fidelity_bond_value(
|
|
utxo_value=100000000,
|
|
confirmation_time=0,
|
|
locktime=6*YEAR,
|
|
current_time=y*YEAR,
|
|
interest_rate=0.01
|
|
)
|
|
for y in range(4)
|
|
]
|
|
value_diff = [values[i] - values[i+1] for i in range(len(values)-1)]
|
|
for vd in value_diff:
|
|
assert abs(vd) < EPSILON
|
|
|
|
#after locktime, the value should go down
|
|
values = [FidelityBondMixin.calculate_timelocked_fidelity_bond_value(
|
|
utxo_value=100000000,
|
|
confirmation_time=0,
|
|
locktime=6*YEAR,
|
|
current_time=(6+y)*YEAR,
|
|
interest_rate=0.01
|
|
)
|
|
for y in range(5)
|
|
]
|
|
value_diff = [values[i+1] - values[i] for i in range(len(values)-1)]
|
|
for vrd in value_diff:
|
|
assert vrd < 0
|
|
|
|
#value of a bond goes up as the locktime goes up
|
|
values = [FidelityBondMixin.calculate_timelocked_fidelity_bond_value(
|
|
utxo_value=100000000,
|
|
confirmation_time=0,
|
|
locktime=y*YEAR,
|
|
current_time=0,
|
|
interest_rate=0.01
|
|
)
|
|
for y in range(5)
|
|
]
|
|
value_ratio = [values[i] / values[i+1] for i in range(len(values)-1)]
|
|
value_ratio_diff = [value_ratio[i] - value_ratio[i+1] for i in range(len(value_ratio)-1)]
|
|
for vrd in value_ratio_diff:
|
|
assert vrd < 0
|
|
|
|
#value of a bond locked into the far future is constant, clamped at the value of burned coins
|
|
values = [FidelityBondMixin.calculate_timelocked_fidelity_bond_value(
|
|
utxo_value=100000000,
|
|
confirmation_time=0,
|
|
locktime=(200+y)*YEAR,
|
|
current_time=0,
|
|
interest_rate=0.01
|
|
)
|
|
for y in range(5)
|
|
]
|
|
value_diff = [values[i] - values[i+1] for i in range(len(values)-1)]
|
|
for vd in value_diff:
|
|
assert abs(vd) < EPSILON
|
|
|
|
@pytest.mark.parametrize('password, wallet_cls', [
|
|
["hunter2", SegwitLegacyWallet],
|
|
["hunter2", SegwitWallet],
|
|
])
|
|
def test_create_wallet(setup_wallet, password, wallet_cls):
|
|
wallet_name = test_create_wallet_filename
|
|
password = password.encode("utf-8")
|
|
# test mainnet (we are not transacting)
|
|
btc.select_chain_params("bitcoin")
|
|
wallet = create_wallet(wallet_name, password, 4, wallet_cls)
|
|
mnemonic = wallet.get_mnemonic_words()[0]
|
|
firstkey = wallet.get_key_from_addr(wallet.get_addr(0,0,0))
|
|
print("Created mnemonic, firstkey: ", mnemonic, firstkey)
|
|
wallet.close()
|
|
# ensure that the wallet file created is openable with the password,
|
|
# and has the parameters that were claimed on creation:
|
|
new_wallet = open_test_wallet_maybe(wallet_name, "", 4,
|
|
password=password, ask_for_password=False)
|
|
assert new_wallet.get_mnemonic_words()[0] == mnemonic
|
|
assert new_wallet.get_key_from_addr(
|
|
new_wallet.get_addr(0,0,0)) == firstkey
|
|
os.remove(wallet_name)
|
|
btc.select_chain_params("bitcoin/regtest")
|
|
|
|
|
|
@pytest.mark.parametrize('wallet_cls', [
|
|
SegwitLegacyWallet, SegwitWallet, SegwitWalletFidelityBonds
|
|
])
|
|
def test_is_standard_wallet_script(setup_wallet, wallet_cls):
|
|
storage = VolatileStorage()
|
|
wallet_cls.initialize(
|
|
storage, get_network(), max_mixdepth=0)
|
|
wallet = wallet_cls(storage)
|
|
script = wallet.get_new_script(0, 1)
|
|
assert wallet.is_known_script(script)
|
|
path = wallet.script_to_path(script)
|
|
assert wallet.is_standard_wallet_script(path)
|
|
|
|
|
|
def test_is_standard_wallet_script_nonstandard(setup_wallet):
|
|
storage = VolatileStorage()
|
|
SegwitWalletFidelityBonds.initialize(
|
|
storage, get_network(), max_mixdepth=0)
|
|
wallet = SegwitWalletFidelityBonds(storage)
|
|
import_path = wallet.import_private_key(
|
|
0, 'cRAGLvPmhpzJNgdMT4W2gVwEW3fusfaDqdQWM2vnWLgXKzCWKtcM')
|
|
assert wallet.is_standard_wallet_script(import_path)
|
|
ts = wallet.datetime_to_time_number(
|
|
datetime.datetime.strptime("2021-07", "%Y-%m"))
|
|
tl_path = wallet.get_path(0, wallet.BIP32_TIMELOCK_ID, ts)
|
|
assert not wallet.is_standard_wallet_script(tl_path)
|
|
|
|
|
|
@pytest.fixture(scope='module')
|
|
def setup_wallet(request):
|
|
load_test_config()
|
|
btc.select_chain_params("bitcoin/regtest")
|
|
#see note in cryptoengine.py:
|
|
cryptoengine.BTC_P2WPKH.VBYTE = 100
|
|
jm_single().bc_interface.tick_forward_chain_interval = 2
|
|
def teardown():
|
|
if os.path.exists(test_create_wallet_filename):
|
|
os.remove(test_create_wallet_filename)
|
|
if os.path.exists(test_cache_cleared_filename):
|
|
os.remove(test_cache_cleared_filename)
|
|
request.addfinalizer(teardown)
|