packages feed

ppad-bolt1-0.0.1: lib/Lightning/Protocol/BOLT1/Message.hs

{-# OPTIONS_HADDOCK prune #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}

-- |
-- Module: Lightning.Protocol.BOLT1.Message
-- Copyright: (c) 2025 Jared Tobin
-- License: MIT
-- Maintainer: Jared Tobin <jared@ppad.tech>
--
-- Message types for BOLT #1.

module Lightning.Protocol.BOLT1.Message (
  -- * Message types
    MsgType(..)
  , msgTypeWord
  , parseMsgType

  -- * Channel identifiers
  , ChannelId
  , channelId
  , unChannelId
  , allChannels

  -- * Setup messages
  , Init(..)
  , Error(..)
  , Warning(..)

  -- * Control messages
  , Ping(..)
  , Pong(..)

  -- * Peer storage messages
  , PeerStorage(..)
  , PeerStorageRetrieval(..)

  -- * Message envelope
  , Message(..)
  , messageType
  , Envelope(..)
  ) where

import Control.DeepSeq (NFData)
import qualified Data.ByteString as BS
import Data.Word (Word16)
import GHC.Generics (Generic)
import Lightning.Protocol.BOLT1.TLV

-- Message types ---------------------------------------------------------------

-- | BOLT #1 message type codes.
data MsgType
  = MsgInit              -- ^ 16
  | MsgError             -- ^ 17
  | MsgPing              -- ^ 18
  | MsgPong              -- ^ 19
  | MsgWarning           -- ^ 1
  | MsgPeerStorage       -- ^ 7
  | MsgPeerStorageRet    -- ^ 9
  | MsgUnknown !Word16   -- ^ Unknown type
  deriving stock (Eq, Show, Generic)

instance NFData MsgType

-- | Get the numeric type code for a message type.
msgTypeWord :: MsgType -> Word16
msgTypeWord MsgInit            = 16
msgTypeWord MsgError           = 17
msgTypeWord MsgPing            = 18
msgTypeWord MsgPong            = 19
msgTypeWord MsgWarning         = 1
msgTypeWord MsgPeerStorage     = 7
msgTypeWord MsgPeerStorageRet  = 9
msgTypeWord (MsgUnknown w)     = w

-- | Parse a message type from a word.
parseMsgType :: Word16 -> MsgType
parseMsgType 16 = MsgInit
parseMsgType 17 = MsgError
parseMsgType 18 = MsgPing
parseMsgType 19 = MsgPong
parseMsgType 1  = MsgWarning
parseMsgType 7  = MsgPeerStorage
parseMsgType 9  = MsgPeerStorageRet
parseMsgType w  = MsgUnknown w

-- Channel identifiers ---------------------------------------------------------

-- | A 32-byte channel identifier.
--
-- Use 'channelId' to construct, which validates the length.
-- Use 'allChannels' for connection-level errors (all-zeros channel ID).
newtype ChannelId = ChannelId BS.ByteString
  deriving stock (Eq, Show, Generic)

instance NFData ChannelId

-- | Construct a 'ChannelId' from a 32-byte 'BS.ByteString'.
--
-- Returns 'Nothing' if the input is not exactly 32 bytes.
--
-- >>> channelId (BS.replicate 32 0x00)
-- Just (ChannelId "\NUL\NUL...")
-- >>> channelId "too short"
-- Nothing
channelId :: BS.ByteString -> Maybe ChannelId
channelId bs
  | BS.length bs == 32 = Just (ChannelId bs)
  | otherwise          = Nothing
{-# INLINE channelId #-}

-- | The all-zeros channel ID, used for connection-level errors.
--
-- Per BOLT #1, setting channel_id to all zeros means the error applies
-- to the connection rather than a specific channel.
allChannels :: ChannelId
allChannels = ChannelId (BS.replicate 32 0x00)

-- | Extract the raw bytes from a 'ChannelId'.
unChannelId :: ChannelId -> BS.ByteString
unChannelId (ChannelId bs) = bs
{-# INLINE unChannelId #-}

-- Message ADTs ----------------------------------------------------------------

-- | The init message (type 16).
data Init = Init
  { initGlobalFeatures :: !BS.ByteString
  , initFeatures       :: !BS.ByteString
  , initTlvs           :: ![InitTlv]
  } deriving stock (Eq, Show, Generic)

instance NFData Init

-- | The error message (type 17).
data Error = Error
  { errorChannelId :: !ChannelId
  , errorData      :: !BS.ByteString
  } deriving stock (Eq, Show, Generic)

instance NFData Error

-- | The warning message (type 1).
data Warning = Warning
  { warningChannelId :: !ChannelId
  , warningData      :: !BS.ByteString
  } deriving stock (Eq, Show, Generic)

instance NFData Warning

-- | The ping message (type 18).
data Ping = Ping
  { pingNumPongBytes :: {-# UNPACK #-} !Word16
  , pingIgnored      :: !BS.ByteString
  } deriving stock (Eq, Show, Generic)

instance NFData Ping

-- | The pong message (type 19).
data Pong = Pong
  { pongIgnored :: !BS.ByteString
  } deriving stock (Eq, Show, Generic)

instance NFData Pong

-- | The peer_storage message (type 7).
data PeerStorage = PeerStorage
  { peerStorageBlob :: !BS.ByteString
  } deriving stock (Eq, Show, Generic)

instance NFData PeerStorage

-- | The peer_storage_retrieval message (type 9).
data PeerStorageRetrieval = PeerStorageRetrieval
  { peerStorageRetrievalBlob :: !BS.ByteString
  } deriving stock (Eq, Show, Generic)

instance NFData PeerStorageRetrieval

-- | All BOLT #1 messages.
data Message
  = MsgInitVal !Init
  | MsgErrorVal !Error
  | MsgWarningVal !Warning
  | MsgPingVal !Ping
  | MsgPongVal !Pong
  | MsgPeerStorageVal !PeerStorage
  | MsgPeerStorageRetrievalVal !PeerStorageRetrieval
  deriving stock (Eq, Show, Generic)

instance NFData Message

-- | Get the message type for a message.
messageType :: Message -> MsgType
messageType (MsgInitVal _)                 = MsgInit
messageType (MsgErrorVal _)                = MsgError
messageType (MsgWarningVal _)              = MsgWarning
messageType (MsgPingVal _)                 = MsgPing
messageType (MsgPongVal _)                 = MsgPong
messageType (MsgPeerStorageVal _)          = MsgPeerStorage
messageType (MsgPeerStorageRetrievalVal _) = MsgPeerStorageRet

-- Message envelope ------------------------------------------------------------

-- | A complete message envelope with type, payload, and optional extension.
data Envelope = Envelope
  { envType      :: !MsgType
  , envPayload   :: !BS.ByteString
  , envExtension :: !(Maybe TlvStream)
  } deriving stock (Eq, Show, Generic)

instance NFData Envelope