Skip to content

Commit

Permalink
nip-44 support
Browse files Browse the repository at this point in the history
  • Loading branch information
pablof7z committed Sep 28, 2024
1 parent 4de471f commit 722345b
Show file tree
Hide file tree
Showing 17 changed files with 386 additions and 49 deletions.
5 changes: 5 additions & 0 deletions .changeset/wise-dolls-provide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@nostr-dev-kit/ndk": patch
---

nip-44 support
24 changes: 12 additions & 12 deletions ndk/src/app-settings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,26 @@ import { NDKRelaySet } from "../relay/sets";

/**
* Implements NIP-78 App Settings
*
*
* @example
* const appSettings = new NDKAppSettings(ndk)
* appSettings.appName = "My App";
* appSettings.set("my_key", "my_value");
* await appSettings.save();
*
*
* @example
* const appSettings = NDKAppSettings.from(event);
* appSettings.appName = "My App";
* console.log(appSettings.get("my_key"));
*
*
* @group Kind Wrapper
*
*
* @see https://github.com/nostr-protocol/nips/blob/master/78.md
*/
export class NDKAppSettings extends NDKEvent {
public appName: string | undefined;
public settings: Record<string, unknown> = {};

constructor(ndk: NDK | undefined, rawEvent?: NostrEvent | NDKEvent) {
super(ndk, rawEvent);
this.kind ??= NDKKind.AppSpecificData;
Expand All @@ -45,19 +45,19 @@ export class NDKAppSettings extends NDKEvent {

/**
* Set a value for a given key.
*
* @param key
* @param value
*
* @param key
* @param value
*/
set(key: string, value: unknown) {
this.settings[key] = value;
}

/**
* Get a value for a given key.
*
* @param key
* @returns
*
* @param key
* @returns
*/
get(key: string) {
return this.settings[key];
Expand All @@ -69,7 +69,7 @@ export class NDKAppSettings extends NDKEvent {
requiredRelayCount?: number
) {
this.content = JSON.stringify(this.settings);

return super.publishReplaceable(relaySet, timeoutMs, requiredRelayCount);
}
}
85 changes: 85 additions & 0 deletions ndk/src/events/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { NDKSubscription } from "../subscription";
import { NDKUser } from "../user";
import { NDKRelaySet } from "../relay/sets";
import { NDKPrivateKeySigner } from "../signers/private-key";
import { NIP73EntityType } from "./nip73";

describe("NDKEvent", () => {
let ndk: NDK;
Expand Down Expand Up @@ -348,4 +349,88 @@ describe("NDKEvent", () => {
);
});
});

describe("tagExternal", () => {
it("correctly tags a URL", () => {
const event = new NDKEvent(ndk);
event.tagExternal("https://example.com/article/123#nostr", "url");

expect(event.tags).toContainEqual(["i", "https://example.com/article/123"]);
expect(event.tags).toContainEqual(["k", "https://example.com"]);
});

it("correctly tags a hashtag", () => {
const event = new NDKEvent(ndk);
event.tagExternal("NostrTest", "hashtag");

expect(event.tags).toContainEqual(["i", "#nostrtest"]);
expect(event.tags).toContainEqual(["k", "#"]);
});

it("correctly tags a geohash", () => {
const event = new NDKEvent(ndk);
event.tagExternal("u4pruydqqvj", "geohash");

expect(event.tags).toContainEqual(["i", "geo:u4pruydqqvj"]);
expect(event.tags).toContainEqual(["k", "geo"]);
});

it("correctly tags an ISBN", () => {
const event = new NDKEvent(ndk);
event.tagExternal("978-3-16-148410-0", "isbn");

expect(event.tags).toContainEqual(["i", "isbn:9783161484100"]);
expect(event.tags).toContainEqual(["k", "isbn"]);
});

it("correctly tags a podcast GUID", () => {
const event = new NDKEvent(ndk);
event.tagExternal("e32b4890-b9ea-4aef-a0bf-54b787833dc5", "podcast:guid");

expect(event.tags).toContainEqual([
"i",
"podcast:guid:e32b4890-b9ea-4aef-a0bf-54b787833dc5",
]);
expect(event.tags).toContainEqual(["k", "podcast:guid"]);
});

it("correctly tags an ISAN", () => {
const event = new NDKEvent(ndk);
event.tagExternal("1881-66C7-3420-0000-7-9F3A-0245-U", "isan");

expect(event.tags).toContainEqual(["i", "isan:1881-66C7-3420-0000"]);
expect(event.tags).toContainEqual(["k", "isan"]);
});

it("correctly tags a DOI", () => {
const event = new NDKEvent(ndk);
event.tagExternal("10.1000/182", "doi");

expect(event.tags).toContainEqual(["i", "doi:10.1000/182"]);
expect(event.tags).toContainEqual(["k", "doi"]);
});

it("adds a marker URL when provided", () => {
const event = new NDKEvent(ndk);
event.tagExternal(
"e32b4890-b9ea-4aef-a0bf-54b787833dc5",
"podcast:guid",
"https://example.com/marker"
);

expect(event.tags).toContainEqual([
"i",
"podcast:guid:e32b4890-b9ea-4aef-a0bf-54b787833dc5",
"https://example.com/marker",
]);
expect(event.tags).toContainEqual(["k", "podcast:guid"]);
});

it("throws an error for unsupported entity types", () => {
const event = new NDKEvent(ndk);
expect(() => {
event.tagExternal("test", "unsupported" as NIP73EntityType);
}).toThrow("Unsupported NIP-73 entity type: unsupported");
});
});
});
76 changes: 76 additions & 0 deletions ndk/src/events/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { fetchReplyEvent, fetchRootEvent, fetchTaggedEvent } from "./fetch-tagge
import { type NDKEventSerialized, deserialize, serialize } from "./serializer.js";
import { validate, verifySignature, getEventHash } from "./validation.js";
import { matchFilter } from "nostr-tools";
import { NIP73EntityType } from "./nip73.js";

const skipClientTagOnKinds = [NDKKind.Contacts];

Expand Down Expand Up @@ -139,6 +140,81 @@ export class NDKEvent extends EventEmitter {
return user;
}

/**
* NIP-73 tagging of external entities
* @param entity to be tagged
* @param type of the entity
* @param markerUrl to be used as the marker URL
*
* @example
* ```typescript
* event.tagExternal("https://example.com/article/123#nostr", "url");
* event.tags => [["i", "https://example.com/123"], ["k", "https://example.com"]]
* ```
*
* @example tag a podcast:item:guid
* ```typescript
* event.tagExternal("e32b4890-b9ea-4aef-a0bf-54b787833dc5", "podcast:item:guid");
* event.tags => [["i", "podcast:item:guid:e32b4890-b9ea-4aef-a0bf-54b787833dc5"], ["k", "podcast:item:guid"]]
* ```
*
* @see https://github.com/nostr-protocol/nips/blob/master/73.md
*/
public tagExternal(entity: string, type: NIP73EntityType, markerUrl?: string) {
let iTag: NDKTag = ["i"];
let kTag: NDKTag = ["k"];

switch (type) {
case "url":
const url = new URL(entity);
url.hash = ""; // Remove the fragment
iTag.push(url.toString());
kTag.push(`${url.protocol}//${url.host}`);
break;
case "hashtag":
iTag.push(`#${entity.toLowerCase()}`);
kTag.push("#");
break;
case "geohash":
iTag.push(`geo:${entity.toLowerCase()}`);
kTag.push("geo");
break;
case "isbn":
iTag.push(`isbn:${entity.replace(/-/g, "")}`);
kTag.push("isbn");
break;
case "podcast:guid":
iTag.push(`podcast:guid:${entity}`);
kTag.push("podcast:guid");
break;
case "podcast:item:guid":
iTag.push(`podcast:item:guid:${entity}`);
kTag.push("podcast:item:guid");
break;
case "podcast:publisher:guid":
iTag.push(`podcast:publisher:guid:${entity}`);
kTag.push("podcast:publisher:guid");
break;
case "isan":
iTag.push(`isan:${entity.split("-").slice(0, 4).join("-")}`);
kTag.push("isan");
break;
case "doi":
iTag.push(`doi:${entity.toLowerCase()}`);
kTag.push("doi");
break;
default:
throw new Error(`Unsupported NIP-73 entity type: ${type}`);
}

if (markerUrl) {
iTag.push(markerUrl);
}

this.tags.push(iTag);
this.tags.push(kTag);
}

/**
* Tag a user with an optional marker.
* @param target What is to be tagged. Can be an NDKUser, NDKEvent, or an NDKTag.
Expand Down
11 changes: 6 additions & 5 deletions ndk/src/events/nip04.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import type { NDKSigner } from "../signers";
import { DEFAULT_ENCRYPTION_SCHEME, ENCRYPTION_SCHEMES, type NDKSigner } from "../signers";
import type { NDKUser } from "../user";
import type { NDKEvent } from "./index.js";

export async function encrypt(
this: NDKEvent,
recipient?: NDKUser,
signer?: NDKSigner
signer?: NDKSigner,
type: ENCRYPTION_SCHEMES = DEFAULT_ENCRYPTION_SCHEME
): Promise<void> {
if (!this.ndk) throw new Error("No NDK instance found!");
if (!signer) {
Expand All @@ -24,10 +25,10 @@ export async function encrypt(
recipient = this.ndk.getUser({ pubkey: pTags[0][1] });
}

this.content = (await signer?.encrypt(recipient, this.content)) as string;
this.content = (await signer?.encrypt(recipient, this.content, type)) as string;
}

export async function decrypt(this: NDKEvent, sender?: NDKUser, signer?: NDKSigner): Promise<void> {
export async function decrypt(this: NDKEvent, sender?: NDKUser, signer?: NDKSigner, type: ENCRYPTION_SCHEMES = DEFAULT_ENCRYPTION_SCHEME): Promise<void> {
if (!this.ndk) throw new Error("No NDK instance found!");
if (!signer) {
await this.ndk.assertSigner();
Expand All @@ -37,5 +38,5 @@ export async function decrypt(this: NDKEvent, sender?: NDKUser, signer?: NDKSign
sender = this.author;
}

this.content = (await signer?.decrypt(sender, this.content)) as string;
this.content = (await signer?.decrypt(sender, this.content, type)) as string;
}
5 changes: 1 addition & 4 deletions ndk/src/events/nip19.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@ import { nip19 } from "nostr-tools";

import type { NDKEvent } from "./index.js";

export function encode(
this: NDKEvent,
maxRelayCount: number = 5
): string {
export function encode(this: NDKEvent, maxRelayCount: number = 5): string {
let relays: string[] = [];

if (this.onRelays.length > 0) {
Expand Down
13 changes: 13 additions & 0 deletions ndk/src/events/nip73.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* NIP-73 entity types
*/
export type NIP73EntityType =
| "url"
| "hashtag"
| "geohash"
| "isbn"
| "podcast:guid"
| "podcast:item:guid"
| "podcast:publisher:guid"
| "isan"
| "doi";
11 changes: 2 additions & 9 deletions ndk/src/ndk/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,8 @@ import { Queue } from "./queue/index.js";
import { signatureVerificationInit } from "../events/signature.js";
import { NDKSubscriptionManager } from "../subscription/manager.js";
import { setActiveUser } from "./active-user.js";
import type {
CashuPayCb,
LnPayCb,
NDKPaymentConfirmation,
NDKZapSplit} from "../zapper/index.js";
import {
NDKZapConfirmation,
NDKZapper
} from "../zapper/index.js";
import type { CashuPayCb, LnPayCb, NDKPaymentConfirmation, NDKZapSplit } from "../zapper/index.js";
import { NDKZapConfirmation, NDKZapper } from "../zapper/index.js";
import type { NostrEvent } from "nostr-tools";
import type { NDKLnUrlData } from "../zapper/ln.js";

Expand Down
11 changes: 9 additions & 2 deletions ndk/src/relay/subscription.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,11 @@ export class NDKRelaySubscription {
this.evaluateExecutionPlan(subscription);
break;
case NDKRelaySubscriptionStatus.CLOSED:
this.debug("Subscription is closed, cannot add new items %o (%o)", subscription, filters);
this.debug(
"Subscription is closed, cannot add new items %o (%o)",
subscription,
filters
);
throw new Error("Cannot add new items to a closed subscription");
}
}
Expand Down Expand Up @@ -313,7 +317,10 @@ export class NDKRelaySubscription {
} else {
// relays don't like to have the subscription close before they eose back,
// so wait until we eose before closing the old subscription
this.debug("We are abandoning an opened subscription, once it EOSE's, the handler will close it", { oldSubId })
this.debug(
"We are abandoning an opened subscription, once it EOSE's, the handler will close it",
{ oldSubId }
);
}
this._subId = undefined;
this.status = NDKRelaySubscriptionStatus.PENDING;
Expand Down
Loading

0 comments on commit 722345b

Please sign in to comment.