diff --git a/apps/desktop2/src/routes/auth/privkey.lazy.tsx b/apps/desktop2/src/routes/auth/privkey.lazy.tsx index afca9f25..b59a38e0 100644 --- a/apps/desktop2/src/routes/auth/privkey.lazy.tsx +++ b/apps/desktop2/src/routes/auth/privkey.lazy.tsx @@ -1,5 +1,5 @@ import { Spinner } from "@lume/ui"; -import { createLazyFileRoute, useNavigate } from "@tanstack/react-router"; +import { createLazyFileRoute } from "@tanstack/react-router"; import { useState } from "react"; import { toast } from "sonner"; @@ -9,7 +9,7 @@ export const Route = createLazyFileRoute("/auth/privkey")({ function Screen() { const { ark } = Route.useRouteContext(); - const navigate = useNavigate(); + const navigate = Route.useNavigate(); const [key, setKey] = useState(""); const [password, setPassword] = useState(""); @@ -20,23 +20,23 @@ function Screen() { return toast.warning( "You need to enter a valid private key starts with nsec or ncryptsec", ); - if (key.length < 30) - return toast.warning("You need to enter a valid private key"); - - setLoading(true); try { + setLoading(true); + const npub = await ark.save_account(key, password); - navigate({ - to: "/auth/settings", - search: { account: npub, new: false }, - replace: true, - }); + + if (npub) { + navigate({ + to: "/auth/settings", + search: { account: npub }, + replace: true, + }); + } } catch (e) { + setLoading(false); toast.error(e); } - - setLoading(false); }; return ( diff --git a/apps/desktop2/src/routes/auth/remote.lazy.tsx b/apps/desktop2/src/routes/auth/remote.lazy.tsx index 74bd94bd..54890bcb 100644 --- a/apps/desktop2/src/routes/auth/remote.lazy.tsx +++ b/apps/desktop2/src/routes/auth/remote.lazy.tsx @@ -1,9 +1,74 @@ +import { Spinner } from "@lume/ui"; import { createLazyFileRoute } from "@tanstack/react-router"; +import { useState } from "react"; +import { toast } from "sonner"; export const Route = createLazyFileRoute("/auth/remote")({ component: Screen, }); function Screen() { - return
#todo
; + const { ark } = Route.useRouteContext(); + const navigate = Route.useNavigate(); + + const [uri, setUri] = useState(""); + const [loading, setLoading] = useState(false); + + const submit = async () => { + if (!uri.startsWith("bunker://")) + return toast.warning( + "You need to enter a valid Connect URI starts with bunker://", + ); + + try { + setLoading(true); + + const npub = await ark.nostr_connect(uri); + + if (npub) { + navigate({ + to: "/auth/settings", + search: { account: npub }, + replace: true, + }); + } + } catch (e) { + setLoading(false); + toast.error(e); + } + }; + + return ( +
+
+

Continue with Nostr Connect

+
+
+
+ + setUri(e.target.value)} + className="h-11 rounded-lg border-transparent bg-neutral-100 px-3 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-950 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800" + /> +
+ +
+
+ ); } diff --git a/packages/ark/src/ark.ts b/packages/ark/src/ark.ts index a627db67..24410f8c 100644 --- a/packages/ark/src/ark.ts +++ b/packages/ark/src/ark.ts @@ -51,6 +51,7 @@ export class Ark { const cmd: boolean = await invoke("load_selected_account", { npub, }); + await invoke("connect_user_relays"); return cmd; } catch (e) { @@ -58,12 +59,19 @@ export class Ark { } } - public async create_guest_account() { + public async nostr_connect(uri: string) { try { - const keys = await this.create_keys(); - await this.save_account(keys.nsec, ""); + const remoteKey = uri.replace("bunker://", "").split("?")[0]; + const npub: string = await invoke("to_npub", { hex: remoteKey }); - return keys.npub; + if (npub) { + const connect: string = await invoke("nostr_connect", { + npub, + uri, + }); + + return connect; + } } catch (e) { throw new Error(String(e)); } @@ -105,10 +113,7 @@ export class Ark { public async get_event(id: string) { try { - const eventId: string = id - .replace("nostr:", "") - .split("'")[0] - .split(".")[0]; + const eventId: string = id.replace("nostr:", "").replace(/[^\w\s]/gi, ""); const cmd: string = await invoke("get_event", { id: eventId }); const event: Event = JSON.parse(cmd); return event; @@ -395,12 +400,7 @@ export class Ark { public async get_profile(pubkey: string) { try { - const id = pubkey - .replace("nostr:", "") - .split("'")[0] - .split(".")[0] - .split(",")[0] - .split("?")[0]; + const id = pubkey.replace("nostr:", "").replace(/[^\w\s]/gi, ""); const cmd: Metadata = await invoke("get_profile", { id }); return cmd; diff --git a/packages/ark/src/hooks/useProfile.ts b/packages/ark/src/hooks/useProfile.ts index 36cc688b..34873fb7 100644 --- a/packages/ark/src/hooks/useProfile.ts +++ b/packages/ark/src/hooks/useProfile.ts @@ -11,12 +11,7 @@ export function useProfile(pubkey: string) { queryKey: ["user", pubkey], queryFn: async () => { try { - const id = pubkey - .replace("nostr:", "") - .split("'")[0] - .split(".")[0] - .split(",")[0] - .split("?")[0]; + const id = pubkey.replace("nostr:", "").replace(/[^\w\s]/gi, ""); const cmd: Metadata = await invoke("get_profile", { id }); return cmd; } catch (e) { diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index c0498275..ef6dd5b8 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -101,11 +101,14 @@ fn main() { nostr::keys::save_key, nostr::keys::get_encrypted_key, nostr::keys::get_stored_nsec, + nostr::keys::nostr_connect, nostr::keys::verify_signer, nostr::keys::load_selected_account, nostr::keys::event_to_bech32, nostr::keys::user_to_bech32, + nostr::keys::to_npub, nostr::keys::verify_nip05, + nostr::metadata::connect_user_relays, nostr::metadata::get_current_user_profile, nostr::metadata::get_profile, nostr::metadata::get_contact_list, diff --git a/src-tauri/src/nostr/keys.rs b/src-tauri/src/nostr/keys.rs index a7d40eed..fbc1b82a 100644 --- a/src-tauri/src/nostr/keys.rs +++ b/src-tauri/src/nostr/keys.rs @@ -74,6 +74,39 @@ pub async fn save_key( } } +#[tauri::command] +pub async fn nostr_connect( + npub: &str, + uri: &str, + app_handle: tauri::AppHandle, + state: State<'_, Nostr>, +) -> Result { + let client = &state.client; + let app_keys = Keys::generate(); + + match NostrConnectURI::parse(uri) { + Ok(bunker_uri) => { + println!("connecting... {}", uri); + + match Nip46Signer::new(bunker_uri, app_keys, Duration::from_secs(120), None).await { + Ok(signer) => { + let home_dir = app_handle.path().home_dir().unwrap(); + let app_dir = home_dir.join("Lume/"); + let file_path = npub.to_owned() + ".npub"; + let keyring = Entry::new("Lume Secret Storage", npub).unwrap(); + let _ = File::create(app_dir.join(file_path)).unwrap(); + let _ = keyring.set_password(uri); + let _ = client.set_signer(Some(signer.into())).await; + + Ok(npub.into()) + } + Err(err) => Err(err.to_string()), + } + } + Err(err) => Err(err.to_string()), + } +} + #[tauri::command] pub async fn verify_signer(state: State<'_, Nostr>) -> Result { let client = &state.client; @@ -119,40 +152,28 @@ pub async fn load_selected_account(npub: &str, state: State<'_, Nostr>) -> Resul let client = &state.client; let keyring = Entry::new("Lume Secret Storage", npub).unwrap(); - if let Ok(nsec) = keyring.get_password() { - // Build nostr signer - let secret_key = SecretKey::from_bech32(nsec).expect("Get secret key failed"); - let keys = Keys::new(secret_key); - let public_key = keys.public_key(); - let signer = NostrSigner::Keys(keys); + if let Ok(password) = keyring.get_password() { + if password.starts_with("bunker://") { + let app_keys = Keys::generate(); + let bunker_uri = NostrConnectURI::parse(password).unwrap(); + let signer = Nip46Signer::new(bunker_uri, app_keys, Duration::from_secs(60), None) + .await + .unwrap(); - // Update signer - client.set_signer(Some(signer)).await; + // Update signer + client.set_signer(Some(signer.into())).await; + // Done + Ok(true) + } else { + let secret_key = SecretKey::from_bech32(password).expect("Get secret key failed"); + let keys = Keys::new(secret_key); + let signer = NostrSigner::Keys(keys); - // Get user's relay list - let filter = Filter::new() - .author(public_key) - .kind(Kind::RelayList) - .limit(1); - let query = client - .get_events_of(vec![filter], Some(Duration::from_secs(10))) - .await; - - // Connect user's relay list - if let Ok(events) = query { - if let Some(event) = events.first() { - let list = nip65::extract_relay_list(&event); - for item in list.into_iter() { - println!("connecting to relay: {}", item.0.to_string()); - client - .connect_relay(item.0.to_string()) - .await - .unwrap_or_default(); - } - } + // Update signer + client.set_signer(Some(signer)).await; + // Done + Ok(true) } - - Ok(true) } else { Err("nsec not found".into()) } @@ -174,6 +195,14 @@ pub fn user_to_bech32(key: &str, relays: Vec) -> Result { Ok(profile.to_bech32().unwrap()) } +#[tauri::command] +pub fn to_npub(hex: &str) -> Result { + let public_key = PublicKey::from_str(hex).unwrap(); + let npub = Nip19::Pubkey(public_key); + + Ok(npub.to_bech32().unwrap()) +} + #[tauri::command(async)] pub async fn verify_nip05(key: &str, nip05: &str) -> Result { let public_key = PublicKey::from_str(key).unwrap(); diff --git a/src-tauri/src/nostr/metadata.rs b/src-tauri/src/nostr/metadata.rs index 5c187339..89a9cd68 100644 --- a/src-tauri/src/nostr/metadata.rs +++ b/src-tauri/src/nostr/metadata.rs @@ -11,6 +11,38 @@ pub struct CacheContact { profile: Metadata, } +#[tauri::command] +pub async fn connect_user_relays(state: State<'_, Nostr>) -> Result<(), ()> { + let client = &state.client; + let signer = client.signer().await.unwrap(); + let public_key = signer.public_key().await.unwrap(); + + // Get user's relay list + let filter = Filter::new() + .author(public_key) + .kind(Kind::RelayList) + .limit(1); + let query = client + .get_events_of(vec![filter], Some(Duration::from_secs(10))) + .await; + + // Connect user's relay list + if let Ok(events) = query { + if let Some(event) = events.first() { + let list = nip65::extract_relay_list(&event); + for item in list.into_iter() { + println!("connecting to relay: {}", item.0.to_string()); + client + .connect_relay(item.0.to_string()) + .await + .unwrap_or_default(); + } + } + } + + Ok(()) +} + #[tauri::command] pub async fn get_current_user_profile(state: State<'_, Nostr>) -> Result { let client = &state.client;