From 5bb8af10da09b9d460db59beef974a19ce1f8a23 Mon Sep 17 00:00:00 2001 From: minibits-cash Date: Fri, 11 Oct 2024 16:21:07 +0200 Subject: [PATCH] Import wallet from backup and fixes --- src/App.tsx | 5 +- src/i18n_messages/en.json | 2 +- src/models/RelaysStore.ts | 2 +- src/models/helpers/setupRootStore.ts | 5 +- src/screens/BackupOptionsScreen.tsx | 2 +- src/screens/DeveloperScreen.tsx | 25 ++-- src/screens/ExportBackupScreen.tsx | 12 +- src/screens/ImportBackupScreen.tsx | 181 ++++++++++++++++---------- src/screens/RecoveryOptionsScreen.tsx | 4 +- src/screens/SeedRecoveryScreen.tsx | 58 ++++----- src/screens/WelcomeScreen.tsx | 17 ++- src/services/mmkvStorage.ts | 1 + src/services/sqlite.ts | 11 +- 13 files changed, 192 insertions(+), 133 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 4d61d6b0..008aa8fd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -68,8 +68,9 @@ function App(props: AppProps) { // This runs after the root store has been initialized and rehydrated from storage. // Creates and opens a sqlite database that stores transactions history and user settings. - // It triggers db migrations if database version has changed. - Database.getDatabaseVersion() + // It triggers db migrations if database version has changed. + const db = Database.getInstance() + Database.getDatabaseVersion(db) // Syncs userSettings store with the database (needed?) userSettingsStore.loadUserSettings() diff --git a/src/i18n_messages/en.json b/src/i18n_messages/en.json index 5d14df22..8453b76d 100644 --- a/src/i18n_messages/en.json +++ b/src/i18n_messages/en.json @@ -309,7 +309,7 @@ "changeWalletaddress": "Change wallet address", "changeWalletaddressSubtext": "Change unique wallet address. Select one of randomly generated or choose your own." }, - "profileToRecoverDesc": "This is the wallet address linked to your seed. If you press Complete, your wallet profile will reset to above seed and address.", + "profileToRecoverDesc": "This is the wallet address linked to your seed. If you continue, your wallet profile will reset to above seed and address.", "pushNotifications": "Push notifications", "qr": { "fail": "Could not display QR code, use option to copy or send the token to the payee wallet address." diff --git a/src/models/RelaysStore.ts b/src/models/RelaysStore.ts index 2cc4999f..f8c45202 100644 --- a/src/models/RelaysStore.ts +++ b/src/models/RelaysStore.ts @@ -46,7 +46,7 @@ export const RelaysStoreModel = types relay.url = normalized if(self.alreadyExists(relay.url)) { - log.warn('[addRelay] Relay already exists', {relay}) + log.info('[addRelay] Relay already exists', {relay}) return } diff --git a/src/models/helpers/setupRootStore.ts b/src/models/helpers/setupRootStore.ts index 3c864afc..66bb2415 100644 --- a/src/models/helpers/setupRootStore.ts +++ b/src/models/helpers/setupRootStore.ts @@ -16,9 +16,8 @@ import { } from 'mobx-state-tree' import { debounce } from "lodash" import * as Sentry from '@sentry/react-native' -import RNExitApp from 'react-native-exit-app' import type {RootStore} from '../RootStore' -import {KeyChain, MinibitsClient, MMKVStorage, NostrClient} from '../../services' +import {KeyChain, MinibitsClient, MMKVStorage} from '../../services' import {Database} from '../../services' import { log } from '../../services/logService' import { rootStoreModelVersion } from '../RootStore' @@ -62,7 +61,7 @@ export async function setupRootStore(rootStore: RootStore) { log.trace('[setupRootStore]', `Loading ${dataSize.toLocaleString()} bytes of state from MMKV took ${(mmkvLoaded - start).toLocaleString()} ms.`) - log.trace({restoredState}) + // log.trace({restoredState}) applySnapshot(rootStore, restoredState) diff --git a/src/screens/BackupOptionsScreen.tsx b/src/screens/BackupOptionsScreen.tsx index abfa7ec0..e4b2da8e 100644 --- a/src/screens/BackupOptionsScreen.tsx +++ b/src/screens/BackupOptionsScreen.tsx @@ -60,7 +60,7 @@ export const BackupOptionsScreen: FC> tx="backupScreen.mnemonicTitle" subTx="backupScreen.mnemonicDescription" leftIcon='faSeedling' - leftIconColor={colors.palette.blue200} + leftIconColor={colors.palette.orange400} leftIconInverse={true} style={$item} onPress={gotoMnemonic} diff --git a/src/screens/DeveloperScreen.tsx b/src/screens/DeveloperScreen.tsx index b9b7d56c..e0a87e19 100644 --- a/src/screens/DeveloperScreen.tsx +++ b/src/screens/DeveloperScreen.tsx @@ -35,6 +35,8 @@ import {MMKVStorage} from '../services' import {maxTransactionsInModel} from '../models/TransactionsStore' import { LogLevel } from '../services/log/logTypes' import { getSnapshot } from 'mobx-state-tree' +import { delay } from '../utils/delay' +import RNExitApp from 'react-native-exit-app' // refresh @@ -50,6 +52,7 @@ export const DeveloperScreen: FC> = observ const [isLoading, setIsLoading] = useState(false) const [rnVersion, setRnVersion] = useState('') const [walletStateSize, setWalletStateSize] = useState(0) + const [dbVersion, setDbVersion] = useState(0) const [isLogLevelSelectorVisible, setIsLogLevelSelectorVisible] = useState(false) const [selectedLogLevel, setSelectedLogLevel] = useState(userSettingsStore.logLevel) const [error, setError] = useState() @@ -62,6 +65,10 @@ export const DeveloperScreen: FC> = observ // log.info('[SNAPSHOT]', {snapshot}) const stateSize = Buffer.byteLength(JSON.stringify(snapshot), 'utf8') + const db = Database.getInstance() + const {version} = Database.getDatabaseVersion(db) + + setDbVersion(version) setWalletStateSize(stateSize) setRnVersion(rn) } @@ -143,6 +150,8 @@ export const DeveloperScreen: FC> = observ Database.getInstance() setIsLoading(false) setInfo(translate("factoryResetSuccess")) + await delay(1000) + RNExitApp.exitApp() } catch (e: any) { handleError(e) } @@ -187,20 +196,6 @@ export const DeveloperScreen: FC> = observ bottomSeparator={true} onPress={toggleLogLevelSelector} /> - {/*} - style={$item} - bottomSeparator={true} - onPress={syncTransactionsFromDb} - />*/} > = observ subText={`Environment: ${APP_ENV} Native version: ${NATIVE_VERSION_ANDROID} JS Bundle version: ${JS_BUNDLE_VERSION} -DB version: ${Database.getDatabaseVersion().version} +DB version: ${dbVersion} State size: ${walletStateSize.toLocaleString()} bytes React Native: ${rnVersion} Commit: ${COMMIT} diff --git a/src/screens/ExportBackupScreen.tsx b/src/screens/ExportBackupScreen.tsx index 222c822c..f68ec7ec 100644 --- a/src/screens/ExportBackupScreen.tsx +++ b/src/screens/ExportBackupScreen.tsx @@ -64,8 +64,11 @@ export const ExportBackupScreen: FC = useEffect(() => { const loadProofs = async () => { setIsLoading(true) + // log.trace('[loadProofs]', {proofs: proofsStore.proofs}) // full refresh of proofs from DB in case the state is broken await proofsStore.loadProofsFromDatabase() + + // log.trace('[loadProofs]', {refreshedProofs: proofsStore.proofs}) setIsLoading(false) } @@ -117,7 +120,14 @@ export const ExportBackupScreen: FC = } if(isEcashInBackup) { - exportedProofsStore = getSnapshot(proofsStore) + // This is emptied in snapshot postprocess! + exportedProofsStore = { + proofs: getSnapshot(proofsStore.proofs), + pendingProofs: getSnapshot(proofsStore.pendingProofs), + pendingByMintSecrets: getSnapshot(proofsStore.pendingByMintSecrets) + } + + // log.trace({exportedProofsStore}) } if(isMintsInBackup) { diff --git a/src/screens/ImportBackupScreen.tsx b/src/screens/ImportBackupScreen.tsx index 205a4fda..ec15b517 100644 --- a/src/screens/ImportBackupScreen.tsx +++ b/src/screens/ImportBackupScreen.tsx @@ -6,7 +6,7 @@ import { btoa, atob } from 'react-native-quick-base64' import QuickCrypto from 'react-native-quick-crypto' import { wordlist } from '@scure/bip39/wordlists/english' import {colors, spacing, typography, useThemeColor} from '../theme' -import {AppStackScreenProps} from '../navigation' // @demo remove-current-line +import {AppStackScreenProps, goBack} from '../navigation' // @demo remove-current-line import { Icon, ListItem, @@ -17,30 +17,21 @@ import { ErrorModal, InfoModal, BottomModal, - Button, - $sizeStyles, + Button, } from '../components' import {useHeader} from '../utils/useHeader' import AppError, { Err } from '../utils/AppError' -import { KeyChain, log, MinibitsClient, NostrClient } from '../services' +import { Database, KeyChain, log, MinibitsClient, NostrClient } from '../services' import Clipboard from '@react-native-clipboard/clipboard' -import { useStores } from '../models' -import { MintListItem } from './Mints/MintListItem' +import { rootStoreInstance, useStores } from '../models' import {MnemonicInput} from './Recovery/MnemonicInput' -import { Mint } from '../models/Mint' -import { MintKeyset } from '@cashu/cashu-ts' -import { CashuUtils } from '../services/cashu/cashuUtils' -import { Proof } from '../models/Proof' -import { Transaction, TransactionData, TransactionRecord, TransactionStatus, TransactionType } from '../models/Transaction' +import { TransactionData, TransactionStatus } from '../models/Transaction' import { ResultModalInfo } from './Wallet/ResultModalInfo' import { deriveSeedFromMnemonic } from '@cashu/cashu-ts' -import { MINIBITS_NIP05_DOMAIN } from '@env' +import { MINIBITS_MINT_URL, MINIBITS_NIP05_DOMAIN } from '@env' import { delay } from '../utils/utils' -import { getSnapshot, isStateTreeNode } from 'mobx-state-tree' -import { scale, verticalScale } from '@gocodingnow/rn-size-matters' -import { WalletUtils } from '../services/wallet/utils' -import { MintUnit, formatCurrency, getCurrency } from '../services/wallet/currency' -import { isObj } from '@cashu/cashu-ts/src/utils' +import { applySnapshot} from 'mobx-state-tree' +import { verticalScale } from '@gocodingnow/rn-size-matters' import { WalletProfileRecord } from '../models/WalletProfileStore' import { translate } from '../i18n' import { ProofsStoreSnapshot } from '../models/ProofsStore' @@ -66,7 +57,8 @@ export const ImportBackupScreen: FC> = obser mintsStore, proofsStore, userSettingsStore, - transactionsStore, + transactionsStore, + contactsStore, walletProfileStore, walletStore } = useStores() @@ -82,7 +74,12 @@ export const ImportBackupScreen: FC> = obser const [seed, setSeed] = useState() const [backup, setBackup] = useState('') const [isValidBackup, setIsValidBackup] = useState(false) - const [walletSnapshot, setWalletSnapshot] = useState(undefined) // type tbd + const [walletSnapshot, setWalletSnapshot] = useState<{ + proofsStore: ProofsStoreSnapshot, + mintsStore: MintsStoreSnapshot, + contactsStore: ContactsStoreSnapshot, + transactionsStore: TransactionsStoreSnapshot + } | undefined>(undefined) // type tbd const [profileToRecover, setProfileToRecover] = useState(undefined) const [isLoading, setIsLoading] = useState(false) const [error, setError] = useState() @@ -115,17 +112,14 @@ export const ImportBackupScreen: FC> = obser setIsResultModalVisible(previousState => !previousState) } - + // Used by both Wallet address and Import backup flows const onConfirmMnemonic = async function () { try { - setStatusMessage(translate("derivingSeedStatus")) - if(!mnemonic) { throw new AppError(Err.VALIDATION_ERROR, translate('backupScreen.missingMnemonicError')) } - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) - setIsLoading(true) + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) if (!validateMnemonic(mnemonic, wordlist)) { throw new AppError(Err.VALIDATION_ERROR, translate("recoveryInvalidMnemonicError")) @@ -137,15 +131,13 @@ export const ImportBackupScreen: FC> = obser console.log(`[onConfirm] deriveSeedFromMnemonic took ${end - start} ms.`) setSeed(binarySeed) - setIsValidMnemonic(true) - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) - setIsLoading(false) + setIsValidMnemonic(true) } catch (e: any) { handleError(e) } } - + // Used by Import backup flow const onPasteBackup = async function () { try { const maybeBackup = await Clipboard.getString() @@ -162,7 +154,7 @@ export const ImportBackupScreen: FC> = obser } } - + // Used by Import backup flow const getWalletSnapshot = function () { try { // decode @@ -187,22 +179,19 @@ export const ImportBackupScreen: FC> = obser } } - + // Used by Import backup flow const onConfirmBackup = async function () { try { if(!backup) { throw new AppError(Err.VALIDATION_ERROR, 'Missing backup.') } - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) - setIsLoading(true) - const snapshot = getWalletSnapshot() // throws setWalletSnapshot(snapshot) setIsValidBackup(true) - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) - setIsLoading(false) + + await onFindWalletAddress() } catch (e: any) { handleError(e) } @@ -213,20 +202,16 @@ export const ImportBackupScreen: FC> = obser return navigation.goBack() } - - const startImport = async function () { - setStatusMessage(translate("recovery.starting")) - setIsLoading(true) - // setTimeout(() => doRecovery(), 100) - } - - + // Used by both Wallet address and Import backup flows const onFindWalletAddress = async () => { try { if(!seed || !mnemonic) { throw new AppError(Err.VALIDATION_ERROR, translate("backupScreen.missingMnemonicOrSeedError")) } + setStatusMessage(translate('recovery.recoveringAddress')) + setIsLoading(true) + const seedHash = QuickCrypto.createHash('sha256') .update(seed) .digest('hex') @@ -244,14 +229,68 @@ export const ImportBackupScreen: FC> = obser await delay(4000) } } else { - setInfo(translate("recovery.noWalletForSeedError")) - } + if(isAddressOnlyRecovery) { + setInfo(translate("recovery.noWalletForSeedError")) + } + } + + setStatusMessage('') + setIsLoading(false) } catch (e: any) { handleError(e) } } + // Used by Import backup flow + const importWallet = async function () { + try { + if(!walletSnapshot) { + throw new AppError(Err.VALIDATION_ERROR, 'Missing wallet spnapshot decoded from backup.') + } + setStatusMessage(translate("recovery.starting")) + setIsLoading(true) + + // import wallet snapshot into the state + const rootStore = rootStoreInstance + + log.trace('Before import', {rootStore}) + log.trace('Snapshot ot be imported', {walletSnapshot}) + + applySnapshot(proofsStore, walletSnapshot.proofsStore) + applySnapshot(mintsStore, walletSnapshot.mintsStore) + applySnapshot(contactsStore, walletSnapshot.contactsStore) + applySnapshot(transactionsStore, walletSnapshot.transactionsStore) + + log.trace('After import', {rootStore}) + + // import proofs into the db + if(proofsStore.proofsCount > 0) { + Database.addOrUpdateProofs(proofsStore.allProofs, false, false) + } + + if(proofsStore.pendingProofsCount > 0) { + Database.addOrUpdateProofs(proofsStore.allPendingProofs, true, false) + } + + if(!mintsStore.mintExists(MINIBITS_MINT_URL)) { + await mintsStore.addMint(MINIBITS_MINT_URL) + } + + if(profileToRecover) { + // If wallet profile has been found on server, recover wallet address + await onCompleteAddress() + } else { + // If there is no profile go back to the welcome screen to create a new one + goBack() + } + } catch (e: any) { + handleError(e) + } + } + + + // Used by both Wallet address and Import backup flows const onCompleteAddress = async () => { try { if(!seed || !mnemonic || !profileToRecover) { @@ -265,7 +304,7 @@ export const ImportBackupScreen: FC> = obser .update(seed) .digest('hex') - // get nostr key from current on device profile + // get nostr key from current on device profile (if we recover to existing wallet in Settings) or create new one (if we recover to new install) const {publicKey: newPublicKey} = await NostrClient.getOrCreateKeyPair() // delete orphaned server profile then update server profile with seedHash with pubkey + update on device wallet name and address @@ -275,16 +314,19 @@ export const ImportBackupScreen: FC> = obser userSettingsStore.setWalletId(walletProfileStore.walletId) await KeyChain.saveMnemonic(mnemonic) await KeyChain.saveSeed(seed as Uint8Array) - - await delay(1000) - setStatusMessage(translate("recovery.completed")) - await delay(2000) - - userSettingsStore.setIsOnboarded(true) + + setStatusMessage(translate('recovery.completed')) + await delay(1000) setStatusMessage('') setIsLoading(false) - navigation.goBack() + + if(isAddressOnlyRecovery) { + // go back to Settings + navigation.goBack() + } else { + navigation.navigate('Tabs', {screen: 'WalletNavigator', params: {screen: 'Wallet', params: {}}}) + } } catch (e: any) { handleError(e) } @@ -301,7 +343,7 @@ export const ImportBackupScreen: FC> = obser const inputBg = useThemeColor('background') const headerTitle = useThemeColor('headerTitle') - if(false) { + if(mnemonicExists && !isAddressOnlyRecovery) { return ( @@ -340,7 +382,7 @@ export const ImportBackupScreen: FC> = obser @@ -369,7 +411,7 @@ export const ImportBackupScreen: FC> = obser ref={backupInputRef} onChangeText={(backup: string) => setBackup(backup)} value={backup} - numberOfLines={5} + numberOfLines={3} multiline={true} autoCapitalize='none' keyboardType='default' @@ -401,7 +443,7 @@ export const ImportBackupScreen: FC> = obser ContentComponent={ } style={$item} @@ -409,14 +451,14 @@ export const ImportBackupScreen: FC> = obser } /> )} - {isValidMnemonic && profileToRecover && isAddressOnlyRecovery && ( + {isValidMnemonic && profileToRecover && ( } + LeftComponent={} style={$item} /> } @@ -455,16 +497,21 @@ export const ImportBackupScreen: FC> = obser )} ) : ( -