@nostr-dev-kit/ndk
Version:
NDK - Nostr Development Kit
427 lines (357 loc) • 13.5 kB
text/typescript
import { nip19 } from "nostr-tools";
import { NDKEvent, type NDKTag, type NostrEvent } from "../events/index.js";
import { NDKKind } from "../events/kinds/index.js";
import { NDKCashuMintList } from "../events/kinds/nutzap/mint-list.js";
import type { NDKFilter, NDKRelay, NDKZapMethod, NDKZapMethodInfo } from "../index.js";
import type { NDK } from "../ndk/index.js";
import { NDKSubscriptionCacheUsage, type NDKSubscriptionOptions } from "../subscription/index.js";
import { follows } from "./follows.js";
import { getNip05For } from "./nip05.js";
import { type NDKUserProfile, profileFromEvent, serializeProfile } from "./profile.js";
export type Hexpubkey = string;
export type Npub = string;
// @ignore
export type ProfilePointer = {
pubkey: string;
relays?: string[];
nip46?: string[];
};
// @ignore
export type EventPointer = {
id: string;
relays?: string[];
author?: string;
kind?: number;
};
export interface NDKUserParams {
npub?: Npub;
hexpubkey?: Hexpubkey;
pubkey?: Hexpubkey;
nip05?: string;
relayUrls?: string[];
nip46Urls?: string[];
nprofile?: string;
}
/**
* Represents a pubkey.
*/
export class NDKUser {
public ndk: NDK | undefined;
public profile?: NDKUserProfile;
public profileEvent?: NDKEvent;
private _npub?: Npub;
private _pubkey?: Hexpubkey;
public relayUrls: string[] = [];
readonly nip46Urls: string[] = [];
public constructor(opts: NDKUserParams) {
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;
if (opts.nip46Urls) this.nip46Urls = opts.nip46Urls;
if (opts.nprofile) {
try {
const decoded = nip19.decode(opts.nprofile);
if (decoded.type === "nprofile") {
this._pubkey = decoded.data.pubkey;
if (decoded.data.relays && decoded.data.relays.length > 0) {
this.relayUrls.push(...decoded.data.relays);
}
}
} catch (e) {
console.error("Failed to decode nprofile", e);
}
}
}
get npub(): string {
if (!this._npub) {
if (!this._pubkey) throw new Error("pubkey not set");
this._npub = nip19.npubEncode(this.pubkey);
}
return this._npub;
}
get nprofile(): string {
const relays = this.profileEvent?.onRelays?.map((r) => r.url);
return nip19.nprofileEncode({
pubkey: this.pubkey,
relays,
});
}
set npub(npub: Npub) {
this._npub = npub;
}
/**
* Get the user's pubkey
* @returns {string} The user's pubkey
*/
get pubkey(): string {
if (!this._pubkey) {
if (!this._npub) throw new Error("npub not set");
this._pubkey = nip19.decode(this.npub).data as Hexpubkey;
}
return this._pubkey;
}
/**
* Set the user's pubkey
* @param pubkey {string} The user's pubkey
*/
set pubkey(pubkey: string) {
this._pubkey = pubkey;
}
/**
* Equivalent to NDKEvent.filters().
* @returns {NDKFilter}
*/
public filter(): NDKFilter {
return { "#p": [this.pubkey] };
}
/**
* Gets NIP-57 and NIP-61 information that this user has signaled
*
* @param getAll {boolean} Whether to get all zap info or just the first one
*/
async getZapInfo(timeoutMs?: number): Promise<Map<NDKZapMethod, NDKZapMethodInfo>> {
if (!this.ndk) throw new Error("No NDK instance found");
const promiseWithTimeout = async <T>(promise: Promise<T>): Promise<T | undefined> => {
if (!timeoutMs) return promise;
let timeoutId: number | NodeJS.Timeout | undefined;
const timeoutPromise = new Promise<never>((_, reject) => {
timeoutId = setTimeout(() => reject(new Error("Timeout")), timeoutMs);
});
try {
const result = await Promise.race([promise, timeoutPromise]);
if (timeoutId) clearTimeout(timeoutId);
return result;
} catch (e) {
if (e instanceof Error && e.message === "Timeout") {
try {
const result = await promise;
return result;
} catch (_originalError) {
return undefined;
}
}
return undefined;
}
};
const [userProfile, mintListEvent] = await Promise.all([
promiseWithTimeout(this.fetchProfile()),
promiseWithTimeout(this.ndk.fetchEvent({ kinds: [NDKKind.CashuMintList], authors: [this.pubkey] })),
]);
const res: Map<NDKZapMethod, NDKZapMethodInfo> = new Map();
if (mintListEvent) {
const mintList = NDKCashuMintList.from(mintListEvent);
if (mintList.mints.length > 0) {
res.set("nip61", {
mints: mintList.mints,
relays: mintList.relays,
p2pk: mintList.p2pk,
});
}
}
if (userProfile) {
const { lud06, lud16 } = userProfile;
res.set("nip57", { lud06, lud16 });
}
return res;
}
/**
* Instantiate an NDKUser from a NIP-05 string
* @param nip05Id {string} The user's NIP-05
* @param ndk {NDK} An NDK instance
* @param skipCache {boolean} Whether to skip the cache or not
* @returns {NDKUser | undefined} An NDKUser if one is found for the given NIP-05, undefined otherwise.
*/
static async fromNip05(nip05Id: string, ndk: NDK, skipCache = false): Promise<NDKUser | undefined> {
if (!ndk) throw new Error("No NDK instance found");
const opts: RequestInit = {};
if (skipCache) opts.cache = "no-cache";
const profile = await getNip05For(ndk, nip05Id, ndk?.httpFetch, opts);
if (profile) {
const user = new NDKUser({
pubkey: profile.pubkey,
relayUrls: profile.relays,
nip46Urls: profile.nip46,
});
user.ndk = ndk;
return user;
}
}
/**
* Fetch a user's profile
* @param opts {NDKSubscriptionOptions} A set of NDKSubscriptionOptions
* @param storeProfileEvent {boolean} Whether to store the profile event or not
* @returns User Profile
*/
public async fetchProfile(
opts?: NDKSubscriptionOptions,
storeProfileEvent = false,
): Promise<NDKUserProfile | null> {
if (!this.ndk) throw new Error("NDK not set");
let setMetadataEvent: NDKEvent | null = null;
if (
this.ndk.cacheAdapter &&
(this.ndk.cacheAdapter.fetchProfile || this.ndk.cacheAdapter.fetchProfileSync) &&
opts?.cacheUsage !== NDKSubscriptionCacheUsage.ONLY_RELAY
) {
let profile: NDKUserProfile | null = null;
if (this.ndk.cacheAdapter.fetchProfileSync) {
profile = this.ndk.cacheAdapter.fetchProfileSync(this.pubkey);
} else if (this.ndk.cacheAdapter.fetchProfile) {
profile = await this.ndk.cacheAdapter.fetchProfile(this.pubkey);
}
if (profile) {
this.profile = profile;
return profile;
}
}
opts ??= {};
opts.cacheUsage ??= NDKSubscriptionCacheUsage.ONLY_RELAY;
opts.closeOnEose ??= true;
opts.groupable ??= true;
opts.groupableDelay ??= 250;
if (!setMetadataEvent) {
setMetadataEvent = await this.ndk.fetchEvent({ kinds: [0], authors: [this.pubkey] }, opts);
}
if (!setMetadataEvent) return null;
// return the most recent profile
this.profile = profileFromEvent(setMetadataEvent);
if (storeProfileEvent && this.profile && this.ndk.cacheAdapter && this.ndk.cacheAdapter.saveProfile) {
this.ndk.cacheAdapter.saveProfile(this.pubkey, this.profile);
}
return this.profile;
}
/**
* Returns a set of users that this user follows.
*
* @deprecated Use followSet instead
*/
public follows = follows.bind(this);
/**
* Returns a set of pubkeys that this user follows.
*
* @param opts - NDKSubscriptionOptions
* @param outbox - boolean
* @param kind - number
*/
public async followSet(
opts?: NDKSubscriptionOptions,
outbox?: boolean,
kind: number = NDKKind.Contacts,
): Promise<Set<Hexpubkey>> {
const follows = await this.follows(opts, outbox, kind);
return new Set(Array.from(follows).map((f) => f.pubkey));
}
/** @deprecated Use referenceTags instead. */
/**
* Get the tag that can be used to reference this user in an event
* @returns {NDKTag} an NDKTag
*/
public tagReference(): NDKTag {
return ["p", this.pubkey];
}
/**
* Get the tags that can be used to reference this user in an event
* @returns {NDKTag[]} an array of NDKTag
*/
public referenceTags(marker?: string): NDKTag[] {
const tag = [["p", this.pubkey]];
if (!marker) return tag;
// TODO: Locate this pubkey's relay
tag[0].push("", marker);
return tag;
}
/**
* Publishes the current profile.
*/
public 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),
} as NostrEvent);
await event.publish();
}
/**
* Add a follow to this user's contact list
*
* @param newFollow {NDKUser} The user to follow
* @param currentFollowList {Set<NDKUser>} The current follow list
* @param kind {NDKKind} The kind to use for this contact list (defaults to `3`)
* @returns {Promise<boolean>} True if the follow was added, false if the follow already exists
*/
public async follow(
newFollow: NDKUser,
currentFollowList?: Set<NDKUser>,
kind = NDKKind.Contacts,
): Promise<boolean> {
if (!this.ndk) throw new Error("No NDK instance found");
this.ndk.assertSigner();
if (!currentFollowList) {
currentFollowList = await this.follows(undefined, undefined, kind);
}
if (currentFollowList.has(newFollow)) {
return false;
}
currentFollowList.add(newFollow);
const event = new NDKEvent(this.ndk, { kind } as NostrEvent);
// This is a horrible hack and I need to fix it
for (const follow of currentFollowList) {
event.tag(follow);
}
await event.publish();
return true;
}
/**
* Remove a follow from this user's contact list
*
* @param user {NDKUser} The user to unfollow
* @param currentFollowList {Set<NDKUser>} The current follow list
* @param kind {NDKKind} The kind to use for this contact list (defaults to `3`)
* @returns The relays were the follow list was published or false if the user wasn't found
*/
public async unfollow(
user: NDKUser,
currentFollowList?: Set<NDKUser>,
kind = NDKKind.Contacts,
): Promise<Set<NDKRelay> | boolean> {
if (!this.ndk) throw new Error("No NDK instance found");
this.ndk.assertSigner();
if (!currentFollowList) {
currentFollowList = await this.follows(undefined, undefined, kind);
}
// find the user that has the same pubkey
const newUserFollowList = new Set<NDKUser>();
let foundUser = false;
for (const follow of currentFollowList) {
if (follow.pubkey !== user.pubkey) {
newUserFollowList.add(follow);
} else {
foundUser = true;
}
}
if (!foundUser) return false;
const event = new NDKEvent(this.ndk, { kind } as NostrEvent);
// Tag users from the new follow list
for (const follow of newUserFollowList) {
event.tag(follow);
}
return await event.publish();
}
/**
* 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<boolean | null>} True if the NIP-05 is found and matches this user's pubkey,
* False if the NIP-05 is found but doesn't match this user's pubkey,
* null if the NIP-05 isn't found on the domain or we're unable to verify (because of network issues, etc.)
*/
public async validateNip05(nip05Id: string): Promise<boolean | null> {
if (!this.ndk) throw new Error("No NDK instance found");
const profilePointer: ProfilePointer | null = await getNip05For(this.ndk, nip05Id);
if (profilePointer === null) return null;
return profilePointer.pubkey === this.pubkey;
}
}