From a39bd39586c89c18c3c80fcb6fcc0758f7fbf58b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Sza=C5=82owski?= Date: Wed, 21 May 2025 22:34:02 +0200 Subject: [PATCH] feat: add support for Parameter Change and Hard Fork Initiation gov actions creation --- CHANGELOG.md | 1 + ...ted-governance-action-proposal-details.sql | 15 ++++ govtool/backend/src/VVA/API.hs | 29 +++++++ govtool/backend/src/VVA/API/Types.hs | 34 ++++++++ govtool/backend/src/VVA/Proposal.hs | 59 +++++++++++-- govtool/backend/src/VVA/Types.hs | 34 ++++++++ govtool/backend/vva-be.cabal | 1 + .../CreateGovernanceActionForm.tsx | 5 ++ .../src/consts/governanceAction/fields.ts | 82 +++++++++++++++++++ govtool/frontend/src/context/wallet.tsx | 5 +- .../forms/useCreateGovernanceActionForm.ts | 58 ++++++++++++- govtool/frontend/src/i18n/locales/en.json | 15 ++++ .../frontend/src/types/governanceAction.ts | 17 +++- 13 files changed, 338 insertions(+), 17 deletions(-) create mode 100644 govtool/backend/sql/get-previous-enacted-governance-action-proposal-details.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index f28ea4458..e3b03bc83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ changes. - Add CIP-129 support for gov_actions hashes in Live Voting (governance actions) [Issue 3619](https://github.com/IntersectMBO/govtool/issues/3619) - Add maintenance ending banner [Issue 3647](https://github.com/IntersectMBO/govtool/issues/3647) +- Add support for the Protocol Parameter Change and Hard Fork Initiation governance actions ### Fixed diff --git a/govtool/backend/sql/get-previous-enacted-governance-action-proposal-details.sql b/govtool/backend/sql/get-previous-enacted-governance-action-proposal-details.sql new file mode 100644 index 000000000..6aabbaa3d --- /dev/null +++ b/govtool/backend/sql/get-previous-enacted-governance-action-proposal-details.sql @@ -0,0 +1,15 @@ +SELECT + gap.id, + tx_id, + index, + description, + encode(hash, 'hex') AS hash +FROM + gov_action_proposal gap +JOIN + tx ON gap.tx_id = tx.id +WHERE + gap.type = ? AND gap.enacted_epoch IS NOT NULL +ORDER BY + gap.id DESC +LIMIT 1; \ No newline at end of file diff --git a/govtool/backend/src/VVA/API.hs b/govtool/backend/src/VVA/API.hs index b95960f52..9488710a6 100644 --- a/govtool/backend/src/VVA/API.hs +++ b/govtool/backend/src/VVA/API.hs @@ -78,6 +78,7 @@ type VVAApi = :> QueryParam "search" Text :> Get '[JSON] ListProposalsResponse :<|> "proposal" :> "get" :> Capture "proposalId" GovActionId :> QueryParam "drepId" HexText :> Get '[JSON] GetProposalResponse + :<|> "proposal" :> "enacted-details" :> QueryParam "type" GovernanceActionType :> Get '[JSON] (Maybe EnactedProposalDetailsResponse) :<|> "epoch" :> "params" :> Get '[JSON] GetCurrentEpochParamsResponse :<|> "transaction" :> "status" :> Capture "transactionId" HexText :> Get '[JSON] GetTransactionStatusResponse :<|> "throw500" :> Get '[JSON] () @@ -95,6 +96,7 @@ server = drepList :<|> getStakeKeyVotingPower :<|> listProposals :<|> getProposal + :<|> getEnactedProposalDetails :<|> getCurrentEpochParams :<|> getTransactionStatus :<|> throw500 @@ -442,6 +444,33 @@ getProposal g@(GovActionId govActionTxHash govActionIndex) mDrepId' = do , getProposalResponseVote = voteResponse } +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 = + case actionType of + HardForkInitiation -> "HardForkInitiation" + ParameterChange -> "ParameterChange" + _ -> "HardForkInitiation" + + enactedProposalDetailsToResponse :: Types.EnactedProposalDetails -> EnactedProposalDetailsResponse + enactedProposalDetailsToResponse Types.EnactedProposalDetails{..} = + EnactedProposalDetailsResponse + { enactedProposalDetailsResponseId = enactedProposalDetailsId + , enactedProposalDetailsResponseTxId = enactedProposalDetailsTxId + , enactedProposalDetailsResponseIndex = enactedProposalDetailsIndex + , enactedProposalDetailsResponseDescription = enactedProposalDetailsDescription + , enactedProposalDetailsResponseHash = HexText enactedProposalDetailsHash + } + getCurrentEpochParams :: App m => m GetCurrentEpochParamsResponse getCurrentEpochParams = do CacheEnv {currentEpochCache} <- asks vvaCache diff --git a/govtool/backend/src/VVA/API/Types.hs b/govtool/backend/src/VVA/API/Types.hs index d1c88adb0..6b517a9ce 100644 --- a/govtool/backend/src/VVA/API/Types.hs +++ b/govtool/backend/src/VVA/API/Types.hs @@ -455,6 +455,40 @@ instance ToSchema ProposalResponse where & example ?~ toJSON exampleProposalResponse +data EnactedProposalDetailsResponse + = EnactedProposalDetailsResponse + { enactedProposalDetailsResponseId :: Integer + , enactedProposalDetailsResponseTxId :: Integer + , enactedProposalDetailsResponseIndex :: Integer + , enactedProposalDetailsResponseDescription :: Maybe Value + , enactedProposalDetailsResponseHash :: HexText + } + deriving (Generic, Show) + +deriveJSON (jsonOptions "enactedProposalDetailsResponse") ''EnactedProposalDetailsResponse + +exampleEnactedProposalDetailsResponse :: Text +exampleEnactedProposalDetailsResponse = "{ \"id\": 123," + <> "\"txId\": 456," + <> "\"index\": 0," + <> "\"description\": {\"key\": \"value\"}," + <> "\"hash\": \"9af10e89979e51b8cdc827c963124a1ef4920d1253eef34a1d5cfe76438e3f11\"}" + +instance ToSchema EnactedProposalDetailsResponse where + declareNamedSchema proxy = do + NamedSchema name_ schema_ <- + genericDeclareNamedSchema + ( fromAesonOptions $ + jsonOptions "enactedProposalDetailsResponse" + ) + proxy + return $ + NamedSchema name_ $ + schema_ + & description ?~ "Enacted Proposal Details Response" + & example + ?~ toJSON exampleEnactedProposalDetailsResponse + exampleListProposalsResponse :: Text exampleListProposalsResponse = "{ \"page\": 0," diff --git a/govtool/backend/src/VVA/Proposal.hs b/govtool/backend/src/VVA/Proposal.hs index 89e72a8c8..6c31a8016 100644 --- a/govtool/backend/src/VVA/Proposal.hs +++ b/govtool/backend/src/VVA/Proposal.hs @@ -17,25 +17,34 @@ import Data.Aeson.Types (Parser, parseMaybe) import Data.ByteString (ByteString) import Data.FileEmbed (embedFile) import Data.Foldable (fold) -import Data.Has (Has) -import qualified Data.Map as Map -import Data.Maybe (fromMaybe) +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 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 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 GHC.IO.Unsafe (unsafePerformIO) + import VVA.Config import VVA.Pool (ConnectionPool, withPool) -import VVA.Types (AppError (..), Proposal (..)) +import VVA.Types (AppError (..), Proposal (..), EnactedProposalDetails (..)) + +query1 :: (SQL.ToRow q, SQL.FromRow r) => SQL.Connection -> SQL.Query -> q -> IO (Maybe r) +query1 conn q params = do + results <- SQL.query conn q params + case results of + [x] -> return (Just x) + _ -> return Nothing sqlFrom :: ByteString -> SQL.Query sqlFrom bs = fromString $ unpack $ Text.decodeUtf8 bs @@ -84,4 +93,36 @@ getProposals mSearchTerms = withPool $ \conn -> do Left (e :: SomeException) -> do putStrLn $ "Error fetching proposals: " <> show e return [] - Right rows -> return rows \ No newline at end of file + Right rows -> return rows + +latestEnactedProposalSql :: SQL.Query +latestEnactedProposalSql = + let rawSql = sqlFrom $(embedFile "sql/get-previous-enacted-governance-action-proposal-details.sql") + in unsafePerformIO $ do + putStrLn $ "[DEBUG] SQL query content: " ++ show rawSql + return rawSql + +getPreviousEnactedProposal :: + (Has ConnectionPool r, Has VVAConfig r, MonadReader r m, MonadIO m, MonadFail m, MonadError AppError m) => + Text -> + m (Maybe EnactedProposalDetails) +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)) + Right proposal -> do + case proposal of + Just details -> do + liftIO $ putStrLn $ "[DEBUG] Previous enacted proposal details: " ++ show details + Nothing -> + liftIO $ putStrLn "[DEBUG] No previous enacted proposal found" + return proposal \ No newline at end of file diff --git a/govtool/backend/src/VVA/Types.hs b/govtool/backend/src/VVA/Types.hs index 35cdd37bf..e6e674925 100644 --- a/govtool/backend/src/VVA/Types.hs +++ b/govtool/backend/src/VVA/Types.hs @@ -211,6 +211,40 @@ instance ToJSON TransactionStatus where , "votingProcedure" .= votingProcedure ] +data EnactedProposalDetails = EnactedProposalDetails + { enactedProposalDetailsId :: Integer + , enactedProposalDetailsTxId :: Integer + , enactedProposalDetailsIndex :: Integer + , enactedProposalDetailsDescription :: Maybe Value + , enactedProposalDetailsHash :: Text + } + deriving (Show) + +instance FromRow EnactedProposalDetails where + fromRow = + EnactedProposalDetails + <$> field + <*> field + <*> (floor @Scientific <$> field) + <*> field + <*> field + +instance ToJSON EnactedProposalDetails where + toJSON EnactedProposalDetails + { enactedProposalDetailsId + , enactedProposalDetailsTxId + , enactedProposalDetailsIndex + , enactedProposalDetailsDescription + , enactedProposalDetailsHash + } = + object + [ "id" .= enactedProposalDetailsId + , "tx_id" .= enactedProposalDetailsTxId + , "index" .= enactedProposalDetailsIndex + , "description" .= enactedProposalDetailsDescription + , "hash" .= enactedProposalDetailsHash + ] + data CacheEnv = CacheEnv { proposalListCache :: Cache.Cache () [Proposal] diff --git a/govtool/backend/vva-be.cabal b/govtool/backend/vva-be.cabal index 1accd1af4..f3131a070 100644 --- a/govtool/backend/vva-be.cabal +++ b/govtool/backend/vva-be.cabal @@ -36,6 +36,7 @@ extra-source-files: sql/get-network-total-stake.sql sql/get-dreps-voting-power-list.sql sql/get-filtered-dreps-voting-power.sql + sql/get-previous-enacted-governance-action-proposal-details.sql executable vva-be main-is: Main.hs diff --git a/govtool/frontend/src/components/organisms/CreateGovernanceActionSteps/CreateGovernanceActionForm.tsx b/govtool/frontend/src/components/organisms/CreateGovernanceActionSteps/CreateGovernanceActionForm.tsx index 5f3e9b010..fbf3f0bd6 100644 --- a/govtool/frontend/src/components/organisms/CreateGovernanceActionSteps/CreateGovernanceActionForm.tsx +++ b/govtool/frontend/src/components/organisms/CreateGovernanceActionSteps/CreateGovernanceActionForm.tsx @@ -50,6 +50,8 @@ export const CreateGovernanceActionForm = ({ | GovernanceActionType.NewCommittee | GovernanceActionType.NewConstitution | GovernanceActionType.NoConfidence + | GovernanceActionType.HardForkInitiation + | GovernanceActionType.ParameterChange ], ).some( (field) => !watch(field as unknown as Parameters[0]), @@ -73,6 +75,8 @@ export const CreateGovernanceActionForm = ({ | GovernanceActionType.NewCommittee | GovernanceActionType.NewConstitution | GovernanceActionType.NoConfidence + | GovernanceActionType.HardForkInitiation + | GovernanceActionType.ParameterChange ], ).map(([key, field]) => { const fieldProps = { @@ -85,6 +89,7 @@ export const CreateGovernanceActionForm = ({ ? t(field.placeholderI18nKey) : undefined, rules: field.rules, + maxLength: field.maxLength, }; if (field.component === GovernanceActionField.Input) { diff --git a/govtool/frontend/src/consts/governanceAction/fields.ts b/govtool/frontend/src/consts/governanceAction/fields.ts index 0367b4799..2b8585390 100644 --- a/govtool/frontend/src/consts/governanceAction/fields.ts +++ b/govtool/frontend/src/consts/governanceAction/fields.ts @@ -262,6 +262,88 @@ export const GOVERNANCE_ACTION_FIELDS: GovernanceActionFields = { "createGovernanceAction.fields.declarations.scriptHash.placeholder", }, }, + [GovernanceActionType.HardForkInitiation]: { + ...sharedGovernanceActionFields, + prevGovernanceActionHash: { + component: GovernanceActionField.Input, + labelI18nKey: + "createGovernanceAction.fields.declarations.prevGovernanceActionHash.label", + placeholderI18nKey: + "createGovernanceAction.fields.declarations.prevGovernanceActionHash.placeholder", + }, + prevGovernanceActionIndex: { + component: GovernanceActionField.Input, + labelI18nKey: + "createGovernanceAction.fields.declarations.prevGovernanceActionIndex.label", + placeholderI18nKey: + "createGovernanceAction.fields.declarations.prevGovernanceActionIndex.placeholder", + rules: { + validate: numberValidation, + }, + }, + major: { + component: GovernanceActionField.Input, + labelI18nKey: "createGovernanceAction.fields.declarations.major.label", + placeholderI18nKey: + "createGovernanceAction.fields.declarations.major.placeholder", + rules: { + required: { + value: true, + message: I18n.t("createGovernanceAction.fields.validations.required"), + }, + validate: numberValidation, + }, + }, + minor: { + component: GovernanceActionField.Input, + labelI18nKey: "createGovernanceAction.fields.declarations.minor.label", + placeholderI18nKey: + "createGovernanceAction.fields.declarations.minor.placeholder", + rules: { + required: { + value: true, + message: I18n.t("createGovernanceAction.fields.validations.required"), + }, + validate: numberValidation, + }, + }, + }, + [GovernanceActionType.ParameterChange]: { + ...sharedGovernanceActionFields, + prevGovernanceActionHash: { + component: GovernanceActionField.Input, + labelI18nKey: + "createGovernanceAction.fields.declarations.prevGovernanceActionHash.label", + placeholderI18nKey: + "createGovernanceAction.fields.declarations.prevGovernanceActionHash.placeholder", + }, + prevGovernanceActionIndex: { + component: GovernanceActionField.Input, + labelI18nKey: + "createGovernanceAction.fields.declarations.prevGovernanceActionIndex.label", + placeholderI18nKey: + "createGovernanceAction.fields.declarations.prevGovernanceActionIndex.placeholder", + rules: { + validate: numberValidation, + }, + }, + protocolParameters: { + component: GovernanceActionField.TextArea, + maxLength: 5000, + labelI18nKey: + "createGovernanceAction.fields.declarations.protocolParameters.label", + placeholderI18nKey: + "createGovernanceAction.fields.declarations.protocolParameters.placeholder", + rules: { + required: { + value: true, + message: I18n.t("createGovernanceAction.fields.validations.required"), + }, + }, + tipI18nKey: + "createGovernanceAction.fields.declarations.protocolParameters.tip", + }, + }, } as const; export const GOVERNANCE_ACTION_CONTEXT = { diff --git a/govtool/frontend/src/context/wallet.tsx b/govtool/frontend/src/context/wallet.tsx index a9a38f2a9..c7a6d1492 100644 --- a/govtool/frontend/src/context/wallet.tsx +++ b/govtool/frontend/src/context/wallet.tsx @@ -130,7 +130,7 @@ type TreasuryProps = { type ProtocolParameterChangeProps = { prevGovernanceActionHash: string; - prevGovernanceActionIndex: number; + prevGovernanceActionIndex: string; protocolParamsUpdate: Partial; } & VotingAnchor; @@ -166,7 +166,6 @@ export type QuorumThreshold = { numerator: string; denominator: string; }; - type ProtocolParamsUpdate = { adaPerUtxo: string; collateralPercentage: number; @@ -1343,7 +1342,7 @@ const CardanoProvider = (props: Props) => { if (prevGovernanceActionHash && prevGovernanceActionIndex) { const prevGovernanceActionId = GovernanceActionId.new( TransactionHash.from_hex(prevGovernanceActionHash), - prevGovernanceActionIndex, + Number(prevGovernanceActionIndex), ); protocolParamChangeAction = ParameterChangeAction.new_with_policy_hash_and_action_id( diff --git a/govtool/frontend/src/hooks/forms/useCreateGovernanceActionForm.ts b/govtool/frontend/src/hooks/forms/useCreateGovernanceActionForm.ts index f792c2bfa..c13eb5bb6 100644 --- a/govtool/frontend/src/hooks/forms/useCreateGovernanceActionForm.ts +++ b/govtool/frontend/src/hooks/forms/useCreateGovernanceActionForm.ts @@ -1,4 +1,10 @@ -import { Dispatch, SetStateAction, useCallback, useState } from "react"; +import { + Dispatch, + SetStateAction, + useCallback, + useEffect, + useState, +} from "react"; import { useNavigate } from "react-router-dom"; import { useFormContext } from "react-hook-form"; import { blake2bHex } from "blakejs"; @@ -60,6 +66,8 @@ export const useCreateGovernanceActionForm = ( buildNewConstitutionGovernanceAction, buildUpdateCommitteeGovernanceAction, buildSignSubmitConwayCertTx, + buildHardForkGovernanceAction, + buildProtocolParameterChangeGovernanceAction, } = useCardano(); // App Management @@ -85,6 +93,12 @@ export const useCreateGovernanceActionForm = ( } = useFormContext(); const govActionType = watch("governance_action_type"); + useEffect(() => { + if (govActionType === GovernanceActionType.ParameterChange) { + setValue("protocolParameters", JSON.stringify(protocolParams)); + } + }, [govActionType]); + // Navigation const backToForm = useCallback(() => { setStep?.(3); @@ -212,6 +226,48 @@ export const useCreateGovernanceActionForm = ( return buildTreasuryGovernanceAction(treasuryActionDetails); } + case GovernanceActionType.HardForkInitiation: { + if ( + data.major === undefined || + data.minor === undefined || + data.prevGovernanceActionHash === undefined || + data.prevGovernanceActionIndex === undefined + ) { + throw new Error( + t("errors.invalidHardForkInitiationGovernanceActionType"), + ); + } + const hardForkActionDetails = { + ...commonGovActionDetails, + prevGovernanceActionHash: data.prevGovernanceActionHash, + prevGovernanceActionIndex: data.prevGovernanceActionIndex, + major: data.major, + minor: data.minor, + }; + return buildHardForkGovernanceAction(hardForkActionDetails); + } + + case GovernanceActionType.ParameterChange: { + if ( + data.protocolParameters === undefined || + data.prevGovernanceActionHash === undefined || + data.prevGovernanceActionIndex === undefined + ) { + throw new Error( + t("errors.invalidParameterChangeGovernanceActionType"), + ); + } + const protocolParamsUpdate = JSON.parse(data.protocolParameters); + const parameterChangeActionDetails = { + ...commonGovActionDetails, + protocolParamsUpdate, + prevGovernanceActionHash: data.prevGovernanceActionHash, + prevGovernanceActionIndex: data.prevGovernanceActionIndex, + }; + return buildProtocolParameterChangeGovernanceAction( + parameterChangeActionDetails, + ); + } default: throw new Error(t("errors.invalidGovernanceActionType")); } diff --git a/govtool/frontend/src/i18n/locales/en.json b/govtool/frontend/src/i18n/locales/en.json index d057b97cb..9e259798d 100644 --- a/govtool/frontend/src/i18n/locales/en.json +++ b/govtool/frontend/src/i18n/locales/en.json @@ -205,6 +205,21 @@ "label": "New Constitution Script Hash", "placeholder": "New Constitution Script Hash", "tip": "Script hash of the new constitution" + }, + "major": { + "label": "Major", + "placeholder": "Major version", + "tip": "Major version of the new hard fork" + }, + "minor": { + "label": "Minor", + "placeholder": "Minor version", + "tip": "Minor version of the new hard fork" + }, + "protocolParameters": { + "label": "Protocol Parameters", + "placeholder": "Protocol parameters", + "tip": "Protocol parameters to be changed" } }, "validations": { diff --git a/govtool/frontend/src/types/governanceAction.ts b/govtool/frontend/src/types/governanceAction.ts index 77bad990a..bdc53934f 100644 --- a/govtool/frontend/src/types/governanceAction.ts +++ b/govtool/frontend/src/types/governanceAction.ts @@ -12,8 +12,8 @@ export enum GovernanceActionType { } export enum GovernanceActionField { - Input = "input", - TextArea = "textarea", + Input = "Input", + TextArea = "TextArea", } export type FieldSchema = { @@ -22,6 +22,7 @@ export type FieldSchema = { placeholderI18nKey: NestedKeys; tipI18nKey?: NestedKeys; rules?: Omit; + maxLength?: number; }; // Following properties are based on [CIP-108](https://github.com/Ryun1/CIPs/blob/governance-metadata-actions/CIP-0108/README.md) @@ -60,19 +61,27 @@ export type NewConstitutionActionFieldSchema = Partial<{ constitutionHash: FieldSchema; scriptHash: FieldSchema; }>; +export type ProtocolParameterActionFieldSchema = Partial<{ + prevGovernanceActionHash: FieldSchema; + prevGovernanceActionIndex: FieldSchema; + protocolParameters: FieldSchema; +}>; export type GovernanceActionFieldSchemas = | SharedGovernanceActionFieldSchema & TreasuryGovernanceActionFieldSchema & NewCommitteeActionFieldSchema & HardForkInitiationActionFieldSchema & - NewConstitutionActionFieldSchema; + NewConstitutionActionFieldSchema & + ProtocolParameterActionFieldSchema; export type GovernanceActionFields = Record< | GovernanceActionType.InfoAction | GovernanceActionType.TreasuryWithdrawals | GovernanceActionType.NoConfidence | GovernanceActionType.NewCommittee - | GovernanceActionType.NewConstitution, + | GovernanceActionType.NewConstitution + | GovernanceActionType.HardForkInitiation + | GovernanceActionType.ParameterChange, GovernanceActionFieldSchemas >;