Seed recovery for testing

This commit is contained in:
minibits-cash
2023-12-02 23:53:37 +01:00
parent 00605c49af
commit 50e9887405
27 changed files with 910 additions and 466 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
module.exports = {
presets: ['module:metro-react-native-babel-preset'],
plugins: [
plugins: [
[
'module-resolver',
{
+1 -1
View File
@@ -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",
+26 -4
View File
@@ -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<Uint8Array> = [];
const rs: Array<bigint> = [];
for (let i = 0; i < amounts.length; i++) {
+1 -1
View File
@@ -31,7 +31,7 @@ export const InfoModal: FC<InfoModalProps> = function ({ message }) {
onBackdropPress={() => onClose()}
onBackButtonPress={() => onClose()}
ContentComponent={
<View style={{ flexDirection: 'row', alignItems: 'center', marginRight: spacing.medium}}>
<View style={{ padding: spacing.small, flexDirection: 'row', alignItems: 'center', marginRight: spacing.medium}}>
<Icon icon="faInfoCircle" size={spacing.large} color="white" />
<Text style={{ marginHorizontal: spacing.extraSmall}}>{message}</Text>
</View>
+4 -2
View File
@@ -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 (
<View style={[{
flex: 1,
alignItems: 'center',
backgroundColor: useThemeColor('background'),
opacity: 0.5,
opacity: props.opacity || 0.5,
justifyContent: 'center',
zIndex: 9999,
}, StyleSheet.absoluteFillObject]}>
<ActivityIndicator color="#ccc" animating size="large" />
{props.statusMessage && (<Text style={{opacity: 1}} text={props.statusMessage}/>)}
</View>
)
}
+1 -1
View File
@@ -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",
+11 -5
View File
@@ -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)
}
+68 -68
View File
@@ -110,76 +110,76 @@ export const BackupScreen: FC<SettingsStackScreenProps<'Backup'>> = observer(fun
<Text preset="heading" text="Backup" style={{color: 'white'}} />
</View>
<View style={$contentContainer}>
<Card
style={$card}
HeadingComponent={
<>
<ListItem
tx="backupScreen.localBackup"
subTx="backupScreen.localBackupDescription"
leftIcon='faDownload'
leftIconColor={
isLocalBackupOn
? colors.palette.success200
: colors.palette.neutral400
}
leftIconInverse={true}
RightComponent={
<View style={$rightContainer}>
<Switch
onValueChange={toggleBackupSwitch}
value={isLocalBackupOn}
/>
</View>
}
style={$item}
/>
{isLocalBackupOn && (
<ListItem
tx="backupScreen.recoveryTool"
subTx="backupScreen.recoveryToolDescription"
leftIcon='faUpload'
leftIconColor={colors.palette.focus300}
<Card
style={$card}
HeadingComponent={
<>
<ListItem
tx="backupScreen.remoteBackup"
subTx="backupScreen.remoteBackupDescription"
leftIcon='faDownload'
leftIconColor={colors.palette.blue200}
leftIconInverse={true}
style={$item}
onPress={gotoLocalRecovery}
/>
)}
</>
}
/>
<Card
style={$card}
HeadingComponent={
<>
<ListItem
tx="backupScreen.remoteBackup"
subTx="backupScreen.remoteBackupDescription"
leftIcon='faDownload'
leftIconColor={colors.palette.blue200}
leftIconInverse={true}
style={$item}
onPress={gotoRemoteBackup}
/>
</>
}
/>
<Card
style={$card}
HeadingComponent={
<>
<ListItem
tx="backupScreen.removeSpentCoins"
subTx="backupScreen.removeSpentCoinsDescription"
leftIcon='faRecycle'
leftIconColor={colors.palette.secondary300}
leftIconInverse={true}
style={$item}
onPress={checkSpent}
/>
</>
}
/>
onPress={gotoRemoteBackup}
/>
</>
}
/>
<Card
style={$card}
HeadingComponent={
<>
<ListItem
tx="backupScreen.localBackup"
subTx="backupScreen.localBackupDescription"
leftIcon='faDownload'
leftIconColor={
isLocalBackupOn
? colors.palette.success200
: colors.palette.neutral400
}
leftIconInverse={true}
RightComponent={
<View style={$rightContainer}>
<Switch
onValueChange={toggleBackupSwitch}
value={isLocalBackupOn}
/>
</View>
}
style={$item}
/>
{isLocalBackupOn && (
<ListItem
tx="backupScreen.recoveryTool"
subTx="backupScreen.recoveryToolDescription"
leftIcon='faUpload'
leftIconColor={colors.palette.focus300}
leftIconInverse={true}
style={$item}
onPress={gotoLocalRecovery}
/>
)}
</>
}
/>
<Card
style={$card}
HeadingComponent={
<>
<ListItem
tx="backupScreen.removeSpentCoins"
subTx="backupScreen.removeSpentCoinsDescription"
leftIcon='faRecycle'
leftIconColor={colors.palette.secondary300}
leftIconInverse={true}
style={$item}
onPress={checkSpent}
/>
</>
}
/>
{isLoading && <Loading />}
</View>
<BottomModal
+2 -3
View File
@@ -6,12 +6,11 @@ import {colors, spacing, typography, useThemeColor} from '../theme'
import {Header, Icon, Screen} from '../components'
import {useStores} from '../models'
import {ContactsStackScreenProps} from '../navigation'
import { NostrClient, NostrUnsignedEvent } from '../services'
import { NostrClient } from '../services'
import { PrivateContacts } from './Contacts/PrivateContacts'
import { PublicContacts } from './Contacts/PublicContacts'
import { log } from '../services/logService'
import { getImageSource } from '../utils/utils'
import { MINIBITS_NIP05_DOMAIN } from '@env'
import { ReceiveOption } from './ReceiveOptionsScreen'
import { SendOption } from './SendOptionsScreen'
@@ -55,7 +54,7 @@ export const ContactsScreen: FC<ContactsScreenProps> = observer(function Contact
const renderScene = ({route}: {route: Route}) => {
switch (route.key) {
case 'first':
return <PrivateContacts navigation={navigation} paymentOption={paymentOption} />
return <PrivateContacts navigation={navigation} paymentOption={paymentOption} />
case 'second':
return <PublicContacts navigation={navigation} paymentOption={paymentOption} />
default:
+21 -17
View File
@@ -117,23 +117,27 @@ export const DeveloperScreen: FC<SettingsStackScreenProps<'Developer'>> = 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)
}
},
},
],
+28 -3
View File
@@ -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<SettingsStackScreenProps<'Mints'>> = observer(function MintsScreen({route, navigation}) {
@@ -37,6 +38,7 @@ export const MintsScreen: FC<SettingsStackScreenProps<'Mints'>> = observer(funct
const mintInputRef = useRef<TextInput>(null)
const [mintUrl, setMintUrl] = useState('')
const [defaultMintUrl, setDefaultMintUrl] = useState<string>('https://mint.minibits.cash/Bitcoin')
const [selectedMint, setSelectedMint] = useState<Mint | undefined>()
const [info, setInfo] = useState('')
const [error, setError] = useState<AppError | undefined>()
@@ -117,6 +119,12 @@ export const MintsScreen: FC<SettingsStackScreenProps<'Mints'>> = observer(funct
}
const addDefaultMint = async function () {
setMintUrl(defaultMintUrl)
toggleAddMintModal()
}
const removeMint = async function () {
if (!selectedMint) {return}
@@ -129,8 +137,6 @@ export const MintsScreen: FC<SettingsStackScreenProps<'Mints'>> = 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<SettingsStackScreenProps<'Mints'>> = observer(funct
<Card
style={$actionCard}
ContentComponent={
<>
<ListItem
text={'Add mint'}
LeftComponent={<Icon
@@ -232,6 +239,22 @@ export const MintsScreen: FC<SettingsStackScreenProps<'Mints'>> = observer(funct
style={$actionItem}
onPress={toggleAddMintModal}
/>
{!mintsStore.alreadyExists(defaultMintUrl) && (
<ListItem
text={'Add Minibits mint'}
LeftComponent={<Icon
containerStyle={$iconContainer}
icon="faCoins"
size={spacing.medium}
color={iconColor}
/>
}
style={$actionItem}
onPress={addDefaultMint}
topSeparator={true}
/>
)}
</>
}
/>
{mintsStore.mintCount > 0 && (
@@ -313,6 +336,7 @@ export const MintsScreen: FC<SettingsStackScreenProps<'Mints'>> = observer(funct
bottomSeparator={true}
style={{paddingHorizontal: spacing.medium}}
/>
<Text text={`Current recovery index: ${selectedMint?.currentProofsCounter?.counter}`} size='xxs' style={{alignSelf: 'center', marginTop: spacing.small}} />
</View>
}
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 = {
+13 -13
View File
@@ -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<SettingsStackScreenProps<'RemoteBackup'>> =
// const {userSettingsStore} = useStores()
const [info, setInfo] = useState('')
const [seed, setSeed] = useState<string>()
const [seedArray, setSeedArray] = useState<string[]>([])
const [mnemonic, setMnemonic] = useState<string>()
const [mnemonicArray, setMnemonicArray] = useState<string[]>([])
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<AppError | undefined>()
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<SettingsStackScreenProps<'RemoteBackup'>> =
style={$card}
ContentComponent={
<ListItem
text='Your seed phrase'
text='Your mnemonic phrase'
subText='These 12 words sequence allows you to recover your ecash balance in case of device loss. Keep them in safe place.'
leftIcon='faInfoCircle'
leftIconColor={colors.palette.iconYellow300}
@@ -99,7 +99,7 @@ export const RemoteBackupScreen: FC<SettingsStackScreenProps<'RemoteBackup'>> =
ContentComponent={
<>
<FlatList
data={seedArray}
data={mnemonicArray}
numColumns={2}
renderItem={({ item, index }) => {
return(
+315 -105
View File
@@ -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<AppStackScreenProps<'RemoteRecovery'>> = observer(function RemoteRecoveryScreen(_props) {
const {navigation, route} = _props
@@ -48,68 +52,96 @@ export const RemoteRecoveryScreen: FC<AppStackScreenProps<'RemoteRecovery'>> = o
})
const {mintsStore, proofsStore, userSettingsStore, transactionsStore} = useStores()
const seedInputRef = useRef<TextInput>(null)
const mnemonicInputRef = useRef<TextInput>(null)
const [info, setInfo] = useState('')
const [seed, setSeed] = useState<string>('')
const [seedExists, setSeedExists] = useState(false)
const [isValidSeed, setIsValidSeed] = useState(false)
const [mnemonic, setMnemonic] = useState<string>('')
const [mnemonicExists, setMnemonicExists] = useState(false)
const [isValidMnemonic, setIsValidMnemonic] = useState(false)
const [seed, setSeed] = useState<Uint8Array>()
const [startIndex, setStartIndex] = useState<number>(0) // start of interval of indexes of proofs to recover
const [endIndex, setEndIndex] = useState<number>(RESTORE_INDEX_INTERVAL) // end of interval
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<AppError | undefined>()
const [isErrorsModalVisible, setIsErrorsModalVisible] = useState(false)
const [resultModalInfo, setResultModalInfo] = useState<{status: TransactionStatus, message: string} | undefined>()
const [isResultModalVisible, setIsResultModalVisible] = useState(false)
const [lastRecoveredAmount, setLastRecoveredAmount] = useState<number>(0)
const [recoveryErrors, setRecoveryErrors] = useState<AppError[]>([])
const [statusMessage, setStatusMessage] = useState<string>()
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<AppStackScreenProps<'RemoteRecovery'>> = 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<AppStackScreenProps<'RemoteRecovery'>> = 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<AppStackScreenProps<'RemoteRecovery'>> = 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<AppStackScreenProps<'RemoteRecovery'>> = 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<AppStackScreenProps<'RemoteRecovery'>> = 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<AppStackScreenProps<'RemoteRecovery'>> = 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<AppStackScreenProps<'RemoteRecovery'>> = 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<AppStackScreenProps<'RemoteRecovery'>> = 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<AppStackScreenProps<'RemoteRecovery'>> = 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<AppStackScreenProps<'RemoteRecovery'>> = 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<AppStackScreenProps<'RemoteRecovery'>> = 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<AppStackScreenProps<'RemoteRecovery'>> = 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<AppStackScreenProps<'RemoteRecovery'>> = 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<AppStackScreenProps<'RemoteRecovery'>> = 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<AppStackScreenProps<'RemoteRecovery'>> = o
</View>
<View style={$contentContainer}>
{seedExists ? (
{mnemonicExists ? (
<Card
style={$card}
ContentComponent={
<ListItem
text='Seed exists'
subText='Your wallet already has another seed in its secure storage. Recovery process works only with freshly installed wallet to avoid loss of your funds.'
text='Mnemonic exists'
subText='Your wallet already has another mnemonic in its secure storage. Recovery process works only with freshly installed wallet to avoid loss of your funds.'
leftIcon='faTriangleExclamation'
// leftIconColor='red'
style={$item}
@@ -393,14 +521,14 @@ export const RemoteRecoveryScreen: FC<AppStackScreenProps<'RemoteRecovery'>> = o
/>
) : (
<>
{isValidSeed ? (
{isValidMnemonic ? (
<>
<Card
style={$card}
ContentComponent={
<ListItem
text='Your seed phrase'
subText={seed}
text='Your mnemonic phrase'
subText={mnemonic}
LeftComponent={<View style={[$numIcon, {backgroundColor: numIconColor}]}><Text text='1'/></View>}
style={$item}
/>
@@ -409,21 +537,32 @@ export const RemoteRecoveryScreen: FC<AppStackScreenProps<'RemoteRecovery'>> = o
<Card
style={$card}
HeadingComponent={
<>
<ListItem
text='Recovery from mints'
subText='Identify mints to recover your ecash from and add them to the list.'
LeftComponent={<View style={[$numIcon, {backgroundColor: numIconColor}]}><Text text='2'/></View>}
RightComponent={
LeftComponent={<View style={[$numIcon, {backgroundColor: numIconColor}]}><Text text='2'/></View>}
RightComponent={mintsStore.mintCount > 0 ? (
<View style={$rightContainer}>
<Button
onPress={onAddMints}
text='Add mints'
preset='secondary'
text='Mints'
preset='secondary'
/>
</View>
}
) : (undefined)
}
style={$item}
/>
{mintsStore.mintCount === 0 && (
<View style={$buttonContainer}>
<Button
onPress={onAddMints}
text='Add mints'
/>
</View>
)}
</>
}
ContentComponent={
<>
@@ -443,21 +582,29 @@ export const RemoteRecoveryScreen: FC<AppStackScreenProps<'RemoteRecovery'>> = o
}
FooterComponent={
<>
{mintsStore.mintCount > 0 && (
<>
<View style={$buttonContainer}>
<Button
onPress={doRecovery}
text={startIndex === 0 ? 'Start recovery' : 'Continue recovery'}
/>
</View>
<Text
text={`Next interval ${startIndex} - ${endIndex}`}
size='xxs'
style={{color: textHint, alignSelf: 'center', marginTop: spacing.small}}
/>
</>
)}
{mintsStore.mintCount > 0 && (
<>
<View style={$buttonContainer}>
{startIndex > 0 && (
<Button
onPress={onComplete}
text={'Complete'}
style={{marginRight: spacing.small}}
/>
)}
<Button
onPress={startRecovery}
text={startIndex === 0 ? 'Start recovery' : 'Next interval'}
preset={startIndex === 0 ? 'default' : 'secondary'}
/>
</View>
<Text
text={`Recovery interval ${startIndex} - ${endIndex}`}
size='xxs'
style={{color: textHint, alignSelf: 'center', marginTop: spacing.small}}
/>
</>
)}
</>
}
/>
@@ -467,7 +614,7 @@ export const RemoteRecoveryScreen: FC<AppStackScreenProps<'RemoteRecovery'>> = o
style={$card}
ContentComponent={
<ListItem
text='Insert backup seed phrase'
text='Insert backup mnemonic phrase'
subText='Paste or rewrite 12 words phrase to recover your ecash balance on this device. Separate words by blank spaces.'
LeftComponent={<View style={[$numIcon, {backgroundColor: numIconColor}]}><Text text='1'/></View>}
style={$item}
@@ -476,20 +623,20 @@ export const RemoteRecoveryScreen: FC<AppStackScreenProps<'RemoteRecovery'>> = o
FooterComponent={
<>
<TextInput
ref={seedInputRef}
onChangeText={(seed: string) => setSeed(seed)}
value={seed}
ref={mnemonicInputRef}
onChangeText={(mnemonic: string) => setMnemonic(mnemonic)}
value={mnemonic}
numberOfLines={3}
multiline={true}
autoCapitalize='none'
keyboardType='default'
maxLength={150}
placeholder='Seed phrase...'
placeholder='Mnemonic phrase...'
selectTextOnFocus={true}
style={[$seedInput, {backgroundColor: inputBg, flexWrap: 'wrap'}]}
style={[$mnemonicInput, {backgroundColor: inputBg, flexWrap: 'wrap'}]}
/>
<View style={$buttonContainer}>
{seed ? (
{mnemonic ? (
<Button
onPress={onConfirm}
text='Confirm'
@@ -507,31 +654,94 @@ export const RemoteRecoveryScreen: FC<AppStackScreenProps<'RemoteRecovery'>> = o
/>
)}
</>
)}
{isLoading && <Loading />}
)}
</View>
<BottomModal
isVisible={isErrorsModalVisible}
style={{alignItems: 'stretch'}}
ContentComponent={
<>
{recoveryErrors?.map(err => {
return(
<ListItem
leftIcon='faTriangleExclamation'
leftIconColor={colors.palette.angry500}
text={err.message}
subText={err.params.mintUrl || ''}
bottomSeparator={true}
style={{paddingHorizontal: spacing.small}}
/>
)
})}
{recoveryErrors?.map((err, index) => (
<ListItem
key={index}
leftIcon='faTriangleExclamation'
leftIconColor={colors.palette.angry500}
text={err.message}
subText={err.params ? err.params.mintUrl : ''}
bottomSeparator={true}
style={{paddingHorizontal: spacing.small}}
/>
)
)}
</>
}
onBackButtonPress={toggleErrorsModal}
onBackdropPress={toggleErrorsModal}
/>
/>
<BottomModal
isVisible={isResultModalVisible ? true : false}
ContentComponent={
<>
{resultModalInfo &&
resultModalInfo.status === TransactionStatus.COMPLETED && (
<>
<ResultModalInfo
icon="faCheckCircle"
iconColor={colors.palette.success200}
title="Recovery success!"
message={resultModalInfo?.message}
/>
<View style={$buttonContainer}>
<Button
preset="secondary"
tx={'common.close'}
onPress={toggleResultModal}
/>
</View>
</>
)}
{resultModalInfo &&
resultModalInfo.status === TransactionStatus.ERROR && (
<>
<ResultModalInfo
icon="faTriangleExclamation"
iconColor={colors.palette.angry500}
title="Recovery failed"
message={resultModalInfo?.message}
/>
<View style={$buttonContainer}>
<Button
preset="secondary"
text={'Show errors'}
onPress={toggleErrorsModal}
/>
</View>
</>
)}
{resultModalInfo &&
resultModalInfo.status === TransactionStatus.EXPIRED && (
<>
<ResultModalInfo
icon='faInfoCircle'
iconColor={colors.palette.neutral400}
title="No ecash recovered"
message={resultModalInfo?.message}
/>
<View style={$buttonContainer}>
<Button
preset="secondary"
tx={'common.close'}
onPress={toggleResultModal}
/>
</View>
</>
)}
</>
}
onBackButtonPress={toggleResultModal}
onBackdropPress={toggleResultModal}
/>
{isLoading && <Loading statusMessage={statusMessage} opacity={0.8}/>}
{error && <ErrorModal error={error} />}
{info && <InfoModal message={info} />}
</Screen>
@@ -564,7 +774,7 @@ const $numIcon: ViewStyle = {
marginRight: spacing.medium
}
const $seedInput: TextStyle = {
const $mnemonicInput: TextStyle = {
// flex: 1,
borderRadius: spacing.small,
fontSize: 16,
+1
View File
@@ -108,6 +108,7 @@ export const ScanScreen: FC<WalletStackScreenProps<'Scan'>> = function ScanScree
return IncomingParser.navigateWithIncomingData(invoiceResult, navigation)
} catch (e: any) {
log.trace('CATCH')
const maybeLnurl = LnurlUtils.findEncodedLnurl(incoming)
if(maybeLnurl) {
+24 -9
View File
@@ -1,8 +1,6 @@
import {observer} from 'mobx-react-lite'
import React, {FC, useState, useCallback} from 'react'
import {useFocusEffect} from '@react-navigation/native'
import React, {FC, useState, useCallback, useEffect} from 'react'
import {Alert, TextStyle, View, ViewStyle} from 'react-native'
import Clipboard from '@react-native-clipboard/clipboard'
import {spacing, useThemeColor, colors} from '../theme'
import {WalletStackScreenProps} from '../navigation'
import {
@@ -20,11 +18,10 @@ import {
import {useHeader} from '../utils/useHeader'
import {log} from '../services/logService'
import AppError from '../utils/AppError'
import useIsInternetReachable from '../utils/useIsInternetReachable'
import { infoMessage } from '../utils/utils'
import { IncomingDataType, IncomingParser } from '../services/incomingParser'
import { LnurlUtils } from '../services/lnurl/lnurlUtils'
import { IncomingDataType } from '../services/incomingParser'
import { SvgXml } from 'react-native-svg'
import { KeyChain, MintClient } from '../services'
import { deriveSeedFromMnemonic } from '@cashu/cashu-ts/src/secrets'
export enum SendOption {
SEND_TOKEN = 'SEND_TOKEN',
@@ -41,10 +38,28 @@ export const SendOptionsScreen: FC<WalletStackScreenProps<'SendOptions'>> = obse
leftIcon: 'faArrowLeft',
onLeftPress: () => navigation.goBack(),
})
const isInternetReachable = useIsInternetReachable()
const [error, setError] = useState<AppError | undefined>()
/* useEffect(() => {
const getMnemonic = async () => {
try {
const mnemonic = await KeyChain.loadMnemonic()
log.trace(mnemonic)
const seed = deriveSeedFromMnemonic(mnemonic as string)
log.trace('real seed', seed)
await KeyChain.saveSeed(seed)
const storedseed = await KeyChain.loadSeed()
} catch (e: any) {
handleError(e)
}
}
getMnemonic()
}, []) */
const gotoContacts = function () {
navigation.navigate('ContactsNavigator', {
+48 -25
View File
@@ -323,21 +323,26 @@ export const SendScreen: FC<WalletStackScreenProps<'Send'>> = observer(
return
}
setIsMintSelectorVisible(false)
if (paymentOption === SendOption.SHOW_TOKEN) {
toggleQRModal()
}
if (paymentOption === SendOption.SEND_TOKEN) {
toggleNostrDMModal()
}
setIsMintSelectorVisible(false)
}
}
const onMintBalanceCancel = async function () {
setIsMintSelectorVisible(false)
amountInputRef && amountInputRef.current
? amountInputRef.current.focus()
: false
}
@@ -675,7 +680,7 @@ export const SendScreen: FC<WalletStackScreenProps<'Send'>> = observer(
encodedTokenToSend={encodedTokenToSend as string}
onCopy={onCopy}
onShareToApp={onShareToApp}
handleError={handleError}
// handleError={handleError}
/>
}
onBackButtonPress={toggleQRModal}
@@ -777,7 +782,7 @@ const MintBalanceSelector = observer(function (props: {
const {mintsStore} = useStores()
return (
<>
<View style={{flex: 1}}>
<Card
style={$card}
heading={'Pay from'}
@@ -805,20 +810,22 @@ const MintBalanceSelector = observer(function (props: {
</>
}
/>
<View style={[$buttonContainer, {marginTop: spacing.large}]}>
<Button
text="Send now"
onPress={props.onMintBalanceConfirm}
style={{marginRight: spacing.medium}}
// LeftAccessory={() => <Icon icon="faCoins" color="white" size={spacing.medium} containerStyle={{marginRight: spacing.small}}/>}
/>
<Button
preset="secondary"
tx={'common.cancel'}
onPress={props.onCancel}
/>
<View style={$bottomContainer}>
<View style={[$buttonContainer, {marginTop: spacing.large}]}>
<Button
text="Send now"
onPress={props.onMintBalanceConfirm}
style={{marginRight: spacing.medium}}
// LeftAccessory={() => <Icon icon="faCoins" color="white" size={spacing.medium} containerStyle={{marginRight: spacing.small}}/>}
/>
<Button
preset="secondary"
tx={'common.cancel'}
onPress={props.onCancel}
/>
</View>
</View>
</>
</View>
)
})
@@ -1000,17 +1007,33 @@ const SendAsQRCodeBlock = observer(function (props: {
encodedTokenToSend: string
onCopy: any
onShareToApp: any
handleError: any
// handleError: any
}) {
const [qrError, setQrError] = useState<Error | undefined>()
const handleQrError = function (error: Error) {
setQrError(error)
}
return (
<View style={[$bottomModal, {marginHorizontal: spacing.small}]}>
<Text text={'Scan to receive'} />
<View style={$qrCodeContainer}>
<QRCode
size={spacing.screenWidth - spacing.large * 2} value={props.encodedTokenToSend}
onError={props.handleError}
/>
<View style={$qrCodeContainer}>
{qrError ? (
<ListItem
text='Could not show QR code'
subText={qrError.message || ''}
leftIcon='faTriangleExclamation'
leftIconColor={colors.palette.angry500}
/>
) : (
<QRCode
size={spacing.screenWidth - spacing.large * 2} value={props.encodedTokenToSend}
onError={(error: any) => handleQrError(error)}
/>
)}
</View>
<View style={$buttonContainer}>
<Button
@@ -1291,7 +1314,7 @@ const $profileIcon: ImageStyle = {
}
const $bottomContainer: ViewStyle = {
position: 'absolute',
// position: 'absolute',
bottom: 0,
left: 0,
right: 0,
+1 -1
View File
@@ -1205,7 +1205,7 @@ const $contentContainer: TextStyle = {
const $actionCard: ViewStyle = {
marginBottom: spacing.extraSmall,
marginTop: -spacing.extraLarge * 2,
paddingTop: 0,
paddingVertical: 0,
}
const $dataCard: ViewStyle = {
+2 -23
View File
@@ -304,29 +304,8 @@ const onAmountEndEditing = async function () {
setIsLoading(true)
const encoded = await LnurlClient.getInvoice(lnurlPayParams, amount * 1000)
const invoice = LightningUtils.decodeInvoice(encoded)
const {description_hash} = LightningUtils.getInvoiceData(invoice)
if(!description_hash || description_hash.length === 0) {
throw new AppError(Err.VALIDATION_ERROR, `Invoice from ${lnurlPayParams.domain} is invalid, missing description_hash`)
}
// check that retrieved invoice matches the previous LNURL pay link
const hashedMetadata = QuickCrypto.createHash('sha256')
.update(lnurlPayParams.metadata)
.digest('hex')
log.trace('hashedMetadata', hashedMetadata)
log.trace('description_hash', description_hash)
if(hashedMetadata !== description_hash) {
throw new AppError(
Err.VALIDATION_ERROR,
`Invoice from ${lnurlPayParams.domain} has invalid description_hash ${description_hash}, expected ${hashedMetadata}`
)
}
setIsLoading(false)
if(encoded) {
return onEncodedInvoice(encoded)
}
@@ -349,7 +328,7 @@ const onEncodedInvoice = async function (encoded: string, paymentRequestDesc: st
setEncodedInvoice(encoded)
const invoice = LightningUtils.decodeInvoice(encoded)
const {amount, expiry, description, description_hash, timestamp} = LightningUtils.getInvoiceData(invoice)
const {amount, expiry, description, timestamp} = LightningUtils.getInvoiceData(invoice)
// log.trace('Decoded invoice', invoice, 'onEncodedInvoice')
log.trace('Invoice data', {amount, expiry, description}, 'onEncodedInvoice')
+20 -10
View File
@@ -107,18 +107,24 @@ export const WalletScreen: FC<WalletScreenProps> = observer(
setUpdateDescription(update.description)
setUpdateSize(`${round(update.packageSize * 0.000001, 2)}MB`)
setIsUpdateAvailable(true)
toggleUpdateModal()
// toggleUpdateModal()
log.info('OTA Update available', update, 'checkForUpdate')
}
} catch (e: any) {
return false // silent
}
}
InteractionManager.runAfterInteractions(async () => {
setTimeout(() => {
if(!isInternetReachable) {
return
}
checkForUpdate()
})
}, 100)
}, [])
const handleBinaryVersionMismatchCallback = function(update: RemotePackage) {
log.info('Native update available, silent', update)
@@ -147,13 +153,13 @@ export const WalletScreen: FC<WalletScreenProps> = observer(
}
InteractionManager.runAfterInteractions(async () => {
setTimeout(async () => {
if(!isInternetReachable) {
return
}
// subscribe once to receive tokens or payment requests by NOSTR DMs
Wallet.checkPendingReceived()
})
}, 200)
EventEmitter.on('receiveTokenCompleted', onReceiveTokenCompleted)
EventEmitter.on('receivePaymentRequest', onReceivePaymentRequest)
@@ -201,14 +207,14 @@ export const WalletScreen: FC<WalletScreenProps> = observer(
useFocusEffect(
useCallback(() => {
InteractionManager.runAfterInteractions(async () => {
setTimeout(() => {
if(!isInternetReachable) {
return
}
Wallet.checkPendingSpent()
Wallet.checkPendingTopups()
})
}, 100)
}, [])
)
@@ -232,10 +238,14 @@ export const WalletScreen: FC<WalletScreenProps> = observer(
appState.current.match(/inactive|background/) &&
nextAppState === 'active') {
InteractionManager.runAfterInteractions(async () => {
setTimeout(() => {
if(!isInternetReachable) {
return
}
Wallet.checkPendingSpent()
Wallet.checkPendingTopups()
})
Wallet.checkPendingTopups()
}, 100)
}
appState.current = nextAppState
+11 -4
View File
@@ -22,12 +22,13 @@ import {
Button,
ErrorModal,
Icon,
Loading,
Screen,
Text,
} from '../components'
import {TxKeyPath} from '../i18n'
import AppError from '../utils/AppError'
import { RestoreClient } from '../services'
import { MintClient } from '../services'
const AnimatedPagerView = Animated.createAnimatedComponent(PagerView)
@@ -81,12 +82,16 @@ export const WelcomeScreen: FC<AppStackScreenProps<'Welcome'>> =
const {userSettingsStore} = useStores()
const [error, setError] = useState<AppError | undefined>()
const [isLoading, setIsLoading] = useState<boolean>(false)
const gotoWallet = async function () {
try {
const seed = await RestoreClient.getOrCreateSeed()
// do not overwrite if one was set during recovery
setIsLoading(true)
const mnemonic = await MintClient.getOrCreateMnemonic()
setIsLoading(false)
userSettingsStore.setIsOnboarded(true)
navigation.navigate('Tabs')
} catch (e: any) {
@@ -103,7 +108,8 @@ export const WelcomeScreen: FC<AppStackScreenProps<'Welcome'>> =
}
}
const handleError = function (e: AppError) {
const handleError = function (e: AppError) {
setIsLoading(false)
setError(e)
}
@@ -232,6 +238,7 @@ export const WelcomeScreen: FC<AppStackScreenProps<'Welcome'>> =
</View>
</View>
{error && <ErrorModal error={error} />}
{isLoading && <Loading statusMessage='Creating wallet seed, this takes a while...' opacity={0.8}/>}
</Screen>
)
}
+137 -46
View File
@@ -5,26 +5,86 @@ import {
PayLnInvoiceResponse,
type Proof as CashuProof,
} from '@cashu/cashu-ts'
import { RestoreClient, TorDaemon } from '../services'
import {rootStoreInstance} from '../models'
import { KeyChain, TorDaemon } from '../services'
import {CashuUtils} from './cashu/cashuUtils'
import AppError, {Err} from '../utils/AppError'
import {log} from './logService'
import {Token} from '../models/Token'
import {Proof} from '../models/Proof'
import { deriveSeedFromMnemonic } from '@cashu/cashu-ts/src/secrets'
export type MintKeys = {[k: number]: string}
export type MintKeySets = {keysets: Array<string>}
let _mints: {[mintUrl: string]: CashuMint} = {}
let _wallets: {[mintUrl: string]: CashuWallet} = {}
let _wallets: {[mintUrl: string]: CashuWallet} = {} // used where seed is not required (perf)
let _seedWallets: {[mintUrl: string]: CashuWallet} = {} // wallet instances with seeds from keystore
let _mnemonic: string | undefined = undefined
let _seed: Uint8Array | undefined = undefined
const getMint = function (mintUrl: string) {
const { mintsStore } = rootStoreInstance
const getOrCreateMnemonic = async function (): Promise<string> {
let mnemonic: string | undefined = undefined
mnemonic = await getMnemonic()
if (!mnemonic) {
mnemonic = KeyChain.generateMnemonic() as string
const seed = deriveSeedFromMnemonic(mnemonic) // expensive
await KeyChain.saveMnemonic(mnemonic)
await KeyChain.saveSeed(seed)
log.trace('[getOrCreateMnemonic]', 'Created and saved new mnemonic and seed')
}
return mnemonic
}
// caching mnemonic
const getMnemonic = async function (): Promise<string | undefined> {
if (_mnemonic) {
log.trace('[getMnemonic]', 'returning cached mnemonic')
return _mnemonic
}
const mnemonic = await KeyChain.loadMnemonic() as string
if (!mnemonic) {
return undefined
}
_mnemonic = mnemonic
return mnemonic
}
const getSeed = async function (): Promise<Uint8Array | undefined> {
if (_seed) {
log.trace('[getSeed]', 'returning cached seed')
return _seed
}
const seed = await KeyChain.loadSeed()
if (!seed) {
return undefined
}
_seed = seed
return seed
}
const getMint = function (mintUrl: string) {
if (_mints[mintUrl]) {
return _mints[mintUrl]
}
if(mintUrl.includes('.onion')) {
log.trace('Creating mint instance with .onion mintUrl', {mintUrl}, 'getMint')
log.trace('[getMint]', 'Creating mint instance with .onion mintUrl', {mintUrl})
const mint = new CashuMint(mintUrl, TorDaemon.torRequest)
_mints[mintUrl] = mint
@@ -37,21 +97,41 @@ const getMint = function (mintUrl: string) {
return mint
}
const getWallet = async function (
mintUrl: string
mintUrl: string,
withSeed: boolean = false
) {
log.trace('[getWallet] start')
if (_wallets[mintUrl]) {
return _wallets[mintUrl]
}
if (withSeed && _seedWallets[mintUrl]) {
log.trace('[getWallet]', 'Returning existing cashuWallet instance with seed')
return _seedWallets[mintUrl]
}
const mint = getMint(mintUrl)
const seed = await RestoreClient.getOrCreateSeed()
if (!withSeed && _wallets[mintUrl]) {
log.trace('[getWallet]', 'Returning existing cashuWallet instance')
return _wallets[mintUrl]
}
const wallet = new CashuWallet(mint, undefined, seed)
_wallets[mintUrl] = wallet
const cashuMint = getMint(mintUrl)
const mint = mintsStore.findByUrl(mintUrl)
return wallet
if(withSeed) {
const seed = await getSeed()
const seedWallet = new CashuWallet(cashuMint, mint ? mint.keys : undefined, seed)
log.trace('[getWallet]', 'Saving CahuWallet instance to cache')
_seedWallets[mintUrl] = seedWallet
return seedWallet
}
const wallet = new CashuWallet(cashuMint, mint ? mint.keys : undefined)
_wallets[mintUrl] = wallet
log.trace('[getWallet]', 'Returning new cashuWallet instance')
return wallet
}
@@ -62,7 +142,6 @@ const getMintKeys = async function (mintUrl: string) {
try {
// keysets = await mint.getKeySets()
log.trace('Sending getKeys request', {mintUrl}, 'getMintKeys')
keys = await mint.getKeys()
} catch (e: any) {
throw new AppError(
@@ -98,7 +177,9 @@ const receiveFromMint = async function (
counter: number
) {
try {
const cashuWallet = await getWallet(mintUrl)
const cashuWallet = await getWallet(mintUrl, true) // with seed
log.trace('[receiveFromMint] calling cashuWallet.receive', {encodedToken, counter})
// this method returns quite a mess, we normalize naming of returned parameters
const {token, tokensWithErrors, newKeys} = await cashuWallet.receive(
@@ -111,8 +192,6 @@ const receiveFromMint = async function (
log.trace('[receiveFromMint] tokensWithErrors', tokensWithErrors)
log.trace('[receiveFromMint] newKeys', newKeys)
// if (newKeys) { _setKeys(mintUrl, newKeys) }
return {
updatedToken: token as Token,
errorToken: tokensWithErrors as Token,
@@ -132,7 +211,7 @@ const sendFromMint = async function (
counter: number
) {
try {
const cashuWallet = await getWallet(mintUrl)
const cashuWallet = await getWallet(mintUrl, true) // with seed
const {returnChange, send, newKeys} = await cashuWallet.send(
amountToSend,
@@ -175,7 +254,7 @@ const sendFromMint = async function (
} catch (e: any) {
throw new AppError(
Err.MINT_ERROR,
`The mint could not return proofs necessary for this transaction. ${e.message}`,
`The mint could not return signatures necessary for this transaction. ${e.message}`,
{
caller: 'sendFromMint',
mintUrl,
@@ -250,7 +329,7 @@ const payLightningInvoice = async function (
counter: number
) {
try {
const cashuWallet = await getWallet(mintUrl)
const cashuWallet = await getWallet(mintUrl, true) // with seed
const {isPaid, change, preimage, newKeys}: PayLnInvoiceResponse =
await cashuWallet.payLnInvoice(
@@ -317,7 +396,7 @@ const requestProofs = async function (
counter: number
) {
try {
const cashuWallet = await getWallet(mintUrl)
const cashuWallet = await getWallet(mintUrl, true) // with seed
/* eslint-disable @typescript-eslint/no-unused-vars */
const {proofs, newKeys} = await cashuWallet.requestTokens(
amount,
@@ -348,25 +427,34 @@ const requestProofs = async function (
const restore = async function (
mintUrl: string,
indexFrom: number,
indexTo: number,
indexTo: number,
seed: Uint8Array
) {
try {
const cashuWallet = await getWallet(mintUrl)
// need special wallet instance to pass seed directly
const cashuMint = getMint(mintUrl)
const mint = mintsStore.findByUrl(mintUrl)
const limit = Math.abs(indexTo - indexFrom)
/* eslint-disable @typescript-eslint/no-unused-vars */
const {proofs, newKeys} = await cashuWallet.restore(
indexFrom,
limit,
)
/* eslint-enable */
log.info('[restore]', 'Number of recovered proofs', {proofs: proofs.length, newKeys})
return {
proofs: proofs || [],
newKeys
}
if(!mint) {
throw new AppError(Err.MINT_ERROR, 'Could not find mint in wallet state', {caller: 'restore', mintUrl})
}
const seedWallet = new CashuWallet(cashuMint, mint.keys, seed)
const count = Math.abs(indexTo - indexFrom)
/* eslint-disable @typescript-eslint/no-unused-vars */
const {proofs, newKeys} = await seedWallet.restore(
count,
indexFrom,
)
/* eslint-enable */
log.info('[restore]', 'Number of recovered proofs', {proofs: proofs.length, newKeys})
return {
proofs: proofs || [],
newKeys
}
} catch (e: any) {
log.error(e)
throw new AppError(Err.MINT_ERROR, e.message, {mintUrl})
@@ -374,13 +462,16 @@ const restore = async function (
}
export const MintClient = {
getMintKeys,
receiveFromMint,
sendFromMint,
getSpentOrPendingProofsFromMint,
getLightningFee,
payLightningInvoice,
requestLightningInvoice,
requestProofs,
restore,
getOrCreateMnemonic,
getMnemonic,
getSeed,
getMintKeys,
receiveFromMint,
sendFromMint,
getSpentOrPendingProofsFromMint,
getLightningFee,
payLightningInvoice,
requestLightningInvoice,
requestProofs,
restore,
}
+1 -1
View File
@@ -7,4 +7,4 @@ export * from "./minibitsService"
export * from "./nostrService"
export * from "./logService"
export * from "./torService"
export * from "./restoreService"
// export * from "./restoreService"
+109 -26
View File
@@ -6,10 +6,12 @@ import * as nostrTools from 'nostr-tools'
import {btoa, atob, fromByteArray} from 'react-native-quick-base64'
import {log} from './logService'
export enum KeyChainServiceName {
MMKV = 'app.minibits.mmkv',
NOSTR = 'app.minibits.nostr',
SEED = 'app.minibits.seed',
MNEMONIC = 'app.minibits.mnemonic',
}
export type KeyPair = {
@@ -17,6 +19,7 @@ export type KeyPair = {
privateKey: string
}
const getSupportedBiometryType = async function () {
try {
const biometryType = await _Keychain.getSupportedBiometryType({})
@@ -28,37 +31,34 @@ const getSupportedBiometryType = async function () {
}
const generateSeed = function () {
const generateMnemonic = function () {
try {
const seed = generateNewMnemonic()
log.trace('[generateMnemonic]', 'start')
log.trace('[generateSeed]', 'New SEED created:', seed)
const mnemonic = generateNewMnemonic()
return seed as string
log.trace('[generateMnemonic]', 'New mnemonic created:', mnemonic)
return mnemonic as string
} catch (e: any) {
throw new AppError(Err.KEYCHAIN_ERROR, e.message, e)
}
}
/**
* Save seed to KeyChain/KeyStore
*
* @param seed The key to save.
*/
const saveSeed = async function (
seed: string,
const saveMnemonic = async function (
mnemonic: string,
): Promise<_Keychain.Result | false> {
try {
const result = await _Keychain.setGenericPassword(
KeyChainServiceName.SEED,
seed,
KeyChainServiceName.MNEMONIC,
mnemonic,
{
service: KeyChainServiceName.SEED
service: KeyChainServiceName.MNEMONIC
},
)
log.trace('[saveSeed]', 'Saved seed to the KeyChain', seed)
log.trace('[saveMnemonic]', 'Saved new mnemonic to the KeyChain', mnemonic)
return result
} catch (e: any) {
@@ -67,19 +67,98 @@ const saveSeed = async function (
}
/**
* Loads seed from the KeyChain/KeyStore
* Loads mnemonic from the KeyChain/KeyStore
*
*/
const loadSeed = async function (): Promise<string | undefined> {
const loadMnemonic = async function (): Promise<string | undefined> {
try {
log.trace('[loadMnemonic]', 'start')
const result = await _Keychain.getGenericPassword({
service: KeyChainServiceName.SEED
service: KeyChainServiceName.MNEMONIC
})
if (result) {
const seed = result.password
const mnemonic = result.password
log.trace('[loadMnemonic]', 'complete', {mnemonic})
return mnemonic
}
log.trace('[loadMnemonic]', 'Did not find existing mnemonic in the KeyChain')
return undefined
} catch (e: any) {
throw new AppError(Err.KEYCHAIN_ERROR, e.message)
}
}
/**
* Removes mnemonic from KeyChain/KeyStore
*
* @param service The key to kill.
*/
const removeMnemonic = async function (): Promise<boolean> {
try {
const result = await _Keychain.resetGenericPassword({
service: KeyChainServiceName.MNEMONIC
})
log.trace('[removeMnemonic]', 'Removed mnemonic.')
return result
} catch (e: any) {
throw new AppError(Err.KEYCHAIN_ERROR, e.message)
}
}
/**
* Save seed to KeyChain/KeyStore
*
* @param seed The key to save.
*/
const saveSeed = async function (
seed: Uint8Array,
): Promise<_Keychain.Result | false> {
try {
const seedStr: string = Buffer.from(seed).toString('base64')
log.trace('[saveSeed]', 'Saved seed base64', seedStr)
const result = await _Keychain.setGenericPassword(
KeyChainServiceName.SEED,
seedStr,
{
service: KeyChainServiceName.SEED
},
)
log.trace('[saveSeed]', 'Saved seed to the KeyChain')
return result
} catch (e: any) {
throw new AppError(Err.KEYCHAIN_ERROR, e.message, e)
}
}
/**
* Loads mnemonic from the KeyChain/KeyStore
*
*/
const loadSeed = async function (): Promise<Uint8Array | undefined> {
try {
log.trace('[loadSeed]', 'start')
const result = await _Keychain.getGenericPassword({
service: KeyChainServiceName.SEED
})
if (result) {
const seedStr = result.password
log.trace('[loadSeed]', 'base64', {seedStr})
const seed: Uint8Array = new Uint8Array(Buffer.from(seedStr, 'base64'))
return seed
}
log.trace('[loadSeed]', 'Did not find existing seed in the KeyChain')
return undefined
} catch (e: any) {
@@ -89,11 +168,11 @@ const saveSeed = async function (
/**
* Removes seed from KeyChain/KeyStore
* Removes mnemonic from KeyChain/KeyStore
*
* @param service The key to kill.
*/
/* const removeSeed = async function (): Promise<boolean> {
const removeSeed = async function (): Promise<boolean> {
try {
const result = await _Keychain.resetGenericPassword({
service: KeyChainServiceName.SEED
@@ -103,7 +182,7 @@ const saveSeed = async function (
} catch (e: any) {
throw new AppError(Err.KEYCHAIN_ERROR, e.message)
}
}*/
}
const generateNostrKeyPair = function () {
@@ -278,13 +357,17 @@ const removeMmkvEncryptionKey = async function (): Promise<boolean> {
}
export const KeyChain = {
export const KeyChain = {
getSupportedBiometryType,
generateSeed,
generateMnemonic,
saveMnemonic,
loadMnemonic,
removeMnemonic,
saveSeed,
loadSeed,
// removeSeed,
removeSeed,
generateNostrKeyPair,
saveNostrKeyPair,
-37
View File
@@ -1,37 +0,0 @@
import {KeyChain} from './keyChain'
import {log} from './logService'
import AppError, { Err } from '../utils/AppError'
const getOrCreateSeed = async function (): Promise<string> {
let seed: string | null = null
seed = await KeyChain.loadSeed() as string
if (!seed) {
seed = KeyChain.generateSeed() as string
await KeyChain.saveSeed(seed)
log.trace('[getOrCreateSeed]', 'Created and saved new seed')
}
return seed
}
const getSeed = async function (): Promise<string | null> {
let seed: string | null = null
seed = await KeyChain.loadSeed() as string
if (!seed) {
return null
}
return seed
}
export const RestoreClient = {
getOrCreateSeed,
getSeed,
}
+43 -48
View File
@@ -745,19 +745,20 @@ const receive = async function (
)
}
let receivedAmount = 0
for (const entry of updatedToken.token) {
// create ProofModel instances and store them into the proofsStore
_addCashuProofs(
const { addedProofs, addedAmount } = _addCashuProofs(
entry.proofs,
entry.mint,
transactionId as number
)
mintsStore.increaseProofsCounter(mintToReceive as string, entry.proofs.length)
receivedAmount += addedAmount
}
// This should be amountToReceive - amountWithErrors but let's set it from updated token
const receivedAmount = CashuUtils.getTokenAmounts(updatedToken as Token).totalAmount
// const receivedAmount = CashuUtils.getTokenAmounts(updatedToken as Token).totalAmount
log.debug('[receive]', `Received amount: ${receivedAmount}`)
// Update tx amount if full amount was not received
@@ -1024,19 +1025,20 @@ const receiveOfflineComplete = async function (
)
}
let receivedAmount = 0
for (const entry of updatedToken.token) {
// create ProofModel instances and store them into the proofsStore
_addCashuProofs(
const { addedProofs, addedAmount } = _addCashuProofs(
entry.proofs,
entry.mint,
transaction.id as number
)
mintsStore.increaseProofsCounter(mintToReceive as string, entry.proofs.length)
receivedAmount += addedAmount
}
// This should be amountToReceive - amountWithErrors but let's set it from updated token
const receivedAmount = CashuUtils.getTokenAmounts(updatedToken as Token).totalAmount
// const receivedAmount = CashuUtils.getTokenAmounts(updatedToken as Token).totalAmount
log.debug('[receiveOfflineComplete]', 'Received amount', receivedAmount)
// Update tx amount if full amount was not received
@@ -1188,37 +1190,25 @@ const _sendFromMint = async function (
if (newKeys) {_updateMintKeys(mintUrl, newKeys)}
// these might be original proofToSendFrom if they matched the exact amount and split was not necessary
for (const proof of proofsToSend) {
if (isStateTreeNode(proof)) {
proof.setTransactionId(transactionId)
proof.setMintUrl(mintUrl)
} else {
;(proof as Proof).tId = transactionId
;(proof as Proof).mintUrl = mintUrl
}
}
// add proofs returned by the mint after the split
if (returnedProofs.length > 0) {
// these should be fresh proofs from the mint but let's be defensive and cover the case that we over-send proofs while matching denominations
for (const proof of returnedProofs) {
if (isStateTreeNode(proof)) {
proof.setTransactionId(transactionId)
proof.setMintUrl(mintUrl)
} else {
;(proof as Proof).tId = transactionId
;(proof as Proof).mintUrl = mintUrl
}
}
proofsStore.addProofs(returnedProofs)
mintsStore.increaseProofsCounter(mintUrl as string, returnedProofs.length)
const { addedProofs, addedAmount } = _addCashuProofs(
returnedProofs,
mintUrl,
transactionId
)
}
// remove used proofs and move sent proofs to pending
proofsStore.removeProofs(proofsToSendFrom)
proofsStore.addProofs(proofsToSend, true) // pending true
// these might be original proofToSendFrom if they matched the exact amount and split was not necessary
const { addedProofs, addedAmount } = _addCashuProofs(
proofsToSend,
mintUrl,
transactionId,
true
)
// Clean private properties to not to send them out. This returns plain js array, not model objects.
const cleanedProofsToSend = proofsToSend.map(proof => {
@@ -1528,13 +1518,13 @@ const transfer = async function (
let finalFee = estimatedFee
if (feeSavedProofs.length) {
const {addedAmount: feeSaved} = _addCashuProofs(
feeSavedProofs,
mintUrl,
transactionId
)
mintsStore.increaseProofsCounter(mintUrl as string, feeSavedProofs.length)
finalFee = estimatedFee - feeSaved
}
// Save final fee in db
@@ -1630,26 +1620,34 @@ const transfer = async function (
const _addCashuProofs = function (
proofsToAdd: CashuProof[],
mintUrl: string,
transactionId: number
transactionId: number,
isPending: boolean = false
): {
amountToAdd: number,
addedAmount: number
addedAmount: number,
addedProofs: Proof[]
} {
// Add internal references
for (const proof of proofsToAdd as Proof[]) {
proof.tId = transactionId
proof.mintUrl = mintUrl
if (isStateTreeNode(proof)) {
proof.setTransactionId(transactionId)
proof.setMintUrl(mintUrl)
} else {
;(proof as Proof).tId = transactionId
;(proof as Proof).mintUrl = mintUrl
}
}
const amountToAdd = CashuUtils.getProofsAmount(proofsToAdd as Proof[])
// Creates proper model instances and adds them to the wallet
const addedAmount = proofsStore.addProofs(proofsToAdd as Proof[])
const { addedAmount, addedProofs} = proofsStore.addProofs(proofsToAdd as Proof[], isPending)
log.trace('[_addCashuProofs]', 'Added proofs to the wallet with amount', { amountToAdd, addedAmount })
return {
amountToAdd,
addedAmount
addedAmount,
addedProofs
}
}
@@ -1869,7 +1867,6 @@ const checkPendingTopups = async function () {
try {
for (const pr of paymentRequests) {
// claim tokens if invoice is paid
const proofsCounter = mintsStore.currentProofsCounterValue(pr.mint as string)
@@ -1916,9 +1913,7 @@ const checkPendingTopups = async function () {
proofs,
pr.mint as string,
pr.transactionId as number
)
mintsStore.increaseProofsCounter(pr.mint as string, proofs.length)
)
if (receivedAmount !== pr.amount) {
throw new AppError(
+13 -4
View File
@@ -25,14 +25,19 @@ export const getImageSource = function(img: string) {
export const infoMessage = function(message: string, description?: string) {
const backgroundColor = colors.dark.info
const backgroundColor = colors.palette.success300
const textColor = 'white'
return showMessage({
message,
description,
backgroundColor,
color: 'white',
style: {minHeight: spacing.screenHeight * 0.15, borderTopLeftRadius: spacing.medium, borderTopRightRadius: spacing.medium},
color: textColor,
style: {
minHeight: spacing.screenHeight * 0.15,
borderTopLeftRadius: spacing.medium,
borderTopRightRadius: spacing.medium
},
})
}
@@ -46,7 +51,11 @@ export const warningMessage = function(message: string, description?: string) {
duration: 3000,
backgroundColor,
color: 'white',
style: {minHeight: spacing.screenHeight * 0.15, borderTopLeftRadius: spacing.medium, borderTopRightRadius: spacing.medium},
style: {
minHeight: spacing.screenHeight * 0.15,
borderTopLeftRadius: spacing.medium,
borderTopRightRadius: spacing.medium
},
})
}
+8 -8
View File
@@ -1625,12 +1625,12 @@
dependencies:
"@noble/hashes" "1.3.2"
"@noble/hashes@1.3.1", "@noble/hashes@~1.3.0", "@noble/hashes@~1.3.1":
"@noble/hashes@1.3.1", "@noble/hashes@~1.3.1":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.1.tgz#8831ef002114670c603c458ab8b11328406953a9"
integrity sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==
"@noble/hashes@1.3.2", "@noble/hashes@~1.3.2":
"@noble/hashes@1.3.2", "@noble/hashes@~1.3.0", "@noble/hashes@~1.3.2":
version "1.3.2"
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39"
integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==
@@ -1972,12 +1972,12 @@
color "^4.2.3"
warn-once "^0.1.0"
"@scure/base@1.1.1", "@scure/base@~1.1.0":
"@scure/base@1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.1.tgz#ebb651ee52ff84f420097055f4bf46cfba403938"
integrity sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==
"@scure/base@~1.1.2":
"@scure/base@~1.1.0", "@scure/base@~1.1.2":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.3.tgz#8584115565228290a6c6c4961973e0903bb3df2f"
integrity sha512-/+SgoRjLq7Xlf0CWuLHq2LUZeL/w65kfzAPG5NH9pcmBhs+nunQTn4gvdwgMTIXnt9b2C/1SeL2XiysZEyIC9Q==
@@ -7009,10 +7009,10 @@ react-native-logs@^5.0.1:
resolved "https://registry.yarnpkg.com/react-native-logs/-/react-native-logs-5.0.1.tgz#53665919587b499e22f514e50cb6a834cedba1ce"
integrity sha512-30aQugeLFfcBEKd6U4I75SKc5q5fb9Qrwv0Vnsy3jwpC716xON9SxRbBEnxoPSDJjzK527r4zi+qYqzig6lPlQ==
react-native-mmkv@^2.8.0:
version "2.10.1"
resolved "https://registry.yarnpkg.com/react-native-mmkv/-/react-native-mmkv-2.10.1.tgz#1a4e64edbf9561fe81a5ae3cefde17e88dfc669f"
integrity sha512-MCrYytUJ3JhSVg7LwVXCj7ExbMHe48FoQdDA1xo1q/M4c1/QyI3M9L9d2dyJUhYPnsSO0+5c2mJICm7v39XA0w==
react-native-mmkv@^2.11.0:
version "2.11.0"
resolved "https://registry.yarnpkg.com/react-native-mmkv/-/react-native-mmkv-2.11.0.tgz#51b9985f6a5c09fe9c16d8c1861cc2901856ace1"
integrity sha512-28PdUHjZJmAw3q+8zJDAAdohnZMpDC7WgRUJxACOMkcmJeqS3u5cKS/lSq2bhf1CvaeIiHYHUWiyatUjMRCDQQ==
react-native-modal@^13.0.1:
version "13.0.1"