// src/user/index.ts import { nip05, nip19 as nip194 } from "nostr-tools"; // src/events/index.ts import EventEmitter4 from "eventemitter3"; import { getEventHash } from "nostr-tools"; // src/relay/index.ts import debug from "debug"; import EventEmitter2 from "eventemitter3"; // src/relay/connectivity.ts import { relayInit } from "nostr-tools"; var NDKRelayConnectivity = class { ndkRelay; _status; relay; connectedAt; _connectionStats = { attempts: 0, success: 0, durations: [] }; debug; constructor(ndkRelay) { this.ndkRelay = ndkRelay; this._status = 3 /* DISCONNECTED */; this.relay = relayInit(this.ndkRelay.url); this.debug = this.ndkRelay.debug.extend("connectivity"); this.relay.on("notice", (notice) => this.handleNotice(notice)); } async connect() { const connectHandler = () => { this.updateConnectionStats.connected(); this._status = 1 /* CONNECTED */; this.ndkRelay.emit("connect"); }; const disconnectHandler = () => { this.updateConnectionStats.disconnected(); if (this._status === 1 /* CONNECTED */) { this._status = 3 /* DISCONNECTED */; this.handleReconnection(); } this.ndkRelay.emit("disconnect"); }; try { this.updateConnectionStats.attempt(); this._status = 0 /* CONNECTING */; this.relay.off("connect", connectHandler); this.relay.off("disconnect", disconnectHandler); this.relay.on("connect", connectHandler); this.relay.on("disconnect", disconnectHandler); await this.relay.connect(); } catch (e) { this.debug("Failed to connect", e); this._status = 3 /* DISCONNECTED */; throw e; } } disconnect() { this._status = 2 /* DISCONNECTING */; this.relay.close(); } get status() { return this._status; } isAvailable() { return this._status === 1 /* CONNECTED */; } /** * Evaluates the connection stats to determine if the relay is flapping. */ isFlapping() { const durations = this._connectionStats.durations; if (durations.length % 3 !== 0) return false; const sum = durations.reduce((a, b) => a + b, 0); const avg = sum / durations.length; const variance = durations.map((x) => Math.pow(x - avg, 2)).reduce((a, b) => a + b, 0) / durations.length; const stdDev = Math.sqrt(variance); const isFlapping = stdDev < 1e3; return isFlapping; } async handleNotice(notice) { if (notice.includes("oo many") || notice.includes("aximum")) { this.disconnect(); setTimeout(() => this.connect(), 2e3); this.debug(this.relay.url, "Relay complaining?", notice); } this.ndkRelay.emit("notice", this, notice); } /** * Called when the relay is unexpectedly disconnected. */ handleReconnection(attempt = 0) { if (this.isFlapping()) { this.ndkRelay.emit("flapping", this, this._connectionStats); this._status = 5 /* FLAPPING */; return; } const reconnectDelay = this.connectedAt ? Math.max(0, 6e4 - (Date.now() - this.connectedAt)) : 0; setTimeout(() => { this._status = 4 /* RECONNECTING */; this.connect().then(() => { this.debug("Reconnected"); }).catch((err) => { this.debug("Reconnect failed", err); if (attempt < 5) { setTimeout(() => { this.handleReconnection(attempt + 1); }, 6e4); } else { this.debug("Reconnect failed after 5 attempts"); } }); }, reconnectDelay); } /** * Utility functions to update the connection stats. */ updateConnectionStats = { connected: () => { this._connectionStats.success++; this._connectionStats.connectedAt = Date.now(); }, disconnected: () => { if (this._connectionStats.connectedAt) { this._connectionStats.durations.push( Date.now() - this._connectionStats.connectedAt ); if (this._connectionStats.durations.length > 100) { this._connectionStats.durations.shift(); } } this._connectionStats.connectedAt = void 0; }, attempt: () => { this._connectionStats.attempts++; } }; /** * Returns the connection stats. */ get connectionStats() { return this._connectionStats; } }; // src/relay/publisher.ts var NDKRelayPublisher = class { ndkRelay; constructor(ndkRelay) { this.ndkRelay = ndkRelay; } /** * Published an event to the relay; if the relay is not connected, it will * wait for the relay to connect before publishing the event. * * If the relay does not connect within the timeout, the publish operation * will fail. * @param event The event to publish * @param timeoutMs The timeout for the publish operation in milliseconds * @returns A promise that resolves when the event has been published or rejects if the operation times out */ async publish(event, timeoutMs = 2500) { const publishWhenConnected = () => { return new Promise((resolve, reject) => { try { this.publishEvent(event, timeoutMs).then((result) => resolve(result)).catch((err) => reject(err)); } catch (err) { reject(err); } }); }; const timeoutPromise = new Promise((_, reject) => { setTimeout(() => reject(new Error("Timeout")), timeoutMs); }); const onConnectHandler = () => { publishWhenConnected().then((result) => connectResolve(result)).catch((err) => connectReject(err)); }; let connectResolve; let connectReject; if (this.ndkRelay.status === 1 /* CONNECTED */) { return Promise.race([publishWhenConnected(), timeoutPromise]); } else { return Promise.race([ new Promise((resolve, reject) => { connectResolve = resolve; connectReject = reject; this.ndkRelay.once("connect", onConnectHandler); }), timeoutPromise ]).finally(() => { this.ndkRelay.removeListener("connect", onConnectHandler); }); } } async publishEvent(event, timeoutMs) { const nostrEvent = await event.toNostrEvent(); const publish = this.ndkRelay.connectivity.relay.publish(nostrEvent); let publishTimeout; const publishPromise = new Promise((resolve, reject) => { publish.then(() => { clearTimeout(publishTimeout); this.ndkRelay.emit("published", event); resolve(true); }).catch((err) => { clearTimeout(publishTimeout); this.ndkRelay.debug("Publish failed", err, event.id); this.ndkRelay.emit("publish:failed", event, err); reject(err); }); }); if (!timeoutMs || event.isEphemeral()) { return publishPromise; } const timeoutPromise = new Promise((_, reject) => { publishTimeout = setTimeout(() => { this.ndkRelay.debug("Publish timed out", event.rawEvent()); this.ndkRelay.emit("publish:failed", event, "Timeout"); reject(new Error("Publish operation timed out")); }, timeoutMs); }); return Promise.race([publishPromise, timeoutPromise]); } }; // src/relay/subscriptions.ts import EventEmitter from "eventemitter3"; import { matchFilter } from "nostr-tools"; // src/subscription/grouping.ts function calculateGroupableId(filters) { const elements = []; for (const filter of filters) { const hasTimeConstraints = filter.since || filter.until; if (hasTimeConstraints) return null; const keys = Object.keys(filter || {}).sort().join("-"); elements.push(keys); } return elements.join("|"); } function mergeFilters(filters) { const result = {}; filters.forEach((filter) => { Object.entries(filter).forEach(([key, value]) => { if (Array.isArray(value)) { if (result[key] === void 0) { result[key] = [...value]; } else { result[key] = Array.from(/* @__PURE__ */ new Set([...result[key], ...value])); } } else { result[key] = value; } }); }); return result; } // src/subscription/utils.ts import { nip19 } from "nostr-tools"; function queryFullyFilled(subscription) { if (filterIncludesIds(subscription.filter)) { if (resultHasAllRequestedIds(subscription)) { return true; } } return false; } function compareFilter(filter1, filter2) { if (Object.keys(filter1).length !== Object.keys(filter2).length) return false; for (const [key, value] of Object.entries(filter1)) { const valuesInFilter2 = filter2[key]; if (!valuesInFilter2) return false; if (Array.isArray(value) && Array.isArray(valuesInFilter2)) { const v = value; for (const valueInFilter2 of valuesInFilter2) { const val = valueInFilter2; if (!v.includes(val)) { return false; } } } else { if (valuesInFilter2 !== value) return false; } } return true; } function filterIncludesIds(filter) { return !!filter["ids"]; } function resultHasAllRequestedIds(subscription) { const ids = subscription.filter["ids"]; return !!ids && ids.length === subscription.eventFirstSeen.size; } function generateSubId(subscriptions, filters) { const subIds = subscriptions.map((sub) => sub.subId).filter(Boolean); const subIdParts = []; const filterNonKindKeys = /* @__PURE__ */ new Set(); const filterKinds = /* @__PURE__ */ new Set(); if (subIds.length > 0) { subIdParts.push(Array.from(new Set(subIds)).join(",")); } else { for (const filter of filters) { for (const key of Object.keys(filter)) { if (key === "kinds") { filter.kinds?.forEach((k) => filterKinds.add(k)); } else { filterNonKindKeys.add(key); } } } if (filterKinds.size > 0) { subIdParts.push("kinds:" + Array.from(filterKinds).join(",")); } if (filterNonKindKeys.size > 0) { subIdParts.push(Array.from(filterNonKindKeys).join(",")); } } if (subIds.length !== 1) { subIdParts.push(Math.floor(Math.random() * 999).toString()); } return subIdParts.join("-"); } function filterFromId(id) { let decoded; if (id.match(NIP33_A_REGEX)) { const [kind, pubkey, identifier] = id.split(":"); const filter = { authors: [pubkey], kinds: [parseInt(kind)] }; if (identifier) { filter["#d"] = [identifier]; } return filter; } try { decoded = nip19.decode(id); switch (decoded.type) { case "nevent": return { ids: [decoded.data.id] }; case "note": return { ids: [decoded.data] }; case "naddr": return { authors: [decoded.data.pubkey], "#d": [decoded.data.identifier], kinds: [decoded.data.kind] }; } } catch (e) { } return { ids: [id] }; } function isNip33AValue(value) { return value.match(NIP33_A_REGEX) !== null; } var NIP33_A_REGEX = /^(\d+):([0-9A-Fa-f]+)(?::(.*))?$/; function relaysFromBech32(bech322) { try { const decoded = nip19.decode(bech322); if (["naddr", "nevent"].includes(decoded?.type)) { const data = decoded.data; if (data?.relays) { return data.relays.map((r) => new NDKRelay(r)); } } } catch (e) { } return []; } // src/relay/subscriptions.ts var NDKGroupedSubscriptions = class extends EventEmitter { subscriptions; req; debug; constructor(subscriptions, debug4) { super(); this.subscriptions = subscriptions; this.debug = debug4 || this.subscriptions[0].subscription.debug.extend("grouped"); for (const subscription of subscriptions) { this.handleSubscriptionClosure(subscription); } } /** * Adds a subscription to this group. * @param subscription */ addSubscription(subscription) { this.subscriptions.push(subscription); this.handleSubscriptionClosure(subscription); } eventReceived(event) { for (const subscription of this.subscriptions) { subscription.eventReceived(event); } } eoseReceived(relay) { const subscriptionsToInform = Array.from(this.subscriptions); subscriptionsToInform.forEach(async (subscription) => { subscription.subscription.eoseReceived(relay); }); } handleSubscriptionClosure(subscription) { subscription.subscription.on("close", () => { const index = this.subscriptions.findIndex( (i) => i.subscription === subscription.subscription ); this.subscriptions.splice(index, 1); if (this.subscriptions.length <= 0) { this.emit("close"); } }); } /** * Maps each subscription through a transformation function. * @param fn - The transformation function. * @returns A new array with each subscription transformed by fn. */ map(fn) { return this.subscriptions.map(fn); } [Symbol.iterator]() { let index = 0; const subscriptions = this.subscriptions; return { next() { if (index < subscriptions.length) { return { value: subscriptions[index++], done: false }; } else { return { value: null, done: true }; } } }; } }; var NDKSubscriptionFilters = class { subscription; filters = []; ndkRelay; constructor(subscription, filters, ndkRelay) { this.subscription = subscription; this.filters = filters; this.ndkRelay = ndkRelay; } eventReceived(event) { if (!this.eventMatchesLocalFilter(event)) return; this.subscription.eventReceived(event, this.ndkRelay, false); } eventMatchesLocalFilter(event) { const rawEvent = event.rawEvent(); return this.filters.some((filter) => matchFilter(filter, rawEvent)); } }; function findMatchingActiveSubscriptions(activeSubscriptions, filters) { if (activeSubscriptions.length !== filters.length) return false; for (let i = 0; i < activeSubscriptions.length; i++) { if (!compareFilter(activeSubscriptions[i], filters[i])) { break; } return activeSubscriptions[i]; } return void 0; } var NDKRelaySubscriptions = class { ndkRelay; delayedItems = /* @__PURE__ */ new Map(); delayedTimers = /* @__PURE__ */ new Map(); /** * Active subscriptions this relay is connected to */ activeSubscriptions = /* @__PURE__ */ new Map(); activeSubscriptionsByGroupId = /* @__PURE__ */ new Map(); debug; groupingDebug; conn; constructor(ndkRelay) { this.ndkRelay = ndkRelay; this.conn = ndkRelay.connectivity; this.debug = ndkRelay.debug.extend("subscriptions"); this.groupingDebug = ndkRelay.debug.extend("grouping"); } /** * Creates or queues a subscription to the relay. */ subscribe(subscription, filters) { const groupableId = calculateGroupableId(filters); const subscriptionFilters = new NDKSubscriptionFilters( subscription, filters, this.ndkRelay ); if (!groupableId || !subscription.isGroupable()) { this.groupingDebug("No groupable ID for filters", filters); this.executeSubscriptions( groupableId, // hacky new NDKGroupedSubscriptions([subscriptionFilters]), filters ); return; } const activeSubscriptions = this.activeSubscriptionsByGroupId.get(groupableId); if (activeSubscriptions) { const matchingSubscription = findMatchingActiveSubscriptions( activeSubscriptions.filters, filters ); if (matchingSubscription) { const activeSubscription = this.activeSubscriptions.get(activeSubscriptions.sub); activeSubscription?.addSubscription( new NDKSubscriptionFilters(subscription, filters, this.ndkRelay) ); return; } } let delayedItem = this.delayedItems.get(groupableId); if (!delayedItem) { delayedItem = new NDKGroupedSubscriptions([subscriptionFilters]); this.delayedItems.set(groupableId, delayedItem); delayedItem.once("close", () => { const delayedItem2 = this.delayedItems.get(groupableId); if (!delayedItem2) return; this.delayedItems.delete(groupableId); }); } else { delayedItem.addSubscription(subscriptionFilters); } const timeout = setTimeout(() => { this.executeGroup(groupableId, subscription); }, subscription.opts.groupableDelay); if (this.delayedTimers.has(groupableId)) { this.delayedTimers.get(groupableId).push(timeout); } else { this.delayedTimers.set(groupableId, [timeout]); } } /** * Executes a delayed subscription via its groupable ID. * @param groupableId */ executeGroup(groupableId, triggeredBy) { const delayedItem = this.delayedItems.get(groupableId); this.delayedItems.delete(groupableId); const timeouts = this.delayedTimers.get(groupableId); this.delayedTimers.delete(groupableId); if (timeouts) { for (const timeout of timeouts) { clearTimeout(timeout); } } if (delayedItem) { const filterCount = delayedItem.subscriptions[0].filters.length; const mergedFilters = []; for (let i = 0; i < filterCount; i++) { const allFiltersAtIndex = delayedItem.map((di) => di.filters[i]); mergedFilters.push(mergeFilters(allFiltersAtIndex)); } this.executeSubscriptions(groupableId, delayedItem, mergedFilters); } } /** * Executes one or more subscriptions. * * If the relay is not connected, subscriptions will be queued * until the relay connects. * * @param groupableId * @param subscriptionFilters * @param mergedFilters */ executeSubscriptions(groupableId, groupedSubscriptions, mergedFilters) { if (this.conn.isAvailable()) { this.executeSubscriptionsConnected(groupableId, groupedSubscriptions, mergedFilters); } else { const connectedListener = () => { this.debug("new relay coming online for active subscription", { relay: this.ndkRelay.url, mergeFilters }); this.executeSubscriptionsConnected( groupableId, groupedSubscriptions, mergedFilters ); }; this.ndkRelay.once("connect", connectedListener); groupedSubscriptions.once("close", () => { this.ndkRelay.removeListener("connect", connectedListener); }); } } /** * Executes one or more subscriptions. * * When there are more than one subscription, results * will be sent to the right subscription * * @param subscriptions * @param filters The filters as they should be sent to the relay */ executeSubscriptionsConnected(groupableId, groupedSubscriptions, mergedFilters) { const subscriptions = []; for (const { subscription } of groupedSubscriptions) { subscriptions.push(subscription); } const subId = generateSubId(subscriptions, mergedFilters); groupedSubscriptions.req = mergedFilters; const sub = this.conn.relay.sub(mergedFilters, { id: subId }); this.activeSubscriptions.set(sub, groupedSubscriptions); if (groupableId) { this.activeSubscriptionsByGroupId.set(groupableId, { filters: mergedFilters, sub }); } sub.on("event", (event) => { const e = new NDKEvent(void 0, event); e.relay = this.ndkRelay; const subFilters = this.activeSubscriptions.get(sub); subFilters?.eventReceived(e); }); sub.on("eose", () => { const subFilters = this.activeSubscriptions.get(sub); subFilters?.eoseReceived(this.ndkRelay); }); groupedSubscriptions.once("close", () => { sub.unsub(); this.activeSubscriptions.delete(sub); if (groupableId) { this.activeSubscriptionsByGroupId.delete(groupableId); } }); return sub; } executedFilters() { const ret = /* @__PURE__ */ new Map(); for (const [, groupedSubscriptions] of this.activeSubscriptions) { ret.set( groupedSubscriptions.req, groupedSubscriptions.map((sub) => sub.subscription) ); } return ret; } }; // src/relay/index.ts var NDKRelayStatus = /* @__PURE__ */ ((NDKRelayStatus2) => { NDKRelayStatus2[NDKRelayStatus2["CONNECTING"] = 0] = "CONNECTING"; NDKRelayStatus2[NDKRelayStatus2["CONNECTED"] = 1] = "CONNECTED"; NDKRelayStatus2[NDKRelayStatus2["DISCONNECTING"] = 2] = "DISCONNECTING"; NDKRelayStatus2[NDKRelayStatus2["DISCONNECTED"] = 3] = "DISCONNECTED"; NDKRelayStatus2[NDKRelayStatus2["RECONNECTING"] = 4] = "RECONNECTING"; NDKRelayStatus2[NDKRelayStatus2["FLAPPING"] = 5] = "FLAPPING"; return NDKRelayStatus2; })(NDKRelayStatus || {}); var NDKRelay = class extends EventEmitter2 { url; scores; connectivity; subs; publisher; complaining = false; debug; constructor(url) { super(); this.url = url; this.scores = /* @__PURE__ */ new Map(); this.debug = debug(`ndk:relay:${url}`); this.connectivity = new NDKRelayConnectivity(this); this.subs = new NDKRelaySubscriptions(this); this.publisher = new NDKRelayPublisher(this); } get status() { return this.connectivity.status; } get connectionStats() { return this.connectivity.connectionStats; } /** * Connects to the relay. */ async connect() { return this.connectivity.connect(); } /** * Disconnects from the relay. */ disconnect() { this.connectivity.disconnect(); } /** * Queues or executes the subscription of a specific set of filters * within this relay. * * @param subscription NDKSubscription this filters belong to. * @param filters Filters to execute */ subscribe(subscription, filters) { this.subs.subscribe(subscription, filters); } /** * Publishes an event to the relay with an optional timeout. * * If the relay is not connected, the event will be published when the relay connects, * unless the timeout is reached before the relay connects. * * @param event The event to publish * @param timeoutMs The timeout for the publish operation in milliseconds * @returns A promise that resolves when the event has been published or rejects if the operation times out */ async publish(event, timeoutMs = 2500) { return this.publisher.publish(event, timeoutMs); } /** * Called when this relay has responded with an event but * wasn't the fastest one. * @param timeDiffInMs The time difference in ms between the fastest and this relay in milliseconds */ // eslint-disable-next-line @typescript-eslint/no-unused-vars scoreSlowerEvent(timeDiffInMs) { } /** @deprecated Use referenceTags instead. */ tagReference(marker) { const tag = ["r", this.url]; if (marker) { tag.push(marker); } return tag; } referenceTags() { return [["r", this.url]]; } activeSubscriptions() { return this.subs.executedFilters(); } }; // src/relay/sets/index.ts var NDKRelaySet = class { relays; debug; ndk; constructor(relays, ndk) { this.relays = relays; this.ndk = ndk; this.debug = ndk.debug.extend("relayset"); } /** * Adds a relay to this set. */ addRelay(relay) { this.relays.add(relay); } /** * Creates a relay set from a list of relay URLs. * * If no connection to the relay is found in the pool it will temporarily * connect to it. * * @param relayUrls - list of relay URLs to include in this set * @param ndk * @returns NDKRelaySet */ static fromRelayUrls(relayUrls, ndk) { const relays = /* @__PURE__ */ new Set(); for (const url of relayUrls) { const relay = ndk.pool.relays.get(url); if (relay) { relays.add(relay); } else { const temporaryRelay = new NDKRelay(url); ndk.pool.useTemporaryRelay(temporaryRelay); relays.add(temporaryRelay); } } return new NDKRelaySet(new Set(relays), ndk); } /** * Publish an event to all relays in this set. Returns the number of relays that have received the event. * @param event * @param timeoutMs - timeout in milliseconds for each publish operation and connection operation * @returns A set where the event was successfully published to */ async publish(event, timeoutMs) { const publishedToRelays = /* @__PURE__ */ new Set(); const isEphemeral2 = event.isEphemeral(); const promises = Array.from(this.relays).map((relay) => { return new Promise((resolve) => { relay.publish(event, timeoutMs).then(() => { publishedToRelays.add(relay); resolve(); }).catch((err) => { if (!isEphemeral2) { this.debug("error publishing to relay", { relay: relay.url, err }); } resolve(); }); }); }); await Promise.all(promises); if (publishedToRelays.size === 0) { if (!isEphemeral2) { throw new Error("No relay was able to receive the event"); } } return publishedToRelays; } size() { return this.relays.size; } }; // src/relay/sets/calculate.ts function calculateRelaySetFromEvent(ndk, event) { const relays = /* @__PURE__ */ new Set(); ndk.pool?.relays.forEach((relay) => relays.add(relay)); return new NDKRelaySet(relays, ndk); } function getWriteRelaysFor(ndk, author) { if (!ndk.outboxTracker) return void 0; return ndk.outboxTracker.data.get(author)?.writeRelays; } function calculateRelaySetsFromFilter(ndk, filters) { const result = /* @__PURE__ */ new Map(); const authors = /* @__PURE__ */ new Set(); filters.forEach((filter) => { if (filter.authors) { filter.authors.forEach((author) => authors.add(author)); } }); if (authors.size > 0) { const authorToRelaysMap = /* @__PURE__ */ new Map(); for (const author of authors) { const userWriteRelays = getWriteRelaysFor(ndk, author); if (userWriteRelays && userWriteRelays.size > 0) { ndk.debug(`Adding ${userWriteRelays.size} relays for ${author}`); userWriteRelays.forEach((relay) => { const authorsInRelay = authorToRelaysMap.get(relay) || []; authorsInRelay.push(author); authorToRelaysMap.set(relay, authorsInRelay); }); } else { ndk.explicitRelayUrls?.forEach((relay) => { const authorsInRelay = authorToRelaysMap.get(relay) || []; authorsInRelay.push(author); authorToRelaysMap.set(relay, authorsInRelay); }); } } for (const relayUrl of authorToRelaysMap.keys()) { result.set(relayUrl, []); } for (const filter of filters) { if (filter.authors) { for (const [relayUrl, authors2] of authorToRelaysMap.entries()) { result.set(relayUrl, [...result.get(relayUrl), { ...filter, authors: authors2 }]); } } else { for (const relayUrl of authorToRelaysMap.keys()) { result.set(relayUrl, [...result.get(relayUrl), filter]); } } } } else { ndk.explicitRelayUrls?.forEach((relay) => { result.set(relay, filters); }); } return result; } function calculateRelaySetsFromFilters(ndk, filters) { return calculateRelaySetsFromFilter(ndk, filters); } // src/zap/index.ts import { bech32 } from "@scure/base"; import EventEmitter3 from "eventemitter3"; import { nip57 } from "nostr-tools"; var DEFAULT_RELAYS = [ "wss://nos.lol", "wss://relay.nostr.band", "wss://relay.f7z.io", "wss://relay.damus.io", "wss://nostr.mom", "wss://no.str.cr" ]; var Zap = class extends EventEmitter3 { ndk; zappedEvent; zappedUser; constructor(args) { super(); this.ndk = args.ndk; this.zappedEvent = args.zappedEvent; this.zappedUser = args.zappedUser || this.ndk.getUser({ hexpubkey: this.zappedEvent?.pubkey }); } async getZapEndpoint() { let lud06; let lud16; let zapEndpoint; let zapEndpointCallback; if (this.zappedUser) { if (!this.zappedUser.profile) { await this.zappedUser.fetchProfile(); } lud06 = (this.zappedUser.profile || {}).lud06; lud16 = (this.zappedUser.profile || {}).lud16; } if (lud16 && !lud16.startsWith("LNURL")) { const [name, domain] = lud16.split("@"); zapEndpoint = `https://${domain}/.well-known/lnurlp/${name}`; } else if (lud06) { const { words } = bech32.decode(lud06, 1e3); const data = bech32.fromWords(words); const utf8Decoder = new TextDecoder("utf-8"); zapEndpoint = utf8Decoder.decode(data); } if (!zapEndpoint) { throw new Error("No zap endpoint found"); } const response = await fetch(zapEndpoint); const body = await response.json(); if (body?.allowsNostr && (body?.nostrPubkey || body?.nostrPubKey)) { zapEndpointCallback = body.callback; } return zapEndpointCallback; } /** * Generates a kind:9734 zap request and returns the payment request * @param amount amount to zap in millisatoshis * @param comment optional comment to include in the zap request * @param extraTags optional extra tags to include in the zap request * @param relays optional relays to ask zapper to publish the zap to * @returns the payment request */ async createZapRequest(amount, comment, extraTags, relays, signer) { const zapEndpoint = await this.getZapEndpoint(); if (!zapEndpoint) { throw new Error("No zap endpoint found"); } if (!this.zappedEvent && !this.zappedUser) throw new Error("No zapped event or user found"); const zapRequest = nip57.makeZapRequest({ profile: this.zappedUser.hexpubkey, // set the event to null since nostr-tools doesn't support nip-33 zaps event: null, amount, comment: comment || "", relays: relays ?? this.relays() }); if (this.zappedEvent) { const tags = this.zappedEvent.referenceTags(); zapRequest.tags.push(...tags); } zapRequest.tags.push(["lnurl", zapEndpoint]); const zapRequestEvent = new NDKEvent(this.ndk, zapRequest); if (extraTags) { zapRequestEvent.tags = zapRequestEvent.tags.concat(extraTags); } await zapRequestEvent.sign(signer); const zapRequestNostrEvent = await zapRequestEvent.toNostrEvent(); const response = await fetch( `${zapEndpoint}?` + new URLSearchParams({ amount: amount.toString(), nostr: JSON.stringify(zapRequestNostrEvent) }) ); const body = await response.json(); return body.pr; } /** * @returns the relays to use for the zap request */ relays() { let r = []; if (this.ndk?.pool?.relays) { r = this.ndk.pool.urls(); } if (!r.length) { r = DEFAULT_RELAYS; } return r; } }; // src/events/content-tagger.ts import { nip19 as nip192 } from "nostr-tools"; async function generateContentTags(content, tags = []) { const tagRegex = /(@|nostr:)(npub|nprofile|note|nevent|naddr)[a-zA-Z0-9]+/g; const hashtagRegex = /#(\w+)/g; let promises = []; const addTagIfNew = (t) => { if (!tags.find((t2) => t2[0] === t[0] && t2[1] === t[1])) { tags.push(t); } }; content = content.replace(tagRegex, (tag) => { try { const entity = tag.split(/(@|nostr:)/)[2]; const { type, data } = nip192.decode(entity); let t; switch (type) { case "npub": t = ["p", data]; break; case "nprofile": t = ["p", data.pubkey]; break; case "note": promises.push( new Promise(async (resolve) => { addTagIfNew([ "e", data, await maybeGetEventRelayUrl(entity), "mention" ]); resolve(); }) ); break; case "nevent": promises.push( new Promise(async (resolve) => { let { id, relays, author } = data; if (!relays || relays.length === 0) { relays = [await maybeGetEventRelayUrl(entity)]; } addTagIfNew(["e", id, relays[0], "mention"]); if (author) addTagIfNew(["p", author]); resolve(); }) ); break; case "naddr": promises.push( new Promise(async (resolve) => { const id = [data.kind, data.pubkey, data.identifier].join(":"); let relays = data.relays ?? []; if (relays.length === 0) { relays = [await maybeGetEventRelayUrl(entity)]; } addTagIfNew(["a", id, relays[0], "mention"]); addTagIfNew(["p", data.pubkey]); resolve(); }) ); break; default: return tag; } if (t) addTagIfNew(t); return `nostr:${entity}`; } catch (error) { return tag; } }); await Promise.all(promises); content = content.replace(hashtagRegex, (tag, word) => { const t = ["t", word]; if (!tags.find((t2) => t2[0] === t[0] && t2[1] === t[1])) { tags.push(t); } return tag; }); return { content, tags }; } async function maybeGetEventRelayUrl(nip19Id) { return ""; } // src/events/kind.ts function isReplaceable() { if (this.kind === void 0) throw new Error("Kind not set"); return this.kind >= 1e4 && this.kind < 2e4; } function isEphemeral() { if (this.kind === void 0) throw new Error("Kind not set"); return this.kind >= 2e4 && this.kind < 3e4; } function isParamReplaceable() { if (this.kind === void 0) throw new Error("Kind not set"); return this.kind >= 3e4 && this.kind < 4e4; } // src/events/kinds/index.ts var NDKKind = /* @__PURE__ */ ((NDKKind2) => { NDKKind2[NDKKind2["Metadata"] = 0] = "Metadata"; NDKKind2[NDKKind2["Text"] = 1] = "Text"; NDKKind2[NDKKind2["RecommendRelay"] = 2] = "RecommendRelay"; NDKKind2[NDKKind2["Contacts"] = 3] = "Contacts"; NDKKind2[NDKKind2["EncryptedDirectMessage"] = 4] = "EncryptedDirectMessage"; NDKKind2[NDKKind2["EventDeletion"] = 5] = "EventDeletion"; NDKKind2[NDKKind2["Repost"] = 6] = "Repost"; NDKKind2[NDKKind2["Reaction"] = 7] = "Reaction"; NDKKind2[NDKKind2["BadgeAward"] = 8] = "BadgeAward"; NDKKind2[NDKKind2["GenericRepost"] = 16] = "GenericRepost"; NDKKind2[NDKKind2["ChannelCreation"] = 40] = "ChannelCreation"; NDKKind2[NDKKind2["ChannelMetadata"] = 41] = "ChannelMetadata"; NDKKind2[NDKKind2["ChannelMessage"] = 42] = "ChannelMessage"; NDKKind2[NDKKind2["ChannelHideMessage"] = 43] = "ChannelHideMessage"; NDKKind2[NDKKind2["ChannelMuteUser"] = 44] = "ChannelMuteUser"; NDKKind2[NDKKind2["Report"] = 1984] = "Report"; NDKKind2[NDKKind2["Label"] = 1985] = "Label"; NDKKind2[NDKKind2["DVMReqTextExtraction"] = 5e3] = "DVMReqTextExtraction"; NDKKind2[NDKKind2["DVMReqTextSummarization"] = 5001] = "DVMReqTextSummarization"; NDKKind2[NDKKind2["DVMReqTextTranslation"] = 5002] = "DVMReqTextTranslation"; NDKKind2[NDKKind2["DVMReqTextGeneration"] = 5050] = "DVMReqTextGeneration"; NDKKind2[NDKKind2["DVMReqImageGeneration"] = 5100] = "DVMReqImageGeneration"; NDKKind2[NDKKind2["DVMReqDiscoveryNostrContent"] = 5300] = "DVMReqDiscoveryNostrContent"; NDKKind2[NDKKind2["DVMReqDiscoveryNostrPeople"] = 5301] = "DVMReqDiscoveryNostrPeople"; NDKKind2[NDKKind2["DVMJobFeedback"] = 7e3] = "DVMJobFeedback"; NDKKind2[NDKKind2["ZapRequest"] = 9734] = "ZapRequest"; NDKKind2[NDKKind2["Zap"] = 9735] = "Zap"; NDKKind2[NDKKind2["Highlight"] = 9802] = "Highlight"; NDKKind2[NDKKind2["MuteList"] = 1e4] = "MuteList"; NDKKind2[NDKKind2["PinList"] = 10001] = "PinList"; NDKKind2[NDKKind2["RelayList"] = 10002] = "RelayList"; NDKKind2[NDKKind2["ClientAuth"] = 22242] = "ClientAuth"; NDKKind2[NDKKind2["NostrConnect"] = 24133] = "NostrConnect"; NDKKind2[NDKKind2["CategorizedPeopleList"] = 3e4] = "CategorizedPeopleList"; NDKKind2[NDKKind2["CategorizedBookmarkList"] = 30001] = "CategorizedBookmarkList"; NDKKind2[NDKKind2["ProfileBadge"] = 30008] = "ProfileBadge"; NDKKind2[NDKKind2["BadgeDefinition"] = 30009] = "BadgeDefinition"; NDKKind2[NDKKind2["InterestsList"] = 30015] = "InterestsList"; NDKKind2[NDKKind2["MarketStall"] = 30017] = "MarketStall"; NDKKind2[NDKKind2["MarketProduct"] = 30018] = "MarketProduct"; NDKKind2[NDKKind2["CategorizedRelayList"] = 30022] = "CategorizedRelayList"; NDKKind2[NDKKind2["Article"] = 30023] = "Article"; NDKKind2[NDKKind2["AppSpecificData"] = 30078] = "AppSpecificData"; NDKKind2[NDKKind2["Classified"] = 30402] = "Classified"; NDKKind2[NDKKind2["AppRecommendation"] = 31989] = "AppRecommendation"; NDKKind2[NDKKind2["AppHandler"] = 31990] = "AppHandler"; NDKKind2[NDKKind2["CategorizedHighlightList"] = 39802] = "CategorizedHighlightList"; return NDKKind2; })(NDKKind || {}); var NDKListKinds = [ 3e4 /* CategorizedPeopleList */, 30001 /* CategorizedBookmarkList */, 39802 /* CategorizedHighlightList */, 30022 /* CategorizedRelayList */, 30015 /* InterestsList */ ]; // src/events/nip04.ts async function encrypt(recipient, signer) { if (!this.ndk) throw new Error("No NDK instance found!"); if (!signer) { await this.ndk.assertSigner(); signer = this.ndk.signer; } if (!recipient) { const pTags = this.getMatchingTags("p"); if (pTags.length !== 1) { throw new Error( "No recipient could be determined and no explicit recipient was provided" ); } recipient = this.ndk.getUser({ hexpubkey: pTags[0][1] }); } this.content = await signer?.encrypt(recipient, this.content); } async function decrypt(sender, signer) { if (!this.ndk) throw new Error("No NDK instance found!"); if (!signer) { await this.ndk.assertSigner(); signer = this.ndk.signer; } if (!sender) { sender = this.author; } this.content = await signer?.decrypt(sender, this.content); } // src/events/nip19.ts import { nip19 as nip193 } from "nostr-tools"; function encode() { if (this.isParamReplaceable()) { return nip193.naddrEncode({ kind: this.kind, pubkey: this.pubkey, identifier: this.replaceableDTag(), relays: this.relay ? [this.relay.url] : [] }); } else if (this.relay) { return nip193.neventEncode({ id: this.tagId(), relays: [this.relay.url], author: this.pubkey }); } else { return nip193.noteEncode(this.tagId()); } } // src/events/repost.ts async function repost(publish = true, signer) { if (!signer && publish) { if (!this.ndk) throw new Error("No NDK instance found"); this.ndk.assertSigner(); signer = this.ndk.signer; } const e = new NDKEvent(this.ndk, { kind: getKind(this), content: "" }); e.tag(this); if (e.kind === 16 /* GenericRepost */) { e.tags.push(["k", `${this.kind}`]); } if (signer) await e.sign(signer); if (publish) await e.publish(); return e; } function getKind(event) { if (event.kind === 1) { return 6 /* Repost */; } return 16 /* GenericRepost */; } // src/events/index.ts var NDKEvent = class extends EventEmitter4 { ndk; created_at; content = ""; tags = []; kind; id = ""; sig; pubkey = ""; _author = void 0; /** * The relay that this event was first received from. */ relay; constructor(ndk, event) { super(); this.ndk = ndk; this.created_at = event?.created_at; this.content = event?.content || ""; this.tags = event?.tags || []; this.id = event?.id || ""; this.sig = event?.sig; this.pubkey = event?.pubkey || ""; this.kind = event?.kind; } /** * Returns the event as is. */ rawEvent() { return { created_at: this.created_at, content: this.content, tags: this.tags, kind: this.kind, pubkey: this.pubkey, id: this.id, sig: this.sig }; } set author(user) { this.pubkey = user.hexpubkey; this._author = void 0; } /** * Returns an NDKUser for the author of the event. */ get author() { if (this._author) return this._author; if (!this.ndk) throw new Error("No NDK instance found"); const user = this.ndk.getUser({ hexpubkey: this.pubkey }); this._author = user; return user; } tag(userOrEvent, marker) { const tags = userOrEvent.referenceTags(); if (marker) tags[0].push(marker); this.tags.push(...tags); if (userOrEvent instanceof NDKEvent) { const tagEventAuthor = userOrEvent.author; if (tagEventAuthor && this.pubkey !== tagEventAuthor.hexpubkey) { this.tag(tagEventAuthor); } for (const pTag of userOrEvent.getMatchingTags("p")) { if (pTag[1] === this.pubkey) continue; if (this.tags.find((t) => t[0] === "p" && t[1] === pTag[1])) continue; this.tags.push(["p", pTag[1]]); } } } /** * Return a NostrEvent object, trying to fill in missing fields * when possible, adding tags when necessary. * @param pubkey {string} The pubkey of the user who the event belongs to. * @returns {Promise} A promise that resolves to a NostrEvent. */ async toNostrEvent(pubkey) { if (!pubkey && this.pubkey === "") { const user = await this.ndk?.signer?.user(); this.pubkey = user?.hexpubkey || ""; } if (!this.created_at) this.created_at = Math.floor(Date.now() / 1e3); const nostrEvent = this.rawEvent(); const { content, tags } = await this.generateTags(); nostrEvent.content = content || ""; nostrEvent.tags = tags; try { this.id = getEventHash(nostrEvent); } catch (e) { } if (this.id) nostrEvent.id = this.id; if (this.sig) nostrEvent.sig = this.sig; return nostrEvent; } isReplaceable = isReplaceable.bind(this); isEphemeral = isEphemeral.bind(this); isParamReplaceable = isParamReplaceable.bind(this); /** * Encodes a bech32 id. * * @returns {string} - Encoded naddr, note or nevent. */ encode = encode.bind(this); encrypt = encrypt.bind(this); decrypt = decrypt.bind(this); /** * Get all tags with the given name * @param tagName {string} The name of the tag to search for * @returns {NDKTag[]} An array of the matching tags */ getMatchingTags(tagName) { return this.tags.filter((tag) => tag[0] === tagName); } /** * Get the first tag with the given name * @param tagName Tag name to search for * @returns The value of the first tag with the given name, or undefined if no such tag exists */ tagValue(tagName) { const tags = this.getMatchingTags(tagName); if (tags.length === 0) return void 0; return tags[0][1]; } /** * Remove all tags with the given name (e.g. "d", "a", "p") * @param tagName Tag name to search for and remove * @returns {void} */ removeTag(tagName) { this.tags = this.tags.filter((tag) => tag[0] !== tagName); } /** * Sign the event if a signer is present. * * It will generate tags. * Repleacable events will have their created_at field set to the current time. * @param signer {NDKSigner} The NDKSigner to use to sign the event * @returns {Promise} A Promise that resolves to the signature of the signed event. */ async sign(signer) { if (!signer) { this.ndk?.assertSigner(); signer = this.ndk.signer; } else { this.author = await signer.user(); } await this.generateTags(); if (this.isReplaceable()) { this.created_at = Math.floor(Date.now() / 1e3); } const nostrEvent = await this.toNostrEvent(); this.sig = await signer.sign(nostrEvent); return this.sig; } /** * Attempt to sign and then publish an NDKEvent to a given relaySet. * If no relaySet is provided, the relaySet will be calculated by NDK. * @param relaySet {NDKRelaySet} The relaySet to publish the even to. * @returns A promise that resolves to the relays the event was published to. */ async publish(relaySet, timeoutMs) { if (!this.sig) await this.sign(); if (!this.ndk) throw new Error("NDKEvent must be associated with an NDK instance to publish"); if (!relaySet) { relaySet = this.ndk.devWriteRelaySet || calculateRelaySetFromEvent(this.ndk, this); } return relaySet.publish(this, timeoutMs); } /** * Generates tags for users, notes, and other events tagged in content. * Will also generate random "d" tag for parameterized replaceable events where needed. * @returns {ContentTag} The tags and content of the event. */ async generateTags() { let tags = []; const g = await generateContentTags(this.content, this.tags); const content = g.content; tags = g.tags; if (this.kind && this.isParamReplaceable()) { const dTag = this.getMatchingTags("d")[0]; if (!dTag) { const str = [...Array(16)].map(() => Math.random().toString(36)[2]).join(""); tags.push(["d", str]); } } return { content: content || "", tags }; } muted() { const authorMutedEntry = this.ndk?.mutedIds.get(this.pubkey); if (authorMutedEntry && authorMutedEntry === "p") return "author"; const eventTagReference = this.tagReference(); const eventMutedEntry = this.ndk?.mutedIds.get(eventTagReference[1]); if (eventMutedEntry && eventMutedEntry === eventTagReference[0]) return "event"; return null; } /** * Returns the "d" tag of a parameterized replaceable event or throws an error if the event isn't * a parameterized replaceable event. * @returns {string} the "d" tag of the event. */ replaceableDTag() { if (this.kind && this.kind >= 3e4 && this.kind <= 4e4) { const dTag = this.getMatchingTags("d")[0]; const dTagId = dTag ? dTag[1] : ""; return dTagId; } throw new Error("Event is not a parameterized replaceable event"); } /** * Provides a deduplication key for the event. * * For kinds 0, 3, 10k-20k this will be the event : * For kinds 30k-40k this will be the event :: * For all other kinds this will be the event id */ deduplicationKey() { if (this.kind === 0 || this.kind === 3 || this.kind && this.kind >= 1e4 && this.kind < 2e4) { return `${this.kind}:${this.pubkey}`; } else { return this.tagId(); } } /** * Returns the id of the event or, if it's a parameterized event, the generated id of the event using "d" tag, pubkey, and kind. * @returns {string} The id */ tagId() { if (this.isParamReplaceable()) { return this.tagAddress(); } return this.id; } /** * Returns the "reference" value ("::") for this replaceable event. * @returns {string} The id */ tagAddress() { if (!this.isParamReplaceable()) { throw new Error("This must only be called on replaceable events"); } const dTagId = this.replaceableDTag(); return `${this.kind}:${this.pubkey}:${dTagId}`; } /** @deprecated Use referenceTags instead. */ /** * Get the tag that can be used to reference this event from another event * @example * event = new NDKEvent(ndk, { kind: 30000, pubkey: 'pubkey', tags: [ ["d", "d-code"] ] }); * event.tagReference(); // ["a", "30000:pubkey:d-code"] * * event = new NDKEvent(ndk, { kind: 1, pubkey: 'pubkey', id: "eventid" }); * event.tagReference(); // ["e", "eventid"] * @returns {NDKTag} The NDKTag object referencing this event */ tagReference() { if (this.isParamReplaceable()) { return ["a", this.tagAddress()]; } return ["e", this.tagId()]; } /** * Get the tags that can be used to reference this event from another event * @param marker The marker to use in the tag * @example * event = new NDKEvent(ndk, { kind: 30000, pubkey: 'pubkey', tags: [ ["d", "d-code"] ] }); * event.referenceTags(); // [["a", "30000:pubkey:d-code"], ["e", "parent-id"]] * * event = new NDKEvent(ndk, { kind: 1, pubkey: 'pubkey', id: "eventid" }); * event.referenceTags(); // [["e", "parent-id"]] * @returns {NDKTag} The NDKTag object referencing this event */ referenceTags(marker) { let tags = []; if (this.isParamReplaceable()) { tags = [ ["a", this.tagAddress()], ["e", this.id] ]; } else { tags = [["e", this.id]]; } if (marker) { tags.forEach((tag) => tag.push(marker)); } tags.push(...this.author.referenceTags()); return tags; } /** * Provides the filter that will return matching events for this event. * * @example * event = new NDKEvent(ndk, { kind: 30000, pubkey: 'pubkey', tags: [ ["d", "d-code"] ] }); * event.filter(); // { "#a": ["30000:pubkey:d-code"] } * @example * event = new NDKEvent(ndk, { kind: 1, pubkey: 'pubkey', id: "eventid" }); * event.filter(); // { "#e": ["eventid"] } * * @returns The filter that will return matching events for this event */ filter() { if (this.isParamReplaceable()) { return { "#a": [this.tagId()] }; } else { return { "#e": [this.tagId()] }; } } /** * Create a zap request for an existing event * * @param amount The amount to zap in millisatoshis * @param comment A comment to add to the zap request * @param extraTags Extra tags to add to the zap request * @param recipient The zap recipient (optional for events) * @param signer The signer to use (will default to the NDK instance's signer) */ async zap(amount, comment, extraTags, recipient, signer) { if (!this.ndk) throw new Error("No NDK instance found"); if (!signer) { this.ndk.assertSigner(); } const zap = new Zap({ ndk: this.ndk, zappedEvent: this, zappedUser: recipient }); const relays = Array.from(this.ndk.pool.relays.keys()); const paymentRequest = await zap.createZapRequest( amount, comment, extraTags, relays, signer ); return paymentRequest; } /** * Generates a deletion event of the current event * * @param reason The reason for the deletion * @returns The deletion event */ async delete(reason) { if (!this.ndk) throw new Error("No NDK instance found"); this.ndk.assertSigner(); const e = new NDKEvent(this.ndk, { kind: 5 /* EventDeletion */, content: reason || "" }); e.tag(this); await e.publish(); return e; } /** * NIP-18 reposting event. * * @param publish Whether to publish the reposted event automatically * @param signer The signer to use for signing the reposted event * @returns The reposted event * * @function */ repost = repost.bind(this); /** * React to an existing event * * @param content The content of the reaction */ async react(content) { if (!this.ndk) throw new Error("No NDK instance found"); this.ndk.assertSigner(); const e = new NDKEvent(this.ndk, { kind: 7 /* Reaction */, content }); e.tag(this); await e.publish(); return e; } }; // src/events/kinds/NDKRelayList.ts var READ_MARKER = "read"; var WRITE_MARKER = "write"; var NDKRelayList = class extends NDKEvent { constructor(ndk, rawEvent) { super(ndk, rawEvent); this.kind ??= 10002 /* RelayList */; } static from(ndkEvent) { return new NDKRelayList(ndkEvent.ndk, ndkEvent.rawEvent()); } get readRelayUrls() { return this.getMatchingTags("r").filter((tag) => !tag[2] || tag[2] && tag[2] === READ_MARKER).map((tag) => tag[1]); } set readRelayUrls(relays) { for (const relay of relays) { this.tags.push(["r", relay, READ_MARKER]); } } get writeRelayUrls() { return this.getMatchingTags("r").filter((tag) => !tag[2] || tag[2] && tag[2] === WRITE_MARKER).map((tag) => tag[1]); } set writeRelayUrls(relays) { for (const relay of relays) { this.tags.push(["r", relay, WRITE_MARKER]); } } get bothRelayUrls() { return this.getMatchingTags("r").filter((tag) => !tag[2]).map((tag) => tag[1]); } set bothRelayUrls(relays) { for (const relay of relays) { this.tags.push(["r", relay]); } } get relays() { return this.getMatchingTags("r").map((tag) => tag[1]); } }; // src/subscription/index.ts import EventEmitter5 from "eventemitter3"; var NDKSubscriptionCacheUsage = /* @__PURE__ */ ((NDKSubscriptionCacheUsage2) => { NDKSubscriptionCacheUsage2["ONLY_CACHE"] = "ONLY_CACHE"; NDKSubscriptionCacheUsage2["CACHE_FIRST"] = "CACHE_FIRST"; NDKSubscriptionCacheUsage2["PARALLEL"] = "PARALLEL"; NDKSubscriptionCacheUsage2["ONLY_RELAY"] = "ONLY_RELAY"; return NDKSubscriptionCacheUsage2; })(NDKSubscriptionCacheUsage || {}); var defaultOpts = { closeOnEose: false, cacheUsage: "CACHE_FIRST" /* CACHE_FIRST */, groupable: true, groupableDelay: 100 }; var NDKSubscription = class extends EventEmitter5 { subId; filters; opts; pool; /** * Tracks the filters as they are executed on each relay */ relayFilters; relaySet; ndk; debug; eoseDebug; /** * Events that have been seen by the subscription, with the time they were first seen. */ eventFirstSeen = /* @__PURE__ */ new Map(); /** * Relays that have sent an EOSE. */ eosesSeen = /* @__PURE__ */ new Set(); /** * Events that have been seen by the subscription per relay. */ eventsPerRelay = /* @__PURE__ */ new Map(); /** * The time the last event was received by the subscription. * This is used to calculate when EOSE should be emitted. */ lastEventReceivedAt; internalId; constructor(ndk, filters, opts, relaySet, subId) { super(); this.ndk = ndk; this.pool = opts?.pool || ndk.pool; this.opts = { ...defaultOpts, ...opts || {} }; this.filters = filters instanceof Array ? filters : [filters]; this.subId = subId || opts?.subId; this.internalId = Math.random().toString(36).substring(7); this.relaySet = relaySet; this.debug = ndk.debug.extend(`subscription[${opts?.subId ?? this.internalId}]`); 5; this.eoseDebug = this.debug.extend("eose"); if (!this.opts.closeOnEose) { this.debug( `Creating a permanent subscription`, this.opts, JSON.stringify(this.filters) ); } if (this.opts.cacheUsage === "ONLY_CACHE" /* ONLY_CACHE */ && !this.opts.closeOnEose) { throw new Error("Cannot use cache-only options with a persistent subscription"); } } /** * Provides access to the first filter of the subscription for * backwards compatibility. */ get filter() { return this.filters[0]; } isGroupable() { return this.opts?.groupable || false; } shouldQueryCache() { return this.opts?.cacheUsage !== "ONLY_RELAY" /* ONLY_RELAY */; } shouldQueryRelays() { return this.opts?.cacheUsage !== "ONLY_CACHE" /* ONLY_CACHE */; } shouldWaitForCache() { return ( // Must want to close on EOSE; subscriptions // that want to receive further updates must // always hit the relay this.opts.closeOnEose && // Cache adapter must claim to be fast !!this.ndk.cacheAdapter?.locking && // If explicitly told to run in parallel, then // we should not wait for the cache this.opts.cacheUsage !== "PARALLEL" /* PARALLEL */ ); } /** * Start the subscription. This is the main method that should be called * after creating a subscription. */ async start() { let cachePromise; if (this.shouldQueryCache()) { cachePromise = this.startWithCache(); if (this.shouldWaitForCache()) { await cachePromise; if (queryFullyFilled(this)) { this.emit("eose", this); return; } } } if (this.shouldQueryRelays()) { this.startWithRelays(); } else { this.emit("eose", this); } return; } stop() { this.emit("close", this); } /** * @returns Whether the subscription has an authors filter. */ hasAuthorsFilter() { return this.filters.some((f) => f.authors?.length); } async startWithCache() { if (this.ndk.cacheAdapter?.query) { const promise = this.ndk.cacheAdapter.query(this); if (this.ndk.cacheAdapter.locking) { await promise; } } } /** * Send REQ to relays */ startWithRelays() { if (!this.relaySet) { this.relayFilters = calculateRelaySetsFromFilters(this.ndk, this.filters); } else { this.relayFilters = /* @__PURE__ */ new Map(); for (const relay of this.relaySet.relays) { this.relayFilters.set(relay.url, this.filters); } } for (const [relayUrl, filters] of this.relayFilters) { const relay = this.pool.getRelay(relayUrl); relay.subscribe(this, filters); } } // EVENT handling /** * Called when an event is received from a relay or the cache * @param event * @param relay * @param fromCache Whether the event was received from the cache */ eventReceived(event, relay, fromCache = false) { if (relay) event.relay = relay; if (!relay) relay = event.relay; if (!fromCache && relay) { let events = this.eventsPerRelay.get(relay); if (!events) { events = /* @__PURE__ */ new Set(); this.eventsPerRelay.set(relay, events); } events.add(event.id); const eventAlreadySeen = this.eventFirstSeen.has(event.id); if (eventAlreadySeen) { const timeSinceFirstSeen = Date.now() - (this.eventFirstSeen.get(event.id) || 0); relay.scoreSlowerEvent(timeSinceFirstSeen); this.emit("event:dup", event, relay, timeSinceFirstSeen, this); return; } if (this.ndk.cacheAdapter) { this.ndk.cacheAdapter.setEvent(event, this.filters[0], relay); } this.eventFirstSeen.set(event.id, Date.now()); } else { this.eventFirstSeen.set(event.id, 0); } if (!event.ndk) event.ndk = this.ndk; this.emit("event", event, relay, this); this.lastEventReceivedAt = Date.now(); } // EOSE handling eoseTimeout; eoseReceived(relay) { this.eosesSeen.add(relay); this.eoseDebug(`received from ${relay.url}`); let lastEventSeen = this.lastEventReceivedAt ? Date.now() - this.lastEventReceivedAt : void 0; const hasSeenAllEoses = this.eosesSeen.size === this.relayFilters?.size; const queryFilled = queryFullyFilled(this); if (queryFilled) { this.emit("eose"); this.eoseDebug(`Query fully filled`); if (this.opts?.closeOnEose) { this.stop(); } else { } } else if (hasSeenAllEoses) { this.emit("eose"); this.eoseDebug(`All EOSEs seen`); if (this.opts?.closeOnEose) { this.stop(); } else { } } else { let timeToWaitForNextEose = 1e3; const percentageOfRelaysThatHaveSentEose = this.eosesSeen.size / this.relayFilters.size; if (this.eosesSeen.size >= 2 && percentageOfRelaysThatHaveSentEose >= 0.5) { timeToWaitForNextEose = timeToWaitForNextEose * (1 - percentageOfRelaysThatHaveSentEose); if (this.eoseTimeout) { clearTimeout(this.eoseTimeout); } const sendEoseTimeout = () => { lastEventSeen = this.lastEventReceivedAt ? Date.now() - this.lastEventReceivedAt : void 0; if (lastEventSeen !== void 0 && lastEventSeen < 20) { this.eoseTimeout = setTimeout(sendEoseTimeout, timeToWaitForNextEose); } else { this.emit("eose"); if (this.opts?.closeOnEose) this.stop(); } }; this.eoseTimeout = setTimeout(sendEoseTimeout, timeToWaitForNextEose); } } } }; // src/user/follows.ts async function follows(opts, outbox) { if (!this.ndk) throw new Error("NDK not set"); const contactListEvent = await this.ndk.fetchEvent( { kinds: [3], authors: [this.hexpubkey] }, opts || { groupable: false } ); if (contactListEvent) { const pubkeys = /* @__PURE__ */ new Set(); contactListEvent.tags.forEach((tag) => { if (tag[0] === "p") { try { pubkeys.add(tag[1]); if (outbox) { this.ndk?.outboxTracker?.trackUsers([tag[1]]); } } catch (e) { } } }); return [...pubkeys].reduce((acc, hexpubkey) => { const user = new NDKUser({ hexpubkey }); user.ndk = this.ndk; acc.add(user); return acc; }, /* @__PURE__ */ new Set()); } return /* @__PURE__ */ new Set(); } // src/user/profile.ts function profileFromEvent(event) { const profile = {}; const payload = JSON.parse(event.content); Object.keys(payload).forEach((key) => { switch (key) { case "name": profile.name = payload.name; break; case "display_name": profile.displayName = payload.display_name; break; case "image": case "picture": profile.image = payload.image || payload.picture; break; case "banner": profile.banner = payload.banner; break; case "bio": profile.bio = payload.bio; break; case "nip05": profile.nip05 = payload.nip05; break; case "lud06": profile.lud06 = payload.lud06; break; case "lud16": profile.lud16 = payload.lud16; break; case "about": profile.about = payload.about; break; case "zapService": profile.zapService = payload.zapService; break; case "website": profile.website = payload.website; break; default: profile[key] = payload[key]; break; } }); return profile; } function serializeProfile(profile) { const payload = {}; for (const [key, val] of Object.entries(profile)) { switch (key) { case "username": case "name": payload.name = val; break; case "displayName": payload.display_name = val; break; case "image": case "picture": payload.picture = val; break; case "bio": case "about": payload.about = val; break; default: payload[key] = val; break; } } return JSON.stringify(payload); } // src/user/index.ts var NDKUser = class { ndk; profile; _npub; _pubkey; relayUrls = []; constructor(opts) { if (opts.npub) this._npub = opts.npub; if (opts.hexpubkey) this._pubkey = opts.hexpubkey; if (opts.pubkey) this._pubkey = opts.pubkey; if (opts.relayUrls) { this.relayUrls = opts.relayUrls; } } get npub() { if (!this._npub) { if (!this._pubkey) throw new Error("hexpubkey not set"); this._npub = nip194.npubEncode(this.hexpubkey); } return this._npub; } set npub(npub) { this._npub = npub; } /** * Get the user's hexpubkey * @returns {Hexpubkey} The user's hexpubkey * * @deprecated Use `pubkey` instead */ get hexpubkey() { return this.pubkey; } /** * Set the user's hexpubkey * @param pubkey {Hexpubkey} The user's hexpubkey * @deprecated Use `pubkey` instead */ set hexpubkey(pubkey) { this._pubkey = pubkey; } /** * Get the user's pubkey * @returns {string} The user's pubkey */ get pubkey() { if (!this._pubkey) { if (!this._npub) throw new Error("npub not set"); this._pubkey = nip194.decode(this.npub).data; } return this._pubkey; } /** * Set the user's pubkey * @param pubkey {string} The user's pubkey */ set pubkey(pubkey) { this._pubkey = pubkey; } /** * Instantiate an NDKUser from a NIP-05 string * @param nip05Id {string} The user's NIP-05 * @returns {NDKUser | undefined} An NDKUser if one is found for the given NIP-05, undefined otherwise. */ static async fromNip05(nip05Id) { const profile = await nip05.queryProfile(nip05Id); if (profile) { return new NDKUser({ hexpubkey: profile.pubkey, relayUrls: profile.relays }); } } /** * Fetch a user's profile * @param opts {NDKSubscriptionOptions} A set of NDKSubscriptionOptions * @returns User Profile */ async fetchProfile(opts) { if (!this.ndk) throw new Error("NDK not set"); if (!this.profile) this.profile = {}; let setMetadataEvents = null; if (this.ndk.cacheAdapter && this.ndk.cacheAdapter.fetchProfile && opts?.cacheUsage !== "ONLY_RELAY" /* ONLY_RELAY */) { const profile = await this.ndk.cacheAdapter.fetchProfile(this.hexpubkey); if (profile) { this.profile = profile; return profile; } } if (!opts && // if no options have been set this.ndk.cacheAdapter && // and we have a cache this.ndk.cacheAdapter.locking) { setMetadataEvents = await this.ndk.fetchEvents( { kinds: [0], authors: [this.hexpubkey] }, { cacheUsage: "ONLY_CACHE" /* ONLY_CACHE */, closeOnEose: true, groupable: false } ); opts = { cacheUsage: "ONLY_RELAY" /* ONLY_RELAY */, closeOnEose: true, groupable: true, groupableDelay: 250 }; } if (!setMetadataEvents || setMetadataEvents.size === 0) { setMetadataEvents = await this.ndk.fetchEvents( { kinds: [0], authors: [this.hexpubkey] }, opts ); } const sortedSetMetadataEvents = Array.from(setMetadataEvents).sort( (a, b) => a.created_at - b.created_at ); if (sortedSetMetadataEvents.length === 0) return null; this.profile = profileFromEvent(sortedSetMetadataEvents[0]); if (this.profile && this.ndk.cacheAdapter && this.ndk.cacheAdapter.saveProfile) { this.ndk.cacheAdapter.saveProfile(this.hexpubkey, this.profile); } return this.profile; } /** * Returns a set of users that this user follows. */ follows = follows.bind(this); /** * Returns a set of relay list events for a user. * @returns {Promise>} A set of NDKEvents returned for the given user. */ async relayList() { if (!this.ndk) throw new Error("NDK not set"); const pool = this.ndk.outboxPool || this.ndk.pool; const set = /* @__PURE__ */ new Set(); for (const relay of pool.relays.values()) set.add(relay); const relaySet = new NDKRelaySet(set, this.ndk); const event = await this.ndk.fetchEvent( { kinds: [10002], authors: [this.hexpubkey] }, { closeOnEose: true, pool, groupable: true, subId: `relay-list-${this.hexpubkey.slice(0, 6)}` }, relaySet ); if (event) return NDKRelayList.from(event); return await this.relayListFromKind3(); } async relayListFromKind3() { if (!this.ndk) throw new Error("NDK not set"); const followList = await this.ndk.fetchEvent({ kinds: [3], authors: [this.hexpubkey] }); if (followList) { try { const content = JSON.parse(followList.content); const relayList = new NDKRelayList(this.ndk); const readRelays = /* @__PURE__ */ new Set(); const writeRelays = /* @__PURE__ */ new Set(); for (const [key, config] of Object.entries(content)) { if (!config) { readRelays.add(key); writeRelays.add(key); } else { const relayConfig = config; if (relayConfig.write) writeRelays.add(key); if (relayConfig.read) readRelays.add(key); } } relayList.readRelayUrls = Array.from(readRelays); relayList.writeRelayUrls = Array.from(writeRelays); return relayList; } catch (e) { } } return void 0; } /** @deprecated Use referenceTags instead. */ /** * Get the tag that can be used to reference this user in an event * @returns {NDKTag} an NDKTag */ tagReference() { return ["p", this.hexpubkey]; } /** * Get the tags that can be used to reference this user in an event * @returns {NDKTag[]} an array of NDKTag */ referenceTags() { return [["p", this.hexpubkey]]; } /** * Publishes the current profile. */ async publish() { if (!this.ndk) throw new Error("No NDK instance found"); if (!this.profile) throw new Error("No profile available"); this.ndk.assertSigner(); const event = new NDKEvent(this.ndk, { kind: 0, content: serializeProfile(this.profile) }); await event.publish(); } /** * Add a follow to this user's contact list * * @param newFollow {NDKUser} The user to follow * @param currentFollowList {Set} The current follow list * @returns {Promise} True if the follow was added, false if the follow already exists */ async follow(newFollow, currentFollowList) { if (!this.ndk) throw new Error("No NDK instance found"); this.ndk.assertSigner(); if (!currentFollowList) { currentFollowList = await this.follows(); } if (currentFollowList.has(newFollow)) { return false; } currentFollowList.add(newFollow); const event = new NDKEvent(this.ndk, { kind: 3 /* Contacts */ }); for (const follow of currentFollowList) { event.tag(follow); } await event.publish(); return true; } /** * Validate a user's NIP-05 identifier (usually fetched from their kind:0 profile data) * * @param nip05Id The NIP-05 string to validate * @returns {Promise} True if the NIP-05 is found and matches this user's hexpubkey, * False if the NIP-05 is found but doesn't match this user's hexpubkey, * null if the NIP-05 isn't found on the domain or we're unable to verify (because of network issues, etc.) */ async validateNip05(nip05Id) { if (!this.ndk) throw new Error("No NDK instance found"); const profilePointer = await nip05.queryProfile(nip05Id); if (profilePointer === null) return null; return profilePointer.pubkey === this.hexpubkey; } /** * Zap a user * * @param amount The amount to zap in millisatoshis * @param comment A comment to add to the zap request * @param extraTags Extra tags to add to the zap request * @param signer The signer to use (will default to the NDK instance's signer) */ async zap(amount, comment, extraTags, signer) { if (!this.ndk) throw new Error("No NDK instance found"); if (!signer) { this.ndk.assertSigner(); } const zap = new Zap({ ndk: this.ndk, zappedUser: this }); const relays = Array.from(this.ndk.pool.relays.keys()); const paymentRequest = await zap.createZapRequest( amount, comment, extraTags, relays, signer ); return paymentRequest; } }; // src/events/kinds/article.ts var NDKArticle = class extends NDKEvent { constructor(ndk, rawEvent) { super(ndk, rawEvent); this.kind = 30023 /* Article */; } /** * Creates a NDKArticle from an existing NDKEvent. * * @param event NDKEvent to create the NDKArticle from. * @returns NDKArticle */ static from(event) { return new NDKArticle(event.ndk, event.rawEvent()); } /** * Getter for the article title. * * @returns {string | undefined} - The article title if available, otherwise undefined. */ get title() { return this.tagValue("title"); } /** * Setter for the article title. * * @param {string | undefined} title - The title to set for the article. */ set title(title) { this.removeTag("title"); if (title) this.tags.push(["title", title]); } /** * Getter for the article image. * * @returns {string | undefined} - The article image if available, otherwise undefined. */ get image() { return this.tagValue("image"); } /** * Setter for the article image. * * @param {string | undefined} image - The image to set for the article. */ set image(image) { this.removeTag("image"); if (image) this.tags.push(["image", image]); } get summary() { return this.tagValue("summary"); } /** * Getter for the article's publication timestamp. * * @returns {number | undefined} - The Unix timestamp of when the article was published or undefined. */ get published_at() { const tag = this.tagValue("published_at"); if (tag) { return parseInt(tag); } return void 0; } /** * Setter for the article's publication timestamp. * * @param {number | undefined} timestamp - The Unix timestamp to set for the article's publication date. */ set published_at(timestamp) { this.removeTag("published_at"); if (timestamp !== void 0) { this.tags.push(["published_at", timestamp.toString()]); } } /** * Generates content tags for the article. * * This method first checks and sets the publication date if not available, * and then generates content tags based on the base NDKEvent class. * * @returns {ContentTag} - The generated content tags. */ async generateTags() { super.generateTags(); if (!this.published_at) { this.published_at = this.created_at; } return super.generateTags(); } /** * Getter for the article's URL. * * @returns {string | undefined} - The article's URL if available, otherwise undefined. */ get url() { return this.tagValue("url"); } /** * Setter for the article's URL. * * @param {string | undefined} url - The URL to set for the article. */ set url(url) { if (url) { this.tags.push(["url", url]); } else { this.removeTag("url"); } } }; // src/events/kinds/highlight.ts import { nip19 as nip195 } from "nostr-tools"; var NDKHighlight = class extends NDKEvent { _article; constructor(ndk, rawEvent) { super(ndk, rawEvent); this.kind ??= 9802 /* Highlight */; } static from(event) { return new NDKHighlight(event.ndk, event.rawEvent()); } get url() { return this.tagValue("r"); } /** * Context tag. */ set context(context) { if (context === void 0) { this.tags = this.tags.filter(([tag, value]) => tag !== "context"); } else { this.tags = this.tags.filter(([tag, value]) => tag !== "context"); this.tags.push(["context", context]); } } get context() { return this.tags.find(([tag, value]) => tag === "context")?.[1] ?? void 0; } /** * Will return the article URL or NDKEvent if they have already been * set (it won't attempt to load remote events) */ get article() { return this._article; } /** * Article the highlight is coming from. * * @param article Article URL or NDKEvent. */ set article(article) { this._article = article; if (typeof article === "string") { this.tags.push(["r", article]); } else { this.tag(article); } } getArticleTag() { return this.getMatchingTags("a")[0] || this.getMatchingTags("e")[0] || this.getMatchingTags("r")[0]; } async getArticle() { if (this._article !== void 0) return this._article; let taggedBech32; const articleTag = this.getArticleTag(); if (!articleTag) return void 0; switch (articleTag[0]) { case "a": const [kind, pubkey, identifier] = articleTag[1].split(":"); taggedBech32 = nip195.naddrEncode({ kind: parseInt(kind), pubkey, identifier }); break; case "e": taggedBech32 = nip195.noteEncode(articleTag[1]); break; case "r": this._article = articleTag[1]; break; } if (taggedBech32) { let a = await this.ndk?.fetchEvent(taggedBech32); if (a) { if (a.kind === 30023 /* Article */) { a = NDKArticle.from(a); } this._article = a; } } return this._article; } }; // src/events/kinds/dvm/NDKDVMJobFeedback.ts var NDKDvmJobFeedbackStatus = /* @__PURE__ */ ((NDKDvmJobFeedbackStatus2) => { NDKDvmJobFeedbackStatus2["Processing"] = "processing"; NDKDvmJobFeedbackStatus2["Success"] = "success"; NDKDvmJobFeedbackStatus2["Scheduled"] = "scheduled"; NDKDvmJobFeedbackStatus2["PayReq"] = "payment_required"; return NDKDvmJobFeedbackStatus2; })(NDKDvmJobFeedbackStatus || {}); var NDKDVMJobFeedback = class extends NDKEvent { constructor(ndk, event) { super(ndk, event); this.kind ??= 7e3 /* DVMJobFeedback */; } static from(event) { return new NDKDVMJobFeedback(event.ndk, event.rawEvent()); } get status() { return this.tagValue("status"); } set status(status) { this.removeTag("status"); if (status !== void 0) { this.tags.push(["status", status]); } } }; // src/events/kinds/dvm/NDKDVMJobResult.ts var NDKDVMJobResult = class extends NDKEvent { constructor(ndk, event) { super(ndk, event); } static from(event) { return new NDKDVMJobResult(event.ndk, event.rawEvent()); } setAmount(msat, invoice) { this.removeTag("amount"); const tag = ["amount", msat.toString()]; if (invoice) tag.push(invoice); this.tags.push(tag); } set result(result) { if (result === void 0) { this.content = ""; } else { this.content = result; } } get result() { if (this.content === "") { return void 0; } return this.content; } set status(status) { this.removeTag("status"); if (status !== void 0) { this.tags.push(["status", status]); } } get status() { return this.tagValue("status"); } get jobRequestId() { for (const eTag of this.getMatchingTags("e")) { if (eTag[2] === "job") return eTag[1]; } if (this.jobRequest) return this.jobRequest.id; return this.tagValue("e"); } set jobRequest(event) { this.removeTag("request"); if (event) { this.kind = event.kind + 1e3; this.tags.push(["request", JSON.stringify(event.rawEvent())]); this.tag(event); } } get jobRequest() { const tag = this.tagValue("request"); if (tag === void 0) { return void 0; } return new NDKEvent(this.ndk, JSON.parse(tag)); } }; // src/events/kinds/dvm/NDKDVMRequest.ts var NDKDVMRequest = class extends NDKEvent { constructor(ndk, event) { super(ndk, event); if (ndk) { this.tags.push(["relays", ...ndk.pool.urls()]); } } static from(event) { return new NDKDVMRequest(event.ndk, event.rawEvent()); } /** * Create a new job feedback for this request */ createFeedback(status) { const feedback = new NDKDVMJobFeedback(this.ndk); feedback.status = status; feedback.tag(this, "job"); return feedback; } /** * Create a new result event for this request */ createResult(data) { const event = new NDKDVMJobResult(this.ndk, data); event.jobRequest = this; return event; } set bid(msatAmount) { if (msatAmount === void 0) { this.removeTag("bid"); } else { this.tags.push(["bid", msatAmount.toString()]); } } get bid() { const v = this.tagValue("bid"); if (v === void 0) return void 0; return parseInt(v); } /** * Adds a new input to the job * @param args The arguments to the input */ addInput(...args) { this.tags.push(["i", ...args]); } /** * Adds a new parameter to the job */ addParam(...args) { this.tags.push(["param", ...args]); } set output(output) { if (output === void 0) { this.removeTag("output"); } else { if (typeof output === "string") output = [output]; this.tags.push(["output", ...output]); } } get output() { const outputTag = this.getMatchingTags("output")[0]; return outputTag ? outputTag.slice(1) : void 0; } get params() { const paramTags = this.getMatchingTags("param"); return paramTags.map((t) => t.slice(1)); } getParam(name) { const paramTag = this.getMatchingTags("param").find((t) => t[1] === name); return paramTag ? paramTag[2] : void 0; } }; // src/events/kinds/dvm/NDKTranscriptionDVM.ts var NDKTranscriptionDVM = class extends NDKDVMRequest { constructor(ndk, event) { super(ndk, event); this.kind = 5e3 /* DVMReqTextExtraction */; } static from(event) { return new NDKTranscriptionDVM(event.ndk, event.rawEvent()); } /** * Returns the original source of the transcription */ get url() { const inputTags = this.getMatchingTags("i"); if (inputTags.length !== 1) { return void 0; } return inputTags[0][1]; } /** * Getter for the title tag */ get title() { return this.tagValue("title"); } /** * Setter for the title tag */ set title(value) { this.removeTag("title"); if (value) { this.tags.push(["title", value]); } } /** * Getter for the image tag */ get image() { return this.tagValue("image"); } /** * Setter for the image tag */ set image(value) { this.removeTag("image"); if (value) { this.tags.push(["image", value]); } } }; // src/events/kinds/lists/index.ts var NDKList = class extends NDKEvent { _encryptedTags; /** * Stores the number of bytes the content was before decryption * to expire the cache when the content changes. */ encryptedTagsLength; constructor(ndk, rawEvent) { super(ndk, rawEvent); this.kind ??= 30001 /* CategorizedBookmarkList */; } /** * Wrap a NDKEvent into a NDKList */ static from(ndkEvent) { return new NDKList(ndkEvent.ndk, ndkEvent.rawEvent()); } /** * Returns the title of the list. Falls back on fetching the name tag value. */ get title() { const titleTag = this.tagValue("title") || this.tagValue("name"); if (this.kind === 3 /* Contacts */ && !titleTag) { return "Contacts"; } else if (this.kind === 1e4 /* MuteList */ && !titleTag) { return "Mute"; } else if (this.kind === 10001 /* PinList */ && !titleTag) { return "Pin"; } else if (this.kind === 10002 /* RelayList */ && !titleTag) { return "Relay Metadata"; } else { return titleTag ?? this.tagValue("d"); } } /** * Sets the title of the list. */ set title(title) { this.removeTag("title"); this.removeTag("name"); if (title) { this.tags.push(["title", title]); } else { throw new Error("Title cannot be empty"); } } /** * Returns the name of the list. * @deprecated Please use "title" instead. */ get name() { const nameTag = this.tagValue("name"); if (this.kind === 3 /* Contacts */ && !nameTag) { return "Contacts"; } else if (this.kind === 1e4 /* MuteList */ && !nameTag) { return "Mute"; } else if (this.kind === 10001 /* PinList */ && !nameTag) { return "Pin"; } else if (this.kind === 10002 /* RelayList */ && !nameTag) { return "Relay Metadata"; } else { return nameTag ?? this.tagValue("d"); } } /** * Sets the name of the list. * @deprecated Please use "title" instead. This method will use the `title` tag instead. */ set name(name) { this.removeTag("name"); if (name) { this.tags.push(["title", name]); } else { throw new Error("Name cannot be empty"); } } /** * Returns the description of the list. */ get description() { return this.tagValue("description"); } /** * Sets the description of the list. */ set description(name) { if (name) { this.tags.push(["description", name]); } else { this.removeTag("description"); } } isEncryptedTagsCacheValid() { return !!(this._encryptedTags && this.encryptedTagsLength === this.content.length); } /** * Returns the decrypted content of the list. */ async encryptedTags(useCache = true) { if (useCache && this.isEncryptedTagsCacheValid()) return this._encryptedTags; if (!this.ndk) throw new Error("NDK instance not set"); if (!this.ndk.signer) throw new Error("NDK signer not set"); const user = await this.ndk.signer.user(); try { if (this.content.length > 0) { try { const decryptedContent = await this.ndk.signer.decrypt(user, this.content); const a = JSON.parse(decryptedContent); if (a && a[0]) { this.encryptedTagsLength = this.content.length; return this._encryptedTags = a; } this.encryptedTagsLength = this.content.length; return this._encryptedTags = []; } catch (e) { console.log(`error decrypting ${this.content}`); } } } catch (e) { } return []; } /** * This method can be overriden to validate that a tag is valid for this list. * * (i.e. the NDKPersonList can validate that items are NDKUser instances) */ validateTag(tagValue) { return true; } /** * Returns the unecrypted items in this list. */ get items() { return this.tags.filter((t) => { return !["d", "name", "description"].includes(t[0]); }); } /** * Adds a new item to the list. * @param relay Relay to add * @param mark Optional mark to add to the item * @param encrypted Whether to encrypt the item */ async addItem(item, mark = void 0, encrypted = false) { if (!this.ndk) throw new Error("NDK instance not set"); if (!this.ndk.signer) throw new Error("NDK signer not set"); let tags; if (item instanceof NDKEvent) { tags = item.referenceTags(); } else if (item instanceof NDKUser) { tags = item.referenceTags(); } else if (item instanceof NDKRelay) { tags = item.referenceTags(); } else if (Array.isArray(item)) { tags = [item]; } else { throw new Error("Invalid object type"); } if (mark) tags[0].push(mark); if (encrypted) { const user = await this.ndk.signer.user(); const currentList = await this.encryptedTags(); currentList.push(...tags); this._encryptedTags = currentList; this.encryptedTagsLength = this.content.length; this.content = JSON.stringify(currentList); await this.encrypt(user); } else { this.tags.push(...tags); } this.created_at = Math.floor(Date.now() / 1e3); this.emit("change"); } /** * Removes an item from the list. * * @param index The index of the item to remove. * @param encrypted Whether to remove from the encrypted list or not. */ async removeItem(index, encrypted) { if (!this.ndk) throw new Error("NDK instance not set"); if (!this.ndk.signer) throw new Error("NDK signer not set"); if (encrypted) { const user = await this.ndk.signer.user(); const currentList = await this.encryptedTags(); currentList.splice(index, 1); this._encryptedTags = currentList; this.encryptedTagsLength = this.content.length; this.content = JSON.stringify(currentList); await this.encrypt(user); } else { this.tags.splice(index, 1); } this.created_at = Math.floor(Date.now() / 1e3); this.emit("change"); return this; } }; var lists_default = NDKList; // src/events/kinds/repost.ts var NDKRepost = class extends NDKEvent { _repostedEvents; constructor(ndk, rawEvent) { super(ndk, rawEvent); } static from(event) { return new NDKRepost(event.ndk, event.rawEvent()); } /** * Returns all reposted events by the current event. * * @param klass Optional class to convert the events to. * @returns */ async repostedEvents(klass, opts) { const items = []; if (!this.ndk) throw new Error("NDK instance not set"); if (this._repostedEvents !== void 0) return this._repostedEvents; for (const eventId of this.repostedEventIds()) { const filter = filterForId(eventId); const event = await this.ndk.fetchEvent(filter, opts); if (event) { items.push(klass ? klass.from(event) : event); } } return items; } /** * Returns the reposted event IDs. */ repostedEventIds() { return this.tags.filter((t) => t[0] === "e" || t[0] === "a").map((t) => t[1]); } }; function filterForId(id) { if (id.match(/:/)) { const [kind, pubkey, identifier] = id.split(":"); return { kinds: [parseInt(kind)], authors: [pubkey], "#d": [identifier] }; } else { return { ids: [id] }; } } // src/events/kinds/nip89/NDKAppHandler.ts var NDKAppHandlerEvent = class extends NDKEvent { profile; constructor(ndk, rawEvent) { super(ndk, rawEvent); this.kind ??= 31990 /* AppHandler */; } static from(ndkEvent) { return new NDKAppHandlerEvent(ndkEvent.ndk, ndkEvent.rawEvent()); } /** * Fetches app handler information * If no app information is available on the kind:31990, * we fetch the event's author's profile and return that instead. */ async fetchProfile() { if (this.profile === void 0 && this.content.length > 0) { try { const profile = JSON.parse(this.content); if (profile && profile.name) { return profile; } else { this.profile = null; } } catch (e) { this.profile = null; } } return new Promise((resolve, reject) => { const author = this.author; author.fetchProfile().then(() => { resolve(author.profile); }).catch(reject); }); } }; // src/signers/nip07/index.ts import debug2 from "debug"; var NDKNip07Signer = class { _userPromise; nip04Queue = []; nip04Processing = false; debug; waitTimeout; /** * @param waitTimeout - The timeout in milliseconds to wait for the NIP-07 to become available */ constructor(waitTimeout = 1e3) { this.debug = debug2("ndk:nip07"); this.waitTimeout = waitTimeout; } async blockUntilReady() { await this.waitForExtension(); const pubkey = await window.nostr.getPublicKey(); if (!pubkey) { throw new Error("User rejected access"); } return new NDKUser({ hexpubkey: pubkey }); } /** * Getter for the user property. * @returns The NDKUser instance. */ async user() { if (!this._userPromise) { this._userPromise = this.blockUntilReady(); } return this._userPromise; } /** * Signs the given Nostr event. * @param event - The Nostr event to be signed. * @returns The signature of the signed event. * @throws Error if the NIP-07 is not available on the window object. */ async sign(event) { await this.waitForExtension(); const signedEvent = await window.nostr.signEvent(event); return signedEvent.sig; } async encrypt(recipient, value) { await this.waitForExtension(); const recipientHexPubKey = recipient.hexpubkey; return this.queueNip04("encrypt", recipientHexPubKey, value); } async decrypt(sender, value) { await this.waitForExtension(); const senderHexPubKey = sender.hexpubkey; return this.queueNip04("decrypt", senderHexPubKey, value); } async queueNip04(type, counterpartyHexpubkey, value) { return new Promise((resolve, reject) => { this.nip04Queue.push({ type, counterpartyHexpubkey, value, resolve, reject }); if (!this.nip04Processing) { this.processNip04Queue(); } }); } async processNip04Queue(item, retries = 0) { if (!item && this.nip04Queue.length === 0) { this.nip04Processing = false; return; } this.nip04Processing = true; const { type, counterpartyHexpubkey, value, resolve, reject } = item || this.nip04Queue.shift(); this.debug("Processing encryption queue item", { type, counterpartyHexpubkey, value }); try { let result; if (type === "encrypt") { result = await window.nostr.nip04.encrypt(counterpartyHexpubkey, value); } else { result = await window.nostr.nip04.decrypt(counterpartyHexpubkey, value); } resolve(result); } catch (error) { if (error.message && error.message.includes("call already executing")) { if (retries < 5) { this.debug("Retrying encryption queue item", { type, counterpartyHexpubkey, value, retries }); setTimeout(() => { this.processNip04Queue(item, retries + 1); }, 50 * retries); return; } } reject(error); } this.processNip04Queue(); } waitForExtension() { return new Promise((resolve, reject) => { if (window.nostr) { resolve(); return; } let timerId; const intervalId = setInterval(() => { if (window.nostr) { clearTimeout(timerId); clearInterval(intervalId); resolve(); } }, 100); timerId = setTimeout(() => { clearInterval(intervalId); reject(new Error("NIP-07 extension not available")); }, this.waitTimeout); }); } }; // src/signers/nip46/backend/index.ts import { verifySignature } from "nostr-tools"; // src/signers/private-key/index.ts import { generatePrivateKey, getPublicKey, getSignature, nip04 } from "nostr-tools"; var NDKPrivateKeySigner = class { _user; privateKey; constructor(privateKey) { if (privateKey) { this.privateKey = privateKey; this._user = new NDKUser({ hexpubkey: getPublicKey(this.privateKey) }); } } static generate() { const privateKey = generatePrivateKey(); return new NDKPrivateKeySigner(privateKey); } async blockUntilReady() { if (!this._user) { throw new Error("NDKUser not initialized"); } return this._user; } async user() { await this.blockUntilReady(); return this._user; } async sign(event) { if (!this.privateKey) { throw Error("Attempted to sign without a private key"); } return getSignature(event, this.privateKey); } async encrypt(recipient, value) { if (!this.privateKey) { throw Error("Attempted to encrypt without a private key"); } const recipientHexPubKey = recipient.hexpubkey; return await nip04.encrypt(this.privateKey, recipientHexPubKey, value); } async decrypt(sender, value) { if (!this.privateKey) { throw Error("Attempted to decrypt without a private key"); } const senderHexPubKey = sender.hexpubkey; return await nip04.decrypt(this.privateKey, senderHexPubKey, value); } }; // src/signers/nip46/rpc.ts import EventEmitter6 from "eventemitter3"; var NDKNostrRpc = class extends EventEmitter6 { ndk; signer; debug; constructor(ndk, signer, debug4) { super(); this.ndk = ndk; this.signer = signer; this.debug = debug4.extend("rpc"); } /** * Subscribe to a filter. This function will resolve once the subscription is ready. */ async subscribe(filter) { const sub = this.ndk.subscribe(filter, { closeOnEose: false }); sub.on("event", async (event) => { try { const parsedEvent = await this.parseEvent(event); if (parsedEvent.method) { this.emit("request", parsedEvent); } else { this.emit(`response-${parsedEvent.id}`, parsedEvent); } } catch (e) { this.debug("error parsing event", e, event); } }); return new Promise((resolve, reject) => { sub.on("eose", () => resolve(sub)); }); } async parseEvent(event) { const remoteUser = this.ndk.getUser({ hexpubkey: event.pubkey }); remoteUser.ndk = this.ndk; const decryptedContent = await this.signer.decrypt(remoteUser, event.content); const parsedContent = JSON.parse(decryptedContent); const { id, method, params, result, error } = parsedContent; if (method) { return { id, pubkey: event.pubkey, method, params, event }; } else { return { id, result, error, event }; } } async sendResponse(id, remotePubkey, result, kind = 24133, error) { const res = { id, result }; if (error) { res.error = error; } const localUser = await this.signer.user(); const remoteUser = this.ndk.getUser({ hexpubkey: remotePubkey }); const event = new NDKEvent(this.ndk, { kind, content: JSON.stringify(res), tags: [["p", remotePubkey]], pubkey: localUser.hexpubkey }); event.content = await this.signer.encrypt(remoteUser, event.content); await event.sign(this.signer); await event.publish(); } /** * Sends a request. * @param remotePubkey * @param method * @param params * @param kind * @param id */ async sendRequest(remotePubkey, method, params = [], kind = 24133, cb) { const id = Math.random().toString(36).substring(7); const localUser = await this.signer.user(); const remoteUser = this.ndk.getUser({ hexpubkey: remotePubkey }); const request = { id, method, params }; const promise = new Promise((resolve) => { if (cb) this.once(`response-${id}`, cb); }); const event = new NDKEvent(this.ndk, { kind, content: JSON.stringify(request), tags: [["p", remotePubkey]], pubkey: localUser.hexpubkey }); event.content = await this.signer.encrypt(remoteUser, event.content); await event.sign(this.signer); this.debug(`sending ${method} request to`, remotePubkey); await event.publish(); return promise; } }; // src/signers/nip46/backend/connect.ts var ConnectEventHandlingStrategy = class { async handle(backend, remotePubkey, params) { const [pubkey, token] = params; const debug4 = backend.debug.extend("connect"); debug4(`connection request from ${pubkey}`); if (token && backend.applyToken) { debug4(`applying token`); await backend.applyToken(pubkey, token); } if (await backend.pubkeyAllowed(pubkey, "connect", token)) { debug4(`connection request from ${pubkey} allowed`); return "ack"; } else { debug4(`connection request from ${pubkey} rejected`); } return void 0; } }; // src/signers/nip46/backend/describe.ts var DescribeHandlingStrategy = class { async handle(backend, remotePubkey, params) { const keys = Object.keys(backend.handlers); return JSON.stringify(keys); } }; // src/signers/nip46/backend/get-public-key.ts var GetPublicKeyHandlingStrategy = class { async handle(backend, remotePubkey, params) { return backend.localUser?.hexpubkey; } }; // src/signers/nip46/backend/nip04-decrypt.ts var Nip04DecryptHandlingStrategy = class { async handle(backend, remotePubkey, params) { const [senderPubkey, payload] = params; const senderUser = new NDKUser({ hexpubkey: senderPubkey }); const decryptedPayload = await backend.decrypt(remotePubkey, senderUser, payload); return JSON.stringify([decryptedPayload]); } }; // src/signers/nip46/backend/nip04-encrypt.ts var Nip04EncryptHandlingStrategy = class { async handle(backend, remotePubkey, params) { const [recipientPubkey, payload] = params; const recipientUser = new NDKUser({ hexpubkey: recipientPubkey }); const decryptedPayload = await backend.encrypt(remotePubkey, recipientUser, payload); return decryptedPayload; } }; // src/signers/nip46/backend/sign-event.ts var SignEventHandlingStrategy = class { async handle(backend, remotePubkey, params) { const event = await backend.signEvent(remotePubkey, params); if (!event) return void 0; return JSON.stringify(await event.toNostrEvent()); } }; // src/signers/nip46/backend/index.ts var NDKNip46Backend = class { ndk; signer; localUser; debug; rpc; permitCallback; /** * @param ndk The NDK instance to use * @param privateKey The private key of the npub that wants to be published as */ constructor(ndk, privateKey, permitCallback) { this.ndk = ndk; this.signer = new NDKPrivateKeySigner(privateKey); this.debug = ndk.debug.extend("nip46:backend"); this.rpc = new NDKNostrRpc(ndk, this.signer, this.debug); this.permitCallback = permitCallback; } /** * This method starts the backend, which will start listening for incoming * requests. */ async start() { this.localUser = await this.signer.user(); const sub = this.ndk.subscribe( { kinds: [24133], "#p": [this.localUser.hexpubkey] }, { closeOnEose: false } ); sub.on("event", (e) => this.handleIncomingEvent(e)); } handlers = { connect: new ConnectEventHandlingStrategy(), sign_event: new SignEventHandlingStrategy(), nip04_encrypt: new Nip04EncryptHandlingStrategy(), nip04_decrypt: new Nip04DecryptHandlingStrategy(), get_public_key: new GetPublicKeyHandlingStrategy(), describe: new DescribeHandlingStrategy() }; /** * Enables the user to set a custom strategy for handling incoming events. * @param method - The method to set the strategy for * @param strategy - The strategy to set */ setStrategy(method, strategy) { this.handlers[method] = strategy; } /** * Overload this method to apply tokens, which can * wrap permission sets to be applied to a pubkey. * @param pubkey public key to apply token to * @param token token to apply */ async applyToken(pubkey, token) { throw new Error("connection token not supported"); } async handleIncomingEvent(event) { const { id, method, params } = await this.rpc.parseEvent(event); const remotePubkey = event.pubkey; let response; this.debug("incoming event", { id, method, params }); if (!verifySignature(event.rawEvent())) { this.debug("invalid signature", event.rawEvent()); return; } const strategy = this.handlers[method]; if (strategy) { try { response = await strategy.handle(this, remotePubkey, params); } catch (e) { this.debug("error handling event", e, { id, method, params }); this.rpc.sendResponse(id, remotePubkey, "error", void 0, e.message); } } else { this.debug("unsupported method", { method, params }); } if (response) { this.debug(`sending response to ${remotePubkey}`, response); this.rpc.sendResponse(id, remotePubkey, response); } else { this.rpc.sendResponse(id, remotePubkey, "error", void 0, "Not authorized"); } } async decrypt(remotePubkey, senderUser, payload) { if (!await this.pubkeyAllowed(remotePubkey, "decrypt", payload)) { this.debug(`decrypt request from ${remotePubkey} rejected`); return void 0; } return await this.signer.decrypt(senderUser, payload); } async encrypt(remotePubkey, recipientUser, payload) { if (!await this.pubkeyAllowed(remotePubkey, "encrypt", payload)) { this.debug(`encrypt request from ${remotePubkey} rejected`); return void 0; } return await this.signer.encrypt(recipientUser, payload); } async signEvent(remotePubkey, params) { const [eventString] = params; this.debug(`sign event request from ${remotePubkey}`); const event = new NDKEvent(this.ndk, JSON.parse(eventString)); this.debug("event to sign", event.rawEvent()); if (!await this.pubkeyAllowed(remotePubkey, "sign_event", event)) { this.debug(`sign event request from ${remotePubkey} rejected`); return void 0; } this.debug(`sign event request from ${remotePubkey} allowed`); await event.sign(this.signer); return event; } /** * This method should be overriden by the user to allow or reject incoming * connections. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any async pubkeyAllowed(pubkey, method, params) { return this.permitCallback(pubkey, method, params); } }; // src/signers/nip46/index.ts var NDKNip46Signer = class { ndk; remoteUser; remotePubkey; token; localSigner; rpc; debug; /** * @param ndk - The NDK instance to use * @param tokenOrRemotePubkey - The public key, or a connection token, of the npub that wants to be published as * @param localSigner - The signer that will be used to request events to be signed */ constructor(ndk, tokenOrRemotePubkey, localSigner) { let remotePubkey; let token; if (tokenOrRemotePubkey.includes("#")) { const parts = tokenOrRemotePubkey.split("#"); remotePubkey = new NDKUser({ npub: parts[0] }).hexpubkey; token = parts[1]; } else if (tokenOrRemotePubkey.startsWith("npub")) { remotePubkey = new NDKUser({ npub: tokenOrRemotePubkey }).hexpubkey; } else { remotePubkey = tokenOrRemotePubkey; } this.ndk = ndk; this.remotePubkey = remotePubkey; this.token = token; this.debug = ndk.debug.extend("nip46:signer"); this.remoteUser = new NDKUser({ hexpubkey: remotePubkey }); if (!localSigner) { this.localSigner = NDKPrivateKeySigner.generate(); } else { this.localSigner = localSigner; } this.rpc = new NDKNostrRpc(ndk, this.localSigner, this.debug); } /** * Get the user that is being published as */ async user() { return this.remoteUser; } async blockUntilReady() { const localUser = await this.localSigner.user(); const user = this.ndk.getUser({ npub: localUser.npub }); await this.rpc.subscribe({ kinds: [24133], "#p": [localUser.hexpubkey] }); return new Promise((resolve, reject) => { setTimeout(() => { const connectParams = [localUser.hexpubkey]; if (this.token) { connectParams.push(this.token); } this.rpc.sendRequest( this.remotePubkey, "connect", connectParams, 24133, (response) => { if (response.result === "ack") { resolve(user); } else { reject(response.error); } } ); }, 100); }); } async encrypt(recipient, value) { this.debug("asking for encryption"); const promise = new Promise((resolve, reject) => { this.rpc.sendRequest( this.remotePubkey, "nip04_encrypt", [recipient.hexpubkey, value], 24133, (response) => { if (!response.error) { resolve(response.result); } else { reject(response.error); } } ); }); return promise; } async decrypt(sender, value) { this.debug("asking for decryption"); const promise = new Promise((resolve, reject) => { this.rpc.sendRequest( this.remotePubkey, "nip04_decrypt", [sender.hexpubkey, value], 24133, (response) => { if (!response.error) { const value2 = JSON.parse(response.result); resolve(value2[0]); } else { reject(response.error); } } ); }); return promise; } async sign(event) { this.debug("asking for a signature"); const promise = new Promise((resolve, reject) => { this.rpc.sendRequest( this.remotePubkey, "sign_event", [JSON.stringify(event)], 24133, (response) => { this.debug("got a response", response); if (!response.error) { const json = JSON.parse(response.result); resolve(json.sig); } else { reject(response.error); } } ); }); return promise; } }; // src/ndk/index.ts import debug3 from "debug"; import EventEmitter9 from "eventemitter3"; // src/events/dedup.ts function dedup(event1, event2) { if (event1.created_at > event2.created_at) { return event1; } return event2; } // src/outbox/tracker.ts import EventEmitter7 from "eventemitter3"; import { LRUCache } from "typescript-lru-cache"; var OutboxItem = class { /** * Type of item */ type; /** * The relay URLs that are of interest to this item */ relayUrlScores; readRelays; writeRelays; constructor(type) { this.type = type; this.relayUrlScores = /* @__PURE__ */ new Map(); this.readRelays = /* @__PURE__ */ new Set(); this.writeRelays = /* @__PURE__ */ new Set(); } }; var OutboxTracker = class extends EventEmitter7 { data; ndk; debug; constructor(ndk) { super(); this.ndk = ndk; this.debug = ndk.debug.extend("outbox-tracker"); this.data = new LRUCache({ maxSize: 1e5, entryExpirationTimeInMS: 5e3 }); } trackUsers(items) { for (const item of items) { const itemKey = getKeyFromItem(item); if (this.data.has(itemKey)) continue; const outboxItem = this.track(item, "user"); const user = item instanceof NDKUser ? item : new NDKUser({ hexpubkey: item }); user.ndk = this.ndk; user.relayList().then((relayList) => { if (relayList) { outboxItem.readRelays = new Set(relayList.readRelayUrls); outboxItem.writeRelays = new Set(relayList.writeRelayUrls); for (const relayUrl of outboxItem.readRelays) { if (this.ndk.pool.blacklistRelayUrls.has(relayUrl)) { this.debug(`removing blacklisted relay ${relayUrl} from read relays`); outboxItem.readRelays.delete(relayUrl); } } for (const relayUrl of outboxItem.writeRelays) { if (this.ndk.pool.blacklistRelayUrls.has(relayUrl)) { this.debug(`removing blacklisted relay ${relayUrl} from write relays`); outboxItem.writeRelays.delete(relayUrl); } } this.data.set(itemKey, outboxItem); this.debug( `Adding ${outboxItem.readRelays.size} read relays and ${outboxItem.writeRelays.size} write relays for ${user.hexpubkey}` ); } }); } } /** * * @param key * @param score */ track(item, type) { const key = getKeyFromItem(item); type ??= getTypeFromItem(item); let outboxItem = this.data.get(key); if (!outboxItem) outboxItem = new OutboxItem(type); this.data.set(key, outboxItem); return outboxItem; } }; function getKeyFromItem(item) { if (item instanceof NDKUser) { return item.hexpubkey; } else { return item; } } function getTypeFromItem(item) { if (item instanceof NDKUser) { return "user"; } else { return "kind"; } } // src/relay/pool/index.ts import EventEmitter8 from "eventemitter3"; var NDKPool = class extends EventEmitter8 { // TODO: This should probably be an LRU cache relays = /* @__PURE__ */ new Map(); blacklistRelayUrls; debug; temporaryRelayTimers = /* @__PURE__ */ new Map(); flappingRelays = /* @__PURE__ */ new Set(); // A map to store timeouts for each flapping relay. backoffTimes = /* @__PURE__ */ new Map(); constructor(relayUrls = [], blacklistedRelayUrls = [], ndk, debug4) { super(); this.debug = debug4 ?? ndk.debug.extend("pool"); for (const relayUrl of relayUrls) { const relay = new NDKRelay(relayUrl); this.addRelay(relay, false); } this.blacklistRelayUrls = new Set(blacklistedRelayUrls); } /** * Adds a relay to the pool, and sets a timer to remove it if it is not used within the specified time. * @param relay - The relay to add to the pool. * @param removeIfUnusedAfter - The time in milliseconds to wait before removing the relay from the pool after it is no longer used. */ useTemporaryRelay(relay, removeIfUnusedAfter = 6e5) { const relayAlreadyInPool = this.relays.has(relay.url); if (!relayAlreadyInPool) { this.addRelay(relay); } const existingTimer = this.temporaryRelayTimers.get(relay.url); if (existingTimer) { clearTimeout(existingTimer); } if (!relayAlreadyInPool || existingTimer) { const timer = setTimeout(() => { this.removeRelay(relay.url); }, removeIfUnusedAfter); this.temporaryRelayTimers.set(relay.url, timer); } } /** * Adds a relay to the pool. * * @param relay - The relay to add to the pool. * @param connect - Whether or not to connect to the relay. */ addRelay(relay, connect = true) { const relayUrl = relay.url; if (this.blacklistRelayUrls?.has(relayUrl)) { this.debug(`Relay ${relayUrl} is blacklisted`); return; } relay.on("notice", (relay2, notice) => this.emit("notice", relay2, notice)); relay.on("connect", () => this.handleRelayConnect(relayUrl)); relay.on("disconnect", () => this.emit("relay:disconnect", relay)); relay.on("flapping", () => this.handleFlapping(relay)); this.relays.set(relayUrl, relay); if (connect) { relay.connect().catch((e) => { this.debug(`Failed to connect to relay ${relayUrl}`, e); }); } } /** * Removes a relay from the pool. * @param relayUrl - The URL of the relay to remove. * @returns {boolean} True if the relay was removed, false if it was not found. */ removeRelay(relayUrl) { const relay = this.relays.get(relayUrl); if (relay) { relay.disconnect(); this.relays.delete(relayUrl); this.emit("relay:disconnect", relay); return true; } const existingTimer = this.temporaryRelayTimers.get(relayUrl); if (existingTimer) { clearTimeout(existingTimer); this.temporaryRelayTimers.delete(relayUrl); } return false; } /** * Fetches a relay from the pool, or creates a new one if it does not exist. * * New relays will be attempted to be connected. */ getRelay(url) { let relay = this.relays.get(url); if (!relay) { relay = new NDKRelay(url); this.addRelay(relay); } return relay; } handleRelayConnect(relayUrl) { this.debug(`Relay ${relayUrl} connected`); this.emit("relay:connect", this.relays.get(relayUrl)); if (this.stats().connected === this.relays.size) { this.emit("connect"); } } /** * Attempts to establish a connection to each relay in the pool. * * @async * @param {number} [timeoutMs] - Optional timeout in milliseconds for each connection attempt. * @returns {Promise} A promise that resolves when all connection attempts have completed. * @throws {Error} If any of the connection attempts result in an error or timeout. */ async connect(timeoutMs) { const promises = []; this.debug( `Connecting to ${this.relays.size} relays${timeoutMs ? `, timeout ${timeoutMs}...` : ""}` ); for (const relay of this.relays.values()) { if (timeoutMs) { const timeoutPromise = new Promise((_, reject) => { setTimeout(() => reject(`Timed out after ${timeoutMs}ms`), timeoutMs); }); promises.push( Promise.race([relay.connect(), timeoutPromise]).catch((e) => { this.debug( `Failed to connect to relay ${relay.url}: ${e ?? "No reason specified"}` ); }) ); } else { promises.push(relay.connect()); } } if (timeoutMs) { setTimeout(() => { const allConnected = this.stats().connected === this.relays.size; const someConnected = this.stats().connected > 0; if (!allConnected && someConnected) { this.emit("connect"); } }, timeoutMs); } await Promise.all(promises); } checkOnFlappingRelays() { const flappingRelaysCount = this.flappingRelays.size; const totalRelays = this.relays.size; if (flappingRelaysCount / totalRelays >= 0.8) { for (const relayUrl of this.flappingRelays) { this.backoffTimes.set(relayUrl, 0); } } } handleFlapping(relay) { this.debug(`Relay ${relay.url} is flapping`); let currentBackoff = this.backoffTimes.get(relay.url) || 5e3; currentBackoff = currentBackoff * 2; this.backoffTimes.set(relay.url, currentBackoff); this.debug(`Backoff time for ${relay.url} is ${currentBackoff}ms`); setTimeout(() => { this.debug(`Attempting to reconnect to ${relay.url}`); relay.connect(); this.checkOnFlappingRelays(); }, currentBackoff); relay.disconnect(); this.emit("flapping", relay); } size() { return this.relays.size; } /** * Returns the status of each relay in the pool. * @returns {NDKPoolStats} An object containing the number of relays in each status. */ stats() { const stats = { total: 0, connected: 0, disconnected: 0, connecting: 0 }; for (const relay of this.relays.values()) { stats.total++; if (relay.status === 1 /* CONNECTED */) { stats.connected++; } else if (relay.status === 3 /* DISCONNECTED */) { stats.disconnected++; } else if (relay.status === 0 /* CONNECTING */) { stats.connecting++; } } return stats; } connectedRelays() { return Array.from(this.relays.values()).filter( (relay) => relay.status === 1 /* CONNECTED */ ); } /** * Get a list of all relay urls in the pool. */ urls() { return Array.from(this.relays.keys()); } }; // src/relay/sets/utils.ts function correctRelaySet(relaySet, pool) { const connectedRelays = pool.connectedRelays(); const includesConnectedRelay = Array.from(relaySet.relays).some((relay) => { return connectedRelays.map((r) => r.url).includes(relay.url); }); if (!includesConnectedRelay) { for (const relay of connectedRelays) { relaySet.addRelay(relay); } } if (connectedRelays.length === 0) { for (const relay of pool.relays.values()) { relaySet.addRelay(relay); } } return relaySet; } // src/ndk/index.ts var DEFAULT_OUTBOX_RELAYS = ["wss://purplepag.es", "wss://relay.snort.social"]; var DEFAULT_BLACKLISTED_RELAYS = [ "wss://brb.io" // BRB ]; var NDK = class extends EventEmitter9 { explicitRelayUrls; pool; outboxPool; _signer; _activeUser; cacheAdapter; debug; devWriteRelaySet; outboxTracker; mutedIds; autoConnectUserRelays = true; autoFetchUserMutelist = true; constructor(opts = {}) { super(); this.debug = opts.debug || debug3("ndk"); this.explicitRelayUrls = opts.explicitRelayUrls; this.pool = new NDKPool(opts.explicitRelayUrls || [], opts.blacklistRelayUrls, this); this.debug(`Starting with explicit relays: ${JSON.stringify(this.explicitRelayUrls)}`); this.autoConnectUserRelays = opts.autoConnectUserRelays ?? true; this.autoFetchUserMutelist = opts.autoFetchUserMutelist ?? true; if (opts.enableOutboxModel) { this.outboxPool = new NDKPool( opts.outboxRelayUrls || DEFAULT_OUTBOX_RELAYS, opts.blacklistRelayUrls || DEFAULT_BLACKLISTED_RELAYS, this, this.debug.extend("outbox-pool") ); this.outboxTracker = new OutboxTracker(this); } this.signer = opts.signer; this.cacheAdapter = opts.cacheAdapter; this.mutedIds = opts.mutedIds || /* @__PURE__ */ new Map(); if (opts.devWriteRelayUrls) { this.devWriteRelaySet = NDKRelaySet.fromRelayUrls(opts.devWriteRelayUrls, this); } } toJSON() { return { relayCount: this.pool.relays.size }.toString(); } get activeUser() { return this._activeUser; } /** * Sets the active user for this NDK instance, typically this will be * called when assigning a signer to the NDK instance. * * This function will automatically connect to the user's relays if * `autoConnectUserRelays` is set to true. * * It will also fetch the user's mutelist if `autoFetchUserMutelist` is set to true. */ set activeUser(user) { const differentUser = this._activeUser !== user; this._activeUser = user; if (user && differentUser) { const connectToUserRelays = async (user2) => { const relayList = await user2.relayList(); if (!relayList) { this.debug("No relay list found for user", { npub: user2.npub }); return; } this.debug("Connecting to user relays", { npub: user2.npub, relays: relayList.relays }); for (const url of relayList.relays) { let relay = this.pool.relays.get(url); if (!relay) { relay = new NDKRelay(url); this.pool.addRelay(relay); } } }; const fetchUserMuteList = async (user2) => { const muteLists = await this.fetchEvents([ { kinds: [1e4 /* MuteList */], authors: [user2.pubkey] }, { kinds: [3e4 /* CategorizedPeopleList */], authors: [user2.pubkey], "#d": ["mute"], limit: 1 } ]); if (!muteLists) { this.debug("No mute list found for user", { npub: user2.npub }); return; } for (const muteList of muteLists) { const list = lists_default.from(muteList); for (const item of list.items) { this.mutedIds.set(item[1], item[0]); } } }; const userFunctions = []; if (this.autoConnectUserRelays) userFunctions.push(connectToUserRelays); if (this.autoFetchUserMutelist) userFunctions.push(fetchUserMuteList); const runUserFunctions = async (user2) => { for (const fn of userFunctions) { await fn(user2); } }; const pool = this.outboxPool || this.pool; if (pool.connectedRelays.length > 0) { runUserFunctions(user); } else { this.debug("Waiting for connection to main relays"); pool.once("relay:connect", (relay) => { this.debug("New relay came online", relay); runUserFunctions(user); }); } } else if (!user) { this.mutedIds = /* @__PURE__ */ new Map(); } } get signer() { return this._signer; } set signer(newSigner) { this._signer = newSigner; this.debug(`setting signer`, this.autoConnectUserRelays); newSigner?.user().then((user) => { user.ndk = this; this.activeUser = user; }); } /** * Connect to relays with optional timeout. * If the timeout is reached, the connection will be continued to be established in the background. */ async connect(timeoutMs) { const connections = [this.pool.connect(timeoutMs)]; if (this.outboxPool) { connections.push(this.outboxPool.connect(timeoutMs)); } this.debug("Connecting to relays", { timeoutMs }); return Promise.allSettled(connections).then(() => { }); } /** * Get a NDKUser object * * @param opts * @returns */ getUser(opts) { const user = new NDKUser(opts); user.ndk = this; return user; } /** * Create a new subscription. Subscriptions automatically start, you can make them automatically close when all relays send back an EOSE by setting `opts.closeOnEose` to `true`) * * @param filters * @param opts * @param relaySet explicit relay set to use * @param autoStart automatically start the subscription * @returns NDKSubscription */ subscribe(filters, opts, relaySet, autoStart = true) { const subscription = new NDKSubscription(this, filters, opts, relaySet); if (relaySet) { for (const relay of relaySet.relays) { this.pool.useTemporaryRelay(relay); } } if (this.outboxPool && subscription.hasAuthorsFilter()) { const authors = subscription.filters.filter((filter) => filter.authors && filter.authors?.length > 0).map((filter) => filter.authors).flat(); this.outboxTracker?.trackUsers(authors); } if (autoStart) subscription.start(); return subscription; } /** * Publish an event to a relay * @param event event to publish * @param relaySet explicit relay set to use * @param timeoutMs timeout in milliseconds to wait for the event to be published * @returns The relays the event was published to * * @deprecated Use `event.publish()` instead */ async publish(event, relaySet, timeoutMs) { this.debug("Deprecated: Use `event.publish()` instead"); return event.publish(relaySet, timeoutMs); } /** * Fetch a single event. * * @param idOrFilter event id in bech32 format or filter * @param opts subscription options * @param relaySet explicit relay set to use */ async fetchEvent(idOrFilter, opts, relaySet) { let filter; if (!relaySet && typeof idOrFilter === "string") { if (!isNip33AValue(idOrFilter)) { const relays = relaysFromBech32(idOrFilter); if (relays.length > 0) { relaySet = new NDKRelaySet(new Set(relays), this); relaySet = correctRelaySet(relaySet, this.pool); } } } if (typeof idOrFilter === "string") { filter = filterFromId(idOrFilter); } else { filter = idOrFilter; } if (!filter) { throw new Error(`Invalid filter: ${JSON.stringify(idOrFilter)}`); } return new Promise((resolve) => { const s = this.subscribe( filter, { ...opts || {}, closeOnEose: true }, relaySet, false ); s.on("event", (event) => { event.ndk = this; resolve(event); }); s.on("eose", () => { resolve(null); }); s.start(); }); } /** * Fetch events */ async fetchEvents(filters, opts, relaySet) { return new Promise((resolve) => { const events = /* @__PURE__ */ new Map(); const relaySetSubscription = this.subscribe( filters, { ...opts || {}, closeOnEose: true }, relaySet, false ); const onEvent = (event) => { const dedupKey = event.deduplicationKey(); const existingEvent = events.get(dedupKey); if (existingEvent) { event = dedup(existingEvent, event); } event.ndk = this; events.set(dedupKey, event); }; relaySetSubscription.on("event", onEvent); relaySetSubscription.on("event:dup", onEvent); relaySetSubscription.on("eose", () => { resolve(new Set(events.values())); }); relaySetSubscription.start(); }); } /** * Ensures that a signer is available to sign an event. */ assertSigner() { if (!this.signer) { this.emit("signerRequired"); throw new Error("Signer required"); } } }; // src/zap/invoice.ts import { decode } from "light-bolt11-decoder"; function zapInvoiceFromEvent(event) { const description = event.getMatchingTags("description")[0]; const bolt11 = event.getMatchingTags("bolt11")[0]; let decodedInvoice; let zapRequest; if (!description || !bolt11 || !bolt11[1]) { return null; } try { let zapRequestPayload = description[1]; if (zapRequestPayload.startsWith("%")) { zapRequestPayload = decodeURIComponent(zapRequestPayload); } if (zapRequestPayload === "") { return null; } zapRequest = JSON.parse(zapRequestPayload); decodedInvoice = decode(bolt11[1]); } catch (e) { return null; } const amountSection = decodedInvoice.sections.find((s) => s.name === "amount"); if (!amountSection) { return null; } const amount = parseInt(amountSection.value); if (!amount) { return null; } const content = zapRequest.content; const sender = zapRequest.pubkey; const recipientTag = event.getMatchingTags("p")[0]; const recipient = recipientTag[1]; let zappedEvent = event.getMatchingTags("e")[0]; if (!zappedEvent) { zappedEvent = event.getMatchingTags("a")[0]; } const zappedEventId = zappedEvent ? zappedEvent[1] : void 0; const zapInvoice = { id: event.id, zapper: event.pubkey, zappee: sender, zapped: recipient, zappedEvent: zappedEventId, amount, comment: content }; return zapInvoice; } export { NDKAppHandlerEvent, NDKArticle, NDKDVMJobFeedback, NDKDVMJobResult, NDKDVMRequest, NDKDvmJobFeedbackStatus, NDKEvent, NDKHighlight, NDKKind, NDKList, NDKListKinds, NDKNip07Signer, NDKNip46Backend, NDKNip46Signer, NDKNostrRpc, NDKPrivateKeySigner, NDKRelay, NDKRelayList, NDKRelaySet, NDKRelayStatus, NDKRepost, NDKSubscription, NDKSubscriptionCacheUsage, NDKTranscriptionDVM, NDKUser, calculateGroupableId, NDK as default, defaultOpts, mergeFilters, profileFromEvent, serializeProfile, zapInvoiceFromEvent };