Browse Source

Merge branch 'francesco-add-context' into transport-integration

Leszek Wiesner 4 years ago
parent
commit
8cf206332a

+ 1 - 1
packages/joy-proposals/src/Proposal/Body.tsx

@@ -1,6 +1,6 @@
 import React from "react";
 import { Card, Header, Item } from "semantic-ui-react";
-import { ProposalType } from "./ProposalTypePreview";
+import { ProposalType } from "../runtime/transport";
 import { blake2AsHex } from '@polkadot/util-crypto';
 import styled from 'styled-components';
 import AddressMini from '@polkadot/react-components/AddressMiniJoy';

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

@@ -3,7 +3,11 @@ import ProposalTypePreview, { ProposalTypeInfo } from "./ProposalTypePreview";
 import { Item, Dropdown } from "semantic-ui-react";
 
 import { useTransport } from "../runtime";
+import { usePromise } from "../utils";
+import Error from "./Error";
+import Loading from "./Loading";
 import "./ChooseProposalType.css";
+import { RouteComponentProps } from "react-router-dom";
 
 export const Categories = {
   storage: "Storage",
@@ -15,19 +19,19 @@ export const Categories = {
 
 export type Category = typeof Categories[keyof typeof Categories];
 
-type ChooseProposalTypeProps = {
-  proposalTypes: ProposalTypeInfo[];
-};
-
-// Make this without Props.
-export default function ProposalPreview(props: ChooseProposalTypeProps) {
-  const { proposalTypes } = props;
+export default function ChooseProposalType(props: RouteComponentProps) {
   const transport = useTransport();
 
-  // We need to get: Stake, Cancellation fee, Grace Period.
-  // Fetch them here...
-
+  const [proposalTypes, error, loading] = usePromise(() => transport.proposalsTypesParameters(), []);
   const [category, setCategory] = useState("");
+
+  if (loading && !error) {
+    return <Loading text="Fetching proposals..." />;
+  } else if (error || proposalTypes == null) {
+    return <Error error={error} />;
+  }
+
+  console.log({ proposalTypes, loading, error });
   return (
     <div className="ChooseProposalType">
       <div className="filters">

+ 1 - 1
packages/joy-proposals/src/Proposal/Error.tsx

@@ -10,7 +10,7 @@ export default function Error({ error }: ErrorProps) {
     <Container style={{ display: "flex", justifyContent: "center", alignItems: "center" }}>
       <Message negative>
         <Message.Header>Oops! We got an error!</Message.Header>
-        <p>{error}</p>
+        <p>{error.message}</p>
       </Message>
     </Container>
   );

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

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

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

@@ -42,14 +42,15 @@ type ProposalPreviewListProps = {
 function ProposalPreviewList({ bestNumber }: ProposalPreviewListProps) {
   const transport = useTransport();
 
-  const [proposals, error, loading] = usePromise<ParsedProposal[]>(transport.proposals(), []);
+  const [proposals, error, loading] = usePromise<ParsedProposal[]>(() => transport.proposals(), []);
 
   if (loading && !error) {
     return <Loading text="Fetching proposals..." />;
   } else if (error) {
     return <Error error={error} />;
   }
-  console.log(proposals);
+
+  console.log({ proposals, error, loading });
 
   return (
     <Container className="Proposal">

+ 12 - 18
packages/joy-proposals/src/Proposal/ProposalTypePreview.tsx

@@ -1,31 +1,20 @@
 import React from "react";
+
 import { Item, Icon, Button } from "semantic-ui-react";
+
 import { Category } from "./ChooseProposalType";
+import { ProposalType } from "../runtime";
+import { slugify } from "../utils";
 
 import "./ProposalType.css";
 
-const ProposalTypes = [
-  "Text",
-  "RuntimeUpgrade",
-  "SetElectionParameters",
-  "Spending",
-  "SetLead",
-  "SetContentWorkingGroupMintCapacity",
-  "EvictStorageProvider",
-  "SetValidatorCount",
-  "SetStorageRoleParameters",
-] as const;
-
-export type ProposalType = typeof ProposalTypes[number];
-
-
 export type ProposalTypeInfo = {
   type: ProposalType;
   category: Category;
   image: string;
   description: string;
   stake: number;
-  cancellationFee: number;
+  cancellationFee?: number;
   gracePeriod: number;
 };
 
@@ -37,6 +26,11 @@ export default function ProposalTypePreview(props: ProposalTypePreviewProps) {
   const {
     typeInfo: { type, image, description, stake, cancellationFee, gracePeriod }
   } = props;
+
+  const handleClick = () => {
+    console.log(`Clicked, should go to ${slugify(type)}`);
+  };
+
   return (
     <Item className="ProposalType">
       <Item.Image size="tiny" src={image} />
@@ -55,13 +49,13 @@ export default function ProposalTypePreview(props: ProposalTypePreviewProps) {
           <div className="proposal-detail">
             <div className="detail-title">Grace period:</div>
             <div className="detail-value">
-              {gracePeriod ? `${gracePeriod} day${gracePeriod > 1 ? "s" : ""}` : "NONE"}
+              {gracePeriod ? `${gracePeriod} block${gracePeriod > 1 ? "s" : ""}` : "NONE"}
             </div>
           </div>
         </div>
       </Item.Content>
       <div className="actions">
-        <Button primary className="btn-create" size="medium">
+        <Button primary className="btn-create" size="medium" onClick={handleClick}>
           Create
           <Icon name="chevron right" />
         </Button>

+ 1 - 1
packages/joy-proposals/src/Proposal/VotingSection.tsx

@@ -57,7 +57,7 @@ export default function VotingSection({
   const transport = useTransport();
   const [voted, setVoted] = useState<VoteKindStr | null >(null);
   const [vote] = usePromise<VoteKind | null | undefined>(
-    transport.voteByProposalAndMember(proposalId, memberId),
+    () => transport.voteByProposalAndMember(proposalId, memberId),
     undefined
   );
 

+ 1 - 1
packages/joy-proposals/src/forms/FormContainer.tsx

@@ -1,7 +1,7 @@
 import React from "react";
 import { withFormik } from "formik";
 
-export function withFormContainer<MyFormProps, FormValues>(formikProps) {
+export function withFormContainer<MyFormProps, FormValues>(formikProps: any) {
   return function(InnerForm: React.ComponentType<any>) {
     return withFormik<MyFormProps, FormValues>(formikProps)(function(props) {
       const handleBlur = (e: React.FocusEvent<HTMLInputElement>, data: any): void => {

+ 1 - 1
packages/joy-proposals/src/runtime/index.ts

@@ -1,4 +1,4 @@
-export { ParsedProposal } from "./transport";
+export { ParsedProposal, ProposalType } from "./transport";
 export { SubstrateTransport } from "./transport.substrate";
 export { MockTransport } from "./transport.mock";
 export { SubstrateProvider, useTransport } from "./TransportContext";

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

@@ -1,12 +1,12 @@
-import { Transport, ParsedProposal } from "./transport";
+import { Transport, ParsedProposal, ProposalType, ProposalTypes } from "./transport";
 import { Proposal, ProposalId, Seats, VoteKind } from "@joystream/types/proposals";
 import { MemberId } from "@joystream/types/members";
 import { ApiProps } from "@polkadot/react-api/types";
 import { u32, bool, Vec } from "@polkadot/types/";
+import { Balance } from "@polkadot/types/interfaces";
 import { ApiPromise } from "@polkadot/api";
 
-import { includeKeys, dateFromBlock } from "../utils";
-import { ProposalType } from "../Proposal/ProposalTypePreview";
+import { includeKeys, dateFromBlock, calculateStake, calculateMetaFromType } from "../utils";
 
 export class SubstrateTransport extends Transport {
   protected api: ApiPromise;
@@ -39,6 +39,10 @@ export class SubstrateTransport extends Transport {
     return this.api.query.council;
   }
 
+  async totalIssuance() {
+    return this.api.query.balances.totalIssuance<Balance>();
+  }
+
   async proposalCount() {
     return this.proposalsEngine.proposalCount<u32>();
   }
@@ -139,16 +143,57 @@ export class SubstrateTransport extends Transport {
     );
   }
 
-  async proposalTypesGracePeriod() {
+  async proposalTypesGracePeriod(): Promise<{ [k in ProposalType]: number }> {
     const methods = includeKeys(this.proposalsCodex, "GracePeriod");
-    // methods = [proposalTypeGracePeriod...]
-    return methods.reduce((obj, method) => ({ ...obj, method: this.proposalsCodex[method]() }), {});
+    // methods = [proposalTypeVotingPeriod...]
+    return methods.reduce(async (prevProm, method) => {
+      const obj = await prevProm;
+      const period = (await this.proposalsCodex[method]()) as u32;
+      // setValidatorCountProposalVotingPeriod to SetValidatorCount
+      const key = method
+        .split(/(?=[A-Z])/)
+        .slice(0, -3)
+        .map((w, i) => (i === 0 ? w.slice(0, 1).toUpperCase() + w.slice(1) : w))
+        .join("") as ProposalType;
+
+      return { ...obj, [`${key}`]: period.toNumber() };
+    }, Promise.resolve({}) as Promise<{ [k in ProposalType]: number }>);
   }
 
-  async proposalTypesVotingPeriod() {
+  async proposalTypesVotingPeriod(): Promise<{ [k in ProposalType]: number }> {
     const methods = includeKeys(this.proposalsCodex, "VotingPeriod");
     // methods = [proposalTypeVotingPeriod...]
-    return methods.reduce((obj, method) => ({ ...obj, method: this.proposalsCodex[method]() }), {});
+    return methods.reduce(async (prevProm, method) => {
+      const obj = await prevProm;
+      const period = (await this.proposalsCodex[method]()) as u32;
+      // setValidatorCountProposalVotingPeriod to SetValidatorCount
+      const key = method
+        .split(/(?=[A-Z])/)
+        .slice(0, -3)
+        .map((w, i) => (i === 0 ? w.slice(0, 1).toUpperCase() + w.slice(1) : w))
+        .join("") as ProposalType;
+
+      return { ...obj, [`${key}`]: period.toNumber() };
+    }, Promise.resolve({}) as Promise<{ [k in ProposalType]: number }>);
+  }
+
+  async parametersFromProposalType(type: ProposalType) {
+    const votingPeriod = (await this.proposalTypesVotingPeriod())[type];
+    const gracePeriod = (await this.proposalTypesGracePeriod())[type];
+    const issuance = (await this.totalIssuance()).toNumber();
+    const stake = calculateStake(type, issuance);
+    const meta = calculateMetaFromType(type);
+    return {
+      type,
+      votingPeriod,
+      gracePeriod,
+      stake,
+      ...meta
+    };
+  }
+
+  async proposalsTypesParameters() {
+    return Promise.all(ProposalTypes.map(type => this.parametersFromProposalType(type)));
   }
 
   async bestBlock() {

+ 15 - 2
packages/joy-proposals/src/runtime/transport.ts

@@ -1,6 +1,19 @@
-import { ProposalType } from "../Proposal/ProposalTypePreview";
 import { Profile } from "@joystream/types/members";
-import { ProposalParametersType, ProposalId } from "@joystream/types/proposals";
+import { ProposalId } from "@joystream/types/proposals";
+// FIXME: Those don't have the same names as in the runtime
+export const ProposalTypes = [
+  "Text",
+  "RuntimeUpgrade",
+  "SetElectionParameters",
+  "Spending",
+  "SetLead",
+  "SetContentWorkingGroupMintCapacity",
+  "EvictStorageProvider",
+  "SetValidatorCount",
+  "SetStorageRoleParameters"
+] as const;
+
+export type ProposalType = typeof ProposalTypes[number];
 
 export type ParsedProposal = {
   id: ProposalId;

+ 11 - 51
packages/joy-proposals/src/stories/data/ProposalDetails.mock.ts

@@ -1,59 +1,19 @@
-import { ProposalProps } from "../../Proposal/ProposalDetails";
+import { ParsedProposal } from "@polkadot/joy-proposals/runtime";
 
-const mockedProposal: Partial<ProposalProps> = {
+const mockedProposal: Partial<ParsedProposal> = {
   title: "Send me some tokens for coffee",
   description:
     "Change the total reward across all validators in a given block. This is not the direct reward, but base reward for Pallet staking module. The minimum value must be greater than 450 tJOY based on current runtime. Also, coffee is getting expensive.",
-  params: {
-    tokensAmount: 123.45,
-    destinationAccount: "0x4977CA8ADB17758aD2eac7220CE0C21D46421BB7"
+  proposer: {
+    name: "Satoshi",
+    avatar_uri: "https://react.semantic-ui.com/images/avatar/large/steve.jpg"
   },
-  details: {
-    createdBy: {
-      name: "Satoshi",
-      avatar: "https://react.semantic-ui.com/images/avatar/large/steve.jpg"
-    },
-    stage: "Active",
-    createdAt: "Mar 25, 2020 at 14:20",
-    type: "Spending Proposal",
-    substage: "Grace period",
-    expiresIn: 5678
-  },
-  votes: [
-    {
-      value: "Approve",
-      by: {
-        name: "Alice Ellison",
-        avatar: "https://react.semantic-ui.com/images/avatar/large/jenny.jpg"
-      },
-      createdAt: "Mar 25, 2020 at 14:20"
-    },
-    {
-      value: "Abstain",
-      by: {
-        name: "Bob Bobston",
-        avatar: "https://react.semantic-ui.com/images/avatar/large/daniel.jpg"
-      },
-      createdAt: "Mar 24, 2020 at 12:11"
-    },
-    {
-      value: "Reject",
-      by: {
-        name: "Charlie Chen",
-        avatar: "https://react.semantic-ui.com/images/avatar/large/matthew.png"
-      },
-      createdAt: "Mar 23, 2020 at 11:34"
-    },
-    {
-      value: "Slash",
-      by: {
-        name: "David Douglas",
-        avatar: "https://react.semantic-ui.com/images/avatar/large/elliot.jpg"
-      },
-      createdAt: "Mar 21, 2020 at 9:54"
-    }
-  ],
-  totalVotes: 12
+  type: "SpendingProposal",
+  createdAtBlock: 6554,
+  details: ["1200tJOY", "5hbcehbehbehifbrjjwodk"],
+
+  status: "Active",
+  createdAt: new Date("Mar 25, 2020 at 14:20")
 };
 
 export default mockedProposal;

+ 86 - 20
packages/joy-proposals/src/utils.ts

@@ -1,8 +1,9 @@
-import { useState, useEffect } from "react";
-
-import { ProposalType } from "./Proposal/ProposalTypePreview";
+import { useState, useEffect, useCallback } from "react";
 import { BlockNumber } from "@polkadot/types/interfaces";
 
+import { ProposalType } from "./runtime";
+import { Category } from "./Proposal/ChooseProposalType";
+
 export function includeKeys<T extends { [k: string]: any }>(obj: T, ...allowedKeys: string[]) {
   return Object.keys(obj).filter(objKey => {
     return allowedKeys.reduce(
@@ -12,6 +13,14 @@ export function includeKeys<T extends { [k: string]: any }>(obj: T, ...allowedKe
   });
 }
 
+export function slugify(str: string) {
+  return str
+    .split(/(?=[A-Z])/) // Splits on UpperCase
+    .map(w => w.toLowerCase())
+    .join("-")
+    .trim();
+}
+
 export function objFromMap(map: Map<string, any>): { [k: string]: any } {
   return Object.fromEntries(
     Array.from(map.entries(), ([key, value]) => (value instanceof Map ? [key, objFromMap(value)] : [key, value]))
@@ -23,21 +32,22 @@ export function dateFromBlock(blockNumber: BlockNumber) {
   return new Date(Date.now() - 6000 * _blockNumber);
 }
 
-export function usePromise<T>(promiseOrFunction: (() => Promise<T>) | Promise<T>, defaultValue: T): [T, any, boolean] {
+export function usePromise<T>(promise: () => Promise<T>, defaultValue: T): [T, any, boolean] {
   const [state, setState] = useState<{
     value: T;
-    error: null | any;
+    error: any;
     isPending: boolean;
   }>({ value: defaultValue, error: null, isPending: true });
 
-  useEffect(() => {
-    const promise = typeof promiseOrFunction === "function" ? promiseOrFunction() : promiseOrFunction;
-
-    let isSubscribed = true;
-    promise
+  let isSubscribed = true;
+  const execute = useCallback(() => {
+    return promise()
       .then(value => (isSubscribed ? setState({ value, error: null, isPending: false }) : null))
       .catch(error => (isSubscribed ? setState({ value: defaultValue, error: error, isPending: false }) : null));
+  }, [promise]);
 
+  useEffect(() => {
+    execute();
     return () => {
       isSubscribed = false;
     };
@@ -48,33 +58,89 @@ export function usePromise<T>(promiseOrFunction: (() => Promise<T>) | Promise<T>
 }
 
 export function calculateStake(type: ProposalType, issuance: number) {
-  const basis = issuance / 100;
   let stake = NaN;
   switch (type) {
     case "EvictStorageProvider": {
-      stake = basis * 0.1;
+      stake = Math.round(issuance * (0.1 / 100));
       break;
     }
-    case "Signal":
+    case "Text":
     case "SetStorageRoleParams":
-    case "SetMaxValidatorCount":
+    case "SetValidatorCount":
     case "SetLead":
-    case "SetWGMintCapacity":
-    case "SpendingProposal": {
-      stake = basis * 0.25;
+    case "SetContentWorkingGroupMintCapacity":
+    case "Spending": {
+      stake = Math.round(issuance * (0.25 / 100));
       break;
     }
     case "SetElectionParameters": {
-      stake = basis * 0.75;
+      stake = Math.round(issuance * (0.75 / 100));
       break;
     }
     case "RuntimeUpgrade": {
-      stake = basis * 1;
+      stake = Math.round(issuance * (1 / 100));
       break;
     }
     default: {
-      throw new Error("'Proposal Type is invalid. Can't calculate issuance.");
+      throw new Error(`Proposal Type is invalid. Got ${type}. Can't calculate issuance.`);
     }
   }
   return stake;
 }
+
+export function calculateMetaFromType(type: ProposalType) {
+  let description = "";
+  const image = "";
+  let category: Category = "Other";
+  switch (type) {
+    case "EvictStorageProvider": {
+      description = "Evicting Storage Provider Proposal";
+      category = "Storage";
+      break;
+    }
+    case "Text": {
+      description = "Signal Proposal";
+      category = "Other";
+      break;
+    }
+    case "SetStorageRoleParams": {
+      description = "Set Storage Role Params Proposal";
+      category = "Storage";
+      break;
+    }
+    case "SetValidatorCount": {
+      description = "Set Max Validator Count Proposal";
+      category = "Validators";
+      break;
+    }
+    case "SetLead": {
+      description = "Set Lead Proposal";
+      category = "Content Working Group";
+      break;
+    }
+    case "SetContentWorkingGroupMintCapacity": {
+      description = "Set WG Mint Capacity Proposal";
+      category = "Content Working Group";
+      break;
+    }
+    case "Spending": {
+      description = "Spending Proposal";
+      category = "Other";
+      break;
+    }
+    case "SetElectionParameters": {
+      description = "Set Election Parameters Proposal";
+      category = "Council";
+      break;
+    }
+    case "RuntimeUpgrade": {
+      description = "Runtime Upgrade Proposal";
+      category = "Other";
+      break;
+    }
+    default: {
+      throw new Error("'Proposal Type is invalid. Can't calculate metadata.");
+    }
+  }
+  return { description, image, category };
+}