Browse Source

Merge pull request #494 from Lezek123/joy-utils-rework

Joy-proposals - extract potentially reusable code into joy-utils
Mokhtar Naamani 4 years ago
parent
commit
af88cd20a5
53 changed files with 811 additions and 894 deletions
  1. 4 5
      pioneer/packages/joy-proposals/src/Proposal/Body.tsx
  2. 22 28
      pioneer/packages/joy-proposals/src/Proposal/ChooseProposalType.tsx
  3. 1 1
      pioneer/packages/joy-proposals/src/Proposal/Details.tsx
  4. 0 17
      pioneer/packages/joy-proposals/src/Proposal/Error.tsx
  5. 0 14
      pioneer/packages/joy-proposals/src/Proposal/Loading.tsx
  6. 0 20
      pioneer/packages/joy-proposals/src/Proposal/PromiseComponent.tsx
  7. 2 2
      pioneer/packages/joy-proposals/src/Proposal/ProposalDetails.tsx
  8. 10 10
      pioneer/packages/joy-proposals/src/Proposal/ProposalFromId.tsx
  9. 1 1
      pioneer/packages/joy-proposals/src/Proposal/ProposalPreview.tsx
  10. 4 4
      pioneer/packages/joy-proposals/src/Proposal/ProposalPreviewList.tsx
  11. 4 5
      pioneer/packages/joy-proposals/src/Proposal/ProposalTypePreview.tsx
  12. 1 1
      pioneer/packages/joy-proposals/src/Proposal/Votes.tsx
  13. 2 3
      pioneer/packages/joy-proposals/src/Proposal/VotingSection.tsx
  14. 2 3
      pioneer/packages/joy-proposals/src/forms/EvictStorageProviderForm.tsx
  15. 3 3
      pioneer/packages/joy-proposals/src/forms/GenericProposalForm.tsx
  16. 1 1
      pioneer/packages/joy-proposals/src/forms/MintCapacityForm.tsx
  17. 4 5
      pioneer/packages/joy-proposals/src/forms/SetContentWorkingGroupLeadForm.tsx
  18. 3 4
      pioneer/packages/joy-proposals/src/forms/SetContentWorkingGroupMintCapForm.tsx
  19. 5 5
      pioneer/packages/joy-proposals/src/forms/SetCouncilParamsForm.tsx
  20. 2 3
      pioneer/packages/joy-proposals/src/forms/SetMaxValidatorCountForm.tsx
  21. 3 3
      pioneer/packages/joy-proposals/src/forms/SetStorageRoleParamsForm.tsx
  22. 3 3
      pioneer/packages/joy-proposals/src/index.tsx
  23. 0 23
      pioneer/packages/joy-proposals/src/runtime/TransportContext.tsx
  24. 0 66
      pioneer/packages/joy-proposals/src/runtime/cache.ts
  25. 0 4
      pioneer/packages/joy-proposals/src/runtime/index.ts
  26. 0 16
      pioneer/packages/joy-proposals/src/runtime/transport.mock.ts
  27. 0 328
      pioneer/packages/joy-proposals/src/runtime/transport.substrate.ts
  28. 1 1
      pioneer/packages/joy-proposals/src/stories/data/ProposalDetails.mock.ts
  29. 0 9
      pioneer/packages/joy-proposals/src/stories/data/ProposalTypesInfo.mock.ts
  30. 0 273
      pioneer/packages/joy-proposals/src/utils.ts
  31. 3 0
      pioneer/packages/joy-utils/src/consts/members.ts
  32. 87 0
      pioneer/packages/joy-utils/src/consts/proposals.ts
  33. 8 0
      pioneer/packages/joy-utils/src/functions/misc.ts
  34. 49 0
      pioneer/packages/joy-utils/src/react/components/PromiseComponent.tsx
  35. 1 0
      pioneer/packages/joy-utils/src/react/components/index.tsx
  36. 1 0
      pioneer/packages/joy-utils/src/react/context/index.tsx
  37. 21 0
      pioneer/packages/joy-utils/src/react/context/transport.tsx
  38. 3 0
      pioneer/packages/joy-utils/src/react/hooks/index.tsx
  39. 56 0
      pioneer/packages/joy-utils/src/react/hooks/proposals/useProposalSubscription.tsx
  40. 26 0
      pioneer/packages/joy-utils/src/react/hooks/usePromise.tsx
  41. 7 0
      pioneer/packages/joy-utils/src/react/hooks/useTransport.tsx
  42. 41 0
      pioneer/packages/joy-utils/src/transport/base.ts
  43. 20 0
      pioneer/packages/joy-utils/src/transport/chain.ts
  44. 38 0
      pioneer/packages/joy-utils/src/transport/contentWorkingGroup.ts
  45. 78 0
      pioneer/packages/joy-utils/src/transport/council.ts
  46. 31 0
      pioneer/packages/joy-utils/src/transport/index.ts
  47. 9 0
      pioneer/packages/joy-utils/src/transport/members.ts
  48. 177 0
      pioneer/packages/joy-utils/src/transport/proposals.ts
  49. 19 0
      pioneer/packages/joy-utils/src/transport/storageProviders.ts
  50. 9 0
      pioneer/packages/joy-utils/src/transport/validators.ts
  51. 13 0
      pioneer/packages/joy-utils/src/types/members.ts
  52. 20 33
      pioneer/packages/joy-utils/src/types/proposals.ts
  53. 16 0
      pioneer/packages/joy-utils/src/types/storageProviders.ts

+ 4 - 5
pioneer/packages/joy-proposals/src/Proposal/Body.tsx

@@ -1,6 +1,6 @@
 import React from "react";
 import { Card, Header, Button, Icon, Message } from "semantic-ui-react";
-import { ProposalType } from "../runtime/transport";
+import { ProposalType } from "@polkadot/joy-utils/types/proposals";
 import { blake2AsHex } from '@polkadot/util-crypto';
 import styled from 'styled-components';
 import AddressMini from '@polkadot/react-components/AddressMiniJoy';
@@ -8,12 +8,11 @@ import TxButton from '@polkadot/joy-utils/TxButton';
 import { ProposalId } from "@joystream/types/proposals";
 import { MemberId } from "@joystream/types/members";
 import ProfilePreview from "@polkadot/joy-utils/MemberProfilePreview";
-import { useTransport } from "../runtime";
-import { usePromise } from "../utils";
+import { useTransport, usePromise } from "@polkadot/joy-utils/react/hooks";
 import { Profile } from "@joystream/types/members";
 import { Option } from "@polkadot/types/";
 import { formatBalance } from "@polkadot/util";
-import PromiseComponent from "./PromiseComponent";
+import { PromiseComponent } from "@polkadot/joy-utils/react/components";
 
 type BodyProps = {
   title: string;
@@ -45,7 +44,7 @@ function ProposedMember(props: { memberId?: MemberId | number | null }) {
 
   const transport = useTransport();
   const [ member, error, loading ] = usePromise<Option<Profile> | null>(
-    () => transport.memberProfile(memberId),
+    () => transport.members.memberProfile(memberId),
     null
   );
 

+ 22 - 28
pioneer/packages/joy-proposals/src/Proposal/ChooseProposalType.tsx

@@ -2,10 +2,8 @@ import React, { useState } from "react";
 import ProposalTypePreview 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 { useTransport, usePromise } from "@polkadot/joy-utils/react/hooks";
+import { PromiseComponent } from "@polkadot/joy-utils/react/components";
 import "./ChooseProposalType.css";
 import { RouteComponentProps } from "react-router-dom";
 
@@ -22,35 +20,31 @@ export type Category = typeof Categories[keyof typeof Categories];
 export default function ChooseProposalType(props: RouteComponentProps) {
   const transport = useTransport();
 
-  const [proposalTypes, error, loading] = usePromise(() => transport.proposalsTypesParameters(), []);
+  const [proposalTypes, error, loading] = usePromise(() => transport.proposals.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">
-        <Dropdown
-          placeholder="Category"
-          options={Object.values(Categories).map(category => ({ value: category, text: category }))}
-          value={category}
-          onChange={(e, data) => setCategory((data.value || "").toString())}
-          clearable
-          selection
-        />
-      </div>
-      <Item.Group>
-        {proposalTypes
-          .filter(typeInfo => !category || typeInfo.category === category)
-          .map((typeInfo, idx) => (
-            <ProposalTypePreview key={`${typeInfo} - ${idx}`} typeInfo={typeInfo} history={props.history} />
-          ))}
-      </Item.Group>
+      <PromiseComponent error={error} loading={loading} message={'Fetching proposals\' parameters...'}>
+        <div className="filters">
+          <Dropdown
+            placeholder="Category"
+            options={Object.values(Categories).map(category => ({ value: category, text: category }))}
+            value={category}
+            onChange={(e, data) => setCategory((data.value || "").toString())}
+            clearable
+            selection
+          />
+        </div>
+        <Item.Group>
+          {proposalTypes
+            .filter(typeInfo => !category || typeInfo.category === category)
+            .map((typeInfo, idx) => (
+              <ProposalTypePreview key={`${typeInfo} - ${idx}`} typeInfo={typeInfo} history={props.history} />
+            ))}
+        </Item.Group>
+      </PromiseComponent>
     </div>
   );
 }

+ 1 - 1
pioneer/packages/joy-proposals/src/Proposal/Details.tsx

@@ -1,6 +1,6 @@
 import React from "react";
 import { Item, Header } from "semantic-ui-react";
-import { ParsedProposal } from "../runtime/transport";
+import { ParsedProposal } from "@polkadot/joy-utils/types/proposals";
 import { ExtendedProposalStatus } from "./ProposalDetails";
 import styled from 'styled-components';
 

+ 0 - 17
pioneer/packages/joy-proposals/src/Proposal/Error.tsx

@@ -1,17 +0,0 @@
-import React from "react";
-import { Container, Message } from "semantic-ui-react";
-
-type ErrorProps = {
-  error: any;
-};
-export default function Error({ error }: ErrorProps) {
-  console.error(error);
-  return (
-    <Container style={{ display: "flex", justifyContent: "center", alignItems: "center" }}>
-      <Message negative>
-        <Message.Header>Oops! We got an error!</Message.Header>
-        <p>{error.message}</p>
-      </Message>
-    </Container>
-  );
-}

+ 0 - 14
pioneer/packages/joy-proposals/src/Proposal/Loading.tsx

@@ -1,14 +0,0 @@
-import React from "react";
-import { Loader, Container } from "semantic-ui-react";
-
-type LoadingProps = {
-  text: string;
-};
-
-export default function Loading({ text }: LoadingProps) {
-  return (
-    <Container style={{ display: "flex", justifyContent: "center", alignItems: "center" }}>
-      <Loader active>{text}</Loader>
-    </Container>
-  );
-}

+ 0 - 20
pioneer/packages/joy-proposals/src/Proposal/PromiseComponent.tsx

@@ -1,20 +0,0 @@
-import React from 'react';
-import Loading from "./Loading";
-import Error from "./Error";
-
-type PromiseComponentProps = {
-  loading: boolean,
-  error: any,
-  message: string,
-}
-const PromiseComponent: React.FunctionComponent<PromiseComponentProps> = ({ loading, error, message, children }) => {
-  if (loading && !error) {
-    return <Loading text={ message } />;
-  } else if (error) {
-    return <Error error={error} />;
-  }
-
-  return <>{ children }</>;
-}
-
-export default PromiseComponent;

+ 2 - 2
pioneer/packages/joy-proposals/src/Proposal/ProposalDetails.tsx

@@ -6,7 +6,7 @@ import Body from "./Body";
 import VotingSection from "./VotingSection";
 import Votes from "./Votes";
 import { MyAccountProps, withMyAccount } from "@polkadot/joy-utils/MyAccount"
-import { ParsedProposal, ProposalVote } from "../runtime";
+import { ParsedProposal, ProposalVote } from "@polkadot/joy-utils/types/proposals";
 import { withCalls } from '@polkadot/react-api';
 import { withMulti } from '@polkadot/react-api/with';
 
@@ -15,7 +15,7 @@ import { ProposalId, ProposalDecisionStatuses, ApprovedProposalStatuses, Executi
 import { BlockNumber } from '@polkadot/types/interfaces'
 import { MemberId } from "@joystream/types/members";
 import { Seat } from "@joystream/types/";
-import PromiseComponent from './PromiseComponent';
+import { PromiseComponent } from "@polkadot/joy-utils/react/components";
 
 type BasicProposalStatus = 'Active' | 'Finalized';
 type ProposalPeriodStatus = 'Voting period' | 'Grace period';

+ 10 - 10
pioneer/packages/joy-proposals/src/Proposal/ProposalFromId.tsx

@@ -1,9 +1,8 @@
 import React from "react";
 import { RouteComponentProps } from "react-router-dom";
 import ProposalDetails from "./ProposalDetails";
-import { useProposalSubscription } from "../utils";
-import Error from "./Error";
-import Loading from "./Loading";
+import { useProposalSubscription } from "@polkadot/joy-utils/react/hooks";
+import { PromiseComponent } from "@polkadot/joy-utils/react/components";
 
 
 export default function ProposalFromId(props: RouteComponentProps<any>) {
@@ -15,11 +14,12 @@ export default function ProposalFromId(props: RouteComponentProps<any>) {
 
   const { proposal: proposalState, votes: votesState } = useProposalSubscription(id);
 
-  if (proposalState.loading && !proposalState.error) {
-    return <Loading text="Fetching Proposal..." />;
-  } else if (proposalState.error) {
-    return <Error error={proposalState.error} />;
-  }
-
-  return <ProposalDetails proposal={ proposalState.data } proposalId={ id } votesListState={ votesState }/>;
+  return (
+    <PromiseComponent
+      error={proposalState.error}
+      loading={proposalState.loading}
+      message={"Fetching proposal..."}>
+      <ProposalDetails proposal={ proposalState.data } proposalId={ id } votesListState={ votesState }/>
+    </PromiseComponent>
+  )
 }

+ 1 - 1
pioneer/packages/joy-proposals/src/Proposal/ProposalPreview.tsx

@@ -1,7 +1,7 @@
 import React from "react";
 import { Header, Card } from "semantic-ui-react";
 import Details from "./Details";
-import { ParsedProposal } from "../runtime/transport";
+import { ParsedProposal } from "@polkadot/joy-utils/types/proposals";
 import { getExtendedStatus } from "./ProposalDetails";
 import { BlockNumber } from '@polkadot/types/interfaces';
 import styled from 'styled-components';

+ 4 - 4
pioneer/packages/joy-proposals/src/Proposal/ProposalPreviewList.tsx

@@ -2,9 +2,9 @@ import React, { useState } from "react";
 import { Card, Container, Menu } from "semantic-ui-react";
 
 import ProposalPreview from "./ProposalPreview";
-import { useTransport, ParsedProposal } from "../runtime";
-import { usePromise } from "../utils";
-import PromiseComponent from './PromiseComponent';
+import { ParsedProposal } from "@polkadot/joy-utils/types/proposals";
+import { useTransport , usePromise } from "@polkadot/joy-utils/react/hooks";
+import { PromiseComponent } from "@polkadot/joy-utils/react/components";
 import { withCalls } from "@polkadot/react-api";
 import { BlockNumber } from "@polkadot/types/interfaces";
 
@@ -52,7 +52,7 @@ 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.proposals(), []);
   const [activeFilter, setActiveFilter] = useState<ProposalFilter>("All");
 
   const proposalsMap = mapFromProposals(proposals);

+ 4 - 5
pioneer/packages/joy-proposals/src/Proposal/ProposalTypePreview.tsx

@@ -4,8 +4,8 @@ import { History } from "history";
 import { Item, Icon, Button, Label } from "semantic-ui-react";
 
 import { Category } from "./ChooseProposalType";
-import { ProposalType } from "../runtime";
-import { slugify, splitOnUpperCase } from "../utils";
+import { ProposalType } from "@polkadot/joy-utils/types/proposals";
+import _ from 'lodash';
 import styled from 'styled-components';
 import useVoteStyles from './useVoteStyles';
 import { formatBalance } from "@polkadot/util";
@@ -46,7 +46,6 @@ const CreateButton = styled(Button)`
 export type ProposalTypeInfo = {
   type: ProposalType;
   category: Category;
-  image: string;
   description: string;
   stake: number;
   cancellationFee?: number;
@@ -88,7 +87,7 @@ export default function ProposalTypePreview(props: ProposalTypePreviewProps) {
 
   const handleClick = () => {
     if (!props.history) return;
-    props.history.push(`/proposals/new/${slugify(type)}`);
+    props.history.push(`/proposals/new/${_.kebabCase(type)}`);
   };
 
   return (
@@ -98,7 +97,7 @@ export default function ProposalTypePreview(props: ProposalTypePreviewProps) {
         <Item.Image size="tiny" src={image} />
       */}
       <Item.Content>
-        <Item.Header>{splitOnUpperCase(type).join(" ")}</Item.Header>
+        <Item.Header>{_.startCase(type)}</Item.Header>
         <Item.Description>{description}</Item.Description>
         <div className="proposal-details">
           <ProposalTypeDetail

+ 1 - 1
pioneer/packages/joy-proposals/src/Proposal/Votes.tsx

@@ -1,7 +1,7 @@
 import React from "react";
 import { Header, Divider, Table, Icon } from "semantic-ui-react";
 import useVoteStyles from "./useVoteStyles";
-import { ProposalVote } from "../runtime";
+import { ProposalVote } from "@polkadot/joy-utils/types/proposals";
 import { VoteKind } from "@joystream/types/proposals";
 import { VoteKindStr } from "./VotingSection";
 import ProfilePreview from "@polkadot/joy-utils/MemberProfilePreview";

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

@@ -5,9 +5,8 @@ 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 { useTransport, usePromise } from "@polkadot/joy-utils/react/hooks";
 import { VoteKind } from '@joystream/types/proposals';
-import { usePromise } from "../utils";
 import { VoteKinds } from "@joystream/types/proposals";
 
 export type VoteKindStr = typeof VoteKinds[number];
@@ -56,7 +55,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.proposals.voteByProposalAndMember(proposalId, memberId),
     undefined
   );
 

+ 2 - 3
pioneer/packages/joy-proposals/src/forms/EvictStorageProviderForm.tsx

@@ -18,8 +18,7 @@ import { withFormContainer } from "./FormContainer";
 import { InputAddress } from "@polkadot/react-components/index";
 import { accountIdsToOptions } from "@polkadot/joy-election/utils";
 import { AccountId } from "@polkadot/types/interfaces";
-import { useTransport } from "../runtime";
-import { usePromise } from "../utils";
+import { useTransport, usePromise } from "@polkadot/joy-utils/react/hooks";
 import "./forms.css";
 
 type FormValues = GenericFormValues & {
@@ -40,7 +39,7 @@ const EvictStorageProviderForm: React.FunctionComponent<FormInnerProps> = props
   const { errors, touched, values, setFieldValue } = props;
   const errorLabelsProps = getFormErrorLabelsProps<FormValues>(errors, touched);
   const transport = useTransport();
-  const [storageProviders /* error */, , loading] = usePromise<AccountId[]>(() => transport.storageProviders(), []);
+  const [storageProviders /* error */, , loading] = usePromise<AccountId[]>(() => transport.storageProviders.providers(), []);
   const storageProvidersOptions = accountIdsToOptions(storageProviders);
   return (
     <GenericProposalForm

+ 3 - 3
pioneer/packages/joy-proposals/src/forms/GenericProposalForm.tsx

@@ -13,8 +13,8 @@ import { withCalls } from "@polkadot/react-api";
 import { CallProps } from "@polkadot/react-api/types";
 import { Balance, Event } from "@polkadot/types/interfaces";
 import { RouteComponentProps } from "react-router";
-import { ProposalType } from "../runtime";
-import { calculateStake } from "../utils";
+import { ProposalType } from "@polkadot/joy-utils/types/proposals";
+import proposalsConsts from "@polkadot/joy-utils/consts/proposals";
 import { formatBalance } from "@polkadot/util"
 import "./forms.css";
 import { ProposalId } from "@joystream/types/proposals";
@@ -125,7 +125,7 @@ export const GenericProposalForm: React.FunctionComponent<GenericFormInnerProps>
   const requiredStake: number | undefined =
     balances_totalIssuance &&
     proposalType &&
-    calculateStake(proposalType, balances_totalIssuance.toNumber());
+    proposalsConsts[proposalType].stake;
 
   return (
     <div className="Forms">

+ 1 - 1
pioneer/packages/joy-proposals/src/forms/MintCapacityForm.tsx

@@ -14,7 +14,7 @@ import {
 import Validation from "../validationSchema";
 import { InputFormField } from "./FormFields";
 import { withFormContainer } from "./FormContainer";
-import { ProposalType } from "../runtime";
+import { ProposalType } from "@polkadot/joy-utils/types/proposals";
 import { formatBalance } from "@polkadot/util";
 import "./forms.css";
 

+ 4 - 5
pioneer/packages/joy-proposals/src/forms/SetContentWorkingGroupLeadForm.tsx

@@ -15,10 +15,9 @@ import {
 import Validation from "../validationSchema";
 import { FormField } from "./FormFields";
 import { withFormContainer } from "./FormContainer";
-import { useTransport } from "../runtime";
-import { usePromise } from "../utils";
+import { useTransport, usePromise } from "@polkadot/joy-utils/react/hooks";
 import { Profile } from "@joystream/types/members";
-import PromiseComponent from "../Proposal/PromiseComponent";
+import { PromiseComponent } from "@polkadot/joy-utils/react/components";
 import _ from 'lodash';
 import "./forms.css";
 
@@ -79,11 +78,11 @@ const SetContentWorkingGroupsLeadForm: React.FunctionComponent<FormInnerProps> =
   // Transport
   const transport = useTransport();
   const [members, /* error */, loading] = usePromise<MemberWithId[]>(
-    () => transport.membersExceptCouncil(),
+    () => transport.council.membersExceptCouncil(),
     []
   );
   const [currentLead, clError, clLoading] = usePromise<MemberWithId | null>(
-    () => transport.WGLead(),
+    () => transport.contentWorkingGroup.currentLead(),
     null
   );
   // Generate members options array on load

+ 3 - 4
pioneer/packages/joy-proposals/src/forms/SetContentWorkingGroupMintCapForm.tsx

@@ -1,13 +1,12 @@
 import React from 'react';
 import { default as MintCapacityForm } from './MintCapacityForm';
 import { RouteComponentProps } from 'react-router';
-import { useTransport } from "../runtime";
-import { usePromise } from "../utils";
-import PromiseComponent from '../Proposal/PromiseComponent';
+import { useTransport, usePromise } from "@polkadot/joy-utils/react/hooks";
+import { PromiseComponent } from "@polkadot/joy-utils/react/components";
 
 const ContentWorkingGroupMintCapForm = (props: RouteComponentProps) => {
   const transport = useTransport();
-  const [ mintCapacity, error, loading ] = usePromise<number>(() => transport.WGMintCap(), 0);
+  const [ mintCapacity, error, loading ] = usePromise<number>(() => transport.contentWorkingGroup.currentMintCap(), 0);
 
   return (
     <PromiseComponent error={error} loading={loading} message="Fetching current mint capacity...">

+ 5 - 5
pioneer/packages/joy-proposals/src/forms/SetCouncilParamsForm.tsx

@@ -17,10 +17,10 @@ import { InputFormField } from "./FormFields";
 import { withFormContainer } from "./FormContainer";
 import { createType } from "@polkadot/types";
 import "./forms.css";
-import { useTransport } from "../runtime";
-import { usePromise, snakeCaseToCamelCase } from "../utils";
+import { useTransport, usePromise } from "@polkadot/joy-utils/react/hooks";
+import _ from "lodash";
 import { ElectionParameters } from "@joystream/types/proposals";
-import PromiseComponent from "../Proposal/PromiseComponent";
+import { PromiseComponent } from "@polkadot/joy-utils/react/components";
 
 type FormValues = GenericFormValues & {
   announcingPeriod: string;
@@ -69,7 +69,7 @@ const SetCouncilParamsForm: React.FunctionComponent<FormInnerProps> = props => {
   const [ placeholders, setPlaceholders ] = useState<{ [k in keyof FormValues]: string }>(defaultValues);
 
   const transport = useTransport();
-  const [ councilParams, error, loading ] = usePromise<ElectionParameters | null>(() => transport.electionParameters(), null);
+  const [ councilParams, error, loading ] = usePromise<ElectionParameters | null>(() => transport.council.electionParameters(), null);
   useEffect(() => {
     if (councilParams) {
       let fetchedPlaceholders = {...placeholders};
@@ -84,7 +84,7 @@ const SetCouncilParamsForm: React.FunctionComponent<FormInnerProps> = props => {
         "council_size"
       ] as const;
       fieldsToPopulate.forEach(field => {
-        const camelCaseField = snakeCaseToCamelCase(field) as keyof FormValues;
+        const camelCaseField = _.camelCase(field) as keyof FormValues;
         setFieldValue(camelCaseField, councilParams[field].toString());
         fetchedPlaceholders[camelCaseField] = councilParams[field].toString();
       });

+ 2 - 3
pioneer/packages/joy-proposals/src/forms/SetMaxValidatorCountForm.tsx

@@ -14,8 +14,7 @@ import {
 import Validation from "../validationSchema";
 import { InputFormField } from "./FormFields";
 import { withFormContainer } from "./FormContainer";
-import { useTransport } from "../runtime";
-import { usePromise } from "../utils";
+import { useTransport, usePromise } from "@polkadot/joy-utils/react/hooks";
 import "./forms.css";
 
 type FormValues = GenericFormValues & {
@@ -34,7 +33,7 @@ type FormInnerProps = ProposalFormInnerProps<FormContainerProps, FormValues>;
 
 const SetMaxValidatorCountForm: React.FunctionComponent<FormInnerProps> = props => {
   const transport = useTransport();
-  const [validatorCount] = usePromise<number>(() => transport.maxValidatorCount(), NaN);
+  const [validatorCount] = usePromise<number>(() => transport.validators.maxCount(), NaN);
   const { handleChange, errors, touched, values, setFieldValue } = props;
   const errorLabelsProps = getFormErrorLabelsProps<FormValues>(errors, touched);
 

+ 3 - 3
pioneer/packages/joy-proposals/src/forms/SetStorageRoleParamsForm.tsx

@@ -18,8 +18,8 @@ import { withFormContainer } from "./FormContainer";
 import { BlockNumber, Balance } from "@polkadot/types/interfaces";
 import { u32 } from "@polkadot/types/primitive";
 import { createType } from "@polkadot/types";
-import { useTransport, StorageRoleParameters, IStorageRoleParameters } from "../runtime";
-import { usePromise } from "../utils";
+import { useTransport, usePromise } from "@polkadot/joy-utils/react/hooks";
+import { StorageRoleParameters, IStorageRoleParameters } from "@polkadot/joy-utils/types/storageProviders";
 import { formatBalance } from "@polkadot/util";
 import "./forms.css";
 
@@ -79,7 +79,7 @@ function createRoleParameters(values: FormValues): RoleParameters {
 
 const SetStorageRoleParamsForm: React.FunctionComponent<FormInnerProps> = props => {
   const transport = useTransport();
-  const [params] = usePromise<IStorageRoleParameters | null>(() => transport.storageRoleParameters(), null);
+  const [params] = usePromise<IStorageRoleParameters | null>(() => transport.storageProviders.roleParameters(), null);
   const { handleChange, errors, touched, values, setFieldValue } = props;
   const [placeholders, setPlaceholders] = useState<{ [k in keyof FormValues]: string }>(defaultValues);
   const errorLabelsProps = getFormErrorLabelsProps<FormValues>(errors, touched);

+ 3 - 3
pioneer/packages/joy-proposals/src/index.tsx

@@ -3,7 +3,7 @@ import { Route, Switch } from "react-router";
 
 import { AppProps, I18nProps } from "@polkadot/react-components/types";
 import Tabs, { TabItem } from "@polkadot/react-components/Tabs";
-import { SubstrateProvider } from "./runtime";
+import { TransportProvider } from "@polkadot/joy-utils/react/context";
 import { ProposalPreviewList, ProposalFromId, ChooseProposalType } from "./Proposal";
 
 import "./index.css";
@@ -40,7 +40,7 @@ function App(props: Props): React.ReactElement<Props> {
   ];
 
   return (
-    <SubstrateProvider>
+    <TransportProvider>
       <main className="proposal--App">
         <header>
           <Tabs basePath={basePath} items={tabs} />
@@ -66,7 +66,7 @@ function App(props: Props): React.ReactElement<Props> {
           <Route component={ProposalPreviewList} />
         </Switch>
       </main>
-    </SubstrateProvider>
+    </TransportProvider>
   );
 }
 

+ 0 - 23
pioneer/packages/joy-proposals/src/runtime/TransportContext.tsx

@@ -1,23 +0,0 @@
-import React, { createContext, useContext } from "react";
-import { ApiContext } from "@polkadot/react-api";
-import { ApiProps } from "@polkadot/react-api/types";
-import { SubstrateTransport } from "./transport.substrate";
-import { MockTransport } from "./transport.mock";
-import { Transport } from "./transport";
-
-const TransportContext = createContext<Transport>((null as unknown) as Transport);
-
-export function MockProvider({ children }: { children: React.PropsWithChildren<{}> }) {
-  return <TransportContext.Provider value={new MockTransport()}>{children}</TransportContext.Provider>;
-}
-
-export function SubstrateProvider({ children }: { children: React.PropsWithChildren<{}> }) {
-  const api: ApiProps = useContext(ApiContext);
-  const transport = new SubstrateTransport(api);
-
-  return <TransportContext.Provider value={transport}>{children}</TransportContext.Provider>;
-}
-
-export function useTransport() {
-  return useContext(TransportContext) as SubstrateTransport;
-}

+ 0 - 66
pioneer/packages/joy-proposals/src/runtime/cache.ts

@@ -1,66 +0,0 @@
-// Set does not do a deep equal when adding elements, so try to only use strings or another primitive for K
-
-export default class Cache<K, T extends { id: K }> extends Map<K, T> {
-  protected neverClear: Set<K>;
-
-  constructor(
-    objects: Iterable<readonly [K, T]>,
-    protected loaderFn: (ids: K[]) => Promise<T[]>,
-    neverClear: K[] | Set<K> = [],
-    public name?: string
-  ) {
-    super(objects);
-    this.name = name;
-    this.neverClear = new Set(neverClear);
-    this.loaderFn = loaderFn;
-  }
-
-  forceClear(): void {
-    const prevCacheSize = this.size;
-    this.clear();
-    console.info(`Removed all ${prevCacheSize} entries from ${this.name}, including ${this.neverClear}`);
-  }
-
-  clearExcept(keepIds: K[] | Set<K>, force: boolean = false): void {
-    const prevCacheSize = this.size;
-    const keepIdsSet = force ? new Set(keepIds) : new Set([...keepIds, ...this.neverClear]);
-
-    for (let key of this.keys()) {
-      if (!keepIdsSet.has(key)) {
-        this.delete(key);
-      }
-    }
-
-    console.info(`Removed ${prevCacheSize - this.size} entries out of ${prevCacheSize} from ${this.name}`);
-  }
-
-  clear(): void {
-    this.clearExcept([]);
-  }
-
-  async load(ids: K[], force: boolean = false): Promise<T[]> {
-    const idsNotInCache: K[] = [];
-    const cachedObjects: T[] = [];
-
-    ids.forEach(id => {
-      let objFromCache = this.get(id);
-      if (objFromCache && !force) {
-        cachedObjects.push(objFromCache);
-      } else {
-        idsNotInCache.push(id);
-      }
-    });
-
-    let loadedObjects: T[] = [];
-
-    if (idsNotInCache.length > 0) {
-      loadedObjects = await this.loaderFn(idsNotInCache);
-      loadedObjects.forEach(obj => {
-        const id = obj.id;
-        this.set(id, obj);
-      });
-    }
-
-    return [...cachedObjects, ...loadedObjects];
-  }
-}

+ 0 - 4
pioneer/packages/joy-proposals/src/runtime/index.ts

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

+ 0 - 16
pioneer/packages/joy-proposals/src/runtime/transport.mock.ts

@@ -1,16 +0,0 @@
-import { Transport, ParsedProposal } from "./transport";
-
-function delay(ms: number) {
-  return new Promise(resolve => setTimeout(resolve, ms));
-}
-
-export class MockTransport extends Transport {
-  constructor() {
-    super();
-  }
-
-  async proposals() {
-    await delay(Math.random() * 2000);
-    return Promise.all((Array.from({ length: 5 }, (_, i) => "Not implemented") as unknown) as ParsedProposal[]);
-  }
-}

+ 0 - 328
pioneer/packages/joy-proposals/src/runtime/transport.substrate.ts

@@ -1,328 +0,0 @@
-import {
-  Transport,
-  ParsedProposal,
-  ProposalType,
-  ProposalTypes,
-  ParsedMember,
-  ProposalVote,
-  IStorageRoleParameters
-} from "./transport";
-import { Proposal, ProposalId, Seats, VoteKind, ElectionParameters } from "@joystream/types/proposals";
-import { MemberId, Profile, ActorInRole, RoleKeys, Role } from "@joystream/types/members";
-import { ApiProps } from "@polkadot/react-api/types";
-import { u32, u128, Vec, Option } from "@polkadot/types/";
-import { Balance, Moment, AccountId, BlockNumber, BalanceOf } from "@polkadot/types/interfaces";
-import { ApiPromise } from "@polkadot/api";
-
-import { FIRST_MEMBER_ID } from "@polkadot/joy-members/constants";
-
-import { includeKeys, calculateStake, calculateMetaFromType, splitOnUpperCase } from "../utils";
-import { MintId, Mint } from "@joystream/types/mint";
-import { LeadId } from "@joystream/types/content-working-group";
-
-export class SubstrateTransport extends Transport {
-  protected api: ApiPromise;
-
-  constructor(api: ApiProps) {
-    super();
-
-    if (!api) {
-      throw new Error("Cannot create SubstrateTransport: A Substrate API is required");
-    } else if (!api.isApiReady) {
-      throw new Error("Cannot create a SubstrateTransport: The Substrate API is not ready yet.");
-    }
-
-    this.api = api.api;
-  }
-
-  get proposalsEngine() {
-    return this.api.query.proposalsEngine;
-  }
-
-  get proposalsCodex() {
-    return this.api.query.proposalsCodex;
-  }
-
-  get members() {
-    return this.api.query.members;
-  }
-
-  get council() {
-    return this.api.query.council;
-  }
-
-  get councilElection() {
-    return this.api.query.councilElection;
-  }
-
-  get actors() {
-    return this.api.query.actors;
-  }
-
-  get contentWorkingGroup() {
-    return this.api.query.contentWorkingGroup;
-  }
-
-  get minting() {
-    return this.api.query.minting;
-  }
-
-  totalIssuance() {
-    return this.api.query.balances.totalIssuance<Balance>();
-  }
-
-  async blockHash(height: number): Promise<string> {
-    const blockHash = await this.api.rpc.chain.getBlockHash(height);
-
-    return blockHash.toString();
-  }
-
-  async blockTimestamp(height: number): Promise<Date> {
-    const blockTime = (await this.api.query.timestamp.now.at(await this.blockHash(height))) as Moment;
-
-    return new Date(blockTime.toNumber());
-  }
-
-  proposalCount() {
-    return this.proposalsEngine.proposalCount<u32>();
-  }
-
-  rawProposalById(id: ProposalId) {
-    return this.proposalsEngine.proposals<Proposal>(id);
-  }
-
-  proposalDetailsById(id: ProposalId) {
-    return this.proposalsCodex.proposalDetailsByProposalId(id);
-  }
-
-  memberProfile(id: MemberId | number): Promise<Option<Profile>> {
-    return this.members.memberProfile(id) as Promise<Option<Profile>>;
-  }
-
-  async cancellationFee(): Promise<number> {
-    return ((await this.api.consts.proposalsEngine.cancellationFee) as BalanceOf).toNumber();
-  }
-
-  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 = Array.isArray(rawDetails[type]) ? rawDetails[type] : [rawDetails[type]];
-    const rawProposal = await this.rawProposalById(id);
-    const proposer = (await this.memberProfile(rawProposal.proposerId)).toJSON() as ParsedMember;
-    const proposal = rawProposal.toJSON() as {
-      title: string;
-      description: string;
-      parameters: any;
-      votingResults: any;
-      proposerId: number;
-      status: any;
-    };
-    const createdAtBlock = rawProposal.createdAt;
-    const createdAt = await this.blockTimestamp(createdAtBlock.toNumber());
-    const cancellationFee = await this.cancellationFee();
-
-    return {
-      id,
-      ...proposal,
-      details,
-      type,
-      proposer,
-      createdAtBlock: createdAtBlock.toJSON(),
-      createdAt,
-      cancellationFee
-    };
-  }
-
-  async proposalsIds() {
-    const total: number = (await this.proposalCount()).toNumber();
-    return Array.from({ length: total }, (_, i) => new ProposalId(i + 1));
-  }
-
-  async proposals() {
-    const ids = await this.proposalsIds();
-    return Promise.all(ids.map(id => this.proposalById(id)));
-  }
-
-  async activeProposals() {
-    const activeProposalIds = await this.proposalsEngine.activeProposalIds<ProposalId[]>();
-
-    return Promise.all(activeProposalIds.map(id => this.proposalById(id)));
-  }
-
-  async proposedBy(member: MemberId) {
-    const proposals = await this.proposals();
-    return proposals.filter(({ proposerId }) => member.eq(proposerId));
-  }
-
-  async proposalDetails(id: ProposalId) {
-    return this.proposalsCodex.proposalDetailsByProposalId(id);
-  }
-
-  async councilMembers(): Promise<(ParsedMember & { memberId: MemberId })[]> {
-    const council = (await this.council.activeCouncil()) as Seats;
-    return Promise.all(
-      council.map(async seat => {
-        const memberIds = (await this.members.memberIdsByControllerAccountId(seat.member)) as Vec<MemberId>;
-        const member = (await this.memberProfile(memberIds[0])).toJSON() as ParsedMember;
-        return {
-          ...member,
-          memberId: memberIds[0]
-        };
-      })
-    );
-  }
-
-  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;
-  }
-
-  async votes(proposalId: ProposalId): Promise<ProposalVote[]> {
-    const councilMembers = await this.councilMembers();
-    return Promise.all(
-      councilMembers.map(async member => {
-        const vote = await this.voteByProposalAndMember(proposalId, member.memberId);
-        return {
-          vote,
-          member
-        };
-      })
-    );
-  }
-
-  async fetchProposalMethodsFromCodex(includeKey: string) {
-    const methods = includeKeys(this.proposalsCodex, includeKey);
-    // 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 = splitOnUpperCase(method)
-        .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 proposalTypesGracePeriod(): Promise<{ [k in ProposalType]: number }> {
-    return this.fetchProposalMethodsFromCodex("GracePeriod");
-  }
-
-  async proposalTypesVotingPeriod(): Promise<{ [k in ProposalType]: number }> {
-    return this.fetchProposalMethodsFromCodex("VotingPeriod");
-  }
-
-  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);
-    // Currently it's same for all types, but this will change soon
-    const cancellationFee = await this.cancellationFee();
-    return {
-      type,
-      votingPeriod,
-      gracePeriod,
-      stake,
-      cancellationFee,
-      ...meta
-    };
-  }
-
-  async proposalsTypesParameters() {
-    return Promise.all(ProposalTypes.map(type => this.parametersFromProposalType(type)));
-  }
-
-  async bestBlock() {
-    return await this.api.derive.chain.bestNumber();
-  }
-
-  async storageProviders(): Promise<AccountId[]> {
-    const providers = (await this.actors.accountIdsByRole(RoleKeys.StorageProvider)) as Vec<AccountId>;
-    return providers.toArray();
-  }
-
-  async membersExceptCouncil(): Promise<{ id: number; profile: Profile }[]> {
-    // Council members to filter out
-    const activeCouncil = (await this.council.activeCouncil()) as Seats;
-    const membersCount = ((await this.members.membersCreated()) as MemberId).toNumber();
-    const profiles: { id: number; profile: Profile }[] = [];
-    for (let id = FIRST_MEMBER_ID.toNumber(); id < membersCount; ++id) {
-      const profile = (await this.memberProfile(new MemberId(id))).unwrapOr(null);
-      if (
-        !profile ||
-        // Filter out council members
-        activeCouncil.some(
-          seat =>
-            seat.member.toString() === profile.controller_account.toString() ||
-            seat.member.toString() === profile.root_account.toString()
-        )
-      ) {
-        continue;
-      }
-      profiles.push({ id, profile });
-    }
-
-    return profiles;
-  }
-
-  async storageRoleParameters(): Promise<IStorageRoleParameters> {
-    const params = (
-      await this.api.query.actors.parameters(RoleKeys.StorageProvider)
-    ).toJSON() as IStorageRoleParameters;
-    return params;
-  }
-
-  async maxValidatorCount(): Promise<number> {
-    const count = ((await this.api.query.staking.validatorCount()) as u32).toNumber();
-    return count;
-  }
-
-  async electionParameters(): Promise<ElectionParameters> {
-    const announcing_period = (await this.councilElection.announcingPeriod()) as BlockNumber;
-    const voting_period = (await this.councilElection.votingPeriod()) as BlockNumber;
-    const revealing_period = (await this.councilElection.revealingPeriod()) as BlockNumber;
-    const new_term_duration = (await this.councilElection.newTermDuration()) as BlockNumber;
-    const min_council_stake = (await this.councilElection.minCouncilStake()) as Balance;
-    const min_voting_stake = (await this.councilElection.minVotingStake()) as Balance;
-    const candidacy_limit = (await this.councilElection.candidacyLimit()) as u32;
-    const council_size = (await this.councilElection.councilSize()) as u32;
-
-    return new ElectionParameters({
-      announcing_period,
-      voting_period,
-      revealing_period,
-      new_term_duration,
-      min_council_stake,
-      min_voting_stake,
-      candidacy_limit,
-      council_size
-    });
-  }
-
-  async WGMintCap(): Promise<number> {
-    const WGMintId = (await this.contentWorkingGroup.mint()) as MintId;
-    const WGMint = (await this.minting.mints(WGMintId)) as Vec<Mint>;
-    return (WGMint[0].get("capacity") as u128).toNumber();
-  }
-
-  async WGLead(): Promise<{ id: number; profile: Profile } | null> {
-    const optLeadId = (await this.contentWorkingGroup.currentLeadId()) as Option<LeadId>;
-    const leadId = optLeadId.unwrapOr(null);
-
-    if (!leadId) return null;
-
-    const actorInRole = new ActorInRole({
-      role: new Role(RoleKeys.CuratorLead),
-      actor_id: leadId
-    });
-    const memberId = (await this.members.membershipIdByActorInRole(actorInRole)) as MemberId;
-    const profile = (await this.memberProfile(memberId)).unwrapOr(null);
-
-    return profile && { id: memberId.toNumber(), profile };
-  }
-}

+ 1 - 1
pioneer/packages/joy-proposals/src/stories/data/ProposalDetails.mock.ts

@@ -1,4 +1,4 @@
-import { ParsedProposal } from "../../runtime";
+import { ParsedProposal } from "@polkadot/joy-utils/types/proposals";
 import { ProposalId } from "@joystream/types/proposals"
 
 const mockedProposal: ParsedProposal = {

+ 0 - 9
pioneer/packages/joy-proposals/src/stories/data/ProposalTypesInfo.mock.ts

@@ -5,7 +5,6 @@ const MockProposalTypesInfo: ProposalTypeInfo[] = [
   {
     type: "Text",
     category: Categories.other,
-    image: "https://react.semantic-ui.com/images/wireframe/image.png",
     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."+
@@ -22,7 +21,6 @@ const MockProposalTypesInfo: ProposalTypeInfo[] = [
   {
     type: "Spending",
     category: Categories.other,
-    image: "https://react.semantic-ui.com/images/wireframe/image.png",
     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."+
@@ -39,7 +37,6 @@ const MockProposalTypesInfo: ProposalTypeInfo[] = [
   {
     type: "RuntimeUpgrade",
     category: Categories.other,
-    image: "https://react.semantic-ui.com/images/wireframe/image.png",
     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."+
@@ -56,7 +53,6 @@ const MockProposalTypesInfo: ProposalTypeInfo[] = [
   {
     type: "EvictStorageProvider",
     category: Categories.storage,
-    image: "https://react.semantic-ui.com/images/wireframe/image.png",
     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."+
@@ -73,7 +69,6 @@ const MockProposalTypesInfo: ProposalTypeInfo[] = [
   {
     type: "SetStorageRoleParameters",
     category: Categories.storage,
-    image: "https://react.semantic-ui.com/images/wireframe/image.png",
     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."+
@@ -90,7 +85,6 @@ const MockProposalTypesInfo: ProposalTypeInfo[] = [
   {
     type: "SetValidatorCount",
     category: Categories.validators,
-    image: "https://react.semantic-ui.com/images/wireframe/image.png",
     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."+
@@ -107,7 +101,6 @@ const MockProposalTypesInfo: ProposalTypeInfo[] = [
   {
     type: "SetContentWorkingGroupMintCapacity",
     category: Categories.cwg,
-    image: "https://react.semantic-ui.com/images/wireframe/image.png",
     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."+
@@ -124,7 +117,6 @@ const MockProposalTypesInfo: ProposalTypeInfo[] = [
   {
     type: "SetLead",
     category: Categories.cwg,
-    image: "https://react.semantic-ui.com/images/wireframe/image.png",
     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."+
@@ -141,7 +133,6 @@ const MockProposalTypesInfo: ProposalTypeInfo[] = [
   {
     type: "SetElectionParameters",
     category: Categories.council,
-    image: "https://react.semantic-ui.com/images/wireframe/image.png",
     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."+

+ 0 - 273
pioneer/packages/joy-proposals/src/utils.ts

@@ -1,273 +0,0 @@
-import { useState, useEffect, useCallback } from "react";
-import { ProposalType } from "./runtime";
-import { Category } from "./Proposal/ChooseProposalType";
-import { useTransport, ParsedProposal, ProposalVote } from "./runtime";
-import { ProposalId } from "@joystream/types/proposals";
-
-type ProposalMeta = {
-  description: string;
-  category: Category;
-  image: string;
-  approvalQuorum: number;
-  approvalThreshold: number;
-  slashingQuorum: number;
-  slashingThreshold: number;
-}
-
-export function includeKeys<T extends { [k: string]: any }>(obj: T, ...allowedKeys: string[]) {
-  return Object.keys(obj).filter(objKey => {
-    return allowedKeys.reduce(
-      (hasAllowed: boolean, allowedKey: string) => hasAllowed || objKey.includes(allowedKey),
-      false
-    );
-  });
-}
-
-export function splitOnUpperCase(str: string) {
-  return str.split(/(?=[A-Z])/);
-}
-
-export function slugify(str: string) {
-  return splitOnUpperCase(str)
-    .map(w => w.toLowerCase())
-    .join("-")
-    .trim();
-}
-
-export function snakeCaseToCamelCase(str: string) {
-  return str
-    .split('_')
-    .map((w, i) => i ? w[0].toUpperCase() + w.substr(1) : w)
-    .join('');
-}
-
-export function camelCaseToSnakeCase(str: string) {
-  return splitOnUpperCase(str)
-    .map(w => w[0].toLocaleLowerCase() + w.substr(1))
-    .join('_');
-}
-
-export function usePromise<T>(promise: () => Promise<T>, defaultValue: T): [T, any, boolean, () => Promise<void|null>] {
-  const [state, setState] = useState<{
-    value: T;
-    error: any;
-    isPending: boolean;
-  }>({ value: defaultValue, error: null, isPending: true });
-
-  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;
-    };
-  }, []);
-
-  const { value, error, isPending } = state;
-  return [value, error, isPending, execute];
-}
-
-// Take advantage of polkadot api subscriptions to re-fetch proposal data and votes
-// each time there is some runtime change in the proposal
-export const useProposalSubscription = (id: ProposalId) => {
-  const transport = useTransport();
-  // State holding an "unsubscribe method"
-  const [unsubscribeProposal, setUnsubscribeProposal] = useState<(() => void) | null>(null);
-
-  const [proposal, proposalError, proposalLoading, refreshProposal] = usePromise<ParsedProposal>(
-    () => transport.proposalById(id),
-    {} as ParsedProposal
-  );
-
-  const [votes, votesError, votesLoading, refreshVotes] = usePromise<ProposalVote[]>(
-    () => transport.votes(id),
-    []
-  );
-
-  // Function to re-fetch the data using transport
-  const refreshProposalData = () => {
-    refreshProposal();
-    refreshVotes();
-  }
-
-  useEffect(() => {
-    // onMount...
-    let unmounted = false;
-    // Create the subscription
-    transport.proposalsEngine.proposals(id, refreshProposalData)
-      .then(unsubscribe => {
-        if (!unmounted) {
-          setUnsubscribeProposal(() => unsubscribe);
-        }
-        else {
-          unsubscribe(); // If already unmounted - unsubscribe immedietally!
-        }
-      });
-    return () => {
-      // onUnmount...
-      // Clean the subscription
-      unmounted = true;
-      if (unsubscribeProposal !== null) unsubscribeProposal();
-    }
-  }, []);
-
-  return {
-    proposal: { data: proposal, error: proposalError, loading: proposalLoading },
-    votes: { data: votes, error: votesError, loading: votesLoading }
-  }
-};
-
-
-export function calculateStake(type: ProposalType, issuance: number) {
-  let stake = NaN;
-  switch (type) {
-    case "EvictStorageProvider": {
-      stake = 25000;
-      break;
-    }
-    case "Text":
-      stake = 25000;
-      break;
-    case "SetStorageRoleParameters":
-      stake = 100000;
-      break;
-    case "SetValidatorCount":
-      stake = 100000;
-      break;
-    case "SetLead":
-      stake = 50000;
-      break;
-    case "SetContentWorkingGroupMintCapacity":
-      stake = 50000;
-      break;
-    case "Spending": {
-      stake = 25000;
-      break;
-    }
-    case "SetElectionParameters": {
-      stake = 200000;
-      break;
-    }
-    case "RuntimeUpgrade": {
-      stake = 1000000;
-      break;
-    }
-    default: {
-      throw new Error(`Proposal Type is invalid. Got ${type}. Can't calculate issuance.`);
-    }
-  }
-  return stake;
-}
-
-export function calculateMetaFromType(type: ProposalType): ProposalMeta {
-  const image = "";
-  switch (type) {
-    case "EvictStorageProvider": {
-      return {
-        description: "Evicting Storage Provider Proposal",
-        category: "Storage",
-        image,
-        approvalQuorum: 50,
-        approvalThreshold: 75,
-        slashingQuorum: 60,
-        slashingThreshold: 80,
-      }
-    }
-    case "Text": {
-      return {
-        description: "Signal Proposal",
-        category: "Other",
-        image,
-        approvalQuorum: 60,
-        approvalThreshold: 80,
-        slashingQuorum: 60,
-        slashingThreshold: 80,
-      }
-    }
-    case "SetStorageRoleParameters": {
-      return {
-        description: "Set Storage Role Params Proposal",
-        category: "Storage",
-        image,
-        approvalQuorum: 66,
-        approvalThreshold: 80,
-        slashingQuorum: 60,
-        slashingThreshold: 80,
-      }
-    }
-    case "SetValidatorCount": {
-      return {
-        description: "Set Max Validator Count Proposal",
-        category: "Validators",
-        image,
-        approvalQuorum: 66,
-        approvalThreshold: 80,
-        slashingQuorum: 60,
-        slashingThreshold: 80,
-      }
-    }
-    case "SetLead": {
-      return {
-        description: "Set Lead Proposal",
-        category: "Content Working Group",
-        image,
-        approvalQuorum: 60,
-        approvalThreshold: 75,
-        slashingQuorum: 60,
-        slashingThreshold: 80,
-      }
-    }
-    case "SetContentWorkingGroupMintCapacity": {
-      return {
-        description: "Set WG Mint Capacity Proposal",
-        category: "Content Working Group",
-        image,
-        approvalQuorum: 60,
-        approvalThreshold: 75,
-        slashingQuorum: 60,
-        slashingThreshold: 80,
-      }
-    }
-    case "Spending": {
-      return {
-        description: "Spending Proposal",
-        category: "Other",
-        image,
-        approvalQuorum: 60,
-        approvalThreshold: 80,
-        slashingQuorum: 60,
-        slashingThreshold: 80,
-      }
-    }
-    case "SetElectionParameters": {
-      return {
-        description: "Set Election Parameters Proposal",
-        category: "Council",
-        image,
-        approvalQuorum: 66,
-        approvalThreshold: 80,
-        slashingQuorum: 60,
-        slashingThreshold: 80,
-      }
-    }
-    case "RuntimeUpgrade": {
-      return {
-        description: "Runtime Upgrade Proposal",
-        category: "Other",
-        image,
-        approvalQuorum: 80,
-        approvalThreshold: 100,
-        slashingQuorum: 60,
-        slashingThreshold: 80,
-      }
-    }
-    default: {
-      throw new Error("'Proposal Type is invalid. Can't calculate metadata.");
-    }
-  }
-}

+ 3 - 0
pioneer/packages/joy-utils/src/consts/members.ts

@@ -0,0 +1,3 @@
+import BN from 'bn.js';
+
+export const FIRST_MEMBER_ID = new BN(0);

+ 87 - 0
pioneer/packages/joy-utils/src/consts/proposals.ts

@@ -0,0 +1,87 @@
+import { ProposalType, ProposalMeta } from "../types/proposals";
+
+const metadata: { [k in ProposalType]: ProposalMeta } = {
+  EvictStorageProvider: {
+      description: "Evicting Storage Provider Proposal",
+      category: "Storage",
+      stake: 25000,
+      approvalQuorum: 50,
+      approvalThreshold: 75,
+      slashingQuorum: 60,
+      slashingThreshold: 80
+  },
+  Text: {
+      description: "Signal Proposal",
+      category: "Other",
+      stake: 25000,
+      approvalQuorum: 60,
+      approvalThreshold: 80,
+      slashingQuorum: 60,
+      slashingThreshold: 80
+  },
+  SetStorageRoleParameters: {
+      description: "Set Storage Role Params Proposal",
+      category: "Storage",
+      stake: 100000,
+      approvalQuorum: 66,
+      approvalThreshold: 80,
+      slashingQuorum: 60,
+      slashingThreshold: 80
+  },
+  SetValidatorCount: {
+      description: "Set Max Validator Count Proposal",
+      category: "Validators",
+      stake: 100000,
+      approvalQuorum: 66,
+      approvalThreshold: 80,
+      slashingQuorum: 60,
+      slashingThreshold: 80
+  },
+  SetLead: {
+      description: "Set Lead Proposal",
+      category: "Content Working Group",
+      stake: 50000,
+      approvalQuorum: 60,
+      approvalThreshold: 75,
+      slashingQuorum: 60,
+      slashingThreshold: 80
+  },
+  SetContentWorkingGroupMintCapacity: {
+      description: "Set WG Mint Capacity Proposal",
+      category: "Content Working Group",
+      stake: 50000,
+      approvalQuorum: 60,
+      approvalThreshold: 75,
+      slashingQuorum: 60,
+      slashingThreshold: 80
+  },
+  Spending: {
+      description: "Spending Proposal",
+      category: "Other",
+      stake: 25000,
+      approvalQuorum: 60,
+      approvalThreshold: 80,
+      slashingQuorum: 60,
+      slashingThreshold: 80
+  },
+  SetElectionParameters: {
+      description: "Set Election Parameters Proposal",
+      category: "Council",
+      stake: 200000,
+      approvalQuorum: 66,
+      approvalThreshold: 80,
+      slashingQuorum: 60,
+      slashingThreshold: 80
+  },
+  RuntimeUpgrade: {
+      description: "Runtime Upgrade Proposal",
+      category: "Other",
+      stake: 1000000,
+      approvalQuorum: 80,
+      approvalThreshold: 100,
+      slashingQuorum: 60,
+      slashingThreshold: 80
+  },
+}
+
+export default metadata;

+ 8 - 0
pioneer/packages/joy-utils/src/functions/misc.ts

@@ -0,0 +1,8 @@
+export function includeKeys<T extends { [k: string]: any }>(obj: T, ...allowedKeys: string[]) {
+  return Object.keys(obj).filter(objKey => {
+    return allowedKeys.reduce(
+      (hasAllowed: boolean, allowedKey: string) => hasAllowed || objKey.includes(allowedKey),
+      false
+    );
+  });
+}

+ 49 - 0
pioneer/packages/joy-utils/src/react/components/PromiseComponent.tsx

@@ -0,0 +1,49 @@
+import React from 'react';
+import { Container, Message, Loader } from "semantic-ui-react";
+
+
+type ErrorProps = {
+  error: any;
+};
+
+export function Error({ error }: ErrorProps) {
+  console.error(error);
+  return (
+    <Container style={{ display: "flex", justifyContent: "center", alignItems: "center" }}>
+      <Message negative>
+        <Message.Header>Oops! We got an error!</Message.Header>
+        <p>{error.message}</p>
+      </Message>
+    </Container>
+  );
+}
+
+type LoadingProps = {
+  text: string;
+};
+
+export function Loading({ text }: LoadingProps) {
+  return (
+    <Container style={{ display: "flex", justifyContent: "center", alignItems: "center" }}>
+      <Loader active inline>{text}</Loader>
+    </Container>
+  );
+}
+
+
+type PromiseComponentProps = {
+  loading: boolean,
+  error: any,
+  message: string,
+}
+const PromiseComponent: React.FunctionComponent<PromiseComponentProps> = ({ loading, error, message, children }) => {
+  if (loading && !error) {
+    return <Loading text={ message }/>;
+  } else if (error) {
+    return <Error error={error} />;
+  }
+
+  return <>{ children }</>;
+}
+
+export default PromiseComponent;

+ 1 - 0
pioneer/packages/joy-utils/src/react/components/index.tsx

@@ -0,0 +1 @@
+export { default as PromiseComponent } from "./PromiseComponent";

+ 1 - 0
pioneer/packages/joy-utils/src/react/context/index.tsx

@@ -0,0 +1 @@
+export { TransportContext, TransportProvider } from "./transport";

+ 21 - 0
pioneer/packages/joy-utils/src/react/context/transport.tsx

@@ -0,0 +1,21 @@
+import React, { createContext, useContext } from "react";
+import { ApiContext } from "@polkadot/react-api";
+import { ApiProps } from "@polkadot/react-api/types";
+
+import Transport from "../../transport";
+
+export const TransportContext = createContext<Transport>((null as unknown) as Transport);
+
+export function TransportProvider({ children }: { children: React.PropsWithChildren<{}> }) {
+  const api: ApiProps = useContext(ApiContext);
+
+  if (!api) {
+    throw new Error("Cannot create Transport: A Substrate API is required");
+  } else if (!api.isApiReady) {
+    throw new Error("Cannot create Transport: The Substrate API is not ready yet.");
+  }
+
+  const transport = new Transport(api.api);
+
+  return <TransportContext.Provider value={transport}>{children}</TransportContext.Provider>;
+}

+ 3 - 0
pioneer/packages/joy-utils/src/react/hooks/index.tsx

@@ -0,0 +1,3 @@
+export { default as usePromise } from "./usePromise";
+export { default as useTransport } from "./useTransport";
+export { default as useProposalSubscription } from "./proposals/useProposalSubscription";

+ 56 - 0
pioneer/packages/joy-utils/src/react/hooks/proposals/useProposalSubscription.tsx

@@ -0,0 +1,56 @@
+import { useState, useEffect } from "react";
+import { ParsedProposal, ProposalVote } from "../../../types/proposals";
+import { useTransport, usePromise } from "../";
+import { ProposalId } from "@joystream/types/proposals";
+
+// Take advantage of polkadot api subscriptions to re-fetch proposal data and votes
+// each time there is some runtime change in the proposal
+const useProposalSubscription = (id: ProposalId) => {
+  const transport = useTransport();
+  // State holding an "unsubscribe method"
+  const [unsubscribeProposal, setUnsubscribeProposal] = useState<(() => void) | null>(null);
+
+  const [proposal, proposalError, proposalLoading, refreshProposal] = usePromise<ParsedProposal>(
+    () => transport.proposals.proposalById(id),
+    {} as ParsedProposal
+  );
+
+  const [votes, votesError, votesLoading, refreshVotes] = usePromise<ProposalVote[]>(
+    () => transport.proposals.votes(id),
+    []
+  );
+
+  // Function to re-fetch the data using transport
+  const refreshProposalData = () => {
+    refreshProposal();
+    refreshVotes();
+  }
+
+  useEffect(() => {
+    // onMount...
+    let unmounted = false;
+    // Create the subscription
+    transport.proposals.subscribeProposal(id, refreshProposalData)
+      .then(unsubscribe => {
+        if (!unmounted) {
+          setUnsubscribeProposal(() => unsubscribe);
+        }
+        else {
+          unsubscribe(); // If already unmounted - unsubscribe immedietally!
+        }
+      });
+    return () => {
+      // onUnmount...
+      // Clean the subscription
+      unmounted = true;
+      if (unsubscribeProposal !== null) unsubscribeProposal();
+    }
+  }, []);
+
+  return {
+    proposal: { data: proposal, error: proposalError, loading: proposalLoading },
+    votes: { data: votes, error: votesError, loading: votesLoading }
+  }
+};
+
+export default useProposalSubscription;

+ 26 - 0
pioneer/packages/joy-utils/src/react/hooks/usePromise.tsx

@@ -0,0 +1,26 @@
+import { useState, useEffect, useCallback } from "react";
+
+export default function usePromise<T>(promise: () => Promise<T>, defaultValue: T): [T, any, boolean, () => Promise<void|null>] {
+  const [state, setState] = useState<{
+    value: T;
+    error: any;
+    isPending: boolean;
+  }>({ value: defaultValue, error: null, isPending: true });
+
+  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;
+    };
+  }, []);
+
+  const { value, error, isPending } = state;
+  return [value, error, isPending, execute];
+}

+ 7 - 0
pioneer/packages/joy-utils/src/react/hooks/useTransport.tsx

@@ -0,0 +1,7 @@
+import { useContext } from 'react';
+import Transport from "../../transport";
+import { TransportContext } from "../context";
+
+export default function useTransport() {
+  return useContext(TransportContext) as Transport;
+}

+ 41 - 0
pioneer/packages/joy-utils/src/transport/base.ts

@@ -0,0 +1,41 @@
+import { ApiPromise } from "@polkadot/api";
+
+export default abstract class BaseTransport {
+  protected api: ApiPromise;
+
+  constructor(api: ApiPromise) {
+    this.api = api;
+  }
+
+  protected get proposalsEngine() {
+    return this.api.query.proposalsEngine;
+  }
+
+  protected get proposalsCodex() {
+    return this.api.query.proposalsCodex;
+  }
+
+  protected get members() {
+    return this.api.query.members;
+  }
+
+  protected get council() {
+    return this.api.query.council;
+  }
+
+  protected get councilElection() {
+    return this.api.query.councilElection;
+  }
+
+  protected get actors() {
+    return this.api.query.actors;
+  }
+
+  protected get contentWorkingGroup() {
+    return this.api.query.contentWorkingGroup;
+  }
+
+  protected get minting() {
+    return this.api.query.minting;
+  }
+}

+ 20 - 0
pioneer/packages/joy-utils/src/transport/chain.ts

@@ -0,0 +1,20 @@
+import BaseTransport from './base';
+import { Moment } from "@polkadot/types/interfaces";
+
+export default class ChainTransport extends BaseTransport {
+  async blockHash(height: number): Promise<string> {
+    const blockHash = await this.api.rpc.chain.getBlockHash(height);
+
+    return blockHash.toString();
+  }
+
+  async blockTimestamp(height: number): Promise<Date> {
+    const blockTime = (await this.api.query.timestamp.now.at(await this.blockHash(height))) as Moment;
+
+    return new Date(blockTime.toNumber());
+  }
+
+  async bestBlock() {
+    return await this.api.derive.chain.bestNumber();
+  }
+}

+ 38 - 0
pioneer/packages/joy-utils/src/transport/contentWorkingGroup.ts

@@ -0,0 +1,38 @@
+import { MemberId, Profile, ActorInRole, RoleKeys, Role } from "@joystream/types/members";
+import { u128, Vec, Option } from "@polkadot/types/";
+import BaseTransport from "./base";
+import { MintId, Mint } from "@joystream/types/mint";
+import { LeadId } from "@joystream/types/content-working-group";
+import { ApiPromise } from "@polkadot/api";
+import MembersTransport from "./members";
+
+export default class ContentWorkingGroupTransport extends BaseTransport {
+  private membersT: MembersTransport;
+
+  constructor(api: ApiPromise, membersTransport: MembersTransport) {
+    super(api);
+    this.membersT = membersTransport;
+  }
+
+  async currentMintCap(): Promise<number> {
+    const WGMintId = (await this.contentWorkingGroup.mint()) as MintId;
+    const WGMint = (await this.minting.mints(WGMintId)) as Vec<Mint>;
+    return (WGMint[0].get("capacity") as u128).toNumber();
+  }
+
+  async currentLead(): Promise<{ id: number; profile: Profile } | null> {
+    const optLeadId = (await this.contentWorkingGroup.currentLeadId()) as Option<LeadId>;
+    const leadId = optLeadId.unwrapOr(null);
+
+    if (!leadId) return null;
+
+    const actorInRole = new ActorInRole({
+      role: new Role(RoleKeys.CuratorLead),
+      actor_id: leadId
+    });
+    const memberId = (await this.members.membershipIdByActorInRole(actorInRole)) as MemberId;
+    const profile = (await this.membersT.memberProfile(memberId)).unwrapOr(null);
+
+    return profile && { id: memberId.toNumber(), profile };
+  }
+}

+ 78 - 0
pioneer/packages/joy-utils/src/transport/council.ts

@@ -0,0 +1,78 @@
+import { ParsedMember } from "../types/members";
+import BaseTransport from './base';
+import { Seats, ElectionParameters } from "@joystream/types/proposals";
+import { MemberId, Profile } from "@joystream/types/members";
+import { u32, Vec } from "@polkadot/types/";
+import { Balance, BlockNumber } from "@polkadot/types/interfaces";
+import { FIRST_MEMBER_ID } from "../consts/members";
+import { ApiPromise } from "@polkadot/api";
+import MembersTransport from "./members";
+
+export default class CouncilTransport extends BaseTransport {
+  private membersT: MembersTransport;
+
+  constructor(api: ApiPromise, membersTransport: MembersTransport) {
+    super(api);
+    this.membersT = membersTransport;
+  }
+
+  async councilMembers(): Promise<(ParsedMember & { memberId: MemberId })[]> {
+    const council = (await this.council.activeCouncil()) as Seats;
+    return Promise.all(
+      council.map(async seat => {
+        const memberIds = (await this.members.memberIdsByControllerAccountId(seat.member)) as Vec<MemberId>;
+        const member = (await this.membersT.memberProfile(memberIds[0])).toJSON() as ParsedMember;
+        return {
+          ...member,
+          memberId: memberIds[0]
+        };
+      })
+    );
+  }
+
+  async membersExceptCouncil(): Promise<{ id: number; profile: Profile }[]> {
+    // Council members to filter out
+    const activeCouncil = (await this.council.activeCouncil()) as Seats;
+    const membersCount = ((await this.members.membersCreated()) as MemberId).toNumber();
+    const profiles: { id: number; profile: Profile }[] = [];
+    for (let id = FIRST_MEMBER_ID.toNumber(); id < membersCount; ++id) {
+      const profile = (await this.membersT.memberProfile(new MemberId(id))).unwrapOr(null);
+      if (
+        !profile ||
+        // Filter out council members
+        activeCouncil.some(
+          seat =>
+            seat.member.toString() === profile.controller_account.toString() ||
+            seat.member.toString() === profile.root_account.toString()
+        )
+      ) {
+        continue;
+      }
+      profiles.push({ id, profile });
+    }
+
+    return profiles;
+  }
+
+  async electionParameters(): Promise<ElectionParameters> {
+    const announcing_period = (await this.councilElection.announcingPeriod()) as BlockNumber;
+    const voting_period = (await this.councilElection.votingPeriod()) as BlockNumber;
+    const revealing_period = (await this.councilElection.revealingPeriod()) as BlockNumber;
+    const new_term_duration = (await this.councilElection.newTermDuration()) as BlockNumber;
+    const min_council_stake = (await this.councilElection.minCouncilStake()) as Balance;
+    const min_voting_stake = (await this.councilElection.minVotingStake()) as Balance;
+    const candidacy_limit = (await this.councilElection.candidacyLimit()) as u32;
+    const council_size = (await this.councilElection.councilSize()) as u32;
+
+    return new ElectionParameters({
+      announcing_period,
+      voting_period,
+      revealing_period,
+      new_term_duration,
+      min_council_stake,
+      min_voting_stake,
+      candidacy_limit,
+      council_size
+    });
+  }
+}

+ 31 - 0
pioneer/packages/joy-utils/src/transport/index.ts

@@ -0,0 +1,31 @@
+import { ApiPromise } from "@polkadot/api";
+import ChainTransport from "./chain";
+import ContentWorkingGroupTransport from "./contentWorkingGroup";
+import ProposalsTransport from "./proposals";
+import MembersTransport from "./members";
+import CouncilTransport from "./council";
+import StorageProvidersTransport from "./storageProviders";
+import ValidatorsTransport from "./validators";
+
+export default class Transport {
+  protected api: ApiPromise;
+  // Specific transports
+  public chain: ChainTransport;
+  public members: MembersTransport;
+  public council: CouncilTransport;
+  public proposals: ProposalsTransport;
+  public contentWorkingGroup: ContentWorkingGroupTransport;
+  public storageProviders: StorageProvidersTransport;
+  public validators: ValidatorsTransport;
+
+  constructor(api: ApiPromise) {
+    this.api = api;
+    this.chain = new ChainTransport(api);
+    this.members = new MembersTransport(api);
+    this.storageProviders = new StorageProvidersTransport(api);
+    this.validators = new ValidatorsTransport(api);
+    this.council = new CouncilTransport(api, this.members);
+    this.contentWorkingGroup = new ContentWorkingGroupTransport(api, this.members);
+    this.proposals = new ProposalsTransport(api, this.members, this.chain, this.council);
+  }
+}

+ 9 - 0
pioneer/packages/joy-utils/src/transport/members.ts

@@ -0,0 +1,9 @@
+import BaseTransport from './base';
+import { MemberId, Profile } from "@joystream/types/members";
+import { Option } from "@polkadot/types/";
+
+export default class MembersTransport extends BaseTransport {
+  memberProfile(id: MemberId | number): Promise<Option<Profile>> {
+    return this.members.memberProfile(id) as Promise<Option<Profile>>;
+  }
+}

+ 177 - 0
pioneer/packages/joy-utils/src/transport/proposals.ts

@@ -0,0 +1,177 @@
+import {
+  ParsedProposal,
+  ProposalType,
+  ProposalTypes,
+  ProposalVote
+} from "../types/proposals";
+import { ParsedMember } from "../types/members";
+
+import BaseTransport from './base';
+
+import { Proposal, ProposalId, VoteKind } from "@joystream/types/proposals";
+import { MemberId } from "@joystream/types/members";
+import { u32 } from "@polkadot/types/";
+import { BalanceOf } from "@polkadot/types/interfaces";
+
+import { includeKeys } from "../functions/misc";
+import _ from 'lodash';
+import proposalsConsts from "../consts/proposals"
+
+import { ApiPromise } from "@polkadot/api";
+import MembersTransport from "./members";
+import ChainTransport from "./chain";
+import CouncilTransport from "./council";
+
+export default class ProposalsTransport extends BaseTransport {
+  private membersT: MembersTransport;
+  private chainT: ChainTransport;
+  private councilT: CouncilTransport;
+
+  constructor(
+    api: ApiPromise,
+    membersTransport: MembersTransport,
+    chainTransport: ChainTransport,
+    councilTransport: CouncilTransport
+  ) {
+    super(api);
+    this.membersT = membersTransport;
+    this.chainT = chainTransport;
+    this.councilT = councilTransport;
+  }
+
+  proposalCount() {
+    return this.proposalsEngine.proposalCount<u32>();
+  }
+
+  rawProposalById(id: ProposalId) {
+    return this.proposalsEngine.proposals<Proposal>(id);
+  }
+
+  proposalDetailsById(id: ProposalId) {
+    return this.proposalsCodex.proposalDetailsByProposalId(id);
+  }
+
+  async cancellationFee(): Promise<number> {
+    return ((await this.api.consts.proposalsEngine.cancellationFee) as BalanceOf).toNumber();
+  }
+
+  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 = Array.isArray(rawDetails[type]) ? rawDetails[type] : [rawDetails[type]];
+    const rawProposal = await this.rawProposalById(id);
+    const proposer = (await this.membersT.memberProfile(rawProposal.proposerId)).toJSON() as ParsedMember;
+    const proposal = rawProposal.toJSON() as {
+      title: string;
+      description: string;
+      parameters: any;
+      votingResults: any;
+      proposerId: number;
+      status: any;
+    };
+    const createdAtBlock = rawProposal.createdAt;
+    const createdAt = await this.chainT.blockTimestamp(createdAtBlock.toNumber());
+    const cancellationFee = await this.cancellationFee();
+
+    return {
+      id,
+      ...proposal,
+      details,
+      type,
+      proposer,
+      createdAtBlock: createdAtBlock.toJSON(),
+      createdAt,
+      cancellationFee
+    };
+  }
+
+  async proposalsIds() {
+    const total: number = (await this.proposalCount()).toNumber();
+    return Array.from({ length: total }, (_, i) => new ProposalId(i + 1));
+  }
+
+  async proposals() {
+    const ids = await this.proposalsIds();
+    return Promise.all(ids.map(id => this.proposalById(id)));
+  }
+
+  async activeProposals() {
+    const activeProposalIds = await this.proposalsEngine.activeProposalIds<ProposalId[]>();
+
+    return Promise.all(activeProposalIds.map(id => this.proposalById(id)));
+  }
+
+  async proposedBy(member: MemberId) {
+    const proposals = await this.proposals();
+    return proposals.filter(({ proposerId }) => member.eq(proposerId));
+  }
+
+  async proposalDetails(id: ProposalId) {
+    return this.proposalsCodex.proposalDetailsByProposalId(id);
+  }
+
+  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;
+  }
+
+  async votes(proposalId: ProposalId): Promise<ProposalVote[]> {
+    const councilMembers = await this.councilT.councilMembers();
+    return Promise.all(
+      councilMembers.map(async member => {
+        const vote = await this.voteByProposalAndMember(proposalId, member.memberId);
+        return {
+          vote,
+          member
+        };
+      })
+    );
+  }
+
+  async fetchProposalMethodsFromCodex(includeKey: string) {
+    const methods = includeKeys(this.proposalsCodex, includeKey);
+    // 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 = _.words(_.startCase(method))
+        .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 proposalTypesGracePeriod(): Promise<{ [k in ProposalType]: number }> {
+    return this.fetchProposalMethodsFromCodex("GracePeriod");
+  }
+
+  async proposalTypesVotingPeriod(): Promise<{ [k in ProposalType]: number }> {
+    return this.fetchProposalMethodsFromCodex("VotingPeriod");
+  }
+
+  async parametersFromProposalType(type: ProposalType) {
+    const votingPeriod = (await this.proposalTypesVotingPeriod())[type];
+    const gracePeriod = (await this.proposalTypesGracePeriod())[type];
+    // Currently it's same for all types, but this will change soon
+    const cancellationFee = await this.cancellationFee();
+    return {
+      type,
+      votingPeriod,
+      gracePeriod,
+      cancellationFee,
+      ...proposalsConsts[type]
+    };
+  }
+
+  async proposalsTypesParameters() {
+    return Promise.all(ProposalTypes.map(type => this.parametersFromProposalType(type)));
+  }
+
+  async subscribeProposal(id: number|ProposalId, callback: () => void) {
+    return this.proposalsEngine.proposals(id, callback);
+  }
+}

+ 19 - 0
pioneer/packages/joy-utils/src/transport/storageProviders.ts

@@ -0,0 +1,19 @@
+import BaseTransport from './base';
+import { IStorageRoleParameters } from "../types/storageProviders";
+import { RoleKeys } from "@joystream/types/members";
+import { Vec } from "@polkadot/types/";
+import { AccountId } from "@polkadot/types/interfaces";
+
+export default class StorageProvidersTransport extends BaseTransport {
+  async roleParameters(): Promise<IStorageRoleParameters> {
+    const params = (
+      await this.api.query.actors.parameters(RoleKeys.StorageProvider)
+    ).toJSON() as IStorageRoleParameters;
+    return params;
+  }
+
+  async providers(): Promise<AccountId[]> {
+    const providers = (await this.actors.accountIdsByRole(RoleKeys.StorageProvider)) as Vec<AccountId>;
+    return providers.toArray();
+  }
+}

+ 9 - 0
pioneer/packages/joy-utils/src/transport/validators.ts

@@ -0,0 +1,9 @@
+import BaseTransport from './base';
+import { u32 } from "@polkadot/types/";
+
+export default class ValidatorsTransport extends BaseTransport {
+  async maxCount(): Promise<number> {
+    const count = ((await this.api.query.staking.validatorCount()) as u32).toNumber();
+    return count;
+  }
+}

+ 13 - 0
pioneer/packages/joy-utils/src/types/members.ts

@@ -0,0 +1,13 @@
+export type ParsedMember = {
+  about: string;
+  avatar_uri: string;
+  handle: string;
+  registered_at_block: number;
+  registered_at_time: number;
+  roles: any[];
+  entry: { [k: string]: any };
+  root_account: string;
+  controller_account: string;
+  subscription: any;
+  suspended: boolean;
+};

+ 20 - 33
pioneer/packages/joy-proposals/src/runtime/transport.ts → pioneer/packages/joy-utils/src/types/proposals.ts

@@ -1,5 +1,7 @@
 import { ProposalId, VoteKind } from "@joystream/types/proposals";
 import { MemberId } from "@joystream/types/members";
+import { ParsedMember } from "./members";
+
 export const ProposalTypes = [
   "Text",
   "RuntimeUpgrade",
@@ -14,20 +16,6 @@ export const ProposalTypes = [
 
 export type ProposalType = typeof ProposalTypes[number];
 
-export type ParsedMember = {
-  about: string;
-  avatar_uri: string;
-  handle: string;
-  registered_at_block: number;
-  registered_at_time: number;
-  roles: any[];
-  entry: { [k: string]: any };
-  root_account: string;
-  controller_account: string;
-  subscription: any;
-  suspended: boolean;
-};
-
 export type ParsedProposal = {
   id: ProposalId;
   type: ProposalType;
@@ -52,28 +40,27 @@ export type ParsedProposal = {
   cancellationFee: number;
 };
 
-export const StorageRoleParameters = [
-  "min_stake",
-  "min_actors",
-  "max_actors",
-  "reward",
-  "reward_period",
-  "bonding_period",
-  "unbonding_period",
-  "min_service_period",
-  "startup_grace_period",
-  "entry_request_fee"
-] as const;
-
-export type IStorageRoleParameters = {
-  [k in typeof StorageRoleParameters[number]]: number;
-};
-
 export type ProposalVote = {
   vote: VoteKind | null;
   member: ParsedMember & { memberId: MemberId };
 };
 
-export abstract class Transport {
-  abstract proposals(): Promise<ParsedProposal[]>;
+export const Categories = {
+  storage: "Storage",
+  council: "Council",
+  validators: "Validators",
+  cwg: "Content Working Group",
+  other: "Other"
+} as const;
+
+export type Category = typeof Categories[keyof typeof Categories];
+
+export type ProposalMeta = {
+  description: string;
+  category: Category;
+  stake: number;
+  approvalQuorum: number;
+  approvalThreshold: number;
+  slashingQuorum: number;
+  slashingThreshold: number;
 }

+ 16 - 0
pioneer/packages/joy-utils/src/types/storageProviders.ts

@@ -0,0 +1,16 @@
+export const StorageRoleParameters = [
+  "min_stake",
+  "min_actors",
+  "max_actors",
+  "reward",
+  "reward_period",
+  "bonding_period",
+  "unbonding_period",
+  "min_service_period",
+  "startup_grace_period",
+  "entry_request_fee"
+] as const;
+
+export type IStorageRoleParameters = {
+  [k in typeof StorageRoleParameters[number]]: number;
+};