diff --git a/babel.config.js b/babel.config.js
index 518e7381..2d82d11e 100644
--- a/babel.config.js
+++ b/babel.config.js
@@ -12,6 +12,7 @@ module.exports = {
},
],
'module:react-native-dotenv',
- 'react-native-reanimated/plugin'
+ 'react-native-reanimated/plugin',
+ '@babel/plugin-proposal-export-namespace-from'
],
};
diff --git a/metro.config.js b/metro.config.js
index 9d41685e..7f358075 100644
--- a/metro.config.js
+++ b/metro.config.js
@@ -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);
diff --git a/package.json b/package.json
index 1a071630..06936e73 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/models/NwcStore.ts b/src/models/NwcStore.ts
index 518aa97c..02104192 100644
--- a/src/models/NwcStore.ts
+++ b/src/models/NwcStore.ts
@@ -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
}
diff --git a/src/models/WalletProfileStore.ts b/src/models/WalletProfileStore.ts
index 1109dc61..ca8c082d 100644
--- a/src/models/WalletProfileStore.ts
+++ b/src/models/WalletProfileStore.ts
@@ -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() {
diff --git a/src/models/WalletStore.ts b/src/models/WalletStore.ts
index 8c38ea0d..9d87bbad 100644
--- a/src/models/WalletStore.ts
+++ b/src/models/WalletStore.ts
@@ -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)
diff --git a/src/screens/DeveloperScreen.tsx b/src/screens/DeveloperScreen.tsx
index c21b5a4a..133876df 100644
--- a/src/screens/DeveloperScreen.tsx
+++ b/src/screens/DeveloperScreen.tsx
@@ -346,6 +346,7 @@ Sentry id: ${userSettingsStore.userSettings.walletId}
RightComponent={}
style={$item}
onPress={deletePending}
+ topSeparator
/>
> = 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> = 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> = 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) {
diff --git a/src/screens/NwcScreen.tsx b/src/screens/NwcScreen.tsx
index e012ca9c..05d581ef 100644
--- a/src/screens/NwcScreen.tsx
+++ b/src/screens/NwcScreen.tsx
@@ -165,6 +165,7 @@ export const NwcScreen: FC = observer(
const labelText = useThemeColor('textDim')
const $subText = {color: useThemeColor('textDim'), fontSize: verticalScale(14)}
const headerTitle = useThemeColor('headerTitle')
+ const buttonBorder = useThemeColor('card')
return (
@@ -347,15 +348,17 @@ export const NwcScreen: FC = observer(
maxLength={8}
placeholder='Enter daily limit in satoshi'
selectTextOnFocus={true}
- style={[$connInput, {backgroundColor: inputBg, borderBottomRightRadius: 0, borderTopRightRadius: 0}]}
+ style={[$connInput, {backgroundColor: inputBg}]}
/>
@@ -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',
diff --git a/src/screens/OwnKeysScreen.tsx b/src/screens/OwnKeysScreen.tsx
index 443f7f43..79812510 100644
--- a/src/screens/OwnKeysScreen.tsx
+++ b/src/screens/OwnKeysScreen.tsx
@@ -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'
diff --git a/src/screens/SeedRecoveryScreen.tsx b/src/screens/SeedRecoveryScreen.tsx
index 650f7aaa..6d3bc56d 100644
--- a/src/screens/SeedRecoveryScreen.tsx
+++ b/src/screens/SeedRecoveryScreen.tsx
@@ -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> = 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)
diff --git a/src/screens/SendScreen.tsx b/src/screens/SendScreen.tsx
index 4600c531..f663e2e9 100644
--- a/src/screens/SendScreen.tsx
+++ b/src/screens/SendScreen.tsx
@@ -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> = 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> = observer(
const sentEvent: NostrEvent | undefined = await NostrClient.publish(
dmEvent,
relaysToShareTo,
- )
+ )*/
setIsNostrDMSending(false)
diff --git a/src/screens/TopupScreen.tsx b/src/screens/TopupScreen.tsx
index f9741c62..d5584a4c 100644
--- a/src/screens/TopupScreen.tsx
+++ b/src/screens/TopupScreen.tsx
@@ -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> = 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> = observer(
const sentEvent: NostrEvent | undefined = await NostrClient.publish(
dmEvent,
relaysToShareTo,
- )
+ ) */
setIsNostrDMSending(false)
diff --git a/src/screens/WalletScreen.tsx b/src/screens/WalletScreen.tsx
index 87b18a8f..8fa1fa78 100644
--- a/src/screens/WalletScreen.tsx
+++ b/src/screens/WalletScreen.tsx
@@ -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 = observer(
function WalletScreen({route, navigation}) {
- const {
- relaysStore,
+ const {
mintsStore,
proofsStore,
transactionsStore,
@@ -102,13 +103,13 @@ export const WalletScreen: FC = observer(
const [defaultMintUrl, setDefaultMintUrl] = useState(MINIBITS_MINT_URL)
const [error, setError] = useState()
const [isLoading, setIsLoading] = useState(false)
- // const [lastClaimCheck, setLastClaimCheck] = useState(0)
- // const [lastPendingCheck, setLastPendingCheck] = useState(0)
const [isMintsModalVisible, setIsMintsModalVisible] = useState(false)
const [isUpdateAvailable, setIsUpdateAvailable] = useState(false)
const [isUpdateModalVisible, setIsUpdateModalVisible] = useState(false)
const [isSendModalVisible, setIsSendModalVisible] = useState(false)
const [isReceiveModalVisible, setIsReceiveModalVisible] = useState(false)
+ const [headerTitle, setHeaderTitle] = useState('')
+ const [headerStyle, setHeaderStyle] = useState<'success' | 'warning' | 'error'>('success')
const [updateDescription, setUpdateDescription] = useState('')
const [updateSize, setUpdateSize] = useState('')
const [isNativeUpdateAvailable, setIsNativeUpdateAvailable] = useState(false)
@@ -188,11 +189,50 @@ export const WalletScreen: FC = 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 = 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 = observer(
const tabWidth = moderateScale(75)
const getActiveTabColor = (state: NavigationState) => {
- /* 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 = observer(
)
}
+
+
+ const HeaderTitle = function (props: any) {
+ if (!isInternetReachable) {
+ return (
+
+ )
+ }
+
+ if (headerTitle.length > 0) {
+ const styleMap: Record = {
+ error: $error,
+ success: $success,
+ warning: $warning,
+ }
+
+ const style = styleMap[headerStyle] || undefined
+
+ return (
+
+ )
+ }
+
+ return undefined
+ }
const headerBg = useThemeColor('header')
const balances = proofsStore.getBalances()
@@ -548,7 +610,7 @@ export const WalletScreen: FC = 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 = observer(
gotoProfile={gotoProfile}
isAvatarVisible={false}
/>}
- TitleActionComponent={!isInternetReachable ? (
-
- ) : ( undefined )
- }
+ TitleActionComponent={headerTitle.length > 0 ? : undefined}
RightActionComponent={
<>
{paymentRequestsStore.countNotExpired > 0 && (
@@ -575,8 +630,8 @@ export const WalletScreen: FC = observer(
style={{flexDirection: 'row', alignItems:'center', marginRight: spacing.medium}}
onPress={() => gotoPaymentRequests()}
>
-
-
+
+
)}
>
@@ -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 (
-
-
-
- )
-}
-
-
-
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,
+}
diff --git a/src/services/keyChain.ts b/src/services/keyChain.ts
index 7fabaf90..fc228d81 100644
--- a/src/services/keyChain.ts
+++ b/src/services/keyChain.ts
@@ -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)
}
diff --git a/src/services/lightning/lightningUtils.ts b/src/services/lightning/lightningUtils.ts
index d8823cfd..f18f69cd 100644
--- a/src/services/lightning/lightningUtils.ts
+++ b/src/services/lightning/lightningUtils.ts
@@ -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
diff --git a/src/services/nostrService.ts b/src/services/nostrService.ts
index cd0526ff..95f21ca0 100644
--- a/src/services/nostrService.ts
+++ b/src/services/nostrService.ts
@@ -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 {
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,
diff --git a/src/services/walletService.ts b/src/services/walletService.ts
index 54a6dddb..962ed94a 100644
--- a/src/services/walletService.ts
+++ b/src/services/walletService.ts
@@ -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 {
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 {
- 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 {
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 {
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 {
}
}
-const _handleReceivedEventTask = async function (event: NostrEvent): Promise {
- // decrypt message content
- const decrypted = await NostrClient.decryptNip04(event.pubkey, event.content)
+const _handleReceivedEventTask = async function (wrappedEvent: NostrEvent): Promise {
+ 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)
}
diff --git a/yarn.lock b/yarn.lock
index ffb2b71c..5f3f32b3 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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