nostr.hs
A NIP-01 compliant Nostr library and client implementation in Haskell.
Features
- NIP-01 Compliant: Full implementation of event structure, rules, and tags.
- Schnorr Signatures: Pure Haskell BIP-340 Schnorr signatures using
ppad-secp256k1.
- Relay Communication: WebSocket-based communication with Nostr relays.
- Type Safety: Strong types for Event IDs, Public Keys, and Signatures.
Implemented NIPs
Installation
This project uses Nix for reproducible builds.
-
Clone the repository:
git clone https://github.com/delirehberi/nostr.hs.git
cd nostr.hs
-
Enter the development shell:
nix develop
-
Build the project:
cabal build all
Development
Building and Testing
make build # Build the project
make test # Run tests
make clean # Clean build artifacts
make docs # Generate documentation
make sdist # Generate source distribution tar.gz
All make commands automatically use the Nix development environment, so you don't need to run nix develop first.
Uploading to Hackage
To package and upload the library to Hackage:
make upload-hackage
This will:
- Generate a source distribution tar.gz file
- Generate Haddock documentation for Hackage
- Upload the package source to Hackage
- Upload the documentation to Hackage
You'll need to be authenticated with Hackage and have upload permissions. The script assumes you have cabal configured with your Hackage credentials.
Usage Examples
High-Level API (Recommended)
The Nostr.Client module provides a monadic interface for managing connections, signing, and publishing. It handles robust reconnections (exponential backoff) and TLS usage automatically.
{-# LANGUAGE OverloadedStrings #-}
import Nostr.Client
import Nostr.Event
import Nostr.Crypto
import Data.Function ((&))
import Control.Monad.IO.Class (liftIO)
main :: IO ()
main = do
-- 1. Generate Keys
-- Note: In a real app, you'd load these from safe storage
(secKey, pubKey) <- generateKeyPair
let keys = Keys secKey pubKey Nothing
-- 2. Connect to Relays
-- connectRelays establishes background threads for each relay and manages reconnection
env <- connectRelays ["wss://relay.damus.io", "wss://nos.lol"]
runNostrApp env $ do
-- A. Publish a simple short text note (Kind 1)
publishShortNote keys "Hello Nostr from Haskell!"
-- B. Publish a complex event using the Builder Pattern
-- Use the (&) operator to chain combinators for tags and kinds
liftIO $ putStrLn "Publishing a reply..."
publish keys $ shortNote "Replying to an event..."
& withKind 1
& withReply "event-id-hex"
& withMention "pubkey-hex"
& withTag ["t", "haskell"]
-- C. Query Events
-- Queries all connected relays concurrently and aggregates unique results
let filter = defaultFilter
{ filterKinds = Just [1]
, filterAuthors = Just [pubKey]
, filterLimit = Just 10
}
events <- queryEvents filter
liftIO $ print events
-- D. Follow Users (NIP-02)
-- This fetches your existing contact list (Kind 3), adds the user, and republishes.
liftIO $ putStrLn "Following Jack..."
follow keys "82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbf71d22" (Just "wss://relay.damus.io") (Just "jack")
-- E. Fetch Contacts
contacts <- getContacts keys
liftIO $ print contacts
-- F. Delete Events (NIP-09)
-- Deletes a list of event IDs with an optional reason.
-- let eventIds = [EventId "hex_id_1...", EventId "hex_id_2..."]
-- deleteEvents keys eventIds (Just "mistake")
-- 3. Disconnect
disconnect env
Low-Level Event Creation
If you need manual control over event creation without the NostrApp environment:
import Nostr.Event
import Nostr.Crypto
import Data.Time.Clock.POSIX (getPOSIXTime)
main :: IO ()
main = do
(secKey, pubKey) <- generateKeyPair
now <- round <$> getPOSIXTime
-- Create unsigned event
let unsigned = createUnsignedEvent pubKey now 1 [] "Manual event content"
-- Sign event
signed <- signEvent secKey unsigned
case signed of
Right event -> putStrLn $ "Event ID: " ++ show (eventId event)
Left err -> putStrLn $ "Error: " ++ show err
### Encrypted Direct Messages (NIP-04)
Use the `Nostr.Nip04` module to securely encrypt and decrypt direct messages using AES-256-CBC and ECDH:
```haskell
import Nostr.Crypto
import Nostr.Nip04
import qualified Data.Text as T
main :: IO ()
main = do
(senderSec, senderPub) <- generateKeyPair
(receiverSec, receiverPub) <- generateKeyPair
let msg = "Super secret message"
-- Encrypt (Sender -> Receiver)
Just encrypted <- encryptNip04 senderSec receiverPub msg
putStrLn $ "Encrypted: " ++ T.unpack encrypted
-- Decrypt (Receiver)
let decrypted = decryptNip04 receiverSec senderPub encrypted
print decrypted -- Just "Super secret message"
Query a relay's metadata document using the Nostr.Nip11 module:
import Nostr.Nip11
main :: IO ()
main = do
info <- fetchRelayInfo "wss://relay.damus.io"
case info of
Right i -> print (riSoftware i, riSupportedNips i)
Left err -> putStrLn $ "Failed to fetch info: " ++ err
Parsing Lightning Zaps (NIP-57)
Easily parse Zap Requests (Kind 9734) and Zap Receipts (Kind 9735):
import Nostr.Nip57
import Nostr.Event
-- Assuming `event` is a parsed Event of kind 9734
handleEvent :: Event -> IO ()
handleEvent event = do
case parseZapRequest event of
Just zr -> putStrLn $ "Zap Request for " ++ T.unpack (zrP zr)
Nothing -> return ()
## Contributing
Contributions are welcome! Please ensure all tests pass before submitting a PR.