packages feed

haskoin-store-0.64.1: src/Haskoin/Store/Logic.hs

{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TupleSections #-}

module Haskoin.Store.Logic (
    ImportException (..),
    MonadImport,
    initBest,
    revertBlock,
    importBlock,
    newMempoolTx,
    deleteUnconfirmedTx,
) where

import Control.Monad (
    forM,
    forM_,
    guard,
    unless,
    void,
    when,
    zipWithM_,
    (<=<),
 )
import Control.Monad.Except (MonadError, throwError)
import Control.Monad.Logger (
    MonadLoggerIO (..),
    logDebugS,
    logErrorS,
 )
import qualified Data.ByteString as B
import Data.Either (rights)
import Data.HashSet (HashSet)
import qualified Data.HashSet as HashSet
import qualified Data.IntMap.Strict as I
import Data.List (nub)
import Data.Maybe (
    catMaybes,
    fromMaybe,
    isJust,
    isNothing,
 )
import Data.Serialize (encode)
import Data.String.Conversions (cs)
import Data.Word (Word32, Word64)
import Haskoin (
    Address,
    Block (..),
    BlockHash,
    BlockHeader (..),
    BlockNode (..),
    Network (..),
    OutPoint (..),
    Tx (..),
    TxHash,
    TxIn (..),
    TxOut (..),
    blockHashToHex,
    computeSubsidy,
    eitherToMaybe,
    genesisBlock,
    genesisNode,
    headerHash,
    isGenesis,
    nullOutPoint,
    scriptToAddressBS,
    txHash,
    txHashToHex,
 )
import Haskoin.Store.Common
import Haskoin.Store.Data (
    Balance (..),
    BlockData (..),
    BlockRef (..),
    Prev (..),
    Spender (..),
    TxData (..),
    TxRef (..),
    UnixTime,
    Unspent (..),
    confirmed,
 )
import UnliftIO (Exception)

type MonadImport m =
    ( MonadError ImportException m
    , MonadLoggerIO m
    , StoreReadBase m
    , StoreWrite m
    )

data ImportException
    = PrevBlockNotBest
    | Orphan
    | UnexpectedCoinbase
    | BestBlockNotFound
    | BlockNotBest
    | TxNotFound
    | DoubleSpend
    | TxConfirmed
    | InsufficientFunds
    | DuplicatePrevOutput
    | TxSpent
    deriving (Eq, Ord, Exception)

instance Show ImportException where
    show PrevBlockNotBest = "Previous block not best"
    show Orphan = "Orphan"
    show UnexpectedCoinbase = "Unexpected coinbase"
    show BestBlockNotFound = "Best block not found"
    show BlockNotBest = "Block not best"
    show TxNotFound = "Transaction not found"
    show DoubleSpend = "Double spend"
    show TxConfirmed = "Transaction confirmed"
    show InsufficientFunds = "Insufficient funds"
    show DuplicatePrevOutput = "Duplicate previous output"
    show TxSpent = "Transaction is spent"

initBest :: MonadImport m => m ()
initBest = do
    $(logDebugS) "BlockStore" "Initializing best block"
    net <- getNetwork
    m <- getBestBlock
    when (isNothing m) . void $ do
        $(logDebugS) "BlockStore" "Importing Genesis block"
        importBlock (genesisBlock net) (genesisNode net)

newMempoolTx :: MonadImport m => Tx -> UnixTime -> m Bool
newMempoolTx tx w =
    getActiveTxData (txHash tx) >>= \case
        Just _ ->
            return False
        Nothing -> do
            freeOutputs True True tx
            rbf <- isRBF (MemRef w) tx
            checkNewTx tx
            importTx (MemRef w) w rbf tx
            return True

bestBlockData :: MonadImport m => m BlockData
bestBlockData = do
    h <-
        getBestBlock >>= \case
            Nothing -> do
                $(logErrorS) "BlockStore" "Best block unknown"
                throwError BestBlockNotFound
            Just h -> return h
    getBlock h >>= \case
        Nothing -> do
            $(logErrorS) "BlockStore" "Best block not found"
            throwError BestBlockNotFound
        Just b -> return b

revertBlock :: MonadImport m => BlockHash -> m ()
revertBlock bh = do
    bd <-
        bestBlockData >>= \b ->
            if headerHash (blockDataHeader b) == bh
                then return b
                else do
                    $(logErrorS) "BlockStore" $
                        "Cannot revert non-head block: " <> blockHashToHex bh
                    throwError BlockNotBest
    $(logDebugS) "BlockStore" $
        "Obtained block data for " <> blockHashToHex bh
    tds <- mapM getImportTxData (blockDataTxs bd)
    $(logDebugS) "BlockStore" $
        "Obtained import tx data for block " <> blockHashToHex bh
    setBest (prevBlock (blockDataHeader bd))
    $(logDebugS) "BlockStore" $
        "Set parent as best block "
            <> blockHashToHex (prevBlock (blockDataHeader bd))
    insertBlock bd{blockDataMainChain = False}
    $(logDebugS) "BlockStore" $
        "Updated as not in main chain: " <> blockHashToHex bh
    forM_ (tail tds) unConfirmTx
    $(logDebugS) "BlockStore" $
        "Unconfirmed " <> cs (show (length tds)) <> " transactions"
    deleteConfirmedTx (txHash (txData (head tds)))
    $(logDebugS) "BlockStore" $
        "Deleted coinbase: " <> txHashToHex (txHash (txData (head tds)))

checkNewBlock :: MonadImport m => Block -> BlockNode -> m ()
checkNewBlock b n =
    getBestBlock >>= \case
        Nothing
            | isGenesis n -> return ()
            | otherwise -> do
                $(logErrorS) "BlockStore" $
                    "Cannot import non-genesis block: "
                        <> blockHashToHex (headerHash (blockHeader b))
                throwError BestBlockNotFound
        Just h
            | prevBlock (blockHeader b) == h -> return ()
            | otherwise -> do
                $(logErrorS) "BlockStore" $
                    "Block does not build on head: "
                        <> blockHashToHex (headerHash (blockHeader b))
                throwError PrevBlockNotBest

importOrConfirm :: MonadImport m => BlockNode -> [Tx] -> m ()
importOrConfirm bn txns = do
    mapM_ (freeOutputs True False . snd) (reverse txs)
    mapM_ (uncurry action) txs
  where
    txs = sortTxs txns
    br i = BlockRef{blockRefHeight = nodeHeight bn, blockRefPos = i}
    bn_time = fromIntegral . blockTimestamp $ nodeHeader bn
    action i tx =
        testPresent tx >>= \case
            False -> import_it i tx
            True -> confirm_it i tx
    confirm_it i tx =
        getActiveTxData (txHash tx) >>= \case
            Just t -> do
                $(logDebugS) "BlockStore" $
                    "Confirming tx: "
                        <> txHashToHex (txHash tx)
                confirmTx t (br i)
                return Nothing
            Nothing -> do
                $(logErrorS) "BlockStore" $
                    "Cannot find tx to confirm: "
                        <> txHashToHex (txHash tx)
                throwError TxNotFound
    import_it i tx = do
        $(logDebugS) "BlockStore" $
            "Importing tx: " <> txHashToHex (txHash tx)
        importTx (br i) bn_time False tx
        return Nothing

importBlock :: MonadImport m => Block -> BlockNode -> m ()
importBlock b n = do
    $(logDebugS) "BlockStore" $
        "Checking new block: "
            <> blockHashToHex (headerHash (nodeHeader n))
    checkNewBlock b n
    $(logDebugS) "BlockStore" "Passed check"
    net <- getNetwork
    let subsidy = computeSubsidy net (nodeHeight n)
    bs <- getBlocksAtHeight (nodeHeight n)
    $(logDebugS) "BlockStore" $
        "Inserting block entries for: "
            <> blockHashToHex (headerHash (nodeHeader n))
    insertBlock
        BlockData
            { blockDataHeight = nodeHeight n
            , blockDataMainChain = True
            , blockDataWork = nodeWork n
            , blockDataHeader = nodeHeader n
            , blockDataSize = fromIntegral (B.length (encode b))
            , blockDataTxs = map txHash (blockTxns b)
            , blockDataWeight = if getSegWit net then w else 0
            , blockDataSubsidy = subsidy
            , blockDataFees = cb_out_val - subsidy
            , blockDataOutputs = ts_out_val
            }
    setBlocksAtHeight
        (nub (headerHash (nodeHeader n) : bs))
        (nodeHeight n)
    setBest (headerHash (nodeHeader n))
    importOrConfirm n (blockTxns b)
    $(logDebugS) "BlockStore" $
        "Finished importing transactions for: "
            <> blockHashToHex (headerHash (nodeHeader n))
  where
    cb_out_val =
        sum $ map outValue $ txOut $ head $ blockTxns b
    ts_out_val =
        sum $ map (sum . map outValue . txOut) $ tail $ blockTxns b
    w =
        let f t = t{txWitness = []}
            b' = b{blockTxns = map f (blockTxns b)}
            x = B.length (encode b)
            s = B.length (encode b')
         in fromIntegral $ s * 3 + x

checkNewTx :: MonadImport m => Tx -> m ()
checkNewTx tx = do
    when (unique_inputs < length (txIn tx)) $ do
        $(logErrorS) "BlockStore" $
            "Transaction spends same output twice: "
                <> txHashToHex (txHash tx)
        throwError DuplicatePrevOutput
    us <- getUnspentOutputs tx
    when (any isNothing us) $ do
        $(logErrorS) "BlockStore" $
            "Orphan: " <> txHashToHex (txHash tx)
        throwError Orphan
    when (isCoinbase tx) $ do
        $(logErrorS) "BlockStore" $
            "Coinbase cannot be imported into mempool: "
                <> txHashToHex (txHash tx)
        throwError UnexpectedCoinbase
    when (length (prevOuts tx) > length us) $ do
        $(logErrorS) "BlockStore" $
            "Orphan: " <> txHashToHex (txHash tx)
        throwError Orphan
    when (outputs > unspents us) $ do
        $(logErrorS) "BlockStore" $
            "Insufficient funds for tx: " <> txHashToHex (txHash tx)
        throwError InsufficientFunds
  where
    unspents = sum . map unspentAmount . catMaybes
    outputs = sum (map outValue (txOut tx))
    unique_inputs = length (nub' (map prevOutput (txIn tx)))

getUnspentOutputs :: StoreReadBase m => Tx -> m [Maybe Unspent]
getUnspentOutputs tx = mapM getUnspent (prevOuts tx)

prepareTxData :: Bool -> BlockRef -> Word64 -> Tx -> [Unspent] -> TxData
prepareTxData rbf br tt tx us =
    TxData
        { txDataBlock = br
        , txData = tx
        , txDataPrevs = ps
        , txDataDeleted = False
        , txDataRBF = rbf
        , txDataTime = tt
        }
  where
    mkprv u = Prev (unspentScript u) (unspentAmount u)
    ps = I.fromList $ zip [0 ..] $ map mkprv us

importTx ::
    MonadImport m =>
    BlockRef ->
    -- | unix time
    Word64 ->
    -- | RBF
    Bool ->
    Tx ->
    m ()
importTx br tt rbf tx = do
    mus <- getUnspentOutputs tx
    us <- forM mus $ \case
        Nothing -> do
            $(logErrorS) "BlockStore" $
                "Attempted to import a tx missing UTXO: "
                    <> txHashToHex (txHash tx)
            throwError Orphan
        Just u -> return u
    let td = prepareTxData rbf br tt tx us
    commitAddTx td

unConfirmTx :: MonadImport m => TxData -> m ()
unConfirmTx t = confTx t Nothing

confirmTx :: MonadImport m => TxData -> BlockRef -> m ()
confirmTx t br = confTx t (Just br)

replaceAddressTx :: MonadImport m => TxData -> BlockRef -> m ()
replaceAddressTx t new = forM_ (txDataAddresses t) $ \a -> do
    deleteAddrTx
        a
        TxRef
            { txRefBlock = txDataBlock t
            , txRefHash = txHash (txData t)
            }
    insertAddrTx
        a
        TxRef
            { txRefBlock = new
            , txRefHash = txHash (txData t)
            }

adjustAddressOutput ::
    MonadImport m =>
    OutPoint ->
    TxOut ->
    BlockRef ->
    BlockRef ->
    m ()
adjustAddressOutput op o old new = do
    let pk = scriptOutput o
    getUnspent op >>= \case
        Nothing -> return ()
        Just u -> do
            unless (unspentBlock u == old) $
                error $ "Existing unspent block bad for output: " <> show op
            replace_unspent pk
  where
    replace_unspent pk = do
        let ma = eitherToMaybe (scriptToAddressBS pk)
        deleteUnspent op
        insertUnspent
            Unspent
                { unspentBlock = new
                , unspentPoint = op
                , unspentAmount = outValue o
                , unspentScript = pk
                , unspentAddress = ma
                }
        forM_ ma $ replace_addr_unspent pk
    replace_addr_unspent pk a = do
        deleteAddrUnspent
            a
            Unspent
                { unspentBlock = old
                , unspentPoint = op
                , unspentAmount = outValue o
                , unspentScript = pk
                , unspentAddress = Just a
                }
        insertAddrUnspent
            a
            Unspent
                { unspentBlock = new
                , unspentPoint = op
                , unspentAmount = outValue o
                , unspentScript = pk
                , unspentAddress = Just a
                }
        decreaseBalance (confirmed old) a (outValue o)
        increaseBalance (confirmed new) a (outValue o)

confTx :: MonadImport m => TxData -> Maybe BlockRef -> m ()
confTx t mbr = do
    replaceAddressTx t new
    forM_ (zip [0 ..] (txOut (txData t))) $ \(n, o) -> do
        let op = OutPoint (txHash (txData t)) n
        adjustAddressOutput op o old new
    rbf <- isRBF new (txData t)
    let td = t{txDataBlock = new, txDataRBF = rbf}
    insertTx td
    updateMempool td
  where
    new = fromMaybe (MemRef (txDataTime t)) mbr
    old = txDataBlock t

freeOutputs ::
    MonadImport m =>
    -- | only delete transaction if unconfirmed
    Bool ->
    -- | only delete RBF
    Bool ->
    Tx ->
    m ()
freeOutputs memonly rbfcheck tx = do
    let prevs = prevOuts tx
    unspents <- mapM getUnspent prevs
    let spents = [p | (p, Nothing) <- zip prevs unspents]
    spndrs <- catMaybes <$> mapM getSpender spents
    let txids = HashSet.fromList $ filter (/= txHash tx) $ map spenderHash spndrs
    mapM_ (deleteTx memonly rbfcheck) $ HashSet.toList txids

deleteConfirmedTx :: MonadImport m => TxHash -> m ()
deleteConfirmedTx = deleteTx False False

deleteUnconfirmedTx :: MonadImport m => Bool -> TxHash -> m ()
deleteUnconfirmedTx rbfcheck th =
    getActiveTxData th >>= \case
        Just _ -> deleteTx True rbfcheck th
        Nothing ->
            $(logDebugS) "BlockStore" $
                "Not found or already deleted: " <> txHashToHex th

deleteTx ::
    MonadImport m =>
    -- | only delete transaction if unconfirmed
    Bool ->
    -- | only delete RBF
    Bool ->
    TxHash ->
    m ()
deleteTx memonly rbfcheck th = do
    chain <- getChain memonly rbfcheck th
    $(logDebugS) "BlockStore" $
        "Deleting " <> cs (show (length chain))
            <> " txs from chain leading to "
            <> txHashToHex th
    mapM_ (\t -> let h = txHash t in deleteSingleTx h >> return h) chain

getChain ::
    (MonadImport m, MonadLoggerIO m) =>
    -- | only delete transaction if unconfirmed
    Bool ->
    -- | only delete RBF
    Bool ->
    TxHash ->
    m [Tx]
getChain memonly rbfcheck th' = do
    $(logDebugS) "BlockStore" $
        "Getting chain for tx " <> txHashToHex th'
    sort_clean <$> go HashSet.empty (HashSet.singleton th')
  where
    sort_clean = reverse . map snd . sortTxs
    get_tx th =
        getActiveTxData th >>= \case
            Nothing -> do
                $(logDebugS) "BlockStore" $
                    "Transaction not found: " <> txHashToHex th
                return Nothing
            Just td
                | memonly && confirmed (txDataBlock td) -> do
                    $(logErrorS) "BlockStore" $
                        "Transaction already confirmed: "
                            <> txHashToHex th
                    throwError TxConfirmed
                | rbfcheck ->
                    isRBF (txDataBlock td) (txData td) >>= \case
                        True -> return $ Just $ txData td
                        False -> do
                            $(logErrorS) "BlockStore" $
                                "Double-spending transaction: "
                                    <> txHashToHex th
                            throwError DoubleSpend
                | otherwise -> return $ Just $ txData td
    go txs pdg = do
        txs1 <- HashSet.fromList . catMaybes <$> mapM get_tx (HashSet.toList pdg)
        pdg1 <-
            HashSet.fromList . concatMap (map spenderHash . I.elems)
                <$> mapM getSpenders (HashSet.toList pdg)
        let txs' = txs1 <> txs
            pdg' = pdg1 `HashSet.difference` HashSet.map txHash txs'
        if HashSet.null pdg'
            then return $ HashSet.toList txs'
            else go txs' pdg'

deleteSingleTx :: MonadImport m => TxHash -> m ()
deleteSingleTx th =
    getActiveTxData th >>= \case
        Nothing -> do
            $(logErrorS) "BlockStore" $
                "Already deleted: " <> txHashToHex th
            throwError TxNotFound
        Just td -> do
            $(logDebugS) "BlockStore" $
                "Deleting tx: " <> txHashToHex th
            getSpenders th >>= \case
                m
                    | I.null m -> commitDelTx td
                    | otherwise -> do
                        $(logErrorS) "BlockStore" $
                            "Tried to delete spent tx: "
                                <> txHashToHex th
                        throwError TxSpent

commitDelTx :: MonadImport m => TxData -> m ()
commitDelTx = commitModTx False

commitAddTx :: MonadImport m => TxData -> m ()
commitAddTx = commitModTx True

commitModTx :: MonadImport m => Bool -> TxData -> m ()
commitModTx add tx_data = do
    mapM_ mod_addr_tx (txDataAddresses td)
    mod_outputs
    mod_unspent
    insertTx td
    updateMempool td
  where
    tx = txData td
    br = txDataBlock td
    td = tx_data{txDataDeleted = not add}
    tx_ref = TxRef br (txHash tx)
    mod_addr_tx a
        | add = do
            insertAddrTx a tx_ref
            modAddressCount add a
        | otherwise = do
            deleteAddrTx a tx_ref
            modAddressCount add a
    mod_unspent
        | add = spendOutputs tx
        | otherwise = unspendOutputs tx
    mod_outputs
        | add = addOutputs br tx
        | otherwise = delOutputs br tx

updateMempool :: MonadImport m => TxData -> m ()
updateMempool td@TxData{txDataDeleted = True} =
    deleteFromMempool (txHash (txData td))
updateMempool td@TxData{txDataBlock = MemRef t} =
    addToMempool (txHash (txData td)) t
updateMempool td@TxData{txDataBlock = BlockRef{}} =
    deleteFromMempool (txHash (txData td))

spendOutputs :: MonadImport m => Tx -> m ()
spendOutputs tx =
    zipWithM_ (spendOutput (txHash tx)) [0 ..] (prevOuts tx)

addOutputs :: MonadImport m => BlockRef -> Tx -> m ()
addOutputs br tx =
    zipWithM_ (addOutput br . OutPoint (txHash tx)) [0 ..] (txOut tx)

isRBF ::
    StoreReadBase m =>
    BlockRef ->
    Tx ->
    m Bool
isRBF br tx
    | confirmed br = return False
    | otherwise =
        getNetwork >>= \net ->
            if getReplaceByFee net
                then go
                else return False
  where
    go
        | any ((< 0xffffffff - 1) . txInSequence) (txIn tx) = return True
        | otherwise = carry_on
    carry_on =
        let hs = nub' $ map (outPointHash . prevOutput) (txIn tx)
            ck [] = return False
            ck (h : hs') =
                getActiveTxData h >>= \case
                    Nothing -> return False
                    Just t
                        | confirmed (txDataBlock t) -> ck hs'
                        | txDataRBF t -> return True
                        | otherwise -> ck hs'
         in ck hs

addOutput :: MonadImport m => BlockRef -> OutPoint -> TxOut -> m ()
addOutput = modOutput True

delOutput :: MonadImport m => BlockRef -> OutPoint -> TxOut -> m ()
delOutput = modOutput False

modOutput :: MonadImport m => Bool -> BlockRef -> OutPoint -> TxOut -> m ()
modOutput add br op o = do
    mod_unspent
    forM_ ma $ \a -> do
        mod_addr_unspent a u
        modBalance (confirmed br) add a (outValue o)
        modifyReceived a v
  where
    v
        | add = (+ outValue o)
        | otherwise = subtract (outValue o)
    ma = eitherToMaybe (scriptToAddressBS (scriptOutput o))
    u =
        Unspent
            { unspentScript = scriptOutput o
            , unspentBlock = br
            , unspentPoint = op
            , unspentAmount = outValue o
            , unspentAddress = ma
            }
    mod_unspent
        | add = insertUnspent u
        | otherwise = deleteUnspent op
    mod_addr_unspent
        | add = insertAddrUnspent
        | otherwise = deleteAddrUnspent

delOutputs :: MonadImport m => BlockRef -> Tx -> m ()
delOutputs br tx =
    forM_ (zip [0 ..] (txOut tx)) $ \(i, o) -> do
        let op = OutPoint (txHash tx) i
        delOutput br op o

getImportTxData :: MonadImport m => TxHash -> m TxData
getImportTxData th =
    getActiveTxData th >>= \case
        Nothing -> do
            $(logDebugS) "BlockStore" $ "Tx not found: " <> txHashToHex th
            throwError TxNotFound
        Just d -> return d

getTxOut :: Word32 -> Tx -> Maybe TxOut
getTxOut i tx = do
    guard (fromIntegral i < length (txOut tx))
    return $ txOut tx !! fromIntegral i

spendOutput :: MonadImport m => TxHash -> Word32 -> OutPoint -> m ()
spendOutput th ix op = do
    u <-
        getUnspent op >>= \case
            Just u -> return u
            Nothing -> error $ "Could not find UTXO to spend: " <> show op
    deleteUnspent op
    insertSpender op (Spender th ix)
    let pk = unspentScript u
    forM_ (scriptToAddressBS pk) $ \a -> do
        decreaseBalance
            (confirmed (unspentBlock u))
            a
            (unspentAmount u)
        deleteAddrUnspent a u

unspendOutputs :: MonadImport m => Tx -> m ()
unspendOutputs = mapM_ unspendOutput . prevOuts

unspendOutput :: MonadImport m => OutPoint -> m ()
unspendOutput op = do
    t <-
        getActiveTxData (outPointHash op) >>= \case
            Nothing -> error $ "Could not find tx data: " <> show (outPointHash op)
            Just t -> return t
    let o =
            fromMaybe
                (error ("Could not find output: " <> show op))
                (getTxOut (outPointIndex op) (txData t))
        m = eitherToMaybe (scriptToAddressBS (scriptOutput o))
        u =
            Unspent
                { unspentAmount = outValue o
                , unspentBlock = txDataBlock t
                , unspentScript = scriptOutput o
                , unspentPoint = op
                , unspentAddress = m
                }
    deleteSpender op
    insertUnspent u
    forM_ m $ \a -> do
        insertAddrUnspent a u
        increaseBalance (confirmed (unspentBlock u)) a (outValue o)

modifyReceived :: MonadImport m => Address -> (Word64 -> Word64) -> m ()
modifyReceived a f = do
    b <- getDefaultBalance a
    setBalance b{balanceTotalReceived = f (balanceTotalReceived b)}

decreaseBalance :: MonadImport m => Bool -> Address -> Word64 -> m ()
decreaseBalance conf = modBalance conf False

increaseBalance :: MonadImport m => Bool -> Address -> Word64 -> m ()
increaseBalance conf = modBalance conf True

modBalance ::
    MonadImport m =>
    -- | confirmed
    Bool ->
    -- | add
    Bool ->
    Address ->
    Word64 ->
    m ()
modBalance conf add a val = do
    b <- getDefaultBalance a
    setBalance $ (g . f) b
  where
    g b = b{balanceUnspentCount = m 1 (balanceUnspentCount b)}
    f b
        | conf = b{balanceAmount = m val (balanceAmount b)}
        | otherwise = b{balanceZero = m val (balanceZero b)}
    m
        | add = (+)
        | otherwise = subtract

modAddressCount :: MonadImport m => Bool -> Address -> m ()
modAddressCount add a = do
    b <- getDefaultBalance a
    setBalance b{balanceTxCount = f (balanceTxCount b)}
  where
    f
        | add = (+ 1)
        | otherwise = subtract 1

txOutAddrs :: [TxOut] -> [Address]
txOutAddrs = nub' . rights . map (scriptToAddressBS . scriptOutput)

txInAddrs :: [Prev] -> [Address]
txInAddrs = nub' . rights . map (scriptToAddressBS . prevScript)

txDataAddresses :: TxData -> [Address]
txDataAddresses t =
    nub' $ txInAddrs prevs <> txOutAddrs outs
  where
    prevs = I.elems (txDataPrevs t)
    outs = txOut (txData t)

isCoinbase :: Tx -> Bool
isCoinbase = all ((== nullOutPoint) . prevOutput) . txIn

prevOuts :: Tx -> [OutPoint]
prevOuts tx = filter (/= nullOutPoint) (map prevOutput (txIn tx))

testPresent :: StoreReadBase m => Tx -> m Bool
testPresent tx = isJust <$> getActiveTxData (txHash tx)