packages feed

lnd-client-0.1.0.1: src/LndClient/LndTest.hs

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TemplateHaskell #-}

module LndClient.LndTest
  ( -- * Btc
    BtcUrl (..),
    BtcLogin (..),
    BtcPassword (..),
    BtcEnv (..),
    newBtcClient,

    -- * TestEnv
    TestEnv (..),
    withTestEnv,
    withChannelWatcher,
    withInvoiceWatcher,
    withSingleInvoiceWatcher,

    -- * Class
    LndTest (..),

    -- * TestUtils
    mine,
    mine1,
    syncWallets,
    syncPendingChannels,
    syncPendingChannelsFor,
    sendTestPayment,
    receiveClosedChannels,
    receiveActiveChannel,
    receiveInvoice,

    -- * LowLevel setup
    lazyMineInitialCoins,
    lazyConnectNodes,
    watchDefaults,
    cancelAllInvoices,
    closeAllChannels,

    -- * HighLevel setip
    setupZeroChannels,
    setupOneChannel,

    -- * Misc
    liftLndResult,
    liftMaybe,
    purgeChan,
    ignore2,
    ignore3,
  )
where

import LndClient.Data.AddInvoice as AddInvoice
  ( AddInvoiceRequest (..),
    AddInvoiceResponse (..),
  )
import LndClient.Data.Channel as Channel (Channel (..))
import LndClient.Data.ChannelPoint as ChannelPoint (ChannelPoint (..))
import LndClient.Data.CloseChannel
  ( ChannelCloseSummary (..),
    CloseChannelRequest (..),
  )
import LndClient.Data.ClosedChannels as ClosedChannels
import LndClient.Data.GetInfo (GetInfoResponse (..))
import qualified LndClient.Data.GetInfo as Lnd (GetInfoResponse (..))
import LndClient.Data.Invoice as Invoice (Invoice (..), InvoiceState (..))
import LndClient.Data.ListChannels as LC (ListChannelsRequest (..))
import qualified LndClient.Data.ListInvoices as ListInvoices
import qualified LndClient.Data.NewAddress as Lnd
  ( AddressType (..),
    NewAddressRequest (..),
    NewAddressResponse (..),
  )
import LndClient.Data.OpenChannel as OpenChannel
  ( OpenChannelRequest (..),
  )
import LndClient.Data.Peer
  ( ConnectPeerRequest (..),
    LightningAddress (..),
  )
import LndClient.Data.PendingChannels (PendingChannelsResponse (..))
import LndClient.Data.SendPayment as SendPayment
  ( SendPaymentRequest (..),
  )
import LndClient.Data.SubscribeChannelEvents
import LndClient.Data.SubscribeInvoices
  ( SubscribeInvoicesRequest (..),
  )
import LndClient.Import
import qualified LndClient.RPC.Katip as Lnd
import LndClient.Watcher as Watcher
import qualified Network.Bitcoin as Btc (Client, getClient)
import qualified Network.Bitcoin.BlockChain as Btc (getBlockCount)
import qualified Network.Bitcoin.Mining as Btc (generateToAddress)

newtype BtcUrl = BtcUrl String

newtype BtcLogin = BtcLogin ByteString

newtype BtcPassword = BtcPassword ByteString

data BtcEnv = BtcEnv
  { btcUrl :: BtcUrl,
    btcLogin :: BtcLogin,
    btcPassword :: BtcPassword
  }

data TestEnv = TestEnv
  { testLndEnv :: LndEnv,
    testNodeLocation :: NodeLocation,
    testChannelWatcher :: Watcher () ChannelEventUpdate,
    testInvoiceWatcher :: Watcher SubscribeInvoicesRequest Invoice,
    testSingleInvoiceWatcher :: Watcher RHash Invoice
  }

uniquePairs :: (Ord a, Enum a, Bounded a) => [(a, a)]
uniquePairs = [(x0, x1) | x0 <- enumerate, x1 <- enumerate, x0 < x1]

newBtcClient :: MonadIO m => BtcEnv -> m Btc.Client
newBtcClient x =
  liftIO $
    Btc.getClient
      (coerce $ btcUrl x)
      (coerce $ btcLogin x)
      (coerce $ btcPassword x)

withTestEnv ::
  ( MonadUnliftIO m
  ) =>
  LndEnv ->
  NodeLocation ->
  (TestEnv -> KatipContextT m a) ->
  KatipContextT m a
withTestEnv lnd loc action =
  withChannelWatcher lnd $ \cw ->
    withInvoiceWatcher lnd $ \iw ->
      withSingleInvoiceWatcher lnd id $ \siw ->
        action
          TestEnv
            { testLndEnv = lnd,
              testNodeLocation = loc,
              testChannelWatcher = cw,
              testInvoiceWatcher = iw,
              testSingleInvoiceWatcher = siw
            }

class
  ( KatipContext m,
    MonadUnliftIO m,
    Ord owner,
    Enum owner,
    Bounded owner,
    Out owner
  ) =>
  LndTest m owner
  where
  getBtcClient :: owner -> m Btc.Client
  getTestEnv :: owner -> m TestEnv
  getLndEnv :: owner -> m LndEnv
  getLndEnv = (testLndEnv <$>) . getTestEnv
  getSev :: owner -> Severity -> m Severity
  getSev owner sev = do
    env <- testLndEnv <$> getTestEnv owner
    pure $ newSev env sev
  getNodeLocation :: owner -> m NodeLocation
  getNodeLocation = (testNodeLocation <$>) . getTestEnv
  getChannelTChan :: owner -> m (TChan ((), ChannelEventUpdate))
  getChannelTChan =
    (Watcher.dupLndTChan . testChannelWatcher) <=< getTestEnv
  getInvoiceTChan :: owner -> m (TChan (SubscribeInvoicesRequest, Invoice))
  getInvoiceTChan =
    (Watcher.dupLndTChan . testInvoiceWatcher) <=< getTestEnv
  getSingleInvoiceTChan :: owner -> m (TChan (RHash, Invoice))
  getSingleInvoiceTChan =
    (Watcher.dupLndTChan . testSingleInvoiceWatcher) <=< getTestEnv

  --
  -- TODO : embed getSingleInvoiceTChan here
  -- because it's really not used separately
  --
  watchSingleInvoice :: owner -> RHash -> m ()
  watchSingleInvoice owner rh = do
    env <- getTestEnv owner
    Watcher.watch (testSingleInvoiceWatcher env) rh

walletAddress :: LndTest m owner => owner -> m Text
walletAddress owner = do
  lnd <- getLndEnv owner
  Lnd.NewAddressResponse x <-
    liftLndResult
      =<< Lnd.newAddress
        lnd
        (Lnd.NewAddressRequest Lnd.WITNESS_PUBKEY_HASH Nothing)
  pure x

lazyMineInitialCoins :: forall m owner. LndTest m owner => Proxy owner -> m ()
lazyMineInitialCoins = const $ do
  mapM_ (liftLndResult <=< Lnd.lazyInitWallet <=< getLndEnv) owners
  bc <- getBtcClient (minBound :: owner)
  h <- liftIO $ Btc.getBlockCount bc
  -- Reward coins are spendable only after 100 blocks,
  -- mine additional 100 blocks per owner to ensure everybody
  -- has coins to spend.
  when (h < fromIntegral (length owners * blocksPerOwner)) $ do
    mapM_ (mine blocksPerOwner) owners
  where
    owners :: [owner]
    owners = enumerate
    blocksPerOwner :: Int
    blocksPerOwner = 200

lazyConnectNodes :: forall m owner. LndTest m owner => Proxy owner -> m ()
lazyConnectNodes = const $ mapM_ this uniquePairs
  where
    this :: (owner, owner) -> m ()
    this (owner0, owner1) = do
      testEnvOwner0 <- getTestEnv owner0
      Lnd.GetInfoResponse pubKeyOwner0 _ _ <-
        liftLndResult =<< Lnd.getInfo (testLndEnv testEnvOwner0)
      let req =
            ConnectPeerRequest
              { addr =
                  LightningAddress
                    { pubkey = pubKeyOwner0,
                      host = testNodeLocation testEnvOwner0
                    },
                perm = False
              }
      lndEnvOwner1 <- getLndEnv owner1
      liftLndResult =<< Lnd.lazyConnectPeer lndEnvOwner1 req

watchDefaults :: forall m owner. LndTest m owner => Proxy owner -> m ()
watchDefaults = const $ mapM_ this (enumerate :: [owner])
  where
    this owner = do
      testEnv <- getTestEnv owner
      Watcher.watchUnit $ testChannelWatcher testEnv
      Watcher.watch (testInvoiceWatcher testEnv) iReq
    iReq =
      --
      -- TODO : this is related to LND bug
      -- https://github.com/lightningnetwork/lnd/issues/2469
      --
      SubscribeInvoicesRequest (Just $ AddIndex 1) Nothing

mine :: forall m owner. LndTest m owner => Int -> owner -> m ()
mine blocks owner = do
  btcAddr <- walletAddress owner
  bc <- getBtcClient owner
  sev <- getSev owner DebugS
  $(logTM) sev $
    logStr $
      "Mining "
        <> inspect blocks
        <> " blocks to "
        <> inspect owner
        <> " wallet"
  void . liftIO $
    Btc.generateToAddress
      bc
      blocks
      btcAddr
      Nothing
  liftLndResult =<< syncWallets (Proxy :: Proxy owner)

mine1 :: forall m owner. LndTest m owner => Proxy owner -> m ()
mine1 = const $ mine 1 (minBound :: owner)

liftLndResult :: MonadIO m => Either LndError a -> m a
liftLndResult (Right x) =
  pure x
liftLndResult (Left x) =
  liftIO . fail $ "LiftLndResult failed " <> inspectStr x

syncWallets ::
  forall m owner.
  LndTest m owner =>
  Proxy owner ->
  m (Either LndError ())
syncWallets = const $ this 0
  where
    this 30 = do
      let msg :: Text = "SyncWallets attempt limit exceeded"
      sev <- getSev (minBound :: owner) ErrorS
      $(logTM) sev $ logStr msg
      pure . Left $ LndError msg
    this (attempt :: Int) = do
      sev <- getSev (minBound :: owner) DebugS
      $(logTM) sev "SyncWallets is running"
      rs <- mapM (Lnd.getInfo <=< getLndEnv) (enumerate :: [owner])
      if all isInSync rs
        then pure $ Right ()
        else do
          sleep $ MicroSecondsDelay 1000000
          this $ attempt + 1
    isInSync = \case
      Left {} -> False
      Right x -> Lnd.syncedToChain x

syncPendingChannels :: forall m owner. LndTest m owner => Proxy owner -> m ()
syncPendingChannels =
  const $ mapM_ (liftLndResult <=< syncPendingChannelsFor) (enumerate :: [owner])

syncPendingChannelsFor ::
  forall m owner.
  LndTest m owner =>
  owner ->
  m (Either LndError ())
syncPendingChannelsFor owner = this 0
  where
    this 30 = do
      let msg =
            "SyncPendingChannelsFor "
              <> inspect owner
              <> " attempt limit exceeded"
      sev <- getSev owner ErrorS
      $(logTM) sev $ logStr msg
      pure . Left $ LndError msg
    this (attempt :: Int) = do
      sev <- getSev owner DebugS
      $(logTM) sev $
        logStr $
          "SyncPendingChannelsFor "
            <> inspect owner
            <> " is running"
      res <- Lnd.pendingChannels =<< getLndEnv owner
      case res of
        Left {} -> this (attempt + 1)
        Right (PendingChannelsResponse _ x0 x1 x2 x3) ->
          if null x0
            && null x1
            && null x2
            && null x3
            then pure $ Right ()
            else mine1 (Proxy :: Proxy owner) >> this (attempt + 1)

receiveClosedChannels ::
  forall m owner.
  LndTest m owner =>
  Proxy owner ->
  [ChannelPoint] ->
  m (Either LndError ())
receiveClosedChannels po = this 0
  where
    this _ [] =
      pure $ Right ()
    this 30 _ =
      pure $
        Left $
          LndError "receiveClosedChannels - exceeded"
    this (attempt :: Integer) cps = do
      let owners = enumerate :: [owner]
      xs <- rights <$> mapM getOwnersCloseCPs owners
      let filteredCps = filter (checkTwiceCP $ concat xs) cps
      if null filteredCps
        then pure $ Right ()
        else do
          mine1 po
          sleep $ MicroSecondsDelay 1000000
          this (attempt + 1) filteredCps
    checkTwiceCP :: [ChannelPoint] -> ChannelPoint -> Bool
    checkTwiceCP cps cp = length (filter (cp ==) cps) < 2
    getOwnersCloseCPs :: owner -> m (Either LndError [ChannelPoint])
    getOwnersCloseCPs o = do
      lnd <- getLndEnv o
      ccs <- liftLndResult =<< Lnd.closedChannels lnd ClosedChannels.defReq
      pure $ Right $ chPoint <$> ccs

cancelAllInvoices ::
  forall m owner.
  LndTest m owner =>
  Proxy owner ->
  m ()
cancelAllInvoices =
  const $ mapM_ (this 0) (enumerate :: [owner])
  where
    listReq =
      ListInvoices.ListInvoiceRequest
        { ListInvoices.pendingOnly = True,
          ListInvoices.indexOffset = AddIndex 0,
          ListInvoices.numMaxInvoices = 0,
          ListInvoices.reversed = False
        }
    this :: Int -> owner -> m ()
    this 30 owner =
      error $
        "CancelAllInvoices attempt limit exceeded for " <> inspect owner
    this attempt owner = do
      lnd <- getLndEnv owner
      let getInvoices :: m [Invoice] =
            ListInvoices.invoices
              <$> (liftLndResult =<< Lnd.listInvoices lnd listReq)
      is0 <- getInvoices
      res <- mapM (Lnd.cancelInvoice lnd) (Invoice.rHash <$> is0)
      is1 <- getInvoices
      if all isRight res && null is1
        then pure ()
        else do
          sleep $ MicroSecondsDelay 1000000
          this (attempt + 1) owner

closeAllChannels :: forall m owner. LndTest m owner => Proxy owner -> m ()
closeAllChannels po = do
  cancelAllInvoices po
  mapM_ (this 0) uniquePairs
  where
    this :: Int -> (owner, owner) -> m ()
    this 30 owners =
      error $
        "CloseAllChannels - limit exceeded for " <> inspect owners
    this attempt (owner0, owner1) = do
      sev <- getSev owner0 DebugS
      $(logTM) sev "CloseAllChannels - closing channels"
      lnd0 <- getLndEnv owner0
      peerLocation <- getNodeLocation owner1
      GetInfoResponse peerPubKey _ _ <-
        liftLndResult =<< Lnd.getInfo =<< getLndEnv owner1
      let getChannels :: m [Channel] =
            liftLndResult
              =<< Lnd.listChannels
                lnd0
                (ListChannelsRequest False False False False (Just peerPubKey))
      cs0 <- getChannels
      let cps = Channel.channelPoint <$> cs0
      res <-
        mapM
          ( \cp ->
              Lnd.closeChannelSync
                lnd0
                ( Just $
                    ConnectPeerRequest (LightningAddress peerPubKey peerLocation) False
                )
                (CloseChannelRequest cp False Nothing Nothing Nothing)
          )
          cps
      cs1 <- getChannels
      if all isRight res && null cs1
        then liftLndResult =<< receiveClosedChannels po cps
        else do
          sleep $ MicroSecondsDelay 1000000
          this (attempt + 1) (owner0, owner1)

receiveActiveChannel ::
  LndTest m owner =>
  Proxy owner ->
  ChannelPoint ->
  TChan ((), ChannelEventUpdate) ->
  m (Either LndError ())
receiveActiveChannel po = this 0
  where
    this (attempt :: Integer) cp cq =
      if attempt > 30
        then pure $ Left $ LndError "receiveActiveChannel - exceeded"
        else do
          x <- readTChanTimeout (MicroSecondsDelay 1000000) cq
          case channelEvent . snd <$> x of
            Just (ChannelEventUpdateChannelActiveChannel gcp) ->
              if cp == gcp
                then pure $ Right ()
                else mine1 po >> this (attempt + 1) cp cq
            _ ->
              mine1 po >> this (attempt + 1) cp cq

setupZeroChannels :: LndTest m owner => Proxy owner -> m ()
setupZeroChannels x = do
  lazyMineInitialCoins x
  lazyConnectNodes x
  watchDefaults x
  closeAllChannels x
  syncPendingChannels x

setupOneChannel ::
  forall m owner.
  LndTest m owner =>
  owner ->
  owner ->
  m ChannelPoint
setupOneChannel ownerFrom ownerTo = do
  lndFrom <- getLndEnv ownerFrom
  lndTo <- getLndEnv ownerTo
  mq <- getChannelTChan ownerTo
  cq <- getChannelTChan ownerFrom
  --
  -- Open channel from Customer to Merchant
  --
  sev <- getSev ownerFrom DebugS
  $(logTM) sev "SetupOneChannel - opening channel"
  GetInfoResponse merchantPubKey _ _ <-
    liftLndResult =<< Lnd.getInfo lndTo
  let openChannelRequest =
        OpenChannel.OpenChannelRequest
          { OpenChannel.nodePubkey = merchantPubKey,
            OpenChannel.localFundingAmount = MSat 200000000,
            OpenChannel.pushMSat = Just $ MSat 10000000,
            OpenChannel.targetConf = Nothing,
            OpenChannel.mSatPerByte = Nothing,
            OpenChannel.private = Nothing,
            OpenChannel.minHtlcMsat = Nothing,
            OpenChannel.remoteCsvDelay = Nothing,
            OpenChannel.minConfs = Nothing,
            OpenChannel.spendUnconfirmed = Nothing,
            OpenChannel.closeAddress = Nothing,
            OpenChannel.fundingShim = Nothing
          }
  cp <-
    liftLndResult
      =<< Lnd.openChannelSync lndFrom openChannelRequest
  let po = Proxy :: Proxy owner
  liftLndResult =<< receiveActiveChannel po cp mq
  liftLndResult =<< receiveActiveChannel po cp cq
  --
  -- TODO : these invoices are added and settled to
  -- raise invoice index to 1 to be able to receive
  -- notifications about all next invoices
  -- remove when LND bug will be fixed
  -- https://github.com/lightningnetwork/lnd/issues/2469
  --
  () <- sendTestPayment (MSat 1000000) ownerFrom ownerTo
  () <- sendTestPayment (MSat 1000000) ownerTo ownerFrom
  $(logTM) sev "SetupOneChannel - finished"
  pure cp

sendTestPayment :: LndTest m owner => MSat -> owner -> owner -> m ()
sendTestPayment amt0 sender0 recepient0 = do
  sender <- getLndEnv sender0
  recepient <- getLndEnv recepient0
  let addInvoiceRequest =
        AddInvoice.AddInvoiceRequest
          { AddInvoice.memo = Just "HELLO",
            AddInvoice.valueMsat = amt0,
            AddInvoice.expiry = Just $ Seconds 1000
          }
  invoice <-
    liftLndResult =<< Lnd.addInvoice recepient addInvoiceRequest
  let payReq =
        SendPayment.SendPaymentRequest
          { SendPayment.paymentRequest =
              AddInvoice.paymentRequest invoice,
            SendPayment.amt =
              amt0,
            SendPayment.outgoingChanId =
              Nothing
          }
  void . liftLndResult =<< Lnd.sendPayment sender payReq

receiveInvoice ::
  ( MonadUnliftIO m,
    KatipContext m
  ) =>
  LndEnv ->
  RHash ->
  Invoice.InvoiceState ->
  TChan (a, Invoice) ->
  m (Either LndError ())
receiveInvoice env rh s q = do
  mx0 <- readTChanTimeout (MicroSecondsDelay 30000000) q
  let mx = snd <$> mx0
  katipAddLndPublic env LndTestReceiveInvoice mx $
    $(logTM) DebugS rpcSucceeded
  case (\x -> Invoice.rHash x == rh && Invoice.state x == s) <$> mx of
    Just True -> return $ Right ()
    Just False -> receiveInvoice env rh s q
    Nothing -> return . Left $ TChanTimeout "receiveInvoice"

liftMaybe :: MonadIO m => String -> Maybe a -> m a
liftMaybe msg mx =
  case mx of
    Just x -> pure x
    Nothing -> liftIO $ fail msg

purgeChan :: MonadUnliftIO m => TChan a -> m ()
purgeChan chan = do
  x <- readTChanTimeout (MicroSecondsDelay 500000) chan
  when (isJust x) $ purgeChan chan

ignore2 :: Monad m => a -> b -> m ()
ignore2 _ _ = pure ()

ignore3 :: Monad m => a -> b -> c -> m ()
ignore3 _ _ _ = pure ()

withChannelWatcher ::
  (KatipContext m, MonadUnliftIO m) =>
  LndEnv ->
  (Watcher () ChannelEventUpdate -> m a) ->
  m a
withChannelWatcher lnd =
  Watcher.withWatcherUnit
    lnd
    Lnd.subscribeChannelEventsChan
    ignore2

withInvoiceWatcher ::
  (KatipContext m, MonadUnliftIO m) =>
  LndEnv ->
  (Watcher SubscribeInvoicesRequest Invoice -> m a) ->
  m a
withInvoiceWatcher lnd =
  Watcher.withWatcher
    lnd
    Lnd.subscribeInvoicesChan
    ignore3

withSingleInvoiceWatcher ::
  (Ord req, KatipContext m, MonadUnliftIO m) =>
  LndEnv ->
  (req -> RHash) ->
  (Watcher req Invoice -> m a) ->
  m a
withSingleInvoiceWatcher lnd accessor =
  Watcher.withWatcher
    lnd
    (Lnd.subscribeSingleInvoiceChan accessor)
    ignore3