diff --git a/CHANGELOG.md b/CHANGELOG.md index 47a85717a..53a5dc973 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ changes. ### Fixed +- Fix missing off chain references in DRep details [Issue 3490](https://github.com/IntersectMBO/govtool/issues/3490) + ### Changed ### Removed diff --git a/govtool/backend/sql/list-dreps.sql b/govtool/backend/sql/list-dreps.sql index 2c3b87aa5..3062c9f48 100644 --- a/govtool/backend/sql/list-dreps.sql +++ b/govtool/backend/sql/list-dreps.sql @@ -126,7 +126,35 @@ DRepData AS ( off_chain_vote_drep_data.motivations, off_chain_vote_drep_data.qualifications, off_chain_vote_drep_data.image_url, - off_chain_vote_drep_data.image_hash + off_chain_vote_drep_data.image_hash, + COALESCE( + ( + SELECT jsonb_agg(ref) + FROM jsonb_array_elements( + CASE + WHEN (ocvd.json::jsonb)->'body'->'references' IS NOT NULL + THEN (ocvd.json::jsonb)->'body'->'references' + ELSE '[]'::jsonb + END + ) AS ref + WHERE ref->>'@type' = 'Identity' + ), + '[]'::jsonb + ) AS identity_references, + COALESCE( + ( + SELECT jsonb_agg(ref) + FROM jsonb_array_elements( + CASE + WHEN (ocvd.json::jsonb)->'body'->'references' IS NOT NULL + THEN (ocvd.json::jsonb)->'body'->'references' + ELSE '[]'::jsonb + END + ) AS ref + WHERE ref->>'@type' = 'Link' + ), + '[]'::jsonb + ) AS link_references FROM drep_hash dh JOIN RankedDRepRegistration ON RankedDRepRegistration.drep_hash_id = dh.id @@ -212,7 +240,29 @@ DRepData AS ( off_chain_vote_drep_data.motivations, off_chain_vote_drep_data.qualifications, off_chain_vote_drep_data.image_url, - off_chain_vote_drep_data.image_hash + off_chain_vote_drep_data.image_hash, + ( + SELECT jsonb_agg(ref) + FROM jsonb_array_elements( + CASE + WHEN (ocvd.json::jsonb)->'body'->'references' IS NOT NULL + THEN (ocvd.json::jsonb)->'body'->'references' + ELSE '[]'::jsonb + END + ) AS ref + WHERE ref->>'@type' = 'Identity' + ), + ( + SELECT jsonb_agg(ref) + FROM jsonb_array_elements( + CASE + WHEN (ocvd.json::jsonb)->'body'->'references' IS NOT NULL + THEN (ocvd.json::jsonb)->'body'->'references' + ELSE '[]'::jsonb + END + ) AS ref + WHERE ref->>'@type' = 'Link' + ) ) SELECT * FROM DRepData WHERE diff --git a/govtool/backend/src/VVA/API.hs b/govtool/backend/src/VVA/API.hs index 9488710a6..124356990 100644 --- a/govtool/backend/src/VVA/API.hs +++ b/govtool/backend/src/VVA/API.hs @@ -134,7 +134,9 @@ drepRegistrationToDrep Types.DRepRegistration {..} = dRepMotivations = dRepRegistrationMotivations, dRepQualifications = dRepRegistrationQualifications, dRepImageUrl = dRepRegistrationImageUrl, - dRepImageHash = HexText <$> dRepRegistrationImageHash + dRepImageHash = HexText <$> dRepRegistrationImageHash, + dRepIdentityReferences = DRepReferences <$> dRepRegistrationIdentityReferences, + dRepLinkReferences = DRepReferences <$> dRepRegistrationLinkReferences } delegationToResponse :: Types.Delegation -> DelegationResponse diff --git a/govtool/backend/src/VVA/API/Types.hs b/govtool/backend/src/VVA/API/Types.hs index 6b517a9ce..f478bd974 100644 --- a/govtool/backend/src/VVA/API/Types.hs +++ b/govtool/backend/src/VVA/API/Types.hs @@ -273,7 +273,7 @@ instance ToParamSchema GovernanceActionSortMode where newtype GovernanceActionDetails - = GovernanceActionDetails { getValue :: Value } + = GovernanceActionDetails { getGovernanceActionValue :: Value } deriving newtype (Show) instance FromJSON GovernanceActionDetails where @@ -593,6 +593,7 @@ instance ToSchema VoteResponse where & example ?~ toJSON exampleVoteResponse + data DRepInfoResponse = DRepInfoResponse { dRepInfoResponseIsScriptBased :: Bool @@ -612,7 +613,7 @@ data DRepInfoResponse , dRepInfoResponseGivenName :: Maybe Text , dRepInfoResponseObjectives :: Maybe Text , dRepInfoResponseMotivations :: Maybe Text - , dRepInfoResponseQualifications :: Maybe Text + , dRepInfoResponseQualifications :: Maybe Text , dRepInfoResponseImageUrl :: Maybe Text , dRepInfoResponseImageHash :: Maybe HexText } @@ -820,6 +821,27 @@ instance ToSchema DRepType where & description ?~ "DRep Type" & enum_ ?~ map toJSON [NormalDRep, SoleVoter] +newtype DRepReferences + = DRepReferences { getDRepReferencesValue :: Value } + deriving newtype (Show) + +instance FromJSON DRepReferences where + parseJSON v = return $ DRepReferences v + +instance ToJSON DRepReferences where + toJSON (DRepReferences d) = d + +instance ToSchema DRepReferences where + declareNamedSchema _ = pure $ NamedSchema (Just "DRepReferences") $ mempty + & type_ ?~ OpenApiObject + & description ?~ "A JSON value that can include nested objects and arrays" + & example ?~ toJSON + (Aeson.object + [ "some_key" .= ("some value" :: String) + , "nested_key" .= Aeson.object ["inner_key" .= (1 :: Int)] + , "array_key" .= [1, 2, 3 :: Int] + ]) + data DRep = DRep { dRepIsScriptBased :: Bool @@ -838,9 +860,11 @@ data DRep , dRepGivenName :: Maybe Text , dRepObjectives :: Maybe Text , dRepMotivations :: Maybe Text - , dRepQualifications :: Maybe Text + , dRepQualifications :: Maybe Text , dRepImageUrl :: Maybe Text , dRepImageHash :: Maybe HexText + , dRepIdentityReferences :: Maybe DRepReferences + , dRepLinkReferences :: Maybe DRepReferences } deriving (Generic, Show) diff --git a/govtool/backend/src/VVA/DRep.hs b/govtool/backend/src/VVA/DRep.hs index 49482b1e9..f8c9f8737 100644 --- a/govtool/backend/src/VVA/DRep.hs +++ b/govtool/backend/src/VVA/DRep.hs @@ -11,41 +11,74 @@ import Control.Monad.Reader import Crypto.Hash +import Data.Aeson (Value) import Data.ByteString (ByteString) -import qualified Data.ByteString.Base16 as Base16 -import qualified Data.ByteString.Char8 as C +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 qualified Data.Map as M import Data.Maybe (fromMaybe, isJust, isNothing) import Data.Scientific import Data.String (fromString) import Data.Text (Text, pack, unpack, intercalate) -import qualified Data.Text.Encoding as Text +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 Database.PostgreSQL.Simple.FromRow import VVA.Config import VVA.Pool (ConnectionPool, withPool) -import qualified VVA.Proposal as Proposal +import qualified VVA.Proposal as Proposal import VVA.Types (AppError, DRepInfo (..), DRepRegistration (..), DRepStatus (..), DRepType (..), Proposal (..), Vote (..), DRepVotingPowerList (..)) +data DRepQueryResult = DRepQueryResult + { queryDrepHash :: Text + , queryDrepView :: Text + , queryIsScriptBased :: Bool + , queryUrl :: Maybe Text + , queryDataHash :: Maybe Text + , queryDeposit :: Scientific + , queryVotingPower :: Maybe Integer + , queryIsActive :: Bool + , queryTxHash :: Maybe Text + , queryDate :: LocalTime + , queryLatestDeposit :: Scientific + , queryLatestNonDeregisterVotingAnchorWasNotNull :: Bool + , queryMetadataError :: Maybe Text + , queryPaymentAddress :: Maybe Text + , queryGivenName :: Maybe Text + , queryObjectives :: Maybe Text + , queryMotivations :: Maybe Text + , queryQualifications :: Maybe Text + , queryImageUrl :: Maybe Text + , queryImageHash :: Maybe Text + , queryIdentityReferences :: Maybe Value + , queryLinkReferences :: Maybe Value + } deriving (Show) + +instance FromRow DRepQueryResult where + fromRow = DRepQueryResult + <$> field <*> field <*> field <*> field <*> field <*> field + <*> field <*> field <*> field <*> field <*> field <*> field + <*> field <*> field <*> field <*> field <*> field <*> field + <*> field <*> field <*> field <*> field + sqlFrom :: ByteString -> SQL.Query sqlFrom bs = fromString $ unpack $ Text.decodeUtf8 bs listDRepsSql :: SQL.Query listDRepsSql = sqlFrom $(embedFile "sql/list-dreps.sql") - listDReps :: (Has ConnectionPool r, Has VVAConfig r, MonadReader r m, MonadIO m) => Maybe Text -> m [DRepRegistration] listDReps mSearchQuery = withPool $ \conn -> do let searchParam = fromMaybe "" mSearchQuery - results <- liftIO $ SQL.query conn listDRepsSql + results <- liftIO (SQL.query conn listDRepsSql ( searchParam -- COALESCE(?, '') , searchParam -- LENGTH(?) , searchParam -- AND ? @@ -56,44 +89,45 @@ listDReps mSearchQuery = withPool $ \conn -> do , "%" <> searchParam <> "%" -- objectives , "%" <> searchParam <> "%" -- motivations , "%" <> searchParam <> "%" -- qualifications - ) + ) :: IO [DRepQueryResult]) timeZone <- liftIO getCurrentTimeZone return - [ DRepRegistration drepHash drepView isScriptBased url dataHash (floor @Scientific deposit) votingPower status drepType txHash (localTimeToUTC timeZone date) metadataError paymentAddress givenName objectives motivations qualifications imageUrl imageHash - | ( drepHash - , drepView - , isScriptBased - , url - , dataHash - , deposit - , votingPower - , isActive - , txHash - , date - , latestDeposit - , latestNonDeregisterVotingAnchorWasNotNull - , metadataError - , paymentAddress - , givenName - , objectives - , motivations - , qualifications - , imageUrl - , imageHash - ) <- results - , let status = case (isActive, deposit) of + [ DRepRegistration + (queryDrepHash result) + (queryDrepView result) + (queryIsScriptBased result) + (queryUrl result) + (queryDataHash result) + (floor @Scientific $ queryDeposit result) + (queryVotingPower result) + status + drepType + (queryTxHash result) + (localTimeToUTC timeZone $ queryDate result) + (queryMetadataError result) + (queryPaymentAddress result) + (queryGivenName result) + (queryObjectives result) + (queryMotivations result) + (queryQualifications result) + (queryImageUrl result) + (queryImageHash result) + (queryIdentityReferences result) + (queryLinkReferences result) + | result <- results + , let status = case (queryIsActive result, queryDeposit result) of (_, d) | d < 0 -> Retired (isActive, d) | d >= 0 && isActive -> Active | d >= 0 && not isActive -> Inactive - , let latestDeposit' = floor @Scientific latestDeposit :: Integer - , let drepType | latestDeposit' >= 0 && isNothing url = SoleVoter - | latestDeposit' >= 0 && isJust url = DRep - | latestDeposit' < 0 && not latestNonDeregisterVotingAnchorWasNotNull = SoleVoter - | latestDeposit' < 0 && latestNonDeregisterVotingAnchorWasNotNull = DRep - | Data.Maybe.isJust url = DRep + , let latestDeposit' = floor @Scientific (queryLatestDeposit result) :: Integer + , let drepType | latestDeposit' >= 0 && isNothing (queryUrl result) = SoleVoter + | latestDeposit' >= 0 && isJust (queryUrl result) = DRep + | latestDeposit' < 0 && not (queryLatestNonDeregisterVotingAnchorWasNotNull result) = SoleVoter + | latestDeposit' < 0 && queryLatestNonDeregisterVotingAnchorWasNotNull result = DRep + | Data.Maybe.isJust (queryUrl result) = DRep ] - + getVotingPowerSql :: SQL.Query getVotingPowerSql = sqlFrom $(embedFile "sql/get-voting-power.sql") diff --git a/govtool/backend/src/VVA/Types.hs b/govtool/backend/src/VVA/Types.hs index e6e674925..154d0619f 100644 --- a/govtool/backend/src/VVA/Types.hs +++ b/govtool/backend/src/VVA/Types.hs @@ -5,26 +5,28 @@ {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE TypeApplications #-} +{-# LANGUAGE ScopedTypeVariables #-} module VVA.Types where import Control.Concurrent.QSem import Control.Exception -import Control.Monad.Except (MonadError) -import Control.Monad.Fail (MonadFail) -import Control.Monad.IO.Class (MonadIO) -import Control.Monad.Reader (MonadReader) +import Control.Monad.Except (MonadError) +import Control.Monad.Fail (MonadFail) +import Control.Monad.IO.Class (MonadIO) +import Control.Monad.Reader (MonadReader) -import Data.Aeson (Value, ToJSON (..), object, (.=)) -import qualified Data.Cache as Cache +import Data.Aeson (Value, ToJSON (..), object, (.=)) +import qualified Data.Cache as Cache import Data.Has -import Data.Pool (Pool) -import Data.Text (Text) -import Data.Time (UTCTime, LocalTime) +import Data.Pool (Pool) +import Data.Text (Text) +import Data.Time (UTCTime, LocalTime) import Data.Scientific -import Database.PostgreSQL.Simple (Connection) +import Database.PostgreSQL.Simple (Connection) import Database.PostgreSQL.Simple.FromRow +import Database.PostgreSQL.Simple.FromField (FromField(..), returnError, ResultError(ConversionFailed)) import VVA.Cache import VVA.Config @@ -107,8 +109,25 @@ data DRepVotingPowerList data DRepStatus = Active | Inactive | Retired deriving (Show, Eq, Ord) +instance FromField DRepStatus where + fromField f mdata = do + (value :: Text) <- fromField f mdata + case value of + "Active" -> return Active + "Inactive" -> return Inactive + "Retired" -> return Retired + _ -> returnError ConversionFailed f "Invalid DRepStatus" + data DRepType = DRep | SoleVoter deriving (Show, Eq) +instance FromField DRepType where + fromField f mdata = do + (value :: Text) <- fromField f mdata + case value of + "DRep" -> return DRep + "SoleVoter" -> return SoleVoter + _ -> returnError ConversionFailed f "Invalid DRepType" + data DRepRegistration = DRepRegistration { dRepRegistrationDRepHash :: Text @@ -127,12 +146,39 @@ data DRepRegistration , dRepRegistrationGivenName :: Maybe Text , dRepRegistrationObjectives :: Maybe Text , dRepRegistrationMotivations :: Maybe Text - , dRepRegistrationQualifications :: Maybe Text + , dRepRegistrationQualifications :: Maybe Text , dRepRegistrationImageUrl :: Maybe Text , dRepRegistrationImageHash :: Maybe Text + , dRepRegistrationIdentityReferences :: Maybe Value + , dRepRegistrationLinkReferences :: Maybe Value } deriving (Show) +instance FromRow DRepRegistration where + fromRow = + DRepRegistration + <$> field -- dRepRegistrationDRepHash + <*> field -- dRepRegistrationView + <*> field -- dRepRegistrationIsScriptBased + <*> field -- dRepRegistrationUrl + <*> field -- dRepRegistrationDataHash + <*> (floor @Scientific <$> field) -- dRepRegistrationDeposit + <*> field -- dRepRegistrationVotingPower + <*> field -- dRepRegistrationStatus + <*> field -- dRepRegistrationType + <*> field -- dRepRegistrationLatestTxHash + <*> field -- dRepRegistrationLatestRegistrationDate + <*> field -- dRepRegistrationMetadataError + <*> field -- dRepRegistrationPaymentAddress + <*> field -- dRepRegistrationGivenName + <*> field -- dRepRegistrationObjectives + <*> field -- dRepRegistrationMotivations + <*> field -- dRepRegistrationQualifications + <*> field -- dRepRegistrationImageUrl + <*> field -- dRepRegistrationImageHash + <*> field -- dRepRegistrationIdentityReferences + <*> field -- dRepRegistrationLinkReferences + data Proposal = Proposal { proposalId :: Integer diff --git a/govtool/frontend/src/components/organisms/DRepDetailsCard.tsx b/govtool/frontend/src/components/organisms/DRepDetailsCard.tsx index 82b560c44..8d692e788 100644 --- a/govtool/frontend/src/components/organisms/DRepDetailsCard.tsx +++ b/govtool/frontend/src/components/organisms/DRepDetailsCard.tsx @@ -41,7 +41,8 @@ export const DRepDetailsCard = ({ objectives, paymentAddress, qualifications, - references, + identityReferences, + linkReferences, status, url, view, @@ -75,21 +76,6 @@ export const DRepDetailsCard = ({ validate(); }, [url]); - const groupedReferences = references?.reduce>( - (acc, reference) => { - const type = reference["@type"]; - if (!acc[type]) { - acc[type] = []; - } - acc[type].push(reference); - return acc; - }, - {}, - ); - - const linkReferences = groupedReferences?.Link; - const identityReferences = groupedReferences?.Identity; - return ( { if (loadUserData) { const data: DRepData = state ?? yourselfDRep; - const groupedReferences = data?.references?.reduce< - Record - >((acc, reference) => { - const type = reference["@type"]; - if (!acc[type]) { - acc[type] = []; - } - acc[type].push(reference); - return acc; - }, {}); + reset({ ...data, objectives: data?.objectives ?? "", @@ -58,8 +49,8 @@ export const EditDRepForm = ({ qualifications: data?.qualifications ?? "", paymentAddress: data?.paymentAddress ?? "", image: data?.image ?? "", - linkReferences: groupedReferences?.Link ?? [getEmptyReference("Link")], - identityReferences: groupedReferences?.Identity ?? [ + linkReferences: data.linkReferences ?? [getEmptyReference("Link")], + identityReferences: data.identityReferences ?? [ getEmptyReference("Identity"), ], }); diff --git a/govtool/frontend/src/models/api.ts b/govtool/frontend/src/models/api.ts index bc374ccad..6ef89f3df 100644 --- a/govtool/frontend/src/models/api.ts +++ b/govtool/frontend/src/models/api.ts @@ -150,6 +150,12 @@ export enum DRepListSort { Status = "Status", } +type Reference = { + "@type": "Identity" | "Links"; + label: string; + uri: string; +}; + export type DrepDataDTO = { deposit: number; drepId: string; @@ -163,6 +169,8 @@ export type DrepDataDTO = { view: string; votingPower?: number; imageUrl: string | null; + identityReferences: Reference[]; + linkReferences: Reference[]; // either base64 for IPFS image or URL for regular image image: string | null; }; @@ -173,7 +181,6 @@ export type DRepData = DrepDataDTO & { objectives: string | null; motivations: string | null; qualifications: string | null; - references: Reference[]; doNotList: boolean; imageUrl: string | null; // either base64 for IPFS image or URL for regular image diff --git a/govtool/frontend/src/stories/DRepDetailsCard.stories.ts b/govtool/frontend/src/stories/DRepDetailsCard.stories.ts index 17754f8c9..71625f098 100644 --- a/govtool/frontend/src/stories/DRepDetailsCard.stories.ts +++ b/govtool/frontend/src/stories/DRepDetailsCard.stories.ts @@ -28,29 +28,31 @@ const meta = { "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras semper tortor ullamcorper volutpat vehicula. Duis varius orci a elit luctus, in fringilla nisl fringilla. Fusce pellentesque convallis dapibus. In hac habitasse platea dictumst. Nunc efficitur ipsum at ipsum blandit, ac eleifend purus pulvinar. Pellentesque orci quam, interdum eget massa id, sollicitudin lacinia turpis. Nullam lectus quam, congue commodo sollicitudin in, pretium sit amet metus. Integer pretium, odio eu dictum posuere.", qualifications: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc porta iaculis sodales. Praesent non nisi fermentum, porta sem in, porta arcu. In dignissim pulvinar est eu dignissim. Duis vitae vehicula dui. Praesent posuere egestas lacus, at pulvinar elit tempus ut. Etiam vulputate, lorem in accumsan.", - references: [ + linkReferences: [ { - "@type": "Link", + "@type": "Links", label: "Link Reference", uri: "https://example.com/", }, { - "@type": "Link", + "@type": "Links", label: "Another Link Reference", uri: "https://example.com/", }, + ], + identityReferences: [ { "@type": "Identity", label: "Identity Reference", uri: "https://example.com/", }, { - "@type": "GovernanceMetadata", + "@type": "Identity", label: "GovernanceMetadata Reference", uri: "https://example.com/", }, { - "@type": "Other", + "@type": "Identity", label: "Other Reference", uri: "https://example.com/", }, diff --git a/govtool/frontend/src/stories/GovernanceActionDetailsCard.stories.ts b/govtool/frontend/src/stories/GovernanceActionDetailsCard.stories.ts index 67e6001f7..7d1b14eab 100644 --- a/govtool/frontend/src/stories/GovernanceActionDetailsCard.stories.ts +++ b/govtool/frontend/src/stories/GovernanceActionDetailsCard.stories.ts @@ -59,7 +59,7 @@ const commonArgs = { metadataHash: "exampleMetadataHash", references: [ { - "@type": "Reference", + "@type": "Links", uri: "https://exampleurl.com", label: "Example label", }, diff --git a/govtool/frontend/src/utils/tests/dRep.test.ts b/govtool/frontend/src/utils/tests/dRep.test.ts index 00a06a4f2..f35f7cbd0 100644 --- a/govtool/frontend/src/utils/tests/dRep.test.ts +++ b/govtool/frontend/src/utils/tests/dRep.test.ts @@ -14,7 +14,8 @@ const EXAMPLE_DREP: DRepData = { status: DRepStatus.Active, type: "DRep" as TDRepType, givenName: "name", - references: [], + identityReferences: [], + linkReferences: [], latestRegistrationDate: "2024-07-10", paymentAddress: null, objectives: null,