Browse Source

SetWorkingGroupLeaderReward proposal

Leszek Wiesner 4 years ago
parent
commit
cfd4e03dd0

+ 7 - 6
pioneer/packages/joy-proposals/src/Proposal/Body.tsx

@@ -23,6 +23,7 @@ import {
 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';
+import { formatReward } from '@polkadot/joy-utils/functions/format';
 
 type BodyProps = {
   title: string;
@@ -196,12 +197,7 @@ const paramParsers: { [x in ProposalType]: (params: any[]) => { [key: string]: s
           acceptedIds={[successful_application_id]}
           group={(new WorkingGroup(working_group)).type as WorkingGroupKeys}/>
       ),
-      'Reward policy': rp
-        ? (
-          `${formatBalance(rp.amount_per_payout)}${rp.payout_interval.isSome ? ` / ${rp.payout_interval} blocks` : ''} ` +
-          `(Next payment: #${rp.next_payment_at_block})`
-        )
-        : 'NONE'
+      'Reward policy': rp ? formatReward(rp, true) : 'NONE'
     };
   },
   SlashWorkingGroupLeaderStake: ([leadId, amount, group]) => ({
@@ -213,6 +209,11 @@ const paramParsers: { [x in ProposalType]: (params: any[]) => { [key: string]: s
     'Working group': (new WorkingGroup(group)).type,
     Lead: <LeadInfoFromId group={(new WorkingGroup(group).type as WorkingGroupKeys)} leadId={leadId}/>,
     'Decrease amount': formatBalance(amount)
+  }),
+  SetWorkingGroupLeaderReward: ([leadId, amount, group]) => ({
+    'Working group': (new WorkingGroup(group)).type,
+    Lead: <LeadInfoFromId group={(new WorkingGroup(group).type as WorkingGroupKeys)} leadId={leadId}/>,
+    'New reward amount': formatBalance(amount)
   })
 };
 

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

@@ -20,7 +20,7 @@ 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';
+import { WorkerData } from '@polkadot/joy-utils/types/workingGroups';
 
 export type FormValues = WGFormValues & {
   amount: string;
@@ -39,7 +39,7 @@ 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);
+  const [lead, setLead] = useState<WorkerData | null>(null);
 
   // Here we validate if stake <= current lead stake.
   // Because it depends on selected working group,
@@ -57,7 +57,7 @@ const DecreaseWorkingGroupLeadStakeForm: React.FunctionComponent<FormInnerProps>
       proposalType="DecreaseWorkingGroupLeaderStake"
       leadRequired={true}
       leadStakeRequired={true}
-      onLeadChange={(lead: LeadData | null) => setLead(lead)}
+      onLeadChange={(lead: WorkerData | null) => setLead(lead)}
       submitParams={[
         myMemberId,
         values.title,

+ 2 - 49
pioneer/packages/joy-proposals/src/forms/FillWorkingGroupLeaderOpeningForm.tsx

@@ -12,7 +12,7 @@ import {
   FormValues as WGFormValues,
   defaultValues as wgFromDefaultValues
 } from './GenericWorkingGroupProposalForm';
-import { FormField, InputFormField } from './FormFields';
+import { FormField, RewardPolicyFields } from './FormFields';
 import { withFormContainer } from './FormContainer';
 import './forms.css';
 import { Dropdown, DropdownItemProps, Header, Checkbox } from 'semantic-ui-react';
@@ -26,7 +26,7 @@ import { withCalls } from '@polkadot/react-api';
 import { Option } from '@polkadot/types';
 import { BlockNumber } from '@polkadot/types/interfaces';
 import { u32 as U32, u128 as U128 } from '@polkadot/types/primitive';
-import { getFormErrorLabelsProps, FormErrorLabelsProps } from './errorHandling';
+import { getFormErrorLabelsProps } from './errorHandling';
 import { RewardPolicy } from '@joystream/types/working-group';
 import { FillOpeningParameters } from '@joystream/types/proposals';
 import { WorkingGroup } from '@joystream/types/common';
@@ -61,53 +61,6 @@ type FormContainerProps = ProposalFormContainerProps<ExportComponentProps> & {
 };
 type FormInnerProps = ProposalFormInnerProps<FormContainerProps, FormValues>;
 
-type RewardPolicyFieldsProps = Pick<FormInnerProps, 'values' | 'handleChange' | 'setFieldValue'> & {
-  errorLabelsProps: FormErrorLabelsProps<FormValues>;
-};
-const RewardPolicyFields: React.FunctionComponent<RewardPolicyFieldsProps> = ({
-  values,
-  errorLabelsProps,
-  handleChange,
-  setFieldValue
-}) => {
-  return (
-    <>
-      <InputFormField
-        label="Amount per payout"
-        unit={formatBalance.getDefaults().unit}
-        onChange={handleChange}
-        name={'rewardAmount'}
-        error={errorLabelsProps.rewardAmount}
-        value={values.rewardAmount}
-        placeholder={'ie. 100'}
-      />
-      <InputFormField
-        label="Next payment at block"
-        onChange={handleChange}
-        name={'rewardNextBlock'}
-        error={errorLabelsProps.rewardNextBlock}
-        value={values.rewardNextBlock}
-      />
-      <FormField>
-        <Checkbox
-          toggle
-          onChange={(e, data) => { setFieldValue('rewardRecurring', data.checked); }}
-          label={'Recurring'}
-          checked={values.rewardRecurring}/>
-      </FormField>
-      { values.rewardRecurring && (
-        <InputFormField
-          label="Reward interval"
-          onChange={handleChange}
-          name={'rewardInterval'}
-          error={errorLabelsProps.rewardInterval}
-          value={values.rewardInterval}
-          unit={'Blocks'}
-        />
-      ) }
-    </>
-  );
-};
 const valuesToFillOpeningParams = (values: FormValues): FillOpeningParameters => (
   new FillOpeningParameters({
     working_group: new WorkingGroup(values.workingGroup),

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

@@ -1,6 +1,9 @@
 import React from 'react';
-import { Form, FormInputProps, FormTextAreaProps, Label, LabelProps } from 'semantic-ui-react';
+import { Form, FormInputProps, FormTextAreaProps, Label, LabelProps, Checkbox } from 'semantic-ui-react';
+import { FormikProps } from 'formik';
 import LabelWithHelp from './LabelWithHelp';
+import { FormErrorLabelsProps } from './errorHandling';
+import { formatBalance } from '@polkadot/util';
 
 /*
  * Generic form field components
@@ -64,4 +67,59 @@ export function FormField (props: React.PropsWithChildren<FormFieldProps>) {
   );
 }
 
+type ReawrdPolicyFieldsType = {
+  rewardAmount: string;
+  rewardNextBlock: string;
+  rewardRecurring: boolean;
+  rewardInterval: string;
+}
+type RewardPolicyFieldsProps<ValuesT extends ReawrdPolicyFieldsType> =
+  Pick<FormikProps<ValuesT>, 'values' | 'handleChange' | 'setFieldValue'> & {
+    errorLabelsProps: FormErrorLabelsProps<ValuesT>;
+  };
+export function RewardPolicyFields<ValuesT extends ReawrdPolicyFieldsType> ({
+  values,
+  errorLabelsProps,
+  handleChange,
+  setFieldValue
+}: RewardPolicyFieldsProps<ValuesT>) {
+  return (
+    <>
+      <InputFormField
+        label="Amount per payout"
+        unit={formatBalance.getDefaults().unit}
+        onChange={handleChange}
+        name={'rewardAmount'}
+        error={errorLabelsProps.rewardAmount}
+        value={values.rewardAmount}
+        placeholder={'ie. 100'}
+      />
+      <InputFormField
+        label="Next payment at block"
+        onChange={handleChange}
+        name={'rewardNextBlock'}
+        error={errorLabelsProps.rewardNextBlock}
+        value={values.rewardNextBlock}
+      />
+      <FormField>
+        <Checkbox
+          toggle
+          onChange={(e, data) => { setFieldValue('rewardRecurring', data.checked); }}
+          label={'Recurring'}
+          checked={values.rewardRecurring}/>
+      </FormField>
+      { values.rewardRecurring && (
+        <InputFormField
+          label="Reward interval"
+          onChange={handleChange}
+          name={'rewardInterval'}
+          error={errorLabelsProps.rewardInterval}
+          value={values.rewardInterval}
+          unit={'Blocks'}
+        />
+      ) }
+    </>
+  );
+}
+
 export default FormField;

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

@@ -15,7 +15,7 @@ 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 { LeadData } from '@polkadot/joy-utils/types/workingGroups';
+import { WorkerData } from '@polkadot/joy-utils/types/workingGroups';
 import { LeadInfo } from '@polkadot/joy-utils/react/components/working-groups/LeadInfo';
 
 export type FormValues = GenericFormValues & {
@@ -35,7 +35,8 @@ type FormAdditionalProps = {
   showLead?: boolean;
   leadRequired?: boolean;
   leadStakeRequired?: boolean;
-  onLeadChange?: (lead: LeadData | null) => void;
+  leadRewardRequired?: boolean;
+  onLeadChange?: (lead: WorkerData | null) => void;
 };
 
 // We don't exactly use "container" and "export" components here, but those types are useful for
@@ -45,7 +46,17 @@ type FormContainerProps = ProposalFormContainerProps<ExportComponentProps>;
 export type FormInnerProps = ProposalFormInnerProps<FormContainerProps, FormValues>;
 
 export const GenericWorkingGroupProposalForm: React.FunctionComponent<FormInnerProps> = props => {
-  const { handleChange, errors, touched, values, showLead = true, leadRequired = false, leadStakeRequired = false, onLeadChange } = props;
+  const {
+    handleChange,
+    errors,
+    touched,
+    values,
+    showLead = true,
+    leadRequired = false,
+    leadStakeRequired = false,
+    leadRewardRequired = false,
+    onLeadChange
+  } = props;
   const transport = useTransport();
   const [lead, error, loading] = usePromise(
     () => transport.workingGroups.currentLead(values.workingGroup),
@@ -56,10 +67,11 @@ export const GenericWorkingGroupProposalForm: React.FunctionComponent<FormInnerP
   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 rewardMissing = leadRewardRequired && (!leadRes.loading && !leadRes.error) && (leadRes.lead && !leadRes.lead.reward);
 
   const errorLabelsProps = getFormErrorLabelsProps<FormValues>(errors, touched);
   return (
-    <GenericProposalForm {...props} disabled={leadMissing || stakeMissing || leadRes.error}>
+    <GenericProposalForm {...props} disabled={leadMissing || stakeMissing || rewardMissing || leadRes.error}>
       <FormField
         error={errorLabelsProps.workingGroup}
         label="Working group"
@@ -87,7 +99,13 @@ export const GenericWorkingGroupProposalForm: React.FunctionComponent<FormInnerP
       { 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.
+          Selected working group leader has no associated role stake, which is required in order to create this proposal.
+        </Message>
+      ) }
+      { rewardMissing && (
+        <Message error visible>
+          <Message.Header>No reward relationship</Message.Header>
+          Selected working group leader has no reward relationship, which is required in order to create this proposal.
         </Message>
       ) }
       { props.children }

+ 93 - 0
pioneer/packages/joy-proposals/src/forms/SetWorkingGroupLeadRewardForm.tsx

@@ -0,0 +1,93 @@
+import React, { useState } 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 { WorkerData } 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 SetWorkingGroupLeadRewardForm: React.FunctionComponent<FormInnerProps> = props => {
+  const { handleChange, errors, touched, values, myMemberId } = props;
+  const errorLabelsProps = getFormErrorLabelsProps<FormValues>(errors, touched);
+  const [lead, setLead] = useState<WorkerData | null>(null);
+
+  return (
+    <GenericWorkingGroupProposalForm
+      {...props}
+      txMethod="createSetWorkingGroupLeaderRewardProposal"
+      proposalType="SetWorkingGroupLeaderReward"
+      leadRequired={true}
+      leadRewardRequired={true}
+      onLeadChange={(lead: WorkerData | null) => setLead(lead)}
+      submitParams={[
+        myMemberId,
+        values.title,
+        values.rationale,
+        '{STAKE}',
+        lead?.workerId,
+        values.amount,
+        values.workingGroup
+      ]}
+    >
+      { (lead && lead.reward) && (
+        <Grid columns="4" doubling stackable verticalAlign="bottom">
+          <Grid.Column>
+            <InputFormField
+              label="New reward amount"
+              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.SetWorkingGroupLeaderReward()
+  }),
+  handleSubmit: genericFormDefaultOptions.handleSubmit,
+  displayName: 'SetWorkingGroupLeadRewardForm'
+})(SetWorkingGroupLeadRewardForm);
+
+export default withProposalFormData<FormContainerProps, ExportComponentProps>(FormContainer);

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

@@ -20,7 +20,7 @@ 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';
+import { WorkerData } from '@polkadot/joy-utils/types/workingGroups';
 
 export type FormValues = WGFormValues & {
   amount: string;
@@ -39,7 +39,7 @@ 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);
+  const [lead, setLead] = useState<WorkerData | 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
@@ -56,7 +56,7 @@ const SlashWorkingGroupLeadStakeForm: React.FunctionComponent<FormInnerProps> =
       proposalType="SlashWorkingGroupLeaderStake"
       leadRequired={true}
       leadStakeRequired={true}
-      onLeadChange={(lead: LeadData | null) => setLead(lead)}
+      onLeadChange={(lead: WorkerData | null) => setLead(lead)}
       submitParams={[
         myMemberId,
         values.title,

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

@@ -13,3 +13,4 @@ export { default as BeginReviewLeaderApplicationsForm } from './BeginReviewLeade
 export { default as FillWorkingGroupLeaderOpeningForm } from './FillWorkingGroupLeaderOpeningForm';
 export { default as DecreaseWorkingGroupLeadStakeFrom } from './DecreaseWorkingGroupLeadStakeForm';
 export { default as SlashWorkingGroupLeadStakeForm } from './SlashWorkingGroupLeadStakeForm';
+export { default as SetWorkingGroupLeadRewardForm } from './SetWorkingGroupLeadRewardForm';

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

@@ -25,7 +25,8 @@ import {
   BeginReviewLeaderApplicationsForm,
   FillWorkingGroupLeaderOpeningForm,
   DecreaseWorkingGroupLeadStakeFrom,
-  SlashWorkingGroupLeadStakeForm
+  SlashWorkingGroupLeadStakeForm,
+  SetWorkingGroupLeadRewardForm
 } from './forms';
 
 interface Props extends AppProps, I18nProps {}
@@ -78,6 +79,7 @@ function App (props: Props): React.ReactElement<Props> {
           <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}/new/set-working-group-leader-reward`} component={SetWorkingGroupLeadRewardForm} />
           <Route exact path={`${basePath}/active`} component={NotDone} />
           <Route exact path={`${basePath}/finalized`} component={NotDone} />
           <Route exact path={`${basePath}/:id`} component={ProposalFromId} />

+ 7 - 1
pioneer/packages/joy-proposals/src/validationSchema.ts

@@ -15,6 +15,7 @@ import { FormValues as BeginReviewLeaderApplicationsFormValues } from './forms/B
 import { FormValues as FillWorkingGroupLeaderOpeningFormValues } from './forms/FillWorkingGroupLeaderOpeningForm';
 import { FormValues as DecreaseWorkingGroupLeadStakeFormValues } from './forms/DecreaseWorkingGroupLeadStakeForm';
 import { FormValues as SlashWorkingGroupLeadStakeFormValues } from './forms/SlashWorkingGroupLeadStakeForm';
+import { FormValues as SetWorkingGroupLeadRewardFormValues } from './forms/SetWorkingGroupLeadRewardForm';
 
 // 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
@@ -86,7 +87,7 @@ const LEAVE_ROLE_UNSTAKING_MAX = 14 * 14400; // 14 days
 const WG_MINT_CAP_MIN = 0;
 const WG_MINT_CAP_MAX = 1000000;
 
-// Fill Working Group Leader Opening
+// Fill Working Group Leader Opening / Set Working Group Lead Reward
 // TODO: Discuss the actual values
 const MIN_REWARD_AMOUNT = 1;
 const MAX_REWARD_AMOUNT = 100000;
@@ -144,6 +145,7 @@ type FormValuesByType<T extends ValidationTypeKeys> =
   T extends 'FillWorkingGroupLeaderOpening' ? Omit<FillWorkingGroupLeaderOpeningFormValues, keyof GenericFormValues> :
   T extends 'DecreaseWorkingGroupLeaderStake' ? Omit<DecreaseWorkingGroupLeadStakeFormValues, keyof GenericFormValues> :
   T extends 'SlashWorkingGroupLeaderStake' ? Omit<SlashWorkingGroupLeadStakeFormValues, keyof GenericFormValues> :
+  T extends 'SetWorkingGroupLeaderReward' ? Omit<SetWorkingGroupLeadRewardFormValues, keyof GenericFormValues> :
   never;
 /* eslint-enable @typescript-eslint/indent */
 
@@ -398,6 +400,10 @@ const Validation: ValidationType = {
     amount: Yup.number()
       .required('Amount is required!')
       .min(SLASH_LEAD_STAKE_MIN, `Amount must be greater than ${SLASH_LEAD_STAKE_MIN}`)
+  }),
+  SetWorkingGroupLeaderReward: () => ({
+    workingGroup: Yup.string(),
+    amount: minMaxInt(MIN_REWARD_AMOUNT, MAX_REWARD_AMOUNT, 'Reward amount')
   })
 };
 

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

@@ -137,6 +137,15 @@ export const metadata: { [k in ProposalType]: ProposalMeta } = {
     approvalThreshold: 75,
     slashingQuorum: 60,
     slashingThreshold: 80
+  },
+  SetWorkingGroupLeaderReward: {
+    description: 'Set Working Group Leader Reward Proposal',
+    category: 'Working Groups',
+    stake: 50000,
+    approvalQuorum: 60,
+    approvalThreshold: 75,
+    slashingQuorum: 60,
+    slashingThreshold: 80
   }
 };
 
@@ -196,6 +205,10 @@ export const apiMethods: { [k in ProposalType]?: ProposalsApiMethodNames } = {
   SlashWorkingGroupLeaderStake: {
     votingPeriod: 'slashWorkingGroupLeaderStakeProposalVotingPeriod',
     gracePeriod: 'slashWorkingGroupLeaderStakeProposalGracePeriod'
+  },
+  SetWorkingGroupLeaderReward: {
+    votingPeriod: 'setWorkingGroupLeaderRewardProposalVotingPeriod',
+    gracePeriod: 'setWorkingGroupLeaderRewardProposalGracePeriod'
   }
 } as const;
 

+ 22 - 0
pioneer/packages/joy-utils/src/functions/format.ts

@@ -0,0 +1,22 @@
+import { RewardRelationship } from '@joystream/types/recurring-rewards';
+import { formatBalance } from '@polkadot/util';
+import { Option } from '@polkadot/types';
+import { RewardPolicy } from '@joystream/types/working-group';
+
+export const formatReward = (
+  {
+    amount_per_payout: amount,
+    payout_interval: interval,
+    next_payment_at_block
+  }: RewardRelationship | RewardPolicy,
+  showNextPaymentBlock = false
+) => {
+  const nextPaymentBlock = (next_payment_at_block instanceof Option)
+    ? next_payment_at_block.unwrapOr(null)
+    : next_payment_at_block;
+
+  return (
+    `${formatBalance(amount)}${interval.isSome ? ` / ${interval.unwrap()} block(s)` : ''}` +
+    ((showNextPaymentBlock && nextPaymentBlock) ? ` (Next payment: #${nextPaymentBlock})` : '')
+  );
+};

+ 14 - 10
pioneer/packages/joy-utils/src/react/components/working-groups/LeadInfo.tsx

@@ -1,19 +1,21 @@
 import React from 'react';
-import { LeadData } from '../../../types/workingGroups';
+import { WorkerData } 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';
+import { formatReward } from '@polkadot/joy-utils/functions/format';
 
 type LeadInfoProps = {
-  lead: LeadData | null;
+  lead: WorkerData | null;
   group?: WorkingGroupKeys;
   header?: boolean;
+  emptyMessage?: string;
 };
 
-export const LeadInfo = ({ lead, group, header = false }: LeadInfoProps) => (
+export const LeadInfo = ({ lead, group, header = false, emptyMessage = 'NONE' }: LeadInfoProps) => (
   <Message>
     <Message.Content>
       { header && <Message.Header>Current {group && `${group} `}Working Group lead:</Message.Header> }
@@ -21,9 +23,12 @@ export const LeadInfo = ({ lead, group, header = false }: LeadInfoProps) => (
         { lead
           ? (
             <MemberPreview profile={lead.profile}>
-              <Label>Role stake: { lead.stake ? formatBalance(lead.stake) : 'NONE'}</Label>
+              <div>
+                <Label>Role stake: <b>{ lead.stake ? formatBalance(lead.stake) : 'NONE'}</b></Label>
+                <Label>Reward: <b>{ lead.reward ? formatReward(lead.reward) : 'NONE' }</b></Label>
+              </div>
             </MemberPreview>
-          ) : 'NONE'
+          ) : emptyMessage
         }
       </div>
     </Message.Content>
@@ -33,20 +38,19 @@ export const LeadInfo = ({ lead, group, header = false }: LeadInfoProps) => (
 type LeadInfoFromIdProps = {
   leadId: number;
   group: WorkingGroupKeys;
-  header?: boolean;
 };
 
-export const LeadInfoFromId = ({ leadId, group, header }: LeadInfoFromIdProps) => {
+export const LeadInfoFromId = ({ leadId, group }: LeadInfoFromIdProps) => {
   const transport = useTransport();
-  const [lead, error, loading] = usePromise<LeadData | null>(
-    () => transport.workingGroups.currentLead(group),
+  const [lead, error, loading] = usePromise<WorkerData | null>(
+    () => transport.workingGroups.groupMemberById(group, leadId),
     null,
     [leadId]
   );
 
   return (
     <PromiseComponent error={error} loading={loading} message="Fetching current lead...">
-      <LeadInfo lead={lead} group={group} header={header}/>
+      <LeadInfo lead={lead} group={group} header={false} emptyMessage="Leader no longer active!"/>
     </PromiseComponent>
   );
 };

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

@@ -52,6 +52,10 @@ export default abstract class BaseTransport {
     return this.api.query.stake;
   }
 
+  protected get recurringRewards () {
+    return this.api.query.recurringRewards;
+  }
+
   protected queryMethodByName (name: string) {
     const [module, method] = name.split('.');
     return this.api.query[module][method];

+ 34 - 21
pioneer/packages/joy-utils/src/transport/workingGroups.ts

@@ -7,10 +7,11 @@ 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 { LeadData, OpeningData, ParsedApplication } from '../types/workingGroups';
+import { WorkerData, 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';
+import { RewardRelationshipId, RewardRelationship } from '@joystream/types/recurring-rewards';
 
 export default class WorkingGroupsTransport extends BaseTransport {
   private membersT: MembersTransport;
@@ -25,35 +26,40 @@ export default class WorkingGroupsTransport extends BaseTransport {
     return this.api.query[module];
   }
 
-  public async currentLead (group: WorkingGroupKeys): Promise<LeadData | null> {
-    const optLeadId = (await this.queryByGroup(group).currentLead()) as Option<WorkerId>;
+  public async groupMemberById (group: WorkingGroupKeys, workerId: number): Promise<WorkerData | null> {
+    const workerLink = new SingleLinkedMapEntry(
+      Worker,
+      await this.queryByGroup(group).workerById(workerId)
+    );
+    const worker = workerLink.value;
 
-    if (!optLeadId.isSome) {
+    if (!worker.is_active) {
       return null;
     }
 
-    const leadWorkerId = optLeadId.unwrap();
-    const leadWorkerLink = new SingleLinkedMapEntry(
-      Worker,
-      await this.queryByGroup(group).workerById(leadWorkerId)
-    );
-    const leadWorker = leadWorkerLink.value;
+    const stake = worker.role_stake_profile.isSome
+      ? (await this.stakeValue(worker.role_stake_profile.unwrap().stake_id)).toNumber()
+      : undefined;
+
+    const reward = worker.reward_relationship.isSome
+      ? (await this.rewardRelationship(worker.reward_relationship.unwrap()))
+      : undefined;
+
+    const profile = await this.membersT.expectedMemberProfile(worker.member_id);
+
+    return { group, workerId, worker, profile, stake, reward };
+  }
+
+  public async currentLead (group: WorkingGroupKeys): Promise<WorkerData | null> {
+    const optLeadId = (await this.queryByGroup(group).currentLead()) as Option<WorkerId>;
 
-    if (!leadWorker.is_active) {
+    if (!optLeadId.isSome) {
       return null;
     }
 
-    const stake = leadWorker.role_stake_profile.isSome
-      ? (await this.stakeValue(leadWorker.role_stake_profile.unwrap().stake_id)).toNumber()
-      : undefined;
+    const leadWorkerId = optLeadId.unwrap().toNumber();
 
-    return {
-      group,
-      workerId: leadWorkerId,
-      worker: leadWorker,
-      profile: await this.membersT.expectedMemberProfile(leadWorker.member_id),
-      stake
-    };
+    return this.groupMemberById(group, leadWorkerId);
   }
 
   public async allOpenings (group: WorkingGroupKeys): Promise<OpeningData[]> {
@@ -104,6 +110,13 @@ export default class WorkingGroupsTransport extends BaseTransport {
     ).value.value;
   }
 
+  protected async rewardRelationship (relationshipId: RewardRelationshipId): Promise<RewardRelationship> {
+    return new SingleLinkedMapEntry(
+      RewardRelationship,
+      await this.recurringRewards.rewardRelationships(relationshipId)
+    ).value;
+  }
+
   protected async parseApplication (wgApplicationId: number, wgApplication: WGApplication): Promise<ParsedApplication> {
     const appId = wgApplication.application_id;
     const application = await this.hiringApplicationById(appId);

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

@@ -18,7 +18,8 @@ export const ProposalTypes = [
   'BeginReviewWorkingGroupLeaderApplication',
   'FillWorkingGroupLeaderOpening',
   'SlashWorkingGroupLeaderStake',
-  'DecreaseWorkingGroupLeaderStake'
+  'DecreaseWorkingGroupLeaderStake',
+  'SetWorkingGroupLeaderReward'
 ] as const;
 
 export type ProposalType = typeof ProposalTypes[number];

+ 5 - 3
pioneer/packages/joy-utils/src/types/workingGroups.ts

@@ -1,14 +1,16 @@
-import { Worker, Opening as WGOpening, WorkerId } from '@joystream/types/working-group';
+import { Worker, Opening as WGOpening } 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';
+import { RewardRelationship } from '@joystream/types/recurring-rewards';
 
-export type LeadData = {
-  workerId: WorkerId;
+export type WorkerData = {
+  workerId: number;
   worker: Worker;
   profile: Profile;
   stake?: number;
+  reward?: RewardRelationship;
   group: WorkingGroupKeys;
 };
 

+ 25 - 9
types/src/recurring-rewards/index.ts

@@ -1,4 +1,4 @@
-import { getTypeRegistry, u32, u64, u128, Option, GenericAccountId } from '@polkadot/types';
+import { getTypeRegistry, u64, u128, Option } from '@polkadot/types';
 import { AccountId, Balance, BlockNumber } from '@polkadot/types/interfaces';
 import { JoyStruct } from '../common';
 import { MintId } from '../mint';
@@ -42,12 +42,12 @@ export class RewardRelationship extends JoyStruct<IRewardRelationship> {
     super({
       recipient: RecipientId,
       mint_id: MintId,
-      account: GenericAccountId,
-      amount_per_payout: u128,
-      next_payment_at_block: Option.with(u32),
-      payout_interval: Option.with(u32),
-      total_reward_received: u128,
-      total_reward_missed: u128,
+      account: 'AccountId',
+      amount_per_payout: 'Balance',
+      next_payment_at_block: Option.with('BlockNumber'),
+      payout_interval: Option.with('BlockNumber'),
+      total_reward_received: 'Balance',
+      total_reward_missed: 'Balance'
     }, value);
   }
 
@@ -55,8 +55,24 @@ export class RewardRelationship extends JoyStruct<IRewardRelationship> {
     return this.getField<RecipientId>('recipient')
   }
 
-  get total_reward_received(): u128 {
-    return this.getField<u128>('total_reward_received');
+  get total_reward_received(): Balance {
+    return this.getField<Balance>('total_reward_received');
+  }
+
+  get total_reward_missed(): Balance {
+    return this.getField<Balance>('total_reward_missed');
+  }
+
+  get amount_per_payout(): Balance {
+    return this.getField<Balance>('amount_per_payout');
+  }
+
+  get payout_interval(): Option<BlockNumber> {
+    return this.getField<Option<BlockNumber>>('payout_interval');
+  }
+
+  get next_payment_at_block(): Option<BlockNumber> {
+    return this.getField<Option<BlockNumber>>('next_payment_at_block');
   }
 };