Browse Source

Slash/Decrease lead stake

Leszek Wiesner 4 years ago
parent
commit
6d4ca61181

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

@@ -22,6 +22,7 @@ import {
 } from '@joystream/types/hiring';
 import { WorkingGroup, WorkingGroupKeys } from '@joystream/types/common';
 import { ApplicationsDetailsByOpening } from '@polkadot/joy-utils/react/components/working-groups/ApplicationDetails';
+import { LeadInfoFromId } from '@polkadot/joy-utils/react/components/working-groups/LeadInfo';
 
 type BodyProps = {
   title: string;
@@ -202,7 +203,17 @@ const paramParsers: { [x in ProposalType]: (params: any[]) => { [key: string]: s
         )
         : 'NONE'
     };
-  }
+  },
+  SlashWorkingGroupLeaderStake: ([leadId, amount, group]) => ({
+    'Working group': (new WorkingGroup(group)).type,
+    Lead: <LeadInfoFromId group={(new WorkingGroup(group).type as WorkingGroupKeys)} leadId={leadId}/>,
+    'Slash amount': formatBalance(amount)
+  }),
+  DecreaseWorkingGroupLeaderStake: ([leadId, amount, group]) => ({
+    'Working group': (new WorkingGroup(group)).type,
+    Lead: <LeadInfoFromId group={(new WorkingGroup(group).type as WorkingGroupKeys)} leadId={leadId}/>,
+    'Decrease amount': formatBalance(amount)
+  })
 };
 
 const StyledProposalDescription = styled(Card.Description)`

+ 102 - 0
pioneer/packages/joy-proposals/src/forms/DecreaseWorkingGroupLeadStakeForm.tsx

@@ -0,0 +1,102 @@
+import React, { useState, useEffect } from 'react';
+import { getFormErrorLabelsProps } from './errorHandling';
+import * as Yup from 'yup';
+import {
+  withProposalFormData,
+  ProposalFormExportProps,
+  ProposalFormContainerProps,
+  ProposalFormInnerProps,
+  genericFormDefaultOptions
+} from './GenericProposalForm';
+import {
+  GenericWorkingGroupProposalForm,
+  FormValues as WGFormValues,
+  defaultValues as wgFromDefaultValues
+} from './GenericWorkingGroupProposalForm';
+import { InputFormField } from './FormFields';
+import { withFormContainer } from './FormContainer';
+import './forms.css';
+import { Grid } from 'semantic-ui-react';
+import { formatBalance } from '@polkadot/util';
+import _ from 'lodash';
+import Validation from '../validationSchema';
+import { LeadData } from '@polkadot/joy-utils/types/workingGroups';
+
+export type FormValues = WGFormValues & {
+  amount: string;
+};
+
+const defaultValues: FormValues = {
+  ...wgFromDefaultValues,
+  amount: ''
+};
+
+type FormAdditionalProps = {}; // Aditional props coming all the way from export component into the inner form.
+type ExportComponentProps = ProposalFormExportProps<FormAdditionalProps, FormValues>;
+type FormContainerProps = ProposalFormContainerProps<ExportComponentProps>;
+type FormInnerProps = ProposalFormInnerProps<FormContainerProps, FormValues>;
+
+const DecreaseWorkingGroupLeadStakeForm: React.FunctionComponent<FormInnerProps> = props => {
+  const { handleChange, errors, touched, values, myMemberId, setFieldError } = props;
+  const errorLabelsProps = getFormErrorLabelsProps<FormValues>(errors, touched);
+  const [lead, setLead] = useState<LeadData | null>(null);
+
+  // Here we validate if stake <= current lead stake.
+  // Because it depends on selected working group,
+  // there's no easy way to do it using validationSchema
+  useEffect(() => {
+    if (lead && parseInt(values.amount) > (lead.stake || 0) && !errors.amount) {
+      setFieldError('amount', `The stake cannot exceed current leader's stake (${formatBalance(lead.stake)})`);
+    }
+  });
+
+  return (
+    <GenericWorkingGroupProposalForm
+      {...props}
+      txMethod="createDecreaseWorkingGroupLeaderStakeProposal"
+      proposalType="DecreaseWorkingGroupLeaderStake"
+      leadRequired={true}
+      leadStakeRequired={true}
+      onLeadChange={(lead: LeadData | null) => setLead(lead)}
+      submitParams={[
+        myMemberId,
+        values.title,
+        values.rationale,
+        '{STAKE}',
+        lead?.workerId,
+        values.amount,
+        values.workingGroup
+      ]}
+    >
+      { (lead && lead.stake) && (
+        <Grid columns="4" doubling stackable verticalAlign="bottom">
+          <Grid.Column>
+            <InputFormField
+              label="Amount to decrease"
+              onChange={handleChange}
+              name="amount"
+              error={errorLabelsProps.amount}
+              value={values.amount}
+              unit={formatBalance.getDefaults().unit}
+            />
+          </Grid.Column>
+        </Grid>
+      ) }
+    </GenericWorkingGroupProposalForm>
+  );
+};
+
+const FormContainer = withFormContainer<FormContainerProps, FormValues>({
+  mapPropsToValues: (props: FormContainerProps) => ({
+    ...defaultValues,
+    ...(props.initialData || {})
+  }),
+  validationSchema: Yup.object().shape({
+    ...genericFormDefaultOptions.validationSchema,
+    ...Validation.DecreaseWorkingGroupLeaderStake()
+  }),
+  handleSubmit: genericFormDefaultOptions.handleSubmit,
+  displayName: 'DecreaseWorkingGroupLeadStakeForm'
+})(DecreaseWorkingGroupLeadStakeForm);
+
+export default withProposalFormData<FormContainerProps, ExportComponentProps>(FormContainer);

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

@@ -49,6 +49,7 @@ type GenericProposalFormAdditionalProps = {
   txMethod?: string;
   submitParams?: any[];
   proposalType?: ProposalType;
+  disabled?: boolean;
 };
 
 type GenericFormContainerProps = ProposalFormContainerProps<
@@ -93,7 +94,8 @@ export const GenericProposalForm: React.FunctionComponent<GenericFormInnerProps>
     setSubmitting,
     history,
     balances_totalIssuance,
-    proposalType
+    proposalType,
+    disabled = false
   } = props;
   const errorLabelsProps = getFormErrorLabelsProps<GenericFormValues>(errors, touched);
   const [afterSubmit, setAfterSubmit] = useState(null as (() => () => void) | null);
@@ -179,7 +181,7 @@ export const GenericProposalForm: React.FunctionComponent<GenericFormInnerProps>
               type="button" // Tx button uses custom submit handler - "onTxButtonClick"
               label="Submit proposal"
               icon="paper plane"
-              isDisabled={isSubmitting}
+              isDisabled={disabled || isSubmitting}
               params={(submitParams || []).map(p => (p === '{STAKE}' ? requiredStake : p))}
               tx={`proposalsCodex.${txMethod}`}
               txFailedCb={onTxFailed}

+ 24 - 12
pioneer/packages/joy-proposals/src/forms/GenericWorkingGroupProposalForm.tsx

@@ -15,7 +15,8 @@ import './forms.css';
 import { Dropdown, Message } from 'semantic-ui-react';
 import { usePromise, useTransport } from '@polkadot/joy-utils/react/hooks';
 import { PromiseComponent } from '@polkadot/joy-utils/react/components';
-import { ProfilePreviewFromStruct as MemberPreview } from '@polkadot/joy-utils/MemberProfilePreview';
+import { LeadData } from '@polkadot/joy-utils/types/workingGroups';
+import { LeadInfo } from '@polkadot/joy-utils/react/components/working-groups/LeadInfo';
 
 export type FormValues = GenericFormValues & {
   workingGroup: WorkingGroupKeys;
@@ -32,6 +33,9 @@ type FormAdditionalProps = {
   submitParams: any[];
   proposalType: ProposalType;
   showLead?: boolean;
+  leadRequired?: boolean;
+  leadStakeRequired?: boolean;
+  onLeadChange?: (lead: LeadData | null) => void;
 };
 
 // We don't exactly use "container" and "export" components here, but those types are useful for
@@ -41,18 +45,21 @@ type FormContainerProps = ProposalFormContainerProps<ExportComponentProps>;
 export type FormInnerProps = ProposalFormInnerProps<FormContainerProps, FormValues>;
 
 export const GenericWorkingGroupProposalForm: React.FunctionComponent<FormInnerProps> = props => {
-  const { handleChange, errors, touched, values, showLead = true } = props;
+  const { handleChange, errors, touched, values, showLead = true, leadRequired = false, leadStakeRequired = false, onLeadChange } = props;
   const transport = useTransport();
   const [lead, error, loading] = usePromise(
     () => transport.workingGroups.currentLead(values.workingGroup),
     null,
-    [values.workingGroup]
+    [values.workingGroup],
+    onLeadChange
   );
   const leadRes = { lead, error, loading };
+  const leadMissing = leadRequired && (!leadRes.loading && !leadRes.error) && !leadRes.lead;
+  const stakeMissing = leadStakeRequired && (!leadRes.loading && !leadRes.error) && (leadRes.lead && !leadRes.lead.stake);
 
   const errorLabelsProps = getFormErrorLabelsProps<FormValues>(errors, touched);
   return (
-    <GenericProposalForm {...props}>
+    <GenericProposalForm {...props} disabled={leadMissing || stakeMissing || leadRes.error}>
       <FormField
         error={errorLabelsProps.workingGroup}
         label="Working group"
@@ -68,16 +75,21 @@ export const GenericWorkingGroupProposalForm: React.FunctionComponent<FormInnerP
       </FormField>
       { showLead && (
         <PromiseComponent message={'Fetching current lead...'} {...leadRes}>
-          <Message info>
-            <Message.Content>
-              <Message.Header>Current {values.workingGroup} Working Group lead:</Message.Header>
-              <div style={{ padding: '0.5rem 0' }}>
-                { leadRes.lead ? <MemberPreview profile={leadRes.lead.profile} /> : 'NONE' }
-              </div>
-            </Message.Content>
-          </Message>
+          <LeadInfo lead={leadRes.lead} header={true}/>
         </PromiseComponent>
       ) }
+      { leadMissing && (
+        <Message error visible>
+          <Message.Header>Leader required</Message.Header>
+          Selected working group has no active leader. An active leader is required in order to create this proposal.
+        </Message>
+      ) }
+      { stakeMissing && (
+        <Message error visible>
+          <Message.Header>No role stake</Message.Header>
+          Selected working group leader has no associated role stake, which is required to create this proposal.
+        </Message>
+      ) }
       { props.children }
     </GenericProposalForm>
   );

+ 101 - 0
pioneer/packages/joy-proposals/src/forms/SlashWorkingGroupLeadStakeForm.tsx

@@ -0,0 +1,101 @@
+import React, { useState, useEffect } from 'react';
+import { getFormErrorLabelsProps } from './errorHandling';
+import * as Yup from 'yup';
+import {
+  withProposalFormData,
+  ProposalFormExportProps,
+  ProposalFormContainerProps,
+  ProposalFormInnerProps,
+  genericFormDefaultOptions
+} from './GenericProposalForm';
+import {
+  GenericWorkingGroupProposalForm,
+  FormValues as WGFormValues,
+  defaultValues as wgFromDefaultValues
+} from './GenericWorkingGroupProposalForm';
+import { InputFormField } from './FormFields';
+import { withFormContainer } from './FormContainer';
+import './forms.css';
+import { Grid } from 'semantic-ui-react';
+import { formatBalance } from '@polkadot/util';
+import _ from 'lodash';
+import Validation from '../validationSchema';
+import { LeadData } from '@polkadot/joy-utils/types/workingGroups';
+
+export type FormValues = WGFormValues & {
+  amount: string;
+};
+
+const defaultValues: FormValues = {
+  ...wgFromDefaultValues,
+  amount: ''
+};
+
+type FormAdditionalProps = {}; // Aditional props coming all the way from export component into the inner form.
+type ExportComponentProps = ProposalFormExportProps<FormAdditionalProps, FormValues>;
+type FormContainerProps = ProposalFormContainerProps<ExportComponentProps>;
+type FormInnerProps = ProposalFormInnerProps<FormContainerProps, FormValues>;
+
+const SlashWorkingGroupLeadStakeForm: React.FunctionComponent<FormInnerProps> = props => {
+  const { handleChange, errors, touched, values, myMemberId, setFieldError } = props;
+  const errorLabelsProps = getFormErrorLabelsProps<FormValues>(errors, touched);
+  const [lead, setLead] = useState<LeadData | null>(null);
+  // Here we validate if stake <= current lead stake.
+  // Because it depends on selected working group,
+  // there's no easy way to do it using validationSchema
+  useEffect(() => {
+    if (lead && parseInt(values.amount) > (lead.stake || 0) && !errors.amount) {
+      setFieldError('amount', `The stake cannot exceed current leader's stake (${formatBalance(lead.stake)})`);
+    }
+  });
+
+  return (
+    <GenericWorkingGroupProposalForm
+      {...props}
+      txMethod="createSlashWorkingGroupLeaderStakeProposal"
+      proposalType="SlashWorkingGroupLeaderStake"
+      leadRequired={true}
+      leadStakeRequired={true}
+      onLeadChange={(lead: LeadData | null) => setLead(lead)}
+      submitParams={[
+        myMemberId,
+        values.title,
+        values.rationale,
+        '{STAKE}',
+        lead?.workerId,
+        values.amount,
+        values.workingGroup
+      ]}
+    >
+      { (lead && lead.stake) && (
+        <Grid columns="4" doubling stackable verticalAlign="bottom">
+          <Grid.Column>
+            <InputFormField
+              label="Amount to slash"
+              onChange={handleChange}
+              name="amount"
+              error={errorLabelsProps.amount}
+              value={values.amount}
+              unit={formatBalance.getDefaults().unit}
+            />
+          </Grid.Column>
+        </Grid>
+      ) }
+    </GenericWorkingGroupProposalForm>
+  );
+};
+
+const FormContainer = withFormContainer<FormContainerProps, FormValues>({
+  mapPropsToValues: (props: FormContainerProps) => ({
+    ...defaultValues,
+    ...(props.initialData || {})
+  }),
+  validationSchema: Yup.object().shape({
+    ...genericFormDefaultOptions.validationSchema,
+    ...Validation.SlashWorkingGroupLeaderStake()
+  }),
+  handleSubmit: genericFormDefaultOptions.handleSubmit,
+  displayName: 'SlashWorkingGroupLeadStakeForm'
+})(SlashWorkingGroupLeadStakeForm);
+
+export default withProposalFormData<FormContainerProps, ExportComponentProps>(FormContainer);

+ 2 - 0
pioneer/packages/joy-proposals/src/forms/index.ts

@@ -11,3 +11,5 @@ export { default as AddWorkingGroupOpeningForm } from './AddWorkingGroupOpeningF
 export { default as SetWorkingGroupMintCapacityForm } from './SetWorkingGroupMintCapacityForm';
 export { default as BeginReviewLeaderApplicationsForm } from './BeginReviewLeaderApplicationsForm';
 export { default as FillWorkingGroupLeaderOpeningForm } from './FillWorkingGroupLeaderOpeningForm';
+export { default as DecreaseWorkingGroupLeadStakeFrom } from './DecreaseWorkingGroupLeadStakeForm';
+export { default as SlashWorkingGroupLeadStakeForm } from './SlashWorkingGroupLeadStakeForm';

+ 5 - 1
pioneer/packages/joy-proposals/src/index.tsx

@@ -23,7 +23,9 @@ import {
   AddWorkingGroupOpeningForm,
   SetWorkingGroupMintCapacityForm,
   BeginReviewLeaderApplicationsForm,
-  FillWorkingGroupLeaderOpeningForm
+  FillWorkingGroupLeaderOpeningForm,
+  DecreaseWorkingGroupLeadStakeFrom,
+  SlashWorkingGroupLeadStakeForm
 } from './forms';
 
 interface Props extends AppProps, I18nProps {}
@@ -74,6 +76,8 @@ function App (props: Props): React.ReactElement<Props> {
           <Route exact path={`${basePath}/new/set-working-group-mint-capacity`} component={SetWorkingGroupMintCapacityForm} />
           <Route exact path={`${basePath}/new/begin-review-working-group-leader-application`} component={BeginReviewLeaderApplicationsForm} />
           <Route exact path={`${basePath}/new/fill-working-group-leader-opening`} component={FillWorkingGroupLeaderOpeningForm} />
+          <Route exact path={`${basePath}/new/decrease-working-group-leader-stake`} component={DecreaseWorkingGroupLeadStakeFrom} />
+          <Route exact path={`${basePath}/new/slash-working-group-leader-stake`} component={SlashWorkingGroupLeadStakeForm} />
           <Route exact path={`${basePath}/active`} component={NotDone} />
           <Route exact path={`${basePath}/finalized`} component={NotDone} />
           <Route exact path={`${basePath}/:id`} component={ProposalFromId} />

+ 21 - 0
pioneer/packages/joy-proposals/src/validationSchema.ts

@@ -13,6 +13,8 @@ import { FormValues as AddWorkingGroupLeaderOpeningFormValues } from './forms/Ad
 import { FormValues as SetWorkingGroupMintCapacityFormValues } from './forms/SetWorkingGroupMintCapacityForm';
 import { FormValues as BeginReviewLeaderApplicationsFormValues } from './forms/BeginReviewLeaderApplicationsForm';
 import { FormValues as FillWorkingGroupLeaderOpeningFormValues } from './forms/FillWorkingGroupLeaderOpeningForm';
+import { FormValues as DecreaseWorkingGroupLeadStakeFormValues } from './forms/DecreaseWorkingGroupLeadStakeForm';
+import { FormValues as SlashWorkingGroupLeadStakeFormValues } from './forms/SlashWorkingGroupLeadStakeForm';
 
 // TODO: If we really need this (currency unit) we can we make "Validation" a functiction that returns an object.
 // We could then "instantialize" it in "withFormContainer" where instead of passing
@@ -95,6 +97,11 @@ const MAX_REWARD_INTERVAL = 30 * 14400; // 30 days
 const MIN_NEXT_PAYMENT_BLOCK_MINUS_CURRENT = 3 * 14400;
 const MAX_NEXT_PAYMENT_BLOCK_MINUS_CURRENT = 30 * 14400; // 30 days
 
+// Decrease/Slash Working Group Leader Stake
+const DECREASE_LEAD_STAKE_MIN = 1;
+const SLASH_LEAD_STAKE_MIN = 1;
+// Max is validated in form component, because it depends on selected working group's leader stake
+
 function errorMessage (name: string, min?: number | string, max?: number | string, unit?: string): string {
   return `${name} should be at least ${min} and no more than ${max}${unit ? ` ${unit}.` : '.'}`;
 }
@@ -135,6 +142,8 @@ type FormValuesByType<T extends ValidationTypeKeys> =
   T extends 'SetWorkingGroupMintCapacity' ? Omit<SetWorkingGroupMintCapacityFormValues, keyof GenericFormValues> :
   T extends 'BeginReviewWorkingGroupLeaderApplication' ? Omit<BeginReviewLeaderApplicationsFormValues, keyof GenericFormValues> :
   T extends 'FillWorkingGroupLeaderOpening' ? Omit<FillWorkingGroupLeaderOpeningFormValues, keyof GenericFormValues> :
+  T extends 'DecreaseWorkingGroupLeaderStake' ? Omit<DecreaseWorkingGroupLeadStakeFormValues, keyof GenericFormValues> :
+  T extends 'SlashWorkingGroupLeaderStake' ? Omit<SlashWorkingGroupLeadStakeFormValues, keyof GenericFormValues> :
   never;
 /* eslint-enable @typescript-eslint/indent */
 
@@ -377,6 +386,18 @@ const Validation: ValidationType = {
         is: true,
         then: minMaxInt(MIN_REWARD_INTERVAL, MAX_REWARD_INTERVAL, 'Reward interval')
       })
+  }),
+  DecreaseWorkingGroupLeaderStake: () => ({
+    workingGroup: Yup.string(),
+    amount: Yup.number()
+      .required('Amount is required!')
+      .min(DECREASE_LEAD_STAKE_MIN, `Amount must be greater than ${DECREASE_LEAD_STAKE_MIN}`)
+  }),
+  SlashWorkingGroupLeaderStake: () => ({
+    workingGroup: Yup.string(),
+    amount: Yup.number()
+      .required('Amount is required!')
+      .min(SLASH_LEAD_STAKE_MIN, `Amount must be greater than ${SLASH_LEAD_STAKE_MIN}`)
   })
 };
 

+ 18 - 8
pioneer/packages/joy-utils/src/MemberProfilePreview.tsx

@@ -25,23 +25,26 @@ const StyledProfilePreview = styled.div`
   }
 `;
 
-const Details = styled.div``;
+const Details = styled.div`
+  margin-left: 1rem;
+  display: grid;
+  grid-row-gap: 0.25rem;
+  grid-template-columns: 100%;
+`;
 
 const DetailsHandle = styled.h4`
   margin: 0;
-  margin-left: 1rem;
   font-weight: bold;
   color: #333;
 `;
 
 const DetailsID = styled.div`
-  margin: 0;
-  margin-top: 0.25rem;
-  margin-left: 1rem;
   color: #777;
 `;
 
-export default function ProfilePreview ({ id, avatar_uri, root_account, handle, link = false }: ProfileItemProps) {
+export default function ProfilePreview (
+  { id, avatar_uri, root_account, handle, link = false, children }: React.PropsWithChildren<ProfileItemProps>
+) {
   const Preview = (
     <StyledProfilePreview>
       {avatar_uri.toString() ? (
@@ -52,6 +55,7 @@ export default function ProfilePreview ({ id, avatar_uri, root_account, handle,
       <Details>
         <DetailsHandle>{handle.toString()}</DetailsHandle>
         { id !== undefined && <DetailsID>ID: {id.toString()}</DetailsID> }
+        { children }
       </Details>
     </StyledProfilePreview>
   );
@@ -69,7 +73,13 @@ type ProfilePreviewFromStructProps = {
   id?: number | MemberId;
 };
 
-export function ProfilePreviewFromStruct ({ profile, link, id }: ProfilePreviewFromStructProps) {
+export function ProfilePreviewFromStruct (
+  { profile, link, id, children }: React.PropsWithChildren<ProfilePreviewFromStructProps>
+) {
   const { avatar_uri, root_account, handle } = profile;
-  return <ProfilePreview {...{ avatar_uri, root_account, handle, link, id }} />;
+  return (
+    <ProfilePreview {...{ avatar_uri, root_account, handle, link, id }}>
+      {children}
+    </ProfilePreview>
+  );
 }

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

@@ -119,6 +119,24 @@ export const metadata: { [k in ProposalType]: ProposalMeta } = {
     approvalThreshold: 75,
     slashingQuorum: 60,
     slashingThreshold: 80
+  },
+  DecreaseWorkingGroupLeaderStake: {
+    description: 'Decrease Working Group Leader Stake Proposal',
+    category: 'Working Groups',
+    stake: 50000,
+    approvalQuorum: 60,
+    approvalThreshold: 75,
+    slashingQuorum: 60,
+    slashingThreshold: 80
+  },
+  SlashWorkingGroupLeaderStake: {
+    description: 'Slash Working Group Leader Stake Proposal',
+    category: 'Working Groups',
+    stake: 50000,
+    approvalQuorum: 60,
+    approvalThreshold: 75,
+    slashingQuorum: 60,
+    slashingThreshold: 80
   }
 };
 
@@ -170,6 +188,14 @@ export const apiMethods: { [k in ProposalType]?: ProposalsApiMethodNames } = {
   FillWorkingGroupLeaderOpening: {
     votingPeriod: 'fillWorkingGroupLeaderOpeningProposalVotingPeriod',
     gracePeriod: 'fillWorkingGroupLeaderOpeningProposalGracePeriod'
+  },
+  DecreaseWorkingGroupLeaderStake: {
+    votingPeriod: 'decreaseWorkingGroupLeaderStakeProposalVotingPeriod',
+    gracePeriod: 'decreaseWorkingGroupLeaderStakeProposalGracePeriod'
+  },
+  SlashWorkingGroupLeaderStake: {
+    votingPeriod: 'slashWorkingGroupLeaderStakeProposalVotingPeriod',
+    gracePeriod: 'slashWorkingGroupLeaderStakeProposalGracePeriod'
   }
 } as const;
 

+ 52 - 0
pioneer/packages/joy-utils/src/react/components/working-groups/LeadInfo.tsx

@@ -0,0 +1,52 @@
+import React from 'react';
+import { LeadData } from '../../../types/workingGroups';
+import { ProfilePreviewFromStruct as MemberPreview } from '../../../MemberProfilePreview';
+import { Label, Message } from 'semantic-ui-react';
+import { formatBalance } from '@polkadot/util';
+import { WorkingGroupKeys } from '@joystream/types/common';
+import { useTransport, usePromise } from '../../hooks';
+import PromiseComponent from '../PromiseComponent';
+
+type LeadInfoProps = {
+  lead: LeadData | null;
+  group?: WorkingGroupKeys;
+  header?: boolean;
+};
+
+export const LeadInfo = ({ lead, group, header = false }: LeadInfoProps) => (
+  <Message>
+    <Message.Content>
+      { header && <Message.Header>Current {group && `${group} `}Working Group lead:</Message.Header> }
+      <div style={{ padding: '0.5rem 0' }}>
+        { lead
+          ? (
+            <MemberPreview profile={lead.profile}>
+              <Label>Role stake: { lead.stake ? formatBalance(lead.stake) : 'NONE'}</Label>
+            </MemberPreview>
+          ) : 'NONE'
+        }
+      </div>
+    </Message.Content>
+  </Message>
+);
+
+type LeadInfoFromIdProps = {
+  leadId: number;
+  group: WorkingGroupKeys;
+  header?: boolean;
+};
+
+export const LeadInfoFromId = ({ leadId, group, header }: LeadInfoFromIdProps) => {
+  const transport = useTransport();
+  const [lead, error, loading] = usePromise<LeadData | null>(
+    () => transport.workingGroups.currentLead(group),
+    null,
+    [leadId]
+  );
+
+  return (
+    <PromiseComponent error={error} loading={loading} message="Fetching current lead...">
+      <LeadInfo lead={lead} group={group} header={header}/>
+    </PromiseComponent>
+  );
+};

+ 22 - 3
pioneer/packages/joy-utils/src/react/hooks/usePromise.tsx

@@ -2,7 +2,12 @@ import { useState, useEffect, useCallback } from 'react';
 
 export type UsePromiseReturnValues<T> = [T, any, boolean, () => Promise<void|null>];
 
-export default function usePromise<T> (promise: () => Promise<T>, defaultValue: T, dependsOn: any[] = []): UsePromiseReturnValues<T> {
+export default function usePromise<T> (
+  promise: () => Promise<T>,
+  defaultValue: T,
+  dependsOn: any[] = [],
+  onUpdate?: (newValue: T) => void
+): UsePromiseReturnValues<T> {
   const [state, setState] = useState<{
     value: T;
     error: any;
@@ -12,8 +17,22 @@ export default function usePromise<T> (promise: () => Promise<T>, defaultValue:
   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));
+      .then(value => {
+        if (isSubscribed) {
+          setState({ value, error: null, isPending: false });
+          if (onUpdate) {
+            onUpdate(value);
+          }
+        }
+      })
+      .catch(error => {
+        if (isSubscribed) {
+          setState({ value: defaultValue, error: error, isPending: false });
+          if (onUpdate) {
+            onUpdate(defaultValue); // This should represent an empty value in most cases
+          }
+        }
+      });
   }, [promise]);
 
   useEffect(() => {

+ 10 - 3
pioneer/packages/joy-utils/src/transport/workingGroups.ts

@@ -7,7 +7,7 @@ import { SingleLinkedMapEntry } from '../index';
 import { Worker, WorkerId, Opening as WGOpening, Application as WGApplication } from '@joystream/types/working-group';
 import { apiModuleByGroup } from '../consts/workingGroups';
 import { WorkingGroupKeys } from '@joystream/types/common';
-import { LeadWithProfile, OpeningData, ParsedApplication } from '../types/workingGroups';
+import { LeadData, OpeningData, ParsedApplication } from '../types/workingGroups';
 import { OpeningId, ApplicationId, Opening, Application } from '@joystream/types/hiring';
 import { MultipleLinkedMapEntry } from '../LinkedMapEntry';
 import { Stake, StakeId } from '@joystream/types/stake';
@@ -25,7 +25,7 @@ export default class WorkingGroupsTransport extends BaseTransport {
     return this.api.query[module];
   }
 
-  public async currentLead (group: WorkingGroupKeys): Promise<LeadWithProfile | null> {
+  public async currentLead (group: WorkingGroupKeys): Promise<LeadData | null> {
     const optLeadId = (await this.queryByGroup(group).currentLead()) as Option<WorkerId>;
 
     if (!optLeadId.isSome) {
@@ -43,9 +43,16 @@ export default class WorkingGroupsTransport extends BaseTransport {
       return null;
     }
 
+    const stake = leadWorker.role_stake_profile.isSome
+      ? (await this.stakeValue(leadWorker.role_stake_profile.unwrap().stake_id)).toNumber()
+      : undefined;
+
     return {
+      group,
+      workerId: leadWorkerId,
       worker: leadWorker,
-      profile: await this.membersT.expectedMemberProfile(leadWorker.member_id)
+      profile: await this.membersT.expectedMemberProfile(leadWorker.member_id),
+      stake
     };
   }
 

+ 3 - 1
pioneer/packages/joy-utils/src/types/proposals.ts

@@ -16,7 +16,9 @@ export const ProposalTypes = [
   'AddWorkingGroupLeaderOpening',
   'SetWorkingGroupMintCapacity',
   'BeginReviewWorkingGroupLeaderApplication',
-  'FillWorkingGroupLeaderOpening'
+  'FillWorkingGroupLeaderOpening',
+  'SlashWorkingGroupLeaderStake',
+  'DecreaseWorkingGroupLeaderStake'
 ] as const;
 
 export type ProposalType = typeof ProposalTypes[number];

+ 6 - 2
pioneer/packages/joy-utils/src/types/workingGroups.ts

@@ -1,11 +1,15 @@
-import { Worker, Opening as WGOpening } from '@joystream/types/working-group';
+import { Worker, Opening as WGOpening, WorkerId } from '@joystream/types/working-group';
 import { Profile } from '@joystream/types/members';
 import { OpeningId, Opening, ApplicationStage } from '@joystream/types/hiring';
 import { AccountId } from '@polkadot/types/interfaces';
+import { WorkingGroupKeys } from '@joystream/types/common';
 
-export type LeadWithProfile = {
+export type LeadData = {
+  workerId: WorkerId;
   worker: Worker;
   profile: Profile;
+  stake?: number;
+  group: WorkingGroupKeys;
 };
 
 export type OpeningData = {