From 6331294580d8f1ae573b63549ca684c1a81278ce Mon Sep 17 00:00:00 2001 From: verbiricha Date: Tue, 16 Jan 2024 18:39:24 +0100 Subject: [PATCH] feat: nip46-login --- apps/docs/stories/login.stories.tsx | 15 +++++ apps/emojis/ui/header.tsx | 2 +- apps/relays/app/components/header.tsx | 2 +- packages/core/package.json | 2 +- packages/core/src/components/Login.tsx | 67 +++++++++++++++++++ packages/core/src/components/LoginButton.tsx | 20 +++++- .../core/src/components/ReactionPicker.tsx | 4 +- packages/core/src/context.tsx | 63 ++++++++++++++++- packages/core/src/lang.json | 20 ++++++ packages/core/src/translations/en.json | 5 ++ packages/core/src/types.ts | 8 ++- pnpm-lock.yaml | 23 ++++++- 12 files changed, 221 insertions(+), 10 deletions(-) diff --git a/apps/docs/stories/login.stories.tsx b/apps/docs/stories/login.stories.tsx index 333aa50..f5afc6a 100644 --- a/apps/docs/stories/login.stories.tsx +++ b/apps/docs/stories/login.stories.tsx @@ -40,3 +40,18 @@ export const Pubkey: Story = { method: "npub", }, }; + +export const Bunker: Story = { + name: "Bunker", + render(props) { + return ( + + + + + ); + }, + args: { + method: "nip46", + }, +}; diff --git a/apps/emojis/ui/header.tsx b/apps/emojis/ui/header.tsx index e10b072..ac30638 100644 --- a/apps/emojis/ui/header.tsx +++ b/apps/emojis/ui/header.tsx @@ -41,7 +41,7 @@ export default function Header() { - + {session?.pubkey && ( <> - + {session?.pubkey && ( <> (); + const [isBusy, setIsBusy] = useState(false); + const bunkerLogin = useBunkerLogin(); + + async function loginWithBunker() { + try { + setIsBusy(true); + if (url) { + const user = await bunkerLogin(url); + if (user) { + onLogin && onLogin(); + } + } + } catch (error) { + console.error(error); + const msg = formatMessage({ + id: "ngine.login-error", + description: "Login failed error message", + defaultMessage: "Could not sign in", + }); + toast.error((error as Error)?.message, msg); + console.error(error); + } finally { + setIsBusy(false); + } + } + + return ( + + + setUrl(ev.target.value)} + /> + + + + ); +} + export default function Login({ method, onLogin }: LoginProps) { if (method === "nip07") { return ; @@ -129,5 +192,9 @@ export default function Login({ method, onLogin }: LoginProps) { return ; } + if (method === "nip46") { + return ; + } + return null; } diff --git a/packages/core/src/components/LoginButton.tsx b/packages/core/src/components/LoginButton.tsx index 6666d47..ca3dc11 100644 --- a/packages/core/src/components/LoginButton.tsx +++ b/packages/core/src/components/LoginButton.tsx @@ -41,6 +41,24 @@ function LoginOption({ method, onLogin }: LoginProps) { )} + {method === "nip46" && ( + <> + + + + + + + + )} {method === "npub" && ( <> @@ -104,7 +122,7 @@ interface LoginButtonProps extends Pick { } export default function LoginButton({ - methods = ["nip07", "npub"], + methods = ["nip07", "nip46", "npub"], children, onLogin, size, diff --git a/packages/core/src/components/ReactionPicker.tsx b/packages/core/src/components/ReactionPicker.tsx index 9506a4a..e16d7e0 100644 --- a/packages/core/src/components/ReactionPicker.tsx +++ b/packages/core/src/components/ReactionPicker.tsx @@ -151,9 +151,9 @@ export default function ReactionPicker({ await event.react(emoji.native); const msg = formatMessage( { - id: "ngine.react-success", + id: "ngine.react-success-native", description: "Success message for reactions", - defaultMessage: "Reacted with {emoji}", + defaultMessage: "Reacted with { emoji }", }, { emoji: emoji.native }, ); diff --git a/packages/core/src/context.tsx b/packages/core/src/context.tsx index cbf7b4b..b5aa7f7 100644 --- a/packages/core/src/context.tsx +++ b/packages/core/src/context.tsx @@ -18,6 +18,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import NDK, { NDKKind, NDKNip07Signer, + NDKNip46Signer, NDKPrivateKeySigner, NDKUser, NostrEvent, @@ -39,6 +40,7 @@ const queryClient = new QueryClient(); interface NgineContextProps { ndk: NDK; nip07Login: () => Promise; + nip46Login: (url: string) => Promise; nsecLogin: (nsec: string) => Promise; npubLogin: (npub: string) => Promise; sign: ( @@ -54,6 +56,9 @@ const NgineContext = createContext({ nip07Login: () => { return Promise.reject(); }, + nip46Login: () => { + return Promise.reject(); + }, nsecLogin: () => { return Promise.reject(); }, @@ -160,7 +165,20 @@ export const NgineProvider = ({ } else if (session?.method === "nsec") { const signer = new NDKPrivateKeySigner(session.privkey); ndk.signer = signer; + } else if (session?.method === "nip46" && session.bunker) { + const { privkey, pubkey, relays } = session.bunker; + const localSigner = new NDKPrivateKeySigner(privkey); + const bunkerNDK = new NDK({ explicitRelayUrls: relays }); + bunkerNDK.connect().then(() => { + const signer = new NDKNip46Signer( + bunkerNDK, + session.pubkey, + localSigner, + ); + ndk.signer = signer; + }); } + // todo: nip05 }, [session]); async function nip07Login() { @@ -176,6 +194,32 @@ export const NgineProvider = ({ return user; } + async function nip46Login(url: string) { + const asURL = new URL(url); + const relays = asURL.searchParams.getAll("relay"); + const pubkey = asURL.pathname.replace(/^\/\//, ""); + const bunkerNDK = new NDK({ + explicitRelayUrls: relays, + }); + await bunkerNDK.connect(); + const localSigner = NDKPrivateKeySigner.generate(); + const signer = new NDKNip46Signer(bunkerNDK, pubkey, localSigner); + const user = await signer.blockUntilReady(); + if (user) { + ndk.signer = signer; + setSession({ + method: "nip46", + pubkey: user.pubkey, + bunker: { + privkey: localSigner.privateKey as string, + pubkey, + relays, + }, + }); + } + return user; + } + async function npubLogin(pubkey: string) { const user = ndk.getUser({ hexpubkey: pubkey }); setSession({ @@ -226,7 +270,16 @@ export const NgineProvider = ({ return ( { return context.npubLogin; }; +export const useBunkerLogin = () => { + const context = useContext(NgineContext); + if (context === undefined) { + throw new Error("Ngine context not found"); + } + return context.nip46Login; +}; + export const useNsecLogin = () => { const context = useContext(NgineContext); if (context === undefined) { diff --git a/packages/core/src/lang.json b/packages/core/src/lang.json index 561e467..85e9842 100644 --- a/packages/core/src/lang.json +++ b/packages/core/src/lang.json @@ -3,6 +3,14 @@ "defaultMessage": "Bookmarks ({count})", "description": "Bookmarks count" }, + "ngine.bunker-login": { + "defaultMessage": "Log in", + "description": "A button for logging in with a nsecbunker" + }, + "ngine.bunker-url-placeholder": { + "defaultMessage": "bunker://", + "description": "Bunker URL placeholder" + }, "ngine.comment-label": { "defaultMessage": "Comment", "description": "Comment field label" @@ -91,6 +99,10 @@ "defaultMessage": "You can use a nostr extension to log in to the site.", "description": "Description of extension login section" }, + "ngine.login-nip46": { + "defaultMessage": "Nsecbunker", + "description": "Title of nip46 login section" + }, "ngine.login-npub": { "defaultMessage": "Public key", "description": "Title of pubkey login section" @@ -99,6 +111,10 @@ "defaultMessage": "You can log in with a pubkey or nostr address for browsing the site in read-only mode.", "description": "Description of pubkey login section" }, + "ngine.login-nsecbunker-descr": { + "defaultMessage": "You can log in with nsecbunker URL and sign events remotely", + "description": "Description of nsecbunker login description" + }, "ngine.menu-reactions": { "defaultMessage": "Reactions", "description": "Reactions dialog opener" @@ -195,6 +211,10 @@ "defaultMessage": "Reacted with", "description": "Success message for reactions" }, + "ngine.react-success-native": { + "defaultMessage": "Reacted with {emoji}", + "description": "Success message for reactions" + }, "ngine.react-to": { "defaultMessage": "Reacting to", "description": "React modal title" diff --git a/packages/core/src/translations/en.json b/packages/core/src/translations/en.json index dd3c574..177c0f5 100644 --- a/packages/core/src/translations/en.json +++ b/packages/core/src/translations/en.json @@ -1,5 +1,7 @@ { "ngine.bookmarks-count": "Bookmarks ({count})", + "ngine.bunker-login": "Log in", + "ngine.bunker-url-placeholder": "bunker://", "ngine.comment-label": "Comment", "ngine.comment-placeholder": "Type your reply here", "ngine.copy": "Copy", @@ -22,8 +24,10 @@ "ngine.login-error": "Could not sign in", "ngine.login-nip07": "Nostr extension", "ngine.login-nip07-descr": "You can use a nostr extension to log in to the site.", + "ngine.login-nip46": "Nsecbunker", "ngine.login-npub": "Public key", "ngine.login-npub-descr": "You can log in with a pubkey or nostr address for browsing the site in read-only mode.", + "ngine.login-nsecbunker-descr": "You can log in with nsecbunker URL and sign events remotely", "ngine.menu-reactions": "Reactions", "ngine.npub-placeholder": "npub, nprofile or nostr address...", "ngine.onboarding.avatar": "Add a profile image", @@ -48,6 +52,7 @@ "ngine.pubkey-login": "Log in (read only)", "ngine.react-error": "Couldn't react", "ngine.react-success": "Reacted with", + "ngine.react-success-native": "Reacted with {emoji}", "ngine.react-to": "Reacting to", "ngine.react-with": "React with", "ngine.reactions-count": "Reactions ({count})", diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index e96ac67..91c3f6d 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -39,12 +39,18 @@ export interface Links { // Sessions -export type LoginMethod = "nip07" | "npub" | "nsec"; +// todo: nip05 with nip46 +export type LoginMethod = "nip07" | "nip46" | "npub" | "nsec"; export interface Session { method: LoginMethod; pubkey: string; privkey?: string; + bunker?: { + privkey: string; + pubkey: string; + relays: string[]; + }; } // Components diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a8dc3ca..9608d7c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -337,8 +337,8 @@ importers: specifier: ^1.3.3 version: 1.3.3 '@nostr-dev-kit/ndk': - specifier: ^2.3.0 - version: 2.3.0(typescript@5.3.2) + specifier: ^2.3.3 + version: 2.3.3(typescript@5.3.2) '@scure/base': specifier: ^1.1.5 version: 1.1.5 @@ -4592,6 +4592,25 @@ packages: - typescript dev: false + /@nostr-dev-kit/ndk@2.3.3(typescript@5.3.2): + resolution: {integrity: sha512-R2r6U1Xt4B7yygQFgTEexNqhuQQrbJ0Kxh4GvcCgNgSjMI+cPJQPWg4g4noWGRnaWf4epqLNCblfo5UfMuijTw==} + dependencies: + '@noble/hashes': 1.3.3 + '@noble/secp256k1': 2.0.0 + '@scure/base': 1.1.5 + debug: 4.3.4 + light-bolt11-decoder: 3.0.0 + node-fetch: 3.3.2 + nostr-tools: 1.17.0(typescript@5.3.2) + tseep: 1.1.3 + typescript-lru-cache: 2.0.0 + utf8-buffer: 1.0.0 + websocket-polyfill: 0.0.3 + transitivePeerDependencies: + - supports-color + - typescript + dev: false + /@npmcli/config@6.2.1: resolution: {integrity: sha512-Cj/OrSbrLvnwWuzquFCDTwFN8QmR+SWH6qLNCBttUreDkKM5D5p36SeSMbcEUiCGdwjUrVy2yd8C0REwwwDPEw==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}