"""Shared interfaces and simple data models used across the public API."""
from collections.abc import Callable
from enum import Flag, StrEnum
from typing import TYPE_CHECKING, Any
from bitcash.network.meta import Unspent
if TYPE_CHECKING:
import cashscript_py
from cashscript_py.helpers.argument_encoding import FunctionArgument
[docs]
class SignatureAlgorithm(Flag):
"""Supported signature algorithms."""
ECDSA = 0x00
SCHNORR = 0x01
[docs]
class HashType(Flag):
"""Supported SIGHASH flags."""
SIGHASH_ALL = 0x01
SIGHASH_NONE = 0x02
SIGHASH_SINGLE = 0x03
SIGHASH_UTXOS = 0x20
SIGHASH_FORKID = 0x40
SIGHASH_ANYONECANPAY = 0x80
[docs]
class NftCapability(StrEnum):
"""NFT CashTokens capabilities."""
NONE = "none"
MUTABLE = "mutable"
MINTING = "minting"
[docs]
class TokenDetails:
"""CashToken details: fungible amount and optional NFT."""
[docs]
class Nft:
"""Nested NFT metadata."""
def __init__(self, capability: NftCapability, commitment: str | None):
"""Create an NFT description with capability and optional commitment."""
self.capability = capability
self.commitment = "" if commitment is None else commitment
def __repr__(self) -> str:
"""Return a concise string representation."""
return f"Nft(capability={self.capability}, commitment={self.commitment})"
def __init__(self, amount: int | None, category: str, nft: "TokenDetails.Nft | None"):
"""Create token details with optional NFT (amount defaults to 0)."""
self.amount = 0 if amount is None else amount
self.category = category
self.nft = nft
def __repr__(self) -> str:
"""Return a concise string representation."""
nft_str = str(self.nft) if self.nft is not None else "None"
return f"TokenDetails(amount={self.amount}, category={self.category}, nft={nft_str})"
[docs]
class Output:
"""Transaction output description.
Attributes:
to: Recipient locking bytecode (bytes) or CashAddress (str).
amount: BCH amount in satoshis.
token: Optional CashToken details.
"""
def __init__(self, to: bytes | str, amount: int, token: TokenDetails | None = None):
"""Create a new Output.
Args:
to: Locking script (bytes) or CashAddress (str).
amount: Amount in satoshis.
token: Optional CashToken details.
"""
self.to = to
self.amount = amount
self.token = token
def __repr__(self) -> str:
"""Return a concise string representation."""
token_str = str(self.token) if self.token is not None else "None"
to = self.to if isinstance(self.to, str) else self.to.hex()
return f"Output(to={to}, amount={self.amount}, token={token_str})"
[docs]
class Transaction:
"""Simple transaction template used for signing and serialization."""
def __init__(self, inputs: list["UnlockableUtxo"], locktime: int, outputs: list[Output], version: int):
"""Create a transaction template.
Args:
inputs: Unlockable inputs to include.
locktime: Transaction locktime.
outputs: Outputs to include.
version: Transaction version (typically 2).
"""
self.inputs = inputs
self.locktime = locktime
self.outputs = outputs
self.version = version
[docs]
class TransactionDetails(Transaction):
"""Transaction including txid and hex representation."""
txid: str
hex: str
[docs]
class Unlocker:
"""Unlocker providing locking/unlocking bytecode generators."""
def __init__(
self,
generate_locking_bytecode: Callable[[], bytes],
generate_unlocking_bytecode: Callable[[Transaction, list[Output], int], bytes],
):
"""Create an Unlocker.
Args:
generate_locking_bytecode: Function that returns locking bytecode.
generate_unlocking_bytecode: Function that returns unlocking bytecode for an input.
"""
self.generate_locking_bytecode = generate_locking_bytecode
self.generate_unlocking_bytecode = generate_unlocking_bytecode
[docs]
class ContractUnlocker(Unlocker):
"""Unlocker associated with a Contract ABI function and its parameters."""
def __init__(
self,
generate_locking_bytecode: Callable[[], bytes],
generate_unlocking_bytecode: Callable[[Transaction, list[Output], int], bytes],
contract: "cashscript_py.Contract",
params: list["FunctionArgument"],
abi_function: dict[str, Any],
):
"""Create a ContractUnlocker.
Args:
generate_locking_bytecode: Locking bytecode generator.
generate_unlocking_bytecode: Unlocking bytecode generator.
contract: Contract instance that created this unlocker.
params: ABI parameters used to construct the unlocker.
abi_function: ABI function metadata.
"""
super().__init__(generate_locking_bytecode, generate_unlocking_bytecode)
self.contract = contract
self.params = params
self.abi_function = abi_function
[docs]
class Utxo:
"""Spendable output reference with optional CashToken details."""
def __init__(self, txid: str, vout: int, satoshis: int, token: TokenDetails | None = None):
"""Create a UTXO reference.
Args:
txid: Transaction ID (hex).
vout: Output index.
satoshis: Output amount (satoshis).
token: Optional CashToken details.
"""
self.txid = txid
self.vout = vout
self.satoshis = satoshis
self.token = token
def __repr__(self) -> str:
"""Return a concise string representation."""
token_str = str(self.token) if self.token is not None else "None"
return f"Utxo(txid={self.txid}, vout={self.vout}, satoshis={self.satoshis}, token={token_str})"
[docs]
@classmethod
def from_unspent(cls, unspent: Unspent) -> "Utxo":
"""Convert a bitcash Unspent to a Utxo, mapping CashToken/NFT details.
Args:
unspent: bitcash Unspent instance.
Returns:
Utxo: Converted UTXO.
"""
token_details = nft = None
if getattr(unspent, "has_cashtoken", False):
if getattr(unspent, "has_nft", False):
nft_capability = NftCapability(unspent.nft_capability)
nft = TokenDetails.Nft(
nft_capability,
unspent.nft_commitment.hex() if unspent.nft_commitment else "",
)
token_details = TokenDetails(unspent.token_amount, unspent.category_id, nft)
return cls(unspent.txid, unspent.txindex, unspent.amount, token_details)
[docs]
class UnlockableUtxo(Utxo):
"""Utxo extended with an unlocker and optional input options."""
def __init__(
self,
txid: str,
vout: int,
satoshis: int,
token: TokenDetails | None,
unlocker: Unlocker,
sequence: int | None,
):
"""Create an UnlockableUtxo from UTXO details and an Unlocker."""
super().__init__(txid, vout, satoshis, token)
self.unlocker: Unlocker = unlocker
self.sequence: int | None = sequence
self.unlocking_bytecode = b""
[docs]
@classmethod
def from_utxo(cls, utxo: Utxo, unlocker: Unlocker, sequence: int | None) -> "UnlockableUtxo":
"""Construct an UnlockableUtxo from a plain Utxo and an Unlocker."""
return cls(utxo.txid, utxo.vout, utxo.satoshis, utxo.token, unlocker, sequence)