nostr_signaling

Dart SDK License pub.dev

A Nostr-based signaling library for Dart. Exchange arbitrary binary data between Nostr peers, with optional GZip compression and multi-relay redundancy.

Features

  • Nostr signaling — publish and subscribe to arbitrary binary data over the Nostr protocol.
  • GZip compression — reduce payload size automatically; detects and rejects already-compressed data.
  • Multi-relay — publish/subscribe across multiple Nostr relays for reliability and redundancy.
  • Key management — generate, import, and validate Nostr key pairs.
  • Pluggable engine — implement ICompressionEngine to add custom compression algorithms.

Getting started

Add nostr_signaling to your pubspec.yaml:

dependencies:
  nostr_signaling: ^0.4.0

Usage

Basic signaling (no compression)

import 'package:nostr_signaling/nostr_signaling.dart';

void main() async {
  final signaling = NostrSignalingFactory.create(
    keyPair: keyPair,
  );

  await signaling.connect();

  // Publish raw bytes to a peer
  final eventId = await signaling.publish([1, 2, 3, 4, 5]);

  // Subscribe to messages from a peer
  await signaling.subscribe('peer-pubkey-hex', (id, data) {
    print('Received from $id: $data');
  });

  await signaling.disconnect();
}

With GZip compression

final signaling = NostrSignalingFactory.create(
  keyPair: keyPair,
  useCompression: true,
);

await signaling.connect();

// Large data is compressed automatically before publishing
final largeData = List<int>.generate(10000, (i) => i % 256);
await signaling.publish(largeData);

Key generation

// Generate a new random key pair
final keyPair = NostrKeys.generate();
print('Private: ${keyPair.privateKey}');
print('Public:  ${keyPair.publicKey}');

// Import from an existing private key
final imported = NostrKeys.fromPrivateKeyHex('your-private-key-hex');

// Import and validate a full key pair
final validated = NostrKeys.fromHex(
  privateKeyHex: 'private-key-hex',
  publicKeyHex: 'public-key-hex',
);

Custom compression engine

class MyCompressionEngine implements ICompressionEngine {
  @override
  Future<CompressedData> compress(List<int> data) async {
    // Your custom compression logic
    return CompressedData(
      originalSize: data.length,
      compressedSize: data.length,
      compressionRatio: 0,
      data: data,
      timestamp: DateTime.now().millisecondsSinceEpoch,
    );
  }

  @override
  Future<List<int>> decompress(CompressedData compressed) async {
    // Your custom decompression logic
    return compressed.data;
  }
}

final signaling = NostrSignalingFactory.create(
  keyPair: keyPair,
  useCompression: true,
  compressionEngine: MyCompressionEngine(),
);

Event deduplication

EventCallback is now a class with built-in dedup. The same event hash is never processed twice.

final callback = EventCallback(
  (id, data) => print('Received from $id: ${data.length} bytes'),
  maxRecords: 5000,           // keep last 5000 hashes
);

Multi-relay redundancy

final signaling = NostrSignalingFactory.create(
  keyPair: keyPair,
  relayUrls: [
    'wss://relay.damus.io',
    'wss://nos.lol',
    'wss://relay.primal.net',
  ],
);

Config file

NostrConfig persists signaling configuration to a JSON file on disk.

Field Type Default Description
relays List<String> 10 standard Nostr relays Relay WebSocket URLs to publish/subscribe
keyPair NostrKeyPair? null Nostr key pair (private + public key hex)
collection String 'nostr_signaling_seen_hashes' Collection name for event callback dedup

Example nostr_config.json:

{
  "relays": ["wss://relay.damus.io", "wss://nos.lol"],
  "collection": "nostr_signaling_seen_hashes",
  "privateKey": "hex-private-key",
  "publicKey": "hex-public-key"
}
// Create and save
final config = NostrConfig(
  keyPair: myKeyPair,
  relays: ['wss://relay.damus.io'],
);
await config.save();                // → nostr_config.json

// Load (auto-creates with new key pair if file missing)
final loaded = await NostrConfig.load();      // async
final loadedSync = NostrConfig.loadSync();    // sync

Initial point from config (no required parameters)

Loads keyPair and relays from the config file automatically. Creates the file with a fresh key pair if missing. Throws StateError if the file exists but has no key pair.

// Singleton DI — no parameters needed
await initialPointNostrSignalingFromConfig();

final signaling = getINostrSignaling();

// Registry DI — supports multiple named instances
initialPointNostrSignalingRegistryFromConfig(key: 'alice');
initialPointNostrSignalingRegistryFromConfig(key: 'bob');

Both accept an optional configPath (default: nostr_config.json).

API overview

Class / Interface Description
INostrSignaling Abstract signaling interface (connect, publish, subscribe, etc.)
NostrSignalingImpl Concrete signaling implementation
INostrRelay Abstract relay interface
NostrRelayImpl Concrete WebSocket relay via dart_nostr
ICompressionEngine Pluggable compression interface
GzipCompressionEngine GZip compression with auto-detection
EventCallback Callback wrapper with automatic hash-based deduplication
PayloadHashLength Payload hash length (32/64/256 bits)
NostrConfig Config file persistence (relays + key pair)
NostrSignalingFactory Factory with create() and createWithCustomRelays()
NostrKeys / NostrKeyPair Key generation, import, and validation
CompressedData Compression metadata (original size, ratio, timestamp)
NostrTestKeys / NostrTestRelays Test constants

Event kinds

Kind Description
1000 Compressed payload (base64-encoded)
1001 Raw payload (base64-encoded)

Additional information

Libraries

generated/nostr_signaling_impl_di
nostr_signaling
A Nostr-based signaling library for exchanging binary data between peers.