Skip to content

Commit

Permalink
optimize which relays are set on zap requests
Browse files Browse the repository at this point in the history
  • Loading branch information
pablof7z committed Jun 21, 2024
1 parent 75b0b93 commit 18c55bb
Show file tree
Hide file tree
Showing 14 changed files with 296 additions and 172 deletions.
5 changes: 5 additions & 0 deletions .changeset/curly-dolphins-speak.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@nostr-dev-kit/ndk": patch
---

fix bug where queued items were not getting processed (e.g. zap fetches)
5 changes: 5 additions & 0 deletions .changeset/kind-news-sin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@nostr-dev-kit/ndk": patch
---

Breaking change: event.zap is now removed, use ndk.zap(event) instead
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -323,16 +323,18 @@ 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("🤙");
```

### Zap an event

```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
Expand Down
1 change: 1 addition & 0 deletions ndk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 .",
Expand Down
43 changes: 0 additions & 43 deletions ndk/src/events/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<string | null> {
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
*
Expand Down
129 changes: 12 additions & 117 deletions ndk/src/events/kinds/NDKRelayList.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -21,117 +22,11 @@ export class NDKRelayList extends NDKEvent {
return new NDKRelayList(ndkEvent.ndk, ndkEvent.rawEvent());
}

static async forUser(pubkey: Hexpubkey, ndk: NDK): Promise<NDKRelayList | undefined> {
// 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<Map<Hexpubkey, NDKRelayList>> {
const pool = ndk.outboxPool || ndk.pool;
const set = new Set<NDKRelay>();

for (const relay of pool.relays.values()) set.add(relay);

const relayLists = new Map<Hexpubkey, NDKRelayList>();
const fromContactList = new Map<Hexpubkey, NDKEvent>();

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<Hexpubkey, NDKEvent>();
const contactListEvents = new Map<Hexpubkey, NDKEvent>();

return new Promise<Map<Hexpubkey, NDKRelayList>>(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"][]) {
Expand All @@ -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"][]) {
Expand Down Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions ndk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
3 changes: 2 additions & 1 deletion ndk/src/ndk/active-user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<NDKRelayList | undefined> {
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) {
Expand Down
46 changes: 44 additions & 2 deletions ndk/src/ndk/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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";
Expand Down Expand Up @@ -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<string | null> {
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
);
}
}
1 change: 1 addition & 0 deletions ndk/src/ndk/queue/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export class Queue<T> {
promise.finally(() => {
this.promises.delete(item.id);
this.processing.delete(item.id);
this.process();
});

return promise;
Expand Down
3 changes: 2 additions & 1 deletion ndk/src/outbox/tracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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<Hexpubkey, NDKRelayList>) => {
for (const [pubkey, relayList] of relayLists) {
const outboxItem = this.data.get(pubkey)!;
Expand Down
Loading

0 comments on commit 18c55bb

Please sign in to comment.