New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

@nostr-dev-kit/ndk

Package Overview
Dependencies
Maintainers
0
Versions
200
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@nostr-dev-kit/ndk - npm Package Compare versions

Comparing version 2.11.0 to 2.11.1

src/events/encryption.test.ts

2

package.json
{
"name": "@nostr-dev-kit/ndk",
"version": "2.11.0",
"version": "2.11.1",
"description": "NDK - Nostr Development Kit",

@@ -5,0 +5,0 @@ "homepage": "https://ndk.fyi",

@@ -38,6 +38,5 @@ import { generateContentTags } from "./content-tagger";

[
"e",
"q",
"6ab77c8baf6d0542131cc70b59ba3ece904fd58efe91d612dc3e870bdaf93034",
"",
"mention",
],

@@ -78,6 +77,5 @@ ]);

[
"e",
"q",
"6ab77c8baf6d0542131cc70b59ba3ece904fd58efe91d612dc3e870bdaf93034",
"",
"mention",
],

@@ -103,6 +101,5 @@ ]);

[
"e",
"q",
"8c9093d06a21a5b738e9d21d907334444e7ea12258c21da333e0fc265cf92a8b",
"",
"mention",
],

@@ -125,6 +122,5 @@ ]);

[
"e",
"q",
"8c9093d06a21a5b738e9d21d907334444e7ea12258c21da333e0fc265cf92a8b",
"",
"mention",
],

@@ -147,6 +143,5 @@ ]);

[
"a",
"q",
"30023:6389be6491e7b693e9f368ece88fcd145f07c068d2c1bbae4247b9b5ef439d32:283946",
"",
"mention",
],

@@ -170,6 +165,5 @@ ["p", "6389be6491e7b693e9f368ece88fcd145f07c068d2c1bbae4247b9b5ef439d32"],

[
"e",
"q",
"4b6d6da883fc7d74d3936b0a4e0fc8ed00fd505e9024e1ae9d96fc1d2616fe5c",
"wss://nos.lol/",
"mention",
],

@@ -193,6 +187,5 @@ ["p", "b190175cef407c593f17c155480eda1a2ee7f6c84378eef0e97353f87ee59300"],

[
"a",
"q",
"30023:6389be6491e7b693e9f368ece88fcd145f07c068d2c1bbae4247b9b5ef439d32:283946",
"wss://nos.lol",
"mention",
],

@@ -199,0 +192,0 @@ ["p", "6389be6491e7b693e9f368ece88fcd145f07c068d2c1bbae4247b9b5ef439d32"],

@@ -11,2 +11,14 @@ import { nip19 } from "nostr-tools";

/**
* Merges two arrays of NDKTag, ensuring uniqueness and preferring more specific tags.
*
* This function consolidates `tags1` and `tags2` by:
* - Combining both tag arrays.
* - Removing duplicate tags based on containment (one tag containing another).
* - Retaining the longer or more detailed tag when overlaps are found.
*
* @param tags1 - The first array of NDKTag.
* @param tags2 - The second array of NDKTag.
* @returns A merged array of unique NDKTag.
*/
export function mergeTags(tags1: NDKTag[], tags2: NDKTag[]): NDKTag[] {

@@ -75,2 +87,24 @@ const tagMap = new Map<string, NDKTag>();

const hashtagRegex = /(?<=\s|^)(#[^\s!@#$%^&*()=+./,[{\]};:'"?><]+)/g;
/**
* Generates a unique list of hashtags as used in the content. If multiple variations
* of the same hashtag are used, only the first one will be used (#ndk and #NDK both resolve to the first one that was used in the content)
* @param content
* @returns
*/
export function generateHashtags(content: string): string[] {
const hashtags = content.match(hashtagRegex);
const tagIds = new Set<string>();
const tag = new Set<string>();
if (hashtags) {
for (const hashtag of hashtags) {
if (tagIds.has(hashtag.slice(1))) continue;
tag.add(hashtag.slice(1));
tagIds.add(hashtag.slice(1));
}
}
return Array.from(tag);
}
export async function generateContentTags(

@@ -81,3 +115,3 @@ content: string,

const tagRegex = /(@|nostr:)(npub|nprofile|note|nevent|naddr)[a-zA-Z0-9]+/g;
const hashtagRegex = /(?<=\s|^)(#[^\s!@#$%^&*()=+./,[{\]};:'"?><]+)/g;
const promises: Promise<void>[] = [];

@@ -110,6 +144,5 @@

addTagIfNew([
"e",
"q",
data,
await maybeGetEventRelayUrl(entity),
"mention",
]);

@@ -132,3 +165,3 @@ resolve();

addTagIfNew(["e", id, relays[0], "mention"]);
addTagIfNew(["q", id, relays[0]]);
if (author) addTagIfNew(["p", author]);

@@ -151,3 +184,3 @@ resolve();

addTagIfNew(["a", id, relays[0], "mention"]);
addTagIfNew(["q", id, relays[0]]);
addTagIfNew(["p", data.pubkey]);

@@ -172,9 +205,4 @@ resolve();

content = content.replace(hashtagRegex, (tag, word) => {
const t: NDKTag = ["t", word.slice(1)];
if (!tags.find((t2) => t2[0] === t[0] && t2[1] === t[1])) {
tags.push(t);
}
return tag; // keep the original tag in the content
});
const newTags = generateHashtags(content).map((hashtag) => ["t", hashtag]);
tags = mergeTags(tags, newTags);

@@ -181,0 +209,0 @@ return { content, tags };

import { EventEmitter } from "tseep";
import type { NDK } from "../ndk/index.js";
import { NDK } from "../ndk/index.js";
import type { NDKRelay } from "../relay/index.js";

@@ -13,3 +13,3 @@ import { calculateRelaySetFromEvent } from "../relay/sets/calculate.js";

import { NDKKind } from "./kinds/index.js";
import { decrypt, encrypt } from "./nip04.js";
import { decrypt, encrypt } from "./encryption.js";
import { encode } from "./nip19.js";

@@ -20,6 +20,13 @@ import { repost } from "./repost.js";

import { validate, verifySignature, getEventHash } from "./validation.js";
import { matchFilter } from "nostr-tools";
import { NIP73EntityType } from "./nip73.js";
const skipClientTagOnKinds = [NDKKind.Contacts];
const skipClientTagOnKinds = new Set([
NDKKind.Metadata,
NDKKind.EncryptedDirectMessage,
NDKKind.GiftWrap,
NDKKind.GiftWrapSeal,
NDKKind.Contacts,
NDKKind.ZapRequest,
NDKKind.EventDeletion
]);

@@ -477,3 +484,3 @@ export type NDKEventId = string;

// add to cache for optimistic updates
if (this.ndk.cacheAdapter?.addUnpublishedEvent) {
if (this.ndk.cacheAdapter?.addUnpublishedEvent && shouldTrackUnpublishedEvent(this)) {
try {

@@ -534,3 +541,3 @@ this.ndk.cacheAdapter.addUnpublishedEvent(this, relaySet.relayUrls);

tags.push(clientTag);
} else {
} else if (this.shouldStripClientTag) {
tags = tags.filter((tag) => tag[0] !== "client");

@@ -544,4 +551,4 @@ }

if (!this.ndk?.clientName && !this.ndk?.clientNip89) return false;
if (skipClientTagOnKinds.includes(this.kind!)) return false;
if (this.isEphemeral()) return false;
if (skipClientTagOnKinds.has(this.kind!)) return false;
if (this.isEphemeral() || this.isReplaceable()) return false;
if (this.hasTag("client")) return false;

@@ -551,2 +558,6 @@ return true;

get shouldStripClientTag(): boolean {
return skipClientTagOnKinds.has(this.kind!);
}
public muted(): string | null {

@@ -567,2 +578,4 @@ const authorMutedEntry = this.ndk?.mutedIds.get(this.pubkey);

* @returns {string} the "d" tag of the event.
*
* @deprecated Use `dTag` instead.
*/

@@ -613,11 +626,18 @@ replaceableDTag() {

/**
* Returns the "reference" value ("<kind>:<author-pubkey>:<d-tag>") for this replaceable event.
* @returns {string} The id
* Returns a stable reference value for a replaceable event.
*
* Param replaceable events are returned in the expected format of `<kind>:<pubkey>:<d-tag>`.
* Kind-replaceable events are returned in the format of `<kind>:<pubkey>:`.
*
* @returns {string} A stable reference value for replaceable events
*/
tagAddress(): string {
if (!this.isParamReplaceable()) {
throw new Error("This must only be called on replaceable events");
if (this.isParamReplaceable()) {
const dTagId = this.dTag ?? "";
return `${this.kind}:${this.pubkey}:${dTagId}`;
} else if (this.isReplaceable()) {
return `${this.kind}:${this.pubkey}:`;
}
const dTagId = this.replaceableDTag();
return `${this.kind}:${this.pubkey}:${dTagId}`;
throw new Error("Event is not a replaceable event");
}

@@ -748,2 +768,10 @@

nip22Filter(): NDKFilter {
if (this.isParamReplaceable()) {
return { "#A": [this.tagId()] };
} else {
return { "#E": [this.tagId()] };
}
}
/**

@@ -776,2 +804,19 @@ * Generates a deletion event of the current event

/**
* Establishes whether this is a NIP-70-protectede event.
* @@satisfies NIP-70
*/
set isProtected(val: boolean) {
this.removeTag('-');
if (val) this.tags.push(['-']);
}
/**
* Whether this is a NIP-70-protectede event.
* @@satisfies NIP-70
*/
get isProtected(): boolean {
return this.hasTag('-');
}
/**
* Fetch an event tagged with the given tag following relay hints if provided.

@@ -906,3 +951,3 @@ * @param tag The tag to search for

// carry over all p tags
// carry over all p tags
reply.tags.push(...this.getMatchingTags("p"));

@@ -915,1 +960,13 @@ reply.tags.push(["p", this.pubkey]);

}
const untrackedUnpublishedEvents = new Set([
NDKKind.NostrConnect,
NDKKind.NostrWaletConnectInfo,
NDKKind.NostrWalletConnectReq,
NDKKind.NostrWalletConnectRes,
]);
function shouldTrackUnpublishedEvent(event: NDKEvent): boolean {
return !untrackedUnpublishedEvents.has(event.kind!);
}

@@ -16,7 +16,10 @@ export enum NDKKind {

GroupReply = 12,
GiftWrapSeal = 13,
// Gift Wrapped Rumors
PrivateDirectMessage = 14,
Image = 20,
// NIP-22
GenericRespose = 22,
// Nip 59 : Gift Wrap
GiftWrap = 1059,

@@ -101,2 +104,5 @@ GenericRepost = 16,

// NIP-60
CashuWallet = 17375,
FollowSet = 30000,

@@ -155,3 +161,4 @@ CategorizedPeopleList = NDKKind.FollowSet, // Deprecated but left for backwards compatibility

CashuWallet = 37375,
// Legacy Cashu Wallet
LegacyCashuWallet = 37375,

@@ -158,0 +165,0 @@ GroupMetadata = 39000, // NIP-29

@@ -68,6 +68,2 @@ import debug from "debug";

}
// remove amount tags
this.removeTag("amount");
this.tags.push(["amount", this.amount.toString()]);
}

@@ -97,3 +93,3 @@

const paddedp2pk = payload[1].data;
const p2pk = paddedp2pk.slice(2, -1);
const p2pk = paddedp2pk.slice(2);

@@ -120,3 +116,3 @@ if (p2pk) return p2pk;

get unit(): string {
return this.tagValue("unit") ?? "msat";
return this.tagValue("unit") ?? "sat";
}

@@ -130,4 +126,5 @@

get amount(): number {
const count = this.proofs.reduce((total, proof) => total + proof.amount, 0);
return count * 1000;
const amount = this.proofs.reduce((total, proof) => total + proof.amount, 0);
if (this.unit === "msat") return amount * 1000;
return amount;
}

@@ -166,2 +163,16 @@

async toNostrEvent(): Promise<NostrEvent> {
// if the unit is msat, convert to sats
if (this.unit === "msat") {
this.unit = "sat";
}
this.removeTag("amount");
this.tags.push(["amount", this.amount.toString()]);
const event = await super.toNostrEvent();
event.content = this.comment;
return event;
}
/**

@@ -171,2 +182,3 @@ * Validates that the nutzap conforms to NIP-61

get isValid(): boolean {
let eTagCount = 0;
let pTagCount = 0;

@@ -176,2 +188,3 @@ let mintTagCount = 0;

for (const tag of this.tags) {
if (tag[0] === "e") eTagCount++;
if (tag[0] === "p") pTagCount++;

@@ -185,2 +198,5 @@ if (tag[0] === "u") mintTagCount++;

mintTagCount === 1 &&
// must have at most one e tag
eTagCount <= 1 &&
// must have at least one proof

@@ -187,0 +203,0 @@ this.proofs.length > 0

@@ -5,2 +5,5 @@ import { NDKKind } from ".";

import type { NDK } from "../../ndk";
import { NDKImetaTag } from "../../utils/imeta";
import { imetaTagToTag } from "../../utils/imeta";
import { mapImetaTag } from "../../utils/imeta";
import type { ContentTag } from "../content-tagger";

@@ -15,2 +18,3 @@

static kinds = [NDKKind.HorizontalVideo, NDKKind.VerticalVideo];
private _imetas: NDKImetaTag[] | undefined;

@@ -58,26 +62,32 @@ constructor(ndk: NDK | undefined, rawEvent?: NostrEvent) {

get thumbnail(): string | undefined {
return this.tagValue("thumb");
let thumbnail: string | undefined;
if (this.imetas && this.imetas.length > 0) {
thumbnail = this.imetas[0].image?.[0];
}
return thumbnail ?? this.tagValue("thumb");
}
/**
* Setter for the article thumbnail.
*
* @param {string | undefined} thumbnail - The thumbnail to set for the article.
*/
set thumbnail(thumbnail: string | undefined) {
this.removeTag("thumb");
get imetas(): NDKImetaTag[] {
if (this._imetas) return this._imetas;
this._imetas = this.tags.filter((tag) => tag[0] === "imeta")
.map(mapImetaTag)
return this._imetas;
}
if (thumbnail) this.tags.push(["thumb", thumbnail]);
set imetas(tags: NDKImetaTag[]) {
this._imetas = tags;
this.tags = this.tags.filter((tag) => tag[0] !== "imeta");
this.tags.push(...tags.map(imetaTagToTag));
}
get url(): string | undefined {
if (this.imetas && this.imetas.length > 0) {
return this.imetas[0].url;
}
return this.tagValue("url");
}
set url(url: string | undefined) {
this.removeTag("url");
if (url) this.tags.push(["url", url]);
}
/**

@@ -84,0 +94,0 @@ * Getter for the article's publication timestamp.

@@ -35,3 +35,3 @@ import "websocket-polyfill";

} as NostrEvent);
event.relay = new NDKRelay("wss://relay.f7z.io/");
event.relay = new NDKRelay("wss://relay.f7z.io/", undefined, ndk);

@@ -51,3 +51,3 @@ const a = event.encode();

} as NostrEvent);
event.relay = new NDKRelay("wss://relay.f7z.io/");
event.relay = new NDKRelay("wss://relay.f7z.io/", undefined, ndk);

@@ -54,0 +54,0 @@ const a = event.encode();

@@ -27,3 +27,5 @@ import type { NDKSigner } from "../signers/index.js";

} as NostrEvent);
e.content = JSON.stringify(this.rawEvent());
if (!this.isProtected)
e.content = JSON.stringify(this.rawEvent());
e.tag(this);

@@ -30,0 +32,0 @@

@@ -8,23 +8,29 @@ import { NDKPool } from "./relay/pool/index.js";

export * from "./events/index.js";
export * from "./events/content-tagger.js";
export * from "./types.js";
// Kinds
export * from "./events/kinds/index.js";
export * from "./events/kinds/article.js";
export * from "./events/kinds/wiki.js";
export * from "./events/kinds/classified.js";
export * from "./events/kinds/video.js";
export * from "./events/kinds/drafts.js";
export * from "./events/kinds/dvm/index.js";
export * from "./events/kinds/highlight.js";
export * from "./events/kinds/image.js";
export * from "./events/kinds/index.js";
export * from "./events/kinds/lists/index.js";
export * from "./events/kinds/NDKRelayList.js";
export * from "./events/kinds/lists/index.js";
export * from "./events/kinds/drafts.js";
export * from "./events/kinds/nip89/NDKAppHandler.js";
export * from "./events/kinds/nutzap/index.js";
export * from "./events/kinds/nutzap/mint-list.js";
export * from "./events/kinds/repost.js";
export * from "./events/kinds/nip89/NDKAppHandler.js";
export * from "./events/kinds/subscriptions/tier.js";
export * from "./events/kinds/subscriptions/amount.js";
export * from "./events/kinds/subscriptions/receipt.js";
export * from "./events/kinds/subscriptions/subscription-start.js";
export * from "./events/kinds/subscriptions/receipt.js";
export * from "./events/kinds/dvm/index.js";
export * from "./events/kinds/nutzap/mint-list.js";
export * from "./events/kinds/nutzap/index.js";
export * from "./events/kinds/subscriptions/tier.js";
export * from "./events/kinds/video.js";
export * from "./events/kinds/wiki.js";
export * from "./events/wrap.js";
export * from "./thread/index.js";

@@ -63,1 +69,2 @@

export * from "./utils/get-users-relay-list.js";
export * from "./utils/imeta.js";

@@ -190,2 +190,7 @@ import debug from "debug";

export interface NDKSubscriptionEventHandlers {
onEvent?: (event: NDKEvent, relay?: NDKRelay) => void;
onEose?: (sub: NDKSubscription) => void;
}
/**

@@ -222,2 +227,3 @@ * The NDK class is the main entry point to the library.

private _explicitRelayUrls?: WebSocket["url"][];
public blacklistRelayUrls?: WebSocket["url"][];
public pool: NDKPool;

@@ -244,2 +250,4 @@ public outboxPool?: NDKPool;

public pools: NDKPool[] = [];
/**

@@ -299,9 +307,10 @@ * Default relay-auth policy that will be used when a relay requests authentication,

this._explicitRelayUrls = opts.explicitRelayUrls || [];
this.blacklistRelayUrls = opts.blacklistRelayUrls || DEFAULT_BLACKLISTED_RELAYS;
this.subManager = new NDKSubscriptionManager(this.debug);
this.pool = new NDKPool(
opts.explicitRelayUrls || [],
opts.blacklistRelayUrls || DEFAULT_BLACKLISTED_RELAYS,
[],
this
);
this.pool.name = "main";
this.pool.name = "Main";

@@ -325,7 +334,9 @@ this.pool.on("relay:auth", async (relay: NDKRelay, challenge: string) => {

opts.outboxRelayUrls || DEFAULT_OUTBOX_RELAYS,
opts.blacklistRelayUrls || DEFAULT_BLACKLISTED_RELAYS,
[],
this,
this.debug.extend("outbox-pool")
{
debug: this.debug.extend("outbox-pool"),
name: "Outbox Pool"
}
);
this.outboxPool.name = "outbox";

@@ -499,3 +510,3 @@ this.outboxTracker = new OutboxTracker(this);

* @param relaySet explicit relay set to use
* @param autoStart automatically start the subscription
* @param autoStart automatically start the subscription -- this can be a boolean or an object with `onEvent` and `onEose` handlers
* @returns NDKSubscription

@@ -507,3 +518,3 @@ */

relaySet?: NDKRelaySet,
autoStart = true
autoStart: boolean | NDKSubscriptionEventHandlers = true
): NDKSubscription {

@@ -534,2 +545,6 @@ const subscription = new NDKSubscription(this, filters, opts, relaySet);

if (autoStart) {
if (typeof autoStart === "object") {
if (autoStart.onEvent) subscription.on("event", autoStart.onEvent);
if (autoStart.onEose) subscription.on("eose", autoStart.onEose);
}
setTimeout(() => subscription.start(), 0);

@@ -733,7 +748,2 @@ }

set wallet(wallet: NDKWalletInterface | undefined) {
console.log('setting wallet', {
lnPay: wallet?.lnPay,
cashuPay: wallet?.cashuPay,
})
if (!wallet) {

@@ -740,0 +750,0 @@ this.walletConfig = undefined;

@@ -15,3 +15,3 @@ import { NDKRelayConnectivity } from "./connectivity";

ndk = new NDK();
relay = new NDKRelay("wss://test.relay");
relay = new NDKRelay("wss://test.relay", undefined, ndk);
connectivity = new NDKRelayConnectivity(relay, ndk);

@@ -18,0 +18,0 @@ });

@@ -6,3 +6,4 @@ import { NDKRelay } from "./index.js";

const ndk = new NDK();
const relay = new NDKRelay("ws://localhost/");
const relayUrl = "ws://localhost/";
const relay = new NDKRelay(relayUrl, undefined, ndk);
ndk.addExplicitRelay(relay, undefined, false);

@@ -9,0 +10,0 @@

@@ -8,8 +8,9 @@ import "websocket-polyfill";

it("refuses connecting to blacklisted relays", async () => {
const blacklistedRelay = new NDKRelay("wss://url1");
const blacklistedRelayUrl = "wss://url1";
const ndk = new NDK({
blacklistRelayUrls: [blacklistedRelay.url],
blacklistRelayUrls: [blacklistedRelayUrl],
});
const { pool } = ndk;
pool.addRelay(blacklistedRelay);
const relay = new NDKRelay(blacklistedRelayUrl, undefined, ndk);
pool.addRelay(relay);

@@ -16,0 +17,0 @@ expect(pool.relays.size).toEqual(0);

@@ -46,4 +46,5 @@ import type debug from "debug";

private _relays = new Map<WebSocket["url"], NDKRelay>();
private status: 'idle' | 'active' = 'idle';
public autoConnectRelays = new Set<WebSocket["url"]>();
public blacklistRelayUrls: Set<WebSocket["url"]>;
public poolBlacklistRelayUrls = new Set<WebSocket["url"]>();
private debug: debug.Debugger;

@@ -56,2 +57,14 @@ private temporaryRelayTimers = new Map<WebSocket["url"], NodeJS.Timeout>();

get blacklistRelayUrls() {
const val = new Set(this.ndk.blacklistRelayUrls);
this.poolBlacklistRelayUrls.forEach((url) => val.add(url));
return val;
}
/**
* @param relayUrls - The URLs of the relays to connect to.
* @param blacklistedRelayUrls - URLs to blacklist for this pool IN ADDITION to those blacklisted at the ndk-level
* @param ndk - The NDK instance.
* @param opts - Options for the pool.
*/
public constructor(

@@ -61,10 +74,16 @@ relayUrls: WebSocket["url"][] = [],

ndk: NDK,
debug?: debug.Debugger
{ debug, name }: {
debug?: debug.Debugger,
name?: string
} = {}
) {
super();
this.debug = debug ?? ndk.debug.extend("pool");
if (name) this._name = name;
this.ndk = ndk;
this.relayUrls = relayUrls;
this.blacklistRelayUrls = new Set(blacklistedRelayUrls);
this.poolBlacklistRelayUrls = new Set(blacklistedRelayUrls);
this.ndk.pools.push(this);
}

@@ -81,7 +100,14 @@

relay.connectivity.netDebug = this.ndk.netDebug;
this.addRelay(relay, false);
this.addRelay(relay);
}
}
private _name: string = 'unnamed';
get name() {
return this._name;
}
set name(name: string) {
this._name = name;
this.debug = this.debug.extend(name);

@@ -207,6 +233,7 @@ }

});
this.relays.set(relayUrl, relay);
this._relays.set(relayUrl, relay);
if (connect) this.autoConnectRelays.add(relayUrl);
if (connect) {
// only connect if the pool is active
if (connect && this.status === 'active') {
this.emit("relay:connecting", relay);

@@ -310,2 +337,4 @@ relay.connect(undefined, reconnect).catch((e) => {

this.status = 'active';
this.debug(

@@ -318,10 +347,9 @@ `Connecting to ${this.relays.size} relays${

const relaysToConnect = new Set(this.autoConnectRelays.keys());
this.ndk.explicitRelayUrls?.forEach((url) => {
const normalizedUrl = normalizeRelayUrl(url);
relaysToConnect.add(normalizedUrl);
});
for (const relayUrl of relaysToConnect) {
const relay = this.relays.get(relayUrl);
if (!relay) continue;
if (!relay) {
console.log(`Relay ${relayUrl} not found in pool ${this.name}`);
continue;
}

@@ -350,16 +378,19 @@ const connectPromise = new Promise<void>((resolve, reject) => {

const maybeEmitConnect = () => {
const allConnected = this.stats().connected === this.relays.size;
const someConnected = this.stats().connected > 0;
if (!allConnected && someConnected) {
this.emit("connect");
}
};
// If we are running with a timeout, check if we need to emit a `connect` event
// in case some, but not all, relays were connected
if (timeoutMs) {
setTimeout(() => {
const allConnected = this.stats().connected === this.relays.size;
const someConnected = this.stats().connected > 0;
if (timeoutMs)
setTimeout(maybeEmitConnect, timeoutMs);
if (!allConnected && someConnected) {
this.emit("connect");
}
}, timeoutMs);
}
await Promise.all(promises);
await Promise.all(promises);
maybeEmitConnect();
}

@@ -366,0 +397,0 @@

@@ -20,3 +20,3 @@ import { NDKRelaySubscription, NDKRelaySubscriptionStatus } from "./subscription";

private relay: NDKRelay;
private subscriptions: Map<NDKFilterFingerprint, NDKRelaySubscription[]>;
public subscriptions: Map<NDKFilterFingerprint, NDKRelaySubscription[]>;
private generalSubManager: NDKSubscriptionManager;

@@ -23,0 +23,0 @@

@@ -52,3 +52,3 @@ // NDKRelaySubscription.test.ts

beforeEach(() => {
ndkRelaySubscription = new NDKRelaySubscription(relay);
ndkRelaySubscription = new NDKRelaySubscription(relay, null, ndk.subManager);
ndkRelaySubscription.debug = debug("test");

@@ -55,0 +55,0 @@ });

import type { NostrEvent } from "../events/index.js";
import type { NDK } from "../ndk/index.js";
import type { NDKRelay } from "../relay/index.js";
import { NDKEncryptionScheme } from "../types.js";
import type { NDKUser } from "../user";
export type ENCRYPTION_SCHEMES = "nip04" | "nip44";
export const DEFAULT_ENCRYPTION_SCHEME: ENCRYPTION_SCHEMES = "nip44";

@@ -39,36 +38,26 @@ /**

/**
* Determine the types of encryption (by nip) that this signer can perform.
* Implementing classes SHOULD return a value even for legacy (only nip04) third party signers.
* @nip Optionally returns an array with single supported nip or empty, to check for truthy or falsy.
* @return A promised list of any (or none) of these strings ['nip04', 'nip44']
*/
encryptionEnabled?(scheme?: NDKEncryptionScheme): Promise<NDKEncryptionScheme[]>
/**
* Encrypts the given Nostr event for the given recipient.
* Implementing classes SHOULD equate legacy (only nip04) to nip == `nip04` || undefined
* @param recipient - The recipient (pubkey or conversationKey) of the encrypted value.
* @param value - The value to be encrypted.
* @param recipient - The recipient of the encrypted value.
* @param type - The encryption scheme to use. Defaults to "nip04".
* @param nip - which NIP is being implemented ('nip04', 'nip44')
*/
encrypt(recipient: NDKUser, value: string, type?: ENCRYPTION_SCHEMES): Promise<string>;
encrypt(recipient: NDKUser, value: string, scheme?: NDKEncryptionScheme): Promise<string>;
/**
* Decrypts the given value.
* @param value
* @param sender
* @param type - The encryption scheme to use. Defaults to "nip04".
* Implementing classes SHOULD equate legacy (only nip04) to nip == `nip04` || undefined
* @param sender - The sender (pubkey or conversationKey) of the encrypted value
* @param value - The value to be decrypted
* @param scheme - which NIP is being implemented ('nip04', 'nip44', 'nip49')
*/
decrypt(sender: NDKUser, value: string, type?: ENCRYPTION_SCHEMES): Promise<string>;
/**
* @deprecated use nip44Encrypt instead
*/
nip04Encrypt(recipient: NDKUser, value: string): Promise<string>;
/**
* @deprecated use nip44Decrypt instead
*/
nip04Decrypt(sender: NDKUser, value: string): Promise<string>;
/**
* @deprecated use nip44Encrypt instead
*/
nip44Encrypt(recipient: NDKUser, value: string): Promise<string>;
/**
* @deprecated use nip44Decrypt instead
*/
nip44Decrypt(sender: NDKUser, value: string): Promise<string>;
decrypt(sender: NDKUser, value: string, scheme?: NDKEncryptionScheme): Promise<string>;
}
import debug from "debug";
import { NDK } from "../../ndk/index.js";
import type { NostrEvent } from "../../events/index.js";
import { Hexpubkey, NDKUser } from "../../user/index.js";
import { DEFAULT_ENCRYPTION_SCHEME, ENCRYPTION_SCHEMES, type NDKSigner } from "../index.js";
import { type NDKSigner } from "../index.js";
import { NDKRelay } from "../../relay/index.js";
import type { NDK } from "../../ndk/index.js";
import { EncryptionMethod } from "../../events/encryption.js";
import { NDKEncryptionScheme } from "../../types.js";
type Nip04QueueItem = {
type: "encrypt" | "decrypt";
type EncryptionQueueItem = {
scheme : NDKEncryptionScheme;
method: EncryptionMethod;
counterpartyHexpubkey: string;

@@ -30,4 +33,4 @@ value: string;

private _userPromise: Promise<NDKUser> | undefined;
public nip04Queue: Nip04QueueItem[] = [];
private nip04Processing = false;
public encryptionQueue: EncryptionQueueItem[] = [];
private encryptionProcessing = false;
private debug: debug.Debugger;

@@ -97,60 +100,28 @@ private waitTimeout: number;

public async encrypt(
recipient: NDKUser,
value: string,
type: ENCRYPTION_SCHEMES = DEFAULT_ENCRYPTION_SCHEME
): Promise<string> {
if (type === "nip44") {
return this.nip44Encrypt(recipient, value);
} else {
return this.nip04Encrypt(recipient, value);
}
public async encryptionEnabled(nip?:NDKEncryptionScheme): Promise<NDKEncryptionScheme[]>{
let enabled : NDKEncryptionScheme[] = []
if((!nip || nip == 'nip04') && Boolean((window as any).nostr!.nip04)) enabled.push('nip04')
if((!nip || nip == 'nip44') && Boolean((window as any).nostr!.nip44)) enabled.push('nip44')
return enabled;
}
public async decrypt(
sender: NDKUser,
value: string,
type: ENCRYPTION_SCHEMES = DEFAULT_ENCRYPTION_SCHEME
): Promise<string> {
if (type === "nip44") {
return this.nip44Decrypt(sender, value);
} else {
return this.nip04Decrypt(sender, value);
}
}
public async nip44Encrypt(recipient: NDKUser, value: string): Promise<string> {
public async encrypt(recipient: NDKUser, value: string, nip:NDKEncryptionScheme = 'nip04'): Promise<string> {
if( !(await this.encryptionEnabled(nip)) ) throw new Error(nip + 'encryption is not available from your browser extension')
await this.waitForExtension();
return await this.nip44.encrypt(recipient.pubkey, value);
}
get nip44(): Nip44 {
if (!window.nostr?.nip44) {
throw new Error("NIP-44 not supported by your browser extension");
}
return window.nostr.nip44;
}
public async nip44Decrypt(sender: NDKUser, value: string): Promise<string> {
await this.waitForExtension();
return await this.nip44.decrypt(sender.pubkey, value);
}
public async nip04Encrypt(recipient: NDKUser, value: string): Promise<string> {
await this.waitForExtension();
const recipientHexPubKey = recipient.pubkey;
return this.queueNip04("encrypt", recipientHexPubKey, value);
return this.queueEncryption(nip, "encrypt", recipientHexPubKey, value);
}
public async nip04Decrypt(sender: NDKUser, value: string): Promise<string> {
public async decrypt(sender: NDKUser, value: string, nip:NDKEncryptionScheme = 'nip04'): Promise<string> {
if( !(await this.encryptionEnabled(nip)) ) throw new Error(nip + 'encryption is not available from your browser extension')
await this.waitForExtension();
const senderHexPubKey = sender.pubkey;
return this.queueNip04("decrypt", senderHexPubKey, value);
return this.queueEncryption(nip, "decrypt", senderHexPubKey, value);
}
private async queueNip04(
type: "encrypt" | "decrypt",
private async queueEncryption(
scheme : NDKEncryptionScheme,
method: EncryptionMethod,
counterpartyHexpubkey: string,

@@ -160,4 +131,5 @@ value: string

return new Promise((resolve, reject) => {
this.nip04Queue.push({
type,
this.encryptionQueue.push({
scheme,
method,
counterpartyHexpubkey,

@@ -169,4 +141,4 @@ value,

if (!this.nip04Processing) {
this.processNip04Queue();
if (!this.encryptionProcessing) {
this.processEncryptionQueue();
}

@@ -176,21 +148,20 @@ });

private async processNip04Queue(item?: Nip04QueueItem, retries = 0): Promise<void> {
if (!item && this.nip04Queue.length === 0) {
this.nip04Processing = false;
private async processEncryptionQueue(item?: EncryptionQueueItem, retries = 0): Promise<void> {
if (!item && this.encryptionQueue.length === 0) {
this.encryptionProcessing = false;
return;
}
this.nip04Processing = true;
const { type, counterpartyHexpubkey, value, resolve, reject } =
item || this.nip04Queue.shift()!;
this.encryptionProcessing = true;
const {scheme, method, counterpartyHexpubkey, value, resolve, reject } =
item || this.encryptionQueue.shift()!;
this.debug("Processing encryption queue item", {
method,
counterpartyHexpubkey,
value,
});
try {
let result;
if (type === "encrypt") {
result = await window.nostr!.nip04!.encrypt(counterpartyHexpubkey, value);
} else {
result = await window.nostr!.nip04!.decrypt(counterpartyHexpubkey, value);
}
let result = await window.nostr![scheme]![method](counterpartyHexpubkey, value)
resolve(result);

@@ -203,3 +174,3 @@ // eslint-disable-next-line @typescript-eslint/no-explicit-any

this.debug("Retrying encryption queue item", {
type,
method,
counterpartyHexpubkey,

@@ -210,3 +181,3 @@ value,

setTimeout(() => {
this.processNip04Queue(item, retries + 1);
this.processEncryptionQueue(item, retries + 1);
}, 50 * retries);

@@ -220,3 +191,3 @@

this.processNip04Queue();
this.processEncryptionQueue();
}

@@ -223,0 +194,0 @@

@@ -12,2 +12,4 @@ import { EventEmitter } from "tseep";

import type { NDKSubscription } from "../../subscription/index.js";
import { NDKEncryptionScheme } from "../../types.js";
import { EncryptionMethod } from "../../events/encryption.js";

@@ -24,3 +26,3 @@ /**

* const ndk = new NDK()
* const nip05 = await prompt("enter your nip-05") // Get a NIP-05 the user wants to login with
* const nip05 = await prompt("enter your scheme-05") // Get a NIP-05 the user wants to login with
* const privateKey = localStorage.getItem("nip46-local-key") // If we have a private key previously saved, use it

@@ -216,30 +218,20 @@ * const signer = new NDKNip46Signer(ndk, nip05, privateKey) // Create a signer with (or without) a private key

public async encrypt(recipient: NDKUser, value: string): Promise<string> {
return this.nip04Encrypt(recipient, value);
public async encryptionEnabled(scheme?: NDKEncryptionScheme): Promise<NDKEncryptionScheme[]> {
if (scheme) return [scheme];
return Promise.resolve(["nip04", "nip44"]);
}
public async decrypt(sender: NDKUser, value: string): Promise<string> {
return this.nip04Decrypt(sender, value);
public async encrypt(recipient: NDKUser, value: string, scheme: NDKEncryptionScheme = 'nip04'): Promise<string> {
return this.encryption(recipient, value, scheme, "encrypt");
}
public async nip04Encrypt(recipient: NDKUser, value: string): Promise<string> {
return this._encrypt(recipient, value, "nip04");
public async decrypt(sender: NDKUser, value: string, scheme: NDKEncryptionScheme = 'nip04'): Promise<string> {
return this.encryption(sender, value, scheme, "decrypt");
}
public async nip04Decrypt(sender: NDKUser, value: string): Promise<string> {
return this._decrypt(sender, value, "nip04");
}
public async nip44Encrypt(recipient: NDKUser, value: string): Promise<string> {
return this._encrypt(recipient, value, "nip44");
}
public async nip44Decrypt(sender: NDKUser, value: string): Promise<string> {
return this._decrypt(sender, value, "nip44");
}
private async _encrypt(
recipient: NDKUser,
private async encryption(
peer: NDKUser,
value: string,
method: "nip04" | "nip44"
scheme: NDKEncryptionScheme,
method: EncryptionMethod
): Promise<string> {

@@ -251,4 +243,4 @@ const promise = new Promise<string>((resolve, reject) => {

this.bunkerPubkey,
method + "_encrypt",
[recipient.pubkey, value],
`${scheme}_${method}`,
[peer.pubkey, value],
24133,

@@ -268,28 +260,2 @@ (response: NDKRpcResponse) => {

private async _decrypt(
sender: NDKUser,
value: string,
method: "nip04" | "nip44"
): Promise<string> {
const promise = new Promise<string>((resolve, reject) => {
if (!this.bunkerPubkey) throw new Error("Bunker pubkey not set");
this.rpc.sendRequest(
this.bunkerPubkey,
method + "_decrypt",
[sender.pubkey, value],
24133,
(response: NDKRpcResponse) => {
if (!response.error) {
resolve(response.result);
} else {
reject(response.error);
}
}
);
});
return promise;
}
public async sign(event: NostrEvent): Promise<string> {

@@ -296,0 +262,0 @@ const promise = new Promise<string>((resolve, reject) => {

@@ -49,7 +49,9 @@ import { EventEmitter } from "tseep";

relayUrls,
Array.from(ndk.pool.blacklistRelayUrls),
[],
ndk,
debug.extend("rpc-pool")
{
debug: debug.extend("rpc-pool"),
name: "Nostr RPC"
}
);
this.pool.name = "nostr-rpc";

@@ -56,0 +58,0 @@ this.relaySet = new NDKRelaySet(new Set(), ndk, this.pool);

import { generateSecretKey } from "nostr-tools";
import type { NostrEvent } from "../../index.js";
import NDK, { NDKEvent, type NostrEvent } from "../../index.js";
import { NDKPrivateKeySigner } from "./index";

@@ -4,0 +4,0 @@ import { bytesToHex, hexToBytes } from "@noble/hashes/utils";

@@ -6,6 +6,8 @@ import type { UnsignedEvent } from "nostr-tools";

import { NDKUser } from "../../user";
import { DEFAULT_ENCRYPTION_SCHEME, ENCRYPTION_SCHEMES, type NDKSigner } from "../index.js";
import { type NDKSigner } from "../index.js";
import { bytesToHex, hexToBytes } from "@noble/hashes/utils";
import { nip19 } from "nostr-tools";
import { NDKEncryptionScheme } from "../../types.js";
export class NDKPrivateKeySigner implements NDKSigner {

@@ -41,3 +43,2 @@ private _user: NDKUser | undefined;

}
get privateKey(): string | undefined {

@@ -73,55 +74,11 @@ if (!this._privateKey) return undefined;

private getConversationKey(recipient: NDKUser): Uint8Array {
if (!this._privateKey) {
throw Error("Attempted to get conversation key without a private key");
}
const recipientHexPubKey = recipient.pubkey;
return nip44.getConversationKey(this._privateKey, recipientHexPubKey);
public async encryptionEnabled(scheme?: NDKEncryptionScheme): Promise<NDKEncryptionScheme[]>{
let enabled: NDKEncryptionScheme[] = []
if((!scheme || scheme == 'nip04')) enabled.push('nip04')
if((!scheme || scheme == 'nip44')) enabled.push('nip44')
return enabled;
}
public async nip44Encrypt(recipient: NDKUser, value: string): Promise<string> {
const conversationKey = this.getConversationKey(recipient);
return await nip44.encrypt(value, conversationKey);
}
public async nip44Decrypt(sender: NDKUser, value: string): Promise<string> {
const conversationKey = this.getConversationKey(sender);
return await nip44.decrypt(value, conversationKey);
}
/**
* This method is deprecated and will be removed in a future release, for compatibility
* this function calls nip04Encrypt.
*/
public async encrypt(
recipient: NDKUser,
value: string,
type: ENCRYPTION_SCHEMES = DEFAULT_ENCRYPTION_SCHEME
): Promise<string> {
if (type === "nip44") {
return this.nip44Encrypt(recipient, value);
} else {
return this.nip04Encrypt(recipient, value);
}
}
/**
* This method is deprecated and will be removed in a future release, for compatibility
* this function calls nip04Decrypt.
*/
public async decrypt(
sender: NDKUser,
value: string,
type: ENCRYPTION_SCHEMES = DEFAULT_ENCRYPTION_SCHEME
): Promise<string> {
if (type === "nip44") {
return this.nip44Decrypt(sender, value);
} else {
return this.nip04Decrypt(sender, value);
}
}
public async nip04Encrypt(recipient: NDKUser, value: string): Promise<string> {
if (!this._privateKey) {
public async encrypt(recipient: NDKUser, value: string, scheme?: NDKEncryptionScheme): Promise<string> {
if (!this._privateKey || !this.privateKey) {
throw Error("Attempted to encrypt without a private key");

@@ -131,7 +88,11 @@ }

const recipientHexPubKey = recipient.pubkey;
if(scheme == 'nip44') {
let conversationKey = nip44.v2.utils.getConversationKey(this._privateKey, recipientHexPubKey);
return await nip44.v2.encrypt(value, conversationKey);
}
return await nip04.encrypt(this._privateKey, recipientHexPubKey, value);
}
public async nip04Decrypt(sender: NDKUser, value: string): Promise<string> {
if (!this._privateKey) {
public async decrypt(sender: NDKUser, value: string, scheme?: NDKEncryptionScheme): Promise<string> {
if (!this._privateKey || !this.privateKey) {
throw Error("Attempted to decrypt without a private key");

@@ -141,4 +102,8 @@ }

const senderHexPubKey = sender.pubkey;
if(scheme == 'nip44') {
let conversationKey = nip44.v2.utils.getConversationKey(this._privateKey, senderHexPubKey);
return await nip44.v2.decrypt(value, conversationKey);
}
return await nip04.decrypt(this._privateKey, senderHexPubKey, value);
}
}

@@ -52,2 +52,7 @@ import { EventEmitter } from "tseep";

/**
* Whether to skip caching events coming from this subscription
**/
dontSaveToCache?: boolean;
/**
* Groupable subscriptions are created with a slight time

@@ -98,3 +103,3 @@ * delayed to allow similar filters to be grouped together.

* Skip event validation. Event validation, checks whether received
* kinds conform to what the expected schema of that kind should look like.
* kinds conform to what the expected schema of that kind should look like.rtwle
* @default false

@@ -109,2 +114,16 @@ */

skipOptimisticPublishEvent?: boolean;
/**
* Remove filter constraints when querying the cache.
*
* This allows setting more aggressive filters that will be removed when hitting the cache.
*
* Useful uses of this include removing `since` or `until` constraints or `limit` filters.
*
* @example
* ndk.subscribe({ kinds: [1], since: 1710000000, limit: 10 }, { cacheUnconstrainFilter: ['since', 'limit'] });
*
* This will hit relays with the since and limit constraints, while loading from the cache without them.
*/
cacheUnconstrainFilter?: (keyof NDKFilter)[];
}

@@ -118,5 +137,7 @@

cacheUsage: NDKSubscriptionCacheUsage.CACHE_FIRST,
dontSaveToCache: false,
groupable: true,
groupableDelay: 100,
groupableDelayType: "at-most",
cacheUnconstrainFilter: ['limit', 'since', 'until']
};

@@ -139,2 +160,4 @@

* * {NDKSubscription} subscription - The subscription that received the event.
*
* @emits cacheEose - Emitted when the cache adapter has reached the end of the events it had.
*

@@ -167,11 +190,31 @@ * @emits eose - Emitted when all relays have reached the end of the event stream.

export class NDKSubscription extends EventEmitter<{
cacheEose: () => void;
eose: (sub: NDKSubscription) => void;
close: (sub: NDKSubscription) => void;
/**
* Emitted when a duplicate event is received by the subscription.
* @param event - The duplicate event received by the subscription.
* @param relay - The relay that received the event.
* @param timeSinceFirstSeen - The time elapsed since the first time the event was seen.
* @param sub - The subscription that received the event.
*/
"event:dup": (
eventId: NDKEventId,
event: NDKEvent | NostrEvent,
relay: NDKRelay | undefined,
timeSinceFirstSeen: number,
sub: NDKSubscription
sub: NDKSubscription,
fromCache: boolean,
optimisticPublish: boolean
) => void;
event: (event: NDKEvent, relay: NDKRelay | undefined, sub: NDKSubscription) => void;
/**
* Emitted when an event is received by the subscription.
* @param event - The event received by the subscription.
* @param relay - The relay that received the event.
* @param sub - The subscription that received the event.
* @param fromCache - Whether the event was received from the cache.
* @param optimisticPublish - Whether the event was received from an optimistic publish.
*/
event: (event: NDKEvent, relay: NDKRelay | undefined, sub: NDKSubscription, fromCache: boolean, optimisticPublish: boolean) => void;

@@ -231,2 +274,7 @@ /**

/**
* Filters to remove when querying the cache.
*/
public cacheUnconstrainFilter?: Array<(keyof NDKFilter)>;
public constructor(

@@ -244,19 +292,11 @@ ndk: NDK,

this.filters = filters instanceof Array ? filters : [filters];
this.subId = subId || opts?.subId;
this.subId = subId || this.opts.subId;
this.internalId = Math.random().toString(36).substring(7);
this.relaySet = relaySet;
this.debug = ndk.debug.extend(`subscription[${opts?.subId ?? this.internalId}]`);
this.skipVerification = opts?.skipVerification || false;
this.skipValidation = opts?.skipValidation || false;
this.closeOnEose = opts?.closeOnEose || false;
this.skipOptimisticPublishEvent = opts?.skipOptimisticPublishEvent || false;
// validate that the caller is not expecting a persistent
// subscription while using an option that will only hit the cache
if (
this.opts.cacheUsage === NDKSubscriptionCacheUsage.ONLY_CACHE &&
!this.opts.closeOnEose
) {
throw new Error("Cannot use cache-only options with a persistent subscription");
}
this.debug = ndk.debug.extend(`subscription[${this.opts.subId ?? this.internalId}]`);
this.skipVerification = this.opts.skipVerification || false;
this.skipValidation = this.opts.skipValidation || false;
this.closeOnEose = this.opts.closeOnEose || false;
this.skipOptimisticPublishEvent = this.opts.skipOptimisticPublishEvent || false;
this.cacheUnconstrainFilter = this.opts.cacheUnconstrainFilter;
}

@@ -329,2 +369,3 @@

cachePromise = this.startWithCache();
cachePromise.then(() => this.emit('cacheEose'))

@@ -482,3 +523,3 @@ if (this.shouldWaitForCache()) {

if (this.ndk.cacheAdapter) {
if (this.ndk.cacheAdapter && !this.opts.dontSaveToCache) {
this.ndk.cacheAdapter.setEvent(ndkEvent, this.filters, relay);

@@ -494,3 +535,3 @@ }

if (!optimisticPublish || this.skipOptimisticPublishEvent !== true) {
this.emit("event", ndkEvent, relay, this);
this.emit("event", ndkEvent, relay, this, fromCache, optimisticPublish);
// mark the eventId as seen

@@ -501,3 +542,3 @@ this.eventFirstSeen.set(eventId, Date.now());

const timeSinceFirstSeen = Date.now() - (this.eventFirstSeen.get(eventId) || 0);
this.emit("event:dup", eventId, relay, timeSinceFirstSeen, this);
this.emit("event:dup", event, relay, timeSinceFirstSeen, this, fromCache, optimisticPublish);

@@ -504,0 +545,0 @@ if (relay) {

@@ -93,5 +93,5 @@ import { matchFilters, type VerifiedEvent } from "nostr-tools";

for (const sub of matchingSubs) {
sub.eventReceived(event, undefined, false, optimisticPublish);
sub.eventReceived(event, relay, false, optimisticPublish);
}
}
}

@@ -6,8 +6,15 @@ import {

eventsBySameAuthor,
getEventReplyId,
getReplyTag,
getRootEventId,
getRootTag,
} from ".";
import type { NDKEventId } from "../events";
import type { NDKEventId, NostrEvent } from "../events";
import { NDKEvent } from "../events";
import { NDK } from "../ndk";
import { NDKPrivateKeySigner } from "../signers/private-key";
const ndk = new NDK();
ndk.signer = NDKPrivateKeySigner.generate();
const op = new NDKEvent(undefined, {

@@ -268,2 +275,42 @@ id: "op",

});
describe("supports nip-22 replies", () => {
let op: NDKEvent;
let reply1: NDKEvent;
let reply2: NDKEvent;
beforeAll(async () => {
op = new NDKEvent(ndk, {
kind: 9999,
content: "This is the root post"
} as NostrEvent)
await op.sign();
reply1 = op.reply();
reply1.content = "this is the reply";
await reply1.sign();
reply2 = reply1.reply();
reply2.content = "this is the reply to the reply";
await reply2.sign();
})
it("finds the root event of the first-level reply", () => {
const rootEventId = getRootEventId(reply1);
expect(rootEventId).toBe(op.id);
})
it("finds the reply event of the first-level reply", () => {
const replyEventId = getEventReplyId(reply1);
expect(replyEventId).toBe(op.id);
})
it("finds the root event of the second-level reply", () => {
const rootEventId = getRootEventId(reply2);
expect(rootEventId).toBe(op.id);
})
it("finds the reply event of the second-level reply", () => {
const replyEventId = getEventReplyId(reply2);
expect(replyEventId).toBe(reply1.id);
})
})
});
import type { NDKEvent, NDKEventId, NDKTag } from "../events";
import { NDKKind } from "../events/kinds";

@@ -106,26 +107,17 @@ export function eventsBySameAuthor(op: NDKEvent, events: NDKEvent[]) {

export function getEventReplyIds(event: NDKEvent): NDKEventId[] {
if (hasMarkers(event, event.tagType())) {
let rootTag: NDKTag | undefined;
const replyTags: NDKTag[] = [];
/**
* Returns the reply ID of an event.
* @param event The event to get the reply ID from
* @returns The reply ID or undefined if the event is not a reply
*/
export function getEventReplyId(event: NDKEvent): NDKEventId | undefined {
const replyTag = getReplyTag(event);
if (replyTag) return replyTag[1];
event.getMatchingTags(event.tagType()).forEach((tag) => {
if (tag[3] === "root") rootTag = tag;
if (tag[3] === "reply") replyTags.push(tag);
});
if (replyTags.length === 0) {
if (rootTag) {
replyTags.push(rootTag);
}
}
return replyTags.map((tag) => tag[1]);
} else {
return event.getMatchingTags("e").map((tag) => tag[1]);
}
const rootTag = getRootTag(event);
if (rootTag) return rootTag[1];
}
export function isEventOriginalPost(event: NDKEvent): boolean {
return getEventReplyIds(event).length === 0;
return getEventReplyId(event) === undefined;
}

@@ -173,3 +165,7 @@

export function eventHasETagMarkers(event: NDKEvent): boolean {
return event.getMatchingTags("e").some((tag) => tag[3]);
for (const tag of event.tags) {
if (tag[0] === "e" && (tag[3] ?? "").length > 0) return true;
}
return false;
}

@@ -201,6 +197,6 @@

searchTag ??= event.tagType();
const rootEventTag = event.tags.find((tag) => tag[3] === "root");
const rootEventTag = event.tags.find(isTagRootTag);
if (!rootEventTag) {
// If we don't have an explicit root marer, this event has no other e-tag markers
// If we don't have an explicit root marker, this event has no other e-tag markers
// and we have a single e-tag, return that value

@@ -216,22 +212,42 @@ if (eventHasETagMarkers(event)) return;

const nip22RootTags = new Set(["A", "E", "I"])
const nip22ReplyTags = new Set(["a", "e", "i"])
export function getReplyTag(event: NDKEvent, searchTag?: string): NDKTag | undefined {
if (event.kind === NDKKind.GenericReply) {
let replyTag: NDKTag | undefined;
for (const tag of event.tags) {
// we look for an "e", "a", or "i" tag and immediately return it if we find it;
// if we don't find an "e", "a", or "i" tag, we return the "E", "A", or "I" tag
if (nip22RootTags.has(tag[0])) replyTag = tag;
else if (nip22ReplyTags.has(tag[0])) {
replyTag = tag;
break;
}
}
return replyTag;
}
searchTag ??= event.tagType();
let replyTag = event.tags.find((tag) => tag[3] === "reply");
let hasMarkers = false;
let replyTag: NDKTag | undefined;
if (replyTag) return replyTag;
for (const tag of event.tags) {
if (tag[0] !== searchTag) continue;
if ((tag[3] ?? "").length > 0) hasMarkers = true;
// Check with root tag
if (!replyTag) replyTag = event.tags.find((tag) => tag[3] === "root");
// If we don't have anything
if (!replyTag) {
// if we do have etag markers and no reply tag, then return undefined
if (eventHasETagMarkers(event)) return;
// If we only have one tag of the requested type
const matchingTags = event.getMatchingTags(searchTag);
if (matchingTags.length === 1) return matchingTags[0];
if (matchingTags.length === 2) return matchingTags[1];
if (hasMarkers && tag[3] === "reply") return tag;
if (hasMarkers && tag[3] === "root") replyTag = tag;
if (!hasMarkers) replyTag = tag;
}
return replyTag;
}
function isTagRootTag(tag: NDKTag): boolean {
return (
tag[0] === "E" ||
tag[3] === "root"
);
}

@@ -11,5 +11,4 @@ import { nip19 } from "nostr-tools";

import type {
NDKFilter,
NDKRelay,
NDKSigner,
NDKZapDetails,
NDKZapMethod,

@@ -19,4 +18,2 @@ NDKZapMethodInfo,

import { NDKCashuMintList } from "../events/kinds/nutzap/mint-list.js";
import type { LNPaymentRequest, NDKLnUrlData } from "../zapper/ln.js";
import { getNip57ZapSpecFromLud, LnPaymentInfo } from "../zapper/ln.js";

@@ -57,2 +54,3 @@ export type Hexpubkey = string;

public profile?: NDKUserProfile;
public profileEvent?: NDKEvent;
private _npub?: Npub;

@@ -83,5 +81,6 @@ private _pubkey?: Hexpubkey;

get nprofile(): string {
console.log('encoding with pubkey', this.pubkey)
const relays = this.profileEvent?.onRelays?.map((r) => r.url);
return nip19.nprofileEncode({
pubkey: this.pubkey
pubkey: this.pubkey,
relays
});

@@ -95,21 +94,2 @@ }

/**
* Get the user's hexpubkey
* @returns {Hexpubkey} The user's hexpubkey
*
* @deprecated Use `pubkey` instead
*/
get hexpubkey(): Hexpubkey {
return this.pubkey;
}
/**
* Set the user's hexpubkey
* @param pubkey {Hexpubkey} The user's hexpubkey
* @deprecated Use `pubkey` instead
*/
set hexpubkey(pubkey: Hexpubkey) {
this._pubkey = pubkey;
}
/**
* Get the user's pubkey

@@ -136,2 +116,10 @@ * @returns {string} The user's pubkey

/**
* Equivalent to NDKEvent.filters().
* @returns {NDKFilter}
*/
public filter(): NDKFilter {
return {"#p": [this.pubkey]}
}
/**
* Gets NIP-57 and NIP-61 information that this user has signaled

@@ -141,67 +129,39 @@ *

*/
async getZapInfo(
getAll = true,
methods: NDKZapMethod[] = ["nip61", "nip57"]
): Promise<NDKZapMethodInfo[]> {
async getZapInfo(timeoutMs?: number): Promise<Map<NDKZapMethod, NDKZapMethodInfo>> {
if (!this.ndk) throw new Error("No NDK instance found");
const kinds: NDKKind[] = [];
if (methods.includes("nip61")) kinds.push(NDKKind.CashuMintList);
if (methods.includes("nip57")) kinds.push(NDKKind.Metadata);
if (kinds.length === 0) return [];
let events = await this.ndk.fetchEvents(
{ kinds, authors: [this.pubkey] },
{
cacheUsage: NDKSubscriptionCacheUsage.ONLY_CACHE,
groupable: false,
const promiseWithTimeout = async <T>(promise: Promise<T>): Promise<T | undefined> => {
if (!timeoutMs) return promise;
try {
return await Promise.race([
promise,
new Promise<T>((_, reject) => setTimeout(() => reject(), timeoutMs))
]);
} catch {
return undefined;
}
);
};
if (events.size < methods.length) {
events = await this.ndk.fetchEvents(
{ kinds, authors: [this.pubkey] },
{
cacheUsage: NDKSubscriptionCacheUsage.ONLY_RELAY,
}
);
}
const [ userProfile, mintListEvent ] = await Promise.all([
promiseWithTimeout(this.fetchProfile()),
promiseWithTimeout(this.ndk.fetchEvent({ kinds: [NDKKind.CashuMintList], authors: [this.pubkey] }))
]);
const res: Map<NDKZapMethod, NDKZapMethodInfo> = new Map();
const res: NDKZapMethodInfo[] = [];
if (mintListEvent) {
const mintList = NDKCashuMintList.from(mintListEvent);
const nip61 = Array.from(events).find((e) => e.kind === NDKKind.CashuMintList);
const nip57 = Array.from(events).find((e) => e.kind === NDKKind.Metadata);
if (nip61) {
const mintList = NDKCashuMintList.from(nip61);
if (mintList.mints.length > 0) {
res.push({
type: "nip61",
data: {
mints: mintList.mints,
relays: mintList.relays,
p2pk: mintList.p2pk,
},
res.set("nip61", {
mints: mintList.mints,
relays: mintList.relays,
p2pk: mintList.p2pk,
});
}
// if we are just getting one and already have one, go back
if (!getAll) return res;
}
if (nip57) {
const profile = profileFromEvent(nip57);
const { lud06, lud16 } = profile;
try {
const zapSpec = await getNip57ZapSpecFromLud({ lud06, lud16 }, this.ndk);
if (zapSpec) {
res.push({ type: "nip57", data: zapSpec });
}
} catch (e) {
console.error("Error getting NIP-57 zap spec", e);
}
if (userProfile) {
const { lud06, lud16 } = userProfile;
res.set("nip57", { lud06, lud16 });
}

@@ -213,65 +173,2 @@

/**
* Determines whether this user
* has signaled support for NIP-60 zaps
**/
// export type UserZapConfiguration = {
// }
// async getRecipientZapConfig(): Promise<> {
// }
/**
* Retrieves the zapper this pubkey has designated as an issuer of zap receipts
*/
async getZapConfiguration(ndk?: NDK): Promise<NDKLnUrlData | undefined> {
ndk ??= this.ndk;
if (!ndk) throw new Error("No NDK instance found");
const process = async (): Promise<NDKLnUrlData | undefined> => {
if (this.ndk?.cacheAdapter?.loadUsersLNURLDoc) {
const doc = await this.ndk.cacheAdapter.loadUsersLNURLDoc(this.pubkey);
if (doc !== "missing") {
if (doc === null) return;
if (doc) return doc;
}
}
let lnurlspec: NDKLnUrlData | undefined;
try {
await this.fetchProfile({ groupable: false });
if (this.profile) {
const { lud06, lud16 } = this.profile;
lnurlspec = await getNip57ZapSpecFromLud({ lud06, lud16 }, ndk!);
}
} catch {}
if (this.ndk?.cacheAdapter?.saveUsersLNURLDoc) {
this.ndk.cacheAdapter.saveUsersLNURLDoc(this.pubkey, lnurlspec || null);
}
if (!lnurlspec) return;
return lnurlspec;
};
return await ndk.queuesZapConfig.add({
id: this.pubkey,
func: process,
});
}
/**
* Fetches the zapper's pubkey for the zapped user
* @returns The zapper's pubkey if one can be found
*/
async getZapperPubkey(): Promise<Hexpubkey | undefined> {
const zapConfig = await this.getZapConfiguration();
return zapConfig?.nostrPubkey;
}
/**
* Instantiate an NDKUser from a NIP-05 string

@@ -320,3 +217,3 @@ * @param nip05Id {string} The user's NIP-05

let setMetadataEvents: Set<NDKEvent> | null = null;
let setMetadataEvent: NDKEvent | null = null;

@@ -345,3 +242,3 @@ if (

) {
setMetadataEvents = await this.ndk.fetchEvents(
setMetadataEvent = await this.ndk.fetchEvent(
{

@@ -366,4 +263,4 @@ kinds: [0],

if (!setMetadataEvents || setMetadataEvents.size === 0) {
setMetadataEvents = await this.ndk.fetchEvents(
if (!setMetadataEvent) {
setMetadataEvent = await this.ndk.fetchEvent(
{

@@ -377,15 +274,10 @@ kinds: [0],

const sortedSetMetadataEvents = Array.from(setMetadataEvents).sort(
(a, b) => (a.created_at as number) - (b.created_at as number)
);
if (!setMetadataEvent) return null;
if (sortedSetMetadataEvents.length === 0) return null;
// return the most recent profile
const event = sortedSetMetadataEvents[0];
this.profile = profileFromEvent(event);
this.profile = profileFromEvent(setMetadataEvent);
if (storeProfileEvent) {
// Store the event as a stringified JSON
this.profile.profileEvent = JSON.stringify(event);
this.profile.profileEvent = JSON.stringify(setMetadataEvent);
}

@@ -392,0 +284,0 @@

@@ -24,3 +24,4 @@ import type { NDKEvent } from "../events/index.js";

ndk: NDK,
skipCache = false
skipCache = false,
timeout = 1000
): Promise<Map<Hexpubkey, NDKRelayList>> {

@@ -40,4 +41,4 @@ const pool = ndk.outboxPool || ndk.pool;

const cachedList = await ndk.fetchEvents(
{ kinds: [3, 10002], authors: pubkeys },
{ cacheUsage: NDKSubscriptionCacheUsage.ONLY_CACHE }
{ kinds: [3, 10002], authors: Array.from(new Set(pubkeys)) },
{ cacheUsage: NDKSubscriptionCacheUsage.ONLY_CACHE, subId: 'ndk-relay-list-fetch' }
);

@@ -119,4 +120,8 @@

setTimeout(() => {
resolve(relayLists)
}, timeout);
sub.start();
});
}

@@ -21,3 +21,3 @@ /* eslint-disable @typescript-eslint/no-explicit-any */

export function normalizeRelayUrl(url: string): string {
let r = normalizeUrl(url.toLowerCase(), {
let r = normalizeUrl(url, {
stripAuthentication: false,

@@ -183,2 +183,5 @@ stripWWW: false,

// Lowercase only the hostname
urlObject.hostname = urlObject.hostname.toLowerCase();
if (options.forceHttp && options.forceHttps) {

@@ -185,0 +188,0 @@ throw new Error("The `forceHttp` and `forceHttps` options cannot be used together");

@@ -13,7 +13,8 @@ import type { NDK } from "../ndk";

import { NDKNutzap } from "../events/kinds/nutzap";
import type {
LnPaymentInfo,
NDKLnUrlData,
NDKPaymentConfirmationLN,
NDKZapConfirmationLN,
import {
getNip57ZapSpecFromLud,
type LnPaymentInfo,
type NDKLnUrlData,
type NDKPaymentConfirmationLN,
type NDKZapConfirmationLN,
} from "./ln";

@@ -64,2 +65,17 @@ import type {

paymentDescription?: string;
/**
* If this payment is for a nip57 zap, this will contain the zap request.
*/
nip57ZapRequest?: NDKEvent;
/**
* When set to true, when a pubkey is not zappable, we will
* automatically fallback to using NIP-61.
*
* Every pubkey must be able to receive money.
*
* @default false
*/
nutzapAsFallback?: boolean;
};

@@ -77,14 +93,6 @@

export type NDKZapMethod = "nip57" | "nip61";
export type NDKLnLudData = { lud06?: string; lud16?: string; };
type ZapMethodInfo = {
nip57: NDKLnUrlData;
nip61: CashuPaymentInfo;
};
export type NDKZapMethodInfo = NDKLnLudData | CashuPaymentInfo;
export type NDKZapMethodInfo = {
type: NDKZapMethod;
data: ZapMethodInfo[NDKZapMethod];
};
export type LnPayCb = (

@@ -94,3 +102,4 @@ payment: NDKZapDetails<LnPaymentInfo>

export type CashuPayCb = (
payment: NDKZapDetails<CashuPaymentInfo>
payment: NDKZapDetails<CashuPaymentInfo>,
onLnInvoice?: (pr: string) => void
) => Promise<NDKPaymentConfirmationCashu | undefined>;

@@ -115,2 +124,3 @@ export type OnCompleteCb = (

onComplete?: OnCompleteCb;
nutzapAsFallback?: boolean;
ndk?: NDK;

@@ -124,2 +134,26 @@ }

/**
* An LN invoice has been fetched.
* @param param0
* @returns
*/
ln_invoice: ({ amount, recipientPubkey, unit, nip57ZapRequest, pr, type }: {
amount: number;
recipientPubkey: string;
unit: string;
nip57ZapRequest?: NDKEvent;
pr: string;
type: NDKZapMethod;
}) => void;
ln_payment: ({ preimage, amount, recipientPubkey, unit, nip57ZapRequest, pr }: {
preimage: string;
amount: number;
recipientPubkey: string;
pr: string;
unit: string;
nip57ZapRequest?: NDKEvent;
type: NDKZapMethod
}) => void;
/**
* Emitted when a zap split has been completed

@@ -133,2 +167,4 @@ */

complete: (results: Map<NDKZapSplit, NDKPaymentConfirmation | Error | undefined>) => void;
notice: (message: string) => void;
}> {

@@ -143,2 +179,3 @@ public target: NDKEvent | NDKUser;

public zapMethod?: NDKZapMethod;
public nutzapAsFallback?: boolean;

@@ -182,2 +219,3 @@ public lnPay?: LnPayCb;

this.signer = opts.signer;
this.nutzapAsFallback = opts.nutzapAsFallback ?? false;

@@ -206,3 +244,3 @@ this.lnPay = opts.lnPay || this.ndk.walletConfig?.lnPay;

} catch (e: any) {
result = e;
result = new Error(e.message);
}

@@ -221,4 +259,7 @@

private async zapNip57(split: NDKZapSplit, data: NDKLnUrlData): Promise<NDKPaymentConfirmation | undefined> {
private async zapNip57(split: NDKZapSplit, data: NDKLnLudData): Promise<NDKPaymentConfirmation | undefined> {
if (!this.lnPay) throw new Error("No lnPay function available");
const zapSpec = await getNip57ZapSpecFromLud(data, this.ndk);
if (!zapSpec) throw new Error("No zap spec available for recipient");

@@ -229,3 +270,3 @@ const relays = await this.relays(split.pubkey);

this.ndk,
data,
zapSpec,
split.pubkey,

@@ -244,3 +285,3 @@ split.amount,

const pr = await this.getLnInvoice(zapRequest, split.amount, data);
const pr = await this.getLnInvoice(zapRequest, split.amount, zapSpec);

@@ -252,3 +293,12 @@ if (!pr) {

return await this.lnPay(
this.emit("ln_invoice", {
amount: split.amount,
recipientPubkey: split.pubkey,
unit: this.unit,
nip57ZapRequest: zapRequest,
pr,
type: "nip57"
});
const res = await this.lnPay(
{

@@ -261,4 +311,19 @@ target: this.target,

unit: this.unit,
nip57ZapRequest: zapRequest,
}
);
if (res?.preimage) {
this.emit("ln_payment", {
preimage: res.preimage,
amount: split.amount,
recipientPubkey: split.pubkey,
pr,
unit: this.unit,
nip57ZapRequest: zapRequest,
type: "nip57"
});
}
return res;
}

@@ -272,3 +337,3 @@

*/
async zapNip61(split: NDKZapSplit, data: CashuPaymentInfo): Promise<NDKNutzap | Error | undefined> {
async zapNip61(split: NDKZapSplit, data?: CashuPaymentInfo): Promise<NDKNutzap | Error | undefined> {
if (!this.cashuPay) throw new Error("No cashuPay function available");

@@ -283,3 +348,11 @@

unit: this.unit,
...data,
...(data ?? {}),
}, (pr: string) => {
this.emit("ln_invoice", {
pr,
amount: split.amount,
recipientPubkey: split.pubkey,
unit: this.unit,
type: "nip61"
});
});

@@ -316,3 +389,2 @@

// mark that we have zapped
return nutzap;

@@ -329,53 +401,59 @@ }

async zapSplit(split: NDKZapSplit): Promise<NDKPaymentConfirmation | undefined> {
const zapped = false;
let zapMethods = await this.getZapMethods(this.ndk, split.pubkey);
const recipient = this.ndk.getUser({ pubkey: split.pubkey });
let zapMethods = await recipient.getZapInfo(2500);
let retVal: NDKPaymentConfirmation | Error | undefined;
if (zapMethods.length === 0) throw new Error("No zap method available for recipient");
const canFallbackToNip61 = this.nutzapAsFallback && this.cashuPay;
// prefer nip61 if available
zapMethods = zapMethods.sort((a, b) => {
if (a.type === "nip61") return -1;
if (b.type === "nip61") return 1;
return 0;
});
if (zapMethods.size === 0 && !canFallbackToNip61) throw new Error("No zap method available for recipient and NIP-61 fallback is disabled");
for (const zapMethod of zapMethods) {
if (zapped) break;
const nip61Fallback = async () => {
if (!this.nutzapAsFallback) return;
d(
"Zapping to %s with %d %s using %s",
split.pubkey,
split.amount,
this.unit,
zapMethod.type
);
const relayLists = await getRelayListForUsers([split.pubkey], this.ndk);
let relayUrls = relayLists.get(split.pubkey)?.readRelayUrls;
relayUrls = this.ndk.pool.connectedRelays().map((r) => r.url);
return await this.zapNip61(split, {
// use the user's relay list
relays: relayUrls,
// lock to the user's actual pubkey
p2pk: split.pubkey,
// allow intramint fallback
allowIntramintFallback: !!canFallbackToNip61,
});
}
const nip61Method = zapMethods.get("nip61") as CashuPaymentInfo;
if (nip61Method) {
try {
if (zapMethod.type === "nip61") {
retVal = await this.zapNip61(split, zapMethod.data as CashuPaymentInfo);
} else if (zapMethod.type === "nip57") {
retVal = await this.zapNip57(split, zapMethod.data as NDKLnUrlData);
}
retVal = await this.zapNip61(split, nip61Method);
if (retVal instanceof NDKNutzap) return retVal;
} catch (e: any) {
this.emit("notice", `NIP-61 attempt failed: ${e.message}`);
}
}
if (!(retVal instanceof Error)) {
// d("looks like zap with method", zapMethod.type, "worked", retVal);
break;
}
const nip57Method = zapMethods.get("nip57") as NDKLnLudData;
if (nip57Method) {
try {
retVal = await this.zapNip57(split, nip57Method);
if (!(retVal instanceof Error)) return retVal;
} catch (e: any) {
if (e instanceof Error) retVal = e;
else retVal = new Error(e);
d(
"Error zapping to %s with %d %s using %s: %o",
split.pubkey,
split.amount,
this.unit,
zapMethod.type,
e
);
this.emit("notice", `NIP-57 attempt failed: ${e.message}`);
}
}
if (canFallbackToNip61) {
retVal = await nip61Fallback();
if (retVal instanceof Error) throw retVal;
return retVal;
} else {
this.emit("notice", "Zap methods exhausted and there was no fallback to NIP-61");
}
if (retVal instanceof Error) throw retVal;
return retVal;

@@ -463,17 +541,9 @@ }

*/
async getZapMethods(ndk: NDK, recipient: Hexpubkey): Promise<NDKZapMethodInfo[]> {
const methods: NDKZapMethod[] = [];
if (this.cashuPay) methods.push("nip61");
if (this.lnPay) methods.push("nip57");
// if there are no methods available, return an empty array
if (methods.length === 0) throw new Error("There are no payment methods available! Please set at least one of lnPay or cashuPay");
async getZapMethods(
ndk: NDK,
recipient: Hexpubkey,
timeout = 2500
): Promise<Map<NDKZapMethod, NDKZapMethodInfo>> {
const user = ndk.getUser({ pubkey: recipient });
const zapInfo = await user.getZapInfo(false, methods);
d("Zap info for %s: %o", user.npub, zapInfo);
return zapInfo;
return await user.getZapInfo(timeout)
}

@@ -480,0 +550,0 @@

@@ -14,3 +14,3 @@ import type { NDKNutzap } from "../events/kinds/nutzap";

*/
mints: string[];
mints?: string[];

@@ -20,3 +20,3 @@ /**

*/
relays: string[];
relays?: string[];

@@ -27,2 +27,10 @@ /**

p2pk?: string;
/**
* Intramint fallback allowed:
*
* When set to true, if cross-mint payments fail, we will
* fallback to sending an intra-mint payment.
*/
allowIntramintFallback?: boolean;
};

@@ -29,0 +37,0 @@

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc