packages feed

bnb-staking-csvs-0.2.2.0: src/Console/BnbStaking/Api.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE RecordWildCards #-}

-- | Binance.org API requests & responses.
module Console.BnbStaking.Api
    ( -- * Rewards
      getAllRewards
    , Reward (..)

      -- * Low-Level Requests & Responses
    , Endpoint (..)
    , makeRequest
    , RewardResponse (..)
    ) where

import Control.Monad (forM)
import Data.Aeson
    ( FromJSON (..)
    , withObject
    , (.:)
    )
import Data.List (sortOn)
import Data.Maybe (fromMaybe)
import Data.Scientific (Scientific)
import Data.Time
    ( UTCTime
    , defaultTimeLocale
    , parseTimeM
    )
import GHC.Generics (Generic)
import Network.HTTP.Req
    ( GET (..)
    , MonadHttp
    , NoReqBody (..)
    , Scheme (Https)
    , Url
    , https
    , jsonResponse
    , req
    , responseBody
    , (/:)
    , (=:)
    )

import Data.Text qualified as T


-- | Fetch all rewards for the given Delegator PubKey.
getAllRewards :: (MonadHttp m) => T.Text -> m [Reward]
getAllRewards pubKey = do
    let pageSize = 50
        jPageSize = Just pageSize
    initialResp <- makeRequest $ GetRewards pubKey jPageSize Nothing
    let rewardCount = rrTotal initialResp
    remainingRewards <-
        if rewardCount < pageSize
            then return []
            else fmap concat
                . forM [pageSize, pageSize * 2 .. rewardCount]
                $ \(Just -> offset) ->
                    rrRewards <$> makeRequest (GetRewards pubKey jPageSize offset)
    return . sortResults $ rrRewards initialResp <> remainingRewards
  where
    sortResults :: [Reward] -> [Reward]
    sortResults = sortOn rRewardTime


-- | Represents all endpoints of the binance.org api, as well as their
-- respective response data.
data Endpoint a where
    GetRewards :: T.Text -> Maybe Integer -> Maybe Integer -> Endpoint RewardResponse


-- | Make a request to an endpoint.
makeRequest :: (MonadHttp m) => Endpoint a -> m a
makeRequest e = case e of
    GetRewards _ mbLimit mbOffset ->
        responseBody
            <$> req
                GET
                url
                NoReqBody
                jsonResponse
                ( ("limit" =: fromMaybe 20 mbLimit) <> ("offset" =: fromMaybe 0 mbOffset)
                )
  where
    url :: Url 'Https
    url = case e of
        GetRewards pubKey _ _ ->
            baseUrl
                /: "staking"
                /: "chains"
                /: "bsc"
                /: "delegators"
                /: pubKey
                /: "rewards"
    baseUrl :: Url 'Https
    baseUrl = https "api.binance.org" /: "v1"


-- | Response of requesting a delegator's rewards.
data RewardResponse = RewardResponse
    { rrTotal :: Integer
    -- ^ Total number of rewards.
    , rrRewards :: [Reward]
    -- ^ Rewards in this page.
    }
    deriving (Show, Read, Eq, Generic)


instance FromJSON RewardResponse where
    parseJSON = withObject "RewardResponse" $ \o -> do
        rrTotal <- o .: "total"
        rrRewards <- o .: "rewardDetails"
        return $ RewardResponse {..}


-- | A single staking reward.
data Reward = Reward
    { rValidatorName :: T.Text
    , rValidatorAddress :: T.Text
    , rDelegator :: T.Text
    , rChainId :: T.Text
    -- ^ Always @bsc@ at the moment - no testnet rewards supported.
    , rHeight :: Integer
    , rReward :: Scientific
    , rRewardTime :: UTCTime
    }
    deriving (Show, Read, Eq, Generic)


instance FromJSON Reward where
    parseJSON = withObject "Reward" $ \o -> do
        rValidatorName <- o .: "valName"
        rValidatorAddress <- o .: "validator"
        rDelegator <- o .: "delegator"
        rChainId <- o .: "chainId"
        rHeight <- o .: "height"
        rReward <- o .: "reward"
        rRewardTime <-
            o .: "rewardTime"
                >>= parseTimeM
                    True
                    defaultTimeLocale
                    "%FT%T%Q%Ez"
        return $ Reward {..}