Jelajahi Sumber

joy-proposals - extract potentially reusable code into joy-utils

Leszek Wiesner 4 tahun lalu
induk
melakukan
0eb04cc115
43 mengubah file dengan 945 tambahan dan 87 penghapusan
  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. 2 2
      pioneer/packages/joy-proposals/src/Proposal/ProposalDetails.tsx
  5. 10 10
      pioneer/packages/joy-proposals/src/Proposal/ProposalFromId.tsx
  6. 1 1
      pioneer/packages/joy-proposals/src/Proposal/ProposalPreview.tsx
  7. 4 4
      pioneer/packages/joy-proposals/src/Proposal/ProposalPreviewList.tsx
  8. 2 2
      pioneer/packages/joy-proposals/src/Proposal/ProposalTypePreview.tsx
  9. 1 1
      pioneer/packages/joy-proposals/src/Proposal/Votes.tsx
  10. 2 3
      pioneer/packages/joy-proposals/src/Proposal/VotingSection.tsx
  11. 2 3
      pioneer/packages/joy-proposals/src/forms/EvictStorageProviderForm.tsx
  12. 3 3
      pioneer/packages/joy-proposals/src/forms/GenericProposalForm.tsx
  13. 1 1
      pioneer/packages/joy-proposals/src/forms/MintCapacityForm.tsx
  14. 4 5
      pioneer/packages/joy-proposals/src/forms/SetContentWorkingGroupLeadForm.tsx
  15. 3 4
      pioneer/packages/joy-proposals/src/forms/SetContentWorkingGroupMintCapForm.tsx
  16. 4 4
      pioneer/packages/joy-proposals/src/forms/SetCouncilParamsForm.tsx
  17. 2 3
      pioneer/packages/joy-proposals/src/forms/SetMaxValidatorCountForm.tsx
  18. 3 3
      pioneer/packages/joy-proposals/src/forms/SetStorageRoleParamsForm.tsx
  19. 3 3
      pioneer/packages/joy-proposals/src/index.tsx
  20. 1 1
      pioneer/packages/joy-proposals/src/stories/data/ProposalDetails.mock.ts
  21. 3 0
      pioneer/packages/joy-utils/src/consts/members.ts
  22. 32 0
      pioneer/packages/joy-utils/src/functions/misc.ts
  23. 152 0
      pioneer/packages/joy-utils/src/functions/proposals.ts
  24. 49 0
      pioneer/packages/joy-utils/src/react/components/PromiseComponent.tsx
  25. 1 0
      pioneer/packages/joy-utils/src/react/components/index.tsx
  26. 1 0
      pioneer/packages/joy-utils/src/react/context/index.tsx
  27. 21 0
      pioneer/packages/joy-utils/src/react/context/transport.tsx
  28. 3 0
      pioneer/packages/joy-utils/src/react/hooks/index.tsx
  29. 56 0
      pioneer/packages/joy-utils/src/react/hooks/proposals/useProposalSubscription.tsx
  30. 26 0
      pioneer/packages/joy-utils/src/react/hooks/usePromise.tsx
  31. 7 0
      pioneer/packages/joy-utils/src/react/hooks/useTransport.tsx
  32. 41 0
      pioneer/packages/joy-utils/src/transport/base.ts
  33. 20 0
      pioneer/packages/joy-utils/src/transport/chain.ts
  34. 38 0
      pioneer/packages/joy-utils/src/transport/contentWorkingGroup.ts
  35. 78 0
      pioneer/packages/joy-utils/src/transport/council.ts
  36. 31 0
      pioneer/packages/joy-utils/src/transport/index.ts
  37. 9 0
      pioneer/packages/joy-utils/src/transport/members.ts
  38. 179 0
      pioneer/packages/joy-utils/src/transport/proposals.ts
  39. 19 0
      pioneer/packages/joy-utils/src/transport/storageProviders.ts
  40. 9 0
      pioneer/packages/joy-utils/src/transport/validators.ts
  41. 13 0
      pioneer/packages/joy-utils/src/types/members.ts
  42. 66 0
      pioneer/packages/joy-utils/src/types/proposals.ts
  43. 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';
 

+ 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);

+ 2 - 2
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 { slugify, splitOnUpperCase } from "@polkadot/joy-utils/functions/misc";
 import styled from 'styled-components';
 import useVoteStyles from './useVoteStyles';
 import { formatBalance } from "@polkadot/util";

+ 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 { calculateStake } from "@polkadot/joy-utils/functions/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());
+    calculateStake(proposalType);
 
   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...">

+ 4 - 4
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 { snakeCaseToCamelCase } from "@polkadot/joy-utils/functions/misc";
 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};

+ 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>
   );
 }
 

+ 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 = {

+ 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);

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

@@ -0,0 +1,32 @@
+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('_');
+}

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

@@ -0,0 +1,152 @@
+import { ProposalType, ProposalMeta } from "../types/proposals";
+
+// TODO: Those may actually be const objects (now that we don't need total issuance etc.)
+
+export function calculateStake(type: ProposalType) {
+  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}.`);
+    }
+  }
+  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.");
+    }
+  }
+}

+ 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>>;
+  }
+}

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

@@ -0,0 +1,179 @@
+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, splitOnUpperCase } from "../functions/misc";
+import { calculateStake, calculateMetaFromType } from "../functions/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 = 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 stake = calculateStake(type);
+    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 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;
+};

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

@@ -0,0 +1,66 @@
+import { ProposalId, VoteKind } from "@joystream/types/proposals";
+import { MemberId } from "@joystream/types/members";
+import { ParsedMember } from "./members";
+
+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;
+  type: ProposalType;
+  title: string;
+  description: string;
+  status: any;
+  proposer: ParsedMember;
+  proposerId: number;
+  createdAtBlock: number;
+  createdAt: Date;
+  details: any[];
+  votingResults: any;
+  parameters: {
+    approvalQuorumPercentage: number;
+    approvalThresholdPercentage: number;
+    gracePeriod: number;
+    requiredStake: number;
+    slashingQuorumPercentage: number;
+    slashingThresholdPercentage: number;
+    votingPeriod: number;
+  };
+  cancellationFee: number;
+};
+
+export type ProposalVote = {
+  vote: VoteKind | null;
+  member: ParsedMember & { memberId: MemberId };
+};
+
+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;
+  image: string;
+  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;
+};