packages feed

bnb-staking-csvs (empty) → 0.1.0.0

raw patch · 8 files changed

+466/−0 lines, 8 filesdep +aesondep +basedep +bnb-staking-csvssetup-changed

Dependencies added: aeson, base, bnb-staking-csvs, bytestring, cassava, hedgehog, req, scientific, tasty, tasty-hedgehog, tasty-hunit, text, time

Files

+ CHANGELOG.md view
@@ -0,0 +1,5 @@+# CHANGELOG++## v0.1.0.0++* Initial release
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Pavan Rikhi (c) 2021++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Pavan Rikhi nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,57 @@+# bnb-staking-csvs++[![bnb-staking-csvs Build Status](https://github.com/prikhi/bnb-staking-csvs/actions/workflows/main.yml/badge.svg)](https://github.com/prikhi/bnb-staking-csvs/actions/workflows/main.yml)+++Generate CSV exports of your staking rewards on BinanceChain.++Requires [`stack`][get-stack]:++```sh+stack run -- <DELEGATOR_PUBKEY>+```++[get-stack]: https://docs.haskellstack.org/en/stable/README/+++## Install++You can install the CLI exe by running `stack install`. This lets you call the+executable directly instead of through stack:++```sh+$ stack install+$ export PATH="${HOME}/.local/bin/:${PATH}"+$ bnb-staking-csvs bnb1hwzqet2vusraqvxfxgr6zhupd8kpgz5hpl2wf2 | head -n 4+time,amount,currency,delegator,validator,validatorAddress,height+2021-04-09T20:00:00-04:00,0.00118792,BNB,bnb1hwzqet2vusraqvxfxgr6zhupd8kpgz5hpl2wf2,BscScan,bva1t42gtf6hawqgpmdpjzmvlzvmlttlqtkvlmgjxt,155843908+2021-04-10T20:00:00-04:00,0.00095682,BNB,bnb1hwzqet2vusraqvxfxgr6zhupd8kpgz5hpl2wf2,BscScan,bva1t42gtf6hawqgpmdpjzmvlzvmlttlqtkvlmgjxt,156060618+2021-04-11T20:00:00-04:00,1.74196290,BNB,bnb1hwzqet2vusraqvxfxgr6zhupd8kpgz5hpl2wf2,TW Staking,bva1c6aqe9ndzcn2nsan963z43xg6kgrvzynl97785,156277904+```+++## Build++You can build the project with stack:++```sh+stack build+```++For development, you can enable fast builds with file-watching,+documentation-building, & test-running:++```sh+stack test --haddock --fast --file-watch --pedantic+```++To build & open the documentation, run++```sh+stack haddock --open bnb-staking-csvs+```+++## LICENSE++BSD-3
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ app/Main.hs view
@@ -0,0 +1,6 @@+module Main where++import           Lib++main :: IO ()+main = run
+ bnb-staking-csvs.cabal view
@@ -0,0 +1,106 @@+cabal-version: 1.12++-- This file has been generated from package.yaml by hpack version 0.34.4.+--+-- 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+                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+homepage:       https://github.com/prikhi/bnb-staking-csvs#readme+bug-reports:    https://github.com/prikhi/bnb-staking-csvs/issues+author:         Pavan Rikhi+maintainer:     pavan.rikhi@gmail.com+copyright:      2021 Pavan Rikhi+license:        BSD3+license-file:   LICENSE+build-type:     Simple+extra-source-files:+    README.md+    CHANGELOG.md++source-repository head+  type: git+  location: https://github.com/prikhi/bnb-staking-csvs++library+  exposed-modules:+      Lib+  other-modules:+      Paths_bnb_staking_csvs+  hs-source-dirs:+      src+  default-extensions:+      DeriveGeneric+      LambdaCase+      NamedFieldPuns+      OverloadedStrings+      TupleSections+      TypeApplications+      TypeOperators+      ViewPatterns+  ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints -O2+  build-depends:+      aeson <2+    , base >=4.7 && <5+    , bytestring <1+    , cassava <1+    , req ==3.*+    , scientific <1+    , text ==1.*+    , time ==1.*+  default-language: Haskell2010++executable bnb-staking-csvs+  main-is: Main.hs+  other-modules:+      Paths_bnb_staking_csvs+  hs-source-dirs:+      app+  default-extensions:+      DeriveGeneric+      LambdaCase+      NamedFieldPuns+      OverloadedStrings+      TupleSections+      TypeApplications+      TypeOperators+      ViewPatterns+  ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints -O2 -threaded -rtsopts -with-rtsopts "-N -T"+  build-depends:+      base >=4.7 && <5+    , bnb-staking-csvs+  default-language: Haskell2010++test-suite bnb-staking-csvs-test+  type: exitcode-stdio-1.0+  main-is: Spec.hs+  other-modules:+      Paths_bnb_staking_csvs+  hs-source-dirs:+      tests+  default-extensions:+      DeriveGeneric+      LambdaCase+      NamedFieldPuns+      OverloadedStrings+      TupleSections+      TypeApplications+      TypeOperators+      ViewPatterns+  ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints -O2 -threaded -rtsopts -with-rtsopts "-N -T"+  build-depends:+      base >=4.7 && <5+    , bnb-staking-csvs+    , hedgehog+    , tasty+    , tasty-hedgehog+    , tasty-hunit+  default-language: Haskell2010
+ src/Lib.hs view
@@ -0,0 +1,226 @@+{-| 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 { .. }
+ tests/Spec.hs view
@@ -0,0 +1,34 @@+import           Hedgehog+import           Test.Tasty+import           Test.Tasty.HUnit+import           Test.Tasty.Hedgehog++import qualified Hedgehog.Gen                  as Gen+import qualified Hedgehog.Range                as Range+++main :: IO ()+main = defaultMain tests+++tests :: TestTree+tests = testGroup "Tests" [unitTests, properties]+++unitTests :: TestTree+unitTests = testGroup "Unit Tests" [testCase "2+2 = 4" testAddition]+  where+    testAddition :: Assertion+    testAddition = (2 + 2) @?= (4 :: Integer)+++properties :: TestTree+properties = testGroup+    "Properties"+    [testProperty "Addition is Communative" testAdditionCommunative]+  where+    testAdditionCommunative :: Property+    testAdditionCommunative = property $ do+        let genInt = Gen.int $ Range.linear 0 9001+        (a, b) <- forAll $ (,) <$> genInt <*> genInt+        (a + b) === (b + a)