From df008b5760a67551a3e422b255a6727613383795 Mon Sep 17 00:00:00 2001 From: First-Terraner Date: Mon, 1 Apr 2024 20:50:11 +0200 Subject: [PATCH] add screen component for keyset selection --- src/components/hooks/Restore.tsx | 157 --------------------------- src/components/nav/Navigator.tsx | 6 +- src/model/nav.ts | 10 +- src/screens/Restore/Recovering.tsx | 137 ++++++++++++++++++++--- src/screens/Restore/SelectKeyset.tsx | 53 +++++++++ src/screens/Restore/Success.tsx | 62 ++++------- 6 files changed, 207 insertions(+), 218 deletions(-) delete mode 100644 src/components/hooks/Restore.tsx create mode 100644 src/screens/Restore/SelectKeyset.tsx diff --git a/src/components/hooks/Restore.tsx b/src/components/hooks/Restore.tsx deleted file mode 100644 index eeafdeec..00000000 --- a/src/components/hooks/Restore.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import type { CashuWallet, MintKeys, Proof } from '@cashu/cashu-ts' -import { RESTORE_INTERVAL, RESTORE_OVERSHOOT } from '@consts/mints' -import { addToken, getMintBalance } from '@db' -import { l } from '@log' -import type { RootStackParamList } from '@model/nav' -import { type NavigationProp, useNavigation } from '@react-navigation/core' -import { usePromptContext } from '@src/context/Prompt' -import { NS } from '@src/i18n' -import { addToHistory } from '@store/latestHistoryEntries' -import { saveSeed } from '@store/restore' -import { isErr } from '@util' -import { _setKeys, getCounterByMintUrl, getSeedWalletByMnemonic, incrementCounterByMintUrl } from '@wallet' -import { useEffect, useState } from 'react' -import { useTranslation } from 'react-i18next' - -type StackNavigation = NavigationProp - -type TRestoreInterval = Promise<{ proofs: Proof[]; newKeys?: MintKeys; start: number; lastCount: number } | undefined> - -interface IUseRestoreProps { - from: number - to: number - mintUrl: string - keysetId: string - mnemonic: string - comingFromOnboarding?: boolean - shouldOvershoot?: boolean -} - -export function useRestore({ from, to, mintUrl, keysetId, mnemonic, comingFromOnboarding, shouldOvershoot }: IUseRestoreProps) { - - const navigation = useNavigation() - const { t } = useTranslation([NS.common]) - const { openPromptAutoClose } = usePromptContext() - - const [restored, setRestored] = useState({ - proofs: [] as Proof[], - start: from, - end: to, - overshoot: 0, - }) - - const resetRestoredState = () => { - setRestored({ - proofs: [] as Proof[], - start: from, - end: to, - overshoot: 0, - }) - } - - const restore = async () => { - try { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const { proofs, start, end } = await restoreWallet(mintUrl, mnemonic) - if (!proofs?.length) { - openPromptAutoClose({ msg: t('noProofsRestored'), success: false }) - resetRestoredState() - if (comingFromOnboarding) { - return navigation.navigate('auth', { pinHash: '' }) - } - return navigation.navigate('dashboard') - } - const bal = await getMintBalance(mintUrl) - await addToHistory({ - mints: [mintUrl], - amount: bal, - type: 4, - value: '', - }) - navigation.navigate('restoreSuccess', { - mnemonic, - mint: mintUrl, - keysetID: keysetId, - cycle: { start, end }, - amount: bal, - comingFromOnboarding, - }) - } catch (e) { - l('[handleRecovery] error: ', e) - resetRestoredState() - navigation.navigate('processingError', { - errorMsg: isErr(e) ? e.message : t('restoreErr'), - comingFromOnboarding, - }) - } - } - - const restoreWallet = async (mintUrl: string, mnemonic: string) => { - try { - const { wallet, seed } = await getSeedWalletByMnemonic({ mintUrl, mnemonic }) - const resp = await restoreInterval(wallet, from, to) - if (!resp) { - l('[restoreWallet] restore interval did not return a proper object!') - throw new Error('[restoreWallet] restore interval did not return a proper object!') - } - const { proofs, newKeys, start, lastCount } = resp - await saveSeed(seed) - // adds counter if not exists - await getCounterByMintUrl(mintUrl) - if (!proofs.length) { - l('[restoreWallet] no proofs found during the restore process!') - return { proofs: [], start: from, end: to } - } - const proofsSpent = await wallet.checkProofsSpent(proofs) - const proofsUnspent = proofs.filter(p => !proofsSpent.map(x => x.secret).includes(p.secret)) - if (newKeys) { _setKeys(mintUrl, newKeys) } - await addToken({ token: [{ mint: mintUrl, proofs: proofsUnspent }] }) - await incrementCounterByMintUrl(mintUrl, lastCount + 1) - return { proofs: proofsUnspent, start, end: lastCount } - } catch (e) { - l('[restoreWallet] error', { e }) - return { proofs: [], start: from, end: to } - } - } - - const restoreInterval = async ( - wallet: CashuWallet, - start: number, - end: number, - restoredProofs: Proof[] = [], - overshoot: number = 0 - ): TRestoreInterval => { - try { - const { proofs, newKeys } = await wallet.restore(start, end, keysetId) - l('[restoreInterval] restored proofs: ', { from: start, to: end, proofsLength: proofs.length }) - if (proofs.length) { - restoredProofs.push(...proofs) - overshoot = 0 - start += RESTORE_INTERVAL - end += RESTORE_INTERVAL - setRestored({ proofs: restoredProofs, start, end, overshoot }) - return restoreInterval(wallet, start, end, restoredProofs, overshoot) - } - if (shouldOvershoot && overshoot < RESTORE_OVERSHOOT) { - l('[restoreInterval] no proofs to restore! overshooting now: ', { proofsLength: proofs.length, overshoot }) - overshoot++ - start += RESTORE_INTERVAL - end += RESTORE_INTERVAL - setRestored({ proofs: restoredProofs, start, end, overshoot }) - return restoreInterval(wallet, start, end, restoredProofs, overshoot) - } - l('[restoreInterval] no proofs to restore! overshooting limit reached: ', { restoredProofsLength: restoredProofs.length, overshoot }) - return { proofs: restoredProofs, newKeys, start, lastCount: end } - } catch (e) { - l('[restoreInterval] error', { e }) - return { proofs: restoredProofs, newKeys: undefined, start, lastCount: end } - } - } - - useEffect(() => { - void restore() - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [mintUrl, keysetId, from, to]) - - return restored -} diff --git a/src/components/nav/Navigator.tsx b/src/components/nav/Navigator.tsx index 0e08a6c7..e6d135de 100644 --- a/src/components/nav/Navigator.tsx +++ b/src/components/nav/Navigator.tsx @@ -38,8 +38,9 @@ import RecoverScreen from '@screens/Restore/Recover' import RecoveringScreen from '@screens/Restore/Recovering' import RestoreWarningScreen from '@screens/Restore/RestoreWarning' import SeedScreen from '@screens/Restore/Seed' +import SelectKeysetScreen from '@screens/Restore/SelectKeyset' import SelectRecoveryMintScreen from '@screens/Restore/SelectRecoveryMint' -import RestoreSuccess from '@screens/Restore/Success' +import RestoreOverviewScreen from '@screens/Restore/Success' import Settings from '@screens/Settings' import AboutSettings from '@screens/Settings/About' import ContactsSettings from '@screens/Settings/Contacts' @@ -198,7 +199,8 @@ export default function Navigator({ - + + ) diff --git a/src/model/nav.ts b/src/model/nav.ts index f6870f7f..76bb03aa 100644 --- a/src/model/nav.ts +++ b/src/model/nav.ts @@ -246,7 +246,7 @@ export type RootStackParamList = { 'Restore warning': { comingFromOnboarding?: boolean } - restoreSuccess: { + restoreOverview: { mnemonic: string amount: number mint: string @@ -257,6 +257,11 @@ export type RootStackParamList = { } comingFromOnboarding?: boolean } + selectKeyset: { + mnemonic: string + mintUrl: string + comingFromOnboarding?: boolean + } } export type TRouteString = 'dashboard' | 'mints' | 'Address book' | 'Settings' @@ -308,7 +313,8 @@ export type IDerivingPageProps = NativeStackScreenProps export type ISelectRecoveryMintPageProps = NativeStackScreenProps export type IRestoreWarningPageProps = NativeStackScreenProps -export type IRestoreSuccessPageProps = NativeStackScreenProps +export type IRestoreSuccessPageProps = NativeStackScreenProps +export type ISelectKeysetPageProps = NativeStackScreenProps export type TBottomNavProps = TNostrOnboardingPageProps | TDashboardPageProps | diff --git a/src/screens/Restore/Recovering.tsx b/src/screens/Restore/Recovering.tsx index 6cf2fff0..e7c10455 100644 --- a/src/screens/Restore/Recovering.tsx +++ b/src/screens/Restore/Recovering.tsx @@ -1,28 +1,138 @@ -import { useRestore } from '@comps/hooks/Restore' +import type { CashuWallet, MintKeys, Proof } from '@cashu/cashu-ts' import Loading from '@comps/Loading' import Txt from '@comps/Txt' +import { RESTORE_INTERVAL, RESTORE_OVERSHOOT } from '@consts/mints' +import { addToken, getMintBalance } from '@db' +import { l } from '@log' import type { IRecoveringPageProps, TBeforeRemoveEvent } from '@model/nav' import { preventBack } from '@nav/utils' -import { RESTORE_OVERSHOOT } from '@src/consts/mints' import { useThemeContext } from '@src/context/Theme' import { NS } from '@src/i18n' -import { formatSatStr } from '@src/util' +import { saveSeed } from '@store/restore' import { globals, mainColors } from '@styles' -import { useEffect } from 'react' +import { formatSatStr, isErr } from '@util' +import { _setKeys, getCounterByMintUrl, getSeedWalletByMnemonic, incrementCounterByMintUrl } from '@wallet' +import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { View } from 'react-native' import { s, ScaledSheet } from 'react-native-size-matters' +type TRestoreInterval = Promise<{ proofs: Proof[]; newKeys?: MintKeys; start: number; lastCount: number } | undefined> + export default function RecoveringScreen({ navigation, route }: IRecoveringPageProps) { const { from, to, mintUrl, keysetId, mnemonic, comingFromOnboarding, shouldOvershoot } = route.params - const { t } = useTranslation([NS.common]) - // Seed recovery process in useRestore hook - const { proofs, start, end, overshoot } = useRestore({ from, to, mintUrl, keysetId, mnemonic, comingFromOnboarding, shouldOvershoot }) - const { color } = useThemeContext() + const [restored, setRestored] = useState({ + proofs: [] as Proof[], + start: from, + end: to, + overshoot: 0, + }) + + const resetRestoredState = () => { + setRestored({ + proofs: [] as Proof[], + start: from, + end: to, + overshoot: 0, + }) + } + + const restore = async () => { + try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const { start, end } = await restoreWallet(mintUrl, mnemonic) + const bal = await getMintBalance(mintUrl) + // TODO save keysetID as restored: [{ keysetId, amount: bal }, ...] + navigation.navigate('restoreOverview', { + mnemonic, + mint: mintUrl, + keysetID: keysetId, + cycle: { start, end }, + amount: bal, + comingFromOnboarding, + }) + } catch (e) { + l('[handleRecovery] error: ', e) + resetRestoredState() + navigation.navigate('processingError', { + errorMsg: isErr(e) ? e.message : t('restoreErr'), + comingFromOnboarding, + }) + } + } + + const restoreWallet = async (mintUrl: string, mnemonic: string) => { + try { + const { wallet, seed } = await getSeedWalletByMnemonic({ mintUrl, mnemonic }) + const resp = await restoreInterval(wallet, from, to) + if (!resp) { + l('[restoreWallet] restore interval did not return a proper object!') + throw new Error('[restoreWallet] restore interval did not return a proper object!') + } + const { proofs, newKeys, start, lastCount } = resp + await saveSeed(seed) + // adds counter if not exists + await getCounterByMintUrl(mintUrl) + if (!proofs.length) { + l('[restoreWallet] no proofs found during the restore process!') + return { start: from, end: to } + } + const proofsSpent = await wallet.checkProofsSpent(proofs) + const proofsUnspent = proofs.filter(p => !proofsSpent.map(x => x.secret).includes(p.secret)) + if (newKeys) { _setKeys(mintUrl, newKeys) } + await addToken({ token: [{ mint: mintUrl, proofs: proofsUnspent }] }) + await incrementCounterByMintUrl(mintUrl, lastCount + 1) + return { start, end: lastCount } + } catch (e) { + l('[restoreWallet] error', { e }) + return { start: from, end: to } + } + } + + const restoreInterval = async ( + wallet: CashuWallet, + start: number, + end: number, + restoredProofs: Proof[] = [], + overshoot: number = 0 + ): TRestoreInterval => { + try { + const { proofs, newKeys } = await wallet.restore(start, end, keysetId) + l('[restoreInterval] restored proofs: ', { from: start, to: end, proofsLength: proofs.length }) + if (proofs.length) { + restoredProofs.push(...proofs) + overshoot = 0 + start += RESTORE_INTERVAL + end += RESTORE_INTERVAL + setRestored({ proofs: restoredProofs, start, end, overshoot }) + return restoreInterval(wallet, start, end, restoredProofs, overshoot) + } + if (shouldOvershoot && overshoot < RESTORE_OVERSHOOT) { + l('[restoreInterval] no proofs to restore! overshooting now: ', { proofsLength: proofs.length, overshoot }) + overshoot++ + start += RESTORE_INTERVAL + end += RESTORE_INTERVAL + setRestored({ proofs: restoredProofs, start, end, overshoot }) + return restoreInterval(wallet, start, end, restoredProofs, overshoot) + } + l('[restoreInterval] no proofs to restore! overshooting limit reached: ', { restoredProofsLength: restoredProofs.length, overshoot }) + return { proofs: restoredProofs, newKeys, start, lastCount: end } + } catch (e) { + l('[restoreInterval] error', { e }) + return { proofs: restoredProofs, newKeys: undefined, start, lastCount: end } + } + } + + useEffect(() => { + resetRestoredState() + void restore() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [mintUrl, keysetId, from, to]) + // prevent back navigation - https://reactnavigation.org/docs/preventing-going-back/ useEffect(() => { const backHandler = (e: TBeforeRemoveEvent) => preventBack(e, navigation.dispatch) @@ -39,10 +149,10 @@ export default function RecoveringScreen({ navigation, route }: IRecoveringPageP /> 0} - styles={[styles.hint, { color: overshoot > 0 ? mainColors.VALID : mainColors.WARN, marginBottom: s(40) }]} + bold={restored.overshoot > 0} + styles={[styles.hint, { color: restored.overshoot > 0 ? mainColors.VALID : mainColors.WARN, marginBottom: s(40) }]} // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - txt={overshoot > 0 ? `${t('doneSafety')} ${overshoot}/${RESTORE_OVERSHOOT}` : t('dontClose')} + txt={restored.overshoot > 0 ? `${t('doneSafety')} ${restored.overshoot}/${RESTORE_OVERSHOOT}` : t('dontClose')} /> @@ -74,10 +184,9 @@ export default function RecoveringScreen({ navigation, route }: IRecoveringPageP /> acc + p.amount, 0))})`} + txt={`${restored.proofs.length} ${t('proofs', { ns: NS.wallet })} (${formatSatStr(restored.proofs.reduce((acc, p) => acc + p.amount, 0))})`} /> - ) } diff --git a/src/screens/Restore/SelectKeyset.tsx b/src/screens/Restore/SelectKeyset.tsx new file mode 100644 index 00000000..a771f08b --- /dev/null +++ b/src/screens/Restore/SelectKeyset.tsx @@ -0,0 +1,53 @@ +import Loading from '@comps/Loading' +import Txt from '@comps/Txt' +import { l } from '@log' +import type { ISelectKeysetPageProps } from '@model/nav' +import { useThemeContext } from '@src/context/Theme' +import { getMintKeySetIds } from '@wallet' +import { useEffect, useState } from 'react' +import { View } from 'react-native' + + +export default function SelectKeysetScreen({ navigation, route }: ISelectKeysetPageProps) { + + const { mnemonic, mintUrl, comingFromOnboarding } = route.params + + const { color } = useThemeContext() + const [loading, setLoading] = useState(true) + const [allKeysets, setAllKeysets] = useState([]) + + const handleKeysets = async () => { + try { + const { keysets } = await getMintKeySetIds(mintUrl) + l({ keysets }) + setLoading(false) + // TODO get keysets that already have been restored and disable them + setAllKeysets(keysets) + } catch (e) { + l('[handleKeysetId] error: ', e) + setLoading(false) + } + } + + useEffect(() => { + void handleKeysets() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + return ( + + {loading ? + + : + + } + + ) +} \ No newline at end of file diff --git a/src/screens/Restore/Success.tsx b/src/screens/Restore/Success.tsx index bc485c30..00fa1d97 100644 --- a/src/screens/Restore/Success.tsx +++ b/src/screens/Restore/Success.tsx @@ -4,34 +4,32 @@ import Separator from '@comps/Separator' import Txt from '@comps/Txt' import type { IRestoreSuccessPageProps } from '@model/nav' import { isIOS } from '@src/consts' -import { RESTORE_INTERVAL } from '@src/consts/mints' import { useThemeContext } from '@src/context/Theme' -import { globals } from '@styles' +import { addToHistory } from '@store/latestHistoryEntries' +import { globals, mainColors } from '@styles' import { formatMintUrl, formatSatStr, vib } from '@util' import { useEffect } from 'react' import { TouchableOpacity, View } from 'react-native' import { useSafeAreaInsets } from 'react-native-safe-area-context' import { s, ScaledSheet } from 'react-native-size-matters' -export default function RestoreSuccess({ navigation, route }: IRestoreSuccessPageProps) { +export default function RestoreOverviewScreen({ navigation, route }: IRestoreSuccessPageProps) { const insets = useSafeAreaInsets() - const { mnemonic, mint, keysetID, amount, cycle, comingFromOnboarding } = route.params + const { mnemonic, mint, keysetID, amount, comingFromOnboarding } = route.params const { color } = useThemeContext() - const handleCycle = () => { - navigation.navigate('Recovering', { - mintUrl: mint, - keysetId: keysetID, - mnemonic, - comingFromOnboarding, - from: cycle.end, - to: cycle.end + RESTORE_INTERVAL + const handleDone = async () => { + await addToHistory({ + mints: [mint], + amount, + type: 4, + value: '', }) - } - - const handleKeysetId = () => { - // navigation.navigate('keysetId', { mint, keysetID, amount, cycle, comingFromOnboarding }) + if (comingFromOnboarding) { + return navigation.navigate('auth', { pinHash: '' }) + } + return navigation.navigate('dashboard') } useEffect(() => vib(400), []) @@ -62,6 +60,7 @@ export default function RestoreSuccess({ navigation, route }: IRestoreSuccessPag @@ -70,7 +69,7 @@ export default function RestoreSuccess({ navigation, route }: IRestoreSuccessPag - - - - - - - - navigation.navigate('selectKeyset', { mnemonic, mintUrl: mint, comingFromOnboarding })} > @@ -117,12 +98,7 @@ export default function RestoreSuccess({ navigation, route }: IRestoreSuccessPag