feat: add nip46

This commit is contained in:
2024-04-18 15:09:33 +07:00
parent cd31b99559
commit 89c36423ae
7 changed files with 189 additions and 65 deletions

View File

@@ -1,5 +1,5 @@
import { Spinner } from "@lume/ui"; import { Spinner } from "@lume/ui";
import { createLazyFileRoute, useNavigate } from "@tanstack/react-router"; import { createLazyFileRoute } from "@tanstack/react-router";
import { useState } from "react"; import { useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -9,7 +9,7 @@ export const Route = createLazyFileRoute("/auth/privkey")({
function Screen() { function Screen() {
const { ark } = Route.useRouteContext(); const { ark } = Route.useRouteContext();
const navigate = useNavigate(); const navigate = Route.useNavigate();
const [key, setKey] = useState(""); const [key, setKey] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
@@ -20,23 +20,23 @@ function Screen() {
return toast.warning( return toast.warning(
"You need to enter a valid private key starts with nsec or ncryptsec", "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 { try {
setLoading(true);
const npub = await ark.save_account(key, password); const npub = await ark.save_account(key, password);
navigate({
to: "/auth/settings", if (npub) {
search: { account: npub, new: false }, navigate({
replace: true, to: "/auth/settings",
}); search: { account: npub },
replace: true,
});
}
} catch (e) { } catch (e) {
setLoading(false);
toast.error(e); toast.error(e);
} }
setLoading(false);
}; };
return ( return (

View File

@@ -1,9 +1,74 @@
import { Spinner } from "@lume/ui";
import { createLazyFileRoute } from "@tanstack/react-router"; import { createLazyFileRoute } from "@tanstack/react-router";
import { useState } from "react";
import { toast } from "sonner";
export const Route = createLazyFileRoute("/auth/remote")({ export const Route = createLazyFileRoute("/auth/remote")({
component: Screen, component: Screen,
}); });
function Screen() { function Screen() {
return <div>#todo</div>; 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 (
<div className="mx-auto flex h-full w-full flex-col items-center justify-center gap-6 px-5 xl:max-w-xl">
<div className="text-center">
<h3 className="text-xl font-semibold">Continue with Nostr Connect</h3>
</div>
<div className="flex w-full flex-col gap-3">
<div className="flex flex-col gap-1">
<label
htmlFor="uri"
className="font-medium text-neutral-900 dark:text-neutral-100"
>
Connect URI
</label>
<input
name="uri"
type="text"
placeholder="bunker://..."
value={uri}
onChange={(e) => 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"
/>
</div>
<button
type="button"
onClick={submit}
disabled={loading}
className="mt-3 inline-flex h-11 w-full shrink-0 items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600 disabled:opacity-50"
>
{loading ? <Spinner /> : "Login"}
</button>
</div>
</div>
);
} }

View File

@@ -51,6 +51,7 @@ export class Ark {
const cmd: boolean = await invoke("load_selected_account", { const cmd: boolean = await invoke("load_selected_account", {
npub, npub,
}); });
await invoke("connect_user_relays");
return cmd; return cmd;
} catch (e) { } catch (e) {
@@ -58,12 +59,19 @@ export class Ark {
} }
} }
public async create_guest_account() { public async nostr_connect(uri: string) {
try { try {
const keys = await this.create_keys(); const remoteKey = uri.replace("bunker://", "").split("?")[0];
await this.save_account(keys.nsec, ""); 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) { } catch (e) {
throw new Error(String(e)); throw new Error(String(e));
} }
@@ -105,10 +113,7 @@ export class Ark {
public async get_event(id: string) { public async get_event(id: string) {
try { try {
const eventId: string = id const eventId: string = id.replace("nostr:", "").replace(/[^\w\s]/gi, "");
.replace("nostr:", "")
.split("'")[0]
.split(".")[0];
const cmd: string = await invoke("get_event", { id: eventId }); const cmd: string = await invoke("get_event", { id: eventId });
const event: Event = JSON.parse(cmd); const event: Event = JSON.parse(cmd);
return event; return event;
@@ -395,12 +400,7 @@ export class Ark {
public async get_profile(pubkey: string) { public async get_profile(pubkey: string) {
try { try {
const id = pubkey const id = pubkey.replace("nostr:", "").replace(/[^\w\s]/gi, "");
.replace("nostr:", "")
.split("'")[0]
.split(".")[0]
.split(",")[0]
.split("?")[0];
const cmd: Metadata = await invoke("get_profile", { id }); const cmd: Metadata = await invoke("get_profile", { id });
return cmd; return cmd;

View File

@@ -11,12 +11,7 @@ export function useProfile(pubkey: string) {
queryKey: ["user", pubkey], queryKey: ["user", pubkey],
queryFn: async () => { queryFn: async () => {
try { try {
const id = pubkey const id = pubkey.replace("nostr:", "").replace(/[^\w\s]/gi, "");
.replace("nostr:", "")
.split("'")[0]
.split(".")[0]
.split(",")[0]
.split("?")[0];
const cmd: Metadata = await invoke("get_profile", { id }); const cmd: Metadata = await invoke("get_profile", { id });
return cmd; return cmd;
} catch (e) { } catch (e) {

View File

@@ -101,11 +101,14 @@ fn main() {
nostr::keys::save_key, nostr::keys::save_key,
nostr::keys::get_encrypted_key, nostr::keys::get_encrypted_key,
nostr::keys::get_stored_nsec, nostr::keys::get_stored_nsec,
nostr::keys::nostr_connect,
nostr::keys::verify_signer, nostr::keys::verify_signer,
nostr::keys::load_selected_account, nostr::keys::load_selected_account,
nostr::keys::event_to_bech32, nostr::keys::event_to_bech32,
nostr::keys::user_to_bech32, nostr::keys::user_to_bech32,
nostr::keys::to_npub,
nostr::keys::verify_nip05, nostr::keys::verify_nip05,
nostr::metadata::connect_user_relays,
nostr::metadata::get_current_user_profile, nostr::metadata::get_current_user_profile,
nostr::metadata::get_profile, nostr::metadata::get_profile,
nostr::metadata::get_contact_list, nostr::metadata::get_contact_list,

View File

@@ -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<String, String> {
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] #[tauri::command]
pub async fn verify_signer(state: State<'_, Nostr>) -> Result<bool, ()> { pub async fn verify_signer(state: State<'_, Nostr>) -> Result<bool, ()> {
let client = &state.client; 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 client = &state.client;
let keyring = Entry::new("Lume Secret Storage", npub).unwrap(); let keyring = Entry::new("Lume Secret Storage", npub).unwrap();
if let Ok(nsec) = keyring.get_password() { if let Ok(password) = keyring.get_password() {
// Build nostr signer if password.starts_with("bunker://") {
let secret_key = SecretKey::from_bech32(nsec).expect("Get secret key failed"); let app_keys = Keys::generate();
let keys = Keys::new(secret_key); let bunker_uri = NostrConnectURI::parse(password).unwrap();
let public_key = keys.public_key(); let signer = Nip46Signer::new(bunker_uri, app_keys, Duration::from_secs(60), None)
let signer = NostrSigner::Keys(keys); .await
.unwrap();
// Update signer // Update signer
client.set_signer(Some(signer)).await; 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 // Update signer
let filter = Filter::new() client.set_signer(Some(signer)).await;
.author(public_key) // Done
.kind(Kind::RelayList) Ok(true)
.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(true)
} else { } else {
Err("nsec not found".into()) Err("nsec not found".into())
} }
@@ -174,6 +195,14 @@ pub fn user_to_bech32(key: &str, relays: Vec<String>) -> Result<String, ()> {
Ok(profile.to_bech32().unwrap()) Ok(profile.to_bech32().unwrap())
} }
#[tauri::command]
pub fn to_npub(hex: &str) -> Result<String, ()> {
let public_key = PublicKey::from_str(hex).unwrap();
let npub = Nip19::Pubkey(public_key);
Ok(npub.to_bech32().unwrap())
}
#[tauri::command(async)] #[tauri::command(async)]
pub async fn verify_nip05(key: &str, nip05: &str) -> Result<bool, ()> { pub async fn verify_nip05(key: &str, nip05: &str) -> Result<bool, ()> {
let public_key = PublicKey::from_str(key).unwrap(); let public_key = PublicKey::from_str(key).unwrap();

View File

@@ -11,6 +11,38 @@ pub struct CacheContact {
profile: Metadata, 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] #[tauri::command]
pub async fn get_current_user_profile(state: State<'_, Nostr>) -> Result<Metadata, String> { pub async fn get_current_user_profile(state: State<'_, Nostr>) -> Result<Metadata, String> {
let client = &state.client; let client = &state.client;