From 9c552a0b76b89d695e2d9cf86f388ff7adf2d7b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Sza=C5=82owski?= Date: Mon, 26 Aug 2024 15:35:08 +0200 Subject: [PATCH] feat: HF, PPC and guardrails script support for Governance Action builders --- CHANGELOG.md | 1 + docs/GOVERNANCE_ACTION_SUBMISSION.md | 91 +++++++- govtool/frontend/src/consts/index.ts | 3 + govtool/frontend/src/context/wallet.tsx | 210 +++++++++++++++++- govtool/frontend/src/utils/index.ts | 1 + .../src/utils/setProtocolParameterUpdate.ts | 28 +++ .../tests/setProtocolParameterUpdate.test.ts | 52 +++++ 7 files changed, 377 insertions(+), 9 deletions(-) create mode 100644 govtool/frontend/src/utils/setProtocolParameterUpdate.ts create mode 100644 govtool/frontend/src/utils/tests/setProtocolParameterUpdate.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f1415220..3ea0d99a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ changes. - Add support for hard fork initiation previous governance action data [Issue 1600](https://github.com/IntersectMBO/govtool/issues/1600) - Add support for CIP-119 on the backend and metadata validation [Issue 1758](https://github.com/IntersectMBO/govtool/issues/1758) - Add support for CIP-119 on the frontend [Issue 1760](https://github.com/IntersectMBO/govtool/issues/1758) +- Add support for HF Initiation and Protocol Parameter Change governance action builders [Issue 1600](https://github.com/IntersectMBO/govtool/issues/1600) & [Issue 1601](https://github.com/IntersectMBO/govtool/issues/1601) ### Fixed diff --git a/docs/GOVERNANCE_ACTION_SUBMISSION.md b/docs/GOVERNANCE_ACTION_SUBMISSION.md index 2262d5ddd..c2aa08257 100644 --- a/docs/GOVERNANCE_ACTION_SUBMISSION.md +++ b/docs/GOVERNANCE_ACTION_SUBMISSION.md @@ -15,7 +15,15 @@ For creating the Governance Action, you need to consume 2 utility methods provid ### Types ```typescript -import { VotingProposalBuilder } from "@emurgo/cardano-serialization-lib-nodejs"; +import { + VotingProposalBuilder, + Costmdls, + DrepVotingThresholds, + ExUnitPrices, + UnitInterval, + ExUnits, + PoolVotingThresholds, +} from "@emurgo/cardano-serialization-lib-nodejs"; interface GovernanceAction { title: string; @@ -37,6 +45,48 @@ interface TreasuryProps { url: string; } +type ProtocolParamsUpdate = { + adaPerUtxo: string; + collateralPercentage: number; + committeeTermLimit: number; + costModels: Costmdls; + drepDeposit: string; + drepInactivityPeriod: number; + drepVotingThresholds: DrepVotingThresholds; + executionCosts: ExUnitPrices; + expansionRate: UnitInterval; + governanceActionDeposit: string; + governanceActionValidityPeriod: number; + keyDeposit: string; + maxBlockBodySize: number; + maxBlockExUnits: ExUnits; + maxBlockHeaderSize: number; + maxCollateralInputs: number; + maxEpoch: number; + maxTxExUnits: ExUnits; + maxTxSize: number; + maxValueSize: number; + minCommitteeSize: number; + minPoolCost: string; + minFeeA: string; + minFeeB: string; + nOpt: number; + poolDeposit: string; + poolPledgeInfluence: UnitInterval; + poolVotingThresholds: PoolVotingThresholds; + refScriptCoinsPerByte: UnitInterval; + treasuryGrowthRate: UnitInterval; +}; + +interface ProtocolParameterChangeProps { + prevGovernanceActionHash: string; + prevGovernanceActionIndex: number; + url: string; + hash: string; + + protocolParamsUpdate: Partial; +} + const createGovernanceActionJsonLD: ( governanceAction: GovernanceAction ) => NodeObject; @@ -111,10 +161,14 @@ Example: ```typescript // When used within a CardanoProvider -const { buildSignSubmitConwayCertTx, buildNewInfoGovernanceAction } = - useCardano(); - -// hash of the generated Governance Action metadata, url of the metadata +const { + buildSignSubmitConwayCertTx, + buildNewInfoGovernanceAction, + buildProtocolParameterChangeGovernanceAction, + buildHardForkInitiationGovernanceAction, +} = useCardano(); + +// Info Governance Action const govActionBuilder = await buildNewInfoGovernanceAction({ hash, url }); // sign and submit the transaction @@ -123,7 +177,7 @@ await buildSignSubmitConwayCertTx({ type: "createGovAction", }); -// or if you want to use the Treasury Governance Action +// Treasury Governance Action const { buildTreasuryGovernanceAction } = useCardano(); // hash of the generated Governance Action metadata, url of the metadata, amount of the transaction, receiving address is the stake key address @@ -134,6 +188,31 @@ const govActionBuilder = await buildTreasuryGovernanceAction({ receivingAddress, }); +// Protocol Parameter Change Governance Action +const { buildProtocolParameterChangeGovernanceAction } = useCardano(); + +// hash of the previous Governance Action, index of the previous Governance Action, url of the metadata, hash of the metadata, and the updated protocol parameters +const govActionBuilder = await buildProtocolParameterChangeGovernanceAction({ + prevGovernanceActionHash, + prevGovernanceActionIndex, + url, + hash, + protocolParamsUpdate, +}); + +// Hard Fork Initiation Governance Action +const { buildHardForkInitiationGovernanceAction } = useCardano(); + +// hash of the previous Governance Action, index of the previous Governance Action, url of the metadata, hash of the metadata, and the major and minor numbers of the hard fork initiation +const govActionBuilder = await buildHardForkInitiationGovernanceAction({ + prevGovernanceActionHash, + prevGovernanceActionIndex, + url, + hash, + major, + minor, +}); + // sign and submit the transaction await buildSignSubmitConwayCertTx({ govActionBuilder, diff --git a/govtool/frontend/src/consts/index.ts b/govtool/frontend/src/consts/index.ts index 5e3c2cb78..7d48cede4 100644 --- a/govtool/frontend/src/consts/index.ts +++ b/govtool/frontend/src/consts/index.ts @@ -24,3 +24,6 @@ export const CEXPLORER_BASE_URLS = { testnet: "https://testnet.cexplorer.io", mainnet: "https://cexplorer.io", }; + +export const GUARDRAIL_SCRIPT = + "59082f59082c0101003232323232323232323232323232323232323232323232323232323232323232323232323232323232323225932325333573466e1d2000001180098121bab357426ae88d55cf001054ccd5cd19b874801000460042c6aae74004dd51aba1357446ae88d55cf1baa325333573466e1d200a35573a00226ae84d5d11aab9e0011637546ae84d5d11aba235573c6ea800642b26006003149a2c8a4c3021801c0052000c00e0070018016006901e40608058c00e00290016007003800c00b0034830268320306007001800600690406d6204e00060001801c0052004c00e007001801600690404001e0006007001800600690404007e00060001801c0052006c00e006023801c006001801a4101000980018000600700148023003801808e0070018006006904827600060001801c005200ac00e0070018016006904044bd4060c00e003000c00d2080ade204c000c0003003800a4019801c00e003002c00d2080cab5ee0180c100d1801c005200ec00e0060238000c00e00290086007003800c00b003483d00e0306007001800600690500fe00040243003800a4025803c00c01a0103003800a4029803c00e003002c00cc07520d00f8079801c006001801980ea4120078001800060070014805b00780180360070018006006603e900a4038c0003003800a4041801c00c04601a3003800a4045801c00e003002c00d20f02e80c1801c006001801a4190cb80010090c00e00290126000c00e0029013600b003803c00e003002c00cc0752032c000c00e003000c00cc075200ac000c0006007007801c006005801980ea418170058001801c006001801980ea41209d80018000c0003003800a4051802c00e007003011c00e003000c00d2080e89226c000c0006007003801808e007001800600690406c4770b7e000600030000c00e0029015600b003801c00c047003800c00300348202e2e1cb00030001801c00e006023801c006001801a410181f905540580018000c0003003800a4059801c00c047003800c00300348203000700030000c00e00290176007003800c00b003483200603060070018006006904801e00040243003800a4061801c00c0430001801c0052032c016006003801801e00600780180140100c00e002901a600b003001c00c00f003003c00c00f003002c00c007003001c00c007003803c00e003002c00c0560184014802000c00e002901b6007003800c00b003480030034801b0001801c006001801a4029800180006007001480e3003801c006005801a4001801a40498000c00e003000c00d20ca04c00080486007001480eb00380180860070018006006900f600060001801c005203cc00e006015801c006001801a4101012bcf138c09800180006007001480fb003801805600700180060069040505bc3f482e00060001801c0052040c00e0070018016006900d4060c00e003000c00d204ac000c0003003800a4085801c00c04601630000000000200f003006c00e003000c00c05a0166000200f003005c00e003000c00c057003010c0006000200f003800c00b003012c00cc05d2028c0004008801c01e007001801600602380010043000400e003000c00c04b003011c0006000800c00b00300d8049001801600601d801980924190038000801c0060010066000801c00600900f6000800c00b003480030034820225eb0001003800c003003483403f0003000400c023000400e003000c00d208094ebdc03c000c001003009c001003300f4800b0004006005801a40058001001801401c6014900518052402860169004180424008600a900a180324005003480030001806240cc6016900d18052402460129004180424004600e900018032400c6014446666aae7c004a0005003328009aab9d0019aab9e0011aba100298019aba200224c6012444a6520071300149a4432005225900689802a4d2219002912c998099bad0020068ac99807002800c4cc03001c00e300244cc03001c02a3002012c801460012218010c00888004c004880094cc8c0040048848c8cc0088c00888c00800c8c00888c00400c8d4cc01001000cd400c0044888cc00c896400a300090999804c00488ccd5cd19b87002001800400a01522333573466e2000800600100291199ab9a33712004003000801488ccd5cd19b89002001801400244666ae68cdc4001000c00a001225333573466e240080044004400a44a666ae68cdc4801000880108008004dd6801484cc010004dd6001484c8ccc02a002452005229003912999ab9a3370e0080042666ae68cdc3801800c00200430022452005229003911980899b820040013370400400648a400a45200722333573466e20cdc100200099b82002003800400880648a400a45200722333573466e24cdc100200099b82002003801400091480148a400e44666ae68cdc419b8200400133704004007002800122593300e0020018800c400922593300e00200188014400400233323357346ae8cd5d10009198051bad357420066eb4d5d08011aba2001268001bac00214800c8ccd5cd1aba3001800400a444b26600c0066ae8400626600a0046ae8800630020c0148894ccd5cd19b87480000045854ccd5cd19b88001480004cc00ccdc0a400000466e05200000113280099b8400300199b840020011980200100098021112999ab9a3370e9000000880109980180099b860020012223300622590018c002443200522323300d225900189804803488564cc0140080322600800318010004b20051900991111111001a3201322222222005448964ce402e444444440100020018c00a30000002225333573466e1c00800460002a666ae68cdc48010008c010600445200522900391199ab9a3371266e08010004cdc1001001c0020041191800800918011198010010009"; diff --git a/govtool/frontend/src/context/wallet.tsx b/govtool/frontend/src/context/wallet.tsx index c5e47ee89..6c9fc7009 100644 --- a/govtool/frontend/src/context/wallet.tsx +++ b/govtool/frontend/src/context/wallet.tsx @@ -41,6 +41,17 @@ import { TreasuryWithdrawals, TreasuryWithdrawalsAction, ChangeConfig, + PlutusScript, + ProtocolParamUpdate, + ParameterChangeAction, + Costmdls, + DrepVotingThresholds, + ExUnitPrices, + UnitInterval, + ExUnits, + PoolVotingThresholds, + ProtocolVersion, + HardForkInitiationAction, } from "@emurgo/cardano-serialization-lib-asmjs"; import { Buffer } from "buffer"; import { useNavigate } from "react-router-dom"; @@ -48,7 +59,7 @@ import { Link } from "@mui/material"; import * as Sentry from "@sentry/react"; import { Trans } from "react-i18next"; -import { PATHS } from "@consts"; +import { PATHS, GUARDRAIL_SCRIPT } from "@consts"; import { CardanoApiWallet, Protocol, VoterInfo } from "@models"; import type { StatusModalState } from "@organisms"; import { @@ -62,6 +73,7 @@ import { NETWORK_INFO_KEY, setItemToLocalStorage, WALLET_LS_KEY, + setProtocolParameterUpdate, } from "@utils"; import { useTranslation } from "@hooks"; import { AutomatedVotingOptionDelegationId } from "@/types/automatedVotingOptions"; @@ -97,6 +109,56 @@ type TreasuryProps = { url: string; }; +type ProtocolParamsUpdate = { + adaPerUtxo: string; + collateralPercentage: number; + committeeTermLimit: number; + costModels: Costmdls; + drepDeposit: string; + drepInactivityPeriod: number; + drepVotingThresholds: DrepVotingThresholds; + executionCosts: ExUnitPrices; + expansionRate: UnitInterval; + governanceActionDeposit: string; + governanceActionValidityPeriod: number; + keyDeposit: string; + maxBlockBodySize: number; + maxBlockExUnits: ExUnits; + maxBlockHeaderSize: number; + maxCollateralInputs: number; + maxEpoch: number; + maxTxExUnits: ExUnits; + maxTxSize: number; + maxValueSize: number; + minCommitteeSize: number; + minPoolCost: string; + minFeeA: string; + minFeeB: string; + nOpt: number; + poolDeposit: string; + poolPledgeInfluence: UnitInterval; + poolVotingThresholds: PoolVotingThresholds; + refScriptCoinsPerByte: UnitInterval; + treasuryGrowthRate: UnitInterval; +}; + +type ProtocolParameterChangeProps = { + prevGovernanceActionHash: string; + prevGovernanceActionIndex: number; + url: string; + hash: string; + protocolParamsUpdate: Partial; +}; + +type HardForkInitiationProps = { + prevGovernanceActionHash: string; + prevGovernanceActionIndex: number; + url: string; + hash: string; + major: number; + minor: number; +}; + type BuildSignSubmitConwayCertTxArgs = { certBuilder?: CertificatesBuilder | Certificate; govActionBuilder?: VotingProposalBuilder; @@ -151,6 +213,12 @@ interface CardanoContextType { buildTreasuryGovernanceAction: ( treasuryProps: TreasuryProps, ) => Promise; + buildProtocolParameterChangeGovernanceAction: ( + protocolParamsProps: ProtocolParameterChangeProps, + ) => Promise; + buildHardForkGovernanceAction: ( + hardForkInitiationProps: HardForkInitiationProps, + ) => Promise; } type Utxos = { @@ -829,8 +897,13 @@ const CardanoProvider = (props: Props) => { const myWithdrawal = BigNum.from_str(amount); const withdrawals = TreasuryWithdrawals.new(); withdrawals.insert(treasuryTarget, myWithdrawal); - // Create new treasury withdrawal gov act - const treasuryAction = TreasuryWithdrawalsAction.new(withdrawals); + const guardrailScript = PlutusScript.from_bytes_v3( + Buffer.from(GUARDRAIL_SCRIPT, "hex"), + ); + const treasuryAction = TreasuryWithdrawalsAction.new_with_policy_hash( + withdrawals, + guardrailScript.hash(), + ); const treasuryGovAct = GovernanceAction.new_treasury_withdrawals_action(treasuryAction); // Create an anchor @@ -856,6 +929,133 @@ const CardanoProvider = (props: Props) => { [epochParams, getRewardAddress], ); + const buildProtocolParameterChangeGovernanceAction = useCallback( + async ({ + prevGovernanceActionHash, + prevGovernanceActionIndex, + url, + hash, + protocolParamsUpdate, + }: ProtocolParameterChangeProps) => { + const govActionBuilder = VotingProposalBuilder.new(); + + try { + const protocolParameterUpdate = ProtocolParamUpdate.new(); + + // eslint-disable-next-line no-restricted-syntax + for (const [key, value] of Object.entries(protocolParamsUpdate)) { + setProtocolParameterUpdate(protocolParameterUpdate, key, value); + } + + const guardrailScript = PlutusScript.from_bytes_v3( + Buffer.from(GUARDRAIL_SCRIPT, "hex"), + ); + let protocolParamChangeAction; + if (prevGovernanceActionHash && prevGovernanceActionIndex) { + const prevGovernanceActionId = GovernanceActionId.new( + TransactionHash.from_hex(prevGovernanceActionHash), + prevGovernanceActionIndex, + ); + protocolParamChangeAction = + ParameterChangeAction.new_with_policy_hash_and_action_id( + prevGovernanceActionId, + protocolParameterUpdate, + guardrailScript.hash(), + ); + } else { + protocolParamChangeAction = + ParameterChangeAction.new_with_policy_hash( + protocolParameterUpdate, + guardrailScript.hash(), + ); + } + + const protocolParamChangeGovAct = + GovernanceAction.new_parameter_change_action( + protocolParamChangeAction, + ); + + // Create an anchor + const anchor = generateAnchor(url, hash); + + const rewardAddr = await getRewardAddress(); + + if (!rewardAddr) throw new Error("Can not get reward address"); + // Create voting proposal + const votingProposal = VotingProposal.new( + protocolParamChangeGovAct, + anchor, + rewardAddr, + BigNum.from_str(epochParams?.gov_action_deposit.toString()), + ); + govActionBuilder.add(votingProposal); + + return govActionBuilder; + } catch (err) { + console.error(err); + } + }, + [], + ); + + const buildHardForkGovernanceAction = useCallback( + async ({ + prevGovernanceActionHash, + prevGovernanceActionIndex, + url, + hash, + major, + minor, + }: HardForkInitiationProps) => { + const govActionBuilder = VotingProposalBuilder.new(); + try { + const newProtocolVersion = ProtocolVersion.new(major, minor); + + let hardForkInitiationAction; + if (prevGovernanceActionHash && prevGovernanceActionIndex) { + const prevGovernanceActionId = GovernanceActionId.new( + TransactionHash.from_hex(prevGovernanceActionHash), + prevGovernanceActionIndex, + ); + hardForkInitiationAction = + HardForkInitiationAction.new_with_action_id( + prevGovernanceActionId, + newProtocolVersion, + ); + } else { + hardForkInitiationAction = + HardForkInitiationAction.new(newProtocolVersion); + } + + const hardForkInitiationGovAct = + GovernanceAction.new_hard_fork_initiation_action( + hardForkInitiationAction, + ); + + // Create an anchor + const anchor = generateAnchor(url, hash); + + const rewardAddr = await getRewardAddress(); + + if (!rewardAddr) throw new Error("Can not get reward address"); + + // Create voting proposal + const votingProposal = VotingProposal.new( + hardForkInitiationGovAct, + anchor, + rewardAddr, + BigNum.from_str(epochParams?.gov_action_deposit.toString()), + ); + govActionBuilder.add(votingProposal); + + return govActionBuilder; + } catch (err) { + console.error(err); + } + }, + [], + ); + const value = useMemo( () => ({ address, @@ -865,6 +1065,8 @@ const CardanoProvider = (props: Props) => { buildNewInfoGovernanceAction, buildSignSubmitConwayCertTx, buildTreasuryGovernanceAction, + buildProtocolParameterChangeGovernanceAction, + buildHardForkGovernanceAction, buildVote, buildVoteDelegationCert, disconnectWallet, @@ -892,6 +1094,8 @@ const CardanoProvider = (props: Props) => { buildNewInfoGovernanceAction, buildSignSubmitConwayCertTx, buildTreasuryGovernanceAction, + buildProtocolParameterChangeGovernanceAction, + buildHardForkGovernanceAction, buildVote, buildVoteDelegationCert, disconnectWallet, diff --git a/govtool/frontend/src/utils/index.ts b/govtool/frontend/src/utils/index.ts index b6ccbd530..a2bb6b903 100644 --- a/govtool/frontend/src/utils/index.ts +++ b/govtool/frontend/src/utils/index.ts @@ -26,5 +26,6 @@ export * from "./numberValidation"; export * from "./openInNewTab"; export * from "./removeDuplicatedProposals"; export * from "./replaceNullValues"; +export * from "./setProtocolParameterUpdate"; export * from "./testIdFromLabel"; export * from "./wait"; diff --git a/govtool/frontend/src/utils/setProtocolParameterUpdate.ts b/govtool/frontend/src/utils/setProtocolParameterUpdate.ts new file mode 100644 index 000000000..dcf83bf8c --- /dev/null +++ b/govtool/frontend/src/utils/setProtocolParameterUpdate.ts @@ -0,0 +1,28 @@ +/** + * Sets the value of a protocol parameter update using the provided key and value. + * If the value is not undefined, it calls the corresponding setter function + * on the protocolParameterUpdate object. + * @param protocolParameterUpdate - The protocol parameter update object. + * @param key - The key of the parameter to update. + * @param value - The new value for the parameter. + */ +export function setProtocolParameterUpdate( + protocolParameterUpdate: P | { [key: string]: (value: V) => void }, + key: string, + value: V, +) { + if (value !== undefined) { + const snakeCaseKey = key.replace(/([A-Z])/g, "_$1").toLowerCase(); + const setterName = `set_${snakeCaseKey}`; + + if ( + (protocolParameterUpdate as { [key: string]: (value: V) => void })[ + setterName + ] !== undefined + ) { + (protocolParameterUpdate as { [key: string]: (value: V) => void })[ + setterName + ](value); + } + } +} diff --git a/govtool/frontend/src/utils/tests/setProtocolParameterUpdate.test.ts b/govtool/frontend/src/utils/tests/setProtocolParameterUpdate.test.ts new file mode 100644 index 000000000..5247dceb1 --- /dev/null +++ b/govtool/frontend/src/utils/tests/setProtocolParameterUpdate.test.ts @@ -0,0 +1,52 @@ +// Typescript checking is not crucial for this unit tests, so its easier to disable it +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { vi } from "vitest"; +import { setProtocolParameterUpdate } from "../setProtocolParameterUpdate"; + +describe("setProtocolParameterUpdate", () => { + it("should call the corresponding setter function if the value is not undefined", () => { + const protocolParameterUpdate: any = { + set_key: vi.fn() as (value: unknown) => void, + }; + + setProtocolParameterUpdate(protocolParameterUpdate, "key", "value"); + + expect(protocolParameterUpdate.set_key).toHaveBeenCalledWith("value"); + }); + + it("should not call any setter function if the value is undefined", () => { + const protocolParameterUpdate: any = { + set_key: vi.fn() as (value: unknown) => void, + }; + + setProtocolParameterUpdate(protocolParameterUpdate, "key", undefined); + + expect(protocolParameterUpdate.set_key).not.toHaveBeenCalled(); + }); + + it("should handle snake case keys correctly", () => { + const protocolParameterUpdate: any = { + set_snake_case_key: vi.fn() as (value: unknown) => void, + }; + + setProtocolParameterUpdate( + protocolParameterUpdate, + "snakeCaseKey", + "value", + ); + + expect(protocolParameterUpdate.set_snake_case_key).toHaveBeenCalledWith( + "value", + ); + }); + + it("should not call any setter function if the corresponding setter does not exist", () => { + const protocolParameterUpdate: any = {}; + + setProtocolParameterUpdate(protocolParameterUpdate, "key", "value"); + + expect(protocolParameterUpdate.set_key).toBeUndefined(); + }); +});