refactor: type events.ts, url.ts and crypto.ts

pull/72/head
OFF0 2 years ago
parent fa97027321
commit 2d46687e12
Signed by: offbyn
GPG Key ID: 94A2F643C51F37FA

@ -0,0 +1,61 @@
import {Event} from 'nostr-tools';
import {zeroLeadingBitsCount} from './utils';
export const isMention = ([tag, , , marker]: string[]) => tag === 'e' && marker === 'mention';
export const hasEventTag = (tag: string[]) => tag[0] === 'e';
/**
* 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;
};

@ -1,9 +1,9 @@
import {generatePrivateKey, getEventHash, getPublicKey, nip19, signEvent} from 'nostr-tools'; import {generatePrivateKey, getEventHash, getPublicKey, nip19, signEvent} from 'nostr-tools';
import {sub24hFeed, subNote, subProfile} from './subscriptions' import {sub24hFeed, subNote, subProfile} from './subscriptions'
import {publish} from './relays'; import {publish} from './relays';
import {getReplyTo, hasEventTag, isMention, sortByCreatedAt, sortEventCreatedAt, validatePow} from './events';
import {clearView, getViewContent, getViewElem, setViewElem, view} from './view'; import {clearView, getViewContent, getViewElem, setViewElem, view} from './view';
import {zeroLeadingBitsCount} from './cryptoutils.js'; import {bounce, dateTime, elem, formatTime, getHost, getNoxyUrl, isWssUrl, parseTextContent, zeroLeadingBitsCount} from './utils';
import {bounce, dateTime, elem, formatTime, parseTextContent} from './utils';
// curl -H 'accept: application/nostr+json' https://relay.nostr.ch/ // curl -H 'accept: application/nostr+json' https://relay.nostr.ch/
function onEvent(evt, relay) { function onEvent(evt, relay) {
@ -39,12 +39,6 @@ let pubkey = localStorage.getItem('pub_key') || (() => {
const textNoteList = []; // could use indexDB const textNoteList = []; // could use indexDB
const eventRelayMap = {}; // eventId: [relay1, relay2] const eventRelayMap = {}; // eventId: [relay1, relay2]
const hasEventTag = tag => tag[0] === 'e';
const isReply = ([tag, , , marker]) => tag === 'e' && marker !== 'mention';
const isMention = ([tag, , , marker]) => tag === 'e' && marker === 'mention';
const hasEnoughPOW = ([tag, , commitment]) => {
return tag === 'nonce' && commitment >= fitlerDifficulty && zeroLeadingBitsCount(note.id) >= fitlerDifficulty;
};
const renderNote = (evt, i, sortedFeeds) => { const renderNote = (evt, i, sortedFeeds) => {
if (getViewElem(evt.id)) { // note already in view if (getViewElem(evt.id)) { // note already in view
return; return;
@ -58,13 +52,17 @@ const renderNote = (evt, i, sortedFeeds) => {
setViewElem(evt.id, article); setViewElem(evt.id, article);
}; };
const hasEnoughPOW = ([tag, , commitment], eventId) => {
return tag === 'nonce' && commitment >= fitlerDifficulty && zeroLeadingBitsCount(eventId) >= fitlerDifficulty;
};
const renderFeed = bounce(() => { const renderFeed = bounce(() => {
const now = Math.floor(Date.now() * 0.001); const now = Math.floor(Date.now() * 0.001);
textNoteList textNoteList
// dont render notes from the future // dont render notes from the future
.filter(note => note.created_at <= now) .filter(note => note.created_at <= now)
// if difficulty filter is configured dont render notes with too little pow // if difficulty filter is configured dont render notes with too little pow
.filter(note => !fitlerDifficulty || note.tags.some(hasEnoughPOW)) .filter(note => !fitlerDifficulty || note.tags.some(tag => hasEnoughPOW(tag, note.id)))
.sort(sortByCreatedAt) .sort(sortByCreatedAt)
.reverse() .reverse()
.forEach(renderNote); .forEach(renderNote);
@ -161,13 +159,6 @@ function handleReaction(evt, relay) {
const restoredReplyTo = localStorage.getItem('reply_to'); const restoredReplyTo = localStorage.getItem('reply_to');
const sortByCreatedAt = (evt1, evt2) => {
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;
};
function rerenderFeed() { function rerenderFeed() {
clearView(); clearView();
renderFeed(); renderFeed();
@ -179,17 +170,6 @@ setInterval(() => {
}); });
}, 10000); }, 10000);
const getNoxyUrl = (type, url, id, relay) => {
if (!isHttpUrl(url)) {
return false;
}
const link = new URL(`https://noxy.nostr.ch/${type}`);
link.searchParams.set('id', id);
link.searchParams.set('relay', relay);
link.searchParams.set('url', url);
return link;
}
const fetchQue = []; const fetchQue = [];
let fetchPending; let fetchPending;
const fetchNext = (href, id, relay) => { const fetchNext = (href, id, relay) => {
@ -300,21 +280,6 @@ function createTextNote(evt, relay) {
], {data: {id: evt.id, pubkey: evt.pubkey, relay}}); ], {data: {id: evt.id, pubkey: evt.pubkey, relay}});
} }
const sortEventCreatedAt = (created_at) => (
{created_at: a},
{created_at: b},
) => (
Math.abs(a - created_at) < Math.abs(b - created_at) ? -1 : 1
);
function isWssUrl(string) {
try {
return 'wss:' === new URL(string).protocol;
} catch (err) {
return false;
}
}
function handleRecommendServer(evt, relay) { function handleRecommendServer(evt, relay) {
if (getViewElem(evt.id) || !isWssUrl(evt.content)) { if (getViewElem(evt.id) || !isWssUrl(evt.content)) {
return; return;
@ -450,22 +415,6 @@ function setMetadata(evt, relay, content) {
// } // }
} }
function isHttpUrl(string) {
try {
return ['http:', 'https:'].includes(new URL(string).protocol);
} catch (err) {
return false;
}
}
const getHost = (url) => {
try {
return new URL(url).host;
} catch(err) {
return err;
}
}
const elemCanvas = (text) => { const elemCanvas = (text) => {
const canvas = elem('canvas', {height: 80, width: 80, data: {pubkey: text}}); const canvas = elem('canvas', {height: 80, width: 80, data: {pubkey: text}});
const context = canvas.getContext('2d'); const context = canvas.getContext('2d');
@ -499,26 +448,6 @@ function getMetadata(evt, relay) {
return {host, img, name, time, userName}; return {host, img, name, time, userName};
} }
/**
* 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
*/
function getReplyTo(evt) {
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)
return eventTags.length ? eventTags.at(-1)[1] : null;
}
const writeForm = document.querySelector('#writeForm'); const writeForm = document.querySelector('#writeForm');
const elemShrink = () => { const elemShrink = () => {
@ -973,25 +902,6 @@ function promptError(error, options = {}) {
errorOverlay.hidden = false; errorOverlay.hidden = false;
} }
/**
* 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
*/
function validatePow(evt) {
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;
}
/** /**
* run proof of work in a worker until at least the specified difficulty. * run proof of work in a worker until at least the specified difficulty.
* if succcessful, the returned event contains the 'nonce' tag * if succcessful, the returned event contains the 'nonce' tag

@ -2,18 +2,18 @@
* evaluate the difficulty of hex32 according to nip-13. * evaluate the difficulty of hex32 according to nip-13.
* @param hex32 a string of 64 chars - 32 bytes in hex representation * @param hex32 a string of 64 chars - 32 bytes in hex representation
*/ */
export const zeroLeadingBitsCount = (hex32) => { export const zeroLeadingBitsCount = (hex32: string) => {
let count = 0; let count = 0;
for (let i = 0; i < 64; i += 2) { for (let i = 0; i < 64; i += 2) {
const hexbyte = hex32.slice(i, i + 2); // grab next byte const hexbyte = hex32.slice(i, i + 2); // grab next byte
if (hexbyte == '00') { if (hexbyte === '00') {
count += 8; count += 8;
continue; continue;
} }
// reached non-zero byte; count number of 0 bits in hexbyte // reached non-zero byte; count number of 0 bits in hexbyte
const bits = parseInt(hexbyte, 16).toString(2).padStart(8, '0'); const bits = parseInt(hexbyte, 16).toString(2).padStart(8, '0');
for (let b = 0; b < 8; b++) { for (let b = 0; b < 8; b++) {
if (bits[b] == '1' ) { if (bits[b] === '1' ) {
break; // reached non-zero bit; stop break; // reached non-zero bit; stop
} }
count += 1; count += 1;

@ -1,2 +1,4 @@
export {zeroLeadingBitsCount} from './crypto';
export {elem, parseTextContent} from './dom'; export {elem, parseTextContent} from './dom';
export {bounce, dateTime, formatTime} from './time'; export {bounce, dateTime, formatTime} from './time';
export {getHost, getNoxyUrl, isHttpUrl, isWssUrl} from './url';

@ -0,0 +1,39 @@
export const getHost = (url: string) => {
try {
return new URL(url).host;
} catch(err) {
return err;
}
};
export const isHttpUrl = (url: string) => {
try {
return ['http:', 'https:'].includes(new URL(url).protocol);
} catch (err) {
return false;
}
};
export const isWssUrl = (url: string) => {
try {
return 'wss:' === new URL(url).protocol;
} catch (err) {
return false;
}
};
export const getNoxyUrl = (
type: 'data' | 'meta',
url: string,
id: string,
relay: string,
) => {
if (!isHttpUrl(url)) {
return false;
}
const link = new URL(`https://noxy.nostr.ch/${type}`);
link.searchParams.set('id', id);
link.searchParams.set('relay', relay);
link.searchParams.set('url', url);
return link;
};

@ -1,5 +1,5 @@
import {getEventHash} from 'nostr-tools'; import {getEventHash} from 'nostr-tools';
import {zeroLeadingBitsCount} from './cryptoutils.js'; import {zeroLeadingBitsCount} from './utils/crypto';
function mine(event, difficulty, timeout = 5) { function mine(event, difficulty, timeout = 5) {
const max = 256; // arbitrary const max = 256; // arbitrary

@ -1,6 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es2021" "moduleResolution": "node",
"target": "es2022"
}, },
"exclude": ["node_modules", "dist", "**/*.test.ts"] "exclude": ["node_modules", "dist", "**/*.test.ts"]
} }
Loading…
Cancel
Save