{-# LANGUAGE OverloadedStrings #-} {-# OPTIONS_GHC -Wall #-} -- | The API exposed in this module should be considered unstable, and is -- subject to change between minor revisions. -- -- If the version number is a.b.c.d, and either a or b changes, then the -- module's whole API may have changed (if only b changes, then it was -- probably a minor change). -- -- If c changed, then only the internal API may change. The rest of the -- module is guaranteed to be stable. -- -- If only d changes, then there were no user-facing code changes made. module Network.Bitcoin.Internal ( module Network.Bitcoin.Types , Text, Vector , FromJSON(..) , callApi , getClient , Nil(..) , tj , tjm , tja , AddrAddress(..) , BitcoinRpcResponse(..) ) where import Control.Applicative import Control.Exception import Control.Monad import Data.Aeson import Data.Maybe import Data.Vector ( Vector ) import qualified Data.Vector as V import Network.Bitcoin.Types import qualified Data.ByteString as BS import Data.Text ( Text ) import Network.HTTP.Client import Network.HTTP.Types.Header -- | RPC calls return an error object. It can either be empty; or have an -- error message + error code. data BitcoinRpcError = NoError -- ^ All good. | BitcoinRpcError Int Text -- ^ Error code + error message. deriving ( Show, Read, Ord, Eq ) instance FromJSON BitcoinRpcError where parseJSON (Object v) = BitcoinRpcError <$> v .: "code" <*> v .: "message" parseJSON Null = return NoError parseJSON _ = mzero -- | A response from bitcoind will contain the result of the JSON-RPC call, and -- an error. The error should be null if a valid response was received. data BitcoinRpcResponse a = BitcoinRpcResponse { btcResult :: a , btcError :: BitcoinRpcError } deriving ( Show, Read, Ord, Eq ) instance FromJSON a => FromJSON (BitcoinRpcResponse a) where parseJSON (Object v) = BitcoinRpcResponse <$> v .: "result" <*> v .: "error" parseJSON _ = mzero -- | 'getClient' takes a url, rpc username, and rpc password -- and returns a Client that can be used to make API calls. Each -- Client encloses a Manager (from http-client) that re-uses -- connections for requests, so long as the same Client is -- is used for each call. getClient :: String -> BS.ByteString -> BS.ByteString -> IO Client getClient url user pass = do url' <- parseUrl url mgr <- newManager defaultManagerSettings let baseReq = applyBasicAuth user pass url' { method = "POST" , requestHeaders = [(hContentType, "application/json")] } return $ \r -> do resp <- httpLbs (baseReq { requestBody = RequestBodyLBS r }) mgr return $ responseBody resp -- | 'callApi' is a low-level interface for making authenticated API -- calls to a Bitcoin daemon. The first argument specifies -- rpc client details (URL, username, password) -- -- The second argument is the command name. The third argument provides -- parameters for the API call. -- -- > genHash = do -- client <- getClient "http://127.0.0.1:8332" "user" "password" -- callApi client "getblockhash" [tj 0] -- -- On error, throws a 'BitcoinException'. callApi :: FromJSON v => Client -- ^ RPC client for bitcoind -> Text -- ^ command name -> [Value] -- ^ command arguments -> IO v callApi client cmd params = readVal =<< client jsonRpcReqBody where readVal bs = case decode' bs of Just r@(BitcoinRpcResponse {btcError=NoError}) -> return $ btcResult r Just (BitcoinRpcResponse {btcError=BitcoinRpcError code msg}) -> throw $ BitcoinApiError code msg Nothing -> throw $ BitcoinResultTypeError bs jsonRpcReqBody = encode $ object [ "jsonrpc" .= ("2.0" :: Text) , "method" .= cmd , "params" .= params , "id" .= (1 :: Int) ] {-# INLINE callApi #-} -- | Used to allow "null" to decode to a tuple. data Nil = Nil { unNil :: () } instance FromJSON Nil where parseJSON Null = return $ Nil () parseJSON x = fail $ "\"null\" was expected, but " ++ show x ++ " was recieved." -- | A handy shortcut for toJSON, because I'm lazy. tj :: ToJSON a => a -> Value tj = toJSON {-# INLINE tj #-} tjm :: ToJSON a => a -> Maybe a -> Value tjm d m = tj $ fromMaybe d m {-# INLINE tjm #-} tja :: ToJSON a => Maybe a -> [Value] tja m = fromMaybe [] $ pure . tj <$> m {-# INLINE tja #-} -- | A wrapper for a vector of address:amount pairs. The RPC expects that as -- an object of "address":"amount" pairs, instead of a vector. So that's what -- we give them with AddrAddress's ToJSON. newtype AddrAddress = AA (Vector (Address, BTC)) instance ToJSON AddrAddress where toJSON (AA vec) = object . V.toList $ uncurry (.=) <$> vec