init
This commit is contained in:
		
						commit
						bdcf1929f5
					
				
							
								
								
									
										21
									
								
								LICENSE
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								LICENSE
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,21 @@ | |||||||
|  | MIT License | ||||||
|  | 
 | ||||||
|  | Copyright (c) 2024 copronista | ||||||
|  | 
 | ||||||
|  | Permission is hereby granted, free of charge, to any person obtaining a copy | ||||||
|  | of this software and associated documentation files (the "Software"), to deal | ||||||
|  | in the Software without restriction, including without limitation the rights | ||||||
|  | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||||||
|  | copies of the Software, and to permit persons to whom the Software is | ||||||
|  | furnished to do so, subject to the following conditions: | ||||||
|  | 
 | ||||||
|  | The above copyright notice and this permission notice shall be included in all | ||||||
|  | copies or substantial portions of the Software. | ||||||
|  | 
 | ||||||
|  | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||||
|  | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||||||
|  | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||||||
|  | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||||
|  | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||||||
|  | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||||
|  | SOFTWARE. | ||||||
							
								
								
									
										20
									
								
								__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								__init__.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,20 @@ | |||||||
|  | from electrum.i18n import _ | ||||||
|  | import subprocess | ||||||
|  | from . import bal_resources | ||||||
|  | BUILD_NUMBER = 0  | ||||||
|  | REVISION_NUMBER = 1 | ||||||
|  | VERSION_NUMBER = 0 | ||||||
|  | def _version(): | ||||||
|  |     return f'{VERSION_NUMBER}.{REVISION_NUMBER}-{BUILD_NUMBER}' | ||||||
|  | 
 | ||||||
|  | version = _version() | ||||||
|  | author = "Bal Enterprise inc." | ||||||
|  | fullname = _('B.A.L.') | ||||||
|  | description = ''.join([ | ||||||
|  |     "<img src='",bal_resources.icon_path('bal16x16.png'),"'>", _("Bitcoin After Life"), '<br/>', | ||||||
|  |     _("For more information, visit"), | ||||||
|  |     " <a href=\"https://bitcoin-after.life/\">https://bitcoin-after.life/</a><br/>", | ||||||
|  |     "<p style='font-size:8pt;vertialAlign:bottom'>Version: ", _version(),"</p>" | ||||||
|  | ]) | ||||||
|  | #available_for = ['qt', 'cmdline', 'qml'] | ||||||
|  | available_for = ['qt'] | ||||||
							
								
								
									
										131
									
								
								bal.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										131
									
								
								bal.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,131 @@ | |||||||
|  | import random | ||||||
|  | import os | ||||||
|  | from hashlib import sha256 | ||||||
|  | from typing import NamedTuple, Optional, Dict, Tuple | ||||||
|  | 
 | ||||||
|  | from electrum.plugin import BasePlugin | ||||||
|  | from electrum.util import to_bytes, bfh | ||||||
|  | from electrum import json_db | ||||||
|  | from electrum.transaction import tx_from_any | ||||||
|  | 
 | ||||||
|  | from . import util as Util | ||||||
|  | from . import willexecutors as Willexecutors | ||||||
|  | import os | ||||||
|  | json_db.register_dict('heirs', tuple, None) | ||||||
|  | json_db.register_dict('will', lambda x: get_will(x), None) | ||||||
|  | json_db.register_dict('will_settings', lambda x:x, None) | ||||||
|  | from electrum.logging import get_logger | ||||||
|  | def get_will(x): | ||||||
|  |     try: | ||||||
|  |         #print("______________________________________________________________________________________________________") | ||||||
|  |         #print(x) | ||||||
|  |         x['tx']=tx_from_any(x['tx']) | ||||||
|  |     except Exception as e: | ||||||
|  |         #Util.print_var(x) | ||||||
|  |         raise e | ||||||
|  | 
 | ||||||
|  |     return x | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class BalPlugin(BasePlugin): | ||||||
|  |     LOCKTIME_TIME = "bal_locktime_time" | ||||||
|  |     LOCKTIME_BLOCKS = "bal_locktime_blocks" | ||||||
|  |     LOCKTIMEDELTA_TIME = "bal_locktimedelta_time" | ||||||
|  |     LOCKTIMEDELTA_BLOCKS = "bal_locktimedelta_blocks" | ||||||
|  |     TX_FEES = "bal_tx_fees" | ||||||
|  |     BROADCAST = "bal_broadcast" | ||||||
|  |     ASK_BROADCAST = "bal_ask_broadcast" | ||||||
|  |     INVALIDATE = "bal_invalidate" | ||||||
|  |     ASK_INVALIDATE = "bal_ask_invalidate" | ||||||
|  |     PREVIEW = "bal_preview" | ||||||
|  |     SAVE_TXS = "bal_save_txs" | ||||||
|  |     WILLEXECUTORS = "bal_willexecutors" | ||||||
|  |     PING_WILLEXECUTORS = "bal_ping_willexecutors" | ||||||
|  |     ASK_PING_WILLEXECUTORS = "bal_ask_ping_willexecutors" | ||||||
|  |     NO_WILLEXECUTOR = "bal_no_willexecutor" | ||||||
|  |     HIDE_REPLACED = "bal_hide_replaced" | ||||||
|  |     HIDE_INVALIDATED = "bal_hide_invalidated" | ||||||
|  |     ALLOW_REPUSH = "bal_allow_repush" | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     DEFAULT_SETTINGS={ | ||||||
|  |         LOCKTIME_TIME: 90, | ||||||
|  |         LOCKTIME_BLOCKS: 144*90, | ||||||
|  |         LOCKTIMEDELTA_TIME: 7, | ||||||
|  |         LOCKTIMEDELTA_BLOCKS:144*7, | ||||||
|  |         TX_FEES: 100, | ||||||
|  |         BROADCAST: True, | ||||||
|  |         ASK_BROADCAST: True, | ||||||
|  |         INVALIDATE: True, | ||||||
|  |         ASK_INVALIDATE: True, | ||||||
|  |         PREVIEW: True, | ||||||
|  |         SAVE_TXS: True, | ||||||
|  |         PING_WILLEXECUTORS: False, | ||||||
|  |         ASK_PING_WILLEXECUTORS: False, | ||||||
|  |         NO_WILLEXECUTOR: False, | ||||||
|  |         HIDE_REPLACED:True, | ||||||
|  |         HIDE_INVALIDATED:True, | ||||||
|  |         ALLOW_REPUSH: False, | ||||||
|  |         WILLEXECUTORS:  { | ||||||
|  |             'http://bitcoin-after.life:9137': { | ||||||
|  |                 "base_fee": 100000, | ||||||
|  |                 "status": "New", | ||||||
|  |                 "info":"Bitcoin After Life Will Executor", | ||||||
|  |                 "address":"bcrt1qa5cntu4hgadw8zd3n6sq2nzjy34sxdtd9u0gp7" | ||||||
|  |             } | ||||||
|  |         }, | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     LATEST_VERSION = '1' | ||||||
|  |     KNOWN_VERSIONS = ('0', '1') | ||||||
|  |     assert LATEST_VERSION in KNOWN_VERSIONS | ||||||
|  | 
 | ||||||
|  |     SIZE = (159, 97) | ||||||
|  | 
 | ||||||
|  |     def __init__(self, parent, config, name): | ||||||
|  |         self.logger= get_logger(__name__) | ||||||
|  |         BasePlugin.__init__(self, parent, config, name) | ||||||
|  |         self.base_dir = os.path.join(config.electrum_path(), 'bal') | ||||||
|  |         self.logger.info(self.base_dir) | ||||||
|  |         self.parent = parent | ||||||
|  |         self.config = config | ||||||
|  |         self.name = name | ||||||
|  |         self._hide_invalidated= self.config_get(self.HIDE_INVALIDATED) | ||||||
|  |         self._hide_replaced= self.config_get(self.HIDE_REPLACED) | ||||||
|  |         self.plugin_dir = os.path.split(os.path.realpath(__file__))[0] | ||||||
|  | 
 | ||||||
|  |     def resource_path(self,*parts): | ||||||
|  |         return os.path.join(self.plugin_dir, *parts) | ||||||
|  | 
 | ||||||
|  |     def config_get(self,key): | ||||||
|  |         v = self.config.get(key,None) | ||||||
|  |         if v is None: | ||||||
|  |             self.config.set_key(key,self.DEFAULT_SETTINGS[key],save=True) | ||||||
|  |             v = self.DEFAULT_SETTINGS[key] | ||||||
|  |         return v | ||||||
|  | 
 | ||||||
|  |     def hide_invalidated(self): | ||||||
|  |         self._hide_invalidated = not self._hide_invalidated | ||||||
|  |         self.config.set_key(BalPlugin.HIDE_INVALIDATED,self.hide_invalidated,save=True) | ||||||
|  | 
 | ||||||
|  |     def hide_replaced(self): | ||||||
|  |         self._hide_replaced = not self._hide_replaced | ||||||
|  |         self.config.set_key(BalPlugin.HIDE_REPLACED,self.hide_invalidated,save=True) | ||||||
|  | 
 | ||||||
|  |     def default_will_settings(self): | ||||||
|  |         return { | ||||||
|  |             'tx_fees':100,  | ||||||
|  |             'threshold':'180d', | ||||||
|  |             'locktime':'1y', | ||||||
|  |         } | ||||||
|  |     def validate_will_settings(self,will_settings): | ||||||
|  |         if int(will_settings.get('tx_fees',1))<1: | ||||||
|  |             will_settings['tx_fees']=1 | ||||||
|  |         if not will_settings.get('threshold'): | ||||||
|  |             will_settings['threshold']='180d' | ||||||
|  |         if not will_settings.get('locktime')=='': | ||||||
|  |             will_settings['locktime']='1y' | ||||||
|  |         return will_settings | ||||||
|  | 
 | ||||||
							
								
								
									
										15
									
								
								bal_resources.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								bal_resources.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,15 @@ | |||||||
|  | import os | ||||||
|  | 
 | ||||||
|  | PLUGIN_DIR = os.path.split(os.path.realpath(__file__))[0] | ||||||
|  | DEFAULT_ICON = 'bal32x32.png' | ||||||
|  | DEFAULT_ICON_PATH = 'icons' | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def icon_path(icon_basename: str = DEFAULT_ICON): | ||||||
|  |     path = resource_path(DEFAULT_ICON_PATH,icon_basename) | ||||||
|  |     return path | ||||||
|  | 
 | ||||||
|  | def resource_path(*parts): | ||||||
|  |     return os.path.join(PLUGIN_DIR, *parts) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
							
								
								
									
										89
									
								
								balqt/amountedit.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								balqt/amountedit.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,89 @@ | |||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  | 
 | ||||||
|  | from typing import Union | ||||||
|  | from decimal import Decimal | ||||||
|  | 
 | ||||||
|  | from . import qt_resources | ||||||
|  | if qt_resources.QT_VERSION == 5: | ||||||
|  |     from PyQt5.QtWidgets import (QLineEdit, QStyle, QStyleOptionFrame, QSizePolicy) | ||||||
|  |     from PyQt5.QtGui import QPalette, QPainter | ||||||
|  |     from PyQt5.QtCore import pyqtSignal, Qt, QSize | ||||||
|  | else: | ||||||
|  |     from PyQt6.QtWidgets import (QLineEdit, QStyle, QStyleOptionFrame, QSizePolicy) | ||||||
|  |     from PyQt6.QtGui import QPalette, QPainter | ||||||
|  |     from PyQt6.QtCore import pyqtSignal, Qt, QSize | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | from electrum.util import (format_satoshis_plain, decimal_point_to_base_unit_name, | ||||||
|  |                            FEERATE_PRECISION, quantize_feerate, DECIMAL_POINT, UI_UNIT_NAME_FEERATE_SAT_PER_VBYTE) | ||||||
|  | 
 | ||||||
|  | from electrum.gui.qt.amountedit import BTCAmountEdit, char_width_in_lineedit, ColorScheme | ||||||
|  | 
 | ||||||
|  | _NOT_GIVEN = object()  # sentinel value | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class PercAmountEdit(BTCAmountEdit): | ||||||
|  |     def __init__(self, decimal_point, is_int=False, parent=None, *, max_amount=_NOT_GIVEN): | ||||||
|  |         super().__init__(decimal_point, is_int, parent, max_amount=max_amount) | ||||||
|  | 
 | ||||||
|  |     def numbify(self): | ||||||
|  |         text = self.text().strip() | ||||||
|  |         if text == '!': | ||||||
|  |             self.shortcut.emit() | ||||||
|  |             return | ||||||
|  |         pos = self.cursorPosition() | ||||||
|  |         chars = '0123456789%' | ||||||
|  |         chars += DECIMAL_POINT | ||||||
|  |          | ||||||
|  |         s = ''.join([i for i in text if i in chars]) | ||||||
|  | 
 | ||||||
|  |         if '%' in s: | ||||||
|  |             self.is_perc=True | ||||||
|  |             s=s.replace('%','') | ||||||
|  |         else: | ||||||
|  |             self.is_perc=False | ||||||
|  | 
 | ||||||
|  |         if DECIMAL_POINT in s: | ||||||
|  |             p = s.find(DECIMAL_POINT) | ||||||
|  |             s = s.replace(DECIMAL_POINT, '') | ||||||
|  |             s = s[:p] + DECIMAL_POINT + s[p:p+8] | ||||||
|  |         if self.is_perc: | ||||||
|  |             s+='%' | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         #if self.max_amount: | ||||||
|  |         #    if (amt := self._get_amount_from_text(s)) and amt >= self.max_amount: | ||||||
|  |         #        s = self._get_text_from_amount(self.max_amount) | ||||||
|  |         self.setText(s) | ||||||
|  |         # setText sets Modified to False.  Instead we want to remember | ||||||
|  |         # if updates were because of user modification. | ||||||
|  |         self.setModified(self.hasFocus()) | ||||||
|  |         self.setCursorPosition(pos) | ||||||
|  |         #if len(s>0) | ||||||
|  |         #    self.drawText("") | ||||||
|  | 
 | ||||||
|  |     def _get_amount_from_text(self, text: str) -> Union[None, Decimal, int]: | ||||||
|  |         try: | ||||||
|  |             text = text.replace(DECIMAL_POINT, '.') | ||||||
|  |             text = text.replace('%', '') | ||||||
|  |             return (Decimal)(text) | ||||||
|  |         except Exception: | ||||||
|  |             return None | ||||||
|  | 
 | ||||||
|  |     def _get_text_from_amount(self, amount): | ||||||
|  |         out = super()._get_text_from_amount(amount) | ||||||
|  |         if self.is_perc: out+='%' | ||||||
|  |         return out | ||||||
|  | 
 | ||||||
|  |     def paintEvent(self, event): | ||||||
|  |         QLineEdit.paintEvent(self, event) | ||||||
|  |         if self.base_unit: | ||||||
|  |             panel = QStyleOptionFrame() | ||||||
|  |             self.initStyleOption(panel) | ||||||
|  |             textRect = self.style().subElementRect(QStyle.SubElement.SE_LineEditContents, panel, self) | ||||||
|  |             textRect.adjust(2, 0, -10, 0) | ||||||
|  |             painter = QPainter(self) | ||||||
|  |             painter.setPen(ColorScheme.GRAY.as_color()) | ||||||
|  |             if len(self.text())==0: | ||||||
|  |                 painter.drawText(textRect, int(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter), self.base_unit() + " or perc value") | ||||||
|  | 
 | ||||||
							
								
								
									
										119
									
								
								balqt/baldialog.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								balqt/baldialog.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,119 @@ | |||||||
|  | from typing import Callable,Any | ||||||
|  | 
 | ||||||
|  | from . import qt_resources | ||||||
|  | if qt_resources.QT_VERSION == 5: | ||||||
|  |     from PyQt5.QtCore import Qt,pyqtSignal | ||||||
|  |     from PyQt5.QtWidgets import QLabel, QVBoxLayout, QCheckBox | ||||||
|  | else: | ||||||
|  |     from PyQt6.QtCore import Qt,pyqtSignal | ||||||
|  |     from PyQt6.QtWidgets import QLabel, QVBoxLayout, QCheckBox | ||||||
|  | 
 | ||||||
|  | from electrum.gui.qt.util import WindowModalDialog, TaskThread | ||||||
|  | from electrum.i18n import _ | ||||||
|  | from electrum.logging import get_logger | ||||||
|  | 
 | ||||||
|  | _logger = get_logger(__name__) | ||||||
|  | 
 | ||||||
|  | class BalDialog(WindowModalDialog): | ||||||
|  | 
 | ||||||
|  |     def __init__(self,parent,title=None, icon = 'bal32x32.png'): | ||||||
|  |         self.parent=parent | ||||||
|  |         WindowModalDialog.__init__(self,self.parent,title) | ||||||
|  |         self.setWindowIcon(qt_resources.read_QIcon(icon)) | ||||||
|  | 
 | ||||||
|  | class BalWaitingDialog(BalDialog): | ||||||
|  |     updatemessage=pyqtSignal([str], arguments=['message']) | ||||||
|  |     def __init__(self, bal_window: 'BalWindow', message: str, task, on_success=None, on_error=None, on_cancel=None,exe=True): | ||||||
|  |         assert bal_window | ||||||
|  |         BalDialog.__init__(self, bal_window.window, _("Please wait")) | ||||||
|  |         self.message_label = QLabel(message) | ||||||
|  |         vbox = QVBoxLayout(self) | ||||||
|  |         vbox.addWidget(self.message_label) | ||||||
|  |         self.updatemessage.connect(self.update_message) | ||||||
|  |         if on_cancel: | ||||||
|  |             self.cancel_button = CancelButton(self) | ||||||
|  |             self.cancel_button.clicked.connect(on_cancel) | ||||||
|  |             vbox.addLayout(Buttons(self.cancel_button)) | ||||||
|  |         self.accepted.connect(self.on_accepted) | ||||||
|  |         self.task=task | ||||||
|  |         self.on_success = on_success | ||||||
|  |         self.on_error = on_error | ||||||
|  |         self.on_cancel = on_cancel | ||||||
|  |         if exe: | ||||||
|  |             self.exe() | ||||||
|  | 
 | ||||||
|  |     def exe(self): | ||||||
|  |         self.thread = TaskThread(self) | ||||||
|  |         self.thread.finished.connect(self.deleteLater)  # see #3956 | ||||||
|  |         self.thread.finished.connect(self.finished) | ||||||
|  |         self.thread.add(self.task, self.on_success, self.accept, self.on_error) | ||||||
|  |         self.exec() | ||||||
|  | 
 | ||||||
|  |     def hello(self): | ||||||
|  |         pass | ||||||
|  |     def finished(self): | ||||||
|  |         _logger.info("finished") | ||||||
|  |     def wait(self): | ||||||
|  |         self.thread.wait() | ||||||
|  | 
 | ||||||
|  |     def on_accepted(self): | ||||||
|  |         self.thread.stop() | ||||||
|  |     def update_message(self,msg): | ||||||
|  |         self.message_label.setText(msg) | ||||||
|  | 
 | ||||||
|  |     def update(self, msg): | ||||||
|  |         self.updatemessage.emit(msg) | ||||||
|  | 
 | ||||||
|  |     def getText(self): | ||||||
|  |          return self.message_label.text() | ||||||
|  | 
 | ||||||
|  |     def closeEvent(self,event): | ||||||
|  |         self.thread.stop() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class BalBlockingWaitingDialog(BalDialog): | ||||||
|  |     def __init__(self, bal_window: 'BalWindow', message: str, task: Callable[[], Any]): | ||||||
|  |         BalDialog.__init__(self, bal_window, _("Please wait")) | ||||||
|  |         self.message_label = QLabel(message) | ||||||
|  |         vbox = QVBoxLayout(self) | ||||||
|  |         vbox.addWidget(self.message_label) | ||||||
|  |         self.finished.connect(self.deleteLater)  # see #3956 | ||||||
|  |         # show popup | ||||||
|  |         self.show() | ||||||
|  |         # refresh GUI; needed for popup to appear and for message_label to get drawn | ||||||
|  |         QCoreApplication.processEvents() | ||||||
|  |         QCoreApplication.processEvents() | ||||||
|  |         try: | ||||||
|  |             # block and run given task | ||||||
|  |             task() | ||||||
|  |         finally: | ||||||
|  |             # close popup | ||||||
|  |             self.accept() | ||||||
|  | 
 | ||||||
|  | class bal_checkbox(QCheckBox): | ||||||
|  |     def __init__(self, plugin,variable,window=None): | ||||||
|  |         QCheckBox.__init__(self) | ||||||
|  |         self.setChecked(plugin.config_get(variable)) | ||||||
|  |         window=window | ||||||
|  |         def on_check(v): | ||||||
|  |             plugin.config.set_key(variable, v == Qt.CheckState.Checked, save=True) | ||||||
|  |             if window: | ||||||
|  |                 plugin._hide_invalidated= plugin.config_get(plugin.HIDE_INVALIDATED) | ||||||
|  |                 plugin._hide_replaced= plugin.config_get(plugin.HIDE_REPLACED) | ||||||
|  | 
 | ||||||
|  |                 window.update_all() | ||||||
|  |         self.stateChanged.connect(on_check) | ||||||
|  | 
 | ||||||
|  |     #TODO IMPLEMENT PREVIEW DIALOG | ||||||
|  |     #tx list display txid, willexecutor, qrcode, button to sign | ||||||
|  |     #   :def preview_dialog(self, txs): | ||||||
|  |     def preview_dialog(self, txs): | ||||||
|  |         w=PreviewDialog(self,txs) | ||||||
|  |         w.exec() | ||||||
|  |         return w | ||||||
|  |     def add_info_from_will(self,tx): | ||||||
|  |         for input in tx.inputs(): | ||||||
|  |             pass | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
							
								
								
									
										384
									
								
								balqt/closedialog.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										384
									
								
								balqt/closedialog.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,384 @@ | |||||||
|  | from .baldialog import BalDialog | ||||||
|  | from . import qt_resources | ||||||
|  | 
 | ||||||
|  | if qt_resources.QT_VERSION == 5: | ||||||
|  |     from PyQt5.QtCore import Qt | ||||||
|  |     from PyQt5.QtWidgets import QLabel, QVBoxLayout, QCheckBox,QWidget | ||||||
|  |     from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject,QEventLoop | ||||||
|  | else: | ||||||
|  |     from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject,QEventLoop | ||||||
|  |     from PyQt6.QtCore import Qt | ||||||
|  |     from PyQt6.QtWidgets import QLabel, QVBoxLayout, QCheckBox, QWidget | ||||||
|  | import time | ||||||
|  | from electrum.i18n import _ | ||||||
|  | from electrum.gui.qt.util import WindowModalDialog, TaskThread | ||||||
|  | from electrum.network import Network,TxBroadcastError, BestEffortRequestFailed | ||||||
|  | from electrum.logging import get_logger | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | from functools import partial | ||||||
|  | import copy | ||||||
|  | 
 | ||||||
|  | from .. import util as Util | ||||||
|  | from .. import will as Will | ||||||
|  | 
 | ||||||
|  | from .. import willexecutors as Willexecutors | ||||||
|  | _logger = get_logger(__name__) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class BalCloseDialog(BalDialog): | ||||||
|  |     updatemessage=pyqtSignal() | ||||||
|  |     def __init__(self,bal_window): | ||||||
|  |         BalDialog.__init__(self,bal_window.window,"Closing BAL") | ||||||
|  |         self.updatemessage.connect(self.update) | ||||||
|  |         self.bal_window=bal_window | ||||||
|  |         self.message_label = QLabel("Closing BAL:") | ||||||
|  |         self.vbox = QVBoxLayout(self) | ||||||
|  |         self.vbox.addWidget(self.message_label) | ||||||
|  |         self.qwidget=QWidget() | ||||||
|  |         self.vbox.addWidget(self.qwidget) | ||||||
|  |         self.labels=[] | ||||||
|  |         #self.checking.connect(self.msg_set_checking) | ||||||
|  |         #self.invalidating.connect(self.msg_set_invalidating) | ||||||
|  |         #self.building.connect(self.msg_set_building) | ||||||
|  |         #self.signing.connect(self.msg_set_signing) | ||||||
|  |         #self.pushing.connect(self.msg_set_pushing) | ||||||
|  | 
 | ||||||
|  |         #self.askpassword.connect(self.ask_password) | ||||||
|  |         #self.passworddone.connect(self.password_done) | ||||||
|  |         self.check_row = None | ||||||
|  |         self.inval_row = None | ||||||
|  |         self.build_row = None | ||||||
|  |         self.sign_row = None | ||||||
|  |         self.push_row = None | ||||||
|  |         self.network = Network.get_instance() | ||||||
|  |         self._stopping = False | ||||||
|  |         self.thread = TaskThread(self) | ||||||
|  |         self.thread.finished.connect(self.task_finished)  # see #3956 | ||||||
|  |     def task_finished(self): | ||||||
|  |         pass | ||||||
|  |         #_logger.trace("task finished") | ||||||
|  |          | ||||||
|  |     def close_plugin_task(self): | ||||||
|  |         _logger.debug("close task to be started") | ||||||
|  |         self.thread.add(self.task_phase1,on_success=self.on_success_phase1,on_done=self.on_accept,on_error=self.on_error_phase1) | ||||||
|  |         self.show() | ||||||
|  |         self.exec() | ||||||
|  | 
 | ||||||
|  |     def task_phase1(self): | ||||||
|  |         _logger.debug("close plugin phase 1 started") | ||||||
|  |         try: | ||||||
|  |             self.bal_window.init_class_variables() | ||||||
|  |         except Will.NoHeirsException: | ||||||
|  |             return False, None | ||||||
|  |         self.msg_set_status("checking variables","Waiting") | ||||||
|  |         try: | ||||||
|  |             Will.check_amounts(self.bal_window.heirs,self.bal_window.willexecutors,self.bal_window.window.wallet.get_utxos(),self.bal_window.date_to_check,self.bal_window.window.wallet.dust_threshold()) | ||||||
|  |         except Will.AmountException: | ||||||
|  |             self.msg_edit_row('<font color="#ff0000">'+_("In the inheritance process, the entire wallet will always be fully emptied. Your settings require an adjustment of the amounts"+"</font>")) | ||||||
|  |             #self.bal_window.show_warning(_("In the inheritance process, the entire wallet will always be fully emptied. Your settings require an adjustment of the amounts"),parent=self) | ||||||
|  | 
 | ||||||
|  |         self.msg_set_checking() | ||||||
|  |         have_to_build=False | ||||||
|  |         try: | ||||||
|  |             self.bal_window.check_will() | ||||||
|  |             self.msg_set_checking('Ok') | ||||||
|  |         except Will.WillExpiredException as e: | ||||||
|  |             self.msg_set_checking("Expired") | ||||||
|  |             fee_per_byte=self.bal_window.will_settings.get('tx_fees',1) | ||||||
|  |             return None, Will.invalidate_will(self.bal_window.willitems,self.bal_window.wallet,fee_per_byte) | ||||||
|  |         except Will.NoHeirsException: | ||||||
|  |             self.msg_set_checking("No Heirs") | ||||||
|  |         except Will.NotCompleteWillException as e: | ||||||
|  |             message = False | ||||||
|  |             have_to_build=True | ||||||
|  |             if isinstance(e,Will.HeirChangeException): | ||||||
|  |                 message ="Heirs changed:" | ||||||
|  |             elif isinstance(e,Will.WillExecutorNotPresent): | ||||||
|  |                 message = "Will-Executor not present" | ||||||
|  |             elif isinstance(e,Will.WillexecutorChangeException): | ||||||
|  |                 message = "Will-Executor changed" | ||||||
|  |             elif isinstance(e,Will.TxFeesChangedException): | ||||||
|  |                 message = "Txfees are changed" | ||||||
|  |             elif isinstance(e,Will.HeirNotFoundException): | ||||||
|  |                 message = "Heir not found" | ||||||
|  |             if message: | ||||||
|  |                 self.msg_set_checking(message) | ||||||
|  |             else: | ||||||
|  |                 self.msg_set_checking("New") | ||||||
|  | 
 | ||||||
|  |         if have_to_build: | ||||||
|  |             self.msg_set_building() | ||||||
|  |             try: | ||||||
|  |                 self.bal_window.build_will() | ||||||
|  |                 self.bal_window.check_will() | ||||||
|  |                 for wid in Will.only_valid(self.bal_window.willitems): | ||||||
|  |                     self.bal_window.wallet.set_label(wid,"BAL Transaction") | ||||||
|  |                 self.msg_set_building("Ok") | ||||||
|  |             except Exception as e: | ||||||
|  |                 self.msg_set_building(self.msg_error(e)) | ||||||
|  |                 return False,None | ||||||
|  |         have_to_sign = False | ||||||
|  |         for wid in Will.only_valid(self.bal_window.willitems): | ||||||
|  |             if not self.bal_window.willitems[wid].get_status("COMPLETE"): | ||||||
|  |                 have_to_sign = True | ||||||
|  |                 break | ||||||
|  |         return have_to_sign, None | ||||||
|  |          | ||||||
|  |     def on_accept(self): | ||||||
|  |         pass | ||||||
|  | 
 | ||||||
|  |     def on_accept_phase2(self): | ||||||
|  |         pass | ||||||
|  | 
 | ||||||
|  |     def on_error_push(self): | ||||||
|  |         pass | ||||||
|  | 
 | ||||||
|  |     def wait(self,secs): | ||||||
|  |         wait_row=None | ||||||
|  |         for i in range(secs,0,-1): | ||||||
|  |             if self._stopping: | ||||||
|  |                 return | ||||||
|  |             wait_row = self.msg_edit_row(f"Please wait {i}secs",      wait_row) | ||||||
|  |             time.sleep(1) | ||||||
|  |         self.msg_del_row(wait_row) | ||||||
|  | 
 | ||||||
|  |     def loop_broadcast_invalidating(self,tx): | ||||||
|  |         self.msg_set_invalidating("Broadcasting") | ||||||
|  |         try: | ||||||
|  |             tx.add_info_from_wallet(self.bal_window.wallet) | ||||||
|  |             self.network.run_from_another_thread(tx.add_info_from_network(self.network)) | ||||||
|  |             txid = self.network.run_from_another_thread(self.network.broadcast_transaction(tx,timeout=120),timeout=120) | ||||||
|  |             self.msg_set_invalidating("Ok") | ||||||
|  |             if not txid: | ||||||
|  |                 _logger.debug(f"should not be none txid: {txid}") | ||||||
|  |              | ||||||
|  | 
 | ||||||
|  |         except TxBroadcastError as e: | ||||||
|  |             _logger.error(e) | ||||||
|  |             msg = e.get_message_for_gui() | ||||||
|  |             self.msg_set_invalidating(self.msg_error(msg)) | ||||||
|  |         except BestEffortRequestFailed as e: | ||||||
|  |             self.msg_set_invalidating(self.msg_error(e)) | ||||||
|  |         #    self.loop_broadcast_invalidating(tx) | ||||||
|  | 
 | ||||||
|  |     def loop_push(self): | ||||||
|  |         self.msg_set_pushing("Broadcasting") | ||||||
|  |         retry = False | ||||||
|  |         try: | ||||||
|  |             willexecutors=Willexecutors.get_willexecutor_transactions(self.bal_window.willitems) | ||||||
|  |             for url,willexecutor in willexecutors.items(): | ||||||
|  |                 try: | ||||||
|  |                     if Willexecutors.is_selected(self.bal_window.willexecutors.get(url)): | ||||||
|  |                         _logger.debug(f"{url}: {willexecutor}") | ||||||
|  |                         if not Willexecutors.push_transactions_to_willexecutor(willexecutor): | ||||||
|  |                             for wid in willexecutor['txsids']: | ||||||
|  |                                 self.bal_window.willitems[wid].set_status('PUSH_FAIL',True) | ||||||
|  |                             retry=True | ||||||
|  |                         else: | ||||||
|  |                             for wid in willexecutor['txsids']: | ||||||
|  |                                 self.bal_window.willitems[wid].set_status('PUSHED',True) | ||||||
|  |                 except Willexecutors.AlreadyPresentException: | ||||||
|  |                     for wid in willexecutor['txsids']: | ||||||
|  |                         row = self.msg_edit_row("checking {} - {} : {}".format(self.bal_window.willitems[wid].we['url'],wid, "Waiting")) | ||||||
|  |                         self.bal_window.willitems[wid].check_willexecutor() | ||||||
|  |                         row = self.msg_edit_row("checked {} - {} : {}".format(self.bal_window.willitems[wid].we['url'],wid,self.bal_window.willitems[wid].get_status("CHECKED" )),row) | ||||||
|  |                          | ||||||
|  |                              | ||||||
|  |                 except Exception as e: | ||||||
|  | 
 | ||||||
|  |                     _logger.error(e) | ||||||
|  |                     raise e | ||||||
|  |             if retry: | ||||||
|  |                 raise Exception("retry") | ||||||
|  | 
 | ||||||
|  |         except Exception as e: | ||||||
|  |             self.msg_set_pushing(self.msg_error(e)) | ||||||
|  |             self.wait(10) | ||||||
|  |             if not self._stopping: | ||||||
|  |                 self.loop_push() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     def invalidate_task(self,tx,password): | ||||||
|  |         _logger.debug(f"invalidate tx: {tx}") | ||||||
|  |         tx = self.bal_window.wallet.sign_transaction(tx,password) | ||||||
|  |         try: | ||||||
|  |             if tx: | ||||||
|  |                 if tx.is_complete(): | ||||||
|  |                     self.loop_broadcast_invalidating(tx) | ||||||
|  |                     self.wait(5) | ||||||
|  |                 else: | ||||||
|  |                     raise | ||||||
|  |             else: | ||||||
|  |                 raise | ||||||
|  |         except Exception as e: | ||||||
|  |             self.msg_set_invalidating("Error") | ||||||
|  |             raise Exception("Impossible to sign") | ||||||
|  |     def on_success_invalidate(self,success): | ||||||
|  |         self.thread.add(self.task_phase1,on_success=self.on_success_phase1,on_done=self.on_accept,on_error=self.on_error_phase1) | ||||||
|  |     def on_error(self,error): | ||||||
|  |         _logger.error(error) | ||||||
|  |         pass | ||||||
|  |     def on_success_phase1(self,result): | ||||||
|  |         self.have_to_sign,tx = list(result) | ||||||
|  |         #if have_to_sign is False and tx is None: | ||||||
|  |             #self._stopping=True | ||||||
|  |             #self.on_success_phase2() | ||||||
|  |         #   return | ||||||
|  |         print("have to sign",self.have_to_sign) | ||||||
|  |         password=None | ||||||
|  |         if self.have_to_sign is None: | ||||||
|  |             self.msg_set_invalidating() | ||||||
|  |            #need to sign invalidate and restart phase 1 | ||||||
|  | 
 | ||||||
|  |             password = self.bal_window.get_wallet_password("Invalidate your old will",parent=self.bal_window.window) | ||||||
|  |             if password is False: | ||||||
|  |                 self.msg_set_invalidating("Aborted") | ||||||
|  |                 self.wait(3) | ||||||
|  |                 self.close() | ||||||
|  |                 return | ||||||
|  |             self.thread.add(partial(self.invalidate_task,tx,password),on_success=self.on_success_invalidate, on_done=self.on_accept, on_error=self.on_error) | ||||||
|  |              | ||||||
|  |             return | ||||||
|  |          | ||||||
|  |         elif self.have_to_sign: | ||||||
|  |             password = self.bal_window.get_wallet_password("Sign your will",parent=self.bal_window.window) | ||||||
|  |             if password is False: | ||||||
|  |                 self.msg_set_signing('Aborted') | ||||||
|  |         else: | ||||||
|  |             self.msg_set_signing('Nothing to do') | ||||||
|  |         self.thread.add(partial(self.task_phase2,password),on_success=self.on_success_phase2,on_done=self.on_accept_phase2,on_error=self.on_error_phase2) | ||||||
|  |         return | ||||||
|  |      | ||||||
|  |     def on_success_phase2(self,arg=False): | ||||||
|  |         self.thread.stop() | ||||||
|  |         self.bal_window.save_willitems() | ||||||
|  |         self.msg_edit_row("Finished") | ||||||
|  |         self.close() | ||||||
|  | 
 | ||||||
|  |     def closeEvent(self,event): | ||||||
|  |         self._stopping=True | ||||||
|  |         self.thread.stop() | ||||||
|  | 
 | ||||||
|  |     def task_phase2(self,password): | ||||||
|  |         if self.have_to_sign: | ||||||
|  |             try: | ||||||
|  |                 if txs:=self.bal_window.sign_transactions(password): | ||||||
|  |                     for txid,tx in txs.items(): | ||||||
|  |                         self.bal_window.willitems[txid].tx = copy.deepcopy(tx) | ||||||
|  |                     self.bal_window.save_willitems() | ||||||
|  |                     self.msg_set_signing("Ok") | ||||||
|  |             except Exception as e: | ||||||
|  |                 self.msg_set_signing(self.msg_error(e)) | ||||||
|  | 
 | ||||||
|  |         self.msg_set_pushing() | ||||||
|  |         have_to_push = False | ||||||
|  |         for wid in Will.only_valid(self.bal_window.willitems): | ||||||
|  |             w=self.bal_window.willitems[wid] | ||||||
|  |             if w.we and w.get_status("COMPLETE") and not w.get_status("PUSHED"): | ||||||
|  |                 have_to_push = True | ||||||
|  |         if not have_to_push: | ||||||
|  |             self.msg_set_pushing("Nothing to do") | ||||||
|  |         else: | ||||||
|  |             try: | ||||||
|  |                 self.loop_push() | ||||||
|  |                 self.msg_set_pushing("Ok") | ||||||
|  | 
 | ||||||
|  |             except Exception as e: | ||||||
|  |                 self.msg_set_pushing(self.msg_error(e)) | ||||||
|  |         self.msg_edit_row("Ok") | ||||||
|  |         self.wait(5) | ||||||
|  | 
 | ||||||
|  |     def on_error_phase1(self,error): | ||||||
|  |         _logger.error(f"error phase1: {error}") | ||||||
|  | 
 | ||||||
|  |     def on_error_phase2(self,error): | ||||||
|  |         _logger.error("error phase2: { error}") | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     def msg_set_checking(self, status = None, row = None): | ||||||
|  |         row = self.check_row if row is None else row | ||||||
|  |         self.check_row = self.msg_set_status("Checking your will",    row,  status) | ||||||
|  | 
 | ||||||
|  |     def msg_set_invalidating(self, status = None, row = None): | ||||||
|  |         row = self.inval_row if row is None else row | ||||||
|  |         self.inval_row = self.msg_set_status("Invalidating old will",    self.inval_row,  status) | ||||||
|  | 
 | ||||||
|  |     def msg_set_building(self, status = None, row = None): | ||||||
|  |         row = self.build_row if row is None else row | ||||||
|  |         self.build_row = self.msg_set_status("Building your will",    self.build_row,  status) | ||||||
|  | 
 | ||||||
|  |     def msg_set_signing(self, status = None, row = None): | ||||||
|  |         row = self.sign_row if row is None else row | ||||||
|  |         self.sign_row = self.msg_set_status("Signing your will",      self.sign_row,   status) | ||||||
|  | 
 | ||||||
|  |     def msg_set_pushing(self, status = None, row = None): | ||||||
|  |         row = self.push_row if row is None else row | ||||||
|  |         self.push_row = self.msg_set_status("Broadcasting your will to executors",      self.push_row,   status) | ||||||
|  | 
 | ||||||
|  |     def msg_set_waiting(self, status = None, row = None): | ||||||
|  |         row = self.wait_row if row is None else row | ||||||
|  |         self.wait_row = self.msg_edit_row(f"Please wait {status}secs",      self.wait_row) | ||||||
|  | 
 | ||||||
|  |     def msg_error(self,e): | ||||||
|  |         return "Error: {}".format(e) | ||||||
|  | 
 | ||||||
|  |     def msg_set_status(self,msg,row,status=None): | ||||||
|  |         status= "Wait" if status is None else status | ||||||
|  |         line="{}:\t{}".format(_(msg), status) | ||||||
|  |         return self.msg_edit_row(line,row) | ||||||
|  |          | ||||||
|  |         #return v$msg_edit_row("{}:\t{}".format(_(msg), status), row) | ||||||
|  |      | ||||||
|  |     def ask_password(self,msg=None): | ||||||
|  |         self.password=self.bal_window.get_wallet_password(msg,parent=self) | ||||||
|  | 
 | ||||||
|  |     def msg_edit_row(self,line,row=None): | ||||||
|  |         _logger.debug(f"{row},{line}") | ||||||
|  |          | ||||||
|  |         #msg=self.get_text() | ||||||
|  |         #rows=msg.split("\n") | ||||||
|  |         #try: | ||||||
|  |         #    rows[row]=line | ||||||
|  |         #except Exception as e: | ||||||
|  |         #    rows.append(line) | ||||||
|  |         #row=len(rows)-1 | ||||||
|  |         #self.update("\n".join(rows)) | ||||||
|  |      | ||||||
|  |         #return row | ||||||
|  | 
 | ||||||
|  |     #def msg_edit_label(self,line,row=None): | ||||||
|  |         #_logger.trace(f"{row},{line}") | ||||||
|  |         | ||||||
|  |         #msg=self.get_text() | ||||||
|  |         #rows=msg.split("\n") | ||||||
|  |         try: | ||||||
|  |             self.labels[row]=line | ||||||
|  |         except Exception as e: | ||||||
|  |             self.labels.append(line) | ||||||
|  |             row=len(self.labels)-1 | ||||||
|  |          | ||||||
|  |         self.updatemessage.emit() | ||||||
|  |      | ||||||
|  |         return row | ||||||
|  | 
 | ||||||
|  |     def msg_del_row(self,row): | ||||||
|  |         #_logger.trace(f"del row: {row}") | ||||||
|  |         try: | ||||||
|  |             del self.labels[row] | ||||||
|  |         except Exception as e: | ||||||
|  |             pass | ||||||
|  |         self.updatemessage.emit() | ||||||
|  | 
 | ||||||
|  |     def update(self): | ||||||
|  |         self.vbox.removeWidget(self.qwidget) | ||||||
|  |         self.qwidget=QWidget(self) | ||||||
|  |         labelsbox = QVBoxLayout(self.qwidget) | ||||||
|  |         for label in self.labels: | ||||||
|  |             labelsbox.addWidget(QLabel(label)) | ||||||
|  |         self.vbox.addWidget(self.qwidget) | ||||||
|  | 
 | ||||||
|  |     def get_text(self): | ||||||
|  |         return self.message_label.text() | ||||||
|  | def ThreadStopped(Exception): | ||||||
|  |     pass | ||||||
							
								
								
									
										283
									
								
								balqt/heir_list.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										283
									
								
								balqt/heir_list.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,283 @@ | |||||||
|  | #!/usr/bin/env python | ||||||
|  | # | ||||||
|  | # Electrum - lightweight Bitcoin client | ||||||
|  | # Copyright (C) 2015 Thomas Voegtlin | ||||||
|  | # | ||||||
|  | # Permission is hereby granted, free of charge, to any person | ||||||
|  | # obtaining a copy of this software and associated documentation files | ||||||
|  | # (the "Software"), to deal in the Software without restriction, | ||||||
|  | # including without limitation the rights to use, copy, modify, merge, | ||||||
|  | # publish, distribute, sublicense, and/or sell copies of the Software, | ||||||
|  | # and to permit persons to whom the Software is furnished to do so, | ||||||
|  | # subject to the following conditions: | ||||||
|  | # | ||||||
|  | # The above copyright notice and this permission notice shall be | ||||||
|  | # included in all copies or substantial portions of the Software. | ||||||
|  | # | ||||||
|  | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | ||||||
|  | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | ||||||
|  | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | ||||||
|  | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS | ||||||
|  | # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN | ||||||
|  | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN | ||||||
|  | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||||
|  | # SOFTWARE. | ||||||
|  | 
 | ||||||
|  | import enum | ||||||
|  | from typing import TYPE_CHECKING | ||||||
|  | from datetime import datetime | ||||||
|  | 
 | ||||||
|  | from . import qt_resources | ||||||
|  | if qt_resources.QT_VERSION == 5: | ||||||
|  |     from PyQt5.QtGui import QStandardItemModel, QStandardItem | ||||||
|  |     from PyQt5.QtCore import Qt, QPersistentModelIndex, QModelIndex | ||||||
|  |     from PyQt5.QtWidgets import (QAbstractItemView, QMenu,QWidget,QHBoxLayout,QLabel,QSpinBox,QPushButton) | ||||||
|  | else: | ||||||
|  |     from PyQt6.QtGui import QStandardItemModel, QStandardItem | ||||||
|  |     from PyQt6.QtCore import Qt, QPersistentModelIndex, QModelIndex | ||||||
|  |     from PyQt6.QtWidgets import (QAbstractItemView, QMenu,QWidget,QHBoxLayout,QLabel,QSpinBox,QPushButton) | ||||||
|  | 
 | ||||||
|  | from electrum.i18n import _ | ||||||
|  | from electrum.bitcoin import is_address | ||||||
|  | from electrum.util import block_explorer_URL  | ||||||
|  | from electrum.plugin import run_hook | ||||||
|  | from electrum.gui.qt.util import webopen, MessageBoxMixin,HelpButton | ||||||
|  | from electrum.gui.qt.my_treeview import MyTreeView, MySortModel | ||||||
|  | 
 | ||||||
|  | from .. import util as Util | ||||||
|  | from .locktimeedit import HeirsLockTimeEdit | ||||||
|  | if TYPE_CHECKING: | ||||||
|  |     from electrum.gui.qt.main_window import ElectrumWindow | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class HeirList(MyTreeView,MessageBoxMixin): | ||||||
|  | 
 | ||||||
|  |     class Columns(MyTreeView.BaseColumnsEnum): | ||||||
|  |         NAME = enum.auto() | ||||||
|  |         ADDRESS = enum.auto() | ||||||
|  |         AMOUNT = enum.auto() | ||||||
|  | 
 | ||||||
|  |     headers = { | ||||||
|  |         Columns.NAME: _('Name'), | ||||||
|  |         Columns.ADDRESS: _('Address'), | ||||||
|  |         Columns.AMOUNT: _('Amount'), | ||||||
|  |         #Columns.LOCKTIME:_('LockTime'), | ||||||
|  |     } | ||||||
|  |     filter_columns = [Columns.NAME, Columns.ADDRESS] | ||||||
|  | 
 | ||||||
|  |     ROLE_SORT_ORDER = Qt.ItemDataRole.UserRole + 1000 | ||||||
|  | 
 | ||||||
|  |     ROLE_HEIR_KEY = Qt.ItemDataRole.UserRole + 1001 | ||||||
|  |     key_role = ROLE_HEIR_KEY | ||||||
|  | 
 | ||||||
|  |     def __init__(self, bal_window: 'BalWindow'): | ||||||
|  |         super().__init__( | ||||||
|  |             parent=bal_window.window, | ||||||
|  |             main_window=bal_window.window, | ||||||
|  |             stretch_column=self.Columns.NAME, | ||||||
|  |             editable_columns=[self.Columns.NAME,self.Columns.ADDRESS,self.Columns.AMOUNT], | ||||||
|  |         ) | ||||||
|  |         self.decimal_point = bal_window.bal_plugin.config.get_decimal_point() | ||||||
|  |         self.bal_window = bal_window | ||||||
|  | 
 | ||||||
|  |         try: | ||||||
|  |             self.setModel(QStandardItemModel(self)) | ||||||
|  |             self.sortByColumn(self.Columns.NAME, Qt.SortOrder.AscendingOrder) | ||||||
|  |             self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) | ||||||
|  |         except: | ||||||
|  |             pass | ||||||
|  |             #self.sortByColumn(self.Columns.NAME, Qt.SortOrder.AscendingOrder) | ||||||
|  |             #self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) | ||||||
|  | 
 | ||||||
|  |         self.setSortingEnabled(True) | ||||||
|  |         self.std_model = self.model() | ||||||
|  | 
 | ||||||
|  |         self.update() | ||||||
|  |      | ||||||
|  | 
 | ||||||
|  |     def on_edited(self, idx, edit_key, *, text): | ||||||
|  |         original = prior_name = self.bal_window.heirs.get(edit_key) | ||||||
|  |         if not prior_name: | ||||||
|  |             return | ||||||
|  |         col = idx.column() | ||||||
|  |         try: | ||||||
|  |             #if col == 3: | ||||||
|  |             #    try: | ||||||
|  |             #        text = Util.str_to_locktime(text) | ||||||
|  |             #    except: | ||||||
|  |             #        print("not a valid locktime") | ||||||
|  |             #        pass | ||||||
|  |             if col == 2: | ||||||
|  |                 text = Util.encode_amount(text,self.decimal_point) | ||||||
|  |             elif col == 0: | ||||||
|  |                 self.bal_window.delete_heirs([edit_key]) | ||||||
|  |                 edit_key = text | ||||||
|  |             prior_name[col-1] = text | ||||||
|  |             prior_name.insert(0,edit_key) | ||||||
|  |             prior_name = tuple(prior_name) | ||||||
|  |         except Exception as e: | ||||||
|  |             #print("eccezione tupla",e) | ||||||
|  |             prior_name = (edit_key,)+prior_name[:col-1]+(text,)+prior_name[col:] | ||||||
|  |         #print("prior_name",prior_name,original) | ||||||
|  |         | ||||||
|  |         try: | ||||||
|  |             self.bal_window.set_heir(prior_name) | ||||||
|  |             #print("setheir") | ||||||
|  |         except Exception as e: | ||||||
|  |             pass | ||||||
|  | 
 | ||||||
|  |             #print("heir non valido ripristino l'originale",e) | ||||||
|  |             try: | ||||||
|  |                 #print("setup_original",(edit_key,)+original) | ||||||
|  |                 self.bal_window.set_heir((edit_key,)+original) | ||||||
|  |             except Exception as e: | ||||||
|  |                 #print("errore nellimpostare original",e,original) | ||||||
|  |                 self.update() | ||||||
|  | 
 | ||||||
|  |     def create_menu(self, position): | ||||||
|  |         menu = QMenu() | ||||||
|  |         idx = self.indexAt(position) | ||||||
|  |         column = idx.column() or self.Columns.NAME | ||||||
|  |         selected_keys = [] | ||||||
|  |         for s_idx in self.selected_in_column(self.Columns.NAME): | ||||||
|  |             #print(s_idx) | ||||||
|  |             sel_key = self.model().itemFromIndex(s_idx).data(0) | ||||||
|  |             selected_keys.append(sel_key) | ||||||
|  |         if selected_keys and idx.isValid(): | ||||||
|  |             column_title = self.model().horizontalHeaderItem(column).text() | ||||||
|  |             column_data = '\n'.join(self.model().itemFromIndex(s_idx).text() | ||||||
|  |                                     for s_idx in self.selected_in_column(column)) | ||||||
|  |             menu.addAction(_("Copy {}").format(column_title), lambda: self.place_text_on_clipboard(column_data, title=column_title)) | ||||||
|  |             if column in self.editable_columns: | ||||||
|  |                 item = self.model().itemFromIndex(idx) | ||||||
|  |                 if item.isEditable(): | ||||||
|  |                     # would not be editable if openalias | ||||||
|  |                     persistent = QPersistentModelIndex(idx) | ||||||
|  |                     menu.addAction(_("Edit {}").format(column_title), lambda p=persistent: self.edit(QModelIndex(p))) | ||||||
|  |             menu.addAction(_("Delete"), lambda: self.bal_window.delete_heirs(selected_keys)) | ||||||
|  |         menu.exec(self.viewport().mapToGlobal(position)) | ||||||
|  | 
 | ||||||
|  |     def update(self): | ||||||
|  |         if self.maybe_defer_update(): | ||||||
|  |             return | ||||||
|  |         current_key = self.get_role_data_for_current_item(col=self.Columns.NAME, role=self.ROLE_HEIR_KEY) | ||||||
|  |         self.model().clear() | ||||||
|  |         self.update_headers(self.__class__.headers) | ||||||
|  |         set_current = None | ||||||
|  |         for key in sorted(self.bal_window.heirs.keys()): | ||||||
|  |             heir = self.bal_window.heirs[key] | ||||||
|  |             labels = [""] * len(self.Columns) | ||||||
|  |             labels[self.Columns.NAME] = key | ||||||
|  |             labels[self.Columns.ADDRESS] = heir[0] | ||||||
|  |             labels[self.Columns.AMOUNT] = Util.decode_amount(heir[1],self.decimal_point) | ||||||
|  |             #labels[self.Columns.LOCKTIME] =  str(Util.locktime_to_str(heir[2])) | ||||||
|  | 
 | ||||||
|  |             items = [QStandardItem(x) for x in labels] | ||||||
|  |             items[self.Columns.NAME].setEditable(True) | ||||||
|  |             items[self.Columns.ADDRESS].setEditable(True) | ||||||
|  |             items[self.Columns.AMOUNT].setEditable(True) | ||||||
|  |             #items[self.Columns.LOCKTIME].setEditable(True) | ||||||
|  |             items[self.Columns.NAME].setData(key, self.ROLE_HEIR_KEY+1) | ||||||
|  |             items[self.Columns.ADDRESS].setData(key, self.ROLE_HEIR_KEY+2) | ||||||
|  |             items[self.Columns.AMOUNT].setData(key, self.ROLE_HEIR_KEY+3) | ||||||
|  |             #items[self.Columns.LOCKTIME].setData(key, self.ROLE_HEIR_KEY+4) | ||||||
|  | 
 | ||||||
|  |             self.model().insertRow(self.model().rowCount(), items) | ||||||
|  | 
 | ||||||
|  |             if key == current_key: | ||||||
|  |                 idx = self.model().index(row_count, self.Columns.NAME) | ||||||
|  |                 set_current = QPersistentModelIndex(idx) | ||||||
|  |         self.set_current_idx(set_current) | ||||||
|  |         # FIXME refresh loses sort order; so set "default" here: | ||||||
|  |         self.filter() | ||||||
|  |         run_hook('update_heirs_tab', self) | ||||||
|  | 
 | ||||||
|  |     def refresh_row(self, key, row): | ||||||
|  |         # nothing to update here | ||||||
|  |         pass | ||||||
|  | 
 | ||||||
|  |     def get_edit_key_from_coordinate(self, row, col): | ||||||
|  |         #print("role_data",self.get_role_data_from_coordinate(row, col, role=self.ROLE_HEIR_KEY)) | ||||||
|  |         #print(col) | ||||||
|  |         return self.get_role_data_from_coordinate(row, col, role=self.ROLE_HEIR_KEY+col+1)  | ||||||
|  |         return col | ||||||
|  | 
 | ||||||
|  |     def create_toolbar(self, config): | ||||||
|  |         toolbar, menu = self.create_toolbar_with_menu('') | ||||||
|  |         menu.addAction(_("&New Heir"), self.bal_window.new_heir_dialog) | ||||||
|  |         menu.addAction(_("Import"), self.bal_window.import_heirs) | ||||||
|  |         menu.addAction(_("Export"), lambda: self.bal_window.export_heirs()) | ||||||
|  |         #menu.addAction(_("Build Traonsactions"), self.build_transactions) | ||||||
|  | 
 | ||||||
|  |         self.heir_locktime = HeirsLockTimeEdit(self.window(),0) | ||||||
|  |         def on_heir_locktime(): | ||||||
|  |             if not self.heir_locktime.get_locktime(): | ||||||
|  |                 self.heir_locktime.set_locktime('1y') | ||||||
|  |             self.bal_window.will_settings['locktime'] = self.heir_locktime.get_locktime() if self.heir_locktime.get_locktime() else "1y" | ||||||
|  |             self.bal_window.bal_plugin.config.set_key('will_settings',self.bal_window.will_settings,save = True) | ||||||
|  |         self.heir_locktime.valueEdited.connect(on_heir_locktime) | ||||||
|  | 
 | ||||||
|  |         self.heir_threshold = HeirsLockTimeEdit(self,0) | ||||||
|  |         def on_heir_threshold(): | ||||||
|  |             if not self.heir_threshold.get_locktime(): | ||||||
|  |                 self.heir_threshold.set_locktime('180d') | ||||||
|  | 
 | ||||||
|  |             self.bal_window.will_settings['threshold'] = self.heir_threshold.get_locktime()  | ||||||
|  |             self.bal_window.bal_plugin.config.set_key('will_settings',self.bal_window.will_settings,save = True) | ||||||
|  |         self.heir_threshold.valueEdited.connect(on_heir_threshold) | ||||||
|  | 
 | ||||||
|  |         self.heir_tx_fees = QSpinBox() | ||||||
|  |         self.heir_tx_fees.setMinimum(1) | ||||||
|  |         self.heir_tx_fees.setMaximum(10000) | ||||||
|  |         def on_heir_tx_fees(): | ||||||
|  |             if not self.heir_tx_fees.value(): | ||||||
|  |                 self.heir_tx_fees.set_value(1) | ||||||
|  |             self.bal_window.will_settings['tx_fees'] = self.heir_tx_fees.value() | ||||||
|  |             self.bal_window.bal_plugin.config.set_key('will_settings',self.bal_window.will_settings,save = True) | ||||||
|  |         self.heir_tx_fees.valueChanged.connect(on_heir_tx_fees) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         self.heirs_widget = QWidget() | ||||||
|  |         layout = QHBoxLayout() | ||||||
|  |         self.heirs_widget.setLayout(layout) | ||||||
|  |          | ||||||
|  |         layout.addWidget(QLabel(_("Delivery Time:"))) | ||||||
|  |         layout.addWidget(self.heir_locktime) | ||||||
|  |         layout.addWidget(HelpButton(_("Locktime* to be used in the transaction\n" | ||||||
|  |                                     +"if you choose Raw, you can insert various options based on suffix:\n" | ||||||
|  |                                     #+" - b: number of blocks after current block(ex: 144b means tomorrow)\n" | ||||||
|  |                                     +" - d: number of days after current day(ex: 1d means tomorrow)\n" | ||||||
|  |                                     +" - y: number of years after currrent day(ex: 1y means one year from today)\n" | ||||||
|  |                                     +"* locktime can be anticipated to update will\n"))) | ||||||
|  | 
 | ||||||
|  |         layout.addWidget(QLabel(" ")) | ||||||
|  |         layout.addWidget(QLabel(_("Check Alive:"))) | ||||||
|  |         layout.addWidget(self.heir_threshold) | ||||||
|  |         layout.addWidget(HelpButton(_("Check  to ask for invalidation.\n" | ||||||
|  |                                     +"When less then this time is missing, ask to invalidate.\n" | ||||||
|  |                                     +"If you fail to invalidate during this time, your transactions will be delivered to your heirs.\n" | ||||||
|  |                                     +"if you choose Raw, you can insert various options based on suffix:\n" | ||||||
|  |                                     #+" - b: number of blocks after current block(ex: 144b means tomorrow)\n" | ||||||
|  |                                     +" - d: number of days after current day(ex: 1d means tomorrow).\n" | ||||||
|  |                                     +" - y: number of years after currrent day(ex: 1y means one year from today).\n\n"))) | ||||||
|  |         layout.addWidget(QLabel(" ")) | ||||||
|  |         layout.addWidget(QLabel(_("Fees:"))) | ||||||
|  |         layout.addWidget(self.heir_tx_fees) | ||||||
|  |         layout.addWidget(HelpButton(_("Fee to be used in the transaction"))) | ||||||
|  |         layout.addWidget(QLabel("sats/vbyte")) | ||||||
|  |         layout.addWidget(QLabel(" ")) | ||||||
|  |         newHeirButton = QPushButton(_("New Heir")) | ||||||
|  |         newHeirButton.clicked.connect(self.bal_window.new_heir_dialog) | ||||||
|  |         layout.addWidget(newHeirButton) | ||||||
|  | 
 | ||||||
|  |         toolbar.insertWidget(2, self.heirs_widget) | ||||||
|  | 
 | ||||||
|  |         return toolbar | ||||||
|  |     def update_will_settings(self): | ||||||
|  |         self.heir_threshold.set_locktime(self.bal_window.will_settings['threshold']) | ||||||
|  |         self.heir_locktime.set_locktime(self.bal_window.will_settings['locktime']) | ||||||
|  |         self.heir_tx_fees.setValue(int(self.bal_window.will_settings['tx_fees'])) | ||||||
|  | 
 | ||||||
|  |     def build_transactions(self): | ||||||
|  |         will = self.bal_window.prepare_will() | ||||||
|  | 
 | ||||||
							
								
								
									
										255
									
								
								balqt/locktimeedit.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										255
									
								
								balqt/locktimeedit.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,255 @@ | |||||||
|  | # Copyright (C) 2020 The Electrum developers | ||||||
|  | # Distributed under the MIT software license, see the accompanying | ||||||
|  | # file LICENCE or http://www.opensource.org/licenses/mit-license.php | ||||||
|  | 
 | ||||||
|  | import time | ||||||
|  | from datetime import datetime | ||||||
|  | from typing import Optional, Any | ||||||
|  | 
 | ||||||
|  | from . import qt_resources | ||||||
|  | if qt_resources.QT_VERSION == 5: | ||||||
|  |     from PyQt5.QtCore import Qt, QDateTime, pyqtSignal | ||||||
|  |     from PyQt5.QtGui import QPalette, QPainter | ||||||
|  |     from PyQt5.QtWidgets import (QWidget, QLineEdit, QStyle, QStyleOptionFrame, QComboBox, QHBoxLayout, QDateTimeEdit) | ||||||
|  | else: | ||||||
|  |     from PyQt6.QtCore import Qt, QDateTime, pyqtSignal | ||||||
|  |     from PyQt6.QtGui import QPalette, QPainter | ||||||
|  |     from PyQt6.QtWidgets import (QWidget, QLineEdit, QStyle, QStyleOptionFrame, QComboBox, QHBoxLayout, QDateTimeEdit) | ||||||
|  | 
 | ||||||
|  | from electrum.i18n import _ | ||||||
|  | from electrum.bitcoin import NLOCKTIME_MIN, NLOCKTIME_MAX, NLOCKTIME_BLOCKHEIGHT_MAX | ||||||
|  | 
 | ||||||
|  | from electrum.gui.qt.util import char_width_in_lineedit, ColorScheme | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class HeirsLockTimeEdit(QWidget): | ||||||
|  | 
 | ||||||
|  |     valueEdited = pyqtSignal() | ||||||
|  |     locktime_threshold = 50000000 | ||||||
|  |     def __init__(self, parent=None,default_index = 1): | ||||||
|  |         QWidget.__init__(self, parent) | ||||||
|  | 
 | ||||||
|  |         hbox = QHBoxLayout() | ||||||
|  |         self.setLayout(hbox) | ||||||
|  |         hbox.setContentsMargins(0, 0, 0, 0) | ||||||
|  |         hbox.setSpacing(0) | ||||||
|  | 
 | ||||||
|  |         self.locktime_raw_e = LockTimeRawEdit(self,time_edit = self) | ||||||
|  |         #self.locktime_height_e = LockTimeHeightEdit(self) | ||||||
|  |         self.locktime_date_e = LockTimeDateEdit(self,time_edit = self) | ||||||
|  |         #self.editors = [self.locktime_raw_e, self.locktime_height_e, self.locktime_date_e] | ||||||
|  |         self.editors = [self.locktime_raw_e, self.locktime_date_e] | ||||||
|  | 
 | ||||||
|  |         self.combo = QComboBox() | ||||||
|  |         #options = [_("Raw"), _("Block height"), _("Date")] | ||||||
|  |         options = [_("Raw"),_("Date")] | ||||||
|  |         self.option_index_to_editor_map = { | ||||||
|  |             0: self.locktime_raw_e, | ||||||
|  |             #1: self.locktime_height_e, | ||||||
|  |             1: self.locktime_date_e, | ||||||
|  |             #2: self.locktime_date_e, | ||||||
|  |         } | ||||||
|  |         self.combo.addItems(options) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         self.editor = self.option_index_to_editor_map[default_index] | ||||||
|  |         self.combo.currentIndexChanged.connect(self.on_current_index_changed) | ||||||
|  |         self.combo.setCurrentIndex(default_index) | ||||||
|  |         self.on_current_index_changed(default_index) | ||||||
|  | 
 | ||||||
|  |         hbox.addWidget(self.combo) | ||||||
|  |         for w in self.editors: | ||||||
|  |             hbox.addWidget(w) | ||||||
|  |         hbox.addStretch(1) | ||||||
|  | 
 | ||||||
|  |         #self.locktime_height_e.textEdited.connect(self.valueEdited.emit) | ||||||
|  |         self.locktime_raw_e.editingFinished.connect(self.valueEdited.emit) | ||||||
|  |         self.locktime_date_e.dateTimeChanged.connect(self.valueEdited.emit) | ||||||
|  |         self.combo.currentIndexChanged.connect(self.valueEdited.emit) | ||||||
|  | 
 | ||||||
|  |     def on_current_index_changed(self,i): | ||||||
|  |         for w in self.editors: | ||||||
|  |             w.setVisible(False) | ||||||
|  |             w.setEnabled(False) | ||||||
|  |         prev_locktime = self.editor.get_locktime() | ||||||
|  |         self.editor = self.option_index_to_editor_map[i] | ||||||
|  |         if self.editor.is_acceptable_locktime(prev_locktime): | ||||||
|  |             self.editor.set_locktime(prev_locktime,force=True) | ||||||
|  |         self.editor.setVisible(True) | ||||||
|  |         self.editor.setEnabled(True) | ||||||
|  | 
 | ||||||
|  |     def get_locktime(self) -> Optional[str]: | ||||||
|  |         return self.editor.get_locktime() | ||||||
|  | 
 | ||||||
|  |     def set_index(self,index): | ||||||
|  |         self.combo.setCurrentIndex(index) | ||||||
|  |         self.on_current_index_changed(index) | ||||||
|  | 
 | ||||||
|  |     def set_locktime(self, x: Any,force=True) -> None: | ||||||
|  |         self.editor.set_locktime(x,force) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class _LockTimeEditor: | ||||||
|  |     min_allowed_value = NLOCKTIME_MIN | ||||||
|  |     max_allowed_value = NLOCKTIME_MAX | ||||||
|  | 
 | ||||||
|  |     def get_locktime(self) -> Optional[int]: | ||||||
|  |         raise NotImplementedError() | ||||||
|  | 
 | ||||||
|  |     def set_locktime(self, x: Any,force=True) -> None: | ||||||
|  |         raise NotImplementedError() | ||||||
|  | 
 | ||||||
|  |     @classmethod | ||||||
|  |     def is_acceptable_locktime(cls, x: Any) -> bool: | ||||||
|  |         if not x:  # e.g. empty string | ||||||
|  |             return True | ||||||
|  |         try: | ||||||
|  |             x = int(x) | ||||||
|  |         except Exception as e: | ||||||
|  |             return False | ||||||
|  |         return cls.min_allowed_value <= x <= cls.max_allowed_value | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class LockTimeRawEdit(QLineEdit, _LockTimeEditor): | ||||||
|  | 
 | ||||||
|  |     def __init__(self, parent=None,time_edit=None): | ||||||
|  |         QLineEdit.__init__(self, parent) | ||||||
|  |         self.setFixedWidth(14 * char_width_in_lineedit()) | ||||||
|  |         self.textChanged.connect(self.numbify) | ||||||
|  |         self.isdays = False | ||||||
|  |         self.isyears = False | ||||||
|  |         self.isblocks = False | ||||||
|  |         self.time_edit=time_edit | ||||||
|  | 
 | ||||||
|  |     def replace_str(self,text): | ||||||
|  |         return str(text).replace('d','').replace('y','').replace('b','') | ||||||
|  | 
 | ||||||
|  |     def checkbdy(self,s,pos,appendix): | ||||||
|  |         try: | ||||||
|  |             charpos = pos-1 | ||||||
|  |             charpos = max(0,charpos) | ||||||
|  |             charpos = min(len(s)-1,charpos) | ||||||
|  |             if appendix ==  s[charpos]: | ||||||
|  |                 s=self.replace_str(s)+appendix | ||||||
|  |                 pos = charpos | ||||||
|  |         except Exception as e: | ||||||
|  |             pass | ||||||
|  |         return pos, s | ||||||
|  | 
 | ||||||
|  |     def numbify(self): | ||||||
|  |         text = self.text().strip() | ||||||
|  |         #chars = '0123456789bdy' removed the option to choose locktime by block | ||||||
|  |         chars = '0123456789dy' | ||||||
|  |         pos = posx = self.cursorPosition() | ||||||
|  |         pos = len(''.join([i for i in text[:pos] if i in chars])) | ||||||
|  |         s = ''.join([i for i in text if i in chars]) | ||||||
|  |         self.isdays = False | ||||||
|  |         self.isyears = False | ||||||
|  |         self.isblocks = False | ||||||
|  | 
 | ||||||
|  |         pos,s = self.checkbdy(s,pos,'d') | ||||||
|  |         pos,s = self.checkbdy(s,pos,'y') | ||||||
|  |         pos,s = self.checkbdy(s,pos,'b') | ||||||
|  | 
 | ||||||
|  |         if 'd' in s: self.isdays = True  | ||||||
|  |         if 'y' in s: self.isyears = True | ||||||
|  |         if 'b' in s: self.isblocks = True | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         if self.isdays: s= self.replace_str(s) + 'd' | ||||||
|  |         if self.isyears: s = self.replace_str(s) + 'y' | ||||||
|  |         if self.isblocks: s= self.replace_str(s) + 'b' | ||||||
|  | 
 | ||||||
|  |         self.set_locktime(s,force=False) | ||||||
|  |         # setText sets Modified to False.  Instead we want to remember | ||||||
|  |         # if updates were because of user modification. | ||||||
|  |         self.setModified(self.hasFocus()) | ||||||
|  |         self.setCursorPosition(pos) | ||||||
|  | 
 | ||||||
|  |     def get_locktime(self) -> Optional[str]: | ||||||
|  |         try: | ||||||
|  |             return str(self.text()) | ||||||
|  |         except Exception as e: | ||||||
|  |             return None | ||||||
|  | 
 | ||||||
|  |     def set_locktime(self, x: Any,force=True) -> None: | ||||||
|  |         out = str(x) | ||||||
|  |         if 'd' in out: | ||||||
|  |             out = self.replace_str(x)+'d' | ||||||
|  |         elif 'y' in out: | ||||||
|  |             out = self.replace_str(x)+'y' | ||||||
|  |         elif 'b' in out: | ||||||
|  |             out = self.replace_str(x)+'b' | ||||||
|  |         else: | ||||||
|  |             try: | ||||||
|  |                 out = int(x) | ||||||
|  |             except Exception as e: | ||||||
|  |                 self.setText('') | ||||||
|  |                 return | ||||||
|  |             out = max(out, self.min_allowed_value) | ||||||
|  |             out = min(out, self.max_allowed_value) | ||||||
|  |         self.setText(str(out)) | ||||||
|  |         #try: | ||||||
|  |         #    if self.time_edit and int(out)>self.time_edit.locktime_threshold and not force: | ||||||
|  |         #        self.time_edit.set_index(1) | ||||||
|  |         #except: | ||||||
|  |         #    pass | ||||||
|  | 
 | ||||||
|  | class LockTimeHeightEdit(LockTimeRawEdit): | ||||||
|  |     max_allowed_value = NLOCKTIME_BLOCKHEIGHT_MAX | ||||||
|  | 
 | ||||||
|  |     def __init__(self, parent=None,time_edit=None): | ||||||
|  |         LockTimeRawEdit.__init__(self, parent) | ||||||
|  |         self.setFixedWidth(20 * char_width_in_lineedit()) | ||||||
|  |         self.time_edit = time_edit | ||||||
|  | 
 | ||||||
|  |     def paintEvent(self, event): | ||||||
|  |         super().paintEvent(event) | ||||||
|  |         panel = QStyleOptionFrame() | ||||||
|  |         self.initStyleOption(panel) | ||||||
|  |         textRect = self.style().subElementRect(QStyle.SE_LineEditContents, panel, self) | ||||||
|  |         textRect.adjust(2, 0, -10, 0) | ||||||
|  |         painter = QPainter(self) | ||||||
|  |         painter.setPen(ColorScheme.GRAY.as_color()) | ||||||
|  |         painter.drawText(textRect, int(Qt.AlignRight | Qt.AlignVCenter), "height") | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def get_max_allowed_timestamp() -> int: | ||||||
|  |     ts = NLOCKTIME_MAX | ||||||
|  |     # Test if this value is within the valid timestamp limits (which is platform-dependent). | ||||||
|  |     # see #6170 | ||||||
|  |     try: | ||||||
|  |         datetime.fromtimestamp(ts) | ||||||
|  |     except (OSError, OverflowError): | ||||||
|  |         ts = 2 ** 31 - 1  # INT32_MAX | ||||||
|  |         datetime.fromtimestamp(ts)  # test if raises | ||||||
|  |     return ts | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class LockTimeDateEdit(QDateTimeEdit, _LockTimeEditor): | ||||||
|  |     min_allowed_value = NLOCKTIME_BLOCKHEIGHT_MAX + 1 | ||||||
|  |     max_allowed_value = get_max_allowed_timestamp() | ||||||
|  | 
 | ||||||
|  |     def __init__(self, parent=None,time_edit=None): | ||||||
|  |         QDateTimeEdit.__init__(self, parent) | ||||||
|  |         self.setMinimumDateTime(datetime.fromtimestamp(self.min_allowed_value)) | ||||||
|  |         self.setMaximumDateTime(datetime.fromtimestamp(self.max_allowed_value)) | ||||||
|  |         self.setDateTime(QDateTime.currentDateTime()) | ||||||
|  |         self.time_edit = time_edit | ||||||
|  | 
 | ||||||
|  |     def get_locktime(self) -> Optional[int]: | ||||||
|  |         dt = self.dateTime().toPyDateTime() | ||||||
|  |         locktime = int(time.mktime(dt.timetuple())) | ||||||
|  |         return locktime | ||||||
|  | 
 | ||||||
|  |     def set_locktime(self, x: Any,force = False) -> None: | ||||||
|  |         if not self.is_acceptable_locktime(x): | ||||||
|  |             self.setDateTime(QDateTime.currentDateTime()) | ||||||
|  |             return | ||||||
|  |         try: | ||||||
|  |             x = int(x) | ||||||
|  |         except Exception: | ||||||
|  |             self.setDateTime(QDateTime.currentDateTime()) | ||||||
|  |             return | ||||||
|  |         dt = datetime.fromtimestamp(x) | ||||||
|  |         self.setDateTime(dt) | ||||||
							
								
								
									
										331
									
								
								balqt/preview_dialog.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										331
									
								
								balqt/preview_dialog.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,331 @@ | |||||||
|  | 
 | ||||||
|  | import enum | ||||||
|  | import copy | ||||||
|  | import json | ||||||
|  | import urllib.request | ||||||
|  | import urllib.parse | ||||||
|  | from functools import partial | ||||||
|  | 
 | ||||||
|  | from . import qt_resources | ||||||
|  | if qt_resources.QT_VERSION == 5: | ||||||
|  |     from PyQt5.QtGui import QStandardItemModel, QStandardItem, QPalette, QColor | ||||||
|  |     from PyQt5.QtCore import Qt,QPersistentModelIndex, QModelIndex | ||||||
|  |     from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QLabel,QMenu,QAbstractItemView,QWidget) | ||||||
|  | else: | ||||||
|  |     from PyQt6.QtGui import QStandardItemModel, QStandardItem, QPalette, QColor | ||||||
|  |     from PyQt6.QtCore import Qt,QPersistentModelIndex, QModelIndex | ||||||
|  |     from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QLabel,QMenu,QAbstractItemView,QWidget) | ||||||
|  | 
 | ||||||
|  | from electrum.i18n import _ | ||||||
|  | from electrum.gui.qt.util import (Buttons,read_QIcon, import_meta_gui, export_meta_gui,MessageBoxMixin) | ||||||
|  | from electrum.util import write_json_file,read_json_file,FileImportFailed | ||||||
|  | from electrum.gui.qt.my_treeview import MyTreeView | ||||||
|  | from electrum.transaction import tx_from_any | ||||||
|  | from electrum.network import Network | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | from ..bal import BalPlugin | ||||||
|  | from .. import willexecutors as Willexecutors | ||||||
|  | from .. import util as Util  | ||||||
|  | from .. import will as  Will | ||||||
|  | from .baldialog import BalDialog | ||||||
|  | 
 | ||||||
|  | class PreviewList(MyTreeView): | ||||||
|  |     class Columns(MyTreeView.BaseColumnsEnum): | ||||||
|  |         LOCKTIME = enum.auto() | ||||||
|  |         TXID = enum.auto() | ||||||
|  |         WILLEXECUTOR = enum.auto() | ||||||
|  |         STATUS = enum.auto() | ||||||
|  | 
 | ||||||
|  |     headers = { | ||||||
|  |         Columns.LOCKTIME: _('Locktime'), | ||||||
|  |         Columns.TXID: _('Txid'), | ||||||
|  |         Columns.WILLEXECUTOR: _('Will-Executor'), | ||||||
|  |         Columns.STATUS: _('Status'), | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     ROLE_HEIR_KEY = Qt.ItemDataRole.UserRole + 2000 | ||||||
|  |     key_role = ROLE_HEIR_KEY | ||||||
|  | 
 | ||||||
|  |     def __init__(self, parent: 'BalWindow',will): | ||||||
|  |         super().__init__( | ||||||
|  |             parent=parent.window, | ||||||
|  |             stretch_column=self.Columns.TXID, | ||||||
|  |         ) | ||||||
|  |         self.decimal_point=parent.bal_plugin.config.get_decimal_point | ||||||
|  |         self.setModel(QStandardItemModel(self)) | ||||||
|  |         self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) | ||||||
|  |      | ||||||
|  | 
 | ||||||
|  |         if not will is None: | ||||||
|  |             self.will = will | ||||||
|  |         else: | ||||||
|  |             self.will = parent.willitems | ||||||
|  | 
 | ||||||
|  |         self.bal_window = parent | ||||||
|  |         self.wallet=parent.window.wallet | ||||||
|  |         self.setModel(QStandardItemModel(self)) | ||||||
|  |         self.setSortingEnabled(True) | ||||||
|  |         self.std_model = self.model() | ||||||
|  |         self.config = parent.bal_plugin.config | ||||||
|  |         self.bal_plugin=self.bal_window.bal_plugin | ||||||
|  | 
 | ||||||
|  |         self.update() | ||||||
|  | 
 | ||||||
|  |     def create_menu(self, position): | ||||||
|  |         menu = QMenu() | ||||||
|  |         idx = self.indexAt(position) | ||||||
|  |         column = idx.column() or self.Columns.TXID | ||||||
|  |         selected_keys = [] | ||||||
|  |         for s_idx in self.selected_in_column(self.Columns.TXID): | ||||||
|  |             sel_key = self.model().itemFromIndex(s_idx).data(0) | ||||||
|  |             selected_keys.append(sel_key) | ||||||
|  |         if selected_keys and idx.isValid(): | ||||||
|  |             column_title = self.model().horizontalHeaderItem(column).text() | ||||||
|  |             column_data = '\n'.join(self.model().itemFromIndex(s_idx).text() | ||||||
|  |                                     for s_idx in self.selected_in_column(column)) | ||||||
|  | 
 | ||||||
|  |             menu.addAction(_("details").format(column_title), lambda: self.show_transaction(selected_keys)).setEnabled(len(selected_keys)<2) | ||||||
|  |             menu.addAction(_("check ").format(column_title), lambda: self.check_transactions(selected_keys)) | ||||||
|  | 
 | ||||||
|  |             menu.addSeparator() | ||||||
|  |             menu.addAction(_("delete").format(column_title), lambda: self.delete(selected_keys)) | ||||||
|  | 
 | ||||||
|  |         menu.exec(self.viewport().mapToGlobal(position)) | ||||||
|  | 
 | ||||||
|  |     def delete(self,selected_keys): | ||||||
|  |         for key in selected_keys: | ||||||
|  |             del self.will[key] | ||||||
|  |             try: | ||||||
|  |                 del self.bal_window.willitems[key] | ||||||
|  |             except: | ||||||
|  |                 pass | ||||||
|  |             try: | ||||||
|  |                 del self.bal_window.will[key] | ||||||
|  |             except: | ||||||
|  |                 pass | ||||||
|  |         self.update() | ||||||
|  | 
 | ||||||
|  |     def check_transactions(self,selected_keys): | ||||||
|  |         wout = {} | ||||||
|  |         for k in selected_keys: | ||||||
|  |             wout[k] = self.will[k] | ||||||
|  |         if wout: | ||||||
|  |             self.bal_window.check_transactions(wout) | ||||||
|  |         self.update() | ||||||
|  | 
 | ||||||
|  |     def show_transaction(self,selected_keys): | ||||||
|  |         for key in selected_keys: | ||||||
|  |             self.bal_window.show_transaction(self.will[key].tx) | ||||||
|  | 
 | ||||||
|  |         self.update() | ||||||
|  | 
 | ||||||
|  |     def select(self,selected_keys): | ||||||
|  |         self.selected += selected_keys | ||||||
|  |         self.update() | ||||||
|  | 
 | ||||||
|  |     def deselect(self,selected_keys): | ||||||
|  |         for key in selected_keys: | ||||||
|  |             self.selected.remove(key) | ||||||
|  |         self.update() | ||||||
|  | 
 | ||||||
|  |     def update_will(self,will): | ||||||
|  |         self.will.update(will) | ||||||
|  |         self.update() | ||||||
|  | 
 | ||||||
|  |     def update(self): | ||||||
|  |         if self.will is None: | ||||||
|  |             return | ||||||
|  | 
 | ||||||
|  |         current_key = self.get_role_data_for_current_item(col=self.Columns.TXID, role=self.ROLE_HEIR_KEY) | ||||||
|  |         self.model().clear() | ||||||
|  |         self.update_headers(self.__class__.headers) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         set_current = None | ||||||
|  |         for txid,bal_tx in self.will.items(): | ||||||
|  |             if self.bal_window.bal_plugin._hide_replaced and bal_tx.get_status('REPLACED'): | ||||||
|  |                 continue | ||||||
|  |             if self.bal_window.bal_plugin._hide_invalidated and bal_tx.get_status('INVALIDATED'): | ||||||
|  |                 continue | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |             tx=bal_tx.tx | ||||||
|  |             labels = [""] * len(self.Columns) | ||||||
|  |             labels[self.Columns.LOCKTIME] = Util.locktime_to_str(tx.locktime) | ||||||
|  |             labels[self.Columns.TXID] = txid | ||||||
|  |             we = 'None' | ||||||
|  |             if bal_tx.we: | ||||||
|  |                 we = bal_tx.we['url'] | ||||||
|  |             labels[self.Columns.WILLEXECUTOR]=we | ||||||
|  |             status = bal_tx.status | ||||||
|  |             if len(bal_tx.status) > 53: | ||||||
|  |                 status = "...{}".format(status[-50:]) | ||||||
|  |             labels[self.Columns.STATUS] = status | ||||||
|  |              | ||||||
|  |              | ||||||
|  |              | ||||||
|  |             items=[] | ||||||
|  |             for e in labels: | ||||||
|  |                 if type(e)== list: | ||||||
|  |                     try: | ||||||
|  |                         items.append(QStandardItem(*e)) | ||||||
|  |                     except Exception as e: | ||||||
|  |                         pass | ||||||
|  |                 else: | ||||||
|  |                     items.append(QStandardItem(str(e))) | ||||||
|  | 
 | ||||||
|  |                 #pal = QPalette() | ||||||
|  |                 #pal.setColor(QPalette.ColorRole.Window, QColor(bal_tx.get_color())) | ||||||
|  |                 #items[-1].setAutoFillBackground(True) | ||||||
|  |                 #items[-1o].setPalette(pal) | ||||||
|  |                 items[-1].setBackground(QColor(bal_tx.get_color())) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |             self.model().insertRow(self.model().rowCount(), items) | ||||||
|  |             if txid == current_key: | ||||||
|  |                 idx = self.model().index(row_count, self.Columns.TXID) | ||||||
|  |                 set_current = QPersistentModelIndex(idx) | ||||||
|  |             self.set_current_idx(set_current) | ||||||
|  | 
 | ||||||
|  |     def create_toolbar(self, config):  | ||||||
|  |         toolbar, menu = self.create_toolbar_with_menu('')  | ||||||
|  |         menu.addAction(_("Prepare"), self.build_transactions)  | ||||||
|  |         menu.addAction(_("Display"), self.bal_window.preview_modal_dialog)  | ||||||
|  |         menu.addAction(_("Sign"), self.ask_password_and_sign_transactions) | ||||||
|  |         menu.addAction(_("Export"), self.export_will) | ||||||
|  |         #menu.addAction(_("Import"), self.import_will) | ||||||
|  |         menu.addAction(_("Broadcast"), self.broadcast) | ||||||
|  |         menu.addAction(_("Check"), self.check) | ||||||
|  |         menu.addAction(_("Invalidate"), self.invalidate_will) | ||||||
|  |         prepareButton = QPushButton(_("Prepare")) | ||||||
|  |         prepareButton.clicked.connect(self.build_transactions) | ||||||
|  |         signButton = QPushButton(_("Sign")) | ||||||
|  |         signButton.clicked.connect(self.ask_password_and_sign_transactions) | ||||||
|  |         pushButton = QPushButton(_("Broadcast")) | ||||||
|  |         pushButton.clicked.connect(self.broadcast) | ||||||
|  |         displayButton = QPushButton(_("Display")) | ||||||
|  |         displayButton.clicked.connect(self.bal_window.preview_modal_dialog) | ||||||
|  |         hlayout = QHBoxLayout() | ||||||
|  |         widget = QWidget() | ||||||
|  |         hlayout.addWidget(prepareButton) | ||||||
|  |         hlayout.addWidget(signButton) | ||||||
|  |         hlayout.addWidget(pushButton) | ||||||
|  |         hlayout.addWidget(displayButton) | ||||||
|  |         widget.setLayout(hlayout) | ||||||
|  |         toolbar.insertWidget(2,widget) | ||||||
|  | 
 | ||||||
|  |         return toolbar | ||||||
|  | 
 | ||||||
|  |     def hide_replaced(self): | ||||||
|  |         self.bal_window.bal_plugin.hide_replaced() | ||||||
|  |         self.update() | ||||||
|  | 
 | ||||||
|  |     def hide_invalidated(self): | ||||||
|  |         f.bal_window.bal_plugin.hide_invalidated() | ||||||
|  |         self.update() | ||||||
|  | 
 | ||||||
|  |     def build_transactions(self): | ||||||
|  |         will = self.bal_window.prepare_will() | ||||||
|  |         if will: | ||||||
|  |             self.update_will(will) | ||||||
|  | 
 | ||||||
|  |     def export_json_file(self,path): | ||||||
|  |         write_json_file(path, self.will) | ||||||
|  | 
 | ||||||
|  |     def export_will(self): | ||||||
|  |         self.bal_window.export_will() | ||||||
|  |         self.update() | ||||||
|  | 
 | ||||||
|  |     def import_will(self): | ||||||
|  |         self.bal_window.import_will() | ||||||
|  | 
 | ||||||
|  |     def ask_password_and_sign_transactions(self): | ||||||
|  |         self.bal_window.ask_password_and_sign_transactions(callback=self.update)  | ||||||
|  |                      | ||||||
|  |     def broadcast(self): | ||||||
|  |         self.bal_window.broadcast_transactions() | ||||||
|  |         self.update() | ||||||
|  | 
 | ||||||
|  |     def check(self): | ||||||
|  |         self.bal_window.check_transactions(self.bal_window.willitems) | ||||||
|  |         self.update() | ||||||
|  | 
 | ||||||
|  |     def invalidate_will(self): | ||||||
|  |         self.bal_window.invalidate_will() | ||||||
|  |         self.update() | ||||||
|  | 
 | ||||||
|  | class PreviewDialog(BalDialog,MessageBoxMixin): | ||||||
|  |     def __init__(self, bal_window, will): | ||||||
|  |         self.parent = bal_window.window | ||||||
|  |         BalDialog.__init__(self,bal_window = bal_window) | ||||||
|  |         self.bal_plugin = bal_window.bal_plugin | ||||||
|  |         self.gui_object = self.bal_plugin.gui_object | ||||||
|  |         self.config = self.bal_plugin.config | ||||||
|  |         self.bal_window = bal_window | ||||||
|  |         self.wallet = bal_window.window.wallet | ||||||
|  |         self.format_amount = bal_window.window.format_amount | ||||||
|  |         self.base_unit = bal_window.window.base_unit | ||||||
|  |         self.format_fiat_and_units = bal_window.window.format_fiat_and_units  | ||||||
|  |         self.fx = bal_window.window.fx  | ||||||
|  |         self.format_fee_rate = bal_window.window.format_fee_rate | ||||||
|  |         self.show_address = bal_window.window.show_address | ||||||
|  |         if not will: | ||||||
|  |             self.will = bal_window.willitems | ||||||
|  |         else: | ||||||
|  |             self.will = will | ||||||
|  |         self.setWindowTitle(_('Transactions Preview')) | ||||||
|  |         self.setMinimumSize(1000, 200) | ||||||
|  |         self.size_label = QLabel() | ||||||
|  |         self.transactions_list = PreviewList(self.bal_window,self.will) | ||||||
|  |         vbox = QVBoxLayout(self) | ||||||
|  |         vbox.addWidget(self.size_label) | ||||||
|  |         vbox.addWidget(self.transactions_list) | ||||||
|  |         buttonbox = QHBoxLayout() | ||||||
|  | 
 | ||||||
|  |         b = QPushButton(_('Sign')) | ||||||
|  |         b.clicked.connect(self.transactions_list.ask_password_and_sign_transactions) | ||||||
|  |         buttonbox.addWidget(b) | ||||||
|  | 
 | ||||||
|  |         b = QPushButton(_('Export Will')) | ||||||
|  |         b.clicked.connect(self.transactions_list.export_will) | ||||||
|  |         buttonbox.addWidget(b) | ||||||
|  | 
 | ||||||
|  |         b = QPushButton(_('Broadcast')) | ||||||
|  |         b.clicked.connect(self.transactions_list.broadcast) | ||||||
|  |         buttonbox.addWidget(b) | ||||||
|  | 
 | ||||||
|  |         b = QPushButton(_('Invalidate will')) | ||||||
|  |         b.clicked.connect(self.transactions_list.invalidate_will) | ||||||
|  |         buttonbox.addWidget(b) | ||||||
|  | 
 | ||||||
|  |         vbox.addLayout(buttonbox) | ||||||
|  | 
 | ||||||
|  |         self.update() | ||||||
|  |      | ||||||
|  | 
 | ||||||
|  |     def update_will(self,will): | ||||||
|  |         self.will.update(will) | ||||||
|  |         self.transactions_list.update_will(will) | ||||||
|  |         self.update() | ||||||
|  |      | ||||||
|  |     def update(self): | ||||||
|  |         self.transactions_list.update() | ||||||
|  | 
 | ||||||
|  |     def is_hidden(self): | ||||||
|  |         return self.isMinimized() or self.isHidden() | ||||||
|  | 
 | ||||||
|  |     def show_or_hide(self): | ||||||
|  |         if self.is_hidden(): | ||||||
|  |             self.bring_to_top() | ||||||
|  |         else: | ||||||
|  |             self.hide() | ||||||
|  | 
 | ||||||
|  |     def bring_to_top(self): | ||||||
|  |         self.show() | ||||||
|  |         self.raise_() | ||||||
|  | 
 | ||||||
|  |     def closeEvent(self, event): | ||||||
|  |         event.accept() | ||||||
							
								
								
									
										16
									
								
								balqt/qt_resources.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								balqt/qt_resources.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,16 @@ | |||||||
|  | from .. import bal_resources | ||||||
|  | import sys | ||||||
|  | try: | ||||||
|  |     QT_VERSION=sys._GUI_QT_VERSION | ||||||
|  | except: | ||||||
|  |     QT_VERSION=6 | ||||||
|  | if QT_VERSION == 5: | ||||||
|  |     from PyQt5.QtGui import QIcon,QPixmap | ||||||
|  | else: | ||||||
|  |     from PyQt6.QtGui import QIcon,QPixmap | ||||||
|  |      | ||||||
|  | def read_QIcon(icon_basename: str=bal_resources.DEFAULT_ICON) -> QIcon: | ||||||
|  |     return QIcon(bal_resources.icon_path(icon_basename)) | ||||||
|  | def read_QPixmap(icon_basename: str=bal_resources.DEFAULT_ICON) -> QPixmap: | ||||||
|  |     return QPixmap(bal_resources.icon_path(icon_basename)) | ||||||
|  | 
 | ||||||
							
								
								
									
										199
									
								
								balqt/willdetail.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										199
									
								
								balqt/willdetail.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,199 @@ | |||||||
|  | from functools import partial | ||||||
|  | 
 | ||||||
|  | from . import qt_resources | ||||||
|  | if qt_resources.QT_VERSION == 5: | ||||||
|  |     from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QLabel,QWidget,QScrollArea) | ||||||
|  |     from PyQt5.QtGui import (QPixmap, QImage, QBitmap, QPainter, QFontDatabase, QPen, QFont, | ||||||
|  |                      QColor, QDesktopServices, qRgba, QPainterPath,QPalette) | ||||||
|  | else: | ||||||
|  |     from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QLabel,QWidget,QScrollArea) | ||||||
|  |     from PyQt6.QtGui import (QPixmap, QImage, QBitmap, QPainter, QFontDatabase, QPen, QFont, | ||||||
|  |                      QColor, QDesktopServices, qRgba, QPainterPath,QPalette) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | from electrum.util import decimal_point_to_base_unit_name | ||||||
|  | from electrum.i18n import _ | ||||||
|  | 
 | ||||||
|  | from ..bal import BalPlugin | ||||||
|  | from .. import will as Will | ||||||
|  | from .. import util as Util | ||||||
|  | from .baldialog import BalDialog | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |      | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class WillDetailDialog(BalDialog): | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     def __init__(self, bal_window): | ||||||
|  | 
 | ||||||
|  |         self.will = bal_window.willitems | ||||||
|  |         self.threshold = Util.parse_locktime_string(bal_window.will_settings['threshold']) | ||||||
|  |         self.bal_window = bal_window  | ||||||
|  |         Will.add_willtree(self.will) | ||||||
|  |         super().__init__(bal_window.window) | ||||||
|  |         self.config = bal_window.window.config | ||||||
|  |         self.wallet = bal_window.wallet | ||||||
|  |         self.format_amount = bal_window.window.format_amount | ||||||
|  |         self.base_unit = bal_window.window.base_unit | ||||||
|  |         self.format_fiat_and_units = bal_window.window.format_fiat_and_units | ||||||
|  |         self.fx = bal_window.window.fx | ||||||
|  |         self.format_fee_rate = bal_window.window.format_fee_rate | ||||||
|  |         self.decimal_point = bal_window.bal_plugin.config.get_decimal_point() | ||||||
|  |         self.base_unit_name = decimal_point_to_base_unit_name(self.decimal_point) | ||||||
|  |         self.setWindowTitle(_('Will Details')) | ||||||
|  |         self.setMinimumSize(670,700) | ||||||
|  |         self.vlayout= QVBoxLayout() | ||||||
|  |         w=QWidget() | ||||||
|  |         hlayout = QHBoxLayout(w) | ||||||
|  | 
 | ||||||
|  |         b = QPushButton(_('Sign')) | ||||||
|  |         b.clicked.connect(self.ask_password_and_sign_transactions) | ||||||
|  |         hlayout.addWidget(b) | ||||||
|  |   | ||||||
|  |         b = QPushButton(_('Broadcast'))  | ||||||
|  |         b.clicked.connect(self.broadcast_transactions)  | ||||||
|  |         hlayout.addWidget(b)  | ||||||
|  | 
 | ||||||
|  |         b = QPushButton(_('Export')) | ||||||
|  |         b.clicked.connect(self.export_will) | ||||||
|  |         hlayout.addWidget(b) | ||||||
|  |         """ | ||||||
|  |         toggle = "Hide" | ||||||
|  |         if self.bal_window.bal_plugin._hide_replaced: | ||||||
|  |             toggle = "Unhide" | ||||||
|  |         self.toggle_replace_button = QPushButton(_(f"{toggle} replaced")) | ||||||
|  |         self.toggle_replace_button.clicked.connect(self.toggle_replaced) | ||||||
|  |         hlayout.addWidget(self.toggle_replace_button) | ||||||
|  | 
 | ||||||
|  |         toggle = "Hide" | ||||||
|  |         if self.bal_window.bal_plugin._hide_invalidated: | ||||||
|  |             toggle = "Unhide" | ||||||
|  | 
 | ||||||
|  |         self.toggle_invalidate_button = QPushButton(_(f"{toggle} invalidated")) | ||||||
|  |         self.toggle_invalidate_button.clicked.connect(self.toggle_invalidated) | ||||||
|  |         hlayout.addWidget(self.toggle_invalidate_button) | ||||||
|  |         """ | ||||||
|  |         b = QPushButton(_('Invalidate')) | ||||||
|  |         b.clicked.connect(bal_window.invalidate_will) | ||||||
|  |         hlayout.addWidget(b) | ||||||
|  |         self.vlayout.addWidget(w) | ||||||
|  | 
 | ||||||
|  |         self.paint_scroll_area() | ||||||
|  |         #vlayout.addWidget(QLabel(_("DON'T PANIC !!! everything is fine, all possible futures are covered"))) | ||||||
|  |         self.vlayout.addWidget(QLabel(_("Expiration date: ")+Util.locktime_to_str(self.threshold))) | ||||||
|  |         self.vlayout.addWidget(self.scrollbox) | ||||||
|  |         w=QWidget() | ||||||
|  |         hlayout = QHBoxLayout(w) | ||||||
|  |         hlayout.addWidget(QLabel(_("Valid Txs:")+ str(len(Will.only_valid_list(self.will))))) | ||||||
|  |         hlayout.addWidget(QLabel(_("Total Txs:")+ str(len(self.will)))) | ||||||
|  |         self.vlayout.addWidget(w) | ||||||
|  |         self.setLayout(self.vlayout) | ||||||
|  | 
 | ||||||
|  |     def paint_scroll_area(self): | ||||||
|  |         #self.scrollbox.deleteLater() | ||||||
|  |         #self.willlayout.deleteLater() | ||||||
|  |         #self.detailsWidget.deleteLater() | ||||||
|  |         self.scrollbox = QScrollArea() | ||||||
|  |         viewport = QWidget(self.scrollbox) | ||||||
|  |         self.willlayout = QVBoxLayout(viewport) | ||||||
|  |         self.detailsWidget = WillWidget(parent=self) | ||||||
|  |         self.willlayout.addWidget(self.detailsWidget) | ||||||
|  | 
 | ||||||
|  |         self.scrollbox.setWidget(viewport) | ||||||
|  |         viewport.setLayout(self.willlayout) | ||||||
|  |     def ask_password_and_sign_transactions(self): | ||||||
|  |         self.bal_window.ask_password_and_sign_transactions(callback=self.update) | ||||||
|  |         self.update() | ||||||
|  |     def broadcast_transactions(self): | ||||||
|  |         self.bal_window.broadcast_transactions() | ||||||
|  |         self.update() | ||||||
|  |     def export_will(self): | ||||||
|  |         self.bal_window.export_will() | ||||||
|  |     def toggle_replaced(self): | ||||||
|  |         self.bal_window.bal_plugin.hide_replaced() | ||||||
|  |         toggle = _("Hide") | ||||||
|  |         if self.bal_window.bal_plugin._hide_replaced: | ||||||
|  |             toggle = _("Unhide") | ||||||
|  |         self.toggle_replace_button.setText(f"{toggle} {_('replaced')}") | ||||||
|  |         self.update() | ||||||
|  | 
 | ||||||
|  |     def toggle_invalidated(self): | ||||||
|  |         self.bal_window.bal_plugin.hide_invalidated() | ||||||
|  |         toggle = _("Hide") | ||||||
|  |         if self.bal_window.bal_plugin._hide_invalidated: | ||||||
|  |             toggle = _("Unhide") | ||||||
|  |         self.toggle_invalidate_button.setText(_(f"{toggle} {_('invalidated')}")) | ||||||
|  |         self.update() | ||||||
|  | 
 | ||||||
|  |     def update(self): | ||||||
|  |         self.will = self.bal_window.willitems | ||||||
|  |         pos = self.vlayout.indexOf(self.scrollbox) | ||||||
|  |         self.vlayout.removeWidget(self.scrollbox) | ||||||
|  |         self.paint_scroll_area() | ||||||
|  |         self.vlayout.insertWidget(pos,self.scrollbox) | ||||||
|  |         super().update() | ||||||
|  | 
 | ||||||
|  | class WillWidget(QWidget): | ||||||
|  |     def __init__(self,father=None,parent = None): | ||||||
|  |         super().__init__() | ||||||
|  |         vlayout = QVBoxLayout() | ||||||
|  |         self.setLayout(vlayout) | ||||||
|  |         self.will = parent.bal_window.willitems | ||||||
|  |         self.parent = parent | ||||||
|  |         for w in self.will: | ||||||
|  |             if self.will[w].get_status('REPLACED') and self.parent.bal_window.bal_plugin._hide_replaced: | ||||||
|  |                 continue | ||||||
|  |             if self.will[w].get_status('INVALIDATED') and self.parent.bal_window.bal_plugin._hide_invalidated: | ||||||
|  |                 continue | ||||||
|  |             f = self.will[w].father | ||||||
|  |             if father == f: | ||||||
|  |                 qwidget = QWidget() | ||||||
|  |                 childWidget = QWidget() | ||||||
|  |                 hlayout=QHBoxLayout(qwidget) | ||||||
|  |                 qwidget.setLayout(hlayout) | ||||||
|  |                 vlayout.addWidget(qwidget) | ||||||
|  |                 detailw=QWidget() | ||||||
|  |                 detaillayout=QVBoxLayout() | ||||||
|  |                 detailw.setLayout(detaillayout) | ||||||
|  | 
 | ||||||
|  |                 willpushbutton = QPushButton(w) | ||||||
|  | 
 | ||||||
|  |                 willpushbutton.clicked.connect(partial(self.parent.bal_window.show_transaction,txid=w)) | ||||||
|  |                 detaillayout.addWidget(willpushbutton) | ||||||
|  |                 locktime = Util.locktime_to_str(self.will[w].tx.locktime) | ||||||
|  |                 creation = Util.locktime_to_str(self.will[w].time) | ||||||
|  |                 def qlabel(title,value): | ||||||
|  |                     label = "<b>"+_(str(title)) + f":</b>\t{str(value)}" | ||||||
|  |                     return QLabel(label) | ||||||
|  |                 detaillayout.addWidget(qlabel("Locktime",locktime)) | ||||||
|  |                 detaillayout.addWidget(qlabel("Creation Time",creation)) | ||||||
|  |                 total_fees = self.will[w].tx.input_value() - self.will[w].tx.output_value() | ||||||
|  |                 decoded_fees = total_fees #Util.decode_amount(total_fees,self.parent.decimal_point) | ||||||
|  |                 fee_per_byte = round(total_fees/self.will[w].tx.estimated_size(),3) | ||||||
|  |                 fees_str = str(decoded_fees) + " ("+  str(fee_per_byte) + " sats/vbyte)" | ||||||
|  |                 detaillayout.addWidget(qlabel("Transaction fees:",fees_str)) | ||||||
|  |                 detaillayout.addWidget(qlabel("Status:",self.will[w].status)) | ||||||
|  |                 detaillayout.addWidget(QLabel("")) | ||||||
|  |                 detaillayout.addWidget(QLabel("<b>Heirs:</b>")) | ||||||
|  |                 for heir in self.will[w].heirs: | ||||||
|  |                     if "w!ll3x3c\"" not in heir: | ||||||
|  |                         decoded_amount = Util.decode_amount(self.will[w].heirs[heir][3],self.parent.decimal_point) | ||||||
|  |                         detaillayout.addWidget(qlabel(heir,f"{decoded_amount} {self.parent.base_unit_name}")) | ||||||
|  |                 if self.will[w].we: | ||||||
|  |                     detaillayout.addWidget(QLabel("")) | ||||||
|  |                     detaillayout.addWidget(QLabel(_("<b>Willexecutor:</b:"))) | ||||||
|  |                     decoded_amount = Util.decode_amount(self.will[w].we['base_fee'],self.parent.decimal_point) | ||||||
|  | 
 | ||||||
|  |                     detaillayout.addWidget(qlabel(self.will[w].we['url'],f"{decoded_amount} {self.parent.base_unit_name}")) | ||||||
|  |                 detaillayout.addStretch() | ||||||
|  |                 pal = QPalette() | ||||||
|  |                 pal.setColor(QPalette.ColorRole.Window, QColor(self.will[w].get_color())) | ||||||
|  |                 detailw.setAutoFillBackground(True) | ||||||
|  |                 detailw.setPalette(pal) | ||||||
|  | 
 | ||||||
|  |                 hlayout.addWidget(detailw) | ||||||
|  |                 hlayout.addWidget(WillWidget(w,parent = parent)) | ||||||
|  | 
 | ||||||
							
								
								
									
										292
									
								
								balqt/willexecutor_dialog.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										292
									
								
								balqt/willexecutor_dialog.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,292 @@ | |||||||
|  | import enum | ||||||
|  | import json | ||||||
|  | import urllib.request | ||||||
|  | import urllib.parse | ||||||
|  | 
 | ||||||
|  | from . import qt_resources | ||||||
|  | if qt_resources.QT_VERSION == 5: | ||||||
|  |     from PyQt5.QtGui import QStandardItemModel, QStandardItem | ||||||
|  |     from PyQt5.QtCore import Qt,QPersistentModelIndex, QModelIndex | ||||||
|  |     from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QLabel,QMenu) | ||||||
|  | else: | ||||||
|  |     from PyQt6.QtGui import QStandardItemModel, QStandardItem | ||||||
|  |     from PyQt6.QtCore import Qt,QPersistentModelIndex, QModelIndex | ||||||
|  |     from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QLabel,QMenu) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | from electrum.i18n import _ | ||||||
|  | from electrum.gui.qt.util import (Buttons,read_QIcon, import_meta_gui, export_meta_gui,MessageBoxMixin) | ||||||
|  | from electrum.util import write_json_file,read_json_file | ||||||
|  | from electrum.gui.qt.my_treeview import MyTreeView | ||||||
|  | 
 | ||||||
|  | from ..bal import BalPlugin | ||||||
|  | from .. import util as Util | ||||||
|  | from .. import willexecutors as Willexecutors | ||||||
|  | from .baldialog import BalDialog,BalBlockingWaitingDialog | ||||||
|  | from electrum.logging import get_logger,Logger | ||||||
|  | 
 | ||||||
|  | _logger=get_logger(__name__) | ||||||
|  | class WillExecutorList(MyTreeView): | ||||||
|  |     class Columns(MyTreeView.BaseColumnsEnum): | ||||||
|  |         SELECTED = enum.auto() | ||||||
|  |         URL = enum.auto() | ||||||
|  |         BASE_FEE = enum.auto() | ||||||
|  |         INFO = enum.auto() | ||||||
|  |         ADDRESS = enum.auto() | ||||||
|  |         STATUS = enum.auto() | ||||||
|  | 
 | ||||||
|  |     headers = { | ||||||
|  |         Columns.SELECTED:_(''), | ||||||
|  |         Columns.URL: _('Url'), | ||||||
|  |         Columns.BASE_FEE: _('Base fee'), | ||||||
|  |         Columns.INFO:_('Info'), | ||||||
|  |         Columns.ADDRESS:_('Default Address'), | ||||||
|  |         Columns.STATUS: _('S'), | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     ROLE_HEIR_KEY = Qt.ItemDataRole.UserRole + 2000 | ||||||
|  |     key_role = ROLE_HEIR_KEY | ||||||
|  | 
 | ||||||
|  |     def __init__(self, parent: 'WillExecutorDialog'): | ||||||
|  |         super().__init__( | ||||||
|  |             parent=parent, | ||||||
|  |             stretch_column=self.Columns.ADDRESS, | ||||||
|  |             editable_columns=[self.Columns.URL,self.Columns.BASE_FEE,self.Columns.ADDRESS,self.Columns.INFO], | ||||||
|  | 
 | ||||||
|  |         ) | ||||||
|  |         self.parent = parent | ||||||
|  |         self.setModel(QStandardItemModel(self)) | ||||||
|  |         self.setSortingEnabled(True) | ||||||
|  |         self.std_model = self.model() | ||||||
|  |         self.config =parent.bal_plugin.config | ||||||
|  |             | ||||||
|  | 
 | ||||||
|  |         self.update() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     def create_menu(self, position): | ||||||
|  |         menu = QMenu() | ||||||
|  |         idx = self.indexAt(position) | ||||||
|  |         column = idx.column() or self.Columns.URL | ||||||
|  |         selected_keys = [] | ||||||
|  |         for s_idx in self.selected_in_column(self.Columns.URL): | ||||||
|  |             sel_key = self.model().itemFromIndex(s_idx).data(0) | ||||||
|  |             selected_keys.append(sel_key) | ||||||
|  |         if selected_keys and idx.isValid(): | ||||||
|  |             column_title = self.model().horizontalHeaderItem(column).text() | ||||||
|  |             column_data = '\n'.join(self.model().itemFromIndex(s_idx).text() | ||||||
|  |                                     for s_idx in self.selected_in_column(column)) | ||||||
|  |             if Willexecutors.is_selected(self.parent.willexecutors_list[sel_key]): | ||||||
|  |                 menu.addAction(_("deselect").format(column_title), lambda: self.deselect(selected_keys)) | ||||||
|  |             else: | ||||||
|  |                 menu.addAction(_("select").format(column_title), lambda: self.select(selected_keys)) | ||||||
|  |             if column in self.editable_columns: | ||||||
|  |                 item = self.model().itemFromIndex(idx) | ||||||
|  |                 if item.isEditable(): | ||||||
|  |                     persistent = QPersistentModelIndex(idx) | ||||||
|  |                     menu.addAction(_("Edit {}").format(column_title), lambda p=persistent: self.edit(QModelIndex(p))) | ||||||
|  | 
 | ||||||
|  |             menu.addAction(_("Ping").format(column_title), lambda: self.ping_willexecutors(selected_keys)) | ||||||
|  |             menu.addSeparator() | ||||||
|  |             menu.addAction(_("delete").format(column_title), lambda: self.delete(selected_keys)) | ||||||
|  | 
 | ||||||
|  |         menu.exec(self.viewport().mapToGlobal(position)) | ||||||
|  | 
 | ||||||
|  |     def ping_willexecutors(self,selected_keys): | ||||||
|  |         wout={} | ||||||
|  |         for k in selected_keys: | ||||||
|  |             wout[k]=self.parent.willexecutors_list[k] | ||||||
|  |         self.parent.update_willexecutors(wout) | ||||||
|  |         self.update() | ||||||
|  | 
 | ||||||
|  |     def get_edit_key_from_coordinate(self, row, col): | ||||||
|  |         a= self.get_role_data_from_coordinate(row, col, role=self.ROLE_HEIR_KEY+col) | ||||||
|  |         return a | ||||||
|  | 
 | ||||||
|  |     def delete(self,selected_keys): | ||||||
|  |         for key in selected_keys: | ||||||
|  |             del self.parent.willexecutors_list[key] | ||||||
|  |         self.update() | ||||||
|  | 
 | ||||||
|  |     def select(self,selected_keys): | ||||||
|  |         for wid,w in self.parent.willexecutors_list.items(): | ||||||
|  |             if wid in selected_keys: | ||||||
|  |                 w['selected']=True | ||||||
|  |         self.update() | ||||||
|  | 
 | ||||||
|  |     def deselect(self,selected_keys): | ||||||
|  |         for wid,w in self.parent.willexecutors_list.items(): | ||||||
|  |             if wid in selected_keys: | ||||||
|  |                 w['selected']=False | ||||||
|  |         self.update() | ||||||
|  | 
 | ||||||
|  |     def on_edited(self, idx, edit_key, *, text): | ||||||
|  |         prior_name = self.parent.willexecutors_list[edit_key] | ||||||
|  |         col = idx.column() | ||||||
|  |         try: | ||||||
|  |             if col == self.Columns.URL: | ||||||
|  |                 self.parent.willexecutors_list[text]=self.parent.willexecutors_list[edit_key] | ||||||
|  |                 del self.parent.willexecutors_list[edit_key] | ||||||
|  |             if col == self.Columns.BASE_FEE: | ||||||
|  |                 self.parent.willexecutors_list[edit_key]["base_fee"] = Util.encode_amount(text,self.config.get_decimal_point()) | ||||||
|  |             if col == self.Columns.ADDRESS: | ||||||
|  |                 self.parent.willexecutors_list[edit_key]["address"] = text | ||||||
|  |             if col == self.Columns.INFO: | ||||||
|  |                 self.parent.willexecutors_list[edit_key]["info"] = text | ||||||
|  |             self.update() | ||||||
|  |         except Exception as e: | ||||||
|  |             pass | ||||||
|  | 
 | ||||||
|  |     def update(self): | ||||||
|  |         if self.parent.willexecutors_list is None: | ||||||
|  |             return | ||||||
|  |         try:  | ||||||
|  |             current_key = self.get_role_data_for_current_item(col=self.Columns.URL, role=self.ROLE_HEIR_KEY) | ||||||
|  |             self.model().clear() | ||||||
|  |             self.update_headers(self.__class__.headers) | ||||||
|  | 
 | ||||||
|  |             set_current = None | ||||||
|  | 
 | ||||||
|  |             for url, value in self.parent.willexecutors_list.items(): | ||||||
|  |                 labels = [""] * len(self.Columns) | ||||||
|  |                 labels[self.Columns.URL] = url  | ||||||
|  |                 if Willexecutors.is_selected(value): | ||||||
|  |                     labels[self.Columns.SELECTED] = [read_QIcon('confirmed.png'),''] | ||||||
|  |                 else: | ||||||
|  |                     labels[self.Columns.SELECTED] = '' | ||||||
|  |                 labels[self.Columns.BASE_FEE] = Util.decode_amount(value.get('base_fee',0),self.config.get_decimal_point()) | ||||||
|  |                 if str(value.get('status',0)) == "200": | ||||||
|  |                     labels[self.Columns.STATUS] = [read_QIcon('status_connected.png'),''] | ||||||
|  |                 else: | ||||||
|  |                     labels[self.Columns.STATUS] = [read_QIcon('unconfirmed.png'),''] | ||||||
|  |                 labels[self.Columns.ADDRESS] = str(value.get('address','')) | ||||||
|  |                 labels[self.Columns.INFO] = str(value.get('info','')) | ||||||
|  |                  | ||||||
|  |                 items=[] | ||||||
|  |                 for e in labels: | ||||||
|  |                     if type(e)== list: | ||||||
|  |                         try: | ||||||
|  |                             items.append(QStandardItem(*e)) | ||||||
|  |                         except Exception as e: | ||||||
|  |                             pass | ||||||
|  |                     else: | ||||||
|  |                         items.append(QStandardItem(e)) | ||||||
|  |                 items[self.Columns.SELECTED].setEditable(False) | ||||||
|  |                 items[self.Columns.URL].setEditable(True) | ||||||
|  |                 items[self.Columns.ADDRESS].setEditable(True) | ||||||
|  |                 items[self.Columns.INFO].setEditable(True) | ||||||
|  |                 items[self.Columns.BASE_FEE].setEditable(True) | ||||||
|  |                 items[self.Columns.STATUS].setEditable(False) | ||||||
|  | 
 | ||||||
|  |                 items[self.Columns.URL].setData(url, self.ROLE_HEIR_KEY+1) | ||||||
|  |                 items[self.Columns.BASE_FEE].setData(url, self.ROLE_HEIR_KEY+2) | ||||||
|  |                 items[self.Columns.INFO].setData(url, self.ROLE_HEIR_KEY+3) | ||||||
|  |                 items[self.Columns.ADDRESS].setData(url, self.ROLE_HEIR_KEY+4) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |                 self.model().insertRow(self.model().rowCount(), items) | ||||||
|  |                 if url == current_key: | ||||||
|  |                     idx = self.model().index(row_count, self.Columns.NAME) | ||||||
|  |                     set_current = QPersistentModelIndex(idx) | ||||||
|  |                 self.set_current_idx(set_current) | ||||||
|  |             self.parent.save_willexecutors() | ||||||
|  |         except Exception as e: | ||||||
|  |             _logger.error(e) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class WillExecutorDialog(BalDialog,MessageBoxMixin): | ||||||
|  |     def __init__(self, bal_window): | ||||||
|  |         BalDialog.__init__(self,bal_window.window) | ||||||
|  |         self.bal_plugin = bal_window.bal_plugin | ||||||
|  |         self.gui_object = self.bal_plugin.gui_object | ||||||
|  |         self.config = self.bal_plugin.config | ||||||
|  |         self.window = bal_window.window | ||||||
|  |         self.bal_window = bal_window | ||||||
|  |         self.willexecutors_list = Willexecutors.get_willexecutors(self.bal_plugin) | ||||||
|  | 
 | ||||||
|  |         self.setWindowTitle(_('Will-Executor Service List')) | ||||||
|  |         self.setMinimumSize(1000, 200) | ||||||
|  |         self.size_label = QLabel() | ||||||
|  |         self.willexecutor_list = WillExecutorList(self) | ||||||
|  | 
 | ||||||
|  |         vbox = QVBoxLayout(self) | ||||||
|  |         vbox.addWidget(self.size_label) | ||||||
|  |         vbox.addWidget(self.willexecutor_list) | ||||||
|  |         buttonbox = QHBoxLayout() | ||||||
|  | 
 | ||||||
|  |         b = QPushButton(_('Ping')) | ||||||
|  |         b.clicked.connect(self.update_willexecutors) | ||||||
|  |         buttonbox.addWidget(b) | ||||||
|  | 
 | ||||||
|  |         b = QPushButton(_('Import')) | ||||||
|  |         b.clicked.connect(self.import_file) | ||||||
|  |         buttonbox.addWidget(b) | ||||||
|  | 
 | ||||||
|  |         b = QPushButton(_('Export')) | ||||||
|  |         b.clicked.connect(self.export_file) | ||||||
|  |         buttonbox.addWidget(b) | ||||||
|  | 
 | ||||||
|  |         b = QPushButton(_('Add')) | ||||||
|  |         b.clicked.connect(self.add) | ||||||
|  |         buttonbox.addWidget(b) | ||||||
|  |          | ||||||
|  |         b = QPushButton(_('Close')) | ||||||
|  |         b.clicked.connect(self.close) | ||||||
|  |         buttonbox.addWidget(b) | ||||||
|  | 
 | ||||||
|  |         vbox.addLayout(buttonbox) | ||||||
|  | 
 | ||||||
|  |         self.willexecutor_list.update() | ||||||
|  | 
 | ||||||
|  |     def add(self): | ||||||
|  |         self.willexecutors_list["http://localhost:8080"]={"info":"New Will Executor","base_fee":0,"status":"-1"} | ||||||
|  |         self.willexecutor_list.update()      | ||||||
|  | 
 | ||||||
|  |     def import_file(self): | ||||||
|  |         import_meta_gui(self, _('willexecutors.json'), self.import_json_file, self.willexecutors_list.update) | ||||||
|  | 
 | ||||||
|  |     def export_file(self, path): | ||||||
|  |         Util.export_meta_gui(self, _('willexecutors.json'), self.export_json_file) | ||||||
|  | 
 | ||||||
|  |     def export_json_file(self,path): | ||||||
|  |         write_json_file(path, self.willexecutors_list) | ||||||
|  | 
 | ||||||
|  |     def update_willexecutors(self,wes=None): | ||||||
|  |         if not wes: | ||||||
|  |             self.willexecutors_list = Willexecutors.get_willexecutors(self.bal_plugin, update = True, bal_window = self.bal_window,force=True) | ||||||
|  |         else: | ||||||
|  |             self.bal_window.ping_willexecutors(wes) | ||||||
|  |             self.willexecutors_list.update(wes) | ||||||
|  |             self.willexecutor_list.update() | ||||||
|  | 
 | ||||||
|  |          | ||||||
|  |     def import_json_file(self, path): | ||||||
|  |         data = read_json_file(path) | ||||||
|  |         data = self._validate(data) | ||||||
|  |         self.willexecutors_list.update(data) | ||||||
|  |         self.willexecutor_list.update() | ||||||
|  | 
 | ||||||
|  |     #TODO validate willexecutor json import file | ||||||
|  |     def _validate(self,data): | ||||||
|  |         return data | ||||||
|  | 
 | ||||||
|  |     def is_hidden(self): | ||||||
|  |         return self.isMinimized() or self.isHidden() | ||||||
|  | 
 | ||||||
|  |     def show_or_hide(self): | ||||||
|  |         if self.is_hidden(): | ||||||
|  |             self.bring_to_top() | ||||||
|  |         else: | ||||||
|  |             self.hide() | ||||||
|  | 
 | ||||||
|  |     def bring_to_top(self): | ||||||
|  |         self.show() | ||||||
|  |         self.raise_() | ||||||
|  | 
 | ||||||
|  |     def closeEvent(self, event): | ||||||
|  |         event.accept() | ||||||
|  | 
 | ||||||
|  |     def save_willexecutors(self): | ||||||
|  |         self.bal_plugin.config.set_key(self.bal_plugin.WILLEXECUTORS,self.willexecutors_list,save=True) | ||||||
|  |      | ||||||
							
								
								
									
										609
									
								
								heirs.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										609
									
								
								heirs.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,609 @@ | |||||||
|  | import re | ||||||
|  | import json | ||||||
|  | from typing import Optional, Tuple, Dict, Any, TYPE_CHECKING,Sequence,List | ||||||
|  | 
 | ||||||
|  | import dns | ||||||
|  | import threading | ||||||
|  | import math | ||||||
|  | from dns.exception import DNSException | ||||||
|  | 
 | ||||||
|  | from electrum import bitcoin,dnssec,descriptor,constants | ||||||
|  | from electrum.util import read_json_file, write_json_file, to_string,bfh,trigger_callback | ||||||
|  | from electrum.logging import Logger, get_logger | ||||||
|  | from electrum.transaction import PartialTxInput, PartialTxOutput,TxOutpoint,PartialTransaction,TxOutput | ||||||
|  | import datetime | ||||||
|  | import urllib.request | ||||||
|  | import urllib.parse | ||||||
|  | from .bal import BalPlugin | ||||||
|  | from . import util as Util | ||||||
|  | from . import willexecutors as Willexecutors | ||||||
|  | if TYPE_CHECKING: | ||||||
|  |     from .wallet_db import WalletDB | ||||||
|  |     from .simple_config import SimpleConfig | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | _logger = get_logger(__name__) | ||||||
|  | 
 | ||||||
|  | HEIR_ADDRESS = 0 | ||||||
|  | HEIR_AMOUNT = 1 | ||||||
|  | HEIR_LOCKTIME = 2 | ||||||
|  | HEIR_REAL_AMOUNT = 3 | ||||||
|  | TRANSACTION_LABEL = "inheritance transaction" | ||||||
|  | class AliasNotFoundException(Exception): | ||||||
|  |     pass | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def reduce_outputs(in_amount, out_amount, fee, outputs): | ||||||
|  |     if in_amount < out_amount: | ||||||
|  |         for output in outputs: | ||||||
|  |             output.value = math.floor((in_amount-fee)/out_amount * output.value) | ||||||
|  | 
 | ||||||
|  | #TODO: put this method inside wallet.db to replace or complete get_locktime_for_new_transaction | ||||||
|  | def get_current_height(network:'Network'): | ||||||
|  |     #if no network or not up to date, just set locktime to zero | ||||||
|  |     if not network: | ||||||
|  |         return 0 | ||||||
|  |     chain = network.blockchain() | ||||||
|  |     if chain.is_tip_stale(): | ||||||
|  |         return 0 | ||||||
|  |     # figure out current block height | ||||||
|  |     chain_height = chain.height()  # learnt from all connected servers, SPV-checked | ||||||
|  |     server_height = network.get_server_height()  # height claimed by main server, unverified | ||||||
|  |     # note: main server might be lagging (either is slow, is malicious, or there is an SPV-invisible-hard-fork) | ||||||
|  |     #       - if it's lagging too much, it is the network's job to switch away | ||||||
|  |     if server_height < chain_height - 10: | ||||||
|  |         # the diff is suspiciously large... give up and use something non-fingerprintable | ||||||
|  |         return 0 | ||||||
|  |     # discourage "fee sniping" | ||||||
|  |     height = min(chain_height, server_height) | ||||||
|  |     return height | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def prepare_transactions(locktimes, available_utxos, fees, wallet): | ||||||
|  |     available_utxos=sorted(available_utxos, key=lambda x:"{}:{}:{}".format(x.value_sats(),x.prevout.txid,x.prevout.out_idx)) | ||||||
|  |     total_used_utxos = [] | ||||||
|  |     txsout={} | ||||||
|  |     locktime,_=Util.get_lowest_locktimes(locktimes) | ||||||
|  |     if not locktime: | ||||||
|  |         return | ||||||
|  |     locktime=locktime[0] | ||||||
|  | 
 | ||||||
|  |     heirs = locktimes[locktime] | ||||||
|  |     vero=True | ||||||
|  |     while vero: | ||||||
|  |         vero=False | ||||||
|  |         fee=fees.get(locktime,0)  | ||||||
|  |         out_amount = fee | ||||||
|  |         description = "" | ||||||
|  |         outputs = [] | ||||||
|  |         paid_heirs = {} | ||||||
|  |         for name,heir in heirs.items(): | ||||||
|  | 
 | ||||||
|  |             try: | ||||||
|  |                 if len(heir)>HEIR_REAL_AMOUNT: | ||||||
|  |                     real_amount = heir[HEIR_REAL_AMOUNT] | ||||||
|  |                     out_amount += real_amount | ||||||
|  |                     description += f"{name}\n" | ||||||
|  |                     paid_heirs[name]=heir | ||||||
|  |                     outputs.append(PartialTxOutput.from_address_and_value(heir[HEIR_ADDRESS], real_amount)) | ||||||
|  |                 else: | ||||||
|  |                     pass | ||||||
|  |             except Exception as e: | ||||||
|  |                 pass | ||||||
|  | 
 | ||||||
|  |         in_amount = 0.0 | ||||||
|  |         used_utxos = [] | ||||||
|  |         try: | ||||||
|  |             while utxo := available_utxos.pop(): | ||||||
|  |                 value = utxo.value_sats() | ||||||
|  |                 in_amount += value | ||||||
|  |                 used_utxos.append(utxo) | ||||||
|  |                 if in_amount > out_amount: | ||||||
|  |                     break | ||||||
|  | 
 | ||||||
|  |         except IndexError as e: | ||||||
|  |             pass | ||||||
|  |         if int(in_amount) < int(out_amount): | ||||||
|  |             break | ||||||
|  |         heirsvalue=out_amount | ||||||
|  |         change = get_change_output(wallet, in_amount, out_amount, fee) | ||||||
|  |         if change: | ||||||
|  |             outputs.append(change) | ||||||
|  | 
 | ||||||
|  |         tx = PartialTransaction.from_io(used_utxos, outputs, locktime=Util.parse_locktime_string(locktime,wallet), version=2) | ||||||
|  |         if len(description)>0: tx.description = description[:-1] | ||||||
|  |         else: tx.description = "" | ||||||
|  |         tx.heirsvalue = heirsvalue | ||||||
|  |         tx.set_rbf(True)  | ||||||
|  |         tx.remove_signatures() | ||||||
|  |         txid = tx.txid() | ||||||
|  |         if txid is None: | ||||||
|  |             raise Exception("txid is none",tx) | ||||||
|  |          | ||||||
|  |         tx.heirs = paid_heirs | ||||||
|  |         tx.my_locktime = locktime | ||||||
|  |         txsout[txid]=tx | ||||||
|  |           | ||||||
|  |         if change: | ||||||
|  |             change_idx=tx.get_output_idxs_from_address(change.address) | ||||||
|  |             prevout = TxOutpoint(txid=bfh(tx.txid()), out_idx=change_idx.pop()) | ||||||
|  |             txin = PartialTxInput(prevout=prevout) | ||||||
|  |             txin._trusted_value_sats = change.value | ||||||
|  |             txin.script_descriptor = change.script_descriptor | ||||||
|  |             txin.is_mine=True | ||||||
|  |             txin._TxInput__address=change.address | ||||||
|  |             txin._TxInput__scriptpubkey = change.scriptpubkey | ||||||
|  |             txin._TxInput__value_sats = change.value | ||||||
|  |             txin.utxo = tx | ||||||
|  |             available_utxos.append(txin) | ||||||
|  |         txsout[txid].available_utxos = available_utxos[:] | ||||||
|  |     return txsout | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def get_utxos_from_inputs(tx_inputs,tx,utxos): | ||||||
|  |     for tx_input in tx_inputs: | ||||||
|  |         prevoutstr=tx_input.prevout.to_str() | ||||||
|  |         utxos[prevoutstr] =utxos.get(prevoutstr,{'input':tx_input,'txs':[]}) | ||||||
|  |         utxos[prevoutstr]['txs'].append(tx) | ||||||
|  |     return utxos | ||||||
|  | 
 | ||||||
|  | #TODO calculate de minimum inputs to be invalidated  | ||||||
|  | def invalidate_inheritance_transactions(wallet): | ||||||
|  |     listids = [] | ||||||
|  |     utxos = {} | ||||||
|  |     dtxs = {} | ||||||
|  |     for k,v in wallet.get_all_labels().items(): | ||||||
|  |         tx = None | ||||||
|  |         if TRANSACTION_LABEL == v: | ||||||
|  |             tx=wallet.adb.get_transaction(k) | ||||||
|  |         if tx: | ||||||
|  |             dtxs[tx.txid()]=tx | ||||||
|  |             get_utxos_from_inputs(tx.inputs(),tx,utxos) | ||||||
|  | 
 | ||||||
|  |     for key,utxo in utxos.items(): | ||||||
|  |         txid=key.split(":")[0] | ||||||
|  |         if txid in dtxs: | ||||||
|  |             for tx in utxo['txs']: | ||||||
|  |                 txid =tx.txid() | ||||||
|  |                 del dtxs[txid] | ||||||
|  |              | ||||||
|  |     utxos = {} | ||||||
|  |     for txid,tx in dtxs.items(): | ||||||
|  |         get_utxos_from_inputs(tx.inputs(),tx,utxos) | ||||||
|  | 
 | ||||||
|  |     utxos = sorted(utxos.items(), key = lambda item: len(item[1])) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     remaining={} | ||||||
|  |     invalidated = [] | ||||||
|  |     for key,value in utxos: | ||||||
|  |         for tx in value['txs']: | ||||||
|  |             txid = tx.txid() | ||||||
|  |             if not txid in invalidated: | ||||||
|  |                 invalidated.append(tx.txid()) | ||||||
|  |                 remaining[key] = value | ||||||
|  | 
 | ||||||
|  | def print_transaction(heirs,tx,locktimes,tx_fees): | ||||||
|  |     jtx=tx.to_json() | ||||||
|  |     print(f"TX: {tx.txid()}\t-\tLocktime: {jtx['locktime']}") | ||||||
|  |     print(f"---") | ||||||
|  |     for inp in jtx["inputs"]: | ||||||
|  |         print(f"{inp['address']}: {inp['value_sats']}") | ||||||
|  |     print(f"---") | ||||||
|  |     for out in jtx["outputs"]: | ||||||
|  |         heirname="" | ||||||
|  |         for key in heirs.keys(): | ||||||
|  |             heir=heirs[key] | ||||||
|  |             if heir[HEIR_ADDRESS] == out['address'] and str(heir[HEIR_LOCKTIME]) == str(jtx['locktime']): | ||||||
|  |                 heirname=key | ||||||
|  |         print(f"{heirname}\t{out['address']}: {out['value_sats']}") | ||||||
|  | 
 | ||||||
|  |     print() | ||||||
|  |     size = tx.estimated_size() | ||||||
|  |     print("fee: {}\texpected: {}\tsize: {}".format(tx.input_value()-tx.output_value(), size*tx_fees, size)) | ||||||
|  | 
 | ||||||
|  |     print() | ||||||
|  |     try: | ||||||
|  |         print(tx.serialize_to_network()) | ||||||
|  |     except: | ||||||
|  |         print("impossible to serialize") | ||||||
|  |     print() | ||||||
|  | 
 | ||||||
|  | def get_change_output(wallet,in_amount,out_amount,fee): | ||||||
|  |     change_amount = int(in_amount - out_amount - fee) | ||||||
|  |     if change_amount > wallet.dust_threshold(): | ||||||
|  |         change_addresses = wallet.get_change_addresses_for_new_transaction() | ||||||
|  |         out = PartialTxOutput.from_address_and_value(change_addresses[0], change_amount) | ||||||
|  |         out.is_change = True | ||||||
|  |         return out | ||||||
|  |      | ||||||
|  | class Heirs(dict, Logger): | ||||||
|  | 
 | ||||||
|  |     def __init__(self, db: 'WalletDB'): | ||||||
|  |         Logger.__init__(self) | ||||||
|  |         self.db = db | ||||||
|  |         d = self.db.get('heirs', {}) | ||||||
|  |         try: | ||||||
|  |             self.update(d) | ||||||
|  |         except e as Exception: | ||||||
|  |             return | ||||||
|  | 
 | ||||||
|  |     def invalidate_transactions(self,wallet): | ||||||
|  |         invalidate_inheritance_transactions(wallet) | ||||||
|  | 
 | ||||||
|  |     def save(self): | ||||||
|  |         self.db.put('heirs', dict(self)) | ||||||
|  | 
 | ||||||
|  |     def import_file(self, path): | ||||||
|  |         data = read_json_file(path) | ||||||
|  |         data = Heirs._validate(data) | ||||||
|  |         self.update(data) | ||||||
|  |         self.save() | ||||||
|  | 
 | ||||||
|  |     def export_file(self, path): | ||||||
|  |         write_json_file(path, self) | ||||||
|  | 
 | ||||||
|  |     def __setitem__(self, key, value): | ||||||
|  |         dict.__setitem__(self, key, value) | ||||||
|  |         self.save() | ||||||
|  | 
 | ||||||
|  |     def pop(self, key): | ||||||
|  |         if key in self.keys(): | ||||||
|  |             res = dict.pop(self, key) | ||||||
|  |             self.save() | ||||||
|  |             return res | ||||||
|  | 
 | ||||||
|  |     def get_locktimes(self,from_locktime, a=False): | ||||||
|  |         locktimes = {} | ||||||
|  |         for key in self.keys(): | ||||||
|  |             locktime = Util.parse_locktime_string(self[key][HEIR_LOCKTIME]) | ||||||
|  |             if locktime > from_locktime and not a \ | ||||||
|  |             or locktime <=from_locktime and a: | ||||||
|  |                 locktimes[int(locktime)]=None | ||||||
|  |         return locktimes.keys() | ||||||
|  | 
 | ||||||
|  |     def check_locktime(self): | ||||||
|  |         return False | ||||||
|  | 
 | ||||||
|  |     def normalize_perc(self, heir_list, total_balance, relative_balance,wallet,real=False): | ||||||
|  |         amount = 0 | ||||||
|  |         for key,v in heir_list.items(): | ||||||
|  |             try: | ||||||
|  |                 column = HEIR_AMOUNT | ||||||
|  |                 if  real: column = HEIR_REAL_AMOUNT | ||||||
|  |                 value = int(math.floor(total_balance/relative_balance*self.amount_to_float(v[column]))) | ||||||
|  |                 if value > wallet.dust_threshold(): | ||||||
|  |                     heir_list[key].insert(HEIR_REAL_AMOUNT, value) | ||||||
|  |                     amount += value | ||||||
|  | 
 | ||||||
|  |             except Exception as e: | ||||||
|  |                 raise e | ||||||
|  |         return amount | ||||||
|  | 
 | ||||||
|  |     def amount_to_float(self,amount): | ||||||
|  |         try: | ||||||
|  |             return float(amount) | ||||||
|  |         except: | ||||||
|  |             try: | ||||||
|  |                 return float(amount[:-1]) | ||||||
|  |             except: | ||||||
|  |                 return 0.0 | ||||||
|  | 
 | ||||||
|  |     def fixed_percent_lists_amount(self,from_locktime,dust_threshold,reverse = False): | ||||||
|  |         fixed_heirs = {} | ||||||
|  |         fixed_amount = 0.0 | ||||||
|  |         percent_heirs= {} | ||||||
|  |         percent_amount = 0.0 | ||||||
|  |         for key in self.keys(): | ||||||
|  |             try: | ||||||
|  |                 cmp= Util.parse_locktime_string(self[key][HEIR_LOCKTIME]) - from_locktime | ||||||
|  |                 if cmp<=0: | ||||||
|  |                     continue | ||||||
|  |                 if Util.is_perc(self[key][HEIR_AMOUNT]): | ||||||
|  |                     percent_amount += float(self[key][HEIR_AMOUNT][:-1]) | ||||||
|  |                     percent_heirs[key] =list(self[key]) | ||||||
|  |                 else: | ||||||
|  |                     heir_amount = int(math.floor(float(self[key][HEIR_AMOUNT]))) | ||||||
|  |                     if heir_amount>dust_threshold: | ||||||
|  |                         fixed_amount += heir_amount | ||||||
|  |                         fixed_heirs[key] = list(self[key]) | ||||||
|  |                         fixed_heirs[key].insert(HEIR_REAL_AMOUNT,heir_amount) | ||||||
|  |                     else: | ||||||
|  |                         pass | ||||||
|  |             except Exception as e:  | ||||||
|  |                 _logger.error(e) | ||||||
|  |         return fixed_heirs,fixed_amount,percent_heirs,percent_amount | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     def prepare_lists(self, balance, total_fees, wallet, willexecutor = False, from_locktime = 0): | ||||||
|  |         willexecutors_amount = 0  | ||||||
|  |         willexecutors = {} | ||||||
|  |         heir_list = {} | ||||||
|  |         onlyfixed = False | ||||||
|  |         newbalance = balance - total_fees | ||||||
|  |         locktimes = self.get_locktimes(from_locktime); | ||||||
|  |         if willexecutor: | ||||||
|  |             for locktime in locktimes: | ||||||
|  |                 if int(Util.int_locktime(locktime)) > int(from_locktime): | ||||||
|  |                     try: | ||||||
|  |                         base_fee = int(willexecutor['base_fee']) | ||||||
|  |                         willexecutors_amount += base_fee | ||||||
|  |                         h = [None] * 4 | ||||||
|  |                         h[HEIR_AMOUNT] = base_fee | ||||||
|  |                         h[HEIR_REAL_AMOUNT] = base_fee | ||||||
|  |                         h[HEIR_LOCKTIME] = locktime | ||||||
|  |                         h[HEIR_ADDRESS] = willexecutor['address'] | ||||||
|  |                         willexecutors["w!ll3x3c\""+willexecutor['url']+"\""+str(locktime)] = h | ||||||
|  |                     except Exception as e: | ||||||
|  |                         return [],False | ||||||
|  |                 else: | ||||||
|  |                     _logger.error(f"heir excluded from will locktime({locktime}){Util.int_locktime(locktime)}<minimum{from_locktime}"),  | ||||||
|  |             heir_list.update(willexecutors) | ||||||
|  |             newbalance -= willexecutors_amount | ||||||
|  |         fixed_heirs,fixed_amount,percent_heirs,percent_amount = self.fixed_percent_lists_amount(from_locktime,wallet.dust_threshold())  | ||||||
|  |         if fixed_amount > newbalance: | ||||||
|  |             fixed_amount = self.normalize_perc(fixed_heirs,newbalance,fixed_amount,wallet) | ||||||
|  |             onlyfixed = True | ||||||
|  | 
 | ||||||
|  |         heir_list.update(fixed_heirs) | ||||||
|  |          | ||||||
|  |         newbalance -= fixed_amount | ||||||
|  | 
 | ||||||
|  |         if newbalance > 0: | ||||||
|  |             perc_amount = self.normalize_perc(percent_heirs,newbalance,percent_amount,wallet) | ||||||
|  |             newbalance -= perc_amount | ||||||
|  |             heir_list.update(percent_heirs) | ||||||
|  | 
 | ||||||
|  |         if newbalance > 0: | ||||||
|  |             newbalance += fixed_amount | ||||||
|  |             fixed_amount = self.normalize_perc(fixed_heirs,newbalance,fixed_amount,wallet,real=True) | ||||||
|  |             newbalance -= fixed_amount | ||||||
|  |             heir_list.update(fixed_heirs) | ||||||
|  |          | ||||||
|  |         heir_list = sorted(heir_list.items(), key = lambda item: Util.parse_locktime_string(item[1][HEIR_LOCKTIME],wallet)) | ||||||
|  |      | ||||||
|  | 
 | ||||||
|  |         locktimes = {} | ||||||
|  |         for key, value in heir_list: | ||||||
|  |             locktime=Util.parse_locktime_string(value[HEIR_LOCKTIME]) | ||||||
|  |             if not locktime in  locktimes: locktimes[locktime]={key:value} | ||||||
|  |             else: locktimes[locktime][key]=value | ||||||
|  |         return locktimes, onlyfixed | ||||||
|  |     def is_perc(self,key): | ||||||
|  |         return Util.is_perc(self[key][HEIR_AMOUNT]) | ||||||
|  | 
 | ||||||
|  |     def buildTransactions(self,bal_plugin,wallet,tx_fees = None, utxos=None,from_locktime=0): | ||||||
|  |         Heirs._validate(self) | ||||||
|  |         if len(self)<=0: | ||||||
|  |             return | ||||||
|  |         balance = 0.0 | ||||||
|  |         len_utxo_set = 0 | ||||||
|  |         available_utxos = [] | ||||||
|  |         if not utxos: | ||||||
|  |             utxos = wallet.get_utxos() | ||||||
|  |         willexecutors = Willexecutors.get_willexecutors(bal_plugin) or {} | ||||||
|  |         self.decimal_point=bal_plugin.config.get_decimal_point() | ||||||
|  |         no_willexecutors = bal_plugin.config_get(BalPlugin.NO_WILLEXECUTOR) | ||||||
|  |         for utxo in utxos: | ||||||
|  |             if utxo.value_sats()> 0*tx_fees: | ||||||
|  |                 balance += utxo.value_sats() | ||||||
|  |                 len_utxo_set += 1 | ||||||
|  |                 available_utxos.append(utxo) | ||||||
|  |         if len_utxo_set==0: return | ||||||
|  |         j=-2 | ||||||
|  |         willexecutorsitems = list(willexecutors.items()) | ||||||
|  |         willexecutorslen = len(willexecutorsitems) | ||||||
|  |         alltxs = {} | ||||||
|  |         while True: | ||||||
|  |             j+=1 | ||||||
|  |             if j >= willexecutorslen: | ||||||
|  |                 break | ||||||
|  |             elif 0 <= j: | ||||||
|  |                 url, willexecutor = willexecutorsitems[j] | ||||||
|  |                 if not Willexecutors.is_selected(willexecutor): | ||||||
|  |                     continue | ||||||
|  |                 else: | ||||||
|  |                     willexecutor['url']=url | ||||||
|  |             elif j == -1: | ||||||
|  |                 if not no_willexecutors: | ||||||
|  |                     continue | ||||||
|  |                 url = willexecutor = False | ||||||
|  |             else: | ||||||
|  |                 break | ||||||
|  |             fees = {} | ||||||
|  |             i=0 | ||||||
|  |             while True: | ||||||
|  |                 txs = {} | ||||||
|  |                 redo = False | ||||||
|  |                 i+=1 | ||||||
|  |                 total_fees=0 | ||||||
|  |                 for fee in fees: | ||||||
|  |                     total_fees += int(fees[fee]) | ||||||
|  |                 newbalance = balance  | ||||||
|  |                 locktimes, onlyfixed = self.prepare_lists(balance, total_fees, wallet, willexecutor, from_locktime) | ||||||
|  |                 try: | ||||||
|  |                     txs = prepare_transactions(locktimes, available_utxos[:], fees, wallet) | ||||||
|  |                     if not txs: | ||||||
|  |                         return {} | ||||||
|  |                 except Exception as e: | ||||||
|  |                     try: | ||||||
|  |                         if "w!ll3x3c" in e.heirname: | ||||||
|  |                             Willexecutors.is_selected(willexecutors[w],False) | ||||||
|  |                             break | ||||||
|  |                     except: | ||||||
|  |                         raise e | ||||||
|  |                 total_fees = 0 | ||||||
|  |                 total_fees_real = 0 | ||||||
|  |                 total_in = 0 | ||||||
|  |                 for txid,tx in txs.items(): | ||||||
|  |                     tx.willexecutor = willexecutor | ||||||
|  |                     fee = tx.estimated_size() * tx_fees     | ||||||
|  |                     txs[txid].tx_fees= tx_fees | ||||||
|  |                     total_fees += fee | ||||||
|  |                     total_fees_real += tx.get_fee() | ||||||
|  |                     total_in += tx.input_value() | ||||||
|  |                     rfee= tx.input_value()-tx.output_value() | ||||||
|  |                     if rfee < fee or rfee > fee + wallet.dust_threshold(): | ||||||
|  |                         redo = True | ||||||
|  |                     oldfees= fees.get(tx.my_locktime,0) | ||||||
|  |                     fees[tx.my_locktime]=fee | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |                 if  balance - total_in > wallet.dust_threshold(): | ||||||
|  |                     redo = True | ||||||
|  |                 if not redo: | ||||||
|  |                     break | ||||||
|  |                 if i>=10: | ||||||
|  |                     break | ||||||
|  |             alltxs.update(txs) | ||||||
|  |              | ||||||
|  |         return alltxs | ||||||
|  |     def get_transactions(self,bal_plugin,wallet,tx_fees,utxos=None,from_locktime=0): | ||||||
|  |         txs=self.buildTransactions(bal_plugin,wallet,tx_fees,utxos,from_locktime) | ||||||
|  |         if txs: | ||||||
|  |             temp_txs = {} | ||||||
|  |             for txid in txs: | ||||||
|  |                 if txs[txid].available_utxos: | ||||||
|  |                     temp_txs.update(self.get_transactions(bal_plugin,wallet,tx_fees,txs[txid].available_utxos,txs[txid].locktime)) | ||||||
|  |             txs.update(temp_txs) | ||||||
|  |         return txs | ||||||
|  | 
 | ||||||
|  |          | ||||||
|  | 
 | ||||||
|  |     def resolve(self, k): | ||||||
|  |         if bitcoin.is_address(k): | ||||||
|  |             return { | ||||||
|  |                 'address': k, | ||||||
|  |                 'type': 'address' | ||||||
|  |             } | ||||||
|  |         if k in self.keys(): | ||||||
|  |             _type, addr = self[k] | ||||||
|  |             if _type == 'address': | ||||||
|  |                 return { | ||||||
|  |                     'address': addr, | ||||||
|  |                     'type': 'heir' | ||||||
|  |                 } | ||||||
|  |         if openalias := self.resolve_openalias(k): | ||||||
|  |             return openalias | ||||||
|  |         raise AliasNotFoundException("Invalid Bitcoin address or alias", k) | ||||||
|  | 
 | ||||||
|  |     @classmethod | ||||||
|  |     def resolve_openalias(cls, url: str) -> Dict[str, Any]: | ||||||
|  |         out = cls._resolve_openalias(url) | ||||||
|  |         if out: | ||||||
|  |             address, name, validated = out | ||||||
|  |             return { | ||||||
|  |                 'address': address, | ||||||
|  |                 'name': name, | ||||||
|  |                 'type': 'openalias', | ||||||
|  |                 'validated': validated | ||||||
|  |             } | ||||||
|  |         return {} | ||||||
|  | 
 | ||||||
|  |     def by_name(self, name): | ||||||
|  |         for k in self.keys(): | ||||||
|  |             _type, addr = self[k] | ||||||
|  |             if addr.casefold() == name.casefold(): | ||||||
|  |                 return { | ||||||
|  |                     'name': addr, | ||||||
|  |                     'type': _type, | ||||||
|  |                     'address': k | ||||||
|  |                 } | ||||||
|  |         return None | ||||||
|  | 
 | ||||||
|  |     def fetch_openalias(self, config: 'SimpleConfig'): | ||||||
|  |         self.alias_info = None | ||||||
|  |         alias = config.OPENALIAS_ID | ||||||
|  |         if alias: | ||||||
|  |             alias = str(alias) | ||||||
|  |             def f(): | ||||||
|  |                 self.alias_info = self._resolve_openalias(alias) | ||||||
|  |                 trigger_callback('alias_received') | ||||||
|  |             t = threading.Thread(target=f) | ||||||
|  |             t.daemon = True | ||||||
|  |             t.start() | ||||||
|  | 
 | ||||||
|  |     @classmethod | ||||||
|  |     def _resolve_openalias(cls, url: str) -> Optional[Tuple[str, str, bool]]: | ||||||
|  |         # support email-style addresses, per the OA standard | ||||||
|  |         url = url.replace('@', '.') | ||||||
|  |         try: | ||||||
|  |             records, validated = dnssec.query(url, dns.rdatatype.TXT) | ||||||
|  |         except DNSException as e: | ||||||
|  |             _logger.info(f'Error resolving openalias: {repr(e)}') | ||||||
|  |             return None | ||||||
|  |         prefix = 'btc' | ||||||
|  |         for record in records: | ||||||
|  |             string = to_string(record.strings[0], 'utf8') | ||||||
|  |             if string.startswith('oa1:' + prefix): | ||||||
|  |                 address = cls.find_regex(string, r'recipient_address=([A-Za-z0-9]+)') | ||||||
|  |                 name = cls.find_regex(string, r'recipient_name=([^;]+)') | ||||||
|  |                 if not name: | ||||||
|  |                     name = address | ||||||
|  |                 if not address: | ||||||
|  |                     continue | ||||||
|  |                 return address, name, validated | ||||||
|  | 
 | ||||||
|  |     @staticmethod | ||||||
|  |     def find_regex(haystack, needle): | ||||||
|  |         regex = re.compile(needle) | ||||||
|  |         try: | ||||||
|  |             return regex.search(haystack).groups()[0] | ||||||
|  |         except AttributeError: | ||||||
|  |             return None | ||||||
|  | 
 | ||||||
|  |                  | ||||||
|  |     def validate_address(address): | ||||||
|  |         if not bitcoin.is_address(address): | ||||||
|  |             raise NotAnAddress(f"not an address,{address}") | ||||||
|  |         return address | ||||||
|  | 
 | ||||||
|  |     def validate_amount(amount): | ||||||
|  |         try: | ||||||
|  |             if Util.is_perc(amount): | ||||||
|  |                 famount = float(amount[:-1]) | ||||||
|  |             else: | ||||||
|  |                 famount = float(amount) | ||||||
|  |             if famount <= 0.00000001: | ||||||
|  |                 raise AmountNotValid(f"amount have to be positive {famount} < 0") | ||||||
|  |         except Exception as e: | ||||||
|  |             raise AmountNotValid(f"amount not properly formatted, {e}") | ||||||
|  |         return amount | ||||||
|  | 
 | ||||||
|  |     def validate_locktime(locktime,timestamp_to_check=False): | ||||||
|  |         try: | ||||||
|  |             locktime = Util.parse_locktime_string(locktime,None) | ||||||
|  |             if timestamp_to_check: | ||||||
|  |                 if locktime < timestamp_to_check: | ||||||
|  |                     raise HeirExpiredException() | ||||||
|  |         except Exception as e: | ||||||
|  |             raise LocktimeNotValid(f"locktime string not properly formatted, {e}") | ||||||
|  |         return locktime | ||||||
|  | 
 | ||||||
|  |     def validate_heir(k,v,timestamp_to_check=False):      | ||||||
|  |         address = Heirs.validate_address(v[HEIR_ADDRESS]) | ||||||
|  |         amount = Heirs.validate_amount(v[HEIR_AMOUNT]) | ||||||
|  |         locktime = Heirs.validate_locktime(v[HEIR_LOCKTIME],timestamp_to_check) | ||||||
|  |         return (address,amount,locktime) | ||||||
|  | 
 | ||||||
|  |     def _validate(data,timestamp_to_check=False): | ||||||
|  |         for k, v in list(data.items()): | ||||||
|  |             if k == 'heirs': | ||||||
|  |                 return Heirs._validate(v) | ||||||
|  |             try: | ||||||
|  |                 Heirs.validate_heir(k,v) | ||||||
|  |             except Exception as e: | ||||||
|  |                 data.pop(k) | ||||||
|  |         return data | ||||||
|  | 
 | ||||||
|  | class NotAnAddress(ValueError): | ||||||
|  |     pass | ||||||
|  | class AmountNotValid(ValueError): | ||||||
|  |     pass | ||||||
|  | class LocktimeNotValid(ValueError): | ||||||
|  |     pass | ||||||
|  | class HeirExpiredException(LocktimeNotValid): | ||||||
|  |     pass | ||||||
							
								
								
									
										
											BIN
										
									
								
								icons/bal16x16.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								icons/bal16x16.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 538 B | 
							
								
								
									
										
											BIN
										
									
								
								icons/bal32x32.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								icons/bal32x32.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 1.1 KiB | 
							
								
								
									
										
											BIN
										
									
								
								icons/heir.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								icons/heir.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 871 B | 
							
								
								
									
										
											BIN
										
									
								
								icons/will.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								icons/will.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 831 B | 
							
								
								
									
										937
									
								
								qt.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										937
									
								
								qt.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,937 @@ | |||||||
|  | ''' | ||||||
|  | 
 | ||||||
|  | Bal | ||||||
|  | 
 | ||||||
|  | Bitcoin after life | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ''' | ||||||
|  | 
 | ||||||
|  | import os | ||||||
|  | import random | ||||||
|  | import traceback | ||||||
|  | from functools import partial | ||||||
|  | import sys | ||||||
|  | import copy | ||||||
|  | 
 | ||||||
|  | import sys | ||||||
|  | 
 | ||||||
|  | from electrum.plugin import hook | ||||||
|  | from electrum.i18n import _ | ||||||
|  | from electrum.util import make_dir, InvalidPassword, UserCancelled,resource_path | ||||||
|  | from electrum.util import bfh, read_json_file,write_json_file,decimal_point_to_base_unit_name,FileImportFailed,FileExportFailed | ||||||
|  | 
 | ||||||
|  | from electrum.gui.qt.util import (EnterButton, WWLabel,  | ||||||
|  |                                     Buttons, CloseButton, OkButton,import_meta_gui,export_meta_gui,char_width_in_lineedit,CancelButton,HelpButton) | ||||||
|  | 
 | ||||||
|  | from electrum.gui.qt.qrtextedit import ScanQRTextEdit | ||||||
|  | from electrum.gui.qt.main_window import StatusBarButton | ||||||
|  | from electrum.gui.qt.password_dialog import PasswordDialog | ||||||
|  | from electrum.gui.qt.transaction_dialog import TxDialog | ||||||
|  | from electrum import constants | ||||||
|  | from electrum.transaction import Transaction | ||||||
|  | from .bal import BalPlugin | ||||||
|  | from .heirs import Heirs | ||||||
|  | from . import util as Util | ||||||
|  | from . import will as Will | ||||||
|  | 
 | ||||||
|  | from .balqt.locktimeedit import HeirsLockTimeEdit | ||||||
|  | from .balqt.willexecutor_dialog import WillExecutorDialog | ||||||
|  | from .balqt.preview_dialog import PreviewDialog,PreviewList | ||||||
|  | from .balqt.heir_list import HeirList | ||||||
|  | from .balqt.amountedit import PercAmountEdit | ||||||
|  | from .balqt.willdetail import WillDetailDialog | ||||||
|  | from .balqt.closedialog import BalCloseDialog | ||||||
|  | from .balqt import qt_resources | ||||||
|  | from . import willexecutors as Willexecutors | ||||||
|  | from electrum.transaction import tx_from_any | ||||||
|  | import time | ||||||
|  | from electrum import json_db | ||||||
|  | from electrum.json_db import StoredDict | ||||||
|  | import datetime | ||||||
|  | import urllib.parse | ||||||
|  | import urllib.request | ||||||
|  | from typing import TYPE_CHECKING, Callable, Optional, List, Union, Tuple, Mapping | ||||||
|  | from .balqt.baldialog import BalDialog,BalWaitingDialog,BalBlockingWaitingDialog,bal_checkbox | ||||||
|  | 
 | ||||||
|  | from electrum.logging import Logger | ||||||
|  | if qt_resources.QT_VERSION == 5: | ||||||
|  |     from PyQt5.QtCore import Qt, QRectF, QRect, QSizeF, QUrl, QPoint, QSize | ||||||
|  |     from PyQt5.QtGui import (QPixmap, QImage, QBitmap, QPainter, QFontDatabase, QPen, QFont,QIcon, | ||||||
|  |                      QColor, QDesktopServices, qRgba, QPainterPath,QPalette) | ||||||
|  | 
 | ||||||
|  |     from PyQt5.QtWidgets import (QGridLayout, QVBoxLayout, QHBoxLayout, QLabel, | ||||||
|  |                      QPushButton, QLineEdit,QCheckBox,QSpinBox,QMenuBar,QMenu,QLineEdit,QScrollArea,QWidget,QSpacerItem,QSizePolicy) | ||||||
|  | else: | ||||||
|  |     from PyQt6.QtCore import Qt, QRectF, QRect, QSizeF, QUrl, QPoint, QSize | ||||||
|  |     from PyQt6.QtGui import (QPixmap, QImage, QBitmap, QPainter, QFontDatabase, QPen, QFont,QIcon, | ||||||
|  |                      QColor, QDesktopServices, qRgba, QPainterPath,QPalette) | ||||||
|  | 
 | ||||||
|  |     from PyQt6.QtWidgets import (QGridLayout, QVBoxLayout, QHBoxLayout, QLabel, | ||||||
|  |                      QPushButton, QLineEdit,QCheckBox,QSpinBox,QMenuBar,QMenu,QLineEdit,QScrollArea,QWidget,QSpacerItem,QSizePolicy) | ||||||
|  | 
 | ||||||
|  | class Plugin(BalPlugin,Logger): | ||||||
|  | 
 | ||||||
|  |     def __init__(self, parent, config, name): | ||||||
|  |         Logger.__init__(self) | ||||||
|  |         self.logger.info("INIT BALPLUGIN") | ||||||
|  |         BalPlugin.__init__(self, parent, config, name) | ||||||
|  |         self.bal_windows={} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     @hook | ||||||
|  |     def init_qt(self,gui_object): | ||||||
|  |         self.logger.info("HOOK init qt")    | ||||||
|  |         try: | ||||||
|  |             self.gui_object=gui_object | ||||||
|  |             for window in gui_object.windows: | ||||||
|  |                 wallet = window.wallet | ||||||
|  |                 if wallet: | ||||||
|  |                     window.show_warning(_('Please restart Electrum to activate the BAL plugin'), title=_('Success')) | ||||||
|  |                     return | ||||||
|  |                 w = BalWindow(self,window) | ||||||
|  |                 self.bal_windows[window.winId]= w | ||||||
|  |                 for child in window.children(): | ||||||
|  |                     if isinstance(child,QMenuBar): | ||||||
|  |                         for menu_child in child.children(): | ||||||
|  |                             if isinstance(menu_child,QMenu): | ||||||
|  |                                 try: | ||||||
|  |                                     if menu_child.title()==_("&Tools"): | ||||||
|  |                                         w.init_menubar_tools(menu_child) | ||||||
|  |                                              | ||||||
|  |                                 except Exception as e: | ||||||
|  |                                     raise e | ||||||
|  |                                     self.logger.error(("except:",menu_child.text())) | ||||||
|  |                                      | ||||||
|  |         except Exception as e: | ||||||
|  |             raise e | ||||||
|  |             self.logger.error("Error loading plugini {}".format(e)) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     @hook | ||||||
|  |     def create_status_bar(self, sb): | ||||||
|  |         self.logger.info("HOOK create status bar") | ||||||
|  |         return | ||||||
|  |         b = StatusBarButton(qt_resources.read_QIcon('bal32x32.png'), "Bal "+_("Bitcoin After Life"), | ||||||
|  |                             partial(self.setup_dialog, sb), sb.height()) | ||||||
|  |         sb.addPermanentWidget(b) | ||||||
|  | 
 | ||||||
|  |     @hook | ||||||
|  |     def init_menubar_tools(self,window,tools_menu): | ||||||
|  |         self.logger.info("HOOK init_menubar") | ||||||
|  |         w = self.get_window(window) | ||||||
|  |         w.init_menubar_tools(tools_menu) | ||||||
|  | 
 | ||||||
|  |     @hook | ||||||
|  |     def load_wallet(self,wallet, main_window): | ||||||
|  |         self.logger.info("HOOK load wallet") | ||||||
|  |         w = self.get_window(main_window) | ||||||
|  |         w.wallet = wallet | ||||||
|  |         w.init_will() | ||||||
|  |         w.willexecutors = Willexecutors.get_willexecutors(self, update=False, bal_window=w) | ||||||
|  |         w.disable_plugin = False | ||||||
|  |         w.ok=True | ||||||
|  |     @hook | ||||||
|  |     def close_wallet(self,wallet): | ||||||
|  |         for winid,win in self.bal_windows.items(): | ||||||
|  |             if win.wallet == wallet: | ||||||
|  |                 win.on_close() | ||||||
|  | 
 | ||||||
|  |     def get_window(self,window): | ||||||
|  |         w = self.bal_windows.get(window.winId,None) | ||||||
|  |         if w is None: | ||||||
|  |             w=BalWindow(self,window) | ||||||
|  |             self.bal_windows[window.winId]=w | ||||||
|  |         return w | ||||||
|  |    | ||||||
|  |     def requires_settings(self): | ||||||
|  |         return True | ||||||
|  |      | ||||||
|  |     def settings_widget(self, window): | ||||||
|  | 
 | ||||||
|  |         w=self.get_window(window.window) | ||||||
|  |         return EnterButton(_('Settings'), partial(w.settings_dialog,window)) | ||||||
|  | 
 | ||||||
|  |     def password_dialog(self, msg=None, parent=None): | ||||||
|  |         parent = parent or self | ||||||
|  |         d = PasswordDialog(parent, msg) | ||||||
|  |         return d.run() | ||||||
|  | 
 | ||||||
|  |     def get_seed(self): | ||||||
|  |         password = None | ||||||
|  |         if self.wallet.has_keystore_encryption(): | ||||||
|  |             password = self.password_dialog(parent=self.d.parent()) | ||||||
|  |             if not password: | ||||||
|  |                 raise UserCancelled() | ||||||
|  | 
 | ||||||
|  |         keystore = self.wallet.get_keystore() | ||||||
|  |         if not keystore or not keystore.has_seed(): | ||||||
|  |             return | ||||||
|  |         self.extension = bool(keystore.get_passphrase(password)) | ||||||
|  |         return keystore.get_seed(password) | ||||||
|  | 
 | ||||||
|  |     | ||||||
|  | class shown_cv(): | ||||||
|  |     _type= bool | ||||||
|  |     def __init__(self,value): | ||||||
|  |         self.value=value | ||||||
|  |     def get(self): | ||||||
|  |         return self.value | ||||||
|  |     def set(self,value): | ||||||
|  |         self.value=value | ||||||
|  | 
 | ||||||
|  | class BalWindow(Logger): | ||||||
|  |     def __init__(self,bal_plugin: 'BalPlugin',window: 'ElectrumWindow'): | ||||||
|  |         Logger.__init__(self) | ||||||
|  |         self.logger.info("loggo tutto") | ||||||
|  |         self.bal_plugin = bal_plugin | ||||||
|  |         self.window = window | ||||||
|  |         self.heirs = {} | ||||||
|  |         self.will = {} | ||||||
|  |         self.willitems = {} | ||||||
|  |         self.willexecutors = {} | ||||||
|  |         self.will_settings = None | ||||||
|  |         self.heirs_tab = self.create_heirs_tab() | ||||||
|  |         self.will_tab = self.create_will_tab() | ||||||
|  |         self.ok= False | ||||||
|  |         self.disable_plugin = True | ||||||
|  |          | ||||||
|  |         if self.window.wallet: | ||||||
|  |             self.wallet = self.window.wallet | ||||||
|  |             self.heirs_tab.wallet = self.wallet | ||||||
|  |             self.will_tab.wallet = self.wallet | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     def init_menubar_tools(self,tools_menu): | ||||||
|  |         self.tools_menu=tools_menu | ||||||
|  | 
 | ||||||
|  |         def add_optional_tab(tabs, tab, icon, description): | ||||||
|  |             tab.tab_icon = icon | ||||||
|  |             tab.tab_description = description | ||||||
|  |             tab.tab_pos = len(tabs) | ||||||
|  |             if tab.is_shown_cv: | ||||||
|  |                 tabs.addTab(tab, icon, description.replace("&", "")) | ||||||
|  |          | ||||||
|  |         add_optional_tab(self.window.tabs, self.heirs_tab, qt_resources.read_QIcon("heir.png"), _("&Heirs")) | ||||||
|  |         add_optional_tab(self.window.tabs, self.will_tab, qt_resources.read_QIcon("will.png"), _("&Will")) | ||||||
|  |         tools_menu.addSeparator() | ||||||
|  |         self.tools_menu.willexecutors_action = tools_menu.addAction(_("&Will-Executors"), self.show_willexecutor_dialog) | ||||||
|  | 
 | ||||||
|  |     def load_willitems(self): | ||||||
|  |         self.willitems={} | ||||||
|  |         for wid,w in self.will.items(): | ||||||
|  |             self.willitems[wid]=Will.WillItem(w,wallet=self.wallet) | ||||||
|  |         if self.willitems: | ||||||
|  |             self.will_list.will=self.willitems | ||||||
|  |             self.will_list.update_will(self.willitems) | ||||||
|  |             self.will_tab.update() | ||||||
|  | 
 | ||||||
|  |     def save_willitems(self): | ||||||
|  |         keys = list(self.will.keys()) | ||||||
|  |         for k in keys: | ||||||
|  |             del self.will[k] | ||||||
|  |         for wid,w in self.willitems.items(): | ||||||
|  |             self.will[wid]=w.to_dict() | ||||||
|  | 
 | ||||||
|  |     def init_will(self): | ||||||
|  |         self.logger.info("********************init_____will____________**********") | ||||||
|  |         if not self.willexecutors: | ||||||
|  |             self.willexecutors = Willexecutors.get_willexecutors(self.bal_plugin, update=False, bal_window=self)  | ||||||
|  |         if not self.heirs: | ||||||
|  |             self.heirs = Heirs._validate(Heirs(self.wallet.db)) | ||||||
|  |         if not self.will: | ||||||
|  |             self.will=self.wallet.db.get_dict("will") | ||||||
|  |             if self.will: | ||||||
|  |                 self.willitems = {} | ||||||
|  |                 try: | ||||||
|  |                     self.load_willitems() | ||||||
|  |                 except: | ||||||
|  |                     self.disable_plugin=True | ||||||
|  |                     self.show_warning(_('Please restart Electrum to activate the BAL plugin'), title=_('Success')) | ||||||
|  |                     self.close_wallet() | ||||||
|  |                     return | ||||||
|  | 
 | ||||||
|  |         if not self.will_settings: | ||||||
|  |             self.will_settings=self.wallet.db.get_dict("will_settings") | ||||||
|  |             self.logger.info("will_settings: {}".format(self.will_settings)) | ||||||
|  |             if not self.will_settings: | ||||||
|  |                 Util.copy(self.will_settings,self.bal_plugin.default_will_settings()) | ||||||
|  |                 self.logger.debug("not_will_settings {}".format(self.will_settings)) | ||||||
|  | 
 | ||||||
|  |             self.bal_plugin.validate_will_settings(self.will_settings) | ||||||
|  |             self.heir_list.update_will_settings() | ||||||
|  |                  | ||||||
|  |     def get_window_title(self,title): | ||||||
|  |         return _('BAL - ') + _(title)  | ||||||
|  | 
 | ||||||
|  |     def show_willexecutor_dialog(self): | ||||||
|  |         self.willexecutor_dialog = WillExecutorDialog(self) | ||||||
|  |         self.willexecutor_dialog.show() | ||||||
|  | 
 | ||||||
|  |     def create_heirs_tab(self): | ||||||
|  |         self.heir_list = l = HeirList(self) | ||||||
|  |         tab = self.window.create_list_tab(l) | ||||||
|  |         tab.is_shown_cv = shown_cv(True) | ||||||
|  |         return tab | ||||||
|  | 
 | ||||||
|  |     def create_will_tab(self): | ||||||
|  |         self.will_list = l = PreviewList(self,None) | ||||||
|  |         tab = self.window.create_list_tab(l) | ||||||
|  |         tab.is_shown_cv = shown_cv(True) | ||||||
|  |         return tab | ||||||
|  | 
 | ||||||
|  |     def new_heir_dialog(self): | ||||||
|  |         d = BalDialog(self.window, self.get_window_title("New heir")) | ||||||
|  |         vbox = QVBoxLayout(d) | ||||||
|  |         grid = QGridLayout() | ||||||
|  | 
 | ||||||
|  |         heir_name = QLineEdit() | ||||||
|  |         heir_name.setFixedWidth(32 * char_width_in_lineedit()) | ||||||
|  |         heir_address = QLineEdit() | ||||||
|  |         heir_address.setFixedWidth(32 * char_width_in_lineedit()) | ||||||
|  |         heir_amount = PercAmountEdit(self.window.get_decimal_point) | ||||||
|  |         heir_locktime = HeirsLockTimeEdit(self.window,0) | ||||||
|  |         heir_is_xpub = QCheckBox() | ||||||
|  | 
 | ||||||
|  |         grid.addWidget(QLabel(_("Name")), 1, 0) | ||||||
|  |         grid.addWidget(heir_name, 1, 1) | ||||||
|  |         grid.addWidget(HelpButton("Unique name or description about heir"),1,2) | ||||||
|  | 
 | ||||||
|  |         grid.addWidget(QLabel(_("Address")), 2, 0) | ||||||
|  |         grid.addWidget(heir_address, 2, 1) | ||||||
|  |         grid.addWidget(HelpButton("heir bitcoin address"),2,2) | ||||||
|  | 
 | ||||||
|  |         #grid.addWidget(QLabel(_("xPub")), 2, 2) | ||||||
|  |         grid.addWidget(QLabel(_("Amount")),3,0) | ||||||
|  |         grid.addWidget(heir_amount,3,1) | ||||||
|  |         grid.addWidget(HelpButton("Fixed or Percentage amount if end with %"),3,2) | ||||||
|  | 
 | ||||||
|  |         #grid.addWidget(QLabel(_("LockTime")), 4, 0) | ||||||
|  |         #grid.addWidget(heir_locktime, 4, 1) | ||||||
|  |         #grid.addWidget(HelpButton("if you choose Raw, you can insert various options based on suffix:\n "  | ||||||
|  |         #                          +" - b: number of blocks after current block(ex: 144b means tomorrow)\n"  | ||||||
|  |         #                          +" - d: number of days after current day(ex: 1d means tomorrow)\n"   | ||||||
|  |         #                           +" - y: number of years after currrent day(ex: 1y means one year from today)\n\n"  | ||||||
|  |         #                          +"when using d or y time will be set to 00:00 for privacy reasons\n"  | ||||||
|  |         #                          +"when used without suffix it can be used to indicate:\n"  | ||||||
|  |         #                          +" - exact block(if value is less than 500,000,000)\n" | ||||||
|  |         #                          +" - exact block timestamp(if value greater than 500,000,000"),4,2) | ||||||
|  | 
 | ||||||
|  |         vbox.addLayout(grid) | ||||||
|  |         vbox.addLayout(Buttons(CancelButton(d), OkButton(d))) | ||||||
|  |         while d.exec(): | ||||||
|  |             #TODO SAVE HEIR | ||||||
|  |             heir = [ | ||||||
|  |                     heir_name.text(), | ||||||
|  |                     heir_address.text(), | ||||||
|  |                     Util.encode_amount(heir_amount.text(),self.bal_plugin.config.get_decimal_point()), | ||||||
|  |                     str(heir_locktime.get_locktime()), | ||||||
|  |                     ] | ||||||
|  |             try: | ||||||
|  |                 self.set_heir(heir) | ||||||
|  |                 break | ||||||
|  |             except Exception as e: | ||||||
|  |                 self.show_error(str(e)) | ||||||
|  | 
 | ||||||
|  |     def export_inheritance_handler(self,path): | ||||||
|  |         txs = self.build_inheritance_transaction(ignore_duplicate=True, keep_original=False) | ||||||
|  |         with open(path,"w") as f: | ||||||
|  |             for tx in txs: | ||||||
|  |                 tx['status']+="."+BalPlugin.STATUS_EXPORTED | ||||||
|  |                 f.write(str(tx['tx'])) | ||||||
|  |                 f.write('\n') | ||||||
|  |   | ||||||
|  |     def set_heir(self,heir): | ||||||
|  |         heir=list(heir) | ||||||
|  |         heir[3]=self.will_settings['locktime'] | ||||||
|  | 
 | ||||||
|  |         h=Heirs.validate_heir(heir[0],heir[1:]) | ||||||
|  |         self.heirs[heir[0]]=h | ||||||
|  |         self.heir_list.update() | ||||||
|  |         return True | ||||||
|  | 
 | ||||||
|  |     def delete_heirs(self,heirs): | ||||||
|  |         for heir in heirs: | ||||||
|  |             del self.heirs[heir] | ||||||
|  |         self.heirs.save() | ||||||
|  |         self.heir_list.update() | ||||||
|  |         return True | ||||||
|  |      | ||||||
|  |     def import_heirs(self,): | ||||||
|  |         import_meta_gui(self.window, _('heirs'), self.heirs.import_file, self.heir_list.update) | ||||||
|  | 
 | ||||||
|  |     def export_heirs(self): | ||||||
|  |         Util.export_meta_gui(self.window, _('heirs'), self.heirs.export_file) | ||||||
|  | 
 | ||||||
|  |     def prepare_will(self, ignore_duplicate = False, keep_original = False): | ||||||
|  |         will = self.build_inheritance_transaction(ignore_duplicate = ignore_duplicate, keep_original=keep_original) | ||||||
|  |         return will | ||||||
|  |      | ||||||
|  |     def delete_not_valid(self,txid,s_utxo): | ||||||
|  |         raise NotImplementedError() | ||||||
|  | 
 | ||||||
|  |     def update_will(self,will): | ||||||
|  |         Will.update_will(self.willitems,will) | ||||||
|  |         self.willitems.update(will) | ||||||
|  |         Will.normalize_will(self.willitems,self.wallet) | ||||||
|  | 
 | ||||||
|  |     def build_will(self, ignore_duplicate = True, keep_original = True ): | ||||||
|  | 
 | ||||||
|  |         will = {} | ||||||
|  |         willtodelete=[] | ||||||
|  |         willtoappend={} | ||||||
|  |         try: | ||||||
|  |             self.init_class_variables() | ||||||
|  |             self.willexecutors = Willexecutors.get_willexecutors(self.bal_plugin, update=False, bal_window=self)  | ||||||
|  |              | ||||||
|  |             if not self.no_willexecutor: | ||||||
|  |                 f=False | ||||||
|  |                 for u,w in self.willexecutors.items(): | ||||||
|  |                     if Willexecutors.is_selected(w): | ||||||
|  |                         f=True | ||||||
|  |                 if not f: | ||||||
|  |                     raise Will.NoWillExecutorNotPresent("No Will-Executor or backup transaction selected") | ||||||
|  |             txs = self.heirs.get_transactions(self.bal_plugin,self.window.wallet,self.will_settings['tx_fees'],None,self.date_to_check) | ||||||
|  |             self.logger.info(txs) | ||||||
|  |             creation_time = time.time() | ||||||
|  |             if txs: | ||||||
|  |                 for txid in txs: | ||||||
|  |                     txtodelete=[] | ||||||
|  |                     _break = False | ||||||
|  |                     tx = {} | ||||||
|  |                     tx['tx'] = txs[txid] | ||||||
|  |                     tx['my_locktime'] = txs[txid].my_locktime | ||||||
|  |                     tx['heirsvalue'] = txs[txid].heirsvalue | ||||||
|  |                     tx['description'] = txs[txid].description | ||||||
|  |                     tx['willexecutor'] = copy.deepcopy(txs[txid].willexecutor) | ||||||
|  |                     tx['status'] = _("New") | ||||||
|  |                     tx['tx_fees'] = txs[txid].tx_fees | ||||||
|  |                     tx['time'] = creation_time | ||||||
|  |                     tx['heirs'] = copy.deepcopy(txs[txid].heirs) | ||||||
|  |                     tx['txchildren'] = [] | ||||||
|  |                     will[txid]=Will.WillItem(tx,_id=txid,wallet=self.wallet) | ||||||
|  |                 self.update_will(will) | ||||||
|  |         except Exception as e: | ||||||
|  |             raise e | ||||||
|  |             pass | ||||||
|  |         return self.willitems  | ||||||
|  | 
 | ||||||
|  |     def check_will(self): | ||||||
|  |         return Will.is_will_valid(self.willitems, self.block_to_check, self.date_to_check, self.will_settings['tx_fees'],self.window.wallet.get_utxos(),heirs=self.heirs,willexecutors=self.willexecutors ,self_willexecutor=self.no_willexecutor, wallet = self.wallet, callback_not_valid_tx=self.delete_not_valid) | ||||||
|  |     def show_message(self,text): | ||||||
|  |         self.window.show_message(text) | ||||||
|  |     def show_warning(self,text,parent =None): | ||||||
|  |         self.window.show_warning(text, parent= None) | ||||||
|  |     def show_error(self,text): | ||||||
|  |         self.window.show_error(text) | ||||||
|  |     def show_critical(self,text): | ||||||
|  |         self.window.show_critical(text) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     def init_heirs_to_locktime(self): | ||||||
|  |         for heir in self.heirs: | ||||||
|  |             h=self.heirs[heir] | ||||||
|  |             self.heirs[heir]=[h[0],h[1],self.will_settings['locktime']] | ||||||
|  | 
 | ||||||
|  |      | ||||||
|  |     def init_class_variables(self): | ||||||
|  |             if not self.heirs: | ||||||
|  |                 raise Will.NoHeirsException() | ||||||
|  |                 return | ||||||
|  |             try:  | ||||||
|  |                 self.date_to_check = Util.parse_locktime_string(self.will_settings['threshold']) | ||||||
|  |                 found = False | ||||||
|  |                 self.locktime_blocks=self.bal_plugin.config_get(BalPlugin.LOCKTIME_BLOCKS) | ||||||
|  |                 self.current_block = Util.get_current_height(self.wallet.network) | ||||||
|  |                 self.block_to_check = self.current_block + self.locktime_blocks | ||||||
|  |                 self.no_willexecutor = self.bal_plugin.config_get(BalPlugin.NO_WILLEXECUTOR) | ||||||
|  |                 self.willexecutors = Willexecutors.get_willexecutors(self.bal_plugin,update=True,bal_window=self,task=False)  | ||||||
|  |                 self.init_heirs_to_locktime() | ||||||
|  | 
 | ||||||
|  |             except Exception as e: | ||||||
|  |                 self.logger.error(e) | ||||||
|  |                 raise e | ||||||
|  | 
 | ||||||
|  |     def build_inheritance_transaction(self,ignore_duplicate = True, keep_original = True): | ||||||
|  |         try: | ||||||
|  |             if self.disable_plugin: | ||||||
|  |                 self.logger.info("plugin is disabled") | ||||||
|  |                 return | ||||||
|  |             if not self.heirs: | ||||||
|  |                 self.logger.warning("not heirs {}".format(self.heirs)) | ||||||
|  |                 return | ||||||
|  |             self.init_class_variables() | ||||||
|  |             try: | ||||||
|  |                 Will.check_amounts(self.heirs,self.willexecutors,self.window.wallet.get_utxos(),self.date_to_check,self.window.wallet.dust_threshold()) | ||||||
|  |             except Will.AmountException as e: | ||||||
|  |                 self.show_warning(_(f"In the inheritance process, the entire wallet will always be fully emptied. Your settings require an adjustment of the amounts.\n{e}")) | ||||||
|  |             locktime = Util.parse_locktime_string(self.will_settings['locktime']) | ||||||
|  |             if locktime < self.date_to_check: | ||||||
|  |                 self.show_error(_("locktime is lower than threshold")) | ||||||
|  |                 return | ||||||
|  |             if not self.no_willexecutor: | ||||||
|  |                 f=False | ||||||
|  |                 for k,we in self.willexecutors.items(): | ||||||
|  |                     if Willexecutors.is_selected(we): | ||||||
|  |                         f=True | ||||||
|  |                 if not f: | ||||||
|  |                     self.show_error(_(" no backup transaction or willexecutor selected")) | ||||||
|  |                     return | ||||||
|  | 
 | ||||||
|  |             try: | ||||||
|  |                 self.check_will() | ||||||
|  |             except Will.WillExpiredException as e: | ||||||
|  |                 self.invalidate_will() | ||||||
|  |                 return | ||||||
|  |             except Will.NoHeirsException: | ||||||
|  |                 return | ||||||
|  |             except Will.NotCompleteWillException as e: | ||||||
|  |                 self.logger.info("{}:{}".format(type(e),e)) | ||||||
|  |                 message = False  | ||||||
|  |                 if isinstance(e,Will.HeirChangeException): | ||||||
|  |                     message ="Heirs changed:" | ||||||
|  |                 elif isinstance(e,Will.WillExecutorNotPresent): | ||||||
|  |                     message = "Will-Executor not present:" | ||||||
|  |                 elif isinstance(e,Will.WillexecutorChangeException): | ||||||
|  |                     message = "Will-Executor changed" | ||||||
|  |                 elif isinstance(e,Will.TxFeesChangedException): | ||||||
|  |                     message = "Txfees are changed" | ||||||
|  |                 elif isinstance(e,Will.HeirNotFoundException): | ||||||
|  |                     message = "Heir not found" | ||||||
|  | 
 | ||||||
|  |                 if message: | ||||||
|  |                     self.show_message(f"{_(message)}:\n {e}\n{_('will have to be built')}") | ||||||
|  |                  | ||||||
|  |                 self.logger.info("build will") | ||||||
|  |                 self.build_will(ignore_duplicate,keep_original) | ||||||
|  | 
 | ||||||
|  |                 try: | ||||||
|  |                     self.check_will() | ||||||
|  |                     for wid,w in self.willitems.items(): | ||||||
|  |                         self.wallet.set_label(wid,"BAL Transaction") | ||||||
|  |                 except Will.WillExpiredException as e: | ||||||
|  |                     self.invalidate_will() | ||||||
|  |                 except Will.NotCompleteWillException as e: | ||||||
|  |                     self.show_error("Error:{}\n {}".format(str(e),_("Please, check your heirs, locktime and threshold!"))) | ||||||
|  | 
 | ||||||
|  |                 self.window.history_list.update() | ||||||
|  |                 self.window.utxo_list.update() | ||||||
|  |             self.update_all() | ||||||
|  |             return self.willitems | ||||||
|  |         except Exception as e: | ||||||
|  |             raise e | ||||||
|  |     def show_transaction_real( | ||||||
|  |         self, | ||||||
|  |         tx: Transaction, | ||||||
|  |         *, | ||||||
|  |         parent: 'ElectrumWindow', | ||||||
|  |         prompt_if_unsaved: bool = False, | ||||||
|  |         external_keypairs: Mapping[bytes, bytes] = None, | ||||||
|  |         payment_identifier: 'PaymentIdentifier' = None, | ||||||
|  |     ): | ||||||
|  |         try: | ||||||
|  |             d = TxDialog( | ||||||
|  |                 tx, | ||||||
|  |                 parent=parent, | ||||||
|  |                 prompt_if_unsaved=prompt_if_unsaved, | ||||||
|  |                 external_keypairs=external_keypairs, | ||||||
|  |                 payment_identifier=payment_identifier, | ||||||
|  |             ) | ||||||
|  |             d.setWindowIcon(qt_resources.read_QIcon("bal32x32.png")) | ||||||
|  |         except SerializationError as e: | ||||||
|  |             self.logger.error('unable to deserialize the transaction') | ||||||
|  |             parent.show_critical(_("Electrum was unable to deserialize the transaction:") + "\n" + str(e)) | ||||||
|  |         else: | ||||||
|  |             d.show() | ||||||
|  |             return d | ||||||
|  | 
 | ||||||
|  |     def show_transaction(self,tx=None,txid=None,parent = None): | ||||||
|  |         if not parent: | ||||||
|  |             parent = self.window | ||||||
|  |         if txid !=None and txid in self.willitems: | ||||||
|  |             tx=self.willitems[txid].tx | ||||||
|  |         if not tx: | ||||||
|  |             raise Exception(_("no tx")) | ||||||
|  |         return self.show_transaction_real(tx,parent=parent) | ||||||
|  | 
 | ||||||
|  |     def invalidate_will(self): | ||||||
|  |         def on_success(result): | ||||||
|  |             if result: | ||||||
|  |                 self.show_message(_("Please sign and broadcast this transaction to invalidate current will")) | ||||||
|  |                 self.wallet.set_label(result.txid(),"BAL Invalidate") | ||||||
|  |                 a=self.show_transaction(result) | ||||||
|  |             else: | ||||||
|  |                 self.show_message(_("No transactions to invalidate")) | ||||||
|  |         def on_failure(exec_info): | ||||||
|  |             self.show_error(f"ERROR:{exec_info}") | ||||||
|  | 
 | ||||||
|  |         fee_per_byte=self.will_settings.get('tx_fees',1) | ||||||
|  |         task = partial(Will.invalidate_will,self.willitems,self.wallet,fee_per_byte) | ||||||
|  |         msg = _("Calculating Transactions") | ||||||
|  |         self.waiting_dialog = BalWaitingDialog(self, msg, task, on_success, on_failure,exe=False) | ||||||
|  |         self.waiting_dialog.exe() | ||||||
|  | 
 | ||||||
|  |     def sign_transactions(self,password): | ||||||
|  |         try: | ||||||
|  |             txs={} | ||||||
|  |             signed = None | ||||||
|  |             tosign = None | ||||||
|  |             def get_message(): | ||||||
|  |                 msg = "" | ||||||
|  |                 if signed: | ||||||
|  |                     msg=_(f"signed: {signed}\n") | ||||||
|  |                 return msg + _(f"signing: {tosign}") | ||||||
|  |             for txid in Will.only_valid(self.willitems): | ||||||
|  |                 wi = self.willitems[txid] | ||||||
|  |                 tx = copy.deepcopy(wi.tx) | ||||||
|  |                 if wi.get_status('COMPLETE'): | ||||||
|  |                     txs[txid]=tx | ||||||
|  |                     continue | ||||||
|  |                 tosign=txid | ||||||
|  |                 try: | ||||||
|  |                     self.waiting_dialog.update(get_message()) | ||||||
|  |                 except:pass | ||||||
|  |                 for txin in tx.inputs(): | ||||||
|  |                     prevout = txin.prevout.to_json() | ||||||
|  |                     if prevout[0] in self.willitems: | ||||||
|  |                         change = self.willitems[prevout[0]].tx.outputs()[prevout[1]] | ||||||
|  |                         txin._trusted_value_sats = change.value | ||||||
|  |                         try: | ||||||
|  |                             txin.script_descriptor = change.script_descriptor | ||||||
|  |                         except: | ||||||
|  |                             pass | ||||||
|  |                         txin.is_mine=True | ||||||
|  |                         txin._TxInput__address=change.address | ||||||
|  |                         txin._TxInput__scriptpubkey = change.scriptpubkey | ||||||
|  |                         txin._TxInput__value_sats = change.value | ||||||
|  |                  | ||||||
|  |                 self.wallet.sign_transaction(tx, password,ignore_warnings=True) | ||||||
|  |                 signed=tosign | ||||||
|  |                 is_complete=False | ||||||
|  |                 if tx.is_complete(): | ||||||
|  |                     is_complete=True | ||||||
|  |                     wi.set_status('COMPLETE',True) | ||||||
|  |                 txs[txid]=tx | ||||||
|  |         except Exception as e: | ||||||
|  |             return None | ||||||
|  |         return txs | ||||||
|  | 
 | ||||||
|  |     def get_wallet_password(self,message=None,parent=None): | ||||||
|  |         parent =self.window if not parent else parent | ||||||
|  |         password = None | ||||||
|  |         if self.wallet.has_keystore_encryption(): | ||||||
|  |             password = self.bal_plugin.password_dialog(parent=parent,msg=message) | ||||||
|  |             if password is None: | ||||||
|  |                 return False | ||||||
|  |             try: | ||||||
|  |                 self.wallet.check_password(password) | ||||||
|  |             except Exception as e: | ||||||
|  |                 self.show_error(str(e)) | ||||||
|  |                 password = self.get_wallet_password(message) | ||||||
|  |         return password | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     def on_close(self): | ||||||
|  |         try: | ||||||
|  |             if not self.disable_plugin: | ||||||
|  |                 close_window=BalCloseDialog(self) | ||||||
|  |                 close_window.close_plugin_task() | ||||||
|  |                 self.save_willitems() | ||||||
|  |                 self.heirs_tab.close() | ||||||
|  |                 self.will_tab.close() | ||||||
|  |                 self.tools_menu.removeAction(self.tools_menu.willexecutors_action) | ||||||
|  |                 self.window.toggle_tab(self.heirs_tab) | ||||||
|  |                 self.window.toggle_tab(self.will_tab) | ||||||
|  |                 self.window.tabs.update() | ||||||
|  |         except Exception as e:  | ||||||
|  |             pass | ||||||
|  | 
 | ||||||
|  |     def ask_password_and_sign_transactions(self,callback=None): | ||||||
|  |         def on_success(txs): | ||||||
|  |             if txs: | ||||||
|  |                 for txid,tx in txs.items(): | ||||||
|  |                     self.willitems[txid].tx=copy.deepcopy(tx) | ||||||
|  |                     self.will[txid]=self.willitems[txid].to_dict() | ||||||
|  |                 try: | ||||||
|  |                     self.will_list.update() | ||||||
|  |                 except: | ||||||
|  |                     pass | ||||||
|  |                 if callback: | ||||||
|  |                     try: | ||||||
|  |                         callback() | ||||||
|  |                     except Exception as e: | ||||||
|  |                         raise e | ||||||
|  | 
 | ||||||
|  |         def on_failure(exc_info): | ||||||
|  |             self.logger.info("sign fail: {}".format(exc_info)) | ||||||
|  |             self.show_error(exc_info) | ||||||
|  |         password= self.get_wallet_password()  | ||||||
|  |         task = partial(self.sign_transactions,password) | ||||||
|  |         msg = _('Signing transactions...') | ||||||
|  |         self.waiting_dialog = BalWaitingDialog(self, msg, task, on_success, on_failure,exe=False) | ||||||
|  |         self.waiting_dialog.exe() | ||||||
|  | 
 | ||||||
|  |     def broadcast_transactions(self,force=False): | ||||||
|  |         def on_success(sulcess): | ||||||
|  |             self.will_list.update() | ||||||
|  |             if sulcess: | ||||||
|  |                 self.logger.info("error, some transaction was not sent"); | ||||||
|  |                 self.show_warning(_("Some transaction was not broadcasted")) | ||||||
|  |                 return | ||||||
|  |             self.logger.debug("OK, sulcess transaction was sent") | ||||||
|  |             self.show_message(_("All transactions are broadcasted to respective Will-Executors")) | ||||||
|  | 
 | ||||||
|  |         def on_failure(err): | ||||||
|  |             self.logger.error(err) | ||||||
|  | 
 | ||||||
|  |         task = partial(self.push_transactions_to_willexecutors,force) | ||||||
|  |         msg = _('Selecting Will-Executors') | ||||||
|  |         self.waiting_dialog = BalWaitingDialog(self,msg,task,on_success,on_failure,exe=False) | ||||||
|  |         self.waiting_dialog.exe() | ||||||
|  | 
 | ||||||
|  |     def push_transactions_to_willexecutors(self,force=False): | ||||||
|  |         willexecutors = Willexecutors.get_willexecutor_transactions(self.willitems) | ||||||
|  |         def getMsg(willexecutors): | ||||||
|  |             msg = "Broadcasting Transactions to Will-Executors:\n" | ||||||
|  |             for url in willexecutors: | ||||||
|  |                 msg += f"{url}:\t{willexecutors[url]['broadcast_status']}\n" | ||||||
|  |             return msg | ||||||
|  |         error=False | ||||||
|  |         for url in willexecutors: | ||||||
|  |             willexecutor = willexecutors[url] | ||||||
|  |             self.waiting_dialog.update(getMsg(willexecutors)) | ||||||
|  |             if 'txs' in willexecutor: | ||||||
|  |                 try: | ||||||
|  |                     if Willexecutors.push_transactions_to_willexecutor(willexecutors[url]): | ||||||
|  |                         for wid in willexecutors[url]['txsids']: | ||||||
|  |                             self.willitems[wid].set_status('PUSHED', True) | ||||||
|  |                         willexecutors[url]['broadcast_stauts'] = _("Success") | ||||||
|  |                     else: | ||||||
|  |                         for wid in willexecutors[url]['txsids']: | ||||||
|  |                             self.willitems[wid].set_status('PUSH_FAIL', True) | ||||||
|  |                             error=True | ||||||
|  |                         willexecutors[url]['broadcast_stauts'] = _("Failed") | ||||||
|  |                     del willexecutor['txs'] | ||||||
|  |                 except Willexecutors.AlreadyPresentException: | ||||||
|  |                     for wid in willexecutor['txsids']: | ||||||
|  |                         row = self.waiting_dialog.update("checking {} - {} : {}".format(self.willitems[wid].we['url'],wid, "Waiting")) | ||||||
|  |                         self.willitems[wid].check_willexecutor() | ||||||
|  |                         row = self.waiting_dialog.update("checked {} - {} : {}".format(self.willitems[wid].we['url'],wid,self.willitems[wid].get_status("CHECKED" ))) | ||||||
|  | 
 | ||||||
|  |         if error: | ||||||
|  |             return True | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     def export_json_file(self,path): | ||||||
|  |         for wid in self.willitems: | ||||||
|  |             self.willitems[wid].set_status('EXPORTED', True) | ||||||
|  |             self.will[wid]=self.willitems[wid].to_dict() | ||||||
|  |         write_json_file(path, self.will) | ||||||
|  | 
 | ||||||
|  |     def export_will(self): | ||||||
|  |         try: | ||||||
|  |             Util.export_meta_gui(self.window, _('will.json'), self.export_json_file) | ||||||
|  |         except Exception as e: | ||||||
|  |             self.show_error(str(e)) | ||||||
|  |             raise e | ||||||
|  |          | ||||||
|  |     def import_will(self): | ||||||
|  |         def sulcess(): | ||||||
|  |             self.will_list.update_will(self.willitems) | ||||||
|  |         import_meta_gui(self.window, _('will'), self.import_json_file,sulcess) | ||||||
|  | 
 | ||||||
|  |     def import_json_file(self,path): | ||||||
|  |         try: | ||||||
|  |             data = read_json_file(path) | ||||||
|  |             willitems = {} | ||||||
|  |             for k,v in data.items(): | ||||||
|  |                 data[k]['tx']=tx_from_any(v['tx']) | ||||||
|  |                 willitems[k]=Will.WillItem(data[k],_id=k) | ||||||
|  |             self.update_will(willitems) | ||||||
|  |         except Exception as e: | ||||||
|  |             raise e | ||||||
|  |             raise FileImportFailed(_("Invalid will file")) | ||||||
|  | 
 | ||||||
|  |     def check_transactions_task(self,will): | ||||||
|  |         start = time.time() | ||||||
|  |         for wid,w in will.items(): | ||||||
|  |             if w.we: | ||||||
|  |                 self.waiting_dialog.update("checking transaction: {}\n willexecutor: {}".format(wid,w.we['url'])) | ||||||
|  |                 w.check_willexecutor() | ||||||
|  | 
 | ||||||
|  |         if time.time()-start < 3: | ||||||
|  |             time.sleep(3-(time.time()-start)) | ||||||
|  | 
 | ||||||
|  |     def check_transactions(self,will): | ||||||
|  |         def on_success(result): | ||||||
|  |             del self.waiting_dialog | ||||||
|  |             self.update_all() | ||||||
|  |             pass | ||||||
|  |         def on_failure(e): | ||||||
|  |             self.logger.error(f"error checking transactions {e}") | ||||||
|  |             pass | ||||||
|  | 
 | ||||||
|  |         task = partial(self.check_transactions_task,will) | ||||||
|  |         msg = _('Check Transaction') | ||||||
|  |         self.waiting_dialog = BalWaitingDialog(self,msg,task,on_success,on_failure,exe=False) | ||||||
|  |         self.waiting_dialog.exe() | ||||||
|  | 
 | ||||||
|  |     def ping_willexecutors_task(self,wes): | ||||||
|  |         self.logger.info("ping willexecutots task") | ||||||
|  |         pinged = [] | ||||||
|  |         failed = [] | ||||||
|  |         def get_title(): | ||||||
|  |             msg = _('Ping Will-Executors:') | ||||||
|  |             msg += "\n\n" | ||||||
|  |             for url in wes: | ||||||
|  |                 urlstr = "{:<50}: ".format(url[:50]) | ||||||
|  |                 if url in pinged: | ||||||
|  |                     urlstr += "Ok" | ||||||
|  |                 elif url in failed: | ||||||
|  |                     urlstr +="Ko" | ||||||
|  |                 else: | ||||||
|  |                     urlstr += "--" | ||||||
|  |                 urlstr+="\n" | ||||||
|  |                 msg+=urlstr | ||||||
|  | 
 | ||||||
|  |             return msg  | ||||||
|  |         for url,we in wes.items(): | ||||||
|  |             try: | ||||||
|  |                 self.waiting_dialog.update(get_title()) | ||||||
|  |             except: | ||||||
|  |                 pass | ||||||
|  |             wes[url]=Willexecutors.get_info_task(url,we) | ||||||
|  |             if wes[url]['status']=='KO': | ||||||
|  |                 failed.append(url) | ||||||
|  |             else: | ||||||
|  |                 pinged.append(url) | ||||||
|  |              | ||||||
|  |     def ping_willexecutors(self,wes): | ||||||
|  |         def on_success(result): | ||||||
|  |             del self.waiting_dialog | ||||||
|  |             try: | ||||||
|  |                 self.willexecutor_dialog.willexecutor_list.update() | ||||||
|  |             except Exception as e: | ||||||
|  |                 _logger.error("error updating willexecutors {e}") | ||||||
|  |                 pass | ||||||
|  |         def on_failure(e): | ||||||
|  |             self.logger.error(e) | ||||||
|  |             pass | ||||||
|  |         self.logger.info("ping willexecutors") | ||||||
|  |         task = partial(self.ping_willexecutors_task,wes) | ||||||
|  |         msg = _('Ping Will-Executors') | ||||||
|  |         self.waiting_dialog = BalWaitingDialog(self,msg,task,on_success,on_failure,exe=False) | ||||||
|  |         self.waiting_dialog.exe() | ||||||
|  | 
 | ||||||
|  |     def preview_modal_dialog(self): | ||||||
|  |         self.dw=WillDetailDialog(self) | ||||||
|  |         self.dw.show() | ||||||
|  |              | ||||||
|  |     def settings_dialog(self,window): | ||||||
|  |         d = BalDialog(window, self.get_window_title("Settings")) | ||||||
|  |         d.setMinimumSize(100, 200) | ||||||
|  |         qicon=qt_resources.read_QPixmap("bal32x32.png") | ||||||
|  |         lbl_logo = QLabel() | ||||||
|  |         lbl_logo.setPixmap(qicon) | ||||||
|  | 
 | ||||||
|  |         heir_locktime_time = QSpinBox() | ||||||
|  |         heir_locktime_time.setMinimum(0) | ||||||
|  |         heir_locktime_time.setMaximum(3650) | ||||||
|  |         heir_locktime_time.setValue(int(self.bal_plugin.config_get(BalPlugin.LOCKTIME_TIME))) | ||||||
|  |         def on_heir_locktime_time(): | ||||||
|  |             value = heir_locktime_time.value() | ||||||
|  |             self.bal_plugin.config.set_key(BalPlugin.LOCKTIME_TIME,value,save=True) | ||||||
|  |         heir_locktime_time.valueChanged.connect(on_heir_locktime_time) | ||||||
|  | 
 | ||||||
|  |         heir_locktimedelta_time = QSpinBox() | ||||||
|  |         heir_locktimedelta_time.setMinimum(0) | ||||||
|  |         heir_locktimedelta_time.setMaximum(3650) | ||||||
|  |         heir_locktimedelta_time.setValue(int(self.bal_plugin.config_get(BalPlugin.LOCKTIMEDELTA_TIME))) | ||||||
|  |         def on_heir_locktime_time(): | ||||||
|  |             value = heir_locktime_time.value | ||||||
|  |             self.bal_plugin.config.set_key(BalPlugin.LOCKTIME_TIME,value,save=True) | ||||||
|  |         heir_locktime_time.valueChanged.connect(on_heir_locktime_time) | ||||||
|  | 
 | ||||||
|  |         heir_locktime_blocks = QSpinBox() | ||||||
|  |         heir_locktime_blocks.setMinimum(0) | ||||||
|  |         heir_locktime_blocks.setMaximum(144*3650) | ||||||
|  |         heir_locktime_blocks.setValue(int(self.bal_plugin.config_get(BalPlugin.LOCKTIME_BLOCKS))) | ||||||
|  |         def on_heir_locktime_blocks(): | ||||||
|  |             value = heir_locktime_blocks.value() | ||||||
|  |             self.bal_plugin.config.set_key(BalPlugin.LOCKTIME_BLOCKS,value,save=True) | ||||||
|  |         heir_locktime_blocks.valueChanged.connect(on_heir_locktime_blocks) | ||||||
|  | 
 | ||||||
|  |         heir_locktimedelta_blocks = QSpinBox() | ||||||
|  |         heir_locktimedelta_blocks.setMinimum(0) | ||||||
|  |         heir_locktimedelta_blocks.setMaximum(144*3650) | ||||||
|  |         heir_locktimedelta_blocks.setValue(int(self.bal_plugin.config_get(BalPlugin.LOCKTIMEDELTA_BLOCKS))) | ||||||
|  |         def on_heir_locktimedelta_blocks(): | ||||||
|  |             value = heir_locktimedelta_blocks.value() | ||||||
|  |             self.bal_plugin.config.set_key(BalPlugin.LOCKTIMEDELTA_TIME,value,save=True) | ||||||
|  |         heir_locktimedelta_blocks.valueChanged.connect(on_heir_locktimedelta_blocks) | ||||||
|  | 
 | ||||||
|  |         heir_tx_fees = QSpinBox() | ||||||
|  |         heir_tx_fees.setMinimum(1) | ||||||
|  |         heir_tx_fees.setMaximum(10000) | ||||||
|  |         heir_tx_fees.setValue(int(self.bal_plugin.config_get(BalPlugin.TX_FEES))) | ||||||
|  |         def on_heir_tx_fees(): | ||||||
|  |             value = heir_tx_fees.value() | ||||||
|  |             self.bal_plugin.config.set_key(BalPlugin.TX_FEES,value,save=True) | ||||||
|  |         heir_tx_fees.valueChanged.connect(on_heir_tx_fees) | ||||||
|  | 
 | ||||||
|  |         heir_broadcast = bal_checkbox(self.bal_plugin, BalPlugin.BROADCAST) | ||||||
|  |         heir_ask_broadcast = bal_checkbox(self.bal_plugin, BalPlugin.ASK_BROADCAST) | ||||||
|  |         heir_invalidate = bal_checkbox(self.bal_plugin, BalPlugin.INVALIDATE) | ||||||
|  |         heir_ask_invalidate = bal_checkbox(self.bal_plugin, BalPlugin.ASK_INVALIDATE) | ||||||
|  |         heir_preview = bal_checkbox(self.bal_plugin, BalPlugin.PREVIEW) | ||||||
|  |         heir_ping_willexecutors = bal_checkbox(self.bal_plugin, BalPlugin.PING_WILLEXECUTORS) | ||||||
|  |         heir_ask_ping_willexecutors = bal_checkbox(self.bal_plugin, BalPlugin.ASK_PING_WILLEXECUTORS) | ||||||
|  |         heir_no_willexecutor = bal_checkbox(self.bal_plugin, BalPlugin.NO_WILLEXECUTOR) | ||||||
|  | 
 | ||||||
|  |          | ||||||
|  |         heir_hide_replaced = bal_checkbox(self.bal_plugin,BalPlugin.HIDE_REPLACED,self) | ||||||
|  |         heir_hide_invalidated = bal_checkbox(self.bal_plugin,BalPlugin.HIDE_INVALIDATED,self) | ||||||
|  |         heir_allow_repush = bal_checkbox(self.bal_plugin,BalPlugin.ALLOW_REPUSH,self) | ||||||
|  |         heir_repush = QPushButton("Rebroadcast transactions") | ||||||
|  |         heir_repush.clicked.connect(partial(self.broadcast_transactions,True)) | ||||||
|  |         grid=QGridLayout(d) | ||||||
|  |         #add_widget(grid,"Refresh Time Days",heir_locktime_time,0,"Delta days for inputs to  be invalidated and transactions resubmitted") | ||||||
|  |         #add_widget(grid,"Refresh Blocks",heir_locktime_blocks,1,"Delta blocks for inputs to be invalidated and transaction resubmitted") | ||||||
|  |         #add_widget(grid,"Transaction fees",heir_tx_fees,1,"Default transaction fees") | ||||||
|  |         #add_widget(grid,"Broadcast transactions",heir_broadcast,3,"") | ||||||
|  |         #add_widget(grid," - Ask before",heir_ask_broadcast,4,"") | ||||||
|  |         #add_widget(grid,"Invalidate transactions",heir_invalidate,5,"") | ||||||
|  |         #add_widget(grid," - Ask before",heir_ask_invalidate,6,"") | ||||||
|  |         #add_widget(grid,"Show preview before sign",heir_preview,7,"") | ||||||
|  | 
 | ||||||
|  |         #grid.addWidget(lbl_logo,0,0)  | ||||||
|  |         add_widget(grid,"Hide Replaced",heir_hide_replaced, 1, "Hide replaced transactions from will detail and list") | ||||||
|  |         add_widget(grid,"Hide Invalidated",heir_hide_invalidated ,2,"Hide invalidated transactions from will detail and list") | ||||||
|  |         add_widget(grid,"Ping Willexecutors",heir_ping_willexecutors,3,"Ping willexecutors to get payment info before compiling will") | ||||||
|  |         add_widget(grid," - Ask before",heir_ask_ping_willexecutors,4,"Ask before to ping willexecutor") | ||||||
|  |         add_widget(grid,"Backup Transaction",heir_no_willexecutor,5,"Add transactions without willexecutor") | ||||||
|  |         grid.addWidget(heir_repush,6,0) | ||||||
|  |         grid.addWidget(HelpButton("Broadcast all transactions to willexecutors including those already pushed"),6,2) | ||||||
|  |         #add_widget(grid,"Max Allowed TimeDelta Days",heir_locktimedelta_time,8,"") | ||||||
|  |         #add_widget(grid,"Max Allowed BlocksDelta",heir_locktimedelta_blocks,9,"") | ||||||
|  | 
 | ||||||
|  |         if ret := bool(d.exec()): | ||||||
|  |             try: | ||||||
|  |                 self.update_all() | ||||||
|  |                 return ret | ||||||
|  |             except: | ||||||
|  |                 pass | ||||||
|  |         return False | ||||||
|  | 
 | ||||||
|  |     def update_all(self): | ||||||
|  |         self.will_list.update_will(self.willitems) | ||||||
|  |         self.heirs_tab.update() | ||||||
|  |         self.will_tab.update() | ||||||
|  |         self.will_list.update() | ||||||
|  | 
 | ||||||
|  | def add_widget(grid,label,widget,row,help_): | ||||||
|  |     grid.addWidget(QLabel(_(label)),row,0) | ||||||
|  |     grid.addWidget(widget,row,1) | ||||||
|  |     grid.addWidget(HelpButton(help_),row,2) | ||||||
							
								
								
									
										458
									
								
								util.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										458
									
								
								util.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,458 @@ | |||||||
|  | from datetime import datetime,timedelta | ||||||
|  | import bisect | ||||||
|  | from electrum.gui.qt.util import getSaveFileName | ||||||
|  | from electrum.i18n import _ | ||||||
|  | from electrum.transaction import PartialTxOutput | ||||||
|  | import urllib.request | ||||||
|  | import urllib.parse | ||||||
|  | from electrum.util import write_json_file,FileImportFailed,FileExportFailed | ||||||
|  | 
 | ||||||
|  | LOCKTIME_THRESHOLD = 500000000 | ||||||
|  | def locktime_to_str(locktime): | ||||||
|  |     try: | ||||||
|  |         locktime=int(locktime) | ||||||
|  |         if locktime > LOCKTIME_THRESHOLD: | ||||||
|  |             dt = datetime.fromtimestamp(locktime).isoformat() | ||||||
|  |             return dt | ||||||
|  | 
 | ||||||
|  |     except Exception as e: | ||||||
|  |         #print(e) | ||||||
|  |         pass | ||||||
|  |     return str(locktime) | ||||||
|  | 
 | ||||||
|  | def str_to_locktime(locktime): | ||||||
|  |     try: | ||||||
|  |         if locktime[-1] in ('y','d','b'): | ||||||
|  |           return locktime | ||||||
|  |         else: return int(locktime) | ||||||
|  |     except Exception as e: | ||||||
|  |         pass | ||||||
|  |         #print(e) | ||||||
|  |     dt_object = datetime.fromisoformat(locktime) | ||||||
|  |     timestamp = dt_object.timestamp() | ||||||
|  |     return int(timestamp) | ||||||
|  | 
 | ||||||
|  | def parse_locktime_string(locktime,w=None):  | ||||||
|  |     try:  | ||||||
|  |         return int(locktime)  | ||||||
|  |   | ||||||
|  |     except Exception as e:  | ||||||
|  |         pass | ||||||
|  |         #print("parse_locktime_string",e)  | ||||||
|  |     try:  | ||||||
|  |         now = datetime.now()  | ||||||
|  |         if locktime[-1] == 'y':  | ||||||
|  |             locktime = str(int(locktime[:-1])*365) + "d"  | ||||||
|  |         if locktime[-1] == 'd':  | ||||||
|  |             return int((now + timedelta(days = int(locktime[:-1]))).replace(hour=0,minute=0,second=0,microsecond=0).timestamp())  | ||||||
|  |         if locktime[-1] == 'b':  | ||||||
|  |             locktime = int(locktime[:-1])  | ||||||
|  |             height = 0  | ||||||
|  |             if w:  | ||||||
|  |                 height = get_current_height(w.network)  | ||||||
|  |             locktime+=int(height)  | ||||||
|  |         return int(locktime)  | ||||||
|  |     except Exception as e:  | ||||||
|  |         print("parse_locktime_string",e)  | ||||||
|  |         #raise e | ||||||
|  |     return 0 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def int_locktime(seconds=0,minutes=0,hours=0, days=0, blocks = 0): | ||||||
|  |     return int(seconds + minutes*60 + hours*60*60 + days*60*60*24 + blocks * 600) | ||||||
|  | 
 | ||||||
|  | def encode_amount(amount, decimal_point): | ||||||
|  |     if is_perc(amount): | ||||||
|  |         return amount | ||||||
|  |     else: | ||||||
|  |         try: | ||||||
|  |             return int(float(amount)*pow(10,decimal_point)) | ||||||
|  |         except: | ||||||
|  |             return 0 | ||||||
|  | 
 | ||||||
|  | def decode_amount(amount,decimal_point): | ||||||
|  |     if is_perc(amount): | ||||||
|  |         return amount | ||||||
|  |     else: | ||||||
|  |         num=8-decimal_point | ||||||
|  |         basestr="{{:0{}.{}f}}".format(num,num) | ||||||
|  |         return "{:08.8f}".format(float(amount)/pow(10,decimal_point)) | ||||||
|  | 
 | ||||||
|  | def is_perc(value):  | ||||||
|  |         try: | ||||||
|  |             return value[-1] == '%' | ||||||
|  |         except: | ||||||
|  |             return False | ||||||
|  | 
 | ||||||
|  | def cmp_array(heira,heirb): | ||||||
|  |     try: | ||||||
|  |         if not len(heira) == len(heirb): | ||||||
|  |             return False | ||||||
|  |         for h in range(0,len(heira)): | ||||||
|  |             if not heira[h] == heirb[h]: | ||||||
|  |                 return False | ||||||
|  |         return True | ||||||
|  |     except: | ||||||
|  |         return False | ||||||
|  | 
 | ||||||
|  | def cmp_heir(heira,heirb): | ||||||
|  |     if heira[0] == heirb[0] and heira[1] == heirb[1]:  | ||||||
|  |         return True | ||||||
|  |     return False | ||||||
|  | 
 | ||||||
|  | def cmp_willexecutor(willexecutora,willexecutorb): | ||||||
|  |     if willexecutora == willexecutorb: | ||||||
|  |         return True | ||||||
|  |     try: | ||||||
|  |         if willexecutora['url']==willexecutorb['url'] and willexecutora['address'] == willexecutorb['address'] and willexecutora['base_fee']==willexecutorb['base_fee']: | ||||||
|  |             return True | ||||||
|  |     except: | ||||||
|  |         return False | ||||||
|  |     return False | ||||||
|  | 
 | ||||||
|  | def search_heir_by_values(heirs,heir,values): | ||||||
|  |     #print() | ||||||
|  |     for h,v in heirs.items(): | ||||||
|  |         found = False | ||||||
|  |         for val in values: | ||||||
|  |             if val in v and v[val] != heir[val]: | ||||||
|  |                 found = True | ||||||
|  | 
 | ||||||
|  |         if not found: | ||||||
|  |             return h | ||||||
|  |     return False | ||||||
|  | 
 | ||||||
|  | def cmp_heir_by_values(heira,heirb,values): | ||||||
|  |     for v in values: | ||||||
|  |         if heira[v] != heirb[v]: | ||||||
|  |             return False | ||||||
|  |     return True | ||||||
|  | 
 | ||||||
|  | def cmp_heirs_by_values(heirsa,heirsb,values,exclude_willexecutors=False,reverse = True): | ||||||
|  |     for heira in heirsa: | ||||||
|  |         if (exclude_willexecutors and not "w!ll3x3c\"" in heira) or not exclude_willexecutors: | ||||||
|  |             found = False | ||||||
|  |             for heirb in heirsb: | ||||||
|  |                 if cmp_heir_by_values(heirsa[heira],heirsb[heirb],values): | ||||||
|  |                     found=True | ||||||
|  |             if not found: | ||||||
|  |                 #print(f"not_found {heira}--{heirsa[heira]}") | ||||||
|  |                 return False | ||||||
|  |     if reverse: | ||||||
|  |         return cmp_heirs_by_values(heirsb,heirsa,values,exclude_willexecutors=exclude_willexecutors,reverse=False) | ||||||
|  |     else: | ||||||
|  |         return True | ||||||
|  |      | ||||||
|  | def cmp_heirs(heirsa,heirsb,cmp_function = lambda x,y: x[0]==y[0] and x[3]==y[3],reverse=True): | ||||||
|  |     try: | ||||||
|  |         for heir in heirsa: | ||||||
|  |             if not "w!ll3x3c\"" in heir: | ||||||
|  |                 if not heir in heirsb or not cmp_function(heirsa[heir],heirsb[heir]): | ||||||
|  |                     if not search_heir_by_values(heirsb,heirsa[heir],[0,3]): | ||||||
|  |                         return False | ||||||
|  |         if reverse: | ||||||
|  |             return cmp_heirs(heirsb,heirsa,cmp_function,False) | ||||||
|  |         else: | ||||||
|  |             return True | ||||||
|  |     except Exception as e: | ||||||
|  |         raise e | ||||||
|  |         return False | ||||||
|  | 
 | ||||||
|  | def cmp_inputs(inputsa,inputsb): | ||||||
|  |     if len(inputsa) != len(inputsb):  | ||||||
|  |         return False  | ||||||
|  |     for inputa in inputsa: | ||||||
|  |         if not in_utxo(inputa,inputsb): | ||||||
|  |             return False | ||||||
|  |     return True | ||||||
|  | 
 | ||||||
|  | def cmp_outputs(outputsa,outputsb,willexecutor_output = None): | ||||||
|  |     if len(outputsa) != len(outputsb):  | ||||||
|  |         return False  | ||||||
|  |     for outputa in outputsa:  | ||||||
|  |         if not cmp_output(outputa,willexecutor_output): | ||||||
|  |             if not in_output(outputa,outputsb):  | ||||||
|  |                 return False | ||||||
|  |     return True | ||||||
|  | 
 | ||||||
|  | def cmp_txs(txa,txb): | ||||||
|  |     if not cmp_inputs(txa.inputs(),txb.inputs()): | ||||||
|  |         return False | ||||||
|  |     if not cmp_outputs(txa.outputs(),txb.outputs()): | ||||||
|  |         return False | ||||||
|  |     return True | ||||||
|  | 
 | ||||||
|  | def get_value_amount(txa,txb): | ||||||
|  |     outputsa=txa.outputs() | ||||||
|  |     outputsb=txb.outputs() | ||||||
|  |     value_amount = 0 | ||||||
|  |     #if len(outputsa) != len(outputsb): | ||||||
|  |     #    print("outputlen is different") | ||||||
|  |     #    return False | ||||||
|  | 
 | ||||||
|  |     for outa in outputsa: | ||||||
|  |         same_amount,same_address = in_output(outa,txb.outputs()) | ||||||
|  |         if not (same_amount or same_address): | ||||||
|  |             #print("outa notin txb", same_amount,same_address) | ||||||
|  |             return False | ||||||
|  |         if same_amount and same_address: | ||||||
|  |             value_amount+=outa.value | ||||||
|  |         if same_amount: | ||||||
|  |             pass | ||||||
|  |             #print("same amount") | ||||||
|  |         if same_address: | ||||||
|  |             pass | ||||||
|  |             #print("same address") | ||||||
|  | 
 | ||||||
|  |     return value_amount | ||||||
|  |     #not needed | ||||||
|  |     #for outb in outputsb: | ||||||
|  |     #    if not in_output(outb,txa.outputs()): | ||||||
|  |     #        print("outb notin txb") | ||||||
|  |     #        return False | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def chk_locktime(timestamp_to_check,block_height_to_check,locktime): | ||||||
|  |     #TODO BUG:  WHAT HAPPEN AT THRESHOLD? | ||||||
|  |     locktime=int(locktime) | ||||||
|  |     if locktime > LOCKTIME_THRESHOLD and locktime > timestamp_to_check: | ||||||
|  |         return True | ||||||
|  |     elif locktime < LOCKTIME_THRESHOLD and locktime > block_height_to_check: | ||||||
|  |         return True | ||||||
|  |     else: | ||||||
|  |         return False | ||||||
|  | 
 | ||||||
|  | def anticipate_locktime(locktime,blocks=0,hours=0,days=0): | ||||||
|  |     locktime = int(locktime) | ||||||
|  |     out=0 | ||||||
|  |     if locktime> LOCKTIME_THRESHOLD: | ||||||
|  |         seconds = blocks*600 + hours*3600 + days*86400 | ||||||
|  |         dt = datetime.fromtimestamp(locktime) | ||||||
|  |         dt -= timedelta(seconds=seconds) | ||||||
|  |         out = dt.timestamp() | ||||||
|  |     else: | ||||||
|  |         blocks -= hours*6 + days*144 | ||||||
|  |         out = locktime + blocks | ||||||
|  | 
 | ||||||
|  |     if out < 1: | ||||||
|  |         out = 1  | ||||||
|  |     return out | ||||||
|  | 
 | ||||||
|  | def cmp_locktime(locktimea,locktimeb): | ||||||
|  |     if locktimea==locktimeb: | ||||||
|  |         return 0 | ||||||
|  |     strlocktime = str(locktimea) | ||||||
|  |     strlocktimeb = str(locktimeb) | ||||||
|  |     intlocktimea = str_to_locktime(strlocktimea) | ||||||
|  |     intlocktimeb = str_to_locktime(strlocktimeb) | ||||||
|  |     if locktimea[-1] in "ydb": | ||||||
|  |         if locktimeb[-1] == locktimea[-1]: | ||||||
|  |             return int(strlocktimea[-1])-int(strlocktimeb[-1]) | ||||||
|  |         else: | ||||||
|  |             return int(locktimea)-(locktimeb) | ||||||
|  |      | ||||||
|  | 
 | ||||||
|  | def get_lowest_valid_tx(available_utxos,will): | ||||||
|  |     will = sorted(will.items(),key = lambda x: x[1]['tx'].locktime) | ||||||
|  |     for txid,willitem in will.items(): | ||||||
|  |         pass | ||||||
|  | 
 | ||||||
|  | def get_locktimes(will): | ||||||
|  |     locktimes = {} | ||||||
|  |     for txid,willitem in will.items(): | ||||||
|  |         locktimes[willitem['tx'].locktime]=True | ||||||
|  |     return locktimes.keys() | ||||||
|  | 
 | ||||||
|  | def get_lowest_locktimes(locktimes): | ||||||
|  |     sorted_timestamp=[] | ||||||
|  |     sorted_block=[] | ||||||
|  |     for l in locktimes: | ||||||
|  |         #print("locktime:",parse_locktime_string(l)) | ||||||
|  |         l=parse_locktime_string(l) | ||||||
|  |         if l < LOCKTIME_THRESHOLD: | ||||||
|  |             bisect.insort(sorted_block,l) | ||||||
|  |         else: | ||||||
|  |             bisect.insort(sorted_timestamp,l) | ||||||
|  | 
 | ||||||
|  |     return sorted(sorted_timestamp), sorted(sorted_block) | ||||||
|  | 
 | ||||||
|  | def get_lowest_locktimes_from_will(will): | ||||||
|  |     return get_lowest_locktimes(get_locktimes(will)) | ||||||
|  | 
 | ||||||
|  | def search_willtx_per_io(will,tx): | ||||||
|  |     for wid, w in will.items(): | ||||||
|  |         if cmp_txs(w['tx'],tx['tx']): | ||||||
|  |             return wid,w | ||||||
|  |     return None, None | ||||||
|  | 
 | ||||||
|  | def invalidate_will(will): | ||||||
|  |     raise Exception("not implemented") | ||||||
|  | 
 | ||||||
|  | def get_will_spent_utxos(will): | ||||||
|  |     utxos=[] | ||||||
|  |     for txid,willitem in will.items(): | ||||||
|  |         utxos+=willitem['tx'].inputs() | ||||||
|  |          | ||||||
|  |     return utxos | ||||||
|  | 
 | ||||||
|  | def utxo_to_str(utxo): | ||||||
|  |     try: return utxo.to_str() | ||||||
|  |     except Exception as e: pass | ||||||
|  |     try: return utxo.prevout.to_str() | ||||||
|  |     except Exception as e: pass | ||||||
|  |     return str(utxo) | ||||||
|  | 
 | ||||||
|  | def cmp_utxo(utxoa,utxob): | ||||||
|  |     utxoa=utxo_to_str(utxoa) | ||||||
|  |     utxob=utxo_to_str(utxob) | ||||||
|  |     if utxoa == utxob: | ||||||
|  |     #if utxoa.prevout.txid==utxob.prevout.txid and utxoa.prevout.out_idx == utxob.prevout.out_idx: | ||||||
|  |         return True | ||||||
|  |     else: | ||||||
|  |         return False | ||||||
|  | 
 | ||||||
|  | def in_utxo(utxo, utxos): | ||||||
|  |     for s_u in utxos: | ||||||
|  |         if cmp_utxo(s_u,utxo): | ||||||
|  |             return True | ||||||
|  |     return False | ||||||
|  | 
 | ||||||
|  | def txid_in_utxo(txid,utxos): | ||||||
|  |     for s_u in utxos: | ||||||
|  |         if s_u.prevout.txid == txid: | ||||||
|  |             return True | ||||||
|  |     return False | ||||||
|  | 
 | ||||||
|  | def cmp_output(outputa,outputb): | ||||||
|  |     return outputa.address == outputb.address and outputa.value == outputb.value | ||||||
|  | 
 | ||||||
|  | def in_output(output,outputs): | ||||||
|  |     for s_o in outputs: | ||||||
|  |         if cmp_output(s_o,output): | ||||||
|  |             return True | ||||||
|  |     return False | ||||||
|  | 
 | ||||||
|  | #check all output with the same amount if none have the same address it can be a change | ||||||
|  | #return true true same address same amount  | ||||||
|  | #return true false same amount different address | ||||||
|  | #return false false different amount, different address not found | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def din_output(out,outputs): | ||||||
|  |     same_amount=[] | ||||||
|  |     for s_o in outputs: | ||||||
|  |         if int(out.value) == int(s_o.value): | ||||||
|  |             same_amount.append(s_o) | ||||||
|  |             if out.address==s_o.address: | ||||||
|  |                 #print("SAME_:",out.address,s_o.address) | ||||||
|  |                 return True, True | ||||||
|  |             else: | ||||||
|  |                 pass | ||||||
|  |                 #print("NOT  SAME_:",out.address,s_o.address) | ||||||
|  | 
 | ||||||
|  |     if len(same_amount)>0: | ||||||
|  |         return True, False | ||||||
|  |     else:return False, False | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def get_change_output(wallet,in_amount,out_amount,fee):  | ||||||
|  |     change_amount = int(in_amount - out_amount - fee)  | ||||||
|  |     if change_amount > wallet.dust_threshold():  | ||||||
|  |         change_addresses = wallet.get_change_addresses_for_new_transaction()  | ||||||
|  |         out = PartialTxOutput.from_address_and_value(change_addresses[0], change_amount)  | ||||||
|  |         out.is_change = True  | ||||||
|  |         return out | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def get_current_height(network:'Network'): | ||||||
|  |     #if no network or not up to date, just set locktime to zero | ||||||
|  |     if not network: | ||||||
|  |         return 0 | ||||||
|  |     chain = network.blockchain() | ||||||
|  |     if chain.is_tip_stale(): | ||||||
|  |         return 0 | ||||||
|  |     # figure out current block height | ||||||
|  |     chain_height = chain.height()  # learnt from all connected servers, SPV-checked | ||||||
|  |     server_height = network.get_server_height()  # height claimed by main server, unverified | ||||||
|  |     # note: main server might be lagging (either is slow, is malicious, or there is an SPV-invisible-hard-fork) | ||||||
|  |     #       - if it's lagging too much, it is the network's job to switch away | ||||||
|  |     if server_height < chain_height - 10: | ||||||
|  |         # the diff is suspiciously large... give up and use something non-fingerprintable | ||||||
|  |         return 0 | ||||||
|  |     # discourage "fee sniping" | ||||||
|  |     height = min(chain_height, server_height) | ||||||
|  |     return height | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def print_var(var,name = "",veryverbose=False): | ||||||
|  |     print(f"---{name}---") | ||||||
|  |     if not var is None: | ||||||
|  |         try: | ||||||
|  |             print("doc:",doc(var)) | ||||||
|  |         except: pass | ||||||
|  |         try: | ||||||
|  |             print("str:",str(var)) | ||||||
|  |         except: pass | ||||||
|  |         try: | ||||||
|  |             print("repr",repr(var)) | ||||||
|  |         except:pass | ||||||
|  |         try: | ||||||
|  |             print("dict",dict(var)) | ||||||
|  |         except:pass | ||||||
|  |         try: | ||||||
|  |             print("dir",dir(var)) | ||||||
|  |         except:pass | ||||||
|  |         try: | ||||||
|  |             print("type",type(var)) | ||||||
|  |         except:pass | ||||||
|  |         try: | ||||||
|  |             print("to_json",var.to_json()) | ||||||
|  |         except: pass | ||||||
|  |         try: | ||||||
|  |             print("__slotnames__",var.__slotnames__) | ||||||
|  |         except:pass | ||||||
|  | 
 | ||||||
|  |     print(f"---end {name}---") | ||||||
|  | 
 | ||||||
|  | def print_utxo(utxo, name = ""): | ||||||
|  |     print(f"---utxo-{name}---") | ||||||
|  |     print_var(utxo,name) | ||||||
|  |     print_prevout(utxo.prevout,name) | ||||||
|  |     print_var(utxo.script_sig,f"{name}-script-sig") | ||||||
|  |     print_var(utxo.witness,f"{name}-witness") | ||||||
|  |     #print("madonnamaiala_TXInput__scriptpubkey:",utxo._TXInput__scriptpubkey) | ||||||
|  |     print("_TxInput__address:",utxo._TxInput__address) | ||||||
|  |     print("_TxInput__scriptpubkey:",utxo._TxInput__scriptpubkey) | ||||||
|  |     print("_TxInput__value_sats:",utxo._TxInput__value_sats) | ||||||
|  |     print(f"---utxo-end {name}---") | ||||||
|  | 
 | ||||||
|  | def print_prevout(prevout, name = ""): | ||||||
|  |     print(f"---prevout-{name}---") | ||||||
|  |     print_var(prevout,f"{name}-prevout") | ||||||
|  |     print_var(prevout._asdict()) | ||||||
|  |     print(f"---prevout-end {name}---") | ||||||
|  | 
 | ||||||
|  | def export_meta_gui(electrum_window: 'ElectrumWindow', title, exporter): | ||||||
|  |     filter_ = "All files (*)" | ||||||
|  |     filename = getSaveFileName( | ||||||
|  |         parent=electrum_window, | ||||||
|  |         title=_("Select file to save your {}").format(title), | ||||||
|  |         filename='BALplugin_{}'.format(title), | ||||||
|  |         filter=filter_, | ||||||
|  |         config=electrum_window.config, | ||||||
|  |     ) | ||||||
|  |     if not filename: | ||||||
|  |         return | ||||||
|  |     try: | ||||||
|  |         exporter(filename) | ||||||
|  |     except FileExportFailed as e: | ||||||
|  |         electrum_window.show_critical(str(e)) | ||||||
|  |     else: | ||||||
|  |         electrum_window.show_message(_("Your {0} were exported to '{1}'") | ||||||
|  |                                  .format(title, str(filename))) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def copy(dicto,dictfrom): | ||||||
|  |     for k,v in dictfrom.items(): | ||||||
|  |         dicto[k]=v | ||||||
							
								
								
									
										802
									
								
								will.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										802
									
								
								will.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,802 @@ | |||||||
|  | import copy | ||||||
|  | 
 | ||||||
|  | from . import willexecutors as Willexecutors | ||||||
|  | from . import util as Util | ||||||
|  | 
 | ||||||
|  | from electrum.i18n import _ | ||||||
|  | 
 | ||||||
|  | from electrum.transaction import TxOutpoint,PartialTxInput,tx_from_any,PartialTransaction,PartialTxOutput,Transaction | ||||||
|  | from electrum.util import bfh, decimal_point_to_base_unit_name | ||||||
|  | from electrum.util import write_json_file,read_json_file,FileImportFailed | ||||||
|  | from electrum.logging import get_logger,Logger | ||||||
|  | from electrum.bitcoin import NLOCKTIME_BLOCKHEIGHT_MAX | ||||||
|  | 
 | ||||||
|  | MIN_LOCKTIME = 1 | ||||||
|  | MIN_BLOCK = 1 | ||||||
|  | _logger = get_logger(__name__) | ||||||
|  | 
 | ||||||
|  | #return an array with the list of children | ||||||
|  | def get_children(will,willid): | ||||||
|  |     out = [] | ||||||
|  |     for _id in will: | ||||||
|  |         inputs = will[_id].tx.inputs() | ||||||
|  |         for idi in range(0,len(inputs)): | ||||||
|  |             _input = inputs[idi] | ||||||
|  |             if _input.prevout.txid.hex() == willid: | ||||||
|  |                 out.append([_id,idi,_input.prevout.out_idx]) | ||||||
|  |     return out | ||||||
|  | 
 | ||||||
|  | #build a tree with parent transactions | ||||||
|  | def add_willtree(will): | ||||||
|  |     for willid in will: | ||||||
|  |         will[willid].children = get_children(will,willid) | ||||||
|  |         for child in will[willid].children: | ||||||
|  |             if not will[child[0]].father:  | ||||||
|  |                 will[child[0]].father = willid | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | #return a list of will sorted by locktime | ||||||
|  | def get_sorted_will(will): | ||||||
|  |     return sorted(will.items(),key = lambda x: x[1]['tx'].locktime) | ||||||
|  |                  | ||||||
|  | 
 | ||||||
|  | def only_valid(will): | ||||||
|  |     for k,v in will.items(): | ||||||
|  |         if v.get_status('VALID'): | ||||||
|  |             yield k | ||||||
|  | 
 | ||||||
|  | def search_equal_tx(will,tx,wid): | ||||||
|  |     for w in will: | ||||||
|  |         if  w != wid and not tx.to_json() != will[w]['tx'].to_json(): | ||||||
|  |             if will[w]['tx'].txid() != tx.txid(): | ||||||
|  |                 if Util.cmp_txs(will[w]['tx'],tx): | ||||||
|  |                     return will[w]['tx'] | ||||||
|  |     return False | ||||||
|  | 
 | ||||||
|  | def get_tx_from_any(x): | ||||||
|  |     try: | ||||||
|  |         a=str(x) | ||||||
|  |         return tx_from_any(a) | ||||||
|  |          | ||||||
|  |     except Exception as e: | ||||||
|  |         raise e | ||||||
|  | 
 | ||||||
|  |     return x | ||||||
|  | 
 | ||||||
|  | def add_info_from_will(will,wid,wallet): | ||||||
|  |     if isinstance(will[wid].tx,str): | ||||||
|  |         will[wid].tx = get_tx_from_any(will[wid].tx) | ||||||
|  |     if wallet: | ||||||
|  |         will[wid].tx.add_info_from_wallet(wallet) | ||||||
|  |     for txin in will[wid].tx.inputs(): | ||||||
|  |         txid = txin.prevout.txid.hex() | ||||||
|  |         if txid in will: | ||||||
|  |             change = will[txid].tx.outputs()[txin.prevout.out_idx] | ||||||
|  |             txin._trusted_value_sats = change.value | ||||||
|  |             try: | ||||||
|  |                 txin.script_descriptor = change.script_descriptor | ||||||
|  |             except: | ||||||
|  |                 pass | ||||||
|  |             txin.is_mine=True | ||||||
|  |             txin._TxInput__address=change.address | ||||||
|  |             txin._TxInput__scriptpubkey = change.scriptpubkey | ||||||
|  |             txin._TxInput__value_sats = change.value | ||||||
|  |             txin._trusted_value_sats = change.value | ||||||
|  | 
 | ||||||
|  | def normalize_will(will,wallet = None,others_inputs = {}): | ||||||
|  |     to_delete = [] | ||||||
|  |     to_add = {} | ||||||
|  |     #add info from wallet | ||||||
|  |     for wid in will: | ||||||
|  |         add_info_from_will(will,wid,wallet) | ||||||
|  |     errors ={} | ||||||
|  |     for wid in will: | ||||||
|  | 
 | ||||||
|  |         txid = will[wid].tx.txid() | ||||||
|  | 
 | ||||||
|  |         if txid is None: | ||||||
|  |             _logger.error("##########") | ||||||
|  |             _logger.error(wid) | ||||||
|  |             _logger.error(will[wid]) | ||||||
|  |             _logger.error(will[wid].tx.to_json()) | ||||||
|  |              | ||||||
|  |             _logger.error("txid is none") | ||||||
|  |             will[wid].set_status('ERROR',True) | ||||||
|  |             errors[wid]=will[wid] | ||||||
|  |             continue | ||||||
|  | 
 | ||||||
|  |         if txid != wid: | ||||||
|  |             outputs = will[wid].tx.outputs() | ||||||
|  |             ow=will[wid] | ||||||
|  |             ow.normalize_locktime(others_inputs) | ||||||
|  |             will[wid]=ow.to_dict() | ||||||
|  | 
 | ||||||
|  |             for i in range(0,len(outputs)): | ||||||
|  |                 change_input(will,wid,i,outputs[i],others_inputs,to_delete,to_add) | ||||||
|  | 
 | ||||||
|  |             to_delete.append(wid) | ||||||
|  |             to_add[ow.tx.txid()]=ow.to_dict() | ||||||
|  | 
 | ||||||
|  |     for eid,err in errors.items(): | ||||||
|  |         new_txid = err.tx.txid() | ||||||
|  | 
 | ||||||
|  |     for k,w in to_add.items(): | ||||||
|  |         will[k] = w | ||||||
|  | 
 | ||||||
|  |     for wid in to_delete: | ||||||
|  |         if wid in will: | ||||||
|  |             del will[wid] | ||||||
|  | 
 | ||||||
|  | def new_input(txid,idx,change): | ||||||
|  |     prevout = TxOutpoint(txid=bfh(txid), out_idx=idx) | ||||||
|  |     inp = PartialTxInput(prevout=prevout) | ||||||
|  |     inp._trusted_value_sats = change.value | ||||||
|  |     inp.is_mine=True | ||||||
|  |     inp._TxInput__address=change.address | ||||||
|  |     inp._TxInput__scriptpubkey = change.scriptpubkey | ||||||
|  |     inp._TxInput__value_sats = change.value | ||||||
|  |     return inp | ||||||
|  | 
 | ||||||
|  | def check_anticipate(ow:'WillItem',nw:'WillItem'): | ||||||
|  |     anticipate = Util.anticipate_locktime(ow.tx.locktime,days=1) | ||||||
|  |     if int(nw.tx.locktime) >= int(anticipate): | ||||||
|  |         if Util.cmp_heirs_by_values(ow.heirs,nw.heirs,[0,1],exclude_willexecutors = True): | ||||||
|  |             print("same heirs",ow._id,nw._id) | ||||||
|  |             if nw.we and ow.we: | ||||||
|  |                 if ow.we['url'] == nw.we['url']: | ||||||
|  |                     print("same willexecutors", ow.we['url'],nw.we['url']) | ||||||
|  |                     if int(ow.we['base_fee'])>int(nw.we['base_fee']): | ||||||
|  |                         print("anticipate") | ||||||
|  |                         return anticipate | ||||||
|  |                     else: | ||||||
|  |                         if int(ow.tx_fees) != int(nw.tx_fees): | ||||||
|  |                             return anticipate | ||||||
|  |                         else: | ||||||
|  |                             print("keep the same") | ||||||
|  |                             #_logger.debug("ow,base fee > nw.base_fee") | ||||||
|  |                             ow.tx.locktime | ||||||
|  |                 else: | ||||||
|  |                     #_logger.debug("ow.we['url']({ow.we['url']}) == nw.we['url']({nw.we['url']})") | ||||||
|  |                     print("keep the same") | ||||||
|  |                     ow.tx.locktime | ||||||
|  |             else: | ||||||
|  |                 if nw.we == ow.we: | ||||||
|  |                     if not Util.cmp_heirs_by_values(ow.heirs,nw.heirs,[0,3]): | ||||||
|  |                         return anticipate | ||||||
|  |                     else: | ||||||
|  |                         return ow.tx.locktime | ||||||
|  |                 else: | ||||||
|  |                     return ow.tx.locktime | ||||||
|  |         else: | ||||||
|  |             return anticipate | ||||||
|  |     return  4294967295+1 | ||||||
|  | 
 | ||||||
|  |       | ||||||
|  | def change_input(will, otxid, idx, change,others_inputs,to_delete,to_append): | ||||||
|  |     ow = will[otxid] | ||||||
|  |     ntxid = ow.tx.txid() | ||||||
|  |     if otxid != ntxid: | ||||||
|  |         for wid in will: | ||||||
|  |             w = will[wid] | ||||||
|  |             inputs = w.tx.inputs() | ||||||
|  |             outputs = w.tx.outputs() | ||||||
|  |             found = False | ||||||
|  |             old_txid = w.tx.txid() | ||||||
|  |             ntx = None | ||||||
|  |             for i in range(0,len(inputs)): | ||||||
|  |                 if inputs[i].prevout.txid.hex() == otxid and inputs[i].prevout.out_idx == idx: | ||||||
|  |                     if isinstance(w.tx,Transaction): | ||||||
|  |                         will[wid].tx = PartialTransaction.from_tx(w.tx) | ||||||
|  |                         will[wid].tx.set_rbf(True) | ||||||
|  |                     will[wid].tx._inputs[i]=new_input(wid,idx,change) | ||||||
|  |                     found = True | ||||||
|  |             if found == True: | ||||||
|  |                 pass | ||||||
|  | 
 | ||||||
|  |             new_txid = will[wid].tx.txid() | ||||||
|  |             if old_txid != new_txid: | ||||||
|  |                 to_delete.append(old_txid) | ||||||
|  |                 to_append[new_txid]=will[wid] | ||||||
|  |                 outputs = will[wid].tx.outputs() | ||||||
|  |                 for i in range(0,len(outputs)): | ||||||
|  |                     change_input(will, wid, i, outputs[i],others_inputs,to_delete,to_append) | ||||||
|  |                      | ||||||
|  | def get_all_inputs(will,only_valid = False): | ||||||
|  |     all_inputs = {} | ||||||
|  |     for w,wi in will.items(): | ||||||
|  |         if not only_valid or wi.get_status('VALID'): | ||||||
|  |             inputs = wi.tx.inputs() | ||||||
|  |             for i in inputs: | ||||||
|  |                 prevout_str = i.prevout.to_str() | ||||||
|  |                 inp=[w,will[w],i] | ||||||
|  |                 if not prevout_str in all_inputs: | ||||||
|  |                     all_inputs[prevout_str] = [inp] | ||||||
|  |                 else: | ||||||
|  |                     all_inputs[prevout_str].append(inp) | ||||||
|  |     return all_inputs | ||||||
|  | 
 | ||||||
|  | def get_all_inputs_min_locktime(all_inputs): | ||||||
|  |     all_inputs_min_locktime = {} | ||||||
|  |      | ||||||
|  |     for i,values in all_inputs.items(): | ||||||
|  |         min_locktime = min(values,key = lambda x:x[1].tx.locktime)[1].tx.locktime | ||||||
|  |         for w in values: | ||||||
|  |             if w[1].tx.locktime == min_locktime: | ||||||
|  |                 if not i in all_inputs_min_locktime: | ||||||
|  |                     all_inputs_min_locktime[i]=[w] | ||||||
|  |                 else: | ||||||
|  |                      all_inputs_min_locktime[i].append(w) | ||||||
|  | 
 | ||||||
|  |     return all_inputs_min_locktime | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def search_anticipate_rec(will,old_inputs): | ||||||
|  |     redo = False | ||||||
|  |     to_delete = [] | ||||||
|  |     to_append = {} | ||||||
|  |     new_inputs = get_all_inputs(will,only_valid = True) | ||||||
|  |     for nid,nwi in will.items(): | ||||||
|  |         if nwi.search_anticipate(new_inputs) or nwi.search_anticipate(old_inputs): | ||||||
|  |             if nid != nwi.tx.txid(): | ||||||
|  |                 redo = True | ||||||
|  |                 to_delete.append(nid) | ||||||
|  |                 to_append[nwi.tx.txid()] = nwi | ||||||
|  |                 outputs = nwi.tx.outputs() | ||||||
|  |                 for i in range(0,len(outputs)): | ||||||
|  |                     change_input(will,nid,i,outputs[i],new_inputs,to_delete,to_append) | ||||||
|  |                      | ||||||
|  | 
 | ||||||
|  |     for w in to_delete: | ||||||
|  |         try: | ||||||
|  |             del will[w] | ||||||
|  |         except: | ||||||
|  |             pass | ||||||
|  |     for k,w in to_append.items(): | ||||||
|  |         will[k]=w | ||||||
|  |     if redo: | ||||||
|  |         search_anticipate_rec(will,old_inputs) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def update_will(old_will,new_will): | ||||||
|  |     all_old_inputs = get_all_inputs(old_will,only_valid=True) | ||||||
|  |     all_inputs_min_locktime = get_all_inputs_min_locktime(all_old_inputs) | ||||||
|  |     all_new_inputs = get_all_inputs(new_will) | ||||||
|  |     #check if the new input is already spent by other transaction | ||||||
|  |     #if it is use the same locktime, or anticipate. | ||||||
|  |     search_anticipate_rec(new_will,all_old_inputs) | ||||||
|  | 
 | ||||||
|  |     other_inputs = get_all_inputs(old_will,{}) | ||||||
|  |     try: | ||||||
|  |         normalize_will(new_will,others_inputs=other_inputs) | ||||||
|  |     except Exception as e: | ||||||
|  |         raise e | ||||||
|  |                          | ||||||
|  | 
 | ||||||
|  |     for oid in only_valid(old_will): | ||||||
|  |         if oid in new_will: | ||||||
|  |             new_heirs = new_will[oid].heirs | ||||||
|  |             new_we = new_will[oid].we | ||||||
|  | 
 | ||||||
|  |             new_will[oid]=old_will[oid] | ||||||
|  |             new_will[oid].heirs = new_heirs | ||||||
|  |             new_will[oid].we = new_we | ||||||
|  |             print(f"found {oid}") | ||||||
|  | 
 | ||||||
|  |             continue | ||||||
|  |         else: | ||||||
|  |             print(f"not found {oid}") | ||||||
|  |             continue | ||||||
|  | 
 | ||||||
|  | def get_higher_input_for_tx(will): | ||||||
|  |     out = {} | ||||||
|  |     for wid in will: | ||||||
|  |         wtx = will[wid].tx | ||||||
|  |         found = False | ||||||
|  |         for inp in wtx.inputs(): | ||||||
|  |             if inp.prevout.txid.hex() in will: | ||||||
|  |                 found = True | ||||||
|  |                 break | ||||||
|  |         if not found: | ||||||
|  |             out[inp.prevout.to_str()] = inp | ||||||
|  |     return out | ||||||
|  | 
 | ||||||
|  | def invalidate_will(will,wallet,fees_per_byte): | ||||||
|  |     will_only_valid = only_valid_list(will) | ||||||
|  |     inputs = get_all_inputs(will_only_valid) | ||||||
|  |     utxos = wallet.get_utxos() | ||||||
|  |     filtered_inputs = [] | ||||||
|  |     prevout_to_spend = [] | ||||||
|  |     for prevout_str,ws in inputs.items():  | ||||||
|  |         for w in ws: | ||||||
|  |             if not w[0] in filtered_inputs:  | ||||||
|  |                 filtered_inputs.append(w[0]) | ||||||
|  |                 if not prevout_str in prevout_to_spend: | ||||||
|  |                     prevout_to_spend.append(prevout_str) | ||||||
|  |     balance = 0 | ||||||
|  |     utxo_to_spend = [] | ||||||
|  |     for utxo in utxos: | ||||||
|  |         utxo_str=utxo.prevout.to_str() | ||||||
|  |         if utxo_str in prevout_to_spend: | ||||||
|  |             balance += inputs[utxo_str][0][2].value_sats() | ||||||
|  |             utxo_to_spend.append(utxo) | ||||||
|  | 
 | ||||||
|  |     if len(utxo_to_spend) > 0:  | ||||||
|  |         change_addresses = wallet.get_change_addresses_for_new_transaction() | ||||||
|  |         out = PartialTxOutput.from_address_and_value(change_addresses[0], balance) | ||||||
|  |         out.is_change = True | ||||||
|  |         locktime = Util.get_current_height(wallet.network) | ||||||
|  |         tx = PartialTransaction.from_io(utxo_to_spend, [out], locktime=locktime, version=2) | ||||||
|  |         tx.set_rbf(True) | ||||||
|  |         fee=tx.estimated_size()*fees_per_byte | ||||||
|  |         if balance -fee >0: | ||||||
|  |             out = PartialTxOutput.from_address_and_value(change_addresses[0],balance - fee) | ||||||
|  |             tx = PartialTransaction.from_io(utxo_to_spend,[out], locktime=locktime, version=2) | ||||||
|  |             tx.set_rbf(True) | ||||||
|  |              | ||||||
|  |             _logger.debug(f"invalidation tx: {tx}") | ||||||
|  |             return tx | ||||||
|  | 
 | ||||||
|  |         else: | ||||||
|  |             _logger.debug("balance - fee <=0") | ||||||
|  |             pass | ||||||
|  |     else: | ||||||
|  |         _logger.debug("len utxo_to_spend <=0") | ||||||
|  |         pass | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def is_new(will): | ||||||
|  |     for wid,w in will.items(): | ||||||
|  |         if w.get_status('VALID') and not w.get_status('COMPLETE'): | ||||||
|  |             return True | ||||||
|  | 
 | ||||||
|  | def search_rai (all_inputs,all_utxos,will,wallet): | ||||||
|  |     will_only_valid = only_valid_or_replaced_list(will) | ||||||
|  |     for inp,ws in all_inputs.items(): | ||||||
|  |         inutxo = Util.in_utxo(inp,all_utxos) | ||||||
|  |         for w in ws: | ||||||
|  |             wi=w[1] | ||||||
|  |             if wi.get_status('VALID') or wi.get_status('CONFIRMED') or wi.get_status('PENDING'): | ||||||
|  |                 prevout_id=w[2].prevout.txid.hex() | ||||||
|  |                 if not inutxo: | ||||||
|  |                     if prevout_id in will: | ||||||
|  |                         wo=will[prevout_id] | ||||||
|  |                         if wo.get_status('REPLACED'): | ||||||
|  |                             wi.set_status('REPLACED',True) | ||||||
|  |                         if wo.get_status("INVALIDATED"): | ||||||
|  |                             wi.set_status('INVALIDATED',True) | ||||||
|  |                          | ||||||
|  |                     else: | ||||||
|  |                         if wallet.db.get_transaction(wi._id): | ||||||
|  |                             wi.set_status('CONFIRMED',True) | ||||||
|  |                         else: | ||||||
|  |                             wi.set_status('INVALIDATED',True) | ||||||
|  |                 #else: | ||||||
|  |                 #    if prevout_id in will: | ||||||
|  |                 #        wo = will[prevout_id] | ||||||
|  |                 #        ttx= wallet.db.get_transaction(prevout_id) | ||||||
|  |                 #        if ttx: | ||||||
|  |                 #            _logger.error("transaction in wallet should be early detected") | ||||||
|  |                 #            #wi.set_status('CONFIRMED',True) | ||||||
|  |                 #    #else: | ||||||
|  |                 #    #    _logger.error("transaction not in will or utxo") | ||||||
|  |                 #    #    wi.set_status('INVALIDATED',True) | ||||||
|  |      | ||||||
|  |                 for child in wi.search(all_inputs): | ||||||
|  |                     if child.tx.locktime < wi.tx.locktime: | ||||||
|  |                         _logger.debug("a child was found") | ||||||
|  |                         wi.set_status('REPLACED',True) | ||||||
|  |             else: | ||||||
|  |                 pass | ||||||
|  | 
 | ||||||
|  | def utxos_strs(utxos): | ||||||
|  |     return [Util.utxo_to_str(u) for u in utxos] | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def set_invalidate(wid,will=[]): | ||||||
|  |     will[wid].set_status("INVALIDATED",True) | ||||||
|  |     if will[wid].children: | ||||||
|  |         for c in self.children.items(): | ||||||
|  |             set_invalidate(c[0],will) | ||||||
|  | 
 | ||||||
|  | def check_tx_height(tx, wallet): | ||||||
|  |     info=wallet.get_tx_info(tx) | ||||||
|  |     return info.tx_mined_status.height | ||||||
|  | 
 | ||||||
|  | #check if transactions are stil valid tecnically valid | ||||||
|  | def check_invalidated(willtree,utxos_list,wallet): | ||||||
|  |     for wid,w in willtree.items(): | ||||||
|  |         if not w.father: | ||||||
|  |             for inp in w.tx.inputs(): | ||||||
|  |                 inp_str = Util.utxo_to_str(inp) | ||||||
|  |                 #print(utxos_list) | ||||||
|  |                 #print(inp_str) | ||||||
|  |                 #print(inp_str in utxos_list) | ||||||
|  |                 #print("notin: ",not inp_str in utxos_list) | ||||||
|  |                 if not inp_str in utxos_list: | ||||||
|  |                     #print("quindi qua non ci arrivo?") | ||||||
|  |                     if wallet: | ||||||
|  |                         height= check_tx_height(w.tx,wallet) | ||||||
|  | 
 | ||||||
|  |                         if height < 0: | ||||||
|  |                             #_logger.debug(f"heigth {height}") | ||||||
|  |                             set_invalidate(wid,willtree) | ||||||
|  |                         elif height == 0: | ||||||
|  |                             w.set_status("PENDING",True) | ||||||
|  |                         else: | ||||||
|  |                             w.set_status('CONFIRMED',True) | ||||||
|  | 
 | ||||||
|  | def reflect_to_children(treeitem): | ||||||
|  |     if not treeitem.get_status("VALID"): | ||||||
|  |         _logger.debug(f"{tree:item._id} status not valid looking for children") | ||||||
|  |         for child in treeitem.children: | ||||||
|  |             wc = willtree[child] | ||||||
|  |             if wc.get_status("VALID"): | ||||||
|  |                 if treeitem.get_status("INVALIDATED"): | ||||||
|  |                     wc.set_status("INVALIDATED",True) | ||||||
|  |                 if treeitem.get_status("REPLACED"): | ||||||
|  |                     wc.set_status("REPLACED",True) | ||||||
|  |                     if wc.children: | ||||||
|  |                         reflect_to_children(wc) | ||||||
|  | 
 | ||||||
|  | def check_amounts(heirs,willexecutors,all_utxos,timestamp_to_check,dust): | ||||||
|  |     fixed_heirs,fixed_amount,perc_heirs,perc_amount = heirs.fixed_percent_lists_amount(timestamp_to_check,dust,reverse=True) | ||||||
|  |     wallet_balance = 0 | ||||||
|  |     for utxo in all_utxos: | ||||||
|  |         wallet_balance += utxo.value_sats() | ||||||
|  | 
 | ||||||
|  |     if fixed_amount >= wallet_balance: | ||||||
|  |         raise FixedAmountException(f"Fixed amount({fixed_amount}) >= {wallet_balance}") | ||||||
|  |     if perc_amount != 100: | ||||||
|  |         raise PercAmountException(f"Perc amount({perc_amount}) =! 100%") | ||||||
|  | 
 | ||||||
|  |     for url,wex in willexecutors.items(): | ||||||
|  |         if Willexecutors.is_selected(wex): | ||||||
|  |             temp_balance = wallet_balance - int(wex['base_fee']) | ||||||
|  |             if fixed_amount >= temp_balance: | ||||||
|  |                 raise FixedAmountException(f"Willexecutor{url} excess base fee({wex['base_fee']}), {fixed_amount} >={temp_balance}") | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def check_will(will,all_utxos,wallet,block_to_check,timestamp_to_check): | ||||||
|  |     add_willtree(will) | ||||||
|  |     utxos_list= utxos_strs(all_utxos) | ||||||
|  | 
 | ||||||
|  |     check_invalidated(will,utxos_list,wallet) | ||||||
|  |     #from pprint import pprint | ||||||
|  |     #for wid,w in will.items(): | ||||||
|  |     #    pprint(w.to_dict()) | ||||||
|  | 
 | ||||||
|  |     all_inputs=get_all_inputs(will,only_valid = True) | ||||||
|  | 
 | ||||||
|  |     all_inputs_min_locktime = get_all_inputs_min_locktime(all_inputs) | ||||||
|  | 
 | ||||||
|  |     check_will_expired(all_inputs_min_locktime,block_to_check,timestamp_to_check) | ||||||
|  | 
 | ||||||
|  |     all_inputs=get_all_inputs(will,only_valid = True) | ||||||
|  |       | ||||||
|  |     search_rai(all_inputs,all_utxos,will,wallet) | ||||||
|  | 
 | ||||||
|  | def is_will_valid(will, block_to_check, timestamp_to_check, tx_fees, all_utxos,heirs={},willexecutors={},self_willexecutor=False, wallet=False, callback_not_valid_tx=None): | ||||||
|  | 
 | ||||||
|  |     check_will(will,all_utxos,wallet,block_to_check,timestamp_to_check) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     if heirs: | ||||||
|  |         if not check_willexecutors_and_heirs(will,heirs,willexecutors,self_willexecutor,timestamp_to_check,tx_fees): | ||||||
|  |             raise NotCompleteWillException() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     all_inputs=get_all_inputs(will,only_valid = True) | ||||||
|  | 
 | ||||||
|  |     _logger.info('check all utxo in wallet are spent') | ||||||
|  |     if all_inputs: | ||||||
|  |         for utxo in all_utxos: | ||||||
|  |             if utxo.value_sats() > 68 * tx_fees:  | ||||||
|  |                 if not Util.in_utxo(utxo,all_inputs.keys()): | ||||||
|  |                         _logger.info("utxo is not spent",utxo.to_json()) | ||||||
|  |                         _logger.debug(all_inputs.keys()) | ||||||
|  |                         raise NotCompleteWillException("Some utxo in the wallet is not included") | ||||||
|  | 
 | ||||||
|  |     _logger.info('will ok') | ||||||
|  |     return True | ||||||
|  | 
 | ||||||
|  | def check_will_expired(all_inputs_min_locktime,block_to_check,timestamp_to_check): | ||||||
|  |     _logger.info("check if some transaction is expired") | ||||||
|  |     for prevout_str, wid in all_inputs_min_locktime.items(): | ||||||
|  |         for w in wid:  | ||||||
|  |             if w[1].get_status('VALID'): | ||||||
|  |                 locktime = int(wid[0][1].tx.locktime) | ||||||
|  |                 if locktime <= NLOCKTIME_BLOCKHEIGHT_MAX: | ||||||
|  |                     if locktime < int(block_to_check): | ||||||
|  |                         raise WillExpiredException(f"Will Expired {wid[0][0]}: {locktime}<{block_to_check}") | ||||||
|  |                 else: | ||||||
|  |                     if locktime < int(timestamp_to_check): | ||||||
|  |                         raise WillExpiredException(f"Will Expired {wid[0][0]}: {locktime}<{timestamp_to_check}") | ||||||
|  |   | ||||||
|  | def check_all_input_spent_are_in_wallet(): | ||||||
|  |     _logger.info("check all input spent are in wallet or valid txs") | ||||||
|  |     for inp,ws in all_inputs.items(): | ||||||
|  |         if not Util.in_utxo(inp,all_utxos): | ||||||
|  |             for w in ws: | ||||||
|  |                 if w[1].get_status('VALID'): | ||||||
|  |                     prevout_id = w[2].prevout.txid.hex() | ||||||
|  |                     parentwill = will.get(prevout_id,False) | ||||||
|  |                     if not parentwill or not parentwill.get_status('VALID'): | ||||||
|  |                         w[1].set_status('INVALIDATED',True) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def only_valid_list(will): | ||||||
|  |     out={} | ||||||
|  |     for wid,w in will.items(): | ||||||
|  |         if w.get_status('VALID'): | ||||||
|  |             out[wid]=w | ||||||
|  |     return out | ||||||
|  | 
 | ||||||
|  | def only_valid_or_replaced_list(will): | ||||||
|  |     out=[] | ||||||
|  |     for wid,w in will.items(): | ||||||
|  |         wi = w | ||||||
|  |         if wi.get_status('VALID') or wi.get_status('REPLACED'): | ||||||
|  |             out.append(wid) | ||||||
|  |     return out | ||||||
|  | 
 | ||||||
|  | def check_willexecutors_and_heirs(will,heirs,willexecutors,self_willexecutor,check_date,tx_fees): | ||||||
|  |     _logger.debug("check willexecutors heirs") | ||||||
|  |     no_willexecutor = 0 | ||||||
|  |     willexecutors_found = {} | ||||||
|  |     heirs_found = {} | ||||||
|  |     will_only_valid = only_valid_list(will) | ||||||
|  |     if len(will_only_valid)<1: | ||||||
|  |         return False | ||||||
|  |     for wid in only_valid_list(will): | ||||||
|  |         w = will[wid] | ||||||
|  |         if w.tx_fees != tx_fees: | ||||||
|  |             #w.set_status('VALID',False) | ||||||
|  |             raise TxFeesChangedException(f"{tx_fees}:",w.tx_fees) | ||||||
|  |         for wheir in w.heirs: | ||||||
|  |             if not 'w!ll3x3c"' == wheir[:9]: | ||||||
|  |                 their = will[wid].heirs[wheir] | ||||||
|  |                 if heir := heirs.get(wheir,None): | ||||||
|  |              | ||||||
|  |                     if heir[0] == their[0] and heir[1] == their[1] and Util.parse_locktime_string(heir[2]) >= Util.parse_locktime_string(their[2]): | ||||||
|  |                         count = heirs_found.get(wheir,0) | ||||||
|  |                         heirs_found[wheir]=count + 1 | ||||||
|  |                 else: | ||||||
|  |                     _logger.debug("heir not present transaction is not valid:",wid,w) | ||||||
|  |                     continue | ||||||
|  |         if willexecutor := w.we: | ||||||
|  |             count = willexecutors_found.get(willexecutor['url'],0) | ||||||
|  |             if Util.cmp_willexecutor(willexecutor,willexecutors.get(willexecutor['url'],None)): | ||||||
|  |                 willexecutors_found[willexecutor['url']]=count+1 | ||||||
|  | 
 | ||||||
|  |         else: | ||||||
|  |             no_willexecutor += 1 | ||||||
|  |     count_heirs = 0 | ||||||
|  |     for h in heirs: | ||||||
|  |         if Util.parse_locktime_string(heirs[h][2])>=check_date: | ||||||
|  |             count_heirs +=1 | ||||||
|  |             if not h in heirs_found: | ||||||
|  |                 _logger.debug(f"heir: {h} not found") | ||||||
|  |                 raise HeirNotFoundException(h) | ||||||
|  |     if not count_heirs: | ||||||
|  |         raise NoHeirsException("there are not valid heirs") | ||||||
|  |     if self_willexecutor and no_willexecutor ==0: | ||||||
|  |         raise NoWillExecutorNotPresent("Backup tx") | ||||||
|  | 
 | ||||||
|  |     for url,we in willexecutors.items(): | ||||||
|  |         if Willexecutors.is_selected(we): | ||||||
|  |             if not url in willexecutors_found: | ||||||
|  |                 _logger.debug(f"will-executor: {url} not fount") | ||||||
|  |                 raise WillExecutorNotPresent(url) | ||||||
|  |     _logger.info("will is coherent with heirs and will-executors") | ||||||
|  |     return True | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class WillItem(Logger):  | ||||||
|  | 
 | ||||||
|  |     STATUS_DEFAULT = { | ||||||
|  |         'ANTICIPATED':  ['Anticipated', False], | ||||||
|  |         'BROADCASTED':  ['Broadcasted', False], | ||||||
|  |         'CHECKED':      ['Checked',     False], | ||||||
|  |         'CHECK_FAIL':   ['Check Failed',False], | ||||||
|  |         'COMPLETE':     ['Signed',      False], | ||||||
|  |         'CONFIRMED':    ['Confirmed',   False], | ||||||
|  |         'ERROR':        ['Error',       False], | ||||||
|  |         'EXPIRED':      ['Expired',     False], | ||||||
|  |         'EXPORTED':     ['Exported',    False], | ||||||
|  |         'IMPORTED':     ['Imported',    False], | ||||||
|  |         'INVALIDATED':  ['Invalidated', False], | ||||||
|  |         'PENDING':      ['Pending',     False], | ||||||
|  |         'PUSH_FAIL':    ['Push failed', False], | ||||||
|  |         'PUSHED':       ['Pushed',      False], | ||||||
|  |         'REPLACED':     ['Replaced',    False], | ||||||
|  |         'RESTORED':     ['Restored',    False], | ||||||
|  |         'VALID':        ['Valid',       True], | ||||||
|  |     } | ||||||
|  |     def set_status(self,status,value=True): | ||||||
|  |         _logger.debug("set status {} - {} {} -> {}".format(self._id,status,self.STATUS[status][1],value)) | ||||||
|  |         if self.STATUS[status][1] == bool(value): | ||||||
|  |             return None | ||||||
|  | 
 | ||||||
|  |         self.status += "." +("NOT " if not value else "" +  _(self.STATUS[status][0])) | ||||||
|  |         self.STATUS[status][1] = bool(value) | ||||||
|  |         if value: | ||||||
|  |             if status in ['INVALIDATED','REPLACED','CONFIRMED','PENDING']: | ||||||
|  |                 self.STATUS['VALID'][1] = False | ||||||
|  | 
 | ||||||
|  |             if status in ['CONFIRMED','PENDING']: | ||||||
|  |                 self.STATUS['INVALIDATED'][1] = False | ||||||
|  | 
 | ||||||
|  |             if status in ['PUSHED']: | ||||||
|  |                 self.STATUS['PUSH_FAIL'][1] = False | ||||||
|  |                 self.STATUS['CHECK_FAIL'][1] = False | ||||||
|  | 
 | ||||||
|  |             #if status in ['CHECK_FAIL']: | ||||||
|  |             #    self.STATUS['PUSHED'][1] = False | ||||||
|  | 
 | ||||||
|  |             if status in ['CHECKED']: | ||||||
|  |                 self.STATUS['PUSHED'][1] = True | ||||||
|  |                 self.STATUS['PUSH_FAIL'][1] = False | ||||||
|  | 
 | ||||||
|  |         return value | ||||||
|  | 
 | ||||||
|  |     def get_status(self,status): | ||||||
|  |         return self.STATUS[status][1] | ||||||
|  | 
 | ||||||
|  |     def __init__(self,w,_id=None,wallet=None):  | ||||||
|  |         if isinstance(w,WillItem,): | ||||||
|  |             self.__dict__ = w.__dict__.copy() | ||||||
|  |         else: | ||||||
|  |             self.tx = get_tx_from_any(w['tx']) | ||||||
|  |             self.heirs = w.get('heirs',None)  | ||||||
|  |             self.we = w.get('willexecutor',None)  | ||||||
|  |             self.status = w.get('status',None)  | ||||||
|  |             self.description = w.get('description',None)  | ||||||
|  |             self.time = w.get('time',None)  | ||||||
|  |             self.change = w.get('change',None)  | ||||||
|  |             self.tx_fees = w.get('tx_fees',0) | ||||||
|  |             self.father = w.get('Father',None) | ||||||
|  |             self.children = w.get('Children',None) | ||||||
|  |             self.STATUS = copy.deepcopy(WillItem.STATUS_DEFAULT) | ||||||
|  |             for s in self.STATUS: | ||||||
|  |                 self.STATUS[s][1]=w.get(s,WillItem.STATUS_DEFAULT[s][1]) | ||||||
|  |             if not _id: | ||||||
|  |                 self._id = self.tx.txid() | ||||||
|  |             else: | ||||||
|  |                 self._id = _id | ||||||
|  | 
 | ||||||
|  |             if not self._id: | ||||||
|  |                 self.status+="ERROR!!!" | ||||||
|  |                 self.valid = False | ||||||
|  | 
 | ||||||
|  |         if wallet: | ||||||
|  |             self.tx.add_info_from_wallet(wallet) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     def to_dict(self): | ||||||
|  |         out = { | ||||||
|  |             '_id':self._id, | ||||||
|  |             'tx':self.tx, | ||||||
|  |             'heirs':self.heirs, | ||||||
|  |             'willexecutor':self.we, | ||||||
|  |             'status':self.status, | ||||||
|  |             'description':self.description, | ||||||
|  |             'time':self.time, | ||||||
|  |             'change':self.change, | ||||||
|  |             'tx_fees':self.tx_fees | ||||||
|  |         } | ||||||
|  |         for key in self.STATUS: | ||||||
|  |             try: | ||||||
|  |                 out[key]=self.STATUS[key][1] | ||||||
|  |             except Exception as e: | ||||||
|  |                 _logger.error(f"{key},{self.STATUS[key]} {e}") | ||||||
|  | 
 | ||||||
|  |         return out | ||||||
|  | 
 | ||||||
|  |     def __repr__(self): | ||||||
|  |         return str(self) | ||||||
|  | 
 | ||||||
|  |     def __str__(self): | ||||||
|  |         return str(self.to_dict()) | ||||||
|  | 
 | ||||||
|  |     def set_anticipate(self, ow:'WillItem'): | ||||||
|  |         nl = min(ow.tx.locktime,check_anticipate(ow,self)) | ||||||
|  |         if int(nl) < self.tx.locktime: | ||||||
|  |             #_logger.debug("actually anticipating") | ||||||
|  |             self.tx.locktime = int(nl) | ||||||
|  |             return True | ||||||
|  |         else: | ||||||
|  |             #_logger.debug("keeping the same locktime") | ||||||
|  |             return False | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     def search_anticipate(self,all_inputs): | ||||||
|  |         anticipated = False | ||||||
|  |         for ow in self.search(all_inputs): | ||||||
|  |             if self.set_anticipate(ow): | ||||||
|  |                 anticipated = True | ||||||
|  |         return anticipated | ||||||
|  | 
 | ||||||
|  |     def search(self,all_inputs): | ||||||
|  |         for inp in self.tx.inputs(): | ||||||
|  |             prevout_str = inp.prevout.to_str() | ||||||
|  |             oinps = all_inputs.get(prevout_str,[]) | ||||||
|  |             for oinp in oinps: | ||||||
|  |                 ow=oinp[1] | ||||||
|  |                 if ow._id!=self._id: | ||||||
|  |                     yield ow | ||||||
|  | 
 | ||||||
|  |     def normalize_locktime(self,all_inputs): | ||||||
|  |         outputs = self.tx.outputs() | ||||||
|  |         for idx in range(0,len(outputs)): | ||||||
|  |             inps = all_inputs.get(f"{self._id}:{idx}",[]) | ||||||
|  |             _logger.debug("****check locktime***") | ||||||
|  |             for inp in inps: | ||||||
|  |                 if inp[0]!= self._id: | ||||||
|  |                     iw = inp[1] | ||||||
|  |                     self.set_anticipate(iw) | ||||||
|  | 
 | ||||||
|  |     def check_willexecutor(self): | ||||||
|  |         try: | ||||||
|  |             if resp:=Willexecutors.check_transaction(self._id,self.we['url']): | ||||||
|  |                 if 'tx' in resp and resp['tx']==str(self.tx): | ||||||
|  |                     self.set_status('PUSHED') | ||||||
|  |                     self.set_status('CHECKED') | ||||||
|  |                 else:    | ||||||
|  |                     self.set_status('CHECK_FAIL') | ||||||
|  |                     self.set_status('PUSHED',False) | ||||||
|  |                 return True | ||||||
|  |             else: | ||||||
|  |                 self.set_status('CHECK_FAIL') | ||||||
|  |                 self.set_status('PUSHED',False) | ||||||
|  |                 return False | ||||||
|  |         except Exception as e: | ||||||
|  |             _logger.error(f"exception checking transaction: {e}") | ||||||
|  |             self.set_status('CHECK_FAIL') | ||||||
|  |     def get_color(self): | ||||||
|  |         if self.get_status("INVALIDATED"): | ||||||
|  |             return "#f87838" | ||||||
|  |         elif self.get_status("REPLACED"): | ||||||
|  |             return "#ff97e9" | ||||||
|  |         elif self.get_status("CONFIRMED"): | ||||||
|  |             return "#bfbfbf" | ||||||
|  |         elif self.get_status("PENDING"): | ||||||
|  |             return "#ffce30" | ||||||
|  |         elif self.get_status("CHECK_FAIL") and not self.get_status("CHECKED"): | ||||||
|  |             return "#e83845" | ||||||
|  |         elif self.get_status("CHECKED"): | ||||||
|  |             return "#8afa6c" | ||||||
|  |         elif self.get_status("PUSH_FAIL"): | ||||||
|  |             return "#e83845" | ||||||
|  |         elif self.get_status("PUSHED"): | ||||||
|  |             return "#73f3c8" | ||||||
|  |         elif self.get_status("COMPLETE"): | ||||||
|  |             return "#2bc8ed" | ||||||
|  |         else: | ||||||
|  |             return "#ffffff" | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class WillExpiredException(Exception): | ||||||
|  |     pass | ||||||
|  | class NotCompleteWillException(Exception): | ||||||
|  |     pass | ||||||
|  | class HeirChangeException(NotCompleteWillException): | ||||||
|  |     pass | ||||||
|  | class TxFeesChangedException(NotCompleteWillException): | ||||||
|  |     pass | ||||||
|  | class HeirNotFoundException(NotCompleteWillException): | ||||||
|  |     pass | ||||||
|  | class WillexecutorChangeException(NotCompleteWillException): | ||||||
|  |     pass | ||||||
|  | class NoWillExecutorNotPresent(NotCompleteWillException): | ||||||
|  |     pass | ||||||
|  | class WillExecutorNotPresent(NotCompleteWillException): | ||||||
|  |     pass | ||||||
|  | class NoHeirsException(Exception): | ||||||
|  |     pass | ||||||
|  | class AmountException(Exception): | ||||||
|  |     pass | ||||||
|  | class PercAmountException(AmountException): | ||||||
|  |     pass | ||||||
|  | class FixedAmountException(AmountException): | ||||||
|  |     pass | ||||||
							
								
								
									
										203
									
								
								willexecutors.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										203
									
								
								willexecutors.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,203 @@ | |||||||
|  | import json | ||||||
|  | from datetime import datetime | ||||||
|  | from functools import partial | ||||||
|  | from aiohttp import ClientResponse | ||||||
|  | 
 | ||||||
|  | from electrum.network import Network | ||||||
|  | from electrum import constants | ||||||
|  | from electrum.logging import get_logger | ||||||
|  | from electrum.gui.qt.util import WaitingDialog | ||||||
|  | from electrum.i18n import _ | ||||||
|  | 
 | ||||||
|  | from .balqt.baldialog import BalWaitingDialog | ||||||
|  | from . import util as Util | ||||||
|  | 
 | ||||||
|  | DEFAULT_TIMEOUT = 5 | ||||||
|  | _logger = get_logger(__name__) | ||||||
|  | 
 | ||||||
|  | def get_willexecutors(bal_plugin, update = False,bal_window=False,force=False,task=True): | ||||||
|  |     willexecutors = bal_plugin.config_get(bal_plugin.WILLEXECUTORS) | ||||||
|  |     for w in willexecutors: | ||||||
|  |         initialize_willexecutor(willexecutors[w],w) | ||||||
|  | 
 | ||||||
|  |     bal=bal_plugin.DEFAULT_SETTINGS[bal_plugin.WILLEXECUTORS] | ||||||
|  |     for bal_url,bal_executor in bal.items(): | ||||||
|  |         if not bal_url in willexecutors: | ||||||
|  |             _logger.debug("replace bal") | ||||||
|  |             willexecutors[bal_url]=bal_executor | ||||||
|  |     if update: | ||||||
|  |         found = False | ||||||
|  |         for url,we in willexecutors.items(): | ||||||
|  |             if is_selected(we): | ||||||
|  |                 found = True | ||||||
|  |         if found or force: | ||||||
|  |             if bal_plugin.config_get(bal_plugin.PING_WILLEXECUTORS) or force: | ||||||
|  |                 ping_willexecutors = True | ||||||
|  |                 if bal_plugin.config_get(bal_plugin.ASK_PING_WILLEXECUTORS) and not force: | ||||||
|  |                     ping_willexecutors = bal_window.window.question(_("Contact willexecutors servers to update payment informations?")) | ||||||
|  |                 if ping_willexecutors: | ||||||
|  |                     if task: | ||||||
|  |                         bal_window.ping_willexecutors(willexecutors) | ||||||
|  |                     else: | ||||||
|  |                         bal_window.ping_willexecutors_task(willexecutors) | ||||||
|  |     return willexecutors | ||||||
|  | 
 | ||||||
|  | def is_selected(willexecutor,value=None): | ||||||
|  |     if not willexecutor: | ||||||
|  |         return False | ||||||
|  |     if not value is None: | ||||||
|  |         willexecutor['selected']=value | ||||||
|  |     try: | ||||||
|  |         return willexecutor['selected'] | ||||||
|  |     except: | ||||||
|  |         willexecutor['selected']=False | ||||||
|  |         return False | ||||||
|  | 
 | ||||||
|  | def get_willexecutor_transactions(will, force=False): | ||||||
|  |     willexecutors ={} | ||||||
|  |     for wid,willitem in will.items(): | ||||||
|  |         if willitem.get_status('VALID'): | ||||||
|  |             if willitem.get_status('COMPLETE'): | ||||||
|  |                 if not willitem.get_status('PUSHED') or force: | ||||||
|  |                     if willexecutor := willitem.we: | ||||||
|  |                         url=willexecutor['url'] | ||||||
|  |                         if  willexecutor and is_selected(willexecutor): | ||||||
|  |                             if not url in willexecutors: | ||||||
|  |                                 willexecutor['txs']="" | ||||||
|  |                                 willexecutor['txsids']=[] | ||||||
|  |                                 willexecutor['broadcast_status']= _("Waiting...") | ||||||
|  |                                 willexecutors[url]=willexecutor | ||||||
|  |                             willexecutors[url]['txs']+=str(willitem.tx)+"\n" | ||||||
|  |                             willexecutors[url]['txsids'].append(wid) | ||||||
|  | 
 | ||||||
|  |     return willexecutors | ||||||
|  | 
 | ||||||
|  | def only_selected_list(willexecutors): | ||||||
|  |     out = {} | ||||||
|  |     for url,v in willexectors.items(): | ||||||
|  |         if is_selected(willexecutor): | ||||||
|  |             out[url]=v | ||||||
|  | def push_transactions_to_willexecutors(will): | ||||||
|  |     willexecutors = get_transactions_to_be_pushed() | ||||||
|  |     for url in willexecutors: | ||||||
|  |         willexecutor = willexecutors[url] | ||||||
|  |         if is_selected(willexecutor): | ||||||
|  |             if 'txs' in willexecutor: | ||||||
|  |                 push_transactions_to_willexecutor(willexecutors[url]['txs'],url) | ||||||
|  | 
 | ||||||
|  | def send_request(method, url, data=None, *, timeout=10): | ||||||
|  |     network = Network.get_instance() | ||||||
|  |     if not network: | ||||||
|  |         raise ErrorConnectingServer('You are offline.') | ||||||
|  |     _logger.debug(f'<-- {method} {url} {data}') | ||||||
|  |     headers = {} | ||||||
|  |     headers['user-agent'] = 'BalPlugin' | ||||||
|  |     headers['Content-Type']='text/plain' | ||||||
|  | 
 | ||||||
|  |     try: | ||||||
|  |         if method == 'get': | ||||||
|  |             response = Network.send_http_on_proxy(method, url, | ||||||
|  |                                                   params=data, | ||||||
|  |                                                   headers=headers, | ||||||
|  |                                                   on_finish=handle_response, | ||||||
|  |                                                   timeout=timeout) | ||||||
|  |         elif method == 'post': | ||||||
|  |             response = Network.send_http_on_proxy(method, url, | ||||||
|  |                                                   body=data, | ||||||
|  |                                                   headers=headers, | ||||||
|  |                                                   on_finish=handle_response, | ||||||
|  |                                                   timeout=timeout) | ||||||
|  |         else: | ||||||
|  |             raise Exception(f"unexpected {method=!r}") | ||||||
|  |     except Exception as e: | ||||||
|  |         _logger.error(f"exception sending request {e}") | ||||||
|  |         raise e | ||||||
|  |     else: | ||||||
|  |         _logger.debug(f'--> {response}') | ||||||
|  |         return response | ||||||
|  | async def handle_response(resp:ClientResponse): | ||||||
|  |     r=await resp.text() | ||||||
|  |     try: | ||||||
|  |         r=json.loads(r) | ||||||
|  |         r['status'] = resp.status | ||||||
|  |         r['selected']=is_selected(willexecutor) | ||||||
|  |         r['url']=url | ||||||
|  |     except: | ||||||
|  |         pass     | ||||||
|  |     return r | ||||||
|  | 
 | ||||||
|  | class AlreadyPresentException(Exception): | ||||||
|  |     pass | ||||||
|  | def push_transactions_to_willexecutor(willexecutor): | ||||||
|  |     out=True | ||||||
|  |     try: | ||||||
|  |         _logger.debug(f"willexecutor['txs']") | ||||||
|  |         if w:=send_request('post', willexecutor['url']+"/"+constants.net.NET_NAME+"/pushtxs", data=willexecutor['txs'].encode('ascii')): | ||||||
|  |             willexecutor['broadcast_stauts'] = _("Success") | ||||||
|  |             _logger.debug(f"pushed: {w}") | ||||||
|  |             if w !='thx': | ||||||
|  |                 _logger.debug(f"error: {w}") | ||||||
|  |                 raise Exception(w) | ||||||
|  |         else: | ||||||
|  |             raise Exception("empty reply from:{willexecutor['url']}") | ||||||
|  |     except Exception as e: | ||||||
|  |         _logger.debug(f"error:{e}") | ||||||
|  |         if str(e) == "already present": | ||||||
|  |             raise AlreadyPresentException() | ||||||
|  |         out=False | ||||||
|  |         willexecutor['broadcast_stauts'] = _("Failed") | ||||||
|  | 
 | ||||||
|  |     return out | ||||||
|  | 
 | ||||||
|  | def ping_servers(willexecutors): | ||||||
|  |     for url,we in willexecutors.items(): | ||||||
|  |         get_info_task(url,we) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def get_info_task(url,willexecutor): | ||||||
|  |     w=None | ||||||
|  |     try: | ||||||
|  |         _logger.info("GETINFO_WILLEXECUTOR") | ||||||
|  |         _logger.debug(url) | ||||||
|  |         w = send_request('get',url+"/"+constants.net.NET_NAME+"/info") | ||||||
|  |         willexecutor['url']=url | ||||||
|  |         willexecutor['status'] = w['status'] | ||||||
|  |         willexecutor['base_fee'] = w['base_fee'] | ||||||
|  |         willexecutor['address'] = w['address'] | ||||||
|  |         if not willexecutor['info']: | ||||||
|  |             willexecutor['info'] = w['info'] | ||||||
|  |         _logger.debug(f"response_data {w['address']}") | ||||||
|  |     except Exception as e: | ||||||
|  |         _logger.error(f"error {e} contacting {url}: {w}") | ||||||
|  |         willexecutor['status']="KO" | ||||||
|  | 
 | ||||||
|  |     willexecutor['last_update'] = datetime.now().timestamp() | ||||||
|  |     return willexecutor | ||||||
|  | 
 | ||||||
|  | def initialize_willexecutor(willexecutor,url,status=None,selected=None): | ||||||
|  |     willexecutor['url']=url | ||||||
|  |     if not status is None: | ||||||
|  |         willexecutor['status'] = status | ||||||
|  |     willexecutor['selected'] = is_selected(willexecutor,selected) | ||||||
|  | 
 | ||||||
|  | def get_willexecutors_list_from_json(bal_plugin): | ||||||
|  |     try: | ||||||
|  |         with open("willexecutors.json") as f: | ||||||
|  |             willexecutors = json.load(f) | ||||||
|  |             for w in willexecutors: | ||||||
|  |                 willexecutor=willexecutors[w] | ||||||
|  |                 willexecutors.initialize_willexecutor(willexecutor,w,'New',False) | ||||||
|  |             bal_plugin.config.set_key(bal_plugin.WILLEXECUTORS,willexecutors,save=True) | ||||||
|  |             return h | ||||||
|  |     except Exception as e: | ||||||
|  |         _logger.error(f"errore aprendo willexecutors.json: {e}") | ||||||
|  |         return {} | ||||||
|  | 
 | ||||||
|  | def check_transaction(txid,url): | ||||||
|  |     _logger.debug(f"{url}:{txid}") | ||||||
|  |     try: | ||||||
|  |         w = send_request('post',url+"/searchtx",data=txid.encode('ascii')) | ||||||
|  |         return w | ||||||
|  |     except Exception as e: | ||||||
|  |         _logger.error(f"error contacting {url} for checking txs {e}") | ||||||
|  |         raise e | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 bitcoinafterlife
						bitcoinafterlife