@nostr-dev-kit/ndk
Version:
NDK - Nostr Development Kit
988 lines (851 loc) • 34.8 kB
text/typescript
import debug from "debug";
import type { NostrEvent } from "nostr-tools";
import { EventEmitter } from "tseep";
import type { NDKCacheAdapter } from "../cache/index.js";
import dedupEvent from "../events/dedup.js";
import type { NDKEventId, NDKTag } from "../events/index.js";
import { NDKEvent } from "../events/index.js";
import { signatureVerificationInit } from "../events/signature.js";
import { OutboxTracker } from "../outbox/tracker.js";
import type { NDKAuthPolicy } from "../relay/auth-policies.js";
import { NDKRelay } from "../relay/index.js";
import { NDKPool } from "../relay/pool/index.js";
import type { NDKPublishError } from "../relay/sets/index.js";
import { NDKRelaySet } from "../relay/sets/index.js";
import { correctRelaySet } from "../relay/sets/utils.js";
import type { NDKSigner } from "../signers/index.js";
import type { NDKFilter, NDKSubscriptionOptions } from "../subscription/index.js";
import { NDKSubscription } from "../subscription/index.js";
import { NDKSubscriptionManager } from "../subscription/manager.js";
import { filterFromId, isNip33AValue, relaysFromBech32 } from "../subscription/utils.js";
import type { Hexpubkey, NDKUserParams, ProfilePointer } from "../user/index.js";
import { NDKUser } from "../user/index.js";
import { normalizeRelayUrl } from "../utils/normalize-url.js";
import type { CashuPayCb, LnPayCb, NDKPaymentConfirmation, NDKZapSplit } from "../zapper/index.js";
import type { NDKLnUrlData } from "../zapper/ln.js";
import { setActiveUser } from "./active-user.js";
import { getEntity } from "./entity.js";
import { fetchEventFromTag } from "./fetch-event-from-tag.js";
import { Queue } from "./queue/index.js";
export type NDKValidationRatioFn = (
relay: NDKRelay,
validatedCount: number,
nonValidatedCount: number
) => number;
export type NDKNetDebug = (msg: string, relay: NDKRelay, direction?: "send" | "recv") => void;
/**
* An interface compatible with ndk-wallet that allows setting multiple handlers and callbacks.
*/
export interface NDKWalletInterface {
lnPay?: LnPayCb;
cashuPay?: CashuPayCb;
onPaymentComplete?: (
results: Map<NDKZapSplit, NDKPaymentConfirmation | Error | undefined>
) => void;
}
export interface NDKConstructorParams {
/**
* Relays we should explicitly connect to
*/
explicitRelayUrls?: string[];
/**
* Relays we should never connect to
*/
blacklistRelayUrls?: string[];
/**
* When this is set, we always write only to this relays.
*/
devWriteRelayUrls?: string[];
/**
* Outbox relay URLs.
*/
outboxRelayUrls?: string[];
/**
* Enable outbox model (defaults to false)
*/
enableOutboxModel?: boolean;
/**
* Auto-connect to main user's relays. The "main" user is determined
* by the presence of a signer. Upon connection to the explicit relays,
* the user's relays will be fetched and connected to if this is set to true.
* @default true
*/
autoConnectUserRelays?: boolean;
/**
* Automatically fetch user's mutelist
* @default true
*/
autoFetchUserMutelist?: boolean;
/**
* Signer to use for signing events by default
*/
signer?: NDKSigner;
/**
* Cache adapter to use for caching events
*/
cacheAdapter?: NDKCacheAdapter;
/**
* Debug instance to use
*/
debug?: debug.Debugger;
/**
* Provide a caller function to receive all networking traffic from relays
*/
netDebug?: NDKNetDebug;
/**
* Muted pubkeys and eventIds
*/
mutedIds?: Map<Hexpubkey | NDKEventId, string>;
/**
* Client name to add to events' tag
*/
clientName?: string;
/**
* Client nip89 to add to events' tag
*/
clientNip89?: string;
/**
* Default relay-auth policy
*/
relayAuthDefaultPolicy?: NDKAuthPolicy;
/**
* Set a Web Worker for signature verification.
*
* @default undefined
*
* When provided, signature verification will be processed in a web worker.
* You should listen for the `event:invalid-sig` event to handle invalid signatures.
*
* @example
* ```typescript
* const worker = new Worker("path/to/signature-verification.js");
* ndk.signatureVerificationWorker = worker;
* ndk.on("event:invalid-sig", (event) => {
* console.error("Invalid signature", event);
* });
* ```
*/
signatureVerificationWorker?: Worker | undefined;
/**
* The signature verification validation ratio for new relays.
*/
initialValidationRatio?: number;
/**
* The lowest validation ratio any single relay can have.
* Relays will have a sample of events verified based on this ratio.
* When using this, you MUST listen for event:invalid-sig events
* to handle invalid signatures and disconnect from evil relays.
*
* @default 0.1
*/
lowestValidationRatio?: number;
/**
* A function that is invoked to calculate the validation ratio for a relay.
*/
validationRatioFn?: NDKValidationRatioFn;
/**
* A custom function to verify event signatures.
* When provided, this function will be used instead of the default verification logic.
* This is particularly useful for platforms like React Native where Web Workers are not available.
*
* @example
* ```typescript
* import { verifySignatureAsync } from "@nostr-dev-kit/ndk-mobile";
*
* const ndk = new NDK({
* signatureVerificationFunction: verifySignatureAsync
* });
* ```
*/
signatureVerificationFunction?: (event: NDKEvent) => Promise<boolean>;
/**
* Whether to automatically blacklist relays that provide invalid signatures.
*
* @default true
*/
autoBlacklistInvalidRelays?: boolean;
}
export interface GetUserParams extends NDKUserParams {
npub?: string;
pubkey?: string;
/**
* @deprecated Use `pubkey` instead
*/
hexpubkey?: string;
}
export const DEFAULT_OUTBOX_RELAYS = ["wss://purplepag.es/", "wss://nos.lol/"];
/**
* TODO: Move this to a outbox policy
*/
export const DEFAULT_BLACKLISTED_RELAYS = [
"wss://brb.io/", // BRB
"wss://nostr.mutinywallet.com/", // Don't try to read from this relay since it's a write-only relay
// "wss://purplepag.es/", // This is a hack, since this is a mostly read-only relay, but not fully. Once we have relay routing this can be removed so it only receives the supported kinds
];
/**
* Defines handlers that can be passed to `ndk.subscribe` via the `autoStart` parameter
* to react to subscription lifecycle events.
*/
export interface NDKSubscriptionEventHandlers {
/**
* Called for each individual event received from relays *after* the initial cache load (if `onEvents` is provided),
* or for *all* events (including cached ones) if `onEvents` is not provided.
* @param event The received NDKEvent.
* @param relay The relay the event was received from (undefined if from cache).
*/
onEvent?: (event: NDKEvent, relay?: NDKRelay) => void;
/**
* Called *once* with an array of all events found synchronously in the cache when the subscription starts.
* If this handler is provided, `onEvent` will *not* be called for this initial batch of cached events.
* This is useful for bulk processing or batching UI updates based on the initial cached state.
* @param events An array of NDKEvents loaded synchronously from the cache.
*/
onEvents?: (events: NDKEvent[]) => void; // Parameter name is already 'events'
/**
* Called when the subscription receives an EOSE (End of Stored Events) marker
* from all connected relays involved in this subscription request.
* @param sub The NDKSubscription instance that reached EOSE.
*/
onEose?: (sub: NDKSubscription) => void;
}
/**
* The NDK class is the main entry point to the library.
*
* @emits signer:ready when a signer is ready
* @emits invalid-signature when an event with an invalid signature is received
*/
export class NDK extends EventEmitter<{
"signer:ready": (signer: NDKSigner) => void;
"signer:required": () => void;
/**
* Emitted when an event with an invalid signature is received.
* Includes the relay that provided the invalid signature.
*/
"event:invalid-sig": (event: NDKEvent, relay?: NDKRelay) => void;
/**
* Emitted when an event fails to publish.
* @param event The event that failed to publish
* @param error The error that caused the event to fail to publish
* @param relays The relays that the event was attempted to be published to
*/
"event:publish-failed": (
event: NDKEvent,
error: NDKPublishError,
relays: WebSocket["url"][]
) => void;
}> {
private _explicitRelayUrls?: WebSocket["url"][];
public blacklistRelayUrls?: WebSocket["url"][];
public pool: NDKPool;
public outboxPool?: NDKPool;
private _signer?: NDKSigner;
private _activeUser?: NDKUser;
public cacheAdapter?: NDKCacheAdapter;
public debug: debug.Debugger;
public devWriteRelaySet?: NDKRelaySet;
public outboxTracker?: OutboxTracker;
public mutedIds: Map<Hexpubkey | NDKEventId, string>;
public clientName?: string;
public clientNip89?: string;
public queuesZapConfig: Queue<NDKLnUrlData | undefined>;
public queuesNip05: Queue<ProfilePointer | null>;
public asyncSigVerification = false;
public initialValidationRatio = 1.0;
public lowestValidationRatio = 0.1;
public validationRatioFn?: NDKValidationRatioFn;
public autoBlacklistInvalidRelays = false;
public subManager: NDKSubscriptionManager;
/**
* Private storage for the signature verification function
*/
private _signatureVerificationFunction?: (event: NDKEvent) => Promise<boolean>;
/**
* Private storage for the signature verification worker
*/
private _signatureVerificationWorker?: Worker;
/**
* Rolling total of time spent (in ms) performing signature verifications.
* Users can read this to monitor or display aggregate verification cost.
*/
public signatureVerificationTimeMs: number = 0;
public publishingFailureHandled = false;
public pools: NDKPool[] = [];
/**
* Default relay-auth policy that will be used when a relay requests authentication,
* if no other policy is specified for that relay.
*
* @example Disconnect from relays that request authentication:
* ```typescript
* ndk.relayAuthDefaultPolicy = NDKAuthPolicies.disconnect(ndk.pool);
* ```
*
* @example Sign in to relays that request authentication:
* ```typescript
* ndk.relayAuthDefaultPolicy = NDKAuthPolicies.signIn({ndk})
* ```
*
* @example Sign in to relays that request authentication, asking the user for confirmation:
* ```typescript
* ndk.relayAuthDefaultPolicy = (relay: NDKRelay) => {
* const signIn = NDKAuthPolicies.signIn({ndk});
* if (confirm(`Relay ${relay.url} is requesting authentication, do you want to sign in?`)) {
* signIn(relay);
* }
* }
* ```
*/
public relayAuthDefaultPolicy?: NDKAuthPolicy;
/**
* Fetch function to use for HTTP requests.
*
* @example
* ```typescript
* import fetch from "node-fetch";
*
* ndk.httpFetch = fetch;
* ```
*/
public httpFetch: typeof fetch | undefined;
/**
* Provide a caller function to receive all networking traffic from relays
*/
readonly netDebug?: NDKNetDebug;
public autoConnectUserRelays = true;
public autoFetchUserMutelist = true;
public walletConfig?: NDKWalletInterface;
public constructor(opts: NDKConstructorParams = {}) {
super();
this.debug = opts.debug || debug("ndk");
this.netDebug = opts.netDebug;
this._explicitRelayUrls = opts.explicitRelayUrls || [];
this.blacklistRelayUrls = opts.blacklistRelayUrls || DEFAULT_BLACKLISTED_RELAYS;
this.subManager = new NDKSubscriptionManager();
this.pool = new NDKPool(opts.explicitRelayUrls || [], [], this);
this.pool.name = "Main";
this.pool.on("relay:auth", async (relay: NDKRelay, challenge: string) => {
if (this.relayAuthDefaultPolicy) {
await this.relayAuthDefaultPolicy(relay, challenge);
}
});
this.autoConnectUserRelays = opts.autoConnectUserRelays ?? true;
this.autoFetchUserMutelist = opts.autoFetchUserMutelist ?? true;
this.clientName = opts.clientName;
this.clientNip89 = opts.clientNip89;
this.relayAuthDefaultPolicy = opts.relayAuthDefaultPolicy;
if (opts.enableOutboxModel) {
this.outboxPool = new NDKPool(opts.outboxRelayUrls || DEFAULT_OUTBOX_RELAYS, [], this, {
debug: this.debug.extend("outbox-pool"),
name: "Outbox Pool",
});
this.outboxTracker = new OutboxTracker(this);
}
this.signer = opts.signer;
this.cacheAdapter = opts.cacheAdapter;
this.mutedIds = opts.mutedIds || new Map();
if (opts.devWriteRelayUrls) {
this.devWriteRelaySet = NDKRelaySet.fromRelayUrls(opts.devWriteRelayUrls, this);
}
this.queuesZapConfig = new Queue("zaps", 3);
this.queuesNip05 = new Queue("nip05", 10);
// Set signature verification methods
// The setters will handle setting asyncSigVerification appropriately
// Handle both worker and function for backward compatibility
if (opts.signatureVerificationWorker) {
this.signatureVerificationWorker = opts.signatureVerificationWorker;
}
// Always set the function if provided, even if a worker is also set
if (opts.signatureVerificationFunction) {
this.signatureVerificationFunction = opts.signatureVerificationFunction;
}
this.initialValidationRatio = opts.initialValidationRatio || 1.0;
this.lowestValidationRatio = opts.lowestValidationRatio || 0.1;
this.autoBlacklistInvalidRelays = opts.autoBlacklistInvalidRelays || false;
this.validationRatioFn = opts.validationRatioFn || this.defaultValidationRatioFn;
try {
this.httpFetch = fetch;
} catch {}
}
set explicitRelayUrls(urls: WebSocket["url"][]) {
this._explicitRelayUrls = urls.map(normalizeRelayUrl);
this.pool.relayUrls = urls;
}
get explicitRelayUrls() {
return this._explicitRelayUrls || [];
}
/**
* Set a Web Worker for signature verification.
*
* This method initializes the worker and sets the asyncSigVerification flag.
* The actual verification is handled by the verifySignatureAsync function in signature.ts,
* which will use the worker if available.
*/
set signatureVerificationWorker(worker: Worker | undefined) {
this._signatureVerificationWorker = worker;
if (worker) {
// Initialize the worker
signatureVerificationInit(worker);
// Set asyncSigVerification flag
this.asyncSigVerification = true;
} else {
// If worker is undefined, clear the flag
this.asyncSigVerification = false;
}
}
/**
* Set a custom signature verification function.
*
* This method is particularly useful for platforms that don't support Web Workers,
* such as React Native.
*
* When a function is provided, it will be used for signature verification
* instead of the default worker-based verification. This enables signature
* verification on platforms where Web Workers are not available.
*
* @example
* ```typescript
* import { verifySignatureAsync } from "@nostr-dev-kit/ndk-mobile";
*
* ndk.signatureVerificationFunction = verifySignatureAsync;
* ```
*/
set signatureVerificationFunction(fn: ((event: NDKEvent) => Promise<boolean>) | undefined) {
this._signatureVerificationFunction = fn;
// Enable async verification when a function is provided
this.asyncSigVerification = !!fn;
}
/**
* Get the custom signature verification function
*/
get signatureVerificationFunction(): ((event: NDKEvent) => Promise<boolean>) | undefined {
return this._signatureVerificationFunction;
}
/**
* Adds an explicit relay to the pool.
* @param url
* @param relayAuthPolicy Authentication policy to use if different from the default
* @param connect Whether to connect to the relay automatically
* @returns
*/
public addExplicitRelay(
urlOrRelay: string | NDKRelay,
relayAuthPolicy?: NDKAuthPolicy,
connect = true
): NDKRelay {
let relay: NDKRelay;
if (typeof urlOrRelay === "string") {
relay = new NDKRelay(urlOrRelay, relayAuthPolicy, this);
} else {
relay = urlOrRelay;
}
this.pool.addRelay(relay, connect);
this.explicitRelayUrls?.push(relay.url);
return relay;
}
public toJSON(): string {
return { relayCount: this.pool.relays.size }.toString();
}
public get activeUser(): NDKUser | undefined {
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.
*/
public set activeUser(user: NDKUser | undefined) {
const differentUser = this._activeUser?.pubkey !== user?.pubkey;
this._activeUser = user;
if (user && differentUser) {
setActiveUser.call(this, user);
} else if (!user) {
// reset mutedIds
this.mutedIds = new Map();
}
}
public get signer(): NDKSigner | undefined {
return this._signer;
}
public set signer(newSigner: NDKSigner | undefined) {
this._signer = newSigner;
if (newSigner) this.emit("signer:ready", newSigner);
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.
*/
public async connect(timeoutMs?: number): Promise<void> {
if (this._signer && this.autoConnectUserRelays) {
this.debug(
"Attempting to connect to user relays specified by signer %o",
await this._signer.relays?.(this)
);
if (this._signer.relays) {
const relays = await this._signer.relays(this);
relays.forEach((relay) => this.pool.addRelay(relay));
}
}
const connections = [this.pool.connect(timeoutMs)];
if (this.outboxPool) {
connections.push(this.outboxPool.connect(timeoutMs));
}
return Promise.allSettled(connections).then(() => {});
}
/**
* Centralized method to report an invalid signature, identifying the relay that provided it.
* A single invalid signature means the relay is considered malicious.
* All invalid signature detections (synchronous or asynchronous) should delegate to this method.
*
* @param event The event with an invalid signature
* @param relay The relay that provided the invalid signature
*/
public reportInvalidSignature(event: NDKEvent, relay?: NDKRelay): void {
this.debug(
`Invalid signature detected for event ${event.id}${relay ? ` from relay ${relay.url}` : ""}`
);
// Emit event with relay information
this.emit("event:invalid-sig", event, relay);
// If auto-blacklisting is enabled and we have a relay, add the relay to the blacklist
if (this.autoBlacklistInvalidRelays && relay) {
this.blacklistRelay(relay.url);
}
}
/**
* Add a relay URL to the blacklist as it has been identified as malicious
*/
public blacklistRelay(url: string): void {
if (!this.blacklistRelayUrls) {
this.blacklistRelayUrls = [];
}
if (!this.blacklistRelayUrls.includes(url)) {
this.blacklistRelayUrls.push(url);
this.debug(`Added relay to blacklist: ${url}`);
// Disconnect from this relay if connected
const relay = this.pool.getRelay(url, false, false);
if (relay) {
relay.disconnect();
this.debug(`Disconnected from blacklisted relay: ${url}`);
}
}
}
/**
* Default function to calculate validation ratio based on historical validation results.
* The more events validated successfully, the lower the ratio goes (down to the minimum).
*/
private defaultValidationRatioFn(
relay: NDKRelay,
validatedCount: number,
nonValidatedCount: number
): number {
if (validatedCount < 10) return this.initialValidationRatio;
// Calculate a logarithmically decreasing ratio that approaches the minimum
// as more events are validated
const trustFactor = Math.min(validatedCount / 100, 1); // Caps at 100 validated events
const calculatedRatio =
this.initialValidationRatio * (1 - trustFactor) +
this.lowestValidationRatio * trustFactor;
return Math.max(calculatedRatio, this.lowestValidationRatio);
}
/**
* Get a NDKUser object
*
* @param opts
* @returns
*/
public getUser(opts: GetUserParams): NDKUser {
const user = new NDKUser(opts);
user.ndk = this;
return user;
}
/**
* Get a NDKUser from a NIP05
* @param nip05 NIP-05 ID
* @param skipCache Skip cache
* @returns
*/
async getUserFromNip05(nip05: string, skipCache = false): Promise<NDKUser | undefined> {
return NDKUser.fromNip05(nip05, this, skipCache);
}
/**
* Creates and starts a new subscription.
*
* Subscriptions automatically start unless `autoStart` is set to `false`.
* You can control automatic closing on EOSE via `opts.closeOnEose`.
*
* @param filters - A single NDKFilter object or an array of filters.
* @param opts - Optional NDKSubscriptionOptions to customize behavior (e.g., caching, grouping).
* @param handlers - Optional handlers for subscription events. Passing handlers is the preferred method of using ndk.subscribe.
* - `onEvent`: Called for each event received.
* - `onEvents`: Called once with an array of events when the subscription starts (from the cache).
* - `onEose`: Called when the subscription receives EOSE.
* For backwards compatibility, this third parameter also accepts a relaySet, the relaySet should be passed via `opts.relaySet`.
*
* @param _autoStart - For backwards compatibility, this can be a boolean indicating whether to start the subscription immediately.
* This parameter is deprecated and will be removed in a future version.
* - `false`: Creates the subscription but does not start it (call `subscription.start()` manually).
* @returns The created NDKSubscription instance.
*
* @example Basic subscription
* ```typescript
* const sub = ndk.subscribe({ kinds: [1], authors: [pubkey] });
* sub.on("event", (event) => console.log("Kind 1 event:", event.content));
* ```
*
* @example Subscription with options and direct handlers
* ```typescript
* const sub = ndk.subscribe(
* { kinds: [0], authors: [pubkey] },
* { closeOnEose: true, cacheUsage: NDKSubscriptionCacheUsage.PARALLEL },
* undefined, // Use default relay set calculation
* {
* onEvents: (events) => { // Renamed parameter
* if (events.length > 0) {
* console.log(`Got ${events.length} profile events from cache:`, events[0].content);
* }
* },
* onEvent: (event) => { // Renamed parameter
* console.log("Got profile update from relay:", event.content); // Clarified source
* },
* onEose: () => console.log("Profile subscription finished.")
* }
* );
* ```
*
* @since 2.13.0 `relaySet` parameter removed; pass `relaySet` or `relayUrls` via `opts`.
*/
public subscribe(
filters: NDKFilter | NDKFilter[],
opts?: NDKSubscriptionOptions,
autoStartOrRelaySet: NDKRelaySet | boolean | NDKSubscriptionEventHandlers = true,
_autoStart = true
): NDKSubscription {
let _relaySet: NDKRelaySet | undefined = opts?.relaySet;
let autoStart: boolean | NDKSubscriptionEventHandlers = _autoStart;
// For backwards compatibility, check if the first parameter is a relaySet
if (autoStartOrRelaySet instanceof NDKRelaySet) {
console.warn(
"relaySet is deprecated, use opts.relaySet instead. This will be removed in version v2.14.0"
);
_relaySet = autoStartOrRelaySet;
autoStart = _autoStart;
} else if (
typeof autoStartOrRelaySet === "boolean" ||
typeof autoStartOrRelaySet === "object"
) {
autoStart = autoStartOrRelaySet;
}
// NDKSubscription constructor now handles relaySet/relayUrls from opts
const subscription = new NDKSubscription(this, filters, { relaySet: _relaySet, ...opts });
this.subManager.add(subscription);
const pool = subscription.pool; // Use the pool determined by the subscription options
// Signal to the relays that they are explicitly being used if a relaySet was provided/created
if (subscription.relaySet) {
for (const relay of subscription.relaySet.relays) {
pool.useTemporaryRelay(relay, undefined, subscription.filters);
}
}
// if we have an authors filter and we are using the outbox pool,
// we want to track the authors in the outbox tracker
if (this.outboxPool && subscription.hasAuthorsFilter()) {
const authors: string[] = subscription.filters
.filter((filter) => filter.authors && filter.authors?.length > 0)
.flatMap((filter) => filter.authors!);
this.outboxTracker?.trackUsers(authors);
}
if (autoStart) {
let eventsHandler: ((events: NDKEvent[]) => void) | undefined;
if (typeof autoStart === "object") {
if (autoStart.onEvent) subscription.on("event", autoStart.onEvent);
if (autoStart.onEose) subscription.on("eose", autoStart.onEose);
if (autoStart.onEvents) eventsHandler = autoStart.onEvents;
}
setTimeout(() => {
const cachedEvents = subscription.start(!eventsHandler);
if (cachedEvents && cachedEvents.length > 0 && !!eventsHandler)
eventsHandler(cachedEvents);
}, 0);
}
return subscription;
}
/**
* Attempts to fetch an event from a tag, following relay hints and
* other best practices.
* @param tag Tag to fetch the event from
* @param originalEvent Event where the tag came from
* @param subOpts Subscription options to use when fetching the event
* @param fallback Fallback options to use when the hint relay doesn't respond
* @returns
*/
public fetchEventFromTag = fetchEventFromTag.bind(this);
/**
* Fetch an event from the cache synchronously.
* @param idOrFilter event id in bech32 format or filter
* @returns events from the cache or null if the cache is empty
*/
public fetchEventSync(idOrFilter: string | NDKFilter[]): NDKEvent[] | null {
if (!this.cacheAdapter) throw new Error("Cache adapter not set");
let filters: NDKFilter[];
if (typeof idOrFilter === "string") filters = [filterFromId(idOrFilter)];
else filters = idOrFilter;
const sub = new NDKSubscription(this, filters);
const events = this.cacheAdapter.query(sub);
if (events instanceof Promise) throw new Error("Cache adapter is async");
return events.map((e) => {
e.ndk = this;
return e;
});
}
/**
* Fetch a single event.
*
* @param idOrFilter event id in bech32 format or filter
* @param opts subscription options
* @param relaySetOrRelay explicit relay set to use
*/
public async fetchEvent(
idOrFilter: string | NDKFilter | NDKFilter[],
opts?: NDKSubscriptionOptions,
relaySetOrRelay?: NDKRelaySet | NDKRelay
): Promise<NDKEvent | null> {
let filters: NDKFilter[];
let relaySet: NDKRelaySet | undefined;
// Check if this relaySetOrRelay is an NDKRelay, if it is, make it a relaySet
if (relaySetOrRelay instanceof NDKRelay) {
relaySet = new NDKRelaySet(new Set([relaySetOrRelay]), this);
} else if (relaySetOrRelay instanceof NDKRelaySet) {
relaySet = relaySetOrRelay;
}
// if no relayset has been provided, try to get one from the event id
if (!relaySetOrRelay && typeof idOrFilter === "string") {
/* Check if this is a NIP-33 */
if (!isNip33AValue(idOrFilter)) {
const relays = relaysFromBech32(idOrFilter, this);
if (relays.length > 0) {
relaySet = new NDKRelaySet(new Set<NDKRelay>(relays), this);
// Make sure we have connected relays in this set
relaySet = correctRelaySet(relaySet, this.pool);
}
}
}
if (typeof idOrFilter === "string") {
filters = [filterFromId(idOrFilter)];
} else if (Array.isArray(idOrFilter)) {
filters = idOrFilter;
} else {
filters = [idOrFilter];
}
if (filters.length === 0) {
throw new Error(`Invalid filter: ${JSON.stringify(idOrFilter)}`);
}
return new Promise((resolve) => {
let fetchedEvent: NDKEvent | null = null;
// Prepare options, including the relaySet if available
const subscribeOpts: NDKSubscriptionOptions = {
...(opts || {}),
closeOnEose: true,
};
if (relaySet) subscribeOpts.relaySet = relaySet;
const s = this.subscribe(
filters,
subscribeOpts,
// relaySet, // Removed: Passed via opts
false // autoStart = false
);
/** This is a workaround, for some reason we're leaking subscriptions that should EOSE and fetchEvent is not
* seeing them; this is a temporary fix until we find the bug.
*/
const t2 = setTimeout(() => {
s.stop();
resolve(fetchedEvent);
}, 10000);
s.on("event", (event: NDKEvent) => {
event.ndk = this;
// We only emit immediately when the event is not replaceable
if (!event.isReplaceable()) {
clearTimeout(t2);
resolve(event);
} else if (!fetchedEvent || fetchedEvent.created_at! < event.created_at!) {
fetchedEvent = event;
}
});
s.on("eose", () => {
clearTimeout(t2);
resolve(fetchedEvent);
});
s.start();
});
}
/**
* Fetch events
*/
public async fetchEvents(
filters: NDKFilter | NDKFilter[],
opts?: NDKSubscriptionOptions,
relaySet?: NDKRelaySet
): Promise<Set<NDKEvent>> {
return new Promise((resolve) => {
const events: Map<string, NDKEvent> = new Map();
// Prepare options, including the relaySet if available
const subscribeOpts: NDKSubscriptionOptions = {
...(opts || {}),
closeOnEose: true,
};
if (relaySet) subscribeOpts.relaySet = relaySet;
const relaySetSubscription = this.subscribe(
filters,
subscribeOpts,
// relaySet, // Removed: Passed via opts
false // autoStart = false
);
const onEvent = (event: NostrEvent | NDKEvent) => {
let _event: NDKEvent;
if (!(event instanceof NDKEvent)) _event = new NDKEvent(undefined, event);
else _event = event;
const dedupKey = _event.deduplicationKey();
const existingEvent = events.get(dedupKey);
if (existingEvent) {
_event = dedupEvent(existingEvent, _event);
}
_event.ndk = this;
events.set(dedupKey, _event);
};
// We want to inspect duplicated events
// so we can dedup them
relaySetSubscription.on("event", onEvent);
// relaySetSubscription.on("event:dup", (rawEvent: NostrEvent) => {
// const ndkEvent = new NDKEvent(undefined, rawEvent);
// onEvent(ndkEvent)
// });
relaySetSubscription.on("eose", () => {
resolve(new Set(events.values()));
});
relaySetSubscription.start();
});
}
/**
* Ensures that a signer is available to sign an event.
*/
public assertSigner() {
if (!this.signer) {
this.emit("signer:required");
throw new Error("Signer required");
}
}
public getEntity = getEntity.bind(this);
set wallet(wallet: NDKWalletInterface | undefined) {
if (!wallet) {
this.walletConfig = undefined;
return;
}
this.walletConfig ??= {};
this.walletConfig.lnPay = wallet?.lnPay?.bind(wallet);
this.walletConfig.cashuPay = wallet?.cashuPay?.bind(wallet);
}
}