Source code for saline_sdk.account

"""
Account management for Saline SDK.

This module provides comprehensive account management for the Saline protocol, 
including both individual subaccounts (key pairs) and multi-account management.
"""

from typing import Optional, Dict, Union
from mnemonic import Mnemonic
from .crypto import (
    derive_master_SK,
    derive_key_from_path
)
from .crypto.bls import BLS

[docs] class Subaccount: """ Individual Saline subaccount representing a single key pair. Handles cryptographic operations and always derived from an Account. """
[docs] def __init__(self, private_key_bytes: bytes, public_key_bytes: Optional[bytes] = None, path: Optional[str] = None, label: Optional[str] = None): """ Initialize a subaccount with private key bytes. Args: private_key_bytes: The private key in bytes public_key_bytes: Optional public key bytes (will be derived if not provided) path: Optional derivation path label: Optional subaccount label """ self.private_key_bytes = private_key_bytes self._private_key = BLS.PrivateKey.from_bytes(private_key_bytes) if public_key_bytes is None: self._public_key_bytes = BLS.sk_to_pk(private_key_bytes) else: self._public_key_bytes = public_key_bytes self.label = label self.path = path
@property def public_key(self) -> str: """Get the public key as hex string.""" return self._public_key_bytes.hex()
[docs] def sign(self, message: bytes) -> bytes: """Sign a message with this subaccount's private key.""" return BLS.sign(self.private_key_bytes, message)
[docs] def __str__(self) -> str: """String representation of the subaccount.""" if self.label: return f"Subaccount '{self.label}' ({self.public_key[:8]}...{self.public_key[-8:]})" else: return f"Subaccount {self.public_key[:8]}...{self.public_key[-8:]}"
[docs] class Account: """ High-level account management. Acts as a container for subaccounts and provides a user-friendly interface for managing keys and performing wallet operations. """
[docs] @classmethod def create(cls) -> 'Account': """Create a new account with a random mnemonic.""" mnemo = Mnemonic("english") mnemonic = mnemo.generate(128) # 12 words return cls.from_mnemonic(mnemonic)
[docs] @classmethod def from_mnemonic(cls, mnemonic: str, base_path: str = "m/12381/997") -> 'Account': """ Create an account from a mnemonic phrase. Args: mnemonic: 24-word mnemonic phrase base_path: Optional base path for derivation (default: m/12381/997) Returns: Account instance Raises: ValueError: If the mnemonic is invalid or the base_path is invalid """ mnemo = Mnemonic("english") if not mnemo.check(mnemonic): raise ValueError("Invalid mnemonic phrase") # Validate the base path if not base_path.startswith("m/"): raise ValueError("Base path must start with 'm/'") # Simple validation: path must have at least two components: m/coin_type/account path_components = base_path.split("/") if len(path_components) < 3: raise ValueError("Base path must have at least coin type and account components (m/coin_type/account)") # Validate that each component is numeric except for the first 'm' for component in path_components[1:]: if not component.isdigit(): raise ValueError(f"Path component '{component}' is not numeric") account = cls() account._mnemonic = mnemonic account._seed = mnemo.to_seed(mnemonic) account.base_path = base_path return account
[docs] def __init__(self): """Initialize an empty account.""" self._subaccounts: Dict[str, Subaccount] = {} self._mnemonic: Optional[str] = None self._seed: Optional[bytes] = None self.default_subaccount: Optional[str] = None self._next_index = 0 self.base_path = "m/12381/997"
[docs] def create_subaccount( self, label: str, path: Optional[str] = None ) -> Subaccount: """ Create a new subaccount. Args: label: Subaccount label path: Optional derivation path (default: m/12381/997/0/0/{next_index}) Returns: Created subaccount Raises: ValueError: If account not initialized or name exists """ if self._seed is None: raise ValueError("Account not initialized with mnemonic") if label in self._subaccounts: raise ValueError(f"Subaccount '{label}' already exists") # Generate default path if none provided if path is None: idx = self._next_index path = f"{self.base_path}/0/0/{idx}" self._next_index += 1 # Create subaccount private_key = derive_key_from_path(self._seed, path) subaccount = Subaccount(private_key, path=path, label=label) # Store subaccount self._subaccounts[label] = subaccount # Set as default if first subaccount if self.default_subaccount is None: self.default_subaccount = label return subaccount
[docs] def get_subaccount(self, label: str) -> Subaccount: """ Get a subaccount by label. Args: label: Subaccount label Returns: Subaccount instance Raises: KeyError: If subaccount not found """ if label not in self._subaccounts: raise KeyError(f"Subaccount '{label}' not found") return self._subaccounts[label]
[docs] def list_subaccounts(self) -> Dict[str, str]: """ Get a list of all subaccounts. Returns: Dict mapping subaccount names to public keys """ return {label: acc.public_key for label, acc in self._subaccounts.items()}
[docs] def set_default_subaccount(self, label: str) -> None: """ Set the default subaccount. Args: label: Subaccount label Raises: KeyError: If subaccount not found """ if label not in self._subaccounts: raise KeyError(f"Subaccount '{label}' not found") self.default_subaccount = label
[docs] def transfer( self, to: str, amount: Union[int, float], currency: str = "USDC", from_subaccount: Optional[str] = None ) -> bytes: """ Create a transfer transaction. Args: to: Recipient address (public key) amount: Amount to transfer currency: Currency to transfer (default: USDC) from_subaccount: Source subaccount name (uses default if None) Returns: Signed transaction Raises: ValueError: If no source subaccount specified or found """ # Get source subaccount if from_subaccount is None: if self.default_subaccount is None: raise ValueError("No source subaccount specified and no default subaccount set") from_subaccount = self.default_subaccount subaccount = self.get_subaccount(from_subaccount) from .transaction.instructions import transfer from .transaction.tx import Transaction transfer_instruction = transfer( sender=subaccount.public_key, recipient=to, token=currency, amount=int(amount) ) tx = Transaction(instructions=[transfer_instruction]) tx.set_signer(subaccount.public_key) tx.add_intent(subaccount.public_key) tx.sign(subaccount) return tx.serialize_for_network()
[docs] def __getitem__(self, name: str) -> Subaccount: """Dict-like access to subaccounts.""" return self.get_subaccount(name)
[docs] def __contains__(self, name: str) -> bool: """Check if subaccount exists.""" return name in self._subaccounts
[docs] def __iter__(self): """Iterate over subaccount names.""" return iter(self._subaccounts)
[docs] def __len__(self) -> int: """Number of subaccounts.""" return len(self._subaccounts)
[docs] def __str__(self) -> str: """String representation.""" subaccounts = [f"{name}: {acc.public_key}" for name, acc in self._subaccounts.items()] default = f" (default: {self.default_subaccount})" if self.default_subaccount else "" return f"Account with {len(subaccounts)} subaccounts{default}:\n" + "\n".join(subaccounts)