2 Commits

Author SHA1 Message Date
609db44c1a blacked 2026-02-09 12:01:06 -04:00
0df6786f50 dust bug fix 2026-02-09 11:58:07 -04:00
11 changed files with 975 additions and 1510 deletions

View File

@@ -1 +1 @@
0.2.8
0.2.4

93
bal.py
View File

@@ -1,14 +1,14 @@
import os
from datetime import date, datetime, timedelta
import platform
# import random
# import zipfile as zipfile_lib
from electrum import constants, 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
_logger = get_logger(__name__)
def get_will_settings(x):
# print(x)
pass
@@ -47,25 +47,18 @@ class BalConfig:
class BalPlugin(BasePlugin):
_version=None
__version__ = "0.2.8" #AUTOMATICALLY GENERATED DO NOT EDIT
default_app={
"Linux":"xdg-open",
"Window":"start",
"Darwin":"open"
}
chainname = constants.net.NET_NAME if constants.net.NET_NAME != "mainnet" else "bitcoin"
def version(self):
if not self._version:
LATEST_VERSION = "1"
KNOWN_VERSIONS = ("0", "1")
assert LATEST_VERSION in KNOWN_VERSIONS
def version():
try:
f = ""
with open("{}/VERSION".format(self.plugin_dir), "r") as fi:
f = str(fi.read())
self._version = f.strip()
except Exception as e:
_logger.error(f"failed to get version: {e}")
self._version="unknown"
return self._version
with open("VERSION", "r") as fi:
f = str(fi.readline())
return f
except:
return "unknown"
SIZE = (159, 97)
@@ -106,21 +99,11 @@ class BalPlugin(BasePlugin):
self.HIDE_INVALIDATED = BalConfig(config, "bal_hide_invalidated", True)
self.ALLOW_REPUSH = BalConfig(config, "bal_allow_repush", True)
self.FIRST_EXECUTION = BalConfig(config, "bal_first_execution", True)
self.WELIST_SERVER = BalConfig(config,"bal_welist_server","https://welist.bitcoin-after.life/")
self.WILLEXECUTORS = BalConfig(
config,
"bal_willexecutors",
{
"mainnet": {
"https://we.bitcoin-after.life": {
"base_fee": 100000,
"status": "New",
"info": "Bitcoin After Life Will Executor",
"address": "bc1qusymuetsz2psaqzqxv8qmzcy64d9meckj3lxxf",
"selected": True,
}
},
"testnet": {
"https://we.bitcoin-after.life": {
"base_fee": 100000,
"status": "New",
@@ -128,34 +111,19 @@ class BalPlugin(BasePlugin):
"address": "bcrt1qa5cntu4hgadw8zd3n6sq2nzjy34sxdtd9u0gp7",
"selected": True,
}
},
"testnet4": {
"https://we.bitcoin-after.life": {
"base_fee": 100000,
"status": "New",
"info": "Bitcoin After Life Will Executor",
"address": "bcrt1qa5cntu4hgadw8zd3n6sq2nzjy34sxdtd9u0gp7",
"selected": True,
}
},
"regtest": {
"https://we.bitcoin-after.life": {
"base_fee": 100000,
"status": "New",
"info": "Bitcoin After Life Will Executor",
"address": "bcrt1qa5cntu4hgadw8zd3n6sq2nzjy34sxdtd9u0gp7",
"selected": True,
}
},
},
)
self.WILL_SETTINGS = BalConfig(
config,
"bal_will_settings",
BalPlugin.default_will_settings(),
{
"baltx_fees": 100,
"threshold": "180d",
"locktime": "1y",
},
)
self.system = platform.system()
self.CALENDAR_APP = BalConfig(config,"bal_open_app",self.default_app[self.system])
self._hide_invalidated = self.HIDE_INVALIDATED.get()
self._hide_replaced = self.HIDE_REPLACED.get()
@@ -171,22 +139,13 @@ class BalPlugin(BasePlugin):
self.HIDE_REPLACED.set(self._hide_replaced)
def validate_will_settings(self, will_settings):
defaults=BalPlugin.default_will_settings()
if not will_settings:
will_settings=[]
if int(will_settings.get("baltx_fees", 0)) < 1:
will_settings["baltx_fees"] = defaults['baltx_fees']
if int(will_settings.get("baltx_fees", 1)) < 1:
will_settings["baltx_fees"] = 1
if not will_settings.get("threshold"):
will_settings["threshold"] = defaults['threshold']
will_settings["threshold"] = "180d"
if not will_settings.get("locktime"):
will_settings["locktime"] = defaults['locktime']
will_settings["locktime"] = "1y"
return will_settings
@staticmethod
def default_will_settings():
today = date.today()
dt = datetime(today.year, today.month, today.day, 0, 0, 0)
threshold =(dt + timedelta(days=180)).timestamp()
locktime =(dt + timedelta(days=365)).timestamp()
return {"baltx_fees": 100, "threshold": threshold, "locktime": locktime}
def default_will_settings(self):
return {"baltx_fees": 100, "threshold": "180d", "locktime": "1y"}

213
heirs.py
View File

@@ -1,40 +1,25 @@
# import datetime
# import json
import datetime
import json
import math
import random
import re
import threading
# import urllib.parse
# import urllib.request
from typing import (
TYPE_CHECKING,
Any,
Dict,
# List,
Optional,
# Sequence,
Tuple,
)
import urllib.parse
import urllib.request
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Tuple
import dns
from dns.exception import DNSException
from electrum import (
bitcoin,
constants,
# descriptor,
dnssec,
)
from electrum import bitcoin, constants, descriptor, dnssec
from electrum.logging import Logger, get_logger
from electrum.transaction import (
PartialTransaction,
PartialTxInput,
PartialTxOutput,
TxOutpoint,
# TxOutput,
TxOutput,
)
from electrum.util import (
BitcoinException,
bfh,
read_json_file,
to_string,
@@ -44,11 +29,12 @@ from electrum.util import (
from .util import Util
from .willexecutors import Willexecutors
from electrum.util import BitcoinException
from electrum import constants
if TYPE_CHECKING:
from .simple_config import SimpleConfig
# from .wallet_db import WalletDB
from .wallet_db import WalletDB
_logger = get_logger(__name__)
@@ -71,22 +57,28 @@ def reduce_outputs(in_amount, out_amount, fee, outputs):
output.value = math.floor((in_amount - fee) / out_amount * output.value)
def create_op_return_script(data_hex: str) -> bytes:
"""Crea scriptpubkey OP_RETURN in bytes"""
data = bytes.fromhex(data_hex)
"""
#TODO: put this method inside wallet.db to replace or complete get_locktime_for_new_transaction
def get_current_height(network:'Network'):
#if no network or not up to date, just set locktime to zero
if not network:
return 0
chain = network.blockchain()
if chain.is_tip_stale():
return 0
# figure out current block height
chain_height = chain.height() # learnt from all connected servers, SPV-checked
server_height = network.get_server_height() # height claimed by main server, unverified
# note: main server might be lagging (either is slow, is malicious, or there is an SPV-invisible-hard-fork)
# - if it's lagging too much, it is the network's job to switch away
if server_height < chain_height - 10:
# the diff is suspiciously large... give up and use something non-fingerprintable
return 0
# discourage "fee sniping"
height = min(chain_height, server_height)
return height
"""
if len(data) > 80:
raise ValueError("OP_RETURN data too big (max 80 bytes)")
# Costruzione manuale: OP_RETURN + push data
if len(data) <= 75:
# Formato più comune: OP_RETURN + 1-byte length + data
script = b'\x6a' + bytes([len(data)]) + data
else:
# Per dati più grandi (fino a 80) si usa OP_PUSHDATA1
script = b'\x6a\x4c' + bytes([len(data)]) + data
return script
def prepare_transactions(locktimes, available_utxos, fees, wallet):
available_utxos = sorted(
@@ -95,7 +87,7 @@ def prepare_transactions(locktimes, available_utxos, fees, wallet):
x.value_sats(), x.prevout.txid, x.prevout.out_idx
),
)
# total_used_utxos = []
total_used_utxos = []
txsout = {}
locktime, _ = Util.get_lowest_locktimes(locktimes)
if not locktime:
@@ -104,16 +96,16 @@ def prepare_transactions(locktimes, available_utxos, fees, wallet):
locktime = locktime[0]
heirs = locktimes[locktime]
true = True
while true:
true = False
vero = True
while vero:
vero = False
fee = fees.get(locktime, 0)
out_amount = fee
description = ""
outputs = []
paid_heirs = {}
for name, heir in heirs.items():
if len(heir) > HEIR_REAL_AMOUNT and "DUST" not in str(
if len(heir) > HEIR_REAL_AMOUNT and not "DUST" in str(
heir[HEIR_REAL_AMOUNT]
):
try:
@@ -126,12 +118,12 @@ def prepare_transactions(locktimes, available_utxos, fees, wallet):
out_amount += real_amount
description += f"{name}\n"
except BitcoinException as e:
_logger.info("exception decoding output {} - {}".format(type(e), 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}")
_logger.info(f"error preparing transactions {e}")
pass
paid_heirs[name] = heir
@@ -146,28 +138,21 @@ def prepare_transactions(locktimes, available_utxos, fees, wallet):
break
except IndexError as e:
_logger.error(
_logger.info(
f"error preparing transactions index error {e} {in_amount}, {out_amount}"
)
pass
if int(in_amount) < int(out_amount):
_logger.error(
_logger.info(
"error preparing transactions in_amount < out_amount ({} < {}) "
)
continue
break
heirsvalue = out_amount
change = get_change_output(wallet, in_amount, out_amount, fee)
if change:
outputs.append(change)
for i in range(0, 100):
random.shuffle(outputs)
#op_return_text = "Hello Bal!"
## Convert text to hex
#op_return_hex = op_return_text.encode('utf-8').hex()
#op_return_script = create_op_return_script(op_return_hex)
#outputs.append(PartialTxOutput(value=0, scriptpubkey=op_return_script))
tx = PartialTransaction.from_io(
used_utxos,
outputs,
@@ -183,7 +168,7 @@ def prepare_transactions(locktimes, available_utxos, fees, wallet):
tx.remove_signatures()
txid = tx.txid()
if txid is None:
raise Exception(f"txid is none: {tx}")
raise Exception("txid is none", tx)
tx.heirs = paid_heirs
tx.my_locktime = locktime
@@ -215,7 +200,7 @@ def get_utxos_from_inputs(tx_inputs, tx, utxos):
# TODO calculate de minimum inputs to be invalidated
def invalidate_inheritance_transactions(wallet):
# listids = []
listids = []
utxos = {}
dtxs = {}
for k, v in wallet.get_all_labels().items():
@@ -244,7 +229,7 @@ def invalidate_inheritance_transactions(wallet):
for key, value in utxos:
for tx in value["txs"]:
txid = tx.txid()
if txid not in invalidated:
if not txid in invalidated:
invalidated.append(tx.txid())
remaining[key] = value
@@ -252,10 +237,10 @@ def invalidate_inheritance_transactions(wallet):
def print_transaction(heirs, tx, locktimes, tx_fees):
jtx = tx.to_json()
print(f"TX: {tx.txid()}\t-\tLocktime: {jtx['locktime']}")
print("---")
print(f"---")
for inp in jtx["inputs"]:
print(f"{inp['address']}: {inp['value_sats']}")
print("---")
print(f"---")
for out in jtx["outputs"]:
heirname = ""
for key in heirs.keys():
@@ -277,7 +262,7 @@ def print_transaction(heirs, tx, locktimes, tx_fees):
print()
try:
print(tx.serialize_to_network())
except Exception:
except:
print("impossible to serialize")
print()
@@ -300,7 +285,7 @@ class Heirs(dict, Logger):
d = self.db.get("heirs", {})
try:
self.update(d)
except Exception:
except e as Exception:
return
def invalidate_transactions(self, wallet):
@@ -334,7 +319,7 @@ class Heirs(dict, Logger):
locktime = Util.parse_locktime_string(self[key][HEIR_LOCKTIME])
if locktime > from_locktime and not a or locktime <= from_locktime and a:
locktimes[int(locktime)] = None
return list(locktimes.keys())
return locktimes.keys()
def check_locktime(self):
return False
@@ -348,8 +333,6 @@ class Heirs(dict, Logger):
column = HEIR_AMOUNT
if real:
column = HEIR_REAL_AMOUNT
if "DUST" in str(v[column]):
column = HEIR_DUST_AMOUNT
value = int(
math.floor(
total_balance
@@ -372,10 +355,10 @@ class Heirs(dict, Logger):
def amount_to_float(self, amount):
try:
return float(amount)
except Exception:
except:
try:
return float(amount[:-1])
except Exception:
except:
return 0.0
def fixed_percent_lists_amount(self, from_locktime, dust_threshold, reverse=False):
@@ -383,50 +366,31 @@ class Heirs(dict, Logger):
fixed_amount = 0.0
percent_heirs = {}
percent_amount = 0.0
fixed_amount_with_dust = 0.0
for key in self.keys():
try:
cmp = (
Util.parse_locktime_string(self[key][HEIR_LOCKTIME]) - from_locktime
)
if cmp <= 0:
_logger.debug(
"cmp < 0 {} {} {} {}".format(
cmp, key, self[key][HEIR_LOCKTIME], from_locktime
)
)
continue
if Util.is_perc(self[key][HEIR_AMOUNT]):
percent_amount += float(self[key][HEIR_AMOUNT][:-1])
percent_heirs[key] = list(self[key])
else:
heir_amount = int(math.floor(float(self[key][HEIR_AMOUNT])))
fixed_amount_with_dust += heir_amount
fixed_heirs[key] = list(self[key])
if heir_amount > dust_threshold:
fixed_amount += heir_amount
fixed_heirs[key] = list(self[key])
fixed_heirs[key].insert(HEIR_REAL_AMOUNT, heir_amount)
else:
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)
pass
except Exception as e:
_logger.error(e)
return (
fixed_heirs,
fixed_amount,
percent_heirs,
percent_amount,
fixed_amount_with_dust,
)
return fixed_heirs, fixed_amount, percent_heirs, percent_amount
def prepare_lists(
self, balance, total_fees, wallet, willexecutor=False, from_locktime=0
):
if balance<total_fees or balance < wallet.dust_threshold():
raise BalanceTooLowException(balance,wallet.dust_threshold(),total_fees)
willexecutors_amount = 0
willexecutors = {}
heir_list = {}
@@ -447,7 +411,7 @@ class Heirs(dict, Logger):
willexecutors[
'w!ll3x3c"' + willexecutor["url"] + '"' + str(locktime)
] = h
except Exception:
except Exception as e:
return [], False
else:
_logger.error(
@@ -455,15 +419,9 @@ class Heirs(dict, Logger):
),
heir_list.update(willexecutors)
newbalance -= willexecutors_amount
if newbalance < 0:
raise WillExecutorFeeException(willexecutor)
(
fixed_heirs,
fixed_amount,
percent_heirs,
percent_amount,
fixed_amount_with_dust,
) = self.fixed_percent_lists_amount(from_locktime, wallet.dust_threshold())
fixed_heirs, fixed_amount, percent_heirs, percent_amount = (
self.fixed_percent_lists_amount(from_locktime, wallet.dust_threshold())
)
if fixed_amount > newbalance:
fixed_amount = self.normalize_perc(
fixed_heirs, newbalance, fixed_amount, wallet
@@ -473,16 +431,18 @@ class Heirs(dict, Logger):
heir_list.update(fixed_heirs)
newbalance -= fixed_amount
if newbalance > 0:
perc_amount = self.normalize_perc(
percent_heirs, newbalance, percent_amount, wallet
)
newbalance -= perc_amount
heir_list.update(percent_heirs)
if newbalance > 0:
newbalance += fixed_amount
fixed_amount = self.normalize_perc(
fixed_heirs, newbalance, fixed_amount_with_dust, wallet, real=True
fixed_heirs, newbalance, fixed_amount, wallet, real=True
)
newbalance -= fixed_amount
heir_list.update(fixed_heirs)
@@ -495,7 +455,7 @@ class Heirs(dict, Logger):
locktimes = {}
for key, value in heir_list:
locktime = Util.parse_locktime_string(value[HEIR_LOCKTIME])
if locktime not in locktimes:
if not locktime in locktimes:
locktimes[locktime] = {key: value}
else:
locktimes[locktime][key] = value
@@ -537,7 +497,7 @@ class Heirs(dict, Logger):
break
elif 0 <= j:
url, willexecutor = willexecutorsitems[j]
if not Willexecutors.is_selected(willexecutor) or willexecutor["base_fee"] < wallet.dust_threshold():
if not Willexecutors.is_selected(willexecutor):
continue
else:
willexecutor["url"] = url
@@ -549,22 +509,17 @@ class Heirs(dict, Logger):
break
fees = {}
i = 0
while i < 10:
while True:
txs = {}
redo = False
i += 1
total_fees = 0
for fee in fees:
total_fees += int(fees[fee])
# newbalance = balance
try:
newbalance = balance
locktimes, onlyfixed = self.prepare_lists(
balance, total_fees, wallet, willexecutor, from_locktime
)
except WillExecutorFeeException:
i = 10
continue
if locktimes:
try:
txs = prepare_transactions(
locktimes, available_utxos[:], fees, wallet
@@ -572,16 +527,12 @@ class Heirs(dict, Logger):
if not txs:
return {}
except Exception as e:
_logger.error(
f"build transactions: error preparing transactions: {e}"
)
_logger.info(f"error preparing transactions{e}")
try:
if "w!ll3x3c" in e.heirname:
Willexecutors.is_selected(
e.heirname[len("w!ll3x3c") :], False
)
Willexecutors.is_selected(willexecutors[w], False)
break
except Exception:
except:
raise e
total_fees = 0
total_fees_real = 0
@@ -596,7 +547,7 @@ class Heirs(dict, Logger):
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)
oldfees = fees.get(tx.my_locktime, 0)
fees[tx.my_locktime] = fee
if balance - total_in > wallet.dust_threshold():
@@ -605,11 +556,6 @@ class Heirs(dict, Logger):
break
if i >= 10:
break
else:
_logger.info(
f"no locktimes for willexecutor {willexecutor} skipped"
)
break
alltxs.update(txs)
return alltxs
@@ -768,24 +714,3 @@ class HeirExpiredException(LocktimeNotValid):
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"]
)
class BalanceTooLowException(Exception):
def __init__(self,balance, dust_threshold, fees):
self.balance=balance
self.dust_threshold = dust_threshold
self.fees = fees
def __str__(self):
return f"Balance too low, balance: {self.balance}, dust threshold: {self.dust_threshold}, fees: {self.fees}"

View File

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

1350
qt.py

File diff suppressed because it is too large Load Diff

161
util.py
View File

@@ -1,13 +1,17 @@
import bisect
import urllib.parse
import urllib.request
from datetime import datetime, timedelta
from electrum.gui.qt.util import getSaveFileName
from electrum.i18n import _
from electrum.transaction import PartialTxOutput
from electrum.util import FileExportFailed, FileImportFailed, write_json_file
LOCKTIME_THRESHOLD = 500000000
class Util:
@staticmethod
def locktime_to_str(locktime):
try:
locktime = int(locktime)
@@ -15,29 +19,27 @@ class Util:
dt = datetime.fromtimestamp(locktime).isoformat()
return dt
except Exception:
except Exception as e:
pass
return str(locktime)
@staticmethod
def str_to_locktime(locktime):
try:
if locktime[-1] in ("y", "d", "b"):
return locktime
else:
return int(locktime)
except Exception:
except Exception as e:
pass
dt_object = datetime.fromisoformat(locktime)
timestamp = dt_object.timestamp()
return int(timestamp)
@staticmethod
def parse_locktime_string(locktime, w=None):
try:
return int(locktime)
except Exception:
except Exception as e:
pass
try:
now = datetime.now()
@@ -56,11 +58,10 @@ class Util:
height = Util.get_current_height(w.network)
locktime += int(height)
return int(locktime)
except Exception:
except Exception as e:
pass
return 0
@staticmethod
def int_locktime(seconds=0, minutes=0, hours=0, days=0, blocks=0):
return int(
seconds
@@ -70,53 +71,45 @@ class Util:
+ blocks * 600
)
@staticmethod
def encode_amount(amount, decimal_point):
if Util.is_perc(amount):
return amount
else:
try:
return int(float(amount) * pow(10, decimal_point))
except Exception:
except:
return 0
@staticmethod
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 Exception:
return str(amount)
num = 8 - decimal_point
basestr = "{{:0{}.{}f}}".format(num, num)
return "{:08.8f}".format(float(amount) / pow(10, decimal_point))
@staticmethod
def is_perc(value):
try:
return value[-1] == "%"
except Exception:
except:
return False
@staticmethod
def cmp_array(heira, heirb):
try:
if len(heira) != len(heirb):
if not len(heira) == len(heirb):
return False
for h in range(0, len(heira)):
if heira[h] != heirb[h]:
if not heira[h] == heirb[h]:
return False
return True
except Exception:
except:
return False
@staticmethod
def cmp_heir(heira, heirb):
if heira[0] == heirb[0] and heira[1] == heirb[1]:
return True
return False
@staticmethod
def cmp_willexecutor(willexecutora, willexecutorb):
if willexecutora == willexecutorb:
return True
@@ -127,11 +120,10 @@ class Util:
and willexecutora["base_fee"] == willexecutorb["base_fee"]
):
return True
except Exception:
except:
return False
return False
@staticmethod
def search_heir_by_values(heirs, heir, values):
for h, v in heirs.items():
found = False
@@ -143,20 +135,18 @@ class Util:
return h
return False
@staticmethod
def cmp_heir_by_values(heira, heirb, values):
for v in values:
if heira[v] != heirb[v]:
return False
return True
@staticmethod
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
exclude_willexecutors and not 'w!ll3x3c"' in heira
) or not exclude_willexecutors:
found = False
for heirb in heirsb:
@@ -175,7 +165,6 @@ class Util:
else:
return True
@staticmethod
def cmp_heirs(
heirsa,
heirsb,
@@ -184,8 +173,8 @@ class Util:
):
try:
for heir in heirsa:
if 'w!ll3x3c"' not in heir:
if heir not in heirsb or not cmp_function(
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]):
@@ -198,7 +187,6 @@ class Util:
raise e
return False
@staticmethod
def cmp_inputs(inputsa, inputsb):
if len(inputsa) != len(inputsb):
return False
@@ -207,7 +195,6 @@ class Util:
return False
return True
@staticmethod
def cmp_outputs(outputsa, outputsb, willexecutor_output=None):
if len(outputsa) != len(outputsb):
return False
@@ -217,7 +204,6 @@ class Util:
return False
return True
@staticmethod
def cmp_txs(txa, txb):
if not Util.cmp_inputs(txa.inputs(), txb.inputs()):
return False
@@ -225,10 +211,9 @@ class Util:
return False
return True
@staticmethod
def get_value_amount(txa, txb):
outputsa = txa.outputs()
# outputsb = txb.outputs()
outputsb = txb.outputs()
value_amount = 0
for outa in outputsa:
@@ -244,7 +229,6 @@ class Util:
return value_amount
@staticmethod
def chk_locktime(timestamp_to_check, block_height_to_check, locktime):
# TODO BUG: WHAT HAPPEN AT THRESHOLD?
locktime = int(locktime)
@@ -255,7 +239,6 @@ class Util:
else:
return False
@staticmethod
def anticipate_locktime(locktime, blocks=0, hours=0, days=0):
locktime = int(locktime)
out = 0
@@ -272,62 +255,54 @@ class Util:
out = 1
return out
@staticmethod
def cmp_locktime(locktimea, locktimeb):
if locktimea == locktimeb:
return 0
strlocktimea = str(locktimea)
strlocktime = str(locktimea)
strlocktimeb = str(locktimeb)
# intlocktimea = Util.str_to_locktime(strlocktimea)
# intlocktimeb = Util.str_to_locktime(strlocktimeb)
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)
@staticmethod
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
@staticmethod
def get_locktimes(will):
locktimes = {}
for txid, willitem in will.items():
locktimes[willitem["tx"].locktime] = True
return locktimes.keys()
@staticmethod
def get_lowest_locktimes(locktimes):
sorted_timestamp = []
sorted_block = []
for locktime in locktimes:
locktime = Util.parse_locktime_string(locktime)
if locktime < LOCKTIME_THRESHOLD:
bisect.insort(sorted_block, locktime)
for l in locktimes:
l = Util.parse_locktime_string(l)
if l < LOCKTIME_THRESHOLD:
bisect.insort(sorted_block, l)
else:
bisect.insort(sorted_timestamp, locktime)
bisect.insort(sorted_timestamp, l)
return sorted(sorted_timestamp), sorted(sorted_block)
@staticmethod
def get_lowest_locktimes_from_will(will):
return Util.get_lowest_locktimes(Util.get_locktimes(will))
@staticmethod
def search_willtx_per_io(will, tx):
for wid, w in will.items():
if Util.cmp_txs(w["tx"], tx["tx"]):
return wid, w
return None, None
@staticmethod
def invalidate_will(will):
raise Exception("not implemented")
@staticmethod
def get_will_spent_utxos(will):
utxos = []
for txid, willitem in will.items():
@@ -335,19 +310,17 @@ class Util:
return utxos
@staticmethod
def utxo_to_str(utxo):
try:
return utxo.to_str()
except Exception:
except Exception as e:
pass
try:
return utxo.prevout.to_str()
except Exception:
except Exception as e:
pass
return str(utxo)
@staticmethod
def cmp_utxo(utxoa, utxob):
utxoa = Util.utxo_to_str(utxoa)
utxob = Util.utxo_to_str(utxob)
@@ -356,25 +329,21 @@ class Util:
else:
return False
@staticmethod
def in_utxo(utxo, utxos):
for s_u in utxos:
if Util.cmp_utxo(s_u, utxo):
return True
return False
@staticmethod
def txid_in_utxo(txid, utxos):
for s_u in utxos:
if s_u.prevout.txid == txid:
return True
return False
@staticmethod
def cmp_output(outputa, outputb):
return outputa.address == outputb.address and outputa.value == outputb.value
@staticmethod
def in_output(output, outputs):
for s_o in outputs:
if Util.cmp_output(s_o, output):
@@ -386,7 +355,6 @@ class Util:
# return true false same amount different address
# return false false different amount, different address not found
@staticmethod
def din_output(out, outputs):
same_amount = []
for s_o in outputs:
@@ -402,7 +370,6 @@ class Util:
else:
return False, False
@staticmethod
def get_change_output(wallet, in_amount, out_amount, fee):
change_amount = int(in_amount - out_amount - fee)
if change_amount > wallet.dust_threshold():
@@ -413,8 +380,7 @@ class Util:
out.is_change = True
return out
@staticmethod
def get_current_height(network):
def get_current_height(network: "Network"):
# if no network or not up to date, just set locktime to zero
if not network:
return 0
@@ -435,42 +401,44 @@ class Util:
height = min(chain_height, server_height)
return height
@staticmethod
def print_var(var, name="", veryverbose=False):
print(f"---{name}---")
if var is not None:
if not var is None:
try:
print("doc:", doc(var))
except:
pass
try:
print("str:", str(var))
except Exception:
except:
pass
try:
print("repr", repr(var))
except Exception:
except:
pass
try:
print("dict", dict(var))
except Exception:
except:
pass
try:
print("dir", dir(var))
except Exception:
except:
pass
try:
print("type", type(var))
except Exception:
except:
pass
try:
print("to_json", var.to_json())
except Exception:
except:
pass
try:
print("__slotnames__", var.__slotnames__)
except Exception:
except:
pass
print(f"---end {name}---")
@staticmethod
def print_utxo(utxo, name=""):
print(f"---utxo-{name}---")
Util.print_var(utxo, name)
@@ -482,20 +450,36 @@ class Util:
print("_TxInput__value_sats:", utxo._TxInput__value_sats)
print(f"---utxo-end {name}---")
@staticmethod
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))
)
@staticmethod
def copy(dicto, dictfrom):
for k, v in dictfrom.items():
dicto[k] = v
@staticmethod
def fix_will_settings_tx_fees(will_settings):
tx_fees = will_settings.get("tx_fees", False)
have_to_update = False
@@ -505,7 +489,6 @@ class Util:
have_to_update = True
return have_to_update
@staticmethod
def fix_will_tx_fees(will):
have_to_update = False
for txid, willitem in will.items():
@@ -515,19 +498,3 @@ class Util:
del will[txid]["tx_fees"]
have_to_update = True
return have_to_update
@staticmethod
def text_to_hex(text: str) -> str:
"""Convert text to hexadecimal string"""
hex_string = text.encode('utf-8').hex()
return hex_string
@staticmethod
def hex_to_text(hex_string: str) -> str:
"""Convert hexadecimal string back to text (for verification)"""
try:
return bytes.fromhex(hex_string).decode('utf-8')
except Exception:
return "Error: Invalid hex string"

View File

@@ -1,50 +0,0 @@
## README
### Overview
This tool provides two entry points: a CLI script (bal_wallet_utils.py) and a Qt GUI script (bal_wallet_utils_qt.py) that operate against an Electrum source tree.
### Installation / Preparation
1. Copy both files into the Electrum project root (the folder that contains the Electrum source package):
- bal_wallet_utils.py
- bal_wallet_utils_qt.py
2. Activate the Electrum Python environment (the virtualenv used to run Electrum). Example (PowerShell, adjust path to your venv):
```
.\env\Scripts\Activate.ps1
```
or (cmd):
```
env\Scripts\activate.bat
```
### Running
- CLI version:
```
python bal_wallet_utils.py
```
- Qt GUI version:
```
python bal_wallet_utils_qt.py
```
### Building a Windows executable with PyInstaller
From the project root (with the Electrum environment active), you can build the Qt executable using PyInstaller. Example command (adjust the paths if your environment path differs):
```
pyinstaller.exe --onefile --noconsole --add-data "electrum\currencies.json;electrum" --add-data "electrum\bip39_wallet_formats.json;electrum" --add-data "electrum\lnwire\peer_wire.csv;electrum\lnwire" --add-data "electrum\lnwire\onion_wire.csv;electrum\lnwire" --add-binary "env/Lib/site-packages\electrum_ecc\libsecp256k1-6.dll;electrum_ecc" bal_wallet_utils_qt.py
```
Notes:
- Run the command from the project root so relative paths resolve correctly.
- On Windows the --add-data and --add-binary arguments use ";" to separate source and destination.
- If electrum expects additional data files or native DLLs, include them with additional --add-data / --add-binary flags.
- For debugging include --onedir first to inspect the created folder before using --onefile.
### Troubleshooting
- If PyInstaller is not found, run it via Python:
```
python -m PyInstaller <same arguments>
```
- If the frozen exe fails because DLLs or JSON files are missing, add those files explicitly with --add-data or --add-binary.
- Test the build on a clean Windows VM to ensure all runtime dependencies are included.
License and attribution: include your preferred license or attribution details here.

View File

@@ -1,11 +1,10 @@
#!env/bin/python3
import getpass
import json
import os
import sys
from electrum.storage import WalletStorage
from electrum.util import MyEncoder
import json
import sys
import getpass
import os
default_fees = 100
@@ -27,11 +26,8 @@ def fix_will_settings_tx_fees(json_wallet):
def uninstall_bal(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
@@ -51,12 +47,12 @@ def save(json_wallet, storage):
def read_wallet(path, password=False):
storage = WalletStorage(path)
if storage.is_encrypted():
if not password:
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, storage
return json_wallet
if __name__ == "__main__":
@@ -69,7 +65,7 @@ if __name__ == "__main__":
exit(1)
command = sys.argv[1]
path = sys.argv[2]
json_wallet, storage = read_wallet(path)
json_wallet = read_wallet(path)
have_to_save = False
if command == "fix":
have_to_save = fix_will_settings_tx_fees(json_wallet)

View File

@@ -1,31 +1,32 @@
#!/usr/bin/env python3
import json
import os
import sys
from bal_wallet_utils import fix_will_settings_tx_fees, save, uninstall_bal
from electrum.storage import WalletStorage
import os
import json
from PyQt6.QtWidgets import (
QApplication,
QFileDialog,
QGroupBox,
QMainWindow,
QVBoxLayout,
QHBoxLayout,
QLabel,
QLineEdit,
QMainWindow,
QPushButton,
QTextEdit,
QVBoxLayout,
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.init_ui()
self.initUI()
def init_ui(self):
def initUI(self):
self.setWindowTitle("BAL Wallet Utility")
self.setFixedSize(500, 400)
@@ -189,6 +190,14 @@ class WalletUtilityGUI(QMainWindow):
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()

177
will.py
View File

@@ -12,7 +12,11 @@ from electrum.transaction import (
tx_from_any,
)
from electrum.util import (
FileImportFailed,
bfh,
decimal_point_to_base_unit_name,
read_json_file,
write_json_file,
)
from .util import Util
@@ -24,7 +28,6 @@ _logger = get_logger(__name__)
class Will:
@staticmethod
def get_children(will, willid):
out = []
for _id in will:
@@ -36,7 +39,6 @@ class Will:
return out
# build a tree with parent transactions
@staticmethod
def add_willtree(will):
for willid in will:
will[willid].children = Will.get_children(will, willid)
@@ -45,17 +47,14 @@ class Will:
will[child[0]].father = willid
# return a list of will sorted by locktime
@staticmethod
def get_sorted_will(will):
return sorted(will.items(), key=lambda x: x[1]["tx"].locktime)
@staticmethod
def only_valid(will):
for k, v in will.items():
if v.get_status("VALID"):
yield k
@staticmethod
def search_equal_tx(will, tx, wid):
for w in will:
if w != wid and not tx.to_json() != will[w]["tx"].to_json():
@@ -64,7 +63,6 @@ class Will:
return will[w]["tx"]
return False
@staticmethod
def get_tx_from_any(x):
try:
a = str(x)
@@ -75,7 +73,6 @@ class Will:
return x
@staticmethod
def add_info_from_will(will, wid, wallet):
if isinstance(will[wid].tx, str):
will[wid].tx = Will.get_tx_from_any(will[wid].tx)
@@ -88,7 +85,7 @@ class Will:
txin._trusted_value_sats = change.value
try:
txin.script_descriptor = change.script_descriptor
except Exception:
except:
pass
txin.is_mine = True
txin._TxInput__address = change.address
@@ -96,9 +93,7 @@ class Will:
txin._TxInput__value_sats = change.value
txin._trusted_value_sats = change.value
@staticmethod
def normalize_will(will, wallet=None, others_inputs=None):
others_input = others_inputs if others_inputs is not None else {}
def normalize_will(will, wallet=None, others_inputs={}):
to_delete = []
to_add = {}
# add info from wallet
@@ -137,8 +132,8 @@ class Will:
to_delete.append(wid)
to_add[ow.tx.txid()] = ow.to_dict()
# for eid, err in errors.items():
# new_txid = err.tx.txid()
for eid, err in errors.items():
new_txid = err.tx.txid()
for k, w in to_add.items():
will[k] = w
@@ -147,7 +142,6 @@ class Will:
if wid in will:
del will[wid]
@staticmethod
def new_input(txid, idx, change):
prevout = TxOutpoint(txid=bfh(txid), out_idx=idx)
inp = PartialTxInput(prevout=prevout)
@@ -158,7 +152,16 @@ class Will:
inp._TxInput__value_sats = change.value
return inp
@staticmethod
"""
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"):
anticipate = Util.anticipate_locktime(ow.tx.locktime, days=1)
if int(nw.tx.locktime) >= int(anticipate):
@@ -188,7 +191,6 @@ class Will:
return anticipate
return 4294967295 + 1
@staticmethod
def change_input(will, otxid, idx, change, others_inputs, to_delete, to_append):
ow = will[otxid]
ntxid = ow.tx.txid()
@@ -199,7 +201,7 @@ class Will:
outputs = w.tx.outputs()
found = False
old_txid = w.tx.txid()
# ntx = None
ntx = None
for i in range(0, len(inputs)):
if (
inputs[i].prevout.txid.hex() == otxid
@@ -210,7 +212,7 @@ class Will:
will[wid].tx.set_rbf(True)
will[wid].tx._inputs[i] = Will.new_input(wid, idx, change)
found = True
if found:
if found == True:
pass
new_txid = will[wid].tx.txid()
@@ -229,7 +231,6 @@ class Will:
to_append,
)
@staticmethod
def get_all_inputs(will, only_valid=False):
all_inputs = {}
for w, wi in will.items():
@@ -238,13 +239,12 @@ class Will:
for i in inputs:
prevout_str = i.prevout.to_str()
inp = [w, will[w], i]
if prevout_str not in all_inputs:
if not prevout_str in all_inputs:
all_inputs[prevout_str] = [inp]
else:
all_inputs[prevout_str].append(inp)
return all_inputs
@staticmethod
def get_all_inputs_min_locktime(all_inputs):
all_inputs_min_locktime = {}
@@ -252,14 +252,13 @@ class Will:
min_locktime = min(values, key=lambda x: x[1].tx.locktime)[1].tx.locktime
for w in values:
if w[1].tx.locktime == min_locktime:
if i not in all_inputs_min_locktime:
if not i in all_inputs_min_locktime:
all_inputs_min_locktime[i] = [w]
else:
all_inputs_min_locktime[i].append(w)
return all_inputs_min_locktime
@staticmethod
def search_anticipate_rec(will, old_inputs):
redo = False
to_delete = []
@@ -291,7 +290,7 @@ class Will:
for w in to_delete:
try:
del will[w]
except Exception:
except:
pass
for k, w in to_append.items():
will[k] = w
@@ -299,11 +298,10 @@ class Will:
Will.search_anticipate_rec(will, old_inputs)
@staticmethod
def update_will(old_will, new_will):
all_old_inputs = Will.get_all_inputs(old_will, only_valid=True)
# all_inputs_min_locktime = Will.get_all_inputs_min_locktime(all_old_inputs)
# all_new_inputs = Will.get_all_inputs(new_will)
all_inputs_min_locktime = Will.get_all_inputs_min_locktime(all_old_inputs)
all_new_inputs = Will.get_all_inputs(new_will)
# check if the new input is already spent by other transaction
# if it is use the same locktime, or anticipate.
Will.search_anticipate_rec(new_will, all_old_inputs)
@@ -326,7 +324,6 @@ class Will:
else:
continue
@staticmethod
def get_higher_input_for_tx(will):
out = {}
for wid in will:
@@ -340,25 +337,21 @@ class Will:
out[inp.prevout.to_str()] = inp
return out
@staticmethod
def invalidate_will(will, wallet, fees_per_byte):
will_only_valid = Will.only_valid_list(will)
inputs = Will.get_all_inputs(will_only_valid)
utxos = wallet.get_utxos()
filtered_inputs = []
prevout_to_spend = []
current_height = Util.get_current_height(wallet.network)
for prevout_str, ws in inputs.items():
for w in ws:
if w[0] not in filtered_inputs:
if not w[0] in filtered_inputs:
filtered_inputs.append(w[0])
if prevout_str not in prevout_to_spend:
if not prevout_str in prevout_to_spend:
prevout_to_spend.append(prevout_str)
balance = 0
utxo_to_spend = []
for utxo in utxos:
if utxo.is_coinbase_output() and utxo.block_height < current_height+100:
continue
utxo_str = utxo.prevout.to_str()
if utxo_str in prevout_to_spend:
balance += inputs[utxo_str][0][2].value_sats()
@@ -367,7 +360,7 @@ class Will:
change_addresses = wallet.get_change_addresses_for_new_transaction()
out = PartialTxOutput.from_address_and_value(change_addresses[0], balance)
out.is_change = True
locktime = current_height
locktime = Util.get_current_height(wallet.network)
tx = PartialTransaction.from_io(
utxo_to_spend, [out], locktime=locktime, version=2
)
@@ -392,15 +385,13 @@ class Will:
_logger.debug("len utxo_to_spend <=0")
pass
@staticmethod
def is_new(will):
for wid, w in will.items():
if w.get_status("VALID") and not w.get_status("COMPLETE"):
return True
@staticmethod
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():
inutxo = Util.in_utxo(inp, all_utxos)
for w in ws:
@@ -432,25 +423,20 @@ class Will:
else:
pass
@staticmethod
def utxos_strs(utxos):
return [Util.utxo_to_str(u) for u in utxos]
@staticmethod
def set_invalidate(wid, will=None):
will = will if will is not None else {}
def set_invalidate(wid, will=[]):
will[wid].set_status("INVALIDATED", True)
if will[wid].children:
for c in will[wid].children.items():
for c in self.children.items():
Will.set_invalidate(c[0], will)
@staticmethod
def check_tx_height(tx, wallet):
info = wallet.get_tx_info(tx)
return info.tx_mined_status.height()
# check if transactions are stil valid tecnically valid
@staticmethod
def check_invalidated(willtree, utxos_list, wallet):
for wid, w in willtree.items():
if (
@@ -460,7 +446,7 @@ class Will:
):
for inp in w.tx.inputs():
inp_str = Util.utxo_to_str(inp)
if inp_str not in utxos_list:
if not inp_str in utxos_list:
if wallet:
height = Will.check_tx_height(w.tx, wallet)
if height < 0:
@@ -470,22 +456,21 @@ class Will:
else:
w.set_status("CONFIRMED", True)
# def reflect_to_children(treeitem):
# if not treeitem.get_status("VALID"):
# _logger.debug(f"{tree:item._id} status not valid looking for children")
# for child in treeitem.children:
# wc = willtree[child]
# if wc.get_status("VALID"):
# if treeitem.get_status("INVALIDATED"):
# wc.set_status("INVALIDATED", True)
# if treeitem.get_status("REPLACED"):
# wc.set_status("REPLACED", True)
# if wc.children:
# Will.reflect_to_children(wc)
def reflect_to_children(treeitem):
if not treeitem.get_status("VALID"):
_logger.debug(f"{tree:item._id} status not valid looking for children")
for child in treeitem.children:
wc = willtree[child]
if wc.get_status("VALID"):
if treeitem.get_status("INVALIDATED"):
wc.set_status("INVALIDATED", True)
if treeitem.get_status("REPLACED"):
wc.set_status("REPLACED", True)
if wc.children:
Will.reflect_to_children(wc)
@staticmethod
def check_amounts(heirs, willexecutors, all_utxos, timestamp_to_check, dust):
fixed_heirs, fixed_amount, perc_heirs, perc_amount, fixed_amount_with_dust = (
fixed_heirs, fixed_amount, perc_heirs, perc_amount = (
heirs.fixed_percent_lists_amount(timestamp_to_check, dust, reverse=True)
)
wallet_balance = 0
@@ -507,7 +492,6 @@ class Will:
f"Willexecutor{url} excess base fee({wex['base_fee']}), {fixed_amount} >={temp_balance}"
)
@staticmethod
def check_will(will, all_utxos, wallet, block_to_check, timestamp_to_check):
Will.add_willtree(will)
utxos_list = Will.utxos_strs(all_utxos)
@@ -524,28 +508,18 @@ class Will:
Will.search_rai(all_inputs, all_utxos, will, wallet)
@staticmethod
def get_min_locktime(will,default_value=None):
return min((v.tx.locktime for v in will.values() if v.get_status('VALID')), default=default_value)
@staticmethod
def is_will_valid(
will,
block_to_check,
timestamp_to_check,
tx_fees,
all_utxos,
heirs=None,
willexecutors=None,
heirs={},
willexecutors={},
self_willexecutor=False,
wallet=False,
callback_not_valid_tx=None,
):
heirs = heirs if heirs is not None else {}
willexecutors= willexecutors if willexecutors is not None else {}
Will.check_will(will, all_utxos, wallet, block_to_check, timestamp_to_check)
if heirs:
if not Will.check_willexecutors_and_heirs(
@@ -574,7 +548,6 @@ class Will:
_logger.info("will ok")
return True
@staticmethod
def check_will_expired(all_inputs_min_locktime, block_to_check, timestamp_to_check):
_logger.info("check if some transaction is expired")
for prevout_str, wid in all_inputs_min_locktime.items():
@@ -591,22 +564,18 @@ class Will:
raise WillExpiredException(
f"Will Expired {wid[0][0]}: {locktime}<{timestamp_to_check}"
)
else:
from datetime import datetime
_logger.debug(f"Will Not Expired {wid[0][0]}: {datetime.fromtimestamp(locktime).isoformat()} > {datetime.fromtimestamp(timestamp_to_check).isoformat()}")
# def check_all_input_spent_are_in_wallet():
# _logger.info("check all input spent are in wallet or valid txs")
# for inp, ws in all_inputs.items():
# if not Util.in_utxo(inp, all_utxos):
# for w in ws:
# if w[1].get_status("VALID"):
# prevout_id = w[2].prevout.txid.hex()
# parentwill = will.get(prevout_id, False)
# if not parentwill or not parentwill.get_status("VALID"):
# w[1].set_status("INVALIDATED", True)
def check_all_input_spent_are_in_wallet():
_logger.info("check all input spent are in wallet or valid txs")
for inp, ws in all_inputs.items():
if not Util.in_utxo(inp, all_utxos):
for w in ws:
if w[1].get_status("VALID"):
prevout_id = w[2].prevout.txid.hex()
parentwill = will.get(prevout_id, False)
if not parentwill or not parentwill.get_status("VALID"):
w[1].set_status("INVALIDATED", True)
@staticmethod
def only_valid_list(will):
out = {}
for wid, w in will.items():
@@ -614,7 +583,6 @@ class Will:
out[wid] = w
return out
@staticmethod
def only_valid_or_replaced_list(will):
out = []
for wid, w in will.items():
@@ -623,7 +591,6 @@ class Will:
out.append(wid)
return out
@staticmethod
def check_willexecutors_and_heirs(
will, heirs, willexecutors, self_willexecutor, check_date, tx_fees
):
@@ -637,7 +604,7 @@ class Will:
for wid in Will.only_valid_list(will):
w = will[wid]
if w.tx_fees != tx_fees:
raise TxFeesChangedException(f"{tx_fees}: {w.tx_fees}")
raise TxFeesChangedException(f"{tx_fees}:", w.tx_fees)
for wheir in w.heirs:
if not 'w!ll3x3c"' == wheir[:9]:
their = will[wid].heirs[wheir]
@@ -653,9 +620,9 @@ class Will:
heirs_found[wheir] = count + 1
else:
_logger.debug(
f"heir not present transaction is not valid:{wheir} {wid}, {w}"
"heir not present transaction is not valid:", wid, w
)
continue
if willexecutor := w.we:
count = willexecutors_found.get(willexecutor["url"], 0)
if Util.cmp_willexecutor(
@@ -667,26 +634,25 @@ class Will:
no_willexecutor += 1
count_heirs = 0
for h in heirs:
if Util.parse_locktime_string(heirs[h][2]) >= check_date:
count_heirs += 1
if h not in heirs_found:
if not h in heirs_found:
_logger.debug(f"heir: {h} not found")
raise HeirNotFoundException(h)
if not count_heirs:
raise NoHeirsException("there are not valid heirs")
if self_willexecutor and no_willexecutor == 0:
raise NoWillExecutorNotPresent("Backup tx")
for url, we in willexecutors.items():
if Willexecutors.is_selected(we):
if url not in willexecutors_found:
if not url in willexecutors_found:
_logger.debug(f"will-executor: {url} not fount")
raise WillExecutorNotPresent(url)
_logger.info("will is coherent with heirs and will-executors")
return True
class WillItem(Logger):
STATUS_DEFAULT = {
"ANTICIPATED": ["Anticipated", False],
@@ -831,9 +797,9 @@ class WillItem(Logger):
iw = inp[1]
self.set_anticipate(iw)
def set_check_willexecutor(self,resp):
def check_willexecutor(self):
try:
if resp :
if resp := Willexecutors.check_transaction(self._id, self.we["url"]):
if "tx" in resp and resp["tx"] == str(self.tx):
self.set_status("PUSHED")
self.set_status("CHECKED")
@@ -873,12 +839,7 @@ class WillItem(Logger):
class WillException(Exception):
def __init__(self,msg="WillException"):
self.msg=msg
Exception.__init__(self)
def __str__(self):
return self.msg
pass
class WillExpiredException(WillException):
@@ -915,6 +876,8 @@ class WillExecutorNotPresent(NotCompleteWillException):
class NoHeirsException(WillException):
pass
class AmountException(WillException):
pass
@@ -925,3 +888,7 @@ class PercAmountException(AmountException):
class FixedAmountException(AmountException):
pass
def test_check_invalidated():
Will.check_invalidated(will, utxos_list, wallet)

View File

@@ -1,38 +1,33 @@
import json
import time
from datetime import datetime
from functools import partial
from aiohttp import ClientResponse
from electrum import constants
from electrum.gui.qt.util import WaitingDialog
from electrum.i18n import _
from electrum.logging import get_logger
from electrum.network import Network
from .bal import BalPlugin
from .util import Util
DEFAULT_TIMEOUT = 5
_logger = get_logger(__name__)
chainname = BalPlugin.chainname
class Willexecutors:
@staticmethod
def save(bal_plugin, willexecutors):
_logger.debug(f"save {willexecutors},{chainname}")
aw = bal_plugin.WILLEXECUTORS.get()
aw[chainname] = willexecutors
aw[constants.net.NET_NAME] = willexecutors
bal_plugin.WILLEXECUTORS.set(aw)
_logger.debug(f"saved: {aw}")
# bal_plugin.WILLEXECUTORS.set(willexecutors)
@staticmethod
def get_willexecutors(
bal_plugin, update=False, bal_window=False, force=False, task=True
):
willexecutors = bal_plugin.WILLEXECUTORS.get()
willexecutors = willexecutors.get(chainname, {})
willexecutors = willexecutors.get(constants.net.NET_NAME, {})
to_del = []
for w in willexecutors:
if not isinstance(willexecutors[w], dict):
@@ -41,14 +36,12 @@ class Willexecutors:
Willexecutors.initialize_willexecutor(willexecutors[w], w)
for w in to_del:
_logger.error(
"error Willexecutor to delete type:{} {}".format(
type(willexecutors[w]), w
)
"error Willexecutor to delete type:{} ", type(willexecutor[w]), w
)
del willexecutors[w]
bal = bal_plugin.WILLEXECUTORS.default.get(chainname, {})
bal = bal_plugin.WILLEXECUTORS.default.get(constants.net.NET_NAME, {})
for bal_url, bal_executor in bal.items():
if bal_url not in willexecutors:
if not bal_url in willexecutors:
_logger.debug(f"force add {bal_url} willexecutor")
willexecutors[bal_url] = bal_executor
# if update:
@@ -79,19 +72,17 @@ class Willexecutors:
)
return w_sorted
@staticmethod
def is_selected(willexecutor, value=None):
if not willexecutor:
return False
if value is not None:
if not value is None:
willexecutor["selected"] = value
try:
return willexecutor["selected"]
except Exception:
except:
willexecutor["selected"] = False
return False
@staticmethod
def get_willexecutor_transactions(will, force=False):
willexecutors = {}
for wid, willitem in will.items():
@@ -101,7 +92,7 @@ class Willexecutors:
if willexecutor := willitem.we:
url = willexecutor["url"]
if willexecutor and Willexecutors.is_selected(willexecutor):
if url not in willexecutors:
if not url in willexecutors:
willexecutor["txs"] = ""
willexecutor["txsids"] = []
willexecutor["broadcast_status"] = _("Waiting...")
@@ -111,35 +102,31 @@ class Willexecutors:
return willexecutors
# def only_selected_list(willexecutors):
# out = {}
# for url, v in willexecutors.items():
# if Willexecutors.is_selected(url):
# out[url] = v
def only_selected_list(willexecutors):
out = {}
for url, v in willexecutors.items():
if Willexecutors.is_selected(willexecutor):
out[url] = v
# def push_transactions_to_willexecutors(will):
# willexecutors = Willexecutors.get_transactions_to_be_pushed()
# for url in willexecutors:
# willexecutor = willexecutors[url]
# if Willexecutors.is_selected(willexecutor):
# if "txs" in willexecutor:
# Willexecutors.push_transactions_to_willexecutor(
# willexecutors[url]["txs"], url
# )
def push_transactions_to_willexecutors(will):
willexecutors = get_transactions_to_be_pushed()
for url in willexecutors:
willexecutor = willexecutors[url]
if Willexecutors.is_selected(willexecutor):
if "txs" in willexecutor:
Willexecutors.push_transactions_to_willexecutor(
willexecutors[url]["txs"], url
)
@staticmethod
def send_request(
method, url, data=None, *, timeout=10, handle_response=None, count_reply=0
):
def send_request(method, url, data=None, *, timeout=10):
network = Network.get_instance()
if not network:
raise Exception("You are offline.")
_logger.debug(f"<-- {method} {url} {data}")
headers = {}
headers["user-agent"] = f"BalPlugin v:{BalPlugin.__version__}"
headers["user-agent"] = f"BalPlugin v:{BalPlugin.version()}"
headers["Content-Type"] = "text/plain"
if not handle_response:
handle_response = Willexecutors.handle_response
try:
if method == "get":
response = Network.send_http_on_proxy(
@@ -147,7 +134,7 @@ class Willexecutors:
url,
params=data,
headers=headers,
on_finish=handle_response,
on_finish=Willexecutors.handle_response,
timeout=timeout,
)
elif method == "post":
@@ -156,64 +143,40 @@ class Willexecutors:
url,
body=data,
headers=headers,
on_finish=handle_response,
on_finish=Willexecutors.handle_response,
timeout=timeout,
)
else:
raise Exception(f"unexpected {method=!r}")
except TimeoutError:
if count_reply < 10:
_logger.debug(f"timeout({count_reply}) error: retry in 3 sec...")
time.sleep(3)
return Willexecutors.send_request(
method,
url,
data,
timeout=timeout,
handle_response=handle_response,
count_reply=count_reply + 1,
)
else:
_logger.debug(f"Too many timeouts: {count_reply}")
except Exception as e:
_logger.error(f"exception sending request {e}")
raise e
else:
_logger.debug(f"--> {response}")
return response
@staticmethod
def get_we_url_from_response(resp):
url_slices = str(resp.url).split("/")
if len(url_slices) > 2:
url_slices = url_slices[:-2]
return "/".join(url_slices)
@staticmethod
async def handle_response(resp: ClientResponse):
r = await resp.text()
try:
r = json.loads(r)
# url = Willexecutors.get_we_url_from_response(resp)
# r["url"]= url
# r["status"]=resp.status
except Exception as e:
_logger.debug(f"error handling response:{e}")
r["status"] = resp.status
r["selected"] = Willexecutors.is_selected(willexecutor)
r["url"] = url
except:
pass
return r
@staticmethod
class AlreadyPresentException(Exception):
pass
@staticmethod
def push_transactions_to_willexecutor(willexecutor):
out = True
try:
_logger.debug(f"{willexecutor['url']}: {willexecutor['txs']}")
_logger.debug(f"{willexecutor[url]}: {willexecutor['txs']}")
if w := Willexecutors.send_request(
"post",
willexecutor["url"] + "/" + chainname + "/pushtxs",
willexecutor["url"] + "/" + constants.net.NET_NAME + "/pushtxs",
data=willexecutor["txs"].encode("ascii"),
):
willexecutor["broadcast_status"] = _("Success")
@@ -232,25 +195,27 @@ class Willexecutors:
return out
@staticmethod
def ping_servers(willexecutors):
for url, we in willexecutors.items():
Willexecutors.get_info_task(url, we)
@staticmethod
def get_info_task(url, willexecutor):
w = None
try:
_logger.info("GETINFO_WILLEXECUTOR")
_logger.debug(url)
w = Willexecutors.send_request("get", url + "/" + chainname + "/info")
if isinstance(w, dict):
netname = "bitcoin"
if constants.net.NET_NAME != "mainnet":
netname = constants.net.NET_NAME
w = Willexecutors.send_request("get", url + "/" + netname + "/info")
willexecutor["url"] = url
willexecutor["status"] = 200
willexecutor["status"] = w["status"]
willexecutor["base_fee"] = w["base_fee"]
willexecutor["address"] = w["address"]
if not willexecutor["info"]:
willexecutor["info"] = w["info"]
_logger.debug(f"response_data {w}")
_logger.debug(f"response_data {w['address']}")
except Exception as e:
_logger.error(f"error {e} contacting {url}: {w}")
willexecutor["status"] = "KO"
@@ -258,44 +223,30 @@ class Willexecutors:
willexecutor["last_update"] = datetime.now().timestamp()
return willexecutor
@staticmethod
def initialize_willexecutor(willexecutor, url, status=None, old_willexecutor=None):
old_willexecutor=old_willexecutor if old_willexecutor is not None else {}
def initialize_willexecutor(willexecutor, url, status=None, selected=None):
willexecutor["url"] = url
if status is not None:
if not status is None:
willexecutor["status"] = status
else:
willexecutor["status"] = old_willexecutor.get("status",willexecutor.get("status","Ko"))
willexecutor["selected"]=Willexecutors.is_selected(old_willexecutor) or willexecutor.get("selected",False)
willexecutor["address"]=old_willexecutor.get("address",willexecutor.get("address",""))
willexecutor["promo_code"]=old_willexecutor.get("promo_code",willexecutor.get("promo_code"))
willexecutor["selected"] = Willexecutors.is_selected(willexecutor, selected)
@staticmethod
def download_list(old_willexecutors,welist_server):
def download_list(bal_plugin):
try:
welist_server = welist_server if welist_server[-1] == '/' else welist_server+'/'
willexecutors = Willexecutors.send_request(
"get",
f"{welist_server}data/{chainname}?page=0&limit=100",
)
# del willexecutors["status"]
for w in willexecutors:
if w not in ("status", "url"):
Willexecutors.initialize_willexecutor(
willexecutors[w], w, None, old_willexecutors.get(w,{})
l = Willexecutors.send_request(
"get", "https://welist.bitcoin-after.life/data/bitcoin?page=0&limit=100"
)
del l["status"]
for w in l:
willexecutor = l[w]
Willexecutors.initialize_willexecutor(willexecutor, w, "New", False)
# bal_plugin.WILLEXECUTORS.set(l)
# bal_plugin.config.set_key(bal_plugin.WILLEXECUTORS,l,save=True)
return willexecutors
return l
except Exception as e:
_logger.error(f"Failed to download willexecutors list: {e}")
return {}
@staticmethod
def get_willexecutors_list_from_json():
def get_willexecutors_list_from_json(bal_plugin):
try:
with open("willexecutors.json") as f:
willexecutors = json.load(f)
@@ -303,13 +254,12 @@ class Willexecutors:
willexecutor = willexecutors[w]
Willexecutors.initialize_willexecutor(willexecutor, w, "New", False)
# bal_plugin.WILLEXECUTORS.set(willexecutors)
return willexecutors
return h
except Exception as e:
_logger.error(f"error opening willexecutors json: {e}")
return {}
@staticmethod
def check_transaction(txid, url):
_logger.debug(f"{url}:{txid}")
try:
@@ -321,54 +271,14 @@ class Willexecutors:
_logger.error(f"error contacting {url} for checking txs {e}")
raise e
@staticmethod
def compute_id(willexecutor):
return "{}-{}".format(willexecutor.get("url"), willexecutor.get("chain"))
class WillExecutor:
def __init__(self, url, base_fee, chain, info, version):
self.url = url
self.base_fee = base_fee
self.chain = chain
self.info = info
self.version = version
#class WillExecutor:
# def __init__(
# self,
# url,
# base_fee,
# chain,
# info,
# version,
# status,
# is_selected=False,
# promo_code="",
# ):
# self.url = url
# self.base_fee = base_fee
# self.chain = chain
# self.info = info
# self.version = version
# self.status = status
# self.promo_code = promo_code
# self.is_selected = is_selected
# self.id = self.compute_id()
#
# def from_dict(d):
# return WillExecutor(
# url=d.get("url", "http://localhost:8000"),
# base_fee=d.get("base_fee", 1000),
# chain=d.get("chain", chainname),
# info=d.get("info", ""),
# version=d.get("version", 0),
# status=d.get("status", "Ko"),
# is_selected=d.get("is_selected", "False"),
# promo_code=d.get("promo_code", ""),
# )
#
# def to_dict(self):
# return {
# "url": self.url,
# "base_fee": self.base_fee,
# "chain": self.chain,
# "info": self.info,
# "version": self.version,
# "promo_code": self.promo_code,
# }
#
# def compute_id(self):
# return f"{self.url}-{self.chain}"
def from_dict(d):
we = WillExecutor(d["url"], d["base_fee"], d["chain"], d["info"], d["version"])