34 Commits

Author SHA1 Message Date
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
15 changed files with 1445 additions and 621 deletions

445
README.md
View File

@@ -1,2 +1,443 @@
# BalPlugin
Bitcoin After Life Electrum Plugin
# Bal Electrum Plugin
**Bitcoin Electrum plugin for managing heir inheritance and locktime-based transactions**
This plugin extends Electrum wallet to support:
- **Heir inheritance management** - Define beneficiaries and inheritance shares
- **Locktime transactions** - Create time-locked transactions for future spending
- **Multi-signature setups** - Configure complex multisig scenarios with heirs
- **User-friendly wizards** - Easy setup interface
---
## 📥 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
1. **Download the source code**
```bash
git clone https://bitcoin-after.life/gitea/bitcoinafterlife/bal-electrum-plugin.git
cd bal-electrum-plugin
```
2. **Create a zip file**
```bash
zip -r bal-electrum-plugin.zip bal_electrum_plugin/
```
3. **Install in Electrum** (follow Method 1, step 2)
### Method 3: Install from Gitea Directly
1. **Download the plugin archive**
```bash
wget https://bitcoin-after.life/gitea/bitcoinafterlife/bal-electrum-plugin/archive/main.zip -O bal-electrum-plugin.zip
```
2. **Install in Electrum** (follow Method 1, step 2)
---
## 🚀 Quick Start
### 1. Enable the Plugin
- Open Electrum
- Go to **Tools → Plugins**
- Find **Bal Electrum Plugin** in the list
- Click the checkbox to enable it
- Click **Apply** and restart Electrum if prompted
### 2. Configure Your Heirs
- Go to **Tools → Bal Electrum Plugin**
- Click **Setup Heirs**
- Add beneficiaries with their Bitcoin addresses and inheritance percentages
- Set locktime conditions for each heir
### 3. Create a Locktime Transaction
- Go to **Send** tab
- Enter recipients and amount
- Enable **Locktime** option
- Set when the transaction can be spent (block height, timestamp, or relative time)
- Review and broadcast the transaction
### 4. Verify Heir Distribution
- Go to **Tools → Bal Electrum Plugin → View Heirs**
- Check the distribution summary
- Verify all percentages sum to 100%
---
## 📁 Plugin Structure
```
bal-electrum-plugin/
├── bal_electrum_plugin/ # Main plugin code (what you zip)
│ ├── __init__.py # Plugin entry point
│ ├── qt.py # Main Qt interface
│ ├── heirs.py # Heir management logic
│ ├── locktime.py # Locktime transaction handling
│ ├── dialogs/ # UI dialogs
│ │ ├── heirs_dialog.py # Heir configuration dialog
│ │ ├── locktime_dialog.py # Locktime setup dialog
│ │ └── wizard.py # Setup wizard dialog
│ └── utils.py # Utility functions
├── README.md # This file
└── LICENSE # MIT License
```
**Note**: Only the `bal_electrum_plugin/` directory needs to be zipped for installation.
---
## 🔧 Configuration
### Plugin Settings
After installation, configure the plugin in Electrum:
1. Go to **Tools → Bal Electrum Plugin → Settings**
2. Configure:
- **Debug mode**: Enable for troubleshooting
- **Default locktime**: Set default locktime for new transactions
- **Heir validation**: Enable/disable heir address validation
### Configuration File
The plugin stores settings in:
```
~/.electrum/plugins/bal_electrum_plugin/config.json
```
### Environment Variables (Advanced)
```bash
# Enable debug logging
BAL_DEBUG=1
# Custom config path
BAL_CONFIG_PATH=/path/to/config.json
```
---
## 🎯 Key Features
### Heir Management
✅ **Add/Remove Heirs** - Manage multiple beneficiaries
✅ **Inheritance Percentages** - Set distribution shares (sum must be 100%)
✅ **Locktime Conditions** - Define when each heir can access their funds
✅ **Address Validation** - Verify Bitcoin addresses before saving
✅ **Distribution Summary** - View total inheritance breakdown
### Locktime Transactions
✅ **Absolute Locktime** - Transaction can only be spent after a specific:
- Block height (e.g., `700000`)
- Timestamp (Unix timestamp, e.g., `1609459200`)
✅ **Relative Locktime** - Transaction can only be spent after:
- **Days**: Use suffix `d` (e.g., `3d` = 3 days from now)
- **Years**: Use suffix `y` (e.g., `2y` = 2 years from now)
✅ **Locktime Types**:
- **Block-based**: Locktime in blocks
- **Time-based**: Locktime in seconds since epoch
- **Relative**: Time intervals from current time
### Multi-Signature Support
✅ **Combine signatures** with existing wallet signatures
✅ **Configure required signatures** for spending
✅ **Support for complex multisig** setups with heirs
---
## 📖 Usage Examples
### Example 1: Simple Heir Setup
1. **Add an heir**:
- Address: `bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq`
- Percentage: `50%`
- Locktime: `1y` (1 year from now)
2. **Add second heir**:
- Address: `bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4`
- Percentage: `50%`
- Locktime: `2y` (2 years from now)
3. **Create transaction**:
- Send 0.5 BTC to heir1 after 1 year
- Send 0.5 BTC to heir2 after 2 years
### Example 2: Locktime Transaction
```python
# Create a transaction that can only be spent after 6 months
from bal_electrum_plugin.locktime import Locktime
locktime = Locktime(
type="relative",
value="6m", # 6 months from now
unit="blocks" # or "seconds"
)
# This transaction will be locked for 6 months
tx = create_transaction(
recipients=[recipient_address],
amount=0.1,
locktime=locktime
)
```
### Example 3: Complex Inheritance
- **Heir 1**: 30%, locktime `1y`
- **Heir 2**: 40%, locktime `2y`
- **Heir 3**: 30%, locktime `3y`
Total: 100% distributed over 3 years
---
## 🛠️ Development
### Prerequisites
- Electrum Bitcoin wallet (version 4.0.0 or later)
- Python 3.7+
- PyQt5
- pytest (for testing)
### Setup Development Environment
```bash
# Clone the 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/
```
### Install for Development
1. Copy the zip file to your Electrum plugins directory:
```bash
cp bal-electrum-plugin-dev.zip ~/.electrum/plugins/
```
2. Install in Electrum (Tools → Plugins → Add)
3. Make changes to the source code in `bal_electrum_plugin/` directory
4. Re-zip and reinstall to test changes:
```bash
cd ~/.electrum/plugins/
unzip bal-electrum-plugin-dev.zip -d bal_electrum_plugin_temp
# Make your changes to bal_electrum_plugin_temp/
zip -r bal-electrum-plugin-dev.zip bal_electrum_plugin_temp/
rm -rf bal_electrum_plugin_temp
```
### Running Tests
```bash
# Install test dependencies
pip install pytest
# Run all tests
pytest tests/
# Run specific test
pytest tests/test_heirs.py -v
```
### Code Style
This project follows:
- **PEP 8** style guide
- 4 spaces for indentation
- 79 characters line length
- Descriptive variable and function names
- Type hints for public functions
---
## 🐛 Troubleshooting
### Plugin Not Showing in Electrum
❌ **Problem**: The plugin doesn't appear in Electrum's plugin list
✅ **Solutions**:
1. Verify you're using Electrum 4.0.0 or later
2. Check that the zip file contains the `bal_electrum_plugin/` directory
3. Ensure the directory structure is correct inside the zip
4. Restart Electrum after installation
5. Check Electrum logs for errors (Help → Debug → Console)
### Locktime Not Working
❌ **Problem**: Locktime conditions aren't being enforced
✅ **Solutions**:
1. Verify locktime format: `3d` (days), `2y` (years), or timestamp
2. Check that the locktime is in the future
3. Ensure your Bitcoin node supports locktime transactions
4. Test with a small amount first
5. Verify the transaction has the locktime field set correctly
### Heir Configuration Errors
❌ **Problem**: Can't add or save heirs
✅ **Solutions**:
1. Verify Bitcoin addresses are valid (use testnet for testing)
2. Check that percentages sum to exactly 100%
3. Ensure locktime values are valid formats
4. Enable debug mode to see detailed error messages
5. Check plugin logs for specific error details
### Common Error Messages
| Error | Cause | Solution |
|-------|-------|----------|
| `Invalid checksum` | Bad Bitcoin address | Verify the address |
| `Percentage must sum to 100%` | Invalid heir distribution | Adjust percentages |
| `Locktime in the past` | Invalid locktime value | Use future date/time |
| `Plugin not found` | Incorrect zip structure | Check zip contents |
---
## 🤝 Contributing
We welcome contributions from the community! Here's how to help:
### Ways to Contribute
1. **Report bugs** - Open an issue on [Gitea](https://bitcoin-after.life/gitea/bitcoinafterlife/bal-electrum-plugin/issues)
2. **Suggest features** - Share your ideas for new functionality
3. **Write documentation** - Improve README, add examples, create tutorials
4. **Submit pull requests** - Fix bugs or add new features
5. **Test the plugin** - Try different scenarios and report issues
6. **Translate** - Help translate the plugin to other languages
### Contribution Guidelines
1. **Fork the repository** and create your feature branch
2. **Add tests** for new functionality
3. **Follow PEP 8** style guide
4. **Write clear commit messages** following [Conventional Commits](https://www.conventionalcommits.org/)
5. **Update documentation** for new features
6. **Open a Pull Request** to the `main` branch
7. **Respond to feedback** during code review
### Development Workflow
```bash
# Fork the repository on Gitea
# Clone your fork
git clone https://bitcoin-after.life/gitea/YOUR_USERNAME/bal-electrum-plugin.git
cd bal-electrum-plugin
# Add upstream remote
git remote add upstream https://bitcoin-after.life/gitea/bitcoinafterlife/bal-electrum-plugin.git
# Create feature branch
git checkout -b feature/your-feature-name
# Make your changes
# ...
# Test your changes
pytest tests/
# Commit changes
git add .
git commit -m "feat: add new feature description"
# Push to your fork
git push origin feature/your-feature-name
# Open Pull Request on Gitea
```
---
## 📜 License
This project is licensed under the **MIT License** - see the [LICENSE](LICENSE) file for details.
### License Summary
- Free to use and modify
- Commercial use allowed
- No warranty provided
- Attribution required
---
## 🙏 Acknowledgments
- **Electrum developers** - For creating the amazing Electrum wallet
- **Bitcoin community** - For continuous innovation and support
- **All contributors** - For improving this plugin
- **Early users** - For testing and feedback
---
## 📞 Support & Contact
### Getting Help
- **Documentation**: This README file
- **Issues**: [Gitea Issues](https://bitcoin-after.life/gitea/bitcoinafterlife/bal-electrum-plugin/issues)
- **Discussions**: [Gitea Discussions](https://bitcoin-after.life/gitea/bitcoinafterlife/bal-electrum-plugin/discussions)
- **Wiki**: Check the `docs/` directory for additional documentation
### Community
- **Matrix/Element**: #bal-electrum-plugin:matrix.org
- **Telegram**: @bal_electrum_plugin
- **Email**: support@bal-electrum-plugin.org
---
## 📊 Version Information
**Current Version**: 1.0.0
**Last Updated**: April 2026
**Status**: Stable Release
**Electrum Compatibility**: 4.0.0+
### Version History
| Version | Date | Changes |
|---------|------|---------|
| 1.0.0 | Apr 2026 | Initial release with heir and locktime features |
| 0.9.0 | Mar 2026 | Beta testing phase |
| 0.1.0 | Feb 2026 | Initial development |
---
## 🔗 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.life/gitea/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**: This plugin deals with real Bitcoin transactions. Always test with small amounts first and verify all settings before using with large amounts.
**🔒 Security**: Never share your seed phrase or private keys. This plugin only creates transactions, it doesn't store your keys.

View File

@@ -1 +1 @@
0.2.2d
0.2.8

61
bal.py
View File

@@ -1,6 +1,7 @@
import os
#import random
#import zipfile as zipfile_lib
# import random
# import zipfile as zipfile_lib
from electrum import json_db
from electrum.logging import get_logger
@@ -9,7 +10,8 @@ from electrum.transaction import tx_from_any
def get_will_settings(x):
print(x)
# print(x)
pass
json_db.register_dict("heirs", tuple, None)
@@ -17,7 +19,6 @@ json_db.register_dict("will", dict, None)
json_db.register_dict("will_settings", lambda x: x, None)
def get_will(x):
try:
x["tx"] = tx_from_any(x["tx"])
@@ -46,37 +47,31 @@ class BalConfig:
class BalPlugin(BasePlugin):
LATEST_VERSION = "1"
KNOWN_VERSIONS = ("0", "1")
assert LATEST_VERSION in KNOWN_VERSIONS
def version():
_version=None
__version__ = "0.2.8" #AUTOMATICALLY GENERATED DO NOT EDIT
def version(self):
if not self._version:
try:
f = ""
with open("VERSION", "r") as fi:
f = str(fi.readline())
return f
except:
return "unknown"
with open("{}/VERSION".format(self.plugin_dir), "r") as fi:
f = str(fi.read())
self._version = f.strip()
except Exception as e:
_logger.error(f"failed to get version: {e}")
self._version="unknown"
return self._version
SIZE = (159, 97)
def __init__(self, parent, config, name):
print("init bal_plugin")
self.logger = get_logger(__name__)
BasePlugin.__init__(self, parent, config, name)
self.base_dir = os.path.join(config.electrum_path(), "bal")
self.plugin_dir = os.path.split(os.path.realpath(__file__))[0]
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
sys.path.insert(0, zipfile)
# print("sono state listate?")
self.parent = parent
self.config = config
self.name = name
@@ -96,10 +91,10 @@ class BalPlugin(BasePlugin):
self.PREVIEW = BalConfig(config, "bal_preview", True)
self.SAVE_TXS = BalConfig(config, "bal_save_txs", True)
self.WILLEXECUTORS = BalConfig(config, "bal_willexecutors", True)
self.PING_WILLEXECUTORS = BalConfig(config, "bal_ping_willexecutors", True)
self.ASK_PING_WILLEXECUTORS = BalConfig(
config, "bal_ask_ping_willexecutors", True
)
# self.PING_WILLEXECUTORS = BalConfig(config, "bal_ping_willexecutors", True)
# self.ASK_PING_WILLEXECUTORS = BalConfig(
# config, "bal_ask_ping_willexecutors", True
# )
self.NO_WILLEXECUTOR = BalConfig(config, "bal_no_willexecutor", True)
self.HIDE_REPLACED = BalConfig(config, "bal_hide_replaced", True)
self.HIDE_INVALIDATED = BalConfig(config, "bal_hide_invalidated", True)
@@ -110,6 +105,15 @@ class BalPlugin(BasePlugin):
"bal_willexecutors",
{
"mainnet": {
"https://we.bitcoin-after.life": {
"base_fee": 100000,
"status": "New",
"info": "Bitcoin After Life Will Executor",
"address": "bc1qusymuetsz2psaqzqxv8qmzcy64d9meckj3lxxf",
"selected": True,
}
},
"testnet": {
"https://we.bitcoin-after.life": {
"base_fee": 100000,
"status": "New",
@@ -117,7 +121,7 @@ class BalPlugin(BasePlugin):
"address": "bcrt1qa5cntu4hgadw8zd3n6sq2nzjy34sxdtd9u0gp7",
"selected": True,
}
}
},
},
)
self.WILL_SETTINGS = BalConfig(
@@ -145,16 +149,13 @@ class BalPlugin(BasePlugin):
self.HIDE_REPLACED.set(self._hide_replaced)
def validate_will_settings(self, will_settings):
# print(type(will_settings))
# print(will_settings.get('baltx_fees',1),1)
if int(will_settings.get("baltx_fees", 1)) < 1:
will_settings["baltx_fees"] = 1
if not will_settings.get("threshold"):
will_settings["threshold"] = "180d"
if not will_settings.get("locktime") == "":
if not will_settings.get("locktime"):
will_settings["locktime"] = "1y"
return will_settings
def default_will_settings(self):
return {"baltx_fees": 100, "threshold": "180d", "locktime": "1y"}

View File

@@ -2,7 +2,7 @@ import os
PLUGIN_DIR = os.path.split(os.path.realpath(__file__))[0]
DEFAULT_ICON = "bal32x32.png"
DEFAULT_ICON_PATH = ""
DEFAULT_ICON_PATH = "icons"
def icon_path(icon_basename: str = DEFAULT_ICON):

254
heirs.py
View File

@@ -1,24 +1,40 @@
import datetime
import json
# import datetime
# import json
import math
import random
import re
import threading
import urllib.parse
import urllib.request
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Tuple
# import urllib.parse
# import urllib.request
from typing import (
TYPE_CHECKING,
Any,
Dict,
# List,
Optional,
# Sequence,
Tuple,
)
import dns
from dns.exception import DNSException
from electrum import bitcoin, constants, descriptor, dnssec
from electrum import (
bitcoin,
constants,
# descriptor,
dnssec,
)
from electrum.logging import Logger, get_logger
from electrum.transaction import (
PartialTransaction,
PartialTxInput,
PartialTxOutput,
TxOutpoint,
TxOutput,
TxOutpoint,
# TxOutput,
)
from electrum.payment_identifier import PaymentIdentifier
from electrum.util import (
bfh,
read_json_file,
@@ -29,10 +45,11 @@ from electrum.util import (
from .util import Util
from .willexecutors import Willexecutors
from electrum.util import BitcoinException
if TYPE_CHECKING:
from .simple_config import SimpleConfig
from .wallet_db import WalletDB
# from .wallet_db import WalletDB
_logger = get_logger(__name__)
@@ -41,6 +58,7 @@ HEIR_ADDRESS = 0
HEIR_AMOUNT = 1
HEIR_LOCKTIME = 2
HEIR_REAL_AMOUNT = 3
HEIR_DUST_AMOUNT = 4
TRANSACTION_LABEL = "inheritance transaction"
@@ -54,28 +72,22 @@ def reduce_outputs(in_amount, out_amount, fee, outputs):
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):
available_utxos = sorted(
@@ -84,39 +96,45 @@ def prepare_transactions(locktimes, available_utxos, fees, wallet):
x.value_sats(), x.prevout.txid, x.prevout.out_idx
),
)
total_used_utxos = []
# total_used_utxos = []
txsout = {}
locktime, _ = Util.get_lowest_locktimes(locktimes)
if not locktime:
_logger.info("prepare transactions, no locktime")
return
locktime = locktime[0]
heirs = locktimes[locktime]
vero = True
while vero:
vero = False
true = True
while true:
true = False
fee = fees.get(locktime, 0)
out_amount = fee
description = ""
outputs = []
paid_heirs = {}
for name, heir in heirs.items():
if len(heir) > HEIR_REAL_AMOUNT and "DUST" not in str(
heir[HEIR_REAL_AMOUNT]
):
try:
if len(heir) > HEIR_REAL_AMOUNT:
real_amount = heir[HEIR_REAL_AMOUNT]
out_amount += real_amount
description += f"{name}\n"
paid_heirs[name] = heir
outputs.append(
PartialTxOutput.from_address_and_value(
heir[HEIR_ADDRESS], real_amount
)
)
else:
pass
out_amount += real_amount
description += f"{name}\n"
except BitcoinException as e:
_logger.info("exception decoding output {} - {}".format(type(e), e))
heir[HEIR_REAL_AMOUNT] = e
except Exception as e:
heir[HEIR_REAL_AMOUNT] = e
_logger.error(f"error preparing transactions: {e}")
pass
paid_heirs[name] = heir
in_amount = 0.0
used_utxos = []
@@ -125,20 +143,32 @@ def prepare_transactions(locktimes, available_utxos, fees, wallet):
value = utxo.value_sats()
in_amount += value
used_utxos.append(utxo)
if in_amount > out_amount:
if in_amount >= out_amount:
break
except IndexError as e:
_logger.error(
f"error preparing transactions index error {e} {in_amount}, {out_amount}"
)
pass
if int(in_amount) < int(out_amount):
break
_logger.error(
"error preparing transactions in_amount < out_amount ({} < {}) "
)
continue
heirsvalue = out_amount
change = get_change_output(wallet, in_amount, out_amount, fee)
if change:
outputs.append(change)
for i in range(0, 100):
random.shuffle(outputs)
print(outputs)
#op_return_text = "Hello Bal!"
## Convert text to hex
#op_return_hex = op_return_text.encode('utf-8').hex()
#op_return_script = create_op_return_script(op_return_hex)
#outputs.append(PartialTxOutput(value=0, scriptpubkey=op_return_script))
tx = PartialTransaction.from_io(
used_utxos,
outputs,
@@ -186,7 +216,7 @@ def get_utxos_from_inputs(tx_inputs, tx, utxos):
# TODO calculate de minimum inputs to be invalidated
def invalidate_inheritance_transactions(wallet):
listids = []
# listids = []
utxos = {}
dtxs = {}
for k, v in wallet.get_all_labels().items():
@@ -215,7 +245,7 @@ def invalidate_inheritance_transactions(wallet):
for key, value in utxos:
for tx in value["txs"]:
txid = tx.txid()
if not txid in invalidated:
if txid not in invalidated:
invalidated.append(tx.txid())
remaining[key] = value
@@ -223,10 +253,10 @@ def invalidate_inheritance_transactions(wallet):
def print_transaction(heirs, tx, locktimes, tx_fees):
jtx = tx.to_json()
print(f"TX: {tx.txid()}\t-\tLocktime: {jtx['locktime']}")
print(f"---")
print("---")
for inp in jtx["inputs"]:
print(f"{inp['address']}: {inp['value_sats']}")
print(f"---")
print("---")
for out in jtx["outputs"]:
heirname = ""
for key in heirs.keys():
@@ -248,7 +278,7 @@ def print_transaction(heirs, tx, locktimes, tx_fees):
print()
try:
print(tx.serialize_to_network())
except:
except Exception:
print("impossible to serialize")
print()
@@ -264,13 +294,14 @@ def get_change_output(wallet, in_amount, out_amount, fee):
class Heirs(dict, Logger):
def __init__(self, db: "WalletDB"):
def __init__(self, wallet):
Logger.__init__(self)
self.db = db
self.db = wallet.db
self.wallet = wallet
d = self.db.get("heirs", {})
try:
self.update(d)
except e as Exception:
except Exception:
return
def invalidate_transactions(self, wallet):
@@ -304,7 +335,7 @@ class Heirs(dict, Logger):
locktime = Util.parse_locktime_string(self[key][HEIR_LOCKTIME])
if locktime > from_locktime and not a or locktime <= from_locktime and a:
locktimes[int(locktime)] = None
return locktimes.keys()
return list(locktimes.keys())
def check_locktime(self):
return False
@@ -318,6 +349,8 @@ class Heirs(dict, Logger):
column = HEIR_AMOUNT
if real:
column = HEIR_REAL_AMOUNT
if "DUST" in str(v[column]):
column = HEIR_DUST_AMOUNT
value = int(
math.floor(
total_balance
@@ -328,6 +361,10 @@ class Heirs(dict, Logger):
if value > wallet.dust_threshold():
heir_list[key].insert(HEIR_REAL_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:
raise e
@@ -336,10 +373,10 @@ class Heirs(dict, Logger):
def amount_to_float(self, amount):
try:
return float(amount)
except:
except Exception:
try:
return float(amount[:-1])
except:
except Exception:
return 0.0
def fixed_percent_lists_amount(self, from_locktime, dust_threshold, reverse=False):
@@ -347,31 +384,50 @@ class Heirs(dict, Logger):
fixed_amount = 0.0
percent_heirs = {}
percent_amount = 0.0
fixed_amount_with_dust = 0.0
for key in self.keys():
try:
cmp = (
Util.parse_locktime_string(self[key][HEIR_LOCKTIME]) - from_locktime
)
if cmp <= 0:
_logger.debug(
"cmp < 0 {} {} {} {}".format(
cmp, key, self[key][HEIR_LOCKTIME], from_locktime
)
)
continue
if Util.is_perc(self[key][HEIR_AMOUNT]):
percent_amount += float(self[key][HEIR_AMOUNT][:-1])
percent_heirs[key] = list(self[key])
else:
heir_amount = int(math.floor(float(self[key][HEIR_AMOUNT])))
fixed_amount_with_dust += heir_amount
fixed_heirs[key] = list(self[key])
if heir_amount > dust_threshold:
fixed_amount += heir_amount
fixed_heirs[key] = list(self[key])
fixed_heirs[key].insert(HEIR_REAL_AMOUNT, heir_amount)
else:
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:
_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(
self, balance, total_fees, wallet, willexecutor=False, from_locktime=0
):
if balance<total_fees or balance < wallet.dust_threshold():
raise BalanceTooLowException(balance,wallet.dust_threshold(),total_fees)
willexecutors_amount = 0
willexecutors = {}
heir_list = {}
@@ -392,7 +448,7 @@ class Heirs(dict, Logger):
willexecutors[
'w!ll3x3c"' + willexecutor["url"] + '"' + str(locktime)
] = h
except Exception as e:
except Exception:
return [], False
else:
_logger.error(
@@ -400,9 +456,15 @@ class Heirs(dict, Logger):
),
heir_list.update(willexecutors)
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:
fixed_amount = self.normalize_perc(
fixed_heirs, newbalance, fixed_amount, wallet
@@ -412,18 +474,16 @@ class Heirs(dict, Logger):
heir_list.update(fixed_heirs)
newbalance -= fixed_amount
if newbalance > 0:
perc_amount = self.normalize_perc(
percent_heirs, newbalance, percent_amount, wallet
)
newbalance -= perc_amount
heir_list.update(percent_heirs)
if newbalance > 0:
newbalance += fixed_amount
fixed_amount = self.normalize_perc(
fixed_heirs, newbalance, fixed_amount, wallet, real=True
fixed_heirs, newbalance, fixed_amount_with_dust, wallet, real=True
)
newbalance -= fixed_amount
heir_list.update(fixed_heirs)
@@ -436,7 +496,7 @@ class Heirs(dict, Logger):
locktimes = {}
for key, value in heir_list:
locktime = Util.parse_locktime_string(value[HEIR_LOCKTIME])
if not locktime in locktimes:
if locktime not in locktimes:
locktimes[locktime] = {key: value}
else:
locktimes[locktime][key] = value
@@ -450,6 +510,7 @@ class Heirs(dict, Logger):
):
Heirs._validate(self)
if len(self) <= 0:
_logger.info("while building transactions there was no heirs")
return
balance = 0.0
len_utxo_set = 0
@@ -465,6 +526,7 @@ class Heirs(dict, Logger):
len_utxo_set += 1
available_utxos.append(utxo)
if len_utxo_set == 0:
_logger.info("no usable utxos")
return
j = -2
willexecutorsitems = list(willexecutors.items())
@@ -476,7 +538,7 @@ class Heirs(dict, Logger):
break
elif 0 <= 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
else:
willexecutor["url"] = url
@@ -488,17 +550,22 @@ class Heirs(dict, Logger):
break
fees = {}
i = 0
while True:
while i < 10:
txs = {}
redo = False
i += 1
total_fees = 0
for fee in fees:
total_fees += int(fees[fee])
newbalance = balance
# newbalance = balance
try:
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
@@ -506,11 +573,16 @@ class Heirs(dict, Logger):
if not txs:
return {}
except Exception as e:
_logger.error(
f"build transactions: error preparing transactions: {e}"
)
try:
if "w!ll3x3c" in e.heirname:
Willexecutors.is_selected(willexecutors[w], False)
Willexecutors.is_selected(
e.heirname[len("w!ll3x3c") :], False
)
break
except:
except Exception:
raise e
total_fees = 0
total_fees_real = 0
@@ -525,7 +597,7 @@ class Heirs(dict, Logger):
rfee = tx.input_value() - tx.output_value()
if rfee < fee or rfee > fee + wallet.dust_threshold():
redo = True
oldfees = fees.get(tx.my_locktime, 0)
# oldfees = fees.get(tx.my_locktime, 0)
fees[tx.my_locktime] = fee
if balance - total_in > wallet.dust_threshold():
@@ -534,6 +606,11 @@ class Heirs(dict, Logger):
break
if i >= 10:
break
else:
_logger.info(
f"no locktimes for willexecutor {willexecutor} skipped"
)
break
alltxs.update(txs)
return alltxs
@@ -633,7 +710,7 @@ class Heirs(dict, Logger):
return None
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}")
return address
@@ -662,12 +739,14 @@ class Heirs(dict, Logger):
return (address, amount, locktime)
def _validate(data, timestamp_to_check=False):
for k, v in list(data.items()):
if k == "heirs":
return Heirs._validate(v)
return Heirs._validate(v, timestamp_to_check)
try:
Heirs.validate_heir(k, v)
Heirs.validate_heir(k, v, timestamp_to_check)
except Exception as e:
_logger.info(f"exception heir removed {e}")
data.pop(k)
return data
@@ -686,3 +765,28 @@ class LocktimeNotValid(ValueError):
class HeirExpiredException(LocktimeNotValid):
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",
"fullname": "Bitcoin After Life",
"description": "Provides free and decentralized inheritance support<br> Version: 0.2.2d",
"description": "Provides free and decentralized inheritance support<br> Version: 0.2.8",
"author":"Svatantrya",
"available_for": ["qt"],
"icon":"icons/bal32x32.png"

675
qt.py

File diff suppressed because it is too large Load Diff

101
util.py
View File

@@ -1,12 +1,10 @@
import bisect
import urllib.parse
import urllib.request
from datetime import datetime, timedelta
from electrum.gui.qt.util import getSaveFileName
from electrum.i18n import _
from electrum.transaction import PartialTxOutput
from electrum.util import FileExportFailed, FileImportFailed, write_json_file
from electrum.util import FileExportFailed
LOCKTIME_THRESHOLD = 500000000
@@ -19,7 +17,7 @@ class Util:
dt = datetime.fromtimestamp(locktime).isoformat()
return dt
except Exception as e:
except Exception:
pass
return str(locktime)
@@ -29,7 +27,7 @@ class Util:
return locktime
else:
return int(locktime)
except Exception as e:
except Exception:
pass
dt_object = datetime.fromisoformat(locktime)
timestamp = dt_object.timestamp()
@@ -39,7 +37,7 @@ class Util:
try:
return int(locktime)
except Exception as e:
except Exception:
pass
try:
now = datetime.now()
@@ -58,7 +56,7 @@ class Util:
height = Util.get_current_height(w.network)
locktime += int(height)
return int(locktime)
except Exception as e:
except Exception:
pass
return 0
@@ -77,32 +75,34 @@ class Util:
else:
try:
return int(float(amount) * pow(10, decimal_point))
except:
except Exception:
return 0
def decode_amount(amount, decimal_point):
if Util.is_perc(amount):
return amount
else:
num = 8 - decimal_point
basestr = "{{:0{}.{}f}}".format(num, num)
return "{:08.8f}".format(float(amount) / pow(10, decimal_point))
basestr = "{{:0.{}f}}".format(decimal_point)
try:
return basestr.format(float(amount) / pow(10, decimal_point))
except Exception:
return str(amount)
def is_perc(value):
try:
return value[-1] == "%"
except:
except Exception:
return False
def cmp_array(heira, heirb):
try:
if not len(heira) == len(heirb):
if len(heira) != len(heirb):
return False
for h in range(0, len(heira)):
if not heira[h] == heirb[h]:
if heira[h] != heirb[h]:
return False
return True
except:
except Exception:
return False
def cmp_heir(heira, heirb):
@@ -120,7 +120,7 @@ class Util:
and willexecutora["base_fee"] == willexecutorb["base_fee"]
):
return True
except:
except Exception:
return False
return False
@@ -146,7 +146,7 @@ class Util:
):
for heira in heirsa:
if (
exclude_willexecutors and not 'w!ll3x3c"' in heira
exclude_willexecutors and 'w!ll3x3c"' not in heira
) or not exclude_willexecutors:
found = False
for heirb in heirsb:
@@ -173,8 +173,8 @@ class Util:
):
try:
for heir in heirsa:
if not 'w!ll3x3c"' in heir:
if not heir in heirsb or not cmp_function(
if 'w!ll3x3c"' not in heir:
if heir not in heirsb or not cmp_function(
heirsa[heir], heirsb[heir]
):
if not Util.search_heir_by_values(heirsb, heirsa[heir], [0, 3]):
@@ -213,7 +213,7 @@ class Util:
def get_value_amount(txa, txb):
outputsa = txa.outputs()
outputsb = txb.outputs()
# outputsb = txb.outputs()
value_amount = 0
for outa in outputsa:
@@ -258,10 +258,10 @@ class Util:
def cmp_locktime(locktimea, locktimeb):
if locktimea == locktimeb:
return 0
strlocktime = str(locktimea)
strlocktimea = str(locktimea)
strlocktimeb = str(locktimeb)
intlocktimea = Util.str_to_locktime(strlocktimea)
intlocktimeb = Util.str_to_locktime(strlocktimeb)
# intlocktimea = Util.str_to_locktime(strlocktimea)
# intlocktimeb = Util.str_to_locktime(strlocktimeb)
if locktimea[-1] in "ydb":
if locktimeb[-1] == locktimea[-1]:
return int(strlocktimea[-1]) - int(strlocktimeb[-1])
@@ -282,12 +282,12 @@ class Util:
def get_lowest_locktimes(locktimes):
sorted_timestamp = []
sorted_block = []
for l in locktimes:
l = Util.parse_locktime_string(l)
if l < LOCKTIME_THRESHOLD:
bisect.insort(sorted_block, l)
for locktime in locktimes:
locktime = Util.parse_locktime_string(locktime)
if locktime < LOCKTIME_THRESHOLD:
bisect.insort(sorted_block, locktime)
else:
bisect.insort(sorted_timestamp, l)
bisect.insort(sorted_timestamp, locktime)
return sorted(sorted_timestamp), sorted(sorted_block)
@@ -313,11 +313,11 @@ class Util:
def utxo_to_str(utxo):
try:
return utxo.to_str()
except Exception as e:
except Exception:
pass
try:
return utxo.prevout.to_str()
except Exception as e:
except Exception:
pass
return str(utxo)
@@ -380,7 +380,7 @@ class Util:
out.is_change = True
return out
def get_current_height(network: "Network"):
def get_current_height(network):
# if no network or not up to date, just set locktime to zero
if not network:
return 0
@@ -403,38 +403,34 @@ class Util:
def print_var(var, name="", veryverbose=False):
print(f"---{name}---")
if not var is None:
try:
print("doc:", doc(var))
except:
pass
if var is not None:
try:
print("str:", str(var))
except:
except Exception:
pass
try:
print("repr", repr(var))
except:
except Exception:
pass
try:
print("dict", dict(var))
except:
except Exception:
pass
try:
print("dir", dir(var))
except:
except Exception:
pass
try:
print("type", type(var))
except:
except Exception:
pass
try:
print("to_json", var.to_json())
except:
except Exception:
pass
try:
print("__slotnames__", var.__slotnames__)
except:
except Exception:
pass
print(f"---end {name}---")
@@ -456,11 +452,11 @@ class Util:
Util.print_var(prevout._asdict())
print(f"---prevout-end {name}---")
def export_meta_gui(electrum_window: "ElectrumWindow", title, exporter):
def export_meta_gui(electrum_window, title, exporter):
filter_ = "All files (*)"
filename = getSaveFileName(
parent=electrum_window,
title=_("Select file to save your {}").format(title),
title=_("Select file to save your {}".format(title)),
filename="BALplugin_{}".format(title),
filter=filter_,
config=electrum_window.config,
@@ -473,7 +469,7 @@ class Util:
electrum_window.show_critical(str(e))
else:
electrum_window.show_message(
_("Your {0} were exported to '{1}'").format(title, str(filename))
_("Your {0} were exported to '{1}'".format(title, str(filename)))
)
def copy(dicto, dictfrom):
@@ -498,3 +494,16 @@ class Util:
del will[txid]["tx_fees"]
have_to_update = True
return have_to_update
def text_to_hex(text: str) -> str:
"""Convert text to hexadecimal string"""
hex_string = text.encode('utf-8').hex()
return hex_string
def hex_to_text(hex_string: str) -> str:
"""Convert hexadecimal string back to text (for verification)"""
try:
return bytes.fromhex(hex_string).decode('utf-8')
except Exception:
return "Error: Invalid hex string"

View File

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

View File

@@ -15,10 +15,8 @@ from PyQt6.QtWidgets import (
QGroupBox,
QTextEdit,
)
from PyQt6.QtCore import Qt
from electrum.storage import WalletStorage
from electrum.util import MyEncoder
from bal_wallet_utils import fix_will_settings_tx_fees, uninstall_bal, read_wallet
from bal_wallet_utils import fix_will_settings_tx_fees, uninstall_bal, save
class WalletUtilityGUI(QMainWindow):
@@ -175,7 +173,7 @@ class WalletUtilityGUI(QMainWindow):
if have_to_save:
try:
save_wallet(json_wallet, storage)
save(json_wallet, storage)
self.log_message(f"SUCCESS: {message}")
except Exception as e:
self.log_message(f"Save error: {str(e)}")
@@ -190,14 +188,6 @@ class WalletUtilityGUI(QMainWindow):
def main():
app = QApplication(sys.argv)
# Check if dependencies are available
try:
from electrum.storage import WalletStorage
from electrum.util import MyEncoder
except ImportError as e:
print(f"ERROR: Cannot import Electrum dependencies: {str(e)}")
return 1
window = WalletUtilityGUI()
window.show()

169
will.py
View File

@@ -12,11 +12,7 @@ from electrum.transaction import (
tx_from_any,
)
from electrum.util import (
FileImportFailed,
bfh,
decimal_point_to_base_unit_name,
read_json_file,
write_json_file,
)
from .util import Util
@@ -28,7 +24,6 @@ _logger = get_logger(__name__)
class Will:
# return an array with the list of children
def get_children(will, willid):
out = []
for _id in will:
@@ -82,13 +77,11 @@ class Will:
for txin in will[wid].tx.inputs():
txid = txin.prevout.txid.hex()
if txid in will:
# print(will[txid].tx.outputs())
# print(txin.prevout.out_idx)
change = will[txid].tx.outputs()[txin.prevout.out_idx]
txin._trusted_value_sats = change.value
try:
txin.script_descriptor = change.script_descriptor
except:
except Exception:
pass
txin.is_mine = True
txin._TxInput__address = change.address
@@ -135,8 +128,8 @@ class Will:
to_delete.append(wid)
to_add[ow.tx.txid()] = ow.to_dict()
for eid, err in errors.items():
new_txid = err.tx.txid()
# for eid, err in errors.items():
# new_txid = err.tx.txid()
for k, w in to_add.items():
will[k] = w
@@ -155,6 +148,16 @@ class Will:
inp._TxInput__value_sats = change.value
return inp
"""
in questa situazione sono presenti due transazioni con id differente(quindi transazioni differenti)
per prima cosa controllo il locktime
se il locktime della nuova transazione e' maggiore del locktime della vecchia transazione, allora
confronto gli eredi, per locktime se corrispondono controllo i willexecutor
se hanno la stessa url ma le fee vecchie sono superiori alle fee nuove, allora anticipare.
"""
def check_anticipate(ow: "WillItem", nw: "WillItem"):
anticipate = Util.anticipate_locktime(ow.tx.locktime, days=1)
if int(nw.tx.locktime) >= int(anticipate):
@@ -194,7 +197,7 @@ class Will:
outputs = w.tx.outputs()
found = False
old_txid = w.tx.txid()
ntx = None
# ntx = None
for i in range(0, len(inputs)):
if (
inputs[i].prevout.txid.hex() == otxid
@@ -205,7 +208,7 @@ class Will:
will[wid].tx.set_rbf(True)
will[wid].tx._inputs[i] = Will.new_input(wid, idx, change)
found = True
if found == True:
if found:
pass
new_txid = will[wid].tx.txid()
@@ -232,7 +235,7 @@ class Will:
for i in inputs:
prevout_str = i.prevout.to_str()
inp = [w, will[w], i]
if not prevout_str in all_inputs:
if prevout_str not in all_inputs:
all_inputs[prevout_str] = [inp]
else:
all_inputs[prevout_str].append(inp)
@@ -245,7 +248,7 @@ class Will:
min_locktime = min(values, key=lambda x: x[1].tx.locktime)[1].tx.locktime
for w in values:
if w[1].tx.locktime == min_locktime:
if not i in all_inputs_min_locktime:
if i not in all_inputs_min_locktime:
all_inputs_min_locktime[i] = [w]
else:
all_inputs_min_locktime[i].append(w)
@@ -258,7 +261,7 @@ class Will:
to_append = {}
new_inputs = Will.get_all_inputs(will, only_valid=True)
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():
redo = True
to_delete.append(nid)
@@ -268,25 +271,36 @@ class Will:
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:
try:
del will[w]
except:
except Exception:
pass
for k, w in to_append.items():
will[k] = w
if redo:
Will.search_anticipate_rec(will, old_inputs)
def update_will(old_will, new_will):
all_old_inputs = Will.get_all_inputs(old_will, only_valid=True)
all_inputs_min_locktime = Will.get_all_inputs_min_locktime(all_old_inputs)
all_new_inputs = Will.get_all_inputs(new_will)
# all_inputs_min_locktime = Will.get_all_inputs_min_locktime(all_old_inputs)
# all_new_inputs = Will.get_all_inputs(new_will)
# check if the new input is already spent by other transaction
# if it is use the same locktime, or anticipate.
Will.search_anticipate_rec(new_will, all_old_inputs)
other_inputs = Will.get_all_inputs(old_will, {})
try:
Will.normalize_will(new_will, others_inputs=other_inputs)
@@ -325,25 +339,27 @@ class Will:
utxos = wallet.get_utxos()
filtered_inputs = []
prevout_to_spend = []
current_height = Util.get_current_height(wallet.network)
for prevout_str, ws in inputs.items():
for w in ws:
if not w[0] in filtered_inputs:
if w[0] not in filtered_inputs:
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)
balance = 0
utxo_to_spend = []
for utxo in utxos:
if utxo.is_coinbase_output() and utxo.block_height < current_height+100:
continue
utxo_str = utxo.prevout.to_str()
if utxo_str in prevout_to_spend:
balance += inputs[utxo_str][0][2].value_sats()
utxo_to_spend.append(utxo)
if len(utxo_to_spend) > 0:
change_addresses = wallet.get_change_addresses_for_new_transaction()
out = PartialTxOutput.from_address_and_value(change_addresses[0], balance)
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
)
@@ -374,7 +390,7 @@ class Will:
return True
def search_rai(all_inputs, all_utxos, will, wallet):
will_only_valid = Will.only_valid_or_replaced_list(will)
# will_only_valid = Will.only_valid_or_replaced_list(will)
for inp, ws in all_inputs.items():
inutxo = Util.in_utxo(inp, all_utxos)
for w in ws:
@@ -412,7 +428,7 @@ class Will:
def set_invalidate(wid, will=[]):
will[wid].set_status("INVALIDATED", True)
if will[wid].children:
for c in self.children.items():
for c in will[wid].children.items():
Will.set_invalidate(c[0], will)
def check_tx_height(tx, wallet):
@@ -422,13 +438,16 @@ class Will:
# check if transactions are stil valid tecnically valid
def check_invalidated(willtree, utxos_list, wallet):
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():
inp_str = Util.utxo_to_str(inp)
if not inp_str in utxos_list:
if inp_str not in utxos_list:
if wallet:
height = Will.check_tx_height(w.tx, wallet)
print(type(height))
if height < 0:
Will.set_invalidate(wid, willtree)
elif height == 0:
@@ -436,21 +455,21 @@ class Will:
else:
w.set_status("CONFIRMED", True)
def reflect_to_children(treeitem):
if not treeitem.get_status("VALID"):
_logger.debug(f"{tree:item._id} status not valid looking for children")
for child in treeitem.children:
wc = willtree[child]
if wc.get_status("VALID"):
if treeitem.get_status("INVALIDATED"):
wc.set_status("INVALIDATED", True)
if treeitem.get_status("REPLACED"):
wc.set_status("REPLACED", True)
if wc.children:
Will.reflect_to_children(wc)
# def reflect_to_children(treeitem):
# if not treeitem.get_status("VALID"):
# _logger.debug(f"{tree:item._id} status not valid looking for children")
# for child in treeitem.children:
# wc = willtree[child]
# if wc.get_status("VALID"):
# if treeitem.get_status("INVALIDATED"):
# wc.set_status("INVALIDATED", True)
# if treeitem.get_status("REPLACED"):
# wc.set_status("REPLACED", True)
# if wc.children:
# Will.reflect_to_children(wc)
def check_amounts(heirs, willexecutors, all_utxos, timestamp_to_check, dust):
fixed_heirs, fixed_amount, perc_heirs, perc_amount = (
fixed_heirs, fixed_amount, perc_heirs, perc_amount, fixed_amount_with_dust = (
heirs.fixed_percent_lists_amount(timestamp_to_check, dust, reverse=True)
)
wallet_balance = 0
@@ -473,23 +492,16 @@ class Will:
)
def check_will(will, all_utxos, wallet, block_to_check, timestamp_to_check):
print("check will2")
Will.add_willtree(will)
print("willtree")
utxos_list = Will.utxos_strs(all_utxos)
print("utxo_list")
Will.check_invalidated(will, utxos_list, wallet)
print("check invalidate")
all_inputs = Will.get_all_inputs(will, only_valid=True)
print("get all inputs")
all_inputs_min_locktime = Will.get_all_inputs_min_locktime(all_inputs)
print("min_locktime")
Will.check_will_expired(
all_inputs_min_locktime, block_to_check, timestamp_to_check
)
print("check expired")
all_inputs = Will.get_all_inputs(will, only_valid=True)
@@ -507,9 +519,7 @@ class Will:
wallet=False,
callback_not_valid_tx=None,
):
print("is_will_valid")
Will.check_will(will, all_utxos, wallet, block_to_check, timestamp_to_check)
print("check will")
if heirs:
if not Will.check_willexecutors_and_heirs(
will,
@@ -553,17 +563,20 @@ class Will:
raise WillExpiredException(
f"Will Expired {wid[0][0]}: {locktime}<{timestamp_to_check}"
)
else:
from datetime import datetime
_logger.debug(f"Will Not Expired {wid[0][0]}: {datetime.fromtimestamp(locktime).isoformat()} > {datetime.fromtimestamp(timestamp_to_check).isoformat()}")
def check_all_input_spent_are_in_wallet():
_logger.info("check all input spent are in wallet or valid txs")
for inp, ws in all_inputs.items():
if not Util.in_utxo(inp, all_utxos):
for w in ws:
if w[1].get_status("VALID"):
prevout_id = w[2].prevout.txid.hex()
parentwill = will.get(prevout_id, False)
if not parentwill or not parentwill.get_status("VALID"):
w[1].set_status("INVALIDATED", True)
# def check_all_input_spent_are_in_wallet():
# _logger.info("check all input spent are in wallet or valid txs")
# for inp, ws in all_inputs.items():
# if not Util.in_utxo(inp, all_utxos):
# for w in ws:
# if w[1].get_status("VALID"):
# prevout_id = w[2].prevout.txid.hex()
# parentwill = will.get(prevout_id, False)
# if not parentwill or not parentwill.get_status("VALID"):
# w[1].set_status("INVALIDATED", True)
def only_valid_list(will):
out = {}
@@ -609,9 +622,9 @@ class Will:
heirs_found[wheir] = count + 1
else:
_logger.debug(
"heir not present transaction is not valid:", wid, w
f"heir not present transaction is not valid:{wheir} {wid}, {w}"
)
continue
if willexecutor := w.we:
count = willexecutors_found.get(willexecutor["url"], 0)
if Util.cmp_willexecutor(
@@ -623,19 +636,19 @@ class Will:
no_willexecutor += 1
count_heirs = 0
for h in heirs:
if Util.parse_locktime_string(heirs[h][2]) >= check_date:
count_heirs += 1
if not h in heirs_found:
if h not in heirs_found:
_logger.debug(f"heir: {h} not found")
raise HeirNotFoundException(h)
if not count_heirs:
raise NoHeirsException("there are not valid heirs")
if self_willexecutor and no_willexecutor == 0:
raise NoWillExecutorNotPresent("Backup tx")
for url, we in willexecutors.items():
if Willexecutors.is_selected(we):
if not url in willexecutors_found:
if url not in willexecutors_found:
_logger.debug(f"will-executor: {url} not fount")
raise WillExecutorNotPresent(url)
_logger.info("will is coherent with heirs and will-executors")
@@ -643,7 +656,6 @@ class Will:
class WillItem(Logger):
STATUS_DEFAULT = {
"ANTICIPATED": ["Anticipated", False],
"BROADCASTED": ["Broadcasted", False],
@@ -665,15 +677,15 @@ class WillItem(Logger):
}
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):
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)
if value:
if status in ["INVALIDATED", "REPLACED", "CONFIRMED", "PENDING"]:
@@ -787,9 +799,9 @@ class WillItem(Logger):
iw = inp[1]
self.set_anticipate(iw)
def check_willexecutor(self):
def set_check_willexecutor(self,resp):
try:
if resp := Willexecutors.check_transaction(self._id, self.we["url"]):
if resp :
if "tx" in resp and resp["tx"] == str(self.tx):
self.set_status("PUSHED")
self.set_status("CHECKED")
@@ -829,7 +841,12 @@ class WillItem(Logger):
class WillException(Exception):
pass
def __init__(self,msg="WillException"):
self.msg=msg
Exception.__init__(self)
def __str__(self):
return self.msg
class WillExpiredException(WillException):
@@ -866,8 +883,6 @@ class WillExecutorNotPresent(NotCompleteWillException):
class NoHeirsException(WillException):
pass
class AmountException(WillException):
pass

View File

@@ -1,33 +1,37 @@
import json
from datetime import datetime
from functools import partial
import time
from aiohttp import ClientResponse
from electrum import constants
from electrum.gui.qt.util import WaitingDialog
from electrum.i18n import _
from electrum.logging import get_logger
from electrum.network import Network
from .bal import BalPlugin
from .util import Util
DEFAULT_TIMEOUT = 5
_logger = get_logger(__name__)
chainname = constants.net.NET_NAME if constants.net.NET_NAME != "mainnet" else "bitcoin"
class Willexecutors:
def save(bal_plugin, willexecutors):
_logger.debug(f"save {willexecutors},{chainname}")
aw = bal_plugin.WILLEXECUTORS.get()
aw[constants.net.NET_NAME] = willexecutors
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
):
willexecutors = bal_plugin.WILLEXECUTORS.get()
willexecutors = willexecutors.get(constants.net.NET_NAME, {})
willexecutors = willexecutors.get(chainname, {})
to_del = []
for w in willexecutors:
if not isinstance(willexecutors[w], dict):
@@ -35,34 +39,38 @@ class Willexecutors:
continue
Willexecutors.initialize_willexecutor(willexecutors[w], w)
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]
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():
if not bal_url in willexecutors:
if bal_url not in willexecutors:
_logger.debug(f"force add {bal_url} willexecutor")
willexecutors[bal_url] = bal_executor
if update:
found = False
for url, we in willexecutors.items():
if Willexecutors.is_selected(we):
found = True
if found or force:
if bal_plugin.PING_WILLEXECUTORS.get() or force:
ping_willexecutors = True
if bal_plugin.ASK_PING_WILLEXECUTORS.get() and not force:
if bal_window:
ping_willexecutors = bal_window.window.question(
_(
"Contact willexecutors servers to update payment informations?"
)
)
# if update:
# found = False
# for url, we in willexecutors.items():
# if Willexecutors.is_selected(we):
# found = True
# if found or force:
# if bal_plugin.PING_WILLEXECUTORS.get() or force:
# ping_willexecutors = True
# if bal_plugin.ASK_PING_WILLEXECUTORS.get() and not force:
# if bal_window:
# ping_willexecutors = bal_window.window.question(
# _(
# "Contact willexecutors servers to update payment informations?"
# )
# )
if ping_willexecutors:
if task:
bal_window.ping_willexecutors(willexecutors, task)
else:
bal_window.ping_willexecutors_task(willexecutors)
# if ping_willexecutors:
# if task:
# bal_window.ping_willexecutors(willexecutors, task)
# else:
# bal_window.ping_willexecutors_task(willexecutors)
w_sorted = dict(
sorted(
willexecutors.items(), key=lambda w: w[1].get("sort", 0), reverse=True
@@ -73,11 +81,11 @@ class Willexecutors:
def is_selected(willexecutor, value=None):
if not willexecutor:
return False
if not value is None:
if value is not None:
willexecutor["selected"] = value
try:
return willexecutor["selected"]
except:
except Exception:
willexecutor["selected"] = False
return False
@@ -90,7 +98,7 @@ class Willexecutors:
if willexecutor := willitem.we:
url = willexecutor["url"]
if willexecutor and Willexecutors.is_selected(willexecutor):
if not url in willexecutors:
if url not in willexecutors:
willexecutor["txs"] = ""
willexecutor["txsids"] = []
willexecutor["broadcast_status"] = _("Waiting...")
@@ -100,31 +108,34 @@ class Willexecutors:
return willexecutors
def only_selected_list(willexecutors):
out = {}
for url, v in willexecutors.items():
if Willexecutors.is_selected(willexecutor):
out[url] = v
# def only_selected_list(willexecutors):
# out = {}
# for url, v in willexecutors.items():
# if Willexecutors.is_selected(url):
# 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 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):
def send_request(
method, url, data=None, *, timeout=10, handle_response=None, count_reply=0
):
network = Network.get_instance()
if not network:
raise ErrorConnectingServer("You are offline.")
raise Exception("You are offline.")
_logger.debug(f"<-- {method} {url} {data}")
headers = {}
headers["user-agent"] = f"BalPlugin v:{BalPlugin.version()}"
headers["user-agent"] = f"BalPlugin v:{BalPlugin.__version__}"
headers["Content-Type"] = "text/plain"
if not handle_response:
handle_response = Willexecutors.handle_response
try:
if method == "get":
response = Network.send_http_on_proxy(
@@ -132,7 +143,7 @@ class Willexecutors:
url,
params=data,
headers=headers,
on_finish=Willexecutors.handle_response,
on_finish=handle_response,
timeout=timeout,
)
elif method == "post":
@@ -141,26 +152,47 @@ class Willexecutors:
url,
body=data,
headers=headers,
on_finish=Willexecutors.handle_response,
on_finish=handle_response,
timeout=timeout,
)
else:
raise Exception(f"unexpected {method=!r}")
except TimeoutError:
if count_reply < 10:
_logger.debug(f"timeout({count_reply}) error: retry in 3 sec...")
time.sleep(3)
return Willexecutors.send_request(
method,
url,
data,
timeout=timeout,
handle_response=handle_response,
count_reply=count_reply + 1,
)
else:
_logger.debug(f"Too many timeouts: {count_reply}")
except Exception as e:
_logger.error(f"exception sending request {e}")
raise e
else:
_logger.debug(f"--> {response}")
return response
def get_we_url_from_response(resp):
url_slices = str(resp.url).split("/")
if len(url_slices) > 2:
url_slices = url_slices[:-2]
return "/".join(url_slices)
async def handle_response(resp: ClientResponse):
r = await resp.text()
try:
r = json.loads(r)
r["status"] = resp.status
r["selected"] = Willexecutors.is_selected(willexecutor)
r["url"] = url
except:
# url = Willexecutors.get_we_url_from_response(resp)
# r["url"]= url
# r["status"]=resp.status
except Exception as e:
_logger.debug(f"error handling response:{e}")
pass
return r
@@ -170,11 +202,10 @@ class Willexecutors:
def push_transactions_to_willexecutor(willexecutor):
out = True
try:
_logger.debug(f"willexecutor['txs']")
_logger.debug(f"{willexecutor['url']}: {willexecutor['txs']}")
if w := Willexecutors.send_request(
"post",
willexecutor["url"] + "/" + constants.net.NET_NAME + "/pushtxs",
willexecutor["url"] + "/" + chainname + "/pushtxs",
data=willexecutor["txs"].encode("ascii"),
):
willexecutor["broadcast_status"] = _("Success")
@@ -202,18 +233,14 @@ class Willexecutors:
try:
_logger.info("GETINFO_WILLEXECUTOR")
_logger.debug(url)
netname = "bitcoin"
if constants.net.NET_NAME != "mainnet":
netname = constants.net.NET_NAME
w = Willexecutors.send_request("get", url + "/" + netname + "/info")
w = Willexecutors.send_request("get", url + "/" + chainname + "/info")
if isinstance(w, dict):
willexecutor["url"] = url
willexecutor["status"] = w["status"]
willexecutor["status"] = 200
willexecutor["base_fee"] = w["base_fee"]
willexecutor["address"] = w["address"]
if not willexecutor["info"]:
willexecutor["info"] = w["info"]
_logger.debug(f"response_data {w['address']}")
_logger.debug(f"response_data {w}")
except Exception as e:
_logger.error(f"error {e} contacting {url}: {w}")
willexecutor["status"] = "KO"
@@ -221,30 +248,39 @@ class Willexecutors:
willexecutor["last_update"] = datetime.now().timestamp()
return willexecutor
def initialize_willexecutor(willexecutor, url, status=None, selected=None):
def initialize_willexecutor(willexecutor, url, status=None, old_willexecutor={}):
willexecutor["url"] = url
if not status is None:
if status is not None:
willexecutor["status"] = status
willexecutor["selected"] = Willexecutors.is_selected(willexecutor, selected)
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(bal_plugin):
def download_list(old_willexecutors):
try:
l = Willexecutors.send_request(
"get", "https://welist.bitcoin-after.life/data/bitcoin?page=0&limit=100"
willexecutors = Willexecutors.send_request(
"get",
f"https://welist.bitcoin-after.life/data/{chainname}?page=0&limit=100",
)
# del willexecutors["status"]
for w in willexecutors:
if w not in ("status", "url"):
Willexecutors.initialize_willexecutor(
willexecutors[w], w, None, old_willexecutors.get(w,{})
)
del l["status"]
for w in l:
willexecutor = l[w]
Willexecutors.initialize_willexecutor(willexecutor, w, "New", False)
# bal_plugin.WILLEXECUTORS.set(l)
# bal_plugin.config.set_key(bal_plugin.WILLEXECUTORS,l,save=True)
return l
return willexecutors
except Exception as e:
_logger.error(f"Failed to download willexecutors list: {e}")
return {}
def get_willexecutors_list_from_json(bal_plugin):
def get_willexecutors_list_from_json():
try:
with open("willexecutors.json") as f:
willexecutors = json.load(f)
@@ -252,7 +288,7 @@ class Willexecutors:
willexecutor = willexecutors[w]
Willexecutors.initialize_willexecutor(willexecutor, w, "New", False)
# bal_plugin.WILLEXECUTORS.set(willexecutors)
return h
return willexecutors
except Exception as e:
_logger.error(f"error opening willexecutors json: {e}")
@@ -269,14 +305,53 @@ class Willexecutors:
_logger.error(f"error contacting {url} for checking txs {e}")
raise e
def compute_id(willexecutor):
return "{}-{}".format(willexecutor.get("url"), willexecutor.get("chain"))
class WillExecutor:
def __init__(self, url, base_fee, chain, info, version):
def __init__(
self,
url,
base_fee,
chain,
info,
version,
status,
is_selected=False,
promo_code="",
):
self.url = url
self.base_fee = base_fee
self.chain = chain
self.info = info
self.version = version
self.status = status
self.promo_code = promo_code
self.is_selected = is_selected
self.id = self.compute_id()
def from_dict(d):
we = WillExecutor(d["url"], d["base_fee"], d["chain"], d["info"], d["version"])
return WillExecutor(
url=d.get("url", "http://localhost:8000"),
base_fee=d.get("base_fee", 1000),
chain=d.get("chain", chainname),
info=d.get("info", ""),
version=d.get("version", 0),
status=d.get("status", "Ko"),
is_selected=d.get("is_selected", "False"),
promo_code=d.get("promo_code", ""),
)
def to_dict(self):
return {
"url": self.url,
"base_fee": self.base_fee,
"chain": self.chain,
"info": self.info,
"version": self.version,
"promo_code": self.promo_code,
}
def compute_id(self):
return f"{self.url}-{self.chain}"