packages feed

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

{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE RecordWildCards #-}

-- | CLI application harness.
module Console.BnbStaking.Main
    ( run
    , getArgs
    , Args (..)
    ) where

import Control.Monad (filterM, foldM, (<=<))
import Data.Maybe (fromMaybe, mapMaybe)
import Data.Time (localDay, toGregorian, utcToLocalZonedTime, zonedTimeToLocalTime)
import Data.Version (showVersion)
import Network.HTTP.Req (defaultHttpConfig, runReq)
import System.Console.CmdArgs
    ( Data
    , Typeable
    , argPos
    , cmdArgs
    , def
    , explicit
    , help
    , helpArg
    , name
    , program
    , summary
    , typ
    , (&=)
    )

import Console.BnbStaking.Api (Reward (..), getAllRewards)
import Console.BnbStaking.CoinTracking (makeCoinTrackingImport)
import Console.BnbStaking.Csv (makeCsvContents)
import Paths_bnb_staking_csvs (version)

import Data.ByteString.Lazy.Char8 qualified as LBC
import Data.List qualified as L
import Data.Map.Strict qualified as M
import Data.Text qualified as T


-- | Run the executable - parsing args, making queries, & printing the
-- results.
run :: Args -> IO ()
run Args {..} = do
    let aggregator = if argAggregate then aggregateRewards else return
        filterer = maybe return filterByYear argYear
    rewards <-
        filterer <=< aggregator <=< runReq defaultHttpConfig $
            getAllRewards (T.pack argPubKey)
    let outputFile = fromMaybe "-" argOutputFile
    if argCoinTracking
        then makeCoinTrackingImport outputFile argPubKey rewards
        else do
            -- TODO: we calculate local zoned day in filterByYear & in
            -- makeCsvContents. Can we do it just once?
            output <- makeCsvContents rewards
            if outputFile == "-"
                then LBC.putStr output
                else LBC.writeFile outputFile output
  where
    aggregateRewards :: [Reward] -> IO [Reward]
    aggregateRewards =
        fmap (mapMaybe (aggregate . snd) . M.toList)
            . foldM
                ( \acc r -> do
                    rewardTime <- utcToLocalZonedTime $ rRewardTime r
                    return $
                        M.insertWith
                            (<>)
                            (localDay $ zonedTimeToLocalTime rewardTime)
                            [r]
                            acc
                )
                M.empty
    aggregate :: [Reward] -> Maybe Reward
    aggregate = \case
        [] -> Nothing
        rs ->
            Just $
                Reward
                    { rValidatorName =
                        "AGGREGATE-"
                            <> T.intercalate
                                "-"
                                (L.sort $ map rValidatorName rs)
                    , rValidatorAddress = "AGGREGATE-" <> T.pack (show $ length rs)
                    , rDelegator = T.intercalate "-" $ L.sort $ map rDelegator rs
                    , rChainId = rChainId $ head rs
                    , rHeight = rHeight $ head rs
                    , rReward = sum $ map rReward rs
                    , rRewardTime = minimum $ map rRewardTime rs
                    }
    filterByYear :: Integer -> [Reward] -> IO [Reward]
    filterByYear year = filterM $ \reward -> do
        rewardTime <- utcToLocalZonedTime $ rRewardTime reward
        return
            . (\(y, _, _) -> y == year)
            . toGregorian
            . localDay
            $ zonedTimeToLocalTime rewardTime


-- | CLI arguments supported by the executable.
data Args = Args
    { argPubKey :: String
    -- ^ BinanceChain account's pubkey.
    , argOutputFile :: Maybe String
    -- ^ Optional output file. 'Nothing' or @'Just' "-"@ will print to
    -- 'System.IO.stdout'.
    , argCoinTracking :: Bool
    -- ^ Flag to enable writing/printing files formatted for CoinTracking
    -- Bulk Imports.
    , argYear :: Maybe Integer
    -- ^ Year for limiting the output.
    , argAggregate :: Bool
    -- ^ Aggregate rewards into day buckets.
    }
    deriving (Show, Read, Eq, Data, Typeable)


-- | Parse the CLI arguments with 'System.Console.CmdArgs'.
getArgs :: IO Args
getArgs = cmdArgs argSpec


argSpec :: Args
argSpec =
    Args
        { argPubKey = def &= argPos 0 &= typ "ACCOUNT_PUBKEY"
        , argOutputFile =
            Nothing
                &= help "File to write the export to. Default: stdout"
                &= explicit
                &= name "output-file"
                &= name "o"
                &= typ "FILE"
        , argCoinTracking =
            False
                &= help "Generate a CoinTracking Import file."
                &= explicit
                &= name "cointracking"
        , argYear =
            Nothing
                &= help "Limit to given year"
                &= explicit
                &= name "y"
                &= name "year"
                &= typ "YYYY"
        , argAggregate =
            False
                &= help "Output one aggregate row per day."
                &= explicit
                &= name "aggregate"
        }
        &= summary
            ( "bnb-staking-csvs v"
                <> showVersion version
                <> ", Pavan Rikhi 2021-2024"
            )
        &= program "bnb-staking-csvs"
        &= helpArg [name "h"]
        &= help "Generate CSV Exports of you BinanceChain Staking Rewards."