Build Status Coverage Pub License

Dart Nostr Development Kit (NDK)

NDK (Nostr Development Kit) is a Dart library that enhances the Nostr development experience.
NDK supplies you with high-level usecases like lists or metadata while still allowing you to use low-level queries enhanced with inbox/outbox (gossip) by default.
Our Target is to make it easy to build constrained Nostr clients, particularly for mobile devices.

Table of Contents:

$~~~~~~~~~~~$

Getting started

Prerequisites

  • dart SDK

Prerequisites ndk_rust_verifier

  • android SDK (also for desktop builds)
  • flutter SDK
  • rust ( + toolchain for target)

Rust toolchain android:

rustup target add \
    aarch64-linux-android \
    armv7-linux-androideabi \
    x86_64-linux-android \
    i686-linux-android

Rust toolchain ios:

# 64 bit targets (real device & simulator):
rustup target add aarch64-apple-ios x86_64-apple-ios
# New simulator target for Xcode 12 and later
rustup target add aarch64-apple-ios-sim
# 32 bit targets (you probably don't need these):
rustup target add armv7-apple-ios i386-apple-ios

Install

Ndk has a core package ndk and optional packages like rust_verifier and amber.

flutter pub add ndk

Optional:

flutter pub add ndk_rust_verifier
flutter pub add ndk_amber

Import

import 'package:ndk/ndk.dart';

Optional:

import 'package:ndk_rust_verifier/ndk_rust_verifier.dart';
import 'package:ndk_amber/ndk_amber.dart';

Usage

more examples 🔗

import 'package:ndk/ndk.dart';
import 'package:ndk_rust_verifier/ndk_rust_verifier.dart';

// init
final ndk = Ndk(
  NdkConfig(
    eventVerifier: RustEventVerifier(),
    cache: MemCacheManager(),
  ),
);

// query
final response = ndk.requests.query(
  filters: [
    Filter(
      authors: ['hexPubkey']
      kinds: [Nip01Event.TEXT_NODE_KIND],
      limit: 10,
    ),
  ],
);

// result
await for (final event in response.stream) {
  print(event);
}

We strongly recommend using RustEventVerifier() for client applications. It uses a separate thread for signature verification and is therefore more performant. $~~~~~~~~~~~$


Features / what does NDK do?

  • return nostr data based on filters (any kind).
  • automatically discover the best relays to satisfy the provided request (using gossip)
  • specify desired coverage on each request (e.g. x relays per pubkey)
  • publish nostr events to optimal relays or explicit relays
  • cache responses to save network bandwidth
  • stream directly from cache and network (if needed)
  • query and subscription, e.g., get data once; subscribe to data.
  • plugin cache interface, bring your own db or use included ones: inMemory
  • plug in verifier interface, bring your own event verifier, or use included ones: bip340, rust
  • plug in event signer interface, bring your own event signer, or use included ones: bip340, amber
  • contact list support, you can convert nostr_event to contact_list
  • nip51 list support, you can convert nostr_event to nip51_list
  • nip05 caching

not Included

  • ready to use feeds, you have to build them on your own (🚫 not planned)
  • create && manage keypairs. You have to provide them (🚫 not planned)
  • file upload (🔜 planned)
  • threading, you can do this on your own if you move ndk or only the event_verifier into its own thread (🔜 planned)
  • support for request overrides (you have to close and reopen requests) (🤔 unsure)

NIPs

  • x Event Builders / WebSocket Subscriptions (NIP-01)
  • x User Profiles (edit/follow/unfollow - NIP-02)
  • x Private Messages (NIP-04)
  • x Nostr Address (NIP-05)
  • x Event Deletion (NIP-09)
  • x Relay Info (NIP-11)
  • x Reactions (NIP-25)
  • x Lists (NIP-51)
  • x Relay List Metadata (NIP-65)
  • x Wallet Connect API (NIP-47)
  • Bech Encoding support (NIP-19)
  • Zaps (private, public, anon, non-zap) (NIP-57)
  • Badges (NIP-58)

Performance

There are two main constrains that we aim for: battery/compute and network bandwidth.

network
Inbox/Outbox (gossip) is our main pillar to help avoid unnecessary nostr requests. We try to leverage the cache as much as possible.
Even splitting the users filters into smaller relay tailored filters if we know the relay has the information we need.

compute
Right now the most compute intensive operation is verifying signatures.
We use the cache to determine if we have already seen the event and only if it is unknown signature verification is done.
To make the operation as optimized as possible we strongly recommend using RustEventVerifier() because it uses a separate thread for verification.

$~~~~~~~~~~~$

Gossip/outbox model of relay discovery and connectivity

The simplest characterization of the gossip model is just this: reading the posts of people you follow from the relays that they wrote them to.

more details on mikedilger.com/gossip-model/

Common terminology

term explanation simmilar to
broadcastEvent push event to nostr network/relays postEvent, publishEvent
JIT Just In Time, e.g. as it happens -
query get data once and close the request get request
subscription stream of events as they come in stream of data
bootstrapRelays default relays to connect when nothing else is specified seed relays, initial relays
engine optimized network resolver for nostr requests -

$~~~~~~~~~~~$

Changelog 🔗

$~~~~~~~~~~~$

Library development 🏗️

Setup

Install prerequisites

run melos bootstrap to install all dependencies.

If you work on rust code (packages/rust_verifier/rust_builder/rust) run flutter_rust_bridge_codegen generate --watch to generate the rust dart glue code.

Run build runner: (e.g for generating mocks)
dart run build_runner build

Architecture

The repo is setup as a monorepo and packages are split to enable user choice of what to include.
The main package is ndk which is the main entry point for the lib user.
Other packages like rust_verifier or amber are optional and can be included if needed.

NDK uses Clean Architecture. Reasons for it being clear separation of concerns and therefore making it more accessible for future contributors.
You can read more about it here.

For initialization we use presentation_layer/init.dart to assemble all dependencies, these are then exposed in presentation_layer/ndk.dart the main entry point for the lib user.

Global state is realized via a simple GlobalState object created by ndk.dart.
The lib user is supposed to keep the NDK object in memory.

Other state objects are created on demand, for example RequestState for each request.

Folder Structure of ndk

lib/
├── config/
│   └── # Configuration files
├── shared/
│   ├── nipX/ # NIP-specific code folders
│   └── # Internal code, no external dependencies
├── data_layer/
│   ├── data_sources/
│   │   └── # External APIs, WebSocket implementations, etc.
│   ├── models/
│   │   └── # Data transformation (e.g., JSON to entity)
│   └── repositories/
│       └── # Concrete repository implementations
├── domain_layer/
│   ├── entities/
│   │   └── # Core business objects
│   ├── repositories/
│   │   └── # Repository contracts
│   └── usecases/
│       └── # Business logic / use cases
├── presentation_layer/
│   └── # API design (exposing use cases to the outside world)
└── ndk.dart # Entry point, directs to presentation layer

Engines

NDK ships with two network Engines. An Engine is part of the code that resolves nostr requests over the network and handles the WebSocket connections.
Its used to handle the inbox/outbox (gossip) model efficiently.

Lists Engine:
Precalculates the best possible relays based on nip65 data. During calculation relay connectivity is taken into account. This works by connecting and checking the health status of a relay before its added to the ranking pool.
This method gets close to the optimal connections given a certain pubkey coverage.

Just in Time (JIT) Engine:
JIT Engine does the ranking on the fly only for the missing coverage/pubkey. Healthy relays are assumed during ranking and replaced later on if a relay fails to connect.
To Avoid rarely used relays and spawning a bunch of unessecary connections, already connected relays get a boost, and a usefulness score is considered for the ranking.
For more information look here

Custom Engine
If you want to implement your own engine with custom behavior you need to touch the following things:

  1. implement NetworkEngine interface
  2. write your response stream to networkController in the RequestState
  3. if you need global state you can register your own data type in global_state.dart
  4. initialize your engine in init.dart

The current state solution is not ideal because it requires coordination between the engine authors and not enforceable by code. If you have ideas how to improve this system, please reach out.

The network engine is only concerned about network requests! Caching and avoiding concurrency is handled by separate usecases. Take a look at requests.dart usecase to learn more.

Libraries

config/bootstrap_relays
config/broadcast_defaults
config/metadata_defaults
config/nip_05_defaults
config/relay_defaults
config/request_defaults
config/rx_defaults
config/user_relay_list_defaults
data_layer/data_sources/http_request
data_layer/data_sources/isar_db
data_layer/data_sources/websocket
data_layer/models/db/db_contact_list
data_layer/models/db/db_event
data_layer/models/db/db_metadata
data_layer/models/db/db_nip05
data_layer/models/db/db_relay_set
data_layer/models/db/db_user_relay_list
data_layer/models/nip_05_model
data_layer/repositories/cache_manager/isar_cache_manager
data_layer/repositories/cache_manager/mem_cache_manager
data_layer/repositories/nip_05_http_impl
data_layer/repositories/nostr_transport/websocket_nostr_transport
data_layer/repositories/nostr_transport/websocket_nostr_transport_factory
data_layer/repositories/signers/bip340_event_signer
data_layer/repositories/verifiers/bip340_event_verifier
domain_layer/entities/broadcast_response
domain_layer/entities/broadcast_state
domain_layer/entities/connection_source
domain_layer/entities/contact_list
domain_layer/entities/event_filter
domain_layer/entities/filter
domain_layer/entities/global_state
domain_layer/entities/jit_engine_relay_connectivity_data
domain_layer/entities/metadata
domain_layer/entities/ndk_request
domain_layer/entities/nip_01_event
domain_layer/entities/nip_05
domain_layer/entities/nip_51_list
domain_layer/entities/nip_65
domain_layer/entities/pubkey_mapping
domain_layer/entities/read_write
domain_layer/entities/read_write_marker
domain_layer/entities/relay
domain_layer/entities/relay_connectivity
domain_layer/entities/relay_info
domain_layer/entities/relay_set
domain_layer/entities/relay_stats
domain_layer/entities/request_response
domain_layer/entities/request_state
domain_layer/entities/tuple
domain_layer/entities/user_relay_list
domain_layer/repositories/cache_manager
domain_layer/repositories/event_signer
domain_layer/repositories/event_verifier
domain_layer/repositories/nip_05_repo
domain_layer/repositories/nostr_transport
domain_layer/usecases/broadcast/broadcast
domain_layer/usecases/cache_read/cache_read
domain_layer/usecases/cache_write/cache_write
domain_layer/usecases/engines/network_engine
domain_layer/usecases/follows/follows
domain_layer/usecases/jit_engine
domain_layer/usecases/lists/lists
domain_layer/usecases/metadatas/metadatas
domain_layer/usecases/nip05/verify_nip_05
domain_layer/usecases/nwc/consts/bitcoin_network
domain_layer/usecases/nwc/consts/error_code
domain_layer/usecases/nwc/consts/nwc_kind
domain_layer/usecases/nwc/consts/nwc_method
domain_layer/usecases/nwc/consts/transaction_type
domain_layer/usecases/nwc/nostr_wallet_connect_uri
domain_layer/usecases/nwc/nwc
domain_layer/usecases/nwc/nwc_connection
domain_layer/usecases/nwc/nwc_notification
domain_layer/usecases/nwc/requests/get_balance
domain_layer/usecases/nwc/requests/get_info
domain_layer/usecases/nwc/requests/list_transactions
domain_layer/usecases/nwc/requests/lookup_invoice
domain_layer/usecases/nwc/requests/make_invoice
domain_layer/usecases/nwc/requests/multi_pay_invoice
domain_layer/usecases/nwc/requests/multi_pay_keysend
domain_layer/usecases/nwc/requests/nwc_request
domain_layer/usecases/nwc/requests/pay_invoice
domain_layer/usecases/nwc/requests/pay_keysend
domain_layer/usecases/nwc/responses/get_balance_response
domain_layer/usecases/nwc/responses/get_info_response
domain_layer/usecases/nwc/responses/list_transactions_response
domain_layer/usecases/nwc/responses/lookup_invoice_response
domain_layer/usecases/nwc/responses/make_invoice_response
domain_layer/usecases/nwc/responses/nwc_response
domain_layer/usecases/nwc/responses/pay_invoice_response
domain_layer/usecases/nwc/tlv_record
domain_layer/usecases/old_relay_manager
domain_layer/usecases/relay_jit_manager/relay_jit_broadcast_strategies/broadcast_strategies_shared
domain_layer/usecases/relay_jit_manager/relay_jit_broadcast_strategies/relay_jit_broadcast_all
domain_layer/usecases/relay_jit_manager/relay_jit_broadcast_strategies/relay_jit_broadcast_other_read
domain_layer/usecases/relay_jit_manager/relay_jit_broadcast_strategies/relay_jit_broadcast_own
domain_layer/usecases/relay_jit_manager/relay_jit_broadcast_strategies/relay_jit_broadcast_specific
domain_layer/usecases/relay_jit_manager/relay_jit_request_strategies/relay_jit_blast_all_strategy
domain_layer/usecases/relay_jit_manager/relay_jit_request_strategies/relay_jit_pubkey_strategy
domain_layer/usecases/relay_jit_manager/relay_jit_request_strategies/relay_jit_strategies_shared
domain_layer/usecases/relay_manager
domain_layer/usecases/relay_sets/relay_sets
domain_layer/usecases/relay_sets_engine
domain_layer/usecases/requests/concurrency_check
domain_layer/usecases/requests/requests
domain_layer/usecases/requests/verify_event_stream
domain_layer/usecases/stream_response_cleaner/stream_response_cleaner
domain_layer/usecases/user_relay_lists/user_relay_lists
entities
exports all entities, intended usage: import 'package:ndk/entities.dart' as ndk_entities;
ndk
presentation_layer/init
presentation_layer/ndk
presentation_layer/ndk_config
shared/helpers/list_casting
shared/helpers/relay_helper
shared/logger/logger
shared/nips/nip01/bip340
shared/nips/nip01/client_msg
shared/nips/nip01/helpers
shared/nips/nip01/key_pair
shared/nips/nip04/nip04
shared/nips/nip09/deletion
shared/nips/nip19/hrps
shared/nips/nip19/nip19
shared/nips/nip25/reactions
shared/nips/nip50/nip50
shared/nips/nip65/relay_ranking
tag_count_event_filter