{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE FlexibleInstances #-}
-- | Communicate with a Bitcoin daemon over JSON RPC
module Network.Bitcoin
    (
    -- * Types
      Auth(..)
    , Address
    , mkAddress
    , Amount
    , Account
    , MinConf
    , AddressValidation
    , isValid
    , isMine
    , account
    , BitcoinException(..)

    -- * Individual API methods
    , getBalance
    , getBlockCount
    , getConnectionCount
    , getDifficulty
    , getGenerate
    , getHashesPerSec
    , getReceivedByAccount
    , getReceivedByAddress
    , validateAddress
    , isValidAddress

    -- * Low-level API
    , callApi
    ) where
import Network.Bitcoin.Address

import Control.Applicative
import Control.Exception
import Control.Monad
import Data.Aeson
import Data.Attoparsec
import Data.Attoparsec.Number
import Data.Fixed
import Data.Maybe (fromJust)
import Data.String (fromString)
import Data.Typeable
import Network.Browser
import Network.HTTP hiding (password)
import Network.URI (parseURI)
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as BL
import qualified Data.Map as M
import qualified Data.Text as T

-- Define Bitcoin's internal precision
data Satoshi = Satoshi
instance HasResolution Satoshi where
    resolution _ = 10^(8::Integer)

-- | Fixed precision Bitcoin amount (to avoid floating point errors)
type Amount = Fixed Satoshi

-- | Name of a Bitcoin wallet account
type Account = String

-- | Minimum number of confirmations for a payment
type MinConf     = Integer

-- | 'Auth' describes authentication credentials for
-- making API requests to the Bitcoin daemon
data Auth = Auth
    { rpcUrl :: String      -- ^ URL, with port, where bitcoind listens
    , rpcUser :: String     -- ^ same as bitcoind's 'rpcuser' config
    , rpcPassword :: String -- ^ same as bitcoind's 'rpcpassword' config
    }
    deriving (Show)

data BitcoinRpcResponse = BitcoinRpcResponse {
        btcResult  :: Value,
        btcError   :: Value
    }
    deriving (Show)
instance FromJSON BitcoinRpcResponse where
    parseJSON (Object v) = BitcoinRpcResponse <$> v .: "result"
                                          <*> v .: "error"
    parseJSON _ = mzero

-- |A 'BitcoinException' is thrown when 'callApi' encounters an
-- error.  The API error code is represented as an @Int@, the message as
-- a @String@.
data BitcoinException
    = BitcoinApiError Int String
    deriving (Show,Typeable)
instance Exception BitcoinException

-- encodes an RPC request into a ByteString containing JSON
jsonRpcReqBody :: String -> [Value] -> BL.ByteString
jsonRpcReqBody cmd params = encode $ object [
                "jsonrpc" .= ("2.0"::String),
                "method"  .= cmd,
                "params"  .= params,
                "id"      .= (1::Int)
              ]

-- |'callApi' is a low-level interface for making authenticated API
-- calls to a Bitcoin daemon.  The first argument specifies
-- authentication details (URL, username, password) and is often
-- curried for convenience:
--
-- > callBtc = callApi $ Auth "http://127.0.0.1:8332" "user" "password"
--
-- The second argument is the command name.  The third argument provides
-- parameters for the API call.
--
-- > let result = callBtc "getbalance" ["account-name", Number 6]
--
-- On error, throws a 'BitcoinException'
callApi :: Auth  -- ^ authentication credentials for bitcoind
               -> String  -- ^ command name
               -> [Value] -- ^ command arguments
               -> IO Value
callApi auth command params = do
    (_,httpRes) <- browse $ do
        setOutHandler $ const $ return ()
        addAuthority authority
        setAllowBasicAuth True
        request $ httpRequest urlString $ jsonRpcReqBody command params
    let res = fromSuccess $ fromJSON $ toVal $ rspBody httpRes
    case res of
        BitcoinRpcResponse {btcError=Null} -> return $ btcResult res
        BitcoinRpcResponse {btcError=e}    -> throw $ buildBtcError e
    where authority     = httpAuthority auth
          urlString     = rpcUrl auth
          toStrict      = B.concat . BL.toChunks
          justParseJSON = fromJust . maybeResult . parse json
          toVal         = justParseJSON . toStrict

-- Internal helper functions to make callApi more readable
httpAuthority :: Auth -> Authority
httpAuthority (Auth urlString username password) =
    AuthBasic {
        auRealm    = "jsonrpc",
        auUsername = username,
        auPassword = password,
        auSite     = uri
    }
    where uri = fromJust $ parseURI urlString
httpRequest :: String -> BL.ByteString -> Request BL.ByteString
httpRequest urlString jsonBody =
    (postRequest urlString){
        rqBody = jsonBody,
        rqHeaders = [
            mkHeader HdrContentType "application/json",
            mkHeader HdrContentLength (show $ BL.length jsonBody)
        ]
    }

fromSuccess :: Data.Aeson.Result t -> t
fromSuccess (Success a) = a
fromSuccess (Error   s) = error s

buildBtcError :: Value -> BitcoinException
buildBtcError (Object o) = BitcoinApiError code msg
    where find k = fromSuccess . fromJSON . fromJust . M.lookup k
          code = find "code" o
          msg  = find "message" o
buildBtcError _ = error "Need an object to buildBtcError"

-- Convert JSON numeric values to more specific numeric types
class FromNumber a where
    fromNumber :: Number -> a
instance FromNumber Amount where
    fromNumber (I i) = fromInteger i
    fromNumber (D d) = fromRational $ toRational d
instance FromNumber Integer where
    fromNumber (I i) = i
    fromNumber (D d) = round d
instance FromNumber Double where
    fromNumber (I i) = fromInteger i
    fromNumber (D d) = d

-- Class of types that can be converted to a JSON representation
class ToValue a where
    toValue :: a -> Value
instance ToValue Address where
    toValue addr = String $ fromString $ show addr
instance ToValue MinConf where
    toValue conf = Number $ fromInteger conf
instance ToValue Account where
    toValue acct = String $ fromString acct

callNumber :: FromNumber a => String -> [Value] -> Auth -> IO a
callNumber cmd args auth = do
    (Number n) <- callApi auth cmd args
    return $ fromNumber n

callBool :: String -> [Value] -> Auth -> IO Bool
callBool cmd args auth = do
    (Bool b) <- callApi auth cmd args
    return b

-- | Returns the balance of a specific Bitcoin account
getBalance :: Auth
           -> Account
           -> MinConf
           -> IO Amount
getBalance auth acct minconf = callNumber "getbalance" args auth
  where
    args = [ String $ fromString acct, Number $ fromInteger minconf ]

-- | Returns the number of blocks in the longest block chain
getBlockCount :: Auth -> IO Integer
getBlockCount = callNumber "getblockcount" []

-- | Returns the number of connections to other nodes
getConnectionCount :: Auth -> IO Integer
getConnectionCount = callNumber "getconnectioncount" []

-- | Returns the proof-of-work difficulty as a multiple of the minimum
-- difficulty
getDifficulty :: Auth -> IO Double
getDifficulty = callNumber "getdifficulty" []

-- | Indicates whether the node is generating or not
getGenerate :: Auth -> IO Bool
getGenerate = callBool "getgenerate" []

-- | Returns a recent hashes per second performance measurement while
-- generating
getHashesPerSec :: Auth -> IO Integer
getHashesPerSec = callNumber "gethashespersec" []

-- | Returns the total amount received by addresses with
-- @account@ in transactions with at least @minconf@ confirmations
getReceivedByAccount :: Auth
                     -> Account
                     -> MinConf
                     -> IO Amount
getReceivedByAccount auth acct conf =
    callNumber "getreceivedbyaccount" [toValue acct,toValue conf] auth

-- | Returns the total amount received by an address in transactions
-- with at least 'minconf' confirmations.
getReceivedByAddress :: Auth
                     -> Address
                     -> MinConf
                     -> IO Amount
getReceivedByAddress auth addr conf =
    callNumber "getreceivedbyaddress" [toValue addr,toValue conf] auth

-- | Encapsulates address validation results from 'validateAddress'
data AddressValidation = AddressValidation
    { isValid :: Bool    -- ^ Is the address valid?
    , isMine  :: Bool    -- ^ Does the address belong to my wallet?
    , account :: Account -- ^ To which account does this address belong?
    } deriving (Show)

-- | Return information about an address.
-- If the address is invalid or doesn't belong to us, the account name
-- is the empty string.
validateAddress :: Auth
                -> Address
                -> IO AddressValidation
validateAddress auth addr = do
    (Object result) <- callApi auth "validateaddress" [toValue addr]
    return AddressValidation
        { isValid = bool False "isvalid" result
        , isMine  = bool False "ismine"  result
        , account = str  ""    "account" result
        }
  where
    bool d k r = maybe d (\(Bool b)->b) $ M.lookup k r
    str  d k r = maybe d (\(String t)->T.unpack t) $ M.lookup k r

-- | Returns true if the RPC says the address is valid.
-- Use this function until 'mkAddress' verifies address checksums
isValidAddress :: Auth -> Address -> IO Bool
isValidAddress auth addr = validateAddress auth addr >>= return . isValid