forked from bitcoinafterlife/bal-electrum-plugin
init
This commit is contained in:
89
balqt/amountedit.py
Normal file
89
balqt/amountedit.py
Normal file
@@ -0,0 +1,89 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from typing import Union
|
||||
from decimal import Decimal
|
||||
|
||||
from . import qt_resources
|
||||
if qt_resources.QT_VERSION == 5:
|
||||
from PyQt5.QtWidgets import (QLineEdit, QStyle, QStyleOptionFrame, QSizePolicy)
|
||||
from PyQt5.QtGui import QPalette, QPainter
|
||||
from PyQt5.QtCore import pyqtSignal, Qt, QSize
|
||||
else:
|
||||
from PyQt6.QtWidgets import (QLineEdit, QStyle, QStyleOptionFrame, QSizePolicy)
|
||||
from PyQt6.QtGui import QPalette, QPainter
|
||||
from PyQt6.QtCore import pyqtSignal, Qt, QSize
|
||||
|
||||
|
||||
from electrum.util import (format_satoshis_plain, decimal_point_to_base_unit_name,
|
||||
FEERATE_PRECISION, quantize_feerate, DECIMAL_POINT, UI_UNIT_NAME_FEERATE_SAT_PER_VBYTE)
|
||||
|
||||
from electrum.gui.qt.amountedit import BTCAmountEdit, char_width_in_lineedit, ColorScheme
|
||||
|
||||
_NOT_GIVEN = object() # sentinel value
|
||||
|
||||
|
||||
class PercAmountEdit(BTCAmountEdit):
|
||||
def __init__(self, decimal_point, is_int=False, parent=None, *, max_amount=_NOT_GIVEN):
|
||||
super().__init__(decimal_point, is_int, parent, max_amount=max_amount)
|
||||
|
||||
def numbify(self):
|
||||
text = self.text().strip()
|
||||
if text == '!':
|
||||
self.shortcut.emit()
|
||||
return
|
||||
pos = self.cursorPosition()
|
||||
chars = '0123456789%'
|
||||
chars += DECIMAL_POINT
|
||||
|
||||
s = ''.join([i for i in text if i in chars])
|
||||
|
||||
if '%' in s:
|
||||
self.is_perc=True
|
||||
s=s.replace('%','')
|
||||
else:
|
||||
self.is_perc=False
|
||||
|
||||
if DECIMAL_POINT in s:
|
||||
p = s.find(DECIMAL_POINT)
|
||||
s = s.replace(DECIMAL_POINT, '')
|
||||
s = s[:p] + DECIMAL_POINT + s[p:p+8]
|
||||
if self.is_perc:
|
||||
s+='%'
|
||||
|
||||
|
||||
#if self.max_amount:
|
||||
# if (amt := self._get_amount_from_text(s)) and amt >= self.max_amount:
|
||||
# s = self._get_text_from_amount(self.max_amount)
|
||||
self.setText(s)
|
||||
# setText sets Modified to False. Instead we want to remember
|
||||
# if updates were because of user modification.
|
||||
self.setModified(self.hasFocus())
|
||||
self.setCursorPosition(pos)
|
||||
#if len(s>0)
|
||||
# self.drawText("")
|
||||
|
||||
def _get_amount_from_text(self, text: str) -> Union[None, Decimal, int]:
|
||||
try:
|
||||
text = text.replace(DECIMAL_POINT, '.')
|
||||
text = text.replace('%', '')
|
||||
return (Decimal)(text)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _get_text_from_amount(self, amount):
|
||||
out = super()._get_text_from_amount(amount)
|
||||
if self.is_perc: out+='%'
|
||||
return out
|
||||
|
||||
def paintEvent(self, event):
|
||||
QLineEdit.paintEvent(self, event)
|
||||
if self.base_unit:
|
||||
panel = QStyleOptionFrame()
|
||||
self.initStyleOption(panel)
|
||||
textRect = self.style().subElementRect(QStyle.SubElement.SE_LineEditContents, panel, self)
|
||||
textRect.adjust(2, 0, -10, 0)
|
||||
painter = QPainter(self)
|
||||
painter.setPen(ColorScheme.GRAY.as_color())
|
||||
if len(self.text())==0:
|
||||
painter.drawText(textRect, int(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter), self.base_unit() + " or perc value")
|
||||
|
||||
119
balqt/baldialog.py
Normal file
119
balqt/baldialog.py
Normal file
@@ -0,0 +1,119 @@
|
||||
from typing import Callable,Any
|
||||
|
||||
from . import qt_resources
|
||||
if qt_resources.QT_VERSION == 5:
|
||||
from PyQt5.QtCore import Qt,pyqtSignal
|
||||
from PyQt5.QtWidgets import QLabel, QVBoxLayout, QCheckBox
|
||||
else:
|
||||
from PyQt6.QtCore import Qt,pyqtSignal
|
||||
from PyQt6.QtWidgets import QLabel, QVBoxLayout, QCheckBox
|
||||
|
||||
from electrum.gui.qt.util import WindowModalDialog, TaskThread
|
||||
from electrum.i18n import _
|
||||
from electrum.logging import get_logger
|
||||
|
||||
_logger = get_logger(__name__)
|
||||
|
||||
class BalDialog(WindowModalDialog):
|
||||
|
||||
def __init__(self,parent,title=None, icon = 'bal32x32.png'):
|
||||
self.parent=parent
|
||||
WindowModalDialog.__init__(self,self.parent,title)
|
||||
self.setWindowIcon(qt_resources.read_QIcon(icon))
|
||||
|
||||
class BalWaitingDialog(BalDialog):
|
||||
updatemessage=pyqtSignal([str], arguments=['message'])
|
||||
def __init__(self, bal_window: 'BalWindow', message: str, task, on_success=None, on_error=None, on_cancel=None,exe=True):
|
||||
assert bal_window
|
||||
BalDialog.__init__(self, bal_window.window, _("Please wait"))
|
||||
self.message_label = QLabel(message)
|
||||
vbox = QVBoxLayout(self)
|
||||
vbox.addWidget(self.message_label)
|
||||
self.updatemessage.connect(self.update_message)
|
||||
if on_cancel:
|
||||
self.cancel_button = CancelButton(self)
|
||||
self.cancel_button.clicked.connect(on_cancel)
|
||||
vbox.addLayout(Buttons(self.cancel_button))
|
||||
self.accepted.connect(self.on_accepted)
|
||||
self.task=task
|
||||
self.on_success = on_success
|
||||
self.on_error = on_error
|
||||
self.on_cancel = on_cancel
|
||||
if exe:
|
||||
self.exe()
|
||||
|
||||
def exe(self):
|
||||
self.thread = TaskThread(self)
|
||||
self.thread.finished.connect(self.deleteLater) # see #3956
|
||||
self.thread.finished.connect(self.finished)
|
||||
self.thread.add(self.task, self.on_success, self.accept, self.on_error)
|
||||
self.exec()
|
||||
|
||||
def hello(self):
|
||||
pass
|
||||
def finished(self):
|
||||
_logger.info("finished")
|
||||
def wait(self):
|
||||
self.thread.wait()
|
||||
|
||||
def on_accepted(self):
|
||||
self.thread.stop()
|
||||
def update_message(self,msg):
|
||||
self.message_label.setText(msg)
|
||||
|
||||
def update(self, msg):
|
||||
self.updatemessage.emit(msg)
|
||||
|
||||
def getText(self):
|
||||
return self.message_label.text()
|
||||
|
||||
def closeEvent(self,event):
|
||||
self.thread.stop()
|
||||
|
||||
|
||||
|
||||
class BalBlockingWaitingDialog(BalDialog):
|
||||
def __init__(self, bal_window: 'BalWindow', message: str, task: Callable[[], Any]):
|
||||
BalDialog.__init__(self, bal_window, _("Please wait"))
|
||||
self.message_label = QLabel(message)
|
||||
vbox = QVBoxLayout(self)
|
||||
vbox.addWidget(self.message_label)
|
||||
self.finished.connect(self.deleteLater) # see #3956
|
||||
# show popup
|
||||
self.show()
|
||||
# refresh GUI; needed for popup to appear and for message_label to get drawn
|
||||
QCoreApplication.processEvents()
|
||||
QCoreApplication.processEvents()
|
||||
try:
|
||||
# block and run given task
|
||||
task()
|
||||
finally:
|
||||
# close popup
|
||||
self.accept()
|
||||
|
||||
class bal_checkbox(QCheckBox):
|
||||
def __init__(self, plugin,variable,window=None):
|
||||
QCheckBox.__init__(self)
|
||||
self.setChecked(plugin.config_get(variable))
|
||||
window=window
|
||||
def on_check(v):
|
||||
plugin.config.set_key(variable, v == Qt.CheckState.Checked, save=True)
|
||||
if window:
|
||||
plugin._hide_invalidated= plugin.config_get(plugin.HIDE_INVALIDATED)
|
||||
plugin._hide_replaced= plugin.config_get(plugin.HIDE_REPLACED)
|
||||
|
||||
window.update_all()
|
||||
self.stateChanged.connect(on_check)
|
||||
|
||||
#TODO IMPLEMENT PREVIEW DIALOG
|
||||
#tx list display txid, willexecutor, qrcode, button to sign
|
||||
# :def preview_dialog(self, txs):
|
||||
def preview_dialog(self, txs):
|
||||
w=PreviewDialog(self,txs)
|
||||
w.exec()
|
||||
return w
|
||||
def add_info_from_will(self,tx):
|
||||
for input in tx.inputs():
|
||||
pass
|
||||
|
||||
|
||||
384
balqt/closedialog.py
Normal file
384
balqt/closedialog.py
Normal file
@@ -0,0 +1,384 @@
|
||||
from .baldialog import BalDialog
|
||||
from . import qt_resources
|
||||
|
||||
if qt_resources.QT_VERSION == 5:
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import QLabel, QVBoxLayout, QCheckBox,QWidget
|
||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject,QEventLoop
|
||||
else:
|
||||
from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject,QEventLoop
|
||||
from PyQt6.QtCore import Qt
|
||||
from PyQt6.QtWidgets import QLabel, QVBoxLayout, QCheckBox, QWidget
|
||||
import time
|
||||
from electrum.i18n import _
|
||||
from electrum.gui.qt.util import WindowModalDialog, TaskThread
|
||||
from electrum.network import Network,TxBroadcastError, BestEffortRequestFailed
|
||||
from electrum.logging import get_logger
|
||||
|
||||
|
||||
from functools import partial
|
||||
import copy
|
||||
|
||||
from .. import util as Util
|
||||
from .. import will as Will
|
||||
|
||||
from .. import willexecutors as Willexecutors
|
||||
_logger = get_logger(__name__)
|
||||
|
||||
|
||||
class BalCloseDialog(BalDialog):
|
||||
updatemessage=pyqtSignal()
|
||||
def __init__(self,bal_window):
|
||||
BalDialog.__init__(self,bal_window.window,"Closing BAL")
|
||||
self.updatemessage.connect(self.update)
|
||||
self.bal_window=bal_window
|
||||
self.message_label = QLabel("Closing BAL:")
|
||||
self.vbox = QVBoxLayout(self)
|
||||
self.vbox.addWidget(self.message_label)
|
||||
self.qwidget=QWidget()
|
||||
self.vbox.addWidget(self.qwidget)
|
||||
self.labels=[]
|
||||
#self.checking.connect(self.msg_set_checking)
|
||||
#self.invalidating.connect(self.msg_set_invalidating)
|
||||
#self.building.connect(self.msg_set_building)
|
||||
#self.signing.connect(self.msg_set_signing)
|
||||
#self.pushing.connect(self.msg_set_pushing)
|
||||
|
||||
#self.askpassword.connect(self.ask_password)
|
||||
#self.passworddone.connect(self.password_done)
|
||||
self.check_row = None
|
||||
self.inval_row = None
|
||||
self.build_row = None
|
||||
self.sign_row = None
|
||||
self.push_row = None
|
||||
self.network = Network.get_instance()
|
||||
self._stopping = False
|
||||
self.thread = TaskThread(self)
|
||||
self.thread.finished.connect(self.task_finished) # see #3956
|
||||
def task_finished(self):
|
||||
pass
|
||||
#_logger.trace("task finished")
|
||||
|
||||
def close_plugin_task(self):
|
||||
_logger.debug("close task to be started")
|
||||
self.thread.add(self.task_phase1,on_success=self.on_success_phase1,on_done=self.on_accept,on_error=self.on_error_phase1)
|
||||
self.show()
|
||||
self.exec()
|
||||
|
||||
def task_phase1(self):
|
||||
_logger.debug("close plugin phase 1 started")
|
||||
try:
|
||||
self.bal_window.init_class_variables()
|
||||
except Will.NoHeirsException:
|
||||
return False, None
|
||||
self.msg_set_status("checking variables","Waiting")
|
||||
try:
|
||||
Will.check_amounts(self.bal_window.heirs,self.bal_window.willexecutors,self.bal_window.window.wallet.get_utxos(),self.bal_window.date_to_check,self.bal_window.window.wallet.dust_threshold())
|
||||
except Will.AmountException:
|
||||
self.msg_edit_row('<font color="#ff0000">'+_("In the inheritance process, the entire wallet will always be fully emptied. Your settings require an adjustment of the amounts"+"</font>"))
|
||||
#self.bal_window.show_warning(_("In the inheritance process, the entire wallet will always be fully emptied. Your settings require an adjustment of the amounts"),parent=self)
|
||||
|
||||
self.msg_set_checking()
|
||||
have_to_build=False
|
||||
try:
|
||||
self.bal_window.check_will()
|
||||
self.msg_set_checking('Ok')
|
||||
except Will.WillExpiredException as e:
|
||||
self.msg_set_checking("Expired")
|
||||
fee_per_byte=self.bal_window.will_settings.get('tx_fees',1)
|
||||
return None, Will.invalidate_will(self.bal_window.willitems,self.bal_window.wallet,fee_per_byte)
|
||||
except Will.NoHeirsException:
|
||||
self.msg_set_checking("No Heirs")
|
||||
except Will.NotCompleteWillException as e:
|
||||
message = False
|
||||
have_to_build=True
|
||||
if isinstance(e,Will.HeirChangeException):
|
||||
message ="Heirs changed:"
|
||||
elif isinstance(e,Will.WillExecutorNotPresent):
|
||||
message = "Will-Executor not present"
|
||||
elif isinstance(e,Will.WillexecutorChangeException):
|
||||
message = "Will-Executor changed"
|
||||
elif isinstance(e,Will.TxFeesChangedException):
|
||||
message = "Txfees are changed"
|
||||
elif isinstance(e,Will.HeirNotFoundException):
|
||||
message = "Heir not found"
|
||||
if message:
|
||||
self.msg_set_checking(message)
|
||||
else:
|
||||
self.msg_set_checking("New")
|
||||
|
||||
if have_to_build:
|
||||
self.msg_set_building()
|
||||
try:
|
||||
self.bal_window.build_will()
|
||||
self.bal_window.check_will()
|
||||
for wid in Will.only_valid(self.bal_window.willitems):
|
||||
self.bal_window.wallet.set_label(wid,"BAL Transaction")
|
||||
self.msg_set_building("Ok")
|
||||
except Exception as e:
|
||||
self.msg_set_building(self.msg_error(e))
|
||||
return False,None
|
||||
have_to_sign = False
|
||||
for wid in Will.only_valid(self.bal_window.willitems):
|
||||
if not self.bal_window.willitems[wid].get_status("COMPLETE"):
|
||||
have_to_sign = True
|
||||
break
|
||||
return have_to_sign, None
|
||||
|
||||
def on_accept(self):
|
||||
pass
|
||||
|
||||
def on_accept_phase2(self):
|
||||
pass
|
||||
|
||||
def on_error_push(self):
|
||||
pass
|
||||
|
||||
def wait(self,secs):
|
||||
wait_row=None
|
||||
for i in range(secs,0,-1):
|
||||
if self._stopping:
|
||||
return
|
||||
wait_row = self.msg_edit_row(f"Please wait {i}secs", wait_row)
|
||||
time.sleep(1)
|
||||
self.msg_del_row(wait_row)
|
||||
|
||||
def loop_broadcast_invalidating(self,tx):
|
||||
self.msg_set_invalidating("Broadcasting")
|
||||
try:
|
||||
tx.add_info_from_wallet(self.bal_window.wallet)
|
||||
self.network.run_from_another_thread(tx.add_info_from_network(self.network))
|
||||
txid = self.network.run_from_another_thread(self.network.broadcast_transaction(tx,timeout=120),timeout=120)
|
||||
self.msg_set_invalidating("Ok")
|
||||
if not txid:
|
||||
_logger.debug(f"should not be none txid: {txid}")
|
||||
|
||||
|
||||
except TxBroadcastError as e:
|
||||
_logger.error(e)
|
||||
msg = e.get_message_for_gui()
|
||||
self.msg_set_invalidating(self.msg_error(msg))
|
||||
except BestEffortRequestFailed as e:
|
||||
self.msg_set_invalidating(self.msg_error(e))
|
||||
# self.loop_broadcast_invalidating(tx)
|
||||
|
||||
def loop_push(self):
|
||||
self.msg_set_pushing("Broadcasting")
|
||||
retry = False
|
||||
try:
|
||||
willexecutors=Willexecutors.get_willexecutor_transactions(self.bal_window.willitems)
|
||||
for url,willexecutor in willexecutors.items():
|
||||
try:
|
||||
if Willexecutors.is_selected(self.bal_window.willexecutors.get(url)):
|
||||
_logger.debug(f"{url}: {willexecutor}")
|
||||
if not Willexecutors.push_transactions_to_willexecutor(willexecutor):
|
||||
for wid in willexecutor['txsids']:
|
||||
self.bal_window.willitems[wid].set_status('PUSH_FAIL',True)
|
||||
retry=True
|
||||
else:
|
||||
for wid in willexecutor['txsids']:
|
||||
self.bal_window.willitems[wid].set_status('PUSHED',True)
|
||||
except Willexecutors.AlreadyPresentException:
|
||||
for wid in willexecutor['txsids']:
|
||||
row = self.msg_edit_row("checking {} - {} : {}".format(self.bal_window.willitems[wid].we['url'],wid, "Waiting"))
|
||||
self.bal_window.willitems[wid].check_willexecutor()
|
||||
row = self.msg_edit_row("checked {} - {} : {}".format(self.bal_window.willitems[wid].we['url'],wid,self.bal_window.willitems[wid].get_status("CHECKED" )),row)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
|
||||
_logger.error(e)
|
||||
raise e
|
||||
if retry:
|
||||
raise Exception("retry")
|
||||
|
||||
except Exception as e:
|
||||
self.msg_set_pushing(self.msg_error(e))
|
||||
self.wait(10)
|
||||
if not self._stopping:
|
||||
self.loop_push()
|
||||
|
||||
|
||||
def invalidate_task(self,tx,password):
|
||||
_logger.debug(f"invalidate tx: {tx}")
|
||||
tx = self.bal_window.wallet.sign_transaction(tx,password)
|
||||
try:
|
||||
if tx:
|
||||
if tx.is_complete():
|
||||
self.loop_broadcast_invalidating(tx)
|
||||
self.wait(5)
|
||||
else:
|
||||
raise
|
||||
else:
|
||||
raise
|
||||
except Exception as e:
|
||||
self.msg_set_invalidating("Error")
|
||||
raise Exception("Impossible to sign")
|
||||
def on_success_invalidate(self,success):
|
||||
self.thread.add(self.task_phase1,on_success=self.on_success_phase1,on_done=self.on_accept,on_error=self.on_error_phase1)
|
||||
def on_error(self,error):
|
||||
_logger.error(error)
|
||||
pass
|
||||
def on_success_phase1(self,result):
|
||||
self.have_to_sign,tx = list(result)
|
||||
#if have_to_sign is False and tx is None:
|
||||
#self._stopping=True
|
||||
#self.on_success_phase2()
|
||||
# return
|
||||
print("have to sign",self.have_to_sign)
|
||||
password=None
|
||||
if self.have_to_sign is None:
|
||||
self.msg_set_invalidating()
|
||||
#need to sign invalidate and restart phase 1
|
||||
|
||||
password = self.bal_window.get_wallet_password("Invalidate your old will",parent=self.bal_window.window)
|
||||
if password is False:
|
||||
self.msg_set_invalidating("Aborted")
|
||||
self.wait(3)
|
||||
self.close()
|
||||
return
|
||||
self.thread.add(partial(self.invalidate_task,tx,password),on_success=self.on_success_invalidate, on_done=self.on_accept, on_error=self.on_error)
|
||||
|
||||
return
|
||||
|
||||
elif self.have_to_sign:
|
||||
password = self.bal_window.get_wallet_password("Sign your will",parent=self.bal_window.window)
|
||||
if password is False:
|
||||
self.msg_set_signing('Aborted')
|
||||
else:
|
||||
self.msg_set_signing('Nothing to do')
|
||||
self.thread.add(partial(self.task_phase2,password),on_success=self.on_success_phase2,on_done=self.on_accept_phase2,on_error=self.on_error_phase2)
|
||||
return
|
||||
|
||||
def on_success_phase2(self,arg=False):
|
||||
self.thread.stop()
|
||||
self.bal_window.save_willitems()
|
||||
self.msg_edit_row("Finished")
|
||||
self.close()
|
||||
|
||||
def closeEvent(self,event):
|
||||
self._stopping=True
|
||||
self.thread.stop()
|
||||
|
||||
def task_phase2(self,password):
|
||||
if self.have_to_sign:
|
||||
try:
|
||||
if txs:=self.bal_window.sign_transactions(password):
|
||||
for txid,tx in txs.items():
|
||||
self.bal_window.willitems[txid].tx = copy.deepcopy(tx)
|
||||
self.bal_window.save_willitems()
|
||||
self.msg_set_signing("Ok")
|
||||
except Exception as e:
|
||||
self.msg_set_signing(self.msg_error(e))
|
||||
|
||||
self.msg_set_pushing()
|
||||
have_to_push = False
|
||||
for wid in Will.only_valid(self.bal_window.willitems):
|
||||
w=self.bal_window.willitems[wid]
|
||||
if w.we and w.get_status("COMPLETE") and not w.get_status("PUSHED"):
|
||||
have_to_push = True
|
||||
if not have_to_push:
|
||||
self.msg_set_pushing("Nothing to do")
|
||||
else:
|
||||
try:
|
||||
self.loop_push()
|
||||
self.msg_set_pushing("Ok")
|
||||
|
||||
except Exception as e:
|
||||
self.msg_set_pushing(self.msg_error(e))
|
||||
self.msg_edit_row("Ok")
|
||||
self.wait(5)
|
||||
|
||||
def on_error_phase1(self,error):
|
||||
_logger.error(f"error phase1: {error}")
|
||||
|
||||
def on_error_phase2(self,error):
|
||||
_logger.error("error phase2: { error}")
|
||||
|
||||
|
||||
def msg_set_checking(self, status = None, row = None):
|
||||
row = self.check_row if row is None else row
|
||||
self.check_row = self.msg_set_status("Checking your will", row, status)
|
||||
|
||||
def msg_set_invalidating(self, status = None, row = None):
|
||||
row = self.inval_row if row is None else row
|
||||
self.inval_row = self.msg_set_status("Invalidating old will", self.inval_row, status)
|
||||
|
||||
def msg_set_building(self, status = None, row = None):
|
||||
row = self.build_row if row is None else row
|
||||
self.build_row = self.msg_set_status("Building your will", self.build_row, status)
|
||||
|
||||
def msg_set_signing(self, status = None, row = None):
|
||||
row = self.sign_row if row is None else row
|
||||
self.sign_row = self.msg_set_status("Signing your will", self.sign_row, status)
|
||||
|
||||
def msg_set_pushing(self, status = None, row = None):
|
||||
row = self.push_row if row is None else row
|
||||
self.push_row = self.msg_set_status("Broadcasting your will to executors", self.push_row, status)
|
||||
|
||||
def msg_set_waiting(self, status = None, row = None):
|
||||
row = self.wait_row if row is None else row
|
||||
self.wait_row = self.msg_edit_row(f"Please wait {status}secs", self.wait_row)
|
||||
|
||||
def msg_error(self,e):
|
||||
return "Error: {}".format(e)
|
||||
|
||||
def msg_set_status(self,msg,row,status=None):
|
||||
status= "Wait" if status is None else status
|
||||
line="{}:\t{}".format(_(msg), status)
|
||||
return self.msg_edit_row(line,row)
|
||||
|
||||
#return v$msg_edit_row("{}:\t{}".format(_(msg), status), row)
|
||||
|
||||
def ask_password(self,msg=None):
|
||||
self.password=self.bal_window.get_wallet_password(msg,parent=self)
|
||||
|
||||
def msg_edit_row(self,line,row=None):
|
||||
_logger.debug(f"{row},{line}")
|
||||
|
||||
#msg=self.get_text()
|
||||
#rows=msg.split("\n")
|
||||
#try:
|
||||
# rows[row]=line
|
||||
#except Exception as e:
|
||||
# rows.append(line)
|
||||
#row=len(rows)-1
|
||||
#self.update("\n".join(rows))
|
||||
|
||||
#return row
|
||||
|
||||
#def msg_edit_label(self,line,row=None):
|
||||
#_logger.trace(f"{row},{line}")
|
||||
|
||||
#msg=self.get_text()
|
||||
#rows=msg.split("\n")
|
||||
try:
|
||||
self.labels[row]=line
|
||||
except Exception as e:
|
||||
self.labels.append(line)
|
||||
row=len(self.labels)-1
|
||||
|
||||
self.updatemessage.emit()
|
||||
|
||||
return row
|
||||
|
||||
def msg_del_row(self,row):
|
||||
#_logger.trace(f"del row: {row}")
|
||||
try:
|
||||
del self.labels[row]
|
||||
except Exception as e:
|
||||
pass
|
||||
self.updatemessage.emit()
|
||||
|
||||
def update(self):
|
||||
self.vbox.removeWidget(self.qwidget)
|
||||
self.qwidget=QWidget(self)
|
||||
labelsbox = QVBoxLayout(self.qwidget)
|
||||
for label in self.labels:
|
||||
labelsbox.addWidget(QLabel(label))
|
||||
self.vbox.addWidget(self.qwidget)
|
||||
|
||||
def get_text(self):
|
||||
return self.message_label.text()
|
||||
def ThreadStopped(Exception):
|
||||
pass
|
||||
283
balqt/heir_list.py
Normal file
283
balqt/heir_list.py
Normal file
@@ -0,0 +1,283 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Electrum - lightweight Bitcoin client
|
||||
# Copyright (C) 2015 Thomas Voegtlin
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person
|
||||
# obtaining a copy of this software and associated documentation files
|
||||
# (the "Software"), to deal in the Software without restriction,
|
||||
# including without limitation the rights to use, copy, modify, merge,
|
||||
# publish, distribute, sublicense, and/or sell copies of the Software,
|
||||
# and to permit persons to whom the Software is furnished to do so,
|
||||
# subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import enum
|
||||
from typing import TYPE_CHECKING
|
||||
from datetime import datetime
|
||||
|
||||
from . import qt_resources
|
||||
if qt_resources.QT_VERSION == 5:
|
||||
from PyQt5.QtGui import QStandardItemModel, QStandardItem
|
||||
from PyQt5.QtCore import Qt, QPersistentModelIndex, QModelIndex
|
||||
from PyQt5.QtWidgets import (QAbstractItemView, QMenu,QWidget,QHBoxLayout,QLabel,QSpinBox,QPushButton)
|
||||
else:
|
||||
from PyQt6.QtGui import QStandardItemModel, QStandardItem
|
||||
from PyQt6.QtCore import Qt, QPersistentModelIndex, QModelIndex
|
||||
from PyQt6.QtWidgets import (QAbstractItemView, QMenu,QWidget,QHBoxLayout,QLabel,QSpinBox,QPushButton)
|
||||
|
||||
from electrum.i18n import _
|
||||
from electrum.bitcoin import is_address
|
||||
from electrum.util import block_explorer_URL
|
||||
from electrum.plugin import run_hook
|
||||
from electrum.gui.qt.util import webopen, MessageBoxMixin,HelpButton
|
||||
from electrum.gui.qt.my_treeview import MyTreeView, MySortModel
|
||||
|
||||
from .. import util as Util
|
||||
from .locktimeedit import HeirsLockTimeEdit
|
||||
if TYPE_CHECKING:
|
||||
from electrum.gui.qt.main_window import ElectrumWindow
|
||||
|
||||
|
||||
class HeirList(MyTreeView,MessageBoxMixin):
|
||||
|
||||
class Columns(MyTreeView.BaseColumnsEnum):
|
||||
NAME = enum.auto()
|
||||
ADDRESS = enum.auto()
|
||||
AMOUNT = enum.auto()
|
||||
|
||||
headers = {
|
||||
Columns.NAME: _('Name'),
|
||||
Columns.ADDRESS: _('Address'),
|
||||
Columns.AMOUNT: _('Amount'),
|
||||
#Columns.LOCKTIME:_('LockTime'),
|
||||
}
|
||||
filter_columns = [Columns.NAME, Columns.ADDRESS]
|
||||
|
||||
ROLE_SORT_ORDER = Qt.ItemDataRole.UserRole + 1000
|
||||
|
||||
ROLE_HEIR_KEY = Qt.ItemDataRole.UserRole + 1001
|
||||
key_role = ROLE_HEIR_KEY
|
||||
|
||||
def __init__(self, bal_window: 'BalWindow'):
|
||||
super().__init__(
|
||||
parent=bal_window.window,
|
||||
main_window=bal_window.window,
|
||||
stretch_column=self.Columns.NAME,
|
||||
editable_columns=[self.Columns.NAME,self.Columns.ADDRESS,self.Columns.AMOUNT],
|
||||
)
|
||||
self.decimal_point = bal_window.bal_plugin.config.get_decimal_point()
|
||||
self.bal_window = bal_window
|
||||
|
||||
try:
|
||||
self.setModel(QStandardItemModel(self))
|
||||
self.sortByColumn(self.Columns.NAME, Qt.SortOrder.AscendingOrder)
|
||||
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
||||
except:
|
||||
pass
|
||||
#self.sortByColumn(self.Columns.NAME, Qt.SortOrder.AscendingOrder)
|
||||
#self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
||||
|
||||
self.setSortingEnabled(True)
|
||||
self.std_model = self.model()
|
||||
|
||||
self.update()
|
||||
|
||||
|
||||
def on_edited(self, idx, edit_key, *, text):
|
||||
original = prior_name = self.bal_window.heirs.get(edit_key)
|
||||
if not prior_name:
|
||||
return
|
||||
col = idx.column()
|
||||
try:
|
||||
#if col == 3:
|
||||
# try:
|
||||
# text = Util.str_to_locktime(text)
|
||||
# except:
|
||||
# print("not a valid locktime")
|
||||
# pass
|
||||
if col == 2:
|
||||
text = Util.encode_amount(text,self.decimal_point)
|
||||
elif col == 0:
|
||||
self.bal_window.delete_heirs([edit_key])
|
||||
edit_key = text
|
||||
prior_name[col-1] = text
|
||||
prior_name.insert(0,edit_key)
|
||||
prior_name = tuple(prior_name)
|
||||
except Exception as e:
|
||||
#print("eccezione tupla",e)
|
||||
prior_name = (edit_key,)+prior_name[:col-1]+(text,)+prior_name[col:]
|
||||
#print("prior_name",prior_name,original)
|
||||
|
||||
try:
|
||||
self.bal_window.set_heir(prior_name)
|
||||
#print("setheir")
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
#print("heir non valido ripristino l'originale",e)
|
||||
try:
|
||||
#print("setup_original",(edit_key,)+original)
|
||||
self.bal_window.set_heir((edit_key,)+original)
|
||||
except Exception as e:
|
||||
#print("errore nellimpostare original",e,original)
|
||||
self.update()
|
||||
|
||||
def create_menu(self, position):
|
||||
menu = QMenu()
|
||||
idx = self.indexAt(position)
|
||||
column = idx.column() or self.Columns.NAME
|
||||
selected_keys = []
|
||||
for s_idx in self.selected_in_column(self.Columns.NAME):
|
||||
#print(s_idx)
|
||||
sel_key = self.model().itemFromIndex(s_idx).data(0)
|
||||
selected_keys.append(sel_key)
|
||||
if selected_keys and idx.isValid():
|
||||
column_title = self.model().horizontalHeaderItem(column).text()
|
||||
column_data = '\n'.join(self.model().itemFromIndex(s_idx).text()
|
||||
for s_idx in self.selected_in_column(column))
|
||||
menu.addAction(_("Copy {}").format(column_title), lambda: self.place_text_on_clipboard(column_data, title=column_title))
|
||||
if column in self.editable_columns:
|
||||
item = self.model().itemFromIndex(idx)
|
||||
if item.isEditable():
|
||||
# would not be editable if openalias
|
||||
persistent = QPersistentModelIndex(idx)
|
||||
menu.addAction(_("Edit {}").format(column_title), lambda p=persistent: self.edit(QModelIndex(p)))
|
||||
menu.addAction(_("Delete"), lambda: self.bal_window.delete_heirs(selected_keys))
|
||||
menu.exec(self.viewport().mapToGlobal(position))
|
||||
|
||||
def update(self):
|
||||
if self.maybe_defer_update():
|
||||
return
|
||||
current_key = self.get_role_data_for_current_item(col=self.Columns.NAME, role=self.ROLE_HEIR_KEY)
|
||||
self.model().clear()
|
||||
self.update_headers(self.__class__.headers)
|
||||
set_current = None
|
||||
for key in sorted(self.bal_window.heirs.keys()):
|
||||
heir = self.bal_window.heirs[key]
|
||||
labels = [""] * len(self.Columns)
|
||||
labels[self.Columns.NAME] = key
|
||||
labels[self.Columns.ADDRESS] = heir[0]
|
||||
labels[self.Columns.AMOUNT] = Util.decode_amount(heir[1],self.decimal_point)
|
||||
#labels[self.Columns.LOCKTIME] = str(Util.locktime_to_str(heir[2]))
|
||||
|
||||
items = [QStandardItem(x) for x in labels]
|
||||
items[self.Columns.NAME].setEditable(True)
|
||||
items[self.Columns.ADDRESS].setEditable(True)
|
||||
items[self.Columns.AMOUNT].setEditable(True)
|
||||
#items[self.Columns.LOCKTIME].setEditable(True)
|
||||
items[self.Columns.NAME].setData(key, self.ROLE_HEIR_KEY+1)
|
||||
items[self.Columns.ADDRESS].setData(key, self.ROLE_HEIR_KEY+2)
|
||||
items[self.Columns.AMOUNT].setData(key, self.ROLE_HEIR_KEY+3)
|
||||
#items[self.Columns.LOCKTIME].setData(key, self.ROLE_HEIR_KEY+4)
|
||||
|
||||
self.model().insertRow(self.model().rowCount(), items)
|
||||
|
||||
if key == current_key:
|
||||
idx = self.model().index(row_count, self.Columns.NAME)
|
||||
set_current = QPersistentModelIndex(idx)
|
||||
self.set_current_idx(set_current)
|
||||
# FIXME refresh loses sort order; so set "default" here:
|
||||
self.filter()
|
||||
run_hook('update_heirs_tab', self)
|
||||
|
||||
def refresh_row(self, key, row):
|
||||
# nothing to update here
|
||||
pass
|
||||
|
||||
def get_edit_key_from_coordinate(self, row, col):
|
||||
#print("role_data",self.get_role_data_from_coordinate(row, col, role=self.ROLE_HEIR_KEY))
|
||||
#print(col)
|
||||
return self.get_role_data_from_coordinate(row, col, role=self.ROLE_HEIR_KEY+col+1)
|
||||
return col
|
||||
|
||||
def create_toolbar(self, config):
|
||||
toolbar, menu = self.create_toolbar_with_menu('')
|
||||
menu.addAction(_("&New Heir"), self.bal_window.new_heir_dialog)
|
||||
menu.addAction(_("Import"), self.bal_window.import_heirs)
|
||||
menu.addAction(_("Export"), lambda: self.bal_window.export_heirs())
|
||||
#menu.addAction(_("Build Traonsactions"), self.build_transactions)
|
||||
|
||||
self.heir_locktime = HeirsLockTimeEdit(self.window(),0)
|
||||
def on_heir_locktime():
|
||||
if not self.heir_locktime.get_locktime():
|
||||
self.heir_locktime.set_locktime('1y')
|
||||
self.bal_window.will_settings['locktime'] = self.heir_locktime.get_locktime() if self.heir_locktime.get_locktime() else "1y"
|
||||
self.bal_window.bal_plugin.config.set_key('will_settings',self.bal_window.will_settings,save = True)
|
||||
self.heir_locktime.valueEdited.connect(on_heir_locktime)
|
||||
|
||||
self.heir_threshold = HeirsLockTimeEdit(self,0)
|
||||
def on_heir_threshold():
|
||||
if not self.heir_threshold.get_locktime():
|
||||
self.heir_threshold.set_locktime('180d')
|
||||
|
||||
self.bal_window.will_settings['threshold'] = self.heir_threshold.get_locktime()
|
||||
self.bal_window.bal_plugin.config.set_key('will_settings',self.bal_window.will_settings,save = True)
|
||||
self.heir_threshold.valueEdited.connect(on_heir_threshold)
|
||||
|
||||
self.heir_tx_fees = QSpinBox()
|
||||
self.heir_tx_fees.setMinimum(1)
|
||||
self.heir_tx_fees.setMaximum(10000)
|
||||
def on_heir_tx_fees():
|
||||
if not self.heir_tx_fees.value():
|
||||
self.heir_tx_fees.set_value(1)
|
||||
self.bal_window.will_settings['tx_fees'] = self.heir_tx_fees.value()
|
||||
self.bal_window.bal_plugin.config.set_key('will_settings',self.bal_window.will_settings,save = True)
|
||||
self.heir_tx_fees.valueChanged.connect(on_heir_tx_fees)
|
||||
|
||||
|
||||
self.heirs_widget = QWidget()
|
||||
layout = QHBoxLayout()
|
||||
self.heirs_widget.setLayout(layout)
|
||||
|
||||
layout.addWidget(QLabel(_("Delivery Time:")))
|
||||
layout.addWidget(self.heir_locktime)
|
||||
layout.addWidget(HelpButton(_("Locktime* to be used in the transaction\n"
|
||||
+"if you choose Raw, you can insert various options based on suffix:\n"
|
||||
#+" - b: number of blocks after current block(ex: 144b means tomorrow)\n"
|
||||
+" - d: number of days after current day(ex: 1d means tomorrow)\n"
|
||||
+" - y: number of years after currrent day(ex: 1y means one year from today)\n"
|
||||
+"* locktime can be anticipated to update will\n")))
|
||||
|
||||
layout.addWidget(QLabel(" "))
|
||||
layout.addWidget(QLabel(_("Check Alive:")))
|
||||
layout.addWidget(self.heir_threshold)
|
||||
layout.addWidget(HelpButton(_("Check to ask for invalidation.\n"
|
||||
+"When less then this time is missing, ask to invalidate.\n"
|
||||
+"If you fail to invalidate during this time, your transactions will be delivered to your heirs.\n"
|
||||
+"if you choose Raw, you can insert various options based on suffix:\n"
|
||||
#+" - b: number of blocks after current block(ex: 144b means tomorrow)\n"
|
||||
+" - d: number of days after current day(ex: 1d means tomorrow).\n"
|
||||
+" - y: number of years after currrent day(ex: 1y means one year from today).\n\n")))
|
||||
layout.addWidget(QLabel(" "))
|
||||
layout.addWidget(QLabel(_("Fees:")))
|
||||
layout.addWidget(self.heir_tx_fees)
|
||||
layout.addWidget(HelpButton(_("Fee to be used in the transaction")))
|
||||
layout.addWidget(QLabel("sats/vbyte"))
|
||||
layout.addWidget(QLabel(" "))
|
||||
newHeirButton = QPushButton(_("New Heir"))
|
||||
newHeirButton.clicked.connect(self.bal_window.new_heir_dialog)
|
||||
layout.addWidget(newHeirButton)
|
||||
|
||||
toolbar.insertWidget(2, self.heirs_widget)
|
||||
|
||||
return toolbar
|
||||
def update_will_settings(self):
|
||||
self.heir_threshold.set_locktime(self.bal_window.will_settings['threshold'])
|
||||
self.heir_locktime.set_locktime(self.bal_window.will_settings['locktime'])
|
||||
self.heir_tx_fees.setValue(int(self.bal_window.will_settings['tx_fees']))
|
||||
|
||||
def build_transactions(self):
|
||||
will = self.bal_window.prepare_will()
|
||||
|
||||
255
balqt/locktimeedit.py
Normal file
255
balqt/locktimeedit.py
Normal file
@@ -0,0 +1,255 @@
|
||||
# Copyright (C) 2020 The Electrum developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENCE or http://www.opensource.org/licenses/mit-license.php
|
||||
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import Optional, Any
|
||||
|
||||
from . import qt_resources
|
||||
if qt_resources.QT_VERSION == 5:
|
||||
from PyQt5.QtCore import Qt, QDateTime, pyqtSignal
|
||||
from PyQt5.QtGui import QPalette, QPainter
|
||||
from PyQt5.QtWidgets import (QWidget, QLineEdit, QStyle, QStyleOptionFrame, QComboBox, QHBoxLayout, QDateTimeEdit)
|
||||
else:
|
||||
from PyQt6.QtCore import Qt, QDateTime, pyqtSignal
|
||||
from PyQt6.QtGui import QPalette, QPainter
|
||||
from PyQt6.QtWidgets import (QWidget, QLineEdit, QStyle, QStyleOptionFrame, QComboBox, QHBoxLayout, QDateTimeEdit)
|
||||
|
||||
from electrum.i18n import _
|
||||
from electrum.bitcoin import NLOCKTIME_MIN, NLOCKTIME_MAX, NLOCKTIME_BLOCKHEIGHT_MAX
|
||||
|
||||
from electrum.gui.qt.util import char_width_in_lineedit, ColorScheme
|
||||
|
||||
|
||||
class HeirsLockTimeEdit(QWidget):
|
||||
|
||||
valueEdited = pyqtSignal()
|
||||
locktime_threshold = 50000000
|
||||
def __init__(self, parent=None,default_index = 1):
|
||||
QWidget.__init__(self, parent)
|
||||
|
||||
hbox = QHBoxLayout()
|
||||
self.setLayout(hbox)
|
||||
hbox.setContentsMargins(0, 0, 0, 0)
|
||||
hbox.setSpacing(0)
|
||||
|
||||
self.locktime_raw_e = LockTimeRawEdit(self,time_edit = self)
|
||||
#self.locktime_height_e = LockTimeHeightEdit(self)
|
||||
self.locktime_date_e = LockTimeDateEdit(self,time_edit = self)
|
||||
#self.editors = [self.locktime_raw_e, self.locktime_height_e, self.locktime_date_e]
|
||||
self.editors = [self.locktime_raw_e, self.locktime_date_e]
|
||||
|
||||
self.combo = QComboBox()
|
||||
#options = [_("Raw"), _("Block height"), _("Date")]
|
||||
options = [_("Raw"),_("Date")]
|
||||
self.option_index_to_editor_map = {
|
||||
0: self.locktime_raw_e,
|
||||
#1: self.locktime_height_e,
|
||||
1: self.locktime_date_e,
|
||||
#2: self.locktime_date_e,
|
||||
}
|
||||
self.combo.addItems(options)
|
||||
|
||||
|
||||
self.editor = self.option_index_to_editor_map[default_index]
|
||||
self.combo.currentIndexChanged.connect(self.on_current_index_changed)
|
||||
self.combo.setCurrentIndex(default_index)
|
||||
self.on_current_index_changed(default_index)
|
||||
|
||||
hbox.addWidget(self.combo)
|
||||
for w in self.editors:
|
||||
hbox.addWidget(w)
|
||||
hbox.addStretch(1)
|
||||
|
||||
#self.locktime_height_e.textEdited.connect(self.valueEdited.emit)
|
||||
self.locktime_raw_e.editingFinished.connect(self.valueEdited.emit)
|
||||
self.locktime_date_e.dateTimeChanged.connect(self.valueEdited.emit)
|
||||
self.combo.currentIndexChanged.connect(self.valueEdited.emit)
|
||||
|
||||
def on_current_index_changed(self,i):
|
||||
for w in self.editors:
|
||||
w.setVisible(False)
|
||||
w.setEnabled(False)
|
||||
prev_locktime = self.editor.get_locktime()
|
||||
self.editor = self.option_index_to_editor_map[i]
|
||||
if self.editor.is_acceptable_locktime(prev_locktime):
|
||||
self.editor.set_locktime(prev_locktime,force=True)
|
||||
self.editor.setVisible(True)
|
||||
self.editor.setEnabled(True)
|
||||
|
||||
def get_locktime(self) -> Optional[str]:
|
||||
return self.editor.get_locktime()
|
||||
|
||||
def set_index(self,index):
|
||||
self.combo.setCurrentIndex(index)
|
||||
self.on_current_index_changed(index)
|
||||
|
||||
def set_locktime(self, x: Any,force=True) -> None:
|
||||
self.editor.set_locktime(x,force)
|
||||
|
||||
|
||||
class _LockTimeEditor:
|
||||
min_allowed_value = NLOCKTIME_MIN
|
||||
max_allowed_value = NLOCKTIME_MAX
|
||||
|
||||
def get_locktime(self) -> Optional[int]:
|
||||
raise NotImplementedError()
|
||||
|
||||
def set_locktime(self, x: Any,force=True) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
@classmethod
|
||||
def is_acceptable_locktime(cls, x: Any) -> bool:
|
||||
if not x: # e.g. empty string
|
||||
return True
|
||||
try:
|
||||
x = int(x)
|
||||
except Exception as e:
|
||||
return False
|
||||
return cls.min_allowed_value <= x <= cls.max_allowed_value
|
||||
|
||||
|
||||
class LockTimeRawEdit(QLineEdit, _LockTimeEditor):
|
||||
|
||||
def __init__(self, parent=None,time_edit=None):
|
||||
QLineEdit.__init__(self, parent)
|
||||
self.setFixedWidth(14 * char_width_in_lineedit())
|
||||
self.textChanged.connect(self.numbify)
|
||||
self.isdays = False
|
||||
self.isyears = False
|
||||
self.isblocks = False
|
||||
self.time_edit=time_edit
|
||||
|
||||
def replace_str(self,text):
|
||||
return str(text).replace('d','').replace('y','').replace('b','')
|
||||
|
||||
def checkbdy(self,s,pos,appendix):
|
||||
try:
|
||||
charpos = pos-1
|
||||
charpos = max(0,charpos)
|
||||
charpos = min(len(s)-1,charpos)
|
||||
if appendix == s[charpos]:
|
||||
s=self.replace_str(s)+appendix
|
||||
pos = charpos
|
||||
except Exception as e:
|
||||
pass
|
||||
return pos, s
|
||||
|
||||
def numbify(self):
|
||||
text = self.text().strip()
|
||||
#chars = '0123456789bdy' removed the option to choose locktime by block
|
||||
chars = '0123456789dy'
|
||||
pos = posx = self.cursorPosition()
|
||||
pos = len(''.join([i for i in text[:pos] if i in chars]))
|
||||
s = ''.join([i for i in text if i in chars])
|
||||
self.isdays = False
|
||||
self.isyears = False
|
||||
self.isblocks = False
|
||||
|
||||
pos,s = self.checkbdy(s,pos,'d')
|
||||
pos,s = self.checkbdy(s,pos,'y')
|
||||
pos,s = self.checkbdy(s,pos,'b')
|
||||
|
||||
if 'd' in s: self.isdays = True
|
||||
if 'y' in s: self.isyears = True
|
||||
if 'b' in s: self.isblocks = True
|
||||
|
||||
|
||||
if self.isdays: s= self.replace_str(s) + 'd'
|
||||
if self.isyears: s = self.replace_str(s) + 'y'
|
||||
if self.isblocks: s= self.replace_str(s) + 'b'
|
||||
|
||||
self.set_locktime(s,force=False)
|
||||
# setText sets Modified to False. Instead we want to remember
|
||||
# if updates were because of user modification.
|
||||
self.setModified(self.hasFocus())
|
||||
self.setCursorPosition(pos)
|
||||
|
||||
def get_locktime(self) -> Optional[str]:
|
||||
try:
|
||||
return str(self.text())
|
||||
except Exception as e:
|
||||
return None
|
||||
|
||||
def set_locktime(self, x: Any,force=True) -> None:
|
||||
out = str(x)
|
||||
if 'd' in out:
|
||||
out = self.replace_str(x)+'d'
|
||||
elif 'y' in out:
|
||||
out = self.replace_str(x)+'y'
|
||||
elif 'b' in out:
|
||||
out = self.replace_str(x)+'b'
|
||||
else:
|
||||
try:
|
||||
out = int(x)
|
||||
except Exception as e:
|
||||
self.setText('')
|
||||
return
|
||||
out = max(out, self.min_allowed_value)
|
||||
out = min(out, self.max_allowed_value)
|
||||
self.setText(str(out))
|
||||
#try:
|
||||
# if self.time_edit and int(out)>self.time_edit.locktime_threshold and not force:
|
||||
# self.time_edit.set_index(1)
|
||||
#except:
|
||||
# pass
|
||||
|
||||
class LockTimeHeightEdit(LockTimeRawEdit):
|
||||
max_allowed_value = NLOCKTIME_BLOCKHEIGHT_MAX
|
||||
|
||||
def __init__(self, parent=None,time_edit=None):
|
||||
LockTimeRawEdit.__init__(self, parent)
|
||||
self.setFixedWidth(20 * char_width_in_lineedit())
|
||||
self.time_edit = time_edit
|
||||
|
||||
def paintEvent(self, event):
|
||||
super().paintEvent(event)
|
||||
panel = QStyleOptionFrame()
|
||||
self.initStyleOption(panel)
|
||||
textRect = self.style().subElementRect(QStyle.SE_LineEditContents, panel, self)
|
||||
textRect.adjust(2, 0, -10, 0)
|
||||
painter = QPainter(self)
|
||||
painter.setPen(ColorScheme.GRAY.as_color())
|
||||
painter.drawText(textRect, int(Qt.AlignRight | Qt.AlignVCenter), "height")
|
||||
|
||||
|
||||
def get_max_allowed_timestamp() -> int:
|
||||
ts = NLOCKTIME_MAX
|
||||
# Test if this value is within the valid timestamp limits (which is platform-dependent).
|
||||
# see #6170
|
||||
try:
|
||||
datetime.fromtimestamp(ts)
|
||||
except (OSError, OverflowError):
|
||||
ts = 2 ** 31 - 1 # INT32_MAX
|
||||
datetime.fromtimestamp(ts) # test if raises
|
||||
return ts
|
||||
|
||||
|
||||
class LockTimeDateEdit(QDateTimeEdit, _LockTimeEditor):
|
||||
min_allowed_value = NLOCKTIME_BLOCKHEIGHT_MAX + 1
|
||||
max_allowed_value = get_max_allowed_timestamp()
|
||||
|
||||
def __init__(self, parent=None,time_edit=None):
|
||||
QDateTimeEdit.__init__(self, parent)
|
||||
self.setMinimumDateTime(datetime.fromtimestamp(self.min_allowed_value))
|
||||
self.setMaximumDateTime(datetime.fromtimestamp(self.max_allowed_value))
|
||||
self.setDateTime(QDateTime.currentDateTime())
|
||||
self.time_edit = time_edit
|
||||
|
||||
def get_locktime(self) -> Optional[int]:
|
||||
dt = self.dateTime().toPyDateTime()
|
||||
locktime = int(time.mktime(dt.timetuple()))
|
||||
return locktime
|
||||
|
||||
def set_locktime(self, x: Any,force = False) -> None:
|
||||
if not self.is_acceptable_locktime(x):
|
||||
self.setDateTime(QDateTime.currentDateTime())
|
||||
return
|
||||
try:
|
||||
x = int(x)
|
||||
except Exception:
|
||||
self.setDateTime(QDateTime.currentDateTime())
|
||||
return
|
||||
dt = datetime.fromtimestamp(x)
|
||||
self.setDateTime(dt)
|
||||
331
balqt/preview_dialog.py
Normal file
331
balqt/preview_dialog.py
Normal file
@@ -0,0 +1,331 @@
|
||||
|
||||
import enum
|
||||
import copy
|
||||
import json
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
from functools import partial
|
||||
|
||||
from . import qt_resources
|
||||
if qt_resources.QT_VERSION == 5:
|
||||
from PyQt5.QtGui import QStandardItemModel, QStandardItem, QPalette, QColor
|
||||
from PyQt5.QtCore import Qt,QPersistentModelIndex, QModelIndex
|
||||
from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QLabel,QMenu,QAbstractItemView,QWidget)
|
||||
else:
|
||||
from PyQt6.QtGui import QStandardItemModel, QStandardItem, QPalette, QColor
|
||||
from PyQt6.QtCore import Qt,QPersistentModelIndex, QModelIndex
|
||||
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QLabel,QMenu,QAbstractItemView,QWidget)
|
||||
|
||||
from electrum.i18n import _
|
||||
from electrum.gui.qt.util import (Buttons,read_QIcon, import_meta_gui, export_meta_gui,MessageBoxMixin)
|
||||
from electrum.util import write_json_file,read_json_file,FileImportFailed
|
||||
from electrum.gui.qt.my_treeview import MyTreeView
|
||||
from electrum.transaction import tx_from_any
|
||||
from electrum.network import Network
|
||||
|
||||
|
||||
from ..bal import BalPlugin
|
||||
from .. import willexecutors as Willexecutors
|
||||
from .. import util as Util
|
||||
from .. import will as Will
|
||||
from .baldialog import BalDialog
|
||||
|
||||
class PreviewList(MyTreeView):
|
||||
class Columns(MyTreeView.BaseColumnsEnum):
|
||||
LOCKTIME = enum.auto()
|
||||
TXID = enum.auto()
|
||||
WILLEXECUTOR = enum.auto()
|
||||
STATUS = enum.auto()
|
||||
|
||||
headers = {
|
||||
Columns.LOCKTIME: _('Locktime'),
|
||||
Columns.TXID: _('Txid'),
|
||||
Columns.WILLEXECUTOR: _('Will-Executor'),
|
||||
Columns.STATUS: _('Status'),
|
||||
}
|
||||
|
||||
ROLE_HEIR_KEY = Qt.ItemDataRole.UserRole + 2000
|
||||
key_role = ROLE_HEIR_KEY
|
||||
|
||||
def __init__(self, parent: 'BalWindow',will):
|
||||
super().__init__(
|
||||
parent=parent.window,
|
||||
stretch_column=self.Columns.TXID,
|
||||
)
|
||||
self.decimal_point=parent.bal_plugin.config.get_decimal_point
|
||||
self.setModel(QStandardItemModel(self))
|
||||
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
||||
|
||||
|
||||
if not will is None:
|
||||
self.will = will
|
||||
else:
|
||||
self.will = parent.willitems
|
||||
|
||||
self.bal_window = parent
|
||||
self.wallet=parent.window.wallet
|
||||
self.setModel(QStandardItemModel(self))
|
||||
self.setSortingEnabled(True)
|
||||
self.std_model = self.model()
|
||||
self.config = parent.bal_plugin.config
|
||||
self.bal_plugin=self.bal_window.bal_plugin
|
||||
|
||||
self.update()
|
||||
|
||||
def create_menu(self, position):
|
||||
menu = QMenu()
|
||||
idx = self.indexAt(position)
|
||||
column = idx.column() or self.Columns.TXID
|
||||
selected_keys = []
|
||||
for s_idx in self.selected_in_column(self.Columns.TXID):
|
||||
sel_key = self.model().itemFromIndex(s_idx).data(0)
|
||||
selected_keys.append(sel_key)
|
||||
if selected_keys and idx.isValid():
|
||||
column_title = self.model().horizontalHeaderItem(column).text()
|
||||
column_data = '\n'.join(self.model().itemFromIndex(s_idx).text()
|
||||
for s_idx in self.selected_in_column(column))
|
||||
|
||||
menu.addAction(_("details").format(column_title), lambda: self.show_transaction(selected_keys)).setEnabled(len(selected_keys)<2)
|
||||
menu.addAction(_("check ").format(column_title), lambda: self.check_transactions(selected_keys))
|
||||
|
||||
menu.addSeparator()
|
||||
menu.addAction(_("delete").format(column_title), lambda: self.delete(selected_keys))
|
||||
|
||||
menu.exec(self.viewport().mapToGlobal(position))
|
||||
|
||||
def delete(self,selected_keys):
|
||||
for key in selected_keys:
|
||||
del self.will[key]
|
||||
try:
|
||||
del self.bal_window.willitems[key]
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
del self.bal_window.will[key]
|
||||
except:
|
||||
pass
|
||||
self.update()
|
||||
|
||||
def check_transactions(self,selected_keys):
|
||||
wout = {}
|
||||
for k in selected_keys:
|
||||
wout[k] = self.will[k]
|
||||
if wout:
|
||||
self.bal_window.check_transactions(wout)
|
||||
self.update()
|
||||
|
||||
def show_transaction(self,selected_keys):
|
||||
for key in selected_keys:
|
||||
self.bal_window.show_transaction(self.will[key].tx)
|
||||
|
||||
self.update()
|
||||
|
||||
def select(self,selected_keys):
|
||||
self.selected += selected_keys
|
||||
self.update()
|
||||
|
||||
def deselect(self,selected_keys):
|
||||
for key in selected_keys:
|
||||
self.selected.remove(key)
|
||||
self.update()
|
||||
|
||||
def update_will(self,will):
|
||||
self.will.update(will)
|
||||
self.update()
|
||||
|
||||
def update(self):
|
||||
if self.will is None:
|
||||
return
|
||||
|
||||
current_key = self.get_role_data_for_current_item(col=self.Columns.TXID, role=self.ROLE_HEIR_KEY)
|
||||
self.model().clear()
|
||||
self.update_headers(self.__class__.headers)
|
||||
|
||||
|
||||
|
||||
|
||||
set_current = None
|
||||
for txid,bal_tx in self.will.items():
|
||||
if self.bal_window.bal_plugin._hide_replaced and bal_tx.get_status('REPLACED'):
|
||||
continue
|
||||
if self.bal_window.bal_plugin._hide_invalidated and bal_tx.get_status('INVALIDATED'):
|
||||
continue
|
||||
|
||||
|
||||
tx=bal_tx.tx
|
||||
labels = [""] * len(self.Columns)
|
||||
labels[self.Columns.LOCKTIME] = Util.locktime_to_str(tx.locktime)
|
||||
labels[self.Columns.TXID] = txid
|
||||
we = 'None'
|
||||
if bal_tx.we:
|
||||
we = bal_tx.we['url']
|
||||
labels[self.Columns.WILLEXECUTOR]=we
|
||||
status = bal_tx.status
|
||||
if len(bal_tx.status) > 53:
|
||||
status = "...{}".format(status[-50:])
|
||||
labels[self.Columns.STATUS] = status
|
||||
|
||||
|
||||
|
||||
items=[]
|
||||
for e in labels:
|
||||
if type(e)== list:
|
||||
try:
|
||||
items.append(QStandardItem(*e))
|
||||
except Exception as e:
|
||||
pass
|
||||
else:
|
||||
items.append(QStandardItem(str(e)))
|
||||
|
||||
#pal = QPalette()
|
||||
#pal.setColor(QPalette.ColorRole.Window, QColor(bal_tx.get_color()))
|
||||
#items[-1].setAutoFillBackground(True)
|
||||
#items[-1o].setPalette(pal)
|
||||
items[-1].setBackground(QColor(bal_tx.get_color()))
|
||||
|
||||
|
||||
self.model().insertRow(self.model().rowCount(), items)
|
||||
if txid == current_key:
|
||||
idx = self.model().index(row_count, self.Columns.TXID)
|
||||
set_current = QPersistentModelIndex(idx)
|
||||
self.set_current_idx(set_current)
|
||||
|
||||
def create_toolbar(self, config):
|
||||
toolbar, menu = self.create_toolbar_with_menu('')
|
||||
menu.addAction(_("Prepare"), self.build_transactions)
|
||||
menu.addAction(_("Display"), self.bal_window.preview_modal_dialog)
|
||||
menu.addAction(_("Sign"), self.ask_password_and_sign_transactions)
|
||||
menu.addAction(_("Export"), self.export_will)
|
||||
#menu.addAction(_("Import"), self.import_will)
|
||||
menu.addAction(_("Broadcast"), self.broadcast)
|
||||
menu.addAction(_("Check"), self.check)
|
||||
menu.addAction(_("Invalidate"), self.invalidate_will)
|
||||
prepareButton = QPushButton(_("Prepare"))
|
||||
prepareButton.clicked.connect(self.build_transactions)
|
||||
signButton = QPushButton(_("Sign"))
|
||||
signButton.clicked.connect(self.ask_password_and_sign_transactions)
|
||||
pushButton = QPushButton(_("Broadcast"))
|
||||
pushButton.clicked.connect(self.broadcast)
|
||||
displayButton = QPushButton(_("Display"))
|
||||
displayButton.clicked.connect(self.bal_window.preview_modal_dialog)
|
||||
hlayout = QHBoxLayout()
|
||||
widget = QWidget()
|
||||
hlayout.addWidget(prepareButton)
|
||||
hlayout.addWidget(signButton)
|
||||
hlayout.addWidget(pushButton)
|
||||
hlayout.addWidget(displayButton)
|
||||
widget.setLayout(hlayout)
|
||||
toolbar.insertWidget(2,widget)
|
||||
|
||||
return toolbar
|
||||
|
||||
def hide_replaced(self):
|
||||
self.bal_window.bal_plugin.hide_replaced()
|
||||
self.update()
|
||||
|
||||
def hide_invalidated(self):
|
||||
f.bal_window.bal_plugin.hide_invalidated()
|
||||
self.update()
|
||||
|
||||
def build_transactions(self):
|
||||
will = self.bal_window.prepare_will()
|
||||
if will:
|
||||
self.update_will(will)
|
||||
|
||||
def export_json_file(self,path):
|
||||
write_json_file(path, self.will)
|
||||
|
||||
def export_will(self):
|
||||
self.bal_window.export_will()
|
||||
self.update()
|
||||
|
||||
def import_will(self):
|
||||
self.bal_window.import_will()
|
||||
|
||||
def ask_password_and_sign_transactions(self):
|
||||
self.bal_window.ask_password_and_sign_transactions(callback=self.update)
|
||||
|
||||
def broadcast(self):
|
||||
self.bal_window.broadcast_transactions()
|
||||
self.update()
|
||||
|
||||
def check(self):
|
||||
self.bal_window.check_transactions(self.bal_window.willitems)
|
||||
self.update()
|
||||
|
||||
def invalidate_will(self):
|
||||
self.bal_window.invalidate_will()
|
||||
self.update()
|
||||
|
||||
class PreviewDialog(BalDialog,MessageBoxMixin):
|
||||
def __init__(self, bal_window, will):
|
||||
self.parent = bal_window.window
|
||||
BalDialog.__init__(self,bal_window = bal_window)
|
||||
self.bal_plugin = bal_window.bal_plugin
|
||||
self.gui_object = self.bal_plugin.gui_object
|
||||
self.config = self.bal_plugin.config
|
||||
self.bal_window = bal_window
|
||||
self.wallet = bal_window.window.wallet
|
||||
self.format_amount = bal_window.window.format_amount
|
||||
self.base_unit = bal_window.window.base_unit
|
||||
self.format_fiat_and_units = bal_window.window.format_fiat_and_units
|
||||
self.fx = bal_window.window.fx
|
||||
self.format_fee_rate = bal_window.window.format_fee_rate
|
||||
self.show_address = bal_window.window.show_address
|
||||
if not will:
|
||||
self.will = bal_window.willitems
|
||||
else:
|
||||
self.will = will
|
||||
self.setWindowTitle(_('Transactions Preview'))
|
||||
self.setMinimumSize(1000, 200)
|
||||
self.size_label = QLabel()
|
||||
self.transactions_list = PreviewList(self.bal_window,self.will)
|
||||
vbox = QVBoxLayout(self)
|
||||
vbox.addWidget(self.size_label)
|
||||
vbox.addWidget(self.transactions_list)
|
||||
buttonbox = QHBoxLayout()
|
||||
|
||||
b = QPushButton(_('Sign'))
|
||||
b.clicked.connect(self.transactions_list.ask_password_and_sign_transactions)
|
||||
buttonbox.addWidget(b)
|
||||
|
||||
b = QPushButton(_('Export Will'))
|
||||
b.clicked.connect(self.transactions_list.export_will)
|
||||
buttonbox.addWidget(b)
|
||||
|
||||
b = QPushButton(_('Broadcast'))
|
||||
b.clicked.connect(self.transactions_list.broadcast)
|
||||
buttonbox.addWidget(b)
|
||||
|
||||
b = QPushButton(_('Invalidate will'))
|
||||
b.clicked.connect(self.transactions_list.invalidate_will)
|
||||
buttonbox.addWidget(b)
|
||||
|
||||
vbox.addLayout(buttonbox)
|
||||
|
||||
self.update()
|
||||
|
||||
|
||||
def update_will(self,will):
|
||||
self.will.update(will)
|
||||
self.transactions_list.update_will(will)
|
||||
self.update()
|
||||
|
||||
def update(self):
|
||||
self.transactions_list.update()
|
||||
|
||||
def is_hidden(self):
|
||||
return self.isMinimized() or self.isHidden()
|
||||
|
||||
def show_or_hide(self):
|
||||
if self.is_hidden():
|
||||
self.bring_to_top()
|
||||
else:
|
||||
self.hide()
|
||||
|
||||
def bring_to_top(self):
|
||||
self.show()
|
||||
self.raise_()
|
||||
|
||||
def closeEvent(self, event):
|
||||
event.accept()
|
||||
16
balqt/qt_resources.py
Normal file
16
balqt/qt_resources.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from .. import bal_resources
|
||||
import sys
|
||||
try:
|
||||
QT_VERSION=sys._GUI_QT_VERSION
|
||||
except:
|
||||
QT_VERSION=6
|
||||
if QT_VERSION == 5:
|
||||
from PyQt5.QtGui import QIcon,QPixmap
|
||||
else:
|
||||
from PyQt6.QtGui import QIcon,QPixmap
|
||||
|
||||
def read_QIcon(icon_basename: str=bal_resources.DEFAULT_ICON) -> QIcon:
|
||||
return QIcon(bal_resources.icon_path(icon_basename))
|
||||
def read_QPixmap(icon_basename: str=bal_resources.DEFAULT_ICON) -> QPixmap:
|
||||
return QPixmap(bal_resources.icon_path(icon_basename))
|
||||
|
||||
199
balqt/willdetail.py
Normal file
199
balqt/willdetail.py
Normal file
@@ -0,0 +1,199 @@
|
||||
from functools import partial
|
||||
|
||||
from . import qt_resources
|
||||
if qt_resources.QT_VERSION == 5:
|
||||
from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QLabel,QWidget,QScrollArea)
|
||||
from PyQt5.QtGui import (QPixmap, QImage, QBitmap, QPainter, QFontDatabase, QPen, QFont,
|
||||
QColor, QDesktopServices, qRgba, QPainterPath,QPalette)
|
||||
else:
|
||||
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QLabel,QWidget,QScrollArea)
|
||||
from PyQt6.QtGui import (QPixmap, QImage, QBitmap, QPainter, QFontDatabase, QPen, QFont,
|
||||
QColor, QDesktopServices, qRgba, QPainterPath,QPalette)
|
||||
|
||||
|
||||
from electrum.util import decimal_point_to_base_unit_name
|
||||
from electrum.i18n import _
|
||||
|
||||
from ..bal import BalPlugin
|
||||
from .. import will as Will
|
||||
from .. import util as Util
|
||||
from .baldialog import BalDialog
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class WillDetailDialog(BalDialog):
|
||||
|
||||
|
||||
def __init__(self, bal_window):
|
||||
|
||||
self.will = bal_window.willitems
|
||||
self.threshold = Util.parse_locktime_string(bal_window.will_settings['threshold'])
|
||||
self.bal_window = bal_window
|
||||
Will.add_willtree(self.will)
|
||||
super().__init__(bal_window.window)
|
||||
self.config = bal_window.window.config
|
||||
self.wallet = bal_window.wallet
|
||||
self.format_amount = bal_window.window.format_amount
|
||||
self.base_unit = bal_window.window.base_unit
|
||||
self.format_fiat_and_units = bal_window.window.format_fiat_and_units
|
||||
self.fx = bal_window.window.fx
|
||||
self.format_fee_rate = bal_window.window.format_fee_rate
|
||||
self.decimal_point = bal_window.bal_plugin.config.get_decimal_point()
|
||||
self.base_unit_name = decimal_point_to_base_unit_name(self.decimal_point)
|
||||
self.setWindowTitle(_('Will Details'))
|
||||
self.setMinimumSize(670,700)
|
||||
self.vlayout= QVBoxLayout()
|
||||
w=QWidget()
|
||||
hlayout = QHBoxLayout(w)
|
||||
|
||||
b = QPushButton(_('Sign'))
|
||||
b.clicked.connect(self.ask_password_and_sign_transactions)
|
||||
hlayout.addWidget(b)
|
||||
|
||||
b = QPushButton(_('Broadcast'))
|
||||
b.clicked.connect(self.broadcast_transactions)
|
||||
hlayout.addWidget(b)
|
||||
|
||||
b = QPushButton(_('Export'))
|
||||
b.clicked.connect(self.export_will)
|
||||
hlayout.addWidget(b)
|
||||
"""
|
||||
toggle = "Hide"
|
||||
if self.bal_window.bal_plugin._hide_replaced:
|
||||
toggle = "Unhide"
|
||||
self.toggle_replace_button = QPushButton(_(f"{toggle} replaced"))
|
||||
self.toggle_replace_button.clicked.connect(self.toggle_replaced)
|
||||
hlayout.addWidget(self.toggle_replace_button)
|
||||
|
||||
toggle = "Hide"
|
||||
if self.bal_window.bal_plugin._hide_invalidated:
|
||||
toggle = "Unhide"
|
||||
|
||||
self.toggle_invalidate_button = QPushButton(_(f"{toggle} invalidated"))
|
||||
self.toggle_invalidate_button.clicked.connect(self.toggle_invalidated)
|
||||
hlayout.addWidget(self.toggle_invalidate_button)
|
||||
"""
|
||||
b = QPushButton(_('Invalidate'))
|
||||
b.clicked.connect(bal_window.invalidate_will)
|
||||
hlayout.addWidget(b)
|
||||
self.vlayout.addWidget(w)
|
||||
|
||||
self.paint_scroll_area()
|
||||
#vlayout.addWidget(QLabel(_("DON'T PANIC !!! everything is fine, all possible futures are covered")))
|
||||
self.vlayout.addWidget(QLabel(_("Expiration date: ")+Util.locktime_to_str(self.threshold)))
|
||||
self.vlayout.addWidget(self.scrollbox)
|
||||
w=QWidget()
|
||||
hlayout = QHBoxLayout(w)
|
||||
hlayout.addWidget(QLabel(_("Valid Txs:")+ str(len(Will.only_valid_list(self.will)))))
|
||||
hlayout.addWidget(QLabel(_("Total Txs:")+ str(len(self.will))))
|
||||
self.vlayout.addWidget(w)
|
||||
self.setLayout(self.vlayout)
|
||||
|
||||
def paint_scroll_area(self):
|
||||
#self.scrollbox.deleteLater()
|
||||
#self.willlayout.deleteLater()
|
||||
#self.detailsWidget.deleteLater()
|
||||
self.scrollbox = QScrollArea()
|
||||
viewport = QWidget(self.scrollbox)
|
||||
self.willlayout = QVBoxLayout(viewport)
|
||||
self.detailsWidget = WillWidget(parent=self)
|
||||
self.willlayout.addWidget(self.detailsWidget)
|
||||
|
||||
self.scrollbox.setWidget(viewport)
|
||||
viewport.setLayout(self.willlayout)
|
||||
def ask_password_and_sign_transactions(self):
|
||||
self.bal_window.ask_password_and_sign_transactions(callback=self.update)
|
||||
self.update()
|
||||
def broadcast_transactions(self):
|
||||
self.bal_window.broadcast_transactions()
|
||||
self.update()
|
||||
def export_will(self):
|
||||
self.bal_window.export_will()
|
||||
def toggle_replaced(self):
|
||||
self.bal_window.bal_plugin.hide_replaced()
|
||||
toggle = _("Hide")
|
||||
if self.bal_window.bal_plugin._hide_replaced:
|
||||
toggle = _("Unhide")
|
||||
self.toggle_replace_button.setText(f"{toggle} {_('replaced')}")
|
||||
self.update()
|
||||
|
||||
def toggle_invalidated(self):
|
||||
self.bal_window.bal_plugin.hide_invalidated()
|
||||
toggle = _("Hide")
|
||||
if self.bal_window.bal_plugin._hide_invalidated:
|
||||
toggle = _("Unhide")
|
||||
self.toggle_invalidate_button.setText(_(f"{toggle} {_('invalidated')}"))
|
||||
self.update()
|
||||
|
||||
def update(self):
|
||||
self.will = self.bal_window.willitems
|
||||
pos = self.vlayout.indexOf(self.scrollbox)
|
||||
self.vlayout.removeWidget(self.scrollbox)
|
||||
self.paint_scroll_area()
|
||||
self.vlayout.insertWidget(pos,self.scrollbox)
|
||||
super().update()
|
||||
|
||||
class WillWidget(QWidget):
|
||||
def __init__(self,father=None,parent = None):
|
||||
super().__init__()
|
||||
vlayout = QVBoxLayout()
|
||||
self.setLayout(vlayout)
|
||||
self.will = parent.bal_window.willitems
|
||||
self.parent = parent
|
||||
for w in self.will:
|
||||
if self.will[w].get_status('REPLACED') and self.parent.bal_window.bal_plugin._hide_replaced:
|
||||
continue
|
||||
if self.will[w].get_status('INVALIDATED') and self.parent.bal_window.bal_plugin._hide_invalidated:
|
||||
continue
|
||||
f = self.will[w].father
|
||||
if father == f:
|
||||
qwidget = QWidget()
|
||||
childWidget = QWidget()
|
||||
hlayout=QHBoxLayout(qwidget)
|
||||
qwidget.setLayout(hlayout)
|
||||
vlayout.addWidget(qwidget)
|
||||
detailw=QWidget()
|
||||
detaillayout=QVBoxLayout()
|
||||
detailw.setLayout(detaillayout)
|
||||
|
||||
willpushbutton = QPushButton(w)
|
||||
|
||||
willpushbutton.clicked.connect(partial(self.parent.bal_window.show_transaction,txid=w))
|
||||
detaillayout.addWidget(willpushbutton)
|
||||
locktime = Util.locktime_to_str(self.will[w].tx.locktime)
|
||||
creation = Util.locktime_to_str(self.will[w].time)
|
||||
def qlabel(title,value):
|
||||
label = "<b>"+_(str(title)) + f":</b>\t{str(value)}"
|
||||
return QLabel(label)
|
||||
detaillayout.addWidget(qlabel("Locktime",locktime))
|
||||
detaillayout.addWidget(qlabel("Creation Time",creation))
|
||||
total_fees = self.will[w].tx.input_value() - self.will[w].tx.output_value()
|
||||
decoded_fees = total_fees #Util.decode_amount(total_fees,self.parent.decimal_point)
|
||||
fee_per_byte = round(total_fees/self.will[w].tx.estimated_size(),3)
|
||||
fees_str = str(decoded_fees) + " ("+ str(fee_per_byte) + " sats/vbyte)"
|
||||
detaillayout.addWidget(qlabel("Transaction fees:",fees_str))
|
||||
detaillayout.addWidget(qlabel("Status:",self.will[w].status))
|
||||
detaillayout.addWidget(QLabel(""))
|
||||
detaillayout.addWidget(QLabel("<b>Heirs:</b>"))
|
||||
for heir in self.will[w].heirs:
|
||||
if "w!ll3x3c\"" not in heir:
|
||||
decoded_amount = Util.decode_amount(self.will[w].heirs[heir][3],self.parent.decimal_point)
|
||||
detaillayout.addWidget(qlabel(heir,f"{decoded_amount} {self.parent.base_unit_name}"))
|
||||
if self.will[w].we:
|
||||
detaillayout.addWidget(QLabel(""))
|
||||
detaillayout.addWidget(QLabel(_("<b>Willexecutor:</b:")))
|
||||
decoded_amount = Util.decode_amount(self.will[w].we['base_fee'],self.parent.decimal_point)
|
||||
|
||||
detaillayout.addWidget(qlabel(self.will[w].we['url'],f"{decoded_amount} {self.parent.base_unit_name}"))
|
||||
detaillayout.addStretch()
|
||||
pal = QPalette()
|
||||
pal.setColor(QPalette.ColorRole.Window, QColor(self.will[w].get_color()))
|
||||
detailw.setAutoFillBackground(True)
|
||||
detailw.setPalette(pal)
|
||||
|
||||
hlayout.addWidget(detailw)
|
||||
hlayout.addWidget(WillWidget(w,parent = parent))
|
||||
|
||||
292
balqt/willexecutor_dialog.py
Normal file
292
balqt/willexecutor_dialog.py
Normal file
@@ -0,0 +1,292 @@
|
||||
import enum
|
||||
import json
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
|
||||
from . import qt_resources
|
||||
if qt_resources.QT_VERSION == 5:
|
||||
from PyQt5.QtGui import QStandardItemModel, QStandardItem
|
||||
from PyQt5.QtCore import Qt,QPersistentModelIndex, QModelIndex
|
||||
from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QLabel,QMenu)
|
||||
else:
|
||||
from PyQt6.QtGui import QStandardItemModel, QStandardItem
|
||||
from PyQt6.QtCore import Qt,QPersistentModelIndex, QModelIndex
|
||||
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QLabel,QMenu)
|
||||
|
||||
|
||||
from electrum.i18n import _
|
||||
from electrum.gui.qt.util import (Buttons,read_QIcon, import_meta_gui, export_meta_gui,MessageBoxMixin)
|
||||
from electrum.util import write_json_file,read_json_file
|
||||
from electrum.gui.qt.my_treeview import MyTreeView
|
||||
|
||||
from ..bal import BalPlugin
|
||||
from .. import util as Util
|
||||
from .. import willexecutors as Willexecutors
|
||||
from .baldialog import BalDialog,BalBlockingWaitingDialog
|
||||
from electrum.logging import get_logger,Logger
|
||||
|
||||
_logger=get_logger(__name__)
|
||||
class WillExecutorList(MyTreeView):
|
||||
class Columns(MyTreeView.BaseColumnsEnum):
|
||||
SELECTED = enum.auto()
|
||||
URL = enum.auto()
|
||||
BASE_FEE = enum.auto()
|
||||
INFO = enum.auto()
|
||||
ADDRESS = enum.auto()
|
||||
STATUS = enum.auto()
|
||||
|
||||
headers = {
|
||||
Columns.SELECTED:_(''),
|
||||
Columns.URL: _('Url'),
|
||||
Columns.BASE_FEE: _('Base fee'),
|
||||
Columns.INFO:_('Info'),
|
||||
Columns.ADDRESS:_('Default Address'),
|
||||
Columns.STATUS: _('S'),
|
||||
}
|
||||
|
||||
ROLE_HEIR_KEY = Qt.ItemDataRole.UserRole + 2000
|
||||
key_role = ROLE_HEIR_KEY
|
||||
|
||||
def __init__(self, parent: 'WillExecutorDialog'):
|
||||
super().__init__(
|
||||
parent=parent,
|
||||
stretch_column=self.Columns.ADDRESS,
|
||||
editable_columns=[self.Columns.URL,self.Columns.BASE_FEE,self.Columns.ADDRESS,self.Columns.INFO],
|
||||
|
||||
)
|
||||
self.parent = parent
|
||||
self.setModel(QStandardItemModel(self))
|
||||
self.setSortingEnabled(True)
|
||||
self.std_model = self.model()
|
||||
self.config =parent.bal_plugin.config
|
||||
|
||||
|
||||
self.update()
|
||||
|
||||
|
||||
|
||||
def create_menu(self, position):
|
||||
menu = QMenu()
|
||||
idx = self.indexAt(position)
|
||||
column = idx.column() or self.Columns.URL
|
||||
selected_keys = []
|
||||
for s_idx in self.selected_in_column(self.Columns.URL):
|
||||
sel_key = self.model().itemFromIndex(s_idx).data(0)
|
||||
selected_keys.append(sel_key)
|
||||
if selected_keys and idx.isValid():
|
||||
column_title = self.model().horizontalHeaderItem(column).text()
|
||||
column_data = '\n'.join(self.model().itemFromIndex(s_idx).text()
|
||||
for s_idx in self.selected_in_column(column))
|
||||
if Willexecutors.is_selected(self.parent.willexecutors_list[sel_key]):
|
||||
menu.addAction(_("deselect").format(column_title), lambda: self.deselect(selected_keys))
|
||||
else:
|
||||
menu.addAction(_("select").format(column_title), lambda: self.select(selected_keys))
|
||||
if column in self.editable_columns:
|
||||
item = self.model().itemFromIndex(idx)
|
||||
if item.isEditable():
|
||||
persistent = QPersistentModelIndex(idx)
|
||||
menu.addAction(_("Edit {}").format(column_title), lambda p=persistent: self.edit(QModelIndex(p)))
|
||||
|
||||
menu.addAction(_("Ping").format(column_title), lambda: self.ping_willexecutors(selected_keys))
|
||||
menu.addSeparator()
|
||||
menu.addAction(_("delete").format(column_title), lambda: self.delete(selected_keys))
|
||||
|
||||
menu.exec(self.viewport().mapToGlobal(position))
|
||||
|
||||
def ping_willexecutors(self,selected_keys):
|
||||
wout={}
|
||||
for k in selected_keys:
|
||||
wout[k]=self.parent.willexecutors_list[k]
|
||||
self.parent.update_willexecutors(wout)
|
||||
self.update()
|
||||
|
||||
def get_edit_key_from_coordinate(self, row, col):
|
||||
a= self.get_role_data_from_coordinate(row, col, role=self.ROLE_HEIR_KEY+col)
|
||||
return a
|
||||
|
||||
def delete(self,selected_keys):
|
||||
for key in selected_keys:
|
||||
del self.parent.willexecutors_list[key]
|
||||
self.update()
|
||||
|
||||
def select(self,selected_keys):
|
||||
for wid,w in self.parent.willexecutors_list.items():
|
||||
if wid in selected_keys:
|
||||
w['selected']=True
|
||||
self.update()
|
||||
|
||||
def deselect(self,selected_keys):
|
||||
for wid,w in self.parent.willexecutors_list.items():
|
||||
if wid in selected_keys:
|
||||
w['selected']=False
|
||||
self.update()
|
||||
|
||||
def on_edited(self, idx, edit_key, *, text):
|
||||
prior_name = self.parent.willexecutors_list[edit_key]
|
||||
col = idx.column()
|
||||
try:
|
||||
if col == self.Columns.URL:
|
||||
self.parent.willexecutors_list[text]=self.parent.willexecutors_list[edit_key]
|
||||
del self.parent.willexecutors_list[edit_key]
|
||||
if col == self.Columns.BASE_FEE:
|
||||
self.parent.willexecutors_list[edit_key]["base_fee"] = Util.encode_amount(text,self.config.get_decimal_point())
|
||||
if col == self.Columns.ADDRESS:
|
||||
self.parent.willexecutors_list[edit_key]["address"] = text
|
||||
if col == self.Columns.INFO:
|
||||
self.parent.willexecutors_list[edit_key]["info"] = text
|
||||
self.update()
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
def update(self):
|
||||
if self.parent.willexecutors_list is None:
|
||||
return
|
||||
try:
|
||||
current_key = self.get_role_data_for_current_item(col=self.Columns.URL, role=self.ROLE_HEIR_KEY)
|
||||
self.model().clear()
|
||||
self.update_headers(self.__class__.headers)
|
||||
|
||||
set_current = None
|
||||
|
||||
for url, value in self.parent.willexecutors_list.items():
|
||||
labels = [""] * len(self.Columns)
|
||||
labels[self.Columns.URL] = url
|
||||
if Willexecutors.is_selected(value):
|
||||
labels[self.Columns.SELECTED] = [read_QIcon('confirmed.png'),'']
|
||||
else:
|
||||
labels[self.Columns.SELECTED] = ''
|
||||
labels[self.Columns.BASE_FEE] = Util.decode_amount(value.get('base_fee',0),self.config.get_decimal_point())
|
||||
if str(value.get('status',0)) == "200":
|
||||
labels[self.Columns.STATUS] = [read_QIcon('status_connected.png'),'']
|
||||
else:
|
||||
labels[self.Columns.STATUS] = [read_QIcon('unconfirmed.png'),'']
|
||||
labels[self.Columns.ADDRESS] = str(value.get('address',''))
|
||||
labels[self.Columns.INFO] = str(value.get('info',''))
|
||||
|
||||
items=[]
|
||||
for e in labels:
|
||||
if type(e)== list:
|
||||
try:
|
||||
items.append(QStandardItem(*e))
|
||||
except Exception as e:
|
||||
pass
|
||||
else:
|
||||
items.append(QStandardItem(e))
|
||||
items[self.Columns.SELECTED].setEditable(False)
|
||||
items[self.Columns.URL].setEditable(True)
|
||||
items[self.Columns.ADDRESS].setEditable(True)
|
||||
items[self.Columns.INFO].setEditable(True)
|
||||
items[self.Columns.BASE_FEE].setEditable(True)
|
||||
items[self.Columns.STATUS].setEditable(False)
|
||||
|
||||
items[self.Columns.URL].setData(url, self.ROLE_HEIR_KEY+1)
|
||||
items[self.Columns.BASE_FEE].setData(url, self.ROLE_HEIR_KEY+2)
|
||||
items[self.Columns.INFO].setData(url, self.ROLE_HEIR_KEY+3)
|
||||
items[self.Columns.ADDRESS].setData(url, self.ROLE_HEIR_KEY+4)
|
||||
|
||||
|
||||
self.model().insertRow(self.model().rowCount(), items)
|
||||
if url == current_key:
|
||||
idx = self.model().index(row_count, self.Columns.NAME)
|
||||
set_current = QPersistentModelIndex(idx)
|
||||
self.set_current_idx(set_current)
|
||||
self.parent.save_willexecutors()
|
||||
except Exception as e:
|
||||
_logger.error(e)
|
||||
|
||||
|
||||
class WillExecutorDialog(BalDialog,MessageBoxMixin):
|
||||
def __init__(self, bal_window):
|
||||
BalDialog.__init__(self,bal_window.window)
|
||||
self.bal_plugin = bal_window.bal_plugin
|
||||
self.gui_object = self.bal_plugin.gui_object
|
||||
self.config = self.bal_plugin.config
|
||||
self.window = bal_window.window
|
||||
self.bal_window = bal_window
|
||||
self.willexecutors_list = Willexecutors.get_willexecutors(self.bal_plugin)
|
||||
|
||||
self.setWindowTitle(_('Will-Executor Service List'))
|
||||
self.setMinimumSize(1000, 200)
|
||||
self.size_label = QLabel()
|
||||
self.willexecutor_list = WillExecutorList(self)
|
||||
|
||||
vbox = QVBoxLayout(self)
|
||||
vbox.addWidget(self.size_label)
|
||||
vbox.addWidget(self.willexecutor_list)
|
||||
buttonbox = QHBoxLayout()
|
||||
|
||||
b = QPushButton(_('Ping'))
|
||||
b.clicked.connect(self.update_willexecutors)
|
||||
buttonbox.addWidget(b)
|
||||
|
||||
b = QPushButton(_('Import'))
|
||||
b.clicked.connect(self.import_file)
|
||||
buttonbox.addWidget(b)
|
||||
|
||||
b = QPushButton(_('Export'))
|
||||
b.clicked.connect(self.export_file)
|
||||
buttonbox.addWidget(b)
|
||||
|
||||
b = QPushButton(_('Add'))
|
||||
b.clicked.connect(self.add)
|
||||
buttonbox.addWidget(b)
|
||||
|
||||
b = QPushButton(_('Close'))
|
||||
b.clicked.connect(self.close)
|
||||
buttonbox.addWidget(b)
|
||||
|
||||
vbox.addLayout(buttonbox)
|
||||
|
||||
self.willexecutor_list.update()
|
||||
|
||||
def add(self):
|
||||
self.willexecutors_list["http://localhost:8080"]={"info":"New Will Executor","base_fee":0,"status":"-1"}
|
||||
self.willexecutor_list.update()
|
||||
|
||||
def import_file(self):
|
||||
import_meta_gui(self, _('willexecutors.json'), self.import_json_file, self.willexecutors_list.update)
|
||||
|
||||
def export_file(self, path):
|
||||
Util.export_meta_gui(self, _('willexecutors.json'), self.export_json_file)
|
||||
|
||||
def export_json_file(self,path):
|
||||
write_json_file(path, self.willexecutors_list)
|
||||
|
||||
def update_willexecutors(self,wes=None):
|
||||
if not wes:
|
||||
self.willexecutors_list = Willexecutors.get_willexecutors(self.bal_plugin, update = True, bal_window = self.bal_window,force=True)
|
||||
else:
|
||||
self.bal_window.ping_willexecutors(wes)
|
||||
self.willexecutors_list.update(wes)
|
||||
self.willexecutor_list.update()
|
||||
|
||||
|
||||
def import_json_file(self, path):
|
||||
data = read_json_file(path)
|
||||
data = self._validate(data)
|
||||
self.willexecutors_list.update(data)
|
||||
self.willexecutor_list.update()
|
||||
|
||||
#TODO validate willexecutor json import file
|
||||
def _validate(self,data):
|
||||
return data
|
||||
|
||||
def is_hidden(self):
|
||||
return self.isMinimized() or self.isHidden()
|
||||
|
||||
def show_or_hide(self):
|
||||
if self.is_hidden():
|
||||
self.bring_to_top()
|
||||
else:
|
||||
self.hide()
|
||||
|
||||
def bring_to_top(self):
|
||||
self.show()
|
||||
self.raise_()
|
||||
|
||||
def closeEvent(self, event):
|
||||
event.accept()
|
||||
|
||||
def save_willexecutors(self):
|
||||
self.bal_plugin.config.set_key(self.bal_plugin.WILLEXECUTORS,self.willexecutors_list,save=True)
|
||||
|
||||
Reference in New Issue
Block a user