Skip to content

Commit

Permalink
add checker
Browse files Browse the repository at this point in the history
  • Loading branch information
thelostone-mc committed Jan 28, 2025
1 parent 0f95078 commit 2816245
Show file tree
Hide file tree
Showing 11 changed files with 895 additions and 52 deletions.
3 changes: 2 additions & 1 deletion apps/admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@
"blo": "~1.2.0",
"dotenv": "^16.4.5",
"events": "^3.3.0",
"gitcoin-ui": "^3.7.0",
"gitcoin-ui": "^3.8.2",
"graphql-request": "^5.2.0",
"json-stringify-deterministic": "^1.0.12",
"pino-pretty": "^13.0.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
Expand Down
25 changes: 25 additions & 0 deletions apps/admin/src/components/Message.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Button } from "gitcoin-ui";

interface MessageProps {
title: string;
message: string;
action?: {
label: string;
onClick: () => void;
};
className?: string;
}

export function MessagePage({ title, message, action, className = "" }: MessageProps) {
return (
<div className={`flex min-h-[50vh] flex-col items-center justify-center p-4 ${className}`}>
<div className="mx-auto max-w-md text-center">
<h1 className="text-gray-900 mb-3 text-2xl font-bold">{title}</h1>

<p className="text-gray-600 mb-8">{message}</p>

{action && <Button onClick={action.onClick} value={action.label} />}
</div>
</div>
);
}
2 changes: 2 additions & 0 deletions apps/admin/src/hooks/checker/usePerformEvaluation/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./usePerformEvaluation";
export * from "./utils";
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { useState, useEffect } from "react";
import { useMutation } from "@tanstack/react-query";
import { EvaluationBody } from "gitcoin-ui/checker";
import { Hex } from "viem";
import { useAccount, useWalletClient } from "wagmi";
import { deterministicKeccakHash, submitEvaluation } from "./utils";

export const usePerformEvaluation = () => {
const [evaluationBody, setEvaluationBody] = useState<EvaluationBody | null>(null);
const { address } = useAccount();
const { data: walletClient } = useWalletClient();

const handleSetEvaluationBody = (data: EvaluationBody) => {
setEvaluationBody(data);
};

const signEvaluationBody = async (): Promise<Hex> => {
if (!walletClient) {
throw new Error("No wallet client found");
}

if (!evaluationBody) {
throw new Error("No evaluation body found");
}

const hash = await deterministicKeccakHash({
chainId: evaluationBody.chainId,
alloPoolId: evaluationBody.alloPoolId,
alloApplicationId: evaluationBody.alloApplicationId,
cid: evaluationBody.cid,
evaluator: address,
summaryInput: evaluationBody.summaryInput,
evaluationStatus: evaluationBody.evaluationStatus,
});

const signature = await walletClient.signMessage({ message: hash });

return signature;
};

// Evaluation mutation
const evaluationMutation = useMutation({
mutationFn: async (data: EvaluationBody) => {
if (!address) {
throw new Error("No address found");
}
const signature = await signEvaluationBody();
await submitEvaluation({ ...data, signature, evaluator: address });
},
});

// Trigger the signing mutation when evaluationBody is set
useEffect(() => {
if (evaluationBody) {
evaluationMutation.mutateAsync(evaluationBody);
}
}, [evaluationBody]);

return {
setEvaluationBody: handleSetEvaluationBody,
isEvaluating: evaluationMutation.isPending,
isError: evaluationMutation.isError,
isSuccess: evaluationMutation.isSuccess,
};
};
65 changes: 65 additions & 0 deletions apps/admin/src/hooks/checker/usePerformEvaluation/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// CHECKER
import { EvaluationBody, SyncPoolBody } from "gitcoin-ui/checker";
import stringify from "json-stringify-deterministic";
import { type Hex, keccak256, toHex } from "viem";

export const CHECKER_ENDPOINT = "https://api.checker.gitcoin.co";

export async function submitEvaluation(
evaluationBody: EvaluationBody,
): Promise<{ evaluationId: string }> {
const url = `${CHECKER_ENDPOINT}/api/evaluate`;

try {
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ ...evaluationBody, evaluatorType: "human" }),
});

if (!response.ok) {
const errorData = await response.json();
throw new Error(`Error: ${response.status} - ${errorData.message || "Unknown error"}`);
}

const data = await response.json();
return data.evaluationId;
} catch (error) {
console.error("Error submitting evaluation:", error);
throw error;
}
}

export async function syncPool(syncPoolBody: SyncPoolBody): Promise<boolean> {
const url = `${CHECKER_ENDPOINT}/api/pools`;

try {
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
...syncPoolBody,
skipEvaluation: false,
}),
});

if (!response.ok) {
const errorData = await response.json();
throw new Error(`Error: ${response.status} - ${errorData.message || "Unknown error"}`);
}

return true;
} catch (error) {
console.error("Error syncing pool:", error);
throw error;
}
}

export async function deterministicKeccakHash<T>(obj: T): Promise<Hex> {
const deterministicString = stringify(obj);
return keccak256(toHex(deterministicString));
}
2 changes: 2 additions & 0 deletions apps/admin/src/hooks/checker/usePerformOnChainReview/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./usePerformOnChainReview";
export * from "./utils";
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import { useState, useEffect, useMemo } from "react";
import { useMutation } from "@tanstack/react-query";
import { ReviewBody } from "gitcoin-ui/checker";
import { ProgressStatus } from "gitcoin-ui/types";
import { Abi, createPublicClient, encodeFunctionData, http } from "viem";
import { useWalletClient } from "wagmi";
import {
waitUntilIndexerSynced,
applicationStatusToNumber,
buildUpdatedRowsOfApplicationStatuses,
getStrategyInstance,
getOnchainEvaluationProgressSteps,
} from ".";

export const usePerformOnChainReview = () => {
const [reviewBody, setReviewBody] = useState<ReviewBody | null>(null);
const [contractUpdatingStatus, setContractUpdatingStatus] = useState<ProgressStatus>(
ProgressStatus.NOT_STARTED,
);
const [indexingStatus, setIndexingStatus] = useState<ProgressStatus>(ProgressStatus.NOT_STARTED);
const [finishingStatus, setFinishingStatus] = useState<ProgressStatus>(
ProgressStatus.NOT_STARTED,
);

const { data: walletClient } = useWalletClient();

const handleSetReviewBody = (reviewBody: ReviewBody | null) => {
setReviewBody(reviewBody);
};

const evaluationMutation = useMutation({
mutationFn: async (data: ReviewBody) => {
if (!walletClient) {
throw new Error("WalletClient is undefined");
}

try {
// Reset statuses before starting
setContractUpdatingStatus(ProgressStatus.IN_PROGRESS);

// Prepare the strategy instance based on the strategy type.
const { strategyInstance, strategyInstanceAbi } = getStrategyInstance(
data.strategyAddress,
walletClient.chain.id,
data.roundId,
data.strategy,
);

// Try to get total applications
let totalApplications = BigInt(0);
try {
totalApplications = await strategyInstance.recipientsCounter();
} catch (error) {
totalApplications = BigInt(data.currentApplications.length + 1);
}

// Build updated rows of application statuses
const rows = buildUpdatedRowsOfApplicationStatuses({
applicationsToUpdate: data.applicationsToUpdate,
currentApplications: data.currentApplications,
statusToNumber: applicationStatusToNumber,
bitsPerStatus: 4,
});

// Send transaction
const account = walletClient.account;
if (!account) {
throw new Error("WalletClient account is undefined");
}

const publicClient = createPublicClient({
chain: walletClient.chain,
transport: http(),
});

let txHash;
let receipt;
try {
txHash = await walletClient.sendTransaction({
account: account,
to: data.strategyAddress,
data: encodeFunctionData({
abi: strategyInstanceAbi as Abi,
functionName: "reviewRecipients",
args: [rows, totalApplications],
}),
});

receipt = await publicClient.waitForTransactionReceipt({
hash: txHash,
confirmations: 1,
});
} catch (sendError) {
setContractUpdatingStatus(ProgressStatus.IS_ERROR);
throw sendError;
}

if (!receipt.status) {
setContractUpdatingStatus(ProgressStatus.IS_ERROR);
throw new Error("Failed to update application status");
}

// Transaction sent successfully
setContractUpdatingStatus(ProgressStatus.IS_SUCCESS);
setIndexingStatus(ProgressStatus.IN_PROGRESS);

// Wait until indexer is synced
try {
await waitUntilIndexerSynced({
chainId: walletClient.chain.id,
blockNumber: receipt.blockNumber,
});
} catch (e) {
setIndexingStatus(ProgressStatus.IS_ERROR);
}

setIndexingStatus(ProgressStatus.IS_SUCCESS);

// Finishing up
setFinishingStatus(ProgressStatus.IN_PROGRESS);
// Any finishing steps can be added here
setFinishingStatus(ProgressStatus.IS_SUCCESS);
} catch (error) {
throw error;
}
},
});

useEffect(() => {
if (reviewBody) {
evaluationMutation.mutate(reviewBody);
}
}, [reviewBody]);

useEffect(() => {
if (evaluationMutation.isSuccess) {
setContractUpdatingStatus(ProgressStatus.NOT_STARTED);
setIndexingStatus(ProgressStatus.NOT_STARTED);
setFinishingStatus(ProgressStatus.NOT_STARTED);
evaluationMutation.reset();
}
}, [evaluationMutation]);

const steps = useMemo(() => {
return getOnchainEvaluationProgressSteps({
contractUpdatingStatus,
indexingStatus,
finishingStatus,
});
}, [contractUpdatingStatus, indexingStatus, finishingStatus]);

return {
setReviewBody: handleSetReviewBody,
steps,
isReviewing: evaluationMutation.isPending,
isError: evaluationMutation.isError,
isSuccess: evaluationMutation.isSuccess,
error: evaluationMutation.error,
};
};
Loading

0 comments on commit 2816245

Please sign in to comment.