From 3212155a4302f6bffe0b6fad39dc3facf6b6d74b Mon Sep 17 00:00:00 2001 From: Kieran Date: Thu, 30 Mar 2023 19:21:33 +0100 Subject: [PATCH 1/4] 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; From c284eb69105c3b18d37bbde032f2691c85dffb35 Mon Sep 17 00:00:00 2001 From: Kieran Date: Thu, 30 Mar 2023 19:23:46 +0100 Subject: [PATCH 2/4] revert local dev config --- packages/app/src/Const.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/Const.ts b/packages/app/src/Const.ts index c2f0120..5d73603 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 = "http://localhost:5097"; +export const ApiHost = "http://api.snort.social"; /** * LibreTranslate endpoint From 13543c5b1058df2ccf8e166865ea3595cf88473a Mon Sep 17 00:00:00 2001 From: Kieran Date: Thu, 30 Mar 2023 19:30:24 +0100 Subject: [PATCH 3/4] fix api host --- packages/app/src/Const.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/Const.ts b/packages/app/src/Const.ts index 5d73603..4d176c9 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 = "http://api.snort.social"; +export const ApiHost = "https://api.snort.social"; /** * LibreTranslate endpoint From 2f6fa47e2755df0192952003562422c02272f996 Mon Sep 17 00:00:00 2001 From: Kieran Date: Thu, 30 Mar 2023 19:36:44 +0100 Subject: [PATCH 4/4] extract lang --- packages/app/src/lang.json | 30 ++++++++++++++++++++++++--- packages/app/src/translations/en.json | 10 ++++++++- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/packages/app/src/lang.json b/packages/app/src/lang.json index 13130cf..5673e61 100644 --- a/packages/app/src/lang.json +++ b/packages/app/src/lang.json @@ -121,6 +121,9 @@ "5rOdPG": { "defaultMessage": "Once you setup your key manager extension and generated a key, you can follow our new users flow to setup your profile and help you find some interesting people on Nostr to follow." }, + "5u6iEc": { + "defaultMessage": "Transfer to Pubkey" + }, "5ykRmX": { "defaultMessage": "Send zap" }, @@ -174,6 +177,9 @@ "defaultMessage": "Login", "description": "Login button" }, + "9pMqYs": { + "defaultMessage": "Nostr Address" + }, "9wO4wJ": { "defaultMessage": "Lightning Invoice" }, @@ -239,6 +245,9 @@ "Dt/Zd5": { "defaultMessage": "Media in posts will automatically be shown for selected people, otherwise only the link will show" }, + "DtYelJ": { + "defaultMessage": "Transfer" + }, "E8a4yq": { "defaultMessage": "Follow some popular accounts" }, @@ -275,6 +284,9 @@ "FmXUJg": { "defaultMessage": "follows you" }, + "FpxElY": { + "defaultMessage": "Verification" + }, "G/yZLu": { "defaultMessage": "Remove" }, @@ -366,6 +378,9 @@ "LF5kYT": { "defaultMessage": "Other Connections" }, + "LQahqW": { + "defaultMessage": "Manage Nostr Adddress (NIP-05)" + }, "LXxsbk": { "defaultMessage": "Anonymous" }, @@ -482,6 +497,9 @@ "defaultMessage": "Hex Salt..", "description": "Hexidecimal 'salt' input for imgproxy" }, + "UDYlxu": { + "defaultMessage": "Pending Subscriptions" + }, "UQ3pOC": { "defaultMessage": "On Nostr, many people have the same username. User names and identity are separate things. You can get a unique identifier in the next step." }, @@ -497,6 +515,9 @@ "VOjC1i": { "defaultMessage": "Pick which upload service you want to upload attachments to" }, + "VR5eHw": { + "defaultMessage": "Public key (npub/nprofile)" + }, "VlJkSk": { "defaultMessage": "{n} muted" }, @@ -669,6 +690,9 @@ "iNWbVV": { "defaultMessage": "Handle" }, + "iXPL0Z": { + "defaultMessage": "Can't login with private key on an insecure connection, please use a Nostr key manager extension instead" + }, "ieGrWo": { "defaultMessage": "Follow" }, @@ -733,6 +757,9 @@ "lvlPhZ": { "defaultMessage": "Pay Invoice" }, + "mErPop": { + "defaultMessage": "It looks like you dont have any, check {link} to buy one!" + }, "mH91FY": { "defaultMessage": "Each contributor will get paid a percentage of all donations and NIP-05 orders, you can see the split amounts below" }, @@ -861,9 +888,6 @@ "uSV4Ti": { "defaultMessage": "Reposts need to be manually confirmed" }, - "ubtr9S": { - "defaultMessage": "Can't login with private key on 'http://' connection, please use a Nostr key manager extension instead" - }, "usAvMr": { "defaultMessage": "Edit Profile" }, diff --git a/packages/app/src/translations/en.json b/packages/app/src/translations/en.json index 17454c7..0d7accc 100644 --- a/packages/app/src/translations/en.json +++ b/packages/app/src/translations/en.json @@ -39,6 +39,7 @@ "4Z3t5i": "Use imgproxy to compress images", "4rYCjn": "Note to Self", "5rOdPG": "Once you setup your key manager extension and generated a key, you can follow our new users flow to setup your profile and help you find some interesting people on Nostr to follow.", + "5u6iEc": "Transfer to Pubkey", "5ykRmX": "Send zap", "6ewQqw": "Likes ({n})", "6tUqAb": "Generate a public / private key pair. Do not share your private key with anyone, this acts as your password. Once lost, it cannot be “reset” or recovered. Keep safe!", @@ -56,6 +57,7 @@ "9SvQep": "Follows {n}", "9WRlF4": "Send", "9gqH2W": "Login", + "9pMqYs": "Nostr Address", "9wO4wJ": "Lightning Invoice", "ADmfQT": "Parent", "ASRK0S": "This author has been muted", @@ -77,6 +79,7 @@ "DZzCem": "Show latest {n} notes", "Dh3hbq": "Auto Zap", "Dt/Zd5": "Media in posts will automatically be shown for selected people, otherwise only the link will show", + "DtYelJ": "Transfer", "E8a4yq": "Follow some popular accounts", "EPYwm7": "Your private key is your password. If you lose this key, you will lose access to your account! Copy it and keep it in a safe place. There is no way to reset your private key.", "EWyQH5": "Global", @@ -89,6 +92,7 @@ "FS3b54": "Done!", "FfYsOb": "An error has occured!", "FmXUJg": "follows you", + "FpxElY": "Verification", "G/yZLu": "Remove", "G1BGCg": "Select Wallet", "GFOoEE": "Salt", @@ -119,6 +123,7 @@ "KahimY": "Unknown event kind: {kind}", "L7SZPr": "For more information about donations see {link}.", "LF5kYT": "Other Connections", + "LQahqW": "Manage Nostr Adddress (NIP-05)", "LXxsbk": "Anonymous", "LgbKvU": "Comment", "LxY9tW": "Generate Key", @@ -156,11 +161,13 @@ "Rs4kCE": "Bookmark", "Sjo1P4": "Custom", "TpgeGw": "Hex Salt..", + "UDYlxu": "Pending Subscriptions", "UQ3pOC": "On Nostr, many people have the same username. User names and identity are separate things. You can get a unique identifier in the next step.", "Up5U7K": "Block", "VBadwB": "Hmm, can't find a key manager extension.. try reloading the page.", "VN0+Fz": "Balance: {amount} sats", "VOjC1i": "Pick which upload service you want to upload attachments to", + "VR5eHw": "Public key (npub/nprofile)", "VlJkSk": "{n} muted", "VnXp8Z": "Avatar", "VtPV/B": "Login with Extension (NIP-07)", @@ -217,6 +224,7 @@ "iDGAbc": "Get a Snort identifier", "iGT1eE": "Prevent fake accounts from imitating you", "iNWbVV": "Handle", + "iXPL0Z": "Can't login with private key on an insecure connection, please use a Nostr key manager extension instead", "ieGrWo": "Follow", "itPgxd": "Profile", "izWS4J": "Unfollow", @@ -238,6 +246,7 @@ "lnaT9F": "Following {n}", "lsNFM1": "Click to load content from {link}", "lvlPhZ": "Pay Invoice", + "mErPop": "It looks like you dont have any, check {link} to buy one!", "mH91FY": "Each contributor will get paid a percentage of all donations and NIP-05 orders, you can see the split amounts below", "mKAr6h": "Follow all", "mKh2HS": "File upload service", @@ -280,7 +289,6 @@ "u4bHcR": "Check out the code here: {link}", "uD/N6c": "Zap {target} {n} sats", "uSV4Ti": "Reposts need to be manually confirmed", - "ubtr9S": "Can't login with private key on 'http://' connection, please use a Nostr key manager extension instead", "usAvMr": "Edit Profile", "ut+2Cd": "Get a partner identifier", "vOKedj": "{n,plural,=1{& {n} other} other{& {n} others}}",