diff --git a/.changeset/curly-dolphins-speak.md b/.changeset/curly-dolphins-speak.md new file mode 100644 index 00000000..b3562ce1 --- /dev/null +++ b/.changeset/curly-dolphins-speak.md @@ -0,0 +1,5 @@ +--- +"@nostr-dev-kit/ndk": patch +--- + +fix bug where queued items were not getting processed (e.g. zap fetches) diff --git a/.changeset/kind-news-sin.md b/.changeset/kind-news-sin.md new file mode 100644 index 00000000..62bdb49e --- /dev/null +++ b/.changeset/kind-news-sin.md @@ -0,0 +1,5 @@ +--- +"@nostr-dev-kit/ndk": patch +--- + +Breaking change: event.zap is now removed, use ndk.zap(event) instead diff --git a/README.md b/README.md index 3f2266fc..da08b708 100644 --- a/README.md +++ b/README.md @@ -323,7 +323,8 @@ await ndk.publish(event); ```ts // Find the first event from @jack, and react/like it. -const event = await ndk.fetchEvent({ author: "jack@cashapp.com" })[0]; +const jack = await ndk.getUserFromNip05("jack@cashapp.com"); +const event = await ndk.fetchEvent({ authors: [jack.pubkey] })[0]; await event.react("🤙"); ``` @@ -331,8 +332,9 @@ await event.react("🤙"); ```ts // Find the first event from @jack, and zap it. -const event = await ndk.fetchEvent({ author: "jack@cashapp.com" })[0]; -await event.zap(1337, "Zapping your post!"); // Returns a bolt11 payment request +const jack = await ndk.getUserFromNip05("jack@cashapp.com"); +const event = await ndk.fetchEvent({ authors: [jack.pubkey] })[0]; +await ndk.zap(event, 1337, "Zapping your post!"); // Returns a bolt11 payment request ``` ## Architecture decisions & suggestions diff --git a/ndk/package.json b/ndk/package.json index 96b64c4d..9bc717c6 100644 --- a/ndk/package.json +++ b/ndk/package.json @@ -34,6 +34,7 @@ "scripts": { "dev": "pnpm build --watch", "build": "tsup src/index.ts --format cjs,esm --dts; tsup src/workers/sig-verification.ts --format cjs,esm --dts -d dist/workers", + "build:core:esm": "tsup src/index.ts --format esm --dts", "clean": "rm -rf dist docs", "test": "jest", "lint": "prettier --check . && eslint .", diff --git a/ndk/src/events/index.ts b/ndk/src/events/index.ts index c32e2700..64ea4daf 100644 --- a/ndk/src/events/index.ts +++ b/ndk/src/events/index.ts @@ -16,7 +16,6 @@ import { repost } from "./repost.js"; import { fetchReplyEvent, fetchRootEvent, fetchTaggedEvent } from "./fetch-tagged-event.js"; import { NDKEventSerialized, deserialize, serialize } from "./serializer.js"; import { validate, verifySignature, getEventHash } from "./validation.js"; -import { NDKZap } from "../zap/index.js"; import { matchFilter } from "nostr-tools"; export type NDKEventId = string; @@ -609,48 +608,6 @@ export class NDKEvent extends EventEmitter { } } - /** - * 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: number, - comment?: string, - extraTags?: NDKTag[], - recipient?: NDKUser, - signer?: NDKSigner - ): Promise { - if (!this.ndk) throw new Error("No NDK instance found"); - - if (!signer) { - this.ndk.assertSigner(); - } - - const zap = new NDKZap({ - 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 - ); - - // await zap.publish(amount); - return paymentRequest; - } - /** * Generates a deletion event of the current event * diff --git a/ndk/src/events/kinds/NDKRelayList.ts b/ndk/src/events/kinds/NDKRelayList.ts index 548d7b3b..de53a326 100644 --- a/ndk/src/events/kinds/NDKRelayList.ts +++ b/ndk/src/events/kinds/NDKRelayList.ts @@ -1,16 +1,17 @@ -import { NDKKind } from "."; -import type { NostrEvent } from ".."; +import { NDKKind } from "./index.js"; +import type { NostrEvent } from "../index.js"; import { NDKEvent } from "../index.js"; -import type { NDK } from "../../ndk"; -import type { NDKRelay } from "../../relay"; -import type { Hexpubkey } from "../../user"; -import { NDKRelaySet } from "../../relay/sets"; -import { normalizeRelayUrl } from "../../utils/normalize-url"; -import { NDKSubscriptionCacheUsage } from "../../subscription"; +import type { NDK } from "../../ndk/index.js"; +import { NDKRelaySet } from "../../relay/sets/index.js"; +import { normalizeRelayUrl } from "../../utils/normalize-url.js"; const READ_MARKER = "read"; const WRITE_MARKER = "write"; +/** + * Represents a relay list for a user, ideally coming from a NIP-65 kind:10002 or alternatively from a kind:3 event's content. + * @group Kind Wrapper + */ export class NDKRelayList extends NDKEvent { constructor(ndk?: NDK, rawEvent?: NostrEvent) { super(ndk, rawEvent); @@ -21,117 +22,11 @@ export class NDKRelayList extends NDKEvent { return new NDKRelayList(ndkEvent.ndk, ndkEvent.rawEvent()); } - static async forUser(pubkey: Hexpubkey, ndk: NDK): Promise { - // call forUsers with a single pubkey - const result = await this.forUsers([pubkey], ndk); - return result.get(pubkey); - } - - /** - * Gathers a set of relay list events for a given set of users. - * @returns A map of pubkeys to relay list. - */ - static async forUsers(pubkeys: Hexpubkey[], ndk: NDK): Promise> { - const pool = ndk.outboxPool || ndk.pool; - const set = new Set(); - - for (const relay of pool.relays.values()) set.add(relay); - - const relayLists = new Map(); - const fromContactList = new Map(); - - const relaySet = new NDKRelaySet(set, ndk); - - // get all kind 10002 events from cache if we have an adapter and is locking - if (ndk.cacheAdapter?.locking) { - const cachedList = await ndk.fetchEvents( - { kinds: [3, 10002], authors: pubkeys }, - { cacheUsage: NDKSubscriptionCacheUsage.ONLY_CACHE } - ); - - // get list of relay lists from cache - for (const relayList of cachedList) { - if (relayList.kind === 10002) - relayLists.set(relayList.pubkey, NDKRelayList.from(relayList)); - } - - for (const relayList of cachedList) { - if (relayList.kind === 3) { - // skip if we already have a relay list for this pubkey - if (relayLists.has(relayList.pubkey)) continue; - const list = relayListFromKind3(ndk, relayList); - if (list) fromContactList.set(relayList.pubkey, list); - } - } - - // remove the pubkeys we found from the list - pubkeys = pubkeys.filter( - (pubkey) => !relayLists.has(pubkey) && !fromContactList.has(pubkey) - ); - } - - // if we have no pubkeys left, return the results - if (pubkeys.length === 0) return relayLists; - - const relayListEvents = new Map(); - const contactListEvents = new Map(); - - return new Promise>(async (resolve) => { - // Get from relays the missing pubkeys - const sub = ndk.subscribe( - { kinds: [3, 10002], authors: pubkeys }, - { - closeOnEose: true, - pool, - groupable: true, - cacheUsage: NDKSubscriptionCacheUsage.ONLY_RELAY, - subId: "ndk-relay-list-fetch", - }, - relaySet, - false - ); - - /* Collect most recent version of events */ - sub.on("event", (event) => { - if (event.kind === NDKKind.RelayList) { - const existingEvent = relayListEvents.get(event.pubkey); - if (existingEvent && existingEvent.created_at! > event.created_at!) return; - relayListEvents.set(event.pubkey, event); - } else if (event.kind === NDKKind.Contacts) { - const existingEvent = contactListEvents.get(event.pubkey); - if (existingEvent && existingEvent.created_at! > event.created_at!) return; - contactListEvents.set(event.pubkey, event); - } - }); - - sub.on("eose", () => { - // Get all kind 10002 events - for (const event of relayListEvents.values()) { - relayLists.set(event.pubkey, NDKRelayList.from(event)); - } - - // Go through the pubkeys we don't have results for and get the from kind 3 events - for (const pubkey of pubkeys) { - if (relayLists.has(pubkey)) continue; - const contactList = contactListEvents.get(pubkey); - if (!contactList) continue; - const list = relayListFromKind3(ndk, contactList); - - if (list) relayLists.set(pubkey, list); - } - - resolve(relayLists); - }); - - sub.start(); - }); - } - get readRelayUrls(): WebSocket["url"][] { return this.tags .filter((tag) => tag[0] === "r" || tag[0] === "relay") .filter((tag) => !tag[2] || (tag[2] && tag[2] === READ_MARKER)) - .map((tag) => tag[1]); + .map((tag) => normalizeRelayUrl(tag[1])); } set readRelayUrls(relays: WebSocket["url"][]) { @@ -144,7 +39,7 @@ export class NDKRelayList extends NDKEvent { return this.tags .filter((tag) => tag[0] === "r" || tag[0] === "relay") .filter((tag) => !tag[2] || (tag[2] && tag[2] === WRITE_MARKER)) - .map((tag) => tag[1]); + .map((tag) => normalizeRelayUrl(tag[1])); } set writeRelayUrls(relays: WebSocket["url"][]) { @@ -182,7 +77,7 @@ export class NDKRelayList extends NDKEvent { } } -function relayListFromKind3(ndk: NDK, contactList: NDKEvent): NDKRelayList | undefined { +export function relayListFromKind3(ndk: NDK, contactList: NDKEvent): NDKRelayList | undefined { try { const content = JSON.parse(contactList.content); const relayList = new NDKRelayList(ndk); diff --git a/ndk/src/index.ts b/ndk/src/index.ts index 3a1d3972..ce0c43ee 100644 --- a/ndk/src/index.ts +++ b/ndk/src/index.ts @@ -46,3 +46,4 @@ export { NDK as default, NDKConstructorParams } from "./ndk/index.js"; export { NDKZapInvoice, zapInvoiceFromEvent } from "./zap/invoice.js"; export * from "./zap/index.js"; export * from "./utils/normalize-url.js"; +export * from './utils/get-users-relay-list.js'; \ No newline at end of file diff --git a/ndk/src/ndk/active-user.ts b/ndk/src/ndk/active-user.ts index 9e16e35b..4b5fe155 100644 --- a/ndk/src/ndk/active-user.ts +++ b/ndk/src/ndk/active-user.ts @@ -7,13 +7,14 @@ import { NDKKind } from "../events/kinds/index.js"; import { NDKEvent } from "../events/index.js"; import NDKList from "../events/kinds/lists/index.js"; import { NDKRelay } from "../relay/index.js"; +import { getRelayListForUser } from "../utils/get-users-relay-list.js"; const debug = createDebug("ndk:active-user"); async function getUserRelayList(this: NDK, user: NDKUser): Promise { if (!this.autoConnectUserRelays) return; - const userRelays = await NDKRelayList.forUser(user.pubkey, this); + const userRelays = await getRelayListForUser(user.pubkey, this); if (!userRelays) return; for (const url of userRelays.relays) { diff --git a/ndk/src/ndk/index.ts b/ndk/src/ndk/index.ts index ac94bbb6..c44c940d 100644 --- a/ndk/src/ndk/index.ts +++ b/ndk/src/ndk/index.ts @@ -3,7 +3,7 @@ import { EventEmitter } from "tseep"; import type { NDKCacheAdapter } from "../cache/index.js"; import dedupEvent from "../events/dedup.js"; -import type { NDKEvent, NDKEventId, NDKTag } from "../events/index.js"; +import { NDKEvent, NDKEventId, NDKTag } from "../events/index.js"; import { OutboxTracker } from "../outbox/tracker.js"; import { NDKRelay } from "../relay/index.js"; import { NDKPool } from "../relay/pool/index.js"; @@ -19,7 +19,7 @@ import { fetchEventFromTag } from "./fetch-event-from-tag.js"; import { NDKAuthPolicy } from "../relay/auth-policies.js"; import { Nip96 } from "../media/index.js"; import { NDKNwc } from "../nwc/index.js"; -import { NDKLnUrlData } from "../zap/index.js"; +import { NDKLnUrlData, NDKZap, ZapConstructorParams } from "../zap/index.js"; import { Queue } from "./queue/index.js"; import { signatureVerificationInit } from "../events/signature.js"; import { NDKSubscriptionManager } from "../subscription/manager.js"; @@ -667,4 +667,46 @@ export class NDK extends EventEmitter<{ } return nwc; } + + /** + * 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) + */ + public async zap( + eventOrUser: NDKEvent | NDKUser, + amount: number, + comment?: string, + extraTags?: NDKTag[], + recipient?: NDKUser, + signer?: NDKSigner + ): Promise { + if (!signer) { + this.assertSigner(); + } + + let zapOpts: ZapConstructorParams; + + if (eventOrUser instanceof NDKEvent) { + zapOpts = { ndk: this, zappedUser: eventOrUser.author, zappedEvent: eventOrUser }; + } else if (eventOrUser instanceof NDKUser) { + zapOpts = { ndk: this, zappedUser: eventOrUser }; + } else { + throw new Error("Invalid recipient"); + } + + const zap = new NDKZap(zapOpts); + + return zap.createZapRequest( + amount, + comment, + extraTags, + undefined, + signer + ); + } } diff --git a/ndk/src/ndk/queue/index.ts b/ndk/src/ndk/queue/index.ts index 114ba05e..8d389cdf 100644 --- a/ndk/src/ndk/queue/index.ts +++ b/ndk/src/ndk/queue/index.ts @@ -51,6 +51,7 @@ export class Queue { promise.finally(() => { this.promises.delete(item.id); this.processing.delete(item.id); + this.process(); }); return promise; diff --git a/ndk/src/outbox/tracker.ts b/ndk/src/outbox/tracker.ts index e3b7e390..a046ee55 100644 --- a/ndk/src/outbox/tracker.ts +++ b/ndk/src/outbox/tracker.ts @@ -2,6 +2,7 @@ import { EventEmitter } from "tseep"; import { LRUCache } from "typescript-lru-cache"; import { NDKRelayList } from "../events/kinds/NDKRelayList.js"; +import { getRelayListForUsers } from "../utils/get-users-relay-list.js"; import type { NDK } from "../ndk/index.js"; import type { Hexpubkey } from "../user/index.js"; import { NDKUser } from "../user/index.js"; @@ -79,7 +80,7 @@ export class OutboxTracker extends EventEmitter { this.data.set(pubkey, new OutboxItem("user")); } - NDKRelayList.forUsers(pubkeys, this.ndk).then( + getRelayListForUsers(pubkeys, this.ndk).then( (relayLists: Map) => { for (const [pubkey, relayList] of relayLists) { const outboxItem = this.data.get(pubkey)!; diff --git a/ndk/src/utils/get-users-relay-list.ts b/ndk/src/utils/get-users-relay-list.ts new file mode 100644 index 00000000..62e077a4 --- /dev/null +++ b/ndk/src/utils/get-users-relay-list.ts @@ -0,0 +1,115 @@ +import { NDKEvent } from "../events/index.js"; +import { NDKKind } from "../events/kinds/index.js"; +import { NDKRelayList, relayListFromKind3 } from "../events/kinds/NDKRelayList.js"; +import { NDK } from "../ndk/index.js"; +import { NDKRelay } from "../relay/index.js"; +import { NDKRelaySet } from "../relay/sets/index.js"; +import { NDKSubscriptionCacheUsage } from "../subscription/index.js"; +import { Hexpubkey } from "../user/index.js"; + +export async function getRelayListForUser(pubkey: Hexpubkey, ndk: NDK): Promise { + const list = await getRelayListForUsers([pubkey], ndk); + return list.get(pubkey)!; +} + +/** + * Fetches a map of relay lists for a number of users + * @param pubkeys + * @param ndk + * @returns + */ +export async function getRelayListForUsers(pubkeys: Hexpubkey[], ndk: NDK): Promise> { + const pool = ndk.outboxPool || ndk.pool; + const set = new Set(); + + for (const relay of pool.relays.values()) set.add(relay); + + const relayLists = new Map(); + const fromContactList = new Map(); + + const relaySet = new NDKRelaySet(set, ndk); + + // get all kind 10002 events from cache if we have an adapter and is locking + if (ndk.cacheAdapter?.locking) { + const cachedList = await ndk.fetchEvents( + { kinds: [3, 10002], authors: pubkeys }, + { cacheUsage: NDKSubscriptionCacheUsage.ONLY_CACHE } + ); + + // get list of relay lists from cache + for (const relayList of cachedList) { + if (relayList.kind === 10002) + relayLists.set(relayList.pubkey, NDKRelayList.from(relayList)); + } + + for (const relayList of cachedList) { + if (relayList.kind === 3) { + // skip if we already have a relay list for this pubkey + if (relayLists.has(relayList.pubkey)) continue; + const list = relayListFromKind3(ndk, relayList); + if (list) fromContactList.set(relayList.pubkey, list); + } + } + + // remove the pubkeys we found from the list + pubkeys = pubkeys.filter( + (pubkey) => !relayLists.has(pubkey) && !fromContactList.has(pubkey) + ); + } + + // if we have no pubkeys left, return the results + if (pubkeys.length === 0) return relayLists; + + const relayListEvents = new Map(); + const contactListEvents = new Map(); + + return new Promise>(async (resolve) => { + // Get from relays the missing pubkeys + const sub = ndk.subscribe( + { kinds: [3, 10002], authors: pubkeys }, + { + closeOnEose: true, + pool, + groupable: true, + cacheUsage: NDKSubscriptionCacheUsage.ONLY_RELAY, + subId: "ndk-relay-list-fetch", + }, + relaySet, + false + ); + + /* Collect most recent version of events */ + sub.on("event", (event) => { + if (event.kind === NDKKind.RelayList) { + const existingEvent = relayListEvents.get(event.pubkey); + if (existingEvent && existingEvent.created_at! > event.created_at!) return; + relayListEvents.set(event.pubkey, event); + } else if (event.kind === NDKKind.Contacts) { + const existingEvent = contactListEvents.get(event.pubkey); + if (existingEvent && existingEvent.created_at! > event.created_at!) return; + contactListEvents.set(event.pubkey, event); + } + }); + + sub.on("eose", () => { + // Get all kind 10002 events + for (const event of relayListEvents.values()) { + relayLists.set(event.pubkey, NDKRelayList.from(event)); + } + + // Go through the pubkeys we don't have results for and get the from kind 3 events + for (const pubkey of pubkeys) { + if (relayLists.has(pubkey)) continue; + const contactList = contactListEvents.get(pubkey); + if (!contactList) continue; + const list = relayListFromKind3(ndk, contactList); + + if (list) relayLists.set(pubkey, list); + } + + resolve(relayLists); + }); + + sub.start(); + }); +} \ No newline at end of file diff --git a/ndk/src/zap/index.test.ts b/ndk/src/zap/index.test.ts new file mode 100644 index 00000000..477f4edc --- /dev/null +++ b/ndk/src/zap/index.test.ts @@ -0,0 +1,69 @@ +import { NDKZap } from "."; +import { NDKEvent } from "../events/index.js"; +import { NDK } from "../ndk/index.js"; +import { Hexpubkey } from "../user"; +import { NDKRelayList } from "../events/kinds/NDKRelayList.js"; + +const ndk = new NDK(); +const user1 = ndk.getUser({npub: "npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft"}); +const user2 = ndk.getUser({npub: "npub1hkmj8cfap65e7gjy3x3auky7mfegjxll9jfk2jed3w9gsnh9j5gsd24r62"}); +const user3 = ndk.getUser({npub: "npub15z0mnjepscd9hk3ywkcvuupap7tt0qle64f0uvvygpl3mqerz4tq4dl33y"}); + +const relays = [ + [ "wss://user1-A", "wss://user1-B", "wss://user1-user2-A", "wss://user1-user2-B", "wss://user1-C", "wss://user1-D", "wss://user1-E", "wss://user1-F", "wss://user1-G", "wss://user1-H", "wss://user1-I", "wss://user1-J", "wss://user1-K", "wss://user1-L", "wss://user1-M", "wss://user1-N", "wss://user1-O", "wss://user1-P", "wss://user1-Q", "wss://user1-R", "wss://user1-S", "wss://user1-T", "wss://user1-U", "wss://user1-V", "wss://user1-W", "wss://user1-X", "wss://user1-Y", "wss://user1-Z" ], + [ "wss://user2-A", "wss://user2-B", "wss://user1-user2-A", "wss://user1-user2-B" ], +] + +jest.mock("../utils/get-users-relay-list.js", () => ({ + getRelayListForUsers: jest.fn(() => { + const map = new Map(); + const e1 = new NDKRelayList(ndk) as any; + jest.spyOn(e1, "readRelayUrls", "get").mockReturnValue(relays[0]); + map.set(user1.npub, e1); + + const e2 = new NDKRelayList(ndk) as any; + jest.spyOn(e2, "readRelayUrls", "get").mockReturnValue(relays[1]); + map.set(user2.npub, e2); + + return map; + }) +})); + +afterAll(() => { + jest.clearAllMocks(); +}); + +describe("NDKZap", () => { + describe("relay", () => { + it("prefers relays that both sender and receiver have in common", async () => { + ndk.activeUser = user1; + const zap = new NDKZap({ + ndk, + zappedUser: user2, + }) + + const r = await zap.relays(); + + expect(r.slice(0, 2).includes("wss://user1-user2-A")).toBe(true); + expect(r.slice(0, 2).includes("wss://user1-user2-B")).toBe(true); + + expect(r.length).toBeLessThanOrEqual(3); + }); + + it("correctly uses sender relays when we don't have relays for the receiver", async () => { + ndk.activeUser = user1; + const zap = new NDKZap({ + ndk, + zappedUser: user3, + }) + + const r = await zap.relays(); + + for (const relay of r) { + expect(relays[0].includes(relay)).toBe(true); + } + + expect(r.length).toBeGreaterThanOrEqual(3); + }); + }) +}) \ No newline at end of file diff --git a/ndk/src/zap/index.ts b/ndk/src/zap/index.ts index 0f6be946..03b3a68d 100644 --- a/ndk/src/zap/index.ts +++ b/ndk/src/zap/index.ts @@ -9,6 +9,7 @@ import type { Hexpubkey, NDKUser } from "../user/index.js"; import { NDKSigner } from "../signers/index.js"; import createDebug from "debug"; import type { NDKUserProfile } from "../user/profile.js"; +import { getRelayListForUsers } from "../utils/get-users-relay-list.js"; const debug = createDebug("ndk:zap"); @@ -21,7 +22,7 @@ const DEFAULT_RELAYS = [ "wss://no.str.cr", ]; -interface ZapConstructorParams { +export interface ZapConstructorParams { ndk: NDK; zappedEvent?: NDKEvent; zappedUser?: NDKUser; @@ -62,6 +63,11 @@ export class NDKZap extends EventEmitter { public zappedUser: NDKUser; private fetch: typeof fetch = fetch; + /** + * The maximum number of relays to request the zapper to publish the zap receipt to. + */ + public maxRelays = 3; + public constructor(args: ZapConstructorParams) { super(); this.ndk = args.ndk; @@ -239,7 +245,7 @@ export class NDKZap extends EventEmitter { event: null, amount, comment: comment || "", - relays: relays ?? this.relays(), + relays: relays ?? await this.relays(), }); // add the event tag if it exists; this supports both 'e' and 'a' tags @@ -262,11 +268,34 @@ export class NDKZap extends EventEmitter { /** * @returns the relays to use for the zap request */ - private relays(): string[] { + public async relays(): Promise { let r: string[] = []; - if (this.ndk?.pool?.relays) { - r = this.ndk.pool.urls(); + if (this.ndk?.activeUser) { + const relayLists = await getRelayListForUsers([ + this.ndk.activeUser.pubkey, + this.zappedUser.pubkey, + ], this.ndk); + + const relayScores = new Map(); + + // go through the relay lists and try to get relays that are shared between the two users + for (const relayList of relayLists.values()) { + for (const url of relayList.readRelayUrls) { + const score = relayScores.get(url) || 0; + relayScores.set(url, score + 1); + } + } + + // get the relays that are shared between the two users + r = Array.from(relayScores.entries()) + .sort((a, b) => b[1] - a[1]) + .map(([url]) => url) + .slice(0, this.maxRelays); + } + + if (this.ndk?.pool?.permanentAndConnectedRelays().length) { + r = this.ndk.pool.permanentAndConnectedRelays().map((relay) => relay.url); } if (!r.length) {