Skip to content

Commit

Permalink
Fix failed auth causing wallet state loss
Browse files Browse the repository at this point in the history
  • Loading branch information
minibits-cash committed Jan 4, 2024
1 parent 141800d commit f6cb6e1
Show file tree
Hide file tree
Showing 5 changed files with 160 additions and 141 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "minibits_wallet",
"version": "0.1.5-beta.21",
"version": "0.1.5-beta.22",
"private": true,
"scripts": {
"android:clean": "cd android && ./gradlew clean",
Expand Down
274 changes: 140 additions & 134 deletions src/models/helpers/setupRootStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import AppError, { Err } from '../../utils/AppError'
import { MINIBITS_NIP05_DOMAIN } from '@env'
import { LogLevel } from '../../services/log/logTypes'
import { MintStatus } from '../Mint'
import useIsInternetReachable from '../../utils/useIsInternetReachable'

/**
* The key we'll be saving our state as within storage.
Expand All @@ -38,181 +37,188 @@ const ROOT_STORAGE_KEY = 'minibits-root-storage'
*/
let _disposer: IDisposer
export async function setupRootStore(rootStore: RootStore) {
let restoredState: any
// let latestSnapshot: any
let restoredState: any
// let latestSnapshot: any

try {
// Give an option to encrypt storage as it might slow down app start on some Android devices
// User settings are mastered in sqlite so we can get the encryption setting before loading root store
const userSettings = Database.getUserSettings()

// random identificator of an app installation for bugs and crash reporting
if(userSettings.walletId) {
Sentry.setUser({ id: userSettings.walletId })
}
try {
// Give an option to encrypt storage as it might slow down app start on some Android devices
// User settings are mastered in sqlite so we can get the encryption setting before loading root store
const userSettings = Database.getUserSettings()
// random identificator of an app installation for bugs and crash reporting
if(userSettings.walletId) {
Sentry.setUser({ id: userSettings.walletId })
}

if (userSettings.isStorageEncrypted) {
await MMKVStorage.initEncryption() // opt-in, key retrieval on Android is sometimes slow
}
if (userSettings.isStorageEncrypted) {
try {
await MMKVStorage.initEncryption()
} catch (e: any) {
if (e && typeof e === 'object') {
const errString = JSON.stringify(e)

const isCancellPressed = errString.includes('code: 10')
const isBackPressed = errString.includes('code: 13')
const isIOSCancel = 'code' in e && String(e.code) === '-128'

// In case user cancels / fails the fingerprint auth, empty app state is loaded.
// If a user updates the empty app state, it is stored unencrypted and returned after restart as primary one,
// making encrypted data inaccessible or later overwritten.
// Therefore this ugly app exit on unsuccessful auth.

if(isCancellPressed || isBackPressed || isIOSCancel) {
log.error('[setupRootStore]', 'Exiting app on failed auth', {error: e})
RNExitApp.exitApp()
}
}
}
}

// load the last known state from storage
restoredState = MMKVStorage.load(ROOT_STORAGE_KEY) || {}
applySnapshot(rootStore, restoredState)

} catch (e: any) {
log.error(Err.STORAGE_ERROR, e.message, e.params, 'setupRootStore')
// load the last known state from storage
restoredState = MMKVStorage.load(ROOT_STORAGE_KEY) || {}
applySnapshot(rootStore, restoredState)

// In case user cancels / fails the fingerprint auth, empty app state is loaded.
// If a user updates the empty app state, it is stored unencrypted and returned after restart as primary one,
// making encrypted data inaccessible or later overwritten.
// Therefore this ugly app exit on unsuccessful auth.
const isCancellPressed = e.params?.some((p: string) => p.includes('code: 13'))
const isBackPressed = e.params?.some((p: string) => p.includes('code: 10'))

if(isCancellPressed || isBackPressed) {
log.info('Exiting app', e.params, 'setupRootStore')
RNExitApp.exitApp()
} catch (e: any) {
log.error('[setupRootStore]', Err.STORAGE_ERROR, e.message, e.params)
}

}

// stop tracking state changes if we've already setup
if (_disposer) {
_disposer()
}
// stop tracking state changes if we've already setup
if (_disposer) {
_disposer()
}

// track changes & save to storage // TODO defering and batching of writes to storage
_disposer = onSnapshot(rootStore, snapshot =>
MMKVStorage.save(ROOT_STORAGE_KEY, snapshot),
)
// track changes & save to storage // TODO defering and batching of writes to storage
_disposer = onSnapshot(rootStore, snapshot =>
MMKVStorage.save(ROOT_STORAGE_KEY, snapshot),
)

// run migrations if needed, needs to be after onSnapshot to be persisted
try {
log.debug('[setupRootStore]', `Device rootStore.version is: ${rootStore.version}`)
// run migrations if needed, needs to be after onSnapshot to be persisted
try {
log.debug('[setupRootStore]', `Device rootStore.version is: ${rootStore.version}`)

if(rootStore.version < rootStoreModelVersion) {
await _runMigrations(rootStore)
await _runMigrations(rootStore)
}
} catch (e: any) {
log.error(Err.STORAGE_ERROR, e.message)
}
} catch (e: any) {
log.error(Err.STORAGE_ERROR, e.message)
}

const unsubscribe = () => {
_disposer()
_disposer = undefined
}
const unsubscribe = () => {
_disposer()
_disposer = undefined
}

return {rootStore, restoredState, unsubscribe}
return {rootStore, restoredState, unsubscribe}
}

/**
* Migrations code to execute based on code and on device model version.
*/

async function _runMigrations(rootStore: RootStore) {
const {
userSettingsStore,
walletProfileStore,
relaysStore,
contactsStore,
mintsStore,
} = rootStore

let currentVersion = rootStore.version

try {
if(currentVersion < 3) {
log.trace(`Starting rootStore migrations from version v${currentVersion} -> v3`)
if(walletProfileStore.pubkey) {
walletProfileStore.setNip05(walletProfileStore.name+MINIBITS_NIP05_DOMAIN)
log.info(`Completed rootStore migrations to the version v${rootStoreModelVersion}`)
rootStore.setVersion(rootStoreModelVersion)
const {
userSettingsStore,
walletProfileStore,
relaysStore,
contactsStore,
mintsStore,
} = rootStore

let currentVersion = rootStore.version

try {
if(currentVersion < 3) {
log.trace(`Starting rootStore migrations from version v${currentVersion} -> v3`)
if(walletProfileStore.pubkey) {
walletProfileStore.setNip05(walletProfileStore.name+MINIBITS_NIP05_DOMAIN)
log.info(`Completed rootStore migrations to the version v${rootStoreModelVersion}`)
rootStore.setVersion(rootStoreModelVersion)
}
}
}


if(currentVersion < 4) {
log.trace(`Starting rootStore migrations from version v${currentVersion} -> v4`)
if(walletProfileStore.pubkey) {
walletProfileStore.setWalletId(userSettingsStore.walletId as string)
if(currentVersion < 4) {
log.trace(`Starting rootStore migrations from version v${currentVersion} -> v4`)
if(walletProfileStore.pubkey) {
walletProfileStore.setWalletId(userSettingsStore.walletId as string)

// publish profile to relays
await walletProfileStore.publishToRelays()
// publish profile to relays
await walletProfileStore.publishToRelays()
log.info(`Completed rootStore migrations to the version v${rootStoreModelVersion}`)
rootStore.setVersion(rootStoreModelVersion)
}
}


if(currentVersion < 5) {
log.trace(`Starting rootStore migrations from version v${currentVersion} -> v5`)
if(contactsStore.publicRelay) {
relaysStore.addOrUpdateRelay({
url: contactsStore.publicRelay,
status: WebSocket.CLOSED
})
}
log.info(`Completed rootStore migrations to the version v${rootStoreModelVersion}`)
rootStore.setVersion(rootStoreModelVersion)
}
}


if(currentVersion < 5) {
log.trace(`Starting rootStore migrations from version v${currentVersion} -> v5`)
if(contactsStore.publicRelay) {
relaysStore.addOrUpdateRelay({
url: contactsStore.publicRelay,
status: WebSocket.CLOSED
})
if(currentVersion < 6) {
log.trace(`Starting rootStore migrations from version v${currentVersion} -> v6`)
userSettingsStore.setLogLevel(LogLevel.ERROR)
log.info(`Completed rootStore migrations to the version v${rootStoreModelVersion}`)
rootStore.setVersion(rootStoreModelVersion)
}
log.info(`Completed rootStore migrations to the version v${rootStoreModelVersion}`)
rootStore.setVersion(rootStoreModelVersion)
}


if(currentVersion < 6) {
log.trace(`Starting rootStore migrations from version v${currentVersion} -> v6`)
userSettingsStore.setLogLevel(LogLevel.ERROR)
log.info(`Completed rootStore migrations to the version v${rootStoreModelVersion}`)
rootStore.setVersion(rootStoreModelVersion)
}
if(currentVersion < 7) {
log.trace(`Starting rootStore migrations from version v${currentVersion} -> v7`)
for (const mint of mintsStore.allMints) {
mint.setStatus(MintStatus.ONLINE)
}
log.info(`Completed rootStore migrations to the version v${rootStoreModelVersion}`)
rootStore.setVersion(rootStoreModelVersion)
}


if(currentVersion < 7) {
log.trace(`Starting rootStore migrations from version v${currentVersion} -> v7`)
for (const mint of mintsStore.allMints) {
mint.setStatus(MintStatus.ONLINE)
}
log.info(`Completed rootStore migrations to the version v${rootStoreModelVersion}`)
rootStore.setVersion(rootStoreModelVersion)
}
if(currentVersion < 8) {
log.trace(`Starting rootStore migrations from version v${currentVersion} -> v8`)
const seedHash = await KeyChain.loadSeedHash()

if(seedHash && walletProfileStore.pubkey) {
await MinibitsClient.migrateSeedHash(
walletProfileStore.pubkey,
{
seedHash
}
)

if(currentVersion < 8) {
log.trace(`Starting rootStore migrations from version v${currentVersion} -> v8`)
const seedHash = await KeyChain.loadSeedHash()
walletProfileStore.setSeedHash(seedHash)
log.info(`Completed rootStore migrations to the version v${rootStoreModelVersion}`)
rootStore.setVersion(rootStoreModelVersion)
}
}

if(seedHash && walletProfileStore.pubkey) {
await MinibitsClient.migrateSeedHash(
walletProfileStore.pubkey,
{
seedHash
if(currentVersion < 9) {
log.trace(`Starting rootStore migrations from version v${currentVersion} -> v9`)

for (const mint of mintsStore.allMints) {
try {
await mint.setShortname()
} catch (e: any) {
continue
}
)
}

walletProfileStore.setSeedHash(seedHash)
log.info(`Completed rootStore migrations to the version v${rootStoreModelVersion}`)
rootStore.setVersion(rootStoreModelVersion)
}
}

if(currentVersion < 9) {
log.trace(`Starting rootStore migrations from version v${currentVersion} -> v9`)

for (const mint of mintsStore.allMints) {
try {
await mint.setShortname()
} catch (e: any) {
continue
}
}

log.info(`Completed rootStore migrations to the version v${rootStoreModelVersion}`)
rootStore.setVersion(rootStoreModelVersion)
} catch (e: any) {
throw new AppError(
Err.STORAGE_ERROR,
'Error when executing rootStore migrations',
e.message,
)
}

} catch (e: any) {
throw new AppError(
Err.STORAGE_ERROR,
'Error when executing rootStore migrations',
e.message,
)
}
}
11 changes: 10 additions & 1 deletion src/screens/ContactsScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { getImageSource } from '../utils/utils'
import { ReceiveOption } from './ReceiveOptionsScreen'
import { SendOption } from './SendOptionsScreen'
import { WalletProfile } from '../models/WalletProfileStore'
import { Err } from '../utils/AppError'
import { getRandomUsername } from '../utils/usernames'

interface ContactsScreenProps extends ContactsStackScreenProps<'Contacts'> {}

Expand All @@ -40,7 +42,14 @@ export const ContactsScreen: FC<ContactsScreenProps> = observer(function Contact
}

} catch(e: any) {
log.error(e.name, e.message)
log.error(e.name, e.message)

// in case we somehow hit the existing name we silently retry
if(e.name && e.name === Err.ALREADY_EXISTS_ERROR) {
const randomName = getRandomUsername()
await walletProfileStore.create(randomName as string)
}

return false // silent
}
}
Expand Down
8 changes: 7 additions & 1 deletion src/screens/MintInfoScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { GetInfoResponse } from '@cashu/cashu-ts'
import { delay } from '../utils/utils'
import JSONTree from 'react-native-json-tree'
import { getSnapshot } from 'mobx-state-tree'
import { Mint } from '../models/Mint'
import { Mint, MintStatus } from '../models/Mint'
import useColorScheme from '../theme/useThemeColor'
import { CommonActions } from '@react-navigation/native'

Expand Down Expand Up @@ -86,6 +86,12 @@ export const MintInfoScreen: FC<SettingsStackScreenProps<'MintInfo'>> = observer
setMintInfo(info)
setIsLoading(false)
} catch (e: any) {
if (route.params.mintUrl) {
const mint = mintsStore.findByUrl(route.params.mintUrl)
if(mint) {
mint.setStatus(MintStatus.OFFLINE)
}
}
handleError(e)
}
}
Expand Down

0 comments on commit f6cb6e1

Please sign in to comment.