Files
com.blitzwallet/context-store/activeAccount.js
T
Blake KaufmanandGitHub 8676db8e16 Remove txs from memory (#849)
* memorizing context values

* reduce stored txs in memory

* improve db lookup

* dismiss keyboard before going back
2026-05-18 11:16:12 -04:00

605 lines
18 KiB
JavaScript

import React, {
createContext,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import {
getLocalStorageItem,
retrieveData,
setLocalStorageItem,
} from '../app/functions';
import {
CUSTODY_ACCOUNTS_STORAGE_KEY,
NWC_SECURE_STORE_MNEMOINC,
MAX_DERIVED_ACCOUNTS,
} from '../app/constants';
import { useKeysContext } from './keys';
import {
decryptMnemonic,
encryptMnemonic,
} from '../app/functions/handleMnemonic';
import { useGlobalContextProvider } from './context';
import { useAuthContext } from './authContext';
import { deriveAccountMnemonic } from '../app/functions/accounts/derivedAccounts';
import customUUID from '../app/functions/customUUID';
import isValidMnemonic from '../app/functions/isValidMnemonic';
import { useAppStatus } from './appStatus';
import { useTranslation } from 'react-i18next';
export const MAIN_ACCOUNT_UUID = 'MW09xd09d8f0a9sf2n332';
export const NWC_ACCOUNT_UUID = 'NWC038rsd0f8234ajsf';
// Create a context for the WebView ref
const ActiveCustodyAccount = createContext(null);
export const ActiveCustodyAccountProvider = ({ children }) => {
const { masterInfoObject, toggleMasterInfoObject } =
useGlobalContextProvider();
const { didGetToHomepage } = useAppStatus();
const { authResetkey } = useAuthContext();
const { t } = useTranslation();
const [custodyAccounts, setCustodyAccounts] = useState([]);
const [isUsingNostr, setIsUsingNostr] = useState(false);
const { accountMnemoinc } = useKeysContext();
const [nostrSeed, setNostrSeed] = useState('');
const [activeDerivedMnemonic, setActiveDerivedMnemonic] = useState(null);
const hasSessionReset = useRef(false);
const hasAutoRestoreCheckRun = useRef(false);
const selectedAltAccount = custodyAccounts.filter(item => item.isActive);
const didSelectAltAccount = !!selectedAltAccount.length;
const isInitialRender = useRef(true);
const enabledNWC = masterInfoObject.didViewNWCMessage;
const currentPins = masterInfoObject.pinnedAccounts || [];
useEffect(() => {
if (nostrSeed.length || !enabledNWC) return;
async function getNostrSeed() {
const NWCMnemoinc = (await retrieveData(NWC_SECURE_STORE_MNEMOINC)).value;
if (!NWCMnemoinc) return;
setNostrSeed(NWCMnemoinc);
}
getNostrSeed();
}, [nostrSeed, enabledNWC]);
const toggleIsUsingNostr = value => {
setIsUsingNostr(value);
};
useEffect(() => {
async function initializeAccouts() {
try {
const accoutList = await getLocalStorageItem(
CUSTODY_ACCOUNTS_STORAGE_KEY,
).then(data => JSON.parse(data) || []);
const decryptedList = accoutList.map(item =>
JSON.parse(decryptMnemonic(item, accountMnemoinc)),
);
setCustodyAccounts(decryptedList);
} catch (err) {
console.log('Custody account intialization error', err);
}
}
console.log('Initializing accounts....');
if (!accountMnemoinc) return;
initializeAccouts();
}, [accountMnemoinc]);
// Clear active account once per session to sync with default accountMnemonic
useEffect(() => {
if (!custodyAccounts.length || hasSessionReset.current || !accountMnemoinc)
return;
async function clearActiveAccountsOnSessionStart() {
try {
const hasActiveAccounts = custodyAccounts.some(
account => account.isActive,
);
if (hasActiveAccounts) {
console.log('Clearing active accounts for session sync...');
const clearedAccounts = custodyAccounts.map(account => ({
...account,
isActive: false,
}));
setLocalStorageItem(
CUSTODY_ACCOUNTS_STORAGE_KEY,
JSON.stringify(encriptAccountsList(clearedAccounts)),
);
setCustodyAccounts(clearedAccounts);
}
hasSessionReset.current = true;
} catch (err) {
console.log('Session reset error', err);
hasSessionReset.current = true;
}
}
clearActiveAccountsOnSessionStart();
}, [custodyAccounts, accountMnemoinc]);
const encriptAccountsList = custodyAccounts => {
return custodyAccounts.map(item =>
encryptMnemonic(JSON.stringify(item), accountMnemoinc),
);
};
const removeAccount = async account => {
try {
let accountInformation = JSON.parse(JSON.stringify(custodyAccounts));
let newAccounts = accountInformation.filter(accounts => {
return accounts.uuid !== account.uuid;
});
const isPinned = currentPins.includes(account.uuid);
if (isPinned) {
// clear from pinned list
toggleMasterInfoObject({
pinnedAccounts: currentPins.filter(id => id !== account.uuid),
});
}
// clear spark information here too. Delte txs from database, reove listeners
await setLocalStorageItem(
CUSTODY_ACCOUNTS_STORAGE_KEY,
JSON.stringify(encriptAccountsList(newAccounts)),
);
setCustodyAccounts(newAccounts);
return { didWork: true };
} catch (err) {
console.log('Remove account error', err);
return { didWork: false, err: err.message };
}
};
const createAccount = async accountInformation => {
try {
let savedAccountInformation = JSON.parse(JSON.stringify(custodyAccounts));
savedAccountInformation.push(accountInformation);
await setLocalStorageItem(
CUSTODY_ACCOUNTS_STORAGE_KEY,
JSON.stringify(encriptAccountsList(savedAccountInformation)),
);
setCustodyAccounts(savedAccountInformation);
return { didWork: true };
} catch (err) {
console.log('Create custody account error', err);
return { didWork: false, err: err.message };
}
};
const updateAccount = async account => {
try {
let accountInformation = JSON.parse(JSON.stringify(custodyAccounts));
let newAccounts = accountInformation.map(accounts => {
if (account.uuid === accounts.uuid) {
return { ...accounts, ...account };
} else return accounts;
});
await setLocalStorageItem(
CUSTODY_ACCOUNTS_STORAGE_KEY,
JSON.stringify(encriptAccountsList(newAccounts)),
);
setCustodyAccounts(newAccounts);
return { didWork: true };
} catch (err) {
console.log('Remove account error', err);
return { didWork: false, err: err.message };
}
};
const updateAccountCacheOnly = async account => {
try {
if (!account) throw new Error('No account selected');
let accountInformation = JSON.parse(JSON.stringify(custodyAccounts));
let newAccounts = accountInformation.map(accounts => {
if (account.uuid === accounts.uuid) {
return { ...accounts, ...account };
} else return { ...accounts, isActive: false };
});
if (account.isActive && typeof account.derivationIndex === 'number') {
const derivedMnemonic = await deriveAccountMnemonic(
accountMnemoinc,
account.derivationIndex,
);
setActiveDerivedMnemonic(derivedMnemonic);
} else {
setActiveDerivedMnemonic(null);
}
setCustodyAccounts(newAccounts);
return { didWork: true };
} catch (err) {
console.log('Remove account error', err);
return { didWork: false, err: err.message };
}
};
const createDerivedAccount = async accountName => {
try {
const nextCloudIndex = masterInfoObject.nextAccountDerivationIndex || 3;
const nextIndex = nextCloudIndex + 1;
// Enforce hard cap to prevent overlap with gifts range (starts at index 1000)
if (nextIndex >= MAX_DERIVED_ACCOUNTS) {
return {
didWork: false,
error: `Maximum of ${MAX_DERIVED_ACCOUNTS} accounts reached. Please delete unused accounts.`,
};
}
// Don't store the mnemonic, just metadata
const accountInfo = {
uuid: customUUID(),
name: accountName,
derivationIndex: nextIndex,
dateCreated: Date.now(),
isActive: false,
accountType: 'derived',
profileEmoji: '',
};
await createAccount(accountInfo);
// Update masterInfoObject with new index (automatically syncs to Firebase)
await toggleMasterInfoObject({
nextAccountDerivationIndex: nextIndex,
});
return { didWork: true };
} catch (err) {
console.log('Create derived account error', err);
return { didWork: false, error: err.message };
}
};
const createImportedAccount = async (accountName, importedSeed) => {
try {
if (!importedSeed || typeof importedSeed !== 'string') {
return { didWork: false, error: 'Invalid seed provided' };
}
const words = importedSeed
.trim()
.toLowerCase()
.split(/\s+/)
.filter(Boolean);
if (words.length !== 12 || !isValidMnemonic(words)) {
return {
didWork: false,
error: 'Seed must be a valid 12-word recovery phrase',
};
}
const accountInfo = {
uuid: customUUID(),
name: accountName,
mnemoinc: words.join(' '),
dateCreated: Date.now(),
isActive: false,
accountType: 'imported',
profileEmoji: '',
};
await createAccount(accountInfo);
// NO cloud backup for imported accounts (contains sensitive seed)
return { didWork: true };
} catch (err) {
console.log('Create imported account error', err);
return { didWork: false, error: err.message };
}
};
const restoreDerivedAccount = async (accountName, derivationIndex) => {
try {
// Validation #1: Type check
if (
typeof derivationIndex !== 'number' ||
!Number.isInteger(derivationIndex)
) {
return {
didWork: false,
error: 'Derivation index must be a whole number',
};
}
// Validation #2: Range check (minimum)
if (derivationIndex < 3) {
return {
didWork: false,
error:
'Derivation index must be 3 or higher (indices 0-2 are reserved)',
};
}
// Validation #3: Range check (maximum - gifts boundary)
if (derivationIndex >= MAX_DERIVED_ACCOUNTS) {
return {
didWork: false,
error: `Derivation index must be less than ${MAX_DERIVED_ACCOUNTS} (gift wallet range)`,
};
}
// Validation #4: Check against nextAccountDerivationIndex
const nextCloudIndex = masterInfoObject.nextAccountDerivationIndex || 3;
if (derivationIndex > nextCloudIndex) {
return {
didWork: false,
error: `Cannot restore index ${derivationIndex}. Highest created account is ${
nextCloudIndex - 1
}`,
};
}
// Validation #5: Check if account already exists (idempotency)
const existingAccount = custodyAccounts.find(
acc => acc.derivationIndex === derivationIndex,
);
if (existingAccount) {
return {
didWork: false,
error: `Account at index ${derivationIndex} already exists: "${existingAccount.name}"`,
};
}
// Create account with EXACT same structure as auto-restore
const accountInfo = {
uuid: customUUID(),
name: accountName,
derivationIndex: derivationIndex,
dateCreated: Date.now(),
isActive: false,
accountType: 'derived',
profileEmoji: '',
};
await createAccount(accountInfo);
// CRITICAL: Do NOT update nextAccountDerivationIndex
// This is a restoration of an existing index, not a new sequential account
return { didWork: true };
} catch (err) {
console.log('Restore derived account error', err);
return { didWork: false, error: err.message };
}
};
const getAccountMnemonic = async account => {
try {
if (!account) throw new Error('No account provided');
// For derived accounts, re-derive on demand from main seed
if (account.derivationIndex !== undefined) {
const derivedMnemonic = await deriveAccountMnemonic(
accountMnemoinc,
account.derivationIndex,
);
return derivedMnemonic;
}
// For imported accounts, return stored mnemonic
return account.mnemoinc;
} catch (err) {
console.log('Get account mnemonic error', err);
throw err;
}
};
const restoreDerivedAccountsFromCloud = async () => {
try {
// masterInfoObject is already loaded from Firebase by GlobalContextProvider
const nextIndex = Number(
masterInfoObject.nextAccountDerivationIndex || 3,
);
if (!nextIndex || nextIndex === 0) {
console.log('No derived accounts to restore');
return { didWork: true, accountsRestored: 0 };
}
const existingDerivedIndexes = new Set(
custodyAccounts
.map(account => account.derivationIndex)
.filter(index => typeof index === 'number'),
);
const accountsToRestore = [];
for (let i = 4; i <= nextIndex; i++) {
if (existingDerivedIndexes.has(i)) continue;
accountsToRestore.push({
uuid: customUUID(),
name: t('accountCard.fallbackAccountName', { index: i }),
derivationIndex: i,
dateCreated: Date.now(),
accountType: 'derived',
isActive: false,
profileEmoji: '',
});
}
if (accountsToRestore.length) {
const mergedAccounts = [...custodyAccounts, ...accountsToRestore];
await setLocalStorageItem(
CUSTODY_ACCOUNTS_STORAGE_KEY,
JSON.stringify(encriptAccountsList(mergedAccounts)),
);
setCustodyAccounts(mergedAccounts);
}
console.log(`Restored ${accountsToRestore.length} derived account(s)`);
return { didWork: true, accountsRestored: accountsToRestore.length };
} catch (err) {
console.log('Restore derived accounts error', err);
return { didWork: false, error: err.message };
}
};
useEffect(() => {
async function restoreIfNeeded() {
const cloudIndex = masterInfoObject?.nextAccountDerivationIndex;
const hasRunRestore = await getLocalStorageItem('hasRunAutoRestore').then(
data => JSON.parse(data),
);
if (hasAutoRestoreCheckRun.current) return;
if (!accountMnemoinc) return;
if (cloudIndex === undefined) return;
if (Number(cloudIndex) <= 0) return;
if (!didGetToHomepage) return;
if (hasRunRestore) return;
if (custodyAccounts.length > 0) {
hasAutoRestoreCheckRun.current = true;
await setLocalStorageItem('hasRunAutoRestore', JSON.stringify(true));
return;
}
console.log('Running auto-restore of derived accounts from cloud...');
hasAutoRestoreCheckRun.current = true;
await setLocalStorageItem('hasRunAutoRestore', JSON.stringify(true));
await restoreDerivedAccountsFromCloud();
}
restoreIfNeeded();
}, [accountMnemoinc, custodyAccounts, masterInfoObject, didGetToHomepage]);
useEffect(() => {
if (isInitialRender.current) {
isInitialRender.current = false;
return;
}
setNostrSeed('');
setIsUsingNostr(false);
setActiveDerivedMnemonic(null);
setCustodyAccounts([]);
hasSessionReset.current = false;
hasAutoRestoreCheckRun.current = false;
}, [authResetkey]);
const currentWalletMnemoinc = useMemo(() => {
if (didSelectAltAccount) {
const activeAccount = selectedAltAccount[0];
// For derived accounts, we'll need to derive the mnemonic
// But for backwards compatibility, check if mnemoinc exists first
if (activeAccount.mnemoinc) {
return activeAccount.mnemoinc; // Imported account
}
return activeDerivedMnemonic || accountMnemoinc;
} else if (isUsingNostr) {
return nostrSeed;
} else {
return accountMnemoinc;
}
}, [
accountMnemoinc,
selectedAltAccount,
didSelectAltAccount,
isUsingNostr,
nostrSeed,
activeDerivedMnemonic,
]);
const isUsingAltAccount = didSelectAltAccount || isUsingNostr;
const custodyAccountsList = useMemo(() => {
return enabledNWC
? [
{
name: t('settings.accounts.mainWalletPlace'),
mnemoinc: accountMnemoinc,
accountType: 'main',
uuid: MAIN_ACCOUNT_UUID,
},
{
name: t('settings.accounts.nwcWalletPlace'),
mnemoinc: nostrSeed,
accountType: 'nwc',
uuid: NWC_ACCOUNT_UUID,
},
...custodyAccounts,
]
: [
{
name: t('settings.accounts.mainWalletPlace'),
mnemoinc: accountMnemoinc,
accountType: 'main',
uuid: MAIN_ACCOUNT_UUID,
},
...custodyAccounts,
];
}, [accountMnemoinc, custodyAccounts, enabledNWC, nostrSeed, t]);
const activeAccount = useMemo(() => {
const activeAltAccount = selectedAltAccount[0];
return custodyAccountsList.find(account => {
const isMainWallet = account.uuid === MAIN_ACCOUNT_UUID;
const isNWC = account.uuid === NWC_ACCOUNT_UUID;
const isActive = isNWC
? isUsingNostr
: isMainWallet
? !activeAltAccount && !isUsingNostr
: activeAltAccount?.uuid === account.uuid;
return isActive;
});
}, [custodyAccountsList, isUsingNostr, selectedAltAccount]);
const accountValues = useMemo(() => {
return {
custodyAccounts,
removeAccount,
createAccount,
updateAccount,
updateAccountCacheOnly,
createDerivedAccount,
createImportedAccount,
restoreDerivedAccount,
getAccountMnemonic,
restoreDerivedAccountsFromCloud,
selectedAltAccount,
isUsingAltAccount,
currentWalletMnemoinc,
toggleIsUsingNostr,
isUsingNostr,
nostrSeed,
activeAccount,
custodyAccountsList,
};
}, [
custodyAccounts,
removeAccount,
createAccount,
updateAccount,
updateAccountCacheOnly,
createDerivedAccount,
createImportedAccount,
restoreDerivedAccount,
getAccountMnemonic,
restoreDerivedAccountsFromCloud,
selectedAltAccount,
isUsingAltAccount,
currentWalletMnemoinc,
toggleIsUsingNostr,
isUsingNostr,
nostrSeed,
activeAccount,
custodyAccountsList,
]);
return (
<ActiveCustodyAccount.Provider value={accountValues}>
{children}
</ActiveCustodyAccount.Provider>
);
};
export const useActiveCustodyAccount = () => {
return React.useContext(ActiveCustodyAccount);
};