packages feed

solana-haskell-sdk-1.2.0.0: src/Network/Solana/Core/Borsh.hs

-- |
-- Module      : Network.Solana.Core.Borsh
-- Description : Borsh serialization helpers for common types.
--
-- This module provides helpers for Borsh serialization, which differs from Bincode
-- in specific length encodings and enum tag widths. Both enforce strict 0/1 validation
-- for Option tags and Bool encoding:
--
-- * __String lengths__: Borsh uses 32-bit LE (u32), Bincode uses 64-bit LE (u64).
-- * __Vec counts__: Borsh uses 32-bit LE (u32), Bincode uses 64-bit LE (u64).
-- * __Enum tags__: Borsh uses 8-bit (u8), Bincode uses 32-bit LE (u32).
module Network.Solana.Core.Borsh
  ( putBorshString,
    getBorshString,
    putBorshOption,
    getBorshOption,
    putBorshVec,
    getBorshVec,
    putBorshBool,
    getBorshBool,
  )
where

import Control.Monad (replicateM)
import Data.Binary.Get (Get, getByteString, getWord32le, getWord8)
import Data.Binary.Put (Put, putByteString, putWord32le, putWord8)
import Data.ByteString qualified as BS
import Data.Text qualified as T
import Data.Text.Encoding qualified as TE

-- | Serialize a 'String' in Borsh format: u32 LE byte-length + UTF-8 bytes.
-- The length prefix is a 32-bit little-endian word (u32).
putBorshString :: String -> Put
putBorshString s = do
  let bs = TE.encodeUtf8 (T.pack s)
  putWord32le (fromIntegral (BS.length bs))
  putByteString bs

-- | Deserialize a 'String' in Borsh format: u32 LE byte-length + UTF-8 bytes.
-- Fails if the length exceeds @maxBound :: Int@ or if the bytes are not valid UTF-8.
getBorshString :: Get String
getBorshString = do
  len <- getWord32le
  if len > fromIntegral (maxBound :: Int)
    then fail "getBorshString: length exceeds Int range"
    else do
      bs <- getByteString (fromIntegral len)
      case TE.decodeUtf8' bs of
        Left _ -> fail "getBorshString: invalid UTF-8"
        Right t -> pure (T.unpack t)

-- | Serialize a 'Maybe' value in Borsh format: u8 tag (0 = Nothing, 1 = Just) + optional value.
putBorshOption :: (a -> Put) -> Maybe a -> Put
putBorshOption _ Nothing = putWord8 0
putBorshOption putVal (Just a) = do
  putWord8 1
  putVal a

-- | Deserialize a 'Maybe' value in Borsh format: u8 tag (0 = Nothing, 1 = Just) + optional value.
-- Fails if the tag is not 0 or 1.
getBorshOption :: Get a -> Get (Maybe a)
getBorshOption getVal = do
  tag <- getWord8
  case tag of
    0 -> pure Nothing
    1 -> Just <$> getVal
    _ -> fail $ "getBorshOption: invalid tag " <> show tag <> " (expected 0 or 1)"

-- | Serialize a list in Borsh format: u32 LE count + elements.
putBorshVec :: (a -> Put) -> [a] -> Put
putBorshVec putVal xs = do
  putWord32le (fromIntegral (length xs))
  mapM_ putVal xs

-- | Deserialize a list in Borsh format: u32 LE count + elements.
-- Fails if the count exceeds @maxBound :: Int@.
getBorshVec :: Get a -> Get [a]
getBorshVec getVal = do
  count <- getWord32le
  if count > fromIntegral (maxBound :: Int)
    then fail "getBorshVec: count exceeds Int range"
    else replicateM (fromIntegral count) getVal

-- | Serialize a 'Bool' in Borsh format: u8 (0 = False, 1 = True).
putBorshBool :: Bool -> Put
putBorshBool False = putWord8 0
putBorshBool True = putWord8 1

-- | Deserialize a 'Bool' in Borsh format: u8 (0 = False, 1 = True).
-- Fails if the byte is not 0 or 1.
getBorshBool :: Get Bool
getBorshBool = do
  b <- getWord8
  case b of
    0 -> pure False
    1 -> pure True
    _ -> fail $ "getBorshBool: invalid byte " <> show b <> " (expected 0 or 1)"