diff --git a/CHANGELOG.md b/CHANGELOG.md index 61f6b9e84..bdb18bc3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ changes. ### Added +- Add drep voting power list endpoint [Issue 3263](https://github.com/IntersectMBO/govtool/issues/3263) + ### Fixed ### Changed @@ -20,7 +22,6 @@ changes. ## [v2.0.18](https://github.com/IntersectMBO/govtool/releases/tag/v2.0.18) 2025-03-20 - ### Added - Add redirection to outcomes when proposal is not found [Issue 3230](https://github.com/IntersectMBO/govtool/issues/3230) diff --git a/govtool/backend/app/Main.hs b/govtool/backend/app/Main.hs index 66514e170..c2d3ca711 100644 --- a/govtool/backend/app/Main.hs +++ b/govtool/backend/app/Main.hs @@ -124,6 +124,7 @@ startApp vvaConfig sentryService = do networkMetricsCache <- newCache networkInfoCache <- newCache networkTotalStakeCache <- newCache + dRepVotingPowerListCache <- newCache return $ CacheEnv { proposalListCache , getProposalCache @@ -137,6 +138,7 @@ startApp vvaConfig sentryService = do , networkMetricsCache , networkInfoCache , networkTotalStakeCache + , dRepVotingPowerListCache } let connectionString = encodeUtf8 (dbSyncConnectionString $ getter vvaConfig) diff --git a/govtool/backend/sql/get-dreps-voting-power-list.sql b/govtool/backend/sql/get-dreps-voting-power-list.sql new file mode 100644 index 000000000..5026177a8 --- /dev/null +++ b/govtool/backend/sql/get-dreps-voting-power-list.sql @@ -0,0 +1,6 @@ +SELECT DISTINCT ON (raw) + view, + encode(raw, 'hex') AS hash_raw, + COALESCE(dd.amount, 0) AS voting_power +FROM drep_hash dh +LEFT JOIN drep_distr dd ON dh.id = dd.hash_id AND dd.epoch_no = (SELECT MAX(no) from epoch) \ No newline at end of file diff --git a/govtool/backend/sql/get-filtered-dreps-voting-power.sql b/govtool/backend/sql/get-filtered-dreps-voting-power.sql new file mode 100644 index 000000000..a18e833c4 --- /dev/null +++ b/govtool/backend/sql/get-filtered-dreps-voting-power.sql @@ -0,0 +1,7 @@ +SELECT DISTINCT ON (raw) + view, + encode(raw, 'hex') AS hash_raw, + COALESCE(dd.amount, 0) AS voting_power +FROM drep_hash dh +LEFT JOIN drep_distr dd ON dh.id = dd.hash_id AND dd.epoch_no = (SELECT MAX(no) from epoch) +WHERE view = ? OR encode(raw, 'hex') = ? \ No newline at end of file diff --git a/govtool/backend/src/VVA/API.hs b/govtool/backend/src/VVA/API.hs index 7b9d0fe4c..360d465b6 100644 --- a/govtool/backend/src/VVA/API.hs +++ b/govtool/backend/src/VVA/API.hs @@ -15,7 +15,7 @@ import Control.Monad.Reader import Data.Aeson (Value(..), Array, decode, encode, ToJSON, toJSON) import Data.Bool (Bool) -import Data.List (sortOn) +import Data.List (sortOn, sort) import qualified Data.Map as Map import Data.Maybe (Maybe (Nothing), catMaybes, fromMaybe, mapMaybe) import Data.Ord (Down (..)) @@ -64,6 +64,9 @@ type VVAApi = :> QueryParam "search" Text :> Get '[JSON] [VoteResponse] :<|> "drep" :> "info" :> Capture "drepId" HexText :> Get '[JSON] DRepInfoResponse + :<|> "drep" :> "voting-power-list" + :> QueryParams "identifiers" Text + :> Get '[JSON] [DRepVotingPowerListResponse] :<|> "ada-holder" :> "get-current-delegation" :> Capture "stakeKey" HexText :> Get '[JSON] (Maybe DelegationResponse) :<|> "ada-holder" :> "get-voting-power" :> Capture "stakeKey" HexText :> Get '[JSON] Integer :<|> "proposal" :> "list" @@ -87,6 +90,7 @@ server = drepList :<|> getVotingPower :<|> getVotes :<|> drepInfo + :<|> drepVotingPowerList :<|> getCurrentDelegation :<|> getStakeKeyVotingPower :<|> listProposals @@ -326,6 +330,24 @@ drepInfo (unHexText -> dRepId) = do , dRepInfoResponseImageHash = HexText <$> dRepInfoImageHash } +drepVotingPowerList :: App m => [Text] -> m [DRepVotingPowerListResponse] +drepVotingPowerList identifiers = do + CacheEnv {dRepVotingPowerListCache} <- asks vvaCache + + let cacheKey = Text.intercalate "," (sort identifiers) + + results <- cacheRequest dRepVotingPowerListCache cacheKey $ + DRep.getDRepsVotingPowerList identifiers + + return $ map toDRepVotingPowerListResponse results + where + toDRepVotingPowerListResponse Types.DRepVotingPowerList{..} = + DRepVotingPowerListResponse + { drepVotingPowerListResponseView = drepView + , drepVotingPowerListResponseHashRaw = HexText drepHashRaw + , drepVotingPowerListResponseVotingPower = drepVotingPower + } + getCurrentDelegation :: App m => HexText -> m (Maybe DelegationResponse) getCurrentDelegation (unHexText -> stakeKey) = do CacheEnv {adaHolderGetCurrentDelegationCache} <- asks vvaCache diff --git a/govtool/backend/src/VVA/API/Types.hs b/govtool/backend/src/VVA/API/Types.hs index ec95925e9..f43888f8a 100644 --- a/govtool/backend/src/VVA/API/Types.hs +++ b/govtool/backend/src/VVA/API/Types.hs @@ -622,6 +622,35 @@ instance ToSchema DRepInfoResponse where & example ?~ toJSON exampleDRepInfoResponse +data DRepVotingPowerListResponse + = DRepVotingPowerListResponse + { drepVotingPowerListResponseView :: Text + , drepVotingPowerListResponseHashRaw :: HexText + , drepVotingPowerListResponseVotingPower :: Integer + } + deriving (Generic, Show) + +deriveJSON (jsonOptions "drepVotingPowerListResponse") ''DRepVotingPowerListResponse + +exampleDRepVotingPowerListResponse :: Text +exampleDRepVotingPowerListResponse = + "{\"view\": \"drep1qq5n7k0r0ff6lf4qvndw9t7vmdqa9y3q9qtjq879rrk9vcjcdy8a4xf92mqsajf9u3nrsh3r6zrp29kuydmfq45fz88qpzmjkc\"," + <> "\"hashRaw\": \"9af10e89979e51b8cdc827c963124a1ef4920d1253eef34a1d5cfe76438e3f11\"," + <> "\"votingPower\": 1000000}" + +instance ToSchema DRepVotingPowerListResponse where + declareNamedSchema proxy = do + NamedSchema name_ schema_ <- + genericDeclareNamedSchema + ( fromAesonOptions $ jsonOptions "drepVotingPowerListResponse" ) + proxy + return $ + NamedSchema name_ $ + schema_ + & description ?~ "DRep Voting Power List Response" + & example + ?~ toJSON exampleDRepVotingPowerListResponse + data GetProposalResponse = GetProposalResponse { getProposalResponseVote :: Maybe VoteParams diff --git a/govtool/backend/src/VVA/DRep.hs b/govtool/backend/src/VVA/DRep.hs index 16f6d70d0..569eb0d86 100644 --- a/govtool/backend/src/VVA/DRep.hs +++ b/govtool/backend/src/VVA/DRep.hs @@ -6,31 +6,33 @@ module VVA.DRep where -import Control.Monad.Except (MonadError) +import Control.Monad.Except (MonadError) import Control.Monad.Reader import Crypto.Hash -import Data.ByteString (ByteString) -import qualified Data.ByteString.Base16 as Base16 -import qualified Data.ByteString.Char8 as C -import Data.FileEmbed (embedFile) -import Data.Foldable (Foldable (sum)) -import Data.Has (Has) -import qualified Data.Map as M -import Data.Maybe (fromMaybe, isJust, isNothing) +import Data.ByteString (ByteString) +import qualified Data.ByteString.Base16 as Base16 +import qualified Data.ByteString.Char8 as C +import Data.FileEmbed (embedFile) +import Data.Foldable (Foldable (sum)) +import Data.Has (Has) +import qualified Data.Map as M +import Data.Maybe (fromMaybe, isJust, isNothing) import Data.Scientific -import Data.String (fromString) -import Data.Text (Text, pack, unpack) -import qualified Data.Text.Encoding as Text +import Data.String (fromString) +import Data.Text (Text, pack, unpack, intercalate) +import qualified Data.Text.Encoding as Text import Data.Time -import qualified Database.PostgreSQL.Simple as SQL +import qualified Database.PostgreSQL.Simple as SQL +import Database.PostgreSQL.Simple.Types (In(..)) + import VVA.Config -import VVA.Pool (ConnectionPool, withPool) -import qualified VVA.Proposal as Proposal -import VVA.Types (AppError, DRepInfo (..), DRepRegistration (..), DRepStatus (..), - DRepType (..), Proposal (..), Vote (..)) +import VVA.Pool (ConnectionPool, withPool) +import qualified VVA.Proposal as Proposal +import VVA.Types (AppError, DRepInfo (..), DRepRegistration (..), DRepStatus (..), + DRepType (..), Proposal (..), Vote (..), DRepVotingPowerList (..)) sqlFrom :: ByteString -> SQL.Query sqlFrom bs = fromString $ unpack $ Text.decodeUtf8 bs @@ -198,3 +200,29 @@ getDRepInfo drepId = withPool $ \conn -> do } [] -> return $ DRepInfo False False False False False Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing _ -> error "Unexpected result from database query in getDRepInfo" + +getAllDRepsVotingPowerSql :: SQL.Query +getAllDRepsVotingPowerSql = sqlFrom $(embedFile "sql/get-dreps-voting-power-list.sql") + +getFilteredDRepVotingPowerSql :: SQL.Query +getFilteredDRepVotingPowerSql = sqlFrom $(embedFile "sql/get-filtered-dreps-voting-power.sql") + +getDRepsVotingPowerList :: + (Has ConnectionPool r, Has VVAConfig r, MonadReader r m, MonadIO m) => + [Text] -> + m [DRepVotingPowerList] +getDRepsVotingPowerList identifiers = withPool $ \conn -> do + results <- if null identifiers + then do + liftIO $ SQL.query_ conn getAllDRepsVotingPowerSql + else do + resultsPerIdentifier <- forM identifiers $ \identifier -> do + liftIO $ SQL.query conn getFilteredDRepVotingPowerSql (identifier, identifier) + + return $ concat resultsPerIdentifier + + return + [ DRepVotingPowerList view hashRaw votingPower + | (view, hashRaw, votingPower') <- results + , let votingPower = floor @Scientific votingPower' + ] \ No newline at end of file diff --git a/govtool/backend/src/VVA/Types.hs b/govtool/backend/src/VVA/Types.hs index 5af17aa16..ad5dd78fc 100644 --- a/govtool/backend/src/VVA/Types.hs +++ b/govtool/backend/src/VVA/Types.hs @@ -96,6 +96,14 @@ data DRepInfo , dRepInfoImageHash :: Maybe Text } +data DRepVotingPowerList + = DRepVotingPowerList + { drepView :: Text + , drepHashRaw :: Text + , drepVotingPower :: Integer + } + deriving (Show, Eq) + data DRepStatus = Active | Inactive | Retired deriving (Show, Eq, Ord) data DRepType = DRep | SoleVoter deriving (Show, Eq) @@ -216,6 +224,7 @@ data CacheEnv , networkMetricsCache :: Cache.Cache () NetworkMetrics , networkInfoCache :: Cache.Cache () NetworkInfo , networkTotalStakeCache :: Cache.Cache () NetworkTotalStake + , dRepVotingPowerListCache :: Cache.Cache Text [DRepVotingPowerList] } data NetworkInfo diff --git a/govtool/backend/vva-be.cabal b/govtool/backend/vva-be.cabal index fe757d486..d9a25afbb 100644 --- a/govtool/backend/vva-be.cabal +++ b/govtool/backend/vva-be.cabal @@ -34,6 +34,8 @@ extra-source-files: sql/get-network-metrics.sql sql/get-network-info.sql sql/get-network-total-stake.sql + sql/get-dreps-voting-power-list.sql + sql/get-filtered-dreps-voting-power.sql executable vva-be main-is: Main.hs