Send ecash using NIP17 sealed and wrapped direct messages, fix generating new seed.

This commit is contained in:
minibits-cash
2025-01-09 00:41:43 +01:00
parent d55665267b
commit 09c09afef7
19 changed files with 488 additions and 363 deletions
+2 -1
View File
@@ -12,6 +12,7 @@ module.exports = {
},
],
'module:react-native-dotenv',
'react-native-reanimated/plugin'
'react-native-reanimated/plugin',
'@babel/plugin-proposal-export-namespace-from'
],
};
+2 -1
View File
@@ -6,6 +6,7 @@ const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');
*
* @type {import('metro-config').MetroConfig}
*/
const config = {};
const config = {
};
module.exports = mergeConfig(getDefaultConfig(__dirname), config);
+5 -4
View File
@@ -1,6 +1,6 @@
{
"name": "minibits_wallet",
"version": "0.1.10-beta.16",
"version": "0.1.10-beta.17",
"private": true,
"scripts": {
"android:clean": "cd android && ./gradlew clean",
@@ -18,7 +18,7 @@
"adb:inspect": "adb shell input keyevent 82",
"postinstall": "patch-package --exclude 'nothing'"
},
"dependencies": {
"dependencies": {
"@cashu/cashu-ts": "minibits-cash/cashu-ts#2b51a7ba7a8461a0040359eecda37b98b603692c",
"@fortawesome/fontawesome-svg-core": "^6.6.0",
"@fortawesome/free-brands-svg-icons": "^6.6.0",
@@ -47,7 +47,7 @@
"mobx": "^6.13.2",
"mobx-react-lite": "^4.0.7",
"mobx-state-tree": "^6.0.1",
"nostr-tools": "2.9.4",
"nostr-tools": "minibits-cash/nostr-tools#2991f59d0150a0cb2c71e2f9a4cb6c8aca3b19d7",
"numbro": "^2.5.0",
"patch-package": "^8.0.0",
"postinstall-postinstall": "^2.1.0",
@@ -80,8 +80,9 @@
"text-encoding-polyfill": "^0.6.7",
"util": "^0.12.5"
},
"devDependencies": {
"devDependencies": {
"@babel/core": "^7.20.0",
"@babel/plugin-proposal-export-namespace-from": "^7.18.9",
"@babel/preset-env": "^7.20.0",
"@babel/runtime": "^7.20.0",
"@react-native/babel-preset": "0.75.3",
+6 -6
View File
@@ -7,7 +7,7 @@ import {
isStateTreeNode,
detach,
} from 'mobx-state-tree'
import { kinds as NostrKinds } from 'nostr-tools'
import { kinds } from 'nostr-tools'
import {withSetPropAction} from './helpers/withSetPropAction'
import {log} from '../services/logService'
import EventEmitter from '../utils/eventEmitter'
@@ -162,7 +162,7 @@ export const NwcConnectionModel = types.model('NwcConnection', {
const responseEvent: NostrUnsignedEvent = {
pubkey: self.walletPubkey,
kind: NostrKinds.NWCWalletResponse,
kind: kinds.NWCWalletResponse,
tags: [["p", requestEvent.pubkey], ["e", requestEvent.id]],
content: encryptedContent,
created_at: Math.floor(Date.now() / 1000)
@@ -673,7 +673,7 @@ export const NwcStoreModel = types
self.nwcConnections.push(newConnection)
const filter = {
kinds: [NostrKinds.NWCWalletInfo],
kinds: [kinds.NWCWalletInfo],
authors: [newConnection.walletPubkey],
}
@@ -683,7 +683,7 @@ export const NwcStoreModel = types
if(!existingInfoEvent) {
// publish info replacable event // seems to be a relict replaced by get_info request?
const infoEvent: NostrUnsignedEvent = {
kind: NostrKinds.NWCWalletInfo,
kind: kinds.NWCWalletInfo,
pubkey: newConnection.walletPubkey,
tags: [],
content: self.supportedMethods.join(' '),
@@ -730,7 +730,7 @@ export const NwcStoreModel = types
let eventsBatch: NostrEvent[] = []
const filter = [{
kinds: [NostrKinds.NWCWalletRequest],
kinds: [kinds.NWCWalletRequest],
authors: connectionsPubkeys,
"#p": [self.walletPubkey],
since
@@ -740,7 +740,7 @@ export const NwcStoreModel = types
const sub = pool.subscribeMany(self.connectionRelays , filter, {
onevent(event) {
if (event.kind != NostrKinds.NWCWalletRequest) {
if (event.kind != kinds.NWCWalletRequest) {
return
}
+19 -37
View File
@@ -1,5 +1,5 @@
import {Instance, SnapshotOut, types, flow} from 'mobx-state-tree'
import { kinds as NostrKinds } from 'nostr-tools'
import { kinds } from 'nostr-tools'
import {KeyChain, MinibitsClient, NostrClient, NostrUnsignedEvent, WalletTask} from '../services'
import {log} from '../services/logService'
import { Err } from '../utils/AppError'
@@ -15,6 +15,7 @@ export type WalletProfile = {
picture: string
lud16?: string | null
device?: string | null
seedHash?: string | null
isOwnProfile: boolean
// isBatchClaimOn: boolean
}
@@ -51,7 +52,7 @@ export const WalletProfileStoreModel = types
// announce to minibits relay + default public relays with replaceable event
const profileEvent: NostrUnsignedEvent = {
kind: NostrKinds.Metadata,
kind: kinds.Metadata,
pubkey,
tags: [],
content: JSON.stringify({
@@ -94,7 +95,7 @@ export const WalletProfileStoreModel = types
// announce to new minibits relay
const profileEvent: NostrUnsignedEvent = {
kind: NostrKinds.Metadata,
kind: kinds.Metadata,
pubkey,
tags: [],
content: JSON.stringify({
@@ -133,7 +134,10 @@ export const WalletProfileStoreModel = types
self.name = name // default name is set on server side, equals walletId
self.nip05 = nip05 // default is name@minibits.cash set on server side
self.lud16 = lud16 // equals for all @minibits.cash addresses
self.picture = avatar // default picture is set on server side
self.picture = avatar // default picture is set on server side
const userSettingsStore = getRootStore(self).userSettingsStore
userSettingsStore.setWalletId(walletId)
const publishedEvent = yield self.publishToRelays()
})
@@ -184,12 +188,9 @@ export const WalletProfileStoreModel = types
}
)
self.name = profileRecord.name
self.nip05 = profileRecord.nip05
self.lud16 = profileRecord.lud16
const publishedEvent = yield self.publishToRelays()
self.hydrate(profileRecord)
log.debug('[updateName]', 'Wallet name updated in the WalletProfileStore', {self, publishedEvent})
log.debug('[updateName]', 'Wallet name updated in the WalletProfileStore', {self})
return self
}),
updatePicture: flow(function* updatePicture(picture: string) {
@@ -224,16 +225,8 @@ export const WalletProfileStoreModel = types
)
log.trace('[updateNip05]', 'profileRecord', {profileRecord})
self.pubkey = newPubkey
self.walletId = profileRecord.walletId
self.nip05 = profileRecord.nip05
self.lud16 = profileRecord.lud16
self.name = profileRecord.name
self.picture = profileRecord.avatar
self.isOwnProfile = isOwnProfile
// do not publish to relay as this is external
self.hydrate(profileRecord)
log.info('[updateNip05]', 'Wallet nip05 updated in the WalletProfileStore', {self})
return self
@@ -247,18 +240,14 @@ export const WalletProfileStoreModel = types
}
)
self.pubkey = currentPubkey
// in seed based or backup import recovery we rotate the wallet seed to provided one
if(!isAddressOnly) {
self.seedHash = seedHash
}
self.walletId = profileRecord.walletId
self.nip05 = profileRecord.nip05
self.lud16 = profileRecord.lud16
self.name = profileRecord.name
self.picture = profileRecord.avatar
const publishedEvent = yield self.publishToRelays()
log.info('[recover]', 'Wallet profile recovered in WalletProfileStore', {self, publishedEvent})
}
self.hydrate(profileRecord)
log.info('[recover]', 'Wallet profile recovered in WalletProfileStore', {self})
return self
}),
setDevice: flow(function* setDevice(device: string) {
@@ -277,14 +266,7 @@ export const WalletProfileStoreModel = types
},
setSeedHash(seedHash: string) { // used in migration to v8 model
self.seedHash = seedHash
},
/* setIsBatchClaimOn: (isBatchClaimOn: boolean) => {
self.isBatchClaimOn = isBatchClaimOn
return isBatchClaimOn
},
setPicture(picture: string) {
self.picture = picture
}*/
}
}))
.views(self => ({
get npub() {
+4 -4
View File
@@ -10,10 +10,10 @@ import {
MintActiveKeys,
OutputAmounts,
Token,
MeltProofsResponse,
ProofState,
MeltProofsResponse,
CheckStateEnum
} from '@cashu/cashu-ts'
import { mnemonicToSeedSync } from '@scure/bip39'
import { isObj } from '@cashu/cashu-ts/src/utils'
import { JS_BUNDLE_VERSION } from '@env'
import {KeyChain, MinibitsClient} from '../services'
@@ -25,7 +25,7 @@ import { Proof } from './Proof'
import { Mint } from './Mint'
import { getRootStore } from './helpers/getRootStore'
import {deriveSeedFromMnemonic} from '@cashu/crypto/modules/client/NUT09'
/*
Not persisted, in-memory only model of the cashu-ts wallet instances and seed.
@@ -156,7 +156,7 @@ export const WalletStoreModel = types
if (!mnemonic) {
mnemonic = KeyChain.generateMnemonic() as string
const seed = deriveSeedFromMnemonic(mnemonic) // expensive
const seed = mnemonicToSeedSync(mnemonic) // expensive
yield KeyChain.saveMnemonic(mnemonic)
yield KeyChain.saveSeed(seed)
+1
View File
@@ -346,6 +346,7 @@ Sentry id: ${userSettingsStore.userSettings.walletId}
RightComponent={<View style={$rightContainer} />}
style={$item}
onPress={deletePending}
topSeparator
/>
<ListItem
tx="developerScreen.reset"
+5 -4
View File
@@ -5,6 +5,7 @@ import {validateMnemonic} from '@scure/bip39'
import { btoa, atob } from 'react-native-quick-base64'
import QuickCrypto from 'react-native-quick-crypto'
import { wordlist } from '@scure/bip39/wordlists/english'
import { mnemonicToSeedSync } from '@scure/bip39'
import {colors, spacing, typography, useThemeColor} from '../theme'
import {AppStackScreenProps, goBack} from '../navigation' // @demo remove-current-line
import {
@@ -27,7 +28,6 @@ import { rootStoreInstance, useStores } from '../models'
import {MnemonicInput} from './Recovery/MnemonicInput'
import { TransactionStatus } from '../models/Transaction'
import { ResultModalInfo } from './Wallet/ResultModalInfo'
import {deriveSeedFromMnemonic} from '@cashu/crypto/modules/client/NUT09'
import { MINIBITS_MINT_URL, MINIBITS_NIP05_DOMAIN } from '@env'
import { delay } from '../utils/utils'
import { applySnapshot} from 'mobx-state-tree'
@@ -125,9 +125,9 @@ export const ImportBackupScreen: FC<AppStackScreenProps<'ImportBackup'>> = obser
}
const start = performance.now()
const binarySeed = deriveSeedFromMnemonic(mnemonic) // expensive
const binarySeed = mnemonicToSeedSync(mnemonic) // expensive
const end = performance.now()
log.debug(`[onConfirm] deriveSeedFromMnemonic took ${end - start} ms.`)
log.debug(`[onConfirm] mnemonicToSeedSync took ${end - start} ms.`)
setSeed(binarySeed)
setIsValidMnemonic(true)
@@ -312,7 +312,7 @@ export const ImportBackupScreen: FC<AppStackScreenProps<'ImportBackup'>> = obser
if(isAddressOnlyRecovery) {
// in case of wallet address only recovery we have existing profile linked to currentPubkey
// on server. Then we migrate wallet address linked to seed to that profile.
// we keep current wallet seed.
// we keep current wallet seed, not rotating to the provided one.
await walletProfileStore.recover(seedHash as string, currentPubkey, true)
} else {
// In case of recovery from backup we have new nostr keys and we link the new pubkey to the profile
@@ -341,6 +341,7 @@ export const ImportBackupScreen: FC<AppStackScreenProps<'ImportBackup'>> = obser
await delay(1000)
setStatusMessage('')
setIsLoading(false)
// go directly to the wallet (profile hase been rehydrated from the one with the seed)
navigation.navigate('Tabs', {screen: 'WalletNavigator', params: {screen: 'Wallet', params: {}}})
}
} catch (e: any) {
+7 -4
View File
@@ -165,6 +165,7 @@ export const NwcScreen: FC<NwcScreenProps> = observer(
const labelText = useThemeColor('textDim')
const $subText = {color: useThemeColor('textDim'), fontSize: verticalScale(14)}
const headerTitle = useThemeColor('headerTitle')
const buttonBorder = useThemeColor('card')
return (
<Screen contentContainerStyle={$screen}>
@@ -347,15 +348,17 @@ export const NwcScreen: FC<NwcScreenProps> = observer(
maxLength={8}
placeholder='Enter daily limit in satoshi'
selectTextOnFocus={true}
style={[$connInput, {backgroundColor: inputBg, borderBottomRightRadius: 0, borderTopRightRadius: 0}]}
style={[$connInput, {backgroundColor: inputBg}]}
/>
<Button
preset='secondary'
text="SAT"
style={{
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
marginHorizontal: 1,
borderBottomLeftRadius: 0,
marginLeft: -spacing.small,
borderLeftWidth: 1,
borderLeftColor: buttonBorder,
}}
/>
</View>
@@ -412,7 +415,7 @@ const $saveButton: ViewStyle = {
const $connInput: TextStyle = {
flex: 1,
borderRadius: spacing.extraSmall,
fontSize: 16,
fontSize: verticalScale(16),
padding: spacing.small,
alignSelf: 'stretch',
textAlignVertical: 'top',
+1 -1
View File
@@ -1,7 +1,7 @@
import {observer} from 'mobx-react-lite'
import React, {FC, useRef, useState} from 'react'
import {Image, LayoutAnimation, Platform, TextInput, TextStyle, UIManager, View, ViewStyle} from 'react-native'
import {getPublicKey} from 'nostr-tools'
import {getPublicKey} from 'nostr-tools/pure'
import { hexToBytes } from '@noble/hashes/utils'
import {colors, spacing, typography, useThemeColor} from '../theme'
import {ContactsStackScreenProps} from '../navigation'
+5 -7
View File
@@ -31,16 +31,14 @@ import { CashuUtils } from '../services/cashu/cashuUtils'
import { Proof } from '../models/Proof'
import { Transaction, TransactionData, TransactionRecord, TransactionStatus, TransactionType } from '../models/Transaction'
import { ResultModalInfo } from './Wallet/ResultModalInfo'
import {deriveSeedFromMnemonic} from '@cashu/crypto/modules/client/NUT09'
import { mnemonicToSeedSync } from '@scure/bip39'
import { MINIBITS_MINT_URL, MINIBITS_NIP05_DOMAIN } from '@env'
import { delay } from '../utils/utils'
import { getSnapshot, isStateTreeNode } from 'mobx-state-tree'
import { getSnapshot } from 'mobx-state-tree'
import { scale } from '@gocodingnow/rn-size-matters'
import { WalletUtils } from '../services/wallet/utils'
import { WalletScreen } from './WalletScreen'
import { MintUnit, formatCurrency, getCurrency } from '../services/wallet/currency'
import { isObj, sumProofs } from '@cashu/cashu-ts/src/utils'
import { WalletProfileRecord } from '../models/WalletProfileStore'
import { isObj } from '@cashu/cashu-ts/src/utils'
import { translate } from '../i18n'
if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
@@ -154,9 +152,9 @@ export const SeedRecoveryScreen: FC<AppStackScreenProps<'SeedRecovery'>> = obser
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
const start = performance.now()
const binarySeed = deriveSeedFromMnemonic(mnemonic)
const binarySeed = mnemonicToSeedSync(mnemonic)
const end = performance.now()
console.log(`[onConfirm] deriveSeedFromMnemonic took ${end - start} ms.`)
log.debug(`[onConfirm] mnemonicToSeedSync took ${end - start} ms.`)
setSeed(binarySeed)
setIsValidMnemonic(true)
+7 -6
View File
@@ -24,7 +24,7 @@ import {
ImageStyle,
Image,
} from 'react-native'
import { kinds as NostrKinds } from 'nostr-tools'
import { kinds } from 'nostr-tools'
import {spacing, typography, useThemeColor, colors} from '../theme'
import {WalletStackScreenProps} from '../navigation'
import {
@@ -512,15 +512,16 @@ export const SendScreen: FC<WalletStackScreenProps<'Send'>> = observer(
const message = `nostr:${walletProfileStore.npub} sent you ${amountToSend} ${getCurrency(unit).code} from Minibits wallet!`
const content = message + ' \n' + encodedTokenToSend
const encryptedContent = await NostrClient.encryptNip04(
const sentEvent = await NostrClient.encryptAndSendDirectMessageNip17(
receiverPubkey as string,
content as string
content as string,
relaysToShareTo
)
// log.trace('Relays', relaysToShareTo)
const dmEvent: NostrUnsignedEvent = {
kind: NostrKinds.EncryptedDirectMessage,
/* const dmEvent: NostrUnsignedEvent = {
kind: kinds.EncryptedDirectMessage,
pubkey: senderPubkey,
tags: [['p', receiverPubkey as string], ['from', walletProfileStore.nip05]],
content: encryptedContent,
@@ -530,7 +531,7 @@ export const SendScreen: FC<WalletStackScreenProps<'Send'>> = observer(
const sentEvent: NostrEvent | undefined = await NostrClient.publish(
dmEvent,
relaysToShareTo,
)
)*/
setIsNostrDMSending(false)
+7 -6
View File
@@ -14,7 +14,7 @@ import {
Image,
ImageStyle,
} from 'react-native'
import { kinds as NostrKinds } from 'nostr-tools'
import { kinds } from 'nostr-tools'
import {spacing, useThemeColor, colors, typography} from '../theme'
import {WalletStackScreenProps} from '../navigation'
import {
@@ -507,15 +507,16 @@ export const TopupScreen: FC<WalletStackScreenProps<'Topup'>> = observer(
content = content + `Memo: ${memo}`
}
const encryptedContent = await NostrClient.encryptNip04(
receiverPubkey as string,
const sentEvent = await NostrClient.encryptAndSendDirectMessageNip17(
receiverPubkey as string,
content as string,
relaysToShareTo
)
// log.trace('Relays', relaysToShareTo)
const dmEvent: NostrUnsignedEvent = {
kind: NostrKinds.EncryptedDirectMessage,
/* const dmEvent: NostrUnsignedEvent = {
kind: kinds.EncryptedDirectMessage,
pubkey: senderPubkey,
tags: [
['p', receiverPubkey as string],
@@ -528,7 +529,7 @@ export const TopupScreen: FC<WalletStackScreenProps<'Topup'>> = observer(
const sentEvent: NostrEvent | undefined = await NostrClient.publish(
dmEvent,
relaysToShareTo,
)
) */
setIsNostrDMSending(false)
+99 -52
View File
@@ -1,5 +1,5 @@
import {observer} from 'mobx-react-lite'
import React, {FC, useState, useEffect, useCallback, useRef} from 'react'
import React, {FC, useState, useEffect, useCallback, useRef, ReactElement} from 'react'
import {useFocusEffect} from '@react-navigation/native'
import {
TextStyle,
@@ -34,11 +34,12 @@ import {
ScanIcon,
MintIcon
} from '../components'
import EventEmitter from '../utils/eventEmitter'
import {useStores} from '../models'
import {WalletStackScreenProps} from '../navigation'
import {Mint, UnitBalance} from '../models/Mint'
import {MintsByUnit} from '../models/MintsStore'
import {log, NostrClient} from '../services'
import {log, NostrClient, WalletTaskResult} from '../services'
import {Env} from '../utils/envtypes'
import {Transaction} from '../models/Transaction'
import {TransactionListItem} from './Transactions/TransactionListItem'
@@ -61,6 +62,7 @@ import { CurrencyAmount } from './Wallet/CurrencyAmount'
import { LeftProfileHeader } from './ContactsScreen'
import { NavigationState, Route, TabBar, TabView } from 'react-native-tab-view'
import { getUnixTime } from 'date-fns/getUnixTime'
import { delay } from '../utils/utils'
const deploymentKey = APP_ENV === Env.PROD ? CODEPUSH_PRODUCTION_DEPLOYMENT_KEY : CODEPUSH_STAGING_DEPLOYMENT_KEY
const PENDING_CHECK_INTERVAL = 30
@@ -70,8 +72,7 @@ interface WalletScreenProps extends WalletStackScreenProps<'Wallet'> {}
export const WalletScreen: FC<WalletScreenProps> = observer(
function WalletScreen({route, navigation}) {
const {
relaysStore,
const {
mintsStore,
proofsStore,
transactionsStore,
@@ -102,13 +103,13 @@ export const WalletScreen: FC<WalletScreenProps> = observer(
const [defaultMintUrl, setDefaultMintUrl] = useState<string>(MINIBITS_MINT_URL)
const [error, setError] = useState<AppError | undefined>()
const [isLoading, setIsLoading] = useState<boolean>(false)
// const [lastClaimCheck, setLastClaimCheck] = useState<number>(0)
// const [lastPendingCheck, setLastPendingCheck] = useState<number>(0)
const [isMintsModalVisible, setIsMintsModalVisible] = useState<boolean>(false)
const [isUpdateAvailable, setIsUpdateAvailable] = useState<boolean>(false)
const [isUpdateModalVisible, setIsUpdateModalVisible] = useState<boolean>(false)
const [isSendModalVisible, setIsSendModalVisible] = useState<boolean>(false)
const [isReceiveModalVisible, setIsReceiveModalVisible] = useState<boolean>(false)
const [headerTitle, setHeaderTitle] = useState<string>('')
const [headerStyle, setHeaderStyle] = useState<'success' | 'warning' | 'error'>('success')
const [updateDescription, setUpdateDescription] = useState<string>('')
const [updateSize, setUpdateSize] = useState<string>('')
const [isNativeUpdateAvailable, setIsNativeUpdateAvailable] = useState<boolean>(false)
@@ -188,11 +189,50 @@ export const WalletScreen: FC<WalletScreenProps> = observer(
const isRemoteDataPushEnabled = walletProfileStore.device ? true : false
if(!isRemoteDataPushEnabled) {nwcStore.receiveNwcEvents()}
}
const handleReceivedEventTaskResult = async (result: WalletTaskResult) => {
log.trace('[handleReceivedEventTaskResult]')
if(result.error) {
setHeaderTitle(result.message.slice(0, 20))
setHeaderStyle('error')
await delay(3000)
setHeaderTitle('')
} else {
setHeaderTitle('Relays connected')
setHeaderStyle('success')
await delay(3000)
setHeaderTitle('')
}
}
const handleClaimTaskResult = async (result: WalletTaskResult) => {
log.trace('[handleClaimTaskResult]')
if(result.error) {
setHeaderTitle(result.message.slice(0, 20))
setHeaderStyle('error')
await delay(3000)
setHeaderTitle('')
} else {
setHeaderTitle('Claim completed')
setHeaderStyle('success')
await delay(3000)
setHeaderTitle('')
}
}
Linking.addEventListener('url', handleDeeplink)
EventEmitter.on('ev__handleReceivedEventTask_result', handleReceivedEventTaskResult)
EventEmitter.on('ev__handleClaimTask_result', handleClaimTaskResult)
getInitialData()
return () => {}
// Unsubscribe from the task result event on component unmount
return () => {
EventEmitter.off('ev__handleReceivedEventTask_result', handleReceivedEventTaskResult)
EventEmitter.off('ev__handleClaimTask_result', handleClaimTaskResult)
}
}, [])
@@ -263,7 +303,7 @@ export const WalletScreen: FC<WalletScreenProps> = observer(
if (nowInSec - lastClaimCheckRef.current > CLAIM_CHECK_INTERVAL) {
lastClaimCheckRef.current = nowInSec
WalletTask.handleClaim().catch(e => false)
WalletTask.handleClaim().catch(e => handleError(e))
if (userSettingsStore.exchangeCurrency) {
walletStore.refreshExchangeRate(userSettingsStore.exchangeCurrency)
@@ -506,17 +546,6 @@ export const WalletScreen: FC<WalletScreenProps> = observer(
const tabWidth = moderateScale(75)
const getActiveTabColor = (state: NavigationState<Route>) => {
/* if(state && state.routes.length > 0) {
switch (state.routes[tabIndex].key) {
case 'usd':
return useThemeColor('usd')
case 'eur':
return useThemeColor('eur')
default:
return useThemeColor('btc')
}
}*/
return useThemeColor('headerTitle')
}
@@ -541,6 +570,39 @@ export const WalletScreen: FC<WalletScreenProps> = observer(
</View>
)
}
const HeaderTitle = function (props: any) {
if (!isInternetReachable) {
return (
<Text
tx="common.offline"
style={$warning}
size="xxs"
/>
)
}
if (headerTitle.length > 0) {
const styleMap: Record<string, any> = {
error: $error,
success: $success,
warning: $warning,
}
const style = styleMap[headerStyle] || undefined
return (
<Text
text={headerTitle}
style={[style, $headerTitle]}
size="xxs"
/>
)
}
return undefined
}
const headerBg = useThemeColor('header')
const balances = proofsStore.getBalances()
@@ -548,7 +610,7 @@ export const WalletScreen: FC<WalletScreenProps> = observer(
const mainButtonIcon = useThemeColor('mainButtonIcon')
const mainButtonColor = useThemeColor('card')
const label = useThemeColor('textDim')
const headerTitle = useThemeColor('headerTitle')
const headerTitleColor = useThemeColor('headerTitle')
const isNwcVisible = nwcStore.all.some(c => c.remainingDailyLimit !== c.dailyLimit)
const nwcCardsData = nwcStore.all.filter(c => c.remainingDailyLimit !== c.dailyLimit)
@@ -560,14 +622,7 @@ export const WalletScreen: FC<WalletScreenProps> = observer(
gotoProfile={gotoProfile}
isAvatarVisible={false}
/>}
TitleActionComponent={!isInternetReachable ? (
<Text
tx={'common.offline'}
style={$offline}
size='xxs'
/>
) : ( undefined )
}
TitleActionComponent={headerTitle.length > 0 ? <HeaderTitle /> : undefined}
RightActionComponent={
<>
{paymentRequestsStore.countNotExpired > 0 && (
@@ -575,8 +630,8 @@ export const WalletScreen: FC<WalletScreenProps> = observer(
style={{flexDirection: 'row', alignItems:'center', marginRight: spacing.medium}}
onPress={() => gotoPaymentRequests()}
>
<Icon icon='faPaperPlane' color={headerTitle}/>
<Text text={`${paymentRequestsStore.countNotExpired}`} style={{color: headerTitle}} />
<Icon icon='faPaperPlane' color={headerTitleColor}/>
<Text text={`${paymentRequestsStore.countNotExpired}`} style={{color: headerTitleColor}} />
</Pressable>
)}
</>
@@ -808,25 +863,6 @@ const UnitBalanceBlock = observer(function (props: {
})
const ZeroBalanceBlock = function () {
const headerBg = useThemeColor('header')
const balanceColor = 'white'
const currencyColor = colors.palette.primary200
return (
<View style={[$headerContainer, {backgroundColor: headerBg}]}>
<Text
preset='heading'
style={[$unitBalance, {color: balanceColor}]}
text={'0'}
/>
</View>
)
}
const MintsByUnitSummary = observer(function (props: {
mintsByUnit: MintsByUnit
navigation: any
@@ -1138,12 +1174,23 @@ const $bottomModal: ViewStyle = {
}
const $offline: TextStyle = {
const $headerTitle: TextStyle = {
paddingHorizontal: spacing.small,
borderRadius: spacing.tiny,
alignSelf: 'center',
marginVertical: spacing.small,
lineHeight: spacing.medium,
backgroundColor: colors.palette.orange400,
lineHeight: spacing.medium,
color: 'white'
}
const $warning: TextStyle = {
backgroundColor: colors.palette.orange400,
}
const $error: TextStyle = {
backgroundColor: colors.palette.angry300,
}
const $success: TextStyle = {
backgroundColor: colors.palette.success200,
}
+6 -5
View File
@@ -1,9 +1,10 @@
import * as _Keychain from 'react-native-keychain'
import AppError, {Err} from '../utils/AppError'
import QuickCrypto from 'react-native-quick-crypto'
import { generateNewMnemonic } from '@cashu/cashu-ts'
import { generateMnemonic as generateNewMnemonic, mnemonicToSeedSync } from "@scure/bip39"
import { wordlist } from "@scure/bip39/wordlists/english"
import { bytesToHex } from '@noble/hashes/utils'
import { generateSecretKey, getPublicKey } from 'nostr-tools'
import { generateSecretKey, getPublicKey } from 'nostr-tools/pure'
import {btoa, fromByteArray} from 'react-native-quick-base64'
import {log} from './logService'
@@ -37,11 +38,11 @@ const generateMnemonic = function () {
try {
log.trace('[generateMnemonic]', 'start')
const mnemonic = generateNewMnemonic()
const mnemonic = generateNewMnemonic(wordlist)
log.trace('[generateMnemonic]', 'New mnemonic created:', mnemonic)
log.trace('[generateMnemonic]', 'New mnemonic created:', {mnemonic})
return mnemonic as string
return mnemonic
} catch (e: any) {
throw new AppError(Err.KEYCHAIN_ERROR, e.message, e)
}
-1
View File
@@ -2,7 +2,6 @@ import { decode } from "@gandlaf21/bolt11-decode"
import AppError, { Err } from '../../utils/AppError'
import { addSeconds } from 'date-fns'
import { log } from '../logService'
import numbro from "numbro"
import { roundUp, toNumber } from "../../utils/number"
// TODO refactor all this into own module
+72 -20
View File
@@ -1,12 +1,9 @@
import type { Event as NostrEvent, UnsignedEvent as NostrUnsignedEvent } from 'nostr-tools/core'
import { validateEvent, finalizeEvent } from 'nostr-tools'
import type { EventTemplate as NostrEventTemplate, Event as NostrEvent, UnsignedEvent as NostrUnsignedEvent } from 'nostr-tools/core'
import type { Filter as NostrFilter } from 'nostr-tools/filter'
import { nip19 } from 'nostr-tools'
import { nip04 } from 'nostr-tools'
import { utils as NostrUtils } from 'nostr-tools'
import { SimplePool } from 'nostr-tools'
import { nip04, nip19, nip59, finalizeEvent, validateEvent, utils } from 'nostr-tools'
import {SimplePool} from 'nostr-tools'
import { hexToBytes } from '@noble/hashes/utils'
import { kinds as NostrKinds } from 'nostr-tools'
import { kinds } from 'nostr-tools'
import {
MINIBITS_RELAY_URL,
} from '@env'
@@ -17,6 +14,7 @@ import { MinibitsClient } from './minibitsService'
import { rootStoreInstance } from '../models'
import { WalletTask } from './walletService'
export {
NostrEvent,
NostrFilter,
@@ -171,6 +169,65 @@ const decryptNip04 = async function(
}
const encryptAndSendDirectMessageNip17 = async function (
recipientPublicKey: string,
message: string,
relays: string[]
) {
const keys: KeyPair = await getOrCreateKeyPair()
const directMessageEvent: NostrEventTemplate = {
created_at: Math.ceil(Date.now() / 1000),
kind: kinds.PrivateDirectMessage,
tags: [['p', recipientPublicKey], ['from', walletProfileStore.nip05]],
content: message,
}
// log.trace('[sendDirectMessageNip17]', {directMessageEvent})
// already signed final event
const wrappedEvent = nip59.wrapEvent(
directMessageEvent,
hexToBytes(keys.privateKey),
recipientPublicKey
)
// log.trace('[sendDirectMessageNip17]', {wrappedEvent})
const pool = getRelayPool()
await Promise.any(pool.publish(relays, wrappedEvent))
const published = await pool.get(relays, {
ids: [wrappedEvent.id]
}) as NostrEvent
if(published) {
log.trace('[NostrClient.publish] Event successfully published')
return published
}
return undefined
}
const decryptDirectMessageNip17 = async function (
wrappedEvent: NostrEvent
) {
const keys: KeyPair = await getOrCreateKeyPair()
// log.trace('[decryptDirectMessageNip59]', {keys})
const decryptedEvent = nip59.unwrapEvent(
wrappedEvent,
hexToBytes(keys.privateKey)
)
log.trace('[decryptDirectMessageNip59]', {decryptedEvent})
return decryptedEvent
}
const publish = async function (
event: NostrUnsignedEvent,
relays: string[],
@@ -178,11 +235,6 @@ const publish = async function (
const keys: KeyPair = await getOrCreateKeyPair()
/* const signed = {...event} as NostrEvent
signed.created_at = Math.floor(Date.now() / 1000)
signed.id = getEventHash(signed)
signed.sig = getSignature(signed, keys.privateKey) */
const privateKeyBytes = hexToBytes(keys.privateKey)
const finalEvent = finalizeEvent(event, privateKeyBytes)
@@ -325,7 +377,7 @@ const getProfileFromRelays = async function (pubkey: string, relays: string[]):
// get profile from the relays for pubkey linked to nip05
const filter: NostrFilter = {
authors: [pubkey],
kinds: [NostrKinds.Metadata],
kinds: [kinds.Metadata],
}
const events = await NostrClient.getEvents(relays, filter)
@@ -435,7 +487,7 @@ const deleteKeyPair = async function (): Promise<void> {
const getNormalizedRelayUrl = function (url: string): string {
try {
return NostrUtils.normalizeURL(url)
return utils.normalizeURL(url)
} catch (e: any) {
throw new AppError(Err.VALIDATION_ERROR, `Invalid relay URL: ${e.message}`)
}
@@ -443,13 +495,11 @@ const getNormalizedRelayUrl = function (url: string): string {
// returns array of values after the first element (tag name)
const getTagsByName = function(tagsArray: string[][], tagName: string) {
let tagValues = tagsArray.find(t => t && t.length && t.length >= 2 && t[0] === tagName)
if(tagValues && tagValues.length > 1) {
tagValues.shift() // remove tag name
return tagValues
const getTagsByName = function(tagsArray: string[][], tagName: string): string[] | undefined {
const tag = tagsArray.find(t => t && t.length >= 2 && t[0] === tagName);
if (tag) {
return tag.slice(1) // Return a copy of the array without modifying the original
}
return undefined
}
@@ -497,6 +547,8 @@ export const NostrClient = { // TODO split helper functions to separate module
neventEncode,
encryptNip04,
decryptNip04,
encryptAndSendDirectMessageNip17,
decryptDirectMessageNip17,
getDomainFromNip05,
getNameFromNip05,
publish,
+213 -197
View File
@@ -1,6 +1,6 @@
import {isBefore} from 'date-fns'
import {getSnapshot} from 'mobx-state-tree'
import { kinds as NostrKinds } from 'nostr-tools'
import { kinds } from 'nostr-tools'
import {log} from './logService'
import {Proof} from '../models/Proof'
import {
@@ -12,7 +12,7 @@ import {CashuProof, CashuUtils} from './cashu/cashuUtils'
import {LightningUtils} from './lightning/lightningUtils'
import AppError, {Err} from '../utils/AppError'
import {MintBalance, MintProofsCounter, MintStatus} from '../models/Mint'
import {CheckStateEnum, MeltQuoteResponse, MintQuoteState, Token, getDecodedToken, getEncodedToken} from '@cashu/cashu-ts'
import {MeltQuoteResponse, MintQuoteState, Token, getDecodedToken, getEncodedToken} from '@cashu/cashu-ts'
import {Mint} from '../models/Mint'
import {pollerExists, stopPolling} from '../utils/poller'
import { NostrClient, NostrEvent, NostrProfile } from './nostrService'
@@ -31,7 +31,7 @@ import { NotificationService } from './notificationService'
import { MintUnit, formatCurrency, getCurrency } from './wallet/currency'
import { MinibitsClient } from './minibitsService'
import { getKeepAmounts } from '@cashu/cashu-ts/src/utils'
import { keys } from 'mobx'
import { KeyChain } from './keyChain'
/**
@@ -1396,41 +1396,41 @@ const handleClaim = async function (): Promise<void> {
log.info('[handleClaim] start')
const {walletId, seedHash, pubkey} = walletProfileStore
const {isBatchClaimOn} = userSettingsStore
let recoveredWalletId: string | null = null
const {isBatchClaimOn} = userSettingsStore
let recoveredSeedHash: string | undefined = undefined
if(!seedHash || !pubkey) {
throw new AppError(
Err.VALIDATION_ERROR,
'Skipping claim of ecash received to your lightning address, missing profile data. Reinstall wallet to fix it.',
{walletId, seedHash, pubkey}
)
// If we somehow lost walletProfile state, try to recover is from the server using the seedHash stored in KeyChain
if(!seedHash) {
recoveredSeedHash = await KeyChain.loadSeedHash()
if(!recoveredSeedHash) {
throw new AppError(Err.VALIDATION_ERROR, 'Wallet data were damaged, please reinstall wallet.')
}
}
if(!walletId) {
// fix immediately in case only walletId missing in walletProfile
const profile = await MinibitsClient.getWalletProfileBySeedHash(seedHash)
if(!walletId || !pubkey) {
// recover profile from the server
const profile = await MinibitsClient.getWalletProfileBySeedHash(seedHash || recoveredSeedHash!)
if(profile) {
recoveredWalletId = profile.walletId
walletProfileStore.setWalletId(recoveredWalletId)
walletProfileStore.hydrate(profile)
}
}
// Based on user setting, ask for batched token if more then 5 payments are waiting to be claimed
const claimedTokens = await MinibitsClient.createClaim(
walletId || recoveredWalletId as string,
seedHash,
pubkey,
walletProfileStore.walletId,
walletProfileStore.seedHash as string,
walletProfileStore.pubkey,
isBatchClaimOn ? 5 : undefined
)
if(claimedTokens.length === 0) {
log.debug('[handleClaim] No claimed invoices returned from the server...')
return
} else {
log.debug(`[handleClaim] Claimed ${claimedTokens.length} tokens from the server...`)
}
log.debug(`[handleClaim] Claimed ${claimedTokens.length} tokens from the server...`)
for(const claimedToken of claimedTokens) {
const now = new Date().getTime()
@@ -1534,7 +1534,7 @@ const _sendTopupNotification = async function (pr: PaymentRequest) {
* Checks with NOSTR relays whether there is ecash to be received or an invoice to be paid.
*/
const receiveEventsFromRelays = async function (): Promise<void> {
log.trace('[receiveEventsFromRelays] start')
log.trace('[receiveEventsFromRelays] starting listening for events')
if(!walletProfileStore.pubkey) {
const message = `No wallet profile created.`
@@ -1544,12 +1544,15 @@ const receiveEventsFromRelays = async function (): Promise<void> {
try {
const { lastPendingReceivedCheck } = contactsStore
const TWO_DAYS = 2 * 24 * 60 * 60
const filter = {
kinds: [NostrKinds.EncryptedDirectMessage],
kinds: [kinds.GiftWrap],
"#p": [walletProfileStore.pubkey],
since: lastPendingReceivedCheck || 0
}
since: lastPendingReceivedCheck ? lastPendingReceivedCheck - TWO_DAYS : 0
} // 2 days variance to the past from real event created_at for dm sent as gift wraps
log.trace('[receiveEventsFromRelays]', {filter})
contactsStore.setLastPendingReceivedCheck()
const pool = NostrClient.getRelayPool()
@@ -1585,8 +1588,9 @@ const receiveEventsFromRelays = async function (): Promise<void> {
eventsBatch.push(event)
contactsStore.addReceivedEventId(event.id)
// move window to receive events to the last event created_at to avoid recive it again
contactsStore.setLastPendingReceivedCheck(event.created_at)
// contactsStore.setLastPendingReceivedCheck(event.created_at)
const now = new Date().getTime()
SyncQueue.addTask(
@@ -1617,192 +1621,204 @@ const receiveEventsFromRelays = async function (): Promise<void> {
}
}
const _handleReceivedEventTask = async function (event: NostrEvent): Promise<WalletTaskResult> {
// decrypt message content
const decrypted = await NostrClient.decryptNip04(event.pubkey, event.content)
const _handleReceivedEventTask = async function (wrappedEvent: NostrEvent): Promise<WalletTaskResult> {
try {
const decryptedDirectMessageEvent = await NostrClient.decryptDirectMessageNip17(wrappedEvent)
// set REAL direct message created_at as the lastPendingReceivedCheck
contactsStore.setLastPendingReceivedCheck(decryptedDirectMessageEvent.created_at)
log.trace('[_handleReceivedEventTask]', 'Received event', {id: event.id, created_at: event.created_at})
// get sender profile and save it as a contact
// this is not valid for events sent from LNURL bridge, that are sent and signed by a minibits server key
// and *** do not contain sentFrom *** // LEGACY, replaced by claim api
let sentFromPubkey = event.pubkey
let sentFrom = NostrClient.getFirstTagValue(event.tags, 'from')
let sentFromNpub = NostrClient.getNpubkey(sentFromPubkey)
let contactFrom: Contact | undefined = undefined
let zapSenderProfile: NostrProfile | undefined = undefined
let sentFromPicture: string | undefined = undefined
// add ecash sender to the contacts
if(sentFrom) {
const sentFromName = NostrClient.getNameFromNip05(sentFrom as string)
log.trace('[_handleReceivedEventTask]', 'Received event', {decryptedDirectMessageEvent})
if(sentFrom.includes(MINIBITS_NIP05_DOMAIN)) {
sentFromPicture = MINIBITS_SERVER_API_HOST + '/profile/avatar/' + sentFromPubkey
}
// get sender profile and save it as a contact
// this is not valid for events sent from LNURL bridge, that are sent and signed by a minibits server key
// and *** do not contain sentFrom *** // LEGACY, replaced by claim api
let sentFromPubkey = decryptedDirectMessageEvent.pubkey
let sentFrom = NostrClient.getFirstTagValue(decryptedDirectMessageEvent.tags, 'from')
let sentFromNpub = NostrClient.getNpubkey(sentFromPubkey)
let contactFrom: Contact | undefined = undefined
let zapSenderProfile: NostrProfile | undefined = undefined
let sentFromPicture: string | undefined = undefined
// we skip retrieval of external nostr profiles to minimize failures
// external contacts will thus miss image and lud16 address...
contactFrom = {
pubkey: sentFromPubkey,
npub: sentFromNpub,
nip05: sentFrom,
lud16: sentFrom.includes(MINIBITS_NIP05_DOMAIN) ? sentFrom : undefined,
name: sentFromName || undefined,
picture: sentFromPicture || undefined,
isExternalDomain: sentFrom.includes(MINIBITS_NIP05_DOMAIN) ? false : true
} as Contact
contactsStore.addContact(contactFrom)
} else {
// Event was sent from Minibits server
const maybeZapSenderString = _extractZapSenderData(decrypted)
if(maybeZapSenderString) {
try {
zapSenderProfile = JSON.parse(maybeZapSenderString)
if(zapSenderProfile) {
sentFromPubkey = zapSenderProfile.pubkey // zap sender pubkey
sentFrom = zapSenderProfile.nip05 ?? zapSenderProfile.name
sentFromPicture = zapSenderProfile.picture
const sentFromLud16 = zapSenderProfile.lud16
// if we have such contact, set or update its lightning address by the one from profile
const contactInstance = contactsStore.findByPubkey(sentFromPubkey)
if(contactInstance && sentFromLud16) {
contactInstance.setLud16(sentFromLud16)
}
}
} catch (e: any) {
log.warn('[_handleReceivedEventTask]', 'Could not get sender from zapRequest', {message: e.message, maybeZapSenderString})
}
}
}
// parse incoming message
const incoming = IncomingParser.findAndExtract(decrypted)
log.trace('[_handleReceivedEventTask]', 'Incoming data', {incoming})
//
// Receive token start
//
if(incoming.type === IncomingDataType.CASHU) {
const decoded = getDecodedToken(incoming.encoded)
const amountToReceive = CashuUtils.getProofsAmount(decoded.proofs)
const memo = decoded.memo || 'Received over Nostr'
const {transaction, receivedAmount} = await receiveTask(
decoded,
amountToReceive,
memo,
incoming.encoded as string,
)
// store contact or zapseder in tx details
if(transaction && sentFrom) {
if (contactFrom) {
transaction.setProfile(JSON.stringify(contactFrom))
transaction.setSentFrom(sentFrom)
}
if (zapSenderProfile) {
transaction.setProfile(JSON.stringify(zapSenderProfile))
transaction.setSentFrom(sentFrom)
// add ecash or pr sender to the contacts
if(sentFrom) {
const sentFromName = NostrClient.getNameFromNip05(sentFrom as string)
if(sentFrom.includes(MINIBITS_NIP05_DOMAIN)) {
sentFromPicture = MINIBITS_SERVER_API_HOST + '/profile/avatar/' + sentFromPubkey
}
const isZap = zapSenderProfile ? true : false
// we skip retrieval of external nostr profiles to minimize failures
// external contacts will thus miss image and lud16 address...
contactFrom = {
pubkey: sentFromPubkey,
npub: sentFromNpub,
nip05: sentFrom,
lud16: sentFrom.includes(MINIBITS_NIP05_DOMAIN) ? sentFrom : undefined,
name: sentFromName || undefined,
picture: sentFromPicture || undefined,
isExternalDomain: sentFrom.includes(MINIBITS_NIP05_DOMAIN) ? false : true
} as Contact
contactsStore.addContact(contactFrom)
} else {
// Event was sent from Minibits server
const maybeZapSenderString = _extractZapSenderData(decryptedDirectMessageEvent.content)
// We do it defensively only after cash is received
// and asynchronously so we speed up queue
_sendReceiveNotification(
receivedAmount,
transaction.unit,
isZap,
sentFrom,
sentFromPicture
) // TODO move to task result handler
if(maybeZapSenderString) {
try {
zapSenderProfile = JSON.parse(maybeZapSenderString)
if(zapSenderProfile) {
sentFromPubkey = zapSenderProfile.pubkey // zap sender pubkey
sentFrom = zapSenderProfile.nip05 ?? zapSenderProfile.name
sentFromPicture = zapSenderProfile.picture
const sentFromLud16 = zapSenderProfile.lud16
// we do not add zappers to contacts but if we have such contact, set or update its lightning address by the one from profile
const contactInstance = contactsStore.findByPubkey(sentFromPubkey)
if(contactInstance && sentFromLud16) {
contactInstance.setLud16(sentFromLud16)
}
}
} catch (e: any) {
log.warn('[_handleReceivedEventTask]', 'Could not get sender from zapRequest', {message: e.message, maybeZapSenderString})
}
}
}
return {
mintUrl: decoded.mint,
taskFunction: '_handleReceivedEventTask',
message: 'Incoming ecash token has been received.',
proofsCount: decoded.proofs.length,
proofsAmount: receivedAmount,
notificationInputs: {
event,
decrypted,
transaction,
receivedAmount
} // not used`
} as WalletTaskResult
}
// parse incoming message
const incoming = IncomingParser.findAndExtract(decryptedDirectMessageEvent.content)
//
// Receive bolt11 invoice start
//
if (incoming.type === IncomingDataType.INVOICE) {
// receiver is current wallet profile
const {
pubkey,
npub,
name,
picture,
} = walletProfileStore
log.trace('[_handleReceivedEventTask]', 'Incoming data', {incoming})
const contactTo: Contact = {
pubkey,
npub,
name,
picture
}
const decoded = LightningUtils.decodeInvoice(incoming.encoded)
const {
amount,
description,
expiry,
payment_hash: paymentHash,
timestamp
} = LightningUtils.getInvoiceData(decoded)
const maybeMemo = NostrClient.findMemo(decrypted)
const paymentRequest = paymentRequestsStore.addPaymentRequest({
type: PaymentRequestType.INCOMING,
status: PaymentRequestStatus.ACTIVE,
encodedInvoice: incoming.encoded,
invoicedUnit: 'sat', // bolt11
invoicedAmount: amount || 0,
description: maybeMemo ? maybeMemo : description,
paymentHash,
contactFrom: contactFrom || {pubkey: sentFromPubkey, npub: sentFromNpub},
contactTo,
expiry,
createdAt: timestamp ? new Date(timestamp * 1000) : new Date()
})
//
// Receive token start
//
if(incoming.type === IncomingDataType.CASHU) {
_sendPaymentRequestNotification(paymentRequest)
const decoded = getDecodedToken(incoming.encoded)
const amountToReceive = CashuUtils.getProofsAmount(decoded.proofs)
const memo = decoded.memo || 'Received over Nostr'
const {transaction, receivedAmount} = await receiveTask(
decoded,
amountToReceive,
memo,
incoming.encoded as string,
)
// store contact or zapseder in tx details
if(transaction && sentFrom) {
if (contactFrom) {
transaction.setProfile(JSON.stringify(contactFrom))
transaction.setSentFrom(sentFrom)
}
if (zapSenderProfile) {
transaction.setProfile(JSON.stringify(zapSenderProfile))
transaction.setSentFrom(sentFrom)
}
const isZap = zapSenderProfile ? true : false
// We do it defensively only after cash is received
// and asynchronously so we speed up queue
_sendReceiveNotification(
receivedAmount,
transaction.unit,
isZap,
sentFrom,
sentFromPicture
) // TODO move to task result handler
}
return {
mintUrl: decoded.mint,
taskFunction: '_handleReceivedEventTask',
message: 'Incoming ecash token has been received.',
proofsCount: decoded.proofs.length,
proofsAmount: receivedAmount,
notificationInputs: {
event: decryptedDirectMessageEvent,
decrypted: decryptedDirectMessageEvent.content,
transaction,
receivedAmount
} // not used`
} as WalletTaskResult
}
//
// Receive bolt11 invoice start
//
else if (incoming.type === IncomingDataType.INVOICE) {
// receiver is current wallet profile
const {
pubkey,
npub,
name,
picture,
} = walletProfileStore
const contactTo: Contact = {
pubkey,
npub,
name,
picture
}
const decoded = LightningUtils.decodeInvoice(incoming.encoded)
const {
amount,
description,
expiry,
payment_hash: paymentHash,
timestamp
} = LightningUtils.getInvoiceData(decoded)
const maybeMemo = NostrClient.findMemo(decryptedDirectMessageEvent.content)
const paymentRequest = paymentRequestsStore.addPaymentRequest({
type: PaymentRequestType.INCOMING,
status: PaymentRequestStatus.ACTIVE,
encodedInvoice: incoming.encoded,
invoicedUnit: 'sat', // bolt11
invoicedAmount: amount || 0,
description: maybeMemo ? maybeMemo : description,
paymentHash,
contactFrom: contactFrom || {pubkey: sentFromPubkey, npub: sentFromNpub},
contactTo,
expiry,
createdAt: timestamp ? new Date(timestamp * 1000) : new Date()
})
_sendPaymentRequestNotification(paymentRequest)
return {
mintUrl: '',
taskFunction: '_handleReceivedEventTask',
message: 'Incoming payment request been received.',
proofsCount: 0,
proofsAmount: amount,
paymentRequest
} as WalletTaskResult
}
else if (incoming.type === IncomingDataType.LNURL) {
throw new AppError(Err.NOTFOUND_ERROR, 'LNURL support is not yet implemented.', {caller: '_handleReceivedEventTask'})
} else {
throw new AppError(Err.NOTFOUND_ERROR, 'Received unknown message', incoming)
}
} catch (e: any) {
log.error('[_handleReceivedEventTask]', e.name, e.message)
return {
mintUrl: '',
taskFunction: '_processReceiveEvent',
message: 'Incoming payment request been received.',
proofsCount: 0,
proofsAmount: amount,
paymentRequest
}
taskFunction: '_handleReceivedEventTask',
message: e.message,
error: WalletUtils.formatError(e)
} as WalletTaskResult
}
if (incoming.type === IncomingDataType.LNURL) {
throw new AppError(Err.NOTFOUND_ERROR, 'LNURL support is not yet implemented.', {caller: '_handleReceivedEventTask'})
}
throw new AppError(Err.NOTFOUND_ERROR, 'Received unknown message', incoming)
}
+27 -7
View File
@@ -224,6 +224,13 @@ __metadata:
languageName: node
linkType: hard
"@babel/helper-plugin-utils@npm:^7.18.9":
version: 7.25.9
resolution: "@babel/helper-plugin-utils@npm:7.25.9"
checksum: e19ec8acf0b696756e6d84531f532c5fe508dce57aa68c75572a77798bd04587a844a9a6c8ea7d62d673e21fdc174d091c9097fb29aea1c1b49f9c6eaa80f022
languageName: node
linkType: hard
"@babel/helper-remap-async-to-generator@npm:^7.18.9, @babel/helper-remap-async-to-generator@npm:^7.24.7, @babel/helper-remap-async-to-generator@npm:^7.25.0":
version: 7.25.0
resolution: "@babel/helper-remap-async-to-generator@npm:7.25.0"
@@ -432,6 +439,18 @@ __metadata:
languageName: node
linkType: hard
"@babel/plugin-proposal-export-namespace-from@npm:^7.18.9":
version: 7.18.9
resolution: "@babel/plugin-proposal-export-namespace-from@npm:7.18.9"
dependencies:
"@babel/helper-plugin-utils": ^7.18.9
"@babel/plugin-syntax-export-namespace-from": ^7.8.3
peerDependencies:
"@babel/core": ^7.0.0-0
checksum: 84ff22bacc5d30918a849bfb7e0e90ae4c5b8d8b65f2ac881803d1cf9068dffbe53bd657b0e4bc4c20b4db301b1c85f1e74183cf29a0dd31e964bd4e97c363ef
languageName: node
linkType: hard
"@babel/plugin-proposal-nullish-coalescing-operator@npm:^7.13.8, @babel/plugin-proposal-nullish-coalescing-operator@npm:^7.18.0":
version: 7.18.6
resolution: "@babel/plugin-proposal-nullish-coalescing-operator@npm:7.18.6"
@@ -9439,6 +9458,7 @@ __metadata:
resolution: "minibits_wallet@workspace:."
dependencies:
"@babel/core": ^7.20.0
"@babel/plugin-proposal-export-namespace-from": ^7.18.9
"@babel/preset-env": ^7.20.0
"@babel/runtime": ^7.20.0
"@cashu/cashu-ts": "minibits-cash/cashu-ts#2b51a7ba7a8461a0040359eecda37b98b603692c"
@@ -9483,7 +9503,7 @@ __metadata:
mobx: ^6.13.2
mobx-react-lite: ^4.0.7
mobx-state-tree: ^6.0.1
nostr-tools: 2.9.4
nostr-tools: "minibits-cash/nostr-tools#2991f59d0150a0cb2c71e2f9a4cb6c8aca3b19d7"
numbro: ^2.5.0
patch-package: ^8.0.0
postinstall-postinstall: ^2.1.0
@@ -9870,9 +9890,9 @@ __metadata:
languageName: node
linkType: hard
"nostr-tools@npm:2.9.4":
version: 2.9.4
resolution: "nostr-tools@npm:2.9.4"
"nostr-tools@minibits-cash/nostr-tools#2991f59d0150a0cb2c71e2f9a4cb6c8aca3b19d7":
version: 2.10.4
resolution: "nostr-tools@https://github.com/minibits-cash/nostr-tools.git#commit=2991f59d0150a0cb2c71e2f9a4cb6c8aca3b19d7"
dependencies:
"@noble/ciphers": ^0.5.1
"@noble/curves": 1.2.0
@@ -9880,7 +9900,7 @@ __metadata:
"@scure/base": 1.1.1
"@scure/bip32": 1.3.1
"@scure/bip39": 1.2.1
nostr-wasm: v0.1.0
nostr-wasm: 0.1.0
peerDependencies:
typescript: ">=5.0.0"
dependenciesMeta:
@@ -9889,11 +9909,11 @@ __metadata:
peerDependenciesMeta:
typescript:
optional: true
checksum: 5c4138baaf78b67136f0387b4f1e1abcea82372e5ee80bf3ce69d6d716520a5e16f1665ad9992777fad12068ce333216aa8117b0ae5aaacb062cfd260feb4734
checksum: c5b9871649eef2479e23026f41a3d771bbd879bd54bd60f226eb547bb85e03e411023a756dbbb24966cf0d00bfb3541fc7ca502e1b71a2844fffa55024214462
languageName: node
linkType: hard
"nostr-wasm@npm:v0.1.0":
"nostr-wasm@npm:0.1.0":
version: 0.1.0
resolution: "nostr-wasm@npm:0.1.0"
checksum: c0b404912d47c98efa69b4838a5bb7b2094c869b1aa2a96359c978c911878e9b37228296ef6432254815c9d15cdeebaa16b6d45106db4f3673bb5a6a00cc669c