From 3212155a4302f6bffe0b6fad39dc3facf6b6d74b Mon Sep 17 00:00:00 2001 From: Kieran Date: Thu, 30 Mar 2023 19:21:33 +0100 Subject: [PATCH] feat: nip5 manager --- packages/app/src/Const.ts | 2 +- packages/app/src/Feed/EventPublisher.ts | 3 +- packages/app/src/Nip05/ServiceProvider.ts | 18 +-- .../app/src/Nip05/SnortServiceProvider.ts | 50 ++++++++ packages/app/src/Pages/SettingsPage.tsx | 5 + packages/app/src/Pages/settings/Index.tsx | 5 + .../app/src/Pages/settings/ManageNip5.tsx | 113 ++++++++++++++++++ packages/nostr/src/legacy/EventKind.ts | 1 + 8 files changed, 187 insertions(+), 10 deletions(-) create mode 100644 packages/app/src/Nip05/SnortServiceProvider.ts create mode 100644 packages/app/src/Pages/settings/ManageNip5.tsx diff --git a/packages/app/src/Const.ts b/packages/app/src/Const.ts index 4d176c9..c2f0120 100644 --- a/packages/app/src/Const.ts +++ b/packages/app/src/Const.ts @@ -3,7 +3,7 @@ import { RelaySettings } from "@snort/nostr"; /** * Add-on api for snort features */ -export const ApiHost = "https://api.snort.social"; +export const ApiHost = "http://localhost:5097"; /** * LibreTranslate endpoint diff --git a/packages/app/src/Feed/EventPublisher.ts b/packages/app/src/Feed/EventPublisher.ts index dfff971..e4769b6 100644 --- a/packages/app/src/Feed/EventPublisher.ts +++ b/packages/app/src/Feed/EventPublisher.ts @@ -370,10 +370,11 @@ export default function useEventPublisher() { publicKey: pubKey, }; }, - generic: async (content: string, kind: EventKind) => { + generic: async (content: string, kind: EventKind, tags?: Array>) => { if (pubKey) { const ev = EventExt.forPubKey(pubKey, kind); ev.content = content; + ev.tags = tags ?? []; return await signEvent(ev); } }, diff --git a/packages/app/src/Nip05/ServiceProvider.ts b/packages/app/src/Nip05/ServiceProvider.ts index 42c97e3..09ea680 100644 --- a/packages/app/src/Nip05/ServiceProvider.ts +++ b/packages/app/src/Nip05/ServiceProvider.ts @@ -16,6 +16,7 @@ export type ServiceErrorCode = export interface ServiceError { error: ServiceErrorCode; + errors: Array; } export interface ServiceConfig { @@ -67,18 +68,18 @@ export class ServiceProvider { } async GetConfig(): Promise { - return await this._GetJson("/config.json"); + return await this.getJson("/config.json"); } async CheckAvailable(handle: string, domain: string): Promise { - return await this._GetJson("/registration/availability", "POST", { + return await this.getJson("/registration/availability", "POST", { name: handle, domain, }); } async RegisterHandle(handle: string, domain: string, pubkey: string): Promise { - return await this._GetJson("/registration/register", "PUT", { + return await this.getJson("/registration/register", "PUT", { name: handle, domain, pk: pubkey, @@ -87,11 +88,12 @@ export class ServiceProvider { } async CheckRegistration(token: string): Promise { - return await this._GetJson("/registration/register/check", "POST", undefined, { + return await this.getJson("/registration/register/check", "POST", undefined, { authorization: token, }); } - async _GetJson( + + protected async getJson( path: string, method?: "GET" | string, body?: { [key: string]: string }, @@ -110,12 +112,12 @@ export class ServiceProvider { const obj = await rsp.json(); if ("error" in obj) { - return obj; + return obj as ServiceError; } - return obj; + return obj as T; } catch (e) { console.warn(e); } - return { error: "UNKNOWN_ERROR" }; + return { error: "UNKNOWN_ERROR", errors: [] }; } } diff --git a/packages/app/src/Nip05/SnortServiceProvider.ts b/packages/app/src/Nip05/SnortServiceProvider.ts new file mode 100644 index 0000000..8cce85f --- /dev/null +++ b/packages/app/src/Nip05/SnortServiceProvider.ts @@ -0,0 +1,50 @@ +import { EventKind } from "@snort/nostr"; +import { EventPublisher } from "Feed/EventPublisher"; +import { ServiceError, ServiceProvider } from "./ServiceProvider"; + +export interface ManageHandle { + id: string; + handle: string; + domain: string; + pubkey: string; + created: Date; +} + +export default class SnortServiceProvider extends ServiceProvider { + readonly #publisher: EventPublisher; + + constructor(publisher: EventPublisher, url: string | URL) { + super(url); + this.#publisher = publisher; + } + + async list() { + return this.getJsonAuthd>("/list", "GET"); + } + + async transfer(id: string, to: string) { + return this.getJsonAuthd(`/${id}?to=${to}`, "PATCH"); + } + + async getJsonAuthd( + path: string, + method?: "GET" | string, + body?: { [key: string]: string }, + headers?: { [key: string]: string } + ): Promise { + const auth = await this.#publisher.generic("", EventKind.HttpAuthentication, [ + ["url", `${this.url}${path}`], + ["method", method ?? "GET"], + ]); + if (!auth) { + return { + error: "INVALID_TOKEN", + } as ServiceError; + } + + return this.getJson(path, method, body, { + ...headers, + authorization: `Nostr ${window.btoa(JSON.stringify(auth))}`, + }); + } +} diff --git a/packages/app/src/Pages/SettingsPage.tsx b/packages/app/src/Pages/SettingsPage.tsx index aa77ee9..3228314 100644 --- a/packages/app/src/Pages/SettingsPage.tsx +++ b/packages/app/src/Pages/SettingsPage.tsx @@ -6,6 +6,7 @@ import Relay from "Pages/settings/Relays"; import Preferences from "Pages/settings/Preferences"; import RelayInfo from "Pages/settings/RelayInfo"; import { WalletSettingsRoutes } from "Pages/settings/WalletSettings"; +import Nip5ManagePage from "Pages/settings/ManageNip5"; import messages from "./messages"; @@ -43,5 +44,9 @@ export const SettingsRoutes: RouteObject[] = [ path: "preferences", element: , }, + { + path: "nip5", + element: , + }, ...WalletSettingsRoutes, ]; diff --git a/packages/app/src/Pages/settings/Index.tsx b/packages/app/src/Pages/settings/Index.tsx index 1ec45fd..678758e 100644 --- a/packages/app/src/Pages/settings/Index.tsx +++ b/packages/app/src/Pages/settings/Index.tsx @@ -47,6 +47,11 @@ const SettingsIndex = () => { +
navigate("nip5")}> + + + +
diff --git a/packages/app/src/Pages/settings/ManageNip5.tsx b/packages/app/src/Pages/settings/ManageNip5.tsx new file mode 100644 index 0000000..048f98f --- /dev/null +++ b/packages/app/src/Pages/settings/ManageNip5.tsx @@ -0,0 +1,113 @@ +import { useEffect, useState } from "react"; +import { FormattedMessage, useIntl } from "react-intl"; +import { Link } from "react-router-dom"; + +import { ApiHost } from "Const"; +import Modal from "Element/Modal"; +import useEventPublisher from "Feed/EventPublisher"; +import { ServiceError } from "Nip05/ServiceProvider"; +import SnortServiceProvider, { ManageHandle } from "Nip05/SnortServiceProvider"; + +export default function Nip5ManagePage() { + const publisher = useEventPublisher(); + const { formatMessage } = useIntl(); + const [handles, setHandles] = useState>(); + const [transfer, setTransfer] = useState(""); + const [newKey, setNewKey] = useState(""); + const [error, setError] = useState>([]); + const sp = new SnortServiceProvider(publisher, `${ApiHost}/api/v1/n5sp`); + + useEffect(() => { + loadHandles().catch(console.error); + }, []); + + async function loadHandles() { + const list = await sp.list(); + setHandles(list as Array); + } + + async function startTransfer() { + if (!transfer || !newKey) return; + setError([]); + const rsp = await sp.transfer(transfer, newKey); + if ("error" in rsp) { + setError((rsp as ServiceError).errors); + return; + } + await loadHandles(); + setTransfer(""); + setNewKey(""); + } + + function close() { + setTransfer(""); + setNewKey(""); + setError([]); + } + + if (!handles) { + return null; + } + return ( + <> +

+ +

+ {handles.length === 0 && ( + + + + ), + }} + /> + )} + {handles.map(a => ( + <> +
+
+

+ {a.handle}@ + + {a.domain} + +

+
+
+ +
+
+ + ))} + {transfer && ( + +

+ +

+
+
+ setNewKey(e.target.value)} + /> +
+ +
+ {error && {error}} +
+ )} + + ); +} diff --git a/packages/nostr/src/legacy/EventKind.ts b/packages/nostr/src/legacy/EventKind.ts index b8d1e79..236b1c3 100644 --- a/packages/nostr/src/legacy/EventKind.ts +++ b/packages/nostr/src/legacy/EventKind.ts @@ -19,6 +19,7 @@ enum EventKind { ProfileBadges = 30008, // NIP-58 ZapRequest = 9734, // NIP 57 ZapReceipt = 9735, // NIP 57 + HttpAuthentication = 27235, // NIP XX - HTTP Authentication } export default EventKind;