mirror of
https://github.com/minibits-cash/minibits_wallet.git
synced 2026-10-05 19:28:26 +00:00
Send ecash using NIP17 sealed and wrapped direct messages, fix generating new seed.
This commit is contained in:
+2
-1
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -346,6 +346,7 @@ Sentry id: ${userSettingsStore.userSettings.walletId}
|
||||
RightComponent={<View style={$rightContainer} />}
|
||||
style={$item}
|
||||
onPress={deletePending}
|
||||
topSeparator
|
||||
/>
|
||||
<ListItem
|
||||
tx="developerScreen.reset"
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user