diff --git a/.github/workflows/test_backend.yml b/.github/workflows/test_backend.yml index 28c7b1599..2e01c721b 100644 --- a/.github/workflows/test_backend.yml +++ b/.github/workflows/test_backend.yml @@ -162,7 +162,7 @@ jobs: publish-status: runs-on: ubuntu-latest - if: always() + if: always() && needs.backend-tests.result != 'skipped' needs: [backend-tests, publish-report] steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/test_integration_playwright.yml b/.github/workflows/test_integration_playwright.yml index bbb9c9052..f17e059e8 100644 --- a/.github/workflows/test_integration_playwright.yml +++ b/.github/workflows/test_integration_playwright.yml @@ -202,7 +202,7 @@ jobs: publish-status: runs-on: ubuntu-latest - if: always() + if: always() && needs.integration-tests.result != 'skipped' needs: [integration-tests, publish-report] steps: - uses: actions/checkout@v4 diff --git a/CHANGELOG.md b/CHANGELOG.md index f5c444349..980512907 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,8 +18,26 @@ changes. ### Removed -## [v2.0.17](https://github.com/IntersectMBO/govtool/releases/tag/v2.0.17) 2025-03-18 +## [v2.0.18](https://github.com/IntersectMBO/govtool/releases/tag/v2.0.18) 2025-03-26 + +### 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) +- Add DRep given name to the voting power list endpoint [Issue 3273](https://github.com/IntersectMBO/govtool/issues/3273) +- Add DRep voting power list query to PDF Pillar [Issue 3277](https://github.com/IntersectMBO/govtool/issues/3277) + +### Fixed + +- Fix post-vote navigation to governance action list [Issue 3242](https://github.com/IntersectMBO/govtool/issues/3242) +### 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 ### Added diff --git a/govtool/backend/Dockerfile b/govtool/backend/Dockerfile index 768cfd7cf..cd0b1ac6c 100644 --- a/govtool/backend/Dockerfile +++ b/govtool/backend/Dockerfile @@ -4,4 +4,4 @@ FROM $BASE_IMAGE_REPO:$BASE_IMAGE_TAG WORKDIR /src COPY . . RUN cabal build -RUN cp dist-newstyle/build/x86_64-linux/ghc-9.2.7/vva-be-2.0.17/x/vva-be/build/vva-be/vva-be /usr/local/bin +RUN cp dist-newstyle/build/x86_64-linux/ghc-9.2.7/vva-be-2.0.18/x/vva-be/build/vva-be/vva-be /usr/local/bin diff --git a/govtool/backend/Dockerfile.qovery b/govtool/backend/Dockerfile.qovery index 53a840711..b698475e0 100644 --- a/govtool/backend/Dockerfile.qovery +++ b/govtool/backend/Dockerfile.qovery @@ -4,7 +4,7 @@ FROM $BASE_IMAGE_REPO:$BASE_IMAGE_TAG WORKDIR /src COPY . . RUN cabal build -RUN cp dist-newstyle/build/x86_64-linux/ghc-9.2.7/vva-be-2.0.17/x/vva-be/build/vva-be/vva-be /usr/local/bin +RUN cp dist-newstyle/build/x86_64-linux/ghc-9.2.7/vva-be-2.0.18/x/vva-be/build/vva-be/vva-be /usr/local/bin # Expose the necessary port EXPOSE 9876 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..9d3701847 --- /dev/null +++ b/govtool/backend/sql/get-dreps-voting-power-list.sql @@ -0,0 +1,37 @@ +WITH LatestExistingVotingAnchor AS ( + SELECT + subquery.drep_registration_id, + subquery.drep_hash_id, + subquery.voting_anchor_id, + subquery.url, + subquery.metadata_hash, + subquery.ocvd_id + FROM ( + SELECT + dr.id AS drep_registration_id, + dr.drep_hash_id, + va.id AS voting_anchor_id, + va.url, + encode(va.data_hash, 'hex') AS metadata_hash, + ocvd.id AS ocvd_id, + ROW_NUMBER() OVER (PARTITION BY dr.drep_hash_id ORDER BY dr.tx_id DESC) AS rn + FROM + drep_registration dr + JOIN voting_anchor va ON dr.voting_anchor_id = va.id + JOIN off_chain_vote_data ocvd ON va.id = ocvd.voting_anchor_id + WHERE + ocvd.voting_anchor_id IS NOT NULL + ) subquery + WHERE + subquery.rn = 1 +) +SELECT DISTINCT ON (raw) + view, + encode(raw, 'hex') AS hash_raw, + COALESCE(dd.amount, 0) AS voting_power, + ocvdd.given_name +FROM drep_hash dh +LEFT JOIN drep_distr dd ON dh.id = dd.hash_id AND dd.epoch_no = (SELECT MAX(no) from epoch) +LEFT JOIN LatestExistingVotingAnchor leva ON leva.drep_hash_id = dh.id +LEFT JOIN off_chain_vote_data ocvd ON ocvd.id = leva.ocvd_id +LEFT JOIN off_chain_vote_drep_data ocvdd ON ocvdd.off_chain_vote_data_id = ocvd.id \ 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..74c4b4700 --- /dev/null +++ b/govtool/backend/sql/get-filtered-dreps-voting-power.sql @@ -0,0 +1,38 @@ +WITH LatestExistingVotingAnchor AS ( + SELECT + subquery.drep_registration_id, + subquery.drep_hash_id, + subquery.voting_anchor_id, + subquery.url, + subquery.metadata_hash, + subquery.ocvd_id + FROM ( + SELECT + dr.id AS drep_registration_id, + dr.drep_hash_id, + va.id AS voting_anchor_id, + va.url, + encode(va.data_hash, 'hex') AS metadata_hash, + ocvd.id AS ocvd_id, + ROW_NUMBER() OVER (PARTITION BY dr.drep_hash_id ORDER BY dr.tx_id DESC) AS rn + FROM + drep_registration dr + JOIN voting_anchor va ON dr.voting_anchor_id = va.id + JOIN off_chain_vote_data ocvd ON va.id = ocvd.voting_anchor_id + WHERE + ocvd.voting_anchor_id IS NOT NULL + ) subquery + WHERE + subquery.rn = 1 +) +SELECT DISTINCT ON (raw) + view, + encode(raw, 'hex') AS hash_raw, + COALESCE(dd.amount, 0) AS voting_power, + ocvdd.given_name +FROM drep_hash dh +LEFT JOIN drep_distr dd ON dh.id = dd.hash_id AND dd.epoch_no = (SELECT MAX(no) from epoch) +LEFT JOIN LatestExistingVotingAnchor leva ON leva.drep_hash_id = dh.id +LEFT JOIN off_chain_vote_data ocvd ON ocvd.id = leva.ocvd_id +LEFT JOIN off_chain_vote_drep_data ocvdd ON ocvdd.off_chain_vote_data_id = ocvd.id +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 5bfbd304d..b95960f52 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 @@ -283,18 +287,20 @@ getVotes (unHexText -> dRepId) selectedTypes sortMode mSearch = do CacheEnv {dRepGetVotesCache} <- asks vvaCache (votes, proposals) <- cacheRequest dRepGetVotesCache dRepId $ DRep.getVotes dRepId [] - let voteMap = Map.fromList $ map (\vote@Types.Vote {..} -> (voteProposalId, vote)) votes - - processedProposals <- filter (isProposalSearchedFor mSearch) <$> mapSortAndFilterProposals selectedTypes sortMode proposals - + let voteMapByTxHash = Map.fromList $ + map (\vote -> (pack $ Prelude.takeWhile (/= '#') (unpack $ Types.voteGovActionId vote), vote)) votes + + processedProposals <- filter (isProposalSearchedFor mSearch) <$> + mapSortAndFilterProposals selectedTypes sortMode proposals + return $ [ VoteResponse { voteResponseVote = voteToResponse vote , voteResponseProposal = proposalResponse } - | proposalResponse@ProposalResponse{proposalResponseId} <- processedProposals - , let proposalIdInt = read (unpack proposalResponseId) :: Int - , Just vote <- [Map.lookup (toInteger proposalIdInt) voteMap] + | proposalResponse <- processedProposals + , let txHash = unHexText (proposalResponseTxHash proposalResponse) + , Just vote <- [Map.lookup txHash voteMapByTxHash] ] drepInfo :: App m => HexText -> m DRepInfoResponse @@ -324,6 +330,25 @@ 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 + , drepVotingPowerListResponseGivenName = drepGivenName + } + 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..d1c88adb0 100644 --- a/govtool/backend/src/VVA/API/Types.hs +++ b/govtool/backend/src/VVA/API/Types.hs @@ -622,6 +622,37 @@ instance ToSchema DRepInfoResponse where & example ?~ toJSON exampleDRepInfoResponse +data DRepVotingPowerListResponse + = DRepVotingPowerListResponse + { drepVotingPowerListResponseView :: Text + , drepVotingPowerListResponseHashRaw :: HexText + , drepVotingPowerListResponseVotingPower :: Integer + , drepVotingPowerListResponseGivenName :: Maybe Text + } + deriving (Generic, Show) + +deriveJSON (jsonOptions "drepVotingPowerListResponse") ''DRepVotingPowerListResponse + +exampleDRepVotingPowerListResponse :: Text +exampleDRepVotingPowerListResponse = + "{\"view\": \"drep1qq5n7k0r0ff6lf4qvndw9t7vmdqa9y3q9qtjq879rrk9vcjcdy8a4xf92mqsajf9u3nrsh3r6zrp29kuydmfq45fz88qpzmjkc\"," + <> "\"hashRaw\": \"9af10e89979e51b8cdc827c963124a1ef4920d1253eef34a1d5cfe76438e3f11\"," + <> "\"votingPower\": 1000000," + <> "\"givenName\": \"John Doe\"}" + +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 187cdc73d..49482b1e9 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 @@ -115,16 +117,14 @@ getVotes :: m ([Vote], [Proposal]) getVotes drepId selectedProposals = withPool $ \conn -> do results <- liftIO $ SQL.query conn getVotesSql (SQL.Only drepId) - + if null results then return ([], []) else do let proposalsToSelect = if null selectedProposals then [ govActionId | (_, govActionId, _, _, _, _, _, _, _) <- results] else selectedProposals - allProposals <- mapM (Proposal.getProposals . Just . (:[])) proposalsToSelect - let proposals = concat allProposals let proposalMap = M.fromList $ map (\x -> (proposalId x, x)) proposals @@ -132,7 +132,7 @@ getVotes drepId selectedProposals = withPool $ \conn -> do timeZone <- liftIO getCurrentTimeZone let votes = - [ Vote proposalId' drepId' vote' url' docHash' epochNo' (localTimeToUTC timeZone date') voteTxHash' + [ Vote proposalId' govActionId' drepId' vote' url' docHash' epochNo' (localTimeToUTC timeZone date') voteTxHash' | (proposalId', govActionId', drepId', vote', url', docHash', epochNo', date', voteTxHash') <- results , govActionId' `elem` proposalsToSelect ] @@ -200,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 givenName + | (view, hashRaw, votingPower', givenName) <- 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 5d1f3ca5f..35cdd37bf 100644 --- a/govtool/backend/src/VVA/Types.hs +++ b/govtool/backend/src/VVA/Types.hs @@ -61,14 +61,15 @@ instance Exception AppError data Vote = Vote - { voteProposalId :: Integer - , voteDrepId :: Text - , voteVote :: Text - , voteUrl :: Maybe Text - , voteDocHash :: Maybe Text - , voteEpochNo :: Integer - , voteDate :: UTCTime - , voteTxHash :: Text + { voteProposalId :: Integer + , voteGovActionId :: Text + , voteDrepId :: Text + , voteVote :: Text + , voteUrl :: Maybe Text + , voteDocHash :: Maybe Text + , voteEpochNo :: Integer + , voteDate :: UTCTime + , voteTxHash :: Text } data DRepInfo @@ -95,6 +96,15 @@ data DRepInfo , dRepInfoImageHash :: Maybe Text } +data DRepVotingPowerList + = DRepVotingPowerList + { drepView :: Text + , drepHashRaw :: Text + , drepVotingPower :: Integer + , drepGivenName :: Maybe Text + } + deriving (Show, Eq) + data DRepStatus = Active | Inactive | Retired deriving (Show, Eq, Ord) data DRepType = DRep | SoleVoter deriving (Show, Eq) @@ -215,6 +225,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 704f1d705..d9a25afbb 100644 --- a/govtool/backend/vva-be.cabal +++ b/govtool/backend/vva-be.cabal @@ -1,6 +1,6 @@ cabal-version: 3.6 name: vva-be -version: 2.0.17 +version: 2.0.18 -- A short (one-line) description of the package. -- synopsis: @@ -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 9eb541707..1aa2694d7 100644 --- a/govtool/frontend/package-lock.json +++ b/govtool/frontend/package-lock.json @@ -1,19 +1,19 @@ { "name": "@govtool/frontend", - "version": "2.0.17", + "version": "2.0.18", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@govtool/frontend", - "version": "2.0.17", + "version": "2.0.18", "hasInstallScript": true, "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 ca68f3066..930773db3 100644 --- a/govtool/frontend/package.json +++ b/govtool/frontend/package.json @@ -1,7 +1,7 @@ { "name": "@govtool/frontend", "private": true, - "version": "2.0.17", + "version": "2.0.18", "type": "module", "scripts": { "build": "vite build", @@ -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/components/organisms/DashboardGovernanceActionDetails.tsx b/govtool/frontend/src/components/organisms/DashboardGovernanceActionDetails.tsx index d7dd7be60..d5ffa61c2 100644 --- a/govtool/frontend/src/components/organisms/DashboardGovernanceActionDetails.tsx +++ b/govtool/frontend/src/components/organisms/DashboardGovernanceActionDetails.tsx @@ -1,3 +1,4 @@ +import { useEffect } from "react"; import { useNavigate, useLocation, @@ -5,8 +6,9 @@ import { generatePath, } from "react-router-dom"; import { Box, CircularProgress, Link, Typography } from "@mui/material"; +import { AxiosError } from "axios"; -import { ICONS, PATHS } from "@consts"; +import { ICONS, OUTCOMES_PATHS, PATHS } from "@consts"; import { useCardano } from "@context"; import { useGetProposalQuery, @@ -42,13 +44,24 @@ export const DashboardGovernanceActionDetails = () => { const shortenedGovActionId = txHash && getShortenedGovActionId(txHash, +index); - const { data, isLoading } = useGetProposalQuery( + const { data, isLoading, error } = useGetProposalQuery( fullProposalId ?? "", !state?.proposal || !state?.vote, ); const proposal = (data ?? state)?.proposal; const vote = (data ?? state)?.vote; + useEffect(() => { + const isProposalNotFound = + (error as AxiosError)?.response?.data === + `Proposal with id: ${fullProposalId} not found`; + if (isProposalNotFound && fullProposalId) { + navigate( + OUTCOMES_PATHS.governanceActionOutcomes.replace(":id", fullProposalId), + ); + } + }, [error]); + return ( { searchPhrase: debouncedSearchText, enabled: !isAdjusting, }); + const { data: votes, areDRepVotesLoading } = useGetDRepVotesQuery( + queryFilters, + chosenSorting, + debouncedSearchText, + ); + + // TODO: Black magic - that filtering should be done on the backend + const filteredProposals = proposals + ?.map((proposalCategory) => { + const filteredActions = proposalCategory.actions.filter((action) => { + const hasVote = votes?.some((voteCategory) => + voteCategory.actions.some( + (voteAction) => voteAction.proposal.txHash === action.txHash, + ), + ); + + return !hasVote; + }); + + return { + ...proposalCategory, + actions: filteredActions, + }; + }) + .filter((category) => category.actions.length > 0); const { state } = useLocation(); const [content, setContent] = useState( @@ -189,14 +215,14 @@ export const DashboardGovernanceActions = () => { onDashboard searchPhrase={debouncedSearchText} sorting={chosenSorting} - proposals={proposals} + proposals={filteredProposals} /> diff --git a/govtool/frontend/src/components/organisms/DashboardGovernanceActionsVotedOn.tsx b/govtool/frontend/src/components/organisms/DashboardGovernanceActionsVotedOn.tsx index 5d4dcf134..552f63061 100644 --- a/govtool/frontend/src/components/organisms/DashboardGovernanceActionsVotedOn.tsx +++ b/govtool/frontend/src/components/organisms/DashboardGovernanceActionsVotedOn.tsx @@ -2,38 +2,34 @@ import { useMemo } from "react"; import { Box, Typography, CircularProgress } from "@mui/material"; import { useCardano } from "@context"; -import { - useGetDRepVotesQuery, - useScreenDimension, - useTranslation, -} from "@hooks"; +import { useScreenDimension, useTranslation } from "@hooks"; import { GovernanceVotedOnCard } from "@molecules"; import { Slider } from "@organisms"; import { getFullGovActionId, getProposalTypeLabel } from "@utils"; +import { VotedProposal } from "@/models"; type DashboardGovernanceActionsVotedOnProps = { - filters: string[]; searchPhrase?: string; - sorting: string; + votes: { + title: string; + actions: VotedProposal[]; + }[]; + areDRepVotesLoading: boolean; }; export const DashboardGovernanceActionsVotedOn = ({ - filters, searchPhrase, - sorting, + votes, + areDRepVotesLoading, }: DashboardGovernanceActionsVotedOnProps) => { - const { data, areDRepVotesLoading } = useGetDRepVotesQuery( - filters, - sorting, - searchPhrase, - ); const { isMobile } = useScreenDimension(); const { pendingTransaction } = useCardano(); const { t } = useTranslation(); + // TODO: Filtering here is some kind of craziness. It should be done on the backend. const filteredData = useMemo(() => { - if (data.length && searchPhrase) { - return data + if (votes.length && searchPhrase) { + return votes .map((entry) => ({ ...entry, actions: entry.actions.filter((action) => @@ -44,8 +40,8 @@ export const DashboardGovernanceActionsVotedOn = ({ })) .filter((entry) => entry.actions?.length > 0); } - return data; - }, [data, searchPhrase, pendingTransaction.vote]); + return votes; + }, [votes, searchPhrase, pendingTransaction.vote]); return areDRepVotesLoading ? ( @@ -53,7 +49,7 @@ export const DashboardGovernanceActionsVotedOn = ({ ) : ( <> - {!data.length ? ( + {!votes.length ? ( {t("govActions.youHaventVotedYet")} diff --git a/govtool/frontend/src/consts/queryKeys.ts b/govtool/frontend/src/consts/queryKeys.ts index 825b5f316..722d6d9fa 100644 --- a/govtool/frontend/src/consts/queryKeys.ts +++ b/govtool/frontend/src/consts/queryKeys.ts @@ -13,6 +13,7 @@ export const QUERY_KEYS = { useGetProposalsInfiniteKey: "useGetProposalsInfiniteKey", useGetProposalsKey: "useGetProposalsKey", useGetVoteContextFromFile: "useGetVoteContextFromFile", + useGetDRepVotingPowerListKey: "useGetDRepVotingPowerListKey", }; export const MUTATION_KEYS = { 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/hooks/queries/index.ts b/govtool/frontend/src/hooks/queries/index.ts index db72778d2..73dea9952 100644 --- a/govtool/frontend/src/hooks/queries/index.ts +++ b/govtool/frontend/src/hooks/queries/index.ts @@ -13,3 +13,4 @@ export * from "./useGetProposalsInfiniteQuery"; export * from "./useGetProposalsQuery"; export * from "./useGetVoteContextTextFromFile"; export * from "./useGetVoterInfoQuery"; +export * from "./useGetDRepVotingPowerList"; diff --git a/govtool/frontend/src/hooks/queries/useGetDRepVotesQuery.ts b/govtool/frontend/src/hooks/queries/useGetDRepVotesQuery.ts index bec791c61..0878191bc 100644 --- a/govtool/frontend/src/hooks/queries/useGetDRepVotesQuery.ts +++ b/govtool/frontend/src/hooks/queries/useGetDRepVotesQuery.ts @@ -30,6 +30,8 @@ export const useGetDRepVotesQuery = ( }, }), enabled: !!dRepID, + refetchOnWindowFocus: true, + keepPreviousData: true, }); const groupedByType = data?.reduce((groups, item) => { diff --git a/govtool/frontend/src/hooks/queries/useGetDRepVotingPowerList.ts b/govtool/frontend/src/hooks/queries/useGetDRepVotingPowerList.ts new file mode 100644 index 000000000..3c40d6ca9 --- /dev/null +++ b/govtool/frontend/src/hooks/queries/useGetDRepVotingPowerList.ts @@ -0,0 +1,32 @@ +import { useQuery, useQueryClient } from "react-query"; +import { getDRepVotingPowerList } from "@/services"; +import { QUERY_KEYS } from "@/consts"; + +export const useGetDRepVotingPowerList = () => { + const queryClient = useQueryClient(); + + const { + data: dRepVotingPowerList, + isError, + error, + isLoading, + } = useQuery({ + queryKey: [QUERY_KEYS.useGetDRepVotingPowerListKey], + queryFn: () => getDRepVotingPowerList([]), + enabled: false, + }); + + const fetchDRepVotingPowerList = async (identifiers: string[]) => + queryClient.fetchQuery({ + queryKey: [QUERY_KEYS.useGetDRepVotingPowerListKey], + queryFn: () => getDRepVotingPowerList(identifiers), + }); + + return { + dRepVotingPowerList, + fetchDRepVotingPowerList, + isError, + error, + isLoading, + }; +}; diff --git a/govtool/frontend/src/hooks/queries/useGetProposalQuery.ts b/govtool/frontend/src/hooks/queries/useGetProposalQuery.ts index 309d6e48e..9828c6d59 100644 --- a/govtool/frontend/src/hooks/queries/useGetProposalQuery.ts +++ b/govtool/frontend/src/hooks/queries/useGetProposalQuery.ts @@ -7,7 +7,7 @@ import { getProposal } from "@services"; export const useGetProposalQuery = (proposalId: string, enabled?: boolean) => { const { dRepID } = useCardano(); - const { data, isLoading, refetch, isRefetching } = useQuery( + const { data, isLoading, refetch, isRefetching, error } = useQuery( [QUERY_KEYS.useGetProposalKey, dRepID, proposalId], () => getProposal(proposalId, dRepID), { @@ -21,5 +21,6 @@ export const useGetProposalQuery = (proposalId: string, enabled?: boolean) => { isLoading, refetch, isFetching: isRefetching, + error, }; }; diff --git a/govtool/frontend/src/hooks/queries/useGetProposalsQuery.ts b/govtool/frontend/src/hooks/queries/useGetProposalsQuery.ts index 6843f76af..0ebbe8677 100644 --- a/govtool/frontend/src/hooks/queries/useGetProposalsQuery.ts +++ b/govtool/frontend/src/hooks/queries/useGetProposalsQuery.ts @@ -12,7 +12,7 @@ export const useGetProposalsQuery = ({ sorting, enabled, }: GetProposalsArguments) => { - const { dRepID, pendingTransaction } = useCardano(); + const { dRepID } = useCardano(); const { voter } = useGetVoterInfo(); const fetchProposals = async (): Promise => { @@ -34,17 +34,12 @@ export const useGetProposalsQuery = ({ }; const { data, isLoading } = useQuery( - [ - QUERY_KEYS.useGetProposalsKey, - filters, - searchPhrase, - sorting, - dRepID, - pendingTransaction.vote?.transactionHash, - ], + [QUERY_KEYS.useGetProposalsKey, filters, searchPhrase, sorting, dRepID], fetchProposals, { enabled, + refetchOnWindowFocus: true, + keepPreviousData: true, }, ); diff --git a/govtool/frontend/src/models/api.ts b/govtool/frontend/src/models/api.ts index 60890fd6d..f298b41f0 100644 --- a/govtool/frontend/src/models/api.ts +++ b/govtool/frontend/src/models/api.ts @@ -265,3 +265,12 @@ export type Infinite = { pageSize: number; total: number; }; + +type DRepVotingPower = { + view: string; + hashRaw: string; + votingPower: number; + givenName: string | null; +}; + +export type DRepVotingPowerListResponse = DRepVotingPower[]; 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 &&