packages feed

bitcoin-address-0.1: lib/Bitcoin/Address/SegWit.hs

{-# LANGUAGE StrictData #-}
module Bitcoin.Address.SegWit
  ( -- * Version
    Version
  , version
  , unVersion
  , versionOp
  , version0
    -- * Programs
  , Program
  , program
  , programVersion
  , programData
  , renderProgram
    -- ** Standard programs
  , p2wpkh
  , p2wsh
  ) where

import qualified Codec.Binary.Bech32 as Bech32
import qualified Codec.Binary.Bech32.Internal as Bech32
import Control.Monad
import qualified Data.Bitcoin.Script as S
import qualified Data.ByteString as B
import qualified Data.ByteString.Builder as BB
import qualified Data.ByteString.Lazy.Char8 as BL8
import qualified Data.Text.Encoding as T
import Data.Word

import Bitcoin.Address.Hash
import Bitcoin.Address.Internal (op0to16)
import Bitcoin.Address.Settings

--------------------------------------------------------------------------------

-- | A SegWit program. Construct with 'program'.
data Program = Program Version B.ByteString
  deriving (Eq, Ord)

-- | Version and base-16 encoded program data.
instance Show Program where
  showsPrec n (Program v d) = showParen (n > 10) $
    showString "Program " .
    mappend (show v) .
    mappend " " .
    mappend (BL8.unpack (BB.toLazyByteString (BB.byteStringHex d)))

-- | SegWit program version.
programVersion :: Program -> Version
{-# INLINE programVersion #-}
programVersion (Program v _) = v

-- | Raw SegWit program data.
programData :: Program -> B.ByteString
{-# INLINE programData #-}
programData (Program _ x) = x

-- | Construct a 'Program' from its raw bytes.
--
-- __WARINING__ This function will prevent you frow constructing invalid SegWit
-- programs, but won't help you write __meaningful__ programs. Prefer to use
-- safe constructions such as 'p2wpkh' or 'p2wsh' instead.
program
  :: Version
  -> B.ByteString  -- ^ Raw SegWit program bytes.
  -> Maybe Program -- ^ Nothing if program length is invalid for version.
program ver prog = do
  let len = B.length prog
  guard $ case unVersion ver of
     0 -> len == 20 || len == 32
     _ -> len >= 2  && len <= 40
  pure (Program ver prog)

--------------------------------------------------------------------------------

-- | Bech32-encode a 'Program'.
renderProgram :: PrefixSegWit -> Program -> B.ByteString
{-# INLINE renderProgram #-}
renderProgram pre (Program ver prog) =
  let w5ver = toEnum $ fromIntegral $ unVersion ver
      dver  = Bech32.dataPartFromWords [w5ver]
      dprog = Bech32.dataPartFromBytes prog
  in T.encodeUtf8 $ Bech32.encodeLenient (prefixSegWitHRP pre) (dver <> dprog)

--------------------------------------------------------------------------------

-- | The version for of a SegWit 'Program'.
newtype Version = Version Word8
  deriving (Eq, Ord, Show)

-- | Construct a SegWit 'Version'.
--
-- The given 'Word8' must be in the inclusive range [0 … 16].
version :: Word8 -> Maybe Version
{-# INLINE version #-}
version w = do
  guard (w <= 16)
  Just (Version w)

-- | The obtained 'Word8' is in the inclusive range [0 … 16].
unVersion :: Version -> Word8
{-# INLINE unVersion #-}
unVersion (Version w) = w

-- | The 'S.ScriptOp' corresponding to the 'Version', in
-- range ['S.OP_0' … 'S.OP_16']
versionOp :: Version -> S.ScriptOp
{-# INLINE versionOp #-}
versionOp (Version w) = case op0to16 (fromIntegral w) of
  Just op -> op
  Nothing -> undefined -- impossible

--------------------------------------------------------------------------------

-- | SegWit version 0
version0 :: Version
{-# INLINE version0 #-}
Just version0 = version 0

-- | Construct a standard SegWit version 0 P2WPKH program.
p2wpkh :: PubHash160 -> Program
{-# INLINE p2wpkh #-}
p2wpkh pkh = case program version0 (unPubHash160 pkh) of
  Just prog -> prog
  Nothing   -> undefined -- impossible

-- | Construct a standard SegWit version 0 P2WSH program.
p2wsh :: ScriptSHA256 -> Program
{-# INLINE p2wsh #-}
p2wsh sh = case program version0 (unScriptSHA256 sh) of
  Just prog -> prog
  Nothing   -> undefined -- impossible