54 Commits
v0.2.0 ... main

Author SHA1 Message Date
kaibot
9ce42d6f12 docs(will): add module header 2026-04-10 15:28:06 +00:00
kaibot
014dd230c1 docs(will): prepend module header 2026-04-10 15:26:13 +00:00
kaibot
2054e4f4b2 docs(util): prepend module header with description 2026-04-10 15:24:00 +00:00
kaibot
ae7ea24bdf docs(util): add module level description 2026-04-10 15:22:03 +00:00
kaibot
06c32268a7 docs(willexecutors.py): Add comprehensive documentation for all classes and methods 2026-04-10 01:32:20 +00:00
kaibot
6fb82dc8d0 docs(README): Remove version references, remove PyQt mentions, update wallet_util steps, simplify contributing section 2026-04-09 11:47:10 +00:00
kaibot
9c91cc8019 docs(README): Remove PyQt5 references, remove configurations section, add onchain invalidation transaction detail 2026-04-09 11:40:29 +00:00
kaibot
5dc1ca36ab docs(README): Update requirements - PyQt6 only, Electrum 4.7.2+, remove pytest, correct wallet_utils usage 2026-04-09 11:35:25 +00:00
kaibot
694e14d851 docs(README): Update executor fee to be fixed amount only, remove percentage option 2026-04-09 11:29:35 +00:00
kaibot
60fc08ad5a docs(README): Update with correct executor fee details and wallet_utils purpose 2026-04-09 11:26:03 +00:00
kaibot
22fa6cd708 docs(README): Update with correct file structure and module descriptions 2026-04-09 11:16:38 +00:00
kaibot
b63dc5ba4f docs(README): Update with accurate plugin functionality description 2026-04-09 11:06:08 +00:00
kaibot
a5f6b9f925 Merge branch 'main' of https://bitcoin-after.life/gitea/kaibot/bal-electrum-plugin
# Conflicts:
#	qt.py
2026-04-09 10:46:08 +00:00
kaibot
c739d110d6 docs(README): Add comprehensive installation and usage guide for Bal Electrum Plugin 2026-04-09 10:45:46 +00:00
d439b1fdde msg_update sizehint 2026-04-09 06:20:19 -04:00
c99f0fd70f msg_update not recreate labels update text 2026-04-09 05:58:01 -04:00
ab6aa7a698 msg_update window size 2026-04-09 05:46:43 -04:00
b55493221d bug in log 2026-04-09 05:02:21 -04:00
kaibot
45d8173cf7 docs(qt.py): Add comprehensive documentation for BalWizardDialog class 2026-04-09 02:43:57 +00:00
kaibot
b739bdab40 docs(qt.py): Add comprehensive documentation for BalWizardDialog class 2026-04-09 02:43:06 +00:00
kaibot
d613438800 docs(qt.py): Add comprehensive documentation for BalWizardDialog class 2026-04-09 02:39:04 +00:00
kaibot
a27df11dfa docs(qt.py): Add detailed interval documentation for HeirsLockTimeEdit class behavior 2026-04-09 02:10:49 +00:00
kaibot
686c11080f Merge branch 'doc' into main with documentation for HeirsLockTimeEdit and BalBuildWillDialog 2026-04-09 01:36:34 +00:00
kaibot
be38c9b589 docs(qt.py): Add comprehensive documentation for HeirsLockTimeEdit class 2026-04-09 01:28:47 +00:00
dff508c25b version 2026-04-08 11:17:54 -04:00
2056ffae7f check alive updated 2026-04-08 11:16:59 -04:00
c8ab85b735 invalidation and locktime 2026-04-05 11:39:17 -04:00
e2de4a3afa skip willexecutor with dust amount 2026-03-27 23:06:27 -04:00
3a44b492e4 __version__0.2.7 2026-03-21 11:31:46 -04:00
9737221914 fix send_request version message 2026-03-18 16:25:59 -04:00
a022c413cc willexecutor manager improved 2026-03-17 02:34:01 -04:00
716d4dd5c5 version 2026-03-05 10:50:03 -04:00
b012dd7a68 black reformatted 2026-03-05 10:47:59 -04:00
ef0ab56de4 fixed refresh and some minor bug about dust amounts and empty wallet 2026-03-05 10:46:38 -04:00
c5ad5a61bb bug delete heirs from wizard 2026-02-10 12:22:43 -04:00
f7bd09df91 dust bugfix 2026-02-09 12:10:31 -04:00
2a4eab81fd print 2026-02-05 17:51:17 -04:00
d86b941fcb refresh button. locktim is correctly saved, minor bugfix in checking confirmed transaction 2026-02-05 17:11:11 -04:00
1836cdd892 version 2026-02-03 22:12:30 -04:00
2416d0ce8d fix willexecutor list edit 2026-02-03 22:11:33 -04:00
8e4e401d1b fix plugin settings removed willexecutor ping 2026-02-03 16:14:12 -04:00
b8859ee5c1 qt thread panic
heirs import wizard
2026-02-03 13:56:47 -04:00
faeff1ff3c gui willexecutor list status moved after url to be more visible.
heirs tab start hidden
2026-02-03 11:25:21 -04:00
437105477d missing icons 2026-01-28 14:47:24 -04:00
4c12136470 release 2026-01-24 19:50:41 -04:00
a918c5564d close task invalidate tx 2026-01-09 16:46:22 -04:00
d2280969de read_file icons 2026-01-05 01:25:20 -04:00
6cf12eec80 black refactor + bugfix 2026-01-04 23:52:53 -04:00
936d4ef467 version 2026-01-04 16:54:27 -04:00
5512ee0e61 version 2026-01-04 16:49:43 -04:00
3e9a841e21 version 2026-01-04 16:41:53 -04:00
5d9636cda1 txfees fixer 2026-01-04 13:45:31 -04:00
b384ac562c wallet utils 2025-11-30 16:29:01 -04:00
9817546064 fix tx_fees wallet and other bug fixes to be compatible with electrum 0.6.2-master(0.6.3) 2025-11-29 23:42:54 -04:00
24 changed files with 4861 additions and 4334 deletions

359
README.md
View File

@@ -1,2 +1,357 @@
# BalPlugin # Bal Electrum Plugin
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.2a 0.2.8

123
bal.py
View File

@@ -1,26 +1,33 @@
import random
import os import os
import zipfile as zipfile_lib
from electrum.plugin import BasePlugin # import random
# import zipfile as zipfile_lib
from electrum import json_db from electrum import json_db
from electrum.logging import get_logger
from electrum.plugin import BasePlugin
from electrum.transaction import tx_from_any from electrum.transaction import tx_from_any
import os
json_db.register_dict('heirs', tuple, None)
json_db.register_dict('will', lambda x: get_will(x), None)
json_db.register_dict('will_settings', lambda x:x, None)
from electrum.logging import get_logger def get_will_settings(x):
# print(x)
pass
json_db.register_dict("heirs", tuple, None)
json_db.register_dict("will", dict, None)
json_db.register_dict("will_settings", lambda x: x, None)
def get_will(x): def get_will(x):
try: try:
x['tx']=tx_from_any(x['tx']) x["tx"] = tx_from_any(x["tx"])
except Exception as e: except Exception as e:
raise e raise e
return x return x
class BalConfig():
class BalConfig:
def __init__(self, config, name, default): def __init__(self, config, name, default):
self.config = config self.config = config
self.name = name self.name = name
@@ -29,7 +36,7 @@ class BalConfig():
def get(self, default=None): def get(self, default=None):
v = self.config.get(self.name, default) v = self.config.get(self.name, default)
if v is None: if v is None:
if not default is None: if default is not None:
v = default v = default
else: else:
v = self.default v = self.default
@@ -38,34 +45,33 @@ class BalConfig():
def set(self, value, save=True): def set(self, value, save=True):
self.config.set_key(self.name, value, save=save) self.config.set_key(self.name, value, save=save)
class BalPlugin(BasePlugin): class BalPlugin(BasePlugin):
LATEST_VERSION = '1' _version=None
KNOWN_VERSIONS = ('0', '1') __version__ = "0.2.8" #AUTOMATICALLY GENERATED DO NOT EDIT
assert LATEST_VERSION in KNOWN_VERSIONS def version(self):
def version(): if not self._version:
try: try:
f = "" f = ""
with open("VERSION","r") as f: with open("{}/VERSION".format(self.plugin_dir), "r") as fi:
f = str(f.readline()) f = str(fi.read())
return f self._version = f.strip()
except: except Exception as e:
return "unknown" _logger.error(f"failed to get version: {e}")
self._version="unknown"
return self._version
SIZE = (159, 97) SIZE = (159, 97)
def __init__(self, parent, config, name): def __init__(self, parent, config, name):
self.logger = get_logger(__name__) self.logger = get_logger(__name__)
BasePlugin.__init__(self, parent, config, name) BasePlugin.__init__(self, parent, config, name)
self.base_dir = os.path.join(config.electrum_path(), 'bal') self.base_dir = os.path.join(config.electrum_path(), "bal")
self.plugin_dir = os.path.split(os.path.realpath(__file__))[0] self.plugin_dir = os.path.split(os.path.realpath(__file__))[0]
zipfile = "/".join(self.plugin_dir.split("/")[:-1]) zipfile = "/".join(self.plugin_dir.split("/")[:-1])
#print("real path",os.path.realpath(__file__))
#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
@@ -75,7 +81,9 @@ class BalPlugin(BasePlugin):
self.LOCKTIME_TIME = BalConfig(config, "bal_locktime_time", 90) self.LOCKTIME_TIME = BalConfig(config, "bal_locktime_time", 90)
self.LOCKTIME_BLOCKS = BalConfig(config, "bal_locktime_blocks", 144 * 90) self.LOCKTIME_BLOCKS = BalConfig(config, "bal_locktime_blocks", 144 * 90)
self.LOCKTIMEDELTA_TIME = BalConfig(config, "bal_locktimedelta_time", 7) self.LOCKTIMEDELTA_TIME = BalConfig(config, "bal_locktimedelta_time", 7)
self.LOCKTIMEDELTA_BLOCKS = BalConfig(config, "bal_locktimedelta_blocks", 144*7) self.LOCKTIMEDELTA_BLOCKS = BalConfig(
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)
@@ -83,35 +91,52 @@ 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(config, "bal_ask_ping_willexecutors", True) # self.ASK_PING_WILLEXECUTORS = BalConfig(
# 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(config, "bal_willexecutors", { self.WILLEXECUTORS = BalConfig(
config,
"bal_willexecutors",
{
"mainnet": { "mainnet": {
'https://we.bitcoin-after.life': { "https://we.bitcoin-after.life": {
"base_fee": 100000,
"status": "New",
"info": "Bitcoin After Life Will Executor",
"address": "bc1qusymuetsz2psaqzqxv8qmzcy64d9meckj3lxxf",
"selected": True,
}
},
"testnet": {
"https://we.bitcoin-after.life": {
"base_fee": 100000, "base_fee": 100000,
"status": "New", "status": "New",
"info": "Bitcoin After Life Will Executor", "info": "Bitcoin After Life Will Executor",
"address": "bcrt1qa5cntu4hgadw8zd3n6sq2nzjy34sxdtd9u0gp7", "address": "bcrt1qa5cntu4hgadw8zd3n6sq2nzjy34sxdtd9u0gp7",
"selected":True "selected": True,
} }
} },
}) },
self.WILL_SETTINGS = BalConfig(config, "bal_will_settings", { )
'tx_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_invalidated = self.HIDE_INVALIDATED.get()
self._hide_replaced = self.HIDE_REPLACED.get() self._hide_replaced = self.HIDE_REPLACED.get()
def resource_path(self, *parts): def resource_path(self, *parts):
return os.path.join(self.plugin_dir, *parts) return os.path.join(self.plugin_dir, *parts)
@@ -124,11 +149,13 @@ class BalPlugin(BasePlugin):
self.HIDE_REPLACED.set(self._hide_replaced) self.HIDE_REPLACED.set(self._hide_replaced)
def validate_will_settings(self, will_settings): def validate_will_settings(self, will_settings):
if int(will_settings.get('tx_fees',1))<1: if int(will_settings.get("baltx_fees", 1)) < 1:
will_settings['tx_fees']=1 will_settings["baltx_fees"] = 1
if not will_settings.get('threshold'): if not will_settings.get("threshold"):
will_settings['threshold']='180d' will_settings["threshold"] = "180d"
if not will_settings.get('locktime')=='': if not will_settings.get("locktime"):
will_settings['locktime']='1y' will_settings["locktime"] = "1y"
return will_settings return will_settings
def default_will_settings(self):
return {"baltx_fees": 100, "threshold": "180d", "locktime": "1y"}

View File

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

View File

@@ -1,89 +0,0 @@
# -*- coding: utf-8 -*-
from typing import Union
from decimal import Decimal
from . import qt_resources
if qt_resources.QT_VERSION == 5:
from PyQt5.QtWidgets import (QLineEdit, QStyle, QStyleOptionFrame, QSizePolicy)
from PyQt5.QtGui import QPalette, QPainter
from PyQt5.QtCore import pyqtSignal, Qt, QSize
else:
from PyQt6.QtWidgets import (QLineEdit, QStyle, QStyleOptionFrame, QSizePolicy)
from PyQt6.QtGui import QPalette, QPainter
from PyQt6.QtCore import pyqtSignal, Qt, QSize
from electrum.util import (format_satoshis_plain, decimal_point_to_base_unit_name,
FEERATE_PRECISION, quantize_feerate, DECIMAL_POINT, UI_UNIT_NAME_FEERATE_SAT_PER_VBYTE)
from electrum.gui.qt.amountedit import BTCAmountEdit, char_width_in_lineedit, ColorScheme
_NOT_GIVEN = object() # sentinel value
class PercAmountEdit(BTCAmountEdit):
def __init__(self, decimal_point, is_int=False, parent=None, *, max_amount=_NOT_GIVEN):
super().__init__(decimal_point, is_int, parent, max_amount=max_amount)
def numbify(self):
text = self.text().strip()
if text == '!':
self.shortcut.emit()
return
pos = self.cursorPosition()
chars = '0123456789%'
chars += DECIMAL_POINT
s = ''.join([i for i in text if i in chars])
if '%' in s:
self.is_perc=True
s=s.replace('%','')
else:
self.is_perc=False
if DECIMAL_POINT in s:
p = s.find(DECIMAL_POINT)
s = s.replace(DECIMAL_POINT, '')
s = s[:p] + DECIMAL_POINT + s[p:p+8]
if self.is_perc:
s+='%'
#if self.max_amount:
# if (amt := self._get_amount_from_text(s)) and amt >= self.max_amount:
# s = self._get_text_from_amount(self.max_amount)
self.setText(s)
# setText sets Modified to False. Instead we want to remember
# if updates were because of user modification.
self.setModified(self.hasFocus())
self.setCursorPosition(pos)
#if len(s>0)
# self.drawText("")
def _get_amount_from_text(self, text: str) -> Union[None, Decimal, int]:
try:
text = text.replace(DECIMAL_POINT, '.')
text = text.replace('%', '')
return (Decimal)(text)
except Exception:
return None
def _get_text_from_amount(self, amount):
out = super()._get_text_from_amount(amount)
if self.is_perc: out+='%'
return out
def paintEvent(self, event):
QLineEdit.paintEvent(self, event)
if self.base_unit:
panel = QStyleOptionFrame()
self.initStyleOption(panel)
textRect = self.style().subElementRect(QStyle.SubElement.SE_LineEditContents, panel, self)
textRect.adjust(2, 0, -10, 0)
painter = QPainter(self)
painter.setPen(ColorScheme.GRAY.as_color())
if len(self.text())==0:
painter.drawText(textRect, int(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter), self.base_unit() + " or perc value")

View File

@@ -1,102 +0,0 @@
from typing import Callable,Any
from . import qt_resources
if qt_resources.QT_VERSION == 5:
from PyQt5.QtCore import Qt,pyqtSignal
from PyQt5.QtWidgets import QLabel, QVBoxLayout, QCheckBox
else:
from PyQt6.QtCore import Qt,pyqtSignal
from PyQt6.QtWidgets import QLabel, QVBoxLayout, QCheckBox
from electrum.gui.qt.util import WindowModalDialog, TaskThread
from electrum.i18n import _
from electrum.logging import get_logger
_logger = get_logger(__name__)
class BalDialog(WindowModalDialog):
def __init__(self,parent,title=None, icon = 'bal32x32.png'):
self.parent=parent
WindowModalDialog.__init__(self,self.parent,title)
self.setWindowIcon(qt_resources.read_QIcon(icon))
class BalWaitingDialog(BalDialog):
updatemessage=pyqtSignal([str], arguments=['message'])
def __init__(self, bal_window: 'BalWindow', message: str, task, on_success=None, on_error=None, on_cancel=None,exe=True):
assert bal_window
BalDialog.__init__(self, bal_window.window, _("Please wait"))
self.message_label = QLabel(message)
vbox = QVBoxLayout(self)
vbox.addWidget(self.message_label)
self.updatemessage.connect(self.update_message)
if on_cancel:
self.cancel_button = CancelButton(self)
self.cancel_button.clicked.connect(on_cancel)
vbox.addLayout(Buttons(self.cancel_button))
self.accepted.connect(self.on_accepted)
self.task=task
self.on_success = on_success
self.on_error = on_error
self.on_cancel = on_cancel
if exe:
self.exe()
def exe(self):
self.thread = TaskThread(self)
self.thread.finished.connect(self.deleteLater) # see #3956
self.thread.finished.connect(self.finished)
self.thread.add(self.task, self.on_success, self.accept, self.on_error)
self.exec()
def hello(self):
pass
def finished(self):
_logger.info("finished")
def wait(self):
self.thread.wait()
def on_accepted(self):
self.thread.stop()
def update_message(self,msg):
self.message_label.setText(msg)
def update(self, msg):
self.updatemessage.emit(msg)
def getText(self):
return self.message_label.text()
def closeEvent(self,event):
self.thread.stop()
class BalBlockingWaitingDialog(BalDialog):
def __init__(self, bal_window: 'BalWindow', message: str, task: Callable[[], Any]):
BalDialog.__init__(self, bal_window, _("Please wait"))
self.message_label = QLabel(message)
vbox = QVBoxLayout(self)
vbox.addWidget(self.message_label)
self.finished.connect(self.deleteLater) # see #3956
# show popup
self.show()
# refresh GUI; needed for popup to appear and for message_label to get drawn
QCoreApplication.processEvents()
QCoreApplication.processEvents()
try:
# block and run given task
task()
finally:
# close popup
self.accept()
class bal_checkbox(QCheckBox):
def __init__(self, plugin,variable,window=None):
QCheckBox.__init__(self)
self.setChecked(plugin.config_get(variable))
def on_check(v):
plugin.config.set_key(variable, v == 2)
plugin.config_get(variable)
self.stateChanged.connect(on_check)

View File

@@ -1,384 +0,0 @@
from .baldialog import BalDialog
from . import qt_resources
if qt_resources.QT_VERSION == 5:
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QLabel, QVBoxLayout, QCheckBox,QWidget
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject,QEventLoop
else:
from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject,QEventLoop
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QLabel, QVBoxLayout, QCheckBox, QWidget
import time
from electrum.i18n import _
from electrum.gui.qt.util import WindowModalDialog, TaskThread
from electrum.network import Network,TxBroadcastError, BestEffortRequestFailed
from electrum.logging import get_logger
from functools import partial
import copy
from .. import util as Util
from .. import will as Will
from .. import willexecutors as Willexecutors
_logger = get_logger(__name__)
class BalCloseDialog(BalDialog):
updatemessage=pyqtSignal()
def __init__(self,bal_window):
BalDialog.__init__(self,bal_window.window,"Closing BAL")
self.updatemessage.connect(self.update)
self.bal_window=bal_window
self.message_label = QLabel("Closing BAL:")
self.vbox = QVBoxLayout(self)
self.vbox.addWidget(self.message_label)
self.qwidget=QWidget()
self.vbox.addWidget(self.qwidget)
self.labels=[]
#self.checking.connect(self.msg_set_checking)
#self.invalidating.connect(self.msg_set_invalidating)
#self.building.connect(self.msg_set_building)
#self.signing.connect(self.msg_set_signing)
#self.pushing.connect(self.msg_set_pushing)
#self.askpassword.connect(self.ask_password)
#self.passworddone.connect(self.password_done)
self.check_row = None
self.inval_row = None
self.build_row = None
self.sign_row = None
self.push_row = None
self.network = Network.get_instance()
self._stopping = False
self.thread = TaskThread(self)
self.thread.finished.connect(self.task_finished) # see #3956
def task_finished(self):
pass
#_logger.trace("task finished")
def close_plugin_task(self):
_logger.debug("close task to be started")
self.thread.add(self.task_phase1,on_success=self.on_success_phase1,on_done=self.on_accept,on_error=self.on_error_phase1)
self.show()
self.exec()
def task_phase1(self):
_logger.debug("close plugin phase 1 started")
try:
self.bal_window.init_class_variables()
except Will.NoHeirsException:
return False, None
self.msg_set_status("checking variables","Waiting")
try:
Will.check_amounts(self.bal_window.heirs,self.bal_window.willexecutors,self.bal_window.window.wallet.get_utxos(),self.bal_window.date_to_check,self.bal_window.window.wallet.dust_threshold())
except Will.AmountException:
self.msg_edit_row('<font color="#ff0000">'+_("In the inheritance process, the entire wallet will always be fully emptied. Your settings require an adjustment of the amounts"+"</font>"))
#self.bal_window.show_warning(_("In the inheritance process, the entire wallet will always be fully emptied. Your settings require an adjustment of the amounts"),parent=self)
self.msg_set_checking()
have_to_build=False
try:
self.bal_window.check_will()
self.msg_set_checking('Ok')
except Will.WillExpiredException as e:
self.msg_set_checking("Expired")
fee_per_byte=self.bal_window.will_settings.get('tx_fees',1)
return None, Will.invalidate_will(self.bal_window.willitems,self.bal_window.wallet,fee_per_byte)
except Will.NoHeirsException:
self.msg_set_checking("No Heirs")
except Will.NotCompleteWillException as e:
message = False
have_to_build=True
if isinstance(e,Will.HeirChangeException):
message ="Heirs changed:"
elif isinstance(e,Will.WillExecutorNotPresent):
message = "Will-Executor not present"
elif isinstance(e,Will.WillexecutorChangeException):
message = "Will-Executor changed"
elif isinstance(e,Will.TxFeesChangedException):
message = "Txfees are changed"
elif isinstance(e,Will.HeirNotFoundException):
message = "Heir not found"
if message:
self.msg_set_checking(message)
else:
self.msg_set_checking("New")
if have_to_build:
self.msg_set_building()
try:
self.bal_window.build_will()
self.bal_window.check_will()
for wid in Will.only_valid(self.bal_window.willitems):
self.bal_window.wallet.set_label(wid,"BAL Transaction")
self.msg_set_building("Ok")
except Exception as e:
self.msg_set_building(self.msg_error(e))
return False,None
have_to_sign = False
for wid in Will.only_valid(self.bal_window.willitems):
if not self.bal_window.willitems[wid].get_status("COMPLETE"):
have_to_sign = True
break
return have_to_sign, None
def on_accept(self):
pass
def on_accept_phase2(self):
pass
def on_error_push(self):
pass
def wait(self,secs):
wait_row=None
for i in range(secs,0,-1):
if self._stopping:
return
wait_row = self.msg_edit_row(f"Please wait {i}secs", wait_row)
time.sleep(1)
self.msg_del_row(wait_row)
def loop_broadcast_invalidating(self,tx):
self.msg_set_invalidating("Broadcasting")
try:
tx.add_info_from_wallet(self.bal_window.wallet)
self.network.run_from_another_thread(tx.add_info_from_network(self.network))
txid = self.network.run_from_another_thread(self.network.broadcast_transaction(tx,timeout=120),timeout=120)
self.msg_set_invalidating("Ok")
if not txid:
_logger.debug(f"should not be none txid: {txid}")
except TxBroadcastError as e:
_logger.error(e)
msg = e.get_message_for_gui()
self.msg_set_invalidating(self.msg_error(msg))
except BestEffortRequestFailed as e:
self.msg_set_invalidating(self.msg_error(e))
# self.loop_broadcast_invalidating(tx)
def loop_push(self):
self.msg_set_pushing("Broadcasting")
retry = False
try:
willexecutors=Willexecutors.get_willexecutor_transactions(self.bal_window.willitems)
for url,willexecutor in willexecutors.items():
try:
if Willexecutors.is_selected(self.bal_window.willexecutors.get(url)):
_logger.debug(f"{url}: {willexecutor}")
if not Willexecutors.push_transactions_to_willexecutor(willexecutor):
for wid in willexecutor['txsids']:
self.bal_window.willitems[wid].set_status('PUSH_FAIL',True)
retry=True
else:
for wid in willexecutor['txsids']:
self.bal_window.willitems[wid].set_status('PUSHED',True)
except Willexecutors.AlreadyPresentException:
for wid in willexecutor['txsids']:
row = self.msg_edit_row("checking {} - {} : {}".format(self.bal_window.willitems[wid].we['url'],wid, "Waiting"))
self.bal_window.willitems[wid].check_willexecutor()
row = self.msg_edit_row("checked {} - {} : {}".format(self.bal_window.willitems[wid].we['url'],wid,self.bal_window.willitems[wid].get_status("CHECKED" )),row)
except Exception as e:
_logger.error(e)
raise e
if retry:
raise Exception("retry")
except Exception as e:
self.msg_set_pushing(self.msg_error(e))
self.wait(10)
if not self._stopping:
self.loop_push()
def invalidate_task(self,tx,password):
_logger.debug(f"invalidate tx: {tx}")
tx = self.bal_window.wallet.sign_transaction(tx,password)
try:
if tx:
if tx.is_complete():
self.loop_broadcast_invalidating(tx)
self.wait(5)
else:
raise
else:
raise
except Exception as e:
self.msg_set_invalidating("Error")
raise Exception("Impossible to sign")
def on_success_invalidate(self,success):
self.thread.add(self.task_phase1,on_success=self.on_success_phase1,on_done=self.on_accept,on_error=self.on_error_phase1)
def on_error(self,error):
_logger.error(error)
pass
def on_success_phase1(self,result):
self.have_to_sign,tx = list(result)
#if have_to_sign is False and tx is None:
#self._stopping=True
#self.on_success_phase2()
# return
_logger.debug("have to sign {}".format(self.have_to_sign))
password=None
if self.have_to_sign is None:
self.msg_set_invalidating()
#need to sign invalidate and restart phase 1
password = self.bal_window.get_wallet_password("Invalidate your old will",parent=self.bal_window.window)
if password is False:
self.msg_set_invalidating("Aborted")
self.wait(3)
self.close()
return
self.thread.add(partial(self.invalidate_task,tx,password),on_success=self.on_success_invalidate, on_done=self.on_accept, on_error=self.on_error)
return
elif self.have_to_sign:
password = self.bal_window.get_wallet_password("Sign your will",parent=self.bal_window.window)
if password is False:
self.msg_set_signing('Aborted')
else:
self.msg_set_signing('Nothing to do')
self.thread.add(partial(self.task_phase2,password),on_success=self.on_success_phase2,on_done=self.on_accept_phase2,on_error=self.on_error_phase2)
return
def on_success_phase2(self,arg=False):
self.thread.stop()
self.bal_window.save_willitems()
self.msg_edit_row("Finished")
self.close()
def closeEvent(self,event):
self._stopping=True
self.thread.stop()
def task_phase2(self,password):
if self.have_to_sign:
try:
if txs:=self.bal_window.sign_transactions(password):
for txid,tx in txs.items():
self.bal_window.willitems[txid].tx = copy.deepcopy(tx)
self.bal_window.save_willitems()
self.msg_set_signing("Ok")
except Exception as e:
self.msg_set_signing(self.msg_error(e))
self.msg_set_pushing()
have_to_push = False
for wid in Will.only_valid(self.bal_window.willitems):
w=self.bal_window.willitems[wid]
if w.we and w.get_status("COMPLETE") and not w.get_status("PUSHED"):
have_to_push = True
if not have_to_push:
self.msg_set_pushing("Nothing to do")
else:
try:
self.loop_push()
self.msg_set_pushing("Ok")
except Exception as e:
self.msg_set_pushing(self.msg_error(e))
self.msg_edit_row("Ok")
self.wait(5)
def on_error_phase1(self,error):
_logger.error(f"error phase1: {error}")
def on_error_phase2(self,error):
_logger.error("error phase2: { error}")
def msg_set_checking(self, status = None, row = None):
row = self.check_row if row is None else row
self.check_row = self.msg_set_status("Checking your will", row, status)
def msg_set_invalidating(self, status = None, row = None):
row = self.inval_row if row is None else row
self.inval_row = self.msg_set_status("Invalidating old will", self.inval_row, status)
def msg_set_building(self, status = None, row = None):
row = self.build_row if row is None else row
self.build_row = self.msg_set_status("Building your will", self.build_row, status)
def msg_set_signing(self, status = None, row = None):
row = self.sign_row if row is None else row
self.sign_row = self.msg_set_status("Signing your will", self.sign_row, status)
def msg_set_pushing(self, status = None, row = None):
row = self.push_row if row is None else row
self.push_row = self.msg_set_status("Broadcasting your will to executors", self.push_row, status)
def msg_set_waiting(self, status = None, row = None):
row = self.wait_row if row is None else row
self.wait_row = self.msg_edit_row(f"Please wait {status}secs", self.wait_row)
def msg_error(self,e):
return "Error: {}".format(e)
def msg_set_status(self,msg,row,status=None):
status= "Wait" if status is None else status
line="{}:\t{}".format(_(msg), status)
return self.msg_edit_row(line,row)
#return v$msg_edit_row("{}:\t{}".format(_(msg), status), row)
def ask_password(self,msg=None):
self.password=self.bal_window.get_wallet_password(msg,parent=self)
def msg_edit_row(self,line,row=None):
_logger.debug(f"{row},{line}")
#msg=self.get_text()
#rows=msg.split("\n")
#try:
# rows[row]=line
#except Exception as e:
# rows.append(line)
#row=len(rows)-1
#self.update("\n".join(rows))
#return row
#def msg_edit_label(self,line,row=None):
#_logger.trace(f"{row},{line}")
#msg=self.get_text()
#rows=msg.split("\n")
try:
self.labels[row]=line
except Exception as e:
self.labels.append(line)
row=len(self.labels)-1
self.updatemessage.emit()
return row
def msg_del_row(self,row):
#_logger.trace(f"del row: {row}")
try:
del self.labels[row]
except Exception as e:
pass
self.updatemessage.emit()
def update(self):
self.vbox.removeWidget(self.qwidget)
self.qwidget=QWidget(self)
labelsbox = QVBoxLayout(self.qwidget)
for label in self.labels:
labelsbox.addWidget(QLabel(label))
self.vbox.addWidget(self.qwidget)
def get_text(self):
return self.message_label.text()
def ThreadStopped(Exception):
pass

View File

@@ -1,283 +0,0 @@
#!/usr/bin/env python
#
# Electrum - lightweight Bitcoin client
# Copyright (C) 2015 Thomas Voegtlin
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import enum
from typing import TYPE_CHECKING
from datetime import datetime
from . import qt_resources
if qt_resources.QT_VERSION == 5:
from PyQt5.QtGui import QStandardItemModel, QStandardItem
from PyQt5.QtCore import Qt, QPersistentModelIndex, QModelIndex
from PyQt5.QtWidgets import (QAbstractItemView, QMenu,QWidget,QHBoxLayout,QLabel,QSpinBox,QPushButton)
else:
from PyQt6.QtGui import QStandardItemModel, QStandardItem
from PyQt6.QtCore import Qt, QPersistentModelIndex, QModelIndex
from PyQt6.QtWidgets import (QAbstractItemView, QMenu,QWidget,QHBoxLayout,QLabel,QSpinBox,QPushButton)
from electrum.i18n import _
from electrum.bitcoin import is_address
from electrum.util import block_explorer_URL
from electrum.plugin import run_hook
from electrum.gui.qt.util import webopen, MessageBoxMixin,HelpButton
from electrum.gui.qt.my_treeview import MyTreeView, MySortModel
from .. import util as Util
from .locktimeedit import HeirsLockTimeEdit
if TYPE_CHECKING:
from electrum.gui.qt.main_window import ElectrumWindow
class HeirList(MyTreeView,MessageBoxMixin):
class Columns(MyTreeView.BaseColumnsEnum):
NAME = enum.auto()
ADDRESS = enum.auto()
AMOUNT = enum.auto()
headers = {
Columns.NAME: _('Name'),
Columns.ADDRESS: _('Address'),
Columns.AMOUNT: _('Amount'),
#Columns.LOCKTIME:_('LockTime'),
}
filter_columns = [Columns.NAME, Columns.ADDRESS]
ROLE_SORT_ORDER = Qt.ItemDataRole.UserRole + 1000
ROLE_HEIR_KEY = Qt.ItemDataRole.UserRole + 1001
key_role = ROLE_HEIR_KEY
def __init__(self, bal_window: 'BalWindow'):
super().__init__(
parent=bal_window.window,
main_window=bal_window.window,
stretch_column=self.Columns.NAME,
editable_columns=[self.Columns.NAME,self.Columns.ADDRESS,self.Columns.AMOUNT],
)
self.decimal_point = bal_window.bal_plugin.config.get_decimal_point()
self.bal_window = bal_window
try:
self.setModel(QStandardItemModel(self))
self.sortByColumn(self.Columns.NAME, Qt.SortOrder.AscendingOrder)
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
except:
pass
#self.sortByColumn(self.Columns.NAME, Qt.SortOrder.AscendingOrder)
#self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
self.setSortingEnabled(True)
self.std_model = self.model()
self.update()
def on_edited(self, idx, edit_key, *, text):
original = prior_name = self.bal_window.heirs.get(edit_key)
if not prior_name:
return
col = idx.column()
try:
#if col == 3:
# try:
# text = Util.str_to_locktime(text)
# except:
# print("not a valid locktime")
# pass
if col == 2:
text = Util.encode_amount(text,self.decimal_point)
elif col == 0:
self.bal_window.delete_heirs([edit_key])
edit_key = text
prior_name[col-1] = text
prior_name.insert(0,edit_key)
prior_name = tuple(prior_name)
except Exception as e:
#print("eccezione tupla",e)
prior_name = (edit_key,)+prior_name[:col-1]+(text,)+prior_name[col:]
#print("prior_name",prior_name,original)
try:
self.bal_window.set_heir(prior_name)
#print("setheir")
except Exception as e:
pass
#print("heir non valido ripristino l'originale",e)
try:
#print("setup_original",(edit_key,)+original)
self.bal_window.set_heir((edit_key,)+original)
except Exception as e:
#print("errore nellimpostare original",e,original)
self.update()
def create_menu(self, position):
menu = QMenu()
idx = self.indexAt(position)
column = idx.column() or self.Columns.NAME
selected_keys = []
for s_idx in self.selected_in_column(self.Columns.NAME):
#print(s_idx)
sel_key = self.model().itemFromIndex(s_idx).data(0)
selected_keys.append(sel_key)
if selected_keys and idx.isValid():
column_title = self.model().horizontalHeaderItem(column).text()
column_data = '\n'.join(self.model().itemFromIndex(s_idx).text()
for s_idx in self.selected_in_column(column))
menu.addAction(_("Copy {}").format(column_title), lambda: self.place_text_on_clipboard(column_data, title=column_title))
if column in self.editable_columns:
item = self.model().itemFromIndex(idx)
if item.isEditable():
# would not be editable if openalias
persistent = QPersistentModelIndex(idx)
menu.addAction(_("Edit {}").format(column_title), lambda p=persistent: self.edit(QModelIndex(p)))
menu.addAction(_("Delete"), lambda: self.bal_window.delete_heirs(selected_keys))
menu.exec(self.viewport().mapToGlobal(position))
def update(self):
if self.maybe_defer_update():
return
current_key = self.get_role_data_for_current_item(col=self.Columns.NAME, role=self.ROLE_HEIR_KEY)
self.model().clear()
self.update_headers(self.__class__.headers)
set_current = None
for key in sorted(self.bal_window.heirs.keys()):
heir = self.bal_window.heirs[key]
labels = [""] * len(self.Columns)
labels[self.Columns.NAME] = key
labels[self.Columns.ADDRESS] = heir[0]
labels[self.Columns.AMOUNT] = Util.decode_amount(heir[1],self.decimal_point)
#labels[self.Columns.LOCKTIME] = str(Util.locktime_to_str(heir[2]))
items = [QStandardItem(x) for x in labels]
items[self.Columns.NAME].setEditable(True)
items[self.Columns.ADDRESS].setEditable(True)
items[self.Columns.AMOUNT].setEditable(True)
#items[self.Columns.LOCKTIME].setEditable(True)
items[self.Columns.NAME].setData(key, self.ROLE_HEIR_KEY+1)
items[self.Columns.ADDRESS].setData(key, self.ROLE_HEIR_KEY+2)
items[self.Columns.AMOUNT].setData(key, self.ROLE_HEIR_KEY+3)
#items[self.Columns.LOCKTIME].setData(key, self.ROLE_HEIR_KEY+4)
self.model().insertRow(self.model().rowCount(), items)
if key == current_key:
idx = self.model().index(row_count, self.Columns.NAME)
set_current = QPersistentModelIndex(idx)
self.set_current_idx(set_current)
# FIXME refresh loses sort order; so set "default" here:
self.filter()
run_hook('update_heirs_tab', self)
def refresh_row(self, key, row):
# nothing to update here
pass
def get_edit_key_from_coordinate(self, row, col):
#print("role_data",self.get_role_data_from_coordinate(row, col, role=self.ROLE_HEIR_KEY))
#print(col)
return self.get_role_data_from_coordinate(row, col, role=self.ROLE_HEIR_KEY+col+1)
return col
def create_toolbar(self, config):
toolbar, menu = self.create_toolbar_with_menu('')
menu.addAction(_("&New Heir"), self.bal_window.new_heir_dialog)
menu.addAction(_("Import"), self.bal_window.import_heirs)
menu.addAction(_("Export"), lambda: self.bal_window.export_heirs())
#menu.addAction(_("Build Traonsactions"), self.build_transactions)
self.heir_locktime = HeirsLockTimeEdit(self.window(),0)
def on_heir_locktime():
if not self.heir_locktime.get_locktime():
self.heir_locktime.set_locktime('1y')
self.bal_window.will_settings['locktime'] = self.heir_locktime.get_locktime() if self.heir_locktime.get_locktime() else "1y"
self.bal_window.bal_plugin.config.set_key('will_settings',self.bal_window.will_settings,save = True)
self.heir_locktime.valueEdited.connect(on_heir_locktime)
self.heir_threshold = HeirsLockTimeEdit(self,0)
def on_heir_threshold():
if not self.heir_threshold.get_locktime():
self.heir_threshold.set_locktime('180d')
self.bal_window.will_settings['threshold'] = self.heir_threshold.get_locktime()
self.bal_window.bal_plugin.config.set_key('will_settings',self.bal_window.will_settings,save = True)
self.heir_threshold.valueEdited.connect(on_heir_threshold)
self.heir_tx_fees = QSpinBox()
self.heir_tx_fees.setMinimum(1)
self.heir_tx_fees.setMaximum(10000)
def on_heir_tx_fees():
if not self.heir_tx_fees.value():
self.heir_tx_fees.set_value(1)
self.bal_window.will_settings['tx_fees'] = self.heir_tx_fees.value()
self.bal_window.bal_plugin.config.set_key('will_settings',self.bal_window.will_settings,save = True)
self.heir_tx_fees.valueChanged.connect(on_heir_tx_fees)
self.heirs_widget = QWidget()
layout = QHBoxLayout()
self.heirs_widget.setLayout(layout)
layout.addWidget(QLabel(_("Delivery Time:")))
layout.addWidget(self.heir_locktime)
layout.addWidget(HelpButton(_("Locktime* to be used in the transaction\n"
+"if you choose Raw, you can insert various options based on suffix:\n"
#+" - b: number of blocks after current block(ex: 144b means tomorrow)\n"
+" - d: number of days after current day(ex: 1d means tomorrow)\n"
+" - y: number of years after currrent day(ex: 1y means one year from today)\n"
+"* locktime can be anticipated to update will\n")))
layout.addWidget(QLabel(" "))
layout.addWidget(QLabel(_("Check Alive:")))
layout.addWidget(self.heir_threshold)
layout.addWidget(HelpButton(_("Check to ask for invalidation.\n"
+"When less then this time is missing, ask to invalidate.\n"
+"If you fail to invalidate during this time, your transactions will be delivered to your heirs.\n"
+"if you choose Raw, you can insert various options based on suffix:\n"
#+" - b: number of blocks after current block(ex: 144b means tomorrow)\n"
+" - d: number of days after current day(ex: 1d means tomorrow).\n"
+" - y: number of years after currrent day(ex: 1y means one year from today).\n\n")))
layout.addWidget(QLabel(" "))
layout.addWidget(QLabel(_("Fees:")))
layout.addWidget(self.heir_tx_fees)
layout.addWidget(HelpButton(_("Fee to be used in the transaction")))
layout.addWidget(QLabel("sats/vbyte"))
layout.addWidget(QLabel(" "))
newHeirButton = QPushButton(_("New Heir"))
newHeirButton.clicked.connect(self.bal_window.new_heir_dialog)
layout.addWidget(newHeirButton)
toolbar.insertWidget(2, self.heirs_widget)
return toolbar
def update_will_settings(self):
self.heir_threshold.set_locktime(self.bal_window.will_settings['threshold'])
self.heir_locktime.set_locktime(self.bal_window.will_settings['locktime'])
self.heir_tx_fees.setValue(int(self.bal_window.will_settings['tx_fees']))
def build_transactions(self):
will = self.bal_window.prepare_will()

View File

@@ -1,255 +0,0 @@
# Copyright (C) 2020 The Electrum developers
# Distributed under the MIT software license, see the accompanying
# file LICENCE or http://www.opensource.org/licenses/mit-license.php
import time
from datetime import datetime
from typing import Optional, Any
from . import qt_resources
if qt_resources.QT_VERSION == 5:
from PyQt5.QtCore import Qt, QDateTime, pyqtSignal
from PyQt5.QtGui import QPalette, QPainter
from PyQt5.QtWidgets import (QWidget, QLineEdit, QStyle, QStyleOptionFrame, QComboBox, QHBoxLayout, QDateTimeEdit)
else:
from PyQt6.QtCore import Qt, QDateTime, pyqtSignal
from PyQt6.QtGui import QPalette, QPainter
from PyQt6.QtWidgets import (QWidget, QLineEdit, QStyle, QStyleOptionFrame, QComboBox, QHBoxLayout, QDateTimeEdit)
from electrum.i18n import _
from electrum.bitcoin import NLOCKTIME_MIN, NLOCKTIME_MAX, NLOCKTIME_BLOCKHEIGHT_MAX
from electrum.gui.qt.util import char_width_in_lineedit, ColorScheme
class HeirsLockTimeEdit(QWidget):
valueEdited = pyqtSignal()
locktime_threshold = 50000000
def __init__(self, parent=None,default_index = 1):
QWidget.__init__(self, parent)
hbox = QHBoxLayout()
self.setLayout(hbox)
hbox.setContentsMargins(0, 0, 0, 0)
hbox.setSpacing(0)
self.locktime_raw_e = LockTimeRawEdit(self,time_edit = self)
#self.locktime_height_e = LockTimeHeightEdit(self)
self.locktime_date_e = LockTimeDateEdit(self,time_edit = self)
#self.editors = [self.locktime_raw_e, self.locktime_height_e, self.locktime_date_e]
self.editors = [self.locktime_raw_e, self.locktime_date_e]
self.combo = QComboBox()
#options = [_("Raw"), _("Block height"), _("Date")]
options = [_("Raw"),_("Date")]
self.option_index_to_editor_map = {
0: self.locktime_raw_e,
#1: self.locktime_height_e,
1: self.locktime_date_e,
#2: self.locktime_date_e,
}
self.combo.addItems(options)
self.editor = self.option_index_to_editor_map[default_index]
self.combo.currentIndexChanged.connect(self.on_current_index_changed)
self.combo.setCurrentIndex(default_index)
self.on_current_index_changed(default_index)
hbox.addWidget(self.combo)
for w in self.editors:
hbox.addWidget(w)
hbox.addStretch(1)
#self.locktime_height_e.textEdited.connect(self.valueEdited.emit)
self.locktime_raw_e.editingFinished.connect(self.valueEdited.emit)
self.locktime_date_e.dateTimeChanged.connect(self.valueEdited.emit)
self.combo.currentIndexChanged.connect(self.valueEdited.emit)
def on_current_index_changed(self,i):
for w in self.editors:
w.setVisible(False)
w.setEnabled(False)
prev_locktime = self.editor.get_locktime()
self.editor = self.option_index_to_editor_map[i]
if self.editor.is_acceptable_locktime(prev_locktime):
self.editor.set_locktime(prev_locktime,force=True)
self.editor.setVisible(True)
self.editor.setEnabled(True)
def get_locktime(self) -> Optional[str]:
return self.editor.get_locktime()
def set_index(self,index):
self.combo.setCurrentIndex(index)
self.on_current_index_changed(index)
def set_locktime(self, x: Any,force=True) -> None:
self.editor.set_locktime(x,force)
class _LockTimeEditor:
min_allowed_value = NLOCKTIME_MIN
max_allowed_value = NLOCKTIME_MAX
def get_locktime(self) -> Optional[int]:
raise NotImplementedError()
def set_locktime(self, x: Any,force=True) -> None:
raise NotImplementedError()
@classmethod
def is_acceptable_locktime(cls, x: Any) -> bool:
if not x: # e.g. empty string
return True
try:
x = int(x)
except Exception as e:
return False
return cls.min_allowed_value <= x <= cls.max_allowed_value
class LockTimeRawEdit(QLineEdit, _LockTimeEditor):
def __init__(self, parent=None,time_edit=None):
QLineEdit.__init__(self, parent)
self.setFixedWidth(14 * char_width_in_lineedit())
self.textChanged.connect(self.numbify)
self.isdays = False
self.isyears = False
self.isblocks = False
self.time_edit=time_edit
def replace_str(self,text):
return str(text).replace('d','').replace('y','').replace('b','')
def checkbdy(self,s,pos,appendix):
try:
charpos = pos-1
charpos = max(0,charpos)
charpos = min(len(s)-1,charpos)
if appendix == s[charpos]:
s=self.replace_str(s)+appendix
pos = charpos
except Exception as e:
pass
return pos, s
def numbify(self):
text = self.text().strip()
#chars = '0123456789bdy' removed the option to choose locktime by block
chars = '0123456789dy'
pos = posx = self.cursorPosition()
pos = len(''.join([i for i in text[:pos] if i in chars]))
s = ''.join([i for i in text if i in chars])
self.isdays = False
self.isyears = False
self.isblocks = False
pos,s = self.checkbdy(s,pos,'d')
pos,s = self.checkbdy(s,pos,'y')
pos,s = self.checkbdy(s,pos,'b')
if 'd' in s: self.isdays = True
if 'y' in s: self.isyears = True
if 'b' in s: self.isblocks = True
if self.isdays: s= self.replace_str(s) + 'd'
if self.isyears: s = self.replace_str(s) + 'y'
if self.isblocks: s= self.replace_str(s) + 'b'
self.set_locktime(s,force=False)
# setText sets Modified to False. Instead we want to remember
# if updates were because of user modification.
self.setModified(self.hasFocus())
self.setCursorPosition(pos)
def get_locktime(self) -> Optional[str]:
try:
return str(self.text())
except Exception as e:
return None
def set_locktime(self, x: Any,force=True) -> None:
out = str(x)
if 'd' in out:
out = self.replace_str(x)+'d'
elif 'y' in out:
out = self.replace_str(x)+'y'
elif 'b' in out:
out = self.replace_str(x)+'b'
else:
try:
out = int(x)
except Exception as e:
self.setText('')
return
out = max(out, self.min_allowed_value)
out = min(out, self.max_allowed_value)
self.setText(str(out))
#try:
# if self.time_edit and int(out)>self.time_edit.locktime_threshold and not force:
# self.time_edit.set_index(1)
#except:
# pass
class LockTimeHeightEdit(LockTimeRawEdit):
max_allowed_value = NLOCKTIME_BLOCKHEIGHT_MAX
def __init__(self, parent=None,time_edit=None):
LockTimeRawEdit.__init__(self, parent)
self.setFixedWidth(20 * char_width_in_lineedit())
self.time_edit = time_edit
def paintEvent(self, event):
super().paintEvent(event)
panel = QStyleOptionFrame()
self.initStyleOption(panel)
textRect = self.style().subElementRect(QStyle.SE_LineEditContents, panel, self)
textRect.adjust(2, 0, -10, 0)
painter = QPainter(self)
painter.setPen(ColorScheme.GRAY.as_color())
painter.drawText(textRect, int(Qt.AlignRight | Qt.AlignVCenter), "height")
def get_max_allowed_timestamp() -> int:
ts = NLOCKTIME_MAX
# Test if this value is within the valid timestamp limits (which is platform-dependent).
# see #6170
try:
datetime.fromtimestamp(ts)
except (OSError, OverflowError):
ts = 2 ** 31 - 1 # INT32_MAX
datetime.fromtimestamp(ts) # test if raises
return ts
class LockTimeDateEdit(QDateTimeEdit, _LockTimeEditor):
min_allowed_value = NLOCKTIME_BLOCKHEIGHT_MAX + 1
max_allowed_value = get_max_allowed_timestamp()
def __init__(self, parent=None,time_edit=None):
QDateTimeEdit.__init__(self, parent)
self.setMinimumDateTime(datetime.fromtimestamp(self.min_allowed_value))
self.setMaximumDateTime(datetime.fromtimestamp(self.max_allowed_value))
self.setDateTime(QDateTime.currentDateTime())
self.time_edit = time_edit
def get_locktime(self) -> Optional[int]:
dt = self.dateTime().toPyDateTime()
locktime = int(time.mktime(dt.timetuple()))
return locktime
def set_locktime(self, x: Any,force = False) -> None:
if not self.is_acceptable_locktime(x):
self.setDateTime(QDateTime.currentDateTime())
return
try:
x = int(x)
except Exception:
self.setDateTime(QDateTime.currentDateTime())
return
dt = datetime.fromtimestamp(x)
self.setDateTime(dt)

View File

@@ -1,331 +0,0 @@
import enum
import copy
import json
import urllib.request
import urllib.parse
from functools import partial
from . import qt_resources
if qt_resources.QT_VERSION == 5:
from PyQt5.QtGui import QStandardItemModel, QStandardItem, QPalette, QColor
from PyQt5.QtCore import Qt,QPersistentModelIndex, QModelIndex
from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QLabel,QMenu,QAbstractItemView,QWidget)
else:
from PyQt6.QtGui import QStandardItemModel, QStandardItem, QPalette, QColor
from PyQt6.QtCore import Qt,QPersistentModelIndex, QModelIndex
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QLabel,QMenu,QAbstractItemView,QWidget)
from electrum.i18n import _
from electrum.gui.qt.util import (Buttons,read_QIcon, import_meta_gui, export_meta_gui,MessageBoxMixin)
from electrum.util import write_json_file,read_json_file,FileImportFailed
from electrum.gui.qt.my_treeview import MyTreeView
from electrum.transaction import tx_from_any
from electrum.network import Network
from ..bal import BalPlugin
from .. import willexecutors as Willexecutors
from .. import util as Util
from .. import will as Will
from .baldialog import BalDialog
class PreviewList(MyTreeView):
class Columns(MyTreeView.BaseColumnsEnum):
LOCKTIME = enum.auto()
TXID = enum.auto()
WILLEXECUTOR = enum.auto()
STATUS = enum.auto()
headers = {
Columns.LOCKTIME: _('Locktime'),
Columns.TXID: _('Txid'),
Columns.WILLEXECUTOR: _('Will-Executor'),
Columns.STATUS: _('Status'),
}
ROLE_HEIR_KEY = Qt.ItemDataRole.UserRole + 2000
key_role = ROLE_HEIR_KEY
def __init__(self, parent: 'BalWindow',will):
super().__init__(
parent=parent.window,
stretch_column=self.Columns.TXID,
)
self.decimal_point=parent.bal_plugin.config.get_decimal_point
self.setModel(QStandardItemModel(self))
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
if not will is None:
self.will = will
else:
self.will = parent.willitems
self.bal_window = parent
self.wallet=parent.window.wallet
self.setModel(QStandardItemModel(self))
self.setSortingEnabled(True)
self.std_model = self.model()
self.config = parent.bal_plugin.config
self.bal_plugin=self.bal_window.bal_plugin
self.update()
def create_menu(self, position):
menu = QMenu()
idx = self.indexAt(position)
column = idx.column() or self.Columns.TXID
selected_keys = []
for s_idx in self.selected_in_column(self.Columns.TXID):
sel_key = self.model().itemFromIndex(s_idx).data(0)
selected_keys.append(sel_key)
if selected_keys and idx.isValid():
column_title = self.model().horizontalHeaderItem(column).text()
column_data = '\n'.join(self.model().itemFromIndex(s_idx).text()
for s_idx in self.selected_in_column(column))
menu.addAction(_("details").format(column_title), lambda: self.show_transaction(selected_keys)).setEnabled(len(selected_keys)<2)
menu.addAction(_("check ").format(column_title), lambda: self.check_transactions(selected_keys))
menu.addSeparator()
menu.addAction(_("delete").format(column_title), lambda: self.delete(selected_keys))
menu.exec(self.viewport().mapToGlobal(position))
def delete(self,selected_keys):
for key in selected_keys:
del self.will[key]
try:
del self.bal_window.willitems[key]
except:
pass
try:
del self.bal_window.will[key]
except:
pass
self.update()
def check_transactions(self,selected_keys):
wout = {}
for k in selected_keys:
wout[k] = self.will[k]
if wout:
self.bal_window.check_transactions(wout)
self.update()
def show_transaction(self,selected_keys):
for key in selected_keys:
self.bal_window.show_transaction(self.will[key].tx)
self.update()
def select(self,selected_keys):
self.selected += selected_keys
self.update()
def deselect(self,selected_keys):
for key in selected_keys:
self.selected.remove(key)
self.update()
def update_will(self,will):
self.will.update(will)
self.update()
def update(self):
if self.will is None:
return
current_key = self.get_role_data_for_current_item(col=self.Columns.TXID, role=self.ROLE_HEIR_KEY)
self.model().clear()
self.update_headers(self.__class__.headers)
set_current = None
for txid,bal_tx in self.will.items():
if self.bal_window.bal_plugin._hide_replaced and bal_tx.get_status('REPLACED'):
continue
if self.bal_window.bal_plugin._hide_invalidated and bal_tx.get_status('INVALIDATED'):
continue
tx=bal_tx.tx
labels = [""] * len(self.Columns)
labels[self.Columns.LOCKTIME] = Util.locktime_to_str(tx.locktime)
labels[self.Columns.TXID] = txid
we = 'None'
if bal_tx.we:
we = bal_tx.we['url']
labels[self.Columns.WILLEXECUTOR]=we
status = bal_tx.status
if len(bal_tx.status) > 53:
status = "...{}".format(status[-50:])
labels[self.Columns.STATUS] = status
items=[]
for e in labels:
if type(e)== list:
try:
items.append(QStandardItem(*e))
except Exception as e:
pass
else:
items.append(QStandardItem(str(e)))
#pal = QPalette()
#pal.setColor(QPalette.ColorRole.Window, QColor(bal_tx.get_color()))
#items[-1].setAutoFillBackground(True)
#items[-1o].setPalette(pal)
items[-1].setBackground(QColor(bal_tx.get_color()))
self.model().insertRow(self.model().rowCount(), items)
if txid == current_key:
idx = self.model().index(row_count, self.Columns.TXID)
set_current = QPersistentModelIndex(idx)
self.set_current_idx(set_current)
def create_toolbar(self, config):
toolbar, menu = self.create_toolbar_with_menu('')
menu.addAction(_("Prepare"), self.build_transactions)
menu.addAction(_("Display"), self.bal_window.preview_modal_dialog)
menu.addAction(_("Sign"), self.ask_password_and_sign_transactions)
menu.addAction(_("Export"), self.export_will)
#menu.addAction(_("Import"), self.import_will)
menu.addAction(_("Broadcast"), self.broadcast)
menu.addAction(_("Check"), self.check)
menu.addAction(_("Invalidate"), self.invalidate_will)
prepareButton = QPushButton(_("Prepare"))
prepareButton.clicked.connect(self.build_transactions)
signButton = QPushButton(_("Sign"))
signButton.clicked.connect(self.ask_password_and_sign_transactions)
pushButton = QPushButton(_("Broadcast"))
pushButton.clicked.connect(self.broadcast)
displayButton = QPushButton(_("Display"))
displayButton.clicked.connect(self.bal_window.preview_modal_dialog)
hlayout = QHBoxLayout()
widget = QWidget()
hlayout.addWidget(prepareButton)
hlayout.addWidget(signButton)
hlayout.addWidget(pushButton)
hlayout.addWidget(displayButton)
widget.setLayout(hlayout)
toolbar.insertWidget(2,widget)
return toolbar
def hide_replaced(self):
self.bal_window.bal_plugin.hide_replaced()
self.update()
def hide_invalidated(self):
f.bal_window.bal_plugin.hide_invalidated()
self.update()
def build_transactions(self):
will = self.bal_window.prepare_will()
if will:
self.update_will(will)
def export_json_file(self,path):
write_json_file(path, self.will)
def export_will(self):
self.bal_window.export_will()
self.update()
def import_will(self):
self.bal_window.import_will()
def ask_password_and_sign_transactions(self):
self.bal_window.ask_password_and_sign_transactions(callback=self.update)
def broadcast(self):
self.bal_window.broadcast_transactions()
self.update()
def check(self):
self.bal_window.check_transactions(self.bal_window.willitems)
self.update()
def invalidate_will(self):
self.bal_window.invalidate_will()
self.update()
class PreviewDialog(BalDialog,MessageBoxMixin):
def __init__(self, bal_window, will):
self.parent = bal_window.window
BalDialog.__init__(self,bal_window = bal_window)
self.bal_plugin = bal_window.bal_plugin
self.gui_object = self.bal_plugin.gui_object
self.config = self.bal_plugin.config
self.bal_window = bal_window
self.wallet = bal_window.window.wallet
self.format_amount = bal_window.window.format_amount
self.base_unit = bal_window.window.base_unit
self.format_fiat_and_units = bal_window.window.format_fiat_and_units
self.fx = bal_window.window.fx
self.format_fee_rate = bal_window.window.format_fee_rate
self.show_address = bal_window.window.show_address
if not will:
self.will = bal_window.willitems
else:
self.will = will
self.setWindowTitle(_('Transactions Preview'))
self.setMinimumSize(1000, 200)
self.size_label = QLabel()
self.transactions_list = PreviewList(self.bal_window,self.will)
vbox = QVBoxLayout(self)
vbox.addWidget(self.size_label)
vbox.addWidget(self.transactions_list)
buttonbox = QHBoxLayout()
b = QPushButton(_('Sign'))
b.clicked.connect(self.transactions_list.ask_password_and_sign_transactions)
buttonbox.addWidget(b)
b = QPushButton(_('Export Will'))
b.clicked.connect(self.transactions_list.export_will)
buttonbox.addWidget(b)
b = QPushButton(_('Broadcast'))
b.clicked.connect(self.transactions_list.broadcast)
buttonbox.addWidget(b)
b = QPushButton(_('Invalidate will'))
b.clicked.connect(self.transactions_list.invalidate_will)
buttonbox.addWidget(b)
vbox.addLayout(buttonbox)
self.update()
def update_will(self,will):
self.will.update(will)
self.transactions_list.update_will(will)
self.update()
def update(self):
self.transactions_list.update()
def is_hidden(self):
return self.isMinimized() or self.isHidden()
def show_or_hide(self):
if self.is_hidden():
self.bring_to_top()
else:
self.hide()
def bring_to_top(self):
self.show()
self.raise_()
def closeEvent(self, event):
event.accept()

View File

@@ -1,16 +0,0 @@
from .. import bal_resources
import sys
try:
QT_VERSION=sys._GUI_QT_VERSION
except:
QT_VERSION=6
if QT_VERSION == 5:
from PyQt5.QtGui import QIcon,QPixmap
else:
from PyQt6.QtGui import QIcon,QPixmap
def read_QIcon(icon_basename: str=bal_resources.DEFAULT_ICON) -> QIcon:
return QIcon(bal_resources.icon_path(icon_basename))
def read_QPixmap(icon_basename: str=bal_resources.DEFAULT_ICON) -> QPixmap:
return QPixmap(bal_resources.icon_path(icon_basename))

View File

@@ -1,199 +0,0 @@
from functools import partial
from . import qt_resources
if qt_resources.QT_VERSION == 5:
from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QLabel,QWidget,QScrollArea)
from PyQt5.QtGui import (QPixmap, QImage, QBitmap, QPainter, QFontDatabase, QPen, QFont,
QColor, QDesktopServices, qRgba, QPainterPath,QPalette)
else:
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QLabel,QWidget,QScrollArea)
from PyQt6.QtGui import (QPixmap, QImage, QBitmap, QPainter, QFontDatabase, QPen, QFont,
QColor, QDesktopServices, qRgba, QPainterPath,QPalette)
from electrum.util import decimal_point_to_base_unit_name
from electrum.i18n import _
from ..bal import BalPlugin
from .. import will as Will
from .. import util as Util
from .baldialog import BalDialog
class WillDetailDialog(BalDialog):
def __init__(self, bal_window):
self.will = bal_window.willitems
self.threshold = Util.parse_locktime_string(bal_window.will_settings['threshold'])
self.bal_window = bal_window
Will.add_willtree(self.will)
super().__init__(bal_window.window)
self.config = bal_window.window.config
self.wallet = bal_window.wallet
self.format_amount = bal_window.window.format_amount
self.base_unit = bal_window.window.base_unit
self.format_fiat_and_units = bal_window.window.format_fiat_and_units
self.fx = bal_window.window.fx
self.format_fee_rate = bal_window.window.format_fee_rate
self.decimal_point = bal_window.bal_plugin.config.get_decimal_point()
self.base_unit_name = decimal_point_to_base_unit_name(self.decimal_point)
self.setWindowTitle(_('Will Details'))
self.setMinimumSize(670,700)
self.vlayout= QVBoxLayout()
w=QWidget()
hlayout = QHBoxLayout(w)
b = QPushButton(_('Sign'))
b.clicked.connect(self.ask_password_and_sign_transactions)
hlayout.addWidget(b)
b = QPushButton(_('Broadcast'))
b.clicked.connect(self.broadcast_transactions)
hlayout.addWidget(b)
b = QPushButton(_('Export'))
b.clicked.connect(self.export_will)
hlayout.addWidget(b)
"""
toggle = "Hide"
if self.bal_window.bal_plugin._hide_replaced:
toggle = "Unhide"
self.toggle_replace_button = QPushButton(_(f"{toggle} replaced"))
self.toggle_replace_button.clicked.connect(self.toggle_replaced)
hlayout.addWidget(self.toggle_replace_button)
toggle = "Hide"
if self.bal_window.bal_plugin._hide_invalidated:
toggle = "Unhide"
self.toggle_invalidate_button = QPushButton(_(f"{toggle} invalidated"))
self.toggle_invalidate_button.clicked.connect(self.toggle_invalidated)
hlayout.addWidget(self.toggle_invalidate_button)
"""
b = QPushButton(_('Invalidate'))
b.clicked.connect(bal_window.invalidate_will)
hlayout.addWidget(b)
self.vlayout.addWidget(w)
self.paint_scroll_area()
#vlayout.addWidget(QLabel(_("DON'T PANIC !!! everything is fine, all possible futures are covered")))
self.vlayout.addWidget(QLabel(_("Expiration date: ")+Util.locktime_to_str(self.threshold)))
self.vlayout.addWidget(self.scrollbox)
w=QWidget()
hlayout = QHBoxLayout(w)
hlayout.addWidget(QLabel(_("Valid Txs:")+ str(len(Will.only_valid_list(self.will)))))
hlayout.addWidget(QLabel(_("Total Txs:")+ str(len(self.will))))
self.vlayout.addWidget(w)
self.setLayout(self.vlayout)
def paint_scroll_area(self):
#self.scrollbox.deleteLater()
#self.willlayout.deleteLater()
#self.detailsWidget.deleteLater()
self.scrollbox = QScrollArea()
viewport = QWidget(self.scrollbox)
self.willlayout = QVBoxLayout(viewport)
self.detailsWidget = WillWidget(parent=self)
self.willlayout.addWidget(self.detailsWidget)
self.scrollbox.setWidget(viewport)
viewport.setLayout(self.willlayout)
def ask_password_and_sign_transactions(self):
self.bal_window.ask_password_and_sign_transactions(callback=self.update)
self.update()
def broadcast_transactions(self):
self.bal_window.broadcast_transactions()
self.update()
def export_will(self):
self.bal_window.export_will()
def toggle_replaced(self):
self.bal_window.bal_plugin.hide_replaced()
toggle = _("Hide")
if self.bal_window.bal_plugin._hide_replaced:
toggle = _("Unhide")
self.toggle_replace_button.setText(f"{toggle} {_('replaced')}")
self.update()
def toggle_invalidated(self):
self.bal_window.bal_plugin.hide_invalidated()
toggle = _("Hide")
if self.bal_window.bal_plugin._hide_invalidated:
toggle = _("Unhide")
self.toggle_invalidate_button.setText(_(f"{toggle} {_('invalidated')}"))
self.update()
def update(self):
self.will = self.bal_window.willitems
pos = self.vlayout.indexOf(self.scrollbox)
self.vlayout.removeWidget(self.scrollbox)
self.paint_scroll_area()
self.vlayout.insertWidget(pos,self.scrollbox)
super().update()
class WillWidget(QWidget):
def __init__(self,father=None,parent = None):
super().__init__()
vlayout = QVBoxLayout()
self.setLayout(vlayout)
self.will = parent.bal_window.willitems
self.parent = parent
for w in self.will:
if self.will[w].get_status('REPLACED') and self.parent.bal_window.bal_plugin._hide_replaced:
continue
if self.will[w].get_status('INVALIDATED') and self.parent.bal_window.bal_plugin._hide_invalidated:
continue
f = self.will[w].father
if father == f:
qwidget = QWidget()
childWidget = QWidget()
hlayout=QHBoxLayout(qwidget)
qwidget.setLayout(hlayout)
vlayout.addWidget(qwidget)
detailw=QWidget()
detaillayout=QVBoxLayout()
detailw.setLayout(detaillayout)
willpushbutton = QPushButton(w)
willpushbutton.clicked.connect(partial(self.parent.bal_window.show_transaction,txid=w))
detaillayout.addWidget(willpushbutton)
locktime = Util.locktime_to_str(self.will[w].tx.locktime)
creation = Util.locktime_to_str(self.will[w].time)
def qlabel(title,value):
label = "<b>"+_(str(title)) + f":</b>\t{str(value)}"
return QLabel(label)
detaillayout.addWidget(qlabel("Locktime",locktime))
detaillayout.addWidget(qlabel("Creation Time",creation))
total_fees = self.will[w].tx.input_value() - self.will[w].tx.output_value()
decoded_fees = total_fees #Util.decode_amount(total_fees,self.parent.decimal_point)
fee_per_byte = round(total_fees/self.will[w].tx.estimated_size(),3)
fees_str = str(decoded_fees) + " ("+ str(fee_per_byte) + " sats/vbyte)"
detaillayout.addWidget(qlabel("Transaction fees:",fees_str))
detaillayout.addWidget(qlabel("Status:",self.will[w].status))
detaillayout.addWidget(QLabel(""))
detaillayout.addWidget(QLabel("<b>Heirs:</b>"))
for heir in self.will[w].heirs:
if "w!ll3x3c\"" not in heir:
decoded_amount = Util.decode_amount(self.will[w].heirs[heir][3],self.parent.decimal_point)
detaillayout.addWidget(qlabel(heir,f"{decoded_amount} {self.parent.base_unit_name}"))
if self.will[w].we:
detaillayout.addWidget(QLabel(""))
detaillayout.addWidget(QLabel(_("<b>Willexecutor:</b:")))
decoded_amount = Util.decode_amount(self.will[w].we['base_fee'],self.parent.decimal_point)
detaillayout.addWidget(qlabel(self.will[w].we['url'],f"{decoded_amount} {self.parent.base_unit_name}"))
detaillayout.addStretch()
pal = QPalette()
pal.setColor(QPalette.ColorRole.Window, QColor(self.will[w].get_color()))
detailw.setAutoFillBackground(True)
detailw.setPalette(pal)
hlayout.addWidget(detailw)
hlayout.addWidget(WillWidget(w,parent = parent))

View File

@@ -1,291 +0,0 @@
import enum
import json
import urllib.request
import urllib.parse
from . import qt_resources
if qt_resources.QT_VERSION == 5:
from PyQt5.QtGui import QStandardItemModel, QStandardItem
from PyQt5.QtCore import Qt,QPersistentModelIndex, QModelIndex
from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QLabel,QMenu)
else:
from PyQt6.QtGui import QStandardItemModel, QStandardItem
from PyQt6.QtCore import Qt,QPersistentModelIndex, QModelIndex
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QLabel,QMenu)
from electrum.i18n import _
from electrum.gui.qt.util import (Buttons,read_QIcon, import_meta_gui, export_meta_gui,MessageBoxMixin)
from electrum.util import write_json_file,read_json_file
from electrum.gui.qt.my_treeview import MyTreeView
from ..bal import BalPlugin
from .. import util as Util
from .. import willexecutors as Willexecutors
from .baldialog import BalDialog,BalBlockingWaitingDialog
from electrum.logging import get_logger,Logger
_logger=get_logger(__name__)
class WillExecutorList(MyTreeView):
class Columns(MyTreeView.BaseColumnsEnum):
SELECTED = enum.auto()
URL = enum.auto()
BASE_FEE = enum.auto()
INFO = enum.auto()
ADDRESS = enum.auto()
STATUS = enum.auto()
headers = {
Columns.SELECTED:_(''),
Columns.URL: _('Url'),
Columns.BASE_FEE: _('Base fee'),
Columns.INFO:_('Info'),
Columns.ADDRESS:_('Default Address'),
Columns.STATUS: _('S'),
}
ROLE_HEIR_KEY = Qt.ItemDataRole.UserRole + 2000
key_role = ROLE_HEIR_KEY
def __init__(self, parent: 'WillExecutorDialog'):
super().__init__(
parent=parent,
stretch_column=self.Columns.ADDRESS,
editable_columns=[self.Columns.URL,self.Columns.BASE_FEE,self.Columns.ADDRESS,self.Columns.INFO],
)
self.parent = parent
self.setModel(QStandardItemModel(self))
self.setSortingEnabled(True)
self.std_model = self.model()
self.config =parent.bal_plugin.config
self.update()
def create_menu(self, position):
menu = QMenu()
idx = self.indexAt(position)
column = idx.column() or self.Columns.URL
selected_keys = []
for s_idx in self.selected_in_column(self.Columns.URL):
sel_key = self.model().itemFromIndex(s_idx).data(0)
selected_keys.append(sel_key)
if selected_keys and idx.isValid():
column_title = self.model().horizontalHeaderItem(column).text()
column_data = '\n'.join(self.model().itemFromIndex(s_idx).text()
for s_idx in self.selected_in_column(column))
if Willexecutors.is_selected(self.parent.willexecutors_list[sel_key]):
menu.addAction(_("deselect").format(column_title), lambda: self.deselect(selected_keys))
else:
menu.addAction(_("select").format(column_title), lambda: self.select(selected_keys))
if column in self.editable_columns:
item = self.model().itemFromIndex(idx)
if item.isEditable():
persistent = QPersistentModelIndex(idx)
menu.addAction(_("Edit {}").format(column_title), lambda p=persistent: self.edit(QModelIndex(p)))
menu.addAction(_("Ping").format(column_title), lambda: self.ping_willexecutors(selected_keys))
menu.addSeparator()
menu.addAction(_("delete").format(column_title), lambda: self.delete(selected_keys))
menu.exec(self.viewport().mapToGlobal(position))
def ping_willexecutors(self,selected_keys):
wout={}
for k in selected_keys:
wout[k]=self.parent.willexecutors_list[k]
self.parent.update_willexecutors(wout)
self.update()
def get_edit_key_from_coordinate(self, row, col):
a= self.get_role_data_from_coordinate(row, col, role=self.ROLE_HEIR_KEY+col)
return a
def delete(self,selected_keys):
for key in selected_keys:
del self.parent.willexecutors_list[key]
self.update()
def select(self,selected_keys):
for wid,w in self.parent.willexecutors_list.items():
if wid in selected_keys:
w['selected']=True
self.update()
def deselect(self,selected_keys):
for wid,w in self.parent.willexecutors_list.items():
if wid in selected_keys:
w['selected']=False
self.update()
def on_edited(self, idx, edit_key, *, text):
prior_name = self.parent.willexecutors_list[edit_key]
col = idx.column()
try:
if col == self.Columns.URL:
self.parent.willexecutors_list[text]=self.parent.willexecutors_list[edit_key]
del self.parent.willexecutors_list[edit_key]
if col == self.Columns.BASE_FEE:
self.parent.willexecutors_list[edit_key]["base_fee"] = Util.encode_amount(text,self.config.get_decimal_point())
if col == self.Columns.ADDRESS:
self.parent.willexecutors_list[edit_key]["address"] = text
if col == self.Columns.INFO:
self.parent.willexecutors_list[edit_key]["info"] = text
self.update()
except Exception as e:
pass
def update(self):
if self.parent.willexecutors_list is None:
return
try:
current_key = self.get_role_data_for_current_item(col=self.Columns.URL, role=self.ROLE_HEIR_KEY)
self.model().clear()
self.update_headers(self.__class__.headers)
set_current = None
for url, value in self.parent.willexecutors_list.items():
labels = [""] * len(self.Columns)
labels[self.Columns.URL] = url
if Willexecutors.is_selected(value):
labels[self.Columns.SELECTED] = [read_QIcon('confirmed.png'),'']
else:
labels[self.Columns.SELECTED] = ''
labels[self.Columns.BASE_FEE] = Util.decode_amount(value.get('base_fee',0),self.config.get_decimal_point())
if str(value.get('status',0)) == "200":
labels[self.Columns.STATUS] = [read_QIcon('status_connected.png'),'']
else:
labels[self.Columns.STATUS] = [read_QIcon('unconfirmed.png'),'']
labels[self.Columns.ADDRESS] = str(value.get('address',''))
labels[self.Columns.INFO] = str(value.get('info',''))
items=[]
for e in labels:
if type(e)== list:
try:
items.append(QStandardItem(*e))
except Exception as e:
pass
else:
items.append(QStandardItem(e))
items[self.Columns.SELECTED].setEditable(False)
items[self.Columns.URL].setEditable(True)
items[self.Columns.ADDRESS].setEditable(True)
items[self.Columns.INFO].setEditable(True)
items[self.Columns.BASE_FEE].setEditable(True)
items[self.Columns.STATUS].setEditable(False)
items[self.Columns.URL].setData(url, self.ROLE_HEIR_KEY+1)
items[self.Columns.BASE_FEE].setData(url, self.ROLE_HEIR_KEY+2)
items[self.Columns.INFO].setData(url, self.ROLE_HEIR_KEY+3)
items[self.Columns.ADDRESS].setData(url, self.ROLE_HEIR_KEY+4)
self.model().insertRow(self.model().rowCount(), items)
if url == current_key:
idx = self.model().index(row_count, self.Columns.NAME)
set_current = QPersistentModelIndex(idx)
self.set_current_idx(set_current)
self.parent.save_willexecutors()
except Exception as e:
_logger.error(e)
class WillExecutorDialog(BalDialog,MessageBoxMixin):
def __init__(self, bal_window):
BalDialog.__init__(self,bal_window.window)
self.bal_plugin = bal_window.bal_plugin
self.config = self.bal_plugin.config
self.window = bal_window.window
self.bal_window = bal_window
self.willexecutors_list = Willexecutors.get_willexecutors(self.bal_plugin)
self.setWindowTitle(_('Will-Executor Service List'))
self.setMinimumSize(1000, 200)
self.size_label = QLabel()
self.willexecutor_list = WillExecutorList(self)
vbox = QVBoxLayout(self)
vbox.addWidget(self.size_label)
vbox.addWidget(self.willexecutor_list)
buttonbox = QHBoxLayout()
b = QPushButton(_('Ping'))
b.clicked.connect(self.update_willexecutors)
buttonbox.addWidget(b)
b = QPushButton(_('Import'))
b.clicked.connect(self.import_file)
buttonbox.addWidget(b)
b = QPushButton(_('Export'))
b.clicked.connect(self.export_file)
buttonbox.addWidget(b)
b = QPushButton(_('Add'))
b.clicked.connect(self.add)
buttonbox.addWidget(b)
b = QPushButton(_('Close'))
b.clicked.connect(self.close)
buttonbox.addWidget(b)
vbox.addLayout(buttonbox)
self.willexecutor_list.update()
def add(self):
self.willexecutors_list["http://localhost:8080"]={"info":"New Will Executor","base_fee":0,"status":"-1"}
self.willexecutor_list.update()
def import_file(self):
import_meta_gui(self, _('willexecutors.json'), self.import_json_file, self.willexecutors_list.update)
def export_file(self, path):
Util.export_meta_gui(self, _('willexecutors.json'), self.export_json_file)
def export_json_file(self,path):
write_json_file(path, self.willexecutors_list)
def update_willexecutors(self,wes=None):
if not wes:
self.willexecutors_list = Willexecutors.get_willexecutors(self.bal_plugin, update = True, bal_window = self.bal_window,force=True)
else:
self.bal_window.ping_willexecutors(wes)
self.willexecutors_list.update(wes)
self.willexecutor_list.update()
def import_json_file(self, path):
data = read_json_file(path)
data = self._validate(data)
self.willexecutors_list.update(data)
self.willexecutor_list.update()
#TODO validate willexecutor json import file
def _validate(self,data):
return data
def is_hidden(self):
return self.isMinimized() or self.isHidden()
def show_or_hide(self):
if self.is_hidden():
self.bring_to_top()
else:
self.hide()
def bring_to_top(self):
self.show()
self.raise_()
def closeEvent(self, event):
event.accept()
def save_willexecutors(self):
self.bal_plugin.config.set_key(self.bal_plugin.WILLEXECUTORS,self.willexecutors_list,save=True)

477
heirs.py
View File

@@ -1,25 +1,56 @@
# import datetime
# import json
import math
import random
import re import re
import json import threading
from typing import Optional, Tuple, Dict, Any, TYPE_CHECKING,Sequence,List
# import urllib.parse
# import urllib.request
from typing import (
TYPE_CHECKING,
Any,
Dict,
# List,
Optional,
# Sequence,
Tuple,
)
import dns import dns
import threading
import math
from dns.exception import DNSException from dns.exception import DNSException
from electrum import (
from electrum import bitcoin,dnssec,descriptor,constants bitcoin,
from electrum.util import read_json_file, write_json_file, to_string,bfh,trigger_callback constants,
# descriptor,
dnssec,
)
from electrum.logging import Logger, get_logger from electrum.logging import Logger, get_logger
from electrum.transaction import PartialTxInput, PartialTxOutput,TxOutpoint,PartialTransaction,TxOutput from electrum.transaction import (
import datetime PartialTransaction,
import urllib.request PartialTxInput,
import urllib.parse PartialTxOutput,
TxOutput,
TxOutpoint,
# TxOutput,
)
from electrum.payment_identifier import PaymentIdentifier
from electrum.util import (
bfh,
read_json_file,
to_string,
trigger_callback,
write_json_file,
)
from .util import Util from .util import Util
from .willexecutors import Willexecutors from .willexecutors import Willexecutors
from electrum.util import BitcoinException
if TYPE_CHECKING: if TYPE_CHECKING:
from .wallet_db import WalletDB
from .simple_config import SimpleConfig from .simple_config import SimpleConfig
# from .wallet_db import WalletDB
_logger = get_logger(__name__) _logger = get_logger(__name__)
@@ -27,7 +58,10 @@ 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"
class AliasNotFoundException(Exception): class AliasNotFoundException(Exception):
pass pass
@@ -37,62 +71,70 @@ def reduce_outputs(in_amount, out_amount, fee, outputs):
for output in outputs: for output in outputs:
output.value = math.floor((in_amount - fee) / out_amount * output.value) output.value = math.floor((in_amount - fee) / out_amount * output.value)
"""
#TODO: put this method inside wallet.db to replace or complete get_locktime_for_new_transaction
def get_current_height(network:'Network'):
#if no network or not up to date, just set locktime to zero
if not network:
return 0
chain = network.blockchain()
if chain.is_tip_stale():
return 0
# figure out current block height
chain_height = chain.height() # learnt from all connected servers, SPV-checked
server_height = network.get_server_height() # height claimed by main server, unverified
# note: main server might be lagging (either is slow, is malicious, or there is an SPV-invisible-hard-fork)
# - if it's lagging too much, it is the network's job to switch away
if server_height < chain_height - 10:
# the diff is suspiciously large... give up and use something non-fingerprintable
return 0
# discourage "fee sniping"
height = min(chain_height, server_height)
return height
"""
def create_op_return_script(data_hex: str) -> bytes:
"""Crea scriptpubkey OP_RETURN in bytes"""
data = bytes.fromhex(data_hex)
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, key=lambda x:"{}:{}:{}".format(x.value_sats(),x.prevout.txid,x.prevout.out_idx)) available_utxos = sorted(
total_used_utxos = [] available_utxos,
key=lambda x: "{}:{}:{}".format(
x.value_sats(), x.prevout.txid, x.prevout.out_idx
),
)
# total_used_utxos = []
txsout = {} txsout = {}
locktime, _ = Util.get_lowest_locktimes(locktimes) locktime, _ = Util.get_lowest_locktimes(locktimes)
if not locktime: if not locktime:
_logger.info("prepare transactions, no locktime")
return return
locktime = locktime[0] locktime = locktime[0]
heirs = locktimes[locktime] heirs = locktimes[locktime]
vero=True true = True
while vero: while true:
vero=False true = False
fee = fees.get(locktime, 0) fee = fees.get(locktime, 0)
out_amount = fee out_amount = fee
description = "" description = ""
outputs = [] outputs = []
paid_heirs = {} paid_heirs = {}
for name, heir in heirs.items(): for name, heir in heirs.items():
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]
outputs.append(
PartialTxOutput.from_address_and_value(
heir[HEIR_ADDRESS], real_amount
)
)
out_amount += real_amount out_amount += real_amount
description += f"{name}\n" description += f"{name}\n"
paid_heirs[name]=heir except BitcoinException as e:
outputs.append(PartialTxOutput.from_address_and_value(heir[HEIR_ADDRESS], real_amount)) _logger.info("exception decoding output {} - {}".format(type(e), e))
else: heir[HEIR_REAL_AMOUNT] = e
pass
except Exception as 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 = []
@@ -101,21 +143,42 @@ def prepare_transactions(locktimes, available_utxos, fees, wallet):
value = utxo.value_sats() value = utxo.value_sats()
in_amount += value in_amount += value
used_utxos.append(utxo) used_utxos.append(utxo)
if in_amount > out_amount: if in_amount >= out_amount:
break break
except IndexError as e: except IndexError as e:
_logger.error(
f"error preparing transactions index error {e} {in_amount}, {out_amount}"
)
pass pass
if int(in_amount) < int(out_amount): if int(in_amount) < int(out_amount):
break _logger.error(
"error preparing transactions in_amount < out_amount ({} < {}) "
)
continue
heirsvalue = out_amount heirsvalue = out_amount
change = get_change_output(wallet, in_amount, out_amount, fee) change = get_change_output(wallet, in_amount, out_amount, fee)
if change: if change:
outputs.append(change) outputs.append(change)
for i in range(0, 100):
random.shuffle(outputs)
tx = PartialTransaction.from_io(used_utxos, outputs, locktime=Util.parse_locktime_string(locktime,wallet), version=2) #op_return_text = "Hello Bal!"
if len(description)>0: tx.description = description[:-1]
else: tx.description = "" ## Convert text to hex
#op_return_hex = op_return_text.encode('utf-8').hex()
#op_return_script = create_op_return_script(op_return_hex)
#outputs.append(PartialTxOutput(value=0, scriptpubkey=op_return_script))
tx = PartialTransaction.from_io(
used_utxos,
outputs,
locktime=Util.parse_locktime_string(locktime, wallet),
version=2,
)
if len(description) > 0:
tx.description = description[:-1]
else:
tx.description = ""
tx.heirsvalue = heirsvalue tx.heirsvalue = heirsvalue
tx.set_rbf(True) tx.set_rbf(True)
tx.remove_signatures() tx.remove_signatures()
@@ -146,13 +209,14 @@ def prepare_transactions(locktimes, available_utxos, fees, wallet):
def get_utxos_from_inputs(tx_inputs, tx, utxos): def get_utxos_from_inputs(tx_inputs, tx, utxos):
for tx_input in tx_inputs: for tx_input in tx_inputs:
prevoutstr = tx_input.prevout.to_str() prevoutstr = tx_input.prevout.to_str()
utxos[prevoutstr] =utxos.get(prevoutstr,{'input':tx_input,'txs':[]}) utxos[prevoutstr] = utxos.get(prevoutstr, {"input": tx_input, "txs": []})
utxos[prevoutstr]['txs'].append(tx) utxos[prevoutstr]["txs"].append(tx)
return utxos return utxos
# TODO calculate de minimum inputs to be invalidated # TODO calculate de minimum inputs to be invalidated
def invalidate_inheritance_transactions(wallet): def invalidate_inheritance_transactions(wallet):
listids = [] # listids = []
utxos = {} utxos = {}
dtxs = {} dtxs = {}
for k, v in wallet.get_all_labels().items(): for k, v in wallet.get_all_labels().items():
@@ -166,7 +230,7 @@ def invalidate_inheritance_transactions(wallet):
for key, utxo in utxos.items(): for key, utxo in utxos.items():
txid = key.split(":")[0] txid = key.split(":")[0]
if txid in dtxs: if txid in dtxs:
for tx in utxo['txs']: for tx in utxo["txs"]:
txid = tx.txid() txid = tx.txid()
del dtxs[txid] del dtxs[txid]
@@ -176,42 +240,49 @@ def invalidate_inheritance_transactions(wallet):
utxos = sorted(utxos.items(), key=lambda item: len(item[1])) utxos = sorted(utxos.items(), key=lambda item: len(item[1]))
remaining = {} remaining = {}
invalidated = [] invalidated = []
for key, value in utxos: for key, value in utxos:
for tx in value['txs']: for tx in value["txs"]:
txid = tx.txid() txid = tx.txid()
if not txid in invalidated: if txid not in invalidated:
invalidated.append(tx.txid()) invalidated.append(tx.txid())
remaining[key] = value remaining[key] = value
def print_transaction(heirs, tx, locktimes, tx_fees): def print_transaction(heirs, tx, locktimes, tx_fees):
jtx = tx.to_json() jtx = tx.to_json()
print(f"TX: {tx.txid()}\t-\tLocktime: {jtx['locktime']}") print(f"TX: {tx.txid()}\t-\tLocktime: {jtx['locktime']}")
print(f"---") print("---")
for inp in jtx["inputs"]: for inp in jtx["inputs"]:
print(f"{inp['address']}: {inp['value_sats']}") print(f"{inp['address']}: {inp['value_sats']}")
print(f"---") print("---")
for out in jtx["outputs"]: for out in jtx["outputs"]:
heirname = "" heirname = ""
for key in heirs.keys(): for key in heirs.keys():
heir = heirs[key] heir = heirs[key]
if heir[HEIR_ADDRESS] == out['address'] and str(heir[HEIR_LOCKTIME]) == str(jtx['locktime']): if heir[HEIR_ADDRESS] == out["address"] and str(heir[HEIR_LOCKTIME]) == str(
jtx["locktime"]
):
heirname = key heirname = key
print(f"{heirname}\t{out['address']}: {out['value_sats']}") print(f"{heirname}\t{out['address']}: {out['value_sats']}")
print() print()
size = tx.estimated_size() size = tx.estimated_size()
print("fee: {}\texpected: {}\tsize: {}".format(tx.input_value()-tx.output_value(), size*tx_fees, size)) print(
"fee: {}\texpected: {}\tsize: {}".format(
tx.input_value() - tx.output_value(), size * tx_fees, size
)
)
print() print()
try: try:
print(tx.serialize_to_network()) print(tx.serialize_to_network())
except: except Exception:
print("impossible to serialize") print("impossible to serialize")
print() print()
def get_change_output(wallet, in_amount, out_amount, fee): def get_change_output(wallet, in_amount, out_amount, fee):
change_amount = int(in_amount - out_amount - fee) change_amount = int(in_amount - out_amount - fee)
if change_amount > wallet.dust_threshold(): if change_amount > wallet.dust_threshold():
@@ -220,22 +291,24 @@ def get_change_output(wallet,in_amount,out_amount,fee):
out.is_change = True out.is_change = True
return out return out
class Heirs(dict, Logger): class Heirs(dict, Logger):
def __init__(self, db: 'WalletDB'): def __init__(self, wallet):
Logger.__init__(self) Logger.__init__(self)
self.db = db self.db = wallet.db
d = self.db.get('heirs', {}) self.wallet = wallet
d = self.db.get("heirs", {})
try: try:
self.update(d) self.update(d)
except e as Exception: except Exception:
return return
def invalidate_transactions(self, wallet): def invalidate_transactions(self, wallet):
invalidate_inheritance_transactions(wallet) invalidate_inheritance_transactions(wallet)
def save(self): def save(self):
self.db.put('heirs', dict(self)) self.db.put("heirs", dict(self))
def import_file(self, path): def import_file(self, path):
data = read_json_file(path) data = read_json_file(path)
@@ -260,24 +333,38 @@ class Heirs(dict, Logger):
locktimes = {} locktimes = {}
for key in self.keys(): for key in self.keys():
locktime = Util.parse_locktime_string(self[key][HEIR_LOCKTIME]) locktime = Util.parse_locktime_string(self[key][HEIR_LOCKTIME])
if locktime > from_locktime and not a \ if locktime > from_locktime and not a or locktime <= from_locktime and a:
or locktime <=from_locktime and a:
locktimes[int(locktime)] = None locktimes[int(locktime)] = None
return locktimes.keys() return list(locktimes.keys())
def check_locktime(self): def check_locktime(self):
return False return False
def normalize_perc(self, heir_list, total_balance, relative_balance,wallet,real=False): def normalize_perc(
self, heir_list, total_balance, relative_balance, wallet, real=False
):
amount = 0 amount = 0
for key, v in heir_list.items(): for key, v in heir_list.items():
try: try:
column = HEIR_AMOUNT column = HEIR_AMOUNT
if real: column = HEIR_REAL_AMOUNT if real:
value = int(math.floor(total_balance/relative_balance*self.amount_to_float(v[column]))) column = HEIR_REAL_AMOUNT
if "DUST" in str(v[column]):
column = HEIR_DUST_AMOUNT
value = int(
math.floor(
total_balance
/ relative_balance
* self.amount_to_float(v[column])
)
)
if value > wallet.dust_threshold(): if value > wallet.dust_threshold():
heir_list[key].insert(HEIR_REAL_AMOUNT, value) heir_list[key].insert(HEIR_REAL_AMOUNT, value)
amount += value amount += value
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
@@ -286,10 +373,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: except Exception:
try: try:
return float(amount[:-1]) return float(amount[:-1])
except: except Exception:
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):
@@ -297,87 +384,133 @@ 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= Util.parse_locktime_string(self[key][HEIR_LOCKTIME]) - from_locktime cmp = (
Util.parse_locktime_string(self[key][HEIR_LOCKTIME]) - from_locktime
)
if cmp <= 0: if cmp <= 0:
_logger.debug(
"cmp < 0 {} {} {} {}".format(
cmp, key, self[key][HEIR_LOCKTIME], from_locktime
)
)
continue continue
if Util.is_perc(self[key][HEIR_AMOUNT]): if Util.is_perc(self[key][HEIR_AMOUNT]):
percent_amount += float(self[key][HEIR_AMOUNT][:-1]) percent_amount += float(self[key][HEIR_AMOUNT][:-1])
percent_heirs[key] = list(self[key]) percent_heirs[key] = list(self[key])
else: else:
heir_amount = int(math.floor(float(self[key][HEIR_AMOUNT]))) heir_amount = int(math.floor(float(self[key][HEIR_AMOUNT])))
fixed_amount_with_dust += heir_amount
fixed_heirs[key] = list(self[key])
if heir_amount > dust_threshold: if heir_amount > dust_threshold:
fixed_amount += heir_amount fixed_amount += heir_amount
fixed_heirs[key] = list(self[key])
fixed_heirs[key].insert(HEIR_REAL_AMOUNT, heir_amount) fixed_heirs[key].insert(HEIR_REAL_AMOUNT, heir_amount)
else: else:
pass fixed_heirs[key] = list(self[key])
fixed_heirs[key].insert(
HEIR_REAL_AMOUNT, f"DUST: {heir_amount}"
)
fixed_heirs[key].insert(HEIR_DUST_AMOUNT, heir_amount)
except Exception as e: except Exception as e:
_logger.error(e) _logger.error(e)
return fixed_heirs,fixed_amount,percent_heirs,percent_amount return (
fixed_heirs,
fixed_amount,
percent_heirs,
percent_amount,
fixed_amount_with_dust,
)
def prepare_lists(
def prepare_lists(self, balance, total_fees, wallet, willexecutor = False, from_locktime = 0): self, balance, total_fees, wallet, willexecutor=False, from_locktime=0
):
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 = {}
onlyfixed = False onlyfixed = False
newbalance = balance - total_fees newbalance = balance - total_fees
locktimes = self.get_locktimes(from_locktime); locktimes = self.get_locktimes(from_locktime)
if willexecutor: if willexecutor:
for locktime in locktimes: for locktime in locktimes:
if int(Util.int_locktime(locktime)) > int(from_locktime): if int(Util.int_locktime(locktime)) > int(from_locktime):
try: try:
base_fee = int(willexecutor['base_fee']) base_fee = int(willexecutor["base_fee"])
willexecutors_amount += base_fee willexecutors_amount += base_fee
h = [None] * 4 h = [None] * 4
h[HEIR_AMOUNT] = base_fee h[HEIR_AMOUNT] = base_fee
h[HEIR_REAL_AMOUNT] = base_fee h[HEIR_REAL_AMOUNT] = base_fee
h[HEIR_LOCKTIME] = locktime h[HEIR_LOCKTIME] = locktime
h[HEIR_ADDRESS] = willexecutor['address'] h[HEIR_ADDRESS] = willexecutor["address"]
willexecutors["w!ll3x3c\""+willexecutor['url']+"\""+str(locktime)] = h willexecutors[
except Exception as e: 'w!ll3x3c"' + willexecutor["url"] + '"' + str(locktime)
] = h
except Exception:
return [], False return [], False
else: else:
_logger.error(f"heir excluded from will locktime({locktime}){Util.int_locktime(locktime)}<minimum{from_locktime}"), _logger.error(
f"heir excluded from will locktime({locktime}){Util.int_locktime(locktime)}<minimum{from_locktime}"
),
heir_list.update(willexecutors) heir_list.update(willexecutors)
newbalance -= willexecutors_amount newbalance -= willexecutors_amount
fixed_heirs,fixed_amount,percent_heirs,percent_amount = self.fixed_percent_lists_amount(from_locktime,wallet.dust_threshold()) if newbalance < 0:
raise WillExecutorFeeException(willexecutor)
(
fixed_heirs,
fixed_amount,
percent_heirs,
percent_amount,
fixed_amount_with_dust,
) = self.fixed_percent_lists_amount(from_locktime, wallet.dust_threshold())
if fixed_amount > newbalance: if fixed_amount > newbalance:
fixed_amount = self.normalize_perc(fixed_heirs,newbalance,fixed_amount,wallet) fixed_amount = self.normalize_perc(
fixed_heirs, newbalance, fixed_amount, wallet
)
onlyfixed = True onlyfixed = True
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(percent_heirs,newbalance,percent_amount,wallet) perc_amount = self.normalize_perc(
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_heirs,newbalance,fixed_amount,wallet,real=True) fixed_amount = self.normalize_perc(
fixed_heirs, newbalance, fixed_amount_with_dust, wallet, real=True
)
newbalance -= fixed_amount newbalance -= fixed_amount
heir_list.update(fixed_heirs) heir_list.update(fixed_heirs)
heir_list = sorted(heir_list.items(), key = lambda item: Util.parse_locktime_string(item[1][HEIR_LOCKTIME],wallet)) heir_list = sorted(
heir_list.items(),
key=lambda item: Util.parse_locktime_string(item[1][HEIR_LOCKTIME], wallet),
)
locktimes = {} locktimes = {}
for key, value in heir_list: for key, value in heir_list:
locktime = Util.parse_locktime_string(value[HEIR_LOCKTIME]) locktime = Util.parse_locktime_string(value[HEIR_LOCKTIME])
if not locktime in locktimes: locktimes[locktime]={key:value} if locktime not in locktimes:
else: locktimes[locktime][key]=value locktimes[locktime] = {key: value}
else:
locktimes[locktime][key] = value
return locktimes, onlyfixed return locktimes, onlyfixed
def is_perc(self, key): def is_perc(self, key):
return Util.is_perc(self[key][HEIR_AMOUNT]) return Util.is_perc(self[key][HEIR_AMOUNT])
def buildTransactions(self,bal_plugin,wallet,tx_fees = None, utxos=None,from_locktime=0): def buildTransactions(
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
@@ -385,14 +518,16 @@ class Heirs(dict, Logger):
if not utxos: if not utxos:
utxos = wallet.get_utxos() utxos = wallet.get_utxos()
willexecutors = Willexecutors.get_willexecutors(bal_plugin) or {} willexecutors = Willexecutors.get_willexecutors(bal_plugin) or {}
self.decimal_point=bal_plugin.config.get_decimal_point() self.decimal_point = bal_plugin.get_decimal_point()
no_willexecutors = bal_plugin.NO_WILLEXECUTOR.get() no_willexecutors = bal_plugin.NO_WILLEXECUTOR.get()
for utxo in utxos: for utxo in utxos:
if utxo.value_sats() > 0 * tx_fees: if utxo.value_sats() > 0 * tx_fees:
balance += utxo.value_sats() balance += utxo.value_sats()
len_utxo_set += 1 len_utxo_set += 1
available_utxos.append(utxo) available_utxos.append(utxo)
if len_utxo_set==0: return if len_utxo_set == 0:
_logger.info("no usable utxos")
return
j = -2 j = -2
willexecutorsitems = list(willexecutors.items()) willexecutorsitems = list(willexecutors.items())
willexecutorslen = len(willexecutorsitems) willexecutorslen = len(willexecutorsitems)
@@ -403,10 +538,10 @@ 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): if not Willexecutors.is_selected(willexecutor) or willexecutor["base_fee"] < wallet.dust_threshold():
continue continue
else: else:
willexecutor['url']=url willexecutor["url"] = url
elif j == -1: elif j == -1:
if not no_willexecutors: if not no_willexecutors:
continue continue
@@ -415,25 +550,39 @@ class Heirs(dict, Logger):
break break
fees = {} fees = {}
i = 0 i = 0
while True: while i < 10:
txs = {} txs = {}
redo = False redo = False
i += 1 i += 1
total_fees = 0 total_fees = 0
for fee in fees: for fee in fees:
total_fees += int(fees[fee]) total_fees += int(fees[fee])
newbalance = balance # newbalance = balance
locktimes, onlyfixed = self.prepare_lists(balance, total_fees, wallet, willexecutor, from_locktime)
try: try:
txs = prepare_transactions(locktimes, available_utxos[:], fees, wallet) locktimes, onlyfixed = self.prepare_lists(
balance, total_fees, wallet, willexecutor, from_locktime
)
except WillExecutorFeeException:
i = 10
continue
if locktimes:
try:
txs = prepare_transactions(
locktimes, available_utxos[:], fees, wallet
)
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[w],False) Willexecutors.is_selected(
e.heirname[len("w!ll3x3c") :], False
)
break break
except: except Exception:
raise e raise e
total_fees = 0 total_fees = 0
total_fees_real = 0 total_fees_real = 0
@@ -448,44 +597,51 @@ 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():
redo = True redo = True
if not redo: if not redo:
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
def get_transactions(self,bal_plugin,wallet,tx_fees,utxos=None,from_locktime=0):
def get_transactions(
self, bal_plugin, wallet, tx_fees, utxos=None, from_locktime=0
):
txs = self.buildTransactions(bal_plugin, wallet, tx_fees, utxos, from_locktime) txs = self.buildTransactions(bal_plugin, wallet, tx_fees, utxos, from_locktime)
if txs: if txs:
temp_txs = {} temp_txs = {}
for txid in txs: for txid in txs:
if txs[txid].available_utxos: if txs[txid].available_utxos:
temp_txs.update(self.get_transactions(bal_plugin,wallet,tx_fees,txs[txid].available_utxos,txs[txid].locktime)) temp_txs.update(
self.get_transactions(
bal_plugin,
wallet,
tx_fees,
txs[txid].available_utxos,
txs[txid].locktime,
)
)
txs.update(temp_txs) txs.update(temp_txs)
return txs return txs
def resolve(self, k): def resolve(self, k):
if bitcoin.is_address(k): if bitcoin.is_address(k):
return { return {"address": k, "type": "address"}
'address': k,
'type': 'address'
}
if k in self.keys(): if k in self.keys():
_type, addr = self[k] _type, addr = self[k]
if _type == 'address': if _type == "address":
return { return {"address": addr, "type": "heir"}
'address': addr,
'type': 'heir'
}
if openalias := self.resolve_openalias(k): if openalias := self.resolve_openalias(k):
return openalias return openalias
raise AliasNotFoundException("Invalid Bitcoin address or alias", k) raise AliasNotFoundException("Invalid Bitcoin address or alias", k)
@@ -496,10 +652,10 @@ class Heirs(dict, Logger):
if out: if out:
address, name, validated = out address, name, validated = out
return { return {
'address': address, "address": address,
'name': name, "name": name,
'type': 'openalias', "type": "openalias",
'validated': validated "validated": validated,
} }
return {} return {}
@@ -507,21 +663,19 @@ class Heirs(dict, Logger):
for k in self.keys(): for k in self.keys():
_type, addr = self[k] _type, addr = self[k]
if addr.casefold() == name.casefold(): if addr.casefold() == name.casefold():
return { return {"name": addr, "type": _type, "address": k}
'name': addr,
'type': _type,
'address': k
}
return None return None
def fetch_openalias(self, config: 'SimpleConfig'): def fetch_openalias(self, config: "SimpleConfig"):
self.alias_info = None self.alias_info = None
alias = config.OPENALIAS_ID alias = config.OPENALIAS_ID
if alias: if alias:
alias = str(alias) alias = str(alias)
def f(): def f():
self.alias_info = self._resolve_openalias(alias) self.alias_info = self._resolve_openalias(alias)
trigger_callback('alias_received') trigger_callback("alias_received")
t = threading.Thread(target=f) t = threading.Thread(target=f)
t.daemon = True t.daemon = True
t.start() t.start()
@@ -529,18 +683,18 @@ class Heirs(dict, Logger):
@classmethod @classmethod
def _resolve_openalias(cls, url: str) -> Optional[Tuple[str, str, bool]]: def _resolve_openalias(cls, url: str) -> Optional[Tuple[str, str, bool]]:
# support email-style addresses, per the OA standard # support email-style addresses, per the OA standard
url = url.replace('@', '.') url = url.replace("@", ".")
try: try:
records, validated = dnssec.query(url, dns.rdatatype.TXT) records, validated = dnssec.query(url, dns.rdatatype.TXT)
except DNSException as e: except DNSException as e:
_logger.info(f'Error resolving openalias: {repr(e)}') _logger.info(f"Error resolving openalias: {repr(e)}")
return None return None
prefix = 'btc' prefix = "btc"
for record in records: for record in records:
string = to_string(record.strings[0], 'utf8') string = to_string(record.strings[0], "utf8")
if string.startswith('oa1:' + prefix): if string.startswith("oa1:" + prefix):
address = cls.find_regex(string, r'recipient_address=([A-Za-z0-9]+)') address = cls.find_regex(string, r"recipient_address=([A-Za-z0-9]+)")
name = cls.find_regex(string, r'recipient_name=([^;]+)') name = cls.find_regex(string, r"recipient_name=([^;]+)")
if not name: if not name:
name = address name = address
if not address: if not address:
@@ -555,9 +709,8 @@ class Heirs(dict, Logger):
except AttributeError: except AttributeError:
return None return None
def validate_address(address): def validate_address(address):
if not bitcoin.is_address(address): if not bitcoin.is_address(address, net=constants.net):
raise NotAnAddress(f"not an address,{address}") raise NotAnAddress(f"not an address,{address}")
return address return address
@@ -586,20 +739,54 @@ class Heirs(dict, Logger):
return (address, amount, locktime) return (address, amount, locktime)
def _validate(data, timestamp_to_check=False): def _validate(data, timestamp_to_check=False):
for k, v in list(data.items()): for k, v in list(data.items()):
if k == 'heirs': if k == "heirs":
return Heirs._validate(v) return Heirs._validate(v, timestamp_to_check)
try: try:
Heirs.validate_heir(k,v) Heirs.validate_heir(k, v, timestamp_to_check)
except Exception as e: except Exception as e:
_logger.info(f"exception heir removed {e}")
data.pop(k) data.pop(k)
return data return data
class NotAnAddress(ValueError): class NotAnAddress(ValueError):
pass pass
class AmountNotValid(ValueError): class AmountNotValid(ValueError):
pass pass
class LocktimeNotValid(ValueError): class LocktimeNotValid(ValueError):
pass pass
class HeirExpiredException(LocktimeNotValid): class HeirExpiredException(LocktimeNotValid):
pass pass
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}"

BIN
icons/confirmed.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

BIN
icons/status_connected.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

BIN
icons/unconfirmed.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

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

2280
qt.py

File diff suppressed because it is too large Load Diff

768
util.py

File diff suppressed because it is too large Load Diff

77
wallet_util/bal_wallet_utils.py Executable file
View File

@@ -0,0 +1,77 @@
#!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")

View File

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

509
will.py
View File

@@ -1,22 +1,37 @@
# -*- 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 .willexecutors import Willexecutors
from .util import Util
from electrum.i18n import _
from electrum.transaction import TxOutpoint,PartialTxInput,tx_from_any,PartialTransaction,PartialTxOutput,Transaction
from electrum.util import bfh, decimal_point_to_base_unit_name
from electrum.util import write_json_file,read_json_file,FileImportFailed
from electrum.logging import get_logger,Logger
from electrum.bitcoin import NLOCKTIME_BLOCKHEIGHT_MAX from electrum.bitcoin import NLOCKTIME_BLOCKHEIGHT_MAX
from electrum.i18n import _
from electrum.logging import Logger, get_logger
from electrum.transaction import (
PartialTransaction,
PartialTxInput,
PartialTxOutput,
Transaction,
TxOutpoint,
tx_from_any,
)
from electrum.util import (
bfh,
)
from .util import Util
from .willexecutors import Willexecutors
MIN_LOCKTIME = 1 MIN_LOCKTIME = 1
MIN_BLOCK = 1 MIN_BLOCK = 1
_logger = get_logger(__name__) _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:
@@ -35,23 +50,21 @@ class Will:
if not will[child[0]].father: if not will[child[0]].father:
will[child[0]].father = willid will[child[0]].father = willid
# return a list of will sorted by locktime # return a list of will sorted by locktime
def get_sorted_will(will): def get_sorted_will(will):
return sorted(will.items(),key = lambda x: x[1]['tx'].locktime) return sorted(will.items(), key=lambda x: x[1]["tx"].locktime)
def only_valid(will): def only_valid(will):
for k, v in will.items(): for k, v in will.items():
if v.get_status('VALID'): if v.get_status("VALID"):
yield k yield k
def search_equal_tx(will, tx, wid): def search_equal_tx(will, tx, wid):
for w in will: for w in will:
if w != wid and not tx.to_json() != will[w]['tx'].to_json(): if w != wid and not tx.to_json() != will[w]["tx"].to_json():
if will[w]['tx'].txid() != tx.txid(): if will[w]["tx"].txid() != tx.txid():
if Util.cmp_txs(will[w]['tx'],tx): if Util.cmp_txs(will[w]["tx"], tx):
return will[w]['tx'] return will[w]["tx"]
return False return False
def get_tx_from_any(x): def get_tx_from_any(x):
@@ -72,13 +85,11 @@ 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: except Exception:
pass pass
txin.is_mine = True txin.is_mine = True
txin._TxInput__address = change.address txin._TxInput__address = change.address
@@ -107,7 +118,7 @@ class Will:
_logger.error(will[wid].tx.to_json()) _logger.error(will[wid].tx.to_json())
_logger.error("txid is none") _logger.error("txid is none")
will[wid].set_status('ERROR',True) will[wid].set_status("ERROR", True)
errors[wid] = will[wid] errors[wid] = will[wid]
continue continue
@@ -118,13 +129,15 @@ class Will:
will[wid] = WillItem(ow.to_dict()) will[wid] = WillItem(ow.to_dict())
for i in range(0, len(outputs)): for i in range(0, len(outputs)):
Will.change_input(will,wid,i,outputs[i],others_inputs,to_delete,to_add) Will.change_input(
will, wid, i, outputs[i], others_inputs, to_delete, to_add
)
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
@@ -143,13 +156,25 @@ class Will:
inp._TxInput__value_sats = change.value inp._TxInput__value_sats = change.value
return inp return inp
def check_anticipate(ow:'WillItem',nw:'WillItem'): """
in questa situazione sono presenti due transazioni con id differente(quindi transazioni differenti)
per prima cosa controllo il locktime
se il locktime della nuova transazione e' maggiore del locktime della vecchia transazione, allora
confronto gli eredi, per locktime se corrispondono controllo i willexecutor
se hanno la stessa url ma le fee vecchie sono superiori alle fee nuove, allora anticipare.
"""
def check_anticipate(ow: "WillItem", nw: "WillItem"):
anticipate = Util.anticipate_locktime(ow.tx.locktime, days=1) anticipate = Util.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(ow.heirs,nw.heirs,[0,1],exclude_willexecutors = True): if Util.cmp_heirs_by_values(
ow.heirs, nw.heirs, [0, 1], exclude_willexecutors=True
):
if nw.we and ow.we: if nw.we and ow.we:
if ow.we['url'] == nw.we['url']: if ow.we["url"] == nw.we["url"]:
if int(ow.we['base_fee'])>int(nw.we['base_fee']): if int(ow.we["base_fee"]) > int(nw.we["base_fee"]):
return anticipate return anticipate
else: else:
if int(ow.tx_fees) != int(nw.tx_fees): if int(ow.tx_fees) != int(nw.tx_fees):
@@ -170,7 +195,6 @@ class Will:
return anticipate return anticipate
return 4294967295 + 1 return 4294967295 + 1
def change_input(will, otxid, idx, change, others_inputs, to_delete, to_append): def change_input(will, otxid, idx, change, others_inputs, to_delete, to_append):
ow = will[otxid] ow = will[otxid]
ntxid = ow.tx.txid() ntxid = ow.tx.txid()
@@ -181,15 +205,18 @@ 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 inputs[i].prevout.txid.hex() == otxid and inputs[i].prevout.out_idx == idx: if (
inputs[i].prevout.txid.hex() == otxid
and inputs[i].prevout.out_idx == idx
):
if isinstance(w.tx, Transaction): if isinstance(w.tx, Transaction):
will[wid].tx = PartialTransaction.from_tx(w.tx) will[wid].tx = PartialTransaction.from_tx(w.tx)
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 == True: if found:
pass pass
new_txid = will[wid].tx.txid() new_txid = will[wid].tx.txid()
@@ -198,17 +225,25 @@ class Will:
to_append[new_txid] = will[wid] to_append[new_txid] = will[wid]
outputs = will[wid].tx.outputs() outputs = will[wid].tx.outputs()
for i in range(0, len(outputs)): for i in range(0, len(outputs)):
Will.change_input(will, wid, i, outputs[i],others_inputs,to_delete,to_append) Will.change_input(
will,
wid,
i,
outputs[i],
others_inputs,
to_delete,
to_append,
)
def get_all_inputs(will, only_valid=False): def get_all_inputs(will, only_valid=False):
all_inputs = {} all_inputs = {}
for w, wi in will.items(): for w, wi in will.items():
if not only_valid or wi.get_status('VALID'): if not only_valid or wi.get_status("VALID"):
inputs = wi.tx.inputs() inputs = wi.tx.inputs()
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 not prevout_str in all_inputs: if prevout_str not 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)
@@ -221,56 +256,65 @@ 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 not i in all_inputs_min_locktime: if i not 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)
return all_inputs_min_locktime return all_inputs_min_locktime
def search_anticipate_rec(will, old_inputs): def search_anticipate_rec(will, old_inputs):
redo = False redo = False
to_delete = [] to_delete = []
to_append = {} to_append = {}
new_inputs = Will.get_all_inputs(will, only_valid=True) new_inputs = Will.get_all_inputs(will, only_valid=True)
for nid, nwi in will.items(): for nid, nwi in will.items():
if nwi.search_anticipate(new_inputs) or nwi.search_anticipate(old_inputs): if nwi.search_anticipate(new_inputs):
if nid != nwi.tx.txid(): if nid != nwi.tx.txid():
redo = True redo = True
to_delete.append(nid) to_delete.append(nid)
to_append[nwi.tx.txid()] = nwi to_append[nwi.tx.txid()] = nwi
outputs = nwi.tx.outputs() outputs = nwi.tx.outputs()
for i in range(0, len(outputs)): for i in range(0, len(outputs)):
Will.change_input(will,nid,i,outputs[i],new_inputs,to_delete,to_append) Will.change_input(
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: except Exception:
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)
except Exception as e: except Exception as e:
raise e raise e
for oid in Will.only_valid(old_will): for oid in Will.only_valid(old_will):
if oid in new_will: if oid in new_will:
new_heirs = new_will[oid].heirs new_heirs = new_will[oid].heirs
@@ -303,31 +347,39 @@ 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 not w[0] in filtered_inputs: if w[0] not in filtered_inputs:
filtered_inputs.append(w[0]) filtered_inputs.append(w[0])
if not prevout_str in prevout_to_spend: if prevout_str not 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 = Util.get_current_height(wallet.network) locktime = current_height
tx = PartialTransaction.from_io(utxo_to_spend, [out], locktime=locktime, version=2) tx = PartialTransaction.from_io(
utxo_to_spend, [out], locktime=locktime, version=2
)
tx.set_rbf(True) tx.set_rbf(True)
fee = tx.estimated_size() * fees_per_byte fee = tx.estimated_size() * fees_per_byte
if balance - fee > 0: if balance - fee > 0:
out = PartialTxOutput.from_address_and_value(change_addresses[0],balance - fee) out = PartialTxOutput.from_address_and_value(
tx = PartialTransaction.from_io(utxo_to_spend,[out], locktime=locktime, version=2) change_addresses[0], balance - fee
)
tx = PartialTransaction.from_io(
utxo_to_spend, [out], locktime=locktime, version=2
)
tx.set_rbf(True) tx.set_rbf(True)
_logger.debug(f"invalidation tx: {tx}") _logger.debug(f"invalidation tx: {tx}")
@@ -340,102 +392,112 @@ class Will:
_logger.debug("len utxo_to_spend <=0") _logger.debug("len utxo_to_spend <=0")
pass pass
def is_new(will): def is_new(will):
for wid, w in will.items(): for wid, w in will.items():
if w.get_status('VALID') and not w.get_status('COMPLETE'): if w.get_status("VALID") and not w.get_status("COMPLETE"):
return True return True
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 = Util.in_utxo(inp, all_utxos)
for w in ws: for w in ws:
wi = w[1] wi = w[1]
if wi.get_status('VALID') or wi.get_status('CONFIRMED') or wi.get_status('PENDING'): if (
wi.get_status("VALID")
or wi.get_status("CONFIRMED")
or wi.get_status("PENDING")
):
prevout_id = w[2].prevout.txid.hex() prevout_id = w[2].prevout.txid.hex()
if not inutxo: if not inutxo:
if prevout_id in will: if prevout_id in will:
wo = will[prevout_id] wo = will[prevout_id]
if wo.get_status('REPLACED'): if wo.get_status("REPLACED"):
wi.set_status('REPLACED',True) wi.set_status("REPLACED", True)
if wo.get_status("INVALIDATED"): if wo.get_status("INVALIDATED"):
wi.set_status('INVALIDATED',True) wi.set_status("INVALIDATED", True)
else: else:
if wallet.db.get_transaction(wi._id): if wallet.db.get_transaction(wi._id):
wi.set_status('CONFIRMED',True) wi.set_status("CONFIRMED", True)
else: else:
wi.set_status('INVALIDATED',True) wi.set_status("INVALIDATED", True)
for child in wi.search(all_inputs): for child in wi.search(all_inputs):
if child.tx.locktime < wi.tx.locktime: if child.tx.locktime < wi.tx.locktime:
_logger.debug("a child was found") _logger.debug("a child was found")
wi.set_status('REPLACED',True) wi.set_status("REPLACED", True)
else: else:
pass pass
def utxos_strs(utxos): def utxos_strs(utxos):
return [Util.utxo_to_str(u) for u in utxos] return [Util.utxo_to_str(u) for u in utxos]
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 self.children.items(): for c in will[wid].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 not w.father: if (
not w.father
or willtree[w.father].get_status("CONFIRMED")
or willtree[w.father].get_status("PENDING")
):
for inp in w.tx.inputs(): for inp in w.tx.inputs():
inp_str = Util.utxo_to_str(inp) inp_str = Util.utxo_to_str(inp)
if not inp_str in utxos_list: if inp_str not 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:
w.set_status("PENDING", True) w.set_status("PENDING", True)
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 = heirs.fixed_percent_lists_amount(timestamp_to_check,dust,reverse=True) fixed_heirs, fixed_amount, perc_heirs, perc_amount, fixed_amount_with_dust = (
heirs.fixed_percent_lists_amount(timestamp_to_check, dust, reverse=True)
)
wallet_balance = 0 wallet_balance = 0
for utxo in all_utxos: for utxo in all_utxos:
wallet_balance += utxo.value_sats() wallet_balance += utxo.value_sats()
if fixed_amount >= wallet_balance: if fixed_amount >= wallet_balance:
raise FixedAmountException(f"Fixed amount({fixed_amount}) >= {wallet_balance}") raise FixedAmountException(
f"Fixed amount({fixed_amount}) >= {wallet_balance}"
)
if perc_amount != 100: if perc_amount != 100:
raise PercAmountException(f"Perc amount({perc_amount}) =! 100%") raise PercAmountException(f"Perc amount({perc_amount}) =! 100%")
for url, wex in willexecutors.items(): for url, wex in willexecutors.items():
if Willexecutors.is_selected(wex): if Willexecutors.is_selected(wex):
temp_balance = wallet_balance - int(wex['base_fee']) temp_balance = wallet_balance - int(wex["base_fee"])
if fixed_amount >= temp_balance: if fixed_amount >= temp_balance:
raise FixedAmountException(f"Willexecutor{url} excess base fee({wex['base_fee']}), {fixed_amount} >={temp_balance}") raise FixedAmountException(
f"Willexecutor{url} excess base fee({wex['base_fee']}), {fixed_amount} >={temp_balance}"
)
def check_will(will, all_utxos, wallet, block_to_check, timestamp_to_check): def check_will(will, all_utxos, wallet, block_to_check, timestamp_to_check):
Will.add_willtree(will) Will.add_willtree(will)
@@ -444,68 +506,90 @@ 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
)
all_inputs = Will.get_all_inputs(will, only_valid=True) all_inputs = Will.get_all_inputs(will, only_valid=True)
Will.search_rai(all_inputs, all_utxos, will, wallet) Will.search_rai(all_inputs, all_utxos, will, wallet)
def is_will_valid(will, block_to_check, timestamp_to_check, tx_fees, all_utxos,heirs={},willexecutors={},self_willexecutor=False, wallet=False, callback_not_valid_tx=None): def is_will_valid(
will,
block_to_check,
timestamp_to_check,
tx_fees,
all_utxos,
heirs={},
willexecutors={},
self_willexecutor=False,
wallet=False,
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(will,heirs,willexecutors,self_willexecutor,timestamp_to_check,tx_fees): if not Will.check_willexecutors_and_heirs(
will,
heirs,
willexecutors,
self_willexecutor,
timestamp_to_check,
tx_fees,
):
raise NotCompleteWillException() raise NotCompleteWillException()
all_inputs = Will.get_all_inputs(will, only_valid=True) all_inputs = Will.get_all_inputs(will, only_valid=True)
_logger.info('check all utxo in wallet are spent') _logger.info("check all utxo in wallet are spent")
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 Util.in_utxo(utxo, all_inputs.keys()):
_logger.info("utxo is not spent", utxo.to_json()) _logger.info("utxo is not spent", utxo.to_json())
_logger.debug(all_inputs.keys()) _logger.debug(all_inputs.keys())
raise NotCompleteWillException("Some utxo in the wallet is not included") raise NotCompleteWillException(
"Some utxo in the wallet is not included"
)
_logger.info('will ok') _logger.info("will ok")
return True return True
def check_will_expired(all_inputs_min_locktime, block_to_check, timestamp_to_check): def check_will_expired(all_inputs_min_locktime, block_to_check, timestamp_to_check):
_logger.info("check if some transaction is expired") _logger.info("check if some transaction is expired")
for prevout_str, wid in all_inputs_min_locktime.items(): for prevout_str, wid in all_inputs_min_locktime.items():
for w in wid: for w in wid:
if w[1].get_status('VALID'): if w[1].get_status("VALID"):
locktime = int(wid[0][1].tx.locktime) locktime = int(wid[0][1].tx.locktime)
if locktime <= NLOCKTIME_BLOCKHEIGHT_MAX: if locktime <= NLOCKTIME_BLOCKHEIGHT_MAX:
if locktime < int(block_to_check): if locktime < int(block_to_check):
raise WillExpiredException(f"Will Expired {wid[0][0]}: {locktime}<{block_to_check}") raise WillExpiredException(
f"Will Expired {wid[0][0]}: {locktime}<{block_to_check}"
)
else: else:
if locktime < int(timestamp_to_check): if locktime < int(timestamp_to_check):
raise WillExpiredException(f"Will Expired {wid[0][0]}: {locktime}<{timestamp_to_check}") raise WillExpiredException(
f"Will Expired {wid[0][0]}: {locktime}<{timestamp_to_check}"
def check_all_input_spent_are_in_wallet(): )
_logger.info("check all input spent are in wallet or valid txs") else:
for inp,ws in all_inputs.items(): from datetime import datetime
if not Util.in_utxo(inp,all_utxos): _logger.debug(f"Will Not Expired {wid[0][0]}: {datetime.fromtimestamp(locktime).isoformat()} > {datetime.fromtimestamp(timestamp_to_check).isoformat()}")
for w in ws:
if w[1].get_status('VALID'):
prevout_id = w[2].prevout.txid.hex()
parentwill = will.get(prevout_id,False)
if not parentwill or not parentwill.get_status('VALID'):
w[1].set_status('INVALIDATED',True)
# def check_all_input_spent_are_in_wallet():
# _logger.info("check all input spent are in wallet or valid txs")
# for inp, ws in all_inputs.items():
# if not Util.in_utxo(inp, all_utxos):
# for w in ws:
# if w[1].get_status("VALID"):
# prevout_id = w[2].prevout.txid.hex()
# parentwill = will.get(prevout_id, False)
# if not parentwill or not parentwill.get_status("VALID"):
# w[1].set_status("INVALIDATED", True)
def only_valid_list(will): def only_valid_list(will):
out = {} out = {}
for wid, w in will.items(): for wid, w in will.items():
if w.get_status('VALID'): if w.get_status("VALID"):
out[wid] = w out[wid] = w
return out return out
@@ -513,11 +597,13 @@ class Will:
out = [] out = []
for wid, w in will.items(): for wid, w in will.items():
wi = w wi = w
if wi.get_status('VALID') or wi.get_status('REPLACED'): if wi.get_status("VALID") or wi.get_status("REPLACED"):
out.append(wid) out.append(wid)
return out return out
def check_willexecutors_and_heirs(will,heirs,willexecutors,self_willexecutor,check_date,tx_fees): def check_willexecutors_and_heirs(
will, heirs, willexecutors, self_willexecutor, check_date, tx_fees
):
_logger.debug("check willexecutors heirs") _logger.debug("check willexecutors heirs")
no_willexecutor = 0 no_willexecutor = 0
willexecutors_found = {} willexecutors_found = {}
@@ -534,34 +620,43 @@ class Will:
their = will[wid].heirs[wheir] their = will[wid].heirs[wheir]
if heir := heirs.get(wheir, None): if heir := heirs.get(wheir, None):
if heir[0] == their[0] and heir[1] == their[1] and Util.parse_locktime_string(heir[2]) >= Util.parse_locktime_string(their[2]): if (
heir[0] == their[0]
and heir[1] == their[1]
and Util.parse_locktime_string(heir[2])
>= Util.parse_locktime_string(their[2])
):
count = heirs_found.get(wheir, 0) count = heirs_found.get(wheir, 0)
heirs_found[wheir] = count + 1 heirs_found[wheir] = count + 1
else: else:
_logger.debug("heir not present transaction is not valid:",wid,w) _logger.debug(
continue f"heir not present transaction is not valid:{wheir} {wid}, {w}"
)
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(willexecutor,willexecutors.get(willexecutor['url'],None)): if Util.cmp_willexecutor(
willexecutors_found[willexecutor['url']]=count+1 willexecutor, willexecutors.get(willexecutor["url"], None)
):
willexecutors_found[willexecutor["url"]] = count + 1
else: else:
no_willexecutor += 1 no_willexecutor += 1
count_heirs = 0 count_heirs = 0
for h in heirs: for h in heirs:
if Util.parse_locktime_string(heirs[h][2]) >= check_date: if Util.parse_locktime_string(heirs[h][2]) >= check_date:
count_heirs += 1 count_heirs += 1
if not h in heirs_found: if h not 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 not url in willexecutors_found: if url not 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")
@@ -569,48 +664,51 @@ class Will:
class WillItem(Logger): class WillItem(Logger):
STATUS_DEFAULT = { STATUS_DEFAULT = {
'ANTICIPATED': ['Anticipated', False], "ANTICIPATED": ["Anticipated", False],
'BROADCASTED': ['Broadcasted', False], "BROADCASTED": ["Broadcasted", False],
'CHECKED': ['Checked', False], "CHECKED": ["Checked", False],
'CHECK_FAIL': ['Check Failed',False], "CHECK_FAIL": ["Check Failed", False],
'COMPLETE': ['Signed', False], "COMPLETE": ["Signed", False],
'CONFIRMED': ['Confirmed', False], "CONFIRMED": ["Confirmed", False],
'ERROR': ['Error', False], "ERROR": ["Error", False],
'EXPIRED': ['Expired', False], "EXPIRED": ["Expired", False],
'EXPORTED': ['Exported', False], "EXPORTED": ["Exported", False],
'IMPORTED': ['Imported', False], "IMPORTED": ["Imported", False],
'INVALIDATED': ['Invalidated', False], "INVALIDATED": ["Invalidated", False],
'PENDING': ['Pending', False], "PENDING": ["Pending", False],
'PUSH_FAIL': ['Push failed', False], "PUSH_FAIL": ["Push failed", False],
'PUSHED': ['Pushed', False], "PUSHED": ["Pushed", False],
'REPLACED': ['Replaced', False], "REPLACED": ["Replaced", False],
'RESTORED': ['Restored', False], "RESTORED": ["Restored", False],
'VALID': ['Valid', True], "VALID": ["Valid", True],
} }
def set_status(self, status, value=True): def set_status(self, status, value=True):
_logger.debug("set status {} - {} {} -> {}".format(self._id,status,self.STATUS[status][1],value)) # _logger.trace(
# "set status {} - {} {} -> {}".format(
# 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"]:
self.STATUS['VALID'][1] = False self.STATUS["VALID"][1] = False
if status in ['CONFIRMED','PENDING']: if status in ["CONFIRMED", "PENDING"]:
self.STATUS['INVALIDATED'][1] = False self.STATUS["INVALIDATED"][1] = False
if status in ['PUSHED']: if status in ["PUSHED"]:
self.STATUS['PUSH_FAIL'][1] = False self.STATUS["PUSH_FAIL"][1] = False
self.STATUS['CHECK_FAIL'][1] = False self.STATUS["CHECK_FAIL"][1] = False
if status in ["CHECKED"]:
if status in ['CHECKED']: self.STATUS["PUSHED"][1] = True
self.STATUS['PUSHED'][1] = True self.STATUS["PUSH_FAIL"][1] = False
self.STATUS['PUSH_FAIL'][1] = False
return value return value
@@ -618,19 +716,22 @@ class WillItem(Logger):
return self.STATUS[status][1] return self.STATUS[status][1]
def __init__(self, w, _id=None, wallet=None): def __init__(self, w, _id=None, wallet=None):
if isinstance(w,WillItem,): if isinstance(
w,
WillItem,
):
self.__dict__ = w.__dict__.copy() self.__dict__ = w.__dict__.copy()
else: else:
self.tx = Will.get_tx_from_any(w['tx']) self.tx = Will.get_tx_from_any(w["tx"])
self.heirs = w.get('heirs',None) self.heirs = w.get("heirs", None)
self.we = w.get('willexecutor',None) self.we = w.get("willexecutor", None)
self.status = w.get('status',None) self.status = w.get("status", None)
self.description = w.get('description',None) self.description = w.get("description", None)
self.time = w.get('time',None) self.time = w.get("time", None)
self.change = w.get('change',None) self.change = w.get("change", None)
self.tx_fees = w.get('tx_fees',0) self.tx_fees = w.get("baltx_fees", 0)
self.father = w.get('Father',None) self.father = w.get("Father", None)
self.children = w.get('Children',None) self.children = w.get("Children", None)
self.STATUS = copy.deepcopy(WillItem.STATUS_DEFAULT) self.STATUS = copy.deepcopy(WillItem.STATUS_DEFAULT)
for s in self.STATUS: for s in self.STATUS:
self.STATUS[s][1] = w.get(s, WillItem.STATUS_DEFAULT[s][1]) self.STATUS[s][1] = w.get(s, WillItem.STATUS_DEFAULT[s][1])
@@ -646,19 +747,17 @@ class WillItem(Logger):
if wallet: if wallet:
self.tx.add_info_from_wallet(wallet) self.tx.add_info_from_wallet(wallet)
def to_dict(self): def to_dict(self):
out = { out = {
'_id':self._id, "_id": self._id,
'tx':self.tx, "tx": self.tx,
'heirs':self.heirs, "heirs": self.heirs,
'willexecutor':self.we, "willexecutor": self.we,
'status':self.status, "status": self.status,
'description':self.description, "description": self.description,
'time':self.time, "time": self.time,
'change':self.change, "change": self.change,
'tx_fees':self.tx_fees "baltx_fees": self.tx_fees,
} }
for key in self.STATUS: for key in self.STATUS:
try: try:
@@ -674,7 +773,7 @@ class WillItem(Logger):
def __str__(self): def __str__(self):
return str(self.to_dict()) return str(self.to_dict())
def set_anticipate(self, ow:'WillItem'): def set_anticipate(self, ow: "WillItem"):
nl = min(ow.tx.locktime, Will.check_anticipate(ow, self)) nl = min(ow.tx.locktime, Will.check_anticipate(ow, self))
if int(nl) < self.tx.locktime: if int(nl) < self.tx.locktime:
self.tx.locktime = int(nl) self.tx.locktime = int(nl)
@@ -682,7 +781,6 @@ class WillItem(Logger):
else: else:
return False return False
def search_anticipate(self, all_inputs): def search_anticipate(self, all_inputs):
anticipated = False anticipated = False
for ow in self.search(all_inputs): for ow in self.search(all_inputs):
@@ -709,23 +807,24 @@ class WillItem(Logger):
iw = inp[1] iw = inp[1]
self.set_anticipate(iw) self.set_anticipate(iw)
def check_willexecutor(self): def set_check_willexecutor(self,resp):
try: try:
if resp:=Willexecutors.check_transaction(self._id,self.we['url']): if resp :
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")
else: else:
self.set_status('CHECK_FAIL') self.set_status("CHECK_FAIL")
self.set_status('PUSHED',False) self.set_status("PUSHED", False)
return True return True
else: else:
self.set_status('CHECK_FAIL') self.set_status("CHECK_FAIL")
self.set_status('PUSHED',False) self.set_status("PUSHED", False)
return False return False
except Exception as e: except Exception as e:
_logger.error(f"exception checking transaction: {e}") _logger.error(f"exception checking transaction: {e}")
self.set_status('CHECK_FAIL') self.set_status("CHECK_FAIL")
def get_color(self): def get_color(self):
if self.get_status("INVALIDATED"): if self.get_status("INVALIDATED"):
return "#f87838" return "#f87838"
@@ -748,29 +847,57 @@ class WillItem(Logger):
else: else:
return "#ffffff" return "#ffffff"
class WillException(Exception): class WillException(Exception):
pass def __init__(self,msg="WillException"):
self.msg=msg
Exception.__init__(self)
def __str__(self):
return self.msg
class WillExpiredException(WillException): class WillExpiredException(WillException):
pass pass
class NotCompleteWillException(WillException): class NotCompleteWillException(WillException):
pass pass
class HeirChangeException(NotCompleteWillException): class HeirChangeException(NotCompleteWillException):
pass pass
class TxFeesChangedException(NotCompleteWillException): class TxFeesChangedException(NotCompleteWillException):
pass pass
class HeirNotFoundException(NotCompleteWillException): class HeirNotFoundException(NotCompleteWillException):
pass pass
class WillexecutorChangeException(NotCompleteWillException): class WillexecutorChangeException(NotCompleteWillException):
pass pass
class NoWillExecutorNotPresent(NotCompleteWillException): class NoWillExecutorNotPresent(NotCompleteWillException):
pass pass
class WillExecutorNotPresent(NotCompleteWillException): class WillExecutorNotPresent(NotCompleteWillException):
pass pass
class NoHeirsException(WillException): class NoHeirsException(WillException):
pass pass
class AmountException(WillException): class AmountException(WillException):
pass pass
class PercAmountException(AmountException): class PercAmountException(AmountException):
pass pass
class FixedAmountException(AmountException): class FixedAmountException(AmountException):
pass pass

View File

@@ -1,28 +1,70 @@
"""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
from functools import partial import time
from aiohttp import ClientResponse
from electrum.network import Network from aiohttp import ClientResponse
from electrum import constants from electrum import constants
from electrum.logging import get_logger
from electrum.gui.qt.util import WaitingDialog
from electrum.i18n import _ from electrum.i18n import _
from electrum.logging import get_logger
from electrum.network import Network
from .bal import BalPlugin from .bal import BalPlugin
from .util import Util
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):
aw=bal_plugin.WILLEXECUTORS.get() """Save will executor configuration to plugin settings.
aw[constants.net.NET_NAME]=willexecutors
bal_plugin.WILLEXECUTORS.set(aw)
def get_willexecutors(bal_plugin, update = False,bal_window=False,force=False,task=True): 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[chainname] = willexecutors
bal_plugin.WILLEXECUTORS.set(aw)
_logger.debug(f"saved: {aw}")
# bal_plugin.WILLEXECUTORS.set(willexecutors)
def get_willexecutors(
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(constants.net.NET_NAME,{}) willexecutors = willexecutors.get(chainname, {})
to_del = [] to_del = []
for w in willexecutors: for w in willexecutors:
if not isinstance(willexecutors[w], dict): if not isinstance(willexecutors[w], dict):
@@ -30,127 +72,240 @@ class Willexecutors:
continue continue
Willexecutors.initialize_willexecutor(willexecutors[w], w) Willexecutors.initialize_willexecutor(willexecutors[w], w)
for w in to_del: for w in to_del:
print("ERROR: WILLEXECUTOR TO DELETE:", w) _logger.error(
"error Willexecutor to delete type:{} {}".format(
type(willexecutors[w]), w
)
)
del willexecutors[w] del willexecutors[w]
bal = bal_plugin.WILLEXECUTORS.default.get(constants.net.NET_NAME,{}) bal = bal_plugin.WILLEXECUTORS.default.get(chainname, {})
for bal_url, bal_executor in bal.items(): for bal_url, bal_executor in bal.items():
if not bal_url in willexecutors: if bal_url not 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(_("Contact willexecutors servers to update payment informations?")) # ping_willexecutors = bal_window.window.question(
# _(
# "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(sorted(willexecutors.items(), key=lambda w:w[1].get('sort',0),reverse=True)) w_sorted = dict(
sorted(
willexecutors.items(), key=lambda w: w[1].get("sort", 0), reverse=True
)
)
return w_sorted return w_sorted
def is_selected(willexecutor, value=None): def is_selected(willexecutor, value=None):
"""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 not value is None: if value is not None:
willexecutor['selected']=value willexecutor["selected"] = value
try: try:
return willexecutor['selected'] return willexecutor["selected"]
except: except Exception:
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"):
if willitem.get_status('COMPLETE'): if willitem.get_status("COMPLETE"):
if not willitem.get_status('PUSHED') or force: if not willitem.get_status("PUSHED") or force:
if willexecutor := willitem.we: if willexecutor := willitem.we:
url=willexecutor['url'] url = willexecutor["url"]
if willexecutor and Willexecutors.is_selected(willexecutor): if willexecutor and Willexecutors.is_selected(willexecutor):
if not url in willexecutors: if url not in willexecutors:
willexecutor['txs']="" willexecutor["txs"] = ""
willexecutor['txsids']=[] willexecutor["txsids"] = []
willexecutor['broadcast_status']= _("Waiting...") willexecutor["broadcast_status"] = _("Waiting...")
willexecutors[url] = willexecutor willexecutors[url] = willexecutor
willexecutors[url]['txs']+=str(willitem.tx)+"\n" willexecutors[url]["txs"] += str(willitem.tx) + "\n"
willexecutors[url]['txsids'].append(wid) willexecutors[url]["txsids"].append(wid)
return willexecutors return willexecutors
def only_selected_list(willexecutors): # def only_selected_list(willexecutors):
out = {} # out = {}
for url,v in willexecutors.items(): # for url, v in willexecutors.items():
if Willexecutors.is_selected(willexecutor): # if Willexecutors.is_selected(url):
out[url]=v # out[url] = v
def push_transactions_to_willexecutors(will):
willexecutors = get_transactions_to_be_pushed()
for url in willexecutors:
willexecutor = willexecutors[url]
if Willexecutors.is_selected(willexecutor):
if 'txs' in willexecutor:
Willexecutors.push_transactions_to_willexecutor(willexecutors[url]['txs'],url)
def send_request(method, url, data=None, *, timeout=10): # def push_transactions_to_willexecutors(will):
# willexecutors = Willexecutors.get_transactions_to_be_pushed()
# for url in willexecutors:
# willexecutor = willexecutors[url]
# if Willexecutors.is_selected(willexecutor):
# if "txs" in willexecutor:
# Willexecutors.push_transactions_to_willexecutor(
# willexecutors[url]["txs"], url
# )
def send_request(
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 ErrorConnectingServer('You are offline.') raise Exception("You are offline.")
_logger.debug(f'<-- {method} {url} {data}') _logger.debug(f"<-- {method} {url} {data}")
headers = {} headers = {}
headers['user-agent'] = f"BalPlugin v:{BalPlugin.version()}" headers["user-agent"] = f"BalPlugin v:{BalPlugin.__version__}"
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(method, url, response = Network.send_http_on_proxy(
method,
url,
params=data, params=data,
headers=headers, headers=headers,
on_finish=Willexecutors.handle_response, on_finish=handle_response,
timeout=timeout) timeout=timeout,
elif method == 'post': )
response = Network.send_http_on_proxy(method, url, elif method == "post":
response = Network.send_http_on_proxy(
method,
url,
body=data, body=data,
headers=headers, headers=headers,
on_finish=Willexecutors.handle_response, on_finish=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)
r['status'] = resp.status # url = Willexecutors.get_we_url_from_response(resp)
r['selected']=Willexecutors.is_selected(willexecutor) # r["url"]= url
r['url']=url # r["status"]=resp.status
except: except Exception as e:
_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', willexecutor['url']+"/"+constants.net.NET_NAME+"/pushtxs", data=willexecutor['txs'].encode('ascii')): "post",
willexecutor['broadcast_status'] = _("Success") willexecutor["url"] + "/" + chainname + "/pushtxs",
data=willexecutor["txs"].encode("ascii"),
):
willexecutor["broadcast_status"] = _("Success")
_logger.debug(f"pushed: {w}") _logger.debug(f"pushed: {w}")
if w !='thx': if w != "thx":
_logger.debug(f"error: {w}") _logger.debug(f"error: {w}")
raise Exception(w) raise Exception(w)
else: else:
@@ -160,97 +315,233 @@ class Willexecutors:
if str(e) == "already present": if str(e) == "already present":
raise Willexecutors.AlreadyPresentException() raise Willexecutors.AlreadyPresentException()
out = False out = False
willexecutor['broadcast_status'] = _("Failed") willexecutor["broadcast_status"] = _("Failed")
return out return out
def ping_servers(willexecutors): def ping_servers(willexecutors):
"""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)
netname="bitcoin" w = Willexecutors.send_request("get", url + "/" + chainname + "/info")
if constants.net.NET_NAME!="mainnet": if isinstance(w, dict):
netname=constants.net.NET_NAME willexecutor["url"] = url
w = Willexecutors.send_request('get',url+"/"+netname+"/info") willexecutor["status"] = 200
willexecutor["base_fee"] = w["base_fee"]
willexecutor['url']=url willexecutor["address"] = w["address"]
willexecutor['status'] = w['status'] willexecutor["info"] = w["info"]
willexecutor['base_fee'] = w['base_fee'] _logger.debug(f"response_data {w}")
willexecutor['address'] = w['address']
if not willexecutor['info']:
willexecutor['info'] = w['info']
_logger.debug(f"response_data {w['address']}")
except Exception as e: except Exception as e:
_logger.error(f"error {e} contacting {url}: {w}") _logger.error(f"error {e} contacting {url}: {w}")
willexecutor['status']="KO" willexecutor["status"] = "KO"
willexecutor['last_update'] = datetime.now().timestamp() willexecutor["last_update"] = datetime.now().timestamp()
return willexecutor return willexecutor
def initialize_willexecutor(willexecutor,url,status=None,selected=None): def initialize_willexecutor(willexecutor, url, status=None, old_willexecutor={}):
willexecutor['url']=url """Initialize or merge executor configuration preserving user settings.
if not status is None:
willexecutor['status'] = status
willexecutor['selected'] = Willexecutors.is_selected(willexecutor,selected)
def download_list(bal_plugin): 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
if status is not None:
willexecutor["status"] = status
else:
willexecutor["status"] = old_willexecutor.get("status",willexecutor.get("status","Ko"))
willexecutor["selected"]=Willexecutors.is_selected(old_willexecutor) or willexecutor.get("selected",False)
willexecutor["address"]=old_willexecutor.get("address",willexecutor.get("address",""))
willexecutor["promo_code"]=old_willexecutor.get("promo_code",willexecutor.get("promo_code"))
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:
l = Willexecutors.send_request('get',"https://welist.bitcoin-after.life/data/bitcoin?page=0&limit=100") willexecutors = Willexecutors.send_request(
del l['status'] "get",
for w in l: f"https://welist.bitcoin-after.life/data/{chainname}?page=0&limit=100",
willexecutor=l[w] )
Willexecutors.initialize_willexecutor(willexecutor,w,'New',False) # del willexecutors["status"]
for w in willexecutors:
if w not in ("status", "url"):
Willexecutors.initialize_willexecutor(
willexecutors[w], w, None, old_willexecutors.get(w,{})
)
# bal_plugin.WILLEXECUTORS.set(l) # bal_plugin.WILLEXECUTORS.set(l)
# bal_plugin.config.set_key(bal_plugin.WILLEXECUTORS,l,save=True) # bal_plugin.config.set_key(bal_plugin.WILLEXECUTORS,l,save=True)
return l return willexecutors
except Exception as e: except Exception as e:
_logger.error(f"Failed to download willexecutors list: {e}") _logger.error(f"Failed to download willexecutors list: {e}")
return {} return {}
def get_willexecutors_list_from_json(bal_plugin):
def get_willexecutors_list_from_json():
"""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)
for w in willexecutors: for w in willexecutors:
willexecutor = willexecutors[w] willexecutor = willexecutors[w]
Willexecutors.initialize_willexecutor(willexecutor,w,'New',False) Willexecutors.initialize_willexecutor(willexecutor, w, "New", False)
# bal_plugin.WILLEXECUTORS.set(willexecutors) # bal_plugin.WILLEXECUTORS.set(willexecutors)
return h return willexecutors
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('post',url+"/searchtx",data=txid.encode('ascii')) w = Willexecutors.send_request(
"post", url + "/searchtx", data=txid.encode("ascii")
)
return w return w
except Exception as e: except Exception as e:
_logger.error(f"error contacting {url} for checking txs {e}") _logger.error(f"error contacting {url} for checking txs {e}")
raise e raise e
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:
def __init__(self,url,base_fee,chain,info,version): """Data class representing a single will executor server.
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):
we = WillExecutor( """Create WillExecutor instance from a dictionary.
d['url'],
d['base_fee'], Args:
d['chain'], d: Dictionary containing executor data fields
d['info'],
d['version'] 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}"