1
0
Files
electrum/tests/test_timelock_recovery.py
Oren 2fb0dd066f Timelock Recovery Extension (#9589)
* Timelock Recovery Extension

* Timelock Recovery Extension tests

* Use fee_policy instead of fee_est

Following 3f327eea07

* making tx with base_tx

Following ab14c3e138

* move plugin metadata from __init__.py to manifest.json

* removing json large indentation

* timelock recovery icon

* timelock recovery plugin: fix typos

* timelock recovery plugin: use menu instead of status bar.

The status bar should be used for displaying status. For example,
hardware wallet plugins use it because their connection status is
changing and needs to be displayed.

* timelock recovery plugin: ask for password only once

* timelock recovery plugin: ask whether to create cancellation tx in the initial window

* remove unnecessary code.

(calling run_hook from a plugin does not make sense)

* show alert and cancellation address at the end.

skip unnecessary dialog

* timelock recovery plugin: do not show transactions one by one.

Set the fee policy in the first dialog, and use the same fee
policy for all tx. We could add 3 sliders to this dialog, if
different fees are needed, but I think this really isn't
really necessary.

* simplify default_wallet for tests

All the lightning-related stuff is irrelevant for
this plugin.

Also use a different destination address
for the test recovery-plan (an address
that does not belong to the same wallet).

* Fee selection should be above fee calculation

also show fee calculation result with "fee: " label.

* hide Sign and Broadcast buttons during view

* recalculate cancellation transaction

The checkbox could be clicked after the fee rate
has been set. Calling update_transactions() may seem
inefficient, but it's the simplest way to avoid such edge-cases.

Also set the context's cancellation transaction to None when the
checkbox is unset.

* use context.cancellation_tx instead of checkbox value

context.cancellation_tx will be None iff the checkbox was unset

* hide cancellation address if not used

* init monospace font correctly

* timelock recovery plugin: add input info at signing time.

Fixes trezor exception: 'Missing previous tx'

* timelock recovery: remove unused parameters

* avoid saving the tx in a separate var

fixing the assertions

* avoid caching recovery & cancellation inputs

* timelock recovery: separate help window from agreement.

move agreement at the end of the flow, rephrase it

* do not cache alert_tx_outputs

* do not crash when not enough funds

not enough funds can happen
when multiple addresses are specified
in payto_e, with an amount larger
than the wallet has - so we set
the payto_e color to red.

It can also happen when the user
selects a really high fee, but this
is not common in a "recovery"
wallet with significant funds.

* If files not saved - ask before closing

* move the checkbox above the save buttons

people read the text from top to
bottom and may not understand
why the buttons are disabled

---------

Co-authored-by: f321x <f321x@tutamail.com>
Co-authored-by: ThomasV <thomasv@electrum.org>
2025-04-22 10:02:01 +02:00

122 lines
5.3 KiB
Python

from io import StringIO
import json
import os, sys
from electrum.bitcoin import address_to_script
from electrum.fee_policy import FixedFeePolicy
from electrum.plugins.timelock_recovery.timelock_recovery import TimelockRecoveryContext
from electrum.simple_config import SimpleConfig
from electrum.storage import WalletStorage
from electrum.transaction import PartialTxOutput
from electrum.wallet import Wallet
from electrum.wallet_db import WalletDB
from . import ElectrumTestCase
class TestTimelockRecovery(ElectrumTestCase):
TESTNET = True
def setUp(self):
super(TestTimelockRecovery, self).setUp()
self.config = SimpleConfig({'electrum_path': self.electrum_path})
self.wallet_path = os.path.join(self.electrum_path, "timelock_recovery_wallet")
self._saved_stdout = sys.stdout
self._stdout_buffer = StringIO()
sys.stdout = self._stdout_buffer
def tearDown(self):
super(TestTimelockRecovery, self).tearDown()
# Restore the "real" stdout
sys.stdout = self._saved_stdout
def _create_default_wallet(self):
with open(os.path.join(os.path.dirname(__file__), "test_timelock_recovery", "default_wallet"), "r") as f:
wallet_str = f.read()
storage = WalletStorage(self.wallet_path)
db = WalletDB(wallet_str, storage=storage, upgrade=True)
wallet = Wallet(db, config=self.config)
return wallet
async def test_get_alert_address(self):
wallet = self._create_default_wallet()
context = TimelockRecoveryContext(wallet)
alert_address = context.get_alert_address()
self.assertEqual(alert_address, 'tb1qchyc02y9mv4xths4je9puc4yzuxt8rfm26ef07')
async def test_get_cancellation_address(self):
wallet = self._create_default_wallet()
context = TimelockRecoveryContext(wallet)
context.get_alert_address()
cancellation_address = context.get_cancellation_address()
self.assertEqual(cancellation_address, 'tb1q6k5h4cz6ra8nzhg90xm9wldvadgh0fpttfthcg')
async def test_make_unsigned_alert_tx(self):
wallet = self._create_default_wallet()
context = TimelockRecoveryContext(wallet)
context.outputs = [
PartialTxOutput(scriptpubkey=address_to_script('tb1q4s8z6g5jqzllkgt8a4har94wl8tg0k9m8kv5zd'), value='!'),
]
alert_tx = context.make_unsigned_alert_tx(fee_policy=FixedFeePolicy(5000))
self.assertEqual(alert_tx.version, 2)
alert_tx_inputs = [tx_input.prevout.to_str() for tx_input in alert_tx.inputs()]
self.assertEqual(alert_tx_inputs, [
'59a9ff5fa62586f102b92504584f52e47f4ca0d5af061e99a0a3023fa70a70e2:1',
'778b01899d5ed48df03e406bc5babd1fdc8f1be4b7e5b9d20dd8caf24dd66ff4:1',
])
alert_tx_outputs = [(tx_output.address, tx_output.value) for tx_output in alert_tx.outputs()]
self.assertEqual(alert_tx_outputs, [
('tb1q4s8z6g5jqzllkgt8a4har94wl8tg0k9m8kv5zd', 600),
('tb1qchyc02y9mv4xths4je9puc4yzuxt8rfm26ef07', 743065),
])
self.assertEqual(alert_tx.txid(), '01c227f136c4490ec7cb0fe2ba5e44c436f58906b7fc29a83cb865d7e3bfaa60')
async def test_make_unsigned_recovery_tx(self):
wallet = self._create_default_wallet()
context = TimelockRecoveryContext(wallet)
context.outputs = [
PartialTxOutput(scriptpubkey=address_to_script('tb1q4s8z6g5jqzllkgt8a4har94wl8tg0k9m8kv5zd'), value='!'),
]
context.alert_tx = context.make_unsigned_alert_tx(fee_policy=FixedFeePolicy(5000))
context.timelock_days = 90
recovery_tx = context.make_unsigned_recovery_tx(fee_policy=FixedFeePolicy(5000))
self.assertEqual(recovery_tx.version, 2)
recovery_tx_inputs = [tx_input.prevout.to_str() for tx_input in recovery_tx.inputs()]
self.assertEqual(recovery_tx_inputs, [
'01c227f136c4490ec7cb0fe2ba5e44c436f58906b7fc29a83cb865d7e3bfaa60:1',
])
self.assertEqual(recovery_tx.inputs()[0].nsequence, 0x00403b54)
recovery_tx_outputs = [(tx_output.address, tx_output.value) for tx_output in recovery_tx.outputs()]
self.assertEqual(recovery_tx_outputs, [
('tb1q4s8z6g5jqzllkgt8a4har94wl8tg0k9m8kv5zd', 738065),
])
async def test_make_unsigned_cancellation_tx(self):
wallet = self._create_default_wallet()
context = TimelockRecoveryContext(wallet)
context.outputs = [
PartialTxOutput(scriptpubkey=address_to_script('tb1q4s8z6g5jqzllkgt8a4har94wl8tg0k9m8kv5zd'), value='!'),
]
context.alert_tx = context.make_unsigned_alert_tx(fee_policy=FixedFeePolicy(5000))
cancellation_tx = context.make_unsigned_cancellation_tx(fee_policy=FixedFeePolicy(6000))
self.assertEqual(cancellation_tx.version, 2)
cancellation_tx_inputs = [tx_input.prevout.to_str() for tx_input in cancellation_tx.inputs()]
self.assertEqual(cancellation_tx_inputs, [
'01c227f136c4490ec7cb0fe2ba5e44c436f58906b7fc29a83cb865d7e3bfaa60:1',
])
self.assertEqual(cancellation_tx.inputs()[0].nsequence, 0xfffffffd)
cancellation_tx_outputs = [(tx_output.address, tx_output.value) for tx_output in cancellation_tx.outputs()]
self.assertEqual(cancellation_tx_outputs, [
('tb1q6k5h4cz6ra8nzhg90xm9wldvadgh0fpttfthcg', 737065),
])