Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
},
"correctness": {
"noUnsafeOptionalChaining": "info"
},
"style": {
"noNonNullAssertion": "off"
}
}
},
Expand Down
159 changes: 100 additions & 59 deletions core/src/user/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
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";
import createDebug from "debug";
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";

const d = createDebug("ndk:user");

export type Hexpubkey = string;

Expand Down Expand Up @@ -125,6 +127,32 @@ export class NDKUser {
return { "#p": [this.pubkey] };
}

/**
* 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;
}
}

/**
* Gets NIP-57 and NIP-61 information that this user has signaled
*
Expand All @@ -133,6 +161,10 @@ export class NDKUser {
async getZapInfo(timeoutMs?: number): Promise<Map<NDKZapMethod, NDKZapMethodInfo>> {
if (!this.ndk) throw new Error("No NDK instance found");

d("Looking for zap info", {
pubkey: this.pubkey,
});

const promiseWithTimeout = async <T>(promise: Promise<T>): Promise<T | undefined> => {
if (!timeoutMs) return promise;

Expand All @@ -157,13 +189,23 @@ export class NDKUser {
return undefined;
}
};

d("Fetching user Profile and mint event", {
pubkey: this.pubkey,
profile: await this.fetchProfile(),
});

const [userProfile, mintListEvent] = await Promise.all([
promiseWithTimeout(this.fetchProfile()),
promiseWithTimeout(
this.ndk.fetchEvent({ kinds: [NDKKind.CashuMintList], authors: [this.pubkey] }),
),
this.fetchProfile(),
this.ndk.fetchEvent({kinds: [NDKKind.CashuMintList], authors: [this.pubkey]}),
]);

d("Fetched user Profile and mint event", {
pubkey: this.pubkey,
profile: userProfile,
mintListEvent: mintListEvent,
});

const res: Map<NDKZapMethod, NDKZapMethodInfo> = new Map();

if (mintListEvent) {
Expand All @@ -185,36 +227,6 @@ export class NDKUser {
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
Expand All @@ -227,13 +239,17 @@ export class NDKUser {
): Promise<NDKUserProfile | null> {
if (!this.ndk) throw new Error("NDK not set");

let setMetadataEvent: NDKEvent | null = null;
d("Fetching profile", {
pubkey: this.pubkey,
});

if (
this.ndk.cacheAdapter &&
(this.ndk.cacheAdapter.fetchProfile || this.ndk.cacheAdapter.fetchProfileSync) &&
opts?.cacheUsage !== NDKSubscriptionCacheUsage.ONLY_RELAY
) {
d("Doing cache adapter stuff", {});

let profile: NDKUserProfile | null = null;

if (this.ndk.cacheAdapter.fetchProfileSync) {
Expand All @@ -243,7 +259,13 @@ export class NDKUser {
}

if (profile) {
d("Return profile from cache", {
pubkey: this.pubkey,
profile,
});

this.profile = profile;

return profile;
}
}
Expand All @@ -254,27 +276,48 @@ export class NDKUser {
opts.groupable ??= true;
opts.groupableDelay ??= 25;

let setMetadataEvent: NDKEvent | null = null;

if (!setMetadataEvent) {
setMetadataEvent = await this.ndk.fetchEvent(
{ kinds: [0], authors: [this.pubkey] },
d("Fetching Metadata event", {
pubkey: this.pubkey,
filters: {kinds: [0], authors: [this.pubkey]},
opts,
);
});

setMetadataEvent = await this.ndk.fetchEvent({kinds: [0], authors: [this.pubkey]}, opts);
}

if (!setMetadataEvent) return null;
d("Finished fetching metadata event", {
pubkey: this.pubkey,
event: setMetadataEvent,
});

if (!setMetadataEvent) {
d("Metadata event not found", {
pubkey: this.pubkey,
});

return null;
}

// return the most recent profile
this.profile = profileFromEvent(setMetadataEvent);

if (
storeProfileEvent &&
this.profile &&
this.ndk.cacheAdapter &&
this.ndk.cacheAdapter.saveProfile
) {
if (storeProfileEvent && this.profile && this.ndk.cacheAdapter && this.ndk.cacheAdapter.saveProfile) {
d("Saving profile in cache adapter", {
pubkey: this.pubkey,
});

this.ndk.cacheAdapter.saveProfile(this.pubkey, this.profile);
}

d("Returning profile", {
pubkey: this.pubkey,
setMetadataEvent,
profile: this.profile,
});

return this.profile;
}

Expand Down Expand Up @@ -415,9 +458,7 @@ export class NDKUser {
}

const usersToUnfollow = Array.isArray(user) ? user : [user];
const unfollowPubkeys = new Set(
usersToUnfollow.map((u) => (typeof u === "string" ? u : u.pubkey)),
);
const unfollowPubkeys = new Set(usersToUnfollow.map((u) => (typeof u === "string" ? u : u.pubkey)));

const newUserFollowList = new Set<NDKUser | Hexpubkey>();
let foundAny = false;
Expand Down
11 changes: 4 additions & 7 deletions core/src/zapper/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,8 @@ describe("NDKZapper", () => {
const event = new NDKEvent();
event.ndk = ndk;

it("uses the author pubkey when the target is the user", () => {
const user = ndk.getUser({
pubkey: "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52",
});
it("uses the author pubkey when the target is the user", async () => {
const user = await ndk.fetchUser("fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52");
const splits = new NDKZapper(user, 1000).getZapSplits();
expect(splits).toEqual([
{
Expand Down Expand Up @@ -74,7 +72,7 @@ describe("NDKZapper", () => {
});
});

describe("getZapMethod", () => {
describe("getZapMethod", async () => {
let ndk: NDK;
let signer: NDKPrivateKeySigner;
let user: NDKUser;
Expand All @@ -85,7 +83,7 @@ describe("getZapMethod", () => {
});
signer = NDKPrivateKeySigner.generate();
ndk.signer = signer;
user = ndk.getUser({ pubkey: signer.pubkey });
user = await ndk.fetchUser(signer.pubkey);
user.ndk = ndk;
});

Expand Down Expand Up @@ -130,7 +128,6 @@ describe("getZapMethod", () => {
});

// Mock both profile and mint list fetching
const _fetchProfileMock = vi.fn().mockResolvedValue(profile);
ndk.fetchEvent = vi.fn().mockImplementation((filter) => {
if (filter.kinds?.[0] === 0) {
return Promise.resolve(profileEvent);
Expand Down
8 changes: 4 additions & 4 deletions core/src/zapper/nip57.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ describe("generateZapRequest", () => {
test("creates valid NIP-57 zap request with required fields", async () => {
const signer = NDKPrivateKeySigner.generate();
const ndk = new NDK({ signer });
const user = ndk.getUser({ pubkey: "recipient-pubkey" });
const user = await ndk.fetchUser("recipient-pubkey");

const lnUrlData = {
callback: "https://example.com/lnurlp/callback",
Expand Down Expand Up @@ -58,7 +58,7 @@ describe("generateZapRequest", () => {
test("limits relays to maximum of 4", async () => {
const signer = NDKPrivateKeySigner.generate();
const ndk = new NDK({ signer });
const user = ndk.getUser({ pubkey: "recipient-pubkey" });
const user = await ndk.fetchUser("recipient-pubkey");

const lnUrlData = {
callback: "https://example.com/callback",
Expand Down Expand Up @@ -116,7 +116,7 @@ describe("generateZapRequest", () => {
test("handles empty comment", async () => {
const signer = NDKPrivateKeySigner.generate();
const ndk = new NDK({ signer });
const user = ndk.getUser({ pubkey: "recipient-pubkey" });
const user = await ndk.fetchUser("recipient-pubkey");

const lnUrlData = {
callback: "https://example.com/callback",
Expand All @@ -136,7 +136,7 @@ describe("generateZapRequest", () => {
test("only includes one p-tag", async () => {
const signer = NDKPrivateKeySigner.generate();
const ndk = new NDK({ signer });
const user = ndk.getUser({ pubkey: "recipient-pubkey" });
const user = await ndk.fetchUser("recipient-pubkey");

const lnUrlData = {
callback: "https://example.com/callback",
Expand Down