From 2fb0dd066f12f7b98fd0d50e8034cce3dc203bd2 Mon Sep 17 00:00:00 2001 From: Oren <115847146+oren-z0@users.noreply.github.com> Date: Tue, 22 Apr 2025 11:02:01 +0300 Subject: [PATCH] Timelock Recovery Extension (#9589) * Timelock Recovery Extension * Timelock Recovery Extension tests * Use fee_policy instead of fee_est Following 3f327eea079986fe557b931c52f58f32710db0cf * making tx with base_tx Following ab14c3e1382c1af48baff73b790aecfbd069eb8a * 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 Co-authored-by: ThomasV --- electrum/gui/qt/main_window.py | 11 +- electrum/gui/qt/transaction_dialog.py | 6 + .../plugins/timelock_recovery/__init__.py | 0 electrum/plugins/timelock_recovery/intro.txt | 72 + .../plugins/timelock_recovery/manifest.json | 8 + electrum/plugins/timelock_recovery/qt.py | 1277 +++++++++++++++ .../timelock_recovery/timelock_recovery.py | 150 ++ .../timelock_recovery_60.png | Bin 0 -> 6113 bytes .../timelock_recovery_820.png | Bin 0 -> 38428 bytes tests/test_timelock_recovery.py | 121 ++ tests/test_timelock_recovery/default_wallet | 1437 +++++++++++++++++ 11 files changed, 3081 insertions(+), 1 deletion(-) create mode 100644 electrum/plugins/timelock_recovery/__init__.py create mode 100644 electrum/plugins/timelock_recovery/intro.txt create mode 100644 electrum/plugins/timelock_recovery/manifest.json create mode 100644 electrum/plugins/timelock_recovery/qt.py create mode 100644 electrum/plugins/timelock_recovery/timelock_recovery.py create mode 100644 electrum/plugins/timelock_recovery/timelock_recovery_60.png create mode 100644 electrum/plugins/timelock_recovery/timelock_recovery_820.png create mode 100644 tests/test_timelock_recovery.py create mode 100644 tests/test_timelock_recovery/default_wallet diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index a6f027da4..5073941e6 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -1118,8 +1118,17 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): *, external_keypairs: Mapping[bytes, bytes] = None, payment_identifier: PaymentIdentifier = None, + show_sign_button: bool = True, + show_broadcast_button: bool = True, ): - show_transaction(tx, parent=self, external_keypairs=external_keypairs, payment_identifier=payment_identifier) + show_transaction( + tx, + parent=self, + external_keypairs=external_keypairs, + payment_identifier=payment_identifier, + show_sign_button=show_sign_button, + show_broadcast_button=show_broadcast_button, + ) def show_lightning_transaction(self, tx_item): from .lightning_tx_dialog import LightningTxDialog diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index 5914acea0..74c50760d 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -411,6 +411,8 @@ def show_transaction( external_keypairs: Mapping[bytes, bytes] = None, payment_identifier: 'PaymentIdentifier' = None, on_closed: Callable[[], None] = None, + show_sign_button: bool = True, + show_broadcast_button: bool = True, ): try: d = TxDialog( @@ -421,6 +423,10 @@ def show_transaction( payment_identifier=payment_identifier, on_closed=on_closed, ) + if not show_sign_button: + d.sign_button.setVisible(False) + if not show_broadcast_button: + d.broadcast_button.setVisible(False) except SerializationError as e: _logger.exception('unable to deserialize the transaction') parent.show_critical(_("Electrum was unable to deserialize the transaction:") + "\n" + str(e)) diff --git a/electrum/plugins/timelock_recovery/__init__.py b/electrum/plugins/timelock_recovery/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/electrum/plugins/timelock_recovery/intro.txt b/electrum/plugins/timelock_recovery/intro.txt new file mode 100644 index 000000000..df53aad6a --- /dev/null +++ b/electrum/plugins/timelock_recovery/intro.txt @@ -0,0 +1,72 @@ +Timelock Recovery is a mechanism which, in case of a catastrophic event +(death or loss of your master key), can send your Bitcoin to a secondary wallet of your choice +within a time-window (i.e. 90 days). +
+During that time-window, you can see that the Timelock Recovery mechanism has been triggered (a +transaction from your wallet to itself is created on the Bitcoin blockchain), and if this +has happened against your will, you can use your master key to cancel the process (by moving +the funds elsewhere before the time-window expires). +
+The implementation of Timelock Recovery is done with two transactions that are signed in advance, +but broadcast only when needed: +
    +
  1. + An Alert Transaction which sends the funds from the wallet to itself (consolidating the UTXOs). +
  2. +
  3. + A Recovery Transaction which sends the funds to a secondary wallet of your choice and can + be added to the blockchain only X days after the Alert Transaction has been broadcast (and mined). +
  4. +
+Optionally, this extension will also let you sign-in-advance a Cancellation Transaction which can be +used to cancel the Timelock Recovery process, by broadcasting it before the time-window expires. +If the Alert Transaction has been broadcast, the Cancellation Transaction will send the funds again to +the same wallet, which would invalidate the Recovery Transaction (technically: the Recovery Transaction +will be seen as a transaction that is trying to spend a UTXO that has already been spent). +
+Timelock Recovery plans do not require any involvement of a third party. +However, two precautions should be taken: +
    +
  1. + Due to the way Bitcoin transactions and UTXOs work, spending funds from the wallet might break + the entire Timelock Recovery plan. +
  2. +
  3. + Adding more funds to the wallet will not be covered by the Timelock Recovery plan. +
  4. +
+
+Therefore it is highly recommended not to use the wallet for any purpose after creating a +Timelock Recovery plan (other than long-term storage). +Instead, for daily purposes use a separate wallet (with a seed in a place that +your loved ones could easily find) and only after accumulating enough funds relevant for long-term +storage, move them to a new highly secured wallet (i.e. with a long passphrase that only you memorize) for +which you create a new Timelock Recovery plan (back to the daily-purpose wallet or to your inheritors' wallet). +
+Each accumulation should be done in a new highly secured wallet, but these are easy to create, i.e. you can +memorize a long passphrase and add a counter at the end (1 for the first accumlation, 2 for the second, etc.). +
+For more details, visit: https://timelockrecovery.com. +
+Before we begin, please note: +
    +
  1. + Please prepare in advance the addresses of your inheritors/backup-wallets. +
  2. +
  3. + Since we are preparing this recovery plan for the long future, it is hard + to estimate what the required mining fees will be. + If the fee is too low, your inheritors, who don't have access to the master + keys, will not be able to simply "replace-by-fee" and use a higher fee. + At the moment of writing this code (year 2025) this is not a big deal, because + there are acceleration-services, such as + + mempool.space's accelerator + , that allows to boost selected transactions for direct payment. + Just in case this service will not be available in the future, the + Alert Transaction will send a small amount of 600 sats to each + destination address. This will allow advance users to boost the + first transaction by spending their unmined UTXO in a mechanism called + Child-Pay-For-Parent. +
  4. +
diff --git a/electrum/plugins/timelock_recovery/manifest.json b/electrum/plugins/timelock_recovery/manifest.json new file mode 100644 index 000000000..a35fd2322 --- /dev/null +++ b/electrum/plugins/timelock_recovery/manifest.json @@ -0,0 +1,8 @@ +{ + "fullname": "Timelock Recovery Utility", + "description": "
This plug-in allows you to create Timelock Recovery Plans for your wallet. See: timelockrecovery.com", + "author": "orenz0@protonmail.com", + "available_for": ["qt"], + "icon":"timelock_recovery_60.png", + "version": "0.1.0" +} diff --git a/electrum/plugins/timelock_recovery/qt.py b/electrum/plugins/timelock_recovery/qt.py new file mode 100644 index 000000000..be8320c26 --- /dev/null +++ b/electrum/plugins/timelock_recovery/qt.py @@ -0,0 +1,1277 @@ +''' + +Timelock Recovery + +Copyright: + 2025 Oren + +Distributed under the MIT software license, see the accompanying +file LICENCE or http://www.opensource.org/licenses/mit-license.php + +''' + +import os +import shutil +import tempfile +import uuid +import json +import hashlib +from datetime import datetime +from functools import partial +from typing import TYPE_CHECKING, Any, List, Optional, Tuple +from decimal import Decimal + +import qrcode +from PyQt6.QtPrintSupport import QPrinter +from PyQt6.QtCore import Qt, QRectF, QMarginsF +from PyQt6.QtGui import (QImage, QPainter, QFont, QIntValidator, QAction, + QPageSize, QPageLayout, QFontMetrics) +from PyQt6.QtWidgets import (QVBoxLayout, QHBoxLayout, QLabel, QMenu, QCheckBox, QToolButton, + QPushButton, QLineEdit, QScrollArea, QGridLayout, QFileDialog, QMessageBox) + +from electrum import constants, version +from electrum.gui.common_qt.util import draw_qr, get_font_id +from electrum.gui.qt.paytoedit import PayToEdit +from electrum.payment_identifier import PaymentIdentifierType +from electrum.plugin import hook +from electrum.i18n import _ +from electrum.transaction import PartialTxOutput +from electrum.util import NotEnoughFunds, make_dir +from electrum.gui.qt.util import ColorScheme, WindowModalDialog, Buttons, HelpLabel +from electrum.gui.qt.util import read_QIcon_from_bytes, read_QPixmap_from_bytes, WaitingDialog +from electrum.fee_policy import FeePolicy +from electrum.gui.qt.fee_slider import FeeSlider, FeeComboBox + +from .timelock_recovery import TimelockRecoveryPlugin, TimelockRecoveryContext + + +if TYPE_CHECKING: + from electrum.gui.qt import ElectrumGui + from electrum.gui.qt.main_window import ElectrumWindow + + +AGREEMENT_TEXT = "I understand that the Timelock Recovery plan will be broken if I keep using this wallet" +MIN_LOCKTIME_DAYS = 2 +# 0xFFFF * 512 seconds = 388.36 days. +MAX_LOCKTIME_DAYS = 388 + + +def selectable_label(text: str) -> QLabel: + label = QLabel(text) + label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse) + return label + + +class FontManager: + def __init__(self, font_name: str, resolution: int): + pixels_per_point = resolution / 72.0 + self.header_font = QFont(font_name, 8) + self.header_line_spacing = QFontMetrics(self.header_font).lineSpacing() * pixels_per_point + self.title_font = QFont(font_name, 18, QFont.Weight.Bold) + self.title_line_spacing = QFontMetrics(self.title_font).height() * pixels_per_point + self.subtitle_font = QFont(font_name, 10) + self.subtitle_line_spacing = QFontMetrics(self.subtitle_font).height() * pixels_per_point + self.title_small_font = QFont(font_name, 16, QFont.Weight.Bold) + self.title_small_line_spacing = QFontMetrics(self.title_small_font).height() * pixels_per_point + self.body_font = QFont(font_name, 9) + self.body_small_font = QFont(font_name, 8) + self.body_small_line_spacing = QFontMetrics(self.body_small_font).lineSpacing() * pixels_per_point + + +class Plugin(TimelockRecoveryPlugin): + base_dir: str + _init_qt_received: bool + font_name: str + small_logo_bytes: bytes + large_logo_bytes: bytes + intro_text: str + + def __init__(self, parent, config, name: str): + TimelockRecoveryPlugin.__init__(self, parent, config, name) + self.base_dir = os.path.join(config.electrum_path(), 'timelock_recovery') + make_dir(self.base_dir) + + self._init_qt_received = False + self.font_name = 'Monospace' + self.small_logo_bytes = self.read_file("timelock_recovery_60.png") + self.large_logo_bytes = self.read_file("timelock_recovery_820.png") + self.intro_text = self.read_file("intro.txt").decode('utf-8') + plugin_metadata: Optional[dict] = parent.get_metadata('timelock_recovery') + self.plugin_version: str = plugin_metadata['version'] if plugin_metadata else 'unknown' + + @hook + def load_wallet(self, wallet, window): + if self._init_qt_received: # only need/want the first signal + return + self._init_qt_received = True + # load custom fonts (note: here, and not in __init__, as it needs the QApplication to be created) + if get_font_id('PTMono-Regular.ttf') >= 0 and get_font_id('PTMono-Bold.ttf') >= 0: + self.font_name = 'PT Mono' + + @hook + def init_menubar(self, window): + m = window.wallet_menu.addAction('Timelock Recovery', lambda: self.setup_dialog(window)) + icon = read_QIcon_from_bytes(self.read_file('timelock_recovery_60.png')) + m.setIcon(icon) + + def setup_dialog(self, main_window: 'ElectrumWindow') -> bool: + context = TimelockRecoveryContext(main_window.wallet) + context.main_window = main_window + return self.create_plan_dialog(context) + + def create_intro_dialog(self, context: TimelockRecoveryContext) -> bool: + intro_dialog = WindowModalDialog(context.main_window, "Timelock Recovery") + intro_dialog.setContentsMargins(11, 11, 1, 1) + + # Create an HBox layout. The logo will be on the left and the rest of the dialog on the right. + hbox_layout = QHBoxLayout(intro_dialog) + + # Create the logo label. + logo_label = QLabel() + + # Set the logo label pixmap. + logo_label.setPixmap(read_QPixmap_from_bytes(self.small_logo_bytes)) + + # Align the logo label to the top left. + logo_label.setAlignment(Qt.AlignmentFlag.AlignLeft) + + # Create a VBox layout for the main contents of the dialog. + vbox_layout = QVBoxLayout() + + # Populate the HBox layout with spacing between the two columns. + hbox_layout.addWidget(logo_label) + hbox_layout.addSpacing(16) + hbox_layout.addLayout(vbox_layout) + + title_label = QLabel(_("What Is Timelock Recovery?")) + vbox_layout.addWidget(title_label) + + intro_label = QLabel(self.intro_text) + intro_label.setWordWrap(True) + intro_label.setTextFormat(Qt.TextFormat.RichText) + intro_label.setOpenExternalLinks(True) + intro_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction) + + intro_wrapper = QScrollArea() + intro_wrapper.setWidget(intro_label) + intro_wrapper.setWidgetResizable(True) + intro_wrapper.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn) + intro_wrapper.setFrameStyle(0) + intro_wrapper.setMinimumHeight(200) + + vbox_layout.addWidget(intro_wrapper) + + close_button = QPushButton(_("Close"), intro_dialog) + close_button.clicked.connect(intro_dialog.close) + vbox_layout.addLayout(Buttons(close_button)) + + # Add stretches to the end of the layouts to prevent the contents from spreading when the dialog is enlarged. + hbox_layout.addStretch(1) + vbox_layout.addStretch(1) + + return bool(intro_dialog.exec()) + + + def create_plan_dialog(self, context: TimelockRecoveryContext) -> bool: + plan_dialog = WindowModalDialog(context.main_window, "Timelock Recovery") + plan_dialog.setContentsMargins(11, 11, 1, 1) + plan_dialog.resize(800, plan_dialog.height()) + + fee_policy = FeePolicy(context.main_window.config.FEE_POLICY) + create_cancel_cb = QCheckBox('', checked=False) + alert_tx_label = QLabel('') + recovery_tx_label = QLabel('') + cancellation_tx_label = QLabel('') + + if not context.get_alert_address(): + plan_dialog.show_error(''.join([ + _("No more addresses in your wallet."), " ", + _("You are using a non-deterministic wallet, which cannot create new addresses."), " ", + _("If you want to create new addresses, use a deterministic wallet instead."), + ])) + plan_dialog.close() + return + + plan_grid = QGridLayout() + plan_grid.setSpacing(8) + grid_row = 0 + + help_button = QPushButton(_("Help")) + help_button.clicked.connect(lambda: self.create_intro_dialog(context)) + + next_button = QPushButton(_("Next"), plan_dialog) + next_button.clicked.connect(plan_dialog.close) + next_button.clicked.connect(lambda: self.start_plan(context)) + next_button.setEnabled(False) + + payto_e = PayToEdit(context.main_window.send_tab) # Reuse configuration from send tab + payto_e.toggle_paytomany() + + context.timelock_days = 90 + timelock_days_widget = QLineEdit() + timelock_days_widget.setValidator(QIntValidator(2, 388)) + timelock_days_widget.setText(str(context.timelock_days)) + + def update_transactions(): + is_valid = self._validate_input_values( + context=context, + payto_e=payto_e, + timelock_days_widget=timelock_days_widget, + ) + if not is_valid: + view_alert_tx_button.setEnabled(False) + view_recovery_tx_button.setEnabled(False) + view_cancellation_tx_button.setEnabled(False) + next_button.setEnabled(False) + return + try: + context.alert_tx = context.make_unsigned_alert_tx(fee_policy) + assert all(tx_input.is_segwit() for tx_input in context.alert_tx.inputs()) + alert_tx_label.setText(_("Fee: {}").format(self.config.format_amount_and_units(context.alert_tx.get_fee()))) + context.recovery_tx = context.make_unsigned_recovery_tx(fee_policy) + assert all(tx_input.is_segwit() for tx_input in context.recovery_tx.inputs()) + recovery_tx_label.setText(_("Fee: {}").format(self.config.format_amount_and_units(context.recovery_tx.get_fee()))) + if create_cancel_cb.isChecked(): + context.cancellation_tx = context.make_unsigned_cancellation_tx(fee_policy) + assert all(tx_input.is_segwit() for tx_input in context.cancellation_tx.inputs()) + cancellation_tx_label.setText(_("Fee: {}").format(self.config.format_amount_and_units(context.cancellation_tx.get_fee()))) + else: + context.cancellation_tx = None + except NotEnoughFunds: + view_alert_tx_button.setEnabled(False) + view_recovery_tx_button.setEnabled(False) + view_cancellation_tx_button.setEnabled(False) + payto_e.setStyleSheet(ColorScheme.RED.as_stylesheet(True)) + payto_e.setToolTip("Not enough funds to create the transactions.") + next_button.setEnabled(False) + return + view_alert_tx_button.setEnabled(True) + view_recovery_tx_button.setEnabled(True) + view_cancellation_tx_button.setEnabled(True) + payto_e.setStyleSheet(ColorScheme.GREEN.as_stylesheet(True)) + payto_e.setToolTip("") + next_button.setEnabled(True) + + + payto_e.paymentIdentifierChanged.connect(update_transactions) + timelock_days_widget.textChanged.connect(update_transactions) + + plan_grid.addWidget(HelpLabel( + _("Recipient of the funds"), + ( + _("Recipient of the funds, after the cancellation time window has expired") + + "\n\n" + + _("This field must contain a single Bitcoin address, or multiple lines in the format: 'address, amount'.") + "\n" + + "\n" + + _("If multiple lines are used, at least one line must be set to 'max', using the '!' special character.") + "\n" + + _("Integers weights can also be used in conjunction with '!', " + "e.g. set one amount to '2!' and another to '3!' to split your coins 40-60.") + ), + ), grid_row, 0) + plan_grid.addWidget(payto_e, grid_row, 1, 1, 4) + grid_row += 1 + + plan_grid.addWidget(HelpLabel( + _("Cancellation time-window (days)"), + ( + _("After broadcasting the Alert Transaction, you have a limited time to cancel the transaction.") + "\n" + + _("Value must be between {} and {} days.").format(MIN_LOCKTIME_DAYS, MAX_LOCKTIME_DAYS) + ) + ), grid_row, 0) + plan_grid.addWidget(timelock_days_widget, grid_row, 1) + grid_row += 1 + plan_grid.addWidget(HelpLabel( + _('Create a cancellation transaction'), + '\n'.join([ + _( + "If the Alert transaction is has been broadcast against your intention," + + " you will be able to broadcast the Cancellation transaction within {} days," + + " to invalidate the Recovery transaction and keep the funds in this wallet" + + " - without the need to restore the seed of this wallet (i.e. in case you have split or hidden it)." + ).format(context.timelock_days), + _( + "However, if the seed of this wallet is lost, broadcasting the Cancellation transaction" + + " might lock the funds on this wallet forever." + ) + ]) + ), grid_row, 0) + plan_grid.addWidget(create_cancel_cb, grid_row, 1, 1, 4) + grid_row += 1 + + fee_slider = FeeSlider( + parent=plan_dialog, network=context.main_window.network, + fee_policy=fee_policy, + callback=lambda x: update_transactions() + ) + + fee_combo = FeeComboBox(fee_slider) + plan_grid.addWidget(QLabel('Fee policy'), grid_row, 0) + plan_grid.addWidget(fee_slider, grid_row, 1) + plan_grid.addWidget(fee_combo, grid_row, 2) + grid_row += 1 + + plan_grid.addWidget(QLabel('Alert transaction'), grid_row, 0) + plan_grid.addWidget(alert_tx_label, grid_row, 1, 1, 3) + view_alert_tx_button = QPushButton(_('View')) + view_alert_tx_button.clicked.connect(lambda: context.main_window.show_transaction(context.alert_tx, show_sign_button=False, show_broadcast_button=False)) + plan_grid.addWidget(view_alert_tx_button, grid_row, 4) + grid_row += 1 + + plan_grid.addWidget(QLabel('Recovery transaction'), grid_row, 0) + plan_grid.addWidget(recovery_tx_label, grid_row, 1, 1, 3) + view_recovery_tx_button = QPushButton(_('View')) + view_recovery_tx_button.clicked.connect(lambda: context.main_window.show_transaction(context.recovery_tx, show_sign_button=False, show_broadcast_button=False)) + plan_grid.addWidget(view_recovery_tx_button, grid_row, 4) + grid_row += 1 + + cancellation_label = QLabel('Cancellation transaction') + plan_grid.addWidget(cancellation_label, grid_row, 0) + plan_grid.addWidget(cancellation_tx_label, grid_row, 1, 1, 3) + view_cancellation_tx_button = QPushButton(_('View')) + view_cancellation_tx_button.clicked.connect(lambda: context.main_window.show_transaction(context.cancellation_tx, show_sign_button=False, show_broadcast_button=False)) + plan_grid.addWidget(view_cancellation_tx_button, grid_row, 4) + grid_row += 1 + + plan_grid.setRowStretch(grid_row, 1) # Make sure the grid does not stretch + # Create an HBox layout. The logo will be on the left and the rest of the dialog on the right. + hbox_layout = QHBoxLayout(plan_dialog) + + def on_cb_change(x): + cancellation_label.setVisible(x) + cancellation_tx_label.setVisible(x) + view_cancellation_tx_button.setVisible(x) + update_transactions() + create_cancel_cb.stateChanged.connect(on_cb_change) + + logo_label = QLabel() + logo_label.setPixmap(read_QPixmap_from_bytes(self.small_logo_bytes)) + logo_label.setAlignment(Qt.AlignmentFlag.AlignLeft) + + # Create a VBox layout for the main contents of the dialog. + vbox_layout = QVBoxLayout() + vbox_layout.addLayout(plan_grid, stretch=1) + vbox_layout.addLayout(Buttons(help_button, next_button)) + + # Populate the HBox layout. + hbox_layout.addWidget(logo_label) + hbox_layout.addSpacing(16) + hbox_layout.addLayout(vbox_layout, stretch=1) + + # initialize + on_cb_change(False) + + return bool(plan_dialog.exec()) + + def _validate_input_values( + self, + context: TimelockRecoveryContext, + payto_e: PayToEdit, + timelock_days_widget: QLineEdit) -> bool: + context.timelock_days = None + try: + timelock_days_str = timelock_days_widget.text() + timelock_days = int(timelock_days_str) + if str(timelock_days) != timelock_days_str or timelock_days < MIN_LOCKTIME_DAYS or timelock_days > MAX_LOCKTIME_DAYS: + raise ValueError("Timelock Days value not in range.") + context.timelock_days = timelock_days + timelock_days_widget.setStyleSheet(None) + timelock_days_widget.setToolTip("") + except ValueError: + timelock_days_widget.setStyleSheet(ColorScheme.RED.as_stylesheet(True)) + timelock_days_widget.setToolTip("Value must be between {} and {} days.".format(MIN_LOCKTIME_DAYS, MAX_LOCKTIME_DAYS)) + return False + pi = payto_e.payment_identifier + if not pi: + return False + if not pi.is_valid(): + # Don't make background red - maybe the user did not complete typing yet. + payto_e.setStyleSheet(ColorScheme.RED.as_stylesheet(True) if '\n' in pi.text.strip() else '') + payto_e.setToolTip((pi.get_error() or "Invalid address.") if pi.text else "") + return False + elif pi.is_multiline(): + if not pi.is_multiline_max(): + payto_e.setStyleSheet(ColorScheme.RED.as_stylesheet(True)) + payto_e.setToolTip("At least one line must be set to max spend ('!' in the amount column).") + return False + context.outputs = pi.multiline_outputs + else: + if not pi.is_available() or pi.type != PaymentIdentifierType.SPK or not pi.spk_is_address: + payto_e.setStyleSheet(ColorScheme.RED.as_stylesheet(True)) + payto_e.setToolTip("Invalid address type - must be a Bitcoin address.") + return False + scriptpubkey, is_address = pi.parse_output(pi.text.strip()) + if not is_address: + payto_e.setStyleSheet(ColorScheme.RED.as_stylesheet(True)) + payto_e.setToolTip("Must be a valid address, not a script.") + return False + context.outputs = [PartialTxOutput(scriptpubkey=scriptpubkey, value='!')] + return True + + def start_plan(self, context: TimelockRecoveryContext): + main_window = context.main_window + wallet = main_window.wallet + password = main_window.get_password() + + def task(): + wallet.sign_transaction(context.alert_tx, password, ignore_warnings=True) + context.add_input_info() + wallet.sign_transaction(context.recovery_tx, password, ignore_warnings=True) + if context.cancellation_tx is not None: + wallet.sign_transaction(context.cancellation_tx, password, ignore_warnings=True) + + def on_success(result): + self.create_download_dialog(context) + def on_failure(exc_info): + main_window.on_error(exc_info) + msg = _('Signing transaction...') + WaitingDialog(main_window, msg, task, on_success, on_failure) + + + def create_download_dialog(self, context: TimelockRecoveryContext) -> bool: + context.recovery_plan_id = str(uuid.uuid4()) + context.recovery_plan_created_at = datetime.now().astimezone() + download_dialog = WindowModalDialog(context.main_window, "Timelock Recovery - Download") + download_dialog.setContentsMargins(11, 11, 1, 1) + download_dialog.resize(800, download_dialog.height()) + + # Create an HBox layout. The logo will be on the left and the rest of the dialog on the right. + hbox_layout = QHBoxLayout(download_dialog) + + # Create the logo label + logo_label = QLabel() + logo_label.setPixmap(read_QPixmap_from_bytes(self.small_logo_bytes)) + logo_label.setAlignment(Qt.AlignmentFlag.AlignLeft) + + # Create a VBox layout for the main contents + vbox_layout = QVBoxLayout() + + # Create and populate the grid + grid = QGridLayout() + grid.setSpacing(8) + grid.setColumnStretch(3, 1) + + line_number = 0 + + # Add Recovery Plan ID row + grid.addWidget(HelpLabel( + _("Recovery Plan ID"), + _("Unique identifier for this recovery plan"), + ), 0, 0) + grid.addWidget(selectable_label(context.recovery_plan_id), line_number, 1, 1, 4) + line_number += 1 + # Add Creation Date row + grid.addWidget(HelpLabel( + _("Created At"), + _("Date and time when this recovery plan was created"), + ), 1, 0) + grid.addWidget(selectable_label(context.recovery_plan_created_at.strftime("%Y-%m-%d %H:%M:%S %Z (%z)")), line_number, 1, 1, 4) + line_number += 1 + + grid.addWidget(HelpLabel( + _("Alert Address"), + _("This address in your wallet will receive the funds when the Alert Transaction is broadcast."), + ), line_number, 0) + alert_address = context.get_alert_address() + grid.addWidget(selectable_label(alert_address), line_number, 1, 1, 3) + copy_button = QPushButton(_("Copy")) + copy_button.clicked.connect(lambda: context.main_window.do_copy(alert_address)) + grid.addWidget(copy_button, line_number, 4) + line_number += 1 + + if context.cancellation_tx is not None: + cancellation_address = context.get_cancellation_address() + grid.addWidget(HelpLabel( + _("Cancellation Address"), + _("This address in your wallet will receive the funds when the Cancellation transaction is broadcast."), + ), line_number, 0) + grid.addWidget(selectable_label(cancellation_address), line_number, 1, 1, 3) + copy_button2 = QPushButton(_("Copy")) + copy_button2.clicked.connect(lambda: context.main_window.do_copy(cancellation_address)) + grid.addWidget(copy_button2, line_number, 4) + line_number += 1 + + grid.addWidget(HelpLabel( + _("Alert Transaction ID"), + _("ID of the Alert transaction"), + ), line_number, 0) + grid.addWidget(selectable_label(context.alert_tx.txid()), line_number, 1, 1, 3) + line_number += 1 + + grid.addWidget(HelpLabel( + _("Recovery Transaction ID"), + _("ID of the Recovery transaction"), + ), line_number, 0) + grid.addWidget(selectable_label(context.recovery_tx.txid()), line_number, 1, 1, 4) + line_number += 1 + + if context.cancellation_tx is not None: + grid.addWidget(HelpLabel( + _("Cancellation Transaction ID"), + _("ID of the Cancellation transaction"), + ), line_number, 0) + grid.addWidget(selectable_label(context.cancellation_tx.txid()), line_number, 1, 1, 4) + line_number += 1 + + grid.setRowStretch(line_number, 1) + # Create butttons + recovery_menu = QMenu() + action = QAction('Save as PDF', recovery_menu) + action.triggered.connect(partial(self._save_recovery_plan_pdf, context, download_dialog)) + recovery_menu.addAction(action) + action = QAction('Save as JSON', recovery_menu) + action.triggered.connect(partial(self._save_recovery_plan_json, context, download_dialog)) + recovery_menu.addAction(action) + recovery_button = QToolButton() + recovery_button.setText(_("Save Recovery Plan")) + recovery_button.setMenu(recovery_menu) + recovery_button.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup) + # Save Cancellation Plan button row (if applicable) + cancellation_menu = QMenu() + action = QAction('Save as PDF', cancellation_menu) + action.triggered.connect(partial(self._save_cancellation_plan_pdf, context, download_dialog)) + cancellation_menu.addAction(action) + action = QAction('Save as JSON', cancellation_menu) + action.triggered.connect(partial(self._save_cancellation_plan_json, context, download_dialog)) + cancellation_menu.addAction(action) + cancellation_button = QToolButton() + cancellation_button.setText(_("Save Cancellation Plan")) + cancellation_button.setMenu(cancellation_menu) + cancellation_button.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup) + # Add layouts to main vbox + vbox_layout.addLayout(grid) + vbox_layout.addStretch() + download_hbox = QHBoxLayout() + download_hbox.addWidget(recovery_button) + if context.cancellation_tx is not None: + download_hbox.addWidget(cancellation_button) + # agree checkbox + def on_agreement(b): + recovery_button.setEnabled(bool(b)) + cancellation_button.setEnabled(bool(b)) + on_agreement(False) + agree_cb = QCheckBox(AGREEMENT_TEXT) + agree_cb.stateChanged.connect(on_agreement) + vbox_layout.addWidget(agree_cb) + vbox_layout.addStretch() + vbox_layout.addLayout(download_hbox) + close_button = QPushButton(_("Close"), download_dialog) + def on_close(): + if context.cancellation_tx is not None and not context.cancellation_plan_saved: + if not context.recovery_plan_saved: + is_sure = download_dialog.question( + _("Are you sure you want to close this dialog without saving any of the files?"), + title=_("Close"), + icon=QMessageBox.Icon.Question + ) + if not is_sure: + return + else: + is_sure = download_dialog.question( + _("Are you sure you want to close this dialog without saving the cancellation-plan?"), + title=_("Close"), + icon=QMessageBox.Icon.Question + ) + if not is_sure: + return + elif not context.recovery_plan_saved: + is_sure = download_dialog.question( + _("Are you sure you want to close this dialog without saving the recovery-plan?"), + title=_("Close"), + icon=QMessageBox.Icon.Question + ) + if not is_sure: + return + download_dialog.close() + close_button.clicked.connect(on_close) + vbox_layout.addLayout(Buttons(close_button)) + # Populate the HBox layout. + hbox_layout.addWidget(logo_label) + hbox_layout.addSpacing(16) + hbox_layout.addLayout(vbox_layout, stretch=1) + + return bool(download_dialog.exec()) + + @classmethod + def _checksum(cls, json_data: dict[str, Any]) -> str: + # Assumes the values have a consistent json representation (not a key-value + # object whose fields can be ordered in multiple ways). + return hashlib.sha256(json.dumps( + sorted(json_data.items()), + skipkeys=False, ensure_ascii=True, check_circular=True, + allow_nan=True, cls=None, indent=None, separators=(',', ':'), + default=None, sort_keys=False, + ).encode()).hexdigest() + + def _save_recovery_plan_json(self, context: TimelockRecoveryContext, download_dialog: WindowModalDialog): + try: + # Open a Save As dialog to get the file path + file_path, _selected_filter = QFileDialog.getSaveFileName( + download_dialog, + _("Save Recovery Plan JSON..."), + os.path.join(self.base_dir, "timelock-recovery-plan-{}.json".format(context.recovery_plan_id)), + _("JSON files (*.json)") + ) + if not file_path: + return + with open(file_path, "w") as json_file: + json_data = { + "kind": "timelock-recovery-plan", + "id": context.recovery_plan_id, + "created_at": context.recovery_plan_created_at.isoformat(), + "plugin_version": self.plugin_version, + "wallet_kind": "Electrum", + "wallet_version": version.ELECTRUM_VERSION, + "wallet_name": context.wallet_name, + "timelock_days": context.timelock_days, + "anchor_amount_sats": context.ANCHOR_OUTPUT_AMOUNT_SATS, + "anchor_addresses": [output.address for output in context.outputs], + "alert_address": context.get_alert_address(), + "alert_inputs": [tx_input.prevout.to_str() for tx_input in context.alert_tx.inputs()], + "alert_tx": context.alert_tx.serialize().upper(), + "alert_txid": context.alert_tx.txid(), + "alert_fee": context.alert_tx.get_fee(), + "alert_weight": context.alert_tx.estimated_weight(), + "recovery_tx": context.recovery_tx.serialize().upper(), + "recovery_txid": context.recovery_tx.txid(), + "recovery_fee": context.recovery_tx.get_fee(), + "recovery_weight": context.recovery_tx.estimated_weight(), + "recovery_outputs": [[tx_output.address, tx_output.value] for tx_output in context.recovery_tx.outputs()], + } + # Simple checksum to ensure the file is not corrupted by foolish users + json_data["checksum"] = self._checksum(json_data) + json.dump(json_data, json_file, indent=2) + download_dialog.show_message(_("File saved successfully")) + context.recovery_plan_saved = True + except Exception as e: + self.logger.exception(repr(e)) + download_dialog.show_error(_("Error saving file")) + + def _save_cancellation_plan_json(self, context: TimelockRecoveryContext, download_dialog: WindowModalDialog): + try: + # Open a Save As dialog to get the file path + file_path, _selected_filter = QFileDialog.getSaveFileName( + download_dialog, + _("Save Cancellation Plan JSON..."), + os.path.join(self.base_dir, "timelock-cancellation-plan-{}.json".format(context.recovery_plan_id)), + _("JSON files (*.json)") + ) + if not file_path: + return + with open(file_path, "w") as f: + json_data = { + "kind": "timelock-cancellation-plan", + "id": context.recovery_plan_id, + "created_at": context.recovery_plan_created_at.isoformat(), + "plugin_version": self.plugin_version, + "wallet_kind": "Electrum", + "wallet_version": version.ELECTRUM_VERSION, + "wallet_name": context.wallet_name, + "timelock_days": context.timelock_days, + "alert_txid": context.alert_tx.txid(), + "cancellation_address": context.get_cancellation_address(), + "cancellation_tx": context.cancellation_tx.serialize().upper(), + "cancellation_txid": context.cancellation_tx.txid(), + "cancellation_fee": context.cancellation_tx.get_fee(), + "cancellation_weight": context.cancellation_tx.estimated_weight(), + "cancellation_amount": context.cancellation_tx.output_value(), + } + # Simple checksum to ensure the file is not corrupted by foolish users + json_data["checksum"] = self._checksum(json_data) + json.dump(json_data, f, indent=2) + download_dialog.show_message(_("File saved successfully")) + context.cancellation_plan_saved = True + except Exception as e: + self.logger.exception(repr(e)) + download_dialog.show_error(_("Error saving file")) + + def _create_pdf_printer(self, file_path: str) -> QPrinter: + printer = QPrinter() + printer.setResolution(600) + printer.setPageSize(QPageSize(QPageSize.PageSizeId.A4)) + printer.setOutputFormat(QPrinter.OutputFormat.PdfFormat) + printer.setOutputFileName(file_path) + printer.setPageMargins(QMarginsF(20, 20, 20, 20), QPageLayout.Unit.Point) + return printer + + def _paint_scaled_logo(self, painter: QPainter, page_width: int, current_height: float) -> int: + logo_pixmap = read_QPixmap_from_bytes(self.large_logo_bytes) + logo_size = int(page_width / 10) + scaled_logo = logo_pixmap.scaled( + logo_size, + logo_size, + Qt.AspectRatioMode.KeepAspectRatio, + Qt.TransformationMode.SmoothTransformation + ) + # Center the logo horizontally and draw at current_height + logo_x = (page_width - scaled_logo.width()) / 2 + painter.drawPixmap(int(logo_x), int(current_height), scaled_logo) + return scaled_logo.height() + + def _save_recovery_plan_pdf(self, context: TimelockRecoveryContext, download_dialog: WindowModalDialog): + # Open a Save As dialog to get the file path + file_path, _selected_filter = QFileDialog.getSaveFileName( + download_dialog, + _("Save Recovery Plan PDF..."), + os.path.join(self.base_dir, "timelock-recovery-plan-{}.pdf".format(context.recovery_plan_id)), + _("PDF files (*.pdf)") + ) + if not file_path: + return + + painter = QPainter() + temp_file_path: Optional[str] = None + + try: + with tempfile.NamedTemporaryFile(dir=os.path.dirname(file_path), prefix=f"{os.path.basename(file_path)}-", delete=False) as temp_file: + temp_file_path = temp_file.name + printer = self._create_pdf_printer(temp_file_path) + if not painter.begin(printer): + return + self._paint_recovery_plan_pdf(context, painter, printer) + painter.end() + shutil.move(temp_file_path, file_path) + download_dialog.show_message(_("File saved successfully")) + context.recovery_plan_saved = True + except (IOError, MemoryError) as e: + self.logger.exception(repr(e)) + download_dialog.show_error(_("Error saving file")) + if temp_file_path is not None and os.path.exists(temp_file_path): + os.remove(temp_file_path) + finally: + if painter.isActive(): + painter.end() + + def _paint_recovery_plan_pdf(self, context: TimelockRecoveryContext, painter: QPainter, printer: QPrinter): + font_manager = FontManager(self.font_name, printer.resolution()) + + # Get page dimensions + page_rect = printer.pageRect(QPrinter.Unit.DevicePixel) + page_width = page_rect.width() + page_height = page_rect.height() + + current_height = 0 + page_number = 1 + + # Header + painter.setFont(font_manager.header_font) + painter.drawText( + QRectF(0, 0, page_width, font_manager.header_line_spacing + 20), + Qt.AlignmentFlag.AlignHCenter, + f"Recovery-Guide Date: {context.recovery_plan_created_at.strftime('%Y-%m-%d %H:%M:%S %Z (%z)')} ID: {context.recovery_plan_id} Page: {page_number}", + ) + current_height += font_manager.header_line_spacing + 40 + + current_height += self._paint_scaled_logo(painter, page_width, current_height) + 40 + + # Title + painter.setFont(font_manager.title_font) + painter.drawText(QRectF(0, current_height, page_width, font_manager.title_line_spacing + 20), Qt.AlignmentFlag.AlignHCenter, "Timelock-Recovery Guide") + current_height += font_manager.title_line_spacing + 20 + + # Subtitle + painter.setFont(font_manager.subtitle_font) + painter.drawText( + QRectF(0, current_height, page_width, font_manager.subtitle_line_spacing + 20), Qt.AlignmentFlag.AlignCenter, + f"Electrum Version: {version.ELECTRUM_VERSION} - Plugin Version: {self.plugin_version}" + ) + current_height += font_manager.subtitle_line_spacing + 60 + + # Main content + recovery_tx_outputs = context.recovery_tx.outputs() + painter.setFont(font_manager.body_font) + intro_text = ( + f"This document will guide you through the process of recovering the funds on wallet: {context.wallet_name}. " + f"The process will take at least {context.timelock_days} days, and will eventually send the following amount " + f"to the following {'address' if len(recovery_tx_outputs) == 1 else 'addresses'}:\n\n" + + '\n'.join(f'• {output.address}: {context.main_window.config.format_amount_and_units(output.value)}' for output in recovery_tx_outputs) + "\n\n" + f"Before proceeding, MAKE SURE THAT YOU HAVE ACCESS TO THE {'WALLET OF THIS ADDRESS' if len(recovery_tx_outputs) == 1 else 'WALLETS OF THESE ADDRESSES'}, " + f"OR TRUST THE {'OWNER OF THIS ADDRESS' if len(recovery_tx_outputs) == 1 else 'OWNERS OF THESE ADDRESSES'}. " + "The simplest way to do so is to send a small amount to the address, and then trying " + "to send all funds from that wallet to a different wallet. Also important: make sure that the " + "seed-phrase of this wallet has not been compromised, or else a malicious actor could steal " + "the funds the moment they reach their destination.\n\n" + "For more information, visit: https://timelockrecovery.com\n" + ) + + drawn_rect = painter.drawText( + QRectF(0, current_height, page_width, page_height - current_height), + Qt.TextFlag.TextWordWrap, + intro_text, + ) + current_height += drawn_rect.height() + 20 + + # Step 1 + painter.setFont(font_manager.title_small_font) + painter.drawText( + QRectF(0, current_height, page_width, font_manager.title_small_line_spacing + 20),Qt.AlignmentFlag.AlignLeft, + "Step 1 - Broadcasting the Alert transaction", + ) + current_height += font_manager.title_small_line_spacing + 20 + + painter.setFont(font_manager.body_font) + # Calculate number of anchors + num_anchors = len(context.alert_tx.outputs()) - 1 + + # Split alert tx into parts if needed + alert_raw = context.alert_tx.serialize().upper() + if len(alert_raw) < 2300: + alert_raw_parts = [alert_raw] + else: + alert_raw_parts = [] + for i in range(0, len(alert_raw), 2100): + alert_raw_parts.append(alert_raw[i:i+2100]) + + # Step 1 explanation text + step1_text = ( + f"The first step is to broadcast the Alert transaction. " + f"This transaction will keep most funds in the same wallet {context.wallet_name}, " + ) + + if num_anchors > 0: + step1_text += ( + f"except for 600 sats that will be sent to " + f"{'each of the following addresses' if num_anchors > 1 else 'the following address'} " + f"(and can be used in case you need to accelerate the transaction via Child-Pay-For-Parent, " + f"as we'll explain later):\n" + ) + for output in context.alert_tx.outputs(): + if output.address != context.get_alert_address() and output.value == context.ANCHOR_OUTPUT_AMOUNT_SATS: + step1_text += f"• {output.address}\n" + else: + step1_text += "except for a small fee.\n" + + step1_text += ( + f"\nTo broadcast the Alert transaction, " + f"{'scan the QR code on the next page' if len(alert_raw_parts) <= 1 else f'scan the QR codes on the next {len(alert_raw_parts)} pages, concatenate the contents of the QR codes (without spaces),'} " + f"and paste the content in one of the following Bitcoin block-explorer websites:\n" + "• https://mempool.space/tx/push\n" + "• https://blockstream.info/tx/push\n" + "• https://coinb.in/#broadcast\n\n" + f"You should then see a success message for broadcasting transaction-id: {context.alert_tx.txid()}" + ) + + drawn_rect = painter.drawText( + QRectF(0, current_height, page_width, page_height - current_height), + Qt.TextFlag.TextWordWrap, + step1_text + ) + current_height += drawn_rect.height() + 20 + + # Generate QR pages for alert tx parts + for i, alert_part in enumerate(alert_raw_parts): + # Add new page + printer.newPage() + page_number += 1 + current_height = 20 + + # Header + painter.setFont(font_manager.header_font) + painter.drawText( + QRectF(0, current_height, page_width, font_manager.header_line_spacing), + Qt.AlignmentFlag.AlignCenter, + f"Recovery-Guide Date: {context.recovery_plan_created_at.strftime('%Y-%m-%d %H:%M:%S %Z (%z)')} ID: {context.recovery_plan_id} Page: {page_number}" + ) + current_height += font_manager.header_line_spacing + 20 + + # Title + painter.setFont(font_manager.title_font) + painter.drawText( + QRectF(0, current_height, page_width, font_manager.title_line_spacing), + Qt.AlignmentFlag.AlignCenter, + "Alert Transaction" + ) + current_height += font_manager.title_line_spacing + 20 + + # Transaction ID + painter.setFont(font_manager.subtitle_font) + painter.drawText( + QRectF(0, current_height, page_width, font_manager.subtitle_line_spacing), + Qt.AlignmentFlag.AlignCenter, + f"Transaction Id: {context.alert_tx.txid()}" + ) + current_height += font_manager.subtitle_line_spacing + 20 + + # Part number if multiple parts + if len(alert_raw_parts) > 1: + painter.setFont(font_manager.subtitle_font) + painter.drawText( + QRectF(0, current_height, page_width, font_manager.subtitle_line_spacing), + Qt.AlignmentFlag.AlignCenter, + f"Part {i+1} of {len(alert_raw_parts)}" + ) + current_height += font_manager.subtitle_line_spacing + 20 + + # QR Code + qr = qrcode.main.QRCode( + error_correction=qrcode.constants.ERROR_CORRECT_Q, + ) + qr.add_data(alert_part) + qr.make() + qr_image = self._paint_qr(qr) + + # Calculate QR position to center it + qr_width = int(page_width * 0.6) + qr_x = (page_width - qr_width) / 2 + painter.drawImage(QRectF(qr_x, current_height, qr_width, qr_width), qr_image) + current_height += qr_width + 40 + + # Raw text below QR + painter.setFont(font_manager.body_font) + painter.drawText( + QRectF(20, current_height, page_width, page_height - current_height), + Qt.TextFlag.TextWrapAnywhere, + alert_part + ) + + printer.newPage() + page_number += 1 + current_height = 20 + # Header + painter.setFont(font_manager.header_font) + painter.drawText( + QRectF(0, current_height, page_width, font_manager.header_line_spacing), + Qt.AlignmentFlag.AlignCenter, + f"Recovery-Guide Date: {context.recovery_plan_created_at.strftime('%Y-%m-%d %H:%M:%S %Z (%z)')} ID: {context.recovery_plan_id} Page: {page_number}" + ) + current_height += font_manager.header_line_spacing + 20 + + # Step 2 page + painter.setFont(font_manager.title_small_font) + painter.drawText(QRectF(20, current_height, page_width, font_manager.title_small_line_spacing), Qt.AlignmentFlag.AlignLeft, "Step 2 - Waiting for the Alert transaction confirmation") + current_height += font_manager.title_small_line_spacing + 20 + + painter.setFont(font_manager.body_font) + painter.drawText(QRectF(20, current_height, page_width, font_manager.subtitle_line_spacing), Qt.AlignmentFlag.AlignLeft, "You can follow the Alert transaction via any of the following links:") + current_height += font_manager.subtitle_line_spacing + 20 + + # QR codes and links for transaction tracking + for link in [f"https://mempool.space/tx/{context.alert_tx.txid()}", f"https://blockstream.info/tx/{context.alert_tx.txid()}"]: + qr = qrcode.main.QRCode( + error_correction=qrcode.constants.ERROR_CORRECT_H, + ) + qr.add_data(link) + qr.make() + qr_image = self._paint_qr(qr) + + qr_width = int(page_width * 0.2) + qr_x = (page_width - qr_width) / 2 + painter.drawImage(QRectF(qr_x, current_height, qr_width, qr_width), qr_image) + current_height += qr_width + 20 + + painter.setFont(font_manager.body_small_font) + painter.drawText(QRectF(0, current_height, page_width, font_manager.body_small_line_spacing), Qt.AlignmentFlag.AlignCenter, link) + current_height += font_manager.body_small_line_spacing + 20 + + # Explanation text + painter.setFont(font_manager.body_font) + explanation_text = ( + "Please wait for a while until the transaction is marked as \"confirmed\" (number of confirmations greater than 0). " + "The time that takes a transaction to confirm depends on the fee that it pays, compared to the fee that other " + "pending transactions are willing to pay. At the time this document was created, it was hard to predict what a " + "reasonable fee would be today. If the transaction is not confirmed after 24 hours, you may try paying to a " + "Transaction Acceleration service, such as the one offered by: https://mempool.space.com ." + ) + if len(context.outputs) > 0: + explanation_text += ( + f" Another solution, which may be cheaper but requires more technical skill, would be to use" + f"{' one of the wallets that receive 600 sats (addresses mentioned in Step 1),' if len(context.outputs) > 1 else ' the wallet that receive 600 sats (address mentioned in Step 1),'}" + " and send a high-fee transaction that includes that 600 sats UTXO (this transaction could also be from the" + " wallet to itself). For more information, visit: https://timelockrecovery.com ." + ) + + drawn_rect = painter.drawText(QRectF(20, current_height, page_width, page_height - current_height), Qt.TextFlag.TextWordWrap, explanation_text) + current_height += drawn_rect.height() + 40 + + # Step 3 header + painter.setFont(font_manager.title_small_font) + painter.drawText(QRectF(20, current_height, page_width, font_manager.title_small_line_spacing), Qt.AlignmentFlag.AlignLeft, "Step 3 - Broadcasting the Recovery transaction") + current_height += font_manager.title_small_line_spacing + 20 + + # Split recovery transaction if needed + recovery_raw = context.recovery_tx.serialize().upper() + recovery_raw_parts = [recovery_raw[i:i+2100] for i in range(0, len(recovery_raw), 2100)] if len(recovery_raw) > 2300 else [recovery_raw] + + # Step 3 explanation + painter.setFont(font_manager.body_font) + step3_text = ( + f"Approximately {context.timelock_days} days after the Alert transaction has been confirmed, you " + "will be able to broadcast the second Recovery transaction that will send the funds to the final" + f"{' destinations,' if len(recovery_tx_outputs) > 1 else ' destination,'} mentioned on the first page. This can be done using the same websites mentioned in Step 1, but " + f"this time you will need to {'scan the QR code on page ' + str(page_number + 1) if len(recovery_raw_parts) <= 1 else 'scan the QR codes on pages ' + str(page_number + 1) + '-' + str(page_number + len(recovery_raw_parts)) + ' and concatenate their content (without spaces)'}. If this transaction remains unconfirmed for a " + "long time, you should use the Transaction Acceleration service mentioned on Step 2, or use the " + "Child-Pay-For-Parent technique." + ) + painter.drawText(QRectF(20, current_height, page_width, page_height - current_height), Qt.TextFlag.TextWordWrap, step3_text) + + # Recovery transaction pages + for i, recovery_part in enumerate(recovery_raw_parts): + printer.newPage() + page_number += 1 + current_height = 20 + + # Header + painter.setFont(font_manager.header_font) + painter.drawText( + QRectF(0, current_height, page_width, font_manager.header_line_spacing), + Qt.AlignmentFlag.AlignCenter, + f"Recovery-Guide Date: {context.recovery_plan_created_at.strftime('%Y-%m-%d %H:%M:%S %Z (%z)')} ID: {context.recovery_plan_id} Page: {page_number}" + ) + current_height += font_manager.header_line_spacing + 20 + + # Title + painter.setFont(font_manager.title_font) + painter.drawText( + QRectF(0, current_height, page_width, font_manager.title_line_spacing), + Qt.AlignmentFlag.AlignCenter, + "Recovery Transaction" + ) + current_height += font_manager.title_line_spacing + 20 + + # Transaction ID + painter.setFont(font_manager.subtitle_font) + painter.drawText( + QRectF(0, current_height, page_width, font_manager.subtitle_line_spacing), + Qt.AlignmentFlag.AlignCenter, + f"Transaction Id: {context.recovery_tx.txid()}" + ) + current_height += font_manager.subtitle_line_spacing + 20 + + # Part number if multiple parts + if len(recovery_raw_parts) > 1: + painter.setFont(font_manager.subtitle_font) + painter.drawText( + QRectF(0, current_height, page_width, font_manager.subtitle_line_spacing), + Qt.AlignmentFlag.AlignCenter, + f"Part {i+1} of {len(recovery_raw_parts)}" + ) + current_height += font_manager.subtitle_line_spacing + 20 + + # QR Code + qr = qrcode.main.QRCode( + error_correction=qrcode.constants.ERROR_CORRECT_Q, + ) + qr.add_data(recovery_part) + qr.make() + qr_image = self._paint_qr(qr) + + # Calculate QR position to center it + qr_width = int(page_width * 0.6) + qr_x = (page_width - qr_width) / 2 + painter.drawImage(QRectF(qr_x, current_height, qr_width, qr_width), qr_image) + current_height += qr_width + 40 + + # Raw text below QR + painter.setFont(font_manager.body_font) + painter.drawText( + QRectF(20, current_height, page_width, page_height - current_height), + Qt.TextFlag.TextWrapAnywhere, + recovery_part + ) + + def _save_cancellation_plan_pdf(self, context: TimelockRecoveryContext, download_dialog: WindowModalDialog): + # Open a Save As dialog to get the file path + file_path, _selected_filter = QFileDialog.getSaveFileName( + download_dialog, + _("Save Cancellation Plan PDF..."), + os.path.join(self.base_dir, "timelock-cancellation-plan-{}.pdf".format(context.recovery_plan_id)), + _("PDF files (*.pdf)") + ) + if not file_path: + return + + painter = QPainter() + temp_file_path: Optional[str] = None + + try: + with tempfile.NamedTemporaryFile(dir=os.path.dirname(file_path), prefix=f"{os.path.basename(file_path)}-", delete=False) as temp_file: + temp_file_path = temp_file.name + printer = self._create_pdf_printer(temp_file_path) + if not painter.begin(printer): + return + self._paint_cancellation_plan_pdf(context, painter, printer) + painter.end() + shutil.move(temp_file_path, file_path) + download_dialog.show_message(_("File saved successfully")) + context.cancellation_plan_saved = True + except (IOError, MemoryError) as e: + self.logger.exception(repr(e)) + download_dialog.show_error(_("Error saving file")) + if temp_file_path is not None and os.path.exists(temp_file_path): + os.remove(temp_file_path) + finally: + if painter.isActive(): + painter.end() + + def _paint_cancellation_plan_pdf(self, context: TimelockRecoveryContext, painter: QPainter, printer: QPrinter): + cancellation_raw = context.cancellation_tx.serialize().upper() + if len(cancellation_raw) > 2300: + # Splitting the cancellation transaction into multiple QR codes is not implemented + # because it is unexpected to happen anyways. + raise Exception("Cancellation transaction is too large to be saved as a single QR code") + + font_manager = FontManager(self.font_name, printer.resolution()) + + # Get page dimensions + page_rect = printer.pageRect(QPrinter.Unit.DevicePixel) + page_width = page_rect.width() + page_height = page_rect.height() + + current_height = 0 + page_number = 1 + + # Header + painter.setFont(font_manager.header_font) + painter.drawText( + QRectF(0, current_height, page_width, font_manager.header_line_spacing), + Qt.AlignmentFlag.AlignCenter, + f"Cancellation-Guide Date: {context.recovery_plan_created_at.strftime('%Y-%m-%d %H:%M:%S %Z (%z)')} ID: {context.recovery_plan_id} Page: {page_number}" + ) + current_height += font_manager.header_line_spacing + 40 + + current_height += self._paint_scaled_logo(painter, page_width, current_height) + 40 + + # Title + painter.setFont(font_manager.title_font) + painter.drawText( + QRectF(0, current_height, page_width, font_manager.title_line_spacing), + Qt.AlignmentFlag.AlignCenter, + "Timelock-Recovery Cancellation Guide" + ) + current_height += font_manager.title_line_spacing + 20 + + # Subtitle + painter.setFont(font_manager.subtitle_font) + painter.drawText( + QRectF(0, current_height, page_width, font_manager.subtitle_line_spacing + 20), Qt.AlignmentFlag.AlignCenter, + f"Electrum Version: {version.ELECTRUM_VERSION} - Plugin Version: {self.plugin_version}" + ) + current_height += font_manager.subtitle_line_spacing + 60 + + # Main text + painter.setFont(font_manager.body_font) + explanation_text = ( + f"This document is intended solely for the eyes of the owner of wallet: {context.wallet_name}. " + f"The Recovery Guide (the other document) will allow to transfer the funds from this wallet to " + f"a different wallet within {context.timelock_days} days. To prevent this from happening accidentally " + f"or maliciously by someone who found that document, you should periodically check if the Alert " + f"transaction has been broadcast, using a Bitcoin block-explorer website such as:" + ) + drawn_rect = painter.drawText( + QRectF(20, current_height, page_width - 40, page_height), + Qt.TextFlag.TextWordWrap, + explanation_text + ) + current_height += drawn_rect.height() + 40 + + # QR codes and links for transaction tracking + for link in [f"https://mempool.space/tx/{context.alert_tx.txid()}", f"https://blockstream.info/tx/{context.alert_tx.txid()}"]: + qr = qrcode.main.QRCode( + error_correction=qrcode.constants.ERROR_CORRECT_H, + ) + qr.add_data(link) + qr.make() + qr_image = self._paint_qr(qr) + + qr_width = int(page_width * 0.2) + qr_x = (page_width - qr_width) / 2 + painter.drawImage(QRectF(qr_x, current_height, qr_width, qr_width), qr_image) + current_height += qr_width + 20 + + painter.setFont(font_manager.body_small_font) + painter.drawText( + QRectF(0, current_height, page_width, font_manager.body_small_line_spacing), + Qt.AlignmentFlag.AlignCenter, + link + ) + current_height += font_manager.body_small_line_spacing + 20 + + # Watch tower text + painter.setFont(font_manager.body_font) + drawn_rect = painter.drawText( + QRectF(20, current_height, page_width - 40, page_height - current_height), + Qt.TextFlag.TextWordWrap, + "It is also recommended to use a Watch-Tower service that will notify you immediately if the" + " Alert transaction has been broadcast. For more details, visit: https://timelockrecovery.com ." + ) + current_height += drawn_rect.height() + 40 + + # Cancellation transaction section + cancellation_text = ( + "In case the Alert transaction has been broadcast, and you want to stop the funds from " + "leaving this wallet, you can scan the QR code on page 2, and broadcast " + "the content using one of the following Bitcoin block-explorer websites:\n\n" + "• https://mempool.space/tx/push\n" + "• https://blockstream.info/tx/push\n" + "• https://coinb.in/#broadcast\n\n" + "If the transaction is not confirmed within reasonable time due to a low fee, you will have " + "to access the wallet and use Replace-By-Fee/Child-Pay-For-Parent to move the funds to a new " + "address on your wallet. (you can also pay to an Acceleration Service such as the one offered " + "by https://mempool.space)\n\n" + f"IMPORTANT NOTICE: If you lost the keys to access wallet {context.wallet_name} - do not broadcast the " + "transaction on page 2! In this case it is recommended to destroy all copies of this document." + ) + painter.drawText( + QRectF(20, current_height, page_width - 40, page_height), + Qt.TextFlag.TextWordWrap, + cancellation_text + ) + + # New page for cancellation transaction + printer.newPage() + page_number += 1 + current_height = 20 + + # Header + painter.setFont(font_manager.header_font) + painter.drawText( + QRectF(0, current_height, page_width, font_manager.header_line_spacing), + Qt.AlignmentFlag.AlignCenter, + f"Cancellation-Guide Date: {context.recovery_plan_created_at.strftime('%Y-%m-%d %H:%M:%S %Z (%z)')} ID: {context.recovery_plan_id} Page: {page_number}" + ) + current_height += font_manager.header_line_spacing + 20 + + # Cancellation transaction title + painter.setFont(font_manager.title_font) + painter.drawText( + QRectF(0, current_height, page_width, font_manager.title_line_spacing), + Qt.AlignmentFlag.AlignCenter, + "Cancellation Transaction" + ) + current_height += font_manager.title_line_spacing + 20 + + # Transaction ID + painter.setFont(font_manager.subtitle_font) + painter.drawText( + QRectF(0, current_height, page_width, font_manager.subtitle_line_spacing), + Qt.AlignmentFlag.AlignCenter, + f"Transaction Id: {context.cancellation_tx.txid()}" + ) + current_height += font_manager.subtitle_line_spacing + 20 + + # QR Code for cancellation transaction + qr = qrcode.main.QRCode( + error_correction=qrcode.constants.ERROR_CORRECT_Q, + ) + qr.add_data(cancellation_raw) + qr.make() + qr_image = self._paint_qr(qr) + + qr_width = int(page_width * 0.6) + qr_x = (page_width - qr_width) / 2 + painter.drawImage(QRectF(qr_x, current_height, qr_width, qr_width), qr_image) + current_height += qr_width + 40 + + # Raw transaction text + painter.setFont(font_manager.body_font) + painter.drawText( + QRectF(20, current_height, page_width - 40, page_height), + Qt.TextFlag.TextWrapAnywhere, + cancellation_raw + ) + + @classmethod + def _paint_qr(cls, qr: qrcode.main.QRCode) -> QImage: + k = len(qr.get_matrix()) + base_img = QImage(k * 5, k * 5, QImage.Format.Format_ARGB32) + draw_qr(qr=qr, paint_device=base_img) + return base_img diff --git a/electrum/plugins/timelock_recovery/timelock_recovery.py b/electrum/plugins/timelock_recovery/timelock_recovery.py new file mode 100644 index 000000000..1368e8c0d --- /dev/null +++ b/electrum/plugins/timelock_recovery/timelock_recovery.py @@ -0,0 +1,150 @@ +from datetime import datetime +from typing import TYPE_CHECKING, Callable, List, Optional, Sequence, Tuple +from electrum.bitcoin import address_to_script +from electrum.plugin import BasePlugin +from electrum.transaction import PartialTxOutput, PartialTxInput, TxOutpoint +from electrum.util import bfh + +if TYPE_CHECKING: + from electrum.gui.qt import ElectrumWindow + from electrum.transaction import PartialTransaction, TxOutput + from electrum.wallet import Abstract_Wallet + +ALERT_ADDRESS_LABEL = "Timelock Recovery Alert Address" +CANCELLATION_ADDRESS_LABEL = "Timelock Recovery Cancellation Address" + +class PartialTxInputWithFixedNsequence(PartialTxInput): + _fixed_nsequence: int + + def __init__(self, *args, nsequence: int = 0xfffffffe, **kwargs): + self._fixed_nsequence = nsequence + super().__init__(*args, **kwargs) + + @property + def nsequence(self) -> int: + return self._fixed_nsequence + + @nsequence.setter + def nsequence(self, value: int): + pass # ignore override attempts + +class TimelockRecoveryContext: + wallet: 'Abstract_Wallet' + wallet_name: str + main_window: Optional['ElectrumWindow'] = None + timelock_days: Optional[int] = None + cancellation_address: Optional[str] = None + outputs: Optional[List['PartialTxOutput']] = None + alert_tx: Optional['PartialTransaction'] = None + recovery_tx: Optional['PartialTransaction'] = None + cancellation_tx: Optional['PartialTransaction'] = None + recovery_plan_id: Optional[str] = None + recovery_plan_created_at: Optional[datetime] = None + _alert_address: Optional[str] = None + _cancellation_address: Optional[str] = None + recovery_plan_saved: bool = False + cancellation_plan_saved: bool = False + + ANCHOR_OUTPUT_AMOUNT_SATS = 600 + + def __init__(self, wallet: 'Abstract_Wallet'): + self.wallet = wallet + self.wallet_name = str(self.wallet) + + def _get_address_by_label(self, label: str) -> str: + unused_addresses = list(self.wallet.get_unused_addresses()) + for addr in unused_addresses: + if self.wallet.get_label_for_address(addr) == label: + return addr + for addr in unused_addresses: + if not self.wallet.is_address_reserved(addr) and not self.wallet.get_label_for_address(addr): + self.wallet.set_label(addr, label) + return addr + if self.wallet.is_deterministic(): + addr = self.wallet.create_new_address(False) + if addr: + self.wallet.set_label(addr, label) + return addr + return '' + + def get_alert_address(self) -> str: + if self._alert_address is None: + self._alert_address = self._get_address_by_label(ALERT_ADDRESS_LABEL) + return self._alert_address + + def get_cancellation_address(self) -> str: + if self._cancellation_address is None: + self._cancellation_address = self._get_address_by_label(CANCELLATION_ADDRESS_LABEL) + return self._cancellation_address + + def make_unsigned_alert_tx(self, fee_policy) -> 'PartialTransaction': + alert_tx_outputs = [ + PartialTxOutput(scriptpubkey=address_to_script(self.get_alert_address()), value='!'), + ] + [ + PartialTxOutput(scriptpubkey=output.scriptpubkey, value=self.ANCHOR_OUTPUT_AMOUNT_SATS) + for output in self.outputs + ] + return self.wallet.make_unsigned_transaction( + coins=self.wallet.get_spendable_coins(confirmed_only=False), + outputs=alert_tx_outputs, + fee_policy=fee_policy, + is_sweep=False, + ) + + def _alert_tx_output(self) -> Tuple[int, 'TxOutput']: + tx_outputs: List[Tuple[int, 'TxOutput']] = [ + (index, tx_output) for index, tx_output in enumerate(self.alert_tx.outputs()) + if tx_output.address == self.get_alert_address() and tx_output.value != self.ANCHOR_OUTPUT_AMOUNT_SATS + ] + if len(tx_outputs) != 1: + # Safety check - not expected to happen + raise ValueError(f"Expected 1 output from the Alert transaction to the Alert Address, but got {len(tx_outputs)}.") + return tx_outputs[0] + + def _alert_tx_outpoint(self, out_idx: int) -> TxOutpoint: + return TxOutpoint(txid=bfh(self.alert_tx.txid()), out_idx=out_idx) + + def make_unsigned_recovery_tx(self, fee_policy) -> 'PartialTransaction': + prevout_index, prevout = self._alert_tx_output() + nsequence: int = round(self.timelock_days * 24 * 60 * 60 / 512) + if nsequence > 0xFFFF: + # Safety check - not expected to happen + raise ValueError("Sequence number is too large") + nsequence += 0x00400000 # time based lock instead of block-height based lock + recovery_tx_input = PartialTxInputWithFixedNsequence( + prevout=self._alert_tx_outpoint(prevout_index), + nsequence=nsequence, + ) + recovery_tx_input.witness_utxo = prevout + + return self.wallet.make_unsigned_transaction( + coins=[recovery_tx_input], + outputs=[output for output in self.outputs if output.value != 0], + fee_policy=fee_policy, + is_sweep=False, + ) + + def add_input_info(self): + self.recovery_tx.inputs()[0].utxo = self.alert_tx + if self.cancellation_tx: + self.cancellation_tx.inputs()[0].utxo = self.alert_tx + + def make_unsigned_cancellation_tx(self, fee_policy) -> 'PartialTransaction': + prevout_index, prevout = self._alert_tx_output() + cancellation_tx_input = PartialTxInput( + prevout=self._alert_tx_outpoint(prevout_index), + ) + cancellation_tx_input.witness_utxo = prevout + + return self.wallet.make_unsigned_transaction( + coins=[cancellation_tx_input], + outputs=[ + PartialTxOutput(scriptpubkey=address_to_script(self.get_cancellation_address()), value='!'), + ], + fee_policy=fee_policy, + is_sweep=False, + ) + +class TimelockRecoveryPlugin(BasePlugin): + def __init__(self, parent, config, name): + BasePlugin.__init__(self, parent, config, name) diff --git a/electrum/plugins/timelock_recovery/timelock_recovery_60.png b/electrum/plugins/timelock_recovery/timelock_recovery_60.png new file mode 100644 index 0000000000000000000000000000000000000000..c520eeb9591a37904596b289bc1bbac2aa3813bd GIT binary patch literal 6113 zcmeHLdo)z-+a6KQLJ6U!p>#0jJk3moG)|)?XOS%CurZi}nZZbMuB2W`uR?`{io}~v zL=H)Bq=TqbDoNxRMGooPgHG%Ft@W+e`hNd4Ywg*4-}m)g&wXF_{XBd2B-n4YR#sT5 z0D(Z1sWujl;MqiS$SnZBNmEY+;Hh?}vzy3~9t!0Pcs}ev04fUO15iNB_JKgeZ5(?K zm0GCMsmti)$XFc2_fdKQ3mc zjM`3VnD^%^g8qk1UT&9n#ecRW8hb-j zgKpm^?elcLS>l>4r?Kh${d2MZZvL7@r5x_)SbcSg?v#5W=_;Z3{j0TdF=o(#n3PkA zN5o!^ermn(^^=6bleOVvQk!yWumy%Her24z6nRvJhlccpq3q==EKtUnhGQwG7nij@ zCI>scs1v)6Rhk9Z-}O)$O1K&1a}57s!H32?4!_+{<>KK&^~^lTU)u4WqtlUw!y&+F$zE7lVrSR+x3weLO$tMe7mrD}k)mZZyj& zWThEA+aA(>uRBAqv3rS@+elX{slG@0P_}z6H!7CmU1;;P*R%b_W8#&_@a-d$Qs-*~ zmCXcS0PMS+Y_PxFXj@239>!v@Q5z@Tgv z8MX~aL(=#Zz>jUSQvf*a-0IBS8Nej6U}mNYCSnpuzyU;bsF)MT6_Uhc*bFZTyq1^| zFz5_K6hMZ#(d?lVo&bPijj_f^xTTmKf`*wYKurWJACjYm)f@$QM~3-{M0^qg5gHn5 z9EvgK349SKB9Vwdq7i5`97Mo{VO$Yi4Ce~ZoC=^G2r-?HKp7(u91h~=5kiq=2uLy)(0?2ubOx6x!VwVif(1;# zG6dj?^na$Xrqb-cGe}bMWpns5NrB@1G8v2c9mfwA1kPYsOau@Ja6nQa7$52vyol}d zL#$t7lXzy*`8g19y6^nIpugol0|rqv8p(pk43te+_C1Z=P?>485-C81)0R7@5MO(6IX;Y2(Vj0Vpl zg3+Mxa3qVtMBxDx6N5#~P)X`QGPkFaVQ6FI4~u;uUF5?PaL6zkoe8yb`r&bAa{wn1 zU7{LlJqC}(p>WvQ3;Yj|D>3wb`GP`Usx_XTwXwF3KdMl0y**%Ge#6&~sbNa_PB21lac zbBYl`_2v{q%w>#_>>A%vHbML^QA}n4KW#JMxLF&xeSy0n;`??uCmI;=fAcd}jQ{2o zQ0SjYeoNm!a{ZC(w-op-@SpDbBiC;!@LS+N-Sz(_m%@)1Du4_A3nQ+)# zY-zL>*u&oN7r=k-++WHL*wCbdxi%12Hw z_6+QaaQGPQsXV1?^KM{uf7PBNrxJOi(xXQwY@-+hO~Y2U59)B_s_Ntj{ec%toenXN z>Z1kKDGI5}TU=wCcd3?by;(liAsMtk>+Z>fy8OVZ)ouw|$>XR{1yet1|+Yc32p&Z!xqoE=%W5uWK2Zv1)*GflRxbsCGZXR{XsML;} zq}!9&>nF$azgj&0(jtw!qaS;sx~NT#@Ie-zntJMB`B>Alm*lVtM&Gr4N|`$O_04-M zCx%x9(ICYhExKMts_{HrlsSO056!n@qb z`P0F&_OwW?>tkk_3;u=ld5+FI7fz1e9~yMd>z3Q)MQM0^OkB3Ia8TdRDRQ4_z?*ogWbc+d)0{3aqHQ;WFtlcZc|5|PdcJu*v#YvDWm3ps&3b@&s+M$ z2fE>SEA209^wx%D<>!z__ny?wO!6<65h%gP>#jL3rCE3FR-9}M>3p)I^JZ{_$?yaI z6KZdA+M<$k({XqXyhbaPRTYIAQFFE?Mi1+W6*8^+V5-YG`me$O+bDc$2< z)V_~HA>$<_r{k(#Zi{*JVb`ePG1;nXA2U<~M;D1xxm9I+nckd(S_`MNlMkOfUKN$$ z_xwgn{2ruV+Q@0e@BuHk`c{U~i`K!6NB5-_i3GnD$kZvh-df!QlgkY4OBBn8=bJ3- z+jvVGZ>y8)kW?D<)wQqrLPzN%hS6sg*SoS$@^?p-bdH6};7L7y>qYqL*XtiJk`v_= zU5RmoDp|bm-f+~xN)2nJ=^uin4}>ovPOm7HzgT=OB!_k~snNLRxkp^HK-p%vP(0r9KJ>Dh*YRlNpHXC{Kma>DawCYSLLNM zMNOSDD$f1$>uQHmdPvajn}5ZIBc$}VfD6GrQcliImw#^vR9RP_YX zF3*o@sLoab$RS_Wl)dzb7UnMQv+8TU_n=}@`*=Y%hr0d-{>*5D+C)P1)9|S!5EJui zn%TLF+64l`tuHQ21Z;Y?tkH8Y=Pk#gAsl%%!0yw<@vmp8j|LomdpIuZxJS+C z;iZ#}t2(=TRJ#Q(yhooq@1(||lzKc4ej4l-FK?UI*0g(cCq^J7U4UX%Wzo_T$CJFE zw=DiK!$b$V1!L)tbty^>7C4^1eP!*z44kycE;IA-7lk!4(J7JDXCsx9Q5xl4T3h23 z!ziXTU6W>%O9M*8g8iSA4BqmaD&mAH7F`P07k(vmRpC*lkF(xI>g11OVgqjClif29 zd~$n28i?K}6}Wu*b0Br(uGPrhN{al;sDG~HsE>WL?!0^DgaSN$VUR;!WR^!_=;kb$ z(C}TX6Q7glE(27^cG=#}(JV_V=eFp>XBLLt?sM5!GER|EY|52BQy^`Neok4o{_B?; zx^H^Y_Qq<|NjnT0tnRN)_H16H`l;d#*KGfomp$U;K=s)?4eb7Q^BD&e>=8*?XToF?u@cq(qEFSXfx3P{>0AEG!%e%r5~R z@WjQ#-UkcoR=9(TiXK!&h5e<6o1KI6b1W?0q@W~u$X7j@2wlUXcS2;|6z;uxIot2< z#@7?6mQpxI7_n$sp5tpXB53H;KiB{KIbuEex!cqj-&mRA@|L^zO9QVYsy$62A?v`G zer)UQo=tlSN-vBu?2bWL+Koe6{CsN%AFMa-;6&?1aP$v~t%3MRQOB{IE;k zx@MwVgRZW&j8d1n=aF_vAjf)5zh7*fQAM41-ettEu#VcnSMT+a)zMrKih}%jqW9I5 zB^Es0;K<=*g=@-bkt9tHW862)#HGsN+vlIrKTn)1mL`yIX>_pLMdA4tD#cx zBV9wUCEZ+iJhK(BOfw&!<4H-~{-&AIzI(XavDZfP11f`!6c}eo$h4qVuhm&zi zb)uTy^oN8KmGZ`7o;qXOz~OMNfR=G+iEPf|$!g z#oAHn#x28F9mhe_K4l?xIW-C8(MHM$ki*|i|DnpKckJkNqd%)6Ku|muoH!B@U?8?a z|AIsmGx*u7WD}AEf+BA9C~>O10R|)w@EC6fwoUg&>1}~0w$(|Y-zYet2+-HK&~NOR z0fRG%1Reu0=zD6Df3Fwv-v&@TCY(5BH4N|{z%F9I0P(qa2>ai&|M^lJ^RnQZl1(B8 zg;|*&A16N3+W|x5Um{5%clMht%H+ef$Js)jVL&>f^u{M(zF#qjPAnD_^PzwK2O8rk zq2EMrP;<-_8@&MnQqdmAWems6wXJsOSF%aR>_Q@`&G3Tc-_FH+IB_>{#U>=4=ieIy z=zvCM7L!Os-q5D^{|(d-kMRZp+tUAwKnuCj%z)hu>N1liRyJN zm^rMs%*Y5ZAFAy%i$D~LBanLwF7K6ACD4V~OCUWnMX2MgaY+qvAvVYKB&1QQ(ndKt z)zIa)C*rtMr{$E&TB;r`3{SZ{WnKE37nUT;o~u?GvwSQH@HbX({13UrAjIGUf5C+r zBI1G4l=hpF(fJoqFg2XTs;ui>H@=9+460p4q>_@3_U&`K&Yv zd7n@+gDVx3Q~~lT5Bp=QDZohq5ecB)d_6z_{Z{toV;2Ej4}JCPXxGX?ovsf-Ob|P~ zKA{%=J1gFj`9xuE0u1r<#Ip%Qa)9j3zh9=E*8 z+2LNlbrKWH18>bl(kY+2Re2IZamP%r1*N#s@$_Bp#7ay5!|S74}rQMT>1rz^ze93i>u|@6GsSGStMggHq>As9q#*Xk&s-IFxHLR z)PKRJbwANIWq}yFo~J$~KHBh@h-2dGllufD*FVudt4_RsEeN}9udHY!``c0Mtfv+P zET|JE{E+u=vq3|V-tClZOMn*S5U)-!CfD0Z@|?nk|4N*g`Ktr37?5MxWH{_g}$q&MM1Lh^5;E(9C8% zn^*y;b|GG0;6^huFN9Vh`6!HzqGfBm7_3gt@rv=$azszW*A%bS$Dr706;rTFcAz2K zYJu$Uxku|gPcJRqY7i2h6-ODKUTTP9Q#m4cWXCh(lUT@ujttvfMs_OywWLAQ4alIN z9vK^ZJadH`p}YtDauF4toL7N*uEGP4$UcvSaFtzU5Ant&B!C=@_}IyCq~Xel!{Auk znfgQ!ygb?V*BM;n?eom%hz`}!#?ou@Gf=^LV|2Y$)n0PLx=w!-j0|B((5}=S1~tIR zai{VCkYVJSf^|bTV3d9ROoS}$iMGgC5$;W^%b-Hh_-_)^exxa1@p=EcamTY3{|FNJ zbxQZU&=r)c3q4n`m(f@zhy&#qG8{dDZdl@Sa9LawA_s&o-qJmZEGPQBP?u?V)sa1D z3fW)<(MtBKzNPZ9Q1x&Y-|w=cM?E30MIh^(U38NH#r8_mcH4*?Rjpm&=@stG7?sq6 zEgj}&imc&(S@{H z2%RDq`33Z+?+j3x9HRz&@SPSB?$&L({4qjkgx4-V*qE)dKrhG(Z)pYkx=@xc>cnvS zv=h#0RT-5fh3)=D4C=^KB74IK$reuyQ*yD%SMX+IYMFIJxgXi)((y6R(a#q8AlUB; zbd?R+%o1q|u?Ra^uKIkCbl(rKtd}5NL~i~a`B(A7kg;UG-%ji;dC-ma zM3YTUVHoXXs&S&X_hFUQFt5C-XuM%7l5~dybxGb;;L^3+sB*nAv3GAC;%HGK{!b(> zJ>m@PURFdb#L$ro%CY*9(u7ZNcw&=e)o|Ad_RF!(H~sZhQMLKrBUaSp3);*me9lH3 z>EoR|uCfaALBnOh_T(2qaD^{zZxda8ar-_Ifk&QL1(t2bj!<)^@Q@p_qM&~0Jc=IH zu-U0HI&TdFC+SIjM)en<&7;;-rCV1b5zj}sEI#Hs&vfrTDt4fg@rTIzL(bpo*|K!~ zx=IOf<=jQ*C3-2Dgm8u-W-DFfQc)PUFFtr%^ma{%?Y+KCD~|GD+GqQ!wx-?Z8}q!x zufHZ3?7~8P17%lj_9A`hzp(#&#aDKg)6W~nDS=70ulo|HyP-;7?lL%iK5RsoOR}|uv8X0B8OifVNsfp` zm#6jgE!5abz0}*BcF2;0!|P}YRsp+JGsxF?eNStVxPWbX3gY+}j}dzQ+45P?>zQ{C z5-ZX7z$bEmrA9JHPCuTX)NS~s+I468(}w=R-sOGwZQWiLgMo$SyzKEIP<^Ux zi0B?JNB|aX`%-fA3o(1&6P`luJ0Ah$2<15~HR2_DyU=X)NYKl z3}=~8<|nUv1ueJzr}Tr?U>rw-uWCBYj7{~J7ofpgtQx1LyWmNCf904|A* ztG=rL476pb>-~$E&=kL*ut&6TDh-MBG)A=j3F@j@ULl7idCak?xKsR@eLIm&B7vCI zGKD=*Z-K9blR2oK?l=2gCsL}+QMZQ zv5v#_|9W=E2cWWR0 zWz!B}#xtTtfd_kY^ezj6XH4n8Hc5A_>2I+5!+oh&f9X5R%1Jxjx7`L_gk5TnuB<0q z%|L*gA-ab!2=HFrBT2#w8?w)lhACd8O4C>*l_5xQ@Ve}?0}l3jDTJMU>vKX-y-^wi zj+n_Gwb~D8>djhB67jJNhyc>5lC$;q1A+zJC|{~2_^Tv|E#J3qCuG161cQZ0T-i;% z?`$r;>M&N5$*Ou}#$gd8Wb0Nqf2FnbhivIrf{qJ4JG2g2aefVZdG!Xl<}&mi9#Mt# zm>f|aVK11~hGgI6lmrWMKMRC=ybw8)XJco*ei-zX6e^?M0b{|Kpc(t17Zq)1X8&VF z6|w3GeTK5{Ry7}q>I`!kzGPPcryZ2V@2fXZLPEFWaS)RCA8h zU-lIBidZDW;leA4_)&=CU1{6(zpaAiX^M|oH!lSq!m>Rs3S5SgXlV9oWBx7Tkr@tfZ%yuVgi(CM6hsJ`s7LmQS?B0 zqh1&RDESfm*;-p3w>e>5j?2fFijm)Z_J9 z*OOD`76}Um!XTOVEvjbj%+n~1Iqy&@*M`{c=;~HvY}SE>D3k{f1Sy<7KYI(|G0&p8 z>_1NNsX+u%t_BFMG$FDr&}_#aGyrBZMT*L~3Hl0uKTyz`(?G|+DRDZ`+GVIv&?c7$ zQO`uOl~rYYZzkneRhDzd`qt4J{`l1RmISSI_bJrclp?`7Vj+*?%>v$jujzczlp)z7 zL2M-#Sta}CcFrux65$tJ1Q=xvwr7Jbz4`5#^@0kqcm}xL*K$z4MzlJ_$b=3GxJjWr zTXr42$_o{KO<|6l&Pb$B$McM`-#sR5+I&uWX~%JN)<{8**D7<-bDh{ctYuLdJqalf zqur?-zfw5C`z#XoDs>+a*?&(G(oJN(CSKaF()Z3)FF#V;Cw^3dt(^@c?fcEtiw8Kw zBYc@3_iRB9zhiRy^u((_vO4kyrI5FO*5m9`;w(!|)oV{YI`BypZij!gPXyH*N^^2} z{1#Dl3A_CC6*SjLvy;)Cwf~?j&L}o?`^$%638_QZMaxQN!^&Py59Tm{FSZ!Nl1=h#y+Jq)4?SA9N{# znPE@#$|l?x)}2Fr#O}D}zC&m4!M`R@^ua{`!WE=^NzdZrXiSZ-GI6ZY{3gdF{Nb7% zZkd_WmNqKd?3)%!4cBT1F{r}8ZFH1qPYcBHiTE;^J!04oUv`tH{T!BD(HyVmu8wpl zJ6PfE{#?Q#{x~^QRt4ho=(3gAt0iJty{JE?=TyJOp zc_nF!@WA)Nm6J=hesvU%JGOckC`X7*5pY)6UKU{gK6A)O=c4F%bceO6l74tCyAEJN zSMps2+Q&Kp?%0kAa?0!Xd|A@I9$TR*mRGzT8@I!e!P?xhu=M`NRS^=n;48CNM{6Vl zERdFUgXZnW69?}77BC|Bi57@VYjHOal!}W)4S#@$1XIt<+RF}YZ(Ebxt5OzBm#U3A z-B6?^3&!Cl|I+o9qDSz%DT;4hsmBr0LYNZ^6$uvFM8qAtyi0!bllHqkQmP^LvND0< z1cu$uL40@f%~>>bj6|TICCTfGsZZEfn4`iBF+MD6xTQ#tVxvM6u*XHV18;AnjC76g zMZios-{DPM+ep$$D{i6L9-p@eovgIBGl)HD6OV`M9+gl#p3@~iIUJt=gPRX^+2c)UFy>t+ zJE+$Vm-4X)YKhk=^rBAb`KuRCZaDW>7oM|MI-M+SXwGLo;^|JTO?Jhw`uW6wTQhpS zGpG7o23#Ly`;-rPNAoBJyPz(wKeejBNXuaNnp5-c=8-wo^e7~gfeWb#nd*qKT^{K5 z#}n-1i>Bs@q2}QucjM_?Zx_9$Sdsm`_xQWBjN$RIMyjOfx?`>8HBMifB;1j_VAjPi zw>B_o>D2Y$LP0ff`vJ$13H1%i<#lg`xlGYUwXa6FGU`4d&|3XUk>PA;K2Sv>EiopeoO58r>?JEtk_x4 z0$I=Q1eE>y&VUvyNK<=w(>QQ&xXStp)JlptbUelWJ?L<*{l1LaE%k}$R?tC6#D!Q72y;Z6Gn!xm^)LlI7EkDdOZu5wO?IRq6Mi&^GY6*d zB#>Hq^HHh0b{xhi7`7T;QP83uZe;AGWbSp-^J0>NdTp4l@6otuM+Uum@giqo1JV7J zuI0R2?^k%(I8cXjCLVWK21F_oeh`ek@%nsL{cClFIw~egaq}BJCO)vsAJ09arfo|B z0w?XU#$KLb!&iK|%*ArGIR~d?{S`3mdX{51xV2q22FUk!m70g6);qH=wLDazj3>^JuANquYB>O&jd?lGcRowGWnQc^5k~>Y% z%T71rSw2t=GQPF?jQ*UcyPfCR!7Jp4s&GfTm_kVkZ0O08{@vJUo&@D`Q);Lpdr;57 z1?o8iDCc<<3nCbCr;4+EB^4PykmG1Hfw8W_p3 zQR7)c{5Q9tZ=GMD_$R*cCCI7C`Gr_t(8XK?4~Q1x5hj-BH|;$>hMAoy_6qw+ZnjPC z$kvM^ZP(?)1D9@hJEE3C6*=SO zNRoXR?IwH;rytES3Hkp>>l3Jb3PZ{|A7MUP!f2y-2AK%GbFU^fx{HH$Bb zn>T*1M5`4NFed_u(!ei(5h6dt;CGT~`8z;d`dh1zv-q1%@b;+lp3G}EZ>oqM2+8Wp_!Mw^QTKDc+^FKcKS=;p zYq8}7xky+uVgtEOdH|pM#ukNoy98iLAbC|Deipd2=A6J_7etUdW4;xa_0oyHg_xTS zn@EH(=VH&6nUnPz{~&;ey(8XhrpTq~K-;LQL3~jcdDQ zrDdt5r*J?<&~8aR%@rH6 z@#rAj&cwQd+`{`RfO1Asititr6`zg-Yn0vV8tSfE=UCCV!&chft0Lg%T$S$0fZLOp ztjmMriWGUA=~BJlsisr{#giv19zA}2u}I=R@1&&d7m(1Spcy=8p8n&K>mX^l>jz!| zHwa#-4Bq3SNbqOCV8I{5@|J3@iVVBP?fOoWO=q4*JaB}(N2Q#v<)=s1jHzf<4gObV-hN} zK+X`Ud*F^UVww|l|D$wwzy$Zj4tR(& zNwJUda(3yYF7cQxCvc&d@}Lz zbl4th6cI0-SpLbu9z(r(L3En+GEZOB9zb3xM4)j}Xa zIOuAfvIXDl0@4!&cm|tv{jA7L_oaOiedlxfv)lkPe{F3sdydVB2UPx5w>5M-F$^5* z4+1%%l)d{j30dSTnCuRq{VEBcx`IcVXCF)?QYT!?cmFl#)n#?~r9ztccy3AF3`2NU zY+_~TuyU!_cx@rAYTVNIIlkQscA)6SoTG0Zl_>>gUQ|;EOf;bt3M^%?ZKsft+rcao zz|XAS;6|iQQ-NdelXipZ)jQOJ(HGG?uAwG}A3qFJVknP7;RQ0n}ALuCL-o)se z+QyHc(r*EsH!~!h@%ZvZpm(jpxexbCO*#nW$FrK2@VU35X*uaA$Radoj|X+>(7zmD zCN=>t*JxPVgjXKEd}M@?bYI$23cNhk^u}T?a*=P4U}wd%hAN7vYob!$;Ucz;eIH7; z3(|wA4v6%}DoJ%mu!f00j#qi(1t%uEGVx)PnNF$76U>V_24DZt|`WD5~zmvz9zw;Tgd3@SsDg;^|(aRL^5Hwp?Yyu9l0aX zUo|5X+PY3Q1 z1FCPpjg%0b-D^(S-n|Q{{ZDAPZA^)|%w#b7kI+h%qA|Io(5t7Rwoe)Q-fwX{lK+ko z*SO|egkS(IN1vIR6&A<4s$ zcqGi;dTOWcfbj@xY2f<2(MgoqUQSGs#}RLRfP&imNn?|0ofmEYa!NvQ`fQrW$0^41 zkKhdptQqS-kvI&Y+jN9GkRjECpz{mmv%eh)_LouGOcbjD*@5!Y>{Ga72MSU zA=~FZD(ha)^46E1I`YUZq4 zglJ?Hdb$>eQv9~Ou2qU+t4|&Dq}kLRkZY-?*kwCoBn3|L1@@Mj?GW+!Jmin-S3mae z@kYrF8HBUZi_R6bum5K+z>(^b$^b8r2p%n(CO+1t0Zk+H%d$X5-5Lyf_y*5d(BJ(xOfJ;@}Kr1wjC>4i^IKP6pWj?=ORuKm=Y zg0YIUsvxnwKyXpr^LXB=Z!zj@p_lnAR1UL`o}LoZkY7r7Uq_j(SC0`LRk05TNp{`S ztpk`Xu@#Y9Ml*W1$B-8A%EfX=q`V0M!9@-(iZanX3Oar(->0OJiHU>e+9p?bqt)el z0BsuEPW${jkISgl+zzLjUbElDD(rjJa;4qa8#z^W*5HtX4cF+3eOpk)qdpmb=AWsU z&^~|%J{$*}-wy%(Wz0dF=&{agFwboH3wE0w`YUvpCyp#<62XLW^mf zj<=Z8ATxsSX{`dfU6rhao{bB(J$L-}#W(0kl|*5ClqyicRH@n7>e`>8|4uOJiU>`G z4|8?AFvP42avVk99g7V{jLu9mo^AiI=~_VFPBwq(rIzWy@FpnrV1stamUxGR)@Gob z7?ZV_Y07ETuJ|D}g}EQem@OrK>3#O4*X~KTK+Xr&F6^bxuy?-OWJ{-gQ!k9qX(`t% zZuNz@#A1kR#nx8teV=Go@lq71Sfr@4hW2So$#CWf@M$S1)vShhB~AU9T25-q@ees56Lbx=>jm4<>tUR@cKYc9;vTJ$jrd_-8@`pkV!Xq?TZ`HrneR zm3q9s+`;4YdFwi^q2E`+zp>T*4gfAd|496uN^(ac*>ltcvV8^12{Jp7Clsv|$u&Lz66D1%#;BZ;uvwwc8hn15g=({1mWO88o8Ek$qBadElV2{`be%xCvIe&{%YR|!@(^!w%9Lc`q+u`mi@=ho(Mr#60VPhldG zoi^8?jA7CnC~Chr{*uZ*Z2Gl%OuSuO{f}#38vym+|tz`^)ZJ);TXw zP~ZGEt<*4Ph!M-s=nfs&bL1rc?Dkoj$fp?DTPkrA9}jv6Y}7MbRkc3=xAYvg;kmjF zY5jts6B?%6;koNnv`@4Mv@hGGzwBV$l=1}iD*-V}$=MoK1*TnzB{ zN27WhFLCa%xgCVNmx$jGoiY?OE|jpxjXH8q|M9yy#q7&%8fra>L;ohMN!+H0HjKMa zIaw$sli(qvh+>2Lj(PIcYZOjwhclU`9@XY?9d-6rCSzs|xrhANKAcFV0l#)zOEXiY zTa97SVx3Vnz3*_5Vhd2qCD>HkaduPK+@vU|jkewO8)E8Fi#OOk=hoUj@Y1Nb>-A}L zn%8GuL)znGOafm1{fvZ&=~8t|LVg9tPlhiz`ex5R9H)D~(d&|D@LI zGt$5~(#*10YRF;_9*7N zLjfN92>W}^)Z~<_R`!aTG^M8iib}mRMzr~R3u?3ocM8RXE|Dk)*=P%=XqKQUFrv69 zhZNL)2pI{e482J;n!NbR1EdQ+pv)kdJAL_n=#8XemS-3w?HbGTe!td)3QVqTv2{W2 zd53ve)0g@S7txN0b;sb)EOP?z0PdWeP+~(7qi=K-1_BX*>kD0+jVc@awi-eUCPK*h zeFh-9i!S11!GJ85IKJRjPoVk>ixoZRn*XKcc&s$8On)x=YbBW1bBhDepfcR^2le+# zS@30qA}46A{PWB8Q8RisObcbg13JWj?ESw!0jt0?>;Z`{&^ZRk^}jv=Hd0}6nBV|y zazGUJNj=}|6=t_pv-yw!A@})c8p8i;W zwlt^Ys0@Gx!p%d1WfeGeBRJjW-oFRQxS1wkX_6+s5TG4!6jI9oZ2A}U|H1=!B_v=5 zFb0DF(rUS!&4(!Y2FW2F3#BK1oX9&)96;+Hpe#^!qqmgCmbzNv1OM?PZp$9%o4Ytj zs+^$mTbqYx0_kVXq44V=Hf}QXvS}b<3wDEyqb`((qmMwGnFz=(GA_4YO<4hrjDQDs z&*V5N2W|b15cLe@)_H^J9UK$fB!xQeDvjTRWIiSor+@J}$H&zF20yPfYzhQ}RhWVE zb#?vQ8zkdI51FvD7x1wDJ4=BvKo;-`Fb4ApAph_4sLDKE!HNAJIUj$SoO#j$&6)rr zk|q7CRkZ{+GAfgfkR~pbyVw(i-@tLy+3XkC^qt88eSmdKN+Lj;*%;Y~-&qsPKOcO6 z(-{=1x$hwXl zA(ETcsmlU3*u`_*)JG2T73Km|#~pq{_CJVrnTErl`i#@tXlC7CCs;_sPAqItc%s*G zinJukC*L=0>oO+T>E7`J+%Avbof+p&+2^}m`uX2Za+4x^u8wIsA~A>{4e#VG<>^XN zsPZOEz@XkdI#-R{0_-3Kg2V|?9ce&g#OKcFpQu6vXu$?5QeZ&S?H;vkU=v_2xk-Qy z21O;eUhb<@PZ@L1pzeV!tvU0PcwEZsimuiH%lC$y~NUeut0Pb(P*QSCF^VDk(qI#zkaK>Is zCV_?&RO2p_%=vs4!G5P768AoG19-X!Y;q0;5Y9x1@K|`p9Vdb$tvdXU%vZ>-g!X&w z3z!?&F_SpVp|uBg{JIx>VD&1@q;{ZVan?scqxn#Tr9LTC&Czrd8t! z0!IPZV`cD60ZZkUUoPUI@di`DQL9gLpL3oJfj0|ORZ z!s=Lg#)r7$R!afUxo!Z&T@SDlL7OCx^RA4(%)d1!V0eM_94D$O@%TDd5e|ad7XX8T zmPIfI{6~N(fg?b}uKgU8*#z9MFGLFl;B!adK1jB>v?k^NIt^{tdvjE^F(4+u5QK!#gV~S4RY0UX=JljI=uz$(u>==1Qzy0LiUt&fL8k0G z(8fj~<1YmGl5xMzoei$RfS3r3)IV;6QMQ6m#K{u?g<@w@W8gO7n+yOGXH0UsNjii? z!azmj=h;Xd;FbLLQh^m?#J;QKrnjMpwv&D(PRRm24)Y5VfS56oen6AK-58$a)c2ny z&gN?%Bnrngu2_LQ8(9Y!iy3AW{s6Gb!A7QO@h$|hB5wj<2NhWimBjeKz0Uw;LwqEH zjhOAo(=gk`Hu`j^CW86`ssUV*fB&w-`_ET899z}@HE(K40&{@ffd3yS+y9@X|8I}S z|KT7VKMqns5EFQg)LOw?Dv1fYs1eAAN0mYH1!K?Kxd6XPn9s{CRkOw9r5H$jdy6Bd z%w#GWUy;!L#d$Df2e2v9TT9HZ9VX!)_B&^sOZC~&p9s`BsZe4oEn@)lTDQUHxsS^X%YZ)!q2q{;6q@58nD4sdr_m40{jQ?#1 zjX~?b*y`S()*ImFA81HiBu}Az=*VzuiJ%*&^AWiulhtk0$y$gbJ?4sI_v_#(p(E;2 zyg#S&d3utWECgSs2;V3xQt(H*e?CJ z-#@R29RQAE!>EY%zzJY%iLN2xP2HT+v+DE%>SwjHM4d|?DR7mS{;TS)l*XNM+B zQ9YZ!hWQ4puAF<~((qfu-K!viD}cTS1+h!N(7)sDZ{9XMFmwwg5jS9J#Acc3IHc?9 zyBCXztDuNXZRO^5YCWKmb#r`679^VDU{D$QagNk$CgH7!_;)M|Zz4z^q9-5a9Xr9} zSqC_Pz6J=;^JSNrG%G%`UEr}`P9PtC6EPYb65$LGhAWcphX8C-W#%#6H-|em)+b^@ zrFr1uv$xVu;MpHiiXNfpi34n!D4>*G1oU$P;f5K{1cz{}$gIGQv zMER8pzj0gUsQlA^-6~JRC18@N_8rOnhio7(1yUcS$Gdl>_h5$22X{&X+zyPz#=$ex`O=?*gC2qw8pTX}>;! zjq9ne@V=e#BSY>og>?}KZ7%s|(+-&krbEAJ5AM=2k zG-R6$;%K>O!By|~GIPyNRBdBha&SXpcaRTgbxE|icI7?M`YI_6t=+Jz6>z&-=A8_L zlmHQB(%t*iM{<82!25cNH+g8_i)?>48;1p7*t3k+wFx}pJAtLl=CE;21!h!WT$5zQ zY{g!rO&14HjSQRGxS zWH@IMSMacRCtO_`lh#{rfVLpehv2NyAe^r64yS~iT}fA6HP2#&%F3k4!3O5<7tYqZ zfE&0`S#3W%k?zP7{oxE<$L3dfV|U&rRW`k`zF=ALQDJT;W%QGbP)$j=yxaYWu{PH2 z<4;6p8gO|%+jN!{Z!c4DP4)Ttp6zgLiD8+Sxnqt+we+6Mmv{Cm%o%9|sZUqZFyY%8 zb@ApE>#7do`QF{uwK({7_mnAVOXWeCP&U(*{Kh*uUy;?(U?)M01rELY5u~_(YDbWY z=Og2jBp5_DGaj6VgvCS{eMN5N>Ta|$Scm&?5fk(JwB;Q9G>W}hrA=w)y`Xu$gW@7X zlag8_=M|g-*IzDeioOUp1pJ)gCI(w9R$hy(g^qH+KtO@NCZKFbKig7D; z9{}#cFHq{GJ1?FdzNM5|r(PRu{FIr&bn?jnd>;}NTZ^YbmkhgDaHU|sLGUrNyaw}y z+-Uv~7C5!~CpZ6eXWx?J8uAYR$W>6Uk>qK(K$i_$Sl}3=rXzociJ?IjG6P{EQC&zY zLvU!Rb}YTe?sa5lHIz+3U=cKac%7~nF)P(26=a^JX%G(7pv#14<~pJ7=qjb!Hf29M z&dK$*anV9gI>+-$_9KPZZ>|)g34P|lxxNS0lK;pCrAe=tJM^}MkE+tQArc=8l@Dv@ zmU)lwe?b+UxA{6q?~RzH6kdDvrXR z+nauhDv$+3)jAcRZMyb%BS)_D4}02+`zjgcPcRNmX1d>|m2bC`a4ePdtj+7|Md=ei zYFids<2ugKqhZd4KVs26jK>umoy&KC8(>l*6-?&?C}(-mGTm{Lv8GoVVR0H*N^d7I z&{g{xl|^>YdM$7-uet%_V<*J=7rNA{9w*wZCu2bZUToMg-efA`n`x4zDzs63m1knm zlYG_5ZE718D|Jo`KAEIJZNF3|cF!o_xTL4m)F9$%=>@I2Lpuc+>1Ha7d{y}0AtXfo z!m+RmrxT~3PMK|k^W~6E!K`Y-FI?o?D{UMVZRg8bW=x7X=J4u}UFzMt`^~cgEbv{w zxGaC8In|x5PGZ%eB}|P3f#jy{iBZX7&Pe%R*<6mebiv*vATQt`!{BraxY2T_c=3rb z(}QiMzEcfoU~X|VBDTPROIKlW3q{^Iq&g3v*{7#!oodXsb_84OsT)}p&<9_JZoA|#lerzvUkd70o0DW%V-w(N^+ znq3HvEuKxOU=rL3-re$dJaj#}dacVr*~fA<1O;x=GoL(S`ERmsJN7}X_oS}$w}S6tx(eJF9wH<8V$MJ{g8(}K*$paOiNIcrC9Kmi^MCvyZFHQS<-aRukxOqc?tM1r1x&@$g73vSBodo)6#eE zvLxXaRw8*vqi=rx^DlPXzyE>1kdmh#OMrhN3dE1FDU(iq9<%$Mv9`Sq^-T@;yms%) zR0TP$%lh?A!HeL_y0=HB{b(pDaTzI-!fZ8a%wgaoTjE~@Z9>c?-r4W^ zUqn6IO0(VyzEwP-OwSZuVTH5Uxv(})j}A##Hct4Jr&-}h<2W+BoF!TRaO4tu&ZLeQ z-3WhrS?0TN{%5$z^%d*Ghcqu9KFp5F)NtNOsGg~6sLBD|mEH4pUS6AucWR?HznE)$ zWi6CW(dMXrwUdL~g8dOjzaunnOz}N?`eKgvp7>RumTA%OvG8lF9QA|#pr>Jt!#NdK z_+4AxztAj{lI0QA7qZxKWz0~!FYAX#ZaIGTQnZx%`IMBKyamNMXf){V#iKIyBJIFWVLQx8>om zaWQfO!c2d0jOzqv7BSoxV*t#rn zi(7DNNcG`CTAB-O(lDK4zdseL_bb+grESF$&sWi`L7cY6N=N*C0@LFjC!lxFVUbNm zhfx_U=DiBOtTT#e{DxnBQ!>DuOPPT6qO03tg@oTMo9v(AMO$U69Q6DBJRToXdPo$e z_TnMZGcr@otn$Z^9Kj+z_CBM`{2rvCrar6vRO-!%dfV^)ioN8ge_Bp9B}87&<(QkR zKhJGrHAnyKkWKWWz7C$IWPa%@I}71h0lwYcvI$qfPBVom&jE{ zbsLf4Y#|GNYSAmV>|y$Nl7?qw`{JDrEEQ$v{FW${eSDp+56o@4@w~zAWZw3qeX-L@ z2iE4-?}rbGAE4Cs$ELIu0nJDc_Mk}`_Ve5t%X!+5n!8`;>}J)T?KOG!qxjtW<1cEh zW)d+@8TU!?iqOwh-(0Wm^bzOXHG3)Guru4vWNVqJDS_tcitU(@xohLoz-rtP#b9=@ zca<^78`fuqAE);k!a7b67wt9HYEEf6DVl$a*OS6=cBViauhWsY#wt<>ex4=#)915m zbLvK4`k&F*Km6zG3WIHOZ+-VkM~~QjgJhMW{kD4VrCQx(4zLc+`IgVzIXz0&_&VoC zMZCzSYJKjj%&+2vsalVx{AVw~muPXfEy|P6Q`d-%7<$7es#3?yS`I$y$~E@ zny~PF@gF-QDu-VhR^Q4_bid|%|H-Su!z)MnlFal&K+He=yCweo#}2Fudq#TelZeAr zPZ3VvpwxO^!8hMO%1rOL-OZyG5~nIT{C~G5E`+(H$4YYi4DIv!bobS*&ZdP5(1$!O z%j@u|?|lEEiuNV*QB6h1)4%0&gKhFlMI>;Qo;<~MCKYP9-gCR6HlI#SQxtNnuxI>5 zdPVbV!)8%@;zzMet!KwAeHSO8WtJan_H$q@cl?e9jX(Z*5UCx39oK$4rhEsoQ{m!S zT)la9RM3qJo$(3Uz#3mI{85Y>f*RJbcdPa6)cW3YO+H>f&AR9=ZX17jU+6?6{6*@p zu33VM@8wXXRNt5?y+{|E!d8w*56``sD3PzOFpB6!$`lzdBjGF!-_!1NEod-{iB$Sv z5-NO~3hb^(D^t0h6qTy^3u1aN6|1g!5UoC9b#BBCXdJ z*X!+AY8= z99FRP=%Gx1#Um~X%9-QgcMC0yV@Qiz16we;A9ksRqN*x=T4(Q)f4Z16Rv$Yp*fP$A zO{1kw6bYhlgt+b6GZ^-@ov4r}p2rlpx|Y8uXR@RTN=wG)sjum$2*)iI9$edv=EZ`m{Za9#ao-2udXlr;P4c1!tZsE!@6jECO7|u25r_X9gt*a4$DB@xys{!ALzl9 zpC@ei-mSyhp+qI~VeZvEu?x)+wlnfi{iUSdv-skCTSetc{c}@fMRO1%wBn1skIY%` zPF@g+Sy1bA;=}&j{xhq%FnE6tWD&rSZh+roXGS+haieX0BG>tZr<^dtO*5Vx1siN< zS=nGqRalrqg5#{|Eirk#7Ss2B_Cnvo%s=gIZG7T?#8`+|InZD-jn&x9gRV&fh+rX(|Ajs&}0_$v47X zDPq43Ys&hWxErz~Q0*Y=V|aoS_Dr0B?5w~@ULab|P(|u+-FNlz4!N2;ahK_*Uu$o< z7V0yK|1EaOWe5f>_BvH_dr>)#z2KaCpfGpcAjf7nhVz9lByC`)jfyvSp(V^j)|jhK zNc*GQOYRwI`z7Zl^?wA6FeDWJ*6)&6#J=wc^BfWxd)~M3qnPFYq3NpQqH4M@T}qd9 zgY?oM-3=0ofRx}Ok^<7*DW$-YBCwQ%q%=z}DIwjBbT@n#c)vgW$(=iQ=A7p|=Q(F) zckFs;c*%T6$LVIjAB|ca^`W&|&BL~MTBOZMrmtUAayU8JRHN#6q$Ru>e+(^--KEoE zR&JG=pBtqKq_0>(yos ze0%(mig!9PQe6%O`dE81&{auW7@eu!(1C>6$DSxo615@ z_@h1htq+ZptJkApdMcA6s@qNR%I8#R5|nz*&Gx(BL0$@yH|maPwW)DJuYB<}UXYd! zBJNHJNVt)?#@Ii}A;%<&`@;DWQ!WSZ0-ZXr*b~40$ZV>4yEtZkUl@Gh!%0OVo+oaGkVxy@8!oZS?Q>5aT4;b=ls8o;Kswl$+ev|i{_ zsbC4B(~Ta%_K?qUWb~-!{E9Gfl8Z-}9(V=SDcXb+ZFIIoBsO}j)gL1BtzBlH%NOy{ z+;t+I#@){9Qg;GZlWKuHNAzwq^_=sm#LIJ$?D*2Gj)eJm2AC@%t zr@9>S>CZd-ue$G4*qwVG+#@IZNkmU~UmZR#j-u3cliZ-utl&u>jO~5r!U?4k_;irY z3BllkgS(VbZG6%KY!u46wOOKX{Yz9(=2SUNO@Ob%&0B*Z%_{_0?Rz%pjDr@@W02JL zd7?J=J?oIU3Ekv5!5h-7&bBR)u)a9SkRMtfvi!W3%Qd*zD{MNa zbI#=WaH-epc9|8)yNb2Ae9CG7Ym4t#m#Is4(G=iga%+^37d|3TgUbDX#8(-Zk#NdS zsL+2Se!c2w*zH^064$81DSGL}UaKe63Jx2elezd{f*UBi6&xc$1;UAa`16hw;3bX& zRi`jgAOErU9aIfMgu9ABz{TQwh(Na;tO7wA@ zgWUddaob$`XLhn;y-iG$|$w-luu(?3vEfj=*Y- zl(c?TlfN4M-9YjY)Yz;6+U)((7RBVTsM#LL-F|VbP><@n?Tx}9iRRxL^*GjC|F4g} z!|*f0Z(q>rN35oqjWy3NYm`ZhH5(i=reBVnNwty*Ne#g&nRu=AKp$FlGXVBlWS0lg zSFPNjj?W#hG&1>utUeVq@5%_IcOpa&a)69QMDKHI!N~YY$u=4fsy9hG?N8kr%hWaF zIT~OWO3~w<&matTp?wbN-KV)YYB7XxwdU>}Bm`UAysdQ^gM>g(MWX4_Jc#)3i{J+% z(f%XUT^97+n#M-o{^w>&;HYkZtw+>alCQdXKUPhbaV-r*-&I;zm&H+2Q}ZIUyRTt* zXSmOP6;io_zkSGK0}~{Oy5ocGGaS!7ez;PR!;0d;*%kA|6y(jd4^Nkx6}=Q1|&14*1r zy>(<*_Z2wsJH|*ZL4&mE)+bTtm`+u@c9FNKyaSuM09}Un>{ifXdc%*K=Ux=0;6^(P_iA?he1!p- z6f#xuG;JlQiSE+3SthY*Gnok?Yv#TUv&Z`)V(=g9U1{7+lvvD3*bmJ>^(mzcCdVZ# z&NiD&m$PpeejyH57osX1zYXHEYjK7kR%@*vQPqA&)%nB^VQAj*O3_d0Wb79WBe_j< z{woAKc6OVIcDs>K7v6PS&sKhnmg&(47%Z6qk}Dkh9xKeF$+1i7zVtU&zZfYJYZ}FN zH$vtug<+#bpjAcx;*kq4cB0XDY?7L5`|P0i;B8b1^@8TFe-t@019qG^+lKdX(Kx7hu?P#b^e zkuLAHArHig^?nwC(gksT|Gp5DfKQG8-UM-GDv8-Z2eKdhqI-8nM7$Lfo zJ?F%NMlx`XvsBo2Y)>-6bO|-Ngk=T1T8%>lZjeaA*SKIau9@34w^C)W>W{PyWGSJk z!3=7HZ{Jz<)MF33YCk}{h3j4h;^J?@eMKLd?X@)TDCO$m@_oUj@k}Smk2rSN0;`!Q z_1T~{wfSC5W^b}mPm>z;>95e8qVS~K@ReZAK%qY;us{=a(5^~AAb6|(IlTvC5ehxM zX9|`~>yYAd%q@0~wbKzwc1-45 zA!B*T8e2uq_TD>{<83NNRZWy;@|qC>PB(E3<~b?QHHJ63{KKrLSfA5ByZChd>rAFR zF|8rZwob~0Y_daWzS4Fh9((k@2r zNe#Vmtmu|*3Zv7H9KR#K&Jw{uQI>1ia4i<#UlkF)4RV+&#XL>j1~0#QOljRM^Dkrv zpqUV7blBKDzNBy9(aU8Z%z@W9V@1jmGO|!cmPafkens@Ha4*)r9qKdqr#Fzb8g8g; z+=yc%i}OWNRrtmjkgO`*px^Bu_;X4*C6CJa8YR0SR)9W6KNE@we zt+yZ`o*AfEh^iywwy}4B9*xfZhZN9)fYil;8-q8KOb)?+%-yHGH)9>HE%!r>o*KW$3Gj#IL?rUbDJ53qyU7m@0=QHK|ZmDqkH~rf3#A5VHx&h)vgB{yV-9o5vy87Hf$H0>=g6;(Y)U(;(l#s_ zW^Ek8|9Px@d>tV`wmF(OwT>$zs^lGg_eIlgabiU;#Ll)mcfgq~( z>p}~|$?W4|z$;!>+||~4=AmUSsh`DNFAK+GtIn!RB40P5ZXjJW5>~_D`(QQk> z)D=(MgBLzUI>SkXaDJmmpjL-Ba&mtCRAAEpz!?G!ane$2`*kyBpG~@Q+^2Na1yc>-2SAL!Ts|CnW1f!ZH{K8#?A3;_Hz-1p2?#m3_42kCbR>0>tvi?+%dZ! ze5GvQLpH_CyWxh#78s=HKXO5a(3ETgS@jYCTK#j5X#UFlbJZkK)$XvbRAHj>TioeRR!HDCECT$T%8ROR>F1>8+&KP#j}M zeN%n3X=z+a2Us>?>&@Bg4iIry9OP5M(ciu@lHug-5y{}hS1a$>Xc>b>?t&yY@b&EL83CE=G#l-qn0B+T8j0-od#oO5IIgyOCO;UmFFPmNda0i~Ms9#C9} zAdvCjvzDFESZ1Ko5|G$ztW8eHPqpx6(na!1752LIcc1>LgYt!N5l-CE@DlZ)KTUX96% zYw$p?KHa4Zf)F)e1r#*5(1=Mh@X^!%BaT2&NGfrb@tLU8>_<%UA7@a6(fTFkP1Y{* zq->2z|Jw-mF$Xvpvr~J__YeHV4C=eoub|TascS;iF<1myGPbKy19v;2x~#Xw+O*$n z*dyH{1Sr>3aUpyMG2Dlj;W1Bb%$$U|Z^%Fh@1pVGI&X9&{MeupVRD0ieUSjL4w-+? zy`GI+YV&9{+RVk21 zBQu+Uh_}L7mT@nz++M9%T?)-CFB;18)M2dLc@Mijc+4sDrsw!|(8pkl74^WBn(sswJ~If73?kGA zZVVB^$BJDqF`=rc&?VA4zY+z@Srk3I*a zlU7zz0?z$kRg!3>cXpJ68Vz%#glZyBMyisoKotvZA`>Q^0eb$ve}aRSzbKBplb1WC zj{c(*Y7vOY7C_W(GzB1z+%^7qfNl2>DVr)hLN$CnL!}&?RWG|I9_6q!4iX#lE7ky6~xRFxh=HkHg*FpZK2ILmS+Ni_zspaJN7s zT_>n$Qe+ovL!08;G~)dcs8!$r8q3H{q-o8rOtOBU(8de3v)kQUJ1d!nCAuZcm|p2* zL^ToEL@K7@UEXU`qWh3GStTVq7|s?*lz0z9lKWlPi17g76$>+Fu0MZG?qcUK{&5c( zgT=&e(`D%@hBw!GAI(@*c#*%su5s4uw>m(@g3mAbV7e>$Ho50*;E&4Shxr;c+wjR& zeT*eC{fZljq2NOVFdLDl0*L&L0p6!vOW!nKJQcVi)Tk%Yx&1VEV)WgabI#8mU*r5o z{TcWtlfkv(5^0mh0Ld!^CD*#?K|s?39Q@@AmkwZMx2tHP&Xuh5;X}uKgrzKRoT%Wf zTj6>GPG>CzX@*w?ES8pgzT5*ExKl@88%M@j%LN3+0Y584~*2{LDjwl%O?_%4pWoK zj;_6Yicfqs%a=T3Z3b+UA-9V6NHP8+>k-q+DYBr+4x?QMxVKajU`O8Ep0B19G&xo` z4E%LodC)F|R{a{Yh%PgL(+e-lgi=@<{8;zZJRDZH9XWKw2Xia{qAsrQAG!rrfF*6K z););~bDj`&!(U7`2e^;%HQEHp8I@ZpDeUrUAv&0Gx%rEvTLX|zxy@e1U9^|B9L=bY zm$MEnT>-^83^7my}J|xQ1haK&=62NBHbeD1&mXjF$ka$ z+PCr#u%!6n0EKN+ss;r6r_h`kqhw{mA53Qn9Q9c7(STk0m;}IGmvA9JAF%}}o$an5 zE(Q%Q?fFsz{tuStrdg_zi^)`}TRpkHsAs{iwn9HRL{~Nz#vQg@0jKCch2aGp2oXF@ zEx7>#;9(dvEj-LQ;^i`-#tX8aO#1GkIRYXZ6BJ>j^<-I}2*B@_#@7fXy*;*;`@l{_ zVEXPT#%E5Luzg6~81VECem()9J`r>RS`^mpt);)7v>gc+hSHXx6v57Pvo$4tsYis{ zEf9+RXXDyHP9WGuBqmN;uJt2Vlyjv1$ea-*xs3$s*p-dmR293EZ7&r^Rc;u(P1*cH zzT_SnSkV3c<3}n8nKvCwnr66)>4C7*d#(@c)->rjE&P?jQP5W7Ub?tbZa9Q{3u&8+ zfy5op5m|%U4DulNJ9w{;bIsFw-br&xbOoa|i65pD zDfrq4Ww~UGpYM^sPg-8bIvo2h#ne&^SBbaIfTAzv-jx#?~0v&i7 zAV@s~Wx3PZ?_s7ZUHe^gPv8b=O~RRif68{PZ0gSDa({hFw73v>rt_^x6G2A} z8`!9G$c$((FFGaNt3O+m334p1_Kj9cS|JzD9bcLT{t@c%NXnEkyhL?9(W-66n%# zC8+puI4U{=Ip!Y=bg<=e(#@b`bu_BNI|L2Xe+xr_ugk?J-wbQS%Qwm84HJ&Fn-8;{ zRFUSYG@EZJy#;j|0Ak`VtqLv-U>GF^{$V=E+6h~Ll4o0A^@-e3b7=7mQ}Q8>^8&Wr z)Ip(gVw%EXD^6LghzBS1ogddz;2{OzAs+oA@~}!4!qE~2kAEcF6p|Y+%{tG?u92H@ zT(Yy#>I3z#jpfanRT&>Gk&2}c089tb>NT|eOV^))0Ni#_M*8mCf2!k6Wu@1@$cLu? zS2Fbpg?yHKvfE;JtFhHBr$|J!hzmgzbcB3WO7zZf8=A$l{G9dQX3qsfY`4NQn*N$4 z|1v$(T9@pHRqBkytaLmH&$z0R0h(g&`(##LB~LcReYjPhNZG5SfE--m4g4k}w0NPF zD--{>83qmKBLBagRX2rfgj@eI^!az1PL=>vT=4q1#8iHwrZx>`&QgBj@f+t9LX8x} z>u*f_a0?VZ)dBBLNwiJ;G9|BrDd)19T5kSivUa{@Y!(+Ec+}|vlMfo5nS8uuFiCkGk_7lGD3F#z#3sa&V9#kKu<2Ll&xA`W$a4LOa*?ZuD-G87{ zz{)wB`2Svj6pNuxITVRIQ@(T7&w)STnbklrOjxd*Ktf@B5<@q8{DVvQ!C}RBUzw(i z7d)l6$K{(A#1sD{?lOJn z^#$m!XOcW8R@JQ5w?vC)UEJ0!_1&|d%67n`_flp%5@2*+`o2m&lGuoe9V7WklXqOIOUG)>1G|OrnMkrV?0a=n7H)=7azee zSl2rbBLo+Uy%%?Vb}bZMgo{B5fC#eV`}>4k#ckqqaya!QhiRc-G2bHvpI51KisJ{8nv^u$O1G z0`~Rx2v@=wxMMHj_Q^(MBcsr4lRvmWqc@fw<)cd`IpMgenHBT3Ne^s3!ifk}? z8o~Q+wXBnQnAd%2bPW7iw=PP39r3^hl6?!Wj#>Dwx?b{sEERigyCc~x@tL$;Ux`13 z;{n&TH_Xut=hqVIcwvapEUH^8>&+_nHnQMx z)=_8&1FeT|hrjaS$9ZA%MNh*euCcdIn-NR&{`9=7qttLa$1Y~z^I^%Pe*e@s<|D9} zKrLH5882lV;JYZiA{YXOzxoZs^f&_Iv0CwIhEO>lXCh?=%}U(Po^Q3Qr&(vf5{zWM z11%mG?X#g1>l!uoNE3N4xqztQTTjAC-v-*IphFo+S+Np+`iGF)j7DKbem08?TUnYb z0f^d)e)8n6;O*sc^O{>45eW4+JLYGug2sPik$#pSj$CdWp;O6#4tUiz<9^d1dLFSF zs+V6gE;0Gi&(@)(+76>~JxpjgyiLs;d+3_R)Y=Cl=nI8?fCVS?AJ9XT8-aZu$g+g} zf_zwA#B2C9aDOH5K8Z7Bq&V=N+2|qh_ND&gqTs zAm_-P%dd9w0-9f)GoEFd)%lsoxpeHHG~%{yjcI-+#s4IC&zjxw!GidbN9M!WdaJa3 zHf_$L;ij#VL7{1WPdwmX7*I(KhYj=OrErmk&nJxf zhgM7Su7uooqKt7GeqloC&md|L}Jt(|Lm49|NSx-=Iua> zWM8@*%7CV|!SA0q-H^NCF)YRc@I!7X2fXNS&DjkfQ93uy;YoMt&R1-Go58lY`nX*lZrMx1{ zylYaorlx0)c@`CIAHSXbEWgv1eao@KEJNszxS)#NmaQR4$=1FIU%X}qaXIdQ%@c{x zIpq4Iy>?A`(8eM}q${|Xd$G#xxCPhHclS(W!zenikifxgiM?+xwk zYIMgxBGt)DzbwG>;p2^x$=;M;8c`EyH=NJcD)!GEhW8Q|(o?4!zn@#(% z0;Qq}Y+&o+g!W8wc-pyZws_1btPQ8(ggz(FS_{Ujc6H31@Ls5~0;HXg^PI+}TFZp; zf&ImaVepC8#CvbQu-Tav&MdL8<}r$47CSC%&`+719Vtk0o8tlc@%oGUS#JD8@zffS zM&6@Mgiw0PNYxz{wGZB)O%H$Pj(wn<-4ajvPVDje>oKo7jdty8TnM4lmY8{7RHd$# zv-}lt8wUkBqSi{(aW z#;b&%!q{qlEiX$_9 zd?cviHex1-%rFq>-tk??x%T;eOc^A!;u-tX?2MER6dRzP&>%T~yMT$3P!iPlY=(Q& z2DXLnBXEABWSkz^FSx1tSQ(7Z2I(|I&LK24!4Q8SpkgOSj9}!Yh3W|~mRg(j6k&8_ zMhTtQ$CgYsf)akhXfB#0h zn?0T`MoqU2zQ@>-sobcU$d@;HpnCZDp=RLq*8pm0)@@$SsPQ4IO>(rhm2_@t&1QWx z@9_XsE-L$$8c8?#GH1@){Y-)=aU7kuV~Zuo&GSmF5J~iAHpNW5xF#2>Asxv8;Ueyg z$7CfCH1fOgb0#FEzU6p8p0VYt27}mPCk`L#7L{+{FjlJ-Ao_a>?uj_QPu&l_30sIA zG>zzavDRVw)MtJ%Nb#Dyp(o8-?ecQRVtqDI@9OAYn=x2ble5rlQ`4cER4i#9n|lHk zMt9wSCI9dVs_^w2uhI)G`Nq1bAiV7LL(zQL_UJq*7|A9`&zm0Bo&YlY)YbGn>K-SA z569(V(oo>cF1}hpxDESC<2O;;%ZYtaPA#}6?;n~7@F!;_-iuF$a<$prsd5>F-h;)g zKkLq9wQlL=KG2Y}8l?CZP0)M2`4~y<&8P?$&hp|J84llF=*AEqopJOLCcc&U=4EDr zcWbQ3F$V_krFMi8?13!I;_!X8ps6FOzbTd4{(XX?!yDcgSF?Qb%#ZaSQm1pu6hsXZ zntz<``$9F@-4iHr1~_z5#fm&fA6GWa*S}&YKiV6WL43?c>z{$itK+=SBdX)b#`i#S z$-`}!mOiAV>ryY;^3}}v&XE-eH5w5ka1c`i|CpsJ>CG!uE?C~f&i$DN3sU;7Yr|hv zzO*dOzgbc-n2m#gPCBgF@N?uYe2qG+k=2)F(IS|@T@aDsnODTsxzebl_Z=4a?ygp| z%r-|9D;7uX&Q#Dt4mIUqtXVSIe!wY~xna(b3s|q-o)&ad%wO>eeCJOZWS>kEjk{f?h0AndDC$V{z@(Ya&%wY)-w{uJ8Ymq-x zE?H+l6<q&B@PRfW8w#kiqn``X7*kd_|Y|pR@z+QXQ7fu zIB@9$FnK6QHC1l3`50l5-M3}>;ORuCZ-Lg~aK%D!ka-Mpa|^Y)eMpKvP`EsNWi!Gj zoGBIO{cSr<72elGqBE>M{~3Q%mmfflm10mu#CDnvoyXtVogpx$rE$!7u`4Wu%#Vtl zhW^cgdBbJbEoUFXvocYoL=&asRvJS^0<35rAOaH%TuVTmU*L(}`e6V*3v`GvfB5%b zA6~22$gtGu;$6V`=g?@u1TdOE4T+0ws_V-JA6B0>q?RVV7-~NYI9#sxHXu6X$x+r@ zkz~{1_pvs8(px+hHKwjMe@TrWHt~gC%yj%%iGTYdbI>MQL!nNXqOp|0Xzur33hpbu z&6LQNuRi;}HXgBJz8sq&?@B?h_l?`f?=RF_HZ{pdL=<&ETzwG-rOu{UL?k6KBy-1&Q}WC)p{+?e>gDNc4Qm$25E%3;q~u8+n!vuWxt}1~oTAP|5B%(y-m3@Fjl$pd z5*BQi<{p%lp8lOkV})+^A+2T=phhkS8mvZw@_q^{yOLLN@{e4a*VwP(CfF8xu4NBT zLkwHTxL~P?A#2;?`LW^+R_UNW+w`=InedL6+TaL6XtpmDsbWzx`J`9Z0)O)l(l_8F zkXGfnF%W|s4(q?zwr{zTEw1qK7#){%*B__Bwp*7w?K*(~joC>V33t|-oTL6&cLF2Q zd#8*zuT>;ryqtXwpjKI!^R{P>3N3wb_gE8>zGAal>bvKgxu7HJ0Ikdxiff}e-yG&n z$%RmqWsR4~?t?5Y2t>8EBYUz9OB(S4k-8?2TM07jgqrHiG>8%cHI;7;OUi89o~w;N zv|9@-7hYvPf#AR7M^c0Cm?TPrh3EdTW3qVR5GLlE?$M>45~~59`ibnL%m+7w>HIU` zY}4|65Mvb1#S*22G0^pTkrD$ee&a4Za13De9P9wW-raaq_XOyyoz4PI{dA+ zdbE>L{Ch+@stoj0M+ZL$!j$O3lO&|-{;7agckKI0TPeJl7guy%P0SV=0)4kaQyv?a zMz%Gw(KPQoG+OQ6AXdX)6~%Jcr+;CkABJrJ4P>Zof`83j?U3GX*V*wHDa1h>$=#YF z>ZflkHO;`9sAND*AIs<4)yR*35f4o}w<@nCH0lm*K#kM$$@A#;En0QaJrW)s=x)f8 z=+#5-vvUkh+7$W=OCRieg=tyz5_3xCPo%@l!NR5>+tq?LP9#u9i;&zF#qmVB3D3NE zR_#-P#qrSJe8F5iEoV^iEGqDCcLQ4hz6oMB=8JM}S#=&*ceeZ8jkn9$6qgL);6pTF zq(SaqOH$JFrHTqX!@1r)N2eUB9?I%&GPgzAe38WYZYm=bry^NCp+a@3 z&qm|RDK(1f7cBh!5L@E80d-zE@rK)NC)19E`qcCK-!Fqg{lj(XxngeEiU@!?L?1m0 z^B@0M$PB!-C3jTPIW~(7ZOmpP8EI^8jao^f4cr_XT&)TDFn3m_{<*ODVb!*+1*e0G z?}(GjT|&tMV9$zem@nLO>uqg&_kd%B#Ddc~!x2hzBLw2~!?m?owACRFYm%VVSA_*c zenj<7m~V=-!+`(Bhz{%KLQVPonGOM{BTKBgNFp=Qpwbh|KH#xj8W^^gK0#$Tcadk7 z8nOJ(>|Qd>5_+cst;H7T*`psu`~Zjo&c>fe44dIv=2NcVBxJSGsM%k+ReUyLOYeOd zy?4k#Mt4=EZw0?ObP6oE7hj2xB$?GY>mi=S&igXD98b1u-XdOqL{IX1;&R2T_Sc6T z^4P)ZO;xk^*KM{ z3FS{{TBG$B{Ub5C{)t4Y_Ce~A7g!Z}4DaTohF>Mcme{y$(Oj7C!M3J=x~kj0EU}kf ziOw_$$q?52N7{Or0hFW(B%Lt%CC8?GPthumjGjD!qMwW3k|mSSCjcGqt-&TxT`-(L2B+t~tj6WSAU3#AWDVoO;L}8%w6o=)< zY@syh)z+Q0ZHa$zHVOo4Cl6ioZVDrIjs#$1_q6kiJt-HSvLs`YvqHLAjq6r{iS$&2 zz|C6Ut+428_M{Pp=%KLWM`KHnFj{t5THi$)(dlqt)IWI&PwDz)FNV`USf}q`cBYNZ zxTS1-%R9m0d?9R#5z_w$4*z?Wzbf#<(pO#hAZKi~>apy7s;$Uj2Waf(EwIQHXq3#n z8OYt6?!amM9R&@F&M(~7zG<20%~_8ZDH(M7UWA*4&C9rw(9eqqpS+f=m4w7`yriq} z9yYNNZt6!rRYM0wl?h>SKJ%RvE`vJ70;(r#w5 zJnX}qJ7}2%5OGfCfN~t-&>$B*{oZ5&dTTt=Q$E(s#8(zHB81cX!j&9|hmB|wa@>UC zE(%*{nVh1^%OsW0|7(m2-Hx5^v@L9-4uy=v2SlnqE3e_4?8c%W7y!o%{y6TjOxzCB zC@19a514%tdY*yGSeNlQNm-X}p=OoS#e@&@SA4;5L{;nF3rvdSNmUjG-ZDL9Or`aR z=Hi|~s@gJ`M`fdhm64sxY6dz7mRLR7se|{S4c}z#9X4En*qk(XS}3;ZfE*4@>K+hj zL=J`c5|+}5t~)J&(y^mO&HMts-`4<{BaeR>&p-uy=wkv%<4)t2lh=)4-`@C?Gyi@|v!+-62ud%I_tzR#lP(uyEl#GXUOyqK2w)G|c1=RY5 zyBgO$Gnc<7ja`z>Ri{&)CCbb~-S0yTV^yW-9Fux>(lSQI3P-#CY2p9rx;&m&UK+gyn$6&25)t1Afatcfw6y^Eq_l;L_S6AzK%g;+B?870k^`fg_hIKumW zidT$J?TsJphvu_S&R2nVPBK0_-Q01w7q9v{o;6W1MBC~dFX%`3I75GA`e;>qak znHJ^W8I^ca5IO=1Hi9XdIrI)yKK0gMcY!m0T-%f9h`toKm`qSU^~vE{I_upE`$`pu z88V{Z&)qRHM1)bPK1zxFjXaaMtp@o+L#7ek<$0fCMOn{l-^G)w-_YYnLZhoVcHx0P1v4*~)kA>ShMonpzJ zgz_&&yA2;(|F?HHSa;r1g`x@_y|-_n5^b8k>|YsuAu9^sniW4~8w4R69d=T$6TH5k z+*f*L@^3cuOut&uB^kzjn{$GYr?NA@&-`H+cKE*UuhhYAH()c0V((Q?gu~_z{$}Ah ziS`n|`bgOw+Wqi;ZDAtZP-5}R&yxdts)srGM%wMB%z7b?-e);BS!#LQZE0RPj9ult3sf^w;3_>3gFA5Jv2{HyCLK(Yd9}U!46)p!3~|(}FAAF# zNbkrEJ)^cI-Z6VCUpR6KG1l8xb&gSIyG=KDM`QDlOA$1aqDv#IZ7O~FE**V`5rxAx z=TZ}&K7ROo&FZSt=2iKTLC5qiS0))ULhKlVA7+S}Fo&mU(smY=ipyKh3)>}eype8u z_Q6@VLy`9$Gt>J3k4@3kEa)e~(z~92@vflR9@$0v*<{AiB`i)*NWCAI)1e2zhLZ>`8 zCYdgA=_~CYZqs=yA)Xc~F|QUY?|R-oyfNIVsdQ1uOX_4=vXnIw2C7)W@sS=HltCr+ zdTZPpZ*@g^aO#_|;*>)L@=jtn%3Y|r?!#p?xTYUMl63%5jRM?S^ypXOT_ zghBfDdn~Pg z`8rcq1a1I#Q!$>PJ3HdeC)#DFAyQ0_r~29__WHx?W`j1Waw@}BVv?R`CPQ=YhlANy zxh1^443{J*5bg)Fj#qMARps9{t@)uSp(jy>U2mZv*zG$R$s@v^V4Gp_&m!L6v96th z=sYn+iz8 z&gM<1Xia|Q$@C$b4}C39i1tVnf89<}jW#YimhZN1gH0~cR|}Qsf@{_x?p>|JK9z5K zpOhA#M%O4o&FG475u}k3s+gy)ss8R=M6AfDUm&7(FO$#%PTaYBQ_yhHoW0Px)ulS2 z{8^p1BLvPVh{;rc_I|Gy!ta}Py?iGHb006OyGJOHQCiwc2%}DB&tad1Z?7!_Tw)OW z)~)z5Hl7Rc>JQr)^_?}TH;0;cqQnDUiXSa)nHzV6GKv=haFsI#3z1%a`@KaZ#RZW` zZ^-rKU{!hVKD2;12^<#Ha9JedJ<$t06Il4b`<9FCE*m_FOtFGPlb@LR{59_VGC)uE zi_AD*4>lnn5dKq^lh&=qvTQVQY-HVLCAD^5AOA+VnX-RZ<omtvi~kzyOU6#z zU5G2(@OP9x0D-e?zF#eHh<@5|0Mg`ueNTsZ!nNl!cTQCc zEY2nT-)_O!a&tVs-xoNAdiXUC=8{H%+suu_Um@Rv4B0fSibHFkV*Zbhla?0$Qu7*wNpX}J zaFy6{_FdV#NBfs<$?gYP38$F)zR@Mr6P)0w1!etPbZ+t*&TrBQqSJX{)X{4nUtnF> zS)5LcazAs3B(oDMO_E6M{sIxsX_VTL7i!W>AEJ5x6AOda-9!K09F&YR4+n|JBezqrE?-sbqzs&JR5=K>uoUBM!Gt zKdj!Na*B>f@=<}l0jwF=z?P`pm9=WN-y1r-PNm;w99}&6oLw&N*}Inr*`_mp$zp#H z9X~s$QYcWOI5$i|{MO6-BP`Z=PwV<P=lS0X8oPzq z_^#5g4NOsF_ZV@3O)lz#>!UJ+C!X!LH4gPjp5`}!iH!G%?ncU~mzYD1=E|G3_t$VW zE{yn^o7_sTH4ShfWmhY1rl|L?E{ts6aCT=YNt}U=x@2;R<)0UXLVd)h8l-?hxcRG57#=9MXtPgv7Lr@j)xc`GB5SueX3UeZEQb(*IWZTfA32k$!n;GYnjZopvK@3 zQ6l?B{ev&Qnyeos7)wvMyY>pC*J{hWAd|`_ugaEgc%wOq&ly7AXI{v{M{_HrrWx$v z`eL(B??36UyPuVg@ptav>GviR$=c5SdJeaWtDO3*wG2N?)$FYU4@pe&mZndizd7(9 z9qdzzr=ZYnxR{Qw;m=Lpe7Sf{15d4rJwd0{!HPnVD&E&@k~`g>@7`^a=Iw%3yj(8w znvc{WKWk3zti|H0Ge)i_DJr|lT#=vs`8y&cG~tshYewx6W2v-(TXaBVNs&^w(kdU{ zkml=N>fhOvV^%jih6cGZ;lR<&`>A$+C4W!8rd+7{sQ`6S_KbbXu`qymPh)*=5~QeZ zd5sIv@zw17R2ut&TdyLI}mm*&1WT%xsBWo!)Ga*2`V{Un=6<>bKE^eoR{cmzy zdp5$omA~9kn4Wt+;$DRndaKWCb<1FRn7!Az`;9r-+v)Do32TAf(qcnX|$-(cF`v8t5y}b?t&R|iRx{+Yp;nHc_7+7kdFydD06AJm;UQ@ zu}z}*_r67m^{a{3xE7NS2BUs&6e6$b4=d`bnDKBIpVxeJ>(5jJ)Rlh83^QcFfoN0HLQ+##8fZ^ed+fh??=CyGg_7|xlku4( zY#A`Q7ZXn$*S{yZvA#)p$Xzqa#A;| zPt%Y2Dv=oQ|;^~+L{zZ&`l$&t@uVb*qu{XG({#e zVBSd$WPn}caCsx`1|b&D*&n(eDDF6`tWKD7`+`qywZ9)}uKs%mwX+%30gLf8J3eGF=yCVJGIcC%o zfO95``b8{cAs`e+>_`HcoTQH1U56t>On7~ge-yQ;+|{e8CT;V(J6(CnE9O0m`>>bq zpS<=Qv7 zk1Wt?!j(?O4sBK)W$uxJ>~!Rkpcg1&X`>UH%?}yZfpC1d%ku#2jAC`KL6^$XHx@5z z?Kre^*jqtKQ{GmWHe)nkB$9u}xP#Wt)KeFf3Rzay_67t9ug}^`t#xVe_jD7n`Klr$ zX$VW{vXJuy<6Qa5{f1;I!A=zncmeuqhW3E(-7Xa2ow!sJ08cEF1tdPrn_6C@s4nFI zAjty=bCoi3Uw(*#n!1*ccx2Lz^+zt9=3-rGyp!lt^b>A4>VCc#yIN;C3jz{^;qM10 zzf&$+95_yZImp~eQi2M<*x$lxb8L^ceo@dYGSt2Y8FAsiFSz<$3Zy@8>j>#U;=}s{B_^89_%MnHY9| z>T)8kHUWr%O>EC?_A`*^A41P?{Z8V?PHb#w zU`2b#%m*LTWE|A!Hx!&J{Nx+eojH=*+c#Kbkse!fX_5g!)*f)bXwK|W4xDBf6}|6F z74H6Kz+JM}NUc%_TfEE3yV^kh(QJ*yupp&xx-Sk3o&4Uv=(%`LYfkTkz0||PN%(Ef zh~$`XL;o}^+pT{vIbgkRS&v?{CgPLT-}ry>>4}KCb$YqbL2388?_Fc@G(Sw!y_iR}&Y*-|rM|jraZFK1S`*rdFGK^FQquS!GVY2{_~vHT;5} zlAdBaabm4Jsvhm*f6|O;NAsXTlRKh#b8M?k%W-;7hq!!vcd)w9I(uRW04lt7>y9P z`9U2v`BxE!w18PT@Unn|L1H~fMe(|q5&_^0?rqHpj?iyt!c|SSM5mYnPQeDx6RonRUk91+j1C#4U`Mvze2T%+vlOzrSr!M_ z(P30p??FS=)k%~l>2>lLRq`tDb5dhh0oyiR+Bg&{3)e5Y@38AC;yo&+K@#+cbg=Ea zTA4z&UspieY(&bxPI=|=aB+mv@(5gXYkq!fE7brh%iVGkZckYbbo^wSu+UBgu0h_P*@-btc@Wba=5uPe!0sUq)V&KEYt+-VZ#Qi|!*(__&xq z^$DHNa6Vu0OA&SY58mCIXA&gE0g!WgsOuH`@d1oF|1F)CL?j7Uz8U_}EibwY4})ky z1kDUSq7o!IeoawXDBk;J7LBn1%{{UZeE^nvN!EQ|k4&%^)Yab2I~W4-PB11j29yi8 ztAX0)dP##VwIX#NZU_G;KUChZ^Mz*m+@K4}%hQ{locLkYd;sDc7>+jy1w2!RohzR` zXIH}TRs3t%IFp$>wOohaGOk|-fT4lQqldAL=xm;85kJ-z%?wMcycqX66Ly8DCywZ^ zLIbk2Y)WJC`k49N#JZHcKc*%+U6XQqV%jW~U9Zj;C{^kJX#HheWU>N2*Q42{nKv>I zLt~~?b`8bNG3cyq#{#m~Z73o_SrXW`2d(CBdhy2c8>8c)*HF9bh>WUQtms{DkAupzHAX%ZV?I|II>QJFAA1BRblo z>ws{ty8NloG#sJ!<6lx-TM??>lb@6CB#b2Syq8GN95I{>j*i>5obJY{)QR8oWaJBP9yt}Hk~FqHESEW zIs`vPaeWPEIcXxN9UfL0w-KXEU1ufylfcn6u}_o#2a^+g9k*W-JA(k>?w;`ioS7ju xp($#r@c0Au{~%oD2!Lo6it9&&h!Xv48XWp!HCpi{s|rp4s@t|rr5pDq{smU#`osVL literal 0 HcmV?d00001 diff --git a/tests/test_timelock_recovery.py b/tests/test_timelock_recovery.py new file mode 100644 index 000000000..1df32e6a1 --- /dev/null +++ b/tests/test_timelock_recovery.py @@ -0,0 +1,121 @@ +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), + ]) diff --git a/tests/test_timelock_recovery/default_wallet b/tests/test_timelock_recovery/default_wallet new file mode 100644 index 000000000..a064672c9 --- /dev/null +++ b/tests/test_timelock_recovery/default_wallet @@ -0,0 +1,1437 @@ +{ + "active_forwardings": {}, + "addr_history": { + "tb1q008n3k9xjpcuyx4mlczn9jm2at90ts55yrtynq": [ + [ + "30bc3855a6837562eb6dffbb3190e71279e2df2a04f2598780c9df7f290706e0", + 1455208 + ], + [ + "3407144b10e0c7f60a88e9f3adcc7e732c83e92fa17a923f523b8acc50718c2f", + 1568264 + ] + ], + "tb1q02g5nde0heaed0y24rztkedh9nvswknw50h7fx": [], + "tb1q069xqa4lej2tljmd8fcvvfedav54nmspvjnfs2": [], + "tb1q07ulrxeuu45uqen0clqe85v5en6rf77cxgxsj5": [ + [ + "0ef1c91636d20331acc2759d0f0335925cd899e270efdd2598e9449c66649d39", + 1346957 + ], + [ + "03eb4932abf231999f5a698a28f70cfe6b1a2975be0310e348a9898434efb8d3", + 1346978 + ] + ], + "tb1q0j2gt4ap2s08cz5vzm5jg87fdeps7x8v4djgrm": [], + "tb1q0quewquwhlfgahhsdg0q3r5lmyzufrtp3fzme4": [ + [ + "737857196825fa2e600bac822c866f10e5f484b0f193cff1fc3d38b91884c7a6", + 1414311 + ], + [ + "41284afa51eccc94d10fac25eaa59d1fa1afdc3e27bae2b16a64399fe0f1be3c", + 1414562 + ] + ], + "tb1q25arh97ze37n6nk74n3js8ls8z7sva3f0d8pnl": [ + [ + "4dbdf2810f130653d1eb17ce3645d0362e4ef41ffbd521aad1f9294c95baf76e", + 1454548 + ], + [ + "30bc3855a6837562eb6dffbb3190e71279e2df2a04f2598780c9df7f290706e0", + 1455208 + ] + ], + "tb1q3s4hkssd34tyxdlhafthv4muckjtgwuhltu37t": [], + "tb1q3t0xcpmzreece8xdxq8k5aaxrt3r623tqldp8n": [], + "tb1q6k5h4cz6ra8nzhg90xm9wldvadgh0fpttfthcg": [], + "tb1q7mhmnxal53vtc5flh69nph44vah5j56eyesjx9": [], + "tb1q8evsj0vkzfak2y5qnqx4yf9lty462l7yfhegyd": [], + "tb1q8m8pzk9gpjamgrw3y6y8xtfmw754nedldje5q5": [ + [ + "c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38", + 2579583 + ], + [ + "778b01899d5ed48df03e406bc5babd1fdc8f1be4b7e5b9d20dd8caf24dd66ff4", + 2579584 + ] + ], + "tb1q99hkhlswfnj8r5wy2xu9a9m0vy8mvffwzhrx6n": [], + "tb1q9u3ufsm9ksql8utguap40ch8zpnw83fgzf4z97": [], + "tb1qad28sgvvrxjnxdnfjxcuepgzzhzlapgxcwuj0k": [], + "tb1qahrz50yej9v7574q9are3urwyqsdcdddmjl9a6": [ + [ + "778b01899d5ed48df03e406bc5babd1fdc8f1be4b7e5b9d20dd8caf24dd66ff4", + 2579584 + ] + ], + "tb1qak6t2hcl3se6epvhlffaprvfjuf37xunnxq7c9": [ + [ + "d57bb7a379c7eb8c86e34d595722ca4f389394b4497c1f0028971e0a8e75c8e1", + 2415285 + ], + [ + "6ff55c62470cf51cbc577e8efac5028e06e41912a39356ba576ab7b4d7f708ce", + 2415285 + ] + ], + "tb1qchyc02y9mv4xths4je9puc4yzuxt8rfm26ef07": [], + "tb1qcmq7v2zg0jjy5g47k90fqd0h7a4mcyp7f3ly6r": [ + [ + "66f846809c2d45842f17e40fb6a696288c3274a66b0bf71107940f250978832e", + 2528392 + ] + ], + "tb1qcwytrrw3wugydlktsh6yvshlk7jwld38akp8l3": [ + [ + "b424362ec4eb39eb84f0bea8e00ffd8382d5dde29092bd1e268b9a027dc5225c", + 1457134 + ], + [ + "3407144b10e0c7f60a88e9f3adcc7e732c83e92fa17a923f523b8acc50718c2f", + 1568264 + ] + ], + "tb1qd7tjvgttaxzkszzh5ty4yq97r8wscgteejustc": [ + [ + "efca2a86e0584d7e359e5bc9ac954c6ed7e5c136ad6d561cba662001221df23c", + 2349374 + ], + [ + "8c09f8984e231fcaebdda00b2597cfde3d825a26dad2c5478c78ccabbac1f28b", + 2349377 + ], + [ + "94fce9de7e82741cc982936ad9f7900372b80be8100cb80e49f5d78c2954fb69", + 2406297 + ] + ], + "tb1qdgscjnjm6w59chq0xgghwtq42vfhhn0murqx6hrfz2yaf2yx9v9skh03as": [ + [ + "94fce9de7e82741cc982936ad9f7900372b80be8100cb80e49f5d78c2954fb69", + 2406297 + ], + [ + "d57bb7a379c7eb8c86e34d595722ca4f389394b4497c1f0028971e0a8e75c8e1", + 2415285 + ] + ], + "tb1qdy4xwmgklqmyrfj336g4f54582zxtm2yhlge8l": [ + [ + "972d16040c71b6660b52a0ad06ddb17aa7097921643bab526ad2e00780004535", + 1414311 + ], + [ + "737857196825fa2e600bac822c866f10e5f484b0f193cff1fc3d38b91884c7a6", + 1414311 + ] + ], + "tb1qf03zdjdnzxwztxs9d3g9ynsvvs5rmjhvtmln35": [], + "tb1qfsxllwl2edccpar9jas9wsxd4vhcewlxqwmn0w27kurkme3jvkdqn4msdp": [ + [ + "97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe", + 2425096 + ], + [ + "66f846809c2d45842f17e40fb6a696288c3274a66b0bf71107940f250978832e", + 2528392 + ] + ], + "tb1qfu50fqxhfkl69n6urjzuhc5ndr96tuaxrh3an8": [ + [ + "23b5490a6b9723a96f8d1170cf0ab8d2e961d775cff0151540400f2964ccbd7f", + 2404107 + ], + [ + "94fce9de7e82741cc982936ad9f7900372b80be8100cb80e49f5d78c2954fb69", + 2406297 + ] + ], + "tb1qgcgk7j9kpt2mygmhmnu4zep79cd289t6aely7z": [ + [ + "2042e1e197e0b21e83de0ee8a92abf5e1fea617cf143dc584a743df075bc6269", + 1583044 + ], + [ + "b234c4f252cd193377bc6cf931ff1ef6a398e3a434e43b22cdb457a8213d749b", + 1583044 + ] + ], + "tb1qgdp7aa38x3p2kpn2s5486mkvvx2sktnmxkf47e": [ + [ + "6ff55c62470cf51cbc577e8efac5028e06e41912a39356ba576ab7b4d7f708ce", + 2415285 + ], + [ + "87dbf5a7a089f1e071559cdfd3e4bddb841cdd0032ffeec328952abf76fe3661", + 2418761 + ] + ], + "tb1qgqfvg54gaads92a6dhwcgvvfjvnxdtq373guj5": [], + "tb1qh0csljush2tad6t0s4qgx4r5t0rzcw9729l7kx": [], + "tb1qh878je0rfut79fkudf4mkl8m4cn8uzfsluersy": [], + "tb1qjm4rr97lxawx5csc3nu3rxhwnpxqe3s4uhwagu": [ + [ + "03eb4932abf231999f5a698a28f70cfe6b1a2975be0310e348a9898434efb8d3", + 1346978 + ], + [ + "7726939a1246c0b99c2f30cfa21504af5b458755231b2cdc1fa1e9af2d9d70ba", + 1414310 + ] + ], + "tb1qjpgepu2p6gyff9a92n2mwst4j2wjktra956lcg": [ + [ + "87dbf5a7a089f1e071559cdfd3e4bddb841cdd0032ffeec328952abf76fe3661", + 2418761 + ], + [ + "97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe", + 2425096 + ] + ], + "tb1qjv7a78ea0jp9d793d5ra7mtzkjzezwldz5zvr6": [], + "tb1qkneqe450eqxtpr3r5z8aw4234sjmpknm0gxsae": [], + "tb1qkp86pkt75ds257snenp3q7vs29pf4g6cmhy2hw": [], + "tb1qlclgzsp2tktdl66xuk3je7ztztstxvjatly8wy": [], + "tb1qm7ckcjsed98zhvhv3dr56a22w3fehlkxyh4wgd": [ + [ + "4029e471c030066ecca20621cc2917cade0fd44c5cbcb6f5299c086f25ec4b6c", + 1455781 + ], + [ + "3407144b10e0c7f60a88e9f3adcc7e732c83e92fa17a923f523b8acc50718c2f", + 1568264 + ] + ], + "tb1qn7d2x7272lznt5hhk9s07q3cqnrqljnwa55w6c": [ + [ + "30bc3855a6837562eb6dffbb3190e71279e2df2a04f2598780c9df7f290706e0", + 1455208 + ], + [ + "b424362ec4eb39eb84f0bea8e00ffd8382d5dde29092bd1e268b9a027dc5225c", + 1457134 + ] + ], + "tb1qndaru6pfal030ev296uxwuulxrezaj0j70ceje": [], + "tb1qplsf242vay6vavy4eguef855fx3klmp9505g88": [ + [ + "03bec7a53ce7c856046f306738a14a976acff34f12a1e3f61f31fa4284486f7d", + 2579582 + ], + [ + "c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38", + 2579583 + ] + ], + "tb1qq0gdz0vz02ypa3cawlljstrx8cxydhvalcv8wc": [], + "tb1qq2tmmcngng78nllq2pvrkchcdukemtj5s6l0zu": [ + [ + "024464eae9858f079d2f49bc3db28f92237c63fe950c8ddc6c9b18d1855fa32c", + 1346870 + ], + [ + "0ef1c91636d20331acc2759d0f0335925cd899e270efdd2598e9449c66649d39", + 1346957 + ] + ], + "tb1qr7mjlxgc6at67tx0s8ypa5efx8clc47xh6yjqg": [], + "tb1qrdzfu6mlgrxpupd4syxrv77ncku89a0y0vd7f3": [ + [ + "59a9ff5fa62586f102b92504584f52e47f4ca0d5af061e99a0a3023fa70a70e2", + 2579584 + ] + ], + "tb1qs087qkcawefutkkv8pg037t6txldk5szfntamj": [], + "tb1qs3j3j05rjefnjqf0mlpztszg8acz868wnxgz6j": [], + "tb1qsd2c4xwg47hnngn2uqg66y5rxz2hp074u9rq6r": [], + "tb1qssypacgyt40r8t95myqgrhrdhq93f5y4jmgmqe53w4tlydphcnaqmpm8kr": [ + [ + "c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38", + 2579583 + ] + ], + "tb1qt339ksrha0n5a6lwpql778erkm272hxgamdc0u": [], + "tb1qtf9mwfv8ux0j90cwtx9nvz9l46jav40sak7ncg": [], + "tb1qtqqddqrlg4xj3dzvjnea8wh5zy2fdf3jxl7qhs": [], + "tb1qucj6lx6eatgm5396fe539x53cp8zr0yzgclk6q": [], + "tb1quhk94rhlsflc4wgxl9qzd6p6wszt30uxt4a0yj": [ + [ + "7726939a1246c0b99c2f30cfa21504af5b458755231b2cdc1fa1e9af2d9d70ba", + 1414310 + ], + [ + "972d16040c71b6660b52a0ad06ddb17aa7097921643bab526ad2e00780004535", + 1414311 + ] + ], + "tb1qusm48zmlzwr32csxdw4ar7atw260h22c8zq7jk": [ + [ + "25ddebe4b7e7caf7b175762ddc4cbb5d704f27520a997b7672057f2455b2fcea", + 1775825 + ], + [ + "10a2a7695a5b6ba857b10d04ad047b968656c6d84b58ea226dc92c150cb73b2f", + 1892447 + ] + ], + "tb1qvsvr06h2phnwmzvwxrlkuzfu2ekhjpfpvguyct": [ + [ + "41284afa51eccc94d10fac25eaa59d1fa1afdc3e27bae2b16a64399fe0f1be3c", + 1414562 + ], + [ + "4dbdf2810f130653d1eb17ce3645d0362e4ef41ffbd521aad1f9294c95baf76e", + 1454548 + ] + ], + "tb1qvwwxv48k9vch5ddmf83g4fhd0tnx3mt8jp6rka": [], + "tb1qwu2d7rjn9yxalg7cjw6phqzk77lf3fmsta8rhu": [ + [ + "ba48bce5cde8a4eacb4f1273c462101c06305e82d6ee89a113cd6b068178651f", + 1583044 + ], + [ + "b234c4f252cd193377bc6cf931ff1ef6a398e3a434e43b22cdb457a8213d749b", + 1583044 + ] + ], + "tb1qym6srwn87eu2sa5prkgd2lqva0nh0tr2xkeftp": [], + "tb1qzyaz308030saay93zqma0at032vfqa9y0gfge3": [] + }, + "addresses": { + "change": [ + "tb1q07ulrxeuu45uqen0clqe85v5en6rf77cxgxsj5", + "tb1qjm4rr97lxawx5csc3nu3rxhwnpxqe3s4uhwagu", + "tb1quhk94rhlsflc4wgxl9qzd6p6wszt30uxt4a0yj", + "tb1qdy4xwmgklqmyrfj336g4f54582zxtm2yhlge8l", + "tb1q0quewquwhlfgahhsdg0q3r5lmyzufrtp3fzme4", + "tb1qvsvr06h2phnwmzvwxrlkuzfu2ekhjpfpvguyct", + "tb1q25arh97ze37n6nk74n3js8ls8z7sva3f0d8pnl", + "tb1q008n3k9xjpcuyx4mlczn9jm2at90ts55yrtynq", + "tb1qcwytrrw3wugydlktsh6yvshlk7jwld38akp8l3", + "tb1qak6t2hcl3se6epvhlffaprvfjuf37xunnxq7c9", + "tb1qgdp7aa38x3p2kpn2s5486mkvvx2sktnmxkf47e", + "tb1qjpgepu2p6gyff9a92n2mwst4j2wjktra956lcg", + "tb1qcmq7v2zg0jjy5g47k90fqd0h7a4mcyp7f3ly6r", + "tb1q8m8pzk9gpjamgrw3y6y8xtfmw754nedldje5q5", + "tb1qgqfvg54gaads92a6dhwcgvvfjvnxdtq373guj5", + "tb1qahrz50yej9v7574q9are3urwyqsdcdddmjl9a6", + "tb1q02g5nde0heaed0y24rztkedh9nvswknw50h7fx", + "tb1qzyaz308030saay93zqma0at032vfqa9y0gfge3", + "tb1q3s4hkssd34tyxdlhafthv4muckjtgwuhltu37t", + "tb1qjv7a78ea0jp9d793d5ra7mtzkjzezwldz5zvr6", + "tb1qndaru6pfal030ev296uxwuulxrezaj0j70ceje", + "tb1qsd2c4xwg47hnngn2uqg66y5rxz2hp074u9rq6r", + "tb1qtqqddqrlg4xj3dzvjnea8wh5zy2fdf3jxl7qhs", + "tb1q9u3ufsm9ksql8utguap40ch8zpnw83fgzf4z97", + "tb1qlclgzsp2tktdl66xuk3je7ztztstxvjatly8wy", + "tb1q7mhmnxal53vtc5flh69nph44vah5j56eyesjx9" + ], + "receiving": [ + "tb1qq2tmmcngng78nllq2pvrkchcdukemtj5s6l0zu", + "tb1qm7ckcjsed98zhvhv3dr56a22w3fehlkxyh4wgd", + "tb1qwu2d7rjn9yxalg7cjw6phqzk77lf3fmsta8rhu", + "tb1qn7d2x7272lznt5hhk9s07q3cqnrqljnwa55w6c", + "tb1qusm48zmlzwr32csxdw4ar7atw260h22c8zq7jk", + "tb1qgcgk7j9kpt2mygmhmnu4zep79cd289t6aely7z", + "tb1qfu50fqxhfkl69n6urjzuhc5ndr96tuaxrh3an8", + "tb1qd7tjvgttaxzkszzh5ty4yq97r8wscgteejustc", + "tb1qchyc02y9mv4xths4je9puc4yzuxt8rfm26ef07", + "tb1q6k5h4cz6ra8nzhg90xm9wldvadgh0fpttfthcg", + "tb1qplsf242vay6vavy4eguef855fx3klmp9505g88", + "tb1qrdzfu6mlgrxpupd4syxrv77ncku89a0y0vd7f3", + "tb1qt339ksrha0n5a6lwpql778erkm272hxgamdc0u", + "tb1qtf9mwfv8ux0j90cwtx9nvz9l46jav40sak7ncg", + "tb1qf03zdjdnzxwztxs9d3g9ynsvvs5rmjhvtmln35", + "tb1qq0gdz0vz02ypa3cawlljstrx8cxydhvalcv8wc", + "tb1qkp86pkt75ds257snenp3q7vs29pf4g6cmhy2hw", + "tb1q3t0xcpmzreece8xdxq8k5aaxrt3r623tqldp8n", + "tb1qr7mjlxgc6at67tx0s8ypa5efx8clc47xh6yjqg", + "tb1qkneqe450eqxtpr3r5z8aw4234sjmpknm0gxsae", + "tb1qs087qkcawefutkkv8pg037t6txldk5szfntamj", + "tb1q069xqa4lej2tljmd8fcvvfedav54nmspvjnfs2", + "tb1q0j2gt4ap2s08cz5vzm5jg87fdeps7x8v4djgrm", + "tb1qs3j3j05rjefnjqf0mlpztszg8acz868wnxgz6j", + "tb1qh878je0rfut79fkudf4mkl8m4cn8uzfsluersy", + "tb1qvwwxv48k9vch5ddmf83g4fhd0tnx3mt8jp6rka", + "tb1q8evsj0vkzfak2y5qnqx4yf9lty462l7yfhegyd", + "tb1q99hkhlswfnj8r5wy2xu9a9m0vy8mvffwzhrx6n", + "tb1qh0csljush2tad6t0s4qgx4r5t0rzcw9729l7kx", + "tb1qad28sgvvrxjnxdnfjxcuepgzzhzlapgxcwuj0k", + "tb1qym6srwn87eu2sa5prkgd2lqva0nh0tr2xkeftp", + "tb1qucj6lx6eatgm5396fe539x53cp8zr0yzgclk6q" + ] + }, + "channels": {}, + "fiat_value": {}, + "forwarding_failures": {}, + "frozen_addresses": [ + "tb1qwu2d7rjn9yxalg7cjw6phqzk77lf3fmsta8rhu", + "tb1qd7tjvgttaxzkszzh5ty4yq97r8wscgteejustc", + "tb1qchyc02y9mv4xths4je9puc4yzuxt8rfm26ef07" + ], + "frozen_coins": { + "66f846809c2d45842f17e40fb6a696288c3274a66b0bf71107940f250978832e:0": true + }, + "imported_channel_backups": {}, + "invoices": {}, + "keystore": { + "derivation": "m/0h", + "pw_hash_version": 1, + "root_fingerprint": "535e473f", + "seed": "9dk", + "seed_type": "segwit", + "type": "bip32", + "xprv": "vprv9FrABTX8HFeSYL9aaMnLRcEkHBbJnBu9foDJaTvcF8SLvHx6uKqL8rtt7kTd66V4QPLfWPaCJMVZa3h9zuzLr7YFZd1uoEevqqyxp66oSbN", + "xpub": "vpub5UqWay427dCjkpE3gPKLnkBUqDRoBed1328uNrLDoTyKo6HFSs9agfDMy1VXbVtcuBVRiAZQsPPsPdu1Ge8m8qvNZPyzJ4ecPsf6U1ieW4x" + }, + "labels": { + "bcrt1q069xqa4lej2tljmd8fcvvfedav54nmspwm2y8r": "", + "bcrt1q07ulrxeuu45uqen0clqe85v5en6rf77cypla9a": "", + "bcrt1q0quewquwhlfgahhsdg0q3r5lmyzufrtpnqmkwu": "", + "bcrt1q3t0xcpmzreece8xdxq8k5aaxrt3r623tzk5vs6": "", + "bcrt1q6k5h4cz6ra8nzhg90xm9wldvadgh0fptfqj60p": "dshsdu", + "bcrt1qchyc02y9mv4xths4je9puc4yzuxt8rfmgnqych": "", + "bcrt1qd7tjvgttaxzkszzh5ty4yq97r8wscgtemm9au3": "", + "bcrt1qdy4xwmgklqmyrfj336g4f54582zxtm2y4k35sk": "", + "bcrt1qf03zdjdnzxwztxs9d3g9ynsvvs5rmjhvfjx7xa": "", + "bcrt1qfu50fqxhfkl69n6urjzuhc5ndr96tuaxp7gsyw": "", + "bcrt1qgcgk7j9kpt2mygmhmnu4zep79cd289t6lsxfft": "dsadsa", + "bcrt1qjm4rr97lxawx5csc3nu3rxhwnpxqe3s477hsl4": "", + "bcrt1qkneqe450eqxtpr3r5z8aw4234sjmpknmdpla2s": "", + "bcrt1qkp86pkt75ds257snenp3q7vs29pf4g6ce7a8q8": "", + "bcrt1qm7ckcjsed98zhvhv3dr56a22w3fehlkxx7vrly": "", + "bcrt1qn7d2x7272lznt5hhk9s07q3cqnrqljnwladrd3": "dsadsaddsdsa", + "bcrt1qplsf242vay6vavy4eguef855fx3klmp9kxd9sw": "a", + "bcrt1qq0gdz0vz02ypa3cawlljstrx8cxydhvaa342e3": "", + "bcrt1qq2tmmcngng78nllq2pvrkchcdukemtj5jnxz44": "", + "bcrt1qr7mjlxgc6at67tx0s8ypa5efx8clc47x4nalhp": "donke", + "bcrt1qrdzfu6mlgrxpupd4syxrv77ncku89a0yd95n7c": "", + "bcrt1qs087qkcawefutkkv8pg037t6txldk5szt6jsvm": "", + "bcrt1qt339ksrha0n5a6lwpql778erkm272hxglj54c4": "", + "bcrt1qtf9mwfv8ux0j90cwtx9nvz9l46jav40sll870p": "", + "bcrt1quhk94rhlsflc4wgxl9qzd6p6wszt30uxfuyznm": "", + "bcrt1qusm48zmlzwr32csxdw4ar7atw260h22c9ten9l": "hihi hi4655", + "bcrt1qvsvr06h2phnwmzvwxrlkuzfu2ekhjpfpwp9f0z": "", + "bcrt1qwu2d7rjn9yxalg7cjw6phqzk77lf3fmsf57wq4": "ddddd", + "c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38": "Open channel", + "ce0bea6a71ef5d8649e67a3235a1d1f5f8611e47725d54cb16d5f9ebabd92c8a": "Miner fee for sending to BTC address", + "d0ae4c76f769dccfa0187633839d704ae803e810fd592e4f2d14178aaacc94d6:1": "JOHN", + "d8a07501eb18302f5b4fa0535c1a7a71eee058de1013f0c2e2ceb53ca50bb99c": "dsadsa", + "eaa745cef67b98bcabd604598406cd782cfafc99a419c2c7a8a1dde163cf7dab": "44", + "fdf0d67d412785e337b2883cf56784345e215513331d966c5ee875ff6d48b38d": "dsjaiodsa", + "tb1qcmq7v2zg0jjy5g47k90fqd0h7a4mcyp7f3ly6r": "at some point there was some change here", + "tb1qwu2d7rjn9yxalg7cjw6phqzk77lf3fmsta8rhu": "old address 95645" + }, + "lightning_payments": {}, + "lightning_preimages": {}, + "lightning_xprv": "vprv9HAix419EKycrbTEJxT1zKCLURqA9LWRHeohrddxQ35QuvfH8fqMurdF4mseJ5oytUCJH5VvhEXSmCTs5SaoT7jEr2hcy7e8uCFLJtuDSme", + "notes_text": "", + "num_parents": { + "59a9ff5fa62586f102b92504584f52e47f4ca0d5af061e99a0a3023fa70a70e2": 1, + "66f846809c2d45842f17e40fb6a696288c3274a66b0bf71107940f250978832e": 9, + "778b01899d5ed48df03e406bc5babd1fdc8f1be4b7e5b9d20dd8caf24dd66ff4": 3 + }, + "onchain_channel_backups": {}, + "payment_requests": {}, + "prevouts_by_scripthash": { + "08cf1ff744d66d4699c5654e162654bacc04558035de45a2f613abe5329d4286": { + "03bec7a53ce7c856046f306738a14a976acff34f12a1e3f61f31fa4284486f7d:1": 500000 + }, + "0dbcae29aaa4fde8a0542b606d89c6ae46350f0fc7a3a957337f4d7ef03b5dd3": { + "c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38:1": 99810 + }, + "0eaed6a38625ac78bde7585d061872cb55609f8d05c2a4b1f4230f1ea0f78a90": { + "972d16040c71b6660b52a0ad06ddb17aa7097921643bab526ad2e00780004535:1": 7596590 + }, + "102dcf626801074a661c851fcc3536c2101eaa2ec03ed03a11b9bb9c19741237": { + "2042e1e197e0b21e83de0ee8a92abf5e1fea617cf143dc584a743df075bc6269:0": 99811 + }, + "143f42f87521ff360780106377c98d2c27599b6028b7f2858fc71636f003eb9e": { + "b424362ec4eb39eb84f0bea8e00ffd8382d5dde29092bd1e268b9a027dc5225c:1": 8798 + }, + "241de66b34b781feb863a601c2d49a05ebea8ad0c0f6760ea73e502fafba5bf7": { + "03eb4932abf231999f5a698a28f70cfe6b1a2975be0310e348a9898434efb8d3:1": 9598000 + }, + "244501478da81fb6a3ffd88d222708dcb6194931136947873cc0fccc55f24661": { + "23b5490a6b9723a96f8d1170cf0ab8d2e961d775cff0151540400f2964ccbd7f:0": 113600 + }, + "2a06d09e67dc4c49523796cf899d2e4002429d9e1280456264b79d1c7caac727": { + "03bec7a53ce7c856046f306738a14a976acff34f12a1e3f61f31fa4284486f7d:0": 381400 + }, + "3248021fc00b3f196a362dc9f940a25d488a040498a2d08b7a60aee9ac18626c": { + "4dbdf2810f130653d1eb17ce3645d0362e4ef41ffbd521aad1f9294c95baf76e:0": 3190000 + }, + "327923b903a21b12aa85f694c8c0de952805a78e4a0164631850c5dc607360df": { + "03eb4932abf231999f5a698a28f70cfe6b1a2975be0310e348a9898434efb8d3:0": 200000 + }, + "3641e700a31ec0763da7ecdab2809abd7e6c7668414902ea13958f12c5ca9ed1": { + "25ddebe4b7e7caf7b175762ddc4cbb5d704f27520a997b7672057f2455b2fcea:0": 1387695 + }, + "3658a8d8b2614ec7814d2973305e174a12f9d4e9cacf84f81b09cf2fa90d6fe2": { + "10a2a7695a5b6ba857b10d04ad047b968656c6d84b58ea226dc92c150cb73b2f:0": 99889 + }, + "374127d2991c7bf24c117cc4d73058d2f56987ce4591930271ae5b87599a40a2": { + "59a9ff5fa62586f102b92504584f52e47f4ca0d5af061e99a0a3023fa70a70e2:1": 654165 + }, + "385645f17d282282564fc60e25d026d1258b7c23074ce18127577fc6e1e5ede1": { + "94fce9de7e82741cc982936ad9f7900372b80be8100cb80e49f5d78c2954fb69:1": 404711 + }, + "39c770e551fa2d861b8bf9be640933abbde97b6fd3d5b86e21e815b493745448": { + "7726939a1246c0b99c2f30cfa21504af5b458755231b2cdc1fa1e9af2d9d70ba:1": 8597295 + }, + "3b64546ad58f7d7d5632bd6ecc6bd8d69bfab794674713dac2a4a260406aa792": { + "87dbf5a7a089f1e071559cdfd3e4bddb841cdd0032ffeec328952abf76fe3661:0": 100000 + }, + "3fea64e654f5b0ebf560cd7def07419823d048251e5cba13b9bc1d19fc5570f9": { + "6ff55c62470cf51cbc577e8efac5028e06e41912a39356ba576ab7b4d7f708ce:0": 403476 + }, + "4940877cf9e1587732a4379592b0490273e92379e93e7a024656f02690a7b955": { + "778b01899d5ed48df03e406bc5babd1fdc8f1be4b7e5b9d20dd8caf24dd66ff4:0": 5100 + }, + "513018b2d303921ccf5194decd9e9c1f2a7f93f687ef301254fcd3a20dea0c71": { + "97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe:1": 302547 + }, + "54f31d22030e3b6ff2ffc9d6fef4a9184ff9bd37285c99bd405b662a4fb4b99c": { + "efca2a86e0584d7e359e5bc9ac954c6ed7e5c136ad6d561cba662001221df23c:1": 1148200 + }, + "5dea1555deacfbc9698f32073bb26ac2b3a6462752816a727af8e2e288d80857": { + "3407144b10e0c7f60a88e9f3adcc7e732c83e92fa17a923f523b8acc50718c2f:0": 17898379 + }, + "6abaf5808033282b0d182f1393cd89d8e73245b317ee42896449206f6d67f009": { + "b424362ec4eb39eb84f0bea8e00ffd8382d5dde29092bd1e268b9a027dc5225c:0": 1000 + }, + "6c9528c720baace52d4daf3af546fc33be00b981618807a99e088d148e3569e9": { + "d57bb7a379c7eb8c86e34d595722ca4f389394b4497c1f0028971e0a8e75c8e1:0": 404573 + }, + "6d6a947e3b89b4901842539aa1059a8f44dafe5029c39d1bbad63bfdbf98d4f4": { + "024464eae9858f079d2f49bc3db28f92237c63fe950c8ddc6c9b18d1855fa32c:0": 10000000 + }, + "76bbdc72fdc74fbb0b049d21b0f80045f2a227cde1cd57a00925a437638984b5": { + "ba48bce5cde8a4eacb4f1273c462101c06305e82d6ee89a113cd6b068178651f:0": 3099663 + }, + "80c018b3fbc6a3d64e21c8a53a4f5b7eb39cb62998272bd6e0f5a5a5b5947bf2": { + "737857196825fa2e600bac822c866f10e5f484b0f193cff1fc3d38b91884c7a6:1": 6595885 + }, + "846dc81dba7a3df5a44c955bc73afc9b51ab7ef56e3170b835ae9d37967e1323": { + "41284afa51eccc94d10fac25eaa59d1fa1afdc3e27bae2b16a64399fe0f1be3c:1": 6394885 + }, + "969c34a4c63b26dada2c5042db47236fe4bf0d1b16736bc644596b9e1a0863f0": { + "41284afa51eccc94d10fac25eaa59d1fa1afdc3e27bae2b16a64399fe0f1be3c:0": 200000 + }, + "9d905269b1d499af60d605b3a3c16afe21eea08dec1b2013f4707bde14b40c97": { + "4029e471c030066ecca20621cc2917cade0fd44c5cbcb6f5299c086f25ec4b6c:0": 14695224 + }, + "a8445ee64c4775b070c64df9b8006788787da9734502ede664c0d17db65f9efc": { + "59a9ff5fa62586f102b92504584f52e47f4ca0d5af061e99a0a3023fa70a70e2:0": 627994 + }, + "af4144146cb7abcd1dd2c812bdcc9661b0611a6a70f682167bb683ea0e91b1de": { + "23b5490a6b9723a96f8d1170cf0ab8d2e961d775cff0151540400f2964ccbd7f:1": 135000 + }, + "b77873e2123e43f37054e972ae4685ff203b9d35c14989da321d242f8a0d0799": { + "c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38:2": 400000 + }, + "b81fe544d6d281c5aa695c99dba16ba1fd2bd66f5d342ace52ead7c168b36e2f": { + "94fce9de7e82741cc982936ad9f7900372b80be8100cb80e49f5d78c2954fb69:0": 0 + }, + "ba80fd5d5ce4a19c5b2750ec73e621dcc46637090dbae62d3f5bc5bb68565360": { + "778b01899d5ed48df03e406bc5babd1fdc8f1be4b7e5b9d20dd8caf24dd66ff4:1": 94500 + }, + "bc42e29c85e9d6096a1fd59582e91eccd045acaa7683326ea4dc10d579c0cbc4": { + "97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe:0": 0 + }, + "ceb18da9abab996544f4120b0b7fc221581fdf81a7ac4fc1d311ff9c3dd8571a": { + "972d16040c71b6660b52a0ad06ddb17aa7097921643bab526ad2e00780004535:0": 1000000 + }, + "cf1356579698ef6bad07be8da12e82f69687505dddffd1f31f70a626347e69bb": { + "0ef1c91636d20331acc2759d0f0335925cd899e270efdd2598e9449c66649d39:1": 9799000 + }, + "d20f43d6c9c52327eb9ac64c2b9f4b2b54ef197a485936a2bb46b2a68b37168e": { + "8c09f8984e231fcaebdda00b2597cfde3d825a26dad2c5478c78ccabbac1f28b:1": 988000 + }, + "d47dd057642f30739fe81d4c56edcbd658ba87445ecf763edfb508b4c615581c": { + "737857196825fa2e600bac822c866f10e5f484b0f193cff1fc3d38b91884c7a6:0": 1000000 + }, + "d5f4d828cfea4006e972fab9e4c23176761621eb98f8b35a4b4a12671ee04453": { + "b234c4f252cd193377bc6cf931ff1ef6a398e3a434e43b22cdb457a8213d749b:0": 3199293 + }, + "dadfddf343dd5eefc2dedbea6f4ad1f48e85852e60d739593ea7d821e59c0893": { + "c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38:0": 0 + }, + "e4dbc5351a813f40d2a14dad58b8337eb7cca5071d54978f0aaae195bcd3dd20": { + "0ef1c91636d20331acc2759d0f0335925cd899e270efdd2598e9449c66649d39:0": 200000 + }, + "e690891dd2a7bfa84bb93d56a9aef8fb6b4e8cdca9014b1bcfb16ab69d05f5f0": { + "66f846809c2d45842f17e40fb6a696288c3274a66b0bf71107940f250978832e:0": 302364 + }, + "e7f95bc81c2de2279d0193d4bc6f5950fa0df01be599f5aeb4ffb63b29d7d1f7": { + "87dbf5a7a089f1e071559cdfd3e4bddb841cdd0032ffeec328952abf76fe3661:1": 302700 + }, + "eac5af8eeb1ff53507f99263ccfc8b8b2a85bf947634293a33c5eb70178d5875": { + "30bc3855a6837562eb6dffbb3190e71279e2df2a04f2598780c9df7f290706e0:1": 3194603 + }, + "ebc0e58e3591ec18b2bb8cce5e58dabfa7d121e026ce0683a02ca4990505aafb": { + "4dbdf2810f130653d1eb17ce3645d0362e4ef41ffbd521aad1f9294c95baf76e:1": 3204744 + }, + "f04bf26fc16a27abaa2f10540da7a24e369e6ec408f2d901202a7c0cd4a6112d": { + "30bc3855a6837562eb6dffbb3190e71279e2df2a04f2598780c9df7f290706e0:0": 10000 + }, + "f38506e05a767c8f162b2271551c63a5ddae94e6eb395e441993de38e921fea6": { + "8c09f8984e231fcaebdda00b2597cfde3d825a26dad2c5478c78ccabbac1f28b:0": 160000, + "efca2a86e0584d7e359e5bc9ac954c6ed7e5c136ad6d561cba662001221df23c:0": 110000 + }, + "f58156a910255e946f7cf2c7daf08ce3faeaf0deda0c9f300679ad9517ef7a68": { + "7726939a1246c0b99c2f30cfa21504af5b458755231b2cdc1fa1e9af2d9d70ba:0": 1000000 + }, + "faf2c38001ff18046f939975176e9ea069090ed1ef4959625f24d31b3368d68f": { + "25ddebe4b7e7caf7b175762ddc4cbb5d704f27520a997b7672057f2455b2fcea:1": 100000 + }, + "fe0e7e43ca025de6ec189c121b245ac207f40177385e829fa725dff68e24e2ec": { + "024464eae9858f079d2f49bc3db28f92237c63fe950c8ddc6c9b18d1855fa32c:1": 15313760 + } + }, + "qt-console-history": [], + "reserved_addresses": [ + "tb1qcmq7v2zg0jjy5g47k90fqd0h7a4mcyp7f3ly6r", + "tb1qgqfvg54gaads92a6dhwcgvvfjvnxdtq373guj5" + ], + "seed_version": 57, + "spent_outpoints": { + "024464eae9858f079d2f49bc3db28f92237c63fe950c8ddc6c9b18d1855fa32c": { + "0": "0ef1c91636d20331acc2759d0f0335925cd899e270efdd2598e9449c66649d39" + }, + "03bec7a53ce7c856046f306738a14a976acff34f12a1e3f61f31fa4284486f7d": { + "1": "c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38" + }, + "03eb4932abf231999f5a698a28f70cfe6b1a2975be0310e348a9898434efb8d3": { + "1": "7726939a1246c0b99c2f30cfa21504af5b458755231b2cdc1fa1e9af2d9d70ba" + }, + "0ef1c91636d20331acc2759d0f0335925cd899e270efdd2598e9449c66649d39": { + "1": "03eb4932abf231999f5a698a28f70cfe6b1a2975be0310e348a9898434efb8d3" + }, + "2042e1e197e0b21e83de0ee8a92abf5e1fea617cf143dc584a743df075bc6269": { + "0": "b234c4f252cd193377bc6cf931ff1ef6a398e3a434e43b22cdb457a8213d749b" + }, + "23b5490a6b9723a96f8d1170cf0ab8d2e961d775cff0151540400f2964ccbd7f": { + "1": "94fce9de7e82741cc982936ad9f7900372b80be8100cb80e49f5d78c2954fb69" + }, + "25ddebe4b7e7caf7b175762ddc4cbb5d704f27520a997b7672057f2455b2fcea": { + "1": "10a2a7695a5b6ba857b10d04ad047b968656c6d84b58ea226dc92c150cb73b2f" + }, + "30bc3855a6837562eb6dffbb3190e71279e2df2a04f2598780c9df7f290706e0": { + "0": "b424362ec4eb39eb84f0bea8e00ffd8382d5dde29092bd1e268b9a027dc5225c", + "1": "3407144b10e0c7f60a88e9f3adcc7e732c83e92fa17a923f523b8acc50718c2f" + }, + "4029e471c030066ecca20621cc2917cade0fd44c5cbcb6f5299c086f25ec4b6c": { + "0": "3407144b10e0c7f60a88e9f3adcc7e732c83e92fa17a923f523b8acc50718c2f" + }, + "41284afa51eccc94d10fac25eaa59d1fa1afdc3e27bae2b16a64399fe0f1be3c": { + "1": "4dbdf2810f130653d1eb17ce3645d0362e4ef41ffbd521aad1f9294c95baf76e" + }, + "4a87be118635dc782e9dd8be26a5be0fa6a1c5267b9db8303f0af85fbf6173ba": { + "1": "ba48bce5cde8a4eacb4f1273c462101c06305e82d6ee89a113cd6b068178651f" + }, + "4dbdf2810f130653d1eb17ce3645d0362e4ef41ffbd521aad1f9294c95baf76e": { + "0": "4029e471c030066ecca20621cc2917cade0fd44c5cbcb6f5299c086f25ec4b6c", + "1": "30bc3855a6837562eb6dffbb3190e71279e2df2a04f2598780c9df7f290706e0" + }, + "60850b8952f6bf52f1fca76e051888637cb38c1c8a85301c7fa5359f76d1703d": { + "1": "4029e471c030066ecca20621cc2917cade0fd44c5cbcb6f5299c086f25ec4b6c" + }, + "6ff55c62470cf51cbc577e8efac5028e06e41912a39356ba576ab7b4d7f708ce": { + "0": "87dbf5a7a089f1e071559cdfd3e4bddb841cdd0032ffeec328952abf76fe3661" + }, + "737857196825fa2e600bac822c866f10e5f484b0f193cff1fc3d38b91884c7a6": { + "1": "41284afa51eccc94d10fac25eaa59d1fa1afdc3e27bae2b16a64399fe0f1be3c" + }, + "7726939a1246c0b99c2f30cfa21504af5b458755231b2cdc1fa1e9af2d9d70ba": { + "1": "972d16040c71b6660b52a0ad06ddb17aa7097921643bab526ad2e00780004535" + }, + "820fb2b07ba74b4ac3974d2ccaaf8096290614fe7ec83266ae028d6d857b1e41": { + "1": "23b5490a6b9723a96f8d1170cf0ab8d2e961d775cff0151540400f2964ccbd7f" + }, + "87dbf5a7a089f1e071559cdfd3e4bddb841cdd0032ffeec328952abf76fe3661": { + "1": "97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe" + }, + "8aed48c4e4ab646c1c5692487c2f1a4414221fef1805463fb21f7549b41a0ff0": { + "1": "2042e1e197e0b21e83de0ee8a92abf5e1fea617cf143dc584a743df075bc6269" + }, + "8c09f8984e231fcaebdda00b2597cfde3d825a26dad2c5478c78ccabbac1f28b": { + "0": "94fce9de7e82741cc982936ad9f7900372b80be8100cb80e49f5d78c2954fb69" + }, + "94fce9de7e82741cc982936ad9f7900372b80be8100cb80e49f5d78c2954fb69": { + "1": "d57bb7a379c7eb8c86e34d595722ca4f389394b4497c1f0028971e0a8e75c8e1" + }, + "972d16040c71b6660b52a0ad06ddb17aa7097921643bab526ad2e00780004535": { + "1": "737857196825fa2e600bac822c866f10e5f484b0f193cff1fc3d38b91884c7a6" + }, + "97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe": { + "1": "66f846809c2d45842f17e40fb6a696288c3274a66b0bf71107940f250978832e" + }, + "983bbe3d0593b37996821733e53417fd06bcf559adcacbf339dd7729cb19673d": { + "0": "59a9ff5fa62586f102b92504584f52e47f4ca0d5af061e99a0a3023fa70a70e2" + }, + "a3ee0b514e9af4735097072dd06beba9b9ba02a072e242d1cfe0fce6bfad4bad": { + "1": "25ddebe4b7e7caf7b175762ddc4cbb5d704f27520a997b7672057f2455b2fcea" + }, + "aff92372faa2717819781f1b9a8771abd3fa20f5b504b682d264cc9575acca19": { + "0": "03bec7a53ce7c856046f306738a14a976acff34f12a1e3f61f31fa4284486f7d" + }, + "b30af3bfa76e031e5aea6c2c5cb7cbcd57a145697b74e820ba2a3a5858bdc98c": { + "1": "efca2a86e0584d7e359e5bc9ac954c6ed7e5c136ad6d561cba662001221df23c" + }, + "b424362ec4eb39eb84f0bea8e00ffd8382d5dde29092bd1e268b9a027dc5225c": { + "1": "3407144b10e0c7f60a88e9f3adcc7e732c83e92fa17a923f523b8acc50718c2f" + }, + "ba48bce5cde8a4eacb4f1273c462101c06305e82d6ee89a113cd6b068178651f": { + "0": "b234c4f252cd193377bc6cf931ff1ef6a398e3a434e43b22cdb457a8213d749b" + }, + "c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38": { + "1": "778b01899d5ed48df03e406bc5babd1fdc8f1be4b7e5b9d20dd8caf24dd66ff4" + }, + "ca675c39c9f64bce9ede8f551d2b5871da2aa4747322a2f75878ad76b0b645d9": { + "1": "ba48bce5cde8a4eacb4f1273c462101c06305e82d6ee89a113cd6b068178651f" + }, + "d1d575990837cfb5b93d92ba4e9b0454b385fe3b865b98088d68b1778b8855e8": { + "0": "024464eae9858f079d2f49bc3db28f92237c63fe950c8ddc6c9b18d1855fa32c" + }, + "d57bb7a379c7eb8c86e34d595722ca4f389394b4497c1f0028971e0a8e75c8e1": { + "0": "6ff55c62470cf51cbc577e8efac5028e06e41912a39356ba576ab7b4d7f708ce" + }, + "efca2a86e0584d7e359e5bc9ac954c6ed7e5c136ad6d561cba662001221df23c": { + "0": "94fce9de7e82741cc982936ad9f7900372b80be8100cb80e49f5d78c2954fb69", + "1": "8c09f8984e231fcaebdda00b2597cfde3d825a26dad2c5478c78ccabbac1f28b" + }, + "f2dbe14e9f10e068c6bd4a534d8c8a2945c6bcd69cc85b4d5eb37aca4d3b5384": { + "0": "4029e471c030066ecca20621cc2917cade0fd44c5cbcb6f5299c086f25ec4b6c" + } + }, + "stored_height": 2579588, + "submarine_swaps": {}, + "transactions": { + "024464eae9858f079d2f49bc3db28f92237c63fe950c8ddc6c9b18d1855fa32c": "01000000000101e855888b77b1688d08985b863bfe85b354049b4eba923db9b5cf37089975d5d10000000000fdffffff0280969800000000001600140297bde2689a3c79ffe050583b62f86f2d9dae5460abe9000000000016001472df47551b6e7e0c8428814d2e572bc5ac773dda024730440220383efa2f0f5b87f8ce5d6b6eaf48cba03bf522b23fbb23b2ac54ff9d9a8f6a8802206f67d1f909f3c7a22ac0308ac4c19853ffca3a9317e1d7e0c88cc3a86853aaac0121035061949222555a0df490978fe6e7ebbaa96332ecb5c266918fd800c0eef736e7358d1400", + "03bec7a53ce7c856046f306738a14a976acff34f12a1e3f61f31fa4284486f7d": "0200000000010119caac7595cc64d282b604b5f520fad3ab71879a1b1f78197871a2fa7223f9af0000000000fdffffff02d8d105000000000016001440bb0029ce2482a73fff367f0f7ea2390d7a1c5f20a10700000000001600140fe095554ce934ceb095ca39949e9449a36fec25024730440220582ac0428398fb55846cb570e26fd78566b4bb67febfc3c835057e74f1e03ee0022022aa084ba38768a4ce451face547fd9217684e5fefebc29853ffc38ee9eed0ee0121031284572b5ebec99319a87ec710d162a9d21e0afa2a89f8a5cbe290f95369a22e7d5c2700", + "03eb4932abf231999f5a698a28f70cfe6b1a2975be0310e348a9898434efb8d3": "01000000000101399d64669c44e99825ddef70e299d85c9235030f9d75c2ac3103d23616c9f10e0100000000feffffff02400d0300000000002200205208fb0ad888e82e4452ab6cbb06a51c09ca36c6f2a86e5e81884de1860cf400307492000000000016001496ea3197df375c6a62188cf9119aee984c0cc6150247304402203e535dd045e4e7d062af6d11888643ddce48649e123abecf6a42f6048f892f18022076b9e21191096e9c1800904e24fce6758a7d93e8795d35c161a334b83e1286860121030644479c1b8c106e2f0a495fe18dc45572b3413bda2db919c1fcf0f77f43241ca18d1400", + "0ef1c91636d20331acc2759d0f0335925cd899e270efdd2598e9449c66649d39": "010000000001012ca35f85d1189b6cdc8d0c95fe637c23928fb23dbc492f9d078f85e9ea6444020000000000feffffff02400d0300000000002200203bf21a5a90ce63776ecf8216ad92b0afc831c1d5fd1d213d29f916181aac74a958859500000000001600147fb9f19b3ce569c0666fc7c193d194ccf434fbd80247304402200571cebd2f7845524862e773661d08d3e14b0a1fd109db2f325ae405e25dd83d02200a92d4ecfe49919389eb9754cc60b5af058a167690b876dad6f09f15f1b69b19012102a0507c8bb3d96dfd7731bafb0ae30e6ed10bbadd6a9f9f88eaf0602b9cc99adc8c8d1400", + "10a2a7695a5b6ba857b10d04ad047b968656c6d84b58ea226dc92c150cb73b2f": "02000000000101eafcb255247f0572767b990a52274f705dbb4cdc2d7675b1f7cae7b7e4ebdd250100000000fdffffff01318601000000000017a914bff6c01dc206f8fb7062e314e378e9aeae017a5c870247304402203cd1869eb6ce787f4f27e471b58acfd288d57ca55c8c996aba89d1865647408902200f57bf52775d397b4f02ac584bf6bab96335e89a2b2bfa0bb8f4aa98ee62dd66012102fb265cce2019e555fe23fb45e4cbc3d966bb399a52b2e93b808ad7961b426de15ee01c00", + "2042e1e197e0b21e83de0ee8a92abf5e1fea617cf143dc584a743df075bc6269": "0200000001f00f1ab449751fb23f460518ef1f2214441a2f7c4892561c6c64abe4c448ed8a010000006b48304502210092443dd340a1caa7791351324aee87b35b6057a7d9237fc95e1f1d029dd2fe2402200cd3cd13551313d186726bd6beb6d386f79d0a978972e5f923303df1700c957d0121030638cdea69f69ff45ed0d97d8a5c41ce7bbe0d774be898143f7a73cb73ad72b0fdffffff01e38501000000000016001446116f48b60ad5b22377dcf951643e2e1aa3957ac3271800", + "23b5490a6b9723a96f8d1170cf0ab8d2e961d775cff0151540400f2964ccbd7f": "02000000000101411e7b856d8d02ae6632c87efe1406299680afca2c4d97c34a4ba77bb0b20f820100000000feffffff02c0bb01000000000016001455d5618cfc2d731d7c3d80b92bd2c87835c1234c580f0200000000001600144f28f480d74dbfa2cf5c1c85cbe29368cba5f3a60247304402201f857d44cbf474275010d4fc996bf78e15b5631ea6557f842c3a3aebdd7fc8be02205e21cabbf5247ad08c62bea14adcde8f0bad722de3473435f500a8fba7ea6c900121031abaf20c4146bdee1ad87cbc56c892cbe93a92fd11614d379f80be5f60b1f3d50aaf2400", + "25ddebe4b7e7caf7b175762ddc4cbb5d704f27520a997b7672057f2455b2fcea": "02000000000101ad4badbfe6fce0cfd142e272a002bab9a9eb6bd02d07975073f49a4e510beea301000000171600142d886eecf1d36b2a317f74ae3ae0088944017d27feffffff02af2c150000000000160014e866546b96bddfa46ae298c8363527531e03573ba086010000000000160014e437538b7f13871562066babd1fbab72b4fba9580247304402202ce3a50013652650b1b93b89aed5d36bab4adc13ab7054aedb908735b0d2923d02202a4a5bd091f88a186b2d9e92ca99f3129c6cd15861eba06d4b6677601fa1fc9a012102c821655842477341331fb3faa44a04547c5c039b8f44f48b1b7c45266f545ea7d0181b00", + "30bc3855a6837562eb6dffbb3190e71279e2df2a04f2598780c9df7f290706e0": "010000000001016ef7ba954c29f9d1aa21d5fb1ff44e2e36d04536ce17ebd15306130f81f2bd4d0100000000fdffffff0210270000000000001600149f9aa3795e57c535d2f7b160ff023804c60fca6eebbe3000000000001600147bcf38d8a69071c21abbfe0532cb6aeacaf5c2940248304502210086c464555baeb19f60d9a8284bbca0c77009e6864e751bd4ca87105d0f9d1acf02207b6d4f3d5142af6fe7636e60967aac294155bdea469bd5c0ac18612e8ed8456d012103cd68ad8bddacdc82a39a82efd7a045f2d9513a9de31049286b696c87a8efbe8767341600", + "3407144b10e0c7f60a88e9f3adcc7e732c83e92fa17a923f523b8acc50718c2f": "02000000000103e00607297fdfc9808759f2042adfe27912e79031bbff6deb627583a65538bc300100000000fdffffff6c4bec256f089c29f5b6bc5c4cd40fdeca1729cc2106a2cc6e0630c071e429400000000000fdffffff5c22c57d029a8b261ebd9290e2ddd58283fd0fe0a8bef084eb39ebc42e3624b40100000000fdffffff018b1b110100000000160014ef8dfaec6a39b7efc77828fc3840a26da11bfe7502473044022073382b79c486c708f16a1e53c8327060c193ba8db376df3c40abd7b2cf0c39c7022028f210de787f1bdc9cf0060e612b1d71b7f36b1f86956b43a53d516f1aeb2657012103ff475c0005da39a068516cbbfe5f0b5fcc126aba81e405728adab8bdd572d9a3024730440220243989034479fd8de480207d59aec4106ff2277f11f7a982509d309ae90e108f02203282db9a7f38d43c2672abe64f0bfd5d28f60b053fcb1b87671d5dbaa987e12301210242c23a20435cd52192581537b2017288b8cdf3fef349100c11a125d9529017e10247304402207712fbcfac6e0011f404945aa1480332da34eeb5ac47623e341a4f9692caa49802202c6ec5e739aa52f675ffefbf4c057ab6b4a357f66902d96b6af5327206c9367f012103834e1e34194508e3fc7db19d3c6964149efd58f8c67548cc765c3284cad24d6707ee1700", + "4029e471c030066ecca20621cc2917cade0fd44c5cbcb6f5299c086f25ec4b6c": "010000000001036ef7ba954c29f9d1aa21d5fb1ff44e2e36d04536ce17ebd15306130f81f2bd4d0000000000fdffffff3d70d1769f35a57f1c30858a1c8cb37c638818056ea7fcf152bff652890b85600100000000fdffffff84533b4dca7ab35e4d5bc89cd6bcc645298a8c4d534abdc668e0109f4ee1dbf20000000000fdffffff01383be00000000000160014dfb16c4a19694e2bb2ec8b474d754a74539bfec602483045022100dfbf8dde6a9070a13a808d317e4fb6d35906a3b0ba4f53543ddababfa0df631e02204bc0b692c63c500d517ee96df74ac13e0d680898d761895ea80ed7458031dbc5012102462df8e9cd6196186dd4d5444b6a4c7acd006a7d4c59b8b16e7517828fb8cfc302483045022100b59c985153d9a2a27bdac393ec238dfca3dc2dd63bb181c778ad41568ae2017802204b3c71b2e90dbfb054f432ea95c6fefe29d8c2c36ee725a7916b0241ba6409130121030ea1026664c47391f82ff1bb693b1bfc959ec3b3cbc80a8dfd31fc637cad765702483045022100d056da28e2cd316f5e86aaa56298f0f5d1a313cbbad95f65712b9a4a0be1448f022059d8719fece4677da694a7ca1b61a9ee386eb108e58bdf648dbde0db704ad40f01210208b66c5067eb109735768b92a2800aa6844ae7575911298e0fd6ced709987d04a4361600", + "41284afa51eccc94d10fac25eaa59d1fa1afdc3e27bae2b16a64399fe0f1be3c": "01000000000101a6c78418b9383dfcf1cf93f1b084f4e5106f862c82ac0b602efa2568195778730100000000feffffff02400d03000000000022002005b05ff139f7a25173a8ada54eaf24c7d9764409c829ca792726fb7aff1eb8360594610000000000160014641837eaea0de6ed898e30ff6e093c566d790521024730440220736f7035d58d08d3e7be11edcdbf3d91cbd86cda16b19ec48b12e5257c21c6bb02202987a4976bbc5e759ba8d5dc7dedc56eadb449966c0e2520f9a1392c2342cb6e0121039c25ce87cddec7e7b0a7a4541047cba7554d6d4093965869787ad2eca4be8a55a1951500", + "4dbdf2810f130653d1eb17ce3645d0362e4ef41ffbd521aad1f9294c95baf76e": "010000000001013cbef1e09f39646ab1e2ba273edcafa11f9da5ea25ac0fd194ccec51fa4a28410100000000fdffffff02f0ac3000000000001600144616df751811e5114e045a6b7cad889463555c0488e6300000000000160014553a3b97c2cc7d3d4edeace3281ff038bd06762902473044022070000f29d154f419ebe2dc740c80eb89dc0d571d96d0469f629761d7fb13cad9022008c94670cb3e682a5f076c4cd6634b8ec32168cd648c3f6c89597c105cf5368a0121030bd1ee5d34698af47ee3ceb954b3009ecc0614bb64b54d669adfb233a36d2ae8d3311600", + "59a9ff5fa62586f102b92504584f52e47f4ca0d5af061e99a0a3023fa70a70e2": "020000000001013d6719cb2977dd39f3cbcaad59f5bc06fd1734e53317829679b393053dbe3b980000000000fdffffff021a950900000000001600141f81d08aedb71cd788380d642b755ecb961a1edc55fb0900000000001600141b449e6b7f40cc1e05b5810c367bd3c5b872f5e402473044022064462613a5a80a83ed4badc54ff4e6e85e06781a55ebccc1eb253dc865c478cb02204c4bd331205a9cf90094ca60ba8f1367f5246b92595a361b40fafce522ba4dbf01210251700930db16fbda3de7141685dfae05fda67b234ff7c4861562a3ce07f592497f5c2700", + "66f846809c2d45842f17e40fb6a696288c3274a66b0bf71107940f250978832e": "02000000000101fe13acd33700e2dc135c833b790eb75e1ed71429773b36192315f3fede7f76970100000000aa771980011c9d040000000000160014c6c1e628487ca44a22beb15e9035f7f76bbc103e04004730440220196ae31431605048d0af586af779d903c813abada54fcf35877b2b4569ee27e502204fa7c6a675369fe1ccc3d5d2e658fc4be7fb480d9f3bd6d1049a7c67264189c401483045022100bd1fdd55c6db44fff54bebf3acd649a5e135254f187561761c150521555e3713022042035709c099da8956af5e6806726170f0104cf5f145ec4632a46d806f3c447601475221022aab353e6b25226a3cb38e81dafe8fca3416642847dab7d38aaafe00eb8294ee2103a5bf1debe9c97bb56c7a4316b45bf2f8d35612db2964253ae8efd0f7ca8e1f7052ae1388d520", + "6ff55c62470cf51cbc577e8efac5028e06e41912a39356ba576ab7b4d7f708ce": "02000000000101e1c8758e0a1e9728001f7c49b49493384fca2257594de3868cebc779a3b77bd50000000000fdffffff0114280600000000001600144343eef6273442ab066a852a7d6ecc61950b2e7b0247304402205f8a63b3c273abd010a9c095d9591767b6a628452c93acb30809a7989fc3bfa602204b5f812b1c7151e279d58dffa29942552dcb2fad9d14dc39790785fa158b6adf012102c0c89554ca035a1d840eb87ae679146d5cca0fac2a57d078811894ed9e3a4b5ab4da2400", + "737857196825fa2e600bac822c866f10e5f484b0f193cff1fc3d38b91884c7a6": "010000000001013545008007e0d26a52ab3b64217909a77ab1dd06ada0520b66b6710c04162d970100000000fdffffff0240420f00000000001600142e61106d61d1ad4f1e44e79c8d5bc5907d48d3552da5640000000000160014783997038ebfd28edef06a1e088e9fd905c48d610247304402207878752ca1cc79758db2959062b2fd0ae0682e83fb258385a340d636c9b85810022049a0275f3e8dde8ddaffaab5298c44195c1066ebb414fc3d942b57728058ff6a012102be2e344c2f49afa8d734a2f79a09827b038d2f8f6bfa77d5a2e077c22a3fd80aa6941500", + "7726939a1246c0b99c2f30cfa21504af5b458755231b2cdc1fa1e9af2d9d70ba": "01000000000101d3b8ef348489a948e31003be75291a6bfe0cf7288a695a9f9931f2ab3249eb030100000000fdffffff0240420f0000000000160014eb23be36ca7b46f3508a042a121b17f36384c4222f2f830000000000160014e5ec5a8eff827f8ab906f94026e83a7404b8bf8602483045022100c61749c026980911afde50838cb72d3484cdd9b9f419d8e6c7647574a52f0a070220471133c1fb6d21a139f91f01e6fd927463d1f5017dd248f2ca6f97363de396d8012103a23dcf3edc18180b0d82b0be8eb1c30ae61597e99e298246e05109b7ba897637a5941500", + "778b01899d5ed48df03e406bc5babd1fdc8f1be4b7e5b9d20dd8caf24dd66ff4": "02000000000101384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bc90100000000fdffffff02ec130000000000001976a9147f20306730683c828f9a3a990193007106aaca9788ac2471010000000000160014edc62a3c999159ea7aa02f4798f06e2020dc35ad0247304402205f8aa5c02cc6d949f11dc6f61b10ffb37a9fbbd5a4dcc840e1048c257a55ae080220153a6e02eb8b12de1f943d0071cef4f710c3406c8a67174ddf491ea94f4104b0012103555bc1ad91fea0ace08ef29d9e1eeaaa6b9fc45d225c60e30d7b8a6ee37ed03e7f5c2700", + "87dbf5a7a089f1e071559cdfd3e4bddb841cdd0032ffeec328952abf76fe3661": "02000000000101ce08f7d7b4b76a57ba5693a31219e4068e02c5fa8e7e57bc1cf50c47625cf56f0000000000fdffffff02a0860100000000002200204915b90ffa7ef166e0818b75a206d7d179efa14af39c23781a083c749aa651a46c9e040000000000160014905190f141d2089497a554d5b74175929d2b2c7d0247304402203c9cbdd54e067f9a45c7795ae570fc49c706bc4174255f934afbaa6bf8e5411f02203b019772e113869efc8573fed181b14e33ea9bfdeb3f495d3ae1e4f8844e3290012103a85bd8a32a699000359752570dd3bc027d1ca9865fe5fa39c7d4aaab785b50cb48e82400", + "8c09f8984e231fcaebdda00b2597cfde3d825a26dad2c5478c78ccabbac1f28b": "020000000001013cf21d22012066ba1c566dad36c1e5d76e4c95acc95b9e357e4d58e0862acaef0100000000feffffff0200710200000000001600146f9726216be985680857a2c95200be19dd0c217960130f0000000000160014a96ad868b42a6944f6adc75b68a4b30a158ad175024730440220235d1cddad375f7d7612c50fa08b2ec1d4772b333795b6cb98754225626757b902200f4a784e44a2a23fd811f9f28e82ee0333068156ba969556aed1f6337337823f0121030ee45e829f5f95c3a0a184897c7fdb687307301164ed55d98411368d962510bd40d92300", + "94fce9de7e82741cc982936ad9f7900372b80be8100cb80e49f5d78c2954fb69": "020000000001037fbdcc64290f40401515f0cf75d761e9d2b80acf70118d6fa923976b0a49b5230100000000feffffff8bf2c1baabcc788c47c5d2da265a823ddecf97250ba0ddebca1f234e98f8098c0000000000feffffff3cf21d22012066ba1c566dad36c1e5d76e4c95acc95b9e357e4d58e0862acaef0000000000feffffff020000000000000000166a14b837667ddbc6acf0464c1eb35b8a2ba485364cd7e72c0600000000002200206a21894e5bd3a85c5c0f3211772c1553137bcdfbe0c06d5c691289d4a8862b0b0247304402206bfc98da12ba0095c12a9929e7b39132ed9f37f8ba3beaf32a479a738f12a66f0220678fa3ac65fbe79801a39931a4d83255bd75c62a76045ddd7f611ad109538b8a012102bb8e9193021c1f704d7ce8577c5fdc7ba22dd05a03b5b5ecd6ef6307dd8a60bc024730440220730470f1b831e706a633091f2bdaa0944e8b80a61db84dfc41aae5e9265809dd022055362b2618d4dee152819fd259db76aca5c7e7cb7f56d7cd6da8d006b44ca3bf01210319d5b935f6f1cd4a1b84645c3f88db404a93baf78451da144a0dc00aaa1d978c024730440220205c9e119f1b2a6951129f8f4a917572ee4c5c7ed55042b1a6e4d787c8d65bdd02204ced0cbfb7bbdb9e89938ebaca90a9a15727c21a03d5d758617aa25077eba4ed01210319d5b935f6f1cd4a1b84645c3f88db404a93baf78451da144a0dc00aaa1d978c98b72400", + "972d16040c71b6660b52a0ad06ddb17aa7097921643bab526ad2e00780004535": "01000000000101ba709d2dafe9a11fdc2c1b235587455baf0415a2cf302f9cb9c046129a9326770100000000fdffffff0240420f0000000000160014e278e5edb47b274f91d137e767190b4d02b09a172eea730000000000160014692a676d16f83641a6518e9154d2b43a8465ed4402473044022074b4e39c36785a2751fa3b944ed1430bab6461714549d32a195132aace1ca507022043c545d8a1d349ecb6cd3d87fd6c73eb5b68e0d2ba99891d72fce278dce090d0012103e0e2b67a3e8244900fa916decb6a9d45787ebe15b882c7a065cd0151d298fb4ea6941500", + "97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe": "020000000001016136fe76bf2a9528c3eeff3200dd1c84dbbde4d3df9c5571e0f189a0a7f5db870100000000feffffff020000000000000000166a1492e1315ad9862cb68d93417085728e974f2f8bbed39d0400000000002200204c0dffbbeacb7180f46597605740cdab2f8cbbe603b737b95eb7076de632659a0247304402204b95fa4019d2d5427646c04a8b740111da39b91ab8cd8e7976aa99912d1cf7d60220458db68cf3d977cfc5ecb38c193b890a3beae714d3485233f28b52b4db5f7563012102576b988dc3260d1dc5e579d604f6d3663287c933cde4ccd60d6875e9ced8fc5607012500", + "b234c4f252cd193377bc6cf931ff1ef6a398e3a434e43b22cdb457a8213d749b": "020000000001026962bc75f03d744a58dc43f17c61ea1f5ebf2aa9e80ede831eb2e097e1e142200000000000fdffffff1f657881066bcd13a189eed6825e30061c1062c473124fcbeaa4e8cde5bc48ba0000000000fdffffff013dd13000000000001976a914fb08dfdd325a10708693d8565b93d2eb38373c0188ac0247304402205039ecdcbbd57fb7093b38bfbd599aeed5c085a0812d7d977f1325d63733ea710220281af83a941ca7e1dd99381904905d095413a654f1d9e9997060bd5f61adf8be01210379da8156b704d5dfe4432bc829588c643ec65c5a409763dccc3da211479f4bb702483045022100e0c07bb37d8da22ea0036b835d86512ecaf584f00534b2548855b02c1d49f59802200cd94644caa3e90349a0e22de080037290b75e2a5906333cb46ef5fb8f36ba7e0121027ba4d3ee6471a985307a37d09eb9a0a73c1a31b57616fe3e53a3d6d454002519c3271800", + "b424362ec4eb39eb84f0bea8e00ffd8382d5dde29092bd1e268b9a027dc5225c": "02000000000101e00607297fdfc9808759f2042adfe27912e79031bbff6deb627583a65538bc300000000000fdffffff02e803000000000000536a4c504a61637175656c696e652c2049206c6f766520796f75206d6f7265207468616e20616e797468696e672e20596f75206172652065766572797468696e6720746f206d652e204c6f76652c2044796c616e5e22000000000000160014c388b18dd1771046fecb85f44642ffb7a4efb62702483045022100fa3523ed4f12e036b952c8c7aa88dfc663086bd6a951d14a7faaa3fd28587294022017c7984bdcfeed7e25de571f020f65d30ff03e0dce4c6c2514c2ffc208280c80012103c3172a9f8820681c62b8bf28961988a4642b33f4920b9da14b06965c7fff83fded3b1600", + "ba48bce5cde8a4eacb4f1273c462101c06305e82d6ee89a113cd6b068178651f": "0200000002ba7361bf5ff80a3f30b89d7b26c5a1a60fbea526bed89d2e78dc358611be874a010000006b483045022100e2886246bf9d98bae1ed94dddb3da345456d4848c02dcd90e0e9357ed609836f0220344890230f0748c7389645e2a71cf693aeb84617822114bc01e15e17960b36cc01210348d667e58824a00bfc719645c0ff685513243f3476b6b05e4f46cf9b8c3146b0fdffffffd945b6b076ad7858f7a2227374a42ada71582b1d558fde9ece4bf6c9395c67ca010000006a473044022047dcdee44123175b7301882c628da72810ff585e30b94c8403aa641ffd6d8825022046ff6818ed60b5dffbe19534ea4b7a275e48aaee183a14eeb66e36d136880c6b012103ef48fb7c170e02b9f6f481e4eb541947ce89709163c4cdef0af65bf8b64b2d8dfdffffff010f4c2f00000000001600147714df0e53290ddfa3d893b41b8056f7be98a770c3271800", + "c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38": "020000000001017d6f488442fa311ff6e3a1124ff3cf6a974aa13867306f0456c8e73ca5c7be030100000000feffffff030000000000000000166a14cbfdb55e747e0cbdc2328518ae6363cc5f5c3fc0e2850100000000001600143ece1158a80cbbb40dd12688732d3b77a959e5bf801a06000000000022002084081ee1045d5e33acb4d90081dc6db80b14d09596d1b066917557f23437c4fa0247304402205113e656225c09f8b05fb5bef8c48196e2520a988bc89832f8cc806c64dc3dce02204fbb91805640038fb7e73eb2bf5a28a3a015982a1d9dcd591e6bb9c8c103bab9012102fe3de4d6b6bf207134f40ada206ff6e2ed088169d2413db656c8de963a15c6e17e5c2700", + "d57bb7a379c7eb8c86e34d595722ca4f389394b4497c1f0028971e0a8e75c8e1": "0200000000010169fb54298cd7f5490eb80c10e80bb8720390f7d96a9382c91c74827edee9fc940100000000ffffffff015d2c060000000000160014edb4b55f1f8c33ac8597fa53d08d8997131f1b93040047304402202033a46f390d2ed27d1e7aaca2c29cc6af20b92a7535ce7bfa9355480a1a83d202204a94016d878e7e25647738ff28884046d27e4ca83287a0fd4e51ad7d899e47ea0147304402201bddd59748ba0a79d73fda2747b9688b86eef9cad625201d26d6591ceebe18ae022014ea8ecb3722b3ab4873476ca77b2d6ae366d2e61af42ead40419221d6192ba301475221031c9cd5530ea64a7e411c21701d5bcac74e55420924110c93706c12ef7184f0a42103672361cd8f92dfac7fb1d1bb8974bf826cf8cc0eaa6fcb23175be9312d5aced652ae00000000", + "efca2a86e0584d7e359e5bc9ac954c6ed7e5c136ad6d561cba662001221df23c": "020000000001018cc9bd58583a2aba20e8747b6945a157cdcbb75c2c6cea5a1e036ea7bff30ab30100000000feffffff02b0ad0100000000001600146f9726216be985680857a2c95200be19dd0c2179288511000000000016001418d1896a5e9360279ab7d07a65ecc992d9b514c50247304402206941c4f8cdd0f4a907931f4400ed37a310b00425a2cf218f11affd53d70268e50220412b7e34545d3aabe2b9f00b9fa18c84a2d030b2f4244127dca69738abec6fe10121033399307690d676c7faf57ac454ca6399017f45ef5aa3bebac07b8691bb572a073dd92300" + }, + "tx_fees": { + "024464eae9858f079d2f49bc3db28f92237c63fe950c8ddc6c9b18d1855fa32c": [ + null, + false, + 1 + ], + "03bec7a53ce7c856046f306738a14a976acff34f12a1e3f61f31fa4284486f7d": [ + null, + false, + 1 + ], + "03eb4932abf231999f5a698a28f70cfe6b1a2975be0310e348a9898434efb8d3": [ + 1000, + true, + 1 + ], + "0ef1c91636d20331acc2759d0f0335925cd899e270efdd2598e9449c66649d39": [ + 1000, + true, + 1 + ], + "10a2a7695a5b6ba857b10d04ad047b968656c6d84b58ea226dc92c150cb73b2f": [ + 111, + true, + 1 + ], + "2042e1e197e0b21e83de0ee8a92abf5e1fea617cf143dc584a743df075bc6269": [ + null, + false, + 1 + ], + "23b5490a6b9723a96f8d1170cf0ab8d2e961d775cff0151540400f2964ccbd7f": [ + null, + false, + 1 + ], + "25ddebe4b7e7caf7b175762ddc4cbb5d704f27520a997b7672057f2455b2fcea": [ + null, + false, + 1 + ], + "30bc3855a6837562eb6dffbb3190e71279e2df2a04f2598780c9df7f290706e0": [ + 141, + true, + 1 + ], + "3407144b10e0c7f60a88e9f3adcc7e732c83e92fa17a923f523b8acc50718c2f": [ + 246, + true, + 3 + ], + "4029e471c030066ecca20621cc2917cade0fd44c5cbcb6f5299c086f25ec4b6c": [ + null, + false, + 3 + ], + "41284afa51eccc94d10fac25eaa59d1fa1afdc3e27bae2b16a64399fe0f1be3c": [ + 1000, + true, + 1 + ], + "4dbdf2810f130653d1eb17ce3645d0362e4ef41ffbd521aad1f9294c95baf76e": [ + 141, + true, + 1 + ], + "59a9ff5fa62586f102b92504584f52e47f4ca0d5af061e99a0a3023fa70a70e2": [ + null, + false, + 1 + ], + "66f846809c2d45842f17e40fb6a696288c3274a66b0bf71107940f250978832e": [ + 183, + true, + 1 + ], + "6ff55c62470cf51cbc577e8efac5028e06e41912a39356ba576ab7b4d7f708ce": [ + 1097, + true, + 1 + ], + "737857196825fa2e600bac822c866f10e5f484b0f193cff1fc3d38b91884c7a6": [ + 705, + true, + 1 + ], + "7726939a1246c0b99c2f30cfa21504af5b458755231b2cdc1fa1e9af2d9d70ba": [ + 705, + true, + 1 + ], + "778b01899d5ed48df03e406bc5babd1fdc8f1be4b7e5b9d20dd8caf24dd66ff4": [ + 210, + true, + 1 + ], + "87dbf5a7a089f1e071559cdfd3e4bddb841cdd0032ffeec328952abf76fe3661": [ + 776, + true, + 1 + ], + "8c09f8984e231fcaebdda00b2597cfde3d825a26dad2c5478c78ccabbac1f28b": [ + null, + false, + 1 + ], + "94fce9de7e82741cc982936ad9f7900372b80be8100cb80e49f5d78c2954fb69": [ + 289, + true, + 3 + ], + "972d16040c71b6660b52a0ad06ddb17aa7097921643bab526ad2e00780004535": [ + 705, + true, + 1 + ], + "97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe": [ + 153, + true, + 1 + ], + "b234c4f252cd193377bc6cf931ff1ef6a398e3a434e43b22cdb457a8213d749b": [ + 181, + true, + 2 + ], + "b424362ec4eb39eb84f0bea8e00ffd8382d5dde29092bd1e268b9a027dc5225c": [ + 202, + true, + 1 + ], + "ba48bce5cde8a4eacb4f1273c462101c06305e82d6ee89a113cd6b068178651f": [ + null, + false, + 2 + ], + "c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38": [ + 190, + true, + 1 + ], + "d57bb7a379c7eb8c86e34d595722ca4f389394b4497c1f0028971e0a8e75c8e1": [ + 138, + true, + 1 + ], + "efca2a86e0584d7e359e5bc9ac954c6ed7e5c136ad6d561cba662001221df23c": [ + null, + false, + 1 + ] + }, + "txi": { + "03eb4932abf231999f5a698a28f70cfe6b1a2975be0310e348a9898434efb8d3": { + "tb1q07ulrxeuu45uqen0clqe85v5en6rf77cxgxsj5": { + "0ef1c91636d20331acc2759d0f0335925cd899e270efdd2598e9449c66649d39:1": 9799000 + } + }, + "0ef1c91636d20331acc2759d0f0335925cd899e270efdd2598e9449c66649d39": { + "tb1qq2tmmcngng78nllq2pvrkchcdukemtj5s6l0zu": { + "024464eae9858f079d2f49bc3db28f92237c63fe950c8ddc6c9b18d1855fa32c:0": 10000000 + } + }, + "10a2a7695a5b6ba857b10d04ad047b968656c6d84b58ea226dc92c150cb73b2f": { + "tb1qusm48zmlzwr32csxdw4ar7atw260h22c8zq7jk": { + "25ddebe4b7e7caf7b175762ddc4cbb5d704f27520a997b7672057f2455b2fcea:1": 100000 + } + }, + "30bc3855a6837562eb6dffbb3190e71279e2df2a04f2598780c9df7f290706e0": { + "tb1q25arh97ze37n6nk74n3js8ls8z7sva3f0d8pnl": { + "4dbdf2810f130653d1eb17ce3645d0362e4ef41ffbd521aad1f9294c95baf76e:1": 3204744 + } + }, + "3407144b10e0c7f60a88e9f3adcc7e732c83e92fa17a923f523b8acc50718c2f": { + "tb1q008n3k9xjpcuyx4mlczn9jm2at90ts55yrtynq": { + "30bc3855a6837562eb6dffbb3190e71279e2df2a04f2598780c9df7f290706e0:1": 3194603 + }, + "tb1qcwytrrw3wugydlktsh6yvshlk7jwld38akp8l3": { + "b424362ec4eb39eb84f0bea8e00ffd8382d5dde29092bd1e268b9a027dc5225c:1": 8798 + }, + "tb1qm7ckcjsed98zhvhv3dr56a22w3fehlkxyh4wgd": { + "4029e471c030066ecca20621cc2917cade0fd44c5cbcb6f5299c086f25ec4b6c:0": 14695224 + } + }, + "41284afa51eccc94d10fac25eaa59d1fa1afdc3e27bae2b16a64399fe0f1be3c": { + "tb1q0quewquwhlfgahhsdg0q3r5lmyzufrtp3fzme4": { + "737857196825fa2e600bac822c866f10e5f484b0f193cff1fc3d38b91884c7a6:1": 6595885 + } + }, + "4dbdf2810f130653d1eb17ce3645d0362e4ef41ffbd521aad1f9294c95baf76e": { + "tb1qvsvr06h2phnwmzvwxrlkuzfu2ekhjpfpvguyct": { + "41284afa51eccc94d10fac25eaa59d1fa1afdc3e27bae2b16a64399fe0f1be3c:1": 6394885 + } + }, + "66f846809c2d45842f17e40fb6a696288c3274a66b0bf71107940f250978832e": { + "tb1qfsxllwl2edccpar9jas9wsxd4vhcewlxqwmn0w27kurkme3jvkdqn4msdp": { + "97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe:1": 302547 + } + }, + "6ff55c62470cf51cbc577e8efac5028e06e41912a39356ba576ab7b4d7f708ce": { + "tb1qak6t2hcl3se6epvhlffaprvfjuf37xunnxq7c9": { + "d57bb7a379c7eb8c86e34d595722ca4f389394b4497c1f0028971e0a8e75c8e1:0": 404573 + } + }, + "737857196825fa2e600bac822c866f10e5f484b0f193cff1fc3d38b91884c7a6": { + "tb1qdy4xwmgklqmyrfj336g4f54582zxtm2yhlge8l": { + "972d16040c71b6660b52a0ad06ddb17aa7097921643bab526ad2e00780004535:1": 7596590 + } + }, + "7726939a1246c0b99c2f30cfa21504af5b458755231b2cdc1fa1e9af2d9d70ba": { + "tb1qjm4rr97lxawx5csc3nu3rxhwnpxqe3s4uhwagu": { + "03eb4932abf231999f5a698a28f70cfe6b1a2975be0310e348a9898434efb8d3:1": 9598000 + } + }, + "778b01899d5ed48df03e406bc5babd1fdc8f1be4b7e5b9d20dd8caf24dd66ff4": { + "tb1q8m8pzk9gpjamgrw3y6y8xtfmw754nedldje5q5": { + "c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38:1": 99810 + } + }, + "87dbf5a7a089f1e071559cdfd3e4bddb841cdd0032ffeec328952abf76fe3661": { + "tb1qgdp7aa38x3p2kpn2s5486mkvvx2sktnmxkf47e": { + "6ff55c62470cf51cbc577e8efac5028e06e41912a39356ba576ab7b4d7f708ce:0": 403476 + } + }, + "94fce9de7e82741cc982936ad9f7900372b80be8100cb80e49f5d78c2954fb69": { + "tb1qd7tjvgttaxzkszzh5ty4yq97r8wscgteejustc": { + "8c09f8984e231fcaebdda00b2597cfde3d825a26dad2c5478c78ccabbac1f28b:0": 160000, + "efca2a86e0584d7e359e5bc9ac954c6ed7e5c136ad6d561cba662001221df23c:0": 110000 + }, + "tb1qfu50fqxhfkl69n6urjzuhc5ndr96tuaxrh3an8": { + "23b5490a6b9723a96f8d1170cf0ab8d2e961d775cff0151540400f2964ccbd7f:1": 135000 + } + }, + "972d16040c71b6660b52a0ad06ddb17aa7097921643bab526ad2e00780004535": { + "tb1quhk94rhlsflc4wgxl9qzd6p6wszt30uxt4a0yj": { + "7726939a1246c0b99c2f30cfa21504af5b458755231b2cdc1fa1e9af2d9d70ba:1": 8597295 + } + }, + "97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe": { + "tb1qjpgepu2p6gyff9a92n2mwst4j2wjktra956lcg": { + "87dbf5a7a089f1e071559cdfd3e4bddb841cdd0032ffeec328952abf76fe3661:1": 302700 + } + }, + "b234c4f252cd193377bc6cf931ff1ef6a398e3a434e43b22cdb457a8213d749b": { + "tb1qgcgk7j9kpt2mygmhmnu4zep79cd289t6aely7z": { + "2042e1e197e0b21e83de0ee8a92abf5e1fea617cf143dc584a743df075bc6269:0": 99811 + }, + "tb1qwu2d7rjn9yxalg7cjw6phqzk77lf3fmsta8rhu": { + "ba48bce5cde8a4eacb4f1273c462101c06305e82d6ee89a113cd6b068178651f:0": 3099663 + } + }, + "b424362ec4eb39eb84f0bea8e00ffd8382d5dde29092bd1e268b9a027dc5225c": { + "tb1qn7d2x7272lznt5hhk9s07q3cqnrqljnwa55w6c": { + "30bc3855a6837562eb6dffbb3190e71279e2df2a04f2598780c9df7f290706e0:0": 10000 + } + }, + "c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38": { + "tb1qplsf242vay6vavy4eguef855fx3klmp9505g88": { + "03bec7a53ce7c856046f306738a14a976acff34f12a1e3f61f31fa4284486f7d:1": 500000 + } + }, + "d57bb7a379c7eb8c86e34d595722ca4f389394b4497c1f0028971e0a8e75c8e1": { + "tb1qdgscjnjm6w59chq0xgghwtq42vfhhn0murqx6hrfz2yaf2yx9v9skh03as": { + "94fce9de7e82741cc982936ad9f7900372b80be8100cb80e49f5d78c2954fb69:1": 404711 + } + } + }, + "txo": { + "024464eae9858f079d2f49bc3db28f92237c63fe950c8ddc6c9b18d1855fa32c": { + "tb1qq2tmmcngng78nllq2pvrkchcdukemtj5s6l0zu": { + "0": [ + 10000000, + false + ] + } + }, + "03bec7a53ce7c856046f306738a14a976acff34f12a1e3f61f31fa4284486f7d": { + "tb1qplsf242vay6vavy4eguef855fx3klmp9505g88": { + "1": [ + 500000, + false + ] + } + }, + "03eb4932abf231999f5a698a28f70cfe6b1a2975be0310e348a9898434efb8d3": { + "tb1qjm4rr97lxawx5csc3nu3rxhwnpxqe3s4uhwagu": { + "1": [ + 9598000, + false + ] + } + }, + "0ef1c91636d20331acc2759d0f0335925cd899e270efdd2598e9449c66649d39": { + "tb1q07ulrxeuu45uqen0clqe85v5en6rf77cxgxsj5": { + "1": [ + 9799000, + false + ] + } + }, + "2042e1e197e0b21e83de0ee8a92abf5e1fea617cf143dc584a743df075bc6269": { + "tb1qgcgk7j9kpt2mygmhmnu4zep79cd289t6aely7z": { + "0": [ + 99811, + false + ] + } + }, + "23b5490a6b9723a96f8d1170cf0ab8d2e961d775cff0151540400f2964ccbd7f": { + "tb1qfu50fqxhfkl69n6urjzuhc5ndr96tuaxrh3an8": { + "1": [ + 135000, + false + ] + } + }, + "25ddebe4b7e7caf7b175762ddc4cbb5d704f27520a997b7672057f2455b2fcea": { + "tb1qusm48zmlzwr32csxdw4ar7atw260h22c8zq7jk": { + "1": [ + 100000, + false + ] + } + }, + "30bc3855a6837562eb6dffbb3190e71279e2df2a04f2598780c9df7f290706e0": { + "tb1q008n3k9xjpcuyx4mlczn9jm2at90ts55yrtynq": { + "1": [ + 3194603, + false + ] + }, + "tb1qn7d2x7272lznt5hhk9s07q3cqnrqljnwa55w6c": { + "0": [ + 10000, + false + ] + } + }, + "4029e471c030066ecca20621cc2917cade0fd44c5cbcb6f5299c086f25ec4b6c": { + "tb1qm7ckcjsed98zhvhv3dr56a22w3fehlkxyh4wgd": { + "0": [ + 14695224, + false + ] + } + }, + "41284afa51eccc94d10fac25eaa59d1fa1afdc3e27bae2b16a64399fe0f1be3c": { + "tb1qvsvr06h2phnwmzvwxrlkuzfu2ekhjpfpvguyct": { + "1": [ + 6394885, + false + ] + } + }, + "4dbdf2810f130653d1eb17ce3645d0362e4ef41ffbd521aad1f9294c95baf76e": { + "tb1q25arh97ze37n6nk74n3js8ls8z7sva3f0d8pnl": { + "1": [ + 3204744, + false + ] + } + }, + "59a9ff5fa62586f102b92504584f52e47f4ca0d5af061e99a0a3023fa70a70e2": { + "tb1qrdzfu6mlgrxpupd4syxrv77ncku89a0y0vd7f3": { + "1": [ + 654165, + false + ] + } + }, + "66f846809c2d45842f17e40fb6a696288c3274a66b0bf71107940f250978832e": { + "tb1qcmq7v2zg0jjy5g47k90fqd0h7a4mcyp7f3ly6r": { + "0": [ + 302364, + false + ] + } + }, + "6ff55c62470cf51cbc577e8efac5028e06e41912a39356ba576ab7b4d7f708ce": { + "tb1qgdp7aa38x3p2kpn2s5486mkvvx2sktnmxkf47e": { + "0": [ + 403476, + false + ] + } + }, + "737857196825fa2e600bac822c866f10e5f484b0f193cff1fc3d38b91884c7a6": { + "tb1q0quewquwhlfgahhsdg0q3r5lmyzufrtp3fzme4": { + "1": [ + 6595885, + false + ] + } + }, + "7726939a1246c0b99c2f30cfa21504af5b458755231b2cdc1fa1e9af2d9d70ba": { + "tb1quhk94rhlsflc4wgxl9qzd6p6wszt30uxt4a0yj": { + "1": [ + 8597295, + false + ] + } + }, + "778b01899d5ed48df03e406bc5babd1fdc8f1be4b7e5b9d20dd8caf24dd66ff4": { + "tb1qahrz50yej9v7574q9are3urwyqsdcdddmjl9a6": { + "1": [ + 94500, + false + ] + } + }, + "87dbf5a7a089f1e071559cdfd3e4bddb841cdd0032ffeec328952abf76fe3661": { + "tb1qjpgepu2p6gyff9a92n2mwst4j2wjktra956lcg": { + "1": [ + 302700, + false + ] + } + }, + "8c09f8984e231fcaebdda00b2597cfde3d825a26dad2c5478c78ccabbac1f28b": { + "tb1qd7tjvgttaxzkszzh5ty4yq97r8wscgteejustc": { + "0": [ + 160000, + false + ] + } + }, + "94fce9de7e82741cc982936ad9f7900372b80be8100cb80e49f5d78c2954fb69": { + "tb1qdgscjnjm6w59chq0xgghwtq42vfhhn0murqx6hrfz2yaf2yx9v9skh03as": { + "1": [ + 404711, + false + ] + } + }, + "972d16040c71b6660b52a0ad06ddb17aa7097921643bab526ad2e00780004535": { + "tb1qdy4xwmgklqmyrfj336g4f54582zxtm2yhlge8l": { + "1": [ + 7596590, + false + ] + } + }, + "97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe": { + "tb1qfsxllwl2edccpar9jas9wsxd4vhcewlxqwmn0w27kurkme3jvkdqn4msdp": { + "1": [ + 302547, + false + ] + } + }, + "b424362ec4eb39eb84f0bea8e00ffd8382d5dde29092bd1e268b9a027dc5225c": { + "tb1qcwytrrw3wugydlktsh6yvshlk7jwld38akp8l3": { + "1": [ + 8798, + false + ] + } + }, + "ba48bce5cde8a4eacb4f1273c462101c06305e82d6ee89a113cd6b068178651f": { + "tb1qwu2d7rjn9yxalg7cjw6phqzk77lf3fmsta8rhu": { + "0": [ + 3099663, + false + ] + } + }, + "c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38": { + "tb1q8m8pzk9gpjamgrw3y6y8xtfmw754nedldje5q5": { + "1": [ + 99810, + false + ] + }, + "tb1qssypacgyt40r8t95myqgrhrdhq93f5y4jmgmqe53w4tlydphcnaqmpm8kr": { + "2": [ + 400000, + false + ] + } + }, + "d57bb7a379c7eb8c86e34d595722ca4f389394b4497c1f0028971e0a8e75c8e1": { + "tb1qak6t2hcl3se6epvhlffaprvfjuf37xunnxq7c9": { + "0": [ + 404573, + false + ] + } + }, + "efca2a86e0584d7e359e5bc9ac954c6ed7e5c136ad6d561cba662001221df23c": { + "tb1qd7tjvgttaxzkszzh5ty4yq97r8wscgteejustc": { + "0": [ + 110000, + false + ] + } + } + }, + "use_change": true, + "use_encryption": false, + "verified_tx3": { + "024464eae9858f079d2f49bc3db28f92237c63fe950c8ddc6c9b18d1855fa32c": [ + 1346870, + 1530271747, + 11, + "0000000000000be4817e499bebdcebd9c792f68551bfedbf14fa058fd8db4e2d" + ], + "03bec7a53ce7c856046f306738a14a976acff34f12a1e3f61f31fa4284486f7d": [ + 2579582, + 1708966959, + 675, + "0000000000002b0be5f214bcdcd765f6e4cefa1226a6d3a4e1b482da9707603a" + ], + "03eb4932abf231999f5a698a28f70cfe6b1a2975be0310e348a9898434efb8d3": [ + 1346978, + 1530282040, + 3, + "00000000000004f73116c1b9d17dafc409710362110c7cd9e52f1e8703147238" + ], + "0ef1c91636d20331acc2759d0f0335925cd899e270efdd2598e9449c66649d39": [ + 1346957, + 1530280346, + 10, + "00000000000004e05e5f82a8f3db5f37cf8b3c8ad4d44a80556c63173f2fdb8d" + ], + "10a2a7695a5b6ba857b10d04ad047b968656c6d84b58ea226dc92c150cb73b2f": [ + 1892447, + 1605658960, + 42, + "000000000000009e8f7f68acdbf0d5d20d920794916249d7300d3d4ddea01a7a" + ], + "2042e1e197e0b21e83de0ee8a92abf5e1fea617cf143dc584a743df075bc6269": [ + 1583044, + 1571148168, + 273, + "00000000691cd68ef23f24f9bd245aaf85f8dec4abe26d1f9ff97681af9eb222" + ], + "23b5490a6b9723a96f8d1170cf0ab8d2e961d775cff0151540400f2964ccbd7f": [ + 2404107, + 1667575669, + 18, + "00000000000000dd5c68b0c41dcd77382c18fada64632c0475b906f0f6a936d7" + ], + "25ddebe4b7e7caf7b175762ddc4cbb5d704f27520a997b7672057f2455b2fcea": [ + 1775825, + 1594136749, + 81, + "000000007583aa1310c8368737317e3443990a03c2bdcc95513f82c21e7e8609" + ], + "30bc3855a6837562eb6dffbb3190e71279e2df2a04f2598780c9df7f290706e0": [ + 1455208, + 1549129020, + 23, + "000000000000002bbb81515fa47a4aaa4af7b324b606df83b90be6018172ba99" + ], + "3407144b10e0c7f60a88e9f3adcc7e732c83e92fa17a923f523b8acc50718c2f": [ + 1568264, + 1562691216, + 85, + "000000000000027001cae7dc4b5b14a66f2219d13a620e1ed12bf5a7a9f444fd" + ], + "4029e471c030066ecca20621cc2917cade0fd44c5cbcb6f5299c086f25ec4b6c": [ + 1455781, + 1549379178, + 61, + "000000000000006f773a59309e17ce987f302945141d657b6e310bf04a5a11d2" + ], + "41284afa51eccc94d10fac25eaa59d1fa1afdc3e27bae2b16a64399fe0f1be3c": [ + 1414562, + 1538150440, + 81, + "0000000085ee7b5accad0063fc1a368d905cbf704223bccac1b9d2e351a4578d" + ], + "4dbdf2810f130653d1eb17ce3645d0362e4ef41ffbd521aad1f9294c95baf76e": [ + 1454548, + 1548893536, + 44, + "000000000000004bfd6914c088814a418c4308cc0f6b87b993455b1a2eca7531" + ], + "59a9ff5fa62586f102b92504584f52e47f4ca0d5af061e99a0a3023fa70a70e2": [ + 2579584, + 1708968409, + 1814, + "00000000000000120fe076771544efb54ec2ed49c0ad1278bb49f9c9efa51944" + ], + "66f846809c2d45842f17e40fb6a696288c3274a66b0bf71107940f250978832e": [ + 2528392, + 1696346138, + 155, + "00000000f7d4a4dcb86d377009f654832d52f9f94299c51434eab00038e4ab6a" + ], + "6ff55c62470cf51cbc577e8efac5028e06e41912a39356ba576ab7b4d7f708ce": [ + 2415285, + 1673173967, + 17, + "00000000000000105c5d1016f327809add5b052aa5032e34cab26050535143ff" + ], + "737857196825fa2e600bac822c866f10e5f484b0f193cff1fc3d38b91884c7a6": [ + 1414311, + 1537898497, + 45, + "0000000000000046df1be1854387a550e40df1f487218b6b977b7323db986c45" + ], + "7726939a1246c0b99c2f30cfa21504af5b458755231b2cdc1fa1e9af2d9d70ba": [ + 1414310, + 1537897872, + 48, + "0000000050c14b6289103b0a666e76bb178e757f0411947e9461ae82483df756" + ], + "778b01899d5ed48df03e406bc5babd1fdc8f1be4b7e5b9d20dd8caf24dd66ff4": [ + 2579584, + 1708968409, + 284, + "00000000000000120fe076771544efb54ec2ed49c0ad1278bb49f9c9efa51944" + ], + "87dbf5a7a089f1e071559cdfd3e4bddb841cdd0032ffeec328952abf76fe3661": [ + 2418761, + 1675433925, + 56, + "00000000000037c0576afb47f7f757544ad44710a08c7829dcfc56710d2216d8" + ], + "8c09f8984e231fcaebdda00b2597cfde3d825a26dad2c5478c78ccabbac1f28b": [ + 2349377, + 1664914506, + 58, + "000000003309f969ede680d3aea4ca8643912b4b4b793172de15a127a790f778" + ], + "94fce9de7e82741cc982936ad9f7900372b80be8100cb80e49f5d78c2954fb69": [ + 2406297, + 1668461654, + 24, + "000000000000001c4dd10ac70eb8f0af901aec27be9469354f98f15beeebdc03" + ], + "972d16040c71b6660b52a0ad06ddb17aa7097921643bab526ad2e00780004535": [ + 1414311, + 1537898497, + 44, + "0000000000000046df1be1854387a550e40df1f487218b6b977b7323db986c45" + ], + "97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe": [ + 2425096, + 1679252503, + 21, + "000000000000001221ae1721de7f87c9313912720a43b867546e1c93f5d94945" + ], + "b234c4f252cd193377bc6cf931ff1ef6a398e3a434e43b22cdb457a8213d749b": [ + 1583044, + 1571148168, + 286, + "00000000691cd68ef23f24f9bd245aaf85f8dec4abe26d1f9ff97681af9eb222" + ], + "b424362ec4eb39eb84f0bea8e00ffd8382d5dde29092bd1e268b9a027dc5225c": [ + 1457134, + 1550165277, + 52, + "0000000000000057c116d32f1edf128ec4f81a5e734a4095cda3c9ef1d6cb7f7" + ], + "ba48bce5cde8a4eacb4f1273c462101c06305e82d6ee89a113cd6b068178651f": [ + 1583044, + 1571148168, + 191, + "00000000691cd68ef23f24f9bd245aaf85f8dec4abe26d1f9ff97681af9eb222" + ], + "c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38": [ + 2579583, + 1708967515, + 479, + "000000000000001f81c50f287422a15099c2bc5cd4464d9962f84e8c1a7b94bb" + ], + "d57bb7a379c7eb8c86e34d595722ca4f389394b4497c1f0028971e0a8e75c8e1": [ + 2415285, + 1673173967, + 16, + "00000000000000105c5d1016f327809add5b052aa5032e34cab26050535143ff" + ], + "efca2a86e0584d7e359e5bc9ac954c6ed7e5c136ad6d561cba662001221df23c": [ + 2349374, + 1664911683, + 13, + "0000000000000012fd19ff17ac6890f0911be5791042eb2a6243e3a6b4817e94" + ] + }, + "wallet_nonce": 129, + "wallet_type": "standard", + "winpos-qt": [ + 344, + 374, + 1336, + 899 + ] +} \ No newline at end of file