From 6c12db57cb3476bc67250aa23c8a24d3662c9d09 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Sat, 13 Apr 2024 06:29:17 -0700 Subject: [PATCH 1/4] feat: add ccip-020 component, hooks, and store --- src/components/votes/ccip-020.tsx | 184 +++++++++++++++++++++ src/hooks/use-ccip-020-vote-actions.tsx | 47 ++++++ src/hooks/use-ccip-020-vote-data.tsx | 39 +++++ src/store/ccip-020.ts | 202 ++++++++++++++++++++++++ 4 files changed, 472 insertions(+) create mode 100644 src/components/votes/ccip-020.tsx create mode 100644 src/hooks/use-ccip-020-vote-actions.tsx create mode 100644 src/hooks/use-ccip-020-vote-data.tsx create mode 100644 src/store/ccip-020.ts diff --git a/src/components/votes/ccip-020.tsx b/src/components/votes/ccip-020.tsx new file mode 100644 index 0000000..675e9d5 --- /dev/null +++ b/src/components/votes/ccip-020.tsx @@ -0,0 +1,184 @@ +import { + Box, + Button, + Divider, + Link, + ListItem, + Spinner, + Stack, + Stat, + StatLabel, + StatNumber, + Text, + UnorderedList, + useColorModeValue, +} from "@chakra-ui/react"; +import { useAtomValue } from "jotai"; +import { useCcip020VoteData } from "../../hooks/use-ccip-020-vote-data"; +import { useCcip020VoteActions } from "../../hooks/use-ccip-020-vote-actions"; +import { formatMicroAmount } from "../../store/common"; +import { hasVotedAtom } from "../../store/ccip-020"; +import VoteProgressBar from "./vote-progress-bar"; + +function VoteButtons() { + const { voteYes, voteNo, isRequestPending } = useCcip020VoteActions(); + const hasVoted = useAtomValue(hasVotedAtom); + + return ( + <> + {hasVoted ? "Change vote" : "Voting"}: + + + + + + ); +} + +function VoteResult() { + const voterInfo = useCcip020VoteData("voterInfo"); + + return ( + + Your Vote: + + + Recorded Vote: {voterInfo.data?.vote ? "Yes" : "No"} + + + MIA: {formatMicroAmount(voterInfo.data?.mia)} + NYC: {formatMicroAmount(voterInfo.data?.nyc)} + + + + ); +} + +function CCIP020() { + const isVoteActive = useCcip020VoteData("isVoteActive"); + const voteTotals = useCcip020VoteData("voteTotals"); + const voterInfo = useCcip020VoteData("voterInfo"); + const hasVoted = useAtomValue(hasVotedAtom); + + return ( + + + + + MIA Cycles + 80, 81 + + + NYC Cycles + 80, 81 + + + + + Yes Vote Count + + {voteTotals.data?.totals.totalVotesYes ?? } + + + + No Vote Count + + {voteTotals.data?.totals.totalVotesNo ?? } + + + + + + + + Related CCIPs: + + + CCIP-020 + + + + + Related Contracts: + + ccip-020-graceful-protocol-shutdown + + + + Voting Method: + + CCIP-015 + + + + Details: + + As the CityCoins Protocol prepares for the upcoming Nakamoto release + on the Stacks blockchain[^1], the accumulation of technical debt with + each change has made it increasingly challenging for core contributors + and volunteers to keep up with the necessary updates. In light of + these challenges and after careful consideration of community + feedback, this CCIP proposes a graceful shutdown of the CityCoins + Protocol. + + + + CCIP-020 + {" "} + implements the CCIP-015 voting mechanism as part of a DAO proposal to + disable mining and stacking in the CityCoins protocol, freezing the + total supply and unlocking any stacked CityCoins to claim. + + + {isVoteActive.data && hasVoted ? ( + <> + + Vote recorded, thank you! + Refresh to see stats once the tx confirms. + + ) : ( + + )} + {voterInfo.data && } + + ); +} + +export default CCIP020; diff --git a/src/hooks/use-ccip-020-vote-actions.tsx b/src/hooks/use-ccip-020-vote-actions.tsx new file mode 100644 index 0000000..84bb063 --- /dev/null +++ b/src/hooks/use-ccip-020-vote-actions.tsx @@ -0,0 +1,47 @@ +import { useOpenContractCall } from "@micro-stacks/react"; +import { boolCV } from "micro-stacks/clarity"; +import { + CONTRACT_ADDRESS, + CONTRACT_NAME, + ccip020HasVotedAtom, +} from "../store/ccip-020"; +import { useSetAtom } from "jotai"; + +export const useCcip020VoteActions = () => { + const { openContractCall, isRequestPending } = useOpenContractCall(); + const setCcip020HasVoted = useSetAtom(ccip020HasVotedAtom); + + const voteYes = async () => { + await openContractCall({ + contractAddress: CONTRACT_ADDRESS, + contractName: CONTRACT_NAME, + functionName: "vote-on-proposal", + functionArgs: [boolCV(true)], + onFinish: async (data) => { + console.log("Vote Yes on CCIP-020 success!", data); + setCcip020HasVoted(true); + }, + onCancel: () => { + console.log("Vote Yes on CCIP-020 popup closed!"); + }, + }); + }; + + const voteNo = async () => { + await openContractCall({ + contractAddress: CONTRACT_ADDRESS, + contractName: CONTRACT_NAME, + functionName: "vote-on-proposal", + functionArgs: [boolCV(false)], + onFinish: async (data) => { + console.log("Vote No on CCIP-020 success!", data); + setCcip020HasVoted(true); + }, + onCancel: () => { + console.log("Vote No on CCIP-020 popup closed!"); + }, + }); + }; + + return { voteYes, voteNo, isRequestPending }; +}; diff --git a/src/hooks/use-ccip-020-vote-data.tsx b/src/hooks/use-ccip-020-vote-data.tsx new file mode 100644 index 0000000..3ff74cd --- /dev/null +++ b/src/hooks/use-ccip-020-vote-data.tsx @@ -0,0 +1,39 @@ +import { useAtom } from "jotai"; +import { loadable } from "jotai/utils"; +import { + Ccip020Atoms, + ccip020IsExecutableQueryAtom, + ccip020IsVoteActiveQueryAtom, + ccip020VoteTotalsQueryAtom, + ccip020VoterInfoQueryAtom, +} from "../store/ccip-020"; +import { LoadableDataset, extractLoadableState } from "../store/common"; + +export const useCcip020VoteData = ( + selector: Ccip020Atoms +): LoadableDataset => { + const ccip020IsExecutableLoader = loadable(ccip020IsExecutableQueryAtom); + const ccip020IsVoteActiveLoader = loadable(ccip020IsVoteActiveQueryAtom); + const ccip020VoteTotalsLoader = loadable(ccip020VoteTotalsQueryAtom); + const ccip020VoterInfoLoader = loadable(ccip020VoterInfoQueryAtom); + + const isExecutable = extractLoadableState( + useAtom(ccip020IsExecutableLoader)[0] + ); + const isVoteActive = extractLoadableState( + useAtom(ccip020IsVoteActiveLoader)[0] + ); + const voteTotals = extractLoadableState(useAtom(ccip020VoteTotalsLoader)[0]); + const voterInfo = extractLoadableState(useAtom(ccip020VoterInfoLoader)[0]); + + switch (selector) { + case "isExecutable": + return isExecutable; + case "isVoteActive": + return isVoteActive; + case "voteTotals": + return voteTotals; + case "voterInfo": + return voterInfo; + } +}; diff --git a/src/store/ccip-020.ts b/src/store/ccip-020.ts new file mode 100644 index 0000000..09615eb --- /dev/null +++ b/src/store/ccip-020.ts @@ -0,0 +1,202 @@ +import { atom } from "jotai"; +import { atomWithStorage } from "jotai/utils"; +import { fetchReadOnlyFunction } from "micro-stacks/api"; +import { validateStacksAddress } from "micro-stacks/crypto"; +import { standardPrincipalCV, uintCV } from "micro-stacks/clarity"; +import { stxAddressAtom } from "./stacks"; + +///////////////////////// +// TYPES +///////////////////////// + +export type Ccip020Atoms = + | "isExecutable" + | "isVoteActive" + | "voteTotals" + | "voterInfo"; + +export type CityVoteRecord = { + totalAmountYes: number; + totalAmountNo: number; + totalVotesYes: number; + totalVotesNo: number; +}; + +export type Ccip020VoteTotals = { + mia: CityVoteRecord; + nyc: CityVoteRecord; + total: CityVoteRecord; +}; + +export type Ccip020VoterInfo = { + mia: number; + nyc: number; + vote: boolean; +}; + +///////////////////////// +// CONSTANTS +///////////////////////// + +export const CONTRACT_ADDRESS = "SP8A9HZ3PKST0S42VM9523Z9NV42SZ026V4K39WH"; +export const CONTRACT_NAME = "ccip020-graceful-protocol-shutdown"; +export const CONTRACT_FQ_NAME = `${CONTRACT_ADDRESS}.${CONTRACT_NAME}`; + +///////////////////////// +// LOCALSTORAGE ATOMS +///////////////////////// + +export const ccip020IsExecutableAtom = atomWithStorage( + "citycoins-ccip020-isExecutable", + false +); +export const ccip020IsVoteActiveAtom = atomWithStorage( + "citycoins-ccip020-isVoteActive", + false +); +export const ccip020VoteTotalsAtom = atomWithStorage( + "citycoins-ccip020-voteTotals", + null +); +export const ccip020VoterInfoAtom = atomWithStorage( + "citycoins-ccip020-voterInfo", + null +); +export const ccip020HasVotedAtom = atomWithStorage( + "citycoins-ccip020-hasVoted", + false +); + +///////////////////////// +// DERIVED ATOMS +///////////////////////// + +export const hasVotedAtom = atom((get) => { + const voterInfo = get(ccip020VoterInfoAtom); + const hasVoted = get(ccip020HasVotedAtom); + if (voterInfo !== null || hasVoted) { + return true; + } + return false; +}); + +///////////////////////// +// LOADABLE ASYNC ATOMS +///////////////////////// + +export const ccip020IsExecutableQueryAtom = atom(async () => { + try { + const isExecutable = await getIsExecutable(); + return isExecutable; + } catch (error) { + throw new Error( + `Failed to fetch is-executable for ${CONTRACT_FQ_NAME}: ${error}` + ); + } +}); + +export const ccip020IsVoteActiveQueryAtom = atom(async () => { + try { + const isVoteActive = await getIsVoteActive(); + return isVoteActive; + } catch (error) { + throw new Error( + `Failed to fetch is-vote-active for ${CONTRACT_FQ_NAME}: ${error}` + ); + } +}); + +export const ccip020VoteTotalsQueryAtom = atom(async () => { + try { + const voteTotals = await getVoteTotals(); + return voteTotals; + } catch (error) { + throw new Error( + `Failed to fetch get-vote-totals for ${CONTRACT_FQ_NAME}: ${error}` + ); + } +}); + +export const ccip020VoterInfoQueryAtom = atom(async (get) => { + const stxAddress = get(stxAddressAtom); + if (stxAddress === null) return undefined; + try { + const voterInfo = await getVoterInfo(stxAddress); + return voterInfo; + } catch (error) { + throw new Error( + `Failed to fetch get-voter-info with ${stxAddress} for ${CONTRACT_FQ_NAME}: ${error}` + ); + } +}); + +///////////////////////// +// HELPER FUNCTIONS +///////////////////////// + +async function getIsExecutable(): Promise { + const isExecutableQuery = await fetchReadOnlyFunction( + { + contractAddress: CONTRACT_ADDRESS, + contractName: CONTRACT_NAME, + functionName: "is-executable", + functionArgs: [], + }, + true + ); + return isExecutableQuery; +} + +async function getIsVoteActive(): Promise { + const isVoteActiveQuery = await fetchReadOnlyFunction( + { + contractAddress: CONTRACT_ADDRESS, + contractName: CONTRACT_NAME, + functionName: "is-vote-active", + functionArgs: [], + }, + true + ); + return isVoteActiveQuery; +} + +async function getVoteTotals(): Promise { + const voteTotalsQuery = await fetchReadOnlyFunction( + { + contractAddress: CONTRACT_ADDRESS, + contractName: CONTRACT_NAME, + functionName: "get-vote-totals", + functionArgs: [], + }, + true + ); + return voteTotalsQuery; +} + +async function getVoterInfo(voterAddress: string): Promise { + if (!validateStacksAddress(voterAddress)) { + throw new Error("Invalid STX address"); + } + console.log("Voter Address", voterAddress); + const voterIdQuery = await fetchReadOnlyFunction( + { + contractAddress: CONTRACT_ADDRESS, + contractName: "ccd003-user-registry", + functionName: "get-user-id", + functionArgs: [standardPrincipalCV(voterAddress)], + }, + true + ); + console.log("Voter ID", voterIdQuery); + const voterInfoQuery = await fetchReadOnlyFunction( + { + contractAddress: CONTRACT_ADDRESS, + contractName: CONTRACT_NAME, + functionName: "get-voter-info", + functionArgs: [uintCV(voterIdQuery)], + }, + true + ); + console.log("Voter Info", voterInfoQuery); + return voterInfoQuery; +} From a65d3e9891f9b19e108409fe1b6fe7641adec01b Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Sat, 13 Apr 2024 06:33:05 -0700 Subject: [PATCH 2/4] fix: add ccip-020 to vote tab in pending status --- src/components/tabs/voting.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/components/tabs/voting.tsx b/src/components/tabs/voting.tsx index d7c447a..ff6121a 100644 --- a/src/components/tabs/voting.tsx +++ b/src/components/tabs/voting.tsx @@ -14,6 +14,7 @@ import CCIP014 from "../votes/ccip-014"; import CCIP017 from "../votes/ccip-017"; import CCIP021 from "../votes/ccip-021"; import VoteTitle from "../votes/vote-title"; +import CCIP020 from "../votes/ccip-020"; function Voting() { return ( @@ -93,6 +94,19 @@ function Voting() { + +

+ + + +

+ + + +
); From e7d681e16150b1a86ca40865ae52450c1e08bfb9 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Sat, 13 Apr 2024 06:33:19 -0700 Subject: [PATCH 3/4] fix: fill in nakamoto ref link --- src/components/votes/ccip-020.tsx | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/components/votes/ccip-020.tsx b/src/components/votes/ccip-020.tsx index 675e9d5..ccea8c8 100644 --- a/src/components/votes/ccip-020.tsx +++ b/src/components/votes/ccip-020.tsx @@ -147,13 +147,18 @@ function CCIP020() { Details: - As the CityCoins Protocol prepares for the upcoming Nakamoto release - on the Stacks blockchain[^1], the accumulation of technical debt with - each change has made it increasingly challenging for core contributors - and volunteers to keep up with the necessary updates. In light of - these challenges and after careful consideration of community - feedback, this CCIP proposes a graceful shutdown of the CityCoins - Protocol. + As the CityCoins Protocol prepares for the{" "} + + upcoming Nakamoto release on the Stacks blockchain + + , the accumulation of technical debt with each change has made it + increasingly challenging for core contributors and volunteers to keep + up with the necessary updates. In light of these challenges and after + careful consideration of community feedback, this CCIP proposes a + graceful shutdown of the CityCoins Protocol. Date: Tue, 16 Apr 2024 17:41:40 -0700 Subject: [PATCH 4/4] fix: update ccip020 status --- src/components/tabs/voting.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/tabs/voting.tsx b/src/components/tabs/voting.tsx index ff6121a..bc0fc15 100644 --- a/src/components/tabs/voting.tsx +++ b/src/components/tabs/voting.tsx @@ -99,7 +99,7 @@ function Voting() {