diff --git a/packages/hooks/dao/useDAOProposalById.ts b/packages/hooks/dao/useDAOProposalById.ts new file mode 100644 index 000000000..9225fb278 --- /dev/null +++ b/packages/hooks/dao/useDAOProposalById.ts @@ -0,0 +1,148 @@ +import { GnoJSONRPCProvider } from "@gnolang/gno-js-client"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { useCallback } from "react"; + +import { useDAOFirstProposalModule } from "./useDAOProposalModules"; +import { + cosmwasmToAppProposal, + GnoDAOProposal, + gnoToAppProposal, +} from "./useDAOProposals"; + +import { useFeedbacks } from "@/context/FeedbacksProvider"; +import { DaoProposalSingleQueryClient } from "@/contracts-clients/dao-proposal-single/DaoProposalSingle.client"; +import { + NetworkKind, + mustGetNonSigningCosmWasmClient, + parseUserId, +} from "@/networks"; +import { extractGnoJSONString } from "@/utils/gno"; + +const daoProposalByIdQueryKey = ( + daoId: string | undefined, + proposalId: number | undefined, +) => ["dao-proposals", daoId, proposalId]; + +export const useDAOProposalById = ( + daoId: string | undefined, + proposalId: number | undefined, +) => { + const { setToast } = useFeedbacks(); + const [network, daoAddress] = parseUserId(daoId); + const { daoProposal: cosmWasmDAOProposal, ...cosmWasmOther } = + useCosmWasmDAOProposalById(daoId, proposalId); + + const { data: gnoDAOProposal, ...gnoOther } = useQuery( + daoProposalByIdQueryKey(daoId, proposalId), + async () => { + try { + if (network?.kind !== NetworkKind.Gno) + throw new Error("Not a Gno network"); + if (!proposalId) throw new Error("Missing proposal id"); + + const provider = new GnoJSONRPCProvider(network.endpoint); + + const gnoProposal: GnoDAOProposal = extractGnoJSONString( + await provider.evaluateExpression( + daoAddress, + `getProposalJSON(0, ${proposalId})`, + ), + ); + + return gnoToAppProposal(gnoProposal); + } catch (err) { + const title = + "Failed to fetch the Gno DAO proposal\nThis proposal might not exist in this DAO"; + const message = err instanceof Error ? err.message : `${err}`; + setToast({ + title, + message, + type: "error", + mode: "normal", + }); + console.error(title, message); + return null; + } + }, + { + staleTime: Infinity, + enabled: !!(daoId && network?.kind === NetworkKind.Gno && proposalId), + }, + ); + + if (network?.kind === NetworkKind.Gno) { + return { + daoProposal: gnoDAOProposal, + ...gnoOther, + }; + } + + return { + daoProposal: cosmWasmDAOProposal, + ...cosmWasmOther, + }; +}; + +const useCosmWasmDAOProposalById = ( + daoId: string | undefined, + proposalId: number | undefined, +) => { + const { setToast } = useFeedbacks(); + const [network] = parseUserId(daoId); + const networkId = network?.id; + const { daoFirstProposalModule } = useDAOFirstProposalModule(daoId); + const proposalModuleAddress = daoFirstProposalModule?.address; + + const { data, ...other } = useQuery( + daoProposalByIdQueryKey(daoId, proposalId), + async () => { + try { + if (!networkId) throw new Error("Missing network id"); + if (!proposalModuleAddress) + throw new Error("No proposal module address"); + if (!proposalId) throw new Error("Missing proposal id"); + + const cosmwasmClient = await mustGetNonSigningCosmWasmClient(networkId); + const daoProposalClient = new DaoProposalSingleQueryClient( + cosmwasmClient, + proposalModuleAddress, + ); + + const daoProposal = await daoProposalClient.proposal({ + proposalId, + }); + + return cosmwasmToAppProposal(daoProposal); + } catch (err) { + const title = + "Failed to fetch the Cosmos DAO proposal\nThis proposal might not exist in this DAO"; + const message = err instanceof Error ? err.message : `${err}`; + setToast({ + title, + message, + type: "error", + mode: "normal", + }); + console.error(title, message); + return null; + } + }, + { + staleTime: Infinity, + enabled: !!(networkId && proposalModuleAddress && proposalId), + }, + ); + return { daoProposal: data, ...other }; +}; + +export const useInvalidateDAOProposalById = ( + daoId: string | undefined, + proposalId: number | undefined, +) => { + const queryClient = useQueryClient(); + return useCallback( + () => + queryClient.invalidateQueries(daoProposalByIdQueryKey(daoId, proposalId)), + [queryClient, daoId, proposalId], + ); +}; diff --git a/packages/hooks/dao/useDAOProposals.ts b/packages/hooks/dao/useDAOProposals.ts index 6ccd21c64..6ff82b99b 100644 --- a/packages/hooks/dao/useDAOProposals.ts +++ b/packages/hooks/dao/useDAOProposals.ts @@ -31,7 +31,7 @@ type GnoProposalVotes = { abstain: number; }; -type GnoDAOProposal = { +export type GnoDAOProposal = { id: number; title: string; description: string; @@ -67,47 +67,7 @@ export const useDAOProposals = (daoId: string | undefined) => { for (let i = 0; i < gnoProposals.length; i++) { const prop = gnoProposals[i]; - const title = prop.title; - const description = prop.description; - const status = prop.status.toLowerCase() as Status; - const proposer = prop.proposer; - const yesVotes = prop.votes.yes; - const noVotes = prop.votes.no; - const abstainVotes = prop.votes.abstain; - const threshold = - prop.threshold.thresholdQuorum.threshold.percent / 10000; - const quorum = prop.threshold.thresholdQuorum.quorum.percent / 10000; - const actions = prop.messages.map((m) => JSON.stringify(m)); - // TODO: render actions - proposals.push({ - id: i, - proposal: { - title, - description, - votes: { - yes: yesVotes.toString(), - no: noVotes.toString(), - abstain: abstainVotes.toString(), - }, - allow_revoting: false, - expiration: "TODO" as any, - msgs: prop.messages.map((m) => ({ - ...m, - gno: true, - })), - actions, - proposer, - start_height: prop.startHeight, - status, - threshold: { - threshold_quorum: { - threshold: { percent: `${threshold}` }, - quorum: { percent: `${quorum}` }, - }, - }, - total_power: prop.totalPower.toString(), - }, - }); + proposals.push(gnoToAppProposal(prop)); } return proposals; }, @@ -153,13 +113,7 @@ const useCosmWasmDAOProposals = (daoId: string | undefined) => { }); if (listProposals.proposals.length === 0) break; allProposals.push( - ...listProposals.proposals.map((p) => ({ - ...p, - proposal: { - ...p.proposal, - actions: [] as string[], - }, - })), + ...listProposals.proposals.map((p) => cosmwasmToAppProposal(p)), ); startAfter += listProposals.proposals.length; } @@ -178,3 +132,61 @@ export const useInvalidateDAOProposals = (daoId: string | undefined) => { [queryClient, daoId], ); }; + +export const gnoToAppProposal = (proposal: GnoDAOProposal) => { + // TODO: render actions + const title = proposal.title; + const description = proposal.description; + const status = proposal.status.toLowerCase() as Status; + const proposer = proposal.proposer; + const yesVotes = proposal.votes.yes; + const noVotes = proposal.votes.no; + const abstainVotes = proposal.votes.abstain; + const threshold = + proposal.threshold.thresholdQuorum.threshold.percent / 10000; + const quorum = proposal.threshold.thresholdQuorum.quorum.percent / 10000; + const actions = proposal.messages.map((m) => JSON.stringify(m)); + + const appProposal: AppProposalResponse = { + id: proposal.id, + proposal: { + title, + description, + votes: { + yes: yesVotes.toString(), + no: noVotes.toString(), + abstain: abstainVotes.toString(), + }, + allow_revoting: false, + expiration: "TODO" as any, + msgs: proposal.messages.map((m) => ({ + ...m, + gno: true, + })), + actions, + proposer, + start_height: proposal.startHeight, + status, + threshold: { + threshold_quorum: { + threshold: { percent: `${threshold}` }, + quorum: { percent: `${quorum}` }, + }, + }, + total_power: proposal.totalPower.toString(), + }, + }; + + return appProposal; +}; + +export const cosmwasmToAppProposal = (proposal: ProposalResponse) => { + const appPrpoposal: AppProposalResponse = { + ...proposal, + proposal: { + ...proposal.proposal, + actions: [] as string[], + }, + }; + return appPrpoposal; +};