From 0c2e717e5355662f3af52e72dbe16feed6a90d08 Mon Sep 17 00:00:00 2001 From: Franz Geffke Date: Sun, 27 Aug 2023 20:03:34 +0100 Subject: [PATCH] common: cleanup; improve content extraction and event relays --- client-web/src/components/event.tsx | 5 +- client-web/src/components/events.tsx | 7 +- client-web/src/components/user-info.tsx | 2 +- client-web/src/state/base-types.ts | 8 +- client-web/src/state/client-types.ts | 4 +- client-web/src/state/client.ts | 10 +-- client-web/src/state/worker.ts | 16 ++-- packages/common/src/classes/event.ts | 79 +++++++++++++------ packages/common/src/types/content.ts | 2 + packages/common/src/types/event-with-user.ts | 71 +++++++++++------ packages/common/src/types/event.ts | 17 ++-- packages/common/src/types/report.ts | 2 +- packages/common/src/utils/bech32.test.ts | 28 ++++--- packages/common/src/utils/bech32.ts | 10 ++- packages/common/src/utils/bolt11.test.ts | 72 ----------------- packages/common/src/utils/bolt11.ts | 3 +- .../common/src/utils/count-leading-zeroes.ts | 22 ------ packages/common/src/utils/event-amount.ts | 19 ++++- .../common/src/utils/event-content-warning.ts | 10 +-- .../common/src/utils/event-content.test.ts | 23 ++++++ packages/common/src/utils/event-content.ts | 32 +++++++- .../common/src/utils/event-coordinates.ts | 56 ++++++------- packages/common/src/utils/event-event.ts | 4 +- .../common/src/utils/event-recommendation.ts | 10 ++- packages/common/src/utils/event-relays.ts | 40 ++++++++-- .../common/src/utils/event-reporting.test.ts | 6 +- packages/common/src/utils/event-reporting.ts | 14 +++- packages/common/src/utils/index.ts | 1 - packages/common/src/utils/proof-of-work.ts | 37 ++++++++- packages/common/src/utils/sign-event.ts | 1 + packages/common/src/utils/websocket-url.ts | 3 + 31 files changed, 364 insertions(+), 250 deletions(-) delete mode 100644 packages/common/src/utils/count-leading-zeroes.ts diff --git a/client-web/src/components/event.tsx b/client-web/src/components/event.tsx index 88a7e6e..bd8f7a6 100644 --- a/client-web/src/components/event.tsx +++ b/client-web/src/components/event.tsx @@ -23,7 +23,7 @@ import { } from "@chakra-ui/react"; import { NEvent, - NEventWithUserBase, + ProcessedEvent, NewQuoteRepost, NewReaction, NewShortTextNoteResponse, @@ -36,7 +36,7 @@ import { unixTimeToRelative } from "../lib/relative-time"; import { excerpt } from "../lib/excerpt"; import { UserIcon } from "./user-icon"; -export interface EventProps extends NEventWithUserBase { +export interface EventProps extends ProcessedEvent { userComponent?: JSX.Element; } @@ -430,6 +430,7 @@ export function Event({ showAbout: true, showBanner: true, relayUrls: eventRelayUrls, + avatarSize: "xs", }} /> diff --git a/client-web/src/components/events.tsx b/client-web/src/components/events.tsx index 1800314..7f09dc7 100644 --- a/client-web/src/components/events.tsx +++ b/client-web/src/components/events.tsx @@ -81,7 +81,12 @@ export function Events(props: { Waiting for fresh content ... hold on. )} {events.length >= maxEvents && ( - + diff --git a/client-web/src/components/user-info.tsx b/client-web/src/components/user-info.tsx index 66b1f84..f6e164b 100644 --- a/client-web/src/components/user-info.tsx +++ b/client-web/src/components/user-info.tsx @@ -39,7 +39,7 @@ export function UserInfo({ - + {displayName} {name} diff --git a/client-web/src/state/base-types.ts b/client-web/src/state/base-types.ts index 0793c3b..f4be524 100644 --- a/client-web/src/state/base-types.ts +++ b/client-web/src/state/base-types.ts @@ -1,6 +1,6 @@ import { Relay, - NEventWithUserBase, + ProcessedEvent, NUserBase, RelayAuth, RelayCount, @@ -47,17 +47,17 @@ export interface NClientBase { subscribe: ( payload: SubscriptionRequest ) => Promise; - eventsMap?: Map; + eventsMap?: Map; /** * Add event to array or map * - In worker, must post message to main thread */ - addEvent: (payload: NEventWithUserBase) => void; + addEvent: (payload: ProcessedEvent) => void; /** * Update event on array or map * - In worker, must post message to main thread */ - updateEvent: (payload: NEventWithUserBase) => void; + updateEvent: (payload: ProcessedEvent) => void; eventsPublishingQueue: PublishingQueueItem[]; diff --git a/client-web/src/state/client-types.ts b/client-web/src/state/client-types.ts index da55a1a..c7d89a2 100644 --- a/client-web/src/state/client-types.ts +++ b/client-web/src/state/client-types.ts @@ -1,6 +1,6 @@ import { NEvent, - NEventWithUserBase, + ProcessedEvent, WebSocketClientInfo, WebSocketEvent, NFilters, @@ -64,7 +64,7 @@ export interface NClient extends NClientBase { generateQueueItems: ( request: PublishingRequest ) => Promise; - events: NEventWithUserBase[]; + events: ProcessedEvent[]; /** * Track kind name like NewShortTextNote */ diff --git a/client-web/src/state/client.ts b/client-web/src/state/client.ts index 7bf560f..7065c4d 100644 --- a/client-web/src/state/client.ts +++ b/client-web/src/state/client.ts @@ -1,6 +1,6 @@ import { nanoid } from "nanoid"; import { - NEventWithUserBase, + ProcessedEvent, WebSocketEvent, Relay, NEvent, @@ -36,7 +36,7 @@ interface Event { | "relay:message" | "event:queue:new" | "event:queue:update"; - data: NEventWithUserBase | WebSocketEvent | PublishingQueueItem; + data: ProcessedEvent | WebSocketEvent | PublishingQueueItem; }; } @@ -72,7 +72,7 @@ export const useNClient = create((set, get) => ({ const payload = event.data; if (payload.type === "event:new" || payload.type === "event:update") { - const data = payload.data as NEventWithUserBase; + const data = payload.data as ProcessedEvent; if (payload.type === "event:new") { get().addEvent(data); } else if (payload.type === "event:update") { @@ -237,12 +237,12 @@ export const useNClient = create((set, get) => ({ return get().store.count(payload); }, events: [], - addEvent: (payload: NEventWithUserBase) => { + addEvent: (payload: ProcessedEvent) => { set({ events: [...get().events, payload], }); }, - updateEvent: (payload: NEventWithUserBase) => { + updateEvent: (payload: ProcessedEvent) => { const eventIndex = get().events.findIndex( (event) => event.event.id === payload.event.id ); diff --git a/client-web/src/state/worker.ts b/client-web/src/state/worker.ts index 354073b..c3b9f04 100644 --- a/client-web/src/state/worker.ts +++ b/client-web/src/state/worker.ts @@ -1,7 +1,7 @@ import { NEvent, RELAY_MESSAGE_TYPE, - NEventWithUserBase, + ProcessedEvent, EventBase, NFilters, NEVENT_KIND, @@ -31,7 +31,7 @@ class WorkerClass implements NClientWorker { connected: boolean; db: IDBPDatabase | null; client: RelayClient | null; - eventsMap: Map = new Map(); + eventsMap: Map = new Map(); maxEvents: number; checkedUsers: string[] = []; checkedEvents: string[] = []; @@ -145,7 +145,7 @@ class WorkerClass implements NClientWorker { * - Add event to map * - Post message to main thread */ - addEvent(payload: NEventWithUserBase) { + addEvent(payload: ProcessedEvent) { this.eventsMap.set(payload.event.id, payload); postMessage({ type: "event:new", @@ -157,7 +157,7 @@ class WorkerClass implements NClientWorker { * - Update event on map * - Post message to main thread */ - updateEvent(payload: NEventWithUserBase) { + updateEvent(payload: ProcessedEvent) { this.eventsMap.set(payload.event.id, payload); postMessage({ type: "event:update", @@ -357,7 +357,7 @@ class WorkerClass implements NClientWorker { return; } - const newEvent: NEventWithUserBase = { + const newEvent: ProcessedEvent = { event: new NEvent(event), eventRelayUrls: [payload.meta.url as string], }; @@ -440,13 +440,13 @@ class WorkerClass implements NClientWorker { if (hasRootTag) { const rootEvent = this.eventsMap.get(hasRootTag.eventId); if (rootEvent) { - if (rootEvent.lightningReceipts) { - rootEvent.lightningReceipts.push({ + if (rootEvent.zapReceipt) { + rootEvent.zapReceipt.push({ event: ev, user: user?.user, }); } else { - rootEvent.lightningReceipts = [ + rootEvent.zapReceipt = [ { event: ev, user: user?.user, diff --git a/packages/common/src/classes/event.ts b/packages/common/src/classes/event.ts index f8c116a..2bd37a2 100644 --- a/packages/common/src/classes/event.ts +++ b/packages/common/src/classes/event.ts @@ -10,7 +10,7 @@ import { iNewShortTextNote, iNewShortTextNoteResponse, iNewUpdateUserMetadata, - Report, + EventReport, NEventContent, iNewLongFormContent, iNewZAPRequest, @@ -49,12 +49,13 @@ import { makeEventAmountTag, eventHasAmountTags, makeEventCoordinatesTag, - countLeadingZeroes, eventAddNonceTag, eventReplaceNonceTag, eventHasEventTags, eventHasPositionalEventTag, eventHasPositionalEventTags, + proofOfWork, + EventRelayTag, } from "../utils"; import { eventHasExternalIdentityClaim, @@ -88,29 +89,49 @@ export class NEvent implements EventBase { this.sig = data.sig ? data.sig : ""; } + /** + * Generate event ID + * - Public key should be set + */ public generateId() { if (this.pubkey === "") { - throw new Error("Cannot generate event ID without a public key"); + throw new Error( + "Cannot generate event ID without a public key. Set a public key first." + ); } const serial = serializeEvent(this.ToObj()); this.id = hash(serial); } + /** + * Sign event + * - Event ID should be set + */ public sign(keyPair: { privateKey: string; publicKey: string }) { + if (this.id === "") { + throw new Error("Cannot sign event without an ID. Generate ID first."); + } this.pubkey = keyPair.publicKey; this.sig = sign(this.id, keyPair.privateKey); } /** - * 1. Sign the event (event.sig) - * 2. Generate the event ID (event.id) + * Sign event and generate ID + * + * + * 1. Generate the event ID (event.id) + * 2. Sign the event (event.sig) * @param privateKey */ public signAndGenerateId(keyPair: { privateKey: string; publicKey: string }) { - this.sign(keyPair); + this.pubkey = keyPair.publicKey; this.generateId(); + this.sign(keyPair); } + /** + * Event object for transmission and storage + */ public ToObj(): any { const cleanObject = {}; for (const [key, value] of Object.entries(this)) { @@ -121,28 +142,25 @@ export class NEvent implements EventBase { return cleanObject; } + /** + * Event as URI + */ public toURI() { return encodeURI(JSON.stringify(this.ToObj())); } /** - * + * Generate proof of work * @param difficulty bits required for proof of work */ - public proofOfWork(targetDifficulty: number) { - let adjustmentValue = 0; - - while (true) { - this.replaceNonceTag([targetDifficulty, adjustmentValue]); - this.generateId(); - const leadingZeroes = countLeadingZeroes(this.id); + public proofOfWork(targetDifficulty: number, limitRounds?: number) { + const ev = proofOfWork(this, targetDifficulty, limitRounds); - if (leadingZeroes >= targetDifficulty) { - console.log("Proof of work complete"); - break; - } - - adjustmentValue++; + if (!ev) { + throw new Error("Failed to generate proof of work."); + } else { + this.id = ev.id; + this.tags = ev.tags; } } @@ -268,7 +286,14 @@ export class NEvent implements EventBase { } } - public hasRelaysTag(): string[] | undefined { + /** + * Get event relay tags or undefined + * usually used on kind:10002 + * + * Standard tag + * Spec: https://github.com/nostr-protocol/nips/blob/master/65.md#relay-list-metadata + */ + public hasRelaysTag(): EventRelayTag[] | undefined { return eventHasRelaysTag(this); } @@ -309,7 +334,9 @@ export class NEvent implements EventBase { } /** + * Create amount tag * Standard tag + * related to ZAP * https://github.com/nostr-protocol/nips/blob/master/README.md#standardized-tags * * @param amount millisats @@ -318,6 +345,12 @@ export class NEvent implements EventBase { this.addTag(makeEventAmountTag(amount)); } + /** + * Get event amount(s) or undefined + * Standard tag + * related to ZAP + * https://github.com/nostr-protocol/nips/blob/master/README.md#standardized-tags + */ public hasAmountTags() { return eventHasAmountTags(this); } @@ -414,7 +447,7 @@ export class NEvent implements EventBase { } /** - * Check if event has a content warning + * Get event content warning reason (may be "") or undefined */ public hasContentWarningTag() { return eventHasContentWarning(this); @@ -441,7 +474,7 @@ export class NEvent implements EventBase { * Standard tag * @param report */ - public addReportTags(report: Report) { + public addReportTags(report: EventReport) { if (this.kind !== NEVENT_KIND.REPORTING) { throw new Error( `Event kind ${this.kind} should not have a report. Expected ${NEVENT_KIND.REPORTING}.` diff --git a/packages/common/src/types/content.ts b/packages/common/src/types/content.ts index 27ebf7f..b9a2fe4 100644 --- a/packages/common/src/types/content.ts +++ b/packages/common/src/types/content.ts @@ -1,6 +1,7 @@ /** * Work with event content * - for ex. "Profile is impersonating nostr:" + * - supports npub and nprofile right now * https://github.com/nostr-protocol/nips/blob/master/56.md#example-events */ export interface NEventContent { @@ -10,5 +11,6 @@ export interface NEventContent { type?: "npub" | "nsec" | "note" | "lnurl" | "nprofile" | "nevent"; // TODO: Not really accurate publicKeys?: string[]; + relayUrls?: string[]; }[]; } diff --git a/packages/common/src/types/event-with-user.ts b/packages/common/src/types/event-with-user.ts index 3aa5aac..8fa3b9b 100644 --- a/packages/common/src/types/event-with-user.ts +++ b/packages/common/src/types/event-with-user.ts @@ -2,38 +2,65 @@ import { NEvent } from "../classes/event"; import { NUserBase } from "../classes/user"; import { UserBase } from "./user"; -export interface EventWithUser { +export interface ProcessedUserBase { user?: UserBase; - event: NEvent; + relayUrls: string[]; } -export interface EventBaseWithUserBase { +/** + * Processed-side event properties + * - user: Author of the event + * - event: The event + */ +export interface ProcessedEventBase { user?: UserBase; event: NEvent; +} + +/** + * Processed-side event properties + * - user: Author of the event + * - event: The event + * - eventRelayUrls: The relayUrl(s) the event was received from + * - reactions: Reactions to the event + * - reposts: Reposts of the event + * - badgeAwards: Badge awards for the event + * - mentions: Mentions of the event + * - replies: Replies to the event + * - zapReceipt: Lightning receipts for the event + */ +export interface ProcessedEvent extends ProcessedEventBase { eventRelayUrls: string[]; - // 7 - reactions?: EventWithUser[]; - // 6 - reposts?: EventWithUser[]; - // 8 - badgeAwards?: NEvent[]; - - // Mentions and replies - mentions?: NUserBase[]; - // Mentions and replies - replies?: EventWithUser[]; + /** + * Kind 7 + */ + reactions?: ProcessedEventBase[]; - // TODO: Implement - // In Response to - // inResponseTo?: EventWithUser[]; + /** + * Kind 6 + */ + reposts?: ProcessedEventBase[]; - lightningReceipts?: EventWithUser[]; -} + /** + * Kind 8 + */ + badgeAwards?: ProcessedEventBase[]; -export interface NEventWithUserBase extends EventBaseWithUserBase { - user?: UserBase; - event: NEvent; + /** + * Kind 1, 2, .. + */ + replies?: ProcessedEventBase[]; + + /** + * from event content + */ + mentions?: NUserBase[]; + + /** + * Kind 9735 + */ + zapReceipt?: ProcessedEventBase[]; } export interface idOrKey { diff --git a/packages/common/src/types/event.ts b/packages/common/src/types/event.ts index c385433..ffafc7e 100644 --- a/packages/common/src/types/event.ts +++ b/packages/common/src/types/event.ts @@ -1,7 +1,7 @@ import { ExternalIdentityClaim } from "../classes/identity-claim"; import { NEVENT_KIND } from "./event-kind"; import { UserMetadata } from "./user-metadata"; -import { Report } from "./report"; +import { EventReport } from "./report"; export interface EventBase { id?: string; @@ -16,24 +16,19 @@ export interface EventBase { sig?: string; } -export interface iNewBase { - pow?: string; - relayUrls?: string[]; -} - -export interface iNewShortTextNote extends iNewBase { +export interface iNewShortTextNote { text: string; subject?: string; } -export interface iNewLongFormContent extends iNewBase { +export interface iNewLongFormContent { text: string; isDraft?: boolean; // d identifier to make it replaceable identifier?: string; } -interface inResponse extends iNewBase { +interface inResponse { /** * The event that this is in response to */ @@ -70,7 +65,7 @@ export interface iNewRecommendRelay { nonce?: [number, number]; } -export interface iNewReport extends Report {} +export interface iNewReport extends EventReport {} export interface iNewUserZapRequest { /** @@ -130,7 +125,7 @@ export interface iNewZAPReceipt { zapRequest?: EventBase; } -export interface iNewEventDeletion extends iNewBase { +export interface iNewEventDeletion { text: string; events: EventBase[]; diff --git a/packages/common/src/types/report.ts b/packages/common/src/types/report.ts index 8d4d70e..e2c23e3 100644 --- a/packages/common/src/types/report.ts +++ b/packages/common/src/types/report.ts @@ -4,7 +4,7 @@ import { NREPORT_KIND } from "./report-types"; * Reporting * https://github.com/nostr-protocol/nips/blob/master/56.md */ -export interface Report { +export interface EventReport { /** * If reporting a note, an e tag MUST also be included referencing the note id. */ diff --git a/packages/common/src/utils/bech32.test.ts b/packages/common/src/utils/bech32.test.ts index 4e61be9..c25aa81 100644 --- a/packages/common/src/utils/bech32.test.ts +++ b/packages/common/src/utils/bech32.test.ts @@ -1,7 +1,10 @@ import { encodeBech32, decodeBech32, BECH32_PREFIX } from ".."; -test("encode and decode nostr entity", () => { - // Decode npub +/** + * DECODE BECH32 + */ + +test("decode bech", () => { const npub = decodeBech32( "npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg" ); @@ -13,7 +16,6 @@ test("encode and decode nostr entity", () => { }, ]); - // Decode npub #2 const npub2 = decodeBech32( "npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6" ); @@ -25,7 +27,6 @@ test("encode and decode nostr entity", () => { }, ]); - // // Decode nsec const nsec = decodeBech32( "nsec1vl029mgpspedva04g90vltkh6fvh240zqtv9k0t9af8935ke9laqsnlfe5" ); @@ -37,7 +38,6 @@ test("encode and decode nostr entity", () => { }, ]); - // Device lnurl const lnurl1 = decodeBech32( "lnurl1dp68gurn8ghj7um5v93kketj9ehx2amn9uh8wetvdskkkmn0wahz7mrww4excup0dajx2mrv92x9xp" ); @@ -50,7 +50,6 @@ test("encode and decode nostr entity", () => { }, ]); - // Decode nprofile const nprofile = decodeBech32( "nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhhytnc9e3k7mgpz4mhxue69uhkg6nzv9ejuumpv34kytnrdaksjlyr9p" ); @@ -64,19 +63,30 @@ test("encode and decode nostr entity", () => { { type: 1, value: "wss://djbas.sadkb.com" }, ]); - // Encode npub + const nnote = decodeBech32( + "note1fntxtkcy9pjwucqwa9mddn7v03wwwsu9j330jj350nvhpky2tuaspk6nqc" + ); + expect(nnote.prefix).toEqual("note"); + expect(nnote.tlvItems).toEqual([ + { + type: 0, + value: "4cd665db042864ee600ee976d6cfcc7c5ce743859462f94a347cd970d88a5f3b", + }, + ]); + + /** + * ENCODE + */ const encodedNpub = encodeBech32(BECH32_PREFIX.PublicKeys, npub.tlvItems); expect(encodedNpub).toEqual( "npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg" ); - // // Encode nsec const encodedNsec = encodeBech32(BECH32_PREFIX.PrivateKeys, nsec.tlvItems); expect(encodedNsec).toEqual( "nsec1vl029mgpspedva04g90vltkh6fvh240zqtv9k0t9af8935ke9laqsnlfe5" ); - // Encode nprofile const encodedNprofile = encodeBech32( BECH32_PREFIX.Profile, nprofile.tlvItems diff --git a/packages/common/src/utils/bech32.ts b/packages/common/src/utils/bech32.ts index 03f0fb0..c3c76ce 100644 --- a/packages/common/src/utils/bech32.ts +++ b/packages/common/src/utils/bech32.ts @@ -114,6 +114,13 @@ function convertTLV(tlvItems: ParsedTLVItem[]): ConvertedTLVItem[] { }); } +/** + * Encode TLV data into a Bech32 string + * Spec: https://github.com/nostr-protocol/nips/blob/master/19.md + * @param prefix + * @param tlvItems + * @returns + */ export function encodeBech32( prefix: BECH32_PREFIX, tlvItems: Array<{ type: number; value: string | number }> @@ -139,7 +146,8 @@ export function encodeBech32( } /** - * https://github.com/nostr-protocol/nips/blob/master/19.md + * Decode a Bech32 string into TLV data + * Spec: https://github.com/nostr-protocol/nips/blob/master/19.md * @param bech32Str * @returns */ diff --git a/packages/common/src/utils/bolt11.test.ts b/packages/common/src/utils/bolt11.test.ts index fe3d016..eff6b7b 100644 --- a/packages/common/src/utils/bolt11.test.ts +++ b/packages/common/src/utils/bolt11.test.ts @@ -1,77 +1,5 @@ import { decodeLightnightPayRequest } from ".."; -/** - * I used to use bolt11 library but due to problems in the browser I went with light-bolt11-decoder for now - */ - -// test("decodeLightnightPayRequest", () => { -// const req = -// "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpp3qjmp7lwpagxun9pygexvgpjdc4jdj85fr9yq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvpeuqafqxu92d8lr6fvg0r5gv0heeeqgcrqlnm6jhphu9y00rrhy4grqszsvpcgpy9qqqqqqgqqqqq7qqzqj9n4evl6mr5aj9f58zp6fyjzup6ywn3x6sk8akg5v4tgn2q8g4fhx05wf6juaxu9760yp46454gpg5mtzgerlzezqcqvjnhjh8z3g2qqdhhwkj"; -// const decoded = decodeLightnightPayRequest(req); -// expect(decoded).toEqual({ -// complete: true, -// millisatoshis: "2000000000", -// network: { -// bech32: "bc", -// pubKeyHash: 0, -// scriptHash: 5, -// validWitnessVersions: [0, 1], -// }, -// payeeNodeKey: -// "03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad", -// paymentRequest: -// "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpp3qjmp7lwpagxun9pygexvgpjdc4jdj85fr9yq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvpeuqafqxu92d8lr6fvg0r5gv0heeeqgcrqlnm6jhphu9y00rrhy4grqszsvpcgpy9qqqqqqgqqqqq7qqzqj9n4evl6mr5aj9f58zp6fyjzup6ywn3x6sk8akg5v4tgn2q8g4fhx05wf6juaxu9760yp46454gpg5mtzgerlzezqcqvjnhjh8z3g2qqdhhwkj", -// prefix: "lnbc20m", -// recoveryFlag: 0, -// satoshis: 2000000, -// signature: -// "91675cb3fad8e9d915343883a49242e074474e26d42c7ed914655689a8074553733e8e4ea5ce9b85f69e40d755a55014536b12323f8b220600c94ef2b9c51428", -// tags: [ -// { -// tagName: "payment_hash", -// data: "0001020304050607080900010203040506070809000102030405060708090102", -// }, -// { -// tagName: "purpose_commit_hash", -// data: "3925b6f67e2c340036ed12093dd44e0368df1b6ea26c53dbe4811f58fd5db8c1", -// }, -// { -// tagName: "fallback_address", -// data: { -// code: 17, -// address: "1RustyRX2oai4EYYDpQGWvEL62BBGqN9T", -// addressHash: "04b61f7dc1ea0dc99424464cc4064dc564d91e89", -// }, -// }, -// { -// tagName: "routing_info", -// data: [ -// { -// pubkey: -// "029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255", -// short_channel_id: "0102030405060708", -// fee_base_msat: 1, -// fee_proportional_millionths: 20, -// cltv_expiry_delta: 3, -// }, -// { -// pubkey: -// "039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255", -// short_channel_id: "030405060708090a", -// fee_base_msat: 2, -// fee_proportional_millionths: 30, -// cltv_expiry_delta: 4, -// }, -// ], -// }, -// ], -// timestamp: 1496314658, -// timestampString: "2017-06-01T10:57:38.000Z", -// wordsTemp: -// "temp1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpp3qjmp7lwpagxun9pygexvgpjdc4jdj85fr9yq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvpeuqafqxu92d8lr6fvg0r5gv0heeeqgcrqlnm6jhphu9y00rrhy4grqszsvpcgpy9qqqqqqgqqqqq7qqzqj9n4evl6mr5aj9f58zp6fyjzup6ywn3x6sk8akg5v4tgn2q8g4fhx05wf6juaxu9760yp46454gpg5mtzgerlzezqcqvjnhjh8z3g2qqpqa4j6", -// }); -// }); - test("decodeLightningPayRequest", () => { const req = "lnbc10n1pjvmrrnpp5yydgk0802tlfukay0kr5alcy4dk6wukzwd9dss22m0xgvgf7szcqhp5rt66kqtmtcd0ruush32mpv9dlglqk07l3ncdssmwl05akmfl9gnqcqzzsxqzfvsp53saq4hkt8r05pes47fw838xuy0fs62xshuqrdx2z98th25cdrzns9qyyssqvrmgjzxkevx5dc6cgh9vkukt6jp7zwq6fepqun4z7z5xkwxd069rl768nm658vy6kdvp0qrjkhvf3swch7ycmfn9jgqjwm75cccalkqq50fkyj"; diff --git a/packages/common/src/utils/bolt11.ts b/packages/common/src/utils/bolt11.ts index fa63a5c..b48fb25 100644 --- a/packages/common/src/utils/bolt11.ts +++ b/packages/common/src/utils/bolt11.ts @@ -4,8 +4,9 @@ export interface DecodedInvoice { paymentRequest: string; sections: { name: string; + tag?: string; letters: string; - value?: any; + value?: object | string | number; }[]; } diff --git a/packages/common/src/utils/count-leading-zeroes.ts b/packages/common/src/utils/count-leading-zeroes.ts deleted file mode 100644 index 47fed01..0000000 --- a/packages/common/src/utils/count-leading-zeroes.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * - * Copied from https://github.com/nostr-protocol/nips/blob/master/13.md#validating - * All credits go to the author - * @param hex - * @returns - */ -export function countLeadingZeroes(hex) { - let count = 0; - - for (let i = 0; i < hex.length; i++) { - const nibble = parseInt(hex[i], 16); - if (nibble === 0) { - count += 4; - } else { - count += Math.clz32(nibble) - 28; - break; - } - } - - return count; -} diff --git a/packages/common/src/utils/event-amount.ts b/packages/common/src/utils/event-amount.ts index bbd41e4..8c47a4d 100644 --- a/packages/common/src/utils/event-amount.ts +++ b/packages/common/src/utils/event-amount.ts @@ -1,19 +1,32 @@ import { EventBase } from "../types"; -export function eventHasAmountTags(event: EventBase) { +/** + * Get event amount(s) or undefined + * Spec: https://github.com/nostr-protocol/nips/blob/master/57.md + * + * @param event + * @returns + */ +export function eventHasAmountTags(event: EventBase): string[] { const tags = event.tags.filter((tag) => tag[0] === "amount"); if (tags.length === 0) { return; } const amounts = []; for (const tag of tags) { - if (tag.length > 0) { + if (tag.length === 2) { amounts.push(tag[1]); } } return amounts && amounts.length > 0 ? amounts : undefined; } -export function makeEventAmountTag(amount: string) { +/** + * Create amount tag + * Spec: https://github.com/nostr-protocol/nips/blob/master/57.md + * + * @param amount millisatoshis + */ +export function makeEventAmountTag(amount: string): string[] { return ["amount", amount]; } diff --git a/packages/common/src/utils/event-content-warning.ts b/packages/common/src/utils/event-content-warning.ts index 17a7c40..d7a5656 100644 --- a/packages/common/src/utils/event-content-warning.ts +++ b/packages/common/src/utils/event-content-warning.ts @@ -1,9 +1,9 @@ import { EventBase } from "../types"; /** - * Check array of tags if any are NIP-36 - * https://github.com/nostr-protocol/nips/blob/master/36.md - * @param tags + * Get event content warning reason (may be "") or undefined + * Spec: https://github.com/nostr-protocol/nips/blob/master/36.md + * * @returns reason for content warning or undefined */ export function eventHasContentWarning(event: EventBase): string | undefined { @@ -27,7 +27,5 @@ export function eventHasContentWarning(event: EventBase): string | undefined { } } - if (hasContentWarning) { - return contentWarningReason; - } + return hasContentWarning ? contentWarningReason : undefined; } diff --git a/packages/common/src/utils/event-content.test.ts b/packages/common/src/utils/event-content.test.ts index 46fc1b3..7c56dfd 100644 --- a/packages/common/src/utils/event-content.test.ts +++ b/packages/common/src/utils/event-content.test.ts @@ -112,6 +112,7 @@ test("extractEventContent: nostr", () => { publicKeys: [ "ab11dda542e625f198612af9c6f176f9165990fd5c5145e9b5178505291e4481", ], + relayUrls: [], }, ], }); @@ -132,6 +133,28 @@ test("extractEventContent: nostr x2", () => { "ab11dda542e625f198612af9c6f176f9165990fd5c5145e9b5178505291e4481", "82f3b82c7f855340fc1905b20ac50b95d64c700d2b9546507415088e81535425", ], + relayUrls: [], + }, + ], + relayUrl: undefined, + }); +}); + +test("extractEventContent: nprofile", () => { + const content = + "Checkout nostr:nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhhytnc9e3k7mgpz4mhxue69uhkg6nzv9ejuumpv34kytnrdaksjlyr9p later"; + const res = extractEventContent(content); + + expect(res).toEqual({ + message: + "Checkout 3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d later", + nurls: [ + { + type: "npub", + publicKeys: [ + "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", + ], + relayUrls: ["wss://r.x.com", "wss://djbas.sadkb.com"], }, ], relayUrl: undefined, diff --git a/packages/common/src/utils/event-content.ts b/packages/common/src/utils/event-content.ts index 38b219c..f9081f9 100644 --- a/packages/common/src/utils/event-content.ts +++ b/packages/common/src/utils/event-content.ts @@ -24,7 +24,6 @@ export function extractEventContent( const validationResult = isValidEventContent(content, kind); if (!validationResult.isValid) { - console.log("Invalid content"); return undefined; } @@ -43,6 +42,8 @@ export function extractEventContent( } const publicKeys: string[] = []; + const relayUrls: string[] = []; + let prefix; let nostrMatch; const replaceIndexes = []; @@ -50,14 +51,36 @@ export function extractEventContent( const item = nostrMatch[0]; if (isNostrUrl(item)) { const decoded = decodeNostrUrl(item); - if (decoded.prefix === "npub" && decoded.tlvItems.length > 0) { - const publicKey = decoded.tlvItems[0].value as string; + prefix = decoded.prefix; + if (prefix === "npub" && decoded.tlvItems.length > 0) { + const match = decoded.tlvItems.filter((i) => i.type === 0); + if (match.length === 0) continue; + const publicKey = match[0].value as string; publicKeys.push(publicKey); replaceIndexes.push({ index: nostrMatch.index, length: item.length, replaceWith: publicKey, }); + } else if (prefix === "nprofile") { + const match = decoded.tlvItems.filter((i) => i.type === 0); + if (match.length === 0) continue; + const publicKey = match[0].value as string; + publicKeys.push(publicKey); + replaceIndexes.push({ + index: nostrMatch.index, + length: item.length, + replaceWith: publicKey, + }); + const relayMatches = decoded.tlvItems.filter((i) => i.type === 1); + if (relayMatches.length === 0) continue; + for (const relayUrl of relayMatches) { + if (isValidWebSocketUrl(relayUrl.value as string)) { + relayUrls.push(relayUrl.value as string); + } else { + console.error(`Invalid relay url ${relayUrl.value}`); + } + } } } } @@ -72,12 +95,13 @@ export function extractEventContent( offset += replaceWith.length - length; } - if (publicKeys.length > 0) { + if (publicKeys.length > 0 && prefix) { evc.message = content.trim(); evc.nurls = [ { type: "npub", publicKeys, + relayUrls, }, ]; return evc; diff --git a/packages/common/src/utils/event-coordinates.ts b/packages/common/src/utils/event-coordinates.ts index be970a3..22852fd 100644 --- a/packages/common/src/utils/event-coordinates.ts +++ b/packages/common/src/utils/event-coordinates.ts @@ -1,37 +1,12 @@ import { EventBase, EventCoordinatesTag } from "../types"; -export function eventHasEventCoordinatesTags( - event: EventBase -): EventCoordinatesTag[] | undefined { - const coordinates = eventCoordinatesFromTags(event.tags); - if (!coordinates) { - return; - } - return coordinates; -} - -export function makeEventCoordinatesTag(opts: EventCoordinatesTag) { - const { kind, pubkey, identifier, relay } = opts; - if (relay) { - return [`a:${kind}:${pubkey}:${identifier}, ${relay}`]; - } else { - return [`a:${kind}:${pubkey}:${identifier}`]; - } -} - /** + * Extract event coordinates from tags * * ["a", "::"] * ["a", "::", ""] - * - * return { - * kind: , - * pubkey: , - * identifier: , - * relay: - * } */ -export function eventCoordinatesFromTags( +function eventCoordinatesFromTags( tags: any[] ): EventCoordinatesTag[] | undefined { if (!tags) { @@ -89,3 +64,30 @@ export function eventCoordinatesFromTags( // Return result array return result; } + +/** + * Get event coordinates or undefined + */ +export function eventHasEventCoordinatesTags( + event: EventBase +): EventCoordinatesTag[] | undefined { + const coordinates = eventCoordinatesFromTags(event.tags); + if (!coordinates) { + return; + } + return coordinates; +} + +/** + * Make event coordinates tag + * @param opts + * @returns + */ +export function makeEventCoordinatesTag(opts: EventCoordinatesTag) { + const { kind, pubkey, identifier, relay } = opts; + if (relay) { + return [`a:${kind}:${pubkey}:${identifier}, ${relay}`]; + } else { + return [`a:${kind}:${pubkey}:${identifier}`]; + } +} diff --git a/packages/common/src/utils/event-event.ts b/packages/common/src/utils/event-event.ts index 95007b5..169fa25 100644 --- a/packages/common/src/utils/event-event.ts +++ b/packages/common/src/utils/event-event.ts @@ -2,7 +2,7 @@ import { EventBase } from "../types/event"; import { EventEventTag } from "../types/event-event-tag"; /** - * Marked "e" tags (PREFERRED) + * Get marked event "e" tags (PREFERRED) * * ["e", ] * ["e", , ] @@ -37,7 +37,7 @@ export function eventHasEventTags( } /** - * Get event positional "e" tags (DEPRECATED) + * Get positional event "e" tags (DEPRECATED) */ export function eventHasPositionalEventTags(event: EventBase) { const tags = event.tags.filter((tag) => tag[0] === "e"); diff --git a/packages/common/src/utils/event-recommendation.ts b/packages/common/src/utils/event-recommendation.ts index a9e4a33..d5b0217 100644 --- a/packages/common/src/utils/event-recommendation.ts +++ b/packages/common/src/utils/event-recommendation.ts @@ -1,14 +1,16 @@ import { NEVENT_KIND, EventBase } from "../types"; import { isValidWebSocketUrl } from "./websocket-url"; +/** + * Check if the event is / has a relay recommentation + * + */ export function eventHasRelayRecommendation( event: EventBase ): string | undefined { if (event.kind !== NEVENT_KIND.RECOMMEND_RELAY) { return; } - const relayUrl = event.content; - if (isValidWebSocketUrl(relayUrl)) { - return relayUrl; - } + + return isValidWebSocketUrl(event.content) ? event.content : undefined; } diff --git a/packages/common/src/utils/event-relays.ts b/packages/common/src/utils/event-relays.ts index 48c1ee3..5a530dc 100644 --- a/packages/common/src/utils/event-relays.ts +++ b/packages/common/src/utils/event-relays.ts @@ -1,14 +1,38 @@ import { EventBase } from "../types"; -export function eventHasRelaysTag(event: EventBase): string[] | undefined { +export interface EventRelayTag { + url: string; + read?: boolean; + write?: boolean; +} + +/** + * Get event relay tags or undefined + * usually used on kind:10002 + * + * Spec: https://github.com/nostr-protocol/nips/blob/master/65.md#relay-list-metadata + */ +export function eventHasRelaysTag( + event: EventBase +): EventRelayTag[] | undefined { const relayTags = event.tags.filter((tag) => tag[0] === "relays"); - if (relayTags.length === 0) { - return; - } - const relays = []; + if (relayTags.length === 0) return; + + const tags: EventRelayTag[] = []; for (const tag of relayTags) { - const tagParts = tag.splice(1); - relays.push(tagParts); + if (tag.length === 2) { + tags.push({ + url: tag[1], + read: true, + write: true, + }); + } else if (tag.length === 3) { + tags.push({ + url: tag[1], + read: tag[2] === "read", + write: tag[2] === "write", + }); + } } - return relays; + return tags.length > 0 ? tags : undefined; } diff --git a/packages/common/src/utils/event-reporting.test.ts b/packages/common/src/utils/event-reporting.test.ts index a6d96f6..39e6887 100644 --- a/packages/common/src/utils/event-reporting.test.ts +++ b/packages/common/src/utils/event-reporting.test.ts @@ -1,4 +1,4 @@ -import { Report, NREPORT_KIND } from "../types"; +import { EventReport, NREPORT_KIND } from "../types"; import { NEvent } from "../classes"; import { eventHasReport, generateReportTags } from "./event-reporting"; @@ -51,7 +51,7 @@ test("eventHasReport: impersonation", () => { }); test("generateReportTags: impersonation", () => { - const report: Report = { + const report: EventReport = { kind: NREPORT_KIND.IMPERSONATION, publicKey: "1234567890123456789012345678901234567890123456789012345678901234", @@ -61,7 +61,7 @@ test("generateReportTags: impersonation", () => { }); test("generateReportTags: event", () => { - const report: Report = { + const report: EventReport = { kind: NREPORT_KIND.ILLEGAL, publicKey: "1234567890123456789012345678901234567890123456789012345678901234", diff --git a/packages/common/src/utils/event-reporting.ts b/packages/common/src/utils/event-reporting.ts index 3de99a0..244f8bd 100644 --- a/packages/common/src/utils/event-reporting.ts +++ b/packages/common/src/utils/event-reporting.ts @@ -1,13 +1,14 @@ -import { EventBase, NEVENT_KIND, Report, NREPORT_KIND } from "../types"; +import { EventBase, NEVENT_KIND, EventReport, NREPORT_KIND } from "../types"; /** * Extracts a report from an event * This will throw an error if event is not a report + * * https://github.com/nostr-protocol/nips/blob/master/56.md * @param event * @returns */ -export function eventHasReport(event: EventBase): Report | undefined { +export function eventHasReport(event: EventBase): EventReport | undefined { if (event.kind !== NEVENT_KIND.REPORTING) { throw new Error( `Event is not a report: ${event.kind}. Expected ${NEVENT_KIND.REPORTING}.` @@ -49,7 +50,7 @@ export function eventHasReport(event: EventBase): Report | undefined { return undefined; } - const report: Report = { + const report: EventReport = { eventId, kind, publicKey, @@ -59,7 +60,12 @@ export function eventHasReport(event: EventBase): Report | undefined { return report; } -export function generateReportTags(report: Report): string[][] { +/** + * Generate report tags + * @param report + * @returns + */ +export function generateReportTags(report: EventReport): string[][] { const { eventId, kind, publicKey } = report; if (!kind) { throw new Error("Report must have a kind."); diff --git a/packages/common/src/utils/index.ts b/packages/common/src/utils/index.ts index 8652f2e..1b209da 100644 --- a/packages/common/src/utils/index.ts +++ b/packages/common/src/utils/index.ts @@ -1,6 +1,5 @@ export * from "./bech32"; export * from "./bolt11"; -export * from "./count-leading-zeroes"; export * from "./event-amount"; export * from "./event-content-warning"; export * from "./event-content"; diff --git a/packages/common/src/utils/proof-of-work.ts b/packages/common/src/utils/proof-of-work.ts index 05743fb..825aec1 100644 --- a/packages/common/src/utils/proof-of-work.ts +++ b/packages/common/src/utils/proof-of-work.ts @@ -1,11 +1,38 @@ -import { countLeadingZeroes } from "./count-leading-zeroes"; import { hash } from "./hash-event"; import { EventBase } from "../types"; /** - * Optimized for speed + * Count leading zeroes in a hex string + * Copied from https://github.com/nostr-protocol/nips/blob/master/13.md#validating + * All credits go to the author + * @param hex + * @returns */ -export function proofOfWork(event: EventBase, bits: number) { +function countLeadingZeroes(hex: string) { + let count = 0; + + for (let i = 0; i < hex.length; i++) { + const nibble = parseInt(hex[i], 16); + if (nibble === 0) { + count += 4; + } else { + count += Math.clz32(nibble) - 28; + break; + } + } + + return count; +} + +/** + * Add proof of work to event + * Importing: Anything above ~15-20 bits might take a while to compute + */ +export function proofOfWork( + event: EventBase, + bits: number, + limitRounds?: number +) { let adjustmentValue = 0; const bitsString = bits.toString(); while (true) { @@ -35,6 +62,10 @@ export function proofOfWork(event: EventBase, bits: number) { return event; } + if (limitRounds && adjustmentValue >= limitRounds) { + return undefined; + } + adjustmentValue++; } } diff --git a/packages/common/src/utils/sign-event.ts b/packages/common/src/utils/sign-event.ts index 9887da5..c148a86 100644 --- a/packages/common/src/utils/sign-event.ts +++ b/packages/common/src/utils/sign-event.ts @@ -4,6 +4,7 @@ import { EventBase } from "../types"; import { hashEvent } from "./hash-event"; export function sign(eventHash: string, privateKey: string) { + if (eventHash.length !== 64) throw new Error("Invalid event hash"); const sig = schnorr.sign(eventHash, privateKey); return bytesToHex(sig); } diff --git a/packages/common/src/utils/websocket-url.ts b/packages/common/src/utils/websocket-url.ts index 6ee7796..9e600df 100644 --- a/packages/common/src/utils/websocket-url.ts +++ b/packages/common/src/utils/websocket-url.ts @@ -1,3 +1,6 @@ +/** + * Check if the given url is a valid websocket url. + */ export function isValidWebSocketUrl(url: string): boolean { const regex = /^(wss?):\/\/([a-zA-Z0-9.-]+)(:\d+)?(\/[a-zA-Z0-9_/.-]*)?$/; return regex.test(url);