Skip to content

jmcore.confirmation

jmcore.confirmation

User confirmation prompts for fund-moving operations.

Functions

confirm_transaction(operation: str, amount: int, destination: str | None = None, fee: int | None = None, mining_fee: int | None = None, additional_info: dict[str, Any] | None = None, skip_confirmation: bool = False) -> bool

Prompt user to confirm a transaction that moves funds.

Args: operation: Type of operation (e.g., "send", "coinjoin") amount: Amount in satoshis (0 for sweep) destination: Destination address (optional) fee: Total fee in satoshis (optional, for CoinJoin this is maker fees + mining fee) mining_fee: Mining/transaction fee in satoshis (optional) additional_info: Additional details to show (e.g., maker fees, counterparties) skip_confirmation: If True, skip prompt (from --yes flag)

Returns: True if user confirms, False otherwise

Raises: RuntimeError: If in non-interactive mode without skip_confirmation

Source code in jmcore/src/jmcore/confirmation.py
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
def confirm_transaction(
    operation: str,
    amount: int,
    destination: str | None = None,
    fee: int | None = None,
    mining_fee: int | None = None,
    additional_info: dict[str, Any] | None = None,
    skip_confirmation: bool = False,
) -> bool:
    """
    Prompt user to confirm a transaction that moves funds.

    Args:
        operation: Type of operation (e.g., "send", "coinjoin")
        amount: Amount in satoshis (0 for sweep)
        destination: Destination address (optional)
        fee: Total fee in satoshis (optional, for CoinJoin this is maker fees + mining fee)
        mining_fee: Mining/transaction fee in satoshis (optional)
        additional_info: Additional details to show (e.g., maker fees, counterparties)
        skip_confirmation: If True, skip prompt (from --yes flag)

    Returns:
        True if user confirms, False otherwise

    Raises:
        RuntimeError: If in non-interactive mode without skip_confirmation
    """
    # Skip if confirmation disabled
    if skip_confirmation:
        return True

    # Error if non-interactive without --yes
    if not is_interactive_mode():
        raise RuntimeError(
            "Cannot prompt for confirmation in non-interactive mode. "
            "Use --yes flag or set NO_INTERACTIVE=1 to skip confirmation."
        )

    # Use different display for coinjoin vs regular transactions
    if operation.lower() == "coinjoin":
        _display_coinjoin_confirmation(
            amount=amount,
            destination=destination,
            mining_fee=mining_fee,
            additional_info=additional_info,
        )
    else:
        _display_standard_confirmation(
            operation=operation,
            amount=amount,
            destination=destination,
            fee=fee,
            mining_fee=mining_fee,
            additional_info=additional_info,
        )

    # Prompt for confirmation - flush stdout and clear any buffered stdin
    try:
        sys.stdout.flush()
        # Drain any pending input to ensure we get fresh user input
        # (important when running in asyncio context with logging)
        try:
            import termios

            # Flush input buffer to discard any stale data
            termios.tcflush(sys.stdin.fileno(), termios.TCIFLUSH)
        except ImportError:
            # Not Unix
            pass
        except (OSError, ValueError):
            # Not a TTY or no terminal settings available
            pass

        response = input("\nProceed with this transaction? [y/N]: ").strip().lower()
        return response in ("y", "yes")
    except (KeyboardInterrupt, EOFError):
        print("\n\nTransaction cancelled by user.")
        return False

format_maker_summary(makers: list[dict[str, Any]], fee_rate: float | None = None) -> dict[str, Any]

Format maker information for confirmation display.

Args: makers: List of selected maker dicts with 'nick', 'fee', 'bond_value', 'location', etc. fee_rate: Fee rate in sat/vB (optional)

Returns: Dict with formatted maker info for confirmation display

Source code in jmcore/src/jmcore/confirmation.py
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
def format_maker_summary(
    makers: list[dict[str, Any]], fee_rate: float | None = None
) -> dict[str, Any]:
    """
    Format maker information for confirmation display.

    Args:
        makers: List of selected maker dicts with 'nick', 'fee', 'bond_value', 'location', etc.
        fee_rate: Fee rate in sat/vB (optional)

    Returns:
        Dict with formatted maker info for confirmation display
    """
    total_maker_fee = sum(m.get("fee", 0) for m in makers)

    # Find max widths for alignment
    max_fee_width = max((len(f"{m.get('fee', 0):,}") for m in makers), default=1)
    max_bond_width = max((len(f"{m.get('bond_value', 0):,}") for m in makers), default=1)

    maker_details = []
    for m in makers:
        nick = m.get("nick", "unknown")
        fee = m.get("fee", 0)
        bond_value = m.get("bond_value", 0)
        location = m.get("location")

        # Right-align fee and bond values
        fee_str = f"{fee:>{max_fee_width},}"
        bond_str = f" [bond: {bond_value:>{max_bond_width},}]" if bond_value > 0 else " [no bond]"

        # Add location info if available
        if location and location != "NOT-SERVING-ONION":
            # Truncate onion address for readability (show first 16 chars)
            if ":" in location:
                onion, port = location.rsplit(":", 1)
                if onion.endswith(".onion") and len(onion) > 20:
                    location_str = f" @ {onion[:16]}...:{port}"
                else:
                    location_str = f" @ {location}"
            else:
                location_str = f" @ {location[:20]}..."
            maker_details.append(f"{nick}: {fee_str} sats{bond_str}{location_str}")
        else:
            maker_details.append(f"{nick}: {fee_str} sats{bond_str}")

    result: dict[str, Any] = {
        "Total Maker Fee": total_maker_fee,
        "Makers": maker_details,
    }

    if fee_rate is not None:
        result["Fee Rate"] = fee_rate

    return result

is_interactive_mode() -> bool

Check if we're running in interactive mode.

Returns False if NO_INTERACTIVE env var is set or if not attached to a TTY.

Source code in jmcore/src/jmcore/confirmation.py
12
13
14
15
16
17
18
19
20
def is_interactive_mode() -> bool:
    """
    Check if we're running in interactive mode.

    Returns False if NO_INTERACTIVE env var is set or if not attached to a TTY.
    """
    if os.environ.get("NO_INTERACTIVE"):
        return False
    return sys.stdin.isatty() and sys.stdout.isatty()