packages feed

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

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

module BtcLsp.Thread.Refunder
  ( apply,
    SendUtxosResult (..),
  )
where

import BtcLsp.Data.Orphan ()
import BtcLsp.Import
import qualified BtcLsp.Import.Psql as Psql
import qualified BtcLsp.Math.OnChain as Math
import BtcLsp.Psbt.Utils
  ( releaseUtxosLocks,
    releaseUtxosPsbtLocks,
    swapUtxoToPsbtUtxo,
  )
import qualified BtcLsp.Storage.Model.SwapIntoLn as SwapIntoLn
import qualified BtcLsp.Storage.Model.SwapUtxo as SwapUtxo
  ( getUtxosForRefundSql,
    updateRefundedSql,
  )
import Data.List (groupBy)
import qualified Data.Map as M
import LndClient (txIdParser)
import qualified LndClient.Data.FinalizePsbt as FNP
import qualified LndClient.Data.FundPsbt as FP
import qualified LndClient.Data.PublishTransaction as PT
import qualified LndClient.RPC.Katip as Lnd
import qualified Network.Bitcoin as Btc
import qualified Network.Bitcoin.Types as Btc

apply :: (Env m) => m ()
apply =
  forever $ do
    runSql $
      SwapUtxo.getUtxosForRefundSql
        >>= mapM_ processRefundSql
          . groupBy (\a b -> swpId a == swpId b)
    sleep300ms
  where
    swpId = entityKey . snd

data SendUtxosResult = SendUtxosResult
  { getGetDecTrx :: Btc.DecodedRawTransaction,
    getTotalAmt :: MSat,
    getFee :: MSat
  }

newtype TxLabel = TxLabel
  { unTxLabel :: Text
  }
  deriving newtype
    ( Show,
      Eq,
      Ord,
      Semigroup
    )

sendUtxos ::
  ( Env m
  ) =>
  Math.SatPerVbyte ->
  [PsbtUtxo] ->
  OnChainAddress 'Refund ->
  TxLabel ->
  ExceptT Failure m SendUtxosResult
sendUtxos feeRate utxos addr txLabel = do
  inQty <- tryFromT "SendUtxos length" $ length utxos
  estFee <-
    tryFailureT "SendUtxos fee estimator" $
      Math.trxEstFee
        (Math.InQty inQty)
        (Math.OutQty 1)
        feeRate
  let finalOutputAmt = totalInputsAmt - estFee
  when (finalOutputAmt < Math.trxDustLimit) . throwE $
    FailureInt . FailurePrivate $
      "Final output amount "
        <> inspectPlain finalOutputAmt
        <> " = "
        <> inspectPlain totalInputsAmt
        <> " - "
        <> inspectPlain estFee
        <> " is below dust limit "
        <> inspectPlain Math.trxDustLimit
  releaseUtxosPsbtLocks utxos
  estPsbt <-
    withLndT
      Lnd.fundPsbt
      ($ newFundPsbtReq feeRate utxos addr finalOutputAmt)
  releaseUtxosLocks $ FP.lockedUtxos estPsbt
  finPsbt <-
    withLndT
      Lnd.finalizePsbt
      ($ FNP.FinalizePsbtRequest (FP.fundedPsbt estPsbt) mempty)
  decodedTrx <-
    withBtcT
      Btc.decodeRawTransaction
      ($ toHex $ FNP.rawFinalTx finPsbt)
  ptRes <-
    withLndT
      Lnd.publishTransaction
      ( $
          PT.PublishTransactionRequest
            (FNP.rawFinalTx finPsbt)
            $ unTxLabel txLabel
      )
  if null $ PT.publishError ptRes
    then pure $ SendUtxosResult decodedTrx totalInputsAmt estFee
    else throwE . FailureInt $ FailurePrivate "Failed to publish refund transaction"
  where
    totalInputsAmt =
      sum $ getAmt <$> utxos

newFundPsbtReq ::
  Math.SatPerVbyte ->
  [PsbtUtxo] ->
  OnChainAddress 'Refund ->
  MSat ->
  FP.FundPsbtRequest
newFundPsbtReq feeRate utxos' outAddr est = do
  let mtpl =
        FP.TxTemplate
          (getOutPoint <$> utxos')
          (M.fromList [(unOnChainAddress outAddr, est)])
  FP.FundPsbtRequest
    { FP.account = mempty,
      FP.template = mtpl,
      FP.minConfs = 2,
      FP.spendUnconfirmed = False,
      FP.fee =
        FP.SatPerVbyte
          . ceiling
          $ Math.unSatPerVbyte feeRate
    }

processRefundSql ::
  ( Env m
  ) =>
  [(Entity SwapUtxo, Entity SwapIntoLn)] ->
  ReaderT Psql.SqlBackend m ()
processRefundSql [] = pure ()
processRefundSql utxos@(x : _) = do
  res <- SwapIntoLn.withLockedRowSql
    (entityKey $ snd x)
    (`elem` swapStatusFinal)
    . const
    $ do
      $(logTM) DebugS . logStr $
        "Start refunding utxos:"
          <> inspect refUtxos
          <> " to address:"
          <> inspect refAddr
      eitherM
        ( \e -> do
            Psql.transactionUndo
            $(logTM) ErrorS . logStr $
              "Failed to refund utxos:"
                <> inspect refUtxos
                <> " to address:"
                <> inspect refAddr
                <> " with error:"
                <> inspect e
        )
        ( \(SendUtxosResult rtx total fee) -> do
            $(logTM) DebugS . logStr $
              "Successfully refunded utxos: "
                <> inspect refUtxos
                <> " to address:"
                <> inspect refAddr
                <> " on chain rawTx:"
                <> inspect rtx
                <> " amount: "
                <> inspect total
                <> " with fee:"
                <> inspect fee
            case txIdParser
              . Btc.unTransactionID
              $ Btc.decTxId rtx of
              Right rtxid ->
                SwapUtxo.updateRefundedSql
                  (entityKey . fst <$> utxos)
                  (from rtxid)
              Left e -> do
                Psql.transactionUndo
                $(logTM) ErrorS . logStr $
                  "Failed to convert txid:" <> inspect e
        )
        . lift
        . runExceptT
        $ sendUtxos
          Math.minFeeRate
          refUtxos
          (coerce refAddr)
          (TxLabel $ "refund to " <> unOnChainAddress refAddr)
  whenLeft res $
    $(logTM) ErrorS
      . logStr
      . ("No refund due to wrong status " <>)
      . inspect
  where
    refAddr = swapIntoLnRefundAddress $ entityVal $ snd x
    refUtxos = swapUtxoToPsbtUtxo . entityVal . fst <$> utxos