Skip to content

Commit a2cbaff

Browse files
authored
Merge pull request #3660 from IntersectMBO/feat/add-support-for-hard-fork-and-protocol-parameter-gov-actions-creation
feat: add support for Parameter Change and Hard Fork Initiation gov actions creation
2 parents ae73c1d + a39bd39 commit a2cbaff

File tree

13 files changed

+338
-17
lines changed

13 files changed

+338
-17
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ changes.
1414
- Add CIP-129 support for gov_actions hashes in Live Voting (governance actions) [Issue 3619](https://github.com/IntersectMBO/govtool/issues/3619)
1515

1616
- Add maintenance ending banner [Issue 3647](https://github.com/IntersectMBO/govtool/issues/3647)
17+
- Add support for the Protocol Parameter Change and Hard Fork Initiation governance actions
1718

1819
### Fixed
1920

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
SELECT
2+
gap.id,
3+
tx_id,
4+
index,
5+
description,
6+
encode(hash, 'hex') AS hash
7+
FROM
8+
gov_action_proposal gap
9+
JOIN
10+
tx ON gap.tx_id = tx.id
11+
WHERE
12+
gap.type = ? AND gap.enacted_epoch IS NOT NULL
13+
ORDER BY
14+
gap.id DESC
15+
LIMIT 1;

govtool/backend/src/VVA/API.hs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ type VVAApi =
7878
:> QueryParam "search" Text
7979
:> Get '[JSON] ListProposalsResponse
8080
:<|> "proposal" :> "get" :> Capture "proposalId" GovActionId :> QueryParam "drepId" HexText :> Get '[JSON] GetProposalResponse
81+
:<|> "proposal" :> "enacted-details" :> QueryParam "type" GovernanceActionType :> Get '[JSON] (Maybe EnactedProposalDetailsResponse)
8182
:<|> "epoch" :> "params" :> Get '[JSON] GetCurrentEpochParamsResponse
8283
:<|> "transaction" :> "status" :> Capture "transactionId" HexText :> Get '[JSON] GetTransactionStatusResponse
8384
:<|> "throw500" :> Get '[JSON] ()
@@ -95,6 +96,7 @@ server = drepList
9596
:<|> getStakeKeyVotingPower
9697
:<|> listProposals
9798
:<|> getProposal
99+
:<|> getEnactedProposalDetails
98100
:<|> getCurrentEpochParams
99101
:<|> getTransactionStatus
100102
:<|> throw500
@@ -442,6 +444,33 @@ getProposal g@(GovActionId govActionTxHash govActionIndex) mDrepId' = do
442444
, getProposalResponseVote = voteResponse
443445
}
444446

447+
getEnactedProposalDetails :: App m => Maybe GovernanceActionType -> m (Maybe EnactedProposalDetailsResponse)
448+
getEnactedProposalDetails maybeType = do
449+
let proposalType = maybe "HardForkInitiation" governanceActionTypeToText maybeType
450+
451+
mDetails <- Proposal.getPreviousEnactedProposal proposalType
452+
453+
let response = enactedProposalDetailsToResponse <$> mDetails
454+
455+
return response
456+
where
457+
governanceActionTypeToText :: GovernanceActionType -> Text
458+
governanceActionTypeToText actionType =
459+
case actionType of
460+
HardForkInitiation -> "HardForkInitiation"
461+
ParameterChange -> "ParameterChange"
462+
_ -> "HardForkInitiation"
463+
464+
enactedProposalDetailsToResponse :: Types.EnactedProposalDetails -> EnactedProposalDetailsResponse
465+
enactedProposalDetailsToResponse Types.EnactedProposalDetails{..} =
466+
EnactedProposalDetailsResponse
467+
{ enactedProposalDetailsResponseId = enactedProposalDetailsId
468+
, enactedProposalDetailsResponseTxId = enactedProposalDetailsTxId
469+
, enactedProposalDetailsResponseIndex = enactedProposalDetailsIndex
470+
, enactedProposalDetailsResponseDescription = enactedProposalDetailsDescription
471+
, enactedProposalDetailsResponseHash = HexText enactedProposalDetailsHash
472+
}
473+
445474
getCurrentEpochParams :: App m => m GetCurrentEpochParamsResponse
446475
getCurrentEpochParams = do
447476
CacheEnv {currentEpochCache} <- asks vvaCache

govtool/backend/src/VVA/API/Types.hs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,40 @@ instance ToSchema ProposalResponse where
455455
& example
456456
?~ toJSON exampleProposalResponse
457457

458+
data EnactedProposalDetailsResponse
459+
= EnactedProposalDetailsResponse
460+
{ enactedProposalDetailsResponseId :: Integer
461+
, enactedProposalDetailsResponseTxId :: Integer
462+
, enactedProposalDetailsResponseIndex :: Integer
463+
, enactedProposalDetailsResponseDescription :: Maybe Value
464+
, enactedProposalDetailsResponseHash :: HexText
465+
}
466+
deriving (Generic, Show)
467+
468+
deriveJSON (jsonOptions "enactedProposalDetailsResponse") ''EnactedProposalDetailsResponse
469+
470+
exampleEnactedProposalDetailsResponse :: Text
471+
exampleEnactedProposalDetailsResponse = "{ \"id\": 123,"
472+
<> "\"txId\": 456,"
473+
<> "\"index\": 0,"
474+
<> "\"description\": {\"key\": \"value\"},"
475+
<> "\"hash\": \"9af10e89979e51b8cdc827c963124a1ef4920d1253eef34a1d5cfe76438e3f11\"}"
476+
477+
instance ToSchema EnactedProposalDetailsResponse where
478+
declareNamedSchema proxy = do
479+
NamedSchema name_ schema_ <-
480+
genericDeclareNamedSchema
481+
( fromAesonOptions $
482+
jsonOptions "enactedProposalDetailsResponse"
483+
)
484+
proxy
485+
return $
486+
NamedSchema name_ $
487+
schema_
488+
& description ?~ "Enacted Proposal Details Response"
489+
& example
490+
?~ toJSON exampleEnactedProposalDetailsResponse
491+
458492
exampleListProposalsResponse :: Text
459493
exampleListProposalsResponse =
460494
"{ \"page\": 0,"

govtool/backend/src/VVA/Proposal.hs

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,25 +17,34 @@ import Data.Aeson.Types (Parser, parseMaybe)
1717
import Data.ByteString (ByteString)
1818
import Data.FileEmbed (embedFile)
1919
import Data.Foldable (fold)
20-
import Data.Has (Has)
21-
import qualified Data.Map as Map
22-
import Data.Maybe (fromMaybe)
20+
import Data.Has (Has, getter)
21+
import qualified Data.Map as Map
22+
import Data.Maybe (fromMaybe, isJust)
2323
import Data.Monoid (Sum (..), getSum)
2424
import Data.Scientific
2525
import Data.String (fromString)
2626
import Data.Text (Text, pack, unpack)
27-
import qualified Data.Text.Encoding as Text
28-
import qualified Data.Text.IO as Text
27+
import qualified Data.Text.Encoding as Text
28+
import qualified Data.Text.IO as Text
2929
import Data.Time
3030

31-
import qualified Database.PostgreSQL.Simple as SQL
32-
import qualified Database.PostgreSQL.Simple.Types as PG
31+
import qualified Database.PostgreSQL.Simple as SQL
32+
import qualified Database.PostgreSQL.Simple.Types as PG
3333
import Database.PostgreSQL.Simple.ToField (ToField(..))
3434
import Database.PostgreSQL.Simple.ToRow (ToRow(..))
3535

36+
import GHC.IO.Unsafe (unsafePerformIO)
37+
3638
import VVA.Config
3739
import VVA.Pool (ConnectionPool, withPool)
38-
import VVA.Types (AppError (..), Proposal (..))
40+
import VVA.Types (AppError (..), Proposal (..), EnactedProposalDetails (..))
41+
42+
query1 :: (SQL.ToRow q, SQL.FromRow r) => SQL.Connection -> SQL.Query -> q -> IO (Maybe r)
43+
query1 conn q params = do
44+
results <- SQL.query conn q params
45+
case results of
46+
[x] -> return (Just x)
47+
_ -> return Nothing
3948

4049
sqlFrom :: ByteString -> SQL.Query
4150
sqlFrom bs = fromString $ unpack $ Text.decodeUtf8 bs
@@ -84,4 +93,36 @@ getProposals mSearchTerms = withPool $ \conn -> do
8493
Left (e :: SomeException) -> do
8594
putStrLn $ "Error fetching proposals: " <> show e
8695
return []
87-
Right rows -> return rows
96+
Right rows -> return rows
97+
98+
latestEnactedProposalSql :: SQL.Query
99+
latestEnactedProposalSql =
100+
let rawSql = sqlFrom $(embedFile "sql/get-previous-enacted-governance-action-proposal-details.sql")
101+
in unsafePerformIO $ do
102+
putStrLn $ "[DEBUG] SQL query content: " ++ show rawSql
103+
return rawSql
104+
105+
getPreviousEnactedProposal ::
106+
(Has ConnectionPool r, Has VVAConfig r, MonadReader r m, MonadIO m, MonadFail m, MonadError AppError m) =>
107+
Text ->
108+
m (Maybe EnactedProposalDetails)
109+
getPreviousEnactedProposal proposalType = withPool $ \conn -> do
110+
let query = latestEnactedProposalSql
111+
let params = [proposalType]
112+
113+
result <- liftIO $ try $ do
114+
rows <- SQL.query conn query params :: IO [EnactedProposalDetails]
115+
case rows of
116+
[x] -> return (Just x)
117+
_ -> return Nothing
118+
119+
case result of
120+
Left err -> do
121+
throwError $ CriticalError $ "Database error: " <> pack (show (err :: SomeException))
122+
Right proposal -> do
123+
case proposal of
124+
Just details -> do
125+
liftIO $ putStrLn $ "[DEBUG] Previous enacted proposal details: " ++ show details
126+
Nothing ->
127+
liftIO $ putStrLn "[DEBUG] No previous enacted proposal found"
128+
return proposal

govtool/backend/src/VVA/Types.hs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,40 @@ instance ToJSON TransactionStatus where
211211
, "votingProcedure" .= votingProcedure
212212
]
213213

214+
data EnactedProposalDetails = EnactedProposalDetails
215+
{ enactedProposalDetailsId :: Integer
216+
, enactedProposalDetailsTxId :: Integer
217+
, enactedProposalDetailsIndex :: Integer
218+
, enactedProposalDetailsDescription :: Maybe Value
219+
, enactedProposalDetailsHash :: Text
220+
}
221+
deriving (Show)
222+
223+
instance FromRow EnactedProposalDetails where
224+
fromRow =
225+
EnactedProposalDetails
226+
<$> field
227+
<*> field
228+
<*> (floor @Scientific <$> field)
229+
<*> field
230+
<*> field
231+
232+
instance ToJSON EnactedProposalDetails where
233+
toJSON EnactedProposalDetails
234+
{ enactedProposalDetailsId
235+
, enactedProposalDetailsTxId
236+
, enactedProposalDetailsIndex
237+
, enactedProposalDetailsDescription
238+
, enactedProposalDetailsHash
239+
} =
240+
object
241+
[ "id" .= enactedProposalDetailsId
242+
, "tx_id" .= enactedProposalDetailsTxId
243+
, "index" .= enactedProposalDetailsIndex
244+
, "description" .= enactedProposalDetailsDescription
245+
, "hash" .= enactedProposalDetailsHash
246+
]
247+
214248
data CacheEnv
215249
= CacheEnv
216250
{ proposalListCache :: Cache.Cache () [Proposal]

govtool/backend/vva-be.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ extra-source-files:
3636
sql/get-network-total-stake.sql
3737
sql/get-dreps-voting-power-list.sql
3838
sql/get-filtered-dreps-voting-power.sql
39+
sql/get-previous-enacted-governance-action-proposal-details.sql
3940
executable vva-be
4041
main-is: Main.hs
4142

govtool/frontend/src/components/organisms/CreateGovernanceActionSteps/CreateGovernanceActionForm.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ export const CreateGovernanceActionForm = ({
5050
| GovernanceActionType.NewCommittee
5151
| GovernanceActionType.NewConstitution
5252
| GovernanceActionType.NoConfidence
53+
| GovernanceActionType.HardForkInitiation
54+
| GovernanceActionType.ParameterChange
5355
],
5456
).some(
5557
(field) => !watch(field as unknown as Parameters<typeof watch>[0]),
@@ -73,6 +75,8 @@ export const CreateGovernanceActionForm = ({
7375
| GovernanceActionType.NewCommittee
7476
| GovernanceActionType.NewConstitution
7577
| GovernanceActionType.NoConfidence
78+
| GovernanceActionType.HardForkInitiation
79+
| GovernanceActionType.ParameterChange
7680
],
7781
).map(([key, field]) => {
7882
const fieldProps = {
@@ -85,6 +89,7 @@ export const CreateGovernanceActionForm = ({
8589
? t(field.placeholderI18nKey)
8690
: undefined,
8791
rules: field.rules,
92+
maxLength: field.maxLength,
8893
};
8994

9095
if (field.component === GovernanceActionField.Input) {

govtool/frontend/src/consts/governanceAction/fields.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,88 @@ export const GOVERNANCE_ACTION_FIELDS: GovernanceActionFields = {
262262
"createGovernanceAction.fields.declarations.scriptHash.placeholder",
263263
},
264264
},
265+
[GovernanceActionType.HardForkInitiation]: {
266+
...sharedGovernanceActionFields,
267+
prevGovernanceActionHash: {
268+
component: GovernanceActionField.Input,
269+
labelI18nKey:
270+
"createGovernanceAction.fields.declarations.prevGovernanceActionHash.label",
271+
placeholderI18nKey:
272+
"createGovernanceAction.fields.declarations.prevGovernanceActionHash.placeholder",
273+
},
274+
prevGovernanceActionIndex: {
275+
component: GovernanceActionField.Input,
276+
labelI18nKey:
277+
"createGovernanceAction.fields.declarations.prevGovernanceActionIndex.label",
278+
placeholderI18nKey:
279+
"createGovernanceAction.fields.declarations.prevGovernanceActionIndex.placeholder",
280+
rules: {
281+
validate: numberValidation,
282+
},
283+
},
284+
major: {
285+
component: GovernanceActionField.Input,
286+
labelI18nKey: "createGovernanceAction.fields.declarations.major.label",
287+
placeholderI18nKey:
288+
"createGovernanceAction.fields.declarations.major.placeholder",
289+
rules: {
290+
required: {
291+
value: true,
292+
message: I18n.t("createGovernanceAction.fields.validations.required"),
293+
},
294+
validate: numberValidation,
295+
},
296+
},
297+
minor: {
298+
component: GovernanceActionField.Input,
299+
labelI18nKey: "createGovernanceAction.fields.declarations.minor.label",
300+
placeholderI18nKey:
301+
"createGovernanceAction.fields.declarations.minor.placeholder",
302+
rules: {
303+
required: {
304+
value: true,
305+
message: I18n.t("createGovernanceAction.fields.validations.required"),
306+
},
307+
validate: numberValidation,
308+
},
309+
},
310+
},
311+
[GovernanceActionType.ParameterChange]: {
312+
...sharedGovernanceActionFields,
313+
prevGovernanceActionHash: {
314+
component: GovernanceActionField.Input,
315+
labelI18nKey:
316+
"createGovernanceAction.fields.declarations.prevGovernanceActionHash.label",
317+
placeholderI18nKey:
318+
"createGovernanceAction.fields.declarations.prevGovernanceActionHash.placeholder",
319+
},
320+
prevGovernanceActionIndex: {
321+
component: GovernanceActionField.Input,
322+
labelI18nKey:
323+
"createGovernanceAction.fields.declarations.prevGovernanceActionIndex.label",
324+
placeholderI18nKey:
325+
"createGovernanceAction.fields.declarations.prevGovernanceActionIndex.placeholder",
326+
rules: {
327+
validate: numberValidation,
328+
},
329+
},
330+
protocolParameters: {
331+
component: GovernanceActionField.TextArea,
332+
maxLength: 5000,
333+
labelI18nKey:
334+
"createGovernanceAction.fields.declarations.protocolParameters.label",
335+
placeholderI18nKey:
336+
"createGovernanceAction.fields.declarations.protocolParameters.placeholder",
337+
rules: {
338+
required: {
339+
value: true,
340+
message: I18n.t("createGovernanceAction.fields.validations.required"),
341+
},
342+
},
343+
tipI18nKey:
344+
"createGovernanceAction.fields.declarations.protocolParameters.tip",
345+
},
346+
},
265347
} as const;
266348

267349
export const GOVERNANCE_ACTION_CONTEXT = {

govtool/frontend/src/context/wallet.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ type TreasuryProps = {
130130

131131
type ProtocolParameterChangeProps = {
132132
prevGovernanceActionHash: string;
133-
prevGovernanceActionIndex: number;
133+
prevGovernanceActionIndex: string;
134134
protocolParamsUpdate: Partial<ProtocolParamsUpdate>;
135135
} & VotingAnchor;
136136

@@ -166,7 +166,6 @@ export type QuorumThreshold = {
166166
numerator: string;
167167
denominator: string;
168168
};
169-
170169
type ProtocolParamsUpdate = {
171170
adaPerUtxo: string;
172171
collateralPercentage: number;
@@ -1343,7 +1342,7 @@ const CardanoProvider = (props: Props) => {
13431342
if (prevGovernanceActionHash && prevGovernanceActionIndex) {
13441343
const prevGovernanceActionId = GovernanceActionId.new(
13451344
TransactionHash.from_hex(prevGovernanceActionHash),
1346-
prevGovernanceActionIndex,
1345+
Number(prevGovernanceActionIndex),
13471346
);
13481347
protocolParamChangeAction =
13491348
ParameterChangeAction.new_with_policy_hash_and_action_id(

0 commit comments

Comments
 (0)