diff --git a/.changeset/big-rivers-deliver.md b/.changeset/big-rivers-deliver.md deleted file mode 100644 index c0ce4178..00000000 --- a/.changeset/big-rivers-deliver.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@nostr-dev-kit/ndk-svelte": patch ---- - -expose a way to peak into events as they come diff --git a/.changeset/clever-bears-knock.md b/.changeset/clever-bears-knock.md deleted file mode 100644 index 71cdcdc4..00000000 --- a/.changeset/clever-bears-knock.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@nostr-dev-kit/ndk": patch ---- - -fix: close subscription on EOSE at the relay level diff --git a/.changeset/curly-dolphins-speak.md b/.changeset/curly-dolphins-speak.md deleted file mode 100644 index b3562ce1..00000000 --- a/.changeset/curly-dolphins-speak.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@nostr-dev-kit/ndk": patch ---- - -fix bug where queued items were not getting processed (e.g. zap fetches) diff --git a/.changeset/kind-news-sin.md b/.changeset/kind-news-sin.md deleted file mode 100644 index 62bdb49e..00000000 --- a/.changeset/kind-news-sin.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@nostr-dev-kit/ndk": patch ---- - -Breaking change: event.zap is now removed, use ndk.zap(event) instead diff --git a/.changeset/old-cats-attack.md b/.changeset/old-cats-attack.md deleted file mode 100644 index 7e0ba2d7..00000000 --- a/.changeset/old-cats-attack.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@nostr-dev-kit/ndk-cache-dexie": patch ---- - -add tests diff --git a/.changeset/three-hats-drop.md b/.changeset/three-hats-drop.md deleted file mode 100644 index 083f4a8f..00000000 --- a/.changeset/three-hats-drop.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@nostr-dev-kit/ndk": patch -"@nostr-dev-kit/ndk-cache-dexie": patch ---- - -add methods to access and manage unpublished events from the cache diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index cc764801..2001b48b 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -1,7 +1,8 @@ import { defineConfig } from 'vitepress' +import { withMermaid } from "vitepress-plugin-mermaid"; // https://vitepress.dev/reference/site-config -export default defineConfig({ +export default withMermaid(defineConfig({ title: "NDK", description: "NDK Docs", base: "/ndk/", @@ -29,6 +30,7 @@ export default defineConfig({ { text: 'Local-first', link: '/tutorial/local-first' }, { text: 'Publishing', link: '/tutorial/publishing' }, { text: "Subscription Management", link: '/tutorial/subscription-management' }, + { text: "Speed", link: '/tutorial/speed' }, ] }, { @@ -44,10 +46,16 @@ export default defineConfig({ { text: 'NDK Svelte', link: '/wrappers/svelte' }, ] }, + { + text: "Internals", + items: [ + { text: "Subscription Lifecycle", link: '/internals/subscriptions' }, + ] + } ], socialLinks: [ { icon: 'github', link: 'https://github.com/nostr-dev-kit/ndk' } ] } -}) +})) \ No newline at end of file diff --git a/docs/internals/subscriptions.md b/docs/internals/subscriptions.md new file mode 100644 index 00000000..ab328ce8 --- /dev/null +++ b/docs/internals/subscriptions.md @@ -0,0 +1,205 @@ +# Subscriptions Lifecycle +When an application creates a subscription a lot of things happen under the hood. + +Say we want to see `kind:1` events from pubkeys `123`, `456`, and `678`. + +```ts +const subscription = ndk.subscribe({ kinds: [1], authors: [ "123", "456", "678" ]}) +``` + +Since the application level didn't explicitly provide a relay-set, which is the most common use case, NDK will calculate a relay set based on the outbox model plus a variety of some other factors. + +So the first thing we'll do before talking to relays is, decide to *which* relays we should talk to. + +The `calculateRelaySetsFromFilters` function will take care of this and provide us with a map of relay URLs and filters for each relay. + +This means that the query, as specified by the client might be broken into distinct queries specialized for the different relays. + +For example, if we have 3 relays, and the query is for `kind:1` events from pubkeys `a` and `b`, the `calculateRelaySetsFromFilters` function might return something like this: + +```ts +{ + "wss://relay1": { kinds: [1], authors: [ "a" ] }, + "wss://relay2": { kinds: [1], authors: [ "b" ] }, +} +``` + +```mermaid +flowchart TD + Client -->|"kinds: [1], authors: [a, b]"| Subscription1 + Subscription1 -->|"kinds: [1], authors: [a]"| wss://relay1 + Subscription1 -->|"kinds: [1], authors: [b]"| wss://relay2 +``` + +## Subscription bundling +Once the subscription has been split into the filters each relay should receive, the filters are sent to the individual `NDKRelay`'s `NDKRelaySubscriptionManager` instances. + +`NDKRelaySubscriptionManager` is responsible for keeping track of the active and scheduled subscriptions that are pending to be executed within an individual relay. + +This is an important aspect to consider: + +> `NDKSubscription` have a different lifecycle than `NDKRelaySubscription`. For example, a subscription that is set to close after EOSE might still be active within the `NDKSubscription` lifecycle, but it might have been already been closed within the `NDKRelaySubscription` lifecycle, since NDK attempts to keep the minimum amount of open subscriptions at any given time. + +## NDKRelaySubscription +Most NDK subscriptions (by default) are set to be executed with a grouping delay. Will cover what this looks like in practice later, but for now, let's understand than when the `NDKRelaySubscriptionManager` receives an order, it might not execute it right away. + +The different filters that can be grouped together (thus executed as a single `REQ` within a relay) are grouped within the same `NDKRelaySubscription` instance and the execution scheduler is computed respecting what each individual `NDKSubscription` has requested. + +(For example, if a subscription with a `groupingDelay` of `at-least` 500 millisecond has been grouped with another subscription with a `groupingDelay` of `at-least` 1000 milliseconds, the `NDKRelaySubscriptionManager` will wait 1000 ms before sending the `REQ` to this particular relay). + +### Execution +Once the filter is executed at the relay level, the `REQ` is submitted into that relay's `NDKRelayConnectivity` instance, which will take care of monitoring for responses for this particular REQ and communicate them back into the `NDKRelaySubscription` instance. + +Each `EVENT` that comes back as a response to our `REQ` within this `NDKRelaySubscription` instance is then compared with the filters of each `NDKSubscription` that has been grouped and if it matches, it is sent back to the `NDKSubscription` instance. + + +# Example + +If an application requests `kind:1` of pubkeys `123`, `456`, and `789`. It creates an `NDKSubscription`: + +```ts +ndk.subscribe({ kinds: [1], authors: [ "123", "456", "789" ]}, { groupableDelay: 500, groupableDelayType: 'at-least' }) +// results in NDKSubscription1 with filters { kinds: [1], authors: [ "123", "456", "789" ] } +``` + +Some other part of the application requests a kind:7 from pubkey `123` at the same time. + +```ts +ndk.subscribe({ kinds: [7], authors: [ "123" ]}, { groupableDelay: 500, groupableDelayType: 'at-most' }) +// results in NDKSubscription2 with filters { kinds: [7], authors: [ "123" ] } +``` + +```mermaid +flowchart TD + subgraph Subscriptions Lifecycle + A[Application] -->|"kinds: [1], authors: [123, 456, 678], groupingDelay: at-least 500ms"| B[NDKSubscription1] + + A2[Application] -->|"kinds: [7], authors: [123], groupingDelay: at-most 1000ms"| B2[NDKSubscription2] + end +``` + +Both subscriptions have their relayset calculated by NDK and, the resulting filters are sent into the `NDKRelaySubscriptionManager`, which will decide what, and how filters can be grouped. + +```mermaid +flowchart TD + subgraph Subscriptions Lifecycle + A[Application] -->|"kinds: [1], authors: [123, 456, 678], groupingDelay: at-least 500ms"| B[NDKSubscription1] + B --> C{Calculate Relay Sets} + + A2[Application] -->|"kinds: [7], authors: [123], groupingDelay: at-most 1000ms"| B2[NDKSubscription2] + B2 --> C2{Calculate Relay Sets} + end + + subgraph Subscription Bundling + C -->|"kinds: [1], authors: [123]"| E1[wss://relay1 NDKRelaySubscriptionManager] + C -->|"kinds: [1], authors: [456]"| E2[wss://relay2 NDKRelaySubscriptionManager] + C -->|"kinds: [1], authors: [678]"| E3[wss://relay3 NDKRelaySubscriptionManager] + + C2 -->|"kinds: [7], authors: [123]"| E1 + end +``` + +The `NDKRelaySubscriptionManager` will create `NDKRelaySubscription` instances, or add filters to them if `NDKRelaySubscription` with the same filter fingerprint exists. + +```mermaid +flowchart TD + subgraph Subscriptions Lifecycle + A[Application] -->|"kinds: [1], authors: [123, 456, 678], groupingDelay: at-least 500ms"| B[NDKSubscription1] + B --> C{Calculate Relay Sets} + + A2[Application] -->|"kinds: [7], authors: [123], groupingDelay: at-most 1000ms"| B2[NDKSubscription2] + B2 --> C2{Calculate Relay Sets} + end + + subgraph Subscription Bundling + C -->|"kinds: [1], authors: [123]"| E1[wss://relay1 NDKRelaySubscriptionManager] + C -->|"kinds: [1], authors: [456]"| E2[wss://relay2 NDKRelaySubscriptionManager] + C -->|"kinds: [1], authors: [678]"| E3[wss://relay3 NDKRelaySubscriptionManager] + + C2 -->|"kinds: [7], authors: [123]"| E1 + + E1 -->|"Grouping Delay: at-most 1000ms"| F1[NDKRelaySubscription] + E2 -->|"Grouping Delay: at-least 500ms"| F2[NDKRelaySubscription] + E3 -->|"Grouping Delay: at-least 500ms"| F3[NDKRelaySubscription] + end +``` + +Each individual `NDKRelaySubscription` computes the execution schedule of the filters it has received and sends them to the `NDKRelayConnectivity` instance, which in turns sends the `REQ` to the relay. + +```mermaid +flowchart TD + subgraph Subscriptions Lifecycle + A[Application] -->|"kinds: [1], authors: [123, 456, 678], groupingDelay: at-least 500ms"| B[NDKSubscription1] + B --> C{Calculate Relay Sets} + + A2[Application] -->|"kinds: [7], authors: [123], groupingDelay: at-most 1000ms"| B2[NDKSubscription2] + B2 --> C2{Calculate Relay Sets} + end + + subgraph Subscription Bundling + C -->|"kinds: [1], authors: [123]"| E1[wss://relay1 NDKRelaySubscriptionManager] + C -->|"kinds: [1], authors: [456]"| E2[wss://relay2 NDKRelaySubscriptionManager] + C -->|"kinds: [1], authors: [678]"| E3[wss://relay3 NDKRelaySubscriptionManager] + + C2 -->|"kinds: [7], authors: [123]"| E1 + + E1 -->|"Grouping Delay: at-most 1000ms"| F1[NDKRelaySubscription] + E2 -->|"Grouping Delay: at-least 500ms"| F2[NDKRelaySubscription] + E3 -->|"Grouping Delay: at-least 500ms"| F3[NDKRelaySubscription] + + F1 -->|"REQ: kinds: [1, 7], authors: [123]"| G1[NDKRelayConnectivity] + F2 -->|"REQ: kinds: [1], authors: [456]"| G2[NDKRelayConnectivity] + F3 -->|"REQ: kinds: [1], authors: [678]"| G3[NDKRelayConnectivity] + end + + subgraph Execution + G1 -->|"Send REQ to wss://relay1 after 1000ms"| R1[Relay1] + G2 -->|"Send REQ to wss://relay2 after 500ms"| R2[Relay2] + G3 -->|"Send REQ to wss://relay3 after 500ms"| R3[Relay3] + end +``` + +As the events come from the relays, `NDKRelayConnectivity` will send them back to the `NDKRelaySubscription` instance, which will compare the event with the filters of the `NDKSubscription` instances that have been grouped together and send the received event back to the correct `NDKSubscription` instance. + +```mermaid +flowchart TD + subgraph Subscriptions Lifecycle + A[Application] -->|"kinds: [1], authors: [123, 456, 678], groupingDelay: at-least 500ms"| B[NDKSubscription1] + B --> C{Calculate Relay Sets} + + A2[Application] -->|"kinds: [7], authors: [123], groupingDelay: at-most 1000ms"| B2[NDKSubscription2] + B2 --> C2{Calculate Relay Sets} + end + + subgraph Subscription Bundling + C -->|"kinds: [1], authors: [123]"| E1[wss://relay1 NDKRelaySubscriptionManager] + C -->|"kinds: [1], authors: [456]"| E2[wss://relay2 NDKRelaySubscriptionManager] + C -->|"kinds: [1], authors: [678]"| E3[wss://relay3 NDKRelaySubscriptionManager] + + C2 -->|"kinds: [7], authors: [123]"| E1 + + E1 -->|"Grouping Delay: at-most 1000ms"| F1[NDKRelaySubscription] + E2 -->|"Grouping Delay: at-least 500ms"| F2[NDKRelaySubscription] + E3 -->|"Grouping Delay: at-least 500ms"| F3[NDKRelaySubscription] + + F1 -->|"REQ: kinds: [1, 7], authors: [123]"| G1[NDKRelayConnectivity] + F2 -->|"REQ: kinds: [1], authors: [456]"| G2[NDKRelayConnectivity] + F3 -->|"REQ: kinds: [1], authors: [678]"| G3[NDKRelayConnectivity] + end + + subgraph Execution + G1 -->|"Send REQ to wss://relay1 after 1000ms"| R1[Relay1] + G2 -->|"Send REQ to wss://relay2 after 500ms"| R2[Relay2] + G3 -->|"Send REQ to wss://relay3 after 500ms"| R3[Relay3] + + R1 -->|"EVENT: kinds: [1]"| H1[NDKRelaySubscription] + R1 -->|"EVENT: kinds: [7]"| H2[NDKRelaySubscription] + R2 -->|"EVENT"| H3[NDKRelaySubscription] + R3 -->|"EVENT"| H4[NDKRelaySubscription] + + H1 -->|"Matched Filters: kinds: [1]"| I1[NDKSubscription1] + H2 -->|"Matched Filters: kinds: [7]"| I2[NDKSubscription2] + H3 -->|"Matched Filters: kinds: [1]"| I1 + H4 -->|"Matched Filters: kinds: [1]"| I1 + end +``` \ No newline at end of file diff --git a/docs/tutorial/speed.md b/docs/tutorial/speed.md new file mode 100644 index 00000000..80cde51b --- /dev/null +++ b/docs/tutorial/speed.md @@ -0,0 +1,48 @@ +# Built for speed + +NDK makes multiple optimizations possible to create a performant client. + +## Signature Verifications +Signature validation is typically the most computationally expensive operation in a nostr client. Thus, NDK attempts to reduce the number of signature verifications that need to be done as much as possible. + +### Service Worker signature validation +In order to create performant clients, it's very useful to offload this computation to a service worker, to avoid blocking the main thread. + +```ts +// Using with vite +const sigWorker = import.meta.env.DEV ? + new Worker(new URL('@nostr-dev-kit/ndk/workers/sig-verification?worker', import.meta.url), { type: 'module' }) : new NDKSigVerificationWorker(); + +const ndk = new NDK(); +ndk.signatureVerificationWorker = worker +``` + +Since signature verification will thus be done asynchronously, it's important to listen for invalid signatures and handle them appropriately; you should +always warn your users when they are receiving invalid signatures from a relay and/or immediately disconnect from an evil relay. + +```ts +ndk.on("event:invalid-sig", (event) => { + const { relay } = event; + console.error("Invalid signature coming from relay", relay.url); +}); +``` + +### Signature verification sampling +Another parameter we can tweak is how many signatures we verify. By default, NDK will verify every signature, but you can change this by setting a per-relay verification rate. + +```ts +ndk.initialValidationRatio = 0.5; // Only verify 50% of the signatures for each relay +ndk.lowestValidationRatio = 0.01; // Never verify less than 1% of the signatures for each relay +``` + +NDK will then begin verifying signatures from each relay and, as signatures as verified, it will reduce the verification rate for that relay. + +### Custom validation ratio function +If you need further control on how the verification rate is adjusted, you can provide a validation ratio function. This function will be called periodically and the returning value will be used to adjust the verification rate. + +```ts +ndk.validationRatioFn = (relay: NDKRelay, validatedEvents: number, nonValidatedEvents: number): number => { + // write your own custom function here + return validatedEvents / (validatedEvents + nonValidatedEvents); +} +``` \ No newline at end of file diff --git a/docs/tutorial/zaps/index.md b/docs/tutorial/zaps/index.md new file mode 100644 index 00000000..c8b1bd84 --- /dev/null +++ b/docs/tutorial/zaps/index.md @@ -0,0 +1,38 @@ +# Zaps + +NDK comes with an interface to make zapping as simple as possible. + +```ts +const user = await ndk.getUserFromNip05("pablo@f7z.io"); +const zapper = await ndk.zap(user, 1000) +``` + +## Connecting to WebLN +Advanced users might have a webln extension available in their browsers. To attempt to use their WebLN to pay, you can connect webln with NDK. + +```ts +import { requestProvider } from "webln"; +let weblnProvider; +requestProvider().then(provider => weblnProvider = provider }); + +// whenever the user wants to pay for something, and using LN is an option for the payment +// this function will be called +ndk.walletConfig.onLnPay = async ({pr: string, amount: number, target?: NDKEvent | NDKUser}) => { + if (weblnProvider) { + if (confirm("Would you like to pay with your WebLN provider?")) { + await weblnProvider.sendPayment(pr); + } + } else { + // show a QR code to the user or handle in some way + } +}); +``` + +Now from anywhere in the app, you can: + +```ts +event.zap(1000); // zap an event 1 sat +``` + +## Configuring a wallet +NDK provides an `ndk-wallet` package that makes it very simple to use a NIP-60 wallet. \ No newline at end of file diff --git a/docs/wallet/index.md b/docs/wallet/index.md new file mode 100644 index 00000000..17c067e8 --- /dev/null +++ b/docs/wallet/index.md @@ -0,0 +1,56 @@ +# Wallet + +NDK provides the `@nostr-dev-kit/ndk-wallet` package, which provides common interfaces and functionalities to create a wallet that leverages nostr. + +## Initialization + +An `NDKWallet` can be provided to ndk in the constructor. + +```ts +// instantiate your NDK +import NDK from "@nostr-dev-kit/ndk"; +import NDKWallet from "@nostr-dev-kit/ndk-wallet"; + +const ndk = new NDK({ + explicitRelayUrls: [ ], +}); +ndk.connect(); + +// Establish the main user's signer +ndk.signer = NDKPrivateKeySigner.generate(); + +// instantiate the wallet +const ndkWallet = new NDKWallet(ndk); +``` + +## Creating a Wallet +Once we have an NDK instance ready we can create a wallet. + +```ts +const wallet = ndkWallet.createCashuWallet(); +wallet.name = "My Wallet"; +wallet.relays = [ "wss://relay1", "wss://relay2" ] +wallet.publish(); +``` + +This will publish a wallet `kind:37376` event, which contains the wallet information. + +But wait, we have no mints! + +## Discovering mints +We need to find mints we want to use. + +```ts +import { getMintRecommendations } from "@nostr-dev-kit/ndk-wallet"; +const mintRecommendations = await getMintRecommendations(ndk); +``` + +Now you can either use WoT to find the most trusted mints from the point of view of the user, or you can use any mechanism to let the user determine which mints to use. + +```ts +wallet.mints = choosenMints; +// We want to publishReplaceable here since the wallet is a replaceable event +wallet.publishReplaceable(); +``` + +## \ No newline at end of file diff --git a/ndk-cache-dexie/CHANGELOG.md b/ndk-cache-dexie/CHANGELOG.md index 5f4ebabe..9516b67c 100644 --- a/ndk-cache-dexie/CHANGELOG.md +++ b/ndk-cache-dexie/CHANGELOG.md @@ -1,5 +1,23 @@ # @nostr-dev-kit/ndk-cache-dexie +## 2.5.1 + +### Patch Changes + +- apply limit filter +- abb3cd9: add tests +- index event kinds and add byKinds filter +- improve profile fetching from dexie +- 3029124: add methods to access and manage unpublished events from the cache +- Updated dependencies [ec83ddc] +- Updated dependencies [18c55bb] +- Updated dependencies +- Updated dependencies [18c55bb] +- Updated dependencies +- Updated dependencies +- Updated dependencies [3029124] + - @nostr-dev-kit/ndk@2.10.0 + ## 2.5.0 ### Minor Changes diff --git a/ndk-cache-dexie/package.json b/ndk-cache-dexie/package.json index ba4b3bbb..359875c3 100644 --- a/ndk-cache-dexie/package.json +++ b/ndk-cache-dexie/package.json @@ -1,6 +1,6 @@ { "name": "@nostr-dev-kit/ndk-cache-dexie", - "version": "2.5.0", + "version": "2.5.1", "description": "NDK Dexie Cache Adapter", "license": "MIT", "docs": "typedoc", diff --git a/ndk-cache-dexie/src/index.ts b/ndk-cache-dexie/src/index.ts index 5570b3b7..a6e32cc4 100644 --- a/ndk-cache-dexie/src/index.ts +++ b/ndk-cache-dexie/src/index.ts @@ -38,12 +38,6 @@ export interface NDKCacheAdapterDexieOptions { */ debug?: debug.IDebugger; - /** - * The number of seconds to store events in Dexie (IndexedDB) before they expire - * Defaults to 3600 seconds (1 hour) - */ - expirationTime?: number; - /** * Number of profiles to keep in an LRU cache */ @@ -56,8 +50,7 @@ export interface NDKCacheAdapterDexieOptions { export default class NDKCacheAdapterDexie implements NDKCacheAdapter { public debug: debug.Debugger; - private expirationTime; - readonly locking = true; + public locking = false; public ready = false; public profiles: CacheHandler; public zappers: CacheHandler; @@ -74,7 +67,6 @@ export default class NDKCacheAdapterDexie implements NDKCacheAdapter { constructor(opts: NDKCacheAdapterDexieOptions = {}) { createDatabase(opts.dbName || "ndk"); this.debug = opts.debug || createDebug("ndk:dexie-adapter"); - this.expirationTime = opts.expirationTime || 3600; this.profiles = new CacheHandler({ maxSize: opts.profileCacheSize || 100000, @@ -145,6 +137,7 @@ export default class NDKCacheAdapterDexie implements NDKCacheAdapter { const endTime = Date.now(); this.warmedUp = true; this.ready = true; + this.locking = true; this.debug("Warm up completed, time", endTime - startTime, "ms"); // call the onReady callback if it's set diff --git a/ndk-cache-nostr/package.json b/ndk-cache-nostr/package.json index f0a74692..011765e3 100644 --- a/ndk-cache-nostr/package.json +++ b/ndk-cache-nostr/package.json @@ -1,6 +1,6 @@ { "name": "@nostr-dev-kit/ndk-cache-nostr", - "version": "0.0.1", + "version": "0.1.0", "description": "NDK cache adapter that uses a local nostr relay.", "main": "./dist/index.js", "module": "./dist/index.mjs", diff --git a/ndk-cache-redis/CHANGELOG.md b/ndk-cache-redis/CHANGELOG.md index 5da13c45..57f70090 100644 --- a/ndk-cache-redis/CHANGELOG.md +++ b/ndk-cache-redis/CHANGELOG.md @@ -1,5 +1,18 @@ # @nostr-dev-kit/ndk-cache-redis +## 2.1.17 + +### Patch Changes + +- Updated dependencies [ec83ddc] +- Updated dependencies [18c55bb] +- Updated dependencies +- Updated dependencies [18c55bb] +- Updated dependencies +- Updated dependencies +- Updated dependencies [3029124] + - @nostr-dev-kit/ndk@2.10.0 + ## 2.1.16 ### Patch Changes diff --git a/ndk-cache-redis/package.json b/ndk-cache-redis/package.json index c4a12578..baa809a2 100644 --- a/ndk-cache-redis/package.json +++ b/ndk-cache-redis/package.json @@ -1,6 +1,6 @@ { "name": "@nostr-dev-kit/ndk-cache-redis", - "version": "2.1.16", + "version": "2.1.17", "description": "NDK cache adapter for redis.", "main": "./dist/index.js", "module": "./dist/index.mjs", diff --git a/ndk-svelte-components/CHANGELOG.md b/ndk-svelte-components/CHANGELOG.md index 44164f39..58d5b441 100644 --- a/ndk-svelte-components/CHANGELOG.md +++ b/ndk-svelte-components/CHANGELOG.md @@ -1,5 +1,18 @@ # @nostr-dev-kit/ndk-svelte-components +## 2.2.19 + +### Patch Changes + +- Updated dependencies [ec83ddc] +- Updated dependencies [18c55bb] +- Updated dependencies +- Updated dependencies [18c55bb] +- Updated dependencies +- Updated dependencies +- Updated dependencies [3029124] + - @nostr-dev-kit/ndk@2.10.0 + ## 2.2.18 ### Patch Changes diff --git a/ndk-svelte-components/package.json b/ndk-svelte-components/package.json index 29fd92ef..a016d425 100644 --- a/ndk-svelte-components/package.json +++ b/ndk-svelte-components/package.json @@ -1,6 +1,6 @@ { "name": "@nostr-dev-kit/ndk-svelte-components", - "version": "2.2.18", + "version": "2.2.19", "description": "", "license": "MIT", "type": "module", @@ -84,7 +84,6 @@ "rehype-autolink-headings": "^7.0.0", "rehype-slug": "^6.0.0", "sanitize-html": "^2.11.0", - "svelte-asciidoc": "^0.0.2", "svelte-preprocess": "^5.0.4", "svelte-time": "^0.8.3" }, diff --git a/ndk-svelte-components/src/lib/event/content/EventContent.svelte b/ndk-svelte-components/src/lib/event/content/EventContent.svelte index 3deab34c..d78ad7ff 100644 --- a/ndk-svelte-components/src/lib/event/content/EventContent.svelte +++ b/ndk-svelte-components/src/lib/event/content/EventContent.svelte @@ -10,9 +10,9 @@ import Kind30000 from "./Kind30000.svelte"; import Kind30001 from "./Kind30001.svelte"; import Kind30023 from "./Kind30023.svelte"; - import Kind30818 from "./Kind30818.svelte"; import type { SvelteComponent } from "svelte"; import type { MarkedExtension } from "marked"; + import type { UrlFactory, UrlType } from "$lib"; export let ndk: NDK; export let event: NDKEvent | null | undefined; @@ -22,6 +22,17 @@ export let showMedia: boolean = true; export let mediaCollectionComponent: typeof SvelteComponent | undefined = undefined; export let eventCardComponent: typeof SvelteComponent | undefined = undefined; + + export let urlFactory: UrlFactory = (type: UrlType, value: string) => { + switch (type) { + case "hashtag": + return `/t/${value}`; + case "mention": + return `/p/${value}`; + default: + return value; + } + }; /** * Markdown marked extensions to use @@ -33,12 +44,12 @@ */ export let content = event?.content; - const markdownKinds = [ NDKKind.Article, 30041 ] + const markdownKinds = [ NDKKind.Article, 30041, NDKKind.Wiki ] {#if event} {#if event.kind === 1} - + {:else if event.kind === 40} {:else if event.kind === 1063} @@ -62,15 +73,20 @@ class={$$props.class} {markedExtensions} /> - {:else if event.kind === 30818} - - {:else} - {/if} {/if} diff --git a/ndk-svelte-components/src/lib/event/content/Kind1.svelte b/ndk-svelte-components/src/lib/event/content/Kind1.svelte index 91cb9f2c..1716b5aa 100644 --- a/ndk-svelte-components/src/lib/event/content/Kind1.svelte +++ b/ndk-svelte-components/src/lib/event/content/Kind1.svelte @@ -25,6 +25,7 @@ import EventCard from '../EventCard.svelte'; import { pluck, values, without } from 'ramda'; import type { SvelteComponent } from 'svelte'; + import type { UrlFactory } from '$lib'; // import NoteContentEntity from "./NoteContentEntity.svelte" export let event, maxLength; @@ -35,6 +36,7 @@ export let content = event.content; export let mediaCollectionComponent: typeof SvelteComponent | undefined = undefined; export let eventCardComponent: typeof SvelteComponent = EventCard; + export let urlFactory: UrlFactory; export const getLinks = (parts: any[]) => pluck( "value", @@ -58,7 +60,7 @@ {#if type === NEWLINE} {:else if type === TOPIC} - + {:else if type === LINK} {:else if type === LINKCOLLECTION} @@ -72,7 +74,7 @@ {/if} {:else if type.match(/^nostr:np(rofile|ub)$/)} - + {:else if type.startsWith('nostr:') && showMedia && isStartOrEnd(i) && value.id !== anchorId} {:else if type.startsWith('nostr:')} diff --git a/ndk-svelte-components/src/lib/event/content/Kind30818.svelte b/ndk-svelte-components/src/lib/event/content/Kind30818.svelte deleted file mode 100644 index c09e595a..00000000 --- a/ndk-svelte-components/src/lib/event/content/Kind30818.svelte +++ /dev/null @@ -1,40 +0,0 @@ - - - -
- -
- - diff --git a/ndk-svelte-components/src/lib/event/content/NoteContentPerson.svelte b/ndk-svelte-components/src/lib/event/content/NoteContentPerson.svelte index 262ecc71..18f5a24d 100644 --- a/ndk-svelte-components/src/lib/event/content/NoteContentPerson.svelte +++ b/ndk-svelte-components/src/lib/event/content/NoteContentPerson.svelte @@ -1,10 +1,12 @@ - + diff --git a/ndk-svelte-components/src/lib/event/content/NoteContentTopic.svelte b/ndk-svelte-components/src/lib/event/content/NoteContentTopic.svelte index c657f050..39cfd007 100644 --- a/ndk-svelte-components/src/lib/event/content/NoteContentTopic.svelte +++ b/ndk-svelte-components/src/lib/event/content/NoteContentTopic.svelte @@ -1,5 +1,8 @@ -#{value} +#{value} diff --git a/ndk-svelte-components/src/lib/index.ts b/ndk-svelte-components/src/lib/index.ts index d52070e7..0af3c569 100644 --- a/ndk-svelte-components/src/lib/index.ts +++ b/ndk-svelte-components/src/lib/index.ts @@ -10,6 +10,10 @@ import EventThread from "./event/EventThread.svelte"; export * from "./utils"; +export type UrlType = "hashtag" | "mention"; + +export type UrlFactory = (type: UrlType, value: string) => string; + export { // Event EventContent, diff --git a/ndk-svelte-components/src/lib/utils/components/WikilinkComponent.svelte b/ndk-svelte-components/src/lib/utils/components/WikilinkComponent.svelte deleted file mode 100644 index 0b881313..00000000 --- a/ndk-svelte-components/src/lib/utils/components/WikilinkComponent.svelte +++ /dev/null @@ -1,50 +0,0 @@ - - -{#if href.startsWith('wikilink')} - -{:else} - - - - - - - - - -{/if} diff --git a/ndk-svelte/CHANGELOG.md b/ndk-svelte/CHANGELOG.md index 6dede3d4..91ba403e 100644 --- a/ndk-svelte/CHANGELOG.md +++ b/ndk-svelte/CHANGELOG.md @@ -1,5 +1,19 @@ # @nostr-dev-kit/ndk-svelte +## 2.2.18 + +### Patch Changes + +- e8ad796: expose a way to peak into events as they come +- Updated dependencies [ec83ddc] +- Updated dependencies [18c55bb] +- Updated dependencies +- Updated dependencies [18c55bb] +- Updated dependencies +- Updated dependencies +- Updated dependencies [3029124] + - @nostr-dev-kit/ndk@2.10.0 + ## 2.2.17 ### Patch Changes diff --git a/ndk-svelte/package.json b/ndk-svelte/package.json index a6b4c612..fc269cb6 100644 --- a/ndk-svelte/package.json +++ b/ndk-svelte/package.json @@ -1,6 +1,6 @@ { "name": "@nostr-dev-kit/ndk-svelte", - "version": "2.2.17", + "version": "2.2.18", "description": "This package provides convenience functionalities to make usage of NDK with Svelte nicer.", "main": "./dist/index.js", "module": "./dist/index.mjs", diff --git a/ndk-svelte/src/index.ts b/ndk-svelte/src/index.ts index 7316d35c..06d2245a 100644 --- a/ndk-svelte/src/index.ts +++ b/ndk-svelte/src/index.ts @@ -15,7 +15,7 @@ import { type Unsubscriber, type Writable, writable } from "svelte/store"; * Type for NDKEvent classes that have a static `from` method like NDKHighlight. */ type ClassWithConvertFunction = { - from: (event: NDKEvent) => T; + from: (event: NDKEvent) => T | undefined; }; export type ExtendedBaseType = T & { @@ -195,7 +195,9 @@ class NDKSvelte extends NDK { let e = event; if (klass) { - e = klass.from(event); + const ev = klass.from(event); + if (!ev) return; + e = ev; e.relay = event.relay; } e.ndk = this; @@ -205,10 +207,18 @@ class NDKSvelte extends NDK { if (eventIds.has(dedupKey)) { const prevEvent = events.find((e) => e.deduplicationKey() === dedupKey); - if (prevEvent && prevEvent.created_at! < event.created_at!) { - // remove the previous event - const index = events.findIndex((e) => e.deduplicationKey() === dedupKey); - events.splice(index, 1); + if (prevEvent) { + if (prevEvent.created_at! < event.created_at!) { + // remove the previous event + const index = events.findIndex((e) => e.deduplicationKey() === dedupKey); + events.splice(index, 1); + } else if (prevEvent.created_at! === event.created_at! && prevEvent.id !== event.id) { + // we have an event with the same created_at but different id, which might be a bug + // in some relays but take the incoming event anyway + console.warn("Received event with same created_at but different id", { prevId: prevEvent.id, newId: event.id }); + const index = events.findIndex((e) => e.deduplicationKey() === dedupKey); + events.splice(index, 1); + } } else { return; } diff --git a/ndk-wallet/CHANGELOG.md b/ndk-wallet/CHANGELOG.md new file mode 100644 index 00000000..1869567c --- /dev/null +++ b/ndk-wallet/CHANGELOG.md @@ -0,0 +1,311 @@ +# @nostr-dev-kit/ndk-cache-redis + +## 0.2.0 + +### Minor Changes + +- Zap improvements + +### Patch Changes + +- Updated dependencies [ec83ddc] +- Updated dependencies [18c55bb] +- Updated dependencies +- Updated dependencies [18c55bb] +- Updated dependencies +- Updated dependencies +- Updated dependencies [3029124] + - @nostr-dev-kit/ndk@2.10.0 + +## 2.1.16 + +### Patch Changes + +- Updated dependencies + - @nostr-dev-kit/ndk@2.9.1 + +## 2.1.15 + +### Patch Changes + +- Updated dependencies [94018b4] +- Updated dependencies [548f4d8] + - @nostr-dev-kit/ndk@2.9.0 + +## 2.1.14 + +### Patch Changes + +- Updated dependencies [0af033f] +- Updated dependencies + - @nostr-dev-kit/ndk@2.8.2 + +## 2.1.13 + +### Patch Changes + +- Updated dependencies [e40312b] +- Updated dependencies + - @nostr-dev-kit/ndk@2.8.1 + +## 2.1.12 + +### Patch Changes + +- Updated dependencies [91d873c] +- Updated dependencies [6fd9ddc] +- Updated dependencies [0b8f331] +- Updated dependencies +- Updated dependencies [f2898ad] +- Updated dependencies [9b92cd9] +- Updated dependencies +- Updated dependencies [6814f0c] +- Updated dependencies [89b5b3f] +- Updated dependencies [9b92cd9] +- Updated dependencies [27b10cc] +- Updated dependencies +- Updated dependencies +- Updated dependencies [ed7cdc4] + - @nostr-dev-kit/ndk@2.8.0 + +## 2.1.11 + +### Patch Changes + +- Updated dependencies + - @nostr-dev-kit/ndk@2.7.1 + +## 2.1.10 + +### Patch Changes + +- Updated dependencies +- Updated dependencies +- Updated dependencies + - @nostr-dev-kit/ndk@2.7.0 + +## 2.1.9 + +### Patch Changes + +- Updated dependencies + - @nostr-dev-kit/ndk@2.6.1 + +## 2.1.8 + +### Patch Changes + +- Updated dependencies +- Updated dependencies [c2db3c1] +- Updated dependencies +- Updated dependencies [c2db3c1] +- Updated dependencies [c2db3c1] + - @nostr-dev-kit/ndk@2.6.0 + +## 2.1.7 + +### Patch Changes + +- Updated dependencies +- Updated dependencies + - @nostr-dev-kit/ndk@2.5.1 + +## 2.1.6 + +### Patch Changes + +- Updated dependencies [e08fc74] + - @nostr-dev-kit/ndk@2.5.0 + +## 2.1.5 + +### Patch Changes + +- Updated dependencies [111c1ea] +- Updated dependencies [5c0ae51] +- Updated dependencies [6f5ea49] +- Updated dependencies [3738d39] +- Updated dependencies [d22239a] + - @nostr-dev-kit/ndk@2.4.1 + +## 2.1.4 + +### Patch Changes + +- Updated dependencies [b9bbf1d] + - @nostr-dev-kit/ndk@2.4.0 + +## 2.1.3 + +### Patch Changes + +- Updated dependencies +- Updated dependencies [885b6c2] +- Updated dependencies [5666d56] + - @nostr-dev-kit/ndk@2.3.3 + +## 2.1.2 + +### Patch Changes + +- Updated dependencies +- Updated dependencies [4628481] +- Updated dependencies + - @nostr-dev-kit/ndk@2.3.2 + +## 2.1.1 + +### Patch Changes + +- Updated dependencies [ece965f] + - @nostr-dev-kit/ndk@2.3.1 + +## 2.1.0 + +### Minor Changes + +- 06c83ea: Aggressively cache all filters and their responses so the same filter can hit the cache + +### Patch Changes + +- Updated dependencies [54cec78] +- Updated dependencies [ef61d83] +- Updated dependencies [98b77dd] +- Updated dependencies [46b0c77] +- Updated dependencies [082e243] + - @nostr-dev-kit/ndk@2.3.0 + +## 2.0.11 + +### Patch Changes + +- Updated dependencies + - @nostr-dev-kit/ndk@2.2.0 + +## 2.0.10 + +### Patch Changes + +- Updated dependencies [180d774] +- Updated dependencies [7f00c40] + - @nostr-dev-kit/ndk@2.1.3 + +## 2.0.9 + +### Patch Changes + +- Updated dependencies +- Updated dependencies +- Updated dependencies + - @nostr-dev-kit/ndk@2.1.2 + +## 2.0.8 + +### Patch Changes + +- Updated dependencies + - @nostr-dev-kit/ndk@2.1.1 + +## 2.0.7 + +### Patch Changes + +- Updated dependencies + - @nostr-dev-kit/ndk@2.1.0 + +## 2.0.6 + +### Patch Changes + +- Updated dependencies +- Updated dependencies + - @nostr-dev-kit/ndk@2.0.6 + +## 2.0.5 + +### Patch Changes + +- Updated dependencies [d45d962] + - @nostr-dev-kit/ndk@2.0.5 + +## 2.0.5 + +### Patch Changes + +- Updated dependencies +- Updated dependencies [d45d962] + - @nostr-dev-kit/ndk@2.0.5 + +## 2.0.4 + +### Patch Changes + +- Updated dependencies + - @nostr-dev-kit/ndk@2.0.4 + +## 2.0.3 + +### Patch Changes + +- Updated dependencies + - @nostr-dev-kit/ndk@2.0.3 + +## 2.0.2 + +### Patch Changes + +- Updated dependencies + - @nostr-dev-kit/ndk@2.0.2 + +## 1.8.7 + +### Patch Changes + +- Updated dependencies + - @nostr-dev-kit/ndk@2.0.0 + +## 1.8.6 + +### Patch Changes + +- Updated dependencies + - @nostr-dev-kit/ndk@1.4.2 + +## 1.8.5 + +### Patch Changes + +- Updated dependencies + - @nostr-dev-kit/ndk@1.4.1 + +## 1.8.4 + +### Patch Changes + +- Updated dependencies + - @nostr-dev-kit/ndk@1.4.0 + +## 1.8.3 + +### Patch Changes + +- Updated dependencies [b3561af] + - @nostr-dev-kit/ndk@1.3.2 + +## 1.8.2 + +### Patch Changes + +- Updated dependencies + - @nostr-dev-kit/ndk@1.3.1 + +## 1.8.1 + +### Patch Changes + +- Updated dependencies [88df10a] +- Updated dependencies [c225094] +- Updated dependencies [cf4a648] +- Updated dependencies [3946078] +- Updated dependencies [3440768] + - @nostr-dev-kit/ndk@1.3.0 diff --git a/ndk-wallet/README.md b/ndk-wallet/README.md new file mode 100644 index 00000000..1ad457a7 --- /dev/null +++ b/ndk-wallet/README.md @@ -0,0 +1,24 @@ +# ndk-wallet + +NDK Wallet provides common interfaces and functionalities to create a wallet that leverages nostr. + +## Usage + +### Install + +``` +npm add @nostr-dev-kit/ndk-wallet +``` + +### Add as a cache adapter + +```ts +import NDKWallet from "@nostr-dev-kit/ndk-wallet"; + +const cacheAdapter = new NDKRedisCacheAdapter(); +const ndk = new NDK({ cacheAdapter }); +``` + +# License + +MIT diff --git a/ndk-wallet/jest.config.ts b/ndk-wallet/jest.config.ts new file mode 100644 index 00000000..931e01ef --- /dev/null +++ b/ndk-wallet/jest.config.ts @@ -0,0 +1,11 @@ +import type { Config } from "jest"; + +const config: Config = { + preset: "ts-jest", + testEnvironment: "node", + moduleNameMapper: { + "^(\\.{1,2}/.*)\\.js$": "$1", + }, +}; + +export default config; diff --git a/ndk-wallet/package.json b/ndk-wallet/package.json new file mode 100644 index 00000000..222b157f --- /dev/null +++ b/ndk-wallet/package.json @@ -0,0 +1,59 @@ +{ + "name": "@nostr-dev-kit/ndk-wallet", + "version": "0.2.0", + "description": "NDK Wallet", + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "exports": { + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "scripts": { + "dev": "pnpm build --watch", + "build": "tsup src/index.ts --format cjs,esm --dts", + "clean": "rm -rf dist", + "test": "jest", + "lint": "prettier --check . && eslint .", + "format": "prettier --write ." + }, + "repository": { + "type": "git", + "url": "git+https://github.com/nostr-dev-kit/ndk.git" + }, + "keywords": [ + "nostr", + "cashu", + "ecash" + ], + "author": "pablof7z", + "license": "MIT", + "bugs": { + "url": "https://github.com/nostr-dev-kit/ndk/issues" + }, + "homepage": "https://github.com/nostr-dev-kit/ndk", + "dependencies": { + "@cashu/cashu-ts": "1.0.0-rc.9", + "@nostr-dev-kit/ndk": "workspace:*", + "debug": "^4.3.4", + "light-bolt11-decoder": "^3.0.0", + "tseep": "^1.1.1", + "typescript": "^5.4.4" + }, + "devDependencies": { + "@nostr-dev-kit/eslint-config-custom": "workspace:*", + "@nostr-dev-kit/tsconfig": "workspace:*", + "@types/debug": "^4.1.7", + "@types/jest": "^29.5.5", + "@types/node": "^18.15.11", + "jest": "^29.7.0", + "ts-jest": "^29.1.2", + "ts-node": "^10.9.2", + "tsup": "^7.2.0" + } +} diff --git a/ndk-wallet/src/cashu/deposit.ts b/ndk-wallet/src/cashu/deposit.ts new file mode 100644 index 00000000..10cfab67 --- /dev/null +++ b/ndk-wallet/src/cashu/deposit.ts @@ -0,0 +1,111 @@ +import type { Proof } from "@cashu/cashu-ts"; +import { CashuMint, CashuWallet } from "@cashu/cashu-ts"; +import type { NDKCashuWallet } from "./wallet"; +import { EventEmitter } from "tseep"; +import { NDKCashuToken } from "./token"; +import createDebug from "debug"; + +const d = createDebug("ndk-wallet:cashu:deposit"); + +function randomMint(wallet: NDKCashuWallet) { + const mints = wallet.mints; + const mint = mints[Math.floor(Math.random() * mints.length)]; + return mint; +} + +export class NDKCashuDeposit extends EventEmitter<{ + success: (token: NDKCashuToken) => void; + error: (error: string) => void; +}> { + private mint: string; + public amount: number; + public quoteId: string | undefined; + private wallet: NDKCashuWallet; + private _wallet: CashuWallet; + public checkTimeout: NodeJS.Timeout | undefined; + public checkIntervalLength = 2500; + public finalized = false; + public unit?: string; + + constructor(wallet: NDKCashuWallet, amount: number, mint?: string, unit?: string) { + super(); + this.wallet = wallet; + this.mint = mint ?? randomMint(wallet); + this.amount = amount; + this.unit = unit; + this._wallet = new CashuWallet(new CashuMint(this.mint), { unit }); + } + + async start() { + const quote = await this._wallet.mintQuote(this.amount); + d("created quote %s for %d %s", quote.quote, this.amount, this.mint); + + this.quoteId = quote.quote; + + this.check(); + + return quote.request; + } + + private async runCheck() { + if (!this.finalized) await this.finalize(); + if (!this.finalized) this.delayCheck(); + } + + private delayCheck() { + setTimeout(() => { + this.runCheck(); + this.checkIntervalLength += 500; + if (this.checkIntervalLength > 30000) { + this.checkIntervalLength = 30000; + } + }, this.checkIntervalLength); + } + + /** + * Check if the deposit has been finalized. + * @param timeout A timeout in milliseconds to wait before giving up. + */ + async check(timeout?: number) { + this.runCheck(); + + if (timeout) { + setTimeout(() => { + clearTimeout(this.checkTimeout); + }, timeout); + } + } + + async finalize() { + if (!this.quoteId) throw new Error("No quoteId set."); + + let ret: { proofs: Array }; + + try { + d("Checking for minting status of %s", this.quoteId); + ret = await this._wallet.mintTokens(this.amount, this.quoteId); + if (!ret?.proofs) return; + } catch (e: any) { + if (e.message.match(/not paid/i)) return; + d(e.message); + return; + } + + try { + this.finalized = true; + + const tokenEvent = new NDKCashuToken(this.wallet.ndk); + tokenEvent.proofs = ret.proofs; + tokenEvent.mint = this.mint; + tokenEvent.wallet = this.wallet; + + await tokenEvent.publish(this.wallet.relaySet); + + this.emit("success", tokenEvent); + } catch (e: any) { + console.log("relayset", this.wallet.relaySet); + this.emit("error", e.message); + console.error(e); + } + } +} diff --git a/ndk-wallet/src/cashu/history.ts b/ndk-wallet/src/cashu/history.ts new file mode 100644 index 00000000..887b5562 --- /dev/null +++ b/ndk-wallet/src/cashu/history.ts @@ -0,0 +1,90 @@ +import type { NDKTag, NostrEvent } from "@nostr-dev-kit/ndk"; +import type NDK from "@nostr-dev-kit/ndk"; +import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk"; +import createDebug from "debug"; + +const d = createDebug("ndk-wallet:wallet-change"); + +const MARKERS = { + REDEEMED: "redeemed", + CREATED: "created", + DESTROYED: "destroyed", +}; + +export class NDKWalletChange extends NDKEvent { + static MARKERS = MARKERS; + + static kind = NDKKind.WalletChange; + static kinds = [NDKKind.WalletChange]; + + constructor(ndk?: NDK, event?: NostrEvent | NDKEvent) { + super(ndk, event); + this.kind ??= NDKKind.WalletChange; + } + + static async from(event: NDKEvent): Promise { + const walletChange = new NDKWalletChange(event.ndk, event); + + const prevContent = walletChange.content; + try { + await walletChange.decrypt(); + } catch (e) { + walletChange.content ??= prevContent; + } + + try { + const contentTags = JSON.parse(walletChange.content); + walletChange.tags = [...contentTags, ...walletChange.tags]; + } catch (e) { + return; + } + + return walletChange; + } + + public addRedeemedNutzap(event: NDKEvent) { + this.tag(event, MARKERS.REDEEMED); + } + + async toNostrEvent(pubkey?: string): Promise { + const encryptedTags: NDKTag[] = []; + const unencryptedTags: NDKTag[] = []; + + for (const tag of this.tags) { + if (!this.shouldEncryptTag(tag)) { + unencryptedTags.push(tag); + } else { + encryptedTags.push(tag); + } + } + + this.tags = unencryptedTags.filter((t) => t[0] !== "client"); + this.content = JSON.stringify(encryptedTags); + + const user = await this.ndk!.signer!.user(); + + await this.encrypt(user); + + return super.toNostrEvent(pubkey) as unknown as NostrEvent; + } + + /** + * Whether this entry includes a redemption of a Nutzap + */ + get hasNutzapRedemption(): boolean { + return this.getMatchingTags("e", MARKERS.REDEEMED).length > 0; + } + + private shouldEncryptTag(tag: NDKTag): boolean { + const unencryptedTagNames = ["d", "client", "a"]; + if (unencryptedTagNames.includes(tag[0])) { + return false; + } + + if (tag[0] === "e" && tag[3] === MARKERS.REDEEMED) { + return false; + } + + return true; + } +} diff --git a/ndk-wallet/src/cashu/mint/utils.ts b/ndk-wallet/src/cashu/mint/utils.ts new file mode 100644 index 00000000..4edfcfbe --- /dev/null +++ b/ndk-wallet/src/cashu/mint/utils.ts @@ -0,0 +1,64 @@ +import type { Hexpubkey, NDKEvent, NDKFilter} from "@nostr-dev-kit/ndk"; +import type NDK from "@nostr-dev-kit/ndk"; +import { NDKKind } from "@nostr-dev-kit/ndk"; + +export type MintUrl = string; +export type MintUsage = { + /** + * All the events that are associated with this mint. + */ + events: NDKEvent[]; + + pubkeys: Set; +}; +export type NDKCashuMintRecommendation = Record; + +/** + * Provides a list of mint recommendations. + * @param ndk + * @param filter optional extra filter to apply to the REQ + */ +export async function getCashuMintRecommendations( + ndk: NDK, + filter?: NDKFilter +): Promise { + const f: NDKFilter[] = [ + { kinds: [NDKKind.EcashMintRecommendation], "#k": ["38002"], ...(filter || {}) }, + { kinds: [NDKKind.CashuMintList], ...(filter || {}) }, + ]; + const res: NDKCashuMintRecommendation = {}; + + const recommendations = await ndk.fetchEvents(f); + + for (const event of recommendations) { + switch (event.kind) { + case NDKKind.EcashMintRecommendation: + for (const uTag of event.getMatchingTags("u")) { + if (uTag[2] && uTag[2] !== "cashu") continue; + + const url = uTag[1]; + if (!url) continue; + + const entry = res[url] || { events: [], pubkeys: new Set() }; + entry.events.push(event); + entry.pubkeys.add(event.pubkey); + res[url] = entry; + } + break; + + case NDKKind.CashuMintList: + for (const mintTag of event.getMatchingTags("mint")) { + const url = mintTag[1]; + if (!url) continue; + + const entry = res[url] || { events: [], pubkeys: new Set() }; + entry.events.push(event); + entry.pubkeys.add(event.pubkey); + res[url] = entry; + } + break; + } + } + + return res; +} diff --git a/ndk-wallet/src/cashu/pay.ts b/ndk-wallet/src/cashu/pay.ts new file mode 100644 index 00000000..7b8016e4 --- /dev/null +++ b/ndk-wallet/src/cashu/pay.ts @@ -0,0 +1,108 @@ +import { Proof } from "@cashu/cashu-ts"; +import type { NDKCashuWallet } from "./wallet"; +import createDebug from "debug"; +import type { LnPaymentInfo } from "@nostr-dev-kit/ndk"; +import type { NutPayment} from "./pay/nut.js"; +import { payNut } from "./pay/nut.js"; +import { payLn } from "./pay/ln.js"; +import { decode as decodeBolt11 } from "light-bolt11-decoder"; + +function correctP2pk(p2pk?: string) { + if (p2pk) { + if (p2pk.length === 64) p2pk = `02${p2pk}`; + } + + return p2pk; +} + +/** + * Uses cashu balance to make a payment, whether a cashu swap or a lightning + */ +export class NDKCashuPay { + public wallet: NDKCashuWallet; + public info: LnPaymentInfo | NutPayment; + public type: "ln" | "nut" = "ln"; + public debug = createDebug("ndk-wallet:cashu:pay"); + public unit: string = "sat"; + + constructor(wallet: NDKCashuWallet, info: LnPaymentInfo | NutPayment) { + this.wallet = wallet; + + if ((info as LnPaymentInfo).pr) { + this.type = "ln"; + this.info = info as LnPaymentInfo; + } else { + this.type = "nut"; + this.info = info as NutPayment; + if (this.info.unit.startsWith("msat")) { + this.info.unit = "sat"; + this.info.amount = this.info.amount / 1000; + this.info.p2pk = correctP2pk(this.info.p2pk); + } + + this.debug("nut payment %o", this.info); + } + } + + public getAmount() { + if (this.type === "ln") { + const bolt11 = (this.info as LnPaymentInfo).pr; + const { sections } = decodeBolt11(bolt11); + for (const section of sections) { + if (section.name === "amount") { + const { value } = section; + return Number(value); + } + } + // stab + return 1; + } else { + return (this.info as NutPayment).amount; + } + } + + public async pay() { + if (this.type === "ln") { + return this.payLn(); + } else { + return this.payNut(); + } + } + + public payNut = payNut.bind(this); + public payLn = payLn.bind(this); +} + +/** + * Finds mints in common in the intersection of the arrays of mints + * @example + * const user1Mints = ["mint1", "mint2"]; + * const user2Mints = ["mint2", "mint3"]; + * const user3Mints = ["mint1", "mint2"]; + * + * findMintsInCommon([user1Mints, user2Mints, user3Mints]); + * + * // returns ["mint2"] + */ +export function findMintsInCommon(mintCollections: string[][]) { + const mintCounts = new Map(); + + for (const mints of mintCollections) { + for (const mint of mints) { + if (!mintCounts.has(mint)) { + mintCounts.set(mint, 1); + } else { + mintCounts.set(mint, mintCounts.get(mint)! + 1); + } + } + } + + const commonMints: string[] = []; + for (const [mint, count] of mintCounts.entries()) { + if (count === mintCollections.length) { + commonMints.push(mint); + } + } + + return commonMints; +} diff --git a/ndk-wallet/src/cashu/pay/ln.ts b/ndk-wallet/src/cashu/pay/ln.ts new file mode 100644 index 00000000..b9af24ca --- /dev/null +++ b/ndk-wallet/src/cashu/pay/ln.ts @@ -0,0 +1,105 @@ +import { CashuWallet, CashuMint } from "@cashu/cashu-ts"; +import type { LnPaymentInfo } from "@nostr-dev-kit/ndk"; +import type { NDKCashuPay } from "../pay"; +import type { TokenSelection} from "../proofs"; +import { rollOverProofs, chooseProofsForPr } from "../proofs"; +import type { MintUrl } from "../mint/utils"; + +/** + * + * @param useMint Forces the payment to use a specific mint + * @returns + */ +export async function payLn(this: NDKCashuPay, useMint?: MintUrl): Promise { + const mintBalances = this.wallet.mintBalances; + const selections: TokenSelection[] = []; + let amount = this.getAmount(); + const data = this.info as LnPaymentInfo; + if (!data.pr) throw new Error("missing pr"); + + amount /= 1000; // convert msat to sat + + let paid = false; + let processingSelections = false; + const processSelections = async (resolve: (value: string) => void, reject: (err: string) => void) => { + processingSelections = true; + for (const selection of selections) { + const _wallet = new CashuWallet(new CashuMint(selection.mint)); + this.debug( + "paying LN invoice for %d sats (%d in fees) with proofs %o, %s", + selection.quote!.amount, + selection.quote!.fee_reserve, + selection.usedProofs, + data.pr + ); + try { + const result = await _wallet.payLnInvoice( + data.pr, + selection.usedProofs, + selection.quote + ); + this.debug("payment result: %o", result); + + if (result.isPaid && result.preimage) { + this.debug("payment successful"); + rollOverProofs(selection, result.change, selection.mint, this.wallet); + paid = true; + resolve(result.preimage); + } + } catch (e) { + this.debug("failed to pay with mint %s", e.message); + if (e?.message.match(/already spent/i)) { + this.debug("proofs already spent, rolling over"); + rollOverProofs(selection, [], selection.mint, this.wallet); + } + } + } + + if (!paid) { + reject("failed to pay with any mint"); + } + + processingSelections = false; + }; + + return new Promise((resolve, reject) => { + let foundMint = false; + + for (const [mint, balance] of Object.entries(mintBalances)) { + if (useMint && mint !== useMint) continue; + + if (balance < amount) { + this.debug("mint %s has insufficient balance %d", mint, balance, amount); + + if (useMint) { + reject(`insufficient balance in mint ${mint} (${balance} < ${amount})`); + return; + } + + continue; + } + + foundMint = true; + + chooseProofsForPr(data.pr, mint, this.wallet).then(async (result) => { + if (result) { + this.debug("successfully chose proofs for mint %s", mint); + selections.push(result); + if (!processingSelections) { + this.debug("processing selections"); + await processSelections(resolve, reject); + } + } + }); + } + + this.debug({foundMint}) + + if (!foundMint) { + this.wallet.emit("insufficient_balance", {amount, pr: data.pr}); + + this.debug("no mint with sufficient balance found"); + reject("no mint with sufficient balance found"); + } + }); +} diff --git a/ndk-wallet/src/cashu/pay/nut.ts b/ndk-wallet/src/cashu/pay/nut.ts new file mode 100644 index 00000000..c36188f6 --- /dev/null +++ b/ndk-wallet/src/cashu/pay/nut.ts @@ -0,0 +1,140 @@ +import type { Proof } from "@cashu/cashu-ts"; +import { CashuMint, CashuWallet } from "@cashu/cashu-ts"; +import type { MintUrl } from "../mint/utils"; +import type { NDKCashuPay } from "../pay"; +import { findMintsInCommon } from "../pay"; +import { chooseProofsForAmount, rollOverProofs } from "../proofs"; + +export type NutPayment = { amount: number; unit: string; mints: MintUrl[]; p2pk?: string }; + +/** + * Generates proof to satisfy a payment. + * Note that this function doesn't send the proofs to the recipient. + */ +export async function payNut( + this: NDKCashuPay +): Promise<{ proofs: Proof[]; mint: MintUrl }> { + const data = this.info as NutPayment; + if (!data.mints) throw new Error("missing mints"); + + const recipientMints = data.mints; + const senderMints = this.wallet.mints; + + const mintsInCommon = findMintsInCommon([recipientMints, senderMints]); + + this.debug( + "mints in common %o, recipient %o, sender %o", + mintsInCommon, + recipientMints, + senderMints + ); + + if (mintsInCommon.length > 0) { + try { + const res = await payNutWithMintBalance(this, mintsInCommon); + return res; + } catch (e) { + this.debug("failed to pay with mints in common: %s %o", e.message, mintsInCommon); + } + } else { + this.debug("no mints in common between sender and recipient"); + } + + return await payNutWithMintTransfer(this); +} + +async function payNutWithMintTransfer( + pay: NDKCashuPay +): Promise<{ proofs: Proof[]; mint: MintUrl }> { + const quotes = []; + const { mints, p2pk } = pay.info as NutPayment; + const amount = pay.getAmount(); + + // get quotes from the mints the recipient has + const quotesPromises = mints.map(async (mint) => { + const wallet = new CashuWallet(new CashuMint(mint), { unit: pay.unit }); + const quote = await wallet.mintQuote(amount); + return { quote, mint }; + }); + + // get the first quote that is successful + const { quote, mint } = await Promise.any(quotesPromises); + + if (!quote) { + pay.debug("failed to get quote from any mint"); + throw new Error("failed to get quote from any mint"); + } + + pay.debug("quote from mint %s: %o", mint, quote); + + const res = await pay.wallet.lnPay({pr: quote.request }); + pay.debug("payment result: %o", res); + + if (!res) { + pay.debug("payment failed"); + throw new Error("payment failed"); + } + + const wallet = new CashuWallet(new CashuMint(mint), { unit: pay.unit }); + + const { proofs } = await wallet.mintTokens(amount, quote.quote, { + pubkey: p2pk, + }); + + pay.debug("minted tokens with proofs %o", proofs); + + return { proofs, mint }; +} + +async function payNutWithMintBalance( + pay: NDKCashuPay, + mints: string[] +): Promise<{ proofs: Proof[]; mint: MintUrl }> { + const { amount, p2pk } = pay.info as NutPayment; + + const mintsWithEnoughBalance = mints.filter((mint) => { + pay.debug("checking mint %s, balance %d", mint, pay.wallet.mintBalances[mint]); + return pay.wallet.mintBalances[mint] >= amount; + }); + + pay.debug("mints with enough balance %o", mintsWithEnoughBalance); + + if (mintsWithEnoughBalance.length === 0) { + pay.debug("no mints with enough balance to satisfy amount %d", amount); + throw new Error("insufficient balance"); + } + + for (const mint of mintsWithEnoughBalance) { + const _wallet = new CashuWallet(new CashuMint(mint)); + const selection = chooseProofsForAmount(amount, mint, pay.wallet); + + if (!selection) { + pay.debug("failed to find proofs for amount %d", amount); + throw new Error("insufficient balance"); + continue; + } + + try { + const res = await _wallet.send(amount, selection.usedProofs, { + pubkey: p2pk, + }); + pay.debug("payment result: %o", res); + + rollOverProofs(selection, res.returnChange, mint, pay.wallet); + + return { proofs: res.send, mint }; + } catch (e: any) { + pay.debug( + "failed to pay with mint %s using proofs %o: %s", + mint, + selection.usedProofs, + e.message + ); + rollOverProofs(selection, [], mint, pay.wallet); + throw new Error("failed to pay with mint " + e?.message); + } + } + + pay.debug("failed to pay with any mint"); + throw new Error("failed to find a mint with enough balance"); +} diff --git a/ndk-wallet/src/cashu/proofs.ts b/ndk-wallet/src/cashu/proofs.ts new file mode 100644 index 00000000..ef3cfe0f --- /dev/null +++ b/ndk-wallet/src/cashu/proofs.ts @@ -0,0 +1,171 @@ +import type { MeltQuoteResponse, Proof } from "@cashu/cashu-ts"; +import { CashuMint, CashuWallet } from "@cashu/cashu-ts"; +import type { NDKCashuWallet } from "./wallet"; +import { NDKCashuToken } from "./token"; +import createDebug from "debug"; +import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk"; + +const d = createDebug("ndk-wallet:cashu:proofs"); + +export type TokenSelection = { + usedProofs: Proof[]; + movedProofs: Proof[]; + usedTokens: NDKCashuToken[]; + quote?: MeltQuoteResponse; + mint: string; +}; + +/** + * Gets a melt quote from a payment request for a mint and tries to get + * proofs that satisfy the amount. + * @param pr + * @param mint + * @param wallet + */ +export async function chooseProofsForPr( + pr: string, + mint: string, + wallet: NDKCashuWallet +): Promise { + const _wallet = new CashuWallet(new CashuMint(mint)); + const quote = await _wallet.meltQuote(pr); + return chooseProofsForQuote(quote, wallet, mint); +} + +export function chooseProofsForAmount( + amount: number, + mint: string, + wallet: NDKCashuWallet +): TokenSelection | undefined { + const mintTokens = wallet.mintTokens[mint]; + let remaining = amount; + const usedProofs: Proof[] = []; + const movedProofs: Proof[] = []; + const usedTokens: NDKCashuToken[] = []; + + if (!mintTokens) { + d("unexpected missing array of tokens for mint %s", mint); + return; + } + + for (const token of mintTokens) { + if (remaining <= 0) break; + + let tokenUsed = false; + for (const proof of token.proofs) { + if (remaining > 0) { + usedProofs.push(proof); + remaining -= proof.amount; + d( + "%s adding proof for amount %d, with %d remaining of total required", + mint, + proof.amount, + remaining, + amount + ); + tokenUsed = true; + } else { + movedProofs.push(proof); + } + } + + if (tokenUsed) { + usedTokens.push(token); + } + } + + if (remaining > 0) { + d( + "insufficient tokens to satisfy amount %d, mint %s had %d", + amount, + mint, + amount - remaining + ); + return; + } + + d( + "%s mint, used %d proofs and %d tokens to satisfy amount %d", + mint, + usedProofs.length, + usedTokens.length, + amount + ); + + return { usedProofs, movedProofs, usedTokens, mint }; +} + +function chooseProofsForQuote( + quote: MeltQuoteResponse, + wallet: NDKCashuWallet, + mint: string +): TokenSelection | undefined { + const amount = quote.amount + quote.fee_reserve; + + d("quote for mint %s is %o", mint, quote); + + const res = chooseProofsForAmount(amount, mint, wallet); + if (!res) { + d("failed to find proofs for amount %d", amount); + return; + } + + return { ...res, quote }; +} + +/** + * Deletes and creates new events to reflect the new state of the proofs + */ +export async function rollOverProofs( + proofs: TokenSelection, + changes: Proof[], + mint: string, + wallet: NDKCashuWallet +) { + const relaySet = wallet.relaySet; + + if (proofs.usedTokens.length > 0) { + console.trace("rolling over proofs for mint %s %d tokens", mint, proofs.usedTokens.length); + + const deleteEvent = new NDKEvent(wallet.ndk); + deleteEvent.kind = NDKKind.EventDeletion; + deleteEvent.tags = [["k", NDKKind.CashuToken.toString()]]; + + proofs.usedTokens.forEach((token) => { + d( + "adding to delete a token that was seen on relay %s %o", + token.relay?.url, + token.onRelays + ); + deleteEvent.tag(["e", token.id]); + if (token.relay) relaySet?.addRelay(token.relay); + }); + + await deleteEvent.sign(); + d("delete event %o sending to %s", deleteEvent.rawEvent(), relaySet?.relayUrls); + deleteEvent.publish(relaySet); + } + wallet.addUsedTokens(proofs.usedTokens); + + const proofsToSave = proofs.movedProofs; + for (const change of changes) { + proofsToSave.push(change); + } + + if (proofsToSave.length === 0) { + d("no new proofs to save"); + return; + } + + const tokenEvent = new NDKCashuToken(wallet.ndk); + tokenEvent.proofs = proofsToSave; + tokenEvent.mint = mint; + tokenEvent.wallet = wallet; + await tokenEvent.sign(); + d("saving %d new proofs", proofsToSave.length); + + wallet.addToken(tokenEvent); + + tokenEvent.publish(wallet.relaySet); + d("created new token event", tokenEvent.rawEvent()); +} diff --git a/ndk-wallet/src/cashu/send.ts b/ndk-wallet/src/cashu/send.ts new file mode 100644 index 00000000..0e806c87 --- /dev/null +++ b/ndk-wallet/src/cashu/send.ts @@ -0,0 +1,6 @@ +import { EventEmitter } from "tseep"; +import type { NDKCashuWallet } from "./wallet"; + +class NDKCashuSend extends EventEmitter { + constructor(wallet: NDKCashuWallet, target: NDKUser | NDKEvent); +} diff --git a/ndk-wallet/src/cashu/token.ts b/ndk-wallet/src/cashu/token.ts new file mode 100644 index 00000000..1a93ed96 --- /dev/null +++ b/ndk-wallet/src/cashu/token.ts @@ -0,0 +1,112 @@ +import type { MintKeys} from "@cashu/cashu-ts"; +import { type Proof } from "@cashu/cashu-ts"; +import type { NDKRelay, NDKRelaySet, NostrEvent } from "@nostr-dev-kit/ndk"; +import type NDK from "@nostr-dev-kit/ndk"; +import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk"; +import type { NDKCashuWallet } from "./wallet"; + +export function proofsTotalBalance(proofs: Proof[]): number { + for (const proof of proofs) { + if (proof.amount < 0) { + throw new Error("proof amount is negative"); + } + } + + return proofs.reduce((acc, proof) => acc + proof.amount, 0); +} + +export class NDKCashuToken extends NDKEvent { + public proofs: Proof[] = []; + private original: NDKEvent | undefined; + + constructor(ndk?: NDK, event?: NostrEvent | NDKEvent) { + super(ndk, event); + this.kind ??= NDKKind.CashuToken; + } + + static async from(event: NDKEvent): Promise { + const token = new NDKCashuToken(event.ndk, event); + + token.original = event; + try { + await token.decrypt(); + } catch { + token.content = token.original.content; + } + + try { + const content = JSON.parse(token.content); + token.proofs = content.proofs; + if (!Array.isArray(token.proofs)) return; + } catch (e) { + return; + } + + return token; + } + + async toNostrEvent(pubkey?: string): Promise { + this.content = JSON.stringify({ + proofs: this.proofs, + }); + + const user = await this.ndk!.signer!.user(); + await this.encrypt(user); + + return super.toNostrEvent(pubkey); + } + + get walletId(): string | undefined { + const aTag = this.tags.find(([tag]) => tag === "a"); + if (!aTag) return; + return aTag[1]?.split(":")[2]; + } + + set wallet(wallet: NDKCashuWallet) { + this.tags.push(["a", wallet.tagId()]); + } + + set mint(mint: string) { + this.removeTag("mint"); + this.tags.push(["mint", mint]); + } + + get mint(): string | undefined { + return this.tagValue("mint"); + } + + get amount(): number { + return proofsTotalBalance(this.proofs); + } + + public async publish( + relaySet?: NDKRelaySet, + timeoutMs?: number, + requiredRelayCount?: number + ): Promise> { + if (this.original) { + return this.original.publish(relaySet, timeoutMs, requiredRelayCount); + } else { + return super.publish(relaySet, timeoutMs, requiredRelayCount); + } + } +} + +export class NDKCashuWalletKey extends NDKEvent { + constructor(ndk?: NDK, event?: NostrEvent) { + super(ndk, event); + this.kind ??= 37376; + } + + set keys(payload: MintKeys) { + this.content = JSON.stringify(payload); + } + + get keys(): MintKeys { + return JSON.parse(this.content); + } + + set wallet(wallet: NDKCashuWallet) { + this.dTag = wallet.dTag; + } +} diff --git a/ndk-wallet/src/cashu/validate.ts b/ndk-wallet/src/cashu/validate.ts new file mode 100644 index 00000000..53c36dfb --- /dev/null +++ b/ndk-wallet/src/cashu/validate.ts @@ -0,0 +1,111 @@ +import type { Proof } from "@cashu/cashu-ts"; +import { CashuMint, CashuWallet } from "@cashu/cashu-ts"; +import type { NDKCashuToken } from "./token"; +import createDebug from "debug"; +import { rollOverProofs } from "./proofs"; +import type { NDKCashuWallet } from "./wallet"; + +const d = createDebug("ndk-wallet:cashu:validate"); + +function checkInvalidToken(token: NDKCashuToken, spentProofsSet: Set) { + const unspentProofs: Proof[] = []; + const spentProofs: Proof[] = []; + let dirty = false; + + if (token.proofs.length === 0) { + d("token %s has no proofs", token.id); + return { dirty: true, unspentProofs, spentProofs }; + } + + for (const proof of token.proofs) { + if (spentProofsSet.has(proof.id)) { + dirty = true; + spentProofs.push(proof); + } else { + unspentProofs.push(proof); + } + } + + return { dirty, unspentProofs, spentProofs }; +} + +export async function checkTokenProofs(this: NDKCashuWallet, tokens?: NDKCashuToken[]) { + if (!tokens) { + tokens = this.tokens; + } + + d("checking %d tokens for spent proofs", tokens.length); + + const mints = new Set(tokens.map((t) => t.mint).filter((mint) => !!mint)); + + d("found %d mints", mints.size); + + mints.forEach((mint) => { + checkTokenProofsForMint( + mint!, + tokens.filter((t) => t.mint === mint), + this + ); + }); +} + +export async function checkTokenProofsForMint( + mint: string, + tokens: NDKCashuToken[], + wallet: NDKCashuWallet +) { + const allProofs = tokens.map((t) => t.proofs).flat(); + const _wallet = new CashuWallet(new CashuMint(mint)); + d( + "checking %d proofs in %d tokens for spent proofs for mint %s", + allProofs.length, + tokens.length, + mint + ); + const spentProofs = await _wallet.checkProofsSpent(allProofs); + d("found %d spent proofs for mint %s", spentProofs.length, mint); + + for (const spent of spentProofs) { + d("%s: spent proof %s", mint, spent.id); + } + + const spentProofsSet = new Set(spentProofs.map((p) => p.id)); + const tokensToDestroy: NDKCashuToken[] = []; + const proofsToSave: Proof[] = []; + + for (const token of tokens) { + const { dirty, unspentProofs, spentProofs } = checkInvalidToken(token, spentProofsSet); + if (dirty) { + tokensToDestroy.push(token); + proofsToSave.push(...unspentProofs); + console.log( + "👉 token has spent proofs", + spentProofs.map((p) => p.id), + token.rawEvent(), + token.onRelays, + token.relay + ); + } + } + + d( + "destroying %d tokens with %dspent proofs, moving %d proofs", + tokensToDestroy.length, + spentProofs.length, + proofsToSave.length + ); + + rollOverProofs( + { + usedProofs: spentProofs, + movedProofs: proofsToSave, + usedTokens: tokensToDestroy, + mint, + }, + [], + mint, + wallet + ); + + return spentProofs; +} diff --git a/ndk-wallet/src/cashu/wallet.ts b/ndk-wallet/src/cashu/wallet.ts new file mode 100644 index 00000000..53dd506e --- /dev/null +++ b/ndk-wallet/src/cashu/wallet.ts @@ -0,0 +1,440 @@ +import type { + CashuPaymentInfo, + NDKEventId, + NDKPaymentConfirmationCashu, + NDKPaymentConfirmationLN, + NDKRelay, + NDKTag, + NDKZapDetails} from "@nostr-dev-kit/ndk"; +import type NDK from "@nostr-dev-kit/ndk"; +import { + NDKEvent, + NDKKind, + NDKPrivateKeySigner, + NDKRelaySet +} from "@nostr-dev-kit/ndk"; +import type { NostrEvent } from "@nostr-dev-kit/ndk"; +import { NDKCashuToken, proofsTotalBalance } from "./token.js"; +import { NDKCashuDeposit } from "./deposit.js"; +import createDebug from "debug"; +import type { MintUrl } from "./mint/utils.js"; +import { NDKCashuPay } from "./pay.js"; +import type { Proof } from "@cashu/cashu-ts"; +import { CashuMint, CashuWallet } from "@cashu/cashu-ts"; +import { NDKWalletChange } from "./history.js"; +import { checkTokenProofs } from "./validate.js"; + +const d = createDebug("ndk-wallet:cashu:wallet"); + +export enum NDKCashuWalletState { + /** + * The wallet tokens are being loaded. + * Queried balance will come from the wallet event cache + */ + LOADING = "loading", + + /** + * Token have completed loading. + * Balance will come from the computed balance from known tokens + */ + READY = "ready", +} + +export class NDKCashuWallet extends NDKEvent { + public tokens: NDKCashuToken[] = []; + public usedTokenIds = new Set(); + private knownTokens: Set = new Set(); + private skipPrivateKey: boolean = false; + public p2pk: string | undefined; + + public state: NDKCashuWalletState = NDKCashuWalletState.LOADING; + + static kind = NDKKind.CashuWallet; + static kinds = [NDKKind.CashuWallet]; + + public privateTags: NDKTag[] = []; + public publicTags: NDKTag[] = []; + + constructor(ndk?: NDK, event?: NostrEvent) { + super(ndk, event); + this.kind ??= NDKKind.CashuWallet; + } + + get walletId(): string { + return this.dTag!; + } + + set walletId(id: string) { + this.dTag = id; + } + + /** + * Returns the tokens that are available for spending + */ + get availableTokens(): NDKCashuToken[] { + return this.tokens.filter((t) => !this.usedTokenIds.has(t.id)); + } + + /** + * Adds a token to the list of used tokens + * to make sure it's proofs are no longer available + */ + public addUsedTokens(token: NDKCashuToken[]) { + for (const t of token) { + this.usedTokenIds.add(t.id); + } + this.emit("balance"); + } + + public checkProofs = checkTokenProofs.bind(this); + + static async from(event: NDKEvent): Promise { + const wallet = new NDKCashuWallet(event.ndk, event.rawEvent() as NostrEvent); + // if (wallet.isDeleted) return; + + const prevContent = wallet.content; + wallet.publicTags = wallet.tags; + try { + await wallet.decrypt(); + + wallet.privateTags = JSON.parse(wallet.content); + } catch (e) { + d("unable to decrypt wallet", e); + } + wallet.content ??= prevContent; + + await wallet.getP2pk(); + + console.log("wallet from event", { + privateTags: wallet.privateTags, + publicTags: wallet.publicTags, + }); + + return wallet; + } + + get allTags(): NDKTag[] { + return this.privateTags.concat(this.publicTags); + } + + private setPrivateTag(name: string, value: string | string[]) { + this.privateTags = this.privateTags.filter((t) => t[0] !== name); + if (Array.isArray(value)) { + for (const v of value) { + this.privateTags.push([name, v]); + } + } else { + this.privateTags.push([name, value]); + } + } + + private getPrivateTags(name: string): string[] { + return this.privateTags.filter((t) => t[0] === name).map((t) => t[1]); + } + + private getPrivateTag(name: string): string | undefined { + return this.privateTags.find((t) => t[0] === name)?.[1]; + } + + private setPublicTag(name: string, value: string | string[]) { + this.publicTags = this.publicTags.filter((t) => t[0] !== name); + if (Array.isArray(value)) { + for (const v of value) { + this.publicTags.push([name, v]); + } + } else { + this.publicTags.push([name, value]); + } + } + + private getPublicTags(name: string): string[] { + return this.publicTags.filter((t) => t[0] === name).map((t) => t[1]); + } + + set relays(urls: WebSocket["url"][]) { + this.setPrivateTag("relay", urls); + } + + get relays(): WebSocket["url"][] { + return this.getPrivateTags("relay"); + } + + set mints(urls: string[]) { + this.setPublicTag("mint", urls); + } + + get mints(): string[] { + return this.getPublicTags("mint"); + } + + set name(value: string) { + this.setPrivateTag("name", value); + } + + get name(): string | undefined { + return this.getPrivateTag("name"); + } + + get unit(): string { + return this.getPrivateTag("unit") ?? "sats"; + } + + set unit(unit: string) { + this.setPrivateTag("unit", unit); + } + + async getP2pk(): Promise { + if (this.p2pk) return this.p2pk; + if (this.privkey) { + const signer = new NDKPrivateKeySigner(this.privkey); + const user = await signer.user(); + this.p2pk = user.pubkey; + return this.p2pk; + } + } + + get privkey(): string | undefined { + const privkey = this.getPrivateTag("privkey"); + if (privkey) return privkey; + + if (this.ndk?.signer instanceof NDKPrivateKeySigner) { + return this.ndk.signer.privateKey; + } + } + + set privkey(privkey: string | undefined | false) { + if (privkey) { + this.setPrivateTag("privkey", privkey ?? false); + } else { + this.skipPrivateKey = privkey === false; + this.p2pk = undefined; + } + } + + /** + * Whether this wallet has been deleted + */ + get isDeleted(): boolean { + return this.tags.some((t) => t[0] === "deleted"); + } + + async toNostrEvent(pubkey?: string): Promise { + if (this.isDeleted) + return super.toNostrEvent(pubkey) as unknown as NostrEvent; + + // if we haven't been instructed to skip the private key + // and we don't have one, generate it + if (!this.skipPrivateKey && !this.privkey) { + const signer = NDKPrivateKeySigner.generate(); + this.privkey = signer.privateKey; + } + + // set the tags to the public tags + this.tags = this.publicTags; + + // ensure we don't have a privkey in the public tags + for (const tag of this.tags) { + if (tag[0] === "privkey") { + throw new Error("privkey should not be in public tags!"); + } + } + + // encrypt private tags + this.content = JSON.stringify(this.privateTags); + const user = await this.ndk!.signer!.user(); + await this.encrypt(user); + + return super.toNostrEvent(pubkey) as unknown as NostrEvent; + } + + get relaySet(): NDKRelaySet | undefined { + if (this.relays.length === 0) return undefined; + + return NDKRelaySet.fromRelayUrls(this.relays, this.ndk!); + } + + public deposit(amount: number, mint?: string, unit?: string): NDKCashuDeposit { + const deposit = new NDKCashuDeposit(this, amount, mint, unit); + deposit.on("success", (token) => { + this.tokens.push(token); + this.knownTokens.add(token.id); + this.emit("update"); + }); + return deposit; + } + + async lnPay({pr}: {pr:string}, useMint?: MintUrl): Promise { + const pay = new NDKCashuPay(this, { pr }); + const preimage = await pay.payLn(useMint); + if (!preimage) return; + return {preimage}; + } + + /** + * Swaps tokens to a specific amount, optionally locking to a p2pk. + * @param amount + */ + async cashuPay(payment: NDKZapDetails): Promise { + const { amount, unit, mints, p2pk } = payment; + const pay = new NDKCashuPay(this, { amount, unit, mints, p2pk }); + return pay.payNut(); + } + + async redeemNutzap(nutzap: NDKEvent) { + this.emit("nutzap:seen", nutzap); + + try { + const mint = nutzap.tagValue("u"); + if (!mint) throw new Error("missing mint"); + const proofs = JSON.parse(nutzap.content); + console.log(proofs); + + const _wallet = new CashuWallet(new CashuMint(mint)); + const res = await _wallet.receiveTokenEntry( + { proofs, mint }, + { + privkey: this.privkey, + } + ); + + if (res) { + this.emit("nutzap:redeemed", nutzap); + } + + const tokenEvent = new NDKCashuToken(this.ndk); + tokenEvent.proofs = proofs; + tokenEvent.mint = mint; + tokenEvent.wallet = this; + await tokenEvent.sign(); + tokenEvent.publish(this.relaySet); + console.log("new token event", tokenEvent.rawEvent()); + + const historyEvent = new NDKWalletChange(this.ndk); + historyEvent.addRedeemedNutzap(nutzap); + historyEvent.tag(this); + historyEvent.tag(tokenEvent, NDKWalletChange.MARKERS.CREATED); + await historyEvent.sign(); + historyEvent.publish(this.relaySet); + } catch (e) { + console.trace(e); + this.emit("nutzap:failed", nutzap, e); + } + } + + /** + * Generates a new token event with proofs to be stored for this wallet + * @param proofs Proofs to be stored + * @param mint Mint URL + * @param nutzap Nutzap event if these proofs are redeemed from a nutzap + * @returns + */ + async saveProofs(proofs: Proof[], mint: MintUrl, nutzap?: NDKEvent) { + const tokenEvent = new NDKCashuToken(this.ndk); + tokenEvent.proofs = proofs; + tokenEvent.mint = mint; + tokenEvent.wallet = this; + await tokenEvent.sign(); + + // we can add it to the wallet here + this.addToken(tokenEvent); + + tokenEvent.publish(this.relaySet).catch((e) => { + console.error("failed to publish token", e, tokenEvent.rawEvent()); + }); + + if (nutzap) { + const historyEvent = new NDKWalletChange(this.ndk); + historyEvent.addRedeemedNutzap(nutzap); + historyEvent.tag(this); + historyEvent.tag(tokenEvent, NDKWalletChange.MARKERS.CREATED); + await historyEvent.sign(); + historyEvent.publish(this.relaySet); + } + + return tokenEvent; + } + + public addToken(token: NDKCashuToken) { + if (!this.knownTokens.has(token.id)) { + this.knownTokens.add(token.id); + this.tokens.push(token); + this.emit("balance"); + } + } + + /** + * Removes a token that has been deleted + */ + public removeTokenId(id: NDKEventId) { + if (!this.knownTokens.has(id)) return false; + + this.tokens = this.tokens.filter((t) => t.id !== id); + this.emit("balance"); + } + + async delete(reason?: string, publish = true): Promise { + this.content = ""; + this.tags = [["d", this.dTag!], ["deleted"]]; + if (publish) this.publishReplaceable(); + + return super.delete(reason, publish); + } + + /** + * Gets all tokens, grouped by mint + */ + get mintTokens(): Record { + const tokens: Record = {}; + + for (const token of this.tokens) { + if (token.mint) { + tokens[token.mint] ??= []; + tokens[token.mint].push(token); + } + } + + return tokens; + } + + get balance(): number | undefined { + if (this.state === NDKCashuWalletState.LOADING) { + const balance = this.getPrivateTag("balance"); + if (balance) return Number(balance); + else return undefined; + } + + // aggregate all token balances + return proofsTotalBalance(this.tokens.map((t) => t.proofs).flat()); + } + + /** + * Writes the wallet balance to relays + */ + async updateBalance() { + this.setPrivateTag("balance", this.balance?.toString() ?? "0"); + d("publishing balance (%d)", this.balance); + this.publishReplaceable(this.relaySet); + } + + public mintBalance(mint: MintUrl) { + return proofsTotalBalance( + this.tokens + .filter((t) => t.mint === mint) + .map((t) => t.proofs) + .flat() + ); + } + + get mintBalances(): Record { + const balances: Record = {}; + + for (const token of this.tokens) { + if (token.mint) { + balances[token.mint] ??= 0; + balances[token.mint] += token.amount; + } + } + + return balances; + } +} diff --git a/ndk-wallet/src/index.ts b/ndk-wallet/src/index.ts new file mode 100644 index 00000000..8e383916 --- /dev/null +++ b/ndk-wallet/src/index.ts @@ -0,0 +1,7 @@ +import NDKWallet from "./wallet"; +export * from "./cashu/wallet.js"; +export * from "./cashu/token.js"; +export * from "./cashu/deposit.js"; +export * from "./cashu/mint/utils"; + +export default NDKWallet; diff --git a/ndk-wallet/src/wallet/index.ts b/ndk-wallet/src/wallet/index.ts new file mode 100644 index 00000000..e74f59fa --- /dev/null +++ b/ndk-wallet/src/wallet/index.ts @@ -0,0 +1,109 @@ +import type { NDKNutzap, NDKUser } from "@nostr-dev-kit/ndk"; +import type NDK from "@nostr-dev-kit/ndk"; +import { NDKCashuMintList } from "@nostr-dev-kit/ndk"; +import { NDKCashuWallet } from "../cashu/wallet.js"; +import { EventEmitter } from "tseep"; +import createDebug from "debug"; +import NDKWalletLifecycle from "./lifecycle/index.js"; +import type { MintUrl } from "../cashu/mint/utils.js"; +import type { NDKCashuToken } from "../cashu/token.js"; + +const d = createDebug("ndk-wallet:wallet"); + +class NDKWallet extends EventEmitter<{ + /** + * New default was has been established + * @param wallet + */ + "wallet:default": (wallet: NDKCashuWallet) => void; + mintlist: (mintList: NDKCashuMintList) => void; + wallet: (wallet: NDKCashuWallet) => void; + wallets: () => void; + "wallet:balance": (wallet: NDKCashuWallet) => void; + + "nutzap:seen": (nutzap: NDKNutzap) => void; + "nutzap:redeemed": (nutzap: NDKNutzap) => void; + "nutzap:failed": (nutzap: NDKNutzap) => void; + + ready: () => void; +}> { + public ndk: NDK; + + private lifecycle: NDKWalletLifecycle | undefined; + + constructor(ndk: NDK) { + super(); + this.ndk = ndk; + } + + get state() { + return this.lifecycle?.state ?? "loading"; + } + + public createCashuWallet() { + return new NDKCashuWallet(this.ndk); + } + + /** + * Starts monitoring changes for the user's wallets + */ + public start(user?: NDKUser) { + this.lifecycle = new NDKWalletLifecycle(this, this.ndk, user ?? this.ndk.activeUser!); + this.lifecycle.start(); + } + + /** + * Publishes the mint list tying to a specific wallet + */ + async setMintList(wallet: NDKCashuWallet) { + const mintList = new NDKCashuMintList(this.ndk); + mintList.relays = wallet.relays; + mintList.mints = wallet.mints; + mintList.p2pk = await wallet.getP2pk(); + return mintList.publish(); + } + + /** + * Get a list of the current wallets of this user. + */ + get wallets(): NDKCashuWallet[] { + if (!this.lifecycle) return []; + return Array.from(this.lifecycle.wallets.values()); + } + + async transfer(wallet: NDKCashuWallet, fromMint: MintUrl, toMint: MintUrl) { + const balanceInMint = wallet.mintBalance(fromMint); + + if (balanceInMint < 4) { + throw new Error("insufficient balance in mint:" + fromMint); + } + + const deposit = wallet.deposit(balanceInMint - 3, toMint); + + return new Promise(async (resolve, reject) => { + d("starting deposit from %s to %s", fromMint, toMint); + deposit.on("success", (token: NDKCashuToken) => { + d("deposit success"); + this.emit("wallet:balance", wallet); + resolve(token); + }); + deposit.on("error", (error: string) => { + d("deposit error: %s", error); + reject(error); + }); + + // generate pr + const pr = await deposit.start(); + d("deposit pr: %s", pr); + if (!pr) { + reject("deposit failed to start"); + return; + } + + const paymentRes = await wallet.lnPay({pr}, fromMint); + d("payment result: %o", paymentRes); + }); + } +} + +export default NDKWallet; diff --git a/ndk-wallet/src/wallet/lifecycle/deletion.ts b/ndk-wallet/src/wallet/lifecycle/deletion.ts new file mode 100644 index 00000000..4363d1e5 --- /dev/null +++ b/ndk-wallet/src/wallet/lifecycle/deletion.ts @@ -0,0 +1,14 @@ +import type { NDKEvent } from "@nostr-dev-kit/ndk"; +import type NDKWalletLifecycle from "."; + +export default function handleEventDeletion(this: NDKWalletLifecycle, event: NDKEvent): void { + const deletedIds = event.getMatchingTags("e").map((tag) => tag[1]); + + for (const deletedId of deletedIds) { + if (!this.knownTokens.has(deletedId)) continue; + + this.wallets.forEach((wallet) => { + wallet.removeTokenId(deletedId); + }); + } +} diff --git a/ndk-wallet/src/wallet/lifecycle/index.ts b/ndk-wallet/src/wallet/lifecycle/index.ts new file mode 100644 index 00000000..20586b52 --- /dev/null +++ b/ndk-wallet/src/wallet/lifecycle/index.ts @@ -0,0 +1,218 @@ +import type { + NDKEvent, + NDKEventId, + NDKRelay, + NDKSubscription, + NDKUser} from "@nostr-dev-kit/ndk"; +import type NDK from "@nostr-dev-kit/ndk"; +import { + getRelayListForUser, + NDKCashuMintList, + NDKKind, + NDKRelaySet, + NDKSubscriptionCacheUsage +} from "@nostr-dev-kit/ndk"; +import type NDKWallet from "../index.js"; +import handleMintList from "./mint-list.js"; +import handleWalletEvent from "./wallet.js"; +import handleTokenEvent from "./token.js"; +import handleEventDeletion from "./deletion.js"; +import type { NDKCashuWallet} from "../../cashu/wallet.js"; +import { NDKCashuWalletState } from "../../cashu/wallet.js"; +import type { NDKCashuToken } from "../../cashu/token.js"; +import createDebug from "debug"; +import { NDKWalletChange } from "../../cashu/history.js"; +import NutzapHandler from "./nutzap.js"; + +/** + * This class is responsible for managing the lifecycle of a user wallets. + * It fetches the user wallets, tokens and nutzaps and keeps them up to date. + */ +class NDKWalletLifecycle { + public wallet: NDKWallet; + private sub: NDKSubscription | undefined; + public eosed = false; + public ndk: NDK; + private user: NDKUser; + public _mintList: NDKCashuMintList | undefined; + public wallets = new Map(); + public walletsByP2pk = new Map(); + public defaultWallet: NDKCashuWallet | undefined; + private tokensSub: NDKSubscription | undefined; + public orphanedTokens = new Map(); + public knownTokens = new Set(); + public tokensSubEosed = false; + public debug = createDebug("ndk-wallet:lifecycle"); + public state: "loading" | "ready" = "loading"; + + public nutzap: NutzapHandler; + + constructor(wallet: NDKWallet, ndk: NDK, user: NDKUser) { + this.wallet = wallet; + this.ndk = ndk; + this.user = user; + this.nutzap = new NutzapHandler(this); + } + + async start() { + const userRelayList = await getRelayListForUser(this.user.pubkey, this.ndk); + this.sub = this.ndk.subscribe( + [ + { + kinds: [NDKKind.CashuMintList, NDKKind.CashuWallet], + authors: [this.user.pubkey], + }, + { kinds: [NDKKind.WalletChange], authors: [this.user!.pubkey], limit: 10 }, + ], + { + subId: "ndk-wallet", + groupable: false, + cacheUsage: NDKSubscriptionCacheUsage.PARALLEL, + }, + userRelayList?.relaySet, + false + ); + + this.sub.on("event", this.eventHandler.bind(this)); + this.sub.on("eose", this.eoseHandler.bind(this)); + + this.sub!.start(); + } + + private eventHandler(event: NDKEvent, relay?: NDKRelay) { + switch (event.kind) { + case NDKKind.CashuMintList: + handleMintList.bind(this, NDKCashuMintList.from(event)).call(this); + break; + case NDKKind.CashuWallet: + handleWalletEvent.bind(this, event, relay).call(this); + break; + case NDKKind.CashuToken: + handleTokenEvent.bind(this, event, relay).call(this); + break; + case NDKKind.EventDeletion: + handleEventDeletion.bind(this, event).call(this); + break; + case NDKKind.Nutzap: + this.nutzap.addNutzap(event); + break; + case NDKKind.WalletChange: + NDKWalletChange.from(event).then((wc) => { + if (wc) { + this.nutzap.addWalletChange(wc); + } + }); + } + } + + private eoseHandler() { + this.debug("Loaded wallets", { + defaultWallet: this.defaultWallet?.rawEvent(), + wallets: Array.from(this.wallets.values()).map((w) => w.rawEvent()), + }); + this.eosed = true; + + if (this.tokensSub) { + this.debug("WE ALREADY HAVE TOKENS SUB!!!"); + return; + } + + // if we don't have a default wallet, choose the first one if there is one + const firstWallet = Array.from(this.wallets.values())[0]; + if (!this.defaultWallet && firstWallet) { + this.debug("setting default wallet to first wallet", firstWallet.walletId); + this.setDefaultWallet(undefined, firstWallet); + } + + // get all relay sets + const relaySet = new NDKRelaySet(new Set(), this.ndk); + + for (const wallet of this.wallets.values()) { + for (const relayUrl of wallet.relays) { + const relay = this.ndk.pool.getRelay(relayUrl, true, true); + relaySet.addRelay(relay); + } + } + + let oldestWalletTimestamp = undefined; + for (const wallet of this.wallets.values()) { + if (!oldestWalletTimestamp || wallet.created_at! > oldestWalletTimestamp) { + oldestWalletTimestamp = wallet.created_at!; + this.debug("oldest wallet timestamp", oldestWalletTimestamp); + } + } + + this.debug("oldest wallet timestamp", oldestWalletTimestamp, this.wallets.values()); + + this.tokensSub = this.ndk.subscribe( + [ + { kinds: [NDKKind.CashuToken], authors: [this.user!.pubkey] }, + { kinds: [NDKKind.EventDeletion], authors: [this.user!.pubkey], limit: 0 }, + { kinds: [NDKKind.WalletChange], authors: [this.user!.pubkey] }, + { + kinds: [NDKKind.Nutzap], + "#p": [this.user!.pubkey], + since: oldestWalletTimestamp, + }, + ], + { + subId: "ndk-wallet-tokens2", + groupable: false, + cacheUsage: NDKSubscriptionCacheUsage.ONLY_RELAY, + }, + relaySet, + false + ); + this.tokensSub.on("event", this.eventHandler.bind(this)); + this.tokensSub.on("eose", this.tokensSubEose.bind(this)); + this.tokensSub.start(); + } + + private tokensSubEose() { + this.state = "ready"; + console.log("EMITTING READY"); + this.wallet.emit("ready"); + this.tokensSubEosed = true; + this.nutzap.eosed().then(() => { + // once we finish processing nutzaps + // we can update the wallet's cached balance + for (const wallet of this.wallets.values()) { + wallet.state = NDKCashuWalletState.READY; + wallet.updateBalance(); + } + }); + } + + // private handleMintList = handleMintList.bind + + public emit(event: string, ...args: any[]) { + this.wallet.emit(event as unknown as any, ...args); + } + + // Sets the default wallet as seen by the mint list + public setDefaultWallet(p2pk?: string, wallet?: NDKCashuWallet) { + let w = wallet; + if (!w && p2pk) w = Array.from(this.wallets.values()).find((w) => w.p2pk === p2pk); + + if (w) { + this.defaultWallet = w; + this.emit("wallet:default", w); + } + } + + /** + * Track when is the most recently redeemed nutzap redemption + * so we know when to start processing nutzaps + */ + public latestNutzapRedemptionAt: number = 0; + + public addNutzapRedemption(event: NDKWalletChange) { + console.log("add nutzap redemption", event.created_at); + if (this.latestNutzapRedemptionAt && this.latestNutzapRedemptionAt > event.created_at!) + return; + + this.latestNutzapRedemptionAt = event.created_at!; + } +} + +export default NDKWalletLifecycle; diff --git a/ndk-wallet/src/wallet/lifecycle/mint-list.ts b/ndk-wallet/src/wallet/lifecycle/mint-list.ts new file mode 100644 index 00000000..dd699b11 --- /dev/null +++ b/ndk-wallet/src/wallet/lifecycle/mint-list.ts @@ -0,0 +1,14 @@ +import type { NDKEvent } from "@nostr-dev-kit/ndk"; +import { NDKCashuMintList } from "@nostr-dev-kit/ndk"; +import type NDKWalletLifecycle from "."; + +export default function handleMintList(this: NDKWalletLifecycle, event: NDKEvent) { + if (this._mintList && this._mintList.created_at! >= event.created_at!) return; + + const mintList = NDKCashuMintList.from(event); + const prevPubkey = this._mintList?.p2pk; + this._mintList = mintList; + + if (this.eosed && this._mintList) this.emit("mintlist", this._mintList); + if (this._mintList.p2pk) this.setDefaultWallet(this._mintList.p2pk); +} diff --git a/ndk-wallet/src/wallet/lifecycle/nutzap.ts b/ndk-wallet/src/wallet/lifecycle/nutzap.ts new file mode 100644 index 00000000..5fe619da --- /dev/null +++ b/ndk-wallet/src/wallet/lifecycle/nutzap.ts @@ -0,0 +1,171 @@ +import type { NDKEvent, NDKEventId} from "@nostr-dev-kit/ndk"; +import { NDKNutzap } from "@nostr-dev-kit/ndk"; +import type { NDKWalletChange } from "../../cashu/history"; +import createDebug from "debug"; +import type NDKWalletLifecycle from "."; +import type { Proof } from "@cashu/cashu-ts"; +import { CashuMint, CashuWallet } from "@cashu/cashu-ts"; +import type { NDKCashuWallet } from "../../cashu/wallet"; +import type { MintUrl } from "../../cashu/mint/utils"; + +const d = createDebug("ndk-wallet:lifecycle:nutzap"); + +class NutzapHandler { + private lifecycle: NDKWalletLifecycle; + private _eosed = false; + private redeemQueue = new Map(); + private knownRedeemedTokens = new Set(); + + constructor(lifecycle: NDKWalletLifecycle) { + this.lifecycle = lifecycle; + } + + get wallet() { + return this.lifecycle.wallet; + } + + /** + * Called when a new nutzap needs to be processed + */ + public addNutzap(event: NDKEvent) { + if (!this._eosed) { + this.pushToRedeemQueue(event); + } else { + this.redeem(event); + } + } + + /** + * Called when a wallet change is seen + */ + public addWalletChange(event: NDKWalletChange) { + const redeemedIds = event.getMatchingTags("e").map((t) => t[1]); + redeemedIds.forEach((id) => this.knownRedeemedTokens.add(id)); + } + + async eosed() { + this._eosed = true; + + // start processing queue of nutzaps + await this.processRedeemQueue(); + } + + private pushToRedeemQueue(event: NDKEvent) { + if (this.redeemQueue.has(event.id)) return; + + const nutzap = NDKNutzap.from(event); + if (!nutzap) return; + this.redeemQueue.set(nutzap.id, nutzap); + } + + private async processRedeemQueue() { + // go through knownRedeemedTokens and remove them from the queue + for (const id of this.knownRedeemedTokens) { + this.redeemQueue.delete(id); + } + + // get a list of all the proofs we are going to try to redeem, group them by mint + // then validate that we can redeem them + const mintProofs: Record = {}; + + for (const nutzap of this.redeemQueue.values()) { + const { mint, proofs } = nutzap; + let existingVal = mintProofs[mint] ?? []; + existingVal = existingVal.concat(proofs); + mintProofs[mint] = existingVal; + } + + // talk to each mint + for (const [mint, proofs] of Object.entries(mintProofs)) { + const wallet = this.cashuWallet(mint); + wallet + .checkProofsSpent(proofs) + .then(async (spentProofs) => { + const spentProofSecrets = spentProofs.map((p) => p.secret); + + for (const nutzap of this.redeemQueue.values()) { + if (nutzap.mint === mint) { + const nutzapProofs = nutzap.proofs; + const validProofs = nutzapProofs.filter( + (p) => !spentProofSecrets.includes(p.secret) + ); + if (validProofs.length) { + nutzap.proofs = validProofs; + await this.redeem(nutzap); + } + } + } + }) + .catch((e) => { + console.error(e); + }); + } + } + + private findWalletForNutzap(nutzap: NDKNutzap): NDKCashuWallet | undefined { + const p2pk = nutzap.p2pk; + let wallet: NDKCashuWallet | undefined; + + if (p2pk) wallet = this.lifecycle.walletsByP2pk.get(p2pk); + + return wallet ?? this.lifecycle.defaultWallet; + } + + private cashuWallets: Map = new Map(); + private cashuWallet(mint: MintUrl, unit: string = "sat"): CashuWallet { + const key = `${mint}:${unit}`; + let wallet = this.cashuWallets.get(key); + if (!wallet) { + wallet = new CashuWallet(new CashuMint(mint), { unit }); + this.cashuWallets.set(key, wallet); + } + + return wallet; + } + + private async redeem(event: NDKEvent) { + if (this.knownRedeemedTokens.has(event.id)) return; + this.knownRedeemedTokens.add(event.id); + + const nutzap = await NDKNutzap.from(event); + if (!nutzap) return; + + try { + const { proofs, mint } = nutzap; + const wallet = this.findWalletForNutzap(nutzap); + if (!wallet) { + const p2pk = nutzap.p2pk; + throw new Error( + "wallet not found for nutzap (p2pk: " + p2pk + ") " + nutzap.content + ); + } + + // we emit a nutzap:seen event only once we know that we have the private key to attempt to redeem it + this.lifecycle.emit("nutzap:seen", nutzap); + + const _wallet = this.cashuWallet(mint); + + try { + const res = await _wallet.receiveTokenEntry( + { proofs, mint }, + { + privkey: wallet.privkey, + } + ); + d("redeemed nutzap %o", nutzap.rawEvent()); + this.lifecycle.emit("nutzap:redeemed", nutzap); + + // save new proofs in wallet + wallet.saveProofs(res, mint, nutzap); + } catch (e: any) { + console.error(e.message); + this.lifecycle.emit("nutzap:failed", nutzap, e.message); + } + } catch (e) { + console.trace(e); + this.lifecycle.emit("nutzap:failed", nutzap, e); + } + } +} + +export default NutzapHandler; diff --git a/ndk-wallet/src/wallet/lifecycle/token.ts b/ndk-wallet/src/wallet/lifecycle/token.ts new file mode 100644 index 00000000..a902cddf --- /dev/null +++ b/ndk-wallet/src/wallet/lifecycle/token.ts @@ -0,0 +1,28 @@ +import type { NDKEvent, NDKRelay } from "@nostr-dev-kit/ndk"; +import { NDKCashuToken } from "../../cashu/token"; +import type NDKWalletLifecycle from "."; +import type { NDKCashuWallet } from "../../cashu/wallet"; + +async function handleToken(this: NDKWalletLifecycle, event: NDKEvent, relay?: NDKRelay) { + this.debug("Received token event %s from %s", event.id, relay?.url); + + if (this.knownTokens.has(event.id)) return; + this.knownTokens.add(event.id); + + const token = await NDKCashuToken.from(event); + if (!token) return; + + const walletId = token.walletId; + let wallet: NDKCashuWallet | undefined; + if (walletId) wallet = this.wallets.get(walletId); + wallet ??= this.defaultWallet; + + if (!wallet) { + this.debug("no wallet found for token %s", token.id); + this.orphanedTokens.set(token.id, token); + } else { + wallet.addToken(token); + } +} + +export default handleToken; diff --git a/ndk-wallet/src/wallet/lifecycle/wallet-change.ts b/ndk-wallet/src/wallet/lifecycle/wallet-change.ts new file mode 100644 index 00000000..ca778ec2 --- /dev/null +++ b/ndk-wallet/src/wallet/lifecycle/wallet-change.ts @@ -0,0 +1,15 @@ +import type { NDKEvent } from "@nostr-dev-kit/ndk"; +import { NDKWalletChange } from "../../cashu/history"; +import type NDKWalletLifecycle from "."; + +async function handleWalletChange(this: NDKWalletLifecycle, event: NDKEvent) { + const walletChange = await NDKWalletChange.from(event); + + if (!walletChange) return; + + if (walletChange.hasNutzapRedemption) { + this.addNutzapRedemption(walletChange); + } +} + +export default handleWalletChange; diff --git a/ndk-wallet/src/wallet/lifecycle/wallet.ts b/ndk-wallet/src/wallet/lifecycle/wallet.ts new file mode 100644 index 00000000..802b16ac --- /dev/null +++ b/ndk-wallet/src/wallet/lifecycle/wallet.ts @@ -0,0 +1,97 @@ +import type { NDKEvent, NDKRelay } from "@nostr-dev-kit/ndk"; +import type NDKWalletLifecycle from "."; +import { NDKCashuWallet, NDKCashuWalletState } from "../../cashu/wallet"; + +function removeDeletedWallet(this: NDKWalletLifecycle, walletId: string) { + this.wallets.delete(walletId); + if (this.defaultWallet?.walletId === walletId) this.setDefaultWallet(undefined); + this.emit("wallets"); +} + +const seenWallets: Record = {}; + +async function handleWalletEvent(this: NDKWalletLifecycle, event: NDKEvent, relay?: NDKRelay) { + const wallet = await NDKCashuWallet.from(event); + if (!wallet) { + this.debug("encountered a deleted wallet from %s (%d)", relay?.url, event.created_at); + } + + // check if we already have this dTag + const dTag = event.dTag!; + const existing = seenWallets[dTag]; + if (existing) { + if (wallet) { + this.debug("wallet with privkey %s (%s)", wallet.privkey, wallet.p2pk); + } + existing.events.push(event); + + if (existing.mostRecentTime < event.created_at!) { + this.debug.extend(dTag)( + "Relay %s sent a newer event %d vs %d (%d)", + relay?.url, + existing.mostRecentTime, + event.created_at, + event.created_at! - existing.mostRecentTime + ); + existing.mostRecentTime = event.created_at!; + } else if (existing.mostRecentTime > event.created_at!) { + this.debug.extend(dTag)( + "Relay %s sent an old event %d vs %d (%d)", + relay?.url, + existing.mostRecentTime, + event.created_at, + existing.mostRecentTime - event.created_at! + ); + return; + } else { + return; + } + } else { + this.debug.extend(dTag)("Relay %s sent a new wallet %s", relay?.url, dTag); + seenWallets[dTag] = { + events: [event], + mostRecentTime: event.created_at!, + }; + } + + // const wallet = await NDKCashuWallet.from(event); + + if (!wallet) { + this.debug("wallet deleted", event.dTag); + removeDeletedWallet.bind(this, event.dTag!); + return; + } else { + if (wallet.balance) this.emit("wallet:balance", wallet); + } + + let walletUpdateDebounce: NodeJS.Timeout | undefined; + + wallet.on("balance", () => { + if (wallet.state !== NDKCashuWalletState.READY) return; + + this.emit("wallet:balance", wallet); + + if (walletUpdateDebounce) clearTimeout(walletUpdateDebounce); + walletUpdateDebounce = setTimeout(() => { + wallet.updateBalance(); + }, 5000); + }); + const existingEvent = this.wallets.get(wallet.walletId); + + // always store the wallet by p2pk, even before checking for their created_at + // that way, if a wallet had it's private key replaced, we can still spend from it + const walletP2pk = await wallet.getP2pk(); + if (walletP2pk) { + this.walletsByP2pk.set(walletP2pk, wallet); + } + + // check if this is the most up to date version of this wallet we have + if (existingEvent && existingEvent.created_at! >= wallet.created_at!) return; + + this.wallets.set(wallet.walletId, wallet); + this.emit("wallet", wallet); + if (this._mintList && wallet.p2pk === this._mintList.p2pk) + this.setDefaultWallet(this._mintList.p2pk); +} + +export default handleWalletEvent; diff --git a/ndk-wallet/tsconfig.json b/ndk-wallet/tsconfig.json new file mode 100644 index 00000000..ecc6d9be --- /dev/null +++ b/ndk-wallet/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "@nostr-dev-kit/tsconfig/ndk-cache-redis.json", + "include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.ts"], + "exclude": ["dist", "build", "node_modules"] +} diff --git a/ndk/CHANGELOG.md b/ndk/CHANGELOG.md index 953e7e2b..6b6f62d1 100644 --- a/ndk/CHANGELOG.md +++ b/ndk/CHANGELOG.md @@ -1,5 +1,20 @@ # @nostr-dev-kit/ndk +## 2.10.0 + +### Minor Changes + +- Massive refactor of how subscriptions are fingerprinted, grouped, ungrouped and their internal lifecycle + +### Patch Changes + +- ec83ddc: fix: close subscription on EOSE at the relay level +- 18c55bb: fix bug where queued items were not getting processed (e.g. zap fetches) +- refactor outbox and be smarter abotu the relays we publish to (account for p-tags and relay hints) +- 18c55bb: Breaking change: event.zap is now removed, use ndk.zap(event) instead +- add filterForEventsTaggingId +- 3029124: add methods to access and manage unpublished events from the cache + ## 2.9.1 ### Patch Changes diff --git a/ndk/package.json b/ndk/package.json index 9bc717c6..f5e22315 100644 --- a/ndk/package.json +++ b/ndk/package.json @@ -1,6 +1,6 @@ { "name": "@nostr-dev-kit/ndk", - "version": "2.9.1", + "version": "2.10.0", "description": "NDK - Nostr Development Kit", "homepage": "https://ndk.fyi", "documentation": "https://github.com/nostr-dev-kit/ndk/blob/master/docs/modules.md", @@ -79,7 +79,7 @@ "debug": "^4.3.4", "light-bolt11-decoder": "^3.0.0", "node-fetch": "^3.3.1", - "nostr-tools": "^2.5.2", + "nostr-tools": "^2.7.1", "tseep": "^1.1.1", "typescript-lru-cache": "^2.0.0", "utf8-buffer": "^1.0.0", diff --git a/ndk/src/cache/index.ts b/ndk/src/cache/index.ts index d6442602..a8c27527 100644 --- a/ndk/src/cache/index.ts +++ b/ndk/src/cache/index.ts @@ -3,7 +3,7 @@ import type { NDKRelay } from "../relay/index.js"; import type { NDKFilter, NDKSubscription } from "../subscription/index.js"; import type { Hexpubkey, ProfilePointer } from "../user/index.js"; import type { NDKUserProfile } from "../user/profile.js"; -import type { NDKLnUrlData } from "../zap/index.js"; +import { NDKLnUrlData } from "../zapper/ln.js"; export interface NDKCacheAdapter { /** diff --git a/ndk/src/dvm/schedule.ts b/ndk/src/dvm/schedule.ts index 01d7d6fb..6821581f 100644 --- a/ndk/src/dvm/schedule.ts +++ b/ndk/src/dvm/schedule.ts @@ -1,7 +1,7 @@ -import { NDKEvent, NostrEvent } from "../events"; +import type { NDKEvent, NostrEvent } from "../events"; import { type NDKUser } from "../user"; import { NDKKind } from "../events/kinds"; -import { NDKSubscription } from "../subscription"; +import type { NDKSubscription } from "../subscription"; import { NDKDVMRequest } from "../events/kinds/dvm/request"; import { NDKDVMJobFeedback } from "../events/kinds/dvm"; diff --git a/ndk/src/events/content-tagger.ts b/ndk/src/events/content-tagger.ts index cd705415..f1e25cdd 100644 --- a/ndk/src/events/content-tagger.ts +++ b/ndk/src/events/content-tagger.ts @@ -117,7 +117,8 @@ export async function generateContentTags( case "nevent": promises.push( new Promise(async (resolve) => { - let { id, relays, author } = data as EventPointer; + const { id, author } = data as EventPointer; + let { relays } = data as EventPointer; // If the nevent doesn't have a relay specified, try to get one if (!relays || relays.length === 0) { diff --git a/ndk/src/events/fetch-tagged-event.ts b/ndk/src/events/fetch-tagged-event.ts index 9affa2b3..66cfa8fa 100644 --- a/ndk/src/events/fetch-tagged-event.ts +++ b/ndk/src/events/fetch-tagged-event.ts @@ -1,5 +1,5 @@ -import { NDKEvent } from "."; -import { NDKSubscriptionOptions } from "../subscription"; +import type { NDKEvent } from "."; +import type { NDKSubscriptionOptions } from "../subscription"; import { getReplyTag, getRootTag } from "../thread"; export async function fetchTaggedEvent( @@ -18,7 +18,7 @@ export async function fetchTaggedEvent( let relay; //= hint !== "" ? this.ndk.pool.getRelay(hint) : undefined; // if we have a relay, attempt to use that first - let event = await this.ndk.fetchEvent(id, {}, relay); + const event = await this.ndk.fetchEvent(id, {}, relay); return event; } diff --git a/ndk/src/events/index.test.ts b/ndk/src/events/index.test.ts index cf44694d..aaa8762b 100644 --- a/ndk/src/events/index.test.ts +++ b/ndk/src/events/index.test.ts @@ -282,6 +282,21 @@ describe("NDKEvent", () => { ["p", "pubkey"], ]); }); + + it("returns h tags if they are present", () => { + const event = new NDKEvent(ndk, { + kind: 1, + pubkey: "pubkey", + id: "eventid", + tags: [["h", "group-id"]], + } as NostrEvent); + + expect(event.referenceTags()).toEqual([ + ["e", "eventid"], + ["h", "group-id"], + ["p", "pubkey"], + ]); + }); }); describe("tagId", () => { diff --git a/ndk/src/events/index.ts b/ndk/src/events/index.ts index 34833381..d36faf52 100644 --- a/ndk/src/events/index.ts +++ b/ndk/src/events/index.ts @@ -14,7 +14,7 @@ import { decrypt, encrypt } from "./nip04.js"; import { encode } from "./nip19.js"; import { repost } from "./repost.js"; import { fetchReplyEvent, fetchRootEvent, fetchTaggedEvent } from "./fetch-tagged-event.js"; -import { NDKEventSerialized, deserialize, serialize } from "./serializer.js"; +import { type NDKEventSerialized, deserialize, serialize } from "./serializer.js"; import { validate, verifySignature, getEventHash } from "./validation.js"; import { matchFilter } from "nostr-tools"; @@ -58,7 +58,15 @@ export class NDKEvent extends EventEmitter { /** * The relays that this event was received from and/or successfully published to. */ - public onRelays: NDKRelay[] = []; + get onRelays(): NDKRelay[] { + let res: NDKRelay[] = []; + if (!this.ndk) { + if (this.relay) res.push(this.relay); + } else { + res = this.ndk.subManager.seenEvents.get(this.id) || []; + } + return res; + } /** * The status of the publish operation. @@ -66,7 +74,7 @@ export class NDKEvent extends EventEmitter { public publishStatus?: "pending" | "success" | "error" = "success"; public publishError?: Error; - constructor(ndk?: NDK, event?: NostrEvent) { + constructor(ndk?: NDK, event?: NostrEvent | NDKEvent) { super(); this.ndk = ndk; this.created_at = event?.created_at; @@ -76,6 +84,15 @@ export class NDKEvent extends EventEmitter { this.sig = event?.sig; this.pubkey = event?.pubkey || ""; this.kind = event?.kind; + + if (event instanceof NDKEvent) { + if (this.relay) { + this.relay = event.relay; + this.ndk?.subManager.seenEvent(event.id, this.relay!); + } + this.publishStatus = event.publishStatus; + this.publishError = event.publishError; + } } /** @@ -124,21 +141,7 @@ export class NDKEvent extends EventEmitter { /** * Tag a user with an optional marker. - * @param user The user to tag. - * @param marker The marker to use in the tag. - */ - public tag(user: NDKUser, marker?: string): void; - - /** - * Tag a user with an optional marker. - * @param user The user to tag. - * @param marker The marker to use in the tag. - */ - public tag(user: NDKUser, marker?: string): void; - - /** - * Tag a user with an optional marker. - * @param event The event to tag. + * @param target What is to be tagged. Can be an NDKUser, NDKEvent, or an NDKTag. * @param marker The marker to use in the tag. * @param skipAuthorTag Whether to explicitly skip adding the author tag of the event. * @param forceTag Force a specific tag to be used instead of the default "e" or "a" tag. @@ -148,23 +151,22 @@ export class NDKEvent extends EventEmitter { * // reply.tags => [["e", , , "reply"]] * ``` */ - public tag(event: NDKEvent, marker?: string, skipAuthorTag?: boolean, forceTag?: string): void; public tag( - userOrTagOrEvent: NDKTag | NDKUser | NDKEvent, + target: NDKTag | NDKUser | NDKEvent, marker?: string, skipAuthorTag?: boolean, forceTag?: string ): void { let tags: NDKTag[] = []; - const isNDKUser = (userOrTagOrEvent as NDKUser).fetchProfile !== undefined; + const isNDKUser = (target as NDKUser).fetchProfile !== undefined; if (isNDKUser) { forceTag ??= "p"; - const tag = [forceTag, (userOrTagOrEvent as NDKUser).pubkey]; + const tag = [forceTag, (target as NDKUser).pubkey]; if (marker) tag.push(...["", marker]); tags.push(tag); - } else if (userOrTagOrEvent instanceof NDKEvent) { - const event = userOrTagOrEvent as NDKEvent; + } else if (target instanceof NDKEvent) { + const event = target as NDKEvent; skipAuthorTag ??= event?.pubkey === this.pubkey; tags = event.referenceTags(marker, skipAuthorTag, forceTag); @@ -175,10 +177,10 @@ export class NDKEvent extends EventEmitter { this.tags.push(["p", pTag[1]]); } - } else if (Array.isArray(userOrTagOrEvent)) { - tags = [userOrTagOrEvent as NDKTag]; + } else if (Array.isArray(target)) { + tags = [target as NDKTag]; } else { - throw new Error("Invalid argument", userOrTagOrEvent as any); + throw new Error("Invalid argument", target as any); } this.tags = mergeTags(this.tags, tags); @@ -246,6 +248,16 @@ export class NDKEvent extends EventEmitter { return t.filter((tag) => tag[3] === marker); } + /** + * Check if the event has a tag with the given name + * @param tagName + * @param marker + * @returns + */ + public hasTag(tagName: string, marker?: string): boolean { + return this.tags.some((tag) => tag[0] === tagName && (!marker || tag[3] === marker)); + } + /** * Get the first tag with the given name * @param tagName Tag name to search for @@ -291,11 +303,12 @@ export class NDKEvent extends EventEmitter { /** * Remove all tags with the given name (e.g. "d", "a", "p") - * @param tagName Tag name to search for and remove + * @param tagName Tag name(s) to search for and remove * @returns {void} */ - public removeTag(tagName: string): void { - this.tags = this.tags.filter((tag) => tag[0] !== tagName); + public removeTag(tagName: string | string[]): void { + const tagNames = Array.isArray(tagName) ? tagName : [tagName]; + this.tags = this.tags.filter((tag) => !tagNames.includes(tag[0])); } /** @@ -359,7 +372,8 @@ export class NDKEvent extends EventEmitter { if (!relaySet) { // If we have a devWriteRelaySet, use it to publish all events - relaySet = this.ndk.devWriteRelaySet || await calculateRelaySetFromEvent(this.ndk, this); + relaySet = + this.ndk.devWriteRelaySet || (await calculateRelaySetFromEvent(this.ndk, this)); } // If the published event is a delete event, notify the cache if there is one @@ -386,7 +400,7 @@ export class NDKEvent extends EventEmitter { }); const relays = await relaySet.publish(this, timeoutMs, requiredRelayCount); - this.onRelays = Array.from(relays); + relays.forEach((relay) => this.ndk?.subManager.seenEvent(this.id, relay)); return relays; } @@ -596,6 +610,9 @@ export class NDKEvent extends EventEmitter { tags.forEach((tag) => tag.push(marker)); // Add the marker to both "a" and "e" tags } + // NIP-29 h-tags + tags = [...tags, ...this.getMatchingTags("h")]; + if (!skipAuthorTag) tags.push(...this.author.referenceTags()); return tags; @@ -637,7 +654,8 @@ export class NDKEvent extends EventEmitter { kind: NDKKind.EventDeletion, content: reason || "", } as NostrEvent); - e.tag(this); + e.tag(this, undefined, true); + e.tags.push(["k", this.kind!.toString()]); if (publish) await e.publish(); return e; diff --git a/ndk/src/events/kinds/article.ts b/ndk/src/events/kinds/article.ts index b38e0995..e68cc69b 100644 --- a/ndk/src/events/kinds/article.ts +++ b/ndk/src/events/kinds/article.ts @@ -1,5 +1,5 @@ import type { NDK } from "../../ndk/index.js"; -import { ContentTag } from "../content-tagger.js"; +import type { ContentTag } from "../content-tagger.js"; import { NDKEvent, type NostrEvent } from "../index.js"; import { NDKKind } from "./index.js"; @@ -9,7 +9,10 @@ import { NDKKind } from "./index.js"; * @group Kind Wrapper */ export class NDKArticle extends NDKEvent { - constructor(ndk: NDK | undefined, rawEvent?: NostrEvent) { + static kind = NDKKind.Article; + static kinds = [NDKKind.Article]; + + constructor(ndk: NDK | undefined, rawEvent?: NostrEvent | NDKEvent) { super(ndk, rawEvent); this.kind ??= NDKKind.Article; } @@ -21,7 +24,7 @@ export class NDKArticle extends NDKEvent { * @returns NDKArticle */ static from(event: NDKEvent) { - return new NDKArticle(event.ndk, event.rawEvent()); + return new NDKArticle(event.ndk, event); } /** diff --git a/ndk/src/events/kinds/classified.ts b/ndk/src/events/kinds/classified.ts index ee90bd04..91a64f4a 100644 --- a/ndk/src/events/kinds/classified.ts +++ b/ndk/src/events/kinds/classified.ts @@ -17,7 +17,7 @@ interface NDKClassifiedPriceTag { * @group Kind Wrapper */ export class NDKClassified extends NDKEvent { - constructor(ndk: NDK | undefined, rawEvent?: NostrEvent) { + constructor(ndk: NDK | undefined, rawEvent?: NostrEvent | NDKEvent) { super(ndk, rawEvent); this.kind ??= NDKKind.Classified; } @@ -29,7 +29,7 @@ export class NDKClassified extends NDKEvent { * @returns NDKClassified */ static from(event: NDKEvent): NDKClassified { - return new NDKClassified(event.ndk, event.rawEvent()); + return new NDKClassified(event.ndk, event); } /** diff --git a/ndk/src/events/kinds/drafts.ts b/ndk/src/events/kinds/drafts.ts index 35f37265..8bb03891 100644 --- a/ndk/src/events/kinds/drafts.ts +++ b/ndk/src/events/kinds/drafts.ts @@ -1,7 +1,8 @@ -import { NDK } from "../../ndk/index.js"; -import { NDKRelaySet } from "../../relay/sets/index.js"; -import { NDKSigner } from "../../signers/index.js"; -import { NDKEvent, NostrEvent } from "../index.js"; +import type { NDK } from "../../ndk/index.js"; +import type { NDKRelaySet } from "../../relay/sets/index.js"; +import type { NDKSigner } from "../../signers/index.js"; +import type { NostrEvent } from "../index.js"; +import { NDKEvent } from "../index.js"; import { NDKKind } from "./index.js"; /** @@ -19,13 +20,13 @@ import { NDKKind } from "./index.js"; export class NDKDraft extends NDKEvent { public _event: NostrEvent | undefined; - constructor(ndk: NDK | undefined, rawEvent?: NostrEvent) { + constructor(ndk: NDK | undefined, rawEvent?: NostrEvent | NDKEvent) { super(ndk, rawEvent); this.kind ??= NDKKind.Draft; } static from(event: NDKEvent) { - return new NDKDraft(event.ndk, event.rawEvent()); + return new NDKDraft(event.ndk, event); } /** diff --git a/ndk/src/events/kinds/dvm/request.ts b/ndk/src/events/kinds/dvm/request.ts index 58a9f199..c7d462a5 100644 --- a/ndk/src/events/kinds/dvm/request.ts +++ b/ndk/src/events/kinds/dvm/request.ts @@ -1,9 +1,10 @@ import type { NDK } from "../../../ndk/index.js"; -import { NDKSigner } from "../../../signers/index.js"; -import { NDKUser } from "../../../user/index.js"; +import type { NDKSigner } from "../../../signers/index.js"; +import type { NDKUser } from "../../../user/index.js"; import type { NDKTag, NostrEvent } from "../../index.js"; import { NDKEvent } from "../../index.js"; -import { NDKDVMJobFeedback, NDKDvmJobFeedbackStatus } from "./feedback.js"; +import type { NDKDvmJobFeedbackStatus } from "./feedback.js"; +import { NDKDVMJobFeedback } from "./feedback.js"; // import type { NDKDvmJobFeedbackStatus } from "./NDKDVMJobFeedback.js"; // import { NDKDVMJobFeedback } from "./NDKDVMJobFeedback.js"; // import { NDKDVMJobResult } from "./NDKDVMJobResult.js"; diff --git a/ndk/src/events/kinds/highlight.ts b/ndk/src/events/kinds/highlight.ts index d29e00b9..3f6f8748 100644 --- a/ndk/src/events/kinds/highlight.ts +++ b/ndk/src/events/kinds/highlight.ts @@ -13,13 +13,16 @@ import { NDKKind } from "./index.js"; export class NDKHighlight extends NDKEvent { private _article: NDKEvent | string | undefined; - constructor(ndk?: NDK, rawEvent?: NostrEvent) { + static kind = NDKKind.Highlight; + static kinds = [NDKKind.Highlight]; + + constructor(ndk?: NDK, rawEvent?: NostrEvent | NDKEvent) { super(ndk, rawEvent); this.kind ??= NDKKind.Highlight; } static from(event: NDKEvent) { - return new NDKHighlight(event.ndk, event.rawEvent()); + return new NDKHighlight(event.ndk, event); } get url(): string | undefined { diff --git a/ndk/src/events/kinds/index.ts b/ndk/src/events/kinds/index.ts index e11fc068..e29d8d50 100644 --- a/ndk/src/events/kinds/index.ts +++ b/ndk/src/events/kinds/index.ts @@ -54,11 +54,16 @@ export enum NDKKind { Unsubscribe = 7002, SubscriptionReceipt = 7003, + CashuToken = 7375, + WalletChange = 7376, + // NIP-29 GroupAdminAddUser = 9000, GroupAdminRemoveUser = 9001, GroupAdminEditMetadata = 9002, GroupAdminEditStatus = 9006, + GroupAdminCreateGroup = 9007, + GroupAdminRequestJoin = 9021, // Lists and Sets MuteList = 10000, @@ -71,6 +76,7 @@ export enum NDKKind { SearchRelayList = 10007, SimpleGroupList = 10009, InterestList = 10015, + CashuMintList = 10019, EmojiList = 10030, BlossomList = 10063, @@ -100,9 +106,13 @@ export enum NDKKind { Wiki = 30818, Draft = 31234, SubscriptionTier = 37001, + + EcashMintRecommendation = 38000, + HighlightSet = 39802, CategorizedHighlightList = NDKKind.HighlightSet, // Deprecated but left for backwards compatibility + Nutzap = 9321, ZapRequest = 9734, Zap = 9735, Highlight = 9802, @@ -125,8 +135,12 @@ export enum NDKKind { AppSpecificData = 30078, Classified = 30402, HorizontalVideo = 34235, + VerticalVideo = 34236, + + CashuWallet = 37375, GroupMetadata = 39000, // NIP-29 + GroupAdmins = 39001, // NIP-29 GroupMembers = 39002, // NIP-29 // NIP-89: App Metadata diff --git a/ndk/src/events/kinds/lists/index.ts b/ndk/src/events/kinds/lists/index.ts index afc333bd..e2f57f1f 100644 --- a/ndk/src/events/kinds/lists/index.ts +++ b/ndk/src/events/kinds/lists/index.ts @@ -38,7 +38,7 @@ export class NDKList extends NDKEvent { */ private encryptedTagsLength: number | undefined; - constructor(ndk?: NDK, rawEvent?: NostrEvent) { + constructor(ndk?: NDK, rawEvent?: NostrEvent | NDKEvent) { super(ndk, rawEvent); this.kind ??= NDKKind.CategorizedBookmarkList; } @@ -47,7 +47,7 @@ export class NDKList extends NDKEvent { * Wrap a NDKEvent into a NDKList */ static from(ndkEvent: NDKEvent): NDKList { - return new NDKList(ndkEvent.ndk, ndkEvent.rawEvent()); + return new NDKList(ndkEvent.ndk, ndkEvent); } /** @@ -55,30 +55,32 @@ export class NDKList extends NDKEvent { */ get title(): string | undefined { const titleTag = this.tagValue("title") || this.tagValue("name"); - if (this.kind === NDKKind.Contacts && !titleTag) { + if (titleTag) return titleTag; + + if (this.kind === NDKKind.Contacts) { return "Contacts"; - } else if (this.kind === NDKKind.MuteList && !titleTag) { + } else if (this.kind === NDKKind.MuteList) { return "Mute"; - } else if (this.kind === NDKKind.PinList && !titleTag) { + } else if (this.kind === NDKKind.PinList) { return "Pinned Notes"; - } else if (this.kind === NDKKind.RelayList && !titleTag) { + } else if (this.kind === NDKKind.RelayList) { return "Relay Metadata"; - } else if (this.kind === NDKKind.BookmarkList && !titleTag) { + } else if (this.kind === NDKKind.BookmarkList) { return "Bookmarks"; - } else if (this.kind === NDKKind.CommunityList && !titleTag) { + } else if (this.kind === NDKKind.CommunityList) { return "Communities"; - } else if (this.kind === NDKKind.PublicChatList && !titleTag) { + } else if (this.kind === NDKKind.PublicChatList) { return "Public Chats"; - } else if (this.kind === NDKKind.BlockRelayList && !titleTag) { + } else if (this.kind === NDKKind.BlockRelayList) { return "Blocked Relays"; - } else if (this.kind === NDKKind.SearchRelayList && !titleTag) { + } else if (this.kind === NDKKind.SearchRelayList) { return "Search Relays"; - } else if (this.kind === NDKKind.InterestList && !titleTag) { + } else if (this.kind === NDKKind.InterestList) { return "Interests"; - } else if (this.kind === NDKKind.EmojiList && !titleTag) { + } else if (this.kind === NDKKind.EmojiList) { return "Emojis"; } else { - return titleTag ?? this.tagValue("d"); + return this.tagValue("d"); } } @@ -86,14 +88,9 @@ export class NDKList extends NDKEvent { * Sets the title of the list. */ set title(title: string | undefined) { - this.removeTag("title"); - this.removeTag("name"); + this.removeTag(["title", "name"]); - if (title) { - this.tags.push(["title", title]); - } else { - throw new Error("Title cannot be empty"); - } + if (title) this.tags.push(["title", title]); } /** @@ -101,32 +98,7 @@ export class NDKList extends NDKEvent { * @deprecated Please use "title" instead. */ get name(): string | undefined { - const nameTag = this.tagValue("name"); - if (this.kind === NDKKind.Contacts && !nameTag) { - return "Contacts"; - } else if (this.kind === NDKKind.MuteList && !nameTag) { - return "Mute"; - } else if (this.kind === NDKKind.PinList && !nameTag) { - return "Pinned Notes"; - } else if (this.kind === NDKKind.RelayList && !nameTag) { - return "Relay Metadata"; - } else if (this.kind === NDKKind.BookmarkList && !nameTag) { - return "Bookmarks"; - } else if (this.kind === NDKKind.CommunityList && !nameTag) { - return "Communities"; - } else if (this.kind === NDKKind.PublicChatList && !nameTag) { - return "Public Chats"; - } else if (this.kind === NDKKind.BlockRelayList && !nameTag) { - return "Blocked Relays"; - } else if (this.kind === NDKKind.SearchRelayList && !nameTag) { - return "Search Relays"; - } else if (this.kind === NDKKind.InterestList && !nameTag) { - return "Interests"; - } else if (this.kind === NDKKind.EmojiList && !nameTag) { - return "Emojis"; - } else { - return nameTag ?? this.tagValue("d"); - } + return this.title; } /** @@ -134,13 +106,7 @@ export class NDKList extends NDKEvent { * @deprecated Please use "title" instead. This method will use the `title` tag instead. */ set name(name: string | undefined) { - this.removeTag("name"); - - if (name) { - this.tags.push(["title", name]); - } else { - throw new Error("Name cannot be empty"); - } + this.title = name; } /** @@ -154,11 +120,23 @@ export class NDKList extends NDKEvent { * Sets the description of the list. */ set description(name: string | undefined) { - if (name) { - this.tags.push(["description", name]); - } else { - this.removeTag("description"); - } + this.removeTag("description"); + if (name) this.tags.push(["description", name]); + } + + /** + * Returns the image of the list. + */ + get image(): string | undefined { + return this.tagValue("image"); + } + + /** + * Sets the image of the list. + */ + set image(name: string | undefined) { + this.removeTag("image"); + if (name) this.tags.push(["image", name]); } private isEncryptedTagsCacheValid(): boolean { @@ -290,6 +268,44 @@ export class NDKList extends NDKEvent { this.emit("change"); } + /** + * Removes an item from the list from both the encrypted and unencrypted lists. + * @param value value of item to remove from the list + * @param publish whether to publish the change + * @returns + */ + async removeItemByValue(value: string, publish = true): Promise | void> { + if (!this.ndk) throw new Error("NDK instance not set"); + if (!this.ndk.signer) throw new Error("NDK signer not set"); + + // check in unecrypted tags + const index = this.tags.findIndex((tag) => tag[1] === value); + if (index >= 0) { + this.tags.splice(index, 1); + } + + // check in encrypted tags + const user = await this.ndk.signer.user(); + const encryptedTags = await this.encryptedTags(); + + const encryptedIndex = encryptedTags.findIndex((tag) => tag[1] === value); + if (encryptedIndex >= 0) { + encryptedTags.splice(encryptedIndex, 1); + this._encryptedTags = encryptedTags; + this.encryptedTagsLength = this.content.length; + this.content = JSON.stringify(encryptedTags); + await this.encrypt(user); + } + + if (publish) { + return this.publishReplaceable(); + } else { + this.created_at = Math.floor(Date.now() / 1000); + } + + this.emit("change"); + } + /** * Removes an item from the list. * diff --git a/ndk/src/events/kinds/nutzap/index.ts b/ndk/src/events/kinds/nutzap/index.ts new file mode 100644 index 00000000..6c110553 --- /dev/null +++ b/ndk/src/events/kinds/nutzap/index.ts @@ -0,0 +1,180 @@ +import debug from "debug"; +import NDK, { Hexpubkey, NDKEvent, NDKKind, NDKUser, NostrEvent } from "../../../index.js"; +import { Proof } from "./proof.js"; + +/** + * Represents a NIP-61 nutzap + */ +export class NDKNutzap extends NDKEvent { + private debug: debug.Debugger; + private _proofs: Proof[] = []; + + static kind = NDKKind.Nutzap; + static kinds = [NDKNutzap.kind]; + + constructor(ndk?: NDK, event?: NostrEvent | NDKEvent) { + super(ndk, event); + this.kind ??= NDKKind.Nutzap; + this.debug = ndk?.debug.extend("nutzap") ?? debug("ndk:nutzap"); + + // ensure we have an alt tag + if (!this.alt) this.alt = "This is a nutzap"; + } + + static from(event: NDKEvent) { + const e = new this(event.ndk, event); + + try { + const proofTags = e.getMatchingTags("proof"); + + if (proofTags.length) { + // preferred version with proofs as tags + e._proofs = proofTags.map((tag) => JSON.parse(tag[1])) as Proof[]; + } else { + // old version with proofs in content? + e._proofs = JSON.parse(e.content) as Proof[]; + } + } catch { + return; + } + + if (!e._proofs || !e._proofs.length) return; + + return e; + } + + set comment(comment: string | undefined) { + this.content = comment ?? ""; + } + + get comment(): string { + const c = this.tagValue("comment"); + if (c) return c; + return this.content; + } + + set proofs(proofs: Proof[]) { + this._proofs = proofs; + + // remove old proof tags + this.tags = this.tags.filter((tag) => tag[0] !== "proof"); + + // add these proof tags + for (const proof of proofs) { + this.tags.push(["proof", JSON.stringify(proof)]); + } + + // remove amount tags + this.removeTag("amount"); + this.tags.push(["amount", this.amount.toString()]); + } + + get proofs(): Proof[] { + return this._proofs; + } + + /** + * Gets the p2pk pubkey that is embedded in the first proof + */ + get p2pk(): string | undefined { + const firstProof = this.proofs[0]; + try { + const secret = JSON.parse(firstProof.secret); + let payload: Record = {}; + if (typeof secret === "string") { + payload = JSON.parse(secret); + this.debug("stringified payload", firstProof.secret); + } else if (typeof secret === "object") { + payload = secret; + } + const isP2PKLocked = payload[0] === "P2PK" && payload[1]?.data; + + if (isP2PKLocked) { + const paddedp2pk = payload[1].data; + const p2pk = paddedp2pk.slice(2, -1); + + if (p2pk) return p2pk; + } + } catch (e) { + this.debug("error parsing p2pk pubkey", e, this.proofs[0]); + } + } + + /** + * Get the mint where this nutzap proofs exist + */ + get mint(): string { + return this.tagValue("u")!; + } + + set mint(value: string) { + this.removeTag("u"); + this.tag(["u", value]); + } + + get unit(): string { + return this.tagValue("unit") ?? "msat"; + } + + set unit(value: string | undefined) { + this.removeTag("unit"); + if (value) this.tag(["unit", value]); + } + + get amount(): number { + const count = this.proofs.reduce((total, proof) => total + proof.amount, 0); + return count * 1000; + } + + public sender = this.author; + + /** + * Set the target of the nutzap + * @param target The target of the nutzap (a user or an event) + */ + set target(target: NDKEvent | NDKUser) { + // ensure we only have a single "p"-tag + this.tags = this.tags.filter((t) => t[0] !== "p"); + + if (target instanceof NDKEvent) { + this.tags.push(); + } + } + + set recipientPubkey(pubkey: Hexpubkey) { + this.removeTag("p"); + this.tag(["p", pubkey]); + } + + get recipientPubkey(): Hexpubkey { + return this.tagValue("p")!; + } + + get recipient(): NDKUser { + const pubkey = this.recipientPubkey; + if (this.ndk) return this.ndk.getUser({ pubkey }); + + return new NDKUser({ pubkey }); + } + + /** + * Validates that the nutzap conforms to NIP-61 + */ + get isValid(): boolean { + let pTagCount = 0; + let mintTagCount = 0; + + for (const tag of this.tags) { + if (tag[0] === "p") pTagCount++; + if (tag[0] === "u") mintTagCount++; + } + + return ( + // exactly one recipient and mint + pTagCount === 1 && + mintTagCount === 1 && + // must have at least one proof + this.proofs.length > 0 + ); + } +} \ No newline at end of file diff --git a/ndk/src/events/kinds/nutzap/mint-list.ts b/ndk/src/events/kinds/nutzap/mint-list.ts new file mode 100644 index 00000000..6376901b --- /dev/null +++ b/ndk/src/events/kinds/nutzap/mint-list.ts @@ -0,0 +1,76 @@ +import type { NDK } from "../../../ndk/index.js"; +import { NDKRelaySet } from "../../../relay/sets/index.js"; +import type { NostrEvent } from "../../index.js"; +import { NDKEvent } from "../../index.js"; +import { NDKKind } from "../index.js"; + +export class NDKCashuMintList extends NDKEvent { + static kind = NDKKind.CashuMintList; + static kinds = [NDKKind.CashuMintList]; + private _p2pk?: string; + + constructor(ndk?: NDK, event?: NostrEvent | NDKEvent) { + super(ndk, event); + this.kind ??= NDKKind.CashuMintList; + } + + static from(event: NDKEvent) { + return new NDKCashuMintList(event.ndk, event); + } + + set relays(urls: WebSocket["url"][]) { + this.tags = this.tags.filter((t) => t[0] !== "relay"); + for (const url of urls) { + this.tags.push(["relay", url]); + } + } + + get relays(): WebSocket["url"][] { + const r = []; + for (const tag of this.tags) { + if (tag[0] === "relay") { + r.push(tag[1]); + } + } + + return r; + } + + set mints(urls: WebSocket["url"][]) { + this.tags = this.tags.filter((t) => t[0] !== "mint"); + for (const url of urls) { + this.tags.push(["mint", url]); + } + } + + get mints(): WebSocket["url"][] { + const r = []; + for (const tag of this.tags) { + if (tag[0] === "mint") { + r.push(tag[1]); + } + } + + return Array.from(new Set(r)); + } + + get p2pk(): string { + if (this._p2pk) { + return this._p2pk; + } + this._p2pk = this.tagValue("pubkey") ?? this.pubkey; + return this._p2pk; + } + + set p2pk(pubkey: string | undefined) { + this._p2pk = pubkey; + this.removeTag("pubkey"); + if (pubkey) { + this.tags.push(["pubkey", pubkey]); + } + } + + get relaySet(): NDKRelaySet | undefined { + return NDKRelaySet.fromRelayUrls(this.relays, this.ndk!); + } +} diff --git a/ndk/src/events/kinds/nutzap/proof.ts b/ndk/src/events/kinds/nutzap/proof.ts new file mode 100644 index 00000000..a49f076c --- /dev/null +++ b/ndk/src/events/kinds/nutzap/proof.ts @@ -0,0 +1,18 @@ +export type Proof = { + /** + * Keyset id, used to link proofs to a mint an its MintKeys. + */ + id: string; + /** + * Amount denominated in Satoshis. Has to match the amount of the mints signing key. + */ + amount: number; + /** + * The initial secret that was (randomly) chosen for the creation of this proof. + */ + secret: string; + /** + * The unblinded signature for this secret, signed by the mints private key. + */ + C: string; +}; diff --git a/ndk/src/events/kinds/simple-group/index.ts b/ndk/src/events/kinds/simple-group/index.ts index 71298167..eff4b0a4 100644 --- a/ndk/src/events/kinds/simple-group/index.ts +++ b/ndk/src/events/kinds/simple-group/index.ts @@ -1,49 +1,105 @@ import { NDKKind } from ".."; -import { NDKEvent, NDKTag, NostrEvent } from "../.."; -import { NDK } from "../../../ndk"; -import { NDKRelaySet } from "../../../relay/sets"; -import { Hexpubkey, NDKUser } from "../../../user"; - -type AddUserOpts = { - /** - * Whether to publish the event. - * @default true - */ - publish?: boolean; - - /** - * Event to add the user to - */ - currentUserListEvent?: NDKEvent; - - /** - * An additional marker to add to the user/group relationship. - * (e.g. tier, role, etc.) - */ - marker?: string; - - /** - * Whether to skip the user list event. - * @default false - */ - skipUserListEvent?: boolean; -}; +import type { NDKTag, NostrEvent } from "../.."; +import { NDKEvent } from "../.."; +import type { NDK } from "../../../ndk/index.js"; +import type { NDKRelaySet } from "../../../relay/sets/index.js"; +import type { NDKSigner } from "../../../signers/index.js"; +import type { Hexpubkey, NDKUser } from "../../../user/index.js"; +import { NDKSimpleGroupMemberList } from "./member-list.js"; +import { NDKSimpleGroupMetadata } from "./metadata.js"; /** * Represents a NIP-29 group. * @catergory Kind Wrapper */ export class NDKSimpleGroup { - private ndk: NDK; - readonly groupId: string; + readonly ndk: NDK; + public groupId: string; readonly relaySet: NDKRelaySet; - constructor(ndk: NDK, groupId: string, relaySet: NDKRelaySet) { + private fetchingMetadata: Promise | undefined; + + public metadata: NDKSimpleGroupMetadata | undefined; + public memberList: NDKSimpleGroupMemberList | undefined; + public adminList: NDKSimpleGroupMemberList | undefined; + + constructor(ndk: NDK, relaySet: NDKRelaySet, groupId?: string) { this.ndk = ndk; - this.groupId = groupId; + this.groupId = groupId ?? randomId(24); this.relaySet = relaySet; } + get id(): string { + return this.groupId; + } + + public relayUrls(): string[] { + return this.relaySet!.relayUrls; + } + + get name(): string | undefined { + return this.metadata?.name; + } + + get about(): string | undefined { + return this.metadata?.about; + } + + get picture(): string | undefined { + return this.metadata?.picture; + } + + get members(): Hexpubkey[] | [] { + return this.memberList?.members ?? []; + } + + get admins(): Hexpubkey[] | [] { + return this.adminList?.members ?? []; + } + + async getMetadata(): Promise { + await this.ensureMetadataEvent(); + return this.metadata!; + } + + /** + * Creates the group by publishing a kind:9007 event. + * @param signer + * @returns + */ + async createGroup(signer?: NDKSigner) { + signer ??= this.ndk.signer!; + if (!signer) throw new Error("No signer available"); + const user = await signer.user(); + if (!user) throw new Error("No user available"); + + const event = new NDKEvent(this.ndk); + event.kind = NDKKind.GroupAdminCreateGroup; + event.tags.push(["h", this.groupId]); + await event.sign(signer); + return event.publish(this.relaySet); + } + + async setMetadata({ + name, + about, + picture, + }: { + name?: string; + about?: string; + picture?: string; + }) { + const event = new NDKEvent(this.ndk); + event.kind = NDKKind.GroupAdminEditMetadata; + event.tags.push(["h", this.groupId]); + if (name) event.tags.push(["name", name]); + if (about) event.tags.push(["about", about]); + if (picture) event.tags.push(["picture", picture]); + + await event.sign(); + return event.publish(this.relaySet); + } + /** * Adds a user to the group using a kind:9000 event * @param user user to add @@ -52,7 +108,6 @@ export class NDKSimpleGroup { async addUser(user: NDKUser): Promise { const addUserEvent = NDKSimpleGroup.generateAddUserEvent(user.pubkey, this.groupId); addUserEvent.ndk = this.ndk; - const relays = await addUserEvent.publish(this.relaySet); return addUserEvent; } @@ -66,7 +121,9 @@ export class NDKSimpleGroup { this.relaySet ); - return memberList; + if (!memberList) return null; + + return NDKSimpleGroupMemberList.from(memberList); } /** @@ -124,7 +181,60 @@ export class NDKSimpleGroup { return event; } + + public async requestToJoin(pubkey: Hexpubkey, content?: string) { + const event = new NDKEvent(this.ndk, { + kind: NDKKind.GroupAdminRequestJoin, + content: content ?? "", + tags: [["h", this.groupId]], + } as NostrEvent); + return event.publish(this.relaySet); + } + + /** + * Makes sure that a metadata event exists locally + */ + async ensureMetadataEvent(): Promise { + if (this.metadata) return; + if (this.fetchingMetadata) return this.fetchingMetadata; + + this.fetchingMetadata = this.ndk + .fetchEvent( + { + kinds: [NDKKind.GroupMetadata], + "#d": [this.groupId], + }, + undefined, + this.relaySet + ) + .then((event) => { + if (event) { + this.metadata = NDKSimpleGroupMetadata.from(event); + } else { + this.metadata = new NDKSimpleGroupMetadata(this.ndk); + this.metadata.dTag = this.groupId; + } + }) + .finally(() => { + this.fetchingMetadata = undefined; + }) + .catch(() => { + throw new Error("Failed to fetch metadata for group " + this.groupId); + }); + + return this.fetchingMetadata; + } } // Remove a p tag of a user const untagUser = (pubkey: Hexpubkey) => (tag: NDKTag) => !(tag[0] === "p" && tag[1] === pubkey); + +function randomId(length: number) { + const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + const charsLength = chars.length; + let result = ""; + for (let i = 0; i < length; i++) { + result += chars.charAt(Math.floor(Math.random() * charsLength)); + } + return result; +} diff --git a/ndk/src/events/kinds/simple-group/member-list.ts b/ndk/src/events/kinds/simple-group/member-list.ts new file mode 100644 index 00000000..4dbdc92f --- /dev/null +++ b/ndk/src/events/kinds/simple-group/member-list.ts @@ -0,0 +1,43 @@ +import { NDKKind } from "../index.js"; +import type { NostrEvent } from "../../index.js"; +import { NDKEvent } from "../../index.js"; +import type { NDK } from "../../../ndk/index.js"; +import type { NDKRelaySet } from "../../../relay/sets/index.js"; +import type { NDKRelay } from "../../../relay/index.js"; +import type { Hexpubkey } from "../../../user/index.js"; + +export class NDKSimpleGroupMemberList extends NDKEvent { + public relaySet: NDKRelaySet | undefined; + public memberSet: Set = new Set(); + + static kind = NDKKind.GroupMembers; + static kinds = [NDKKind.GroupMembers]; + + constructor(ndk: NDK | undefined, rawEvent?: NostrEvent | NDKEvent) { + super(ndk, rawEvent); + this.kind ??= NDKKind.GroupMembers; + + this.memberSet = new Set(this.members); + } + + static from(event: NDKEvent) { + return new NDKSimpleGroupMemberList(event.ndk, event); + } + + get members(): string[] { + return this.getMatchingTags("p").map((tag) => tag[1]); + } + + public hasMember(member: Hexpubkey): boolean { + return this.memberSet.has(member); + } + + public async publish( + relaySet?: NDKRelaySet, + timeoutMs?: number, + requiredRelayCount?: number + ): Promise> { + relaySet ??= this.relaySet; + return super.publishReplaceable(relaySet, timeoutMs, requiredRelayCount); + } +} diff --git a/ndk/src/events/kinds/simple-group/metadata.ts b/ndk/src/events/kinds/simple-group/metadata.ts new file mode 100644 index 00000000..ad832f5e --- /dev/null +++ b/ndk/src/events/kinds/simple-group/metadata.ts @@ -0,0 +1,62 @@ +import { NDKKind } from "../index.js"; +import type { NostrEvent } from "../../index.js"; +import { NDKEvent } from "../../index.js"; +import type { NDK } from "../../../ndk/index.js"; + +export class NDKSimpleGroupMetadata extends NDKEvent { + static kind = NDKKind.GroupMetadata; + static kinds = [NDKKind.GroupMetadata]; + + constructor(ndk: NDK | undefined, rawEvent?: NostrEvent | NDKEvent) { + super(ndk, rawEvent); + this.kind ??= NDKKind.GroupMetadata; + } + + static from(event: NDKEvent) { + return new NDKSimpleGroupMetadata(event.ndk, event); + } + + get name(): string | undefined { + return this.tagValue("name"); + } + + get picture(): string | undefined { + return this.tagValue("picture"); + } + + get about(): string | undefined { + return this.tagValue("about"); + } + + get scope(): "public" | "private" | undefined { + if (this.getMatchingTags("public").length > 0) return "public"; + if (this.getMatchingTags("public").length > 0) return "private"; + } + + set scope(scope: "public" | "private" | undefined) { + this.removeTag("public"); + this.removeTag("private"); + + if (scope === "public") { + this.tags.push(["public", ""]); + } else if (scope === "private") { + this.tags.push(["private", ""]); + } + } + + get access(): "open" | "closed" | undefined { + if (this.getMatchingTags("open").length > 0) return "open"; + if (this.getMatchingTags("closed").length > 0) return "closed"; + } + + set access(access: "open" | "closed" | undefined) { + this.removeTag("open"); + this.removeTag("closed"); + + if (access === "open") { + this.tags.push(["open", ""]); + } else if (access === "closed") { + this.tags.push(["closed", ""]); + } + } +} diff --git a/ndk/src/events/kinds/subscriptions/amount.ts b/ndk/src/events/kinds/subscriptions/amount.ts index 83ce90fd..ca9e96b8 100644 --- a/ndk/src/events/kinds/subscriptions/amount.ts +++ b/ndk/src/events/kinds/subscriptions/amount.ts @@ -1,4 +1,4 @@ -import { NDKTag } from "../.."; +import type { NDKTag } from "../.."; export type NDKIntervalFrequency = "daily" | "weekly" | "monthly" | "quarterly" | "yearly"; diff --git a/ndk/src/events/kinds/subscriptions/receipt.ts b/ndk/src/events/kinds/subscriptions/receipt.ts index 52fbd5f5..127ca19f 100644 --- a/ndk/src/events/kinds/subscriptions/receipt.ts +++ b/ndk/src/events/kinds/subscriptions/receipt.ts @@ -1,8 +1,9 @@ import debug from "debug"; import { NDKKind } from ".."; -import { NDKEvent, NDKTag, NostrEvent } from "../.."; -import { NDK } from "../../../ndk"; -import { NDKSubscriptionStart } from "./subscription-start"; +import type { NostrEvent } from "../.."; +import { NDKEvent, NDKTag } from "../.."; +import type { NDK } from "../../../ndk"; +import type { NDKSubscriptionStart } from "./subscription-start"; import { NDKUser } from "../../../user"; type ValidPeriod = { start: Date; end: Date }; diff --git a/ndk/src/events/kinds/subscriptions/subscription-start.ts b/ndk/src/events/kinds/subscriptions/subscription-start.ts index fa837004..28a73212 100644 --- a/ndk/src/events/kinds/subscriptions/subscription-start.ts +++ b/ndk/src/events/kinds/subscriptions/subscription-start.ts @@ -1,9 +1,11 @@ import debug from "debug"; import { NDKKind } from ".."; -import { NDKEvent, NostrEvent } from "../.."; -import { NDK } from "../../../ndk"; +import type { NostrEvent } from "../.."; +import { NDKEvent } from "../.."; +import type { NDK } from "../../../ndk"; import { NDKUser } from "../../../user"; -import { NDKSubscriptionAmount, newAmount, parseTagToSubscriptionAmount } from "./amount.js"; +import type { NDKSubscriptionAmount } from "./amount.js"; +import { newAmount, parseTagToSubscriptionAmount } from "./amount.js"; import { NDKSubscriptionTier } from "./tier"; /** diff --git a/ndk/src/events/kinds/subscriptions/tier.ts b/ndk/src/events/kinds/subscriptions/tier.ts index 792386c4..7f76393c 100644 --- a/ndk/src/events/kinds/subscriptions/tier.ts +++ b/ndk/src/events/kinds/subscriptions/tier.ts @@ -3,12 +3,8 @@ import { type NostrEvent } from "../../index.js"; import type { NDKEvent, NDKTag } from "../../index.js"; import { NDKKind } from "../index.js"; import { NDKArticle } from "../article.js"; -import { - NDKIntervalFrequency, - NDKSubscriptionAmount, - newAmount, - parseTagToSubscriptionAmount, -} from "./amount.js"; +import type { NDKIntervalFrequency, NDKSubscriptionAmount } from "./amount.js"; +import { newAmount, parseTagToSubscriptionAmount } from "./amount.js"; /** * @description @@ -28,9 +24,13 @@ import { * tier.addPerk("Access to my private content"); */ export class NDKSubscriptionTier extends NDKArticle { - constructor(ndk: NDK | undefined, rawEvent?: NostrEvent) { + static kind = NDKKind.SubscriptionTier; + static kinds = [NDKKind.SubscriptionTier]; + + constructor(ndk: NDK | undefined, rawEvent?: NostrEvent | NDKEvent) { + const k = rawEvent?.kind ?? NDKKind.SubscriptionTier; super(ndk, rawEvent); - this.kind ??= NDKKind.SubscriptionTier; + this.kind = k; } /** @@ -39,7 +39,7 @@ export class NDKSubscriptionTier extends NDKArticle { * @returns NDKSubscriptionTier */ static from(event: NDKEvent) { - return new NDKSubscriptionTier(event.ndk, event.rawEvent()); + return new NDKSubscriptionTier(event.ndk, event); } /** diff --git a/ndk/src/events/kinds/video.ts b/ndk/src/events/kinds/video.ts index b7ce7862..f231415c 100644 --- a/ndk/src/events/kinds/video.ts +++ b/ndk/src/events/kinds/video.ts @@ -1,13 +1,17 @@ import { NDKKind } from "."; -import { NDKEvent, NostrEvent } from ".."; -import { NDK } from "../../ndk"; -import { ContentTag } from "../content-tagger"; +import type { NostrEvent } from ".."; +import { NDKEvent } from ".."; +import type { NDK } from "../../ndk"; +import type { ContentTag } from "../content-tagger"; /** * Represents a horizontal or vertical video. * @group Kind Wrapper */ export class NDKVideo extends NDKEvent { + static kind = NDKKind.HorizontalVideo; + static kinds = [NDKKind.HorizontalVideo, NDKKind.VerticalVideo]; + constructor(ndk: NDK | undefined, rawEvent?: NostrEvent) { super(ndk, rawEvent); this.kind ??= NDKKind.HorizontalVideo; diff --git a/ndk/src/events/kinds/wiki.ts b/ndk/src/events/kinds/wiki.ts new file mode 100644 index 00000000..4d56079f --- /dev/null +++ b/ndk/src/events/kinds/wiki.ts @@ -0,0 +1,7 @@ +import { NDKKind } from "."; +import { NDKArticle } from "./article"; + +export class NDKWiki extends NDKArticle { + static kind = NDKKind.Wiki; + static kinds = [NDKKind.Wiki]; +} diff --git a/ndk/src/events/nip19.test.ts b/ndk/src/events/nip19.test.ts index 8c2d084c..31d22678 100644 --- a/ndk/src/events/nip19.test.ts +++ b/ndk/src/events/nip19.test.ts @@ -36,21 +36,22 @@ describe("NDKEvent", () => { const a = event.encode(); expect(a).toBe( - "naddr1qvzqqqr4xqpzp75cf0tahv5z7plpdeaws7ex52nmnwgtwfr2g3m37r844evqrr6jqyf8wumn8ghj7un9d3shjtnxxaazu6t0qqzrzv3nxsrcfx9f" + "naddr1qvzqqqr4xqpzp75cf0tahv5z7plpdeaws7ex52nmnwgtwfr2g3m37r844evqrr6jqyfhwumn8ghj7un9d3shjtnxxaazu6t09uqqgvfjxv6qrvzzck" ); }); it("encodes events as notes when the relay is known", () => { const event = new NDKEvent(ndk, { kind: 1, + content: "hello world", pubkey: "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52", - tags: [["d", "1234"]], + tags: [["e", "1234"]], } as NostrEvent); event.relay = new NDKRelay("wss://relay.f7z.io/"); const a = event.encode(); expect(a).toBe( - "nevent1qgs04xzt6ldm9qhs0ctw0t58kf4z57umjzmjg6jywu0seadwtqqc75spzfmhxue69uhhyetvv9ujue3h0ghxjmcqqqwvqq2y" + "nevent1qgs04xzt6ldm9qhs0ctw0t58kf4z57umjzmjg6jywu0seadwtqqc75spzdmhxue69uhhyetvv9ujue3h0ghxjme0qqqqcmeuul" ); }); }); diff --git a/ndk/src/events/signature.ts b/ndk/src/events/signature.ts index df8edbf5..abdd37fc 100644 --- a/ndk/src/events/signature.ts +++ b/ndk/src/events/signature.ts @@ -1,8 +1,8 @@ -import { NDKEvent, NDKEventId } from "./index.js"; +import type { NDKEvent, NDKEventId } from "./index.js"; let worker: Worker | undefined; -let processingQueue: Record< +const processingQueue: Record< NDKEventId, { event: NDKEvent; resolves: ((result: boolean) => void)[] } > = {}; diff --git a/ndk/src/events/validation.ts b/ndk/src/events/validation.ts index 3a69dda4..342f70f9 100644 --- a/ndk/src/events/validation.ts +++ b/ndk/src/events/validation.ts @@ -31,7 +31,7 @@ export function validate(this: NDKEvent): boolean { return true; } -const verifiedEvents = new LRUCache({ +export const verifiedSignatures = new LRUCache({ maxSize: 1000, entryExpirationTimeInMS: 60000, }); @@ -44,9 +44,9 @@ const verifiedEvents = new LRUCache({ export function verifySignature(this: NDKEvent, persist: boolean): boolean | undefined { if (typeof this.signatureVerified === "boolean") return this.signatureVerified; - const prevVerification = verifiedEvents.get(this.id); + const prevVerification = verifiedSignatures.get(this.id); if (prevVerification !== null) { - return (this.signatureVerified = prevVerification); + return (this.signatureVerified = !!prevVerification); } try { @@ -54,17 +54,19 @@ export function verifySignature(this: NDKEvent, persist: boolean): boolean | und verifySignatureAsync(this, persist).then((result) => { if (persist) { this.signatureVerified = result; - verifiedEvents.set(this.id, result); + if (result) verifiedSignatures.set(this.id, this.sig!); } if (!result) { this.ndk!.emit("event:invalid-sig", this); + verifiedSignatures.set(this.id, false); } }); } else { const hash = sha256(new TextEncoder().encode(this.serialize())); const res = schnorr.verify(this.sig as string, hash, this.pubkey); - verifiedEvents.set(this.id, res); + if (res) verifiedSignatures.set(this.id, this.sig!); + else verifiedSignatures.set(this.id, false); return (this.signatureVerified = res); } } catch (err) { diff --git a/ndk/src/index.ts b/ndk/src/index.ts index 5e5c6e8e..ddcc9a5a 100644 --- a/ndk/src/index.ts +++ b/ndk/src/index.ts @@ -6,6 +6,7 @@ export * from "./events/index.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/highlight.js"; @@ -19,11 +20,15 @@ export * from "./events/kinds/subscriptions/amount.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 "./nwc/index.js"; export * from "./thread/index.js"; export * from "./events/kinds/simple-group/index.js"; +export * from "./events/kinds/simple-group/metadata.js"; +export * from "./events/kinds/simple-group/member-list.js"; export * from "./relay/index.js"; export * from "./relay/auth-policies.js"; @@ -44,6 +49,9 @@ export * from "./dvm/schedule.js"; export { type NDKEventSerialized, deserialize, serialize } from "./events/serializer.js"; export { NDK as default, NDKConstructorParams } from "./ndk/index.js"; export { NDKZapInvoice, zapInvoiceFromEvent } from "./zap/invoice.js"; -export * from "./zap/index.js"; +export * from "./zapper/index.js"; +export * from "./zapper/ln.js"; +export * from "./zapper/nip57.js"; +export * from "./zapper/nip61.js"; export * from "./utils/normalize-url.js"; export * from "./utils/get-users-relay-list.js"; diff --git a/ndk/src/ndk/active-user.ts b/ndk/src/ndk/active-user.ts index 4b5fe155..3d6cccdc 100644 --- a/ndk/src/ndk/active-user.ts +++ b/ndk/src/ndk/active-user.ts @@ -1,10 +1,10 @@ -import { NDK } from "./index.js"; -import { NDKRelayList } from "../events/kinds/NDKRelayList.js"; -import { NDKUser } from "../user/index.js"; +import type { NDK } from "./index.js"; +import type { NDKRelayList } from "../events/kinds/NDKRelayList.js"; +import type { NDKUser } from "../user/index.js"; import createDebug from "debug"; -import { NDKFilter } from "../subscription/index.js"; +import type { NDKFilter } from "../subscription/index.js"; import { NDKKind } from "../events/kinds/index.js"; -import { NDKEvent } from "../events/index.js"; +import type { 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"; @@ -20,7 +20,7 @@ async function getUserRelayList(this: NDK, user: NDKUser): Promise = new Map(); + const events: Map = new Map(); // Collect most recent version of these events sub.on("event", (event) => { diff --git a/ndk/src/ndk/fetch-event-from-tag.test.ts b/ndk/src/ndk/fetch-event-from-tag.test.ts index 40800637..bb833ee7 100644 --- a/ndk/src/ndk/fetch-event-from-tag.test.ts +++ b/ndk/src/ndk/fetch-event-from-tag.test.ts @@ -1,6 +1,7 @@ import { NDK } from "."; import { NDKEvent } from "../events"; -import { NDKSubscriptionCacheUsage, NDKSubscriptionOptions } from "../subscription"; +import type { NDKSubscriptionOptions } from "../subscription"; +import { NDKSubscriptionCacheUsage } from "../subscription"; const ndk = new NDK(); diff --git a/ndk/src/ndk/fetch-event-from-tag.ts b/ndk/src/ndk/fetch-event-from-tag.ts index 0794859e..5a8cf2f4 100644 --- a/ndk/src/ndk/fetch-event-from-tag.ts +++ b/ndk/src/ndk/fetch-event-from-tag.ts @@ -1,9 +1,9 @@ -import { NDK } from "."; -import { NDKEvent, NDKTag } from "../events"; +import type { NDK } from "."; +import type { NDKEvent, NDKTag } from "../events"; import { getRelaysForSync } from "../outbox/write"; import { NDKRelaySet } from "../relay/sets"; import { calculateRelaySetsFromFilters } from "../relay/sets/calculate"; -import { NDKSubscriptionOptions } from "../subscription"; +import type { NDKSubscriptionOptions } from "../subscription"; /** * Options on how to handle when a relay hint doesn't respond @@ -77,7 +77,6 @@ export async function fetchEventFromTag( // XXXXX subOpts = {}; - if (!isValidHint(hint)) return; d("fetching event from tag", tag, subOpts, fallback); @@ -94,7 +93,7 @@ export async function fetchEventFromTag( if (authorRelays && authorRelays.size > 0) { d("fetching event from author relays %o", Array.from(authorRelays)); const relaySet = NDKRelaySet.fromRelayUrls(Array.from(authorRelays), this); - let event = await this.fetchEvent(id, subOpts, relaySet); + const event = await this.fetchEvent(id, subOpts, relaySet); if (event) return event; } else { d("no author relays found for %s", originalEvent.pubkey, originalEvent); @@ -103,12 +102,12 @@ export async function fetchEventFromTag( // Attempt without relay hint on whatever NDK calculates const relaySet = calculateRelaySetsFromFilters(this, [{ ids: [id] }], this.pool); d("fetching event without relay hint", relaySet); - let event = await this.fetchEvent(id, subOpts); + const event = await this.fetchEvent(id, subOpts); if (event) return event; // If we didn't get the event, try to fetch in the relay hint if (hint && hint !== "") { - let event = await this.fetchEvent( + const event = await this.fetchEvent( id, subOpts, this.pool.getRelay(hint, true, true, [{ ids: [id] }]) @@ -118,7 +117,7 @@ export async function fetchEventFromTag( let result: NDKEvent | null | undefined = undefined; - let relay = isValidHint(hint) + const relay = isValidHint(hint) ? this.pool.getRelay(hint, false, true, [{ ids: [id] }]) : undefined; @@ -134,11 +133,11 @@ export async function fetchEventFromTag( /** * Fallback fetch promise. */ - let fallbackFetchPromise = new Promise(async (resolve) => { - let fallbackRelaySet = fallback.relaySet; + const fallbackFetchPromise = new Promise(async (resolve) => { + const fallbackRelaySet = fallback.relaySet; - let timeout = fallback.timeout ?? 1500; - let timeoutPromise = new Promise((resolve) => setTimeout(resolve, timeout)); + const timeout = fallback.timeout ?? 1500; + const timeoutPromise = new Promise((resolve) => setTimeout(resolve, timeout)); // if this is a timeout fallback, we need to wait for the timeout to resolve if (fallback.type === "timeout") await timeoutPromise; @@ -147,7 +146,7 @@ export async function fetchEventFromTag( resolve(result); } else { d("fallback fetch triggered"); - let fallbackEvent = await this.fetchEvent(id, subOpts, fallbackRelaySet); + const fallbackEvent = await this.fetchEvent(id, subOpts, fallbackRelaySet); resolve(fallbackEvent); } }); diff --git a/ndk/src/ndk/index.ts b/ndk/src/ndk/index.ts index 4a74b595..a6d7c986 100644 --- a/ndk/src/ndk/index.ts +++ b/ndk/src/ndk/index.ts @@ -3,11 +3,13 @@ import { EventEmitter } from "tseep"; import type { NDKCacheAdapter } from "../cache/index.js"; import dedupEvent from "../events/dedup.js"; -import { NDKEvent, NDKEventId, NDKTag } from "../events/index.js"; +import type { NDKEventId, NDKTag } from "../events/index.js"; +import { NDKEvent } from "../events/index.js"; import { OutboxTracker } from "../outbox/tracker.js"; import { NDKRelay } from "../relay/index.js"; import { NDKPool } from "../relay/pool/index.js"; -import { NDKRelaySet, NDKPublishError } from "../relay/sets/index.js"; +import type { NDKPublishError } from "../relay/sets/index.js"; +import { NDKRelaySet } from "../relay/sets/index.js"; import { correctRelaySet } from "../relay/sets/utils.js"; import type { NDKSigner } from "../signers/index.js"; import type { NDKFilter, NDKSubscriptionOptions } from "../subscription/index.js"; @@ -16,14 +18,28 @@ import { filterFromId, isNip33AValue, relaysFromBech32 } from "../subscription/u import type { Hexpubkey, NDKUserParams, ProfilePointer } from "../user/index.js"; import { NDKUser } from "../user/index.js"; import { fetchEventFromTag } from "./fetch-event-from-tag.js"; -import { NDKAuthPolicy } from "../relay/auth-policies.js"; +import type { NDKAuthPolicy } from "../relay/auth-policies.js"; import { Nip96 } from "../media/index.js"; import { NDKNwc } from "../nwc/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"; import { setActiveUser } from "./active-user.js"; +import { CashuPayCb, LnPayCb, NDKPaymentConfirmation, NDKZapConfirmation, NDKZapper, NDKZapSplit } from "../zapper/index.js"; +import type { NostrEvent } from "nostr-tools"; +import { NDKLnUrlData } from "../zapper/ln.js"; + +export type NDKValidationRatioFn = ( + relay: NDKRelay, + validatedCount: number, + nonValidatedCount: number +) => number; + +export interface NDKWalletConfig { + onLnPay?: LnPayCb; + onCashuPay?: CashuPayCb; + onPaymentComplete: (results: Map) => void; +} export interface NDKConstructorParams { /** @@ -119,14 +135,24 @@ export interface NDKConstructorParams { signatureVerificationWorker?: Worker | undefined; /** - * Specify a ratio of events that will be verified on a per relay basis. + * The signature verification validation ratio for new relays. + */ + initialValidationRatio?: number; + + /** + * The lowest validation ratio any single relay can have. * Relays will have a sample of events verified based on this ratio. - * When using this, you should definitely listen for event:invalid-sig events + * When using this, you MUST listen for event:invalid-sig events * to handle invalid signatures and disconnect from evil relays. * - * @default 1.0 + * @default 0.1 */ - validationRatio?: number; + lowestValidationRatio?: number; + + /** + * A function that is invoked to calculate the validation ratio for a relay. + */ + validationRatioFn?: NDKValidationRatioFn; } export interface GetUserParams extends NDKUserParams { @@ -195,7 +221,9 @@ export class NDK extends EventEmitter<{ public queuesZapConfig: Queue; public queuesNip05: Queue; public asyncSigVerification: boolean = false; - public validationRatio: number = 1.0; + public initialValidationRatio: number = 1.0; + public lowestValidationRatio: number = 1.0; + public validationRatioFn?: NDKValidationRatioFn; public subManager: NDKSubscriptionManager; public publishingFailureHandled = false; @@ -241,11 +269,14 @@ export class NDK extends EventEmitter<{ public autoConnectUserRelays = true; public autoFetchUserMutelist = true; + public walletConfig?: NDKWalletConfig; + public constructor(opts: NDKConstructorParams = {}) { super(); this.debug = opts.debug || debug("ndk"); this.explicitRelayUrls = opts.explicitRelayUrls || []; + this.subManager = new NDKSubscriptionManager(this.debug); this.pool = new NDKPool( opts.explicitRelayUrls || [], opts.blacklistRelayUrls || DEFAULT_BLACKLISTED_RELAYS, @@ -253,8 +284,6 @@ export class NDK extends EventEmitter<{ ); this.pool.name = "main"; - this.debug(`Starting with explicit relays: ${JSON.stringify(this.explicitRelayUrls)}`); - this.pool.on("relay:auth", async (relay: NDKRelay, challenge: string) => { if (this.relayAuthDefaultPolicy) { await this.relayAuthDefaultPolicy(relay, challenge); @@ -294,8 +323,8 @@ export class NDK extends EventEmitter<{ this.signatureVerificationWorker = opts.signatureVerificationWorker; - this.validationRatio = opts.validationRatio || 1.0; - this.subManager = new NDKSubscriptionManager(this.debug); + this.initialValidationRatio = opts.initialValidationRatio || 1.0; + this.lowestValidationRatio = opts.lowestValidationRatio || 1.0; try { this.httpFetch = fetch; @@ -324,7 +353,7 @@ export class NDK extends EventEmitter<{ let relay: NDKRelay; if (typeof urlOrRelay === "string") { - relay = new NDKRelay(urlOrRelay, relayAuthPolicy); + relay = new NDKRelay(urlOrRelay, relayAuthPolicy, this); } else { relay = urlOrRelay; } @@ -385,10 +414,13 @@ export class NDK extends EventEmitter<{ */ public async connect(timeoutMs?: number): Promise { if (this._signer && this.autoConnectUserRelays) { - this.debug("Attempting to connect to user relays specified by signer"); + this.debug( + "Attempting to connect to user relays specified by signer %o", + await this._signer.relays?.(this) + ); if (this._signer.relays) { - const relays = await this._signer.relays(); + const relays = await this._signer.relays(this); relays.forEach((relay) => this.pool.addRelay(relay)); } } @@ -399,7 +431,7 @@ export class NDK extends EventEmitter<{ connections.push(this.outboxPool.connect(timeoutMs)); } - this.debug("Connecting to relays", { timeoutMs }); + this.debug("Connecting to relays %o", { timeoutMs }); // eslint-disable-next-line @typescript-eslint/no-empty-function return Promise.allSettled(connections).then(() => {}); @@ -448,8 +480,7 @@ export class NDK extends EventEmitter<{ // Signal to the relays that they are explicitly being used if (relaySet) { for (const relay of relaySet.relays) { - this.pool.useTemporaryRelay(relay); - this.debug("Adding temporary relay %s for filters %o", relay.url, filters); + this.pool.useTemporaryRelay(relay, undefined, subscription.filters); } } @@ -527,7 +558,7 @@ export class NDK extends EventEmitter<{ if (!relaySetOrRelay && typeof idOrFilter === "string") { /* Check if this is a NIP-33 */ if (!isNip33AValue(idOrFilter)) { - const relays = relaysFromBech32(idOrFilter); + const relays = relaysFromBech32(idOrFilter, this); if (relays.length > 0) { relaySet = new NDKRelaySet(new Set(relays), this); @@ -607,7 +638,9 @@ export class NDK extends EventEmitter<{ false ); - const onEvent = (event: NDKEvent) => { + const onEvent = (event: NostrEvent | NDKEvent) => { + if (!(event instanceof NDKEvent)) event = new NDKEvent(undefined, event); + const dedupKey = event.deduplicationKey(); const existingEvent = events.get(dedupKey); @@ -622,7 +655,10 @@ export class NDK extends EventEmitter<{ // We want to inspect duplicated events // so we can dedup them relaySetSubscription.on("event", onEvent); - relaySetSubscription.on("event:dup", onEvent); + // relaySetSubscription.on("event:dup", (rawEvent: NostrEvent) => { + // const ndkEvent = new NDKEvent(undefined, rawEvent); + // onEvent(ndkEvent) + // }); relaySetSubscription.on("eose", () => { resolve(new Set(events.values())); @@ -674,7 +710,9 @@ export class NDK extends EventEmitter<{ } /** - * Create a zap request for an existing event + * Zap a user or an event + * + * This function wi * * @param amount The amount to zap in millisatoshis * @param comment A comment to add to the zap request @@ -682,30 +720,41 @@ export class NDK extends EventEmitter<{ * @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, + public zap( + target: NDKEvent | NDKUser, amount: number, - comment?: string, - extraTags?: NDKTag[], - recipient?: NDKUser, - signer?: NDKSigner - ): Promise { - if (!signer) { - this.assertSigner(); + { + comment, + unit, + signer, + tags, + onLnPay, + onCashuPay, + onComplete, + }: { + comment?: string; + unit?: string; + tags?: NDKTag[]; + onLnPay?: LnPayCb | false; + onCashuPay?: CashuPayCb | false; + onComplete?: (results: Map) => void; + signer?: NDKSigner; } + ): NDKZapper { + if (!signer) this.assertSigner(); - let zapOpts: ZapConstructorParams; + const zapper = new NDKZapper(target, amount, unit, comment, this, tags, signer); - 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"); - } + if (onLnPay !== false) zapper.onLnPay = onLnPay ?? this.walletConfig?.onLnPay; + if (onCashuPay !== false) zapper.onCashuPay = onCashuPay ?? this.walletConfig?.onCashuPay; + zapper.onComplete = onComplete ?? this.walletConfig?.onPaymentComplete; - const zap = new NDKZap(zapOpts); + /** + * If there is a wallet configured to handle payments, we start + * zapping + */ + if (onLnPay) zapper.zap(); - return zap.createZapRequest(amount, comment, extraTags, undefined, signer); + return zapper; } } diff --git a/ndk/src/nwc/get_info.ts b/ndk/src/nwc/get_info.ts index 04def932..305a241f 100644 --- a/ndk/src/nwc/get_info.ts +++ b/ndk/src/nwc/get_info.ts @@ -1,4 +1,4 @@ -import { NDKNWcCommands, NDKNwc, NDKNwcResponse } from "."; +import type { NDKNWcCommands, NDKNwc, NDKNwcResponse } from "."; export interface GetInfoResponse { alias?: string; diff --git a/ndk/src/outbox/index.ts b/ndk/src/outbox/index.ts index 270d31d8..740ae800 100644 --- a/ndk/src/outbox/index.ts +++ b/ndk/src/outbox/index.ts @@ -1,6 +1,6 @@ -import { NDK } from "../ndk"; -import { NDKRelay } from "../relay"; -import { Hexpubkey } from "../user"; +import type { NDK } from "../ndk"; +import type { NDKRelay } from "../relay"; +import type { Hexpubkey } from "../user"; import { getTopRelaysForAuthors } from "./relay-ranking"; import { getRelaysForSync } from "./write"; @@ -46,11 +46,11 @@ export function chooseRelayCombinationForPubkeys( ndk: NDK, pubkeys: Hexpubkey[], type: "write" | "read", - { count, preferredRelays, }: { count?: number; preferredRelays?: Set} = {} + { count, preferredRelays }: { count?: number; preferredRelays?: Set } = {} ): Map { - count ??= 2 + count ??= 2; preferredRelays ??= new Set(); - + const pool = ndk.pool; const connectedRelays = pool.connectedRelays(); @@ -58,7 +58,7 @@ export function chooseRelayCombinationForPubkeys( connectedRelays.forEach((relay) => { preferredRelays!.add(relay.url); }); - + const relayToAuthorsMap = new Map(); const { pubkeysToRelays, authorsMissingRelays } = getAllRelaysForAllPubkeys(ndk, pubkeys, type); @@ -115,4 +115,4 @@ export function chooseRelayCombinationForPubkeys( } return relayToAuthorsMap; -} \ No newline at end of file +} diff --git a/ndk/src/outbox/read/with-authors.ts b/ndk/src/outbox/read/with-authors.ts index bd25fc73..a77fcb13 100644 --- a/ndk/src/outbox/read/with-authors.ts +++ b/ndk/src/outbox/read/with-authors.ts @@ -1,8 +1,8 @@ import { chooseRelayCombinationForPubkeys, getAllRelaysForAllPubkeys } from ".."; -import { NDK } from "../../ndk"; +import type { NDK } from "../../ndk"; import { NDKRelay } from "../../relay"; import { NDKPool } from "../../relay/pool"; -import { Hexpubkey } from "../../user"; +import type { Hexpubkey } from "../../user"; import { getTopRelaysForAuthors } from "../relay-ranking"; import { getWriteRelaysFor, getRelaysForSync } from "../write"; @@ -20,5 +20,5 @@ export function getRelaysForFilterWithAuthors( authors: Hexpubkey[], relayGoalPerAuthor: number = 2 ): Map { - return chooseRelayCombinationForPubkeys(ndk, authors, 'write', { count: relayGoalPerAuthor }); + return chooseRelayCombinationForPubkeys(ndk, authors, "write", { count: relayGoalPerAuthor }); } diff --git a/ndk/src/outbox/relay-ranking.ts b/ndk/src/outbox/relay-ranking.ts index 4e263fe2..bd85de00 100644 --- a/ndk/src/outbox/relay-ranking.ts +++ b/ndk/src/outbox/relay-ranking.ts @@ -1,5 +1,5 @@ -import { NDK } from "../ndk"; -import { Hexpubkey } from "../user"; +import type { NDK } from "../ndk"; +import type { Hexpubkey } from "../user"; import { getRelaysForSync } from "./write"; export function getTopRelaysForAuthors(ndk: NDK, authors: Hexpubkey[]): WebSocket["url"][] { @@ -21,7 +21,7 @@ export function getTopRelaysForAuthors(ndk: NDK, authors: Hexpubkey[]): WebSocke */ // Sort the relays by the number of authors that write to them - let sortedRelays = Array.from(relaysWithCount.entries()).sort((a, b) => b[1] - a[1]); + const sortedRelays = Array.from(relaysWithCount.entries()).sort((a, b) => b[1] - a[1]); return sortedRelays.map((entry) => entry[0]); } diff --git a/ndk/src/outbox/tracker.ts b/ndk/src/outbox/tracker.ts index e9550f3a..fb109de3 100644 --- a/ndk/src/outbox/tracker.ts +++ b/ndk/src/outbox/tracker.ts @@ -1,7 +1,7 @@ import { EventEmitter } from "tseep"; import { LRUCache } from "typescript-lru-cache"; -import { NDKRelayList } from "../events/kinds/NDKRelayList.js"; +import type { 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"; @@ -67,18 +67,15 @@ export class OutboxTracker extends EventEmitter { /** * Adds a list of users to the tracker. - * @param items - * @param skipCache + * @param items + * @param skipCache */ - async trackUsers( - items: NDKUser[] | Hexpubkey[], - skipCache = false, - ) { + async trackUsers(items: NDKUser[] | Hexpubkey[], skipCache = false) { const promises: Promise[] = []; for (let i = 0; i < items.length; i += 400) { const slice = items.slice(i, i + 400); - let pubkeys = slice + const pubkeys = slice .map((item) => getKeyFromItem(item)) .filter((pubkey) => !this.data.has(pubkey)); // filter out items that are already being tracked @@ -90,48 +87,53 @@ export class OutboxTracker extends EventEmitter { this.data.set(pubkey, new OutboxItem("user")); } - promises.push(new Promise((resolve) => { - getRelayListForUsers(pubkeys, this.ndk, skipCache).then( - (relayLists: Map) => { - for (const [pubkey, relayList] of relayLists) { - let outboxItem = this.data.get(pubkey)!; - outboxItem ??= new OutboxItem("user"); - - if (relayList) { - outboxItem.readRelays = new Set(normalize(relayList.readRelayUrls)); - outboxItem.writeRelays = new Set(normalize(relayList.writeRelayUrls)); - - // remove all blacklisted relays - for (const relayUrl of outboxItem.readRelays) { - if (this.ndk.pool.blacklistRelayUrls.has(relayUrl)) { - // this.debug( - // `removing blacklisted relay ${relayUrl} from read relays` - // ); - outboxItem.readRelays.delete(relayUrl); + promises.push( + new Promise((resolve) => { + getRelayListForUsers(pubkeys, this.ndk, skipCache) + .then((relayLists: Map) => { + for (const [pubkey, relayList] of relayLists) { + let outboxItem = this.data.get(pubkey)!; + outboxItem ??= new OutboxItem("user"); + + if (relayList) { + outboxItem.readRelays = new Set( + normalize(relayList.readRelayUrls) + ); + outboxItem.writeRelays = new Set( + normalize(relayList.writeRelayUrls) + ); + + // remove all blacklisted relays + for (const relayUrl of outboxItem.readRelays) { + if (this.ndk.pool.blacklistRelayUrls.has(relayUrl)) { + // this.debug( + // `removing blacklisted relay ${relayUrl} from read relays` + // ); + outboxItem.readRelays.delete(relayUrl); + } } - } - // remove all blacklisted relays - for (const relayUrl of outboxItem.writeRelays) { - if (this.ndk.pool.blacklistRelayUrls.has(relayUrl)) { - // this.debug( - // `removing blacklisted relay ${relayUrl} from write relays` - // ); - outboxItem.writeRelays.delete(relayUrl); + // remove all blacklisted relays + for (const relayUrl of outboxItem.writeRelays) { + if (this.ndk.pool.blacklistRelayUrls.has(relayUrl)) { + // this.debug( + // `removing blacklisted relay ${relayUrl} from write relays` + // ); + outboxItem.writeRelays.delete(relayUrl); + } } - } - this.data.set(pubkey, outboxItem); + this.data.set(pubkey, outboxItem); - // this.debug( - // `Adding ${outboxItem.readRelays.size} read relays and ${outboxItem.writeRelays.size} write relays for ${pubkey}, %o`, relayList?.rawEvent() - // ); + // this.debug( + // `Adding ${outboxItem.readRelays.size} read relays and ${outboxItem.writeRelays.size} write relays for ${pubkey}, %o`, relayList?.rawEvent() + // ); + } } - } - } - ) - .finally(resolve); - })); + }) + .finally(resolve); + }) + ); } return Promise.all(promises); diff --git a/ndk/src/outbox/write.ts b/ndk/src/outbox/write.ts index bad979ac..64824f47 100644 --- a/ndk/src/outbox/write.ts +++ b/ndk/src/outbox/write.ts @@ -1,5 +1,5 @@ -import { NDK } from "../ndk"; -import { Hexpubkey } from "../user"; +import type { NDK } from "../ndk"; +import type { Hexpubkey } from "../user"; /** * Gets write relays for a given pubkey as tracked by the outbox tracker. @@ -35,4 +35,4 @@ export async function getWriteRelaysFor( } return getRelaysForSync(ndk, author, type); -} \ No newline at end of file +} diff --git a/ndk/src/relay/auth-policies.ts b/ndk/src/relay/auth-policies.ts index 03abc64d..39032c61 100644 --- a/ndk/src/relay/auth-policies.ts +++ b/ndk/src/relay/auth-policies.ts @@ -1,9 +1,9 @@ -import { NDKRelay } from "."; +import type { NDKRelay } from "."; import { NDKEvent } from "../events"; import { NDKKind } from "../events/kinds"; -import { NDK } from "../ndk"; -import { NDKSigner } from "../signers"; -import { NDKPool } from "./pool"; +import type { NDK } from "../ndk"; +import type { NDKSigner } from "../signers"; +import type { NDKPool } from "./pool"; import createDebug from "debug"; /** @@ -52,7 +52,9 @@ async function signAndAuth( } /** - * Uses the signer to sign an event and then authenticate with the relay. If no signer is provided the NDK signer will be used. If none is not available it will wait for one to be ready. + * Uses the signer to sign an event and then authenticate with the relay. + * If no signer is provided the NDK signer will be used. + * If none is not available it will wait for one to be ready. */ function signIn({ ndk, signer, debug }: ISignIn = {}) { debug ??= createDebug("ndk:auth-policies:signIn"); @@ -70,6 +72,7 @@ function signIn({ ndk, signer, debug }: ISignIn = {}) { signer ??= ndk?.signer; // If we dont have a signer, we need to wait for one to be ready + // eslint-disable-next-line no-async-promise-executor return new Promise(async (resolve, reject) => { if (signer) { await signAndAuth(event, relay, signer, debug!, resolve, reject); diff --git a/ndk/src/relay/connectivity.test.ts b/ndk/src/relay/connectivity.test.ts index c6d56fd2..b672713a 100644 --- a/ndk/src/relay/connectivity.test.ts +++ b/ndk/src/relay/connectivity.test.ts @@ -1,67 +1,122 @@ -import { NDKRelayConnectivity } from "./connectivity.js"; -import { NDKRelay, NDKRelayStatus } from "./index.js"; +import { NDKRelayConnectivity } from "./connectivity"; +import { NDKRelay, NDKRelayStatus } from "./index"; +import { NDK } from "../ndk/index"; + +jest.mock("ws"); +jest.useFakeTimers(); describe("NDKRelayConnectivity", () => { - let ndkRelayConnectivity: NDKRelayConnectivity; - let ndkRelayMock: NDKRelay; - let relayConnectSpy: jest.SpyInstance; - let relayDisconnectSpy: jest.SpyInstance; - - beforeEach(async () => { - ndkRelayMock = new NDKRelay("ws://localhost"); - ndkRelayConnectivity = new NDKRelayConnectivity(ndkRelayMock); - // Mock the connect method on nostr tools relay - relayConnectSpy = jest.spyOn(ndkRelayConnectivity.relay, "connect").mockResolvedValue(); - // Mock the close method on the nostr tools relay - relayDisconnectSpy = jest.spyOn(ndkRelayConnectivity.relay, "close").mockReturnValue(); - await ndkRelayConnectivity.connect(); + let ndk: NDK; + let relay: NDKRelay; + let connectivity: NDKRelayConnectivity; + + beforeEach(() => { + ndk = new NDK(); + relay = new NDKRelay("wss://test.relay"); + connectivity = new NDKRelayConnectivity(relay, ndk); }); afterEach(() => { - jest.restoreAllMocks(); + jest.clearAllMocks(); }); - describe("connecting", () => { - it("calls connect() on the nostr-tools AbstractRelay instance", () => { - expect(relayConnectSpy).toHaveBeenCalled(); + describe("connect", () => { + it("should set status to CONNECTING when disconnected", async () => { + await connectivity.connect(); + expect(connectivity.status).toBe(NDKRelayStatus.CONNECTING); }); - it("updates connected status properly", () => { - // Check that we updated our status - expect(ndkRelayConnectivity.status).toBe(NDKRelayStatus.CONNECTED); - // Check that we're available - expect(ndkRelayConnectivity.isAvailable()).toBe(true); + it("should set status to RECONNECTING when not disconnected", async () => { + connectivity["_status"] = NDKRelayStatus.CONNECTED; + await connectivity.connect(); + expect(connectivity.status).toBe(NDKRelayStatus.RECONNECTING); }); - it("updates connectionStats on connect", () => { - expect(ndkRelayConnectivity.connectionStats.attempts).toBe(1); - expect(ndkRelayConnectivity.connectionStats.connectedAt).toBeDefined(); + it("should create a new WebSocket connection", async () => { + const mockWebSocket = jest.fn(); + global.WebSocket = mockWebSocket as any; + + await connectivity.connect(); + expect(mockWebSocket).toHaveBeenCalledWith("wss://test.relay/"); }); }); - // TODO: Test auth - - describe("disconnecting", () => { + describe("disconnect", () => { beforeEach(() => { - ndkRelayConnectivity.disconnect(); + connectivity["_status"] = NDKRelayStatus.CONNECTED; + }); + it("should set status to DISCONNECTING", () => { + connectivity.disconnect(); + expect(connectivity.status).toBe(NDKRelayStatus.DISCONNECTING); + }); + + it("should close the WebSocket connection", () => { + const mockClose = jest.fn(); + connectivity["ws"] = { close: mockClose } as any; + connectivity.disconnect(); + expect(mockClose).toHaveBeenCalled(); + }); + + it("should handle disconnect error", () => { + const mockClose = jest.fn(() => { + throw new Error("Disconnect failed"); + }); + connectivity["ws"] = { close: mockClose } as any; + connectivity.disconnect(); + expect(connectivity.status).toBe(NDKRelayStatus.DISCONNECTED); }); + }); - it("disconnects from the relay", async () => { - expect(relayDisconnectSpy).toHaveBeenCalled(); + describe("isAvailable", () => { + it("should return true when status is CONNECTED", () => { + connectivity["_status"] = NDKRelayStatus.CONNECTED; + expect(connectivity.isAvailable()).toBe(true); }); - it("updates connected status properly", () => { - expect(ndkRelayConnectivity.status).toBe(NDKRelayStatus.DISCONNECTING); - expect(ndkRelayConnectivity.isAvailable()).toBe(false); + it("should return false when status is not CONNECTED", () => { + connectivity["_status"] = NDKRelayStatus.DISCONNECTED; + expect(connectivity.isAvailable()).toBe(false); }); + }); - // Test that onclose callback was properly called - it.skip("updates the connectionStats for disconnect", () => { - expect(ndkRelayConnectivity.connectionStats.connectedAt).toBe(undefined); - expect(ndkRelayConnectivity.connectionStats.durations.length).toBe(1); + describe("send", () => { + it("should send message when connected and WebSocket is open", async () => { + const mockSend = jest.fn(); + connectivity["_status"] = NDKRelayStatus.CONNECTED; + connectivity["ws"] = { readyState: WebSocket.OPEN, send: mockSend } as any; + await connectivity.send("test message"); + expect(mockSend).toHaveBeenCalledWith("test message"); }); - // TODO: Can we test the emit on NDKRelay? - // TODO: Test reconnection logic (disconnect called from AbstractRelay) + it("should throw error when not connected", async () => { + connectivity["_status"] = NDKRelayStatus.DISCONNECTED; + await expect(connectivity.send("test message")).rejects.toThrow( + "Attempting to send on a closed relay connection" + ); + }); + }); + + describe("publish", () => { + it("should send EVENT message and return a promise", async () => { + const mockSend = jest.spyOn(connectivity, "send").mockResolvedValue(undefined); + const event = { id: "test-id", content: "test-content" }; + const publishPromise = connectivity.publish(event as any); + expect(mockSend).toHaveBeenCalledWith( + '["EVENT",{"id":"test-id","content":"test-content"}]' + ); + expect(publishPromise).toBeInstanceOf(Promise); + }); + }); + + describe("count", () => { + it("should send COUNT message and return a promise", async () => { + const mockSend = jest.spyOn(connectivity, "send").mockResolvedValue(undefined); + const filters = [{ authors: ["test-author"] }]; + const countPromise = connectivity.count(filters, {}); + expect(mockSend).toHaveBeenCalledWith( + expect.stringMatching(/^\["COUNT","count:\d+",\{"authors":\["test-author"\]\}\]$/) + ); + expect(countPromise).toBeInstanceOf(Promise); + }); }); }); diff --git a/ndk/src/relay/connectivity.ts b/ndk/src/relay/connectivity.ts index d8e0ca95..e52c29c7 100644 --- a/ndk/src/relay/connectivity.ts +++ b/ndk/src/relay/connectivity.ts @@ -1,16 +1,29 @@ -import { EventTemplate, NostrEvent, Relay, VerifiedEvent } from "nostr-tools"; import type { NDKRelay, NDKRelayConnectionStats } from "."; import { NDKRelayStatus } from "."; -import { runWithTimeout } from "../utils/timeout"; import { NDKEvent } from "../events/index.js"; -import { NDK } from "../ndk/index.js"; +import type { NDK } from "../ndk/index.js"; +import type { NostrEvent } from "../events/index.js"; +import type { NDKFilter } from "../subscription"; +import { NDKKind } from "../events/kinds"; +import type { NDKRelaySubscription } from "./subscription"; const MAX_RECONNECT_ATTEMPTS = 5; +const FLAPPING_THRESHOLD_MS = 1000; + +export type CountResolver = { + resolve: (count: number) => void; + reject: (err: Error) => void; +}; + +export type EventPublishResolver = { + resolve: (reason: string) => void; + reject: (err: Error) => void; +}; export class NDKRelayConnectivity { private ndkRelay: NDKRelay; + private ws?: WebSocket; private _status: NDKRelayStatus; - public relay: Relay; private timeoutMs?: number; private connectedAt?: number; private _connectionStats: NDKRelayConnectionStats = { @@ -19,85 +32,64 @@ export class NDKRelayConnectivity { durations: [], }; private debug: debug.Debugger; + private connectTimeout: ReturnType | undefined; private reconnectTimeout: ReturnType | undefined; private ndk?: NDK; + public openSubs: Map = new Map(); + private openCountRequests = new Map(); + private openEventPublishes = new Map(); + private serial: number = 0; + public baseEoseTimeout: number = 4_400; constructor(ndkRelay: NDKRelay, ndk?: NDK) { this.ndkRelay = ndkRelay; this._status = NDKRelayStatus.DISCONNECTED; - this.relay = new Relay(this.ndkRelay.url); - this.debug = this.ndkRelay.debug.extend("connectivity"); + const rand = Math.floor(Math.random() * 1000); + this.debug = this.ndkRelay.debug.extend("connectivity" + rand); this.ndk = ndk; - - this.relay.onnotice = (notice: string) => this.handleNotice(notice); } + /** + * Connects to the NDK relay and handles the connection lifecycle. + * + * This method attempts to establish a WebSocket connection to the NDK relay specified in the `ndkRelay` object. + * If the connection is successful, it updates the connection statistics, sets the connection status to `CONNECTED`, + * and emits `connect` and `ready` events on the `ndkRelay` object. + * + * If the connection attempt fails, it handles the error by either initiating a reconnection attempt or emitting a + * `delayed-connect` event on the `ndkRelay` object, depending on the `reconnect` parameter. + * + * @param timeoutMs - The timeout in milliseconds for the connection attempt. If not provided, the default timeout from the `ndkRelay` object is used. + * @param reconnect - Indicates whether a reconnection should be attempted if the connection fails. Defaults to `true`. + * @returns A Promise that resolves when the connection is established, or rejects if the connection fails. + */ public async connect(timeoutMs?: number, reconnect = true): Promise { + if (this._status !== NDKRelayStatus.DISCONNECTED || this.reconnectTimeout) { + this.debug( + "Relay requested to be connected but was in state %s or it had a reconnect timeout", + this._status + ); + return; + } + if (this.reconnectTimeout) { clearTimeout(this.reconnectTimeout); this.reconnectTimeout = undefined; } + if (this.connectTimeout) { + clearTimeout(this.connectTimeout); + this.connectTimeout = undefined; + } + timeoutMs ??= this.timeoutMs; if (!this.timeoutMs && timeoutMs) this.timeoutMs = timeoutMs; - const connectHandler = () => { - this.updateConnectionStats.connected(); - - this._status = NDKRelayStatus.CONNECTED; - this.ndkRelay.emit("connect"); - this.ndkRelay.emit("ready"); - }; - - const disconnectHandler = () => { - this.updateConnectionStats.disconnected(); - - if (this._status === NDKRelayStatus.CONNECTED) { - this._status = NDKRelayStatus.DISCONNECTED; - - this.handleReconnection(); - } - this.ndkRelay.emit("disconnect"); - }; - - const authHandler = async (challenge: string) => { - const authPolicy = this.ndkRelay.authPolicy ?? this.ndk?.relayAuthDefaultPolicy; - - this.debug("Relay requested authentication", { - havePolicy: !!authPolicy, - }); - - if (authPolicy) { - if (this._status !== NDKRelayStatus.AUTHENTICATING) { - this._status = NDKRelayStatus.AUTHENTICATING; - const res = await authPolicy(this.ndkRelay, challenge); - this.debug("Authentication policy returned", !!res); - - if (res instanceof NDKEvent) { - this.relay.auth(async (evt: EventTemplate): Promise => { - return res.rawEvent() as VerifiedEvent; - }); - } - - if (res === true) { - if (!this.ndk?.signer) { - throw new Error("No signer available for authentication"); - } else if (this._status === NDKRelayStatus.AUTHENTICATING) { - this.debug("Authentication policy finished"); - this.relay.auth(async (evt: EventTemplate): Promise => { - const event = new NDKEvent(this.ndk, evt as NostrEvent); - await event.sign(); - return event.rawEvent() as VerifiedEvent; - }); - this._status = NDKRelayStatus.CONNECTED; - this.ndkRelay.emit("authed"); - } - } - } - } else { - this.ndkRelay.emit("auth", challenge); - } - }; + if (this.timeoutMs) + this.connectTimeout = setTimeout( + () => this.onConnectionError(reconnect), + this.timeoutMs + ); try { this.updateConnectionStats.attempt(); @@ -105,23 +97,13 @@ export class NDKRelayConnectivity { this._status = NDKRelayStatus.CONNECTING; else this._status = NDKRelayStatus.RECONNECTING; - this.relay.onclose = disconnectHandler; - this.relay._onauth = authHandler; - - // We have to call bind here otherwise the relay object isn't available in the runWithTimeout function - await runWithTimeout( - this.relay.connect.bind(this.relay), - timeoutMs, - "Timed out while connecting" - ) - .then(() => { - connectHandler(); - }) - .catch((e) => { - this.debug("Failed to connect", this.relay.url, e); - }); + this.ws = new WebSocket(this.ndkRelay.url); + this.ws.onopen = this.onConnect.bind(this); + this.ws.onclose = this.onDisconnect.bind(this); + this.ws.onmessage = this.onMessage.bind(this); + this.ws.onerror = this.onError.bind(this); } catch (e) { - // this.debug("Failed to connect", e); + this.debug(`Failed to connect to ${this.ndkRelay.url}`, e); this._status = NDKRelayStatus.DISCONNECTED; if (reconnect) this.handleReconnection(); else this.ndkRelay.emit("delayed-connect", 2 * 24 * 60 * 60 * 1000); @@ -129,26 +111,281 @@ export class NDKRelayConnectivity { } } + /** + * Disconnects the WebSocket connection to the NDK relay. + * This method sets the connection status to `NDKRelayStatus.DISCONNECTING`, + * attempts to close the WebSocket connection, and sets the status to + * `NDKRelayStatus.DISCONNECTED` if the disconnect operation fails. + */ public disconnect(): void { this._status = NDKRelayStatus.DISCONNECTING; try { - this.relay.close(); + this.ws?.close(); } catch (e) { this.debug("Failed to disconnect", e); this._status = NDKRelayStatus.DISCONNECTED; } } + /** + * Handles the error that occurred when attempting to connect to the NDK relay. + * If `reconnect` is `true`, this method will initiate a reconnection attempt. + * Otherwise, it will emit a `delayed-connect` event on the `ndkRelay` object, + * indicating that a reconnection should be attempted after a delay. + * + * @param reconnect - Indicates whether a reconnection should be attempted. + */ + onConnectionError(reconnect: boolean): void { + this.debug(`Error connecting to ${this.ndkRelay.url}`, this.timeoutMs); + if (reconnect && !this.reconnectTimeout) { + this.handleReconnection(); + } + } + + /** + * Handles the connection event when the WebSocket connection is established. + * This method is called when the WebSocket connection is successfully opened. + * It clears any existing connection and reconnection timeouts, updates the connection statistics, + * sets the connection status to `CONNECTED`, and emits `connect` and `ready` events on the `ndkRelay` object. + */ + private onConnect() { + if (this.reconnectTimeout) { + clearTimeout(this.reconnectTimeout); + this.reconnectTimeout = undefined; + } + if (this.connectTimeout) { + clearTimeout(this.connectTimeout); + this.connectTimeout = undefined; + } + this.updateConnectionStats.connected(); + this._status = NDKRelayStatus.CONNECTED; + this.ndkRelay.emit("connect"); + this.ndkRelay.emit("ready"); + } + + /** + * Handles the disconnection event when the WebSocket connection is closed. + * This method is called when the WebSocket connection is successfully closed. + * It updates the connection statistics, sets the connection status to `DISCONNECTED`, + * initiates a reconnection attempt if we didn't disconnect ourselves, + * and emits a `disconnect` event on the `ndkRelay` object. + */ + private onDisconnect() { + this.updateConnectionStats.disconnected(); + + // if (this._status === NDKRelayStatus.CONNECTED) { + this._status = NDKRelayStatus.DISCONNECTED; + + // this.handleReconnection(); + // } + // this.ndkRelay.emit("disconnect"); + } + + /** + * Handles incoming messages from the NDK relay WebSocket connection. + * This method is called whenever a message is received from the relay. + * It parses the message data and dispatches the appropriate handling logic based on the message type. + * + * @param event - The MessageEvent containing the received message data. + */ + private onMessage(event: MessageEvent): void { + try { + const data = JSON.parse(event.data); + const [cmd, id, ...rest] = data; + + switch (cmd) { + case "EVENT": { + const so = this.openSubs.get(id) as NDKRelaySubscription; + const event = data[2] as NostrEvent; + if (!so) { + this.debug(`Received event for unknown subscription ${id}`); + return; + } + so.onevent(event); + return; + } + case "COUNT": { + const payload = data[2] as { count: number }; + const cr = this.openCountRequests.get(id) as CountResolver; + if (cr) { + cr.resolve(payload.count); + this.openCountRequests.delete(id); + } + return; + } + case "EOSE": { + const so = this.openSubs.get(id); + if (!so) return; + so.oneose(); + return; + } + case "OK": { + const ok: boolean = data[2]; + const reason: string = data[3]; + const ep = this.openEventPublishes.get(id) as EventPublishResolver[] | undefined; + const firstEp = ep?.pop(); + + if (!ep || !firstEp) { + this.debug("Received OK for unknown event publish", id); + return; + } + + if (ok) firstEp.resolve(reason); + else firstEp.reject(new Error(reason)); + + if (ep.length === 0) { + this.openEventPublishes.delete(id); + } else { + this.openEventPublishes.set(id, ep); + } + return; + } + case "CLOSED": { + const so = this.openSubs.get(id); + if (!so) return; + so.onclosed(data[2] as string); + return; + } + case "NOTICE": + this.onNotice(data[1] as string); + return; + case "AUTH": { + this.onAuthRequested(data[1] as string); + return; + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (error: any) { + this.debug( + `Error parsing message from ${this.ndkRelay.url}: ${error.message}`, + error?.stack + ); + return; + } + } + + /** + * Handles an authentication request from the NDK relay. + * + * If an authentication policy is configured, it will be used to authenticate the connection. + * Otherwise, the `auth` event will be emitted to allow the application to handle the authentication. + * + * @param challenge - The authentication challenge provided by the NDK relay. + */ + private async onAuthRequested(challenge: string) { + const authPolicy = this.ndkRelay.authPolicy ?? this.ndk?.relayAuthDefaultPolicy; + + this.debug("Relay requested authentication", { + havePolicy: !!authPolicy, + }); + + if (this._status === NDKRelayStatus.AUTHENTICATING) { + this.debug("Already authenticating, ignoring"); + return; + } + + this._status = NDKRelayStatus.AUTH_REQUESTED; + + if (authPolicy) { + if (this._status >= NDKRelayStatus.CONNECTED) { + this._status = NDKRelayStatus.AUTHENTICATING; + let res: boolean | NDKEvent | undefined | void; + try { + res = await authPolicy(this.ndkRelay, challenge); + } catch (e) { + this.debug("Authentication policy threw an error", e); + res = false; + } + this.debug("Authentication policy returned", !!res); + + if (res instanceof NDKEvent || res === true) { + if (res instanceof NDKEvent) { + await this.auth(res); + } + + const authenticate = async () => { + if ( + this._status >= NDKRelayStatus.CONNECTED && + this._status < NDKRelayStatus.AUTHENTICATED + ) { + const event = new NDKEvent(this.ndk); + event.kind = NDKKind.ClientAuth; + event.tags = [ + ["relay", this.ndkRelay.url], + ["challenge", challenge], + ]; + await event.sign(); + this.auth(event) + .then(() => { + this._status = NDKRelayStatus.AUTHENTICATED; + this.ndkRelay.emit("authed"); + this.debug("Authentication successful"); + }) + .catch((e) => { + this._status = NDKRelayStatus.AUTH_REQUESTED; + this.ndkRelay.emit("auth:failed", e); + this.debug("Authentication failed", e); + }); + } else { + this.debug( + "Authentication failed, it changed status, status is %d", + this._status + ); + } + }; + + if (res === true) { + if (!this.ndk?.signer) { + this.debug("No signer available for authentication localhost"); + this.ndk?.once("signer:ready", authenticate); + } else { + authenticate().catch((e) => { + console.error("Error authenticating", e); + }); + } + } + + this._status = NDKRelayStatus.CONNECTED; + this.ndkRelay.emit("authed"); + } + } + } else { + this.ndkRelay.emit("auth", challenge); + } + } + + /** + * Handles errors that occur on the WebSocket connection to the NDK relay. + * @param error - The error or event that occurred. + */ + private onError(error: Error | Event): void { + this.debug(`WebSocket error on ${this.ndkRelay.url}:`, error); + } + + /** + * Gets the current status of the NDK relay connection. + * @returns {NDKRelayStatus} The current status of the NDK relay connection. + */ get status(): NDKRelayStatus { return this._status; } + /** + * Checks if the NDK relay connection is currently available. + * @returns {boolean} `true` if the relay connection is in the `CONNECTED` status, `false` otherwise. + */ public isAvailable(): boolean { return this._status === NDKRelayStatus.CONNECTED; } /** - * Evaluates the connection stats to determine if the relay is flapping. + * Checks if the NDK relay connection is flapping, which means the connection is rapidly + * disconnecting and reconnecting. This is determined by analyzing the durations of the + * last three connection attempts. If the standard deviation of the durations is less + * than 1000 milliseconds, the connection is considered to be flapping. + * + * @returns {boolean} `true` if the connection is flapping, `false` otherwise. */ private isFlapping(): boolean { const durations = this._connectionStats.durations; @@ -160,36 +397,36 @@ export class NDKRelayConnectivity { durations.map((x) => Math.pow(x - avg, 2)).reduce((a, b) => a + b, 0) / durations.length; const stdDev = Math.sqrt(variance); - const isFlapping = stdDev < 1000; + const isFlapping = stdDev < FLAPPING_THRESHOLD_MS; return isFlapping; } - private async handleNotice(notice: string) { - // This is a prototype; if the relay seems to be complaining - // remove it from relay set selection for a minute. - if (notice.includes("oo many") || notice.includes("aximum")) { - this.disconnect(); - - // fixme - setTimeout(() => this.connect(), 2000); - this.debug(this.relay.url, "Relay complaining?", notice); - // this.complaining = true; - // setTimeout(() => { - // this.complaining = false; - // console.log(this.relay.url, 'Reactivate relay'); - // }, 60000); - } - + /** + * Handles a notice received from the NDK relay. + * If the notice indicates the relay is complaining (e.g. "too many" or "maximum"), + * the method disconnects from the relay and attempts to reconnect after a 2-second delay. + * A debug message is logged with the relay URL and the notice text. + * The "notice" event is emitted on the ndkRelay instance with the notice text. + * + * @param notice - The notice text received from the NDK relay. + */ + private async onNotice(notice: string) { this.ndkRelay.emit("notice", notice); } /** - * Called when the relay is unexpectedly disconnected. + * Attempts to reconnect to the NDK relay after a connection is lost. + * This function is called recursively to handle multiple reconnection attempts. + * It checks if the relay is flapping and emits a "flapping" event if so. + * It then calculates a delay before the next reconnection attempt based on the number of previous attempts. + * The function sets a timeout to execute the next reconnection attempt after the calculated delay. + * If the maximum number of reconnection attempts is reached, a debug message is logged. + * + * @param attempt - The current attempt number (default is 0). */ private handleReconnection(attempt = 0): void { if (this.reconnectTimeout) return; - this.debug("Attempting to reconnect", { attempt }); if (this.isFlapping()) { this.ndkRelay.emit("flapping", this._connectionStats); @@ -205,21 +442,17 @@ export class NDKRelayConnectivity { this.reconnectTimeout = undefined; this._status = NDKRelayStatus.RECONNECTING; // this.debug(`Reconnection attempt #${attempt}`); - this.connect() - .then(() => { - this.debug("Reconnected"); - }) - .catch((err) => { - // this.debug("Reconnect failed", err); - - if (attempt < MAX_RECONNECT_ATTEMPTS) { - setTimeout(() => { - this.handleReconnection(attempt + 1); - }, (1000 * (attempt + 1)) ^ 4); - } else { - this.debug("Reconnect failed"); - } - }); + this.connect().catch((err) => { + // this.debug("Reconnect failed", err); + + if (attempt < MAX_RECONNECT_ATTEMPTS) { + setTimeout(() => { + this.handleReconnection(attempt + 1); + }, (1000 * (attempt + 1)) ^ 4); + } else { + this.debug("Reconnect failed"); + } + }); }, reconnectDelay); this.ndkRelay.emit("delayed-connect", reconnectDelay); @@ -228,6 +461,103 @@ export class NDKRelayConnectivity { this._connectionStats.nextReconnectAt = Date.now() + reconnectDelay; } + /** + * Sends a message to the NDK relay if the connection is in the CONNECTED state and the WebSocket is open. + * If the connection is not in the CONNECTED state or the WebSocket is not open, logs a debug message and throws an error. + * + * @param message - The message to send to the NDK relay. + * @throws {Error} If attempting to send on a closed relay connection. + */ + public async send(message: string) { + if (this._status >= NDKRelayStatus.CONNECTED && this.ws?.readyState === WebSocket.OPEN) { + this.ws?.send(message); + } else { + this.debug( + `Not connected to ${this.ndkRelay.url} (%d), not sending message ${message}`, + this._status + ); + } + } + + /** + * Authenticates the NDK event by sending it to the NDK relay and returning a promise that resolves with the result. + * + * @param event - The NDK event to authenticate. + * @returns A promise that resolves with the authentication result. + */ + private async auth(event: NDKEvent): Promise { + const ret = new Promise((resolve, reject) => { + const val = this.openEventPublishes.get(event.id) ?? []; + val.push({ resolve, reject }); + this.openEventPublishes.set(event.id, val); + }); + this.send('["AUTH",' + JSON.stringify(event.rawEvent()) + "]"); + return ret; + } + + /** + * Publishes an NDK event to the relay and returns a promise that resolves with the result. + * + * @param event - The NDK event to publish. + * @returns A promise that resolves with the result of the event publication. + * @throws {Error} If attempting to publish on a closed relay connection. + */ + public async publish(event: NostrEvent): Promise { + const ret = new Promise((resolve, reject) => { + const val = this.openEventPublishes.get(event.id!) ?? []; + if (val.length > 0) { + console.warn("Duplicate event publishing detected, you are publishing event "+event.id!+" twice"); + } + + val.push({ resolve, reject }); + this.openEventPublishes.set(event.id!, val); + }); + this.send('["EVENT",' + JSON.stringify(event) + "]"); + return ret; + } + + /** + * Counts the number of events that match the provided filters. + * + * @param filters - The filters to apply to the count request. + * @param params - An optional object containing a custom id for the count request. + * @returns A promise that resolves with the number of matching events. + * @throws {Error} If attempting to send the count request on a closed relay connection. + */ + public async count(filters: NDKFilter[], params: { id?: string | null }): Promise { + this.serial++; + const id = params?.id || "count:" + this.serial; + const ret = new Promise((resolve, reject) => { + this.openCountRequests.set(id, { resolve, reject }); + }); + this.send('["COUNT","' + id + '",' + JSON.stringify(filters).substring(1)); + return ret; + } + + public close(subId: string, reason?: string): void { + this.send('["CLOSE","' + subId + '"]'); + const sub = this.openSubs.get(subId); + this.openSubs.delete(subId); + if (sub) sub.onclose(reason); + } + + /** + * Subscribes to the NDK relay with the provided filters and parameters. + * + * @param filters - The filters to apply to the subscription. + * @param params - The subscription parameters, including an optional custom id. + * @returns A new NDKRelaySubscription instance. + */ + public req(relaySub: NDKRelaySubscription): void { + this.send( + '["REQ","' + + relaySub.subId + + '",' + + JSON.stringify(relaySub.executeFilters).substring(1) + ) + "]"; + this.openSubs.set(relaySub.subId, relaySub); + } + /** * Utility functions to update the connection stats. */ @@ -252,13 +582,21 @@ export class NDKRelayConnectivity { attempt: () => { this._connectionStats.attempts++; + this._connectionStats.connectedAt = Date.now(); }, }; - /** - * Returns the connection stats. - */ + /** Returns the connection stats. */ get connectionStats(): NDKRelayConnectionStats { return this._connectionStats; } + + /** Returns the relay URL */ + get url(): WebSocket["url"] { + return this.ndkRelay.url; + } + + get connected(): boolean { + return this._status >= NDKRelayStatus.CONNECTED && this.ws?.readyState === WebSocket.OPEN; + } } diff --git a/ndk/src/relay/index.ts b/ndk/src/relay/index.ts index 3f0e5c8a..1208978f 100644 --- a/ndk/src/relay/index.ts +++ b/ndk/src/relay/index.ts @@ -7,23 +7,27 @@ import type { NDKUser } from "../user/index.js"; import { NDKRelayConnectivity } from "./connectivity.js"; import { NDKRelayPublisher } from "./publisher.js"; import type { NDKRelayScore } from "./score.js"; -import { NDKRelaySubscriptions } from "./subscriptions.js"; -import { NDKAuthPolicy } from "./auth-policies.js"; +import { NDKRelaySubscriptionManager } from "./sub-manager.js"; +import type { NDKAuthPolicy } from "./auth-policies.js"; import { normalizeRelayUrl } from "../utils/normalize-url.js"; -import { NDK } from "../ndk/index.js"; +import type { NDK } from "../ndk/index.js"; +import type { NDKRelaySubscription } from "./subscription.js"; /** @deprecated Use `WebSocket['url']` instead. */ export type NDKRelayUrl = WebSocket["url"]; export enum NDKRelayStatus { - CONNECTING, - CONNECTED, - DISCONNECTING, - DISCONNECTED, - RECONNECTING, - FLAPPING, - AUTH_REQUIRED, - AUTHENTICATING, + DISCONNECTING, // 0 + DISCONNECTED, // 1 + RECONNECTING, // 2 + FLAPPING, // 3 + CONNECTING, // 4 + + // connected states + CONNECTED, // 5 + AUTH_REQUESTED, // 6 + AUTHENTICATING, // 7 + AUTHENTICATED, // 8 } export interface NDKRelayConnectionStats { @@ -86,6 +90,7 @@ export class NDKRelay extends EventEmitter<{ notice: (notice: string) => void; auth: (challenge: string) => void; authed: () => void; + "auth:failed": (error: Error) => void; published: (event: NDKEvent) => void; "publish:failed": (event: NDKEvent, error: Error) => void; "delayed-connect": (delayInMs: number) => void; @@ -93,12 +98,37 @@ export class NDKRelay extends EventEmitter<{ readonly url: WebSocket["url"]; readonly scores: Map; public connectivity: NDKRelayConnectivity; - private subs: NDKRelaySubscriptions; + private subs: NDKRelaySubscriptionManager; private publisher: NDKRelayPublisher; public authPolicy?: NDKAuthPolicy; - public validationRatio?: number; - private validatedEventCount: number = 0; - private skippedEventCount: number = 0; + + /** + * The lowest validation ratio this relay can reach. + */ + public lowestValidationRatio?: number; + + /** + * Current validation ratio this relay is targeting. + */ + public targetValidationRatio?: number; + + public validationRatioFn?: ( + relay: NDKRelay, + validatedCount: number, + nonValidatedCount: number + ) => number; + + /** + * This tracks events that have been seen by this relay + * with a valid signature. + */ + private validatedEventCount = 0; + + /** + * This tracks events that have been seen by this relay + * but have not been validated. + */ + private nonValidatedEventCount = 0; /** * Whether this relay is trusted. @@ -110,16 +140,56 @@ export class NDKRelay extends EventEmitter<{ public complaining = false; readonly debug: debug.Debugger; + static defaultValidationRatioUpdateFn = ( + relay: NDKRelay, + validatedCount: number, + nonValidatedCount: number + ): number => { + if (relay.lowestValidationRatio === undefined || relay.targetValidationRatio === undefined) + return 1; + + let newRatio = relay.validationRatio; + + if (relay.validationRatio > relay.targetValidationRatio) { + const factor = validatedCount / 100; + newRatio = Math.max(relay.lowestValidationRatio, relay.validationRatio - factor); + } + + if (newRatio < relay.validationRatio) { + return newRatio; + } + + return relay.validationRatio; + }; + public constructor(url: WebSocket["url"], authPolicy?: NDKAuthPolicy, ndk?: NDK) { super(); this.url = normalizeRelayUrl(url); this.scores = new Map(); this.debug = debug(`ndk:relay:${url}`); this.connectivity = new NDKRelayConnectivity(this, ndk); - this.subs = new NDKRelaySubscriptions(this); + this.req = this.connectivity.req.bind(this.connectivity); + this.close = this.connectivity.close.bind(this.connectivity); + this.subs = new NDKRelaySubscriptionManager(this, ndk?.subManager); this.publisher = new NDKRelayPublisher(this); this.authPolicy = authPolicy; - this.validationRatio = undefined; + this.targetValidationRatio = ndk?.initialValidationRatio; + this.lowestValidationRatio = ndk?.lowestValidationRatio; + this.validationRatioFn = ( + ndk?.validationRatioFn ?? NDKRelay.defaultValidationRatioUpdateFn + ).bind(this); + + this.updateValidationRatio(); + + if (!ndk) { + console.trace("relay created without ndk"); + } + } + + private updateValidationRatio(): void { + setTimeout(() => { + this.updateValidationRatio(); + }, 30000); } get status(): NDKRelayStatus { @@ -156,7 +226,7 @@ export class NDKRelay extends EventEmitter<{ * @param filters Filters to execute */ public subscribe(subscription: NDKSubscription, filters: NDKFilter[]): void { - this.subs.subscribe(subscription, filters); + this.subs.addSubscription(subscription, filters); } /** @@ -173,49 +243,31 @@ export class NDKRelay extends EventEmitter<{ return this.publisher.publish(event, timeoutMs); } - /** - * Called when this relay has responded with an event but - * wasn't the fastest one. - * @param timeDiffInMs The time difference in ms between the fastest and this relay in milliseconds - */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - public scoreSlowerEvent(timeDiffInMs: number): void { - // TODO - } - - /** @deprecated Use referenceTags instead. */ - public tagReference(marker?: string): NDKTag { - const tag = ["r", this.url]; - - if (marker) { - tag.push(marker); - } - - return tag; - } - public referenceTags(): NDKTag[] { return [["r", this.url]]; } - public activeSubscriptions(): Map { - return this.subs.executedFilters(); - } + // public activeSubscriptions(): Map { + // // return this.subs.executedFilters(); + // } public addValidatedEvent(): void { this.validatedEventCount++; } - public addSkippedEvent(): void { - this.skippedEventCount++; + public addNonValidatedEvent(): void { + this.nonValidatedEventCount++; } - public getValidationRatio(): number { - if (this.skippedEventCount === 0) { + /** + * The current validation ratio this relay has achieved. + */ + get validationRatio(): number { + if (this.nonValidatedEventCount === 0) { return 1; } - return this.validatedEventCount / (this.validatedEventCount + this.skippedEventCount); + return this.validatedEventCount / (this.validatedEventCount + this.nonValidatedEventCount); } public shouldValidateEvent(): boolean { @@ -223,11 +275,18 @@ export class NDKRelay extends EventEmitter<{ return false; } - if (this.validationRatio === undefined) { + if (this.targetValidationRatio === undefined) { return true; } // if the current validation ratio is below the threshold, validate the event - return this.getValidationRatio() < this.validationRatio; + return this.validationRatio < this.targetValidationRatio; } + + get connected(): boolean { + return this.connectivity.connected; + } + + public req: (relaySub: NDKRelaySubscription) => void; + public close: (subId: string) => void; } diff --git a/ndk/src/relay/pool/index.ts b/ndk/src/relay/pool/index.ts index 92ef3a01..cfe73462 100644 --- a/ndk/src/relay/pool/index.ts +++ b/ndk/src/relay/pool/index.ts @@ -3,7 +3,7 @@ import { EventEmitter } from "tseep"; import type { NDK } from "../../ndk/index.js"; import { NDKRelay, NDKRelayStatus } from "../index.js"; -import { NDKFilter } from "../../subscription/index.js"; +import type { NDKFilter } from "../../subscription/index.js"; import { normalizeRelayUrl } from "../../utils/normalize-url.js"; export type NDKPoolStats = { @@ -43,6 +43,7 @@ export class NDKPool extends EventEmitter<{ }> { // TODO: This should probably be an LRU cache public relays = new Map(); + public autoConnectRelays = new Set(); public blacklistRelayUrls: Set; private debug: debug.Debugger; private temporaryRelayTimers = new Map(); @@ -78,13 +79,17 @@ export class NDKPool extends EventEmitter<{ * @param relay - The relay to add to the pool. * @param removeIfUnusedAfter - The time in milliseconds to wait before removing the relay from the pool after it is no longer used. */ - public useTemporaryRelay(relay: NDKRelay, removeIfUnusedAfter = 30000, filters?: NDKFilter[]) { + public useTemporaryRelay( + relay: NDKRelay, + removeIfUnusedAfter = 30000, + filters?: NDKFilter[] | string + ) { const relayAlreadyInPool = this.relays.has(relay.url); // check if the relay is already in the pool if (!relayAlreadyInPool) { - // console.trace("adding relay to pool", relay.url, filters); this.addRelay(relay); + this.debug("Adding temporary relay %s for filters %o", relay.url, filters); } // check if the relay already has a disconnecting timer @@ -187,6 +192,7 @@ export class NDKPool extends EventEmitter<{ } }); this.relays.set(relayUrl, relay); + if (connect) this.autoConnectRelays.add(relayUrl); if (connect) { this.emit("relay:connecting", relay); @@ -206,6 +212,7 @@ export class NDKPool extends EventEmitter<{ if (relay) { relay.disconnect(); this.relays.delete(relayUrl); + this.autoConnectRelays.delete(relayUrl); this.emit("relay:disconnect", relay); return true; } @@ -242,7 +249,7 @@ export class NDKPool extends EventEmitter<{ temporary = false, filters?: NDKFilter[] ): NDKRelay { - let relay = this.relays.get(url); + let relay = this.relays.get(normalizeRelayUrl(url)); if (!relay) { relay = new NDKRelay(url, undefined, this.ndk); @@ -285,7 +292,16 @@ export class NDKPool extends EventEmitter<{ }` ); - for (const relay of this.relays.values()) { + 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; + const connectPromise = new Promise((resolve, reject) => { this.emit("relay:connecting", relay); return relay.connect(timeoutMs).then(resolve).catch(reject); @@ -397,7 +413,7 @@ export class NDKPool extends EventEmitter<{ public permanentAndConnectedRelays(): NDKRelay[] { return Array.from(this.relays.values()).filter( (relay) => - relay.status === NDKRelayStatus.CONNECTED || + relay.status >= NDKRelayStatus.CONNECTED && !this.temporaryRelayTimers.has(relay.url) ); } diff --git a/ndk/src/relay/publisher.ts b/ndk/src/relay/publisher.ts index 3fe2597b..0dd6c190 100644 --- a/ndk/src/relay/publisher.ts +++ b/ndk/src/relay/publisher.ts @@ -4,9 +4,11 @@ import type { NDKEvent } from "../events"; export class NDKRelayPublisher { private ndkRelay: NDKRelay; + private debug: debug.Debugger; public constructor(ndkRelay: NDKRelay) { this.ndkRelay = ndkRelay; + this.debug = ndkRelay.debug.extend("publisher"); } /** @@ -20,12 +22,18 @@ export class NDKRelayPublisher { * @returns A promise that resolves when the event has been published or rejects if the operation times out */ public async publish(event: NDKEvent, timeoutMs = 2500): Promise { - const publishWhenConnected = () => { + let timeout: NodeJS.Timeout | number | undefined; + + const publishConnected = () => { return new Promise((resolve, reject) => { try { - this.publishEvent(event, timeoutMs) - .then((result) => resolve(result)) - .catch((err) => reject(err)); + this.publishEvent(event) + .then((result) => { + this.ndkRelay.emit("published", event); + event.emit("relay:published", this.ndkRelay); + resolve(true); + }) + .catch(reject); } catch (err) { reject(err); } @@ -33,21 +41,45 @@ export class NDKRelayPublisher { }; const timeoutPromise = new Promise((_, reject) => { - setTimeout(() => reject(new Error("Timeout")), timeoutMs); + timeout = setTimeout(() => { + timeout = undefined; + reject(new Error("Timeout: " + timeoutMs + "ms")); + }, timeoutMs); }); const onConnectHandler = () => { - publishWhenConnected() + publishConnected() .then((result) => connectResolve(result)) .catch((err) => connectReject(err)); }; let connectResolve: (value: boolean | PromiseLike) => void; + // eslint-disable-next-line @typescript-eslint/no-explicit-any let connectReject: (reason?: any) => void; - if (this.ndkRelay.status === NDKRelayStatus.CONNECTED) { - return Promise.race([publishWhenConnected(), timeoutPromise]); + const onError = (err: Error) => { + this.ndkRelay.debug("Publish failed", err, event.id); + this.ndkRelay.emit("publish:failed", event, err); + event.emit("relay:publish:failed", this.ndkRelay, err); + throw err; + }; + + const onFinally = () => { + if (timeout) clearTimeout(timeout as NodeJS.Timeout); + this.ndkRelay.removeListener("connect", onConnectHandler); + }; + + if (this.ndkRelay.status >= NDKRelayStatus.CONNECTED) { + /** + * If we're already connected, publish the event right now + */ + return Promise.race([publishConnected(), timeoutPromise]) + .catch(onError) + .finally(onFinally); } else { + /** + * If we are not connected, try to connect and, once connected, publish the event + */ return Promise.race([ new Promise((resolve, reject) => { connectResolve = resolve; @@ -55,51 +87,13 @@ export class NDKRelayPublisher { this.ndkRelay.once("connect", onConnectHandler); }), timeoutPromise, - ]).finally(() => { - // Remove the event listener to avoid memory leaks - this.ndkRelay.removeListener("connect", onConnectHandler); - }); + ]) + .catch(onError) + .finally(onFinally); } } - private async publishEvent(event: NDKEvent, timeoutMs?: number): Promise { - const nostrEvent = event.rawEvent(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const publish = this.ndkRelay.connectivity.relay.publish(nostrEvent as any); - let publishTimeout: NodeJS.Timeout | number; - - const publishPromise = new Promise((resolve, reject) => { - publish - .then(() => { - clearTimeout(publishTimeout as unknown as NodeJS.Timeout); - this.ndkRelay.emit("published", event); - event.emit("published", this.ndkRelay); - resolve(true); - }) - .catch((err) => { - clearTimeout(publishTimeout as NodeJS.Timeout); - this.ndkRelay.debug("Publish failed", err, event.id); - this.ndkRelay.emit("publish:failed", event, err); - reject(err); - }); - }); - - // If no timeout is specified, just return the publish promise - // or if this is an ephemeral event, don't wait for the publish to complete - if (!timeoutMs || event.isEphemeral()) { - return publishPromise; - } - - // Create a promise that rejects after timeoutMs milliseconds - const timeoutPromise = new Promise((_, reject) => { - publishTimeout = setTimeout(() => { - this.ndkRelay.debug("Publish timed out", event.rawEvent()); - this.ndkRelay.emit("publish:failed", event, new Error("Timeout")); - reject(new Error("Publish operation timed out")); - }, timeoutMs); - }); - - // wait for either the publish operation to complete or the timeout to occur - return Promise.race([publishPromise, timeoutPromise]); + private async publishEvent(event: NDKEvent): Promise { + return this.ndkRelay.connectivity.publish(event.rawEvent()); } } diff --git a/ndk/src/relay/sets/calculate.test.ts b/ndk/src/relay/sets/calculate.test.ts index 3fc50250..95fa8437 100644 --- a/ndk/src/relay/sets/calculate.test.ts +++ b/ndk/src/relay/sets/calculate.test.ts @@ -1,8 +1,9 @@ -import { NDKEvent, NostrEvent } from "../../events/index.js"; +import type { NostrEvent } from "../../events/index.js"; +import { NDKEvent } from "../../events/index.js"; import { NDKRelayList } from "../../events/kinds/NDKRelayList.js"; import { NDK } from "../../ndk/index.js"; import { NDKPrivateKeySigner } from "../../signers/private-key/index.js"; -import { Hexpubkey, NDKUser } from "../../user/index.js"; +import type { Hexpubkey, NDKUser } from "../../user/index.js"; import { calculateRelaySetFromEvent, calculateRelaySetsFromFilters } from "./calculate.js"; const explicitRelayUrl = "wss://explicit-relay.com/"; @@ -16,16 +17,16 @@ const signers = [ ]; let ndk: NDK; -let users: NDKUser[] = []; -let readRelays: string[][] = []; -let writeRelays: string[][] = []; +const users: NDKUser[] = []; +const readRelays: string[][] = []; +const writeRelays: string[][] = []; beforeEach(() => { ndk = new NDK({ explicitRelayUrls: [explicitRelayUrl], - enableOutboxModel: true + enableOutboxModel: true, }); -}) +}); beforeAll(async () => { signers.forEach(async (signer, i) => { @@ -34,7 +35,7 @@ beforeAll(async () => { readRelays[i] = [ // relays that will have users in common `wss://relay${i}/`, - `wss://relay${i+2}/`, + `wss://relay${i + 2}/`, // a relay only this user will have `wss://user${i}-relay/`, @@ -42,7 +43,7 @@ beforeAll(async () => { writeRelays[i] = [ // relays that will have users in common `wss://relay${i}/`, - `wss://relay${i+1}/`, + `wss://relay${i + 1}/`, // a relay only this user will have `wss://user${i}-relay/`, @@ -76,17 +77,17 @@ function combineRelays(relays: string[][]) { describe("calculateRelaySetFromEvent", () => { it("prefers to use the author's write relays", async () => { - const event = new NDKEvent(ndk, { kind: 1} as NostrEvent) + const event = new NDKEvent(ndk, { kind: 1 } as NostrEvent); await event.sign(signers[0]); const set = await calculateRelaySetFromEvent(ndk, event); const expectedRelays = combineRelays([writeRelays[0], [explicitRelayUrl]]); - expect (set.relayUrls).toEqual(expectedRelays); - }) + expect(set.relayUrls).toEqual(expectedRelays); + }); - it("writes to the p-tagged pubkey write relays", async() => { - const event = new NDKEvent(ndk, { kind: 1} as NostrEvent) + it("writes to the p-tagged pubkey write relays", async () => { + const event = new NDKEvent(ndk, { kind: 1 } as NostrEvent); const taggedUserIndexes = [1, 2, 4]; for (const i of taggedUserIndexes) { @@ -115,11 +116,13 @@ describe("calculateRelaySetFromEvent", () => { for (const relay of writeRelays[0]) { expect(resultedRelays).toContain(relay); } - }) + }); - it("if some tagged pubkey doesn't have write relays, writes to the explicit relay list", async() => { - const userWithoutRelays = ndk.getUser({ pubkey: "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"}); - const event = new NDKEvent(ndk, { kind: 1} as NostrEvent); + it("if some tagged pubkey doesn't have write relays, writes to the explicit relay list", async () => { + const userWithoutRelays = ndk.getUser({ + pubkey: "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52", + }); + const event = new NDKEvent(ndk, { kind: 1 } as NostrEvent); event.tag(userWithoutRelays); await event.sign(signers[0]); @@ -130,19 +133,19 @@ describe("calculateRelaySetFromEvent", () => { expectedRelays.push(explicitRelayUrl); expect(resultedRelays).toEqual(expectedRelays); - }) + }); - it("writes to any relay that has been hinted at too", async() => { - const event = new NDKEvent(ndk, { kind: 1} as NostrEvent); - event.tags.push(["e", "123", "wss://hinted-relay.com/"]) + it("writes to any relay that has been hinted at too", async () => { + const event = new NDKEvent(ndk, { kind: 1 } as NostrEvent); + event.tags.push(["e", "123", "wss://hinted-relay.com/"]); await event.sign(signers[0]); const result = await calculateRelaySetFromEvent(ndk, event); const resultedRelays = result.relayUrls; expect(resultedRelays).toContain("wss://hinted-relay.com/"); - }) -}) + }); +}); describe("calculateRelaySetsFromFilters", () => { it("falls back to the explicit relay when authors don't have relays", () => { diff --git a/ndk/src/relay/sets/calculate.ts b/ndk/src/relay/sets/calculate.ts index 49ed40e8..3d6ea5c4 100644 --- a/ndk/src/relay/sets/calculate.ts +++ b/ndk/src/relay/sets/calculate.ts @@ -7,7 +7,7 @@ import type { NDKFilter } from "../../subscription/index.js"; import type { Hexpubkey } from "../../user/index.js"; import { normalizeRelayUrl } from "../../utils/normalize-url.js"; import type { NDKRelay } from "../index.js"; -import { NDKPool } from "../pool/index.js"; +import type { NDKPool } from "../pool/index.js"; import { NDKRelaySet } from "./index.js"; import createDebug from "debug"; @@ -29,20 +29,24 @@ export async function calculateRelaySetFromEvent(ndk: NDK, event: NDKEvent): Pro if (authorWriteRelays) { authorWriteRelays.forEach((relayUrl) => { const relay = ndk.pool?.getRelay(relayUrl); - if (relay) { - d("Adding author write relay %s", relayUrl); - relays.add(relay); - } + if (relay) relays.add(relay); }); } // get all the hinted relays let relayHints = event.tags - .filter(tag => ["a", "e"].includes(tag[0])) - .map(tag => tag[2]) + .filter((tag) => ["a", "e"].includes(tag[0])) + .map((tag) => tag[2]) // verify it's a valid URL .filter((url: string | undefined) => url && url.startsWith("wss://")) - .filter((url: string) => { try { new URL(url); return true; } catch { return false; } }) + .filter((url: string) => { + try { + new URL(url); + return true; + } catch { + return false; + } + }) .map((url: string) => normalizeRelayUrl(url)); // make unique @@ -58,9 +62,11 @@ export async function calculateRelaySetFromEvent(ndk: NDK, event: NDKEvent): Pro const pTags = event.getMatchingTags("p").map((tag) => tag[1]); if (pTags.length < 5) { - const pTaggedRelays = Array.from(chooseRelayCombinationForPubkeys(ndk, pTags, "read", { - preferredRelays: new Set(authorWriteRelays), - }).keys()) + const pTaggedRelays = Array.from( + chooseRelayCombinationForPubkeys(ndk, pTags, "read", { + preferredRelays: new Set(authorWriteRelays), + }).keys() + ); pTaggedRelays.forEach((relayUrl) => { const relay = ndk.pool?.getRelay(relayUrl, false, true); if (relay) { @@ -138,9 +144,24 @@ export function calculateRelaySetsFromFilter( } } else { // If we don't, add the explicit relays - pool.permanentAndConnectedRelays().forEach((relay: NDKRelay) => { - result.set(relay.url, filters); - }); + if (ndk.explicitRelayUrls) { + ndk.explicitRelayUrls.forEach((relayUrl) => { + result.set(relayUrl, filters); + }); + } + } + + if (result.size === 0) { + // If we don't have any relays, add all the permanent relays + pool.permanentAndConnectedRelays() + .slice(0, 5) + .forEach((relay) => { + result.set(relay.url, filters); + }); + } + + if (result.size === 0) { + console.warn("No relays found for filter", filters); } return result; @@ -156,5 +177,7 @@ export function calculateRelaySetsFromFilters( filters: NDKFilter[], pool: NDKPool ): Map { - return calculateRelaySetsFromFilter(ndk, filters, pool); + const a = calculateRelaySetsFromFilter(ndk, filters, pool); + + return a; } diff --git a/ndk/src/relay/sets/index.ts b/ndk/src/relay/sets/index.ts index c8615f32..a47b3350 100644 --- a/ndk/src/relay/sets/index.ts +++ b/ndk/src/relay/sets/index.ts @@ -1,7 +1,9 @@ import type { NDKEvent } from "../../events/index.js"; import type { NDK } from "../../ndk/index.js"; import { normalizeRelayUrl } from "../../utils/normalize-url.js"; -import { NDKRelay } from "../index.js"; +import { NDKRelay, NDKRelayStatus } from "../index.js"; + +export { calculateRelaySetFromEvent } from "./calculate.js"; export class NDKPublishError extends Error { public errors: Map; @@ -72,17 +74,30 @@ export class NDKRelaySet { * * @param relayUrls - list of relay URLs to include in this set * @param ndk + * @param connect - whether to connect to the relay immediately if it was already in the pool but not connected * @returns NDKRelaySet */ - static fromRelayUrls(relayUrls: string[], ndk: NDK): NDKRelaySet { + static fromRelayUrls(relayUrls: string[], ndk: NDK, connect = true): NDKRelaySet { const relays = new Set(); for (const url of relayUrls) { const relay = ndk.pool.relays.get(normalizeRelayUrl(url)); if (relay) { + if (relay.status < NDKRelayStatus.CONNECTED && connect) { + relay.connect(); + } + relays.add(relay); } else { - const temporaryRelay = new NDKRelay(normalizeRelayUrl(url), undefined, ndk); - ndk.pool.useTemporaryRelay(temporaryRelay); + const temporaryRelay = new NDKRelay( + normalizeRelayUrl(url), + ndk?.relayAuthDefaultPolicy, + ndk + ); + ndk.pool.useTemporaryRelay( + temporaryRelay, + undefined, + "requested from fromRelayUrls " + relayUrls + ); relays.add(temporaryRelay); } } @@ -159,10 +174,7 @@ export class NDKRelaySet { this.ndk.emit("event:publish-failed", event, error, this.relayUrls); - // Only throw if we don't have an active handler for failed publish event - if (this.ndk.listeners("event:publish-failed").length === 0) { - throw error; - } + throw error; } } else { event.emit("published", { relaySet: this, publishedToRelays }); diff --git a/ndk/src/relay/sub-manager.ts b/ndk/src/relay/sub-manager.ts new file mode 100644 index 00000000..35b65c55 --- /dev/null +++ b/ndk/src/relay/sub-manager.ts @@ -0,0 +1,67 @@ +import { NDKRelaySubscription } from "./subscription"; +import type { NDKSubscription } from "../subscription/index.js"; +import type { NDKFilter } from "../subscription/index.js"; +import type { NDKFilterFingerprint } from "../subscription/grouping.js"; +import { filterFingerprint } from "../subscription/grouping.js"; +import type { NDKRelay } from "."; +import type { NDKSubscriptionManager } from "../subscription/manager"; + +/** + * The subscription manager of an NDKRelay is in charge of orchestrating the subscriptions + * that are created and closed in a given relay. + * + * The manager is responsible for: + * * restarting subscriptions when they are unexpectedly closed + * * scheduling subscriptions that are received before the relay is connected + * * grouping similar subscriptions to be compiled into individual REQs + */ +export class NDKRelaySubscriptionManager { + private relay: NDKRelay; + private subscriptions: Map; + private topSubscriptionManager?: NDKSubscriptionManager; + private debug: debug.Debugger; + + constructor(relay: NDKRelay, topSubscriptionManager?: NDKSubscriptionManager) { + this.relay = relay; + this.subscriptions = new Map(); + this.debug = relay.debug.extend("sub-manager"); + this.topSubscriptionManager = topSubscriptionManager; + } + + /** + * Adds a subscription to the manager. + */ + public addSubscription(sub: NDKSubscription, filters: NDKFilter[]) { + let relaySub: NDKRelaySubscription | undefined; + + if (!sub.isGroupable()) { + // if the subscription is not groupable, just execute it + relaySub = this.createSubscription(sub, filters); + } else { + const filterFp = filterFingerprint(filters, sub.closeOnEose); + if (filterFp) relaySub = this.subscriptions.get(filterFp); + relaySub ??= this.createSubscription(sub, filters, filterFp); + } + + // at this point, relaySub is guaranteed to be defined + relaySub.addItem(sub, filters); + } + + public createSubscription( + sub: NDKSubscription, + filters: NDKFilter[], + fingerprint?: NDKFilterFingerprint + ): NDKRelaySubscription { + const relaySub = new NDKRelaySubscription(this.relay, fingerprint); + relaySub.topSubscriptionManager = this.topSubscriptionManager; + relaySub.onClose = this.onRelaySubscriptionClose.bind(this); + + this.subscriptions.set(relaySub.fingerprint, relaySub); + + return relaySub; + } + + private onRelaySubscriptionClose(sub: NDKRelaySubscription) { + this.subscriptions.delete(sub.fingerprint); + } +} diff --git a/ndk/src/relay/subscription.test.ts b/ndk/src/relay/subscription.test.ts new file mode 100644 index 00000000..14d80230 --- /dev/null +++ b/ndk/src/relay/subscription.test.ts @@ -0,0 +1,165 @@ +// NDKRelaySubscription.test.ts + +import { NDKRelaySubscription, NDKRelaySubscriptionStatus } from "./subscription.js"; +import type { NDKFilter, NDKSubscriptionInternalId } from "../subscription/index.js"; +import { NDKSubscription } from "../subscription/index.js"; +import debug from "debug"; +import { NDK } from "../ndk/index.js"; +import { NDKRelay } from "../index.js"; + +const ndk = new NDK(); +const relay = new NDKRelay("wss://fake-relay.com"); +const filters: NDKFilter[] = [{ kinds: [1] }]; + +// mock +relay.req = jest.fn(); + +// Mock classes for NDKSubscription and NDKFilter +class MockNDKSubscription extends NDKSubscription { + internalId: NDKSubscriptionInternalId; + private _groupableDelay: number; + private _groupableDelayType: "at-most" | "at-least"; + public groupable = true; + + constructor( + internalId: NDKSubscriptionInternalId, + delay: number, + delayType: "at-most" | "at-least" + ) { + super(ndk, filters); + this.internalId = internalId; + this._groupableDelay = delay; + this._groupableDelayType = delayType; + } + + get groupableDelay() { + return this._groupableDelay; + } + + get groupableDelayType() { + return this._groupableDelayType; + } + + public isGroupable(): boolean { + return this.groupable; + } +} + +describe("NDKRelaySubscription", () => { + let ndkRelaySubscription: NDKRelaySubscription; + + beforeEach(() => { + ndkRelaySubscription = new NDKRelaySubscription(relay); + ndkRelaySubscription.debug = debug("test"); + }); + + it("should initialize with status INITIAL", () => { + expect(ndkRelaySubscription["status"]).toBe(NDKRelaySubscriptionStatus.INITIAL); + }); + + it("should add item and schedule execution", () => { + const subscription = new MockNDKSubscription("sub1", 1000, "at-least"); + + ndkRelaySubscription.addItem(subscription, filters); + expect(ndkRelaySubscription.items.size).toBe(1); + expect(ndkRelaySubscription.items.get("sub1")).toEqual({ subscription, filters }); + expect(ndkRelaySubscription["status"]).toBe(NDKRelaySubscriptionStatus.PENDING); + }); + + it("should execute immediately if subscription is not groupable", () => { + const subscription = new MockNDKSubscription("sub2", 1000, "at-least"); + jest.spyOn(subscription, "isGroupable").mockReturnValue(false); + + const executeSpy = jest.spyOn(ndkRelaySubscription as any, "execute"); + ndkRelaySubscription.addItem(subscription, filters); + expect(executeSpy).toHaveBeenCalled(); + }); + + it("should not add items to a closed subscription", () => { + const subscription = new MockNDKSubscription("sub4", 1000, "at-least"); + ndkRelaySubscription["status"] = NDKRelaySubscriptionStatus.CLOSED; + + expect(() => { + ndkRelaySubscription.addItem(subscription, []); + }).toThrow("Cannot add new items to a closed subscription"); + }); + + it("should schedule execution correctly", () => { + const subscription = new MockNDKSubscription("sub5", 1000, "at-least"); + + ndkRelaySubscription.addItem(subscription, filters); + expect(ndkRelaySubscription["fireTime"]).toBeGreaterThan(Date.now()); + }); + + it("should execute subscription", () => { + const executeSpy = jest.spyOn(ndkRelaySubscription as any, "execute"); + const subscription = new MockNDKSubscription("sub6", 1000, "at-least"); + + ndkRelaySubscription.addItem(subscription, filters); + jest.advanceTimersByTime(1000); + expect(executeSpy).toHaveBeenCalled(); + }); + + it("should reschedule execution when a new subscription with a longer delay is added", () => { + const subscription1 = new MockNDKSubscription("sub7", 5000, "at-least"); + const subscription2 = new MockNDKSubscription("sub8", 10000, "at-least"); + + ndkRelaySubscription.addItem(subscription1, filters); + const initialTimer = ndkRelaySubscription["executionTimer"]; + + ndkRelaySubscription.addItem(subscription2, filters); + const rescheduledTimer = ndkRelaySubscription["executionTimer"]; + + expect(ndkRelaySubscription["fireTime"]).toBeGreaterThan(Date.now() + 5000); + expect(rescheduledTimer).not.toBe(initialTimer); + }); + + it('should reset timer to shorter "at-most" delay when added after an "at-least" delay', () => { + const subscription1 = new MockNDKSubscription("sub9", 5000, "at-least"); + const subscription2 = new MockNDKSubscription("sub10", 3000, "at-most"); + + ndkRelaySubscription.addItem(subscription1, filters); + ndkRelaySubscription.addItem(subscription2, filters); + + // Since the second subscription is "at-most", the timer should be reset to 3000ms + expect(ndkRelaySubscription["fireTime"]).toBeLessThanOrEqual(Date.now() + 3000); + }); + + it('should maintain timer for shorter "at-most" delay when an "at-least" delay is added afterwards', () => { + const subscription1 = new MockNDKSubscription("sub11", 3000, "at-most"); + const subscription2 = new MockNDKSubscription("sub12", 5000, "at-least"); + + ndkRelaySubscription.addItem(subscription1, filters); + const initialTimer = ndkRelaySubscription["executionTimer"]; + + ndkRelaySubscription.addItem(subscription2, filters); + const rescheduledTimer = ndkRelaySubscription["executionTimer"]; + + // Since the first subscription is "at-most", it should not change when "at-least" is added + expect(ndkRelaySubscription["fireTime"]).toBeLessThanOrEqual(Date.now() + 3000); + expect(rescheduledTimer).toBe(initialTimer); + }); + + fit("should not close until we have reached EOSE", () => { + const sub = new MockNDKSubscription("sub11", 0, "at-most"); + sub.groupable = false; + ndkRelaySubscription.addItem(sub, filters); + const closeSpy = jest.spyOn(ndkRelaySubscription as any, "close"); + sub.stop(); + expect(closeSpy).not.toHaveBeenCalled(); + }); + + fit("it should close when we reach EOSE if the subscription said it was closed", () => { + const sub = new MockNDKSubscription("sub11", 0, "at-most"); + sub.groupable = false; + sub.closeOnEose = true; + ndkRelaySubscription.addItem(sub, filters); + const closeSpy = jest.spyOn(ndkRelaySubscription as any, "close"); + sub.stop(); + expect(closeSpy).not.toHaveBeenCalled(); + ndkRelaySubscription.oneose(); + expect(closeSpy).toHaveBeenCalled(); + }); +}); + +jest.useFakeTimers(); diff --git a/ndk/src/relay/subscription.ts b/ndk/src/relay/subscription.ts new file mode 100644 index 00000000..604d1ef9 --- /dev/null +++ b/ndk/src/relay/subscription.ts @@ -0,0 +1,392 @@ +import type { Event } from "nostr-tools"; +import { matchFilters } from "nostr-tools"; +import type { NDKRelay } from "."; +import { NDKRelayStatus } from "."; +import type { NDKEventId, NostrEvent } from "../events"; +import type { + NDKFilter, + NDKSubscription, + NDKSubscriptionDelayedType, + NDKSubscriptionInternalId, +} from "../subscription"; +import type { NDKFilterFingerprint } from "../subscription/grouping"; +import { mergeFilters } from "../subscription/grouping"; +import type { NDKSubscriptionManager } from "../subscription/manager"; + +type Item = { + subscription: NDKSubscription; + filters: NDKFilter[]; +}; + +export enum NDKRelaySubscriptionStatus { + INITIAL, + + /** + * The subscription is pending execution. + */ + PENDING, + + /** + * The subscription is waiting for the relay to be ready. + */ + WAITING, + + /** + * The subscription is currently running. + */ + RUNNING, + CLOSED, +} + +/** + * Groups together a number of NDKSubscriptions (as created by the user), + * filters (as computed internally), executed, or to be executed, within + * a single specific relay. + */ +export class NDKRelaySubscription { + public fingerprint: NDKFilterFingerprint; + public items: Map = new Map(); + public topSubscriptionManager?: NDKSubscriptionManager; + + public debug: debug.Debugger; + + /** + * Tracks the status of this REQ. + */ + private status: NDKRelaySubscriptionStatus = NDKRelaySubscriptionStatus.INITIAL; + + public onClose?: (sub: NDKRelaySubscription) => void; + + private relay: NDKRelay; + + /** + * Whether this subscription has reached EOSE. + */ + private eosed = false; + + /** + * These are subscriptions that have indicated they want to close before + * we received an EOSE. + * + * This happens when this relay is the slowest to respond once the NDKSubscription + * has received enough EOSEs to give up on this relay. + */ + private itemsToRemoveAfterEose: NDKSubscriptionInternalId[] = []; + + /** + * Timeout at which this subscription will + * start executing. + */ + private executionTimer?: NodeJS.Timeout | number; + + /** + * Track the time at which this subscription will fire. + */ + private fireTime?: number; + + /** + * The delay type that the current fireTime was calculated with. + */ + private delayType?: NDKSubscriptionDelayedType; + + /** + * The filters that have been executed. + */ + public executeFilters?: NDKFilter[]; + + /** + * Event IDs that have been seen by this subscription. + */ + private eventIds: Set = new Set(); + + /** + * + * @param fingerprint The fingerprint of this subscription. + */ + constructor(relay: NDKRelay, fingerprint?: NDKFilterFingerprint) { + this.relay = relay; + const rand = Math.random().toString(36).substring(7); + this.debug = relay.debug.extend("subscription-" + rand); + this.fingerprint = fingerprint || Math.random().toString(36).substring(7); + } + + private _subId?: string; + + get subId(): string { + if (this._subId) return this._subId; + + this._subId = this.fingerprint.slice(0, 15); + return this._subId; + } + + private subIdParts = new Set(); + private addSubIdPart(part: string) { + this.subIdParts.add(part); + } + + public addItem(subscription: NDKSubscription, filters: NDKFilter[]) { + if (this.items.has(subscription.internalId)) return; + + subscription.on("close", this.removeItem.bind(this, subscription)); + this.items.set(subscription.internalId, { subscription, filters }); + + if (this.status !== NDKRelaySubscriptionStatus.RUNNING) { + // if we have an explicit subId in this subscription, append it to the subId + if (subscription.subId && (!this._subId || this._subId.length < 48)) { + if ( + this.status === NDKRelaySubscriptionStatus.INITIAL || + this.status === NDKRelaySubscriptionStatus.PENDING + ) { + this.addSubIdPart(subscription.subId); + } + } + } + + switch (this.status) { + case NDKRelaySubscriptionStatus.INITIAL: + this.evaluateExecutionPlan(subscription); + break; + case NDKRelaySubscriptionStatus.RUNNING: + // the subscription was already running when this new NDKSubscription came + // so we might have some events this new NDKSubscription wants + // this.catchUpSubscription(subscription, filters); + break; + case NDKRelaySubscriptionStatus.PENDING: + // this subscription is already scheduled to be executed + // we need to evaluate whether this new NDKSubscription + // modifies our execution plan + this.evaluateExecutionPlan(subscription); + break; + case NDKRelaySubscriptionStatus.CLOSED: + this.debug("Subscription is closed, cannot add new items"); + throw new Error("Cannot add new items to a closed subscription"); + } + } + + /** + * A subscription has been closed, remove it from the list of items. + * @param subscription + */ + public removeItem(subscription: NDKSubscription) { + // If we have not EOSEd yet, don't delete the item, rather mark it for deletion + if (!this.eosed) { + this.itemsToRemoveAfterEose.push(subscription.internalId); + return; + } + + this.items.delete(subscription.internalId); + + if (this.items.size === 0) { + // no more items, close the subscription + this.close(); + } + } + + private close() { + if (this.status === NDKRelaySubscriptionStatus.CLOSED) return; + + const prevStatus = this.status; + this.status = NDKRelaySubscriptionStatus.CLOSED; + if (prevStatus === NDKRelaySubscriptionStatus.RUNNING) { + try { + this.relay.close(this.subId); + } catch (e) { + this.debug("Error closing subscription", e, this); + } + } else { + this.debug("Subscription wanted to close but it wasn't running, this is probably ok", { + subId: this.subId, + prevStatus, + sub: this, + }); + } + this.cleanup(); + } + + public cleanup() { + // remove delayed execution + if (this.executionTimer) clearTimeout(this.executionTimer as NodeJS.Timeout); + + // remove callback from relay + this.relay.off("ready", this.executeOnRelayReady); + this.relay.off("authed", this.reExecuteAfterAuth); + + // callback + if (this.onClose) this.onClose(this); + } + + private catchUpSubscription(subscription: NDKSubscription, filters: NDKFilter[]) { + this.debug("TODO: catch up subscription", subscription, filters); + } + + private evaluateExecutionPlan(subscription: NDKSubscription) { + if (!subscription.isGroupable()) { + // execute immediately + this.status = NDKRelaySubscriptionStatus.PENDING; + this.execute(); + return; + } + + const delay = subscription.groupableDelay; + const delayType = subscription.groupableDelayType; + + if (!delay) throw new Error("Cannot group a subscription without a delay"); + + if (this.status === NDKRelaySubscriptionStatus.INITIAL) { + this.schedule(delay, delayType); + } else { + // we already scheduled it, do we need to change it? + const existingDelayType = this.delayType; + const timeUntilFire = this.fireTime! - Date.now(); + + if (existingDelayType === "at-least" && delayType === "at-least") { + if (timeUntilFire < delay) { + // extend the timeout to the bigger timeout + if (this.executionTimer) clearTimeout(this.executionTimer as NodeJS.Timeout); + this.schedule(delay, delayType); + } + } else if (existingDelayType === "at-least" && delayType === "at-most") { + if (timeUntilFire > delay) { + if (this.executionTimer) clearTimeout(this.executionTimer as NodeJS.Timeout); + this.schedule(delay, delayType); + } + } else if (existingDelayType === "at-most" && delayType === "at-most") { + if (timeUntilFire > delay) { + if (this.executionTimer) clearTimeout(this.executionTimer as NodeJS.Timeout); + this.schedule(delay, delayType); + } + } else if (existingDelayType === "at-most" && delayType === "at-least") { + if (timeUntilFire > delay) { + if (this.executionTimer) clearTimeout(this.executionTimer as NodeJS.Timeout); + this.schedule(delay, delayType); + } + } else { + throw new Error( + "Unknown delay type combination " + existingDelayType + " " + delayType + ); + } + } + } + + private schedule(delay: number, delayType: NDKSubscriptionDelayedType) { + this.status = NDKRelaySubscriptionStatus.PENDING; + const currentTime = Date.now(); + this.fireTime = currentTime + delay; + this.delayType = delayType; + const timer = setTimeout(this.execute.bind(this), delay); + + /** + * We only store the execution timer if it's an "at-least" delay, + * since "at-most" delays should not be cancelled. + */ + if (delayType === "at-least") { + this.executionTimer = timer; + } + } + + private executeOnRelayReady = () => { + if (this.status !== NDKRelaySubscriptionStatus.WAITING) return; + + this.status = NDKRelaySubscriptionStatus.PENDING; + this.execute(); + }; + + private finalizeSubId() { + // if we have subId parts, join those + if (this.subIdParts.size > 0) { + this._subId = Array.from(this.subIdParts).join("-"); + } else { + this._subId = this.fingerprint.slice(0, 15); + } + + this._subId += "-" + Math.random().toString(36).substring(2, 7); + } + + // we do it this way so that we can remove the listener + private reExecuteAfterAuth = (() => { + const oldSubId = this.subId; + this.debug("Re-executing after auth", this.items.size); + this.relay.close(this.subId); + this._subId = undefined; + this.status = NDKRelaySubscriptionStatus.PENDING; + this.execute(); + this.debug("Re-executed after auth %s 👉 %s", oldSubId, this.subId); + }).bind(this); + + private execute() { + if (this.status !== NDKRelaySubscriptionStatus.PENDING) { + // Because we might schedule this execution multiple times, + // ensure we only execute once + return; + } + + // check on the relay connectivity status + if (!this.relay.connected) { + this.status = NDKRelaySubscriptionStatus.WAITING; + this.relay.once("ready", this.executeOnRelayReady); + return; + } else if (this.relay.status < NDKRelayStatus.AUTHENTICATED) { + this.relay.once("authed", this.reExecuteAfterAuth); + } + + this.status = NDKRelaySubscriptionStatus.RUNNING; + + this.finalizeSubId(); + + this.executeFilters = this.compileFilters(); + + this.relay.req(this); + } + + public onstart() {} + public onevent(event: NostrEvent) { + this.topSubscriptionManager?.seenEvent(event.id!, this.relay); + + for (const { subscription } of this.items.values()) { + if (matchFilters(subscription.filters, event as Event)) { + subscription.eventReceived(event, this.relay, false); + } + } + } + + public oneose() { + this.eosed = true; + + for (const { subscription } of this.items.values()) { + subscription.eoseReceived(this.relay); + + if (subscription.closeOnEose) { + this.removeItem(subscription); + } + } + } + + public onclose(reason?: string) { + this.status = NDKRelaySubscriptionStatus.CLOSED; + } + + public onclosed(reason?: string) { + if (!reason) return; + + for (const { subscription } of this.items.values()) { + subscription.closedReceived(this.relay, reason); + } + } + + /** + * Grabs the filters from all the subscriptions + * and merges them into a single filter. + */ + private compileFilters(): NDKFilter[] { + const mergedFilters: NDKFilter[] = []; + const filters = Array.from(this.items.values()).map((item) => item.filters); + const filterCount = filters[0].length; + + for (let i = 0; i < filterCount; i++) { + const allFiltersAtIndex = filters.map((filter) => filter[i]); + mergedFilters.push(mergeFilters(allFiltersAtIndex)); + } + + return mergedFilters; + } +} diff --git a/ndk/src/relay/subscriptions.ts b/ndk/src/relay/subscriptions.ts deleted file mode 100644 index 3bc086ff..00000000 --- a/ndk/src/relay/subscriptions.ts +++ /dev/null @@ -1,443 +0,0 @@ -import { EventEmitter } from "tseep"; -import type { Subscription, SubscriptionParams } from "nostr-tools"; -import { matchFilter } from "nostr-tools"; - -import type { NDKRelay } from "."; -import type { NostrEvent } from "../events"; -import { NDKEvent } from "../events"; -import type { NDKFilter, NDKSubscription } from "../subscription"; -import type { NDKFilterGroupingId } from "../subscription/grouping.js"; -import { calculateGroupableId, mergeFilters } from "../subscription/grouping.js"; -import { compareFilter, generateSubId } from "../subscription/utils"; -import type { NDKRelayConnectivity } from "./connectivity.js"; - -export type CountPayload = { - count: number; -}; - -export type SubscriptionOptions = { id?: string } & SubscriptionParams; - -export type SubEvent = { - event: (event: NostrEvent) => void | Promise; - count: (payload: CountPayload) => void | Promise; - eose: () => void | Promise; -}; - -/** - * Represents a collection of NDKSubscriptions (through NDKRelaySubscriptionFilters) - * that are grouped together to be sent to a relay as a single REQ. - * - * @emits closed It monitors the contained subscriptions and when all subscriptions are closed it emits "close". - */ -class NDKGroupedSubscriptions extends EventEmitter implements Iterable { - public subscriptions: NDKSubscriptionFilters[]; - public req?: NDKFilter[]; - public debug: debug.Debugger; - - public constructor(subscriptions: NDKSubscriptionFilters[], debug?: debug.Debugger) { - super(); - this.subscriptions = subscriptions; - this.debug = debug || this.subscriptions[0].subscription.debug.extend("grouped"); - - for (const subscription of subscriptions) { - this.handleSubscriptionClosure(subscription); - } - } - - /** - * Adds a subscription to this group. - * @param subscription - */ - public addSubscription(subscription: NDKSubscriptionFilters) { - // this.debug(`adding subscription`, subscription); - this.subscriptions.push(subscription); - - this.handleSubscriptionClosure(subscription); - } - - public eventReceived(rawEvent: NostrEvent) { - for (const subscription of this.subscriptions) { - subscription.eventReceived(rawEvent); - } - } - - public eoseReceived(relay: NDKRelay) { - // this.debug(`received EOSE from ${relay.url}, will send it to ${this.subscriptions.length} subscriptions`); - - // Loop through a copy since the "close" handler will modify the subscriptions array - const subscriptionsToInform = Array.from(this.subscriptions); - subscriptionsToInform.forEach(async (subscription) => { - // this.debug(`sending EOSE to subscription ${subscription.subscription.internalId}`); - subscription.subscription.eoseReceived(relay); - }); - } - - private handleSubscriptionClosure(subscription: NDKSubscriptionFilters) { - subscription.subscription.on("close", () => { - // this.debug(`going to remove subscription ${subscription.subscription.internalId} from grouped subscriptions, before removing there are ${this.subscriptions.length} subscriptions`, this.subscriptions); - const index = this.subscriptions.findIndex( - (i) => i.subscription === subscription.subscription - ); - this.subscriptions.splice(index, 1); - - // this.debug(`there are ${this.subscriptions.length} subscriptions left`); - - if (this.subscriptions.length <= 0) { - // this.debug(`going to emit close`); - this.emit("close"); - } - }); - } - - /** - * Maps each subscription through a transformation function. - * @param fn - The transformation function. - * @returns A new array with each subscription transformed by fn. - */ - public map( - fn: (sub: NDKSubscriptionFilters, index: number, array: NDKSubscriptionFilters[]) => T - ): T[] { - return this.subscriptions.map(fn); - } - - [Symbol.iterator](): Iterator { - let index = 0; - const subscriptions = this.subscriptions; - - return { - next(): IteratorResult { - if (index < subscriptions.length) { - return { value: subscriptions[index++], done: false }; - } else { - return { value: null, done: true }; - } - }, - }; - } -} - -/** - * Maintains an association of which filters belong to which subscription - * as sent to this particular relay. - */ -class NDKSubscriptionFilters { - public subscription: NDKSubscription; - public filters: NDKFilter[] = []; - private ndkRelay: NDKRelay; - - public constructor(subscription: NDKSubscription, filters: NDKFilter[], ndkRelay: NDKRelay) { - this.subscription = subscription; - this.filters = filters; - this.ndkRelay = ndkRelay; - } - - public eventReceived(rawEvent: NostrEvent) { - if (!this.eventMatchesLocalFilter(rawEvent)) return; - const event = new NDKEvent(undefined, rawEvent); - event.relay = this.ndkRelay; - this.subscription.eventReceived(event, this.ndkRelay, false); - } - - private eventMatchesLocalFilter(rawEvent: NostrEvent): boolean { - return this.filters.some((filter) => matchFilter(filter, rawEvent as any)); - } -} - -function findMatchingActiveSubscriptions(activeSubscriptions: NDKFilter[], filters: NDKFilter[]) { - if (activeSubscriptions.length !== filters.length) return false; - - for (let i = 0; i < activeSubscriptions.length; i++) { - if (!compareFilter(activeSubscriptions[i], filters[i])) { - break; - } - - return activeSubscriptions[i]; - } - - return undefined; -} - -type FiltersSub = { - filters: NDKFilter[]; - sub: Subscription; -}; - -/** - * @ignore - */ -export class NDKRelaySubscriptions { - private ndkRelay: NDKRelay; - private delayedItems: Map = new Map(); - private delayedTimers: Map = new Map(); - - /** - * Active subscriptions this relay is connected to - */ - readonly activeSubscriptions: Map = new Map(); - private activeSubscriptionsByGroupId: Map = new Map(); - private executionTimeoutsByGroupId: Map = new Map(); - private debug: debug.Debugger; - private groupingDebug: debug.Debugger; - private conn: NDKRelayConnectivity; - - public constructor(ndkRelay: NDKRelay) { - this.ndkRelay = ndkRelay; - this.conn = ndkRelay.connectivity; - this.debug = ndkRelay.debug.extend("subscriptions"); - this.groupingDebug = ndkRelay.debug.extend("grouping"); - } - - /** - * Creates or queues a subscription to the relay. - */ - public subscribe(subscription: NDKSubscription, filters: NDKFilter[]): void { - const groupableId = calculateGroupableId(filters, subscription.closeOnEose); - const subscriptionFilters = new NDKSubscriptionFilters( - subscription, - filters, - this.ndkRelay - ); - - const isNotGroupable = !groupableId || !subscription.isGroupable(); - - // If this subscription is not groupable, execute it immediately - if (isNotGroupable) { - this.executeSubscriptions( - groupableId, - // hacky - new NDKGroupedSubscriptions([subscriptionFilters]), - filters - ); - return; - } - - /* Check if there is an existing connection we can hook into */ - // TODO: Need a way to allow developers to opt out from this in case they want to receive before EOSE - // events - const activeSubscriptions = this.activeSubscriptionsByGroupId.get(groupableId); - if (activeSubscriptions) { - const matchingSubscription = findMatchingActiveSubscriptions( - activeSubscriptions.filters, - filters - ); - - if (matchingSubscription) { - const activeSubscription = this.activeSubscriptions.get(activeSubscriptions.sub); - activeSubscription?.addSubscription( - new NDKSubscriptionFilters(subscription, filters, this.ndkRelay) - ); - - return; - } - } - - let delayedItem = this.delayedItems.get(groupableId); - - if (!delayedItem) { - // No similar subscription exists, create a new delayed subscription - // this.debug("New delayed subscription with ID", groupableId, filters); - delayedItem = new NDKGroupedSubscriptions([subscriptionFilters]); - this.delayedItems.set(groupableId, delayedItem); - - // When the subscription closes, remove it from the delayed items - // XXX Need to remove this listener when the delayed item is executed - delayedItem.once("close", () => { - const delayedItem = this.delayedItems.get(groupableId); - if (!delayedItem) return; - this.delayedItems.delete(groupableId); - }); - } else { - // A similar subscription exists, add this subscription to the delayed subscription - // this.debug("Adding filters to delayed subscription", groupableId, filters); - delayedItem.addSubscription(subscriptionFilters); - } - - // Check if we have a timeout for this groupable ID - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let timeout: any = this.executionTimeoutsByGroupId.get(groupableId); - - // If we don't, or if this subscription's delay is marked as "at-most", then schedule the timeout too - // (it will empty the group when it runs so the race is not a problem) - if (!timeout || subscription.opts.groupableDelayType === "at-most") { - timeout = setTimeout(() => { - this.executionTimeoutsByGroupId.delete(groupableId); - this.executeGroup(groupableId, subscription); - }, subscription.opts.groupableDelay); - this.executionTimeoutsByGroupId.set(groupableId, timeout as unknown as number); - } - - if (this.delayedTimers.has(groupableId)) { - this.delayedTimers.get(groupableId)!.push(timeout as unknown as number); - } else { - this.delayedTimers.set(groupableId, [timeout as unknown as number]); - } - } - - /** - * Executes a delayed subscription via its groupable ID. - * @param groupableId - */ - private executeGroup(groupableId: NDKFilterGroupingId, triggeredBy: NDKSubscription) { - const delayedItem = this.delayedItems.get(groupableId); - this.delayedItems.delete(groupableId); - - const timeouts = this.delayedTimers.get(groupableId); - this.delayedTimers.delete(groupableId); - // this.groupingDebug(`Executing group ${groupableId} triggered by which has ${timeouts?.length} timeouts, sub delay is ${triggeredBy.opts.groupableDelay}ms ${triggeredBy.opts.subId}`, JSON.stringify(triggeredBy.filters)); - - // clear all timeouts - if (timeouts) { - for (const timeout of timeouts) { - clearTimeout(timeout); - } - } - - if (delayedItem) { - // Go through each index of one of the delayed item's filters so we can merge each items' index with the filters in the same index. The groupable ID guarantees that the filters will be mergable at the index level and that all filters have the same number of filters. - const filterCount = delayedItem.subscriptions[0].filters.length; - const mergedFilters: NDKFilter[] = []; - - for (let i = 0; i < filterCount; i++) { - const allFiltersAtIndex: NDKFilter[] = delayedItem.map((di) => di.filters[i]); - mergedFilters.push(mergeFilters(allFiltersAtIndex)); - } - - // this.groupingDebug("Merged filters", groupableId, JSON.stringify(mergedFilters), delayedItem.map((di) => di.filters[0])); - - this.executeSubscriptions(groupableId, delayedItem, mergedFilters); - } - } - - private executeSubscriptionsWhenConnected( - groupableId: NDKFilterGroupingId | null, - groupedSubscriptions: NDKGroupedSubscriptions, - mergedFilters: NDKFilter[] - ) { - // If the relay is not ready, add a one-time listener to wait for the 'ready' event - const readyListener = () => { - // this.debug("new relay coming online for active subscription", - // mergedFilters, - // this.ndkRelay.url, - // groupableId, - // ); - this.executeSubscriptionsConnected(groupableId, groupedSubscriptions, mergedFilters); - }; - - this.ndkRelay.once("ready", readyListener); - - // Add a one-time listener to remove the readyListener when the subscription stops - // in case it was stopped before the relay ever becamse available - groupedSubscriptions.once("close", () => { - this.ndkRelay.removeListener("ready", readyListener); - }); - } - - /** - * Executes one or more subscriptions. - * - * If the relay is not connected, subscriptions will be queued - * until the relay connects. - * - * @param groupableId - * @param subscriptionFilters - * @param mergedFilters - */ - private executeSubscriptions( - groupableId: NDKFilterGroupingId | null, - groupedSubscriptions: NDKGroupedSubscriptions, - mergedFilters: NDKFilter[] - ) { - if (this.conn.isAvailable()) { - this.executeSubscriptionsConnected(groupableId, groupedSubscriptions, mergedFilters); - } else { - this.executeSubscriptionsWhenConnected( - groupableId, - groupedSubscriptions, - mergedFilters - ); - } - } - - /** - * Executes one or more subscriptions. - * - * When there are more than one subscription, results - * will be sent to the right subscription - * - * @param subscriptions - * @param filters The filters as they should be sent to the relay - */ - public executeSubscriptionsConnected( - groupableId: NDKFilterGroupingId | null, - groupedSubscriptions: NDKGroupedSubscriptions, - mergedFilters: NDKFilter[] - ): Subscription { - const subscriptions: NDKSubscription[] = []; - - for (const { subscription } of groupedSubscriptions) { - subscriptions.push(subscription); - } - - const subId = generateSubId(subscriptions, mergedFilters); - groupedSubscriptions.req = mergedFilters; - - const subOptions: SubscriptionOptions = { - id: subId, - onevent: (event: NostrEvent) => { - const e = new NDKEvent(undefined, event); - e.relay = this.ndkRelay; - - const subFilters = this.activeSubscriptions.get(sub); - subFilters?.eventReceived(e.rawEvent()); - }, - oneose: () => { - const subFilters = this.activeSubscriptions.get(sub); - subFilters?.eoseReceived(this.ndkRelay); - - // if we are supposed to close on EOSE, close the subscription - if (subscriptions.every((sub) => sub.closeOnEose)) { - sub.close(); - } - }, - onclose: () => {}, - }; - - // TODO: Looks like nostr-tools doesn't allow skipping verification anymore - // if (this.ndkRelay.trusted || subscriptions.every((sub) => sub.opts.skipVerification)) { - // subOptions.skipVerification = true; - // } - - const sub = this.conn.relay.subscribe(mergedFilters, subOptions); - - this.activeSubscriptions.set(sub, groupedSubscriptions); - if (groupableId) { - this.activeSubscriptionsByGroupId.set(groupableId, { filters: mergedFilters, sub }); - } - - groupedSubscriptions.once("close", () => { - // this.debug(`Closing subscription ${this.ndkRelay.url} for subscription ${subId}`); - sub.close(); - this.activeSubscriptions.delete(sub); - if (groupableId) { - this.activeSubscriptionsByGroupId.delete(groupableId); - } - }); - - this.executeSubscriptionsWhenConnected(groupableId, groupedSubscriptions, mergedFilters); - - return sub; - } - - public executedFilters(): Map { - const ret = new Map(); - - for (const [, groupedSubscriptions] of this.activeSubscriptions) { - ret.set( - groupedSubscriptions.req!, - groupedSubscriptions.map((sub) => sub.subscription) - ); - } - - return ret; - } -} diff --git a/ndk/src/signers/index.ts b/ndk/src/signers/index.ts index 9e2e52aa..a1a28492 100644 --- a/ndk/src/signers/index.ts +++ b/ndk/src/signers/index.ts @@ -1,5 +1,6 @@ import type { NostrEvent } from "../events/index.js"; -import { NDKRelay } from "../relay/index.js"; +import type { NDK } from "../ndk/index.js"; +import type { NDKRelay } from "../relay/index.js"; import type { NDKUser } from "../user"; /** @@ -29,7 +30,7 @@ export interface NDKSigner { * Getter for the preferred relays. * @returns A promise containing a simple map of preferred relays and their read/write policies. */ - relays?(): Promise; + relays?(ndk?: NDK): Promise; /** * Encrypts the given Nostr event for the given recipient. diff --git a/ndk/src/signers/nip07/index.ts b/ndk/src/signers/nip07/index.ts index 0a6500f6..cfe85897 100644 --- a/ndk/src/signers/nip07/index.ts +++ b/ndk/src/signers/nip07/index.ts @@ -4,6 +4,7 @@ import type { NostrEvent } from "../../events/index.js"; import { NDKUser } from "../../user/index.js"; import type { NDKSigner } from "../index.js"; import { NDKRelay } from "../../relay/index.js"; +import type { NDK } from "../../ndk/index.js"; type Nip04QueueItem = { type: "encrypt" | "decrypt"; @@ -77,7 +78,7 @@ export class NDKNip07Signer implements NDKSigner { return signedEvent.sig; } - public async relays(): Promise { + public async relays(ndk?: NDK): Promise { await this.waitForExtension(); const relays = (await window.nostr!.getRelays?.()) || {}; @@ -89,7 +90,7 @@ export class NDKNip07Signer implements NDKSigner { activeRelays.push(url); } } - return activeRelays.map((url) => new NDKRelay(url)); + return activeRelays.map((url) => new NDKRelay(url, ndk?.relayAuthDefaultPolicy, ndk)); } public async encrypt(recipient: NDKUser, value: string): Promise { @@ -136,12 +137,6 @@ export class NDKNip07Signer implements NDKSigner { const { type, counterpartyHexpubkey, value, resolve, reject } = item || this.nip04Queue.shift()!; - this.debug("Processing encryption queue item", { - type, - counterpartyHexpubkey, - value, - }); - try { let result; diff --git a/ndk/src/signers/nip46/index.ts b/ndk/src/signers/nip46/index.ts index 8c29a190..aae3143b 100644 --- a/ndk/src/signers/nip46/index.ts +++ b/ndk/src/signers/nip46/index.ts @@ -1,13 +1,14 @@ import { EventEmitter } from "tseep"; import type { NostrEvent } from "../../events/index.js"; import type { NDK } from "../../ndk/index.js"; -import { Hexpubkey, NDKUser } from "../../user/index.js"; +import type { Hexpubkey } from "../../user/index.js"; +import { NDKUser } from "../../user/index.js"; import type { NDKSigner } from "../index.js"; import { NDKPrivateKeySigner } from "../private-key/index.js"; import type { NDKRpcResponse } from "./rpc.js"; import { NDKNostrRpc } from "./rpc.js"; import { NDKKind } from "../../events/kinds/index.js"; -import { NDKSubscription } from "../../subscription/index.js"; +import type { NDKSubscription } from "../../subscription/index.js"; /** * This NDKSigner implements NIP-46, which allows remote signing of events. diff --git a/ndk/src/subscription/grouping.test.ts b/ndk/src/subscription/grouping.test.ts index 25703fa8..20d1d5c0 100644 --- a/ndk/src/subscription/grouping.test.ts +++ b/ndk/src/subscription/grouping.test.ts @@ -1,40 +1,36 @@ -import { calculateGroupableId } from "./grouping.js"; +import { filterFingerprint } from "./grouping.js"; import type { NDKFilter } from "./index.js"; -describe("calculateGroupableId", () => { +describe("filterFingerprint", () => { it("includes filters in the ID", () => { const filters = [{ kinds: [1], authors: ["author1"] }]; - expect(calculateGroupableId(filters, false)).toEqual("authors-kinds"); + expect(filterFingerprint(filters, false)).toEqual("authors-kinds"); }); it("order of keys is irrelevant", () => { const filters = [{ authors: ["author1"], kinds: [1] }]; - expect(calculateGroupableId(filters, false)).toEqual("authors-kinds"); + expect(filterFingerprint(filters, false)).toEqual("authors-kinds"); }); it("does not group when there are time constraints", () => { const filters = [{ kinds: [1], authors: ["author1"], since: 1 }]; - expect(calculateGroupableId(filters, false)).toBeNull(); + expect(filterFingerprint(filters, false)).toBeNull(); }); it("generates different group IDs when the same filter keys are used but in incompatible filters", () => { const filters1: NDKFilter[] = [{ kinds: [1], authors: ["author1"] }, { "#e": ["id1"] }]; const filters2: NDKFilter[] = [{ kinds: [1] }, { authors: ["author2"], "#e": ["id2"] }]; - expect(calculateGroupableId(filters1, false)).not.toEqual( - calculateGroupableId(filters2, false) - ); + expect(filterFingerprint(filters1, false)).not.toEqual(filterFingerprint(filters2, false)); }); it("generates the same group IDs with multiple compatible filters", () => { const filters1: NDKFilter[] = [{ kinds: [1] }, { authors: ["author1"], "#e": ["id1"] }]; const filters2: NDKFilter[] = [{ kinds: [1] }, { authors: ["author2"], "#e": ["id2"] }]; - expect(calculateGroupableId(filters1, false)).toEqual( - calculateGroupableId(filters2, false) - ); + expect(filterFingerprint(filters1, false)).toEqual(filterFingerprint(filters2, false)); }); }); diff --git a/ndk/src/subscription/grouping.ts b/ndk/src/subscription/grouping.ts index 196d5f5e..95c5bb8c 100644 --- a/ndk/src/subscription/grouping.ts +++ b/ndk/src/subscription/grouping.ts @@ -1,12 +1,13 @@ import type { NDKFilter } from "../index.js"; -export type NDKFilterGroupingId = string; +export type NDKFilterFingerprint = string; /** - * Calculates the groupable ID for this filters. - * The groupable ID is a deterministic association of the filters + * Creates a fingerprint for this filter + * + * This a deterministic association of the filters * used in a filters. When the combination of filters makes it - * possible to group them, the groupable ID is used to group them. + * possible to group them, the fingerprint is used to group them. * * The different filters in the array are differentiated so that * filters can only be grouped with other filters that have the same signature @@ -15,18 +16,18 @@ export type NDKFilterGroupingId = string; * that intend to close immediately after EOSE and those that are probably * going to be kept open. * - * @returns The groupable ID, or null if the filters are not groupable. + * @returns The fingerprint, or undefined if the filters are not groupable. */ -export function calculateGroupableId( +export function filterFingerprint( filters: NDKFilter[], closeOnEose: boolean -): NDKFilterGroupingId | null { +): NDKFilterFingerprint | undefined { const elements: string[] = []; for (const filter of filters) { const hasTimeConstraints = filter.since || filter.until; - if (hasTimeConstraints) return null; + if (hasTimeConstraints) return undefined; const keys = Object.keys(filter || {}) .sort() diff --git a/ndk/src/subscription/index.ts b/ndk/src/subscription/index.ts index dab28db9..72ee0b38 100644 --- a/ndk/src/subscription/index.ts +++ b/ndk/src/subscription/index.ts @@ -1,13 +1,19 @@ import { EventEmitter } from "tseep"; -import { NDKEvent, NDKEventId } from "../events/index.js"; -import type { NDKKind } from "../events/kinds/index.js"; +import type { NDKEventId, NostrEvent } from "../events/index.js"; +import { NDKEvent } from "../events/index.js"; import type { NDK } from "../ndk/index.js"; -import { NDKRelay } from "../relay"; +import type { NDKRelay } from "../relay"; import type { NDKPool } from "../relay/pool/index.js"; import { calculateRelaySetsFromFilters } from "../relay/sets/calculate"; import type { NDKRelaySet } from "../relay/sets/index.js"; import { queryFullyFilled } from "./utils.js"; +import type { NDKKind } from "../events/kinds/index.js"; +import { verifiedSignatures } from "../events/validation.js"; + +export type NDKSubscriptionInternalId = string; + +export type NDKSubscriptionDelayedType = "at-least" | "at-most"; export type NDKFilter = { ids?: string[]; @@ -68,7 +74,7 @@ export interface NDKSubscriptionOptions { * const sub2 = ndk.subscribe({ kinds: [0], authors: ["alice"] }, { groupableDelay: 1000, groupableDelayType: "at-most" }); * // sub1 and sub2 will be grouped together and executed 1000ms after sub1 was created */ - groupableDelayType?: "at-least" | "at-most"; + groupableDelayType?: NDKSubscriptionDelayedType; /** * The subscription ID to use for the subscription. @@ -149,12 +155,20 @@ export class NDKSubscription extends EventEmitter<{ eose: (sub: NDKSubscription) => void; close: (sub: NDKSubscription) => void; "event:dup": ( - event: NDKEvent, + eventId: NDKEventId, relay: NDKRelay | undefined, timeSinceFirstSeen: number, sub: NDKSubscription ) => void; event: (event: NDKEvent, relay: NDKRelay | undefined, sub: NDKSubscription) => void; + + /** + * Emitted when a relay unilaterally closes the subscription. + * @param relay + * @param reason + * @returns + */ + closed: (relay: NDKRelay, reason: string) => void; }> { readonly subId?: string; readonly filters: NDKFilter[]; @@ -181,24 +195,24 @@ export class NDKSubscription extends EventEmitter<{ */ public eosesSeen = new Set(); - /** - * Events that have been seen by the subscription per relay. - */ - public eventsPerRelay: Map> = new Map(); - /** * The time the last event was received by the subscription. * This is used to calculate when EOSE should be emitted. */ private lastEventReceivedAt: number | undefined; - public internalId: string; + public internalId: NDKSubscriptionInternalId; /** * Whether the subscription should close when all relays have reached the end of the event stream. */ public closeOnEose: boolean; + /** + * Pool monitor callback + */ + private poolMonitor: ((relay: NDKRelay) => void) | undefined; + public constructor( ndk: NDK, filters: NDKFilter | NDKFilter[], @@ -250,6 +264,15 @@ export class NDKSubscription extends EventEmitter<{ return this.filters[0]; } + get groupableDelay(): number | undefined { + if (!this.isGroupable()) return undefined; + return this.opts?.groupableDelay; + } + + get groupableDelayType(): NDKSubscriptionDelayedType { + return this.opts?.groupableDelayType || "at-most"; + } + public isGroupable(): boolean { return this.opts?.groupable || false; } @@ -299,6 +322,7 @@ export class NDKSubscription extends EventEmitter<{ if (this.shouldQueryRelays()) { this.startWithRelays(); + this.startPoolMonitor(); } else { this.emit("eose", this); } @@ -306,8 +330,35 @@ export class NDKSubscription extends EventEmitter<{ return; } + /** + * We want to monitor for new relays that are coming online, in case + * they should be part of this subscription. + */ + private startPoolMonitor(): void { + const d = this.debug.extend("pool-monitor"); + + this.poolMonitor = (relay: NDKRelay) => { + // check if the pool monitor is already in the relayFilters + if (this.relayFilters?.has(relay.url)) return; + + const calc = calculateRelaySetsFromFilters(this.ndk, this.filters, this.pool); + + // check if the new relay is included + if (calc.get(relay.url)) { + // add it to the relayFilters + this.relayFilters?.set(relay.url, this.filters); + + // d("New relay connected -- adding to subscription", relay.url); + relay.subscribe(this, this.filters); + } + }; + + this.pool.on("relay:connect", this.poolMonitor); + } + public stop(): void { this.emit("close", this); + this.poolMonitor && this.pool.off("relay:connect", this.poolMonitor); this.removeAllListeners(); } @@ -341,15 +392,9 @@ export class NDKSubscription extends EventEmitter<{ } } - // if relayset is empty, we can't start, log it - if (!this.relayFilters || this.relayFilters.size === 0) { - this.debug(`No relays to subscribe to (%d connected relays)`, this.pool.connectedRelays().length); - return; - } + if (!this.relayFilters || this.relayFilters.size === 0) return; // iterate through the this.relayFilters - // console.log(this.relayFilters); - // console.log('start with relays', {relayFilters: this.relayFilters.values(), filters: JSON.stringify(this.filters), size: this.relayFilters.size}); for (const [relayUrl, filters] of this.relayFilters) { const relay = this.pool.getRelay(relayUrl, true, true, filters); relay.subscribe(this, filters); @@ -366,88 +411,94 @@ export class NDKSubscription extends EventEmitter<{ * @param optimisticPublish Whether this event is coming from an optimistic publish */ public eventReceived( - event: NDKEvent, + event: NDKEvent | NostrEvent, relay: NDKRelay | undefined, fromCache: boolean = false, optimisticPublish: boolean = false ) { - if (relay) { - event.relay ??= relay; - event.onRelays.push(relay); - } - if (!relay) relay = event.relay; - - event.ndk ??= this.ndk; - - if (!fromCache && relay) { - this.ndk.emit("event", event, relay); - } + const eventId = event.id! as NDKEventId; + const eventAlreadySeen = this.eventFirstSeen.has(eventId); + let ndkEvent: NDKEvent; + + if (event instanceof NDKEvent) ndkEvent = event; + + if (!eventAlreadySeen) { + // generate the ndkEvent + ndkEvent ??= new NDKEvent(this.ndk, event); + ndkEvent.ndk = this.ndk; + ndkEvent.relay = relay; + + // we don't want to validate/verify events that are either + // coming from the cache or have been published by us from within + // the client + if (!fromCache && !optimisticPublish) { + // validate it + if (!this.skipValidation) { + if (!ndkEvent.isValid) { + this.debug(`Event failed validation %s from relay %s`, eventId, relay?.url); + return; + } + } - // mark the event as seen - // move here to avoid verifying signature of duplicate events - const eventAlreadySeen = this.eventFirstSeen.has(event.id); + // verify it + if (relay) { + if (relay?.shouldValidateEvent() !== false) { + if (!this.skipVerification) { + if (!ndkEvent.verifySignature(true) && !this.ndk.asyncSigVerification) { + this.debug(`Event failed signature validation`, event); + return; + } else if (relay) { + relay.addValidatedEvent(); + } + } + } else { + relay.addNonValidatedEvent(); + } + } - if (eventAlreadySeen) { - const timeSinceFirstSeen = Date.now() - (this.eventFirstSeen.get(event.id) || 0); - if (relay) { - relay.scoreSlowerEvent(timeSinceFirstSeen); - this.trackPerRelay(event, relay); + if (this.ndk.cacheAdapter) { + this.ndk.cacheAdapter.setEvent(ndkEvent, this.filters, relay); + } } - this.emit("event:dup", event, relay, timeSinceFirstSeen, this); + // emit it + if (!fromCache && relay) { + this.ndk.emit("event", ndkEvent, relay); + } - return; - } + this.emit("event", ndkEvent, relay, this); - if (!fromCache && !optimisticPublish) { - if (!this.skipValidation) { - if (!event.isValid) { - this.debug(`Event failed validation`, event.rawEvent()); - return; - } - } + // mark the eventId as seen + this.eventFirstSeen.set(eventId, Date.now()); + } else { + const timeSinceFirstSeen = Date.now() - (this.eventFirstSeen.get(eventId) || 0); + this.emit("event:dup", eventId, relay, timeSinceFirstSeen, this); - if (event.relay?.shouldValidateEvent() !== false) { - if (!this.skipVerification) { - if (!event.verifySignature(true) && !this.ndk.asyncSigVerification) { - this.debug(`Event failed signature validation`, event); - return; + if (relay) { + // Let's see if we have already verified this event id's signature + const signature = verifiedSignatures.get(eventId); + if (signature && typeof signature === "string") { + // If it matches then we can increase the relay verification count + if (event.sig === signature) { + relay.addValidatedEvent(); } } } } - if (!fromCache && !optimisticPublish && relay) { - this.trackPerRelay(event, relay); - - if (this.ndk.cacheAdapter) { - this.ndk.cacheAdapter.setEvent(event, this.filters, relay); - } - - this.eventFirstSeen.set(event.id, Date.now()); - } else { - this.eventFirstSeen.set(event.id, 0); - } - - this.emit("event", event, relay, this); this.lastEventReceivedAt = Date.now(); } - private trackPerRelay(event: NDKEvent, relay: NDKRelay): void { - let events = this.eventsPerRelay.get(relay); - - if (!events) { - events = new Set(); - this.eventsPerRelay.set(relay, events); - } - - events.add(event.id); + public closedReceived(relay: NDKRelay, reason: string): void { + this.emit("closed", relay, reason); } // EOSE handling private eoseTimeout: ReturnType | undefined; + private eosed = false; public eoseReceived(relay: NDKRelay): void { + this.debug(`Received EOSE from %s`, relay.url); this.eosesSeen.add(relay); let lastEventSeen = this.lastEventReceivedAt @@ -457,24 +508,24 @@ export class NDKSubscription extends EventEmitter<{ const hasSeenAllEoses = this.eosesSeen.size === this.relayFilters?.size; const queryFilled = queryFullyFilled(this); - if (queryFilled) { + const performEose = (reason: string) => { + if (this.eosed) return; + if (this.eoseTimeout) clearTimeout(this.eoseTimeout); + this.debug(`Performing EOSE: ${reason}`); this.emit("eose", this); + this.eosed = true; - if (this.opts?.closeOnEose) { - this.stop(); - } - } else if (hasSeenAllEoses) { - this.emit("eose", this); + if (this.opts?.closeOnEose) this.stop(); + }; - if (this.opts?.closeOnEose) { - this.stop(); - } - } else { + if (queryFilled || hasSeenAllEoses) { + performEose("query filled or seen all"); + } else if (this.relayFilters) { let timeToWaitForNextEose = 1000; const connectedRelays = new Set(this.pool.connectedRelays().map((r) => r.url)); - let connectedRelaysWithFilters = Array.from(this.relayFilters!.keys()).filter((url) => + const connectedRelaysWithFilters = Array.from(this.relayFilters.keys()).filter((url) => connectedRelays.has(url) ); @@ -495,10 +546,13 @@ export class NDKSubscription extends EventEmitter<{ timeToWaitForNextEose = timeToWaitForNextEose * (1 - percentageOfRelaysThatHaveSentEose); - if (this.eoseTimeout) { - clearTimeout(this.eoseTimeout); + if (timeToWaitForNextEose === 0) { + performEose("tiem to wait was 0"); + return; } + if (this.eoseTimeout) clearTimeout(this.eoseTimeout); + const sendEoseTimeout = () => { lastEventSeen = this.lastEventReceivedAt ? Date.now() - this.lastEventReceivedAt @@ -509,8 +563,7 @@ export class NDKSubscription extends EventEmitter<{ if (lastEventSeen !== undefined && lastEventSeen < 20) { this.eoseTimeout = setTimeout(sendEoseTimeout, timeToWaitForNextEose); } else { - this.emit("eose", this); - if (this.opts?.closeOnEose) this.stop(); + performEose("send eose timeout: " + timeToWaitForNextEose); } }; diff --git a/ndk/src/subscription/manager.ts b/ndk/src/subscription/manager.ts index 4effe091..f20fc1f2 100644 --- a/ndk/src/subscription/manager.ts +++ b/ndk/src/subscription/manager.ts @@ -1,10 +1,16 @@ -import { NDKSubscription } from "./index.js"; -import debug from "debug"; +import type { NDKEventId } from "../events/index.js"; +import type { NDKRelay } from "../relay/index.js"; +import type { NDKSubscription } from "./index.js"; +import type debug from "debug"; export type NDKSubscriptionId = string; +/** + * This class monitors active subscriptions. + */ export class NDKSubscriptionManager { public subscriptions: Map; + public seenEvents = new Map(); private debug: debug.Debugger; constructor(debug: debug.Debugger) { @@ -19,4 +25,10 @@ export class NDKSubscriptionManager { this.subscriptions.delete(sub.internalId); }); } + + public seenEvent(eventId: NDKEventId, relay: NDKRelay) { + const current = this.seenEvents.get(eventId) || []; + current.push(relay); + this.seenEvents.set(eventId, current); + } } diff --git a/ndk/src/subscription/utils.test.ts b/ndk/src/subscription/utils.test.ts index fe96d348..f4326c01 100644 --- a/ndk/src/subscription/utils.test.ts +++ b/ndk/src/subscription/utils.test.ts @@ -59,26 +59,34 @@ describe("generateSubId", () => { describe("filterFromId", () => { it("handles nevents", () => { - const filter = filterFromId("nevent1qgs9kqvr4dkruv3t7n2pc6e6a7v9v2s5fprmwjv4gde8c4fe5y29v0spzamhxue69uhhyetvv9ujuurjd9kkzmpwdejhgtcqype6ycavy2e9zpx9mzeuekaahgw96ken0mzkcmgz40ljccwyrn88gxv2ewr"); + const filter = filterFromId( + "nevent1qgs9kqvr4dkruv3t7n2pc6e6a7v9v2s5fprmwjv4gde8c4fe5y29v0spzamhxue69uhhyetvv9ujuurjd9kkzmpwdejhgtcqype6ycavy2e9zpx9mzeuekaahgw96ken0mzkcmgz40ljccwyrn88gxv2ewr" + ); expect(filter).toEqual({ ids: ["73a263ac22b25104c5d8b3ccdbbdba1c5d5b337ec56c6d02abff2c61c41cce74"], authors: ["5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e"], }); - }) -}) + }); +}); describe("filterForEventsTaggingId", () => { fit("handles nevents", () => { - const filter = filterForEventsTaggingId("nevent1qgs9kqvr4dkruv3t7n2pc6e6a7v9v2s5fprmwjv4gde8c4fe5y29v0spzamhxue69uhhyetvv9ujuurjd9kkzmpwdejhgtcqype6ycavy2e9zpx9mzeuekaahgw96ken0mzkcmgz40ljccwyrn88gxv2ewr"); + const filter = filterForEventsTaggingId( + "nevent1qgs9kqvr4dkruv3t7n2pc6e6a7v9v2s5fprmwjv4gde8c4fe5y29v0spzamhxue69uhhyetvv9ujuurjd9kkzmpwdejhgtcqype6ycavy2e9zpx9mzeuekaahgw96ken0mzkcmgz40ljccwyrn88gxv2ewr" + ); expect(filter).toEqual({ "#e": ["73a263ac22b25104c5d8b3ccdbbdba1c5d5b337ec56c6d02abff2c61c41cce74"], }); - }) + }); fit("handles naddr", () => { - const filter = filterForEventsTaggingId("naddr1qvzqqqr4gupzpjjwt0eqm6as279wf079c0j42jysp2t4s37u8pg5w2dfyktxgkntqqxnzde38yen2desxqmn2d3332u3ff"); + const filter = filterForEventsTaggingId( + "naddr1qvzqqqr4gupzpjjwt0eqm6as279wf079c0j42jysp2t4s37u8pg5w2dfyktxgkntqqxnzde38yen2desxqmn2d3332u3ff" + ); expect(filter).toEqual({ - "#a": ["30023:ca4e5bf20debb0578ae4bfc5c3e55548900a975847dc38514729a92596645a6b:1719357007561"], + "#a": [ + "30023:ca4e5bf20debb0578ae4bfc5c3e55548900a975847dc38514729a92596645a6b:1719357007561", + ], }); - }) -}) \ No newline at end of file + }); +}); diff --git a/ndk/src/subscription/utils.ts b/ndk/src/subscription/utils.ts index be63cabe..ff6af939 100644 --- a/ndk/src/subscription/utils.ts +++ b/ndk/src/subscription/utils.ts @@ -2,7 +2,8 @@ import { nip19 } from "nostr-tools"; import { NDKRelay } from "../relay/index.js"; import type { NDKFilter, NDKSubscription } from "./index.js"; -import { EventPointer } from "../user/index.js"; +import type { EventPointer } from "../user/index.js"; +import type { NDK } from "../ndk/index.js"; /** * Don't generate subscription Ids longer than this amount of characters @@ -131,7 +132,7 @@ export function generateSubId(subscriptions: NDKSubscription[], filters: NDKFilt * const bech32 = "nevent1qgs9kqvr4dkruv3t7n2pc6e6a7v9v2s5fprmwjv4gde8c4fe5y29v0spzamhxue69uhhyetvv9ujuurjd9kkzmpwdejhgtcqype6ycavy2e9zpx9mzeuekaahgw96ken0mzkcmgz40ljccwyrn88gxv2ewr" * const filter = filterForEventsTaggingId(bech32); * // filter => { "#e": [] } - * + * * @example * const bech32 = "naddr1qvzqqqr4gupzpjjwt0eqm6as279wf079c0j42jysp2t4s37u8pg5w2dfyktxgkntqqxnzde38yen2desxqmn2d3332u3ff"; * const filter = filterForEventsTaggingId(bech32); @@ -142,18 +143,27 @@ export function filterForEventsTaggingId(id: string): NDKFilter | undefined { const decoded = nip19.decode(id); switch (decoded.type) { - case 'naddr': return { "#a": [`${decoded.data.kind}:${decoded.data.pubkey}:${decoded.data.identifier}`] } - case 'nevent': return { "#e": [decoded.data.id] } - case 'note': return { "#e": [decoded.data] } - case 'nprofile': return { "#p": [decoded.data.pubkey] } - case 'npub': return { "#p": [decoded.data] } + case "naddr": + return { + "#a": [ + `${decoded.data.kind}:${decoded.data.pubkey}:${decoded.data.identifier}`, + ], + }; + case "nevent": + return { "#e": [decoded.data.id] }; + case "note": + return { "#e": [decoded.data] }; + case "nprofile": + return { "#p": [decoded.data.pubkey] }; + case "npub": + return { "#p": [decoded.data] }; } } catch {} } /** * Creates a valid nostr filter from an event id or a NIP-19 bech32. - * + * * @example * const bech32 = "nevent1qgs9kqvr4dkruv3t7n2pc6e6a7v9v2s5fprmwjv4gde8c4fe5y29v0spzamhxue69uhhyetvv9ujuurjd9kkzmpwdejhgtcqype6ycavy2e9zpx9mzeuekaahgw96ken0mzkcmgz40ljccwyrn88gxv2ewr" * const filter = filterFromBech32(bech32); @@ -225,7 +235,7 @@ export const BECH32_REGEX = /^n(event|ote|profile|pub|addr)1[\d\w]+$/; * * @param bech32 The NIP-19 bech32. */ -export function relaysFromBech32(bech32: string): NDKRelay[] { +export function relaysFromBech32(bech32: string, ndk?: NDK): NDKRelay[] { try { const decoded = nip19.decode(bech32); @@ -233,7 +243,9 @@ export function relaysFromBech32(bech32: string): NDKRelay[] { const data = decoded.data as unknown as EventPointer; if (data?.relays) { - return data.relays.map((r: string) => new NDKRelay(r)); + return data.relays.map( + (r: string) => new NDKRelay(r, ndk?.relayAuthDefaultPolicy, ndk) + ); } } } catch (e) { diff --git a/ndk/src/thread/index.test.ts b/ndk/src/thread/index.test.ts index b0c88719..fc9accc2 100644 --- a/ndk/src/thread/index.test.ts +++ b/ndk/src/thread/index.test.ts @@ -6,7 +6,8 @@ import { getReplyTag, getRootTag, } from "."; -import { NDKEvent, NDKEventId } from "../events"; +import type { NDKEventId } from "../events"; +import { NDKEvent } from "../events"; const op = new NDKEvent(undefined, { id: "op", diff --git a/ndk/src/thread/index.ts b/ndk/src/thread/index.ts index 71ab809e..2f4cf409 100644 --- a/ndk/src/thread/index.ts +++ b/ndk/src/thread/index.ts @@ -1,4 +1,4 @@ -import { NDKEvent, NDKEventId, NDKTag } from "../events"; +import type { NDKEvent, NDKEventId, NDKTag } from "../events"; export function eventsBySameAuthor(op: NDKEvent, events: NDKEvent[]) { const eventsByAuthor = new Map(); @@ -106,7 +106,7 @@ export function eventThreads(op: NDKEvent, events: NDKEvent[]) { export function getEventReplyIds(event: NDKEvent): NDKEventId[] { if (hasMarkers(event, event.tagType())) { let rootTag: NDKTag | undefined; - let replyTags: NDKTag[] = []; + const replyTags: NDKTag[] = []; event.getMatchingTags(event.tagType()).forEach((tag) => { if (tag[3] === "root") rootTag = tag; @@ -196,7 +196,7 @@ export function getRootEventId(event: NDKEvent, searchTag?: string): NDKEventId */ export function getRootTag(event: NDKEvent, searchTag?: string): NDKTag | undefined { searchTag ??= event.tagType(); - let rootEventTag = event.tags.find((tag) => tag[3] === "root"); + const rootEventTag = event.tags.find((tag) => tag[3] === "root"); if (!rootEventTag) { // If we don't have an explicit root marer, this event has no other e-tag markers diff --git a/ndk/src/user/index.ts b/ndk/src/user/index.ts index f5988ca1..96a22992 100644 --- a/ndk/src/user/index.ts +++ b/ndk/src/user/index.ts @@ -6,10 +6,21 @@ import type { NDK } from "../ndk/index.js"; import { NDKSubscriptionCacheUsage, type NDKSubscriptionOptions } from "../subscription/index.js"; import { follows } from "./follows.js"; import { type NDKUserProfile, profileFromEvent, serializeProfile } from "./profile.js"; -import type { NDKSigner } from "../signers/index.js"; -import { NDKLnUrlData } from "../zap/index.js"; import { getNip05For } from "./nip05.js"; -import { NDKRelay, NDKZap } from "../index.js"; +import type { + NDKRelay, + NDKSigner, + NDKZapDetails, + NDKZapMethod, + NDKZapMethodInfo, +} from "../index.js"; +import { NDKCashuMintList } from "../events/kinds/nutzap/mint-list.js"; +import { + getNip57ZapSpecFromLud, + LnPaymentInfo, + LNPaymentRequest, + NDKLnUrlData, +} from "../zapper/ln.js"; export type Hexpubkey = string; @@ -113,6 +124,92 @@ export class NDKUser { this._pubkey = pubkey; } + /** + * Gets NIP-57 and NIP-61 information that this user has signaled + * + * @param getAll {boolean} Whether to get all zap info or just the first one + */ + async getZapInfo( + getAll = true, + methods: NDKZapMethod[] = ["nip61", "nip57"] + ): Promise { + 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, + } + ); + + if (events.size < methods.length) { + events = await this.ndk.fetchEvents( + { kinds, authors: [this.pubkey] }, + { + cacheUsage: NDKSubscriptionCacheUsage.ONLY_RELAY, + } + ); + } + + const res: NDKZapMethodInfo[] = []; + + 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, + }, + }); + } + + // 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); + } + } + + return res; + } + + /** + * 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 */ @@ -131,10 +228,13 @@ export class NDKUser { } } - const zap = new NDKZap({ ndk: ndk!, zappedUser: this }); let lnurlspec: NDKLnUrlData | undefined; try { - lnurlspec = await zap.getZapSpecWithoutCache(); + 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) { @@ -176,7 +276,7 @@ export class NDKUser { ): Promise { if (!ndk) throw new Error("No NDK instance found"); - let opts: RequestInit = {}; + const opts: RequestInit = {}; if (skipCache) opts.cache = "no-cache"; const profile = await getNip05For(ndk, nip05Id, ndk?.httpFetch, opts); @@ -432,32 +532,23 @@ export class NDKUser { * @param extraTags Extra tags to add to the zap request * @param signer The signer to use (will default to the NDK instance's signer) */ - async zap( - amount: number, - comment?: string, - extraTags?: NDKTag[], - signer?: NDKSigner - ): Promise { - if (!this.ndk) throw new Error("No NDK instance found"); + async zap(amount: number, comment?: string, tags?: NDKTag[], signer?: NDKSigner) { + return new Promise((resolve, reject) => { + if (!this.ndk) { + reject("No NDK instance found"); + return; + } - if (!signer) { - this.ndk.assertSigner(); - } + // If we already have a wallet configured, we'll use that + // otherwise we'll just get the payment request and return it + // to maintain compatibility with the old behavior + let onLnPay = this.ndk.walletConfig?.onLnPay; + onLnPay ??= async ({ pr }: { pr: LNPaymentRequest }): Promise => { + resolve(pr); + }; - const zap = new NDKZap({ - ndk: this.ndk, - zappedUser: this, + const zapper = this.ndk.zap(this, amount, { comment, tags, signer, onLnPay }); + zapper.zap().then(resolve).catch(reject); }); - - const relays = Array.from(this.ndk.pool.relays.keys()); - - const paymentRequest = await zap.createZapRequest( - amount, - comment, - extraTags, - relays, - signer - ); - return paymentRequest; } } diff --git a/ndk/src/user/nip05.ts b/ndk/src/user/nip05.ts index af23d6fa..9f9e242d 100644 --- a/ndk/src/user/nip05.ts +++ b/ndk/src/user/nip05.ts @@ -1,4 +1,5 @@ -import { Hexpubkey, NDKUser, ProfilePointer } from "."; +import type { Hexpubkey, ProfilePointer } from "."; +import { NDKUser } from "."; import type { NDK } from "../ndk"; export const NIP05_REGEX = /^(?:([\w.+-]+)@)?([\w.-]+)$/; diff --git a/ndk/src/user/pin.ts b/ndk/src/user/pin.ts index 9c250788..1cb0470f 100644 --- a/ndk/src/user/pin.ts +++ b/ndk/src/user/pin.ts @@ -1,5 +1,6 @@ -import { NDKUser } from "."; -import { NDKEvent, NostrEvent } from "../events"; +import type { NDKUser } from "."; +import type { NostrEvent } from "../events"; +import { NDKEvent } from "../events"; import { NDKKind } from "../events/kinds"; import NDKList from "../events/kinds/lists"; import { NDKSubscriptionCacheUsage } from "../subscription"; diff --git a/ndk/src/utils/get-users-relay-list.ts b/ndk/src/utils/get-users-relay-list.ts index c08bca2f..719efa54 100644 --- a/ndk/src/utils/get-users-relay-list.ts +++ b/ndk/src/utils/get-users-relay-list.ts @@ -1,11 +1,11 @@ -import { NDKEvent } from "../events/index.js"; +import type { NDKEvent } from "../events/index.js"; import { NDKKind } from "../events/kinds/index.js"; import { NDKRelayList, relayListFromKind3 } from "../events/kinds/NDKRelayList.js"; -import { NDK } from "../ndk/index.js"; -import { NDKRelay } from "../relay/index.js"; +import type { NDK } from "../ndk/index.js"; +import type { NDKRelay } from "../relay/index.js"; import { NDKRelaySet } from "../relay/sets/index.js"; import { NDKSubscriptionCacheUsage } from "../subscription/index.js"; -import { Hexpubkey } from "../user/index.js"; +import type { Hexpubkey } from "../user/index.js"; export async function getRelayListForUser(pubkey: Hexpubkey, ndk: NDK): Promise { const list = await getRelayListForUsers([pubkey], ndk); diff --git a/ndk/src/zap/index.test.ts b/ndk/src/zap/index.test.ts deleted file mode 100644 index da8314b5..00000000 --- a/ndk/src/zap/index.test.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { NDKZap } from "."; -import { NDKEvent } from "../events/index.js"; -import { NDK } from "../ndk/index.js"; -import { Hexpubkey } from "../user"; -import { NDKRelayList } from "../events/kinds/NDKRelayList.js"; - -const ndk = new NDK(); -const user1 = ndk.getUser({ - npub: "npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft", -}); -const user2 = ndk.getUser({ - npub: "npub1hkmj8cfap65e7gjy3x3auky7mfegjxll9jfk2jed3w9gsnh9j5gsd24r62", -}); -const user3 = ndk.getUser({ - npub: "npub15z0mnjepscd9hk3ywkcvuupap7tt0qle64f0uvvygpl3mqerz4tq4dl33y", -}); - -const relays = [ - [ - "wss://user1-A", - "wss://user1-B", - "wss://user1-user2-A", - "wss://user1-user2-B", - "wss://user1-C", - "wss://user1-D", - "wss://user1-E", - "wss://user1-F", - "wss://user1-G", - "wss://user1-H", - "wss://user1-I", - "wss://user1-J", - "wss://user1-K", - "wss://user1-L", - "wss://user1-M", - "wss://user1-N", - "wss://user1-O", - "wss://user1-P", - "wss://user1-Q", - "wss://user1-R", - "wss://user1-S", - "wss://user1-T", - "wss://user1-U", - "wss://user1-V", - "wss://user1-W", - "wss://user1-X", - "wss://user1-Y", - "wss://user1-Z", - ], - ["wss://user2-A", "wss://user2-B", "wss://user1-user2-A", "wss://user1-user2-B"], -]; - -jest.mock("../utils/get-users-relay-list.js", () => ({ - getRelayListForUsers: jest.fn(() => { - const map = new Map(); - const e1 = new NDKRelayList(ndk) as any; - jest.spyOn(e1, "readRelayUrls", "get").mockReturnValue(relays[0]); - map.set(user1.npub, e1); - - const e2 = new NDKRelayList(ndk) as any; - jest.spyOn(e2, "readRelayUrls", "get").mockReturnValue(relays[1]); - map.set(user2.npub, e2); - - return map; - }), -})); - -afterAll(() => { - jest.clearAllMocks(); -}); - -describe("NDKZap", () => { - describe("relay", () => { - it("prefers relays that both sender and receiver have in common", async () => { - ndk.activeUser = user1; - const zap = new NDKZap({ - ndk, - zappedUser: user2, - }); - - const r = await zap.relays(); - - expect(r.slice(0, 2).includes("wss://user1-user2-A")).toBe(true); - expect(r.slice(0, 2).includes("wss://user1-user2-B")).toBe(true); - - expect(r.length).toBeLessThanOrEqual(3); - }); - - it("correctly uses sender relays when we don't have relays for the receiver", async () => { - ndk.activeUser = user1; - const zap = new NDKZap({ - ndk, - zappedUser: user3, - }); - - const r = await zap.relays(); - - for (const relay of r) { - expect(relays[0].includes(relay)).toBe(true); - } - - expect(r.length).toBeGreaterThanOrEqual(3); - }); - }); -}); diff --git a/ndk/src/zap/index.ts b/ndk/src/zap/index.ts deleted file mode 100644 index d1f0f26c..00000000 --- a/ndk/src/zap/index.ts +++ /dev/null @@ -1,307 +0,0 @@ -import { bech32 } from "@scure/base"; -import { EventEmitter } from "tseep"; -import { nip57 } from "nostr-tools"; - -import type { NostrEvent, NDKTag } from "../events/index.js"; -import { NDKEvent } from "../events/index.js"; -import type { NDK } from "../ndk/index.js"; -import type { Hexpubkey, NDKUser } from "../user/index.js"; -import { NDKSigner } from "../signers/index.js"; -import createDebug from "debug"; -import type { NDKUserProfile } from "../user/profile.js"; -import { getRelayListForUsers } from "../utils/get-users-relay-list.js"; - -const debug = createDebug("ndk:zap"); - -const DEFAULT_RELAYS = [ - "wss://nos.lol", - "wss://relay.nostr.band", - "wss://relay.f7z.io", - "wss://relay.damus.io", - "wss://nostr.mom", - "wss://no.str.cr", -]; - -export interface ZapConstructorParams { - ndk: NDK; - zappedEvent?: NDKEvent; - zappedUser?: NDKUser; - _fetch?: typeof fetch; -} - -export type NDKLUD18ServicePayerData = Partial<{ - name: { mandatory: boolean }; - pubkey: { mandatory: boolean }; - identifier: { mandatory: boolean }; - email: { mandatory: boolean }; - auth: { - mandatory: boolean; - k1: string; - }; -}> & - Record; - -export type NDKLnUrlData = { - tag: string; - callback: string; - minSendable: number; - maxSendable: number; - metadata: string; - payerData?: NDKLUD18ServicePayerData; - commentAllowed?: number; - - /** - * Pubkey of the zapper that should publish zap receipts for this user - */ - nostrPubkey?: Hexpubkey; - allowsNostr?: boolean; -}; - -export class NDKZap extends EventEmitter { - public ndk: NDK; - public zappedEvent?: NDKEvent; - public zappedUser: NDKUser; - private fetch: typeof fetch = fetch; - - /** - * The maximum number of relays to request the zapper to publish the zap receipt to. - */ - public maxRelays = 3; - - public constructor(args: ZapConstructorParams) { - super(); - this.ndk = args.ndk; - this.zappedEvent = args.zappedEvent; - this.fetch = args._fetch || fetch; - - this.zappedUser = args.zappedUser || this.ndk.getUser({ pubkey: this.zappedEvent?.pubkey }); - } - - /** - * Fetches the zapper's pubkey for the zapped user - */ - static async getZapperPubkey(ndk: NDK, forUser: Hexpubkey): Promise { - const zappedUser = ndk.getUser({ pubkey: forUser }); - const zap = new NDKZap({ ndk, zappedUser }); - const lnurlspec = await zap.getZapSpec(); - return lnurlspec?.nostrPubkey; - } - - public async getZapSpec(): Promise { - if (!this.zappedUser) throw new Error("No user to zap was provided"); - - return this.zappedUser.getZapConfiguration(this.ndk); - } - - public async getZapSpecWithoutCache(): Promise { - let lud06: string | undefined; - let lud16: string | undefined; - let zapEndpoint: string | undefined; - let profile: NDKUserProfile | undefined; - - if (this.zappedUser) { - // check if user has a profile, otherwise request it - if (!this.zappedUser.profile) { - await this.zappedUser.fetchProfile({ groupable: false }); - } - - profile = this.zappedUser.profile; - - lud06 = (this.zappedUser.profile || {}).lud06; - lud16 = (this.zappedUser.profile || {}).lud16; - } - - if (lud16 && !lud16.startsWith("LNURL")) { - const [name, domain] = lud16.split("@"); - zapEndpoint = `https://${domain}/.well-known/lnurlp/${name}`; - } else if (lud06) { - const { words } = bech32.decode(lud06, 1000); - const data = bech32.fromWords(words); - const utf8Decoder = new TextDecoder("utf-8"); - zapEndpoint = utf8Decoder.decode(data); - } - - if (!zapEndpoint) { - debug("No zap endpoint found", profile, { lud06, lud16 }); - throw new Error("No zap endpoint found"); - } - - try { - const _fetch = this.fetch || this.ndk.httpFetch; - const response = await _fetch(zapEndpoint); - - if (response.status !== 200) { - const text = await response.text(); - throw new Error(`Unable to fetch zap endpoint ${zapEndpoint}: ${text}`); - } - - return await response.json(); - } catch (e) { - throw new Error(`Unable to fetch zap endpoint ${zapEndpoint}: ${e}`); - } - } - - public async getZapEndpoint(): Promise { - const zapSpec = await this.getZapSpec(); - if (!zapSpec) return; - - let zapEndpointCallback: string | undefined; - - if (zapSpec?.allowsNostr && (zapSpec?.nostrPubkey || zapSpec?.nostrPubkey)) { - zapEndpointCallback = zapSpec.callback; - } - - return zapEndpointCallback; - } - - /** - * Generates a kind:9734 zap request and returns the payment request - * @param amount amount to zap in millisatoshis - * @param comment optional comment to include in the zap request - * @param extraTags optional extra tags to include in the zap request - * @param relays optional relays to ask zapper to publish the zap to - * @returns the payment request - */ - public async createZapRequest( - amount: number, // amount to zap in millisatoshis - comment?: string, - extraTags?: NDKTag[], - relays?: string[], - signer?: NDKSigner - ): Promise { - const res = await this.generateZapRequest(amount, comment, extraTags, relays); - if (!res) return null; - const { event, zapEndpoint } = res; - - if (!event) { - throw new Error("No zap request event found"); - } - - await event.sign(signer); - - let invoice: string | null; - - try { - debug(`Getting invoice for zap request: ${zapEndpoint}`); - invoice = await this.getInvoice(event, amount, zapEndpoint); - } catch (e) { - throw new Error("Failed to get invoice: " + e); - } - - return invoice; - } - - public async getInvoice( - event: NDKEvent, - amount: number, - zapEndpoint: string - ): Promise { - debug( - `Fetching invoice from ${zapEndpoint}?` + - new URLSearchParams({ - amount: amount.toString(), - nostr: encodeURIComponent(JSON.stringify(event.rawEvent())), - }) - ); - const url = new URL(zapEndpoint); - url.searchParams.append("amount", amount.toString()); - url.searchParams.append("nostr", JSON.stringify(event.rawEvent())); - debug(`Fetching invoice from ${url.toString()}`); - const response = await fetch(url.toString()); - debug(`Got response from zap endpoint: ${zapEndpoint}`, { status: response.status }); - if (response.status !== 200) { - debug(`Received non-200 status from zap endpoint: ${zapEndpoint}`, { - status: response.status, - amount: amount, - nostr: JSON.stringify(event.rawEvent()), - }); - const text = await response.text(); - throw new Error(`Unable to fetch zap endpoint ${zapEndpoint}: ${text}`); - } - const body = await response.json(); - - return body.pr; - } - - public async generateZapRequest( - amount: number, // amount to zap in millisatoshis - comment?: string, - extraTags?: NDKTag[], - relays?: string[], - signer?: NDKSigner - ): Promise<{ event: NDKEvent; zapEndpoint: string } | null> { - const zapEndpoint = await this.getZapEndpoint(); - - if (!zapEndpoint) { - throw new Error("No zap endpoint found"); - } - - if (!this.zappedEvent && !this.zappedUser) throw new Error("No zapped event or user found"); - - const zapRequest = nip57.makeZapRequest({ - profile: this.zappedUser.pubkey, - - // set the event to null since nostr-tools doesn't support nip-33 zaps - event: null, - amount, - comment: comment || "", - relays: relays ?? (await this.relays()), - }); - - // add the event tag if it exists; this supports both 'e' and 'a' tags - if (this.zappedEvent) { - const tags = this.zappedEvent.referenceTags(); - const nonPTags = tags.filter((tag) => tag[0] !== "p"); - zapRequest.tags.push(...nonPTags); - } - - zapRequest.tags.push(["lnurl", zapEndpoint]); - - const event = new NDKEvent(this.ndk, zapRequest as NostrEvent); - if (extraTags) { - event.tags = event.tags.concat(extraTags); - } - - return { event, zapEndpoint }; - } - - /** - * @returns the relays to use for the zap request - */ - public async relays(): Promise { - let r: string[] = []; - - if (this.ndk?.activeUser) { - const relayLists = await getRelayListForUsers( - [this.ndk.activeUser.pubkey, this.zappedUser.pubkey], - this.ndk - ); - - const relayScores = new Map(); - - // go through the relay lists and try to get relays that are shared between the two users - for (const relayList of relayLists.values()) { - for (const url of relayList.readRelayUrls) { - const score = relayScores.get(url) || 0; - relayScores.set(url, score + 1); - } - } - - // get the relays that are shared between the two users - r = Array.from(relayScores.entries()) - .sort((a, b) => b[1] - a[1]) - .map(([url]) => url) - .slice(0, this.maxRelays); - } - - if (this.ndk?.pool?.permanentAndConnectedRelays().length) { - r = this.ndk.pool.permanentAndConnectedRelays().map((relay) => relay.url); - } - - if (!r.length) { - r = DEFAULT_RELAYS; - } - - return r; - } -} diff --git a/ndk/src/zapper/index.test.ts b/ndk/src/zapper/index.test.ts new file mode 100644 index 00000000..d64ff506 --- /dev/null +++ b/ndk/src/zapper/index.test.ts @@ -0,0 +1,120 @@ +import type { NostrEvent } from "nostr-tools"; +import { NDKZapper } from "."; +import { NDKEvent } from "../events"; +import { NDKCashuMintList } from "../events/kinds/nutzap/mint-list"; +import { NDK } from "../ndk"; +import { NDKPrivateKeySigner } from "../signers/private-key"; +import type { NDKUser } from "../user"; + +jest.mock("./ln.js", () => ({ + getNip57ZapSpecFromLud: jest.fn(async () => { + return { + status: "OK", + tag: "payRequest", + callback: "https://primal.net/lnurlp/pablof7z/callback", + metadata: '[["text/plain","sats for pablof7z@primal.net"]]', + minSendable: 1000, + maxSendable: 11000000000, + nostrPubkey: "f81611363554b64306467234d7396ec88455707633f54738f6c4683535098cd3", + allowsNostr: true, + commentAllowed: 200, + }; + }), +})); + +const ndk = new NDK(); + +describe("NDKZapper", () => { + describe("getZapSplits", () => { + const event = new NDKEvent(); + event.ndk = ndk; + + it("uses the author pubkey when the target is the user", () => { + const user = ndk.getUser({ + pubkey: "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52", + }); + const splits = new NDKZapper(user, 1000).getZapSplits(); + expect(splits).toEqual([ + { + pubkey: "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52", + amount: 1000, + }, + ]); + }); + + it("uses the author pubkey when there are no splits", () => { + event.pubkey = "author-pubkey"; + + const zapper = new NDKZapper(event, 1000); + const splits = zapper.getZapSplits(); + expect(splits).toEqual([ + { + pubkey: "author-pubkey", + amount: 1000, + }, + ]); + }); + + it("properly calculates splits", () => { + event.tags = [ + ["zap", "pubkey1", "1"], + ["zap", "pubkey2", "2"], // pubkey2 gets double + ]; + + const zapper = new NDKZapper(event, 1000); + const splits = zapper.getZapSplits(); + expect(splits).toEqual([ + { pubkey: "pubkey1", amount: 333 }, + { pubkey: "pubkey2", amount: 666 }, + ]); + }); + }); +}); + +describe("getZapMethod", () => { + let signer: NDKPrivateKeySigner; + let user: NDKUser; + + beforeAll(async () => { + signer = NDKPrivateKeySigner.generate(); + user = await signer.user(); + user.ndk = ndk; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it("identifies when the user has signaled nutzaps", async () => { + const mintList = new NDKCashuMintList(); + mintList.mints = ["https://mint1", "https://mint2"]; + await mintList.sign(signer); + ndk.fetchEvents = jest.fn().mockResolvedValue(new Set([mintList])); + const zapper = new NDKZapper(user, 1000); + zapper.onNutPay = async (payment: NDKZapPaymentDetails): Promise => false; + const zapMethod = await zapper.getZapMethod(ndk, user.pubkey); + expect(zapMethod.type).toBe("nip61"); + expect((zapMethod.data as NutPaymentInfo).mints).toEqual([ + "https://mint1", + "https://mint2", + ]); + }); + + it("defaults to nip57 when the user has not signaled nutzaps", async () => { + const profile = new NDKEvent(ndk, { + content: JSON.stringify({ lud16: "pablo@primal.net" }), + kind: 0, + } as NostrEvent); + ndk.fetchEvents = jest.fn().mockResolvedValue(new Set([profile])); + const zapper = new NDKZapper(user, 1000); + zapper.onNutPay = async (payment: NDKZapPaymentDetails): Promise => false; + zapper.onLnPay = async (payment: NDKZapPaymentDetails): Promise => + false; + const zapMethod = await zapper.getZapMethod( + ndk, + "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52" + ); + + expect(zapMethod.type).toBe("nip57"); + }); +}); diff --git a/ndk/src/zapper/index.ts b/ndk/src/zapper/index.ts new file mode 100644 index 00000000..0f1c45f2 --- /dev/null +++ b/ndk/src/zapper/index.ts @@ -0,0 +1,427 @@ +import type { NDK } from "../ndk"; +import type { NDKTag } from "../events"; +import { NDKEvent } from "../events"; +import type { Hexpubkey } from "../user"; +import { NDKUser } from "../user"; +import type { NDKSigner } from "../signers"; +import createDebug from "debug"; + +import { getRelayListForUsers } from "../utils/get-users-relay-list"; +import { EventEmitter } from "tseep"; +import { generateZapRequest } from "./nip57"; +import { NDKNutzap } from "../events/kinds/nutzap"; +import { LnPaymentInfo, NDKLnUrlData, NDKPaymentConfirmationLN, NDKZapConfirmationLN } from "./ln"; +import { NDKZapConfirmationCashu, CashuPaymentInfo, NDKPaymentConfirmationCashu } from "./nip61"; +import { NDKRelaySet } from "../relay/sets"; + +const d = createDebug("ndk:zapper"); + +export type NDKZapDetails = T & { + /** + * Target of the zap + */ + target: NDKEvent | NDKUser; + + /** + * Comment for the zap + */ + comment?: string; + + /** + * Tags to add to the zap + */ + tags?: NDKTag[]; + + /** + * Pubkey of the user to zap to + */ + recipientPubkey: string; + + /** + * Amount of the payment + */ + amount: number; + + /** + * Unit of the payment (e.g. msat) + */ + unit: string; +}; + +export type NDKZapConfirmation = NDKZapConfirmationLN | NDKZapConfirmationCashu; + +export type NDKPaymentConfirmation = NDKPaymentConfirmationLN | NDKPaymentConfirmationCashu; + +export type NDKZapSplit = { + pubkey: string; + amount: number; +}; + +export type NDKZapMethod = "nip57" | "nip61"; + +type ZapMethodInfo = { + nip57: NDKLnUrlData; + nip61: CashuPaymentInfo; +}; + +export type NDKZapMethodInfo = { + type: NDKZapMethod; + + data: ZapMethodInfo[NDKZapMethod]; +}; + +export type LnPayCb = ( + payment: NDKZapDetails +) => Promise; +export type CashuPayCb = ( + payment: NDKZapDetails +) => Promise; + +/** + * + */ +class NDKZapper extends EventEmitter<{ + /** + * Emitted when a zap split has been completed + */ + "split:complete": ( + split: NDKZapSplit, + info: NDKPaymentConfirmation | Error | undefined + ) => void; + + complete: (results: Map) => void; +}> { + public target: NDKEvent | NDKUser; + public ndk: NDK; + public comment?: string; + public amount: number; + public unit: string; + public tags?: NDKTag[]; + public signer?: NDKSigner; + public zapMethod?: NDKZapMethod; + + public onLnPay?: LnPayCb; + + /** + * Called when a cashu payment is to be made. + * This function should swap/mint proofs for the required amount, in the required unit, + * in any of the provided mints and return the proofs and mint used. + */ + public onCashuPay?: CashuPayCb; + public onComplete?: (results: Map) => void; + + public maxRelays = 3; + + constructor( + target: NDKEvent | NDKUser, + amount: number, + unit: string = "msat", + comment?: string, + ndk?: NDK, + tags?: NDKTag[], + signer?: NDKSigner + ) { + super(); + this.target = target; + this.ndk = ndk || target.ndk!; + if (!this.ndk) { + throw new Error("No NDK instance provided"); + } + + this.amount = amount; + this.comment = comment; + this.unit = unit; + this.tags = tags; + this.signer = signer; + } + + /** + * Initiate zapping process + */ + async zap() { + // get all splits + const splits = this.getZapSplits(); + const results = new Map(); + + await Promise.all( + splits.map(async (split) => { + let result: NDKPaymentConfirmation | Error | undefined; + + try { + result = await this.zapSplit(split); + } catch (e: any) { + result = e; + } + + this.emit("split:complete", split, result); + results.set(split, result); + }) + ); + + this.emit("complete", results); + if (this.onComplete) this.onComplete(results); + + return results; + } + + async zapSplit(split: NDKZapSplit): Promise { + let zapped = false; + let zapMethods = await this.getZapMethods(this.ndk, split.pubkey); + let retVal: NDKPaymentConfirmation | Error | undefined; + + if (zapMethods.length === 0) throw new Error("No zap method available for recipient"); + + // prefer nip61 if available + zapMethods = zapMethods.sort((a, b) => { + if (a.type === "nip61") return -1; + if (b.type === "nip61") return 1; + return 0; + }); + + const relays = await this.relays(split.pubkey); + + for (const zapMethod of zapMethods) { + if (zapped) break; + + d( + "Zapping to %s with %d %s using %s", + split.pubkey, + split.amount, + this.unit, + zapMethod.type + ); + + try { + switch (zapMethod.type) { + case "nip61": { + const data = zapMethod.data as CashuPaymentInfo; + let ret: NDKPaymentConfirmationCashu | Error; + ret = await this.onCashuPay!({ + target: this.target, + comment: this.comment, + tags: this.tags, + recipientPubkey: split.pubkey, + amount: split.amount, + unit: this.unit, + ...data, + }); + + d("NIP-61 Zap result: %o", ret); + + if (ret instanceof Error) { + // we assign the error instead of throwing it so that we can try the next zap method + // but we want to keep the error around in case there is no successful zap + retVal = ret; + } else if (ret) { + const { proofs, mint } = ret as NDKZapConfirmationCashu; + + if (!proofs || !mint) + throw new Error( + "Invalid zap confirmation: missing proofs or mint: " + ret + ); + + const relaySet = NDKRelaySet.fromRelayUrls(relays, this.ndk); + + // we have a confirmation, generate the nutzap + const nutzap = new NDKNutzap(this.ndk); + nutzap.tags = [ ...nutzap.tags, ...(this.tags || []) ]; + nutzap.proofs = proofs; + nutzap.mint = mint; + nutzap.comment = this.comment; + nutzap.unit = this.unit; + nutzap.recipientPubkey = split.pubkey; + await nutzap.sign(this.signer); + await nutzap.publish(relaySet); + + // mark that we have zapped + return nutzap; + } + + break; + } + case "nip57": { + const lnUrlData = zapMethod.data as NDKLnUrlData; + + const zapRequest = await generateZapRequest( + this.target, + this.ndk, + lnUrlData, + split.pubkey, + split.amount, + relays, + this.comment, + this.tags, + this.signer + ); + + if (!zapRequest) { + d("Unable to generate zap request"); + throw new Error("Unable to generate zap request"); + } + + const pr = await this.getLnInvoice(zapRequest, split.amount, lnUrlData); + + if (!pr) { + d("Unable to get payment request"); + throw new Error("Unable to get payment request"); + } + + retVal = await this.onLnPay!({ + target: this.target, + comment: this.comment, + recipientPubkey: split.pubkey, + amount: split.amount, + unit: this.unit, + pr, + }); + + break; + } + } + } 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); + } + } + + if (retVal instanceof Error) throw retVal; + + return retVal; + } + + /** + * Gets a bolt11 for a nip57 zap + * @param event + * @param amount + * @param zapEndpoint + * @returns + */ + public async getLnInvoice( + zapRequest: NDKEvent, + amount: number, + data: NDKLnUrlData + ): Promise { + const zapEndpoint = data.callback; + const eventPayload = JSON.stringify(zapRequest.rawEvent()); + d( + `Fetching invoice from ${zapEndpoint}?` + + new URLSearchParams({ + amount: amount.toString(), + nostr: eventPayload, + }) + ); + const url = new URL(zapEndpoint); + url.searchParams.append("amount", amount.toString()); + url.searchParams.append("nostr", eventPayload); + d(`Fetching invoice from ${url.toString()}`); + const response = await fetch(url.toString()); + d(`Got response from zap endpoint: ${zapEndpoint}`, { status: response.status }); + if (response.status !== 200) { + d(`Received non-200 status from zap endpoint: ${zapEndpoint}`, { + status: response.status, + amount: amount, + nostr: eventPayload, + }); + const text = await response.text(); + throw new Error(`Unable to fetch zap endpoint ${zapEndpoint}: ${text}`); + } + const body = await response.json(); + + return body.pr; + } + + public getZapSplits(): NDKZapSplit[] { + if (this.target instanceof NDKUser) { + return [ + { + pubkey: this.target.pubkey, + amount: this.amount, + }, + ]; + } + + const zapTags = this.target.getMatchingTags("zap"); + if (zapTags.length === 0) { + return [ + { + pubkey: this.target.pubkey, + amount: this.amount, + }, + ]; + } + + const splits: NDKZapSplit[] = []; + const total = zapTags.reduce((acc, tag) => acc + parseInt(tag[2]), 0); + + for (const tag of zapTags) { + const pubkey = tag[1]; + const amount = Math.floor((parseInt(tag[2]) / total) * this.amount); + splits.push({ pubkey, amount }); + } + + return splits; + } + + /** + * Gets the zap method that should be used to zap a pubbkey + * @param ndk + * @param pubkey + * @returns + */ + async getZapMethods(ndk: NDK, recipient: Hexpubkey): Promise { + const methods: NDKZapMethod[] = []; + + if (this.onCashuPay) methods.push("nip61"); + methods.push("nip57"); // we always support nip57 + + const user = ndk.getUser({ pubkey: recipient }); + const zapInfo = await user.getZapInfo(false, methods); + + d("Zap info for %s: %o", user.npub, zapInfo); + + return zapInfo; + } + + /** + * @returns the relays to use for the zap request + */ + public async relays(pubkey: Hexpubkey): Promise { + let r: string[] = []; + + if (this.ndk?.activeUser) { + const relayLists = await getRelayListForUsers( + [this.ndk.activeUser.pubkey, pubkey], + this.ndk + ); + + const relayScores = new Map(); + + // go through the relay lists and try to get relays that are shared between the two users + for (const relayList of relayLists.values()) { + for (const url of relayList.readRelayUrls) { + const score = relayScores.get(url) || 0; + relayScores.set(url, score + 1); + } + } + + // get the relays that are shared between the two users + r = Array.from(relayScores.entries()) + .sort((a, b) => b[1] - a[1]) + .map(([url]) => url) + .slice(0, this.maxRelays); + } + + if (this.ndk?.pool?.permanentAndConnectedRelays().length) { + r = this.ndk.pool.permanentAndConnectedRelays().map((relay) => relay.url); + } + + if (!r.length) { + r = []; + } + + return r; + } +} + +export { NDKZapper }; diff --git a/ndk/src/zapper/ln.ts b/ndk/src/zapper/ln.ts new file mode 100644 index 00000000..1ca97e96 --- /dev/null +++ b/ndk/src/zapper/ln.ts @@ -0,0 +1,84 @@ +import { bech32 } from "@scure/base"; +import type { NDK } from "../ndk"; +import createDebug from "debug"; +import { Hexpubkey } from "../user"; + +const d = createDebug("ndk:zapper:ln"); + +export type NDKZapConfirmationLN = { + preimage: string; +}; + +export type NDKPaymentConfirmationLN = { + preimage: string; +}; + +export type LNPaymentRequest = string; + +export type LnPaymentInfo = { + pr: LNPaymentRequest; +}; + +export type NDKLUD18ServicePayerData = Partial<{ + name: { mandatory: boolean }; + pubkey: { mandatory: boolean }; + identifier: { mandatory: boolean }; + email: { mandatory: boolean }; + auth: { + mandatory: boolean; + k1: string; + }; +}> & + Record; + +export type NDKLnUrlData = { + tag: string; + callback: string; + minSendable: number; + maxSendable: number; + metadata: string; + payerData?: NDKLUD18ServicePayerData; + commentAllowed?: number; + + /** + * Pubkey of the zapper that should publish zap receipts for this user + */ + nostrPubkey?: Hexpubkey; + allowsNostr?: boolean; +}; + +export async function getNip57ZapSpecFromLud( + { lud06, lud16 }: { lud06?: string; lud16?: string }, + ndk: NDK +): Promise { + let zapEndpoint: string | undefined; + + if (lud16 && !lud16.startsWith("LNURL")) { + const [name, domain] = lud16.split("@"); + zapEndpoint = `https://${domain}/.well-known/lnurlp/${name}`; + } else if (lud06) { + const { words } = bech32.decode(lud06, 1000); + const data = bech32.fromWords(words); + const utf8Decoder = new TextDecoder("utf-8"); + zapEndpoint = utf8Decoder.decode(data); + } + + if (!zapEndpoint) { + d("No zap endpoint found %o", { lud06, lud16 }); + throw new Error("No zap endpoint found"); + } + + try { + const _fetch = ndk.httpFetch || fetch; + const response = await _fetch(zapEndpoint); + + if (response.status !== 200) { + const text = await response.text(); + throw new Error(`Unable to fetch zap endpoint ${zapEndpoint}: ${text}`); + } + + return await response.json(); + } catch (e) { + throw new Error(`Unable to fetch zap endpoint ${zapEndpoint}: ${e}`); + } +} diff --git a/ndk/src/zapper/nip57.ts b/ndk/src/zapper/nip57.ts new file mode 100644 index 00000000..1005f73a --- /dev/null +++ b/ndk/src/zapper/nip57.ts @@ -0,0 +1,51 @@ +import { nip57, NostrEvent } from "nostr-tools"; +import { NDKTag, NDKEvent } from "../events"; +import { NDKSigner } from "../signers"; +import { NDKUser } from "../user"; +import { NDK } from "../ndk"; +import { NDKLnUrlData } from "./ln.js"; + +export async function generateZapRequest( + target: NDKEvent | NDKUser, + ndk: NDK, + data: NDKLnUrlData, + pubkey: string, + amount: number, // amount to zap in millisatoshis + relays: string[], + comment?: string, + tags?: NDKTag[], + signer?: NDKSigner +): Promise { + const zapEndpoint = data.callback; + const zapRequest = nip57.makeZapRequest({ + profile: pubkey, + + // set the event to null since nostr-tools doesn't support nip-33 zaps + event: null, + amount, + comment: comment || "", + relays: relays.slice(0, 4), + }); + + // add the event tag if it exists; this supports both 'e' and 'a' tags + if (target instanceof NDKEvent) { + const tags = target.referenceTags(); + const nonPTags = tags.filter((tag) => tag[0] !== "p"); + zapRequest.tags.push(...nonPTags); + } + + zapRequest.tags.push(["lnurl", zapEndpoint]); + + const event = new NDKEvent(ndk, zapRequest as NostrEvent); + if (tags) { + event.tags = event.tags.concat(tags); + } + + // make sure we only have one `p` tag + event.tags = event.tags.filter((tag) => tag[0] !== "p"); + event.tags.push(["p", pubkey]); + + await event.sign(signer); + + return event; +} diff --git a/ndk/src/zapper/nip61.ts b/ndk/src/zapper/nip61.ts new file mode 100644 index 00000000..31115fc9 --- /dev/null +++ b/ndk/src/zapper/nip61.ts @@ -0,0 +1,43 @@ +import { NDKNutzap } from "../events/kinds/nutzap"; +import { Proof } from "../events/kinds/nutzap/proof"; + +/** + * Provides information that should be used to send a NIP-61 nutzap. + * mints: URLs of the mints that can be used. + * relays: URLs of the relays where nutzap must be published + * p2pk: Optional pubkey to use for P2PK lock + */ +export type CashuPaymentInfo = { + /** + * Mints that must be used for the payment + */ + mints: string[]; + + /** + * Relays where nutzap must be published + */ + relays: string[]; + + /** + * Optional pubkey to use for P2PK lock + */ + p2pk?: string; +}; + +export type NDKZapConfirmationCashu = NDKNutzap; + +/** + * This is what a wallet implementing Cashu payments should provide back + * when a payment has been requested. + */ +export type NDKPaymentConfirmationCashu = { + /** + * Proof of the payment + */ + proofs: Proof[]; + + /** + * Mint + */ + mint: string; +}; diff --git a/package.json b/package.json index ca1061be..3432d4ca 100755 --- a/package.json +++ b/package.json @@ -14,19 +14,20 @@ "docs:preview": "vitepress preview" }, "devDependencies": { + "@braintree/sanitize-url": "^7.1.0", "@changesets/cli": "^2.22.0", "@nostr-dev-kit/eslint-config-custom": "workspace:*", "@nostr-dev-kit/tsconfig": "workspace:*", "eslint": "^8.49.0", + "mermaid": "^10.9.1", "prettier": "^3.0.3", "turbo": "^1.10.14", - "typescript": "^5.2.2" + "typescript": "^5.5.4", + "vitepress": "^1.2.3", + "vitepress-plugin-mermaid": "^2.0.16" }, "packageManager": "pnpm@8.15.6", "engines": { "node": ">=16.0" - }, - "dependencies": { - "vitepress": "^1.2.3" } } \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e956c21f..105bb405 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,11 +7,10 @@ settings: importers: .: - dependencies: - vitepress: - specifier: ^1.2.3 - version: 1.2.3(@algolia/client-search@4.24.0)(search-insights@2.14.0)(typescript@5.2.2) devDependencies: + '@braintree/sanitize-url': + specifier: ^7.1.0 + version: 7.1.0 '@changesets/cli': specifier: ^2.22.0 version: 2.26.2 @@ -24,6 +23,9 @@ importers: eslint: specifier: ^8.49.0 version: 8.50.0 + mermaid: + specifier: ^10.9.1 + version: 10.9.1 prettier: specifier: ^3.0.3 version: 3.0.3 @@ -31,8 +33,14 @@ importers: specifier: ^1.10.14 version: 1.10.14 typescript: - specifier: ^5.2.2 - version: 5.2.2 + specifier: ^5.5.4 + version: 5.5.4 + vitepress: + specifier: ^1.2.3 + version: 1.2.3(@algolia/client-search@4.24.0)(search-insights@2.15.0)(typescript@5.5.4) + vitepress-plugin-mermaid: + specifier: ^2.0.16 + version: 2.0.16(mermaid@10.9.1)(vitepress@1.2.3) demos: dependencies: @@ -105,7 +113,7 @@ importers: version: 29.7.0(@types/node@14.18.63)(ts-node@10.9.1) ts-jest: specifier: ^29.1.0 - version: 29.1.1(@babel/core@7.24.5)(esbuild@0.17.19)(jest@29.7.0)(typescript@5.3.3) + version: 29.1.1(@babel/core@7.24.9)(esbuild@0.17.19)(jest@29.7.0)(typescript@5.3.3) ts-node: specifier: ^10.9.1 version: 10.9.1(@types/node@14.18.63)(typescript@5.3.3) @@ -140,8 +148,8 @@ importers: specifier: ^3.3.1 version: 3.3.2 nostr-tools: - specifier: ^2.5.2 - version: 2.5.2(typescript@5.2.2) + specifier: ^2.7.1 + version: 2.7.1(typescript@5.2.2) tseep: specifier: ^1.1.1 version: 1.1.1 @@ -187,7 +195,7 @@ importers: version: 29.7.0(@types/node@14.18.63)(ts-node@10.9.1) ts-jest: specifier: ^29.1.0 - version: 29.1.1(@babel/core@7.24.7)(esbuild@0.17.19)(jest@29.7.0)(typescript@5.2.2) + version: 29.1.1(@babel/core@7.25.2)(esbuild@0.17.19)(jest@29.7.0)(typescript@5.2.2) ts-node: specifier: ^10.9.1 version: 10.9.1(@types/node@14.18.63)(typescript@5.2.2) @@ -217,7 +225,7 @@ importers: version: 4.0.2 nostr-tools: specifier: ^2.4.0 - version: 2.4.0(typescript@5.5.2) + version: 2.4.0(typescript@5.5.4) typescript-lru-cache: specifier: ^2.0.0 version: 2.0.0 @@ -248,13 +256,13 @@ importers: version: 3.0.3 ts-jest: specifier: ^29.1.5 - version: 29.1.5(@babel/core@7.24.7)(esbuild@0.18.20)(jest@29.7.0)(typescript@5.5.2) + version: 29.1.5(@babel/core@7.25.2)(esbuild@0.18.20)(jest@29.7.0)(typescript@5.5.4) tsup: specifier: ^7.2.0 - version: 7.2.0(typescript@5.5.2) + version: 7.2.0(typescript@5.5.4) typedoc: specifier: ^0.26.3 - version: 0.26.3(typescript@5.5.2) + version: 0.26.3(typescript@5.5.4) typedoc-plugin-markdown: specifier: ^4.1.1 version: 4.1.1(typedoc@0.26.3) @@ -294,7 +302,7 @@ importers: version: 29.7.0(@types/node@18.17.19)(ts-node@10.9.2) ts-jest: specifier: ^29.1.2 - version: 29.1.5(@babel/core@7.24.7)(esbuild@0.18.20)(jest@29.7.0)(typescript@5.4.5) + version: 29.1.5(@babel/core@7.25.2)(esbuild@0.18.20)(jest@29.7.0)(typescript@5.4.5) ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@18.17.19)(typescript@5.4.5) @@ -340,7 +348,7 @@ importers: version: 29.7.0(@types/node@18.17.19)(ts-node@10.9.2) ts-jest: specifier: ^29.1.2 - version: 29.1.2(@babel/core@7.24.7)(esbuild@0.18.20)(jest@29.7.0)(typescript@5.4.4) + version: 29.1.2(@babel/core@7.25.2)(esbuild@0.18.20)(jest@29.7.0)(typescript@5.4.4) ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@18.17.19)(typescript@5.4.4) @@ -365,7 +373,7 @@ importers: version: link:../packages/tsconfig tsup: specifier: ^7.2.0 - version: 7.2.0(typescript@5.2.2) + version: 7.2.0(typescript@5.5.4) ndk-svelte-components: dependencies: @@ -395,7 +403,7 @@ importers: version: 1.1.4(marked@9.0.3) nostr-tools: specifier: ^2.4.0 - version: 2.4.0(typescript@5.5.2) + version: 2.4.0(typescript@5.5.4) ramda: specifier: ^0.29.0 version: 0.29.0 @@ -408,12 +416,9 @@ importers: sanitize-html: specifier: ^2.11.0 version: 2.11.0 - svelte-asciidoc: - specifier: ^0.0.2 - version: 0.0.2(svelte@4.2.17) svelte-preprocess: specifier: ^5.0.4 - version: 5.0.4(@babel/core@7.22.20)(postcss@8.4.30)(svelte@4.2.17)(typescript@5.5.2) + version: 5.0.4(@babel/core@7.22.20)(postcss@8.4.30)(svelte@4.2.17)(typescript@5.5.4) svelte-time: specifier: ^0.8.3 version: 0.8.3 @@ -459,7 +464,7 @@ importers: version: 7.4.4(svelte@4.2.17) '@storybook/sveltekit': specifier: ^7.4.0 - version: 7.4.4(svelte@4.2.17)(typescript@5.5.2)(vite@4.5.3) + version: 7.4.4(svelte@4.2.17)(typescript@5.5.4)(vite@4.5.3) '@storybook/testing-library': specifier: ^0.2.0 version: 0.2.1 @@ -474,7 +479,7 @@ importers: version: 2.5.10(@sveltejs/vite-plugin-svelte@3.1.0)(svelte@4.2.17)(vite@4.5.3) '@sveltejs/package': specifier: ^2.2.2 - version: 2.2.2(svelte@4.2.17)(typescript@5.5.2) + version: 2.2.2(svelte@4.2.17)(typescript@5.5.4) '@types/ramda': specifier: ^0.29.3 version: 0.29.9 @@ -483,16 +488,16 @@ importers: version: 2.9.0 '@typescript-eslint/eslint-plugin': specifier: ^6.6.0 - version: 6.7.2(@typescript-eslint/parser@6.7.2)(eslint@8.57.0)(typescript@5.5.2) + version: 6.7.2(@typescript-eslint/parser@6.7.2)(eslint@8.57.0)(typescript@5.5.4) '@typescript-eslint/parser': specifier: ^6.6.0 - version: 6.7.2(eslint@8.57.0)(typescript@5.5.2) + version: 6.7.2(eslint@8.57.0)(typescript@5.5.4) autoprefixer: specifier: ^10.4.15 version: 10.4.16(postcss@8.4.30) eslint-plugin-storybook: specifier: 0.6.14 - version: 0.6.14(eslint@8.57.0)(typescript@5.5.2) + version: 0.6.14(eslint@8.57.0)(typescript@5.5.4) mdsvex: specifier: ^0.11.0 version: 0.11.0(svelte@4.2.17) @@ -533,6 +538,55 @@ importers: specifier: ^4.4.9 version: 4.5.3 + ndk-wallet: + dependencies: + '@cashu/cashu-ts': + specifier: 1.0.0-rc.9 + version: 1.0.0-rc.9 + '@nostr-dev-kit/ndk': + specifier: workspace:* + version: link:../ndk + debug: + specifier: ^4.3.4 + version: 4.3.5 + light-bolt11-decoder: + specifier: ^3.0.0 + version: 3.0.0 + tseep: + specifier: ^1.1.1 + version: 1.1.1 + typescript: + specifier: ^5.4.4 + version: 5.5.3 + devDependencies: + '@nostr-dev-kit/eslint-config-custom': + specifier: workspace:* + version: link:../packages/eslint-config-custom + '@nostr-dev-kit/tsconfig': + specifier: workspace:* + version: link:../packages/tsconfig + '@types/debug': + specifier: ^4.1.7 + version: 4.1.9 + '@types/jest': + specifier: ^29.5.5 + version: 29.5.5 + '@types/node': + specifier: ^18.15.11 + version: 18.17.19 + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@18.17.19)(ts-node@10.9.2) + ts-jest: + specifier: ^29.1.2 + version: 29.1.5(@babel/core@7.25.2)(esbuild@0.18.20)(jest@29.7.0)(typescript@5.5.3) + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@types/node@18.17.19)(typescript@5.5.3) + tsup: + specifier: ^7.2.0 + version: 7.2.0(ts-node@10.9.2)(typescript@5.5.3) + packages/eslint-config-custom: dependencies: '@typescript-eslint/eslint-plugin': @@ -575,28 +629,28 @@ packages: engines: {node: '>=0.10.0'} dev: true - /@algolia/autocomplete-core@1.9.3(@algolia/client-search@4.24.0)(algoliasearch@4.23.3)(search-insights@2.14.0): + /@algolia/autocomplete-core@1.9.3(@algolia/client-search@4.24.0)(algoliasearch@4.23.3)(search-insights@2.15.0): resolution: {integrity: sha512-009HdfugtGCdC4JdXUbVJClA0q0zh24yyePn+KUGk3rP7j8FEe/m5Yo/z65gn6nP/cM39PxpzqKrL7A6fP6PPw==} dependencies: - '@algolia/autocomplete-plugin-algolia-insights': 1.9.3(@algolia/client-search@4.24.0)(algoliasearch@4.23.3)(search-insights@2.14.0) + '@algolia/autocomplete-plugin-algolia-insights': 1.9.3(@algolia/client-search@4.24.0)(algoliasearch@4.23.3)(search-insights@2.15.0) '@algolia/autocomplete-shared': 1.9.3(@algolia/client-search@4.24.0)(algoliasearch@4.23.3) transitivePeerDependencies: - '@algolia/client-search' - algoliasearch - search-insights - dev: false + dev: true - /@algolia/autocomplete-plugin-algolia-insights@1.9.3(@algolia/client-search@4.24.0)(algoliasearch@4.23.3)(search-insights@2.14.0): + /@algolia/autocomplete-plugin-algolia-insights@1.9.3(@algolia/client-search@4.24.0)(algoliasearch@4.23.3)(search-insights@2.15.0): resolution: {integrity: sha512-a/yTUkcO/Vyy+JffmAnTWbr4/90cLzw+CC3bRbhnULr/EM0fGNvM13oQQ14f2moLMcVDyAx/leczLlAOovhSZg==} peerDependencies: search-insights: '>= 1 < 3' dependencies: '@algolia/autocomplete-shared': 1.9.3(@algolia/client-search@4.24.0)(algoliasearch@4.23.3) - search-insights: 2.14.0 + search-insights: 2.15.0 transitivePeerDependencies: - '@algolia/client-search' - algoliasearch - dev: false + dev: true /@algolia/autocomplete-preset-algolia@1.9.3(@algolia/client-search@4.24.0)(algoliasearch@4.23.3): resolution: {integrity: sha512-d4qlt6YmrLMYy95n5TB52wtNDr6EgAIPH81dvvvW8UmuWRgxEtY0NJiPwl/h95JtG2vmRM804M0DSwMCNZlzRA==} @@ -607,7 +661,7 @@ packages: '@algolia/autocomplete-shared': 1.9.3(@algolia/client-search@4.24.0)(algoliasearch@4.23.3) '@algolia/client-search': 4.24.0 algoliasearch: 4.23.3 - dev: false + dev: true /@algolia/autocomplete-shared@1.9.3(@algolia/client-search@4.24.0)(algoliasearch@4.23.3): resolution: {integrity: sha512-Wnm9E4Ye6Rl6sTTqjoymD+l8DjSTHsHboVRYrKgEt8Q7UHm9nYbqhN/i0fhUYA3OAEH7WA8x3jfpnmJm3rKvaQ==} @@ -617,27 +671,27 @@ packages: dependencies: '@algolia/client-search': 4.24.0 algoliasearch: 4.23.3 - dev: false + dev: true /@algolia/cache-browser-local-storage@4.23.3: resolution: {integrity: sha512-vRHXYCpPlTDE7i6UOy2xE03zHF2C8MEFjPN2v7fRbqVpcOvAUQK81x3Kc21xyb5aSIpYCjWCZbYZuz8Glyzyyg==} dependencies: '@algolia/cache-common': 4.23.3 - dev: false + dev: true /@algolia/cache-common@4.23.3: resolution: {integrity: sha512-h9XcNI6lxYStaw32pHpB1TMm0RuxphF+Ik4o7tcQiodEdpKK+wKufY6QXtba7t3k8eseirEMVB83uFFF3Nu54A==} - dev: false + dev: true /@algolia/cache-common@4.24.0: resolution: {integrity: sha512-emi+v+DmVLpMGhp0V9q9h5CdkURsNmFC+cOS6uK9ndeJm9J4TiqSvPYVu+THUP8P/S08rxf5x2P+p3CfID0Y4g==} - dev: false + dev: true /@algolia/cache-in-memory@4.23.3: resolution: {integrity: sha512-yvpbuUXg/+0rbcagxNT7un0eo3czx2Uf0y4eiR4z4SD7SiptwYTpbuS0IHxcLHG3lq22ukx1T6Kjtk/rT+mqNg==} dependencies: '@algolia/cache-common': 4.23.3 - dev: false + dev: true /@algolia/client-account@4.23.3: resolution: {integrity: sha512-hpa6S5d7iQmretHHF40QGq6hz0anWEHGlULcTIT9tbUssWUriN9AUXIFQ8Ei4w9azD0hc1rUok9/DeQQobhQMA==} @@ -645,7 +699,7 @@ packages: '@algolia/client-common': 4.23.3 '@algolia/client-search': 4.23.3 '@algolia/transporter': 4.23.3 - dev: false + dev: true /@algolia/client-analytics@4.23.3: resolution: {integrity: sha512-LBsEARGS9cj8VkTAVEZphjxTjMVCci+zIIiRhpFun9jGDUlS1XmhCW7CTrnaWeIuCQS/2iPyRqSy1nXPjcBLRA==} @@ -654,21 +708,21 @@ packages: '@algolia/client-search': 4.23.3 '@algolia/requester-common': 4.23.3 '@algolia/transporter': 4.23.3 - dev: false + dev: true /@algolia/client-common@4.23.3: resolution: {integrity: sha512-l6EiPxdAlg8CYhroqS5ybfIczsGUIAC47slLPOMDeKSVXYG1n0qGiz4RjAHLw2aD0xzh2EXZ7aRguPfz7UKDKw==} dependencies: '@algolia/requester-common': 4.23.3 '@algolia/transporter': 4.23.3 - dev: false + dev: true /@algolia/client-common@4.24.0: resolution: {integrity: sha512-bc2ROsNL6w6rqpl5jj/UywlIYC21TwSSoFHKl01lYirGMW+9Eek6r02Tocg4gZ8HAw3iBvu6XQiM3BEbmEMoiA==} dependencies: '@algolia/requester-common': 4.24.0 '@algolia/transporter': 4.24.0 - dev: false + dev: true /@algolia/client-personalization@4.23.3: resolution: {integrity: sha512-3E3yF3Ocr1tB/xOZiuC3doHQBQ2zu2MPTYZ0d4lpfWads2WTKG7ZzmGnsHmm63RflvDeLK/UVx7j2b3QuwKQ2g==} @@ -676,7 +730,7 @@ packages: '@algolia/client-common': 4.23.3 '@algolia/requester-common': 4.23.3 '@algolia/transporter': 4.23.3 - dev: false + dev: true /@algolia/client-search@4.23.3: resolution: {integrity: sha512-P4VAKFHqU0wx9O+q29Q8YVuaowaZ5EM77rxfmGnkHUJggh28useXQdopokgwMeYw2XUht49WX5RcTQ40rZIabw==} @@ -684,7 +738,7 @@ packages: '@algolia/client-common': 4.23.3 '@algolia/requester-common': 4.23.3 '@algolia/transporter': 4.23.3 - dev: false + dev: true /@algolia/client-search@4.24.0: resolution: {integrity: sha512-uRW6EpNapmLAD0mW47OXqTP8eiIx5F6qN9/x/7HHO6owL3N1IXqydGwW5nhDFBrV+ldouro2W1VX3XlcUXEFCA==} @@ -692,21 +746,21 @@ packages: '@algolia/client-common': 4.24.0 '@algolia/requester-common': 4.24.0 '@algolia/transporter': 4.24.0 - dev: false + dev: true /@algolia/logger-common@4.23.3: resolution: {integrity: sha512-y9kBtmJwiZ9ZZ+1Ek66P0M68mHQzKRxkW5kAAXYN/rdzgDN0d2COsViEFufxJ0pb45K4FRcfC7+33YB4BLrZ+g==} - dev: false + dev: true /@algolia/logger-common@4.24.0: resolution: {integrity: sha512-LLUNjkahj9KtKYrQhFKCzMx0BY3RnNP4FEtO+sBybCjJ73E8jNdaKJ/Dd8A/VA4imVHP5tADZ8pn5B8Ga/wTMA==} - dev: false + dev: true /@algolia/logger-console@4.23.3: resolution: {integrity: sha512-8xoiseoWDKuCVnWP8jHthgaeobDLolh00KJAdMe9XPrWPuf1by732jSpgy2BlsLTaT9m32pHI8CRfrOqQzHv3A==} dependencies: '@algolia/logger-common': 4.23.3 - dev: false + dev: true /@algolia/recommend@4.23.3: resolution: {integrity: sha512-9fK4nXZF0bFkdcLBRDexsnGzVmu4TSYZqxdpgBW2tEyfuSSY54D4qSRkLmNkrrz4YFvdh2GM1gA8vSsnZPR73w==} @@ -722,27 +776,27 @@ packages: '@algolia/requester-common': 4.23.3 '@algolia/requester-node-http': 4.23.3 '@algolia/transporter': 4.23.3 - dev: false + dev: true /@algolia/requester-browser-xhr@4.23.3: resolution: {integrity: sha512-jDWGIQ96BhXbmONAQsasIpTYWslyjkiGu0Quydjlowe+ciqySpiDUrJHERIRfELE5+wFc7hc1Q5hqjGoV7yghw==} dependencies: '@algolia/requester-common': 4.23.3 - dev: false + dev: true /@algolia/requester-common@4.23.3: resolution: {integrity: sha512-xloIdr/bedtYEGcXCiF2muajyvRhwop4cMZo+K2qzNht0CMzlRkm8YsDdj5IaBhshqfgmBb3rTg4sL4/PpvLYw==} - dev: false + dev: true /@algolia/requester-common@4.24.0: resolution: {integrity: sha512-k3CXJ2OVnvgE3HMwcojpvY6d9kgKMPRxs/kVohrwF5WMr2fnqojnycZkxPoEg+bXm8fi5BBfFmOqgYztRtHsQA==} - dev: false + dev: true /@algolia/requester-node-http@4.23.3: resolution: {integrity: sha512-zgu++8Uj03IWDEJM3fuNl34s746JnZOWn1Uz5taV1dFyJhVM/kTNw9Ik7YJWiUNHJQXcaD8IXD1eCb0nq/aByA==} dependencies: '@algolia/requester-common': 4.23.3 - dev: false + dev: true /@algolia/transporter@4.23.3: resolution: {integrity: sha512-Wjl5gttqnf/gQKJA+dafnD0Y6Yw97yvfY8R9h0dQltX1GXTgNs1zWgvtWW0tHl1EgMdhAyw189uWiZMnL3QebQ==} @@ -750,7 +804,7 @@ packages: '@algolia/cache-common': 4.23.3 '@algolia/logger-common': 4.23.3 '@algolia/requester-common': 4.23.3 - dev: false + dev: true /@algolia/transporter@4.24.0: resolution: {integrity: sha512-86nI7w6NzWxd1Zp9q3413dRshDqAzSbsQjhcDhPIatEFiZrL1/TjnHL8S7jVKFePlIMzDsZWXAXwXzcok9c5oA==} @@ -758,7 +812,7 @@ packages: '@algolia/cache-common': 4.24.0 '@algolia/logger-common': 4.24.0 '@algolia/requester-common': 4.24.0 - dev: false + dev: true /@alloc/quick-lru@5.2.0: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} @@ -779,22 +833,6 @@ packages: '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 - /@asciidoctor/core@3.0.4: - resolution: {integrity: sha512-41SDMi7iRRBViPe0L6VWFTe55bv6HEOJeRqMj5+E5wB1YPdUPuTucL4UAESPZM6OWmn4t/5qM5LusXomFUVwVQ==} - engines: {node: '>=16', npm: '>=8'} - dependencies: - '@asciidoctor/opal-runtime': 3.0.1 - unxhr: 1.2.0 - dev: false - - /@asciidoctor/opal-runtime@3.0.1: - resolution: {integrity: sha512-iW7ACahOG0zZft4A/4CqDcc7JX+fWRNjV5tFAVkNCzwZD+EnFolPaUOPYt8jzadc0+Bgd80cQTtRMQnaaV1kkg==} - engines: {node: '>=16'} - dependencies: - glob: 8.1.0 - unxhr: 1.2.0 - dev: false - /@aw-web-design/x-default-browser@1.4.126: resolution: {integrity: sha512-Xk1sIhyNC/esHGGVjL/niHLowM0csl/kFO5uawBy4IrWwy0o1G8LGt3jP6nmWGz+USxeeqbihAmp/oVZju6wug==} hasBin: true @@ -815,22 +853,6 @@ packages: '@babel/highlight': 7.22.20 chalk: 2.4.2 - /@babel/code-frame@7.23.5: - resolution: {integrity: sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/highlight': 7.23.4 - chalk: 2.4.2 - dev: true - - /@babel/code-frame@7.24.2: - resolution: {integrity: sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/highlight': 7.24.2 - picocolors: 1.0.0 - dev: true - /@babel/code-frame@7.24.7: resolution: {integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==} engines: {node: '>=6.9.0'} @@ -843,13 +865,18 @@ packages: resolution: {integrity: sha512-BQYjKbpXjoXwFW5jGqiizJQQT/aC7pFm9Ok1OWssonuguICi264lbgMzRp2ZMmRSlfkX6DsWDDcsrctK8Rwfiw==} engines: {node: '>=6.9.0'} - /@babel/compat-data@7.24.4: - resolution: {integrity: sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ==} + /@babel/compat-data@7.24.7: + resolution: {integrity: sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/compat-data@7.24.9: + resolution: {integrity: sha512-e701mcfApCJqMMueQI0Fb68Amflj83+dvAvHawoBpAz+GDjCIyGHzNwnefjsWJ3xiYAqqiQFoWbspGYBdb2/ng==} engines: {node: '>=6.9.0'} dev: true - /@babel/compat-data@7.24.7: - resolution: {integrity: sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==} + /@babel/compat-data@7.25.2: + resolution: {integrity: sha512-bYcppcpKBvX4znYaPEeFau03bp89ShqNMLs+rmdptMw+heSZh9+z84d2YG+K7cYLbWwzdjtDoW/uqZmPjulClQ==} engines: {node: '>=6.9.0'} dev: true @@ -868,29 +895,29 @@ packages: '@babel/traverse': 7.22.20 '@babel/types': 7.22.19 convert-source-map: 1.9.0 - debug: 4.3.4 + debug: 4.3.5 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 transitivePeerDependencies: - supports-color - /@babel/core@7.23.5: - resolution: {integrity: sha512-Cwc2XjUrG4ilcfOw4wBAK+enbdgwAcAJCfGUItPBKR7Mjw4aEfAFYrLxeRp4jWgtNIKn3n2AlBOfwwafl+42/g==} + /@babel/core@7.24.7: + resolution: {integrity: sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==} engines: {node: '>=6.9.0'} dependencies: - '@ampproject/remapping': 2.2.1 - '@babel/code-frame': 7.23.5 - '@babel/generator': 7.23.5 - '@babel/helper-compilation-targets': 7.22.15 - '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.5) - '@babel/helpers': 7.23.5 - '@babel/parser': 7.23.5 - '@babel/template': 7.22.15 - '@babel/traverse': 7.23.5 - '@babel/types': 7.23.5 + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.24.7 + '@babel/generator': 7.24.7 + '@babel/helper-compilation-targets': 7.24.7 + '@babel/helper-module-transforms': 7.24.7(@babel/core@7.24.7) + '@babel/helpers': 7.24.7 + '@babel/parser': 7.24.7 + '@babel/template': 7.24.7 + '@babel/traverse': 7.24.7 + '@babel/types': 7.24.7 convert-source-map: 2.0.0 - debug: 4.3.4 + debug: 4.3.5 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -898,22 +925,22 @@ packages: - supports-color dev: true - /@babel/core@7.24.5: - resolution: {integrity: sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==} + /@babel/core@7.24.9: + resolution: {integrity: sha512-5e3FI4Q3M3Pbr21+5xJwCv6ZT6KmGkI0vw3Tozy5ODAQFTIWe37iT8Cr7Ice2Ntb+M3iSKCEWMB1MBgKrW3whg==} engines: {node: '>=6.9.0'} dependencies: '@ampproject/remapping': 2.3.0 - '@babel/code-frame': 7.24.2 - '@babel/generator': 7.24.5 - '@babel/helper-compilation-targets': 7.23.6 - '@babel/helper-module-transforms': 7.24.5(@babel/core@7.24.5) - '@babel/helpers': 7.24.5 - '@babel/parser': 7.24.5 - '@babel/template': 7.24.0 - '@babel/traverse': 7.24.5 - '@babel/types': 7.24.5 + '@babel/code-frame': 7.24.7 + '@babel/generator': 7.24.10 + '@babel/helper-compilation-targets': 7.24.8 + '@babel/helper-module-transforms': 7.24.9(@babel/core@7.24.9) + '@babel/helpers': 7.24.8 + '@babel/parser': 7.24.8 + '@babel/template': 7.24.7 + '@babel/traverse': 7.24.8 + '@babel/types': 7.24.9 convert-source-map: 2.0.0 - debug: 4.3.4 + debug: 4.3.5 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -921,22 +948,22 @@ packages: - supports-color dev: true - /@babel/core@7.24.7: - resolution: {integrity: sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==} + /@babel/core@7.25.2: + resolution: {integrity: sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==} engines: {node: '>=6.9.0'} dependencies: '@ampproject/remapping': 2.3.0 '@babel/code-frame': 7.24.7 - '@babel/generator': 7.24.7 - '@babel/helper-compilation-targets': 7.24.7 - '@babel/helper-module-transforms': 7.24.7(@babel/core@7.24.7) - '@babel/helpers': 7.24.7 - '@babel/parser': 7.24.7 - '@babel/template': 7.24.7 - '@babel/traverse': 7.24.7 - '@babel/types': 7.24.7 + '@babel/generator': 7.25.0 + '@babel/helper-compilation-targets': 7.25.2 + '@babel/helper-module-transforms': 7.25.2(@babel/core@7.25.2) + '@babel/helpers': 7.25.0 + '@babel/parser': 7.25.0 + '@babel/template': 7.25.0 + '@babel/traverse': 7.25.2 + '@babel/types': 7.25.2 convert-source-map: 2.0.0 - debug: 4.3.5 + debug: 4.3.6 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -953,31 +980,31 @@ packages: '@jridgewell/trace-mapping': 0.3.19 jsesc: 2.5.2 - /@babel/generator@7.23.5: - resolution: {integrity: sha512-BPssCHrBD+0YrxviOa3QzpqwhNIXKEtOa2jQrm4FlmkC2apYgRnQcmPWiGZDlGxiNtltnUFolMe8497Esry+jA==} + /@babel/generator@7.24.10: + resolution: {integrity: sha512-o9HBZL1G2129luEUlG1hB4N/nlYNWHnpwlND9eOMclRqqu1YDy2sSYVCFUZwl8I1Gxh+QSRrP2vD7EpUmFVXxg==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.24.5 + '@babel/types': 7.24.9 '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 jsesc: 2.5.2 dev: true - /@babel/generator@7.24.5: - resolution: {integrity: sha512-x32i4hEXvr+iI0NEoEfDKzlemF8AmtOP8CcrRaEcpzysWuoEb1KknpcvMsHKPONoKZiDuItklgWhB18xEhr9PA==} + /@babel/generator@7.24.7: + resolution: {integrity: sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.24.5 + '@babel/types': 7.24.9 '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 jsesc: 2.5.2 dev: true - /@babel/generator@7.24.7: - resolution: {integrity: sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==} + /@babel/generator@7.25.0: + resolution: {integrity: sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.24.7 + '@babel/types': 7.25.2 '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 jsesc: 2.5.2 @@ -1007,24 +1034,35 @@ packages: lru-cache: 5.1.1 semver: 6.3.1 - /@babel/helper-compilation-targets@7.23.6: - resolution: {integrity: sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==} + /@babel/helper-compilation-targets@7.24.7: + resolution: {integrity: sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==} engines: {node: '>=6.9.0'} dependencies: - '@babel/compat-data': 7.24.4 - '@babel/helper-validator-option': 7.23.5 - browserslist: 4.23.0 + '@babel/compat-data': 7.24.7 + '@babel/helper-validator-option': 7.24.7 + browserslist: 4.23.1 lru-cache: 5.1.1 semver: 6.3.1 dev: true - /@babel/helper-compilation-targets@7.24.7: - resolution: {integrity: sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==} + /@babel/helper-compilation-targets@7.24.8: + resolution: {integrity: sha512-oU+UoqCHdp+nWVDkpldqIQL/i/bvAv53tRqLG/s+cOXxe66zOYLU7ar/Xs3LdmBihrUMEUhwu6dMZwbNOYDwvw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/compat-data': 7.24.7 - '@babel/helper-validator-option': 7.24.7 - browserslist: 4.23.1 + '@babel/compat-data': 7.24.9 + '@babel/helper-validator-option': 7.24.8 + browserslist: 4.23.2 + lru-cache: 5.1.1 + semver: 6.3.1 + dev: true + + /@babel/helper-compilation-targets@7.25.2: + resolution: {integrity: sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/compat-data': 7.25.2 + '@babel/helper-validator-option': 7.24.8 + browserslist: 4.23.2 lru-cache: 5.1.1 semver: 6.3.1 dev: true @@ -1067,7 +1105,7 @@ packages: '@babel/core': 7.22.20 '@babel/helper-compilation-targets': 7.22.15 '@babel/helper-plugin-utils': 7.22.5 - debug: 4.3.4 + debug: 4.3.5 lodash.debounce: 4.0.8 resolve: 1.22.6 transitivePeerDependencies: @@ -1082,7 +1120,7 @@ packages: resolution: {integrity: sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.24.7 + '@babel/types': 7.24.9 dev: true /@babel/helper-function-name@7.22.5: @@ -1092,20 +1130,12 @@ packages: '@babel/template': 7.22.15 '@babel/types': 7.22.19 - /@babel/helper-function-name@7.23.0: - resolution: {integrity: sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/template': 7.24.0 - '@babel/types': 7.24.5 - dev: true - /@babel/helper-function-name@7.24.7: resolution: {integrity: sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==} engines: {node: '>=6.9.0'} dependencies: '@babel/template': 7.24.7 - '@babel/types': 7.24.7 + '@babel/types': 7.24.9 dev: true /@babel/helper-hoist-variables@7.22.5: @@ -1118,7 +1148,7 @@ packages: resolution: {integrity: sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.24.7 + '@babel/types': 7.24.9 dev: true /@babel/helper-member-expression-to-functions@7.22.15: @@ -1134,19 +1164,12 @@ packages: dependencies: '@babel/types': 7.22.19 - /@babel/helper-module-imports@7.24.3: - resolution: {integrity: sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.24.5 - dev: true - /@babel/helper-module-imports@7.24.7: resolution: {integrity: sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==} engines: {node: '>=6.9.0'} dependencies: '@babel/traverse': 7.24.7 - '@babel/types': 7.24.7 + '@babel/types': 7.24.9 transitivePeerDependencies: - supports-color dev: true @@ -1164,46 +1187,49 @@ packages: '@babel/helper-split-export-declaration': 7.22.6 '@babel/helper-validator-identifier': 7.22.20 - /@babel/helper-module-transforms@7.23.3(@babel/core@7.23.5): - resolution: {integrity: sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==} + /@babel/helper-module-transforms@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.23.5 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-module-imports': 7.22.15 - '@babel/helper-simple-access': 7.22.5 - '@babel/helper-split-export-declaration': 7.22.6 - '@babel/helper-validator-identifier': 7.22.20 + '@babel/core': 7.24.7 + '@babel/helper-environment-visitor': 7.24.7 + '@babel/helper-module-imports': 7.24.7 + '@babel/helper-simple-access': 7.24.7 + '@babel/helper-split-export-declaration': 7.24.7 + '@babel/helper-validator-identifier': 7.24.7 + transitivePeerDependencies: + - supports-color dev: true - /@babel/helper-module-transforms@7.24.5(@babel/core@7.24.5): - resolution: {integrity: sha512-9GxeY8c2d2mdQUP1Dye0ks3VDyIMS98kt/llQ2nUId8IsWqTF0l1LkSX0/uP7l7MCDrzXS009Hyhe2gzTiGW8A==} + /@babel/helper-module-transforms@7.24.9(@babel/core@7.24.9): + resolution: {integrity: sha512-oYbh+rtFKj/HwBQkFlUzvcybzklmVdVV3UU+mN7n2t/q3yGHbuVdNxyFvSBO1tfvjyArpHNcWMAzsSPdyI46hw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.24.5 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-module-imports': 7.24.3 - '@babel/helper-simple-access': 7.24.5 - '@babel/helper-split-export-declaration': 7.24.5 - '@babel/helper-validator-identifier': 7.24.5 + '@babel/core': 7.24.9 + '@babel/helper-environment-visitor': 7.24.7 + '@babel/helper-module-imports': 7.24.7 + '@babel/helper-simple-access': 7.24.7 + '@babel/helper-split-export-declaration': 7.24.7 + '@babel/helper-validator-identifier': 7.24.7 + transitivePeerDependencies: + - supports-color dev: true - /@babel/helper-module-transforms@7.24.7(@babel/core@7.24.7): - resolution: {integrity: sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==} + /@babel/helper-module-transforms@7.25.2(@babel/core@7.25.2): + resolution: {integrity: sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.24.7 - '@babel/helper-environment-visitor': 7.24.7 + '@babel/core': 7.25.2 '@babel/helper-module-imports': 7.24.7 '@babel/helper-simple-access': 7.24.7 - '@babel/helper-split-export-declaration': 7.24.7 '@babel/helper-validator-identifier': 7.24.7 + '@babel/traverse': 7.25.2 transitivePeerDependencies: - supports-color dev: true @@ -1250,19 +1276,12 @@ packages: dependencies: '@babel/types': 7.22.19 - /@babel/helper-simple-access@7.24.5: - resolution: {integrity: sha512-uH3Hmf5q5n7n8mz7arjUlDOCbttY/DW4DYhE6FUsjKJ/oYC1kQQUvwEQWxRwUpX9qQKRXeqLwWxrqilMrf32sQ==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.24.5 - dev: true - /@babel/helper-simple-access@7.24.7: resolution: {integrity: sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==} engines: {node: '>=6.9.0'} dependencies: '@babel/traverse': 7.24.7 - '@babel/types': 7.24.7 + '@babel/types': 7.24.9 transitivePeerDependencies: - supports-color dev: true @@ -1280,35 +1299,24 @@ packages: dependencies: '@babel/types': 7.22.19 - /@babel/helper-split-export-declaration@7.24.5: - resolution: {integrity: sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.24.5 - dev: true - /@babel/helper-split-export-declaration@7.24.7: resolution: {integrity: sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.24.7 + '@babel/types': 7.24.9 dev: true /@babel/helper-string-parser@7.22.5: resolution: {integrity: sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==} engines: {node: '>=6.9.0'} - /@babel/helper-string-parser@7.23.4: - resolution: {integrity: sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==} + /@babel/helper-string-parser@7.24.7: + resolution: {integrity: sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==} engines: {node: '>=6.9.0'} dev: true - /@babel/helper-string-parser@7.24.1: - resolution: {integrity: sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==} - engines: {node: '>=6.9.0'} - - /@babel/helper-string-parser@7.24.7: - resolution: {integrity: sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==} + /@babel/helper-string-parser@7.24.8: + resolution: {integrity: sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==} engines: {node: '>=6.9.0'} dev: true @@ -1316,10 +1324,6 @@ packages: resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} engines: {node: '>=6.9.0'} - /@babel/helper-validator-identifier@7.24.5: - resolution: {integrity: sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==} - engines: {node: '>=6.9.0'} - /@babel/helper-validator-identifier@7.24.7: resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==} engines: {node: '>=6.9.0'} @@ -1329,13 +1333,13 @@ packages: resolution: {integrity: sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==} engines: {node: '>=6.9.0'} - /@babel/helper-validator-option@7.23.5: - resolution: {integrity: sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==} + /@babel/helper-validator-option@7.24.7: + resolution: {integrity: sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==} engines: {node: '>=6.9.0'} dev: true - /@babel/helper-validator-option@7.24.7: - resolution: {integrity: sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==} + /@babel/helper-validator-option@7.24.8: + resolution: {integrity: sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==} engines: {node: '>=6.9.0'} dev: true @@ -1358,62 +1362,37 @@ packages: transitivePeerDependencies: - supports-color - /@babel/helpers@7.23.5: - resolution: {integrity: sha512-oO7us8FzTEsG3U6ag9MfdF1iA/7Z6dz+MtFhifZk8C8o453rGJFFWUP1t+ULM9TUIAzC9uxXEiXjOiVMyd7QPg==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/template': 7.22.15 - '@babel/traverse': 7.23.5 - '@babel/types': 7.24.5 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/helpers@7.24.5: - resolution: {integrity: sha512-CiQmBMMpMQHwM5m01YnrM6imUG1ebgYJ+fAIW4FZe6m4qHTPaRHti+R8cggAwkdz4oXhtO4/K9JWlh+8hIfR2Q==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/template': 7.24.0 - '@babel/traverse': 7.24.5 - '@babel/types': 7.24.5 - transitivePeerDependencies: - - supports-color - dev: true - /@babel/helpers@7.24.7: resolution: {integrity: sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==} engines: {node: '>=6.9.0'} dependencies: '@babel/template': 7.24.7 - '@babel/types': 7.24.7 + '@babel/types': 7.24.9 dev: true - /@babel/highlight@7.22.20: - resolution: {integrity: sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==} + /@babel/helpers@7.24.8: + resolution: {integrity: sha512-gV2265Nkcz7weJJfvDoAEVzC1e2OTDpkGbEsebse8koXUJUXPsCMi7sRo/+SPMuMZ9MtUPnGwITTnQnU5YjyaQ==} engines: {node: '>=6.9.0'} dependencies: - '@babel/helper-validator-identifier': 7.22.20 - chalk: 2.4.2 - js-tokens: 4.0.0 + '@babel/template': 7.24.7 + '@babel/types': 7.24.9 + dev: true - /@babel/highlight@7.23.4: - resolution: {integrity: sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==} + /@babel/helpers@7.25.0: + resolution: {integrity: sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/helper-validator-identifier': 7.22.20 - chalk: 2.4.2 - js-tokens: 4.0.0 + '@babel/template': 7.25.0 + '@babel/types': 7.25.2 dev: true - /@babel/highlight@7.24.2: - resolution: {integrity: sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==} + /@babel/highlight@7.22.20: + resolution: {integrity: sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==} engines: {node: '>=6.9.0'} dependencies: '@babel/helper-validator-identifier': 7.22.20 chalk: 2.4.2 js-tokens: 4.0.0 - picocolors: 1.0.0 - dev: true /@babel/highlight@7.24.7: resolution: {integrity: sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==} @@ -1432,35 +1411,28 @@ packages: dependencies: '@babel/types': 7.22.19 - /@babel/parser@7.23.5: - resolution: {integrity: sha512-hOOqoiNXrmGdFbhgCzu6GiURxUgM27Xwd/aPuu8RfHEZPBzL1Z54okAHAQjXfcQNwvrlkAmAp4SlRTZ45vlthQ==} + /@babel/parser@7.24.7: + resolution: {integrity: sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==} engines: {node: '>=6.0.0'} hasBin: true dependencies: - '@babel/types': 7.24.5 + '@babel/types': 7.24.7 dev: true - /@babel/parser@7.24.4: - resolution: {integrity: sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==} + /@babel/parser@7.24.8: + resolution: {integrity: sha512-WzfbgXOkGzZiXXCqk43kKwZjzwx4oulxZi3nq2TYL9mOjQv6kYwul9mz6ID36njuL7Xkp6nJEfok848Zj10j/w==} engines: {node: '>=6.0.0'} hasBin: true dependencies: - '@babel/types': 7.24.5 + '@babel/types': 7.24.9 dev: true - /@babel/parser@7.24.5: - resolution: {integrity: sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==} + /@babel/parser@7.25.0: + resolution: {integrity: sha512-CzdIU9jdP0dg7HdyB+bHvDJGagUv+qtzZt5rYCWwW6tITNqV9odjp6Qu41gkG0ca5UfdDUWrKkiAnHHdGRnOrA==} engines: {node: '>=6.0.0'} hasBin: true dependencies: - '@babel/types': 7.24.5 - - /@babel/parser@7.24.7: - resolution: {integrity: sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==} - engines: {node: '>=6.0.0'} - hasBin: true - dependencies: - '@babel/types': 7.24.7 + '@babel/types': 7.25.2 dev: true /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.22.15(@babel/core@7.22.20): @@ -1540,39 +1512,21 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.23.5): - resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.23.5 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - - /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.24.5): + /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.24.9): resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - - /@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.23.5): - resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.23.5 + '@babel/core': 7.24.9 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.24.5): + /@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.24.9): resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.5 + '@babel/core': 7.24.9 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -1585,21 +1539,12 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.23.5): - resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.23.5 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - - /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.24.5): + /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.24.9): resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.5 + '@babel/core': 7.24.9 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -1670,21 +1615,12 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.23.5): + /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.24.9): resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.5 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - - /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.24.5): - resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 + '@babel/core': 7.24.9 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -1697,21 +1633,12 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.23.5): + /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.24.9): resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.5 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - - /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.24.5): - resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 + '@babel/core': 7.24.9 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -1725,13 +1652,13 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-jsx@7.22.5(@babel/core@7.24.5): + /@babel/plugin-syntax-jsx@7.22.5(@babel/core@7.24.9): resolution: {integrity: sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.5 + '@babel/core': 7.24.9 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -1744,21 +1671,12 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.23.5): - resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.23.5 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - - /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.24.5): + /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.24.9): resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.5 + '@babel/core': 7.24.9 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -1771,21 +1689,12 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.23.5): + /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.24.9): resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.5 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - - /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.24.5): - resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 + '@babel/core': 7.24.9 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -1798,21 +1707,12 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.23.5): + /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.24.9): resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.5 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - - /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.24.5): - resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 + '@babel/core': 7.24.9 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -1825,21 +1725,12 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.23.5): - resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.23.5 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - - /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.24.5): + /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.24.9): resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.5 + '@babel/core': 7.24.9 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -1852,21 +1743,12 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.23.5): + /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.24.9): resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.5 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - - /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.24.5): - resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 + '@babel/core': 7.24.9 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -1879,21 +1761,12 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.23.5): + /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.24.9): resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.5 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - - /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.24.5): - resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 + '@babel/core': 7.24.9 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -1917,23 +1790,13 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.23.5): + /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.24.9): resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.5 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - - /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.24.5): - resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 + '@babel/core': 7.24.9 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -1947,13 +1810,13 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-typescript@7.22.5(@babel/core@7.24.5): + /@babel/plugin-syntax-typescript@7.22.5(@babel/core@7.24.9): resolution: {integrity: sha512-1mS2o03i7t1c6VzH6fdQ3OA8tcEIxwG18zIPRp+UY1Ihv6W+XZzBCVxExF9upussPXJ0xE9XRHwMoNs1ep/nRQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.5 + '@babel/core': 7.24.9 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -2685,22 +2548,22 @@ packages: '@babel/parser': 7.22.16 '@babel/types': 7.22.19 - /@babel/template@7.24.0: - resolution: {integrity: sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==} + /@babel/template@7.24.7: + resolution: {integrity: sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==} engines: {node: '>=6.9.0'} dependencies: - '@babel/code-frame': 7.24.2 - '@babel/parser': 7.24.4 - '@babel/types': 7.24.5 + '@babel/code-frame': 7.24.7 + '@babel/parser': 7.24.8 + '@babel/types': 7.24.9 dev: true - /@babel/template@7.24.7: - resolution: {integrity: sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==} + /@babel/template@7.25.0: + resolution: {integrity: sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==} engines: {node: '>=6.9.0'} dependencies: '@babel/code-frame': 7.24.7 - '@babel/parser': 7.24.7 - '@babel/types': 7.24.7 + '@babel/parser': 7.25.0 + '@babel/types': 7.25.2 dev: true /@babel/traverse@7.22.20: @@ -2715,60 +2578,57 @@ packages: '@babel/helper-split-export-declaration': 7.22.6 '@babel/parser': 7.22.16 '@babel/types': 7.22.19 - debug: 4.3.4 + debug: 4.3.5 globals: 11.12.0 transitivePeerDependencies: - supports-color - /@babel/traverse@7.23.5: - resolution: {integrity: sha512-czx7Xy5a6sapWWRx61m1Ke1Ra4vczu1mCTtJam5zRTBOonfdJ+S/B6HYmGYu3fJtr8GGET3si6IhgWVBhJ/m8w==} + /@babel/traverse@7.24.7: + resolution: {integrity: sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==} engines: {node: '>=6.9.0'} dependencies: - '@babel/code-frame': 7.24.2 - '@babel/generator': 7.24.5 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-function-name': 7.23.0 - '@babel/helper-hoist-variables': 7.22.5 - '@babel/helper-split-export-declaration': 7.22.6 - '@babel/parser': 7.23.5 - '@babel/types': 7.24.5 - debug: 4.3.4 + '@babel/code-frame': 7.24.7 + '@babel/generator': 7.24.10 + '@babel/helper-environment-visitor': 7.24.7 + '@babel/helper-function-name': 7.24.7 + '@babel/helper-hoist-variables': 7.24.7 + '@babel/helper-split-export-declaration': 7.24.7 + '@babel/parser': 7.24.7 + '@babel/types': 7.24.9 + debug: 4.3.5 globals: 11.12.0 transitivePeerDependencies: - supports-color dev: true - /@babel/traverse@7.24.5: - resolution: {integrity: sha512-7aaBLeDQ4zYcUFDUD41lJc1fG8+5IU9DaNSJAgal866FGvmD5EbWQgnEC6kO1gGLsX0esNkfnJSndbTXA3r7UA==} + /@babel/traverse@7.24.8: + resolution: {integrity: sha512-t0P1xxAPzEDcEPmjprAQq19NWum4K0EQPjMwZQZbHt+GiZqvjCHjj755Weq1YRPVzBI+3zSfvScfpnuIecVFJQ==} engines: {node: '>=6.9.0'} dependencies: - '@babel/code-frame': 7.24.2 - '@babel/generator': 7.24.5 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-function-name': 7.23.0 - '@babel/helper-hoist-variables': 7.22.5 - '@babel/helper-split-export-declaration': 7.24.5 - '@babel/parser': 7.24.5 - '@babel/types': 7.24.5 - debug: 4.3.4 + '@babel/code-frame': 7.24.7 + '@babel/generator': 7.24.10 + '@babel/helper-environment-visitor': 7.24.7 + '@babel/helper-function-name': 7.24.7 + '@babel/helper-hoist-variables': 7.24.7 + '@babel/helper-split-export-declaration': 7.24.7 + '@babel/parser': 7.24.8 + '@babel/types': 7.24.9 + debug: 4.3.5 globals: 11.12.0 transitivePeerDependencies: - supports-color dev: true - /@babel/traverse@7.24.7: - resolution: {integrity: sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==} + /@babel/traverse@7.25.2: + resolution: {integrity: sha512-s4/r+a7xTnny2O6FcZzqgT6nE4/GHEdcqj4qAeglbUOh0TeglEfmNJFAd/OLoVtGd6ZhAO8GCVvCNUO5t/VJVQ==} engines: {node: '>=6.9.0'} dependencies: '@babel/code-frame': 7.24.7 - '@babel/generator': 7.24.7 - '@babel/helper-environment-visitor': 7.24.7 - '@babel/helper-function-name': 7.24.7 - '@babel/helper-hoist-variables': 7.24.7 - '@babel/helper-split-export-declaration': 7.24.7 - '@babel/parser': 7.24.7 - '@babel/types': 7.24.7 - debug: 4.3.5 + '@babel/generator': 7.25.0 + '@babel/parser': 7.25.0 + '@babel/template': 7.25.0 + '@babel/types': 7.25.2 + debug: 4.3.6 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -2782,28 +2642,29 @@ packages: '@babel/helper-validator-identifier': 7.22.20 to-fast-properties: 2.0.0 - /@babel/types@7.23.5: - resolution: {integrity: sha512-ON5kSOJwVO6xXVRTvOI0eOnWe7VdUcIpsovGo9U/Br4Ie4UVFQTboO2cYnDhAGU6Fp+UxSiT+pMft0SMHfuq6w==} + /@babel/types@7.24.7: + resolution: {integrity: sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==} engines: {node: '>=6.9.0'} dependencies: - '@babel/helper-string-parser': 7.23.4 - '@babel/helper-validator-identifier': 7.22.20 + '@babel/helper-string-parser': 7.24.7 + '@babel/helper-validator-identifier': 7.24.7 to-fast-properties: 2.0.0 dev: true - /@babel/types@7.24.5: - resolution: {integrity: sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==} + /@babel/types@7.24.9: + resolution: {integrity: sha512-xm8XrMKz0IlUdocVbYJe0Z9xEgidU7msskG8BbhnTPK/HZ2z/7FP7ykqPgrUH+C+r414mNfNWam1f2vqOjqjYQ==} engines: {node: '>=6.9.0'} dependencies: - '@babel/helper-string-parser': 7.24.1 - '@babel/helper-validator-identifier': 7.24.5 + '@babel/helper-string-parser': 7.24.8 + '@babel/helper-validator-identifier': 7.24.7 to-fast-properties: 2.0.0 + dev: true - /@babel/types@7.24.7: - resolution: {integrity: sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==} + /@babel/types@7.25.2: + resolution: {integrity: sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==} engines: {node: '>=6.9.0'} dependencies: - '@babel/helper-string-parser': 7.24.7 + '@babel/helper-string-parser': 7.24.8 '@babel/helper-validator-identifier': 7.24.7 to-fast-properties: 2.0.0 dev: true @@ -2812,6 +2673,35 @@ packages: resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} dev: true + /@braintree/sanitize-url@6.0.4: + resolution: {integrity: sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==} + dev: true + + /@braintree/sanitize-url@7.1.0: + resolution: {integrity: sha512-o+UlMLt49RvtCASlOMW0AkHnabN9wR9rwCCherxO0yG4Npy34GkvrAqdXQvrhNs+jh+gkK8gB8Lf05qL/O7KWg==} + dev: true + + /@cashu/cashu-ts@1.0.0-rc.9: + resolution: {integrity: sha512-8tWKGi+0syV1Aqhz+UB5vQMhflMw+9zeb6LbeI3Q3ZsKs7u2SSZp5x+DVV9XLGFDCwFJ2pCnumYNMn8bY2iCVw==} + dependencies: + '@cashu/crypto': 0.2.7 + '@noble/curves': 1.4.0 + '@noble/hashes': 1.4.0 + '@scure/bip32': 1.4.0 + '@scure/bip39': 1.3.0 + buffer: 6.0.3 + dev: false + + /@cashu/crypto@0.2.7: + resolution: {integrity: sha512-1aaDfUjiHNXoJqg8nW+341TLWV9W28DsVNXJUKcHL0yAmwLs5+56SSnb8LLDJzPamLVoYL0U0bda91klAzptig==} + dependencies: + '@noble/curves': 1.4.0 + '@noble/hashes': 1.4.0 + '@scure/bip32': 1.4.0 + '@scure/bip39': 1.3.0 + buffer: 6.0.3 + dev: false + /@changesets/apply-release-plan@6.1.4: resolution: {integrity: sha512-FMpKF1fRlJyCZVYHr3CbinpZZ+6MwvOtWUuO8uo+svcATEoc1zRDcj23pAurJ2TZ/uVz1wFHH6K3NlACy0PLew==} dependencies: @@ -3017,12 +2907,12 @@ packages: /@docsearch/css@3.6.0: resolution: {integrity: sha512-+sbxb71sWre+PwDK7X2T8+bhS6clcVMLwBPznX45Qu6opJcgRjAp7gYSDzVFp187J+feSj5dNBN1mJoi6ckkUQ==} - dev: false + dev: true - /@docsearch/js@3.6.0(@algolia/client-search@4.24.0)(search-insights@2.14.0): + /@docsearch/js@3.6.0(@algolia/client-search@4.24.0)(search-insights@2.15.0): resolution: {integrity: sha512-QujhqINEElrkIfKwyyyTfbsfMAYCkylInLYMRqHy7PHc8xTBQCow73tlo/Kc7oIwBrCLf0P3YhjlOeV4v8hevQ==} dependencies: - '@docsearch/react': 3.6.0(@algolia/client-search@4.24.0)(search-insights@2.14.0) + '@docsearch/react': 3.6.0(@algolia/client-search@4.24.0)(search-insights@2.15.0) preact: 10.22.0 transitivePeerDependencies: - '@algolia/client-search' @@ -3030,9 +2920,9 @@ packages: - react - react-dom - search-insights - dev: false + dev: true - /@docsearch/react@3.6.0(@algolia/client-search@4.24.0)(search-insights@2.14.0): + /@docsearch/react@3.6.0(@algolia/client-search@4.24.0)(search-insights@2.15.0): resolution: {integrity: sha512-HUFut4ztcVNmqy9gp/wxNbC7pTOHhgVVkHVGCACTuLhUKUhKAF9KYHJtMiLUJxEqiFLQiuri1fWF8zqwM/cu1w==} peerDependencies: '@types/react': '>= 16.8.0 < 19.0.0' @@ -3049,14 +2939,14 @@ packages: search-insights: optional: true dependencies: - '@algolia/autocomplete-core': 1.9.3(@algolia/client-search@4.24.0)(algoliasearch@4.23.3)(search-insights@2.14.0) + '@algolia/autocomplete-core': 1.9.3(@algolia/client-search@4.24.0)(algoliasearch@4.23.3)(search-insights@2.15.0) '@algolia/autocomplete-preset-algolia': 1.9.3(@algolia/client-search@4.24.0)(algoliasearch@4.23.3) '@docsearch/css': 3.6.0 algoliasearch: 4.23.3 - search-insights: 2.14.0 + search-insights: 2.15.0 transitivePeerDependencies: - '@algolia/client-search' - dev: false + dev: true /@emotion/use-insertion-effect-with-fallbacks@1.0.1(react@18.2.0): resolution: {integrity: sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==} @@ -3072,7 +2962,7 @@ packages: cpu: [ppc64] os: [aix] requiresBuild: true - dev: false + dev: true optional: true /@esbuild/android-arm64@0.17.19: @@ -3107,7 +2997,7 @@ packages: cpu: [arm64] os: [android] requiresBuild: true - dev: false + dev: true optional: true /@esbuild/android-arm@0.17.19: @@ -3142,7 +3032,7 @@ packages: cpu: [arm] os: [android] requiresBuild: true - dev: false + dev: true optional: true /@esbuild/android-x64@0.17.19: @@ -3177,7 +3067,7 @@ packages: cpu: [x64] os: [android] requiresBuild: true - dev: false + dev: true optional: true /@esbuild/darwin-arm64@0.17.19: @@ -3212,7 +3102,7 @@ packages: cpu: [arm64] os: [darwin] requiresBuild: true - dev: false + dev: true optional: true /@esbuild/darwin-x64@0.17.19: @@ -3247,7 +3137,7 @@ packages: cpu: [x64] os: [darwin] requiresBuild: true - dev: false + dev: true optional: true /@esbuild/freebsd-arm64@0.17.19: @@ -3282,7 +3172,7 @@ packages: cpu: [arm64] os: [freebsd] requiresBuild: true - dev: false + dev: true optional: true /@esbuild/freebsd-x64@0.17.19: @@ -3317,7 +3207,7 @@ packages: cpu: [x64] os: [freebsd] requiresBuild: true - dev: false + dev: true optional: true /@esbuild/linux-arm64@0.17.19: @@ -3352,7 +3242,7 @@ packages: cpu: [arm64] os: [linux] requiresBuild: true - dev: false + dev: true optional: true /@esbuild/linux-arm@0.17.19: @@ -3387,7 +3277,7 @@ packages: cpu: [arm] os: [linux] requiresBuild: true - dev: false + dev: true optional: true /@esbuild/linux-ia32@0.17.19: @@ -3422,7 +3312,7 @@ packages: cpu: [ia32] os: [linux] requiresBuild: true - dev: false + dev: true optional: true /@esbuild/linux-loong64@0.17.19: @@ -3457,7 +3347,7 @@ packages: cpu: [loong64] os: [linux] requiresBuild: true - dev: false + dev: true optional: true /@esbuild/linux-mips64el@0.17.19: @@ -3492,7 +3382,7 @@ packages: cpu: [mips64el] os: [linux] requiresBuild: true - dev: false + dev: true optional: true /@esbuild/linux-ppc64@0.17.19: @@ -3527,7 +3417,7 @@ packages: cpu: [ppc64] os: [linux] requiresBuild: true - dev: false + dev: true optional: true /@esbuild/linux-riscv64@0.17.19: @@ -3562,7 +3452,7 @@ packages: cpu: [riscv64] os: [linux] requiresBuild: true - dev: false + dev: true optional: true /@esbuild/linux-s390x@0.17.19: @@ -3597,7 +3487,7 @@ packages: cpu: [s390x] os: [linux] requiresBuild: true - dev: false + dev: true optional: true /@esbuild/linux-x64@0.17.19: @@ -3632,7 +3522,7 @@ packages: cpu: [x64] os: [linux] requiresBuild: true - dev: false + dev: true optional: true /@esbuild/netbsd-x64@0.17.19: @@ -3667,7 +3557,7 @@ packages: cpu: [x64] os: [netbsd] requiresBuild: true - dev: false + dev: true optional: true /@esbuild/openbsd-x64@0.17.19: @@ -3702,7 +3592,7 @@ packages: cpu: [x64] os: [openbsd] requiresBuild: true - dev: false + dev: true optional: true /@esbuild/sunos-x64@0.17.19: @@ -3737,7 +3627,7 @@ packages: cpu: [x64] os: [sunos] requiresBuild: true - dev: false + dev: true optional: true /@esbuild/win32-arm64@0.17.19: @@ -3772,7 +3662,7 @@ packages: cpu: [arm64] os: [win32] requiresBuild: true - dev: false + dev: true optional: true /@esbuild/win32-ia32@0.17.19: @@ -3807,7 +3697,7 @@ packages: cpu: [ia32] os: [win32] requiresBuild: true - dev: false + dev: true optional: true /@esbuild/win32-x64@0.17.19: @@ -3842,7 +3732,7 @@ packages: cpu: [x64] os: [win32] requiresBuild: true - dev: false + dev: true optional: true /@eslint-community/eslint-utils@4.4.0(eslint@8.50.0): @@ -3877,7 +3767,7 @@ packages: engines: {node: ^10.12.0 || >=12.0.0} dependencies: ajv: 6.12.6 - debug: 4.3.4 + debug: 4.3.5 espree: 7.3.1 globals: 13.22.0 ignore: 4.0.6 @@ -3894,7 +3784,7 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: ajv: 6.12.6 - debug: 4.3.4 + debug: 4.3.5 espree: 9.6.1 globals: 13.22.0 ignore: 5.3.1 @@ -3928,7 +3818,7 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: ajv: 6.12.6 - debug: 4.3.5 + debug: 4.3.6 espree: 9.6.1 globals: 13.24.0 ignore: 5.3.1 @@ -3997,7 +3887,7 @@ packages: deprecated: Use @eslint/config-array instead dependencies: '@humanwhocodes/object-schema': 2.0.3 - debug: 4.3.5 + debug: 4.3.6 minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -4007,7 +3897,7 @@ packages: engines: {node: '>=10.10.0'} dependencies: '@humanwhocodes/object-schema': 1.2.1 - debug: 4.3.4 + debug: 4.3.5 minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -4018,7 +3908,7 @@ packages: engines: {node: '>=10.10.0'} dependencies: '@humanwhocodes/object-schema': 1.2.1 - debug: 4.3.4 + debug: 4.3.5 minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -4274,7 +4164,7 @@ packages: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@jridgewell/trace-mapping': 0.3.20 + '@jridgewell/trace-mapping': 0.3.25 '@types/node': 20.6.4 chalk: 4.1.2 collect-v8-coverage: 1.0.2 @@ -4308,7 +4198,7 @@ packages: resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jridgewell/trace-mapping': 0.3.20 + '@jridgewell/trace-mapping': 0.3.25 callsites: 3.1.0 graceful-fs: 4.2.11 dev: true @@ -4337,7 +4227,7 @@ packages: resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@babel/core': 7.24.5 + '@babel/core': 7.24.9 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.25 babel-plugin-istanbul: 6.1.1 @@ -4420,13 +4310,6 @@ packages: '@jridgewell/resolve-uri': 3.1.1 '@jridgewell/sourcemap-codec': 1.4.15 - /@jridgewell/trace-mapping@0.3.20: - resolution: {integrity: sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==} - dependencies: - '@jridgewell/resolve-uri': 3.1.1 - '@jridgewell/sourcemap-codec': 1.4.15 - dev: true - /@jridgewell/trace-mapping@0.3.25: resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} dependencies: @@ -4436,7 +4319,7 @@ packages: /@jridgewell/trace-mapping@0.3.9: resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} dependencies: - '@jridgewell/resolve-uri': 3.1.1 + '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.4.15 dev: true @@ -4474,6 +4357,20 @@ packages: react: 18.2.0 dev: true + /@mermaid-js/mermaid-mindmap@9.3.0: + resolution: {integrity: sha512-IhtYSVBBRYviH1Ehu8gk69pMDF8DSRqXBRDMWrEfHoaMruHeaP2DXA3PBnuwsMaCdPQhlUUcy/7DBLAEIXvCAw==} + requiresBuild: true + dependencies: + '@braintree/sanitize-url': 6.0.4 + cytoscape: 3.30.1 + cytoscape-cose-bilkent: 4.1.0(cytoscape@3.30.1) + cytoscape-fcose: 2.2.0(cytoscape@3.30.1) + d3: 7.9.0 + khroma: 2.1.0 + non-layered-tidy-tree-layout: 2.0.2 + dev: true + optional: true + /@ndelangen/get-tarball@3.0.9: resolution: {integrity: sha512-9JKTEik4vq+yGosHYhZ1tiH/3WpUS0Nh0kej4Agndhox8pAdWhEx5knFVRcb/ya9knCRCs1rPxNrSXTDdfVqpA==} dependencies: @@ -5080,7 +4977,7 @@ packages: cpu: [arm] os: [android] requiresBuild: true - dev: false + dev: true optional: true /@rollup/rollup-android-arm-eabi@4.9.1: @@ -5096,7 +4993,7 @@ packages: cpu: [arm64] os: [android] requiresBuild: true - dev: false + dev: true optional: true /@rollup/rollup-android-arm64@4.9.1: @@ -5112,7 +5009,7 @@ packages: cpu: [arm64] os: [darwin] requiresBuild: true - dev: false + dev: true optional: true /@rollup/rollup-darwin-arm64@4.9.1: @@ -5128,7 +5025,7 @@ packages: cpu: [x64] os: [darwin] requiresBuild: true - dev: false + dev: true optional: true /@rollup/rollup-darwin-x64@4.9.1: @@ -5144,7 +5041,7 @@ packages: cpu: [arm] os: [linux] requiresBuild: true - dev: false + dev: true optional: true /@rollup/rollup-linux-arm-gnueabihf@4.9.1: @@ -5160,7 +5057,7 @@ packages: cpu: [arm] os: [linux] requiresBuild: true - dev: false + dev: true optional: true /@rollup/rollup-linux-arm64-gnu@4.18.0: @@ -5168,7 +5065,7 @@ packages: cpu: [arm64] os: [linux] requiresBuild: true - dev: false + dev: true optional: true /@rollup/rollup-linux-arm64-gnu@4.9.1: @@ -5184,7 +5081,7 @@ packages: cpu: [arm64] os: [linux] requiresBuild: true - dev: false + dev: true optional: true /@rollup/rollup-linux-arm64-musl@4.9.1: @@ -5200,7 +5097,7 @@ packages: cpu: [ppc64] os: [linux] requiresBuild: true - dev: false + dev: true optional: true /@rollup/rollup-linux-riscv64-gnu@4.18.0: @@ -5208,7 +5105,7 @@ packages: cpu: [riscv64] os: [linux] requiresBuild: true - dev: false + dev: true optional: true /@rollup/rollup-linux-riscv64-gnu@4.9.1: @@ -5224,7 +5121,7 @@ packages: cpu: [s390x] os: [linux] requiresBuild: true - dev: false + dev: true optional: true /@rollup/rollup-linux-x64-gnu@4.18.0: @@ -5232,7 +5129,7 @@ packages: cpu: [x64] os: [linux] requiresBuild: true - dev: false + dev: true optional: true /@rollup/rollup-linux-x64-gnu@4.9.1: @@ -5248,7 +5145,7 @@ packages: cpu: [x64] os: [linux] requiresBuild: true - dev: false + dev: true optional: true /@rollup/rollup-linux-x64-musl@4.9.1: @@ -5264,7 +5161,7 @@ packages: cpu: [arm64] os: [win32] requiresBuild: true - dev: false + dev: true optional: true /@rollup/rollup-win32-arm64-msvc@4.9.1: @@ -5280,7 +5177,7 @@ packages: cpu: [ia32] os: [win32] requiresBuild: true - dev: false + dev: true optional: true /@rollup/rollup-win32-ia32-msvc@4.9.1: @@ -5296,7 +5193,7 @@ packages: cpu: [x64] os: [win32] requiresBuild: true - dev: false + dev: true optional: true /@rollup/rollup-win32-x64-msvc@4.9.1: @@ -5315,19 +5212,38 @@ packages: resolution: {integrity: sha512-/+SgoRjLq7Xlf0CWuLHq2LUZeL/w65kfzAPG5NH9pcmBhs+nunQTn4gvdwgMTIXnt9b2C/1SeL2XiysZEyIC9Q==} dev: false + /@scure/base@1.1.7: + resolution: {integrity: sha512-PPNYBslrLNNUQ/Yad37MHYsNQtK67EhWb6WtSvNLLPo7SdVZgkUjD6Dg+5On7zNwmskf8OX7I7Nx5oN+MIWE0g==} + dev: false + /@scure/bip32@1.3.1: resolution: {integrity: sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==} dependencies: '@noble/curves': 1.1.0 - '@noble/hashes': 1.3.1 - '@scure/base': 1.1.1 + '@noble/hashes': 1.3.2 + '@scure/base': 1.1.7 + dev: false + + /@scure/bip32@1.4.0: + resolution: {integrity: sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==} + dependencies: + '@noble/curves': 1.4.0 + '@noble/hashes': 1.4.0 + '@scure/base': 1.1.7 dev: false /@scure/bip39@1.2.1: resolution: {integrity: sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==} dependencies: - '@noble/hashes': 1.3.1 - '@scure/base': 1.1.1 + '@noble/hashes': 1.3.2 + '@scure/base': 1.1.7 + dev: false + + /@scure/bip39@1.3.0: + resolution: {integrity: sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==} + dependencies: + '@noble/hashes': 1.4.0 + '@scure/base': 1.1.7 dev: false /@shikijs/core@1.10.0: @@ -5336,13 +5252,13 @@ packages: /@shikijs/core@1.7.0: resolution: {integrity: sha512-O6j27b7dGmJbR3mjwh/aHH8Ld+GQvA0OQsNO43wKWnqbAae3AYXrhFyScHGX8hXZD6vX2ngjzDFkZY5srtIJbQ==} - dev: false + dev: true /@shikijs/transformers@1.7.0: resolution: {integrity: sha512-QX3TP+CS4yYLt4X4Dk7wT0MsC7yweTYHMAAKY+ay+uuR9yRdFae/h+hivny2O+YixJHfZl57xtiZfWSrHdyVhQ==} dependencies: shiki: 1.7.0 - dev: false + dev: true /@sinclair/typebox@0.27.8: resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} @@ -5757,7 +5673,7 @@ packages: - supports-color dev: true - /@storybook/builder-vite@7.4.4(typescript@5.5.2)(vite@4.5.3): + /@storybook/builder-vite@7.4.4(typescript@5.5.4)(vite@4.5.3): resolution: {integrity: sha512-FpHlwTmrT9gYxfke77HcHSVoTvJCgunLGnrmNUgLwC0vVAWibWWTGtunfcV2fjBjzqVuH398qpaM+kIS9rjR8A==} peerDependencies: '@preact/preset-vite': '*' @@ -5791,7 +5707,7 @@ packages: remark-external-links: 8.0.0 remark-slug: 6.1.0 rollup: 3.29.3 - typescript: 5.5.2 + typescript: 5.5.4 vite: 4.5.3 transitivePeerDependencies: - encoding @@ -6163,14 +6079,14 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: true - /@storybook/svelte-vite@7.4.4(svelte@4.2.17)(typescript@5.5.2)(vite@4.5.3): + /@storybook/svelte-vite@7.4.4(svelte@4.2.17)(typescript@5.5.4)(vite@4.5.3): resolution: {integrity: sha512-v2t5i4ZgfXlSG0+jgiq6WY+Ee0fSTXKFZ/WGemxmCPbbU1DuMD3WgYwqMk+yYgiGv0kO8do8PkuOIN+zVXpGhw==} engines: {node: ^14.18 || >=16} peerDependencies: svelte: ^3.0.0 || ^4.0.0 vite: ^3.0.0 || ^4.0.0 dependencies: - '@storybook/builder-vite': 7.4.4(typescript@5.5.2)(vite@4.5.3) + '@storybook/builder-vite': 7.4.4(typescript@5.5.4)(vite@4.5.3) '@storybook/node-logger': 7.4.4 '@storybook/svelte': 7.4.4(svelte@4.2.17) '@sveltejs/vite-plugin-svelte': 2.4.6(svelte@4.2.17)(vite@4.5.3) @@ -6208,16 +6124,16 @@ packages: - supports-color dev: true - /@storybook/sveltekit@7.4.4(svelte@4.2.17)(typescript@5.5.2)(vite@4.5.3): + /@storybook/sveltekit@7.4.4(svelte@4.2.17)(typescript@5.5.4)(vite@4.5.3): resolution: {integrity: sha512-khhbHxiyEiu3PZp5tihahw5T+kwfbMi9tfT97y1qmbyNzfvnO3emfUgcjqxxqVVd5NL058D8wWwjfXq+Wtbf+Q==} engines: {node: ^14.18 || >=16} peerDependencies: svelte: ^3.0.0 || ^4.0.0 vite: ^4.0.0 dependencies: - '@storybook/builder-vite': 7.4.4(typescript@5.5.2)(vite@4.5.3) + '@storybook/builder-vite': 7.4.4(typescript@5.5.4)(vite@4.5.3) '@storybook/svelte': 7.4.4(svelte@4.2.17) - '@storybook/svelte-vite': 7.4.4(svelte@4.2.17)(typescript@5.5.2)(vite@4.5.3) + '@storybook/svelte-vite': 7.4.4(svelte@4.2.17)(typescript@5.5.4)(vite@4.5.3) svelte: 4.2.17 vite: 4.5.3 transitivePeerDependencies: @@ -6311,7 +6227,7 @@ packages: vite: 4.5.3 dev: true - /@sveltejs/package@2.2.2(svelte@4.2.17)(typescript@5.5.2): + /@sveltejs/package@2.2.2(svelte@4.2.17)(typescript@5.5.4): resolution: {integrity: sha512-rP3sVv6cAntcdcG4r4KspLU6nZYYUrHJBAX3Arrw0KJFdgxtlsi2iDwN0Jwr/vIkgjcU0ZPWM8kkT5kpZDlWAw==} engines: {node: ^16.14 || >=18} hasBin: true @@ -6323,7 +6239,7 @@ packages: sade: 1.8.1 semver: 7.5.4 svelte: 4.2.17 - svelte2tsx: 0.6.22(svelte@4.2.17)(typescript@5.5.2) + svelte2tsx: 0.6.22(svelte@4.2.17)(typescript@5.5.4) transitivePeerDependencies: - typescript dev: true @@ -6337,7 +6253,7 @@ packages: vite: ^4.0.0 dependencies: '@sveltejs/vite-plugin-svelte': 2.4.6(svelte@4.2.17)(vite@4.5.3) - debug: 4.3.4 + debug: 4.3.5 svelte: 4.2.17 vite: 4.5.3 transitivePeerDependencies: @@ -6353,7 +6269,7 @@ packages: vite: ^5.0.0 dependencies: '@sveltejs/vite-plugin-svelte': 3.1.0(svelte@4.2.17)(vite@4.5.3) - debug: 4.3.4 + debug: 4.3.5 svelte: 4.2.17 vite: 4.5.3 transitivePeerDependencies: @@ -6367,7 +6283,7 @@ packages: vite: ^4.0.0 dependencies: '@sveltejs/vite-plugin-svelte-inspector': 1.0.4(@sveltejs/vite-plugin-svelte@2.4.6)(svelte@4.2.17)(vite@4.5.3) - debug: 4.3.4 + debug: 4.3.5 deepmerge: 4.3.1 kleur: 4.1.5 magic-string: 0.30.3 @@ -6460,8 +6376,8 @@ packages: /@types/babel__core@7.20.2: resolution: {integrity: sha512-pNpr1T1xLUc2l3xJKuPtsEky3ybxN3m4fJkknfIpTCTfIZCDW57oAg+EfCgIIp2rvCe0Wn++/FfodDS4YXxBwA==} dependencies: - '@babel/parser': 7.22.16 - '@babel/types': 7.22.19 + '@babel/parser': 7.24.7 + '@babel/types': 7.24.7 '@types/babel__generator': 7.6.5 '@types/babel__template': 7.4.2 '@types/babel__traverse': 7.20.2 @@ -6470,20 +6386,20 @@ packages: /@types/babel__generator@7.6.5: resolution: {integrity: sha512-h9yIuWbJKdOPLJTbmSpPzkF67e659PbQDba7ifWm5BJ8xTv+sDmS7rFmywkWOvXedGTivCdeGSIIX8WLcRTz8w==} dependencies: - '@babel/types': 7.22.19 + '@babel/types': 7.24.7 dev: true /@types/babel__template@7.4.2: resolution: {integrity: sha512-/AVzPICMhMOMYoSx9MoKpGDKdBRsIXMNByh1PXSZoa+v6ZoLa8xxtsT/uLQ/NJm0XVAWl/BvId4MlDeXJaeIZQ==} dependencies: - '@babel/parser': 7.22.16 - '@babel/types': 7.22.19 + '@babel/parser': 7.24.7 + '@babel/types': 7.24.7 dev: true /@types/babel__traverse@7.20.2: resolution: {integrity: sha512-ojlGK1Hsfce93J0+kn3H5R73elidKUaZonirN33GSmgTUMpzI/MIFfSpF3haANe3G1bEBS9/9/QEqwTzwqFsKw==} dependencies: - '@babel/types': 7.22.19 + '@babel/types': 7.24.7 dev: true /@types/body-parser@1.19.3: @@ -6509,6 +6425,20 @@ packages: '@types/node': 20.6.4 dev: true + /@types/d3-scale-chromatic@3.0.3: + resolution: {integrity: sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw==} + dev: true + + /@types/d3-scale@4.0.8: + resolution: {integrity: sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==} + dependencies: + '@types/d3-time': 3.0.3 + dev: true + + /@types/d3-time@3.0.3: + resolution: {integrity: sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==} + dev: true + /@types/debug@4.1.9: resolution: {integrity: sha512-8Hz50m2eoS56ldRlepxSBa6PWEVCtzUo/92HgLc2qTMnotJNIm7xP+UZhyWoYsyOdd5dxZ+NZLb24rsKyFs2ow==} dependencies: @@ -6616,7 +6546,7 @@ packages: /@types/linkify-it@5.0.0: resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} - dev: false + dev: true /@types/lodash@4.14.199: resolution: {integrity: sha512-Vrjz5N5Ia4SEzWWgIVwnHNEnb1UE1XMkvY5DGXrAeOGE9imk0hgTHh5GyDjLDJi9OTCn9oo9dXH1uToK1VRfrg==} @@ -6627,7 +6557,7 @@ packages: dependencies: '@types/linkify-it': 5.0.0 '@types/mdurl': 2.0.0 - dev: false + dev: true /@types/mdast@3.0.12: resolution: {integrity: sha512-DT+iNIRNX884cx0/Q1ja7NyUPpZuv0KPyL5rGNxm1WC1OtHstl7n4Jb7nk+xacNShQMbczJjt8uFzznpp6kYBg==} @@ -6637,7 +6567,7 @@ packages: /@types/mdurl@2.0.0: resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} - dev: false + dev: true /@types/mdx@2.0.7: resolution: {integrity: sha512-BG4tyr+4amr3WsSEmHn/fXPqaCba/AYZ7dsaQTiavihQunHSIxk+uAtqsjvicNpyHN6cm+B9RVrUOtW9VzIKHw==} @@ -6769,7 +6699,7 @@ packages: /@types/web-bluetooth@0.0.20: resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==} - dev: false + dev: true /@types/yargs-parser@21.0.0: resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==} @@ -6801,7 +6731,7 @@ packages: '@typescript-eslint/experimental-utils': 4.33.0(eslint@7.32.0)(typescript@5.2.2) '@typescript-eslint/parser': 4.33.0(eslint@7.32.0)(typescript@5.2.2) '@typescript-eslint/scope-manager': 4.33.0 - debug: 4.3.4 + debug: 4.3.5 eslint: 7.32.0 functional-red-black-tree: 1.0.1 ignore: 5.2.4 @@ -6830,7 +6760,7 @@ packages: '@typescript-eslint/type-utils': 6.7.2(eslint@8.57.0)(typescript@5.2.2) '@typescript-eslint/utils': 6.7.2(eslint@8.57.0)(typescript@5.2.2) '@typescript-eslint/visitor-keys': 6.7.2 - debug: 4.3.4 + debug: 4.3.5 eslint: 8.57.0 graphemer: 1.4.0 ignore: 5.2.4 @@ -6842,7 +6772,7 @@ packages: - supports-color dev: false - /@typescript-eslint/eslint-plugin@6.7.2(@typescript-eslint/parser@6.7.2)(eslint@8.57.0)(typescript@5.5.2): + /@typescript-eslint/eslint-plugin@6.7.2(@typescript-eslint/parser@6.7.2)(eslint@8.57.0)(typescript@5.5.4): resolution: {integrity: sha512-ooaHxlmSgZTM6CHYAFRlifqh1OAr3PAQEwi7lhYhaegbnXrnh7CDcHmc3+ihhbQC7H0i4JF0psI5ehzkF6Yl6Q==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -6854,10 +6784,10 @@ packages: optional: true dependencies: '@eslint-community/regexpp': 4.8.1 - '@typescript-eslint/parser': 6.7.2(eslint@8.57.0)(typescript@5.5.2) + '@typescript-eslint/parser': 6.7.2(eslint@8.57.0)(typescript@5.5.4) '@typescript-eslint/scope-manager': 6.7.2 - '@typescript-eslint/type-utils': 6.7.2(eslint@8.57.0)(typescript@5.5.2) - '@typescript-eslint/utils': 6.7.2(eslint@8.57.0)(typescript@5.5.2) + '@typescript-eslint/type-utils': 6.7.2(eslint@8.57.0)(typescript@5.5.4) + '@typescript-eslint/utils': 6.7.2(eslint@8.57.0)(typescript@5.5.4) '@typescript-eslint/visitor-keys': 6.7.2 debug: 4.3.4 eslint: 8.57.0 @@ -6865,8 +6795,8 @@ packages: ignore: 5.2.4 natural-compare: 1.4.0 semver: 7.5.4 - ts-api-utils: 1.0.3(typescript@5.5.2) - typescript: 5.5.2 + ts-api-utils: 1.0.3(typescript@5.5.4) + typescript: 5.5.4 transitivePeerDependencies: - supports-color dev: true @@ -6902,7 +6832,7 @@ packages: '@typescript-eslint/scope-manager': 4.33.0 '@typescript-eslint/types': 4.33.0 '@typescript-eslint/typescript-estree': 4.33.0(typescript@5.2.2) - debug: 4.3.4 + debug: 4.3.5 eslint: 7.32.0 typescript: 5.2.2 transitivePeerDependencies: @@ -6923,14 +6853,14 @@ packages: '@typescript-eslint/types': 6.7.2 '@typescript-eslint/typescript-estree': 6.7.2(typescript@5.2.2) '@typescript-eslint/visitor-keys': 6.7.2 - debug: 4.3.4 + debug: 4.3.5 eslint: 8.57.0 typescript: 5.2.2 transitivePeerDependencies: - supports-color dev: false - /@typescript-eslint/parser@6.7.2(eslint@8.57.0)(typescript@5.5.2): + /@typescript-eslint/parser@6.7.2(eslint@8.57.0)(typescript@5.5.4): resolution: {integrity: sha512-KA3E4ox0ws+SPyxQf9iSI25R6b4Ne78ORhNHeVKrPQnoYsb9UhieoiRoJgrzgEeKGOXhcY1i8YtOeCHHTDa6Fw==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -6942,11 +6872,11 @@ packages: dependencies: '@typescript-eslint/scope-manager': 6.7.2 '@typescript-eslint/types': 6.7.2 - '@typescript-eslint/typescript-estree': 6.7.2(typescript@5.5.2) + '@typescript-eslint/typescript-estree': 6.7.2(typescript@5.5.4) '@typescript-eslint/visitor-keys': 6.7.2 - debug: 4.3.4 + debug: 4.3.5 eslint: 8.57.0 - typescript: 5.5.2 + typescript: 5.5.4 transitivePeerDependencies: - supports-color dev: true @@ -6986,7 +6916,7 @@ packages: dependencies: '@typescript-eslint/typescript-estree': 6.7.2(typescript@5.2.2) '@typescript-eslint/utils': 6.7.2(eslint@8.57.0)(typescript@5.2.2) - debug: 4.3.4 + debug: 4.3.5 eslint: 8.57.0 ts-api-utils: 1.0.3(typescript@5.2.2) typescript: 5.2.2 @@ -6994,7 +6924,7 @@ packages: - supports-color dev: false - /@typescript-eslint/type-utils@6.7.2(eslint@8.57.0)(typescript@5.5.2): + /@typescript-eslint/type-utils@6.7.2(eslint@8.57.0)(typescript@5.5.4): resolution: {integrity: sha512-36F4fOYIROYRl0qj95dYKx6kybddLtsbmPIYNK0OBeXv2j9L5nZ17j9jmfy+bIDHKQgn2EZX+cofsqi8NPATBQ==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -7004,12 +6934,12 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/typescript-estree': 6.7.2(typescript@5.5.2) - '@typescript-eslint/utils': 6.7.2(eslint@8.57.0)(typescript@5.5.2) - debug: 4.3.4 + '@typescript-eslint/typescript-estree': 6.7.2(typescript@5.5.4) + '@typescript-eslint/utils': 6.7.2(eslint@8.57.0)(typescript@5.5.4) + debug: 4.3.5 eslint: 8.57.0 - ts-api-utils: 1.0.3(typescript@5.5.2) - typescript: 5.5.2 + ts-api-utils: 1.0.3(typescript@5.5.4) + typescript: 5.5.4 transitivePeerDependencies: - supports-color dev: true @@ -7039,7 +6969,7 @@ packages: dependencies: '@typescript-eslint/types': 4.33.0 '@typescript-eslint/visitor-keys': 4.33.0 - debug: 4.3.4 + debug: 4.3.5 globby: 11.1.0 is-glob: 4.0.3 semver: 7.5.4 @@ -7049,7 +6979,7 @@ packages: - supports-color dev: true - /@typescript-eslint/typescript-estree@5.62.0(typescript@5.5.2): + /@typescript-eslint/typescript-estree@5.62.0(typescript@5.5.4): resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -7060,12 +6990,12 @@ packages: dependencies: '@typescript-eslint/types': 5.62.0 '@typescript-eslint/visitor-keys': 5.62.0 - debug: 4.3.4 + debug: 4.3.5 globby: 11.1.0 is-glob: 4.0.3 semver: 7.5.4 - tsutils: 3.21.0(typescript@5.5.2) - typescript: 5.5.2 + tsutils: 3.21.0(typescript@5.5.4) + typescript: 5.5.4 transitivePeerDependencies: - supports-color dev: true @@ -7081,7 +7011,7 @@ packages: dependencies: '@typescript-eslint/types': 6.7.2 '@typescript-eslint/visitor-keys': 6.7.2 - debug: 4.3.4 + debug: 4.3.5 globby: 11.1.0 is-glob: 4.0.3 semver: 7.5.4 @@ -7091,7 +7021,7 @@ packages: - supports-color dev: false - /@typescript-eslint/typescript-estree@6.7.2(typescript@5.5.2): + /@typescript-eslint/typescript-estree@6.7.2(typescript@5.5.4): resolution: {integrity: sha512-kiJKVMLkoSciGyFU0TOY0fRxnp9qq1AzVOHNeN1+B9erKFCJ4Z8WdjAkKQPP+b1pWStGFqezMLltxO+308dJTQ==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -7102,17 +7032,17 @@ packages: dependencies: '@typescript-eslint/types': 6.7.2 '@typescript-eslint/visitor-keys': 6.7.2 - debug: 4.3.4 + debug: 4.3.5 globby: 11.1.0 is-glob: 4.0.3 semver: 7.5.4 - ts-api-utils: 1.0.3(typescript@5.5.2) - typescript: 5.5.2 + ts-api-utils: 1.0.3(typescript@5.5.4) + typescript: 5.5.4 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/utils@5.62.0(eslint@8.57.0)(typescript@5.5.2): + /@typescript-eslint/utils@5.62.0(eslint@8.57.0)(typescript@5.5.4): resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -7123,7 +7053,7 @@ packages: '@types/semver': 7.5.2 '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/types': 5.62.0 - '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.5.2) + '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.5.4) eslint: 8.57.0 eslint-scope: 5.1.1 semver: 7.5.4 @@ -7151,7 +7081,7 @@ packages: - typescript dev: false - /@typescript-eslint/utils@6.7.2(eslint@8.57.0)(typescript@5.5.2): + /@typescript-eslint/utils@6.7.2(eslint@8.57.0)(typescript@5.5.4): resolution: {integrity: sha512-ZCcBJug/TS6fXRTsoTkgnsvyWSiXwMNiPzBUani7hDidBdj1779qwM1FIAmpH4lvlOZNF3EScsxxuGifjpLSWQ==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -7162,7 +7092,7 @@ packages: '@types/semver': 7.5.2 '@typescript-eslint/scope-manager': 6.7.2 '@typescript-eslint/types': 6.7.2 - '@typescript-eslint/typescript-estree': 6.7.2(typescript@5.5.2) + '@typescript-eslint/typescript-estree': 6.7.2(typescript@5.5.4) eslint: 8.57.0 semver: 7.5.4 transitivePeerDependencies: @@ -7204,30 +7134,30 @@ packages: vue: ^3.2.25 dependencies: vite: 5.3.1 - vue: 3.4.27(typescript@5.2.2) - dev: false + vue: 3.4.27(typescript@5.5.4) + dev: true /@vue/compiler-core@3.4.27: resolution: {integrity: sha512-E+RyqY24KnyDXsCuQrI+mlcdW3ALND6U7Gqa/+bVwbcpcR3BRRIckFoz7Qyd4TTlnugtwuI7YgjbvsLmxb+yvg==} dependencies: - '@babel/parser': 7.24.5 + '@babel/parser': 7.24.8 '@vue/shared': 3.4.27 entities: 4.5.0 estree-walker: 2.0.2 source-map-js: 1.2.0 - dev: false + dev: true /@vue/compiler-dom@3.4.27: resolution: {integrity: sha512-kUTvochG/oVgE1w5ViSr3KUBh9X7CWirebA3bezTbB5ZKBQZwR2Mwj9uoSKRMFcz4gSMzzLXBPD6KpCLb9nvWw==} dependencies: '@vue/compiler-core': 3.4.27 '@vue/shared': 3.4.27 - dev: false + dev: true /@vue/compiler-sfc@3.4.27: resolution: {integrity: sha512-nDwntUEADssW8e0rrmE0+OrONwmRlegDA1pD6QhVeXxjIytV03yDqTey9SBDiALsvAd5U4ZrEKbMyVXhX6mCGA==} dependencies: - '@babel/parser': 7.24.5 + '@babel/parser': 7.24.8 '@vue/compiler-core': 3.4.27 '@vue/compiler-dom': 3.4.27 '@vue/compiler-ssr': 3.4.27 @@ -7236,14 +7166,14 @@ packages: magic-string: 0.30.10 postcss: 8.4.38 source-map-js: 1.2.0 - dev: false + dev: true /@vue/compiler-ssr@3.4.27: resolution: {integrity: sha512-CVRzSJIltzMG5FcidsW0jKNQnNRYC8bT21VegyMMtHmhW3UOI7knmUehzswXLrExDLE6lQCZdrhD4ogI7c+vuw==} dependencies: '@vue/compiler-dom': 3.4.27 '@vue/shared': 3.4.27 - dev: false + dev: true /@vue/devtools-api@7.2.1(vue@3.4.27): resolution: {integrity: sha512-6oNCtyFOrNdqm6GUkFujsCgFlpbsHLnZqq7edeM/+cxAbMyCWvsaCsIMUaz7AiluKLccCGEM8fhOsjaKgBvb7g==} @@ -7251,7 +7181,7 @@ packages: '@vue/devtools-kit': 7.2.1(vue@3.4.27) transitivePeerDependencies: - vue - dev: false + dev: true /@vue/devtools-kit@7.2.1(vue@3.4.27): resolution: {integrity: sha512-Wak/fin1X0Q8LLIfCAHBrdaaB+R6IdpSXsDByPHbQ3BmkCP0/cIo/oEGp9i0U2+gEqD4L3V9RDjNf1S34DTzQQ==} @@ -7263,27 +7193,27 @@ packages: mitt: 3.0.1 perfect-debounce: 1.0.0 speakingurl: 14.0.1 - vue: 3.4.27(typescript@5.2.2) - dev: false + vue: 3.4.27(typescript@5.5.4) + dev: true /@vue/devtools-shared@7.2.1: resolution: {integrity: sha512-PCJF4UknJmOal68+X9XHyVeQ+idv0LFujkTOIW30+GaMJqwFVN9LkQKX4gLqn61KkGMdJTzQ1bt7EJag3TI6AA==} dependencies: rfdc: 1.3.1 - dev: false + dev: true /@vue/reactivity@3.4.27: resolution: {integrity: sha512-kK0g4NknW6JX2yySLpsm2jlunZJl2/RJGZ0H9ddHdfBVHcNzxmQ0sS0b09ipmBoQpY8JM2KmUw+a6sO8Zo+zIA==} dependencies: '@vue/shared': 3.4.27 - dev: false + dev: true /@vue/runtime-core@3.4.27: resolution: {integrity: sha512-7aYA9GEbOOdviqVvcuweTLe5Za4qBZkUY7SvET6vE8kyypxVgaT1ixHLg4urtOlrApdgcdgHoTZCUuTGap/5WA==} dependencies: '@vue/reactivity': 3.4.27 '@vue/shared': 3.4.27 - dev: false + dev: true /@vue/runtime-dom@3.4.27: resolution: {integrity: sha512-ScOmP70/3NPM+TW9hvVAz6VWWtZJqkbdf7w6ySsws+EsqtHvkhxaWLecrTorFxsawelM5Ys9FnDEMt6BPBDS0Q==} @@ -7291,7 +7221,7 @@ packages: '@vue/runtime-core': 3.4.27 '@vue/shared': 3.4.27 csstype: 3.1.3 - dev: false + dev: true /@vue/server-renderer@3.4.27(vue@3.4.27): resolution: {integrity: sha512-dlAMEuvmeA3rJsOMJ2J1kXU7o7pOxgsNHVr9K8hB3ImIkSuBrIdy0vF66h8gf8Tuinf1TK3mPAz2+2sqyf3KzA==} @@ -7300,12 +7230,12 @@ packages: dependencies: '@vue/compiler-ssr': 3.4.27 '@vue/shared': 3.4.27 - vue: 3.4.27(typescript@5.2.2) - dev: false + vue: 3.4.27(typescript@5.5.4) + dev: true /@vue/shared@3.4.27: resolution: {integrity: sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==} - dev: false + dev: true /@vueuse/core@10.11.0(vue@3.4.27): resolution: {integrity: sha512-x3sD4Mkm7PJ+pcq3HX8PLPBadXCAlSDR/waK87dz0gQE+qJnaaFhc/dZVfJz+IUYzTMVGum2QlR7ImiJQN4s6g==} @@ -7317,7 +7247,7 @@ packages: transitivePeerDependencies: - '@vue/composition-api' - vue - dev: false + dev: true /@vueuse/integrations@10.11.0(focus-trap@7.5.4)(vue@3.4.27): resolution: {integrity: sha512-Pp6MtWEIr+NDOccWd8j59Kpjy5YDXogXI61Kb1JxvSfVBO8NzFQkmrKmSZz47i+ZqHnIzxaT38L358yDHTncZg==} @@ -7367,11 +7297,11 @@ packages: transitivePeerDependencies: - '@vue/composition-api' - vue - dev: false + dev: true /@vueuse/metadata@10.11.0: resolution: {integrity: sha512-kQX7l6l8dVWNqlqyN3ePW3KmjCQO3ZMgXuBMddIu83CmucrsBfXlH+JoviYyRBws/yLTQO8g3Pbw+bdIoVm4oQ==} - dev: false + dev: true /@vueuse/shared@10.11.0(vue@3.4.27): resolution: {integrity: sha512-fyNoIXEq3PfX1L3NkNhtVQUSRtqYwJtJg+Bp9rIzculIZWHTkKSysujrOk2J+NrRulLTQH9+3gGSfYLWSEWU1A==} @@ -7380,7 +7310,7 @@ packages: transitivePeerDependencies: - '@vue/composition-api' - vue - dev: false + dev: true /@yarnpkg/esbuild-plugin-pnp@3.0.0-rc.15(esbuild@0.18.20): resolution: {integrity: sha512-kYzDJO5CA9sy+on/s2aIW0411AklfCi8Ck/4QDivOqsMKpStZA2SsR+X27VTggGwpStWaLrjJcDcdDMowtG8MA==} @@ -7430,6 +7360,14 @@ packages: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 dependencies: acorn: 8.10.0 + dev: true + + /acorn-jsx@5.3.2(acorn@8.11.3): + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + acorn: 8.11.3 /acorn-walk@8.2.0: resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==} @@ -7466,7 +7404,7 @@ packages: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} dependencies: - debug: 4.3.4 + debug: 4.3.5 transitivePeerDependencies: - supports-color dev: true @@ -7514,7 +7452,7 @@ packages: '@algolia/requester-common': 4.23.3 '@algolia/requester-node-http': 4.23.3 '@algolia/transporter': 4.23.3 - dev: false + dev: true /ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} @@ -7738,17 +7676,17 @@ packages: '@babel/core': 7.22.20 dev: true - /babel-jest@29.7.0(@babel/core@7.23.5): + /babel-jest@29.7.0(@babel/core@7.24.9): resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: '@babel/core': ^7.8.0 dependencies: - '@babel/core': 7.23.5 + '@babel/core': 7.24.9 '@jest/transform': 29.7.0 '@types/babel__core': 7.20.2 babel-plugin-istanbul: 6.1.1 - babel-preset-jest: 29.6.3(@babel/core@7.23.5) + babel-preset-jest: 29.6.3(@babel/core@7.24.9) chalk: 4.1.2 graceful-fs: 4.2.11 slash: 3.0.0 @@ -7773,8 +7711,8 @@ packages: resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@babel/template': 7.22.15 - '@babel/types': 7.24.5 + '@babel/template': 7.24.7 + '@babel/types': 7.24.9 '@types/babel__core': 7.20.2 '@types/babel__traverse': 7.20.2 dev: true @@ -7815,55 +7753,35 @@ packages: - supports-color dev: true - /babel-preset-current-node-syntax@1.0.1(@babel/core@7.23.5): - resolution: {integrity: sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.23.5 - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.23.5) - '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.23.5) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.23.5) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.23.5) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.23.5) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.23.5) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.23.5) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.23.5) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.23.5) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.23.5) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.23.5) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.23.5) - dev: true - - /babel-preset-current-node-syntax@1.0.1(@babel/core@7.24.5): + /babel-preset-current-node-syntax@1.0.1(@babel/core@7.24.9): resolution: {integrity: sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.24.5 - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.5) - '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.24.5) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.24.5) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.24.5) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.24.5) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.24.5) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.5) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.24.5) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.5) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.5) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.5) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.24.5) - dev: true - - /babel-preset-jest@29.6.3(@babel/core@7.23.5): + '@babel/core': 7.24.9 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.9) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.24.9) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.24.9) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.24.9) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.24.9) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.24.9) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.9) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.24.9) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.9) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.9) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.9) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.24.9) + dev: true + + /babel-preset-jest@29.6.3(@babel/core@7.24.9): resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.23.5 + '@babel/core': 7.24.9 babel-plugin-jest-hoist: 29.6.3 - babel-preset-current-node-syntax: 1.0.1(@babel/core@7.23.5) + babel-preset-current-node-syntax: 1.0.1(@babel/core@7.24.9) dev: true /bail@2.0.2: @@ -7874,7 +7792,6 @@ packages: /base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - dev: true /better-opn@3.0.2: resolution: {integrity: sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==} @@ -7945,6 +7862,7 @@ packages: resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} dependencies: balanced-match: 1.0.2 + dev: true /braces@3.0.2: resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} @@ -7978,17 +7896,6 @@ packages: node-releases: 2.0.13 update-browserslist-db: 1.0.13(browserslist@4.21.11) - /browserslist@4.23.0: - resolution: {integrity: sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - dependencies: - caniuse-lite: 1.0.30001607 - electron-to-chromium: 1.4.730 - node-releases: 2.0.14 - update-browserslist-db: 1.0.13(browserslist@4.23.0) - dev: true - /browserslist@4.23.1: resolution: {integrity: sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -8000,6 +7907,17 @@ packages: update-browserslist-db: 1.0.16(browserslist@4.23.1) dev: true + /browserslist@4.23.2: + resolution: {integrity: sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + dependencies: + caniuse-lite: 1.0.30001643 + electron-to-chromium: 1.4.832 + node-releases: 2.0.18 + update-browserslist-db: 1.1.0(browserslist@4.23.2) + dev: true + /bs-logger@0.2.6: resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} engines: {node: '>= 6'} @@ -8027,6 +7945,13 @@ packages: ieee754: 1.2.1 dev: true + /buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + dev: false + /bufferutil@4.0.7: resolution: {integrity: sha512-kukuqc39WOHtdxtw4UScxF/WVnMFVSQVKhtx3AjZJzhd0RGZZldcrfSEbVsWWe6KNH253574cq5F+wpv0G9pJw==} engines: {node: '>=6.14.2'} @@ -8118,14 +8043,14 @@ packages: /caniuse-lite@1.0.30001538: resolution: {integrity: sha512-HWJnhnID+0YMtGlzcp3T9drmBJUVDchPJ08tpUGFLs9CYlwWPH2uLgpHn8fND5pCgXVtnGS3H4QR9XLMHVNkHw==} - /caniuse-lite@1.0.30001607: - resolution: {integrity: sha512-WcvhVRjXLKFB/kmOFVwELtMxyhq3iM/MvmXcyCe2PNf166c39mptscOc/45TTS96n2gpNV2z7+NakArTWZCQ3w==} - dev: true - /caniuse-lite@1.0.30001639: resolution: {integrity: sha512-eFHflNTBIlFwP2AIKaYuBQN/apnUoKNhBdza8ZnW/h2di4LCZ4xFqYlxUxo+LQ76KFI1PGcC1QDxMbxTZpSCAg==} dev: true + /caniuse-lite@1.0.30001643: + resolution: {integrity: sha512-ERgWGNleEilSrHM6iUz/zJNSQTP8Mr21wDWpdgvRwcTXGAq6jMtOUPP4dqFPTdKqZ2wKTdtB+uucZ3MRpAUSmg==} + dev: true + /ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} dev: true @@ -8327,6 +8252,16 @@ packages: engines: {node: '>= 6'} dev: true + /commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + dev: true + + /commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + dev: true + /commondir@1.0.1: resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} dev: true @@ -8409,6 +8344,20 @@ packages: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} dev: true + /cose-base@1.0.3: + resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} + dependencies: + layout-base: 1.0.2 + dev: true + + /cose-base@2.2.0: + resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==} + requiresBuild: true + dependencies: + layout-base: 2.0.1 + dev: true + optional: true + /create-esm-loader@0.2.5: resolution: {integrity: sha512-WSg6l2sre6yVp0C4HYzYSJUn6H5tp7av6ZkGyiIKayR+xBqEYPrtxL+LAz8f+wD2VJs7/PDlK+SokaMkCxIGpw==} engines: {node: '>=14.x'} @@ -8534,7 +8483,7 @@ packages: /csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} - dev: false + dev: true /csv-generate@3.4.3: resolution: {integrity: sha512-w/T+rqR0vwvHqWs/1ZyMDWtHHSJaN06klRqJXBEpDJaM/+dZkso0OKh1VcuuYvK3XM53KysVNq8Ko/epCK8wOw==} @@ -8558,6 +8507,302 @@ packages: stream-transform: 2.1.3 dev: true + /cytoscape-cose-bilkent@4.1.0(cytoscape@3.30.1): + resolution: {integrity: sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==} + peerDependencies: + cytoscape: ^3.2.0 + dependencies: + cose-base: 1.0.3 + cytoscape: 3.30.1 + dev: true + + /cytoscape-fcose@2.2.0(cytoscape@3.30.1): + resolution: {integrity: sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==} + requiresBuild: true + peerDependencies: + cytoscape: ^3.2.0 + dependencies: + cose-base: 2.2.0 + cytoscape: 3.30.1 + dev: true + optional: true + + /cytoscape@3.30.1: + resolution: {integrity: sha512-TRJc3HbBPkHd50u9YfJh2FxD1lDLZ+JXnJoyBn5LkncoeuT7fapO/Hq/Ed8TdFclaKshzInge2i30bg7VKeoPQ==} + engines: {node: '>=0.10'} + dev: true + + /d3-array@2.12.1: + resolution: {integrity: sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==} + dependencies: + internmap: 1.0.1 + dev: true + + /d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + dependencies: + internmap: 2.0.3 + dev: true + + /d3-axis@3.0.0: + resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==} + engines: {node: '>=12'} + dev: true + + /d3-brush@3.0.0: + resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==} + engines: {node: '>=12'} + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + dev: true + + /d3-chord@3.0.1: + resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==} + engines: {node: '>=12'} + dependencies: + d3-path: 3.1.0 + dev: true + + /d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + dev: true + + /d3-contour@4.0.2: + resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==} + engines: {node: '>=12'} + dependencies: + d3-array: 3.2.4 + dev: true + + /d3-delaunay@6.0.4: + resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} + engines: {node: '>=12'} + dependencies: + delaunator: 5.0.1 + dev: true + + /d3-dispatch@3.0.1: + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} + engines: {node: '>=12'} + dev: true + + /d3-drag@3.0.0: + resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} + engines: {node: '>=12'} + dependencies: + d3-dispatch: 3.0.1 + d3-selection: 3.0.0 + dev: true + + /d3-dsv@3.0.1: + resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==} + engines: {node: '>=12'} + hasBin: true + dependencies: + commander: 7.2.0 + iconv-lite: 0.6.3 + rw: 1.3.3 + dev: true + + /d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + dev: true + + /d3-fetch@3.0.1: + resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==} + engines: {node: '>=12'} + dependencies: + d3-dsv: 3.0.1 + dev: true + + /d3-force@3.0.0: + resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} + engines: {node: '>=12'} + dependencies: + d3-dispatch: 3.0.1 + d3-quadtree: 3.0.1 + d3-timer: 3.0.1 + dev: true + + /d3-format@3.1.0: + resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} + engines: {node: '>=12'} + dev: true + + /d3-geo@3.1.1: + resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} + engines: {node: '>=12'} + dependencies: + d3-array: 3.2.4 + dev: true + + /d3-hierarchy@3.1.2: + resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} + engines: {node: '>=12'} + dev: true + + /d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + dependencies: + d3-color: 3.1.0 + dev: true + + /d3-path@1.0.9: + resolution: {integrity: sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==} + dev: true + + /d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + dev: true + + /d3-polygon@3.0.1: + resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==} + engines: {node: '>=12'} + dev: true + + /d3-quadtree@3.0.1: + resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} + engines: {node: '>=12'} + dev: true + + /d3-random@3.0.1: + resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==} + engines: {node: '>=12'} + dev: true + + /d3-sankey@0.12.3: + resolution: {integrity: sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==} + dependencies: + d3-array: 2.12.1 + d3-shape: 1.3.7 + dev: true + + /d3-scale-chromatic@3.1.0: + resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} + engines: {node: '>=12'} + dependencies: + d3-color: 3.1.0 + d3-interpolate: 3.0.1 + dev: true + + /d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.0 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + dev: true + + /d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + engines: {node: '>=12'} + dev: true + + /d3-shape@1.3.7: + resolution: {integrity: sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==} + dependencies: + d3-path: 1.0.9 + dev: true + + /d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + dependencies: + d3-path: 3.1.0 + dev: true + + /d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + dependencies: + d3-time: 3.1.0 + dev: true + + /d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + dependencies: + d3-array: 3.2.4 + dev: true + + /d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + dev: true + + /d3-transition@3.0.1(d3-selection@3.0.0): + resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} + engines: {node: '>=12'} + peerDependencies: + d3-selection: 2 - 3 + dependencies: + d3-color: 3.1.0 + d3-dispatch: 3.0.1 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + dev: true + + /d3-zoom@3.0.0: + resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} + engines: {node: '>=12'} + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + dev: true + + /d3@7.9.0: + resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==} + engines: {node: '>=12'} + dependencies: + d3-array: 3.2.4 + d3-axis: 3.0.0 + d3-brush: 3.0.0 + d3-chord: 3.0.1 + d3-color: 3.1.0 + d3-contour: 4.0.2 + d3-delaunay: 6.0.4 + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-dsv: 3.0.1 + d3-ease: 3.0.1 + d3-fetch: 3.0.1 + d3-force: 3.0.0 + d3-format: 3.1.0 + d3-geo: 3.1.1 + d3-hierarchy: 3.1.2 + d3-interpolate: 3.0.1 + d3-path: 3.1.0 + d3-polygon: 3.0.1 + d3-quadtree: 3.0.1 + d3-random: 3.0.1 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.1.0 + d3-selection: 3.0.0 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + d3-timer: 3.0.1 + d3-transition: 3.0.1(d3-selection@3.0.0) + d3-zoom: 3.0.0 + dev: true + /d@1.0.1: resolution: {integrity: sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==} dependencies: @@ -8565,6 +8810,13 @@ packages: type: 1.2.0 dev: false + /dagre-d3-es@7.0.10: + resolution: {integrity: sha512-qTCQmEhcynucuaZgY5/+ti3X/rnszKZhEQH/ZdWdtP1tA/y3VoHJzcVrO9pjjJCNpigfscAtoUB5ONcd2wNn0A==} + dependencies: + d3: 7.9.0 + lodash-es: 4.17.21 + dev: true + /daisyui@3.7.7: resolution: {integrity: sha512-2/nFdW/6R9MMnR8tTm07jPVyPaZwpUSkVsFAADb7Oq8N2Ynbls57laDdNqxTCUmn0QvcZi01TKl8zQbAwRfw1w==} engines: {node: '>=16.9.0'} @@ -8585,7 +8837,6 @@ packages: /dayjs@1.11.10: resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==} - dev: false /debug@2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} @@ -8619,6 +8870,17 @@ packages: dependencies: ms: 2.1.2 + /debug@4.3.6: + resolution: {integrity: sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + /decamelize-keys@1.1.1: resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} engines: {node: '>=0.10.0'} @@ -8736,6 +8998,12 @@ packages: slash: 3.0.0 dev: true + /delaunator@5.0.1: + resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==} + dependencies: + robust-predicates: 3.0.2 + dev: true + /delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -8785,7 +9053,7 @@ packages: hasBin: true dependencies: address: 1.2.2 - debug: 4.3.4 + debug: 4.3.5 transitivePeerDependencies: - supports-color dev: true @@ -8881,6 +9149,10 @@ packages: dependencies: domelementtype: 2.3.0 + /dompurify@3.1.6: + resolution: {integrity: sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ==} + dev: true + /domutils@2.8.0: resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} dependencies: @@ -8939,14 +9211,18 @@ packages: /electron-to-chromium@1.4.528: resolution: {integrity: sha512-UdREXMXzLkREF4jA8t89FQjA8WHI6ssP38PMY4/4KhXFQbtImnghh4GkCgrtiZwLKUKVD2iTVXvDVQjfomEQuA==} - /electron-to-chromium@1.4.730: - resolution: {integrity: sha512-oJRPo82XEqtQAobHpJIR3zW5YO3sSRRkPz2an4yxi1UvqhsGm54vR/wzTFV74a3soDOJ8CKW7ajOOX5ESzddwg==} - dev: true - /electron-to-chromium@1.4.815: resolution: {integrity: sha512-OvpTT2ItpOXJL7IGcYakRjHCt8L5GrrN/wHCQsRB4PQa1X9fe+X9oen245mIId7s14xvArCGSTIq644yPUKKLg==} dev: true + /electron-to-chromium@1.4.832: + resolution: {integrity: sha512-cTen3SB0H2SGU7x467NRe1eVcQgcuS6jckKfWJHia2eo0cHIGOqHoAxevIYZD4eRHcWjkvFzo93bi3vJ9W+1lA==} + dev: true + + /elkjs@0.9.3: + resolution: {integrity: sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ==} + dev: true + /emittery@0.13.1: resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} engines: {node: '>=12'} @@ -9123,7 +9399,7 @@ packages: peerDependencies: esbuild: '>=0.12 <1' dependencies: - debug: 4.3.4 + debug: 4.3.5 esbuild: 0.18.20 transitivePeerDependencies: - supports-color @@ -9247,7 +9523,7 @@ packages: '@esbuild/win32-arm64': 0.21.5 '@esbuild/win32-ia32': 0.21.5 '@esbuild/win32-x64': 0.21.5 - dev: false + dev: true /escalade@3.1.1: resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} @@ -9364,14 +9640,14 @@ packages: prettier-linter-helpers: 1.0.0 dev: true - /eslint-plugin-storybook@0.6.14(eslint@8.57.0)(typescript@5.5.2): + /eslint-plugin-storybook@0.6.14(eslint@8.57.0)(typescript@5.5.4): resolution: {integrity: sha512-IeYigPur/MvESNDo43Z+Z5UvlcEVnt0dDZmnw1odi9X2Th1R3bpGyOZsHXb9bp1pFecOpRUuoMG5xdID2TwwOg==} engines: {node: 12.x || 14.x || >= 16} peerDependencies: eslint: '>=6' dependencies: '@storybook/csf': 0.0.1 - '@typescript-eslint/utils': 5.62.0(eslint@8.57.0)(typescript@5.5.2) + '@typescript-eslint/utils': 5.62.0(eslint@8.57.0)(typescript@5.5.4) eslint: 8.57.0 requireindex: 1.2.0 ts-dedent: 2.2.0 @@ -9392,7 +9668,7 @@ packages: dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) '@jridgewell/sourcemap-codec': 1.4.15 - debug: 4.3.4 + debug: 4.3.5 eslint: 8.57.0 esutils: 2.0.3 known-css-properties: 0.28.0 @@ -9487,7 +9763,7 @@ packages: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.4 + debug: 4.3.5 doctrine: 3.0.0 enquirer: 2.4.1 escape-string-regexp: 4.0.0 @@ -9535,7 +9811,7 @@ packages: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.4 + debug: 4.3.5 doctrine: 3.0.0 enquirer: 2.4.1 escape-string-regexp: 4.0.0 @@ -9634,13 +9910,13 @@ packages: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.5 + debug: 4.3.6 doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 eslint-visitor-keys: 3.4.3 espree: 9.6.1 - esquery: 1.5.0 + esquery: 1.6.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 file-entry-cache: 6.0.1 @@ -9699,8 +9975,8 @@ packages: resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: - acorn: 8.10.0 - acorn-jsx: 5.3.2(acorn@8.10.0) + acorn: 8.11.3 + acorn-jsx: 5.3.2(acorn@8.11.3) eslint-visitor-keys: 3.4.3 /esprima@4.0.1: @@ -9714,6 +9990,13 @@ packages: engines: {node: '>=0.10'} dependencies: estraverse: 5.3.0 + dev: true + + /esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + dependencies: + estraverse: 5.3.0 /esrecurse@4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} @@ -9732,7 +10015,7 @@ packages: /estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} - dev: false + dev: true /estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} @@ -10027,7 +10310,7 @@ packages: resolution: {integrity: sha512-N7kHdlgsO/v+iD/dMoJKtsSqs5Dz/dXZVebRgJw23LDk+jMi/974zyiOYDziY2JPp8xivq9BmUGwIJMiuSBi7w==} dependencies: tabbable: 6.2.0 - dev: false + dev: true /for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} @@ -10247,6 +10530,7 @@ packages: /glob@7.1.6: resolution: {integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==} + deprecated: Glob versions prior to v9 are no longer supported dependencies: fs.realpath: 1.0.0 inflight: 1.0.6 @@ -10258,6 +10542,7 @@ packages: /glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported dependencies: fs.realpath: 1.0.0 inflight: 1.0.6 @@ -10275,6 +10560,7 @@ packages: inherits: 2.0.4 minimatch: 5.1.6 once: 1.4.0 + dev: true /globals@11.12.0: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} @@ -10311,7 +10597,7 @@ packages: array-union: 2.1.0 dir-glob: 3.0.1 fast-glob: 3.3.1 - ignore: 5.2.4 + ignore: 5.3.1 merge2: 1.4.1 slash: 3.0.0 @@ -10384,7 +10670,7 @@ packages: source-map: 0.6.1 wordwrap: 1.0.0 optionalDependencies: - uglify-js: 3.18.0 + uglify-js: 3.19.1 dev: true /hard-rejection@2.1.0: @@ -10454,7 +10740,7 @@ packages: /hookable@5.5.3: resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} - dev: false + dev: true /hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} @@ -10471,12 +10757,6 @@ packages: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} dev: true - /html5parser@2.0.2: - resolution: {integrity: sha512-L0y+IdTVxHsovmye8MBtFgBvWZnq1C9WnI/SmJszxoQjmUH1psX2uzDk21O5k5et6udxdGjwxkbmT9eVRoG05w==} - dependencies: - tslib: 2.6.2 - dev: false - /htmlparser2-svelte@4.1.0: resolution: {integrity: sha512-+4f4RBFz7Rj2Hp0ZbFbXC+Kzbd6S9PgjiuFtdT76VMNgKogrEZy0pG2UrPycPbrZzVEIM5lAT3lAdkSTCHLPjg==} dependencies: @@ -10510,7 +10790,7 @@ packages: engines: {node: '>= 6.0.0'} dependencies: agent-base: 5.1.1 - debug: 4.3.4 + debug: 4.3.5 transitivePeerDependencies: - supports-color dev: true @@ -10520,7 +10800,7 @@ packages: engines: {node: '>= 6'} dependencies: agent-base: 6.0.2 - debug: 4.3.4 + debug: 4.3.5 transitivePeerDependencies: - supports-color dev: true @@ -10534,8 +10814,15 @@ packages: engines: {node: '>=10.17.0'} dev: true - /iconv-lite@0.4.24: - resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + /iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + dev: true + + /iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} dependencies: safer-buffer: 2.1.2 @@ -10543,7 +10830,6 @@ packages: /ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - dev: true /ignore-walk@5.0.1: resolution: {integrity: sha512-yemi4pMf51WKT7khInJqAvsIGzoqYXblnsz0ql8tM+yi1EKYTY1evX4NAbJrLL/Aanr2HyZeluqU+Oi7MGHokw==} @@ -10600,6 +10886,7 @@ packages: /inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. dependencies: once: 1.4.0 wrappy: 1.0.2 @@ -10635,6 +10922,15 @@ packages: side-channel: 1.0.4 dev: true + /internmap@1.0.1: + resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==} + dev: true + + /internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + dev: true + /invariant@2.2.4: resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} dependencies: @@ -10647,7 +10943,7 @@ packages: dependencies: '@ioredis/commands': 1.2.0 cluster-key-slot: 1.1.2 - debug: 4.3.4 + debug: 4.3.5 denque: 2.1.0 lodash.defaults: 4.2.0 lodash.isarguments: 3.1.0 @@ -10971,8 +11267,8 @@ packages: resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} engines: {node: '>=8'} dependencies: - '@babel/core': 7.24.5 - '@babel/parser': 7.24.5 + '@babel/core': 7.24.7 + '@babel/parser': 7.24.7 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.0 semver: 6.3.1 @@ -10984,8 +11280,8 @@ packages: resolution: {integrity: sha512-x58orMzEVfzPUKqlbLd1hXCnySCxKdDKa6Rjg97CwuLLRI4g3FHTdnExu1OqffVFay6zeMW+T6/DowFLndWnIw==} engines: {node: '>=10'} dependencies: - '@babel/core': 7.22.20 - '@babel/parser': 7.22.16 + '@babel/core': 7.24.7 + '@babel/parser': 7.24.7 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.0 semver: 7.5.4 @@ -11006,7 +11302,7 @@ packages: resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} engines: {node: '>=10'} dependencies: - debug: 4.3.4 + debug: 4.3.5 istanbul-lib-coverage: 3.2.0 source-map: 0.6.1 transitivePeerDependencies: @@ -11175,11 +11471,11 @@ packages: ts-node: optional: true dependencies: - '@babel/core': 7.23.5 + '@babel/core': 7.24.9 '@jest/test-sequencer': 29.7.0 '@jest/types': 29.6.3 '@types/node': 14.18.63 - babel-jest: 29.7.0(@babel/core@7.23.5) + babel-jest: 29.7.0(@babel/core@7.24.9) chalk: 4.1.2 ci-info: 3.8.0 deepmerge: 4.3.1 @@ -11216,11 +11512,11 @@ packages: ts-node: optional: true dependencies: - '@babel/core': 7.23.5 + '@babel/core': 7.24.9 '@jest/test-sequencer': 29.7.0 '@jest/types': 29.6.3 '@types/node': 18.17.19 - babel-jest: 29.7.0(@babel/core@7.23.5) + babel-jest: 29.7.0(@babel/core@7.24.9) chalk: 4.1.2 ci-info: 3.8.0 deepmerge: 4.3.1 @@ -11257,11 +11553,11 @@ packages: ts-node: optional: true dependencies: - '@babel/core': 7.23.5 + '@babel/core': 7.24.9 '@jest/test-sequencer': 29.7.0 '@jest/types': 29.6.3 '@types/node': 20.6.4 - babel-jest: 29.7.0(@babel/core@7.23.5) + babel-jest: 29.7.0(@babel/core@7.24.9) chalk: 4.1.2 ci-info: 3.8.0 deepmerge: 4.3.1 @@ -11297,11 +11593,11 @@ packages: ts-node: optional: true dependencies: - '@babel/core': 7.23.5 + '@babel/core': 7.24.9 '@jest/test-sequencer': 29.7.0 '@jest/types': 29.6.3 '@types/node': 20.6.4 - babel-jest: 29.7.0(@babel/core@7.23.5) + babel-jest: 29.7.0(@babel/core@7.24.9) chalk: 4.1.2 ci-info: 3.8.0 deepmerge: 4.3.1 @@ -11338,11 +11634,11 @@ packages: ts-node: optional: true dependencies: - '@babel/core': 7.23.5 + '@babel/core': 7.24.9 '@jest/test-sequencer': 29.7.0 '@jest/types': 29.6.3 '@types/node': 20.6.4 - babel-jest: 29.7.0(@babel/core@7.23.5) + babel-jest: 29.7.0(@babel/core@7.24.9) chalk: 4.1.2 ci-info: 3.8.0 deepmerge: 4.3.1 @@ -11453,7 +11749,7 @@ packages: resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@babel/code-frame': 7.24.2 + '@babel/code-frame': 7.24.7 '@jest/types': 29.6.3 '@types/stack-utils': 2.0.1 chalk: 4.1.2 @@ -11586,15 +11882,15 @@ packages: resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@babel/core': 7.24.5 - '@babel/generator': 7.24.5 - '@babel/plugin-syntax-jsx': 7.22.5(@babel/core@7.24.5) - '@babel/plugin-syntax-typescript': 7.22.5(@babel/core@7.24.5) - '@babel/types': 7.24.5 + '@babel/core': 7.24.9 + '@babel/generator': 7.24.10 + '@babel/plugin-syntax-jsx': 7.22.5(@babel/core@7.24.9) + '@babel/plugin-syntax-typescript': 7.22.5(@babel/core@7.24.9) + '@babel/types': 7.24.9 '@jest/expect-utils': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - babel-preset-current-node-syntax: 1.0.1(@babel/core@7.24.5) + babel-preset-current-node-syntax: 1.0.1(@babel/core@7.24.9) chalk: 4.1.2 expect: 29.7.0 graceful-fs: 4.2.11 @@ -11832,11 +12128,22 @@ packages: graceful-fs: 4.2.11 dev: true + /katex@0.16.11: + resolution: {integrity: sha512-RQrI8rlHY92OLf3rho/Ts8i/XvjgguEjOkO1BEXcU3N8BqPpSzBNwV/G0Ukr+P/l3ivvJUE/Fa/CwbS6HesGNQ==} + hasBin: true + dependencies: + commander: 8.3.0 + dev: true + /keyv@4.5.3: resolution: {integrity: sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==} dependencies: json-buffer: 3.0.1 + /khroma@2.1.0: + resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==} + dev: true + /kind-of@6.0.3: resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} engines: {node: '>=0.10.0'} @@ -11855,6 +12162,16 @@ packages: resolution: {integrity: sha512-9pSL5XB4J+ifHP0e0jmmC98OGC1nL8/JjS+fi6mnTlIf//yt/MfVLtKg7S6nCtj/8KTcWX7nRlY0XywoYY1ISQ==} dev: false + /layout-base@1.0.2: + resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==} + dev: true + + /layout-base@2.0.1: + resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==} + requiresBuild: true + dev: true + optional: true + /lazy-universal-dotenv@4.0.0: resolution: {integrity: sha512-aXpZJRnTkpK6gQ/z4nk+ZBLd/Qdp118cvPruLSIQzQNRhKwEcdXCOzXuF55VDqIiuAaY3UGZ10DJtvZzDcvsxg==} engines: {node: '>=14.0.0'} @@ -11945,6 +12262,10 @@ packages: dependencies: p-locate: 5.0.0 + /lodash-es@4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + dev: true + /lodash.castarray@4.4.0: resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==} dev: true @@ -12118,7 +12439,7 @@ packages: /mark.js@8.11.1: resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==} - dev: false + dev: true /markdown-it@14.1.0: resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} @@ -12384,6 +12705,33 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + /mermaid@10.9.1: + resolution: {integrity: sha512-Mx45Obds5W1UkW1nv/7dHRsbfMM1aOKA2+Pxs/IGHNonygDHwmng8xTHyS9z4KWVi0rbko8gjiBmuwwXQ7tiNA==} + dependencies: + '@braintree/sanitize-url': 6.0.4 + '@types/d3-scale': 4.0.8 + '@types/d3-scale-chromatic': 3.0.3 + cytoscape: 3.30.1 + cytoscape-cose-bilkent: 4.1.0(cytoscape@3.30.1) + d3: 7.9.0 + d3-sankey: 0.12.3 + dagre-d3-es: 7.0.10 + dayjs: 1.11.10 + dompurify: 3.1.6 + elkjs: 0.9.3 + katex: 0.16.11 + khroma: 2.1.0 + lodash-es: 4.17.21 + mdast-util-from-markdown: 1.3.1 + non-layered-tidy-tree-layout: 2.0.2 + stylis: 4.3.2 + ts-dedent: 2.2.0 + uuid: 9.0.1 + web-worker: 1.3.0 + transitivePeerDependencies: + - supports-color + dev: true + /methods@1.1.2: resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} engines: {node: '>= 0.6'} @@ -12616,7 +12964,7 @@ packages: resolution: {integrity: sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==} dependencies: '@types/debug': 4.1.9 - debug: 4.3.4 + debug: 4.3.5 decode-named-character-reference: 1.0.2 micromark-core-commonmark: 1.1.0 micromark-factory-space: 1.1.0 @@ -12686,6 +13034,7 @@ packages: engines: {node: '>=10'} dependencies: brace-expansion: 2.0.1 + dev: true /minimatch@9.0.3: resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} @@ -12732,7 +13081,7 @@ packages: /minisearch@6.3.0: resolution: {integrity: sha512-ihFnidEeU8iXzcVHy74dhkxh/dn8Dc08ERl0xwoMMGqp4+LvRSCgicb+zGqWthVokQKvCSxITlh3P08OzdTYCQ==} - dev: false + dev: true /minizlib@2.1.2: resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} @@ -12744,7 +13093,7 @@ packages: /mitt@3.0.1: resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} - dev: false + dev: true /mixme@0.5.9: resolution: {integrity: sha512-VC5fg6ySUscaWUpI4gxCBTQMH2RdUpNrk+MsbpCYtIvf9SBJdiUey4qE7BXviJsJR4nDQxCZ+3yaYNW3guz/Pw==} @@ -12898,6 +13247,14 @@ packages: resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} dev: true + /node-releases@2.0.18: + resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} + dev: true + + /non-layered-tidy-tree-layout@2.0.2: + resolution: {integrity: sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==} + dev: true + /normalize-package-data@2.5.0: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} dependencies: @@ -12946,7 +13303,7 @@ packages: nostr-wasm: 0.1.0 dev: false - /nostr-tools@2.4.0(typescript@5.5.2): + /nostr-tools@2.4.0(typescript@5.5.4): resolution: {integrity: sha512-xQC7XdGeh0gLyprcKhvx5lwr7OQ+ZOiQ9C6GpzlVAj+EBv+AiN8kySb57t3uJoG1HK15oT9jf++MmQLwhp1xNQ==} peerDependencies: typescript: '>=5.0.0' @@ -12960,12 +13317,12 @@ packages: '@scure/base': 1.1.1 '@scure/bip32': 1.3.1 '@scure/bip39': 1.2.1 - typescript: 5.5.2 + typescript: 5.5.4 optionalDependencies: nostr-wasm: 0.1.0 dev: false - /nostr-tools@2.5.2(typescript@5.2.2): + /nostr-tools@2.5.2(typescript@5.3.3): resolution: {integrity: sha512-Ls2FKh694eudBye6q89yJ5JhXjQle1MWp1yD2sBZ5j9M3IOBEW8ia9IED5W6daSAjlT/Z/pV77yTkdF45c1Rbg==} peerDependencies: typescript: '>=5.0.0' @@ -12979,13 +13336,13 @@ packages: '@scure/base': 1.1.1 '@scure/bip32': 1.3.1 '@scure/bip39': 1.2.1 - typescript: 5.2.2 + typescript: 5.3.3 optionalDependencies: nostr-wasm: 0.1.0 dev: false - /nostr-tools@2.5.2(typescript@5.3.3): - resolution: {integrity: sha512-Ls2FKh694eudBye6q89yJ5JhXjQle1MWp1yD2sBZ5j9M3IOBEW8ia9IED5W6daSAjlT/Z/pV77yTkdF45c1Rbg==} + /nostr-tools@2.7.1(typescript@5.2.2): + resolution: {integrity: sha512-4qAvlHSqBAA8lQMwRWE6dalSNdQT77Xut9lPiJZgEcb9RAlR69wR2+KVBAgnZVaabVYH7FJ7gOQXLw/jQBAYBg==} peerDependencies: typescript: '>=5.0.0' peerDependenciesMeta: @@ -12998,7 +13355,7 @@ packages: '@scure/base': 1.1.1 '@scure/bip32': 1.3.1 '@scure/bip39': 1.2.1 - typescript: 5.3.3 + typescript: 5.2.2 optionalDependencies: nostr-wasm: 0.1.0 dev: false @@ -13251,7 +13608,7 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} dependencies: - '@babel/code-frame': 7.22.13 + '@babel/code-frame': 7.24.7 error-ex: 1.3.2 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 @@ -13340,7 +13697,7 @@ packages: /perfect-debounce@1.0.0: resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} - dev: false + dev: true /periscopic@3.1.0: resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==} @@ -13354,7 +13711,6 @@ packages: /picocolors@1.0.1: resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} - dev: true /picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} @@ -13604,12 +13960,12 @@ packages: engines: {node: ^10 || ^12 || >=14} dependencies: nanoid: 3.3.7 - picocolors: 1.0.0 + picocolors: 1.0.1 source-map-js: 1.2.0 /preact@10.22.0: resolution: {integrity: sha512-RRurnSjJPj4rp5K6XoP45Ui33ncb7e4H7WiOHVpjbkvqvA3U+N8Z6Qbo0AE6leGYBV66n8EhEaFixvIu3SkxFw==} - dev: false + dev: true /preferred-pm@3.1.2: resolution: {integrity: sha512-nk7dKrcW8hfCZ4H6klWcdRknBOXWzNQByJ0oJyX97BOupsYD+FzLS4hflgEu/uPUEHZCuRfMxzCBsuWd7OzT8Q==} @@ -13778,7 +14134,7 @@ packages: engines: {node: '>=8.16.0'} dependencies: '@types/mime-types': 2.1.1 - debug: 4.3.4 + debug: 4.3.5 extract-zip: 1.7.0 https-proxy-agent: 4.0.0 mime: 2.6.0 @@ -14213,7 +14569,7 @@ packages: /rfdc@1.3.1: resolution: {integrity: sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==} - dev: false + dev: true /rimraf@2.6.3: resolution: {integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==} @@ -14234,6 +14590,10 @@ packages: dependencies: glob: 7.2.3 + /robust-predicates@3.0.2: + resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} + dev: true + /rollup@3.29.3: resolution: {integrity: sha512-T7du6Hum8jOkSWetjRgbwpM6Sy0nECYrYRSmZjayFcOddtKJWU4d17AC3HNUk7HRuqy4p+G7aEZclSHytqUmEg==} engines: {node: '>=14.18.0', npm: '>=8.0.0'} @@ -14273,7 +14633,7 @@ packages: '@rollup/rollup-win32-ia32-msvc': 4.18.0 '@rollup/rollup-win32-x64-msvc': 4.18.0 fsevents: 2.3.3 - dev: false + dev: true /rollup@4.9.1: resolution: {integrity: sha512-pgPO9DWzLoW/vIhlSoDByCzcpX92bKEorbgXuZrqxByte3JFk2xSW2JEeAcyLc9Ru9pqcNNW+Ob7ntsk2oT/Xw==} @@ -14306,6 +14666,10 @@ packages: dependencies: queue-microtask: 1.2.3 + /rw@1.3.3: + resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + dev: true + /rxjs@6.6.7: resolution: {integrity: sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==} engines: {npm: '>=2.0.0'} @@ -14379,9 +14743,9 @@ packages: loose-envify: 1.4.0 dev: true - /search-insights@2.14.0: - resolution: {integrity: sha512-OLN6MsPMCghDOqlCtsIsYgtsC0pnwVTyT9Mu6A3ewOj1DxvzZF6COrn2g86E/c05xbktB0XN04m/t1Z+n+fTGw==} - dev: false + /search-insights@2.15.0: + resolution: {integrity: sha512-ch2sPCUDD4sbPQdknVl9ALSi9H7VyoeVbsxznYz6QV55jJ8CI3EtwpO1i84keN4+hF5IeHWIeGvc08530JkVXQ==} + dev: true /semver@5.7.2: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} @@ -14516,7 +14880,7 @@ packages: resolution: {integrity: sha512-H5pMn4JA7ayx8H0qOz1k2qANq6mZVCMl1gKLK6kWIrv1s2Ial4EmD4s4jE8QB5Dw03d/oCQUxc24sotuyR5byA==} dependencies: '@shikijs/core': 1.7.0 - dev: false + dev: true /side-channel@1.0.4: resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} @@ -14660,7 +15024,7 @@ packages: /speakingurl@14.0.1: resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} engines: {node: '>=0.10.0'} - dev: false + dev: true /sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} @@ -14825,12 +15189,16 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + /stylis@4.3.2: + resolution: {integrity: sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==} + dev: true + /sucrase@3.34.0: resolution: {integrity: sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==} engines: {node: '>=8'} hasBin: true dependencies: - '@jridgewell/gen-mapping': 0.3.3 + '@jridgewell/gen-mapping': 0.3.5 commander: 4.1.1 glob: 7.1.6 lines-and-columns: 1.2.4 @@ -14871,16 +15239,6 @@ packages: engines: {node: '>= 0.4'} dev: true - /svelte-asciidoc@0.0.2(svelte@4.2.17): - resolution: {integrity: sha512-0wYzCY0YekzOFazcFjFI4CVPQ1G1uWf41m21WKXfDBQyYuzI/jltZb+1WIXz7osc2CiexPj6LmuA3FYmBv7iQw==} - peerDependencies: - svelte: ^4.0.0 - dependencies: - '@asciidoctor/core': 3.0.4 - html5parser: 2.0.2 - svelte: 4.2.17 - dev: false - /svelte-check@3.5.2(@babel/core@7.22.20)(postcss@8.4.30)(svelte@4.2.17): resolution: {integrity: sha512-5a/YWbiH4c+AqAUP+0VneiV5bP8YOk9JL3jwvN+k2PEPLgpu85bjQc5eE67+eIZBBwUEJzmO3I92OqKcqbp3fw==} hasBin: true @@ -14894,8 +15252,8 @@ packages: picocolors: 1.0.0 sade: 1.8.1 svelte: 4.2.17 - svelte-preprocess: 5.0.4(@babel/core@7.22.20)(postcss@8.4.30)(svelte@4.2.17)(typescript@5.4.5) - typescript: 5.4.5 + svelte-preprocess: 5.0.4(@babel/core@7.22.20)(postcss@8.4.30)(svelte@4.2.17)(typescript@5.5.4) + typescript: 5.5.4 transitivePeerDependencies: - '@babel/core' - coffeescript @@ -14941,56 +15299,7 @@ packages: dependencies: svelte: 4.2.17 - /svelte-preprocess@5.0.4(@babel/core@7.22.20)(postcss@8.4.30)(svelte@4.2.17)(typescript@5.4.5): - resolution: {integrity: sha512-ABia2QegosxOGsVlsSBJvoWeXy1wUKSfF7SWJdTjLAbx/Y3SrVevvvbFNQqrSJw89+lNSsM58SipmZJ5SRi5iw==} - engines: {node: '>= 14.10.0'} - requiresBuild: true - peerDependencies: - '@babel/core': ^7.10.2 - coffeescript: ^2.5.1 - less: ^3.11.3 || ^4.0.0 - postcss: ^7 || ^8 - postcss-load-config: ^2.1.0 || ^3.0.0 || ^4.0.0 - pug: ^3.0.0 - sass: ^1.26.8 - stylus: ^0.55.0 - sugarss: ^2.0.0 || ^3.0.0 || ^4.0.0 - svelte: ^3.23.0 || ^4.0.0-next.0 || ^4.0.0 - typescript: '>=3.9.5 || ^4.0.0 || ^5.0.0' - peerDependenciesMeta: - '@babel/core': - optional: true - coffeescript: - optional: true - less: - optional: true - postcss: - optional: true - postcss-load-config: - optional: true - pug: - optional: true - sass: - optional: true - stylus: - optional: true - sugarss: - optional: true - typescript: - optional: true - dependencies: - '@babel/core': 7.22.20 - '@types/pug': 2.0.7 - detect-indent: 6.1.0 - magic-string: 0.27.0 - postcss: 8.4.30 - sorcery: 0.11.0 - strip-indent: 3.0.0 - svelte: 4.2.17 - typescript: 5.4.5 - dev: true - - /svelte-preprocess@5.0.4(@babel/core@7.22.20)(postcss@8.4.30)(svelte@4.2.17)(typescript@5.5.2): + /svelte-preprocess@5.0.4(@babel/core@7.22.20)(postcss@8.4.30)(svelte@4.2.17)(typescript@5.5.4): resolution: {integrity: sha512-ABia2QegosxOGsVlsSBJvoWeXy1wUKSfF7SWJdTjLAbx/Y3SrVevvvbFNQqrSJw89+lNSsM58SipmZJ5SRi5iw==} engines: {node: '>= 14.10.0'} requiresBuild: true @@ -15036,8 +15345,7 @@ packages: sorcery: 0.11.0 strip-indent: 3.0.0 svelte: 4.2.17 - typescript: 5.5.2 - dev: false + typescript: 5.5.4 /svelte-time@0.8.3: resolution: {integrity: sha512-8uBYoOgn7PwiTF/aNAx31yxbz7AAcD/JJF3SlgzsH/pZ/XSllmAgeCAYZ1j6G52QbNnUhwd8homQonDow616GA==} @@ -15045,7 +15353,7 @@ packages: dayjs: 1.11.10 dev: false - /svelte2tsx@0.6.22(svelte@4.2.17)(typescript@5.5.2): + /svelte2tsx@0.6.22(svelte@4.2.17)(typescript@5.5.4): resolution: {integrity: sha512-eFCfz0juaWeanbwGeQV21kPMwH3LKhfrUYRy1PqRmlieuHvJs8VeK7CaoHJdpBZWCXba2cltHVdywJmwOGhbww==} peerDependencies: svelte: ^3.55 || ^4.0.0-next.0 || ^4.0 @@ -15054,7 +15362,7 @@ packages: dedent-js: 1.0.1 pascal-case: 3.1.2 svelte: 4.2.17 - typescript: 5.5.2 + typescript: 5.5.4 dev: true /svelte@4.2.1: @@ -15112,7 +15420,7 @@ packages: /tabbable@6.2.0: resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==} - dev: false + dev: true /table@6.8.1: resolution: {integrity: sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==} @@ -15336,13 +15644,13 @@ packages: typescript: 5.2.2 dev: false - /ts-api-utils@1.0.3(typescript@5.5.2): + /ts-api-utils@1.0.3(typescript@5.5.4): resolution: {integrity: sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==} engines: {node: '>=16.13.0'} peerDependencies: typescript: '>=4.2.0' dependencies: - typescript: 5.5.2 + typescript: 5.5.4 dev: true /ts-dedent@2.2.0: @@ -15354,7 +15662,7 @@ packages: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} dev: true - /ts-jest@29.1.1(@babel/core@7.24.5)(esbuild@0.17.19)(jest@29.7.0)(typescript@5.3.3): + /ts-jest@29.1.1(@babel/core@7.24.9)(esbuild@0.17.19)(jest@29.7.0)(typescript@5.3.3): resolution: {integrity: sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -15375,7 +15683,7 @@ packages: esbuild: optional: true dependencies: - '@babel/core': 7.24.5 + '@babel/core': 7.24.9 bs-logger: 0.2.6 esbuild: 0.17.19 fast-json-stable-stringify: 2.1.0 @@ -15389,7 +15697,7 @@ packages: yargs-parser: 21.1.1 dev: true - /ts-jest@29.1.1(@babel/core@7.24.7)(esbuild@0.17.19)(jest@29.7.0)(typescript@5.2.2): + /ts-jest@29.1.1(@babel/core@7.25.2)(esbuild@0.17.19)(jest@29.7.0)(typescript@5.2.2): resolution: {integrity: sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -15410,7 +15718,7 @@ packages: esbuild: optional: true dependencies: - '@babel/core': 7.24.7 + '@babel/core': 7.25.2 bs-logger: 0.2.6 esbuild: 0.17.19 fast-json-stable-stringify: 2.1.0 @@ -15424,7 +15732,7 @@ packages: yargs-parser: 21.1.1 dev: true - /ts-jest@29.1.2(@babel/core@7.24.7)(esbuild@0.18.20)(jest@29.7.0)(typescript@5.4.4): + /ts-jest@29.1.2(@babel/core@7.25.2)(esbuild@0.18.20)(jest@29.7.0)(typescript@5.4.4): resolution: {integrity: sha512-br6GJoH/WUX4pu7FbZXuWGKGNDuU7b8Uj77g/Sp7puZV6EXzuByl6JrECvm0MzVzSTkSHWTihsXt+5XYER5b+g==} engines: {node: ^16.10.0 || ^18.0.0 || >=20.0.0} hasBin: true @@ -15445,7 +15753,7 @@ packages: esbuild: optional: true dependencies: - '@babel/core': 7.24.7 + '@babel/core': 7.25.2 bs-logger: 0.2.6 esbuild: 0.18.20 fast-json-stable-stringify: 2.1.0 @@ -15459,7 +15767,7 @@ packages: yargs-parser: 21.1.1 dev: true - /ts-jest@29.1.5(@babel/core@7.24.7)(esbuild@0.18.20)(jest@29.7.0)(typescript@5.4.5): + /ts-jest@29.1.5(@babel/core@7.25.2)(esbuild@0.18.20)(jest@29.7.0)(typescript@5.4.5): resolution: {integrity: sha512-UuClSYxM7byvvYfyWdFI+/2UxMmwNyJb0NPkZPQE2hew3RurV7l7zURgOHAd/1I1ZdPpe3GUsXNXAcN8TFKSIg==} engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} hasBin: true @@ -15483,7 +15791,7 @@ packages: esbuild: optional: true dependencies: - '@babel/core': 7.24.7 + '@babel/core': 7.25.2 bs-logger: 0.2.6 esbuild: 0.18.20 fast-json-stable-stringify: 2.1.0 @@ -15497,7 +15805,7 @@ packages: yargs-parser: 21.1.1 dev: true - /ts-jest@29.1.5(@babel/core@7.24.7)(esbuild@0.18.20)(jest@29.7.0)(typescript@5.5.2): + /ts-jest@29.1.5(@babel/core@7.25.2)(esbuild@0.18.20)(jest@29.7.0)(typescript@5.5.3): resolution: {integrity: sha512-UuClSYxM7byvvYfyWdFI+/2UxMmwNyJb0NPkZPQE2hew3RurV7l7zURgOHAd/1I1ZdPpe3GUsXNXAcN8TFKSIg==} engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} hasBin: true @@ -15521,7 +15829,45 @@ packages: esbuild: optional: true dependencies: - '@babel/core': 7.24.7 + '@babel/core': 7.25.2 + bs-logger: 0.2.6 + esbuild: 0.18.20 + fast-json-stable-stringify: 2.1.0 + jest: 29.7.0(@types/node@18.17.19)(ts-node@10.9.2) + jest-util: 29.7.0 + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.5.4 + typescript: 5.5.3 + yargs-parser: 21.1.1 + dev: true + + /ts-jest@29.1.5(@babel/core@7.25.2)(esbuild@0.18.20)(jest@29.7.0)(typescript@5.5.4): + resolution: {integrity: sha512-UuClSYxM7byvvYfyWdFI+/2UxMmwNyJb0NPkZPQE2hew3RurV7l7zURgOHAd/1I1ZdPpe3GUsXNXAcN8TFKSIg==} + engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@babel/core': '>=7.0.0-beta.0 <8' + '@jest/transform': ^29.0.0 + '@jest/types': ^29.0.0 + babel-jest: ^29.0.0 + esbuild: '*' + jest: ^29.0.0 + typescript: '>=4.3 <6' + peerDependenciesMeta: + '@babel/core': + optional: true + '@jest/transform': + optional: true + '@jest/types': + optional: true + babel-jest: + optional: true + esbuild: + optional: true + dependencies: + '@babel/core': 7.25.2 bs-logger: 0.2.6 esbuild: 0.18.20 fast-json-stable-stringify: 2.1.0 @@ -15531,7 +15877,7 @@ packages: lodash.memoize: 4.1.2 make-error: 1.3.6 semver: 7.5.4 - typescript: 5.5.2 + typescript: 5.5.4 yargs-parser: 21.1.1 dev: true @@ -15659,6 +16005,37 @@ packages: yn: 3.1.1 dev: true + /ts-node@10.9.2(@types/node@18.17.19)(typescript@5.5.3): + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.9 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 18.17.19 + acorn: 8.11.3 + acorn-walk: 8.2.0 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.5.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + dev: true + /ts-toolbelt@9.6.0: resolution: {integrity: sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==} dev: true @@ -15687,6 +16064,7 @@ packages: /tslib@2.6.2: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + dev: true /tstl@2.5.13: resolution: {integrity: sha512-h9wayHHFI5+yqt8iau0vqH96cTNhezhZ/Fk/hrIdpfkiMu3lg9nzyvMfs5bIdX51IVzZO6DudLqhkL/rVXpT6g==} @@ -15800,7 +16178,7 @@ packages: - ts-node dev: true - /tsup@7.2.0(typescript@5.2.2): + /tsup@7.2.0(ts-node@10.9.2)(typescript@5.5.3): resolution: {integrity: sha512-vDHlczXbgUvY3rWvqFEbSqmC1L7woozbzngMqTtL2PGBODTtWlRwGDDawhvWzr5c1QjKe4OAKqJGfE1xeXUvtQ==} engines: {node: '>=16.14'} hasBin: true @@ -15824,19 +16202,19 @@ packages: execa: 5.1.1 globby: 11.1.0 joycon: 3.1.1 - postcss-load-config: 4.0.1 + postcss-load-config: 4.0.1(ts-node@10.9.2) resolve-from: 5.0.0 rollup: 3.29.3 source-map: 0.8.0-beta.0 sucrase: 3.34.0 tree-kill: 1.2.2 - typescript: 5.2.2 + typescript: 5.5.3 transitivePeerDependencies: - supports-color - ts-node dev: true - /tsup@7.2.0(typescript@5.5.2): + /tsup@7.2.0(typescript@5.5.4): resolution: {integrity: sha512-vDHlczXbgUvY3rWvqFEbSqmC1L7woozbzngMqTtL2PGBODTtWlRwGDDawhvWzr5c1QjKe4OAKqJGfE1xeXUvtQ==} engines: {node: '>=16.14'} hasBin: true @@ -15866,7 +16244,7 @@ packages: source-map: 0.8.0-beta.0 sucrase: 3.34.0 tree-kill: 1.2.2 - typescript: 5.5.2 + typescript: 5.5.4 transitivePeerDependencies: - supports-color - ts-node @@ -15921,14 +16299,14 @@ packages: typescript: 5.2.2 dev: true - /tsutils@3.21.0(typescript@5.5.2): + /tsutils@3.21.0(typescript@5.5.4): resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} engines: {node: '>= 6'} peerDependencies: typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' dependencies: tslib: 1.14.1 - typescript: 5.5.2 + typescript: 5.5.4 dev: true /tty-table@4.2.1: @@ -16124,7 +16502,7 @@ packages: peerDependencies: typedoc: 0.26.x dependencies: - typedoc: 0.26.3(typescript@5.5.2) + typedoc: 0.26.3(typescript@5.5.4) dev: true /typedoc-plugin-rename-defaults@0.6.6(typedoc@0.25.1): @@ -16149,7 +16527,7 @@ packages: typescript: 5.2.2 dev: true - /typedoc@0.26.3(typescript@5.5.2): + /typedoc@0.26.3(typescript@5.5.4): resolution: {integrity: sha512-6d2Sw9disvvpdk4K7VNjKr5/3hzijtfQVHRthhDqJgnhMHy1wQz4yPMJVKXElvnZhFr0nkzo+GzjXDTRV5yLpg==} engines: {node: '>= 18'} hasBin: true @@ -16160,7 +16538,7 @@ packages: markdown-it: 14.1.0 minimatch: 9.0.5 shiki: 1.10.0 - typescript: 5.5.2 + typescript: 5.5.4 yaml: 2.4.5 dev: true @@ -16194,8 +16572,13 @@ packages: engines: {node: '>=14.17'} hasBin: true - /typescript@5.5.2: - resolution: {integrity: sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==} + /typescript@5.5.3: + resolution: {integrity: sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==} + engines: {node: '>=14.17'} + hasBin: true + + /typescript@5.5.4: + resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==} engines: {node: '>=14.17'} hasBin: true @@ -16203,8 +16586,8 @@ packages: resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} dev: true - /uglify-js@3.18.0: - resolution: {integrity: sha512-SyVVbcNBCk0dzr9XL/R/ySrmYf0s372K6/hFklzgcp2lBFyXtw4I7BOdDjlLhE1aVqaI/SHWXWmYdlZxuyF38A==} + /uglify-js@3.19.1: + resolution: {integrity: sha512-y/2wiW+ceTYR2TSSptAhfnEtpLaQ4Ups5zrjB2d3kuVxHj16j/QJwPl5PvuGy9uARb39J0+iKxcRPvtpsx4A4A==} engines: {node: '>=0.8.0'} hasBin: true requiresBuild: true @@ -16382,11 +16765,6 @@ packages: engines: {node: '>=8'} dev: true - /unxhr@1.2.0: - resolution: {integrity: sha512-6cGpm8NFXPD9QbSNx0cD2giy7teZ6xOkCUH3U89WKVkL9N9rBrWjlCwhR94Re18ZlAop4MOc3WU1M3Hv/bgpIw==} - engines: {node: '>=8.11'} - dev: false - /update-browserslist-db@1.0.13(browserslist@4.21.11): resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} hasBin: true @@ -16395,26 +16773,26 @@ packages: dependencies: browserslist: 4.21.11 escalade: 3.1.1 - picocolors: 1.0.0 + picocolors: 1.0.1 - /update-browserslist-db@1.0.13(browserslist@4.23.0): - resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} + /update-browserslist-db@1.0.16(browserslist@4.23.1): + resolution: {integrity: sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' dependencies: - browserslist: 4.23.0 - escalade: 3.1.1 - picocolors: 1.0.0 + browserslist: 4.23.1 + escalade: 3.1.2 + picocolors: 1.0.1 dev: true - /update-browserslist-db@1.0.16(browserslist@4.23.1): - resolution: {integrity: sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==} + /update-browserslist-db@1.1.0(browserslist@4.23.2): + resolution: {integrity: sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' dependencies: - browserslist: 4.23.1 + browserslist: 4.23.2 escalade: 3.1.2 picocolors: 1.0.1 dev: true @@ -16523,7 +16901,7 @@ packages: resolution: {integrity: sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==} engines: {node: '>=10.12.0'} dependencies: - '@jridgewell/trace-mapping': 0.3.20 + '@jridgewell/trace-mapping': 0.3.25 '@types/istanbul-lib-coverage': 2.0.4 convert-source-map: 1.9.0 dev: true @@ -16645,7 +17023,7 @@ packages: rollup: 4.18.0 optionalDependencies: fsevents: 2.3.3 - dev: false + dev: true /vitefu@0.2.4(vite@4.5.3): resolution: {integrity: sha512-fanAXjSaf9xXtOOeno8wZXIhgia+CZury481LsDaV++lSvcU2R9Ch2bPh3PYFyoHW+w9LqAeYRISVQjUIew14g==} @@ -16668,7 +17046,19 @@ packages: dependencies: vite: 4.5.3 - /vitepress@1.2.3(@algolia/client-search@4.24.0)(search-insights@2.14.0)(typescript@5.2.2): + /vitepress-plugin-mermaid@2.0.16(mermaid@10.9.1)(vitepress@1.2.3): + resolution: {integrity: sha512-sW0Eu4+1EzRdwZBMGjzwKDsbQiuJIxCy8BlMw7Ur88p9fXalrFYKqZ3wYWLxsFTBipeooFIeanef/xw1P+v7vQ==} + peerDependencies: + mermaid: '10' + vitepress: ^1.0.0 || ^1.0.0-alpha + dependencies: + mermaid: 10.9.1 + vitepress: 1.2.3(@algolia/client-search@4.24.0)(search-insights@2.15.0)(typescript@5.5.4) + optionalDependencies: + '@mermaid-js/mermaid-mindmap': 9.3.0 + dev: true + + /vitepress@1.2.3(@algolia/client-search@4.24.0)(search-insights@2.15.0)(typescript@5.5.4): resolution: {integrity: sha512-GvEsrEeNLiDE1+fuwDAYJCYLNZDAna+EtnXlPajhv/MYeTjbNK6Bvyg6NoTdO1sbwuQJ0vuJR99bOlH53bo6lg==} hasBin: true peerDependencies: @@ -16681,7 +17071,7 @@ packages: optional: true dependencies: '@docsearch/css': 3.6.0 - '@docsearch/js': 3.6.0(@algolia/client-search@4.24.0)(search-insights@2.14.0) + '@docsearch/js': 3.6.0(@algolia/client-search@4.24.0)(search-insights@2.15.0) '@shikijs/core': 1.7.0 '@shikijs/transformers': 1.7.0 '@types/markdown-it': 14.1.1 @@ -16695,7 +17085,7 @@ packages: minisearch: 6.3.0 shiki: 1.7.0 vite: 5.3.1 - vue: 3.4.27(typescript@5.2.2) + vue: 3.4.27(typescript@5.5.4) transitivePeerDependencies: - '@algolia/client-search' - '@types/node' @@ -16722,7 +17112,7 @@ packages: - terser - typescript - universal-cookie - dev: false + dev: true /vscode-oniguruma@1.7.0: resolution: {integrity: sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==} @@ -16744,10 +17134,10 @@ packages: '@vue/composition-api': optional: true dependencies: - vue: 3.4.27(typescript@5.2.2) - dev: false + vue: 3.4.27(typescript@5.5.4) + dev: true - /vue@3.4.27(typescript@5.2.2): + /vue@3.4.27(typescript@5.5.4): resolution: {integrity: sha512-8s/56uK6r01r1icG/aEOHqyMVxd1bkYcSe9j8HcKtr/xTOFWvnzIVTehNW+5Yt89f+DLBe4A569pnZLS5HzAMA==} peerDependencies: typescript: '*' @@ -16760,8 +17150,8 @@ packages: '@vue/runtime-dom': 3.4.27 '@vue/server-renderer': 3.4.27(vue@3.4.27) '@vue/shared': 3.4.27 - typescript: 5.2.2 - dev: false + typescript: 5.5.4 + dev: true /walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} @@ -16788,6 +17178,10 @@ packages: engines: {node: '>= 8'} dev: false + /web-worker@1.3.0: + resolution: {integrity: sha512-BSR9wyRsy/KOValMgd5kMyr3JzpdeoR9KVId8u5GVlTTAtNChlsE4yTxeY7zMdNSyOmoKBv8NH2qeRY9Tg+IaA==} + dev: true + /webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} dev: true @@ -17073,7 +17467,7 @@ packages: engines: {node: '>=12'} dependencies: cliui: 8.0.1 - escalade: 3.1.1 + escalade: 3.1.2 get-caller-file: 2.0.5 require-directory: 2.1.1 string-width: 4.2.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index ce51ca20..239ed258 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -7,4 +7,5 @@ packages: - "ndk-cache-nostr" - "ndk-svelte" - "ndk-svelte-components" + - "ndk-wallet" - "demos"