bitcoind-rpc-0.2.0.0: src/Servant/Bitcoind.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NumericUnderscores #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
module Servant.Bitcoind (
-- * Bitcoind api endpoint DSL
C,
CX,
F,
I,
O,
-- * Types related to defaulting
EmptyString,
EmptyList,
DefFalse,
DefTrue,
DefZero,
-- * Types related to the client
BitcoindClient,
BitcoindEndpoint,
BitcoindException (..),
-- * Client generation mechanism
HasBitcoindClient (..),
Rewrite (..),
-- * Utility functions
utcTime,
toSatoshis,
decodeFromHex,
HexEncoded (..),
) where
import Control.Exception (Exception)
import Control.Monad ((>=>))
import Control.Monad.Trans.Except (ExceptT (..))
import Control.Monad.Trans.Reader (ReaderT (..))
import Data.Aeson (
FromJSON (..),
ToJSON (..),
Value,
withText,
)
import qualified Data.Aeson.Types as Ae
import Data.Bifunctor (first)
import Data.Proxy (Proxy (..))
import Data.Scientific (Scientific)
import Data.Serialize (Serialize, decode)
import Data.Text (Text)
import Data.Time (UTCTime)
import Data.Time.Clock.POSIX (posixSecondsToUTCTime)
import Data.Word (Word32, Word64)
import GHC.TypeLits (KnownSymbol, Symbol)
import Haskoin.Util (decodeHex)
import Servant.API ((:<|>) (..), (:>))
import Servant.API.BasicAuth (BasicAuth, BasicAuthData)
import Servant.Client (ClientError, ClientM, client)
import Servant.Client.JsonRpc (
JsonRpc,
JsonRpcErr (..),
JsonRpcResponse (..),
)
-- | Exceptions resulting from interacting with bitcoind
data BitcoindException
= -- | The error message returned by bitcoind on failure
RpcException String
| ClientException ClientError
| DecodingError String
deriving (Show)
instance Exception BitcoindException
data BitcoindEndpoint (m :: Symbol) a
-- | A client returning @Either BitcoindException r@
data C r
-- | A client returning @Either BitcoindException ()@
data CX
-- | An argument with a fixed value
data F x r
-- | An optional argument
data O r
-- | An ordinary argument
data I r
class HasDefault x a where
getDefault :: p x -> a
data EmptyString
instance HasDefault EmptyString Text where getDefault _ = ""
data DefFalse
instance HasDefault DefFalse Bool where getDefault _ = False
data DefTrue
instance HasDefault DefTrue Bool where getDefault _ = True
data EmptyList
instance HasDefault EmptyList [a] where getDefault _ = []
data DefZero
instance Num a => HasDefault DefZero a where getDefault _ = 0
class HasBitcoindClient x where
type TheBitcoindClient x :: *
toBitcoindClient :: p x -> TheBitcoindClient x
instance
(Rewrite a, RewriteFrom a ~ NakedClient, KnownSymbol m) =>
HasBitcoindClient (BitcoindEndpoint m a)
where
type TheBitcoindClient (BitcoindEndpoint m a) = RewriteTo a
toBitcoindClient _ =
rewriteRpc (Proxy @a)
. client
$ Proxy @(BitcoindRpc m)
instance
(HasBitcoindClient x, HasBitcoindClient y) =>
HasBitcoindClient (x :<|> y)
where
type TheBitcoindClient (x :<|> y) = TheBitcoindClient x :<|> TheBitcoindClient y
toBitcoindClient _ = toBitcoindClient (Proxy @x) :<|> toBitcoindClient (Proxy @y)
type BitcoindRpc m = BasicAuth "bitcoind" () :> JsonRpc m [Value] String Value
type BitcoindClient r = ReaderT BasicAuthData (ExceptT BitcoindException ClientM) r
type NakedClient =
BasicAuthData ->
[Value] ->
ClientM (JsonRpcResponse String Value)
{- | Bitcoind uses JSON arrays to serialize parameters. This typeclass
describes a generic rewriting system, but we apply it here to transform
clients of the form @BasicAuthData -> [Value] -> ClientM Value@ into curried
functions with endpoint specific arguments.
-}
class Rewrite a where
type RewriteFrom a :: *
type RewriteTo a :: *
rewriteRpc :: p a -> RewriteFrom a -> RewriteTo a
-- | Handle endpoints which do not have an expected return value
instance Rewrite CX where
type RewriteFrom CX = NakedClient
type RewriteTo CX = BitcoindClient ()
rewriteRpc _ f = ReaderT $ ExceptT . fmap repack . (`f` [])
where
repack = \case
Ack _ -> return ()
Errors _ (JsonRpcErr _ e _) -> Left $ RpcException e
Result{} -> Left $ RpcException "Expecting ack; got result"
-- | Endpoints which simply return a value
instance FromJSON r => Rewrite (C r) where
type RewriteFrom (C r) = NakedClient
type RewriteTo (C r) = BitcoindClient r
rewriteRpc _ f = ReaderT $ ExceptT . fmap repack . (`f` [])
where
repack = \case
Result _ x -> first DecodingError $ Ae.parseEither parseJSON x
Errors _ (JsonRpcErr _ e _) -> Left $ RpcException e
Ack{} -> Left $ RpcException "Expecting result; got ack"
-- | Add a normal argument
instance
(RewriteFrom b ~ NakedClient, Rewrite b, ToJSON a) =>
Rewrite (I a -> b)
where
type RewriteFrom (I a -> b) = NakedClient
type RewriteTo (I a -> b) = a -> RewriteTo b
rewriteRpc _ f x = rewriteRpc (Proxy @b) $ \auth args -> f auth (toJSON x : args)
-- | Add an optional argument
instance
(RewriteFrom b ~ NakedClient, Rewrite b, ToJSON a) =>
Rewrite (O a -> b)
where
type RewriteFrom (O a -> b) = NakedClient
type RewriteTo (O a -> b) = Maybe a -> RewriteTo b
rewriteRpc _ f x = rewriteRpc (Proxy @b) $ \auth args -> f auth ((toJSON <$> x) `maybeCons` args)
-- | Add a fixed argument
instance
(RewriteFrom b ~ NakedClient, Rewrite b, ToJSON a, HasDefault x a) =>
Rewrite (F x a -> b)
where
type RewriteFrom (F x a -> b) = NakedClient
type RewriteTo (F x a -> b) = RewriteTo b
rewriteRpc _ f = rewriteRpc (Proxy @b) f'
where
f' auth args = f auth $ fixedVal : args
fixedVal = toJSON @a . getDefault $ Proxy @x
instance (Rewrite a, Rewrite b) => Rewrite (a :<|> b) where
type RewriteFrom (a :<|> b) = RewriteFrom a :<|> RewriteFrom b
type RewriteTo (a :<|> b) = RewriteTo a :<|> RewriteTo b
rewriteRpc _ (x :<|> y) = rewriteRpc (Proxy @a) x :<|> rewriteRpc (Proxy @b) y
maybeCons :: Maybe a -> [a] -> [a]
maybeCons mx xs = maybe xs (: xs) mx
-- | Helper function for decoding POSIX timestamps
utcTime :: Word64 -> UTCTime
utcTime = posixSecondsToUTCTime . fromIntegral
-- | Convert BTC to Satoshis
toSatoshis :: Scientific -> Word32
toSatoshis = floor . (* 100_000_000)
-- | Read a serializable from a hex string
decodeFromHex :: Serialize a => Text -> Either String a
decodeFromHex = maybe (Left "Invalid hex") Right . decodeHex >=> decode
newtype HexEncoded a = HexEncoded {unHexEncoded :: a} deriving (Eq, Show)
instance Serialize a => FromJSON (HexEncoded a) where
parseJSON = withText "HexEncoded" $ either fail (return . HexEncoded) . decodeFromHex