Skip to content

maker.config

maker.config

Maker bot configuration.

Attributes

Classes

MakerConfig

Bases: WalletConfig

Configuration for maker bot.

Inherits base wallet configuration from jmcore.config.WalletConfig and adds maker-specific settings for offers, hidden services, and UTXO selection.

Offer Configuration: - Simple single-offer: use offer_type, min_size, cj_fee_relative/absolute, tx_fee_contribution - Multi-offer setup: use offer_configs list (overrides single-offer fields when non-empty)

The multi-offer system allows running both relative and absolute fee offers simultaneously, each with a unique offer ID. This is extensible to support N offers in the future.

Source code in maker/src/maker/config.py
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
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
class MakerConfig(WalletConfig):
    """
    Configuration for maker bot.

    Inherits base wallet configuration from jmcore.config.WalletConfig
    and adds maker-specific settings for offers, hidden services, and
    UTXO selection.

    Offer Configuration:
    - Simple single-offer: use offer_type, min_size, cj_fee_relative/absolute, tx_fee_contribution
    - Multi-offer setup: use offer_configs list (overrides single-offer fields when non-empty)

    The multi-offer system allows running both relative and absolute fee offers simultaneously,
    each with a unique offer ID. This is extensible to support N offers in the future.
    """

    # Hidden service configuration for direct peer connections
    # If onion_host is set, maker will serve on a hidden service
    # If tor_control is enabled and onion_host is None, it will be auto-generated
    onion_host: str | None = Field(
        default=None, description="Hidden service address (e.g., 'mymaker...onion')"
    )
    onion_serving_host: str = Field(
        default="127.0.0.1", description="Local bind address for incoming connections"
    )
    onion_serving_port: int = Field(
        default=5222, ge=0, le=65535, description="Default JoinMarket port (0 = auto-assign)"
    )
    tor_target_host: str = Field(
        default="127.0.0.1",
        description="Target host for Tor hidden service (use service name in Docker Compose)",
    )

    # Tor control port configuration for dynamic hidden service creation
    tor_control: TorControlConfig = Field(
        default_factory=create_tor_control_config_from_env,
        description="Tor control port configuration",
    )

    # Tor hidden service DoS defense configuration
    # These settings are applied at the Tor level for protection before traffic reaches the app
    hidden_service_dos: HiddenServiceDoSConfig = Field(
        default_factory=HiddenServiceDoSConfig,
        description=(
            "Tor-level DoS defense for the hidden service. "
            "Includes intro point rate limiting and optional Proof-of-Work. "
            "See https://community.torproject.org/onion-services/advanced/dos/"
        ),
    )

    # Multi-offer configuration (takes precedence over single-offer fields when non-empty)
    # Each OfferConfig gets a unique offer_id (0, 1, 2, ...) based on position
    offer_configs: list[OfferConfig] = Field(
        default_factory=list,
        description=(
            "List of offer configurations. When non-empty, overrides single-offer fields. "
            "Allows running multiple offers (e.g., relative + absolute) simultaneously."
        ),
    )

    # Single offer configuration (legacy, used when offer_configs is empty)
    offer_type: OfferType = Field(
        default=OfferType.SW0_RELATIVE, description="Offer type (relative/absolute fee)"
    )
    min_size: int = Field(
        default=DUST_THRESHOLD, ge=0, description="Minimum CoinJoin amount in satoshis"
    )
    cj_fee_relative: str = Field(default="0.001", description="Relative CJ fee (0.001 = 0.1%)")
    cj_fee_absolute: int = Field(default=500, ge=0, description="Absolute CJ fee in satoshis")
    tx_fee_contribution: int = Field(
        default=0, ge=0, description="Transaction fee contribution in satoshis"
    )

    # Minimum confirmations for UTXOs
    min_confirmations: int = Field(default=1, ge=0, description="Minimum confirmations for UTXOs")

    # Fidelity bond configuration
    # List of locktimes (Unix timestamps) to scan for fidelity bonds
    # These should match locktimes used when creating bond UTXOs
    fidelity_bond_locktimes: list[int] = Field(
        default_factory=list, description="List of locktimes to scan for fidelity bonds"
    )

    # Manual fidelity bond specification (bypasses registry)
    # Use this when you don't have a registry or want to specify a bond directly
    fidelity_bond_index: int | None = Field(
        default=None, description="Fidelity bond derivation index (bypasses registry)"
    )

    # Selected fidelity bond (txid, vout) - if not set, largest bond is used automatically
    selected_fidelity_bond: tuple[str, int] | None = Field(
        default=None, description="Selected fidelity bond UTXO (txid, vout)"
    )

    # Explicitly disable fidelity bonds - skips registry lookup and bond proof generation
    # even when bonds exist in the registry
    no_fidelity_bond: bool = Field(
        default=False, description="Disable fidelity bond usage (run without bond proof)"
    )

    # Timeouts
    session_timeout_sec: int = Field(
        default=300,
        ge=60,
        description="Maximum time for a CoinJoin session to complete (all states)",
    )

    # Pending transaction timeout
    pending_tx_timeout_min: int = Field(
        default=60,
        ge=10,
        le=1440,
        description=(
            "Minutes to wait for a pending CoinJoin transaction to appear on-chain "
            "before marking it as failed. If the taker doesn't broadcast the transaction "
            "within this time, we assume it was abandoned."
        ),
    )

    # Wallet rescan configuration
    post_coinjoin_rescan_delay: int = Field(
        default=60,
        ge=5,
        description="Seconds to wait before rescanning wallet after CoinJoin completion",
    )
    rescan_interval_sec: int = Field(
        default=600,
        ge=60,
        description="Interval in seconds for periodic wallet rescans (default: 10 minutes)",
    )

    # UTXO merge algorithm - how many UTXOs to use
    merge_algorithm: MergeAlgorithm = Field(
        default=MergeAlgorithm.DEFAULT,
        description=(
            "UTXO selection strategy: default (minimum), gradual (+1), "
            "greedy (all), random (0-2 extra)"
        ),
    )

    # Generic message rate limiting (protects against spam/DoS)
    message_rate_limit: int = Field(
        default=10,
        ge=1,
        description="Maximum messages per second per peer (sustained)",
    )
    message_burst_limit: int = Field(
        default=100,
        ge=1,
        description="Maximum burst messages per peer (default: 100, allows ~10s at max rate)",
    )

    # Rate limiting for orderbook requests (protects against spam attacks)
    orderbook_rate_limit: int = Field(
        default=1,
        ge=1,
        description="Maximum orderbook responses per peer per interval",
    )
    orderbook_rate_interval: float = Field(
        default=10.0,
        ge=1.0,
        description="Interval in seconds for orderbook rate limiting (default: 10s)",
    )
    orderbook_violation_ban_threshold: int = Field(
        default=100,
        ge=1,
        description="Ban peer after this many rate limit violations",
    )
    orderbook_violation_warning_threshold: int = Field(
        default=10,
        ge=1,
        description="Start exponential backoff after this many violations",
    )
    orderbook_violation_severe_threshold: int = Field(
        default=50,
        ge=1,
        description="Severe backoff threshold (higher penalty)",
    )
    orderbook_ban_duration: float = Field(
        default=3600.0,
        ge=60.0,
        description="Ban duration in seconds (default: 1 hour)",
    )

    # Directory reconnection configuration
    directory_reconnect_interval: int = Field(
        default=300,
        ge=60,
        description="Interval between reconnection attempts for failed directories (5 min)",
    )
    directory_reconnect_max_retries: int = Field(
        default=0,
        ge=0,
        description="Maximum reconnection attempts per directory (0 = unlimited)",
    )
    directory_startup_timeout: int = Field(
        default=120,
        ge=10,
        description=(
            "Seconds to keep retrying directory connections at startup before giving up "
            "and letting the background reconnect task take over (default: 120s)"
        ),
    )

    model_config = {"frozen": False}

    @field_validator("cj_fee_relative", mode="before")
    @classmethod
    def normalize_cj_fee_relative(cls, v: str | float | int) -> str:
        """Normalize cj_fee_relative to avoid scientific notation."""
        return normalize_decimal_string(v)

    @model_validator(mode="after")
    def validate_config(self) -> MakerConfig:
        """Validate configuration after initialization."""
        # Set bitcoin_network default (handled by parent WalletConfig)
        if self.bitcoin_network is None:
            object.__setattr__(self, "bitcoin_network", self.network)

        # Only validate single-offer fields if offer_configs is empty
        # (when offer_configs is set, those fields are ignored)
        if not self.offer_configs:
            # Validate cj_fee_relative for relative offer types
            if self.offer_type in (OfferType.SW0_RELATIVE, OfferType.SWA_RELATIVE):
                try:
                    cj_fee_float = float(self.cj_fee_relative)
                    if cj_fee_float <= 0:
                        raise ValueError(
                            f"cj_fee_relative must be > 0 for relative offer types, "
                            f"got {self.cj_fee_relative}"
                        )
                except ValueError as e:
                    if "could not convert" in str(e):
                        raise ValueError(
                            f"cj_fee_relative must be a valid number, got {self.cj_fee_relative}"
                        ) from e
                    raise

        return self

    def get_effective_offer_configs(self) -> list[OfferConfig]:
        """
        Get the effective list of offer configurations.

        If offer_configs is set (non-empty), returns it directly.
        Otherwise, creates a single OfferConfig from the legacy single-offer fields.

        This provides backward compatibility while supporting the new multi-offer system.

        Returns:
            List of OfferConfig objects to use for creating offers.
        """
        if self.offer_configs:
            return self.offer_configs

        # Create single OfferConfig from legacy fields
        return [
            OfferConfig(
                offer_type=self.offer_type,
                min_size=self.min_size,
                cj_fee_relative=self.cj_fee_relative,
                cj_fee_absolute=self.cj_fee_absolute,
                tx_fee_contribution=self.tx_fee_contribution,
            )
        ]
Attributes
cj_fee_absolute: int = Field(default=500, ge=0, description='Absolute CJ fee in satoshis') class-attribute instance-attribute
cj_fee_relative: str = Field(default='0.001', description='Relative CJ fee (0.001 = 0.1%)') class-attribute instance-attribute
directory_reconnect_interval: int = Field(default=300, ge=60, description='Interval between reconnection attempts for failed directories (5 min)') class-attribute instance-attribute
directory_reconnect_max_retries: int = Field(default=0, ge=0, description='Maximum reconnection attempts per directory (0 = unlimited)') class-attribute instance-attribute
directory_startup_timeout: int = Field(default=120, ge=10, description='Seconds to keep retrying directory connections at startup before giving up and letting the background reconnect task take over (default: 120s)') class-attribute instance-attribute
fidelity_bond_index: int | None = Field(default=None, description='Fidelity bond derivation index (bypasses registry)') class-attribute instance-attribute
fidelity_bond_locktimes: list[int] = Field(default_factory=list, description='List of locktimes to scan for fidelity bonds') class-attribute instance-attribute
hidden_service_dos: HiddenServiceDoSConfig = Field(default_factory=HiddenServiceDoSConfig, description='Tor-level DoS defense for the hidden service. Includes intro point rate limiting and optional Proof-of-Work. See https://community.torproject.org/onion-services/advanced/dos/') class-attribute instance-attribute
merge_algorithm: MergeAlgorithm = Field(default=(MergeAlgorithm.DEFAULT), description='UTXO selection strategy: default (minimum), gradual (+1), greedy (all), random (0-2 extra)') class-attribute instance-attribute
message_burst_limit: int = Field(default=100, ge=1, description='Maximum burst messages per peer (default: 100, allows ~10s at max rate)') class-attribute instance-attribute
message_rate_limit: int = Field(default=10, ge=1, description='Maximum messages per second per peer (sustained)') class-attribute instance-attribute
min_confirmations: int = Field(default=1, ge=0, description='Minimum confirmations for UTXOs') class-attribute instance-attribute
min_size: int = Field(default=DUST_THRESHOLD, ge=0, description='Minimum CoinJoin amount in satoshis') class-attribute instance-attribute
model_config = {'frozen': False} class-attribute instance-attribute
no_fidelity_bond: bool = Field(default=False, description='Disable fidelity bond usage (run without bond proof)') class-attribute instance-attribute
offer_configs: list[OfferConfig] = Field(default_factory=list, description='List of offer configurations. When non-empty, overrides single-offer fields. Allows running multiple offers (e.g., relative + absolute) simultaneously.') class-attribute instance-attribute
offer_type: OfferType = Field(default=(OfferType.SW0_RELATIVE), description='Offer type (relative/absolute fee)') class-attribute instance-attribute
onion_host: str | None = Field(default=None, description="Hidden service address (e.g., 'mymaker...onion')") class-attribute instance-attribute
onion_serving_host: str = Field(default='127.0.0.1', description='Local bind address for incoming connections') class-attribute instance-attribute
onion_serving_port: int = Field(default=5222, ge=0, le=65535, description='Default JoinMarket port (0 = auto-assign)') class-attribute instance-attribute
orderbook_ban_duration: float = Field(default=3600.0, ge=60.0, description='Ban duration in seconds (default: 1 hour)') class-attribute instance-attribute
orderbook_rate_interval: float = Field(default=10.0, ge=1.0, description='Interval in seconds for orderbook rate limiting (default: 10s)') class-attribute instance-attribute
orderbook_rate_limit: int = Field(default=1, ge=1, description='Maximum orderbook responses per peer per interval') class-attribute instance-attribute
orderbook_violation_ban_threshold: int = Field(default=100, ge=1, description='Ban peer after this many rate limit violations') class-attribute instance-attribute
orderbook_violation_severe_threshold: int = Field(default=50, ge=1, description='Severe backoff threshold (higher penalty)') class-attribute instance-attribute
orderbook_violation_warning_threshold: int = Field(default=10, ge=1, description='Start exponential backoff after this many violations') class-attribute instance-attribute
pending_tx_timeout_min: int = Field(default=60, ge=10, le=1440, description="Minutes to wait for a pending CoinJoin transaction to appear on-chain before marking it as failed. If the taker doesn't broadcast the transaction within this time, we assume it was abandoned.") class-attribute instance-attribute
post_coinjoin_rescan_delay: int = Field(default=60, ge=5, description='Seconds to wait before rescanning wallet after CoinJoin completion') class-attribute instance-attribute
rescan_interval_sec: int = Field(default=600, ge=60, description='Interval in seconds for periodic wallet rescans (default: 10 minutes)') class-attribute instance-attribute
selected_fidelity_bond: tuple[str, int] | None = Field(default=None, description='Selected fidelity bond UTXO (txid, vout)') class-attribute instance-attribute
session_timeout_sec: int = Field(default=300, ge=60, description='Maximum time for a CoinJoin session to complete (all states)') class-attribute instance-attribute
tor_control: TorControlConfig = Field(default_factory=create_tor_control_config_from_env, description='Tor control port configuration') class-attribute instance-attribute
tor_target_host: str = Field(default='127.0.0.1', description='Target host for Tor hidden service (use service name in Docker Compose)') class-attribute instance-attribute
tx_fee_contribution: int = Field(default=0, ge=0, description='Transaction fee contribution in satoshis') class-attribute instance-attribute
Functions
get_effective_offer_configs() -> list[OfferConfig]

Get the effective list of offer configurations.

If offer_configs is set (non-empty), returns it directly. Otherwise, creates a single OfferConfig from the legacy single-offer fields.

This provides backward compatibility while supporting the new multi-offer system.

Returns: List of OfferConfig objects to use for creating offers.

Source code in maker/src/maker/config.py
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
def get_effective_offer_configs(self) -> list[OfferConfig]:
    """
    Get the effective list of offer configurations.

    If offer_configs is set (non-empty), returns it directly.
    Otherwise, creates a single OfferConfig from the legacy single-offer fields.

    This provides backward compatibility while supporting the new multi-offer system.

    Returns:
        List of OfferConfig objects to use for creating offers.
    """
    if self.offer_configs:
        return self.offer_configs

    # Create single OfferConfig from legacy fields
    return [
        OfferConfig(
            offer_type=self.offer_type,
            min_size=self.min_size,
            cj_fee_relative=self.cj_fee_relative,
            cj_fee_absolute=self.cj_fee_absolute,
            tx_fee_contribution=self.tx_fee_contribution,
        )
    ]
normalize_cj_fee_relative(v: str | float | int) -> str classmethod

Normalize cj_fee_relative to avoid scientific notation.

Source code in maker/src/maker/config.py
334
335
336
337
338
@field_validator("cj_fee_relative", mode="before")
@classmethod
def normalize_cj_fee_relative(cls, v: str | float | int) -> str:
    """Normalize cj_fee_relative to avoid scientific notation."""
    return normalize_decimal_string(v)
validate_config() -> MakerConfig

Validate configuration after initialization.

Source code in maker/src/maker/config.py
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
@model_validator(mode="after")
def validate_config(self) -> MakerConfig:
    """Validate configuration after initialization."""
    # Set bitcoin_network default (handled by parent WalletConfig)
    if self.bitcoin_network is None:
        object.__setattr__(self, "bitcoin_network", self.network)

    # Only validate single-offer fields if offer_configs is empty
    # (when offer_configs is set, those fields are ignored)
    if not self.offer_configs:
        # Validate cj_fee_relative for relative offer types
        if self.offer_type in (OfferType.SW0_RELATIVE, OfferType.SWA_RELATIVE):
            try:
                cj_fee_float = float(self.cj_fee_relative)
                if cj_fee_float <= 0:
                    raise ValueError(
                        f"cj_fee_relative must be > 0 for relative offer types, "
                        f"got {self.cj_fee_relative}"
                    )
            except ValueError as e:
                if "could not convert" in str(e):
                    raise ValueError(
                        f"cj_fee_relative must be a valid number, got {self.cj_fee_relative}"
                    ) from e
                raise

    return self

MergeAlgorithm

Bases: StrEnum

UTXO selection algorithm for makers.

Determines how many UTXOs to use when participating in a CoinJoin. Since takers pay all tx fees, makers can add extra inputs "for free" which helps consolidate UTXOs and improves taker privacy.

  • default: Select minimum UTXOs needed (frugal)
  • gradual: Select 1 additional UTXO beyond minimum
  • greedy: Select ALL UTXOs from the mixdepth (max consolidation)
  • random: Select between 0-2 additional UTXOs randomly

Reference: joinmarket-clientserver policy.py merge_algorithm

Source code in maker/src/maker/config.py
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
class MergeAlgorithm(StrEnum):
    """
    UTXO selection algorithm for makers.

    Determines how many UTXOs to use when participating in a CoinJoin.
    Since takers pay all tx fees, makers can add extra inputs "for free"
    which helps consolidate UTXOs and improves taker privacy.

    - default: Select minimum UTXOs needed (frugal)
    - gradual: Select 1 additional UTXO beyond minimum
    - greedy: Select ALL UTXOs from the mixdepth (max consolidation)
    - random: Select between 0-2 additional UTXOs randomly

    Reference: joinmarket-clientserver policy.py merge_algorithm
    """

    DEFAULT = "default"
    GRADUAL = "gradual"
    GREEDY = "greedy"
    RANDOM = "random"
Attributes
DEFAULT = 'default' class-attribute instance-attribute
GRADUAL = 'gradual' class-attribute instance-attribute
GREEDY = 'greedy' class-attribute instance-attribute
RANDOM = 'random' class-attribute instance-attribute

OfferConfig

Bases: BaseModel

Configuration for a single offer.

This model represents an individual offer that the maker will advertise. Multiple OfferConfigs can be used to create multiple offers simultaneously (e.g., one relative and one absolute fee offer).

The offer_id is assigned automatically based on position in the list.

Source code in maker/src/maker/config.py
 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
class OfferConfig(BaseModel):
    """
    Configuration for a single offer.

    This model represents an individual offer that the maker will advertise.
    Multiple OfferConfigs can be used to create multiple offers simultaneously
    (e.g., one relative and one absolute fee offer).

    The offer_id is assigned automatically based on position in the list.
    """

    offer_type: OfferType = Field(
        default=OfferType.SW0_RELATIVE,
        description="Offer type (sw0reloffer for relative, sw0absoffer for absolute)",
    )
    min_size: int = Field(
        default=DUST_THRESHOLD,
        ge=0,
        description="Minimum CoinJoin amount in satoshis (default: dust threshold)",
    )
    cj_fee_relative: str = Field(
        default="0.001",
        description="Relative CJ fee as decimal (0.001 = 0.1%). Used when offer_type is relative.",
    )
    cj_fee_absolute: int = Field(
        default=500,
        ge=0,
        description="Absolute CJ fee in satoshis. Used when offer_type is absolute.",
    )
    tx_fee_contribution: int = Field(
        default=0,
        ge=0,
        description="Transaction fee contribution in satoshis",
    )

    @field_validator("cj_fee_relative", mode="before")
    @classmethod
    def normalize_cj_fee_relative(cls, v: str | float | int) -> str:
        """Normalize cj_fee_relative to avoid scientific notation."""
        return normalize_decimal_string(v)

    @model_validator(mode="after")
    def validate_fee_config(self) -> OfferConfig:
        """Validate fee configuration based on offer type."""
        if self.offer_type in (OfferType.SW0_RELATIVE, OfferType.SWA_RELATIVE):
            try:
                cj_fee_float = float(self.cj_fee_relative)
                if cj_fee_float <= 0:
                    raise ValueError(
                        f"cj_fee_relative must be > 0 for relative offer types, "
                        f"got {self.cj_fee_relative}"
                    )
            except ValueError as e:
                if "could not convert" in str(e):
                    raise ValueError(
                        f"cj_fee_relative must be a valid number, got {self.cj_fee_relative}"
                    ) from e
                raise
        return self

    def get_cjfee(self) -> str | int:
        """Get the appropriate cjfee value based on offer type."""
        if self.offer_type in (OfferType.SW0_ABSOLUTE, OfferType.SWA_ABSOLUTE):
            return self.cj_fee_absolute
        return self.cj_fee_relative

    model_config = {"frozen": False}
Attributes
cj_fee_absolute: int = Field(default=500, ge=0, description='Absolute CJ fee in satoshis. Used when offer_type is absolute.') class-attribute instance-attribute
cj_fee_relative: str = Field(default='0.001', description='Relative CJ fee as decimal (0.001 = 0.1%). Used when offer_type is relative.') class-attribute instance-attribute
min_size: int = Field(default=DUST_THRESHOLD, ge=0, description='Minimum CoinJoin amount in satoshis (default: dust threshold)') class-attribute instance-attribute
model_config = {'frozen': False} class-attribute instance-attribute
offer_type: OfferType = Field(default=(OfferType.SW0_RELATIVE), description='Offer type (sw0reloffer for relative, sw0absoffer for absolute)') class-attribute instance-attribute
tx_fee_contribution: int = Field(default=0, ge=0, description='Transaction fee contribution in satoshis') class-attribute instance-attribute
Functions
get_cjfee() -> str | int

Get the appropriate cjfee value based on offer type.

Source code in maker/src/maker/config.py
 97
 98
 99
100
101
def get_cjfee(self) -> str | int:
    """Get the appropriate cjfee value based on offer type."""
    if self.offer_type in (OfferType.SW0_ABSOLUTE, OfferType.SWA_ABSOLUTE):
        return self.cj_fee_absolute
    return self.cj_fee_relative
normalize_cj_fee_relative(v: str | float | int) -> str classmethod

Normalize cj_fee_relative to avoid scientific notation.

Source code in maker/src/maker/config.py
72
73
74
75
76
@field_validator("cj_fee_relative", mode="before")
@classmethod
def normalize_cj_fee_relative(cls, v: str | float | int) -> str:
    """Normalize cj_fee_relative to avoid scientific notation."""
    return normalize_decimal_string(v)
validate_fee_config() -> OfferConfig

Validate fee configuration based on offer type.

Source code in maker/src/maker/config.py
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
@model_validator(mode="after")
def validate_fee_config(self) -> OfferConfig:
    """Validate fee configuration based on offer type."""
    if self.offer_type in (OfferType.SW0_RELATIVE, OfferType.SWA_RELATIVE):
        try:
            cj_fee_float = float(self.cj_fee_relative)
            if cj_fee_float <= 0:
                raise ValueError(
                    f"cj_fee_relative must be > 0 for relative offer types, "
                    f"got {self.cj_fee_relative}"
                )
        except ValueError as e:
            if "could not convert" in str(e):
                raise ValueError(
                    f"cj_fee_relative must be a valid number, got {self.cj_fee_relative}"
                ) from e
            raise
    return self

Functions

normalize_decimal_string(v: str | float | int) -> str

Normalize a decimal value to avoid scientific notation.

Pydantic may coerce float values (from env vars, TOML, or JSON) to strings, which can result in scientific notation for small values (e.g., 1e-05). The JoinMarket protocol expects decimal notation (e.g., 0.00001).

Source code in maker/src/maker/config.py
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def normalize_decimal_string(v: str | float | int) -> str:
    """
    Normalize a decimal value to avoid scientific notation.

    Pydantic may coerce float values (from env vars, TOML, or JSON) to strings,
    which can result in scientific notation for small values (e.g., 1e-05).
    The JoinMarket protocol expects decimal notation (e.g., 0.00001).
    """
    if isinstance(v, (int, float)):
        # Use Decimal to preserve precision and avoid scientific notation
        return format(Decimal(str(v)), "f")
    # Already a string - check if it contains scientific notation
    if "e" in v.lower():
        try:
            return format(Decimal(v), "f")
        except InvalidOperation:
            pass  # Let pydantic handle the validation error
    return v