Skip to content

Commit 4ed75a6

Browse files
committed
add NIP-22 support
1 parent 5da9c99 commit 4ed75a6

File tree

5 files changed

+217
-4
lines changed

5 files changed

+217
-4
lines changed

.changeset/strong-files-fly.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@nostr-dev-kit/ndk": patch
3+
---
4+
5+
add NIP-22 support

ndk/src/events/index.test.ts

Lines changed: 133 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,17 @@ import { NDKUser } from "../user";
77
import { NDKRelaySet } from "../relay/sets";
88
import { NDKPrivateKeySigner } from "../signers/private-key";
99
import { NIP73EntityType } from "./nip73";
10+
import { NDKSigner } from "../signers";
11+
import { NDKKind } from "./kinds";
12+
13+
const ndk = new NDK();
1014

1115
describe("NDKEvent", () => {
12-
let ndk: NDK;
1316
let event: NDKEvent;
1417
let user1: NDKUser;
1518
let user2: NDKUser;
1619

1720
beforeEach(() => {
18-
ndk = new NDK();
1921
user1 = new NDKUser({
2022
npub: "npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft",
2123
});
@@ -433,4 +435,133 @@ describe("NDKEvent", () => {
433435
}).toThrow("Unsupported NIP-73 entity type: unsupported");
434436
});
435437
});
438+
439+
fdescribe("reply", () => {
440+
const signers = [0,1,2].map(i => NDKPrivateKeySigner.generate());
441+
let users: NDKUser[];
442+
443+
beforeEach(async () => {
444+
users = await Promise.all(signers.map(s => s.user()));
445+
});
446+
447+
const sign = async (event: Partial<NostrEvent>, signer = signers[0]) => {
448+
const e = new NDKEvent(ndk, event as NostrEvent);
449+
await e.sign(signer);
450+
return e;
451+
};
452+
453+
const reply = async (event: NDKEvent, signer: NDKSigner) => {
454+
const reply = event.reply();
455+
await reply.sign(signer);
456+
return reply;
457+
}
458+
459+
describe("replies to kind:1 events", () => {
460+
it("creates a reply using a kind 1 event", async () => {
461+
const op = await sign({ kind: 1 });
462+
const reply = op.reply();
463+
expect(reply.kind).toBe(1);
464+
});
465+
466+
it('carries over the root event of the OP', async () => {
467+
const root = await sign({ kind: 1 });
468+
const reply1 = await reply(root, signers[1]);
469+
const reply2 = reply1.reply();
470+
471+
expect(reply2.tags).toContainEqual(['e', root.id, '', 'root', root.pubkey]);
472+
expect(reply2.tags).toContainEqual(['p', root.pubkey]);
473+
});
474+
475+
it('adds a root marker for root events', async () => {
476+
const op = await sign({ kind: 1 });
477+
const reply = op.reply();
478+
expect(reply.tags).toContainEqual(['e', op.id, '', 'root', op.pubkey]);
479+
expect(reply.tags).toContainEqual(['p', op.pubkey]);
480+
});
481+
482+
it('adds a reply marker for non-root events', async () => {
483+
const op = await sign({ kind: 1 });
484+
const reply1 = await reply(op, signers[1]);
485+
const reply2 = reply1.reply();
486+
expect(reply2.tags).toContainEqual(['e', reply1.id, '', 'reply', reply1.pubkey]);
487+
expect(reply2.tags).toContainEqual(['p', reply1.pubkey]);
488+
});
489+
});
490+
491+
describe("replies to other kinds", () => {
492+
let root: NDKEvent;
493+
beforeAll(async () => {
494+
root = await sign({ kind: 30023 });
495+
});
496+
497+
it("creates a reply using a kind 1111 event", async () => {
498+
const reply1 = await reply(root, signers[1]);
499+
expect(reply1.kind).toBe(NDKKind.GenericReply);
500+
});
501+
502+
it("tags the root event or scope using an appropriate uppercase tag (e.g., 'A', 'E', 'I')", async () => {
503+
const reply1 = await reply(root, signers[1]);
504+
expect(reply1.tags).toContainEqual(['A', root.tagId(), ""]);
505+
});
506+
507+
it("tags the root event with an 'a' for addressable events when it's a top level reply", async () => {
508+
const root = await sign({ kind: 30023 });
509+
const reply1 = root.reply();
510+
expect(reply1.tags).toContainEqual(['A', root.tagId(), ""]);
511+
expect(reply1.tags).toContainEqual(['a', root.tagId(), ""]);
512+
});
513+
514+
it("p-tags the author of the root event", async () => {
515+
const root = await sign({ kind: 30023 });
516+
const reply1 = root.reply();
517+
expect(reply1.tags).toContainEqual(['p', root.pubkey]);
518+
});
519+
520+
it('p-tags the author of the reply event', async () => {
521+
const root = await sign({ kind: 30023 });
522+
const reply1 = await reply(root, signers[1]);
523+
const reply2 = reply1.reply();
524+
expect(reply2.tags).toContainEqual(['p', reply1.pubkey]);
525+
});
526+
527+
it("p-tags the author of the root event only once when it's the root reply", async () => {
528+
const root = await sign({ kind: 30023 });
529+
const reply1 = root.reply();
530+
expect(reply1.tags).toContainEqual(['p', root.pubkey]);
531+
expect(reply1.tags.filter(t => t[0] === 'p')).toHaveLength(1);
532+
});
533+
534+
it('p-tags the author of the root and reply events', async () => {
535+
const reply1 = await reply(root, signers[1]);
536+
const reply2 = reply1.reply();
537+
expect(reply2.tags).toContainEqual(['p', root.pubkey]);
538+
expect(reply2.tags).toContainEqual(['p', reply1.pubkey]);
539+
});
540+
541+
it("tags the root event or scope using an appropriate uppercase tag with the pubkey when it's an E tag", async () => {
542+
const root = await sign({ kind: 20 }, signers[0]);
543+
const reply1 = await reply(root, signers[1]);
544+
expect(reply1.tags).toContainEqual(['E', root.tagId(), "", root.pubkey]);
545+
});
546+
547+
it("tags the parent item using an appropriate lowercase tag (e.g., 'a', 'e', 'i')", async () => {
548+
const reply1 = await reply(root, signers[1]);
549+
const reply2 = reply1.reply();
550+
expect(reply2.tags).toContainEqual(['A', root.tagId(), ""]);
551+
expect(reply2.tags).toContainEqual(['e', reply1.tagId(), "", reply1.pubkey]);
552+
});
553+
554+
it("adds a 'K' tag to specify the root kind", async () => {
555+
const reply1 = await reply(root, signers[1]);
556+
expect(reply1.tags).toContainEqual(['K', root.kind!.toString()]);
557+
});
558+
559+
it("adds a 'k' tag to specify the parent kind", async () => {
560+
const reply1 = await reply(root, signers[1]);
561+
const reply2 = reply1.reply();
562+
expect(reply2.tags).toContainEqual(['k', reply1.kind!.toString()]);
563+
});
564+
});
565+
});
566+
436567
});

ndk/src/events/index.ts

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -645,8 +645,10 @@ export class NDKEvent extends EventEmitter {
645645
tag.push("");
646646
}
647647

648-
if (marker) {
649-
tag.push(marker);
648+
tag.push(marker ?? "");
649+
650+
if (!this.isParamReplaceable()) {
651+
tag.push(this.pubkey);
650652
}
651653

652654
return tag;
@@ -827,4 +829,58 @@ export class NDKEvent extends EventEmitter {
827829
get isValid(): boolean {
828830
return this.validate();
829831
}
832+
833+
public reply(): NDKEvent {
834+
const reply = new NDKEvent(this.ndk);
835+
836+
if (this.kind === 1) {
837+
reply.kind = 1;
838+
const opHasETag = this.hasTag("e");
839+
840+
if (opHasETag) {
841+
reply.tags = [
842+
...reply.tags,
843+
...this.getMatchingTags("e"),
844+
...this.getMatchingTags("p"),
845+
...this.getMatchingTags("a"),
846+
...this.referenceTags("reply")
847+
];
848+
} else {
849+
reply.tag(this, "root");
850+
}
851+
} else {
852+
reply.kind = NDKKind.GenericReply;
853+
854+
const carryOverTags = ["A", "E", "I"];
855+
const rootTag = this.tags.find((tag) => carryOverTags.includes(tag[0]));
856+
857+
// we have a root tag already
858+
if (rootTag) {
859+
const rootKind = this.tagValue("K");
860+
reply.tags.push(rootTag);
861+
if (rootKind) reply.tags.push(["K", rootKind]);
862+
863+
reply.tags.push(["k", this.kind!.toString()]);
864+
865+
const [ type, id, _, ...extra] = this.tagReference();
866+
const tag = [type, id, ...extra];
867+
reply.tags.push(tag);
868+
} else {
869+
const [ type, id, _, relayHint] = this.tagReference();
870+
const tag = [type, id, relayHint ?? ""];
871+
if (type === "e") tag.push(this.pubkey);
872+
reply.tags.push(tag);
873+
const uppercaseTag = [...tag];
874+
uppercaseTag[0] = uppercaseTag[0].toUpperCase();
875+
reply.tags.push(uppercaseTag);
876+
reply.tags.push(["K", this.kind!.toString()])
877+
}
878+
879+
// carry over all p tags
880+
reply.tags.push(...this.getMatchingTags("p"));
881+
reply.tags.push(["p", this.pubkey]);
882+
}
883+
884+
return reply;
885+
}
830886
}

ndk/src/events/kinds/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ export enum NDKKind {
2525
ChannelMessage = 42,
2626
ChannelHideMessage = 43,
2727
ChannelMuteUser = 44,
28+
29+
GenericReply = 1111,
30+
2831
Media = 1063,
2932
Report = 1984,
3033
Label = 1985,

ndk/src/user/index.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,9 +387,27 @@ export class NDKUser {
387387

388388
/**
389389
* Returns a set of users that this user follows.
390+
*
391+
* @deprecated Use followSet instead
390392
*/
391393
public follows = follows.bind(this);
392394

395+
/**
396+
* Returns a set of pubkeys that this user follows.
397+
*
398+
* @param opts - NDKSubscriptionOptions
399+
* @param outbox - boolean
400+
* @param kind - number
401+
*/
402+
public async followSet(
403+
opts?: NDKSubscriptionOptions,
404+
outbox?: boolean,
405+
kind: number = NDKKind.Contacts
406+
): Promise<Set<Hexpubkey>> {
407+
const follows = await this.follows(opts, outbox, kind);
408+
return new Set(Array.from(follows).map((f) => f.pubkey));
409+
}
410+
393411
/** @deprecated Use referenceTags instead. */
394412
/**
395413
* Get the tag that can be used to reference this user in an event

0 commit comments

Comments
 (0)