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, term_width: int = 120, *, allowed_mixdepth: int | None = None, min_confirmations: int = 0, columns: list[tuple[str, int]] | None = None, allow_fidelity_bonds: bool = True) -> str

Format a single UTXO row with separate Label and State columns.

Source code in jmwallet/src/jmwallet/utxo_selector.py
 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
def format_utxo_line(
    utxo: UTXOInfo,
    max_width: int = 120,
    prev_address: str = "",
    excluded_outpoints: set[tuple[str, int]] | None = None,
    term_width: int = 120,
    *,
    allowed_mixdepth: int | None = None,
    min_confirmations: int = 0,
    columns: list[tuple[str, int]] | None = None,
    allow_fidelity_bonds: bool = True,
) -> str:
    """Format a single UTXO row with separate Label and State columns."""
    columns = columns or _table_columns(term_width, [utxo])
    addr_col_width = dict(columns).get("Address", 12)

    # All addresses use the calculated column width.
    if utxo.address == prev_address:
        addr_str = "..."
    else:
        if len(utxo.address) > addr_col_width:
            # Middle-ellipsis: show start...end
            prefix_len = (addr_col_width - 3) // 2
            suffix_len = addr_col_width - 3 - prefix_len
            addr_str = utxo.address[:prefix_len] + "..." + utxo.address[-suffix_len:]
        else:
            addr_str = utxo.address

    # Label column: FB status for fidelity bonds; remap "non-cj-change" to the
    # shorter "reg-change" so it fits the 11-char column.
    if utxo.is_fidelity_bond:
        label_col = "FB-active" if utxo.is_locked else "FB-expired"
    else:
        label_col = "reg-change" if utxo.label == "non-cj-change" else (utxo.label or "")

    values = {
        "MD": f"m{utxo.mixdepth}",
        "Address": addr_str,
        "Amount": f"{utxo.value:,} sats",
        "Amount (sats)": f"{utxo.value:,}",
        "Confs": f"{utxo.confirmations:,}",
        "Outpoint": f"{utxo.txid[:8]}...:{utxo.vout}",
        "Label": label_col,
        "State": _utxo_state_col(
            utxo,
            excluded_outpoints,
            allowed_mixdepth,
            min_confirmations,
            allow_fidelity_bonds=allow_fidelity_bonds,
        ),
    }
    return _clip(
        _format_cells(values, [(name, size) for name, size in columns if name != "Sel"]) + " |",
        max_width,
    )

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, *, allow_fidelity_bonds: bool = True) -> 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 - PgUp/PgDn: Page up/down - Tab/Space: Toggle selection - Enter: Confirm selection - q/Escape: Cancel - s: Select all (in the pinned/cursor mixdepth) - d: 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 State in-use with a [-] mark; unavailable for selection. allow_fidelity_bonds: Direct sends (the default) may spend expired fidelity bonds. CoinJoin callers pass False so every bond is shown as State no-cj with a [-] mark: bond inputs would be disclosed to makers and then rejected at signing.

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
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
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,
    *,
    allow_fidelity_bonds: bool = True,
) -> 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
    - PgUp/PgDn: Page up/down
    - Tab/Space: Toggle selection
    - Enter: Confirm selection
    - q/Escape: Cancel
    - s: Select all (in the pinned/cursor mixdepth)
    - d: 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 State ``in-use``
            with a ``[-]`` mark; unavailable for selection.
        allow_fidelity_bonds: Direct sends (the default) may spend expired
            fidelity bonds. CoinJoin callers pass ``False`` so every bond is
            shown as State ``no-cj`` with a ``[-]`` mark: bond inputs would
            be disclosed to makers and then rejected at signing.

    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,
                allow_fidelity_bonds=allow_fidelity_bonds,
            ):
                return []
            return utxos
        raise RuntimeError("Interactive UTXO selection requires a terminal")

    # Sort UTXOs by derivation path (same order as freeze manager) and add separators
    sorted_utxos = sorted(utxos, key=utxo_sort_key)
    display_items = build_display_items(sorted_utxos)

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