mirror of
https://github.com/minibits-cash/minibits_wallet.git
synced 2026-10-06 11:48:24 +00:00
Full recovery from local backup, fix in case migration during upgrade fails
This commit is contained in:
+1
-1
@@ -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",
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user