diff --git a/babel.config.js b/babel.config.js index f9b56f53..e20b34ff 100644 --- a/babel.config.js +++ b/babel.config.js @@ -1,6 +1,6 @@ module.exports = { presets: ['module:metro-react-native-babel-preset'], - plugins: [ + plugins: [ [ 'module-resolver', { diff --git a/package.json b/package.json index 6b3d95af..3349c2ef 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "react-native-json-tree": "^1.3.0", "react-native-keychain": "^8.1.1", "react-native-logs": "^5.0.1", - "react-native-mmkv": "^2.8.0", + "react-native-mmkv": "^2.11.0", "react-native-modal": "^13.0.1", "react-native-pager-view": "^6.2.0", "react-native-qrcode-svg": "^6.2.0", diff --git a/patches/@cashu+cashu-ts+0.8.2-rc.7.patch b/patches/@cashu+cashu-ts+0.8.2-rc.7.patch index 189e4213..f9ee1491 100644 --- a/patches/@cashu+cashu-ts+0.8.2-rc.7.patch +++ b/patches/@cashu+cashu-ts+0.8.2-rc.7.patch @@ -13,7 +13,7 @@ index af7649e..91a54c4 100644 "type": "git", "url": "git+https://github.com/cashubtc/cashu-ts" diff --git a/node_modules/@cashu/cashu-ts/src/CashuMint.ts b/node_modules/@cashu/cashu-ts/src/CashuMint.ts -index f6bc3b1..0bfe1dc 100644 +index f6bc3b1..d055786 100644 --- a/node_modules/@cashu/cashu-ts/src/CashuMint.ts +++ b/node_modules/@cashu/cashu-ts/src/CashuMint.ts @@ -6,14 +6,14 @@ import { @@ -281,7 +281,7 @@ index f6bc3b1..0bfe1dc 100644 method: 'POST', requestBody: restorePayload diff --git a/node_modules/@cashu/cashu-ts/src/CashuWallet.ts b/node_modules/@cashu/cashu-ts/src/CashuWallet.ts -index b7cf354..d0a9af2 100644 +index b7cf354..0565096 100644 --- a/node_modules/@cashu/cashu-ts/src/CashuWallet.ts +++ b/node_modules/@cashu/cashu-ts/src/CashuWallet.ts @@ -1,7 +1,7 @@ @@ -315,7 +315,29 @@ index b7cf354..d0a9af2 100644 /** * Class that represents a Cashu wallet. -@@ -69,13 +69,16 @@ class CashuWallet { +@@ -43,15 +43,18 @@ class CashuWallet { + * @param mint Cashu mint instance is used to make api calls + * @param mnemonic mnemonic seed phrase to initial derivation key for this wallets deterministic secrets + */ +- constructor(mint: CashuMint, keys?: MintKeys, mnemonic?: string) { ++ constructor(mint: CashuMint, keys?: MintKeys, seed?: Uint8Array) { + this._keys = keys || {}; + this.mint = mint; + if (keys) { + this._keysetId = deriveKeysetId(this._keys); + } +- if (mnemonic) { ++ /* if (mnemonic) { + this._seed = deriveSeedFromMnemonic(mnemonic); +- } ++ }*/ ++ if (seed) { ++ this._seed = seed; ++ } + } + + get keys(): MintKeys { +@@ -69,13 +72,16 @@ class CashuWallet { * @param proofs (only the 'secret' field is required) * @returns */ @@ -335,7 +357,7 @@ index b7cf354..d0a9af2 100644 } /** * Starts a minting process by requesting an invoice from the mint -@@ -512,8 +515,8 @@ class CashuWallet { +@@ -512,8 +518,8 @@ class CashuWallet { const secrets: Array = []; const rs: Array = []; for (let i = 0; i < amounts.length; i++) { diff --git a/src/components/InfoModal.tsx b/src/components/InfoModal.tsx index 9b129c74..9f5a80d7 100644 --- a/src/components/InfoModal.tsx +++ b/src/components/InfoModal.tsx @@ -31,7 +31,7 @@ export const InfoModal: FC = function ({ message }) { onBackdropPress={() => onClose()} onBackButtonPress={() => onClose()} ContentComponent={ - + {message} diff --git a/src/components/Loading.tsx b/src/components/Loading.tsx index d7ee9274..5d04a4c9 100644 --- a/src/components/Loading.tsx +++ b/src/components/Loading.tsx @@ -1,18 +1,20 @@ import * as React from 'react' import { StyleSheet, View, ActivityIndicator, ViewProps } from 'react-native' import { useThemeColor } from '../theme' +import { Text } from './Text' -export function Loading(props: ViewProps) { +export function Loading(props: ViewProps & {statusMessage?: string, opacity?: number}) { return ( + {props.statusMessage && ()} ) } \ No newline at end of file diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 2655be99..5fa54744 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -59,7 +59,7 @@ const en = { other: "{{count}} mints have been added to your wallet", }, toReceive: "Amount to receive", - received: "Amount received:", + received: "Amount received", sharePaymentRequest: "Send payment request", sharePaymentRequestDescription: "Send payment request to one of your contacts, so that the payer can pay you exact amount.", scanToReceive: "Scan or paste to receive", diff --git a/src/models/ProofsStore.ts b/src/models/ProofsStore.ts index d001c659..c4be9217 100644 --- a/src/models/ProofsStore.ts +++ b/src/models/ProofsStore.ts @@ -62,10 +62,11 @@ export const ProofsStoreModel = types }, })) .actions(self => ({ - addProofs(newProofs: Proof[], isPending: boolean = false): number { + addProofs(newProofs: Proof[], isPending: boolean = false): {addedAmount: number, addedProofs: Proof[]} { try { const proofs = isPending ? self.pendingProofs : self.proofs let addedAmount: number = 0 + let addedProofs: Proof[] = [] for (const proof of newProofs) { if(self.alreadyExists(proof)) { @@ -81,18 +82,23 @@ export const ProofsStoreModel = types } addedAmount += proof.amount + addedProofs.push(proof) } - log.debug('[addProofs]', `Added new ${newProofs.length}${isPending ? ' pending' : ''} proofs to the ProofsStore`,) + // Handle counter increment + const mintsStore = getRootStore(self).mintsStore + mintsStore.increaseProofsCounter(newProofs[0].mintUrl as string, addedProofs.length) + + log.debug('[addProofs]', `Added new ${addedProofs.length}${isPending ? ' pending' : ''} proofs to the ProofsStore`,) const rootStore = getRootStore(self) const {userSettingsStore} = rootStore - if (userSettingsStore.isLocalBackupOn === true) { - Database.addOrUpdateProofs(newProofs, isPending) // isSpent = false + if (userSettingsStore.isLocalBackupOn === true && addedProofs.length > 0) { + Database.addOrUpdateProofs(addedProofs, isPending) // isSpent = false } - return addedAmount + return { addedAmount, addedProofs } } catch (e: any) { throw new AppError(Err.STORAGE_ERROR, e.message) } diff --git a/src/screens/BackupScreen.tsx b/src/screens/BackupScreen.tsx index 1ff0dbb9..e1b6f3a0 100644 --- a/src/screens/BackupScreen.tsx +++ b/src/screens/BackupScreen.tsx @@ -110,76 +110,76 @@ export const BackupScreen: FC> = observer(fun - - - - - } - style={$item} - /> - {isLocalBackupOn && ( - + - )} - - } - /> - - - - } - /> - - - - } - /> + onPress={gotoRemoteBackup} + /> + + } + /> + + + + + } + style={$item} + /> + {isLocalBackupOn && ( + + )} + + } + /> + + + + } + /> {isLoading && } = observer(function Contact const renderScene = ({route}: {route: Route}) => { switch (route.key) { case 'first': - return + return case 'second': return default: diff --git a/src/screens/DeveloperScreen.tsx b/src/screens/DeveloperScreen.tsx index fa0ac3f4..80a9972d 100644 --- a/src/screens/DeveloperScreen.tsx +++ b/src/screens/DeveloperScreen.tsx @@ -117,23 +117,27 @@ export const DeveloperScreen: FC> = observ { text: 'Confirm', onPress: async () => { - setIsLoading(true) - try { - // Delete database - Database.cleanAll() - // Delete Nostr keys - await KeyChain.removeNostrKeypair() - // Delete Encryption key - await KeyChain.removeMmkvEncryptionKey() - // Clean mobx storage - MMKVStorage.clearAll() - // recreate db schema - Database.getInstance() - setIsLoading(false) - setInfo('Factory reset completed, please restart immediately.') - } catch (e: any) { - handleError(e) - } + setIsLoading(true) + try { + // Delete database + Database.cleanAll() + // Delete Nostr keys + await KeyChain.removeNostrKeypair() + // Delete Encryption key + await KeyChain.removeMmkvEncryptionKey() + // Delete mnemonic + await KeyChain.removeMnemonic() + // Delete seed + await KeyChain.removeSeed() + // Clean mobx storage + MMKVStorage.clearAll() + // recreate db schema + Database.getInstance() + setIsLoading(false) + setInfo('Factory reset completed, please restart immediately.') + } catch (e: any) { + handleError(e) + } }, }, ], diff --git a/src/screens/MintsScreen.tsx b/src/screens/MintsScreen.tsx index 05550efc..36466b12 100644 --- a/src/screens/MintsScreen.tsx +++ b/src/screens/MintsScreen.tsx @@ -25,6 +25,7 @@ import AppError from '../utils/AppError' import {translate} from '../i18n' import {MintListItem} from './Mints/MintListItem' import { infoMessage } from '../utils/utils' +import { spyReportEnd } from 'mobx/dist/internal' export const MintsScreen: FC> = observer(function MintsScreen({route, navigation}) { @@ -37,6 +38,7 @@ export const MintsScreen: FC> = observer(funct const mintInputRef = useRef(null) const [mintUrl, setMintUrl] = useState('') + const [defaultMintUrl, setDefaultMintUrl] = useState('https://mint.minibits.cash/Bitcoin') const [selectedMint, setSelectedMint] = useState() const [info, setInfo] = useState('') const [error, setError] = useState() @@ -117,6 +119,12 @@ export const MintsScreen: FC> = observer(funct } + const addDefaultMint = async function () { + setMintUrl(defaultMintUrl) + toggleAddMintModal() + } + + const removeMint = async function () { if (!selectedMint) {return} @@ -129,8 +137,6 @@ export const MintsScreen: FC> = observer(funct message += 'Do you really want to remove this mint from the wallet?' - - Alert.alert( 'Warning', message, @@ -220,6 +226,7 @@ export const MintsScreen: FC> = observer(funct > = observer(funct style={$actionItem} onPress={toggleAddMintModal} /> + {!mintsStore.alreadyExists(defaultMintUrl) && ( + + } + style={$actionItem} + onPress={addDefaultMint} + topSeparator={true} + /> + )} + } /> {mintsStore.mintCount > 0 && ( @@ -313,6 +336,7 @@ export const MintsScreen: FC> = observer(funct bottomSeparator={true} style={{paddingHorizontal: spacing.medium}} /> + } onBackButtonPress={onMintUnselect} @@ -404,8 +428,9 @@ const $contentContainer: TextStyle = { const $actionCard: ViewStyle = { marginBottom: spacing.small, - marginTop: -spacing.extraLarge * 1.5, + marginTop: -spacing.extraLarge * 2, minHeight: 70, + paddingVertical: 0, } const $actionItem: ViewStyle = { diff --git a/src/screens/RemoteBackupScreen.tsx b/src/screens/RemoteBackupScreen.tsx index 0140c686..b58ffc5e 100644 --- a/src/screens/RemoteBackupScreen.tsx +++ b/src/screens/RemoteBackupScreen.tsx @@ -18,7 +18,7 @@ import { } from '../components' import {useHeader} from '../utils/useHeader' import AppError, { Err } from '../utils/AppError' -import { log, RestoreClient } from '../services' +import { log, MintClient } from '../services' import { scale } from '@gocodingnow/rn-size-matters' import Clipboard from '@react-native-clipboard/clipboard' @@ -35,34 +35,34 @@ export const RemoteBackupScreen: FC> = // const {userSettingsStore} = useStores() const [info, setInfo] = useState('') - const [seed, setSeed] = useState() - const [seedArray, setSeedArray] = useState([]) + const [mnemonic, setMnemonic] = useState() + const [mnemonicArray, setMnemonicArray] = useState([]) const [isLoading, setIsLoading] = useState(false) const [error, setError] = useState() useEffect(() => { - const getSeed = async () => { + const getmnemonic = async () => { try { setIsLoading(true) - const seed = await RestoreClient.getOrCreateSeed() - setSeed(seed) - setSeedArray(seed.split(/\s+/)) + const mnemonic = await MintClient.getOrCreateMnemonic() + setMnemonic(mnemonic) + setMnemonicArray(mnemonic.split(/\s+/)) setIsLoading(false) } catch (e: any) { handleError(e) } } - getSeed() + getmnemonic() }, []) const onCopy = function (): void { try { - if(seed) { - Clipboard.setString(seed) + if(mnemonic) { + Clipboard.setString(mnemonic) return } - throw new AppError(Err.VALIDATION_ERROR, 'Missing seed.') + throw new AppError(Err.VALIDATION_ERROR, 'Missing mnemonic.') } catch (e: any) { setInfo(`Could not copy: ${e.message}`) } @@ -85,7 +85,7 @@ export const RemoteBackupScreen: FC> = style={$card} ContentComponent={ > = ContentComponent={ <> { return( diff --git a/src/screens/RemoteRecoveryScreen.tsx b/src/screens/RemoteRecoveryScreen.tsx index a2949fea..039ac854 100644 --- a/src/screens/RemoteRecoveryScreen.tsx +++ b/src/screens/RemoteRecoveryScreen.tsx @@ -1,8 +1,10 @@ import {observer} from 'mobx-react-lite' import React, {FC, useEffect, useRef, useState} from 'react' -import {FlatList, ImageBackground, LayoutAnimation, Linking, Platform, Switch, TextInput, TextStyle, UIManager, View, ViewStyle} from 'react-native' +import {LayoutAnimation, Platform, Switch, TextInput, TextStyle, UIManager, View, ViewStyle} from 'react-native' +import {validateMnemonic} from '@scure/bip39' +import { wordlist } from '@scure/bip39/wordlists/english' import {colors, spacing, useThemeColor} from '../theme' -import {AppStackScreenProps, SettingsStackScreenProps} from '../navigation' // @demo remove-current-line +import {AppStackScreenProps} from '../navigation' // @demo remove-current-line import { Icon, ListItem, @@ -18,25 +20,27 @@ import { } from '../components' import {useHeader} from '../utils/useHeader' import AppError, { Err } from '../utils/AppError' -import { log, MintClient, MintKeys, RestoreClient } from '../services' +import { KeyChain, log, MintClient, MintKeys } from '../services' import Clipboard from '@react-native-clipboard/clipboard' import { useStores } from '../models' import { MintListItem } from './Mints/MintListItem' import { Mint } from '../models/Mint' -import { deriveKeysetId } from '@cashu/cashu-ts' +import { CashuMint, deriveKeysetId } from '@cashu/cashu-ts' import { CashuUtils } from '../services/cashu/cashuUtils' import { Proof } from '../models/Proof' import { type Proof as CashuProof, } from '@cashu/cashu-ts' import { Transaction, TransactionData, TransactionRecord, TransactionStatus, TransactionType } from '../models/Transaction' +import { ResultModalInfo } from './Wallet/ResultModalInfo' +import { deriveSeedFromMnemonic } from '@cashu/cashu-ts/src/secrets' if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) { UIManager.setLayoutAnimationEnabledExperimental(true) } -const RESTORE_INDEX_INTERVAL = 100 +const RESTORE_INDEX_INTERVAL = 50 export const RemoteRecoveryScreen: FC> = observer(function RemoteRecoveryScreen(_props) { const {navigation, route} = _props @@ -48,68 +52,96 @@ export const RemoteRecoveryScreen: FC> = o }) const {mintsStore, proofsStore, userSettingsStore, transactionsStore} = useStores() - const seedInputRef = useRef(null) + const mnemonicInputRef = useRef(null) const [info, setInfo] = useState('') - const [seed, setSeed] = useState('') - const [seedExists, setSeedExists] = useState(false) - const [isValidSeed, setIsValidSeed] = useState(false) + const [mnemonic, setMnemonic] = useState('') + const [mnemonicExists, setMnemonicExists] = useState(false) + const [isValidMnemonic, setIsValidMnemonic] = useState(false) + const [seed, setSeed] = useState() const [startIndex, setStartIndex] = useState(0) // start of interval of indexes of proofs to recover const [endIndex, setEndIndex] = useState(RESTORE_INDEX_INTERVAL) // end of interval const [isLoading, setIsLoading] = useState(false) const [error, setError] = useState() const [isErrorsModalVisible, setIsErrorsModalVisible] = useState(false) + const [resultModalInfo, setResultModalInfo] = useState<{status: TransactionStatus, message: string} | undefined>() + const [isResultModalVisible, setIsResultModalVisible] = useState(false) + const [lastRecoveredAmount, setLastRecoveredAmount] = useState(0) const [recoveryErrors, setRecoveryErrors] = useState([]) + const [statusMessage, setStatusMessage] = useState() useEffect(() => { - const getSeed = async () => { + const getMnemonic = async () => { try { setIsLoading(true) - const existing = await RestoreClient.getSeed() + const existing = await MintClient.getMnemonic() if(existing) { - //setSeedExists(true) // TEMP!!! + setMnemonicExists(true) } setIsLoading(false) } catch (e: any) { handleError(e) } } - getSeed() + getMnemonic() }, []) + const toggleResultModal = () => { + if(isResultModalVisible === true) { + setResultModalInfo(undefined) + } + setIsResultModalVisible(previousState => !previousState) + } + + + const toggleErrorsModal = () => { + setIsErrorsModalVisible(previousState => !previousState) + } + + const onPaste = async function () { try { - const maybeSeed = await Clipboard.getString() + const maybeMnemonic = await Clipboard.getString() - if(!maybeSeed) { - throw new AppError(Err.VALIDATION_ERROR, 'Missing seed phrase.') + if(!maybeMnemonic) { + throw new AppError(Err.VALIDATION_ERROR, 'Missing mnemonic phrase.') } - setSeed(maybeSeed) + setMnemonic(maybeMnemonic) } catch (e: any) { handleError(e) } } - const onConfirm = function (): void { + const onConfirm = async function () { try { + setStatusMessage('Deriving seed, this takes a while...') setIsLoading(true) - if(!seed) { - throw new AppError(Err.VALIDATION_ERROR, 'Missing seed.') + if(!mnemonic) { + throw new AppError(Err.VALIDATION_ERROR, 'Missing mnemonic.') } - const seedArray: string[] = seed.trim().split(/\s+/) - if(seedArray.length !== 12) { - // throw new AppError(Err.VALIDATION_ERROR, 'Invalid seed phrase. Provide 12 word sequence separated by blank spaces.') // TEMP!!! - } + /* const mnemonicArray: string[] = mnemonic.trim().split(/\s+/) + if(mnemonicArray.length !== 12) { + throw new AppError(Err.VALIDATION_ERROR, 'Invalid mnemonic phrase. Provide 12 word sequence separated by blank spaces.') + } */ + if (!validateMnemonic(mnemonic, wordlist)) { + throw new AppError(Err.VALIDATION_ERROR, 'Invalid mnemonic phrase. Provide 12 words sequence separated by blank spaces.') + } - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) + // - setIsValidSeed(true) - setIsLoading(false) + setTimeout(() => { + // LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) + + const binarySeed = deriveSeedFromMnemonic(mnemonic) // expensive + setSeed(binarySeed) + setIsValidMnemonic(true) + setIsLoading(false) + }, 200) } catch (e: any) { handleError(e) } @@ -125,12 +157,20 @@ export const RemoteRecoveryScreen: FC> = o } - const doRecovery = async function () { + const startRecovery = async function () { + setStatusMessage('Starting recovery...') + setIsLoading(true) + setTimeout(() => doRecovery(), 200) + } - setIsLoading(true) + const doRecovery = async function () { let errors: AppError[] = [] + let recoveredAmount: number = 0 + let alreadySpentAmount: number = 0 - for (const mint of mintsStore.allMints) { + setStatusMessage('Loading mints...') + + for (const mint of mintsStore.allMints) { const transactionData: TransactionData[] = [] let transactionId: number = 0 @@ -140,24 +180,41 @@ export const RemoteRecoveryScreen: FC> = o try { // TODO allow input or get previous keysets from mint and try to restore from them + setStatusMessage(`Restoring from ${mint.hostname}...`) + log.info('[restore]', `Restoring from ${mint.hostname}...`) + const { proofs, newKeys } = await MintClient.restore( mint.mintUrl, startIndex, - endIndex + endIndex, + seed as Uint8Array ) + + log.debug('[restore]', `Restored proofs`, proofs.length) + setStatusMessage(`Found ${proofs.length} proofs...`) if(newKeys) {updateMintKeys(mint.mintUrl as string, newKeys)} - + const {spent, pending} = await MintClient.getSpentOrPendingProofsFromMint( mint.mintUrl, proofs as Proof[] ) - const unspent = proofs.filter(proof => !spent.includes(proof)) + log.debug('[restore]', `Spent and pending proofs`, {spent: spent.length, pending: pending.length}) + setStatusMessage(`${spent.length} proofs were already spent...`) + + const spentAmount = CashuUtils.getProofsAmount(spent as Proof[]) + alreadySpentAmount += spentAmount + + const unspent = proofs.filter(proof => !spent.includes(proof)) + if(unspent && unspent.length > 0) { + + setStatusMessage(`Completing recovery...`) const amount = CashuUtils.getProofsAmount(unspent as Proof[]) + recoveredAmount += amount // Let's create new draft receive transaction in database transactionData.push({ @@ -170,7 +227,7 @@ export const RemoteRecoveryScreen: FC> = o type: TransactionType.RECEIVE, amount, data: JSON.stringify(transactionData), - memo: 'Recovered ecash', + memo: 'Wallet recovery', mint: mint.mintUrl, status: TransactionStatus.PREPARED, } @@ -178,11 +235,20 @@ export const RemoteRecoveryScreen: FC> = o const draftTransaction: TransactionRecord = await transactionsStore.addTransaction(newTransaction) transactionId = draftTransaction.id as number - addCashuProofs( + const { amountToAdd, addedAmount } = addCashuProofs( unspent, mint.mintUrl, transactionId as number - ) + ) + + if (amountToAdd !== addedAmount) { + await transactionsStore.updateReceivedAmount( + transactionId as number, + addedAmount, + ) + + recoveredAmount = recoveredAmount - amount + addedAmount + } // Finally, update completed transaction transactionData.push({ @@ -190,14 +256,22 @@ export const RemoteRecoveryScreen: FC> = o createdAt: new Date(), }) - const completedTransaction = await transactionsStore.updateStatus( + await transactionsStore.updateStatus( transactionId, TransactionStatus.COMPLETED, JSON.stringify(transactionData), ) + + const balanceAfter = proofsStore.getBalances().totalBalance + await transactionsStore.updateBalanceAfter(transactionId, balanceAfter) } if(pending && pending.length > 0) { + + // infoMessage(`Found pending ecash...`) + setStatusMessage(`Found ${pending.length} pending proofs...`) + log.debug(`Found pending ecash with ${mint.hostname}...`) + const amount = CashuUtils.getProofsAmount(pending as Proof[]) // Let's create new draft receive transaction in database @@ -211,7 +285,7 @@ export const RemoteRecoveryScreen: FC> = o type: TransactionType.RECEIVE, amount, data: JSON.stringify(transactionData), - memo: 'Recovered pending ecash', + memo: 'Wallet recovery - pending', mint: mint.mintUrl, status: TransactionStatus.PREPARED, } @@ -219,20 +293,27 @@ export const RemoteRecoveryScreen: FC> = o const draftTransaction: TransactionRecord = await transactionsStore.addTransaction(newTransaction) pendingTransactionId = draftTransaction.id as number - addCashuProofs( + const { amountToAdd, addedAmount } = addCashuProofs( pending, mint.mintUrl, pendingTransactionId as number, true // isPending = true ) + if (amountToAdd !== addedAmount) { + await transactionsStore.updateReceivedAmount( + transactionId as number, + addedAmount, + ) + } + // Finally, update pending transaction pendingTransactionData.push({ status: TransactionStatus.PENDING, createdAt: new Date(), }) - const pendingTransaction = await transactionsStore.updateStatus( + await transactionsStore.updateStatus( pendingTransactionId, TransactionStatus.PENDING, JSON.stringify(pendingTransactionData), @@ -240,7 +321,12 @@ export const RemoteRecoveryScreen: FC> = o } } catch(e: any) { - e.params.mintUrl = mint.mintUrl + + if (mint) { + e.params = {mintUrl: mint.mintUrl} + } + + // log.error(e, {mintUrl: mint.mintUrl}) errors.push(e) if (transactionId > 0) { @@ -250,7 +336,7 @@ export const RemoteRecoveryScreen: FC> = o createdAt: new Date(), }) - const errorTransaction = await transactionsStore.updateStatus( + await transactionsStore.updateStatus( transactionId, TransactionStatus.ERROR, JSON.stringify(transactionData), @@ -264,25 +350,53 @@ export const RemoteRecoveryScreen: FC> = o createdAt: new Date(), }) - const errorTransaction = await transactionsStore.updateStatus( + await transactionsStore.updateStatus( pendingTransactionId, TransactionStatus.ERROR, JSON.stringify(pendingTransactionData), ) } - + setStatusMessage(undefined) continue } } + setLastRecoveredAmount(recoveredAmount) setStartIndex(startIndex + RESTORE_INDEX_INTERVAL) - setEndIndex(endIndex + RESTORE_INDEX_INTERVAL) + setEndIndex(endIndex + RESTORE_INDEX_INTERVAL) + setStatusMessage(undefined) setIsLoading(false) - - if(errors.length > 0) { - setRecoveryErrors(errors) - toggleErrorsModal() // open + + if(recoveredAmount > 0) { + setResultModalInfo({ + status: TransactionStatus.COMPLETED, + message: `${recoveredAmount} sats were recovered into your wallet.` + }) + } else { + if(errors.length > 0) { + setResultModalInfo({ + status: TransactionStatus.ERROR, + message: `Recovery ended up with errors.` + }) + setRecoveryErrors(errors) + } else { + if(alreadySpentAmount > 0) { + setResultModalInfo({ + status: TransactionStatus.EXPIRED, + message: `Good news is that already spent ecash has been found. Continue with next recovery interval.` + }) + } else { + setResultModalInfo({ + status: TransactionStatus.EXPIRED, + message: `Nothing has been found in this recovery interval.` + }) + } + + } } + + toggleResultModal() // open + } @@ -297,7 +411,7 @@ export const RemoteRecoveryScreen: FC> = o const keyset = deriveKeysetId(newKeys) const mint = mintsStore.findByUrl(mintUrl) - return mint?.updateKeys(keyset, newKeys) + return mint?.updateKeys(keyset, newKeys) // TODO make only newKeys as param } @@ -309,7 +423,8 @@ export const RemoteRecoveryScreen: FC> = o isPending: boolean = false ): { amountToAdd: number, - addedAmount: number + addedAmount: number, + addedProofs: Proof[] } { // Add internal references for (const proof of proofsToAdd as Proof[]) { @@ -319,13 +434,14 @@ export const RemoteRecoveryScreen: FC> = o const amountToAdd = CashuUtils.getProofsAmount(proofsToAdd as Proof[]) // Creates proper model instances and adds them to the wallet - const addedAmount = proofsStore.addProofs(proofsToAdd as Proof[], isPending) - + const {addedAmount, addedProofs} = proofsStore.addProofs(proofsToAdd as Proof[], isPending) + log.trace('[addCashuProofs]', 'Added recovered proofs to the wallet with amount', { amountToAdd, addedAmount, isPending }) return { amountToAdd, - addedAmount + addedAmount, + addedProofs } } @@ -339,8 +455,20 @@ export const RemoteRecoveryScreen: FC> = o } - const toggleErrorsModal = () => { - setIsErrorsModalVisible(previousState => !previousState) + const onComplete = async () => { + try { + if(!seed || !mnemonic) { + throw new AppError(Err.VALIDATION_ERROR, 'Missing mnemonic or seed.') + } + + await KeyChain.saveMnemonic(mnemonic) + await KeyChain.saveSeed(seed as Uint8Array) + + userSettingsStore.setIsOnboarded(true) + navigation.navigate('Tabs') + } catch (e: any) { + handleError(e) + } } @@ -368,13 +496,13 @@ export const RemoteRecoveryScreen: FC> = o - {seedExists ? ( + {mnemonicExists ? ( > = o /> ) : ( <> - {isValidSeed ? ( + {isValidMnemonic ? ( <> } style={$item} /> @@ -409,21 +537,32 @@ export const RemoteRecoveryScreen: FC> = o } - RightComponent={ + LeftComponent={} + RightComponent={mintsStore.mintCount > 0 ? (