From ddf233e4aee63f60387b5d5fc63cd46c3b5ea572 Mon Sep 17 00:00:00 2001 From: Sudip Bhattarai Date: Fri, 25 Jul 2025 12:32:07 +0545 Subject: [PATCH 01/39] Add Pinata ipfs upload API --- govtool/backend/example-config.json | 1 + govtool/backend/src/VVA/API.hs | 27 ++++++++-- govtool/backend/src/VVA/API/Types.hs | 19 +++++++ govtool/backend/src/VVA/Config.hs | 19 ++++--- govtool/backend/src/VVA/Ipfs.hs | 77 ++++++++++++++++++++++++++++ govtool/backend/stack.yaml | 1 + govtool/backend/stack.yaml.lock | 9 +++- govtool/backend/vva-be.cabal | 9 ++-- 8 files changed, 149 insertions(+), 13 deletions(-) create mode 100644 govtool/backend/src/VVA/Ipfs.hs diff --git a/govtool/backend/example-config.json b/govtool/backend/example-config.json index fe6a47420..800f7dde1 100644 --- a/govtool/backend/example-config.json +++ b/govtool/backend/example-config.json @@ -6,6 +6,7 @@ "password" : "postgres", "port" : 5432 }, + "pinataapijwt": "", "port" : 9999, "host" : "localhost", "cachedurationseconds": 20, diff --git a/govtool/backend/src/VVA/API.hs b/govtool/backend/src/VVA/API.hs index ca7660358..c17f25d7b 100644 --- a/govtool/backend/src/VVA/API.hs +++ b/govtool/backend/src/VVA/API.hs @@ -13,7 +13,7 @@ import Control.Exception (throw, throwIO) import Control.Monad.Except (runExceptT, throwError) import Control.Monad.Reader -import Data.Aeson (Value(..), Array, decode, encode, ToJSON, toJSON) +import Data.Aeson (Value(..), Array, decode, ToJSON, toJSON) import Data.Bool (Bool) import Data.List (sortOn, sort) import qualified Data.Map as Map @@ -21,6 +21,8 @@ import Data.Maybe (Maybe (Nothing), catMaybes, fromMaybe import Data.Ord (Down (..)) import Data.Text hiding (any, drop, elem, filter, length, map, null, take) import qualified Data.Text as Text +import qualified Data.Text.Lazy as TL +import qualified Data.Text.Lazy.Encoding as TL import qualified Data.Vector as V import Data.Time.LocalTime (TimeZone, getCurrentTimeZone) @@ -48,9 +50,14 @@ import VVA.Types (App, AppEnv (..), AppError (CriticalError, InternalError, ValidationError), CacheEnv (..)) import Data.Time (TimeZone, localTimeToUTC) +import qualified VVA.Ipfs as Ipfs +import Data.ByteString.Lazy (ByteString) +import qualified Data.ByteString.Lazy as BSL type VVAApi = - "drep" :> "list" + "ipfs" + :> "upload" :> QueryParam "fileName" Text :> ReqBody '[PlainText] Text :> Post '[JSON] UploadResponse + :<|> "drep" :> "list" :> QueryParam "search" Text :> QueryParams "status" DRepStatus :> QueryParam "sort" DRepSortMode @@ -89,7 +96,8 @@ type VVAApi = :<|> "account" :> Capture "stakeKey" HexText :> Get '[JSON] GetAccountInfoResponse server :: App m => ServerT VVAApi m -server = drepList +server = upload + :<|> drepList :<|> getVotingPower :<|> getVotes :<|> drepInfo @@ -107,6 +115,19 @@ server = drepList :<|> getNetworkTotalStake :<|> getAccountInfo +upload :: App m => Maybe Text -> Text -> m UploadResponse +upload mFileName fileContentText = do + AppEnv {vvaConfig} <- ask + let fileContent = TL.encodeUtf8 $ TL.fromStrict fileContentText + vvaPinataJwt = pinataApiJwt vvaConfig + fileName = fromMaybe "data.txt" mFileName -- Default to data.txt if no filename is provided + when (BSL.length fileContent > 1024 * 512) $ + throwError $ ValidationError "The uploaded file is larger than 500Kb" + eIpfsHash <- liftIO $ Ipfs.ipfsUpload vvaPinataJwt fileName fileContent + case eIpfsHash of + Left err -> throwError $ InternalError $ "IPFS upload failed: " <> pack err + Right ipfsHash -> return $ UploadResponse ipfsHash + mapDRepType :: Types.DRepType -> DRepType mapDRepType Types.DRep = NormalDRep mapDRepType Types.SoleVoter = SoleVoter diff --git a/govtool/backend/src/VVA/API/Types.hs b/govtool/backend/src/VVA/API/Types.hs index d14fd54cb..360f9be64 100644 --- a/govtool/backend/src/VVA/API/Types.hs +++ b/govtool/backend/src/VVA/API/Types.hs @@ -1112,6 +1112,14 @@ data GetAccountInfoResponse } deriving (Generic, Show) deriveJSON (jsonOptions "getAccountInfoResponse") ''GetAccountInfoResponse + +data UploadResponse + = UploadResponse + { uploadResponseIpfsCid :: Text + } + deriving (Generic, Show) +deriveJSON (jsonOptions "uploadResponse") ''UploadResponse + exampleGetAccountInfoResponse :: Text exampleGetAccountInfoResponse = "{\"stakeKey\": \"stake1u9\"," @@ -1125,3 +1133,14 @@ instance ToSchema GetAccountInfoResponse where & description ?~ "GetAccountInfoResponse" & example ?~ toJSON exampleGetAccountInfoResponse + +exampleUploadResponse :: Text +exampleUploadResponse = + "{\"ipfsHash\": \"QmZKLGf2D3Z3F2J2K5J2L5J2L5J2L5J2L5J2L5J2L5J2L5\"}" + +instance ToSchema UploadResponse where + declareNamedSchema _ = pure $ NamedSchema (Just "UploadResponse") $ mempty + & type_ ?~ OpenApiObject + & description ?~ "UploadResponse" + & example + ?~ toJSON exampleUploadResponse diff --git a/govtool/backend/src/VVA/Config.hs b/govtool/backend/src/VVA/Config.hs index cea9e3eb8..49151d630 100644 --- a/govtool/backend/src/VVA/Config.hs +++ b/govtool/backend/src/VVA/Config.hs @@ -32,7 +32,7 @@ import qualified Conferer.Source.Env as Env import Control.Monad.Reader -import Data.Aeson +import Data.Aeson as Aeson import qualified Data.Aeson.Encode.Pretty as AP import Data.ByteString (ByteString, toStrict) import Data.Has (Has, getter) @@ -58,7 +58,7 @@ data DBConfig -- | Port , dBConfigPort :: Int } - deriving (FromConfig, Generic, Show) + deriving (FromConfig, FromJSON, Generic, Show) instance DefaultConfig DBConfig where configDef = DBConfig "localhost" "cexplorer" "postgres" "test" 9903 @@ -79,9 +79,12 @@ data VVAConfigInternal , vVAConfigInternalSentrydsn :: String -- | Sentry environment , vVAConfigInternalSentryEnv :: String + -- | Pinata API JWT + , vVAConfigInternalPinataApiJwt :: Maybe Text } deriving (FromConfig, Generic, Show) + instance DefaultConfig VVAConfigInternal where configDef = VVAConfigInternal @@ -90,7 +93,8 @@ instance DefaultConfig VVAConfigInternal where vVAConfigInternalHost = "localhost", vVaConfigInternalCacheDurationSeconds = 20, vVAConfigInternalSentrydsn = "https://username:password@senty.host/id", - vVAConfigInternalSentryEnv = "development" + vVAConfigInternalSentryEnv = "development", + vVAConfigInternalPinataApiJwt = Nothing } -- | DEX configuration. @@ -108,6 +112,8 @@ data VVAConfig , sentryDSN :: String -- | Sentry environment , sentryEnv :: String + -- | Pinata API JWT + , pinataApiJwt :: Maybe Text } deriving (Generic, Show, ToJSON) @@ -148,7 +154,8 @@ convertConfig VVAConfigInternal {..} = serverHost = vVAConfigInternalHost, cacheDurationSeconds = vVaConfigInternalCacheDurationSeconds, sentryDSN = vVAConfigInternalSentrydsn, - sentryEnv = vVAConfigInternalSentryEnv + sentryEnv = vVAConfigInternalSentryEnv, + pinataApiJwt = vVAConfigInternalPinataApiJwt } -- | Load configuration from a file specified on the command line. Load from @@ -163,7 +170,7 @@ loadVVAConfig configFile = do where buildConfig :: IO Config buildConfig = - Conferer.mkConfig' + mkConfig' [] [ Env.fromConfig "vva", JSON.fromFilePath (fromMaybe "example-config.json" configFile) @@ -185,4 +192,4 @@ getServerPort = asks (serverPort . getter) getServerHost :: (Has VVAConfig r, MonadReader r m) => m Text -getServerHost = asks (serverHost . getter) \ No newline at end of file +getServerHost = asks (serverHost . getter) diff --git a/govtool/backend/src/VVA/Ipfs.hs b/govtool/backend/src/VVA/Ipfs.hs new file mode 100644 index 000000000..2f99414f0 --- /dev/null +++ b/govtool/backend/src/VVA/Ipfs.hs @@ -0,0 +1,77 @@ +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} + +module VVA.Ipfs (ipfsUpload) where + +import Control.Exception (SomeException, try) +import Control.Monad.IO.Class (liftIO) +import Data.Aeson (FromJSON(parseJSON), withObject, (.:), eitherDecode) +import qualified Data.ByteString.Lazy as LBS +import Data.Text (Text) +import qualified Data.Text.Encoding as TE +import GHC.Generics (Generic) +import Network.HTTP.Client (newManager, parseRequest, httpLbs, method, requestHeaders, RequestBody(..), Request, responseBody, responseStatus) +import Network.HTTP.Client.TLS (tlsManagerSettings) +import Network.HTTP.Client.MultipartFormData (formDataBody, partBS, partFileRequestBody) +import Network.HTTP.Types.Status (statusIsSuccessful) +import qualified Data.ByteString.Lazy.Char8 as LBS8 +import qualified Data.Text.Lazy as TL +import qualified Data.Text.Lazy.Encoding as TL +import qualified Data.Text as T + + +data PinataData = PinataData + { cid :: Text + , size :: Int + , created_at :: Text + , isDuplicate :: Maybe Bool + } deriving (Show, Generic) + +instance FromJSON PinataData + +data PinataSuccessResponse = PinataSuccessResponse + { pinataData :: PinataData + } deriving (Show) + +instance FromJSON PinataSuccessResponse where + parseJSON = withObject "PinataSuccessResponse" $ \v -> PinataSuccessResponse + <$> v .: "data" + +ipfsUpload :: Maybe Text -> Text -> LBS.ByteString -> IO (Either String Text) +ipfsUpload maybeJwt fileName fileContent = + case maybeJwt of + Nothing -> pure $ Left "Backend is not configured to support ipfs upload" + Just "" -> pure $ Left "Backend is not configured to support ipfs upload" + Just jwt -> do + manager <- newManager tlsManagerSettings + initialRequest <- parseRequest "https://uploads.pinata.cloud/v3/files" + let req = initialRequest + { method = "POST" + , requestHeaders = [("Authorization", "Bearer " <> TE.encodeUtf8 jwt)] + } + result <- try $ flip httpLbs manager =<< formDataBody + [ partBS "network" "public" + , partFileRequestBody "file" (T.unpack fileName) $ RequestBodyLBS fileContent + ] + req + + case result of + Left (e :: SomeException) -> do + let errMsg = show e + liftIO $ putStrLn errMsg + pure $ Left errMsg + Right response -> do + let body = responseBody response + let status = responseStatus response + if statusIsSuccessful status + then case eitherDecode body of + Left err -> do + let errMsg = "Failed to decode Pinata API reponse: " <> err <> "\nResponse body: " <> LBS8.unpack body + liftIO $ putStrLn errMsg + pure $ Left errMsg + Right (res :: PinataSuccessResponse) -> pure $ Right $ cid $ pinataData res + else do + let errMsg = "Pinata API request failed with status: " <> show status <> "\nResponse body: " <> LBS8.unpack body + liftIO $ putStrLn errMsg + pure $ Left errMsg diff --git a/govtool/backend/stack.yaml b/govtool/backend/stack.yaml index b6238c401..8de57aa42 100644 --- a/govtool/backend/stack.yaml +++ b/govtool/backend/stack.yaml @@ -4,3 +4,4 @@ packages: extra-deps: - raven-haskell-0.1.4.1@sha256:9187272adc064197528645b5ad9b89163b668f386f34016d97fa646d5c790784 +- http-client-multipart-0.3.0.0@sha256:d675f10cba69c98233467dd533ba46e64f34798fc2ea528efe662ad2ea6c89bf,554 \ No newline at end of file diff --git a/govtool/backend/stack.yaml.lock b/govtool/backend/stack.yaml.lock index 985ab39a4..d03add71d 100644 --- a/govtool/backend/stack.yaml.lock +++ b/govtool/backend/stack.yaml.lock @@ -1,7 +1,7 @@ # This file was autogenerated by Stack. # You should not edit this file by hand. # For more information, please see the documentation at: -# https://docs.haskellstack.org/en/stable/lock_files +# https://docs.haskellstack.org/en/stable/topics/lock_files packages: - completed: @@ -11,6 +11,13 @@ packages: size: 632 original: hackage: raven-haskell-0.1.4.1@sha256:9187272adc064197528645b5ad9b89163b668f386f34016d97fa646d5c790784 +- completed: + hackage: http-client-multipart-0.3.0.0@sha256:d675f10cba69c98233467dd533ba46e64f34798fc2ea528efe662ad2ea6c89bf,554 + pantry-tree: + sha256: a35e249bf5a162c18e5fa2309c5cfcdaaead1d8fc914be029f3f1239102bd648 + size: 164 + original: + hackage: http-client-multipart-0.3.0.0@sha256:d675f10cba69c98233467dd533ba46e64f34798fc2ea528efe662ad2ea6c89bf,554 snapshots: - completed: sha256: e019cd29e3f7f9dbad500225829a3f7a50f73c674614f2f452e21bb8bf5d99ea diff --git a/govtool/backend/vva-be.cabal b/govtool/backend/vva-be.cabal index b644dad9a..37cbf1062 100644 --- a/govtool/backend/vva-be.cabal +++ b/govtool/backend/vva-be.cabal @@ -45,8 +45,8 @@ executable vva-be -- other-modules: -- LANGUAGE extensions used by modules in this package. - -- other-extensions: - build-depends: base >=4.16 && <4.18 + -- other-extensions9 + build-depends: base >=4.16 && <4.19 , vva-be , optparse-applicative , text @@ -80,7 +80,7 @@ executable vva-be library hs-source-dirs: src - build-depends: base >=4.16 && <4.18 + build-depends: base >=4.16 && <4.19 , servant-server , conferer , mtl @@ -107,9 +107,11 @@ library , swagger2 , http-client , http-client-tls + , http-client-multipart , vector , async , random + , http-types exposed-modules: VVA.Config , VVA.CommandLine @@ -126,4 +128,5 @@ library , VVA.Types , VVA.Network , VVA.Account + , VVA.Ipfs ghc-options: -threaded From 01af374c963180f66555df65bdf054242ae69c3d Mon Sep 17 00:00:00 2001 From: Sudip Bhattarai Date: Fri, 25 Jul 2025 12:41:17 +0545 Subject: [PATCH 02/39] Add dummy flow for saving metadata with govtool --- .../VoteContext/VoteContextChoice.tsx | 68 ++++++++++ .../VoteContext/VoteContextGovTool.tsx | 93 +++++++++++++ .../VoteContext/VoteContextModal.tsx | 123 +++++++++++++++--- .../VoteContext/VoteContextTerms.tsx | 2 +- .../VoteContext/VoteContextWrapper.tsx | 45 ++++--- .../src/components/organisms/index.ts | 2 + .../src/hooks/forms/useVoteContextForm.tsx | 3 +- .../frontend/src/services/requests/index.ts | 1 + .../src/services/requests/postIpfs.ts | 10 ++ 9 files changed, 309 insertions(+), 38 deletions(-) create mode 100644 govtool/frontend/src/components/organisms/VoteContext/VoteContextChoice.tsx create mode 100644 govtool/frontend/src/components/organisms/VoteContext/VoteContextGovTool.tsx create mode 100644 govtool/frontend/src/services/requests/postIpfs.ts diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextChoice.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextChoice.tsx new file mode 100644 index 000000000..9ad563265 --- /dev/null +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextChoice.tsx @@ -0,0 +1,68 @@ +import { Dispatch, SetStateAction } from "react"; +import { Box, Button } from "@mui/material"; + +import { Spacer, Typography } from "@atoms"; +import { useScreenDimension, useTranslation } from "@hooks"; +import { VoteContextWrapper } from "@organisms"; +import { NodeObject } from "jsonld"; + +type VoteContextChoiceProps = { + setStep: Dispatch>; + setStoreDataYourself: Dispatch>; + setJsonldContent: Dispatch>; + setMetadataHash: Dispatch>; + generateMetadata: () => Promise<{ jsonld: NodeObject; jsonHash: string }>; + onCancel: () => void; +}; + +export const VoteContextChoice = ({ + setStep, + setStoreDataYourself, + setJsonldContent, + setMetadataHash, + generateMetadata, + onCancel, +}: VoteContextChoiceProps) => { + const { t } = useTranslation(); + const { isMobile } = useScreenDimension(); + + const handleStoreItMyself = () => { + setStoreDataYourself(true); + setStep(3); + }; + + const handleLetGovToolStore = async () => { + setStoreDataYourself(false); + const { jsonld, jsonHash } = await generateMetadata(); + setJsonldContent(jsonld); + setMetadataHash(jsonHash); + setStep(3); + }; + + return ( + + + {t("createGovernanceAction.storeDataTitle")} + + + + + + + + + + ); +}; diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextGovTool.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextGovTool.tsx new file mode 100644 index 000000000..e79dde8f8 --- /dev/null +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextGovTool.tsx @@ -0,0 +1,93 @@ +import { useEffect, Dispatch, SetStateAction, useState } from "react"; +import { Box, Button, CircularProgress, Link, Typography } from "@mui/material"; +import { useMutation } from "react-query"; + +import { Spacer } from "@atoms"; +import { useTranslation } from "@hooks"; +import { VoteContextWrapper } from "@organisms"; +import { postIpfs } from "@services"; +import { downloadTextFile } from "@utils"; +import { NodeObject } from "jsonld"; +import { UseFormSetValue } from "react-hook-form"; +import { VoteContextFormValues } from "@hooks"; + +interface PostIpfsResponse { + ipfsHash: string; +} + +type VoteContextGovToolProps = { + setStep: Dispatch>; + setSavedHash: Dispatch>; + onCancel: () => void; + submitVoteContext: () => void; + jsonldContent: NodeObject | null; + metadataHash: string | null; + setValue: UseFormSetValue; +}; + +export const VoteContextGovTool = ({ + setStep, + setSavedHash, + onCancel, + submitVoteContext, + jsonldContent, + metadataHash, + setValue, +}: VoteContextGovToolProps) => { + const [apiResponse, setApiResponse] = useState(null); + const { t } = useTranslation(); + + const { mutate, isLoading } = useMutation({ + mutationFn: postIpfs, + onSuccess: (data) => { + const ipfsUrl = `ipfs://${data.ipfsHash}`; + setValue("storingURL", ipfsUrl); + setSavedHash(metadataHash); // Set savedHash to metadataHash + setApiResponse(JSON.stringify(data, null, 2)); + }, + }); + + useEffect(() => { + if (jsonldContent) { + mutate({ content: JSON.stringify(jsonldContent, null, 2) }); + } + }, [jsonldContent, mutate]); + + const handleDownload = () => { + if (jsonldContent) { + downloadTextFile(JSON.stringify(jsonldContent, null, 2), "voteContext.jsonld"); + } + }; + + return ( + + + {t("createGovernanceAction.letGovToolStore")} + + + {isLoading ? ( + + + + ) : apiResponse ? ( + <> + + {apiResponse} + + + + {t("createGovernanceAction.downloadJsonLd")} + + + ) : ( + + {t("createGovernanceAction.uploadingToIPFS")} + + )} + + ); +}; diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextModal.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextModal.tsx index 4ceca049a..878a19d42 100644 --- a/govtool/frontend/src/components/organisms/VoteContext/VoteContextModal.tsx +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextModal.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { Dispatch, SetStateAction, useState } from "react"; import { useForm, FormProvider } from "react-hook-form"; import { ModalWrapper } from "@atoms"; @@ -8,8 +8,12 @@ import { VoteContextCheckResult, VoteContextTerms, VoteContextText, + VoteContextChoice, + VoteContextGovTool, } from "@organisms"; -import { VoteContextFormValues } from "@hooks"; +import { NodeObject } from "jsonld"; +import { VoteContextFormValues, useVoteContextForm } from "@hooks"; +import { UseFormReturn } from "react-hook-form"; export type VoteContextModalState = { onSubmit: (url: string, hash: string | null, voteContextText: string) => void; @@ -17,10 +21,13 @@ export type VoteContextModalState = { export const VoteContextModal = () => { const [step, setStep] = useState(1); + const [storeDataYourself, setStoreDataYourself] = useState(true); const [savedHash, setSavedHash] = useState(""); const [errorMessage, setErrorMessage] = useState( undefined, ); + const [jsonldContent, setJsonldContent] = useState(null); + const [metadataHash, setMetadataHash] = useState(null); const { state, closeModal } = useModal(); @@ -47,29 +54,109 @@ export const VoteContextModal = () => { }} > - {step === 1 && ( - - )} - {step === 2 && ( - - )} - {step === 3 && ( - - )} - {step === 4 && ( - )} + {step === 1 && ( + + )} ); }; + +// New component to encapsulate the flow that uses useVoteContextForm +const VoteContextFlow = ({ + step, + setStep, + storeDataYourself, + setStoreDataYourself, + setSavedHash, + setErrorMessage, + jsonldContent, + setJsonldContent, + metadataHash, + setMetadataHash, + submitVoteContext, + onCancel, + errorMessage, + methods, // Accept methods +}: { + step: number; + setStep: Dispatch>; + storeDataYourself: boolean; + setStoreDataYourself: Dispatch>; + setSavedHash: Dispatch>; + setErrorMessage: Dispatch>; + jsonldContent: NodeObject | null; + setJsonldContent: Dispatch>; + metadataHash: string | null; + setMetadataHash: Dispatch>; + submitVoteContext: () => void; + onCancel: () => void; + errorMessage: string | undefined; + methods: UseFormReturn; // Type for methods +}) => { + const { generateMetadata } = useVoteContextForm(); + + return ( + <> + {step === 2 && ( + + )} + {step === 3 && storeDataYourself && ( + + )} + {step === 3 && !storeDataYourself && ( + + )} + {step === 4 && ( + + )} + {step === 5 && ( + + )} + + ); +}; diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextTerms.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextTerms.tsx index d59d1af38..ce92076d3 100644 --- a/govtool/frontend/src/components/organisms/VoteContext/VoteContextTerms.tsx +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextTerms.tsx @@ -23,7 +23,7 @@ export const VoteContextTerms = ({ setStep, onCancel }: StoreDataInfoProps) => { return ( setStep(3)} + onContinue={() => setStep(4)} isContinueDisabled={isContinueDisabled} onCancel={onCancel} > diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextWrapper.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextWrapper.tsx index 91ceb8ed5..97a421a73 100644 --- a/govtool/frontend/src/components/organisms/VoteContext/VoteContextWrapper.tsx +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextWrapper.tsx @@ -5,14 +5,21 @@ import { useScreenDimension, useTranslation } from "@hooks"; import { Button } from "@atoms"; type VoteContextWrapperProps = { - onContinue: () => void; + onContinue?: () => void; isContinueDisabled?: boolean; onCancel: () => void; + showContinueButton?: boolean; }; export const VoteContextWrapper: FC< PropsWithChildren -> = ({ onContinue, isContinueDisabled, onCancel, children }) => { +> = ({ + onContinue, + isContinueDisabled, + onCancel, + children, + showContinueButton = true, +}) => { const { isMobile } = useScreenDimension(); const { t } = useTranslation(); @@ -43,22 +50,24 @@ export const VoteContextWrapper: FC< width: isMobile ? "100%" : "154px", }} variant="outlined" - > - {t("cancel")} - - - +> + {t("cancel")} + +{showContinueButton && ( + +)} + ); }; diff --git a/govtool/frontend/src/components/organisms/index.ts b/govtool/frontend/src/components/organisms/index.ts index add7263b4..9ef017f4e 100644 --- a/govtool/frontend/src/components/organisms/index.ts +++ b/govtool/frontend/src/components/organisms/index.ts @@ -28,5 +28,7 @@ export * from "./UncontrolledImageInput"; export * from "./ValidatedGovernanceActionCard"; export * from "./ValidatedGovernanceVotedOnCard"; export * from "./VoteContext"; +export * from "./VoteContext/VoteContextChoice"; +export * from "./VoteContext/VoteContextGovTool"; export * from "./WrongRouteInfo"; export * from "./MaintenanceEndingBanner"; diff --git a/govtool/frontend/src/hooks/forms/useVoteContextForm.tsx b/govtool/frontend/src/hooks/forms/useVoteContextForm.tsx index e7f8c0144..acd8b0a96 100644 --- a/govtool/frontend/src/hooks/forms/useVoteContextForm.tsx +++ b/govtool/frontend/src/hooks/forms/useVoteContextForm.tsx @@ -51,7 +51,7 @@ export const useVoteContextForm = ( setHash(jsonHash); setJson(jsonld); - return jsonld; + return { jsonld, jsonHash }; }, [getValues]); const onClickDownloadJson = () => { @@ -102,5 +102,6 @@ export const useVoteContextForm = ( setValue, watch, hash, + json, }; }; diff --git a/govtool/frontend/src/services/requests/index.ts b/govtool/frontend/src/services/requests/index.ts index 598fe76d3..5887c0ee9 100644 --- a/govtool/frontend/src/services/requests/index.ts +++ b/govtool/frontend/src/services/requests/index.ts @@ -21,5 +21,6 @@ export * from "./postDRepRegister"; export * from "./postDRepRemoveVote"; export * from "./postDRepRetire"; export * from "./postDRepVote"; +export * from "./postIpfs"; export * from "./getDRepVotingPowerList"; export * from "./getAccount"; diff --git a/govtool/frontend/src/services/requests/postIpfs.ts b/govtool/frontend/src/services/requests/postIpfs.ts new file mode 100644 index 000000000..105065ed2 --- /dev/null +++ b/govtool/frontend/src/services/requests/postIpfs.ts @@ -0,0 +1,10 @@ +import { API } from "../API"; + +export const postIpfs = async ({ content }: { content: string }) => { + const response = await API.post("/ipfs/upload", content,{ + headers: { + "Content-Type": "text/plain;charset=utf-8" + } + }); + return response.data; +}; From 7a33b9db411e01182792a9df770b6375346a69f0 Mon Sep 17 00:00:00 2001 From: Sudip Bhattarai Date: Fri, 25 Jul 2025 13:13:47 +0545 Subject: [PATCH 03/39] Update "govtool-pins-metadata" flow to match design --- .../VoteContext/VoteContextChoice.tsx | 56 ++++++++++-- .../VoteContext/VoteContextGovTool.tsx | 89 ++++++++++++++++--- govtool/frontend/src/i18n/locales/en.json | 14 +++ govtool/frontend/src/utils/clipboard.ts | 3 + govtool/frontend/src/utils/index.ts | 1 + 5 files changed, 140 insertions(+), 23 deletions(-) create mode 100644 govtool/frontend/src/utils/clipboard.ts diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextChoice.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextChoice.tsx index 9ad563265..03893daa7 100644 --- a/govtool/frontend/src/components/organisms/VoteContext/VoteContextChoice.tsx +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextChoice.tsx @@ -1,10 +1,12 @@ import { Dispatch, SetStateAction } from "react"; -import { Box, Button } from "@mui/material"; +import { Box, Button, Link } from "@mui/material"; import { Spacer, Typography } from "@atoms"; import { useScreenDimension, useTranslation } from "@hooks"; import { VoteContextWrapper } from "@organisms"; import { NodeObject } from "jsonld"; +import { openInNewTab } from "@utils"; +import { LINKS } from "@/consts/links"; type VoteContextChoiceProps = { setStep: Dispatch>; @@ -26,6 +28,8 @@ export const VoteContextChoice = ({ const { t } = useTranslation(); const { isMobile } = useScreenDimension(); + const openLink = () => openInNewTab(LINKS.STORING_INFORMATION_OFFLINE); + const handleStoreItMyself = () => { setStoreDataYourself(true); setStep(3); @@ -42,23 +46,57 @@ export const VoteContextChoice = ({ return ( - {t("createGovernanceAction.storeDataTitle")} + {t("createGovernanceAction.storeAndMaintainDataTitle")} + + + {t("createGovernanceAction.learnMoreAboutStoringInformation")} + + + {t("createGovernanceAction.govToolProvidesOptions")} + + +
    +
  • + + {t("createGovernanceAction.govToolCanPinToIPFS")} + +
  • +
  • + + {t("createGovernanceAction.storeYourselfInRepo")} + +
  • +
+
+ + {t("createGovernanceAction.chooseDataStorageOption")} - + diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextGovTool.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextGovTool.tsx index e79dde8f8..c8214e4c3 100644 --- a/govtool/frontend/src/components/organisms/VoteContext/VoteContextGovTool.tsx +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextGovTool.tsx @@ -1,18 +1,23 @@ import { useEffect, Dispatch, SetStateAction, useState } from "react"; import { Box, Button, CircularProgress, Link, Typography } from "@mui/material"; +import OpenInNewIcon from "@mui/icons-material/OpenInNew"; import { useMutation } from "react-query"; import { Spacer } from "@atoms"; import { useTranslation } from "@hooks"; import { VoteContextWrapper } from "@organisms"; import { postIpfs } from "@services"; -import { downloadTextFile } from "@utils"; +import { downloadTextFile, openInNewTab } from "@utils"; import { NodeObject } from "jsonld"; import { UseFormSetValue } from "react-hook-form"; import { VoteContextFormValues } from "@hooks"; +import { LINKS } from "@/consts/links"; +import { ICONS } from "@/consts/icons"; +import { useSnackbar } from "@context"; +import { copyToClipboard } from "@utils"; interface PostIpfsResponse { - ipfsHash: string; + ipfsCid: string; } type VoteContextGovToolProps = { @@ -34,24 +39,29 @@ export const VoteContextGovTool = ({ metadataHash, setValue, }: VoteContextGovToolProps) => { - const [apiResponse, setApiResponse] = useState(null); + const [apiResponse, setApiResponse] = useState(null); + const [uploadInitiated, setUploadInitiated] = useState(false); // New state to track upload const { t } = useTranslation(); + const { addSuccessAlert } = useSnackbar(); + + const openLink = () => openInNewTab(LINKS.STORING_INFORMATION_OFFLINE); const { mutate, isLoading } = useMutation({ mutationFn: postIpfs, onSuccess: (data) => { - const ipfsUrl = `ipfs://${data.ipfsHash}`; + const ipfsUrl = `ipfs://${data.ipfsCid}`; setValue("storingURL", ipfsUrl); setSavedHash(metadataHash); // Set savedHash to metadataHash - setApiResponse(JSON.stringify(data, null, 2)); + setApiResponse(data); }, }); useEffect(() => { - if (jsonldContent) { + if (jsonldContent && !uploadInitiated) { mutate({ content: JSON.stringify(jsonldContent, null, 2) }); + setUploadInitiated(true); // Set flag after initiating upload } - }, [jsonldContent, mutate]); + }, [jsonldContent, mutate, uploadInitiated]); const handleDownload = () => { if (jsonldContent) { @@ -66,7 +76,33 @@ export const VoteContextGovTool = ({ onCancel={onCancel} > - {t("createGovernanceAction.letGovToolStore")} + {t("createGovernanceAction.rationalePinnedToIPFS")} + + + {t("createGovernanceAction.readFullGuide")} + + + + {t("createGovernanceAction.recommendations")} {isLoading ? ( @@ -75,13 +111,38 @@ export const VoteContextGovTool = ({ ) : apiResponse ? ( <> - - {apiResponse} + + {t("createGovernanceAction.downloadAndStoreMetadataFile")} + + + + {t("createGovernanceAction.rePinYourFile")} - - - {t("createGovernanceAction.downloadJsonLd")} - + + + {apiResponse.ipfsCid ? `ipfs://${apiResponse.ipfsCid}` : "[URI]"} + + {apiResponse.ipfsCid && ( + { + copyToClipboard(`ipfs://${apiResponse.ipfsCid}`); + addSuccessAlert(t("alerts.copiedToClipboard")); + }} + sx={{ cursor: "pointer", display: "flex", alignItems: "center" }} + > + copy + + )} + ) : ( diff --git a/govtool/frontend/src/i18n/locales/en.json b/govtool/frontend/src/i18n/locales/en.json index 73836a465..99c192a7c 100644 --- a/govtool/frontend/src/i18n/locales/en.json +++ b/govtool/frontend/src/i18n/locales/en.json @@ -246,6 +246,20 @@ "storingInformationURLPlaceholder": "URL", "supportingLinks": "Supporting links", "title": "Create a Governance Action", + "storeAndMaintainDataTitle": "Store and Maintain the Data Yourself", + "learnMoreAboutStoringInformation": "Learn more about storing information", + "govToolProvidesOptions": "GovTool currently provides two options for storing your rationale:", + "govToolCanPinToIPFS": "GovTool can pin it to IPFS", + "storeYourselfInRepo": "Store it yourself in a repository such as GitHub", + "chooseDataStorageOption": "Choose a data storage option:", + "govToolPinsDataToIPFS": "GovTool pins data to IPFS", + "downloadAndStoreYourself": "Download and store yourself", + "uploadingToIPFS": "Uploading to IPFS...", + "rationalePinnedToIPFS": "Your rationale will be pinned to IPFS", + "readFullGuide": "Read full guide", + "recommendations": "Recommendations", + "downloadAndStoreMetadataFile": "Download and store your metadata file\n(if you needed in the future you can re-pin it on IPFS)", + "rePinYourFile": "Re-pin your file", "modals": { "submitTransactionSuccess": { "message": "Your Governance Action may take a little time to submit to the chain.", diff --git a/govtool/frontend/src/utils/clipboard.ts b/govtool/frontend/src/utils/clipboard.ts new file mode 100644 index 000000000..63e04ad3d --- /dev/null +++ b/govtool/frontend/src/utils/clipboard.ts @@ -0,0 +1,3 @@ +export const copyToClipboard = (text: string) => { + navigator.clipboard.writeText(text); +}; diff --git a/govtool/frontend/src/utils/index.ts b/govtool/frontend/src/utils/index.ts index f331202b3..9ab32b8f0 100644 --- a/govtool/frontend/src/utils/index.ts +++ b/govtool/frontend/src/utils/index.ts @@ -39,3 +39,4 @@ export * from "./getBase64ImageDetails"; export * from "./parseBoolean"; export * from "./validateSignature"; export * from "./cip8verification"; +export * from "./clipboard"; From 1405f09f342e3889355c86bd51631fb8edebbd33 Mon Sep 17 00:00:00 2001 From: Aaron Boyle Date: Sat, 26 Jul 2025 22:23:51 +0100 Subject: [PATCH 04/39] removes higher environments limiting to dev only --- .github/workflows/merge.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/merge.yaml b/.github/workflows/merge.yaml index 0511b4023..a753c4c22 100644 --- a/.github/workflows/merge.yaml +++ b/.github/workflows/merge.yaml @@ -12,11 +12,11 @@ permissions: packages: write env: - ENVIRONMENT: ${{ (github.ref_name == 'main' && 'prod-govtool') || (github.ref_name == 'staging' && 'pre-prod-govtool') || (github.ref_name == 'test' && 'qa-govtool') || (github.ref_name == 'develop' && 'dev-govtool') }} + ENVIRONMENT: ${{ (github.ref_name == 'develop' && 'dev-govtool') }} jobs: check-build-deploy: - environment: ${{ (github.ref_name == 'main' && 'prod-govtool') || (github.ref_name == 'staging' && 'pre-prod-govtool') || (github.ref_name == 'test' && 'qa-govtool') || (github.ref_name == 'develop' && 'dev-govtool') }} + environment: ${{ (github.ref_name == 'develop' && 'dev-govtool') }} strategy: fail-fast: false matrix: From f072420ee7bf28fb2d6d627aa28f2429ff3d8351 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 28 Jul 2025 10:37:45 +0000 Subject: [PATCH 05/39] chore: update @intersect.mbo/govtool-outcomes-pillar-ui to v1.5.4 --- govtool/frontend/package-lock.json | 134 ++++++++++++++++++++--------- govtool/frontend/package.json | 2 +- 2 files changed, 95 insertions(+), 41 deletions(-) diff --git a/govtool/frontend/package-lock.json b/govtool/frontend/package-lock.json index 39903d276..90f4d8612 100644 --- a/govtool/frontend/package-lock.json +++ b/govtool/frontend/package-lock.json @@ -13,7 +13,7 @@ "@emotion/styled": "^11.11.0", "@emurgo/cardano-serialization-lib-asmjs": "^14.1.1", "@hookform/resolvers": "^3.3.1", - "@intersect.mbo/govtool-outcomes-pillar-ui": "v1.5.3", + "@intersect.mbo/govtool-outcomes-pillar-ui": "v1.5.4", "@intersect.mbo/intersectmbo.org-icons-set": "^1.0.8", "@intersect.mbo/pdf-ui": "1.0.9-beta", "@mui/icons-material": "^5.14.3", @@ -3392,14 +3392,14 @@ } }, "node_modules/@intersect.mbo/govtool-outcomes-pillar-ui": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@intersect.mbo/govtool-outcomes-pillar-ui/-/govtool-outcomes-pillar-ui-1.5.3.tgz", - "integrity": "sha512-dMIiaqyS7S3c3UO/NDweCwMsX197WwNdn6elkbizQbZjBLNwKr0mqsoKOMvkbeGDx7UgaRqjB+Nere2eu1xAQA==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@intersect.mbo/govtool-outcomes-pillar-ui/-/govtool-outcomes-pillar-ui-1.5.4.tgz", + "integrity": "sha512-lg6+GmP6Be5P+jtSqHnpf+tHGhzJt23P0wj6PO9KVnAWtSnYgF/T/JZY82LI57+ayTVoEN01lQpTGROxJJF6rA==", "license": "ISC", "dependencies": { "@fontsource/poppins": "^5.0.14", "@intersect.mbo/intersectmbo.org-icons-set": "^1.0.8", - "axios": "^1.8.4", + "axios": "^1.10.0", "bech32": "^2.0.0", "buffer": "^6.0.3", "react-diff-view": "^3.2.1", @@ -9352,13 +9352,13 @@ } }, "node_modules/axios": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", - "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", + "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", + "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, @@ -10074,6 +10074,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -12089,6 +12102,20 @@ "license": "BSD-2-Clause", "peer": true }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", @@ -12283,13 +12310,10 @@ "peer": true }, "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, "engines": { "node": ">= 0.4" } @@ -12357,9 +12381,9 @@ "license": "MIT" }, "node_modules/es-object-atoms": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -12369,14 +12393,15 @@ } }, "node_modules/es-set-tostringtag": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", - "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.4", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", - "hasown": "^2.0.1" + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -14952,13 +14977,15 @@ } }, "node_modules/form-data": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", - "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -15146,16 +15173,21 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -15180,6 +15212,19 @@ "node": ">=8.0.0" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -15364,12 +15409,12 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.1.3" + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -15470,9 +15515,9 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -20044,6 +20089,15 @@ "remove-accents": "0.5.0" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/mdast-util-find-and-replace": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", diff --git a/govtool/frontend/package.json b/govtool/frontend/package.json index bf96a1742..c5dac98c7 100644 --- a/govtool/frontend/package.json +++ b/govtool/frontend/package.json @@ -27,7 +27,7 @@ "@emotion/styled": "^11.11.0", "@emurgo/cardano-serialization-lib-asmjs": "^14.1.1", "@hookform/resolvers": "^3.3.1", - "@intersect.mbo/govtool-outcomes-pillar-ui": "v1.5.3", + "@intersect.mbo/govtool-outcomes-pillar-ui": "v1.5.4", "@intersect.mbo/intersectmbo.org-icons-set": "^1.0.8", "@intersect.mbo/pdf-ui": "1.0.9-beta", "@mui/icons-material": "^5.14.3", From 55223cb533cbcbd629aa719d01337ccdd17e4579 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 29 Jul 2025 08:25:38 +0000 Subject: [PATCH 06/39] chore: update @intersect.mbo/pdf-ui to 1.0.10-beta --- govtool/frontend/package-lock.json | 8 ++++---- govtool/frontend/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/govtool/frontend/package-lock.json b/govtool/frontend/package-lock.json index 90f4d8612..fa0e37c66 100644 --- a/govtool/frontend/package-lock.json +++ b/govtool/frontend/package-lock.json @@ -15,7 +15,7 @@ "@hookform/resolvers": "^3.3.1", "@intersect.mbo/govtool-outcomes-pillar-ui": "v1.5.4", "@intersect.mbo/intersectmbo.org-icons-set": "^1.0.8", - "@intersect.mbo/pdf-ui": "1.0.9-beta", + "@intersect.mbo/pdf-ui": "1.0.10-beta", "@mui/icons-material": "^5.14.3", "@mui/material": "^5.14.4", "@noble/ed25519": "^2.3.0", @@ -3426,9 +3426,9 @@ "license": "ISC" }, "node_modules/@intersect.mbo/pdf-ui": { - "version": "1.0.9-beta", - "resolved": "https://registry.npmjs.org/@intersect.mbo/pdf-ui/-/pdf-ui-1.0.9-beta.tgz", - "integrity": "sha512-rFWt8PrRt6ecbW0mN2p3HQVUGtJyYEmJSaaMYcN/XgwGfehmJ5CblP9S7/W5ZoDiQrFTaH3bBLl9Bu31mbcJpA==", + "version": "1.0.10-beta", + "resolved": "https://registry.npmjs.org/@intersect.mbo/pdf-ui/-/pdf-ui-1.0.10-beta.tgz", + "integrity": "sha512-1IrictQBHpAGSqj5/psGB/Puxkkvmfi/ZZV0Bn60mBgJoTcUpoIO7tVr+h9pP+FSPFNnQaGeeFhca50PuUUp4Q==", "dependencies": { "@emurgo/cardano-serialization-lib-asmjs": "^12.0.0-beta.2", "@fontsource/poppins": "^5.0.14", diff --git a/govtool/frontend/package.json b/govtool/frontend/package.json index c5dac98c7..a4fa2c8d4 100644 --- a/govtool/frontend/package.json +++ b/govtool/frontend/package.json @@ -29,7 +29,7 @@ "@hookform/resolvers": "^3.3.1", "@intersect.mbo/govtool-outcomes-pillar-ui": "v1.5.4", "@intersect.mbo/intersectmbo.org-icons-set": "^1.0.8", - "@intersect.mbo/pdf-ui": "1.0.9-beta", + "@intersect.mbo/pdf-ui": "1.0.10-beta", "@mui/icons-material": "^5.14.3", "@mui/material": "^5.14.4", "@noble/ed25519": "^2.3.0", From 448b6b10adc27d5bbc6ca4b72ac379e1236f9068 Mon Sep 17 00:00:00 2001 From: bosko-m <88723596+bosko-m@users.noreply.github.com> Date: Tue, 29 Jul 2025 16:54:53 +0200 Subject: [PATCH 07/39] Update dataActionsBar.tsx - empty list of GAs on back after idle time Signed-off-by: bosko-m <88723596+bosko-m@users.noreply.github.com> --- govtool/frontend/src/context/dataActionsBar.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/govtool/frontend/src/context/dataActionsBar.tsx b/govtool/frontend/src/context/dataActionsBar.tsx index 9ef913a26..2c7de5c5b 100644 --- a/govtool/frontend/src/context/dataActionsBar.tsx +++ b/govtool/frontend/src/context/dataActionsBar.tsx @@ -42,7 +42,7 @@ interface ProviderProps { } const DataActionsBarProvider: FC = ({ children }) => { - const isAdjusting = useRef(false); + const [isAdjusting, setIsAdjusting] = useState(false); const [searchText, setSearchText] = useState(""); const debouncedSearchText = useDebounce(searchText.trim(), 300); const [filtersOpen, setFiltersOpen] = useState(false); @@ -79,7 +79,11 @@ const DataActionsBarProvider: FC = ({ children }) => { pathname.includes("governance_actions/category"); useEffect(() => { - isAdjusting.current = true; + setIsAdjusting(true); + + const timeout = setTimeout(() => { + setIsAdjusting(false); + }, 150); // Adjust delay if needed if ( (!pathname.includes("drep_directory") && @@ -89,6 +93,8 @@ const DataActionsBarProvider: FC = ({ children }) => { ) { resetState(); } + + return () => clearTimeout(timeout); }, [pathname, resetState]); useEffect(() => { @@ -97,7 +103,7 @@ const DataActionsBarProvider: FC = ({ children }) => { const contextValue = useMemo( () => ({ - isAdjusting: isAdjusting.current, + isAdjusting, chosenFilters, chosenFiltersLength: chosenFilters.length, chosenSorting, From a56f5a55a8836c7ad2a3cd6b2a63f2d9417f8b7a Mon Sep 17 00:00:00 2001 From: bosko-m <88723596+bosko-m@users.noreply.github.com> Date: Tue, 29 Jul 2025 17:09:29 +0200 Subject: [PATCH 08/39] Update dataActionsBar.tsx - lint check fix Signed-off-by: bosko-m <88723596+bosko-m@users.noreply.github.com> --- govtool/frontend/src/context/dataActionsBar.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/govtool/frontend/src/context/dataActionsBar.tsx b/govtool/frontend/src/context/dataActionsBar.tsx index 2c7de5c5b..d596f5d2f 100644 --- a/govtool/frontend/src/context/dataActionsBar.tsx +++ b/govtool/frontend/src/context/dataActionsBar.tsx @@ -8,7 +8,6 @@ import React, { useEffect, useMemo, FC, - useRef, } from "react"; import { useLocation } from "react-router-dom"; @@ -65,7 +64,6 @@ const DataActionsBarProvider: FC = ({ children }) => { setSearchText(""); setChosenFilters([]); setChosenSorting(""); - isAdjusting.current = false; }, []); const userMovedToDifferentAppArea = From 2e9124451615d194c199ca668108878c0f5db3dd Mon Sep 17 00:00:00 2001 From: bosko-m <88723596+bosko-m@users.noreply.github.com> Date: Tue, 29 Jul 2025 18:07:43 +0200 Subject: [PATCH 09/39] Update useGetProposalsQuery.ts - 3978 - voterReady logic added Signed-off-by: bosko-m <88723596+bosko-m@users.noreply.github.com> --- .../src/hooks/queries/useGetProposalsQuery.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/govtool/frontend/src/hooks/queries/useGetProposalsQuery.ts b/govtool/frontend/src/hooks/queries/useGetProposalsQuery.ts index 27f349615..b0188e30c 100644 --- a/govtool/frontend/src/hooks/queries/useGetProposalsQuery.ts +++ b/govtool/frontend/src/hooks/queries/useGetProposalsQuery.ts @@ -10,11 +10,19 @@ export const useGetProposalsQuery = ({ filters = [], searchPhrase, sorting, - enabled, + enabled = true, }: GetProposalsArguments) => { const { dRepID } = useCardano(); const { voter } = useGetVoterInfo(); +// Determine if voter is ready to be used in the query +const voterReady = + !!dRepID && (voter?.isRegisteredAsDRep || voter?.isRegisteredAsSoleVoter); + +// Only run the query if enabled externally and voter info is ready +const shouldFetch = enabled && voterReady; + + const fetchProposals = async (): Promise => { const allProposals = await Promise.all( filters.map((filter) => @@ -45,7 +53,7 @@ export const useGetProposalsQuery = ({ ], fetchProposals, { - enabled, + enabled: shouldFetch, refetchOnWindowFocus: true, keepPreviousData: true, }, From 5c8059e1b6a6548fa8b9741b65306f0a83b2e6d0 Mon Sep 17 00:00:00 2001 From: Aaron Boyle Date: Tue, 29 Jul 2025 17:55:12 +0100 Subject: [PATCH 10/39] update to corrects empty line linting error --- govtool/frontend/src/hooks/queries/useGetProposalsQuery.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/govtool/frontend/src/hooks/queries/useGetProposalsQuery.ts b/govtool/frontend/src/hooks/queries/useGetProposalsQuery.ts index b0188e30c..da62db474 100644 --- a/govtool/frontend/src/hooks/queries/useGetProposalsQuery.ts +++ b/govtool/frontend/src/hooks/queries/useGetProposalsQuery.ts @@ -22,7 +22,6 @@ const voterReady = // Only run the query if enabled externally and voter info is ready const shouldFetch = enabled && voterReady; - const fetchProposals = async (): Promise => { const allProposals = await Promise.all( filters.map((filter) => From bcd8875c1bd6054611849c67288ede30a81ccb55 Mon Sep 17 00:00:00 2001 From: Sudip Bhattarai Date: Wed, 30 Jul 2025 00:20:11 +0545 Subject: [PATCH 11/39] feat: Json response on errors in vva-be --- govtool/backend/app/Main.hs | 20 +++++--- govtool/backend/src/VVA/API.hs | 12 +++-- govtool/backend/src/VVA/Ipfs.hs | 78 +++++++++++++++++++++++++++----- govtool/backend/src/VVA/Types.hs | 11 ++++- govtool/backend/vva-be.cabal | 2 + 5 files changed, 100 insertions(+), 23 deletions(-) diff --git a/govtool/backend/app/Main.hs b/govtool/backend/app/Main.hs index d82821424..4ba525ad0 100644 --- a/govtool/backend/app/Main.hs +++ b/govtool/backend/app/Main.hs @@ -16,6 +16,7 @@ import Control.Monad.Trans.Except import Control.Monad.Trans.Reader import Data.Aeson hiding (Error) +import Data.Aeson (encode) import qualified Data.ByteString as BS import Data.ByteString.Char8 (unpack) import qualified Data.Cache as Cache @@ -35,7 +36,7 @@ import Data.Text.Encoding (encodeUtf8) import qualified Data.Text.IO as Text import qualified Data.Text.Lazy as LazyText import qualified Data.Text.Lazy.Encoding as LazyText - +import qualified Data.ByteString.Lazy.Char8 as BS8 import Database.PostgreSQL.Simple (close, connectPostgreSQL, Connection) import Network.Wai @@ -62,8 +63,10 @@ import VVA.API.Types import VVA.CommandLine import VVA.Config import VVA.Types (AppEnv (..), - AppError (CriticalError, InternalError, NotFoundError, ValidationError), + AppError (..), CacheEnv (..)) +import VVA.Ipfs (IpfsError(..)) + -- Function to create a connection pool with optimized settings createOptimizedConnectionPool :: BS.ByteString -> IO (Pool Connection) @@ -288,10 +291,15 @@ liftServer appEnv = where handleErrors :: Either AppError a -> Handler a handleErrors (Right x) = pure x - handleErrors (Left (ValidationError msg)) = throwError $ err400 { errBody = BS.fromStrict $ encodeUtf8 msg } - handleErrors (Left (NotFoundError msg)) = throwError $ err404 { errBody = BS.fromStrict $ encodeUtf8 msg } - handleErrors (Left (CriticalError msg)) = throwError $ err500 { errBody = BS.fromStrict $ encodeUtf8 msg } - handleErrors (Left (InternalError msg)) = throwError $ err500 { errBody = BS.fromStrict $ encodeUtf8 msg } + handleErrors (Left appError) = do + let status = case appError of + ValidationError _ -> err400 + NotFoundError _ -> err404 + CriticalError _ -> err500 + InternalError _ -> err500 + AppIpfsError (OtherIpfsError _) -> err400 + AppIpfsError _ -> err503 + throwError $ status { errBody = encode appError, errHeaders = [("Content-Type", "application/json")] } -- * Swagger type SwaggerAPI = SwaggerSchemaUI "swagger-ui" "swagger.json" diff --git a/govtool/backend/src/VVA/API.hs b/govtool/backend/src/VVA/API.hs index c17f25d7b..6ab25a806 100644 --- a/govtool/backend/src/VVA/API.hs +++ b/govtool/backend/src/VVA/API.hs @@ -1,10 +1,10 @@ -{-# LANGUAGE DataKinds #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TypeOperators #-} {-# LANGUAGE ViewPatterns #-} +{-# LANGUAGE DataKinds #-} module VVA.API where @@ -31,6 +31,7 @@ import Numeric.Natural (Natural) import Servant.API import Servant.Server +import Servant.Exception (Throws) import System.Random (randomRIO) import Text.Read (readMaybe) @@ -47,16 +48,17 @@ import qualified VVA.Proposal as Proposal import qualified VVA.Transaction as Transaction import qualified VVA.Types as Types import VVA.Types (App, AppEnv (..), - AppError (CriticalError, InternalError, ValidationError), + AppError (CriticalError, InternalError, ValidationError, AppIpfsError), CacheEnv (..)) import Data.Time (TimeZone, localTimeToUTC) import qualified VVA.Ipfs as Ipfs import Data.ByteString.Lazy (ByteString) import qualified Data.ByteString.Lazy as BSL +import Servant.Exception (Throws) type VVAApi = "ipfs" - :> "upload" :> QueryParam "fileName" Text :> ReqBody '[PlainText] Text :> Post '[JSON] UploadResponse + :> "upload" :> QueryParam "fileName" Text :> ReqBody '[PlainText] Text :> Post '[JSON] UploadResponse :<|> "drep" :> "list" :> QueryParam "search" Text :> QueryParams "status" DRepStatus @@ -125,8 +127,8 @@ upload mFileName fileContentText = do throwError $ ValidationError "The uploaded file is larger than 500Kb" eIpfsHash <- liftIO $ Ipfs.ipfsUpload vvaPinataJwt fileName fileContent case eIpfsHash of - Left err -> throwError $ InternalError $ "IPFS upload failed: " <> pack err - Right ipfsHash -> return $ UploadResponse ipfsHash + Left err -> throwError $ AppIpfsError err + Right ipfsHash -> return $ UploadResponse ipfsHash mapDRepType :: Types.DRepType -> DRepType mapDRepType Types.DRep = NormalDRep diff --git a/govtool/backend/src/VVA/Ipfs.hs b/govtool/backend/src/VVA/Ipfs.hs index 2f99414f0..86202f298 100644 --- a/govtool/backend/src/VVA/Ipfs.hs +++ b/govtool/backend/src/VVA/Ipfs.hs @@ -2,11 +2,12 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} -module VVA.Ipfs (ipfsUpload) where +module VVA.Ipfs (ipfsUpload, IpfsError(..)) where import Control.Exception (SomeException, try) import Control.Monad.IO.Class (liftIO) -import Data.Aeson (FromJSON(parseJSON), withObject, (.:), eitherDecode) +import qualified Data.Aeson as A +import Data.Aeson (FromJSON(parseJSON), withObject, (.:), eitherDecode, ToJSON(..), encode,(.=),object) import qualified Data.ByteString.Lazy as LBS import Data.Text (Text) import qualified Data.Text.Encoding as TE @@ -14,11 +15,13 @@ import GHC.Generics (Generic) import Network.HTTP.Client (newManager, parseRequest, httpLbs, method, requestHeaders, RequestBody(..), Request, responseBody, responseStatus) import Network.HTTP.Client.TLS (tlsManagerSettings) import Network.HTTP.Client.MultipartFormData (formDataBody, partBS, partFileRequestBody) -import Network.HTTP.Types.Status (statusIsSuccessful) +import Network.HTTP.Types.Status (statusIsSuccessful, Status, status503, status400) import qualified Data.ByteString.Lazy.Char8 as LBS8 import qualified Data.Text.Lazy as TL import qualified Data.Text.Lazy.Encoding as TL import qualified Data.Text as T +import Servant.Server (ServerError (errBody)) +import Servant.Exception (ToServantErr(..), Exception(..)) data PinataData = PinataData @@ -38,11 +41,64 @@ instance FromJSON PinataSuccessResponse where parseJSON = withObject "PinataSuccessResponse" $ \v -> PinataSuccessResponse <$> v .: "data" -ipfsUpload :: Maybe Text -> Text -> LBS.ByteString -> IO (Either String Text) +data IpfsError + = PinataConnectionError String + | PinataAPIError Status LBS.ByteString + | PinataDecodingError String LBS.ByteString + | IpfsUnconfiguredError + | OtherIpfsError String + deriving (Show, Generic) + +instance ToJSON IpfsError where + toJSON (PinataConnectionError msg) = + object ["errorType" .= A.String "PinataConnectionError", "message" .= msg] + + toJSON (PinataAPIError status body) = + object + [ "errorType" .= A.String "PinataAPIError" + , "message" .= ("Pinata API returned error status : " ++ show status) + , "pinataResponse" .= object + [ "status" .= show status + , "body" .= TL.unpack (TL.decodeUtf8 body) + ] + ] + + toJSON (PinataDecodingError msg body) = + object + [ "errorType" .= A.String "PinataDecodingError" + , "message" .= msg + , "pinataResponse" .= object + [ "status" .= ("unknown" :: String) + , "body" .= TL.unpack (TL.decodeUtf8 body) + ] + ] + + toJSON IpfsUnconfiguredError = + object ["errorType" .= A.String "IpfsUnconfiguredError", "message" .= ("Backend is not configured for upfs upload" :: String)] + + toJSON (OtherIpfsError msg) = + object ["errorType" .= A.String "OtherIpfsError", "message" .= msg] + + +instance Exception IpfsError + + + +instance ToServantErr IpfsError where + status (OtherIpfsError _) = status400 + status _ = status503 + + message (PinataConnectionError msg) = T.pack ("Pinata service connection error: " <> msg) + message (PinataAPIError status body) = T.pack ("Pinata API error: " <> show status <> " - " <> LBS8.unpack body) + message (PinataDecodingError msg body) = T.pack ("Pinata decoding error: " <> msg <> " - " <> LBS8.unpack body) + message IpfsUnconfiguredError = T.pack ("Backend is not configured to support ipfs upload") + message (OtherIpfsError msg) = T.pack msg + +ipfsUpload :: Maybe Text -> Text -> LBS.ByteString -> IO (Either IpfsError Text) ipfsUpload maybeJwt fileName fileContent = case maybeJwt of - Nothing -> pure $ Left "Backend is not configured to support ipfs upload" - Just "" -> pure $ Left "Backend is not configured to support ipfs upload" + Nothing -> pure $ Left $ IpfsUnconfiguredError + Just "" -> pure $ Left $ IpfsUnconfiguredError Just jwt -> do manager <- newManager tlsManagerSettings initialRequest <- parseRequest "https://uploads.pinata.cloud/v3/files" @@ -60,18 +116,18 @@ ipfsUpload maybeJwt fileName fileContent = Left (e :: SomeException) -> do let errMsg = show e liftIO $ putStrLn errMsg - pure $ Left errMsg + pure $ Left $ PinataConnectionError errMsg Right response -> do let body = responseBody response let status = responseStatus response if statusIsSuccessful status then case eitherDecode body of Left err -> do - let errMsg = "Failed to decode Pinata API reponse: " <> err <> "\nResponse body: " <> LBS8.unpack body + let errMsg = "Failed to decode Pinata API reponse: " <> err liftIO $ putStrLn errMsg - pure $ Left errMsg + pure $ Left $ PinataDecodingError errMsg body Right (res :: PinataSuccessResponse) -> pure $ Right $ cid $ pinataData res else do - let errMsg = "Pinata API request failed with status: " <> show status <> "\nResponse body: " <> LBS8.unpack body + let errMsg = "Pinata API request failed with status: " <> show status liftIO $ putStrLn errMsg - pure $ Left errMsg + pure $ Left $ PinataAPIError status body diff --git a/govtool/backend/src/VVA/Types.hs b/govtool/backend/src/VVA/Types.hs index 25c248bd3..d5b0f2cf2 100644 --- a/govtool/backend/src/VVA/Types.hs +++ b/govtool/backend/src/VVA/Types.hs @@ -15,7 +15,7 @@ import Control.Monad.Except (MonadError) import Control.Monad.Fail (MonadFail) import Control.Monad.IO.Class (MonadIO) import Control.Monad.Reader (MonadReader) - +import qualified Data.Aeson as A import Data.Aeson (Value, ToJSON (..), object, (.=)) import qualified Data.Cache as Cache import Data.Has @@ -30,6 +30,7 @@ import Database.PostgreSQL.Simple.FromField (FromField(..), returnErr import VVA.Cache import VVA.Config +import VVA.Ipfs (IpfsError) type App m = (MonadReader AppEnv m, MonadIO m, MonadFail m, MonadError AppError m) @@ -57,10 +58,18 @@ data AppError | NotFoundError Text | CriticalError Text | InternalError Text + | AppIpfsError IpfsError deriving (Show) instance Exception AppError +instance ToJSON AppError where + toJSON (ValidationError msg) = object ["errorType" .= A.String "ValidationError", "message" .= msg] + toJSON (NotFoundError msg) = object ["errorType" .= A.String "NotFoundError", "message" .= msg] + toJSON (CriticalError msg) = object ["errorType" .= A.String "CriticalError", "message" .= msg] + toJSON (InternalError msg) = object ["errorType" .= A.String "InternalError", "message" .= msg] + toJSON (AppIpfsError err) = toJSON err + data Vote = Vote { voteProposalId :: Integer diff --git a/govtool/backend/vva-be.cabal b/govtool/backend/vva-be.cabal index 37cbf1062..8eb0de86d 100644 --- a/govtool/backend/vva-be.cabal +++ b/govtool/backend/vva-be.cabal @@ -52,6 +52,7 @@ executable vva-be , text , servant-swagger-ui , servant-server + , servant-exceptions , servant-openapi3 , servant , wai @@ -91,6 +92,7 @@ library , bytestring , optparse-applicative , servant + , servant-exceptions , openapi3 , lens , postgresql-simple From 53f2860f099c42552562bdebbb32047b7696fd62 Mon Sep 17 00:00:00 2001 From: joseph rana Date: Mon, 28 Jul 2025 14:32:58 +0545 Subject: [PATCH 12/39] chore: update vote context store options card styles and texts --- .../VoteContext/VoteContextChoice.tsx | 94 ++++++++----------- .../VoteContext/VoteContextWrapper.tsx | 64 +++++++------ govtool/frontend/src/i18n/locales/en.json | 1 + 3 files changed, 76 insertions(+), 83 deletions(-) diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextChoice.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextChoice.tsx index 03893daa7..10edd7514 100644 --- a/govtool/frontend/src/components/organisms/VoteContext/VoteContextChoice.tsx +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextChoice.tsx @@ -44,63 +44,45 @@ export const VoteContextChoice = ({ }; return ( - - - {t("createGovernanceAction.storeAndMaintainDataTitle")} - - - {t("createGovernanceAction.learnMoreAboutStoringInformation")} - - - {t("createGovernanceAction.govToolProvidesOptions")} - - -
    -
  • - - {t("createGovernanceAction.govToolCanPinToIPFS")} - -
  • -
  • - - {t("createGovernanceAction.storeYourselfInRepo")} - -
  • -
-
- - {t("createGovernanceAction.chooseDataStorageOption")} - - - - - - - - + {t("createGovernanceAction.learnMoreAboutStoringInformation")} + + + + {t("createGovernanceAction.chooseDataStorageOption")} + + + + + +
); }; diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextWrapper.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextWrapper.tsx index 97a421a73..a7f48f99d 100644 --- a/govtool/frontend/src/components/organisms/VoteContext/VoteContextWrapper.tsx +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextWrapper.tsx @@ -7,8 +7,10 @@ import { Button } from "@atoms"; type VoteContextWrapperProps = { onContinue?: () => void; isContinueDisabled?: boolean; + showCancelButton? : boolean; onCancel: () => void; showContinueButton?: boolean; + showAllButtons? : boolean; }; export const VoteContextWrapper: FC< @@ -17,8 +19,10 @@ export const VoteContextWrapper: FC< onContinue, isContinueDisabled, onCancel, + showCancelButton = true, children, showContinueButton = true, + showAllButtons = true }) => { const { isMobile } = useScreenDimension(); const { t } = useTranslation(); @@ -34,40 +38,46 @@ export const VoteContextWrapper: FC< > {children} + { + showAllButtons && + { + showCancelButton && + + } + {showContinueButton && ( -{showContinueButton && ( - -)} - + variant="contained" + > + {t("continue")} + + )} + + } ); }; diff --git a/govtool/frontend/src/i18n/locales/en.json b/govtool/frontend/src/i18n/locales/en.json index 99c192a7c..d06178a76 100644 --- a/govtool/frontend/src/i18n/locales/en.json +++ b/govtool/frontend/src/i18n/locales/en.json @@ -251,6 +251,7 @@ "govToolProvidesOptions": "GovTool currently provides two options for storing your rationale:", "govToolCanPinToIPFS": "GovTool can pin it to IPFS", "storeYourselfInRepo": "Store it yourself in a repository such as GitHub", + "storingOptionsForYourVoterRationale" : "Storing options for your voter rationale", "chooseDataStorageOption": "Choose a data storage option:", "govToolPinsDataToIPFS": "GovTool pins data to IPFS", "downloadAndStoreYourself": "Download and store yourself", From 7481c0581092fff2419b6bb5c55ab37cc447b23f Mon Sep 17 00:00:00 2001 From: joseph rana Date: Mon, 28 Jul 2025 15:59:57 +0545 Subject: [PATCH 13/39] chore: update vote rationale pinned card styles and text --- .../VoteContext/VoteContextGovTool.tsx | 78 +++++++++++-------- govtool/frontend/src/i18n/locales/en.json | 13 ++-- 2 files changed, 51 insertions(+), 40 deletions(-) diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextGovTool.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextGovTool.tsx index c8214e4c3..000c754f8 100644 --- a/govtool/frontend/src/components/organisms/VoteContext/VoteContextGovTool.tsx +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextGovTool.tsx @@ -4,7 +4,7 @@ import OpenInNewIcon from "@mui/icons-material/OpenInNew"; import { useMutation } from "react-query"; import { Spacer } from "@atoms"; -import { useTranslation } from "@hooks"; +import { useScreenDimension, useTranslation } from "@hooks"; import { VoteContextWrapper } from "@organisms"; import { postIpfs } from "@services"; import { downloadTextFile, openInNewTab } from "@utils"; @@ -15,6 +15,7 @@ import { LINKS } from "@/consts/links"; import { ICONS } from "@/consts/icons"; import { useSnackbar } from "@context"; import { copyToClipboard } from "@utils"; +import { primaryBlue } from "@/consts"; interface PostIpfsResponse { ipfsCid: string; @@ -44,6 +45,8 @@ export const VoteContextGovTool = ({ const { t } = useTranslation(); const { addSuccessAlert } = useSnackbar(); + const { isMobile } = useScreenDimension(); + const openLink = () => openInNewTab(LINKS.STORING_INFORMATION_OFFLINE); const { mutate, isLoading } = useMutation({ @@ -74,8 +77,9 @@ export const VoteContextGovTool = ({ onContinue={submitVoteContext} isContinueDisabled={!apiResponse} onCancel={onCancel} + showAllButtons={false} > - + {t("createGovernanceAction.rationalePinnedToIPFS")} - {t("createGovernanceAction.readFullGuide")} - + {t("createGovernanceAction.learnMore")} - - {t("createGovernanceAction.recommendations")} - - {isLoading ? ( ) : apiResponse ? ( <> - - {t("createGovernanceAction.downloadAndStoreMetadataFile")} + + {t("createGovernanceAction.optionalDownloadAndStoreMetadataFile")} - - {t("createGovernanceAction.rePinYourFile")} + + {t("createGovernanceAction.rePinYourFileToIPFS")} - - {apiResponse.ipfsCid ? `ipfs://${apiResponse.ipfsCid}` : "[URI]"} - - {apiResponse.ipfsCid && ( - { - copyToClipboard(`ipfs://${apiResponse.ipfsCid}`); - addSuccessAlert(t("alerts.copiedToClipboard")); - }} - sx={{ cursor: "pointer", display: "flex", alignItems: "center" }} + + {apiResponse.ipfsCid ? ( + - copy - + IPFS URI: {`https://ipfs.io/ipfs/${apiResponse.ipfsCid}`} + + ) : ( + "[URI]" )} + ) : ( @@ -149,6 +143,22 @@ export const VoteContextGovTool = ({ {t("createGovernanceAction.uploadingToIPFS")} )} + + + +
); }; diff --git a/govtool/frontend/src/i18n/locales/en.json b/govtool/frontend/src/i18n/locales/en.json index d06178a76..f7a54695e 100644 --- a/govtool/frontend/src/i18n/locales/en.json +++ b/govtool/frontend/src/i18n/locales/en.json @@ -256,17 +256,18 @@ "govToolPinsDataToIPFS": "GovTool pins data to IPFS", "downloadAndStoreYourself": "Download and store yourself", "uploadingToIPFS": "Uploading to IPFS...", - "rationalePinnedToIPFS": "Your rationale will be pinned to IPFS", - "readFullGuide": "Read full guide", - "recommendations": "Recommendations", - "downloadAndStoreMetadataFile": "Download and store your metadata file\n(if you needed in the future you can re-pin it on IPFS)", - "rePinYourFile": "Re-pin your file", + "rationalePinnedToIPFS": "GovTool has pinned your rationale to IPFS", + "learnMore" : "Learn more", + "optionalDownloadAndStoreMetadataFile": "Optional: Download and store a backup copy of your metadata file", + "rePinYourFileToIPFS": "Re-pin your file to IPFS", "modals": { "submitTransactionSuccess": { "message": "Your Governance Action may take a little time to submit to the chain.", "title": "Governance Action submitted!" } - } + }, + "back" : "Back", + "continue" : "Continue" }, "delegation": { "description": "You can delegate your voting power to a DRep or to a pre-defined voting option.", From 9382b1e789978f961324db8b59342741301cddba Mon Sep 17 00:00:00 2001 From: joseph rana Date: Mon, 28 Jul 2025 16:51:27 +0545 Subject: [PATCH 14/39] chore: update vote context terms style --- .../src/components/organisms/VoteContext/VoteContextTerms.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextTerms.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextTerms.tsx index ce92076d3..07423d052 100644 --- a/govtool/frontend/src/components/organisms/VoteContext/VoteContextTerms.tsx +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextTerms.tsx @@ -27,7 +27,7 @@ export const VoteContextTerms = ({ setStep, onCancel }: StoreDataInfoProps) => { isContinueDisabled={isContinueDisabled} onCancel={onCancel} > - + {t("createGovernanceAction.storeDataTitle")} Date: Tue, 29 Jul 2025 12:23:26 +0545 Subject: [PATCH 15/39] fix: update vote context workflow --- .../VoteContext/VoteContextCheckResult.tsx | 7 +- .../VoteContext/VoteContextChoice.tsx | 2 +- .../VoteContext/VoteContextGovTool.tsx | 33 +------- .../VoteContext/VoteContextModal.tsx | 3 +- .../VoteContextStoringInformation.tsx | 1 + .../VoteContext/VoteContextWrapper.tsx | 81 +++++++++---------- .../src/hooks/forms/useVoteContextForm.tsx | 4 +- govtool/frontend/src/i18n/locales/en.json | 1 + 8 files changed, 51 insertions(+), 81 deletions(-) diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextCheckResult.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextCheckResult.tsx index 2e5ab3def..7d23df7c3 100644 --- a/govtool/frontend/src/components/organisms/VoteContext/VoteContextCheckResult.tsx +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextCheckResult.tsx @@ -48,7 +48,10 @@ export const VoteContextCheckResult = ({ {errorMessage ? "Data validation failed" : "Success"} - {errorMessage ?? "Data check has been successful"} + {errorMessage ?? "GovTool has processed has your rationale"} + + + {errorMessage ?? "You can now proceed to vote."} {!errorMessage ? ( - -
); }; diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextModal.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextModal.tsx index 878a19d42..7913f6196 100644 --- a/govtool/frontend/src/components/organisms/VoteContext/VoteContextModal.tsx +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextModal.tsx @@ -134,11 +134,10 @@ const VoteContextFlow = ({ )} {step === 4 && ( diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextStoringInformation.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextStoringInformation.tsx index 953528fba..45bb80ba7 100644 --- a/govtool/frontend/src/components/organisms/VoteContext/VoteContextStoringInformation.tsx +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextStoringInformation.tsx @@ -49,6 +49,7 @@ export const VoteContextStoringInformation = ({ onContinue={validateURL} isContinueDisabled={isContinueDisabled} onCancel={onCancel} + useSubmitLabel > {t("createGovernanceAction.storingInformationTitle")} diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextWrapper.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextWrapper.tsx index a7f48f99d..b86e5c2b5 100644 --- a/govtool/frontend/src/components/organisms/VoteContext/VoteContextWrapper.tsx +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextWrapper.tsx @@ -7,10 +7,10 @@ import { Button } from "@atoms"; type VoteContextWrapperProps = { onContinue?: () => void; isContinueDisabled?: boolean; - showCancelButton? : boolean; - onCancel: () => void; - showContinueButton?: boolean; - showAllButtons? : boolean; + onCancel? : () => void; + hideAllBtn? : boolean; + useBackLabel? : boolean; + useSubmitLabel? : boolean; }; export const VoteContextWrapper: FC< @@ -19,10 +19,10 @@ export const VoteContextWrapper: FC< onContinue, isContinueDisabled, onCancel, - showCancelButton = true, children, - showContinueButton = true, - showAllButtons = true + hideAllBtn = false, + useBackLabel = false, + useSubmitLabel = false }) => { const { isMobile } = useScreenDimension(); const { t } = useTranslation(); @@ -39,44 +39,35 @@ export const VoteContextWrapper: FC< {children} { - showAllButtons && - - { - showCancelButton && - - } - {showContinueButton && ( - - )} - + !hideAllBtn && + + + + } ); diff --git a/govtool/frontend/src/hooks/forms/useVoteContextForm.tsx b/govtool/frontend/src/hooks/forms/useVoteContextForm.tsx index acd8b0a96..e53fb168c 100644 --- a/govtool/frontend/src/hooks/forms/useVoteContextForm.tsx +++ b/govtool/frontend/src/hooks/forms/useVoteContextForm.tsx @@ -79,11 +79,11 @@ export const useVoteContextForm = ( } catch (error: any) { if (Object.values(MetadataValidationStatus).includes(error)) { if (setErrorMessage) setErrorMessage(error); - if (setStep) setStep(4); + if (setStep) setStep(5); } } finally { if (setSavedHash) setSavedHash(hash); - if (setStep) setStep(4); + if (setStep) setStep(5); } }, [hash], diff --git a/govtool/frontend/src/i18n/locales/en.json b/govtool/frontend/src/i18n/locales/en.json index f7a54695e..5ae5f1609 100644 --- a/govtool/frontend/src/i18n/locales/en.json +++ b/govtool/frontend/src/i18n/locales/en.json @@ -260,6 +260,7 @@ "learnMore" : "Learn more", "optionalDownloadAndStoreMetadataFile": "Optional: Download and store a backup copy of your metadata file", "rePinYourFileToIPFS": "Re-pin your file to IPFS", + "oneMomentPlease" : "One Moment please", "modals": { "submitTransactionSuccess": { "message": "Your Governance Action may take a little time to submit to the chain.", From 618205fb23cd9b47ab18aa52704ed018eae62e51 Mon Sep 17 00:00:00 2001 From: joseph rana Date: Tue, 29 Jul 2025 14:23:43 +0545 Subject: [PATCH 16/39] feat: validate and render vote context --- .../components/molecules/VoteActionForm.tsx | 60 +++++++++++-------- .../queries/useGetVoteContextTextFromFile.ts | 33 ++++++---- govtool/frontend/src/i18n/locales/en.json | 2 +- .../frontend/src/models/metadataValidation.ts | 1 + .../requests/getVoteContextTextFromFile.ts | 20 ++++--- 5 files changed, 72 insertions(+), 44 deletions(-) diff --git a/govtool/frontend/src/components/molecules/VoteActionForm.tsx b/govtool/frontend/src/components/molecules/VoteActionForm.tsx index 9bc79a797..4ce7a28d9 100644 --- a/govtool/frontend/src/components/molecules/VoteActionForm.tsx +++ b/govtool/frontend/src/components/molecules/VoteActionForm.tsx @@ -36,7 +36,7 @@ export const VoteActionForm = ({ useState(false); const { voter } = useGetVoterInfo(); - const { voteContextText } = useGetVoteContextTextFromFile(voteContextUrl); + const { voteContextText } = useGetVoteContextTextFromFile(voteContextUrl , voteContextHash); const { isMobile, screenWidth } = useScreenDimension(); const { openModal } = useModal(); @@ -61,6 +61,7 @@ export const VoteActionForm = ({ if (previousVote?.vote) { setValue("vote", previousVote.vote); setIsVoteSubmitted(true); + } }, [previousVote?.vote, setValue, setIsVoteSubmitted]); @@ -68,33 +69,36 @@ export const VoteActionForm = ({ if (previousVote?.url) { setVoteContextUrl(previousVote.url); } + if (previousVote?.metadataHash) { + setVoteContextHash(previousVote.metadataHash); + } }, [previousVote?.url, setVoteContextUrl]); - + const renderCancelButton = useMemo( () => ( ), [previousVote?.vote, setValue], ); - + const renderChangeVoteButton = useMemo( () => ( )} - { - !voteContextText && - - {t("optional")} - - } - - {voteContextText - ? t("govActions.yourVoteRationale") - : t("govActions.youCanProvideContext") - } - {voteContextText && ( )} - @@ -369,7 +329,7 @@ export const VoteActionForm = ({ !voteContextHash)) } isLoading={isVoteLoading} - onClick={confirmVote} + onClick={handleVoteClick} size="extraLarge" > {t("govActions.vote")} diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextCheckResult.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextCheckResult.tsx index 7d23df7c3..d4f18a340 100644 --- a/govtool/frontend/src/components/organisms/VoteContext/VoteContextCheckResult.tsx +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextCheckResult.tsx @@ -67,7 +67,7 @@ export const VoteContextCheckResult = ({ }} variant="contained" > - {t("govActions.goToVote")} + {t("govActions.submitVote")} ) : ( void; + vote?: Vote; + confirmVote: ( + vote?: Vote, + url?: string, + hash?: string | null, + ) => void; }; export const VoteContextModal = () => { @@ -35,7 +42,7 @@ export const VoteContextModal = () => { const { getValues } = methods; const submitVoteContext = () => { - if (state && savedHash) { + if (state) { state.onSubmit( getValues("storingURL"), savedHash, @@ -74,7 +81,12 @@ export const VoteContextModal = () => { /> )} {step === 1 && ( - + {})} + vote={state?.vote} + /> )} diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextText.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextText.tsx index 3f40d7622..e24f4f58b 100644 --- a/govtool/frontend/src/components/organisms/VoteContext/VoteContextText.tsx +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextText.tsx @@ -5,16 +5,25 @@ import { Typography } from "@atoms"; import { VoteContextWrapper } from "@organisms"; import { useTranslation, useVoteContextForm } from "@/hooks"; import { ControlledField } from ".."; +import { Vote } from "@/models"; type VoteContextTextProps = { setStep: Dispatch>; onCancel: () => void; + confirmVote: ( + vote?: Vote, + url?: string, + hash?: string | null, + ) => void; + vote?: Vote; }; const MAX_LENGTH = 10000; export const VoteContextText = ({ setStep, onCancel, + confirmVote, + vote, }: VoteContextTextProps) => { const { t } = useTranslation(); @@ -26,10 +35,6 @@ export const VoteContextText = ({ name: "voteContextText", placeholder: t("govActions.provideContext"), rules: { - required: { - value: true, - message: t("createGovernanceAction.fields.validations.required"), - }, maxLength: { value: MAX_LENGTH, message: t("createGovernanceAction.fields.validations.maxLength", { @@ -44,6 +49,10 @@ export const VoteContextText = ({ onContinue={() => setStep(2)} isContinueDisabled={isContinueDisabled} onCancel={onCancel} + onSkip={() => confirmVote(vote)} + continueLabel={ + isContinueDisabled ? t("govActions.skip") : t("govActions.continue") + } > void; isContinueDisabled?: boolean; - onCancel? : () => void; - hideAllBtn? : boolean; - useBackLabel? : boolean; - useSubmitLabel? : boolean; + onCancel?: () => void; + hideAllBtn?: boolean; + useBackLabel?: boolean; + useSubmitLabel?: boolean; + onSkip?: () => void; + continueLabel?: string; }; export const VoteContextWrapper: FC< @@ -22,7 +24,9 @@ export const VoteContextWrapper: FC< children, hideAllBtn = false, useBackLabel = false, - useSubmitLabel = false + useSubmitLabel = false, + onSkip, + continueLabel, }) => { const { isMobile } = useScreenDimension(); const { t } = useTranslation(); @@ -59,13 +63,21 @@ export const VoteContextWrapper: FC< } diff --git a/govtool/frontend/src/hooks/forms/useVoteActionForm.tsx b/govtool/frontend/src/hooks/forms/useVoteActionForm.tsx index 343ff146f..8c2891da5 100644 --- a/govtool/frontend/src/hooks/forms/useVoteActionForm.tsx +++ b/govtool/frontend/src/hooks/forms/useVoteActionForm.tsx @@ -7,7 +7,7 @@ import { useLocation, useNavigate, useParams } from "react-router-dom"; import { PATHS } from "@consts"; import { useCardano, useSnackbar } from "@context"; import { useWalletErrorModal } from "@hooks"; -import { ProposalVote } from "@/models"; +import { ProposalVote, Vote } from "@/models"; export interface VoteActionFormValues { vote: string; @@ -72,19 +72,23 @@ export const useVoteActionForm = ({ !areFormErrors; const confirmVote = useCallback( - async (values: VoteActionFormValues) => { - if (!canVote) return; + async ( + vote?: Vote, + url?: string, + hash?: string | null, + ) => { + if (!canVote || !vote) return; setIsLoading(true); - const urlSubmitValue = voteContextUrl ?? ""; - const hashSubmitValue = voteContextHash ?? ""; + const urlSubmitValue = url ?? ""; + const hashSubmitValue = hash ?? ""; try { const isPendingTx = isPendingTransaction(); if (isPendingTx) return; const votingBuilder = await buildVote( - values.vote, + vote, txHash, index, urlSubmitValue, @@ -116,7 +120,7 @@ export const useVoteActionForm = ({ ); return { - confirmVote: handleSubmit(confirmVote), + confirmVote, setValue, vote, registerInput, diff --git a/govtool/frontend/src/i18n/locales/en.json b/govtool/frontend/src/i18n/locales/en.json index 7411317c2..fde58d430 100644 --- a/govtool/frontend/src/i18n/locales/en.json +++ b/govtool/frontend/src/i18n/locales/en.json @@ -468,6 +468,9 @@ "cip129GovernanceActionId": "Governance Action ID:", "governanceActionType": "Governance Action Type:", "goToVote": "Go to Vote", + "submitVote": "Submit Vote", + "skip": "Skip", + "continue": "Continue", "membersToBeRemovedFromTheCommittee": "Members to be removed from the Committee", "membersToBeAddedToTheCommittee": "Members to be added to the Committee", "changeToTermsOfExistingMembers": "Change to terms of existing members", From c6112f28bee33faf138943858d1046a458dc148c Mon Sep 17 00:00:00 2001 From: joseph rana Date: Tue, 29 Jul 2025 19:49:49 +0545 Subject: [PATCH 18/39] feat: add edit vote context --- .../components/molecules/VoteActionForm.tsx | 118 ++++++++++++------ .../VoteContext/VoteContextModal.tsx | 3 + .../organisms/VoteContext/VoteContextText.tsx | 3 + govtool/frontend/src/i18n/locales/en.json | 1 + 4 files changed, 87 insertions(+), 38 deletions(-) diff --git a/govtool/frontend/src/components/molecules/VoteActionForm.tsx b/govtool/frontend/src/components/molecules/VoteActionForm.tsx index 669467333..afed1a452 100644 --- a/govtool/frontend/src/components/molecules/VoteActionForm.tsx +++ b/govtool/frontend/src/components/molecules/VoteActionForm.tsx @@ -3,7 +3,7 @@ import { Box } from "@mui/material"; import { Trans } from "react-i18next"; import { Button, Radio, Typography } from "@atoms"; -import { orange } from "@consts"; +import { fadedPurple } from "@/consts"; import { useModal } from "@context"; import { useScreenDimension, @@ -60,9 +60,11 @@ export const VoteActionForm = ({ setVoteContextUrl(url); setVoteContextHash(hash ?? undefined); confirmVote(vote as Vote, url, hash); + setVoteContextData(url , hash); }, vote: vote as Vote, confirmVote, + previousRationale : voteContextText } satisfies VoteContextModalState, }); }; @@ -126,6 +128,10 @@ export const VoteActionForm = ({ ), [confirmVote, areFormErrors, vote, isVoteLoading], ); + + useEffect(()=>{ + console.log(previousVote?.metadataHash , voteContextHash) + } , [previousVote?.metadataHash , voteContextHash]) return ( )} {voteContextText && ( + <> + {t("govActions.yourVoteRationale")} + {voteContextText && ( + - {voteContextText} - - + {t("showMore")} + + + + )} + + )} + + )} + - - {t("govActions.selectDifferentOption")} - - {previousVote?.vote && previousVote?.vote !== vote ? ( + { + voteContextText && ( + + ) + } + {previousVote?.vote && previousVote?.vote !== vote ? ( void; vote?: Vote; + previousRationale?: string confirmVote: ( vote?: Vote, url?: string, hash?: string | null, ) => void; + // onRationaleChange : () }; export const VoteContextModal = () => { @@ -86,6 +88,7 @@ export const VoteContextModal = () => { onCancel={closeModal} confirmVote={state?.confirmVote ?? (() => {})} vote={state?.vote} + previousRationale={state?.previousRationale} /> )} diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextText.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextText.tsx index e24f4f58b..4d852b129 100644 --- a/govtool/frontend/src/components/organisms/VoteContext/VoteContextText.tsx +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextText.tsx @@ -16,6 +16,7 @@ type VoteContextTextProps = { hash?: string | null, ) => void; vote?: Vote; + previousRationale? : string }; const MAX_LENGTH = 10000; @@ -24,6 +25,7 @@ export const VoteContextText = ({ onCancel, confirmVote, vote, + previousRationale }: VoteContextTextProps) => { const { t } = useTranslation(); @@ -82,6 +84,7 @@ export const VoteContextText = ({ isModifiedLayout maxLength={MAX_LENGTH} data-testid="provide-context-input" + defaultValue={previousRationale} /> ); diff --git a/govtool/frontend/src/i18n/locales/en.json b/govtool/frontend/src/i18n/locales/en.json index fde58d430..85726ec6b 100644 --- a/govtool/frontend/src/i18n/locales/en.json +++ b/govtool/frontend/src/i18n/locales/en.json @@ -454,6 +454,7 @@ "changeYourVote": "Change your vote", "chooseHowToVote": "Choose how you want to vote:", "yourVoteRationale": "Your Vote Rationale", + "editRationale" : "Edit Rationale", "dataMissing": "Data Missing", "dataMissingTooltipExplanation": "Please click “View Details” for more information.", "details": "Governance Details:", From 982cdb23e358d41c751d5d16a21ad9135c05a6e3 Mon Sep 17 00:00:00 2001 From: Sudip Bhattarai Date: Tue, 29 Jul 2025 21:25:43 +0545 Subject: [PATCH 19/39] Use single button to change vote or rationale --- .../components/molecules/VoteActionForm.tsx | 29 +++++-------------- .../organisms/VoteContext/VoteContextText.tsx | 2 +- .../VoteContext/VoteContextWrapper.tsx | 3 ++ govtool/frontend/src/i18n/locales/en.json | 10 ++++--- 4 files changed, 17 insertions(+), 27 deletions(-) diff --git a/govtool/frontend/src/components/molecules/VoteActionForm.tsx b/govtool/frontend/src/components/molecules/VoteActionForm.tsx index afed1a452..57811af54 100644 --- a/govtool/frontend/src/components/molecules/VoteActionForm.tsx +++ b/govtool/frontend/src/components/molecules/VoteActionForm.tsx @@ -331,24 +331,9 @@ export const VoteActionForm = ({ )} - + - { - voteContextText && ( - - ) - } {previousVote?.vote && previousVote?.vote !== vote ? ( - {t("govActions.vote")} + {previousVote?.vote && previousVote?.vote === vote + ? t("govActions.changeRationale") + : t("govActions.vote")} )} diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextText.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextText.tsx index 4d852b129..322df0050 100644 --- a/govtool/frontend/src/components/organisms/VoteContext/VoteContextText.tsx +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextText.tsx @@ -53,7 +53,7 @@ export const VoteContextText = ({ onCancel={onCancel} onSkip={() => confirmVote(vote)} continueLabel={ - isContinueDisabled ? t("govActions.skip") : t("govActions.continue") + isContinueDisabled ? t("govActions.voting.voteWithoutMetadata") : t("govActions.voting.continue") } > void; continueLabel?: string; + disableNext?: boolean; }; export const VoteContextWrapper: FC< @@ -27,6 +28,7 @@ export const VoteContextWrapper: FC< useSubmitLabel = false, onSkip, continueLabel, + disableNext = false, }) => { const { isMobile } = useScreenDimension(); const { t } = useTranslation(); @@ -63,6 +65,7 @@ export const VoteContextWrapper: FC< ) : ( {setStep(2)}} onContinue = {() => {setStep(5)}} useBackLabel + isContinueDisabled={!apiResponse} > {t("createGovernanceAction.rationalePinnedToIPFS")} diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextStoringInformation.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextStoringInformation.tsx index 45bb80ba7..47f3b86e9 100644 --- a/govtool/frontend/src/components/organisms/VoteContext/VoteContextStoringInformation.tsx +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextStoringInformation.tsx @@ -49,6 +49,7 @@ export const VoteContextStoringInformation = ({ onContinue={validateURL} isContinueDisabled={isContinueDisabled} onCancel={onCancel} + isVoteWithMetadata useSubmitLabel > diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextTerms.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextTerms.tsx index 07423d052..672623a5c 100644 --- a/govtool/frontend/src/components/organisms/VoteContext/VoteContextTerms.tsx +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextTerms.tsx @@ -26,6 +26,7 @@ export const VoteContextTerms = ({ setStep, onCancel }: StoreDataInfoProps) => { onContinue={() => setStep(4)} isContinueDisabled={isContinueDisabled} onCancel={onCancel} + isVoteWithMetadata > {t("createGovernanceAction.storeDataTitle")} diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextText.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextText.tsx index 8fc2bcae2..28e19cdff 100644 --- a/govtool/frontend/src/components/organisms/VoteContext/VoteContextText.tsx +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextText.tsx @@ -65,8 +65,7 @@ export const VoteContextText = ({ onCancel={onCancel} onSkip={() => confirmVote(vote)} continueLabel={buttonLabel} - isChangeVote={previousRationale !== undefined && previousRationale !== null} - isRationaleChanged={isRationaleChanged} + isContinueDisabled={(previousRationale !== undefined && previousRationale !== null ) && !isRationaleChanged} > void; continueLabel?: string; - isChangeVote?: boolean; - isRationaleChanged?:boolean; isApiLoading?:boolean; + isContinueDisabled?:boolean }; export const VoteContextWrapper: FC< @@ -30,8 +29,7 @@ export const VoteContextWrapper: FC< useSubmitLabel = false, onSkip, continueLabel, - isChangeVote = false, - isRationaleChanged=true, + isContinueDisabled }) => { const { isMobile } = useScreenDimension(); const { t } = useTranslation(); @@ -67,7 +65,7 @@ export const VoteContextWrapper: FC< ), [previousVote?.vote, setValue], ); - + const renderChangeVoteButton = useMemo( () => ( - + > + {t("showMore")} + + + )} - + )} - + )} - - + - {previousVote?.vote && previousVote?.vote !== vote ? ( + {previousVote?.vote && previousVote?.vote !== vote ? ( handleVoteClick(false)} + onClick={() => handleVoteClick(false)} size="extraLarge" > {previousVote?.vote && previousVote?.vote === vote diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextChoice.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextChoice.tsx index 3b4119d46..a108b3261 100644 --- a/govtool/frontend/src/components/organisms/VoteContext/VoteContextChoice.tsx +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextChoice.tsx @@ -14,7 +14,6 @@ type VoteContextChoiceProps = { setJsonldContent: Dispatch>; setMetadataHash: Dispatch>; generateMetadata: () => Promise<{ jsonld: NodeObject; jsonHash: string }>; - onCancel: () => void; }; export const VoteContextChoice = ({ @@ -23,7 +22,6 @@ export const VoteContextChoice = ({ setJsonldContent, setMetadataHash, generateMetadata, - onCancel, }: VoteContextChoiceProps) => { const { t } = useTranslation(); const { isMobile } = useScreenDimension(); @@ -44,13 +42,13 @@ export const VoteContextChoice = ({ }; return ( - - - {t("createGovernanceAction.storingOptionsForYourVoterRationale")} - - + + {t("createGovernanceAction.storingOptionsForYourVoterRationale")} + + + {t("createGovernanceAction.learnMoreAboutStoringInformation")} + + + + {t("createGovernanceAction.chooseDataStorageOption")} + + + + - - + {t("createGovernanceAction.govToolPinsDataToIPFS")} + + + ); }; diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextGovTool.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextGovTool.tsx index 8050fe30a..152464a09 100644 --- a/govtool/frontend/src/components/organisms/VoteContext/VoteContextGovTool.tsx +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextGovTool.tsx @@ -7,11 +7,10 @@ import { postIpfs } from "@services"; import { downloadTextFile, openInNewTab } from "@utils"; import { NodeObject } from "jsonld"; import { UseFormSetValue } from "react-hook-form"; -import { VoteContextFormValues } from "@hooks"; +import { VoteContextFormValues, useTranslation } from "@hooks"; import { LINKS } from "@/consts/links"; import { ICONS } from "@/consts/icons"; import { primaryBlue } from "@/consts"; -import { useTranslation } from "@hooks"; interface PostIpfsResponse { ipfsCid: string; @@ -20,7 +19,6 @@ interface PostIpfsResponse { type VoteContextGovToolProps = { setStep: Dispatch>; setSavedHash: Dispatch>; - submitVoteContext: () => void; jsonldContent: NodeObject | null; metadataHash: string | null; setValue: UseFormSetValue; @@ -37,10 +35,9 @@ export const VoteContextGovTool = ({ const [uploadInitiated, setUploadInitiated] = useState(false); // New state to track upload const { t } = useTranslation(); - const openLink = () => openInNewTab(LINKS.STORING_INFORMATION_OFFLINE); - const { mutate, isLoading , isError } = useMutation({ + const { mutate, isLoading, isError } = useMutation({ mutationFn: postIpfs, onSuccess: (data) => { const ipfsUrl = `ipfs://${data.ipfsCid}`; @@ -65,13 +62,13 @@ export const VoteContextGovTool = ({ return ( {setStep(2)}} - onContinue = {() => {setStep(5)}} + onCancel={() => { setStep(2); }} + onContinue={() => { setStep(5); }} useBackLabel isContinueDisabled={!apiResponse || isError} isVoteWithMetadata > - + {t("createGovernanceAction.rationalePinnedToIPFS")} ) : apiResponse ? ( <> - + {t("createGovernanceAction.optionalDownloadAndStoreMetadataFile")} - + {t("createGovernanceAction.rePinYourFileToIPFS")} - - {apiResponse.ipfsCid ? ( - - IPFS URI: {`https://ipfs.io/ipfs/${apiResponse.ipfsCid}`} - + }, }} + variant="body1" + > + {apiResponse.ipfsCid ? ( + + IPFS URI: {`https://ipfs.io/ipfs/${apiResponse.ipfsCid}`} + ) : ( "[URI]" )} - + - ) : ( + ) : ( {t("createGovernanceAction.uploadingToIPFS")} diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextModal.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextModal.tsx index a9f77d65a..4e95affa3 100644 --- a/govtool/frontend/src/components/organisms/VoteContext/VoteContextModal.tsx +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextModal.tsx @@ -1,5 +1,5 @@ import { Dispatch, SetStateAction, useState } from "react"; -import { useForm, FormProvider } from "react-hook-form"; +import { useForm, FormProvider, UseFormReturn } from "react-hook-form"; import { ModalWrapper } from "@atoms"; import { useModal } from "@context"; @@ -13,7 +13,6 @@ import { } from "@organisms"; import { NodeObject } from "jsonld"; import { VoteContextFormValues, useVoteContextForm } from "@hooks"; -import { UseFormReturn } from "react-hook-form"; import { Vote } from "@/models"; export type VoteContextModalState = { @@ -144,7 +143,6 @@ const VoteContextFlow = ({ setJsonldContent={setJsonldContent} setMetadataHash={setMetadataHash} generateMetadata={generateMetadata} - onCancel={onCancel} /> )} {step === 3 && storeDataYourself && ( @@ -157,7 +155,7 @@ const VoteContextFlow = ({ submitVoteContext={submitVoteContext} jsonldContent={jsonldContent} metadataHash={metadataHash} - setValue={methods.setValue} + setValue={methods.setValue} /> )} {step === 4 && ( diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextTerms.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextTerms.tsx index 672623a5c..beadd03ca 100644 --- a/govtool/frontend/src/components/organisms/VoteContext/VoteContextTerms.tsx +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextTerms.tsx @@ -28,7 +28,7 @@ export const VoteContextTerms = ({ setStep, onCancel }: StoreDataInfoProps) => { onCancel={onCancel} isVoteWithMetadata > - + {t("createGovernanceAction.storeDataTitle")} { - console.log({"currentRationale":currentRationale,previousRationale:previousRationale}) - return currentRationale !== previousRationale; - }, [currentRationale, previousRationale]); + const isRationaleChanged = useMemo(() => currentRationale !== previousRationale, + [currentRationale, previousRationale]); const buttonLabel = useMemo(() => { if (currentRationale === "") { @@ -57,7 +55,6 @@ export const VoteContextText = ({ }, }, }; - console.log("Previous rationale",previousRationale) return ( setStep(2)} @@ -65,7 +62,8 @@ export const VoteContextText = ({ onCancel={onCancel} onSkip={() => confirmVote(vote)} continueLabel={buttonLabel} - isContinueDisabled={(previousRationale !== undefined && previousRationale !== null ) && !isRationaleChanged} + isContinueDisabled={(previousRationale !== undefined && previousRationale !== null) + && !isRationaleChanged} > void; continueLabel?: string; - isApiLoading?:boolean; isContinueDisabled?:boolean }; @@ -47,42 +46,40 @@ export const VoteContextWrapper: FC< { !hideAllBtn && - - + - + : t("continue"))} + + } ); diff --git a/govtool/frontend/src/hooks/forms/useVoteActionForm.tsx b/govtool/frontend/src/hooks/forms/useVoteActionForm.tsx index 7df909089..8d25e7804 100644 --- a/govtool/frontend/src/hooks/forms/useVoteActionForm.tsx +++ b/govtool/frontend/src/hooks/forms/useVoteActionForm.tsx @@ -38,8 +38,6 @@ type Props = { export const useVoteActionForm = ({ previousVote, - voteContextHash, - voteContextUrl, closeModal, }: Props) => { const [isLoading, setIsLoading] = useState(false); @@ -54,7 +52,6 @@ export const useVoteActionForm = ({ const { control, - handleSubmit, formState: { errors, isDirty }, setValue, register: registerInput, @@ -73,52 +70,55 @@ export const useVoteActionForm = ({ index !== null && !areFormErrors; - const confirmVote = useCallback( - async ( - vote?: Vote, - url?: string, - hash?: string | null, - ) => { - if (!canVote || !vote) return; - - setIsLoading(true); - - const urlSubmitValue = url ?? ""; - const hashSubmitValue = hash ?? ""; - - try { - const isPendingTx = isPendingTransaction(); - if (isPendingTx) return; - const votingBuilder = await buildVote( - vote, - txHash, - index, - urlSubmitValue, - hashSubmitValue, - ); - const result = await buildSignSubmitConwayCertTx({ - votingBuilder, - type: "vote", - resourceId: txHash + index, - }); - if (result) { - addSuccessAlert("Vote submitted"); - navigate(PATHS.dashboardGovernanceActions, { - state: { - isVotedListOnLoad: !!previousVote?.vote, - }, - }); - closeModal(); - } - } catch (error) { - openWalletErrorModal({ - error, - dataTestId: "vote-transaction-error-modal", + const confirmVote = useCallback( + async ( + voteValue?: Vote, + url?: string, + hashValue?: string | null, + ) => { + if (!canVote || !voteValue) return; + + setIsLoading(true); + + const urlSubmitValue = url ?? ""; + const hashSubmitValue = hashValue ?? ""; + + try { + const isPendingTx = isPendingTransaction(); + if (isPendingTx) return; + + const votingBuilder = await buildVote( + voteValue, + txHash, + index, + urlSubmitValue, + hashSubmitValue, + ); + + const result = await buildSignSubmitConwayCertTx({ + votingBuilder, + type: "vote", + resourceId: txHash + index, + }); + + if (result) { + addSuccessAlert("Vote submitted"); + navigate(PATHS.dashboardGovernanceActions, { + state: { + isVotedListOnLoad: !!previousVote?.vote, + }, }); - } finally { - setIsLoading(false); + closeModal(); } - }, + } catch (error) { + openWalletErrorModal({ + error, + dataTestId: "vote-transaction-error-modal", + }); + } finally { + setIsLoading(false); + } + }, [buildVote, buildSignSubmitConwayCertTx, txHash, index, canVote], ); diff --git a/govtool/frontend/src/hooks/forms/useVoteContextForm.tsx b/govtool/frontend/src/hooks/forms/useVoteContextForm.tsx index 687f9a687..e53fb168c 100644 --- a/govtool/frontend/src/hooks/forms/useVoteContextForm.tsx +++ b/govtool/frontend/src/hooks/forms/useVoteContextForm.tsx @@ -35,7 +35,6 @@ export const useVoteContextForm = ( reset, } = useFormContext(); - const generateMetadata = useCallback(async () => { const { voteContextText } = getValues(); const body = await generateMetadataBody({ diff --git a/govtool/frontend/src/hooks/queries/useGetVoteContextTextFromFile.ts b/govtool/frontend/src/hooks/queries/useGetVoteContextTextFromFile.ts index 3dd7a78f5..440fb3275 100644 --- a/govtool/frontend/src/hooks/queries/useGetVoteContextTextFromFile.ts +++ b/govtool/frontend/src/hooks/queries/useGetVoteContextTextFromFile.ts @@ -5,14 +5,14 @@ import { QUERY_KEYS } from "@/consts"; import { useCardano } from "@/context"; import { useGetVoterInfo } from "."; -export const useGetVoteContextTextFromFile = (url: string | undefined , contextHash : string | undefined) => { +export const useGetVoteContextTextFromFile = (url: string | undefined, + contextHash : string | undefined) => { const { dRepID } = useCardano(); const { voter } = useGetVoterInfo(); - const { data, isLoading } = useQuery( [QUERY_KEYS.useGetVoteContextFromFile, url], - () => getVoteContextTextFromFile(url , contextHash), + () => getVoteContextTextFromFile(url, contextHash), { enabled: !!url && @@ -26,12 +26,11 @@ export const useGetVoteContextTextFromFile = (url: string | undefined , contextH if (data?.valid) { return { voteContextText, - isLoading - } + isLoading + }; } return { - voteContextText : undefined, + voteContextText: undefined, isLoading - } - + }; }; diff --git a/govtool/frontend/src/services/requests/getVoteContextTextFromFile.ts b/govtool/frontend/src/services/requests/getVoteContextTextFromFile.ts index 0e94a3ae0..32778cf8b 100644 --- a/govtool/frontend/src/services/requests/getVoteContextTextFromFile.ts +++ b/govtool/frontend/src/services/requests/getVoteContextTextFromFile.ts @@ -1,15 +1,16 @@ import { postValidate } from "./metadataValidation"; import { MetadataStandard } from "@/models"; -export const getVoteContextTextFromFile = async (url: string | undefined , contextHash : string | undefined) => { +export const getVoteContextTextFromFile = async (url: string | undefined, + contextHash : string | undefined) => { if (!url || !contextHash) { throw new Error("Missing Vote Context values"); } const response = await postValidate({ - "standard" : MetadataStandard.CIP100, - "url" : url, - "hash" : contextHash - }) - - return {valid : response.valid , metadata : response.metadata}; + standard: MetadataStandard.CIP100, + url, + hash: contextHash + }); + + return { valid: response.valid, metadata: response.metadata }; }; diff --git a/govtool/frontend/src/services/requests/postIpfs.ts b/govtool/frontend/src/services/requests/postIpfs.ts index 105065ed2..346dff3c4 100644 --- a/govtool/frontend/src/services/requests/postIpfs.ts +++ b/govtool/frontend/src/services/requests/postIpfs.ts @@ -1,7 +1,7 @@ import { API } from "../API"; export const postIpfs = async ({ content }: { content: string }) => { - const response = await API.post("/ipfs/upload", content,{ + const response = await API.post("/ipfs/upload", content, { headers: { "Content-Type": "text/plain;charset=utf-8" } From 4ad9f76be0bd618704c53c58b18ea35cb08c6e07 Mon Sep 17 00:00:00 2001 From: Sudip Bhattarai Date: Wed, 30 Jul 2025 11:52:10 +0545 Subject: [PATCH 26/39] fix: tsc lint error on voteContextModal --- .../src/components/organisms/VoteContext/VoteContextModal.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextModal.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextModal.tsx index 4e95affa3..8bd2582bf 100644 --- a/govtool/frontend/src/components/organisms/VoteContext/VoteContextModal.tsx +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextModal.tsx @@ -152,7 +152,6 @@ const VoteContextFlow = ({ Date: Wed, 30 Jul 2025 09:46:05 +0200 Subject: [PATCH 27/39] Fix/Disapearing GA list after 5 minutes later back from details --- govtool/frontend/src/hooks/queries/useGetProposalsQuery.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/govtool/frontend/src/hooks/queries/useGetProposalsQuery.ts b/govtool/frontend/src/hooks/queries/useGetProposalsQuery.ts index 27f349615..bee3cd39d 100644 --- a/govtool/frontend/src/hooks/queries/useGetProposalsQuery.ts +++ b/govtool/frontend/src/hooks/queries/useGetProposalsQuery.ts @@ -48,6 +48,7 @@ export const useGetProposalsQuery = ({ enabled, refetchOnWindowFocus: true, keepPreviousData: true, + cacheTime: Infinity, }, ); From 34db03fd225129f0d494cc63bfdc105742700cd4 Mon Sep 17 00:00:00 2001 From: Aaron Boyle Date: Wed, 30 Jul 2025 09:08:35 +0100 Subject: [PATCH 28/39] Revert "update to corrects empty line linting error" This reverts commit 5c8059e1b6a6548fa8b9741b65306f0a83b2e6d0. --- govtool/frontend/src/hooks/queries/useGetProposalsQuery.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/govtool/frontend/src/hooks/queries/useGetProposalsQuery.ts b/govtool/frontend/src/hooks/queries/useGetProposalsQuery.ts index da62db474..b0188e30c 100644 --- a/govtool/frontend/src/hooks/queries/useGetProposalsQuery.ts +++ b/govtool/frontend/src/hooks/queries/useGetProposalsQuery.ts @@ -22,6 +22,7 @@ const voterReady = // Only run the query if enabled externally and voter info is ready const shouldFetch = enabled && voterReady; + const fetchProposals = async (): Promise => { const allProposals = await Promise.all( filters.map((filter) => From 464ba4c01899ab1d6e1da396e9a7a85ec67c48f3 Mon Sep 17 00:00:00 2001 From: Aaron Boyle Date: Wed, 30 Jul 2025 09:09:17 +0100 Subject: [PATCH 29/39] Revert "Update useGetProposalsQuery.ts - 3978 - voterReady logic added" This reverts commit 2e9124451615d194c199ca668108878c0f5db3dd. --- .../src/hooks/queries/useGetProposalsQuery.ts | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/govtool/frontend/src/hooks/queries/useGetProposalsQuery.ts b/govtool/frontend/src/hooks/queries/useGetProposalsQuery.ts index b0188e30c..27f349615 100644 --- a/govtool/frontend/src/hooks/queries/useGetProposalsQuery.ts +++ b/govtool/frontend/src/hooks/queries/useGetProposalsQuery.ts @@ -10,19 +10,11 @@ export const useGetProposalsQuery = ({ filters = [], searchPhrase, sorting, - enabled = true, + enabled, }: GetProposalsArguments) => { const { dRepID } = useCardano(); const { voter } = useGetVoterInfo(); -// Determine if voter is ready to be used in the query -const voterReady = - !!dRepID && (voter?.isRegisteredAsDRep || voter?.isRegisteredAsSoleVoter); - -// Only run the query if enabled externally and voter info is ready -const shouldFetch = enabled && voterReady; - - const fetchProposals = async (): Promise => { const allProposals = await Promise.all( filters.map((filter) => @@ -53,7 +45,7 @@ const shouldFetch = enabled && voterReady; ], fetchProposals, { - enabled: shouldFetch, + enabled, refetchOnWindowFocus: true, keepPreviousData: true, }, From f4c9a683de95da3cbb49a6dfa3f06a3147ba7e7e Mon Sep 17 00:00:00 2001 From: Aaron Boyle Date: Wed, 30 Jul 2025 09:10:28 +0100 Subject: [PATCH 30/39] Revert "Update dataActionsBar.tsx - lint check fix" This reverts commit a56f5a55a8836c7ad2a3cd6b2a63f2d9417f8b7a. --- govtool/frontend/src/context/dataActionsBar.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/govtool/frontend/src/context/dataActionsBar.tsx b/govtool/frontend/src/context/dataActionsBar.tsx index d596f5d2f..2c7de5c5b 100644 --- a/govtool/frontend/src/context/dataActionsBar.tsx +++ b/govtool/frontend/src/context/dataActionsBar.tsx @@ -8,6 +8,7 @@ import React, { useEffect, useMemo, FC, + useRef, } from "react"; import { useLocation } from "react-router-dom"; @@ -64,6 +65,7 @@ const DataActionsBarProvider: FC = ({ children }) => { setSearchText(""); setChosenFilters([]); setChosenSorting(""); + isAdjusting.current = false; }, []); const userMovedToDifferentAppArea = From b1f4fdd5623471a2ca1da2c3a607f3b54176e62d Mon Sep 17 00:00:00 2001 From: Aaron Boyle Date: Wed, 30 Jul 2025 09:10:40 +0100 Subject: [PATCH 31/39] Revert "Update dataActionsBar.tsx - empty list of GAs on back after idle time" This reverts commit 448b6b10adc27d5bbc6ca4b72ac379e1236f9068. --- govtool/frontend/src/context/dataActionsBar.tsx | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/govtool/frontend/src/context/dataActionsBar.tsx b/govtool/frontend/src/context/dataActionsBar.tsx index 2c7de5c5b..9ef913a26 100644 --- a/govtool/frontend/src/context/dataActionsBar.tsx +++ b/govtool/frontend/src/context/dataActionsBar.tsx @@ -42,7 +42,7 @@ interface ProviderProps { } const DataActionsBarProvider: FC = ({ children }) => { - const [isAdjusting, setIsAdjusting] = useState(false); + const isAdjusting = useRef(false); const [searchText, setSearchText] = useState(""); const debouncedSearchText = useDebounce(searchText.trim(), 300); const [filtersOpen, setFiltersOpen] = useState(false); @@ -79,11 +79,7 @@ const DataActionsBarProvider: FC = ({ children }) => { pathname.includes("governance_actions/category"); useEffect(() => { - setIsAdjusting(true); - - const timeout = setTimeout(() => { - setIsAdjusting(false); - }, 150); // Adjust delay if needed + isAdjusting.current = true; if ( (!pathname.includes("drep_directory") && @@ -93,8 +89,6 @@ const DataActionsBarProvider: FC = ({ children }) => { ) { resetState(); } - - return () => clearTimeout(timeout); }, [pathname, resetState]); useEffect(() => { @@ -103,7 +97,7 @@ const DataActionsBarProvider: FC = ({ children }) => { const contextValue = useMemo( () => ({ - isAdjusting, + isAdjusting: isAdjusting.current, chosenFilters, chosenFiltersLength: chosenFilters.length, chosenSorting, From dd102c0cdc14552cb0b58e79d936e759647fc963 Mon Sep 17 00:00:00 2001 From: joseph rana Date: Wed, 30 Jul 2025 15:43:38 +0545 Subject: [PATCH 32/39] fix: handle empty rationale states for first and revote --- .../src/components/molecules/VoteActionForm.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/govtool/frontend/src/components/molecules/VoteActionForm.tsx b/govtool/frontend/src/components/molecules/VoteActionForm.tsx index e839fffa1..2b3c8c947 100644 --- a/govtool/frontend/src/components/molecules/VoteActionForm.tsx +++ b/govtool/frontend/src/components/molecules/VoteActionForm.tsx @@ -36,7 +36,12 @@ export const VoteActionForm = ({ useState(false); const { voter } = useGetVoterInfo(); - const { voteContextText } = useGetVoteContextTextFromFile(voteContextUrl, voteContextHash); + const { voteContextText } = useGetVoteContextTextFromFile(voteContextUrl, voteContextHash) || {}; + + const finalVoteContextText = + ((previousVote != null || undefined) && !voteContextUrl && !voteContextHash) + ? "" + : voteContextText; const { isMobile } = useScreenDimension(); const { openModal, closeModal } = useModal(); @@ -64,7 +69,7 @@ export const VoteActionForm = ({ }, vote: vote as Vote, confirmVote, - previousRationale: isVoteChanged ? undefined : voteContextText + previousRationale: isVoteChanged ? undefined : finalVoteContextText } satisfies VoteContextModalState, }); }; @@ -242,7 +247,7 @@ export const VoteActionForm = ({ {t("govActions.showVotes")} )} - {voteContextText && ( + {finalVoteContextText && ( <> {t("govActions.yourVoteRationale")} - {voteContextText && ( + {finalVoteContextText && ( - {voteContextText} + {finalVoteContextText} {!showWholeVoteContext && ( From ed197ab585ceaeb1005e8a5ab57d698a045a59a2 Mon Sep 17 00:00:00 2001 From: joseph rana Date: Wed, 30 Jul 2025 16:59:13 +0545 Subject: [PATCH 33/39] fix: add error text display for invalid vote context --- .../components/molecules/VoteActionForm.tsx | 11 ++++++-- .../queries/useGetVoteContextTextFromFile.ts | 25 +++++++++++++------ govtool/frontend/src/i18n/locales/en.json | 1 + 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/govtool/frontend/src/components/molecules/VoteActionForm.tsx b/govtool/frontend/src/components/molecules/VoteActionForm.tsx index 2b3c8c947..25c1807d2 100644 --- a/govtool/frontend/src/components/molecules/VoteActionForm.tsx +++ b/govtool/frontend/src/components/molecules/VoteActionForm.tsx @@ -12,7 +12,7 @@ import { useGetVoteContextTextFromFile, } from "@hooks"; import { formatDisplayDate } from "@utils"; -import { fadedPurple } from "@/consts"; +import { errorRed, fadedPurple } from "@/consts"; import { ProposalData, ProposalVote, Vote } from "@/models"; import { VoteContextModalState, SubmittedVotesModalState } from "../organisms"; @@ -36,7 +36,8 @@ export const VoteActionForm = ({ useState(false); const { voter } = useGetVoterInfo(); - const { voteContextText } = useGetVoteContextTextFromFile(voteContextUrl, voteContextHash) || {}; + const { voteContextText, valid: voteContextValid = true } = + useGetVoteContextTextFromFile(voteContextUrl, voteContextHash) || {}; const finalVoteContextText = ((previousVote != null || undefined) && !voteContextUrl && !voteContextHash) @@ -247,6 +248,12 @@ export const VoteActionForm = ({ {t("govActions.showVotes")} )} + { + !voteContextValid && + + {t("govActions.invalidVoteContext")} + + } {finalVoteContextText && ( <> {t("govActions.yourVoteRationale")} diff --git a/govtool/frontend/src/hooks/queries/useGetVoteContextTextFromFile.ts b/govtool/frontend/src/hooks/queries/useGetVoteContextTextFromFile.ts index 440fb3275..7fc7f8f5c 100644 --- a/govtool/frontend/src/hooks/queries/useGetVoteContextTextFromFile.ts +++ b/govtool/frontend/src/hooks/queries/useGetVoteContextTextFromFile.ts @@ -23,14 +23,25 @@ export const useGetVoteContextTextFromFile = (url: string | undefined, const voteContextText = (data?.metadata as { comment?: string })?.comment || ""; - if (data?.valid) { + if (url === undefined || contextHash === undefined) { return { - voteContextText, - isLoading - }; - } - return { voteContextText: undefined, - isLoading + isLoading: false, + valid: true }; + } + if (data) { + if (data?.valid) { + return { + voteContextText, + isLoading, + valid: true + }; + } + return { + voteContextText: undefined, + isLoading, + valid: false + }; + } }; diff --git a/govtool/frontend/src/i18n/locales/en.json b/govtool/frontend/src/i18n/locales/en.json index 8cd3b74d8..a60dfb632 100644 --- a/govtool/frontend/src/i18n/locales/en.json +++ b/govtool/frontend/src/i18n/locales/en.json @@ -456,6 +456,7 @@ "changeYourVote": "Change your vote", "chooseHowToVote": "Choose how you want to vote:", "yourVoteRationale": "Your Vote Rationale", + "invalidVoteContext": "Invalid Vote Context", "dataMissing": "Data Missing", "dataMissingTooltipExplanation": "Please click “View Details” for more information.", "details": "Governance Details:", From 70b485bf7bbb6951be102403603cea2671d5c86f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 30 Jul 2025 19:13:47 +0000 Subject: [PATCH 34/39] chore: update @intersect.mbo/govtool-outcomes-pillar-ui to v1.5.5 --- govtool/frontend/package-lock.json | 8 ++++---- govtool/frontend/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/govtool/frontend/package-lock.json b/govtool/frontend/package-lock.json index fa0e37c66..5db72292f 100644 --- a/govtool/frontend/package-lock.json +++ b/govtool/frontend/package-lock.json @@ -13,7 +13,7 @@ "@emotion/styled": "^11.11.0", "@emurgo/cardano-serialization-lib-asmjs": "^14.1.1", "@hookform/resolvers": "^3.3.1", - "@intersect.mbo/govtool-outcomes-pillar-ui": "v1.5.4", + "@intersect.mbo/govtool-outcomes-pillar-ui": "v1.5.5", "@intersect.mbo/intersectmbo.org-icons-set": "^1.0.8", "@intersect.mbo/pdf-ui": "1.0.10-beta", "@mui/icons-material": "^5.14.3", @@ -3392,9 +3392,9 @@ } }, "node_modules/@intersect.mbo/govtool-outcomes-pillar-ui": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@intersect.mbo/govtool-outcomes-pillar-ui/-/govtool-outcomes-pillar-ui-1.5.4.tgz", - "integrity": "sha512-lg6+GmP6Be5P+jtSqHnpf+tHGhzJt23P0wj6PO9KVnAWtSnYgF/T/JZY82LI57+ayTVoEN01lQpTGROxJJF6rA==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@intersect.mbo/govtool-outcomes-pillar-ui/-/govtool-outcomes-pillar-ui-1.5.5.tgz", + "integrity": "sha512-m91glJAw3Hi/tw2S00EmyNByupaBuDw2g2h88RCAc3TdC6AHA2jkPzjmRVr21rl/gk9/p9S+GY0X+4kr4d6ULw==", "license": "ISC", "dependencies": { "@fontsource/poppins": "^5.0.14", diff --git a/govtool/frontend/package.json b/govtool/frontend/package.json index a4fa2c8d4..6c634a711 100644 --- a/govtool/frontend/package.json +++ b/govtool/frontend/package.json @@ -27,7 +27,7 @@ "@emotion/styled": "^11.11.0", "@emurgo/cardano-serialization-lib-asmjs": "^14.1.1", "@hookform/resolvers": "^3.3.1", - "@intersect.mbo/govtool-outcomes-pillar-ui": "v1.5.4", + "@intersect.mbo/govtool-outcomes-pillar-ui": "v1.5.5", "@intersect.mbo/intersectmbo.org-icons-set": "^1.0.8", "@intersect.mbo/pdf-ui": "1.0.10-beta", "@mui/icons-material": "^5.14.3", From 6c64ca88e4c3cc92a2d61070ee5bf673e754a6ff Mon Sep 17 00:00:00 2001 From: joseph rana Date: Thu, 31 Jul 2025 10:48:34 +0545 Subject: [PATCH 35/39] fix: handle ipfs link for vote context validation --- .../src/hooks/queries/useGetVoteContextTextFromFile.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/govtool/frontend/src/hooks/queries/useGetVoteContextTextFromFile.ts b/govtool/frontend/src/hooks/queries/useGetVoteContextTextFromFile.ts index 7fc7f8f5c..216f9ad39 100644 --- a/govtool/frontend/src/hooks/queries/useGetVoteContextTextFromFile.ts +++ b/govtool/frontend/src/hooks/queries/useGetVoteContextTextFromFile.ts @@ -10,6 +10,10 @@ export const useGetVoteContextTextFromFile = (url: string | undefined, const { dRepID } = useCardano(); const { voter } = useGetVoterInfo(); + if (url && url.startsWith("ipfs://")) { + url = url.replace("ipfs://", "https://ipfs.io/ipfs/"); + } + const { data, isLoading } = useQuery( [QUERY_KEYS.useGetVoteContextFromFile, url], () => getVoteContextTextFromFile(url, contextHash), From 8c7e3398c255fa6bbcb13d5b72158f8a4fbe70da Mon Sep 17 00:00:00 2001 From: Sudip Bhattarai Date: Thu, 31 Jul 2025 12:05:54 +0545 Subject: [PATCH 36/39] chore: fix haskell lint and formatting --- govtool/backend/src/VVA/API.hs | 71 +++++---- govtool/backend/src/VVA/API/Types.hs | 42 +++--- govtool/backend/src/VVA/Account.hs | 13 +- govtool/backend/src/VVA/AdaHolder.hs | 6 +- govtool/backend/src/VVA/Config.hs | 28 ++-- govtool/backend/src/VVA/DRep.hs | 85 ++++++----- govtool/backend/src/VVA/Ipfs.hs | 88 ++++++----- govtool/backend/src/VVA/Network.hs | 2 +- govtool/backend/src/VVA/Proposal.hs | 58 ++++---- govtool/backend/src/VVA/Transaction.hs | 4 +- govtool/backend/src/VVA/Types.hs | 198 +++++++++++++------------ 11 files changed, 312 insertions(+), 283 deletions(-) diff --git a/govtool/backend/src/VVA/API.hs b/govtool/backend/src/VVA/API.hs index cfbb35960..5185c3882 100644 --- a/govtool/backend/src/VVA/API.hs +++ b/govtool/backend/src/VVA/API.hs @@ -1,10 +1,10 @@ +{-# LANGUAGE DataKinds #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TypeOperators #-} {-# LANGUAGE ViewPatterns #-} -{-# LANGUAGE DataKinds #-} module VVA.API where @@ -13,9 +13,11 @@ import Control.Exception (throw, throwIO) import Control.Monad.Except (runExceptT, throwError) import Control.Monad.Reader -import Data.Aeson (Value(..), Array, decode, ToJSON, toJSON) +import Data.Aeson (Array, ToJSON, Value (..), decode, toJSON) import Data.Bool (Bool) -import Data.List (sortOn, sort) +import Data.ByteString.Lazy (ByteString) +import qualified Data.ByteString.Lazy as BSL +import Data.List (sort, sortOn) import qualified Data.Map as Map import Data.Maybe (Maybe (Nothing), catMaybes, fromMaybe, mapMaybe) import Data.Ord (Down (..)) @@ -23,38 +25,35 @@ import Data.Text hiding (any, drop, elem, filter, lengt import qualified Data.Text as Text import qualified Data.Text.Lazy as TL import qualified Data.Text.Lazy.Encoding as TL -import qualified Data.Vector as V +import Data.Time (TimeZone, localTimeToUTC) import Data.Time.LocalTime (TimeZone, getCurrentTimeZone) - +import qualified Data.Vector as V import Numeric.Natural (Natural) import Servant.API -import Servant.Server import Servant.Exception (Throws) +import Servant.Server + import System.Random (randomRIO) import Text.Read (readMaybe) +import VVA.Account as Account import qualified VVA.AdaHolder as AdaHolder import VVA.API.Types import VVA.Cache (cacheRequest) import VVA.Config import qualified VVA.DRep as DRep import qualified VVA.Epoch as Epoch +import qualified VVA.Ipfs as Ipfs import VVA.Network as Network -import VVA.Account as Account import qualified VVA.Proposal as Proposal import qualified VVA.Transaction as Transaction import qualified VVA.Types as Types import VVA.Types (App, AppEnv (..), - AppError (CriticalError, InternalError, ValidationError, AppIpfsError), + AppError (AppIpfsError, CriticalError, InternalError, ValidationError), CacheEnv (..)) -import Data.Time (TimeZone, localTimeToUTC) -import qualified VVA.Ipfs as Ipfs -import Data.ByteString.Lazy (ByteString) -import qualified Data.ByteString.Lazy as BSL -import Servant.Exception (Throws) type VVAApi = "ipfs" @@ -127,7 +126,7 @@ upload mFileName fileContentText = do throwError $ ValidationError "The uploaded file is larger than 500Kb" eIpfsHash <- liftIO $ Ipfs.ipfsUpload vvaPinataJwt fileName fileContent case eIpfsHash of - Left err -> throwError $ AppIpfsError err + Left err -> throwError $ AppIpfsError err Right ipfsHash -> return $ UploadResponse ipfsHash mapDRepType :: Types.DRepType -> DRepType @@ -188,9 +187,9 @@ drepList mSearchQuery statuses mSortMode mPage mPageSize = do viewLower = Text.toLower dRepRegistrationView hashLower = Text.toLower dRepRegistrationDRepHash in case dRepRegistrationType of - Types.SoleVoter -> + Types.SoleVoter -> searchLower == viewLower || searchLower == hashLower - Types.DRep -> + Types.DRep -> True @@ -318,13 +317,13 @@ getVotes :: App m => HexText -> [GovernanceActionType] -> Maybe GovernanceAction getVotes (unHexText -> dRepId) selectedTypes sortMode mSearch = do CacheEnv {dRepGetVotesCache} <- asks vvaCache (votes, proposals) <- cacheRequest dRepGetVotesCache dRepId $ DRep.getVotes dRepId [] - + let voteMapById = Map.fromList $ map (\vote -> (Types.voteGovActionId vote, vote)) votes - - processedProposals <- filter (isProposalSearchedFor mSearch) <$> + + processedProposals <- filter (isProposalSearchedFor mSearch) <$> mapSortAndFilterProposals selectedTypes sortMode proposals - + return [ VoteResponse { voteResponseVote = voteToResponse vote @@ -334,7 +333,7 @@ getVotes (unHexText -> dRepId) selectedTypes sortMode mSearch = do , let govActionId = unHexText (proposalResponseTxHash proposalResponse) <> "#" <> pack (show $ proposalResponseIndex proposalResponse) , Just vote <- [Map.lookup govActionId voteMapById] ] - + drepInfo :: App m => HexText -> m DRepInfoResponse drepInfo (unHexText -> dRepId) = do CacheEnv {dRepInfoCache} <- asks vvaCache @@ -365,15 +364,15 @@ drepInfo (unHexText -> dRepId) = do drepVotingPowerList :: App m => [Text] -> m [DRepVotingPowerListResponse] drepVotingPowerList identifiers = do CacheEnv {dRepVotingPowerListCache} <- asks vvaCache - + let cacheKey = Text.intercalate "," (sort identifiers) - - results <- cacheRequest dRepVotingPowerListCache cacheKey $ + + results <- cacheRequest dRepVotingPowerListCache cacheKey $ DRep.getDRepsVotingPowerList identifiers - + return $ map toDRepVotingPowerListResponse results where - toDRepVotingPowerListResponse Types.DRepVotingPowerList{..} = + toDRepVotingPowerListResponse Types.DRepVotingPowerList{..} = DRepVotingPowerListResponse { drepVotingPowerListResponseView = drepView , drepVotingPowerListResponseHashRaw = HexText drepHashRaw @@ -456,9 +455,9 @@ getProposal g@(GovActionId govActionTxHash govActionIndex) mDrepId' = do let mDrepId = unHexText <$> mDrepId' CacheEnv {getProposalCache} <- asks vvaCache proposal@Types.Proposal {proposalUrl, proposalDocHash} <- cacheRequest getProposalCache (unHexText govActionTxHash, govActionIndex) (Proposal.getProposal (unHexText govActionTxHash) govActionIndex) - + timeZone <- liftIO getCurrentTimeZone - + let proposalResponse = proposalToResponse timeZone proposal voteResponse <- case mDrepId of Nothing -> return Nothing @@ -478,20 +477,20 @@ getProposal g@(GovActionId govActionTxHash govActionIndex) mDrepId' = do getEnactedProposalDetails :: App m => Maybe GovernanceActionType -> m (Maybe EnactedProposalDetailsResponse) getEnactedProposalDetails maybeType = do let proposalType = maybe "HardForkInitiation" governanceActionTypeToText maybeType - + mDetails <- Proposal.getPreviousEnactedProposal proposalType - + let response = enactedProposalDetailsToResponse <$> mDetails - + return response where governanceActionTypeToText :: GovernanceActionType -> Text - governanceActionTypeToText actionType = + governanceActionTypeToText actionType = case actionType of HardForkInitiation -> "HardForkInitiation" - ParameterChange -> "ParameterChange" - _ -> "HardForkInitiation" - + ParameterChange -> "ParameterChange" + _ -> "HardForkInitiation" + enactedProposalDetailsToResponse :: Types.EnactedProposalDetails -> EnactedProposalDetailsResponse enactedProposalDetailsToResponse Types.EnactedProposalDetails{..} = EnactedProposalDetailsResponse @@ -513,7 +512,7 @@ getTransactionStatus (unHexText -> transactionId) = do return $ GetTransactionStatusResponse $ case status of Just value -> Just $ toJSON value Nothing -> Nothing - + throw500 :: App m => m () throw500 = throwError $ CriticalError "intentional system break for testing purposes" diff --git a/govtool/backend/src/VVA/API/Types.hs b/govtool/backend/src/VVA/API/Types.hs index 360f9be64..2b1badf27 100644 --- a/govtool/backend/src/VVA/API/Types.hs +++ b/govtool/backend/src/VVA/API/Types.hs @@ -205,7 +205,14 @@ instance ToParamSchema GovernanceActionType where & enum_ ?~ map toJSON (enumFromTo minBound maxBound :: [GovernanceActionType]) -data DRepSortMode = Random | VotingPower | RegistrationDate | Status deriving (Bounded, Enum, Eq, Generic, Read, Show) +data DRepSortMode = Random | VotingPower | RegistrationDate | Status deriving + ( Bounded + , Enum + , Eq + , Generic + , Read + , Show + ) instance FromJSON DRepSortMode where parseJSON (Aeson.String dRepSortMode) = pure $ fromJust $ readMaybe (Text.unpack dRepSortMode) @@ -406,7 +413,8 @@ data ProposalResponse } deriving (Generic, Show) -newtype ProposalAuthors = ProposalAuthors { getProposalAuthors :: Value } +newtype ProposalAuthors + = ProposalAuthors { getProposalAuthors :: Value } deriving newtype (Show) instance FromJSON ProposalAuthors where @@ -659,7 +667,7 @@ data DRepInfoResponse , dRepInfoResponseGivenName :: Maybe Text , dRepInfoResponseObjectives :: Maybe Text , dRepInfoResponseMotivations :: Maybe Text - , dRepInfoResponseQualifications :: Maybe Text + , dRepInfoResponseQualifications :: Maybe Text , dRepInfoResponseImageUrl :: Maybe Text , dRepInfoResponseImageHash :: Maybe HexText } @@ -906,7 +914,7 @@ 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 @@ -1011,11 +1019,11 @@ instance ToSchema DelegationResponse where data GetNetworkInfoResponse = GetNetworkInfoResponse - { getNetworkInfoResponseCurrentTime :: UTCTime - , getNetworkInfoResponseEpochNo :: Integer - , getNetworkInfoResponseBlockNo :: Integer - , getNetworkInfoResponseNetworkName :: Text - } + { getNetworkInfoResponseCurrentTime :: UTCTime + , getNetworkInfoResponseEpochNo :: Integer + , getNetworkInfoResponseBlockNo :: Integer + , getNetworkInfoResponseNetworkName :: Text + } deriveJSON (jsonOptions "getNetworkInfoResponse") ''GetNetworkInfoResponse @@ -1035,11 +1043,11 @@ instance ToSchema GetNetworkInfoResponse where data GetNetworkTotalStakeResponse = GetNetworkTotalStakeResponse - { getNetworkTotalStakeResponseTotalStakeControlledByDReps :: Integer - , getNetworkTotalStakeResponseTotalStakeControlledBySPOs :: Integer - , getNetworkTotalStakeResponseAlwaysAbstainVotingPower :: Integer - , getNetworkTotalStakeResponseAlwaysNoConfidenceVotingPower :: Integer - } + { getNetworkTotalStakeResponseTotalStakeControlledByDReps :: Integer + , getNetworkTotalStakeResponseTotalStakeControlledBySPOs :: Integer + , getNetworkTotalStakeResponseAlwaysAbstainVotingPower :: Integer + , getNetworkTotalStakeResponseAlwaysNoConfidenceVotingPower :: Integer + } deriveJSON (jsonOptions "getNetworkTotalStakeResponse") ''GetNetworkTotalStakeResponse @@ -1113,10 +1121,8 @@ data GetAccountInfoResponse deriving (Generic, Show) deriveJSON (jsonOptions "getAccountInfoResponse") ''GetAccountInfoResponse -data UploadResponse - = UploadResponse - { uploadResponseIpfsCid :: Text - } +newtype UploadResponse + = UploadResponse { uploadResponseIpfsCid :: Text } deriving (Generic, Show) deriveJSON (jsonOptions "uploadResponse") ''UploadResponse diff --git a/govtool/backend/src/VVA/Account.hs b/govtool/backend/src/VVA/Account.hs index 10aa92e52..5ce98ae78 100644 --- a/govtool/backend/src/VVA/Account.hs +++ b/govtool/backend/src/VVA/Account.hs @@ -1,21 +1,24 @@ -{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TemplateHaskell #-} module VVA.Account where import Control.Monad.Except (MonadError, throwError) import Control.Monad.Reader (MonadIO, MonadReader, liftIO) + import Data.ByteString (ByteString) import Data.FileEmbed (embedFile) +import Data.Has (Has) import Data.String (fromString) -import qualified Database.PostgreSQL.Simple as SQL -import VVA.Types (AppError(..), AccountInfo(..)) import Data.Text (Text, unpack) import qualified Data.Text.Encoding as Text import qualified Data.Text.IO as Text -import Data.Has (Has) + +import qualified Database.PostgreSQL.Simple as SQL + import VVA.Pool (ConnectionPool, withPool) +import VVA.Types (AccountInfo (..), AppError (..)) sqlFrom :: ByteString -> SQL.Query sqlFrom = fromString . unpack . Text.decodeUtf8 diff --git a/govtool/backend/src/VVA/AdaHolder.hs b/govtool/backend/src/VVA/AdaHolder.hs index 6232a8310..b708cd26e 100644 --- a/govtool/backend/src/VVA/AdaHolder.hs +++ b/govtool/backend/src/VVA/AdaHolder.hs @@ -1,12 +1,12 @@ {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeApplications #-} -{-# LANGUAGE ScopedTypeVariables #-} module VVA.AdaHolder where -import Control.Exception (try, SomeException) +import Control.Exception (SomeException, try) import Control.Monad.Except import Control.Monad.Reader @@ -65,4 +65,4 @@ getStakeKeyVotingPower stakeKey = withPool $ \conn -> do return 0 Right _ -> do Text.putStrLn ("Unexpected result for stake key: " <> stakeKey) - return 0 \ No newline at end of file + return 0 diff --git a/govtool/backend/src/VVA/Config.hs b/govtool/backend/src/VVA/Config.hs index 49151d630..997a3c231 100644 --- a/govtool/backend/src/VVA/Config.hs +++ b/govtool/backend/src/VVA/Config.hs @@ -68,19 +68,19 @@ instance DefaultConfig DBConfig where data VVAConfigInternal = VVAConfigInternal { -- | db-sync database access. - vVAConfigInternalDbsyncconfig :: DBConfig + vVAConfigInternalDbsyncconfig :: DBConfig -- | Server port. - , vVAConfigInternalPort :: Int + , vVAConfigInternalPort :: Int -- | Server host. - , vVAConfigInternalHost :: Text + , vVAConfigInternalHost :: Text -- | Request cache duration - , vVaConfigInternalCacheDurationSeconds :: Int + , vVaConfigInternalCacheDurationSeconds :: Int -- | Sentry DSN - , vVAConfigInternalSentrydsn :: String + , vVAConfigInternalSentrydsn :: String -- | Sentry environment - , vVAConfigInternalSentryEnv :: String + , vVAConfigInternalSentryEnv :: String -- | Pinata API JWT - , vVAConfigInternalPinataApiJwt :: Maybe Text + , vVAConfigInternalPinataApiJwt :: Maybe Text } deriving (FromConfig, Generic, Show) @@ -101,19 +101,19 @@ instance DefaultConfig VVAConfigInternal where data VVAConfig = VVAConfig { -- | db-sync database credentials. - dbSyncConnectionString :: Text + dbSyncConnectionString :: Text -- | Server port. - , serverPort :: Int + , serverPort :: Int -- | Server host. - , serverHost :: Text + , serverHost :: Text -- | Request cache duration - , cacheDurationSeconds :: Int + , cacheDurationSeconds :: Int -- | Sentry DSN - , sentryDSN :: String + , sentryDSN :: String -- | Sentry environment - , sentryEnv :: String + , sentryEnv :: String -- | Pinata API JWT - , pinataApiJwt :: Maybe Text + , pinataApiJwt :: Maybe Text } deriving (Generic, Show, ToJSON) diff --git a/govtool/backend/src/VVA/DRep.hs b/govtool/backend/src/VVA/DRep.hs index f8c9f8737..39342a7eb 100644 --- a/govtool/backend/src/VVA/DRep.hs +++ b/govtool/backend/src/VVA/DRep.hs @@ -13,56 +13,59 @@ 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 Data.Text (Text, intercalate, pack, unpack) +import qualified Data.Text.Encoding as Text import Data.Time -import qualified Database.PostgreSQL.Simple as SQL -import Database.PostgreSQL.Simple.Types (In(..)) +import qualified Database.PostgreSQL.Simple as SQL import Database.PostgreSQL.Simple.FromRow +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 (..), DRepVotingPowerList (..)) +import qualified VVA.Proposal as Proposal +import VVA.Types (AppError, DRepInfo (..), DRepRegistration (..), + DRepStatus (..), DRepType (..), DRepVotingPowerList (..), + Proposal (..), Vote (..)) -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) +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 + fromRow = DRepQueryResult <$> field <*> field <*> field <*> field <*> field <*> field <*> field <*> field <*> field <*> field <*> field <*> field <*> field <*> field <*> field <*> field <*> field <*> field @@ -93,7 +96,7 @@ listDReps mSearchQuery = withPool $ \conn -> do timeZone <- liftIO getCurrentTimeZone return - [ DRepRegistration + [ DRepRegistration (queryDrepHash result) (queryDrepView result) (queryIsScriptBased result) @@ -127,7 +130,7 @@ listDReps mSearchQuery = withPool $ \conn -> do | latestDeposit' < 0 && queryLatestNonDeregisterVotingAnchorWasNotNull result = DRep | Data.Maybe.isJust (queryUrl result) = DRep ] - + getVotingPowerSql :: SQL.Query getVotingPowerSql = sqlFrom $(embedFile "sql/get-voting-power.sql") @@ -165,7 +168,7 @@ getVotes drepId selectedProposals = withPool $ \conn -> do timeZone <- liftIO getCurrentTimeZone - let votes = + let votes = [ 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 @@ -252,11 +255,11 @@ getDRepsVotingPowerList identifiers = withPool $ \conn -> do 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/Ipfs.hs b/govtool/backend/src/VVA/Ipfs.hs index 86202f298..067ff43db 100644 --- a/govtool/backend/src/VVA/Ipfs.hs +++ b/govtool/backend/src/VVA/Ipfs.hs @@ -1,41 +1,53 @@ -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} -module VVA.Ipfs (ipfsUpload, IpfsError(..)) where - -import Control.Exception (SomeException, try) -import Control.Monad.IO.Class (liftIO) -import qualified Data.Aeson as A -import Data.Aeson (FromJSON(parseJSON), withObject, (.:), eitherDecode, ToJSON(..), encode,(.=),object) -import qualified Data.ByteString.Lazy as LBS -import Data.Text (Text) -import qualified Data.Text.Encoding as TE -import GHC.Generics (Generic) -import Network.HTTP.Client (newManager, parseRequest, httpLbs, method, requestHeaders, RequestBody(..), Request, responseBody, responseStatus) -import Network.HTTP.Client.TLS (tlsManagerSettings) +module VVA.Ipfs + ( IpfsError (..) + , ipfsUpload + ) where + +import Control.Exception (SomeException, try) +import Control.Monad.IO.Class (liftIO) + +import Data.Aeson (FromJSON (parseJSON), ToJSON (..), eitherDecode, + encode, object, withObject, (.:), (.=)) +import qualified Data.Aeson as A +import qualified Data.ByteString.Lazy as LBS +import qualified Data.ByteString.Lazy.Char8 as LBS8 +import Data.Text (Text) +import qualified Data.Text as T +import qualified Data.Text.Encoding as TE +import qualified Data.Text.Lazy as TL +import qualified Data.Text.Lazy.Encoding as TL + +import GHC.Generics (Generic) + +import Network.HTTP.Client (Request, RequestBody (..), httpLbs, method, + newManager, parseRequest, requestHeaders, + responseBody, responseStatus) import Network.HTTP.Client.MultipartFormData (formDataBody, partBS, partFileRequestBody) -import Network.HTTP.Types.Status (statusIsSuccessful, Status, status503, status400) -import qualified Data.ByteString.Lazy.Char8 as LBS8 -import qualified Data.Text.Lazy as TL -import qualified Data.Text.Lazy.Encoding as TL -import qualified Data.Text as T -import Servant.Server (ServerError (errBody)) -import Servant.Exception (ToServantErr(..), Exception(..)) - - -data PinataData = PinataData - { cid :: Text - , size :: Int - , created_at :: Text - , isDuplicate :: Maybe Bool - } deriving (Show, Generic) +import Network.HTTP.Client.TLS (tlsManagerSettings) +import Network.HTTP.Types.Status (Status, status400, status503, statusIsSuccessful) + +import Servant.Exception (Exception (..), ToServantErr (..)) +import Servant.Server (ServerError (errBody)) + + +data PinataData + = PinataData + { cid :: Text + , size :: Int + , created_at :: Text + , isDuplicate :: Maybe Bool + } + deriving (Generic, Show) instance FromJSON PinataData -data PinataSuccessResponse = PinataSuccessResponse - { pinataData :: PinataData - } deriving (Show) +newtype PinataSuccessResponse + = PinataSuccessResponse { pinataData :: PinataData } + deriving (Show) instance FromJSON PinataSuccessResponse where parseJSON = withObject "PinataSuccessResponse" $ \v -> PinataSuccessResponse @@ -45,9 +57,9 @@ data IpfsError = PinataConnectionError String | PinataAPIError Status LBS.ByteString | PinataDecodingError String LBS.ByteString - | IpfsUnconfiguredError + | IpfsUnconfiguredError | OtherIpfsError String - deriving (Show, Generic) + deriving (Generic, Show) instance ToJSON IpfsError where toJSON (PinataConnectionError msg) = @@ -86,19 +98,19 @@ instance Exception IpfsError instance ToServantErr IpfsError where status (OtherIpfsError _) = status400 - status _ = status503 + status _ = status503 message (PinataConnectionError msg) = T.pack ("Pinata service connection error: " <> msg) message (PinataAPIError status body) = T.pack ("Pinata API error: " <> show status <> " - " <> LBS8.unpack body) message (PinataDecodingError msg body) = T.pack ("Pinata decoding error: " <> msg <> " - " <> LBS8.unpack body) - message IpfsUnconfiguredError = T.pack ("Backend is not configured to support ipfs upload") + message IpfsUnconfiguredError = T.pack "Backend is not configured to support ipfs upload" message (OtherIpfsError msg) = T.pack msg ipfsUpload :: Maybe Text -> Text -> LBS.ByteString -> IO (Either IpfsError Text) ipfsUpload maybeJwt fileName fileContent = case maybeJwt of - Nothing -> pure $ Left $ IpfsUnconfiguredError - Just "" -> pure $ Left $ IpfsUnconfiguredError + Nothing -> pure $ Left IpfsUnconfiguredError + Just "" -> pure $ Left IpfsUnconfiguredError Just jwt -> do manager <- newManager tlsManagerSettings initialRequest <- parseRequest "https://uploads.pinata.cloud/v3/files" diff --git a/govtool/backend/src/VVA/Network.hs b/govtool/backend/src/VVA/Network.hs index 797d47ac3..ec99dc82a 100644 --- a/govtool/backend/src/VVA/Network.hs +++ b/govtool/backend/src/VVA/Network.hs @@ -34,7 +34,7 @@ networkInfo :: networkInfo = withPool $ \conn -> do result <- liftIO $ SQL.query_ conn networkInfoSql current_time <- liftIO getCurrentTime - case result of + case result of [( network_epoch , network_block_no , network_name diff --git a/govtool/backend/src/VVA/Proposal.hs b/govtool/backend/src/VVA/Proposal.hs index 4d61ec752..b4c5b502d 100644 --- a/govtool/backend/src/VVA/Proposal.hs +++ b/govtool/backend/src/VVA/Proposal.hs @@ -2,42 +2,43 @@ {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeApplications #-} -{-# LANGUAGE ScopedTypeVariables #-} module VVA.Proposal where -import Control.Exception (throw, SomeException, try) -import Control.Monad.Except (MonadError, throwError) +import Control.Exception (SomeException, throw, try) +import Control.Monad.Except (MonadError, throwError) import Control.Monad.Reader import Data.Aeson -import Data.Aeson.Types (Parser, parseMaybe) -import Data.ByteString (ByteString) -import Data.FileEmbed (embedFile) -import Data.Foldable (fold) -import Data.Has (Has, getter) -import qualified Data.Map as Map -import Data.Maybe (fromMaybe, isJust) -import Data.Monoid (Sum (..), getSum) +import Data.Aeson.Types (Parser, parseMaybe) +import Data.ByteString (ByteString) +import Data.FileEmbed (embedFile) +import Data.Foldable (fold) +import Data.Has (Has, getter) +import qualified Data.Map as Map +import Data.Maybe (fromMaybe, isJust) +import Data.Monoid (Sum (..), getSum) import Data.Scientific -import Data.String (fromString) -import Data.Text (Text, pack, unpack) -import qualified Data.Text.Encoding as Text -import qualified Data.Text.IO as Text +import Data.String (fromString) +import Data.Text (Text, pack, unpack) +import qualified Data.Text.Encoding as Text +import qualified Data.Text.IO as Text import Data.Time -import qualified Database.PostgreSQL.Simple as SQL -import qualified Database.PostgreSQL.Simple.Types as PG -import Database.PostgreSQL.Simple.ToField (ToField(..)) -import Database.PostgreSQL.Simple.ToRow (ToRow(..)) +import qualified Database.PostgreSQL.Simple as SQL +import Database.PostgreSQL.Simple.ToField (ToField (..)) +import Database.PostgreSQL.Simple.ToRow (ToRow (..)) +import qualified Database.PostgreSQL.Simple.Types as PG -import GHC.IO.Unsafe (unsafePerformIO) +import GHC.IO.Unsafe (unsafePerformIO) import VVA.Config -import VVA.Pool (ConnectionPool, withPool) -import VVA.Types (AppError (..), Proposal (..), EnactedProposalDetails (..)) +import VVA.Pool (ConnectionPool, withPool) +import VVA.Types (AppError (..), EnactedProposalDetails (..), + Proposal (..)) query1 :: (SQL.ToRow q, SQL.FromRow r) => SQL.Connection -> SQL.Query -> q -> IO (Maybe r) query1 conn q params = do @@ -52,7 +53,8 @@ sqlFrom bs = fromString $ unpack $ Text.decodeUtf8 bs listProposalsSql :: SQL.Query listProposalsSql = sqlFrom $(embedFile "sql/list-proposals.sql") -newtype TextArray = TextArray [Text] +newtype TextArray + = TextArray [Text] instance ToRow TextArray where toRow (TextArray texts) = map toField texts @@ -81,7 +83,7 @@ getProposals :: getProposals mSearchTerms = withPool $ \conn -> do let searchParam = maybe "" head mSearchTerms liftIO $ do - result <- try $ SQL.query conn listProposalsSql + result <- try $ SQL.query conn listProposalsSql ( searchParam , "%" <> searchParam <> "%" , "%" <> searchParam <> "%" @@ -96,7 +98,7 @@ getProposals mSearchTerms = withPool $ \conn -> do Right rows -> return rows latestEnactedProposalSql :: SQL.Query -latestEnactedProposalSql = +latestEnactedProposalSql = let rawSql = sqlFrom $(embedFile "sql/get-previous-enacted-governance-action-proposal-details.sql") in unsafePerformIO $ do putStrLn $ "[DEBUG] SQL query content: " ++ show rawSql @@ -109,13 +111,13 @@ getPreviousEnactedProposal :: getPreviousEnactedProposal proposalType = withPool $ \conn -> do let query = latestEnactedProposalSql let params = [proposalType] - + result <- liftIO $ try $ do rows <- SQL.query conn query params :: IO [EnactedProposalDetails] case rows of [x] -> return (Just x) _ -> return Nothing - + case result of Left err -> do throwError $ CriticalError $ "Database error: " <> pack (show (err :: SomeException)) @@ -123,6 +125,6 @@ getPreviousEnactedProposal proposalType = withPool $ \conn -> do case proposal of Just details -> do liftIO $ putStrLn $ "[DEBUG] Previous enacted proposal details: " ++ show details - Nothing -> + Nothing -> liftIO $ putStrLn "[DEBUG] No previous enacted proposal found" return proposal diff --git a/govtool/backend/src/VVA/Transaction.hs b/govtool/backend/src/VVA/Transaction.hs index 11c1e349e..36b6edeaa 100644 --- a/govtool/backend/src/VVA/Transaction.hs +++ b/govtool/backend/src/VVA/Transaction.hs @@ -12,11 +12,11 @@ import Data.Aeson (Value) import Data.ByteString (ByteString) import Data.FileEmbed (embedFile) import Data.Has (Has) +import Data.Maybe (fromMaybe) import Data.String (fromString) import Data.Text (Text, pack, unpack) import qualified Data.Text.Encoding as Text import qualified Data.Text.IO as Text -import Data.Maybe (fromMaybe) import qualified Database.PostgreSQL.Simple as SQL @@ -39,4 +39,4 @@ getTransactionStatus transactionId = withPool $ \conn -> do case result of [(transactionConfirmed, votingProcedure)] -> do return $ Just $ TransactionStatus transactionConfirmed votingProcedure - _ -> return Nothing \ No newline at end of file + _ -> return Nothing diff --git a/govtool/backend/src/VVA/Types.hs b/govtool/backend/src/VVA/Types.hs index d5b0f2cf2..98fb7b44e 100644 --- a/govtool/backend/src/VVA/Types.hs +++ b/govtool/backend/src/VVA/Types.hs @@ -1,36 +1,38 @@ -{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE NamedFieldPuns #-} -{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} 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 qualified Data.Aeson as A -import Data.Aeson (Value, ToJSON (..), object, (.=)) -import qualified Data.Cache as Cache +import Control.Monad.Except (MonadError) +import Control.Monad.Fail (MonadFail) +import Control.Monad.IO.Class (MonadIO) +import Control.Monad.Reader (MonadReader) + +import Data.Aeson (ToJSON (..), Value, object, (.=)) +import qualified Data.Aeson as A +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.Scientific +import Data.Text (Text) +import Data.Time (LocalTime, UTCTime) -import Database.PostgreSQL.Simple (Connection) +import Database.PostgreSQL.Simple (Connection) +import Database.PostgreSQL.Simple.FromField (FromField (..), ResultError (ConversionFailed), + returnError) import Database.PostgreSQL.Simple.FromRow -import Database.PostgreSQL.Simple.FromField (FromField(..), returnError, ResultError(ConversionFailed)) import VVA.Cache import VVA.Config -import VVA.Ipfs (IpfsError) +import VVA.Ipfs (IpfsError) type App m = (MonadReader AppEnv m, MonadIO m, MonadFail m, MonadError AppError m) @@ -65,10 +67,10 @@ instance Exception AppError instance ToJSON AppError where toJSON (ValidationError msg) = object ["errorType" .= A.String "ValidationError", "message" .= msg] - toJSON (NotFoundError msg) = object ["errorType" .= A.String "NotFoundError", "message" .= msg] - toJSON (CriticalError msg) = object ["errorType" .= A.String "CriticalError", "message" .= msg] - toJSON (InternalError msg) = object ["errorType" .= A.String "InternalError", "message" .= msg] - toJSON (AppIpfsError err) = toJSON err + toJSON (NotFoundError msg) = object ["errorType" .= A.String "NotFoundError", "message" .= msg] + toJSON (CriticalError msg) = object ["errorType" .= A.String "CriticalError", "message" .= msg] + toJSON (InternalError msg) = object ["errorType" .= A.String "InternalError", "message" .= msg] + toJSON (AppIpfsError err) = toJSON err data Vote = Vote @@ -114,28 +116,28 @@ data DRepVotingPowerList , drepVotingPower :: Integer , drepGivenName :: Maybe Text } - deriving (Show, Eq) + deriving (Eq, Show) -data DRepStatus = Active | Inactive | Retired deriving (Show, Eq, Ord) +data DRepStatus = Active | Inactive | Retired deriving (Eq, Ord, Show) instance FromField DRepStatus where fromField f mdata = do (value :: Text) <- fromField f mdata case value of - "Active" -> return Active + "Active" -> return Active "Inactive" -> return Inactive - "Retired" -> return Retired - _ -> returnError ConversionFailed f "Invalid DRepStatus" + "Retired" -> return Retired + _ -> returnError ConversionFailed f "Invalid DRepStatus" -data DRepType = DRep | SoleVoter deriving (Show, Eq) +data DRepType = DRep | SoleVoter deriving (Eq, Show) instance FromField DRepType where fromField f mdata = do (value :: Text) <- fromField f mdata case value of - "DRep" -> return DRep + "DRep" -> return DRep "SoleVoter" -> return SoleVoter - _ -> returnError ConversionFailed f "Invalid DRepType" + _ -> returnError ConversionFailed f "Invalid DRepType" data DRepRegistration = DRepRegistration @@ -155,7 +157,7 @@ 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 @@ -188,37 +190,37 @@ instance FromRow DRepRegistration where <*> field -- dRepRegistrationIdentityReferences <*> field -- dRepRegistrationLinkReferences -data Proposal +data Proposal = Proposal - { proposalId :: Integer - , proposalTxHash :: Text - , proposalIndex :: Integer - , proposalType :: Text - , proposalDetails :: Maybe Value - , proposalExpiryDate :: Maybe LocalTime - , proposalExpiryEpochNo :: Maybe Integer - , proposalCreatedDate :: LocalTime - , proposalCreatedEpochNo :: Integer - , proposalUrl :: Text - , proposalDocHash :: Text - , proposalProtocolParams :: Maybe Value - , proposalTitle :: Maybe Text - , proposalAbstract :: Maybe Text - , proposalMotivation :: Maybe Text - , proposalRationale :: Maybe Text - , proposalDRepYesVotes :: Integer - , proposalDRepNoVotes :: Integer - , proposalDRepAbstainVotes :: Integer - , proposalPoolYesVotes :: Integer - , proposalPoolNoVotes :: Integer - , proposalPoolAbstainVotes :: Integer - , proposalCcYesVotes :: Integer - , proposalCcNoVotes :: Integer - , proposalCcAbstainVotes :: Integer - , proposalPrevGovActionIndex :: Maybe Integer - , proposalPrevGovActionTxHash :: Maybe Text - , proposalJson :: Maybe Value - , proposalAuthors :: Maybe Value + { proposalId :: Integer + , proposalTxHash :: Text + , proposalIndex :: Integer + , proposalType :: Text + , proposalDetails :: Maybe Value + , proposalExpiryDate :: Maybe LocalTime + , proposalExpiryEpochNo :: Maybe Integer + , proposalCreatedDate :: LocalTime + , proposalCreatedEpochNo :: Integer + , proposalUrl :: Text + , proposalDocHash :: Text + , proposalProtocolParams :: Maybe Value + , proposalTitle :: Maybe Text + , proposalAbstract :: Maybe Text + , proposalMotivation :: Maybe Text + , proposalRationale :: Maybe Text + , proposalDRepYesVotes :: Integer + , proposalDRepNoVotes :: Integer + , proposalDRepAbstainVotes :: Integer + , proposalPoolYesVotes :: Integer + , proposalPoolNoVotes :: Integer + , proposalPoolAbstainVotes :: Integer + , proposalCcYesVotes :: Integer + , proposalCcNoVotes :: Integer + , proposalCcAbstainVotes :: Integer + , proposalPrevGovActionIndex :: Maybe Integer + , proposalPrevGovActionTxHash :: Maybe Text + , proposalJson :: Maybe Value + , proposalAuthors :: Maybe Value } deriving (Show) @@ -255,10 +257,11 @@ instance FromRow Proposal where <*> field -- proposalJson <*> field -- proposalAuthors -data TransactionStatus = TransactionStatus - { transactionConfirmed :: Bool - , votingProcedure :: Maybe Value - } +data TransactionStatus + = TransactionStatus + { transactionConfirmed :: Bool + , votingProcedure :: Maybe Value + } instance FromRow TransactionStatus where fromRow = TransactionStatus <$> field <*> field @@ -270,13 +273,14 @@ instance ToJSON TransactionStatus where , "votingProcedure" .= votingProcedure ] -data EnactedProposalDetails = EnactedProposalDetails - { enactedProposalDetailsId :: Integer - , enactedProposalDetailsTxId :: Integer - , enactedProposalDetailsIndex :: Integer - , enactedProposalDetailsDescription :: Maybe Value - , enactedProposalDetailsHash :: Text - } +data EnactedProposalDetails + = EnactedProposalDetails + { enactedProposalDetailsId :: Integer + , enactedProposalDetailsTxId :: Integer + , enactedProposalDetailsIndex :: Integer + , enactedProposalDetailsDescription :: Maybe Value + , enactedProposalDetailsHash :: Text + } deriving (Show) instance FromRow EnactedProposalDetails where @@ -306,20 +310,20 @@ instance ToJSON EnactedProposalDetails where data CacheEnv = CacheEnv - { proposalListCache :: Cache.Cache () [Proposal] - , getProposalCache :: Cache.Cache (Text, Integer) Proposal - , currentEpochCache :: Cache.Cache () (Maybe Value) - , adaHolderVotingPowerCache :: Cache.Cache Text Integer - , adaHolderGetCurrentDelegationCache :: Cache.Cache Text (Maybe Delegation) - , dRepGetVotesCache :: Cache.Cache Text ([Vote], [Proposal]) - , dRepInfoCache :: Cache.Cache Text DRepInfo - , dRepVotingPowerCache :: Cache.Cache Text Integer - , dRepListCache :: Cache.Cache Text [DRepRegistration] - , networkMetricsCache :: Cache.Cache () NetworkMetrics - , networkInfoCache :: Cache.Cache () NetworkInfo - , networkTotalStakeCache :: Cache.Cache () NetworkTotalStake - , dRepVotingPowerListCache :: Cache.Cache Text [DRepVotingPowerList] - , accountInfoCache :: Cache.Cache Text AccountInfo + { proposalListCache :: Cache.Cache () [Proposal] + , getProposalCache :: Cache.Cache (Text, Integer) Proposal + , currentEpochCache :: Cache.Cache () (Maybe Value) + , adaHolderVotingPowerCache :: Cache.Cache Text Integer + , adaHolderGetCurrentDelegationCache :: Cache.Cache Text (Maybe Delegation) + , dRepGetVotesCache :: Cache.Cache Text ([Vote], [Proposal]) + , dRepInfoCache :: Cache.Cache Text DRepInfo + , dRepVotingPowerCache :: Cache.Cache Text Integer + , dRepListCache :: Cache.Cache Text [DRepRegistration] + , networkMetricsCache :: Cache.Cache () NetworkMetrics + , networkInfoCache :: Cache.Cache () NetworkInfo + , networkTotalStakeCache :: Cache.Cache () NetworkTotalStake + , dRepVotingPowerListCache :: Cache.Cache Text [DRepVotingPowerList] + , accountInfoCache :: Cache.Cache Text AccountInfo } data NetworkInfo @@ -335,24 +339,24 @@ data NetworkTotalStake { networkTotalStakeControlledByDReps :: Integer , networkTotalStakeControlledBySPOs :: Integer , networkTotalAlwaysAbstainVotingPower :: Integer - , networkTotalAlwaysNoConfidenceVotingPower :: Integer + , networkTotalAlwaysNoConfidenceVotingPower :: Integer } data NetworkMetrics = NetworkMetrics - { networkMetricsUniqueDelegators :: Integer - , networkMetricsTotalDelegations :: Integer - , networkMetricsTotalGovernanceActions :: Integer - , networkMetricsTotalDRepVotes :: Integer - , networkMetricsTotalRegisteredDReps :: Integer - , networkMetricsTotalDRepDistr :: Integer - , networkMetricsTotalActiveDReps :: Integer - , networkMetricsTotalInactiveDReps :: Integer - , networkMetricsTotalActiveCIP119CompliantDReps :: Integer - , networkMetricsTotalRegisteredDirectVoters :: Integer - , networkMetricsNoOfCommitteeMembers :: Integer - , networkMetricsQuorumNumerator :: Integer - , networkMetricsQuorumDenominator :: Integer + { networkMetricsUniqueDelegators :: Integer + , networkMetricsTotalDelegations :: Integer + , networkMetricsTotalGovernanceActions :: Integer + , networkMetricsTotalDRepVotes :: Integer + , networkMetricsTotalRegisteredDReps :: Integer + , networkMetricsTotalDRepDistr :: Integer + , networkMetricsTotalActiveDReps :: Integer + , networkMetricsTotalInactiveDReps :: Integer + , networkMetricsTotalActiveCIP119CompliantDReps :: Integer + , networkMetricsTotalRegisteredDirectVoters :: Integer + , networkMetricsNoOfCommitteeMembers :: Integer + , networkMetricsQuorumNumerator :: Integer + , networkMetricsQuorumDenominator :: Integer } data Delegation From e8aecf6d4efbc6dd2d5703bd81487a132a396af7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 31 Jul 2025 07:12:25 +0000 Subject: [PATCH 37/39] chore: update @intersect.mbo/pdf-ui to 1.0.11-beta --- govtool/frontend/package-lock.json | 8 ++++---- govtool/frontend/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/govtool/frontend/package-lock.json b/govtool/frontend/package-lock.json index 5db72292f..f823350c9 100644 --- a/govtool/frontend/package-lock.json +++ b/govtool/frontend/package-lock.json @@ -15,7 +15,7 @@ "@hookform/resolvers": "^3.3.1", "@intersect.mbo/govtool-outcomes-pillar-ui": "v1.5.5", "@intersect.mbo/intersectmbo.org-icons-set": "^1.0.8", - "@intersect.mbo/pdf-ui": "1.0.10-beta", + "@intersect.mbo/pdf-ui": "1.0.11-beta", "@mui/icons-material": "^5.14.3", "@mui/material": "^5.14.4", "@noble/ed25519": "^2.3.0", @@ -3426,9 +3426,9 @@ "license": "ISC" }, "node_modules/@intersect.mbo/pdf-ui": { - "version": "1.0.10-beta", - "resolved": "https://registry.npmjs.org/@intersect.mbo/pdf-ui/-/pdf-ui-1.0.10-beta.tgz", - "integrity": "sha512-1IrictQBHpAGSqj5/psGB/Puxkkvmfi/ZZV0Bn60mBgJoTcUpoIO7tVr+h9pP+FSPFNnQaGeeFhca50PuUUp4Q==", + "version": "1.0.11-beta", + "resolved": "https://registry.npmjs.org/@intersect.mbo/pdf-ui/-/pdf-ui-1.0.11-beta.tgz", + "integrity": "sha512-F/IMaJHDKXvXVuGG2Qb8BrpfFInRVApTbHSk7ERvz6OsW1Xe2bKWEBAn4nIa1BF5KwKEWq62bor4EC8ybB11ug==", "dependencies": { "@emurgo/cardano-serialization-lib-asmjs": "^12.0.0-beta.2", "@fontsource/poppins": "^5.0.14", diff --git a/govtool/frontend/package.json b/govtool/frontend/package.json index 6c634a711..2ef6e7e8f 100644 --- a/govtool/frontend/package.json +++ b/govtool/frontend/package.json @@ -29,7 +29,7 @@ "@hookform/resolvers": "^3.3.1", "@intersect.mbo/govtool-outcomes-pillar-ui": "v1.5.5", "@intersect.mbo/intersectmbo.org-icons-set": "^1.0.8", - "@intersect.mbo/pdf-ui": "1.0.10-beta", + "@intersect.mbo/pdf-ui": "1.0.11-beta", "@mui/icons-material": "^5.14.3", "@mui/material": "^5.14.4", "@noble/ed25519": "^2.3.0", From d896ece1ac56fa013355d9769329e690767ac238 Mon Sep 17 00:00:00 2001 From: Aaron Boyle Date: Thu, 31 Jul 2025 08:49:08 +0100 Subject: [PATCH 38/39] updates to ignore root.json file and do clean install --- .github/workflows/code_check_backend.yml | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/.github/workflows/code_check_backend.yml b/.github/workflows/code_check_backend.yml index fca3fdf7c..747818b1c 100644 --- a/.github/workflows/code_check_backend.yml +++ b/.github/workflows/code_check_backend.yml @@ -13,8 +13,9 @@ jobs: - name: Use Haskell uses: haskell-actions/setup@v2 with: - ghc-version: '9.2.7' - cabal-version: '3.6.0.0' + ghc-version: '9.2.8' + cabal-version: '3.8.1.0' + cabal-update: false - name: Use Python uses: actions/setup-python@v2 @@ -26,6 +27,13 @@ jobs: - name: Install HLint run: | + rm -rf ~/.cabal ~/.ghcup + mkdir -p ~/.cabal + cat < ~/.cabal/config + repository hackage.haskell.org + url: http://hackage.haskell.org/ + secure: False + EOF cabal update cabal install hlint @@ -41,8 +49,9 @@ jobs: - name: Use Haskell uses: haskell-actions/setup@v2 with: - ghc-version: '9.2.7' - cabal-version: '3.6.0.0' + ghc-version: '9.2.8' + cabal-version: '3.8.1.0' + cabal-update: false - name: Use Python uses: actions/setup-python@v2 @@ -54,6 +63,13 @@ jobs: - name: Install HLint run: | + rm -rf ~/.cabal ~/.ghcup + mkdir -p ~/.cabal + cat < ~/.cabal/config + repository hackage.haskell.org + url: http://hackage.haskell.org/ + secure: False + EOF cabal update cabal install stylish-haskell From 5973c3bcf902791e63844e8170101ba3b12ceacd Mon Sep 17 00:00:00 2001 From: Ciabas Date: Tue, 29 Jul 2025 09:53:52 +0200 Subject: [PATCH 39/39] (fix#3954): Incorrect Display of New Committee Parameters in Governance Action Details --- CHANGELOG.md | 1 + govtool/backend/sql/list-proposals.sql | 66 +++++++++++++++-- ...nceActionNewCommitteeDetailsTabContent.tsx | 71 ++++++++++--------- govtool/frontend/src/i18n/locales/en.json | 2 +- 4 files changed, 97 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c630fce7..911cd6242 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ changes. ### Fixed - Fix disappearing proposals in the governance actions list for the same tx hashes [Issue 3918](https://github.com/IntersectMBO/govtool/issues/3918) +- Fix incorrect display of new committee parameters in Governance Action details [Issue 3954](https://github.com/IntersectMBO/govtool/issues/3954) ### Changed diff --git a/govtool/backend/sql/list-proposals.sql b/govtool/backend/sql/list-proposals.sql index c0779c6d8..931c069b8 100644 --- a/govtool/backend/sql/list-proposals.sql +++ b/govtool/backend/sql/list-proposals.sql @@ -42,6 +42,21 @@ CommitteeData AS ( FROM committee_member cm JOIN committee_hash ch ON cm.committee_hash_id = ch.id + WHERE EXISTS ( + SELECT 1 + FROM committee_registration cr + WHERE cr.cold_key_id = ch.id + ) + AND NOT EXISTS ( + SELECT 1 + FROM committee_de_registration cdr + WHERE cdr.cold_key_id = ch.id + AND cdr.tx_id > ( + SELECT MAX(cr2.tx_id) + FROM committee_registration cr2 + WHERE cr2.cold_key_id = ch.id + ) + ) ORDER BY ch.raw, cm.expiration_epoch DESC ), @@ -61,10 +76,31 @@ ParsedDescription AS ( MembersToBeRemoved AS ( SELECT id, - json_agg(VALUE->>'keyHash') AS members_to_be_removed + json_agg( + json_build_object( + 'hash', COALESCE( + VALUE->>'keyHash', + VALUE->>'scriptHash' + ), + 'type', CASE + WHEN VALUE->>'keyHash' IS NOT NULL THEN 'keyHash' + WHEN VALUE->>'scriptHash' IS NOT NULL THEN 'scriptHash' + ELSE 'unknown' + END, + 'hasScript', CASE + WHEN VALUE->>'scriptHash' IS NOT NULL THEN true + ELSE false + END + ) + ) AS members_to_be_removed FROM ParsedDescription pd, - json_array_elements(members_to_be_removed::json) AS value + json_array_elements( + CASE + WHEN pd.members_to_be_removed IS NULL THEN '[]'::json + ELSE pd.members_to_be_removed::json + END + ) AS value GROUP BY id ), @@ -73,7 +109,15 @@ ProcessedCurrentMembers AS ( pd.id, json_agg( json_build_object( - 'hash', regexp_replace(kv.key, '^keyHash-', ''), + 'hash', COALESCE( + regexp_replace(kv.key, '^keyHash-', ''), + regexp_replace(kv.key, '^scriptHash-', '') + ), + 'type', CASE + WHEN kv.key LIKE 'keyHash-%' THEN 'keyHash' + WHEN kv.key LIKE 'scriptHash-%' THEN 'scriptHash' + ELSE 'unknown' + END, 'newExpirationEpoch', kv.value::int ) ) AS current_members @@ -88,9 +132,17 @@ EnrichedCurrentMembers AS ( pcm.id, json_agg( json_build_object( - 'hash', cm.hash, + 'hash', CASE + WHEN (member->>'hash') LIKE 'scriptHash-%' THEN + regexp_replace(member->>'hash', '^scriptHash-', '') + WHEN (member->>'hash') LIKE 'keyHash-%' THEN + regexp_replace(member->>'hash', '^keyHash-', '') + ELSE + member->>'hash' + END, + 'type', member->>'type', 'expirationEpoch', cm.expiration_epoch, - 'hasScript', cm.has_script, + 'hasScript', COALESCE(cm.has_script, member->>'type' = 'scriptHash'), 'newExpirationEpoch', (member->>'newExpirationEpoch')::int ) ) AS enriched_members @@ -247,9 +299,9 @@ SELECT ) FROM ParsedDescription pd - JOIN + LEFT JOIN MembersToBeRemoved mtr ON pd.id = mtr.id - JOIN + LEFT JOIN EnrichedCurrentMembers em ON pd.id = em.id WHERE pd.id = gov_action_proposal.id diff --git a/govtool/frontend/src/components/molecules/GovernanceActionNewCommitteeDetailsTabContent.tsx b/govtool/frontend/src/components/molecules/GovernanceActionNewCommitteeDetailsTabContent.tsx index e3a2a286a..ffa415025 100644 --- a/govtool/frontend/src/components/molecules/GovernanceActionNewCommitteeDetailsTabContent.tsx +++ b/govtool/frontend/src/components/molecules/GovernanceActionNewCommitteeDetailsTabContent.tsx @@ -12,41 +12,51 @@ type CCMember = { hash: string; newExpirationEpoch?: number; }; +type CCMemberToBeRemoved = { + hash: string; + hasScript?: boolean; +}; -function isArrayOfStrings(value: unknown): value is string[] { - return ( - Array.isArray(value) && value.every((item) => typeof item === "string") - ); -} +const getCip129Identifier = (hash: string, hasScript?: boolean) => + encodeCIP129Identifier({ + txID: (hasScript ? "13" : "12") + hash, + bech32Prefix: "cc_cold", + }); export const GovernanceActionNewCommitteeDetailsTabContent = ({ details, }: Pick) => { const { t } = useTranslation(); const membersToBeAdded = ((details?.members as CCMember[]) || []) - .filter((member) => member.newExpirationEpoch === undefined) + .filter( + (member) => + member?.expirationEpoch === undefined || + member?.expirationEpoch === null, + ) + .filter((member) => member?.hash) .map((member) => ({ - cip129Identifier: encodeCIP129Identifier({ - txID: (member.hasScript ? "02" : "13") + member.hash, - bech32Prefix: member.hasScript ? "cc_hot" : "cc_cold", - }), + cip129Identifier: getCip129Identifier(member.hash, member.hasScript), expirationEpoch: member.expirationEpoch, })); const membersToBeUpdated = ((details?.members as CCMember[]) || []) - .filter((member) => member.newExpirationEpoch !== undefined) + .filter( + (member) => !!member?.expirationEpoch && !!member?.newExpirationEpoch, + ) + .filter((member) => member?.hash) .map((member) => ({ - cip129Identifier: encodeCIP129Identifier({ - txID: (member.hasScript ? "02" : "13") + member.hash, - bech32Prefix: member.hasScript ? "cc_hot" : "cc_cold", - }), + cip129Identifier: getCip129Identifier(member.hash, member.hasScript), expirationEpoch: member.expirationEpoch, newExpirationEpoch: member.newExpirationEpoch, })); - const membersToBeRemoved = isArrayOfStrings(details?.membersToBeRemoved) - ? details.membersToBeRemoved - : []; + const membersToBeRemoved = ( + (details?.membersToBeRemoved as CCMemberToBeRemoved[]) || [] + ) + .filter((member) => member?.hash && member.hash.trim() !== "") + .map((member) => ({ + cip129Identifier: getCip129Identifier(member.hash, member.hasScript), + })); return ( @@ -63,7 +73,7 @@ export const GovernanceActionNewCommitteeDetailsTabContent = ({ whiteSpace: "nowrap", }} > - {t("govActions.membersToBeAdded")} + {t("govActions.membersToBeAddedToTheCommittee")} {membersToBeAdded.map(({ cip129Identifier }) => ( @@ -101,10 +111,10 @@ export const GovernanceActionNewCommitteeDetailsTabContent = ({ whiteSpace: "nowrap", }} > - {t("govActions.membersToBeRemoved")} + {t("govActions.membersToBeRemovedFromTheCommittee")} - {membersToBeRemoved.map((hash) => ( - + {membersToBeRemoved.map(({ cip129Identifier }) => ( + - {encodeCIP129Identifier({ - txID: hash, - bech32Prefix: "cc_cold", - })} + {cip129Identifier} - + ))} @@ -179,8 +180,8 @@ export const GovernanceActionNewCommitteeDetailsTabContent = ({ }} > {t("govActions.changeToTermsEpochs", { - epochTo: newExpirationEpoch, - epochFrom: expirationEpoch, + epochTo: newExpirationEpoch ?? "N/A", + epochFrom: expirationEpoch ?? "N/A", })} diff --git a/govtool/frontend/src/i18n/locales/en.json b/govtool/frontend/src/i18n/locales/en.json index a60dfb632..f8ebb5efd 100644 --- a/govtool/frontend/src/i18n/locales/en.json +++ b/govtool/frontend/src/i18n/locales/en.json @@ -479,7 +479,7 @@ "membersToBeRemovedFromTheCommittee": "Members to be removed from the Committee", "membersToBeAddedToTheCommittee": "Members to be added to the Committee", "changeToTermsOfExistingMembers": "Change to terms of existing members", - "changeToTermsEpochs": "To {{epochTo}} epoch {{epochFrom}} epoch", + "changeToTermsEpochs": "To {{epochTo}} epoch, from {{epochFrom}} epoch", "newThresholdValue": "New threshold value", "protocolParamsDetails": { "existing": "Existing",