Skip to content

Commit

Permalink
Improved wallet backup and recovery
Browse files Browse the repository at this point in the history
  • Loading branch information
minibits-cash committed Jan 13, 2025
1 parent b4695c0 commit 5d8f90d
Show file tree
Hide file tree
Showing 15 changed files with 534 additions and 537 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.10-beta.17",
"version": "0.1.10-beta.18",
"private": true,
"scripts": {
"android:clean": "cd android && ./gradlew clean",
Expand Down
7 changes: 6 additions & 1 deletion src/models/ContactsStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,13 @@ import { MINIBITS_NIP05_DOMAIN } from '@env'
self.lastPendingReceivedCheck = ts2
log.trace('[setLastPendingReceivedCheck]', {ts2})
},
addReceivedEventId(id: string) {
addReceivedEventId(id: string) {
if(self.receivedEventIds.includes(id)) {
return
}

const num = self.receivedEventIds.unshift(id)

if(num > 50) {
self.receivedEventIds.pop()
}
Expand Down
9 changes: 3 additions & 6 deletions src/models/WalletProfileStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,12 +143,9 @@ export const WalletProfileStoreModel = types
})
}))
.actions(self => ({
create: flow(function* create(walletId: string) {

const {publicKey} = yield NostrClient.getOrCreateKeyPair()
const seedHash: string = yield KeyChain.loadSeedHash() // used to recover wallet address
create: flow(function* create(publicKey: string, walletId: string, seedHash: string) {

let profileRecord: WalletProfileRecord

self.seedHash = seedHash

log.trace('[create]', {seedHash, publicKey})
Expand Down Expand Up @@ -240,7 +237,7 @@ export const WalletProfileStoreModel = types
}
)

// in seed based or backup import recovery we rotate the wallet seed to provided one
// rotate the wallet seed to the provided one only on fresh install recovery from seed or import backup
if(!isAddressOnly) {
self.seedHash = seedHash
}
Expand Down
5 changes: 4 additions & 1 deletion src/navigation/AppNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
SeedRecoveryScreen,
MintsScreen,
RecoveryOptionsScreen,
RecoverWalletAddressScreen,
ImportBackupScreen
} from "../screens"
import { useStores } from "../models"
Expand All @@ -44,7 +45,8 @@ export type AppStackParamList = {
Welcome: undefined
RecoveryOptions: {fromScreen?: string}
SeedRecovery: undefined
ImportBackup: {isAddressOnlyRecovery: boolean}
ImportBackup: undefined
RecoverWalletAddress: undefined
Mints: {}
Tabs: NavigatorScreenParams<TabsParamList>
}
Expand Down Expand Up @@ -81,6 +83,7 @@ const AppStack = observer(function AppStack() {
<Stack.Screen name="RecoveryOptions" component={RecoveryOptionsScreen} />
<Stack.Screen name="SeedRecovery" component={SeedRecoveryScreen} />
<Stack.Screen name="ImportBackup" component={ImportBackupScreen} />
<Stack.Screen name="RecoverWalletAddress" component={RecoverWalletAddressScreen} />
<Stack.Screen name="Mints" component={MintsScreen} />
{!userSettingsStore.isUserOnboarded && (
<Stack.Screen name="Tabs" component={TabsNavigator} />
Expand Down
29 changes: 1 addition & 28 deletions src/screens/ContactsScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,41 +18,14 @@ import { verticalScale } from '@gocodingnow/rn-size-matters'
interface ContactsScreenProps extends ContactsStackScreenProps<'Contacts'> {}

export const ContactsScreen: FC<ContactsScreenProps> = observer(function ContactsScreen({route, navigation}) {
const {userSettingsStore, walletProfileStore} = useStores()
const {walletProfileStore} = useStores()

let paymentOption: ReceiveOption | SendOption | undefined

if(route.params && route.params.paymentOption) {
paymentOption = route.params.paymentOption
}


useEffect(() => {
const load = async () => {
try {
log.trace(walletProfileStore)

if(!walletProfileStore.pubkey || !walletProfileStore.picture) {
// pic check needed to be sure profile does not exists on the server
// create random name, NIP05 identifier, random picture and sharable profile
// announce new profile to the added default public and minibits relays

// this is now done in wallet onboarding so this serves only as a recovery in case
// of wallet state loss
const walletId = userSettingsStore.walletId
await walletProfileStore.create(walletId as string)
}

} catch(e: any) {
log.error(e.name, e.message)
return false // silent
}
}
load()
return () => {}
}, [])


const renderScene = ({route}: {route: Route}) => {
switch (route.key) {
case 'first':
Expand Down
178 changes: 22 additions & 156 deletions src/screens/ExportBackupScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ import { Transaction, TransactionStatus } from '../models/Transaction'
import { ResultModalInfo } from './Wallet/ResultModalInfo'
import { verticalScale } from '@gocodingnow/rn-size-matters'
import { Token, getDecodedToken, getEncodedToken } from '@cashu/cashu-ts'
import { encodeCBOR } from '@cashu/cashu-ts/src/cbor'
import { encodeUint8toBase64Url } from '@cashu/cashu-ts/src/base64'

interface ExportBackupScreenProps extends SettingsStackScreenProps<'ExportBackup'> {}

Expand Down Expand Up @@ -284,8 +286,14 @@ export const ExportBackupScreen: FC<ExportBackupScreenProps> =
}
}

if(isMintsInBackup) {
exportedMintsStore = getSnapshot(mintsStore)
if(isMintsInBackup) {
exportedMintsStore = JSON.parse(JSON.stringify(getSnapshot(mintsStore)))

exportedMintsStore.mints.forEach((mint: any) => {
mint.keys = [];
})

//log.trace({exportedMintsStore})
}

if(isContactsInBackup) {
Expand All @@ -299,8 +307,19 @@ export const ExportBackupScreen: FC<ExportBackupScreenProps> =
}

log.trace({exportedSnapshot})

const prefix = 'minibits'
const version = 'A'

// CBOR - WIP, not working
// const encodedData = encodeCBOR(exportedSnapshot)
// const base64Data = encodeUint8toBase64Url(encodedData)

// Simple BASE64
const base64Data = btoa(JSON.stringify(exportedSnapshot))

const base64Encoded = prefix + version + base64Data

const base64Encoded = btoa(JSON.stringify(exportedSnapshot))
Clipboard.setString(base64Encoded)
setIsLoading(false)

Expand Down Expand Up @@ -410,159 +429,6 @@ export const ExportBackupScreen: FC<ExportBackupScreenProps> =
}
}


/* const onRecovery = async function () {
if (!showUnspentOnly) {
setInfo(translate("unspentOnlyRecoverable"))
return
}
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 = translate("backupWillOverwriteBalanceWarning")
message += "\n\n"
}
message += translate("confirmBackupRecovery")
Alert.alert(
translate("attention"),
message,
[
{
text: translate('common.cancel'),
style: 'cancel',
onPress: () => {
// Action canceled
},
},
{
text: translate("startRecovery"),
onPress: () => {
try {
doLocalRecovery()
} catch (e: any) {
handleError(e)
}
},
},
],
)
}
const doLocalRecovery = async function () {
try {
if(!showUnspentOnly) {
setInfo(translate('unspentOnlyRecoverable'))
return
}
if(mintsStore.allMints.length === 0) {
setInfo(translate('missingMintsForProofsUserMessage'))
}
setIsLoading(true)
const groupedByMint = groupProofsByMint(proofs)
await transactionsStore.expireAllAfterRecovery()
for (const mint in groupedByMint) {
const proofsByMint = groupedByMint[mint]
if(proofsByMint.length === 0) {
continue
}
proofsStore.removeOnLocalRecovery(proofsByMint, false)
const groupedByKeyset = groupProofsByKeysets(proofsByMint)
for (const keysetId in groupedByKeyset) {
const proofsByKeysetId = groupedByKeyset[keysetId]
const proofsToImport: ProofV3[] = []
for (const proof of proofsByKeysetId) {
const { tId, unit, isPending, isSpent, updatedAt, ...proofToImport } = proof
proofsToImport.push(proofToImport)
}
if(proofsToImport.length === 0) {
continue
}
const amount = sumProofs(proofsToImport)
const unit = proofsByKeysetId[0].unit
log.trace('[doLocalRecovery] to be recovered', {mint, keysetId, unit, amount})
let transactionData: TransactionData[] = []
transactionData.push({
status: TransactionStatus.PREPARED,
amount,
createdAt: new Date(),
})
const newTransaction = {
type: TransactionType.RECEIVE,
amount,
fee: 0,
unit: unit as MintUnit,
data: JSON.stringify(transactionData),
memo: 'Recovery from backup',
mint: mint,
status: TransactionStatus.PREPARED,
}
const transaction = await transactionsStore.addTransaction(newTransaction)
const { amountToAdd, addedAmount } = WalletUtils.addCashuProofs(
mint,
proofsToImport,
{
unit: unit as MintUnit,
transactionId: transaction.id,
isPending: false
}
)
if (amountToAdd !== addedAmount) {
transaction.setReceivedAmount(addedAmount)
}
const balanceAfter = proofsStore.getUnitBalance(unit as MintUnit)?.unitBalance || 0
transaction.setBalanceAfter(balanceAfter)
// Finally, update completed transaction
transactionData.push({
status: TransactionStatus.COMPLETED,
addedAmount,
createdAt: new Date(),
})
transaction.setStatus(
TransactionStatus.COMPLETED,
JSON.stringify(transactionData),
)
}
}
setIsLoading(false)
} catch (e: any) {
handleError(e)
}
} */


const handleError = function (e: AppError): void {
setIsLoading(false)
setError(e)
Expand Down
Loading

0 comments on commit 5d8f90d

Please sign in to comment.