From 4d021e1d82e686c97645331dde711d13e4bfa548 Mon Sep 17 00:00:00 2001 From: bitcoinafterlife Date: Fri, 4 Jul 2025 13:22:05 -0400 Subject: [PATCH] download welist --- bal.py | 14 +- heirs.py | 7 +- manifest.json | 3 +- qt.py | 2140 ++++++++++++++++++++++++++++++++++++++++++---- util.py | 809 +++++++++--------- will.py | 1060 +++++++++++------------ willexecutors.py | 383 +++++---- 7 files changed, 3095 insertions(+), 1321 deletions(-) diff --git a/bal.py b/bal.py index b9abd54..62a5da2 100644 --- a/bal.py +++ b/bal.py @@ -1,15 +1,10 @@ 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) @@ -17,11 +12,10 @@ 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 @@ -69,7 +63,7 @@ class BalPlugin(BasePlugin): HIDE_INVALIDATED:True, ALLOW_REPUSH: False, WILLEXECUTORS: { - 'http://bitcoin-after.life:9137': { + 'https://bitcoin-after.life:9137': { "base_fee": 100000, "status": "New", "info":"Bitcoin After Life Will Executor", @@ -102,10 +96,8 @@ class BalPlugin(BasePlugin): def config_get(self,key): v = self.config.get(key,None) - print("config get",key,v) if v is None: self.config.set_key(key,self.DEFAULT_SETTINGS[key]) - print("config setkey",key) v = self.DEFAULT_SETTINGS[key] return v diff --git a/heirs.py b/heirs.py index 2523ee2..3a86be4 100644 --- a/heirs.py +++ b/heirs.py @@ -15,8 +15,8 @@ import datetime import urllib.request import urllib.parse from .bal import BalPlugin -from . import util as Util -from . import willexecutors as Willexecutors +from .util import Util +from .willexecutors import Willexecutors if TYPE_CHECKING: from .wallet_db import WalletDB from .simple_config import SimpleConfig @@ -38,6 +38,7 @@ def reduce_outputs(in_amount, out_amount, fee, outputs): 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 @@ -57,7 +58,7 @@ def get_current_height(network:'Network'): # discourage "fee sniping" height = min(chain_height, server_height) return height - +""" diff --git a/manifest.json b/manifest.json index b1061fc..16ca7ba 100644 --- a/manifest.json +++ b/manifest.json @@ -3,5 +3,6 @@ "fullname": "Bitcoin After Life", "description": "Provides free and decentralized inheritance support", "author":"Svātantrya", - "available_for": ["qt"] + "available_for": ["qt"], + "icon":"icons/bal32x32.png" } diff --git a/qt.py b/qt.py index adf081d..8989342 100644 --- a/qt.py +++ b/qt.py @@ -7,68 +7,334 @@ Bitcoin after life ''' + + + + +import copy +from datetime import datetime +from decimal import Decimal +import enum +from functools import partial +import json import os import random +import sys 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 +from typing import ( + TYPE_CHECKING, + Callable, + Optional, + List, + Union, + Tuple, + Mapping,Any) 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) +try: + QT_VERSION=sys._GUI_QT_VERSION +except: + QT_VERSION=6 + +if 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) + from PyQt5.QtCore import ( + QPersistentModelIndex, + QModelIndex, + Qt, + QRectF, + QRect, + QSizeF, + QUrl, + QPoint, + QSize, + QDateTime, + pyqtProperty, + pyqtSignal, + pyqtSlot, + QObject, + QEventLoop, + pyqtSignal) + from PyQt5.QtGui import ( + QStandardItemModel, + QStandardItem, + QPalette, + QColor, + QPixmap, + QImage, + QBitmap, + QPainter, + QFontDatabase, + QPen, + QFont, + QColor, + QDesktopServices, + qRgba, + QPainterPath, + QPalette, + QPixmap, + QImage, + QBitmap, + QPainter, + QFontDatabase, + QPen, + QFont, + QIcon, + QColor, + QDesktopServices, + qRgba, + QPainterPath, + QPalette) + from PyQt5.QtWidgets import ( + QDialog, + QVBoxLayout, + QHBoxLayout, + QPushButton, + QLabel, + QMenu, + QDialog, + QVBoxLayout, + QHBoxLayout, + QPushButton, + QLabel, + QWidget, + QScrollArea, + QAbstractItemView, + QWidget, + QDateTimeEdit, + QLineEdit, + QStyle, + QStyleOptionFrame, + QSizePolicy, + QCheckBox, + QGridLayout, + QHBoxLayout, + QLabel, + QLineEdit, + QMenu, + QMenuBar, + QPushButton, + QScrollArea, + QSpacerItem, + QSizePolicy, + QSpinBox, + QVBoxLayout, + QWidget, + QStyle, + QStyleOptionFrame, + QComboBox, + QHBoxLayout, + ) +else: #QT6 + from PyQt6.QtCore import ( + Qt, + QDateTime, + QPersistentModelIndex, + QModelIndex, + pyqtProperty, + pyqtSignal, + pyqtSlot, + QObject, + pyqtSignal, + QSize, + Qt, + QRectF, + QRect, + QSizeF, + QUrl, + QPoint, + QSize) + from PyQt6.QtGui import ( + QStandardItemModel, + QStandardItem, + QPalette, + QColor, + QPixmap, + QImage, + QBitmap, + QPainter, + QFontDatabase, + QPen, + QFont, + QColor, + QDesktopServices, + qRgba, + QPainterPath, + QPalette, + QPainter, + QPixmap, + QImage, + QBitmap, + QPainter, + QFontDatabase, + QPen, + QFont, + QIcon, + QColor, + QDesktopServices, + qRgba, + QPainterPath, + QPalette) + + from PyQt6.QtWidgets import ( + QDialog, + QVBoxLayout, + QHBoxLayout, + QPushButton, + QLabel, + QMenu, + QAbstractItemView, + QWidget, + QDialog, + QVBoxLayout, + QHBoxLayout, + QPushButton, + QLabel, + QWidget, + QScrollArea, + QDateTimeEdit, + QLabel, + QVBoxLayout, + QCheckBox, + QWidget, + QLabel, + QVBoxLayout, + QCheckBox, + QLineEdit, + QStyle, + QStyleOptionFrame, + QSizePolicy, + QGridLayout, + QVBoxLayout, + QHBoxLayout, + QLabel, + QPushButton, + QLineEdit, + QCheckBox, + QSpinBox, + QMenuBar, + QMenu, + QLineEdit, + QScrollArea, + QWidget, + QSpacerItem, + QComboBox, + QSizePolicy) + + + + +from electrum import json_db +from electrum import constants +from electrum.bitcoin import ( + is_address, + NLOCKTIME_MIN, + NLOCKTIME_MAX, + NLOCKTIME_BLOCKHEIGHT_MAX) +from electrum.gui.qt.amountedit import ( + BTCAmountEdit, + char_width_in_lineedit, + ColorScheme) + +if TYPE_CHECKING: + from electrum.gui.qt.main_window import ElectrumWindow + +from electrum.gui.qt.util import ( + Buttons, + read_QIcon, + import_meta_gui, + export_meta_gui, + MessageBoxMixin, + Buttons, + read_QIcon, + export_meta_gui, + MessageBoxMixin, + char_width_in_lineedit, + ColorScheme, + Buttons, + CancelButton, + char_width_in_lineedit, + CloseButton, + EnterButton, + HelpButton, + MessageBoxMixin, + OkButton, + TaskThread, + WindowModalDialog, + WWLabel, + ) +from electrum.gui.qt.main_window import StatusBarButton +from electrum.gui.qt.my_treeview import ( + MyTreeView, + MySortModel) +from electrum.gui.qt.transaction_dialog import TxDialog +from electrum.gui.qt.password_dialog import PasswordDialog +from electrum.gui.qt.qrtextedit import ScanQRTextEdit +from electrum.i18n import _ +from electrum.logging import get_logger,Logger +from electrum.json_db import StoredDict +from electrum.network import ( + Network, + TxBroadcastError, + BestEffortRequestFailed + ) +from electrum.plugin import ( + hook, + run_hook) +from electrum.transaction import ( + Transaction, + tx_from_any) +from electrum.util import ( + write_json_file, + read_json_file, + make_dir, + InvalidPassword, + UserCancelled, + resource_path, + write_json_file, + read_json_file, + FileImportFailed, + bfh, + read_json_file, + write_json_file, + decimal_point_to_base_unit_name, + FileImportFailed, + DECIMAL_POINT, + FEERATE_PRECISION, + quantize_feerate, + UI_UNIT_NAME_FEERATE_SAT_PER_VBYTE, + FileExportFailed) + + +from .bal import BalPlugin +from .bal_resources import ( + DEFAULT_ICON, + icon_path) +from .heirs import Heirs +from .util import Util +from .will import ( + Will, + WillItem, + NoHeirsException, + NoWillExecutorNotPresent, + NotCompleteWillException, + AmountException, + HeirNotFoundException, + HeirChangeException, + WillexecutorChangeException, + WillExecutorNotPresent, + TxFeesChangedException, + WillExpiredException) + +from .willexecutors import Willexecutors +_logger = get_logger(__name__) + - 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): @@ -81,29 +347,22 @@ class Plugin(BalPlugin,Logger): @hook def init_qt(self,gui_object): - print("********************************************************************************************************************") self.logger.info("HOOK init qt") - print("logger") try: self.gui_object=gui_object - print(dir(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) - print("windows.winid",window.winid) self.bal_windows[window.winId]= w for child in window.children(): if isinstance(child,QMenuBar): - print("found menubar") for menu_child in child.children(): if isinstance(menu_child,QMenu): - print("found qmenu") try: if menu_child.title()==_("&Tools"): - print("found tools") w.init_menubar_tools(menu_child) except Exception as e: @@ -120,7 +379,7 @@ class Plugin(BalPlugin,Logger): 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"), + b = StatusBarButton(read_QIcon('bal32x32.png'), "Bal "+_("Bitcoin After Life"), partial(self.setup_dialog, sb), sb.height()) sb.addPermanentWidget(b) @@ -134,12 +393,12 @@ class Plugin(BalPlugin,Logger): def load_wallet(self,wallet, main_window): self.logger.info("HOOK load wallet") w = self.get_window(main_window) - print(dir(w)) 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(): @@ -152,7 +411,7 @@ class Plugin(BalPlugin,Logger): w=BalWindow(self,window) self.bal_windows[window.winId]=w return w - + def requires_settings(self): return True @@ -178,87 +437,26 @@ class Plugin(BalPlugin,Logger): return self.extension = bool(keystore.get_passphrase(password)) return keystore.get_seed(password) + def settings_dialog(self,window,wallet): d = BalDialog(window, self.get_window_title("Settings")) d.setMinimumSize(100, 200) - qicon=qt_resources.read_QPixmap("bal32x32.png") + qicon=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.config_get(BalPlugin.LOCKTIME_TIME))) - #def on_heir_locktime_time(): - # value = heir_locktime_time.value() - # self.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.config_get(BalPlugin.LOCKTIMEDELTA_TIME))) - #def on_heir_locktime_time(): - #value = heir_locktime_time.value - #self.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.config_get(BalPlugin.LOCKTIME_BLOCKS))) - #def on_heir_locktime_blocks(): - #value = heir_locktime_blocks.value() - #self.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.config_get(BalPlugin.LOCKTIMEDELTA_BLOCKS))) - #def on_heir_locktimedelta_blocks(): - #value = heir_locktimedelta_blocks.value() - #self.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.config_get(BalPlugin.TX_FEES))) - #def on_heir_tx_fees(): - #value = heir_tx_fees.value() - #self.config.set_key(BalPlugin.TX_FEES,value,save=True) - #heir_tx_fees.valueChanged.connect(on_heir_tx_fees) - #heir_broadcast = bal_checkbox(self, BalPlugin.BROADCAST) - #heir_ask_broadcast = bal_checkbox(self, BalPlugin.ASK_BROADCAST) - #heir_invalidate = bal_checkbox(self, BalPlugin.INVALIDATE) - #heir_ask_invalidate = bal_checkbox(self, BalPlugin.ASK_INVALIDATE) - #heir_preview = bal_checkbox(self, BalPlugin.PREVIEW) heir_ping_willexecutors = bal_checkbox(self, BalPlugin.PING_WILLEXECUTORS) heir_ask_ping_willexecutors = bal_checkbox(self, BalPlugin.ASK_PING_WILLEXECUTORS) - #print("setkey broadcast") - #self.config.set_key(BalPlugin.BROADCAST,True) heir_no_willexecutor = bal_checkbox(self, BalPlugin.NO_WILLEXECUTOR) heir_hide_replaced = bal_checkbox(self,BalPlugin.HIDE_REPLACED,self) heir_hide_invalidated = bal_checkbox(self,BalPlugin.HIDE_INVALIDATED,self) - #heir_allow_repush = bal_checkbox(self,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") @@ -266,8 +464,6 @@ class Plugin(BalPlugin,Logger): 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: @@ -279,7 +475,6 @@ class Plugin(BalPlugin,Logger): def broadcast_transactions(self,force): for k,w in self.bal_windows.items(): - print(dir(w)) w.broadcast_transactions(force) def update_all(self): @@ -300,7 +495,6 @@ class shown_cv(): 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 = {} @@ -336,9 +530,8 @@ class BalWindow(Logger): - print("add tab heir",self.heirs_tab); - 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")) + add_optional_tab(self.window.tabs, self.heirs_tab, read_QIcon("heir.png"), _("&Heirs")) + add_optional_tab(self.window.tabs, self.will_tab, read_QIcon("will.png"), _("&Will")) tools_menu.addSeparator() self.tools_menu.willexecutors_action = tools_menu.addAction(_("&Will-Executors"), self.show_willexecutor_dialog) self.window.view_menu.addSeparator() @@ -348,7 +541,7 @@ class BalWindow(Logger): def load_willitems(self): self.willitems={} for wid,w in self.will.items(): - self.willitems[wid]=Will.WillItem(w,wallet=self.wallet) + self.willitems[wid]=WillItem(w,wallet=self.wallet) if self.willitems: self.will_list.will=self.willitems self.will_list.update_will(self.willitems) @@ -397,7 +590,6 @@ class BalWindow(Logger): def create_heirs_tab(self): self.heir_list = l = HeirList(self) - print("heir_list",l) tab = self.window.create_list_tab(l) tab.is_shown_cv = shown_cv(True) return tab @@ -409,7 +601,7 @@ class BalWindow(Logger): return tab def new_heir_dialog(self): - d = BalDialog(self.window, self.get_window_title("New heir")) + d = BalDialog(self.window, self.bal_plugin.get_window_title("New heir")) vbox = QVBoxLayout(d) grid = QGridLayout() @@ -429,22 +621,10 @@ class BalWindow(Logger): 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(): @@ -518,7 +698,7 @@ class BalWindow(Logger): if Willexecutors.is_selected(w): f=True if not f: - raise Will.NoWillExecutorNotPresent("No Will-Executor or backup transaction selected") + raise 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() @@ -537,7 +717,7 @@ class BalWindow(Logger): tx['time'] = creation_time tx['heirs'] = copy.deepcopy(txs[txid].heirs) tx['txchildren'] = [] - will[txid]=Will.WillItem(tx,_id=txid,wallet=self.wallet) + will[txid]=WillItem(tx,_id=txid,wallet=self.wallet) self.update_will(will) except Exception as e: raise e @@ -565,7 +745,7 @@ class BalWindow(Logger): def init_class_variables(self): if not self.heirs: - raise Will.NoHeirsException() + raise NoHeirsException() return try: self.date_to_check = Util.parse_locktime_string(self.will_settings['threshold']) @@ -592,7 +772,7 @@ class BalWindow(Logger): 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: + except 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: @@ -609,23 +789,23 @@ class BalWindow(Logger): try: self.check_will() - except Will.WillExpiredException as e: + except WillExpiredException as e: self.invalidate_will() return - except Will.NoHeirsException: + except NoHeirsException: return - except Will.NotCompleteWillException as e: + except NotCompleteWillException as e: self.logger.info("{}:{}".format(type(e),e)) message = False - if isinstance(e,Will.HeirChangeException): + if isinstance(e,HeirChangeException): message ="Heirs changed:" - elif isinstance(e,Will.WillExecutorNotPresent): + elif isinstance(e,WillExecutorNotPresent): message = "Will-Executor not present:" - elif isinstance(e,Will.WillexecutorChangeException): + elif isinstance(e,WillexecutorChangeException): message = "Will-Executor changed" - elif isinstance(e,Will.TxFeesChangedException): + elif isinstance(e,TxFeesChangedException): message = "Txfees are changed" - elif isinstance(e,Will.HeirNotFoundException): + elif isinstance(e,HeirNotFoundException): message = "Heir not found" if message: @@ -638,9 +818,9 @@ class BalWindow(Logger): self.check_will() for wid,w in self.willitems.items(): self.wallet.set_label(wid,"BAL Transaction") - except Will.WillExpiredException as e: + except WillExpiredException as e: self.invalidate_will() - except Will.NotCompleteWillException as e: + except NotCompleteWillException as e: self.show_error("Error:{}\n {}".format(str(e),_("Please, check your heirs, locktime and threshold!"))) self.window.history_list.update() @@ -666,7 +846,7 @@ class BalWindow(Logger): external_keypairs=external_keypairs, payment_identifier=payment_identifier, ) - d.setWindowIcon(qt_resources.read_QIcon("bal32x32.png")) + d.setWindowIcon(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)) @@ -967,3 +1147,1637 @@ 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) + + +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_date_e = LockTimeDateEdit(self,time_edit = self) + self.editors = [self.locktime_raw_e, self.locktime_date_e] + + self.combo = QComboBox() + options = [_("Raw"),_("Date")] + self.option_index_to_editor_map = { + 0: self.locktime_raw_e, + 1: 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_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() + + 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)) + +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) + + +_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+='%' + + + self.setText(s) + self.setModified(self.hasFocus()) + self.setCursorPosition(pos) + + 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") + + + +class BalDialog(WindowModalDialog): + + def __init__(self,parent,title=None, icon = 'bal32x32.png'): + self.parent=parent + WindowModalDialog.__init__(self,self.parent,title) + self.setWindowIcon(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)) + def on_check(v): + plugin.config.set_key(variable, v == 2) + plugin.config_get(variable) + self.stateChanged.connect(on_check) + + + + + +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.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 + + 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 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 AmountException: + self.msg_edit_row(''+_("In the inheritance process, the entire wallet will always be fully emptied. Your settings require an adjustment of the amounts"+"")) + + self.msg_set_checking() + have_to_build=False + try: + self.bal_window.check_will() + self.msg_set_checking('Ok') + except 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 NoHeirsException: + self.msg_set_checking("No Heirs") + except NotCompleteWillException as e: + message = False + have_to_build=True + if isinstance(e,HeirChangeException): + message ="Heirs changed:" + elif isinstance(e,WillExecutorNotPresent): + message = "Will-Executor not present" + elif isinstance(e,WillexecutorChangeException): + message = "Will-Executor changed" + elif isinstance(e,TxFeesChangedException): + message = "Txfees are changed" + elif isinstance(e,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)) + + 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) + _logger.debug("have to sign {}".format(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) + + + 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}") + + 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): + 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 + + + +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'), + } + 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.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 == 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: + prior_name = (edit_key,)+prior_name[:col-1]+(text,)+prior_name[col:] + + try: + self.bal_window.set_heir(prior_name) + except Exception as e: + pass + + try: + self.bal_window.set_heir((edit_key,)+original) + except Exception as e: + 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): + 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(): + 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) + + 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.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) + + 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): + return self.get_role_data_from_coordinate(row, col, role=self.ROLE_HEIR_KEY+col+1) + + 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()) + + 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" + +" - 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" + +" - 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() + + + + +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))) + + 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(_("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() + +def read_QIcon(icon_basename: str=DEFAULT_ICON) -> QIcon: + return QIcon(icon_path(icon_basename)) +def read_QPixmap(icon_basename: str=DEFAULT_ICON) -> QPixmap: + return QPixmap(icon_path(icon_basename)) + + +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) + b = QPushButton(_('Invalidate')) + b.clicked.connect(bal_window.invalidate_will) + hlayout.addWidget(b) + self.vlayout.addWidget(w) + + self.paint_scroll_area() + 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 = 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 = ""+_(str(title)) + f":\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 + 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("Heirs:")) + 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(_("Willexecutor: 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: +class Util: + def locktime_to_str(locktime): try: - return int(float(amount)*pow(10,decimal_point)) - except: - return 0 + locktime=int(locktime) + if locktime > LOCKTIME_THRESHOLD: + dt = datetime.fromtimestamp(locktime).isoformat() + return dt -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)) + except Exception as e: + pass + return str(locktime) -def is_perc(value): + def str_to_locktime(locktime): + try: + if locktime[-1] in ('y','d','b'): + return locktime + else: return int(locktime) + except Exception as e: + pass + 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 + 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 = Util.get_current_height(w.network) + locktime+=int(height) + return int(locktime) + except Exception as e: + pass + 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 Util.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 Util.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]: + def cmp_array(heira,heirb): + try: + if not len(heira) == len(heirb): 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']: + for h in range(0,len(heira)): + if not heira[h] == heirb[h]: + return False 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]: + except: 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: + 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): + for h,v in heirs.items(): found = False - for heirb in heirsb: - if cmp_heir_by_values(heirsa[heira],heirsb[heirb],values): - found=True + for val in values: + if val in v and v[val] != heir[val]: + found = True + if not found: - #print(f"not_found {heira}--{heirsa[heira]}") + return h + return False + + def cmp_heir_by_values(heira,heirb,values): + for v in values: + if heira[v] != heirb[v]: 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 + + 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 Util.cmp_heir_by_values(heirsa[heira],heirsb[heirb],values): + found=True + if not found: + return False if reverse: - return cmp_heirs(heirsb,heirsa,cmp_function,False) + return Util.cmp_heirs_by_values(heirsb,heirsa,values,exclude_willexecutors=exclude_willexecutors,reverse=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 + 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 Util.search_heir_by_values(heirsb,heirsa[heir],[0,3]): + return False + if reverse: + return Util.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 Util.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 Util.cmp_output(outputa,willexecutor_output): + if not Util.in_output(outputa,outputsb): + return False + return True + + def cmp_txs(txa,txb): + if not Util.cmp_inputs(txa.inputs(),txb.inputs()): + return False + if not Util.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 + + for outa in outputsa: + same_amount,same_address = Util.in_output(outa,txb.outputs()) + if not (same_amount or same_address): + return False + if same_amount and same_address: + value_amount+=outa.value + if same_amount: + pass + if same_address: pass - #print("NOT SAME_:",out.address,s_o.address) - if len(same_amount)>0: - return True, False - else:return False, False + return value_amount -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 + + 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 = Util.str_to_locktime(strlocktimea) + intlocktimeb = Util.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_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 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: + l=Util.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 Util.get_lowest_locktimes(Util.get_locktimes(will)) + + def search_willtx_per_io(will,tx): + for wid, w in will.items(): + if Util.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=Util.utxo_to_str(utxoa) + utxob=Util.utxo_to_str(utxob) + if utxoa == utxob: + return True + else: + return False + + def in_utxo(utxo, utxos): + for s_u in utxos: + if Util.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 Util.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 print_var(var,name = "",veryverbose=False): - print(f"---{name}---") - if not var is None: + 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: + return True, True + else: + pass + + 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}---") + Util.print_var(utxo,name) + Util.print_prevout(utxo.prevout,name) + Util.print_var(utxo.script_sig,f"{name}-script-sig") + Util.print_var(utxo.witness,f"{name}-witness") + 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}---") + Util.print_var(prevout,f"{name}-prevout") + Util.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: - 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))) + 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 + def copy(dicto,dictfrom): + for k,v in dictfrom.items(): + dicto[k]=v diff --git a/will.py b/will.py index b651806..1cfdea8 100644 --- a/will.py +++ b/will.py @@ -1,7 +1,7 @@ import copy -from . import willexecutors as Willexecutors -from . import util as Util +from .willexecutors import Willexecutors +from .util import Util from electrum.i18n import _ @@ -16,579 +16,551 @@ 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 +class Will: + 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 + #build a tree with parent transactions + def add_willtree(will): + for willid in will: + will[willid].children = Will.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) - + #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 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 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()) + def get_tx_from_any(x): + try: + a=str(x) + return tx_from_any(a) - _logger.error("txid is none") - will[wid].set_status('ERROR',True) - errors[wid]=will[wid] - continue + except Exception as e: + raise e - if txid != wid: - outputs = will[wid].tx.outputs() - ow=will[wid] - ow.normalize_locktime(others_inputs) - will[wid]=ow.to_dict() + return x - for i in range(0,len(outputs)): - change_input(will,wid,i,outputs[i],others_inputs,to_delete,to_add) + def add_info_from_will(will,wid,wallet): + if isinstance(will[wid].tx,str): + will[wid].tx = Will.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 - to_delete.append(wid) - to_add[ow.tx.txid()]=ow.to_dict() + def normalize_will(will,wallet = None,others_inputs = {}): + to_delete = [] + to_add = {} + #add info from wallet + for wid in will: + Will.add_info_from_will(will,wid,wallet) + errors ={} + for wid in will: - for eid,err in errors.items(): - new_txid = err.tx.txid() + txid = will[wid].tx.txid() - for k,w in to_add.items(): - will[k] = w + 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 - for wid in to_delete: - if wid in will: - del will[wid] + if txid != wid: + outputs = will[wid].tx.outputs() + ow=will[wid] + ow.normalize_locktime(others_inputs) + will[wid]=ow.to_dict() -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 + for i in range(0,len(outputs)): + Will.change_input(will,wid,i,outputs[i],others_inputs,to_delete,to_add) -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): + 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): + if nw.we and ow.we: + if ow.we['url'] == nw.we['url']: + if int(ow.we['base_fee'])>int(nw.we['base_fee']): return anticipate else: - print("keep the same") - #_logger.debug("ow,base fee > nw.base_fee") - ow.tx.locktime + if int(ow.tx_fees) != int(nw.tx_fees): + return anticipate + else: + ow.tx.locktime + else: + 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 + 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 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: + 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]=Will.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)): + Will.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 = Will.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)): + Will.change_input(will,nid,i,outputs[i],new_inputs,to_delete,to_append) + + + for w in to_delete: + try: + del will[w] + except: pass - -def utxos_strs(utxos): - return [Util.utxo_to_str(u) for u in utxos] + for k,w in to_append.items(): + will[k]=w + if redo: + Will.search_anticipate_rec(will,old_inputs) -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 update_will(old_will,new_will): + all_old_inputs = Will.get_all_inputs(old_will,only_valid=True) + all_inputs_min_locktime = Will.get_all_inputs_min_locktime(all_old_inputs) + all_new_inputs = Will.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. + Will.search_anticipate_rec(new_will,all_old_inputs) -def check_tx_height(tx, wallet): - info=wallet.get_tx_info(tx) - return info.tx_mined_status.height + other_inputs = Will.get_all_inputs(old_will,{}) + try: + Will.normalize_will(new_will,others_inputs=other_inputs) + except Exception as e: + raise e + -#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) + for oid in Will.only_valid(old_will): + if oid in new_will: + new_heirs = new_will[oid].heirs + new_we = new_will[oid].we - 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) + new_will[oid]=old_will[oid] + new_will[oid].heirs = new_heirs + new_will[oid].we = new_we -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) + continue + else: + continue -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() + 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 - 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): + def invalidate_will(will,wallet,fees_per_byte): + will_only_valid = Will.only_valid_list(will) + inputs = Will.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 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) + 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 -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: + _logger.debug("balance - fee <=0") + pass 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") + _logger.debug("len utxo_to_spend <=0") + pass - 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 + + 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 = Will.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) + + 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(): + Will.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) + if not inp_str in utxos_list: + if wallet: + height= Will.check_tx_height(w.tx,wallet) + + if height < 0: + Will.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: + Will.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): + Will.add_willtree(will) + utxos_list= Will.utxos_strs(all_utxos) + + Will.check_invalidated(will,utxos_list,wallet) + + all_inputs=Will.get_all_inputs(will,only_valid = True) + + all_inputs_min_locktime = Will.get_all_inputs_min_locktime(all_inputs) + + Will.check_will_expired(all_inputs_min_locktime,block_to_check,timestamp_to_check) + + all_inputs=Will.get_all_inputs(will,only_valid = True) + + Will.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): + + Will.check_will(will,all_utxos,wallet,block_to_check,timestamp_to_check) + + + if heirs: + if not Will.check_willexecutors_and_heirs(will,heirs,willexecutors,self_willexecutor,timestamp_to_check,tx_fees): + raise NotCompleteWillException() + + + all_inputs=Will.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 = Will.only_valid_list(will) + if len(will_only_valid)<1: + return False + for wid in Will.only_valid_list(will): + w = will[wid] + if w.tx_fees != tx_fees: + 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): @@ -630,8 +602,6 @@ class WillItem(Logger): 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 @@ -646,7 +616,7 @@ class WillItem(Logger): if isinstance(w,WillItem,): self.__dict__ = w.__dict__.copy() else: - self.tx = get_tx_from_any(w['tx']) + self.tx = Will.get_tx_from_any(w['tx']) self.heirs = w.get('heirs',None) self.we = w.get('willexecutor',None) self.status = w.get('status',None) @@ -700,13 +670,11 @@ class WillItem(Logger): return str(self.to_dict()) def set_anticipate(self, ow:'WillItem'): - nl = min(ow.tx.locktime,check_anticipate(ow,self)) + nl = min(ow.tx.locktime,Will.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 diff --git a/willexecutors.py b/willexecutors.py index b18a5f0..6620397 100644 --- a/willexecutors.py +++ b/willexecutors.py @@ -9,195 +9,214 @@ 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 +from .util import Util DEFAULT_TIMEOUT = 5 _logger = get_logger(__name__) +class Willexecutors: + 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: + Willexecutors.initialize_willexecutor(willexecutors[w],w) -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 Willexecutors.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 - 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 + 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 Willexecutors.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 willexecutors.items(): + if Willexecutors.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 Willexecutors.is_selected(willexecutor): + if 'txs' in willexecutor: + Willexecutors.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=Willexecutors.handle_response, + timeout=timeout) + elif method == 'post': + response = Network.send_http_on_proxy(method, url, + body=data, + headers=headers, + on_finish=Willexecutors.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']=Willexecutors.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:=Willexecutors.send_request('post', willexecutor['url']+"/"+constants.net.NET_NAME+"/pushtxs", data=willexecutor['txs'].encode('ascii')): + willexecutor['broadcast_status'] = _("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 Willexecutors.AlreadyPresentException() + out=False + willexecutor['broadcast_status'] = _("Failed") + + return out + + def ping_servers(willexecutors): 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_status'] = _("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_status'] = _("Failed") - - return out - -def ping_servers(willexecutors): - for url,we in willexecutors.items(): - get_info_task(url,we) + Willexecutors.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") + def get_info_task(url,willexecutor): + w=None + try: + _logger.info("GETINFO_WILLEXECUTOR") + _logger.debug(url) + netname="bitcoin" + if constants.net.NET_NAME!="mainnet": + netname=constants.net.NET_NAME + w = Willexecutors.send_request('get',url+"/"+netname+"/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): + print("1",willexecutor) 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" + print("2",status) + if not status is None: + willexecutor['status'] = status + willexecutor['selected'] = Willexecutors.is_selected(willexecutor,selected) + def download_list(bal_plugin): + try: + l = Willexecutors.send_request('get',"https://welist.bitcoin-after.life/data/bitcoin?page=0&limit=100") + del l['status'] + from pprint import pprint + pprint(l) + for w in l: + print("----") + willexecutor=l[w] + Willexecutors.initialize_willexecutor(willexecutor,w,'New',False) + bal_plugin.config.set_key(bal_plugin.WILLEXECUTORS,l,save=True) + return l + except Exception as e: + _logger.error(f"error downloading willexecutors list:{e}") + return {} + 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 {} - 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 + def check_transaction(txid,url): + _logger.debug(f"{url}:{txid}") + try: + w = Willexecutors.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