mirror of
https://github.com/minibits-cash/minibits_wallet.git
synced 2026-10-06 03:38:24 +00:00
Seed recovery for testing
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
module.exports = {
|
||||
presets: ['module:metro-react-native-babel-preset'],
|
||||
plugins: [
|
||||
plugins: [
|
||||
[
|
||||
'module-resolver',
|
||||
{
|
||||
|
||||
+1
-1
@@ -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",
|
||||
|
||||
@@ -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++) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -1205,7 +1205,7 @@ const $contentContainer: TextStyle = {
|
||||
const $actionCard: ViewStyle = {
|
||||
marginBottom: spacing.extraSmall,
|
||||
marginTop: -spacing.extraLarge * 2,
|
||||
paddingTop: 0,
|
||||
paddingVertical: 0,
|
||||
}
|
||||
|
||||
const $dataCard: ViewStyle = {
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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,
|
||||
}
|
||||
|
||||
@@ -7,4 +7,4 @@ export * from "./minibitsService"
|
||||
export * from "./nostrService"
|
||||
export * from "./logService"
|
||||
export * from "./torService"
|
||||
export * from "./restoreService"
|
||||
// export * from "./restoreService"
|
||||
|
||||
+109
-26
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user