2 Commits

Author SHA1 Message Date
56ebf760b3 default_will_settings 2025-11-29 15:03:56 -04:00
e5e342649f default_will_settings 2025-11-29 15:02:03 -04:00
23 changed files with 4094 additions and 3125 deletions

View File

@@ -1 +1 @@
0.2.4 0.2.2b

134
bal.py
View File

@@ -1,82 +1,71 @@
import random
import os import os
#import random import zipfile as zipfile_lib
#import zipfile as zipfile_lib
from electrum import json_db
from electrum.logging import get_logger
from electrum.plugin import BasePlugin from electrum.plugin import BasePlugin
from electrum import json_db
from electrum.transaction import tx_from_any from electrum.transaction import tx_from_any
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)
def get_will_settings(x): from electrum.logging import get_logger
#print(x)
pass
json_db.register_dict("heirs", tuple, None)
json_db.register_dict("will", dict, None)
json_db.register_dict("will_settings", lambda x: x, None)
def get_will(x): def get_will(x):
try: try:
x["tx"] = tx_from_any(x["tx"]) x['tx']=tx_from_any(x['tx'])
except Exception as e: except Exception as e:
raise e raise e
return x return x
class BalConfig():
class BalConfig:
def __init__(self, config, name, default): def __init__(self, config, name, default):
self.config = config self.config = config
self.name = name self.name = name
self.default = default self.default = default
def get(self, default=None): def get(self,default=None):
v = self.config.get(self.name, default) v = self.config.get(self.name, default)
if v is None: if v is None:
if default is not None: if not default is None:
v = default v = default
else: else:
v = self.default v = self.default
return v return v
def set(self, value, save=True): def set(self,value,save=True):
self.config.set_key(self.name, value, save=save) self.config.set_key(self.name,value,save=save)
class BalPlugin(BasePlugin): class BalPlugin(BasePlugin):
LATEST_VERSION = "1" LATEST_VERSION = '1'
KNOWN_VERSIONS = ("0", "1") KNOWN_VERSIONS = ('0', '1')
assert LATEST_VERSION in KNOWN_VERSIONS assert LATEST_VERSION in KNOWN_VERSIONS
def version(): def version():
try: try:
f = "" f=""
with open("VERSION", "r") as fi: with open("VERSION","r") as f:
f = str(fi.readline()) f = str(f.readline())
return f return f
except: except:
return "unknown" return "unknown"
SIZE = (159, 97) SIZE = (159, 97)
def __init__(self, parent, config, name): def __init__(self, parent, config, name):
self.logger = get_logger(__name__) self.logger = get_logger(__name__)
BasePlugin.__init__(self, parent, config, name) BasePlugin.__init__(self, parent, config, name)
self.base_dir = os.path.join(config.electrum_path(), "bal") self.base_dir = os.path.join(config.electrum_path(), 'bal')
self.plugin_dir = os.path.split(os.path.realpath(__file__))[0] self.plugin_dir = os.path.split(os.path.realpath(__file__))[0]
zipfile = "/".join(self.plugin_dir.split("/")[:-1]) zipfile="/".join(self.plugin_dir.split("/")[:-1])
# print("real path",os.path.realpath(__file__)) #print("real path",os.path.realpath(__file__))
# self.logger.info(self.base_dir) #self.logger.info(self.base_dir)
# print("base_dir:", self.base_dir) #print("base_dir:", self.base_dir)
# print("suca:",zipfile) #print("suca:",zipfile)
# print("plugin_dir:", self.plugin_dir) #print("plugin_dir:", self.plugin_dir)
import sys import sys
sys.path.insert(0, zipfile) sys.path.insert(0, zipfile)
# print("sono state listate?") #print("sono state listate?")
self.parent = parent self.parent = parent
self.config = config self.config = config
self.name = name self.name = name
@@ -84,11 +73,9 @@ class BalPlugin(BasePlugin):
self.ASK_BROADCAST = BalConfig(config, "bal_ask_broadcast", True) self.ASK_BROADCAST = BalConfig(config, "bal_ask_broadcast", True)
self.BROADCAST = BalConfig(config, "bal_broadcast", True) self.BROADCAST = BalConfig(config, "bal_broadcast", True)
self.LOCKTIME_TIME = BalConfig(config, "bal_locktime_time", 90) self.LOCKTIME_TIME = BalConfig(config, "bal_locktime_time", 90)
self.LOCKTIME_BLOCKS = BalConfig(config, "bal_locktime_blocks", 144 * 90) self.LOCKTIME_BLOCKS = BalConfig(config, "bal_locktime_blocks", 144*90)
self.LOCKTIMEDELTA_TIME = BalConfig(config, "bal_locktimedelta_time", 7) self.LOCKTIMEDELTA_TIME = BalConfig(config, "bal_locktimedelta_time", 7)
self.LOCKTIMEDELTA_BLOCKS = BalConfig( self.LOCKTIMEDELTA_BLOCKS = BalConfig(config, "bal_locktimedelta_blocks", 144*7)
config, "bal_locktimedelta_blocks", 144 * 7
)
self.ENABLE_MULTIVERSE = BalConfig(config, "bal_enable_multiverse", False) self.ENABLE_MULTIVERSE = BalConfig(config, "bal_enable_multiverse", False)
self.TX_FEES = BalConfig(config, "bal_tx_fees", 100) self.TX_FEES = BalConfig(config, "bal_tx_fees", 100)
self.INVALIDATE = BalConfig(config, "bal_invalidate", True) self.INVALIDATE = BalConfig(config, "bal_invalidate", True)
@@ -96,44 +83,36 @@ class BalPlugin(BasePlugin):
self.PREVIEW = BalConfig(config, "bal_preview", True) self.PREVIEW = BalConfig(config, "bal_preview", True)
self.SAVE_TXS = BalConfig(config, "bal_save_txs", True) self.SAVE_TXS = BalConfig(config, "bal_save_txs", True)
self.WILLEXECUTORS = BalConfig(config, "bal_willexecutors", True) self.WILLEXECUTORS = BalConfig(config, "bal_willexecutors", True)
#self.PING_WILLEXECUTORS = BalConfig(config, "bal_ping_willexecutors", True) self.PING_WILLEXECUTORS = BalConfig(config, "bal_ping_willexecutors", True)
#self.ASK_PING_WILLEXECUTORS = BalConfig( self.ASK_PING_WILLEXECUTORS = BalConfig(config, "bal_ask_ping_willexecutors", True)
# config, "bal_ask_ping_willexecutors", True
#)
self.NO_WILLEXECUTOR = BalConfig(config, "bal_no_willexecutor", True) self.NO_WILLEXECUTOR = BalConfig(config, "bal_no_willexecutor", True)
self.HIDE_REPLACED = BalConfig(config, "bal_hide_replaced", True) self.HIDE_REPLACED = BalConfig(config, "bal_hide_replaced", True)
self.HIDE_INVALIDATED = BalConfig(config, "bal_hide_invalidated", True) self.HIDE_INVALIDATED = BalConfig(config, "bal_hide_invalidated", True)
self.ALLOW_REPUSH = BalConfig(config, "bal_allow_repush", True) self.ALLOW_REPUSH = BalConfig(config, "bal_allow_repush", True)
self.FIRST_EXECUTION = BalConfig(config, "bal_first_execution", True) self.FIRST_EXECUTION = BalConfig(config, "bal_first_execution", True)
self.WILLEXECUTORS = BalConfig( self.WILLEXECUTORS = BalConfig(config, "bal_willexecutors", {
config,
"bal_willexecutors",
{
"mainnet": { "mainnet": {
"https://we.bitcoin-after.life": { 'https://we.bitcoin-after.life': {
"base_fee": 100000, "base_fee": 100000,
"status": "New", "status": "New",
"info": "Bitcoin After Life Will Executor", "info":"Bitcoin After Life Will Executor",
"address": "bcrt1qa5cntu4hgadw8zd3n6sq2nzjy34sxdtd9u0gp7", "address":"bcrt1qa5cntu4hgadw8zd3n6sq2nzjy34sxdtd9u0gp7",
"selected": True, "selected":True
} }
} }
}, })
) self.WILL_SETTINGS = BalConfig(config, "bal_will_settings", {
self.WILL_SETTINGS = BalConfig( 'tx_fees':100,
config, 'threshold':'180d',
"bal_will_settings", 'locktime':'1y',
{ })
"baltx_fees": 100,
"threshold": "180d",
"locktime": "1y",
},
)
self._hide_invalidated = self.HIDE_INVALIDATED.get()
self._hide_replaced = self.HIDE_REPLACED.get()
def resource_path(self, *parts): self._hide_invalidated= self.HIDE_INVALIDATED.get()
self._hide_replaced= self.HIDE_REPLACED.get()
def resource_path(self,*parts):
return os.path.join(self.plugin_dir, *parts) return os.path.join(self.plugin_dir, *parts)
def hide_invalidated(self): def hide_invalidated(self):
@@ -144,17 +123,14 @@ class BalPlugin(BasePlugin):
self._hide_replaced = not self._hide_replaced self._hide_replaced = not self._hide_replaced
self.HIDE_REPLACED.set(self._hide_replaced) self.HIDE_REPLACED.set(self._hide_replaced)
def validate_will_settings(self, will_settings): def validate_will_settings(self,will_settings):
# print(type(will_settings)) if int(will_settings.get('tx_fees',1))<1:
# print(will_settings.get('baltx_fees',1),1) will_settings['tx_fees']=1
if int(will_settings.get("baltx_fees", 1)) < 1: if not will_settings.get('threshold'):
will_settings["baltx_fees"] = 1 will_settings['threshold']='180d'
if not will_settings.get("threshold"): if not will_settings.get('locktime')=='':
will_settings["threshold"] = "180d" will_settings['locktime']='1y'
if not will_settings.get("locktime") == "":
will_settings["locktime"] = "1y"
return will_settings return will_settings
def default_will_settings(self): def default_will_settings(self):
return {"baltx_fees": 100, "threshold": "180d", "locktime": "1y"} return {}

View File

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

102
balqt/baldialog.py Normal file
View File

@@ -0,0 +1,102 @@
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))
def on_check(v):
plugin.config.set_key(variable, v == 2)
plugin.config_get(variable)
self.stateChanged.connect(on_check)

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
_logger.debug("have to sign {}".format(self.have_to_sign))
password=None
if self.have_to_sign is None:
self.msg_set_invalidating()
#need to sign invalidate and restart phase 1
password = self.bal_window.get_wallet_password("Invalidate your old will",parent=self.bal_window.window)
if password is False:
self.msg_set_invalidating("Aborted")
self.wait(3)
self.close()
return
self.thread.add(partial(self.invalidate_task,tx,password),on_success=self.on_success_invalidate, on_done=self.on_accept, on_error=self.on_error)
return
elif self.have_to_sign:
password = self.bal_window.get_wallet_password("Sign your will",parent=self.bal_window.window)
if password is False:
self.msg_set_signing('Aborted')
else:
self.msg_set_signing('Nothing to do')
self.thread.add(partial(self.task_phase2,password),on_success=self.on_success_phase2,on_done=self.on_accept_phase2,on_error=self.on_error_phase2)
return
def on_success_phase2(self,arg=False):
self.thread.stop()
self.bal_window.save_willitems()
self.msg_edit_row("Finished")
self.close()
def closeEvent(self,event):
self._stopping=True
self.thread.stop()
def task_phase2(self,password):
if self.have_to_sign:
try:
if txs:=self.bal_window.sign_transactions(password):
for txid,tx in txs.items():
self.bal_window.willitems[txid].tx = copy.deepcopy(tx)
self.bal_window.save_willitems()
self.msg_set_signing("Ok")
except Exception as e:
self.msg_set_signing(self.msg_error(e))
self.msg_set_pushing()
have_to_push = False
for wid in Will.only_valid(self.bal_window.willitems):
w=self.bal_window.willitems[wid]
if w.we and w.get_status("COMPLETE") and not w.get_status("PUSHED"):
have_to_push = True
if not have_to_push:
self.msg_set_pushing("Nothing to do")
else:
try:
self.loop_push()
self.msg_set_pushing("Ok")
except Exception as e:
self.msg_set_pushing(self.msg_error(e))
self.msg_edit_row("Ok")
self.wait(5)
def on_error_phase1(self,error):
_logger.error(f"error phase1: {error}")
def on_error_phase2(self,error):
_logger.error("error phase2: { error}")
def msg_set_checking(self, status = None, row = None):
row = self.check_row if row is None else row
self.check_row = self.msg_set_status("Checking your will", row, status)
def msg_set_invalidating(self, status = None, row = None):
row = self.inval_row if row is None else row
self.inval_row = self.msg_set_status("Invalidating old will", self.inval_row, status)
def msg_set_building(self, status = None, row = None):
row = self.build_row if row is None else row
self.build_row = self.msg_set_status("Building your will", self.build_row, status)
def msg_set_signing(self, status = None, row = None):
row = self.sign_row if row is None else row
self.sign_row = self.msg_set_status("Signing your will", self.sign_row, status)
def msg_set_pushing(self, status = None, row = None):
row = self.push_row if row is None else row
self.push_row = self.msg_set_status("Broadcasting your will to executors", self.push_row, status)
def msg_set_waiting(self, status = None, row = None):
row = self.wait_row if row is None else row
self.wait_row = self.msg_edit_row(f"Please wait {status}secs", self.wait_row)
def msg_error(self,e):
return "Error: {}".format(e)
def msg_set_status(self,msg,row,status=None):
status= "Wait" if status is None else status
line="{}:\t{}".format(_(msg), status)
return self.msg_edit_row(line,row)
#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,291 @@
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.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)

413
heirs.py
View File

@@ -1,38 +1,25 @@
import datetime
import json
import math
import random
import re import re
import threading import json
import urllib.parse from typing import Optional, Tuple, Dict, Any, TYPE_CHECKING,Sequence,List
import urllib.request
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Tuple
import dns import dns
import threading
import math
from dns.exception import DNSException from dns.exception import DNSException
from electrum import bitcoin, constants, descriptor, dnssec
from electrum.logging import Logger, get_logger
from electrum.transaction import (
PartialTransaction,
PartialTxInput,
PartialTxOutput,
TxOutpoint,
TxOutput,
)
from electrum.util import (
bfh,
read_json_file,
to_string,
trigger_callback,
write_json_file,
)
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
import random
from .util import Util from .util import Util
from .willexecutors import Willexecutors from .willexecutors import Willexecutors
if TYPE_CHECKING: if TYPE_CHECKING:
from .simple_config import SimpleConfig
from .wallet_db import WalletDB from .wallet_db import WalletDB
from .simple_config import SimpleConfig
_logger = get_logger(__name__) _logger = get_logger(__name__)
@@ -42,8 +29,6 @@ HEIR_AMOUNT = 1
HEIR_LOCKTIME = 2 HEIR_LOCKTIME = 2
HEIR_REAL_AMOUNT = 3 HEIR_REAL_AMOUNT = 3
TRANSACTION_LABEL = "inheritance transaction" TRANSACTION_LABEL = "inheritance transaction"
class AliasNotFoundException(Exception): class AliasNotFoundException(Exception):
pass pass
@@ -51,8 +36,7 @@ class AliasNotFoundException(Exception):
def reduce_outputs(in_amount, out_amount, fee, outputs): def reduce_outputs(in_amount, out_amount, fee, outputs):
if in_amount < out_amount: if in_amount < out_amount:
for output in outputs: for output in outputs:
output.value = math.floor((in_amount - fee) / out_amount * output.value) 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 #TODO: put this method inside wallet.db to replace or complete get_locktime_for_new_transaction
@@ -77,42 +61,35 @@ def get_current_height(network:'Network'):
""" """
def prepare_transactions(locktimes, available_utxos, fees, wallet): def prepare_transactions(locktimes, available_utxos, fees, wallet):
available_utxos = sorted( available_utxos=sorted(available_utxos, key=lambda x:"{}:{}:{}".format(x.value_sats(),x.prevout.txid,x.prevout.out_idx))
available_utxos,
key=lambda x: "{}:{}:{}".format(
x.value_sats(), x.prevout.txid, x.prevout.out_idx
),
)
total_used_utxos = [] total_used_utxos = []
txsout = {} txsout={}
locktime, _ = Util.get_lowest_locktimes(locktimes) locktime,_=Util.get_lowest_locktimes(locktimes)
if not locktime: if not locktime:
return return
locktime = locktime[0] locktime=locktime[0]
heirs = locktimes[locktime] heirs = locktimes[locktime]
vero = True vero=True
while vero: while vero:
vero = False vero=False
fee = fees.get(locktime, 0) fee=fees.get(locktime,0)
out_amount = fee out_amount = fee
description = "" description = ""
outputs = [] outputs = []
paid_heirs = {} paid_heirs = {}
for name, heir in heirs.items(): for name,heir in heirs.items():
try: try:
if len(heir) > HEIR_REAL_AMOUNT: if len(heir)>HEIR_REAL_AMOUNT:
real_amount = heir[HEIR_REAL_AMOUNT] real_amount = heir[HEIR_REAL_AMOUNT]
out_amount += real_amount out_amount += real_amount
description += f"{name}\n" description += f"{name}\n"
paid_heirs[name] = heir paid_heirs[name]=heir
outputs.append( outputs.append(PartialTxOutput.from_address_and_value(heir[HEIR_ADDRESS], real_amount))
PartialTxOutput.from_address_and_value(
heir[HEIR_ADDRESS], real_amount
)
)
else: else:
pass pass
except Exception as e: except Exception as e:
@@ -132,41 +109,35 @@ def prepare_transactions(locktimes, available_utxos, fees, wallet):
pass pass
if int(in_amount) < int(out_amount): if int(in_amount) < int(out_amount):
break break
heirsvalue = out_amount heirsvalue=out_amount
change = get_change_output(wallet, in_amount, out_amount, fee) change = get_change_output(wallet, in_amount, out_amount, fee)
if change: if change:
outputs.append(change) outputs.append(change)
for i in range(0, 100): for i in range(0,100):
random.shuffle(outputs) random.shuffle(outputs)
tx = PartialTransaction.from_io( print(outputs)
used_utxos, tx = PartialTransaction.from_io(used_utxos, outputs, locktime=Util.parse_locktime_string(locktime,wallet), version=2)
outputs, if len(description)>0: tx.description = description[:-1]
locktime=Util.parse_locktime_string(locktime, wallet), else: tx.description = ""
version=2,
)
if len(description) > 0:
tx.description = description[:-1]
else:
tx.description = ""
tx.heirsvalue = heirsvalue tx.heirsvalue = heirsvalue
tx.set_rbf(True) tx.set_rbf(True)
tx.remove_signatures() tx.remove_signatures()
txid = tx.txid() txid = tx.txid()
if txid is None: if txid is None:
raise Exception("txid is none", tx) raise Exception("txid is none",tx)
tx.heirs = paid_heirs tx.heirs = paid_heirs
tx.my_locktime = locktime tx.my_locktime = locktime
txsout[txid] = tx txsout[txid]=tx
if change: if change:
change_idx = tx.get_output_idxs_from_address(change.address) change_idx=tx.get_output_idxs_from_address(change.address)
prevout = TxOutpoint(txid=bfh(tx.txid()), out_idx=change_idx.pop()) prevout = TxOutpoint(txid=bfh(tx.txid()), out_idx=change_idx.pop())
txin = PartialTxInput(prevout=prevout) txin = PartialTxInput(prevout=prevout)
txin._trusted_value_sats = change.value txin._trusted_value_sats = change.value
txin.script_descriptor = change.script_descriptor txin.script_descriptor = change.script_descriptor
txin.is_mine = True txin.is_mine=True
txin._TxInput__address = change.address txin._TxInput__address=change.address
txin._TxInput__scriptpubkey = change.scriptpubkey txin._TxInput__scriptpubkey = change.scriptpubkey
txin._TxInput__value_sats = change.value txin._TxInput__value_sats = change.value
txin.utxo = tx txin.utxo = tx
@@ -175,74 +146,67 @@ def prepare_transactions(locktimes, available_utxos, fees, wallet):
return txsout return txsout
def get_utxos_from_inputs(tx_inputs, tx, utxos): def get_utxos_from_inputs(tx_inputs,tx,utxos):
for tx_input in tx_inputs: for tx_input in tx_inputs:
prevoutstr = tx_input.prevout.to_str() prevoutstr=tx_input.prevout.to_str()
utxos[prevoutstr] = utxos.get(prevoutstr, {"input": tx_input, "txs": []}) utxos[prevoutstr] =utxos.get(prevoutstr,{'input':tx_input,'txs':[]})
utxos[prevoutstr]["txs"].append(tx) utxos[prevoutstr]['txs'].append(tx)
return utxos return utxos
#TODO calculate de minimum inputs to be invalidated
# TODO calculate de minimum inputs to be invalidated
def invalidate_inheritance_transactions(wallet): def invalidate_inheritance_transactions(wallet):
listids = [] listids = []
utxos = {} utxos = {}
dtxs = {} dtxs = {}
for k, v in wallet.get_all_labels().items(): for k,v in wallet.get_all_labels().items():
tx = None tx = None
if TRANSACTION_LABEL == v: if TRANSACTION_LABEL == v:
tx = wallet.adb.get_transaction(k) tx=wallet.adb.get_transaction(k)
if tx: if tx:
dtxs[tx.txid()] = tx dtxs[tx.txid()]=tx
get_utxos_from_inputs(tx.inputs(), tx, utxos) get_utxos_from_inputs(tx.inputs(),tx,utxos)
for key, utxo in utxos.items(): for key,utxo in utxos.items():
txid = key.split(":")[0] txid=key.split(":")[0]
if txid in dtxs: if txid in dtxs:
for tx in utxo["txs"]: for tx in utxo['txs']:
txid = tx.txid() txid =tx.txid()
del dtxs[txid] del dtxs[txid]
utxos = {} utxos = {}
for txid, tx in dtxs.items(): for txid,tx in dtxs.items():
get_utxos_from_inputs(tx.inputs(), tx, utxos) get_utxos_from_inputs(tx.inputs(),tx,utxos)
utxos = sorted(utxos.items(), key=lambda item: len(item[1])) utxos = sorted(utxos.items(), key = lambda item: len(item[1]))
remaining = {}
remaining={}
invalidated = [] invalidated = []
for key, value in utxos: for key,value in utxos:
for tx in value["txs"]: for tx in value['txs']:
txid = tx.txid() txid = tx.txid()
if not txid in invalidated: if not txid in invalidated:
invalidated.append(tx.txid()) invalidated.append(tx.txid())
remaining[key] = value remaining[key] = value
def print_transaction(heirs,tx,locktimes,tx_fees):
def print_transaction(heirs, tx, locktimes, tx_fees): jtx=tx.to_json()
jtx = tx.to_json()
print(f"TX: {tx.txid()}\t-\tLocktime: {jtx['locktime']}") print(f"TX: {tx.txid()}\t-\tLocktime: {jtx['locktime']}")
print(f"---") print(f"---")
for inp in jtx["inputs"]: for inp in jtx["inputs"]:
print(f"{inp['address']}: {inp['value_sats']}") print(f"{inp['address']}: {inp['value_sats']}")
print(f"---") print(f"---")
for out in jtx["outputs"]: for out in jtx["outputs"]:
heirname = "" heirname=""
for key in heirs.keys(): for key in heirs.keys():
heir = heirs[key] heir=heirs[key]
if heir[HEIR_ADDRESS] == out["address"] and str(heir[HEIR_LOCKTIME]) == str( if heir[HEIR_ADDRESS] == out['address'] and str(heir[HEIR_LOCKTIME]) == str(jtx['locktime']):
jtx["locktime"] heirname=key
):
heirname = key
print(f"{heirname}\t{out['address']}: {out['value_sats']}") print(f"{heirname}\t{out['address']}: {out['value_sats']}")
print() print()
size = tx.estimated_size() size = tx.estimated_size()
print( print("fee: {}\texpected: {}\tsize: {}".format(tx.input_value()-tx.output_value(), size*tx_fees, size))
"fee: {}\texpected: {}\tsize: {}".format(
tx.input_value() - tx.output_value(), size * tx_fees, size
)
)
print() print()
try: try:
@@ -251,8 +215,7 @@ def print_transaction(heirs, tx, locktimes, tx_fees):
print("impossible to serialize") print("impossible to serialize")
print() print()
def get_change_output(wallet,in_amount,out_amount,fee):
def get_change_output(wallet, in_amount, out_amount, fee):
change_amount = int(in_amount - out_amount - fee) change_amount = int(in_amount - out_amount - fee)
if change_amount > wallet.dust_threshold(): if change_amount > wallet.dust_threshold():
change_addresses = wallet.get_change_addresses_for_new_transaction() change_addresses = wallet.get_change_addresses_for_new_transaction()
@@ -260,23 +223,22 @@ def get_change_output(wallet, in_amount, out_amount, fee):
out.is_change = True out.is_change = True
return out return out
class Heirs(dict, Logger): class Heirs(dict, Logger):
def __init__(self, db: "WalletDB"): def __init__(self, db: 'WalletDB'):
Logger.__init__(self) Logger.__init__(self)
self.db = db self.db = db
d = self.db.get("heirs", {}) d = self.db.get('heirs', {})
try: try:
self.update(d) self.update(d)
except e as Exception: except e as Exception:
return return
def invalidate_transactions(self, wallet): def invalidate_transactions(self,wallet):
invalidate_inheritance_transactions(wallet) invalidate_inheritance_transactions(wallet)
def save(self): def save(self):
self.db.put("heirs", dict(self)) self.db.put('heirs', dict(self))
def import_file(self, path): def import_file(self, path):
data = read_json_file(path) data = read_json_file(path)
@@ -297,33 +259,25 @@ class Heirs(dict, Logger):
self.save() self.save()
return res return res
def get_locktimes(self, from_locktime, a=False): def get_locktimes(self,from_locktime, a=False):
locktimes = {} locktimes = {}
for key in self.keys(): for key in self.keys():
locktime = Util.parse_locktime_string(self[key][HEIR_LOCKTIME]) locktime = Util.parse_locktime_string(self[key][HEIR_LOCKTIME])
if locktime > from_locktime and not a or locktime <= from_locktime and a: if locktime > from_locktime and not a \
locktimes[int(locktime)] = None or locktime <=from_locktime and a:
locktimes[int(locktime)]=None
return locktimes.keys() return locktimes.keys()
def check_locktime(self): def check_locktime(self):
return False return False
def normalize_perc( def normalize_perc(self, heir_list, total_balance, relative_balance,wallet,real=False):
self, heir_list, total_balance, relative_balance, wallet, real=False
):
amount = 0 amount = 0
for key, v in heir_list.items(): for key,v in heir_list.items():
try: try:
column = HEIR_AMOUNT column = HEIR_AMOUNT
if real: if real: column = HEIR_REAL_AMOUNT
column = HEIR_REAL_AMOUNT value = int(math.floor(total_balance/relative_balance*self.amount_to_float(v[column])))
value = int(
math.floor(
total_balance
/ relative_balance
* self.amount_to_float(v[column])
)
)
if value > wallet.dust_threshold(): if value > wallet.dust_threshold():
heir_list[key].insert(HEIR_REAL_AMOUNT, value) heir_list[key].insert(HEIR_REAL_AMOUNT, value)
amount += value amount += value
@@ -332,7 +286,7 @@ class Heirs(dict, Logger):
raise e raise e
return amount return amount
def amount_to_float(self, amount): def amount_to_float(self,amount):
try: try:
return float(amount) return float(amount)
except: except:
@@ -341,71 +295,60 @@ class Heirs(dict, Logger):
except: except:
return 0.0 return 0.0
def fixed_percent_lists_amount(self, from_locktime, dust_threshold, reverse=False): def fixed_percent_lists_amount(self,from_locktime,dust_threshold,reverse = False):
fixed_heirs = {} fixed_heirs = {}
fixed_amount = 0.0 fixed_amount = 0.0
percent_heirs = {} percent_heirs= {}
percent_amount = 0.0 percent_amount = 0.0
for key in self.keys(): for key in self.keys():
try: try:
cmp = ( cmp= Util.parse_locktime_string(self[key][HEIR_LOCKTIME]) - from_locktime
Util.parse_locktime_string(self[key][HEIR_LOCKTIME]) - from_locktime if cmp<=0:
)
if cmp <= 0:
continue continue
if Util.is_perc(self[key][HEIR_AMOUNT]): if Util.is_perc(self[key][HEIR_AMOUNT]):
percent_amount += float(self[key][HEIR_AMOUNT][:-1]) percent_amount += float(self[key][HEIR_AMOUNT][:-1])
percent_heirs[key] = list(self[key]) percent_heirs[key] =list(self[key])
else: else:
heir_amount = int(math.floor(float(self[key][HEIR_AMOUNT]))) heir_amount = int(math.floor(float(self[key][HEIR_AMOUNT])))
if heir_amount > dust_threshold: if heir_amount>dust_threshold:
fixed_amount += heir_amount fixed_amount += heir_amount
fixed_heirs[key] = list(self[key]) fixed_heirs[key] = list(self[key])
fixed_heirs[key].insert(HEIR_REAL_AMOUNT, heir_amount) fixed_heirs[key].insert(HEIR_REAL_AMOUNT,heir_amount)
else: else:
pass pass
except Exception as e: except Exception as e:
_logger.error(e) _logger.error(e)
return fixed_heirs, fixed_amount, percent_heirs, percent_amount return fixed_heirs,fixed_amount,percent_heirs,percent_amount
def prepare_lists(
self, balance, total_fees, wallet, willexecutor=False, from_locktime=0 def prepare_lists(self, balance, total_fees, wallet, willexecutor = False, from_locktime = 0):
):
willexecutors_amount = 0 willexecutors_amount = 0
willexecutors = {} willexecutors = {}
heir_list = {} heir_list = {}
onlyfixed = False onlyfixed = False
newbalance = balance - total_fees newbalance = balance - total_fees
locktimes = self.get_locktimes(from_locktime) locktimes = self.get_locktimes(from_locktime);
if willexecutor: if willexecutor:
for locktime in locktimes: for locktime in locktimes:
if int(Util.int_locktime(locktime)) > int(from_locktime): if int(Util.int_locktime(locktime)) > int(from_locktime):
try: try:
base_fee = int(willexecutor["base_fee"]) base_fee = int(willexecutor['base_fee'])
willexecutors_amount += base_fee willexecutors_amount += base_fee
h = [None] * 4 h = [None] * 4
h[HEIR_AMOUNT] = base_fee h[HEIR_AMOUNT] = base_fee
h[HEIR_REAL_AMOUNT] = base_fee h[HEIR_REAL_AMOUNT] = base_fee
h[HEIR_LOCKTIME] = locktime h[HEIR_LOCKTIME] = locktime
h[HEIR_ADDRESS] = willexecutor["address"] h[HEIR_ADDRESS] = willexecutor['address']
willexecutors[ willexecutors["w!ll3x3c\""+willexecutor['url']+"\""+str(locktime)] = h
'w!ll3x3c"' + willexecutor["url"] + '"' + str(locktime)
] = h
except Exception as e: except Exception as e:
return [], False return [],False
else: else:
_logger.error( _logger.error(f"heir excluded from will locktime({locktime}){Util.int_locktime(locktime)}<minimum{from_locktime}"),
f"heir excluded from will locktime({locktime}){Util.int_locktime(locktime)}<minimum{from_locktime}"
),
heir_list.update(willexecutors) heir_list.update(willexecutors)
newbalance -= willexecutors_amount newbalance -= willexecutors_amount
fixed_heirs, fixed_amount, percent_heirs, percent_amount = ( fixed_heirs,fixed_amount,percent_heirs,percent_amount = self.fixed_percent_lists_amount(from_locktime,wallet.dust_threshold())
self.fixed_percent_lists_amount(from_locktime, wallet.dust_threshold())
)
if fixed_amount > newbalance: if fixed_amount > newbalance:
fixed_amount = self.normalize_perc( fixed_amount = self.normalize_perc(fixed_heirs,newbalance,fixed_amount,wallet)
fixed_heirs, newbalance, fixed_amount, wallet
)
onlyfixed = True onlyfixed = True
heir_list.update(fixed_heirs) heir_list.update(fixed_heirs)
@@ -413,42 +356,31 @@ class Heirs(dict, Logger):
newbalance -= fixed_amount newbalance -= fixed_amount
if newbalance > 0: if newbalance > 0:
perc_amount = self.normalize_perc( perc_amount = self.normalize_perc(percent_heirs,newbalance,percent_amount,wallet)
percent_heirs, newbalance, percent_amount, wallet
)
newbalance -= perc_amount newbalance -= perc_amount
heir_list.update(percent_heirs) heir_list.update(percent_heirs)
if newbalance > 0: if newbalance > 0:
newbalance += fixed_amount newbalance += fixed_amount
fixed_amount = self.normalize_perc( fixed_amount = self.normalize_perc(fixed_heirs,newbalance,fixed_amount,wallet,real=True)
fixed_heirs, newbalance, fixed_amount, wallet, real=True
)
newbalance -= fixed_amount newbalance -= fixed_amount
heir_list.update(fixed_heirs) heir_list.update(fixed_heirs)
heir_list = sorted( heir_list = sorted(heir_list.items(), key = lambda item: Util.parse_locktime_string(item[1][HEIR_LOCKTIME],wallet))
heir_list.items(),
key=lambda item: Util.parse_locktime_string(item[1][HEIR_LOCKTIME], wallet),
)
locktimes = {} locktimes = {}
for key, value in heir_list: for key, value in heir_list:
locktime = Util.parse_locktime_string(value[HEIR_LOCKTIME]) locktime=Util.parse_locktime_string(value[HEIR_LOCKTIME])
if not locktime in locktimes: if not locktime in locktimes: locktimes[locktime]={key:value}
locktimes[locktime] = {key: value} else: locktimes[locktime][key]=value
else:
locktimes[locktime][key] = value
return locktimes, onlyfixed return locktimes, onlyfixed
def is_perc(self,key):
def is_perc(self, key):
return Util.is_perc(self[key][HEIR_AMOUNT]) return Util.is_perc(self[key][HEIR_AMOUNT])
def buildTransactions( def buildTransactions(self,bal_plugin,wallet,tx_fees = None, utxos=None,from_locktime=0):
self, bal_plugin, wallet, tx_fees=None, utxos=None, from_locktime=0
):
Heirs._validate(self) Heirs._validate(self)
if len(self) <= 0: if len(self)<=0:
return return
balance = 0.0 balance = 0.0
len_utxo_set = 0 len_utxo_set = 0
@@ -456,21 +388,20 @@ class Heirs(dict, Logger):
if not utxos: if not utxos:
utxos = wallet.get_utxos() utxos = wallet.get_utxos()
willexecutors = Willexecutors.get_willexecutors(bal_plugin) or {} willexecutors = Willexecutors.get_willexecutors(bal_plugin) or {}
self.decimal_point = bal_plugin.get_decimal_point() self.decimal_point=bal_plugin.config.get_decimal_point()
no_willexecutors = bal_plugin.NO_WILLEXECUTOR.get() no_willexecutors = bal_plugin.NO_WILLEXECUTOR.get()
for utxo in utxos: for utxo in utxos:
if utxo.value_sats() > 0 * tx_fees: if utxo.value_sats()> 0*tx_fees:
balance += utxo.value_sats() balance += utxo.value_sats()
len_utxo_set += 1 len_utxo_set += 1
available_utxos.append(utxo) available_utxos.append(utxo)
if len_utxo_set == 0: if len_utxo_set==0: return
return j=-2
j = -2
willexecutorsitems = list(willexecutors.items()) willexecutorsitems = list(willexecutors.items())
willexecutorslen = len(willexecutorsitems) willexecutorslen = len(willexecutorsitems)
alltxs = {} alltxs = {}
while True: while True:
j += 1 j+=1
if j >= willexecutorslen: if j >= willexecutorslen:
break break
elif 0 <= j: elif 0 <= j:
@@ -478,7 +409,7 @@ class Heirs(dict, Logger):
if not Willexecutors.is_selected(willexecutor): if not Willexecutors.is_selected(willexecutor):
continue continue
else: else:
willexecutor["url"] = url willexecutor['url']=url
elif j == -1: elif j == -1:
if not no_willexecutors: if not no_willexecutors:
continue continue
@@ -486,84 +417,78 @@ class Heirs(dict, Logger):
else: else:
break break
fees = {} fees = {}
i = 0 i=0
while True: while True:
txs = {} txs = {}
redo = False redo = False
i += 1 i+=1
total_fees = 0 total_fees=0
for fee in fees: for fee in fees:
total_fees += int(fees[fee]) total_fees += int(fees[fee])
newbalance = balance newbalance = balance
locktimes, onlyfixed = self.prepare_lists( locktimes, onlyfixed = self.prepare_lists(balance, total_fees, wallet, willexecutor, from_locktime)
balance, total_fees, wallet, willexecutor, from_locktime
)
try: try:
txs = prepare_transactions( txs = prepare_transactions(locktimes, available_utxos[:], fees, wallet)
locktimes, available_utxos[:], fees, wallet
)
if not txs: if not txs:
return {} return {}
except Exception as e: except Exception as e:
try: try:
if "w!ll3x3c" in e.heirname: if "w!ll3x3c" in e.heirname:
Willexecutors.is_selected(willexecutors[w], False) Willexecutors.is_selected(willexecutors[w],False)
break break
except: except:
raise e raise e
total_fees = 0 total_fees = 0
total_fees_real = 0 total_fees_real = 0
total_in = 0 total_in = 0
for txid, tx in txs.items(): for txid,tx in txs.items():
tx.willexecutor = willexecutor tx.willexecutor = willexecutor
fee = tx.estimated_size() * tx_fees fee = tx.estimated_size() * tx_fees
txs[txid].tx_fees = tx_fees txs[txid].tx_fees= tx_fees
total_fees += fee total_fees += fee
total_fees_real += tx.get_fee() total_fees_real += tx.get_fee()
total_in += tx.input_value() total_in += tx.input_value()
rfee = tx.input_value() - tx.output_value() rfee= tx.input_value()-tx.output_value()
if rfee < fee or rfee > fee + wallet.dust_threshold(): if rfee < fee or rfee > fee + wallet.dust_threshold():
redo = True redo = True
oldfees = fees.get(tx.my_locktime, 0) oldfees= fees.get(tx.my_locktime,0)
fees[tx.my_locktime] = fee fees[tx.my_locktime]=fee
if balance - total_in > wallet.dust_threshold(): if balance - total_in > wallet.dust_threshold():
redo = True redo = True
if not redo: if not redo:
break break
if i >= 10: if i>=10:
break break
alltxs.update(txs) alltxs.update(txs)
return alltxs return alltxs
def get_transactions(self,bal_plugin,wallet,tx_fees,utxos=None,from_locktime=0):
def get_transactions( txs=self.buildTransactions(bal_plugin,wallet,tx_fees,utxos,from_locktime)
self, bal_plugin, wallet, tx_fees, utxos=None, from_locktime=0
):
txs = self.buildTransactions(bal_plugin, wallet, tx_fees, utxos, from_locktime)
if txs: if txs:
temp_txs = {} temp_txs = {}
for txid in txs: for txid in txs:
if txs[txid].available_utxos: if txs[txid].available_utxos:
temp_txs.update( temp_txs.update(self.get_transactions(bal_plugin,wallet,tx_fees,txs[txid].available_utxos,txs[txid].locktime))
self.get_transactions(
bal_plugin,
wallet,
tx_fees,
txs[txid].available_utxos,
txs[txid].locktime,
)
)
txs.update(temp_txs) txs.update(temp_txs)
return txs return txs
def resolve(self, k): def resolve(self, k):
if bitcoin.is_address(k): if bitcoin.is_address(k):
return {"address": k, "type": "address"} return {
'address': k,
'type': 'address'
}
if k in self.keys(): if k in self.keys():
_type, addr = self[k] _type, addr = self[k]
if _type == "address": if _type == 'address':
return {"address": addr, "type": "heir"} return {
'address': addr,
'type': 'heir'
}
if openalias := self.resolve_openalias(k): if openalias := self.resolve_openalias(k):
return openalias return openalias
raise AliasNotFoundException("Invalid Bitcoin address or alias", k) raise AliasNotFoundException("Invalid Bitcoin address or alias", k)
@@ -574,10 +499,10 @@ class Heirs(dict, Logger):
if out: if out:
address, name, validated = out address, name, validated = out
return { return {
"address": address, 'address': address,
"name": name, 'name': name,
"type": "openalias", 'type': 'openalias',
"validated": validated, 'validated': validated
} }
return {} return {}
@@ -585,19 +510,21 @@ class Heirs(dict, Logger):
for k in self.keys(): for k in self.keys():
_type, addr = self[k] _type, addr = self[k]
if addr.casefold() == name.casefold(): if addr.casefold() == name.casefold():
return {"name": addr, "type": _type, "address": k} return {
'name': addr,
'type': _type,
'address': k
}
return None return None
def fetch_openalias(self, config: "SimpleConfig"): def fetch_openalias(self, config: 'SimpleConfig'):
self.alias_info = None self.alias_info = None
alias = config.OPENALIAS_ID alias = config.OPENALIAS_ID
if alias: if alias:
alias = str(alias) alias = str(alias)
def f(): def f():
self.alias_info = self._resolve_openalias(alias) self.alias_info = self._resolve_openalias(alias)
trigger_callback("alias_received") trigger_callback('alias_received')
t = threading.Thread(target=f) t = threading.Thread(target=f)
t.daemon = True t.daemon = True
t.start() t.start()
@@ -605,18 +532,18 @@ class Heirs(dict, Logger):
@classmethod @classmethod
def _resolve_openalias(cls, url: str) -> Optional[Tuple[str, str, bool]]: def _resolve_openalias(cls, url: str) -> Optional[Tuple[str, str, bool]]:
# support email-style addresses, per the OA standard # support email-style addresses, per the OA standard
url = url.replace("@", ".") url = url.replace('@', '.')
try: try:
records, validated = dnssec.query(url, dns.rdatatype.TXT) records, validated = dnssec.query(url, dns.rdatatype.TXT)
except DNSException as e: except DNSException as e:
_logger.info(f"Error resolving openalias: {repr(e)}") _logger.info(f'Error resolving openalias: {repr(e)}')
return None return None
prefix = "btc" prefix = 'btc'
for record in records: for record in records:
string = to_string(record.strings[0], "utf8") string = to_string(record.strings[0], 'utf8')
if string.startswith("oa1:" + prefix): if string.startswith('oa1:' + prefix):
address = cls.find_regex(string, r"recipient_address=([A-Za-z0-9]+)") address = cls.find_regex(string, r'recipient_address=([A-Za-z0-9]+)')
name = cls.find_regex(string, r"recipient_name=([^;]+)") name = cls.find_regex(string, r'recipient_name=([^;]+)')
if not name: if not name:
name = address name = address
if not address: if not address:
@@ -631,6 +558,7 @@ class Heirs(dict, Logger):
except AttributeError: except AttributeError:
return None return None
def validate_address(address): def validate_address(address):
if not bitcoin.is_address(address): if not bitcoin.is_address(address):
raise NotAnAddress(f"not an address,{address}") raise NotAnAddress(f"not an address,{address}")
@@ -645,43 +573,36 @@ class Heirs(dict, Logger):
raise AmountNotValid(f"amount not properly formatted, {e}") raise AmountNotValid(f"amount not properly formatted, {e}")
return amount return amount
def validate_locktime(locktime, timestamp_to_check=False): def validate_locktime(locktime,timestamp_to_check=False):
try: try:
if timestamp_to_check: if timestamp_to_check:
if Util.parse_locktime_string(locktime, None) < timestamp_to_check: if Util.parse_locktime_string(locktime,None) < timestamp_to_check:
raise HeirExpiredException() raise HeirExpiredException()
except Exception as e: except Exception as e:
raise LocktimeNotValid(f"locktime string not properly formatted, {e}") raise LocktimeNotValid(f"locktime string not properly formatted, {e}")
return locktime return locktime
def validate_heir(k, v, timestamp_to_check=False): def validate_heir(k,v,timestamp_to_check=False):
address = Heirs.validate_address(v[HEIR_ADDRESS]) address = Heirs.validate_address(v[HEIR_ADDRESS])
amount = Heirs.validate_amount(v[HEIR_AMOUNT]) amount = Heirs.validate_amount(v[HEIR_AMOUNT])
locktime = Heirs.validate_locktime(v[HEIR_LOCKTIME], timestamp_to_check) locktime = Heirs.validate_locktime(v[HEIR_LOCKTIME],timestamp_to_check)
return (address, amount, locktime) return (address,amount,locktime)
def _validate(data, timestamp_to_check=False): def _validate(data,timestamp_to_check=False):
for k, v in list(data.items()): for k, v in list(data.items()):
if k == "heirs": if k == 'heirs':
return Heirs._validate(v) return Heirs._validate(v)
try: try:
Heirs.validate_heir(k, v) Heirs.validate_heir(k,v)
except Exception as e: except Exception as e:
data.pop(k) data.pop(k)
return data return data
class NotAnAddress(ValueError): class NotAnAddress(ValueError):
pass pass
class AmountNotValid(ValueError): class AmountNotValid(ValueError):
pass pass
class LocktimeNotValid(ValueError): class LocktimeNotValid(ValueError):
pass pass
class HeirExpiredException(LocktimeNotValid): class HeirExpiredException(LocktimeNotValid):
pass pass

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -1,7 +1,7 @@
{ {
"name": "BAL", "name": "BAL",
"fullname": "Bitcoin After Life", "fullname": "Bitcoin After Life",
"description": "Provides free and decentralized inheritance support<br> Version: 0.2.4", "description": "Provides free and decentralized inheritance support<br> Version: 0.2.2a",
"author":"Svatantrya", "author":"Svatantrya",
"available_for": ["qt"], "available_for": ["qt"],
"icon":"icons/bal32x32.png" "icon":"icons/bal32x32.png"

2662
qt.py

File diff suppressed because it is too large Load Diff

361
util.py
View File

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

View File

@@ -1,77 +0,0 @@
#!env/bin/python3
from electrum.storage import WalletStorage
from electrum.util import MyEncoder
import json
import sys
import getpass
import os
default_fees = 100
def fix_will_settings_tx_fees(json_wallet):
tx_fees = json_wallet.get("will_settings", {}).get("tx_fees", False)
have_to_update = False
if tx_fees:
json_wallet["will_settings"]["baltx_fees"] = tx_fees
del json_wallet["will_settings"]["tx_fees"]
have_to_update = True
for txid, willitem in json_wallet["will"].items():
tx_fees = willitem.get("tx_fees", False)
if tx_fees:
json_wallet["will"][txid]["baltx_fees"] = tx_fees
del json_wallet["will"][txid]["tx_fees"]
have_to_update = True
return have_to_update
def uninstall_bal(json_wallet):
del json_wallet["will_settings"]
del json_wallet["will"]
del json_wallet["heirs"]
return True
def save(json_wallet, storage):
human_readable = not storage.is_encrypted()
storage.write(
json.dumps(
json_wallet,
indent=4 if human_readable else None,
sort_keys=bool(human_readable),
cls=MyEncoder,
)
)
def read_wallet(path, password=False):
storage = WalletStorage(path)
if storage.is_encrypted():
if password == False:
password = getpass.getpass("Enter wallet password: ", stream=None)
storage.decrypt(password)
data = storage.read()
json_wallet = json.loads("[" + data + "]")[0]
return json_wallet
if __name__ == "__main__":
if len(sys.argv) < 3:
print("usage: ./bal_wallet_utils <command> <wallet path>")
print("available commands: uninstall, fix")
exit(1)
if not os.path.exists(sys.argv[2]):
print("Error: wallet not found")
exit(1)
command = sys.argv[1]
path = sys.argv[2]
json_wallet = read_wallet(path)
have_to_save = False
if command == "fix":
have_to_save = fix_will_settings_tx_fees(json_wallet)
if command == "uninstall":
have_to_save = uninstall_bal(json_wallet)
if have_to_save:
save(json_wallet, storage)
else:
print("nothing to do")

View File

@@ -1,208 +0,0 @@
#!/usr/bin/env python3
import sys
import os
import json
from PyQt6.QtWidgets import (
QApplication,
QMainWindow,
QVBoxLayout,
QHBoxLayout,
QLabel,
QLineEdit,
QPushButton,
QWidget,
QFileDialog,
QGroupBox,
QTextEdit,
)
from PyQt6.QtCore import Qt
from electrum.storage import WalletStorage
from electrum.util import MyEncoder
from bal_wallet_utils import fix_will_settings_tx_fees, uninstall_bal, read_wallet, save
class WalletUtilityGUI(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setWindowTitle("BAL Wallet Utility")
self.setFixedSize(500, 400)
# Central widget
central_widget = QWidget()
self.setCentralWidget(central_widget)
# Main layout
layout = QVBoxLayout(central_widget)
# Wallet input group
wallet_group = QGroupBox("Wallet Settings")
wallet_layout = QVBoxLayout(wallet_group)
# Wallet path
wallet_path_layout = QHBoxLayout()
wallet_path_layout.addWidget(QLabel("Wallet Path:"))
self.wallet_path_edit = QLineEdit()
self.wallet_path_edit.setPlaceholderText("Select wallet path...")
wallet_path_layout.addWidget(self.wallet_path_edit)
self.browse_btn = QPushButton("Browse...")
self.browse_btn.clicked.connect(self.browse_wallet)
wallet_path_layout.addWidget(self.browse_btn)
wallet_layout.addLayout(wallet_path_layout)
# Password
password_layout = QHBoxLayout()
password_layout.addWidget(QLabel("Password:"))
self.password_edit = QLineEdit()
self.password_edit.setEchoMode(QLineEdit.EchoMode.Password)
self.password_edit.setPlaceholderText("Enter password (if encrypted)")
password_layout.addWidget(self.password_edit)
wallet_layout.addLayout(password_layout)
layout.addWidget(wallet_group)
# Output area
output_group = QGroupBox("Output")
output_layout = QVBoxLayout(output_group)
self.output_text = QTextEdit()
self.output_text.setReadOnly(True)
output_layout.addWidget(self.output_text)
layout.addWidget(output_group)
# Action buttons
buttons_layout = QHBoxLayout()
self.fix_btn = QPushButton("Fix")
self.fix_btn.clicked.connect(self.fix_wallet)
self.fix_btn.setEnabled(False)
buttons_layout.addWidget(self.fix_btn)
self.uninstall_btn = QPushButton("Uninstall")
self.uninstall_btn.clicked.connect(self.uninstall_wallet)
self.uninstall_btn.setEnabled(False)
buttons_layout.addWidget(self.uninstall_btn)
layout.addLayout(buttons_layout)
# Connections to enable buttons when path is entered
self.wallet_path_edit.textChanged.connect(self.check_inputs)
def browse_wallet(self):
file_path, _ = QFileDialog.getOpenFileName(
self, "Select Wallet", "*", "Electrum Wallet (*)"
)
if file_path:
self.wallet_path_edit.setText(file_path)
def check_inputs(self):
wallet_path = self.wallet_path_edit.text().strip()
has_path = bool(wallet_path) and os.path.exists(wallet_path)
self.fix_btn.setEnabled(has_path)
self.uninstall_btn.setEnabled(has_path)
def log_message(self, message):
self.output_text.append(message)
def fix_wallet(self):
self.process_wallet("fix")
def uninstall_wallet(self):
self.log_message(
"WARNING: This will remove all BAL settings. This operation cannot be undone."
)
self.process_wallet("uninstall")
def process_wallet(self, command):
wallet_path = self.wallet_path_edit.text().strip()
password = self.password_edit.text()
if not wallet_path:
self.log_message("ERROR: Please enter wallet path")
return
if not os.path.exists(wallet_path):
self.log_message("ERROR: Wallet not found")
return
try:
self.log_message(f"Processing wallet: {wallet_path}")
storage = WalletStorage(wallet_path)
# Decrypt if necessary
if storage.is_encrypted():
if not password:
self.log_message(
"ERROR: Wallet is encrypted, please enter password"
)
return
try:
storage.decrypt(password)
self.log_message("Wallet decrypted successfully")
except Exception as e:
self.log_message(f"ERROR: Wrong password: {str(e)}")
return
# Read wallet
data = storage.read()
json_wallet = json.loads("[" + data + "]")[0]
have_to_save = False
message = ""
if command == "fix":
have_to_save = fix_will_settings_tx_fees(json_wallet)
message = (
"Fix applied successfully" if have_to_save else "No fix needed"
)
elif command == "uninstall":
have_to_save = uninstall_bal(json_wallet)
message = (
"BAL uninstalled successfully"
if have_to_save
else "No BAL settings found to uninstall"
)
if have_to_save:
try:
save(json_wallet, storage)
self.log_message(f"SUCCESS: {message}")
except Exception as e:
self.log_message(f"Save error: {str(e)}")
else:
self.log_message(f"INFO: {message}")
except Exception as e:
error_msg = f"ERROR: Processing failed: {str(e)}"
self.log_message(error_msg)
def main():
app = QApplication(sys.argv)
# Check if dependencies are available
try:
from electrum.storage import WalletStorage
from electrum.util import MyEncoder
except ImportError as e:
print(f"ERROR: Cannot import Electrum dependencies: {str(e)}")
return 1
window = WalletUtilityGUI()
window.show()
return app.exec()
if __name__ == "__main__":
sys.exit(main())

647
will.py

File diff suppressed because it is too large Load Diff

View File

@@ -1,185 +1,156 @@
import json import json
from datetime import datetime from datetime import datetime
from functools import partial from functools import partial
from aiohttp import ClientResponse from aiohttp import ClientResponse
from electrum.network import Network
from electrum import constants from electrum import constants
from electrum.logging import get_logger
from electrum.gui.qt.util import WaitingDialog from electrum.gui.qt.util import WaitingDialog
from electrum.i18n import _ from electrum.i18n import _
from electrum.logging import get_logger
from electrum.network import Network
from .bal import BalPlugin from .bal import BalPlugin
from .util import Util from .util import Util
DEFAULT_TIMEOUT = 5 DEFAULT_TIMEOUT = 5
_logger = get_logger(__name__) _logger = get_logger(__name__)
class Willexecutors: class Willexecutors:
def save(bal_plugin, willexecutors): def save(bal_plugin, willexecutors):
aw = bal_plugin.WILLEXECUTORS.get() aw=bal_plugin.WILLEXECUTORS.get()
aw[constants.net.NET_NAME] = willexecutors aw[constants.net.NET_NAME]=willexecutors
bal_plugin.WILLEXECUTORS.set(aw) bal_plugin.WILLEXECUTORS.set(aw)
def get_willexecutors( def get_willexecutors(bal_plugin, update = False,bal_window=False,force=False,task=True):
bal_plugin, update=False, bal_window=False, force=False, task=True
):
willexecutors = bal_plugin.WILLEXECUTORS.get() willexecutors = bal_plugin.WILLEXECUTORS.get()
willexecutors = willexecutors.get(constants.net.NET_NAME, {}) willexecutors=willexecutors.get(constants.net.NET_NAME,{})
to_del = [] to_del=[]
for w in willexecutors: for w in willexecutors:
if not isinstance(willexecutors[w], dict): if not isinstance(willexecutors[w],dict):
to_del.append(w) to_del.append(w)
continue continue
Willexecutors.initialize_willexecutor(willexecutors[w], w) Willexecutors.initialize_willexecutor(willexecutors[w],w)
for w in to_del: for w in to_del:
_logger.error("error Willexecutor to delete type:{} ", type(willexecutor[w]),w) print("ERROR: WILLEXECUTOR TO DELETE:", w)
del willexecutors[w] del willexecutors[w]
bal = bal_plugin.WILLEXECUTORS.default.get(constants.net.NET_NAME, {}) bal = bal_plugin.WILLEXECUTORS.default.get(constants.net.NET_NAME,{})
for bal_url, bal_executor in bal.items(): for bal_url,bal_executor in bal.items():
if not bal_url in willexecutors: if not bal_url in willexecutors:
_logger.debug(f"force add {bal_url} willexecutor") _logger.debug(f"force add {bal_url} willexecutor")
willexecutors[bal_url] = bal_executor willexecutors[bal_url] = bal_executor
#if update: if update:
# found = False found = False
# for url, we in willexecutors.items(): for url,we in willexecutors.items():
# if Willexecutors.is_selected(we): if Willexecutors.is_selected(we):
# found = True found = True
# if found or force: if found or force:
# if bal_plugin.PING_WILLEXECUTORS.get() or force: if bal_plugin.PING_WILLEXECUTORS.get() or force:
# ping_willexecutors = True ping_willexecutors = True
# if bal_plugin.ASK_PING_WILLEXECUTORS.get() and not force: if bal_plugin.ASK_PING_WILLEXECUTORS.get() and not force:
# if bal_window: if bal_window:
# ping_willexecutors = bal_window.window.question( ping_willexecutors = bal_window.window.question(_("Contact willexecutors servers to update payment informations?"))
# _(
# "Contact willexecutors servers to update payment informations?"
# )
# )
# if ping_willexecutors: if ping_willexecutors:
# if task: if task:
# bal_window.ping_willexecutors(willexecutors, task) bal_window.ping_willexecutors(willexecutors,task)
# else: else:
# bal_window.ping_willexecutors_task(willexecutors) bal_window.ping_willexecutors_task(willexecutors)
w_sorted = dict( w_sorted = dict(sorted(willexecutors.items(), key=lambda w:w[1].get('sort',0),reverse=True))
sorted(
willexecutors.items(), key=lambda w: w[1].get("sort", 0), reverse=True
)
)
return w_sorted return w_sorted
def is_selected(willexecutor,value=None):
def is_selected(willexecutor, value=None):
if not willexecutor: if not willexecutor:
return False return False
if not value is None: if not value is None:
willexecutor["selected"] = value willexecutor['selected']=value
try: try:
return willexecutor["selected"] return willexecutor['selected']
except: except:
willexecutor["selected"] = False willexecutor['selected']=False
return False return False
def get_willexecutor_transactions(will, force=False): def get_willexecutor_transactions(will, force=False):
willexecutors = {} willexecutors ={}
for wid, willitem in will.items(): for wid,willitem in will.items():
if willitem.get_status("VALID"): if willitem.get_status('VALID'):
if willitem.get_status("COMPLETE"): if willitem.get_status('COMPLETE'):
if not willitem.get_status("PUSHED") or force: if not willitem.get_status('PUSHED') or force:
if willexecutor := willitem.we: if willexecutor := willitem.we:
url = willexecutor["url"] url=willexecutor['url']
if willexecutor and Willexecutors.is_selected(willexecutor): if willexecutor and Willexecutors.is_selected(willexecutor):
if not url in willexecutors: if not url in willexecutors:
willexecutor["txs"] = "" willexecutor['txs']=""
willexecutor["txsids"] = [] willexecutor['txsids']=[]
willexecutor["broadcast_status"] = _("Waiting...") willexecutor['broadcast_status']= _("Waiting...")
willexecutors[url] = willexecutor willexecutors[url]=willexecutor
willexecutors[url]["txs"] += str(willitem.tx) + "\n" willexecutors[url]['txs']+=str(willitem.tx)+"\n"
willexecutors[url]["txsids"].append(wid) willexecutors[url]['txsids'].append(wid)
return willexecutors return willexecutors
def only_selected_list(willexecutors): def only_selected_list(willexecutors):
out = {} out = {}
for url, v in willexecutors.items(): for url,v in willexecutors.items():
if Willexecutors.is_selected(willexecutor): if Willexecutors.is_selected(willexecutor):
out[url] = v out[url]=v
def push_transactions_to_willexecutors(will): def push_transactions_to_willexecutors(will):
willexecutors = get_transactions_to_be_pushed() willexecutors = get_transactions_to_be_pushed()
for url in willexecutors: for url in willexecutors:
willexecutor = willexecutors[url] willexecutor = willexecutors[url]
if Willexecutors.is_selected(willexecutor): if Willexecutors.is_selected(willexecutor):
if "txs" in willexecutor: if 'txs' in willexecutor:
Willexecutors.push_transactions_to_willexecutor( Willexecutors.push_transactions_to_willexecutor(willexecutors[url]['txs'],url)
willexecutors[url]["txs"], url
)
def send_request(method, url, data=None, *, timeout=10): def send_request(method, url, data=None, *, timeout=10):
network = Network.get_instance() network = Network.get_instance()
if not network: if not network:
raise ErrorConnectingServer("You are offline.") raise ErrorConnectingServer('You are offline.')
_logger.debug(f"<-- {method} {url} {data}") _logger.debug(f'<-- {method} {url} {data}')
headers = {} headers = {}
headers["user-agent"] = f"BalPlugin v:{BalPlugin.version()}" headers['user-agent'] = f"BalPlugin v:{BalPlugin.version()}"
headers["Content-Type"] = "text/plain" headers['Content-Type']='text/plain'
try: try:
if method == "get": if method == 'get':
response = Network.send_http_on_proxy( response = Network.send_http_on_proxy(method, url,
method,
url,
params=data, params=data,
headers=headers, headers=headers,
on_finish=Willexecutors.handle_response, on_finish=Willexecutors.handle_response,
timeout=timeout, timeout=timeout)
) elif method == 'post':
elif method == "post": response = Network.send_http_on_proxy(method, url,
response = Network.send_http_on_proxy(
method,
url,
body=data, body=data,
headers=headers, headers=headers,
on_finish=Willexecutors.handle_response, on_finish=Willexecutors.handle_response,
timeout=timeout, timeout=timeout)
)
else: else:
raise Exception(f"unexpected {method=!r}") raise Exception(f"unexpected {method=!r}")
except Exception as e: except Exception as e:
_logger.error(f"exception sending request {e}") _logger.error(f"exception sending request {e}")
raise e raise e
else: else:
_logger.debug(f"--> {response}") _logger.debug(f'--> {response}')
return response return response
async def handle_response(resp:ClientResponse):
async def handle_response(resp: ClientResponse): r=await resp.text()
r = await resp.text()
try: try:
r = json.loads(r) r=json.loads(r)
r["status"] = resp.status r['status'] = resp.status
r["selected"] = Willexecutors.is_selected(willexecutor) r['selected']=Willexecutors.is_selected(willexecutor)
r["url"] = url r['url']=url
except: except:
pass pass
return r return r
class AlreadyPresentException(Exception): class AlreadyPresentException(Exception):
pass pass
def push_transactions_to_willexecutor(willexecutor): def push_transactions_to_willexecutor(willexecutor):
out = True out=True
try: try:
_logger.debug(f"willexecutor['txs']") _logger.debug(f"willexecutor['txs']")
if w := Willexecutors.send_request( if w:=Willexecutors.send_request('post', willexecutor['url']+"/"+constants.net.NET_NAME+"/pushtxs", data=willexecutor['txs'].encode('ascii')):
"post", willexecutor['broadcast_status'] = _("Success")
willexecutor["url"] + "/" + constants.net.NET_NAME + "/pushtxs",
data=willexecutor["txs"].encode("ascii"),
):
willexecutor["broadcast_status"] = _("Success")
_logger.debug(f"pushed: {w}") _logger.debug(f"pushed: {w}")
if w != "thx": if w !='thx':
_logger.debug(f"error: {w}") _logger.debug(f"error: {w}")
raise Exception(w) raise Exception(w)
else: else:
@@ -188,90 +159,85 @@ class Willexecutors:
_logger.debug(f"error:{e}") _logger.debug(f"error:{e}")
if str(e) == "already present": if str(e) == "already present":
raise Willexecutors.AlreadyPresentException() raise Willexecutors.AlreadyPresentException()
out = False out=False
willexecutor["broadcast_status"] = _("Failed") willexecutor['broadcast_status'] = _("Failed")
return out return out
def ping_servers(willexecutors): def ping_servers(willexecutors):
for url, we in willexecutors.items(): for url,we in willexecutors.items():
Willexecutors.get_info_task(url, we) Willexecutors.get_info_task(url,we)
def get_info_task(url, willexecutor):
w = None def get_info_task(url,willexecutor):
w=None
try: try:
_logger.info("GETINFO_WILLEXECUTOR") _logger.info("GETINFO_WILLEXECUTOR")
_logger.debug(url) _logger.debug(url)
netname = "bitcoin" netname="bitcoin"
if constants.net.NET_NAME != "mainnet": if constants.net.NET_NAME!="mainnet":
netname = constants.net.NET_NAME netname=constants.net.NET_NAME
w = Willexecutors.send_request("get", url + "/" + netname + "/info") w = Willexecutors.send_request('get',url+"/"+netname+"/info")
willexecutor["url"] = url willexecutor['url']=url
willexecutor["status"] = w["status"] willexecutor['status'] = w['status']
willexecutor["base_fee"] = w["base_fee"] willexecutor['base_fee'] = w['base_fee']
willexecutor["address"] = w["address"] willexecutor['address'] = w['address']
if not willexecutor["info"]: if not willexecutor['info']:
willexecutor["info"] = w["info"] willexecutor['info'] = w['info']
_logger.debug(f"response_data {w['address']}") _logger.debug(f"response_data {w['address']}")
except Exception as e: except Exception as e:
_logger.error(f"error {e} contacting {url}: {w}") _logger.error(f"error {e} contacting {url}: {w}")
willexecutor["status"] = "KO" willexecutor['status']="KO"
willexecutor["last_update"] = datetime.now().timestamp() willexecutor['last_update'] = datetime.now().timestamp()
return willexecutor return willexecutor
def initialize_willexecutor(willexecutor, url, status=None, selected=None): def initialize_willexecutor(willexecutor,url,status=None,selected=None):
willexecutor["url"] = url willexecutor['url']=url
if not status is None: if not status is None:
willexecutor["status"] = status willexecutor['status'] = status
willexecutor["selected"] = Willexecutors.is_selected(willexecutor, selected) willexecutor['selected'] = Willexecutors.is_selected(willexecutor,selected)
def download_list(bal_plugin): def download_list(bal_plugin):
try: try:
l = Willexecutors.send_request( l = Willexecutors.send_request('get',"https://welist.bitcoin-after.life/data/bitcoin?page=0&limit=100")
"get", "https://welist.bitcoin-after.life/data/bitcoin?page=0&limit=100" del l['status']
)
del l["status"]
for w in l: for w in l:
willexecutor = l[w] willexecutor=l[w]
Willexecutors.initialize_willexecutor(willexecutor, w, "New", False) Willexecutors.initialize_willexecutor(willexecutor,w,'New',False)
# bal_plugin.WILLEXECUTORS.set(l) #bal_plugin.WILLEXECUTORS.set(l)
# bal_plugin.config.set_key(bal_plugin.WILLEXECUTORS,l,save=True) #bal_plugin.config.set_key(bal_plugin.WILLEXECUTORS,l,save=True)
return l return l
except Exception as e: except Exception as e:
_logger.error(f"Failed to download willexecutors list: {e}") _logger.error(f"Failed to download willexecutors list: {e}")
return {} return {}
def get_willexecutors_list_from_json(bal_plugin): def get_willexecutors_list_from_json(bal_plugin):
try: try:
with open("willexecutors.json") as f: with open("willexecutors.json") as f:
willexecutors = json.load(f) willexecutors = json.load(f)
for w in willexecutors: for w in willexecutors:
willexecutor = willexecutors[w] willexecutor=willexecutors[w]
Willexecutors.initialize_willexecutor(willexecutor, w, "New", False) Willexecutors.initialize_willexecutor(willexecutor,w,'New',False)
# bal_plugin.WILLEXECUTORS.set(willexecutors) #bal_plugin.WILLEXECUTORS.set(willexecutors)
return h return h
except Exception as e: except Exception as e:
_logger.error(f"error opening willexecutors json: {e}") _logger.error(f"error opening willexecutors json: {e}")
return {} return {}
def check_transaction(txid, url): def check_transaction(txid,url):
_logger.debug(f"{url}:{txid}") _logger.debug(f"{url}:{txid}")
try: try:
w = Willexecutors.send_request( w = Willexecutors.send_request('post',url+"/searchtx",data=txid.encode('ascii'))
"post", url + "/searchtx", data=txid.encode("ascii")
)
return w return w
except Exception as e: except Exception as e:
_logger.error(f"error contacting {url} for checking txs {e}") _logger.error(f"error contacting {url} for checking txs {e}")
raise e raise e
class WillExecutor: class WillExecutor:
def __init__(self, url, base_fee, chain, info, version): def __init__(self,url,base_fee,chain,info,version):
self.url = url self.url = url
self.base_fee = base_fee self.base_fee = base_fee
self.chain = chain self.chain = chain
@@ -279,4 +245,12 @@ class WillExecutor:
self.version = version self.version = version
def from_dict(d): def from_dict(d):
we = WillExecutor(d["url"], d["base_fee"], d["chain"], d["info"], d["version"]) we = WillExecutor(
d['url'],
d['base_fee'],
d['chain'],
d['info'],
d['version']
)