From a7ead3901596431bf0bdd3226182cc831c1f4204 Mon Sep 17 00:00:00 2001 From: Martti Malmi Date: Mon, 21 Aug 2023 22:43:03 +0300 Subject: [PATCH] Hex, PublicKey and EventID classes --- src/js/components/searchbox/SearchBox.tsx | 2 +- src/js/nostr/Key.ts | 6 +- src/js/utils/Hex.test.ts | 57 +++++++++++++++ src/js/utils/Hex.ts | 88 +++++++++++++++++++++++ src/js/views/Note.tsx | 4 +- src/js/views/profile/Profile.tsx | 32 ++++----- 6 files changed, 167 insertions(+), 22 deletions(-) create mode 100644 src/js/utils/Hex.test.ts create mode 100644 src/js/utils/Hex.ts diff --git a/src/js/components/searchbox/SearchBox.tsx b/src/js/components/searchbox/SearchBox.tsx index 02ab01d89..39667aa02 100644 --- a/src/js/components/searchbox/SearchBox.tsx +++ b/src/js/components/searchbox/SearchBox.tsx @@ -183,7 +183,7 @@ class SearchBox extends Component { Key.getPubKeyByNip05Address(query).then((pubKey) => { // if query hasn't changed since we started the request if (pubKey && query === String(this.props.query || this.inputRef.current.value)) { - this.props.onSelect?.({ key: pubKey }); + this.props.onSelect?.({ key: pubKey.toHex() }); } }); } diff --git a/src/js/nostr/Key.ts b/src/js/nostr/Key.ts index d2f4f88fb..b93dc71ce 100644 --- a/src/js/nostr/Key.ts +++ b/src/js/nostr/Key.ts @@ -9,6 +9,8 @@ import { } from 'nostr-tools'; import { route } from 'preact-router'; +import { PublicKey } from '@/utils/Hex.ts'; + import localState from '../state/LocalState.ts'; import Helpers from '../utils/Helpers'; @@ -193,14 +195,14 @@ export default { console.error(e); } }, - async getPubKeyByNip05Address(address: string): Promise { + async getPubKeyByNip05Address(address: string): Promise { try { const [localPart, domain] = address.split('@'); const url = `https://${domain}/.well-known/nostr.json?name=${localPart}`; const response = await fetch(url); const json = await response.json(); const names = json.names; - return names[localPart] || null; + return new PublicKey(names[localPart]) || null; } catch (error) { console.error(error); return null; diff --git a/src/js/utils/Hex.test.ts b/src/js/utils/Hex.test.ts new file mode 100644 index 000000000..b48c69406 --- /dev/null +++ b/src/js/utils/Hex.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, it } from 'vitest'; + +import { EventID, PublicKey } from '@/utils/Hex'; + +describe('PublicKey', () => { + it('should convert npub bech32 to hex', () => { + const bech32 = 'npub1g53mukxnjkcmr94fhryzkqutdz2ukq4ks0gvy5af25rgmwsl4ngq43drvk'; + const hex = '4523be58d395b1b196a9b8c82b038b6895cb02b683d0c253a955068dba1facd0'; + const publicKey = new PublicKey(bech32); + expect(publicKey.toHex()).toEqual(hex); + expect(publicKey.toBech32()).toEqual(bech32); + }); + + it('should init from hex', () => { + const hex = '4523be58d395b1b196a9b8c82b038b6895cb02b683d0c253a955068dba1facd0'; + const publicKey = new PublicKey(hex); + expect(publicKey.toHex()).toEqual(hex); + expect(publicKey.toBech32()).toEqual( + 'npub1g53mukxnjkcmr94fhryzkqutdz2ukq4ks0gvy5af25rgmwsl4ngq43drvk', + ); + }); + + it('should fail with too long hex', () => { + const hex = + '4523be58d395b1b196a9b8c82b038b6895cb02b683d0c253a955068dba1facd04523be58d395b1b196a9b8c82b038b6895cb02b683d0c253a955068dba1facd0'; + expect(() => new PublicKey(hex)).toThrow(); + }); + + it('equals(hexStr)', () => { + const hex = '4523be58d395b1b196a9b8c82b038b6895cb02b683d0c253a955068dba1facd0'; + const publicKey = new PublicKey(hex); + expect(publicKey.equals(hex)).toEqual(true); + }); + + it('equals(PublicKey)', () => { + const hex = '4523be58d395b1b196a9b8c82b038b6895cb02b683d0c253a955068dba1facd0'; + const publicKey = new PublicKey(hex); + const publicKey2 = new PublicKey(hex); + expect(publicKey.equals(publicKey2)).toEqual(true); + }); + + it('equals(bech32)', () => { + const bech32 = 'npub1g53mukxnjkcmr94fhryzkqutdz2ukq4ks0gvy5af25rgmwsl4ngq43drvk'; + const publicKey = new PublicKey(bech32); + expect(publicKey.equals(bech32)).toEqual(true); + }); +}); + +describe('EventID', () => { + it('should convert note id bech32 to hex', () => { + const noteBech32 = 'note1wdyajan9c9d72wanqe2l34lxgdu3q5esglhquusfkg34fqq6462qh4cjd5'; + const noteHex = '7349d97665c15be53bb30655f8d7e6437910533047ee0e7209b22354801aae94'; + const eventId = new EventID(noteBech32); + expect(eventId.toHex()).toEqual(noteHex); + expect(eventId.toBech32()).toEqual(noteBech32); + }); +}); diff --git a/src/js/utils/Hex.ts b/src/js/utils/Hex.ts new file mode 100644 index 000000000..155479854 --- /dev/null +++ b/src/js/utils/Hex.ts @@ -0,0 +1,88 @@ +import * as bech32 from 'bech32-buffer'; + +import Helpers from '@/utils/Helpers.tsx'; + +function bech32ToHex(str: string): string { + try { + const { data } = bech32.decode(str); + const addr = Helpers.arrayToHex(data); + return addr; + } catch (e) { + throw new Error('The provided string is not a valid bech32 address: ' + str); + } +} + +export class Hex { + value: string; + + constructor(str: string, expectedLength?: number) { + this.validateHex(str, expectedLength); + this.value = str; + } + + private validateHex(str: string, expectedLength?: number): void { + if (!/^[0-9a-fA-F]+$/.test(str)) { + throw new Error(`The provided string is not a valid hex value: "${str}"`); + } + + if (expectedLength && str.length !== expectedLength) { + throw new Error( + `The provided hex value does not match the expected length of ${expectedLength} characters: ${str}`, + ); + } + } + + toBech32(prefix: string): string { + if (!prefix) { + throw new Error('prefix is required'); + } + + const bytesArray = this.value.match(/.{1,2}/g); + const bytes = new Uint8Array(bytesArray!.map((byte) => parseInt(byte, 16))); + return bech32.encode(prefix, bytes); + } + + toHex(): string { + return this.value; + } +} + +export class EventID extends Hex { + constructor(str: string) { + if (str.startsWith('note')) { + str = bech32ToHex(str); + } + super(str, 64); + } + + toBech32(): string { + return super.toBech32('note'); + } + + equals(other: EventID | string): boolean { + if (typeof other === 'string') { + other = new EventID(other); + } + return this.value === other.value; + } +} + +export class PublicKey extends Hex { + constructor(str: string) { + if (str.startsWith('npub')) { + str = bech32ToHex(str); + } + super(str, 64); + } + + toBech32(): string { + return super.toBech32('npub'); + } + + equals(other: PublicKey | string): boolean { + if (typeof other === 'string') { + other = new PublicKey(other); + } + return this.value === other.value; + } +} diff --git a/src/js/views/Note.tsx b/src/js/views/Note.tsx index 79467a116..8f76631d1 100644 --- a/src/js/views/Note.tsx +++ b/src/js/views/Note.tsx @@ -1,16 +1,16 @@ import { useEffect } from 'preact/hooks'; import { route } from 'preact-router'; +import { EventID } from '@/utils/Hex.ts'; import View from '@/views/View.tsx'; import CreateNoteForm from '../components/create/CreateNoteForm'; import EventComponent from '../components/events/EventComponent'; -import Key from '../nostr/Key'; import { translate as t } from '../translations/Translation.mjs'; const Note = (props) => { useEffect(() => { - const nostrBech32Id = Key.toNostrBech32Address(props.id, 'note'); + const nostrBech32Id = new EventID(props.id).toBech32(); if (nostrBech32Id && props.id !== nostrBech32Id) { route(`/${nostrBech32Id}`, true); return; diff --git a/src/js/views/profile/Profile.tsx b/src/js/views/profile/Profile.tsx index d69ddbf03..351231fd9 100644 --- a/src/js/views/profile/Profile.tsx +++ b/src/js/views/profile/Profile.tsx @@ -5,6 +5,7 @@ import SimpleImageModal from '@/components/modal/Image.tsx'; import { useProfile } from '@/nostr/hooks/useProfile.ts'; import { getEventReplyingTo, isRepost } from '@/nostr/utils.ts'; import useLocalState from '@/state/useLocalState.ts'; +import { PublicKey } from '@/utils/Hex.ts'; import ProfileHelmet from '@/views/profile/Helmet.tsx'; import Feed from '../../components/feed/Feed.tsx'; @@ -60,21 +61,20 @@ function Profile(props) { }, [profile]); useEffect(() => { - const pub = props.id; - const npubComputed = Key.toNostrBech32Address(pub, 'npub'); + try { + const pub = new PublicKey(props.id); + const npubComputed = pub.toBech32(); - if (npubComputed && npubComputed !== pub) { - route(`/${npubComputed}`, true); - return; - } + if (npubComputed !== props.id) { + route(`/${npubComputed}`, true); + return; + } - const hexPubComputed = Key.toNostrHexAddress(pub) || ''; + setHexPub(pub.toHex()); + setNpub(npubComputed); + } catch (e) { + let nostrAddress = props.id; - if (hexPubComputed) { - setHexPub(hexPubComputed); - setNpub(Key.toNostrBech32Address(hexPubComputed, 'npub') || ''); - } else { - let nostrAddress = pub; if (!nostrAddress.match(/.+@.+\..+/)) { if (nostrAddress.match(/.+\..+/)) { nostrAddress = '_@' + nostrAddress; @@ -85,11 +85,8 @@ function Profile(props) { Key.getPubKeyByNip05Address(nostrAddress).then((pubKey) => { if (pubKey) { - const npubComputed = Key.toNostrBech32Address(pubKey, 'npub'); - if (npubComputed && npubComputed !== pubKey) { - setNpub(npubComputed); - setHexPub(pubKey); - } + setNpub(pubKey.toBech32()); + setHexPub(pubKey.toHex()); } else { setNpub(''); // To indicate not found } @@ -99,6 +96,7 @@ function Profile(props) { setTimeout(() => { window.prerenderReady = true; }, 1000); + return () => { setIsMyProfile(false); };