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 +5/−0
- LICENSE +30/−0
- README.md +57/−0
- Setup.hs +2/−0
- app/Main.hs +6/−0
- bnb-staking-csvs.cabal +106/−0
- src/Lib.hs +226/−0
- tests/Spec.hs +34/−0
+ 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++[](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)