Browse Source

ProposalDetails, ProposalList etc. - transport integration

Leszek Wiesner 4 years ago
parent
commit
2cb366e017

+ 98 - 17
packages/joy-proposals/src/Proposal/Body.tsx

@@ -1,16 +1,103 @@
 import React from "react";
 import { Card, Header, Item } from "semantic-ui-react";
+import { ProposalType } from "./ProposalTypePreview";
+import { blake2AsHex } from '@polkadot/util-crypto';
+import styled from 'styled-components';
+import AddressMini from '@polkadot/react-components/AddressMiniJoy';
 
 type BodyProps = {
   title: string;
   description: string;
-  params: {
-    tokensAmount: number;
-    destinationAccount: string;
-  };
+  params: any[];
+  type: ProposalType;
 };
 
-export default function Body({ title, description, params }: BodyProps) {
+function ProposedAddress(props: { address: string }) {
+  return (
+    <AddressMini
+      value={props.address}
+      isShort={false}
+      isPadded={false}
+      withAddress={true}
+      style={{padding: 0}} />
+  );
+}
+
+// The methods for parsing params by Proposal type.
+// They take the params as array and return { LABEL: VALUE } object.
+const paramParsers: { [x in ProposalType]: (params: any[]) => { [key: string]: string | number | JSX.Element } } = {
+  "Text" : ([content]) => ({
+    "Content": content,
+  }),
+  "RuntimeUpgrade" : ([wasm]) => {
+    const buffer: Buffer = Buffer.from(wasm.replace('0x', ''), 'hex');
+    return {
+      "Blake2b256 hash of WASM code": blake2AsHex(buffer, 256),
+      "File size": buffer.length + ' bytes',
+    }
+  },
+  "SetElectionParameters" : ([params]) => ({
+      "Announcing period": params.announcingPeriod + " blocks",
+      "Voting period": params.votingPeriod + " blocks",
+      "Revealing period": params.revealingPeriod + " blocks",
+      "Council size": params.councilSize + " members",
+      "Candidacy limit": params.candidacyLimit + " members",
+      "New term duration": params.newTermDuration + " blocks",
+      "Min. council stake": params.minCouncilStake + " tJOY",
+      "Min. voting stake": params.minVotingStake + " tJOY"
+  }),
+  "Spending" : ([amount, account]) => ({
+    "Amount": amount + ' tJOY',
+    "Account": <ProposedAddress address={account}/>,
+  }),
+  "SetLead" : ([memberId, accountId]) => ({
+    "Member id": memberId, // TODO: Link with avatar and handle?
+    "Account id": <ProposedAddress address={accountId}/>,
+  }),
+  "SetContentWorkingGroupMintCapacity" : ([capacity]) => ({
+    "Mint capacity": capacity + ' tJOY',
+  }),
+  "EvictStorageProvider" : ([accountId]) => ({
+    "Storage provider account": <ProposedAddress address={accountId}/>,
+  }),
+  "SetValidatorCount" : ([count]) => ({
+    "Validator count": count,
+  }),
+  "SetStorageRoleParameters" : ([params]) => ({
+    "Min. stake": params.min_stake + " tJOY",
+    "Min. actors": params.min_actors,
+    "Max. actors": params.max_actors,
+    "Reward": params.reward + " tJOY",
+    "Reward period": params.reward_period + " blocks",
+    "Bonding period": params.bonding_period + " blocks",
+    "Unbonding period": params.unbonding_period + " blocks",
+    "Min. service period": params.min_service_period + " blocks",
+    "Startup grace period": params.startup_grace_period + " blocks",
+    "Entry request fee": params.entry_request_fee + " tJOY",
+  }),
+};
+
+const ProposalParam = styled.div`
+  display: flex;
+  font-weight: bold;
+  margin-bottom: 0.5em;
+  @media only screen and (max-width: 767px) {
+    flex-direction: column;
+  }
+`;
+const ProposalParamName = styled.div`
+  min-width: ${ (p: { longestParamName:number }) =>
+    p.longestParamName > 20 ? '240px'
+    : (p.longestParamName > 15 ? '200px' : '160px') };
+`;
+const ProposalParamValue = styled.div`
+  color: #000;
+`;
+
+export default function Body({ type, title, description, params = [] }: BodyProps) {
+  const parseParams = paramParsers[type];
+  const parsedParams = parseParams(params);
+  const longestParamName: number = Object.keys(parsedParams).reduce((a, b) => b.length > a ? b.length : a, 0);
   return (
     <Card fluid>
       <Card.Content>
@@ -20,18 +107,12 @@ export default function Body({ title, description, params }: BodyProps) {
         <Card.Description>{description}</Card.Description>
         <Header as="h4">Parameters:</Header>
         <Item.Group textAlign="left" relaxed>
-          <Item>
-            <Item.Content>
-              <span className="text-grey">Amount of tokens: </span>
-              {`${params.tokensAmount} tJOY`}
-            </Item.Content>
-          </Item>
-          <Item>
-            <Item.Content>
-              <span className="text-grey">Destination account: </span>
-              {params.destinationAccount}
-            </Item.Content>
-          </Item>
+          { Object.entries(parseParams(params)).map(([paramName, paramValue]) => (
+            <ProposalParam key={paramName}>
+              <ProposalParamName longestParamName={longestParamName}>{paramName}:</ProposalParamName>
+              <ProposalParamValue>{paramValue}</ProposalParamValue>
+            </ProposalParam>
+          )) }
         </Item.Group>
       </Card.Content>
     </Card>

+ 0 - 1
packages/joy-proposals/src/Proposal/ChooseProposalType.tsx

@@ -3,7 +3,6 @@ import ProposalTypePreview, { ProposalTypeInfo } from "./ProposalTypePreview";
 import { Item, Dropdown } from "semantic-ui-react";
 
 import { useTransport } from "../runtime";
-import ProposalTypes from "../proposalTypes.json";
 import "./ChooseProposalType.css";
 
 export const Categories = {

+ 46 - 24
packages/joy-proposals/src/Proposal/Details.tsx

@@ -1,31 +1,43 @@
 import React from "react";
 import { Item, Image, Header } from "semantic-ui-react";
+import { ParsedProposal } from "../runtime/transport";
+import { IdentityIcon } from '@polkadot/react-components';
+import { BlockNumber } from '@polkadot/types/interfaces';
 
-import { ProposalType } from "./ProposalTypePreview";
-
-type Proposer = {
-  about: string;
-  handle: string;
-  avatar_uri: string;
+type DetailsProps = {
+  proposal: ParsedProposal,
+  bestNumber?: BlockNumber
 };
 
-export type DetailsProps = {
-  stage: string;
-  expiresIn: number;
-  type: ProposalType;
-  createdBy: Proposer;
-  createdAt: Date;
-};
+export default function Details({
+  proposal: { type, createdAt, createdAtBlock, proposer, status, parameters },
+  bestNumber
+}: DetailsProps) {
+  const statusStr = Object.keys(status)[0];
+  const isActive = statusStr === 'Active';
+  const { votingPeriod, gracePeriod } = parameters;
 
-export default function Details({ stage, createdAt, createdBy, type, expiresIn }: DetailsProps) {
+  const blockAge = bestNumber ? (bestNumber.toNumber() - createdAtBlock) : 0;
+  const substage = isActive && (
+    votingPeriod - blockAge  > 0 ?
+      'Voting period'
+      : 'Grace period'
+  );
+  const expiresIn = substage && (
+    substage === 'Voting period' ?
+      votingPeriod - blockAge
+      : (gracePeriod + votingPeriod) - blockAge
+  )
   return (
     <Item.Group className="details-container">
       <Item>
         <Item.Content>
           <Item.Extra>Proposed By:</Item.Extra>
-          <Image src={createdBy.avatar_uri} avatar floated="left" />
-          <Header as="h4">{createdBy.about}</Header>
-          <Item.Extra>{createdAt.toUTCString()}</Item.Extra>
+          { proposer.avatar_uri ?
+            <Image src={ proposer.avatar_uri } avatar floated="left" />
+            : <IdentityIcon className="image" value={proposer.root_account} size={40} /> }
+          <Header as="h4">{ proposer.handle }</Header>
+          <Item.Extra>{ createdAt.toLocaleString() }</Item.Extra>
         </Item.Content>
       </Item>
       <Item>
@@ -37,15 +49,25 @@ export default function Details({ stage, createdAt, createdBy, type, expiresIn }
       <Item>
         <Item.Content>
           <Item.Extra>Stage:</Item.Extra>
-          <Header as="h4">{stage}</Header>
-        </Item.Content>
-      </Item>
-      <Item>
-        <Item.Content>
-          <Item.Extra>Expires in:</Item.Extra>
-          <Header as="h4">{`${expiresIn.toLocaleString("en-US")} blocks`}</Header>
+          <Header as="h4">{ statusStr }</Header>
         </Item.Content>
       </Item>
+      { isActive && (
+        <Item>
+          <Item.Content>
+            <Item.Extra>Substage:</Item.Extra>
+            <Header as="h4">{ substage }</Header>
+          </Item.Content>
+        </Item>
+      ) }
+      { isActive && (
+        <Item>
+          <Item.Content>
+            <Item.Extra>Expires in:</Item.Extra>
+            <Header as="h4">{`${ expiresIn } blocks`}</Header>
+          </Item.Content>
+        </Item>
+      ) }
     </Item.Group>
   );
 }

+ 33 - 52
packages/joy-proposals/src/Proposal/ProposalDetails.tsx

@@ -1,69 +1,50 @@
 import React from "react";
 
 import { Container } from "semantic-ui-react";
-import Votes from "./Votes";
 import Details from "./Details";
 import Body from "./Body";
 import VotingSection from "./VotingSection";
+import { MyAccountProps, withMyAccount } from "@polkadot/joy-utils/MyAccount"
+import { ParsedProposal } from "../runtime/transport";
+import { withCalls } from '@polkadot/react-api';
+import { withMulti } from '@polkadot/react-api/with';
 
 import "./Proposal.css";
+import { ProposalId } from "@joystream/types/proposals";
+import { BlockNumber } from '@polkadot/types/interfaces'
+import { MemberId } from "@joystream/types/members";
+import { Seat } from "@joystream/types/";
 
-export type Member = {
-  name: string;
-  avatar: string;
-};
-
-export type VoteValue = "Approve" | "Slash" | "Abstain" | "Reject";
 
-export type Vote = {
-  value: VoteValue;
-  by: Member;
-  createdAt: string;
+type ProposalDetailsProps = MyAccountProps & {
+  proposal: ParsedProposal,
+  proposalId: ProposalId,
+  bestNumber?: BlockNumber,
+  council?: Seat[]
 };
 
-export type ProposalProps = {
-  title: string;
-  description: string;
-  finalized: "approved" | "rejected" | "slashed" | "withdrawn";
-  params: {
-    tokensAmount: number;
-    destinationAccount: string;
-  };
-  votes: Vote[];
-  totalVotes: number;
-  details: {
-    // FIXME: Stage, substage and type all should be an enum
-    stage: string;
-    substage: string;
-    expiresIn: number;
-    type: string;
-    createdBy: Member;
-    createdAt: string;
-  };
-  onVote: (vote: VoteValue) => void;
-  vote: {
-    hasVoted: boolean;
-    value: VoteValue;
-  };
-};
-
-export default function ProposalDetails({
-  title,
-  description,
-  params,
-  details,
-  votes,
-  totalVotes,
-  onVote,
-  vote
-}: ProposalProps) {
-  const { hasVoted = false, value = undefined } = vote || {};
+function ProposalDetails({ proposal, proposalId, myAddress, myMemberId, iAmMember, council, bestNumber }: ProposalDetailsProps) {
+  const iAmCouncilMember = iAmMember && council && council.some(seat => seat.member.toString() === myAddress);
   return (
     <Container className="Proposal">
-      <Details {...details} />
-      <Body title={title} description={description} params={params} />
-      <VotingSection onVote={onVote} hasVoted={hasVoted} value={value || "Approve"} />
-      <Votes votes={votes} total={totalVotes} />
+      <Details proposal={proposal} bestNumber={bestNumber}/>
+      <Body
+        type={ proposal.type }
+        title={ proposal.title }
+        description={ proposal.description }
+        params={ proposal.details }
+        />
+      { iAmCouncilMember && <VotingSection proposalId={proposalId} memberId={ myMemberId as MemberId }/> }
+      {/* <Votes votes={votes} total={totalVotes} />  TODO: Implement */}
     </Container>
   );
 }
+
+export default withMulti<ProposalDetailsProps>(
+  ProposalDetails,
+  withMyAccount,
+  withCalls(
+    ['derive.chain.bestNumber', { propName: 'bestNumber' }],
+    ['query.council.activeCouncil', { propName: 'council' }], // TODO: Handle via transport?
+  )
+);

+ 3 - 5
packages/joy-proposals/src/Proposal/ProposalFromId.tsx

@@ -6,7 +6,7 @@ import { useTransport, ParsedProposal } from "../runtime";
 import { usePromise } from "../utils";
 import Error from "./Error";
 import Loading from "./Loading";
-import NotDone from "../NotDone";
+import { Proposal } from "@joystream/types/proposals";
 
 export default function ProposalFromId(props: RouteComponentProps<any>) {
   const {
@@ -17,7 +17,7 @@ export default function ProposalFromId(props: RouteComponentProps<any>) {
   const transport = useTransport();
 
   const [proposal, loading, error] = usePromise<any>(transport.proposalById(id), {});
-  const [votes, loadVotes, errorVote] = usePromise<any>(transport.votes(id), []);
+  //const [votes, loadVotes, errorVote] = usePromise<any>(transport.votes(id), []);
 
   if (loading && !error) {
     return <Loading text="Fetching Proposal..." />;
@@ -27,8 +27,6 @@ export default function ProposalFromId(props: RouteComponentProps<any>) {
   console.log(`With ${id} we fetched proposal...`);
   console.log(proposal);
 
-  console.log("With votes...");
-  console.log(votes);
 
-  return <NotDone {...props} />;
+  return <ProposalDetails proposal={ proposal as Proposal } proposalId={id}/>;
 }

+ 11 - 10
packages/joy-proposals/src/Proposal/ProposalPreview.tsx

@@ -1,24 +1,25 @@
 import React from "react";
 import { Header, Card } from "semantic-ui-react";
-
-import { ProposalProps } from "./ProposalDetails";
-import Details, { DetailsProps } from "./Details";
+import Details from "./Details";
+import { ParsedProposal } from "../runtime/transport"
 
 import "./Proposal.css";
 
 type ProposalPreviewProps = {
-  title: string;
-  description: string;
+  proposal: ParsedProposal
 };
-export default function ProposalPreview({ title, description, ...details }: ProposalPreviewProps & DetailsProps) {
+export default function ProposalPreview({ proposal }: ProposalPreviewProps) {
   return (
-    <Card fluid className="Proposal">
+    <Card
+      fluid
+      className="Proposal"
+      href={`#/proposals/${proposal.id}`}>
       <Card.Content>
         <Card.Header>
-          <Header as="h1">{title}</Header>
+          <Header as="h1">{proposal.title}</Header>
         </Card.Header>
-        <Card.Description>{description}</Card.Description>
-        <Details {...details} />
+        <Card.Description>{proposal.description}</Card.Description>
+        <Details proposal={proposal} />
       </Card.Content>
     </Card>
   );

+ 2 - 8
packages/joy-proposals/src/Proposal/ProposalPreviewList.tsx

@@ -36,7 +36,7 @@ type ProposalFilter = "all" | "active" | "withdrawn" | "approved" | "rejected" |
 export default function ProposalPreviewList() {
   const transport = useTransport();
 
-  const [proposals, error, loading] = usePromise<ParsedProposal[]>(transport.councilMembers(), []);
+  const [proposals, error, loading] = usePromise<ParsedProposal[]>(transport.proposals(), []);
 
   if (loading && !error) {
     return <Loading text="Fetching proposals..." />;
@@ -84,13 +84,7 @@ export default function ProposalPreviewList() {
         {proposals.map((prop: ParsedProposal, idx: number) => (
           <ProposalPreview
             key={`${prop.title}-${idx}`}
-            title={prop.title}
-            description={prop.description}
-            stage={"Active"}
-            createdAt={prop.createdAt}
-            createdBy={prop.proposer}
-            type={prop.type}
-            expiresIn={prop.parameters.votingPeriod - prop.createdAtBlock}
+            proposal={prop}
           />
         ))}
       </Card.Group>

+ 14 - 10
packages/joy-proposals/src/Proposal/ProposalTypePreview.tsx

@@ -4,16 +4,20 @@ import { Category } from "./ChooseProposalType";
 
 import "./ProposalType.css";
 
-export type ProposalType =
-  | "EvictStorageProvider"
-  | "Signal"
-  | "SetStorageRoleParams"
-  | "SetMaxValidatorCount"
-  | "SetElectionParameters"
-  | "SpendingProposal"
-  | "SetWGMintCapacity"
-  | "SetLead"
-  | "RuntimeUpgrade";
+const ProposalTypes = [
+  "Text",
+  "RuntimeUpgrade",
+  "SetElectionParameters",
+  "Spending",
+  "SetLead",
+  "SetContentWorkingGroupMintCapacity",
+  "EvictStorageProvider",
+  "SetValidatorCount",
+  "SetStorageRoleParameters",
+] as const;
+
+export type ProposalType = typeof ProposalTypes[number];
+
 
 export type ProposalTypeInfo = {
   type: ProposalType;

+ 72 - 26
packages/joy-proposals/src/Proposal/VotingSection.tsx

@@ -1,25 +1,79 @@
-import React from "react";
+import React, { useState } from "react";
 
 import { Icon, Button, Message, Divider, Header } from "semantic-ui-react";
-
-import { VoteValue } from "./ProposalDetails";
 import useVoteStyles from "./useVoteStyles";
+import TxButton from "@polkadot/joy-utils/TxButton";
+import { MemberId } from "@joystream/types/members";
+import { ProposalId } from "@joystream/types/proposals";
+import { useTransport } from "../runtime";
+import { VoteKind } from '@joystream/types/proposals';
+import { usePromise } from "../utils";
+
+// TODO: joy-types (there is something similar already I think)
+const voteKinds = ["Approve", "Slash", "Abstain", "Reject"] as const;
+type VoteKindStr = "Approve" | "Slash" | "Abstain" | "Reject";
+
+type VoteButtonProps = {
+  memberId: MemberId,
+  voteKind: VoteKindStr,
+  proposalId: ProposalId,
+  onSuccess: () => void
+}
+function VoteButton({ voteKind, proposalId, memberId, onSuccess }: VoteButtonProps) {
+  const { icon, color } = useVoteStyles(voteKind);
+  return (
+    // Button.Group "cheat" to force TxButton color
+    <Button.Group color={color} style={{ marginRight: '5px' }}>
+      <TxButton
+        // isDisabled={ isSubmitting }
+        params={[
+          memberId,
+          proposalId,
+          voteKind
+        ]}
+        tx={ `proposalsEngine.vote` }
+        onClick={ sendTx => sendTx() }
+        txFailedCb={ () => null }
+        txSuccessCb={ onSuccess }
+        className={`icon left labeled`}>
+        <Icon name={icon} inverted />
+        { voteKind }
+      </TxButton>
+    </Button.Group>
+  )
+}
 
 type VotingSectionProps = {
-  onVote: (vote: VoteValue) => void;
-  hasVoted: boolean;
-  value: VoteValue;
+  memberId: MemberId,
+  proposalId: ProposalId,
 };
 
-export default function VotingSection({ onVote, hasVoted, value }: VotingSectionProps) {
-  if (hasVoted) {
-    const { icon, color } = useVoteStyles(value);
+export default function VotingSection({
+  memberId,
+  proposalId,
+}: VotingSectionProps) {
+  const transport = useTransport();
+  const [voted, setVoted] = useState<VoteKindStr | null >(null);
+  const [vote] = usePromise<VoteKind | null | undefined>(
+    transport.voteByProposalAndMember(proposalId, memberId),
+    undefined
+  );
+
+  if (vote === undefined) {
+    // Loading / error
+    return null;
+  }
+
+  const voteStr: VoteKindStr | null = voted ? voted : (vote && vote.type.toString() as VoteKindStr);
+
+  if (voteStr) {
+    const { icon, color } = useVoteStyles(voteStr);
 
     return (
       <Message icon color={color}>
         <Icon name={icon} />
         <Message.Content>
-          You voted <span className="bold">{`"${value}"`}</span>
+          You voted <span className="bold">{`"${voteStr}"`}</span>
         </Message.Content>
       </Message>
     );
@@ -29,22 +83,14 @@ export default function VotingSection({ onVote, hasVoted, value }: VotingSection
     <>
       <Header as="h3">Sumbit your vote</Header>
       <Divider />
-      <Button color="green" icon labelPosition="left" onPress={() => onVote("Approve")}>
-        <Icon name="smile" inverted />
-        Approve
-      </Button>
-      <Button color="grey" icon labelPosition="left" onPress={() => onVote("Abstain")}>
-        <Icon name="meh" inverted />
-        Abstain
-      </Button>
-      <Button color="orange" icon labelPosition="left" onPress={() => onVote("Reject")}>
-        <Icon name="frown" inverted />
-        Reject
-      </Button>
-      <Button color="red" icon labelPosition="left" onPress={() => onVote("Slash")}>
-        <Icon name="times" inverted />
-        Slash
-      </Button>
+      { voteKinds.map((voteKind) =>
+        <VoteButton
+          voteKind={voteKind}
+          memberId={memberId}
+          proposalId={proposalId}
+          key={voteKind}
+          onSuccess={ () => setVoted(voteKind) }/>
+      ) }
     </>
   );
 }

+ 1 - 0
packages/joy-proposals/src/index.tsx

@@ -57,6 +57,7 @@ function App(props: Props): React.ReactElement<Props> {
           <Route path={`${basePath}/new/set-storage-role-params`} component={SetStorageRoleParamsForm} />
           <Route path={`${basePath}/new/set-max-validator-count`} component={SetMaxValidatorCountForm} />
           <Route path={`${basePath}/new/runtime-upgrade`} component={RuntimeUpgradeForm} />
+          <Route path={`${basePath}/new`} component={ChooseProposalType} />
           <Route path={`${basePath}/active`} component={NotDone} />
           <Route path={`${basePath}/finalized`} component={NotDone} />
           <Route path={`${basePath}/:id`} component={ProposalFromId} />

+ 8 - 8
packages/joy-proposals/src/runtime/transport.substrate.ts

@@ -58,7 +58,7 @@ export class SubstrateTransport extends Transport {
   async proposalById(id: ProposalId): Promise<ParsedProposal> {
     const rawDetails = (await this.proposalDetailsById(id)).toJSON() as { [k: string]: any };
     const type = Object.keys(rawDetails)[0] as ProposalType;
-    const details = rawDetails[type];
+    const details = Array.isArray(rawDetails[type]) ? rawDetails[type] : [rawDetails[type]];
     const rawProposal = await this.rawProposalById(id);
     const proposer = (await this.memberProfile(rawProposal.proposerId)).toJSON();
     const proposal = rawProposal.toJSON() as {
@@ -71,6 +71,7 @@ export class SubstrateTransport extends Transport {
     const createdAtBlock = rawProposal.createdAt;
 
     return {
+      id,
       ...proposal,
       details,
       type,
@@ -82,7 +83,7 @@ export class SubstrateTransport extends Transport {
 
   async proposalsIds() {
     const total: number = (await this.proposalCount()).toBn().toNumber();
-    return Array.from({ length: total + 1 }, (_, i) => new ProposalId(i));
+    return Array.from({ length: total }, (_, i) => new ProposalId(i+1));
   }
 
   async proposals() {
@@ -90,11 +91,6 @@ export class SubstrateTransport extends Transport {
     return Promise.all(ids.map(id => this.proposalById(id)));
   }
 
-  async hasVotedOnProposal(proposalId: ProposalId, voterId: MemberId) {
-    const hasVoted = await this.proposalsEngine.voteExistsByProposalByVoter<bool>(proposalId, voterId);
-    return hasVoted.eq(true);
-  }
-
   async activeProposals() {
     const activeProposalIds = await this.proposalsEngine.activeProposalIds<ProposalId[]>();
 
@@ -124,7 +120,7 @@ export class SubstrateTransport extends Transport {
     );
   }
 
-  async voteByProposalAndMember(proposalId: ProposalId, voterId: MemberId) {
+  async voteByProposalAndMember(proposalId: ProposalId, voterId: MemberId): Promise<VoteKind | null> {
     const vote = await this.proposalsEngine.voteExistsByProposalByVoter<VoteKind>(proposalId, voterId);
     const hasVoted = (await this.proposalsEngine.voteExistsByProposalByVoter.size(proposalId, voterId)).toNumber();
     return hasVoted ? vote : null;
@@ -154,4 +150,8 @@ export class SubstrateTransport extends Transport {
     // methods = [proposalTypeVotingPeriod...]
     return methods.reduce((obj, method) => ({ ...obj, method: this.proposalsCodex[method]() }), {});
   }
+
+  async bestBlock() {
+    return await this.api.derive.chain.bestNumber();
+  }
 }

+ 4 - 1
packages/joy-proposals/src/runtime/transport.ts

@@ -1,11 +1,14 @@
 import { ProposalType } from "../Proposal/ProposalTypePreview";
+import { Profile } from "@joystream/types/members";
+import { ProposalParametersType, ProposalId } from "@joystream/types/proposals";
 
 export type ParsedProposal = {
+  id: ProposalId;
   type: ProposalType;
   title: string;
   description: string;
   status: any;
-  proposer: any;
+  proposer: Profile;
   proposerId: number;
   createdAtBlock: number;
   createdAt: Date;

+ 36 - 0
packages/joy-types/src/proposals.ts

@@ -80,6 +80,42 @@ class ProposalParameters extends Struct {
       },
       value
     );
+  }
+
+        // During this period, votes can be accepted
+  get votingPeriod(): BlockNumber {
+    return this.get("votingPeriod") as BlockNumber;
+  }
+
+        /* A pause before execution of the approved proposal. Zero means approved proposal would be
+     executed immediately. */
+  get gracePeriod(): BlockNumber {
+    return this.get("gracePeriod") as BlockNumber;
+  }
+
+        // Quorum percentage of approving voters required to pass the proposal.
+  get approvalQuorumPercentage(): u32 {
+    return this.get("approvalQuorumPercentage") as u32;
+  }
+
+        // Approval votes percentage threshold to pass the proposal.
+  get approvalThresholdPercentage(): u32 {
+    return this.get("approvalThresholdPercentage") as u32;
+  }
+
+        // Quorum percentage of voters required to slash the proposal.
+  get slashingQuorumPercentage(): u32 {
+    return this.get("slashingQuorumPercentage") as u32;
+  }
+
+        // Slashing votes percentage threshold to slash the proposal.
+  get slashingThresholdPercentage(): u32 {
+    return this.get("slashingThresholdPercentage") as u32;
+  }
+
+        // Proposal stake
+  get requiredStake(): Option<Balance> {
+    return this.get("requiredStake") as Option<Balance>;
   }
 }
 

+ 1 - 1
packages/joy-utils/src/TxButton.tsx

@@ -130,7 +130,7 @@ function MockTxButton (props: BasicButtonProps) {
 function ResolvedButton (props: BasicButtonProps) {
   const isMock = useTransportContext() instanceof MockTransport;
 
-  return isMock 
+  return isMock
     ? <MockTxButton {...props} />
     : <SubstrateTxButton {...props} />
 }