packages feed

btc-lsp-0.1.0.0: src/BtcLsp/Data/Type.hs

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

module BtcLsp.Data.Type
  ( Nonce,
    newNonce,
    LnInvoice (..),
    LnInvoiceStatus (..),
    LnChanStatus (..),
    Liquidity (..),
    Money (..),
    FeeRate (..),
    UnsafeOnChainAddress (..),
    Seconds (..),
    LogFormat (..),
    YesodLog (..),
    MicroSeconds (..),
    SwapStatus (..),
    swapStatusChain,
    swapStatusLn,
    swapStatusFinal,
    Failure (..),
    FailureInternal (..),
    FailureInput (..),
    tryFailureE,
    tryFailureT,
    tryFromE,
    tryFromT,
    SocketAddress (..),
    BlkHash (..),
    BlkHeight (..),
    BlkStatus (..),
    SwapUtxoStatus (..),
    Privacy (..),
    NodePubKeyHex (..),
    NodeUri (..),
    NodeUriHex (..),
    UtxoLockId (..),
    RHashHex (..),
    Uuid,
    unUuid,
    newUuid,
    Vbyte (..),
    RowQty (..),
    PsbtUtxo (..),
    SwapHash (..),
  )
where

import BtcLsp.Data.Kind
import BtcLsp.Data.Orphan ()
import BtcLsp.Import.External
import qualified BtcLsp.Import.Psql as Psql
import qualified BtcLsp.Text as T
import qualified Data.ByteString.Base16 as B16
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import qualified Data.Time.Clock as Clock
import Data.Time.Clock.POSIX (posixSecondsToUTCTime)
import qualified Data.UUID as UUID
import qualified Data.UUID.V4 as UUID
import qualified LndClient as Lnd
import qualified LndClient.Data.OutPoint as OP
import qualified Network.Bitcoin.BlockChain as Btc
import Text.Julius (ToJavascript)
import qualified Universum
import qualified Witch
import Yesod.Core

newtype Nonce
  = Nonce Word64
  deriving newtype
    ( Eq,
      Ord,
      Show,
      Read,
      Psql.PersistField,
      Psql.PersistFieldSql
    )
  deriving stock
    ( Generic
    )

instance Out Nonce

instance From Nonce Word64

instance From Word64 Nonce

newNonce :: (MonadIO m) => m Nonce
newNonce =
  liftIO $
    Nonce
      . utcTimeToMicros
      <$> Clock.getCurrentTime

utcTimeToMicros :: UTCTime -> Word64
utcTimeToMicros x =
  fromInteger $
    diffTimeToPicoseconds
      ( fromRational
          . toRational
          $ diffUTCTime x epoch
      )
      `div` 1000000

epoch :: UTCTime
epoch =
  posixSecondsToUTCTime 0

data LogFormat
  = Bracket
  | JSON
  deriving stock
    ( Read
    )

data YesodLog
  = YesodLogAll
  | YesodLogNoMain
  | YesodLogNothing
  deriving stock
    ( Eq,
      Ord,
      Show,
      Read,
      Generic
    )

instance FromJSON YesodLog

newtype Seconds
  = Seconds Word64
  deriving newtype
    ( Eq,
      Ord,
      Show,
      Num
    )
  deriving stock
    ( Generic
    )

instance Out Seconds

newtype MicroSeconds
  = MicroSeconds Integer
  deriving newtype
    ( Eq,
      Ord,
      Show,
      Num
    )
  deriving stock
    ( Generic
    )

instance Out MicroSeconds

newtype LnInvoice (mrel :: MoneyRelation)
  = LnInvoice Lnd.PaymentRequest
  deriving newtype
    ( Eq,
      Show,
      Psql.PersistField,
      Psql.PersistFieldSql,
      PathPiece
    )
  deriving stock
    ( Generic
    )

instance Out (LnInvoice mrel)

instance From Lnd.PaymentRequest (LnInvoice mrel)

instance From (LnInvoice mrel) Lnd.PaymentRequest

instance From Text (LnInvoice mrel) where
  from =
    via @Lnd.PaymentRequest

instance From (LnInvoice mrel) Text where
  from =
    via @Lnd.PaymentRequest

newtype SwapHash = SwapHash Text
  deriving newtype
    ( Eq,
      Show,
      Read,
      PathPiece,
      ToJavascript,
      ToJSON
    )
  deriving stock
    ( Generic
    )

instance Out SwapHash

instance ToTypedContent (Maybe SwapHash) where
  toTypedContent = toTypedContent . toJSON

instance ToContent (Maybe SwapHash) where
  toContent = toContent . toJSON

data LnInvoiceStatus
  = LnInvoiceStatusNew
  | LnInvoiceStatusLocked
  | LnInvoiceStatusSettled
  | LnInvoiceStatusCancelled
  | LnInvoiceStatusExpired
  deriving stock
    ( Generic,
      Show,
      Read,
      Eq
    )

instance Out LnInvoiceStatus

data LnChanStatus
  = LnChanStatusPendingOpen
  | LnChanStatusOpened
  | LnChanStatusActive
  | LnChanStatusFullyResolved
  | LnChanStatusInactive
  | LnChanStatusPendingClose
  | LnChanStatusClosed
  deriving stock
    ( Generic,
      Show,
      Read,
      Eq,
      Ord
    )

instance Out LnChanStatus

newtype Liquidity (dir :: Direction) = Liquidity
  { unLiquidity :: MSat
  }
  deriving newtype
    ( Eq,
      Ord,
      Show,
      Read,
      Num
    )
  deriving stock
    ( Generic
    )

instance Out (Liquidity dir)

newtype
  Money
    (owner :: Owner)
    (btcl :: BitcoinLayer)
    (mrel :: MoneyRelation) = Money
  { unMoney :: MSat
  }
  deriving newtype
    ( Eq,
      Ord,
      Show,
      Read,
      Num,
      Psql.PersistField,
      Psql.PersistFieldSql
    )
  deriving stock
    ( Generic
    )

instance Out (Money owner btcl mrel)

instance From MSat (Money owner btcl mrel)

instance From (Money owner btcl mrel) MSat

instance From Word64 (Money owner btcl mrel) where
  from =
    via @MSat

instance From (Money owner btcl mrel) Word64 where
  from =
    via @MSat

instance TryFrom Natural (Money owner btcl mrel) where
  tryFrom =
    from @Word64 `composeTryRhs` tryFrom

instance From (Money owner btcl mrel) Natural where
  from =
    via @Word64

instance TryFrom (Ratio Natural) (Money owner btcl mrel) where
  tryFrom =
    tryFrom @Natural
      `composeTry` tryFrom

instance From (Money owner btcl mrel) (Ratio Natural) where
  from =
    via @Natural

instance TryFrom Rational (Money owner btcl mrel) where
  tryFrom =
    tryFrom @(Ratio Natural)
      `composeTry` tryFrom

instance From (Money owner btcl mrel) Rational where
  from =
    via @(Ratio Natural)

instance ToMessage (Money owner btcl mrel) where
  toMessage =
    T.displayRational 1
      . (/ 1000)
      . from

newtype FeeRate
  = FeeRate (Ratio Word64)
  deriving newtype
    ( Eq,
      Ord,
      Show
    )
  deriving stock
    ( Generic
    )

instance From (Ratio Word64) FeeRate

instance From FeeRate (Ratio Word64)

instance From FeeRate (Ratio Natural) where
  from =
    via @(Ratio Word64)

instance TryFrom Rational FeeRate where
  tryFrom =
    from @(Ratio Word64)
      `composeTryRhs` tryFrom

instance From FeeRate Rational where
  from =
    via @(Ratio Word64)

instance ToMessage FeeRate where
  toMessage =
    (<> "%")
      . T.displayRational 1
      . (* 100)
      . from

newtype UnsafeOnChainAddress (mrel :: MoneyRelation)
  = UnsafeOnChainAddress Text
  deriving newtype
    ( Eq,
      Ord,
      Show,
      Read,
      PathPiece,
      Psql.PersistField,
      Psql.PersistFieldSql
    )
  deriving stock
    ( Generic
    )

instance Out (UnsafeOnChainAddress mrel)

instance From Text (UnsafeOnChainAddress mrel)

instance From (UnsafeOnChainAddress mrel) Text

data SwapStatus
  = -- | Waiting on-chain funding trx with
    -- given amt from user with
    -- some confirmations.
    SwapWaitingFundChain
  | -- | Swap has been funded on-chain,
    -- need to open LN channel now.
    SwapWaitingPeer
  | -- | Waiting channel opening trx
    -- to be mined with some confirmations.
    SwapWaitingChan
  | -- | Final statuses
    SwapSucceeded
  | SwapExpired
  deriving stock
    ( Eq,
      Ord,
      Show,
      Read,
      Generic,
      Enum,
      Bounded
    )

instance Out SwapStatus

swapStatusChain :: [SwapStatus]
swapStatusChain =
  [ SwapWaitingFundChain,
    SwapWaitingPeer
  ]

swapStatusLn :: [SwapStatus]
swapStatusLn =
  [ SwapWaitingChan
  ]

swapStatusFinal :: [SwapStatus]
swapStatusFinal =
  [ SwapSucceeded,
    SwapExpired
  ]

instance PathPiece SwapStatus where
  fromPathPiece :: Text -> Maybe SwapStatus
  fromPathPiece =
    readMaybe
      . unpack
      . T.toTitle
  toPathPiece :: SwapStatus -> Text
  toPathPiece =
    T.toLower
      . Universum.show

data Failure
  = FailureInp FailureInput
  | FailureInt FailureInternal
  deriving stock
    ( Eq,
      Show,
      Generic
    )

instance Out Failure

data FailureInput
  = FailureNonce
  | FailureNonSegwitAddr
  | FailureNonValidAddr
  deriving stock
    ( Eq,
      Show,
      Generic
    )

instance Out FailureInput

data FailureInternal
  = FailureGrpcServer Text
  | FailureGrpcClient Text
  | FailureMath Text
  | FailurePrivate Text
  | FailureRedacted
  deriving stock
    ( Eq,
      Show,
      Generic
    )

instance Out FailureInternal

tryFailureE ::
  forall source target.
  ( Show source,
    Typeable source,
    Typeable target
  ) =>
  Text ->
  Either (TryFromException source target) target ->
  Either Failure target
tryFailureE label =
  first $
    FailureInt
      . FailureMath
      . (label <>)
      . (" " <>)
      . Universum.show

tryFailureT ::
  forall source target m.
  ( Show source,
    Typeable source,
    Typeable target,
    Monad m
  ) =>
  Text ->
  Either (TryFromException source target) target ->
  ExceptT Failure m target
tryFailureT label =
  except . tryFailureE label

tryFromE ::
  forall source target.
  ( Show source,
    Typeable source,
    Typeable target,
    TryFrom source target,
    'False ~ (source == target)
  ) =>
  Text ->
  source ->
  Either Failure target
tryFromE label =
  tryFailureE label . tryFrom

tryFromT ::
  forall source target m.
  ( Show source,
    Typeable source,
    Typeable target,
    TryFrom source target,
    Monad m,
    'False ~ (source == target)
  ) =>
  Text ->
  source ->
  ExceptT Failure m target
tryFromT label =
  except . tryFromE label

data SocketAddress = SocketAddress
  { socketAddressHost :: HostName,
    socketAddressPort :: PortNumber
  }
  deriving stock
    ( Eq,
      Ord,
      Show,
      Generic
    )

instance Out SocketAddress

newtype BlkHash
  = BlkHash Btc.BlockHash
  deriving stock (Eq, Ord, Show, Generic)
  deriving newtype (Psql.PersistField, Psql.PersistFieldSql)

instance Out BlkHash

instance From Btc.BlockHash BlkHash

instance From BlkHash Btc.BlockHash

newtype BlkHeight
  = BlkHeight Word64
  deriving stock
    ( Eq,
      Ord,
      Show,
      Generic
    )
  deriving newtype
    ( Num,
      Psql.PersistField,
      Psql.PersistFieldSql
    )

instance Out BlkHeight

instance ToJSON BlkHeight

instance From Word64 BlkHeight

instance From BlkHeight Word64

instance From BlkHeight Natural where
  from =
    via @Word64

instance TryFrom Btc.BlockHeight BlkHeight where
  tryFrom =
    from @Word64
      `composeTryRhs` tryFrom

instance From BlkHeight Btc.BlockHeight where
  from =
    via @Word64

data BlkStatus
  = BlkConfirmed
  | BlkOrphan
  deriving stock
    ( Eq,
      Ord,
      Show,
      Read,
      Generic
    )

instance Out BlkStatus

data SwapUtxoStatus
  = SwapUtxoUnspent
  | SwapUtxoUnspentDust
  | SwapUtxoUnspentChanReserve
  | SwapUtxoSpentChanSwapped
  | SwapUtxoSpentRefund
  | SwapUtxoOrphan
  deriving stock
    ( Eq,
      Ord,
      Show,
      Read,
      Generic
    )

instance Out SwapUtxoStatus

data Privacy
  = Public
  | Private
  deriving stock
    ( Eq,
      Ord,
      Show,
      Read,
      Enum,
      Bounded,
      Generic
    )

instance Out Privacy

newtype NodePubKeyHex
  = NodePubKeyHex Text
  deriving newtype (Eq, Ord, Show, Read, IsString)
  deriving stock (Generic)

instance Out NodePubKeyHex

instance From NodePubKeyHex Text

instance From Text NodePubKeyHex

instance TryFrom NodePubKey NodePubKeyHex where
  tryFrom src =
    from
      `composeTryRhs` ( first
                          ( TryFromException src
                              . Just
                              . toException
                          )
                          . TE.decodeUtf8'
                          . B16.encode
                          . coerce
                      )
      $ src

newtype UtxoLockId = UtxoLockId ByteString
  deriving newtype (Eq, Ord, Show, Read)
  deriving stock (Generic)

instance Out UtxoLockId

data NodeUri = NodeUri
  { nodeUriPubKey :: NodePubKey,
    nodeUriSocketAddress :: SocketAddress
  }
  deriving stock
    ( Eq,
      Ord,
      Show,
      Generic
    )

instance Out NodeUri

newtype NodeUriHex
  = NodeUriHex Text
  deriving newtype (Eq, Ord, Show, Read, IsString)
  deriving stock (Generic)

instance Out NodeUriHex

instance From NodeUriHex Text

instance From Text NodeUriHex

instance TryFrom NodeUri NodeUriHex where
  tryFrom src =
    bimap
      (withTarget @NodeUriHex . withSource src)
      ( \pubHex ->
          from @Text $
            from pubHex
              <> "@"
              <> from host
              <> ":"
              <> from (showIntegral port)
      )
      $ tryFrom @NodePubKey @NodePubKeyHex $
        nodeUriPubKey src
    where
      sock = nodeUriSocketAddress src
      host = socketAddressHost sock
      port = socketAddressPort sock

newtype RHashHex = RHashHex
  { unRHashHex :: Text
  }
  deriving newtype
    ( Eq,
      Ord,
      Show,
      Read,
      PathPiece
    )
  deriving stock
    ( Generic
    )

instance Out RHashHex

instance From RHashHex Text

instance From Text RHashHex

instance From RHash RHashHex where
  from =
    --
    -- NOTE : decodeUtf8 in general is unsafe
    -- but here we know that it will not fail
    -- because of B16
    --
    RHashHex
      . decodeUtf8
      . B16.encode
      . coerce

instance From RHashHex RHash where
  from =
    --
    -- NOTE : this is not RFC 4648-compliant,
    -- using only for the practical purposes
    --
    RHash
      . B16.decodeLenient
      . encodeUtf8
      . unRHashHex

newtype Uuid (tab :: Table) = Uuid
  { unUuid' :: UUID
  }
  deriving newtype
    ( Eq,
      Ord,
      Show,
      Read
    )
  deriving stock (Generic)

unUuid :: Uuid tab -> UUID
unUuid =
  unUuid'

instance Out (Uuid tab) where
  docPrec x =
    docPrec x
      . UUID.toText
      . unUuid
  doc =
    docPrec 0

newUuid :: (MonadIO m) => m (Uuid tab)
newUuid =
  liftIO $
    Uuid <$> UUID.nextRandom

newtype Vbyte = Vbyte
  { unVbyte :: Ratio Natural
  }
  deriving newtype
    ( Eq,
      Ord,
      Show,
      Num
    )
  deriving stock
    ( Generic
    )

instance Out Vbyte

instance From Vbyte (Ratio Natural)

instance From (Ratio Natural) Vbyte

newtype RowQty = RowQty
  { unRowQty :: Int64
  }
  deriving newtype
    ( Eq,
      Ord,
      Show
    )
  deriving stock
    ( Generic
    )

instance Out RowQty

data PsbtUtxo = PsbtUtxo
  { getOutPoint :: OP.OutPoint,
    getAmt :: MSat,
    getLockId :: Maybe UtxoLockId
  }
  deriving stock (Show, Generic)

instance Out PsbtUtxo

instance From RowQty Int64

instance From Int64 RowQty

instance From Int RowQty where
  from =
    via @Int64

--
-- NOTE :  we're taking advantage of
-- PostgreSQL understanding UUID values
--
instance Psql.PersistField (Uuid tab) where
  toPersistValue =
    Psql.PersistLiteral_ Psql.Escaped
      . UUID.toASCIIBytes
      . unUuid
  fromPersistValue = \case
    Psql.PersistLiteral_ Psql.Escaped x ->
      maybe
        ( Left $
            "Failed to deserialize a UUID, got literal: "
              <> inspectPlain x
        )
        ( Right
            . Uuid
        )
        $ UUID.fromASCIIBytes x
    failure ->
      Left $
        "Failed to deserialize a UUID, got: "
          <> inspectPlain failure

instance Psql.PersistFieldSql (Uuid tab) where
  sqlType =
    const $
      Psql.SqlOther "uuid"

instance ToMessage (Uuid tab) where
  toMessage =
    (<> "...")
      . T.take 5
      . UUID.toText
      . unUuid

instance PathPiece (Uuid tab) where
  fromPathPiece =
    (Uuid <$>)
      . UUID.fromText
  toPathPiece =
    UUID.toText
      . unUuid

Psql.derivePersistField "LnInvoiceStatus"
Psql.derivePersistField "LnChanStatus"
Psql.derivePersistField "SwapStatus"
Psql.derivePersistField "BlkStatus"
Psql.derivePersistField "SwapUtxoStatus"
Psql.derivePersistField "Privacy"
Psql.derivePersistField "UtxoLockId"