packages feed

bnb-staking-csvs 0.1.0.0 → 0.2.0.0

raw patch · 9 files changed

+423/−236 lines, 9 filesdep +cmdargsdep +cointracking-importsdep ~aesonPVP ok

version bump matches the API change (PVP)

Dependencies added: cmdargs, cointracking-imports

Dependency ranges changed: aeson

API changes (from Hackage documentation)

- Lib: ExportData :: MyZonedTime -> Text -> Text -> Text -> Text -> Text -> Integer -> ExportData
- Lib: MyZonedTime :: ZonedTime -> MyZonedTime
- Lib: Reward :: Text -> Text -> Text -> Text -> Integer -> Scientific -> UTCTime -> Reward
- Lib: RewardResponse :: Integer -> [Reward] -> RewardResponse
- Lib: [GetRewards] :: Text -> Maybe Integer -> Maybe Integer -> Endpoint RewardResponse
- Lib: [amount] :: ExportData -> Text
- Lib: [currency] :: ExportData -> Text
- Lib: [delegator] :: ExportData -> Text
- Lib: [fromMyZonedTime] :: MyZonedTime -> ZonedTime
- Lib: [height] :: ExportData -> Integer
- Lib: [rChainId] :: Reward -> Text
- Lib: [rDelegator] :: Reward -> Text
- Lib: [rHeight] :: Reward -> Integer
- Lib: [rRewardTime] :: Reward -> UTCTime
- Lib: [rReward] :: Reward -> Scientific
- Lib: [rValidatorAddress] :: Reward -> Text
- Lib: [rValidatorName] :: Reward -> Text
- Lib: [rrRewards] :: RewardResponse -> [Reward]
- Lib: [rrTotal] :: RewardResponse -> Integer
- Lib: [time] :: ExportData -> MyZonedTime
- Lib: [validatorAddress] :: ExportData -> Text
- Lib: [validator] :: ExportData -> Text
- Lib: convertReward :: Reward -> IO ExportData
- Lib: data Endpoint a
- Lib: data ExportData
- Lib: data Reward
- Lib: data RewardResponse
- Lib: getAllRewards :: MonadHttp m => Text -> m [Reward]
- Lib: instance Data.Aeson.Types.FromJSON.FromJSON Lib.Reward
- Lib: instance Data.Aeson.Types.FromJSON.FromJSON Lib.RewardResponse
- Lib: instance Data.Csv.Conversion.DefaultOrdered Lib.ExportData
- Lib: instance Data.Csv.Conversion.ToField Lib.MyZonedTime
- Lib: instance Data.Csv.Conversion.ToNamedRecord Lib.ExportData
- Lib: instance GHC.Classes.Eq Lib.Reward
- Lib: instance GHC.Classes.Eq Lib.RewardResponse
- Lib: instance GHC.Generics.Generic Lib.ExportData
- Lib: instance GHC.Generics.Generic Lib.Reward
- Lib: instance GHC.Generics.Generic Lib.RewardResponse
- Lib: instance GHC.Read.Read Lib.ExportData
- Lib: instance GHC.Read.Read Lib.MyZonedTime
- Lib: instance GHC.Read.Read Lib.Reward
- Lib: instance GHC.Read.Read Lib.RewardResponse
- Lib: instance GHC.Show.Show Lib.ExportData
- Lib: instance GHC.Show.Show Lib.MyZonedTime
- Lib: instance GHC.Show.Show Lib.Reward
- Lib: instance GHC.Show.Show Lib.RewardResponse
- Lib: makeRequest :: MonadHttp m => Endpoint a -> m a
- Lib: newtype MyZonedTime
- Lib: run :: IO ()
+ Console.BnbStaking.Api: Reward :: Text -> Text -> Text -> Text -> Integer -> Scientific -> UTCTime -> Reward
+ Console.BnbStaking.Api: RewardResponse :: Integer -> [Reward] -> RewardResponse
+ Console.BnbStaking.Api: [GetRewards] :: Text -> Maybe Integer -> Maybe Integer -> Endpoint RewardResponse
+ Console.BnbStaking.Api: [rChainId] :: Reward -> Text
+ Console.BnbStaking.Api: [rDelegator] :: Reward -> Text
+ Console.BnbStaking.Api: [rHeight] :: Reward -> Integer
+ Console.BnbStaking.Api: [rRewardTime] :: Reward -> UTCTime
+ Console.BnbStaking.Api: [rReward] :: Reward -> Scientific
+ Console.BnbStaking.Api: [rValidatorAddress] :: Reward -> Text
+ Console.BnbStaking.Api: [rValidatorName] :: Reward -> Text
+ Console.BnbStaking.Api: [rrRewards] :: RewardResponse -> [Reward]
+ Console.BnbStaking.Api: [rrTotal] :: RewardResponse -> Integer
+ Console.BnbStaking.Api: data Endpoint a
+ Console.BnbStaking.Api: data Reward
+ Console.BnbStaking.Api: data RewardResponse
+ Console.BnbStaking.Api: getAllRewards :: MonadHttp m => Text -> m [Reward]
+ Console.BnbStaking.Api: instance Data.Aeson.Types.FromJSON.FromJSON Console.BnbStaking.Api.Reward
+ Console.BnbStaking.Api: instance Data.Aeson.Types.FromJSON.FromJSON Console.BnbStaking.Api.RewardResponse
+ Console.BnbStaking.Api: instance GHC.Classes.Eq Console.BnbStaking.Api.Reward
+ Console.BnbStaking.Api: instance GHC.Classes.Eq Console.BnbStaking.Api.RewardResponse
+ Console.BnbStaking.Api: instance GHC.Generics.Generic Console.BnbStaking.Api.Reward
+ Console.BnbStaking.Api: instance GHC.Generics.Generic Console.BnbStaking.Api.RewardResponse
+ Console.BnbStaking.Api: instance GHC.Read.Read Console.BnbStaking.Api.Reward
+ Console.BnbStaking.Api: instance GHC.Read.Read Console.BnbStaking.Api.RewardResponse
+ Console.BnbStaking.Api: instance GHC.Show.Show Console.BnbStaking.Api.Reward
+ Console.BnbStaking.Api: instance GHC.Show.Show Console.BnbStaking.Api.RewardResponse
+ Console.BnbStaking.Api: makeRequest :: MonadHttp m => Endpoint a -> m a
+ Console.BnbStaking.CoinTracking: bnb :: Currency
+ Console.BnbStaking.CoinTracking: makeCoinTrackingImport :: FilePath -> String -> [Reward] -> IO ()
+ Console.BnbStaking.CoinTracking: makeImportData :: String -> [Reward] -> IO [CTImportData]
+ Console.BnbStaking.CoinTracking: writeOrPrintImportData :: FilePath -> [CTImportData] -> IO ()
+ Console.BnbStaking.Csv: ExportData :: MyZonedTime -> Text -> Text -> Text -> Text -> Text -> Integer -> ExportData
+ Console.BnbStaking.Csv: MyZonedTime :: ZonedTime -> MyZonedTime
+ Console.BnbStaking.Csv: [amount] :: ExportData -> Text
+ Console.BnbStaking.Csv: [currency] :: ExportData -> Text
+ Console.BnbStaking.Csv: [delegator] :: ExportData -> Text
+ Console.BnbStaking.Csv: [fromMyZonedTime] :: MyZonedTime -> ZonedTime
+ Console.BnbStaking.Csv: [height] :: ExportData -> Integer
+ Console.BnbStaking.Csv: [time] :: ExportData -> MyZonedTime
+ Console.BnbStaking.Csv: [validatorAddress] :: ExportData -> Text
+ Console.BnbStaking.Csv: [validator] :: ExportData -> Text
+ Console.BnbStaking.Csv: convertReward :: Reward -> IO ExportData
+ Console.BnbStaking.Csv: data ExportData
+ Console.BnbStaking.Csv: instance Data.Csv.Conversion.DefaultOrdered Console.BnbStaking.Csv.ExportData
+ Console.BnbStaking.Csv: instance Data.Csv.Conversion.ToField Console.BnbStaking.Csv.MyZonedTime
+ Console.BnbStaking.Csv: instance Data.Csv.Conversion.ToNamedRecord Console.BnbStaking.Csv.ExportData
+ Console.BnbStaking.Csv: instance GHC.Generics.Generic Console.BnbStaking.Csv.ExportData
+ Console.BnbStaking.Csv: instance GHC.Read.Read Console.BnbStaking.Csv.ExportData
+ Console.BnbStaking.Csv: instance GHC.Read.Read Console.BnbStaking.Csv.MyZonedTime
+ Console.BnbStaking.Csv: instance GHC.Show.Show Console.BnbStaking.Csv.ExportData
+ Console.BnbStaking.Csv: instance GHC.Show.Show Console.BnbStaking.Csv.MyZonedTime
+ Console.BnbStaking.Csv: makeCsvContents :: [Reward] -> IO ByteString
+ Console.BnbStaking.Csv: newtype MyZonedTime
+ Console.BnbStaking.Main: Args :: String -> Maybe String -> Bool -> Args
+ Console.BnbStaking.Main: [argCoinTracking] :: Args -> Bool
+ Console.BnbStaking.Main: [argOutputFile] :: Args -> Maybe String
+ Console.BnbStaking.Main: [argPubKey] :: Args -> String
+ Console.BnbStaking.Main: data Args
+ Console.BnbStaking.Main: getArgs :: IO Args
+ Console.BnbStaking.Main: instance Data.Data.Data Console.BnbStaking.Main.Args
+ Console.BnbStaking.Main: instance GHC.Classes.Eq Console.BnbStaking.Main.Args
+ Console.BnbStaking.Main: instance GHC.Read.Read Console.BnbStaking.Main.Args
+ Console.BnbStaking.Main: instance GHC.Show.Show Console.BnbStaking.Main.Args
+ Console.BnbStaking.Main: run :: Args -> IO ()

Files

CHANGELOG.md view
@@ -1,5 +1,18 @@ # CHANGELOG +## master+++## v0.2.0.0++* Add a `--cointracking` flag to format the data for use with CoinTracking's+  Bulk Import feature.+* Add an `--output-file` CLI argument to make the executable write to a file+  instead of print to stdout.+* Use the `cmdargs` package to parse CLI arguments.+* Split library into `Console.BnbStaking` module hierarchy.++ ## v0.1.0.0  * Initial release
README.md view
@@ -9,6 +9,7 @@  ```sh stack run -- <DELEGATOR_PUBKEY>+stack run -- --help ```  [get-stack]: https://docs.haskellstack.org/en/stable/README/
app/Main.hs view
@@ -1,6 +1,8 @@ module Main where -import           Lib+import           Console.BnbStaking.Main        ( getArgs+                                                , run+                                                )  main :: IO ()-main = run+main = getArgs >>= run
bnb-staking-csvs.cabal view
@@ -5,15 +5,21 @@ -- see: https://github.com/sol/hpack  name:           bnb-staking-csvs-version:        0.1.0.0-synopsis:       Generate CSV Exports of Your BNB Staking Rewards-description:    @bnb-staking-rewards@ is a CLI program that queries the Binance.org API for+version:        0.2.0.0+synopsis:       Generate CSV Exports of Your BNB Staking Rewards.+description:    @bnb-staking-csvs@ is a CLI program that queries the Binance.org API for                 all of a delegator's rewards and exports the resulting data to a CSV file.                 .-                You can install @bnb-staking-rewards@ with Stack: @stack install --resolver-                nightly bnb-stakingrewards@. Then run @bnb-staking-rewards-                <DELEGATOR_PUBKEY>@ to print out your rewards in CSV format.-category:       Web+                You can install @bnb-staking-csvs@ with Stack: @stack install --resolver+                nightly bnb-staking-csvs@. Then run the following to print out your+                rewards in CSV format:+                .+                @+                  bnb-staking-csvs \<DELEGATOR_PUBKEY>+                @+                .+                See @bnb-staking-csvs --help@ for additional options.+category:       Web, Finance, Console homepage:       https://github.com/prikhi/bnb-staking-csvs#readme bug-reports:    https://github.com/prikhi/bnb-staking-csvs/issues author:         Pavan Rikhi@@ -32,7 +38,10 @@  library   exposed-modules:-      Lib+      Console.BnbStaking.Api+      Console.BnbStaking.CoinTracking+      Console.BnbStaking.Csv+      Console.BnbStaking.Main   other-modules:       Paths_bnb_staking_csvs   hs-source-dirs:@@ -52,6 +61,8 @@     , base >=4.7 && <5     , bytestring <1     , cassava <1+    , cmdargs >=0.10 && <1+    , cointracking-imports <1     , req ==3.*     , scientific <1     , text ==1.*
+ src/Console/BnbStaking/Api.hs view
@@ -0,0 +1,136 @@+{-# 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 qualified Data.Text                     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 { .. }
+ src/Console/BnbStaking/CoinTracking.hs view
@@ -0,0 +1,74 @@+{-# LANGUAGE RecordWildCards #-}+{- | Generate & write/print CoinTracking Bulk Import files.++-}+module Console.BnbStaking.CoinTracking+    ( makeCoinTrackingImport+    , writeOrPrintImportData+    , makeImportData+    , bnb+    ) where++import           Control.Monad                  ( (>=>) )+import           Data.Time                      ( utcToLocalZonedTime )+import           Web.CoinTracking.Imports       ( Amount(..)+                                                , CTImportData(..)+                                                , CTTransactionType(Staking)+                                                , Currency(..)+                                                , coinTrackingCsvImport+                                                , writeImportDataToFile+                                                )++import           Console.BnbStaking.Api         ( Reward(..) )++import qualified Data.ByteString.Lazy.Char8    as LBC+import qualified Data.Text                     as T+++-- | Generate the Bulk Import file for CoinTracking & write to destination+-- or print to stdout if destinatin is @"-"@.+makeCoinTrackingImport+    :: FilePath+    -- ^ Destination. @xls@ or @xlsx@ extensions generate the Excel import,+    -- any other extension will generate the CSV import.+    -> String+    -- ^ Account's PubKey+    -> [Reward]+    -> IO ()+makeCoinTrackingImport dest pubkey =+    makeImportData pubkey >=> writeOrPrintImportData dest++-- | Write or print the generated import data.+writeOrPrintImportData :: FilePath -> [CTImportData] -> IO ()+writeOrPrintImportData dest importData = if dest == "-"+    then LBC.putStrLn $ coinTrackingCsvImport importData+    else writeImportDataToFile dest importData+++-- | Turn an account pubkey & reward into a 'CTImportData', localizing the+-- reward time.+makeImportData :: String -> [Reward] -> IO [CTImportData]+makeImportData pubkey = mapM $ \Reward {..} -> do+    zonedTime <- utcToLocalZonedTime rRewardTime+    return CTImportData+        { ctidType      = Staking+        , ctidBuy       = Just $ Amount rReward bnb+        , ctidSell      = Nothing+        , ctidFee       = Nothing+        , ctidExchange  = "BNB Wallet"+        , ctidGroup     = "Staking"+        , ctidComment   = "Imported From bnb-staking-csvs"+        , ctidDate      = zonedTime+        , ctidTradeId   = "BNB-STAKE-"+                          <> T.pack pubkey+                          <> "-"+                          <> rValidatorAddress+                          <> "-"+                          <> T.pack (show rHeight)+        , ctidBuyValue  = Nothing+        , ctidSellValue = Nothing+        }++-- | Binance Coin currency with the @BNB@ ticker & 8 decimals of precision.+bnb :: Currency+bnb = Currency 8 "BNB"
+ src/Console/BnbStaking/Csv.hs view
@@ -0,0 +1,81 @@+{-# LANGUAGE RecordWildCards #-}+{- | CSV serialization of BNB Staking Rewards.+-}+module Console.BnbStaking.Csv+    ( makeCsvContents+    , ExportData(..)+    , convertReward+    , MyZonedTime(..)+    ) where++import           Data.Csv                       ( DefaultOrdered+                                                , ToField(..)+                                                , ToNamedRecord+                                                , encodeDefaultOrderedByName+                                                )+import           Data.Scientific                ( FPFormat(..)+                                                , formatScientific+                                                )+import           Data.Time                      ( ZonedTime(..)+                                                , defaultTimeLocale+                                                , formatTime+                                                , utcToLocalZonedTime+                                                )+import           GHC.Generics                   ( Generic )++import           Console.BnbStaking.Api         ( Reward(..) )++import qualified Data.ByteString.Lazy          as LBS+import qualified Data.Text                     as T+++-- | Build the CSV contents for the given rewards, including the header+-- row.+makeCsvContents :: [Reward] -> IO LBS.ByteString+makeCsvContents = fmap encodeDefaultOrderedByName . mapM convertReward++-- | Datatype representing a single row in the CSV export.+data ExportData = ExportData+    { time             :: MyZonedTime+    -- ^ The time of the reward.+    , amount           :: T.Text+    -- ^ The reward amount.+    , currency         :: T.Text+    -- ^ Always @BNB@, but sometimes a useful column for CSV imports.+    , delegator        :: T.Text+    -- ^ The address that was rewarded.+    , validator        :: T.Text+    -- ^ The validator's name.+    , validatorAddress :: T.Text+    -- ^ The address the delegator is validating to.+    , height           :: Integer+    -- ^ The height of the reward's block.+    }+    deriving (Show, Read, Generic)++instance ToNamedRecord ExportData+instance DefaultOrdered ExportData++-- | Render a 'Reward' into our target export data by converting to+-- localtime(respecting DST), & formatting the amount column to 8 decimal+-- places.+convertReward :: Reward -> IO ExportData+convertReward Reward {..} = do+    localRewardTime <- utcToLocalZonedTime rRewardTime+    return $ ExportData+        { time             = MyZonedTime localRewardTime+        , amount           = T.pack $ formatScientific Fixed (Just 8) rReward+        , currency         = "BNB"+        , delegator        = rDelegator+        , validator        = rValidatorName+        , validatorAddress = rValidatorAddress+        , height           = rHeight+        }++-- | Wrapper type to support custom 'ToField' instance.+newtype MyZonedTime = MyZonedTime { fromMyZonedTime :: ZonedTime } deriving (Show, Read)++-- | Render with @%FT%T%Q%Ez@ formatting string.+instance ToField MyZonedTime where+    toField (MyZonedTime zt) =+        toField $ formatTime defaultTimeLocale "%FT%T%Q%Ez" zt
+ src/Console/BnbStaking/Main.hs view
@@ -0,0 +1,95 @@+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE RecordWildCards #-}+{- | CLI application harness.++-}+module Console.BnbStaking.Main+    ( run+    , getArgs+    , Args(..)+    ) where+import           Data.Maybe                     ( fromMaybe )+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         ( getAllRewards )+import           Console.BnbStaking.CoinTracking+                                                ( makeCoinTrackingImport )+import           Console.BnbStaking.Csv         ( makeCsvContents )+import           Paths_bnb_staking_csvs         ( version )++import qualified Data.ByteString.Lazy.Char8    as LBC+import qualified Data.Text                     as T++-- | Run the executable - parsing args, making queries, & printing the+-- results.+run :: Args -> IO ()+run Args {..} = do+    rewards <- runReq defaultHttpConfig $ getAllRewards $ T.pack argPubKey+    let outputFile = fromMaybe "-" argOutputFile+    if argCoinTracking+        then makeCoinTrackingImport outputFile argPubKey rewards+        else do+            output <- makeCsvContents rewards+            if outputFile == "-"+                then LBC.putStr output+                else LBC.writeFile outputFile output+++-- | 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.+    }+    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"+            }+        &= summary+               (  "bnb-staking-csvs v"+               <> showVersion version+               <> ", Pavan Rikhi 2021"+               )+        &= program "bnb-staking-csvs"+        &= helpArg [name "h"]+        &= help "Generate CSV Exports of you BinanceChain Staking Rewards."
− src/Lib.hs
@@ -1,226 +0,0 @@-{-| Contains all the functionality of the app.--TODO: Should split out into BnbStaking sub-modules: Main, Binance, Csv---}-{-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE DataKinds #-}-{-# LANGUAGE ViewPatterns #-}-module Lib-    ( run-      -- * CSV Data-    , ExportData(..)-    , convertReward-    , MyZonedTime(..)-      -- * Binance.org API-    , getAllRewards-    , makeRequest-    , Endpoint(..)-    , RewardResponse(..)-    , Reward(..)-    ) where--import           Control.Monad                  ( forM )-import           Data.Aeson                     ( (.:)-                                                , FromJSON(..)-                                                , withObject-                                                )-import           Data.Csv                       ( DefaultOrdered-                                                , ToField(..)-                                                , ToNamedRecord-                                                , encodeDefaultOrderedByName-                                                )-import           Data.List                      ( sortOn )-import           Data.Maybe                     ( fromMaybe )-import           Data.Scientific                ( FPFormat(..)-                                                , Scientific-                                                , formatScientific-                                                )-import           Data.Time                      ( UTCTime-                                                , ZonedTime(..)-                                                , defaultTimeLocale-                                                , formatTime-                                                , parseTimeM-                                                , utcToLocalZonedTime-                                                )-import           GHC.Generics                   ( Generic )-import           Network.HTTP.Req               ( (/:)-                                                , (=:)-                                                , GET(..)-                                                , MonadHttp-                                                , NoReqBody(..)-                                                , Scheme(Https)-                                                , Url-                                                , defaultHttpConfig-                                                , https-                                                , jsonResponse-                                                , req-                                                , responseBody-                                                , runReq-                                                )-import           System.Environment             ( getArgs )-import           System.IO                      ( hPutStrLn-                                                , stderr-                                                )--import qualified Data.ByteString.Lazy.Char8    as LBS-import qualified Data.Text                     as T----- | Run the executable - parsing args, making queries, & printing the--- results.-run :: IO ()-run = getArgs >>= \case-    [pubKey] ->-        runReq defaultHttpConfig (getAllRewards $ T.pack pubKey)-            >>= mapM convertReward-            >>= LBS.putStr-            .   encodeDefaultOrderedByName-    _ -> hPutStrLn-        stderr-        "bnb-staking-csvs: expected 1 argument - <DELEGATOR_PUBKEY>"----- CSV Data---- | Datatype representing a single row in the CSV export.-data ExportData = ExportData-    { time             :: MyZonedTime-    -- ^ The time of the reward.-    , amount           :: T.Text-    -- ^ The reward amount.-    , currency         :: T.Text-    -- ^ Always @BNB@, but sometimes a useful column for CSV imports.-    , delegator        :: T.Text-    -- ^ The address that was rewarded.-    , validator        :: T.Text-    -- ^ The validator's name.-    , validatorAddress :: T.Text-    -- ^ The address the delegator is validating to.-    , height           :: Integer-    -- ^ The height of the reward's block.-    }-    deriving (Show, Read, Generic)--instance ToNamedRecord ExportData-instance DefaultOrdered ExportData---- | Render a 'Reward' into our target export data by converting to--- localtime(respecting DST), & formatting the amount column to 8 decimal--- places.-convertReward :: Reward -> IO ExportData-convertReward Reward {..} = do-    localRewardTime <- utcToLocalZonedTime rRewardTime-    return $ ExportData-        { time             = MyZonedTime localRewardTime-        , amount           = T.pack $ formatScientific Fixed (Just 8) rReward-        , currency         = "BNB"-        , delegator        = rDelegator-        , validator        = rValidatorName-        , validatorAddress = rValidatorAddress-        , height           = rHeight-        }---- | Wrapper type to support custom 'ToField' instance.-newtype MyZonedTime = MyZonedTime { fromMyZonedTime :: ZonedTime } deriving (Show, Read)--instance ToField MyZonedTime where-    toField (MyZonedTime zt) =-        toField $ formatTime defaultTimeLocale "%FT%T%Q%Ez" zt----- Binance.org API---- | 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 { .. }