feat: add nip46
This commit is contained in:
@@ -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 (
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user