Browse Source

imported wallets: respect "use_change" option; default off

Imported wallets used to send change back to the "from address".
We keep this behaviour as default.

There has already been an option "Use change addresses" (exposed in GUI),
ignored so far by imported wallets (was only used by HD wallets).
With this change, imported wallets no longer ignore that option, and if set,
they will send change to a random unused imported address, instead of back to "from address".
If all addresses are used, it falls back to sending change back to the "from address".

see: https://github.com/spesmilo/electrum/pull/7330
see: https://github.com/spesmilo/electrum/issues/5353
patch-4
SomberNight 4 years ago
parent
commit
fbd8c5f7b0
No known key found for this signature in database GPG Key ID: B33B5F232C6271E9
  1. 117
      electrum/tests/test_wallet_vertical.py
  2. 19
      electrum/wallet.py

117
electrum/tests/test_wallet_vertical.py

@ -2197,6 +2197,123 @@ class TestWalletSending(TestCaseForTestnet):
self.assertEqual(1, len(tx.inputs()))
self.assertEqual(2, len(tx.outputs()))
@mock.patch.object(wallet.Abstract_Wallet, 'save_db')
def test_imported_wallet_usechange_off(self, mock_save_db):
wallet = restore_wallet_from_text(
"p2wpkh:cVcwSp488C8Riguq55Tuktgi6TpzuyLdDwUxkBDBz3yzV7FW4af2 p2wpkh:cPWyoPvnv2hiyyxbhMkhX3gPEENzB6DqoP9bbR8SDTg5njK5SL9n",
path='if_this_exists_mocking_failed_648151893',
config=self.config)['wallet'] # type: Abstract_Wallet
# bootstrap wallet
funding_tx = Transaction('02000000000101c6edaaf0157020a38de8b07810b22ffe331d5b79c83b680dad24da15c572ae7d0000000000fdffffff026080010000000000160014eabbd791df76eeeaa3ed273cac4e1dde3be295cca0860100000000001600147a65e09bb1da80abfc65d545388a2e61aab7c7210247304402203cb8b2f84ed4fb8de5f51a07b2159bc0d8d474e5dba0f77cc66ab641cf48621b022076fb3c6b4bc76aa06dd29ebe1dd081c063cdbd2949ffcf4ab4bd8bddae6c948b0121029f16b602a6b3c738b66a03dd5133abe810169a377bbc2fdf5c5363f59b8d9bdec3951e00')
funding_txid = funding_tx.txid()
self.assertEqual('9bed2a210b4154183295bc7b78c8841a3a6116197713f744e5cd95ab0c0c01ce', funding_txid)
wallet.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED)
# imported wallets do not send change to change addresses by default
# (they send it back to the "from address")
self.assertFalse(wallet.use_change)
outputs = [PartialTxOutput.from_address_and_value('tb1qq4pypzwxf5uanfyckmsu3ejxxf6rrvjqchza3v', 49646)]
coins = wallet.get_spendable_coins(domain=None)
tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee=1000)
tx.set_rbf(True)
tx.locktime = 2004420
tx.version = 2
# check that change is sent back to the "from address"
self.assertEqual(2, len(tx.outputs()))
self.assertTrue(tx.output_value_for_address("tb1q0fj7pxa3m2q2hlr964zn3z3wvx4t03ep5fgnhy") > 0)
self.assertEqual(49646, tx.output_value_for_address("tb1qq4pypzwxf5uanfyckmsu3ejxxf6rrvjqchza3v"))
self.assertEqual("70736274ff0100710200000001ce010c0cab95cde544f713771916613a1a84c8787bbc95321854410b212aed9b0100000000fdffffff02cac00000000000001600147a65e09bb1da80abfc65d545388a2e61aab7c721eec100000000000016001405424089c64d39d9a498b6e1c8e646327431b240c4951e00000100de02000000000101c6edaaf0157020a38de8b07810b22ffe331d5b79c83b680dad24da15c572ae7d0000000000fdffffff026080010000000000160014eabbd791df76eeeaa3ed273cac4e1dde3be295cca0860100000000001600147a65e09bb1da80abfc65d545388a2e61aab7c7210247304402203cb8b2f84ed4fb8de5f51a07b2159bc0d8d474e5dba0f77cc66ab641cf48621b022076fb3c6b4bc76aa06dd29ebe1dd081c063cdbd2949ffcf4ab4bd8bddae6c948b0121029f16b602a6b3c738b66a03dd5133abe810169a377bbc2fdf5c5363f59b8d9bdec3951e00000000",
tx.serialize_as_bytes().hex())
wallet.sign_transaction(tx, password=None)
tx_copy = tx_from_any(tx.serialize())
self.assertEqual('02000000000101ce010c0cab95cde544f713771916613a1a84c8787bbc95321854410b212aed9b0100000000fdffffff02cac00000000000001600147a65e09bb1da80abfc65d545388a2e61aab7c721eec100000000000016001405424089c64d39d9a498b6e1c8e646327431b240024730440220526eac6c56cba19842b67f6c9e45af113b1a2d44fb229335bdeaf08cb2cc164e0220087fba65619016fd3f62f6c8717070e48f94b45743b86d8e0517698d2b9c3afc012102d67eaa10463f5c786271feb9ae3456c27d35c3cf6c7d881617e915d1f32cb875c4951e00',
str(tx_copy))
@mock.patch.object(wallet.Abstract_Wallet, 'save_db')
def test_imported_wallet_usechange_on(self, mock_save_db):
wallet = restore_wallet_from_text(
"p2wpkh:cVcwSp488C8Riguq55Tuktgi6TpzuyLdDwUxkBDBz3yzV7FW4af2 p2wpkh:cPWyoPvnv2hiyyxbhMkhX3gPEENzB6DqoP9bbR8SDTg5njK5SL9n",
path='if_this_exists_mocking_failed_648151893',
config=self.config)['wallet'] # type: Abstract_Wallet
# bootstrap wallet
funding_tx = Transaction('02000000000101c6edaaf0157020a38de8b07810b22ffe331d5b79c83b680dad24da15c572ae7d0000000000fdffffff026080010000000000160014eabbd791df76eeeaa3ed273cac4e1dde3be295cca0860100000000001600147a65e09bb1da80abfc65d545388a2e61aab7c7210247304402203cb8b2f84ed4fb8de5f51a07b2159bc0d8d474e5dba0f77cc66ab641cf48621b022076fb3c6b4bc76aa06dd29ebe1dd081c063cdbd2949ffcf4ab4bd8bddae6c948b0121029f16b602a6b3c738b66a03dd5133abe810169a377bbc2fdf5c5363f59b8d9bdec3951e00')
funding_txid = funding_tx.txid()
self.assertEqual('9bed2a210b4154183295bc7b78c8841a3a6116197713f744e5cd95ab0c0c01ce', funding_txid)
wallet.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED)
# instead of sending the change back to the "from address", we want it sent to another unused address
wallet.use_change = True
outputs = [PartialTxOutput.from_address_and_value('tb1qq4pypzwxf5uanfyckmsu3ejxxf6rrvjqchza3v', 49646)]
coins = wallet.get_spendable_coins(domain=None)
tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee=1000)
tx.set_rbf(True)
tx.locktime = 2004420
tx.version = 2
# check that change is sent to another unused imported address
self.assertEqual(2, len(tx.outputs()))
self.assertTrue(tx.output_value_for_address("tb1qetcgdwuzlpdnt5fmzxxdpczjhadz06cynpttpv") > 0)
self.assertEqual(49646, tx.output_value_for_address("tb1qq4pypzwxf5uanfyckmsu3ejxxf6rrvjqchza3v"))
self.assertEqual("70736274ff0100710200000001ce010c0cab95cde544f713771916613a1a84c8787bbc95321854410b212aed9b0100000000fdffffff02cac0000000000000160014caf086bb82f85b35d13b118cd0e052bf5a27eb04eec100000000000016001405424089c64d39d9a498b6e1c8e646327431b240c4951e00000100de02000000000101c6edaaf0157020a38de8b07810b22ffe331d5b79c83b680dad24da15c572ae7d0000000000fdffffff026080010000000000160014eabbd791df76eeeaa3ed273cac4e1dde3be295cca0860100000000001600147a65e09bb1da80abfc65d545388a2e61aab7c7210247304402203cb8b2f84ed4fb8de5f51a07b2159bc0d8d474e5dba0f77cc66ab641cf48621b022076fb3c6b4bc76aa06dd29ebe1dd081c063cdbd2949ffcf4ab4bd8bddae6c948b0121029f16b602a6b3c738b66a03dd5133abe810169a377bbc2fdf5c5363f59b8d9bdec3951e00000000",
tx.serialize_as_bytes().hex())
wallet.sign_transaction(tx, password=None)
tx_copy = tx_from_any(tx.serialize())
self.assertEqual('02000000000101ce010c0cab95cde544f713771916613a1a84c8787bbc95321854410b212aed9b0100000000fdffffff02cac0000000000000160014caf086bb82f85b35d13b118cd0e052bf5a27eb04eec100000000000016001405424089c64d39d9a498b6e1c8e646327431b24002473044022006dfe30f851b0174e5c920fd5b2e294a25fe5d449b17b422f3fda485d514c39b022047a6760f9d6ddfac5273094bed1f640fc1622a42938ebfb0b5f61cce7b161a00012102d67eaa10463f5c786271feb9ae3456c27d35c3cf6c7d881617e915d1f32cb875c4951e00',
str(tx_copy))
@mock.patch.object(wallet.Abstract_Wallet, 'save_db')
def test_imported_wallet_usechange_on__no_more_unused_addresses(self, mock_save_db):
wallet = restore_wallet_from_text(
"p2wpkh:cVcwSp488C8Riguq55Tuktgi6TpzuyLdDwUxkBDBz3yzV7FW4af2 p2wpkh:cPWyoPvnv2hiyyxbhMkhX3gPEENzB6DqoP9bbR8SDTg5njK5SL9n",
path='if_this_exists_mocking_failed_648151893',
config=self.config)['wallet'] # type: Abstract_Wallet
# bootstrap wallet
funding_tx = Transaction('02000000000101c6edaaf0157020a38de8b07810b22ffe331d5b79c83b680dad24da15c572ae7d0000000000fdffffff026080010000000000160014eabbd791df76eeeaa3ed273cac4e1dde3be295cca0860100000000001600147a65e09bb1da80abfc65d545388a2e61aab7c7210247304402203cb8b2f84ed4fb8de5f51a07b2159bc0d8d474e5dba0f77cc66ab641cf48621b022076fb3c6b4bc76aa06dd29ebe1dd081c063cdbd2949ffcf4ab4bd8bddae6c948b0121029f16b602a6b3c738b66a03dd5133abe810169a377bbc2fdf5c5363f59b8d9bdec3951e00')
funding_txid = funding_tx.txid()
self.assertEqual('9bed2a210b4154183295bc7b78c8841a3a6116197713f744e5cd95ab0c0c01ce', funding_txid)
wallet.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED)
# add more txs so that all addresses become used
_txs = [
("077c8f7a3b0cbb660192c3e35d01a65694f7b90b10e4c6434713912c44cdbfb7", "02000000000101bc125beec2014e3b89679207116e28bcf5bf85cab63ac2903119c8c21ab84cac0100000000fdffffff02daff000000000000160014caf086bb82f85b35d13b118cd0e052bf5a27eb04814201000000000016001491145275b4c4a4814b733fbd28f2a519a5874bad02473044022008ae14e4f7802639a34e92348db7eef95c9fb5d480d7a110d4b11e7d0c45a0cc02205d29414eebcdc76a07f5e2422ed3e560cd663de4b733a0f9c7b3ad7102a733510121030438b8bdbe8121b6a6508e54247b9d1b0547d9ac94c4d3154afd7d7376fe7ae6b6951e00"),
("5f8e17612ad4e04819f1b1cf9039509518e230db07140b2eec81582a8647f8d6", "02000000000101b7bfcd442c91134743c6e4100bb9f79456a6015de3c3920166bb0c3b7a8f7c070000000000fdffffff016cff0000000000001600146a84f3681e545d13fa41de090b6e404401198e7d0247304402204e16704d836cb6e1fffa34244c42578267853e8c3933a3d367bd6a236c24596a0220025a7be9483eeba06a433b96b5cb35a6a4b117ffa884569b09cedc4a5f3d6381012103c19caa2ced1b74bf31ba7885d83eeda35c0011e740273ebdf6750e0298588cc5c7951e00"),
]
for txid, rawtx in _txs:
tx = Transaction(rawtx)
self.assertEqual(txid, tx.txid())
wallet.receive_tx_callback(txid, tx, TX_HEIGHT_UNCONFIRMED)
# instead of sending the change back to the "from address", we want it sent to another unused address.
# (except all our addresses are used! so we expect change sent back to "from address")
wallet.use_change = True
outputs = [PartialTxOutput.from_address_and_value('tb1qq4pypzwxf5uanfyckmsu3ejxxf6rrvjqchza3v', 49646)]
coins = wallet.get_spendable_coins(domain=None)
tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee=1000)
tx.set_rbf(True)
tx.locktime = 2004420
tx.version = 2
# check that change is sent back to the "from address"
self.assertEqual(2, len(tx.outputs()))
self.assertTrue(tx.output_value_for_address("tb1q0fj7pxa3m2q2hlr964zn3z3wvx4t03ep5fgnhy") > 0)
self.assertEqual(49646, tx.output_value_for_address("tb1qq4pypzwxf5uanfyckmsu3ejxxf6rrvjqchza3v"))
self.assertEqual("70736274ff0100710200000001ce010c0cab95cde544f713771916613a1a84c8787bbc95321854410b212aed9b0100000000fdffffff02cac00000000000001600147a65e09bb1da80abfc65d545388a2e61aab7c721eec100000000000016001405424089c64d39d9a498b6e1c8e646327431b240c4951e00000100de02000000000101c6edaaf0157020a38de8b07810b22ffe331d5b79c83b680dad24da15c572ae7d0000000000fdffffff026080010000000000160014eabbd791df76eeeaa3ed273cac4e1dde3be295cca0860100000000001600147a65e09bb1da80abfc65d545388a2e61aab7c7210247304402203cb8b2f84ed4fb8de5f51a07b2159bc0d8d474e5dba0f77cc66ab641cf48621b022076fb3c6b4bc76aa06dd29ebe1dd081c063cdbd2949ffcf4ab4bd8bddae6c948b0121029f16b602a6b3c738b66a03dd5133abe810169a377bbc2fdf5c5363f59b8d9bdec3951e00000000",
tx.serialize_as_bytes().hex())
wallet.sign_transaction(tx, password=None)
tx_copy = tx_from_any(tx.serialize())
self.assertEqual('02000000000101ce010c0cab95cde544f713771916613a1a84c8787bbc95321854410b212aed9b0100000000fdffffff02cac00000000000001600147a65e09bb1da80abfc65d545388a2e61aab7c721eec100000000000016001405424089c64d39d9a498b6e1c8e646327431b240024730440220526eac6c56cba19842b67f6c9e45af113b1a2d44fb229335bdeaf08cb2cc164e0220087fba65619016fd3f62f6c8717070e48f94b45743b86d8e0517698d2b9c3afc012102d67eaa10463f5c786271feb9ae3456c27d35c3cf6c7d881617e915d1f32cb875c4951e00',
str(tx_copy))
class TestWalletOfflineSigning(TestCaseForTestnet):

19
electrum/wallet.py

@ -2640,6 +2640,7 @@ class Imported_Wallet(Simple_Wallet):
def __init__(self, db, storage, *, config):
Abstract_Wallet.__init__(self, db, storage, config=config)
self.use_change = db.get('use_change', False)
def is_watching_only(self):
return self.keystore is None
@ -2682,7 +2683,7 @@ class Imported_Wallet(Simple_Wallet):
return self.get_addresses()
def get_change_addresses(self, **kwargs):
return []
return self.get_addresses()
def import_addresses(self, addresses: List[str], *,
write_to_disk=True) -> Tuple[List[str], List[Tuple[str, str]]]:
@ -2749,6 +2750,22 @@ class Imported_Wallet(Simple_Wallet):
self.save_keystore()
self.save_db()
def get_change_addresses_for_new_transaction(self, *args, **kwargs) -> List[str]:
# for an imported wallet, if all "change addresses" are already used,
# it is probably better to send change back to the "from address", than to
# send it to another random used address and link them together, hence
# we force "allow_reusing_used_change_addrs=False"
return super().get_change_addresses_for_new_transaction(
*args,
**{**kwargs, "allow_reusing_used_change_addrs": False},
)
def calc_unused_change_addresses(self) -> Sequence[str]:
with self.lock:
unused_addrs = [addr for addr in self.get_change_addresses()
if not self.is_used(addr) and not self.is_address_reserved(addr)]
return unused_addrs
def is_mine(self, address) -> bool:
if not address: return False
return self.db.has_imported_address(address)

Loading…
Cancel
Save