From 6e3bfd0a153ce5760e8a4e60638017c24aec4986 Mon Sep 17 00:00:00 2001 From: hemanthghs Date: Sat, 25 Nov 2023 04:11:58 +0530 Subject: [PATCH] wip(gov): gov slice --- frontend/src/store/features/gov/govService.ts | 72 +++++++ frontend/src/store/features/gov/govSlice.ts | 191 ++++++++++++++++++ frontend/src/store/store.ts | 2 + frontend/src/types/gov.d.ts | 116 +++++++++++ 4 files changed, 381 insertions(+) create mode 100644 frontend/src/store/features/gov/govService.ts create mode 100644 frontend/src/store/features/gov/govSlice.ts create mode 100644 frontend/src/types/gov.d.ts diff --git a/frontend/src/store/features/gov/govService.ts b/frontend/src/store/features/gov/govService.ts new file mode 100644 index 000000000..abf6c4bd1 --- /dev/null +++ b/frontend/src/store/features/gov/govService.ts @@ -0,0 +1,72 @@ +import Axios, { AxiosResponse } from 'axios'; +import { convertPaginationToParams, cleanURL } from '../../../utils/util'; + +const proposalsURL = '/cosmos/gov/v1beta1/proposals'; +const proposalTallyURL = (id: string): string => + `/cosmos/gov/v1beta1/proposals/${id}/tally`; + +const voterVoteURL = (id: string, voter: string): string => + `/cosmos/gov/v1beta1/proposals/${id}/votes/${voter}`; + +const depositParamsURL = `/cosmos/gov/v1beta1/params/deposit`; + +const fetchProposals = ( + baseURL: string, + key: string | undefined, + limit: number | undefined, + status: number +): Promise> => { + let uri = `${cleanURL(baseURL)}${proposalsURL}`; + uri += `?proposal_status=${status}`; + + const params = convertPaginationToParams({ + key: key, + limit: limit, + }); + + if (params !== '') uri += `&${params}`; + return Axios.get(uri); +}; + +const fetchProposalTally = ( + baseURL: string, + proposalId: string +): Promise => { + let uri = `${cleanURL(baseURL)}${proposalTallyURL(proposalId)}`; + return Axios.get(uri); +}; + +const fetchVoterVote = ( + baseURL: string, + proposalId: string, + voter: string, + key: string | undefined, + limit: number | undefined +): Promise> => { + let uri = `${cleanURL(baseURL)}${voterVoteURL(proposalId, voter)}`; + const params = convertPaginationToParams({ + key: key, + limit: limit, + }); + if (params !== '') uri += `?${params}`; + return Axios.get(uri); +}; + +const fetchProposal = ( + baseURL: string, + proposalId: number +): Promise => + Axios.get(`${cleanURL(baseURL)}${proposalsURL}/${proposalId}`); + +const fetchDepositParams = (baseURL: string): Promise => + Axios.get(`${cleanURL(baseURL)}${depositParamsURL}`); + +const result = { + proposals: fetchProposals, + tally: fetchProposalTally, + votes: fetchVoterVote, + proposal: fetchProposal, + depositParams: fetchDepositParams, +}; + +export default result; diff --git a/frontend/src/store/features/gov/govSlice.ts b/frontend/src/store/features/gov/govSlice.ts new file mode 100644 index 000000000..c06ef5b50 --- /dev/null +++ b/frontend/src/store/features/gov/govSlice.ts @@ -0,0 +1,191 @@ +'use client'; + +import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; +import govService from './govService'; +import { cloneDeep } from 'lodash'; +import { AxiosError } from 'axios'; +import { ERR_UNKNOWN } from '@/utils/errors'; +import { TxStatus } from '@/types/enums'; + +interface Chain { + active: { + status: string; + errMsg: string; + proposals: ActiveProposal[]; + }; + votes: VotesData; + tally: ProposalTallyData; +} + +interface Chains { + [key: string]: Chain; +} + +interface GovState { + chains: Chains; + defaultState: Chain; +} + +const initialState: GovState = { + chains: {}, + defaultState: { + active: { + status: TxStatus.INIT, + errMsg: '', + proposals: [], + }, + votes: { + status: TxStatus.INIT, + errMsg: '', + proposals: {}, + }, + tally: { + status: TxStatus.INIT, + errMsg: '', + proposalTally: {}, + }, + }, +}; + +export const getProposalsInVoting = createAsyncThunk( + 'gov/active-proposals', + async (data: GetProposalsInVotingInputs, { rejectWithValue, dispatch }) => { + try { + const response = await govService.proposals( + data.baseURL, + data.key, + data.limit, + 2 + ); + + if (response?.data?.proposals?.length > 0) { + const proposals = response?.data?.proposals; + for (let i = 0; i < proposals.length; i++) { + dispatch( + getProposalTally({ + baseURL: data.baseURL, + proposalId: proposals[i].proposal_id, + chainID: data.chainID, + }) + ); + + dispatch( + getVotes({ + baseURL: data.baseURL, + proposalId: proposals[i].proposal_id, + voter: data.voter, + chainID: data.chainID, + }) + ); + } + } + + return { + chainID: data.chainID, + data: response.data, + }; + } catch (error) { + if (error instanceof AxiosError) return rejectWithValue(error.message); + return rejectWithValue(ERR_UNKNOWN); + } + } +); + +export const getVotes = createAsyncThunk( + 'gov/voter-votes', + async (data: GetVotesInputs) => { + const response = await govService.votes( + data.baseURL, + data.proposalId, + data.voter, + data.key, + data.limit + ); + + response.data.vote.proposal_id = data.proposalId; + + return { + chainID: data.chainID, + data: response.data, + }; + } +); + +export const getProposalTally = createAsyncThunk( + 'gov/proposal-tally', + async (data: GetProposalTallyInputs) => { + const response = await govService.tally(data.baseURL, data.proposalId); + + response.data.tally.proposal_id = data.proposalId; + + return { + chainID: data.chainID, + data: response.data, + }; + } +); + +export const govSlice = createSlice({ + name: 'gov', + initialState, + reducers: {}, + extraReducers: (builder) => { + // active proposals + builder + .addCase(getProposalsInVoting.pending, (state, action) => { + const chainID = action.meta?.arg?.chainID; + if (!state.chains[chainID]) + state.chains[chainID] = cloneDeep(initialState.defaultState); + }) + .addCase(getProposalsInVoting.fulfilled, (state, action) => { + const chainID = action.payload?.chainID || ''; + if (chainID.length > 0) { + let result = { + status: 'idle', + errMsg: '', + proposals: action.payload?.data?.proposals, + }; + state.chains[chainID].active = result; + } + }) + .addCase(getProposalsInVoting.rejected, (state, action) => {}); + + // votes + builder + .addCase(getVotes.pending, () => {}) + .addCase(getVotes.fulfilled, (state, action) => { + const chainID = action.payload.chainID; + let result: VotesData = { + status: 'idle', + errMsg: '', + proposals: state.chains[chainID].votes?.proposals || {}, + }; + + result.proposals[action.payload?.data?.vote?.proposal_id] = + action.payload.data; + + state.chains[chainID].votes = result; + }) + .addCase(getVotes.rejected, () => {}); + + // tally + builder + .addCase(getProposalTally.pending, () => {}) + .addCase(getProposalTally.fulfilled, (state, action) => { + const chainID = action.payload.chainID; + let result = { + status: 'idle', + errMsg: '', + proposalTally: state.chains[chainID].tally?.proposalTally || {}, + }; + + result.proposalTally[action.payload?.data?.tally?.proposal_id] = + action.payload?.data.tally; + state.chains[chainID].tally = result; + }) + .addCase(getProposalTally.rejected, () => {}); + }, +}); + +export const {} = govSlice.actions; +export default govSlice.reducer; diff --git a/frontend/src/store/store.ts b/frontend/src/store/store.ts index 86385af66..06061f865 100644 --- a/frontend/src/store/store.ts +++ b/frontend/src/store/store.ts @@ -8,6 +8,7 @@ import stakeSlice from './features/staking/stakeSlice'; import bankSlice from './features/bank/bankSlice'; import distributionSlice from './features/distribution/distributionSlice'; import authSlice from './features/auth/authSlice'; +import govSlice from './features/gov/govSlice'; export const store = configureStore({ reducer: { @@ -18,6 +19,7 @@ export const store = configureStore({ bank: bankSlice, auth: authSlice, distribution: distributionSlice, + gov: govSlice, }, }); diff --git a/frontend/src/types/gov.d.ts b/frontend/src/types/gov.d.ts new file mode 100644 index 000000000..e96fdeec2 --- /dev/null +++ b/frontend/src/types/gov.d.ts @@ -0,0 +1,116 @@ +interface ActiveProposal { + proposal_id: string; + content: { + '@type': string; + title: string; + description: string; + changes?: { + subspace: string; + key: string; + value: string; + }[]; + }; + status: string; + final_tally_result: { + yes: string; + abstain: string; + no: string; + no_with_veto: string; + }; + submit_time: string; + deposit_end_time: string; + total_deposit: { + denom: string; + amount: string; + }[]; + voting_start_time: string; + voting_end_time: string; +} + +interface CosmosHubProposal { + status: string; + errMsg: string; + proposals: ActiveProposal[]; +} + +interface GovPagination { + next_key: string | undefined; + total: string; +} + +interface GetProposalsInVotingResponse { + proposals: ActiveProposal[]; + pagination: GovPagination; +} + +interface VoteOption { + option: string; + weight: string; +} + +interface Vote { + proposal_id: string; + voter: string; + option: string; + options: VoteOption[]; +} + +interface ProposalVote { + vote: Vote; +} + +interface VotesData { + status: string; + errMsg: string; + proposals: { + [key: string]: ProposalVote; + }; +} + +interface ProposalTally { + [key: string]: { + yes: string; + abstain: string; + no: string; + no_with_veto: string; + proposal_id: string; + }; +} + +interface GetProposalTallyResponse { + [key: string]: { + yes: string; + abstain: string; + no: string; + no_with_veto: string; + }; +} + +interface ProposalTallyData { + status: string; + errMsg: string; + proposalTally: ProposalTally; +} + +interface GetProposalsInVotingInputs { + baseURL: string; + chainID: string; + voter: string; + key?: string; + limit?: number; +} + +interface GetVotesInputs { + baseURL: string; + proposalId: string; + voter: string; + chainID: string; + key?: string; + limit?: number; +} + +interface GetProposalTallyInputs { + baseURL: string; + proposalId: string; + chainID: string; +}