Skip to content

Commit

Permalink
[#378] feat: add hash and validation of the metadata
Browse files Browse the repository at this point in the history
  • Loading branch information
MSzalowski committed Mar 12, 2024
1 parent 59db1a1 commit ea8d6c3
Show file tree
Hide file tree
Showing 12 changed files with 372 additions and 26 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ changes.
- Create GA creation form [Issue 360](https://github.com/IntersectMBO/govtool/issues/360)
- Create TextArea [Issue 110](https://github.com/IntersectMBO/govtool/issues/110)
- Choose GA type - GA Submiter [Issue 358](https://github.com/IntersectMBO/govtool/issues/358)

- Add on-chain inputs validation [Issue 377](https://github.com/IntersectMBO/govtool/issues/377)
- Add hash and validation of the metadata [Issue 378](https://github.com/IntersectMBO/govtool/issues/378)

### Added

Expand Down Expand Up @@ -44,6 +44,7 @@ changes.
- Fixed CSP settings to allow error reports with Sentry [Issue 291](https://github.com/IntersectMBO/govtool/issues/291).

### Changed

- `drep/list` now return also `status` and `type` fields. Also it now returns the retired dreps, and you can search for given drep by name using optional query parameter. If the drep name is passed exactly, then you can even find a drep that's sole voter. [Issue 446](https://github.com/IntersectMBO/govtool/issues/446)
- `drep/list` and `drep/info` endpoints now return additional data such as metadata url and hash, and voting power [Issue 223](https://github.com/IntersectMBO/govtool/issues/223)
- `drep/info` now does not return sole voters (dreps without metadata) [Issue 317](https://github.com/IntersectMBO/govtool/issues/317)
Expand Down
2 changes: 2 additions & 0 deletions govtool/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"@mui/icons-material": "^5.14.3",
"@mui/material": "^5.14.4",
"@sentry/react": "^7.77.0",
"@types/jsonld": "^1.5.13",
"@types/react": "^18.2.12",
"@types/react-gtm-module": "^2.0.2",
"axios": "^1.4.0",
Expand All @@ -31,6 +32,7 @@
"date-fns": "^2.30.0",
"esbuild": "^0.19.8",
"i18next": "^23.7.19",
"jsonld": "^8.3.2",
"keen-slider": "^6.8.5",
"react": "^18.2.0",
"react-dom": "^18.2.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import { Button, InfoText, Spacer, Typography } from "@atoms";
import { GOVERNANCE_ACTION_FIELDS } from "@consts";
import { useCreateGovernanceActionForm, useTranslation } from "@hooks";
import { Field } from "@molecules";
import { URL_REGEX } from "@/utils";
import { GovernanceActionField } from "@/types/governanceAction";

import { BgCard } from "../BgCard";
import { ControlledField } from "../ControlledField";
import { GovernanceActionField } from "@/types/governanceAction";
import { URL_REGEX } from "@/utils";

const LINK_PLACEHOLDER = "https://website.com/";
const MAX_NUMBER_OF_LINKS = 8;
Expand Down Expand Up @@ -117,10 +117,6 @@ export const CreateGovernanceActionForm = ({
placeholder={LINK_PLACEHOLDER}
name={`links.${index}.link`}
rules={{
required: {
value: true,
message: t("createGovernanceAction.fields.validations.required"),
},
pattern: {
value: URL_REGEX,
message: t("createGovernanceAction.fields.validations.url"),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,85 @@
import { Dispatch, SetStateAction, useCallback, useState } from "react";
import { useNavigate } from "react-router-dom";
import { Box } from "@mui/material";
import OpenInNewIcon from "@mui/icons-material/OpenInNew";

import { Button, Spacer, Typography } from "@atoms";
import { ICONS } from "@consts";
import { ICONS, PATHS } from "@consts";
import { useCreateGovernanceActionForm, useTranslation } from "@hooks";
import { Step } from "@molecules";
import { BgCard, ControlledField } from "@organisms";
import { URL_REGEX, downloadJson, openInNewTab } from "@utils";
import { BgCard, ControlledField, StatusModalState } from "@organisms";
import {
URL_REGEX,
downloadJson,
openInNewTab,
validateMetadataHash,
MetadataHashValidationErrors,
} from "@utils";
import { ModalState, useModal } from "@/context";
import I18n from "@/i18n";

type StorageInformationProps = {
setStep: Dispatch<SetStateAction<number>>;
};

const externalDataDoesntMatchModal = {
type: "statusModal",
state: {
status: "warning",
title: I18n.t(
"createGovernanceAction.modals.externalDataDoesntMatch.title"
),
message: I18n.t(
"createGovernanceAction.modals.externalDataDoesntMatch.message"
),
buttonText: I18n.t(
"createGovernanceAction.modals.externalDataDoesntMatch.buttonText"
),
cancelText: I18n.t(
"createGovernanceAction.modals.externalDataDoesntMatch.cancelRegistrationText"
),
feedbackText: I18n.t(
"createGovernanceAction.modals.externalDataDoesntMatch.feedbackText"
),
},
} as const;

const urlCannotBeFound = {
type: "statusModal",
state: {
status: "warning",
title: I18n.t("createGovernanceAction.modals.urlCannotBeFound.title"),
message: I18n.t("createGovernanceAction.modals.urlCannotBeFound.message"),
link: "https://docs.sanchogov.tools",
linkText: I18n.t("createGovernanceAction.modals.urlCannotBeFound.linkText"),
buttonText: I18n.t(
"createGovernanceAction.modals.urlCannotBeFound.buttonText"
),
cancelText: I18n.t(
"createGovernanceAction.modals.urlCannotBeFound.cancelRegistrationText"
),
feedbackText: I18n.t(
"createGovernanceAction.modals.urlCannotBeFound.feedbackText"
),
},
} as const;

const storageInformationErrorModals: Record<
MetadataHashValidationErrors,
ModalState<
| (typeof externalDataDoesntMatchModal)["state"]
| (typeof urlCannotBeFound)["state"]
>
> = {
[MetadataHashValidationErrors.INVALID_URL]: urlCannotBeFound,
[MetadataHashValidationErrors.FETCH_ERROR]: urlCannotBeFound,
[MetadataHashValidationErrors.INVALID_JSON]: externalDataDoesntMatchModal,
[MetadataHashValidationErrors.INVALID_HASH]: externalDataDoesntMatchModal,
};

export const StorageInformation = ({ setStep }: StorageInformationProps) => {
const { t } = useTranslation();
const navigate = useNavigate();
const {
control,
errors,
Expand All @@ -23,6 +88,7 @@ export const StorageInformation = ({ setStep }: StorageInformationProps) => {
getValues,
watch,
} = useCreateGovernanceActionForm();
const { openModal, closeModal } = useModal();
const [isJsonDownloaded, setIsJsonDownloaded] = useState<boolean>(false);

// TODO: change on correct file name
Expand All @@ -45,12 +111,41 @@ export const StorageInformation = ({ setStep }: StorageInformationProps) => {
setIsJsonDownloaded(true);
};

const backToDashboard = () => {
navigate(PATHS.dashboard);
closeModal();
};

const handleStoringURLJSONValidation = useCallback(async () => {
const storingURL = getValues("storingURL");
try {
await createGovernanceAction();

// TODO: To be replaced wtih the correct hash
await validateMetadataHash(storingURL, "hash");
} catch (error: any) {
if (Object.values(MetadataHashValidationErrors).includes(error.message)) {
openModal({
...storageInformationErrorModals[
error.message as MetadataHashValidationErrors
],
onSubmit: () => {
setStep(3);
},
onCancel: backToDashboard,
// TODO: Open usersnap feedback
onFeedback: backToDashboard,
} as ModalState<StatusModalState>);
}
}
}, [getValues, generateJsonBody]);

return (
<BgCard
actionButtonLabel={t("continue")}
backButtonLabel={t("back")}
isActionButtonDisabled={isActionButtonDisabled}
onClickActionButton={createGovernanceAction}
onClickActionButton={handleStoringURLJSONValidation}
onClickBackButton={onClickBack}
>
<Typography sx={{ textAlign: "center" }} variant="headline4">
Expand Down
46 changes: 43 additions & 3 deletions govtool/frontend/src/components/organisms/StatusModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,16 @@ import { useScreenDimension, useTranslation } from "@/hooks";

export interface StatusModalState {
buttonText?: string;
cancelText?: string;
feedbackText?: string;
status: "warning" | "info" | "success";
isInfo?: boolean;
link?: string;
linkText?: string;
message: React.ReactNode;
onSubmit?: () => void;
onCancel?: () => void;
onFeedback?: () => void;
title: string;
dataTestId: string;
}
Expand Down Expand Up @@ -43,32 +48,67 @@ export function StatusModal() {
textAlign="center"
sx={{ fontSize: "16px", fontWeight: "400" }}
>
{state?.message}{" "}
{state?.message}
{state?.link && (
<Link
onClick={() => openInNewTab(state?.link || "")}
target="_blank"
sx={[{ "&:hover": { cursor: "pointer" } }]}
>
{t("thisLink")}
{state?.linkText || t("thisLink")}
</Link>
)}
</Typography>
</ModalContents>
<Button
data-testid={"confirm-modal-button"}
data-testid="confirm-modal-button"
onClick={state?.onSubmit ? state?.onSubmit : closeModal}
sx={{
borderRadius: 50,
margin: "0 auto",
padding: "10px 26px",
textTransform: "none",
marginTop: "38px",
width: "100%",
}}
variant="contained"
>
{state?.buttonText || t("confirm")}
</Button>
{state?.cancelText && (
<Button
data-testid="cancel-modal-button"
onClick={state?.onCancel ? state?.onCancel : closeModal}
sx={{
borderRadius: 50,
margin: "0 auto",
padding: "10px 26px",
textTransform: "none",
marginTop: "24px",
width: "100%",
}}
variant="outlined"
>
{state?.cancelText}
</Button>
)}
{state?.feedbackText && (
<Button
data-testid="feedback-button"
onClick={state?.onFeedback ? state?.onFeedback : closeModal}
sx={{
borderRadius: 50,
margin: "0 auto",
padding: "10px 26px",
textTransform: "none",
marginTop: "24px",
width: "100%",
}}
variant="outlined"
>
{state?.feedbackText}
</Button>
)}
</ModalWrapper>
);
}
23 changes: 12 additions & 11 deletions govtool/frontend/src/consts/governanceActionFields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,15 @@ export const GOVERNANCE_ACTION_FIELDS: GovernanceActionFields = {
placeholderI18nKey:
"createGovernanceAction.fields.declarations.receivingAddress.placeholder",
rules: {
validate: (value) => {
if (bech32.decode(value).words.length) {
return true;
} else {
validate: async (value) => {
try {
const decoded = await bech32.decode(value);
if (decoded.words.length) {
return true;
} else {
throw new Error();
}
} catch (error) {
return I18n.t("createGovernanceAction.fields.validations.bech32");
}
},
Expand All @@ -109,13 +114,9 @@ export const GOVERNANCE_ACTION_FIELDS: GovernanceActionFields = {
value: true,
message: I18n.t("createGovernanceAction.fields.validations.required"),
},
validate: (value) => {
if (Number.isInteger(Number(value))) {
return true;
} else {
return I18n.t("createGovernanceAction.fields.validations.number");
}
},
validate: (value) =>
Number.isInteger(Number(value)) ||
I18n.t("createGovernanceAction.fields.validations.number"),
},
},
},
Expand Down
2 changes: 1 addition & 1 deletion govtool/frontend/src/context/modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ const modals: Record<ModalType, ContextModal> = {

type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;

interface ModalState<T> {
export interface ModalState<T> {
type: ModalType;
state: T | null;
}
Expand Down
19 changes: 19 additions & 0 deletions govtool/frontend/src/i18n/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,25 @@ export const en = {
url: "Invalid URL",
},
},
modals: {
externalDataDoesntMatch: {
title: "Your External Data Does Not Match the Original File.",
message:
"GovTool checks the URL you entered to see if the JSON file that you self-host matches the one that was generated in GovTool. To complete registration, this match must be exact.\n\nIn this case, there is a mismatch. You can go back to the data edit screen and try the process again.",
buttonText: "Go to Data Edit Screen",
cancelRegistrationText: "Cancel Registration",
feedbackText: "Feedback",
},
urlCannotBeFound: {
title: "The URL You Entered Cannot Be Found",
message:
"GovTool cannot find the URL that you entered. Please check it and re-enter.",
linkText: "Learn More about self-hosting",
buttonText: "Go to Data Edit Screen",
cancelRegistrationText: "Cancel Registration",
feedbackText: "Feedback",
},
},
},
delegation: {
description:
Expand Down
12 changes: 12 additions & 0 deletions govtool/frontend/src/utils/canonizeJSON.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import jsonld from "jsonld";

/**
* Canonizes a JSON object using jsonld.canonize.
*
* @param json - The JSON object to be canonized.
* @returns A Promise that resolves to the canonized JSON object.
*/
export const canonizeJSON = async (json: Record<string, unknown>) => {
const canonized = await jsonld.canonize(json);
return canonized;
};
2 changes: 2 additions & 0 deletions govtool/frontend/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from "./adaFormat";
export * from "./basicReducer";
export * from "./callAll";
export * from "./canonizeJSON";
export * from "./checkIsMaintenanceOn";
export * from "./checkIsWalletConnected";
export * from "./formatDate";
Expand All @@ -14,3 +15,4 @@ export * from "./jsonUtils";
export * from "./localStorage";
export * from "./openInNewTab";
export * from "./removeDuplicatedProposals";
export * from "./validateMetadataHash";
Loading

0 comments on commit ea8d6c3

Please sign in to comment.