def test_wallet_change_addresses(bitcoin_regtest, devices_filled_data_folder, device_manager): wm = WalletManager(devices_filled_data_folder, bitcoin_regtest.get_cli(), "regtest", device_manager) # A wallet-creation needs a device device = device_manager.get_by_alias('specter') key = Key.from_json({ "derivation": "m/48h/1h/0h/2h", "original": "Vpub5n9kKePTPPGtw3RddeJWJe29epEyBBcoHbbPi5HhpoG2kTVsSCUzsad33RJUt3LktEUUPPofcZczuudnwR7ZgkAkT6N2K2Z7wdyjYrVAkXM", "fingerprint": "08686ac6", "type": "wsh", "xpub": "tpubDFHpKypXq4kwUrqLotPs6fCic5bFqTRGMBaTi9s5YwwGymE8FLGwB2kDXALxqvNwFxB1dLWYBmmeFVjmUSdt2AsaQuPmkyPLBKRZW8BGCiL" }) wallet = wm.create_wallet('a_second_test_wallet', 1, 'wpkh', [key], [device]) address = wallet.address change_address = wallet.change_address assert wallet.addresses == [address] assert wallet.change_addresses == [change_address] assert wallet.active_addresses == [address] assert wallet.labels == ['Address #0'] wallet.cli.generatetoaddress(20, change_address) random_address = "mruae2834buqxk77oaVpephnA5ZAxNNJ1r" wallet.cli.generatetoaddress(110, random_address) wallet.getdata() # new change address should be genrated automatically after receiving # assert wallet.change_addresses == [change_address, wallet.change_address] # This will not work here since Bitcoin Core doesn't count mining rewards in `getreceivedbyaddress` # See: https://github.com/bitcoin/bitcoin/issues/14654 assert wallet.active_addresses == [address, change_address] # labels should return only active addresses assert wallet.labels == ['Address #0', 'Change #0']
def test_wallet_change_addresses( bitcoin_regtest, devices_filled_data_folder, device_manager ): wm = WalletManager( 200100, devices_filled_data_folder, bitcoin_regtest.get_rpc(), "regtest", device_manager, allow_threading=False, ) # A wallet-creation needs a device device = device_manager.get_by_alias("specter") key = Key.from_json( { "derivation": "m/48h/1h/0h/2h", "original": "Vpub5n9kKePTPPGtw3RddeJWJe29epEyBBcoHbbPi5HhpoG2kTVsSCUzsad33RJUt3LktEUUPPofcZczuudnwR7ZgkAkT6N2K2Z7wdyjYrVAkXM", "fingerprint": "08686ac6", "type": "wsh", "xpub": "tpubDFHpKypXq4kwUrqLotPs6fCic5bFqTRGMBaTi9s5YwwGymE8FLGwB2kDXALxqvNwFxB1dLWYBmmeFVjmUSdt2AsaQuPmkyPLBKRZW8BGCiL", } ) wallet = wm.create_wallet("a_second_test_wallet", 1, "wpkh", [key], [device]) address = wallet.address change_address = wallet.change_address assert wallet.addresses == [address] assert wallet.change_addresses == [change_address] wallet.rpc.generatetoaddress(20, change_address) random_address = "mruae2834buqxk77oaVpephnA5ZAxNNJ1r" wallet.rpc.generatetoaddress(110, random_address) wallet.getdata()
def test_wallet_createpsbt(bitcoin_regtest, devices_filled_data_folder, device_manager): wm = WalletManager(devices_filled_data_folder, bitcoin_regtest.get_cli(), "regtest", device_manager) # A wallet-creation needs a device device = device_manager.get_by_alias('specter') key = Key.from_json({ "derivation": "m/48h/1h/0h/2h", "original": "Vpub5n9kKePTPPGtw3RddeJWJe29epEyBBcoHbbPi5HhpoG2kTVsSCUzsad33RJUt3LktEUUPPofcZczuudnwR7ZgkAkT6N2K2Z7wdyjYrVAkXM", "fingerprint": "08686ac6", "type": "wsh", "xpub": "tpubDFHpKypXq4kwUrqLotPs6fCic5bFqTRGMBaTi9s5YwwGymE8FLGwB2kDXALxqvNwFxB1dLWYBmmeFVjmUSdt2AsaQuPmkyPLBKRZW8BGCiL" }) wallet = wm.create_wallet('a_second_test_wallet', 1, 'wpkh', [key], [device]) # Let's fund the wallet with ... let's say 40 blocks a 50 coins each --> 200 coins address = wallet.getnewaddress() assert address == 'bcrt1qtnrv2jpygx2ef3zqfjhqplnycxak2m6ljnhq6z' wallet.cli.generatetoaddress(20, address) # in two addresses address = wallet.getnewaddress() wallet.cli.generatetoaddress(20, address) # newly minted coins need 100 blocks to get spendable # let's mine another 100 blocks to get these coins spendable random_address = "mruae2834buqxk77oaVpephnA5ZAxNNJ1r" wallet.cli.generatetoaddress(110, random_address) # update the wallet data wallet.get_balance() # Now we have loads of potential inputs # Let's spend 500 coins assert wallet.fullbalance >= 250 # From this print-statement, let's grab some txids which we'll use for coinselect unspents = wallet.cli.listunspent(0) # Lets take 3 more or less random txs from the unspents: selected_coins = [ "{},{},{}".format(unspents[5]['txid'], unspents[5]['vout'], unspents[5]['amount']), "{},{},{}".format(unspents[9]['txid'], unspents[9]['vout'], unspents[9]['amount']), "{},{},{}".format(unspents[12]['txid'], unspents[12]['vout'], unspents[12]['amount']) ] selected_coins_amount_sum = unspents[5]['amount'] + unspents[9]['amount'] + unspents[12]['amount'] number_of_coins_to_spend = selected_coins_amount_sum - 0.1 # Let's spend almost all of them psbt = wallet.createpsbt([random_address], [number_of_coins_to_spend], True, 10, selected_coins=selected_coins) assert len(psbt['tx']['vin']) == 3 psbt_txs = [ tx['txid'] for tx in psbt['tx']['vin'] ] for coin in selected_coins: assert coin.split(",")[0] in psbt_txs # Now let's spend more coins than we have selected. This should result in an exception: try: psbt = wallet.createpsbt([random_address], [number_of_coins_to_spend +1], True, 10, selected_coins=selected_coins) assert False, "should throw an exception!" except SpecterError as e: pass assert wallet.locked_amount == selected_coins_amount_sum assert len(wallet.cli.listlockunspent()) == 3 assert wallet.full_available_balance == wallet.fullbalance - selected_coins_amount_sum wallet.delete_pending_psbt(psbt['tx']['txid']) assert wallet.locked_amount == 0 assert len(wallet.cli.listlockunspent()) == 0 assert wallet.full_available_balance == wallet.fullbalance
def test_wallet_labeling(bitcoin_regtest, devices_filled_data_folder, device_manager): wm = WalletManager( 200100, devices_filled_data_folder, bitcoin_regtest.get_rpc(), "regtest", device_manager, allow_threading=False, ) # A wallet-creation needs a device device = device_manager.get_by_alias("specter") key = Key.from_json( { "derivation": "m/48h/1h/0h/2h", "original": "Vpub5n9kKePTPPGtw3RddeJWJe29epEyBBcoHbbPi5HhpoG2kTVsSCUzsad33RJUt3LktEUUPPofcZczuudnwR7ZgkAkT6N2K2Z7wdyjYrVAkXM", "fingerprint": "08686ac6", "type": "wsh", "xpub": "tpubDFHpKypXq4kwUrqLotPs6fCic5bFqTRGMBaTi9s5YwwGymE8FLGwB2kDXALxqvNwFxB1dLWYBmmeFVjmUSdt2AsaQuPmkyPLBKRZW8BGCiL", } ) wallet = wm.create_wallet("a_second_test_wallet", 1, "wpkh", [key], [device]) address = wallet.address assert wallet.getlabel(address) == "Address #0" wallet.setlabel(address, "Random label") assert wallet.getlabel(address) == "Random label" wallet.rpc.generatetoaddress(20, address) random_address = "mruae2834buqxk77oaVpephnA5ZAxNNJ1r" wallet.rpc.generatetoaddress(100, random_address) # update utxo wallet.getdata() # update balance wallet.get_balance() address_balance = wallet.fullbalance assert len(wallet.full_utxo) == 20 new_address = wallet.getnewaddress() wallet.setlabel(new_address, "") wallet.rpc.generatetoaddress(20, new_address) random_address = "mruae2834buqxk77oaVpephnA5ZAxNNJ1r" wallet.rpc.generatetoaddress(100, random_address) wallet.getdata() wallet.get_balance() assert len(wallet.full_utxo) == 40 wallet.setlabel(new_address, "") third_address = wallet.getnewaddress() wallet.getdata() assert sorted(wallet.addresses) == sorted([address, new_address, third_address])
def _check_duplicate_keys(cls, keys): """raise a SpecterError when a xpub in the passed KeyList is listed twice. Should prevent MultisigWallets where xpubs are used twice. """ # normalizing xpubs in order to ignore slip132 differences xpubs = [Key.parse_xpub(key.original).xpub for key in keys] for xpub in xpubs: if xpubs.count(xpub) > 1: raise SpecterError(_(f"xpub {xpub} seem to be used at least twice!"))
def test_write_device(app): a_key = Key( "Vpub5n9kKePTPPGtw3RddeJWJe29epEyBBcoHbbPi5HhpoG2kTVsSCUzsad33RJUt3LktEUUPPofcZczuudnwR7ZgkAkT6N2K2Z7wdyjYrVAkXM", "08686ac6", "m/48h/1h/0h/2h", "wsh", "tpubDFHpKypXq4kwUrqLotPs6fCic5bFqTRGMBaTi9s5YwwGymE8FLGwB2kDXALxqvNwFxB1dLWYBmmeFVjmUSdt2AsaQuPmkyPLBKRZW8BGCiL", ) # the DeviceManager doesn't care so much about the content of a key # so this is a minimal valid "key": another_key = Key.from_json( { "original": "tpubDDZ5jjGT5RvrAyjoLZfdCfv1PAPmicnhNctwZGKiCMF1Zy5hCGMqppxwYZzWgvPqk7LucMMHo7rkB6Dyj5ZLd2W62FAEP3U6pV4jD5gb9ma" } ) app.specter.device_manager.add_device("some_name", "the_type", [a_key, another_key]) write_device( app.specter.device_manager.get_by_alias("some_name"), "/tmp/delete_me_test_file.json", ) os.remove("/tmp/delete_me_test_file.json")
def parse_signers(self, devices, cosigners_types): keys = [] cosigners = [] unknown_cosigners = [] unknown_cosigners_types = [] if self.multisig_N == None: self.multisig_N = 1 self.multisig_M = 1 self.origin_fingerprint = [self.origin_fingerprint] self.origin_path = [self.origin_path] self.base_key = [self.base_key] for i in range(self.multisig_N): cosigner_found = False for device in devices: cosigner = devices[device] if self.origin_fingerprint[i] is None: self.origin_fingerprint[i] = "" if self.origin_path[i] is None: self.origin_path[i] = self.origin_fingerprint[i] for key in cosigner.keys: if key.fingerprint + key.derivation.replace( "m", "" ) == self.origin_fingerprint[i] + self.origin_path[i].replace( "'", "h" ): keys.append(key) cosigners.append(cosigner) cosigner_found = True break if cosigner_found: break if not cosigner_found: desc_key = Key.parse_xpub( "[{}{}]{}".format( self.origin_fingerprint[i], self.origin_path[i], self.base_key[i], ) ) if len(cosigners_types) > i: unknown_cosigners.append((desc_key, cosigners_types[i]["label"])) else: unknown_cosigners.append((desc_key, None)) if len(unknown_cosigners) > len(cosigners_types): unknown_cosigners_types.append("other") else: unknown_cosigners_types.append(cosigners_types[i]["type"]) return (keys, cosigners, unknown_cosigners, unknown_cosigners_types)
def parse_signers(self, devices, cosigners_types): keys = [] cosigners = [] unknown_cosigners = [] unknown_cosigners_types = [] for i, descriptor_key in enumerate(self.descriptor.keys): # remove derivation from the key for comparison account_key = DescriptorKey.from_string(str(descriptor_key)) account_key.allowed_derivation = None # Specter Key class desc_key = Key.parse_xpub(str(account_key)) cosigner_found = False for cosigner in devices.values(): for key in cosigner.keys: # check key matches if key.to_string(slip132=False) == desc_key.to_string( slip132=False ): keys.append(key) cosigners.append(cosigner) cosigner_found = True break if cosigner_found: break if not cosigner_found: if len(cosigners_types) > i: unknown_cosigners.append((desc_key, cosigners_types[i]["label"])) else: unknown_cosigners.append((desc_key, None)) if len(unknown_cosigners) > len(cosigners_types): unknown_cosigners_types.append("other") else: unknown_cosigners_types.append(cosigners_types[i]["type"]) return (keys, cosigners, unknown_cosigners, unknown_cosigners_types)
def test_wallet_labeling(bitcoin_regtest, devices_filled_data_folder, device_manager): wm = WalletManager(devices_filled_data_folder, bitcoin_regtest.get_rpc(), "regtest", device_manager) # A wallet-creation needs a device device = device_manager.get_by_alias('specter') key = Key.from_json({ "derivation": "m/48h/1h/0h/2h", "original": "Vpub5n9kKePTPPGtw3RddeJWJe29epEyBBcoHbbPi5HhpoG2kTVsSCUzsad33RJUt3LktEUUPPofcZczuudnwR7ZgkAkT6N2K2Z7wdyjYrVAkXM", "fingerprint": "08686ac6", "type": "wsh", "xpub": "tpubDFHpKypXq4kwUrqLotPs6fCic5bFqTRGMBaTi9s5YwwGymE8FLGwB2kDXALxqvNwFxB1dLWYBmmeFVjmUSdt2AsaQuPmkyPLBKRZW8BGCiL" }) wallet = wm.create_wallet('a_second_test_wallet', 1, 'wpkh', [key], [device]) address = wallet.address assert wallet.getlabel(address) == 'Address #0' wallet.setlabel(address, 'Random label') assert wallet.getlabel(address) == 'Random label' wallet.rpc.generatetoaddress(20, address) random_address = "mruae2834buqxk77oaVpephnA5ZAxNNJ1r" wallet.rpc.generatetoaddress(100, random_address) # update utxo wallet.getdata() # update balance wallet.get_balance() address_balance = wallet.fullbalance assert len(wallet.utxo) == 20 assert wallet.is_current_address_used assert wallet.balance_on_address(address) == address_balance assert wallet.balance_on_label('Random label') == address_balance assert wallet.addresses_on_label('Random label') == [address] assert wallet.utxo_addresses == [address] assert wallet.utxo_labels == ['Random label'] assert wallet.utxo_addresses == [address] new_address = wallet.getnewaddress() wallet.setlabel(new_address, '') wallet.rpc.generatetoaddress(20, new_address) random_address = "mruae2834buqxk77oaVpephnA5ZAxNNJ1r" wallet.rpc.generatetoaddress(100, random_address) wallet.getdata() wallet.get_balance() assert len(wallet.utxo) == 40 assert wallet.is_current_address_used assert wallet.utxo_on_address(address) == 20 assert wallet.balance_on_address( new_address) == wallet.fullbalance - address_balance assert sorted(wallet.utxo_addresses) == sorted([address, new_address]) assert sorted(wallet.utxo_labels) == sorted(['Random label', new_address]) assert sorted(wallet.utxo_addresses) == sorted([address, new_address]) assert wallet.get_address_name(new_address, -1) == new_address assert wallet.get_address_name(new_address, 5) == 'Address #5' assert wallet.get_address_name(address, 5) == 'Random label' wallet.setlabel(new_address, '') third_address = wallet.getnewaddress() wallet.getdata() assert sorted(wallet.labels) == sorted( ['Random label', new_address, 'Address #2']) assert sorted(wallet.utxo_labels) == sorted(['Random label', new_address]) assert sorted(wallet.addresses) == sorted( [address, new_address, third_address]) assert sorted(wallet.utxo_addresses) == sorted([address, new_address]) wallet.setlabel(third_address, 'Random label') wallet.getdata() assert sorted(wallet.addresses_on_label('Random label')) == sorted( [address, third_address])
def test_DeviceManager(empty_data_folder): # A DeviceManager manages devices, specifically the persistence # of them via json-files in an empty data folder dm = DeviceManager(data_folder=empty_data_folder) # initialization will load from the folder but it's empty at first assert len(dm.devices) == 0 # a device has a name, a type and a list of keys a_key = Key( 'Vpub5n9kKePTPPGtw3RddeJWJe29epEyBBcoHbbPi5HhpoG2kTVsSCUzsad33RJUt3LktEUUPPofcZczuudnwR7ZgkAkT6N2K2Z7wdyjYrVAkXM', '08686ac6', 'm/48h/1h/0h/2h', 'wsh', 'tpubDFHpKypXq4kwUrqLotPs6fCic5bFqTRGMBaTi9s5YwwGymE8FLGwB2kDXALxqvNwFxB1dLWYBmmeFVjmUSdt2AsaQuPmkyPLBKRZW8BGCiL' ) # the DeviceManager doesn't care so much about the content of a key # so this is a minimal valid "key": another_key = Key.from_json({'original': 'blub'}) dm.add_device("some_name", "the_type", [a_key, another_key]) # A json file was generated for the new device: assert os.path.isfile(dm.devices['some_name'].fullpath) # You can access the new device either by its name of with `get_by_alias` by its alias assert dm.get_by_alias('some_name').name == 'some_name' assert dm.get_by_alias('some_name').device_type == 'the_type' assert dm.get_by_alias('some_name').keys[0].fingerprint == '08686ac6' # Now it has a length of 1 assert len(dm.devices) == 1 # and is iterable assert [the_type.device_type for the_type in dm.devices.values()] == ['the_type'] # The DeviceManager will return Device-Types (subclass of dict) assert type(dm.devices['some_name']) == Device # The DeviceManager also has a `devices_names` property, returning a sorted list of the names of all devices assert dm.devices_names == ['some_name'] dm.add_device("another_name", "the_type", [a_key, another_key]) assert dm.devices_names == ['another_name', 'some_name'] # You can also remove a device - which will delete its json and remove it from the manager another_device_fullpath = dm.devices['another_name'].fullpath assert os.path.isfile(another_device_fullpath) dm.remove_device(dm.devices['another_name']) assert not os.path.isfile(another_device_fullpath) assert len(dm.devices) == 1 assert dm.devices_names == ['some_name'] # A device is mainly a Domain-Object which assumes an underlying # json-file which can be found in the "fullpath"-key # It derives from a dict # It needs a DeviceManager to be injected and can't reasonable # be created on your own. # It has 5 dict keys: `fullpath`, `alias`, `name`, `type`, `keys` some_device = dm.devices['some_name'] assert some_device.fullpath == empty_data_folder + '/some_name.json' assert some_device.alias == 'some_name' assert some_device.name == 'some_name' assert some_device.device_type == 'the_type' assert len(some_device.keys) == 2 assert some_device.keys[0] == a_key assert some_device.keys[1] == another_key # Keys can be added and removed. It will instantly update the underlying json # Adding keys can be done by passing an array of keys object to the `add_keys` method of a device # A key dict must contain an `original` property third_key = Key.from_json({'original': 'third_key'}) some_device.add_keys([third_key]) assert len(some_device.keys) == 3 assert some_device.keys[0] == a_key assert some_device.keys[1] == another_key assert some_device.keys[2] == third_key # adding an existing key will do nothing some_device.add_keys([third_key]) assert len(some_device.keys) == 3 assert some_device.keys[0] == a_key assert some_device.keys[1] == another_key assert some_device.keys[2] == third_key # Removing a key can be done by passing the `original` property of the key to remove to the `remove_key` method of a device some_device.remove_key(third_key) assert len(some_device.keys) == 2 assert some_device.keys[0] == a_key assert some_device.keys[1] == another_key # removing a none existing key will do nothing some_device.remove_key(third_key) assert len(some_device.keys) == 2 assert some_device.keys[0] == a_key assert some_device.keys[1] == another_key
def test_fingerprint(ghost_machine_xpub_44): key = Key.parse_xpub(ghost_machine_xpub_44) assert key.fingerprint == "81f802e3"
def test_derivation(ghost_machine_zpub): key = Key.parse_xpub(f"[81f802e3/84'/0'/3]{ghost_machine_zpub}") assert key.derivation == "m/84h/0h/3"
def test_WalletManager_check_duplicate_keys(empty_data_folder): wm = WalletManager( 200100, empty_data_folder, None, "regtest", None, allow_threading=False, ) key1 = Key( "[f3e6eaff/84h/0h/0h]xpub6C5cCQfycZrPJnNg6cDdUU5efJrab8thRQDBxSSB4gP2J3xGdWu8cqiLvPZkejtuaY9LursCn6Es9PqHgLhBktW8217BomGDVBAJjUms8iG", "f3e6eaff", "84h/0h/0h", "", None, "xpub6C5cCQfycZrPJnNg6cDdUU5efJrab8thRQDBxSSB4gP2J3xGdWu8cqiLvPZkejtuaY9LursCn6Es9PqHgLhBktW8217BomGDVBAJjUms8iG", ) key2 = Key( "[1ef4e492/49h/0h/0h]xpub6CRWp2zfwRYsVTuT2p96hKE2UT4vjq9gwvW732KWQjwoG7v6NCXyaTdz7NE5yDxsd72rAGK7qrjF4YVrfZervsJBjsXxvTL98Yhc7poBk7K", "1ef4e492", "m/49h/0h/0h", "sh-wpkh", None, "xpub6CRWp2zfwRYsVTuT2p96hKE2UT4vjq9gwvW732KWQjwoG7v6NCXyaTdz7NE5yDxsd72rAGK7qrjF4YVrfZervsJBjsXxvTL98Yhc7poBk7K", ) key3 = Key( "[1ef4e492/49h/0h/0h]zpub6qk8ok1ouvwM1NkumKnsteGf1F9UUNshFdFdXEDwph8nQFaj8qEFry2cxoUveZCkPpNxQp4KhQwxuy4R7jXDMMsKkgW2yauC2dHbWYnr2Ee", "1ef4e492", "m/49h/0h/0h", "sh-wpkh", None, "zpub6qk8ok1ouvwM1NkumKnsteGf1F9UUNshFdFdXEDwph8nQFaj8qEFry2cxoUveZCkPpNxQp4KhQwxuy4R7jXDMMsKkgW2yauC2dHbWYnr2Ee", ) key4 = Key( "[6ea15da6/49h/0h/0h]xpub6BtcNhqbaFaoC3oEfKky3Sm22pF48U2jmAf78cB3wdAkkGyAgmsVrgyt1ooSt3bHWgzsdUQh2pTJ867yTeUAMmFDKNSBp8J7WPmp7Df7zjv", "6ea15da6", "m/49h/0h/0h", "sh-wpkh", None, "xpub6BtcNhqbaFaoC3oEfKky3Sm22pF48U2jmAf78cB3wdAkkGyAgmsVrgyt1ooSt3bHWgzsdUQh2pTJ867yTeUAMmFDKNSBp8J7WPmp7Df7zjv", ) key5 = Key( "[6ea15da6/49h/0h/0h]xpub6BtcNhqbaFaoG3xcuncx9xzL3X38FuWXdcdvsdG5Q99Cb4EgeVYPEYaVpX28he6472gEsCokg8v9oMVRTrZNe5LHtGSPcC5ofehYkhD1Kxy", "6ea15da6", "m/49h/0h/1h", "sh-wpkh", None, # slightly different ypub than key4 "xpub6BtcNhqbaFaoG3xcuncx9xzL3X38FuWXdcdvsdG5Q99Cb4EgeVYPEYaVpX28he6472gEsCokg8v9oMVRTrZNe5LHtGSPcC5ofehYkhD1Kxy", ) # Case 1: Identical keys keys = [key1, key1] with pytest.raises(SpecterError): wm._check_duplicate_keys(keys) # Case 2: different keys # key2 and 3 are different as they don't have the same xpub. See #1500 for discussion keys = [key1, key2, key3] # key2 xpub is the same than key3 zpub with pytest.raises(SpecterError): wm._check_duplicate_keys(keys) keys = [key4, key5] wm._check_duplicate_keys(keys)
def test_key_type(ghost_machine_ypub): key = Key.parse_xpub(ghost_machine_ypub) assert key.key_type == "sh-wpkh"
def test_wallet_labeling(bitcoin_regtest, devices_filled_data_folder, device_manager): wm = WalletManager( 200100, devices_filled_data_folder, bitcoin_regtest.get_rpc(), "regtest", device_manager, allow_threading=False, ) # A wallet-creation needs a device device = device_manager.get_by_alias("specter") key = Key.from_json({ "derivation": "m/48h/1h/0h/2h", "original": "Vpub5n9kKePTPPGtw3RddeJWJe29epEyBBcoHbbPi5HhpoG2kTVsSCUzsad33RJUt3LktEUUPPofcZczuudnwR7ZgkAkT6N2K2Z7wdyjYrVAkXM", "fingerprint": "08686ac6", "type": "wsh", "xpub": "tpubDFHpKypXq4kwUrqLotPs6fCic5bFqTRGMBaTi9s5YwwGymE8FLGwB2kDXALxqvNwFxB1dLWYBmmeFVjmUSdt2AsaQuPmkyPLBKRZW8BGCiL", }) wallet = wm.create_wallet("a_second_test_wallet", 1, "wpkh", [key], [device]) address = wallet.address assert wallet.getlabel(address) == "Address #0" wallet.setlabel(address, "Random label") assert wallet.getlabel(address) == "Random label" wallet.rpc.generatetoaddress(20, address) random_address = "mruae2834buqxk77oaVpephnA5ZAxNNJ1r" wallet.rpc.generatetoaddress(100, random_address) # update utxo wallet.getdata() # update balance wallet.update_balance() address_balance = wallet.fullbalance assert len(wallet.full_utxo) == 20 print(wallet.full_utxo[4]) # Something like: # { 'txid': 'fab823558781745179916b4bfdfd65b382bfc0e70e85188f1b9538604202f537', # 'vout': 0, 'address': 'bcrt1qmlrraffw0evkjy2yrxmt263ksgfgv2gqhcddrt', # 'label': 'Random label', 'scriptPubKey': '0014dfc63ea52e7e5969114419b6b56a368212862900', # 'amount': 50.0, 'confirmations': 101, 'spendable': False, 'solvable': True, # 'desc': "wpkh([08686ac6/48'/1'/0'/2'/0/0]02fa445808af849209038f422a22e335754fa07a2ece42fc483660606dcda3e0e9)#8q60z40m", # 'safe': True, 'time': 1637091575, 'category': 'generate', 'locked': False # } new_address = wallet.getnewaddress() wallet.setlabel(new_address, "") wallet.rpc.generatetoaddress(20, new_address) random_address = "mruae2834buqxk77oaVpephnA5ZAxNNJ1r" wallet.rpc.generatetoaddress(100, random_address) wallet.getdata() wallet.update_balance() assert len(wallet.full_utxo) == 40 wallet.setlabel(new_address, "") third_address = wallet.getnewaddress() wallet.getdata() assert sorted(wallet.addresses) == sorted( [address, new_address, third_address])
def test_DeviceManager(empty_data_folder): # A DeviceManager manages devices, specifically the persistence # of them via json-files in an empty data folder dm = DeviceManager(data_folder=empty_data_folder) # initialization will load from the folder but it's empty at first assert len(dm.devices) == 0 # a device has a name, a type and a list of keys a_key = Key( "Vpub5n9kKePTPPGtw3RddeJWJe29epEyBBcoHbbPi5HhpoG2kTVsSCUzsad33RJUt3LktEUUPPofcZczuudnwR7ZgkAkT6N2K2Z7wdyjYrVAkXM", "08686ac6", "m/48h/1h/0h/2h", "wsh", "", "tpubDFHpKypXq4kwUrqLotPs6fCic5bFqTRGMBaTi9s5YwwGymE8FLGwB2kDXALxqvNwFxB1dLWYBmmeFVjmUSdt2AsaQuPmkyPLBKRZW8BGCiL", ) # the DeviceManager doesn't care so much about the content of a key # so this is a minimal valid "key": another_key = Key.from_json( { "original": "tpubDDZ5jjGT5RvrAyjoLZfdCfv1PAPmicnhNctwZGKiCMF1Zy5hCGMqppxwYZzWgvPqk7LucMMHo7rkB6Dyj5ZLd2W62FAEP3U6pV4jD5gb9ma" } ) dm.add_device("some_name", "the_type", [a_key, another_key]) # A json file was generated for the new device: assert os.path.isfile(dm.devices["some_name"].fullpath) # You can access the new device either by its name of with `get_by_alias` by its alias assert dm.get_by_alias("some_name").name == "some_name" # unknown device is replaced by 'other' assert dm.get_by_alias("some_name").device_type == "other" assert dm.get_by_alias("some_name").keys[0].fingerprint == "08686ac6" # Now it has a length of 1 assert len(dm.devices) == 1 # and is iterable assert [the_type.device_type for the_type in dm.devices.values()] == ["other"] # The DeviceManager will return Device-Types (subclass of dict) # any unknown type is replaced by GenericDevice assert type(dm.devices["some_name"]) == GenericDevice # The DeviceManager also has a `devices_names` property, returning a sorted list of the names of all devices assert dm.devices_names == ["some_name"] dm.add_device("another_name", "the_type", [a_key, another_key]) assert dm.devices_names == ["another_name", "some_name"] # You can also remove a device - which will delete its json and remove it from the manager another_device_fullpath = dm.devices["another_name"].fullpath assert os.path.isfile(another_device_fullpath) dm.remove_device(dm.devices["another_name"]) assert not os.path.isfile(another_device_fullpath) assert len(dm.devices) == 1 assert dm.devices_names == ["some_name"] # A device is mainly a Domain-Object which assumes an underlying # json-file which can be found in the "fullpath"-key # It derives from a dict # It needs a DeviceManager to be injected and can't reasonable # be created on your own. # It has 5 dict keys: `fullpath`, `alias`, `name`, `type`, `keys` some_device = dm.devices["some_name"] assert some_device.fullpath == empty_data_folder + "/some_name.json" assert some_device.alias == "some_name" assert some_device.name == "some_name" assert some_device.device_type == "other" assert len(some_device.keys) == 2 assert some_device.keys[0] == a_key assert some_device.keys[1] == another_key # Keys can be added and removed. It will instantly update the underlying json # Adding keys can be done by passing an array of keys object to the `add_keys` method of a device # A key dict must contain an `original` property third_key = Key.from_json( { "original": "tpubDEmTg3b5aPNFnkHXx481F3h9dPSVJiyvqV24dBMXWncoRRu6VJzPDeEtQ4H7EnRtLbn2aPkxhTn8odWXsXkSRDdmAvCCrPmfjfPSVswfDhg" } ) some_device.add_keys([third_key]) assert len(some_device.keys) == 3 assert some_device.keys[0] == a_key assert some_device.keys[1] == another_key assert some_device.keys[2] == third_key # adding an existing key will do nothing some_device.add_keys([third_key]) assert len(some_device.keys) == 3 assert some_device.keys[0] == a_key assert some_device.keys[1] == another_key assert some_device.keys[2] == third_key # Removing a key can be done by passing the `original` property of the key to remove to the `remove_key` method of a device some_device.remove_key(third_key) assert len(some_device.keys) == 2 assert some_device.keys[0] == a_key assert some_device.keys[1] == another_key # removing a none existing key will do nothing some_device.remove_key(third_key) assert len(some_device.keys) == 2 assert some_device.keys[0] == a_key assert some_device.keys[1] == another_key
def test_purpose(ghost_machine_ypub): key = Key.parse_xpub(ghost_machine_ypub) assert key.purpose == "Single (Nested)"
def test_xpub(ghost_machine_ypub, ghost_machine_xpub_49): key = Key.parse_xpub(ghost_machine_ypub) assert key.xpub == ghost_machine_xpub_49
def test_wallet_createpsbt(docker, request, devices_filled_data_folder, device_manager): # Instantiate a fresh bitcoind instance to isolate this test. bitcoind_controller = instantiate_bitcoind_controller(docker, request, rpcport=18978) try: wm = WalletManager( 200100, devices_filled_data_folder, bitcoind_controller.rpcconn.get_rpc(), "regtest", device_manager, allow_threading=False, ) # A wallet-creation needs a device device = device_manager.get_by_alias("specter") key = Key.from_json({ "derivation": "m/48h/1h/0h/2h", "original": "Vpub5n9kKePTPPGtw3RddeJWJe29epEyBBcoHbbPi5HhpoG2kTVsSCUzsad33RJUt3LktEUUPPofcZczuudnwR7ZgkAkT6N2K2Z7wdyjYrVAkXM", "fingerprint": "08686ac6", "type": "wsh", "xpub": "tpubDFHpKypXq4kwUrqLotPs6fCic5bFqTRGMBaTi9s5YwwGymE8FLGwB2kDXALxqvNwFxB1dLWYBmmeFVjmUSdt2AsaQuPmkyPLBKRZW8BGCiL", }) wallet = wm.create_wallet("a_second_test_wallet", 1, "wpkh", [key], [device]) # Let's fund the wallet with ... let's say 40 blocks a 50 coins each --> 200 coins address = wallet.getnewaddress() assert address == "bcrt1qtnrv2jpygx2ef3zqfjhqplnycxak2m6ljnhq6z" wallet.rpc.generatetoaddress(20, address) # in two addresses address = wallet.getnewaddress() wallet.rpc.generatetoaddress(20, address) # newly minted coins need 100 blocks to get spendable # let's mine another 100 blocks to get these coins spendable random_address = "mruae2834buqxk77oaVpephnA5ZAxNNJ1r" wallet.rpc.generatetoaddress(110, random_address) # update the wallet data wallet.update_balance() # Now we have loads of potential inputs # Let's spend 500 coins assert wallet.fullbalance >= 250 # From this print-statement, let's grab some txids which we'll use for coinselect unspents = wallet.rpc.listunspent(0) # Lets take 3 more or less random txs from the unspents: selected_coins = [{ "txid": u["txid"], "vout": u["vout"] } for u in [unspents[5], unspents[9], unspents[12]]] selected_coins_amount_sum = (unspents[5]["amount"] + unspents[9]["amount"] + unspents[12]["amount"]) number_of_coins_to_spend = (selected_coins_amount_sum - 0.1 ) # Let's spend almost all of them psbt = wallet.createpsbt( [random_address], [number_of_coins_to_spend], True, 0, 10, selected_coins=selected_coins, ) assert len(psbt["tx"]["vin"]) == 3 psbt_txs = [tx["txid"] for tx in psbt["tx"]["vin"]] for coin in selected_coins: assert coin["txid"] in psbt_txs # Now let's spend more coins than we have selected. This should result in an exception: try: psbt = wallet.createpsbt( [random_address], [number_of_coins_to_spend + 1], True, 0, 10, selected_coins=selected_coins, ) assert False, "should throw an exception!" except SpecterError as e: pass assert wallet.locked_amount == selected_coins_amount_sum assert len(wallet.rpc.listlockunspent()) == 3 assert (wallet.full_available_balance == wallet.fullbalance - selected_coins_amount_sum) wallet.delete_pending_psbt(psbt["tx"]["txid"]) assert wallet.locked_amount == 0 assert len(wallet.rpc.listlockunspent()) == 0 assert wallet.full_available_balance == wallet.fullbalance finally: # cleanup bitcoind_controller.stop_bitcoind()