feat: nip5 manager
This commit is contained in:
parent
625d3c3d5b
commit
3212155a43
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
},
|
||||
|
@ -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: [] };
|
||||
}
|
||||
}
|
||||
|
50
packages/app/src/Nip05/SnortServiceProvider.ts
Normal file
50
packages/app/src/Nip05/SnortServiceProvider.ts
Normal 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))}`,
|
||||
});
|
||||
}
|
||||
}
|
@ -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,
|
||||
];
|
||||
|
@ -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} />
|
||||
|
113
packages/app/src/Pages/settings/ManageNip5.tsx
Normal file
113
packages/app/src/Pages/settings/ManageNip5.tsx
Normal 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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user