2 Commits

10 changed files with 806 additions and 925 deletions

65
bal.py
View File

@@ -1,14 +1,14 @@
import os import os
from datetime import date, datetime, timedelta
import platform
# import random # import random
# import zipfile as zipfile_lib # import zipfile as zipfile_lib
from electrum import constants, json_db
from electrum import json_db
from electrum.logging import get_logger from electrum.logging import get_logger
from electrum.plugin import BasePlugin from electrum.plugin import BasePlugin
from electrum.transaction import tx_from_any from electrum.transaction import tx_from_any
_logger = get_logger(__name__)
def get_will_settings(x): def get_will_settings(x):
# print(x) # print(x)
pass pass
@@ -48,13 +48,7 @@ class BalConfig:
class BalPlugin(BasePlugin): class BalPlugin(BasePlugin):
_version=None _version=None
__version__ = "0.2.8" #AUTOMATICALLY GENERATED DO NOT EDIT __version__ = "0.2.7" #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): def version(self):
if not self._version: if not self._version:
try: try:
@@ -106,7 +100,6 @@ class BalPlugin(BasePlugin):
self.HIDE_INVALIDATED = BalConfig(config, "bal_hide_invalidated", True) self.HIDE_INVALIDATED = BalConfig(config, "bal_hide_invalidated", True)
self.ALLOW_REPUSH = BalConfig(config, "bal_allow_repush", True) self.ALLOW_REPUSH = BalConfig(config, "bal_allow_repush", True)
self.FIRST_EXECUTION = BalConfig(config, "bal_first_execution", True) self.FIRST_EXECUTION = BalConfig(config, "bal_first_execution", True)
self.WELIST_SERVER = BalConfig(config,"bal_welist_server","https://welist.bitcoin-after.life/")
self.WILLEXECUTORS = BalConfig( self.WILLEXECUTORS = BalConfig(
config, config,
"bal_willexecutors", "bal_willexecutors",
@@ -129,33 +122,18 @@ class BalPlugin(BasePlugin):
"selected": True, "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( self.WILL_SETTINGS = BalConfig(
config, config,
"bal_will_settings", "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_invalidated = self.HIDE_INVALIDATED.get()
self._hide_replaced = self.HIDE_REPLACED.get() self._hide_replaced = self.HIDE_REPLACED.get()
@@ -171,22 +149,13 @@ class BalPlugin(BasePlugin):
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):
defaults=BalPlugin.default_will_settings() if int(will_settings.get("baltx_fees", 1)) < 1:
if not will_settings: will_settings["baltx_fees"] = 1
will_settings=[]
if int(will_settings.get("baltx_fees", 0)) < 1:
will_settings["baltx_fees"] = defaults['baltx_fees']
if not will_settings.get("threshold"): if not will_settings.get("threshold"):
will_settings["threshold"] = defaults['threshold'] will_settings["threshold"] = "180d"
if not will_settings.get("locktime"): if not will_settings.get("locktime"):
will_settings["locktime"] = defaults['locktime'] will_settings["locktime"] = "1y"
return will_settings return will_settings
@staticmethod def default_will_settings(self):
def default_will_settings(): return {"baltx_fees": 100, "threshold": "180d", "locktime": "1y"}
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}

View File

@@ -34,7 +34,6 @@ from electrum.transaction import (
# TxOutput, # TxOutput,
) )
from electrum.util import ( from electrum.util import (
BitcoinException,
bfh, bfh,
read_json_file, read_json_file,
to_string, to_string,
@@ -44,6 +43,7 @@ from electrum.util import (
from .util import Util from .util import Util
from .willexecutors import Willexecutors from .willexecutors import Willexecutors
from electrum.util import BitcoinException
if TYPE_CHECKING: if TYPE_CHECKING:
from .simple_config import SimpleConfig from .simple_config import SimpleConfig
@@ -71,22 +71,28 @@ def reduce_outputs(in_amount, out_amount, fee, outputs):
output.value = math.floor((in_amount - fee) / out_amount * output.value) 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""" #TODO: put this method inside wallet.db to replace or complete get_locktime_for_new_transaction
data = bytes.fromhex(data_hex) 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): def prepare_transactions(locktimes, available_utxos, fees, wallet):
available_utxos = sorted( available_utxos = sorted(
@@ -161,13 +167,6 @@ def prepare_transactions(locktimes, available_utxos, fees, wallet):
outputs.append(change) outputs.append(change)
for i in range(0, 100): for i in range(0, 100):
random.shuffle(outputs) 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( tx = PartialTransaction.from_io(
used_utxos, used_utxos,
outputs, outputs,
@@ -183,7 +182,7 @@ def prepare_transactions(locktimes, available_utxos, fees, wallet):
tx.remove_signatures() tx.remove_signatures()
txid = tx.txid() txid = tx.txid()
if txid is None: if txid is None:
raise Exception(f"txid is none: {tx}") raise Exception("txid is none", tx)
tx.heirs = paid_heirs tx.heirs = paid_heirs
tx.my_locktime = locktime tx.my_locktime = locktime

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

1188
qt.py

File diff suppressed because it is too large Load Diff

81
util.py
View File

@@ -1,13 +1,15 @@
import bisect import bisect
from datetime import datetime, timedelta from datetime import datetime, timedelta
from electrum.gui.qt.util import getSaveFileName
from electrum.i18n import _
from electrum.transaction import PartialTxOutput from electrum.transaction import PartialTxOutput
from electrum.util import FileExportFailed
LOCKTIME_THRESHOLD = 500000000 LOCKTIME_THRESHOLD = 500000000
class Util: class Util:
@staticmethod
def locktime_to_str(locktime): def locktime_to_str(locktime):
try: try:
locktime = int(locktime) locktime = int(locktime)
@@ -19,7 +21,6 @@ class Util:
pass pass
return str(locktime) return str(locktime)
@staticmethod
def str_to_locktime(locktime): def str_to_locktime(locktime):
try: try:
if locktime[-1] in ("y", "d", "b"): if locktime[-1] in ("y", "d", "b"):
@@ -32,7 +33,6 @@ class Util:
timestamp = dt_object.timestamp() timestamp = dt_object.timestamp()
return int(timestamp) return int(timestamp)
@staticmethod
def parse_locktime_string(locktime, w=None): def parse_locktime_string(locktime, w=None):
try: try:
return int(locktime) return int(locktime)
@@ -60,7 +60,6 @@ class Util:
pass pass
return 0 return 0
@staticmethod
def int_locktime(seconds=0, minutes=0, hours=0, days=0, blocks=0): def int_locktime(seconds=0, minutes=0, hours=0, days=0, blocks=0):
return int( return int(
seconds seconds
@@ -70,7 +69,6 @@ class Util:
+ blocks * 600 + blocks * 600
) )
@staticmethod
def encode_amount(amount, decimal_point): def encode_amount(amount, decimal_point):
if Util.is_perc(amount): if Util.is_perc(amount):
return amount return amount
@@ -80,7 +78,6 @@ class Util:
except Exception: except Exception:
return 0 return 0
@staticmethod
def decode_amount(amount, decimal_point): def decode_amount(amount, decimal_point):
if Util.is_perc(amount): if Util.is_perc(amount):
return amount return amount
@@ -91,14 +88,12 @@ class Util:
except Exception: except Exception:
return str(amount) return str(amount)
@staticmethod
def is_perc(value): def is_perc(value):
try: try:
return value[-1] == "%" return value[-1] == "%"
except Exception: except Exception:
return False return False
@staticmethod
def cmp_array(heira, heirb): def cmp_array(heira, heirb):
try: try:
if len(heira) != len(heirb): if len(heira) != len(heirb):
@@ -110,13 +105,11 @@ class Util:
except Exception: except Exception:
return False return False
@staticmethod
def cmp_heir(heira, heirb): def cmp_heir(heira, heirb):
if heira[0] == heirb[0] and heira[1] == heirb[1]: if heira[0] == heirb[0] and heira[1] == heirb[1]:
return True return True
return False return False
@staticmethod
def cmp_willexecutor(willexecutora, willexecutorb): def cmp_willexecutor(willexecutora, willexecutorb):
if willexecutora == willexecutorb: if willexecutora == willexecutorb:
return True return True
@@ -131,7 +124,6 @@ class Util:
return False return False
return False return False
@staticmethod
def search_heir_by_values(heirs, heir, values): def search_heir_by_values(heirs, heir, values):
for h, v in heirs.items(): for h, v in heirs.items():
found = False found = False
@@ -143,14 +135,12 @@ class Util:
return h return h
return False return False
@staticmethod
def cmp_heir_by_values(heira, heirb, values): def cmp_heir_by_values(heira, heirb, values):
for v in values: for v in values:
if heira[v] != heirb[v]: if heira[v] != heirb[v]:
return False return False
return True return True
@staticmethod
def cmp_heirs_by_values( def cmp_heirs_by_values(
heirsa, heirsb, values, exclude_willexecutors=False, reverse=True heirsa, heirsb, values, exclude_willexecutors=False, reverse=True
): ):
@@ -175,7 +165,6 @@ class Util:
else: else:
return True return True
@staticmethod
def cmp_heirs( def cmp_heirs(
heirsa, heirsa,
heirsb, heirsb,
@@ -198,7 +187,6 @@ class Util:
raise e raise e
return False return False
@staticmethod
def cmp_inputs(inputsa, inputsb): def cmp_inputs(inputsa, inputsb):
if len(inputsa) != len(inputsb): if len(inputsa) != len(inputsb):
return False return False
@@ -207,7 +195,6 @@ class Util:
return False return False
return True return True
@staticmethod
def cmp_outputs(outputsa, outputsb, willexecutor_output=None): def cmp_outputs(outputsa, outputsb, willexecutor_output=None):
if len(outputsa) != len(outputsb): if len(outputsa) != len(outputsb):
return False return False
@@ -217,7 +204,6 @@ class Util:
return False return False
return True return True
@staticmethod
def cmp_txs(txa, txb): def cmp_txs(txa, txb):
if not Util.cmp_inputs(txa.inputs(), txb.inputs()): if not Util.cmp_inputs(txa.inputs(), txb.inputs()):
return False return False
@@ -225,7 +211,6 @@ class Util:
return False return False
return True return True
@staticmethod
def get_value_amount(txa, txb): def get_value_amount(txa, txb):
outputsa = txa.outputs() outputsa = txa.outputs()
# outputsb = txb.outputs() # outputsb = txb.outputs()
@@ -244,7 +229,6 @@ class Util:
return value_amount return value_amount
@staticmethod
def chk_locktime(timestamp_to_check, block_height_to_check, locktime): def chk_locktime(timestamp_to_check, block_height_to_check, locktime):
# TODO BUG: WHAT HAPPEN AT THRESHOLD? # TODO BUG: WHAT HAPPEN AT THRESHOLD?
locktime = int(locktime) locktime = int(locktime)
@@ -255,7 +239,6 @@ class Util:
else: else:
return False return False
@staticmethod
def anticipate_locktime(locktime, blocks=0, hours=0, days=0): def anticipate_locktime(locktime, blocks=0, hours=0, days=0):
locktime = int(locktime) locktime = int(locktime)
out = 0 out = 0
@@ -272,7 +255,6 @@ class Util:
out = 1 out = 1
return out return out
@staticmethod
def cmp_locktime(locktimea, locktimeb): def cmp_locktime(locktimea, locktimeb):
if locktimea == locktimeb: if locktimea == locktimeb:
return 0 return 0
@@ -286,20 +268,17 @@ class Util:
else: else:
return int(locktimea) - (locktimeb) return int(locktimea) - (locktimeb)
@staticmethod
def get_lowest_valid_tx(available_utxos, will): def get_lowest_valid_tx(available_utxos, will):
will = sorted(will.items(), key=lambda x: x[1]["tx"].locktime) will = sorted(will.items(), key=lambda x: x[1]["tx"].locktime)
for txid, willitem in will.items(): for txid, willitem in will.items():
pass pass
@staticmethod
def get_locktimes(will): def get_locktimes(will):
locktimes = {} locktimes = {}
for txid, willitem in will.items(): for txid, willitem in will.items():
locktimes[willitem["tx"].locktime] = True locktimes[willitem["tx"].locktime] = True
return locktimes.keys() return locktimes.keys()
@staticmethod
def get_lowest_locktimes(locktimes): def get_lowest_locktimes(locktimes):
sorted_timestamp = [] sorted_timestamp = []
sorted_block = [] sorted_block = []
@@ -312,22 +291,18 @@ class Util:
return sorted(sorted_timestamp), sorted(sorted_block) return sorted(sorted_timestamp), sorted(sorted_block)
@staticmethod
def get_lowest_locktimes_from_will(will): def get_lowest_locktimes_from_will(will):
return Util.get_lowest_locktimes(Util.get_locktimes(will)) return Util.get_lowest_locktimes(Util.get_locktimes(will))
@staticmethod
def search_willtx_per_io(will, tx): def search_willtx_per_io(will, tx):
for wid, w in will.items(): for wid, w in will.items():
if Util.cmp_txs(w["tx"], tx["tx"]): if Util.cmp_txs(w["tx"], tx["tx"]):
return wid, w return wid, w
return None, None return None, None
@staticmethod
def invalidate_will(will): def invalidate_will(will):
raise Exception("not implemented") raise Exception("not implemented")
@staticmethod
def get_will_spent_utxos(will): def get_will_spent_utxos(will):
utxos = [] utxos = []
for txid, willitem in will.items(): for txid, willitem in will.items():
@@ -335,7 +310,6 @@ class Util:
return utxos return utxos
@staticmethod
def utxo_to_str(utxo): def utxo_to_str(utxo):
try: try:
return utxo.to_str() return utxo.to_str()
@@ -347,7 +321,6 @@ class Util:
pass pass
return str(utxo) return str(utxo)
@staticmethod
def cmp_utxo(utxoa, utxob): def cmp_utxo(utxoa, utxob):
utxoa = Util.utxo_to_str(utxoa) utxoa = Util.utxo_to_str(utxoa)
utxob = Util.utxo_to_str(utxob) utxob = Util.utxo_to_str(utxob)
@@ -356,25 +329,21 @@ class Util:
else: else:
return False return False
@staticmethod
def in_utxo(utxo, utxos): def in_utxo(utxo, utxos):
for s_u in utxos: for s_u in utxos:
if Util.cmp_utxo(s_u, utxo): if Util.cmp_utxo(s_u, utxo):
return True return True
return False return False
@staticmethod
def txid_in_utxo(txid, utxos): def txid_in_utxo(txid, utxos):
for s_u in utxos: for s_u in utxos:
if s_u.prevout.txid == txid: if s_u.prevout.txid == txid:
return True return True
return False return False
@staticmethod
def cmp_output(outputa, outputb): def cmp_output(outputa, outputb):
return outputa.address == outputb.address and outputa.value == outputb.value return outputa.address == outputb.address and outputa.value == outputb.value
@staticmethod
def in_output(output, outputs): def in_output(output, outputs):
for s_o in outputs: for s_o in outputs:
if Util.cmp_output(s_o, output): if Util.cmp_output(s_o, output):
@@ -386,7 +355,6 @@ class Util:
# return true false same amount different address # return true false same amount different address
# return false false different amount, different address not found # return false false different amount, different address not found
@staticmethod
def din_output(out, outputs): def din_output(out, outputs):
same_amount = [] same_amount = []
for s_o in outputs: for s_o in outputs:
@@ -402,7 +370,6 @@ class Util:
else: else:
return False, False return False, False
@staticmethod
def get_change_output(wallet, in_amount, out_amount, fee): def get_change_output(wallet, in_amount, out_amount, fee):
change_amount = int(in_amount - out_amount - fee) change_amount = int(in_amount - out_amount - fee)
if change_amount > wallet.dust_threshold(): if change_amount > wallet.dust_threshold():
@@ -413,7 +380,6 @@ class Util:
out.is_change = True out.is_change = True
return out return out
@staticmethod
def get_current_height(network): def get_current_height(network):
# if no network or not up to date, just set locktime to zero # if no network or not up to date, just set locktime to zero
if not network: if not network:
@@ -435,7 +401,6 @@ class Util:
height = min(chain_height, server_height) height = min(chain_height, server_height)
return height return height
@staticmethod
def print_var(var, name="", veryverbose=False): def print_var(var, name="", veryverbose=False):
print(f"---{name}---") print(f"---{name}---")
if var is not None: if var is not None:
@@ -470,7 +435,6 @@ class Util:
print(f"---end {name}---") print(f"---end {name}---")
@staticmethod
def print_utxo(utxo, name=""): def print_utxo(utxo, name=""):
print(f"---utxo-{name}---") print(f"---utxo-{name}---")
Util.print_var(utxo, name) Util.print_var(utxo, name)
@@ -482,20 +446,36 @@ class Util:
print("_TxInput__value_sats:", utxo._TxInput__value_sats) print("_TxInput__value_sats:", utxo._TxInput__value_sats)
print(f"---utxo-end {name}---") print(f"---utxo-end {name}---")
@staticmethod
def print_prevout(prevout, name=""): def print_prevout(prevout, name=""):
print(f"---prevout-{name}---") print(f"---prevout-{name}---")
Util.print_var(prevout, f"{name}-prevout") Util.print_var(prevout, f"{name}-prevout")
Util.print_var(prevout._asdict()) Util.print_var(prevout._asdict())
print(f"---prevout-end {name}---") print(f"---prevout-end {name}---")
def export_meta_gui(electrum_window, 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): def copy(dicto, dictfrom):
for k, v in dictfrom.items(): for k, v in dictfrom.items():
dicto[k] = v dicto[k] = v
@staticmethod
def fix_will_settings_tx_fees(will_settings): def fix_will_settings_tx_fees(will_settings):
tx_fees = will_settings.get("tx_fees", False) tx_fees = will_settings.get("tx_fees", False)
have_to_update = False have_to_update = False
@@ -505,7 +485,6 @@ class Util:
have_to_update = True have_to_update = True
return have_to_update return have_to_update
@staticmethod
def fix_will_tx_fees(will): def fix_will_tx_fees(will):
have_to_update = False have_to_update = False
for txid, willitem in will.items(): for txid, willitem in will.items():
@@ -515,19 +494,3 @@ class Util:
del will[txid]["tx_fees"] del will[txid]["tx_fees"]
have_to_update = True have_to_update = True
return have_to_update 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 #!env/bin/python3
import getpass
import json
import os
import sys
from electrum.storage import WalletStorage from electrum.storage import WalletStorage
import json
from electrum.util import MyEncoder from electrum.util import MyEncoder
import sys
import getpass
import os
default_fees = 100 default_fees = 100
@@ -27,11 +26,8 @@ def fix_will_settings_tx_fees(json_wallet):
def uninstall_bal(json_wallet): def uninstall_bal(json_wallet):
if "will_settings" in json_wallet:
del json_wallet["will_settings"] del json_wallet["will_settings"]
if "will" in json_wallet:
del json_wallet["will"] del json_wallet["will"]
if "heirs" in json_wallet:
del json_wallet["heirs"] del json_wallet["heirs"]
return True return True

View File

@@ -1,31 +1,30 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import json
import os
import sys import sys
import os
from bal_wallet_utils import fix_will_settings_tx_fees, save, uninstall_bal import json
from electrum.storage import WalletStorage
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QApplication, QApplication,
QFileDialog, QMainWindow,
QGroupBox, QVBoxLayout,
QHBoxLayout, QHBoxLayout,
QLabel, QLabel,
QLineEdit, QLineEdit,
QMainWindow,
QPushButton, QPushButton,
QTextEdit,
QVBoxLayout,
QWidget, QWidget,
QFileDialog,
QGroupBox,
QTextEdit,
) )
from electrum.storage import WalletStorage
from bal_wallet_utils import fix_will_settings_tx_fees, uninstall_bal, save
class WalletUtilityGUI(QMainWindow): class WalletUtilityGUI(QMainWindow):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.init_ui() self.initUI()
def init_ui(self): def initUI(self):
self.setWindowTitle("BAL Wallet Utility") self.setWindowTitle("BAL Wallet Utility")
self.setFixedSize(500, 400) self.setFixedSize(500, 400)

62
will.py
View File

@@ -24,7 +24,6 @@ _logger = get_logger(__name__)
class Will: class Will:
@staticmethod
def get_children(will, willid): def get_children(will, willid):
out = [] out = []
for _id in will: for _id in will:
@@ -36,7 +35,6 @@ class Will:
return out return out
# build a tree with parent transactions # build a tree with parent transactions
@staticmethod
def add_willtree(will): def add_willtree(will):
for willid in will: for willid in will:
will[willid].children = Will.get_children(will, willid) will[willid].children = Will.get_children(will, willid)
@@ -45,17 +43,14 @@ class Will:
will[child[0]].father = willid will[child[0]].father = willid
# return a list of will sorted by locktime # return a list of will sorted by locktime
@staticmethod
def get_sorted_will(will): def get_sorted_will(will):
return sorted(will.items(), key=lambda x: x[1]["tx"].locktime) return sorted(will.items(), key=lambda x: x[1]["tx"].locktime)
@staticmethod
def only_valid(will): def only_valid(will):
for k, v in will.items(): for k, v in will.items():
if v.get_status("VALID"): if v.get_status("VALID"):
yield k yield k
@staticmethod
def search_equal_tx(will, tx, wid): def search_equal_tx(will, tx, wid):
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():
@@ -64,7 +59,6 @@ class Will:
return will[w]["tx"] return will[w]["tx"]
return False return False
@staticmethod
def get_tx_from_any(x): def get_tx_from_any(x):
try: try:
a = str(x) a = str(x)
@@ -75,7 +69,6 @@ class Will:
return x return x
@staticmethod
def add_info_from_will(will, wid, wallet): def add_info_from_will(will, wid, wallet):
if isinstance(will[wid].tx, str): if isinstance(will[wid].tx, str):
will[wid].tx = Will.get_tx_from_any(will[wid].tx) will[wid].tx = Will.get_tx_from_any(will[wid].tx)
@@ -96,9 +89,7 @@ class Will:
txin._TxInput__value_sats = change.value txin._TxInput__value_sats = change.value
txin._trusted_value_sats = change.value txin._trusted_value_sats = change.value
@staticmethod def normalize_will(will, wallet=None, others_inputs={}):
def normalize_will(will, wallet=None, others_inputs=None):
others_input = others_inputs if others_inputs is not None else {}
to_delete = [] to_delete = []
to_add = {} to_add = {}
# add info from wallet # add info from wallet
@@ -147,7 +138,6 @@ class Will:
if wid in will: if wid in will:
del will[wid] del will[wid]
@staticmethod
def new_input(txid, idx, change): def new_input(txid, idx, change):
prevout = TxOutpoint(txid=bfh(txid), out_idx=idx) prevout = TxOutpoint(txid=bfh(txid), out_idx=idx)
inp = PartialTxInput(prevout=prevout) inp = PartialTxInput(prevout=prevout)
@@ -158,7 +148,16 @@ class Will:
inp._TxInput__value_sats = change.value inp._TxInput__value_sats = change.value
return inp 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"): def check_anticipate(ow: "WillItem", nw: "WillItem"):
anticipate = Util.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):
@@ -188,7 +187,6 @@ class Will:
return anticipate return anticipate
return 4294967295 + 1 return 4294967295 + 1
@staticmethod
def change_input(will, otxid, idx, change, others_inputs, to_delete, to_append): def change_input(will, otxid, idx, change, others_inputs, to_delete, to_append):
ow = will[otxid] ow = will[otxid]
ntxid = ow.tx.txid() ntxid = ow.tx.txid()
@@ -229,7 +227,6 @@ class Will:
to_append, to_append,
) )
@staticmethod
def get_all_inputs(will, only_valid=False): def get_all_inputs(will, only_valid=False):
all_inputs = {} all_inputs = {}
for w, wi in will.items(): for w, wi in will.items():
@@ -244,7 +241,6 @@ class Will:
all_inputs[prevout_str].append(inp) all_inputs[prevout_str].append(inp)
return all_inputs return all_inputs
@staticmethod
def get_all_inputs_min_locktime(all_inputs): def get_all_inputs_min_locktime(all_inputs):
all_inputs_min_locktime = {} all_inputs_min_locktime = {}
@@ -259,7 +255,6 @@ class Will:
return all_inputs_min_locktime return all_inputs_min_locktime
@staticmethod
def search_anticipate_rec(will, old_inputs): def search_anticipate_rec(will, old_inputs):
redo = False redo = False
to_delete = [] to_delete = []
@@ -299,7 +294,6 @@ class Will:
Will.search_anticipate_rec(will, old_inputs) Will.search_anticipate_rec(will, old_inputs)
@staticmethod
def update_will(old_will, new_will): def update_will(old_will, new_will):
all_old_inputs = Will.get_all_inputs(old_will, only_valid=True) 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_inputs_min_locktime = Will.get_all_inputs_min_locktime(all_old_inputs)
@@ -326,7 +320,6 @@ class Will:
else: else:
continue continue
@staticmethod
def get_higher_input_for_tx(will): def get_higher_input_for_tx(will):
out = {} out = {}
for wid in will: for wid in will:
@@ -340,7 +333,6 @@ class Will:
out[inp.prevout.to_str()] = inp out[inp.prevout.to_str()] = inp
return out return out
@staticmethod
def invalidate_will(will, wallet, fees_per_byte): def invalidate_will(will, wallet, fees_per_byte):
will_only_valid = Will.only_valid_list(will) will_only_valid = Will.only_valid_list(will)
inputs = Will.get_all_inputs(will_only_valid) inputs = Will.get_all_inputs(will_only_valid)
@@ -392,13 +384,11 @@ class Will:
_logger.debug("len utxo_to_spend <=0") _logger.debug("len utxo_to_spend <=0")
pass pass
@staticmethod
def is_new(will): def is_new(will):
for wid, w in will.items(): for wid, w in will.items():
if w.get_status("VALID") and not w.get_status("COMPLETE"): if w.get_status("VALID") and not w.get_status("COMPLETE"):
return True return True
@staticmethod
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():
@@ -432,25 +422,20 @@ class Will:
else: else:
pass pass
@staticmethod
def utxos_strs(utxos): def utxos_strs(utxos):
return [Util.utxo_to_str(u) for u in utxos] return [Util.utxo_to_str(u) for u in utxos]
@staticmethod def set_invalidate(wid, will=[]):
def set_invalidate(wid, will=None):
will = will if will is not None else {}
will[wid].set_status("INVALIDATED", True) will[wid].set_status("INVALIDATED", True)
if will[wid].children: if will[wid].children:
for c in will[wid].children.items(): for c in will[wid].children.items():
Will.set_invalidate(c[0], will) Will.set_invalidate(c[0], will)
@staticmethod
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
@staticmethod
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 ( if (
@@ -483,7 +468,6 @@ class Will:
# if wc.children: # if wc.children:
# Will.reflect_to_children(wc) # Will.reflect_to_children(wc)
@staticmethod
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_amount_with_dust = ( 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)
@@ -507,7 +491,6 @@ class Will:
f"Willexecutor{url} excess base fee({wex['base_fee']}), {fixed_amount} >={temp_balance}" 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): def check_will(will, all_utxos, wallet, block_to_check, timestamp_to_check):
Will.add_willtree(will) Will.add_willtree(will)
utxos_list = Will.utxos_strs(all_utxos) utxos_list = Will.utxos_strs(all_utxos)
@@ -524,28 +507,18 @@ class Will:
Will.search_rai(all_inputs, all_utxos, will, wallet) 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( def is_will_valid(
will, will,
block_to_check, block_to_check,
timestamp_to_check, timestamp_to_check,
tx_fees, tx_fees,
all_utxos, all_utxos,
heirs=None, heirs={},
willexecutors=None, willexecutors={},
self_willexecutor=False, self_willexecutor=False,
wallet=False, wallet=False,
callback_not_valid_tx=None, 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) 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(
@@ -574,7 +547,6 @@ class Will:
_logger.info("will ok") _logger.info("will ok")
return True return True
@staticmethod
def check_will_expired(all_inputs_min_locktime, block_to_check, timestamp_to_check): def check_will_expired(all_inputs_min_locktime, block_to_check, timestamp_to_check):
_logger.info("check if some transaction is expired") _logger.info("check if some transaction is expired")
for prevout_str, wid in all_inputs_min_locktime.items(): for prevout_str, wid in all_inputs_min_locktime.items():
@@ -606,7 +578,6 @@ class Will:
# if not parentwill or not parentwill.get_status("VALID"): # if not parentwill or not parentwill.get_status("VALID"):
# w[1].set_status("INVALIDATED", True) # w[1].set_status("INVALIDATED", True)
@staticmethod
def only_valid_list(will): def only_valid_list(will):
out = {} out = {}
for wid, w in will.items(): for wid, w in will.items():
@@ -614,7 +585,6 @@ class Will:
out[wid] = w out[wid] = w
return out return out
@staticmethod
def only_valid_or_replaced_list(will): def only_valid_or_replaced_list(will):
out = [] out = []
for wid, w in will.items(): for wid, w in will.items():
@@ -623,7 +593,6 @@ class Will:
out.append(wid) out.append(wid)
return out return out
@staticmethod
def check_willexecutors_and_heirs( def check_willexecutors_and_heirs(
will, heirs, willexecutors, self_willexecutor, check_date, tx_fees will, heirs, willexecutors, self_willexecutor, check_date, tx_fees
): ):
@@ -637,7 +606,7 @@ class Will:
for wid in Will.only_valid_list(will): for wid in Will.only_valid_list(will):
w = will[wid] w = will[wid]
if w.tx_fees != tx_fees: 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: for wheir in w.heirs:
if not 'w!ll3x3c"' == wheir[:9]: if not 'w!ll3x3c"' == wheir[:9]:
their = will[wid].heirs[wheir] their = will[wid].heirs[wheir]
@@ -686,7 +655,6 @@ class Will:
return True return True
class WillItem(Logger): class WillItem(Logger):
STATUS_DEFAULT = { STATUS_DEFAULT = {
"ANTICIPATED": ["Anticipated", False], "ANTICIPATED": ["Anticipated", False],

View File

@@ -1,8 +1,9 @@
import json import json
import time
from datetime import datetime from datetime import datetime
import time
from aiohttp import ClientResponse from aiohttp import ClientResponse
from electrum import constants
from electrum.i18n import _ from electrum.i18n import _
from electrum.logging import get_logger from electrum.logging import get_logger
from electrum.network import Network from electrum.network import Network
@@ -13,12 +14,11 @@ DEFAULT_TIMEOUT = 5
_logger = get_logger(__name__) _logger = get_logger(__name__)
chainname = BalPlugin.chainname chainname = constants.net.NET_NAME if constants.net.NET_NAME != "mainnet" else "bitcoin"
class Willexecutors: class Willexecutors:
@staticmethod
def save(bal_plugin, willexecutors): def save(bal_plugin, willexecutors):
_logger.debug(f"save {willexecutors},{chainname}") _logger.debug(f"save {willexecutors},{chainname}")
aw = bal_plugin.WILLEXECUTORS.get() aw = bal_plugin.WILLEXECUTORS.get()
@@ -27,7 +27,6 @@ class Willexecutors:
_logger.debug(f"saved: {aw}") _logger.debug(f"saved: {aw}")
# bal_plugin.WILLEXECUTORS.set(willexecutors) # bal_plugin.WILLEXECUTORS.set(willexecutors)
@staticmethod
def get_willexecutors( def get_willexecutors(
bal_plugin, update=False, bal_window=False, force=False, task=True bal_plugin, update=False, bal_window=False, force=False, task=True
): ):
@@ -79,7 +78,6 @@ class Willexecutors:
) )
return w_sorted return w_sorted
@staticmethod
def is_selected(willexecutor, value=None): def is_selected(willexecutor, value=None):
if not willexecutor: if not willexecutor:
return False return False
@@ -91,7 +89,6 @@ class Willexecutors:
willexecutor["selected"] = False willexecutor["selected"] = False
return False return False
@staticmethod
def get_willexecutor_transactions(will, force=False): def get_willexecutor_transactions(will, force=False):
willexecutors = {} willexecutors = {}
for wid, willitem in will.items(): for wid, willitem in will.items():
@@ -127,7 +124,6 @@ class Willexecutors:
# willexecutors[url]["txs"], url # willexecutors[url]["txs"], url
# ) # )
@staticmethod
def send_request( def send_request(
method, url, data=None, *, timeout=10, handle_response=None, count_reply=0 method, url, data=None, *, timeout=10, handle_response=None, count_reply=0
): ):
@@ -181,14 +177,12 @@ class Willexecutors:
_logger.debug(f"--> {response}") _logger.debug(f"--> {response}")
return response return response
@staticmethod
def get_we_url_from_response(resp): def get_we_url_from_response(resp):
url_slices = str(resp.url).split("/") url_slices = str(resp.url).split("/")
if len(url_slices) > 2: if len(url_slices) > 2:
url_slices = url_slices[:-2] url_slices = url_slices[:-2]
return "/".join(url_slices) return "/".join(url_slices)
@staticmethod
async def handle_response(resp: ClientResponse): async def handle_response(resp: ClientResponse):
r = await resp.text() r = await resp.text()
try: try:
@@ -202,11 +196,9 @@ class Willexecutors:
pass pass
return r return r
@staticmethod
class AlreadyPresentException(Exception): class AlreadyPresentException(Exception):
pass pass
@staticmethod
def push_transactions_to_willexecutor(willexecutor): def push_transactions_to_willexecutor(willexecutor):
out = True out = True
try: try:
@@ -232,12 +224,10 @@ class Willexecutors:
return out return out
@staticmethod
def ping_servers(willexecutors): def ping_servers(willexecutors):
for url, we in willexecutors.items(): for url, we in willexecutors.items():
Willexecutors.get_info_task(url, we) Willexecutors.get_info_task(url, we)
@staticmethod
def get_info_task(url, willexecutor): def get_info_task(url, willexecutor):
w = None w = None
try: try:
@@ -258,9 +248,7 @@ class Willexecutors:
willexecutor["last_update"] = datetime.now().timestamp() willexecutor["last_update"] = datetime.now().timestamp()
return willexecutor return willexecutor
@staticmethod def initialize_willexecutor(willexecutor, url, status=None, old_willexecutor={}):
def initialize_willexecutor(willexecutor, url, status=None, old_willexecutor=None):
old_willexecutor=old_willexecutor if old_willexecutor is not None else {}
willexecutor["url"] = url willexecutor["url"] = url
if status is not None: if status is not None:
willexecutor["status"] = status willexecutor["status"] = status
@@ -272,13 +260,11 @@ class Willexecutors:
@staticmethod def download_list(old_willexecutors):
def download_list(old_willexecutors,welist_server):
try: try:
welist_server = welist_server if welist_server[-1] == '/' else welist_server+'/'
willexecutors = Willexecutors.send_request( willexecutors = Willexecutors.send_request(
"get", "get",
f"{welist_server}data/{chainname}?page=0&limit=100", f"https://welist.bitcoin-after.life/data/{chainname}?page=0&limit=100",
) )
# del willexecutors["status"] # del willexecutors["status"]
for w in willexecutors: for w in willexecutors:
@@ -294,7 +280,6 @@ class Willexecutors:
_logger.error(f"Failed to download willexecutors list: {e}") _logger.error(f"Failed to download willexecutors list: {e}")
return {} return {}
@staticmethod
def get_willexecutors_list_from_json(): def get_willexecutors_list_from_json():
try: try:
with open("willexecutors.json") as f: with open("willexecutors.json") as f:
@@ -309,7 +294,6 @@ class Willexecutors:
return {} return {}
@staticmethod
def check_transaction(txid, url): def check_transaction(txid, url):
_logger.debug(f"{url}:{txid}") _logger.debug(f"{url}:{txid}")
try: try:
@@ -321,54 +305,53 @@ class Willexecutors:
_logger.error(f"error contacting {url} for checking txs {e}") _logger.error(f"error contacting {url} for checking txs {e}")
raise e raise e
@staticmethod
def compute_id(willexecutor): def compute_id(willexecutor):
return "{}-{}".format(willexecutor.get("url"), willexecutor.get("chain")) return "{}-{}".format(willexecutor.get("url"), willexecutor.get("chain"))
#class WillExecutor: class WillExecutor:
# def __init__( def __init__(
# self, self,
# url, url,
# base_fee, base_fee,
# chain, chain,
# info, info,
# version, version,
# status, status,
# is_selected=False, is_selected=False,
# promo_code="", promo_code="",
# ): ):
# self.url = url self.url = url
# self.base_fee = base_fee self.base_fee = base_fee
# self.chain = chain self.chain = chain
# self.info = info self.info = info
# self.version = version self.version = version
# self.status = status self.status = status
# self.promo_code = promo_code self.promo_code = promo_code
# self.is_selected = is_selected self.is_selected = is_selected
# self.id = self.compute_id() self.id = self.compute_id()
#
# def from_dict(d): def from_dict(d):
# return WillExecutor( return WillExecutor(
# url=d.get("url", "http://localhost:8000"), url=d.get("url", "http://localhost:8000"),
# base_fee=d.get("base_fee", 1000), base_fee=d.get("base_fee", 1000),
# chain=d.get("chain", chainname), chain=d.get("chain", chainname),
# info=d.get("info", ""), info=d.get("info", ""),
# version=d.get("version", 0), version=d.get("version", 0),
# status=d.get("status", "Ko"), status=d.get("status", "Ko"),
# is_selected=d.get("is_selected", "False"), is_selected=d.get("is_selected", "False"),
# promo_code=d.get("promo_code", ""), promo_code=d.get("promo_code", ""),
# ) )
#
# def to_dict(self): def to_dict(self):
# return { return {
# "url": self.url, "url": self.url,
# "base_fee": self.base_fee, "base_fee": self.base_fee,
# "chain": self.chain, "chain": self.chain,
# "info": self.info, "info": self.info,
# "version": self.version, "version": self.version,
# "promo_code": self.promo_code, "promo_code": self.promo_code,
# } }
#
# def compute_id(self): def compute_id(self):
# return f"{self.url}-{self.chain}" return f"{self.url}-{self.chain}"