This commit is contained in:
bitcoinafterlife 2025-03-23 13:53:10 -04:00
commit bdcf1929f5
23 changed files with 5166 additions and 0 deletions

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 copronista
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

2
README.md Normal file
View File

@ -0,0 +1,2 @@
# BalPlugin
Bitcoin After Life Electrum Plugin

20
__init__.py Normal file
View File

@ -0,0 +1,20 @@
from electrum.i18n import _
import subprocess
from . import bal_resources
BUILD_NUMBER = 0
REVISION_NUMBER = 1
VERSION_NUMBER = 0
def _version():
return f'{VERSION_NUMBER}.{REVISION_NUMBER}-{BUILD_NUMBER}'
version = _version()
author = "Bal Enterprise inc."
fullname = _('B.A.L.')
description = ''.join([
"<img src='",bal_resources.icon_path('bal16x16.png'),"'>", _("Bitcoin After Life"), '<br/>',
_("For more information, visit"),
" <a href=\"https://bitcoin-after.life/\">https://bitcoin-after.life/</a><br/>",
"<p style='font-size:8pt;vertialAlign:bottom'>Version: ", _version(),"</p>"
])
#available_for = ['qt', 'cmdline', 'qml']
available_for = ['qt']

131
bal.py Normal file
View File

@ -0,0 +1,131 @@
import random
import os
from hashlib import sha256
from typing import NamedTuple, Optional, Dict, Tuple
from electrum.plugin import BasePlugin
from electrum.util import to_bytes, bfh
from electrum import json_db
from electrum.transaction import tx_from_any
from . import util as Util
from . import willexecutors as Willexecutors
import os
json_db.register_dict('heirs', tuple, None)
json_db.register_dict('will', lambda x: get_will(x), None)
json_db.register_dict('will_settings', lambda x:x, None)
from electrum.logging import get_logger
def get_will(x):
try:
#print("______________________________________________________________________________________________________")
#print(x)
x['tx']=tx_from_any(x['tx'])
except Exception as e:
#Util.print_var(x)
raise e
return x
class BalPlugin(BasePlugin):
LOCKTIME_TIME = "bal_locktime_time"
LOCKTIME_BLOCKS = "bal_locktime_blocks"
LOCKTIMEDELTA_TIME = "bal_locktimedelta_time"
LOCKTIMEDELTA_BLOCKS = "bal_locktimedelta_blocks"
TX_FEES = "bal_tx_fees"
BROADCAST = "bal_broadcast"
ASK_BROADCAST = "bal_ask_broadcast"
INVALIDATE = "bal_invalidate"
ASK_INVALIDATE = "bal_ask_invalidate"
PREVIEW = "bal_preview"
SAVE_TXS = "bal_save_txs"
WILLEXECUTORS = "bal_willexecutors"
PING_WILLEXECUTORS = "bal_ping_willexecutors"
ASK_PING_WILLEXECUTORS = "bal_ask_ping_willexecutors"
NO_WILLEXECUTOR = "bal_no_willexecutor"
HIDE_REPLACED = "bal_hide_replaced"
HIDE_INVALIDATED = "bal_hide_invalidated"
ALLOW_REPUSH = "bal_allow_repush"
DEFAULT_SETTINGS={
LOCKTIME_TIME: 90,
LOCKTIME_BLOCKS: 144*90,
LOCKTIMEDELTA_TIME: 7,
LOCKTIMEDELTA_BLOCKS:144*7,
TX_FEES: 100,
BROADCAST: True,
ASK_BROADCAST: True,
INVALIDATE: True,
ASK_INVALIDATE: True,
PREVIEW: True,
SAVE_TXS: True,
PING_WILLEXECUTORS: False,
ASK_PING_WILLEXECUTORS: False,
NO_WILLEXECUTOR: False,
HIDE_REPLACED:True,
HIDE_INVALIDATED:True,
ALLOW_REPUSH: False,
WILLEXECUTORS: {
'http://bitcoin-after.life:9137': {
"base_fee": 100000,
"status": "New",
"info":"Bitcoin After Life Will Executor",
"address":"bcrt1qa5cntu4hgadw8zd3n6sq2nzjy34sxdtd9u0gp7"
}
},
}
LATEST_VERSION = '1'
KNOWN_VERSIONS = ('0', '1')
assert LATEST_VERSION in KNOWN_VERSIONS
SIZE = (159, 97)
def __init__(self, parent, config, name):
self.logger= get_logger(__name__)
BasePlugin.__init__(self, parent, config, name)
self.base_dir = os.path.join(config.electrum_path(), 'bal')
self.logger.info(self.base_dir)
self.parent = parent
self.config = config
self.name = name
self._hide_invalidated= self.config_get(self.HIDE_INVALIDATED)
self._hide_replaced= self.config_get(self.HIDE_REPLACED)
self.plugin_dir = os.path.split(os.path.realpath(__file__))[0]
def resource_path(self,*parts):
return os.path.join(self.plugin_dir, *parts)
def config_get(self,key):
v = self.config.get(key,None)
if v is None:
self.config.set_key(key,self.DEFAULT_SETTINGS[key],save=True)
v = self.DEFAULT_SETTINGS[key]
return v
def hide_invalidated(self):
self._hide_invalidated = not self._hide_invalidated
self.config.set_key(BalPlugin.HIDE_INVALIDATED,self.hide_invalidated,save=True)
def hide_replaced(self):
self._hide_replaced = not self._hide_replaced
self.config.set_key(BalPlugin.HIDE_REPLACED,self.hide_invalidated,save=True)
def default_will_settings(self):
return {
'tx_fees':100,
'threshold':'180d',
'locktime':'1y',
}
def validate_will_settings(self,will_settings):
if int(will_settings.get('tx_fees',1))<1:
will_settings['tx_fees']=1
if not will_settings.get('threshold'):
will_settings['threshold']='180d'
if not will_settings.get('locktime')=='':
will_settings['locktime']='1y'
return will_settings

15
bal_resources.py Normal file
View File

@ -0,0 +1,15 @@
import os
PLUGIN_DIR = os.path.split(os.path.realpath(__file__))[0]
DEFAULT_ICON = 'bal32x32.png'
DEFAULT_ICON_PATH = 'icons'
def icon_path(icon_basename: str = DEFAULT_ICON):
path = resource_path(DEFAULT_ICON_PATH,icon_basename)
return path
def resource_path(*parts):
return os.path.join(PLUGIN_DIR, *parts)

89
balqt/amountedit.py Normal file
View File

@ -0,0 +1,89 @@
# -*- coding: utf-8 -*-
from typing import Union
from decimal import Decimal
from . import qt_resources
if qt_resources.QT_VERSION == 5:
from PyQt5.QtWidgets import (QLineEdit, QStyle, QStyleOptionFrame, QSizePolicy)
from PyQt5.QtGui import QPalette, QPainter
from PyQt5.QtCore import pyqtSignal, Qt, QSize
else:
from PyQt6.QtWidgets import (QLineEdit, QStyle, QStyleOptionFrame, QSizePolicy)
from PyQt6.QtGui import QPalette, QPainter
from PyQt6.QtCore import pyqtSignal, Qt, QSize
from electrum.util import (format_satoshis_plain, decimal_point_to_base_unit_name,
FEERATE_PRECISION, quantize_feerate, DECIMAL_POINT, UI_UNIT_NAME_FEERATE_SAT_PER_VBYTE)
from electrum.gui.qt.amountedit import BTCAmountEdit, char_width_in_lineedit, ColorScheme
_NOT_GIVEN = object() # sentinel value
class PercAmountEdit(BTCAmountEdit):
def __init__(self, decimal_point, is_int=False, parent=None, *, max_amount=_NOT_GIVEN):
super().__init__(decimal_point, is_int, parent, max_amount=max_amount)
def numbify(self):
text = self.text().strip()
if text == '!':
self.shortcut.emit()
return
pos = self.cursorPosition()
chars = '0123456789%'
chars += DECIMAL_POINT
s = ''.join([i for i in text if i in chars])
if '%' in s:
self.is_perc=True
s=s.replace('%','')
else:
self.is_perc=False
if DECIMAL_POINT in s:
p = s.find(DECIMAL_POINT)
s = s.replace(DECIMAL_POINT, '')
s = s[:p] + DECIMAL_POINT + s[p:p+8]
if self.is_perc:
s+='%'
#if self.max_amount:
# if (amt := self._get_amount_from_text(s)) and amt >= self.max_amount:
# s = self._get_text_from_amount(self.max_amount)
self.setText(s)
# setText sets Modified to False. Instead we want to remember
# if updates were because of user modification.
self.setModified(self.hasFocus())
self.setCursorPosition(pos)
#if len(s>0)
# self.drawText("")
def _get_amount_from_text(self, text: str) -> Union[None, Decimal, int]:
try:
text = text.replace(DECIMAL_POINT, '.')
text = text.replace('%', '')
return (Decimal)(text)
except Exception:
return None
def _get_text_from_amount(self, amount):
out = super()._get_text_from_amount(amount)
if self.is_perc: out+='%'
return out
def paintEvent(self, event):
QLineEdit.paintEvent(self, event)
if self.base_unit:
panel = QStyleOptionFrame()
self.initStyleOption(panel)
textRect = self.style().subElementRect(QStyle.SubElement.SE_LineEditContents, panel, self)
textRect.adjust(2, 0, -10, 0)
painter = QPainter(self)
painter.setPen(ColorScheme.GRAY.as_color())
if len(self.text())==0:
painter.drawText(textRect, int(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter), self.base_unit() + " or perc value")

119
balqt/baldialog.py Normal file
View File

@ -0,0 +1,119 @@
from typing import Callable,Any
from . import qt_resources
if qt_resources.QT_VERSION == 5:
from PyQt5.QtCore import Qt,pyqtSignal
from PyQt5.QtWidgets import QLabel, QVBoxLayout, QCheckBox
else:
from PyQt6.QtCore import Qt,pyqtSignal
from PyQt6.QtWidgets import QLabel, QVBoxLayout, QCheckBox
from electrum.gui.qt.util import WindowModalDialog, TaskThread
from electrum.i18n import _
from electrum.logging import get_logger
_logger = get_logger(__name__)
class BalDialog(WindowModalDialog):
def __init__(self,parent,title=None, icon = 'bal32x32.png'):
self.parent=parent
WindowModalDialog.__init__(self,self.parent,title)
self.setWindowIcon(qt_resources.read_QIcon(icon))
class BalWaitingDialog(BalDialog):
updatemessage=pyqtSignal([str], arguments=['message'])
def __init__(self, bal_window: 'BalWindow', message: str, task, on_success=None, on_error=None, on_cancel=None,exe=True):
assert bal_window
BalDialog.__init__(self, bal_window.window, _("Please wait"))
self.message_label = QLabel(message)
vbox = QVBoxLayout(self)
vbox.addWidget(self.message_label)
self.updatemessage.connect(self.update_message)
if on_cancel:
self.cancel_button = CancelButton(self)
self.cancel_button.clicked.connect(on_cancel)
vbox.addLayout(Buttons(self.cancel_button))
self.accepted.connect(self.on_accepted)
self.task=task
self.on_success = on_success
self.on_error = on_error
self.on_cancel = on_cancel
if exe:
self.exe()
def exe(self):
self.thread = TaskThread(self)
self.thread.finished.connect(self.deleteLater) # see #3956
self.thread.finished.connect(self.finished)
self.thread.add(self.task, self.on_success, self.accept, self.on_error)
self.exec()
def hello(self):
pass
def finished(self):
_logger.info("finished")
def wait(self):
self.thread.wait()
def on_accepted(self):
self.thread.stop()
def update_message(self,msg):
self.message_label.setText(msg)
def update(self, msg):
self.updatemessage.emit(msg)
def getText(self):
return self.message_label.text()
def closeEvent(self,event):
self.thread.stop()
class BalBlockingWaitingDialog(BalDialog):
def __init__(self, bal_window: 'BalWindow', message: str, task: Callable[[], Any]):
BalDialog.__init__(self, bal_window, _("Please wait"))
self.message_label = QLabel(message)
vbox = QVBoxLayout(self)
vbox.addWidget(self.message_label)
self.finished.connect(self.deleteLater) # see #3956
# show popup
self.show()
# refresh GUI; needed for popup to appear and for message_label to get drawn
QCoreApplication.processEvents()
QCoreApplication.processEvents()
try:
# block and run given task
task()
finally:
# close popup
self.accept()
class bal_checkbox(QCheckBox):
def __init__(self, plugin,variable,window=None):
QCheckBox.__init__(self)
self.setChecked(plugin.config_get(variable))
window=window
def on_check(v):
plugin.config.set_key(variable, v == Qt.CheckState.Checked, save=True)
if window:
plugin._hide_invalidated= plugin.config_get(plugin.HIDE_INVALIDATED)
plugin._hide_replaced= plugin.config_get(plugin.HIDE_REPLACED)
window.update_all()
self.stateChanged.connect(on_check)
#TODO IMPLEMENT PREVIEW DIALOG
#tx list display txid, willexecutor, qrcode, button to sign
# :def preview_dialog(self, txs):
def preview_dialog(self, txs):
w=PreviewDialog(self,txs)
w.exec()
return w
def add_info_from_will(self,tx):
for input in tx.inputs():
pass

384
balqt/closedialog.py Normal file
View File

@ -0,0 +1,384 @@
from .baldialog import BalDialog
from . import qt_resources
if qt_resources.QT_VERSION == 5:
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QLabel, QVBoxLayout, QCheckBox,QWidget
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject,QEventLoop
else:
from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject,QEventLoop
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QLabel, QVBoxLayout, QCheckBox, QWidget
import time
from electrum.i18n import _
from electrum.gui.qt.util import WindowModalDialog, TaskThread
from electrum.network import Network,TxBroadcastError, BestEffortRequestFailed
from electrum.logging import get_logger
from functools import partial
import copy
from .. import util as Util
from .. import will as Will
from .. import willexecutors as Willexecutors
_logger = get_logger(__name__)
class BalCloseDialog(BalDialog):
updatemessage=pyqtSignal()
def __init__(self,bal_window):
BalDialog.__init__(self,bal_window.window,"Closing BAL")
self.updatemessage.connect(self.update)
self.bal_window=bal_window
self.message_label = QLabel("Closing BAL:")
self.vbox = QVBoxLayout(self)
self.vbox.addWidget(self.message_label)
self.qwidget=QWidget()
self.vbox.addWidget(self.qwidget)
self.labels=[]
#self.checking.connect(self.msg_set_checking)
#self.invalidating.connect(self.msg_set_invalidating)
#self.building.connect(self.msg_set_building)
#self.signing.connect(self.msg_set_signing)
#self.pushing.connect(self.msg_set_pushing)
#self.askpassword.connect(self.ask_password)
#self.passworddone.connect(self.password_done)
self.check_row = None
self.inval_row = None
self.build_row = None
self.sign_row = None
self.push_row = None
self.network = Network.get_instance()
self._stopping = False
self.thread = TaskThread(self)
self.thread.finished.connect(self.task_finished) # see #3956
def task_finished(self):
pass
#_logger.trace("task finished")
def close_plugin_task(self):
_logger.debug("close task to be started")
self.thread.add(self.task_phase1,on_success=self.on_success_phase1,on_done=self.on_accept,on_error=self.on_error_phase1)
self.show()
self.exec()
def task_phase1(self):
_logger.debug("close plugin phase 1 started")
try:
self.bal_window.init_class_variables()
except Will.NoHeirsException:
return False, None
self.msg_set_status("checking variables","Waiting")
try:
Will.check_amounts(self.bal_window.heirs,self.bal_window.willexecutors,self.bal_window.window.wallet.get_utxos(),self.bal_window.date_to_check,self.bal_window.window.wallet.dust_threshold())
except Will.AmountException:
self.msg_edit_row('<font color="#ff0000">'+_("In the inheritance process, the entire wallet will always be fully emptied. Your settings require an adjustment of the amounts"+"</font>"))
#self.bal_window.show_warning(_("In the inheritance process, the entire wallet will always be fully emptied. Your settings require an adjustment of the amounts"),parent=self)
self.msg_set_checking()
have_to_build=False
try:
self.bal_window.check_will()
self.msg_set_checking('Ok')
except Will.WillExpiredException as e:
self.msg_set_checking("Expired")
fee_per_byte=self.bal_window.will_settings.get('tx_fees',1)
return None, Will.invalidate_will(self.bal_window.willitems,self.bal_window.wallet,fee_per_byte)
except Will.NoHeirsException:
self.msg_set_checking("No Heirs")
except Will.NotCompleteWillException as e:
message = False
have_to_build=True
if isinstance(e,Will.HeirChangeException):
message ="Heirs changed:"
elif isinstance(e,Will.WillExecutorNotPresent):
message = "Will-Executor not present"
elif isinstance(e,Will.WillexecutorChangeException):
message = "Will-Executor changed"
elif isinstance(e,Will.TxFeesChangedException):
message = "Txfees are changed"
elif isinstance(e,Will.HeirNotFoundException):
message = "Heir not found"
if message:
self.msg_set_checking(message)
else:
self.msg_set_checking("New")
if have_to_build:
self.msg_set_building()
try:
self.bal_window.build_will()
self.bal_window.check_will()
for wid in Will.only_valid(self.bal_window.willitems):
self.bal_window.wallet.set_label(wid,"BAL Transaction")
self.msg_set_building("Ok")
except Exception as e:
self.msg_set_building(self.msg_error(e))
return False,None
have_to_sign = False
for wid in Will.only_valid(self.bal_window.willitems):
if not self.bal_window.willitems[wid].get_status("COMPLETE"):
have_to_sign = True
break
return have_to_sign, None
def on_accept(self):
pass
def on_accept_phase2(self):
pass
def on_error_push(self):
pass
def wait(self,secs):
wait_row=None
for i in range(secs,0,-1):
if self._stopping:
return
wait_row = self.msg_edit_row(f"Please wait {i}secs", wait_row)
time.sleep(1)
self.msg_del_row(wait_row)
def loop_broadcast_invalidating(self,tx):
self.msg_set_invalidating("Broadcasting")
try:
tx.add_info_from_wallet(self.bal_window.wallet)
self.network.run_from_another_thread(tx.add_info_from_network(self.network))
txid = self.network.run_from_another_thread(self.network.broadcast_transaction(tx,timeout=120),timeout=120)
self.msg_set_invalidating("Ok")
if not txid:
_logger.debug(f"should not be none txid: {txid}")
except TxBroadcastError as e:
_logger.error(e)
msg = e.get_message_for_gui()
self.msg_set_invalidating(self.msg_error(msg))
except BestEffortRequestFailed as e:
self.msg_set_invalidating(self.msg_error(e))
# self.loop_broadcast_invalidating(tx)
def loop_push(self):
self.msg_set_pushing("Broadcasting")
retry = False
try:
willexecutors=Willexecutors.get_willexecutor_transactions(self.bal_window.willitems)
for url,willexecutor in willexecutors.items():
try:
if Willexecutors.is_selected(self.bal_window.willexecutors.get(url)):
_logger.debug(f"{url}: {willexecutor}")
if not Willexecutors.push_transactions_to_willexecutor(willexecutor):
for wid in willexecutor['txsids']:
self.bal_window.willitems[wid].set_status('PUSH_FAIL',True)
retry=True
else:
for wid in willexecutor['txsids']:
self.bal_window.willitems[wid].set_status('PUSHED',True)
except Willexecutors.AlreadyPresentException:
for wid in willexecutor['txsids']:
row = self.msg_edit_row("checking {} - {} : {}".format(self.bal_window.willitems[wid].we['url'],wid, "Waiting"))
self.bal_window.willitems[wid].check_willexecutor()
row = self.msg_edit_row("checked {} - {} : {}".format(self.bal_window.willitems[wid].we['url'],wid,self.bal_window.willitems[wid].get_status("CHECKED" )),row)
except Exception as e:
_logger.error(e)
raise e
if retry:
raise Exception("retry")
except Exception as e:
self.msg_set_pushing(self.msg_error(e))
self.wait(10)
if not self._stopping:
self.loop_push()
def invalidate_task(self,tx,password):
_logger.debug(f"invalidate tx: {tx}")
tx = self.bal_window.wallet.sign_transaction(tx,password)
try:
if tx:
if tx.is_complete():
self.loop_broadcast_invalidating(tx)
self.wait(5)
else:
raise
else:
raise
except Exception as e:
self.msg_set_invalidating("Error")
raise Exception("Impossible to sign")
def on_success_invalidate(self,success):
self.thread.add(self.task_phase1,on_success=self.on_success_phase1,on_done=self.on_accept,on_error=self.on_error_phase1)
def on_error(self,error):
_logger.error(error)
pass
def on_success_phase1(self,result):
self.have_to_sign,tx = list(result)
#if have_to_sign is False and tx is None:
#self._stopping=True
#self.on_success_phase2()
# return
print("have to sign",self.have_to_sign)
password=None
if self.have_to_sign is None:
self.msg_set_invalidating()
#need to sign invalidate and restart phase 1
password = self.bal_window.get_wallet_password("Invalidate your old will",parent=self.bal_window.window)
if password is False:
self.msg_set_invalidating("Aborted")
self.wait(3)
self.close()
return
self.thread.add(partial(self.invalidate_task,tx,password),on_success=self.on_success_invalidate, on_done=self.on_accept, on_error=self.on_error)
return
elif self.have_to_sign:
password = self.bal_window.get_wallet_password("Sign your will",parent=self.bal_window.window)
if password is False:
self.msg_set_signing('Aborted')
else:
self.msg_set_signing('Nothing to do')
self.thread.add(partial(self.task_phase2,password),on_success=self.on_success_phase2,on_done=self.on_accept_phase2,on_error=self.on_error_phase2)
return
def on_success_phase2(self,arg=False):
self.thread.stop()
self.bal_window.save_willitems()
self.msg_edit_row("Finished")
self.close()
def closeEvent(self,event):
self._stopping=True
self.thread.stop()
def task_phase2(self,password):
if self.have_to_sign:
try:
if txs:=self.bal_window.sign_transactions(password):
for txid,tx in txs.items():
self.bal_window.willitems[txid].tx = copy.deepcopy(tx)
self.bal_window.save_willitems()
self.msg_set_signing("Ok")
except Exception as e:
self.msg_set_signing(self.msg_error(e))
self.msg_set_pushing()
have_to_push = False
for wid in Will.only_valid(self.bal_window.willitems):
w=self.bal_window.willitems[wid]
if w.we and w.get_status("COMPLETE") and not w.get_status("PUSHED"):
have_to_push = True
if not have_to_push:
self.msg_set_pushing("Nothing to do")
else:
try:
self.loop_push()
self.msg_set_pushing("Ok")
except Exception as e:
self.msg_set_pushing(self.msg_error(e))
self.msg_edit_row("Ok")
self.wait(5)
def on_error_phase1(self,error):
_logger.error(f"error phase1: {error}")
def on_error_phase2(self,error):
_logger.error("error phase2: { error}")
def msg_set_checking(self, status = None, row = None):
row = self.check_row if row is None else row
self.check_row = self.msg_set_status("Checking your will", row, status)
def msg_set_invalidating(self, status = None, row = None):
row = self.inval_row if row is None else row
self.inval_row = self.msg_set_status("Invalidating old will", self.inval_row, status)
def msg_set_building(self, status = None, row = None):
row = self.build_row if row is None else row
self.build_row = self.msg_set_status("Building your will", self.build_row, status)
def msg_set_signing(self, status = None, row = None):
row = self.sign_row if row is None else row
self.sign_row = self.msg_set_status("Signing your will", self.sign_row, status)
def msg_set_pushing(self, status = None, row = None):
row = self.push_row if row is None else row
self.push_row = self.msg_set_status("Broadcasting your will to executors", self.push_row, status)
def msg_set_waiting(self, status = None, row = None):
row = self.wait_row if row is None else row
self.wait_row = self.msg_edit_row(f"Please wait {status}secs", self.wait_row)
def msg_error(self,e):
return "Error: {}".format(e)
def msg_set_status(self,msg,row,status=None):
status= "Wait" if status is None else status
line="{}:\t{}".format(_(msg), status)
return self.msg_edit_row(line,row)
#return v$msg_edit_row("{}:\t{}".format(_(msg), status), row)
def ask_password(self,msg=None):
self.password=self.bal_window.get_wallet_password(msg,parent=self)
def msg_edit_row(self,line,row=None):
_logger.debug(f"{row},{line}")
#msg=self.get_text()
#rows=msg.split("\n")
#try:
# rows[row]=line
#except Exception as e:
# rows.append(line)
#row=len(rows)-1
#self.update("\n".join(rows))
#return row
#def msg_edit_label(self,line,row=None):
#_logger.trace(f"{row},{line}")
#msg=self.get_text()
#rows=msg.split("\n")
try:
self.labels[row]=line
except Exception as e:
self.labels.append(line)
row=len(self.labels)-1
self.updatemessage.emit()
return row
def msg_del_row(self,row):
#_logger.trace(f"del row: {row}")
try:
del self.labels[row]
except Exception as e:
pass
self.updatemessage.emit()
def update(self):
self.vbox.removeWidget(self.qwidget)
self.qwidget=QWidget(self)
labelsbox = QVBoxLayout(self.qwidget)
for label in self.labels:
labelsbox.addWidget(QLabel(label))
self.vbox.addWidget(self.qwidget)
def get_text(self):
return self.message_label.text()
def ThreadStopped(Exception):
pass

283
balqt/heir_list.py Normal file
View File

@ -0,0 +1,283 @@
#!/usr/bin/env python
#
# Electrum - lightweight Bitcoin client
# Copyright (C) 2015 Thomas Voegtlin
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import enum
from typing import TYPE_CHECKING
from datetime import datetime
from . import qt_resources
if qt_resources.QT_VERSION == 5:
from PyQt5.QtGui import QStandardItemModel, QStandardItem
from PyQt5.QtCore import Qt, QPersistentModelIndex, QModelIndex
from PyQt5.QtWidgets import (QAbstractItemView, QMenu,QWidget,QHBoxLayout,QLabel,QSpinBox,QPushButton)
else:
from PyQt6.QtGui import QStandardItemModel, QStandardItem
from PyQt6.QtCore import Qt, QPersistentModelIndex, QModelIndex
from PyQt6.QtWidgets import (QAbstractItemView, QMenu,QWidget,QHBoxLayout,QLabel,QSpinBox,QPushButton)
from electrum.i18n import _
from electrum.bitcoin import is_address
from electrum.util import block_explorer_URL
from electrum.plugin import run_hook
from electrum.gui.qt.util import webopen, MessageBoxMixin,HelpButton
from electrum.gui.qt.my_treeview import MyTreeView, MySortModel
from .. import util as Util
from .locktimeedit import HeirsLockTimeEdit
if TYPE_CHECKING:
from electrum.gui.qt.main_window import ElectrumWindow
class HeirList(MyTreeView,MessageBoxMixin):
class Columns(MyTreeView.BaseColumnsEnum):
NAME = enum.auto()
ADDRESS = enum.auto()
AMOUNT = enum.auto()
headers = {
Columns.NAME: _('Name'),
Columns.ADDRESS: _('Address'),
Columns.AMOUNT: _('Amount'),
#Columns.LOCKTIME:_('LockTime'),
}
filter_columns = [Columns.NAME, Columns.ADDRESS]
ROLE_SORT_ORDER = Qt.ItemDataRole.UserRole + 1000
ROLE_HEIR_KEY = Qt.ItemDataRole.UserRole + 1001
key_role = ROLE_HEIR_KEY
def __init__(self, bal_window: 'BalWindow'):
super().__init__(
parent=bal_window.window,
main_window=bal_window.window,
stretch_column=self.Columns.NAME,
editable_columns=[self.Columns.NAME,self.Columns.ADDRESS,self.Columns.AMOUNT],
)
self.decimal_point = bal_window.bal_plugin.config.get_decimal_point()
self.bal_window = bal_window
try:
self.setModel(QStandardItemModel(self))
self.sortByColumn(self.Columns.NAME, Qt.SortOrder.AscendingOrder)
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
except:
pass
#self.sortByColumn(self.Columns.NAME, Qt.SortOrder.AscendingOrder)
#self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
self.setSortingEnabled(True)
self.std_model = self.model()
self.update()
def on_edited(self, idx, edit_key, *, text):
original = prior_name = self.bal_window.heirs.get(edit_key)
if not prior_name:
return
col = idx.column()
try:
#if col == 3:
# try:
# text = Util.str_to_locktime(text)
# except:
# print("not a valid locktime")
# pass
if col == 2:
text = Util.encode_amount(text,self.decimal_point)
elif col == 0:
self.bal_window.delete_heirs([edit_key])
edit_key = text
prior_name[col-1] = text
prior_name.insert(0,edit_key)
prior_name = tuple(prior_name)
except Exception as e:
#print("eccezione tupla",e)
prior_name = (edit_key,)+prior_name[:col-1]+(text,)+prior_name[col:]
#print("prior_name",prior_name,original)
try:
self.bal_window.set_heir(prior_name)
#print("setheir")
except Exception as e:
pass
#print("heir non valido ripristino l'originale",e)
try:
#print("setup_original",(edit_key,)+original)
self.bal_window.set_heir((edit_key,)+original)
except Exception as e:
#print("errore nellimpostare original",e,original)
self.update()
def create_menu(self, position):
menu = QMenu()
idx = self.indexAt(position)
column = idx.column() or self.Columns.NAME
selected_keys = []
for s_idx in self.selected_in_column(self.Columns.NAME):
#print(s_idx)
sel_key = self.model().itemFromIndex(s_idx).data(0)
selected_keys.append(sel_key)
if selected_keys and idx.isValid():
column_title = self.model().horizontalHeaderItem(column).text()
column_data = '\n'.join(self.model().itemFromIndex(s_idx).text()
for s_idx in self.selected_in_column(column))
menu.addAction(_("Copy {}").format(column_title), lambda: self.place_text_on_clipboard(column_data, title=column_title))
if column in self.editable_columns:
item = self.model().itemFromIndex(idx)
if item.isEditable():
# would not be editable if openalias
persistent = QPersistentModelIndex(idx)
menu.addAction(_("Edit {}").format(column_title), lambda p=persistent: self.edit(QModelIndex(p)))
menu.addAction(_("Delete"), lambda: self.bal_window.delete_heirs(selected_keys))
menu.exec(self.viewport().mapToGlobal(position))
def update(self):
if self.maybe_defer_update():
return
current_key = self.get_role_data_for_current_item(col=self.Columns.NAME, role=self.ROLE_HEIR_KEY)
self.model().clear()
self.update_headers(self.__class__.headers)
set_current = None
for key in sorted(self.bal_window.heirs.keys()):
heir = self.bal_window.heirs[key]
labels = [""] * len(self.Columns)
labels[self.Columns.NAME] = key
labels[self.Columns.ADDRESS] = heir[0]
labels[self.Columns.AMOUNT] = Util.decode_amount(heir[1],self.decimal_point)
#labels[self.Columns.LOCKTIME] = str(Util.locktime_to_str(heir[2]))
items = [QStandardItem(x) for x in labels]
items[self.Columns.NAME].setEditable(True)
items[self.Columns.ADDRESS].setEditable(True)
items[self.Columns.AMOUNT].setEditable(True)
#items[self.Columns.LOCKTIME].setEditable(True)
items[self.Columns.NAME].setData(key, self.ROLE_HEIR_KEY+1)
items[self.Columns.ADDRESS].setData(key, self.ROLE_HEIR_KEY+2)
items[self.Columns.AMOUNT].setData(key, self.ROLE_HEIR_KEY+3)
#items[self.Columns.LOCKTIME].setData(key, self.ROLE_HEIR_KEY+4)
self.model().insertRow(self.model().rowCount(), items)
if key == current_key:
idx = self.model().index(row_count, self.Columns.NAME)
set_current = QPersistentModelIndex(idx)
self.set_current_idx(set_current)
# FIXME refresh loses sort order; so set "default" here:
self.filter()
run_hook('update_heirs_tab', self)
def refresh_row(self, key, row):
# nothing to update here
pass
def get_edit_key_from_coordinate(self, row, col):
#print("role_data",self.get_role_data_from_coordinate(row, col, role=self.ROLE_HEIR_KEY))
#print(col)
return self.get_role_data_from_coordinate(row, col, role=self.ROLE_HEIR_KEY+col+1)
return col
def create_toolbar(self, config):
toolbar, menu = self.create_toolbar_with_menu('')
menu.addAction(_("&New Heir"), self.bal_window.new_heir_dialog)
menu.addAction(_("Import"), self.bal_window.import_heirs)
menu.addAction(_("Export"), lambda: self.bal_window.export_heirs())
#menu.addAction(_("Build Traonsactions"), self.build_transactions)
self.heir_locktime = HeirsLockTimeEdit(self.window(),0)
def on_heir_locktime():
if not self.heir_locktime.get_locktime():
self.heir_locktime.set_locktime('1y')
self.bal_window.will_settings['locktime'] = self.heir_locktime.get_locktime() if self.heir_locktime.get_locktime() else "1y"
self.bal_window.bal_plugin.config.set_key('will_settings',self.bal_window.will_settings,save = True)
self.heir_locktime.valueEdited.connect(on_heir_locktime)
self.heir_threshold = HeirsLockTimeEdit(self,0)
def on_heir_threshold():
if not self.heir_threshold.get_locktime():
self.heir_threshold.set_locktime('180d')
self.bal_window.will_settings['threshold'] = self.heir_threshold.get_locktime()
self.bal_window.bal_plugin.config.set_key('will_settings',self.bal_window.will_settings,save = True)
self.heir_threshold.valueEdited.connect(on_heir_threshold)
self.heir_tx_fees = QSpinBox()
self.heir_tx_fees.setMinimum(1)
self.heir_tx_fees.setMaximum(10000)
def on_heir_tx_fees():
if not self.heir_tx_fees.value():
self.heir_tx_fees.set_value(1)
self.bal_window.will_settings['tx_fees'] = self.heir_tx_fees.value()
self.bal_window.bal_plugin.config.set_key('will_settings',self.bal_window.will_settings,save = True)
self.heir_tx_fees.valueChanged.connect(on_heir_tx_fees)
self.heirs_widget = QWidget()
layout = QHBoxLayout()
self.heirs_widget.setLayout(layout)
layout.addWidget(QLabel(_("Delivery Time:")))
layout.addWidget(self.heir_locktime)
layout.addWidget(HelpButton(_("Locktime* to be used in the transaction\n"
+"if you choose Raw, you can insert various options based on suffix:\n"
#+" - b: number of blocks after current block(ex: 144b means tomorrow)\n"
+" - d: number of days after current day(ex: 1d means tomorrow)\n"
+" - y: number of years after currrent day(ex: 1y means one year from today)\n"
+"* locktime can be anticipated to update will\n")))
layout.addWidget(QLabel(" "))
layout.addWidget(QLabel(_("Check Alive:")))
layout.addWidget(self.heir_threshold)
layout.addWidget(HelpButton(_("Check to ask for invalidation.\n"
+"When less then this time is missing, ask to invalidate.\n"
+"If you fail to invalidate during this time, your transactions will be delivered to your heirs.\n"
+"if you choose Raw, you can insert various options based on suffix:\n"
#+" - b: number of blocks after current block(ex: 144b means tomorrow)\n"
+" - d: number of days after current day(ex: 1d means tomorrow).\n"
+" - y: number of years after currrent day(ex: 1y means one year from today).\n\n")))
layout.addWidget(QLabel(" "))
layout.addWidget(QLabel(_("Fees:")))
layout.addWidget(self.heir_tx_fees)
layout.addWidget(HelpButton(_("Fee to be used in the transaction")))
layout.addWidget(QLabel("sats/vbyte"))
layout.addWidget(QLabel(" "))
newHeirButton = QPushButton(_("New Heir"))
newHeirButton.clicked.connect(self.bal_window.new_heir_dialog)
layout.addWidget(newHeirButton)
toolbar.insertWidget(2, self.heirs_widget)
return toolbar
def update_will_settings(self):
self.heir_threshold.set_locktime(self.bal_window.will_settings['threshold'])
self.heir_locktime.set_locktime(self.bal_window.will_settings['locktime'])
self.heir_tx_fees.setValue(int(self.bal_window.will_settings['tx_fees']))
def build_transactions(self):
will = self.bal_window.prepare_will()

255
balqt/locktimeedit.py Normal file
View File

@ -0,0 +1,255 @@
# Copyright (C) 2020 The Electrum developers
# Distributed under the MIT software license, see the accompanying
# file LICENCE or http://www.opensource.org/licenses/mit-license.php
import time
from datetime import datetime
from typing import Optional, Any
from . import qt_resources
if qt_resources.QT_VERSION == 5:
from PyQt5.QtCore import Qt, QDateTime, pyqtSignal
from PyQt5.QtGui import QPalette, QPainter
from PyQt5.QtWidgets import (QWidget, QLineEdit, QStyle, QStyleOptionFrame, QComboBox, QHBoxLayout, QDateTimeEdit)
else:
from PyQt6.QtCore import Qt, QDateTime, pyqtSignal
from PyQt6.QtGui import QPalette, QPainter
from PyQt6.QtWidgets import (QWidget, QLineEdit, QStyle, QStyleOptionFrame, QComboBox, QHBoxLayout, QDateTimeEdit)
from electrum.i18n import _
from electrum.bitcoin import NLOCKTIME_MIN, NLOCKTIME_MAX, NLOCKTIME_BLOCKHEIGHT_MAX
from electrum.gui.qt.util import char_width_in_lineedit, ColorScheme
class HeirsLockTimeEdit(QWidget):
valueEdited = pyqtSignal()
locktime_threshold = 50000000
def __init__(self, parent=None,default_index = 1):
QWidget.__init__(self, parent)
hbox = QHBoxLayout()
self.setLayout(hbox)
hbox.setContentsMargins(0, 0, 0, 0)
hbox.setSpacing(0)
self.locktime_raw_e = LockTimeRawEdit(self,time_edit = self)
#self.locktime_height_e = LockTimeHeightEdit(self)
self.locktime_date_e = LockTimeDateEdit(self,time_edit = self)
#self.editors = [self.locktime_raw_e, self.locktime_height_e, self.locktime_date_e]
self.editors = [self.locktime_raw_e, self.locktime_date_e]
self.combo = QComboBox()
#options = [_("Raw"), _("Block height"), _("Date")]
options = [_("Raw"),_("Date")]
self.option_index_to_editor_map = {
0: self.locktime_raw_e,
#1: self.locktime_height_e,
1: self.locktime_date_e,
#2: self.locktime_date_e,
}
self.combo.addItems(options)
self.editor = self.option_index_to_editor_map[default_index]
self.combo.currentIndexChanged.connect(self.on_current_index_changed)
self.combo.setCurrentIndex(default_index)
self.on_current_index_changed(default_index)
hbox.addWidget(self.combo)
for w in self.editors:
hbox.addWidget(w)
hbox.addStretch(1)
#self.locktime_height_e.textEdited.connect(self.valueEdited.emit)
self.locktime_raw_e.editingFinished.connect(self.valueEdited.emit)
self.locktime_date_e.dateTimeChanged.connect(self.valueEdited.emit)
self.combo.currentIndexChanged.connect(self.valueEdited.emit)
def on_current_index_changed(self,i):
for w in self.editors:
w.setVisible(False)
w.setEnabled(False)
prev_locktime = self.editor.get_locktime()
self.editor = self.option_index_to_editor_map[i]
if self.editor.is_acceptable_locktime(prev_locktime):
self.editor.set_locktime(prev_locktime,force=True)
self.editor.setVisible(True)
self.editor.setEnabled(True)
def get_locktime(self) -> Optional[str]:
return self.editor.get_locktime()
def set_index(self,index):
self.combo.setCurrentIndex(index)
self.on_current_index_changed(index)
def set_locktime(self, x: Any,force=True) -> None:
self.editor.set_locktime(x,force)
class _LockTimeEditor:
min_allowed_value = NLOCKTIME_MIN
max_allowed_value = NLOCKTIME_MAX
def get_locktime(self) -> Optional[int]:
raise NotImplementedError()
def set_locktime(self, x: Any,force=True) -> None:
raise NotImplementedError()
@classmethod
def is_acceptable_locktime(cls, x: Any) -> bool:
if not x: # e.g. empty string
return True
try:
x = int(x)
except Exception as e:
return False
return cls.min_allowed_value <= x <= cls.max_allowed_value
class LockTimeRawEdit(QLineEdit, _LockTimeEditor):
def __init__(self, parent=None,time_edit=None):
QLineEdit.__init__(self, parent)
self.setFixedWidth(14 * char_width_in_lineedit())
self.textChanged.connect(self.numbify)
self.isdays = False
self.isyears = False
self.isblocks = False
self.time_edit=time_edit
def replace_str(self,text):
return str(text).replace('d','').replace('y','').replace('b','')
def checkbdy(self,s,pos,appendix):
try:
charpos = pos-1
charpos = max(0,charpos)
charpos = min(len(s)-1,charpos)
if appendix == s[charpos]:
s=self.replace_str(s)+appendix
pos = charpos
except Exception as e:
pass
return pos, s
def numbify(self):
text = self.text().strip()
#chars = '0123456789bdy' removed the option to choose locktime by block
chars = '0123456789dy'
pos = posx = self.cursorPosition()
pos = len(''.join([i for i in text[:pos] if i in chars]))
s = ''.join([i for i in text if i in chars])
self.isdays = False
self.isyears = False
self.isblocks = False
pos,s = self.checkbdy(s,pos,'d')
pos,s = self.checkbdy(s,pos,'y')
pos,s = self.checkbdy(s,pos,'b')
if 'd' in s: self.isdays = True
if 'y' in s: self.isyears = True
if 'b' in s: self.isblocks = True
if self.isdays: s= self.replace_str(s) + 'd'
if self.isyears: s = self.replace_str(s) + 'y'
if self.isblocks: s= self.replace_str(s) + 'b'
self.set_locktime(s,force=False)
# setText sets Modified to False. Instead we want to remember
# if updates were because of user modification.
self.setModified(self.hasFocus())
self.setCursorPosition(pos)
def get_locktime(self) -> Optional[str]:
try:
return str(self.text())
except Exception as e:
return None
def set_locktime(self, x: Any,force=True) -> None:
out = str(x)
if 'd' in out:
out = self.replace_str(x)+'d'
elif 'y' in out:
out = self.replace_str(x)+'y'
elif 'b' in out:
out = self.replace_str(x)+'b'
else:
try:
out = int(x)
except Exception as e:
self.setText('')
return
out = max(out, self.min_allowed_value)
out = min(out, self.max_allowed_value)
self.setText(str(out))
#try:
# if self.time_edit and int(out)>self.time_edit.locktime_threshold and not force:
# self.time_edit.set_index(1)
#except:
# pass
class LockTimeHeightEdit(LockTimeRawEdit):
max_allowed_value = NLOCKTIME_BLOCKHEIGHT_MAX
def __init__(self, parent=None,time_edit=None):
LockTimeRawEdit.__init__(self, parent)
self.setFixedWidth(20 * char_width_in_lineedit())
self.time_edit = time_edit
def paintEvent(self, event):
super().paintEvent(event)
panel = QStyleOptionFrame()
self.initStyleOption(panel)
textRect = self.style().subElementRect(QStyle.SE_LineEditContents, panel, self)
textRect.adjust(2, 0, -10, 0)
painter = QPainter(self)
painter.setPen(ColorScheme.GRAY.as_color())
painter.drawText(textRect, int(Qt.AlignRight | Qt.AlignVCenter), "height")
def get_max_allowed_timestamp() -> int:
ts = NLOCKTIME_MAX
# Test if this value is within the valid timestamp limits (which is platform-dependent).
# see #6170
try:
datetime.fromtimestamp(ts)
except (OSError, OverflowError):
ts = 2 ** 31 - 1 # INT32_MAX
datetime.fromtimestamp(ts) # test if raises
return ts
class LockTimeDateEdit(QDateTimeEdit, _LockTimeEditor):
min_allowed_value = NLOCKTIME_BLOCKHEIGHT_MAX + 1
max_allowed_value = get_max_allowed_timestamp()
def __init__(self, parent=None,time_edit=None):
QDateTimeEdit.__init__(self, parent)
self.setMinimumDateTime(datetime.fromtimestamp(self.min_allowed_value))
self.setMaximumDateTime(datetime.fromtimestamp(self.max_allowed_value))
self.setDateTime(QDateTime.currentDateTime())
self.time_edit = time_edit
def get_locktime(self) -> Optional[int]:
dt = self.dateTime().toPyDateTime()
locktime = int(time.mktime(dt.timetuple()))
return locktime
def set_locktime(self, x: Any,force = False) -> None:
if not self.is_acceptable_locktime(x):
self.setDateTime(QDateTime.currentDateTime())
return
try:
x = int(x)
except Exception:
self.setDateTime(QDateTime.currentDateTime())
return
dt = datetime.fromtimestamp(x)
self.setDateTime(dt)

331
balqt/preview_dialog.py Normal file
View File

@ -0,0 +1,331 @@
import enum
import copy
import json
import urllib.request
import urllib.parse
from functools import partial
from . import qt_resources
if qt_resources.QT_VERSION == 5:
from PyQt5.QtGui import QStandardItemModel, QStandardItem, QPalette, QColor
from PyQt5.QtCore import Qt,QPersistentModelIndex, QModelIndex
from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QLabel,QMenu,QAbstractItemView,QWidget)
else:
from PyQt6.QtGui import QStandardItemModel, QStandardItem, QPalette, QColor
from PyQt6.QtCore import Qt,QPersistentModelIndex, QModelIndex
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QLabel,QMenu,QAbstractItemView,QWidget)
from electrum.i18n import _
from electrum.gui.qt.util import (Buttons,read_QIcon, import_meta_gui, export_meta_gui,MessageBoxMixin)
from electrum.util import write_json_file,read_json_file,FileImportFailed
from electrum.gui.qt.my_treeview import MyTreeView
from electrum.transaction import tx_from_any
from electrum.network import Network
from ..bal import BalPlugin
from .. import willexecutors as Willexecutors
from .. import util as Util
from .. import will as Will
from .baldialog import BalDialog
class PreviewList(MyTreeView):
class Columns(MyTreeView.BaseColumnsEnum):
LOCKTIME = enum.auto()
TXID = enum.auto()
WILLEXECUTOR = enum.auto()
STATUS = enum.auto()
headers = {
Columns.LOCKTIME: _('Locktime'),
Columns.TXID: _('Txid'),
Columns.WILLEXECUTOR: _('Will-Executor'),
Columns.STATUS: _('Status'),
}
ROLE_HEIR_KEY = Qt.ItemDataRole.UserRole + 2000
key_role = ROLE_HEIR_KEY
def __init__(self, parent: 'BalWindow',will):
super().__init__(
parent=parent.window,
stretch_column=self.Columns.TXID,
)
self.decimal_point=parent.bal_plugin.config.get_decimal_point
self.setModel(QStandardItemModel(self))
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
if not will is None:
self.will = will
else:
self.will = parent.willitems
self.bal_window = parent
self.wallet=parent.window.wallet
self.setModel(QStandardItemModel(self))
self.setSortingEnabled(True)
self.std_model = self.model()
self.config = parent.bal_plugin.config
self.bal_plugin=self.bal_window.bal_plugin
self.update()
def create_menu(self, position):
menu = QMenu()
idx = self.indexAt(position)
column = idx.column() or self.Columns.TXID
selected_keys = []
for s_idx in self.selected_in_column(self.Columns.TXID):
sel_key = self.model().itemFromIndex(s_idx).data(0)
selected_keys.append(sel_key)
if selected_keys and idx.isValid():
column_title = self.model().horizontalHeaderItem(column).text()
column_data = '\n'.join(self.model().itemFromIndex(s_idx).text()
for s_idx in self.selected_in_column(column))
menu.addAction(_("details").format(column_title), lambda: self.show_transaction(selected_keys)).setEnabled(len(selected_keys)<2)
menu.addAction(_("check ").format(column_title), lambda: self.check_transactions(selected_keys))
menu.addSeparator()
menu.addAction(_("delete").format(column_title), lambda: self.delete(selected_keys))
menu.exec(self.viewport().mapToGlobal(position))
def delete(self,selected_keys):
for key in selected_keys:
del self.will[key]
try:
del self.bal_window.willitems[key]
except:
pass
try:
del self.bal_window.will[key]
except:
pass
self.update()
def check_transactions(self,selected_keys):
wout = {}
for k in selected_keys:
wout[k] = self.will[k]
if wout:
self.bal_window.check_transactions(wout)
self.update()
def show_transaction(self,selected_keys):
for key in selected_keys:
self.bal_window.show_transaction(self.will[key].tx)
self.update()
def select(self,selected_keys):
self.selected += selected_keys
self.update()
def deselect(self,selected_keys):
for key in selected_keys:
self.selected.remove(key)
self.update()
def update_will(self,will):
self.will.update(will)
self.update()
def update(self):
if self.will is None:
return
current_key = self.get_role_data_for_current_item(col=self.Columns.TXID, role=self.ROLE_HEIR_KEY)
self.model().clear()
self.update_headers(self.__class__.headers)
set_current = None
for txid,bal_tx in self.will.items():
if self.bal_window.bal_plugin._hide_replaced and bal_tx.get_status('REPLACED'):
continue
if self.bal_window.bal_plugin._hide_invalidated and bal_tx.get_status('INVALIDATED'):
continue
tx=bal_tx.tx
labels = [""] * len(self.Columns)
labels[self.Columns.LOCKTIME] = Util.locktime_to_str(tx.locktime)
labels[self.Columns.TXID] = txid
we = 'None'
if bal_tx.we:
we = bal_tx.we['url']
labels[self.Columns.WILLEXECUTOR]=we
status = bal_tx.status
if len(bal_tx.status) > 53:
status = "...{}".format(status[-50:])
labels[self.Columns.STATUS] = status
items=[]
for e in labels:
if type(e)== list:
try:
items.append(QStandardItem(*e))
except Exception as e:
pass
else:
items.append(QStandardItem(str(e)))
#pal = QPalette()
#pal.setColor(QPalette.ColorRole.Window, QColor(bal_tx.get_color()))
#items[-1].setAutoFillBackground(True)
#items[-1o].setPalette(pal)
items[-1].setBackground(QColor(bal_tx.get_color()))
self.model().insertRow(self.model().rowCount(), items)
if txid == current_key:
idx = self.model().index(row_count, self.Columns.TXID)
set_current = QPersistentModelIndex(idx)
self.set_current_idx(set_current)
def create_toolbar(self, config):
toolbar, menu = self.create_toolbar_with_menu('')
menu.addAction(_("Prepare"), self.build_transactions)
menu.addAction(_("Display"), self.bal_window.preview_modal_dialog)
menu.addAction(_("Sign"), self.ask_password_and_sign_transactions)
menu.addAction(_("Export"), self.export_will)
#menu.addAction(_("Import"), self.import_will)
menu.addAction(_("Broadcast"), self.broadcast)
menu.addAction(_("Check"), self.check)
menu.addAction(_("Invalidate"), self.invalidate_will)
prepareButton = QPushButton(_("Prepare"))
prepareButton.clicked.connect(self.build_transactions)
signButton = QPushButton(_("Sign"))
signButton.clicked.connect(self.ask_password_and_sign_transactions)
pushButton = QPushButton(_("Broadcast"))
pushButton.clicked.connect(self.broadcast)
displayButton = QPushButton(_("Display"))
displayButton.clicked.connect(self.bal_window.preview_modal_dialog)
hlayout = QHBoxLayout()
widget = QWidget()
hlayout.addWidget(prepareButton)
hlayout.addWidget(signButton)
hlayout.addWidget(pushButton)
hlayout.addWidget(displayButton)
widget.setLayout(hlayout)
toolbar.insertWidget(2,widget)
return toolbar
def hide_replaced(self):
self.bal_window.bal_plugin.hide_replaced()
self.update()
def hide_invalidated(self):
f.bal_window.bal_plugin.hide_invalidated()
self.update()
def build_transactions(self):
will = self.bal_window.prepare_will()
if will:
self.update_will(will)
def export_json_file(self,path):
write_json_file(path, self.will)
def export_will(self):
self.bal_window.export_will()
self.update()
def import_will(self):
self.bal_window.import_will()
def ask_password_and_sign_transactions(self):
self.bal_window.ask_password_and_sign_transactions(callback=self.update)
def broadcast(self):
self.bal_window.broadcast_transactions()
self.update()
def check(self):
self.bal_window.check_transactions(self.bal_window.willitems)
self.update()
def invalidate_will(self):
self.bal_window.invalidate_will()
self.update()
class PreviewDialog(BalDialog,MessageBoxMixin):
def __init__(self, bal_window, will):
self.parent = bal_window.window
BalDialog.__init__(self,bal_window = bal_window)
self.bal_plugin = bal_window.bal_plugin
self.gui_object = self.bal_plugin.gui_object
self.config = self.bal_plugin.config
self.bal_window = bal_window
self.wallet = bal_window.window.wallet
self.format_amount = bal_window.window.format_amount
self.base_unit = bal_window.window.base_unit
self.format_fiat_and_units = bal_window.window.format_fiat_and_units
self.fx = bal_window.window.fx
self.format_fee_rate = bal_window.window.format_fee_rate
self.show_address = bal_window.window.show_address
if not will:
self.will = bal_window.willitems
else:
self.will = will
self.setWindowTitle(_('Transactions Preview'))
self.setMinimumSize(1000, 200)
self.size_label = QLabel()
self.transactions_list = PreviewList(self.bal_window,self.will)
vbox = QVBoxLayout(self)
vbox.addWidget(self.size_label)
vbox.addWidget(self.transactions_list)
buttonbox = QHBoxLayout()
b = QPushButton(_('Sign'))
b.clicked.connect(self.transactions_list.ask_password_and_sign_transactions)
buttonbox.addWidget(b)
b = QPushButton(_('Export Will'))
b.clicked.connect(self.transactions_list.export_will)
buttonbox.addWidget(b)
b = QPushButton(_('Broadcast'))
b.clicked.connect(self.transactions_list.broadcast)
buttonbox.addWidget(b)
b = QPushButton(_('Invalidate will'))
b.clicked.connect(self.transactions_list.invalidate_will)
buttonbox.addWidget(b)
vbox.addLayout(buttonbox)
self.update()
def update_will(self,will):
self.will.update(will)
self.transactions_list.update_will(will)
self.update()
def update(self):
self.transactions_list.update()
def is_hidden(self):
return self.isMinimized() or self.isHidden()
def show_or_hide(self):
if self.is_hidden():
self.bring_to_top()
else:
self.hide()
def bring_to_top(self):
self.show()
self.raise_()
def closeEvent(self, event):
event.accept()

16
balqt/qt_resources.py Normal file
View File

@ -0,0 +1,16 @@
from .. import bal_resources
import sys
try:
QT_VERSION=sys._GUI_QT_VERSION
except:
QT_VERSION=6
if QT_VERSION == 5:
from PyQt5.QtGui import QIcon,QPixmap
else:
from PyQt6.QtGui import QIcon,QPixmap
def read_QIcon(icon_basename: str=bal_resources.DEFAULT_ICON) -> QIcon:
return QIcon(bal_resources.icon_path(icon_basename))
def read_QPixmap(icon_basename: str=bal_resources.DEFAULT_ICON) -> QPixmap:
return QPixmap(bal_resources.icon_path(icon_basename))

199
balqt/willdetail.py Normal file
View File

@ -0,0 +1,199 @@
from functools import partial
from . import qt_resources
if qt_resources.QT_VERSION == 5:
from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QLabel,QWidget,QScrollArea)
from PyQt5.QtGui import (QPixmap, QImage, QBitmap, QPainter, QFontDatabase, QPen, QFont,
QColor, QDesktopServices, qRgba, QPainterPath,QPalette)
else:
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QLabel,QWidget,QScrollArea)
from PyQt6.QtGui import (QPixmap, QImage, QBitmap, QPainter, QFontDatabase, QPen, QFont,
QColor, QDesktopServices, qRgba, QPainterPath,QPalette)
from electrum.util import decimal_point_to_base_unit_name
from electrum.i18n import _
from ..bal import BalPlugin
from .. import will as Will
from .. import util as Util
from .baldialog import BalDialog
class WillDetailDialog(BalDialog):
def __init__(self, bal_window):
self.will = bal_window.willitems
self.threshold = Util.parse_locktime_string(bal_window.will_settings['threshold'])
self.bal_window = bal_window
Will.add_willtree(self.will)
super().__init__(bal_window.window)
self.config = bal_window.window.config
self.wallet = bal_window.wallet
self.format_amount = bal_window.window.format_amount
self.base_unit = bal_window.window.base_unit
self.format_fiat_and_units = bal_window.window.format_fiat_and_units
self.fx = bal_window.window.fx
self.format_fee_rate = bal_window.window.format_fee_rate
self.decimal_point = bal_window.bal_plugin.config.get_decimal_point()
self.base_unit_name = decimal_point_to_base_unit_name(self.decimal_point)
self.setWindowTitle(_('Will Details'))
self.setMinimumSize(670,700)
self.vlayout= QVBoxLayout()
w=QWidget()
hlayout = QHBoxLayout(w)
b = QPushButton(_('Sign'))
b.clicked.connect(self.ask_password_and_sign_transactions)
hlayout.addWidget(b)
b = QPushButton(_('Broadcast'))
b.clicked.connect(self.broadcast_transactions)
hlayout.addWidget(b)
b = QPushButton(_('Export'))
b.clicked.connect(self.export_will)
hlayout.addWidget(b)
"""
toggle = "Hide"
if self.bal_window.bal_plugin._hide_replaced:
toggle = "Unhide"
self.toggle_replace_button = QPushButton(_(f"{toggle} replaced"))
self.toggle_replace_button.clicked.connect(self.toggle_replaced)
hlayout.addWidget(self.toggle_replace_button)
toggle = "Hide"
if self.bal_window.bal_plugin._hide_invalidated:
toggle = "Unhide"
self.toggle_invalidate_button = QPushButton(_(f"{toggle} invalidated"))
self.toggle_invalidate_button.clicked.connect(self.toggle_invalidated)
hlayout.addWidget(self.toggle_invalidate_button)
"""
b = QPushButton(_('Invalidate'))
b.clicked.connect(bal_window.invalidate_will)
hlayout.addWidget(b)
self.vlayout.addWidget(w)
self.paint_scroll_area()
#vlayout.addWidget(QLabel(_("DON'T PANIC !!! everything is fine, all possible futures are covered")))
self.vlayout.addWidget(QLabel(_("Expiration date: ")+Util.locktime_to_str(self.threshold)))
self.vlayout.addWidget(self.scrollbox)
w=QWidget()
hlayout = QHBoxLayout(w)
hlayout.addWidget(QLabel(_("Valid Txs:")+ str(len(Will.only_valid_list(self.will)))))
hlayout.addWidget(QLabel(_("Total Txs:")+ str(len(self.will))))
self.vlayout.addWidget(w)
self.setLayout(self.vlayout)
def paint_scroll_area(self):
#self.scrollbox.deleteLater()
#self.willlayout.deleteLater()
#self.detailsWidget.deleteLater()
self.scrollbox = QScrollArea()
viewport = QWidget(self.scrollbox)
self.willlayout = QVBoxLayout(viewport)
self.detailsWidget = WillWidget(parent=self)
self.willlayout.addWidget(self.detailsWidget)
self.scrollbox.setWidget(viewport)
viewport.setLayout(self.willlayout)
def ask_password_and_sign_transactions(self):
self.bal_window.ask_password_and_sign_transactions(callback=self.update)
self.update()
def broadcast_transactions(self):
self.bal_window.broadcast_transactions()
self.update()
def export_will(self):
self.bal_window.export_will()
def toggle_replaced(self):
self.bal_window.bal_plugin.hide_replaced()
toggle = _("Hide")
if self.bal_window.bal_plugin._hide_replaced:
toggle = _("Unhide")
self.toggle_replace_button.setText(f"{toggle} {_('replaced')}")
self.update()
def toggle_invalidated(self):
self.bal_window.bal_plugin.hide_invalidated()
toggle = _("Hide")
if self.bal_window.bal_plugin._hide_invalidated:
toggle = _("Unhide")
self.toggle_invalidate_button.setText(_(f"{toggle} {_('invalidated')}"))
self.update()
def update(self):
self.will = self.bal_window.willitems
pos = self.vlayout.indexOf(self.scrollbox)
self.vlayout.removeWidget(self.scrollbox)
self.paint_scroll_area()
self.vlayout.insertWidget(pos,self.scrollbox)
super().update()
class WillWidget(QWidget):
def __init__(self,father=None,parent = None):
super().__init__()
vlayout = QVBoxLayout()
self.setLayout(vlayout)
self.will = parent.bal_window.willitems
self.parent = parent
for w in self.will:
if self.will[w].get_status('REPLACED') and self.parent.bal_window.bal_plugin._hide_replaced:
continue
if self.will[w].get_status('INVALIDATED') and self.parent.bal_window.bal_plugin._hide_invalidated:
continue
f = self.will[w].father
if father == f:
qwidget = QWidget()
childWidget = QWidget()
hlayout=QHBoxLayout(qwidget)
qwidget.setLayout(hlayout)
vlayout.addWidget(qwidget)
detailw=QWidget()
detaillayout=QVBoxLayout()
detailw.setLayout(detaillayout)
willpushbutton = QPushButton(w)
willpushbutton.clicked.connect(partial(self.parent.bal_window.show_transaction,txid=w))
detaillayout.addWidget(willpushbutton)
locktime = Util.locktime_to_str(self.will[w].tx.locktime)
creation = Util.locktime_to_str(self.will[w].time)
def qlabel(title,value):
label = "<b>"+_(str(title)) + f":</b>\t{str(value)}"
return QLabel(label)
detaillayout.addWidget(qlabel("Locktime",locktime))
detaillayout.addWidget(qlabel("Creation Time",creation))
total_fees = self.will[w].tx.input_value() - self.will[w].tx.output_value()
decoded_fees = total_fees #Util.decode_amount(total_fees,self.parent.decimal_point)
fee_per_byte = round(total_fees/self.will[w].tx.estimated_size(),3)
fees_str = str(decoded_fees) + " ("+ str(fee_per_byte) + " sats/vbyte)"
detaillayout.addWidget(qlabel("Transaction fees:",fees_str))
detaillayout.addWidget(qlabel("Status:",self.will[w].status))
detaillayout.addWidget(QLabel(""))
detaillayout.addWidget(QLabel("<b>Heirs:</b>"))
for heir in self.will[w].heirs:
if "w!ll3x3c\"" not in heir:
decoded_amount = Util.decode_amount(self.will[w].heirs[heir][3],self.parent.decimal_point)
detaillayout.addWidget(qlabel(heir,f"{decoded_amount} {self.parent.base_unit_name}"))
if self.will[w].we:
detaillayout.addWidget(QLabel(""))
detaillayout.addWidget(QLabel(_("<b>Willexecutor:</b:")))
decoded_amount = Util.decode_amount(self.will[w].we['base_fee'],self.parent.decimal_point)
detaillayout.addWidget(qlabel(self.will[w].we['url'],f"{decoded_amount} {self.parent.base_unit_name}"))
detaillayout.addStretch()
pal = QPalette()
pal.setColor(QPalette.ColorRole.Window, QColor(self.will[w].get_color()))
detailw.setAutoFillBackground(True)
detailw.setPalette(pal)
hlayout.addWidget(detailw)
hlayout.addWidget(WillWidget(w,parent = parent))

View File

@ -0,0 +1,292 @@
import enum
import json
import urllib.request
import urllib.parse
from . import qt_resources
if qt_resources.QT_VERSION == 5:
from PyQt5.QtGui import QStandardItemModel, QStandardItem
from PyQt5.QtCore import Qt,QPersistentModelIndex, QModelIndex
from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QLabel,QMenu)
else:
from PyQt6.QtGui import QStandardItemModel, QStandardItem
from PyQt6.QtCore import Qt,QPersistentModelIndex, QModelIndex
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QLabel,QMenu)
from electrum.i18n import _
from electrum.gui.qt.util import (Buttons,read_QIcon, import_meta_gui, export_meta_gui,MessageBoxMixin)
from electrum.util import write_json_file,read_json_file
from electrum.gui.qt.my_treeview import MyTreeView
from ..bal import BalPlugin
from .. import util as Util
from .. import willexecutors as Willexecutors
from .baldialog import BalDialog,BalBlockingWaitingDialog
from electrum.logging import get_logger,Logger
_logger=get_logger(__name__)
class WillExecutorList(MyTreeView):
class Columns(MyTreeView.BaseColumnsEnum):
SELECTED = enum.auto()
URL = enum.auto()
BASE_FEE = enum.auto()
INFO = enum.auto()
ADDRESS = enum.auto()
STATUS = enum.auto()
headers = {
Columns.SELECTED:_(''),
Columns.URL: _('Url'),
Columns.BASE_FEE: _('Base fee'),
Columns.INFO:_('Info'),
Columns.ADDRESS:_('Default Address'),
Columns.STATUS: _('S'),
}
ROLE_HEIR_KEY = Qt.ItemDataRole.UserRole + 2000
key_role = ROLE_HEIR_KEY
def __init__(self, parent: 'WillExecutorDialog'):
super().__init__(
parent=parent,
stretch_column=self.Columns.ADDRESS,
editable_columns=[self.Columns.URL,self.Columns.BASE_FEE,self.Columns.ADDRESS,self.Columns.INFO],
)
self.parent = parent
self.setModel(QStandardItemModel(self))
self.setSortingEnabled(True)
self.std_model = self.model()
self.config =parent.bal_plugin.config
self.update()
def create_menu(self, position):
menu = QMenu()
idx = self.indexAt(position)
column = idx.column() or self.Columns.URL
selected_keys = []
for s_idx in self.selected_in_column(self.Columns.URL):
sel_key = self.model().itemFromIndex(s_idx).data(0)
selected_keys.append(sel_key)
if selected_keys and idx.isValid():
column_title = self.model().horizontalHeaderItem(column).text()
column_data = '\n'.join(self.model().itemFromIndex(s_idx).text()
for s_idx in self.selected_in_column(column))
if Willexecutors.is_selected(self.parent.willexecutors_list[sel_key]):
menu.addAction(_("deselect").format(column_title), lambda: self.deselect(selected_keys))
else:
menu.addAction(_("select").format(column_title), lambda: self.select(selected_keys))
if column in self.editable_columns:
item = self.model().itemFromIndex(idx)
if item.isEditable():
persistent = QPersistentModelIndex(idx)
menu.addAction(_("Edit {}").format(column_title), lambda p=persistent: self.edit(QModelIndex(p)))
menu.addAction(_("Ping").format(column_title), lambda: self.ping_willexecutors(selected_keys))
menu.addSeparator()
menu.addAction(_("delete").format(column_title), lambda: self.delete(selected_keys))
menu.exec(self.viewport().mapToGlobal(position))
def ping_willexecutors(self,selected_keys):
wout={}
for k in selected_keys:
wout[k]=self.parent.willexecutors_list[k]
self.parent.update_willexecutors(wout)
self.update()
def get_edit_key_from_coordinate(self, row, col):
a= self.get_role_data_from_coordinate(row, col, role=self.ROLE_HEIR_KEY+col)
return a
def delete(self,selected_keys):
for key in selected_keys:
del self.parent.willexecutors_list[key]
self.update()
def select(self,selected_keys):
for wid,w in self.parent.willexecutors_list.items():
if wid in selected_keys:
w['selected']=True
self.update()
def deselect(self,selected_keys):
for wid,w in self.parent.willexecutors_list.items():
if wid in selected_keys:
w['selected']=False
self.update()
def on_edited(self, idx, edit_key, *, text):
prior_name = self.parent.willexecutors_list[edit_key]
col = idx.column()
try:
if col == self.Columns.URL:
self.parent.willexecutors_list[text]=self.parent.willexecutors_list[edit_key]
del self.parent.willexecutors_list[edit_key]
if col == self.Columns.BASE_FEE:
self.parent.willexecutors_list[edit_key]["base_fee"] = Util.encode_amount(text,self.config.get_decimal_point())
if col == self.Columns.ADDRESS:
self.parent.willexecutors_list[edit_key]["address"] = text
if col == self.Columns.INFO:
self.parent.willexecutors_list[edit_key]["info"] = text
self.update()
except Exception as e:
pass
def update(self):
if self.parent.willexecutors_list is None:
return
try:
current_key = self.get_role_data_for_current_item(col=self.Columns.URL, role=self.ROLE_HEIR_KEY)
self.model().clear()
self.update_headers(self.__class__.headers)
set_current = None
for url, value in self.parent.willexecutors_list.items():
labels = [""] * len(self.Columns)
labels[self.Columns.URL] = url
if Willexecutors.is_selected(value):
labels[self.Columns.SELECTED] = [read_QIcon('confirmed.png'),'']
else:
labels[self.Columns.SELECTED] = ''
labels[self.Columns.BASE_FEE] = Util.decode_amount(value.get('base_fee',0),self.config.get_decimal_point())
if str(value.get('status',0)) == "200":
labels[self.Columns.STATUS] = [read_QIcon('status_connected.png'),'']
else:
labels[self.Columns.STATUS] = [read_QIcon('unconfirmed.png'),'']
labels[self.Columns.ADDRESS] = str(value.get('address',''))
labels[self.Columns.INFO] = str(value.get('info',''))
items=[]
for e in labels:
if type(e)== list:
try:
items.append(QStandardItem(*e))
except Exception as e:
pass
else:
items.append(QStandardItem(e))
items[self.Columns.SELECTED].setEditable(False)
items[self.Columns.URL].setEditable(True)
items[self.Columns.ADDRESS].setEditable(True)
items[self.Columns.INFO].setEditable(True)
items[self.Columns.BASE_FEE].setEditable(True)
items[self.Columns.STATUS].setEditable(False)
items[self.Columns.URL].setData(url, self.ROLE_HEIR_KEY+1)
items[self.Columns.BASE_FEE].setData(url, self.ROLE_HEIR_KEY+2)
items[self.Columns.INFO].setData(url, self.ROLE_HEIR_KEY+3)
items[self.Columns.ADDRESS].setData(url, self.ROLE_HEIR_KEY+4)
self.model().insertRow(self.model().rowCount(), items)
if url == current_key:
idx = self.model().index(row_count, self.Columns.NAME)
set_current = QPersistentModelIndex(idx)
self.set_current_idx(set_current)
self.parent.save_willexecutors()
except Exception as e:
_logger.error(e)
class WillExecutorDialog(BalDialog,MessageBoxMixin):
def __init__(self, bal_window):
BalDialog.__init__(self,bal_window.window)
self.bal_plugin = bal_window.bal_plugin
self.gui_object = self.bal_plugin.gui_object
self.config = self.bal_plugin.config
self.window = bal_window.window
self.bal_window = bal_window
self.willexecutors_list = Willexecutors.get_willexecutors(self.bal_plugin)
self.setWindowTitle(_('Will-Executor Service List'))
self.setMinimumSize(1000, 200)
self.size_label = QLabel()
self.willexecutor_list = WillExecutorList(self)
vbox = QVBoxLayout(self)
vbox.addWidget(self.size_label)
vbox.addWidget(self.willexecutor_list)
buttonbox = QHBoxLayout()
b = QPushButton(_('Ping'))
b.clicked.connect(self.update_willexecutors)
buttonbox.addWidget(b)
b = QPushButton(_('Import'))
b.clicked.connect(self.import_file)
buttonbox.addWidget(b)
b = QPushButton(_('Export'))
b.clicked.connect(self.export_file)
buttonbox.addWidget(b)
b = QPushButton(_('Add'))
b.clicked.connect(self.add)
buttonbox.addWidget(b)
b = QPushButton(_('Close'))
b.clicked.connect(self.close)
buttonbox.addWidget(b)
vbox.addLayout(buttonbox)
self.willexecutor_list.update()
def add(self):
self.willexecutors_list["http://localhost:8080"]={"info":"New Will Executor","base_fee":0,"status":"-1"}
self.willexecutor_list.update()
def import_file(self):
import_meta_gui(self, _('willexecutors.json'), self.import_json_file, self.willexecutors_list.update)
def export_file(self, path):
Util.export_meta_gui(self, _('willexecutors.json'), self.export_json_file)
def export_json_file(self,path):
write_json_file(path, self.willexecutors_list)
def update_willexecutors(self,wes=None):
if not wes:
self.willexecutors_list = Willexecutors.get_willexecutors(self.bal_plugin, update = True, bal_window = self.bal_window,force=True)
else:
self.bal_window.ping_willexecutors(wes)
self.willexecutors_list.update(wes)
self.willexecutor_list.update()
def import_json_file(self, path):
data = read_json_file(path)
data = self._validate(data)
self.willexecutors_list.update(data)
self.willexecutor_list.update()
#TODO validate willexecutor json import file
def _validate(self,data):
return data
def is_hidden(self):
return self.isMinimized() or self.isHidden()
def show_or_hide(self):
if self.is_hidden():
self.bring_to_top()
else:
self.hide()
def bring_to_top(self):
self.show()
self.raise_()
def closeEvent(self, event):
event.accept()
def save_willexecutors(self):
self.bal_plugin.config.set_key(self.bal_plugin.WILLEXECUTORS,self.willexecutors_list,save=True)

609
heirs.py Normal file
View File

@ -0,0 +1,609 @@
import re
import json
from typing import Optional, Tuple, Dict, Any, TYPE_CHECKING,Sequence,List
import dns
import threading
import math
from dns.exception import DNSException
from electrum import bitcoin,dnssec,descriptor,constants
from electrum.util import read_json_file, write_json_file, to_string,bfh,trigger_callback
from electrum.logging import Logger, get_logger
from electrum.transaction import PartialTxInput, PartialTxOutput,TxOutpoint,PartialTransaction,TxOutput
import datetime
import urllib.request
import urllib.parse
from .bal import BalPlugin
from . import util as Util
from . import willexecutors as Willexecutors
if TYPE_CHECKING:
from .wallet_db import WalletDB
from .simple_config import SimpleConfig
_logger = get_logger(__name__)
HEIR_ADDRESS = 0
HEIR_AMOUNT = 1
HEIR_LOCKTIME = 2
HEIR_REAL_AMOUNT = 3
TRANSACTION_LABEL = "inheritance transaction"
class AliasNotFoundException(Exception):
pass
def reduce_outputs(in_amount, out_amount, fee, outputs):
if in_amount < out_amount:
for output in outputs:
output.value = math.floor((in_amount-fee)/out_amount * output.value)
#TODO: put this method inside wallet.db to replace or complete get_locktime_for_new_transaction
def get_current_height(network:'Network'):
#if no network or not up to date, just set locktime to zero
if not network:
return 0
chain = network.blockchain()
if chain.is_tip_stale():
return 0
# figure out current block height
chain_height = chain.height() # learnt from all connected servers, SPV-checked
server_height = network.get_server_height() # height claimed by main server, unverified
# note: main server might be lagging (either is slow, is malicious, or there is an SPV-invisible-hard-fork)
# - if it's lagging too much, it is the network's job to switch away
if server_height < chain_height - 10:
# the diff is suspiciously large... give up and use something non-fingerprintable
return 0
# discourage "fee sniping"
height = min(chain_height, server_height)
return height
def prepare_transactions(locktimes, available_utxos, fees, wallet):
available_utxos=sorted(available_utxos, key=lambda x:"{}:{}:{}".format(x.value_sats(),x.prevout.txid,x.prevout.out_idx))
total_used_utxos = []
txsout={}
locktime,_=Util.get_lowest_locktimes(locktimes)
if not locktime:
return
locktime=locktime[0]
heirs = locktimes[locktime]
vero=True
while vero:
vero=False
fee=fees.get(locktime,0)
out_amount = fee
description = ""
outputs = []
paid_heirs = {}
for name,heir in heirs.items():
try:
if len(heir)>HEIR_REAL_AMOUNT:
real_amount = heir[HEIR_REAL_AMOUNT]
out_amount += real_amount
description += f"{name}\n"
paid_heirs[name]=heir
outputs.append(PartialTxOutput.from_address_and_value(heir[HEIR_ADDRESS], real_amount))
else:
pass
except Exception as e:
pass
in_amount = 0.0
used_utxos = []
try:
while utxo := available_utxos.pop():
value = utxo.value_sats()
in_amount += value
used_utxos.append(utxo)
if in_amount > out_amount:
break
except IndexError as e:
pass
if int(in_amount) < int(out_amount):
break
heirsvalue=out_amount
change = get_change_output(wallet, in_amount, out_amount, fee)
if change:
outputs.append(change)
tx = PartialTransaction.from_io(used_utxos, outputs, locktime=Util.parse_locktime_string(locktime,wallet), version=2)
if len(description)>0: tx.description = description[:-1]
else: tx.description = ""
tx.heirsvalue = heirsvalue
tx.set_rbf(True)
tx.remove_signatures()
txid = tx.txid()
if txid is None:
raise Exception("txid is none",tx)
tx.heirs = paid_heirs
tx.my_locktime = locktime
txsout[txid]=tx
if change:
change_idx=tx.get_output_idxs_from_address(change.address)
prevout = TxOutpoint(txid=bfh(tx.txid()), out_idx=change_idx.pop())
txin = PartialTxInput(prevout=prevout)
txin._trusted_value_sats = change.value
txin.script_descriptor = change.script_descriptor
txin.is_mine=True
txin._TxInput__address=change.address
txin._TxInput__scriptpubkey = change.scriptpubkey
txin._TxInput__value_sats = change.value
txin.utxo = tx
available_utxos.append(txin)
txsout[txid].available_utxos = available_utxos[:]
return txsout
def get_utxos_from_inputs(tx_inputs,tx,utxos):
for tx_input in tx_inputs:
prevoutstr=tx_input.prevout.to_str()
utxos[prevoutstr] =utxos.get(prevoutstr,{'input':tx_input,'txs':[]})
utxos[prevoutstr]['txs'].append(tx)
return utxos
#TODO calculate de minimum inputs to be invalidated
def invalidate_inheritance_transactions(wallet):
listids = []
utxos = {}
dtxs = {}
for k,v in wallet.get_all_labels().items():
tx = None
if TRANSACTION_LABEL == v:
tx=wallet.adb.get_transaction(k)
if tx:
dtxs[tx.txid()]=tx
get_utxos_from_inputs(tx.inputs(),tx,utxos)
for key,utxo in utxos.items():
txid=key.split(":")[0]
if txid in dtxs:
for tx in utxo['txs']:
txid =tx.txid()
del dtxs[txid]
utxos = {}
for txid,tx in dtxs.items():
get_utxos_from_inputs(tx.inputs(),tx,utxos)
utxos = sorted(utxos.items(), key = lambda item: len(item[1]))
remaining={}
invalidated = []
for key,value in utxos:
for tx in value['txs']:
txid = tx.txid()
if not txid in invalidated:
invalidated.append(tx.txid())
remaining[key] = value
def print_transaction(heirs,tx,locktimes,tx_fees):
jtx=tx.to_json()
print(f"TX: {tx.txid()}\t-\tLocktime: {jtx['locktime']}")
print(f"---")
for inp in jtx["inputs"]:
print(f"{inp['address']}: {inp['value_sats']}")
print(f"---")
for out in jtx["outputs"]:
heirname=""
for key in heirs.keys():
heir=heirs[key]
if heir[HEIR_ADDRESS] == out['address'] and str(heir[HEIR_LOCKTIME]) == str(jtx['locktime']):
heirname=key
print(f"{heirname}\t{out['address']}: {out['value_sats']}")
print()
size = tx.estimated_size()
print("fee: {}\texpected: {}\tsize: {}".format(tx.input_value()-tx.output_value(), size*tx_fees, size))
print()
try:
print(tx.serialize_to_network())
except:
print("impossible to serialize")
print()
def get_change_output(wallet,in_amount,out_amount,fee):
change_amount = int(in_amount - out_amount - fee)
if change_amount > wallet.dust_threshold():
change_addresses = wallet.get_change_addresses_for_new_transaction()
out = PartialTxOutput.from_address_and_value(change_addresses[0], change_amount)
out.is_change = True
return out
class Heirs(dict, Logger):
def __init__(self, db: 'WalletDB'):
Logger.__init__(self)
self.db = db
d = self.db.get('heirs', {})
try:
self.update(d)
except e as Exception:
return
def invalidate_transactions(self,wallet):
invalidate_inheritance_transactions(wallet)
def save(self):
self.db.put('heirs', dict(self))
def import_file(self, path):
data = read_json_file(path)
data = Heirs._validate(data)
self.update(data)
self.save()
def export_file(self, path):
write_json_file(path, self)
def __setitem__(self, key, value):
dict.__setitem__(self, key, value)
self.save()
def pop(self, key):
if key in self.keys():
res = dict.pop(self, key)
self.save()
return res
def get_locktimes(self,from_locktime, a=False):
locktimes = {}
for key in self.keys():
locktime = Util.parse_locktime_string(self[key][HEIR_LOCKTIME])
if locktime > from_locktime and not a \
or locktime <=from_locktime and a:
locktimes[int(locktime)]=None
return locktimes.keys()
def check_locktime(self):
return False
def normalize_perc(self, heir_list, total_balance, relative_balance,wallet,real=False):
amount = 0
for key,v in heir_list.items():
try:
column = HEIR_AMOUNT
if real: column = HEIR_REAL_AMOUNT
value = int(math.floor(total_balance/relative_balance*self.amount_to_float(v[column])))
if value > wallet.dust_threshold():
heir_list[key].insert(HEIR_REAL_AMOUNT, value)
amount += value
except Exception as e:
raise e
return amount
def amount_to_float(self,amount):
try:
return float(amount)
except:
try:
return float(amount[:-1])
except:
return 0.0
def fixed_percent_lists_amount(self,from_locktime,dust_threshold,reverse = False):
fixed_heirs = {}
fixed_amount = 0.0
percent_heirs= {}
percent_amount = 0.0
for key in self.keys():
try:
cmp= Util.parse_locktime_string(self[key][HEIR_LOCKTIME]) - from_locktime
if cmp<=0:
continue
if Util.is_perc(self[key][HEIR_AMOUNT]):
percent_amount += float(self[key][HEIR_AMOUNT][:-1])
percent_heirs[key] =list(self[key])
else:
heir_amount = int(math.floor(float(self[key][HEIR_AMOUNT])))
if heir_amount>dust_threshold:
fixed_amount += heir_amount
fixed_heirs[key] = list(self[key])
fixed_heirs[key].insert(HEIR_REAL_AMOUNT,heir_amount)
else:
pass
except Exception as e:
_logger.error(e)
return fixed_heirs,fixed_amount,percent_heirs,percent_amount
def prepare_lists(self, balance, total_fees, wallet, willexecutor = False, from_locktime = 0):
willexecutors_amount = 0
willexecutors = {}
heir_list = {}
onlyfixed = False
newbalance = balance - total_fees
locktimes = self.get_locktimes(from_locktime);
if willexecutor:
for locktime in locktimes:
if int(Util.int_locktime(locktime)) > int(from_locktime):
try:
base_fee = int(willexecutor['base_fee'])
willexecutors_amount += base_fee
h = [None] * 4
h[HEIR_AMOUNT] = base_fee
h[HEIR_REAL_AMOUNT] = base_fee
h[HEIR_LOCKTIME] = locktime
h[HEIR_ADDRESS] = willexecutor['address']
willexecutors["w!ll3x3c\""+willexecutor['url']+"\""+str(locktime)] = h
except Exception as e:
return [],False
else:
_logger.error(f"heir excluded from will locktime({locktime}){Util.int_locktime(locktime)}<minimum{from_locktime}"),
heir_list.update(willexecutors)
newbalance -= willexecutors_amount
fixed_heirs,fixed_amount,percent_heirs,percent_amount = self.fixed_percent_lists_amount(from_locktime,wallet.dust_threshold())
if fixed_amount > newbalance:
fixed_amount = self.normalize_perc(fixed_heirs,newbalance,fixed_amount,wallet)
onlyfixed = True
heir_list.update(fixed_heirs)
newbalance -= fixed_amount
if newbalance > 0:
perc_amount = self.normalize_perc(percent_heirs,newbalance,percent_amount,wallet)
newbalance -= perc_amount
heir_list.update(percent_heirs)
if newbalance > 0:
newbalance += fixed_amount
fixed_amount = self.normalize_perc(fixed_heirs,newbalance,fixed_amount,wallet,real=True)
newbalance -= fixed_amount
heir_list.update(fixed_heirs)
heir_list = sorted(heir_list.items(), key = lambda item: Util.parse_locktime_string(item[1][HEIR_LOCKTIME],wallet))
locktimes = {}
for key, value in heir_list:
locktime=Util.parse_locktime_string(value[HEIR_LOCKTIME])
if not locktime in locktimes: locktimes[locktime]={key:value}
else: locktimes[locktime][key]=value
return locktimes, onlyfixed
def is_perc(self,key):
return Util.is_perc(self[key][HEIR_AMOUNT])
def buildTransactions(self,bal_plugin,wallet,tx_fees = None, utxos=None,from_locktime=0):
Heirs._validate(self)
if len(self)<=0:
return
balance = 0.0
len_utxo_set = 0
available_utxos = []
if not utxos:
utxos = wallet.get_utxos()
willexecutors = Willexecutors.get_willexecutors(bal_plugin) or {}
self.decimal_point=bal_plugin.config.get_decimal_point()
no_willexecutors = bal_plugin.config_get(BalPlugin.NO_WILLEXECUTOR)
for utxo in utxos:
if utxo.value_sats()> 0*tx_fees:
balance += utxo.value_sats()
len_utxo_set += 1
available_utxos.append(utxo)
if len_utxo_set==0: return
j=-2
willexecutorsitems = list(willexecutors.items())
willexecutorslen = len(willexecutorsitems)
alltxs = {}
while True:
j+=1
if j >= willexecutorslen:
break
elif 0 <= j:
url, willexecutor = willexecutorsitems[j]
if not Willexecutors.is_selected(willexecutor):
continue
else:
willexecutor['url']=url
elif j == -1:
if not no_willexecutors:
continue
url = willexecutor = False
else:
break
fees = {}
i=0
while True:
txs = {}
redo = False
i+=1
total_fees=0
for fee in fees:
total_fees += int(fees[fee])
newbalance = balance
locktimes, onlyfixed = self.prepare_lists(balance, total_fees, wallet, willexecutor, from_locktime)
try:
txs = prepare_transactions(locktimes, available_utxos[:], fees, wallet)
if not txs:
return {}
except Exception as e:
try:
if "w!ll3x3c" in e.heirname:
Willexecutors.is_selected(willexecutors[w],False)
break
except:
raise e
total_fees = 0
total_fees_real = 0
total_in = 0
for txid,tx in txs.items():
tx.willexecutor = willexecutor
fee = tx.estimated_size() * tx_fees
txs[txid].tx_fees= tx_fees
total_fees += fee
total_fees_real += tx.get_fee()
total_in += tx.input_value()
rfee= tx.input_value()-tx.output_value()
if rfee < fee or rfee > fee + wallet.dust_threshold():
redo = True
oldfees= fees.get(tx.my_locktime,0)
fees[tx.my_locktime]=fee
if balance - total_in > wallet.dust_threshold():
redo = True
if not redo:
break
if i>=10:
break
alltxs.update(txs)
return alltxs
def get_transactions(self,bal_plugin,wallet,tx_fees,utxos=None,from_locktime=0):
txs=self.buildTransactions(bal_plugin,wallet,tx_fees,utxos,from_locktime)
if txs:
temp_txs = {}
for txid in txs:
if txs[txid].available_utxos:
temp_txs.update(self.get_transactions(bal_plugin,wallet,tx_fees,txs[txid].available_utxos,txs[txid].locktime))
txs.update(temp_txs)
return txs
def resolve(self, k):
if bitcoin.is_address(k):
return {
'address': k,
'type': 'address'
}
if k in self.keys():
_type, addr = self[k]
if _type == 'address':
return {
'address': addr,
'type': 'heir'
}
if openalias := self.resolve_openalias(k):
return openalias
raise AliasNotFoundException("Invalid Bitcoin address or alias", k)
@classmethod
def resolve_openalias(cls, url: str) -> Dict[str, Any]:
out = cls._resolve_openalias(url)
if out:
address, name, validated = out
return {
'address': address,
'name': name,
'type': 'openalias',
'validated': validated
}
return {}
def by_name(self, name):
for k in self.keys():
_type, addr = self[k]
if addr.casefold() == name.casefold():
return {
'name': addr,
'type': _type,
'address': k
}
return None
def fetch_openalias(self, config: 'SimpleConfig'):
self.alias_info = None
alias = config.OPENALIAS_ID
if alias:
alias = str(alias)
def f():
self.alias_info = self._resolve_openalias(alias)
trigger_callback('alias_received')
t = threading.Thread(target=f)
t.daemon = True
t.start()
@classmethod
def _resolve_openalias(cls, url: str) -> Optional[Tuple[str, str, bool]]:
# support email-style addresses, per the OA standard
url = url.replace('@', '.')
try:
records, validated = dnssec.query(url, dns.rdatatype.TXT)
except DNSException as e:
_logger.info(f'Error resolving openalias: {repr(e)}')
return None
prefix = 'btc'
for record in records:
string = to_string(record.strings[0], 'utf8')
if string.startswith('oa1:' + prefix):
address = cls.find_regex(string, r'recipient_address=([A-Za-z0-9]+)')
name = cls.find_regex(string, r'recipient_name=([^;]+)')
if not name:
name = address
if not address:
continue
return address, name, validated
@staticmethod
def find_regex(haystack, needle):
regex = re.compile(needle)
try:
return regex.search(haystack).groups()[0]
except AttributeError:
return None
def validate_address(address):
if not bitcoin.is_address(address):
raise NotAnAddress(f"not an address,{address}")
return address
def validate_amount(amount):
try:
if Util.is_perc(amount):
famount = float(amount[:-1])
else:
famount = float(amount)
if famount <= 0.00000001:
raise AmountNotValid(f"amount have to be positive {famount} < 0")
except Exception as e:
raise AmountNotValid(f"amount not properly formatted, {e}")
return amount
def validate_locktime(locktime,timestamp_to_check=False):
try:
locktime = Util.parse_locktime_string(locktime,None)
if timestamp_to_check:
if locktime < timestamp_to_check:
raise HeirExpiredException()
except Exception as e:
raise LocktimeNotValid(f"locktime string not properly formatted, {e}")
return locktime
def validate_heir(k,v,timestamp_to_check=False):
address = Heirs.validate_address(v[HEIR_ADDRESS])
amount = Heirs.validate_amount(v[HEIR_AMOUNT])
locktime = Heirs.validate_locktime(v[HEIR_LOCKTIME],timestamp_to_check)
return (address,amount,locktime)
def _validate(data,timestamp_to_check=False):
for k, v in list(data.items()):
if k == 'heirs':
return Heirs._validate(v)
try:
Heirs.validate_heir(k,v)
except Exception as e:
data.pop(k)
return data
class NotAnAddress(ValueError):
pass
class AmountNotValid(ValueError):
pass
class LocktimeNotValid(ValueError):
pass
class HeirExpiredException(LocktimeNotValid):
pass

BIN
icons/bal16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 538 B

BIN
icons/bal32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
icons/heir.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 871 B

BIN
icons/will.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 831 B

937
qt.py Normal file
View File

@ -0,0 +1,937 @@
'''
Bal
Bitcoin after life
'''
import os
import random
import traceback
from functools import partial
import sys
import copy
import sys
from electrum.plugin import hook
from electrum.i18n import _
from electrum.util import make_dir, InvalidPassword, UserCancelled,resource_path
from electrum.util import bfh, read_json_file,write_json_file,decimal_point_to_base_unit_name,FileImportFailed,FileExportFailed
from electrum.gui.qt.util import (EnterButton, WWLabel,
Buttons, CloseButton, OkButton,import_meta_gui,export_meta_gui,char_width_in_lineedit,CancelButton,HelpButton)
from electrum.gui.qt.qrtextedit import ScanQRTextEdit
from electrum.gui.qt.main_window import StatusBarButton
from electrum.gui.qt.password_dialog import PasswordDialog
from electrum.gui.qt.transaction_dialog import TxDialog
from electrum import constants
from electrum.transaction import Transaction
from .bal import BalPlugin
from .heirs import Heirs
from . import util as Util
from . import will as Will
from .balqt.locktimeedit import HeirsLockTimeEdit
from .balqt.willexecutor_dialog import WillExecutorDialog
from .balqt.preview_dialog import PreviewDialog,PreviewList
from .balqt.heir_list import HeirList
from .balqt.amountedit import PercAmountEdit
from .balqt.willdetail import WillDetailDialog
from .balqt.closedialog import BalCloseDialog
from .balqt import qt_resources
from . import willexecutors as Willexecutors
from electrum.transaction import tx_from_any
import time
from electrum import json_db
from electrum.json_db import StoredDict
import datetime
import urllib.parse
import urllib.request
from typing import TYPE_CHECKING, Callable, Optional, List, Union, Tuple, Mapping
from .balqt.baldialog import BalDialog,BalWaitingDialog,BalBlockingWaitingDialog,bal_checkbox
from electrum.logging import Logger
if qt_resources.QT_VERSION == 5:
from PyQt5.QtCore import Qt, QRectF, QRect, QSizeF, QUrl, QPoint, QSize
from PyQt5.QtGui import (QPixmap, QImage, QBitmap, QPainter, QFontDatabase, QPen, QFont,QIcon,
QColor, QDesktopServices, qRgba, QPainterPath,QPalette)
from PyQt5.QtWidgets import (QGridLayout, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QLineEdit,QCheckBox,QSpinBox,QMenuBar,QMenu,QLineEdit,QScrollArea,QWidget,QSpacerItem,QSizePolicy)
else:
from PyQt6.QtCore import Qt, QRectF, QRect, QSizeF, QUrl, QPoint, QSize
from PyQt6.QtGui import (QPixmap, QImage, QBitmap, QPainter, QFontDatabase, QPen, QFont,QIcon,
QColor, QDesktopServices, qRgba, QPainterPath,QPalette)
from PyQt6.QtWidgets import (QGridLayout, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QLineEdit,QCheckBox,QSpinBox,QMenuBar,QMenu,QLineEdit,QScrollArea,QWidget,QSpacerItem,QSizePolicy)
class Plugin(BalPlugin,Logger):
def __init__(self, parent, config, name):
Logger.__init__(self)
self.logger.info("INIT BALPLUGIN")
BalPlugin.__init__(self, parent, config, name)
self.bal_windows={}
@hook
def init_qt(self,gui_object):
self.logger.info("HOOK init qt")
try:
self.gui_object=gui_object
for window in gui_object.windows:
wallet = window.wallet
if wallet:
window.show_warning(_('Please restart Electrum to activate the BAL plugin'), title=_('Success'))
return
w = BalWindow(self,window)
self.bal_windows[window.winId]= w
for child in window.children():
if isinstance(child,QMenuBar):
for menu_child in child.children():
if isinstance(menu_child,QMenu):
try:
if menu_child.title()==_("&Tools"):
w.init_menubar_tools(menu_child)
except Exception as e:
raise e
self.logger.error(("except:",menu_child.text()))
except Exception as e:
raise e
self.logger.error("Error loading plugini {}".format(e))
@hook
def create_status_bar(self, sb):
self.logger.info("HOOK create status bar")
return
b = StatusBarButton(qt_resources.read_QIcon('bal32x32.png'), "Bal "+_("Bitcoin After Life"),
partial(self.setup_dialog, sb), sb.height())
sb.addPermanentWidget(b)
@hook
def init_menubar_tools(self,window,tools_menu):
self.logger.info("HOOK init_menubar")
w = self.get_window(window)
w.init_menubar_tools(tools_menu)
@hook
def load_wallet(self,wallet, main_window):
self.logger.info("HOOK load wallet")
w = self.get_window(main_window)
w.wallet = wallet
w.init_will()
w.willexecutors = Willexecutors.get_willexecutors(self, update=False, bal_window=w)
w.disable_plugin = False
w.ok=True
@hook
def close_wallet(self,wallet):
for winid,win in self.bal_windows.items():
if win.wallet == wallet:
win.on_close()
def get_window(self,window):
w = self.bal_windows.get(window.winId,None)
if w is None:
w=BalWindow(self,window)
self.bal_windows[window.winId]=w
return w
def requires_settings(self):
return True
def settings_widget(self, window):
w=self.get_window(window.window)
return EnterButton(_('Settings'), partial(w.settings_dialog,window))
def password_dialog(self, msg=None, parent=None):
parent = parent or self
d = PasswordDialog(parent, msg)
return d.run()
def get_seed(self):
password = None
if self.wallet.has_keystore_encryption():
password = self.password_dialog(parent=self.d.parent())
if not password:
raise UserCancelled()
keystore = self.wallet.get_keystore()
if not keystore or not keystore.has_seed():
return
self.extension = bool(keystore.get_passphrase(password))
return keystore.get_seed(password)
class shown_cv():
_type= bool
def __init__(self,value):
self.value=value
def get(self):
return self.value
def set(self,value):
self.value=value
class BalWindow(Logger):
def __init__(self,bal_plugin: 'BalPlugin',window: 'ElectrumWindow'):
Logger.__init__(self)
self.logger.info("loggo tutto")
self.bal_plugin = bal_plugin
self.window = window
self.heirs = {}
self.will = {}
self.willitems = {}
self.willexecutors = {}
self.will_settings = None
self.heirs_tab = self.create_heirs_tab()
self.will_tab = self.create_will_tab()
self.ok= False
self.disable_plugin = True
if self.window.wallet:
self.wallet = self.window.wallet
self.heirs_tab.wallet = self.wallet
self.will_tab.wallet = self.wallet
def init_menubar_tools(self,tools_menu):
self.tools_menu=tools_menu
def add_optional_tab(tabs, tab, icon, description):
tab.tab_icon = icon
tab.tab_description = description
tab.tab_pos = len(tabs)
if tab.is_shown_cv:
tabs.addTab(tab, icon, description.replace("&", ""))
add_optional_tab(self.window.tabs, self.heirs_tab, qt_resources.read_QIcon("heir.png"), _("&Heirs"))
add_optional_tab(self.window.tabs, self.will_tab, qt_resources.read_QIcon("will.png"), _("&Will"))
tools_menu.addSeparator()
self.tools_menu.willexecutors_action = tools_menu.addAction(_("&Will-Executors"), self.show_willexecutor_dialog)
def load_willitems(self):
self.willitems={}
for wid,w in self.will.items():
self.willitems[wid]=Will.WillItem(w,wallet=self.wallet)
if self.willitems:
self.will_list.will=self.willitems
self.will_list.update_will(self.willitems)
self.will_tab.update()
def save_willitems(self):
keys = list(self.will.keys())
for k in keys:
del self.will[k]
for wid,w in self.willitems.items():
self.will[wid]=w.to_dict()
def init_will(self):
self.logger.info("********************init_____will____________**********")
if not self.willexecutors:
self.willexecutors = Willexecutors.get_willexecutors(self.bal_plugin, update=False, bal_window=self)
if not self.heirs:
self.heirs = Heirs._validate(Heirs(self.wallet.db))
if not self.will:
self.will=self.wallet.db.get_dict("will")
if self.will:
self.willitems = {}
try:
self.load_willitems()
except:
self.disable_plugin=True
self.show_warning(_('Please restart Electrum to activate the BAL plugin'), title=_('Success'))
self.close_wallet()
return
if not self.will_settings:
self.will_settings=self.wallet.db.get_dict("will_settings")
self.logger.info("will_settings: {}".format(self.will_settings))
if not self.will_settings:
Util.copy(self.will_settings,self.bal_plugin.default_will_settings())
self.logger.debug("not_will_settings {}".format(self.will_settings))
self.bal_plugin.validate_will_settings(self.will_settings)
self.heir_list.update_will_settings()
def get_window_title(self,title):
return _('BAL - ') + _(title)
def show_willexecutor_dialog(self):
self.willexecutor_dialog = WillExecutorDialog(self)
self.willexecutor_dialog.show()
def create_heirs_tab(self):
self.heir_list = l = HeirList(self)
tab = self.window.create_list_tab(l)
tab.is_shown_cv = shown_cv(True)
return tab
def create_will_tab(self):
self.will_list = l = PreviewList(self,None)
tab = self.window.create_list_tab(l)
tab.is_shown_cv = shown_cv(True)
return tab
def new_heir_dialog(self):
d = BalDialog(self.window, self.get_window_title("New heir"))
vbox = QVBoxLayout(d)
grid = QGridLayout()
heir_name = QLineEdit()
heir_name.setFixedWidth(32 * char_width_in_lineedit())
heir_address = QLineEdit()
heir_address.setFixedWidth(32 * char_width_in_lineedit())
heir_amount = PercAmountEdit(self.window.get_decimal_point)
heir_locktime = HeirsLockTimeEdit(self.window,0)
heir_is_xpub = QCheckBox()
grid.addWidget(QLabel(_("Name")), 1, 0)
grid.addWidget(heir_name, 1, 1)
grid.addWidget(HelpButton("Unique name or description about heir"),1,2)
grid.addWidget(QLabel(_("Address")), 2, 0)
grid.addWidget(heir_address, 2, 1)
grid.addWidget(HelpButton("heir bitcoin address"),2,2)
#grid.addWidget(QLabel(_("xPub")), 2, 2)
grid.addWidget(QLabel(_("Amount")),3,0)
grid.addWidget(heir_amount,3,1)
grid.addWidget(HelpButton("Fixed or Percentage amount if end with %"),3,2)
#grid.addWidget(QLabel(_("LockTime")), 4, 0)
#grid.addWidget(heir_locktime, 4, 1)
#grid.addWidget(HelpButton("if you choose Raw, you can insert various options based on suffix:\n "
# +" - b: number of blocks after current block(ex: 144b means tomorrow)\n"
# +" - d: number of days after current day(ex: 1d means tomorrow)\n"
# +" - y: number of years after currrent day(ex: 1y means one year from today)\n\n"
# +"when using d or y time will be set to 00:00 for privacy reasons\n"
# +"when used without suffix it can be used to indicate:\n"
# +" - exact block(if value is less than 500,000,000)\n"
# +" - exact block timestamp(if value greater than 500,000,000"),4,2)
vbox.addLayout(grid)
vbox.addLayout(Buttons(CancelButton(d), OkButton(d)))
while d.exec():
#TODO SAVE HEIR
heir = [
heir_name.text(),
heir_address.text(),
Util.encode_amount(heir_amount.text(),self.bal_plugin.config.get_decimal_point()),
str(heir_locktime.get_locktime()),
]
try:
self.set_heir(heir)
break
except Exception as e:
self.show_error(str(e))
def export_inheritance_handler(self,path):
txs = self.build_inheritance_transaction(ignore_duplicate=True, keep_original=False)
with open(path,"w") as f:
for tx in txs:
tx['status']+="."+BalPlugin.STATUS_EXPORTED
f.write(str(tx['tx']))
f.write('\n')
def set_heir(self,heir):
heir=list(heir)
heir[3]=self.will_settings['locktime']
h=Heirs.validate_heir(heir[0],heir[1:])
self.heirs[heir[0]]=h
self.heir_list.update()
return True
def delete_heirs(self,heirs):
for heir in heirs:
del self.heirs[heir]
self.heirs.save()
self.heir_list.update()
return True
def import_heirs(self,):
import_meta_gui(self.window, _('heirs'), self.heirs.import_file, self.heir_list.update)
def export_heirs(self):
Util.export_meta_gui(self.window, _('heirs'), self.heirs.export_file)
def prepare_will(self, ignore_duplicate = False, keep_original = False):
will = self.build_inheritance_transaction(ignore_duplicate = ignore_duplicate, keep_original=keep_original)
return will
def delete_not_valid(self,txid,s_utxo):
raise NotImplementedError()
def update_will(self,will):
Will.update_will(self.willitems,will)
self.willitems.update(will)
Will.normalize_will(self.willitems,self.wallet)
def build_will(self, ignore_duplicate = True, keep_original = True ):
will = {}
willtodelete=[]
willtoappend={}
try:
self.init_class_variables()
self.willexecutors = Willexecutors.get_willexecutors(self.bal_plugin, update=False, bal_window=self)
if not self.no_willexecutor:
f=False
for u,w in self.willexecutors.items():
if Willexecutors.is_selected(w):
f=True
if not f:
raise Will.NoWillExecutorNotPresent("No Will-Executor or backup transaction selected")
txs = self.heirs.get_transactions(self.bal_plugin,self.window.wallet,self.will_settings['tx_fees'],None,self.date_to_check)
self.logger.info(txs)
creation_time = time.time()
if txs:
for txid in txs:
txtodelete=[]
_break = False
tx = {}
tx['tx'] = txs[txid]
tx['my_locktime'] = txs[txid].my_locktime
tx['heirsvalue'] = txs[txid].heirsvalue
tx['description'] = txs[txid].description
tx['willexecutor'] = copy.deepcopy(txs[txid].willexecutor)
tx['status'] = _("New")
tx['tx_fees'] = txs[txid].tx_fees
tx['time'] = creation_time
tx['heirs'] = copy.deepcopy(txs[txid].heirs)
tx['txchildren'] = []
will[txid]=Will.WillItem(tx,_id=txid,wallet=self.wallet)
self.update_will(will)
except Exception as e:
raise e
pass
return self.willitems
def check_will(self):
return Will.is_will_valid(self.willitems, self.block_to_check, self.date_to_check, self.will_settings['tx_fees'],self.window.wallet.get_utxos(),heirs=self.heirs,willexecutors=self.willexecutors ,self_willexecutor=self.no_willexecutor, wallet = self.wallet, callback_not_valid_tx=self.delete_not_valid)
def show_message(self,text):
self.window.show_message(text)
def show_warning(self,text,parent =None):
self.window.show_warning(text, parent= None)
def show_error(self,text):
self.window.show_error(text)
def show_critical(self,text):
self.window.show_critical(text)
def init_heirs_to_locktime(self):
for heir in self.heirs:
h=self.heirs[heir]
self.heirs[heir]=[h[0],h[1],self.will_settings['locktime']]
def init_class_variables(self):
if not self.heirs:
raise Will.NoHeirsException()
return
try:
self.date_to_check = Util.parse_locktime_string(self.will_settings['threshold'])
found = False
self.locktime_blocks=self.bal_plugin.config_get(BalPlugin.LOCKTIME_BLOCKS)
self.current_block = Util.get_current_height(self.wallet.network)
self.block_to_check = self.current_block + self.locktime_blocks
self.no_willexecutor = self.bal_plugin.config_get(BalPlugin.NO_WILLEXECUTOR)
self.willexecutors = Willexecutors.get_willexecutors(self.bal_plugin,update=True,bal_window=self,task=False)
self.init_heirs_to_locktime()
except Exception as e:
self.logger.error(e)
raise e
def build_inheritance_transaction(self,ignore_duplicate = True, keep_original = True):
try:
if self.disable_plugin:
self.logger.info("plugin is disabled")
return
if not self.heirs:
self.logger.warning("not heirs {}".format(self.heirs))
return
self.init_class_variables()
try:
Will.check_amounts(self.heirs,self.willexecutors,self.window.wallet.get_utxos(),self.date_to_check,self.window.wallet.dust_threshold())
except Will.AmountException as e:
self.show_warning(_(f"In the inheritance process, the entire wallet will always be fully emptied. Your settings require an adjustment of the amounts.\n{e}"))
locktime = Util.parse_locktime_string(self.will_settings['locktime'])
if locktime < self.date_to_check:
self.show_error(_("locktime is lower than threshold"))
return
if not self.no_willexecutor:
f=False
for k,we in self.willexecutors.items():
if Willexecutors.is_selected(we):
f=True
if not f:
self.show_error(_(" no backup transaction or willexecutor selected"))
return
try:
self.check_will()
except Will.WillExpiredException as e:
self.invalidate_will()
return
except Will.NoHeirsException:
return
except Will.NotCompleteWillException as e:
self.logger.info("{}:{}".format(type(e),e))
message = False
if isinstance(e,Will.HeirChangeException):
message ="Heirs changed:"
elif isinstance(e,Will.WillExecutorNotPresent):
message = "Will-Executor not present:"
elif isinstance(e,Will.WillexecutorChangeException):
message = "Will-Executor changed"
elif isinstance(e,Will.TxFeesChangedException):
message = "Txfees are changed"
elif isinstance(e,Will.HeirNotFoundException):
message = "Heir not found"
if message:
self.show_message(f"{_(message)}:\n {e}\n{_('will have to be built')}")
self.logger.info("build will")
self.build_will(ignore_duplicate,keep_original)
try:
self.check_will()
for wid,w in self.willitems.items():
self.wallet.set_label(wid,"BAL Transaction")
except Will.WillExpiredException as e:
self.invalidate_will()
except Will.NotCompleteWillException as e:
self.show_error("Error:{}\n {}".format(str(e),_("Please, check your heirs, locktime and threshold!")))
self.window.history_list.update()
self.window.utxo_list.update()
self.update_all()
return self.willitems
except Exception as e:
raise e
def show_transaction_real(
self,
tx: Transaction,
*,
parent: 'ElectrumWindow',
prompt_if_unsaved: bool = False,
external_keypairs: Mapping[bytes, bytes] = None,
payment_identifier: 'PaymentIdentifier' = None,
):
try:
d = TxDialog(
tx,
parent=parent,
prompt_if_unsaved=prompt_if_unsaved,
external_keypairs=external_keypairs,
payment_identifier=payment_identifier,
)
d.setWindowIcon(qt_resources.read_QIcon("bal32x32.png"))
except SerializationError as e:
self.logger.error('unable to deserialize the transaction')
parent.show_critical(_("Electrum was unable to deserialize the transaction:") + "\n" + str(e))
else:
d.show()
return d
def show_transaction(self,tx=None,txid=None,parent = None):
if not parent:
parent = self.window
if txid !=None and txid in self.willitems:
tx=self.willitems[txid].tx
if not tx:
raise Exception(_("no tx"))
return self.show_transaction_real(tx,parent=parent)
def invalidate_will(self):
def on_success(result):
if result:
self.show_message(_("Please sign and broadcast this transaction to invalidate current will"))
self.wallet.set_label(result.txid(),"BAL Invalidate")
a=self.show_transaction(result)
else:
self.show_message(_("No transactions to invalidate"))
def on_failure(exec_info):
self.show_error(f"ERROR:{exec_info}")
fee_per_byte=self.will_settings.get('tx_fees',1)
task = partial(Will.invalidate_will,self.willitems,self.wallet,fee_per_byte)
msg = _("Calculating Transactions")
self.waiting_dialog = BalWaitingDialog(self, msg, task, on_success, on_failure,exe=False)
self.waiting_dialog.exe()
def sign_transactions(self,password):
try:
txs={}
signed = None
tosign = None
def get_message():
msg = ""
if signed:
msg=_(f"signed: {signed}\n")
return msg + _(f"signing: {tosign}")
for txid in Will.only_valid(self.willitems):
wi = self.willitems[txid]
tx = copy.deepcopy(wi.tx)
if wi.get_status('COMPLETE'):
txs[txid]=tx
continue
tosign=txid
try:
self.waiting_dialog.update(get_message())
except:pass
for txin in tx.inputs():
prevout = txin.prevout.to_json()
if prevout[0] in self.willitems:
change = self.willitems[prevout[0]].tx.outputs()[prevout[1]]
txin._trusted_value_sats = change.value
try:
txin.script_descriptor = change.script_descriptor
except:
pass
txin.is_mine=True
txin._TxInput__address=change.address
txin._TxInput__scriptpubkey = change.scriptpubkey
txin._TxInput__value_sats = change.value
self.wallet.sign_transaction(tx, password,ignore_warnings=True)
signed=tosign
is_complete=False
if tx.is_complete():
is_complete=True
wi.set_status('COMPLETE',True)
txs[txid]=tx
except Exception as e:
return None
return txs
def get_wallet_password(self,message=None,parent=None):
parent =self.window if not parent else parent
password = None
if self.wallet.has_keystore_encryption():
password = self.bal_plugin.password_dialog(parent=parent,msg=message)
if password is None:
return False
try:
self.wallet.check_password(password)
except Exception as e:
self.show_error(str(e))
password = self.get_wallet_password(message)
return password
def on_close(self):
try:
if not self.disable_plugin:
close_window=BalCloseDialog(self)
close_window.close_plugin_task()
self.save_willitems()
self.heirs_tab.close()
self.will_tab.close()
self.tools_menu.removeAction(self.tools_menu.willexecutors_action)
self.window.toggle_tab(self.heirs_tab)
self.window.toggle_tab(self.will_tab)
self.window.tabs.update()
except Exception as e:
pass
def ask_password_and_sign_transactions(self,callback=None):
def on_success(txs):
if txs:
for txid,tx in txs.items():
self.willitems[txid].tx=copy.deepcopy(tx)
self.will[txid]=self.willitems[txid].to_dict()
try:
self.will_list.update()
except:
pass
if callback:
try:
callback()
except Exception as e:
raise e
def on_failure(exc_info):
self.logger.info("sign fail: {}".format(exc_info))
self.show_error(exc_info)
password= self.get_wallet_password()
task = partial(self.sign_transactions,password)
msg = _('Signing transactions...')
self.waiting_dialog = BalWaitingDialog(self, msg, task, on_success, on_failure,exe=False)
self.waiting_dialog.exe()
def broadcast_transactions(self,force=False):
def on_success(sulcess):
self.will_list.update()
if sulcess:
self.logger.info("error, some transaction was not sent");
self.show_warning(_("Some transaction was not broadcasted"))
return
self.logger.debug("OK, sulcess transaction was sent")
self.show_message(_("All transactions are broadcasted to respective Will-Executors"))
def on_failure(err):
self.logger.error(err)
task = partial(self.push_transactions_to_willexecutors,force)
msg = _('Selecting Will-Executors')
self.waiting_dialog = BalWaitingDialog(self,msg,task,on_success,on_failure,exe=False)
self.waiting_dialog.exe()
def push_transactions_to_willexecutors(self,force=False):
willexecutors = Willexecutors.get_willexecutor_transactions(self.willitems)
def getMsg(willexecutors):
msg = "Broadcasting Transactions to Will-Executors:\n"
for url in willexecutors:
msg += f"{url}:\t{willexecutors[url]['broadcast_status']}\n"
return msg
error=False
for url in willexecutors:
willexecutor = willexecutors[url]
self.waiting_dialog.update(getMsg(willexecutors))
if 'txs' in willexecutor:
try:
if Willexecutors.push_transactions_to_willexecutor(willexecutors[url]):
for wid in willexecutors[url]['txsids']:
self.willitems[wid].set_status('PUSHED', True)
willexecutors[url]['broadcast_stauts'] = _("Success")
else:
for wid in willexecutors[url]['txsids']:
self.willitems[wid].set_status('PUSH_FAIL', True)
error=True
willexecutors[url]['broadcast_stauts'] = _("Failed")
del willexecutor['txs']
except Willexecutors.AlreadyPresentException:
for wid in willexecutor['txsids']:
row = self.waiting_dialog.update("checking {} - {} : {}".format(self.willitems[wid].we['url'],wid, "Waiting"))
self.willitems[wid].check_willexecutor()
row = self.waiting_dialog.update("checked {} - {} : {}".format(self.willitems[wid].we['url'],wid,self.willitems[wid].get_status("CHECKED" )))
if error:
return True
def export_json_file(self,path):
for wid in self.willitems:
self.willitems[wid].set_status('EXPORTED', True)
self.will[wid]=self.willitems[wid].to_dict()
write_json_file(path, self.will)
def export_will(self):
try:
Util.export_meta_gui(self.window, _('will.json'), self.export_json_file)
except Exception as e:
self.show_error(str(e))
raise e
def import_will(self):
def sulcess():
self.will_list.update_will(self.willitems)
import_meta_gui(self.window, _('will'), self.import_json_file,sulcess)
def import_json_file(self,path):
try:
data = read_json_file(path)
willitems = {}
for k,v in data.items():
data[k]['tx']=tx_from_any(v['tx'])
willitems[k]=Will.WillItem(data[k],_id=k)
self.update_will(willitems)
except Exception as e:
raise e
raise FileImportFailed(_("Invalid will file"))
def check_transactions_task(self,will):
start = time.time()
for wid,w in will.items():
if w.we:
self.waiting_dialog.update("checking transaction: {}\n willexecutor: {}".format(wid,w.we['url']))
w.check_willexecutor()
if time.time()-start < 3:
time.sleep(3-(time.time()-start))
def check_transactions(self,will):
def on_success(result):
del self.waiting_dialog
self.update_all()
pass
def on_failure(e):
self.logger.error(f"error checking transactions {e}")
pass
task = partial(self.check_transactions_task,will)
msg = _('Check Transaction')
self.waiting_dialog = BalWaitingDialog(self,msg,task,on_success,on_failure,exe=False)
self.waiting_dialog.exe()
def ping_willexecutors_task(self,wes):
self.logger.info("ping willexecutots task")
pinged = []
failed = []
def get_title():
msg = _('Ping Will-Executors:')
msg += "\n\n"
for url in wes:
urlstr = "{:<50}: ".format(url[:50])
if url in pinged:
urlstr += "Ok"
elif url in failed:
urlstr +="Ko"
else:
urlstr += "--"
urlstr+="\n"
msg+=urlstr
return msg
for url,we in wes.items():
try:
self.waiting_dialog.update(get_title())
except:
pass
wes[url]=Willexecutors.get_info_task(url,we)
if wes[url]['status']=='KO':
failed.append(url)
else:
pinged.append(url)
def ping_willexecutors(self,wes):
def on_success(result):
del self.waiting_dialog
try:
self.willexecutor_dialog.willexecutor_list.update()
except Exception as e:
_logger.error("error updating willexecutors {e}")
pass
def on_failure(e):
self.logger.error(e)
pass
self.logger.info("ping willexecutors")
task = partial(self.ping_willexecutors_task,wes)
msg = _('Ping Will-Executors')
self.waiting_dialog = BalWaitingDialog(self,msg,task,on_success,on_failure,exe=False)
self.waiting_dialog.exe()
def preview_modal_dialog(self):
self.dw=WillDetailDialog(self)
self.dw.show()
def settings_dialog(self,window):
d = BalDialog(window, self.get_window_title("Settings"))
d.setMinimumSize(100, 200)
qicon=qt_resources.read_QPixmap("bal32x32.png")
lbl_logo = QLabel()
lbl_logo.setPixmap(qicon)
heir_locktime_time = QSpinBox()
heir_locktime_time.setMinimum(0)
heir_locktime_time.setMaximum(3650)
heir_locktime_time.setValue(int(self.bal_plugin.config_get(BalPlugin.LOCKTIME_TIME)))
def on_heir_locktime_time():
value = heir_locktime_time.value()
self.bal_plugin.config.set_key(BalPlugin.LOCKTIME_TIME,value,save=True)
heir_locktime_time.valueChanged.connect(on_heir_locktime_time)
heir_locktimedelta_time = QSpinBox()
heir_locktimedelta_time.setMinimum(0)
heir_locktimedelta_time.setMaximum(3650)
heir_locktimedelta_time.setValue(int(self.bal_plugin.config_get(BalPlugin.LOCKTIMEDELTA_TIME)))
def on_heir_locktime_time():
value = heir_locktime_time.value
self.bal_plugin.config.set_key(BalPlugin.LOCKTIME_TIME,value,save=True)
heir_locktime_time.valueChanged.connect(on_heir_locktime_time)
heir_locktime_blocks = QSpinBox()
heir_locktime_blocks.setMinimum(0)
heir_locktime_blocks.setMaximum(144*3650)
heir_locktime_blocks.setValue(int(self.bal_plugin.config_get(BalPlugin.LOCKTIME_BLOCKS)))
def on_heir_locktime_blocks():
value = heir_locktime_blocks.value()
self.bal_plugin.config.set_key(BalPlugin.LOCKTIME_BLOCKS,value,save=True)
heir_locktime_blocks.valueChanged.connect(on_heir_locktime_blocks)
heir_locktimedelta_blocks = QSpinBox()
heir_locktimedelta_blocks.setMinimum(0)
heir_locktimedelta_blocks.setMaximum(144*3650)
heir_locktimedelta_blocks.setValue(int(self.bal_plugin.config_get(BalPlugin.LOCKTIMEDELTA_BLOCKS)))
def on_heir_locktimedelta_blocks():
value = heir_locktimedelta_blocks.value()
self.bal_plugin.config.set_key(BalPlugin.LOCKTIMEDELTA_TIME,value,save=True)
heir_locktimedelta_blocks.valueChanged.connect(on_heir_locktimedelta_blocks)
heir_tx_fees = QSpinBox()
heir_tx_fees.setMinimum(1)
heir_tx_fees.setMaximum(10000)
heir_tx_fees.setValue(int(self.bal_plugin.config_get(BalPlugin.TX_FEES)))
def on_heir_tx_fees():
value = heir_tx_fees.value()
self.bal_plugin.config.set_key(BalPlugin.TX_FEES,value,save=True)
heir_tx_fees.valueChanged.connect(on_heir_tx_fees)
heir_broadcast = bal_checkbox(self.bal_plugin, BalPlugin.BROADCAST)
heir_ask_broadcast = bal_checkbox(self.bal_plugin, BalPlugin.ASK_BROADCAST)
heir_invalidate = bal_checkbox(self.bal_plugin, BalPlugin.INVALIDATE)
heir_ask_invalidate = bal_checkbox(self.bal_plugin, BalPlugin.ASK_INVALIDATE)
heir_preview = bal_checkbox(self.bal_plugin, BalPlugin.PREVIEW)
heir_ping_willexecutors = bal_checkbox(self.bal_plugin, BalPlugin.PING_WILLEXECUTORS)
heir_ask_ping_willexecutors = bal_checkbox(self.bal_plugin, BalPlugin.ASK_PING_WILLEXECUTORS)
heir_no_willexecutor = bal_checkbox(self.bal_plugin, BalPlugin.NO_WILLEXECUTOR)
heir_hide_replaced = bal_checkbox(self.bal_plugin,BalPlugin.HIDE_REPLACED,self)
heir_hide_invalidated = bal_checkbox(self.bal_plugin,BalPlugin.HIDE_INVALIDATED,self)
heir_allow_repush = bal_checkbox(self.bal_plugin,BalPlugin.ALLOW_REPUSH,self)
heir_repush = QPushButton("Rebroadcast transactions")
heir_repush.clicked.connect(partial(self.broadcast_transactions,True))
grid=QGridLayout(d)
#add_widget(grid,"Refresh Time Days",heir_locktime_time,0,"Delta days for inputs to be invalidated and transactions resubmitted")
#add_widget(grid,"Refresh Blocks",heir_locktime_blocks,1,"Delta blocks for inputs to be invalidated and transaction resubmitted")
#add_widget(grid,"Transaction fees",heir_tx_fees,1,"Default transaction fees")
#add_widget(grid,"Broadcast transactions",heir_broadcast,3,"")
#add_widget(grid," - Ask before",heir_ask_broadcast,4,"")
#add_widget(grid,"Invalidate transactions",heir_invalidate,5,"")
#add_widget(grid," - Ask before",heir_ask_invalidate,6,"")
#add_widget(grid,"Show preview before sign",heir_preview,7,"")
#grid.addWidget(lbl_logo,0,0)
add_widget(grid,"Hide Replaced",heir_hide_replaced, 1, "Hide replaced transactions from will detail and list")
add_widget(grid,"Hide Invalidated",heir_hide_invalidated ,2,"Hide invalidated transactions from will detail and list")
add_widget(grid,"Ping Willexecutors",heir_ping_willexecutors,3,"Ping willexecutors to get payment info before compiling will")
add_widget(grid," - Ask before",heir_ask_ping_willexecutors,4,"Ask before to ping willexecutor")
add_widget(grid,"Backup Transaction",heir_no_willexecutor,5,"Add transactions without willexecutor")
grid.addWidget(heir_repush,6,0)
grid.addWidget(HelpButton("Broadcast all transactions to willexecutors including those already pushed"),6,2)
#add_widget(grid,"Max Allowed TimeDelta Days",heir_locktimedelta_time,8,"")
#add_widget(grid,"Max Allowed BlocksDelta",heir_locktimedelta_blocks,9,"")
if ret := bool(d.exec()):
try:
self.update_all()
return ret
except:
pass
return False
def update_all(self):
self.will_list.update_will(self.willitems)
self.heirs_tab.update()
self.will_tab.update()
self.will_list.update()
def add_widget(grid,label,widget,row,help_):
grid.addWidget(QLabel(_(label)),row,0)
grid.addWidget(widget,row,1)
grid.addWidget(HelpButton(help_),row,2)

458
util.py Normal file
View File

@ -0,0 +1,458 @@
from datetime import datetime,timedelta
import bisect
from electrum.gui.qt.util import getSaveFileName
from electrum.i18n import _
from electrum.transaction import PartialTxOutput
import urllib.request
import urllib.parse
from electrum.util import write_json_file,FileImportFailed,FileExportFailed
LOCKTIME_THRESHOLD = 500000000
def locktime_to_str(locktime):
try:
locktime=int(locktime)
if locktime > LOCKTIME_THRESHOLD:
dt = datetime.fromtimestamp(locktime).isoformat()
return dt
except Exception as e:
#print(e)
pass
return str(locktime)
def str_to_locktime(locktime):
try:
if locktime[-1] in ('y','d','b'):
return locktime
else: return int(locktime)
except Exception as e:
pass
#print(e)
dt_object = datetime.fromisoformat(locktime)
timestamp = dt_object.timestamp()
return int(timestamp)
def parse_locktime_string(locktime,w=None):
try:
return int(locktime)
except Exception as e:
pass
#print("parse_locktime_string",e)
try:
now = datetime.now()
if locktime[-1] == 'y':
locktime = str(int(locktime[:-1])*365) + "d"
if locktime[-1] == 'd':
return int((now + timedelta(days = int(locktime[:-1]))).replace(hour=0,minute=0,second=0,microsecond=0).timestamp())
if locktime[-1] == 'b':
locktime = int(locktime[:-1])
height = 0
if w:
height = get_current_height(w.network)
locktime+=int(height)
return int(locktime)
except Exception as e:
print("parse_locktime_string",e)
#raise e
return 0
def int_locktime(seconds=0,minutes=0,hours=0, days=0, blocks = 0):
return int(seconds + minutes*60 + hours*60*60 + days*60*60*24 + blocks * 600)
def encode_amount(amount, decimal_point):
if is_perc(amount):
return amount
else:
try:
return int(float(amount)*pow(10,decimal_point))
except:
return 0
def decode_amount(amount,decimal_point):
if is_perc(amount):
return amount
else:
num=8-decimal_point
basestr="{{:0{}.{}f}}".format(num,num)
return "{:08.8f}".format(float(amount)/pow(10,decimal_point))
def is_perc(value):
try:
return value[-1] == '%'
except:
return False
def cmp_array(heira,heirb):
try:
if not len(heira) == len(heirb):
return False
for h in range(0,len(heira)):
if not heira[h] == heirb[h]:
return False
return True
except:
return False
def cmp_heir(heira,heirb):
if heira[0] == heirb[0] and heira[1] == heirb[1]:
return True
return False
def cmp_willexecutor(willexecutora,willexecutorb):
if willexecutora == willexecutorb:
return True
try:
if willexecutora['url']==willexecutorb['url'] and willexecutora['address'] == willexecutorb['address'] and willexecutora['base_fee']==willexecutorb['base_fee']:
return True
except:
return False
return False
def search_heir_by_values(heirs,heir,values):
#print()
for h,v in heirs.items():
found = False
for val in values:
if val in v and v[val] != heir[val]:
found = True
if not found:
return h
return False
def cmp_heir_by_values(heira,heirb,values):
for v in values:
if heira[v] != heirb[v]:
return False
return True
def cmp_heirs_by_values(heirsa,heirsb,values,exclude_willexecutors=False,reverse = True):
for heira in heirsa:
if (exclude_willexecutors and not "w!ll3x3c\"" in heira) or not exclude_willexecutors:
found = False
for heirb in heirsb:
if cmp_heir_by_values(heirsa[heira],heirsb[heirb],values):
found=True
if not found:
#print(f"not_found {heira}--{heirsa[heira]}")
return False
if reverse:
return cmp_heirs_by_values(heirsb,heirsa,values,exclude_willexecutors=exclude_willexecutors,reverse=False)
else:
return True
def cmp_heirs(heirsa,heirsb,cmp_function = lambda x,y: x[0]==y[0] and x[3]==y[3],reverse=True):
try:
for heir in heirsa:
if not "w!ll3x3c\"" in heir:
if not heir in heirsb or not cmp_function(heirsa[heir],heirsb[heir]):
if not search_heir_by_values(heirsb,heirsa[heir],[0,3]):
return False
if reverse:
return cmp_heirs(heirsb,heirsa,cmp_function,False)
else:
return True
except Exception as e:
raise e
return False
def cmp_inputs(inputsa,inputsb):
if len(inputsa) != len(inputsb):
return False
for inputa in inputsa:
if not in_utxo(inputa,inputsb):
return False
return True
def cmp_outputs(outputsa,outputsb,willexecutor_output = None):
if len(outputsa) != len(outputsb):
return False
for outputa in outputsa:
if not cmp_output(outputa,willexecutor_output):
if not in_output(outputa,outputsb):
return False
return True
def cmp_txs(txa,txb):
if not cmp_inputs(txa.inputs(),txb.inputs()):
return False
if not cmp_outputs(txa.outputs(),txb.outputs()):
return False
return True
def get_value_amount(txa,txb):
outputsa=txa.outputs()
outputsb=txb.outputs()
value_amount = 0
#if len(outputsa) != len(outputsb):
# print("outputlen is different")
# return False
for outa in outputsa:
same_amount,same_address = in_output(outa,txb.outputs())
if not (same_amount or same_address):
#print("outa notin txb", same_amount,same_address)
return False
if same_amount and same_address:
value_amount+=outa.value
if same_amount:
pass
#print("same amount")
if same_address:
pass
#print("same address")
return value_amount
#not needed
#for outb in outputsb:
# if not in_output(outb,txa.outputs()):
# print("outb notin txb")
# return False
def chk_locktime(timestamp_to_check,block_height_to_check,locktime):
#TODO BUG: WHAT HAPPEN AT THRESHOLD?
locktime=int(locktime)
if locktime > LOCKTIME_THRESHOLD and locktime > timestamp_to_check:
return True
elif locktime < LOCKTIME_THRESHOLD and locktime > block_height_to_check:
return True
else:
return False
def anticipate_locktime(locktime,blocks=0,hours=0,days=0):
locktime = int(locktime)
out=0
if locktime> LOCKTIME_THRESHOLD:
seconds = blocks*600 + hours*3600 + days*86400
dt = datetime.fromtimestamp(locktime)
dt -= timedelta(seconds=seconds)
out = dt.timestamp()
else:
blocks -= hours*6 + days*144
out = locktime + blocks
if out < 1:
out = 1
return out
def cmp_locktime(locktimea,locktimeb):
if locktimea==locktimeb:
return 0
strlocktime = str(locktimea)
strlocktimeb = str(locktimeb)
intlocktimea = str_to_locktime(strlocktimea)
intlocktimeb = str_to_locktime(strlocktimeb)
if locktimea[-1] in "ydb":
if locktimeb[-1] == locktimea[-1]:
return int(strlocktimea[-1])-int(strlocktimeb[-1])
else:
return int(locktimea)-(locktimeb)
def get_lowest_valid_tx(available_utxos,will):
will = sorted(will.items(),key = lambda x: x[1]['tx'].locktime)
for txid,willitem in will.items():
pass
def get_locktimes(will):
locktimes = {}
for txid,willitem in will.items():
locktimes[willitem['tx'].locktime]=True
return locktimes.keys()
def get_lowest_locktimes(locktimes):
sorted_timestamp=[]
sorted_block=[]
for l in locktimes:
#print("locktime:",parse_locktime_string(l))
l=parse_locktime_string(l)
if l < LOCKTIME_THRESHOLD:
bisect.insort(sorted_block,l)
else:
bisect.insort(sorted_timestamp,l)
return sorted(sorted_timestamp), sorted(sorted_block)
def get_lowest_locktimes_from_will(will):
return get_lowest_locktimes(get_locktimes(will))
def search_willtx_per_io(will,tx):
for wid, w in will.items():
if cmp_txs(w['tx'],tx['tx']):
return wid,w
return None, None
def invalidate_will(will):
raise Exception("not implemented")
def get_will_spent_utxos(will):
utxos=[]
for txid,willitem in will.items():
utxos+=willitem['tx'].inputs()
return utxos
def utxo_to_str(utxo):
try: return utxo.to_str()
except Exception as e: pass
try: return utxo.prevout.to_str()
except Exception as e: pass
return str(utxo)
def cmp_utxo(utxoa,utxob):
utxoa=utxo_to_str(utxoa)
utxob=utxo_to_str(utxob)
if utxoa == utxob:
#if utxoa.prevout.txid==utxob.prevout.txid and utxoa.prevout.out_idx == utxob.prevout.out_idx:
return True
else:
return False
def in_utxo(utxo, utxos):
for s_u in utxos:
if cmp_utxo(s_u,utxo):
return True
return False
def txid_in_utxo(txid,utxos):
for s_u in utxos:
if s_u.prevout.txid == txid:
return True
return False
def cmp_output(outputa,outputb):
return outputa.address == outputb.address and outputa.value == outputb.value
def in_output(output,outputs):
for s_o in outputs:
if cmp_output(s_o,output):
return True
return False
#check all output with the same amount if none have the same address it can be a change
#return true true same address same amount
#return true false same amount different address
#return false false different amount, different address not found
def din_output(out,outputs):
same_amount=[]
for s_o in outputs:
if int(out.value) == int(s_o.value):
same_amount.append(s_o)
if out.address==s_o.address:
#print("SAME_:",out.address,s_o.address)
return True, True
else:
pass
#print("NOT SAME_:",out.address,s_o.address)
if len(same_amount)>0:
return True, False
else:return False, False
def get_change_output(wallet,in_amount,out_amount,fee):
change_amount = int(in_amount - out_amount - fee)
if change_amount > wallet.dust_threshold():
change_addresses = wallet.get_change_addresses_for_new_transaction()
out = PartialTxOutput.from_address_and_value(change_addresses[0], change_amount)
out.is_change = True
return out
def get_current_height(network:'Network'):
#if no network or not up to date, just set locktime to zero
if not network:
return 0
chain = network.blockchain()
if chain.is_tip_stale():
return 0
# figure out current block height
chain_height = chain.height() # learnt from all connected servers, SPV-checked
server_height = network.get_server_height() # height claimed by main server, unverified
# note: main server might be lagging (either is slow, is malicious, or there is an SPV-invisible-hard-fork)
# - if it's lagging too much, it is the network's job to switch away
if server_height < chain_height - 10:
# the diff is suspiciously large... give up and use something non-fingerprintable
return 0
# discourage "fee sniping"
height = min(chain_height, server_height)
return height
def print_var(var,name = "",veryverbose=False):
print(f"---{name}---")
if not var is None:
try:
print("doc:",doc(var))
except: pass
try:
print("str:",str(var))
except: pass
try:
print("repr",repr(var))
except:pass
try:
print("dict",dict(var))
except:pass
try:
print("dir",dir(var))
except:pass
try:
print("type",type(var))
except:pass
try:
print("to_json",var.to_json())
except: pass
try:
print("__slotnames__",var.__slotnames__)
except:pass
print(f"---end {name}---")
def print_utxo(utxo, name = ""):
print(f"---utxo-{name}---")
print_var(utxo,name)
print_prevout(utxo.prevout,name)
print_var(utxo.script_sig,f"{name}-script-sig")
print_var(utxo.witness,f"{name}-witness")
#print("madonnamaiala_TXInput__scriptpubkey:",utxo._TXInput__scriptpubkey)
print("_TxInput__address:",utxo._TxInput__address)
print("_TxInput__scriptpubkey:",utxo._TxInput__scriptpubkey)
print("_TxInput__value_sats:",utxo._TxInput__value_sats)
print(f"---utxo-end {name}---")
def print_prevout(prevout, name = ""):
print(f"---prevout-{name}---")
print_var(prevout,f"{name}-prevout")
print_var(prevout._asdict())
print(f"---prevout-end {name}---")
def export_meta_gui(electrum_window: 'ElectrumWindow', title, exporter):
filter_ = "All files (*)"
filename = getSaveFileName(
parent=electrum_window,
title=_("Select file to save your {}").format(title),
filename='BALplugin_{}'.format(title),
filter=filter_,
config=electrum_window.config,
)
if not filename:
return
try:
exporter(filename)
except FileExportFailed as e:
electrum_window.show_critical(str(e))
else:
electrum_window.show_message(_("Your {0} were exported to '{1}'")
.format(title, str(filename)))
def copy(dicto,dictfrom):
for k,v in dictfrom.items():
dicto[k]=v

802
will.py Normal file
View File

@ -0,0 +1,802 @@
import copy
from . import willexecutors as Willexecutors
from . import util as Util
from electrum.i18n import _
from electrum.transaction import TxOutpoint,PartialTxInput,tx_from_any,PartialTransaction,PartialTxOutput,Transaction
from electrum.util import bfh, decimal_point_to_base_unit_name
from electrum.util import write_json_file,read_json_file,FileImportFailed
from electrum.logging import get_logger,Logger
from electrum.bitcoin import NLOCKTIME_BLOCKHEIGHT_MAX
MIN_LOCKTIME = 1
MIN_BLOCK = 1
_logger = get_logger(__name__)
#return an array with the list of children
def get_children(will,willid):
out = []
for _id in will:
inputs = will[_id].tx.inputs()
for idi in range(0,len(inputs)):
_input = inputs[idi]
if _input.prevout.txid.hex() == willid:
out.append([_id,idi,_input.prevout.out_idx])
return out
#build a tree with parent transactions
def add_willtree(will):
for willid in will:
will[willid].children = get_children(will,willid)
for child in will[willid].children:
if not will[child[0]].father:
will[child[0]].father = willid
#return a list of will sorted by locktime
def get_sorted_will(will):
return sorted(will.items(),key = lambda x: x[1]['tx'].locktime)
def only_valid(will):
for k,v in will.items():
if v.get_status('VALID'):
yield k
def search_equal_tx(will,tx,wid):
for w in will:
if w != wid and not tx.to_json() != will[w]['tx'].to_json():
if will[w]['tx'].txid() != tx.txid():
if Util.cmp_txs(will[w]['tx'],tx):
return will[w]['tx']
return False
def get_tx_from_any(x):
try:
a=str(x)
return tx_from_any(a)
except Exception as e:
raise e
return x
def add_info_from_will(will,wid,wallet):
if isinstance(will[wid].tx,str):
will[wid].tx = get_tx_from_any(will[wid].tx)
if wallet:
will[wid].tx.add_info_from_wallet(wallet)
for txin in will[wid].tx.inputs():
txid = txin.prevout.txid.hex()
if txid in will:
change = will[txid].tx.outputs()[txin.prevout.out_idx]
txin._trusted_value_sats = change.value
try:
txin.script_descriptor = change.script_descriptor
except:
pass
txin.is_mine=True
txin._TxInput__address=change.address
txin._TxInput__scriptpubkey = change.scriptpubkey
txin._TxInput__value_sats = change.value
txin._trusted_value_sats = change.value
def normalize_will(will,wallet = None,others_inputs = {}):
to_delete = []
to_add = {}
#add info from wallet
for wid in will:
add_info_from_will(will,wid,wallet)
errors ={}
for wid in will:
txid = will[wid].tx.txid()
if txid is None:
_logger.error("##########")
_logger.error(wid)
_logger.error(will[wid])
_logger.error(will[wid].tx.to_json())
_logger.error("txid is none")
will[wid].set_status('ERROR',True)
errors[wid]=will[wid]
continue
if txid != wid:
outputs = will[wid].tx.outputs()
ow=will[wid]
ow.normalize_locktime(others_inputs)
will[wid]=ow.to_dict()
for i in range(0,len(outputs)):
change_input(will,wid,i,outputs[i],others_inputs,to_delete,to_add)
to_delete.append(wid)
to_add[ow.tx.txid()]=ow.to_dict()
for eid,err in errors.items():
new_txid = err.tx.txid()
for k,w in to_add.items():
will[k] = w
for wid in to_delete:
if wid in will:
del will[wid]
def new_input(txid,idx,change):
prevout = TxOutpoint(txid=bfh(txid), out_idx=idx)
inp = PartialTxInput(prevout=prevout)
inp._trusted_value_sats = change.value
inp.is_mine=True
inp._TxInput__address=change.address
inp._TxInput__scriptpubkey = change.scriptpubkey
inp._TxInput__value_sats = change.value
return inp
def check_anticipate(ow:'WillItem',nw:'WillItem'):
anticipate = Util.anticipate_locktime(ow.tx.locktime,days=1)
if int(nw.tx.locktime) >= int(anticipate):
if Util.cmp_heirs_by_values(ow.heirs,nw.heirs,[0,1],exclude_willexecutors = True):
print("same heirs",ow._id,nw._id)
if nw.we and ow.we:
if ow.we['url'] == nw.we['url']:
print("same willexecutors", ow.we['url'],nw.we['url'])
if int(ow.we['base_fee'])>int(nw.we['base_fee']):
print("anticipate")
return anticipate
else:
if int(ow.tx_fees) != int(nw.tx_fees):
return anticipate
else:
print("keep the same")
#_logger.debug("ow,base fee > nw.base_fee")
ow.tx.locktime
else:
#_logger.debug("ow.we['url']({ow.we['url']}) == nw.we['url']({nw.we['url']})")
print("keep the same")
ow.tx.locktime
else:
if nw.we == ow.we:
if not Util.cmp_heirs_by_values(ow.heirs,nw.heirs,[0,3]):
return anticipate
else:
return ow.tx.locktime
else:
return ow.tx.locktime
else:
return anticipate
return 4294967295+1
def change_input(will, otxid, idx, change,others_inputs,to_delete,to_append):
ow = will[otxid]
ntxid = ow.tx.txid()
if otxid != ntxid:
for wid in will:
w = will[wid]
inputs = w.tx.inputs()
outputs = w.tx.outputs()
found = False
old_txid = w.tx.txid()
ntx = None
for i in range(0,len(inputs)):
if inputs[i].prevout.txid.hex() == otxid and inputs[i].prevout.out_idx == idx:
if isinstance(w.tx,Transaction):
will[wid].tx = PartialTransaction.from_tx(w.tx)
will[wid].tx.set_rbf(True)
will[wid].tx._inputs[i]=new_input(wid,idx,change)
found = True
if found == True:
pass
new_txid = will[wid].tx.txid()
if old_txid != new_txid:
to_delete.append(old_txid)
to_append[new_txid]=will[wid]
outputs = will[wid].tx.outputs()
for i in range(0,len(outputs)):
change_input(will, wid, i, outputs[i],others_inputs,to_delete,to_append)
def get_all_inputs(will,only_valid = False):
all_inputs = {}
for w,wi in will.items():
if not only_valid or wi.get_status('VALID'):
inputs = wi.tx.inputs()
for i in inputs:
prevout_str = i.prevout.to_str()
inp=[w,will[w],i]
if not prevout_str in all_inputs:
all_inputs[prevout_str] = [inp]
else:
all_inputs[prevout_str].append(inp)
return all_inputs
def get_all_inputs_min_locktime(all_inputs):
all_inputs_min_locktime = {}
for i,values in all_inputs.items():
min_locktime = min(values,key = lambda x:x[1].tx.locktime)[1].tx.locktime
for w in values:
if w[1].tx.locktime == min_locktime:
if not i in all_inputs_min_locktime:
all_inputs_min_locktime[i]=[w]
else:
all_inputs_min_locktime[i].append(w)
return all_inputs_min_locktime
def search_anticipate_rec(will,old_inputs):
redo = False
to_delete = []
to_append = {}
new_inputs = get_all_inputs(will,only_valid = True)
for nid,nwi in will.items():
if nwi.search_anticipate(new_inputs) or nwi.search_anticipate(old_inputs):
if nid != nwi.tx.txid():
redo = True
to_delete.append(nid)
to_append[nwi.tx.txid()] = nwi
outputs = nwi.tx.outputs()
for i in range(0,len(outputs)):
change_input(will,nid,i,outputs[i],new_inputs,to_delete,to_append)
for w in to_delete:
try:
del will[w]
except:
pass
for k,w in to_append.items():
will[k]=w
if redo:
search_anticipate_rec(will,old_inputs)
def update_will(old_will,new_will):
all_old_inputs = get_all_inputs(old_will,only_valid=True)
all_inputs_min_locktime = get_all_inputs_min_locktime(all_old_inputs)
all_new_inputs = get_all_inputs(new_will)
#check if the new input is already spent by other transaction
#if it is use the same locktime, or anticipate.
search_anticipate_rec(new_will,all_old_inputs)
other_inputs = get_all_inputs(old_will,{})
try:
normalize_will(new_will,others_inputs=other_inputs)
except Exception as e:
raise e
for oid in only_valid(old_will):
if oid in new_will:
new_heirs = new_will[oid].heirs
new_we = new_will[oid].we
new_will[oid]=old_will[oid]
new_will[oid].heirs = new_heirs
new_will[oid].we = new_we
print(f"found {oid}")
continue
else:
print(f"not found {oid}")
continue
def get_higher_input_for_tx(will):
out = {}
for wid in will:
wtx = will[wid].tx
found = False
for inp in wtx.inputs():
if inp.prevout.txid.hex() in will:
found = True
break
if not found:
out[inp.prevout.to_str()] = inp
return out
def invalidate_will(will,wallet,fees_per_byte):
will_only_valid = only_valid_list(will)
inputs = get_all_inputs(will_only_valid)
utxos = wallet.get_utxos()
filtered_inputs = []
prevout_to_spend = []
for prevout_str,ws in inputs.items():
for w in ws:
if not w[0] in filtered_inputs:
filtered_inputs.append(w[0])
if not prevout_str in prevout_to_spend:
prevout_to_spend.append(prevout_str)
balance = 0
utxo_to_spend = []
for utxo in utxos:
utxo_str=utxo.prevout.to_str()
if utxo_str in prevout_to_spend:
balance += inputs[utxo_str][0][2].value_sats()
utxo_to_spend.append(utxo)
if len(utxo_to_spend) > 0:
change_addresses = wallet.get_change_addresses_for_new_transaction()
out = PartialTxOutput.from_address_and_value(change_addresses[0], balance)
out.is_change = True
locktime = Util.get_current_height(wallet.network)
tx = PartialTransaction.from_io(utxo_to_spend, [out], locktime=locktime, version=2)
tx.set_rbf(True)
fee=tx.estimated_size()*fees_per_byte
if balance -fee >0:
out = PartialTxOutput.from_address_and_value(change_addresses[0],balance - fee)
tx = PartialTransaction.from_io(utxo_to_spend,[out], locktime=locktime, version=2)
tx.set_rbf(True)
_logger.debug(f"invalidation tx: {tx}")
return tx
else:
_logger.debug("balance - fee <=0")
pass
else:
_logger.debug("len utxo_to_spend <=0")
pass
def is_new(will):
for wid,w in will.items():
if w.get_status('VALID') and not w.get_status('COMPLETE'):
return True
def search_rai (all_inputs,all_utxos,will,wallet):
will_only_valid = only_valid_or_replaced_list(will)
for inp,ws in all_inputs.items():
inutxo = Util.in_utxo(inp,all_utxos)
for w in ws:
wi=w[1]
if wi.get_status('VALID') or wi.get_status('CONFIRMED') or wi.get_status('PENDING'):
prevout_id=w[2].prevout.txid.hex()
if not inutxo:
if prevout_id in will:
wo=will[prevout_id]
if wo.get_status('REPLACED'):
wi.set_status('REPLACED',True)
if wo.get_status("INVALIDATED"):
wi.set_status('INVALIDATED',True)
else:
if wallet.db.get_transaction(wi._id):
wi.set_status('CONFIRMED',True)
else:
wi.set_status('INVALIDATED',True)
#else:
# if prevout_id in will:
# wo = will[prevout_id]
# ttx= wallet.db.get_transaction(prevout_id)
# if ttx:
# _logger.error("transaction in wallet should be early detected")
# #wi.set_status('CONFIRMED',True)
# #else:
# # _logger.error("transaction not in will or utxo")
# # wi.set_status('INVALIDATED',True)
for child in wi.search(all_inputs):
if child.tx.locktime < wi.tx.locktime:
_logger.debug("a child was found")
wi.set_status('REPLACED',True)
else:
pass
def utxos_strs(utxos):
return [Util.utxo_to_str(u) for u in utxos]
def set_invalidate(wid,will=[]):
will[wid].set_status("INVALIDATED",True)
if will[wid].children:
for c in self.children.items():
set_invalidate(c[0],will)
def check_tx_height(tx, wallet):
info=wallet.get_tx_info(tx)
return info.tx_mined_status.height
#check if transactions are stil valid tecnically valid
def check_invalidated(willtree,utxos_list,wallet):
for wid,w in willtree.items():
if not w.father:
for inp in w.tx.inputs():
inp_str = Util.utxo_to_str(inp)
#print(utxos_list)
#print(inp_str)
#print(inp_str in utxos_list)
#print("notin: ",not inp_str in utxos_list)
if not inp_str in utxos_list:
#print("quindi qua non ci arrivo?")
if wallet:
height= check_tx_height(w.tx,wallet)
if height < 0:
#_logger.debug(f"heigth {height}")
set_invalidate(wid,willtree)
elif height == 0:
w.set_status("PENDING",True)
else:
w.set_status('CONFIRMED',True)
def reflect_to_children(treeitem):
if not treeitem.get_status("VALID"):
_logger.debug(f"{tree:item._id} status not valid looking for children")
for child in treeitem.children:
wc = willtree[child]
if wc.get_status("VALID"):
if treeitem.get_status("INVALIDATED"):
wc.set_status("INVALIDATED",True)
if treeitem.get_status("REPLACED"):
wc.set_status("REPLACED",True)
if wc.children:
reflect_to_children(wc)
def check_amounts(heirs,willexecutors,all_utxos,timestamp_to_check,dust):
fixed_heirs,fixed_amount,perc_heirs,perc_amount = heirs.fixed_percent_lists_amount(timestamp_to_check,dust,reverse=True)
wallet_balance = 0
for utxo in all_utxos:
wallet_balance += utxo.value_sats()
if fixed_amount >= wallet_balance:
raise FixedAmountException(f"Fixed amount({fixed_amount}) >= {wallet_balance}")
if perc_amount != 100:
raise PercAmountException(f"Perc amount({perc_amount}) =! 100%")
for url,wex in willexecutors.items():
if Willexecutors.is_selected(wex):
temp_balance = wallet_balance - int(wex['base_fee'])
if fixed_amount >= temp_balance:
raise FixedAmountException(f"Willexecutor{url} excess base fee({wex['base_fee']}), {fixed_amount} >={temp_balance}")
def check_will(will,all_utxos,wallet,block_to_check,timestamp_to_check):
add_willtree(will)
utxos_list= utxos_strs(all_utxos)
check_invalidated(will,utxos_list,wallet)
#from pprint import pprint
#for wid,w in will.items():
# pprint(w.to_dict())
all_inputs=get_all_inputs(will,only_valid = True)
all_inputs_min_locktime = get_all_inputs_min_locktime(all_inputs)
check_will_expired(all_inputs_min_locktime,block_to_check,timestamp_to_check)
all_inputs=get_all_inputs(will,only_valid = True)
search_rai(all_inputs,all_utxos,will,wallet)
def is_will_valid(will, block_to_check, timestamp_to_check, tx_fees, all_utxos,heirs={},willexecutors={},self_willexecutor=False, wallet=False, callback_not_valid_tx=None):
check_will(will,all_utxos,wallet,block_to_check,timestamp_to_check)
if heirs:
if not check_willexecutors_and_heirs(will,heirs,willexecutors,self_willexecutor,timestamp_to_check,tx_fees):
raise NotCompleteWillException()
all_inputs=get_all_inputs(will,only_valid = True)
_logger.info('check all utxo in wallet are spent')
if all_inputs:
for utxo in all_utxos:
if utxo.value_sats() > 68 * tx_fees:
if not Util.in_utxo(utxo,all_inputs.keys()):
_logger.info("utxo is not spent",utxo.to_json())
_logger.debug(all_inputs.keys())
raise NotCompleteWillException("Some utxo in the wallet is not included")
_logger.info('will ok')
return True
def check_will_expired(all_inputs_min_locktime,block_to_check,timestamp_to_check):
_logger.info("check if some transaction is expired")
for prevout_str, wid in all_inputs_min_locktime.items():
for w in wid:
if w[1].get_status('VALID'):
locktime = int(wid[0][1].tx.locktime)
if locktime <= NLOCKTIME_BLOCKHEIGHT_MAX:
if locktime < int(block_to_check):
raise WillExpiredException(f"Will Expired {wid[0][0]}: {locktime}<{block_to_check}")
else:
if locktime < int(timestamp_to_check):
raise WillExpiredException(f"Will Expired {wid[0][0]}: {locktime}<{timestamp_to_check}")
def check_all_input_spent_are_in_wallet():
_logger.info("check all input spent are in wallet or valid txs")
for inp,ws in all_inputs.items():
if not Util.in_utxo(inp,all_utxos):
for w in ws:
if w[1].get_status('VALID'):
prevout_id = w[2].prevout.txid.hex()
parentwill = will.get(prevout_id,False)
if not parentwill or not parentwill.get_status('VALID'):
w[1].set_status('INVALIDATED',True)
def only_valid_list(will):
out={}
for wid,w in will.items():
if w.get_status('VALID'):
out[wid]=w
return out
def only_valid_or_replaced_list(will):
out=[]
for wid,w in will.items():
wi = w
if wi.get_status('VALID') or wi.get_status('REPLACED'):
out.append(wid)
return out
def check_willexecutors_and_heirs(will,heirs,willexecutors,self_willexecutor,check_date,tx_fees):
_logger.debug("check willexecutors heirs")
no_willexecutor = 0
willexecutors_found = {}
heirs_found = {}
will_only_valid = only_valid_list(will)
if len(will_only_valid)<1:
return False
for wid in only_valid_list(will):
w = will[wid]
if w.tx_fees != tx_fees:
#w.set_status('VALID',False)
raise TxFeesChangedException(f"{tx_fees}:",w.tx_fees)
for wheir in w.heirs:
if not 'w!ll3x3c"' == wheir[:9]:
their = will[wid].heirs[wheir]
if heir := heirs.get(wheir,None):
if heir[0] == their[0] and heir[1] == their[1] and Util.parse_locktime_string(heir[2]) >= Util.parse_locktime_string(their[2]):
count = heirs_found.get(wheir,0)
heirs_found[wheir]=count + 1
else:
_logger.debug("heir not present transaction is not valid:",wid,w)
continue
if willexecutor := w.we:
count = willexecutors_found.get(willexecutor['url'],0)
if Util.cmp_willexecutor(willexecutor,willexecutors.get(willexecutor['url'],None)):
willexecutors_found[willexecutor['url']]=count+1
else:
no_willexecutor += 1
count_heirs = 0
for h in heirs:
if Util.parse_locktime_string(heirs[h][2])>=check_date:
count_heirs +=1
if not h in heirs_found:
_logger.debug(f"heir: {h} not found")
raise HeirNotFoundException(h)
if not count_heirs:
raise NoHeirsException("there are not valid heirs")
if self_willexecutor and no_willexecutor ==0:
raise NoWillExecutorNotPresent("Backup tx")
for url,we in willexecutors.items():
if Willexecutors.is_selected(we):
if not url in willexecutors_found:
_logger.debug(f"will-executor: {url} not fount")
raise WillExecutorNotPresent(url)
_logger.info("will is coherent with heirs and will-executors")
return True
class WillItem(Logger):
STATUS_DEFAULT = {
'ANTICIPATED': ['Anticipated', False],
'BROADCASTED': ['Broadcasted', False],
'CHECKED': ['Checked', False],
'CHECK_FAIL': ['Check Failed',False],
'COMPLETE': ['Signed', False],
'CONFIRMED': ['Confirmed', False],
'ERROR': ['Error', False],
'EXPIRED': ['Expired', False],
'EXPORTED': ['Exported', False],
'IMPORTED': ['Imported', False],
'INVALIDATED': ['Invalidated', False],
'PENDING': ['Pending', False],
'PUSH_FAIL': ['Push failed', False],
'PUSHED': ['Pushed', False],
'REPLACED': ['Replaced', False],
'RESTORED': ['Restored', False],
'VALID': ['Valid', True],
}
def set_status(self,status,value=True):
_logger.debug("set status {} - {} {} -> {}".format(self._id,status,self.STATUS[status][1],value))
if self.STATUS[status][1] == bool(value):
return None
self.status += "." +("NOT " if not value else "" + _(self.STATUS[status][0]))
self.STATUS[status][1] = bool(value)
if value:
if status in ['INVALIDATED','REPLACED','CONFIRMED','PENDING']:
self.STATUS['VALID'][1] = False
if status in ['CONFIRMED','PENDING']:
self.STATUS['INVALIDATED'][1] = False
if status in ['PUSHED']:
self.STATUS['PUSH_FAIL'][1] = False
self.STATUS['CHECK_FAIL'][1] = False
#if status in ['CHECK_FAIL']:
# self.STATUS['PUSHED'][1] = False
if status in ['CHECKED']:
self.STATUS['PUSHED'][1] = True
self.STATUS['PUSH_FAIL'][1] = False
return value
def get_status(self,status):
return self.STATUS[status][1]
def __init__(self,w,_id=None,wallet=None):
if isinstance(w,WillItem,):
self.__dict__ = w.__dict__.copy()
else:
self.tx = get_tx_from_any(w['tx'])
self.heirs = w.get('heirs',None)
self.we = w.get('willexecutor',None)
self.status = w.get('status',None)
self.description = w.get('description',None)
self.time = w.get('time',None)
self.change = w.get('change',None)
self.tx_fees = w.get('tx_fees',0)
self.father = w.get('Father',None)
self.children = w.get('Children',None)
self.STATUS = copy.deepcopy(WillItem.STATUS_DEFAULT)
for s in self.STATUS:
self.STATUS[s][1]=w.get(s,WillItem.STATUS_DEFAULT[s][1])
if not _id:
self._id = self.tx.txid()
else:
self._id = _id
if not self._id:
self.status+="ERROR!!!"
self.valid = False
if wallet:
self.tx.add_info_from_wallet(wallet)
def to_dict(self):
out = {
'_id':self._id,
'tx':self.tx,
'heirs':self.heirs,
'willexecutor':self.we,
'status':self.status,
'description':self.description,
'time':self.time,
'change':self.change,
'tx_fees':self.tx_fees
}
for key in self.STATUS:
try:
out[key]=self.STATUS[key][1]
except Exception as e:
_logger.error(f"{key},{self.STATUS[key]} {e}")
return out
def __repr__(self):
return str(self)
def __str__(self):
return str(self.to_dict())
def set_anticipate(self, ow:'WillItem'):
nl = min(ow.tx.locktime,check_anticipate(ow,self))
if int(nl) < self.tx.locktime:
#_logger.debug("actually anticipating")
self.tx.locktime = int(nl)
return True
else:
#_logger.debug("keeping the same locktime")
return False
def search_anticipate(self,all_inputs):
anticipated = False
for ow in self.search(all_inputs):
if self.set_anticipate(ow):
anticipated = True
return anticipated
def search(self,all_inputs):
for inp in self.tx.inputs():
prevout_str = inp.prevout.to_str()
oinps = all_inputs.get(prevout_str,[])
for oinp in oinps:
ow=oinp[1]
if ow._id!=self._id:
yield ow
def normalize_locktime(self,all_inputs):
outputs = self.tx.outputs()
for idx in range(0,len(outputs)):
inps = all_inputs.get(f"{self._id}:{idx}",[])
_logger.debug("****check locktime***")
for inp in inps:
if inp[0]!= self._id:
iw = inp[1]
self.set_anticipate(iw)
def check_willexecutor(self):
try:
if resp:=Willexecutors.check_transaction(self._id,self.we['url']):
if 'tx' in resp and resp['tx']==str(self.tx):
self.set_status('PUSHED')
self.set_status('CHECKED')
else:
self.set_status('CHECK_FAIL')
self.set_status('PUSHED',False)
return True
else:
self.set_status('CHECK_FAIL')
self.set_status('PUSHED',False)
return False
except Exception as e:
_logger.error(f"exception checking transaction: {e}")
self.set_status('CHECK_FAIL')
def get_color(self):
if self.get_status("INVALIDATED"):
return "#f87838"
elif self.get_status("REPLACED"):
return "#ff97e9"
elif self.get_status("CONFIRMED"):
return "#bfbfbf"
elif self.get_status("PENDING"):
return "#ffce30"
elif self.get_status("CHECK_FAIL") and not self.get_status("CHECKED"):
return "#e83845"
elif self.get_status("CHECKED"):
return "#8afa6c"
elif self.get_status("PUSH_FAIL"):
return "#e83845"
elif self.get_status("PUSHED"):
return "#73f3c8"
elif self.get_status("COMPLETE"):
return "#2bc8ed"
else:
return "#ffffff"
class WillExpiredException(Exception):
pass
class NotCompleteWillException(Exception):
pass
class HeirChangeException(NotCompleteWillException):
pass
class TxFeesChangedException(NotCompleteWillException):
pass
class HeirNotFoundException(NotCompleteWillException):
pass
class WillexecutorChangeException(NotCompleteWillException):
pass
class NoWillExecutorNotPresent(NotCompleteWillException):
pass
class WillExecutorNotPresent(NotCompleteWillException):
pass
class NoHeirsException(Exception):
pass
class AmountException(Exception):
pass
class PercAmountException(AmountException):
pass
class FixedAmountException(AmountException):
pass

203
willexecutors.py Normal file
View File

@ -0,0 +1,203 @@
import json
from datetime import datetime
from functools import partial
from aiohttp import ClientResponse
from electrum.network import Network
from electrum import constants
from electrum.logging import get_logger
from electrum.gui.qt.util import WaitingDialog
from electrum.i18n import _
from .balqt.baldialog import BalWaitingDialog
from . import util as Util
DEFAULT_TIMEOUT = 5
_logger = get_logger(__name__)
def get_willexecutors(bal_plugin, update = False,bal_window=False,force=False,task=True):
willexecutors = bal_plugin.config_get(bal_plugin.WILLEXECUTORS)
for w in willexecutors:
initialize_willexecutor(willexecutors[w],w)
bal=bal_plugin.DEFAULT_SETTINGS[bal_plugin.WILLEXECUTORS]
for bal_url,bal_executor in bal.items():
if not bal_url in willexecutors:
_logger.debug("replace bal")
willexecutors[bal_url]=bal_executor
if update:
found = False
for url,we in willexecutors.items():
if is_selected(we):
found = True
if found or force:
if bal_plugin.config_get(bal_plugin.PING_WILLEXECUTORS) or force:
ping_willexecutors = True
if bal_plugin.config_get(bal_plugin.ASK_PING_WILLEXECUTORS) and not force:
ping_willexecutors = bal_window.window.question(_("Contact willexecutors servers to update payment informations?"))
if ping_willexecutors:
if task:
bal_window.ping_willexecutors(willexecutors)
else:
bal_window.ping_willexecutors_task(willexecutors)
return willexecutors
def is_selected(willexecutor,value=None):
if not willexecutor:
return False
if not value is None:
willexecutor['selected']=value
try:
return willexecutor['selected']
except:
willexecutor['selected']=False
return False
def get_willexecutor_transactions(will, force=False):
willexecutors ={}
for wid,willitem in will.items():
if willitem.get_status('VALID'):
if willitem.get_status('COMPLETE'):
if not willitem.get_status('PUSHED') or force:
if willexecutor := willitem.we:
url=willexecutor['url']
if willexecutor and is_selected(willexecutor):
if not url in willexecutors:
willexecutor['txs']=""
willexecutor['txsids']=[]
willexecutor['broadcast_status']= _("Waiting...")
willexecutors[url]=willexecutor
willexecutors[url]['txs']+=str(willitem.tx)+"\n"
willexecutors[url]['txsids'].append(wid)
return willexecutors
def only_selected_list(willexecutors):
out = {}
for url,v in willexectors.items():
if is_selected(willexecutor):
out[url]=v
def push_transactions_to_willexecutors(will):
willexecutors = get_transactions_to_be_pushed()
for url in willexecutors:
willexecutor = willexecutors[url]
if is_selected(willexecutor):
if 'txs' in willexecutor:
push_transactions_to_willexecutor(willexecutors[url]['txs'],url)
def send_request(method, url, data=None, *, timeout=10):
network = Network.get_instance()
if not network:
raise ErrorConnectingServer('You are offline.')
_logger.debug(f'<-- {method} {url} {data}')
headers = {}
headers['user-agent'] = 'BalPlugin'
headers['Content-Type']='text/plain'
try:
if method == 'get':
response = Network.send_http_on_proxy(method, url,
params=data,
headers=headers,
on_finish=handle_response,
timeout=timeout)
elif method == 'post':
response = Network.send_http_on_proxy(method, url,
body=data,
headers=headers,
on_finish=handle_response,
timeout=timeout)
else:
raise Exception(f"unexpected {method=!r}")
except Exception as e:
_logger.error(f"exception sending request {e}")
raise e
else:
_logger.debug(f'--> {response}')
return response
async def handle_response(resp:ClientResponse):
r=await resp.text()
try:
r=json.loads(r)
r['status'] = resp.status
r['selected']=is_selected(willexecutor)
r['url']=url
except:
pass
return r
class AlreadyPresentException(Exception):
pass
def push_transactions_to_willexecutor(willexecutor):
out=True
try:
_logger.debug(f"willexecutor['txs']")
if w:=send_request('post', willexecutor['url']+"/"+constants.net.NET_NAME+"/pushtxs", data=willexecutor['txs'].encode('ascii')):
willexecutor['broadcast_stauts'] = _("Success")
_logger.debug(f"pushed: {w}")
if w !='thx':
_logger.debug(f"error: {w}")
raise Exception(w)
else:
raise Exception("empty reply from:{willexecutor['url']}")
except Exception as e:
_logger.debug(f"error:{e}")
if str(e) == "already present":
raise AlreadyPresentException()
out=False
willexecutor['broadcast_stauts'] = _("Failed")
return out
def ping_servers(willexecutors):
for url,we in willexecutors.items():
get_info_task(url,we)
def get_info_task(url,willexecutor):
w=None
try:
_logger.info("GETINFO_WILLEXECUTOR")
_logger.debug(url)
w = send_request('get',url+"/"+constants.net.NET_NAME+"/info")
willexecutor['url']=url
willexecutor['status'] = w['status']
willexecutor['base_fee'] = w['base_fee']
willexecutor['address'] = w['address']
if not willexecutor['info']:
willexecutor['info'] = w['info']
_logger.debug(f"response_data {w['address']}")
except Exception as e:
_logger.error(f"error {e} contacting {url}: {w}")
willexecutor['status']="KO"
willexecutor['last_update'] = datetime.now().timestamp()
return willexecutor
def initialize_willexecutor(willexecutor,url,status=None,selected=None):
willexecutor['url']=url
if not status is None:
willexecutor['status'] = status
willexecutor['selected'] = is_selected(willexecutor,selected)
def get_willexecutors_list_from_json(bal_plugin):
try:
with open("willexecutors.json") as f:
willexecutors = json.load(f)
for w in willexecutors:
willexecutor=willexecutors[w]
willexecutors.initialize_willexecutor(willexecutor,w,'New',False)
bal_plugin.config.set_key(bal_plugin.WILLEXECUTORS,willexecutors,save=True)
return h
except Exception as e:
_logger.error(f"errore aprendo willexecutors.json: {e}")
return {}
def check_transaction(txid,url):
_logger.debug(f"{url}:{txid}")
try:
w = send_request('post',url+"/searchtx",data=txid.encode('ascii'))
return w
except Exception as e:
_logger.error(f"error contacting {url} for checking txs {e}")
raise e