diff --git a/packages/app/src/Element/Copy.tsx b/packages/app/src/Element/Copy.tsx
index 7d05ba66..837705a4 100644
--- a/packages/app/src/Element/Copy.tsx
+++ b/packages/app/src/Element/Copy.tsx
@@ -5,14 +5,15 @@ import { useCopy } from "useCopy";
export interface CopyProps {
text: string;
maxSize?: number;
+ className?: string;
}
-export default function Copy({ text, maxSize = 32 }: CopyProps) {
+export default function Copy({ text, maxSize = 32, className }: CopyProps) {
const { copy, copied } = useCopy();
const sliceLength = maxSize / 2;
const trimmed = text.length > maxSize ? `${text.slice(0, sliceLength)}...${text.slice(-sliceLength)}` : text;
return (
-
goToEvent(e, ev.Id, true)}>
+
goToEvent(e, unwrap(ev.Original), true)}>
{transformBody()}
{translation()}
{options.showReactionsLink && (
@@ -319,7 +330,7 @@ export default function Note(props: NoteProps) {
const note = (
goToEvent(e, ev.Id)}
+ onClick={e => goToEvent(e, unwrap(ev.Original))}
ref={ref}>
{content()}
diff --git a/packages/app/src/Element/NoteFooter.tsx b/packages/app/src/Element/NoteFooter.tsx
index 8bc36ae2..b1256f37 100644
--- a/packages/app/src/Element/NoteFooter.tsx
+++ b/packages/app/src/Element/NoteFooter.tsx
@@ -3,14 +3,14 @@ import { useSelector, useDispatch } from "react-redux";
import { useIntl, FormattedMessage } from "react-intl";
import { Menu, MenuItem } from "@szhsin/react-menu";
import { useLongPress } from "use-long-press";
-import { Event as NEvent, TaggedRawEvent, HexKey, u256 } from "@snort/nostr";
+import { Event as NEvent, TaggedRawEvent, HexKey, u256, encodeTLV, NostrPrefix } from "@snort/nostr";
import Icon from "Icons/Icon";
import Spinner from "Icons/Spinner";
import { formatShort } from "Number";
import useEventPublisher from "Feed/EventPublisher";
-import { bech32ToHex, delay, hexToBech32, normalizeReaction, unwrap } from "Util";
+import { bech32ToHex, delay, normalizeReaction, unwrap } from "Util";
import { NoteCreator } from "Element/NoteCreator";
import Reactions from "Element/Reactions";
import SendSats from "Element/SendSats";
@@ -263,7 +263,8 @@ export default function NoteFooter(props: NoteFooterProps) {
}
async function share() {
- const url = `${window.location.protocol}//${window.location.host}/e/${hexToBech32("note", ev.Id)}`;
+ const link = encodeTLV(ev.Id, NostrPrefix.Event, ev.Original?.relays);
+ const url = `${window.location.protocol}//${window.location.host}/e/${link}`;
if ("share" in window.navigator) {
await window.navigator.share({
title: "Snort",
@@ -298,7 +299,8 @@ export default function NoteFooter(props: NoteFooterProps) {
}
async function copyId() {
- await navigator.clipboard.writeText(hexToBech32("note", ev.Id));
+ const link = encodeTLV(ev.Id, NostrPrefix.Event, ev.Original?.relays);
+ await navigator.clipboard.writeText(link);
}
async function pin(id: HexKey) {
diff --git a/packages/app/src/Element/NoteReaction.tsx b/packages/app/src/Element/NoteReaction.tsx
index d4949783..a31ca7a3 100644
--- a/packages/app/src/Element/NoteReaction.tsx
+++ b/packages/app/src/Element/NoteReaction.tsx
@@ -2,7 +2,7 @@ import "./NoteReaction.css";
import { Link } from "react-router-dom";
import { useMemo } from "react";
-import { EventKind, Event as NEvent } from "@snort/nostr";
+import { EventKind, Event as NEvent, NostrPrefix } from "@snort/nostr";
import Note from "Element/Note";
import ProfileImage from "Element/ProfileImage";
import { eventLink, hexToBech32 } from "Util";
@@ -24,7 +24,7 @@ export default function NoteReaction(props: NoteReactionProps) {
if (ev) {
const eTags = ev.Tags.filter(a => a.Key === "e");
if (eTags.length > 0) {
- return eTags[0].Event;
+ return eTags[0];
}
}
return null;
@@ -34,7 +34,7 @@ export default function NoteReaction(props: NoteReactionProps) {
ev.Kind !== EventKind.Reaction &&
ev.Kind !== EventKind.Repost &&
(ev.Kind !== EventKind.TextNote ||
- ev.Tags.every((a, i) => a.Event !== refEvent || a.Marker !== "mention" || ev.Content !== `#[${i}]`))
+ ev.Tags.every((a, i) => a.Event !== refEvent?.Event || a.Marker !== "mention" || ev.Content !== `#[${i}]`))
) {
return null;
}
@@ -73,7 +73,9 @@ export default function NoteReaction(props: NoteReactionProps) {
{root ?
: null}
{!root && refEvent ? (
- #{hexToBech32("note", refEvent).substring(0, 12)}
+
+ #{hexToBech32(NostrPrefix.Event, refEvent.Event).substring(0, 12)}
+
) : null}
diff --git a/packages/app/src/Element/ProfileImage.tsx b/packages/app/src/Element/ProfileImage.tsx
index b382db19..55661053 100644
--- a/packages/app/src/Element/ProfileImage.tsx
+++ b/packages/app/src/Element/ProfileImage.tsx
@@ -6,7 +6,7 @@ import { useUserProfile } from "Hooks/useUserProfile";
import { hexToBech32, profileLink } from "Util";
import Avatar from "Element/Avatar";
import Nip05 from "Element/Nip05";
-import { HexKey } from "@snort/nostr";
+import { HexKey, NostrPrefix } from "@snort/nostr";
import { MetadataCache } from "State/Users";
import usePageWidth from "Hooks/usePageWidth";
@@ -77,7 +77,7 @@ export default function ProfileImage({
}
export function getDisplayName(user: MetadataCache | undefined, pubkey: HexKey) {
- let name = hexToBech32("npub", pubkey).substring(0, 12);
+ let name = hexToBech32(NostrPrefix.PublicKey, pubkey).substring(0, 12);
if (user?.display_name !== undefined && user.display_name.length > 0) {
name = user.display_name;
} else if (user?.name !== undefined && user.name.length > 0) {
diff --git a/packages/app/src/Element/Text.tsx b/packages/app/src/Element/Text.tsx
index 0e13507c..077546db 100644
--- a/packages/app/src/Element/Text.tsx
+++ b/packages/app/src/Element/Text.tsx
@@ -4,7 +4,7 @@ import { Link, useLocation } from "react-router-dom";
import ReactMarkdown from "react-markdown";
import { visit, SKIP } from "unist-util-visit";
import * as unist from "unist";
-import { HexKey, Tag } from "@snort/nostr";
+import { HexKey, NostrPrefix, Tag } from "@snort/nostr";
import { MentionRegex, InvoiceRegex, HashtagRegex, MagnetRegex } from "Const";
import { eventLink, hexToBech32, magnetURIDecode, splitByUrl, unwrap } from "Util";
@@ -35,7 +35,7 @@ export default function Text({ content, tags, creator }: TextProps) {
.map(f => {
if (typeof f === "string") {
return splitByUrl(f).map(a => {
- if (a.match(/^https?:\/\//)) {
+ if (a.match(/^(?:https?|(?:web\+)?nostr):/i)) {
return
;
}
return a;
@@ -77,13 +77,13 @@ export default function Text({ content, tags, creator }: TextProps) {
if (ref) {
switch (ref.Key) {
case "p": {
- return
;
+ return
;
}
case "e": {
- const eText = hexToBech32("note", ref.Event).substring(0, 12);
+ const eText = hexToBech32(NostrPrefix.Event, ref.Event).substring(0, 12);
return ref.Event ? (
e.stopPropagation()}
state={{ from: location.pathname }}>
#{eText}
diff --git a/packages/app/src/Element/Textarea.tsx b/packages/app/src/Element/Textarea.tsx
index 60ba2a3b..677c73d0 100644
--- a/packages/app/src/Element/Textarea.tsx
+++ b/packages/app/src/Element/Textarea.tsx
@@ -5,6 +5,7 @@ import { useIntl } from "react-intl";
import ReactTextareaAutocomplete from "@webscopeio/react-textarea-autocomplete";
import emoji from "@jukben/emoji-search";
import TextareaAutosize from "react-textarea-autosize";
+import { NostrPrefix } from "@snort/nostr";
import Avatar from "Element/Avatar";
import Nip05 from "Element/Nip05";
@@ -82,7 +83,7 @@ const Textarea = (props: TextareaProps) => {
afterWhitespace: true,
dataProvider: userDataProvider,
component: (props: { entity: MetadataCache }) =>
,
- output: (item: { pubkey: string }) => `@${hexToBech32("npub", item.pubkey)}`,
+ output: (item: { pubkey: string }) => `@${hexToBech32(NostrPrefix.PublicKey, item.pubkey)}`,
},
}}
/>
diff --git a/packages/app/src/Element/Thread.tsx b/packages/app/src/Element/Thread.tsx
index d1ba5895..7a2ca558 100644
--- a/packages/app/src/Element/Thread.tsx
+++ b/packages/app/src/Element/Thread.tsx
@@ -255,14 +255,14 @@ const TierThree = ({ active, path, isLastSubthread, from, notes, related, chains
export interface ThreadProps {
notes?: TaggedRawEvent[];
+ selected?: u256;
}
export default function Thread(props: ThreadProps) {
const notes = props.notes ?? [];
const parsedNotes = notes.map(a => new NEvent(a));
const [path, setPath] = useState
([]);
- const currentId = path.length > 0 && path[path.length - 1];
- const currentRoot = useMemo(() => parsedNotes.find(a => a.Id === currentId), [notes, currentId]);
+ const currentRoot = useMemo(() => parsedNotes.find(a => a.Id === props.selected), [notes, props.selected]);
const [navigated, setNavigated] = useState(false);
const navigate = useNavigate();
const isSingleNote = parsedNotes.filter(a => a.Kind === EventKind.TextNote).length === 1;
diff --git a/packages/app/src/Feed/ThreadFeed.ts b/packages/app/src/Feed/ThreadFeed.ts
index 73a6922b..9e9d05f5 100644
--- a/packages/app/src/Feed/ThreadFeed.ts
+++ b/packages/app/src/Feed/ThreadFeed.ts
@@ -5,10 +5,10 @@ import useSubscription from "Feed/Subscription";
import { useSelector } from "react-redux";
import { RootState } from "State/Store";
import { UserPreferences } from "State/Login";
-import { debounce } from "Util";
+import { debounce, NostrLink } from "Util";
-export default function useThreadFeed(id: u256) {
- const [trackingEvents, setTrackingEvent] = useState([id]);
+export default function useThreadFeed(link: NostrLink) {
+ const [trackingEvents, setTrackingEvent] = useState([link.id]);
const pref = useSelector(s => s.login.preferences);
function addId(id: u256[]) {
@@ -25,7 +25,7 @@ export default function useThreadFeed(id: u256) {
const sub = useMemo(() => {
const thisSub = new Subscriptions();
- thisSub.Id = `thread:${id.substring(0, 8)}`;
+ thisSub.Id = `thread:${link.id.substring(0, 8)}`;
thisSub.Ids = new Set(trackingEvents);
// get replies to this event
@@ -39,7 +39,7 @@ export default function useThreadFeed(id: u256) {
thisSub.AddSubscription(subRelated);
return thisSub;
- }, [trackingEvents, pref, id]);
+ }, [trackingEvents, pref, link.id]);
const main = useSubscription(sub, { leaveOpen: true, cache: true });
diff --git a/packages/app/src/Pages/EventPage.tsx b/packages/app/src/Pages/EventPage.tsx
index 753a41ee..b0d72430 100644
--- a/packages/app/src/Pages/EventPage.tsx
+++ b/packages/app/src/Pages/EventPage.tsx
@@ -1,12 +1,17 @@
import { useParams } from "react-router-dom";
+
import Thread from "Element/Thread";
import useThreadFeed from "Feed/ThreadFeed";
-import { parseId } from "Util";
+import { parseNostrLink, unwrap } from "Util";
export default function EventPage() {
const params = useParams();
- const id = parseId(params.id ?? "");
- const thread = useThreadFeed(id);
+ const link = parseNostrLink(params.id ?? "");
+ const thread = useThreadFeed(unwrap(link));
- return ;
+ if (link) {
+ return ;
+ } else {
+ return {params.id};
+ }
}
diff --git a/packages/app/src/Pages/Login.tsx b/packages/app/src/Pages/Login.tsx
index f27d37cc..1ca1b63b 100644
--- a/packages/app/src/Pages/Login.tsx
+++ b/packages/app/src/Pages/Login.tsx
@@ -10,7 +10,8 @@ import { HexKey } from "@snort/nostr";
import { RootState } from "State/Store";
import { setPrivateKey, setPublicKey, setRelays, setGeneratedPrivateKey } from "State/Login";
import { DefaultRelays, EmailRegex, MnemonicRegex } from "Const";
-import { bech32ToHex, generateBip39Entropy, entropyToDerivedKey, unwrap } from "Util";
+import { bech32ToHex, unwrap } from "Util";
+import { generateBip39Entropy, entropyToDerivedKey } from "nip6";
import ZapButton from "Element/ZapButton";
import useImgProxy from "Hooks/useImgProxy";
diff --git a/packages/app/src/Pages/NostrLinkHandler.tsx b/packages/app/src/Pages/NostrLinkHandler.tsx
index b61e5f51..b60e47cc 100644
--- a/packages/app/src/Pages/NostrLinkHandler.tsx
+++ b/packages/app/src/Pages/NostrLinkHandler.tsx
@@ -1,9 +1,10 @@
-import { decodeTLV, NostrPrefix, TLVEntryType } from "@snort/nostr";
+import { NostrPrefix } from "@snort/nostr";
import { useEffect } from "react";
import { useDispatch } from "react-redux";
import { useNavigate, useParams } from "react-router-dom";
+
import { setRelays } from "State/Login";
-import { eventLink, profileLink } from "Util";
+import { parseNostrLink, unixNowMs, unwrap } from "Util";
export default function NostrLinkHandler() {
const params = useParams();
@@ -13,29 +14,21 @@ export default function NostrLinkHandler() {
useEffect(() => {
if (link.length > 0) {
- const entity = link.startsWith("web+nostr:") ? link.split(":")[1] : link;
- if (entity.startsWith(NostrPrefix.PublicKey)) {
- navigate(`/p/${entity}`);
- } else if (entity.startsWith(NostrPrefix.Note)) {
- navigate(`/e/${entity}`);
- } else if (entity.startsWith(NostrPrefix.Profile) || entity.startsWith(NostrPrefix.Event)) {
- const decoded = decodeTLV(entity);
- console.debug(decoded);
-
- const id = decoded.find(a => a.type === TLVEntryType.Special)?.value as string;
- const relays = decoded.filter(a => a.type === TLVEntryType.Relay);
- if (relays.length > 0) {
- const relayObj = {
- relays: Object.fromEntries(relays.map(a => [a.value, { read: true, write: false }])),
- createdAt: new Date().getTime(),
- };
- dispatch(setRelays(relayObj));
+ const nav = parseNostrLink(link);
+ if (nav) {
+ if ((nav.relays?.length ?? 0) > 0) {
+ // todo: add as ephemerial connection
+ dispatch(
+ setRelays({
+ relays: Object.fromEntries(unwrap(nav.relays).map(a => [a, { read: true, write: false }])),
+ createdAt: unixNowMs(),
+ })
+ );
}
-
- if (entity.startsWith(NostrPrefix.Profile)) {
- navigate(profileLink(id));
- } else if (entity.startsWith(NostrPrefix.Event)) {
- navigate(eventLink(id));
+ if (nav.type === NostrPrefix.Event || nav.type === NostrPrefix.Note || nav.type === NostrPrefix.Address) {
+ navigate(`/e/${nav.encode()}`);
+ } else if (nav.type === NostrPrefix.PublicKey || nav.type === NostrPrefix.Profile) {
+ navigate(`/p/${nav.encode()}`);
}
}
}
diff --git a/packages/app/src/Pages/ProfilePage.tsx b/packages/app/src/Pages/ProfilePage.tsx
index d56d4f71..d8b51118 100644
--- a/packages/app/src/Pages/ProfilePage.tsx
+++ b/packages/app/src/Pages/ProfilePage.tsx
@@ -3,9 +3,9 @@ import { useEffect, useState } from "react";
import { useIntl, FormattedMessage } from "react-intl";
import { useSelector } from "react-redux";
import { useNavigate, useParams } from "react-router-dom";
-import { NostrPrefix } from "@snort/nostr";
+import { encodeTLV, NostrPrefix } from "@snort/nostr";
-import { unwrap } from "Util";
+import { parseNostrLink, unwrap } from "Util";
import { formatShort } from "Number";
import Note from "Element/Note";
import Bookmarks from "Element/Bookmarks";
@@ -73,7 +73,7 @@ export default function ProfilePage() {
tags: [],
creator: "",
});
- const npub = !id?.startsWith("npub") ? hexToBech32("npub", id || undefined) : id;
+ const npub = !id?.startsWith(NostrPrefix.PublicKey) ? hexToBech32(NostrPrefix.PublicKey, id || undefined) : id;
const lnurl = extractLnAddress(user?.lud16 || user?.lud06 || "");
const website_url =
@@ -116,7 +116,13 @@ export default function ProfilePage() {
setId(a);
});
} else {
- setId(parseId(params.id ?? ""));
+ const nav = parseNostrLink(params.id ?? "");
+ if (nav?.type === NostrPrefix.PublicKey || nav?.type === NostrPrefix.Profile) {
+ // todo: use relays if any for nprofile
+ setId(nav.id);
+ } else {
+ setId(parseId(params.id ?? ""));
+ }
}
setTab(ProfileTab.Notes);
}, [params]);
@@ -252,6 +258,10 @@ export default function ProfilePage() {
}
function renderIcons() {
+ if (!id) return;
+
+ const firstRelay = relays.find(a => a.settings.write)?.url;
+ const link = encodeTLV(id, NostrPrefix.Profile, firstRelay ? [firstRelay] : undefined);
return (
setShowProfileQr(true)}>
@@ -259,13 +269,9 @@ export default function ProfilePage() {
{showProfileQr && (
setShowProfileQr(false)}>
-
-
-
+
+
+
)}
{isMe ? (
@@ -295,12 +301,13 @@ export default function ProfilePage() {
}
function userDetails() {
+ if (!id) return;
return (
{username()}
{renderIcons()}
- {!isMe && }
+ {!isMe && }
{bio()}
diff --git a/packages/app/src/Pages/new/NewUserFlow.tsx b/packages/app/src/Pages/new/NewUserFlow.tsx
index 6e5b3e80..891ea943 100644
--- a/packages/app/src/Pages/new/NewUserFlow.tsx
+++ b/packages/app/src/Pages/new/NewUserFlow.tsx
@@ -6,7 +6,8 @@ import Logo from "Element/Logo";
import { CollapsedSection } from "Element/Collapsed";
import Copy from "Element/Copy";
import { RootState } from "State/Store";
-import { hexToBech32, hexToMnemonic } from "Util";
+import { hexToBech32 } from "Util";
+import { hexToMnemonic } from "nip6";
import messages from "./messages";
diff --git a/packages/app/src/Util.test.ts b/packages/app/src/Util.test.ts
index a70c34a3..7745f888 100644
--- a/packages/app/src/Util.test.ts
+++ b/packages/app/src/Util.test.ts
@@ -31,6 +31,21 @@ describe("splitByUrl", () => {
expect(splitByUrl(inputStr)).toEqual(expectedOutput);
});
+ it("should parse nostr links", () => {
+ const input =
+ "web+nostr:npub1v0lxxxxutpvrelsksy8cdhgfux9l6a42hsj2qzquu2zk7vc9qnkszrqj49\nnostr:note1jp6d36lmquhxqn2s5n4ce00pzu2jrpkek8udav6l0y3qcdngpnxsle6ngm\nnostr:naddr1qqv8x6r0wf6x2um594cxzarg946x7ttpwajhxmmdv5pzqx78pgq53vlnzmdr8l3u38eru0n3438lnxqz0mr39wg9e5j0dfq3qvzqqqr4gu5d05rr\nnostr is cool";
+ const expected = [
+ "",
+ "web+nostr:npub1v0lxxxxutpvrelsksy8cdhgfux9l6a42hsj2qzquu2zk7vc9qnkszrqj49",
+ "\n",
+ "nostr:note1jp6d36lmquhxqn2s5n4ce00pzu2jrpkek8udav6l0y3qcdngpnxsle6ngm",
+ "\n",
+ "nostr:naddr1qqv8x6r0wf6x2um594cxzarg946x7ttpwajhxmmdv5pzqx78pgq53vlnzmdr8l3u38eru0n3438lnxqz0mr39wg9e5j0dfq3qvzqqqr4gu5d05rr",
+ "\nnostr is cool",
+ ];
+ expect(splitByUrl(input)).toEqual(expected);
+ });
+
it("should return an array with a single string if no URLs are found", () => {
const inputStr = "This is a regular string with no URLs";
const expectedOutput = ["This is a regular string with no URLs"];
diff --git a/packages/app/src/Util.ts b/packages/app/src/Util.ts
index ab493195..53940f38 100644
--- a/packages/app/src/Util.ts
+++ b/packages/app/src/Util.ts
@@ -5,12 +5,7 @@ import { bytesToHex } from "@noble/hashes/utils";
import { decode as invoiceDecode } from "light-bolt11-decoder";
import { bech32 } from "bech32";
import base32Decode from "base32-decode";
-import { HexKey, TaggedRawEvent, u256, EventKind, encodeTLV, NostrPrefix } from "@snort/nostr";
-import * as bip39 from "@scure/bip39";
-import { wordlist } from "@scure/bip39/wordlists/english";
-import { HDKey } from "@scure/bip32";
-
-import { DerivationPath } from "Const";
+import { HexKey, TaggedRawEvent, u256, EventKind, encodeTLV, NostrPrefix, decodeTLV, TLVEntryType } from "@snort/nostr";
import { MetadataCache } from "State/Users";
export const sha256 = (str: string) => {
@@ -80,8 +75,21 @@ export function bech32ToText(str: string) {
* @param hex
* @returns
*/
-export function eventLink(hex: u256) {
- return `/e/${hexToBech32(NostrPrefix.Note, hex)}`;
+export function eventLink(hex: u256, relays?: Array
| string) {
+ const encoded = relays
+ ? encodeTLV(hex, NostrPrefix.Event, Array.isArray(relays) ? relays : [relays])
+ : hexToBech32(NostrPrefix.Note, hex);
+ return `/e/${encoded}`;
+}
+
+/**
+ * Convert hex pubkey to bech32 link url
+ */
+export function profileLink(hex: HexKey, relays?: Array | string) {
+ const encoded = relays
+ ? encodeTLV(hex, NostrPrefix.Profile, Array.isArray(relays) ? relays : [relays])
+ : hexToBech32(NostrPrefix.PublicKey, hex);
+ return `/p/${encoded}`;
}
/**
@@ -105,46 +113,6 @@ export function hexToBech32(hrp: string, hex?: string) {
}
}
-export function generateBip39Entropy(mnemonic?: string): Uint8Array {
- try {
- const mn = mnemonic ?? bip39.generateMnemonic(wordlist, 256);
- return bip39.mnemonicToEntropy(mn, wordlist);
- } catch (e) {
- throw new Error("INVALID MNEMONIC PHRASE");
- }
-}
-
-/**
- * Convert hex-encoded entropy into mnemonic phrase
- */
-export function hexToMnemonic(hex: string): string {
- const bytes = secp.utils.hexToBytes(hex);
- return bip39.entropyToMnemonic(bytes, wordlist);
-}
-
-/**
- * Convert mnemonic phrase into hex-encoded private key
- * using the derivation path specified in NIP06
- * @param mnemonic the mnemonic-encoded entropy
- */
-export function entropyToDerivedKey(entropy: Uint8Array): string {
- const masterKey = HDKey.fromMasterSeed(entropy);
- const newKey = masterKey.derive(DerivationPath);
-
- if (!newKey.privateKey) {
- throw new Error("INVALID KEY DERIVATION");
- }
-
- return secp.utils.bytesToHex(newKey.privateKey);
-}
-
-/**
- * Convert hex pubkey to bech32 link url
- */
-export function profileLink(hex: HexKey) {
- return `/p/${hexToBech32(NostrPrefix.PublicKey, hex)}`;
-}
-
/**
* Reaction types
*/
@@ -275,7 +243,7 @@ export function groupByPubkey(acc: Record, user: Metadata
export function splitByUrl(str: string) {
const urlRegex =
- /((?:http|ftp|https):\/\/(?:[\w+?.\w+])+(?:[a-zA-Z0-9~!@#$%^&*()_\-=+\\/?.:;',]*)?(?:[-A-Za-z0-9+&@#/%=~_|]))/i;
+ /((?:http|ftp|https|nostr|web\+nostr):\/?\/?(?:[\w+?.\w+])+(?:[a-zA-Z0-9~!@#$%^&*()_\-=+\\/?.:;',]*)?(?:[-A-Za-z0-9+&@#/%=~_|]))/i;
return str.split(urlRegex);
}
@@ -467,3 +435,75 @@ export function getRelayName(url: string) {
const parsedUrl = new URL(url);
return parsedUrl.host + parsedUrl.search;
}
+
+export interface NostrLink {
+ type: NostrPrefix;
+ id: string;
+ kind?: number;
+ author?: string;
+ relays?: Array;
+ encode(): string;
+}
+
+export function parseNostrLink(link: string): NostrLink | undefined {
+ const entity = link.startsWith("web+nostr:") || link.startsWith("nostr:") ? link.split(":")[1] : link;
+
+ if (entity.startsWith(NostrPrefix.PublicKey)) {
+ const id = bech32ToHex(entity);
+ return {
+ type: NostrPrefix.PublicKey,
+ id: id,
+ encode: () => hexToBech32(NostrPrefix.PublicKey, id),
+ };
+ } else if (entity.startsWith(NostrPrefix.Note)) {
+ const id = bech32ToHex(entity);
+ return {
+ type: NostrPrefix.Note,
+ id: id,
+ encode: () => hexToBech32(NostrPrefix.Note, id),
+ };
+ } else if (
+ entity.startsWith(NostrPrefix.Profile) ||
+ entity.startsWith(NostrPrefix.Event) ||
+ entity.startsWith(NostrPrefix.Address)
+ ) {
+ const decoded = decodeTLV(entity);
+
+ const id = decoded.find(a => a.type === TLVEntryType.Special)?.value as string;
+ const relays = decoded.filter(a => a.type === TLVEntryType.Relay).map(a => a.value as string);
+ const author = decoded.find(a => a.type === TLVEntryType.Author)?.value as string;
+ const kind = decoded.find(a => a.type === TLVEntryType.Kind)?.value as number;
+
+ const encode = () => {
+ return entity; // return original
+ };
+ if (entity.startsWith(NostrPrefix.Profile)) {
+ return {
+ type: NostrPrefix.Profile,
+ id,
+ relays,
+ kind,
+ author,
+ encode,
+ };
+ } else if (entity.startsWith(NostrPrefix.Event)) {
+ return {
+ type: NostrPrefix.Event,
+ id,
+ relays,
+ kind,
+ author,
+ encode,
+ };
+ } else if (entity.startsWith(NostrPrefix.Address)) {
+ return {
+ type: NostrPrefix.Address,
+ id,
+ relays,
+ kind,
+ author,
+ encode,
+ };
+ }
+ }
+}
diff --git a/packages/app/src/nip6.ts b/packages/app/src/nip6.ts
new file mode 100644
index 00000000..89639c4c
--- /dev/null
+++ b/packages/app/src/nip6.ts
@@ -0,0 +1,39 @@
+import * as secp from "@noble/secp256k1";
+import * as bip39 from "@scure/bip39";
+import { wordlist } from "@scure/bip39/wordlists/english";
+import { HDKey } from "@scure/bip32";
+
+import { DerivationPath } from "Const";
+
+export function generateBip39Entropy(mnemonic?: string): Uint8Array {
+ try {
+ const mn = mnemonic ?? bip39.generateMnemonic(wordlist, 256);
+ return bip39.mnemonicToEntropy(mn, wordlist);
+ } catch (e) {
+ throw new Error("INVALID MNEMONIC PHRASE");
+ }
+}
+
+/**
+ * Convert hex-encoded entropy into mnemonic phrase
+ */
+export function hexToMnemonic(hex: string): string {
+ const bytes = secp.utils.hexToBytes(hex);
+ return bip39.entropyToMnemonic(bytes, wordlist);
+}
+
+/**
+ * Convert mnemonic phrase into hex-encoded private key
+ * using the derivation path specified in NIP06
+ * @param mnemonic the mnemonic-encoded entropy
+ */
+export function entropyToDerivedKey(entropy: Uint8Array): string {
+ const masterKey = HDKey.fromMasterSeed(entropy);
+ const newKey = masterKey.derive(DerivationPath);
+
+ if (!newKey.privateKey) {
+ throw new Error("INVALID KEY DERIVATION");
+ }
+
+ return secp.utils.bytesToHex(newKey.privateKey);
+}
diff --git a/packages/nostr/src/legacy/Links.ts b/packages/nostr/src/legacy/Links.ts
index 4b5a0d94..3b315ba0 100644
--- a/packages/nostr/src/legacy/Links.ts
+++ b/packages/nostr/src/legacy/Links.ts
@@ -11,6 +11,7 @@ export enum NostrPrefix {
Profile = "nprofile",
Event = "nevent",
Relay = "nrelay",
+ Address = "naddr",
}
export enum TLVEntryType {
@@ -43,7 +44,7 @@ export function encodeTLV(hex: string, prefix: NostrPrefix, relays?: string[]) {
})
.flat() ?? [];
- return bech32.encode(prefix, bech32.toWords([...tl0, ...tl1]));
+ return bech32.encode(prefix, bech32.toWords([...tl0, ...tl1]), 1_000);
}
export function decodeTLV(str: string) {