Skip to content

jmwallet.utxo_selector

jmwallet.utxo_selector

Interactive UTXO selector TUI.

Shows every UTXO in the wallet grouped by mixdepth (same layout as the freeze manager) so the user can compare coins across the whole wallet before picking which one(s) to spend.

A spend can only draw from a single mixdepth, so the selector pins the source mixdepth to the first selected UTXO: while anything is selected, UTXOs in other mixdepths render as unselectable [-] rows. Deselecting everything unpins the mixdepth again. Callers that already know the source mixdepth can pass allowed_mixdepth to pin it up front (other mixdepths are then shown for context only).

Attributes

Classes

Functions:

format_utxo_line(utxo: UTXOInfo, max_width: int = 120, prev_address: str = '', excluded_outpoints: set[tuple[str, int]] | None = None) -> str

Format a single UTXO row (without the selection-state prefix).

Args: utxo: The UTXO to format max_width: Maximum line width (longer lines are truncated with ...) prev_address: Address of the previous row; consecutive duplicates render blanks so the column stays visually grouped

Returns: Formatted string with mixdepth, address, amount, confirmations, outpoint, fidelity bond / frozen / in-use indicators, and label.

Source code in jmwallet/src/jmwallet/utxo_selector.py
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
def format_utxo_line(
    utxo: UTXOInfo,
    max_width: int = 120,
    prev_address: str = "",
    excluded_outpoints: set[tuple[str, int]] | None = None,
) -> str:
    """Format a single UTXO row (without the selection-state prefix).

    Args:
        utxo: The UTXO to format
        max_width: Maximum line width (longer lines are truncated with ``...``)
        prev_address: Address of the previous row; consecutive duplicates
            render blanks so the column stays visually grouped

    Returns:
        Formatted string with mixdepth, address, amount, confirmations,
        outpoint, fidelity bond / frozen / in-use indicators, and label.
    """
    md_str = f"m{utxo.mixdepth}"
    addr_str = format_address_column(utxo.address, prev_address)
    amount_str = f"{utxo.value:,} sats"
    conf_str = f"{utxo.confirmations:>5,} conf"
    outpoint = f"{utxo.txid[:8]}...:{utxo.vout}"

    # Fidelity bond indicator (locked vs unlocked)
    fb_indicator = ""
    if utxo.is_fidelity_bond:
        fb_indicator = " [FB-LOCKED]" if utxo.is_locked else " [FB]"

    # Label/note for UTXO type
    label_str = f" ({utxo.label})" if utxo.label else ""

    # Frozen indicator (placed after label for consistency with --extended view)
    frozen_indicator = " [FROZEN]" if utxo.frozen else ""
    in_use_indicator = (
        " [IN-USE]" if excluded_outpoints and (utxo.txid, utxo.vout) in excluded_outpoints else ""
    )

    line = (
        f"{md_str:>2} | {addr_str:<{ADDRESS_COL_WIDTH}} | {amount_str:>15} | {conf_str} | "
        f"{outpoint}{fb_indicator}{label_str}{frozen_indicator}{in_use_indicator}"
    )

    if len(line) > max_width:
        line = line[: max_width - 3] + "..."

    return line

select_utxos_interactive(utxos: list[UTXOInfo], target_amount: int = 0, allowed_mixdepth: int | None = None, min_confirmations: int = 0, excluded_outpoints: set[tuple[str, int]] | None = None) -> list[UTXOInfo]

Display an interactive UTXO selector over the whole wallet.

UTXOs are grouped by mixdepth (freeze-manager layout). Selection is limited to a single mixdepth: the first toggled UTXO pins the source mixdepth until everything is deselected again.

Keys: - Up/Down or j/k: Navigate - Tab/Space: Toggle selection - Enter: Confirm selection - q/Escape: Cancel - a: Select all (in the pinned/cursor mixdepth) - n: Deselect all - g/G: Go to top/bottom

Args: utxos: List of available UTXOs to choose from (any mixdepth) target_amount: Target amount in sats (0 for sweep, used for display) allowed_mixdepth: When set, restrict selection to this mixdepth (other mixdepths are displayed for context only) min_confirmations: UTXOs below this many confirmations are shown but unselectable excluded_outpoints: In-flight CoinJoin inputs shown as [IN-USE] but unavailable for selection.

Returns: List of selected UTXOs (all from one mixdepth), empty if cancelled

Raises: RuntimeError: If not running in a terminal

Source code in jmwallet/src/jmwallet/utxo_selector.py
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
def select_utxos_interactive(
    utxos: list[UTXOInfo],
    target_amount: int = 0,
    allowed_mixdepth: int | None = None,
    min_confirmations: int = 0,
    excluded_outpoints: set[tuple[str, int]] | None = None,
) -> list[UTXOInfo]:
    """Display an interactive UTXO selector over the whole wallet.

    UTXOs are grouped by mixdepth (freeze-manager layout). Selection is
    limited to a single mixdepth: the first toggled UTXO pins the source
    mixdepth until everything is deselected again.

    Keys:
    - Up/Down or j/k: Navigate
    - Tab/Space: Toggle selection
    - Enter: Confirm selection
    - q/Escape: Cancel
    - a: Select all (in the pinned/cursor mixdepth)
    - n: Deselect all
    - g/G: Go to top/bottom

    Args:
        utxos: List of available UTXOs to choose from (any mixdepth)
        target_amount: Target amount in sats (0 for sweep, used for display)
        allowed_mixdepth: When set, restrict selection to this mixdepth
            (other mixdepths are displayed for context only)
        min_confirmations: UTXOs below this many confirmations are shown
            but unselectable
        excluded_outpoints: In-flight CoinJoin inputs shown as ``[IN-USE]``
            but unavailable for selection.

    Returns:
        List of selected UTXOs (all from one mixdepth), empty if cancelled

    Raises:
        RuntimeError: If not running in a terminal
    """
    # Handle trivial cases without requiring a terminal
    if not utxos:
        return []
    excluded_outpoints = excluded_outpoints or set()

    # For multiple UTXOs, we need a terminal
    if not sys.stdin.isatty() or not sys.stdout.isatty():
        # If only one UTXO and no terminal, auto-select it (only if selectable)
        if len(utxos) == 1:
            utxo = utxos[0]
            if not _is_base_selectable(
                utxo, allowed_mixdepth, min_confirmations, excluded_outpoints
            ):
                return []
            return utxos
        raise RuntimeError("Interactive UTXO selection requires a terminal")

    # Sort UTXOs by mixdepth, then by value (descending), and add separators
    sorted_utxos = sorted(utxos, key=lambda u: (u.mixdepth, -u.value))
    display_items = build_display_items(sorted_utxos)

    return curses.wrapper(
        _run_selector,
        display_items,
        target_amount,
        allowed_mixdepth,
        min_confirmations,
        excluded_outpoints,
    )