packages feed

btc-lsp-0.1.0.0: src/BtcLsp/Storage/Model/SwapIntoLn.hs

{-# LANGUAGE TemplateHaskell #-}

module BtcLsp.Storage.Model.SwapIntoLn
  ( createIgnoreSql,
    updateWaitingPeerSql,
    updateWaitingChanSql,
    updateExpiredSql,
    updateSucceededSql,
    getSwapsWaitingPeerSql,
    getSwapsWaitingChanSql,
    getSwapsAboutToExpirySql,
    updateSucceededWithoutInvoiceSql,
    getByUuidSql,
    getByFundAddressSql,
    withLockedRowSql,
    UtxoInfo (..),
    SwapInfo (..),
  )
where

import BtcLsp.Import hiding (Storage (..))
import qualified BtcLsp.Import.Psql as Psql
import qualified BtcLsp.Math.Swap as Math
import qualified BtcLsp.Storage.Util as Util

createIgnoreSql ::
  ( MonadIO m
  ) =>
  Entity User ->
  OnChainAddress 'Fund ->
  OnChainAddress 'Gain ->
  OnChainAddress 'Refund ->
  UTCTime ->
  Privacy ->
  ReaderT Psql.SqlBackend m (Entity SwapIntoLn)
createIgnoreSql userEnt fundAddr feeAndChangeAddr refundAddr expAt chanPrivacy = do
  ct <- getCurrentTime
  uuid <- newUuid
  --
  -- NOTE : Set initial amount to zero because
  -- we don't know how much user will deposit
  -- into on-chain address.
  --
  Psql.upsertBy
    (UniqueSwapIntoLnFundAddress fundAddr)
    SwapIntoLn
      { swapIntoLnUuid = uuid,
        swapIntoLnUserId = entityKey userEnt,
        swapIntoLnFundAddress = fundAddr,
        swapIntoLnLspFeeAndChangeAddress = feeAndChangeAddr,
        swapIntoLnRefundAddress = refundAddr,
        swapIntoLnChanCapUser = Money 0,
        swapIntoLnChanCapLsp = Money 0,
        swapIntoLnFeeLsp = Money 0,
        swapIntoLnFeeMiner = Money 0,
        swapIntoLnStatus = SwapWaitingFundChain,
        swapIntoLnPrivacy = chanPrivacy,
        swapIntoLnExpiresAt = expAt,
        swapIntoLnInsertedAt = ct,
        swapIntoLnUpdatedAt = ct
      }
    [ SwapIntoLnUpdatedAt
        Psql.=. Psql.val ct
    ]

updateWaitingPeerSql ::
  ( MonadIO m
  ) =>
  SwapIntoLnId ->
  SwapCap ->
  ReaderT Psql.SqlBackend m ()
updateWaitingPeerSql sid cap = do
  ct <- getCurrentTime
  Psql.update $ \row -> do
    Psql.set
      row
      [ SwapIntoLnChanCapUser
          Psql.=. Psql.val (swapCapUsr cap),
        SwapIntoLnChanCapLsp
          Psql.=. Psql.val (swapCapLsp cap),
        SwapIntoLnFeeLsp
          Psql.=. Psql.val (swapCapFee cap),
        SwapIntoLnStatus
          Psql.=. Psql.val SwapWaitingPeer,
        SwapIntoLnUpdatedAt
          Psql.=. Psql.val ct
      ]
    Psql.where_ $
      ( row Psql.^. SwapIntoLnId
          Psql.==. Psql.val sid
      )
        Psql.&&. ( row Psql.^. SwapIntoLnStatus
                     `Psql.in_` Psql.valList
                       [ SwapWaitingFundChain,
                         SwapWaitingPeer
                       ]
                 )

updateWaitingChanSql ::
  ( MonadIO m
  ) =>
  SwapIntoLnId ->
  ReaderT Psql.SqlBackend m ()
updateWaitingChanSql id0 = do
  ct <- getCurrentTime
  Psql.update $ \row -> do
    Psql.set
      row
      [ SwapIntoLnStatus
          Psql.=. Psql.val SwapWaitingChan,
        SwapIntoLnUpdatedAt
          Psql.=. Psql.val ct
      ]
    Psql.where_ $
      ( row Psql.^. SwapIntoLnId
          Psql.==. Psql.val id0
      )
        Psql.&&. ( row Psql.^. SwapIntoLnStatus
                     Psql.==. Psql.val SwapWaitingPeer
                 )

updateExpiredSql ::
  ( MonadIO m,
    KatipContext m
  ) =>
  SwapIntoLnId ->
  ReaderT Psql.SqlBackend m ()
updateExpiredSql rowId = do
  ct <- getCurrentTime
  qty <- Psql.updateCount $ \row -> do
    Psql.set
      row
      [ SwapIntoLnStatus
          Psql.=. Psql.val SwapExpired,
        SwapIntoLnUpdatedAt
          Psql.=. Psql.val ct
      ]
    Psql.where_ $
      ( row Psql.^. SwapIntoLnId
          Psql.==. Psql.val rowId
      )
        Psql.&&. ( row Psql.^. SwapIntoLnStatus
                     `Psql.in_` Psql.valList
                       [ SwapWaitingFundChain,
                         SwapWaitingPeer
                       ]
                 )
  when (qty /= 1)
    . $(logTM) ErrorS
    . logStr
    $ "Wrong expiry update result "
      <> inspect qty
      <> " for the swap "
      <> inspect rowId

updateSucceededWithoutInvoiceSql ::
  ( MonadIO m
  ) =>
  SwapIntoLnId ->
  ReaderT Psql.SqlBackend m ()
updateSucceededWithoutInvoiceSql sid = do
  ct <- getCurrentTime
  Psql.update $ \row -> do
    Psql.set
      row
      [ SwapIntoLnStatus
          Psql.=. Psql.val SwapSucceeded,
        SwapIntoLnUpdatedAt
          Psql.=. Psql.val ct
      ]
    Psql.where_ $
      ( row Psql.^. SwapIntoLnId
          Psql.==. Psql.val sid
      )
        Psql.&&. ( row Psql.^. SwapIntoLnStatus
                     Psql.==. Psql.val SwapWaitingChan
                 )

updateSucceededSql ::
  ( MonadIO m
  ) =>
  SwapIntoLnId ->
  ReaderT Psql.SqlBackend m ()
updateSucceededSql sid = do
  ct <- getCurrentTime
  Psql.update $ \row -> do
    Psql.set
      row
      [ SwapIntoLnStatus
          Psql.=. Psql.val SwapSucceeded,
        SwapIntoLnUpdatedAt
          Psql.=. Psql.val ct
      ]
    Psql.where_ $
      ( row Psql.^. SwapIntoLnId
          Psql.==. Psql.val sid
      )
        Psql.&&. ( row Psql.^. SwapIntoLnStatus
                     Psql.==. Psql.val SwapWaitingChan
                 )

getSwapsWaitingPeerSql ::
  ( MonadIO m
  ) =>
  ReaderT
    Psql.SqlBackend
    m
    [ ( Entity SwapIntoLn,
        Entity User
      )
    ]
getSwapsWaitingPeerSql =
  Psql.select $
    Psql.from $ \(swap `Psql.InnerJoin` user) -> do
      Psql.locking Psql.ForUpdate
      Psql.on
        ( swap Psql.^. SwapIntoLnUserId
            Psql.==. user Psql.^. UserId
        )
      Psql.where_
        ( swap Psql.^. SwapIntoLnStatus
            Psql.==. Psql.val SwapWaitingPeer
        )
      --
      -- TODO : some sort of exp backoff in case
      -- where user node is offline for a long time.
      -- Maybe limits, some proper retries etc.
      --
      pure (swap, user)

getSwapsWaitingChanSql ::
  ( MonadIO m
  ) =>
  ReaderT
    Psql.SqlBackend
    m
    [ ( Entity SwapIntoLn,
        Entity User
      )
    ]
getSwapsWaitingChanSql =
  Psql.select $
    Psql.from $ \(swap `Psql.InnerJoin` user) -> do
      Psql.locking Psql.ForUpdate
      Psql.on
        ( swap Psql.^. SwapIntoLnUserId
            Psql.==. user Psql.^. UserId
        )
      Psql.where_
        ( swap Psql.^. SwapIntoLnStatus
            Psql.==. Psql.val SwapWaitingChan
        )
      --
      -- TODO : some sort of exp backoff in case
      -- where user node is offline for a long time.
      -- Maybe limits, some proper retries etc.
      --
      pure (swap, user)

getSwapsAboutToExpirySql ::
  ( MonadIO m
  ) =>
  ReaderT Psql.SqlBackend m [Entity SwapIntoLn]
getSwapsAboutToExpirySql = do
  nearExpTime <- getFutureTime Math.swapExpiryLimitInternal
  Psql.select $
    Psql.from $ \row -> do
      Psql.locking Psql.ForUpdate
      Psql.where_
        ( ( row Psql.^. SwapIntoLnStatus
              `Psql.in_` Psql.valList
                --
                -- TODO : somehow handle corner case
                -- where channel is opening or already
                -- opened and waiting for the swap,
                -- but swap invoice has been expired.
                -- Maybe ask user for the new invoice in
                -- this case? Or maybe we can use keysend
                -- feature?
                --
                [ SwapWaitingFundChain,
                  SwapWaitingPeer
                ]
          )
            Psql.&&. ( row Psql.^. SwapIntoLnExpiresAt
                         Psql.<. Psql.val nearExpTime
                     )
        )
      pure row

data UtxoInfo = UtxoInfo
  { utxoInfoUtxo :: Entity SwapUtxo,
    utxoInfoBlock :: Entity Block
  }
  deriving stock
    ( Eq,
      Show
    )

data SwapInfo = SwapInfo
  { swapInfoSwap :: Entity SwapIntoLn,
    swapInfoUser :: Entity User,
    swapInfoUtxo :: [UtxoInfo],
    swapInfoChan :: [Entity LnChan]
  }
  deriving stock
    ( Eq,
      Show
    )

getByUuidSql ::
  ( MonadIO m
  ) =>
  Uuid 'SwapIntoLnTable ->
  ReaderT Psql.SqlBackend m (Maybe SwapInfo)
getByUuidSql uuid =
  (prettifyGetByUuid <$>) $
    Psql.select $
      Psql.from $
        \( mUtxo
             `Psql.InnerJoin` mBlock
             `Psql.RightOuterJoin` swap
             `Psql.LeftOuterJoin` mChan
             `Psql.InnerJoin` user
           ) -> do
            Psql.on
              ( swap Psql.^. SwapIntoLnUserId
                  Psql.==. user Psql.^. UserId
              )
            Psql.on
              ( mChan Psql.?. LnChanSwapIntoLnId
                  Psql.==. Psql.just
                    ( Psql.just $
                        swap Psql.^. SwapIntoLnId
                    )
              )
            Psql.on
              ( mUtxo Psql.?. SwapUtxoSwapIntoLnId
                  Psql.==. Psql.just (swap Psql.^. SwapIntoLnId)
              )
            Psql.on
              ( mUtxo Psql.?. SwapUtxoBlockId
                  Psql.==. mBlock Psql.?. BlockId
              )
            Psql.where_
              ( swap Psql.^. SwapIntoLnUuid
                  Psql.==. Psql.val uuid
              )
            pure (mUtxo, mBlock, mChan, swap, user)

prettifyGetByUuid ::
  [ ( Maybe (Entity SwapUtxo),
      Maybe (Entity Block),
      Maybe (Entity LnChan),
      Entity SwapIntoLn,
      Entity User
    )
  ] ->
  Maybe SwapInfo
prettifyGetByUuid = \case
  [] ->
    Nothing
  xs@((_, _, _, swap, user) : _) ->
    Just
      SwapInfo
        { swapInfoSwap = swap,
          swapInfoUser = user,
          swapInfoUtxo =
            ( \(mUtxo, mBlock, _, _, _) ->
                maybe
                  mempty
                  pure
                  $ UtxoInfo
                    <$> mUtxo
                    <*> mBlock
            )
              =<< xs,
          swapInfoChan =
            nubOrd $
              ( \(_, _, mChan, _, _) ->
                  maybe
                    mempty
                    pure
                    mChan
              )
                =<< xs
        }

getByFundAddressSql ::
  ( MonadIO m
  ) =>
  OnChainAddress 'Fund ->
  ReaderT Psql.SqlBackend m (Maybe (Entity SwapIntoLn))
getByFundAddressSql =
  Util.lockByUnique
    . UniqueSwapIntoLnFundAddress

withLockedRowSql ::
  ( MonadIO m
  ) =>
  SwapIntoLnId ->
  (SwapStatus -> Bool) ->
  (SwapIntoLn -> ReaderT Psql.SqlBackend m a) ->
  ReaderT Psql.SqlBackend m (Either (Entity SwapIntoLn) a)
withLockedRowSql rowId pre action = do
  rowVal <- Util.lockByRow rowId
  if pre $ swapIntoLnStatus rowVal
    then Right <$> action rowVal
    else pure . Left $ Entity rowId rowVal