diff --git a/CHANGELOG.md b/CHANGELOG.md index b02bc7455..c660571e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ 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 @@ -25,6 +26,7 @@ changes. - 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/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(); }); }); });