feat: nip5 manager

This commit is contained in:
Kieran 2023-03-30 19:21:33 +01:00
parent 625d3c3d5b
commit 3212155a43
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
8 changed files with 187 additions and 10 deletions

View File

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

View File

@ -370,10 +370,11 @@ export default function useEventPublisher() {
publicKey: pubKey,
};
},
generic: async (content: string, kind: EventKind) => {
generic: async (content: string, kind: EventKind, tags?: Array<Array<string>>) => {
if (pubKey) {
const ev = EventExt.forPubKey(pubKey, kind);
ev.content = content;
ev.tags = tags ?? [];
return await signEvent(ev);
}
},

View File

@ -16,6 +16,7 @@ export type ServiceErrorCode =
export interface ServiceError {
error: ServiceErrorCode;
errors: Array<string>;
}
export interface ServiceConfig {
@ -67,18 +68,18 @@ export class ServiceProvider {
}
async GetConfig(): Promise<ServiceConfig | ServiceError> {
return await this._GetJson("/config.json");
return await this.getJson("/config.json");
}
async CheckAvailable(handle: string, domain: string): Promise<HandleAvailability | ServiceError> {
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<HandleRegisterResponse | ServiceError> {
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<CheckRegisterResponse | ServiceError> {
return await this._GetJson("/registration/register/check", "POST", undefined, {
return await this.getJson("/registration/register/check", "POST", undefined, {
authorization: token,
});
}
async _GetJson<T>(
protected async getJson<T>(
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 <ServiceError>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: [] };
}
}

View File

@ -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<Array<ManageHandle>>("/list", "GET");
}
async transfer(id: string, to: string) {
return this.getJsonAuthd<object>(`/${id}?to=${to}`, "PATCH");
}
async getJsonAuthd<T>(
path: string,
method?: "GET" | string,
body?: { [key: string]: string },
headers?: { [key: string]: string }
): Promise<T | ServiceError> {
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<T>(path, method, body, {
...headers,
authorization: `Nostr ${window.btoa(JSON.stringify(auth))}`,
});
}
}

View File

@ -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: <Preferences />,
},
{
path: "nip5",
element: <Nip5ManagePage />,
},
...WalletSettingsRoutes,
];

View File

@ -47,6 +47,11 @@ const SettingsIndex = () => {
<FormattedMessage {...messages.Donate} />
<Icon name="arrowFront" />
</div>
<div className="settings-row" onClick={() => navigate("nip5")}>
<Icon name="badge" />
<FormattedMessage defaultMessage="Manage Nostr Adddress (NIP-05)" />
<Icon name="arrowFront" />
</div>
<div className="settings-row" onClick={handleLogout}>
<Icon name="logout" />
<FormattedMessage {...messages.LogOut} />

View File

@ -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<Array<ManageHandle>>();
const [transfer, setTransfer] = useState("");
const [newKey, setNewKey] = useState("");
const [error, setError] = useState<Array<string>>([]);
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<ManageHandle>);
}
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 (
<>
<h3>
<FormattedMessage defaultMessage="Nostr Address" />
</h3>
{handles.length === 0 && (
<FormattedMessage
defaultMessage="It looks like you dont have any, check {link} to buy one!"
values={{
link: (
<Link to="/verification">
<FormattedMessage defaultMessage="Verification" />
</Link>
),
}}
/>
)}
{handles.map(a => (
<>
<div className="card flex" key={a.id}>
<div className="f-grow">
<h4 className="nip05">
{a.handle}@
<span className="domain" data-domain={a.domain?.toLowerCase()}>
{a.domain}
</span>
</h4>
</div>
<div>
<button className="button" onClick={() => setTransfer(a.id)}>
<FormattedMessage defaultMessage="Transfer" />
</button>
</div>
</div>
</>
))}
{transfer && (
<Modal onClose={close}>
<h4>
<FormattedMessage defaultMessage="Transfer to Pubkey" />
</h4>
<div className="flex">
<div className="f-grow">
<input
type="text"
className="w-max mr10"
placeholder={formatMessage({
defaultMessage: "Public key (npub/nprofile)",
})}
value={newKey}
onChange={e => setNewKey(e.target.value)}
/>
</div>
<button className="button" onClick={() => startTransfer()}>
<FormattedMessage defaultMessage="Transfer" />
</button>
</div>
{error && <b className="error">{error}</b>}
</Modal>
)}
</>
);
}

View File

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