packages feed

nostr-1.3.0.0: lib/Nostr/Nip05.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}

module Nostr.Nip05 
  ( Nip05Id
  , verifyNip05
  , resolveNip05
  ) where

import Control.Exception (try, SomeException)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Data.Aeson (FromJSON, parseJSON, withObject, (.:?), (.:))
import qualified Data.Aeson as Aeson
import Data.Map (Map)
import qualified Data.Map as Map
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import Network.HTTP.Simple 
  ( httpJSON
  , getResponseBody
  , parseRequest 
  , setRequestSecure
  , setRequestPort
  , Response
  )

import Nostr.Event (PubKey(..))
import Nostr.Client (parsePubKey)

type Nip05Id = Text

-- | JSON Response structure for NIP-05
-- { "names": { "bob": "pubkey_hex" } }
data Nip05Response = Nip05Response
  { nip05Names :: Map Text Text
  } deriving (Show)

instance FromJSON Nip05Response where
  parseJSON = withObject "Nip05Response" $ \v -> Nip05Response
    <$> v .: "names"

-- | Verify a NIP-05 identifier matches a given public key
verifyNip05 :: MonadIO m => Nip05Id -> PubKey -> m Bool
verifyNip05 identifier (PubKey pkHex) = do
  mPub <- resolveNip05 identifier
  case mPub of
    Just (PubKey resolvedHex) -> return $ pkHex == resolvedHex
    Nothing -> return False

-- | Resolve a NIP-05 identifier to a Public Key
resolveNip05 :: MonadIO m => Nip05Id -> m (Maybe PubKey)
resolveNip05 identifier = liftIO $ do
  let (name, domain) = parseIdentifier identifier
  let url = "https://" <> domain <> "/.well-known/nostr.json?name=" <> name
  
  -- PutStrLn for debugging
  -- putStrLn $ "Resolving NIP-05: " ++ T.unpack url
  
  req <- parseRequest (T.unpack url)
  -- Ensure HTTPS
  let req' = setRequestSecure True $ setRequestPort 443 req
  
  result <- try (httpJSON req') :: IO (Either SomeException (Response Nip05Response))
  
  case result of
    Left _ -> return Nothing
    Right response -> do
      let nip05Resp = getResponseBody response
      case Map.lookup name (nip05Names nip05Resp) of
        Just pubKeyHex -> return $ parsePubKey pubKeyHex
        Nothing        -> return Nothing

  where
    parseIdentifier :: Text -> (Text, Text)
    parseIdentifier t = 
      case T.splitOn "@" t of
        [n, d] -> (n, d)
        [d]    -> ("_", d) -- Handle domain-only as root? Spec says <name>@<domain> usually
        _      -> (t, "")  -- Invalid fallback