Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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!) {
Expand Down Expand Up @@ -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()
}, [])
Expand Down Expand Up @@ -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)
Expand Down
45 changes: 19 additions & 26 deletions app/hooks/use-nostr-profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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...")
Expand All @@ -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...")
Expand Down Expand Up @@ -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 = {
Expand All @@ -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
Expand Down
6 changes: 3 additions & 3 deletions app/navigation/stack-param-lists.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ export type RootStackParamList = {
CashoutSuccess: undefined
EditNostrProfile: undefined
NostrSettingsScreen: undefined
Contacts: { userPrivateKey: string }
Contacts: undefined
SignInViaQRCode: undefined
Nip29GroupChat: { groupId: string }
}
Expand All @@ -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 = {
Expand Down
4 changes: 3 additions & 1 deletion app/nostr/runtime/SubscriptionRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ type SubscriptionEntry = {
filter: Filter
closer: SubCloser
refCount: number
onEvent: (event: Event) => void
}

export class SubscriptionRegistry {
Expand Down Expand Up @@ -45,6 +46,7 @@ export class SubscriptionRegistry {
filter,
closer,
refCount: 1,
onEvent,
})
}

Expand All @@ -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
Expand Down
39 changes: 39 additions & 0 deletions app/nostr/signer/index.ts
Original file line number Diff line number Diff line change
@@ -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<NostrSigner> | null = null

export async function getSigner(): Promise<NostrSigner> {
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"
46 changes: 46 additions & 0 deletions app/nostr/signer/localSigner.ts
Original file line number Diff line number Diff line change
@@ -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)
},
}
}
22 changes: 22 additions & 0 deletions app/nostr/signer/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Event, EventTemplate } from "nostr-tools"

export interface NostrSigner {
getPublicKey(): Promise<string>
signEvent(event: EventTemplate): Promise<Event>

/**
* 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<string>

nip04: {
encrypt(pubkey: string, plaintext: string): Promise<string>
decrypt(pubkey: string, ciphertext: string): Promise<string>
}

nip44: {
encrypt(pubkey: string, plaintext: string): Promise<string>
decrypt(pubkey: string, ciphertext: string): Promise<string>
}
}
14 changes: 6 additions & 8 deletions app/screens/chat/GroupChat/GroupChatProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -218,8 +218,7 @@ export const NostrGroupChatProvider: React.FC<NostrGroupChatProviderProps> = ({
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,
Expand All @@ -229,7 +228,7 @@ export const NostrGroupChatProvider: React.FC<NostrGroupChatProviderProps> = ({
pubkey: userPublicKey,
}

const signedEvent = finalizeEvent(nostrEvent as any, secretKey)
const signedEvent = await signer.signEvent(nostrEvent as any)
pool.publish(relayUrls, signedEvent)
},
[userPublicKey, groupId, relayUrls],
Expand All @@ -238,8 +237,7 @@ export const NostrGroupChatProvider: React.FC<NostrGroupChatProviderProps> = ({
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,
Expand All @@ -249,7 +247,7 @@ export const NostrGroupChatProvider: React.FC<NostrGroupChatProviderProps> = ({
pubkey: userPublicKey,
}

const signedJoinEvent = finalizeEvent(joinEvent as any, secretKey)
const signedJoinEvent = await signer.signEvent(joinEvent as any)
pool.publish(relayUrls, signedJoinEvent)

// Optimistic system note
Expand Down
Loading