packages feed

nostr 1.3.0.0 → 1.3.1.0

raw patch · 5 files changed

+297/−61 lines, 5 filesdep ~QuickCheckdep ~aesondep ~async

Dependency ranges changed: QuickCheck, aeson, async, base16-bytestring, base64-bytestring, bech32, bytestring, containers, cryptonite, entropy, hspec, http-conduit, memory, mtl, nonempty-wrapper-text, ppad-fixed, ppad-secp256k1, stm, text, time, unix-time, vector, websockets, wuss

Files

+ README.md view
@@ -0,0 +1,224 @@+# 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++- [x] NIP-01: Basic protocol flow+- [x] NIP-02: Contact Lists+- [x] NIP-05: DNS-based verification+- [x] NIP-09: Event Deletion Request+- [x] NIP-10: Text Notes and Threads+- [x] NIP-19: bech32-encoded entities+- [x] NIP-21: nostr: URI scheme+- [x] NIP-42: Relay Authentication+++## Installation++This project uses **Nix** for reproducible builds.++1. **Clone the repository:**+   ```bash+   git clone https://github.com/delirehberi/nostr.hs.git+   cd nostr.hs+   ```++2. **Enter the development shell:**+   ```bash+   nix develop+   ```++3. **Build the project:**+   ```bash+   cabal build all+   ```++## Development++### Building and Testing++```bash+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:++```bash+make upload-hackage+```++This will:+1. Generate a source distribution tar.gz file+2. Generate Haddock documentation for Hackage+3. Upload the package source to Hackage+4. 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.++```haskell+{-# 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:++```haskell+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"+```++### Fetching Relay Information (NIP-11)++Query a relay's metadata document using the `Nostr.Nip11` module:++```haskell+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):++```haskell+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.
lib/Nostr/Crypto.hs view
@@ -21,6 +21,7 @@   , generateKeyPair   , pubKeyFromSecKey   , exportSecKey+  , widerToInteger   , exportPubKey   , secKeyFromBytes   , Keys(..)@@ -77,9 +78,8 @@ -- | Derive public key from secret key (x-only 32-byte for BIP-340) pubKeyFromSecKey :: SecKey -> IO PubKey pubKeyFromSecKey secKey = do-  -- ppad-secp256k1 uses derive_pub to create public key from secret key-  -- It returns Maybe Pub (Projective point)-  case Secp.derive_pub secKey of+  let pkInt = widerToInteger secKey+  case Secp.derive_pub pkInt of     Just pubKey -> do       -- Serialize to compressed format (33 bytes) and drop the first byte       -- to get the 32-byte x-only public key as per BIP-340@@ -203,29 +203,34 @@ -- BIP-340 Schnorr Signatures -- ============================================================================ --- | Sign an event and set its id and sig fields using BIP-340 Schnorr signatures+widerToInteger :: SecKey -> Integer+widerToInteger sec =+  case hexToBytes (exportSecKey sec) of+    Right bs -> BS.foldl' (\acc b -> acc * 256 + toInteger b) 0 bs+    Left _ -> 0++-- | Sign an event with a secret key signEvent :: SecKey -> Event -> IO (Either Text Event) signEvent secKey event = do-  -- First compute the event ID-  let eventWithId = event { eventId = computeEventId event }+  -- 1. Create the serialized event to get the ID+  let idBytes = computeEventId event   -  -- Get the event ID bytes for signing (must be 32 bytes for Schnorr)-  case hexToBytes (unEventId (eventId eventWithId)) of-    Left err -> return $ Left err-    Right idBytes -> do-      -- Generate 32 bytes of auxiliary randomness (recommended by BIP-340)+  case hexToBytes (unEventId idBytes) of+    Right idBytesRaw -> do+      let eventWithId = event { eventId = idBytes }+      +      -- 2. Generate 32 bytes of auxiliary randomness as recommended by BIP-340       auxRand <- E.getEntropy 32       -      -- Sign with BIP-340 Schnorr-      case Secp.sign_schnorr secKey idBytes auxRand of-        Nothing -> return $ Left "Failed to create Schnorr signature"+      -- 3. Sign the 32-byte event ID+      case Secp.sign_schnorr (widerToInteger secKey) idBytesRaw auxRand of         Just sigBytes -> do-          let sigHex = bytesToHex sigBytes-          -          -- Create Signature with proper validation (should be 64 bytes = 128 hex chars)-          case mkSignature sigHex of-            Right signature -> return $ Right $ eventWithId { eventSig = signature }-            Left err -> return $ Left err+            let sigHex = bytesToHex sigBytes+            case mkSignature sigHex of+                Right signature -> return $ Right $ eventWithId { eventSig = signature }+                Left err -> return $ Left err+        Nothing -> return $ Left "Failed to create Schnorr signature"+    Left err -> return $ Left err  -- | Verify event signature using BIP-340 Schnorr verification verifySignature :: Event -> IO Bool
lib/Nostr/Event.hs view
@@ -113,12 +113,20 @@ validateHex128 t = T.length t == 128 && T.all isValidHex t  -- | Smart constructor for EventId with validation+--+-- >>> mkEventId "41ce9bc50daea4225efcd82edcb90e7eb25ce8d2d6cba2dfbb61e15e86979601"+-- Right (EventId "41ce9bc50daea4225efcd82edcb90e7eb25ce8d2d6cba2dfbb61e15e86979601")+-- >>> mkEventId "invalid"+-- Left "Invalid EventId: must be 64 lowercase hex characters, got: invalid" mkEventId :: Text -> Either Text EventId mkEventId t   | validateHex64 t = Right (EventId t)   | otherwise = Left $ "Invalid EventId: must be 64 lowercase hex characters, got: " <> t  -- | Smart constructor for PubKey with validation+--+-- >>> mkPubKey "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245"+-- Right (PubKey "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245") mkPubKey :: Text -> Either Text PubKey mkPubKey t   | validateHex64 t = Right (PubKey t)
lib/Nostr/Nip04.hs view
@@ -30,7 +30,7 @@ import qualified Data.Text.Encoding as TE import qualified System.Entropy as E -import Nostr.Crypto (SecKey, hexToBytes)+import Nostr.Crypto (SecKey, hexToBytes, widerToInteger) import Nostr.Event (PubKey(..))  -- | Compute shared secret using ECDH@@ -41,10 +41,9 @@   let compressedPubKey = BS.cons 0x02 pubBytes   pubPoint <- Secp.parse_point compressedPubKey   -  -- Use ppad-secp256k1 ecdh. Note: If this computes SHA256 of the point instead of   -- just returning the X coordinate, we might need to use `mul` directly.   -- Let's try `ecdh` first.-  Secp.ecdh pubPoint secKey+  Secp.ecdh pubPoint (widerToInteger secKey)  -- | PKCS7 padding padPKCS7 :: Int -> ByteString -> ByteString
nostr.cabal view
@@ -20,7 +20,7 @@ -- PVP summary:     +-+------- breaking API changes --                  | | +----- non-breaking API additions --                  | | | +--- code changes with no API change-version:            1.3.0.0+version:            1.3.1.0 bug-reports:        https://github.com/delirehberi/nostr.hs/issues  -- A short (one-line) description of the package.@@ -53,7 +53,7 @@ build-type:         Simple  -- Extra doc files to be distributed with the package, such as a CHANGELOG or a README.-extra-doc-files:    CHANGELOG.md+extra-doc-files:    CHANGELOG.md README.md  -- Extra source files to be distributed with the package, such as examples, or a tutorial module. -- extra-source-files:@@ -81,28 +81,28 @@          -- Other library packages from which modules are imported.     build-depends:    base >=4.18.0.0 && <4.20-        , aeson -        , bytestring -        , text -        , unix-time -        , websockets-        , ppad-secp256k1 >= 0.4.0-        , ppad-fixed-        , nonempty-wrapper-text-        , cryptonite-        , memory-        , base16-bytestring-        , vector-        , entropy-        , containers-        , async-        , stm-        , http-conduit-        , mtl-        , time-        , wuss-        , bech32-        , base64-bytestring+        , aeson < 3+        , bytestring < 0.13+        , text < 3+        , unix-time < 0.5+        , websockets < 0.14+        , ppad-secp256k1 >= 0.4.0 && < 0.5+        , ppad-fixed < 0.2+        , nonempty-wrapper-text < 0.2+        , cryptonite < 0.31+        , memory < 0.19+        , base16-bytestring < 1.1+        , vector < 0.14+        , entropy < 0.5+        , containers < 0.8+        , async < 2.3+        , stm < 2.6+        , http-conduit < 2.4+        , mtl < 2.4+        , time < 1.13+        , wuss < 2.1+        , bech32 < 1.2+        , base64-bytestring < 1.3      -- Directories containing source files.     hs-source-dirs:   lib@@ -127,10 +127,10 @@     build-depends:         base >=4.18.0.0 && <4.20,         nostr,-        aeson,-        bytestring,-        text,-        time+        aeson < 3,+        bytestring < 0.13,+        text < 3,+        time < 1.13      -- Directories containing source files.     hs-source-dirs:   app@@ -144,9 +144,9 @@     build-depends:         base >=4.18.0.0 && <4.20,         nostr,-        text,-        bytestring,-        time+        text < 3,+        bytestring < 0.13,+        time < 1.13      hs-source-dirs:   app     default-language: GHC2021@@ -172,10 +172,10 @@     build-depends:         base >=4.18.0.0 && <4.20,         nostr,-        hspec,-        QuickCheck,-        text,-        bytestring,-        aeson,-        containers,-        base64-bytestring+        hspec < 3.0,+        QuickCheck < 2.15,+        text < 3,+        bytestring < 0.13,+        aeson < 3,+        containers < 0.8,+        base64-bytestring < 1.3