diff --git a/src/storage.ts b/src/storage.ts index 2080047..1a88046 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -8,11 +8,17 @@ type Address = { address: string; }; +type MultisigHistoryEntry = { + network: NetworkChoice; + address: string; +}; + type SafelyStorage = { addresses: Address[]; multisig?: string; network?: NetworkChoice; profile?: string; + multisigHistory?: MultisigHistoryEntry[]; }; // Config path: ~/.safely/config.json @@ -24,6 +30,7 @@ const defaultData: SafelyStorage = { multisig: undefined, network: undefined, profile: undefined, + multisigHistory: [], }; // Ensure config directory exists @@ -157,3 +164,41 @@ export const ensureMultisigAddressExists = createEnsure( export const ensureNetworkExists = createEnsure(NetworkDefault, 'No network provided'); export const ensureProfileExists = createEnsure(ProfileDefault, 'No profile provided'); + +// **Multisig History Operations** +export const MultisigHistory = { + async getAll(): Promise { + return readStorage((config) => config.multisigHistory || []); + }, + + async getForNetwork(network: NetworkChoice): Promise { + return readStorage((config) => { + const history = config.multisigHistory || []; + return history.filter((entry) => entry.network === network).map((entry) => entry.address); + }); + }, + + async add(network: NetworkChoice, address: string) { + await updateStorage((config) => { + if (!config.multisigHistory) { + config.multisigHistory = []; + } + // Dedupe: check if this network+address combo already exists + const exists = config.multisigHistory.some( + (entry) => entry.network === network && entry.address === address + ); + if (!exists) { + config.multisigHistory.push({ network, address }); + } + }); + }, + + async remove(network: NetworkChoice, address: string) { + await updateStorage((config) => { + if (!config.multisigHistory) return; + config.multisigHistory = config.multisigHistory.filter( + (entry) => !(entry.network === network && entry.address === address) + ); + }); + }, +}; diff --git a/src/ui/HomeView.tsx b/src/ui/HomeView.tsx index 5cfea7e..0f6b53b 100644 --- a/src/ui/HomeView.tsx +++ b/src/ui/HomeView.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { Box, Text, useInput, useApp, render } from 'ink'; import TextInput from 'ink-text-input'; import Spinner from 'ink-spinner'; -import { ProfileDefault, MultisigDefault, NetworkDefault } from '../storage.js'; +import { ProfileDefault, MultisigDefault, NetworkDefault, MultisigHistory } from '../storage.js'; import { getAllProfiles, ProfileInfo } from '../profiles.js'; import { NETWORK_CHOICES, NetworkChoice } from '../constants.js'; import { validateAddress } from '../validators.js'; @@ -23,6 +23,7 @@ interface Config { profiles: ProfileInfo[]; multisigOwners: string[]; profileAddress: string | null; + multisigHistory: string[]; } interface MenuState { @@ -44,7 +45,8 @@ const HomeView: React.FC = ({ onNavigate }) => { const [config, setConfig] = useState({ profiles: [], multisigOwners: [], - profileAddress: null + profileAddress: null, + multisigHistory: [] }); const [menu, setMenu] = useState({ selectedIndex: 0, @@ -64,13 +66,15 @@ const HomeView: React.FC = ({ onNavigate }) => { ProfileDefault.get() ]); const owners = network && multisig ? await fetchMultisigOwners(network, multisig) : []; + const multisigHistory = network ? await MultisigHistory.getForNetwork(network) : []; setConfig({ network, multisig, profile, profiles: getAllProfiles(), multisigOwners: owners, - profileAddress: null + profileAddress: null, + multisigHistory }); }; load(); @@ -153,6 +157,9 @@ const HomeView: React.FC = ({ onNavigate }) => { newConfig.profile = undefined; newConfig.multisigOwners = []; newConfig.profileAddress = null; + // Load multisig history for new network + const multisigHistory = await MultisigHistory.getForNetwork(updates.network); + newConfig.multisigHistory = multisigHistory; } if ('multisig' in updates && updates.multisig) { await MultisigDefault.set(updates.multisig); @@ -160,6 +167,11 @@ const HomeView: React.FC = ({ onNavigate }) => { if (config.network) { const owners = await fetchMultisigOwners(config.network, updates.multisig); newConfig.multisigOwners = owners; + // Add to history + await MultisigHistory.add(config.network, updates.multisig); + // Refresh history + const multisigHistory = await MultisigHistory.getForNetwork(config.network); + newConfig.multisigHistory = multisigHistory; } } if ('profile' in updates && updates.profile) { @@ -189,7 +201,11 @@ const HomeView: React.FC = ({ onNavigate }) => { const idx = filteredProfiles.findIndex(p => p.name === config.profile); updates.subIndex = Math.max(0, idx); } else if (item === 'multisig') { - updates.multisigInput = config.multisig || ''; + updates.multisigInput = ''; + updates.isValidating = false; + updates.multisigError = ''; + // Start with input field selected (subIndex = history.length means input field) + updates.subIndex = config.multisigHistory.length; } setMenu(m => ({ ...m, ...updates })); @@ -233,39 +249,49 @@ const HomeView: React.FC = ({ onNavigate }) => { } else if (expandedItem === 'multisig') { if (key.escape) { collapseMenu(); + } else if (key.upArrow) { + setMenu(m => ({ ...m, subIndex: Math.max(0, m.subIndex - 1) })); + } else if (key.downArrow) { + setMenu(m => ({ ...m, subIndex: Math.min(config.multisigHistory.length, m.subIndex + 1) })); } else if (key.return) { - // Validate and save multisig address - (async () => { - try { - validateAddress(menu.multisigInput); - setMenu(m => ({ ...m, isValidating: true, multisigError: '' })); - - // Check if address has MultisigAccount resource - const aptos = initAptos(config.network!); + // If on input field (subIndex == history.length), validate and save + if (subIndex === config.multisigHistory.length) { + (async () => { try { - const resource = await aptos.getAccountResource({ - accountAddress: menu.multisigInput, - resourceType: '0x1::multisig_account::MultisigAccount' - }); - // Successfully got the resource, it's a valid multisig - await updateConfig({ multisig: menu.multisigInput, multisigOwners: resource.owners }); - collapseMenu(); - } catch (resourceError) { - // Resource doesn't exist or error fetching + validateAddress(menu.multisigInput); + setMenu(m => ({ ...m, isValidating: true, multisigError: '' })); + + // Check if address has MultisigAccount resource + const aptos = initAptos(config.network!); + try { + const resource = await aptos.getAccountResource({ + accountAddress: menu.multisigInput, + resourceType: '0x1::multisig_account::MultisigAccount' + }); + // Successfully got the resource, it's a valid multisig + await updateConfig({ multisig: menu.multisigInput, multisigOwners: resource.owners }); + collapseMenu(); + } catch (resourceError) { + // Resource doesn't exist or error fetching + setMenu(m => ({ + ...m, + multisigError: 'Address is not a multisig account', + isValidating: false + })); + } + } catch (error) { setMenu(m => ({ ...m, - multisigError: 'Address is not a multisig account', + multisigError: String(error).replace('Error: ', ''), isValidating: false })); } - } catch (error) { - setMenu(m => ({ - ...m, - multisigError: String(error).replace('Error: ', ''), - isValidating: false - })); - } - })(); + })(); + } else if (config.multisigHistory[subIndex]) { + // Select from history + updateConfig({ multisig: config.multisigHistory[subIndex] }); + collapseMenu(); + } } } else { // Main menu navigation @@ -340,6 +366,9 @@ const HomeView: React.FC = ({ onNavigate }) => { isSelected={selectedIndex === 1} isValidating={menu.isValidating} onInputChange={value => setMenu(m => ({ ...m, multisigInput: value }))} + multisigHistory={config.multisigHistory} + currentMultisig={config.multisig} + subIndex={subIndex} /> ) : ( void; -}> = ({ input, error, isSelected, isValidating, onInputChange }) => ( - <> - {isSelected ? '▼' : ' '} Multisig: - - Address: - {isValidating ? ( - - Validating... + multisigHistory: string[]; + currentMultisig?: string; + subIndex: number; +}> = ({ input, error, isSelected, isValidating, onInputChange, multisigHistory, currentMultisig, subIndex }) => { + const isInputSelected = subIndex === multisigHistory.length; + + return ( + <> + {isSelected ? '▼' : ' '} Multisig: + {multisigHistory.map((address, index) => ( + + {' '}{index === subIndex ? '▶' : ' '} {address.slice(0, 10)}...{address.slice(-6)} + {address === currentMultisig && } - ) : ( - - )} - - {error && ✗ {error}} - {!isValidating && [Enter] Save | [Esc] Cancel} - -); + ))} + + {isInputSelected ? '▶' : ' '} New: + {isValidating ? ( + + Validating... + + ) : ( + + )} + + {error && ✗ {error}} + + ); +}; // Component: Profile Expanded const ProfileExpanded: React.FC<{