diff --git a/apps/namada-interface/src/App/Governance/Governance.components.ts b/apps/namada-interface/src/App/Governance/Governance.components.ts index 3cb97460c..b5ce31a82 100644 --- a/apps/namada-interface/src/App/Governance/Governance.components.ts +++ b/apps/namada-interface/src/App/Governance/Governance.components.ts @@ -139,6 +139,12 @@ export const ProposalDetailsContent = styled.div` color: ${(props) => props.theme.colors.utility1.main}; `; +export const ProposalDetailsContentMainHeader = styled.h1``; +export const ProposalDetailsContentSubHeader = styled.h3` + text-transform: capitalize; +`; +export const ProposalDetailsContentParagraph = styled.p``; + export const ProposalDetailsAddresses = styled.div``; export const ProposalDetailsAddressesHeader = styled.h4` margin-bottom: 0px; diff --git a/apps/namada-interface/src/App/Governance/Governance.tsx b/apps/namada-interface/src/App/Governance/Governance.tsx index f571c6cf3..2ba608ca7 100644 --- a/apps/namada-interface/src/App/Governance/Governance.tsx +++ b/apps/namada-interface/src/App/Governance/Governance.tsx @@ -19,32 +19,7 @@ import { SettingsState } from "slices/settings"; import { useAppSelector } from "store"; import { useCallback, useEffect, useState } from "react"; import { ProposalDetails } from "./ProposalDetails"; - -export type Proposal = { - id: string; - proposalType: string; - author: string; - startEpoch: bigint; - endEpoch: bigint; - graceEpoch: bigint; - content: Content; - status: string; - yesVotes?: string; - totalVotingPower?: string; - result?: string; -}; - -export type Content = { - abstract: string; - authors: string; - created: string; - details: string; - discussionsTo: string; - license: string; - motivation: string; - requires: string; - title: string; -}; +import { Proposal } from "./types"; const getStatus = (status: string, result?: string): string => { return result || status; @@ -85,18 +60,10 @@ export const Governance = (): JSX.Element => { useEffect(() => { const fetchProposals = async (): Promise => { try { - const sdkProposals = await query.query_proposals(); + const sdkProposals = await query.queryProposals(); const proposals = sdkProposals.map((proposal) => ({ ...proposal, - proposalType: proposal.proposal_type, - startEpoch: BigInt(proposal.start_epoch), - endEpoch: BigInt(proposal.end_epoch), - graceEpoch: BigInt(proposal.grace_epoch), - content: { - ...proposal.content, - discussionsTo: proposal.content["discussions-to"], - }, - totalVotingPower: proposal.total_voting_power, + content: JSON.parse(proposal.contentJSON) as Record, })); setProposals(proposals); } catch (e) { @@ -131,12 +98,13 @@ export const Governance = (): JSX.Element => { {"#" + proposal.id} - {proposal.content.title}: {proposal.content.details} + {proposal.content.title && `${proposal.content.title}: `} + {proposal.content.details || ""} {proposal.yesVotes && proposal.totalVotingPower && ( )} diff --git a/apps/namada-interface/src/App/Governance/ProposalDetails.tsx b/apps/namada-interface/src/App/Governance/ProposalDetails.tsx index f5e3a3995..862105cb3 100644 --- a/apps/namada-interface/src/App/Governance/ProposalDetails.tsx +++ b/apps/namada-interface/src/App/Governance/ProposalDetails.tsx @@ -17,13 +17,14 @@ import { ProposalDetailsAddressesHeader, ProposalDetailsButtons, ProposalDetailsContent, + ProposalDetailsContentSubHeader, + ProposalDetailsContentParagraph, + ProposalDetailsContentMainHeader, } from "./Governance.components"; -import { Proposal } from "slices/proposals"; import { useCallback, useEffect, useState } from "react"; import { SettingsState } from "slices/settings"; import { AccountsState } from "slices/accounts"; -import { AppLoader } from "App/App.components"; -import { pipe } from "fp-ts/lib/function"; +import { Proposal } from "./types"; export type ProposalDetailsProps = { open: boolean; @@ -31,13 +32,17 @@ export type ProposalDetailsProps = { maybeProposal: O.Option; }; -export const ProposalDetails = (props: ProposalDetailsProps): JSX.Element => { - if (O.isNone(props.maybeProposal)) { - return <>; - } +const EXPECTED_CONTENT_FIELDS = [ + "id", + "title", + "authors", + "details", + "motivation", + "license", +]; - const proposal = props.maybeProposal.value; - const { onClose } = props; +export const ProposalDetails = (props: ProposalDetailsProps): JSX.Element => { + const { onClose, maybeProposal } = props; const { chainId } = useAppSelector((state) => state.settings); const { derived } = useAppSelector((state) => state.accounts); @@ -73,9 +78,14 @@ export const ProposalDetails = (props: ProposalDetailsProps): JSX.Element => { const integration = getIntegration(chainId); const signer = integration.signer() as Signer; + if (O.isNone(maybeProposal)) { + throw new Error("No proposal"); + } + if (O.isNone(activeDelegator)) { throw new Error("No active delegator"); } + const proposal = maybeProposal.value; await signer.submitVoteProposal( { @@ -93,11 +103,11 @@ export const ProposalDetails = (props: ProposalDetailsProps): JSX.Element => { AccountType.Mnemonic ); }, - [activeDelegator, proposal] + [activeDelegator, maybeProposal] ); useEffect(() => { - const fetchData = async () => { + const fetchData = async (proposal: Proposal): Promise => { try { const votes = await query.get_proposal_votes(BigInt(proposal.id)); setActiveProposalVotes(new Map(votes)); @@ -113,30 +123,90 @@ export const ProposalDetails = (props: ProposalDetailsProps): JSX.Element => { } }; - if (addresses.length > 0) { - fetchData(); + if (addresses.length > 0 && O.isSome(maybeProposal)) { + fetchData(maybeProposal.value); } - }, [JSON.stringify(addresses)]); + }, [JSON.stringify(addresses), maybeProposal]); - if (O.isSome(activeDelegator) && O.isSome(delegators)) { + if ( + O.isSome(activeDelegator) && + O.isSome(delegators) && + O.isSome(maybeProposal) + ) { const delegatorAddress = activeDelegator.value; const delegations = delegators.value; + const { id, content, status, result } = maybeProposal.value; + const { title, authors, details, motivation, license } = content; + + const unexpectedFields = Object.entries(content).filter( + ([k]) => !EXPECTED_CONTENT_FIELDS.includes(k) + ); return ( -

- #{proposal.id} {proposal.content.title} -

-

by: {proposal.content.authors}

-

Details:

-

{proposal.content.details}

-

Motivation:

-

{proposal.content.motivation}

-

License:

-

{proposal.content.license}

- - {proposal.status === "on-going" && !proposal.result && ( + {/* main header */} + + #{id} {title} + + + {/* authors */} + {authors && ( + + by: {authors} + + )} + + {/* details */} + {details && ( + <> + + Details: + + + {details} + + + )} + + {/* motivation */} + {motivation && ( + <> + + Motivation: + + + {motivation} + + + )} + + {/* license */} + {license && ( + <> + + License: + + + {license} + + + )} + {unexpectedFields.map(([k, v]) => ( + <> + + {k}: + + + {v} + + + ))} + + {/* status */} + + {/* voting section */} + {status === "on-going" && !result && ( <> @@ -185,6 +255,6 @@ export const ProposalDetails = (props: ProposalDetailsProps): JSX.Element => {
); } else { - return ; + return <>; } }; diff --git a/apps/namada-interface/src/App/Governance/types.ts b/apps/namada-interface/src/App/Governance/types.ts new file mode 100644 index 000000000..a89fe2c13 --- /dev/null +++ b/apps/namada-interface/src/App/Governance/types.ts @@ -0,0 +1,16 @@ +import BigNumber from "bignumber.js"; + +// TODO: move types to @namada/types +export type Proposal = { + id: string; + proposalType: string; + author: string; + startEpoch: bigint; + endEpoch: bigint; + graceEpoch: bigint; + content: Partial<{ [key: string]: string }>; + status: string; + yesVotes?: BigNumber; + totalVotingPower?: BigNumber; + result?: string; +}; diff --git a/apps/namada-interface/src/slices/index.ts b/apps/namada-interface/src/slices/index.ts index f2006f30d..06ca80dbe 100644 --- a/apps/namada-interface/src/slices/index.ts +++ b/apps/namada-interface/src/slices/index.ts @@ -3,6 +3,5 @@ export { default as transfersReducer } from "./transfers"; export { default as channelsReducer } from "./channels"; export { default as settingsReducer } from "./settings"; export { default as coinsReducer } from "./coins"; -export { default as proposalsReducer } from "./proposals"; export { notificationsReducer } from "./notifications"; export { stakingAndGovernanceReducers } from "./StakingAndGovernance"; diff --git a/apps/namada-interface/src/slices/proposals.ts b/apps/namada-interface/src/slices/proposals.ts deleted file mode 100644 index aabc46bf5..000000000 --- a/apps/namada-interface/src/slices/proposals.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { chains } from "@namada/chains"; -import { Query } from "@namada/shared"; -import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; -import BigNumber from "bignumber.js"; -import { RootState } from "store"; - -//TODO: change case in those types -export type Proposal = { - id: string; - proposalType: string; - author: string; - startEpoch: bigint; - endEpoch: bigint; - graceEpoch: bigint; - content: Content; - status: string; - yesVotes?: string; - totalVotingPower?: string; - result?: string; -}; - -export type Content = { - abstract: string; - authors: string; - created: string; - details: string; - discussionsTo: string; - license: string; - motivation: string; - requires: string; - title: string; -}; - -export type ProposalsState = { - proposals: Proposal[]; - active?: { - proposalId: string; - delegations: Record; - }; -}; - -const PROPOSALS_ACTIONS_BASE = "proposals"; -const INITIAL_STATE: ProposalsState = { - proposals: [], -}; - -enum ProposalsActions { - FetchProposals = "fetchProposals", -} - -export const fetchProposals = createAsyncThunk< - Proposal[], - void, - { state: RootState } ->( - `${PROPOSALS_ACTIONS_BASE}/${ProposalsActions.FetchProposals}`, - async (_, thunkApi) => { - const state = thunkApi.getState(); - const chainId = state.settings.chainId; - const { rpc } = chains[chainId]; - const query = new Query(rpc); - let proposals: Proposal[] = []; - - try { - const sdkProposals = await query.query_proposals(); - proposals = sdkProposals.map((p) => ({ - ...p, - proposalType: p.proposal_type, - startEpoch: BigInt(p.start_epoch), - endEpoch: BigInt(p.end_epoch), - graceEpoch: BigInt(p.grace_epoch), - content: { ...p.content, discussionsTo: p.content["discussions-to"] }, - })); - } catch (e) { - console.error(e); - } - - return proposals; - } -); - -const proposalsSlice = createSlice({ - name: PROPOSALS_ACTIONS_BASE, - initialState: INITIAL_STATE, - reducers: {}, - extraReducers: (builder) => { - builder.addCase(fetchProposals.fulfilled, (state, action) => { - state.proposals = action.payload; - }); - }, -}); - -const { reducer } = proposalsSlice; - -export default reducer; diff --git a/apps/namada-interface/src/store/mocks.ts b/apps/namada-interface/src/store/mocks.ts index 48cc5f31b..8c252b342 100644 --- a/apps/namada-interface/src/store/mocks.ts +++ b/apps/namada-interface/src/store/mocks.ts @@ -146,7 +146,4 @@ export const mockAppState: RootState = { toasts: {}, pendingActions: [], }, - proposals: { - proposals: [], - }, }; diff --git a/apps/namada-interface/src/store/store.ts b/apps/namada-interface/src/store/store.ts index 48319c3b2..c7f0a38f5 100644 --- a/apps/namada-interface/src/store/store.ts +++ b/apps/namada-interface/src/store/store.ts @@ -11,7 +11,6 @@ import { coinsReducer, notificationsReducer, stakingAndGovernanceReducers, - proposalsReducer, } from "slices"; import { LocalStorageKeys } from "App/types"; import { createTransform } from "redux-persist"; @@ -43,7 +42,6 @@ const reducers = combineReducers({ coins: coinsReducer, notifications: notificationsReducer, stakingAndGovernance: stakingAndGovernanceReducers, - proposals: proposalsReducer, }); const persistConfig = { diff --git a/packages/shared/lib/Cargo.lock b/packages/shared/lib/Cargo.lock index 482907d00..faa2134a7 100644 --- a/packages/shared/lib/Cargo.lock +++ b/packages/shared/lib/Cargo.lock @@ -2916,7 +2916,7 @@ checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" [[package]] name = "namada" -version = "0.20.0" +version = "0.20.1" dependencies = [ "async-trait", "bimap", @@ -2964,7 +2964,7 @@ dependencies = [ [[package]] name = "namada_core" -version = "0.20.0" +version = "0.20.1" dependencies = [ "ark-bls12-381", "ark-ec", @@ -3014,7 +3014,7 @@ dependencies = [ [[package]] name = "namada_ethereum_bridge" -version = "0.20.0" +version = "0.20.1" dependencies = [ "borsh", "ethers", @@ -3034,7 +3034,7 @@ dependencies = [ [[package]] name = "namada_macros" -version = "0.20.0" +version = "0.20.1" dependencies = [ "proc-macro2", "quote", @@ -3043,7 +3043,7 @@ dependencies = [ [[package]] name = "namada_proof_of_stake" -version = "0.20.0" +version = "0.20.1" dependencies = [ "borsh", "data-encoding", diff --git a/packages/shared/lib/src/query.rs b/packages/shared/lib/src/query.rs index 7eafaa6d5..eb25d7ee1 100644 --- a/packages/shared/lib/src/query.rs +++ b/packages/shared/lib/src/query.rs @@ -1,3 +1,5 @@ +use borsh::BorshSerialize; +use js_sys::Uint8Array; use masp_primitives::{transaction::components::Amount, zip32::ExtendedFullViewingKey}; use namada::ledger::governance::storage as gov_storage; use namada::ledger::masp::ShieldedContext; @@ -8,7 +10,7 @@ use namada::ledger::rpc::{ query_storage_value, }; use namada::proof_of_stake::Epoch; -use namada::types::governance::{ProposalVote, TallyResult, VotePower}; +use namada::types::governance::{ProposalVote, TallyResult}; use namada::types::transaction::governance::ProposalType; use namada::types::{ address::Address, @@ -305,13 +307,12 @@ impl Query { to_js_result(result) } - pub async fn query_proposals(&self) -> Result { + pub async fn query_proposals(&self) -> Result { async fn print_proposal( client: &C, id: u64, current_epoch: Epoch, ) -> Option { - web_sys::console::log_1(&format!("Proposal id: {}", id).into()); let author_key = gov_storage::get_author_key(id); let start_epoch_key = gov_storage::get_voting_start_epoch_key(id); let end_epoch_key = gov_storage::get_voting_end_epoch_key(id); @@ -329,15 +330,13 @@ impl Query { let content_key = gov_storage::get_content_key(id); let content = query_storage_value::>(client, &content_key).await?; + let content = serde_json::to_string(&content).expect("TODO: handle error"); - web_sys::console::log_1(&"ASD!".into()); let votes = get_proposal_votes(client, start_epoch, id).await; - web_sys::console::log_1(&"ASD2!".into()); let total_stake = get_total_staked_tokens(client, start_epoch) .await .try_into() .unwrap(); - web_sys::console::log_1(&"ASD3!".into()); let status; let mut yes_votes = None; let mut total_voting_power = None; @@ -382,10 +381,10 @@ impl Query { let res = ProposalInfo { id: id.to_string(), proposal_type: String::from(proposal_type), - author, - start_epoch: start_epoch.to_string(), - end_epoch: end_epoch.to_string(), - grace_epoch: grace_epoch.to_string(), + author: author.to_string(), + start_epoch: start_epoch.0, + end_epoch: end_epoch.0, + grace_epoch: grace_epoch.0, content, status: status.to_string(), yes_votes: yes_votes.map(|v| v.to_string()), @@ -415,9 +414,12 @@ impl Query { let v = print_proposal(&self.client, last_proposal_id - 1, current_epoch) .await .expect("WORK"); + let mut writer = vec![]; + results.push(v); + BorshSerialize::serialize(&results, &mut writer)?; - to_js_result(results) + Ok(Uint8Array::from(writer.as_slice())) } pub async fn get_total_delegations( @@ -475,20 +477,19 @@ impl Query { } #[derive(Serialize)] -struct Votes { - pub validator_votes: HashMap, - pub delegator_votes: HashMap>, +struct ContentInfo { + content: HashMap, } -#[derive(Serialize)] +#[derive(BorshSerialize)] struct ProposalInfo { id: String, proposal_type: String, - author: Address, - start_epoch: String, - end_epoch: String, - grace_epoch: String, - content: HashMap, + author: String, + start_epoch: u64, + end_epoch: u64, + grace_epoch: u64, + content: String, status: String, yes_votes: Option, total_voting_power: Option, diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index c51348b9a..9f1d04375 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -1,10 +1,10 @@ import { isLeft } from "fp-ts/Either"; import { PathReporter } from "io-ts/PathReporter"; -import { Proposals } from "./types"; -import type { Proposal } from "./types"; import { Query as RustQuery } from "./shared/shared"; import { Type } from "io-ts"; +import { Proposal, Proposals } from "./types"; +import { deserialize } from "@dao-xyz/borsh"; export * from "./shared/shared"; export * from "./types"; @@ -69,11 +69,12 @@ export class Query extends RustQuery { super.query_my_validators.bind(this) ); get_proposal_votes = promiseWithTimeout(super.get_proposal_votes.bind(this)); - query_proposals = async (): Promise => { + // query_proposals = promiseWithTimeout(super.query_proposals.bind(this)); + queryProposals = async (): Promise => { const fn = this._query_proposals; const proposals = await fn(); - console.log(proposals); - return validateData(proposals, Proposals); + const deserialized = deserialize(proposals, Proposals); + return deserialized.proposals; }; get_total_delegations = promiseWithTimeout( super.get_total_delegations.bind(this) diff --git a/packages/shared/src/types.ts b/packages/shared/src/types.ts index e7c9d4bc2..0a3784a9e 100644 --- a/packages/shared/src/types.ts +++ b/packages/shared/src/types.ts @@ -1,34 +1,61 @@ -import * as t from "io-ts"; - -const Content = t.type({ - abstract: t.string, - authors: t.string, - created: t.string, - details: t.string, - "discussions-to": t.string, - license: t.string, - motivation: t.string, - requires: t.string, - title: t.string, -}); - -export const Proposal = t.intersection([ - t.type({ - id: t.string, - proposal_type: t.string, - author: t.string, - start_epoch: t.string, - end_epoch: t.string, - grace_epoch: t.string, - content: Content, - status: t.string, - }), - t.partial({ - yes_votes: t.string, - total_voting_power: t.string, - result: t.string, - }), -]); - -export const Proposals = t.array(Proposal); -export type Proposal = t.TypeOf; +import { field, option, vec } from "@dao-xyz/borsh"; +import BigNumber from "bignumber.js"; +import { BinaryWriter, BinaryReader } from "@dao-xyz/borsh"; + +export const BigNumberSerializer = { + serialize: (value: BigNumber, writer: BinaryWriter) => { + writer.string(value.toString()); + }, + deserialize: (reader: BinaryReader): BigNumber => { + const valueString = reader.string(); + return new BigNumber(valueString); + }, +}; + +export class Proposal { + @field({ type: "string" }) + id!: string; + + @field({ type: "string" }) + proposalType!: string; + + @field({ type: "string" }) + author!: string; + + @field({ type: "u64" }) + startEpoch!: bigint; + + @field({ type: "u64" }) + endEpoch!: bigint; + + @field({ type: "u64" }) + graceEpoch!: bigint; + + @field({ type: "string" }) + contentJSON!: string; + + @field({ type: "string" }) + status!: string; + + @field({ type: option(BigNumberSerializer) }) + yesVotes?: BigNumber; + + @field({ type: option(BigNumberSerializer) }) + totalVotingPower?: BigNumber; + + @field({ type: option("string") }) + result?: string; + + constructor(data: Proposal) { + Object.assign(this, data); + } +} + +export class Proposals { + @field({ type: vec(Proposal) }) + proposals!: Proposal[]; + + constructor(data: Proposals) { + Object.assign(this, data); + } +} diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json index c9d5f74da..b3d0954ff 100644 --- a/packages/shared/tsconfig.json +++ b/packages/shared/tsconfig.json @@ -1,20 +1,21 @@ { "compilerOptions": { - "baseUrl": "./src", - "target": "es2015", - "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, - "skipLibCheck": true, - "esModuleInterop": true, "allowSyntheticDefaultImports": true, - "strict": true, + "baseUrl": "./src", + "esModuleInterop": true, + "experimentalDecorators": true, "forceConsistentCasingInFileNames": true, - "noFallthroughCasesInSwitch": true, + "isolatedModules": true, + "lib": ["dom", "dom.iterable", "esnext"], "module": "esnext", "moduleResolution": "node", - "resolveJsonModule": true, - "isolatedModules": true, "noEmit": true, + "noFallthroughCasesInSwitch": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "target": "es2015", "types": ["node", "jest"] }, "include": ["src"],