Tests. Autocomplete. Few Fixes. Mocks for Electrum Service. Template-to-Json parser. Fix global paths. Use IO Dependency injection for logging from cli. Additional commands in CLI.
This commit is contained in:
@@ -16,6 +16,8 @@ import { colors, logo } from '../theme.js';
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { createMnemonicFile } from '../../cli/mnemonic.js';
|
||||
import { getMnemonicsDir } from '../../utils/paths.js';
|
||||
import { BCHMnemonicURL } from '../../utils/bch-mnemonic-url.js';
|
||||
import { encodeBip39Mnemonic } from '@bitauth/libauth';
|
||||
|
||||
@@ -39,33 +41,41 @@ interface MnemonicFileEntry {
|
||||
* Focus sections the user can tab between.
|
||||
* When saved wallets exist the file list is shown first.
|
||||
*/
|
||||
type FocusSection = 'files' | 'input' | 'button';
|
||||
type FocusSection = 'files' | 'input' | 'saveCheckbox' | 'button';
|
||||
|
||||
/**
|
||||
* Reads mnemonic-* files from cwd, parses each as a BCHMnemonicURL,
|
||||
* and converts the entropy back to a BIP39 English mnemonic phrase.
|
||||
* Reads mnemonic-* files from ~/.config/xo-cli/mnemonics/ (same as xo-cli),
|
||||
* then from cwd for legacy installs. Parses each as a BCHMnemonicURL.
|
||||
*/
|
||||
function loadMnemonicFiles(): MnemonicFileEntry[] {
|
||||
const cwd = process.cwd();
|
||||
const filenames = fs.readdirSync(cwd).filter((f) => f.startsWith('mnemonic-'));
|
||||
const dirs = [getMnemonicsDir(), process.cwd()];
|
||||
const seenBasenames = new Set<string>();
|
||||
const entries: MnemonicFileEntry[] = [];
|
||||
|
||||
for (const filename of filenames) {
|
||||
try {
|
||||
const content = fs.readFileSync(path.join(cwd, filename), 'utf-8').trim();
|
||||
const parsed = BCHMnemonicURL.fromURL(content);
|
||||
const raw = parsed.toObject();
|
||||
for (const dir of dirs) {
|
||||
if (!fs.existsSync(dir)) continue;
|
||||
const filenames = fs
|
||||
.readdirSync(dir)
|
||||
.filter((f) => f.startsWith('mnemonic-'));
|
||||
for (const filename of filenames) {
|
||||
if (seenBasenames.has(filename)) continue;
|
||||
try {
|
||||
const content = fs.readFileSync(path.join(dir, filename), 'utf-8').trim();
|
||||
const parsed = BCHMnemonicURL.fromURL(content);
|
||||
const raw = parsed.toObject();
|
||||
|
||||
const mnemonicResult = encodeBip39Mnemonic(raw.entropy);
|
||||
if (typeof mnemonicResult === 'string') continue;
|
||||
const mnemonicResult = encodeBip39Mnemonic(raw.entropy);
|
||||
if (typeof mnemonicResult === 'string') continue;
|
||||
|
||||
/** Use the URL comment as the label, falling back to a cleaned-up filename. */
|
||||
const label = raw.comment
|
||||
?? filename.replace(/^mnemonic-/, '').replace(/\.[^.]+$/, '');
|
||||
/** Use the URL comment as the label, falling back to a cleaned-up filename. */
|
||||
const label = raw.comment
|
||||
?? filename.replace(/^mnemonic-/, '').replace(/\.[^.]+$/, '');
|
||||
|
||||
entries.push({ filename, label, mnemonic: mnemonicResult.phrase });
|
||||
} catch {
|
||||
// Skip files that can't be parsed
|
||||
entries.push({ filename, label, mnemonic: mnemonicResult.phrase });
|
||||
seenBasenames.add(filename);
|
||||
} catch {
|
||||
// Skip files that can't be parsed
|
||||
}
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
@@ -91,6 +101,9 @@ export function SeedInputScreen(): React.ReactElement {
|
||||
const [mnemonicFiles, setMnemonicFiles] = useState<MnemonicFileEntry[]>([]);
|
||||
const [selectedFileIndex, setSelectedFileIndex] = useState(0);
|
||||
|
||||
/** When set, manual seed is written to ~/.config/xo-cli/mnemonics/ after a successful unlock. */
|
||||
const [saveMnemonicChecked, setSaveMnemonicChecked] = useState(false);
|
||||
|
||||
// Focus: when saved wallets exist default to the file list, otherwise the input.
|
||||
const [focusedSection, setFocusedSection] = useState<FocusSection>('input');
|
||||
|
||||
@@ -104,8 +117,8 @@ export function SeedInputScreen(): React.ReactElement {
|
||||
* The ordered list of focusable sections (files section only when entries exist).
|
||||
*/
|
||||
const focusSections: FocusSection[] = mnemonicFiles.length > 0
|
||||
? ['files', 'input', 'button']
|
||||
: ['input', 'button'];
|
||||
? ['files', 'input', 'saveCheckbox', 'button']
|
||||
: ['input', 'saveCheckbox', 'button'];
|
||||
|
||||
/**
|
||||
* Shows a status message with the given type.
|
||||
@@ -118,28 +131,46 @@ export function SeedInputScreen(): React.ReactElement {
|
||||
/**
|
||||
* Shared wallet initialization handler used by both manual entry and file selection.
|
||||
*/
|
||||
const doInitialize = useCallback(async (seed: string) => {
|
||||
showStatus('Initializing wallet...', 'loading');
|
||||
setStatus('Initializing wallet...');
|
||||
setIsSubmitting(true);
|
||||
const doInitialize = useCallback(
|
||||
async (seed: string, options?: { saveMnemonic?: boolean }) => {
|
||||
showStatus('Initializing wallet...', 'loading');
|
||||
setStatus('Initializing wallet...');
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
await initializeWallet(seed);
|
||||
try {
|
||||
await initializeWallet(seed);
|
||||
|
||||
showStatus('Wallet initialized successfully!', 'success');
|
||||
setStatus('Wallet ready');
|
||||
setSeedPhrase('');
|
||||
let statusText = 'Wallet initialized successfully!';
|
||||
if (options?.saveMnemonic) {
|
||||
try {
|
||||
const savedAs = createMnemonicFile(getMnemonicsDir(), seed);
|
||||
setMnemonicFiles(loadMnemonicFiles());
|
||||
statusText = `Wallet initialized! Mnemonic saved as ${savedAs}`;
|
||||
} catch (saveErr) {
|
||||
const saveMsg =
|
||||
saveErr instanceof Error ? saveErr.message : String(saveErr);
|
||||
statusText = `Wallet initialized, but could not save mnemonic: ${saveMsg}`;
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
navigate('wallet');
|
||||
}, 500);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to initialize wallet';
|
||||
showStatus(message, 'error');
|
||||
setStatus('Initialization failed');
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}, [initializeWallet, navigate, showStatus, setStatus]);
|
||||
showStatus(statusText, 'success');
|
||||
setStatus('Wallet ready');
|
||||
setSeedPhrase('');
|
||||
setSaveMnemonicChecked(false);
|
||||
|
||||
setTimeout(() => {
|
||||
navigate('wallet');
|
||||
}, 500);
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : 'Failed to initialize wallet';
|
||||
showStatus(message, 'error');
|
||||
setStatus('Initialization failed');
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
},
|
||||
[initializeWallet, navigate, showStatus, setStatus],
|
||||
);
|
||||
|
||||
/**
|
||||
* Handles manual seed phrase submission with validation.
|
||||
@@ -158,8 +189,8 @@ export function SeedInputScreen(): React.ReactElement {
|
||||
return;
|
||||
}
|
||||
|
||||
await doInitialize(seed);
|
||||
}, [seedPhrase, doInitialize, showStatus]);
|
||||
await doInitialize(seed, { saveMnemonic: saveMnemonicChecked });
|
||||
}, [seedPhrase, saveMnemonicChecked, doInitialize, showStatus]);
|
||||
|
||||
/**
|
||||
* Handles selecting a mnemonic file from the list.
|
||||
@@ -186,6 +217,14 @@ export function SeedInputScreen(): React.ReactElement {
|
||||
return;
|
||||
}
|
||||
|
||||
// Space or Enter toggles "save mnemonic" when that row is focused
|
||||
if (focusedSection === 'saveCheckbox') {
|
||||
if (_input === ' ' || key.return) {
|
||||
setSaveMnemonicChecked((v) => !v);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Arrow keys inside the file list
|
||||
if (focusedSection === 'files' && mnemonicFiles.length > 0) {
|
||||
if (key.upArrow) {
|
||||
@@ -319,6 +358,32 @@ export function SeedInputScreen(): React.ReactElement {
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Save mnemonic checkbox (manual entry only; applies on Continue) */}
|
||||
<Box
|
||||
marginTop={1}
|
||||
paddingX={1}
|
||||
borderStyle='single'
|
||||
borderColor={
|
||||
focusedSection === 'saveCheckbox' ? colors.focus : colors.borderMuted
|
||||
}
|
||||
>
|
||||
<Text
|
||||
color={focusedSection === 'saveCheckbox' ? colors.focus : colors.text}
|
||||
bold={focusedSection === 'saveCheckbox'}
|
||||
>
|
||||
{saveMnemonicChecked ? '[x] ' : '[ ] '}
|
||||
</Text>
|
||||
<Text color={colors.text}>Save this mnemonic</Text>
|
||||
<Text color={colors.textMuted}> (~/.config/xo-cli/mnemonics/)</Text>
|
||||
</Box>
|
||||
{focusedSection === 'saveCheckbox' && (
|
||||
<Box marginTop={0} paddingX={1}>
|
||||
<Text color={colors.textMuted} dimColor>
|
||||
Space / Enter: toggle
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Status message */}
|
||||
<Box marginTop={1} height={1}>
|
||||
{statusMessage && (
|
||||
@@ -345,7 +410,7 @@ export function SeedInputScreen(): React.ReactElement {
|
||||
{/* Help text */}
|
||||
<Box marginTop={2}>
|
||||
<Text color={colors.textMuted} dimColor>
|
||||
Tab: navigate sections • Enter: submit • Esc: back
|
||||
Tab: navigate • Enter: submit, load wallet, or toggle save • Space: toggle save • Esc: back
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
Reference in New Issue
Block a user