Module jmwallet.wallet.bond_registry
Fidelity bond registry for persistent storage of bond metadata.
This module provides storage and retrieval of fidelity bond information, including addresses, locktimes, witness scripts, and UTXO tracking.
Functions
def create_bond_info(address: str,
locktime: int,
index: int,
path: str,
pubkey_hex: str,
witness_script: bytes,
network: str) ‑> FidelityBondInfo-
Expand source code
def create_bond_info( address: str, locktime: int, index: int, path: str, pubkey_hex: str, witness_script: bytes, network: str, ) -> FidelityBondInfo: """ Create a FidelityBondInfo instance. Args: address: The P2WSH address locktime: Unix timestamp locktime index: Derivation index path: Full derivation path pubkey_hex: Public key as hex witness_script: The witness script bytes network: Network name Returns: FidelityBondInfo instance """ locktime_dt = datetime.fromtimestamp(locktime) return FidelityBondInfo( address=address, locktime=locktime, locktime_human=locktime_dt.strftime("%Y-%m-%d %H:%M:%S"), index=index, path=path, pubkey=pubkey_hex, witness_script_hex=witness_script.hex(), network=network, created_at=datetime.now().isoformat(), )Create a FidelityBondInfo instance.
Args
address- The P2WSH address
locktime- Unix timestamp locktime
index- Derivation index
path- Full derivation path
pubkey_hex- Public key as hex
witness_script- The witness script bytes
network- Network name
Returns
FidelityBondInfo instance
def get_active_locktimes(data_dir: Path) ‑> list[int]-
Expand source code
def get_active_locktimes(data_dir: Path) -> list[int]: """ Get all locktimes from the bond registry that have funded, active bonds. This is useful for the maker bot to automatically discover which locktimes to scan for when syncing fidelity bonds, without requiring the user to manually specify --fidelity-bond-locktime. Args: data_dir: Data directory path Returns: List of unique locktimes (Unix timestamps) for active bonds """ registry = load_registry(data_dir) active_bonds = registry.get_active_bonds() # Get unique locktimes locktimes = list({bond.locktime for bond in active_bonds}) return sorted(locktimes)Get all locktimes from the bond registry that have funded, active bonds.
This is useful for the maker bot to automatically discover which locktimes to scan for when syncing fidelity bonds, without requiring the user to manually specify –fidelity-bond-locktime.
Args
data_dir- Data directory path
Returns
List of unique locktimes (Unix timestamps) for active bonds
def get_all_locktimes(data_dir: Path) ‑> list[int]-
Expand source code
def get_all_locktimes(data_dir: Path) -> list[int]: """ Get all locktimes from the bond registry (funded or not). This includes all bonds in the registry to allow scanning for UTXOs that may have been funded since the last sync. Args: data_dir: Data directory path Returns: List of unique locktimes (Unix timestamps) for all bonds """ registry = load_registry(data_dir) # Get unique locktimes from ALL bonds (not just funded ones) locktimes = list({bond.locktime for bond in registry.bonds}) return sorted(locktimes)Get all locktimes from the bond registry (funded or not).
This includes all bonds in the registry to allow scanning for UTXOs that may have been funded since the last sync.
Args
data_dir- Data directory path
Returns
List of unique locktimes (Unix timestamps) for all bonds
def get_registry_path(data_dir: Path) ‑> pathlib.Path-
Expand source code
def get_registry_path(data_dir: Path) -> Path: """Get the path to the bond registry file.""" return data_dir / "fidelity_bonds.json"Get the path to the bond registry file.
def load_registry(data_dir: Path) ‑> BondRegistry-
Expand source code
def load_registry(data_dir: Path) -> BondRegistry: """ Load the bond registry from disk. Args: data_dir: Data directory path Returns: BondRegistry instance (empty if file doesn't exist) """ registry_path = get_registry_path(data_dir) if not registry_path.exists(): return BondRegistry() try: data = json.loads(registry_path.read_text()) return BondRegistry.model_validate(data) except Exception as e: logger.error(f"Failed to load bond registry: {e}") # Return empty registry on error, but don't overwrite the file return BondRegistry()Load the bond registry from disk.
Args
data_dir- Data directory path
Returns
BondRegistry instance (empty if file doesn't exist)
def save_registry(registry: BondRegistry,
data_dir: Path) ‑> None-
Expand source code
def save_registry(registry: BondRegistry, data_dir: Path) -> None: """ Save the bond registry to disk. Args: registry: BondRegistry instance data_dir: Data directory path """ registry_path = get_registry_path(data_dir) registry_path.parent.mkdir(parents=True, exist_ok=True) try: registry_path.write_text(registry.model_dump_json(indent=2)) logger.debug(f"Saved bond registry to {registry_path}") except Exception as e: logger.error(f"Failed to save bond registry: {e}") raiseSave the bond registry to disk.
Args
registry- BondRegistry instance
data_dir- Data directory path
Classes
class BondRegistry (**data: Any)-
Expand source code
class BondRegistry(BaseModel): """Registry of all fidelity bonds for a wallet.""" version: int = 1 bonds: list[FidelityBondInfo] = [] def add_bond(self, bond: FidelityBondInfo) -> None: """Add a new bond to the registry.""" # Check for duplicate address for existing in self.bonds: if existing.address == bond.address: logger.warning(f"Bond with address {bond.address} already exists, updating") self.bonds.remove(existing) break self.bonds.append(bond) def get_bond_by_address(self, address: str) -> FidelityBondInfo | None: """Get a bond by its address.""" for bond in self.bonds: if bond.address == address: return bond return None def get_bond_by_index(self, index: int, locktime: int) -> FidelityBondInfo | None: """Get a bond by its index and locktime.""" for bond in self.bonds: if bond.index == index and bond.locktime == locktime: return bond return None def get_funded_bonds(self) -> list[FidelityBondInfo]: """Get all funded bonds.""" return [b for b in self.bonds if b.is_funded] def get_active_bonds(self) -> list[FidelityBondInfo]: """Get all funded bonds that are not yet expired.""" return [b for b in self.bonds if b.is_funded and not b.is_expired] def get_best_bond(self) -> FidelityBondInfo | None: """ Get the best bond for advertising. Selection criteria (in order): 1. Must be funded 2. Must not be expired 3. Highest value wins 4. If tied, longest locktime remaining wins """ active = self.get_active_bonds() if not active: return None # Sort by value (descending), then by time_until_unlock (descending) active.sort(key=lambda b: (b.value or 0, b.time_until_unlock), reverse=True) return active[0] def update_utxo_info( self, address: str, txid: str, vout: int, value: int, confirmations: int, ) -> bool: """Update UTXO information for a bond.""" bond = self.get_bond_by_address(address) if bond: bond.txid = txid bond.vout = vout bond.value = value bond.confirmations = confirmations return True return FalseRegistry of all fidelity bonds for a wallet.
Create a new model by parsing and validating input data from keyword arguments.
Raises [
ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.selfis explicitly positional-only to allowselfas a field name.Ancestors
- pydantic.main.BaseModel
Class variables
var bonds : list[FidelityBondInfo]-
The type of the None singleton.
var model_config-
The type of the None singleton.
var version : int-
The type of the None singleton.
Methods
def add_bond(self,
bond: FidelityBondInfo) ‑> None-
Expand source code
def add_bond(self, bond: FidelityBondInfo) -> None: """Add a new bond to the registry.""" # Check for duplicate address for existing in self.bonds: if existing.address == bond.address: logger.warning(f"Bond with address {bond.address} already exists, updating") self.bonds.remove(existing) break self.bonds.append(bond)Add a new bond to the registry.
def get_active_bonds(self) ‑> list[FidelityBondInfo]-
Expand source code
def get_active_bonds(self) -> list[FidelityBondInfo]: """Get all funded bonds that are not yet expired.""" return [b for b in self.bonds if b.is_funded and not b.is_expired]Get all funded bonds that are not yet expired.
def get_best_bond(self) ‑> FidelityBondInfo | None-
Expand source code
def get_best_bond(self) -> FidelityBondInfo | None: """ Get the best bond for advertising. Selection criteria (in order): 1. Must be funded 2. Must not be expired 3. Highest value wins 4. If tied, longest locktime remaining wins """ active = self.get_active_bonds() if not active: return None # Sort by value (descending), then by time_until_unlock (descending) active.sort(key=lambda b: (b.value or 0, b.time_until_unlock), reverse=True) return active[0]Get the best bond for advertising.
Selection criteria (in order): 1. Must be funded 2. Must not be expired 3. Highest value wins 4. If tied, longest locktime remaining wins
def get_bond_by_address(self, address: str) ‑> FidelityBondInfo | None-
Expand source code
def get_bond_by_address(self, address: str) -> FidelityBondInfo | None: """Get a bond by its address.""" for bond in self.bonds: if bond.address == address: return bond return NoneGet a bond by its address.
def get_bond_by_index(self, index: int, locktime: int) ‑> FidelityBondInfo | None-
Expand source code
def get_bond_by_index(self, index: int, locktime: int) -> FidelityBondInfo | None: """Get a bond by its index and locktime.""" for bond in self.bonds: if bond.index == index and bond.locktime == locktime: return bond return NoneGet a bond by its index and locktime.
def get_funded_bonds(self) ‑> list[FidelityBondInfo]-
Expand source code
def get_funded_bonds(self) -> list[FidelityBondInfo]: """Get all funded bonds.""" return [b for b in self.bonds if b.is_funded]Get all funded bonds.
def update_utxo_info(self, address: str, txid: str, vout: int, value: int, confirmations: int) ‑> bool-
Expand source code
def update_utxo_info( self, address: str, txid: str, vout: int, value: int, confirmations: int, ) -> bool: """Update UTXO information for a bond.""" bond = self.get_bond_by_address(address) if bond: bond.txid = txid bond.vout = vout bond.value = value bond.confirmations = confirmations return True return FalseUpdate UTXO information for a bond.
class FidelityBondInfo (**data: Any)-
Expand source code
class FidelityBondInfo(BaseModel): """Information about a single fidelity bond.""" address: str locktime: int locktime_human: str index: int path: str pubkey: str witness_script_hex: str network: str created_at: str # UTXO info (populated when bond is funded) txid: str | None = None vout: int | None = None value: int | None = None # in satoshis confirmations: int | None = None @property def is_funded(self) -> bool: """Check if this bond has been funded.""" return self.txid is not None and self.value is not None and self.value > 0 @property def is_expired(self) -> bool: """Check if the locktime has passed.""" import time return time.time() >= self.locktime @property def time_until_unlock(self) -> int: """Seconds until the bond can be unlocked. Returns 0 if already expired.""" import time remaining = self.locktime - int(time.time()) return max(0, remaining)Information about a single fidelity bond.
Create a new model by parsing and validating input data from keyword arguments.
Raises [
ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.selfis explicitly positional-only to allowselfas a field name.Ancestors
- pydantic.main.BaseModel
Class variables
var address : str-
The type of the None singleton.
var confirmations : int | None-
The type of the None singleton.
var created_at : str-
The type of the None singleton.
var index : int-
The type of the None singleton.
var locktime : int-
The type of the None singleton.
var locktime_human : str-
The type of the None singleton.
var model_config-
The type of the None singleton.
var network : str-
The type of the None singleton.
var path : str-
The type of the None singleton.
var pubkey : str-
The type of the None singleton.
var txid : str | None-
The type of the None singleton.
var value : int | None-
The type of the None singleton.
var vout : int | None-
The type of the None singleton.
var witness_script_hex : str-
The type of the None singleton.
Instance variables
prop is_expired : bool-
Expand source code
@property def is_expired(self) -> bool: """Check if the locktime has passed.""" import time return time.time() >= self.locktimeCheck if the locktime has passed.
prop is_funded : bool-
Expand source code
@property def is_funded(self) -> bool: """Check if this bond has been funded.""" return self.txid is not None and self.value is not None and self.value > 0Check if this bond has been funded.
prop time_until_unlock : int-
Expand source code
@property def time_until_unlock(self) -> int: """Seconds until the bond can be unlocked. Returns 0 if already expired.""" import time remaining = self.locktime - int(time.time()) return max(0, remaining)Seconds until the bond can be unlocked. Returns 0 if already expired.