nostr-1.3.0.0: lib/Nostr/Nip19.hs
{-# LANGUAGE OverloadedStrings #-}
module Nostr.Nip19
( -- * Encoding
toNpub
, toNsec
, toNote
, toNprofile
, toNevent
-- * Decoding
, Nip19Object(..)
, ProfilePtr(..)
, EventPtr(..)
, decode
) where
import Codec.Binary.Bech32 (dataPartFromBytes, dataPartToBytes, encode, decodeLenient, humanReadablePartFromText)
import qualified Codec.Binary.Bech32 as Bech32
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Base16 as B16
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import Data.Maybe (fromMaybe, mapMaybe)
import Data.Word (Word8)
import Nostr.Crypto
import Nostr.Event (EventId(..), PubKey(..), mkPubKey, mkEventId)
-- | Decoded NIP-19 Object
data Nip19Object
= Nip19Pub PubKey
| Nip19Sec SecKey
| Nip19Note EventId
| Nip19Profile ProfilePtr
| Nip19Event EventPtr
deriving (Show)
instance Eq Nip19Object where
(Nip19Pub a) == (Nip19Pub b) = a == b
(Nip19Sec a) == (Nip19Sec b) = exportSecKey a == exportSecKey b
(Nip19Note a) == (Nip19Note b) = a == b
(Nip19Profile a) == (Nip19Profile b) = a == b
(Nip19Event a) == (Nip19Event b) = a == b
_ == _ = False
data ProfilePtr = ProfilePtr
{ ppPubkey :: PubKey
, ppRelays :: [Text]
} deriving (Show, Eq)
data EventPtr = EventPtr
{ epEventId :: EventId
, epRelays :: [Text]
, epAuthor :: Maybe PubKey
} deriving (Show, Eq)
-- | Encode a Public Key as npub
toNpub :: PubKey -> Maybe Text
toNpub pk = do
pkBytes <- either (const Nothing) Just $ hexToBytes (exportPubKey pk)
encodeBech32 "npub" pkBytes
-- | Encode a Secret Key as nsec
toNsec :: SecKey -> Maybe Text
toNsec sk = do
skBytes <- either (const Nothing) Just $ hexToBytes (exportSecKey sk)
encodeBech32 "nsec" skBytes
-- | Encode an Event ID as note
toNote :: EventId -> Maybe Text
toNote (EventId eid) = do
eidBytes <- either (const Nothing) Just $ hexToBytes eid
encodeBech32 "note" eidBytes
-- | Encode a profile pointer as nprofile (TLV)
toNprofile :: PubKey -> [Text] -> Maybe Text
toNprofile pk relays = do
pkBytes <- either (const Nothing) Just $ hexToBytes (exportPubKey pk)
-- TLV encoding: type (1 byte) + length (1 byte) + value
-- Type 0: special (pubkey), 32 bytes
-- Type 1: relay, variable length
let tlv = encodeTLV 0 pkBytes <> mconcat (map (encodeTLV 1 . TE.encodeUtf8) relays)
encodeBech32 "nprofile" tlv
-- | Encode an event pointer as nevent (TLV)
toNevent :: EventId -> [Text] -> Maybe PubKey -> Maybe Text
toNevent (EventId eid) relays author = do
eidBytes <- either (const Nothing) Just $ hexToBytes eid
authorBytes <- case author of
Just pk -> do
pkBytes <- either (const Nothing) Just $ hexToBytes (exportPubKey pk)
return $ encodeTLV 2 pkBytes
Nothing -> return BS.empty
let tlv = encodeTLV 0 eidBytes
<> mconcat (map (encodeTLV 1 . TE.encodeUtf8) relays)
<> authorBytes
encodeBech32 "nevent" tlv
-- | Generic Bech32 Encoder
encodeBech32 :: Text -> ByteString -> Maybe Text
encodeBech32 prefix dataBytes = do
hrp <- qualifiedHumanReadablePartFromText prefix
let dataPart = dataPartFromBytes dataBytes
either (const Nothing) Just $ encode hrp dataPart
where
qualifiedHumanReadablePartFromText t =
either (const Nothing) Just $ humanReadablePartFromText t
-- | TLV Encoder
-- Type (8-bit), Length (8-bit), Value
encodeTLV :: Word8 -> ByteString -> ByteString
encodeTLV typ val =
let len = fromIntegral (BS.length val) :: Word8
in BS.pack [typ, len] <> val
-- | Decode a Bech32 string
decode :: Text -> Either Text Nip19Object
decode input = case decodeLenient input of
Left err -> Left $ "Bech32 decode error: " <> T.pack (show err)
Right (hrp, dataPart) -> do
let prefix = Bech32.humanReadablePartToText hrp
bytes <- maybe (Left "Invalid data part") Right $ dataPartToBytes dataPart
case prefix of
"npub" -> do
pk <- parsePubKey bytes
Right $ Nip19Pub pk
"nsec" -> do
sk <- parseSecKey bytes
Right $ Nip19Sec sk
"note" -> do
eid <- parseEventId bytes
Right $ Nip19Note eid
"nprofile" -> do
ptr <- parseProfileTLV bytes
Right $ Nip19Profile ptr
"nevent" -> do
ptr <- parseEventTLV bytes
Right $ Nip19Event ptr
_ -> Left $ "Unknown prefix: " <> prefix
-- Helpers for parsing
parsePubKey :: ByteString -> Either Text PubKey
parsePubKey bs =
let hex = bytesToHex bs
in Nostr.Event.mkPubKey hex
parseSecKey :: ByteString -> Either Text SecKey
parseSecKey bs =
case secKeyFromBytes bs of
Just sk -> Right sk
Nothing -> Left "Invalid secret key bytes"
parseEventId :: ByteString -> Either Text EventId
parseEventId bs =
let hex = bytesToHex bs
in Nostr.Event.mkEventId hex
parseProfileTLV :: ByteString -> Either Text ProfilePtr
parseProfileTLV bs = do
tlv <- parseTLV bs
pubkeyBs <- maybe (Left "Missing pubkey in nprofile") Right (findTLV 0 tlv)
pubkey <- parsePubKey pubkeyBs
let relays = map TE.decodeUtf8 (findAllTLV 1 tlv)
Right $ ProfilePtr pubkey relays
parseEventTLV :: ByteString -> Either Text EventPtr
parseEventTLV bs = do
tlv <- parseTLV bs
eventIdBs <- maybe (Left "Missing event ID in nevent") Right (findTLV 0 tlv)
eventId <- parseEventId eventIdBs
let relays = map TE.decodeUtf8 (findAllTLV 1 tlv)
author <- case findTLV 2 tlv of
Just authorBs -> Just <$> parsePubKey authorBs
Nothing -> Right Nothing
Right $ EventPtr eventId relays author
-- | Parse TLV data into list of (type, value) pairs
parseTLV :: ByteString -> Either Text [(Word8, ByteString)]
parseTLV bs = go bs []
where
go remaining acc
| BS.null remaining = Right (reverse acc)
| BS.length remaining < 2 = Left "Incomplete TLV entry"
| otherwise = do
let typ = BS.head remaining
let len = fromIntegral (BS.index remaining 1) :: Int
let remaining' = BS.drop 2 remaining
if BS.length remaining' < len
then Left "TLV value truncated"
else do
let val = BS.take len remaining'
let rest = BS.drop len remaining'
go rest ((typ, val) : acc)
-- | Find first TLV entry of given type
findTLV :: Word8 -> [(Word8, ByteString)] -> Maybe ByteString
findTLV typ = lookup typ
-- | Find all TLV entries of given type
findAllTLV :: Word8 -> [(Word8, ByteString)] -> [ByteString]
findAllTLV typ = mapMaybe (\(t, v) -> if t == typ then Just v else Nothing)