diff --git a/CHANGELOG.md b/CHANGELOG.md index 61f6b9e84..e1ae987a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,12 +18,12 @@ changes. ### Removed -## [v2.0.18](https://github.com/IntersectMBO/govtool/releases/tag/v2.0.18) 2025-03-20 - +## [v2.0.18](https://github.com/IntersectMBO/govtool/releases/tag/v2.0.18) 2025-03-25 ### Added - Add redirection to outcomes when proposal is not found [Issue 3230](https://github.com/IntersectMBO/govtool/issues/3230) +- Add drep voting power list endpoint [Issue 3263](https://github.com/IntersectMBO/govtool/issues/3263) ### Fixed @@ -31,6 +31,8 @@ changes. ### Changed +- Bump CSL to v14 [Issue 3037](https://github.com/IntersectMBO/govtool/issues/3037) + ### Removed ## [v2.0.17](https://github.com/IntersectMBO/govtool/releases/tag/v2.0.17) 2025-03-18 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 diff --git a/govtool/frontend/package-lock.json b/govtool/frontend/package-lock.json index 62274e080..1aa2694d7 100644 --- a/govtool/frontend/package-lock.json +++ b/govtool/frontend/package-lock.json @@ -11,9 +11,9 @@ "dependencies": { "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", - "@emurgo/cardano-serialization-lib-asmjs": "^12.1.1", + "@emurgo/cardano-serialization-lib-asmjs": "^14.1.1", "@hookform/resolvers": "^3.3.1", - "@intersect.mbo/govtool-outcomes-pillar-ui": "1.2.5", + "@intersect.mbo/govtool-outcomes-pillar-ui": "1.3.0", "@intersect.mbo/intersectmbo.org-icons-set": "^1.0.8", "@intersect.mbo/pdf-ui": "0.6.4", "@mui/icons-material": "^5.14.3", @@ -2653,9 +2653,9 @@ "license": "MIT" }, "node_modules/@emurgo/cardano-serialization-lib-asmjs": { - "version": "12.1.1", - "resolved": "https://registry.npmjs.org/@emurgo/cardano-serialization-lib-asmjs/-/cardano-serialization-lib-asmjs-12.1.1.tgz", - "integrity": "sha512-K3f28QUfLDJ7seO6MtKfMYtRm5ccf36TQ5yxyTmZqX1TA85MkriEdxqpgV9KLiLEA95emwnlvU2/WmlHMRPg1A==", + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/@emurgo/cardano-serialization-lib-asmjs/-/cardano-serialization-lib-asmjs-14.1.1.tgz", + "integrity": "sha512-Q2HVpPRt417Quxv3qagGWbkJQU8SiQCl1K/344ZtQMwsLoqTfRlCNzmSWMBN7jyBxbtKoh+vdbSiLqwG1NAjYg==", "license": "MIT" }, "node_modules/@esbuild/aix-ppc64": { @@ -3389,16 +3389,20 @@ } }, "node_modules/@intersect.mbo/govtool-outcomes-pillar-ui": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@intersect.mbo/govtool-outcomes-pillar-ui/-/govtool-outcomes-pillar-ui-1.2.5.tgz", - "integrity": "sha512-z0QDUZKj262vvNCtjr2L8ILgeub8da3ryhMhgLJpuxuO1zfEBUhTuwUPkRuLBE1Gf52TW/AfgmR2qXANsjY4/Q==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@intersect.mbo/govtool-outcomes-pillar-ui/-/govtool-outcomes-pillar-ui-1.3.0.tgz", + "integrity": "sha512-6+H+QG8kyM2UUEycNsjrF1K5+UGUw6+wy7gRxlyOtFjIrZ9CUdTfwwyD1hrh+g55awZ4t+EmQgYB00An0iXOag==", "license": "ISC", "dependencies": { "@fontsource/poppins": "^5.0.14", "@intersect.mbo/intersectmbo.org-icons-set": "^1.0.8", - "axios": "^1.7.9", + "axios": "^1.8.4", "bech32": "^2.0.0", - "buffer": "^6.0.3" + "buffer": "^6.0.3", + "react-diff-view": "^3.2.1", + "rehype-katex": "^7.0.1", + "remark-math": "^6.0.0", + "unidiff": "^1.0.4" }, "peerDependencies": { "@emotion/react": "^11.11.4", @@ -3444,6 +3448,12 @@ "sass": "^1.77.2" } }, + "node_modules/@intersect.mbo/pdf-ui/node_modules/@emurgo/cardano-serialization-lib-asmjs": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/@emurgo/cardano-serialization-lib-asmjs/-/cardano-serialization-lib-asmjs-12.1.1.tgz", + "integrity": "sha512-K3f28QUfLDJ7seO6MtKfMYtRm5ccf36TQ5yxyTmZqX1TA85MkriEdxqpgV9KLiLEA95emwnlvU2/WmlHMRPg1A==", + "license": "MIT" + }, "node_modules/@intersect.mbo/pdf-ui/node_modules/@types/hast": { "version": "2.3.10", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", @@ -10025,9 +10035,9 @@ } }, "node_modules/axios": { - "version": "1.7.9", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", - "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", + "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", diff --git a/govtool/frontend/package.json b/govtool/frontend/package.json index 4f9ba9651..930773db3 100644 --- a/govtool/frontend/package.json +++ b/govtool/frontend/package.json @@ -25,9 +25,9 @@ "dependencies": { "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", - "@emurgo/cardano-serialization-lib-asmjs": "^12.1.1", + "@emurgo/cardano-serialization-lib-asmjs": "^14.1.1", "@hookform/resolvers": "^3.3.1", - "@intersect.mbo/govtool-outcomes-pillar-ui": "1.2.5", + "@intersect.mbo/govtool-outcomes-pillar-ui": "1.3.0", "@intersect.mbo/intersectmbo.org-icons-set": "^1.0.8", "@intersect.mbo/pdf-ui": "0.6.4", "@mui/icons-material": "^5.14.3", diff --git a/govtool/frontend/src/context/wallet.tsx b/govtool/frontend/src/context/wallet.tsx index 77c3de83a..a9a38f2a9 100644 --- a/govtool/frontend/src/context/wallet.tsx +++ b/govtool/frontend/src/context/wallet.tsx @@ -540,6 +540,7 @@ const CardanoProvider = (props: Props) => { .max_value_size(epochParams.max_val_size) .max_tx_size(epochParams.max_tx_size) .prefer_pure_change(true) + .do_not_burn_extra_change(true) .ex_unit_prices( ExUnitPrices.new( UnitInterval.new( diff --git a/govtool/frontend/src/pages/GovernanceActionOutComes.tsx b/govtool/frontend/src/pages/GovernanceActionOutComes.tsx index cf703d9b0..79db53f63 100644 --- a/govtool/frontend/src/pages/GovernanceActionOutComes.tsx +++ b/govtool/frontend/src/pages/GovernanceActionOutComes.tsx @@ -3,6 +3,7 @@ import React, { Suspense } from "react"; import { Footer, TopNav } from "@/components/organisms"; import { useCardano } from "@/context"; import { useScreenDimension } from "@/hooks"; +import { Background } from "@/components/atoms"; const GovernanceActionsOutcomes = React.lazy( () => import("@intersect.mbo/govtool-outcomes-pillar-ui/dist/esm"), @@ -10,45 +11,49 @@ const GovernanceActionsOutcomes = React.lazy( export const GovernanceActionOutComesPillar = () => { const { pagePadding } = useScreenDimension(); - const { ...context } = useCardano(); + const { walletApi, ...context } = useCardano(); return ( - - {!context.isEnabled && } + - - - - } + {!context.isEnabled && } + - - + + + + } + > + + + + {!context.isEnabled &&