diff --git a/CHANGELOG.md b/CHANGELOG.md index b02bc7455..2b666031f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,17 +14,20 @@ changes. - Add metadata url and hash to drep details [Issue 2911](https://github.com/IntersectMBO/govtool/issues/2911) - Add CC votes percentages, not voted and Ratification threshold +- Add support for submitting all 7 governance action types [Issue 2258](https://github.com/IntersectMBO/govtool/issues/2258) ### Fixed - Fix calculating votes counting for governance actions - Fix crashing backend on unhandled missing proposal from vote [Issue 2920](https://github.com/IntersectMBO/govtool/issues/2920) - Remove abstain votes (not auto abstain) from total DRep stake +- Fix counting committee members [Issue 2948](https://github.com/IntersectMBO/govtool/issues/2948) ### Changed - Change threshold visual representation in governance action votes - Resize governance action details columns +- Update @intersect.mbo/pdf-ui to v0.6.0 ### Removed diff --git a/docs/GOVERNANCE_ACTION_SUBMISSION.md b/docs/GOVERNANCE_ACTION_SUBMISSION.md index 140b6b0e0..1c6a29d99 100644 --- a/docs/GOVERNANCE_ACTION_SUBMISSION.md +++ b/docs/GOVERNANCE_ACTION_SUBMISSION.md @@ -33,16 +33,16 @@ interface GovernanceAction { references: [{ label: string; uri: string }]; } -interface InfoProps { - hash: string; +type VotingAnchor = { url: string; + hash: string; } -interface TreasuryProps { - amount: string; - hash: string; +type InfoProps = VotingAnchor; + +type TreasuryProps { withdrawals: { receivingAddress: string; amount: string }[]; -} +} & VotingAnchor; type ProtocolParamsUpdate = { adaPerUtxo: string; @@ -77,14 +77,44 @@ type ProtocolParamsUpdate = { treasuryGrowthRate: UnitInterval; }; -interface ProtocolParameterChangeProps { +type ProtocolParameterChangeProps { prevGovernanceActionHash: string; prevGovernanceActionIndex: number; - url: string; - hash: string; - protocolParamsUpdate: Partial; -} +} & VotingAnchor; + +type HardForkInitiationProps = { + prevGovernanceActionHash: string; + prevGovernanceActionIndex: number; + major: number; + minor: number; +} & VotingAnchor; + +type NewConstitutionProps = { + prevGovernanceActionHash: string; + prevGovernanceActionIndex: number; + constitutionUrl: string; + constitutionHash: string; + scriptHash: string; +} & VotingAnchor; + +type UpdateCommitteeProps = { + prevGovernanceActionHash?: string; + prevGovernanceActionIndex?: number; + quorumThreshold: QuorumThreshold; + newCommittee?: CommitteeToAdd[]; + removeCommittee?: string[]; +} & VotingAnchor; + +type CommitteeToAdd = { + expiryEpoch: number; + committee: string; +}; + +type QuorumThreshold = { + numerator: number; + denominator: number; +}; const createGovernanceActionJsonLD: ( governanceAction: GovernanceAction @@ -100,6 +130,22 @@ const buildTreasuryGovernanceAction: ( treasuryProps: TreasuryProps ) => Promise; +const buildProtocolParameterChangeGovernanceAction: ( + protocolParameterChangeProps: ProtocolParameterChangeProps +) => Promise; + +const buildHardForkInitiationGovernanceAction: ( + hardForkInitiationProps: HardForkInitiationProps +) => Promise; + +const buildNewConstitutionGovernanceAction: ( + newConstitutionProps: NewConstitutionProps +) => Promise; + +const buildUpdateCommitteeGovernanceAction: ( + updateCommitteeProps: UpdateCommitteeProps +) => Promise; + const buildSignSubmitConwayCertTx: (params: { govActionBuilder: VotingProposalBuilder; type: "createGovAction"; @@ -165,32 +211,28 @@ const { buildNewInfoGovernanceAction, buildProtocolParameterChangeGovernanceAction, buildHardForkInitiationGovernanceAction, + buildTreasuryGovernanceAction, + buildNewConstitutionGovernanceAction, + buildUpdateCommitteeGovernanceAction, + buildNoConfidenceGovernanceAction, } = useCardano(); // Info Governance Action -const govActionBuilder = await buildNewInfoGovernanceAction({ hash, url }); +let govActionBuilder = await buildNewInfoGovernanceAction({ hash, url }); -// sign and submit the transaction -await buildSignSubmitConwayCertTx({ - govActionBuilder, - type: "createGovAction", -}); +// And for the other type of governance actions: -// Treasury Governance Action -const { buildTreasuryGovernanceAction } = useCardano(); +govActionBuilder = await buildNoConfidenceGovernanceAction({ hash, url }); // hash of the generated Governance Action metadata, url of the metadata, amount of the transaction, receiving address is the stake key address -const govActionBuilder = await buildTreasuryGovernanceAction({ +govActionBuilder = await buildTreasuryGovernanceAction({ hash, url, withdrawals: [{ amount, 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({ +govActionBuilder = await buildProtocolParameterChangeGovernanceAction({ prevGovernanceActionHash, prevGovernanceActionIndex, url, @@ -198,11 +240,8 @@ const govActionBuilder = await buildProtocolParameterChangeGovernanceAction({ 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({ +govActionBuilder = await buildHardForkInitiationGovernanceAction({ prevGovernanceActionHash, prevGovernanceActionIndex, url, @@ -211,6 +250,24 @@ const govActionBuilder = await buildHardForkInitiationGovernanceAction({ minor, }); +// hash of the previous Governance Action, index of the previous Governance Action, url of the metadata, hash of the metadata, and the constitution script hash +govActionBuilder = await buildNewConstitutionGovernanceAction({ + prevGovernanceActionHash, + prevGovernanceActionIndex, + constitutionUrl, + constitutionHash, + scriptHash, +}); + +// hash of the previous Governance Action, index of the previous Governance Action, url of the metadata, hash of the metadata, and the quorum threshold and the new committee members +govActionBuilder = await buildUpdateCommitteeGovernanceAction({ + prevGovernanceActionHash, + prevGovernanceActionIndex, + quorumThreshold, + newCommittee, + removeCommittee, +}); + // sign and submit the transaction await buildSignSubmitConwayCertTx({ govActionBuilder, diff --git a/govtool/backend/sql/get-network-metrics.sql b/govtool/backend/sql/get-network-metrics.sql index 06c3c013c..21efd2a91 100644 --- a/govtool/backend/sql/get-network-metrics.sql +++ b/govtool/backend/sql/get-network-metrics.sql @@ -21,6 +21,26 @@ DRepDistr AS ( CurrentEpoch AS ( SELECT MAX(no) AS no FROM epoch ), +CommitteeMembers AS ( + SELECT DISTINCT ON (cm.committee_hash_id) + cr.id, + block.time, + encode(cold_key_hash.raw, 'hex') cold_key, + encode(hot_key_hash.raw, 'hex') hot_key + FROM committee_registration cr + JOIN tx ON tx.id = cr.tx_id + JOIN block ON block.id = tx.block_id + JOIN committee_hash cold_key_hash ON cr.cold_key_id = cold_key_hash.id + JOIN committee_hash hot_key_hash ON cr.hot_key_id = hot_key_hash.id + JOIN committee_member cm ON cm.committee_hash_id = cold_key_hash.id OR cm.committee_hash_id = hot_key_hash.id + LEFT JOIN committee_de_registration cdr ON cdr.cold_key_id = cold_key_hash.id + CROSS JOIN CurrentEpoch + WHERE + cdr.id IS NULL AND cm.expiration_epoch > CurrentEpoch.no +), +NoOfCommitteeMembers AS ( + SELECT COUNT(*) total FROM CommitteeMembers +), ActiveDRepBoundaryEpoch AS ( SELECT epoch_no - drep_activity AS epoch_no FROM DRepActivity ), @@ -187,9 +207,6 @@ AlwaysNoConfidenceVotingPower AS ( TotalDRepDistr AS ( SELECT SUM(COALESCE(amount, 0))::bigint total_drep_distr FROM drep_distr where epoch_no = (SELECT no from CurrentEpoch) ), -CommitteeMembersCount AS ( - SELECT COUNT(*) AS no_of_committee_members FROM committee_member -), LatestGovAction AS ( SELECT gap.id, gap.enacted_epoch FROM gov_action_proposal gap @@ -223,7 +240,7 @@ SELECT AlwaysAbstainVotingPower.amount AS always_abstain_voting_power, AlwaysNoConfidenceVotingPower.amount AS always_no_confidence_voting_power, meta.network_name, - CommitteeMembersCount.no_of_committee_members, + NoOfCommitteeMembers.total no_of_committee_members, CommitteeThreshold.quorum_numerator, CommitteeThreshold.quorum_denominator FROM CurrentEpoch @@ -242,6 +259,6 @@ CROSS JOIN TotalActiveCIP119CompliantDReps CROSS JOIN TotalRegisteredDirectVoters CROSS JOIN AlwaysAbstainVotingPower CROSS JOIN AlwaysNoConfidenceVotingPower -CROSS JOIN CommitteeMembersCount +CROSS JOIN NoOfCommitteeMembers CROSS JOIN CommitteeThreshold CROSS JOIN meta; diff --git a/govtool/frontend/package-lock.json b/govtool/frontend/package-lock.json index b2684379f..14051e68f 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": "1.0.0", "@intersect.mbo/intersectmbo.org-icons-set": "^1.0.8", - "@intersect.mbo/pdf-ui": "^0.5.11", + "@intersect.mbo/pdf-ui": "^0.6.0", "@mui/icons-material": "^5.14.3", "@mui/material": "^5.14.4", "@rollup/plugin-babel": "^6.0.4", @@ -3397,13 +3397,13 @@ "license": "ISC" }, "node_modules/@intersect.mbo/pdf-ui": { - "version": "0.5.11", - "resolved": "https://registry.npmjs.org/@intersect.mbo/pdf-ui/-/pdf-ui-0.5.11.tgz", - "integrity": "sha512-LchrkbU2cbNP3zajkVoFy/+ZcqBDAt9AnF9weYGQPU0qD2ryj93JNeKzUctsYsWfGLy5d7TslNdnpx0zGVcc9A==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@intersect.mbo/pdf-ui/-/pdf-ui-0.6.0.tgz", + "integrity": "sha512-4lNDqUp03UQEy5Fwu2kTewM8Jxfc5uNxk7h40wOWx4zNIIOos187kF1NsE8yW2/VXRSkklsxSbc1ckifEJJakw==", "dependencies": { "@emurgo/cardano-serialization-lib-asmjs": "^12.0.0-beta.2", "@fontsource/poppins": "^5.0.14", - "@intersect.mbo/intersectmbo.org-icons-set": "^1.0.8", + "@intersect.mbo/intersectmbo.org-icons-set": "^1.1.0", "axios": "^1.7.2", "date-fns": "^3.6.0", "react-markdown": "^8.0.6", diff --git a/govtool/frontend/package.json b/govtool/frontend/package.json index 90dd255c8..f7b5d9175 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": "1.0.0", "@intersect.mbo/intersectmbo.org-icons-set": "^1.0.8", - "@intersect.mbo/pdf-ui": "^0.5.11", + "@intersect.mbo/pdf-ui": "^0.6.0", "@mui/icons-material": "^5.14.3", "@mui/material": "^5.14.4", "@rollup/plugin-babel": "^6.0.4", diff --git a/govtool/frontend/src/components/organisms/CreateGovernanceActionSteps/CreateGovernanceActionForm.tsx b/govtool/frontend/src/components/organisms/CreateGovernanceActionSteps/CreateGovernanceActionForm.tsx index d1cc18b0c..5f3e9b010 100644 --- a/govtool/frontend/src/components/organisms/CreateGovernanceActionSteps/CreateGovernanceActionForm.tsx +++ b/govtool/frontend/src/components/organisms/CreateGovernanceActionSteps/CreateGovernanceActionForm.tsx @@ -47,6 +47,9 @@ export const CreateGovernanceActionForm = ({ type! as | GovernanceActionType.InfoAction | GovernanceActionType.TreasuryWithdrawals + | GovernanceActionType.NewCommittee + | GovernanceActionType.NewConstitution + | GovernanceActionType.NoConfidence ], ).some( (field) => !watch(field as unknown as Parameters[0]), @@ -67,6 +70,9 @@ export const CreateGovernanceActionForm = ({ type! as | GovernanceActionType.InfoAction | GovernanceActionType.TreasuryWithdrawals + | GovernanceActionType.NewCommittee + | GovernanceActionType.NewConstitution + | GovernanceActionType.NoConfidence ], ).map(([key, field]) => { const fieldProps = { diff --git a/govtool/frontend/src/consts/governanceAction/fields.ts b/govtool/frontend/src/consts/governanceAction/fields.ts index 259a72396..0367b4799 100644 --- a/govtool/frontend/src/consts/governanceAction/fields.ts +++ b/govtool/frontend/src/consts/governanceAction/fields.ts @@ -97,6 +97,92 @@ export const sharedGovernanceActionFields: SharedGovernanceActionFieldSchema = { export const GOVERNANCE_ACTION_FIELDS: GovernanceActionFields = { [GovernanceActionType.InfoAction]: sharedGovernanceActionFields, + [GovernanceActionType.NoConfidence]: sharedGovernanceActionFields, + [GovernanceActionType.NewCommittee]: { + ...sharedGovernanceActionFields, + numerator: { + component: GovernanceActionField.Input, + labelI18nKey: + "createGovernanceAction.fields.declarations.numerator.label", + placeholderI18nKey: + "createGovernanceAction.fields.declarations.numerator.placeholder", + rules: { + required: { + value: true, + message: I18n.t("createGovernanceAction.fields.validations.required"), + }, + validate: numberValidation, + }, + }, + denominator: { + component: GovernanceActionField.Input, + labelI18nKey: + "createGovernanceAction.fields.declarations.denominator.label", + placeholderI18nKey: + "createGovernanceAction.fields.declarations.denominator.placeholder", + rules: { + required: { + value: true, + message: I18n.t("createGovernanceAction.fields.validations.required"), + }, + validate: numberValidation, + }, + }, + newCommitteeHash: { + component: GovernanceActionField.Input, + labelI18nKey: "createGovernanceAction.fields.declarations.members.label", + placeholderI18nKey: + "createGovernanceAction.fields.declarations.members.placeholder", + tipI18nKey: "createGovernanceAction.fields.declarations.members.tip", + rules: { + required: { + value: true, + message: I18n.t("createGovernanceAction.fields.validations.required"), + }, + maxLength: { + value: 500, + message: I18n.t( + "createGovernanceAction.fields.validations.maxLength", + { + maxLength: 500, + }, + ), + }, + }, + }, + newCommitteeExpiryEpoch: { + component: GovernanceActionField.Input, + labelI18nKey: + "createGovernanceAction.fields.declarations.expiryEpoch.label", + placeholderI18nKey: + "createGovernanceAction.fields.declarations.expiryEpoch.placeholder", + rules: { + required: { + value: true, + message: I18n.t("createGovernanceAction.fields.validations.required"), + }, + validate: numberValidation, + }, + }, + removeCommitteeHash: { + component: GovernanceActionField.Input, + labelI18nKey: "createGovernanceAction.fields.declarations.remove.label", + placeholderI18nKey: + "createGovernanceAction.fields.declarations.remove.placeholder", + tipI18nKey: "createGovernanceAction.fields.declarations.remove.tip", + rules: { + maxLength: { + value: 500, + message: I18n.t( + "createGovernanceAction.fields.validations.maxLength", + { + maxLength: 500, + }, + ), + }, + }, + }, + }, [GovernanceActionType.TreasuryWithdrawals]: { ...sharedGovernanceActionFields, receivingAddress: { @@ -123,6 +209,59 @@ export const GOVERNANCE_ACTION_FIELDS: GovernanceActionFields = { }, }, }, + [GovernanceActionType.NewConstitution]: { + ...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, + }, + }, + constitutionUrl: { + component: GovernanceActionField.Input, + labelI18nKey: + "createGovernanceAction.fields.declarations.constitutionUrl.label", + placeholderI18nKey: + "createGovernanceAction.fields.declarations.constitutionUrl.placeholder", + rules: { + required: { + value: true, + message: I18n.t("createGovernanceAction.fields.validations.required"), + }, + }, + }, + constitutionHash: { + component: GovernanceActionField.Input, + labelI18nKey: + "createGovernanceAction.fields.declarations.constitutionHash.label", + placeholderI18nKey: + "createGovernanceAction.fields.declarations.constitutionHash.placeholder", + rules: { + required: { + value: true, + message: I18n.t("createGovernanceAction.fields.validations.required"), + }, + }, + }, + scriptHash: { + component: GovernanceActionField.Input, + labelI18nKey: + "createGovernanceAction.fields.declarations.scriptHash.label", + placeholderI18nKey: + "createGovernanceAction.fields.declarations.scriptHash.placeholder", + }, + }, } as const; export const GOVERNANCE_ACTION_CONTEXT = { diff --git a/govtool/frontend/src/context/wallet.tsx b/govtool/frontend/src/context/wallet.tsx index a398730d2..24e397613 100644 --- a/govtool/frontend/src/context/wallet.tsx +++ b/govtool/frontend/src/context/wallet.tsx @@ -64,6 +64,12 @@ import { CostModel, Language, TxInputsBuilder, + NoConfidenceAction, + Constitution, + NewConstitutionAction, + UpdateCommitteeAction, + Committee, + Credentials, } from "@emurgo/cardano-serialization-lib-asmjs"; import { Buffer } from "buffer"; import { useNavigate } from "react-router-dom"; @@ -109,15 +115,56 @@ interface EnableResponse { error?: string; } -type InfoProps = { - hash: string; +type VotingAnchor = { url: string; + hash: string; }; +type InfoProps = VotingAnchor; + +type NoConfidenceProps = VotingAnchor; + type TreasuryProps = { - hash: string; - url: string; withdrawals: { receivingAddress: string; amount: string }[]; +} & VotingAnchor; + +type ProtocolParameterChangeProps = { + prevGovernanceActionHash: string; + prevGovernanceActionIndex: number; + protocolParamsUpdate: Partial; +} & VotingAnchor; + +type HardForkInitiationProps = { + prevGovernanceActionHash: string; + prevGovernanceActionIndex: string; + major: string; + minor: string; +} & VotingAnchor; + +type NewConstitutionProps = { + prevGovernanceActionHash?: string; + prevGovernanceActionIndex?: string; + constitutionUrl: string; + constitutionHash: string; + scriptHash?: string; +} & VotingAnchor; + +type UpdateCommitteeProps = { + prevGovernanceActionHash?: string; + prevGovernanceActionIndex?: string; + quorumThreshold: QuorumThreshold; + newCommittee?: CommitteeToAdd[]; + removeCommittee?: string[]; +} & VotingAnchor; + +export type CommitteeToAdd = { + expiryEpoch: string; + committee: string; +}; + +export type QuorumThreshold = { + numerator: string; + denominator: string; }; type ProtocolParamsUpdate = { @@ -153,23 +200,6 @@ type ProtocolParamsUpdate = { 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; @@ -231,6 +261,15 @@ interface CardanoContextType { buildHardForkGovernanceAction: ( hardForkInitiationProps: HardForkInitiationProps, ) => Promise; + buildNewConstitutionGovernanceAction: ( + newConstitutionProps: NewConstitutionProps, + ) => Promise; + buildUpdateCommitteeGovernanceAction: ( + updateCommitteeProps: UpdateCommitteeProps, + ) => Promise; + buildNoConfidenceGovernanceAction: ( + noConfidenceProps: NoConfidenceProps, + ) => Promise; } type Utxos = { @@ -734,13 +773,24 @@ const CardanoProvider = (props: Props) => { ], ); + const buildCredentialFromBech32Key = useCallback(async (key: string) => { + try { + const keyHash = Ed25519KeyHash.from_hex(key); + return Credential.from_keyhash(keyHash); + } catch (e) { + console.error(e); + throw e; + } + }, []); + const buildStakeKeyRegCert = useCallback(async (): Promise => { try { if (!stakeKey) { throw new Error(t("errors.noStakeKeySelected")); } - const stakeKeyHash = Ed25519KeyHash.from_hex(stakeKey.substring(2)); - const stakeCred = Credential.from_keyhash(stakeKeyHash); + const stakeCred = await buildCredentialFromBech32Key( + stakeKey.substring(2), + ); const stakeKeyRegCert = StakeRegistration.new_with_explicit_deposit( stakeCred, BigNum.from_str(`${epochParams.key_deposit}`), @@ -760,8 +810,9 @@ const CardanoProvider = (props: Props) => { throw new Error(t("errors.noStakeKeySelected")); } // Remove network tag from stake key hash - const stakeKeyHash = Ed25519KeyHash.from_hex(stakeKey.substring(2)); - const stakeCred = Credential.from_keyhash(stakeKeyHash); + const stakeCred = await buildCredentialFromBech32Key( + stakeKey.substring(2), + ); // Create correct DRep let targetDRep; @@ -796,8 +847,7 @@ const CardanoProvider = (props: Props) => { ): Promise => { try { // Get wallet's DRep key - const dRepKeyHash = Ed25519KeyHash.from_hex(dRepID); - const dRepCred = Credential.from_keyhash(dRepKeyHash); + const dRepCred = await buildCredentialFromBech32Key(dRepID); let dRepRegCert; // If there is an anchor @@ -832,8 +882,7 @@ const CardanoProvider = (props: Props) => { ): Promise => { try { // Get wallet's DRep key - const dRepKeyHash = Ed25519KeyHash.from_hex(dRepID); - const dRepCred = Credential.from_keyhash(dRepKeyHash); + const dRepCred = await buildCredentialFromBech32Key(dRepID); let dRepUpdateCert; // If there is an anchor @@ -857,8 +906,7 @@ const CardanoProvider = (props: Props) => { async (voterDeposit: string): Promise => { try { // Get wallet's DRep key - const dRepKeyHash = Ed25519KeyHash.from_hex(dRepID); - const dRepCred = Credential.from_keyhash(dRepKeyHash); + const dRepCred = await buildCredentialFromBech32Key(dRepID); const dRepRetirementCert = DRepDeregistration.new( dRepCred, @@ -883,12 +931,10 @@ const CardanoProvider = (props: Props) => { cip95MetadataHash?: string, ): Promise => { try { - // Get wallet's DRep key - const dRepKeyHash = Ed25519KeyHash.from_hex(dRepID); - // Vote things - const voter = Voter.new_drep_credential( - Credential.from_keyhash(dRepKeyHash), - ); + // Get wallet's DRep credential + const dRepCredential = await buildCredentialFromBech32Key(dRepID); + // Vote credential + const voter = Voter.new_drep_credential(dRepCredential); const govActionId = GovernanceActionId.new( // placeholder TransactionHash.from_hex(txHash), @@ -977,6 +1023,149 @@ const CardanoProvider = (props: Props) => { [], ); + // new constitution action + const buildNewConstitutionGovernanceAction = useCallback( + async ({ + prevGovernanceActionHash, + prevGovernanceActionIndex, + constitutionUrl, + constitutionHash, + url, + hash, + scriptHash, + }: NewConstitutionProps) => { + const govActionBuilder = VotingProposalBuilder.new(); + try { + const constitutionAnchor = generateAnchor( + constitutionUrl, + constitutionHash, + ); + const anchor = generateAnchor(url, hash); + + const rewardAddr = await getRewardAddress(); + if (!rewardAddr) throw new Error("Can not get reward address"); + + let constitution; + if (scriptHash) { + constitution = Constitution.new_with_script_hash( + constitutionAnchor, + ScriptHash.from_hex(scriptHash), + ); + } else { + constitution = Constitution.new(constitutionAnchor); + } + + let newConstitution; + if (prevGovernanceActionHash && prevGovernanceActionIndex) { + const prevGovernanceActionId = GovernanceActionId.new( + TransactionHash.from_hex(prevGovernanceActionHash), + Number(prevGovernanceActionIndex), + ); + newConstitution = NewConstitutionAction.new_with_action_id( + prevGovernanceActionId, + constitution, + ); + } else { + newConstitution = NewConstitutionAction.new(constitution); + } + + const newConstitutionAction = + GovernanceAction.new_new_constitution_action(newConstitution); + + // Create voting proposal + const votingProposal = VotingProposal.new( + newConstitutionAction, + anchor, + rewardAddr, + BigNum.from_str(epochParams?.gov_action_deposit.toString()), + ); + + govActionBuilder.add(votingProposal); + + return govActionBuilder; + } catch (e) { + console.error(e); + } + }, + [], + ); + + // update committee action + const buildUpdateCommitteeGovernanceAction = useCallback( + async ({ + prevGovernanceActionHash, + prevGovernanceActionIndex, + url, + hash, + newCommittee, + removeCommittee, + quorumThreshold, + }: UpdateCommitteeProps) => { + const govActionBuilder = VotingProposalBuilder.new(); + try { + const anchor = generateAnchor(url, hash); + const rewardAddr = await getRewardAddress(); + if (!rewardAddr) throw new Error("Can not get reward address"); + + const threshold = UnitInterval.new( + BigNum.from_str(quorumThreshold.numerator.toString()), + BigNum.from_str(quorumThreshold.denominator.toString()), + ); + const committeeToAdd = Committee.new(threshold); + if (newCommittee) { + newCommittee.forEach(async (member) => { + const credential = await buildCredentialFromBech32Key( + member.committee, + ); + committeeToAdd.add_member(credential, Number(member.expiryEpoch)); + }); + } + const committeeToRemoveCredentials = Credentials.new(); + if (removeCommittee) { + removeCommittee.forEach(async (member) => { + const credential = await buildCredentialFromBech32Key(member); + committeeToRemoveCredentials.add(credential); + }); + } + + let updateCommitteeAction; + if (prevGovernanceActionHash && prevGovernanceActionIndex) { + const prevGovernanceActionId = GovernanceActionId.new( + TransactionHash.from_hex(prevGovernanceActionHash), + Number(prevGovernanceActionIndex), + ); + updateCommitteeAction = UpdateCommitteeAction.new_with_action_id( + prevGovernanceActionId, + committeeToAdd, + committeeToRemoveCredentials, + ); + } else { + updateCommitteeAction = UpdateCommitteeAction.new( + committeeToAdd, + committeeToRemoveCredentials, + ); + } + + const updateCommitteeGovernanceAction = + GovernanceAction.new_new_committee_action(updateCommitteeAction); + + const votingProposal = VotingProposal.new( + updateCommitteeGovernanceAction, + anchor, + rewardAddr, + BigNum.from_str(epochParams?.gov_action_deposit.toString()), + ); + + govActionBuilder.add(votingProposal); + + return govActionBuilder; + } catch (e) { + console.error(e); + } + }, + [], + ); + // info action const buildNewInfoGovernanceAction = useCallback( async ({ hash, url }: InfoProps) => { @@ -1008,6 +1197,38 @@ const CardanoProvider = (props: Props) => { [epochParams, getRewardAddress], ); + // no confidence action + const buildNoConfidenceGovernanceAction = useCallback( + async ({ hash, url }: NoConfidenceProps) => { + const govActionBuilder = VotingProposalBuilder.new(); + try { + // Create new no confidence action + const noConfidenceAction = NoConfidenceAction.new(); + const noConfidenceGovAct = + GovernanceAction.new_no_confidence_action(noConfidenceAction); + // 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( + noConfidenceGovAct, + anchor, + rewardAddr, + BigNum.from_str(epochParams?.gov_action_deposit.toString()), + ); + govActionBuilder.add(votingProposal); + + return govActionBuilder; + } catch (err) { + console.error(err); + } + }, + [], + ); + // treasury action const buildTreasuryGovernanceAction = useCallback( async ({ hash, url, withdrawals }: TreasuryProps) => { @@ -1170,13 +1391,16 @@ const CardanoProvider = (props: Props) => { }: HardForkInitiationProps) => { const govActionBuilder = VotingProposalBuilder.new(); try { - const newProtocolVersion = ProtocolVersion.new(major, minor); + const newProtocolVersion = ProtocolVersion.new( + Number(major), + Number(minor), + ); let hardForkInitiationAction; if (prevGovernanceActionHash && prevGovernanceActionIndex) { const prevGovernanceActionId = GovernanceActionId.new( TransactionHash.from_hex(prevGovernanceActionHash), - prevGovernanceActionIndex, + Number(prevGovernanceActionIndex), ); hardForkInitiationAction = HardForkInitiationAction.new_with_action_id( @@ -1226,6 +1450,9 @@ const CardanoProvider = (props: Props) => { buildHardForkGovernanceAction, buildNewInfoGovernanceAction, buildProtocolParameterChangeGovernanceAction, + buildNoConfidenceGovernanceAction, + buildNewConstitutionGovernanceAction, + buildUpdateCommitteeGovernanceAction, buildSignSubmitConwayCertTx, buildStakeKeyRegCert, buildTreasuryGovernanceAction, @@ -1256,6 +1483,9 @@ const CardanoProvider = (props: Props) => { buildHardForkGovernanceAction, buildNewInfoGovernanceAction, buildProtocolParameterChangeGovernanceAction, + buildNoConfidenceGovernanceAction, + buildNewConstitutionGovernanceAction, + buildUpdateCommitteeGovernanceAction, buildSignSubmitConwayCertTx, buildStakeKeyRegCert, buildTreasuryGovernanceAction, diff --git a/govtool/frontend/src/hooks/forms/useCreateGovernanceActionForm.ts b/govtool/frontend/src/hooks/forms/useCreateGovernanceActionForm.ts index 0760b27cd..5e2a45454 100644 --- a/govtool/frontend/src/hooks/forms/useCreateGovernanceActionForm.ts +++ b/govtool/frontend/src/hooks/forms/useCreateGovernanceActionForm.ts @@ -10,7 +10,7 @@ import { PATHS, storageInformationErrorModals, } from "@consts"; -import { useCardano, useModal, useAppContext } from "@context"; +import { useCardano, useModal, useAppContext, QuorumThreshold } from "@context"; import { correctAdaFormat, downloadJson, @@ -56,6 +56,9 @@ export const useCreateGovernanceActionForm = ( const { buildNewInfoGovernanceAction, buildTreasuryGovernanceAction, + buildNoConfidenceGovernanceAction, + buildNewConstitutionGovernanceAction, + buildUpdateCommitteeGovernanceAction, buildSignSubmitConwayCertTx, } = useCardano(); @@ -131,6 +134,64 @@ export const useCreateGovernanceActionForm = ( switch (govActionType) { case GovernanceActionType.InfoAction: return buildNewInfoGovernanceAction(commonGovActionDetails); + case GovernanceActionType.NoConfidence: + return buildNoConfidenceGovernanceAction(commonGovActionDetails); + case GovernanceActionType.NewConstitution: { + if ( + data.constitutionUrl === undefined || + data.constitutionHash === undefined + ) { + throw new Error( + t("errors.invalidNewCommitteeGovernanceActionType"), + ); + } + + return buildNewConstitutionGovernanceAction({ + ...commonGovActionDetails, + constitutionUrl: data.constitutionUrl, + constitutionHash: data.constitutionHash, + scriptHash: data.scriptHash, + prevGovernanceActionHash: data.prevGovernanceActionHash, + prevGovernanceActionIndex: data.prevGovernanceActionIndex, + }); + } + case GovernanceActionType.NewCommittee: { + if ( + data.newCommitteeHash === undefined || + data.newCommitteeExpiryEpoch === undefined + ) { + throw new Error( + t("errors.invalidUpdateCommitteeGovernanceActionType"), + ); + } + + let quorumThreshold: QuorumThreshold = { + numerator: "1", + denominator: "2", + }; + if (data.numerator !== undefined && data.denominator !== undefined) { + quorumThreshold = { + numerator: data.numerator, + denominator: data.denominator, + }; + } + + return buildUpdateCommitteeGovernanceAction({ + ...commonGovActionDetails, + newCommittee: [ + { + committee: data.newCommitteeHash, + expiryEpoch: data.newCommitteeExpiryEpoch, + }, + ], + removeCommittee: data.removeCommitteeHash + ? [data.removeCommitteeHash] + : [], + quorumThreshold, + prevGovernanceActionHash: data.prevGovernanceActionHash, + prevGovernanceActionIndex: data.prevGovernanceActionIndex, + }); + } case GovernanceActionType.TreasuryWithdrawals: { if ( data.amount === undefined || diff --git a/govtool/frontend/src/i18n/locales/en.json b/govtool/frontend/src/i18n/locales/en.json index 40e878484..09b478923 100644 --- a/govtool/frontend/src/i18n/locales/en.json +++ b/govtool/frontend/src/i18n/locales/en.json @@ -154,6 +154,56 @@ "title": { "label": "Title", "placeholder": "A name for this Action" + }, + "denominator": { + "label": "Denominator", + "placeholder": "Denominator", + "tip": "Denominator of the threshold" + }, + "numerator": { + "label": "Numerator", + "placeholder": "Numerator", + "tip": "Numerator of the threshold" + }, + "members": { + "label": "New committee member", + "placeholder": "Members to be added", + "tip": "Bech32 address of the new member" + }, + "expiryEpoch": { + "label": "Expiry Epoch", + "placeholder": "Expiry Epoch of the new Committee member", + "tip": "Epoch when the new committee will expire" + }, + "remove": { + "label": "Remove committee member", + "placeholder": "Members to be removed", + "tip": "Bech32 address of the member to be removed" + }, + "prevGovernanceActionIndex": { + "label": "Previous Governance Action ID", + "placeholder": "Previous Governance Action ID", + "tip": "Previous Governance Action ID" + }, + "prevGovernanceActionHash": { + "label": "Previous Governance Action Hash", + "placeholder": "Previous Governance Action Hash", + "tip": "Previous Governance Action Hash" + }, + "constitutionUrl": { + "label": "New Constitution URL", + "placeholder": "New Constitution URL", + "tip": "URL of the new constitution" + }, + "constitutionHash": { + "label": "New Constitution Hash", + "placeholder": "New Constitution Hash", + "tip": "Hash of the new constitution" + }, + "scriptHash": { + "label": "New Constitution Script Hash", + "placeholder": "New Constitution Script Hash", + "tip": "Script hash of the new constitution" } }, "validations": { diff --git a/govtool/frontend/src/pages/GovernanceActionOutComes.tsx b/govtool/frontend/src/pages/GovernanceActionOutComes.tsx index 5801cea32..a5b66289e 100644 --- a/govtool/frontend/src/pages/GovernanceActionOutComes.tsx +++ b/govtool/frontend/src/pages/GovernanceActionOutComes.tsx @@ -40,6 +40,9 @@ export const GovernanceActionOutComesPillar = () => { } > + {/* TODO: Remove this comments when tsc issue is resolved */} + {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} + {/* @ts-expect-error */} diff --git a/govtool/frontend/src/types/governanceAction.ts b/govtool/frontend/src/types/governanceAction.ts index c150444dd..77bad990a 100644 --- a/govtool/frontend/src/types/governanceAction.ts +++ b/govtool/frontend/src/types/governanceAction.ts @@ -21,7 +21,7 @@ export type FieldSchema = { labelI18nKey: NestedKeys; placeholderI18nKey: NestedKeys; tipI18nKey?: NestedKeys; - rules: Omit; + rules?: Omit; }; // Following properties are based on [CIP-108](https://github.com/Ryun1/CIPs/blob/governance-metadata-actions/CIP-0108/README.md) @@ -34,18 +34,45 @@ export type SharedGovernanceActionFieldSchema = { rationale: FieldSchema; }; -export type InfoGovernanceActionFieldSchema = SharedGovernanceActionFieldSchema; -export type TreasuryGovernanceActionFieldSchema = - SharedGovernanceActionFieldSchema & - Partial<{ - receivingAddress: FieldSchema; - amount: FieldSchema; - }>; +export type TreasuryGovernanceActionFieldSchema = Partial<{ + receivingAddress: FieldSchema; + amount: FieldSchema; +}>; +export type NewCommitteeActionFieldSchema = Partial<{ + prevGovernanceActionHash: FieldSchema; + prevGovernanceActionIndex: FieldSchema; + numerator: FieldSchema; + denominator: FieldSchema; + newCommitteeHash: FieldSchema; + newCommitteeExpiryEpoch: FieldSchema; + removeCommitteeHash: FieldSchema; +}>; +export type HardForkInitiationActionFieldSchema = Partial<{ + prevGovernanceActionHash: FieldSchema; + prevGovernanceActionIndex: FieldSchema; + major: FieldSchema; + minor: FieldSchema; +}>; +export type NewConstitutionActionFieldSchema = Partial<{ + prevGovernanceActionHash: FieldSchema; + prevGovernanceActionIndex: FieldSchema; + constitutionUrl: FieldSchema; + constitutionHash: FieldSchema; + scriptHash: FieldSchema; +}>; export type GovernanceActionFieldSchemas = - | InfoGovernanceActionFieldSchema & TreasuryGovernanceActionFieldSchema; + | SharedGovernanceActionFieldSchema & + TreasuryGovernanceActionFieldSchema & + NewCommitteeActionFieldSchema & + HardForkInitiationActionFieldSchema & + NewConstitutionActionFieldSchema; export type GovernanceActionFields = Record< - GovernanceActionType.InfoAction | GovernanceActionType.TreasuryWithdrawals, + | GovernanceActionType.InfoAction + | GovernanceActionType.TreasuryWithdrawals + | GovernanceActionType.NoConfidence + | GovernanceActionType.NewCommittee + | GovernanceActionType.NewConstitution, GovernanceActionFieldSchemas >; diff --git a/govtool/frontend/yarn.lock b/govtool/frontend/yarn.lock index b5dd9c3ce..9dc9df1c6 100644 --- a/govtool/frontend/yarn.lock +++ b/govtool/frontend/yarn.lock @@ -1367,15 +1367,15 @@ resolved "https://registry.npmjs.org/@emurgo/cardano-serialization-lib-asmjs/-/cardano-serialization-lib-asmjs-12.1.1.tgz" integrity sha512-K3f28QUfLDJ7seO6MtKfMYtRm5ccf36TQ5yxyTmZqX1TA85MkriEdxqpgV9KLiLEA95emwnlvU2/WmlHMRPg1A== -"@esbuild/linux-x64@0.21.5": +"@esbuild/darwin-arm64@0.21.5": version "0.21.5" - resolved "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz" - integrity sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ== + resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz" + integrity sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ== -"@esbuild/linux-x64@0.24.2": +"@esbuild/darwin-arm64@0.24.2": version "0.24.2" - resolved "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz" - integrity sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q== + resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz" + integrity sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA== "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": version "4.4.1" @@ -1500,19 +1500,19 @@ buffer "^6.0.3" react-query "^3.39.3" -"@intersect.mbo/intersectmbo.org-icons-set@^1.0.8": +"@intersect.mbo/intersectmbo.org-icons-set@^1.0.8", "@intersect.mbo/intersectmbo.org-icons-set@^1.1.0": version "1.1.0" resolved "https://registry.npmjs.org/@intersect.mbo/intersectmbo.org-icons-set/-/intersectmbo.org-icons-set-1.1.0.tgz" integrity sha512-sjKEtnK9eLYH/8kCD0YRQCms3byFA/tnSsei9NHTZbBYX9sBpeX6ErfR0sKYjOSxQOxl4FumX9D0X+vHIqxo8g== -"@intersect.mbo/pdf-ui@^0.5.11": - version "0.5.11" - resolved "https://registry.npmjs.org/@intersect.mbo/pdf-ui/-/pdf-ui-0.5.11.tgz" - integrity sha512-LchrkbU2cbNP3zajkVoFy/+ZcqBDAt9AnF9weYGQPU0qD2ryj93JNeKzUctsYsWfGLy5d7TslNdnpx0zGVcc9A== +"@intersect.mbo/pdf-ui@^0.6.0": + version "0.6.0" + resolved "https://registry.npmjs.org/@intersect.mbo/pdf-ui/-/pdf-ui-0.6.0.tgz" + integrity sha512-4lNDqUp03UQEy5Fwu2kTewM8Jxfc5uNxk7h40wOWx4zNIIOos187kF1NsE8yW2/VXRSkklsxSbc1ckifEJJakw== dependencies: "@emurgo/cardano-serialization-lib-asmjs" "^12.0.0-beta.2" "@fontsource/poppins" "^5.0.14" - "@intersect.mbo/intersectmbo.org-icons-set" "^1.0.8" + "@intersect.mbo/intersectmbo.org-icons-set" "^1.1.0" axios "^1.7.2" date-fns "^3.6.0" react-markdown "^8.0.6" @@ -2166,15 +2166,10 @@ resolved "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz" integrity sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg== -"@parcel/watcher-linux-x64-glibc@2.5.0": - version "2.5.0" - resolved "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.0.tgz" - integrity sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw== - -"@parcel/watcher-linux-x64-musl@2.5.0": +"@parcel/watcher-darwin-arm64@2.5.0": version "2.5.0" - resolved "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.0.tgz" - integrity sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA== + resolved "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.0.tgz" + integrity sha512-hyZ3TANnzGfLpRA2s/4U1kbw2ZI4qGxaRJbBH2DCSREFfubMswheh8TeiC1sGZ3z2jUf3s37P0BBlrD3sjVTUw== "@parcel/watcher@^2.4.1": version "2.5.0" @@ -2287,15 +2282,10 @@ estree-walker "^2.0.2" picomatch "^4.0.2" -"@rollup/rollup-linux-x64-gnu@4.27.4": - version "4.27.4" - resolved "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.27.4.tgz" - integrity sha512-Ni8mMtfo+o/G7DVtweXXV/Ol2TFf63KYjTtoZ5f078AUgJTmaIJnj4JFU7TK/9SVWTaSJGxPi5zMDgK4w+Ez7Q== - -"@rollup/rollup-linux-x64-musl@4.27.4": +"@rollup/rollup-darwin-arm64@4.27.4": version "4.27.4" - resolved "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.27.4.tgz" - integrity sha512-5AeeAF1PB9TUzD+3cROzFTnAJAcVUGLuR8ng0E0WXGkYhp6RD6L+6szYVX+64Rs0r72019KHZS1ka1q+zU/wUw== + resolved "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.27.4.tgz" + integrity sha512-PlNiRQapift4LNS8DPUHuDX/IdXiLjf8mc5vdEmUR0fF/pyy2qWwzdLjB+iZquGr8LuN4LnUoSEvKRwjSVYz3Q== "@rtsao/scc@^1.1.0": version "1.1.0" @@ -2874,15 +2864,10 @@ "@svgr/plugin-svgo" "^5.5.0" loader-utils "^2.0.0" -"@swc/core-linux-x64-gnu@1.9.3": +"@swc/core-darwin-arm64@1.9.3": version "1.9.3" - resolved "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.9.3.tgz" - integrity sha512-ivXXBRDXDc9k4cdv10R21ccBmGebVOwKXT/UdH1PhxUn9m/h8erAWjz5pcELwjiMf27WokqPgaWVfaclDbgE+w== - -"@swc/core-linux-x64-musl@1.9.3": - version "1.9.3" - resolved "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.9.3.tgz" - integrity sha512-ILsGMgfnOz1HwdDz+ZgEuomIwkP1PHT6maigZxaCIuC6OPEhKE8uYna22uU63XvYcLQvZYDzpR3ms47WQPuNEg== + resolved "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.9.3.tgz" + integrity sha512-hGfl/KTic/QY4tB9DkTbNuxy5cV4IeejpPD4zo+Lzt4iLlDWIeANL4Fkg67FiVceNJboqg48CUX+APhDHO5G1w== "@swc/core@*", "@swc/core@^1.5.22", "@swc/core@^1.7.26": version "1.9.3" @@ -7188,6 +7173,16 @@ fs@^0.0.1-security: resolved "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz" integrity sha512-3XY9e1pP0CVEUCdj5BmfIZxRBTSDycnbqhIOGec9QYtmVH2fbLpj86CFWkrNOkt/Fvty4KZG5lTglL9j/gJ87w== +fsevents@^2.3.2, fsevents@~2.3.2, fsevents@~2.3.3: + version "2.3.3" + resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +fsevents@2.3.2: + version "2.3.2" + resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + function-bind@^1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" diff --git a/tests/govtool-frontend/playwright/tests/6-miscellaneous/miscellaneous.spec.ts b/tests/govtool-frontend/playwright/tests/6-miscellaneous/miscellaneous.spec.ts index 77316a04f..aaa88a863 100644 --- a/tests/govtool-frontend/playwright/tests/6-miscellaneous/miscellaneous.spec.ts +++ b/tests/govtool-frontend/playwright/tests/6-miscellaneous/miscellaneous.spec.ts @@ -13,7 +13,8 @@ import { faker } from "@faker-js/faker"; import { test } from "@fixtures/walletExtension"; import { setAllureEpic } from "@helpers/allure"; import { isMobile, openDrawer } from "@helpers/mobile"; -import { expect } from "@playwright/test"; +import { expect, Page } from "@playwright/test"; +import { randomUUID } from "crypto"; import environments from "lib/constants/environments"; test.beforeEach(async () => { @@ -172,17 +173,50 @@ test.describe("User Snap", () => { const attachmentInputSelector = "input[type=file]"; const feedbackApiUrl = "https://widget.usersnap.com/api/widget/xhrrpc?submit_feedback"; + const bucketUrl = + "https://s3.eu-central-1.amazonaws.com/upload.usersnap.com"; const mockAttachmentPath = "./lib/_mock/mockAttachment.png"; - test("6Q. Should report an issue", async ({ page }) => { - // Intercept Usersnap submit API + const interceptBucket = async (page: Page) => { + await page.route(bucketUrl, async (route) => + route.fulfill({ + status: 204, + }) + ); + }; + + const interceptUsersnap = async (page: Page) => { await page.route(feedbackApiUrl, async (route) => route.fulfill({ - status: 403, - body: JSON.stringify({ error: "Blocked by test" }), + status: 200, + body: JSON.stringify({ + status: true, + data: { + feedback: { + feedback_id: randomUUID(), + assignee_id: randomUUID(), + labels: [], + }, + screen_recording_url: null, + attachment_urls: [ + { + url: bucketUrl, + fields: { + "Content-Type": "image/png", + key: randomUUID(), + }, + }, + ], + }, + }), }) ); + }; + test("6Q. Should report an issue", async ({ page }) => { + // Intercept Usersnap submit API + await interceptUsersnap(page); + await interceptBucket(page); await page .getByRole("button", { name: "Report an issue Something", @@ -196,17 +230,13 @@ test.describe("User Snap", () => { await page.getByRole("button", { name: "Submit" }).click(); - await expect(page.getByText("Feedback was not submitted,")).toBeVisible(); + await expect(page.getByText("Thank you!")).toBeVisible(); }); test("6R. Should submit an idea or new feature", async ({ page }) => { // Intercept Usersnap submit API - await page.route(feedbackApiUrl, async (route) => - route.fulfill({ - status: 403, - body: JSON.stringify({ error: "Blocked by test" }), - }) - ); + await interceptUsersnap(page); + await interceptBucket(page); await page .getByRole("button", { @@ -227,7 +257,7 @@ test.describe("User Snap", () => { await page.getByRole("button", { name: "Submit" }).click(); - await expect(page.getByText("Feedback was not submitted,")).toBeVisible(); + await expect(page.getByText("Thank you!")).toBeVisible(); }); }); }); diff --git a/tests/test-infrastructure/docker-compose-govtool.yml b/tests/test-infrastructure/docker-compose-govtool.yml index 7cdbaec79..9a714524c 100644 --- a/tests/test-infrastructure/docker-compose-govtool.yml +++ b/tests/test-infrastructure/docker-compose-govtool.yml @@ -50,6 +50,7 @@ services: VITE_PDF_API_URL: ${PDF_API_URL} VITE_IPFS_GATEWAY: ${IPFS_GATEWAY} VITE_IPFS_PROJECT_ID: ${IPFS_PROJECT_ID} + VITE_IS_GOVERNANCE_OUTCOMES_PILLAR_ENABLED: "true" environment: VIRTUAL_HOST: https://${BASE_DOMAIN} networks: