Fix receive and send

This commit is contained in:
2026-03-16 06:48:29 +00:00
parent 9ef1720e1f
commit dd275593cd
28 changed files with 1918 additions and 769 deletions

View File

@@ -1,259 +1,92 @@
/**
* History utility functions.
*
* Pure functions for parsing and formatting wallet history data.
* These functions have no React dependencies and can be used
* in both TUI and CLI contexts.
*/
import type { HistoryItem, HistoryInvitationItem, HistoryUtxoItem } from '../services/history.js';
import type { HistoryItem, HistoryItemType } from '../services/history.js';
/**
* Color names for history item types.
* These are semantic color names that can be mapped to actual colors
* by the consuming application (TUI or CLI).
*/
export type HistoryColorName = 'info' | 'warning' | 'success' | 'error' | 'muted' | 'text';
/**
* Formatted history list item data.
*/
export interface FormattedHistoryItem {
/** The display label for the history item */
export type HistoryRowType = 'invitation' | 'invitation_input' | 'invitation_output' | 'utxo';
export interface HistoryDisplayRow {
id: string;
type: HistoryRowType;
label: string;
/** Optional secondary description */
description?: string;
/** The formatted date string */
dateStr?: string;
/** The semantic color name for this item type */
color: HistoryColorName;
/** The history item type */
type: HistoryItemType;
/** Whether the item data is valid */
isValid: boolean;
timestamp?: number;
isNested: boolean;
utxo?: HistoryUtxoItem;
invitation?: HistoryInvitationItem;
}
/**
* Get the semantic color name for a history item type.
*
* @param type - The history item type
* @param isSelected - Whether the item is currently selected
* @returns A semantic color name
*/
export function getHistoryItemColorName(type: HistoryItemType, isSelected: boolean = false): HistoryColorName {
if (isSelected) return 'info'; // Use focus color when selected
switch (type) {
case 'invitation_created':
return 'text';
case 'utxo_reserved':
return 'warning';
case 'utxo_received':
return 'success';
default:
return 'text';
}
}
/**
* Format a satoshi value for display.
*
* @param satoshis - The value in satoshis
* @returns Formatted string with BCH amount
*/
export function formatSatoshisValue(satoshis: bigint | number): string {
const value = typeof satoshis === 'bigint' ? satoshis : BigInt(satoshis);
const bch = Number(value) / 100_000_000;
return `${bch.toFixed(8)} BCH (${value.toLocaleString()} sats)`;
}
/**
* Format a timestamp for display.
*
* @param timestamp - Unix timestamp in milliseconds
* @returns Formatted date string or undefined
*/
export function formatHistoryDate(timestamp?: number): string | undefined {
if (!timestamp) return undefined;
return new Date(timestamp).toLocaleDateString();
}
/**
* Format a history item for display in a list.
*
* @param item - The history item to format
* @param isSelected - Whether the item is currently selected
* @returns Formatted item data for display
*/
export function formatHistoryListItem(
item: HistoryItem | null | undefined,
isSelected: boolean = false
): FormattedHistoryItem {
if (!item) {
return {
label: '',
description: undefined,
dateStr: undefined,
color: 'muted',
type: 'utxo_received',
isValid: false,
};
}
const dateStr = formatHistoryDate(item.timestamp);
const color = getHistoryItemColorName(item.type, isSelected);
switch (item.type) {
case 'invitation_created':
return {
label: `[Invitation] ${item.description}`,
description: undefined,
dateStr,
color,
type: item.type,
isValid: true,
};
case 'utxo_reserved': {
const satsStr = item.valueSatoshis !== undefined
? formatSatoshisValue(item.valueSatoshis)
: 'Unknown amount';
return {
label: `[Reserved] ${satsStr}`,
description: item.description,
dateStr,
color,
type: item.type,
isValid: true,
};
}
case 'utxo_received': {
const satsStr = item.valueSatoshis !== undefined
? formatSatoshisValue(item.valueSatoshis)
: 'Unknown amount';
const reservedTag = item.reserved ? ' [Reserved]' : '';
return {
label: satsStr,
description: `${item.description}${reservedTag}`,
dateStr,
color,
type: item.type,
isValid: true,
};
}
default:
return {
label: `${item.type}: ${item.description}`,
description: undefined,
dateStr,
color: 'text',
type: item.type,
isValid: true,
};
}
}
/**
* Get a type label for display.
*
* @param type - The history item type
* @returns Human-readable type label
*/
export function getHistoryTypeLabel(type: HistoryItemType): string {
switch (type) {
case 'invitation_created':
return 'Invitation';
case 'utxo_reserved':
return 'Reserved';
case 'utxo_received':
return 'Received';
default:
return type;
}
}
/**
* Calculate scrolling window indices for a list.
*
* @param selectedIndex - Currently selected index
* @param totalItems - Total number of items
* @param maxVisible - Maximum visible items
* @returns Start and end indices for the visible window
*/
export function calculateScrollWindow(
selectedIndex: number,
totalItems: number,
maxVisible: number
): { startIndex: number; endIndex: number } {
const halfWindow = Math.floor(maxVisible / 2);
let startIndex = Math.max(0, selectedIndex - halfWindow);
const endIndex = Math.min(totalItems, startIndex + maxVisible);
// Adjust start if we're near the end
if (endIndex - startIndex < maxVisible) {
startIndex = Math.max(0, endIndex - maxVisible);
}
return { startIndex, endIndex };
}
/**
* Check if a history item is a UTXO-related event.
*
* @param item - The history item to check
* @returns True if the item is UTXO-related
*/
export function isUtxoEvent(item: HistoryItem): boolean {
return item.type === 'utxo_received' || item.type === 'utxo_reserved';
}
/**
* Filter history items by type.
*
* @param items - Array of history items
* @param types - Types to include
* @returns Filtered array
*/
export function filterHistoryByType(
items: HistoryItem[],
types: HistoryItemType[]
): HistoryItem[] {
return items.filter(item => types.includes(item.type));
}
/**
* Get summary statistics for history items.
*
* @param items - Array of history items
* @returns Summary statistics
*/
export function getHistorySummary(items: HistoryItem[]): {
totalReceived: bigint;
totalReserved: bigint;
invitationCount: number;
utxoCount: number;
} {
let totalReceived = 0n;
let totalReserved = 0n;
let invitationCount = 0;
let utxoCount = 0;
export function buildHistoryDisplayRows(items: HistoryItem[]): HistoryDisplayRow[] {
const rows: HistoryDisplayRow[] = [];
for (const item of items) {
switch (item.type) {
case 'invitation_created':
invitationCount++;
break;
case 'utxo_reserved':
totalReserved += item.valueSatoshis ?? 0n;
break;
case 'utxo_received':
totalReceived += item.valueSatoshis ?? 0n;
utxoCount++;
break;
if (item.kind === 'invitation') {
rows.push({
id: item.id,
type: 'invitation',
label: item.description,
timestamp: item.createdAtTimestamp,
isNested: false,
invitation: item,
});
for (const input of item.inputs) {
const satsPrefix = input.valueSatoshis !== undefined ? `${input.valueSatoshis.toLocaleString()} sats ` : '';
rows.push({
id: `${item.id}-input-${input.id}`,
type: 'invitation_input',
label: `${satsPrefix}${input.outpoint.txid}:${input.outpoint.index}`,
description: input.description,
isNested: true,
utxo: input,
invitation: item,
});
}
for (const output of item.outputs) {
rows.push({
id: `${item.id}-output-${output.id}`,
type: 'invitation_output',
label: output.valueSatoshis !== undefined ? `${output.valueSatoshis.toLocaleString()} sats` : 'Output',
description: output.description,
isNested: true,
utxo: output,
invitation: item,
});
}
continue;
}
rows.push({
id: item.id,
type: 'utxo',
label: item.valueSatoshis !== undefined ? `${item.valueSatoshis.toLocaleString()} sats` : 'UTXO',
description: item.description,
isNested: false,
utxo: item,
});
}
return { totalReceived, totalReserved, invitationCount, utxoCount };
return rows;
}
export function getHistoryItemColorName(row: HistoryDisplayRow, isSelected: boolean = false): HistoryColorName {
if (isSelected) return 'info';
switch (row.type) {
case 'invitation':
return 'text';
case 'invitation_input':
return 'error';
case 'invitation_output':
return 'success';
case 'utxo':
return row.utxo?.reserved ? 'warning' : 'success';
default:
return 'text';
}
}