20 Commits

Author SHA1 Message Date
ef0ab56de4 fixed refresh and some minor bug about dust amounts and empty wallet 2026-03-05 10:46:38 -04:00
c5ad5a61bb bug delete heirs from wizard 2026-02-10 12:22:43 -04:00
f7bd09df91 dust bugfix 2026-02-09 12:10:31 -04:00
2a4eab81fd print 2026-02-05 17:51:17 -04:00
d86b941fcb refresh button. locktim is correctly saved, minor bugfix in checking confirmed transaction 2026-02-05 17:11:11 -04:00
1836cdd892 version 2026-02-03 22:12:30 -04:00
2416d0ce8d fix willexecutor list edit 2026-02-03 22:11:33 -04:00
8e4e401d1b fix plugin settings removed willexecutor ping 2026-02-03 16:14:12 -04:00
b8859ee5c1 qt thread panic
heirs import wizard
2026-02-03 13:56:47 -04:00
faeff1ff3c gui willexecutor list status moved after url to be more visible.
heirs tab start hidden
2026-02-03 11:25:21 -04:00
437105477d missing icons 2026-01-28 14:47:24 -04:00
4c12136470 release 2026-01-24 19:50:41 -04:00
a918c5564d close task invalidate tx 2026-01-09 16:46:22 -04:00
d2280969de read_file icons 2026-01-05 01:25:20 -04:00
6cf12eec80 black refactor + bugfix 2026-01-04 23:52:53 -04:00
936d4ef467 version 2026-01-04 16:54:27 -04:00
5512ee0e61 version 2026-01-04 16:49:43 -04:00
3e9a841e21 version 2026-01-04 16:41:53 -04:00
5d9636cda1 txfees fixer 2026-01-04 13:45:31 -04:00
b384ac562c wallet utils 2025-11-30 16:29:01 -04:00
15 changed files with 1357 additions and 1226 deletions

View File

@@ -1 +1 @@
0.2.2c 0.2.4

180
bal.py
View File

@@ -1,125 +1,133 @@
import random
import os import os
import zipfile as zipfile_lib
from electrum.plugin import BasePlugin # import random
# import zipfile as zipfile_lib
from electrum import json_db from electrum import json_db
from electrum.logging import get_logger
from electrum.plugin import BasePlugin
from electrum.transaction import tx_from_any from electrum.transaction import tx_from_any
import os
def get_will_settings(x):
print(x)
json_db.register_dict('heirs', tuple, None)
json_db.register_dict('will', dict,None)
json_db.register_dict('will_settings', lambda x:x,None)
#{'rubiconda': ['bcrt1qgv0wu4v6kjzef5mnxfh2m9z6y7mez0ja0tt8mu', '45%', '1y'], 'veronica': ['bcrt1q6vxuvwrt8x5c9u9u29y5uq7frscr0vgc2dy60j', '15%', '1y']}
from electrum.logging import get_logger
def get_will_settings(x): def get_will_settings(x):
print(x) # 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):
print("init bal_config")
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 not default is None: if default is not 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 f: with open("VERSION", "r") as fi:
f = str(f.readline()) f = str(fi.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):
print("init bal_plugin")
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__))
#self.logger.info(self.base_dir)
#print("base_dir:", self.base_dir)
#print("suca:",zipfile)
#print("plugin_dir:", self.plugin_dir)
import sys import sys
sys.path.insert(0, zipfile) sys.path.insert(0, zipfile)
#print("sono state listate?")
self.parent = parent self.parent = parent
self.config = config self.config = config
self.name = name self.name = name
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(config, "bal_locktimedelta_blocks", 144*7) self.LOCKTIMEDELTA_BLOCKS = BalConfig(
self.ENABLE_MULTIVERSE = BalConfig(config, "bal_enable_multiverse", False) config, "bal_locktimedelta_blocks", 144 * 7
self.TX_FEES = BalConfig(config, "bal_tx_fees", 100) )
self.INVALIDATE = BalConfig(config, "bal_invalidate", True) self.ENABLE_MULTIVERSE = BalConfig(config, "bal_enable_multiverse", False)
self.ASK_INVALIDATE = BalConfig(config, "bal_ask_invalidate", True) self.TX_FEES = BalConfig(config, "bal_tx_fees", 100)
self.PREVIEW = BalConfig(config, "bal_preview", True) self.INVALIDATE = BalConfig(config, "bal_invalidate", True)
self.SAVE_TXS = BalConfig(config, "bal_save_txs", True) self.ASK_INVALIDATE = BalConfig(config, "bal_ask_invalidate", True)
self.WILLEXECUTORS = BalConfig(config, "bal_willexecutors", True) self.PREVIEW = BalConfig(config, "bal_preview", True)
self.PING_WILLEXECUTORS = BalConfig(config, "bal_ping_willexecutors", True) self.SAVE_TXS = BalConfig(config, "bal_save_txs", True)
self.ASK_PING_WILLEXECUTORS = BalConfig(config, "bal_ask_ping_willexecutors", True) self.WILLEXECUTORS = BalConfig(config, "bal_willexecutors", True)
self.NO_WILLEXECUTOR = BalConfig(config, "bal_no_willexecutor", True) # self.PING_WILLEXECUTORS = BalConfig(config, "bal_ping_willexecutors", True)
self.HIDE_REPLACED = BalConfig(config, "bal_hide_replaced", True) # self.ASK_PING_WILLEXECUTORS = BalConfig(
self.HIDE_INVALIDATED = BalConfig(config, "bal_hide_invalidated", True) # config, "bal_ask_ping_willexecutors", True
self.ALLOW_REPUSH = BalConfig(config, "bal_allow_repush", True) # )
self.FIRST_EXECUTION = BalConfig(config, "bal_first_execution", True) self.NO_WILLEXECUTOR = BalConfig(config, "bal_no_willexecutor", True)
self.WILLEXECUTORS = BalConfig(config, "bal_willexecutors", { self.HIDE_REPLACED = BalConfig(config, "bal_hide_replaced", True)
"mainnet": { self.HIDE_INVALIDATED = BalConfig(config, "bal_hide_invalidated", True)
'https://we.bitcoin-after.life': { self.ALLOW_REPUSH = BalConfig(config, "bal_allow_repush", True)
"base_fee": 100000, self.FIRST_EXECUTION = BalConfig(config, "bal_first_execution", True)
"status": "New", self.WILLEXECUTORS = BalConfig(
"info":"Bitcoin After Life Will Executor", config,
"address":"bcrt1qa5cntu4hgadw8zd3n6sq2nzjy34sxdtd9u0gp7", "bal_willexecutors",
"selected":True {
} "mainnet": {
} "https://we.bitcoin-after.life": {
}) "base_fee": 100000,
self.WILL_SETTINGS = BalConfig(config, "bal_will_settings", { "status": "New",
'baltx_fees':100, "info": "Bitcoin After Life Will Executor",
'threshold':'180d', "address": "bcrt1qa5cntu4hgadw8zd3n6sq2nzjy34sxdtd9u0gp7",
'locktime':'1y', "selected": True,
}) }
}
},
)
self.WILL_SETTINGS = BalConfig(
config,
"bal_will_settings",
{
"baltx_fees": 100,
"threshold": "180d",
"locktime": "1y",
},
)
self._hide_invalidated = self.HIDE_INVALIDATED.get()
self._hide_replaced = self.HIDE_REPLACED.get()
self._hide_invalidated= self.HIDE_INVALIDATED.get() def resource_path(self, *parts):
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):
@@ -130,20 +138,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("baltx_fees", 1)) < 1:
print(will_settings.get('baltx_fees',1),1) will_settings["baltx_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 { return {"baltx_fees": 100, "threshold": "180d", "locktime": "1y"}
'baltx_fees':100,
'threshold':'180d',
'locktime':'1y'
}

View File

@@ -1,15 +1,14 @@
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 = '' DEFAULT_ICON_PATH = "icons"
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)

View File

@@ -1,61 +0,0 @@
#!env/bin/python3
#same as qt but for command line, useful if you are going to fix various wallet
#also easier to read the code
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)
if tx_fees:
json_wallet['will_settings']['baltx_fees']=json_wallet.get('will_settings',{}).get('tx_fees',default_fees)
del json_wallet['will_settings']['tx_fees']
return True
return False
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,
))
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]
storage=WalletStorage(path)
if storage.is_encrypted():
password = getpass.getpass("Enter wallet password: ", stream = None)
storage.decrypt(password)
data=storage.read()
json_wallet=json.loads(data)
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")

219
heirs.py
View File

@@ -1,12 +1,11 @@
# import json import datetime
import json
import math import math
# import datetime
# import urllib.request
# import urllib.parse
import random import random
import re import re
import threading import threading
import urllib.parse
import urllib.request
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Tuple from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Tuple
import dns import dns
@@ -28,8 +27,10 @@ from electrum.util import (
write_json_file, write_json_file,
) )
from .util import * from .util import Util
from .willexecutors import Willexecutors from .willexecutors import Willexecutors
from electrum.util import BitcoinException
from electrum import constants
if TYPE_CHECKING: if TYPE_CHECKING:
from .simple_config import SimpleConfig from .simple_config import SimpleConfig
@@ -42,6 +43,7 @@ HEIR_ADDRESS = 0
HEIR_AMOUNT = 1 HEIR_AMOUNT = 1
HEIR_LOCKTIME = 2 HEIR_LOCKTIME = 2
HEIR_REAL_AMOUNT = 3 HEIR_REAL_AMOUNT = 3
HEIR_DUST_AMOUNT = 4
TRANSACTION_LABEL = "inheritance transaction" TRANSACTION_LABEL = "inheritance transaction"
@@ -87,36 +89,43 @@ def prepare_transactions(locktimes, available_utxos, fees, wallet):
) )
total_used_utxos = [] total_used_utxos = []
txsout = {} txsout = {}
locktime, _ = get_lowest_locktimes(locktimes) locktime, _ = Util.get_lowest_locktimes(locktimes)
if not locktime: if not locktime:
_logger.info("prepare transactions, no locktime")
return return
locktime = locktime[0] locktime = locktime[0]
heirs = locktimes[locktime] heirs = locktimes[locktime]
vero = True true = True
while vero: while true:
vero = False true = 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: if len(heir) > HEIR_REAL_AMOUNT and not "DUST" in str(
if len(heir) > HEIR_REAL_AMOUNT: heir[HEIR_REAL_AMOUNT]
):
try:
real_amount = heir[HEIR_REAL_AMOUNT] real_amount = heir[HEIR_REAL_AMOUNT]
out_amount += real_amount
description += f"{name}\n"
paid_heirs[name] = heir
outputs.append( outputs.append(
PartialTxOutput.from_address_and_value( PartialTxOutput.from_address_and_value(
heir[HEIR_ADDRESS], real_amount heir[HEIR_ADDRESS], real_amount
) )
) )
else: out_amount += real_amount
description += f"{name}\n"
except BitcoinException as e:
_logger.info("exception decoding output {} - {}".format(type(e), e))
heir[HEIR_REAL_AMOUNT] = e
except Exception as e:
heir[HEIR_REAL_AMOUNT] = e
_logger.error(f"error preparing transactions: {e}")
pass pass
except Exception as e: paid_heirs[name] = heir
pass
in_amount = 0.0 in_amount = 0.0
used_utxos = [] used_utxos = []
@@ -125,24 +134,29 @@ def prepare_transactions(locktimes, available_utxos, fees, wallet):
value = utxo.value_sats() value = utxo.value_sats()
in_amount += value in_amount += value
used_utxos.append(utxo) used_utxos.append(utxo)
if in_amount > out_amount: if in_amount >= out_amount:
break break
except IndexError as e: except IndexError as e:
_logger.error(
f"error preparing transactions index error {e} {in_amount}, {out_amount}"
)
pass pass
if int(in_amount) < int(out_amount): if int(in_amount) < int(out_amount):
break _logger.error(
"error preparing transactions in_amount < out_amount ({} < {}) "
)
continue
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)
print(outputs)
tx = PartialTransaction.from_io( tx = PartialTransaction.from_io(
used_utxos, used_utxos,
outputs, outputs,
locktime=parse_locktime_string(locktime, wallet), locktime=Util.parse_locktime_string(locktime, wallet),
version=2, version=2,
) )
if len(description) > 0: if len(description) > 0:
@@ -263,9 +277,11 @@ def get_change_output(wallet, in_amount, out_amount, fee):
class Heirs(dict, Logger): class Heirs(dict, Logger):
def __init__(self, db: "WalletDB"):
def __init__(self, wallet):
Logger.__init__(self) Logger.__init__(self)
self.db = db self.db = wallet.db
self.wallet = wallet
d = self.db.get("heirs", {}) d = self.db.get("heirs", {})
try: try:
self.update(d) self.update(d)
@@ -300,10 +316,10 @@ class Heirs(dict, Logger):
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 = 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 or locktime <= from_locktime and a:
locktimes[int(locktime)] = None locktimes[int(locktime)] = None
return locktimes.keys() return list(locktimes.keys())
def check_locktime(self): def check_locktime(self):
return False return False
@@ -317,6 +333,8 @@ class Heirs(dict, Logger):
column = HEIR_AMOUNT column = HEIR_AMOUNT
if real: if real:
column = HEIR_REAL_AMOUNT column = HEIR_REAL_AMOUNT
if "DUST" in str(v[column]):
column = HEIR_DUST_AMOUNT
value = int( value = int(
math.floor( math.floor(
total_balance total_balance
@@ -327,6 +345,10 @@ class Heirs(dict, Logger):
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
else:
heir_list[key].insert(HEIR_REAL_AMOUNT, f"DUST: {value}")
heir_list[key].insert(HEIR_DUST_AMOUNT, value)
_logger.info(f"{key}, {value} is dust will be ignored")
except Exception as e: except Exception as e:
raise e raise e
@@ -346,25 +368,32 @@ class Heirs(dict, Logger):
fixed_amount = 0.0 fixed_amount = 0.0
percent_heirs = {} percent_heirs = {}
percent_amount = 0.0 percent_amount = 0.0
fixed_amount_with_dust =0.0
for key in self.keys(): for key in self.keys():
try: try:
cmp = parse_locktime_string(self[key][HEIR_LOCKTIME]) - from_locktime cmp = (
Util.parse_locktime_string(self[key][HEIR_LOCKTIME]) - from_locktime
)
if cmp <= 0: if cmp <= 0:
_logger.debug("cmp < 0 {} {} {} ".format(cmp, key, self[key][HEIR_LOCKTIME], from_locktime))
continue continue
if 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])))
fixed_amount_with_dust += heir_amount
fixed_heirs[key] = list(self[key])
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].insert(HEIR_REAL_AMOUNT, heir_amount) fixed_heirs[key].insert(HEIR_REAL_AMOUNT, heir_amount)
else: else:
pass fixed_heirs[key] = list(self[key])
fixed_heirs[key].insert(HEIR_REAL_AMOUNT, f"DUST: {heir_amount}")
fixed_heirs[key].insert(HEIR_DUST_AMOUNT, heir_amount)
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, fixed_amount_with_dust
def prepare_lists( def prepare_lists(
self, balance, total_fees, wallet, willexecutor=False, from_locktime=0 self, balance, total_fees, wallet, willexecutor=False, from_locktime=0
@@ -377,7 +406,7 @@ class Heirs(dict, Logger):
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(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
@@ -392,16 +421,18 @@ class Heirs(dict, Logger):
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}){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 = ( if newbalance<0:
raise WillExecutorFeeException(willexecutor)
a=list(self.fixed_percent_lists_amount(from_locktime, wallet.dust_threshold()))
fixed_heirs, fixed_amount, percent_heirs, percent_amount,fixed_amount_with_dust = (
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
@@ -422,19 +453,19 @@ class Heirs(dict, Logger):
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_with_dust, 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(), heir_list.items(),
key=lambda item: parse_locktime_string(item[1][HEIR_LOCKTIME], wallet), 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 = 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: else:
@@ -442,13 +473,14 @@ class Heirs(dict, Logger):
return locktimes, onlyfixed return locktimes, onlyfixed
def is_perc(self, key): def is_perc(self, key):
return 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:
_logger.info("while building transactions there was no heirs")
return return
balance = 0.0 balance = 0.0
len_utxo_set = 0 len_utxo_set = 0
@@ -464,6 +496,7 @@ class Heirs(dict, Logger):
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:
_logger.info("no usable utxos")
return return
j = -2 j = -2
willexecutorsitems = list(willexecutors.items()) willexecutorsitems = list(willexecutors.items())
@@ -487,7 +520,7 @@ class Heirs(dict, Logger):
break break
fees = {} fees = {}
i = 0 i = 0
while True: while i<10:
txs = {} txs = {}
redo = False redo = False
i += 1 i += 1
@@ -495,46 +528,55 @@ class Heirs(dict, Logger):
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(
balance, total_fees, wallet, willexecutor, from_locktime
)
try: try:
txs = prepare_transactions( locktimes, onlyfixed = self.prepare_lists(
locktimes, available_utxos[:], fees, wallet balance, total_fees, wallet, willexecutor, from_locktime
) )
if not txs: except WillExecutorFeeException as e:
return {} i=10
except Exception as e: continue
if locktimes:
try: try:
if "w!ll3x3c" in e.heirname: txs = prepare_transactions(
Willexecutors.is_selected(willexecutors[w], False) locktimes, available_utxos[:], fees, wallet
break )
except: if not txs:
raise e return {}
total_fees = 0 except Exception as e:
total_fees_real = 0 _logger.error(f"build transactions: error preparing transactions: {e}")
total_in = 0 try:
for txid, tx in txs.items(): if "w!ll3x3c" in e.heirname:
tx.willexecutor = willexecutor Willexecutors.is_selected(willexecutors[w], False)
fee = tx.estimated_size() * tx_fees break
txs[txid].tx_fees = tx_fees except:
total_fees += fee raise e
total_fees_real += tx.get_fee() total_fees = 0
total_in += tx.input_value() total_fees_real = 0
rfee = tx.input_value() - tx.output_value() total_in = 0
if rfee < fee or rfee > fee + wallet.dust_threshold(): for txid, tx in txs.items():
redo = True tx.willexecutor = willexecutor
oldfees = fees.get(tx.my_locktime, 0) fee = tx.estimated_size() * tx_fees
fees[tx.my_locktime] = fee txs[txid].tx_fees = tx_fees
total_fees += fee
total_fees_real += tx.get_fee()
total_in += tx.input_value()
rfee = tx.input_value() - tx.output_value()
if rfee < fee or rfee > fee + wallet.dust_threshold():
redo = True
oldfees = fees.get(tx.my_locktime, 0)
fees[tx.my_locktime] = fee
if balance - total_in > wallet.dust_threshold(): 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
else:
_logger.info(f"no locktimes for willexecutor {willexecutor} skipped")
break break
alltxs.update(txs) alltxs.update(txs)
return alltxs return alltxs
def get_transactions( def get_transactions(
@@ -632,13 +674,13 @@ class Heirs(dict, Logger):
return None return None
def validate_address(address): def validate_address(address):
if not bitcoin.is_address(address): if not bitcoin.is_address(address, net=constants.net):
raise NotAnAddress(f"not an address,{address}") raise NotAnAddress(f"not an address,{address}")
return address return address
def validate_amount(amount): def validate_amount(amount):
try: try:
famount = float(amount[:-1]) if is_perc(amount) else float(amount) famount = float(amount[:-1]) if Util.is_perc(amount) else float(amount)
if famount <= 0.00000001: if famount <= 0.00000001:
raise AmountNotValid(f"amount have to be positive {famount} < 0") raise AmountNotValid(f"amount have to be positive {famount} < 0")
except Exception as e: except Exception as e:
@@ -648,7 +690,7 @@ class Heirs(dict, Logger):
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 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}")
@@ -661,12 +703,14 @@ class Heirs(dict, Logger):
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, timestamp_to_check)
try: try:
Heirs.validate_heir(k, v) Heirs.validate_heir(k, v, timestamp_to_check)
except Exception as e: except Exception as e:
_logger.info(f"exception heir removed {e}")
data.pop(k) data.pop(k)
return data return data
@@ -685,3 +729,14 @@ class LocktimeNotValid(ValueError):
class HeirExpiredException(LocktimeNotValid): class HeirExpiredException(LocktimeNotValid):
pass pass
class HeirAmountIsDustException(Exception):
pass
class NoHeirsException(Exception):
pass
class WillExecutorFeeException(Exception):
def __init__(self,willexecutor):
self.willexecutor=willexecutor
def __str__(self):
return "WillExecutorFeeException: {} fee:{}".format(self.willexecutor['url'],self.willexecutor['base_fee'])

BIN
icons/confirmed.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

BIN
icons/status_connected.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

BIN
icons/unconfirmed.png Normal file

Binary file not shown.

After

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.2b", "description": "Provides free and decentralized inheritance support<br> Version: 0.2.4",
"author":"Svatantrya", "author":"Svatantrya",
"available_for": ["qt"], "available_for": ["qt"],
"icon":"icons/bal32x32.png" "icon":"icons/bal32x32.png"

753
qt.py

File diff suppressed because it is too large Load Diff

948
util.py
View File

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

77
wallet_util/bal_wallet_utils.py Executable file
View File

@@ -0,0 +1,77 @@
#!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,55 +1,59 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
#this script will help to fix tx_fees wallet bug
#also added an uninstall button to remove any bal data from wallet
#still very work in progress.
#
#have to be executed from electrum source code directory in example /home/user/projects/electrum/
#
import sys import sys
import os import os
import json import json
from PyQt6.QtWidgets import (QApplication, QMainWindow, QVBoxLayout, QHBoxLayout, from PyQt6.QtWidgets import (
QLabel, QLineEdit, QPushButton, QWidget, QFileDialog, QApplication,
QGroupBox, QTextEdit) QMainWindow,
QVBoxLayout,
QHBoxLayout,
QLabel,
QLineEdit,
QPushButton,
QWidget,
QFileDialog,
QGroupBox,
QTextEdit,
)
from PyQt6.QtCore import Qt from PyQt6.QtCore import Qt
from electrum.storage import WalletStorage from electrum.storage import WalletStorage
from electrum.util import MyEncoder from electrum.util import MyEncoder
from bal_wallet_utils import fix_will_settings_tx_fees, uninstall_bal, read_wallet, save
default_fees = 100
class WalletUtilityGUI(QMainWindow): class WalletUtilityGUI(QMainWindow):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.initUI() self.initUI()
def initUI(self): def initUI(self):
self.setWindowTitle('BAL Wallet Utility') self.setWindowTitle("BAL Wallet Utility")
self.setFixedSize(500, 400) self.setFixedSize(500, 400)
# Central widget # Central widget
central_widget = QWidget() central_widget = QWidget()
self.setCentralWidget(central_widget) self.setCentralWidget(central_widget)
# Main layout # Main layout
layout = QVBoxLayout(central_widget) layout = QVBoxLayout(central_widget)
# Wallet input group # Wallet input group
wallet_group = QGroupBox("Wallet Settings") wallet_group = QGroupBox("Wallet Settings")
wallet_layout = QVBoxLayout(wallet_group) wallet_layout = QVBoxLayout(wallet_group)
# Wallet path # Wallet path
wallet_path_layout = QHBoxLayout() wallet_path_layout = QHBoxLayout()
wallet_path_layout.addWidget(QLabel("Wallet Path:")) wallet_path_layout.addWidget(QLabel("Wallet Path:"))
self.wallet_path_edit = QLineEdit() self.wallet_path_edit = QLineEdit()
self.wallet_path_edit.setPlaceholderText("Select wallet path...") self.wallet_path_edit.setPlaceholderText("Select wallet path...")
wallet_path_layout.addWidget(self.wallet_path_edit) wallet_path_layout.addWidget(self.wallet_path_edit)
self.browse_btn = QPushButton("Browse...") self.browse_btn = QPushButton("Browse...")
self.browse_btn.clicked.connect(self.browse_wallet) self.browse_btn.clicked.connect(self.browse_wallet)
wallet_path_layout.addWidget(self.browse_btn) wallet_path_layout.addWidget(self.browse_btn)
wallet_layout.addLayout(wallet_path_layout) wallet_layout.addLayout(wallet_path_layout)
# Password # Password
password_layout = QHBoxLayout() password_layout = QHBoxLayout()
password_layout.addWidget(QLabel("Password:")) password_layout.addWidget(QLabel("Password:"))
@@ -57,157 +61,135 @@ class WalletUtilityGUI(QMainWindow):
self.password_edit.setEchoMode(QLineEdit.EchoMode.Password) self.password_edit.setEchoMode(QLineEdit.EchoMode.Password)
self.password_edit.setPlaceholderText("Enter password (if encrypted)") self.password_edit.setPlaceholderText("Enter password (if encrypted)")
password_layout.addWidget(self.password_edit) password_layout.addWidget(self.password_edit)
wallet_layout.addLayout(password_layout) wallet_layout.addLayout(password_layout)
layout.addWidget(wallet_group) layout.addWidget(wallet_group)
# Output area # Output area
output_group = QGroupBox("Output") output_group = QGroupBox("Output")
output_layout = QVBoxLayout(output_group) output_layout = QVBoxLayout(output_group)
self.output_text = QTextEdit() self.output_text = QTextEdit()
self.output_text.setReadOnly(True) self.output_text.setReadOnly(True)
output_layout.addWidget(self.output_text) output_layout.addWidget(self.output_text)
layout.addWidget(output_group) layout.addWidget(output_group)
# Action buttons # Action buttons
buttons_layout = QHBoxLayout() buttons_layout = QHBoxLayout()
self.fix_btn = QPushButton("Fix") self.fix_btn = QPushButton("Fix")
self.fix_btn.clicked.connect(self.fix_wallet) self.fix_btn.clicked.connect(self.fix_wallet)
self.fix_btn.setEnabled(False) self.fix_btn.setEnabled(False)
buttons_layout.addWidget(self.fix_btn) buttons_layout.addWidget(self.fix_btn)
self.uninstall_btn = QPushButton("Uninstall") self.uninstall_btn = QPushButton("Uninstall")
self.uninstall_btn.clicked.connect(self.uninstall_wallet) self.uninstall_btn.clicked.connect(self.uninstall_wallet)
self.uninstall_btn.setEnabled(False) self.uninstall_btn.setEnabled(False)
buttons_layout.addWidget(self.uninstall_btn) buttons_layout.addWidget(self.uninstall_btn)
layout.addLayout(buttons_layout) layout.addLayout(buttons_layout)
# Connections to enable buttons when path is entered # Connections to enable buttons when path is entered
self.wallet_path_edit.textChanged.connect(self.check_inputs) self.wallet_path_edit.textChanged.connect(self.check_inputs)
def browse_wallet(self): def browse_wallet(self):
file_path, _ = QFileDialog.getOpenFileName( file_path, _ = QFileDialog.getOpenFileName(
self, self, "Select Wallet", "*", "Electrum Wallet (*)"
"Select Wallet",
"",
"Electrum Wallet (*.dat)"
) )
if file_path: if file_path:
self.wallet_path_edit.setText(file_path) self.wallet_path_edit.setText(file_path)
def check_inputs(self): def check_inputs(self):
wallet_path = self.wallet_path_edit.text().strip() wallet_path = self.wallet_path_edit.text().strip()
has_path = bool(wallet_path) and os.path.exists(wallet_path) has_path = bool(wallet_path) and os.path.exists(wallet_path)
self.fix_btn.setEnabled(has_path) self.fix_btn.setEnabled(has_path)
self.uninstall_btn.setEnabled(has_path) self.uninstall_btn.setEnabled(has_path)
def log_message(self, message): def log_message(self, message):
self.output_text.append(message) self.output_text.append(message)
def fix_will_settings_tx_fees(self, json_wallet):
tx_fees = json_wallet.get('will_settings',{}).get('tx_fees',False)
if tx_fees:
json_wallet['will_settings']['baltx_fees'] = json_wallet.get('will_settings',{}).get('tx_fees', default_fees)
del json_wallet['will_settings']['tx_fees']
return True
return False
def uninstall_bal(self, json_wallet):
if 'will_settings' in json_wallet:
del json_wallet['will_settings']
if 'will' in json_wallet:
del json_wallet['will']
if 'heirs' in json_wallet:
del json_wallet['heirs']
return True
def save_wallet(self, json_wallet, storage):
try:
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,
))
return True
except Exception as e:
self.log_message(f"Save error: {str(e)}")
return False
def fix_wallet(self): def fix_wallet(self):
self.process_wallet('fix') self.process_wallet("fix")
def uninstall_wallet(self): def uninstall_wallet(self):
self.log_message("WARNING: This will remove all BAL settings. This operation cannot be undone.") self.log_message(
self.process_wallet('uninstall') "WARNING: This will remove all BAL settings. This operation cannot be undone."
)
self.process_wallet("uninstall")
def process_wallet(self, command): def process_wallet(self, command):
wallet_path = self.wallet_path_edit.text().strip() wallet_path = self.wallet_path_edit.text().strip()
password = self.password_edit.text() password = self.password_edit.text()
if not wallet_path: if not wallet_path:
self.log_message("ERROR: Please enter wallet path") self.log_message("ERROR: Please enter wallet path")
return return
if not os.path.exists(wallet_path): if not os.path.exists(wallet_path):
self.log_message("ERROR: Wallet not found") self.log_message("ERROR: Wallet not found")
return return
try: try:
self.log_message(f"Processing wallet: {wallet_path}") self.log_message(f"Processing wallet: {wallet_path}")
storage = WalletStorage(wallet_path) storage = WalletStorage(wallet_path)
# Decrypt if necessary # Decrypt if necessary
if storage.is_encrypted(): if storage.is_encrypted():
if not password: if not password:
self.log_message("ERROR: Wallet is encrypted, please enter password") self.log_message(
"ERROR: Wallet is encrypted, please enter password"
)
return return
try: try:
storage.decrypt(password) storage.decrypt(password)
self.log_message("Wallet decrypted successfully") self.log_message("Wallet decrypted successfully")
except Exception as e: except Exception as e:
self.log_message(f"ERROR: Wrong password: {str(e)}") self.log_message(f"ERROR: Wrong password: {str(e)}")
return return
# Read wallet # Read wallet
data = storage.read() data = storage.read()
json_wallet = json.loads(data) json_wallet = json.loads("[" + data + "]")[0]
have_to_save = False have_to_save = False
message = "" message = ""
if command == 'fix': if command == "fix":
have_to_save = self.fix_will_settings_tx_fees(json_wallet) have_to_save = fix_will_settings_tx_fees(json_wallet)
message = "Fix applied successfully" if have_to_save else "No fix needed" message = (
"Fix applied successfully" if have_to_save else "No fix needed"
elif command == 'uninstall': )
have_to_save = self.uninstall_bal(json_wallet)
message = "BAL uninstalled successfully" if have_to_save else "No BAL settings found to uninstall" 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: if have_to_save:
if self.save_wallet(json_wallet, storage): try:
save(json_wallet, storage)
self.log_message(f"SUCCESS: {message}") self.log_message(f"SUCCESS: {message}")
else: except Exception as e:
self.log_message("ERROR: Failed to save wallet") self.log_message(f"Save error: {str(e)}")
else: else:
self.log_message(f"INFO: {message}") self.log_message(f"INFO: {message}")
except Exception as e: except Exception as e:
error_msg = f"ERROR: Processing failed: {str(e)}" error_msg = f"ERROR: Processing failed: {str(e)}"
self.log_message(error_msg) self.log_message(error_msg)
def main(): def main():
app = QApplication(sys.argv) app = QApplication(sys.argv)
# Check if dependencies are available # Check if dependencies are available
try: try:
from electrum.storage import WalletStorage from electrum.storage import WalletStorage
@@ -215,11 +197,12 @@ def main():
except ImportError as e: except ImportError as e:
print(f"ERROR: Cannot import Electrum dependencies: {str(e)}") print(f"ERROR: Cannot import Electrum dependencies: {str(e)}")
return 1 return 1
window = WalletUtilityGUI() window = WalletUtilityGUI()
window.show() window.show()
return app.exec() return app.exec()
if __name__ == '__main__':
if __name__ == "__main__":
sys.exit(main()) sys.exit(main())

97
will.py
View File

@@ -19,7 +19,7 @@ from electrum.util import (
write_json_file, write_json_file,
) )
from .util import * from .util import Util
from .willexecutors import Willexecutors from .willexecutors import Willexecutors
MIN_LOCKTIME = 1 MIN_LOCKTIME = 1
@@ -28,7 +28,6 @@ _logger = get_logger(__name__)
class Will: class Will:
# return an array with the list of children
def get_children(will, willid): def get_children(will, willid):
out = [] out = []
for _id in will: for _id in will:
@@ -60,7 +59,7 @@ class Will:
for w in will: for w in will:
if w != wid and not tx.to_json() != will[w]["tx"].to_json(): if w != wid and not tx.to_json() != will[w]["tx"].to_json():
if will[w]["tx"].txid() != tx.txid(): if will[w]["tx"].txid() != tx.txid():
if cmp_txs(will[w]["tx"], tx): if Util.cmp_txs(will[w]["tx"], tx):
return will[w]["tx"] return will[w]["tx"]
return False return False
@@ -82,8 +81,6 @@ class Will:
for txin in will[wid].tx.inputs(): for txin in will[wid].tx.inputs():
txid = txin.prevout.txid.hex() txid = txin.prevout.txid.hex()
if txid in will: if txid in will:
# print(will[txid].tx.outputs())
# print(txin.prevout.out_idx)
change = will[txid].tx.outputs()[txin.prevout.out_idx] change = will[txid].tx.outputs()[txin.prevout.out_idx]
txin._trusted_value_sats = change.value txin._trusted_value_sats = change.value
try: try:
@@ -107,6 +104,7 @@ class Will:
will = willitems will = willitems
errors = {} errors = {}
for wid in will: for wid in will:
txid = will[wid].tx.txid() txid = will[wid].tx.txid()
if txid is None: if txid is None:
@@ -154,10 +152,20 @@ class Will:
inp._TxInput__value_sats = change.value inp._TxInput__value_sats = change.value
return inp return inp
"""
in questa situazione sono presenti due transazioni con id differente(quindi transazioni differenti)
per prima cosa controllo il locktime
se il locktime della nuova transazione e' maggiore del locktime della vecchia transazione, allora
confronto gli eredi, per locktime se corrispondono controllo i willexecutor
se hanno la stessa url ma le fee vecchie sono superiori alle fee nuove, allora anticipare.
"""
def check_anticipate(ow: "WillItem", nw: "WillItem"): def check_anticipate(ow: "WillItem", nw: "WillItem"):
anticipate = anticipate_locktime(ow.tx.locktime, days=1) anticipate = Util.anticipate_locktime(ow.tx.locktime, days=1)
if int(nw.tx.locktime) >= int(anticipate): if int(nw.tx.locktime) >= int(anticipate):
if cmp_heirs_by_values( if Util.cmp_heirs_by_values(
ow.heirs, nw.heirs, [0, 1], exclude_willexecutors=True ow.heirs, nw.heirs, [0, 1], exclude_willexecutors=True
): ):
if nw.we and ow.we: if nw.we and ow.we:
@@ -173,7 +181,7 @@ class Will:
ow.tx.locktime ow.tx.locktime
else: else:
if nw.we == ow.we: if nw.we == ow.we:
if not cmp_heirs_by_values(ow.heirs, nw.heirs, [0, 3]): if not Util.cmp_heirs_by_values(ow.heirs, nw.heirs, [0, 3]):
return anticipate return anticipate
else: else:
return ow.tx.locktime return ow.tx.locktime
@@ -257,7 +265,7 @@ class Will:
to_append = {} to_append = {}
new_inputs = Will.get_all_inputs(will, only_valid=True) new_inputs = Will.get_all_inputs(will, only_valid=True)
for nid, nwi in will.items(): for nid, nwi in will.items():
if nwi.search_anticipate(new_inputs) or nwi.search_anticipate(old_inputs): if nwi.search_anticipate(new_inputs):
if nid != nwi.tx.txid(): if nid != nwi.tx.txid():
redo = True redo = True
to_delete.append(nid) to_delete.append(nid)
@@ -267,6 +275,17 @@ class Will:
Will.change_input( Will.change_input(
will, nid, i, outputs[i], new_inputs, to_delete, to_append will, nid, i, outputs[i], new_inputs, to_delete, to_append
) )
if nwi.search_anticipate(old_inputs):
if nid != nwi.tx.txid():
redo = True
to_delete.append(nid)
to_append[nwi.tx.txid()] = nwi
outputs = nwi.tx.outputs()
for i in range(0, len(outputs)):
Will.change_input(
will, nid, i, outputs[i], new_inputs, to_delete, to_append
)
for w in to_delete: for w in to_delete:
try: try:
@@ -276,6 +295,7 @@ class Will:
for k, w in to_append.items(): for k, w in to_append.items():
will[k] = w will[k] = w
if redo: if redo:
Will.search_anticipate_rec(will, old_inputs) Will.search_anticipate_rec(will, old_inputs)
def update_will(old_will, new_will): def update_will(old_will, new_will):
@@ -285,7 +305,6 @@ class Will:
# check if the new input is already spent by other transaction # check if the new input is already spent by other transaction
# if it is use the same locktime, or anticipate. # if it is use the same locktime, or anticipate.
Will.search_anticipate_rec(new_will, all_old_inputs) Will.search_anticipate_rec(new_will, all_old_inputs)
other_inputs = Will.get_all_inputs(old_will, {}) other_inputs = Will.get_all_inputs(old_will, {})
try: try:
Will.normalize_will(new_will, others_inputs=other_inputs) Will.normalize_will(new_will, others_inputs=other_inputs)
@@ -337,12 +356,11 @@ class Will:
if utxo_str in prevout_to_spend: if utxo_str in prevout_to_spend:
balance += inputs[utxo_str][0][2].value_sats() balance += inputs[utxo_str][0][2].value_sats()
utxo_to_spend.append(utxo) utxo_to_spend.append(utxo)
if len(utxo_to_spend) > 0: if len(utxo_to_spend) > 0:
change_addresses = wallet.get_change_addresses_for_new_transaction() change_addresses = wallet.get_change_addresses_for_new_transaction()
out = PartialTxOutput.from_address_and_value(change_addresses[0], balance) out = PartialTxOutput.from_address_and_value(change_addresses[0], balance)
out.is_change = True out.is_change = True
locktime = get_current_height(wallet.network) locktime = Util.get_current_height(wallet.network)
tx = PartialTransaction.from_io( tx = PartialTransaction.from_io(
utxo_to_spend, [out], locktime=locktime, version=2 utxo_to_spend, [out], locktime=locktime, version=2
) )
@@ -375,7 +393,7 @@ class Will:
def search_rai(all_inputs, all_utxos, will, wallet): def search_rai(all_inputs, all_utxos, will, wallet):
will_only_valid = Will.only_valid_or_replaced_list(will) will_only_valid = Will.only_valid_or_replaced_list(will)
for inp, ws in all_inputs.items(): for inp, ws in all_inputs.items():
inutxo = in_utxo(inp, all_utxos) inutxo = Util.in_utxo(inp, all_utxos)
for w in ws: for w in ws:
wi = w[1] wi = w[1]
if ( if (
@@ -406,7 +424,7 @@ class Will:
pass pass
def utxos_strs(utxos): def utxos_strs(utxos):
return [utxo_to_str(u) for u in utxos] return [Util.utxo_to_str(u) for u in utxos]
def set_invalidate(wid, will=[]): def set_invalidate(wid, will=[]):
will[wid].set_status("INVALIDATED", True) will[wid].set_status("INVALIDATED", True)
@@ -416,18 +434,21 @@ class Will:
def check_tx_height(tx, wallet): def check_tx_height(tx, wallet):
info = wallet.get_tx_info(tx) info = wallet.get_tx_info(tx)
return info.tx_mined_status.height return info.tx_mined_status.height()
# check if transactions are stil valid tecnically valid # check if transactions are stil valid tecnically valid
def check_invalidated(willtree, utxos_list, wallet): def check_invalidated(willtree, utxos_list, wallet):
for wid, w in willtree.items(): for wid, w in willtree.items():
if not w.father: if (
not w.father
or willtree[w.father].get_status("CONFIRMED")
or willtree[w.father].get_status("PENDING")
):
for inp in w.tx.inputs(): for inp in w.tx.inputs():
inp_str = utxo_to_str(inp) inp_str = Util.utxo_to_str(inp)
if not inp_str in utxos_list: if not inp_str in utxos_list:
if wallet: if wallet:
height = Will.check_tx_height(w.tx, wallet) height = Will.check_tx_height(w.tx, wallet)
if height < 0: if height < 0:
Will.set_invalidate(wid, willtree) Will.set_invalidate(wid, willtree)
elif height == 0: elif height == 0:
@@ -449,7 +470,7 @@ class Will:
Will.reflect_to_children(wc) Will.reflect_to_children(wc)
def check_amounts(heirs, willexecutors, all_utxos, timestamp_to_check, dust): def check_amounts(heirs, willexecutors, all_utxos, timestamp_to_check, dust):
fixed_heirs, fixed_amount, perc_heirs, perc_amount = ( fixed_heirs, fixed_amount, perc_heirs, perc_amount,fixed_amount_with_dust = (
heirs.fixed_percent_lists_amount(timestamp_to_check, dust, reverse=True) heirs.fixed_percent_lists_amount(timestamp_to_check, dust, reverse=True)
) )
wallet_balance = 0 wallet_balance = 0
@@ -478,9 +499,7 @@ class Will:
Will.check_invalidated(will, utxos_list, wallet) Will.check_invalidated(will, utxos_list, wallet)
all_inputs = Will.get_all_inputs(will, only_valid=True) all_inputs = Will.get_all_inputs(will, only_valid=True)
all_inputs_min_locktime = Will.get_all_inputs_min_locktime(all_inputs) all_inputs_min_locktime = Will.get_all_inputs_min_locktime(all_inputs)
Will.check_will_expired( Will.check_will_expired(
all_inputs_min_locktime, block_to_check, timestamp_to_check all_inputs_min_locktime, block_to_check, timestamp_to_check
) )
@@ -502,7 +521,6 @@ class Will:
callback_not_valid_tx=None, callback_not_valid_tx=None,
): ):
Will.check_will(will, all_utxos, wallet, block_to_check, timestamp_to_check) Will.check_will(will, all_utxos, wallet, block_to_check, timestamp_to_check)
if heirs: if heirs:
if not Will.check_willexecutors_and_heirs( if not Will.check_willexecutors_and_heirs(
will, will,
@@ -520,7 +538,7 @@ class Will:
if all_inputs: if all_inputs:
for utxo in all_utxos: for utxo in all_utxos:
if utxo.value_sats() > 68 * tx_fees: if utxo.value_sats() > 68 * tx_fees:
if not in_utxo(utxo, all_inputs.keys()): if not Util.in_utxo(utxo, all_inputs.keys()):
_logger.info("utxo is not spent", utxo.to_json()) _logger.info("utxo is not spent", utxo.to_json())
_logger.debug(all_inputs.keys()) _logger.debug(all_inputs.keys())
raise NotCompleteWillException( raise NotCompleteWillException(
@@ -550,7 +568,7 @@ class Will:
def check_all_input_spent_are_in_wallet(): def check_all_input_spent_are_in_wallet():
_logger.info("check all input spent are in wallet or valid txs") _logger.info("check all input spent are in wallet or valid txs")
for inp, ws in all_inputs.items(): for inp, ws in all_inputs.items():
if not in_utxo(inp, all_utxos): if not Util.in_utxo(inp, all_utxos):
for w in ws: for w in ws:
if w[1].get_status("VALID"): if w[1].get_status("VALID"):
prevout_id = w[2].prevout.txid.hex() prevout_id = w[2].prevout.txid.hex()
@@ -591,22 +609,23 @@ class Will:
if not 'w!ll3x3c"' == wheir[:9]: if not 'w!ll3x3c"' == wheir[:9]:
their = will[wid].heirs[wheir] their = will[wid].heirs[wheir]
if heir := heirs.get(wheir, None): if heir := heirs.get(wheir, None):
if ( if (
heir[0] == their[0] heir[0] == their[0]
and heir[1] == their[1] and heir[1] == their[1]
and parse_locktime_string(heir[2]) and Util.parse_locktime_string(heir[2])
>= parse_locktime_string(their[2]) >= Util.parse_locktime_string(their[2])
): ):
count = heirs_found.get(wheir, 0) count = heirs_found.get(wheir, 0)
heirs_found[wheir] = count + 1 heirs_found[wheir] = count + 1
else: else:
_logger.debug( _logger.debug(
"heir not present transaction is not valid:", wid, w f"heir not present transaction is not valid:{wheir} {wid}, {w}"
) )
continue
if willexecutor := w.we: if willexecutor := w.we:
count = willexecutors_found.get(willexecutor["url"], 0) count = willexecutors_found.get(willexecutor["url"], 0)
if cmp_willexecutor( if Util.cmp_willexecutor(
willexecutor, willexecutors.get(willexecutor["url"], None) willexecutor, willexecutors.get(willexecutor["url"], None)
): ):
willexecutors_found[willexecutor["url"]] = count + 1 willexecutors_found[willexecutor["url"]] = count + 1
@@ -615,7 +634,8 @@ class Will:
no_willexecutor += 1 no_willexecutor += 1
count_heirs = 0 count_heirs = 0
for h in heirs: for h in heirs:
if parse_locktime_string(heirs[h][2]) >= check_date:
if Util.parse_locktime_string(heirs[h][2]) >= check_date:
count_heirs += 1 count_heirs += 1
if not h in heirs_found: if not h in heirs_found:
_logger.debug(f"heir: {h} not found") _logger.debug(f"heir: {h} not found")
@@ -624,7 +644,6 @@ class Will:
raise NoHeirsException("there are not valid heirs") raise NoHeirsException("there are not valid heirs")
if self_willexecutor and no_willexecutor == 0: if self_willexecutor and no_willexecutor == 0:
raise NoWillExecutorNotPresent("Backup tx") raise NoWillExecutorNotPresent("Backup tx")
for url, we in willexecutors.items(): for url, we in willexecutors.items():
if Willexecutors.is_selected(we): if Willexecutors.is_selected(we):
if not url in willexecutors_found: if not url in willexecutors_found:
@@ -656,15 +675,15 @@ class WillItem(Logger):
} }
def set_status(self, status, value=True): def set_status(self, status, value=True):
_logger.debug( # _logger.trace(
"set status {} - {} {} -> {}".format( # "set status {} - {} {} -> {}".format(
self._id, status, self.STATUS[status][1], value # self._id, status, self.STATUS[status][1], value
) # )
) # )
if self.STATUS[status][1] == bool(value): if self.STATUS[status][1] == bool(value):
return None return None
self.status += "." + ("NOT " if not value else "" + _(self.STATUS[status][0])) self.status += "." + (("NOT " if not value else "") + _(self.STATUS[status][0]))
self.STATUS[status][1] = bool(value) self.STATUS[status][1] = bool(value)
if value: if value:
if status in ["INVALIDATED", "REPLACED", "CONFIRMED", "PENDING"]: if status in ["INVALIDATED", "REPLACED", "CONFIRMED", "PENDING"]:
@@ -869,3 +888,7 @@ class PercAmountException(AmountException):
class FixedAmountException(AmountException): class FixedAmountException(AmountException):
pass pass
def test_check_invalidated():
Will.check_invalidated(will, utxos_list, wallet)

View File

@@ -10,14 +10,14 @@ from electrum.logging import get_logger
from electrum.network import Network from electrum.network import Network
from .bal import BalPlugin from .bal import BalPlugin
from .util import Util
# from .util import *
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
@@ -35,34 +35,36 @@ class Willexecutors:
continue continue
Willexecutors.initialize_willexecutor(willexecutors[w], w) Willexecutors.initialize_willexecutor(willexecutors[w], w)
for w in to_del: for w in to_del:
print("ERROR: WILLEXECUTOR TO DELETE:", w) _logger.error(
"error Willexecutor to delete type:{} ", type(willexecutor[w]), 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( sorted(
willexecutors.items(), key=lambda w: w[1].get("sort", 0), reverse=True willexecutors.items(), key=lambda w: w[1].get("sort", 0), reverse=True
@@ -119,7 +121,7 @@ class Willexecutors:
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 Exception("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()}"
@@ -170,7 +172,7 @@ class Willexecutors:
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['url']}: {willexecutor['txs']}")
if w := Willexecutors.send_request( if w := Willexecutors.send_request(
"post", "post",
willexecutor["url"] + "/" + constants.net.NET_NAME + "/pushtxs", willexecutor["url"] + "/" + constants.net.NET_NAME + "/pushtxs",