Full recovery from local backup, fix in case migration during upgrade fails

This commit is contained in:
minibits-cash
2024-05-15 11:51:23 +02:00
parent ebf14fcf93
commit 25fbe99111
6 changed files with 135 additions and 82 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "minibits_wallet",
"version": "0.1.7-beta.7",
"version": "0.1.7-beta.8",
"private": true,
"scripts": {
"android:clean": "cd android && ./gradlew clean",
+14
View File
@@ -177,6 +177,20 @@ export const ProofsStoreModel = types
self.pendingByMintSecrets.remove(proof.secret)
log.trace('[removeFromPendingByMint]', 'Proof removed from pending by mint, secret', proof.secret)
},
removeOnLocalRecovery(proofsToRemove: Proof[], isPending: boolean = false) {
const proofs = isPending ? self.pendingProofs : self.proofs
proofsToRemove.map((proof) => {
if (isStateTreeNode(proof)) {
detach(proof) // vital
} else {
const proofInstance = self.getProofInstance(proof, isPending)
detach(proofInstance) // vital
}
})
proofs.replace(proofs.filter(proof => !proofsToRemove.some(removed => removed.secret === proof.secret)))
},
}))
.views(self => ({
get proofsCount() {
+81 -73
View File
@@ -5,6 +5,7 @@ import {
ViewStyle,
View,
useColorScheme,
Alert,
} from 'react-native'
import {
type Proof as CashuProof,
@@ -34,7 +35,7 @@ import Clipboard from '@react-native-clipboard/clipboard'
import { getEncodedToken } from '@cashu/cashu-ts'
import { Transaction, TransactionData, TransactionRecord, TransactionStatus, TransactionType } from '../models/Transaction'
import { WalletUtils } from '../services/wallet/utils'
import { MintUnit } from '../services/wallet/currency'
import { MintUnit, MintUnits, getCurrency } from '../services/wallet/currency'
import { ScrollView } from 'react-native-gesture-handler'
import { CurrencyAmount } from './Wallet/CurrencyAmount'
@@ -77,7 +78,10 @@ export const LocalRecoveryScreen: FC<LocalRecoveryScreenProps> =
) {
try {
setIsLoading(true)
const proofs = await Database.getProofs(isUnspent, isPending, isDeleted)
// update empty unit fields to resolve v0.1.7 upgrade issue of backed up proofs not having unit migrated
const update = await Database.updateProofsToDefaultUnit()
const proofs = await Database.getProofs(isUnspent, isPending, isDeleted)
setProofs(proofs)
setIsLoading(false)
@@ -181,34 +185,74 @@ export const LocalRecoveryScreen: FC<LocalRecoveryScreenProps> =
}
const recoverProofs = async function () {
try {
if(proofsStore.proofsCount > 0) {
setInfo('Your wallet contains unspent ecash, recovery is not safe.')
return
}
const onRecovery = async function () {
if(!showUnspentOnly) {
setInfo('Can not recover other then unspent proofs.')
return
}
if(showSpentOnly) {
setInfo('Can not recover proofs that were already spent.')
const balances = proofsStore.getBalances()
let message: string = ''
const nonZeroBalances = balances.mintBalances.filter(b => Object.values(b.balances).some(b => b && b > 0))
log.trace('[onRecovery]', nonZeroBalances)
if (nonZeroBalances && nonZeroBalances.length > 0) {
message = `Your wallet has non zero balance. If you continue, existing Ecash will be deleted and replaced by backup.\n\n`
}
message += `Do you really want to recover Ecash from the backup?`
Alert.alert(
'Attention!',
message,
[
{
text: 'Cancel',
style: 'cancel',
onPress: () => {
// Action canceled
},
},
{
text: 'Start recovery',
onPress: () => {
try {
doLocalRecovery()
} catch (e: any) {
handleError(e)
}
},
},
],
)
}
const doLocalRecovery = async function () {
try {
if(!showUnspentOnly) {
setInfo('Can not recover other then unspent proofs.')
return
}
if(mintsStore.allMints.length === 0) {
setInfo('Please add all mints your proofs belong to the wallet. Then try again.')
setInfo('Please add all your mints to the wallet before recovery. Then try again.')
}
setIsLoading(true)
for (const mint of mintsStore.allMints) {
let proofsByMint: CashuProof[] = []
let proofsByMint: Proof[] = []
let transactionData: TransactionData[] = []
for (const proof of proofs) {
const proofMint = CashuUtils.getMintFromProof(proof, mintsStore.allMints)
const { tId, isPending, isSpent, updatedAt, ...cleanedProof } = proof
const { isPending, isSpent, updatedAt, ...cleanedProof } = proof
if (!proofMint) { continue }
@@ -219,9 +263,12 @@ export const LocalRecoveryScreen: FC<LocalRecoveryScreenProps> =
if (proofsByMint.length > 0) {
const groupedByUnit: Record<string, Proof[]> = proofsByMint.reduce((acc, proof) => {
// delete from wallet storage
proofsStore.removeOnLocalRecovery(proofsByMint, false)
const groupedByUnit = proofsByMint.reduce((acc: Record<string, Proof[]>, proof) => {
// Check if there's already an array for this unit, if not, create one
if (!acc[proof.unit]) {
if (!acc[proof.unit as MintUnit]) {
acc[proof.unit] = []
}
// Push the object into the array corresponding to its unit
@@ -276,6 +323,9 @@ export const LocalRecoveryScreen: FC<LocalRecoveryScreenProps> =
addedAmount,
)
}
const balanceAfter = proofsStore.getUnitBalance(unit as MintUnit)?.unitBalance || 0
await transactionsStore.updateBalanceAfter(transactionId, balanceAfter)
// Finally, update completed transaction
transactionData.push({
@@ -381,65 +431,23 @@ export const LocalRecoveryScreen: FC<LocalRecoveryScreenProps> =
<ListItem
text={'Number of proofs'}
RightComponent={
<Text text={`${proofs.length}`} />
<Text text={`${proofs.length}`} style={{marginRight: spacing.small}} />
}
/>
{proofs.some(p => p.unit === 'sat') && (
<ListItem
text={'SAT'}
RightComponent={
<CurrencyAmount
amount={CashuUtils.getProofsAmount(proofs.filter(p => p.unit === 'sat'))}
mintUnit='sat'
/>
}
/>
)}
{proofs.some(p => p.unit === 'msat') && (
<ListItem
text={'mSAT'}
RightComponent={
<CurrencyAmount
amount={CashuUtils.getProofsAmount(proofs.filter(p => p.unit === 'msat'))}
mintUnit='msat'
/>
}
/>
)}
{proofs.some(p => p.unit === 'btc') && (
<ListItem
text={'BTC'}
RightComponent={
<CurrencyAmount
amount={CashuUtils.getProofsAmount(proofs.filter(p => p.unit === 'btc'))}
mintUnit='btc'
/>
}
/>
)}
{proofs.some(p => p.unit === 'usd') && (
<ListItem
text={'USD'}
RightComponent={
<CurrencyAmount
amount={CashuUtils.getProofsAmount(proofs.filter(p => p.unit === 'usd'))}
mintUnit='usd'
/>
}
/>
)}
{proofs.some(p => p.unit === 'eur') && (
<ListItem
text={'EUR'}
RightComponent={
<CurrencyAmount
amount={CashuUtils.getProofsAmount(proofs.filter(p => p.unit === 'eur'))}
mintUnit='eur'
/>
}
/>
)}
{Object.values(MintUnits).map(unit =>
proofs.some(p => p.unit === unit) && (
<ListItem
key={unit}
text={getCurrency(unit).code}
RightComponent={
<CurrencyAmount
amount={CashuUtils.getProofsAmount(proofs.filter(p => p.unit === unit))}
mintUnit='sat'
containerStyle={{marginRight: spacing.extraSmall}}
/>
}
/>
))}
</>
}
FooterComponent={
@@ -475,7 +483,7 @@ export const LocalRecoveryScreen: FC<LocalRecoveryScreenProps> =
<View style={$bottomContainer}>
<View style={$buttonContainer}>
<Button
onPress={recoverProofs}
onPress={onRecovery}
text={`Recover to wallet`}
/>
</View>
+12 -6
View File
@@ -224,6 +224,12 @@ export const RemoteRecoveryScreen: FC<AppStackScreenProps<'RemoteRecovery'>> = o
try {
if(!recoveredMint) {
setInfo('Please select the mint.')
return
}
if(!selectedKeyset) {
setInfo('Please select keyset.')
return
}
@@ -236,7 +242,7 @@ export const RemoteRecoveryScreen: FC<AppStackScreenProps<'RemoteRecovery'>> = o
{
indexFrom: startIndex,
indexTo: endIndex,
keysetId: selectedKeyset?.id as string
keysetId: selectedKeyset.id as string
}
)
@@ -245,7 +251,7 @@ export const RemoteRecoveryScreen: FC<AppStackScreenProps<'RemoteRecovery'>> = o
if (proofs.length > 0) {
// need to move counter by whole interval to avoid duplicate _B!!!
recoveredMint.increaseProofsCounter(selectedKeyset?.id as string, Math.abs(endIndex - startIndex))
recoveredMint.increaseProofsCounter(selectedKeyset.id as string, Math.abs(endIndex - startIndex))
const {spent, pending} = await MintClient.getSpentOrPendingProofsFromMint(
recoveredMint.mintUrl,
@@ -279,7 +285,7 @@ export const RemoteRecoveryScreen: FC<AppStackScreenProps<'RemoteRecovery'>> = o
type: TransactionType.RECEIVE,
amount,
fee: 0,
unit: selectedKeyset?.unit as MintUnit,
unit: selectedKeyset.unit as MintUnit,
data: JSON.stringify(transactionData),
memo: 'Wallet recovery from seed',
mint: recoveredMint.mintUrl,
@@ -293,7 +299,7 @@ export const RemoteRecoveryScreen: FC<AppStackScreenProps<'RemoteRecovery'>> = o
recoveredMint.mintUrl,
unspent,
{
unit: selectedKeyset?.unit as MintUnit,
unit: selectedKeyset.unit as MintUnit,
transactionId: pendingTransactionId as number,
isPending: false
}
@@ -321,8 +327,8 @@ export const RemoteRecoveryScreen: FC<AppStackScreenProps<'RemoteRecovery'>> = o
JSON.stringify(transactionData),
)
const balanceAfter = proofsStore.getBalances().mintBalances[recoveredMint.mintUrl as any].balances[selectedKeyset?.unit as MintUnit]
await transactionsStore.updateBalanceAfter(transactionId, balanceAfter as number)
const balanceAfter = proofsStore.getUnitBalance(selectedKeyset.unit as MintUnit)?.unitBalance
await transactionsStore.updateBalanceAfter(transactionId, balanceAfter || 0)
}
if(pending && pending.length > 0) {
+1 -1
View File
@@ -556,7 +556,7 @@ const restore = async function (
proofs: proofs || []
}
} catch (e: any) {
throw new AppError(Err.MINT_ERROR, e.message.toString(), {mintUrl})
throw new AppError(Err.MINT_ERROR, isObj(e.message) ? JSON.stringify(e.message) : e.message, {mintUrl})
}
}
+26 -1
View File
@@ -226,10 +226,13 @@ const _runMigrations = function (db: QuickSQLiteConnection) {
migrationQueries.push([
`UPDATE transactions
SET unit = ?`, ['sat']
], [
`UPDATE proofs
SET unit = ?`, ['sat']
])
log.info(`Prepared database migrations from ${currentVersion} -> 10`)
}
}
// Update db version as a part of migration sqls
migrationQueries.push([
@@ -1051,6 +1054,27 @@ const getProofsByTransaction = function (transactionId: number): BackupProof[] {
}
}
const updateProofsToDefaultUnit = async function () {
try {
const query = `
UPDATE proofs
SET unit = ?
WHERE unit IS NULL OR unit = ''
`
const params = ['sat']
const _db = getInstance()
const result = await _db.executeAsync(query, params)
log.info('[migrateProofsToDefaultUnit] executed in the database', {result})
} catch (e: any) {
throw new AppError(Err.DATABASE_ERROR, 'Could not migrateProofsToDefaultUnit in database', e.message)
}
}
export const Database = {
getInstance,
getDatabaseVersion,
@@ -1077,4 +1101,5 @@ export const Database = {
getProofById,
getProofs,
getProofsByTransaction,
updateProofsToDefaultUnit
}