Module maker.offers

Offer management for makers.

Creates and manages liquidity offers based on wallet balance and configuration. Supports multiple simultaneous offers with different fee structures (relative/absolute).

Classes

class OfferManager (wallet: WalletService, config: MakerConfig, maker_nick: str)
Expand source code
class OfferManager:
    """
    Creates and manages offers for the maker bot.

    Supports creating multiple offers simultaneously, each with a unique offer ID.
    This allows makers to advertise both relative and absolute fee offers at the same time.
    """

    def __init__(self, wallet: WalletService, config: MakerConfig, maker_nick: str):
        self.wallet = wallet
        self.config = config
        self.maker_nick = maker_nick

    async def create_offers(self) -> list[Offer]:
        """
        Create offers based on wallet balance and configuration.

        Logic:
        1. Find mixdepth with maximum balance
        2. Calculate available amount (balance - dust - max_txfee)
        3. Create offer(s) with configured fee structure(s)
        4. Attach fidelity bond value if available

        Returns:
            List of offers. Each offer gets a unique oid (0, 1, 2, ...).
        """
        try:
            balances = {}
            for mixdepth in range(self.wallet.mixdepth_count):
                balance = await self.wallet.get_balance(mixdepth)
                balances[mixdepth] = balance

            available_mixdepths = {md: bal for md, bal in balances.items() if bal > 0}

            if not available_mixdepths:
                logger.warning("No mixdepth with positive balance")
                return []

            max_mixdepth = max(available_mixdepths, key=lambda md: available_mixdepths[md])
            max_balance = available_mixdepths[max_mixdepth]

            # Get effective offer configurations
            offer_configs = self.config.get_effective_offer_configs()

            # Get fidelity bond value if available (shared across all offers)
            fidelity_bond_value = 0
            bond = await get_best_fidelity_bond(self.wallet)
            if bond:
                fidelity_bond_value = bond.bond_value
                logger.info(
                    f"Fidelity bond found: {bond.txid}:{bond.vout} "
                    f"value={bond.value} sats, bond_value={bond.bond_value}"
                )

            # Create an offer for each configuration
            offers: list[Offer] = []
            for offer_id, offer_cfg in enumerate(offer_configs):
                offer = self._create_single_offer(
                    offer_id=offer_id,
                    offer_cfg=offer_cfg,
                    max_balance=max_balance,
                    fidelity_bond_value=fidelity_bond_value,
                )
                if offer:
                    offers.append(offer)

            if not offers:
                logger.warning("No valid offers could be created")
                return []

            logger.info(f"Created {len(offers)} offer(s)")
            return offers

        except Exception as e:
            logger.error(f"Failed to create offers: {e}")
            return []

    def _create_single_offer(
        self,
        offer_id: int,
        offer_cfg: OfferConfig,
        max_balance: int,
        fidelity_bond_value: int,
    ) -> Offer | None:
        """
        Create a single offer from configuration.

        Args:
            offer_id: Unique offer ID (0, 1, 2, ...)
            offer_cfg: Offer configuration
            max_balance: Maximum available balance
            fidelity_bond_value: Fidelity bond value to attach

        Returns:
            Offer object or None if creation failed
        """
        try:
            # Reserve dust threshold + tx fee contribution
            max_available = max_balance - max(
                self.config.dust_threshold, offer_cfg.tx_fee_contribution
            )

            if max_available <= offer_cfg.min_size:
                logger.warning(
                    f"Offer {offer_id}: Insufficient balance: "
                    f"{max_available} <= {offer_cfg.min_size}"
                )
                return None

            # Calculate min_size based on offer type
            if offer_cfg.offer_type in (OfferType.SW0_RELATIVE, OfferType.SWA_RELATIVE):
                cjfee = offer_cfg.cj_fee_relative

                # Validate cj_fee_relative to prevent division by zero
                cj_fee_float = float(offer_cfg.cj_fee_relative)
                if cj_fee_float <= 0:
                    logger.error(
                        f"Offer {offer_id}: Invalid cj_fee_relative: {offer_cfg.cj_fee_relative}. "
                        "Must be > 0 for relative offer types."
                    )
                    return None

                # Calculate minimum size for profitability
                min_size_for_profit = int(1.5 * offer_cfg.tx_fee_contribution / cj_fee_float)
                min_size = max(min_size_for_profit, offer_cfg.min_size)
            else:
                cjfee = str(offer_cfg.cj_fee_absolute)
                min_size = offer_cfg.min_size

            offer = Offer(
                counterparty=self.maker_nick,
                oid=offer_id,
                ordertype=offer_cfg.offer_type,
                minsize=min_size,
                maxsize=max_available,
                txfee=offer_cfg.tx_fee_contribution,
                cjfee=cjfee,
                fidelity_bond_value=fidelity_bond_value,
            )

            logger.info(
                f"Created offer {offer_id}: type={offer.ordertype.value}, "
                f"size={min_size}-{max_available}, "
                f"cjfee={cjfee}, txfee={offer_cfg.tx_fee_contribution}, "
                f"bond_value={fidelity_bond_value}"
            )

            return offer

        except Exception as e:
            logger.error(f"Failed to create offer {offer_id}: {e}")
            return None

    def validate_offer_fill(self, offer: Offer, amount: int) -> tuple[bool, str]:
        """
        Validate a fill request for an offer.

        Args:
            offer: The offer being filled
            amount: Requested amount

        Returns:
            (is_valid, error_message)
        """
        if amount < offer.minsize:
            return False, f"Amount {amount} below minimum {offer.minsize}"

        if amount > offer.maxsize:
            return False, f"Amount {amount} above maximum {offer.maxsize}"

        return True, ""

    def get_offer_by_id(self, offers: list[Offer], offer_id: int) -> Offer | None:
        """
        Find an offer by its ID.

        Args:
            offers: List of current offers
            offer_id: Offer ID to find

        Returns:
            Offer with matching oid, or None if not found
        """
        for offer in offers:
            if offer.oid == offer_id:
                return offer
        return None

Creates and manages offers for the maker bot.

Supports creating multiple offers simultaneously, each with a unique offer ID. This allows makers to advertise both relative and absolute fee offers at the same time.

Methods

async def create_offers(self) ‑> list[Offer]
Expand source code
async def create_offers(self) -> list[Offer]:
    """
    Create offers based on wallet balance and configuration.

    Logic:
    1. Find mixdepth with maximum balance
    2. Calculate available amount (balance - dust - max_txfee)
    3. Create offer(s) with configured fee structure(s)
    4. Attach fidelity bond value if available

    Returns:
        List of offers. Each offer gets a unique oid (0, 1, 2, ...).
    """
    try:
        balances = {}
        for mixdepth in range(self.wallet.mixdepth_count):
            balance = await self.wallet.get_balance(mixdepth)
            balances[mixdepth] = balance

        available_mixdepths = {md: bal for md, bal in balances.items() if bal > 0}

        if not available_mixdepths:
            logger.warning("No mixdepth with positive balance")
            return []

        max_mixdepth = max(available_mixdepths, key=lambda md: available_mixdepths[md])
        max_balance = available_mixdepths[max_mixdepth]

        # Get effective offer configurations
        offer_configs = self.config.get_effective_offer_configs()

        # Get fidelity bond value if available (shared across all offers)
        fidelity_bond_value = 0
        bond = await get_best_fidelity_bond(self.wallet)
        if bond:
            fidelity_bond_value = bond.bond_value
            logger.info(
                f"Fidelity bond found: {bond.txid}:{bond.vout} "
                f"value={bond.value} sats, bond_value={bond.bond_value}"
            )

        # Create an offer for each configuration
        offers: list[Offer] = []
        for offer_id, offer_cfg in enumerate(offer_configs):
            offer = self._create_single_offer(
                offer_id=offer_id,
                offer_cfg=offer_cfg,
                max_balance=max_balance,
                fidelity_bond_value=fidelity_bond_value,
            )
            if offer:
                offers.append(offer)

        if not offers:
            logger.warning("No valid offers could be created")
            return []

        logger.info(f"Created {len(offers)} offer(s)")
        return offers

    except Exception as e:
        logger.error(f"Failed to create offers: {e}")
        return []

Create offers based on wallet balance and configuration.

Logic: 1. Find mixdepth with maximum balance 2. Calculate available amount (balance - dust - max_txfee) 3. Create offer(s) with configured fee structure(s) 4. Attach fidelity bond value if available

Returns

List of offers. Each offer gets a unique oid (0, 1, 2, …).

def get_offer_by_id(self, offers: list[Offer], offer_id: int) ‑> Offer | None
Expand source code
def get_offer_by_id(self, offers: list[Offer], offer_id: int) -> Offer | None:
    """
    Find an offer by its ID.

    Args:
        offers: List of current offers
        offer_id: Offer ID to find

    Returns:
        Offer with matching oid, or None if not found
    """
    for offer in offers:
        if offer.oid == offer_id:
            return offer
    return None

Find an offer by its ID.

Args

offers
List of current offers
offer_id
Offer ID to find

Returns

Offer with matching oid, or None if not found

def validate_offer_fill(self, offer: Offer, amount: int) ‑> tuple[bool, str]
Expand source code
def validate_offer_fill(self, offer: Offer, amount: int) -> tuple[bool, str]:
    """
    Validate a fill request for an offer.

    Args:
        offer: The offer being filled
        amount: Requested amount

    Returns:
        (is_valid, error_message)
    """
    if amount < offer.minsize:
        return False, f"Amount {amount} below minimum {offer.minsize}"

    if amount > offer.maxsize:
        return False, f"Amount {amount} above maximum {offer.maxsize}"

    return True, ""

Validate a fill request for an offer.

Args

offer
The offer being filled
amount
Requested amount

Returns

(is_valid, error_message)