From 94024b4bf75476213bdb45eb7b342590cb86e7ba Mon Sep 17 00:00:00 2001 From: Abhay Date: Tue, 17 Feb 2026 17:58:10 +0530 Subject: [PATCH 1/3] Fix Subscription restore, unfollow handler --- app/nostr/runtime/SubscriptionRegistry.ts | 4 +++- app/screens/chat/contactDetailsScreen.tsx | 20 ++++++++++++++------ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/app/nostr/runtime/SubscriptionRegistry.ts b/app/nostr/runtime/SubscriptionRegistry.ts index 2aabdd904..7313c7b65 100644 --- a/app/nostr/runtime/SubscriptionRegistry.ts +++ b/app/nostr/runtime/SubscriptionRegistry.ts @@ -6,6 +6,7 @@ type SubscriptionEntry = { filter: Filter closer: SubCloser refCount: number + onEvent: (event: Event) => void } export class SubscriptionRegistry { @@ -45,6 +46,7 @@ export class SubscriptionRegistry { filter, closer, refCount: 1, + onEvent, }) } @@ -62,7 +64,7 @@ export class SubscriptionRegistry { restore() { for (const [key, sub] of this.subs.entries()) { const closer = this.relayManager.subscribe(sub.filter, { - onevent: () => {}, + onevent: sub.onEvent, }) sub.closer = closer diff --git a/app/screens/chat/contactDetailsScreen.tsx b/app/screens/chat/contactDetailsScreen.tsx index 1fa2b3d06..785d16aba 100644 --- a/app/screens/chat/contactDetailsScreen.tsx +++ b/app/screens/chat/contactDetailsScreen.tsx @@ -17,7 +17,7 @@ import Clipboard from "@react-native-clipboard/clipboard" import { Screen } from "../../components/screen" import { useI18nContext } from "@app/i18n/i18n-react" -import { nip19, getPublicKey, Event } from "nostr-tools" +import { nip19, getPublicKey, Event, finalizeEvent } from "nostr-tools" import { bytesToHex, hexToBytes } from "@noble/hashes/utils" import { FeedItem } from "@app/components/nostr-feed/FeedItem" @@ -31,6 +31,7 @@ import { useChatContext } from "./chatContext" import { useBusinessMapMarkersQuery } from "@app/graphql/generated" import { nostrRuntime } from "@app/nostr/runtime/NostrRuntime" import { pool } from "@app/utils/nostr/pool" +import { getSecretKey } from "@app/utils/nostr" type ContactDetailsRouteProp = RouteProp @@ -94,15 +95,22 @@ const ContactDetailsScreen: React.FC = () => { } // Actions - const handleUnfollow = () => { + const handleUnfollow = async () => { if (!contactsEvent) return + const secretKey = await getSecretKey() + if (!secretKey) return const profiles = contactsEvent.tags.filter((p) => p[0] === "p").map((p) => p[1]) const tagsWithoutProfiles = contactsEvent.tags.filter((p) => p[0] !== "p") const newProfiles = profiles.filter((p) => p !== contactPubkey) - const newContactsEvent = { - ...contactsEvent, - tags: [...tagsWithoutProfiles, ...newProfiles.map((p) => ["p", p])], - } + const newContactsEvent = finalizeEvent( + { + kind: 3, + content: contactsEvent.content, + created_at: Math.floor(Date.now() / 1000), + tags: [...tagsWithoutProfiles, ...newProfiles.map((p) => ["p", p])], + }, + secretKey, + ) pool.publish(RELAYS, newContactsEvent) setContactsEvent(newContactsEvent) navigation.goBack() From 0f2ecc64c042016405d7e2199bf25402fde17fc1 Mon Sep 17 00:00:00 2001 From: Abhay Date: Tue, 17 Feb 2026 19:03:05 +0530 Subject: [PATCH 2/3] Replace private key use with signer --- .../set-lightning-address-modal.tsx | 15 +- app/hooks/use-nostr-profile.ts | 45 +++--- app/navigation/stack-param-lists.ts | 6 +- app/nostr/signer/index.ts | 39 +++++ app/nostr/signer/localSigner.ts | 46 ++++++ app/nostr/signer/types.ts | 22 +++ .../chat/GroupChat/GroupChatProvider.tsx | 14 +- app/screens/chat/NIP17Chat.tsx | 47 +++--- app/screens/chat/UserSearchBar.tsx | 36 ++--- app/screens/chat/chatContext.tsx | 45 +++--- app/screens/chat/contactDetailsScreen.tsx | 38 ++--- app/screens/chat/contacts.tsx | 31 +--- app/screens/chat/historyListItem.tsx | 14 +- app/screens/chat/messages.tsx | 15 +- app/screens/chat/searchListItem.tsx | 20 ++- .../send-bitcoin-confirmation-screen.tsx | 13 +- .../account/settings/SignInQRCode.tsx | 13 +- .../nostr-settings/advanced-settings.tsx | 33 ++--- .../nostr-settings/key-modal.tsx | 51 +++++-- .../manual-republish-button.tsx | 13 +- .../nostr-settings/nostr-settings-screen.tsx | 41 +++--- app/screens/social/iris-browser.tsx | 2 - app/screens/social/post-success.tsx | 7 +- app/screens/social/post.tsx | 23 +-- app/utils/nostr.ts | 134 +++++++----------- 25 files changed, 393 insertions(+), 370 deletions(-) create mode 100644 app/nostr/signer/index.ts create mode 100644 app/nostr/signer/localSigner.ts create mode 100644 app/nostr/signer/types.ts diff --git a/app/components/set-lightning-address-modal/set-lightning-address-modal.tsx b/app/components/set-lightning-address-modal/set-lightning-address-modal.tsx index 35f9f9d6a..08f065a80 100644 --- a/app/components/set-lightning-address-modal/set-lightning-address-modal.tsx +++ b/app/components/set-lightning-address-modal/set-lightning-address-modal.tsx @@ -16,8 +16,8 @@ import useNostrProfile from "@app/hooks/use-nostr-profile" // store import { useAppDispatch } from "@app/store/redux" import { updateUserData } from "@app/store/redux/slices/userSlice" -import { getSecretKey, setPreferredRelay } from "@app/utils/nostr" -import { getPublicKey } from "nostr-tools" +import { setPreferredRelay } from "@app/utils/nostr" +import { getSigner } from "@app/nostr/signer" gql` mutation userUpdateUsername($input: UserUpdateUsernameInput!) { @@ -58,9 +58,12 @@ export const SetLightningAddressModal = ({ useEffect(() => { async function getNostrPubkey() { - let secretKey = await getSecretKey() - if (secretKey) setNostrPubkey(getPublicKey(secretKey)) - else console.warn("NOSTR SECRET KEY NOT FOUND") + try { + const signer = await getSigner() + setNostrPubkey(await signer.getPublicKey()) + } catch { + console.warn("NOSTR SECRET KEY NOT FOUND") + } } getNostrPubkey() }, []) @@ -121,7 +124,7 @@ export const SetLightningAddressModal = ({ nip05: lnAddress, }, }) - setPreferredRelay() + getSigner().then((signer) => setPreferredRelay(signer)).catch(console.warn) if ((data?.userUpdateUsername?.errors ?? []).length > 0) { if (data?.userUpdateUsername?.errors[0]?.code === "USERNAME_ERROR") { setError(SetAddressError.ADDRESS_UNAVAILABLE) diff --git a/app/hooks/use-nostr-profile.ts b/app/hooks/use-nostr-profile.ts index d8680e0d1..92b9d677c 100644 --- a/app/hooks/use-nostr-profile.ts +++ b/app/hooks/use-nostr-profile.ts @@ -4,9 +4,9 @@ import { generateSecretKey, getPublicKey, SimplePool, - finalizeEvent, } from "nostr-tools" -import { createContactListEvent, getSecretKey, setPreferredRelay } from "@app/utils/nostr" +import { createContactListEvent, setPreferredRelay } from "@app/utils/nostr" +import { getSigner, createSignerFromKey, clearSigner } from "@app/nostr/signer" import { publishEventToRelays, verifyEventOnRelays, @@ -102,8 +102,11 @@ const useNostrProfile = () => { KEYCHAIN_NOSTRCREDS_KEY, nostrSecret, ) - await setPreferredRelay(secretKey) - await createContactListEvent(secretKey) + // Clear the signer singleton so it reloads the newly stored key + clearSigner() + const signer = createSignerFromKey(secretKey) + await setPreferredRelay(signer) + await createContactListEvent(signer) // Generate profile images automatically let pictureUrl: string | undefined @@ -189,7 +192,7 @@ const useNostrProfile = () => { created_at: Math.floor(Date.now() / 1000), } - const signedKind0Event = finalizeEvent(kind0Event, secretKey) + const signedKind0Event = await signer.signEvent(kind0Event) console.log("Profile event signed with ID:", signedKind0Event.id) // Get appropriate relays and publish @@ -245,12 +248,9 @@ const useNostrProfile = () => { try { progressCallback?.("Generating profile picture...") - const secret = await getSecretKey() - if (!secret) { - throw new Error("No Nostr profile found. Please create a Nostr profile first.") - } - - const pubKey = getPublicKey(secret) + const signer = await getSigner() + const pubKey = await signer.getPublicKey() + const nsec = signer.getSecretKeyNsec ? await signer.getSecretKeyNsec() : "" // Generate images console.log("Generating RoboHash avatar...") @@ -263,19 +263,11 @@ const useNostrProfile = () => { // Upload to nostr.build progressCallback?.("Uploading profile picture...") console.log("Uploading avatar to nostr.build...") - const pictureUrl = await uploadToNostrBuild( - avatarUri, - nip19.nsecEncode(secret), - false, - ) + const pictureUrl = await uploadToNostrBuild(avatarUri, nsec, false) progressCallback?.("Uploading banner image...") console.log("Uploading banner to nostr.build...") - const bannerUrl = await uploadToNostrBuild( - bannerUri, - nip19.nsecEncode(secret), - false, - ) + const bannerUrl = await uploadToNostrBuild(bannerUri, nsec, false) // Update profile with new images progressCallback?.("Updating profile...") @@ -383,19 +375,20 @@ const useNostrProfile = () => { const publicRelays = getPublishingRelays("profile") console.log(`📡 Will publish to ${publicRelays.length} relays`) - let secret = await getSecretKey() - if (!secret) { + let signer + try { + signer = await getSigner() + } catch { if (dataAuthed && dataAuthed.me && !dataAuthed.me.npub) { console.log("No secret key found, creating new profile with provided content...") await saveNewNostrKey(undefined, content) console.log("Profile created with images and content, returning early") - // Return early - saveNewNostrKey already published the profile with images return { successCount: 1, totalRelays: 1, successfulRelays: [] } } else { throw Error("Could not verify npub") } } - let pubKey = getPublicKey(secret) + const pubKey = await signer.getPublicKey() console.log(`🔑 Publishing with pubkey: ${pubKey}`) const kind0Event = { @@ -406,7 +399,7 @@ const useNostrProfile = () => { created_at: Math.floor(Date.now() / 1000), } - const signedKind0Event = finalizeEvent(kind0Event, secret) + const signedKind0Event = await signer.signEvent(kind0Event) console.log(`✍️ Event signed with id: ${signedKind0Event.id}`) // Use the new helper function for publishing diff --git a/app/navigation/stack-param-lists.ts b/app/navigation/stack-param-lists.ts index c6b5e5266..7373739f8 100644 --- a/app/navigation/stack-param-lists.ts +++ b/app/navigation/stack-param-lists.ts @@ -154,7 +154,7 @@ export type RootStackParamList = { CashoutSuccess: undefined EditNostrProfile: undefined NostrSettingsScreen: undefined - Contacts: { userPrivateKey: string } + Contacts: undefined SignInViaQRCode: undefined Nip29GroupChat: { groupId: string } } @@ -164,8 +164,8 @@ export type ChatStackParamList = { chatDetail: { chat: Chat; giftwraps: Event[] } sendBitcoinDestination: { username: string } transactionDetail: { txid: string } - messages: { userPrivateKey: string; groupId: string } - contactDetails: { contactPubkey: string; userPrivateKey: string } + messages: { groupId: string } + contactDetails: { contactPubkey: string } } export type ContactStackParamList = { diff --git a/app/nostr/signer/index.ts b/app/nostr/signer/index.ts new file mode 100644 index 000000000..af148a1e0 --- /dev/null +++ b/app/nostr/signer/index.ts @@ -0,0 +1,39 @@ +import { nip19 } from "nostr-tools" +import { fetchSecretFromLocalStorage } from "@app/utils/nostr" +import { LocalSigner } from "./localSigner" +import { NostrSigner } from "./types" + +let signer: NostrSigner | null = null +let initializing: Promise | null = null + +export async function getSigner(): Promise { + if (signer) return signer + + if (!initializing) { + initializing = (async () => { + const nsec = await fetchSecretFromLocalStorage() + if (!nsec) throw new Error("No signer available") + + const { data } = nip19.decode(nsec) + signer = new LocalSigner(data as Uint8Array) + return signer + })() + } + + return initializing +} + +/** + * Creates a temporary signer from a secret key. + * Used during key generation before the key is stored. + */ +export function createSignerFromKey(sk: Uint8Array): NostrSigner { + return new LocalSigner(sk) +} + +export function clearSigner() { + signer = null + initializing = null +} + +export type { NostrSigner } from "./types" diff --git a/app/nostr/signer/localSigner.ts b/app/nostr/signer/localSigner.ts new file mode 100644 index 000000000..d048e42eb --- /dev/null +++ b/app/nostr/signer/localSigner.ts @@ -0,0 +1,46 @@ +import { getPublicKey, finalizeEvent, nip04, nip44, nip19 } from "nostr-tools" +import { EventTemplate } from "nostr-tools" +import { NostrSigner } from "./types" +import { bytesToHex } from "@noble/curves/abstract/utils" + +export class LocalSigner implements NostrSigner { + constructor(private readonly sk: Uint8Array) {} + + async getPublicKey() { + return getPublicKey(this.sk) + } + + async signEvent(event: EventTemplate) { + return finalizeEvent(event, this.sk) + } + + async getSecretKeyNsec() { + return nip19.nsecEncode(this.sk) + } + + nip04 = { + encrypt: async (pubkey: string, plaintext: string) => + nip04.encrypt(this.sk, pubkey, plaintext), + + decrypt: async (pubkey: string, ciphertext: string) => + nip04.decrypt(this.sk, pubkey, ciphertext), + } + + nip44 = { + encrypt: async (pubkey: string, plaintext: string) => { + const conversationKey = nip44.v2.utils.getConversationKey( + bytesToHex(this.sk), + pubkey, + ) + return nip44.v2.encrypt(plaintext, conversationKey) + }, + + decrypt: async (pubkey: string, ciphertext: string) => { + const conversationKey = nip44.v2.utils.getConversationKey( + bytesToHex(this.sk), + pubkey, + ) + return nip44.v2.decrypt(ciphertext, conversationKey) + }, + } +} diff --git a/app/nostr/signer/types.ts b/app/nostr/signer/types.ts new file mode 100644 index 000000000..ced9bd838 --- /dev/null +++ b/app/nostr/signer/types.ts @@ -0,0 +1,22 @@ +import { Event, EventTemplate } from "nostr-tools" + +export interface NostrSigner { + getPublicKey(): Promise + signEvent(event: EventTemplate): Promise + + /** + * Returns the nsec-encoded secret key for backup/display purposes. + * Optional since remote signers (NIP-46) won't have access to the raw key. + */ + getSecretKeyNsec?(): Promise + + nip04: { + encrypt(pubkey: string, plaintext: string): Promise + decrypt(pubkey: string, ciphertext: string): Promise + } + + nip44: { + encrypt(pubkey: string, plaintext: string): Promise + decrypt(pubkey: string, ciphertext: string): Promise + } +} diff --git a/app/screens/chat/GroupChat/GroupChatProvider.tsx b/app/screens/chat/GroupChat/GroupChatProvider.tsx index af3bb0bfc..c80a4f30f 100644 --- a/app/screens/chat/GroupChat/GroupChatProvider.tsx +++ b/app/screens/chat/GroupChat/GroupChatProvider.tsx @@ -7,9 +7,9 @@ import React, { useRef, useState, } from "react" -import { Event, finalizeEvent } from "nostr-tools" +import { Event } from "nostr-tools" import { MessageType } from "@flyerhq/react-native-chat-ui" -import { getSecretKey } from "@app/utils/nostr" +import { getSigner } from "@app/nostr/signer" import { useChatContext } from "../../../screens/chat/chatContext" import { nostrRuntime } from "@app/nostr/runtime/NostrRuntime" import { pool } from "@app/utils/nostr/pool" @@ -218,8 +218,7 @@ export const NostrGroupChatProvider: React.FC = ({ async (text: string) => { if (!userPublicKey) throw Error("No user pubkey present") - const secretKey = await getSecretKey() - if (!secretKey) throw Error("Could not get Secret Key") + const signer = await getSigner() const nostrEvent = { kind: 9, @@ -229,7 +228,7 @@ export const NostrGroupChatProvider: React.FC = ({ pubkey: userPublicKey, } - const signedEvent = finalizeEvent(nostrEvent as any, secretKey) + const signedEvent = await signer.signEvent(nostrEvent as any) pool.publish(relayUrls, signedEvent) }, [userPublicKey, groupId, relayUrls], @@ -238,8 +237,7 @@ export const NostrGroupChatProvider: React.FC = ({ const requestJoin = useCallback(async () => { if (!userPublicKey) throw Error("No user pubkey present") - const secretKey = await getSecretKey() - if (!secretKey) throw Error("Could not get Secret Key") + const signer = await getSigner() const joinEvent = { kind: 9021, @@ -249,7 +247,7 @@ export const NostrGroupChatProvider: React.FC = ({ pubkey: userPublicKey, } - const signedJoinEvent = finalizeEvent(joinEvent as any, secretKey) + const signedJoinEvent = await signer.signEvent(joinEvent as any) pool.publish(relayUrls, signedJoinEvent) // Optimistic system note diff --git a/app/screens/chat/NIP17Chat.tsx b/app/screens/chat/NIP17Chat.tsx index e9b0157ba..81612997b 100644 --- a/app/screens/chat/NIP17Chat.tsx +++ b/app/screens/chat/NIP17Chat.tsx @@ -14,11 +14,10 @@ import { FlatList } from "react-native-gesture-handler" import Icon from "react-native-vector-icons/Ionicons" import { Screen } from "../../components/screen" -import { bytesToHex } from "@noble/hashes/utils" import { testProps } from "../../utils/testProps" import { useI18nContext } from "@app/i18n/i18n-react" -import { getPublicKey, nip19 } from "nostr-tools" +import { nip19 } from "nostr-tools" import { convertRumorsToGroups, fetchSecretFromLocalStorage } from "@app/utils/nostr" import { useStyles } from "./style" import { HistoryListItem } from "./historyListItem" @@ -68,7 +67,6 @@ export const NIP17Chat: React.FC = () => { const [initialized, setInitialized] = useState(false) const [searchedUsers, setSearchedUsers] = useState([]) - const [privateKey, setPrivateKey] = useState() const [showImportModal, setShowImportModal] = useState(false) const [skipMismatchCheck, setskipMismatchCheck] = useState(false) const { LL } = useI18nContext() @@ -91,18 +89,17 @@ export const NIP17Chat: React.FC = () => { return } - const secret = nip19.decode(secretKeyString).data as Uint8Array - setPrivateKey(secret) - const accountNpub = dataAuthed?.me?.npub - const storedNpub = nip19.npubEncode(getPublicKey(secret)) - if (!skipMismatchCheck && accountNpub && storedNpub !== accountNpub) { - console.log("Account Info mismatch", accountNpub, storedNpub) - setShowImportModal(true) + if (userPublicKey) { + const storedNpub = nip19.npubEncode(userPublicKey) + if (!skipMismatchCheck && accountNpub && storedNpub !== accountNpub) { + console.log("Account Info mismatch", accountNpub, storedNpub) + setShowImportModal(true) + } } if (!initialized) { - await initializeChat() // runtime handles all subscriptions + await initializeChat() setInitialized(true) } } @@ -123,11 +120,12 @@ export const NIP17Chat: React.FC = () => { setShowImportModal(true) return } - const secret = nip19.decode(secretKeyString).data as Uint8Array - const accountNpub = dataAuthed?.me?.npub - const storedNpub = nip19.npubEncode(getPublicKey(secret)) - if (!skipMismatchCheck && accountNpub && storedNpub !== accountNpub) { - setShowImportModal(true) + if (userPublicKey) { + const accountNpub = dataAuthed?.me?.npub + const storedNpub = nip19.npubEncode(userPublicKey) + if (!skipMismatchCheck && accountNpub && storedNpub !== accountNpub) { + setShowImportModal(true) + } } } @@ -139,7 +137,7 @@ export const NIP17Chat: React.FC = () => { return () => { isMounted = false } - }, [setSearchedUsers, dataAuthed, isAuthed, skipMismatchCheck, initialized]), + }, [setSearchedUsers, dataAuthed, isAuthed, skipMismatchCheck, initialized, userPublicKey]), ) // ------------------------ @@ -172,8 +170,7 @@ export const NIP17Chat: React.FC = () => { return (lastBRumor?.created_at || 0) - (lastARumor?.created_at || 0) }) - const currentUserPubKey = privateKey ? getPublicKey(privateKey) : null - const userProfile = currentUserPubKey ? profileMap?.get(currentUserPubKey) : null + const userProfile = userPublicKey ? profileMap?.get(userPublicKey) : null // ------------------------ // Render @@ -181,7 +178,7 @@ export const NIP17Chat: React.FC = () => { return ( - {privateKey && !showImportModal ? ( + {userPublicKey && !showImportModal ? ( ( @@ -220,7 +217,7 @@ export const NIP17Chat: React.FC = () => { data={searchedUsers} ListEmptyComponent={ListEmptyContent} renderItem={({ item }) => ( - + )} keyExtractor={(item) => item.id} /> @@ -232,7 +229,7 @@ export const NIP17Chat: React.FC = () => { signed in as:{" "} {userData?.username || - nip19.npubEncode(getPublicKey(privateKey))} + nip19.npubEncode(userPublicKey)} @@ -305,7 +302,6 @@ export const NIP17Chat: React.FC = () => { renderItem={({ item }) => ( )} @@ -321,15 +317,14 @@ export const NIP17Chat: React.FC = () => { name={`Profile: ${userProfile?.name}`} component={ContactDetailsScreen} initialParams={{ - contactPubkey: getPublicKey(privateKey), - userPrivateKey: bytesToHex(privateKey), + contactPubkey: userPublicKey, }} /> {() => ( - + )} diff --git a/app/screens/chat/UserSearchBar.tsx b/app/screens/chat/UserSearchBar.tsx index e2df99c7c..44f9e5641 100644 --- a/app/screens/chat/UserSearchBar.tsx +++ b/app/screens/chat/UserSearchBar.tsx @@ -1,16 +1,14 @@ import { useI18nContext } from "@app/i18n/i18n-react" import { SearchBar } from "@rneui/themed" -import { Event, getPublicKey, nip05, nip19, SubCloser } from "nostr-tools" -import { useCallback, useEffect, useState } from "react" +import { Event, nip05, nip19, SubCloser } from "nostr-tools" +import { useCallback, useState } from "react" import { useChatContext } from "./chatContext" import { fetchNostrUsers, - fetchSecretFromLocalStorage, getGroupId, } from "@app/utils/nostr" -import { hexToBytes } from "@noble/curves/abstract/utils" +import { pool } from "@app/utils/nostr/pool" import { useStyles } from "./style" -import { Alert } from "react-native" import { useAppConfig } from "@app/hooks" import { testProps } from "@app/utils/testProps" import Icon from "react-native-vector-icons/Ionicons" @@ -21,9 +19,8 @@ interface UserSearchBarProps { export const UserSearchBar: React.FC = ({ setSearchedUsers }) => { const [searchText, setSearchText] = useState("") - const { rumors, poolRef, addEventToProfiles, profileMap } = useChatContext() + const { rumors, addEventToProfiles, profileMap, userPublicKey } = useChatContext() const [refreshing, setRefreshing] = useState(false) - const [privateKey, setPrivateKey] = useState(null) const styles = useStyles() const { appConfig } = useAppConfig() @@ -33,21 +30,12 @@ export const UserSearchBar: React.FC = ({ setSearchedUsers } setRefreshing(false) }, []) - useEffect(() => { - const initialize = async () => { - let secretKeyString = await fetchSecretFromLocalStorage() - if (secretKeyString) setPrivateKey(nip19.decode(secretKeyString).data as Uint8Array) - } - initialize() - }, []) - const { LL } = useI18nContext() const searchedUsersHandler = (event: Event, closer: SubCloser) => { let nostrProfile = JSON.parse(event.content) addEventToProfiles(event) - let userPubkey = getPublicKey(privateKey!) - let participants = [event.pubkey, userPubkey] + let participants = [event.pubkey, userPublicKey!].filter(Boolean) setSearchedUsers([ { ...nostrProfile, id: event.pubkey, groupId: getGroupId(participants) }, ]) @@ -62,8 +50,7 @@ export const UserSearchBar: React.FC = ({ setSearchedUsers } console.log("nostr user for", alias, nostrUser) if (nostrUser) { let nostrProfile = profileMap?.get(nostrUser.pubkey) - let userPubkey = getPublicKey(privateKey!) - let participants = [nostrUser.pubkey, userPubkey] + let participants = [nostrUser.pubkey, userPublicKey!].filter(Boolean) setSearchedUsers([ { id: nostrUser.pubkey, @@ -73,7 +60,7 @@ export const UserSearchBar: React.FC = ({ setSearchedUsers } }, ]) if (!nostrProfile) - fetchNostrUsers([nostrUser.pubkey], poolRef!.current, searchedUsersHandler) + fetchNostrUsers([nostrUser.pubkey], pool, searchedUsersHandler) return true } return false @@ -86,10 +73,9 @@ export const UserSearchBar: React.FC = ({ setSearchedUsers } setSearchText(newSearchText) if (newSearchText.startsWith("npub1") && newSearchText.length == 63) { let hexPubkey = nip19.decode(newSearchText).data as string - let userPubkey = getPublicKey(privateKey!) - let participants = [hexPubkey, userPubkey] + let participants = [hexPubkey, userPublicKey!].filter(Boolean) setSearchedUsers([{ id: hexPubkey, groupId: getGroupId(participants) }]) - fetchNostrUsers([hexPubkey], poolRef!.current, searchedUsersHandler) + fetchNostrUsers([hexPubkey], pool, searchedUsersHandler) setRefreshing(false) return } else if (newSearchText.match(aliasPattern)) { @@ -107,10 +93,10 @@ export const UserSearchBar: React.FC = ({ setSearchedUsers } } } }, - [privateKey], + [userPublicKey], ) - return privateKey ? ( + return userPublicKey ? ( = ({ children }) = // ------------------------ // Helper: handle new giftwrap event // ------------------------ - const handleGiftWrapEvent = async (event: Event, secret: Uint8Array) => { + const handleGiftWrapEvent = async (event: Event) => { if (processedEventIds.current.has(event.id)) return processedEventIds.current.add(event.id) @@ -77,7 +78,8 @@ export const ChatContextProvider: React.FC = ({ children }) = }) try { - const rumor = getRumorFromWrap(event, secret) + const signer = await getSigner() + const rumor = await getRumorFromWrap(event, signer) setRumors((prev) => { if (!prev.map((r) => r.id).includes(rumor.id)) { return [...prev, rumor] @@ -90,7 +92,7 @@ export const ChatContextProvider: React.FC = ({ children }) = } // ------------------------ - // Initialize chat (fetch secret, set pubkey) + // Initialize chat (fetch signer, set pubkey) // ------------------------ const initializeChat = async (count = 0) => { const secretKeyString = await fetchSecretFromLocalStorage() @@ -100,43 +102,50 @@ export const ChatContextProvider: React.FC = ({ children }) = return } - const secret = nip19.decode(secretKeyString).data as Uint8Array - const publicKey = getPublicKey(secret) + let signer + try { + signer = await getSigner() + } catch { + if (count >= 3) return + setTimeout(() => initializeChat(count + 1), 500) + return + } + + const publicKey = await signer.getPublicKey() setUserPublicKey(publicKey) // Load cached giftwraps const cachedGiftwraps = await loadGiftwrapsFromStorage() setGiftWraps(cachedGiftwraps) - setRumors( - cachedGiftwraps - .map((wrap) => { + const decryptedRumors = ( + await Promise.all( + cachedGiftwraps.map(async (wrap) => { try { - return getRumorFromWrap(wrap, secret) + return await getRumorFromWrap(wrap, signer) } catch { return null } - }) - .filter((r) => r !== null) as Rumor[], - ) + }), + ) + ).filter((r): r is Rumor => r !== null) + setRumors(decryptedRumors) // ------------------------ // Subscribe to giftwraps via NostrRuntime // ------------------------ nostrRuntime.ensureSubscription( `giftwraps:${publicKey}`, - { "kinds": [1059], "#p": [publicKey], "limit": 150, }, - (event) => handleGiftWrapEvent(event, secret), + (event) => handleGiftWrapEvent(event), ) // Subscribe to contact list nostrRuntime.ensureSubscription( `contacts:${publicKey}`, - { kinds: [3], authors: [publicKey], @@ -149,7 +158,6 @@ export const ChatContextProvider: React.FC = ({ children }) = // Subscribe to user profile nostrRuntime.ensureSubscription( `profile:${publicKey}`, - { kinds: [0], authors: [publicKey], @@ -190,9 +198,10 @@ export const ChatContextProvider: React.FC = ({ children }) = } // ------------------------ - // Reset chat (clear events & resubscribe) + // Reset chat (clear signer, events & resubscribe) // ------------------------ const resetChat = async () => { + clearSigner() setGiftWraps([]) setRumors([]) setUserProfileEvent(null) diff --git a/app/screens/chat/contactDetailsScreen.tsx b/app/screens/chat/contactDetailsScreen.tsx index 785d16aba..8167440ea 100644 --- a/app/screens/chat/contactDetailsScreen.tsx +++ b/app/screens/chat/contactDetailsScreen.tsx @@ -17,8 +17,7 @@ import Clipboard from "@react-native-clipboard/clipboard" import { Screen } from "../../components/screen" import { useI18nContext } from "@app/i18n/i18n-react" -import { nip19, getPublicKey, Event, finalizeEvent } from "nostr-tools" -import { bytesToHex, hexToBytes } from "@noble/hashes/utils" +import { nip19, Event } from "nostr-tools" import { FeedItem } from "@app/components/nostr-feed/FeedItem" import { ExplainerVideo } from "@app/components/explainer-video" @@ -31,7 +30,7 @@ import { useChatContext } from "./chatContext" import { useBusinessMapMarkersQuery } from "@app/graphql/generated" import { nostrRuntime } from "@app/nostr/runtime/NostrRuntime" import { pool } from "@app/utils/nostr/pool" -import { getSecretKey } from "@app/utils/nostr" +import { getSigner } from "@app/nostr/signer" type ContactDetailsRouteProp = RouteProp @@ -51,8 +50,8 @@ const ContactDetailsScreen: React.FC = () => { const styles = useStyles() const { LL } = useI18nContext() - const { profileMap, contactsEvent, setContactsEvent } = useChatContext() - const { contactPubkey, userPrivateKey } = route.params + const { profileMap, contactsEvent, setContactsEvent, userPublicKey } = useChatContext() + const { contactPubkey } = route.params const profile = profileMap?.get(contactPubkey) const npub = nip19.npubEncode(contactPubkey) const postsKey = `posts:${contactPubkey}` @@ -79,13 +78,8 @@ const ContactDetailsScreen: React.FC = () => { ? businessUsernames.includes(profile.username) : false - const userPrivateKeyHex = - typeof userPrivateKey === "string" ? userPrivateKey : bytesToHex(userPrivateKey) - const selfPubkey = userPrivateKey ? getPublicKey(hexToBytes(userPrivateKeyHex)) : null - const isOwnProfile = selfPubkey === contactPubkey - const userPubkey = getPublicKey( - typeof userPrivateKey === "string" ? hexToBytes(userPrivateKey) : userPrivateKey, - ) + const isOwnProfile = userPublicKey === contactPubkey + const userPubkey = userPublicKey || "" const groupId = [userPubkey, contactPubkey].sort().join(",") // Copy npub @@ -97,20 +91,16 @@ const ContactDetailsScreen: React.FC = () => { // Actions const handleUnfollow = async () => { if (!contactsEvent) return - const secretKey = await getSecretKey() - if (!secretKey) return + const signer = await getSigner() const profiles = contactsEvent.tags.filter((p) => p[0] === "p").map((p) => p[1]) const tagsWithoutProfiles = contactsEvent.tags.filter((p) => p[0] !== "p") const newProfiles = profiles.filter((p) => p !== contactPubkey) - const newContactsEvent = finalizeEvent( - { - kind: 3, - content: contactsEvent.content, - created_at: Math.floor(Date.now() / 1000), - tags: [...tagsWithoutProfiles, ...newProfiles.map((p) => ["p", p])], - }, - secretKey, - ) + const newContactsEvent = await signer.signEvent({ + kind: 3, + content: contactsEvent.content, + created_at: Math.floor(Date.now() / 1000), + tags: [...tagsWithoutProfiles, ...newProfiles.map((p) => ["p", p])], + }) pool.publish(RELAYS, newContactsEvent) setContactsEvent(newContactsEvent) navigation.goBack() @@ -122,7 +112,7 @@ const ContactDetailsScreen: React.FC = () => { } const handleStartChat = () => { - navigation.replace("messages", { groupId, userPrivateKey: userPrivateKeyHex }) + navigation.replace("messages", { groupId }) } // Re-sync posts from runtime store when screen gains focus (e.g. after publishing a new post) diff --git a/app/screens/chat/contacts.tsx b/app/screens/chat/contacts.tsx index f635ecf24..be149d2b4 100644 --- a/app/screens/chat/contacts.tsx +++ b/app/screens/chat/contacts.tsx @@ -3,36 +3,26 @@ import { FlatList, Text, View, ActivityIndicator } from "react-native" import { useStyles } from "./style" import { useChatContext } from "./chatContext" import { Event } from "nostr-tools" -import { useNavigation, useRoute, RouteProp } from "@react-navigation/native" +import { useNavigation } from "@react-navigation/native" import { useTheme } from "@rneui/themed" import { StackNavigationProp } from "@react-navigation/stack" -import { ChatStackParamList, RootStackParamList } from "@app/navigation/stack-param-lists" +import { ChatStackParamList } from "@app/navigation/stack-param-lists" import { UserSearchBar } from "./UserSearchBar" import { SearchListItem } from "./searchListItem" -import { hexToBytes } from "@noble/curves/abstract/utils" import { getContactsFromEvent } from "./utils" import ContactCard from "./contactCard" import { useI18nContext } from "@app/i18n/i18n-react" import { nostrRuntime } from "@app/nostr/runtime/NostrRuntime" -// Route params type -type ContactsRouteProp = RouteProp - -interface ContactsProps { - userPrivateKey?: string -} - -const Contacts: React.FC = ({ userPrivateKey: propKey }) => { +const Contacts: React.FC = () => { const baseStyles = useStyles() const [searchedUsers, setSearchedUsers] = useState([]) const { profileMap, contactsEvent, addEventToProfiles, userPublicKey } = useChatContext() const navigation = useNavigation>() - const route = useRoute() const { theme } = useTheme() const colors = theme.colors const { LL } = useI18nContext() - const realUserKey = propKey || route.params?.userPrivateKey const [showAltMessage, setShowAltMessage] = useState(false) // Show alternative message if loading takes too long @@ -57,15 +47,6 @@ const Contacts: React.FC = ({ userPrivateKey: propKey }) => { ) }, [contactsEvent]) - // Safety check: if we still don't have a key - if (!realUserKey) { - return ( - - - - ) - } - const styles = { ...baseStyles, container: { flex: 1 }, @@ -89,7 +70,6 @@ const Contacts: React.FC = ({ userPrivateKey: propKey }) => { const navigateToContactDetails = (contactPubkey: string) => { navigation.navigate("contactDetails", { contactPubkey, - userPrivateKey: realUserKey, }) } @@ -103,10 +83,7 @@ const Contacts: React.FC = ({ userPrivateKey: propKey }) => { data={searchedUsers} ListEmptyComponent={ListEmptyContent} renderItem={({ item }) => ( - + )} keyExtractor={(item) => item.id} /> diff --git a/app/screens/chat/historyListItem.tsx b/app/screens/chat/historyListItem.tsx index e0f875a09..edecb1b68 100644 --- a/app/screens/chat/historyListItem.tsx +++ b/app/screens/chat/historyListItem.tsx @@ -1,7 +1,7 @@ import { ListItem } from "@rneui/themed" import { useStyles } from "./style" import { Image, Text, View } from "react-native" -import { nip19, Event, getPublicKey } from "nostr-tools" +import { nip19, Event } from "nostr-tools" import { useFocusEffect, useNavigation } from "@react-navigation/native" import { StackNavigationProp } from "@react-navigation/stack" import { ChatStackParamList } from "@app/navigation/stack-param-lists" @@ -9,26 +9,23 @@ import { useEffect, useState } from "react" import { useChatContext } from "./chatContext" import { Rumor } from "@app/utils/nostr" import { getLastSeen } from "./utils" -import { bytesToHex } from "@noble/hashes/utils" import Icon from "react-native-vector-icons/Ionicons" import { nostrRuntime } from "@app/nostr/runtime/NostrRuntime" interface HistoryListItemProps { item: string - userPrivateKey: Uint8Array groups: Map } export const HistoryListItem: React.FC = ({ item, - userPrivateKey, groups, }) => { - const { profileMap, addEventToProfiles } = useChatContext() + const { profileMap, addEventToProfiles, userPublicKey } = useChatContext() const [hasUnread, setHasUnread] = useState(false) const [subscribedPubkeys, setSubscribedPubkeys] = useState>(new Set()) - const userPublicKey = userPrivateKey ? getPublicKey(userPrivateKey) : "" + const userPublicKeyVal = userPublicKey || "" const selfNote = item.split(",").length === 1 const navigation = useNavigation>() @@ -79,14 +76,13 @@ export const HistoryListItem: React.FC = ({ onPress={() => navigation.navigate("messages", { groupId: item, - userPrivateKey: bytesToHex(userPrivateKey), }) } > {/* Profile Images */} {item .split(",") - .filter((p) => p !== userPublicKey) + .filter((p) => p !== userPublicKeyVal) .map((p) => ( = ({ {item .split(",") - .filter((p) => p !== userPublicKey) + .filter((p) => p !== userPublicKeyVal) .map((pubkey) => { const profile = profileMap?.get(pubkey) return ( diff --git a/app/screens/chat/messages.tsx b/app/screens/chat/messages.tsx index 32485db65..77a1a0be7 100644 --- a/app/screens/chat/messages.tsx +++ b/app/screens/chat/messages.tsx @@ -15,24 +15,22 @@ import { isIos } from "@app/utils/helper" import { Chat, MessageType, defaultTheme } from "@flyerhq/react-native-chat-ui" import { ChatMessage } from "./chatMessage" import Icon from "react-native-vector-icons/Ionicons" -import { getPublicKey, nip19, Event } from "nostr-tools" +import { nip19, Event } from "nostr-tools" import { Rumor, convertRumorsToGroups, sendNip17Message } from "@app/utils/nostr" import { useEffect, useState } from "react" import { useChatContext } from "./chatContext" import { SafeAreaProvider } from "react-native-safe-area-context" import { updateLastSeen } from "./utils" -import { hexToBytes } from "@noble/hashes/utils" import { nostrRuntime } from "@app/nostr/runtime/NostrRuntime" -import { pool } from "@app/utils/nostr/pool" +import { getSigner } from "@app/nostr/signer" type MessagesProps = { route: RouteProp } export const Messages: React.FC = ({ route }) => { - const userPrivateKeyBytes = hexToBytes(route.params.userPrivateKey) - const userPubkey = getPublicKey(userPrivateKeyBytes) const groupId = route.params.groupId + const { userPublicKey } = useChatContext() const [profileMap, setProfileMap] = useState>(new Map()) const [preferredRelaysMap, setPreferredRelaysMap] = useState>( new Map(), @@ -60,11 +58,10 @@ export const Messages: React.FC = ({ route }) => { return ( ) } @@ -72,14 +69,12 @@ export const Messages: React.FC = ({ route }) => { type MessagesScreenProps = { groupId: string userPubkey: string - userPrivateKey: Uint8Array profileMap: Map preferredRelaysMap: Map } export const MessagesScreen: React.FC = ({ userPubkey, - userPrivateKey, groupId, profileMap, preferredRelaysMap, @@ -136,10 +131,12 @@ export const MessagesScreen: React.FC = ({ } } + const signer = await getSigner() const result = await sendNip17Message( groupId.split(","), message.text, preferredRelaysMap, + signer, onSent, ) diff --git a/app/screens/chat/searchListItem.tsx b/app/screens/chat/searchListItem.tsx index 52fc4ee55..822bcc80d 100644 --- a/app/screens/chat/searchListItem.tsx +++ b/app/screens/chat/searchListItem.tsx @@ -4,8 +4,7 @@ import { Image, TouchableOpacity } from "react-native" import { useNavigation } from "@react-navigation/native" import { StackNavigationProp } from "@react-navigation/stack" import { ChatStackParamList } from "@app/navigation/stack-param-lists" -import { getPublicKey, nip19 } from "nostr-tools" -import { bytesToHex } from "@noble/hashes/utils" +import { nip19 } from "nostr-tools" import { useChatContext } from "./chatContext" import { addToContactList } from "@app/utils/nostr" import Icon from "react-native-vector-icons/Ionicons" @@ -13,16 +12,13 @@ import { getContactsFromEvent } from "./utils" import { useState } from "react" import { ActivityIndicator } from "react-native" import { pool } from "@app/utils/nostr/pool" +import { getSigner } from "@app/nostr/signer" interface SearchListItemProps { item: Chat - userPrivateKey: Uint8Array } -export const SearchListItem: React.FC = ({ - item, - userPrivateKey, -}) => { - const { contactsEvent } = useChatContext() +export const SearchListItem: React.FC = ({ item }) => { + const { contactsEvent, userPublicKey } = useChatContext() const [isLoading, setIsLoading] = useState(false) const isUserAdded = () => { @@ -38,9 +34,9 @@ export const SearchListItem: React.FC = ({ const navigation = useNavigation>() const getIcon = () => { - let itemPubkey = item.groupId + const itemPubkey = item.groupId .split(",") - .filter((p) => p !== getPublicKey(userPrivateKey))[0] + .filter((p) => p !== userPublicKey)[0] if (contactsEvent) return getContactsFromEvent(contactsEvent).filter((c) => c.pubkey! === itemPubkey) .length === 0 @@ -53,8 +49,9 @@ export const SearchListItem: React.FC = ({ if (isUserAdded()) return try { setIsLoading(true) + const signer = await getSigner() await addToContactList( - userPrivateKey, + signer, item.id, pool, () => Promise.resolve(true), @@ -75,7 +72,6 @@ export const SearchListItem: React.FC = ({ onPress={() => { navigation.navigate("messages", { groupId: item.groupId, - userPrivateKey: bytesToHex(userPrivateKey), }) }} > diff --git a/app/screens/send-bitcoin-screen/send-bitcoin-confirmation-screen.tsx b/app/screens/send-bitcoin-screen/send-bitcoin-confirmation-screen.tsx index 36954c48a..5772ba42d 100644 --- a/app/screens/send-bitcoin-screen/send-bitcoin-confirmation-screen.tsx +++ b/app/screens/send-bitcoin-screen/send-bitcoin-confirmation-screen.tsx @@ -44,7 +44,8 @@ import { RootStackParamList } from "@app/navigation/stack-param-lists" import { logPaymentAttempt, logPaymentResult } from "@app/utils/analytics" import { getUsdWallet } from "@app/graphql/wallets-utils" import { useChatContext } from "../chat/chatContext" -import { addToContactList, getSecretKey } from "@app/utils/nostr" +import { addToContactList } from "@app/utils/nostr" +import { getSigner } from "@app/nostr/signer" import { nip19 } from "nostr-tools" import { useRequireContactList } from "./require-contact-list-modal" @@ -171,11 +172,15 @@ const SendBitcoinConfirmationScreen: React.FC = ({ route, navigation }) = const destinationNpub = queryResult.data?.npubByUsername?.npub if (!destinationNpub) return - const secretKey = await getSecretKey() - if (!secretKey) return + let signer + try { + signer = await getSigner() + } catch { + return + } await addToContactList( - secretKey, + signer, nip19.decode(destinationNpub).data as string, poolRef.current, promptForContactList, diff --git a/app/screens/settings-screen/account/settings/SignInQRCode.tsx b/app/screens/settings-screen/account/settings/SignInQRCode.tsx index 952bb1382..8c0b3a32d 100644 --- a/app/screens/settings-screen/account/settings/SignInQRCode.tsx +++ b/app/screens/settings-screen/account/settings/SignInQRCode.tsx @@ -3,7 +3,6 @@ import { StackNavigationProp } from "@react-navigation/stack" import { Alert, useWindowDimensions, View } from "react-native" import { RootStackParamList } from "@app/navigation/stack-param-lists" import { useTheme, Text, makeStyles } from "@rneui/themed" -import { bytesToHex } from "@noble/curves/abstract/utils" import * as Keychain from "react-native-keychain" import QRCode from "react-native-qrcode-svg" import { base64encode } from "byte-base64" @@ -22,7 +21,7 @@ import { useFocusEffect, useNavigation } from "@react-navigation/native" import { KEYCHAIN_MNEMONIC_KEY } from "@app/utils/breez-sdk-liquid" import KeyStoreWrapper from "@app/utils/storage/secureStorage" import { PinScreenPurpose } from "@app/utils/enum" -import { getSecretKey } from "@app/utils/nostr" +import { getSigner } from "@app/nostr/signer" // assets import Logo from "@app/assets/logo/blink-logo-icon.png" @@ -60,9 +59,13 @@ export const SignInQRCode = () => { obj.mnemonicKey = mnemonicKey.password } - const secret = await getSecretKey() - if (secret) { - obj.nsec = bytesToHex(secret) + try { + const signer = await getSigner() + if (signer.getSecretKeyNsec) { + obj.nsec = await signer.getSecretKeyNsec() + } + } catch { + // no nostr key } setQRCodeValue(base64encode(JSON.stringify(obj))) diff --git a/app/screens/settings-screen/nostr-settings/advanced-settings.tsx b/app/screens/settings-screen/nostr-settings/advanced-settings.tsx index 8709d5616..67acd1e77 100644 --- a/app/screens/settings-screen/nostr-settings/advanced-settings.tsx +++ b/app/screens/settings-screen/nostr-settings/advanced-settings.tsx @@ -3,8 +3,7 @@ import { Text, useTheme } from "@rneui/themed" import { useStyles } from "./styles" import Ionicons from "react-native-vector-icons/Ionicons" import { useState } from "react" -import { hexToBytes } from "@noble/curves/abstract/utils" -import { getPublicKey, nip19 } from "nostr-tools" +import { nip19 } from "nostr-tools" import { useUserUpdateNpubMutation } from "@app/graphql/generated" import useNostrProfile from "@app/hooks/use-nostr-profile" import { ImportNsecModal } from "@app/components/import-nsec/import-nsec-modal" @@ -15,10 +14,10 @@ import { StackNavigationProp } from "@react-navigation/stack" import { RootStackParamList } from "@app/navigation/stack-param-lists" import { useI18nContext } from "@app/i18n/i18n-react" import { createContactListEvent } from "@app/utils/nostr" +import { getSigner } from "@app/nostr/signer" interface AdvancedSettingsProps { expandAdvanced: boolean - secretKeyHex: string copyToClipboard: (text: string, onSuccess?: (copied: boolean) => void) => void onReconnect: () => Promise accountLinked: boolean | null @@ -26,7 +25,6 @@ interface AdvancedSettingsProps { export const AdvancedSettings: React.FC = ({ expandAdvanced, - secretKeyHex, copyToClipboard, onReconnect, accountLinked, @@ -37,8 +35,7 @@ export const AdvancedSettings: React.FC = ({ } = useTheme() const { LL } = useI18nContext() - // Grab contactsEvent and pool from context - const { resetChat, refreshUserProfile, contactsEvent, poolRef } = useChatContext() + const { resetChat, refreshUserProfile, contactsEvent } = useChatContext() const [showSecretModal, setShowSecretModal] = useState(false) const [keysModalType, setKeysModalType] = useState<"public" | "private">("public") @@ -57,16 +54,19 @@ export const AdvancedSettings: React.FC = ({ } const handleReconnectNostr = async () => { - if (!secretKeyHex) { + let signer + try { + signer = await getSigner() + } catch { Alert.alert(LL.Nostr.noProfileIdExists()) return } - const secretKey = hexToBytes(secretKeyHex) setUpdatingNpub(true) - const data = await userUpdateNpub({ + const pubKey = await signer.getPublicKey() + await userUpdateNpub({ variables: { input: { - npub: nip19.npubEncode(getPublicKey(secretKey)), + npub: nip19.npubEncode(pubKey), }, }, }) @@ -106,10 +106,9 @@ export const AdvancedSettings: React.FC = ({ onPress: async () => { try { setCreatingContacts(true) - // Call your utility function console.log("Creating contact list") - await createContactListEvent(hexToBytes(secretKeyHex)) - // Refresh the context to see the new list immediately + const signer = await getSigner() + await createContactListEvent(signer) Alert.alert(LL.common.success(), "Contact list created successfully.") await refreshUserProfile() setCreatingContacts(false) @@ -125,13 +124,8 @@ export const AdvancedSettings: React.FC = ({ ) } - // --- NEW: Create Contact List Logic --- - const handleViewContacts = () => { - // Navigate to the existing Contacts screen - // Ensure "Contacts" is in your RootStackParamList - // Pass userPrivateKey as string if the screen requires it - navigation.navigate("Contacts", { userPrivateKey: secretKeyHex }) + navigation.navigate("Contacts") } const contactSectionText = contactsEvent ? LL.Nostr.Contacts.manageContacts() @@ -264,7 +258,6 @@ export const AdvancedSettings: React.FC = ({ /> setShowSecretModal(false)} copyToClipboard={copyToClipboard} keysModalType={keysModalType} diff --git a/app/screens/settings-screen/nostr-settings/key-modal.tsx b/app/screens/settings-screen/nostr-settings/key-modal.tsx index 528d20744..22fe6da24 100644 --- a/app/screens/settings-screen/nostr-settings/key-modal.tsx +++ b/app/screens/settings-screen/nostr-settings/key-modal.tsx @@ -1,24 +1,22 @@ -import { hexToBytes } from "@noble/curves/abstract/utils" import { useStyles } from "./styles" import { getPublicKey, nip19 } from "nostr-tools" -import { useState } from "react" +import { useEffect, useState } from "react" import ReactNativeModal from "react-native-modal" import { Alert, TouchableOpacity, View } from "react-native" import { useTheme, Text } from "@rneui/themed" import Ionicons from "react-native-vector-icons/Ionicons" import { PrimaryBtn } from "@app/components/buttons" -import { useI18nContext } from "@app/i18n/i18n-react" // <-- import i18n +import { useI18nContext } from "@app/i18n/i18n-react" +import { getSigner } from "@app/nostr/signer" interface KeyModalProps { isOpen: boolean - secretKeyHex: string keysModalType: string onClose: () => void copyToClipboard: (text: string, onSuccess?: (copied: boolean) => void) => void } export const KeyModal: React.FC = ({ isOpen, - secretKeyHex, keysModalType, onClose, copyToClipboard, @@ -26,25 +24,48 @@ export const KeyModal: React.FC = ({ const styles = useStyles() const { mode } = useTheme().theme const [hideSecret, setHideSecret] = useState(true) - const secretKey = hexToBytes(secretKeyHex) - const nostrPubKey = nip19.npubEncode(getPublicKey(secretKey)) + const [nostrPubKey, setNostrPubKey] = useState(null) + const [nsec, setNsec] = useState(null) + const isPublic = keysModalType === "public" - const keyValue = isPublic - ? nostrPubKey - : hideSecret - ? "***************" - : nip19.nsecEncode(secretKey) const { theme: { colors }, } = useTheme() - const { LL } = useI18nContext() // <-- use translations + const { LL } = useI18nContext() + + useEffect(() => { + if (!isOpen) return + const load = async () => { + try { + const signer = await getSigner() + const pubKey = await signer.getPublicKey() + setNostrPubKey(nip19.npubEncode(pubKey)) + if (!isPublic && signer.getSecretKeyNsec) { + setNsec(await signer.getSecretKeyNsec()) + } + } catch { + setNostrPubKey(null) + setNsec(null) + } + } + load() + }, [isOpen, isPublic]) + + const keyValue = isPublic + ? (nostrPubKey ?? "") + : hideSecret + ? "***************" + : (nsec ?? "") - const onCopy = () => - copyToClipboard(isPublic ? nostrPubKey : nip19.nsecEncode(secretKey), () => + const onCopy = () => { + const valueToCopy = isPublic ? nostrPubKey : nsec + if (!valueToCopy) return + copyToClipboard(valueToCopy, () => Alert.alert(LL.Nostr.common.copied(), LL.Nostr.KeyModal.keyCopiedToClipboard()), ) + } return ( = ({ setIsPublishing(true) try { - const secretKey = await getSecretKey() - if (!secretKey) { + let signer + try { + signer = await getSigner() + } catch { Alert.alert("Error", "No Nostr key found. Please create a profile first.") return } - const pubKey = getPublicKey(secretKey) + const pubKey = await signer.getPublicKey() const lud16 = `${username}@${lnDomain}` const nip05 = `${username}@${lnDomain}` @@ -68,7 +69,7 @@ export const ManualRepublishButton: React.FC = ({ created_at: Math.floor(Date.now() / 1000), } - const signedEvent = finalizeEvent(kind0Event, secretKey) + const signedEvent = await signer.signEvent(kind0Event) console.log("Event signed with ID:", signedEvent.id) // Get relays and publish diff --git a/app/screens/settings-screen/nostr-settings/nostr-settings-screen.tsx b/app/screens/settings-screen/nostr-settings/nostr-settings-screen.tsx index f9516146f..8db77fc4a 100644 --- a/app/screens/settings-screen/nostr-settings/nostr-settings-screen.tsx +++ b/app/screens/settings-screen/nostr-settings/nostr-settings-screen.tsx @@ -1,9 +1,9 @@ import { View, Pressable } from "react-native" import { Switch, Text, useTheme } from "@rneui/themed" import { useEffect, useState } from "react" -import { getPublicKey, nip19 } from "nostr-tools" +import { nip19 } from "nostr-tools" import Ionicons from "react-native-vector-icons/Ionicons" -import { getSecretKey } from "@app/utils/nostr" +import { getSigner } from "@app/nostr/signer" import useNostrProfile from "@app/hooks/use-nostr-profile" import { useNavigation } from "@react-navigation/native" import { useHomeAuthedQuery } from "@app/graphql/generated" @@ -15,14 +15,13 @@ import { useI18nContext } from "@app/i18n/i18n-react" import { useStyles } from "./styles" import { ProfileHeader } from "./profile-header" import { AdvancedSettings } from "./advanced-settings" -import { bytesToHex } from "@noble/curves/abstract/utils" import { usePersistentStateContext } from "@app/store/persistent-state" import { useAppConfig } from "@app/hooks/use-app-config" import { ManualRepublishButton } from "./manual-republish-button" export const NostrSettingsScreen = () => { const { LL } = useI18nContext() - const [secretKey, setSecretKey] = useState(null) + const [nostrPubKey, setNostrPubKey] = useState(null) const [linked, setLinked] = useState(null) const [expandAdvanced, setExpandAdvanced] = useState(false) @@ -50,28 +49,26 @@ export const NostrSettingsScreen = () => { useEffect(() => { const initialize = async () => { - let secret - if (!secretKey) { - secret = await getSecretKey() - setSecretKey(secret) - } else { - secret = secretKey - } - if (secret && dataAuthed?.me?.npub === nip19.npubEncode(getPublicKey(secret))) { - setLinked(true) - } else { + try { + const signer = await getSigner() + const pubKey = await signer.getPublicKey() + const npub = nip19.npubEncode(pubKey) + setNostrPubKey(npub) + if (dataAuthed?.me?.npub === npub) { + setLinked(true) + } else { + setLinked(false) + } + } catch { + setNostrPubKey(null) setLinked(false) } } initialize() - }, [secretKey, dataAuthed]) + }, [dataAuthed]) const { saveNewNostrKey } = useNostrProfile() const { refreshUserProfile, resetChat } = useChatContext() - let nostrPubKey = "" - if (secretKey) { - nostrPubKey = nip19.npubEncode(getPublicKey(secretKey as Uint8Array)) - } const { theme: { colors }, @@ -95,7 +92,7 @@ export const NostrSettingsScreen = () => { } const renderEmptyContent = () => { - if (!secretKey) { + if (!nostrPubKey) { return ( { if (isGenerating) return setIsGenerating(true) setProgressMessage("Creating Nostr profile...") - let newSecret = await saveNewNostrKey( + await saveNewNostrKey( (message) => { setProgressMessage(message) }, @@ -147,7 +144,6 @@ export const NostrSettingsScreen = () => { nip05: `${dataAuthed?.me?.username}@${lnDomain}`, }, ) - setSecretKey(newSecret) setIsGenerating(false) setProgressMessage("") await resetChat() @@ -212,7 +208,6 @@ export const NostrSettingsScreen = () => { { await refetch() }} diff --git a/app/screens/social/iris-browser.tsx b/app/screens/social/iris-browser.tsx index 68e3af367..0a4255c4b 100644 --- a/app/screens/social/iris-browser.tsx +++ b/app/screens/social/iris-browser.tsx @@ -5,8 +5,6 @@ import { makeStyles, useTheme, Icon } from "@rneui/themed" import { StackNavigationProp } from "@react-navigation/stack" import { RootStackParamList } from "@app/navigation/stack-param-lists" import { useNavigation } from "@react-navigation/native" -import { getPublicKey } from "nostr-tools" -import { getSecretKey } from "@app/utils/nostr" type IrisBrowserNavigationProp = StackNavigationProp diff --git a/app/screens/social/post-success.tsx b/app/screens/social/post-success.tsx index 0f451b491..2214f0043 100644 --- a/app/screens/social/post-success.tsx +++ b/app/screens/social/post-success.tsx @@ -10,8 +10,6 @@ import { useAppSelector } from "@app/store/redux" import LinearGradient from "react-native-linear-gradient" import { FeedItem } from "@app/components/nostr-feed/FeedItem" import { Event, nip19 } from "nostr-tools" -import { bytesToHex } from "@noble/hashes/utils" -import { getSecretKey } from "@app/utils/nostr" import { useChatContext } from "../chat/chatContext" type PostSuccessNavigationProp = StackNavigationProp @@ -27,13 +25,10 @@ const PostSuccess = () => { const { postContent, userNpub, event } = route.params - const handleViewProfile = async () => { - const privateKey = await getSecretKey() - if (!privateKey) return + const handleViewProfile = () => { const pubkey = extractPubkey(userNpub) navigation.navigate("contactDetails", { contactPubkey: pubkey, - userPrivateKey: bytesToHex(privateKey), }) } diff --git a/app/screens/social/post.tsx b/app/screens/social/post.tsx index 0aca1ce0f..89688f188 100644 --- a/app/screens/social/post.tsx +++ b/app/screens/social/post.tsx @@ -21,8 +21,8 @@ import { useI18nContext } from "@app/i18n/i18n-react" import { usePersistentStateContext } from "@app/store/persistent-state" import { ExplainerVideo } from "@app/components/explainer-video" -import { nip19, getPublicKey, finalizeEvent, Relay, SimplePool } from "nostr-tools" -import { getSecretKey } from "@app/utils/nostr" +import { nip19 } from "nostr-tools" +import { getSigner } from "@app/nostr/signer" import { pool } from "@app/utils/nostr/pool" import { publishEventToRelays, @@ -44,7 +44,7 @@ const FIXED_TEXT_LINE_2 = "#introductions" const FIXED_TEXT_LINE_3 = "If you would like to remove the Flash credit from this post, uncheck the box below." -const MakeNostrPost = ({ privateKey }: { privateKey: string }) => { +const MakeNostrPost = () => { const styles = useStyles() const navigation = useNavigation() const { theme } = useTheme() @@ -73,9 +73,8 @@ const MakeNostrPost = ({ privateKey }: { privateKey: string }) => { method: string, ): Promise => { try { - const privateKey = await getSecretKey() - if (!privateKey) throw Error - const publicKey = getPublicKey(privateKey) + const signer = await getSigner() + const publicKey = await signer.getPublicKey() const authEvent = { kind: 27235, // NIP-98 HTTP Auth @@ -88,7 +87,7 @@ const MakeNostrPost = ({ privateKey }: { privateKey: string }) => { content: "", } - const signedAuthEvent = await finalizeEvent(authEvent, privateKey) + const signedAuthEvent = await signer.signEvent(authEvent) const encodedAuth = btoa(JSON.stringify(signedAuthEvent)) return `Nostr ${encodedAuth}` } catch (error) { @@ -240,8 +239,10 @@ const MakeNostrPost = ({ privateKey }: { privateKey: string }) => { const publishNostrNote = async (content: string) => { try { setLoading(true) - const privateKey = await getSecretKey() - if (!privateKey) { + let signer + try { + signer = await getSigner() + } catch { Alert.alert("Your nostr key is not yet set.") setLoading(false) return @@ -302,7 +303,7 @@ const MakeNostrPost = ({ privateKey }: { privateKey: string }) => { } } - const pubkey = getPublicKey(privateKey) + const pubkey = await signer.getPublicKey() // Extract hashtags and create t tags const hashtags = extractHashtags(finalContent) @@ -319,7 +320,7 @@ const MakeNostrPost = ({ privateKey }: { privateKey: string }) => { content: finalContent, } - const signedEvent = await finalizeEvent(event, privateKey) + const signedEvent = await signer.signEvent(event) console.log("Publishing kind-1 note to relays...") console.log("Author pubkey:", pubkey) diff --git a/app/utils/nostr.ts b/app/utils/nostr.ts index 447bd64df..ca0be1355 100644 --- a/app/utils/nostr.ts +++ b/app/utils/nostr.ts @@ -1,4 +1,3 @@ -import { useAppConfig } from "@app/hooks" import { getContactsFromEvent } from "@app/screens/chat/utils" import { bytesToHex } from "@noble/curves/abstract/utils" import AsyncStorage from "@react-native-async-storage/async-storage" @@ -7,7 +6,6 @@ import { finalizeEvent, generateSecretKey, getEventHash, - getPublicKey, Event, nip19, nip44, @@ -17,10 +15,10 @@ import { SubCloser, AbstractRelay, } from "nostr-tools" -import { Alert } from "react-native" import * as Keychain from "react-native-keychain" import { pool } from "./nostr/pool" +import type { NostrSigner } from "@app/nostr/signer/types" export const publicRelays = [ "wss://relay.damus.io", @@ -35,13 +33,16 @@ const now = () => Math.round(Date.now() / 1000) export type Rumor = UnsignedEvent & { id: string } export type Group = { subject: string; participants: string[] } -export const createRumor = (event: Partial, privateKey: Uint8Array) => { +export const createRumor = async ( + event: Partial, + signer: NostrSigner, +): Promise => { const rumor = { created_at: now(), content: "", tags: [], ...event, - pubkey: getPublicKey(privateKey), + pubkey: await signer.getPublicKey(), } as any rumor.id = getEventHash(rumor) @@ -49,41 +50,31 @@ export const createRumor = (event: Partial, privateKey: Uint8Arra return rumor as Rumor } -function encrypNip44Message( - privateKey: Uint8Array, - message: string, - receiverPublicKey: string, -) { - let conversationKey = nip44.v2.utils.getConversationKey( - bytesToHex(privateKey), - receiverPublicKey, - ) - let ciphertext = nip44.v2.encrypt(message, conversationKey) - return ciphertext -} - -export const createSeal = ( +export const createSeal = async ( rumor: Rumor, - privateKey: Uint8Array, + signer: NostrSigner, recipientPublicKey: string, -) => { - return finalizeEvent( - { - kind: 13, - content: encrypNip44Message(privateKey, JSON.stringify(rumor), recipientPublicKey), - created_at: now(), - tags: [], - }, - privateKey, - ) as Event +): Promise => { + const ciphertext = await signer.nip44.encrypt(recipientPublicKey, JSON.stringify(rumor)) + return signer.signEvent({ + kind: 13, + content: ciphertext, + created_at: now(), + tags: [], + }) } export const createWrap = (event: Event, recipientPublicKey: string) => { const randomKey = generateSecretKey() + const conversationKey = nip44.v2.utils.getConversationKey( + bytesToHex(randomKey), + recipientPublicKey, + ) + const ciphertext = nip44.v2.encrypt(JSON.stringify(event), conversationKey) return finalizeEvent( { kind: 1059, - content: encrypNip44Message(randomKey, JSON.stringify(event), recipientPublicKey), + content: ciphertext, created_at: now(), tags: [["p", recipientPublicKey]], }, @@ -91,25 +82,14 @@ export const createWrap = (event: Event, recipientPublicKey: string) => { ) as Event } -export const decryptNip44Message = ( - cipher: string, - publicKey: string, - privateKey: Uint8Array, -) => { - let conversationKey = nip44.v2.utils.getConversationKey( - bytesToHex(privateKey), - publicKey, - ) - let message = nip44.v2.decrypt(cipher, conversationKey) - return message -} - -export const getRumorFromWrap = (wrapEvent: Event, privateKey: Uint8Array) => { - let sealString = decryptNip44Message(wrapEvent.content, wrapEvent.pubkey, privateKey) - let seal = JSON.parse(sealString) as Event - let rumorString = decryptNip44Message(seal.content, seal.pubkey, privateKey) - let rumor = JSON.parse(rumorString) - return rumor +export const getRumorFromWrap = async ( + wrapEvent: Event, + signer: NostrSigner, +): Promise => { + const sealString = await signer.nip44.decrypt(wrapEvent.pubkey, wrapEvent.content) + const seal = JSON.parse(sealString) as Event + const rumorString = await signer.nip44.decrypt(seal.pubkey, seal.content) + return JSON.parse(rumorString) } export const fetchSecretFromLocalStorage = async () => { @@ -162,6 +142,7 @@ export const getGroupId = (participantsHex: string[]) => { return id } +/** @deprecated Use getSigner() from @app/nostr/signer instead */ export const getSecretKey = async () => { let secretKeyString = await fetchSecretFromLocalStorage() if (!secretKeyString) { @@ -215,9 +196,8 @@ export const fetchPreferredRelays = async (pubKeys: string[], pool: SimplePool) return relayMap } -export const sendNIP4Message = async (message: string, recipient: string) => { - let privateKey = await getSecretKey() - let NIP4Messages = {} +export const sendNIP4Message = async (_message: string, _recipient: string) => { + // TODO: implement NIP-04 via signer.nip04 } export const fetchContactList = async ( @@ -243,19 +223,9 @@ export const fetchContactList = async ( ) } -export const setPreferredRelay = async (secretKey?: Uint8Array) => { - let secret: Uint8Array | null = null - if (!secretKey) { - secret = await getSecretKey() - if (!secret) { - Alert.alert("Nostr Private Key Not Assigned") - return - } - } else { - secret = secretKey - } - const pubKey = getPublicKey(secret) - let relayEvent: UnsignedEvent = { +export const setPreferredRelay = async (signer: NostrSigner) => { + const pubKey = await signer.getPublicKey() + const relayEvent: UnsignedEvent = { pubkey: pubKey, tags: [ ["relay", "wss://relay.flashapp.me"], @@ -266,7 +236,7 @@ export const setPreferredRelay = async (secretKey?: Uint8Array) => { kind: 10050, content: "", } - const finalEvent = finalizeEvent(relayEvent, secret) + const finalEvent = await signer.signEvent(relayEvent) let messages = await Promise.allSettled(pool.publish(publicRelays, finalEvent)) console.log("Message from relays", messages) setTimeout(() => { @@ -275,23 +245,21 @@ export const setPreferredRelay = async (secretKey?: Uint8Array) => { } export const addToContactList = async ( - userPrivateKey: Uint8Array, + signer: NostrSigner, hexPubKeyToAdd: string, pool: SimplePool, - confirmOverwrite: () => Promise, // 🔸 mandatory callback + confirmOverwrite: () => Promise, contactsEvent?: Event, ) => { - const userPubkey = getPublicKey(userPrivateKey) + const userPubkey = await signer.getPublicKey() const existingContacts = contactsEvent ? getContactsFromEvent(contactsEvent) : [] const tags = contactsEvent?.tags || [] - // ✅ Prevent duplicates if (existingContacts.some((p: NostrProfile) => p.pubkey === hexPubKeyToAdd)) { console.log("Contact already in list.") return } - // 🟡 No existing contact list event found if (!contactsEvent) { const confirmed = await confirmOverwrite() if (!confirmed) { @@ -300,7 +268,6 @@ export const addToContactList = async ( } } - // 🧩 Build updated contact list event tags.push(["p", hexPubKeyToAdd]) const newEvent: UnsignedEvent = { kind: 3, @@ -310,7 +277,7 @@ export const addToContactList = async ( tags, } - const finalNewEvent = finalizeEvent(newEvent, userPrivateKey) + const finalNewEvent = await signer.signEvent(newEvent) const messages = await Promise.allSettled( pool.publish( ["wss://relay.damus.io", "wss://relay.primal.net", "wss://nos.lol"], @@ -325,14 +292,11 @@ export async function sendNip17Message( recipients: string[], message: string, preferredRelaysMap: Map, + signer: NostrSigner, onSent?: (rumor: Rumor) => void, ) { - let privateKey = await getSecretKey() - if (!privateKey) { - throw Error("Couldnt find private key in local storage") - } let p_tags = recipients.map((recipientId: string) => ["p", recipientId]) - let rumor = createRumor({ content: message, kind: 14, tags: p_tags }, privateKey) + let rumor = await createRumor({ content: message, kind: 14, tags: p_tags }, signer) let outputs: { acceptedRelays: string[]; rejectedRelays: string[] }[] = [] console.log("total recipients", recipients) await Promise.allSettled( @@ -347,7 +311,7 @@ export async function sendNip17Message( "wss://nostr.oxtr.dev", ]), ] - let seal = createSeal(rumor, privateKey, recipientId) + let seal = await createSeal(rumor, signer, recipientId) let wrap = createWrap(seal, recipientId) console.log("wrap created") try { @@ -459,20 +423,20 @@ export const saveGiftwrapsToStorage = async (giftwraps: Event[]) => { } } -export const createContactListEvent = async (secretKey: Uint8Array) => { - const selfPublicKey = getPublicKey(secretKey) - let event: UnsignedEvent | Event = { +export const createContactListEvent = async (signer: NostrSigner) => { + const selfPublicKey = await signer.getPublicKey() + const event: UnsignedEvent = { kind: 3, tags: [["p", selfPublicKey]], content: "", created_at: Math.floor(Date.now() / 1000), pubkey: selfPublicKey, } - event = finalizeEvent(event, secretKey) + const signedEvent = await signer.signEvent(event) const messages = await Promise.allSettled( pool.publish( ["wss://relay.damus.io", "wss://relay.primal.net", "wss://nos.lol"], - event as Event, + signedEvent, ), ) console.log("Message from relays for contact list publish", messages) From 94db152698942b2af83193abb1f16e0d0cb7d8dc Mon Sep 17 00:00:00 2001 From: Abhay Date: Tue, 17 Feb 2026 19:17:59 +0530 Subject: [PATCH 3/3] Fix Page referesh on create nostr profile --- .../nostr-settings/nostr-settings-screen.tsx | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/app/screens/settings-screen/nostr-settings/nostr-settings-screen.tsx b/app/screens/settings-screen/nostr-settings/nostr-settings-screen.tsx index 8db77fc4a..129fcc7b6 100644 --- a/app/screens/settings-screen/nostr-settings/nostr-settings-screen.tsx +++ b/app/screens/settings-screen/nostr-settings/nostr-settings-screen.tsx @@ -1,6 +1,6 @@ import { View, Pressable } from "react-native" import { Switch, Text, useTheme } from "@rneui/themed" -import { useEffect, useState } from "react" +import { useCallback, useEffect, useState } from "react" import { nip19 } from "nostr-tools" import Ionicons from "react-native-vector-icons/Ionicons" import { getSigner } from "@app/nostr/signer" @@ -47,26 +47,27 @@ export const NostrSettingsScreen = () => { : null console.log("USER PROFILE IS", userProfile) - useEffect(() => { - const initialize = async () => { - try { - const signer = await getSigner() - const pubKey = await signer.getPublicKey() - const npub = nip19.npubEncode(pubKey) - setNostrPubKey(npub) - if (dataAuthed?.me?.npub === npub) { - setLinked(true) - } else { - setLinked(false) - } - } catch { - setNostrPubKey(null) + const initialize = useCallback(async () => { + try { + const signer = await getSigner() + const pubKey = await signer.getPublicKey() + const npub = nip19.npubEncode(pubKey) + setNostrPubKey(npub) + if (dataAuthed?.me?.npub === npub) { + setLinked(true) + } else { setLinked(false) } + } catch { + setNostrPubKey(null) + setLinked(false) } - initialize() }, [dataAuthed]) + useEffect(() => { + initialize() + }, [initialize]) + const { saveNewNostrKey } = useNostrProfile() const { refreshUserProfile, resetChat } = useChatContext() @@ -147,6 +148,7 @@ export const NostrSettingsScreen = () => { setIsGenerating(false) setProgressMessage("") await resetChat() + await initialize() }} disabled={isGenerating} >