Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@ changes.

### Removed

## [v2.0.14](https://github.com/IntersectMBO/govtool/releases/tag/v2.0.14) 2025-03-05

## [v2.0.14](https://github.com/IntersectMBO/govtool/releases/tag/v2.0.14) 2025-03-06

### Added

- Add image tag for DRep in GovTool metadata [Issue 3137](https://github.com/IntersectMBO/govtool/issues/3137)

### Fixed

- Fix calculating withdrawals when rewards records are empty for a given stake key [Issue 3134](https://github.com/IntersectMBO/govtool/issues/3134)
Expand Down
3,240 changes: 776 additions & 2,464 deletions govtool/frontend/package-lock.json

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions govtool/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,9 @@
"@typescript-eslint/parser": "^7.3.1",
"@vitejs/plugin-legacy": "^6.0.0",
"@vitejs/plugin-react-swc": "^3.7.2",
"@vitest/browser": "^2.1.8",
"@vitest/coverage-v8": "^2.1.8",
"@vitest/ui": "^2.1.8",
"@vitest/browser": "^3.0.7",
"@vitest/coverage-v8": "^3.0.7",
"@vitest/ui": "^3.0.7",
"chromatic": "^11.20.0",
"eslint": "^8.38.0",
"eslint-config-airbnb": "^19.0.4",
Expand All @@ -108,7 +108,7 @@
"typescript": "^5.0.2",
"vite": "^6.0.3",
"vite-plugin-compression": "^0.5.1",
"vitest": "^2.1.8"
"vitest": "^3.0.7"
},
"optionalDependencies": {
"@rollup/rollup-linux-arm64-musl": "4.12.0"
Expand Down
15 changes: 14 additions & 1 deletion govtool/frontend/src/components/molecules/DRepDataForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline";
import { Button, InfoText, Spacer, Typography } from "@atoms";
import { Rules } from "@consts";
import { useScreenDimension, useTranslation } from "@hooks";
import { ControlledField } from "../organisms";
import { DRepDataFormValues } from "@/types/dRep";

import { ControlledField } from "../organisms";

const MAX_NUMBER_OF_LINKS = 7;

type Props = {
Expand Down Expand Up @@ -100,6 +101,18 @@ export const DRepDataForm = ({ control, errors, register, watch }: Props) => {
maxLength={Rules.QUALIFICATIONS.maxLength.value}
/>
</div>
<div>
<FieldDescription
title={t("forms.dRepData.image")}
subtitle={t("forms.dRepData.imageHelpfulText")}
/>
<ControlledField.Input
{...{ control, errors }}
data-testid="image-input"
name="image"
rules={Rules.IMAGE_URL}
/>
</div>
<Box sx={{ display: "flex", flexDirection: "column", gap: 4, mt: 3 }}>
<Typography
variant="title2"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export const EditDRepForm = ({
motivations: data?.motivations ?? "",
qualifications: data?.qualifications ?? "",
paymentAddress: data?.paymentAddress ?? "",
image: data?.image ?? "",
linkReferences: groupedReferences?.Link ?? [getEmptyReference("Link")],
identityReferences: groupedReferences?.Identity ?? [
getEmptyReference("Identity"),
Expand Down
13 changes: 12 additions & 1 deletion govtool/frontend/src/consts/dRepActions/fields.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import i18n from "@/i18n";
import { URL_REGEX, isReceivingAddress, isValidURLLength } from "@/utils";
import {
IMAGE_REGEX,
URL_REGEX,
isReceivingAddress,
isValidURLLength,
} from "@/utils";

export const Rules = {
GIVEN_NAME: {
Expand Down Expand Up @@ -66,4 +71,10 @@ export const Rules = {
}),
},
},
IMAGE_URL: {
pattern: {
value: IMAGE_REGEX,
message: i18n.t("registration.fields.validations.image"),
},
},
};
9 changes: 8 additions & 1 deletion govtool/frontend/src/consts/dRepActions/jsonContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,14 @@ export const DREP_CONTEXT = {
},
paymentAddress: "CIP119:paymentAddress",
givenName: "CIP119:givenName",
image: "CIP119:image",
image: {
"@id": "CIP119:image",
"@context": {
ImageObject: "https://schema.org/ImageObject",
contentUrl: "CIP119:contentUrl",
sha256: "CIP119:sha256",
},
},
objectives: "CIP119:objectives",
motivations: "CIP119:motivations",
qualifications: "CIP119:qualifications",
Expand Down
2 changes: 1 addition & 1 deletion govtool/frontend/src/context/governanceAction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ const GovernanceActionProvider = ({ children }: PropsWithChildren) => {
const createGovernanceActionJsonLD = useCallback(
async (govActionMetadata: GovActionMetadata) => {
try {
const metadataBody = generateMetadataBody({
const metadataBody = await generateMetadataBody({
data: govActionMetadata,
acceptedKeys: ["title", "abstract", "motivation", "rationale"],
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export const useCreateGovernanceActionForm = (
throw new Error("Governance action type is not defined");
}

const body = generateMetadataBody({
const body = await generateMetadataBody({
data: getValues(),
acceptedKeys: ["title", "motivation", "abstract", "rationale"],
});
Expand Down
4 changes: 3 additions & 1 deletion govtool/frontend/src/hooks/forms/useEditDRepInfoForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const defaultEditDRepInfoValues: DRepDataFormValues = {
objectives: "",
motivations: "",
qualifications: "",
image: "",
paymentAddress: "",
linkReferences: [{ "@type": "Link", uri: "", label: "" }],
identityReferences: [{ "@type": "Identity", uri: "", label: "" }],
Expand Down Expand Up @@ -75,7 +76,7 @@ export const useEditDRepInfoForm = (
// Business Logic
const generateMetadata = useCallback(async () => {
const { linkReferences, identityReferences, ...rest } = getValues();
const body = generateMetadataBody({
const body = await generateMetadataBody({
data: {
...rest,
references: [...(linkReferences ?? []), ...(identityReferences ?? [])],
Expand All @@ -87,6 +88,7 @@ export const useEditDRepInfoForm = (
"qualifications",
"paymentAddress",
"doNotList",
"image",
],
});

Expand Down
4 changes: 3 additions & 1 deletion govtool/frontend/src/hooks/forms/useRegisterAsdRepForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const defaultRegisterAsDRepValues: DRepDataFormValues = {
motivations: "",
qualifications: "",
paymentAddress: "",
image: "",
linkReferences: [{ "@type": "Link", uri: "", label: "" }],
identityReferences: [{ "@type": "Identity", uri: "", label: "" }],
storeData: false,
Expand Down Expand Up @@ -91,7 +92,7 @@ export const useRegisterAsdRepForm = (
// Business Logic
const generateMetadata = useCallback(async () => {
const { linkReferences, identityReferences, ...rest } = getValues();
const body = generateMetadataBody({
const body = await generateMetadataBody({
data: {
...rest,
references: [...(linkReferences ?? []), ...(identityReferences ?? [])],
Expand All @@ -103,6 +104,7 @@ export const useRegisterAsdRepForm = (
"qualifications",
"paymentAddress",
"doNotList",
"image",
],
});

Expand Down
2 changes: 1 addition & 1 deletion govtool/frontend/src/hooks/forms/useVoteContextForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export const useVoteContextForm = (

const generateMetadata = useCallback(async () => {
const { voteContextText } = getValues();
const body = generateMetadataBody({
const body = await generateMetadataBody({
data: {
comment: voteContextText,
},
Expand Down
5 changes: 4 additions & 1 deletion govtool/frontend/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,8 @@
"motivationsHelpfulText": "Why do you want to be a DRep, what personal and professional experiences do you want to share.",
"qualifications": "Qualifications",
"qualificationsHelpfulText": "List any qualifications that are relevant to your role as a DRep",
"image": "Image",
"imageHelpfulText": "A URL to an image or base64-encoded image that represents you. This could be an photo or a logo.",
"paymentAddress": "Payment Address",
"paymentAddressHelpfulText": "An address for DReps to receive payments. Only one address can be entered.",
"doNotList": "Do Not List",
Expand Down Expand Up @@ -693,7 +695,8 @@
"maxLength": "Max {{maxLength}} characters",
"required": "This field is required",
"url": "Invalid URL",
"noSpaces": "No spaces allowed"
"noSpaces": "No spaces allowed",
"image": "Invalid image URL or properly formatted base64-encoded image"
}
}
},
Expand Down
1 change: 1 addition & 0 deletions govtool/frontend/src/types/dRep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export type DRepDataFormValues = {
objectives: string;
motivations: string;
qualifications: string;
image: string;
paymentAddress: string;
storeData?: boolean;
storingURL: string;
Expand Down
29 changes: 27 additions & 2 deletions govtool/frontend/src/utils/generateMetadataBody.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { getImageSha } from "./getImageSha";
import { URL_REGEX } from "./isValidFormat";

type MetadataConfig = {
data: Record<string, unknown>;
acceptedKeys: string[];
Expand All @@ -10,7 +13,7 @@ type MetadataConfig = {
* the data, accepted keys, and standard reference.
* @returns {Object} - The generated metadata body.
*/
export const generateMetadataBody = ({
export const generateMetadataBody = async ({
data,
acceptedKeys,
}: MetadataConfig) => {
Expand All @@ -24,15 +27,37 @@ export const generateMetadataBody = ({
.filter((link) => link.uri)
.map((link) => ({
"@type": link["@type"] ?? "Other",
label: link.label || "Label",
label: link.label ?? "Label",
uri: link.uri,
}))
: undefined;

const isUrl = (url?: unknown) => URL_REGEX.test(url as string);
let image;

if (isUrl(data?.image)) {
image = {
"@type": "ImageObject",
contentUrl: data.image,
sha256: await getImageSha(data.image as string),
};
} else {
image = data?.image
? {
"@type": "ImageObject",
contentUrl: data.image,
}
: undefined;
}

const body = Object.fromEntries(filteredData);
if (references?.length) {
body.references = references;
}

if (image) {
body.image = image;
}

return body;
};
24 changes: 24 additions & 0 deletions govtool/frontend/src/utils/getImageSha.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Retrieves the SHA-256 hash of an image from the specified URL.
* @param imageUrl - The URL of the image.
* @returns The SHA-256 hash of the image.
* @throws If the image URL is invalid or if there is an error fetching the image.
*/
export const getImageSha = async (imageUrl: string) => {
try {
const response = await fetch(imageUrl);
if (!response.ok)
throw new Error(`Failed to fetch image: ${response.statusText}`);

const imageBuffer = await response.arrayBuffer();
const hashBuffer = await crypto.subtle.digest("SHA-256", imageBuffer);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray
.map((b) => b.toString(16).padStart(2, "0"))
.join("");

return hashHex;
} catch (error) {
throw new Error(`Failed to process image: ${(error as Error).message}`);
}
};
2 changes: 2 additions & 0 deletions govtool/frontend/src/utils/isValidFormat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export const URL_REGEX =
/^(?:(?:https?:\/\/)?(?:\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,})(?:\/[^\s]*)?)|(?:ipfs:\/\/(?:[a-zA-Z0-9]+(?:\/[a-zA-Z0-9._-]+)*))$|^$/;
export const HASH_REGEX = /^[0-9A-Fa-f]+$/;
export const PAYMENT_ADDRESS_REGEX = /addr1[a-z0-9]+/i;
export const IMAGE_REGEX =
/^(https?:\/\/[^\s]+\.(?:png|jpg|jpeg|gif|bmp|webp|svg)(\?.*)?$|https?:\/\/[^\s]+$|data:image\/(?:png|jpeg|gif|bmp|webp|svg\+xml);base64,[A-Za-z0-9+/]+={0,2}$)/;

export function isValidURLFormat(str: string) {
if (!str.length) return false;
Expand Down
12 changes: 6 additions & 6 deletions govtool/frontend/src/utils/tests/generateMetadataBody.test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { generateMetadataBody } from "../generateMetadataBody";

describe("generateMetadataBody", () => {
it("generates metadata body with filtered data", () => {
it("generates metadata body with filtered data", async () => {
const data = {
name: "John Doe",
age: 30,
email: "johndoe@example.com",
};
const acceptedKeys = ["name", "age"];

const result = generateMetadataBody({
const result = await generateMetadataBody({
data,
acceptedKeys,
});
Expand All @@ -20,7 +20,7 @@ describe("generateMetadataBody", () => {
});
});

it("generates metadata body with filtered data and references", () => {
it("generates metadata body with filtered data and references", async () => {
const data = {
name: "John Doe",
age: 30,
Expand All @@ -32,7 +32,7 @@ describe("generateMetadataBody", () => {
};
const acceptedKeys = ["name", "age"];

const result = generateMetadataBody({
const result = await generateMetadataBody({
data,
acceptedKeys,
});
Expand All @@ -55,11 +55,11 @@ describe("generateMetadataBody", () => {
});
});

it("generates metadata body with empty data", () => {
it("generates metadata body with empty data", async () => {
const data = {};
const acceptedKeys = ["name", "age"];

const result = generateMetadataBody({
const result = await generateMetadataBody({
data,
acceptedKeys,
});
Expand Down
1 change: 1 addition & 0 deletions govtool/frontend/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ const viteConfig = defineViteConfig({
const vitestConfig = defineVitestConfig({
test: {
setupFiles: "./src/setupTests.ts",
testTimeout: 10000,
globals: true,
pool: "forks",
poolOptions: {
Expand Down
Loading