Home

Awesome

Build Status Coverage Pub License

Dart Nostr Development Kit (NDK)

NDK (Nostr Development Kit) is a Dart library designed to enhance the Nostr development experience.
It provides streamlined solutions for common use cases and abstracts away complex relay management, making it ideal for building constrained Nostr clients, particularly on mobile devices.
NDK implements the inbox/outbox (gossip) model by default, optimizing network usage and improving performance.

Table of Contents:

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

Getting started

Prerequisites

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

flutter pub add ndk

Import

import 'package:ndk/ndk.dart';

Usage

usage examples

import 'package:ndk/ndk.dart';

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

// query
NdkResponse 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);
}

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


Features / what does NDK do?

not Included


NIPs

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.

<img src="https://mikedilger.com/gossip-model/gossip-model.png" style="width:400px; height:400px"/>

more details on https://mikedilger.com/gossip-model/

Common terminology

termexplanationsimmilar to
broadcastEventpush event to nostr network/relayspostEvent, publishEvent
JITJust In Time, e.g. as it happens-
queryget data once and close the requestget request
subscriptionstream of events as they come instream of data
bootstrapRelaysdefault relays to connect when nothing else is specifiedseed relays, initial relays
engineoptimized network resolver for nostr requests-

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

Changelog šŸ”—

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

Library development šŸ—ļø

Setup

Install prerequisites

If you work on rust code (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

This project 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

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.