Big changes and fixes. Uses action history. Improve role selection. Remove unused logs

This commit is contained in:
2026-02-08 15:41:14 +00:00
parent da096af0fa
commit df57f1b9ad
16 changed files with 1250 additions and 1181 deletions

View File

@@ -1,19 +1,19 @@
/**
* Wallet State Screen - Displays wallet balances and UTXOs.
* Wallet State Screen - Displays wallet balances and history.
*
* Shows:
* - Total balance
* - List of unspent outputs
* - Wallet history (invitations, reservations)
* - Navigation to other actions
*/
import React, { useState, useEffect, useCallback } from 'react';
import { Box, Text, useInput } from 'ink';
import SelectInput from 'ink-select-input';
import { type ListItem } from '../components/List.js';
import { useNavigation } from '../hooks/useNavigation.js';
import { useAppContext, useStatus } from '../hooks/useAppContext.js';
import { colors, formatSatoshis, formatHex, logoSmall } from '../theme.js';
import type { HistoryItem } from '../../services/history.js';
/**
* Menu action items.
@@ -26,20 +26,9 @@ const menuItems = [
{ label: 'Refresh', value: 'refresh' },
];
/**
* UTXO display item.
*/
interface UTXOItem {
key: string;
satoshis: bigint;
txid: string;
index: number;
reserved: boolean;
}
/**
* Wallet State Screen Component.
* Displays wallet balance, UTXOs, and action menu.
* Displays wallet balance, history, and action menu.
*/
export function WalletStateScreen(): React.ReactElement {
const { navigate } = useNavigation();
@@ -48,9 +37,9 @@ export function WalletStateScreen(): React.ReactElement {
// State
const [balance, setBalance] = useState<{ totalSatoshis: bigint; utxoCount: number } | null>(null);
const [utxos, setUtxos] = useState<UTXOItem[]>([]);
const [focusedPanel, setFocusedPanel] = useState<'menu' | 'utxos'>('menu');
const [selectedUtxoIndex, setSelectedUtxoIndex] = useState(0);
const [history, setHistory] = useState<HistoryItem[]>([]);
const [focusedPanel, setFocusedPanel] = useState<'menu' | 'history'>('menu');
const [selectedHistoryIndex, setSelectedHistoryIndex] = useState(0);
const [isLoading, setIsLoading] = useState(true);
/**
@@ -66,23 +55,21 @@ export function WalletStateScreen(): React.ReactElement {
setIsLoading(true);
setStatus('Loading wallet state...');
// Get UTXOs
// Get UTXOs for balance calculation
const utxoData = await appService.engine.listUnspentOutputsData();
setUtxos(utxoData.map((utxo) => ({
key: `${utxo.outpointTransactionHash}:${utxo.outpointIndex}`,
satoshis: BigInt(utxo.valueSatoshis),
txid: utxo.outpointTransactionHash,
index: utxo.outpointIndex,
reserved: utxo.reserved ?? false,
})));
// Get balance
const balanceData = utxoData.reduce((acc, utxo) => acc + BigInt(utxo.valueSatoshis), BigInt(0));
// Calculate balance
const selectableUtxos = utxoData.filter(utxo => utxo.selectable);
const balanceData = selectableUtxos.reduce((acc, utxo) => acc + BigInt(utxo.valueSatoshis), BigInt(0));
setBalance({
totalSatoshis: balanceData,
utxoCount: utxoData.length,
utxoCount: selectableUtxos.length,
});
// Get wallet history from the history service
const historyData = await appService.history.getHistory();
setHistory(historyData);
setStatus('Wallet ready');
setIsLoading(false);
} catch (error) {
@@ -162,17 +149,19 @@ export function WalletStateScreen(): React.ReactElement {
// Handle keyboard navigation between panels
useInput((input, key) => {
if (key.tab) {
setFocusedPanel(prev => prev === 'menu' ? 'utxos' : 'menu');
setFocusedPanel(prev => prev === 'menu' ? 'history' : 'menu');
}
// Navigate history items when focused
if (focusedPanel === 'history' && history.length > 0) {
if (key.upArrow) {
setSelectedHistoryIndex(prev => Math.max(0, prev - 1));
} else if (key.downArrow) {
setSelectedHistoryIndex(prev => Math.min(history.length - 1, prev + 1));
}
}
});
// Convert UTXOs to list items
const utxoListItems: ListItem[] = utxos.map((utxo, index) => ({
key: utxo.key,
label: `${formatSatoshis(utxo.satoshis)} | ${formatHex(utxo.txid, 16)}:${utxo.index}`,
description: utxo.reserved ? '[Reserved]' : undefined,
}));
return (
<Box flexDirection='column' flexGrow={1}>
{/* Header */}
@@ -251,33 +240,96 @@ export function WalletStateScreen(): React.ReactElement {
</Box>
</Box>
{/* UTXO list */}
{/* Wallet History */}
<Box marginTop={1} flexGrow={1}>
<Box
borderStyle='single'
borderColor={focusedPanel === 'utxos' ? colors.focus : colors.border}
borderColor={focusedPanel === 'history' ? colors.focus : colors.border}
flexDirection='column'
paddingX={1}
width='100%'
height={14}
overflow='hidden'
>
<Text color={colors.primary} bold> Unspent Outputs (UTXOs) </Text>
<Text color={colors.primary} bold> Wallet History {history.length > 0 ? `(${selectedHistoryIndex + 1}/${history.length})` : ''}</Text>
<Box marginTop={1} flexDirection='column'>
{isLoading ? (
<Text color={colors.textMuted}>Loading...</Text>
) : utxoListItems.length === 0 ? (
<Text color={colors.textMuted}>No unspent outputs found</Text>
) : history.length === 0 ? (
<Text color={colors.textMuted}>No history found</Text>
) : (
utxoListItems.map((item, index) => (
<Box key={item.key}>
<Text color={index === selectedUtxoIndex && focusedPanel === 'utxos' ? colors.focus : colors.text}>
{index === selectedUtxoIndex && focusedPanel === 'utxos' ? '▸ ' : ' '}
{index + 1}. {item.label}
</Text>
{item.description && (
<Text color={colors.warning}> {item.description}</Text>
)}
</Box>
))
// Show a scrolling window of items
(() => {
const maxVisible = 10;
const halfWindow = Math.floor(maxVisible / 2);
let startIndex = Math.max(0, selectedHistoryIndex - halfWindow);
const endIndex = Math.min(history.length, startIndex + maxVisible);
// Adjust start if we're near the end
if (endIndex - startIndex < maxVisible) {
startIndex = Math.max(0, endIndex - maxVisible);
}
const visibleItems = history.slice(startIndex, endIndex);
return visibleItems.map((item, idx) => {
const actualIndex = startIndex + idx;
const isSelected = actualIndex === selectedHistoryIndex && focusedPanel === 'history';
const indicator = isSelected ? '▸ ' : ' ';
const dateStr = item.timestamp
? new Date(item.timestamp).toLocaleDateString()
: '';
// Format the history item based on type
if (item.type === 'invitation_created') {
return (
<Box key={item.id} flexDirection='row' justifyContent='space-between'>
<Text color={isSelected ? colors.focus : colors.text}>
{indicator}[Invitation] {item.description}
</Text>
{dateStr && <Text color={colors.textMuted}>{dateStr}</Text>}
</Box>
);
} else if (item.type === 'utxo_reserved') {
const sats = item.valueSatoshis ?? 0n;
return (
<Box key={item.id} flexDirection='row' justifyContent='space-between'>
<Box>
<Text color={isSelected ? colors.focus : colors.warning}>
{indicator}[Reserved] {formatSatoshis(sats)}
</Text>
<Text color={colors.textMuted}> {item.description}</Text>
</Box>
{dateStr && <Text color={colors.textMuted}>{dateStr}</Text>}
</Box>
);
} else if (item.type === 'utxo_received') {
const sats = item.valueSatoshis ?? 0n;
const reservedTag = item.reserved ? ' [Reserved]' : '';
return (
<Box key={item.id} flexDirection='row' justifyContent='space-between'>
<Box flexDirection='row'>
<Text color={isSelected ? colors.focus : colors.success}>
{indicator}{formatSatoshis(sats)}
</Text>
<Text color={colors.textMuted}>
{' '}{item.description}{reservedTag}
</Text>
</Box>
{dateStr && <Text color={colors.textMuted}>{dateStr}</Text>}
</Box>
);
}
// Fallback for other types
return (
<Box key={item.id} flexDirection='row' justifyContent='space-between'>
<Text color={isSelected ? colors.focus : colors.text}>
{indicator}{item.type}: {item.description}
</Text>
{dateStr && <Text color={colors.textMuted}>{dateStr}</Text>}
</Box>
);
});
})()
)}
</Box>
</Box>