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