coinbase-pro-0.5.0.0: src/lib/CoinbasePro/Authenticated/Request.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
module CoinbasePro.Authenticated.Request
( CBAuthT (..)
, runCbAuthT
, CoinbaseProCredentials (..)
, CBSecretKey (..)
, AuthGet
, AuthPost
, AuthDelete
, authRequest
) where
import Control.Monad.IO.Class (MonadIO, liftIO)
import Control.Monad.Trans.Class (MonadTrans, lift)
import Control.Monad.Trans.Reader (ReaderT, asks, runReaderT)
import Crypto.Hash.Algorithms (SHA256)
import qualified Crypto.MAC.HMAC as HMAC
import Data.ByteArray.Encoding (Base (Base64),
convertFromBase,
convertToBase)
import Data.ByteString (ByteString)
import qualified Data.ByteString.Char8 as C8
import Data.Time.Clock (getCurrentTime)
import Data.Time.Format (defaultTimeLocale,
formatTime)
import GHC.TypeLits (Symbol)
import Network.HTTP.Types (Method)
import Servant.API ((:>), AuthProtect, Delete,
Get, JSON, Post)
import Servant.Client (ClientM)
import Servant.Client.Core (AuthClientData,
AuthenticatedRequest,
addHeader,
mkAuthenticatedRequest)
import qualified Servant.Client.Core as SCC
import CoinbasePro.Authenticated.Headers (CBAccessKey (..),
CBAccessPassphrase (..),
CBAccessSign (..),
CBAccessTimeStamp (..))
import CoinbasePro.Headers (userAgent)
import CoinbasePro.Request (Body, RequestPath, run)
newtype CBSecretKey = CBSecretKey String
deriving (Eq)
data CoinbaseProCredentials = CoinbaseProCredentials
{ cbAccessKey :: CBAccessKey
, cbSecretKey :: CBSecretKey
, cbAccessPassphrase :: CBAccessPassphrase
} deriving (Eq)
newtype CBAuthT m a = CBAuthT { unCbAuth :: ReaderT CoinbaseProCredentials m a }
deriving (Functor, Applicative, Monad, MonadIO, MonadTrans)
runCbAuthT :: CoinbaseProCredentials -> CBAuthT ClientM a -> IO a
runCbAuthT cpc = run . flip runReaderT cpc . unCbAuth
type instance AuthClientData (AuthProtect "CBAuth") = (CBAccessKey, CBAccessSign, CBAccessTimeStamp, CBAccessPassphrase)
type CBAuthAPI (auth :: Symbol) method a = AuthProtect auth :> method '[JSON] a
type AuthGet a = CBAuthAPI "CBAuth" Get a
type AuthPost a = CBAuthAPI "CBAuth" Post a
type AuthDelete a = CBAuthAPI "CBAuth" Delete a
addAuthHeaders :: (CBAccessKey, CBAccessSign, CBAccessTimeStamp, CBAccessPassphrase) -> SCC.Request -> SCC.Request
addAuthHeaders (key, sig, timestamp, pass) req =
addHeader "CB-ACCESS-KEY" key
$ addHeader "CB-ACCESS-SIGN" sig
$ addHeader "CB-ACCESS-TIMESTAMP" timestamp
$ addHeader "CB-ACCESS-PASSPHRASE" pass
$ addHeader "User-Agent" userAgent req
authRequest :: Method -> RequestPath -> Body
-> (AuthenticatedRequest (AuthProtect "CBAuth") -> ClientM b)
-> CBAuthT ClientM b
authRequest method requestPath body f = do
ak <- CBAuthT $ asks cbAccessKey
sk <- CBAuthT $ asks cbSecretKey
pp <- CBAuthT $ asks cbAccessPassphrase
ts <- liftIO mkCBAccessTimeStamp
let cbs = mkCBAccessSign sk ts method requestPath body
lift . f $ mkAuthenticatedRequest (ak, cbs, ts, pp) addAuthHeaders
mkCBAccessTimeStamp :: IO CBAccessTimeStamp
mkCBAccessTimeStamp = CBAccessTimeStamp . formatTime defaultTimeLocale "%s%Q" <$> getCurrentTime
mkCBAccessSign :: CBSecretKey -> CBAccessTimeStamp -> Method -> RequestPath -> Body -> CBAccessSign
mkCBAccessSign sk ts method requestPath body = CBAccessSign $ convertToBase Base64 hmac
where
dak = decodeApiKey sk
msg = mkMsg ts method requestPath body
hmac = HMAC.hmac dak msg :: HMAC.HMAC SHA256
mkMsg :: CBAccessTimeStamp -> Method -> RequestPath -> Body -> ByteString
mkMsg (CBAccessTimeStamp s) m rp b = C8.pack $ s ++ C8.unpack m ++ rp ++ b
decodeApiKey :: CBSecretKey -> ByteString
decodeApiKey (CBSecretKey s) = either error id . convertFromBase Base64 $ C8.pack s