From 1b379abd346dc9dd272f738c113611c0d423a4de Mon Sep 17 00:00:00 2001 From: smolgrrr Date: Thu, 14 Sep 2023 22:26:16 +1000 Subject: [PATCH] Revert "add a bunch of random shit from nostr.ch" This reverts commit 7dd6769a3637ce10131b327513e999654fe978c4. --- client/src/components/Home.tsx | 36 +-- client/src/components/PostCard/PostCard.tsx | 7 +- client/src/constants/settings.ts | 4 - client/src/func/events.ts | 64 ------ client/src/func/main.ts | 188 ---------------- client/src/func/relays.ts | 111 --------- client/src/func/settings.ts | 236 -------------------- client/src/func/subscriptions.ts | 136 ----------- client/src/func/system.ts | 124 ---------- client/src/func/worker.js | 42 ---- client/src/utils/crypto.ts | 25 --- 11 files changed, 3 insertions(+), 970 deletions(-) delete mode 100644 client/src/constants/settings.ts delete mode 100644 client/src/func/events.ts delete mode 100644 client/src/func/main.ts delete mode 100644 client/src/func/relays.ts delete mode 100644 client/src/func/settings.ts delete mode 100644 client/src/func/subscriptions.ts delete mode 100644 client/src/func/system.ts delete mode 100644 client/src/func/worker.js delete mode 100644 client/src/utils/crypto.ts diff --git a/client/src/components/Home.tsx b/client/src/components/Home.tsx index 90ecb2a..83a329c 100644 --- a/client/src/components/Home.tsx +++ b/client/src/components/Home.tsx @@ -4,8 +4,6 @@ import PostCard from './PostCard/PostCard'; import Header from './Header/Header'; import NewThreadCard from './PostCard/NewThreadCard'; -let filterDifficulty: number = 1; - // Define the Event interface interface Event { id: string; @@ -23,47 +21,17 @@ const Home = () => { useEffect(() => { relay.on('connect', async () => { console.log(`connected to ${relay.url}`); - console.log(Math.floor(Date.now() - (30 * 24 * 60 * 60 * 1000))) - - // Pad the ID with leading zeros based on filterDifficulty - const paddedId = "0".padStart(filterDifficulty + 1, '0'); - console.log(paddedId); const eventList = await relay.list([ { - ids: ['00'], //prefix number of leading zeros from filterDifficulty + ids: ['0000'], kinds: [1], - //until: Date.now(), limit: 10, - //since: Math.floor(Date.now() - (30 * 24 * 60 * 60 * 1000)), // 24 hours ago }, ]); - // const socket = new WebSocket('wss://relay.damus.io'); - // socket.onopen = () => { - // console.log('WebSocket connected'); - // const subscription = [ - // 'REQ', - // 'POW-TEST', - // { - // ids: ["0000"], - // limit: 10, - // }, - // ]; - // socket.send(JSON.stringify(subscription)); - // }; - - // let i = 0; - // let start = Date.now(); - // socket.onmessage = event => { - // console.log(event.data); - // }; - // socket.onerror = error => { - // console.error('WebSocket error:', error); - // }; // Assuming eventList is of type Event[] setEvents(eventList); - console.log(eventList); }); relay.on('error', () => { @@ -79,7 +47,7 @@ const Home = () => {
{events.map((event, index) => ( - + ))}
diff --git a/client/src/components/PostCard/PostCard.tsx b/client/src/components/PostCard/PostCard.tsx index 43e4446..5fe46ca 100644 --- a/client/src/components/PostCard/PostCard.tsx +++ b/client/src/components/PostCard/PostCard.tsx @@ -1,16 +1,11 @@ import CardContainer from './CardContainer'; -interface PostCardProps { - content: string; - time: Date; -} +const PostCard = ({ content }: { content: string }) => { -const PostCard = ({ content, time }: PostCardProps) => { return ( <>
- {time.toString()}
{content}
diff --git a/client/src/constants/settings.ts b/client/src/constants/settings.ts deleted file mode 100644 index ada2d67..0000000 --- a/client/src/constants/settings.ts +++ /dev/null @@ -1,4 +0,0 @@ -let filterDifficulty: number = 0; -let difficulty: number = 16; -let timeout: number = 5; - diff --git a/client/src/func/events.ts b/client/src/func/events.ts deleted file mode 100644 index f410469..0000000 --- a/client/src/func/events.ts +++ /dev/null @@ -1,64 +0,0 @@ -import {Event} from 'nostr-tools'; -import {zeroLeadingBitsCount} from '../utils/crypto'; - -export const isEvent = (evt?: T): evt is T => evt !== undefined; -export const isMention = ([tag, , , marker]: string[]) => tag === 'e' && marker === 'mention'; -export const isPTag = ([tag]: string[]) => tag === 'p'; -export const hasEventTag = (tag: string[]) => tag[0] === 'e'; -export const isNotNonceTag = ([tag]: string[]) => tag !== 'nonce'; - -/** - * validate proof-of-work of a nostr event per nip-13. - * the validation always requires difficulty commitment in the nonce tag. - * - * @param {EventObj} evt event to validate - * TODO: @param {number} targetDifficulty target proof-of-work difficulty - */ -export const validatePow = (evt: Event) => { - const tag = evt.tags.find(tag => tag[0] === 'nonce'); - if (!tag) { - return false; - } - const difficultyCommitment = Number(tag[2]); - if (!difficultyCommitment || Number.isNaN(difficultyCommitment)) { - return false; - } - return zeroLeadingBitsCount(evt.id) >= difficultyCommitment; -} - -export const sortByCreatedAt = (evt1: Event, evt2: Event) => { - if (evt1.created_at === evt2.created_at) { - // console.log('TODO: OMG exactly at the same time, figure out how to sort then', evt1, evt2); - } - return evt1.created_at > evt2.created_at ? -1 : 1; -}; - -export const sortEventCreatedAt = (created_at: number) => ( - {created_at: a}: Event, - {created_at: b}: Event, -) => ( - Math.abs(a - created_at) < Math.abs(b - created_at) ? -1 : 1 -); - -const isReply = ([tag, , , marker]: string[]) => tag === 'e' && marker !== 'mention'; - -/** - * find reply-to ID according to nip-10, find marked reply or root tag or - * fallback to positional (last) e tag or return null - * @param {event} evt - * @returns replyToID | null - */ -export const getReplyTo = (evt: Event): string | null => { - const eventTags = evt.tags.filter(isReply); - const withReplyMarker = eventTags.filter(([, , , marker]) => marker === 'reply'); - if (withReplyMarker.length === 1) { - return withReplyMarker[0][1]; - } - const withRootMarker = eventTags.filter(([, , , marker]) => marker === 'root'); - if (withReplyMarker.length === 0 && withRootMarker.length === 1) { - return withRootMarker[0][1]; - } - // fallback to deprecated positional 'e' tags (nip-10) - const lastTag = eventTags.at(-1); - return lastTag ? lastTag[1] : null; -}; diff --git a/client/src/func/main.ts b/client/src/func/main.ts deleted file mode 100644 index 3b7ad7f..0000000 --- a/client/src/func/main.ts +++ /dev/null @@ -1,188 +0,0 @@ -import {Event, nip19} from 'nostr-tools'; -import {zeroLeadingBitsCount} from './utils/crypto'; -import {elem} from './utils/dom'; -import {bounce} from './utils/time'; -import {isWssUrl} from './utils/url'; -import {closeSettingsView, config, toggleSettingsView} from './settings'; -import {subGlobalFeed, subEventID, subNote} from './subscriptions' -import {getReplyTo, hasEventTag, isEvent, isMention, sortByCreatedAt, sortEventCreatedAt} from './events'; -import {clearView, getViewContent, getViewElem, getViewOptions, setViewElem, view} from './view'; -import {EventWithNip19, EventWithNip19AndReplyTo, textNoteList, replyList} from './notes'; -import {createContact, createTextNote, renderEventDetails, renderRecommendServer, renderUpdateContact} from './ui'; - -// curl -H 'accept: application/nostr+json' https://relay.nostr.ch/ - -type EventRelayMap = { - [eventId: string]: string[]; -}; -const eventRelayMap: EventRelayMap = {}; // eventId: [relay1, relay2] - -const renderNote = ( - evt: EventWithNip19, - i: number, - sortedFeeds: EventWithNip19[], -) => { - if (getViewElem(evt.id)) { // note already in view - return; - } - const article = createTextNote(evt, eventRelayMap[evt.id][0]); - if (i === 0) { - getViewContent().append(article); - } else { - getViewElem(sortedFeeds[i - 1].id).before(article); - } - setViewElem(evt.id, article); -}; - -const hasEnoughPOW = ( - [tag, , commitment]: string[], - eventId: string -) => { - return tag === 'nonce' && Number(commitment) >= config.filterDifficulty && zeroLeadingBitsCount(eventId) >= config.filterDifficulty; -}; - -const renderFeed = bounce(() => { - const view = getViewOptions(); - switch (view.type) { - case 'note': - textNoteList - .concat(replyList) // search id in notes and replies - .filter(note => note.id === view.id) - .forEach(renderNote); - break; - case 'feed': - const now = Math.floor(Date.now() * 0.001); - textNoteList - .filter(note => { - // dont render notes from the future - if (note.created_at > now) return false; - // if difficulty filter is configured dont render notes with too little pow - return !config.filterDifficulty || note.tags.some(tag => hasEnoughPOW(tag, note.id)) - }) - .sort(sortByCreatedAt) - .reverse() - .forEach(renderNote); - break; - } -}, 17); // (16.666 rounded, an arbitrary value to limit updates to max 60x per s) - -const renderReply = (evt: EventWithNip19AndReplyTo) => { - const parent = getViewElem(evt.replyTo); - if (!parent || getViewElem(evt.id)) { - return; - } - let replyContainer = parent.querySelector('.mbox-replies'); - if (!replyContainer) { - replyContainer = elem('div', {className: 'mbox-replies'}); - parent.append(replyContainer); - parent.classList.add('mbox-has-replies'); - } - const reply = createTextNote(evt, eventRelayMap[evt.id][0]); - replyContainer.append(reply); - setViewElem(evt.id, reply); -}; - -const handleReply = (evt: EventWithNip19, relay: string) => { - if ( - getViewElem(evt.id) // already rendered probably received from another relay - || evt.tags.some(isMention) // ignore mentions for now - ) { - return; - } - const replyTo = getReplyTo(evt); - if (!replyTo) { - return; - } - const evtWithReplyTo = {replyTo, ...evt}; - replyList.push(evtWithReplyTo); - renderReply(evtWithReplyTo); -}; - -const handleTextNote = (evt: Event, relay: string) => { - if (evt.content.startsWith('vmess://') && !evt.content.includes(' ')) { - console.info('drop VMESS encrypted message'); - return; - } - if (eventRelayMap[evt.id]) { - eventRelayMap[evt.id] = [...(eventRelayMap[evt.id]), relay]; // TODO: remove eventRelayMap and just check for getViewElem? - } else { - eventRelayMap[evt.id] = [relay]; - const evtWithNip19 = { - nip19: { - note: nip19.noteEncode(evt.id), - npub: nip19.npubEncode(evt.pubkey), - }, - ...evt, - }; - if (evt.tags.some(hasEventTag)) { - handleReply(evtWithNip19, relay); - } else { - textNoteList.push(evtWithNip19); - } - } - if (!getViewElem(evt.id)) { - renderFeed(); - } -}; - -const rerenderFeed = () => { - clearView(); - renderFeed(); -}; -config.rerenderFeed = rerenderFeed; - -const onEvent = (evt: Event, relay: string) => { - switch (evt.kind) { - case 0: - handleTextNote(evt, relay); - break; - default: - // console.log(`TODO: add support for event kind ${evt.kind}`/*, evt*/) - } -}; - -// subscribe and change view -const route = (path: string) => { - if (path === '/') { - subGlobalFeed(onEvent); - view('/feed', {type: 'feed'}); - return; - } - if (path === '/feed') { - subGlobalFeed(onEvent); - view('/feed', {type: 'feed'}); - } else if (path.length === 64 && path.match(/^\/[0-9a-z]+$/)) { - const {type, data} = nip19.decode(path.slice(1)); - if (typeof data !== 'string') { - console.warn('nip19 ProfilePointer, EventPointer and AddressPointer are not yet supported'); - return; - } - switch(type) { - case 'note': - subNote(data, onEvent); - view(path, {type: 'note', id: data}); - break; - default: - console.warn(`type ${type} not yet supported`); - } - renderFeed(); - } else if (path.length === 65) { - const eventID = path.slice(1); - subEventID(eventID, onEventDetails); - view(path, {type: 'event', id: eventID}); - } else { - console.warn('no support for ', path); - } -}; - -// onload -route(location.pathname); - -// only push a new entry if there is no history onload -if (!history.length) { - history.pushState({}, '', location.pathname); -} - -window.addEventListener('popstate', (event) => { - route(location.pathname); -}); diff --git a/client/src/func/relays.ts b/client/src/func/relays.ts deleted file mode 100644 index a934344..0000000 --- a/client/src/func/relays.ts +++ /dev/null @@ -1,111 +0,0 @@ -import {Event, Filter, relayInit, Relay, Sub} from 'nostr-tools'; - -type SubCallback = ( - event: Readonly, - relay: Readonly, -) => void; - -type Subscribe = { - cb: SubCallback; - filter: Filter; - unsub?: boolean; -}; - -const subList: Array = []; -const currentSubList: Array = []; -const relayMap = new Map(); - -export const addRelay = async (url: string) => { - const relay = relayInit(url); - relay.on('connect', () => { - console.info(`connected to ${relay.url}`); - }); - relay.on('error', () => { - console.warn(`failed to connect to ${relay.url}`); - }); - try { - await relay.connect(); - currentSubList.forEach(({cb, filter}) => subscribe(cb, filter, relay)); - relayMap.set(url, relay); - } catch { - console.warn(`could not connect to ${url}`); - } -}; - -export const unsubscribe = (sub: Sub) => { - sub.unsub(); - subList.splice(subList.indexOf(sub), 1); -}; - -const subscribe = ( - cb: SubCallback, - filter: Filter, - relay: Relay, - unsub?: boolean -) => { - const sub = relay.sub([filter]); - subList.push(sub); - sub.on('event', (event: Event) => { - cb(event, relay.url); - }); - if (unsub) { - sub.on('eose', () => { - // console.log('eose', relay.url); - unsubscribe(sub); - }); - } - return sub; -}; - -export const sub = (obj: Subscribe) => { - currentSubList.push(obj); - relayMap.forEach((relay) => subscribe(obj.cb, obj.filter, relay, obj.unsub)); -}; - -export const subOnce = ( - obj: Subscribe & {relay: string} -) => { - const relay = relayMap.get(obj.relay); - if (relay) { - const sub = subscribe(obj.cb, obj.filter, relay); - sub.on('eose', () => { - // console.log('eose', obj.relay); - unsubscribe(sub); - }); - } -}; - -export const unsubAll = () => { - subList.forEach(unsubscribe); - currentSubList.length = 0; -}; - -type PublishCallback = ( - relay: string, - errorMessage?: string, -) => void; - -export const publish = ( - event: Event, - cb: PublishCallback, -) => { - relayMap.forEach((relay, url) => { - const pub = relay.publish(event); - pub.on('ok', () => { - console.info(`${relay.url} has accepted our event`); - cb(relay.url); - }); - pub.on('failed', (reason: any) => { - console.error(`failed to publish to ${relay.url}: ${reason}`); - cb(relay.url, reason); - }); - }); -}; - - -addRelay('wss://relay.snort.social'); -addRelay('wss://nostr.bitcoiner.social'); -addRelay('wss://nostr.mom'); -addRelay('wss://relay.nostr.bg'); -addRelay('wss://nos.lol'); -// addRelay('wss://relay.nostr.ch'); diff --git a/client/src/func/settings.ts b/client/src/func/settings.ts deleted file mode 100644 index a26504c..0000000 --- a/client/src/func/settings.ts +++ /dev/null @@ -1,236 +0,0 @@ -import {generatePrivateKey, getPublicKey, signEvent} from 'nostr-tools'; -import {updateElemHeight} from './utils/dom'; -import {powEvent} from './system'; -import {publish} from './relays'; - -const settingsView = document.querySelector('#settings') as HTMLElement; - -export const closeSettingsView = () => settingsView.hidden = true; - -export const toggleSettingsView = () => settingsView.hidden = !settingsView.hidden; - -let pubkey: string = ''; - -const loadOrGenerateKeys = () => { - const storedPubKey = localStorage.getItem('pub_key'); - if (storedPubKey) { - return storedPubKey; - } - const privatekey = generatePrivateKey(); - const pubkey = getPublicKey(privatekey); - localStorage.setItem('private_key', privatekey); - localStorage.setItem('pub_key', pubkey); - return pubkey; -}; - -let filterDifficulty: number = 0; -let difficulty: number = 16; -let timeout: number = 5; -let rerenderFeed: (() => void) | undefined; - -/** - * global config object - * config.pubkey, if not set loaded from localStorage or generate a new key - */ -export const config = { - get pubkey() { - if (!pubkey) { - pubkey = loadOrGenerateKeys(); - } - return pubkey; - }, - set pubkey(value) { - console.info(`pubkey was set to ${value}`); - pubkey = value; - }, - get filterDifficulty() { - return filterDifficulty; - }, - get difficulty() { - return difficulty; - }, - get timeout() { - return timeout; - }, - set rerenderFeed(value: () => void) { - rerenderFeed = value; - } -}; - -const getNumberFromStorage = ( - item: string, - fallback: number, -) => { - const stored = localStorage.getItem(item); - if (!stored) { - return fallback; - } - return Number(stored); -}; - -// filter difficulty -const filterDifficultyInput = document.querySelector('#filterDifficulty') as HTMLInputElement; -const filterDifficultyDisplay = document.querySelector('[data-display="filter_difficulty"]') as HTMLElement; -filterDifficultyInput.addEventListener('input', (e) => { - localStorage.setItem('filter_difficulty', filterDifficultyInput.value); - filterDifficulty = filterDifficultyInput.valueAsNumber; - filterDifficultyDisplay.textContent = filterDifficultyInput.value; - rerenderFeed && rerenderFeed(); -}); -filterDifficulty = getNumberFromStorage('filter_difficulty', 0); -filterDifficultyInput.valueAsNumber = filterDifficulty; -filterDifficultyDisplay.textContent = filterDifficultyInput.value; - -// mining difficulty target -const miningTargetInput = document.querySelector('#miningTarget') as HTMLInputElement; -miningTargetInput.addEventListener('input', (e) => { - localStorage.setItem('mining_target', miningTargetInput.value); - difficulty = miningTargetInput.valueAsNumber; -}); -// arbitrary difficulty default, still experimenting. -difficulty = getNumberFromStorage('mining_target', 16); -miningTargetInput.valueAsNumber = difficulty; - -// mining timeout -const miningTimeoutInput = document.querySelector('#miningTimeout') as HTMLInputElement; -miningTimeoutInput.addEventListener('input', (e) => { - localStorage.setItem('mining_timeout', miningTimeoutInput.value); - timeout = miningTimeoutInput.valueAsNumber; -}); -timeout = getNumberFromStorage('mining_timeout', 5); -miningTimeoutInput.valueAsNumber = timeout; - - -// settings -const settingsForm = document.querySelector('form[name="settings"]') as HTMLFormElement; -const privateKeyInput = settingsForm.querySelector('#privatekey') as HTMLInputElement; -const pubKeyInput = settingsForm.querySelector('#pubkey') as HTMLInputElement; -const statusMessage = settingsForm.querySelector('#keystatus') as HTMLElement; -const generateBtn = settingsForm.querySelector('button[name="generate"]') as HTMLButtonElement; -const importBtn = settingsForm.querySelector('button[name="import"]') as HTMLButtonElement; -const privateTgl = settingsForm.querySelector('button[name="privatekey-toggle"]') as HTMLButtonElement; - -const validKeys = ( - privatekey: string, - pubkey: string, -) => { - try { - if (getPublicKey(privatekey) === pubkey) { - statusMessage.hidden = true; - statusMessage.textContent = 'public-key corresponds to private-key'; - importBtn.removeAttribute('disabled'); - return true; - } else { - statusMessage.textContent = 'private-key does not correspond to public-key!' - } - } catch (e) { - statusMessage.textContent = `not a valid private-key: ${e.message || e}`; - } - statusMessage.hidden = false; - importBtn.disabled = true; - return false; -}; - -generateBtn.addEventListener('click', () => { - const privatekey = generatePrivateKey(); - const pubkey = getPublicKey(privatekey); - if (validKeys(privatekey, pubkey)) { - privateKeyInput.value = privatekey; - pubKeyInput.value = pubkey; - statusMessage.textContent = 'private-key created!'; - statusMessage.hidden = false; - } -}); - -importBtn.addEventListener('click', () => { - const privatekey = privateKeyInput.value; - const pubkeyInput = pubKeyInput.value; - if (validKeys(privatekey, pubkeyInput)) { - localStorage.setItem('private_key', privatekey); - localStorage.setItem('pub_key', pubkeyInput); - statusMessage.textContent = 'stored private and public key locally!'; - statusMessage.hidden = false; - config.pubkey = pubkeyInput; - } -}); - -settingsForm.addEventListener('input', () => validKeys(privateKeyInput.value, pubKeyInput.value)); - -privateKeyInput.addEventListener('paste', (event) => { - if (pubKeyInput.value || !event.clipboardData) { - return; - } - if (privateKeyInput.value === '' || ( // either privatekey field is empty - privateKeyInput.selectionStart === 0 // or the whole text is selected and replaced with the clipboard - && privateKeyInput.selectionEnd === privateKeyInput.value.length - )) { // only generate the pubkey if no data other than the text from clipboard will be used - try { - pubKeyInput.value = getPublicKey(event.clipboardData.getData('text')); - } catch(err) {} // settings form will call validKeys on input and display the error - } -}); - -privateTgl.addEventListener('click', () => { - privateKeyInput.type = privateKeyInput.type === 'text' ? 'password' : 'text'; -}); - -privateKeyInput.value = localStorage.getItem('private_key') || ''; -pubKeyInput.value = localStorage.getItem('pub_key') || ''; - -// profile -const profileForm = document.querySelector('form[name="profile"]') as HTMLFormElement; -const profileSubmit = profileForm.querySelector('button[type="submit"]') as HTMLButtonElement; -const profileStatus = document.querySelector('#profilestatus') as HTMLElement; - -profileForm.addEventListener('input', (e) => { - if (e.target instanceof HTMLElement) { - if (e.target?.nodeName === 'TEXTAREA') { - updateElemHeight(e.target as HTMLTextAreaElement); - } - } - const form = new FormData(profileForm); - const name = form.get('name'); - const about = form.get('about'); - const picture = form.get('picture'); - profileSubmit.disabled = !(name || about || picture); -}); - -profileForm.addEventListener('submit', async (e) => { - e.preventDefault(); - const form = new FormData(profileForm); - const newProfile = await powEvent({ - kind: 0, - pubkey: config.pubkey, - content: JSON.stringify(Object.fromEntries(form)), - tags: [], - created_at: Math.floor(Date.now() * 0.001) - }, { - difficulty: config.difficulty, - statusElem: profileStatus, - timeout: config.timeout, - }).catch(console.warn); - if (!newProfile) { - profileStatus.textContent = 'publishing profile data canceled'; - profileStatus.hidden = false; - return; - } - const privatekey = localStorage.getItem('private_key'); - if (!privatekey) { - profileStatus.textContent = 'no private key to sign'; - profileStatus.hidden = false; - return; - } - const sig = signEvent(newProfile, privatekey); - // TODO: validateEvent - if (sig) { - publish({...newProfile, sig}, (relay, error) => { - if (error) { - return console.error(error, relay); - } - console.info(`publish request sent to ${relay}`); - profileStatus.textContent = 'profile successfully published'; - profileStatus.hidden = false; - profileSubmit.disabled = true; - }); - } -}); diff --git a/client/src/func/subscriptions.ts b/client/src/func/subscriptions.ts deleted file mode 100644 index d47777f..0000000 --- a/client/src/func/subscriptions.ts +++ /dev/null @@ -1,136 +0,0 @@ -import {Event} from 'nostr-tools'; -import {getReplyTo, hasEventTag, isMention, isPTag} from './events'; -import {config} from './settings'; -import {sub, subOnce, unsubAll} from './relays'; - -type SubCallback = ( - event: Event, - relay: string, -) => void; - -/** subscribe to global feed */ -export const subGlobalFeed = (onEvent: SubCallback) => { - console.info('subscribe to global feed'); - unsubAll(); - const now = Math.floor(Date.now() * 0.001); - const pubkeys = new Set(); - const notes = new Set(); - const prefix = Math.floor(config.filterDifficulty / 4); // 4 bits in each '0' character - sub({ // get past events - cb: (evt, relay) => { - pubkeys.add(evt.pubkey); - notes.add(evt.id); - onEvent(evt, relay); - }, - filter: { - ...(prefix && {ids: ['0'.repeat(prefix)]}), - kinds: [1], - until: now, - ...(!prefix && {since: Math.floor(now - (24 * 60 * 60))}), - limit: 100, - }, - unsub: true - }); - - setTimeout(() => { - // get profile info - sub({ - cb: onEvent, - filter: { - authors: Array.from(pubkeys), - kinds: [0], - limit: pubkeys.size, - }, - unsub: true, - }); - pubkeys.clear(); - - notes.clear(); - }, 2000); - - // subscribe to future notes, reactions and profile updates - sub({ - cb: (evt, relay) => { - onEvent(evt, relay); - if ( - evt.kind !== 1 - || pubkeys.has(evt.pubkey) - ) { - return; - } - }, - filter: { - ...(prefix && {ids: ['0'.repeat(prefix)]}), - kinds: [1], - since: now, - }, - }); -}; - -/** subscribe to a note id (nip-19) */ -export const subNote = ( - eventId: string, - onEvent: SubCallback, -) => { - unsubAll(); - sub({ - cb: onEvent, - filter: { - ids: [eventId], - kinds: [1], - limit: 1, - }, - unsub: true, - }); - - const replies = new Set(); - - const onReply = (evt: Event, relay: string) => { - replies.add(evt.id) - onEvent(evt, relay); - unsubAll(); - sub({ - cb: onEvent, - filter: { - '#e': Array.from(replies), - kinds: [1, 7], - }, - unsub: true, - }); - }; - - replies.add(eventId); - setTimeout(() => { - sub({ - cb: onReply, - filter: { - '#e': [eventId], - kinds: [1, 7], - }, - unsub: true, // TODO: probably keep this subscription also after onReply/unsubAll - }); - }, 200); -}; - -export const subEventID = ( - id: string, - onEvent: SubCallback, -) => { - unsubAll(); - sub({ - cb: onEvent, - filter: { - ids: [id], - limit: 1, - }, - unsub: true, - }); - sub({ - cb: onEvent, - filter: { - authors: [id], - limit: 200, - }, - unsub: true, - }); -}; diff --git a/client/src/func/system.ts b/client/src/func/system.ts deleted file mode 100644 index 1bb2b96..0000000 --- a/client/src/func/system.ts +++ /dev/null @@ -1,124 +0,0 @@ -import {Event, getEventHash, UnsignedEvent} from 'nostr-tools'; -import {elem, lockScroll, unlockScroll} from './utils/dom'; - -const errorOverlay = document.querySelector('section#errorOverlay') as HTMLElement; - -type PromptErrorOptions = { - onCancel?: () => void; - onRetry?: () => void; -}; - -/** - * Creates an error overlay, currently with hardcoded POW related message, this could be come a generic prompt - * @param error message - * @param options {onRetry, onCancel} callbacks - */ -const promptError = ( - error: string, - options: PromptErrorOptions, -) => { - const {onCancel, onRetry} = options; - lockScroll(); - errorOverlay.replaceChildren( - elem('h1', {className: 'error-title'}, error), - elem('p', {}, 'time ran out finding a proof with the desired mining difficulty. either try again, lower the mining difficulty or increase the timeout in profile settings.'), - elem('div', {className: 'buttons'}, [ - onCancel ? elem('button', {data: {action: 'close'}}, 'close') : '', - onRetry ? elem('button', {data: {action: 'again'}}, 'try again') : '', - ]), - ); - const handleOverlayClick = (e: MouseEvent) => { - if (e.target instanceof Element) { - const button = e.target.closest('button'); - if (button) { - switch(button.dataset.action) { - case 'close': - onCancel && onCancel(); - break; - case 'again': - onRetry && onRetry(); - break; - } - errorOverlay.removeEventListener('click', handleOverlayClick); - errorOverlay.hidden = true; - unlockScroll(); - } - } - }; - errorOverlay.addEventListener('click', handleOverlayClick); - errorOverlay.hidden = false; -} - -type PowEventOptions = { - difficulty: number; - statusElem: HTMLElement; - timeout: number; -}; - -type WorkerResponse = { - error: string; - event: Event; -}; - -type HashedEvent = UnsignedEvent & { - id: string; -}; - -/** - * run proof of work in a worker until at least the specified difficulty. - * if succcessful, the returned event contains the 'nonce' tag - * and the updated created_at timestamp. - * - * powEvent returns a rejected promise if the funtion runs for longer than timeout. - * a zero timeout makes mineEvent run without a time limit. - * a zero mining target just resolves the promise without trying to find a 'nonce'. - */ -export const powEvent = ( - evt: UnsignedEvent, - options: PowEventOptions -): Promise => { - const {difficulty, statusElem, timeout} = options; - if (difficulty === 0) { - return Promise.resolve({ - ...evt, - id: getEventHash(evt), - }); - } - const cancelBtn = elem('button', {className: 'btn-inline'}, [elem('small', {}, 'cancel')]); - statusElem.replaceChildren('working…', cancelBtn); - statusElem.hidden = false; - return new Promise((resolve, reject) => { - const worker = new Worker('/worker.js'); - - const onCancel = () => { - worker.terminate(); - reject(`mining kind ${evt.kind} event canceled`); - }; - cancelBtn.addEventListener('click', onCancel); - - worker.onmessage = (msg: MessageEvent) => { - worker.terminate(); - cancelBtn.removeEventListener('click', onCancel); - if (msg.data.error) { - promptError(msg.data.error, { - onCancel: () => reject(`mining kind ${evt.kind} event canceled`), - onRetry: async () => { - const result = await powEvent(evt, {difficulty, statusElem, timeout}).catch(console.warn); - resolve(result); - } - }) - } else { - resolve(msg.data.event); - } - }; - - worker.onerror = (err) => { - worker.terminate(); - // promptError(msg.data.error, {}); - cancelBtn.removeEventListener('click', onCancel); - reject(err); - }; - - worker.postMessage({event: evt, difficulty, timeout}); - }); -}; diff --git a/client/src/func/worker.js b/client/src/func/worker.js deleted file mode 100644 index c9007b2..0000000 --- a/client/src/func/worker.js +++ /dev/null @@ -1,42 +0,0 @@ -import {getEventHash} from 'nostr-tools'; -import {zeroLeadingBitsCount} from './utils/crypto'; - -const mine = (event, difficulty, timeout = 5) => { - const max = 256; // arbitrary - if (!Number.isInteger(difficulty) || difficulty < 0 || difficulty > max) { - throw new Error(`difficulty must be an integer between 0 and ${max}`); - } - // continue with mining - let n = BigInt(0); - event.tags.unshift(['nonce', n.toString(), `${difficulty}`]); - - const until = Math.floor(Date.now() * 0.001) + timeout; - console.time('pow'); - while (true) { - const now = Math.floor(Date.now() * 0.001); - if (timeout !== 0 && (now > until)) { - console.timeEnd('pow'); - throw 'timeout'; - } - if (now !== event.created_at) { - event.created_at = now; - // n = BigInt(0); // could reset nonce as we have a new timestamp - } - event.tags[0][1] = (++n).toString(); - const id = getEventHash(event); - if (zeroLeadingBitsCount(id) === difficulty) { - console.timeEnd('pow'); - return {id, ...event}; - } - } -}; - -addEventListener('message', (msg) => { - const {difficulty, event, timeout} = msg.data; - try { - const minedEvent = mine(event, difficulty, timeout); - postMessage({event: minedEvent}); - } catch (err) { - postMessage({error: err}); - } -}); \ No newline at end of file diff --git a/client/src/utils/crypto.ts b/client/src/utils/crypto.ts deleted file mode 100644 index 8ea6b85..0000000 --- a/client/src/utils/crypto.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * evaluate the difficulty of hex32 according to nip-13. - * @param hex32 a string of 64 chars - 32 bytes in hex representation - */ -export const zeroLeadingBitsCount = (hex32: string) => { - let count = 0; - for (let i = 0; i < 64; i += 2) { - const hexbyte = hex32.slice(i, i + 2); // grab next byte - if (hexbyte === '00') { - count += 8; - continue; - } - // reached non-zero byte; count number of 0 bits in hexbyte - const bits = parseInt(hexbyte, 16).toString(2).padStart(8, '0'); - for (let b = 0; b < 8; b++) { - if (bits[b] === '1' ) { - break; // reached non-zero bit; stop - } - count += 1; - } - break; - } - return count; - }; - \ No newline at end of file