4 Commits

Author SHA1 Message Date
eea2497340 version 2026-01-04 16:29:03 -04:00
436c4a1348 version 2026-01-04 16:24:24 -04:00
f083d6a8f6 version 2026-01-04 16:02:30 -04:00
da937f2c1b refactor-022b.diff 2026-01-04 10:01:37 -04:00
16 changed files with 1520 additions and 2967 deletions

359
README.md
View File

@@ -1,357 +1,2 @@
# Bal Electrum Plugin # BalPlugin
Bitcoin After Life Electrum Plugin
**Bitcoin Electrum plugin for managing heir inheritance with locktime-based will execution**
---
## 📥 Installation
### Method 1: Install from Release (Recommended)
1. **Download the plugin**
- Go to: [https://bitcoin-after.life/gitea/bitcoinafterlife/bal-electrum-plugin/releases](https://bitcoin-after.life/gitea/bitcoinafterlife/bal-electrum-plugin/releases)
- Download the latest `bal-electrum-plugin-vX.X.X.zip` file
2. **Install in Electrum**
- Open Electrum Bitcoin wallet
- Go to **Tools → Plugins**
- Click **Add**
- Select the downloaded `.zip` file
- Click **Open** or **Install**
- Restart Electrum if required
### Method 2: Install from Source
```bash
git clone https://bitcoin-after.life/gitea/bitcoinafterlife/bal-electrum-plugin.git
cd bal-electrum-plugin
zip -r bal-electrum-plugin.zip bal_electrum_plugin/
```
Then install using Method 1, step 2.
---
## 🚀 How It Works
### Core Functionality
The plugin allows users to create a **Bitcoin will** that automatically executes under specific conditions:
1. **Define Heirs** - Create a list of beneficiaries with Bitcoin addresses and inheritance amounts
2. **Set Locktime** - Configure when transactions can be spent (timestamp or interval)
3. **Set CheckAlive** - Set health verification mechanism (interval or fixed date)
4. **Select Executor** - Choose which will-executor will handle the transaction
5. **Create Transaction** - Plugin generates a timelocked transaction sent to the executor
### Transaction Flow
```
User Setup → [Plugin Creates Transaction] → Executor Receives →
Executor Broadcasts at Locktime → Transaction Confirmed → Heirs Receive Funds
```
### Key Incentive
**Executor Fee is Included in Transaction**:
- The will-executor receives their fee directly in the transaction output
- Fee is paid only when transaction is confirmed on the Bitcoin network
- Executor is **financially incentivized** to broadcast and confirm the transaction
- Fee is a **fixed amount only** (no percentage option)
---
## 📁 Plugin Structure
```
bal-electrum-plugin/
├── bal_electrum_plugin/ # Main plugin directory (what to zip)
│ ├── __init__.py # Plugin initialization and entry point
│ ├── bal.py # Main plugin logic and core functionality
│ ├── bal_resources.py # Resource management and asset handling
│ ├── heirs.py # Heir management logic and validation
│ ├── qt.py # Main Qt interface and GUI components
│ ├── util.py # General utility functions
│ ├── wallet_util/
│ │ ├── bal_wallet_utils.py # ⚠️ Advanced wallet utilities for emergency fixes
│ │ └── bal_wallet_utils_qt.py # Qt-specific wallet utilities
│ ├── will.py # Will creation, locktime, and checkalive logic
│ └── willexecutors.py # Will executor management and fee handling
├── README.md # This file
└── LICENSE # MIT License
```
**Important**: Only the `bal_electrum_plugin/` directory needs to be zipped for installation.
---
## 🎯 Key Features
### Heir Management (heirs.py)
- **Multiple heirs**: Add unlimited beneficiaries
- **Flexible distribution**: Percentage-based or fixed amount
- **Address validation**: Verify Bitcoin addresses before saving
- **Distribution summary**: View total inheritance breakdown
- **Percentage validation**: Ensures percentages sum to 100%
### Locktime Configuration (will.py)
- **Timestamp format**: Unix timestamp (e.g., `1735689600` for Jan 1, 2025)
- **Interval format**: Days (`180d`) or years (`1y`)
- **Automatic conversion**: Intervals converted to timestamps internally
- **Validation**: Ensures locktime is in the future
- **Flexible timing**: Set exact date or relative intervals
### CheckAlive Mechanism (will.py)
- **Two verification modes**:
**Interval Mode**:
- Format: Same as locktime (e.g., `180d`, `2y`)
- Logic: `locktime - interval > current_time`
- Example: Locktime=1y, CheckAlive=180d → Valid if 365-180 > current days
**Fixed Date Mode**:
- Format: Timestamp
- Logic: `fixed_date > current_time`
- Example: CheckAlive=2025-12-31 → Valid if date is in the future
- **Effect**: If CheckAlive expires, the old will is invalidated
- **Safety mechanism**: Prevents stale wills from executing
### Will Executor System (willexecutors.py)
- **Multiple executors**: Choose from available executors
- **Fixed fee only**: Executor receives a fixed BTC amount (no percentage option)
- **Fee included in transaction**: Fee is part of the timelocked transaction output
- **Financial motivation**: Executor must broadcast to get paid
- **Automatic transmission**: Transaction sent to executor after creation
- **Blockchain monitoring**: Executor broadcasts at locktime expiration
**Executor Fee Details**:
- Fee is specified as fixed BTC amount only
- Fee is added to transaction outputs
- Executor receives fee only when transaction confirms
- No separate payment channel needed
### Advanced Wallet Utilities (wallet_util/)
- **⚠️ Emergency tools only**: For fixing wallet database issues
- **Purpose**: Handle compatibility across different Electrum walletdb versions
- **Usage**: Advanced users only, requires manual installation
- **Installation Steps**:
1. Copy `wallet_util/` files to Electrum's plugin directory
2. Load Electrum's virtualenv
3. Run `./bal_wallet_utils_qt.py`
- **Risk**: Advanced operations that can affect wallet data
- **Documentation**: Limited to inline code comments
**Normal operation does NOT require wallet_util/ files.**
### Main Interface (qt.py)
- **User-friendly wizards**: Step-by-step setup interface
- **Real-time validation**: Immediate feedback on inputs
- **Transaction preview**: Review before finalizing
- **Status monitoring**: Track will execution progress
- **Error handling**: Clear messages for invalid inputs
---
## 📖 Usage Examples
### Example 1: Simple Inheritance Will
**Scenario**: Leave 1 BTC to three heirs after 1 year, with 180-day CheckAlive
**Configuration**:
```
Heirs:
- Heir 1: bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq - 40% (0.4 BTC)
- Heir 2: bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 - 35% (0.35 BTC)
- Heir 3: bc1ql4wk3ym38m8p3kns5w5q4tech38x58s3yy263q - 25% (0.25 BTC)
Locktime: 1y (1 year from now)
CheckAlive: 180d (180 days before locktime)
Executor: "Alice Executor" (fee: 0.01 BTC fixed)
```
**Transaction Created**:
- Input: All UTXOs in wallet (1.0 BTC total)
- Outputs:
- Heir 1: 0.4 BTC
- Heir 2: 0.35 BTC
- Heir 3: 0.25 BTC
- Executor fee: 0.01 BTC (fixed amount)
- Network fee: 0.001 BTC
- Locktime: 1 year from creation
- Sent to: Alice Executor's address
**Executor Incentive**:
- Alice receives 0.01 BTC **only if** she broadcasts and transaction confirms
- Alice has financial motivation to ensure transaction is confirmed
- Transaction cannot be spent until locktime expires
### Example 2: Fixed Date CheckAlive
**Scenario**: Leave funds with CheckAlive on specific date
**Configuration**:
```
Locktime: 2026-06-01 (timestamp: 1748832000)
CheckAlive: 2025-12-31 (timestamp: 1767187200)
Heirs: 50% to heir1, 50% to heir2
Executor: "Bob Executor" (fee: 0.005 BTC fixed)
```
**Logic**:
- If today > 2025-12-31 → Old will is invalid
- If old will is invalid, a non-timelocked transaction is sent onchain to invalidate the old will
- Transaction executes on 2026-06-01
- Bob receives 0.005 BTC only when transaction confirms
- Bob is incentivized to broadcast at correct time
### Example 3: Complex Distribution with Fixed Amounts
**Scenario**: Leave different fixed amounts to heirs
**Configuration**:
```
Heirs:
- Heir 1: bc1... - 0.3 BTC (fixed amount)
- Heir 2: bc1... - 0.5 BTC (fixed amount)
- Heir 3: bc1... - 0.2 BTC (fixed amount)
Total: 1.0 BTC
Locktime: 365d (1 year from now)
CheckAlive: 180d (6 months before locktime)
Executor: "Charlie Executor" (fee: 0.02 BTC fixed)
```
---
## 🛠️ Development
### Prerequisites
- Electrum Bitcoin wallet
### Setup Development Environment
```bash
# Clone repository
git clone https://bitcoin-after.life/gitea/bitcoinafterlife/bal-electrum-plugin.git
cd bal-electrum-plugin
# Create development zip
zip -r bal-electrum-plugin-dev.zip bal_electrum_plugin/
```
### Important Notes on wallet_util/
**These utilities require manual installation**:
- **Purpose**: Fix wallet database compatibility issues across Electrum versions
- **Usage**: Only when experiencing wallet corruption or version mismatches
- **Installation Steps**:
1. Copy `wallet_util/` files to Electrum's plugin directory
2. Load Electrum's virtualenv
3. Run `./bal_wallet_utils_qt.py`
- **Risk**: Advanced operations that can affect wallet data
- **Documentation**: Limited to inline code comments
**Normal operation does NOT require wallet_util/ files.**
### Install for Development
1. Copy zip to Electrum plugins directory:
```bash
cp bal-electrum-plugin-dev.zip ~/.electrum/plugins/
```
2. Install in Electrum (Tools → Plugins → Add)
3. Make changes to files in `bal_electrum_plugin/` directory
4. Re-zip and reinstall to test
### Running the Plugin
The plugin uses Electrum's existing virtualenv.
---
## 🐛 Troubleshooting
### Common Issues
#### Plugin Not Showing in Electrum
- ✅ Check zip contains `bal_electrum_plugin/` directory at root
- ✅ Verify directory structure inside zip is correct
- ✅ Restart Electrum completely after installation
- ✅ Check Electrum logs: Help → Debug → Console
#### Locktime Format Errors
- ✅ Use valid formats: `180d`, `1y`, or Unix timestamp (e.g., `1735689600`)
- ✅ Ensure locktime is in the future (after current time)
- ✅ Check for typos: lowercase `d` and `y` only
- ✅ Verify interval calculations: `1y` = 365 days
#### CheckAlive Not Working
- ✅ Verify CheckAlive mode setting (interval or fixed_date)
- ✅ Ensure CheckAlive value is before Locktime value
- ✅ For interval mode: Check logic `locktime - interval > now`
- ✅ For fixed date: Verify timestamp is in the future
- ✅ If CheckAlive expires, old will is invalidated by sending a non-timelocked transaction onchain
#### Heir Distribution Errors
- ✅ Verify percentages sum to exactly 100%
- ✅ For fixed amounts: Ensure total doesn't exceed wallet balance
- ✅ Check Bitcoin addresses are valid (use testnet for testing)
- ✅ Ensure no duplicate addresses in heir list
- ✅ Verify address format: bc1... for native segwit, 1... for legacy
#### Transaction Creation Fails
- ✅ Check wallet has sufficient funds (including fees)
- ✅ Verify all heirs have valid, unique addresses
- ✅ Ensure locktime format is correct
- ✅ Check executor is selected and available
- ✅ Verify executor fee is properly configured (fixed amount only)
#### Executor Fee Issues
- ✅ Fee is a fixed BTC amount only (no percentage option)
- ✅ Fee is included in transaction output
- ✅ Executor receives fee only when transaction confirms
- ✅ Transaction must be broadcast and confirmed for executor to get paid
#### wallet_util/ Not Working
- ✅ Copy files to Electrum's plugin directory
- ✅ Load Electrum's virtualenv before using
- ✅ Run `./bal_wallet_utils_qt.py`
- ✅ Only use for emergency wallet fixes
- ✅ Backup wallet before using wallet utilities
---
## 🤝 Contributing
Thanks for considering contributing!
---
## 📜 License
MIT License - see [LICENSE](LICENSE) for details.
---
## 🔗 Links
- **Repository**: [https://bitcoin-after.life/gitea/bitcoinafterlife/bal-electrum-plugin](https://bitcoin-after.life/gitea/bitcoinafterlife/bal-electrum-plugin)
- **Releases**: [https://bitcoin-after.life/gitea/bitcoinafterlife/bal-electrum-plugin/releases](https://bitcoin-after.life/gitea/bitcoinafterlife/bal-electrum-plugin/releases)
- **Issues**: [https://bitcoin-after.life/gitea/bitcoinafterlife/bal-electrum-plugin/issues](https://bitcoin-after.like/bitcoinafterlife/bal-electrum-plugin/issues)
- **Discussions**: [https://bitcoin-after.life/gitea/bitcoinafterlife/bal-electrum-plugin/discussions](https://bitcoin-after.life/gitea/bitcoinafterlife/bal-electrum-plugin/discussions)
---
**⚠️ Important**: Always test with small amounts first. This plugin deals with Bitcoin transactions that may be irreversible.
**🔒 Security**: Never share your seed phrase or private keys. This plugin only creates transactions, it doesn't store your keys.
**💡 Note**: The executor fee is a fixed BTC amount included in the transaction output. The executor is financially incentivized to broadcast the transaction and ensure it confirms on the Bitcoin network.

View File

@@ -1 +1 @@
0.2.8 0.2.2c

156
bal.py
View File

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

View File

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

61
bal_wallet_utils.py Executable file
View File

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

View File

@@ -1,23 +1,21 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
#this script will help to fix tx_fees wallet bug
#also added an uninstall button to remove any bal data from wallet
#still very work in progress.
#
#have to be executed from electrum source code directory in example /home/user/projects/electrum/
#
import sys import sys
import os import os
import json import json
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (QApplication, QMainWindow, QVBoxLayout, QHBoxLayout,
QApplication, QLabel, QLineEdit, QPushButton, QWidget, QFileDialog,
QMainWindow, QGroupBox, QTextEdit)
QVBoxLayout, from PyQt6.QtCore import Qt
QHBoxLayout,
QLabel,
QLineEdit,
QPushButton,
QWidget,
QFileDialog,
QGroupBox,
QTextEdit,
)
from electrum.storage import WalletStorage from electrum.storage import WalletStorage
from bal_wallet_utils import fix_will_settings_tx_fees, uninstall_bal, save from electrum.util import MyEncoder
default_fees = 100
class WalletUtilityGUI(QMainWindow): class WalletUtilityGUI(QMainWindow):
def __init__(self): def __init__(self):
@@ -25,7 +23,7 @@ class WalletUtilityGUI(QMainWindow):
self.initUI() self.initUI()
def initUI(self): def initUI(self):
self.setWindowTitle("BAL Wallet Utility") self.setWindowTitle('BAL Wallet Utility')
self.setFixedSize(500, 400) self.setFixedSize(500, 400)
# Central widget # Central widget
@@ -94,7 +92,10 @@ class WalletUtilityGUI(QMainWindow):
def browse_wallet(self): def browse_wallet(self):
file_path, _ = QFileDialog.getOpenFileName( file_path, _ = QFileDialog.getOpenFileName(
self, "Select Wallet", "*", "Electrum Wallet (*)" self,
"Select Wallet",
"",
"Electrum Wallet (*.dat)"
) )
if file_path: if file_path:
self.wallet_path_edit.setText(file_path) self.wallet_path_edit.setText(file_path)
@@ -109,14 +110,43 @@ class WalletUtilityGUI(QMainWindow):
def log_message(self, message): def log_message(self, message):
self.output_text.append(message) self.output_text.append(message)
def fix_will_settings_tx_fees(self, json_wallet):
tx_fees = json_wallet.get('will_settings',{}).get('tx_fees',False)
if tx_fees:
json_wallet['will_settings']['baltx_fees'] = json_wallet.get('will_settings',{}).get('tx_fees', default_fees)
del json_wallet['will_settings']['tx_fees']
return True
return False
def uninstall_bal(self, json_wallet):
if 'will_settings' in json_wallet:
del json_wallet['will_settings']
if 'will' in json_wallet:
del json_wallet['will']
if 'heirs' in json_wallet:
del json_wallet['heirs']
return True
def save_wallet(self, json_wallet, storage):
try:
human_readable = not storage.is_encrypted()
storage.write(json.dumps(
json_wallet,
indent=4 if human_readable else None,
sort_keys=bool(human_readable),
cls=MyEncoder,
))
return True
except Exception as e:
self.log_message(f"Save error: {str(e)}")
return False
def fix_wallet(self): def fix_wallet(self):
self.process_wallet("fix") self.process_wallet('fix')
def uninstall_wallet(self): def uninstall_wallet(self):
self.log_message( self.log_message("WARNING: This will remove all BAL settings. This operation cannot be undone.")
"WARNING: This will remove all BAL settings. This operation cannot be undone." self.process_wallet('uninstall')
)
self.process_wallet("uninstall")
def process_wallet(self, command): def process_wallet(self, command):
wallet_path = self.wallet_path_edit.text().strip() wallet_path = self.wallet_path_edit.text().strip()
@@ -138,9 +168,7 @@ class WalletUtilityGUI(QMainWindow):
# Decrypt if necessary # Decrypt if necessary
if storage.is_encrypted(): if storage.is_encrypted():
if not password: if not password:
self.log_message( self.log_message("ERROR: Wallet is encrypted, please enter password")
"ERROR: Wallet is encrypted, please enter password"
)
return return
try: try:
@@ -152,31 +180,24 @@ class WalletUtilityGUI(QMainWindow):
# Read wallet # Read wallet
data = storage.read() data = storage.read()
json_wallet = json.loads("[" + data + "]")[0] json_wallet = json.loads(data)
have_to_save = False have_to_save = False
message = "" message = ""
if command == "fix": if command == 'fix':
have_to_save = fix_will_settings_tx_fees(json_wallet) have_to_save = self.fix_will_settings_tx_fees(json_wallet)
message = ( message = "Fix applied successfully" if have_to_save else "No fix needed"
"Fix applied successfully" if have_to_save else "No fix needed"
)
elif command == "uninstall": elif command == 'uninstall':
have_to_save = uninstall_bal(json_wallet) have_to_save = self.uninstall_bal(json_wallet)
message = ( message = "BAL uninstalled successfully" if have_to_save else "No BAL settings found to uninstall"
"BAL uninstalled successfully"
if have_to_save
else "No BAL settings found to uninstall"
)
if have_to_save: if have_to_save:
try: if self.save_wallet(json_wallet, storage):
save(json_wallet, storage)
self.log_message(f"SUCCESS: {message}") self.log_message(f"SUCCESS: {message}")
except Exception as e: else:
self.log_message(f"Save error: {str(e)}") self.log_message("ERROR: Failed to save wallet")
else: else:
self.log_message(f"INFO: {message}") self.log_message(f"INFO: {message}")
@@ -184,15 +205,21 @@ class WalletUtilityGUI(QMainWindow):
error_msg = f"ERROR: Processing failed: {str(e)}" error_msg = f"ERROR: Processing failed: {str(e)}"
self.log_message(error_msg) self.log_message(error_msg)
def main(): def main():
app = QApplication(sys.argv) app = QApplication(sys.argv)
# Check if dependencies are available
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 = WalletUtilityGUI()
window.show() window.show()
return app.exec() return app.exec()
if __name__ == '__main__':
if __name__ == "__main__":
sys.exit(main()) sys.exit(main())

285
heirs.py
View File

@@ -1,40 +1,25 @@
# import datetime
# import json # import json
import math import math
# import datetime
# import urllib.request
# import urllib.parse
import random import random
import re import re
import threading import threading
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 import dns
from dns.exception import DNSException from dns.exception import DNSException
from electrum import ( from electrum import bitcoin, constants, descriptor, dnssec
bitcoin,
constants,
# descriptor,
dnssec,
)
from electrum.logging import Logger, get_logger from electrum.logging import Logger, get_logger
from electrum.transaction import ( from electrum.transaction import (
PartialTransaction, PartialTransaction,
PartialTxInput, PartialTxInput,
PartialTxOutput, PartialTxOutput,
TxOutput,
TxOutpoint, TxOutpoint,
# TxOutput, TxOutput,
) )
from electrum.payment_identifier import PaymentIdentifier
from electrum.util import ( from electrum.util import (
bfh, bfh,
read_json_file, read_json_file,
@@ -43,13 +28,12 @@ from electrum.util import (
write_json_file, write_json_file,
) )
from .util import Util from .util import *
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
from .wallet_db import WalletDB
# from .wallet_db import WalletDB
_logger = get_logger(__name__) _logger = get_logger(__name__)
@@ -58,7 +42,6 @@ HEIR_ADDRESS = 0
HEIR_AMOUNT = 1 HEIR_AMOUNT = 1
HEIR_LOCKTIME = 2 HEIR_LOCKTIME = 2
HEIR_REAL_AMOUNT = 3 HEIR_REAL_AMOUNT = 3
HEIR_DUST_AMOUNT = 4
TRANSACTION_LABEL = "inheritance transaction" TRANSACTION_LABEL = "inheritance transaction"
@@ -72,22 +55,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(
@@ -96,45 +85,38 @@ def prepare_transactions(locktimes, available_utxos, fees, wallet):
x.value_sats(), x.prevout.txid, x.prevout.out_idx x.value_sats(), x.prevout.txid, x.prevout.out_idx
), ),
) )
# total_used_utxos = [] total_used_utxos = []
txsout = {} txsout = {}
locktime, _ = Util.get_lowest_locktimes(locktimes) locktime, _ = get_lowest_locktimes(locktimes)
if not locktime: if not locktime:
_logger.info("prepare transactions, no locktime")
return return
locktime = locktime[0] locktime = locktime[0]
heirs = locktimes[locktime] heirs = locktimes[locktime]
true = True vero = True
while true: while vero:
true = False vero = False
fee = fees.get(locktime, 0) fee = fees.get(locktime, 0)
out_amount = fee out_amount = fee
description = "" description = ""
outputs = [] outputs = []
paid_heirs = {} paid_heirs = {}
for name, heir in heirs.items(): for name, heir in heirs.items():
if len(heir) > HEIR_REAL_AMOUNT and "DUST" not in str(
heir[HEIR_REAL_AMOUNT]
):
try: try:
if len(heir) > HEIR_REAL_AMOUNT:
real_amount = heir[HEIR_REAL_AMOUNT] real_amount = heir[HEIR_REAL_AMOUNT]
out_amount += real_amount
description += f"{name}\n"
paid_heirs[name] = heir
outputs.append( outputs.append(
PartialTxOutput.from_address_and_value( PartialTxOutput.from_address_and_value(
heir[HEIR_ADDRESS], real_amount heir[HEIR_ADDRESS], real_amount
) )
) )
out_amount += real_amount else:
description += f"{name}\n" pass
except BitcoinException as e: except Exception as e:
_logger.info("exception decoding output {} - {}".format(type(e), e))
heir[HEIR_REAL_AMOUNT] = e
except Exception as e:
heir[HEIR_REAL_AMOUNT] = e
_logger.error(f"error preparing transactions: {e}")
pass pass
paid_heirs[name] = heir
in_amount = 0.0 in_amount = 0.0
used_utxos = [] used_utxos = []
@@ -143,36 +125,24 @@ def prepare_transactions(locktimes, available_utxos, fees, wallet):
value = utxo.value_sats() value = utxo.value_sats()
in_amount += value in_amount += value
used_utxos.append(utxo) used_utxos.append(utxo)
if in_amount >= out_amount: if in_amount > out_amount:
break break
except IndexError as e: except IndexError as e:
_logger.error(
f"error preparing transactions index error {e} {in_amount}, {out_amount}"
)
pass pass
if int(in_amount) < int(out_amount): if int(in_amount) < int(out_amount):
_logger.error( break
"error preparing transactions in_amount < out_amount ({} < {}) "
)
continue
heirsvalue = out_amount heirsvalue = out_amount
change = get_change_output(wallet, in_amount, out_amount, fee) change = get_change_output(wallet, in_amount, out_amount, fee)
if change: if change:
outputs.append(change) outputs.append(change)
for i in range(0, 100): for i in range(0, 100):
random.shuffle(outputs) random.shuffle(outputs)
print(outputs)
#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,
locktime=Util.parse_locktime_string(locktime, wallet), locktime=parse_locktime_string(locktime, wallet),
version=2, version=2,
) )
if len(description) > 0: if len(description) > 0:
@@ -216,7 +186,7 @@ def get_utxos_from_inputs(tx_inputs, tx, utxos):
# TODO calculate de minimum inputs to be invalidated # TODO calculate de minimum inputs to be invalidated
def invalidate_inheritance_transactions(wallet): def invalidate_inheritance_transactions(wallet):
# listids = [] listids = []
utxos = {} utxos = {}
dtxs = {} dtxs = {}
for k, v in wallet.get_all_labels().items(): for k, v in wallet.get_all_labels().items():
@@ -245,7 +215,7 @@ def invalidate_inheritance_transactions(wallet):
for key, value in utxos: for key, value in utxos:
for tx in value["txs"]: for tx in value["txs"]:
txid = tx.txid() txid = tx.txid()
if txid not in invalidated: if not txid in invalidated:
invalidated.append(tx.txid()) invalidated.append(tx.txid())
remaining[key] = value remaining[key] = value
@@ -253,10 +223,10 @@ def invalidate_inheritance_transactions(wallet):
def print_transaction(heirs, tx, locktimes, tx_fees): def print_transaction(heirs, tx, locktimes, tx_fees):
jtx = tx.to_json() jtx = tx.to_json()
print(f"TX: {tx.txid()}\t-\tLocktime: {jtx['locktime']}") print(f"TX: {tx.txid()}\t-\tLocktime: {jtx['locktime']}")
print("---") print(f"---")
for inp in jtx["inputs"]: for inp in jtx["inputs"]:
print(f"{inp['address']}: {inp['value_sats']}") print(f"{inp['address']}: {inp['value_sats']}")
print("---") print(f"---")
for out in jtx["outputs"]: for out in jtx["outputs"]:
heirname = "" heirname = ""
for key in heirs.keys(): for key in heirs.keys():
@@ -278,7 +248,7 @@ def print_transaction(heirs, tx, locktimes, tx_fees):
print() print()
try: try:
print(tx.serialize_to_network()) print(tx.serialize_to_network())
except Exception: except:
print("impossible to serialize") print("impossible to serialize")
print() print()
@@ -293,15 +263,13 @@ def get_change_output(wallet, in_amount, out_amount, fee):
class Heirs(dict, Logger): class Heirs(dict, Logger):
def __init__(self, db: "WalletDB"):
def __init__(self, wallet):
Logger.__init__(self) Logger.__init__(self)
self.db = wallet.db self.db = db
self.wallet = wallet
d = self.db.get("heirs", {}) d = self.db.get("heirs", {})
try: try:
self.update(d) self.update(d)
except Exception: except e as Exception:
return return
def invalidate_transactions(self, wallet): def invalidate_transactions(self, wallet):
@@ -332,10 +300,10 @@ class Heirs(dict, Logger):
def get_locktimes(self, from_locktime, a=False): def get_locktimes(self, from_locktime, a=False):
locktimes = {} locktimes = {}
for key in self.keys(): for key in self.keys():
locktime = Util.parse_locktime_string(self[key][HEIR_LOCKTIME]) locktime = parse_locktime_string(self[key][HEIR_LOCKTIME])
if locktime > from_locktime and not a or locktime <= from_locktime and a: if locktime > from_locktime and not a or locktime <= from_locktime and a:
locktimes[int(locktime)] = None locktimes[int(locktime)] = None
return list(locktimes.keys()) return locktimes.keys()
def check_locktime(self): def check_locktime(self):
return False return False
@@ -349,8 +317,6 @@ class Heirs(dict, Logger):
column = HEIR_AMOUNT column = HEIR_AMOUNT
if real: if real:
column = HEIR_REAL_AMOUNT column = HEIR_REAL_AMOUNT
if "DUST" in str(v[column]):
column = HEIR_DUST_AMOUNT
value = int( value = int(
math.floor( math.floor(
total_balance total_balance
@@ -361,10 +327,6 @@ class Heirs(dict, Logger):
if value > wallet.dust_threshold(): if value > wallet.dust_threshold():
heir_list[key].insert(HEIR_REAL_AMOUNT, value) heir_list[key].insert(HEIR_REAL_AMOUNT, value)
amount += value amount += value
else:
heir_list[key].insert(HEIR_REAL_AMOUNT, f"DUST: {value}")
heir_list[key].insert(HEIR_DUST_AMOUNT, value)
_logger.info(f"{key}, {value} is dust will be ignored")
except Exception as e: except Exception as e:
raise e raise e
@@ -373,10 +335,10 @@ class Heirs(dict, Logger):
def amount_to_float(self, amount): def amount_to_float(self, amount):
try: try:
return float(amount) return float(amount)
except Exception: except:
try: try:
return float(amount[:-1]) return float(amount[:-1])
except Exception: except:
return 0.0 return 0.0
def fixed_percent_lists_amount(self, from_locktime, dust_threshold, reverse=False): def fixed_percent_lists_amount(self, from_locktime, dust_threshold, reverse=False):
@@ -384,50 +346,29 @@ class Heirs(dict, Logger):
fixed_amount = 0.0 fixed_amount = 0.0
percent_heirs = {} percent_heirs = {}
percent_amount = 0.0 percent_amount = 0.0
fixed_amount_with_dust = 0.0
for key in self.keys(): for key in self.keys():
try: try:
cmp = ( cmp = parse_locktime_string(self[key][HEIR_LOCKTIME]) - from_locktime
Util.parse_locktime_string(self[key][HEIR_LOCKTIME]) - from_locktime
)
if cmp <= 0: if cmp <= 0:
_logger.debug(
"cmp < 0 {} {} {} {}".format(
cmp, key, self[key][HEIR_LOCKTIME], from_locktime
)
)
continue continue
if Util.is_perc(self[key][HEIR_AMOUNT]): if is_perc(self[key][HEIR_AMOUNT]):
percent_amount += float(self[key][HEIR_AMOUNT][:-1]) percent_amount += float(self[key][HEIR_AMOUNT][:-1])
percent_heirs[key] = list(self[key]) percent_heirs[key] = list(self[key])
else: else:
heir_amount = int(math.floor(float(self[key][HEIR_AMOUNT]))) heir_amount = int(math.floor(float(self[key][HEIR_AMOUNT])))
fixed_amount_with_dust += heir_amount
fixed_heirs[key] = list(self[key])
if heir_amount > dust_threshold: if heir_amount > dust_threshold:
fixed_amount += heir_amount fixed_amount += heir_amount
fixed_heirs[key] = list(self[key])
fixed_heirs[key].insert(HEIR_REAL_AMOUNT, heir_amount) fixed_heirs[key].insert(HEIR_REAL_AMOUNT, heir_amount)
else: else:
fixed_heirs[key] = list(self[key]) pass
fixed_heirs[key].insert(
HEIR_REAL_AMOUNT, f"DUST: {heir_amount}"
)
fixed_heirs[key].insert(HEIR_DUST_AMOUNT, heir_amount)
except Exception as e: except Exception as e:
_logger.error(e) _logger.error(e)
return ( return fixed_heirs, fixed_amount, percent_heirs, percent_amount
fixed_heirs,
fixed_amount,
percent_heirs,
percent_amount,
fixed_amount_with_dust,
)
def prepare_lists( def prepare_lists(
self, balance, total_fees, wallet, willexecutor=False, from_locktime=0 self, balance, total_fees, wallet, willexecutor=False, from_locktime=0
): ):
if balance<total_fees or balance < wallet.dust_threshold():
raise BalanceTooLowException(balance,wallet.dust_threshold(),total_fees)
willexecutors_amount = 0 willexecutors_amount = 0
willexecutors = {} willexecutors = {}
heir_list = {} heir_list = {}
@@ -436,7 +377,7 @@ class Heirs(dict, Logger):
locktimes = self.get_locktimes(from_locktime) locktimes = self.get_locktimes(from_locktime)
if willexecutor: if willexecutor:
for locktime in locktimes: for locktime in locktimes:
if int(Util.int_locktime(locktime)) > int(from_locktime): if int(int_locktime(locktime)) > int(from_locktime):
try: try:
base_fee = int(willexecutor["base_fee"]) base_fee = int(willexecutor["base_fee"])
willexecutors_amount += base_fee willexecutors_amount += base_fee
@@ -448,23 +389,19 @@ class Heirs(dict, Logger):
willexecutors[ willexecutors[
'w!ll3x3c"' + willexecutor["url"] + '"' + str(locktime) 'w!ll3x3c"' + willexecutor["url"] + '"' + str(locktime)
] = h ] = h
except Exception: except Exception as e:
return [], False return [], False
else: else:
(
_logger.error( _logger.error(
f"heir excluded from will locktime({locktime}){Util.int_locktime(locktime)}<minimum{from_locktime}" f"heir excluded from will locktime({locktime}){int_locktime(locktime)}<minimum{from_locktime}"
), ),
)
heir_list.update(willexecutors) heir_list.update(willexecutors)
newbalance -= willexecutors_amount newbalance -= willexecutors_amount
if newbalance < 0: fixed_heirs, fixed_amount, percent_heirs, percent_amount = (
raise WillExecutorFeeException(willexecutor) self.fixed_percent_lists_amount(from_locktime, wallet.dust_threshold())
( )
fixed_heirs,
fixed_amount,
percent_heirs,
percent_amount,
fixed_amount_with_dust,
) = self.fixed_percent_lists_amount(from_locktime, wallet.dust_threshold())
if fixed_amount > newbalance: if fixed_amount > newbalance:
fixed_amount = self.normalize_perc( fixed_amount = self.normalize_perc(
fixed_heirs, newbalance, fixed_amount, wallet fixed_heirs, newbalance, fixed_amount, wallet
@@ -474,43 +411,44 @@ class Heirs(dict, Logger):
heir_list.update(fixed_heirs) heir_list.update(fixed_heirs)
newbalance -= fixed_amount newbalance -= fixed_amount
if newbalance > 0: if newbalance > 0:
perc_amount = self.normalize_perc( perc_amount = self.normalize_perc(
percent_heirs, newbalance, percent_amount, wallet percent_heirs, newbalance, percent_amount, wallet
) )
newbalance -= perc_amount newbalance -= perc_amount
heir_list.update(percent_heirs) heir_list.update(percent_heirs)
if newbalance > 0: if newbalance > 0:
newbalance += fixed_amount newbalance += fixed_amount
fixed_amount = self.normalize_perc( fixed_amount = self.normalize_perc(
fixed_heirs, newbalance, fixed_amount_with_dust, wallet, real=True fixed_heirs, newbalance, fixed_amount, wallet, real=True
) )
newbalance -= fixed_amount newbalance -= fixed_amount
heir_list.update(fixed_heirs) heir_list.update(fixed_heirs)
heir_list = sorted( heir_list = sorted(
heir_list.items(), heir_list.items(),
key=lambda item: Util.parse_locktime_string(item[1][HEIR_LOCKTIME], wallet), key=lambda item: parse_locktime_string(item[1][HEIR_LOCKTIME], wallet),
) )
locktimes = {} locktimes = {}
for key, value in heir_list: for key, value in heir_list:
locktime = Util.parse_locktime_string(value[HEIR_LOCKTIME]) locktime = parse_locktime_string(value[HEIR_LOCKTIME])
if locktime not in locktimes: if not locktime in locktimes:
locktimes[locktime] = {key: value} locktimes[locktime] = {key: value}
else: else:
locktimes[locktime][key] = value locktimes[locktime][key] = value
return locktimes, onlyfixed return locktimes, onlyfixed
def is_perc(self, key): def is_perc(self, key):
return Util.is_perc(self[key][HEIR_AMOUNT]) return is_perc(self[key][HEIR_AMOUNT])
def buildTransactions( def buildTransactions(
self, bal_plugin, wallet, tx_fees=None, utxos=None, from_locktime=0 self, bal_plugin, wallet, tx_fees=None, utxos=None, from_locktime=0
): ):
Heirs._validate(self) Heirs._validate(self)
if len(self) <= 0: if len(self) <= 0:
_logger.info("while building transactions there was no heirs")
return return
balance = 0.0 balance = 0.0
len_utxo_set = 0 len_utxo_set = 0
@@ -526,7 +464,6 @@ class Heirs(dict, Logger):
len_utxo_set += 1 len_utxo_set += 1
available_utxos.append(utxo) available_utxos.append(utxo)
if len_utxo_set == 0: if len_utxo_set == 0:
_logger.info("no usable utxos")
return return
j = -2 j = -2
willexecutorsitems = list(willexecutors.items()) willexecutorsitems = list(willexecutors.items())
@@ -538,7 +475,7 @@ class Heirs(dict, Logger):
break break
elif 0 <= j: elif 0 <= j:
url, willexecutor = willexecutorsitems[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 continue
else: else:
willexecutor["url"] = url willexecutor["url"] = url
@@ -550,22 +487,17 @@ class Heirs(dict, Logger):
break break
fees = {} fees = {}
i = 0 i = 0
while i < 10: while True:
txs = {} txs = {}
redo = False redo = False
i += 1 i += 1
total_fees = 0 total_fees = 0
for fee in fees: for fee in fees:
total_fees += int(fees[fee]) total_fees += int(fees[fee])
# newbalance = balance newbalance = balance
try:
locktimes, onlyfixed = self.prepare_lists( locktimes, onlyfixed = self.prepare_lists(
balance, total_fees, wallet, willexecutor, from_locktime balance, total_fees, wallet, willexecutor, from_locktime
) )
except WillExecutorFeeException:
i = 10
continue
if locktimes:
try: try:
txs = prepare_transactions( txs = prepare_transactions(
locktimes, available_utxos[:], fees, wallet locktimes, available_utxos[:], fees, wallet
@@ -573,16 +505,11 @@ class Heirs(dict, Logger):
if not txs: if not txs:
return {} return {}
except Exception as e: except Exception as e:
_logger.error(
f"build transactions: error preparing transactions: {e}"
)
try: try:
if "w!ll3x3c" in e.heirname: if "w!ll3x3c" in e.heirname:
Willexecutors.is_selected( Willexecutors.is_selected(willexecutors[w], False)
e.heirname[len("w!ll3x3c") :], False
)
break break
except Exception: except:
raise e raise e
total_fees = 0 total_fees = 0
total_fees_real = 0 total_fees_real = 0
@@ -597,7 +524,7 @@ class Heirs(dict, Logger):
rfee = tx.input_value() - tx.output_value() rfee = tx.input_value() - tx.output_value()
if rfee < fee or rfee > fee + wallet.dust_threshold(): if rfee < fee or rfee > fee + wallet.dust_threshold():
redo = True redo = True
# oldfees = fees.get(tx.my_locktime, 0) oldfees = fees.get(tx.my_locktime, 0)
fees[tx.my_locktime] = fee fees[tx.my_locktime] = fee
if balance - total_in > wallet.dust_threshold(): if balance - total_in > wallet.dust_threshold():
@@ -606,11 +533,6 @@ class Heirs(dict, Logger):
break break
if i >= 10: if i >= 10:
break break
else:
_logger.info(
f"no locktimes for willexecutor {willexecutor} skipped"
)
break
alltxs.update(txs) alltxs.update(txs)
return alltxs return alltxs
@@ -710,13 +632,13 @@ class Heirs(dict, Logger):
return None return None
def validate_address(address): def validate_address(address):
if not bitcoin.is_address(address, net=constants.net): if not bitcoin.is_address(address):
raise NotAnAddress(f"not an address,{address}") raise NotAnAddress(f"not an address,{address}")
return address return address
def validate_amount(amount): def validate_amount(amount):
try: try:
famount = float(amount[:-1]) if Util.is_perc(amount) else float(amount) famount = float(amount[:-1]) if is_perc(amount) else float(amount)
if famount <= 0.00000001: if famount <= 0.00000001:
raise AmountNotValid(f"amount have to be positive {famount} < 0") raise AmountNotValid(f"amount have to be positive {famount} < 0")
except Exception as e: except Exception as e:
@@ -726,7 +648,7 @@ class Heirs(dict, Logger):
def validate_locktime(locktime, timestamp_to_check=False): def validate_locktime(locktime, timestamp_to_check=False):
try: try:
if timestamp_to_check: if timestamp_to_check:
if Util.parse_locktime_string(locktime, None) < timestamp_to_check: if parse_locktime_string(locktime, None) < timestamp_to_check:
raise HeirExpiredException() raise HeirExpiredException()
except Exception as e: except Exception as e:
raise LocktimeNotValid(f"locktime string not properly formatted, {e}") raise LocktimeNotValid(f"locktime string not properly formatted, {e}")
@@ -739,14 +661,12 @@ class Heirs(dict, Logger):
return (address, amount, locktime) return (address, amount, locktime)
def _validate(data, timestamp_to_check=False): def _validate(data, timestamp_to_check=False):
for k, v in list(data.items()): for k, v in list(data.items()):
if k == "heirs": if k == "heirs":
return Heirs._validate(v, timestamp_to_check) return Heirs._validate(v)
try: try:
Heirs.validate_heir(k, v, timestamp_to_check) Heirs.validate_heir(k, v)
except Exception as e: except Exception as e:
_logger.info(f"exception heir removed {e}")
data.pop(k) data.pop(k)
return data return data
@@ -765,28 +685,3 @@ class LocktimeNotValid(ValueError):
class HeirExpiredException(LocktimeNotValid): class HeirExpiredException(LocktimeNotValid):
pass pass
class HeirAmountIsDustException(Exception):
pass
class NoHeirsException(Exception):
pass
class WillExecutorFeeException(Exception):
def __init__(self, willexecutor):
self.willexecutor = willexecutor
def __str__(self):
return "WillExecutorFeeException: {} fee:{}".format(
self.willexecutor["url"], self.willexecutor["base_fee"]
)
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}"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

View File

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

1080
qt.py

File diff suppressed because it is too large Load Diff

755
util.py

File diff suppressed because it is too large Load Diff

View File

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

200
will.py
View File

@@ -1,11 +1,3 @@
# -*- coding: utf-8 -*-
# Will module for the BAL Electrum plugin.
# -*- coding: utf-8 -*-
# Will module for the BAL Electrum plugin.
import copy import copy
from electrum.bitcoin import NLOCKTIME_BLOCKHEIGHT_MAX from electrum.bitcoin import NLOCKTIME_BLOCKHEIGHT_MAX
@@ -20,10 +12,14 @@ from electrum.transaction import (
tx_from_any, tx_from_any,
) )
from electrum.util import ( from electrum.util import (
FileImportFailed,
bfh, bfh,
decimal_point_to_base_unit_name,
read_json_file,
write_json_file,
) )
from .util import Util from .util import *
from .willexecutors import Willexecutors from .willexecutors import Willexecutors
MIN_LOCKTIME = 1 MIN_LOCKTIME = 1
@@ -32,6 +28,7 @@ _logger = get_logger(__name__)
class Will: class Will:
# return an array with the list of children
def get_children(will, willid): def get_children(will, willid):
out = [] out = []
for _id in will: for _id in will:
@@ -63,7 +60,7 @@ class Will:
for w in will: for w in will:
if w != wid and not tx.to_json() != will[w]["tx"].to_json(): if w != wid and not tx.to_json() != will[w]["tx"].to_json():
if will[w]["tx"].txid() != tx.txid(): if will[w]["tx"].txid() != tx.txid():
if Util.cmp_txs(will[w]["tx"], tx): if cmp_txs(will[w]["tx"], tx):
return will[w]["tx"] return will[w]["tx"]
return False return False
@@ -85,11 +82,13 @@ class Will:
for txin in will[wid].tx.inputs(): for txin in will[wid].tx.inputs():
txid = txin.prevout.txid.hex() txid = txin.prevout.txid.hex()
if txid in will: if txid in will:
# print(will[txid].tx.outputs())
# print(txin.prevout.out_idx)
change = will[txid].tx.outputs()[txin.prevout.out_idx] change = will[txid].tx.outputs()[txin.prevout.out_idx]
txin._trusted_value_sats = change.value txin._trusted_value_sats = change.value
try: try:
txin.script_descriptor = change.script_descriptor txin.script_descriptor = change.script_descriptor
except Exception: except:
pass pass
txin.is_mine = True txin.is_mine = True
txin._TxInput__address = change.address txin._TxInput__address = change.address
@@ -108,7 +107,6 @@ class Will:
will = willitems will = willitems
errors = {} errors = {}
for wid in will: for wid in will:
txid = will[wid].tx.txid() txid = will[wid].tx.txid()
if txid is None: if txid is None:
@@ -136,8 +134,8 @@ class Will:
to_delete.append(wid) to_delete.append(wid)
to_add[ow.tx.txid()] = ow.to_dict() to_add[ow.tx.txid()] = ow.to_dict()
# for eid, err in errors.items(): for eid, err in errors.items():
# new_txid = err.tx.txid() new_txid = err.tx.txid()
for k, w in to_add.items(): for k, w in to_add.items():
will[k] = w will[k] = w
@@ -156,20 +154,10 @@ class Will:
inp._TxInput__value_sats = change.value inp._TxInput__value_sats = change.value
return inp return inp
"""
in questa situazione sono presenti due transazioni con id differente(quindi transazioni differenti)
per prima cosa controllo il locktime
se il locktime della nuova transazione e' maggiore del locktime della vecchia transazione, allora
confronto gli eredi, per locktime se corrispondono controllo i willexecutor
se hanno la stessa url ma le fee vecchie sono superiori alle fee nuove, allora anticipare.
"""
def check_anticipate(ow: "WillItem", nw: "WillItem"): def check_anticipate(ow: "WillItem", nw: "WillItem"):
anticipate = Util.anticipate_locktime(ow.tx.locktime, days=1) anticipate = anticipate_locktime(ow.tx.locktime, days=1)
if int(nw.tx.locktime) >= int(anticipate): if int(nw.tx.locktime) >= int(anticipate):
if Util.cmp_heirs_by_values( if cmp_heirs_by_values(
ow.heirs, nw.heirs, [0, 1], exclude_willexecutors=True ow.heirs, nw.heirs, [0, 1], exclude_willexecutors=True
): ):
if nw.we and ow.we: if nw.we and ow.we:
@@ -185,7 +173,7 @@ class Will:
ow.tx.locktime ow.tx.locktime
else: else:
if nw.we == ow.we: if nw.we == ow.we:
if not Util.cmp_heirs_by_values(ow.heirs, nw.heirs, [0, 3]): if not cmp_heirs_by_values(ow.heirs, nw.heirs, [0, 3]):
return anticipate return anticipate
else: else:
return ow.tx.locktime return ow.tx.locktime
@@ -205,7 +193,7 @@ class Will:
outputs = w.tx.outputs() outputs = w.tx.outputs()
found = False found = False
old_txid = w.tx.txid() old_txid = w.tx.txid()
# ntx = None ntx = None
for i in range(0, len(inputs)): for i in range(0, len(inputs)):
if ( if (
inputs[i].prevout.txid.hex() == otxid inputs[i].prevout.txid.hex() == otxid
@@ -216,7 +204,7 @@ class Will:
will[wid].tx.set_rbf(True) will[wid].tx.set_rbf(True)
will[wid].tx._inputs[i] = Will.new_input(wid, idx, change) will[wid].tx._inputs[i] = Will.new_input(wid, idx, change)
found = True found = True
if found: if found == True:
pass pass
new_txid = will[wid].tx.txid() new_txid = will[wid].tx.txid()
@@ -243,7 +231,7 @@ class Will:
for i in inputs: for i in inputs:
prevout_str = i.prevout.to_str() prevout_str = i.prevout.to_str()
inp = [w, will[w], i] inp = [w, will[w], i]
if prevout_str not in all_inputs: if not prevout_str in all_inputs:
all_inputs[prevout_str] = [inp] all_inputs[prevout_str] = [inp]
else: else:
all_inputs[prevout_str].append(inp) all_inputs[prevout_str].append(inp)
@@ -256,7 +244,7 @@ class Will:
min_locktime = min(values, key=lambda x: x[1].tx.locktime)[1].tx.locktime min_locktime = min(values, key=lambda x: x[1].tx.locktime)[1].tx.locktime
for w in values: for w in values:
if w[1].tx.locktime == min_locktime: 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] all_inputs_min_locktime[i] = [w]
else: else:
all_inputs_min_locktime[i].append(w) all_inputs_min_locktime[i].append(w)
@@ -269,7 +257,7 @@ class Will:
to_append = {} to_append = {}
new_inputs = Will.get_all_inputs(will, only_valid=True) new_inputs = Will.get_all_inputs(will, only_valid=True)
for nid, nwi in will.items(): for nid, nwi in will.items():
if nwi.search_anticipate(new_inputs): if nwi.search_anticipate(new_inputs) or nwi.search_anticipate(old_inputs):
if nid != nwi.tx.txid(): if nid != nwi.tx.txid():
redo = True redo = True
to_delete.append(nid) to_delete.append(nid)
@@ -279,36 +267,25 @@ class Will:
Will.change_input( Will.change_input(
will, nid, i, outputs[i], new_inputs, to_delete, to_append will, nid, i, outputs[i], new_inputs, to_delete, to_append
) )
if nwi.search_anticipate(old_inputs):
if nid != nwi.tx.txid():
redo = True
to_delete.append(nid)
to_append[nwi.tx.txid()] = nwi
outputs = nwi.tx.outputs()
for i in range(0, len(outputs)):
Will.change_input(
will, nid, i, outputs[i], new_inputs, to_delete, to_append
)
for w in to_delete: for w in to_delete:
try: try:
del will[w] del will[w]
except Exception: except:
pass pass
for k, w in to_append.items(): for k, w in to_append.items():
will[k] = w will[k] = w
if redo: if redo:
Will.search_anticipate_rec(will, old_inputs) Will.search_anticipate_rec(will, old_inputs)
def update_will(old_will, new_will): def update_will(old_will, new_will):
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)
# all_new_inputs = Will.get_all_inputs(new_will) all_new_inputs = Will.get_all_inputs(new_will)
# check if the new input is already spent by other transaction # check if the new input is already spent by other transaction
# if it is use the same locktime, or anticipate. # if it is use the same locktime, or anticipate.
Will.search_anticipate_rec(new_will, all_old_inputs) Will.search_anticipate_rec(new_will, all_old_inputs)
other_inputs = Will.get_all_inputs(old_will, {}) other_inputs = Will.get_all_inputs(old_will, {})
try: try:
Will.normalize_will(new_will, others_inputs=other_inputs) Will.normalize_will(new_will, others_inputs=other_inputs)
@@ -347,27 +324,25 @@ class Will:
utxos = wallet.get_utxos() utxos = wallet.get_utxos()
filtered_inputs = [] filtered_inputs = []
prevout_to_spend = [] prevout_to_spend = []
current_height = Util.get_current_height(wallet.network)
for prevout_str, ws in inputs.items(): for prevout_str, ws in inputs.items():
for w in ws: for w in ws:
if w[0] not in filtered_inputs: if not w[0] in filtered_inputs:
filtered_inputs.append(w[0]) 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) prevout_to_spend.append(prevout_str)
balance = 0 balance = 0
utxo_to_spend = [] utxo_to_spend = []
for utxo in utxos: for utxo in utxos:
if utxo.is_coinbase_output() and utxo.block_height < current_height+100:
continue
utxo_str = utxo.prevout.to_str() utxo_str = utxo.prevout.to_str()
if utxo_str in prevout_to_spend: if utxo_str in prevout_to_spend:
balance += inputs[utxo_str][0][2].value_sats() balance += inputs[utxo_str][0][2].value_sats()
utxo_to_spend.append(utxo) utxo_to_spend.append(utxo)
if len(utxo_to_spend) > 0: if len(utxo_to_spend) > 0:
change_addresses = wallet.get_change_addresses_for_new_transaction() change_addresses = wallet.get_change_addresses_for_new_transaction()
out = PartialTxOutput.from_address_and_value(change_addresses[0], balance) out = PartialTxOutput.from_address_and_value(change_addresses[0], balance)
out.is_change = True out.is_change = True
locktime = current_height locktime = get_current_height(wallet.network)
tx = PartialTransaction.from_io( tx = PartialTransaction.from_io(
utxo_to_spend, [out], locktime=locktime, version=2 utxo_to_spend, [out], locktime=locktime, version=2
) )
@@ -398,9 +373,9 @@ class Will:
return True return True
def search_rai(all_inputs, all_utxos, will, wallet): def search_rai(all_inputs, all_utxos, will, wallet):
# will_only_valid = Will.only_valid_or_replaced_list(will) will_only_valid = Will.only_valid_or_replaced_list(will)
for inp, ws in all_inputs.items(): for inp, ws in all_inputs.items():
inutxo = Util.in_utxo(inp, all_utxos) inutxo = in_utxo(inp, all_utxos)
for w in ws: for w in ws:
wi = w[1] wi = w[1]
if ( if (
@@ -431,31 +406,28 @@ class Will:
pass pass
def utxos_strs(utxos): def utxos_strs(utxos):
return [Util.utxo_to_str(u) for u in utxos] return [utxo_to_str(u) for u in utxos]
def set_invalidate(wid, will=[]): def set_invalidate(wid, will=[]):
will[wid].set_status("INVALIDATED", True) will[wid].set_status("INVALIDATED", True)
if will[wid].children: if will[wid].children:
for c in will[wid].children.items(): for c in self.children.items():
Will.set_invalidate(c[0], will) Will.set_invalidate(c[0], will)
def check_tx_height(tx, wallet): def check_tx_height(tx, wallet):
info = wallet.get_tx_info(tx) info = wallet.get_tx_info(tx)
return info.tx_mined_status.height() return info.tx_mined_status.height
# check if transactions are stil valid tecnically valid # check if transactions are stil valid tecnically valid
def check_invalidated(willtree, utxos_list, wallet): def check_invalidated(willtree, utxos_list, wallet):
for wid, w in willtree.items(): for wid, w in willtree.items():
if ( if not w.father:
not w.father
or willtree[w.father].get_status("CONFIRMED")
or willtree[w.father].get_status("PENDING")
):
for inp in w.tx.inputs(): for inp in w.tx.inputs():
inp_str = Util.utxo_to_str(inp) inp_str = utxo_to_str(inp)
if inp_str not in utxos_list: if not inp_str in utxos_list:
if wallet: if wallet:
height = Will.check_tx_height(w.tx, wallet) height = Will.check_tx_height(w.tx, wallet)
if height < 0: if height < 0:
Will.set_invalidate(wid, willtree) Will.set_invalidate(wid, willtree)
elif height == 0: elif height == 0:
@@ -463,21 +435,21 @@ class Will:
else: else:
w.set_status("CONFIRMED", True) w.set_status("CONFIRMED", True)
# def reflect_to_children(treeitem): def reflect_to_children(treeitem):
# if not treeitem.get_status("VALID"): if not treeitem.get_status("VALID"):
# _logger.debug(f"{tree:item._id} status not valid looking for children") _logger.debug(f"{tree:item._id} status not valid looking for children")
# for child in treeitem.children: for child in treeitem.children:
# wc = willtree[child] wc = willtree[child]
# if wc.get_status("VALID"): if wc.get_status("VALID"):
# if treeitem.get_status("INVALIDATED"): if treeitem.get_status("INVALIDATED"):
# wc.set_status("INVALIDATED", True) wc.set_status("INVALIDATED", True)
# if treeitem.get_status("REPLACED"): if treeitem.get_status("REPLACED"):
# wc.set_status("REPLACED", True) wc.set_status("REPLACED", True)
# if wc.children: if wc.children:
# Will.reflect_to_children(wc) Will.reflect_to_children(wc)
def check_amounts(heirs, willexecutors, all_utxos, timestamp_to_check, dust): def check_amounts(heirs, willexecutors, all_utxos, timestamp_to_check, dust):
fixed_heirs, fixed_amount, perc_heirs, perc_amount, fixed_amount_with_dust = ( fixed_heirs, fixed_amount, perc_heirs, perc_amount = (
heirs.fixed_percent_lists_amount(timestamp_to_check, dust, reverse=True) heirs.fixed_percent_lists_amount(timestamp_to_check, dust, reverse=True)
) )
wallet_balance = 0 wallet_balance = 0
@@ -506,7 +478,9 @@ class Will:
Will.check_invalidated(will, utxos_list, wallet) Will.check_invalidated(will, utxos_list, wallet)
all_inputs = Will.get_all_inputs(will, only_valid=True) all_inputs = Will.get_all_inputs(will, only_valid=True)
all_inputs_min_locktime = Will.get_all_inputs_min_locktime(all_inputs) all_inputs_min_locktime = Will.get_all_inputs_min_locktime(all_inputs)
Will.check_will_expired( Will.check_will_expired(
all_inputs_min_locktime, block_to_check, timestamp_to_check all_inputs_min_locktime, block_to_check, timestamp_to_check
) )
@@ -528,6 +502,7 @@ class Will:
callback_not_valid_tx=None, callback_not_valid_tx=None,
): ):
Will.check_will(will, all_utxos, wallet, block_to_check, timestamp_to_check) Will.check_will(will, all_utxos, wallet, block_to_check, timestamp_to_check)
if heirs: if heirs:
if not Will.check_willexecutors_and_heirs( if not Will.check_willexecutors_and_heirs(
will, will,
@@ -545,7 +520,7 @@ class Will:
if all_inputs: if all_inputs:
for utxo in all_utxos: for utxo in all_utxos:
if utxo.value_sats() > 68 * tx_fees: if utxo.value_sats() > 68 * tx_fees:
if not Util.in_utxo(utxo, all_inputs.keys()): if not in_utxo(utxo, all_inputs.keys()):
_logger.info("utxo is not spent", utxo.to_json()) _logger.info("utxo is not spent", utxo.to_json())
_logger.debug(all_inputs.keys()) _logger.debug(all_inputs.keys())
raise NotCompleteWillException( raise NotCompleteWillException(
@@ -571,20 +546,17 @@ class Will:
raise WillExpiredException( raise WillExpiredException(
f"Will Expired {wid[0][0]}: {locktime}<{timestamp_to_check}" 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(): def check_all_input_spent_are_in_wallet():
# _logger.info("check all input spent are in wallet or valid txs") _logger.info("check all input spent are in wallet or valid txs")
# for inp, ws in all_inputs.items(): for inp, ws in all_inputs.items():
# if not Util.in_utxo(inp, all_utxos): if not in_utxo(inp, all_utxos):
# for w in ws: for w in ws:
# if w[1].get_status("VALID"): if w[1].get_status("VALID"):
# prevout_id = w[2].prevout.txid.hex() prevout_id = w[2].prevout.txid.hex()
# parentwill = will.get(prevout_id, False) parentwill = will.get(prevout_id, False)
# 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)
def only_valid_list(will): def only_valid_list(will):
out = {} out = {}
@@ -619,23 +591,22 @@ class Will:
if not 'w!ll3x3c"' == wheir[:9]: if not 'w!ll3x3c"' == wheir[:9]:
their = will[wid].heirs[wheir] their = will[wid].heirs[wheir]
if heir := heirs.get(wheir, None): if heir := heirs.get(wheir, None):
if ( if (
heir[0] == their[0] heir[0] == their[0]
and heir[1] == their[1] and heir[1] == their[1]
and Util.parse_locktime_string(heir[2]) and parse_locktime_string(heir[2])
>= Util.parse_locktime_string(their[2]) >= parse_locktime_string(their[2])
): ):
count = heirs_found.get(wheir, 0) count = heirs_found.get(wheir, 0)
heirs_found[wheir] = count + 1 heirs_found[wheir] = count + 1
else: else:
_logger.debug( _logger.debug(
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: if willexecutor := w.we:
count = willexecutors_found.get(willexecutor["url"], 0) count = willexecutors_found.get(willexecutor["url"], 0)
if Util.cmp_willexecutor( if cmp_willexecutor(
willexecutor, willexecutors.get(willexecutor["url"], None) willexecutor, willexecutors.get(willexecutor["url"], None)
): ):
willexecutors_found[willexecutor["url"]] = count + 1 willexecutors_found[willexecutor["url"]] = count + 1
@@ -644,19 +615,19 @@ class Will:
no_willexecutor += 1 no_willexecutor += 1
count_heirs = 0 count_heirs = 0
for h in heirs: for h in heirs:
if parse_locktime_string(heirs[h][2]) >= check_date:
if Util.parse_locktime_string(heirs[h][2]) >= check_date:
count_heirs += 1 count_heirs += 1
if h not in heirs_found: if not h in heirs_found:
_logger.debug(f"heir: {h} not found") _logger.debug(f"heir: {h} not found")
raise HeirNotFoundException(h) raise HeirNotFoundException(h)
if not count_heirs: if not count_heirs:
raise NoHeirsException("there are not valid heirs") raise NoHeirsException("there are not valid heirs")
if self_willexecutor and no_willexecutor == 0: if self_willexecutor and no_willexecutor == 0:
raise NoWillExecutorNotPresent("Backup tx") raise NoWillExecutorNotPresent("Backup tx")
for url, we in willexecutors.items(): for url, we in willexecutors.items():
if Willexecutors.is_selected(we): if Willexecutors.is_selected(we):
if url not in willexecutors_found: if not url in willexecutors_found:
_logger.debug(f"will-executor: {url} not fount") _logger.debug(f"will-executor: {url} not fount")
raise WillExecutorNotPresent(url) raise WillExecutorNotPresent(url)
_logger.info("will is coherent with heirs and will-executors") _logger.info("will is coherent with heirs and will-executors")
@@ -685,15 +656,15 @@ class WillItem(Logger):
} }
def set_status(self, status, value=True): def set_status(self, status, value=True):
# _logger.trace( _logger.debug(
# "set status {} - {} {} -> {}".format( "set status {} - {} {} -> {}".format(
# self._id, status, self.STATUS[status][1], value self._id, status, self.STATUS[status][1], value
# ) )
# ) )
if self.STATUS[status][1] == bool(value): if self.STATUS[status][1] == bool(value):
return None return None
self.status += "." + (("NOT " if not value else "") + _(self.STATUS[status][0])) self.status += "." + ("NOT " if not value else "" + _(self.STATUS[status][0]))
self.STATUS[status][1] = bool(value) self.STATUS[status][1] = bool(value)
if value: if value:
if status in ["INVALIDATED", "REPLACED", "CONFIRMED", "PENDING"]: if status in ["INVALIDATED", "REPLACED", "CONFIRMED", "PENDING"]:
@@ -807,9 +778,9 @@ class WillItem(Logger):
iw = inp[1] iw = inp[1]
self.set_anticipate(iw) self.set_anticipate(iw)
def set_check_willexecutor(self,resp): def check_willexecutor(self):
try: try:
if resp : if resp := Willexecutors.check_transaction(self._id, self.we["url"]):
if "tx" in resp and resp["tx"] == str(self.tx): if "tx" in resp and resp["tx"] == str(self.tx):
self.set_status("PUSHED") self.set_status("PUSHED")
self.set_status("CHECKED") self.set_status("CHECKED")
@@ -849,12 +820,7 @@ class WillItem(Logger):
class WillException(Exception): class WillException(Exception):
def __init__(self,msg="WillException"): pass
self.msg=msg
Exception.__init__(self)
def __str__(self):
return self.msg
class WillExpiredException(WillException): class WillExpiredException(WillException):
@@ -891,6 +857,8 @@ class WillExecutorNotPresent(NotCompleteWillException):
class NoHeirsException(WillException): class NoHeirsException(WillException):
pass pass
class AmountException(WillException): class AmountException(WillException):
pass pass

View File

@@ -1,70 +1,33 @@
"""Willexecutors module for BAL Electrum Plugin.
This module provides functionality to manage will executor servers that
broadcast timelocked transactions at the appropriate locktime.
Classes:
Willexecutors: Static class for managing executor list and communication
WillExecutor: Data class representing a single will executor
"""
import json import json
from datetime import datetime from datetime import datetime
import time from functools import partial
from aiohttp import ClientResponse from aiohttp import ClientResponse
from electrum import constants from electrum import constants
from electrum.gui.qt.util import WaitingDialog
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
from .bal import BalPlugin from .bal import BalPlugin
# from .util import *
DEFAULT_TIMEOUT = 5 DEFAULT_TIMEOUT = 5
_logger = get_logger(__name__) _logger = get_logger(__name__)
chainname = constants.net.NET_NAME if constants.net.NET_NAME != "mainnet" else "bitcoin"
class Willexecutors: class Willexecutors:
"""Static class managing will executor servers.
Provides methods to save/load configurations, communicate via HTTP,
push transactions, and download executor lists from remote sources.
"""
def save(bal_plugin, willexecutors): def save(bal_plugin, willexecutors):
"""Save will executor configuration to plugin settings.
Args:
bal_plugin: BAL plugin instance
willexecutors: Dictionary of executor configs keyed by URL
"""
_logger.debug(f"save {willexecutors},{chainname}")
aw = bal_plugin.WILLEXECUTORS.get() aw = bal_plugin.WILLEXECUTORS.get()
aw[chainname] = willexecutors aw[constants.net.NET_NAME] = willexecutors
bal_plugin.WILLEXECUTORS.set(aw) bal_plugin.WILLEXECUTORS.set(aw)
_logger.debug(f"saved: {aw}")
# bal_plugin.WILLEXECUTORS.set(willexecutors)
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
): ):
"""Retrieve and update the list of available will executors.
Args:
bal_plugin: BAL plugin instance
update: If True, ping servers to refresh data
bal_window: GUI window for user prompts
force: Force update regardless of cached data age
task: Run as background task if True
Returns:
dict: Sorted dictionary of executor configurations
"""
willexecutors = bal_plugin.WILLEXECUTORS.get() willexecutors = bal_plugin.WILLEXECUTORS.get()
willexecutors = willexecutors.get(chainname, {}) willexecutors = willexecutors.get(constants.net.NET_NAME, {})
to_del = [] to_del = []
for w in willexecutors: for w in willexecutors:
if not isinstance(willexecutors[w], dict): if not isinstance(willexecutors[w], dict):
@@ -72,38 +35,34 @@ class Willexecutors:
continue continue
Willexecutors.initialize_willexecutor(willexecutors[w], w) Willexecutors.initialize_willexecutor(willexecutors[w], w)
for w in to_del: for w in to_del:
_logger.error( print("ERROR: WILLEXECUTOR TO DELETE:", w)
"error Willexecutor to delete type:{} {}".format(
type(willexecutors[w]), w
)
)
del willexecutors[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(): 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") _logger.debug(f"force add {bal_url} willexecutor")
willexecutors[bal_url] = bal_executor willexecutors[bal_url] = bal_executor
# if update: if update:
# found = False found = False
# for url, we in willexecutors.items(): for url, we in willexecutors.items():
# if Willexecutors.is_selected(we): if Willexecutors.is_selected(we):
# found = True found = True
# if found or force: if found or force:
# if bal_plugin.PING_WILLEXECUTORS.get() or force: if bal_plugin.PING_WILLEXECUTORS.get() or force:
# ping_willexecutors = True ping_willexecutors = True
# if bal_plugin.ASK_PING_WILLEXECUTORS.get() and not force: if bal_plugin.ASK_PING_WILLEXECUTORS.get() and not force:
# if bal_window: if bal_window:
# ping_willexecutors = bal_window.window.question( ping_willexecutors = bal_window.window.question(
# _( _(
# "Contact willexecutors servers to update payment informations?" "Contact willexecutors servers to update payment informations?"
# ) )
# ) )
# if ping_willexecutors: if ping_willexecutors:
# if task: if task:
# bal_window.ping_willexecutors(willexecutors, task) bal_window.ping_willexecutors(willexecutors, task)
# else: else:
# bal_window.ping_willexecutors_task(willexecutors) bal_window.ping_willexecutors_task(willexecutors)
w_sorted = dict( w_sorted = dict(
sorted( sorted(
willexecutors.items(), key=lambda w: w[1].get("sort", 0), reverse=True willexecutors.items(), key=lambda w: w[1].get("sort", 0), reverse=True
@@ -112,35 +71,17 @@ class Willexecutors:
return w_sorted return w_sorted
def is_selected(willexecutor, value=None): def is_selected(willexecutor, value=None):
"""Check or set the selection status of an executor.
Args:
willexecutor: Executor configuration dictionary
value: Optional boolean to set selection status
Returns:
bool: Current selection status
"""
if not willexecutor: if not willexecutor:
return False return False
if value is not None: if not value is None:
willexecutor["selected"] = value willexecutor["selected"] = value
try: try:
return willexecutor["selected"] return willexecutor["selected"]
except Exception: except:
willexecutor["selected"] = False willexecutor["selected"] = False
return False return False
def get_willexecutor_transactions(will, force=False): def get_willexecutor_transactions(will, force=False):
"""Collect transactions grouped by executor for valid, complete wills.
Args:
will: Dictionary of will items keyed by ID
force: Include already-pushed transactions
Returns:
dict: Executors with their aggregated transactions
"""
willexecutors = {} willexecutors = {}
for wid, willitem in will.items(): for wid, willitem in will.items():
if willitem.get_status("VALID"): if willitem.get_status("VALID"):
@@ -149,7 +90,7 @@ class Willexecutors:
if willexecutor := willitem.we: if willexecutor := willitem.we:
url = willexecutor["url"] url = willexecutor["url"]
if willexecutor and Willexecutors.is_selected(willexecutor): if willexecutor and Willexecutors.is_selected(willexecutor):
if url not in willexecutors: if not url in willexecutors:
willexecutor["txs"] = "" willexecutor["txs"] = ""
willexecutor["txsids"] = [] willexecutor["txsids"] = []
willexecutor["broadcast_status"] = _("Waiting...") willexecutor["broadcast_status"] = _("Waiting...")
@@ -159,50 +100,31 @@ class Willexecutors:
return willexecutors return willexecutors
# def only_selected_list(willexecutors): def only_selected_list(willexecutors):
# out = {} out = {}
# for url, v in willexecutors.items(): for url, v in willexecutors.items():
# if Willexecutors.is_selected(url): if Willexecutors.is_selected(willexecutor):
# out[url] = v out[url] = v
# def push_transactions_to_willexecutors(will): def push_transactions_to_willexecutors(will):
# willexecutors = Willexecutors.get_transactions_to_be_pushed() willexecutors = get_transactions_to_be_pushed()
# for url in willexecutors: for url in willexecutors:
# willexecutor = willexecutors[url] willexecutor = willexecutors[url]
# if Willexecutors.is_selected(willexecutor): if Willexecutors.is_selected(willexecutor):
# if "txs" in willexecutor: if "txs" in willexecutor:
# Willexecutors.push_transactions_to_willexecutor( Willexecutors.push_transactions_to_willexecutor(
# willexecutors[url]["txs"], url willexecutors[url]["txs"], url
# ) )
def send_request( def send_request(method, url, data=None, *, timeout=10):
method, url, data=None, *, timeout=10, handle_response=None, count_reply=0
):
"""Send HTTP request to a will executor server.
Args:
method: HTTP method (get/post)
url: Target server URL
data: Request payload
timeout: Timeout in seconds
handle_response: Response processing function
count_reply: Retry counter for timeouts
Returns:
Server response object
Raises:
Exception: On connection errors or invalid method
"""
network = Network.get_instance() network = Network.get_instance()
if not network: if not network:
raise Exception("You are offline.") raise ErrorConnectingServer("You are offline.")
_logger.debug(f"<-- {method} {url} {data}") _logger.debug(f"<-- {method} {url} {data}")
headers = {} headers = {}
headers["user-agent"] = f"BalPlugin v:{BalPlugin.__version__}" headers["user-agent"] = f"BalPlugin v:{BalPlugin.version()}"
headers["Content-Type"] = "text/plain" headers["Content-Type"] = "text/plain"
if not handle_response:
handle_response = Willexecutors.handle_response
try: try:
if method == "get": if method == "get":
response = Network.send_http_on_proxy( response = Network.send_http_on_proxy(
@@ -210,7 +132,7 @@ class Willexecutors:
url, url,
params=data, params=data,
headers=headers, headers=headers,
on_finish=handle_response, on_finish=Willexecutors.handle_response,
timeout=timeout, timeout=timeout,
) )
elif method == "post": elif method == "post":
@@ -219,88 +141,39 @@ class Willexecutors:
url, url,
body=data, body=data,
headers=headers, headers=headers,
on_finish=handle_response, on_finish=Willexecutors.handle_response,
timeout=timeout, timeout=timeout,
) )
else: else:
raise Exception(f"unexpected {method=!r}") raise Exception(f"unexpected {method=!r}")
except 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: except Exception as e:
_logger.error(f"exception sending request {e}")
raise e raise e
else: else:
_logger.debug(f"--> {response}") _logger.debug(f"--> {response}")
return response return response
def get_we_url_from_response(resp):
"""Extract base URL from response object.
Args:
resp: Response object with url attribute
Returns:
str: Base URL without trailing paths
"""
url_slices = str(resp.url).split("/")
if len(url_slices) > 2:
url_slices = url_slices[:-2]
return "/".join(url_slices)
async def handle_response(resp: ClientResponse): async def handle_response(resp: ClientResponse):
"""Parse JSON response from executor server.
Args:
resp: aiohttp ClientResponse object
Returns:
Parsed JSON data or raw text on parse failure
"""
r = await resp.text() r = await resp.text()
try: try:
r = json.loads(r) r = json.loads(r)
# url = Willexecutors.get_we_url_from_response(resp) r["status"] = resp.status
# r["url"]= url r["selected"] = Willexecutors.is_selected(willexecutor)
# r["status"]=resp.status r["url"] = url
except Exception as e: except:
_logger.debug(f"error handling response:{e}")
pass pass
return r return r
class AlreadyPresentException(Exception): class AlreadyPresentException(Exception):
"""Raised when transactions already exist on executor server."""
pass pass
def push_transactions_to_willexecutor(willexecutor): def push_transactions_to_willexecutor(willexecutor):
"""Push timelocked transactions to an executor server for broadcast.
Args:
willexecutor: Dict containing url and txs keys
Returns:
bool: True on success, False on failure
Raises:
AlreadyPresentException: If transactions already exist
"""
out = True out = True
try: try:
_logger.debug(f"{willexecutor['url']}: {willexecutor['txs']}") _logger.debug(f"willexecutor['txs']")
if w := Willexecutors.send_request( if w := Willexecutors.send_request(
"post", "post",
willexecutor["url"] + "/" + chainname + "/pushtxs", willexecutor["url"] + "/" + constants.net.NET_NAME + "/pushtxs",
data=willexecutor["txs"].encode("ascii"), data=willexecutor["txs"].encode("ascii"),
): ):
willexecutor["broadcast_status"] = _("Success") willexecutor["broadcast_status"] = _("Success")
@@ -320,36 +193,26 @@ class Willexecutors:
return out return out
def ping_servers(willexecutors): def ping_servers(willexecutors):
"""Ping all executor servers to update their information.
Args:
willexecutors: Dictionary of executor configurations
"""
for url, we in willexecutors.items(): for url, we in willexecutors.items():
Willexecutors.get_info_task(url, we) Willexecutors.get_info_task(url, we)
def get_info_task(url, willexecutor): def get_info_task(url, willexecutor):
"""Fetch current information from a single executor server.
Args:
url: Executor server URL
willexecutor: Configuration dict to update
Returns:
Updated willexecutor dict with status, base_fee, address
"""
w = None w = None
try: try:
_logger.info("GETINFO_WILLEXECUTOR") _logger.info("GETINFO_WILLEXECUTOR")
_logger.debug(url) _logger.debug(url)
w = Willexecutors.send_request("get", url + "/" + chainname + "/info") netname = "bitcoin"
if isinstance(w, dict): if constants.net.NET_NAME != "mainnet":
netname = constants.net.NET_NAME
w = Willexecutors.send_request("get", url + "/" + netname + "/info")
willexecutor["url"] = url willexecutor["url"] = url
willexecutor["status"] = 200 willexecutor["status"] = w["status"]
willexecutor["base_fee"] = w["base_fee"] willexecutor["base_fee"] = w["base_fee"]
willexecutor["address"] = w["address"] willexecutor["address"] = w["address"]
if not willexecutor["info"]:
willexecutor["info"] = w["info"] willexecutor["info"] = w["info"]
_logger.debug(f"response_data {w}") _logger.debug(f"response_data {w['address']}")
except Exception as e: except Exception as e:
_logger.error(f"error {e} contacting {url}: {w}") _logger.error(f"error {e} contacting {url}: {w}")
willexecutor["status"] = "KO" willexecutor["status"] = "KO"
@@ -357,60 +220,30 @@ class Willexecutors:
willexecutor["last_update"] = datetime.now().timestamp() willexecutor["last_update"] = datetime.now().timestamp()
return willexecutor return willexecutor
def initialize_willexecutor(willexecutor, url, status=None, old_willexecutor={}): def initialize_willexecutor(willexecutor, url, status=None, selected=None):
"""Initialize or merge executor configuration preserving user settings.
Args:
willexecutor: New executor configuration dict
url: Executor server URL
status: Optional status override
old_willexecutor: Previous config to preserve user preferences
"""
willexecutor["url"] = url willexecutor["url"] = url
if status is not None: if not status is None:
willexecutor["status"] = status willexecutor["status"] = status
else: willexecutor["selected"] = Willexecutors.is_selected(willexecutor, selected)
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"))
def download_list(bal_plugin):
def download_list(old_willexecutors):
"""Download latest executor list from remote source.
Args:
old_willexecutors: Existing configs to merge with new list
Returns:
dict: Merged executor configurations
"""
try: try:
willexecutors = Willexecutors.send_request( l = Willexecutors.send_request(
"get", "get", "https://welist.bitcoin-after.life/data/bitcoin?page=0&limit=100"
f"https://welist.bitcoin-after.life/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,{})
) )
del l["status"]
for w in l:
willexecutor = l[w]
Willexecutors.initialize_willexecutor(willexecutor, w, "New", False)
# bal_plugin.WILLEXECUTORS.set(l) # bal_plugin.WILLEXECUTORS.set(l)
# bal_plugin.config.set_key(bal_plugin.WILLEXECUTORS,l,save=True) # bal_plugin.config.set_key(bal_plugin.WILLEXECUTORS,l,save=True)
return willexecutors return l
except Exception as e: except Exception as e:
_logger.error(f"Failed to download willexecutors list: {e}") _logger.error(f"Failed to download willexecutors list: {e}")
return {} return {}
def get_willexecutors_list_from_json(): def get_willexecutors_list_from_json(bal_plugin):
"""Load executor list from local willexecutors.json file.
Returns:
dict: Executor configurations from JSON file
"""
try: try:
with open("willexecutors.json") as f: with open("willexecutors.json") as f:
willexecutors = json.load(f) willexecutors = json.load(f)
@@ -418,22 +251,13 @@ class Willexecutors:
willexecutor = willexecutors[w] willexecutor = willexecutors[w]
Willexecutors.initialize_willexecutor(willexecutor, w, "New", False) Willexecutors.initialize_willexecutor(willexecutor, w, "New", False)
# bal_plugin.WILLEXECUTORS.set(willexecutors) # bal_plugin.WILLEXECUTORS.set(willexecutors)
return willexecutors return h
except Exception as e: except Exception as e:
_logger.error(f"error opening willexecutors json: {e}") _logger.error(f"error opening willexecutors json: {e}")
return {} return {}
def check_transaction(txid, url): def check_transaction(txid, url):
"""Check if a transaction exists on executor server.
Args:
txid: Transaction ID string
url: Executor server URL
Returns:
Server response about transaction existence
"""
_logger.debug(f"{url}:{txid}") _logger.debug(f"{url}:{txid}")
try: try:
w = Willexecutors.send_request( w = Willexecutors.send_request(
@@ -444,104 +268,14 @@ 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
def compute_id(willexecutor):
"""Compute unique identifier for an executor.
Args:
willexecutor: Executor configuration dict
Returns:
str: Unique ID combining URL and chain name
"""
return "{}-{}".format(willexecutor.get("url"), willexecutor.get("chain"))
class WillExecutor: class WillExecutor:
"""Data class representing a single will executor server. def __init__(self, url, base_fee, chain, info, version):
Attributes:
url: Executor server URL
base_fee: Fixed fee in satoshis
chain: Bitcoin chain name
info: Additional executor information
version: Plugin version compatibility
status: Connection status
is_selected: User selection flag
promo_code: Promotional discount code
"""
def __init__(
self,
url,
base_fee,
chain,
info,
version,
status,
is_selected=False,
promo_code="",
):
"""Initialize a new WillExecutor instance.
Args:
url: Executor server URL
base_fee: Fixed fee in satoshis
chain: Bitcoin chain name
info: Additional executor information
version: Plugin version compatibility
status: Connection status (OK/Ko)
is_selected: Whether user has selected this executor
promo_code: Promotional discount 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.promo_code = promo_code
self.is_selected = is_selected
self.id = self.compute_id()
def from_dict(d): def from_dict(d):
"""Create WillExecutor instance from a dictionary. we = WillExecutor(d["url"], d["base_fee"], d["chain"], d["info"], d["version"])
Args:
d: Dictionary containing executor data fields
Returns:
WillExecutor: New instance with defaults for missing fields
"""
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):
"""Convert WillExecutor to dictionary representation.
Returns:
dict: Serializable representation excluding computed fields
"""
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):
"""Generate unique identifier for this executor.
Returns:
str: Unique ID from URL and chain name
"""
return f"{self.url}-{self.chain}"