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