Skip to content

Commit

Permalink
Vote execute (#124)
Browse files Browse the repository at this point in the history
Co-authored-by: Tupui <[email protected]>
  • Loading branch information
0xExp-po and tupui authored Dec 24, 2024
1 parent ba2bc4b commit 40bebb6
Show file tree
Hide file tree
Showing 6 changed files with 221 additions and 15 deletions.
87 changes: 77 additions & 10 deletions dapp/src/components/page/proposal/ExecuteProposalModal.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,56 @@
import React from "react";
import { useState, useEffect } from "react";
import { useState, useEffect, useMemo } from "react";
import JsonView from "react18-json-view";
import { modifySlashInXdr } from "utils/utils";
import { stellarLabViewXdrLink } from "constants/serviceLinks";
import * as StellarXdr from "utils/stellarXdr";
import { parseToLosslessJson } from "utils/passToLosslessJson";
import type { ProposalOutcome, VoteStatus, VoteType } from "types/proposal";

interface VotersModalProps {
xdr: string;
projectName: string;
proposalId: number | undefined;
outcome: ProposalOutcome | null;
voteStatus: VoteStatus | null;
onClose: () => void;
}

const VotersModal: React.FC<VotersModalProps> = ({ xdr, onClose }) => {
const VotersModal: React.FC<VotersModalProps> = ({
projectName,
proposalId,
outcome,
voteStatus,
onClose,
}) => {
const [content, setContent] = useState<any>(null);

const signAndExecute = () => {};
const voteResultAndXdr: { voteResult: VoteType | null; xdr: string | null } =
useMemo(() => {
if (voteStatus && outcome) {
const { approve, abstain } = voteStatus;
let voteResult: VoteType | null = null;
let xdr: string | null = null;
if (approve.score > abstain.score) {
voteResult = "approve";
xdr = outcome?.approved?.xdr || null;
} else if (approve.score < abstain.score) {
voteResult = "reject";
xdr = outcome?.rejected?.xdr || null;
} else {
voteResult = "abstain";
xdr = outcome?.cancelled?.xdr || null;
}
return { voteResult, xdr };
} else {
return { voteResult: null, xdr: null };
}
}, [voteStatus, outcome]);

useEffect(() => {
getContentFromXdr(voteResultAndXdr.xdr);
}, [voteResultAndXdr.xdr]);

const getContentFromXdr = async (_xdr: string) => {
const getContentFromXdr = async (_xdr: string | null) => {
try {
if (_xdr) {
const decoded = StellarXdr.decode("TransactionEnvelope", _xdr);
Expand All @@ -27,9 +61,42 @@ const VotersModal: React.FC<VotersModalProps> = ({ xdr, onClose }) => {
}
};

useEffect(() => {
getContentFromXdr(xdr);
}, [xdr]);
const signAndExecute = async () => {
if (!projectName) {
alert("Project name is required");
return;
}

if (proposalId === undefined) {
alert("Proposal ID is required");
return;
}

if (!voteResultAndXdr.voteResult) {
alert("Vote result is required");
return;
}

if (!voteResultAndXdr.xdr) {
alert("XDR is required");
return;
}

const { executeProposal } = await import("@service/WriteContractService");
const res = await executeProposal(
projectName,
proposalId,
voteResultAndXdr.xdr,
);
if (res.error) {
alert(res.errorMessage);
onClose();
} else {
console.log("execute result:", res.data);
alert("Proposal executed successfully");
onClose();
}
};

return (
<div
Expand All @@ -48,7 +115,7 @@ const VotersModal: React.FC<VotersModalProps> = ({ xdr, onClose }) => {
<div className="text-sm sm:text-lg md:text-[22px]">Outcome</div>
<div className="flex flex-col gap-1 sm:gap-2 md:gap-3">
<a
href={`${stellarLabViewXdrLink}${modifySlashInXdr(xdr)};;`}
href={`${stellarLabViewXdrLink}${modifySlashInXdr(voteResultAndXdr.xdr || "")};;`}
target="_blank"
rel="noreferrer"
className="flex items-center gap-0.5 sm:gap-1 text-black hover:text-blue group"
Expand All @@ -65,7 +132,7 @@ const VotersModal: React.FC<VotersModalProps> = ({ xdr, onClose }) => {
>
<path
d="M12.283 1.851A10.154 10.154 0 0 0 1.846 12.002c0 0.259 0.01 0.516 0.03 0.773A1.847 1.847 0 0 1 0.872 14.56L0 15.005v2.074l2.568 -1.309 0.832 -0.424 0.82 -0.417 14.71 -7.496 1.653 -0.842L24 4.85V2.776l-3.387 1.728 -2.89 1.473 -13.955 7.108a8.376 8.376 0 0 1 -0.07 -1.086 8.313 8.313 0 0 1 12.366 -7.247l1.654 -0.843 0.247 -0.126a10.154 10.154 0 0 0 -5.682 -1.932zM24 6.925 5.055 16.571l-1.653 0.844L0 19.15v2.072L3.378 19.5l2.89 -1.473 13.97 -7.117a8.474 8.474 0 0 1 0.07 1.092A8.313 8.313 0 0 1 7.93 19.248l-0.101 0.054 -1.793 0.914a10.154 10.154 0 0 0 16.119 -8.214c0 -0.26 -0.01 -0.522 -0.03 -0.78a1.848 1.848 0 0 1 1.003 -1.785L24 8.992Z"
stroke-width="1"
strokeWidth="1"
></path>
</svg>
<span className="text-xs sm:text-sm md:text-base">
Expand Down
8 changes: 6 additions & 2 deletions dapp/src/components/page/proposal/ProposalPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,13 @@ const ProposalPage: React.FC = () => {
};

const openExecuteProposalModal = () => {
if (proposal?.status === "active") {
if (proposal?.status === "voted") {
setIsExecuteProposalModalOpen(true);
} else {
alert("Cannot execute proposal.");
}
};

const getProposalDetails = async () => {
if (id !== undefined && projectName) {
setIsLoading(true);
Expand Down Expand Up @@ -149,7 +150,10 @@ const ProposalPage: React.FC = () => {
)}
{isExecuteProposalModalOpen && (
<ExecuteProposalModal
xdr={outcome?.approved.xdr ?? ""}
projectName={projectName}
proposalId={id}
outcome={outcome}
voteStatus={proposal?.voteStatus ?? null}
onClose={() => setIsExecuteProposalModalOpen(false)}
/>
)}
Expand Down
6 changes: 5 additions & 1 deletion dapp/src/components/page/proposal/VotingModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { VoteType } from "types/proposal";

interface VotersModalProps {
projectName: string;
proposalId: number;
proposalId: number | undefined;
isVoted: boolean;
setIsVoted: React.Dispatch<React.SetStateAction<boolean>>;
onClose: () => void;
Expand Down Expand Up @@ -34,6 +34,10 @@ const VotingModal: React.FC<VotersModalProps> = ({

setIsLoading(true);
const { voteToProposal } = await import("@service/WriteContractService");
if (proposalId === undefined) {
alert("Proposal ID is required");
return;
}
const res = await voteToProposal(projectName, proposalId, selectedOption);

if (res.data) {
Expand Down
1 change: 1 addition & 0 deletions dapp/src/constants/contractErrorMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const contractErrorMessages = {
8: "Proposal or page could not be found.",
9: "You have already voted.",
10: "The proposal voting time has expired.",
11: "The proposal has already been executed.",
};

export type ContractErrorMessageKey = keyof typeof contractErrorMessages;
132 changes: 131 additions & 1 deletion dapp/src/service/WriteContractService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,14 @@ import { loadedPublicKey } from "./walletService";
import Versioning from "../contracts/soroban_versioning";

import { loadedProjectId } from "./StateService";
import { keccak256 } from "js-sha3";
import * as pkg from "js-sha3";
const { keccak256 } = pkg;
import {
TransactionBuilder,
Transaction,
xdr,
rpc,
} from "@stellar/stellar-sdk";
import type { Vote } from "soroban_versioning";
import type { VoteType } from "types/proposal";
import type { Response } from "types/response";
Expand All @@ -12,6 +19,8 @@ import {
type ContractErrorMessageKey,
} from "constants/contractErrorMessages";

const server = new rpc.Server(import.meta.env.PUBLIC_SOROBAN_RPC_URL);

// Function to map VoteType to Vote
function mapVoteTypeToVote(voteType: VoteType): Vote {
switch (voteType) {
Expand Down Expand Up @@ -276,10 +285,131 @@ async function voteToProposal(
}
}

async function execute(
project_name: string,
proposal_id: number,
): Promise<Response<any>> {
const publicKey = loadedPublicKey();

if (!publicKey) {
return {
data: false,
error: true,
errorCode: -1,
errorMessage: "Please connect your wallet first",
};
} else {
Versioning.options.publicKey = publicKey;
}

const project_key = Buffer.from(
keccak256.create().update(project_name).digest(),
);

const tx = await Versioning.execute({
maintainer: publicKey,
project_key: project_key,
proposal_id: Number(proposal_id),
});

try {
const result = await tx.signAndSend({
signTransaction: async (xdr) => {
return await kit.signTransaction(xdr);
},
});
return {
data: result.result,
error: false,
errorCode: -1,
errorMessage: "",
};
} catch (e) {
console.error(e);
const { errorCode, errorMessage } = fetchErrorCode(e);
return { data: null, error: true, errorCode, errorMessage };
}
}

async function executeProposal(
project_name: string,
proposal_id: number,
executeXdr: string,
): Promise<Response<any>> {
const publicKey = loadedPublicKey();

if (!publicKey) {
return {
data: false,
error: true,
errorCode: -1,
errorMessage: "Please connect your wallet first",
};
}

const res = await execute(project_name, proposal_id);
if (res.error) {
return res;
}

const executorAccount = await server.getAccount(publicKey);
try {
const outcomeTransactionEnvelope = xdr.TransactionEnvelope.fromXDR(
executeXdr,
"base64",
);

const outcomeTransaction = outcomeTransactionEnvelope.v1().tx();

const transactionBuilder = new TransactionBuilder(executorAccount, {
fee: import.meta.env.PUBLIC_DEFAULT_FEE,
networkPassphrase: import.meta.env.PUBLIC_SOROBAN_NETWORK_PASSPHRASE,
});

outcomeTransaction.operations().forEach((operation) => {
transactionBuilder.addOperation(operation);
});

const compositeTransaction = transactionBuilder.setTimeout(180).build();

const { signedTxXdr } = await kit.signTransaction(
compositeTransaction.toXDR(),
);

const signedTransaction = new Transaction(
signedTxXdr,
import.meta.env.PUBLIC_SOROBAN_NETWORK_PASSPHRASE,
);

const result = await server.sendTransaction(signedTransaction);

if (result.status === "ERROR") {
return {
data: null,
error: true,
errorCode: -1,
errorMessage: "Transaction failed",
};
} else {
return {
data: result.hash,
error: false,
errorCode: -1,
errorMessage: "",
};
}
} catch (e) {
console.error(e);
const { errorCode, errorMessage } = fetchErrorCode(e);
return { data: null, error: true, errorCode, errorMessage };
}
}

export {
commitHash,
registerProject,
updateConfig,
createProposal,
voteToProposal,
executeProposal,
};
2 changes: 1 addition & 1 deletion dapp/src/utils/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ export const projectInfoLoaded = atom(false);
export const latestCommit = atom("");
export const projectCardModalOpen = atom(false);
export const projectNameForGovernance = atom("");
export const proposalId = atom(0);
export const proposalId = atom(undefined);
export const connectedPublicKey = atom("");

0 comments on commit 40bebb6

Please sign in to comment.