packages feed

haskoin-store-0.64.0: src/Haskoin/Store/Common.hs

{-# LANGUAGE DeriveAnyClass    #-}
{-# LANGUAGE DeriveGeneric     #-}
{-# LANGUAGE FlexibleContexts  #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase        #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards   #-}
{-# LANGUAGE TupleSections     #-}
module Haskoin.Store.Common
    ( Limits(..)
    , Start(..)
    , StoreReadBase(..)
    , StoreReadExtra(..)
    , StoreWrite(..)
    , StoreEvent(..)
    , PubExcept(..)
    , DataMetrics(..)
    , getActiveBlock
    , getActiveTxData
    , getDefaultBalance
    , getSpenders
    , getTransaction
    , getNumTransaction
    , blockAtOrAfter
    , blockAtOrBefore
    , blockAtOrAfterMTP
    , xPubSummary
    , deriveAddresses
    , deriveFunction
    , deOffset
    , applyLimits
    , applyLimitsC
    , applyLimit
    , applyLimitC
    , sortTxs
    , nub'
    , microseconds
    , streamThings
    , joinDescStreams
    , createDataMetrics
    ) where

import           Conduit                    (ConduitT, await, dropC, mapC,
                                             sealConduitT, takeC, yield, ($$++))
import           Control.DeepSeq            (NFData)
import           Control.Exception          (Exception)
import           Control.Monad              (forM)
import           Control.Monad.Trans        (lift)
import           Control.Monad.Trans.Maybe  (MaybeT (..), runMaybeT)
import           Control.Monad.Trans.Reader (runReaderT)
import           Data.ByteString            (ByteString)
import           Data.Default               (Default (..))
import qualified Data.HashSet               as H
import           Data.Hashable              (Hashable)
import           Data.IntMap.Strict         (IntMap)
import qualified Data.IntMap.Strict         as I
import           Data.List                  (sortOn)
import qualified Data.Map.Strict            as Map
import           Data.Maybe                 (catMaybes, mapMaybe)
import           Data.Ord                   (Down (..))
import           Data.Serialize             (Serialize (..))
import           Data.Time.Clock.System     (getSystemTime, systemNanoseconds,
                                             systemSeconds)
import           Data.Word                  (Word32, Word64)
import           GHC.Generics               (Generic)
import           Haskoin                    (Address, BlockHash,
                                             BlockHeader (..), BlockHeight,
                                             BlockNode (..), KeyIndex,
                                             Network (..), OutPoint (..),
                                             RejectCode (..), Tx (..),
                                             TxHash (..), TxIn (..),
                                             XPubKey (..), deriveAddr,
                                             deriveCompatWitnessAddr,
                                             deriveWitnessAddr,
                                             firstGreaterOrEqual, headerHash,
                                             lastSmallerOrEqual, mtp, pubSubKey,
                                             txHash)
import           Haskoin.Node               (Chain, Peer)
import           Haskoin.Store.Data         (Balance (..), BlockData (..),
                                             DeriveType (..), Spender,
                                             Transaction (..), TxData (..),
                                             TxRef (..), UnixTime, Unspent (..),
                                             XPubBal (..), XPubSpec (..),
                                             XPubSummary (..), XPubUnspent (..),
                                             nullBalance, toTransaction,
                                             zeroBalance)
import qualified System.Metrics             as Metrics
import           System.Metrics.Counter     (Counter)
import qualified System.Metrics.Counter     as Counter
import           UnliftIO                   (MonadIO, liftIO)

type DeriveAddr = XPubKey -> KeyIndex -> Address

type Offset = Word32
type Limit = Word32

data Start
    = AtTx{atTxHash :: !TxHash}
    | AtBlock{atBlockHeight :: !BlockHeight}
    deriving (Eq, Show)

data Limits = Limits
    { limit  :: !Word32
    , offset :: !Word32
    , start  :: !(Maybe Start)
    }
    deriving (Eq, Show)

defaultLimits :: Limits
defaultLimits = Limits { limit = 0, offset = 0, start = Nothing }

instance Default Limits where
    def = defaultLimits

class Monad m => StoreReadBase m where
    getNetwork :: m Network
    getBestBlock :: m (Maybe BlockHash)
    getBlocksAtHeight :: BlockHeight -> m [BlockHash]
    getBlock :: BlockHash -> m (Maybe BlockData)
    getTxData :: TxHash -> m (Maybe TxData)
    getSpender :: OutPoint -> m (Maybe Spender)
    getBalance :: Address -> m (Maybe Balance)
    getUnspent :: OutPoint -> m (Maybe Unspent)
    getMempool :: m [(UnixTime, TxHash)]

class StoreReadBase m => StoreReadExtra m where
    getAddressesTxs :: [Address] -> Limits -> m [TxRef]
    getAddressesUnspents :: [Address] -> Limits -> m [Unspent]
    getInitialGap :: m Word32
    getMaxGap :: m Word32
    getNumTxData :: Word64 -> m [TxData]
    getBalances :: [Address] -> m [Balance]
    getAddressTxs :: Address -> Limits -> m [TxRef]
    getAddressUnspents :: Address -> Limits -> m [Unspent]
    xPubBals :: XPubSpec -> m [XPubBal]
    xPubUnspents :: XPubSpec -> [XPubBal] -> Limits -> m [XPubUnspent]
    xPubTxs :: XPubSpec -> [XPubBal] -> Limits -> m [TxRef]
    xPubTxCount :: XPubSpec -> [XPubBal] -> m Word32

class StoreWrite m where
    setBest :: BlockHash -> m ()
    insertBlock :: BlockData -> m ()
    setBlocksAtHeight :: [BlockHash] -> BlockHeight -> m ()
    insertTx :: TxData -> m ()
    insertSpender :: OutPoint -> Spender -> m ()
    deleteSpender :: OutPoint -> m ()
    insertAddrTx :: Address -> TxRef -> m ()
    deleteAddrTx :: Address -> TxRef -> m ()
    insertAddrUnspent :: Address -> Unspent -> m ()
    deleteAddrUnspent :: Address -> Unspent -> m ()
    addToMempool :: TxHash -> UnixTime -> m ()
    deleteFromMempool :: TxHash -> m ()
    setBalance :: Balance -> m ()
    insertUnspent :: Unspent -> m ()
    deleteUnspent :: OutPoint -> m ()

getSpenders :: StoreReadBase m => TxHash -> m (IntMap Spender)
getSpenders th =
    getActiveTxData th >>= \case
        Nothing -> return I.empty
        Just td -> I.fromList . catMaybes <$>
                   mapM get_spender [0 .. length (txOut (txData td)) - 1]
  where
    get_spender i = fmap (i,) <$> getSpender (OutPoint th (fromIntegral i))

getActiveBlock :: StoreReadExtra m => BlockHash -> m (Maybe BlockData)
getActiveBlock bh = getBlock bh >>= \case
    Just b | blockDataMainChain b -> return (Just b)
    _                             -> return Nothing

getActiveTxData :: StoreReadBase m => TxHash -> m (Maybe TxData)
getActiveTxData th = getTxData th >>= \case
    Just td | not (txDataDeleted td) -> return (Just td)
    _                                -> return Nothing

getDefaultBalance :: StoreReadBase m => Address -> m Balance
getDefaultBalance a = getBalance a >>= \case
    Nothing -> return $ zeroBalance a
    Just  b -> return b

deriveAddresses :: DeriveAddr -> XPubKey -> Word32 -> [(Word32, Address)]
deriveAddresses derive xpub start = map (\i -> (i, derive xpub i)) [start ..]

deriveFunction :: DeriveType -> DeriveAddr
deriveFunction DeriveNormal i = fst . deriveAddr i
deriveFunction DeriveP2SH i   = fst . deriveCompatWitnessAddr i
deriveFunction DeriveP2WPKH i = fst . deriveWitnessAddr i

xPubSummary :: XPubSpec -> [XPubBal] -> XPubSummary
xPubSummary _xspec xbals =
    XPubSummary
    { xPubSummaryConfirmed = sum (map (balanceAmount . xPubBal) bs)
    , xPubSummaryZero = sum (map (balanceZero . xPubBal) bs)
    , xPubSummaryReceived = rx
    , xPubUnspentCount = uc
    , xPubChangeIndex = ch
    , xPubExternalIndex = ex
    }
  where
    bs = filter (not . nullBalance . xPubBal) xbals
    ex = foldl max 0 [i | XPubBal {xPubBalPath = [0, i]} <- bs]
    ch = foldl max 0 [i | XPubBal {xPubBalPath = [1, i]} <- bs]
    uc = sum [balanceUnspentCount (xPubBal b) | b <- bs]
    xt = [b | b@XPubBal {xPubBalPath = [0, _]} <- bs]
    rx = sum [balanceTotalReceived (xPubBal b) | b <- xt]

getTransaction ::
       (Monad m, StoreReadBase m) => TxHash -> m (Maybe Transaction)
getTransaction h = runMaybeT $ do
    d <- MaybeT $ getTxData h
    sm <- lift $ getSpenders h
    return $ toTransaction d sm

getNumTransaction ::
    (Monad m, StoreReadExtra m) => Word64 -> m [Transaction]
getNumTransaction i = do
    ds <- getNumTxData i
    forM ds $ \d -> do
        sm <- getSpenders (txHash (txData d))
        return $ toTransaction d sm

blockAtOrAfter :: (MonadIO m, StoreReadExtra m)
               => Chain
               -> UnixTime
               -> m (Maybe BlockData)
blockAtOrAfter ch q = runMaybeT $ do
    net <- lift getNetwork
    x <- MaybeT $ liftIO $ runReaderT (firstGreaterOrEqual net f) ch
    MaybeT $ getBlock (headerHash (nodeHeader x))
  where
    f x = let t = fromIntegral (blockTimestamp (nodeHeader x))
          in return $ t `compare` q

blockAtOrBefore :: (MonadIO m, StoreReadExtra m)
                => Chain
                -> UnixTime
                -> m (Maybe BlockData)
blockAtOrBefore ch q = runMaybeT $ do
    net <- lift getNetwork
    x <- MaybeT $ liftIO $ runReaderT (lastSmallerOrEqual net f) ch
    MaybeT $ getBlock (headerHash (nodeHeader x))
  where
    f x = let t = fromIntegral (blockTimestamp (nodeHeader x))
          in return $ t `compare` q

blockAtOrAfterMTP :: (MonadIO m, StoreReadExtra m)
                  => Chain
                  -> UnixTime
                  -> m (Maybe BlockData)
blockAtOrAfterMTP ch q = runMaybeT $ do
    net <- lift getNetwork
    x <- MaybeT $ liftIO $ runReaderT (firstGreaterOrEqual net f) ch
    MaybeT $ getBlock (headerHash (nodeHeader x))
  where
    f x = do
        t <- fromIntegral <$> mtp x
        return $ t `compare` q


-- | Events that the store can generate.
data StoreEvent
    = StoreBestBlock !BlockHash
    | StoreMempoolNew !TxHash
    | StoreMempoolDelete !TxHash
    | StorePeerConnected !Peer
    | StorePeerDisconnected !Peer
    | StorePeerPong !Peer !Word64
    | StoreTxAnnounce !Peer ![TxHash]
    | StoreTxReject !Peer !TxHash !RejectCode !ByteString

data PubExcept = PubNoPeers
    | PubReject RejectCode
    | PubTimeout
    | PubPeerDisconnected
    deriving (Eq, NFData, Generic, Serialize)

instance Show PubExcept where
    show PubNoPeers = "no peers"
    show (PubReject c) =
        "rejected: " <>
        case c of
            RejectMalformed       -> "malformed"
            RejectInvalid         -> "invalid"
            RejectObsolete        -> "obsolete"
            RejectDuplicate       -> "duplicate"
            RejectNonStandard     -> "not standard"
            RejectDust            -> "dust"
            RejectInsufficientFee -> "insufficient fee"
            RejectCheckpoint      -> "checkpoint"
    show PubTimeout = "peer timeout or silent rejection"
    show PubPeerDisconnected = "peer disconnected"

instance Exception PubExcept

applyLimits :: Limits -> [a] -> [a]
applyLimits Limits {..} = applyLimit limit . applyOffset offset

applyOffset :: Offset -> [a] -> [a]
applyOffset = drop . fromIntegral

applyLimit :: Limit -> [a] -> [a]
applyLimit 0 = id
applyLimit l = take (fromIntegral l)

deOffset :: Limits -> Limits
deOffset l = case limit l of
    0 -> l{offset = 0}
    _ -> l{limit = limit l + offset l, offset = 0}

applyLimitsC :: Monad m => Limits -> ConduitT i i m ()
applyLimitsC Limits {..} = applyOffsetC offset >> applyLimitC limit

applyOffsetC :: Monad m => Offset -> ConduitT i i m ()
applyOffsetC = dropC . fromIntegral

applyLimitC :: Monad m => Limit -> ConduitT i i m ()
applyLimitC 0 = mapC id
applyLimitC l = takeC (fromIntegral l)

sortTxs :: [Tx] -> [(Word32, Tx)]
sortTxs txs = go [] thset $ zip [0 ..] txs
  where
    thset = H.fromList (map txHash txs)
    go [] _ [] = []
    go orphans ths [] = go [] ths orphans
    go orphans ths ((i, tx):xs) =
      let ops = map (outPointHash . prevOutput) (txIn tx)
          orp = any (`H.member` ths) ops
       in if orp
            then go ((i, tx) : orphans) ths xs
            else (i, tx) : go orphans (txHash tx `H.delete` ths) xs

nub' :: (Eq a, Hashable a) => [a] -> [a]
nub' = H.toList . H.fromList

microseconds :: MonadIO m => m Integer
microseconds =
    let f t = toInteger (systemSeconds t) * 1000000
            + toInteger (systemNanoseconds t) `div` 1000
    in liftIO $ f <$> getSystemTime

streamThings :: Monad m
             => (Limits -> m [a])
             -> Maybe (a -> TxHash)
             -> Limits
             -> ConduitT () a m ()
streamThings getit gettx limits =
    lift (getit limits) >>= \case
    [] -> return ()
    ls -> mapM_ yield ls >> go limits (last ls)
  where
    h l x = case gettx of
        Just g -> Just l{offset = 1, start = Just (AtTx (g x))}
        Nothing -> case limit l of
            0 -> Nothing
            _ -> Just l{offset = offset l + limit l}
    go l x = case h l x of
        Nothing -> return ()
        Just l' -> lift (getit l') >>= \case
            [] -> return ()
            ls -> mapM_ yield ls >> go l' (last ls)

joinDescStreams :: (Monad m, Ord a)
                => [ConduitT () a m ()]
                -> ConduitT () a m ()
joinDescStreams xs = do
    let ss = map sealConduitT xs
    go Nothing =<< g ss
  where
    j (x, y) = (, [x]) <$> y
    g ss = let l = mapMaybe j <$> lift (traverse ($$++ await) ss)
           in Map.fromListWith (++) <$> l
    go m mp = case Map.lookupMax mp of
        Nothing -> return ()
        Just (x, ss) -> do
            case m of
                Nothing -> yield x
                Just x'
                  | x == x' -> return ()
                  | otherwise -> yield x
            mp1 <- g ss
            let mp2 = Map.deleteMax mp
                mp' = Map.unionWith (++) mp1 mp2
            go (Just x) mp'

data DataMetrics =
    DataMetrics
        { dataBestCount    :: !Counter
        , dataBlockCount   :: !Counter
        , dataTxCount      :: !Counter
        , dataSpenderCount :: !Counter
        , dataMempoolCount :: !Counter
        , dataBalanceCount :: !Counter
        , dataUnspentCount :: !Counter
        , dataAddrTxCount  :: !Counter
        , dataXPubBals     :: !Counter
        , dataXPubUnspents :: !Counter
        , dataXPubTxs      :: !Counter
        , dataXPubTxCount  :: !Counter
        }

createDataMetrics :: MonadIO m => Metrics.Store -> m DataMetrics
createDataMetrics s = liftIO $ do
    dataBestCount           <- Metrics.createCounter "data.best_block"     s
    dataBlockCount          <- Metrics.createCounter "data.blocks"         s
    dataTxCount             <- Metrics.createCounter "data.txs"            s
    dataSpenderCount        <- Metrics.createCounter "data.spenders"       s
    dataMempoolCount        <- Metrics.createCounter "data.mempool"        s
    dataBalanceCount        <- Metrics.createCounter "data.balances"       s
    dataUnspentCount        <- Metrics.createCounter "data.unspents"       s
    dataAddrTxCount         <- Metrics.createCounter "data.address_txs"    s
    dataXPubBals            <- Metrics.createCounter "data.xpub_balances"  s
    dataXPubUnspents        <- Metrics.createCounter "data.xpub_unspents"  s
    dataXPubTxs             <- Metrics.createCounter "data.xpub_txs"       s
    dataXPubTxCount         <- Metrics.createCounter "data.xpub_tx_count"  s
    return DataMetrics{..}