Module jmcore.confirmation

User confirmation prompts for fund-moving operations.

Functions

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
Expand source code
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/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."
        )

    # Build transaction summary
    print("\n" + "=" * 80)
    if operation.lower() == "coinjoin":
        print("EXPECTED CJ TX")
    else:
        print(f"TRANSACTION CONFIRMATION - {operation.upper()}")
    print("=" * 80)

    # Amount
    if amount == 0:
        print("Amount:       SWEEP (all available funds)")
    else:
        from jmcore.bitcoin import format_amount

        print(f"Amount:       {format_amount(amount)}")

    # Destination
    if destination:
        if destination == "INTERNAL":
            print("Destination:  INTERNAL (next mixdepth)")
        else:
            print(f"Destination:  {destination}")

    # Fee (total cost: maker fees + mining fee)
    if fee is not None:
        from jmcore.bitcoin import format_amount

        print(f"Total Fees (makers+network): {format_amount(fee)}")

    # Mining fee (transaction fee)
    if mining_fee is not None:
        from jmcore.bitcoin import format_amount

        print(f"Mining Fee:   {format_amount(mining_fee)}")

    # Additional info
    if additional_info:
        for key, value in additional_info.items():
            # Format based on type
            if isinstance(value, int) and key.lower().endswith(("fee", "amount", "value")):
                from jmcore.bitcoin import format_amount

                print(f"{key}:  {format_amount(value)}".ljust(80))
            elif isinstance(value, list):
                print(f"{key}:  {len(value)} item(s)")
                for i, item in enumerate(value, 1):
                    if isinstance(item, dict):
                        # Show dict items nicely
                        print(f"  {i}. {item}")
                    else:
                        print(f"  {i}. {item}")
            else:
                print(f"{key}:  {value}".ljust(80))

    print("=" * 80)

    # 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

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/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
def format_maker_summary(makers: list[dict[str, Any]]) ‑> dict[str, typing.Any]
Expand source code
def format_maker_summary(makers: list[dict[str, Any]]) -> dict[str, Any]:
    """
    Format maker information for confirmation display.

    Args:
        makers: List of selected maker dicts with 'nick', 'fee', 'bond_value', 'location', etc.

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

    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")

        bond_str = f" [bond: {bond_value:,}]" 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:,} sats{bond_str}{location_str}")
        else:
            maker_details.append(f"{nick}: {fee:,} sats{bond_str}")

    return {
        "Counterparties": len(makers),
        "Total Maker Fees": total_maker_fee,
        "Makers": maker_details,
    }

Format maker information for confirmation display.

Args

makers
List of selected maker dicts with 'nick', 'fee', 'bond_value', 'location', etc.

Returns

Dict with formatted maker info

def is_interactive_mode() ‑> bool
Expand source code
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()

Check if we're running in interactive mode.

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