Skip to content

maker.offers

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

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.

Source code in maker/src/maker/offers.py
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
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 available for offers (excludes fidelity bonds)
        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):
                # Use balance for offers (excludes fidelity bonds)
                balance = await self.wallet.get_balance_for_offers(
                    mixdepth, min_confirmations=self.config.min_confirmations
                )
                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 []

            logger.debug(f"Mixdepth balances (excluding fidelity bonds): {balances}")

            max_mixdepth = max(available_mixdepths, key=lambda md: available_mixdepths[md])
            max_balance = available_mixdepths[max_mixdepth]
            logger.info(f"Selected mixdepth {max_mixdepth} with balance {max_balance} sats")

            # 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={max_available} <= min_size={offer_cfg.min_size} "
                    f"(max_balance={max_balance}, dust_threshold={self.config.dust_threshold})"
                )
                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
Attributes
config = config instance-attribute
maker_nick = maker_nick instance-attribute
wallet = wallet instance-attribute
Functions
__init__(wallet: WalletService, config: MakerConfig, maker_nick: str)
Source code in maker/src/maker/offers.py
26
27
28
29
def __init__(self, wallet: WalletService, config: MakerConfig, maker_nick: str):
    self.wallet = wallet
    self.config = config
    self.maker_nick = maker_nick
create_offers() -> list[Offer] async

Create offers based on wallet balance and configuration.

Logic: 1. Find mixdepth with maximum balance available for offers (excludes fidelity bonds) 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, ...).

Source code in maker/src/maker/offers.py
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
async def create_offers(self) -> list[Offer]:
    """
    Create offers based on wallet balance and configuration.

    Logic:
    1. Find mixdepth with maximum balance available for offers (excludes fidelity bonds)
    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):
            # Use balance for offers (excludes fidelity bonds)
            balance = await self.wallet.get_balance_for_offers(
                mixdepth, min_confirmations=self.config.min_confirmations
            )
            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 []

        logger.debug(f"Mixdepth balances (excluding fidelity bonds): {balances}")

        max_mixdepth = max(available_mixdepths, key=lambda md: available_mixdepths[md])
        max_balance = available_mixdepths[max_mixdepth]
        logger.info(f"Selected mixdepth {max_mixdepth} with balance {max_balance} sats")

        # 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 []
get_offer_by_id(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

Source code in maker/src/maker/offers.py
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
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
validate_offer_fill(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)

Source code in maker/src/maker/offers.py
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
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, ""

Functions