packages feed

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 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"]