feat: add nip46

This commit is contained in:
reya 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 { 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 (

View File

@ -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 <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", {
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;

View File

@ -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) {

View File

@ -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,

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]
pub async fn verify_signer(state: State<'_, Nostr>) -> Result<bool, ()> {
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<String>) -> Result<String, ()> {
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)]
pub async fn verify_nip05(key: &str, nip05: &str) -> Result<bool, ()> {
let public_key = PublicKey::from_str(key).unwrap();

View File

@ -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<Metadata, String> {
let client = &state.client;