solana-staking-csvs 0.1.3.0 → 0.2.0.0
raw patch · 4 files changed
+86/−58 lines, 4 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
- Console.SolanaStaking.Api: [saTotalPages] :: StakingAccounts -> Integer
- Console.SolanaStaking.Api: data StakingAccounts
+ Console.SolanaStaking.Api: instance Data.Aeson.Types.FromJSON.FromJSON Console.SolanaStaking.Api.PaginationData
+ Console.SolanaStaking.Api: instance Data.Aeson.Types.FromJSON.FromJSON Console.SolanaStaking.Api.StakingRewards
+ Console.SolanaStaking.Api: instance GHC.Classes.Eq Console.SolanaStaking.Api.PaginationData
+ Console.SolanaStaking.Api: instance GHC.Classes.Eq Console.SolanaStaking.Api.StakingRewards
+ Console.SolanaStaking.Api: instance GHC.Generics.Generic Console.SolanaStaking.Api.PaginationData
+ Console.SolanaStaking.Api: instance GHC.Generics.Generic Console.SolanaStaking.Api.StakingRewards
+ Console.SolanaStaking.Api: instance GHC.Read.Read Console.SolanaStaking.Api.PaginationData
+ Console.SolanaStaking.Api: instance GHC.Read.Read Console.SolanaStaking.Api.StakingRewards
+ Console.SolanaStaking.Api: instance GHC.Show.Show Console.SolanaStaking.Api.PaginationData
+ Console.SolanaStaking.Api: instance GHC.Show.Show Console.SolanaStaking.Api.StakingRewards
+ Console.SolanaStaking.Api: newtype StakingAccounts
- Console.SolanaStaking.Api: StakingAccounts :: [StakingAccount] -> Integer -> StakingAccounts
+ Console.SolanaStaking.Api: StakingAccounts :: [StakingAccount] -> StakingAccounts
- Console.SolanaStaking.Api: getStakeRewards :: (MonadReader Config m, MonadCatch m, MonadIO m) => StakingPubKey -> Maybe Integer -> m (APIResponse [StakeReward])
+ Console.SolanaStaking.Api: getStakeRewards :: (MonadReader Config m, MonadCatch m, MonadIO m) => StakingPubKey -> Maybe Integer -> m (APIResponse StakingRewards)
Files
- CHANGELOG.md +5/−0
- solana-staking-csvs.cabal +3/−3
- src/Console/SolanaStaking/Api.hs +58/−34
- src/Console/SolanaStaking/Main.hs +20/−21
CHANGELOG.md view
@@ -4,6 +4,11 @@ +## v0.2.0.0++* Migrate to new `v2` API endpoints.++ ## v0.1.3.0 * Bump dependency versions(text).
solana-staking-csvs.cabal view
@@ -1,11 +1,11 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.36.0.+-- This file has been generated from package.yaml by hpack version 0.38.1. -- -- see: https://github.com/sol/hpack name: solana-staking-csvs-version: 0.1.3.0+version: 0.2.0.0 synopsis: Generate CSV Exports of your Solana Staking Rewards. description: @solana-staking-csvs@ is a CLI program that queries the Solana blockchain for an account's staking accounts and exports all their staking rewards to@@ -29,7 +29,7 @@ bug-reports: https://github.com/prikhi/solana-staking-csvs/issues author: Pavan Rikhi maintainer: pavan.rikhi@gmail.com-copyright: 2021-2024 Pavan Rikhi+copyright: 2021-2025 Pavan Rikhi license: BSD3 license-file: LICENSE build-type: Simple
src/Console/SolanaStaking/Api.hs view
@@ -1,5 +1,6 @@ {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-} @@ -114,7 +115,7 @@ -- | Base URL to Solana Beach's API baseUrl :: Url 'Https-baseUrl = https "api.solanabeach.io" /: "v1"+baseUrl = https "api.solanabeach.io" /: "v2" -- | Get the staking accounts for the 'cAccountPubKey'.@@ -123,24 +124,19 @@ => m (APIResponse StakingAccounts) getAccountStakes = do pubkey <- asks cAccountPubKey- getReq (baseUrl /: "account" /~ pubkey /: "stakes") mempty+ getReq (baseUrl /: "account" /~ pubkey /: "stake-accounts") mempty -- | Single Result Page of Staking Accounts Query.-data StakingAccounts = StakingAccounts+newtype StakingAccounts = StakingAccounts { saResults :: [StakingAccount] -- ^ The returned staking accounts- , saTotalPages :: Integer- -- ^ The total number of pages for the Account's PubKey. } deriving (Show, Read, Eq, Generic) instance FromJSON StakingAccounts where- parseJSON = withObject "StakingAccounts" $ \o -> do- saResults <- o .: "data"- saTotalPages <- o .: "totalPages"- return StakingAccounts {..}+ parseJSON v = StakingAccounts <$> parseJSON v -- | A single Staking Account.@@ -157,15 +153,9 @@ instance FromJSON StakingAccount where parseJSON = withObject "StakingAccount" $ \o -> do- saPubKey <- o .: "pubkey" >>= (.: "address")- saLamports <- o .: "lamports"- saValidatorName <-- o- .: "data"- >>= (.: "stake")- >>= (.: "delegation")- >>= (.: "validatorInfo")- >>= (.: "name")+ saPubKey <- o .: "stakePubkey"+ saLamports <- o .: "stake"+ saValidatorName <- o .: "name" return StakingAccount {..} @@ -194,11 +184,13 @@ :: (MonadReader Config m, MonadCatch m, MonadIO m) => StakingPubKey -> Maybe Integer- -> m (APIResponse [StakeReward])-getStakeRewards (StakingPubKey stakeAccountPubkey) mbEpochCursor = do+ -> m (APIResponse StakingRewards)+getStakeRewards (StakingPubKey stakeAccountPubkey) mbOffset = do getReq- (baseUrl /: "account" /: stakeAccountPubkey /: "stake-rewards")- (queryParam "cursor" mbEpochCursor)+ (baseUrl /: "account" /: stakeAccountPubkey /: "staking-rewards")+ ( queryParam "limit" (Just 1000 :: Maybe Integer)+ <> queryParam "offset" mbOffset+ ) -- | Get all the staking rewards for the given account.@@ -251,20 +243,34 @@ => StakingPubKey -> ([StakeReward] -> Bool) -> ([APIError], [StakeReward])- -> Either APIError [StakeReward]+ -> Either APIError StakingRewards -> m ([APIError], [StakeReward]) getStakeRewardsUntil pubkey cond (errs, rws) = \case Left err -> return (err : errs, rws)- Right rewards ->- if null rewards || cond rewards- then return (errs, rewards <> rws)+ Right res ->+ if null res.srRewards || cond res.srRewards+ then return (errs, res.srRewards <> rws) else- let minEpoch = minimum $ map srEpoch rewards- in getStakeRewards pubkey (Just minEpoch)+ let offset = res.srPagination.limit + res.srPagination.offset+ in getStakeRewards pubkey (Just offset) >>= runApi- >>= getStakeRewardsUntil pubkey cond (errs, rewards <> rws)+ >>= getStakeRewardsUntil pubkey cond (errs, res.srRewards <> rws) +data StakingRewards = StakingRewards+ { srRewards :: [StakeReward]+ , srPagination :: PaginationData+ }+ deriving (Show, Read, Eq, Generic)+++instance FromJSON StakingRewards where+ parseJSON = withObject "StakingRewards" $ \o -> do+ srRewards <- o .: "rewards"+ srPagination <- o .: "pagination"+ pure StakingRewards {..}++ -- | A Staking Reward Payment. data StakeReward = StakeReward { srEpoch :: Integer@@ -280,9 +286,12 @@ instance FromJSON StakeReward where parseJSON = withObject "StakeReward" $ \o -> do- srEpoch <- o .: "epoch"- srSlot <- o .: "effectiveSlot"- srAmount <- o .: "amount"+ -- v1 API returned a different epoch than v2.+ -- We continue returning the v1 amount(1 less than v2).+ -- Maybe "effective" vs "granted" epoch?+ srEpoch <- pred <$> o .: "epoch"+ srSlot <- o .: "slot"+ srAmount <- o .: "lamports" srTimestamp <- o .: "timestamp" return StakeReward {..} @@ -316,6 +325,22 @@ return Block {..} +data PaginationData = PaginationData+ { total :: Integer+ , offset :: Integer+ , limit :: Integer+ }+ deriving (Show, Read, Eq, Generic)+++instance FromJSON PaginationData where+ parseJSON = withObject "PaginationData" $ \o -> do+ total <- o .: "total"+ offset <- o .: "offset"+ limit <- o .: "limit"+ pure PaginationData {..}++ -- | Generic GET request to the Solana Beach API with up to 5 retries for -- @ProcessingResponse@. --@@ -342,8 +367,7 @@ catchRateLimitError $ runReq defaultHttpConfig- ( req GET endpoint NoReqBody jsonResponse $ authHeader <> options- )+ (req GET endpoint NoReqBody jsonResponse $ authHeader <> options) case respBody of ProcessingResponse -> do liftIO $
src/Console/SolanaStaking/Main.hs view
@@ -13,7 +13,8 @@ import Control.Monad.Reader (MonadIO, ReaderT, liftIO, runReaderT) import Data.Bifunctor (bimap, second) import Data.List (sortOn)-import Data.Maybe (fromMaybe, mapMaybe)+import Data.List.NonEmpty (NonEmpty (..))+import Data.Maybe (fromMaybe) import Data.Time (LocalTime (..), ZonedTime (..), utcToLocalZonedTime) import Data.Time.Clock.POSIX (posixSecondsToUTCTime) import Data.Version (showVersion)@@ -40,6 +41,7 @@ import Paths_solana_staking_csvs (version) import Data.ByteString.Lazy qualified as LBS+import Data.List.NonEmpty qualified as NE import Data.Map.Strict qualified as M import Data.Text qualified as T @@ -79,7 +81,7 @@ runner = runExceptT . flip runReaderT (mkConfig argApiKey argPubKey) aggregateRewards :: (MonadIO m) => [(StakingAccount, StakeReward)] -> m [(StakingAccount, StakeReward)] aggregateRewards =- fmap (mapMaybe (aggregate . snd) . M.toList)+ fmap (map (aggregate . snd) . M.toList) . foldM ( \acc r -> do rewardTime <- liftIO . utcToLocalZonedTime $ posixSecondsToUTCTime $ srTimestamp $ snd r@@ -87,28 +89,25 @@ M.insertWith (<>) (localDay $ zonedTimeToLocalTime rewardTime)- [r]+ (pure r) acc ) M.empty - aggregate :: [(StakingAccount, StakeReward)] -> Maybe (StakingAccount, StakeReward)- aggregate = \case- [] -> Nothing- rs ->- Just- ( StakingAccount- { saValidatorName = "AGGREGATE-" <> T.pack (show $ length rs)- , saPubKey = StakingPubKey $ T.pack argPubKey- , saLamports = sum $ map (saLamports . fst) rs- }- , StakeReward- { srTimestamp = minimum $ map (srTimestamp . snd) rs- , srSlot = srSlot $ snd $ head rs- , srEpoch = srEpoch $ snd $ head rs- , srAmount = sum $ map (srAmount . snd) rs- }- )+ aggregate :: NonEmpty (StakingAccount, StakeReward) -> (StakingAccount, StakeReward)+ aggregate rs =+ ( StakingAccount+ { saValidatorName = "AGGREGATE-" <> T.pack (show $ length rs)+ , saPubKey = StakingPubKey $ T.pack argPubKey+ , saLamports = sum $ fmap (saLamports . fst) rs+ }+ , StakeReward+ { srTimestamp = minimum $ fmap (srTimestamp . snd) rs+ , srSlot = srSlot . snd $ NE.head rs+ , srEpoch = srEpoch . snd $ NE.head rs+ , srAmount = sum $ fmap (srAmount . snd) rs+ }+ ) -- | CLI arguments supported by the executable.@@ -169,7 +168,7 @@ &= summary ( "solana-staking-csvs v" <> showVersion version- <> ", Pavan Rikhi 2021-2024"+ <> ", Pavan Rikhi 2021-2025" ) &= program "solana-staking-csvs" &= helpArg [name "h"]