diff --git a/src/tui/App.tsx b/src/tui/App.tsx
index 1676398..000c258 100644
--- a/src/tui/App.tsx
+++ b/src/tui/App.tsx
@@ -17,7 +17,6 @@ import { WalletStateScreen } from './screens/WalletState.js';
import { TemplateListScreen } from './screens/TemplateList.js';
import { ActionWizardScreen } from './screens/action-wizard/ActionWizardScreen.js';
import { InvitationScreen } from './screens/invitations/InvitationScreen.js';
-import { TransactionScreen } from './screens/Transaction.js';
import { MessageDialog } from './components/Dialog.js';
@@ -45,8 +44,6 @@ function Router(): React.ReactElement {
return ;
case 'invitations':
return ;
- case 'transaction':
- return ;
default:
return Unknown screen: {screen};
}
diff --git a/src/tui/screens/Transaction.tsx b/src/tui/screens/Transaction.tsx
deleted file mode 100644
index ec1bbfb..0000000
--- a/src/tui/screens/Transaction.tsx
+++ /dev/null
@@ -1,413 +0,0 @@
-/**
- * Transaction Screen - Reviews and broadcasts transactions.
- *
- * Provides:
- * - Transaction details review
- * - Input/output inspection
- * - Fee calculation display
- * - Broadcast confirmation
- */
-
-import React, { useState, useEffect, useCallback } from 'react';
-import { Box, Text } from 'ink';
-import { ConfirmDialog } from '../components/Dialog.js';
-import { useNavigation } from '../hooks/useNavigation.js';
-import { useAppContext, useStatus } from '../hooks/useAppContext.js';
-import { useBlockableInput } from '../hooks/useInputLayer.js';
-import { useInvitation } from '../hooks/useInvitations.js';
-import { colors, logoSmall, formatSatoshis, formatHex } from '../theme.js';
-import { copyToClipboard } from '../utils/clipboard.js';
-
-/**
- * Action menu items.
- */
-const actionItems = [
- { label: 'Broadcast Transaction', value: 'broadcast' },
- { label: 'Sign Transaction', value: 'sign' },
- { label: 'Copy Transaction Hex', value: 'copy' },
- { label: 'Back to Invitation', value: 'back' },
-];
-
-/**
- * Transaction Screen Component.
- */
-export function TransactionScreen(): React.ReactElement {
- const { navigate, goBack, data: navData } = useNavigation();
- const { showError, showInfo } = useAppContext();
- const { setStatus } = useStatus();
-
- // Extract invitation ID from navigation data
- const invitationId = navData.invitationId as string | undefined;
-
- // Use hook to get invitation reactively
- const invitationInstance = useInvitation(invitationId ?? null);
-
- // State
- const [focusedPanel, setFocusedPanel] = useState<'inputs' | 'outputs' | 'actions'>('actions');
- const [selectedActionIndex, setSelectedActionIndex] = useState(0);
- const [isLoading, setIsLoading] = useState(false);
- const [showBroadcastConfirm, setShowBroadcastConfirm] = useState(false);
-
- // Check if invitation exists
- useEffect(() => {
- if (!invitationId) {
- showError('No invitation ID provided');
- goBack();
- return;
- }
-
- if (invitationId && !invitationInstance) {
- showError('Invitation not found');
- goBack();
- }
- }, [invitationId, invitationInstance, showError, goBack]);
-
- const invitation = invitationInstance?.data ?? null;
-
- /**
- * Broadcast transaction.
- */
- const broadcastTransaction = useCallback(async () => {
- if (!invitationInstance) return;
-
- setShowBroadcastConfirm(false);
- setIsLoading(true);
- setStatus('Broadcasting transaction...');
-
- try {
- await invitationInstance.broadcast();
- showInfo(
- `Transaction Broadcast Successful!\n\n` +
- `The transaction has been submitted to the network.`
- );
- navigate('wallet');
- } catch (error) {
- showError(`Failed to broadcast: ${error instanceof Error ? error.message : String(error)}`);
- } finally {
- setIsLoading(false);
- setStatus('Ready');
- }
- }, [invitationInstance, showInfo, showError, navigate, setStatus]);
-
- /**
- * Sign transaction.
- */
- const signTransaction = useCallback(async () => {
- if (!invitationInstance) return;
-
- setIsLoading(true);
- setStatus('Signing transaction...');
-
- try {
- await invitationInstance.sign();
- showInfo('Transaction signed successfully!');
- } catch (error) {
- showError(`Failed to sign: ${error instanceof Error ? error.message : String(error)}`);
- } finally {
- setIsLoading(false);
- setStatus('Ready');
- }
- }, [invitationInstance, showInfo, showError, setStatus]);
-
- /**
- * Copy transaction hex.
- */
- const copyTransactionHex = useCallback(async () => {
- if (!invitation) return;
-
- try {
- await copyToClipboard(invitation.invitationIdentifier);
- showInfo(
- `Copied Invitation ID!\n\n` +
- `ID: ${invitation.invitationIdentifier}\n` +
- `Commits: ${invitation.commits.length}`
- );
- } catch (error) {
- showError(`Failed to copy: ${error instanceof Error ? error.message : String(error)}`);
- }
- }, [invitation, showInfo, showError]);
-
- /**
- * Handle action selection.
- */
- const handleAction = useCallback((action: string) => {
- switch (action) {
- case 'broadcast':
- setShowBroadcastConfirm(true);
- break;
- case 'sign':
- signTransaction();
- break;
- case 'copy':
- copyTransactionHex();
- break;
- case 'back':
- goBack();
- break;
- }
- }, [signTransaction, copyTransactionHex, goBack]);
-
- // Handle keyboard navigation — automatically blocked when the confirm dialog is open.
- useBlockableInput((input, key) => {
- // Tab to switch panels
- if (key.tab) {
- setFocusedPanel(prev => {
- if (prev === 'inputs') return 'outputs';
- if (prev === 'outputs') return 'actions';
- return 'inputs';
- });
- return;
- }
-
- // Up/Down in actions
- if (focusedPanel === 'actions') {
- if (key.upArrow || input === 'k') {
- setSelectedActionIndex(prev => Math.max(0, prev - 1));
- } else if (key.downArrow || input === 'j') {
- setSelectedActionIndex(prev => Math.min(actionItems.length - 1, prev + 1));
- }
- }
-
- // Enter to select
- if (key.return && focusedPanel === 'actions') {
- const action = actionItems[selectedActionIndex];
- if (action) {
- handleAction(action.value);
- }
- }
- });
-
- // Extract transaction data from invitation
- const commits = invitation?.commits ?? [];
- const inputs: Array<{ txid: string; index: number; value?: bigint; inputIdentifier?: string }> = [];
- const outputs: Array<{ value?: bigint; lockingBytecode: string; outputIdentifier?: string; isTemplate: boolean }> = [];
- const variables: Array<{ id: string; value: string }> = [];
-
- // Parse commits for inputs, outputs, and variables
- for (const commit of commits) {
- // Extract variables (to help understand output values)
- if (commit.data?.variables) {
- for (const variable of commit.data.variables) {
- variables.push({
- id: variable.variableIdentifier,
- value: String(variable.value),
- });
- }
- }
-
- if (commit.data?.inputs) {
- for (const input of commit.data.inputs) {
- // Convert Uint8Array to hex string if needed
- const txidHex = input.outpointTransactionHash
- ? typeof input.outpointTransactionHash === 'string'
- ? input.outpointTransactionHash
- : Buffer.from(input.outpointTransactionHash).toString('hex')
- : undefined;
-
- // Skip inputs that are just placeholders (no txid)
- if (txidHex) {
- inputs.push({
- txid: txidHex,
- index: input.outpointIndex ?? 0,
- value: undefined, // Will be looked up from UTXO data
- inputIdentifier: (input as any).inputIdentifier,
- });
- }
- }
- }
- if (commit.data?.outputs) {
- for (const output of commit.data.outputs) {
- // Convert Uint8Array to hex string if needed
- const lockingBytecodeHex = output.lockingBytecode
- ? typeof output.lockingBytecode === 'string'
- ? output.lockingBytecode
- : Buffer.from(output.lockingBytecode).toString('hex')
- : undefined;
-
- // Check if this is a template-defined output (has outputIdentifier but no direct value)
- const isTemplateOutput = !!(output as any).outputIdentifier && !output.valueSatoshis;
-
- outputs.push({
- value: output.valueSatoshis,
- lockingBytecode: lockingBytecodeHex ?? '(pending)',
- outputIdentifier: (output as any).outputIdentifier,
- isTemplate: isTemplateOutput,
- });
- }
- }
- }
-
- // Try to resolve template output values from variables
- const resolvedOutputs = outputs.map(output => {
- if (output.isTemplate && output.outputIdentifier) {
- // Look for a matching variable (e.g., requestSatoshisOutput -> requestedSatoshis)
- const satoshiVar = variables.find(v =>
- v.id.toLowerCase().includes('satoshi') ||
- v.id.toLowerCase().includes('amount')
- );
- if (satoshiVar) {
- return {
- ...output,
- value: BigInt(satoshiVar.value),
- resolvedFrom: satoshiVar.id,
- };
- }
- }
- return output;
- });
-
- // Calculate totals (only for resolved values)
- const totalOut = resolvedOutputs.reduce((sum, o) => sum + (o.value ?? 0n), 0n);
- // Note: We can't calculate totalIn without UTXO lookup, so fee is unknown
- const hasUnresolvedOutputs = resolvedOutputs.some(o => o.value === undefined);
- const hasUnresolvedInputs = inputs.length > 0; // Input values are always unknown from commit data
-
- return (
-
- {/* Header */}
-
- {logoSmall} - Transaction Review
-
-
- {/* Summary box */}
-
- Transaction Summary
- {invitation ? (
-
- Inputs: {inputs.length} | Outputs: {resolvedOutputs.length} | Commits: {commits.length}
- {hasUnresolvedInputs && (
- Total In: (requires UTXO lookup)
- )}
- Total Out: {formatSatoshis(totalOut)}{hasUnresolvedOutputs ? ' (partial)' : ''}
- {hasUnresolvedInputs ? (
- Fee: (calculated at broadcast)
- ) : (
- Fee: {formatSatoshis(0n)}
- )}
-
- ) : (
- Loading...
- )}
-
-
- {/* Inputs and Outputs */}
-
- {/* Inputs */}
-
- Inputs
-
- {inputs.length === 0 ? (
- No inputs
- ) : (
- inputs.map((input, index) => (
-
-
- {index + 1}. {formatHex(input.txid, 12)}:{input.index}
-
- {input.value !== undefined && (
- {formatSatoshis(input.value)}
- )}
-
- ))
- )}
-
-
-
- {/* Outputs */}
-
- Outputs
-
- {resolvedOutputs.length === 0 ? (
- No outputs
- ) : (
- resolvedOutputs.map((output, index) => (
-
-
- {index + 1}. {output.value !== undefined ? formatSatoshis(output.value) : '(pending)'}
- {output.outputIdentifier && (
- [{output.outputIdentifier}]
- )}
-
- {output.lockingBytecode !== '(pending)' ? formatHex(output.lockingBytecode, 20) : '(pending)'}
- {(output as any).resolvedFrom && (
- (from ${(output as any).resolvedFrom})
- )}
-
- ))
- )}
-
-
-
-
- {/* Actions */}
-
- Actions
-
- {actionItems.map((item, index) => (
-
- {index === selectedActionIndex && focusedPanel === 'actions' ? '▸ ' : ' '}
- {item.label}
-
- ))}
-
-
-
- {/* Help text */}
-
-
- Tab: Switch focus • Enter: Select • Esc: Back
-
-
-
- {/* Broadcast confirmation dialog */}
- {showBroadcastConfirm && (
-
- setShowBroadcastConfirm(false)}
- />
-
- )}
-
- );
-}