Hex, PublicKey and EventID classes

This commit is contained in:
Martti Malmi 2023-08-21 22:43:03 +03:00
parent de72141343
commit a7ead39015
6 changed files with 167 additions and 22 deletions

View File

@ -183,7 +183,7 @@ class SearchBox extends Component<Props, State> {
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() });
}
});
}

View File

@ -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<string | null> {
async getPubKeyByNip05Address(address: string): Promise<PublicKey | null> {
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;

57
src/js/utils/Hex.test.ts Normal file
View File

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

88
src/js/utils/Hex.ts Normal file
View File

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

View File

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

View File

@ -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) {
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);
};