packages feed

btc-lsp-0.1.0.0: src/BtcLsp/Thread/BlockScanner.hs

{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeApplications #-}

module BtcLsp.Thread.BlockScanner
  ( apply,
    scan,
    Utxo (..),
    extractRelatedUtxoFromBlock,
    mapVout,
  )
where

import BtcLsp.Data.Orphan ()
import qualified BtcLsp.Data.Smart as Smart
import BtcLsp.Import
import qualified BtcLsp.Import.Psql as Psql
import qualified BtcLsp.Math.OnChain as Math
import BtcLsp.Psbt.Utils (lockUtxo)
import qualified BtcLsp.Storage.Model.Block as Block
import qualified BtcLsp.Storage.Model.SwapIntoLn as SwapIntoLn
import qualified BtcLsp.Storage.Model.SwapUtxo as SwapUtxo
import qualified Data.Vector as V
import LndClient (txIdParser)
import qualified LndClient.Data.FundPsbt as FP
import qualified LndClient.Data.OutPoint as OP
import qualified LndClient.RPC.Katip as Lnd
import qualified Network.Bitcoin as Btc
import qualified Network.Bitcoin.BlockChain as Btc
import qualified Network.Bitcoin.Types as Btc
import qualified Universum

apply :: (Env m) => m ()
apply =
  forever $ do
    eitherM
      ( $(logTM) ErrorS
          . logStr
          . inspect
      )
      maybeFunded
      . runExceptT
      $ monitorOnChainLiquidity >> scan
    sleep300ms

maybeFunded :: (Env m) => [Utxo] -> m ()
maybeFunded [] =
  pure ()
maybeFunded utxos =
  mapM_ maybeFundSwap
    . nubOrd
    $ utxoSwapId <$> utxos

maybeFundSwap :: (Env m) => SwapIntoLnId -> m ()
maybeFundSwap swapId = do
  res <- runSql
    . SwapIntoLn.withLockedRowSql
      swapId
      (`elem` swapStatusChain)
    $ \swapVal -> do
      us <- SwapUtxo.getSpendableUtxosBySwapIdSql swapId
      let amt = sum $ swapUtxoAmount . entityVal <$> us
      mCap <- lift $ newSwapCapM amt
      $(logTM) DebugS . logStr $
        maybe
          ( "Not enough funds for "
              <> inspect swapId
              <> " with amt = "
              <> inspect amt
          )
          ( \cap ->
              "Marking funded "
                <> inspect swapId
                <> " with amt = "
                <> inspect amt
                <> " and cap = "
                <> inspect cap
          )
          mCap
      whenJust mCap $ \swapCap -> do
        qty <-
          SwapUtxo.updateUnspentChanReserveSql $
            entityKey <$> us
        if qty /= from (length us)
          then do
            Psql.transactionUndo
            $(logTM) ErrorS . logStr $
              "Funding update "
                <> inspect swapVal
                <> " failed for UTXOs "
                <> inspect us
          else
            SwapIntoLn.updateWaitingPeerSql
              swapId
              swapCap
  whenLeft res $
    $(logTM) WarningS
      . logStr
      . ("Funding failed due to wrong status " <>)
      . inspect

data Utxo = Utxo
  { utxoAmt :: MSat,
    utxoVout :: Vout 'Funding,
    utxoTxId :: TxId 'Funding,
    utxoSwapId :: SwapIntoLnId,
    utxoLockId :: Maybe UtxoLockId
  }
  deriving stock (Show)

mapVout ::
  ( Env m
  ) =>
  Btc.TransactionID ->
  Btc.TxOut ->
  m (Maybe Utxo)
mapVout txid (Btc.TxOut amt vout (Btc.StandardScriptPubKeyV22 _ _ _ addr)) =
  handleAddr addr amt vout txid
mapVout txid txout@(Btc.TxOut amt vout (Btc.StandardScriptPubKey _ _ _ _ addrsV)) =
  case V.toList addrsV of
    [addr] -> handleAddr addr amt vout txid
    _ -> do
      $(logTM) ErrorS . logStr $
        "Unsupported address vector in txid = "
          <> inspect txid
          <> " and txout = "
          <> Universum.show txout
      pure Nothing
mapVout _ (Btc.TxOut _ _ Btc.NonStandardScriptPubKey {}) =
  pure Nothing

handleAddr ::
  ( Env m
  ) =>
  Btc.Address ->
  Btc.BTC ->
  Integer ->
  Btc.TransactionID ->
  m (Maybe Utxo)
handleAddr addr amt vout txid = do
  mswp <- maybeSwap addr
  case mswp of
    Just swp ->
      newUtxo
        (Math.trySatToMsat amt)
        (tryFrom vout)
        (txIdParser $ Btc.unTransactionID txid)
        swp
    Nothing ->
      pure Nothing

newUtxo ::
  ( Env m
  ) =>
  Either Failure MSat ->
  Either (TryFromException Integer (Vout 'Funding)) (Vout 'Funding) ->
  Either LndError ByteString ->
  Entity SwapIntoLn ->
  m (Maybe Utxo)
newUtxo (Right amt) (Right n) (Right txid) swp =
  pure . Just $
    Utxo amt n (from txid) (entityKey swp) Nothing
newUtxo amt vout txid swp = do
  $(logTM) ErrorS . logStr $
    "TryFrom overflow error amt = "
      <> Universum.show amt
      <> " vout = "
      <> Universum.show vout
      <> " txid = "
      <> inspect txid
      <> " and swap = "
      <> inspect swp
  pure Nothing

extractRelatedUtxoFromBlock ::
  ( Env m
  ) =>
  Btc.BlockVerbose ->
  m [Utxo]
extractRelatedUtxoFromBlock blk =
  foldrM foldTrx [] $
    Btc.vSubTransactions blk
  where
    foldTrx trx acc = do
      utxos <-
        mapM (mapVout $ Btc.decTxId trx)
          . V.toList
          $ Btc.decVout trx
      pure $
        catMaybes utxos <> acc

persistBlockT ::
  ( Storage m,
    Env m
  ) =>
  Btc.BlockVerbose ->
  [Utxo] ->
  ExceptT Failure m ()
persistBlockT blk utxos = do
  height <-
    tryFromT "persistBlockT block height" $
      Btc.vBlkHeight blk
  lift . runSql $ do
    blockId <-
      entityKey
        <$> Block.createUpdateConfirmedSql
          height
          (from $ Btc.vBlockHash blk)
    ct <-
      getCurrentTime
    res <-
      Block.withLockedRowSql blockId (== BlkConfirmed)
        . const
        $ do
          SwapUtxo.createIgnoreManySql $
            newSwapUtxo ct blockId <$> utxos
          SwapUtxo.updateRefundBlockIdSql blockId
    whenLeft res $
      $(logTM) ErrorS
        . logStr
        . ("UTXOs are not persisted for the block " <>)
        . inspect

newSwapUtxo :: UTCTime -> BlockId -> Utxo -> SwapUtxo
newSwapUtxo ct blkId utxo = do
  SwapUtxo
    { swapUtxoSwapIntoLnId = utxoSwapId utxo,
      swapUtxoBlockId = blkId,
      swapUtxoTxid = utxoTxId utxo,
      swapUtxoVout = utxoVout utxo,
      swapUtxoAmount = from amt,
      swapUtxoStatus =
        if amt >= Math.trxDustLimit
          then SwapUtxoUnspent
          else SwapUtxoUnspentDust,
      swapUtxoRefundTxId = Nothing,
      swapUtxoRefundBlockId = Nothing,
      swapUtxoLockId = utxoLockId utxo,
      swapUtxoInsertedAt = ct,
      swapUtxoUpdatedAt = ct
    }
  where
    amt = utxoAmt utxo

scan ::
  ( Env m
  ) =>
  ExceptT Failure m [Utxo]
scan = do
  mBlk <- lift $ runSql Block.getLatestSql
  cHeight <-
    tryFromT "BlockScanner block count"
      =<< withBtcT Btc.getBlockCount id
  case mBlk of
    Nothing -> do
      $(logTM) DebugS . logStr $
        "Found no blocks, scanning height = "
          <> inspect cHeight
      scanOneBlock cHeight
    Just lBlk -> do
      reorgDetected <- detectReorg lBlk
      case reorgDetected of
        Nothing -> do
          let known = from . blockHeight $ entityVal lBlk
          scannerStep [] (1 + known) $ from cHeight
        Just height -> do
          $(logTM) WarningS . logStr $
            "Reorg detected from height = "
              <> inspect height
          bHeight <-
            tryFromT "BlockScanner reorg block height" height
          withExceptT
            ( FailureInt
                . FailurePrivate
                . ("Block scanner failed due to bad status " <>)
                . inspectPlain
            )
            . ExceptT
            . runSql
            . Block.withLockedRowSql
              (entityKey lBlk)
              (== BlkConfirmed)
            . const
            $ do
              blks <- Block.getBlocksHigherSql bHeight
              Block.updateOrphanHigherSql bHeight
              SwapUtxo.revertRefundedSql (entityKey <$> blks)
              SwapUtxo.updateOrphanSql (entityKey <$> blks)
          scannerStep [] (1 + coerce bHeight) $ from cHeight

scannerStep ::
  ( Env m
  ) =>
  [Utxo] ->
  Word64 ->
  Word64 ->
  ExceptT Failure m [Utxo]
scannerStep acc cur end =
  if cur > end
    then pure acc
    else do
      $(logTM) DebugS . logStr $
        "Scanner step cur = "
          <> inspect cur
          <> " end = "
          <> inspect end
          <> " got utxos qty = "
          <> inspect (length acc)
      utxos <- scanOneBlock (BlkHeight cur)
      scannerStep (acc <> utxos) (cur + 1) end

detectReorg ::
  ( Env m
  ) =>
  Entity Block ->
  ExceptT Failure m (Maybe Btc.BlockHeight)
detectReorg blk = do
  cReorgHeight <- checkReorgHeight cHeight
  pure $
    if cReorgHeight == cHeight
      then Nothing
      else Just cReorgHeight
  where
    cHeight =
      from . blockHeight $ entityVal blk

checkReorgHeight ::
  ( Env m
  ) =>
  Btc.BlockHeight ->
  ExceptT Failure m Btc.BlockHeight
checkReorgHeight bHeight = do
  res <- compareHash bHeight
  case res of
    Just True -> pure bHeight
    Just False -> checkReorgHeight (bHeight - 1)
    Nothing -> pure bHeight

compareHash ::
  ( Env m
  ) =>
  Btc.BlockHeight ->
  ExceptT Failure m (Maybe Bool)
compareHash height = do
  w64h <- tryFromT "BlockScanner compareHash" height
  cHash <- withBtcT Btc.getBlockHash ($ height)
  lift
    . ( (== cHash)
          . coerce
          . blockHash
          . entityVal
          <<$>>
      )
    . (listToMaybe <$>)
    . runSql
    . Block.getBlockByHeightSql
    $ BlkHeight w64h

scanOneBlock ::
  ( Env m
  ) =>
  BlkHeight ->
  ExceptT Failure m [Utxo]
scanOneBlock height = do
  hash <- withBtcT Btc.getBlockHash ($ from height)
  blk <- withBtcT Btc.getBlockVerbose ($ hash)
  $(logTM) DebugS . logStr $
    "Got new block with height = "
      <> inspect height
      <> " and hash = "
      <> inspect hash
  utxos <- lift $ extractRelatedUtxoFromBlock blk
  lockedUtxos <- mapM lockUtxo' utxos
  persistBlockT blk lockedUtxos
  pure utxos

--
-- TODO : Verify that it's possible to lock already locked UTXO.
-- It's corner case where UTXO has been locked but storage
-- procedure later failed.
--

utxoToOutPoint :: Utxo -> OP.OutPoint
utxoToOutPoint u = OP.OutPoint (coerce $ utxoTxId u) (coerce $ utxoVout u)

lockUtxo' :: Env m => Utxo -> ExceptT Failure m Utxo
lockUtxo' u = do
  l <- lockUtxo (utxoToOutPoint u)
  pure $ u {utxoLockId = Just $ UtxoLockId $ FP.id l}

maybeSwap ::
  ( Env m
  ) =>
  Btc.Address ->
  m (Maybe (Entity SwapIntoLn))
maybeSwap =
  runSql
    . SwapIntoLn.getByFundAddressSql
    . Smart.unsafeNewOnChainAddress

monitorOnChainLiquidity ::
  ( Env m
  ) =>
  ExceptT Failure m ()
monitorOnChainLiquidity =
  withLndT Lnd.walletBalance id
    >>= lift . monitorTotalOnChainLiquidity