Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vote execute #124

Merged
merged 18 commits into from
Dec 24, 2024
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
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("");
Loading