module Coinbase.Exchange.Rest
( coinbaseGet
, coinbasePost
, coinbaseDelete
, voidBody
) where
import Control.Monad.Except
import Control.Monad.Reader
import Control.Monad.Trans.Resource
import Crypto.Hash
import Data.Aeson
import Data.Byteable
import qualified Data.ByteString as BS
import qualified Data.ByteString.Base64 as Base64
import qualified Data.ByteString.Char8 as CBS
import qualified Data.ByteString.Lazy as LBS
import Data.Conduit
import Data.Conduit.Attoparsec (sinkParser)
import qualified Data.Conduit.Binary as CB
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import Data.Time
import Data.Time.Clock.POSIX
import Network.HTTP.Conduit
import Network.HTTP.Types
import Text.Printf
import Coinbase.Exchange.Types
type Signed = Bool
voidBody :: Maybe ()
voidBody = Nothing
coinbaseGet :: ( ToJSON a
, FromJSON b
, MonadResource m
, MonadReader ExchangeConf m
, MonadError ExchangeFailure m )
=> Signed -> Path -> Maybe a -> m b
coinbaseGet sgn p ma = coinbaseRequest "GET" sgn p ma >>= processResponse
coinbasePost :: ( ToJSON a
, FromJSON b
, MonadResource m
, MonadReader ExchangeConf m
, MonadError ExchangeFailure m )
=> Signed -> Path -> Maybe a -> m b
coinbasePost sgn p ma = coinbaseRequest "POST" sgn p ma >>= processResponse
coinbaseDelete :: ( ToJSON a
, MonadResource m
, MonadReader ExchangeConf m
, MonadError ExchangeFailure m )
=> Signed -> Path -> Maybe a -> m ()
coinbaseDelete sgn p ma = coinbaseRequest "DELETE" sgn p ma >>= processEmpty
coinbaseRequest :: ( ToJSON a
, MonadResource m
, MonadReader ExchangeConf m
, MonadError ExchangeFailure m )
=> Method -> Signed -> Path -> Maybe a -> m (Response (ResumableSource m BS.ByteString))
coinbaseRequest meth sgn p ma = do
conf <- ask
req <- case apiType conf of
Sandbox -> parseUrl $ sandboxRest ++ p
Live -> parseUrl $ liveRest ++ p
let req' = req { method = meth
, requestHeaders = [ ("user-agent", "haskell")
, ("accept", "application/json")
]
}
flip http (manager conf) =<< signMessage sgn meth p
=<< encodeBody ma req'
encodeBody :: (ToJSON a, Monad m)
=> Maybe a -> Request -> m Request
encodeBody (Just a) req = return req
{ requestHeaders = requestHeaders req ++
[ ("content-type", "application/json") ]
, requestBody = RequestBodyBS $ LBS.toStrict $ encode a
}
encodeBody Nothing req = return req
signMessage :: (MonadIO m, MonadReader ExchangeConf m, MonadError ExchangeFailure m)
=> Signed -> Method -> Path -> Request -> m Request
signMessage True meth p req = do
conf <- ask
case authToken conf of
Just tok -> do time <- liftM (realToFrac . utcTimeToPOSIXSeconds) (liftIO getCurrentTime)
>>= \t -> return . CBS.pack $ printf "%.0f" (t::Double)
rBody <- pullBody $ requestBody req
let presign = CBS.concat [time, meth, CBS.pack p, rBody]
sign = toBytes (hmac (secret tok) presign :: HMAC SHA256)
return req
{ requestBody = RequestBodyBS rBody
, requestHeaders = requestHeaders req ++
[ ("cb-access-key", key tok)
, ("cb-access-sign", Base64.encode sign)
, ("cb-access-timestamp", time)
, ("cb-access-passphrase", passphrase tok)
]
}
Nothing -> throwError $ AuthenticationRequiredFailure $ T.pack p
where pullBody (RequestBodyBS b) = return b
pullBody (RequestBodyLBS b) = return $ LBS.toStrict b
pullBody _ = throwError AuthenticationRequiresByteStrings
signMessage False _ _ req = return req
processResponse :: ( FromJSON b
, MonadResource m
, MonadReader ExchangeConf m
, MonadError ExchangeFailure m )
=> Response (ResumableSource m BS.ByteString) -> m b
processResponse res =
case responseStatus res of
s | s == status200 -> do body <- responseBody res $$+- sinkParser (fmap fromJSON json)
case body of
Success b -> return b
Error er -> throwError $ ParseFailure $ T.pack er
| otherwise -> do body <- responseBody res $$+- CB.sinkLbs
throwError $ ApiFailure $ T.decodeUtf8 $ LBS.toStrict body
processEmpty :: ( MonadResource m
, MonadReader ExchangeConf m
, MonadError ExchangeFailure m )
=> Response (ResumableSource m BS.ByteString) -> m ()
processEmpty res =
case responseStatus res of
s | s == status200 -> return ()
| otherwise -> do body <- responseBody res $$+- CB.sinkLbs
throwError $ ApiFailure $ T.decodeUtf8 $ LBS.toStrict body