packages feed

haskoin-core-0.21.0: src/Haskoin/Transaction/Common.hs

{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}

{- |
Module      : Haskoin.Transaction.Common
Copyright   : No rights reserved
License     : MIT
Maintainer  : jprupp@protonmail.ch
Stability   : experimental
Portability : POSIX

Code related to transactions parsing and serialization.
-}
module Haskoin.Transaction.Common (
    -- * Transactions
    Tx (..),
    TxIn (..),
    TxOut (..),
    OutPoint (..),
    TxHash (..),
    WitnessData,
    WitnessStack,
    WitnessStackItem,
    txHash,
    hexToTxHash,
    txHashToHex,
    nosigTxHash,
    nullOutPoint,
) where

import Control.Applicative ((<|>))
import Control.DeepSeq
import Control.Monad (
    forM_,
    guard,
    liftM2,
    mzero,
    replicateM,
    unless,
    when,
    (<=<),
 )
import Data.Aeson as A
import Data.Aeson.Encoding (unsafeToEncoding)
import Data.Binary (Binary (..))
import Data.ByteString (ByteString)
import qualified Data.ByteString as B
import Data.ByteString.Builder (char7)
import qualified Data.ByteString.Lazy as BL
import Data.Bytes.Get
import Data.Bytes.Put
import Data.Bytes.Serial
import Data.Hashable (Hashable)
import Data.Maybe (fromMaybe)
import Data.Serialize (Serialize (..))
import Data.String (IsString, fromString)
import Data.String.Conversions (cs)
import Data.Text (Text)
import Data.Word (Word32, Word64)
import GHC.Generics (Generic)
import Haskoin.Crypto.Hash
import Haskoin.Network.Common
import Haskoin.Util
import Text.Read as R

-- | Transaction id: hash of transaction excluding witness data.
newtype TxHash = TxHash {getTxHash :: Hash256}
    deriving (Eq, Ord, Generic, Hashable, Serial, NFData)

instance Serialize TxHash where
    put = serialize
    get = deserialize

instance Binary TxHash where
    put = serialize
    get = deserialize

instance Show TxHash where
    showsPrec _ = shows . txHashToHex

instance Read TxHash where
    readPrec = do
        R.String str <- R.lexP
        maybe R.pfail return $ hexToTxHash $ cs str

instance IsString TxHash where
    fromString s =
        let e = error "Could not read transaction hash from hex string"
         in fromMaybe e $ hexToTxHash $ cs s

instance FromJSON TxHash where
    parseJSON =
        withText "txid" $
            maybe mzero return . hexToTxHash

instance ToJSON TxHash where
    toJSON = A.String . txHashToHex
    toEncoding h =
        unsafeToEncoding $
            char7 '"'
                <> hexBuilder (BL.reverse (runPutL (serialize h)))
                <> char7 '"'

-- | Transaction hash excluding signatures.
nosigTxHash :: Tx -> TxHash
nosigTxHash tx =
    TxHash $
        doubleSHA256 $
            runPutS $
                serialize tx{txIn = map clearInput $ txIn tx}
  where
    clearInput ti = ti{scriptInput = B.empty}

-- | Convert transaction hash to hex form, reversing bytes.
txHashToHex :: TxHash -> Text
txHashToHex (TxHash h) = encodeHex (B.reverse (runPutS (serialize h)))

-- | Convert transaction hash from hex, reversing bytes.
hexToTxHash :: Text -> Maybe TxHash
hexToTxHash hex = do
    bs <- B.reverse <$> decodeHex hex
    h <- either (const Nothing) Just (runGetS deserialize bs)
    return $ TxHash h

-- | Witness stack for SegWit transactions.
type WitnessData = [WitnessStack]

-- | Witness stack for SegWit transactions.
type WitnessStack = [WitnessStackItem]

-- | Witness stack item for SegWit transactions.
type WitnessStackItem = ByteString

-- | Data type representing a transaction.
data Tx = Tx
    { -- | transaction data format version
      txVersion :: !Word32
    , -- | list of transaction inputs
      txIn :: ![TxIn]
    , -- | list of transaction outputs
      txOut :: ![TxOut]
    , -- | witness data for the transaction
      txWitness :: !WitnessData
    , -- | earliest mining height or time
      txLockTime :: !Word32
    }
    deriving (Show, Read, Eq, Ord, Generic, Hashable, NFData)

-- | Compute transaction hash.
txHash :: Tx -> TxHash
txHash tx = TxHash . doubleSHA256 . runPutS $ serialize tx{txWitness = []}

instance IsString Tx where
    fromString =
        fromMaybe e . (eitherToMaybe . runGetS deserialize <=< decodeHex) . cs
      where
        e = error "Could not read transaction from hex string"

instance Serial Tx where
    deserialize =
        isWitnessTx >>= \w -> if w then parseWitnessTx else parseLegacyTx
    serialize tx
        | null (txWitness tx) = putLegacyTx tx
        | otherwise = putWitnessTx tx

instance Binary Tx where
    put = serialize
    get = deserialize

instance Serialize Tx where
    put = serialize
    get = deserialize

putInOut :: MonadPut m => Tx -> m ()
putInOut tx = do
    putVarInt $ length (txIn tx)
    forM_ (txIn tx) serialize
    putVarInt $ length (txOut tx)
    forM_ (txOut tx) serialize

-- | Non-SegWit transaction serializer.
putLegacyTx :: MonadPut m => Tx -> m ()
putLegacyTx tx = do
    putWord32le (txVersion tx)
    putInOut tx
    putWord32le (txLockTime tx)

-- | Witness transaciton serializer.
putWitnessTx :: MonadPut m => Tx -> m ()
putWitnessTx tx = do
    putWord32le (txVersion tx)
    putWord8 0x00
    putWord8 0x01
    putInOut tx
    putWitnessData (txWitness tx)
    putWord32le (txLockTime tx)

isWitnessTx :: MonadGet m => m Bool
isWitnessTx = lookAhead $ do
    _ <- getWord32le
    m <- getWord8
    f <- getWord8
    return (m == 0x00 && f == 0x01)

-- | Non-SegWit transaction deseralizer.
parseLegacyTx :: MonadGet m => m Tx
parseLegacyTx = do
    v <- getWord32le
    is <- replicateList =<< deserialize
    os <- replicateList =<< deserialize
    when (length is == 0x00 && length os == 0x01) $ fail "Witness transaction"
    l <- getWord32le
    return
        Tx
            { txVersion = v
            , txIn = is
            , txOut = os
            , txWitness = []
            , txLockTime = l
            }
  where
    replicateList (VarInt c) = replicateM (fromIntegral c) deserialize

-- | Witness transaction deserializer.
parseWitnessTx :: MonadGet m => m Tx
parseWitnessTx = do
    v <- getWord32le
    m <- getWord8
    f <- getWord8
    unless (m == 0x00 && f == 0x01) $ fail "Not a witness transaction"
    is <- replicateList =<< deserialize
    os <- replicateList =<< deserialize
    w <- parseWitnessData $ length is
    l <- getWord32le
    return
        Tx{txVersion = v, txIn = is, txOut = os, txWitness = w, txLockTime = l}
  where
    replicateList (VarInt c) = replicateM (fromIntegral c) deserialize

-- | Witness data deserializer. Requires count of inputs.
parseWitnessData :: MonadGet m => Int -> m WitnessData
parseWitnessData n = replicateM n parseWitnessStack
  where
    parseWitnessStack = do
        VarInt i <- deserialize
        replicateM (fromIntegral i) parseWitnessStackItem
    parseWitnessStackItem = do
        VarInt i <- deserialize
        getByteString $ fromIntegral i

-- | Witness data serializer.
putWitnessData :: MonadPut m => WitnessData -> m ()
putWitnessData = mapM_ putWitnessStack
  where
    putWitnessStack ws = do
        putVarInt $ length ws
        mapM_ putWitnessStackItem ws
    putWitnessStackItem bs = do
        putVarInt $ B.length bs
        putByteString bs

instance FromJSON Tx where
    parseJSON = withObject "Tx" $ \o ->
        Tx <$> o .: "version"
            <*> o .: "inputs"
            <*> o .: "outputs"
            <*> (mapM (mapM f) =<< o .: "witnessdata")
            <*> o .: "locktime"
      where
        f = maybe mzero return . decodeHex

instance ToJSON Tx where
    toJSON (Tx v i o w l) =
        object
            [ "version" .= v
            , "inputs" .= i
            , "outputs" .= o
            , "witnessdata" .= fmap (fmap encodeHex) w
            , "locktime" .= l
            ]
    toEncoding (Tx v i o w l) =
        pairs
            ( "version" .= v
                <> "inputs" .= i
                <> "outputs" .= o
                <> "witnessdata" .= fmap (fmap encodeHex) w
                <> "locktime" .= l
            )

-- | Data type representing a transaction input.
data TxIn = TxIn
    { -- | output being spent
      prevOutput :: !OutPoint
    , -- | signatures and redeem script
      scriptInput :: !ByteString
    , -- | lock-time using sequence numbers (BIP-68)
      txInSequence :: !Word32
    }
    deriving (Eq, Show, Read, Ord, Generic, Hashable, NFData)

instance Serial TxIn where
    deserialize =
        TxIn <$> deserialize <*> (readBS =<< deserialize) <*> getWord32le
      where
        readBS (VarInt len) = getByteString $ fromIntegral len

    serialize (TxIn o s q) = do
        serialize o
        putVarInt $ B.length s
        putByteString s
        putWord32le q

instance Binary TxIn where
    get = deserialize
    put = serialize

instance Serialize TxIn where
    get = deserialize
    put = serialize

instance FromJSON TxIn where
    parseJSON =
        withObject "TxIn" $ \o ->
            TxIn <$> o .: "prevoutput"
                <*> (maybe mzero return . decodeHex =<< o .: "inputscript")
                <*> o .: "sequence"

instance ToJSON TxIn where
    toJSON (TxIn o s q) =
        object
            [ "prevoutput" .= o
            , "inputscript" .= encodeHex s
            , "sequence" .= q
            ]
    toEncoding (TxIn o s q) =
        pairs
            ( "prevoutput" .= o
                <> "inputscript" .= encodeHex s
                <> "sequence" .= q
            )

-- | Data type representing a transaction output.
data TxOut = TxOut
    { -- | value of output is satoshi
      outValue :: !Word64
    , -- | pubkey script
      scriptOutput :: !ByteString
    }
    deriving (Eq, Show, Read, Ord, Generic, Hashable, NFData)

instance Serial TxOut where
    deserialize = do
        val <- getWord64le
        VarInt len <- deserialize
        TxOut val <$> getByteString (fromIntegral len)

    serialize (TxOut o s) = do
        putWord64le o
        putVarInt $ B.length s
        putByteString s

instance Binary TxOut where
    put = serialize
    get = deserialize

instance Serialize TxOut where
    put = serialize
    get = deserialize

instance FromJSON TxOut where
    parseJSON =
        withObject "TxOut" $ \o ->
            TxOut <$> o .: "value"
                <*> (maybe mzero return . decodeHex =<< o .: "outputscript")

instance ToJSON TxOut where
    toJSON (TxOut o s) =
        object ["value" .= o, "outputscript" .= encodeHex s]
    toEncoding (TxOut o s) =
        pairs ("value" .= o <> "outputscript" .= encodeHex s)

-- | The 'OutPoint' refers to a transaction output being spent.
data OutPoint = OutPoint
    { -- | hash of previous transaction
      outPointHash :: !TxHash
    , -- | position of output in previous transaction
      outPointIndex :: !Word32
    }
    deriving (Show, Read, Eq, Ord, Generic, Hashable, NFData)

instance Serial OutPoint where
    deserialize = do
        (h, i) <- liftM2 (,) deserialize getWord32le
        return $ OutPoint h i
    serialize (OutPoint h i) = serialize h >> putWord32le i

instance Binary OutPoint where
    put = serialize
    get = deserialize

instance Serialize OutPoint where
    put = serialize
    get = deserialize

instance FromJSON OutPoint where
    parseJSON =
        withObject "OutPoint" $ \o ->
            OutPoint <$> o .: "txid" <*> o .: "index"

instance ToJSON OutPoint where
    toJSON (OutPoint h i) = object ["txid" .= h, "index" .= i]
    toEncoding (OutPoint h i) = pairs ("txid" .= h <> "index" .= i)

-- | Outpoint used in coinbase transactions.
nullOutPoint :: OutPoint
nullOutPoint =
    OutPoint
        { outPointHash =
            "0000000000000000000000000000000000000000000000000000000000000000"
        , outPointIndex = maxBound
        }