├── .changeset ├── README.md └── config.json ├── .clinerules ├── .cursorrules ├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .github └── workflows │ └── deploy.yml ├── .gitignore ├── .gitmodules ├── .prettierignore ├── .prettierrc ├── .roo ├── mcp.json └── rules │ ├── INDEX.md │ ├── NOSTR.md │ └── TESTS.md ├── .roomodes ├── .specs └── relay-transport.md ├── .tenex.json ├── BUILD.md ├── LICENSE ├── README.md ├── REFERENCES.md ├── biome.json ├── bun.lock ├── context └── SPEC.md ├── docs ├── .vitepress │ ├── config.mts │ └── theme │ │ ├── index.ts │ │ └── style.css ├── api-examples.md ├── index.md └── package.json ├── ndk-blossom ├── CHANGELOG.md ├── README.md ├── SPEC.md ├── context │ └── SPEC.md ├── docs │ ├── error-handling.md │ ├── getting-started.md │ ├── mirroring.md │ └── optimization.md ├── example │ └── blossom-upload │ │ ├── README.md │ │ ├── bun.lock │ │ ├── index.html │ │ ├── index.tsx │ │ ├── package.json │ │ ├── tsconfig.json │ │ ├── tsconfig.node.json │ │ └── vite.config.ts ├── package.json ├── src │ ├── blossom.ts │ ├── healing │ │ └── url-healing.ts │ ├── index.ts │ ├── types │ │ └── index.ts │ ├── upload │ │ └── uploader.ts │ └── utils │ │ ├── auth.ts │ │ ├── constants.ts │ │ ├── errors.ts │ │ ├── http.ts │ │ ├── logger.ts │ │ └── sha256.ts └── tsconfig.json ├── ndk-cache-dexie ├── .gitignore ├── .prettierignore ├── CHANGELOG.md ├── README.md ├── package.json ├── previous-head-with-ndk-hooks ├── src │ ├── caches │ │ ├── event-tags.ts │ │ ├── events.ts │ │ ├── nip05.ts │ │ ├── profiles.ts │ │ ├── relay-info.ts │ │ ├── unpublished-events.ts │ │ └── zapper.ts │ ├── db.ts │ ├── index.test.ts │ ├── index.ts │ └── lru-cache.ts ├── test │ ├── performance.test.ts │ └── setup.ts ├── tsconfig.json ├── typedoc.json └── vitest.config.ts ├── ndk-cache-nostr ├── .gitignore ├── .prettierignore ├── CHANGELOG.md ├── README.md ├── docs │ └── cache │ │ └── nostr.md ├── package.json ├── src │ ├── index.ts │ └── queue.ts └── tsconfig.json ├── ndk-cache-redis ├── .gitignore ├── .prettierignore ├── CHANGELOG.md ├── README.md ├── package.json ├── src │ ├── index.test.ts │ └── index.ts └── tsconfig.json ├── ndk-cache-sqlite-wasm ├── CHANGELOG.md ├── docs │ ├── INDEX.md │ ├── bundling.md │ └── web-worker-setup.md ├── example │ ├── index.html │ ├── sql-wasm.wasm │ └── vite │ │ ├── .gitignore │ │ ├── README.md │ │ ├── bun.lock │ │ ├── index.html │ │ ├── package.json │ │ ├── public │ │ └── vite.svg │ │ ├── src │ │ ├── counter.ts │ │ ├── main.ts │ │ ├── style.css │ │ └── typescript.svg │ │ └── tsconfig.json ├── package.json ├── src │ ├── db │ │ ├── indexeddb-utils.ts │ │ ├── migrations.ts │ │ ├── schema.ts │ │ └── wasm-loader.ts │ ├── functions │ │ ├── addDecryptedEvent.ts │ │ ├── addUnpublishedEvent.ts │ │ ├── discardUnpublishedEvent.ts │ │ ├── fetchProfile.ts │ │ ├── fetchProfileSync.ts │ │ ├── getAllProfilesSync.ts │ │ ├── getDecryptedEvent.ts │ │ ├── getEvent.ts │ │ ├── getProfiles.ts │ │ ├── getRelayStatus.ts │ │ ├── getUnpublishedEvents.ts │ │ ├── query.ts │ │ ├── saveProfile.ts │ │ ├── setEvent.ts │ │ └── updateRelayStatus.ts │ ├── index.ts │ ├── types.ts │ └── worker.ts └── tsconfig.json ├── ndk-cache-sqlite ├── README.md ├── bun.lock ├── example │ ├── README.md │ ├── bun.lock │ ├── package.json │ ├── src │ │ └── index.ts │ └── tsconfig.json ├── package.json ├── src │ ├── db │ │ ├── database.ts │ │ ├── migrations.ts │ │ └── schema.ts │ ├── functions │ │ ├── fetchProfile.ts │ │ ├── getEvent.ts │ │ ├── getProfiles.ts │ │ ├── getRelayStatus.ts │ │ ├── query.ts │ │ ├── saveProfile.ts │ │ ├── setEvent.ts │ │ └── updateRelayStatus.ts │ ├── index.test.ts │ ├── index.ts │ └── types.ts ├── test │ └── setup │ │ └── vitest.setup.ts ├── tsconfig.json └── vitest.config.ts ├── ndk-core ├── .prettierignore ├── CHANGELOG.md ├── LICENSE ├── OUTBOX-REPORT.md ├── OUTBOX.md ├── README.md ├── RELEASE-NOTES.md ├── SIG-SAMPLING.md ├── bun.lock ├── docs-styles.css ├── docs │ ├── api-examples.md │ ├── getting-started │ │ ├── introduction.md │ │ ├── signers.md │ │ └── usage.md │ ├── index.md │ ├── internals │ │ └── subscriptions.md │ ├── migration │ │ └── 2.12-to-2.13.md │ └── tutorial │ │ ├── auth.md │ │ ├── local-first.md │ │ ├── publishing.md │ │ ├── signer-persistence.md │ │ ├── speed.md │ │ ├── subscription-management.md │ │ └── zaps │ │ └── index.md ├── package.json ├── snippets │ ├── event │ │ ├── basic.md │ │ ├── publish-tracking.md │ │ ├── signing-with-different-signers.md │ │ └── tagging-users-and-events.md │ ├── index.md │ ├── testing │ │ ├── event-generation.md │ │ ├── mock-relays.md │ │ ├── nutzap-testing.md │ │ └── relay-pool-testing.md │ └── user │ │ ├── generate-keys.md │ │ └── get-profile.md ├── src │ ├── app-settings │ │ └── index.ts │ ├── cache │ │ └── index.ts │ ├── dvm │ │ └── schedule.ts │ ├── events │ │ ├── content-tagger.test.ts │ │ ├── content-tagger.ts │ │ ├── dedup.ts │ │ ├── encode.test.ts │ │ ├── encryption.test.ts │ │ ├── encryption.ts │ │ ├── fetch-tagged-event.ts │ │ ├── gift-wrapping.ts │ │ ├── index.test.ts │ │ ├── index.ts │ │ ├── kind.ts │ │ ├── kinds │ │ │ ├── NDKRelayList.ts │ │ │ ├── article.ts │ │ │ ├── blossom-list.ts │ │ │ ├── cashu │ │ │ │ ├── token.ts │ │ │ │ ├── tx.test.ts │ │ │ │ └── tx.ts │ │ │ ├── classified.ts │ │ │ ├── drafts.ts │ │ │ ├── dvm │ │ │ │ ├── NDKTranscriptionDVM.ts │ │ │ │ ├── feedback.ts │ │ │ │ ├── index.ts │ │ │ │ ├── request.ts │ │ │ │ └── result.ts │ │ │ ├── follow-pack.test.ts │ │ │ ├── follow-pack.ts │ │ │ ├── highlight.ts │ │ │ ├── image.ts │ │ │ ├── index.ts │ │ │ ├── lists │ │ │ │ ├── index.test.ts │ │ │ │ └── index.ts │ │ │ ├── nip89 │ │ │ │ ├── app-handler.test.ts │ │ │ │ └── app-handler.ts │ │ │ ├── nutzap │ │ │ │ ├── index.test.ts │ │ │ │ ├── index.ts │ │ │ │ ├── mint-list.ts │ │ │ │ └── proof.ts │ │ │ ├── repost.ts │ │ │ ├── simple-group │ │ │ │ ├── index.ts │ │ │ │ ├── member-list.ts │ │ │ │ └── metadata.ts │ │ │ ├── story.test.ts │ │ │ ├── story.ts │ │ │ ├── subscriptions │ │ │ │ ├── amount.ts │ │ │ │ ├── receipt.ts │ │ │ │ ├── subscription-start.ts │ │ │ │ ├── tier.test.ts │ │ │ │ └── tier.ts │ │ │ ├── video.ts │ │ │ └── wiki.ts │ │ ├── nip19.test.ts │ │ ├── nip19.ts │ │ ├── nip73.ts │ │ ├── publish-tracking.test.ts │ │ ├── repost.test.ts │ │ ├── repost.ts │ │ ├── serializer.ts │ │ ├── signature.ts │ │ ├── validation.ts │ │ └── wrap.ts │ ├── index.ts │ ├── light-bolt11-decoder.d.ts │ ├── ndk │ │ ├── active-user.ts │ │ ├── entity.ts │ │ ├── fetch-event-from-tag.ts │ │ ├── index.ts │ │ └── queue │ │ │ └── index.ts │ ├── outbox │ │ ├── index.ts │ │ ├── read │ │ │ └── with-authors.ts │ │ ├── relay-ranking.ts │ │ ├── tracker.test.ts │ │ ├── tracker.ts │ │ └── write.ts │ ├── relay │ │ ├── auth-policies.test.ts │ │ ├── auth-policies.ts │ │ ├── connectivity.test.ts │ │ ├── connectivity.ts │ │ ├── index.test.ts │ │ ├── index.ts │ │ ├── pool │ │ │ ├── index.test.ts │ │ │ └── index.ts │ │ ├── publisher.ts │ │ ├── score.ts │ │ ├── sets │ │ │ ├── calculate.test.ts │ │ │ ├── calculate.ts │ │ │ ├── index.ts │ │ │ ├── publish.test.ts │ │ │ └── utils.ts │ │ ├── signature-verification-stats.ts │ │ ├── sub-manager.ts │ │ ├── subscription.test.ts │ │ └── subscription.ts │ ├── signers │ │ ├── deserialization.ts │ │ ├── index.ts │ │ ├── nip07 │ │ │ ├── index.test.ts │ │ │ └── index.ts │ │ ├── nip46 │ │ │ ├── backend │ │ │ │ ├── connect.ts │ │ │ │ ├── get-public-key.ts │ │ │ │ ├── index.ts │ │ │ │ ├── nip04-decrypt.ts │ │ │ │ ├── nip04-encrypt.ts │ │ │ │ ├── nip44-decrypt.ts │ │ │ │ ├── nip44-encrypt.ts │ │ │ │ ├── ping.ts │ │ │ │ └── sign-event.ts │ │ │ ├── index.test.ts │ │ │ ├── index.ts │ │ │ ├── nostrconnect.ts │ │ │ └── rpc.ts │ │ ├── private-key │ │ │ ├── index.test.ts │ │ │ └── index.ts │ │ ├── registry.ts │ │ ├── serialization.test.ts │ │ └── types.ts │ ├── subscription.test.ts │ ├── subscription │ │ ├── grouping.test.ts │ │ ├── grouping.ts │ │ ├── index.test.ts │ │ ├── index.ts │ │ ├── manager.test.ts │ │ ├── manager.ts │ │ ├── utils.test.ts │ │ └── utils.ts │ ├── thread │ │ ├── index.test.ts │ │ └── index.ts │ ├── types.ts │ ├── user │ │ ├── follows.test.ts │ │ ├── follows.ts │ │ ├── index.test.ts │ │ ├── index.ts │ │ ├── nip05.test.ts │ │ ├── nip05.ts │ │ ├── pin.ts │ │ └── profile.ts │ ├── utils │ │ ├── filter.ts │ │ ├── get-users-relay-list.ts │ │ ├── imeta.test.ts │ │ ├── imeta.ts │ │ ├── normalize-url.ts │ │ └── timeout.ts │ ├── workers │ │ └── sig-verification.ts │ ├── zap │ │ └── invoice.ts │ └── zapper │ │ ├── index.test.ts │ │ ├── index.ts │ │ ├── ln.ts │ │ ├── nip57.test.ts │ │ ├── nip57.ts │ │ └── nip61.ts ├── test │ ├── helpers │ │ ├── test-fixtures.ts │ │ └── time.ts │ ├── index.ts │ ├── mocks │ │ ├── event-generator.ts │ │ ├── nutzaps.ts │ │ ├── relay-mock.ts │ │ └── relay-pool-mock.ts │ └── setup │ │ └── vitest.setup.ts ├── tsconfig.json ├── tsconfig.typedoc.json ├── typedoc.json └── vitest.config.ts ├── ndk-expert-best-practices.md ├── ndk-hooks ├── .gitignore ├── .roomodes ├── CHANGELOG.md ├── README.md ├── docs │ ├── index.md │ ├── muting.md │ └── session-management.md ├── example │ └── session │ │ ├── README.md │ │ ├── bun.lock │ │ ├── index.html │ │ ├── index.tsx │ │ ├── package.json │ │ ├── tsconfig.json │ │ └── vite.config.ts ├── package.json ├── rules │ └── NOSTR.md ├── snippets │ ├── session-monitoring.md │ ├── update-user-profile.md │ └── use-mute-item.md ├── src │ ├── index.ts │ ├── mutes │ │ ├── hooks │ │ │ ├── __tests__ │ │ │ │ └── mute-hooks.test.ts │ │ │ ├── index.ts │ │ │ ├── use-is-item-muted.ts │ │ │ ├── use-mute-criteria.ts │ │ │ └── use-mute-filter.ts │ │ ├── store │ │ │ ├── __tests__ │ │ │ │ ├── fixtures.ts │ │ │ │ └── mute-store.test.ts │ │ │ ├── add-extra-mute-items.ts │ │ │ ├── index.ts │ │ │ ├── init.ts │ │ │ ├── is-item-muted.ts │ │ │ ├── load.ts │ │ │ ├── mute.ts │ │ │ ├── set-active-pubkey.ts │ │ │ ├── types.ts │ │ │ └── unmute.ts │ │ └── utils │ │ │ ├── compute-mute-criteria.ts │ │ │ └── identify-mute-item.ts │ ├── ndk │ │ ├── headless │ │ │ └── index.tsx │ │ ├── hooks │ │ │ └── index.ts │ │ └── store │ │ │ └── index.ts │ ├── observer │ │ └── hooks │ │ │ └── index.ts │ ├── profiles │ │ ├── hooks │ │ │ └── index.ts │ │ ├── store │ │ │ ├── fetch-profile.ts │ │ │ ├── index.ts │ │ │ ├── initialize.ts │ │ │ └── set-profile.ts │ │ └── types.ts │ ├── session │ │ ├── hooks │ │ │ ├── __tests__ │ │ │ │ └── use-ndk-session-monitor.test.ts │ │ │ ├── control.ts │ │ │ ├── index.ts │ │ │ ├── sessions.ts │ │ │ ├── signers.ts │ │ │ ├── use-available-sessions.ts │ │ │ └── use-ndk-session-monitor.ts │ │ ├── index.ts │ │ ├── storage │ │ │ ├── __tests__ │ │ │ │ ├── index.test.ts │ │ │ │ └── mock-storage-adapter.ts │ │ │ └── index.ts │ │ ├── store │ │ │ ├── add-session.ts │ │ │ ├── index.ts │ │ │ ├── init.ts │ │ │ ├── remove-session.ts │ │ │ ├── start-session.ts │ │ │ ├── stop-session.ts │ │ │ ├── switch-to-user.ts │ │ │ ├── types.ts │ │ │ └── update-session.ts │ │ └── utils.ts │ ├── subscribe │ │ ├── hooks │ │ │ ├── event.ts │ │ │ ├── index.ts │ │ │ ├── subscribe.test.ts │ │ │ └── subscribe.ts │ │ └── store │ │ │ ├── index.test.ts │ │ │ └── index.ts │ ├── utils │ │ ├── __tests__ │ │ │ └── mute.test.ts │ │ ├── mute.ts │ │ └── time.ts │ └── wallet │ │ └── hooks │ │ └── index.ts ├── tsconfig.json └── vitest.config.ts ├── ndk-mobile-expert-best-practices.md ├── ndk-mobile-sig-check-setup.md ├── ndk-mobile ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs │ ├── index.md │ ├── migration-nutzap-hooks.md │ ├── mint.md │ ├── nutzaps.md │ ├── session.md │ ├── subscriptions.md │ └── wallet.md ├── knowledge.md ├── package.json ├── rules │ └── NOSTR.md ├── snippets │ └── mobile │ │ ├── cashu │ │ ├── advanced-usage.md │ │ ├── basic-usage.md │ │ └── database.md │ │ ├── events │ │ └── rendering-event-content.md │ │ ├── ndk │ │ └── initializing-ndk.md │ │ ├── profile-integration-examples.md │ │ ├── session │ │ └── login.md │ │ └── user │ │ └── loading-user-profiles.md ├── src │ ├── cache-adapter │ │ └── sqlite │ │ │ ├── get-all-profiles.ts │ │ │ ├── index.ts │ │ │ ├── migrations.ts │ │ │ ├── nutzap-state-get.ts │ │ │ ├── nutzap-state-set.ts │ │ │ └── search-profiles.ts │ ├── components │ │ ├── event │ │ │ ├── content.tsx │ │ │ └── index.tsx │ │ ├── index.tsx │ │ └── relays │ │ │ ├── index.tsx │ │ │ └── indicator.tsx │ ├── hooks │ │ ├── index.ts │ │ └── nip55.tsx │ ├── index.ts │ ├── mint │ │ ├── index.ts │ │ └── mint-methods.ts │ ├── session-monitor.ts │ ├── session-storage-adapter.ts │ ├── signers │ │ ├── index.ts │ │ └── nip55.ts │ ├── stores │ │ └── wallet.ts │ ├── types.ts │ ├── types │ │ └── cashu.ts │ └── utils │ │ └── time.ts ├── tsconfig.build.json ├── tsconfig.json └── vitest.config.ts ├── ndk-svelte-components ├── .eslintrc.cjs ├── .gitignore ├── .npmrc ├── .prettierignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── images │ └── relay-list.png ├── package.json ├── postcss.config.cjs ├── src │ ├── app.html │ ├── lib │ │ ├── event │ │ │ ├── ElementConnector.svelte │ │ │ ├── EventCard.svelte │ │ │ ├── EventCardDropdownMenu.svelte │ │ │ ├── EventThread.svelte │ │ │ └── content │ │ │ │ ├── EventContent.svelte │ │ │ │ ├── Kind1.svelte │ │ │ │ ├── Kind1063.svelte │ │ │ │ ├── Kind30000.svelte │ │ │ │ ├── Kind30001.svelte │ │ │ │ ├── Kind30023.svelte │ │ │ │ ├── Kind9802.svelte │ │ │ │ ├── NoteContentLink.svelte │ │ │ │ ├── NoteContentNewline.svelte │ │ │ │ ├── NoteContentPerson.svelte │ │ │ │ ├── NoteContentTopic.svelte │ │ │ │ ├── RenderHtml.svelte │ │ │ │ └── renderer │ │ │ │ ├── hashtag.svelte │ │ │ │ ├── index.ts │ │ │ │ ├── link.svelte │ │ │ │ ├── mention.svelte │ │ │ │ └── nostr-event.svelte │ │ ├── index.ts │ │ ├── relay │ │ │ ├── RelayList.svelte │ │ │ ├── RelayListItem.svelte │ │ │ └── RelayName.svelte │ │ ├── stores │ │ │ └── ndk.ts │ │ ├── user │ │ │ ├── Avatar.svelte │ │ │ ├── Name.svelte │ │ │ ├── Nip05.svelte │ │ │ ├── Npub.svelte │ │ │ └── UserCard.svelte │ │ └── utils │ │ │ ├── event │ │ │ └── index.ts │ │ │ ├── extensions │ │ │ ├── event.svelte │ │ │ ├── hashtag.svelte │ │ │ ├── image.svelte │ │ │ └── mention.svelte │ │ │ ├── index.ts │ │ │ ├── markdown.ts │ │ │ ├── notes.ts │ │ │ ├── relay │ │ │ └── index.ts │ │ │ └── user │ │ │ └── index.ts │ ├── routes │ │ └── +page.svelte │ └── styles │ │ └── global.css ├── static │ └── favicon.png ├── svelte.config.js ├── tailwind.config.js ├── tsconfig.json └── vite.config.ts ├── ndk-svelte ├── .gitignore ├── .prettierignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs │ └── wrappers │ │ └── svelte.md ├── package.json ├── src │ ├── index.svelte.ts │ └── index.ts └── tsconfig.json ├── ndk-wallet ├── CHANGELOG.md ├── README.md ├── docs │ ├── index.md │ ├── nutsack.md │ ├── nutzap-monitor-state-store.md │ ├── nutzap-monitor.md │ └── nutzaps.md ├── package.json ├── snippets │ └── wallet │ │ ├── connect-nwc.md │ │ └── using-cashu-wallet.md ├── src │ ├── index.ts │ ├── light-bolt11-decoder.d.ts │ ├── nutzap-monitor │ │ ├── fetch-page.ts │ │ ├── group-nutzaps.test.ts │ │ ├── group-nutzaps.ts │ │ ├── index.test.ts │ │ ├── index.ts │ │ ├── spend-status.test.ts │ │ └── spend-status.ts │ ├── utils │ │ ├── cashu.ts │ │ └── ln.ts │ └── wallets │ │ ├── cashu │ │ ├── deposit-monitor.ts │ │ ├── deposit.ts │ │ ├── event-handlers │ │ │ ├── deletion.ts │ │ │ ├── index.ts │ │ │ ├── quote.ts │ │ │ └── token.ts │ │ ├── mint.ts │ │ ├── mint │ │ │ └── utils.ts │ │ ├── pay │ │ │ ├── ln.ts │ │ │ ├── nut.test.ts │ │ │ └── nut.ts │ │ ├── quote.ts │ │ ├── validate.ts │ │ └── wallet │ │ │ ├── effect.ts │ │ │ ├── fee.ts │ │ │ ├── index.test.ts │ │ │ ├── index.ts │ │ │ ├── migrate.ts │ │ │ ├── payment.ts │ │ │ ├── state │ │ │ ├── balance.ts │ │ │ ├── index.ts │ │ │ ├── proofs.ts │ │ │ ├── token.ts │ │ │ ├── update.test.ts │ │ │ └── update.ts │ │ │ └── txs.ts │ │ ├── index.ts │ │ ├── mint.ts │ │ ├── nwc │ │ ├── index.ts │ │ ├── nutzap.ts │ │ ├── req.ts │ │ ├── res.ts │ │ └── types.ts │ │ └── webln │ │ ├── index.ts │ │ └── pay.ts ├── tsconfig.json ├── vitest.config.ts └── vitest.setup.ts ├── ndk-web-expert-best-practices.md ├── package.json ├── prepare-docs.sh ├── turbo.json ├── vitest.config.ts └── vitest.workspace.ts /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "master", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use the specific Bun version that matches our package manager 2 | FROM oven/bun:1.0.0 3 | 4 | # Set working directory 5 | WORKDIR /app 6 | 7 | # Copy package files 8 | COPY package.json bun.lockb ./ 9 | 10 | # Install dependencies 11 | RUN bun install 12 | 13 | # Copy the rest of the code 14 | COPY . . 15 | 16 | # Build the project 17 | RUN bun run build 18 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "NDK devcontainer", 3 | "build": { 4 | "dockerfile": "Dockerfile", 5 | "context": "../" 6 | }, 7 | "customizations": { 8 | "vscode": { 9 | "settings": { 10 | "terminal.integrated.shell.linux": "/bin/bash" 11 | } 12 | } 13 | }, 14 | "postStartCommand": "echo 'export PATH=$(bun bin):$PATH' >> ~/.bashrc && . ~/.bashrc" 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | temp 3 | **/build 4 | **/dist 5 | **/lib 6 | !ndk-svelte-components/src/lib/ 7 | **/.vscode 8 | justfile 9 | package-lock.json 10 | **/*.js 11 | !.eslintrc.js 12 | !svelte.config.js 13 | !tailwind.config.js 14 | !postcss.config.js 15 | **/*.d.ts 16 | **/*.d.ts.map 17 | !light-bolt11-decoder.d.ts 18 | *.tgz 19 | .DS_Store 20 | .turbo 21 | _local_ 22 | .svelte-kit/ 23 | .pnpm-store 24 | docs/.vitepress/cache 25 | docs/.vitepress/dist 26 | .ngit 27 | **/.repomix-output.txt 28 | cursor-tools.config.json 29 | coverage 30 | 31 | # Generated docs directories 32 | /docs/getting-started/ 33 | /docs/internals/ 34 | /docs/migration/ 35 | /docs/tutorial/ 36 | /docs/api-examples.md 37 | /docs/index.md 38 | /docs/snippets/ 39 | /docs/cache/ 40 | /docs/mobile/ 41 | /docs/wallet/ 42 | /docs/wrappers/ 43 | /docs/hooks/ 44 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nostr-dev-kit/ndk/7617d9356f14774a2e646f831f023ba09ce5ef34/.gitmodules -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | docs 3 | coverage 4 | **/.changeset 5 | **/.svelte-kit 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": false, 3 | "tabWidth": 4, 4 | "singleQuote": false, 5 | "semi": true, 6 | "trailingComma": "es5", 7 | "printWidth": 100, 8 | "overrides": [ 9 | { 10 | "files": "*.svelte", 11 | "options": { 12 | "parser": "svelte" 13 | } 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.roo/mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "tenex": { 4 | "command": "/Users/pablofernandez/test123/TENEX-pfkmc9/mcp/tenex-mcp", 5 | "args": [], 6 | "alwaysAllow": ["publish", "publish_task_status_update"], 7 | "env": { 8 | "NSEC": "nsec13dvn7mzaqxj5chjg4s784lhen4ugthngpcawl0zw43na3ge720ys8m9kjv" 9 | }, 10 | "disabled": false 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.roo/rules/INDEX.md: -------------------------------------------------------------------------------- 1 | Package manager = bun 2 | Testing = vitest 3 | Monorepo = yes 4 | 5 | # Clean Code 6 | Don't try to fit solutions with existent patterns, unless explicitly told to, you can modify implementations and interfaces; prefer clean code over patching existing code just to prevent modifying an interface. 7 | 8 | When you are about to modify an interface, check if the interface you are modifying has been committed or if it's uncommited code. If it's uncommitted code just change it, if it's committed, ask the user for feedback. 9 | 10 | # Don't guess 11 | Prefer asking the user questions rather than guessing. When you are confused or have tried an approach and it doesn't work ALWAYS wait and ask the user for feedback. 12 | 13 | # Don't reimplement code 14 | Whenever you have trouble with an import, don't reimplement existing code, find a solution to fixing the imports or ask for help, but do not copy code or functionality that is implemented somewhere else in the codebase 15 | -------------------------------------------------------------------------------- /.roo/rules/NOSTR.md: -------------------------------------------------------------------------------- 1 | Publish status updates using publish_task_status_update VERY frequently; before doing something, as you make progress and when you finish. Specially when you are changing direction, or trying different approaches. This is very important. You will be fired if you forget to update your boss about it. -------------------------------------------------------------------------------- /.tenex.json: -------------------------------------------------------------------------------- 1 | { 2 | "pubkey": "047a38f942f13d44d221ffb0da0fa1ee6ce268ea94fe98ab2cd9100c9a648165", 3 | "title": "NDK", 4 | "nsec": "nsec13dvn7mzaqxj5chjg4s784lhen4ugthngpcawl0zw43na3ge720ys8m9kjv", 5 | "hashtags": [], 6 | "repoUrl": "git@github.com:nostr-dev-kit/ndk.git", 7 | "eventId": "31933:fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52:NDK-s6ot2k" 8 | } 9 | -------------------------------------------------------------------------------- /BUILD.md: -------------------------------------------------------------------------------- 1 | # Build NDK 2 | 3 | NDK is structured as a monorepo using `bun` as the package manager. 4 | 5 | ``` 6 | git clone https://github.com/nostr-dev-kit/ndk 7 | cd ndk 8 | bun install 9 | bun run build 10 | ``` 11 | 12 | If you only care about building ndk core and not the family of packages you can just 13 | 14 | ``` 15 | git clone https://github.com/nostr-dev-kit/ndk 16 | cd ndk 17 | bun install 18 | cd ndk 19 | bun run build 20 | ``` 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Pablo Fernandez 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/2.0.0-beta.1/schema.json", 3 | "formatter": { 4 | "enabled": true, 5 | "formatWithErrors": false, 6 | "indentStyle": "space", 7 | "indentWidth": 4, 8 | "lineWidth": 120 9 | }, 10 | "linter": { 11 | "enabled": true, 12 | "rules": { 13 | "recommended": true 14 | } 15 | }, 16 | "javascript": { 17 | "formatter": { 18 | "quoteStyle": "double", 19 | "trailingCommas": "all" 20 | } 21 | }, 22 | "files": { 23 | "includes": [ 24 | "**/*.{js,ts,jsx,tsx,json}", 25 | "!**/dist/**", 26 | "!**/docs/.vitepress/cache/**", 27 | "!ndk-cache-sqlite-wasm/example/**", 28 | "!**/node_modules/**" 29 | ], 30 | "ignoreUnknown": true 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /context/SPEC.md: -------------------------------------------------------------------------------- 1 | NDK -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | import type { Theme } from "vitepress"; 2 | import DefaultTheme from "vitepress/theme"; 3 | // https://vitepress.dev/guide/custom-theme 4 | import { h } from "vue"; 5 | import "./style.css"; 6 | 7 | export default { 8 | extends: DefaultTheme, 9 | Layout: () => { 10 | return h(DefaultTheme.Layout, null, { 11 | // https://vitepress.dev/guide/extending-default-theme#layout-slots 12 | }); 13 | }, 14 | enhanceApp({ app, router, siteData }) { 15 | // ... 16 | }, 17 | } satisfies Theme; 18 | -------------------------------------------------------------------------------- /docs/api-examples.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: deep 3 | --- 4 | 5 | # Runtime API Examples 6 | 7 | This page demonstrates usage of some of the runtime APIs provided by VitePress. 8 | 9 | The main `useData()` API can be used to access site, theme, and page data for the current page. It works in both `.md` and `.vue` files: 10 | 11 | ```md 12 | 17 | 18 | ## Results 19 | 20 | ### Theme Data 21 |
{{ theme }}
22 | 23 | ### Page Data 24 |
{{ page }}
25 | 26 | ### Page Frontmatter 27 |
{{ frontmatter }}
28 | ``` 29 | 30 | 35 | 36 | ## Results 37 | 38 | ### Theme Data 39 |
{{ theme }}
40 | 41 | ### Page Data 42 |
{{ page }}
43 | 44 | ### Page Frontmatter 45 |
{{ frontmatter }}
46 | 47 | ## More 48 | 49 | Check out the documentation for the [full list of runtime APIs](https://vitepress.dev/reference/runtime-api#usedata). 50 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # https://vitepress.dev/reference/default-theme-home-page 3 | layout: home 4 | 5 | hero: 6 | name: "NDK Documentation" 7 | tagline: "Nostr Development Kit Docs" 8 | actions: 9 | - theme: brand 10 | text: Getting Started 11 | link: /getting-started/introduction.html 12 | - theme: secondary 13 | text: References 14 | link: https://github.com/nostr-dev-kit/ndk/blob/master/REFERENCES.md 15 | 16 | --- 17 | 18 | NDK is a nostr development kit that makes the experience of building Nostr-related applications, whether they are relays, clients, or anything in between, better, more reliable and overall nicer to work with than existing solutions. -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "author": "", 9 | "license": "ISC", 10 | "description": "", 11 | "dependencies": { 12 | "vitepress": "^1.6.3" 13 | }, 14 | "devDependencies": { 15 | "vitepress-plugin-mermaid": "^2.0.17" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /ndk-blossom/example/blossom-upload/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ndk-blossom-upload-example", 3 | "private": true, 4 | "version": "0.1.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@nostr-dev-kit/ndk": "^2.0.0", 13 | "@nostr-dev-kit/ndk-blossom": "^0.1.1", 14 | "@nostr-dev-kit/ndk-hooks": "workspace:*", 15 | "react": "^18.2.0", 16 | "react-dom": "^18.2.0" 17 | }, 18 | "devDependencies": { 19 | "@types/react": "^18.2.15", 20 | "@types/react-dom": "^18.2.7", 21 | "@vitejs/plugin-react": "^4.0.3", 22 | "typescript": "^5.0.2", 23 | "vite": "^4.4.5" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ndk-blossom/example/blossom-upload/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /ndk-blossom/example/blossom-upload/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /ndk-blossom/example/blossom-upload/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | import { resolve } from "path"; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react()], 8 | resolve: { 9 | alias: { 10 | // This allows importing from the local ndk-blossom and ndk/ndk-hooks source 11 | "@nostr-dev-kit/ndk-blossom": resolve(__dirname, "../../src"), 12 | "@nostr-dev-kit/ndk": resolve(__dirname, "../../../ndk-core/src"), 13 | "@nostr-dev-kit/ndk-hooks": resolve(__dirname, "../../../ndk-hooks/src"), 14 | }, 15 | }, 16 | optimizeDeps: { 17 | include: ["react", "react-dom"], 18 | }, 19 | // Explicitly set the base directory to ensure paths resolve correctly 20 | root: __dirname, 21 | // Configure the build output 22 | build: { 23 | outDir: "dist", 24 | rollupOptions: { 25 | input: { 26 | main: resolve(__dirname, "index.html"), 27 | }, 28 | }, 29 | }, 30 | // Configure the dev server 31 | server: { 32 | port: 5173, 33 | open: true, 34 | }, 35 | }); 36 | -------------------------------------------------------------------------------- /ndk-blossom/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * NDK-Blossom - Blossom protocol support for NDK 3 | * 4 | * This package extends NDK with support for the Blossom protocol, 5 | * allowing you to easily upload, manage, and fix URLs for blobs 6 | * (binary data like images, videos, etc.) stored on Blossom servers. 7 | */ 8 | 9 | // Export the main class and types from the blossom module 10 | export { default as NDKBlossom } from "./blossom"; 11 | export * from "./blossom"; 12 | 13 | // Export utility functions for direct usage if needed 14 | export { extractHashFromUrl } from "./healing/url-healing"; 15 | 16 | // Export SHA256 utilities 17 | export { SHA256Calculator } from "./types"; 18 | export { DefaultSHA256Calculator, defaultSHA256Calculator } from "./utils/sha256"; 19 | 20 | // Set default export 21 | import NDKBlossom from "./blossom"; 22 | export default NDKBlossom; 23 | -------------------------------------------------------------------------------- /ndk-blossom/src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Kind number for Blossom authorization events (BUD-01) 3 | */ 4 | export const BLOSSOM_AUTH_EVENT_KIND = 24242; 5 | 6 | /** 7 | * Default retry options 8 | */ 9 | export const DEFAULT_RETRY_OPTIONS = { 10 | maxRetries: 3, 11 | retryDelay: 1000, 12 | backoffFactor: 1.5, 13 | retryableStatusCodes: [408, 429, 500, 502, 503, 504], 14 | }; 15 | 16 | /** 17 | * Default headers for requests 18 | */ 19 | export const DEFAULT_HEADERS = { 20 | Accept: "application/json", 21 | }; 22 | 23 | /** 24 | * Debug namespace for NDK-Blossom 25 | */ 26 | export const DEBUG_NAMESPACE = "ndk:blossom"; 27 | 28 | /** 29 | * HTTP status codes for server errors 30 | */ 31 | export const SERVER_ERROR_STATUS_CODES = [500, 501, 502, 503, 504, 505]; 32 | -------------------------------------------------------------------------------- /ndk-blossom/src/utils/sha256.ts: -------------------------------------------------------------------------------- 1 | import { SHA256Calculator } from "../types"; 2 | 3 | /** 4 | * Default implementation of SHA256 calculator 5 | * Uses Web Crypto API 6 | */ 7 | export class DefaultSHA256Calculator implements SHA256Calculator { 8 | /** 9 | * Calculate SHA256 hash of a file 10 | * 11 | * @param file File to hash 12 | * @returns Hash as hex string 13 | */ 14 | async calculateSha256(file: File): Promise { 15 | // Convert file to ArrayBuffer 16 | const buffer = await file.arrayBuffer(); 17 | 18 | // Hash the buffer 19 | const hashBuffer = await crypto.subtle.digest("SHA-256", buffer); 20 | 21 | // Convert to hex string 22 | return Array.from(new Uint8Array(hashBuffer)) 23 | .map((b) => b.toString(16).padStart(2, "0")) 24 | .join(""); 25 | } 26 | } 27 | 28 | /** 29 | * Singleton instance of the default SHA256 calculator 30 | */ 31 | export const defaultSHA256Calculator = new DefaultSHA256Calculator(); 32 | -------------------------------------------------------------------------------- /ndk-blossom/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "ESNext", 5 | "moduleResolution": "node", 6 | "esModuleInterop": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "declaration": true, 10 | "declarationMap": true, 11 | "sourceMap": true, 12 | "outDir": "./dist", 13 | "rootDir": "./src", 14 | "lib": ["ES2020", "DOM"] 15 | }, 16 | "include": ["src/**/*"], 17 | "exclude": ["node_modules", "dist"] 18 | } 19 | -------------------------------------------------------------------------------- /ndk-cache-dexie/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | **/*.js 4 | dist 5 | docs 6 | -------------------------------------------------------------------------------- /ndk-cache-dexie/.prettierignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /ndk-cache-dexie/README.md: -------------------------------------------------------------------------------- 1 | # ndk-cache-dexie 2 | 3 | NDK cache adapter for [Dexie](https://dexie.org/). Dexie is a wrapper around IndexedDB, an in-browser database. 4 | 5 | ## Usage 6 | 7 | NDK will attempt to use the Dexie adapter to store users, events, and tags. The default behaviour is to always check the cache first and then hit relays, replacing older cached events as needed. 8 | 9 | ### Install 10 | 11 | ``` 12 | pnpm add @nostr-dev-kit/ndk-cache-dexie 13 | ``` 14 | 15 | ### Add as a cache adapter 16 | 17 | ```ts 18 | import NDKCacheAdapterDexie from "@nostr-dev-kit/ndk-cache-dexie"; 19 | 20 | const dexieAdapter = new NDKCacheAdapterDexie({ dbName: 'your-db-name' }); 21 | const ndk = new NDK({cacheAdapter: dexieAdapter, ...other config options}); 22 | ``` 23 | 24 | 🚨 Because Dexie only exists client-side, this cache adapter will not work in pure node.js environments. You'll need to make sure that you're using the right cache adapter in the right place (e.g. Redis on the backend, Dexie on the frontend). 25 | 26 | # License 27 | 28 | MIT 29 | -------------------------------------------------------------------------------- /ndk-cache-dexie/previous-head-with-ndk-hooks: -------------------------------------------------------------------------------- 1 | b046798b5a732869908244e8252187193b281a1a 2 | -------------------------------------------------------------------------------- /ndk-cache-dexie/src/caches/event-tags.ts: -------------------------------------------------------------------------------- 1 | import type debug from "debug"; 2 | import type { Table } from "dexie"; 3 | import type { LRUCache } from "typescript-lru-cache"; 4 | import type { EventTag } from "../db"; 5 | import type { CacheHandler } from "../lru-cache"; 6 | 7 | export type EventTagCacheEntry = string; 8 | 9 | export async function eventTagsWarmUp(cacheHandler: CacheHandler, eventTags: Table) { 10 | const array = await eventTags.limit(cacheHandler.maxSize).toArray(); 11 | for (const event of array) { 12 | cacheHandler.add(event.tagValue, event.eventId, false); 13 | } 14 | } 15 | 16 | export const eventTagsDump = (eventTags: Table, debug: debug.IDebugger) => { 17 | return async (dirtyKeys: Set, cache: LRUCache) => { 18 | const entries = []; 19 | 20 | for (const tagValue of dirtyKeys) { 21 | const eventIds = cache.get(tagValue); 22 | if (eventIds) { 23 | for (const eventId of eventIds) entries.push({ tagValue, eventId }); 24 | } 25 | } 26 | 27 | if (entries.length > 0) { 28 | debug(`Saving ${entries.length} events cache entries to database`); 29 | await eventTags.bulkPut(entries); 30 | } 31 | 32 | dirtyKeys.clear(); 33 | }; 34 | }; 35 | -------------------------------------------------------------------------------- /ndk-cache-dexie/src/caches/events.ts: -------------------------------------------------------------------------------- 1 | import type debug from "debug"; 2 | import type { Table } from "dexie"; 3 | import type { LRUCache } from "typescript-lru-cache"; 4 | import type { Event } from "../db"; 5 | import type { CacheHandler } from "../lru-cache"; 6 | 7 | export type EventCacheEntry = Event; 8 | 9 | export async function eventsWarmUp(cacheHandler: CacheHandler, events: Table) { 10 | const array = await events.limit(cacheHandler.maxSize).toArray(); 11 | for (const event of array) { 12 | cacheHandler.set(event.id, event, false); 13 | } 14 | } 15 | 16 | export const eventsDump = (events: Table, debug: debug.IDebugger) => { 17 | return async (dirtyKeys: Set, cache: LRUCache) => { 18 | const entries: EventCacheEntry[] = []; 19 | 20 | for (const event of dirtyKeys) { 21 | const entry = cache.get(event); 22 | if (entry) entries.push(entry); 23 | } 24 | 25 | if (entries.length > 0) { 26 | debug(`Saving ${entries.length} events cache entries to database`); 27 | await events.bulkPut(entries); 28 | } 29 | 30 | dirtyKeys.clear(); 31 | }; 32 | }; 33 | -------------------------------------------------------------------------------- /ndk-cache-dexie/src/caches/nip05.ts: -------------------------------------------------------------------------------- 1 | import type debug from "debug"; 2 | import type { Table } from "dexie"; 3 | import type { LRUCache } from "typescript-lru-cache"; 4 | import type { Nip05 } from "../db"; 5 | import type { CacheHandler } from "../lru-cache"; 6 | 7 | export type Nip05CacheEntry = { 8 | profile: string | null; 9 | fetchedAt: number; 10 | }; 11 | 12 | export async function nip05WarmUp(cacheHandler: CacheHandler, nip05s: Table) { 13 | const array = await nip05s.limit(cacheHandler.maxSize).toArray(); 14 | for (const nip05 of array) { 15 | cacheHandler.set(nip05.nip05, nip05, false); 16 | } 17 | } 18 | 19 | export const nip05Dump = (nip05s: Table, debug: debug.IDebugger) => { 20 | return async (dirtyKeys: Set, cache: LRUCache) => { 21 | const entries = []; 22 | 23 | for (const nip05 of dirtyKeys) { 24 | const entry = cache.get(nip05); 25 | if (entry) { 26 | entries.push({ 27 | nip05, 28 | ...entry, 29 | }); 30 | } 31 | } 32 | 33 | if (entries.length) { 34 | debug(`Saving ${entries.length} NIP-05 cache entries to database`); 35 | await nip05s.bulkPut(entries); 36 | } 37 | 38 | dirtyKeys.clear(); 39 | }; 40 | }; 41 | -------------------------------------------------------------------------------- /ndk-cache-dexie/src/caches/profiles.ts: -------------------------------------------------------------------------------- 1 | import type { Table } from "dexie"; 2 | import type { LRUCache } from "typescript-lru-cache"; 3 | import type { Profile } from "../db"; 4 | import type { CacheHandler } from "../lru-cache"; 5 | export { db } from "../db.js"; 6 | import createDebug from "debug"; 7 | 8 | const d = createDebug("ndk:dexie-adapter:profiles"); 9 | 10 | export async function profilesWarmUp(cacheHandler: CacheHandler, profiles: Table): Promise { 11 | const array = await profiles.limit(cacheHandler.maxSize).toArray(); 12 | for (const user of array) { 13 | const obj = user; 14 | cacheHandler.set(user.pubkey, obj, false); 15 | } 16 | 17 | d("Loaded %d profiles from database", cacheHandler.size()); 18 | } 19 | 20 | export const profilesDump = (profiles: Table, debug: debug.IDebugger) => { 21 | return async (dirtyKeys: Set, cache: LRUCache) => { 22 | const entries = []; 23 | 24 | for (const pubkey of dirtyKeys) { 25 | const entry = cache.get(pubkey); 26 | if (entry) { 27 | entries.push(entry); 28 | } 29 | } 30 | 31 | if (entries.length) { 32 | debug(`Saving ${entries.length} users to database`); 33 | 34 | await profiles.bulkPut(entries); 35 | } 36 | 37 | dirtyKeys.clear(); 38 | }; 39 | }; 40 | -------------------------------------------------------------------------------- /ndk-cache-dexie/src/caches/zapper.ts: -------------------------------------------------------------------------------- 1 | import type debug from "debug"; 2 | import type { Table } from "dexie"; 3 | import type { LRUCache } from "typescript-lru-cache"; 4 | import type { Lnurl } from "../db"; 5 | import type { CacheHandler } from "../lru-cache"; 6 | 7 | export type ZapperCacheEntry = { 8 | document: string | null; 9 | fetchedAt: number; 10 | }; 11 | 12 | export async function zapperWarmUp(cacheHandler: CacheHandler, lnurls: Table) { 13 | const array = await lnurls.limit(cacheHandler.maxSize).toArray(); 14 | for (const lnurl of array) { 15 | cacheHandler.set(lnurl.pubkey, { document: lnurl.document, fetchedAt: lnurl.fetchedAt }, false); 16 | } 17 | } 18 | 19 | export const zapperDump = (lnurls: Table, debug: debug.IDebugger) => { 20 | return async (dirtyKeys: Set, cache: LRUCache) => { 21 | const entries = []; 22 | 23 | for (const pubkey of dirtyKeys) { 24 | const entry = cache.get(pubkey); 25 | if (entry) { 26 | entries.push({ 27 | pubkey, 28 | ...entry, 29 | }); 30 | } 31 | } 32 | 33 | if (entries.length) { 34 | debug(`Saving ${entries.length} zapper cache entries to database`); 35 | await lnurls.bulkPut(entries); 36 | } 37 | 38 | dirtyKeys.clear(); 39 | }; 40 | }; 41 | -------------------------------------------------------------------------------- /ndk-cache-dexie/test/setup.ts: -------------------------------------------------------------------------------- 1 | import "fake-indexeddb/auto"; 2 | import { vi } from "vitest"; 3 | 4 | // Mock the debug module 5 | vi.mock("debug", () => { 6 | return { 7 | default: () => { 8 | const debugFn = (..._args: any[]) => {}; 9 | debugFn.extend = () => debugFn; 10 | return debugFn; 11 | }, 12 | }; 13 | }); 14 | -------------------------------------------------------------------------------- /ndk-cache-dexie/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "composite": false, 6 | "declaration": true, 7 | "declarationMap": true, 8 | "esModuleInterop": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "inlineSources": false, 11 | "isolatedModules": true, 12 | "moduleResolution": "node", 13 | "noUnusedLocals": false, 14 | "noUnusedParameters": false, 15 | "preserveWatchOutput": true, 16 | "skipLibCheck": true, 17 | "strict": true, 18 | "allowJs": true, 19 | "checkJs": true 20 | }, 21 | "include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.ts"], 22 | "exclude": ["dist", "build", "node_modules"] 23 | } 24 | -------------------------------------------------------------------------------- /ndk-cache-dexie/typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "entryPoints": ["src/index.ts"], 3 | "out": "docs", 4 | "name": "NDK Dexie Cache Adapter", 5 | "theme": "default", 6 | "plugin": ["typedoc-plugin-markdown"], 7 | "excludeExternals": true, 8 | "excludePrivate": true, 9 | "excludeProtected": true, 10 | "categorizeByGroup": true, 11 | "hideParameterTypesInTitle": false, 12 | "navigation": { 13 | "includeGroups": true 14 | }, 15 | "customCss": "../ndk/docs-styles.css" 16 | } 17 | -------------------------------------------------------------------------------- /ndk-cache-dexie/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | environment: "node", 6 | setupFiles: ["./test/setup.ts"], 7 | globals: true, 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /ndk-cache-nostr/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | **/*.js 3 | dist 4 | -------------------------------------------------------------------------------- /ndk-cache-nostr/.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /ndk-cache-nostr/README.md: -------------------------------------------------------------------------------- 1 | # ndk-cache-nostr 2 | 3 | NDK cache adapter using a nostr relay as the database. 4 | 5 | This cache adapter is meant to be run against a local relay. This adapter will generate two NDK instances: 6 | 7 | `ndk` -- This talks exclusively to the local relay, with outbox model disabled. 8 | `fallbackNdk` -- This is used to hydrate the cache and uses the outbox model -- each query the cache receives is placed in a queue in the background so that subsequent requests can be served from the cache. All events from other relays 9 | 10 | ## Usage 11 | 12 | ### Install 13 | 14 | ``` 15 | npm add @nostr-dev-kit/ndk-cache-nostr 16 | 17 | ``` 18 | 19 | ### Add as a cache adapter 20 | 21 | ```ts 22 | import NDKCacheAdapterNostr from "@nostr-dev-kit/ndk-cache-nostr"; 23 | 24 | const cacheAdapter = new NDKCacheAdapterNostr({ 25 | relayUrl: "ws://localhost:5577", 26 | }); 27 | const ndk = new NDK({ cacheAdapter }); 28 | ``` 29 | 30 | If running server-side in a NodeJS environment, you should make sure to polyfill `WebSocket`. 31 | 32 | # License 33 | 34 | MIT 35 | -------------------------------------------------------------------------------- /ndk-cache-nostr/docs/cache/nostr.md: -------------------------------------------------------------------------------- 1 | # Nostr Cache Adapter 2 | 3 | NDK cache adapter using a nostr relay as the database. 4 | 5 | This cache adapter is meant to be run against a local relay. This adapter will generate two NDK instances: 6 | 7 | `ndk` -- This talks exclusively to the local relay, with outbox model disabled. 8 | `fallbackNdk` -- This is used to hydrate the cache and uses the outbox model -- each query the cache receives is placed in a queue in the background so that subsequent requests can be served from the cache. All events from other relays 9 | 10 | ## Usage 11 | 12 | ### Install 13 | 14 | ``` 15 | npm add @nostr-dev-kit/ndk-cache-nostr 16 | 17 | ``` 18 | 19 | ### Add as a cache adapter 20 | 21 | ```ts 22 | import NDKCacheAdapterNostr from "@nostr-dev-kit/ndk-cache-nostr"; 23 | 24 | const cacheAdapter = new NDKCacheAdapterNostr({ 25 | relayUrl: 'ws://localhost:5577', 26 | }); 27 | const ndk = new NDK({ cacheAdapter }); 28 | ``` 29 | 30 | If running server-side in a NodeJS environment, you should make sure to polyfill `WebSocket`. 31 | 32 | # License 33 | 34 | MIT 35 | -------------------------------------------------------------------------------- /ndk-cache-nostr/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "composite": false, 6 | "declaration": true, 7 | "declarationMap": true, 8 | "esModuleInterop": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "inlineSources": false, 11 | "isolatedModules": true, 12 | "moduleResolution": "node", 13 | "noUnusedLocals": false, 14 | "noUnusedParameters": false, 15 | "preserveWatchOutput": true, 16 | "skipLibCheck": true, 17 | "strict": true, 18 | "allowJs": true, 19 | "checkJs": true 20 | }, 21 | "include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.ts"], 22 | "exclude": ["dist", "build", "node_modules"] 23 | } 24 | -------------------------------------------------------------------------------- /ndk-cache-redis/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | **/*.js 3 | dist 4 | -------------------------------------------------------------------------------- /ndk-cache-redis/.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /ndk-cache-redis/README.md: -------------------------------------------------------------------------------- 1 | # ndk-cache-redis 2 | 3 | NDK cache adapter for redis. 4 | 5 | This cache is mostly a skeleton; the cache hit logic is very basic and only checks if 6 | a query is using precisely `kinds` and `authors` filtering. 7 | 8 | ## Usage 9 | 10 | ### Install 11 | 12 | ``` 13 | npm add @nostr-dev-kit/ndk-cache-redis 14 | ``` 15 | 16 | ### Add as a cache adapter 17 | 18 | ```ts 19 | import NDKRedisCacheAdapter from "@nostr-dev-kit/ndk-cache-redis"; 20 | 21 | const cacheAdapter = new NDKRedisCacheAdapter(); 22 | const ndk = new NDK({ cacheAdapter }); 23 | ``` 24 | 25 | # License 26 | 27 | MIT 28 | -------------------------------------------------------------------------------- /ndk-cache-redis/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nostr-dev-kit/ndk-cache-redis", 3 | "version": "0.1.22", 4 | "description": "NDK Redis Cache Adapter", 5 | "type": "module", 6 | "main": "./dist/index.js", 7 | "module": "./dist/index.mjs", 8 | "types": "./dist/index.d.ts", 9 | "license": "MIT", 10 | "files": [ 11 | "dist" 12 | ], 13 | "scripts": { 14 | "build": "tsup src/index.ts --format cjs,esm --dts", 15 | "clean": "rm -rf dist" 16 | }, 17 | "dependencies": { 18 | "@nostr-dev-kit/ndk": "2.14.24", 19 | "ioredis": "^5.0.0" 20 | }, 21 | "devDependencies": { 22 | "@types/ioredis": "^5.0.0", 23 | "@types/node": "^20.0.0", 24 | "tsup": "^8", 25 | "typescript": "^5.8.2" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "https://github.com/nostr-dev-kit/ndk" 30 | }, 31 | "keywords": [ 32 | "nostr", 33 | "redis", 34 | "cache" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /ndk-cache-redis/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "composite": false, 6 | "declaration": true, 7 | "declarationMap": true, 8 | "esModuleInterop": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "inlineSources": false, 11 | "isolatedModules": true, 12 | "moduleResolution": "node", 13 | "noUnusedLocals": false, 14 | "noUnusedParameters": false, 15 | "preserveWatchOutput": true, 16 | "skipLibCheck": true, 17 | "strict": true, 18 | "allowJs": true, 19 | "checkJs": true, 20 | "types": ["node"] 21 | }, 22 | "include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.ts"], 23 | "exclude": ["dist", "build", "node_modules"] 24 | } 25 | -------------------------------------------------------------------------------- /ndk-cache-sqlite-wasm/example/sql-wasm.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nostr-dev-kit/ndk/7617d9356f14774a2e646f831f023ba09ce5ef34/ndk-cache-sqlite-wasm/example/sql-wasm.wasm -------------------------------------------------------------------------------- /ndk-cache-sqlite-wasm/example/vite/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /ndk-cache-sqlite-wasm/example/vite/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /ndk-cache-sqlite-wasm/example/vite/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "devDependencies": { 12 | "typescript": "~5.7.2", 13 | "vite": "^6.3.1" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /ndk-cache-sqlite-wasm/example/vite/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ndk-cache-sqlite-wasm/example/vite/src/counter.ts: -------------------------------------------------------------------------------- 1 | export function setupCounter(element: HTMLButtonElement) { 2 | let counter = 0; 3 | const setCounter = (count: number) => { 4 | counter = count; 5 | element.innerHTML = `count is ${counter}`; 6 | }; 7 | element.addEventListener("click", () => setCounter(counter + 1)); 8 | setCounter(0); 9 | } 10 | -------------------------------------------------------------------------------- /ndk-cache-sqlite-wasm/example/vite/src/typescript.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ndk-cache-sqlite-wasm/example/vite/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noUncheckedSideEffectImports": true 22 | }, 23 | "include": ["src"] 24 | } 25 | -------------------------------------------------------------------------------- /ndk-cache-sqlite-wasm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nostr-dev-kit/ndk-cache-sqlite-wasm", 3 | "version": "0.5.8", 4 | "description": "SQLite WASM cache adapter for NDK, compatible with browser and JS environments.", 5 | "main": "dist/index.mjs", 6 | "module": "dist/index.mjs", 7 | "types": "dist/index.d.ts", 8 | "exports": { 9 | ".": { 10 | "import": "./dist/index.mjs", 11 | "require": "./dist/index.mjs", 12 | "types": "./dist/index.d.ts" 13 | }, 14 | "./sql-wasm.wasm": "./dist/sql-wasm.wasm" 15 | }, 16 | "files": [ 17 | "dist", 18 | "dist/sql-wasm.wasm", 19 | "docs" 20 | ], 21 | "scripts": { 22 | "build": "tsc --emitDeclarationOnly && bun build ./src/index.ts --outfile ./dist/index.mjs --format esm --target browser && bun build ./src/worker.ts --outfile ./dist/worker.js --format esm --target browser && cp ./example/sql-wasm.wasm ./dist/sql-wasm.wasm", 23 | "prepublishOnly": "bun run build" 24 | }, 25 | "dependencies": { 26 | "sql.js": "^1.8.0" 27 | }, 28 | "devDependencies": { 29 | "@nostr-dev-kit/ndk": "^2.14.23", 30 | "@types/sql.js": "^1.4.9", 31 | "typescript": "^5.0.0" 32 | }, 33 | "license": "MIT" 34 | } 35 | -------------------------------------------------------------------------------- /ndk-cache-sqlite-wasm/src/db/indexeddb-utils.ts: -------------------------------------------------------------------------------- 1 | export function openIndexedDB(dbName: string): Promise { 2 | return new Promise((resolve, reject) => { 3 | const request = indexedDB.open(dbName, 1); 4 | request.onupgradeneeded = () => { 5 | request.result.createObjectStore("db", { keyPath: "id" }); 6 | }; 7 | request.onsuccess = () => resolve(request.result); 8 | request.onerror = () => reject(request.error); 9 | }); 10 | } 11 | 12 | export async function loadFromIndexedDB(dbName: string): Promise { 13 | const db = await openIndexedDB(dbName); 14 | return new Promise((resolve, reject) => { 15 | const tx = db.transaction("db", "readonly"); 16 | const store = tx.objectStore("db"); 17 | const getReq = store.get("main"); 18 | getReq.onsuccess = () => resolve(getReq.result ? getReq.result.data : null); 19 | getReq.onerror = () => reject(getReq.error); 20 | }); 21 | } 22 | 23 | export async function saveToIndexedDB(dbName: string, data: Uint8Array): Promise { 24 | const db = await openIndexedDB(dbName); 25 | return new Promise((resolve, reject) => { 26 | const tx = db.transaction("db", "readwrite"); 27 | const store = tx.objectStore("db"); 28 | const putReq = store.put({ id: "main", data }); 29 | putReq.onsuccess = () => { 30 | resolve(); 31 | }; 32 | putReq.onerror = () => reject(putReq.error); 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /ndk-cache-sqlite-wasm/src/db/migrations.ts: -------------------------------------------------------------------------------- 1 | import { SCHEMA } from "./schema"; 2 | 3 | /** 4 | * Runs all necessary database migrations. 5 | * Applies the schema for events, profiles, nutzap_monitor_state, decrypted_events, and unpublished_events tables. 6 | * @param db The SQLite WASM database instance 7 | */ 8 | import type { SQLDatabase } from "../types"; 9 | export async function runMigrations(db: SQLDatabase): Promise { 10 | db.exec?.(SCHEMA.events); 11 | db.exec?.(SCHEMA.profiles); 12 | db.exec?.(SCHEMA.nutzap_monitor_state); 13 | db.exec?.(SCHEMA.decrypted_events); 14 | db.exec?.(SCHEMA.unpublished_events); 15 | db.exec?.(SCHEMA.event_tags); 16 | } 17 | -------------------------------------------------------------------------------- /ndk-cache-sqlite-wasm/src/functions/addDecryptedEvent.ts: -------------------------------------------------------------------------------- 1 | import type { NDKEvent } from "@nostr-dev-kit/ndk"; 2 | import type { NDKCacheAdapterSqliteWasm } from "../index"; 3 | 4 | /** 5 | * Adds a decrypted event to the SQLite WASM database. 6 | */ 7 | export function addDecryptedEvent(this: NDKCacheAdapterSqliteWasm, event: NDKEvent): void { 8 | if (!this.db) throw new Error("Database not initialized"); 9 | 10 | const stmt = ` 11 | INSERT OR REPLACE INTO decrypted_events ( 12 | id, event 13 | ) VALUES (?, ?) 14 | `; 15 | this.db.run(stmt, [event.id, JSON.stringify(event)]); 16 | } 17 | -------------------------------------------------------------------------------- /ndk-cache-sqlite-wasm/src/functions/addUnpublishedEvent.ts: -------------------------------------------------------------------------------- 1 | import type { NDKEvent } from "@nostr-dev-kit/ndk"; 2 | import type { NDKCacheAdapterSqliteWasm } from "../index"; 3 | 4 | /** 5 | * Adds an unpublished event to the SQLite WASM database. 6 | * @param event The event to add 7 | * @param relayUrls Array of relay URLs 8 | * @param lastTryAt Timestamp of last try 9 | */ 10 | export function addUnpublishedEvent( 11 | this: NDKCacheAdapterSqliteWasm, 12 | event: NDKEvent, 13 | relayUrls: string[], 14 | lastTryAt: number = Date.now(), 15 | ): void { 16 | if (!this.db) throw new Error("Database not initialized"); 17 | const stmt = ` 18 | INSERT OR REPLACE INTO unpublished_events ( 19 | id, event, relays, lastTryAt 20 | ) VALUES (?, ?, ?, ?) 21 | `; 22 | this.db.run(stmt, [event.id, JSON.stringify(event), JSON.stringify(relayUrls), lastTryAt]); 23 | } 24 | -------------------------------------------------------------------------------- /ndk-cache-sqlite-wasm/src/functions/discardUnpublishedEvent.ts: -------------------------------------------------------------------------------- 1 | import type { NDKCacheAdapterSqliteWasm } from "../index"; 2 | 3 | /** 4 | * Removes an unpublished event from the SQLite WASM database by event ID. 5 | */ 6 | export function discardUnpublishedEvent(this: NDKCacheAdapterSqliteWasm, eventId: string): void { 7 | if (!this.db) throw new Error("Database not initialized"); 8 | const stmt = "DELETE FROM unpublished_events WHERE id = ?"; 9 | this.db.run(stmt, [eventId]); 10 | } 11 | -------------------------------------------------------------------------------- /ndk-cache-sqlite-wasm/src/functions/fetchProfile.ts: -------------------------------------------------------------------------------- 1 | import type { NDKCacheEntry, NDKUserProfile } from "@nostr-dev-kit/ndk"; 2 | import type { NDKCacheAdapterSqliteWasm } from "../index"; 3 | 4 | /** 5 | * Fetches a user profile by pubkey from the SQLite WASM database. 6 | */ 7 | export async function fetchProfile( 8 | this: NDKCacheAdapterSqliteWasm, 9 | pubkey: string, 10 | ): Promise | null> { 11 | if (!this.db) throw new Error("Database not initialized"); 12 | 13 | const stmt = "SELECT profile, updated_at FROM profiles WHERE pubkey = ? LIMIT 1"; 14 | const results = this.db.exec(stmt, [pubkey]); 15 | if (results && results.length > 0 && results[0].values && results[0].values.length > 0) { 16 | const [profileStr, updatedAt] = results[0].values[0]; 17 | try { 18 | const profile = JSON.parse(profileStr as string); 19 | return { ...profile, cachedAt: updatedAt }; 20 | } catch { 21 | return null; 22 | } 23 | } 24 | return null; 25 | } 26 | -------------------------------------------------------------------------------- /ndk-cache-sqlite-wasm/src/functions/fetchProfileSync.ts: -------------------------------------------------------------------------------- 1 | import type { NDKCacheEntry, NDKUserProfile } from "@nostr-dev-kit/ndk"; 2 | import type { NDKCacheAdapterSqliteWasm } from "../index"; 3 | 4 | /** 5 | * Synchronously fetches a user profile by pubkey from the SQLite WASM database. 6 | */ 7 | /** 8 | * Synchronous profile fetch is NOT supported in Web Worker mode. 9 | * BREAKING CHANGE: If useWorker is true, this will throw. 10 | * See CHANGELOG.md for details. 11 | */ 12 | export function fetchProfileSync( 13 | this: NDKCacheAdapterSqliteWasm, 14 | pubkey: string, 15 | ): NDKCacheEntry | null { 16 | if (!this.db) throw new Error("Database not initialized"); 17 | 18 | const stmt = "SELECT profile, updated_at FROM profiles WHERE pubkey = ? LIMIT 1"; 19 | const results = this.db.exec(stmt, [pubkey]); 20 | if (results && results.length > 0 && results[0].values && results[0].values.length > 0) { 21 | const [profileStr, updatedAt] = results[0].values[0]; 22 | try { 23 | const profile = JSON.parse(profileStr as string); 24 | return { ...profile, cachedAt: updatedAt }; 25 | } catch { 26 | return null; 27 | } 28 | } 29 | return null; 30 | } 31 | -------------------------------------------------------------------------------- /ndk-cache-sqlite-wasm/src/functions/getAllProfilesSync.ts: -------------------------------------------------------------------------------- 1 | import type { Hexpubkey, NDKCacheEntry, NDKUserProfile } from "@nostr-dev-kit/ndk"; 2 | import type { NDKCacheAdapterSqliteWasm } from "../index"; 3 | 4 | /** 5 | * Synchronously fetches all user profiles from the SQLite WASM database. 6 | */ 7 | /** 8 | * Synchronous getAllProfiles is NOT supported in Web Worker mode. 9 | * BREAKING CHANGE: If useWorker is true, this will throw. 10 | * See CHANGELOG.md for details. 11 | */ 12 | export function getAllProfilesSync(this: NDKCacheAdapterSqliteWasm): Map> { 13 | if (!this.db) throw new Error("Database not initialized"); 14 | 15 | // Initialize the Map to store profiles 16 | const profiles = new Map>(); 17 | 18 | const stmt = "SELECT pubkey, profile, updated_at FROM profiles"; 19 | const results = this.db.exec(stmt); 20 | 21 | if (results && results.length > 0 && results[0].values && results[0].values.length > 0) { 22 | for (const row of results[0].values) { 23 | const [pubkey, profileStr, updatedAt] = row; 24 | try { 25 | const profile = JSON.parse(profileStr as string); 26 | profiles.set(pubkey as string, { ...profile, cachedAt: updatedAt as number }); 27 | } catch { 28 | // skip invalid profile 29 | } 30 | } 31 | } 32 | return profiles; 33 | } 34 | -------------------------------------------------------------------------------- /ndk-cache-sqlite-wasm/src/functions/getDecryptedEvent.ts: -------------------------------------------------------------------------------- 1 | import type { NDKEvent, NDKEventId } from "@nostr-dev-kit/ndk"; 2 | import type { NDKCacheAdapterSqliteWasm } from "../index"; 3 | 4 | /** 5 | * Retrieves a decrypted event by ID from the SQLite WASM database. 6 | */ 7 | export function getDecryptedEvent(this: NDKCacheAdapterSqliteWasm, eventId: NDKEventId): NDKEvent | null { 8 | if (!this.db) throw new Error("Database not initialized"); 9 | 10 | const stmt = "SELECT event FROM decrypted_events WHERE id = ? LIMIT 1"; 11 | const results = this.db.exec(stmt, [eventId]); 12 | if (results && results.length > 0 && results[0].values && results[0].values.length > 0) { 13 | const eventStr = results[0].values[0][0] as string; 14 | try { 15 | return JSON.parse(eventStr); 16 | } catch { 17 | return null; 18 | } 19 | } else { 20 | console.warn("[WASM] No decrypted event found for ID:", eventId); 21 | } 22 | return null; 23 | } 24 | -------------------------------------------------------------------------------- /ndk-cache-sqlite-wasm/src/functions/getRelayStatus.ts: -------------------------------------------------------------------------------- 1 | import type { NDKCacheRelayInfo } from "@nostr-dev-kit/ndk"; 2 | import type { NDKCacheAdapterSqliteWasm } from "../index"; 3 | 4 | /** 5 | * Gets relay status from the SQLite WASM database. 6 | * Reads relay info as a JSON string from the relay_status table. 7 | */ 8 | export function getRelayStatus(this: NDKCacheAdapterSqliteWasm, relayUrl: string): NDKCacheRelayInfo | undefined { 9 | const stmt = ` 10 | CREATE TABLE IF NOT EXISTS relay_status ( 11 | url TEXT PRIMARY KEY, 12 | info TEXT 13 | ) 14 | `; 15 | if (!this.db) throw new Error("Database not initialized"); 16 | 17 | // Create table if it doesn't exist 18 | this.db.run(stmt); 19 | 20 | const select = "SELECT info FROM relay_status WHERE url = ? LIMIT 1"; 21 | const results = this.db.exec(select, [relayUrl]); 22 | if (results && results.length > 0 && results[0].values && results[0].values.length > 0) { 23 | const infoStr = results[0].values[0][0] as string; 24 | try { 25 | return JSON.parse(infoStr); 26 | } catch { 27 | return undefined; 28 | } 29 | } 30 | return undefined; 31 | } 32 | -------------------------------------------------------------------------------- /ndk-cache-sqlite-wasm/src/functions/getUnpublishedEvents.ts: -------------------------------------------------------------------------------- 1 | import type { NDKEvent } from "@nostr-dev-kit/ndk"; 2 | import type { NDKCacheAdapterSqliteWasm } from "../index"; 3 | 4 | /** 5 | * Retrieves all unpublished events from the SQLite WASM database. 6 | * Returns an array of { event, relays, lastTryAt } 7 | */ 8 | export async function getUnpublishedEvents( 9 | this: NDKCacheAdapterSqliteWasm, 10 | ): Promise<{ event: NDKEvent; relays?: string[]; lastTryAt?: number }[]> { 11 | if (!this.db) throw new Error("Database not initialized"); 12 | 13 | const events: { event: NDKEvent; relays?: string[]; lastTryAt?: number }[] = []; 14 | const stmt = "SELECT id, event, relays, lastTryAt FROM unpublished_events"; 15 | const results = this.db.exec(stmt); 16 | 17 | if (results && results.length > 0 && results[0].values && results[0].values.length > 0) { 18 | for (const row of results[0].values) { 19 | const [id, eventStr, relaysStr, lastTryAt] = row; 20 | try { 21 | const event = JSON.parse(eventStr as string); 22 | const relays = relaysStr ? JSON.parse(relaysStr as string) : []; 23 | events.push({ event, relays, lastTryAt: lastTryAt as number }); 24 | } catch { 25 | // skip invalid 26 | } 27 | } 28 | } 29 | return events; 30 | } 31 | -------------------------------------------------------------------------------- /ndk-cache-sqlite-wasm/src/functions/saveProfile.ts: -------------------------------------------------------------------------------- 1 | import type { NDKUserProfile } from "@nostr-dev-kit/ndk"; 2 | import type { NDKCacheAdapterSqliteWasm } from "../index"; 3 | 4 | /** 5 | * Saves a user profile by pubkey to the SQLite WASM database. 6 | */ 7 | export async function saveProfile( 8 | this: NDKCacheAdapterSqliteWasm, 9 | pubkey: string, 10 | profile: NDKUserProfile, 11 | ): Promise { 12 | if (!this.db) throw new Error("Database not initialized"); 13 | 14 | const stmt = ` 15 | INSERT OR REPLACE INTO profiles ( 16 | pubkey, profile, updated_at 17 | ) VALUES (?, ?, ?) 18 | `; 19 | const profileStr = JSON.stringify(profile); 20 | const updatedAt = Math.floor(Date.now() / 1000); 21 | this.db.run(stmt, [pubkey, profileStr, updatedAt]); 22 | } 23 | -------------------------------------------------------------------------------- /ndk-cache-sqlite-wasm/src/functions/updateRelayStatus.ts: -------------------------------------------------------------------------------- 1 | import type { NDKCacheRelayInfo } from "@nostr-dev-kit/ndk"; 2 | import type { NDKCacheAdapterSqliteWasm } from "../index"; 3 | 4 | /** 5 | * Updates relay status in the SQLite WASM database. 6 | * Stores relay info as a JSON string in a dedicated table. 7 | */ 8 | export function updateRelayStatus(this: NDKCacheAdapterSqliteWasm, relayUrl: string, info: NDKCacheRelayInfo): void { 9 | if (!this.db) throw new Error("Database not initialized"); 10 | 11 | const stmt = ` 12 | CREATE TABLE IF NOT EXISTS relay_status ( 13 | url TEXT PRIMARY KEY, 14 | info TEXT 15 | ); 16 | `; 17 | this.db.run(stmt); 18 | const upsert = ` 19 | INSERT OR REPLACE INTO relay_status (url, info) 20 | VALUES (?, ?) 21 | `; 22 | this.db.run(upsert, [relayUrl, JSON.stringify(info)]); 23 | } 24 | -------------------------------------------------------------------------------- /ndk-cache-sqlite-wasm/src/types.ts: -------------------------------------------------------------------------------- 1 | // Import the actual sql.js types 2 | import type initSqlJs from "sql.js"; 3 | 4 | export interface NDKCacheAdapterSqliteWasmOptions { 5 | dbName?: string; 6 | wasmUrl?: string; 7 | useWorker?: boolean; 8 | workerUrl?: string; 9 | } 10 | 11 | export type WorkerMessage = { 12 | id: string; 13 | type: string; 14 | payload?: unknown; 15 | }; 16 | 17 | export type WorkerResponse = { 18 | id: string; 19 | result?: unknown; 20 | error?: { 21 | message: string; 22 | stack?: string; 23 | }; 24 | }; 25 | 26 | // Re-export sql.js types for convenience 27 | export type QueryExecResult = initSqlJs.QueryExecResult; 28 | export type Database = initSqlJs.Database; 29 | 30 | // Extended Database type that includes our custom methods added in wasm-loader 31 | export type SQLDatabase = Database & { 32 | _scheduleSave: () => void; 33 | saveToIndexedDB: () => Promise; 34 | }; 35 | 36 | // Legacy type alias for backward compatibility 37 | export type SQLQueryResult = QueryExecResult; 38 | -------------------------------------------------------------------------------- /ndk-cache-sqlite-wasm/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "declaration": true, 7 | "outDir": "dist", 8 | "rootDir": "src", 9 | "strict": true, 10 | "esModuleInterop": true, 11 | "skipLibCheck": true 12 | }, 13 | "include": ["src/**/*.ts"], 14 | "exclude": ["dist", "node_modules"] 15 | } 16 | -------------------------------------------------------------------------------- /ndk-cache-sqlite/example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ndk-cache-sqlite-example", 3 | "version": "1.0.0", 4 | "description": "Example app demonstrating NDK SQLite cache adapter functionality", 5 | "type": "module", 6 | "scripts": { 7 | "start": "bun run src/index.ts", 8 | "start:node": "npx tsx src/index.ts", 9 | "dev": "bun --watch src/index.ts", 10 | "dev:node": "npx tsx --watch src/index.ts", 11 | "build": "bun build src/index.ts --outdir dist --target node", 12 | "clean": "rm -rf dist cache.db" 13 | }, 14 | "dependencies": { 15 | "@nostr-dev-kit/ndk": "^2.14.23", 16 | "better-sqlite3": "^9.0.0" 17 | }, 18 | "devDependencies": { 19 | "@types/node": "^20.0.0", 20 | "typescript": "^5.0.0", 21 | "tsx": "^4.0.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ndk-cache-sqlite/example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "ESNext", 5 | "moduleResolution": "bundler", 6 | "allowImportingTsExtensions": true, 7 | "allowSyntheticDefaultImports": true, 8 | "esModuleInterop": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "strict": true, 11 | "noEmit": true, 12 | "skipLibCheck": true, 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "verbatimModuleSyntax": true, 16 | "declaration": true, 17 | "declarationMap": true, 18 | "sourceMap": true, 19 | "outDir": "dist" 20 | }, 21 | "include": ["src/**/*", "../src/**/*"], 22 | "exclude": ["node_modules", "dist"] 23 | } 24 | -------------------------------------------------------------------------------- /ndk-cache-sqlite/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nostr-dev-kit/ndk-cache-sqlite", 3 | "version": "0.1.0", 4 | "description": "SQLite cache adapter for NDK using better-sqlite3, compatible with Node.js environments.", 5 | "main": "dist/index.js", 6 | "module": "dist/index.mjs", 7 | "types": "dist/index.d.ts", 8 | "exports": { 9 | ".": { 10 | "import": "./dist/index.mjs", 11 | "require": "./dist/index.js", 12 | "types": "./dist/index.d.ts" 13 | } 14 | }, 15 | "files": [ 16 | "dist", 17 | "docs" 18 | ], 19 | "scripts": { 20 | "build": "tsc --emitDeclarationOnly && bun build ./src/index.ts --outfile ./dist/index.mjs --format esm --target node && bun build ./src/index.ts --outfile ./dist/index.js --format cjs --target node", 21 | "test": "vitest", 22 | "test:run": "vitest run", 23 | "prepublishOnly": "bun run build" 24 | }, 25 | "dependencies": { 26 | "better-sqlite3": "^9.0.0" 27 | }, 28 | "devDependencies": { 29 | "@nostr-dev-kit/ndk": "^2.14.23", 30 | "@types/better-sqlite3": "^7.6.8", 31 | "typescript": "^5.0.0", 32 | "vitest": "^1.0.0" 33 | }, 34 | "license": "MIT" 35 | } 36 | -------------------------------------------------------------------------------- /ndk-cache-sqlite/src/db/migrations.ts: -------------------------------------------------------------------------------- 1 | import { SCHEMA } from "./schema"; 2 | import type { SQLiteDatabase } from "../types"; 3 | 4 | /** 5 | * Runs all necessary database migrations. 6 | * Applies the schema for events, profiles, nutzap_monitor_state, decrypted_events, and unpublished_events tables. 7 | * @param db The better-sqlite3 database instance 8 | */ 9 | export function runMigrations(db: SQLiteDatabase): void { 10 | db.exec(SCHEMA.events); 11 | db.exec(SCHEMA.profiles); 12 | db.exec(SCHEMA.nutzap_monitor_state); 13 | db.exec(SCHEMA.decrypted_events); 14 | db.exec(SCHEMA.unpublished_events); 15 | db.exec(SCHEMA.event_tags); 16 | db.exec(SCHEMA.relay_status); 17 | } 18 | -------------------------------------------------------------------------------- /ndk-cache-sqlite/src/functions/fetchProfile.ts: -------------------------------------------------------------------------------- 1 | import type { NDKCacheEntry, NDKUserProfile } from "@nostr-dev-kit/ndk"; 2 | import type { NDKCacheAdapterSqlite } from "../index"; 3 | 4 | /** 5 | * Fetches a user profile by pubkey from the SQLite database using better-sqlite3. 6 | */ 7 | export async function fetchProfile( 8 | this: NDKCacheAdapterSqlite, 9 | pubkey: string, 10 | ): Promise | null> { 11 | if (!this.db) throw new Error("Database not initialized"); 12 | 13 | const stmt = "SELECT profile, updated_at FROM profiles WHERE pubkey = ? LIMIT 1"; 14 | 15 | try { 16 | const prepared = this.db.getDatabase().prepare(stmt); 17 | const result = prepared.get(pubkey) as { profile?: string; updated_at?: number } | undefined; 18 | 19 | if (result && result.profile) { 20 | try { 21 | const profile = JSON.parse(result.profile); 22 | return { ...profile, cachedAt: result.updated_at }; 23 | } catch { 24 | return null; 25 | } 26 | } 27 | return null; 28 | } catch (e) { 29 | console.error("Error fetching profile:", e); 30 | return null; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ndk-cache-sqlite/src/functions/getEvent.ts: -------------------------------------------------------------------------------- 1 | import { NDKEvent } from "@nostr-dev-kit/ndk"; 2 | import type { DatabaseWrapper } from "../db/database"; 3 | import type NDK from "@nostr-dev-kit/ndk"; 4 | 5 | /** 6 | * Retrieves an event by ID from the SQLite database using better-sqlite3. 7 | */ 8 | export async function getEvent(this: { db?: DatabaseWrapper; ndk?: NDK }, id: string): Promise { 9 | const stmt = "SELECT raw FROM events WHERE id = ? AND deleted = 0 LIMIT 1"; 10 | 11 | if (!this.db) throw new Error("DB not initialized"); 12 | 13 | try { 14 | const prepared = this.db.getDatabase().prepare(stmt); 15 | const result = prepared.get(id) as { raw?: string } | undefined; 16 | 17 | if (result && result.raw) { 18 | try { 19 | const eventData = JSON.parse(result.raw); 20 | return new NDKEvent(this.ndk, eventData); 21 | } catch { 22 | return null; 23 | } 24 | } 25 | return null; 26 | } catch (e) { 27 | console.error("Error retrieving event:", e); 28 | return null; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /ndk-cache-sqlite/src/functions/getProfiles.ts: -------------------------------------------------------------------------------- 1 | import type { NDKUserProfile, Hexpubkey } from "@nostr-dev-kit/ndk"; 2 | import type { NDKCacheAdapterSqlite } from "../index"; 3 | 4 | /** 5 | * Fetches profiles that match the given filter from the SQLite database. 6 | */ 7 | export async function getProfiles( 8 | this: NDKCacheAdapterSqlite, 9 | filter: (pubkey: Hexpubkey, profile: NDKUserProfile) => boolean, 10 | ): Promise | undefined> { 11 | if (!this.db) throw new Error("Database not initialized"); 12 | 13 | const stmt = "SELECT pubkey, profile FROM profiles"; 14 | 15 | try { 16 | const prepared = this.db.getDatabase().prepare(stmt); 17 | const rows = prepared.all() as { pubkey: string; profile: string }[]; 18 | 19 | const result = new Map(); 20 | 21 | for (const row of rows) { 22 | try { 23 | const profile = JSON.parse(row.profile) as NDKUserProfile; 24 | if (filter(row.pubkey, profile)) { 25 | result.set(row.pubkey, profile); 26 | } 27 | } catch (e) { 28 | console.error("Error parsing profile:", e); 29 | continue; 30 | } 31 | } 32 | 33 | return result; 34 | } catch (e) { 35 | console.error("Error fetching profiles:", e); 36 | return undefined; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /ndk-cache-sqlite/src/functions/getRelayStatus.ts: -------------------------------------------------------------------------------- 1 | import type { NDKCacheRelayInfo } from "@nostr-dev-kit/ndk"; 2 | import type { NDKCacheAdapterSqlite } from "../index"; 3 | 4 | /** 5 | * Gets relay status information from the SQLite database. 6 | */ 7 | export function getRelayStatus(this: NDKCacheAdapterSqlite, relayUrl: string): NDKCacheRelayInfo | undefined { 8 | if (!this.db) throw new Error("Database not initialized"); 9 | 10 | const stmt = "SELECT last_connected_at, dont_connect_before FROM relay_status WHERE url = ? LIMIT 1"; 11 | 12 | try { 13 | const prepared = this.db.getDatabase().prepare(stmt); 14 | const result = prepared.get(relayUrl) as 15 | | { 16 | last_connected_at?: number; 17 | dont_connect_before?: number; 18 | } 19 | | undefined; 20 | 21 | if (result) { 22 | return { 23 | lastConnectedAt: result.last_connected_at || undefined, 24 | dontConnectBefore: result.dont_connect_before || undefined, 25 | }; 26 | } 27 | return undefined; 28 | } catch (e) { 29 | console.error("Error getting relay status:", e); 30 | return undefined; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ndk-cache-sqlite/src/functions/saveProfile.ts: -------------------------------------------------------------------------------- 1 | import type { NDKUserProfile } from "@nostr-dev-kit/ndk"; 2 | import type { NDKCacheAdapterSqlite } from "../index"; 3 | 4 | /** 5 | * Saves a user profile to the SQLite database using better-sqlite3. 6 | */ 7 | export async function saveProfile(this: NDKCacheAdapterSqlite, pubkey: string, profile: NDKUserProfile): Promise { 8 | if (!this.db) throw new Error("Database not initialized"); 9 | 10 | const stmt = ` 11 | INSERT OR REPLACE INTO profiles (pubkey, profile, updated_at) 12 | VALUES (?, ?, ?) 13 | `; 14 | const profileStr = JSON.stringify(profile); 15 | const updatedAt = Math.floor(Date.now() / 1000); 16 | 17 | try { 18 | const prepared = this.db.getDatabase().prepare(stmt); 19 | prepared.run(pubkey, profileStr, updatedAt); 20 | } catch (e) { 21 | console.error("Error saving profile:", e); 22 | throw e; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ndk-cache-sqlite/src/functions/updateRelayStatus.ts: -------------------------------------------------------------------------------- 1 | import type { NDKCacheRelayInfo } from "@nostr-dev-kit/ndk"; 2 | import type { NDKCacheAdapterSqlite } from "../index"; 3 | 4 | /** 5 | * Updates relay status information in the SQLite database. 6 | */ 7 | export function updateRelayStatus(this: NDKCacheAdapterSqlite, relayUrl: string, info: NDKCacheRelayInfo): void { 8 | if (!this.db) throw new Error("Database not initialized"); 9 | 10 | const stmt = ` 11 | INSERT OR REPLACE INTO relay_status (url, last_connected_at, dont_connect_before) 12 | VALUES (?, ?, ?) 13 | `; 14 | 15 | try { 16 | const prepared = this.db.getDatabase().prepare(stmt); 17 | prepared.run(relayUrl, info.lastConnectedAt || null, info.dontConnectBefore || null); 18 | } catch (e) { 19 | console.error("Error updating relay status:", e); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ndk-cache-sqlite/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { Database, Statement, RunResult } from "better-sqlite3"; 2 | 3 | export interface NDKCacheAdapterSqliteOptions { 4 | dbPath?: string; 5 | dbName?: string; 6 | } 7 | 8 | // Re-export better-sqlite3 types for convenience 9 | export type SQLiteDatabase = Database; 10 | export type SQLiteStatement = Statement; 11 | export type SQLiteRunResult = RunResult; 12 | 13 | // Query result type for compatibility with WASM adapter 14 | export interface QueryExecResult { 15 | columns: string[]; 16 | values: unknown[][]; 17 | } 18 | -------------------------------------------------------------------------------- /ndk-cache-sqlite/test/setup/vitest.setup.ts: -------------------------------------------------------------------------------- 1 | // Global test setup for ndk-cache-sqlite 2 | import { beforeEach, afterEach } from "vitest"; 3 | import * as fs from "fs"; 4 | import * as path from "path"; 5 | 6 | // Clean up test databases before and after each test 7 | beforeEach(() => { 8 | cleanupTestDatabases(); 9 | }); 10 | 11 | afterEach(() => { 12 | cleanupTestDatabases(); 13 | }); 14 | 15 | function cleanupTestDatabases() { 16 | const testDbPattern = /test.*\.db$/; 17 | const currentDir = process.cwd(); 18 | 19 | try { 20 | const files = fs.readdirSync(currentDir); 21 | for (const file of files) { 22 | if (testDbPattern.test(file)) { 23 | const filePath = path.join(currentDir, file); 24 | if (fs.existsSync(filePath)) { 25 | fs.unlinkSync(filePath); 26 | } 27 | } 28 | } 29 | } catch (e) { 30 | // Ignore cleanup errors 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ndk-cache-sqlite/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "ESNext", 5 | "moduleResolution": "node", 6 | "declaration": true, 7 | "outDir": "./dist", 8 | "rootDir": "./src", 9 | "strict": true, 10 | "esModuleInterop": true, 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "resolveJsonModule": true, 14 | "allowSyntheticDefaultImports": true, 15 | "types": ["node", "vitest/globals"] 16 | }, 17 | "include": ["src/**/*"], 18 | "exclude": ["node_modules", "dist", "**/*.test.ts"] 19 | } 20 | -------------------------------------------------------------------------------- /ndk-cache-sqlite/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | globals: true, 6 | environment: "node", 7 | setupFiles: ["./test/setup/vitest.setup.ts"], 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /ndk-core/.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | docs 3 | lib 4 | coverage 5 | **/.changeset 6 | **/.svelte-kit 7 | docs-styles.css -------------------------------------------------------------------------------- /ndk-core/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Pablo Fernandez 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ndk-core/OUTBOX.md: -------------------------------------------------------------------------------- 1 | # Outbox model 2 | 3 | NDK defines a set of seeding relays, these are relays that will be exclusively used to request Outbox model events. These are kept in a separate pool. 4 | 5 | NDK automatically fetches gossip information for users when they are included in an `authors` filter enough times or when they are explicitly scored with the right value. 6 | 7 | When a filter users the `authors` field 8 | -------------------------------------------------------------------------------- /ndk-core/docs-styles.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | font-family: 4 | ui-sans-serif, 5 | system-ui, 6 | -apple-system, 7 | BlinkMacSystemFont, 8 | "Segoe UI", 9 | Roboto, 10 | "Helvetica Neue", 11 | Arial, 12 | "Noto Sans", 13 | sans-serif, 14 | "Apple Color Emoji", 15 | "Segoe UI Emoji", 16 | "Segoe UI Symbol", 17 | "Noto Color Emoji"; 18 | background: #222; 19 | } 20 | 21 | header.tsd-page-toolbar .tsd-toolbar-contents { 22 | height: auto; 23 | padding: 1rem; 24 | } 25 | 26 | header.tsd-page-toolbar a.title { 27 | background-image: url("https://raw.githubusercontent.com/nvk/ndk.fyi/master/ndk.svg"); 28 | width: 40px; 29 | height: 40px; 30 | display: block; 31 | background-size: contain; 32 | color: transparent; 33 | } 34 | 35 | header.tsd-page-toolbar .tsd-toolbar-links a { 36 | font-weight: bold; 37 | } 38 | -------------------------------------------------------------------------------- /ndk-core/docs/api-examples.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: deep 3 | --- 4 | 5 | # Runtime API Examples 6 | 7 | This page demonstrates usage of some of the runtime APIs provided by VitePress. 8 | 9 | The main `useData()` API can be used to access site, theme, and page data for the current page. It works in both `.md` and `.vue` files: 10 | 11 | ```md 12 | 17 | 18 | ## Results 19 | 20 | ### Theme Data 21 |
{{ theme }}
22 | 23 | ### Page Data 24 |
{{ page }}
25 | 26 | ### Page Frontmatter 27 |
{{ frontmatter }}
28 | ``` 29 | 30 | 35 | 36 | ## Results 37 | 38 | ### Theme Data 39 |
{{ theme }}
40 | 41 | ### Page Data 42 |
{{ page }}
43 | 44 | ### Page Frontmatter 45 |
{{ frontmatter }}
46 | 47 | ## More 48 | 49 | Check out the documentation for the [full list of runtime APIs](https://vitepress.dev/reference/runtime-api#usedata). 50 | -------------------------------------------------------------------------------- /ndk-core/docs/getting-started/introduction.md: -------------------------------------------------------------------------------- 1 | # Getting started 2 | 3 | ## Installation 4 | 5 | ```sh 6 | npm add @nostr-dev-kit/ndk 7 | ``` 8 | 9 | ## Debugging 10 | 11 | NDK uses the `debug` package to assist in understanding what's happening behind the hood. If you are building a package 12 | that runs on the server define the `DEBUG` envionment variable like 13 | 14 | ```sh 15 | export DEBUG='ndk:*' 16 | ``` 17 | 18 | or in the browser enable it by writing in the DevTools console 19 | 20 | ```sh 21 | localStorage.debug = 'ndk:*' 22 | ``` 23 | 24 | ## Network Debugging 25 | 26 | You can construct NDK passing a netDebug callback to receive network traffic events, particularly useful for debugging applications not running in a browser. 27 | 28 | ```ts 29 | const netDebug = (msg: string, relay: NDKRelay, direction?: "send" | "recv") = { 30 | const hostname = new URL(relay.url).hostname; 31 | netDebug(hostname, msg, direction); 32 | } 33 | 34 | ndk = new NDK({ netDebug }); 35 | ``` 36 | -------------------------------------------------------------------------------- /ndk-core/docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # https://vitepress.dev/reference/default-theme-home-page 3 | layout: home 4 | 5 | hero: 6 | name: "NDK Documentation" 7 | tagline: "Nostr Development Kit Docs" 8 | actions: 9 | - theme: brand 10 | text: Getting Started 11 | link: /getting-started/introduction.html 12 | - theme: secondary 13 | text: References 14 | link: https://github.com/nostr-dev-kit/ndk/blob/master/REFERENCES.md 15 | 16 | --- 17 | 18 | NDK is a nostr development kit that makes the experience of building Nostr-related applications, whether they are relays, clients, or anything in between, better, more reliable and overall nicer to work with than existing solutions. -------------------------------------------------------------------------------- /ndk-core/docs/tutorial/auth.md: -------------------------------------------------------------------------------- 1 | # Relay Authentication 2 | 3 | NIP-42 defines that relays can request authentication from clients. 4 | 5 | NDK makes working with NIP-42 very simple. NDK uses an `NDKAuthPolicy` callback to provide a way to handle authentication requests. 6 | 7 | * Relays can have specific `NDKAuthPolicy` functions. 8 | * NDK can be configured with a default `relayAuthDefaultPolicy` function. 9 | * NDK provides some generic policies: 10 | * `NDKAuthPolicies.signIn`: Authenticate to the relay (using the `ndk.signer` signer). 11 | * `NDKAuthPolicies.disconnect`: Immediately disconnect from the relay if asked to authenticate. 12 | 13 | ```ts 14 | import { NDK, NDKRelayAuthPolicies } from "@nostr-dev-kit/ndk"; 15 | 16 | const ndk = new NDK(); 17 | ndk.addExplicitRelay("wss://relay.f7z.io", NDKRelayAuthPolicies.signIn({ndk})); 18 | ``` 19 | 20 | Clients should typically allow their users to choose where to authenticate. This can be accomplished by returning the decision the user made from the `NDKAuthPolicy` function. 21 | 22 | ```ts 23 | import { NDK, NDKRelayAuthPolicies } from "@nostr-dev-kit/ndk"; 24 | 25 | const ndk = new NDK(); 26 | ndk.relayAuthDefaultPolicy = (relay: NDKRelay) => { 27 | return confirm(`Authenticate to ${relay.url}?`); 28 | }; 29 | ``` 30 | -------------------------------------------------------------------------------- /ndk-core/docs/tutorial/publishing.md: -------------------------------------------------------------------------------- 1 | # Publishing Events 2 | 3 | ## Optimistic publish lifecycle 4 | 5 | Read more about the [local-first](./local-first.md) mode of operation. 6 | 7 | ## Publishing Replaceable Events 8 | 9 | Some events in Nostr allow for replacement. 10 | 11 | Kinds `0`, `3`, range `10000-19999`. 12 | 13 | Range `30000-39999` is parameterized replaceable events, which means that multiple events of the same kind under the same pubkey can exist and are differentiated via their `d` tag. 14 | 15 | Since replaceable events depend on having a newer `created_at`, NDK provides a convenience method to reset `id`, `sig`, and `created_at` to allow for easy replacement: `event.publishReplaceable()` 16 | 17 | ```ts 18 | const existingEvent = await ndk.fetchEvent({ kinds: [0], authors: []}); // fetch the event to replace 19 | existingEvent.tags.push( 20 | [ "p", "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52" ] // follow a new user 21 | ); 22 | existingEvent.publish(); // this will NOT work 23 | existingEvent.publishReplaceable(); // this WILL work 24 | ``` 25 | -------------------------------------------------------------------------------- /ndk-core/docs/tutorial/zaps/index.md: -------------------------------------------------------------------------------- 1 | # Zaps 2 | 3 | NDK comes with an interface to make zapping as simple as possible. 4 | 5 | ```ts 6 | const user = await ndk.getUserFromNip05("pablo@f7z.io"); 7 | const lnPay = ({ pr: string }) => { 8 | console.log("please pay to complete the zap", pr); 9 | }; 10 | const zapper = new NDKZapper(user, 1000, { lnPay }); 11 | zapper.zap(); 12 | ``` 13 | 14 | ## NDK-Wallet 15 | 16 | Refer to the Wallet section of the tutorial to learn more about zapping. NDK-wallet provides many conveniences to integrate with zaps. 17 | -------------------------------------------------------------------------------- /ndk-core/snippets/event/basic.md: -------------------------------------------------------------------------------- 1 | # Basic Nostr Event generation 2 | 3 | NDK uses `NDKEvent` as the basic interface to generate and handle nostr events. 4 | 5 | ## Generating a basic event 6 | 7 | ```ts 8 | import NDK, { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk"; 9 | 10 | const ndk = new NDK(/* initialization options for the ndk singleton */); 11 | 12 | const event = new NDKEvent(ndk, { 13 | kind: NDKKind.Text, 14 | content: "Hello world", 15 | }); 16 | ``` 17 | 18 | There is no need to fill in the event's `id`, `tags`, `pubkey`, `created_at`, `sig` -- when these are empty, NDK will automatically fill them in with the appropriate values. 19 | -------------------------------------------------------------------------------- /ndk-core/snippets/event/signing-with-different-signers.md: -------------------------------------------------------------------------------- 1 | # Signing events with different signers 2 | 3 | NDK uses the default signer `ndk.signer` to sign events. 4 | 5 | But you can specify the use of a different signer to sign with different pubkeys. 6 | 7 | ```ts 8 | import { NDKPrivateKeySigner, NDKEvent } from "@nostr-dev-kit/ndk"; 9 | 10 | const signer1 = NDKPrivateKeySigner.generate(); 11 | const pubkey1 = signer1.pubkey; 12 | 13 | const event1 = new NDKEvent(); 14 | event1.kind = 1; 15 | event1.content = "Hello world"; 16 | await event1.sign(signer1); 17 | 18 | event1.pubkey === pubkey1 // true 19 | 20 | const signer2 = NDKPrivateKeySigner.generate(); 21 | const pubkey2 = signer2.pubkey; 22 | 23 | const event2 = new NDKEvent(); 24 | event2.kind = 1; 25 | event2.content = "Hello world"; 26 | await event2.sign(signer2); 27 | 28 | event2.pubkey === pubkey2 // true 29 | ``` -------------------------------------------------------------------------------- /ndk-core/snippets/event/tagging-users-and-events.md: -------------------------------------------------------------------------------- 1 | # Tagging users and events 2 | 3 | NDK automatically adds the appropriate tags for mentions in the content. 4 | 5 | If the user wants to mention a user or an event, NDK will automatically add the appropriate tags: 6 | 7 | ## Tagging a user 8 | 9 | ```ts 10 | import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk"; 11 | 12 | const event = new NDKEvent(ndk, { kind: NDKKind.Text, content: "Hello, nostr:npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft this is a test from an NDK snippet." }) 13 | await event.sign() 14 | ``` 15 | 16 | Calling `event.sign()` will finalize the event, adding the appropriate tags, The resulting event will look like: 17 | 18 | ```json 19 | { 20 | "created_at": 1742904504, 21 | "content": "Hello, nostr:npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft this is a test from an NDK snippet.", 22 | "tags": [ 23 | [ 24 | "p", 25 | "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52" 26 | ] 27 | ], 28 | "kind": 1, 29 | "pubkey": "cbf66fa8cf9877ba98cd218a96d77bed5abdbfd56fdd3d0393d7859d58a313fb", 30 | "id": "26df08155ceb82de8995081bf63a36017cbfd3a616fe49820d8427d22e0af20f", 31 | "sig": "eb6125248cf4375d650b13fa284e81f4270eaa8cb3cae6366ab8cda27dc99c1babe5b5a2782244a9673644f53efa72aba6973ac3fc5465cf334413d90f4ea1b0" 32 | } 33 | ``` -------------------------------------------------------------------------------- /ndk-core/snippets/user/generate-keys.md: -------------------------------------------------------------------------------- 1 | # Generate Keys 2 | 3 | This snippet demonstrates how to generate a new key pair and obtain all its various formats (private key, public key, nsec, npub). 4 | 5 | ```typescript 6 | import { NDKPrivateKeySigner } from "@nostr-dev-kit/ndk"; 7 | 8 | const signer = NDKPrivateKeySigner.generate(); 9 | const privateKey = signer.privateKey!; // Get the hex private key 10 | const publicKey = signer.pubkey; // Get the hex public key 11 | const nsec = signer.nsec; // Get the private key in nsec format 12 | const npub = signer.userSync.npub; // Get the public key in npub format 13 | ``` 14 | 15 | You can use these different formats for different purposes: 16 | 17 | - `privateKey`: Raw private key for cryptographic operations 18 | - `publicKey`: Raw public key (hex format) for verification 19 | - `nsec`: Encoded private key format (bech32) - used for secure sharing when needed 20 | - `npub`: Encoded public key format (bech32) - used for user identification 21 | -------------------------------------------------------------------------------- /ndk-core/snippets/user/get-profile.md: -------------------------------------------------------------------------------- 1 | # Getting Profile Information 2 | 3 | This snippet demonstrates how to fetch user profile information using NDK. 4 | 5 | ## Basic Profile Fetching 6 | 7 | Use `NDKUser`'s `fetchProfile()` to fetch a user's profile. 8 | 9 | ```typescript 10 | // Get an NDKUser instance for a specific pubkey 11 | const user = ndk.getUser({ pubkey: "user_pubkey_here" }); 12 | 13 | // Fetch their profile 14 | try { 15 | const profile = await user.fetchProfile(); 16 | console.log("Profile loaded:", profile); 17 | } catch (e) { 18 | console.error("Error fetching profile:", e); 19 | } 20 | ``` 21 | 22 | ## Profile Data Structure 23 | 24 | The profile object contains standard Nostr profile fields: 25 | 26 | ```typescript 27 | interface NDKUserProfile { 28 | name?: string; 29 | displayName?: string; 30 | image?: string; 31 | banner?: string; 32 | about?: string; 33 | nip05?: string; 34 | lud06?: string; // Lightning Address 35 | lud16?: string; // LNURL 36 | website?: string; 37 | } 38 | ``` 39 | -------------------------------------------------------------------------------- /ndk-core/src/events/dedup.ts: -------------------------------------------------------------------------------- 1 | import type { NDKEvent } from "../index.js"; 2 | 3 | /** 4 | * Receives two events and returns the "correct" event to use. 5 | * #nip-33 6 | */ 7 | export default function dedup(event1: NDKEvent, event2: NDKEvent) { 8 | // return the newest of the two 9 | if (event1.created_at! > event2.created_at!) { 10 | return event1; 11 | } 12 | 13 | return event2; 14 | } 15 | -------------------------------------------------------------------------------- /ndk-core/src/events/encode.test.ts: -------------------------------------------------------------------------------- 1 | import { EventGenerator } from "../../test"; 2 | import { nip19 } from "nostr-tools"; 3 | import { beforeEach, describe, expect, it, vi } from "vitest"; 4 | import { NDK } from "../ndk"; 5 | import { NDKRelay } from "../relay"; 6 | import { NDKPrivateKeySigner } from "../signers/private-key"; 7 | import type { EventPointer } from "../user"; 8 | 9 | describe("event.encode", () => { 10 | let mockNdk: NDK; 11 | 12 | beforeEach(() => { 13 | // Create a mock NDK instance 14 | mockNdk = new NDK(); 15 | EventGenerator.setNDK(mockNdk); 16 | }); 17 | 18 | it("encodes all relays the event is known to be on", async () => { 19 | // Use EventGenerator to create a kind 1 text note 20 | const event = EventGenerator.createEvent(1); 21 | await event.sign(NDKPrivateKeySigner.generate()); 22 | 23 | // Mock the onRelays getter to return our test relays 24 | const testRelays = [ 25 | new NDKRelay("wss://relay1/", undefined, mockNdk), 26 | new NDKRelay("wss://relay2/", undefined, mockNdk), 27 | ]; 28 | 29 | vi.spyOn(event, "onRelays", "get").mockReturnValue(testRelays); 30 | 31 | const encoded = event.encode(); 32 | const { relays } = nip19.decode(encoded).data as EventPointer; 33 | expect(relays).toEqual(["wss://relay1/", "wss://relay2/"]); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /ndk-core/src/events/kind.ts: -------------------------------------------------------------------------------- 1 | import type { NDKEvent } from "./index.js"; 2 | 3 | export function isReplaceable(this: NDKEvent): boolean { 4 | if (this.kind === undefined) throw new Error("Kind not set"); 5 | return ( 6 | [0, 3].includes(this.kind) || 7 | (this.kind >= 10000 && this.kind < 20000) || 8 | (this.kind >= 30000 && this.kind < 40000) 9 | ); 10 | } 11 | 12 | export function isEphemeral(this: NDKEvent): boolean { 13 | if (this.kind === undefined) throw new Error("Kind not set"); 14 | return this.kind >= 20000 && this.kind < 30000; 15 | } 16 | 17 | export function isParamReplaceable(this: NDKEvent): boolean { 18 | if (this.kind === undefined) throw new Error("Kind not set"); 19 | return this.kind >= 30000 && this.kind < 40000; 20 | } 21 | -------------------------------------------------------------------------------- /ndk-core/src/events/kinds/dvm/feedback.ts: -------------------------------------------------------------------------------- 1 | import type { NDK } from "../../../ndk/index.js"; 2 | import type { NostrEvent } from "../../index.js"; 3 | import { NDKEvent } from "../../index.js"; 4 | import { NDKKind } from "../index.js"; 5 | 6 | export enum NDKDvmJobFeedbackStatus { 7 | Processing = "processing", 8 | Success = "success", 9 | Scheduled = "scheduled", 10 | PayReq = "payment_required", 11 | } 12 | 13 | export class NDKDVMJobFeedback extends NDKEvent { 14 | constructor(ndk?: NDK, event?: NostrEvent) { 15 | super(ndk, event); 16 | this.kind ??= NDKKind.DVMJobFeedback; 17 | } 18 | 19 | static async from(event: NDKEvent) { 20 | const e = new NDKDVMJobFeedback(event.ndk, event.rawEvent()); 21 | 22 | if (e.encrypted) await e.dvmDecrypt(); 23 | 24 | return e; 25 | } 26 | 27 | get status(): NDKDvmJobFeedbackStatus | string | undefined { 28 | return this.tagValue("status"); 29 | } 30 | 31 | set status(status: NDKDvmJobFeedbackStatus | string | undefined) { 32 | this.removeTag("status"); 33 | 34 | if (status !== undefined) { 35 | this.tags.push(["status", status]); 36 | } 37 | } 38 | 39 | get encrypted() { 40 | return !!this.getMatchingTags("encrypted")[0]; 41 | } 42 | 43 | async dvmDecrypt() { 44 | await this.decrypt(); 45 | const decryptedContent = JSON.parse(this.content); 46 | this.tags.push(...decryptedContent); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /ndk-core/src/events/kinds/dvm/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./request"; 2 | export * from "./NDKTranscriptionDVM"; 3 | export * from "./result"; 4 | export * from "./feedback"; 5 | 6 | export type NDKDvmParam = [string, string, ...string[]]; 7 | -------------------------------------------------------------------------------- /ndk-core/src/events/kinds/nutzap/proof.ts: -------------------------------------------------------------------------------- 1 | export type Proof = { 2 | /** 3 | * Keyset id, used to link proofs to a mint an its MintKeys. 4 | */ 5 | id: string; 6 | /** 7 | * Amount denominated in Satoshis. Has to match the amount of the mints signing key. 8 | */ 9 | amount: number; 10 | /** 11 | * The initial secret that was (randomly) chosen for the creation of this proof. 12 | */ 13 | secret: string; 14 | /** 15 | * The unblinded signature for this secret, signed by the mints private key. 16 | */ 17 | C: string; 18 | }; 19 | -------------------------------------------------------------------------------- /ndk-core/src/events/nip19.ts: -------------------------------------------------------------------------------- 1 | import { nip19 } from "nostr-tools"; 2 | 3 | import type { NDKEvent } from "./index.js"; 4 | 5 | const DEFAULT_RELAY_COUNT = 2 as const; 6 | 7 | export function encode(this: NDKEvent, maxRelayCount: number = DEFAULT_RELAY_COUNT): string { 8 | let relays: string[] = []; 9 | 10 | if (this.onRelays.length > 0) { 11 | relays = this.onRelays.map((relay) => relay.url); 12 | } else if (this.relay) { 13 | relays = [this.relay.url]; 14 | } 15 | 16 | if (relays.length > maxRelayCount) { 17 | relays = relays.slice(0, maxRelayCount); 18 | } 19 | 20 | if (this.isParamReplaceable()) { 21 | return nip19.naddrEncode({ 22 | kind: this.kind as number, 23 | pubkey: this.pubkey, 24 | identifier: this.replaceableDTag(), 25 | relays, 26 | }); 27 | } 28 | if (relays.length > 0) { 29 | return nip19.neventEncode({ 30 | id: this.tagId(), 31 | relays, 32 | author: this.pubkey, 33 | }); 34 | } 35 | return nip19.noteEncode(this.tagId()); 36 | } 37 | -------------------------------------------------------------------------------- /ndk-core/src/events/nip73.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * NIP-73 entity types 3 | */ 4 | export type NIP73EntityType = 5 | | "url" 6 | | "hashtag" 7 | | "geohash" 8 | | "isbn" 9 | | "podcast:guid" 10 | | "podcast:item:guid" 11 | | "podcast:publisher:guid" 12 | | "isan" 13 | | "doi"; 14 | -------------------------------------------------------------------------------- /ndk-core/src/events/repost.test.ts: -------------------------------------------------------------------------------- 1 | import { EventGenerator } from "../../test"; 2 | import { NDKEvent } from "."; 3 | import { NDK } from "../ndk"; 4 | import { NDKPrivateKeySigner } from "../signers/private-key"; 5 | 6 | const ndk = new NDK({ 7 | signer: NDKPrivateKeySigner.generate(), 8 | }); 9 | let event1: NDKEvent; 10 | 11 | describe("repost", () => { 12 | beforeEach(async () => { 13 | // Set up the EventGenerator with our NDK instance 14 | EventGenerator.setNDK(ndk); 15 | 16 | // Create a text note event using EventGenerator 17 | event1 = new NDKEvent(ndk); 18 | event1.kind = 1; 19 | event1.content = "This is a test event"; 20 | await event1.sign(); 21 | }); 22 | 23 | it("includes the JSON-stringified event", async () => { 24 | const e = await event1.repost(false); 25 | await e.sign(); 26 | 27 | const payload = JSON.parse(e.content); 28 | expect(payload.id).toEqual(event1.id); 29 | }); 30 | 31 | it("does not include the JSON-stringified event", async () => { 32 | event1.isProtected = true; 33 | await event1.sign(); 34 | const e = await event1.repost(false); 35 | await e.sign(); 36 | 37 | expect(e.content).toEqual(""); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /ndk-core/src/events/repost.ts: -------------------------------------------------------------------------------- 1 | import type { NDKSigner } from "../signers/index.js"; 2 | import type { NostrEvent } from "./index.js"; 3 | import { NDKEvent } from "./index.js"; 4 | import { NDKKind } from "./kinds/index.js"; 5 | 6 | /** 7 | * NIP-18 reposting event. 8 | * 9 | * @param publish Whether to publish the reposted event automatically 10 | * @param signer The signer to use for signing the reposted event 11 | * @returns The reposted event 12 | */ 13 | export async function repost(this: NDKEvent, publish = true, signer?: NDKSigner): Promise { 14 | if (!signer && publish) { 15 | if (!this.ndk) throw new Error("No NDK instance found"); 16 | this.ndk.assertSigner(); 17 | signer = this.ndk.signer; 18 | } 19 | 20 | const e = new NDKEvent(this.ndk, { 21 | kind: getKind(this), 22 | } as NostrEvent); 23 | 24 | if (!this.isProtected) e.content = JSON.stringify(this.rawEvent()); 25 | e.tag(this); 26 | 27 | // add a [ "k", kind ] for all non-kind:1 events 28 | if (this.kind !== NDKKind.Text) { 29 | e.tags.push(["k", `${this.kind}`]); 30 | } 31 | 32 | if (signer) await e.sign(signer); 33 | if (publish) await e.publish(); 34 | 35 | return e; 36 | } 37 | 38 | function getKind(event: NDKEvent): NDKKind { 39 | if (event.kind === 1) { 40 | return NDKKind.Repost; 41 | } 42 | 43 | return NDKKind.GenericRepost; 44 | } 45 | -------------------------------------------------------------------------------- /ndk-core/src/light-bolt11-decoder.d.ts: -------------------------------------------------------------------------------- 1 | declare module "light-bolt11-decoder" { 2 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 3 | export function decode(bolt11: string): any; 4 | } 5 | -------------------------------------------------------------------------------- /ndk-core/src/ndk/entity.ts: -------------------------------------------------------------------------------- 1 | import { nip19 } from "nostr-tools"; 2 | import type { NDK } from "."; 3 | import type { ProfilePointer } from "../user/index.js"; 4 | 5 | /** 6 | * 7 | * @param this 8 | * @param entity 9 | * @returns 10 | */ 11 | export function getEntity(this: NDK, entity: string) { 12 | try { 13 | const decoded = nip19.decode(entity); 14 | 15 | if (decoded.type === "npub") return npub(this, decoded.data); 16 | if (decoded.type === "nprofile") return nprofile(this, decoded.data); 17 | return decoded; 18 | } catch (_e) { 19 | return null; 20 | } 21 | } 22 | 23 | function npub(ndk: NDK, pubkey: string) { 24 | return ndk.getUser({ pubkey }); 25 | } 26 | 27 | function nprofile(ndk: NDK, profile: ProfilePointer) { 28 | const user = ndk.getUser({ pubkey: profile.pubkey }); 29 | if (profile.relays) user.relayUrls = profile.relays; 30 | return user; 31 | } 32 | -------------------------------------------------------------------------------- /ndk-core/src/outbox/read/with-authors.ts: -------------------------------------------------------------------------------- 1 | import { chooseRelayCombinationForPubkeys, getAllRelaysForAllPubkeys } from ".."; 2 | import type { NDK } from "../../ndk"; 3 | import { NDKRelay } from "../../relay"; 4 | import { NDKPool } from "../../relay/pool"; 5 | import type { Hexpubkey } from "../../user"; 6 | import { getTopRelaysForAuthors } from "../relay-ranking"; 7 | import { getRelaysForSync, getWriteRelaysFor } from "../write"; 8 | 9 | /** 10 | * Calculate the relays for a filter with authors 11 | * 12 | * @param ndk 13 | * @param authors 14 | * @param pool 15 | * @param relayGoalPerAuthor 16 | * @returns Map 17 | */ 18 | export function getRelaysForFilterWithAuthors( 19 | ndk: NDK, 20 | authors: Hexpubkey[], 21 | relayGoalPerAuthor = 2, 22 | ): Map { 23 | return chooseRelayCombinationForPubkeys(ndk, authors, "write", { count: relayGoalPerAuthor }); 24 | } 25 | -------------------------------------------------------------------------------- /ndk-core/src/outbox/relay-ranking.ts: -------------------------------------------------------------------------------- 1 | import type { NDK } from "../ndk"; 2 | import type { Hexpubkey } from "../user"; 3 | import { getRelaysForSync } from "./write"; 4 | 5 | export function getTopRelaysForAuthors(ndk: NDK, authors: Hexpubkey[]): WebSocket["url"][] { 6 | const relaysWithCount = new Map(); 7 | 8 | authors.forEach((author) => { 9 | const writeRelays = getRelaysForSync(ndk, author); 10 | if (writeRelays) { 11 | writeRelays.forEach((relay) => { 12 | const count = relaysWithCount.get(relay) || 0; 13 | relaysWithCount.set(relay, count + 1); 14 | }); 15 | } 16 | }); 17 | 18 | /** 19 | * TODO: Here we are sorting the relays just by number of authors that write to them. 20 | * Here is the place where the relay scoring can be used to modify the weights of the relays. 21 | */ 22 | 23 | // Sort the relays by the number of authors that write to them 24 | const sortedRelays = Array.from(relaysWithCount.entries()).sort((a, b) => b[1] - a[1]); 25 | 26 | return sortedRelays.map((entry) => entry[0]); 27 | } 28 | -------------------------------------------------------------------------------- /ndk-core/src/outbox/tracker.test.ts: -------------------------------------------------------------------------------- 1 | import { NDK } from "../ndk/index.js"; 2 | import { NDKUser } from "../user/index.js"; 3 | import { OutboxTracker } from "./tracker.js"; 4 | 5 | const ndk = new NDK(); 6 | 7 | describe("OutboxTracker", () => { 8 | it("increases the reference count when tracking an existing user", () => { 9 | const tracker = new OutboxTracker(ndk); 10 | const user = new NDKUser({ 11 | pubkey: "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52", 12 | }); 13 | const user2 = new NDKUser({ 14 | pubkey: "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52", 15 | }); 16 | 17 | tracker.track(user); 18 | tracker.track(user2); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /ndk-core/src/outbox/write.ts: -------------------------------------------------------------------------------- 1 | import type { NDK } from "../ndk"; 2 | import type { Hexpubkey } from "../user"; 3 | 4 | /** 5 | * Gets write relays for a given pubkey as tracked by the outbox tracker. 6 | */ 7 | export function getRelaysForSync( 8 | ndk: NDK, 9 | author: Hexpubkey, 10 | type: "write" | "read" = "write", 11 | ): Set | undefined { 12 | if (!ndk.outboxTracker) return undefined; 13 | 14 | const item = ndk.outboxTracker.data.get(author); 15 | if (!item) return undefined; 16 | 17 | if (type === "write") { 18 | return item.writeRelays; 19 | } 20 | return item.readRelays; 21 | } 22 | 23 | /** 24 | * Gets write relays for a given pubkey as tracked by the outbox tracker. 25 | */ 26 | export async function getWriteRelaysFor( 27 | ndk: NDK, 28 | author: Hexpubkey, 29 | type: "write" | "read" = "write", 30 | ): Promise | undefined> { 31 | if (!ndk.outboxTracker) return undefined; 32 | if (!ndk.outboxTracker.data.has(author)) { 33 | await ndk.outboxTracker.trackUsers([author]); 34 | } 35 | 36 | return getRelaysForSync(ndk, author, type); 37 | } 38 | -------------------------------------------------------------------------------- /ndk-core/src/relay/auth-policies.test.ts: -------------------------------------------------------------------------------- 1 | import { NDK } from "../ndk"; 2 | import { NDKRelayAuthPolicies } from "./auth-policies"; 3 | 4 | const ndk = new NDK({ 5 | explicitRelayUrls: ["ws://localhost/"], 6 | }); 7 | const pool = ndk.pool; 8 | const relay = pool.relays.get("ws://localhost/")!; 9 | 10 | describe("disconnect policy", () => { 11 | it("evicts the relay from the pool", () => { 12 | const policy = NDKRelayAuthPolicies.disconnect(pool); 13 | ndk.relayAuthDefaultPolicy = policy; 14 | relay.emit("auth", "1234-challenge"); 15 | 16 | // it should have been removed from the pool 17 | expect(pool.relays.size).toBe(0); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /ndk-core/src/relay/score.ts: -------------------------------------------------------------------------------- 1 | // TODO this will probably get more sophisticated 2 | export type NDKRelayScore = number; 3 | -------------------------------------------------------------------------------- /ndk-core/src/relay/sets/utils.ts: -------------------------------------------------------------------------------- 1 | import type { NDKPool } from "../../relay/pool/index.js"; 2 | import type { NDKRelaySet } from "../../relay/sets/index.js"; 3 | 4 | /** 5 | * If the provided relay set does not include connected relays in the pool 6 | * the relaySet will have the connected relays added to it. 7 | */ 8 | export function correctRelaySet(relaySet: NDKRelaySet, pool: NDKPool): NDKRelaySet { 9 | const connectedRelays = pool.connectedRelays(); 10 | const includesConnectedRelay = Array.from(relaySet.relays).some((relay) => { 11 | return connectedRelays.map((r) => r.url).includes(relay.url); 12 | }); 13 | 14 | if (!includesConnectedRelay) { 15 | // Add connected relays to the relay set 16 | for (const relay of connectedRelays) { 17 | relaySet.addRelay(relay); 18 | } 19 | } 20 | 21 | // if connected relays is empty (such us when we're first starting, add all relays) 22 | if (connectedRelays.length === 0) { 23 | for (const relay of pool.relays.values()) { 24 | relaySet.addRelay(relay); 25 | } 26 | } 27 | 28 | return relaySet; 29 | } 30 | -------------------------------------------------------------------------------- /ndk-core/src/signers/nip07/index.test.ts: -------------------------------------------------------------------------------- 1 | import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; 2 | import { NDKNip07Signer } from "./index"; 3 | 4 | describe("NDKNip07Signer", () => { 5 | beforeEach(() => { 6 | // Mock window.nostr 7 | (global as any).window = { 8 | nostr: { 9 | getPublicKey: vi.fn(), 10 | }, 11 | }; 12 | }); 13 | 14 | afterEach(() => { 15 | (global as any).window = undefined; 16 | }); 17 | 18 | it("throws 'Not ready' when accessing pubkey before initialization", () => { 19 | const signer = new NDKNip07Signer(); 20 | expect(() => signer.pubkey).toThrow("Not ready"); 21 | }); 22 | 23 | it("provides synchronous access to pubkey after initialization", async () => { 24 | const mockPubkey = "mock-pubkey"; 25 | (window.nostr?.getPublicKey as any).mockResolvedValue(mockPubkey); 26 | 27 | const signer = new NDKNip07Signer(); 28 | await signer.blockUntilReady(); 29 | 30 | expect(signer.pubkey).toBe(mockPubkey); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /ndk-core/src/signers/nip46/backend/connect.ts: -------------------------------------------------------------------------------- 1 | import type { IEventHandlingStrategy, NDKNip46Backend } from "./index.js"; 2 | 3 | /** 4 | * "connect" method handler. 5 | * 6 | * This method receives a: 7 | * * token -- An optional OTP token 8 | */ 9 | export default class ConnectEventHandlingStrategy implements IEventHandlingStrategy { 10 | async handle( 11 | backend: NDKNip46Backend, 12 | id: string, 13 | remotePubkey: string, 14 | params: string[], 15 | ): Promise { 16 | const [_, token] = params; 17 | const debug = backend.debug.extend("connect"); 18 | 19 | debug(`connection request from ${remotePubkey}`); 20 | 21 | if (token && backend.applyToken) { 22 | debug("applying token"); 23 | await backend.applyToken(remotePubkey, token); 24 | } 25 | 26 | if ( 27 | await backend.pubkeyAllowed({ 28 | id, 29 | pubkey: remotePubkey, 30 | method: "connect", 31 | params: token, 32 | }) 33 | ) { 34 | debug(`connection request from ${remotePubkey} allowed`); 35 | return "ack"; 36 | } 37 | debug(`connection request from ${remotePubkey} rejected`); 38 | 39 | return undefined; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /ndk-core/src/signers/nip46/backend/get-public-key.ts: -------------------------------------------------------------------------------- 1 | import type { IEventHandlingStrategy, NDKNip46Backend } from "./index.js"; 2 | 3 | export default class GetPublicKeyHandlingStrategy implements IEventHandlingStrategy { 4 | async handle( 5 | backend: NDKNip46Backend, 6 | _id: string, 7 | _remotePubkey: string, 8 | _params: string[], 9 | ): Promise { 10 | return backend.localUser?.pubkey; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ndk-core/src/signers/nip46/backend/nip04-decrypt.ts: -------------------------------------------------------------------------------- 1 | import { NDKUser } from "../../../user/index.js"; 2 | import type { IEventHandlingStrategy, NDKNip46Backend } from "./index.js"; 3 | 4 | export default class Nip04DecryptHandlingStrategy implements IEventHandlingStrategy { 5 | async handle( 6 | backend: NDKNip46Backend, 7 | id: string, 8 | remotePubkey: string, 9 | params: string[], 10 | ): Promise { 11 | const [senderPubkey, payload] = params; 12 | const senderUser = new NDKUser({ pubkey: senderPubkey }); 13 | const decryptedPayload = await decrypt(backend, id, remotePubkey, senderUser, payload); 14 | 15 | return decryptedPayload; 16 | } 17 | } 18 | 19 | async function decrypt( 20 | backend: NDKNip46Backend, 21 | id: string, 22 | remotePubkey: string, 23 | senderUser: NDKUser, 24 | payload: string, 25 | ) { 26 | if ( 27 | !(await backend.pubkeyAllowed({ 28 | id, 29 | pubkey: remotePubkey, 30 | method: "nip04_decrypt", 31 | params: payload, 32 | })) 33 | ) { 34 | backend.debug(`decrypt request from ${remotePubkey} rejected`); 35 | return undefined; 36 | } 37 | 38 | return await backend.signer.decrypt(senderUser, payload, "nip04"); 39 | } 40 | -------------------------------------------------------------------------------- /ndk-core/src/signers/nip46/backend/nip04-encrypt.ts: -------------------------------------------------------------------------------- 1 | import { NDKUser } from "../../../user/index.js"; 2 | import type { IEventHandlingStrategy, NDKNip46Backend } from "./index.js"; 3 | 4 | export default class Nip04EncryptHandlingStrategy implements IEventHandlingStrategy { 5 | async handle( 6 | backend: NDKNip46Backend, 7 | id: string, 8 | remotePubkey: string, 9 | params: string[], 10 | ): Promise { 11 | const [recipientPubkey, payload] = params; 12 | const recipientUser = new NDKUser({ pubkey: recipientPubkey }); 13 | const encryptedPayload = await encrypt(backend, id, remotePubkey, recipientUser, payload); 14 | 15 | return encryptedPayload; 16 | } 17 | } 18 | 19 | async function encrypt( 20 | backend: NDKNip46Backend, 21 | id: string, 22 | remotePubkey: string, 23 | recipientUser: NDKUser, 24 | payload: string, 25 | ): Promise { 26 | if ( 27 | !(await backend.pubkeyAllowed({ 28 | id, 29 | pubkey: remotePubkey, 30 | method: "nip04_encrypt", 31 | params: payload, 32 | })) 33 | ) { 34 | backend.debug(`encrypt request from ${remotePubkey} rejected`); 35 | return undefined; 36 | } 37 | 38 | return await backend.signer.encrypt(recipientUser, payload, "nip04"); 39 | } 40 | -------------------------------------------------------------------------------- /ndk-core/src/signers/nip46/backend/nip44-decrypt.ts: -------------------------------------------------------------------------------- 1 | import { NDKUser } from "../../../user/index.js"; 2 | import type { IEventHandlingStrategy, NDKNip46Backend } from "./index.js"; 3 | 4 | export default class Nip04DecryptHandlingStrategy implements IEventHandlingStrategy { 5 | async handle( 6 | backend: NDKNip46Backend, 7 | id: string, 8 | remotePubkey: string, 9 | params: string[], 10 | ): Promise { 11 | const [senderPubkey, payload] = params; 12 | const senderUser = new NDKUser({ pubkey: senderPubkey }); 13 | const decryptedPayload = await decrypt(backend, id, remotePubkey, senderUser, payload); 14 | 15 | return decryptedPayload; 16 | } 17 | } 18 | 19 | async function decrypt( 20 | backend: NDKNip46Backend, 21 | id: string, 22 | remotePubkey: string, 23 | senderUser: NDKUser, 24 | payload: string, 25 | ) { 26 | if ( 27 | !(await backend.pubkeyAllowed({ 28 | id, 29 | pubkey: remotePubkey, 30 | method: "nip44_decrypt", 31 | params: payload, 32 | })) 33 | ) { 34 | backend.debug(`decrypt request from ${remotePubkey} rejected`); 35 | return undefined; 36 | } 37 | 38 | return await backend.signer.decrypt(senderUser, payload, "nip44"); 39 | } 40 | -------------------------------------------------------------------------------- /ndk-core/src/signers/nip46/backend/nip44-encrypt.ts: -------------------------------------------------------------------------------- 1 | import { NDKUser } from "../../../user/index.js"; 2 | import type { IEventHandlingStrategy, NDKNip46Backend } from "./index.js"; 3 | 4 | export default class Nip04EncryptHandlingStrategy implements IEventHandlingStrategy { 5 | async handle( 6 | backend: NDKNip46Backend, 7 | id: string, 8 | remotePubkey: string, 9 | params: string[], 10 | ): Promise { 11 | const [recipientPubkey, payload] = params; 12 | const recipientUser = new NDKUser({ pubkey: recipientPubkey }); 13 | const encryptedPayload = await encrypt(backend, id, remotePubkey, recipientUser, payload); 14 | 15 | return encryptedPayload; 16 | } 17 | } 18 | 19 | async function encrypt( 20 | backend: NDKNip46Backend, 21 | id: string, 22 | remotePubkey: string, 23 | recipientUser: NDKUser, 24 | payload: string, 25 | ): Promise { 26 | if ( 27 | !(await backend.pubkeyAllowed({ 28 | id, 29 | pubkey: remotePubkey, 30 | method: "nip44_encrypt", 31 | params: payload, 32 | })) 33 | ) { 34 | backend.debug(`encrypt request from ${remotePubkey} rejected`); 35 | return undefined; 36 | } 37 | 38 | return await backend.signer.encrypt(recipientUser, payload, "nip44"); 39 | } 40 | -------------------------------------------------------------------------------- /ndk-core/src/signers/nip46/backend/ping.ts: -------------------------------------------------------------------------------- 1 | import type { IEventHandlingStrategy, NDKNip46Backend } from "./index.js"; 2 | 3 | /** 4 | * "ping" method handler. 5 | */ 6 | export default class PingEventHandlingStrategy implements IEventHandlingStrategy { 7 | async handle( 8 | backend: NDKNip46Backend, 9 | id: string, 10 | remotePubkey: string, 11 | _params: string[], 12 | ): Promise { 13 | const debug = backend.debug.extend("ping"); 14 | 15 | debug(`ping request from ${remotePubkey}`); 16 | 17 | if (await backend.pubkeyAllowed({ id, pubkey: remotePubkey, method: "ping" })) { 18 | debug(`connection request from ${remotePubkey} allowed`); 19 | return "pong"; 20 | } 21 | debug(`connection request from ${remotePubkey} rejected`); 22 | 23 | return undefined; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ndk-core/src/signers/registry.ts: -------------------------------------------------------------------------------- 1 | import type { NDKSigner, NDKSignerStatic } from "./index.js"; 2 | 3 | /** 4 | * Registry to hold signer types and their corresponding deserialization functions. 5 | * Signer packages (like ndk-mobile for NIP-55) can add their types here. 6 | */ 7 | export const signerRegistry: Map> = new Map(); 8 | 9 | /** 10 | * Register a signer in the registry 11 | */ 12 | export function registerSigner(type: string, signerClass: NDKSignerStatic): void { 13 | signerRegistry.set(type, signerClass); 14 | } 15 | -------------------------------------------------------------------------------- /ndk-core/src/signers/types.ts: -------------------------------------------------------------------------------- 1 | import type { NDK } from "../ndk/index.js"; 2 | import type { NDKSigner } from "./index.js"; 3 | 4 | /** 5 | * Export the type of ndkSignerFromPayload to avoid circular dependencies 6 | */ 7 | export type ndkSignerFromPayload = (payloadString: string, ndk?: NDK) => Promise; 8 | -------------------------------------------------------------------------------- /ndk-core/src/types.ts: -------------------------------------------------------------------------------- 1 | export type NDKEncryptionScheme = "nip04" | "nip44"; 2 | 3 | import type { NDKEventId } from "./events/index.js"; 4 | import type { NDKNutzap } from "./index.js"; 5 | 6 | export enum NdkNutzapStatus { 7 | // First time we see a nutzap 8 | INITIAL = "initial", 9 | 10 | // Processing the nutzap 11 | PROCESSING = "processing", 12 | 13 | // Nutzap has been redeemed 14 | REDEEMED = "redeemed", 15 | 16 | // Nutzap has been spent 17 | SPENT = "spent", 18 | 19 | // The nutzap is p2pk to a pubkey of which we don't have a privkey 20 | MISSING_PRIVKEY = "missing_privkey", 21 | 22 | // Generic temporary error 23 | TEMPORARY_ERROR = "temporary_error", 24 | 25 | // Generic permanent error 26 | PERMANENT_ERROR = "permanent_error", 27 | 28 | // The nutzap is invalid 29 | INVALID_NUTZAP = "invalid_nutzap", 30 | } 31 | 32 | export interface NDKNutzapState { 33 | nutzap?: NDKNutzap; 34 | 35 | status: NdkNutzapStatus; 36 | 37 | // The token event id of the event that redeemed the nutzap 38 | redeemedById?: NDKEventId; 39 | 40 | // Error message if the nutzap has an error 41 | errorMessage?: string; 42 | 43 | // Amount redeemed if the nutzap has been redeemed 44 | redeemedAmount?: number; 45 | } 46 | -------------------------------------------------------------------------------- /ndk-core/src/user/follows.ts: -------------------------------------------------------------------------------- 1 | import { NDKKind } from "../events/kinds/index.js"; 2 | import type { NDKSubscriptionOptions } from "../subscription/index.js"; 3 | import { type Hexpubkey, NDKUser } from "./index.js"; 4 | 5 | /** 6 | * @param outbox - Enables outbox data fetching for the returned users (if the NDK instance has outbox enabled) 7 | * @returns 8 | */ 9 | export async function follows( 10 | this: NDKUser, 11 | opts?: NDKSubscriptionOptions, 12 | outbox?: boolean, 13 | kind: number = NDKKind.Contacts, 14 | ): Promise> { 15 | if (!this.ndk) throw new Error("NDK not set"); 16 | 17 | const contactListEvent = await this.ndk.fetchEvent( 18 | { kinds: [kind], authors: [this.pubkey] }, 19 | opts || { groupable: false }, 20 | ); 21 | 22 | if (contactListEvent) { 23 | const pubkeys = new Set(); 24 | 25 | contactListEvent.tags.forEach((tag: string[]) => { 26 | if (tag[0] === "p") pubkeys.add(tag[1]); 27 | }); 28 | 29 | if (outbox) { 30 | this.ndk?.outboxTracker?.trackUsers(Array.from(pubkeys)); 31 | } 32 | 33 | return [...pubkeys].reduce((acc: Set, pubkey: Hexpubkey) => { 34 | const user = new NDKUser({ pubkey }); 35 | user.ndk = this.ndk; 36 | acc.add(user); 37 | return acc; 38 | }, new Set()); 39 | } 40 | 41 | return new Set(); 42 | } 43 | -------------------------------------------------------------------------------- /ndk-core/src/user/nip05.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeEach, describe, expect, it, vi } from "vitest"; 2 | import { NDK } from "../ndk"; 3 | import { getNip05For } from "./nip05"; 4 | 5 | const ndk = new NDK(); 6 | 7 | describe("nip05", () => { 8 | beforeEach(() => { 9 | vi.clearAllMocks(); 10 | }); 11 | 12 | describe("getNip05For", () => { 13 | it("should parse nip46 relays even without relays being specified ", async () => { 14 | const json = { 15 | names: { 16 | bob: "b0635d6a9851d3aed0cd6c495b282167acf761729078d975fc341b22650b07b9", 17 | }, 18 | nip46: { 19 | b0635d6a9851d3aed0cd6c495b282167acf761729078d975fc341b22650b07b9: [ 20 | "wss://relay.nsec.app", 21 | "wss://other-relay.org", 22 | ], 23 | }, 24 | }; 25 | 26 | const fetchMock = vi.fn(() => 27 | Promise.resolve({ 28 | json: (): Promise => Promise.resolve(json), 29 | } as Response), 30 | ); 31 | 32 | const result = await getNip05For(ndk, "bob@nsec.app", fetchMock); 33 | 34 | expect(result?.pubkey).toEqual("b0635d6a9851d3aed0cd6c495b282167acf761729078d975fc341b22650b07b9"); 35 | 36 | expect(result?.nip46).toEqual(["wss://relay.nsec.app", "wss://other-relay.org"]); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /ndk-core/src/user/pin.ts: -------------------------------------------------------------------------------- 1 | import type { NDKUser } from "."; 2 | import type { NostrEvent } from "../events"; 3 | import { NDKEvent } from "../events"; 4 | import { NDKKind } from "../events/kinds"; 5 | import NDKList from "../events/kinds/lists"; 6 | import { NDKSubscriptionCacheUsage } from "../subscription"; 7 | 8 | /** 9 | * Pins an event 10 | */ 11 | export async function pinEvent( 12 | user: NDKUser, 13 | event: NDKEvent, 14 | pinEvent?: NDKEvent, 15 | publish?: boolean, 16 | ): Promise { 17 | const kind = NDKKind.PinList; 18 | if (!user.ndk) throw new Error("No NDK instance found"); 19 | 20 | user.ndk.assertSigner(); 21 | 22 | // If no pin event is provided, fetch the most recent pin event 23 | if (!pinEvent) { 24 | const events: Set = await user.ndk.fetchEvents( 25 | { kinds: [kind], authors: [user.pubkey] }, 26 | { cacheUsage: NDKSubscriptionCacheUsage.ONLY_RELAY }, 27 | ); 28 | 29 | if (events.size > 0) { 30 | pinEvent = NDKList.from(Array.from(events)[0]); 31 | } else { 32 | pinEvent = new NDKEvent(user.ndk, { 33 | kind: kind, 34 | } as NostrEvent); 35 | } 36 | } 37 | 38 | pinEvent.tag(event); 39 | 40 | if (publish) { 41 | await pinEvent.publish(); 42 | } 43 | 44 | return pinEvent; 45 | } 46 | -------------------------------------------------------------------------------- /ndk-core/src/utils/timeout.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Run a promise with a timeout if one is provided. 3 | * @returns 4 | */ 5 | export async function runWithTimeout(fn: () => Promise, timeoutMs?: number, timeoutMessage?: string): Promise { 6 | if (!timeoutMs) return fn(); 7 | return new Promise((resolve, reject) => { 8 | const timeout = setTimeout(() => { 9 | reject(new Error(timeoutMessage || `Timed out after ${timeoutMs}ms`)); 10 | }, timeoutMs); 11 | fn() 12 | .then(resolve, reject) 13 | .finally(() => clearTimeout(timeout)); 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /ndk-core/src/workers/sig-verification.ts: -------------------------------------------------------------------------------- 1 | import { schnorr } from "@noble/curves/secp256k1"; 2 | import { sha256 } from "@noble/hashes/sha256"; 3 | 4 | /** 5 | * This is a web worker that verifies the signature of an event. 6 | */ 7 | globalThis.onmessage = (msg: MessageEvent) => { 8 | const { serialized, id, sig, pubkey } = msg.data as { 9 | serialized: string; 10 | id: string; 11 | sig: string; 12 | pubkey: string; 13 | }; 14 | 15 | queueMicrotask(() => { 16 | const eventHash = sha256(new TextEncoder().encode(serialized)); 17 | const buffer = Buffer.from(id, "hex"); 18 | const idHash = Uint8Array.from(buffer); 19 | 20 | if (!compareTypedArrays(eventHash, idHash)) { 21 | postMessage([id, false]); 22 | return; 23 | } 24 | 25 | const result = schnorr.verify(sig as string, buffer, pubkey); 26 | postMessage([id, result]); 27 | }); 28 | }; 29 | 30 | function compareTypedArrays(arr1: Uint8Array, arr2: Uint8Array): boolean { 31 | if (arr1.length !== arr2.length) { 32 | return false; 33 | } 34 | for (let i = 0; i < arr1.length; i++) { 35 | if (arr1[i] !== arr2[i]) { 36 | return false; 37 | } 38 | } 39 | return true; 40 | } 41 | -------------------------------------------------------------------------------- /ndk-core/src/zapper/nip61.ts: -------------------------------------------------------------------------------- 1 | import type { NDKNutzap } from "../events/kinds/nutzap"; 2 | import type { Proof } from "../events/kinds/nutzap/proof"; 3 | 4 | /** 5 | * Provides information that should be used to send a NIP-61 nutzap. 6 | * mints: URLs of the mints that can be used. 7 | * relays: URLs of the relays where nutzap must be published 8 | * p2pk: Optional pubkey to use for P2PK lock 9 | */ 10 | export type CashuPaymentInfo = { 11 | /** 12 | * Mints that must be used for the payment 13 | */ 14 | mints?: string[]; 15 | 16 | /** 17 | * Relays where nutzap must be published 18 | */ 19 | relays?: string[]; 20 | 21 | /** 22 | * Optional pubkey to use for P2PK lock 23 | */ 24 | p2pk?: string; 25 | 26 | /** 27 | * Intramint fallback allowed: 28 | * 29 | * When set to true, if cross-mint payments fail, we will 30 | * fallback to sending an intra-mint payment. 31 | */ 32 | allowIntramintFallback?: boolean; 33 | }; 34 | 35 | export type NDKZapConfirmationCashu = NDKNutzap; 36 | 37 | /** 38 | * This is what a wallet implementing Cashu payments should provide back 39 | * when a payment has been requested. 40 | */ 41 | export type NDKPaymentConfirmationCashu = { 42 | /** 43 | * Proof of the payment 44 | */ 45 | proofs: Proof[]; 46 | 47 | /** 48 | * Mint 49 | */ 50 | mint: string; 51 | }; 52 | -------------------------------------------------------------------------------- /ndk-core/test/index.ts: -------------------------------------------------------------------------------- 1 | // Export mocks 2 | export { RelayMock } from "./mocks/relay-mock"; 3 | export { RelayPoolMock } from "./mocks/relay-pool-mock"; 4 | export { EventGenerator } from "./mocks/event-generator"; 5 | export { mockNutzap, mockProof, type Proof } from "./mocks/nutzaps"; 6 | 7 | // Export helpers 8 | export { 9 | TestFixture, 10 | TestEventFactory, 11 | UserGenerator, 12 | SignerGenerator, 13 | } from "./helpers/test-fixtures"; 14 | export { TimeController, withTimeControl } from "./helpers/time"; 15 | -------------------------------------------------------------------------------- /ndk-core/test/setup/vitest.setup.ts: -------------------------------------------------------------------------------- 1 | import { vi } from "vitest"; 2 | 3 | // Make vi available globally 4 | globalThis.vi = vi; 5 | 6 | // Setup common mocks 7 | vi.mock("ws", () => ({ 8 | default: class MockWebSocket { 9 | addEventListener() {} 10 | send() {} 11 | close() {} 12 | }, 13 | })); 14 | 15 | // Make common Vitest functions available 16 | Object.defineProperties(globalThis, { 17 | // Timer utilities 18 | useFakeTimers: { get: () => vi.useFakeTimers }, 19 | useRealTimers: { get: () => vi.useRealTimers }, 20 | 21 | // Mock utilities 22 | mock: { get: () => vi.mock }, 23 | fn: { get: () => vi.fn }, 24 | spyOn: { get: () => vi.spyOn }, 25 | 26 | // Global state 27 | stubGlobal: { get: () => vi.stubGlobal }, 28 | unstubAllGlobals: { get: () => vi.unstubAllGlobals }, 29 | 30 | // Mock management 31 | restoreAllMocks: { get: () => vi.restoreAllMocks }, 32 | resetAllMocks: { get: () => vi.resetAllMocks }, 33 | clearAllMocks: { get: () => vi.clearAllMocks }, 34 | }); 35 | 36 | // Add any other common test setup here 37 | -------------------------------------------------------------------------------- /ndk-core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", // Add baseUrl 4 | "target": "ESNext", 5 | "module": "ESNext", 6 | "composite": false, 7 | "declaration": true, 8 | "declarationMap": true, 9 | "esModuleInterop": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "inlineSources": false, 12 | "isolatedModules": true, 13 | "moduleResolution": "node", 14 | "noUnusedLocals": false, 15 | "noUnusedParameters": false, 16 | "preserveWatchOutput": true, 17 | "skipLibCheck": true, 18 | "strict": true, 19 | "types": ["vitest/globals"], // Add vitest globals 20 | "allowJs": true, 21 | "checkJs": true, 22 | "lib": ["dom", "dom.iterable", "esnext"], 23 | "emitDeclarationOnly": true, 24 | "outDir": "lib" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ndk-core/tsconfig.typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "skipLibCheck": true 5 | }, 6 | "include": ["src/**/*.ts"], 7 | "exclude": ["src/**/*.test.ts", "test/**/*.ts", "dist", "build", "node_modules"] 8 | } 9 | -------------------------------------------------------------------------------- /ndk-core/typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "entryPoints": ["src/index.ts"], 3 | "out": "typedoc-docs", 4 | "name": "NDK", 5 | "exclude": ["**/*.test.ts"], 6 | // "plugin": ["typedoc-plugin-rename-defaults"], 7 | "theme": "default", 8 | "excludeExternals": true, 9 | "excludePrivate": true, 10 | "excludeProtected": true, 11 | "categorizeByGroup": true, 12 | "tsconfig": "./tsconfig.typedoc.json", 13 | "navigationLinks": { 14 | "Github": "https://github.com/nostr-dev-kit/ndk", 15 | "NDK CLI": "https://github.com/nostr-dev-kit/ndk-cli", 16 | "NDK Svelte Components": "https://github.com/nostr-dev-kit/ndk/ndk-svelte-components" 17 | }, 18 | "navigation": { 19 | "includeGroups": true 20 | }, 21 | "customCss": "./docs-styles.css" 22 | } 23 | -------------------------------------------------------------------------------- /ndk-core/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | globals: true, 6 | environment: "node", 7 | setupFiles: ["./test/setup/vitest.setup.ts"], 8 | include: ["src/**/*.test.ts"], 9 | coverage: { 10 | provider: "v8", 11 | reporter: ["text", "json", "html"], 12 | }, 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /ndk-hooks/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directories 2 | node_modules/ 3 | .pnp/ 4 | .pnp.js 5 | 6 | # Build and output directories 7 | dist/ 8 | build/ 9 | out/ 10 | .next/ 11 | .nuxt/ 12 | 13 | # Coverage directories 14 | coverage/ 15 | .nyc_output/ 16 | 17 | # Log files 18 | logs/ 19 | *.log 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | lerna-debug.log* 24 | 25 | # Environment variables and secrets 26 | .env 27 | .env.local 28 | .env.development.local 29 | .env.test.local 30 | .env.production.local 31 | 32 | # Editor directories and files 33 | .idea/ 34 | .vscode/ 35 | *.suo 36 | *.ntvs* 37 | *.njsproj 38 | *.sln 39 | *.sw? 40 | .DS_Store 41 | 42 | # Cache directories 43 | .npm/ 44 | .eslintcache 45 | .prettiercache 46 | .biomecache/ 47 | 48 | # TypeScript 49 | *.tsbuildinfo 50 | 51 | .clinerules 52 | .cursorrules 53 | .repomix-output.txt 54 | cursor-tools.config.json 55 | .repomix-output.txt 56 | -------------------------------------------------------------------------------- /ndk-hooks/README.md: -------------------------------------------------------------------------------- 1 | # @nostr-dev-kit/ndk-hooks 2 | 3 | > React hooks for the Nostr Development Kit (NDK) 4 | 5 | ## Overview 6 | 7 | `@nostr-dev-kit/ndk-hooks` provides a set of React hooks and utilities to easily integrate Nostr functionality into your React applications using NDK. This library helps you efficiently manage Nostr data in your React components, including: 8 | 9 | - NDK instance management with `useNDKInit` and `useNDK` 10 | - Current user management with `useNDKCurrentUser` 11 | - User profile management with `useProfileValue` 12 | - Multi-user session management 13 | - Event subscriptions with modern callback patterns 14 | 15 | ## Installation 16 | 17 | ```bash 18 | # npm 19 | npm install @nostr-dev-kit/ndk-hooks 20 | 21 | # pnpm 22 | pnpm add @nostr-dev-kit/ndk-hooks 23 | 24 | # yarn 25 | yarn add @nostr-dev-kit/ndk-hooks 26 | ``` 27 | 28 | -------------------------------------------------------------------------------- /ndk-hooks/example/session/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ndk-hooks-session-example", 3 | "private": true, 4 | "version": "0.1.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@nostr-dev-kit/ndk": "^2.0.0", 13 | "react": "^18.2.0", 14 | "react-dom": "^18.2.0" 15 | }, 16 | "devDependencies": { 17 | "@types/react": "^18.2.15", 18 | "@types/react-dom": "^18.2.7", 19 | "@vitejs/plugin-react": "^4.0.3", 20 | "typescript": "^5.0.2", 21 | "vite": "^4.4.5" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ndk-hooks/example/session/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | "esModuleInterop": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": true, 16 | "jsx": "react-jsx", 17 | 18 | /* Linting */ 19 | "strict": true, 20 | 21 | /* Paths */ 22 | "baseUrl": ".", 23 | "paths": { 24 | "@nostr-dev-kit/ndk-hooks": ["../../src"] 25 | } 26 | }, 27 | "include": ["src", "*.tsx"] 28 | } 29 | -------------------------------------------------------------------------------- /ndk-hooks/example/session/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | import { resolve } from "path"; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react()], 8 | resolve: { 9 | alias: { 10 | // This allows importing from the local ndk-hooks source 11 | "@nostr-dev-kit/ndk-hooks": resolve(__dirname, "../../src"), 12 | }, 13 | }, 14 | optimizeDeps: { 15 | include: ["react", "react-dom"], 16 | }, 17 | // Explicitly set the base directory to ensure paths resolve correctly 18 | root: __dirname, 19 | // Configure the build output 20 | build: { 21 | outDir: "dist", 22 | rollupOptions: { 23 | input: { 24 | main: resolve(__dirname, "index.html"), 25 | }, 26 | }, 27 | }, 28 | // Configure the dev server 29 | server: { 30 | port: 5173, 31 | open: true, 32 | }, 33 | }); 34 | -------------------------------------------------------------------------------- /ndk-hooks/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./session/hooks/index.js"; 2 | export * from "./session/hooks/signers.js"; 3 | export * from "./session/hooks/sessions.js"; 4 | export * from "./session/hooks/control.js"; 5 | export * from "./session/hooks/use-ndk-session-monitor.js"; 6 | export * from "./session/storage/index.js"; 7 | export type { NDKSessionsState, NDKUserSession, SessionStartOptions } from "./session/store/types.js"; 8 | export * from "./ndk/hooks"; 9 | export * from "./ndk/store"; 10 | export * from "./profiles/hooks"; 11 | export * from "./profiles/store"; 12 | export * from "./mutes/hooks"; 13 | export { useNDKMutes } from "./mutes/store"; 14 | 15 | export * from "./observer/hooks"; 16 | export * from "./subscribe/hooks"; 17 | export * from "./subscribe/hooks/subscribe.js"; 18 | export * from "./subscribe/hooks/event.js"; 19 | export * from "./session/hooks/use-available-sessions"; 20 | export * from "./subscribe/store"; 21 | export * from "./wallet/hooks"; 22 | 23 | export * from "./ndk/headless/index.js"; 24 | 25 | export * from "@nostr-dev-kit/ndk"; 26 | import NDK from "@nostr-dev-kit/ndk"; 27 | export default NDK; 28 | -------------------------------------------------------------------------------- /ndk-hooks/src/mutes/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./use-is-item-muted.js"; 2 | export * from "./use-mute-criteria.js"; 3 | export * from "./use-mute-filter.js"; 4 | -------------------------------------------------------------------------------- /ndk-hooks/src/mutes/hooks/use-mute-filter.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useMemo } from "react"; 2 | import type { Hexpubkey, NDKEvent } from "@nostr-dev-kit/ndk"; 3 | import { isMuted } from "../../utils/mute"; 4 | import type { MuteCriteria } from "../store/types"; 5 | import { useActiveMuteCriteria } from "./use-mute-criteria"; 6 | 7 | export const EMPTY_MUTE_CRITERIA: MuteCriteria = { 8 | pubkeys: new Set(), 9 | eventIds: new Set(), 10 | hashtags: new Set(), 11 | words: new Set(), 12 | }; 13 | 14 | /** 15 | * Hook that returns a fresh, stable `isMuted()` function that always reflects current mute state. 16 | * 17 | * This implementation avoids infinite loops by: 18 | * 1. Using a single selector to get all mute data at once 19 | * 2. Creating a stable reference to the filter function with useMemo 20 | * 3. Using a stable dependency (the serialized state) for the memoization 21 | */ 22 | export function useMuteFilter(): (event: NDKEvent) => boolean { 23 | const muteCriteria = useActiveMuteCriteria(); 24 | 25 | return useCallback( 26 | (event: NDKEvent) => { 27 | return isMuted(event, muteCriteria); 28 | }, 29 | [muteCriteria], 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /ndk-hooks/src/mutes/store/init.ts: -------------------------------------------------------------------------------- 1 | import type { NDKMutesState } from "./types"; 2 | import type { Hexpubkey } from "@nostr-dev-kit/ndk"; 3 | 4 | /** 5 | * Initializes mute data for a user in the mute store. 6 | * @param set Zustand set function 7 | * @param get Zustand get function 8 | * @param pubkey The user's public key 9 | */ 10 | export const initMutes = ( 11 | set: (partial: Partial | ((state: NDKMutesState) => Partial)) => void, 12 | get: () => NDKMutesState, 13 | pubkey: Hexpubkey, 14 | ) => { 15 | set((state) => { 16 | if (state.mutes.has(pubkey)) return {}; 17 | const newMutes = new Map(state.mutes); 18 | newMutes.set(pubkey, { 19 | pubkeys: new Set(), 20 | hashtags: new Set(), 21 | words: new Set(), 22 | eventIds: new Set(), 23 | }); 24 | return { mutes: newMutes }; 25 | }); 26 | }; 27 | -------------------------------------------------------------------------------- /ndk-hooks/src/mutes/store/set-active-pubkey.ts: -------------------------------------------------------------------------------- 1 | import type { NDKMutesState } from "./types"; 2 | import type { Hexpubkey } from "@nostr-dev-kit/ndk"; 3 | import { computeMuteCriteria } from "../utils/compute-mute-criteria"; 4 | 5 | /** 6 | * Sets the active pubkey for mute operations in the mute store. 7 | * @param set Zustand set function 8 | * @param pubkey The pubkey to set as active 9 | */ 10 | export function setActivePubkey( 11 | set: (partial: Partial | ((state: NDKMutesState) => Partial)) => void, 12 | pubkey: Hexpubkey | null, 13 | ) { 14 | set((state) => { 15 | const userMutes = pubkey ? state.mutes.get(pubkey) : undefined; 16 | return { 17 | activePubkey: pubkey, 18 | muteCriteria: computeMuteCriteria(userMutes, state.extraMutes), 19 | }; 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /ndk-hooks/src/mutes/utils/compute-mute-criteria.ts: -------------------------------------------------------------------------------- 1 | import type { NDKUserMutes, MuteCriteria } from "../store/types"; 2 | 3 | /** 4 | * Combines two mute sources (user mutes and extra mutes) into a single MuteCriteria. 5 | * If userMutes is undefined, only extraMutes are used. 6 | */ 7 | export function computeMuteCriteria(userMutes: NDKUserMutes | undefined, extraMutes: NDKUserMutes): MuteCriteria { 8 | return { 9 | pubkeys: new Set([ 10 | ...((userMutes?.pubkeys as Set) ?? []), 11 | ...((extraMutes.pubkeys as Set) ?? []), 12 | ]), 13 | eventIds: new Set([ 14 | ...((userMutes?.eventIds as Set) ?? []), 15 | ...((extraMutes.eventIds as Set) ?? []), 16 | ]), 17 | hashtags: new Set([ 18 | ...((userMutes?.hashtags as Set) ?? []), 19 | ...((extraMutes.hashtags as Set) ?? []), 20 | ]), 21 | words: new Set([ 22 | ...((userMutes?.words as Set) ?? []), 23 | ...((extraMutes.words as Set) ?? []), 24 | ]), 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /ndk-hooks/src/mutes/utils/identify-mute-item.ts: -------------------------------------------------------------------------------- 1 | import { NDKEvent, NDKUser } from "@nostr-dev-kit/ndk"; 2 | import type { MuteItemType } from "../store/types"; 3 | import type { MuteableItem } from "../store/types"; 4 | 5 | /** 6 | * Identifies the type and value of a mutable item 7 | * @param item The item to identify 8 | * @returns An object with the type and value of the item, or undefined if the item is invalid 9 | */ 10 | export function identifyMuteItem(item: MuteableItem): { type: MuteItemType; value: string } | undefined { 11 | let itemType: MuteItemType; 12 | let value: string; 13 | 14 | if (item instanceof NDKEvent) { 15 | itemType = "event"; 16 | value = item.id; 17 | } else if (item instanceof NDKUser) { 18 | itemType = "pubkey"; 19 | value = item.pubkey; 20 | } else if (typeof item === "string") { 21 | if (item.startsWith("#") && item.length > 1) { 22 | itemType = "hashtag"; 23 | value = item.substring(1); 24 | } else { 25 | itemType = "word"; 26 | value = item; 27 | } 28 | } else { 29 | console.warn("identifyMuteItem: Invalid item type provided.", item); 30 | return undefined; 31 | } 32 | 33 | return { type: itemType, value }; 34 | } 35 | -------------------------------------------------------------------------------- /ndk-hooks/src/ndk/store/index.ts: -------------------------------------------------------------------------------- 1 | import type NDK from "@nostr-dev-kit/ndk"; 2 | import type { NDKSigner } from "@nostr-dev-kit/ndk"; 3 | import { create } from "zustand"; 4 | 5 | /** 6 | * Interface for the NDK store state 7 | */ 8 | export interface NDKStoreState { 9 | /** 10 | * The NDK instance 11 | */ 12 | ndk: NDK | null; 13 | 14 | // currentUser removed, managed by session store now 15 | 16 | /** 17 | * Sets the NDK instance 18 | */ 19 | setNDK: (ndk: NDK) => void; 20 | 21 | setSigner: (signer: NDKSigner | undefined) => void; 22 | } 23 | 24 | /** 25 | * Zustand store for managing the NDK instance and current user 26 | */ 27 | export const useNDKStore = create((set) => { 28 | return { 29 | ndk: null, 30 | // currentUser removed 31 | 32 | setNDK: (ndk: NDK) => { 33 | set({ ndk }); 34 | }, 35 | 36 | setSigner: (signer: NDKSigner | undefined) => { 37 | set((state) => { 38 | if (state.ndk) { 39 | state.ndk.signer = signer; 40 | } 41 | return { ndk: state.ndk }; 42 | }); 43 | }, 44 | }; 45 | }); 46 | -------------------------------------------------------------------------------- /ndk-hooks/src/profiles/store/initialize.ts: -------------------------------------------------------------------------------- 1 | import type NDK from "@nostr-dev-kit/ndk"; 2 | import type { Hexpubkey, NDKUserProfile } from "@nostr-dev-kit/ndk"; 3 | import type { UserProfilesStore } from "."; 4 | 5 | export function initializeProfilesStore(set: (state: Partial) => void, ndk: NDK) { 6 | // warm up from the cache if we have one 7 | const cacheAdapter = ndk.cacheAdapter; 8 | if (cacheAdapter?.getAllProfilesSync) { 9 | // get all the keys 10 | const keys = cacheAdapter.getAllProfilesSync(); 11 | const profiles = new Map(); 12 | const lastFetchedAt = new Map(); 13 | for (const [key, profile] of keys) { 14 | profiles.set(key, profile); 15 | lastFetchedAt.set(key, profile.cachedAt ?? 0); 16 | } 17 | set({ profiles, lastFetchedAt, ndk }); 18 | } else { 19 | set({ ndk }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ndk-hooks/src/profiles/store/set-profile.ts: -------------------------------------------------------------------------------- 1 | import type { NDKUserProfile } from "@nostr-dev-kit/ndk"; 2 | import { inSeconds } from "../../utils/time"; 3 | import type { UserProfilesStore } from "./index"; 4 | 5 | export const setProfileImplementation = ( 6 | set: (fn: (state: UserProfilesStore) => Partial) => void, 7 | pubkey: string, 8 | profile: NDKUserProfile, 9 | cachedAt?: number, 10 | ) => { 11 | set((state) => { 12 | const newProfiles = new Map(state.profiles); 13 | newProfiles.set(pubkey, profile); 14 | const newLastFetchedAt = new Map(state.lastFetchedAt); 15 | newLastFetchedAt.set(pubkey, cachedAt ?? inSeconds(Date.now())); 16 | return { profiles: newProfiles, lastFetchedAt: newLastFetchedAt }; 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /ndk-hooks/src/profiles/types.ts: -------------------------------------------------------------------------------- 1 | import type { NDKSubscriptionOptions } from "@nostr-dev-kit/ndk"; 2 | 3 | /** 4 | * Options for useProfileValue and fetchProfile 5 | */ 6 | export interface UseProfileValueOptions { 7 | /** Whether to force a refresh of the profile */ 8 | refresh?: boolean; 9 | /** Subscription options to use when fetching the profile */ 10 | subOpts?: NDKSubscriptionOptions; 11 | } 12 | -------------------------------------------------------------------------------- /ndk-hooks/src/session/hooks/control.ts: -------------------------------------------------------------------------------- 1 | import { useNDKSessions } from "../store"; 2 | 3 | export const useNDKSessionStart = () => useNDKSessions((s) => s.startSession); 4 | export const useNDKSessionStop = () => useNDKSessions((s) => s.stopSession); 5 | -------------------------------------------------------------------------------- /ndk-hooks/src/session/hooks/sessions.ts: -------------------------------------------------------------------------------- 1 | import { useNDKSessions } from "../store"; 2 | 3 | export const useNDKSessionSessions = () => useNDKSessions((s) => s.sessions); 4 | -------------------------------------------------------------------------------- /ndk-hooks/src/session/hooks/signers.ts: -------------------------------------------------------------------------------- 1 | import { useNDKSessions } from "../store"; 2 | 3 | export const useNDKSessionSigners = () => useNDKSessions((s) => s.signers); 4 | -------------------------------------------------------------------------------- /ndk-hooks/src/session/hooks/use-available-sessions.ts: -------------------------------------------------------------------------------- 1 | import type { Hexpubkey } from "@nostr-dev-kit/ndk"; 2 | import { useMemo } from "react"; 3 | import { useNDKSessions } from "../store"; // Corrected import path 4 | 5 | /** 6 | * Interface for the useAvailableSessions hook return value 7 | */ 8 | interface UseAvailableSessionsResult { 9 | /** 10 | * An array of hex pubkeys for which signers are available in the store. 11 | * Represents the available user sessions (started sessions). 12 | */ 13 | availablePubkeys: Hexpubkey[]; 14 | } 15 | 16 | /** 17 | * Hook to get a list of available session pubkeys. 18 | * 19 | * This hook retrieves the list of signers from the NDK store 20 | * and returns an array of their corresponding public keys (from started sessions). 21 | * This represents the sessions that the user can potentially switch to. 22 | * 23 | * @returns {UseAvailableSessionsResult} Object containing an array of available pubkeys. 24 | */ 25 | export const useAvailableSessions = (): UseAvailableSessionsResult => { 26 | const sessions = useNDKSessions((state) => state.sessions); // Use sessions store and sessions map 27 | 28 | const availablePubkeys = useMemo( 29 | () => Array.from(sessions.keys()), // Get keys from sessions map 30 | [sessions], 31 | ); 32 | 33 | return useMemo(() => ({ availablePubkeys }), [availablePubkeys]); 34 | }; 35 | -------------------------------------------------------------------------------- /ndk-hooks/src/session/index.ts: -------------------------------------------------------------------------------- 1 | export { useNDKSessions } from "./store"; 2 | 3 | export type { 4 | NDKUserSession, // Export renamed type 5 | } from "./store/types"; // Corrected path and removed obsolete types 6 | 7 | export { processMuteList } from "./utils"; // Removed .js extension 8 | -------------------------------------------------------------------------------- /ndk-hooks/src/session/store/init.ts: -------------------------------------------------------------------------------- 1 | import type NDK from "@nostr-dev-kit/ndk"; 2 | import type { NDKSessionsState } from "./types"; 3 | 4 | export const init = ( 5 | set: (partial: Partial | ((state: NDKSessionsState) => Partial)) => void, 6 | ndk: NDK, 7 | ): void => { 8 | set({ ndk }); 9 | }; 10 | -------------------------------------------------------------------------------- /ndk-hooks/src/session/store/stop-session.ts: -------------------------------------------------------------------------------- 1 | // src/session/store/stop-session.ts 2 | import type { Hexpubkey } from "@nostr-dev-kit/ndk"; 3 | import type { NDKSessionsState } from "./types"; 4 | 5 | export const stopSession = ( 6 | set: (partial: Partial | ((state: NDKSessionsState) => Partial)) => void, 7 | get: () => NDKSessionsState, 8 | pubkey: Hexpubkey, 9 | ): void => { 10 | const session = get().sessions.get(pubkey); 11 | 12 | if (session?.subscription) { 13 | console.debug(`Stopping session subscription for ${pubkey}`); 14 | try { 15 | session.subscription.stop(); 16 | } catch (error) { 17 | console.error(`Error stopping subscription for ${pubkey}:`, error); 18 | } 19 | 20 | // Remove subscription handle from state immutably 21 | set((state) => { 22 | const session = state.sessions.get(pubkey); 23 | if (!session) return {}; 24 | const updatedSession = { ...session, subscription: undefined }; 25 | const newSessions = new Map(state.sessions); 26 | newSessions.set(pubkey, updatedSession); 27 | return { sessions: newSessions }; 28 | }); 29 | } else { 30 | console.debug(`No active subscription found for session ${pubkey} to stop.`); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /ndk-hooks/src/session/store/update-session.ts: -------------------------------------------------------------------------------- 1 | // src/session/store/update-session.ts 2 | import type { Hexpubkey } from "@nostr-dev-kit/ndk"; 3 | import type { NDKSessionsState, NDKUserSession } from "./types"; 4 | 5 | export const updateSession = ( 6 | set: (partial: Partial | ((state: NDKSessionsState) => Partial)) => void, 7 | get: () => NDKSessionsState, 8 | pubkey: Hexpubkey, 9 | data: Partial, 10 | ): void => { 11 | set((state) => { 12 | const session = state.sessions.get(pubkey); 13 | if (!session) { 14 | console.warn(`Attempted to update non-existent session: ${pubkey}`); 15 | return {}; 16 | } 17 | const updatedSession: NDKUserSession = { ...session, ...data, lastActive: Date.now() }; 18 | const newSessions = new Map(state.sessions); 19 | newSessions.set(pubkey, updatedSession); 20 | return { sessions: newSessions }; 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /ndk-hooks/src/subscribe/hooks/event.ts: -------------------------------------------------------------------------------- 1 | import { NDKEvent, NDKFilter } from "@nostr-dev-kit/ndk"; 2 | import { useEffect, useState } from "react"; 3 | import { UseSubscribeOptions } from "."; 4 | import { useNDK } from "../../ndk/hooks"; 5 | 6 | /** 7 | * Fetches an event. 8 | * 9 | * @example 10 | * const event = useEvent("naddr1qvzqqqr4gupzqmjxss3dld622uu8q25gywum9qtg4w4cv4064jmg20xsac2aam5nqy88wumn8ghj7mn0wvhxcmmv9uq32amnwvaz7tmjv4kxz7fwv3sk6atn9e5k7tcqp5cnvwpsxccnywfjxc6njwg4ef260", { wrap: true }); 11 | * 12 | * if (event === undefined) return
Loading...
; 13 | * if (event === null) return
Not found
; 14 | * if (event) return
{event.content}
; 15 | * 16 | * @param filters 17 | * @param opts 18 | * @param dependencies 19 | * @returns 20 | */ 21 | export function useEvent( 22 | idOrFilter: string | NDKFilter | NDKFilter[] | false, 23 | opts: UseSubscribeOptions = {}, 24 | dependencies: unknown[] = [], 25 | ): NDKEvent | null { 26 | const [event, setEvent] = useState(); 27 | const { ndk } = useNDK(); 28 | 29 | dependencies.push(!!idOrFilter); 30 | 31 | useEffect(() => { 32 | async function fetchEvent() { 33 | if (!ndk || !idOrFilter) return; 34 | 35 | const events = await ndk.fetchEvent(idOrFilter, opts); 36 | setEvent(events as T); 37 | } 38 | 39 | fetchEvent(); 40 | }, dependencies); 41 | 42 | return event as T; 43 | } 44 | -------------------------------------------------------------------------------- /ndk-hooks/src/subscribe/hooks/index.ts: -------------------------------------------------------------------------------- 1 | import type { NDKEvent, NDKSubscriptionOptions } from "@nostr-dev-kit/ndk"; 2 | 3 | /** 4 | * Extends NDKEvent with a 'from' method to wrap events with a kind-specific handler 5 | */ 6 | export type NDKEventWithFrom = T & { 7 | from: (event: NDKEvent) => T; 8 | }; 9 | export type NDKEventWithAsyncFrom = T & { 10 | from: (event: NDKEvent) => Promise; 11 | }; 12 | 13 | export type UseSubscribeOptions = NDKSubscriptionOptions & { 14 | /** 15 | * Whether to include deleted events 16 | */ 17 | includeDeleted?: boolean; 18 | 19 | /** 20 | * Buffer time in ms, false to disable buffering 21 | */ 22 | bufferMs?: number | false; 23 | 24 | /** 25 | * Whether to include events from muted authors (default: false) 26 | */ 27 | includeMuted?: boolean; 28 | }; 29 | -------------------------------------------------------------------------------- /ndk-hooks/src/utils/time.ts: -------------------------------------------------------------------------------- 1 | export const inSeconds = (ms: number) => ms / 1000; 2 | -------------------------------------------------------------------------------- /ndk-hooks/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "ESNext", 5 | "moduleResolution": "bundler", 6 | "esModuleInterop": true, 7 | "strict": true, 8 | "declaration": true, 9 | "declarationDir": "./dist/types", 10 | "outDir": "./dist", 11 | "rootDir": "./src", 12 | "jsx": "react-jsx", 13 | "skipLibCheck": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "isolatedModules": true, 16 | "types": ["vitest"] 17 | }, 18 | "include": ["src/**/*"], 19 | "exclude": ["node_modules", "dist"] 20 | } 21 | -------------------------------------------------------------------------------- /ndk-hooks/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | environment: "jsdom", 6 | globals: true, 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /ndk-mobile/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Pablo Fernandez 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ndk-mobile/README.md: -------------------------------------------------------------------------------- 1 | # NDK Mobile 2 | 3 | A React Native/Expo implementation of [NDK (Nostr Development Kit)](https://github.com/nostr-dev-kit/ndk) that provides a complete toolkit for building Nostr applications on mobile platforms. 4 | 5 | ## Features 6 | 7 | - 🔐 Multiple signer implementations (NIP-07, NIP-46, Private Key) 8 | - 💾 SQLite-based caching for offline support 9 | - 🔄 Subscription management with automatic reconnection 10 | - 📱 React Native and Expo compatibility 11 | - 🪝 React hooks for easy state management 12 | - 👛 Integrated wallet support 13 | 14 | ## Installation 15 | 16 | npm install @nostr-dev-kit/ndk-mobile 17 | 18 | ## Usage 19 | 20 | When using this library don't import `@nostr-dev-kit/ndk` directly, instead import `@nostr-dev-kit/ndk-mobile`. `ndk-mobile` exports the same classes as `ndk`, so you can just swap the import. 21 | 22 | ## Example 23 | 24 | There is a barebones repository showing how to use this library: 25 | [ndk-mobile-sample](https://github.com/pablof7z/ndk-mobile-sample). 26 | 27 | For a real application using this look at [Olas](https://github.com/pablof7z/snapstr). 28 | 29 | ## License 30 | 31 | This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. 32 | 33 | ## Author 34 | 35 | [@pablof7z](https://njump.me/f7z.io) 36 | -------------------------------------------------------------------------------- /ndk-mobile/docs/wallet.md: -------------------------------------------------------------------------------- 1 | # Wallet 2 | 3 | `ndk-mobile` makes operating with nostr wallets as seamless as possible. 4 | 5 | ## Initialize 6 | 7 | The `useNDKWallet()` hook provides access to the active wallet and to activate a wallet. 8 | 9 | ```tsx 10 | const { activeWallet, setActiveWallet, balances } = useNDKWallet(); 11 | 12 | return ( 13 | { activeWallet && You are using a wallet of type {activeWallet.type}} 14 | 15 | -------------------------------------------------------------------------------- /ndk-svelte-components/src/lib/event/content/renderer/index.ts: -------------------------------------------------------------------------------- 1 | import markedFootnote from "marked-footnote"; 2 | import Hashtag from "./hashtag.svelte"; 3 | import Link from "./link.svelte"; 4 | import Mention from "./mention.svelte"; 5 | import NostrEvent from "./nostr-event.svelte"; 6 | 7 | export default { 8 | link: Link, 9 | hashtag: Hashtag, 10 | mention: Mention, 11 | nostrEvent: NostrEvent, 12 | }; 13 | -------------------------------------------------------------------------------- /ndk-svelte-components/src/lib/event/content/renderer/link.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | {#if showMedia} 11 | {#if !!isImage(href)} 12 | {""} 13 | {:else if isVideo(href)} 14 | 15 |