transport.substrate.ts 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769
  1. import { map, switchMap } from 'rxjs/operators';
  2. import ApiPromise from '@polkadot/api/promise';
  3. import { Balance } from '@polkadot/types/interfaces';
  4. import { Option, Vec } from '@polkadot/types';
  5. import { Constructor } from '@polkadot/types/types';
  6. import { Moment } from '@polkadot/types/interfaces/runtime';
  7. import { QueueTxExtrinsicAdd } from '@polkadot/react-components/Status/types';
  8. import keyringOption from '@polkadot/ui-keyring/options';
  9. import { APIQueryCache } from '@polkadot/joy-utils/transport/APIQueryCache';
  10. import { Subscribable } from '@polkadot/joy-utils/react/helpers';
  11. import BaseTransport from '@polkadot/joy-utils/transport/base';
  12. import { ITransport } from './transport';
  13. import { GroupMember } from './elements';
  14. import { Curator, CuratorId,
  15. CuratorApplication, CuratorApplicationId,
  16. CuratorRoleStakeProfile,
  17. CuratorOpening, CuratorOpeningId,
  18. Lead, LeadId } from '@joystream/types/content-working-group';
  19. import { Application as WGApplication,
  20. Opening as WGOpening,
  21. Worker, WorkerId,
  22. RoleStakeProfile } from '@joystream/types/working-group';
  23. import { Application, Opening, OpeningId, ApplicationId, ActiveApplicationStage } from '@joystream/types/hiring';
  24. import { Stake, StakeId } from '@joystream/types/stake';
  25. import { RewardRelationship, RewardRelationshipId } from '@joystream/types/recurring-rewards';
  26. import { Membership, MemberId } from '@joystream/types/members';
  27. import { createAccount, generateSeed } from '@polkadot/joy-utils/functions/accounts';
  28. import { WorkingGroupMembership, GroupLeadStatus } from './tabs/WorkingGroup';
  29. import { WorkingGroupOpening } from './tabs/Opportunities';
  30. import { ActiveRole, OpeningApplication } from './tabs/MyRoles';
  31. import { keyPairDetails } from './flows/apply';
  32. import { classifyApplicationCancellation,
  33. classifyOpeningStage,
  34. classifyOpeningStakes,
  35. isApplicationHired } from './classifiers';
  36. import { WorkingGroups, AvailableGroups, workerRoleNameByGroup } from './working_groups';
  37. import { Sort, Sum, Zero } from './balances';
  38. import _ from 'lodash';
  39. type WorkingGroupPair<HiringModuleType, WorkingGroupType> = {
  40. hiringModule: HiringModuleType;
  41. workingGroup: WorkingGroupType;
  42. }
  43. type StakePair<T = Balance> = {
  44. application: T;
  45. role: T;
  46. }
  47. type WGApiMethodType =
  48. 'nextOpeningId'
  49. | 'openingById'
  50. | 'nextApplicationId'
  51. | 'applicationById'
  52. | 'nextWorkerId'
  53. | 'workerById';
  54. type WGApiTxMethodType =
  55. 'applyOnOpening'
  56. | 'withdrawApplication'
  57. | 'leaveRole';
  58. type WGApiMethodsMapping = {
  59. query: { [key in WGApiMethodType]: string };
  60. tx: { [key in WGApiTxMethodType]: string };
  61. };
  62. type GroupApplication = CuratorApplication | WGApplication;
  63. type GroupApplicationId = CuratorApplicationId | ApplicationId;
  64. type GroupOpening = CuratorOpening | WGOpening;
  65. type GroupOpeningId = CuratorOpeningId | OpeningId;
  66. type GroupWorker = Worker | Curator;
  67. type GroupWorkerId = CuratorId | WorkerId;
  68. type GroupWorkerStakeProfile = RoleStakeProfile | CuratorRoleStakeProfile;
  69. type GroupLead = Lead | Worker;
  70. type GroupLeadWithMemberId = {
  71. lead: GroupLead;
  72. memberId: MemberId;
  73. workerId?: WorkerId; // Only when it's `working-groups` module lead
  74. }
  75. type WGApiMapping = {
  76. [key in WorkingGroups]: {
  77. module: string;
  78. methods: WGApiMethodsMapping;
  79. openingType: Constructor<GroupOpening>;
  80. applicationType: Constructor<GroupApplication>;
  81. workerType: Constructor<GroupWorker>;
  82. }
  83. };
  84. const workingGroupsApiMapping: WGApiMapping = {
  85. [WorkingGroups.StorageProviders]: {
  86. module: 'storageWorkingGroup',
  87. methods: {
  88. query: {
  89. nextOpeningId: 'nextOpeningId',
  90. openingById: 'openingById',
  91. nextApplicationId: 'nextApplicationId',
  92. applicationById: 'applicationById',
  93. nextWorkerId: 'nextWorkerId',
  94. workerById: 'workerById'
  95. },
  96. tx: {
  97. applyOnOpening: 'applyOnOpening',
  98. withdrawApplication: 'withdrawApplication',
  99. leaveRole: 'leaveRole'
  100. }
  101. },
  102. openingType: WGOpening,
  103. applicationType: WGApplication,
  104. workerType: Worker
  105. },
  106. [WorkingGroups.ContentCurators]: {
  107. module: 'contentWorkingGroup',
  108. methods: {
  109. query: {
  110. nextOpeningId: 'nextCuratorOpeningId',
  111. openingById: 'curatorOpeningById',
  112. nextApplicationId: 'nextCuratorApplicationId',
  113. applicationById: 'curatorApplicationById',
  114. nextWorkerId: 'nextCuratorId',
  115. workerById: 'curatorById'
  116. },
  117. tx: {
  118. applyOnOpening: 'applyOnCuratorOpening',
  119. withdrawApplication: 'withdrawCuratorApplication',
  120. leaveRole: 'leaveCuratorRole'
  121. }
  122. },
  123. openingType: CuratorOpening,
  124. applicationType: CuratorApplication,
  125. workerType: Curator
  126. }
  127. };
  128. export class Transport extends BaseTransport implements ITransport {
  129. protected queueExtrinsic: QueueTxExtrinsicAdd
  130. constructor (api: ApiPromise, queueExtrinsic: QueueTxExtrinsicAdd) {
  131. super(api, new APIQueryCache(api));
  132. this.queueExtrinsic = queueExtrinsic;
  133. }
  134. apiMethodByGroup (group: WorkingGroups, method: WGApiMethodType) {
  135. const apiModule = workingGroupsApiMapping[group].module;
  136. const apiMethod = workingGroupsApiMapping[group].methods.query[method];
  137. return this.api.query[apiModule][apiMethod];
  138. }
  139. cachedApiMethodByGroup (group: WorkingGroups, method: WGApiMethodType) {
  140. const apiModule = workingGroupsApiMapping[group].module;
  141. const apiMethod = workingGroupsApiMapping[group].methods.query[method];
  142. return this.cacheApi.query[apiModule][apiMethod];
  143. }
  144. apiExtrinsicByGroup (group: WorkingGroups, method: WGApiTxMethodType) {
  145. const apiModule = workingGroupsApiMapping[group].module;
  146. const apiMethod = workingGroupsApiMapping[group].methods.tx[method];
  147. return this.api.tx[apiModule][apiMethod];
  148. }
  149. unsubscribe () {
  150. this.cacheApi.unsubscribe();
  151. }
  152. protected async stakeValue (stakeId: StakeId): Promise<Balance> {
  153. const stake = await this.cacheApi.query.stake.stakes(stakeId) as Stake;
  154. return stake.value;
  155. }
  156. protected async workerStake (stakeProfile: GroupWorkerStakeProfile): Promise<Balance> {
  157. return this.stakeValue(stakeProfile.stake_id);
  158. }
  159. protected async rewardRelationshipById (id: RewardRelationshipId): Promise<RewardRelationship | undefined> {
  160. const rewardRelationship = await this.cacheApi.query.recurringRewards.rewardRelationships(id) as RewardRelationship;
  161. return rewardRelationship.isEmpty ? undefined : rewardRelationship;
  162. }
  163. protected async workerTotalReward (relationshipId: RewardRelationshipId): Promise<Balance> {
  164. const relationship = await this.rewardRelationshipById(relationshipId);
  165. return relationship?.total_reward_received || this.api.createType('Balance', 0);
  166. }
  167. protected async curatorMemberId (curator: Curator): Promise<MemberId> {
  168. const curatorApplicationId = curator.induction.curator_application_id;
  169. const curatorApplication =
  170. await this.cacheApi.query.contentWorkingGroup.curatorApplicationById(curatorApplicationId) as CuratorApplication;
  171. return curatorApplication.member_id;
  172. }
  173. protected async workerRewardRelationship (worker: GroupWorker): Promise<RewardRelationship | undefined> {
  174. const rewardRelationship = worker.reward_relationship.isSome
  175. ? await this.rewardRelationshipById(worker.reward_relationship.unwrap())
  176. : undefined;
  177. return rewardRelationship;
  178. }
  179. protected async groupMember (
  180. group: WorkingGroups,
  181. id: GroupWorkerId,
  182. worker: GroupWorker
  183. ): Promise<GroupMember> {
  184. const roleAccount = worker.role_account_id;
  185. const memberId = group === WorkingGroups.ContentCurators
  186. ? await this.curatorMemberId(worker as Curator)
  187. : (worker as Worker).member_id;
  188. const profile = await this.cacheApi.query.members.membershipById(memberId) as Membership;
  189. if (profile.handle.isEmpty) {
  190. throw new Error('No group member profile found!');
  191. }
  192. let stakeValue: Balance = this.api.createType('Balance', 0);
  193. if (worker.role_stake_profile && worker.role_stake_profile.isSome) {
  194. stakeValue = await this.workerStake(worker.role_stake_profile.unwrap());
  195. }
  196. const rewardRelationship = await this.workerRewardRelationship(worker);
  197. return ({
  198. roleAccount,
  199. group,
  200. memberId,
  201. workerId: id.toNumber(),
  202. profile,
  203. title: workerRoleNameByGroup[group],
  204. stake: stakeValue,
  205. rewardRelationship
  206. });
  207. }
  208. protected async areGroupRolesOpen (group: WorkingGroups, lead = false): Promise<boolean> {
  209. const groupOpenings = await this.entriesByIds<GroupOpeningId, GroupOpening>(
  210. this.apiMethodByGroup(group, 'openingById')
  211. );
  212. for (const [/* id */, groupOpening] of groupOpenings) {
  213. const opening = await this.opening(groupOpening.hiring_opening_id.toNumber());
  214. if (
  215. opening.is_active &&
  216. (
  217. groupOpening instanceof WGOpening
  218. ? (lead === groupOpening.opening_type.isOfType('Leader'))
  219. : !lead // Lead openings are never available for content working group currently
  220. )
  221. ) {
  222. return true;
  223. }
  224. }
  225. return false;
  226. }
  227. protected async currentCuratorLead (): Promise<GroupLeadWithMemberId | null> {
  228. const optLeadId = (await this.cacheApi.query.contentWorkingGroup.currentLeadId()) as Option<LeadId>;
  229. if (!optLeadId.isSome) {
  230. return null;
  231. }
  232. const leadId = optLeadId.unwrap();
  233. const lead = await this.cacheApi.query.contentWorkingGroup.leadById(leadId) as Lead;
  234. if (lead.isEmpty || !lead.stage.isOfType('Active')) {
  235. return null;
  236. }
  237. const memberId = lead.member_id;
  238. return { lead, memberId };
  239. }
  240. protected async currentStorageLead (): Promise <GroupLeadWithMemberId | null> {
  241. const optLeadId = (await this.cacheApi.query.storageWorkingGroup.currentLead()) as Option<WorkerId>;
  242. if (!optLeadId.isSome) {
  243. return null;
  244. }
  245. const leadWorkerId = optLeadId.unwrap();
  246. const leadWorker = await this.cacheApi.query.storageWorkingGroup.workerById(leadWorkerId) as Worker;
  247. if (leadWorker.isEmpty) {
  248. return null;
  249. }
  250. return {
  251. lead: leadWorker,
  252. memberId: leadWorker.member_id,
  253. workerId: leadWorkerId
  254. };
  255. }
  256. async groupLeadStatus (group: WorkingGroups = WorkingGroups.ContentCurators): Promise<GroupLeadStatus> {
  257. const currentLead = group === WorkingGroups.ContentCurators
  258. ? await this.currentCuratorLead()
  259. : await this.currentStorageLead();
  260. if (currentLead !== null) {
  261. const profile = await this.cacheApi.query.members.membershipById(currentLead.memberId) as Membership;
  262. if (profile.handle.isEmpty) {
  263. throw new Error(`${group} lead profile not found!`);
  264. }
  265. const rewardRelationshipId = currentLead.lead.reward_relationship;
  266. const rewardRelationship = rewardRelationshipId.isSome
  267. ? await this.rewardRelationshipById(rewardRelationshipId.unwrap())
  268. : undefined;
  269. const stake = group === WorkingGroups.StorageProviders && (currentLead.lead as Worker).role_stake_profile.isSome
  270. ? await this.workerStake((currentLead.lead as Worker).role_stake_profile.unwrap())
  271. : undefined;
  272. return {
  273. lead: {
  274. memberId: currentLead.memberId,
  275. workerId: currentLead.workerId,
  276. roleAccount: currentLead.lead.role_account_id,
  277. profile,
  278. title: _.startCase(group) + ' Lead',
  279. stage: group === WorkingGroups.ContentCurators ? (currentLead.lead as Lead).stage : undefined,
  280. stake,
  281. rewardRelationship
  282. },
  283. loaded: true
  284. };
  285. } else {
  286. return {
  287. loaded: true
  288. };
  289. }
  290. }
  291. async groupOverview (group: WorkingGroups): Promise<WorkingGroupMembership> {
  292. const workerRolesAvailable = await this.areGroupRolesOpen(group);
  293. const leadRolesAvailable = await this.areGroupRolesOpen(group, true);
  294. const leadStatus = await this.groupLeadStatus(group);
  295. const workers = (await this.entriesByIds<GroupWorkerId, GroupWorker>(
  296. this.apiMethodByGroup(group, 'workerById')
  297. ))
  298. .filter(([id, worker]) => worker.is_active && (!leadStatus.lead?.workerId || !id.eq(leadStatus.lead.workerId)));
  299. return {
  300. leadStatus,
  301. workers: await Promise.all(workers.map(([id, worker]) => this.groupMember(group, id, worker))),
  302. workerRolesAvailable,
  303. leadRolesAvailable
  304. };
  305. }
  306. curationGroup (): Promise<WorkingGroupMembership> {
  307. return this.groupOverview(WorkingGroups.ContentCurators);
  308. }
  309. storageGroup (): Promise<WorkingGroupMembership> {
  310. return this.groupOverview(WorkingGroups.StorageProviders);
  311. }
  312. async opportunitiesByGroup (group: WorkingGroups): Promise<WorkingGroupOpening[]> {
  313. const output = new Array<WorkingGroupOpening>();
  314. const nextId = (await this.cachedApiMethodByGroup(group, 'nextOpeningId')()) as GroupOpeningId;
  315. // This is chain specfic, but if next id is still 0, it means no curator openings have been added yet
  316. if (!nextId.eq(0)) {
  317. const highestId = nextId.toNumber() - 1;
  318. for (let i = highestId; i >= 0; i--) {
  319. output.push(await this.groupOpening(group, i));
  320. }
  321. }
  322. return output;
  323. }
  324. async currentOpportunities (): Promise<WorkingGroupOpening[]> {
  325. let opportunities: WorkingGroupOpening[] = [];
  326. for (const group of AvailableGroups) {
  327. opportunities = opportunities.concat(await this.opportunitiesByGroup(group));
  328. }
  329. return opportunities.sort((a, b) => b.stage.starting_block - a.stage.starting_block);
  330. }
  331. protected async opening (id: number): Promise<Opening> {
  332. const opening = await this.cacheApi.query.hiring.openingById(id) as Opening;
  333. return opening;
  334. }
  335. protected async groupOpeningApplications (group: WorkingGroups, groupOpeningId: number): Promise<WorkingGroupPair<Application, GroupApplication>[]> {
  336. const groupApplications = await this.entriesByIds<GroupApplicationId, GroupApplication>(
  337. this.apiMethodByGroup(group, 'applicationById')
  338. );
  339. const openingGroupApplications = groupApplications
  340. .filter(([id, groupApplication]) => groupApplication.opening_id.toNumber() === groupOpeningId);
  341. const openingHiringApplications = (await Promise.all(
  342. openingGroupApplications.map(
  343. ([id, groupApplication]) => this.cacheApi.query.hiring.applicationById(groupApplication.application_id)
  344. )
  345. )) as Application[];
  346. return openingHiringApplications.map((hiringApplication, index) => ({
  347. hiringModule: hiringApplication,
  348. workingGroup: openingGroupApplications[index][1]
  349. }));
  350. }
  351. async groupOpening (group: WorkingGroups, id: number): Promise<WorkingGroupOpening> {
  352. const nextId = (await this.cachedApiMethodByGroup(group, 'nextOpeningId')() as GroupOpeningId).toNumber();
  353. if (id < 0 || id >= nextId) {
  354. throw new Error('invalid id');
  355. }
  356. const groupOpening = await this.cachedApiMethodByGroup(group, 'openingById')(id) as GroupOpening;
  357. const opening = await this.opening(
  358. groupOpening.hiring_opening_id.toNumber()
  359. );
  360. const applications = await this.groupOpeningApplications(group, id);
  361. const stakes = classifyOpeningStakes(opening);
  362. return ({
  363. opening: opening,
  364. meta: {
  365. id: id.toString(),
  366. group,
  367. type: groupOpening instanceof WGOpening ? groupOpening.opening_type : undefined
  368. },
  369. stage: await classifyOpeningStage(this, opening),
  370. applications: {
  371. numberOfApplications: applications.length,
  372. maxNumberOfApplications: opening.max_applicants,
  373. requiredApplicationStake: stakes.application,
  374. requiredRoleStake: stakes.role,
  375. defactoMinimumStake: this.api.createType('u128', 0)
  376. },
  377. defactoMinimumStake: this.api.createType('u128', 0)
  378. });
  379. }
  380. protected async openingApplicationTotalStake (application: Application): Promise<Balance> {
  381. const promises = new Array<Promise<Balance>>();
  382. if (application.active_application_staking_id.isSome) {
  383. promises.push(this.stakeValue(application.active_application_staking_id.unwrap()));
  384. }
  385. if (application.active_role_staking_id.isSome) {
  386. promises.push(this.stakeValue(application.active_role_staking_id.unwrap()));
  387. }
  388. return Sum(await Promise.all(promises));
  389. }
  390. async openingApplicationRanks (group: WorkingGroups, openingId: number): Promise<Balance[]> {
  391. const applications = await this.groupOpeningApplications(group, openingId);
  392. return Sort(
  393. (await Promise.all(
  394. applications
  395. .filter((a) => a.hiringModule.stage.value instanceof ActiveApplicationStage)
  396. .map((application) => this.openingApplicationTotalStake(application.hiringModule))
  397. ))
  398. );
  399. }
  400. expectedBlockTime (): number {
  401. return (this.api.consts.babe.expectedBlockTime as Moment).toNumber() / 1000;
  402. }
  403. async blockHash (height: number): Promise<string> {
  404. const blockHash = await this.api.rpc.chain.getBlockHash(height);
  405. return blockHash.toString();
  406. }
  407. async blockTimestamp (height: number): Promise<Date> {
  408. const blockTime = await this.api.query.timestamp.now.at(
  409. await this.blockHash(height)
  410. );
  411. return new Date(blockTime.toNumber());
  412. }
  413. accounts (): Subscribable<keyPairDetails[]> {
  414. return keyringOption.optionsSubject.pipe(
  415. map((accounts) => {
  416. return accounts.all
  417. .filter((x) => x.value)
  418. .map(async (result, k) => {
  419. return {
  420. shortName: result.name,
  421. accountId: this.api.createType('AccountId', result.value),
  422. balance: (await this.api.derive.balances.account(result.value as string)).freeBalance
  423. };
  424. });
  425. }),
  426. switchMap(async (x) => Promise.all(x))
  427. ) as Subscribable<keyPairDetails[]>;
  428. }
  429. protected async applicationStakes (app: Application): Promise<StakePair<Balance>> {
  430. const stakes = {
  431. application: Zero,
  432. role: Zero
  433. };
  434. const appStake = app.active_application_staking_id;
  435. if (appStake.isSome) {
  436. stakes.application = await this.stakeValue(appStake.unwrap());
  437. }
  438. const roleStake = app.active_role_staking_id;
  439. if (roleStake.isSome) {
  440. stakes.role = await this.stakeValue(roleStake.unwrap());
  441. }
  442. return stakes;
  443. }
  444. protected async myApplicationRank (myApp: Application, applications: Array<Application>): Promise<number> {
  445. const activeApplications = applications.filter((app) => app.stage.value instanceof ActiveApplicationStage);
  446. const stakes = await Promise.all(
  447. activeApplications.map((app) => this.openingApplicationTotalStake(app))
  448. );
  449. const appvalues = activeApplications.map((app, key) => {
  450. return {
  451. app: app,
  452. value: stakes[key]
  453. };
  454. });
  455. appvalues.sort((a, b): number => {
  456. if (a.value.eq(b.value)) {
  457. return 0;
  458. } else if (a.value.gt(b.value)) {
  459. return -1;
  460. }
  461. return 1;
  462. });
  463. return appvalues.findIndex((v) => v.app.eq(myApp)) + 1;
  464. }
  465. async openingApplicationsByAddressAndGroup (group: WorkingGroups, roleKey: string): Promise<OpeningApplication[]> {
  466. const myGroupApplications = (await this.entriesByIds<GroupApplicationId, GroupApplication>(
  467. this.apiMethodByGroup(group, 'applicationById')
  468. ))
  469. .filter(([id, groupApplication]) => groupApplication.role_account_id.eq(roleKey));
  470. const myHiringApplications = await Promise.all(
  471. myGroupApplications.map(
  472. ([id, groupApplication]) => this.cacheApi.query.hiring.applicationById(groupApplication.application_id)
  473. )
  474. ) as Application[];
  475. const stakes = await Promise.all(
  476. myHiringApplications.map((app) => this.applicationStakes(app))
  477. );
  478. const openings = await Promise.all(
  479. myGroupApplications.map(([id, groupApplication]) => {
  480. return this.groupOpening(group, groupApplication.opening_id.toNumber());
  481. })
  482. );
  483. const allAppsByOpening = (await Promise.all(
  484. myGroupApplications.map(([id, groupApplication]) => (
  485. this.groupOpeningApplications(group, groupApplication.opening_id.toNumber())
  486. ))
  487. ));
  488. return await Promise.all(
  489. openings.map(async (o, key) => {
  490. return {
  491. id: myGroupApplications[key][0].toNumber(),
  492. hired: isApplicationHired(myHiringApplications[key]),
  493. cancelledReason: classifyApplicationCancellation(myHiringApplications[key]),
  494. rank: await this.myApplicationRank(myHiringApplications[key], allAppsByOpening[key].map((a) => a.hiringModule)),
  495. capacity: o.applications.maxNumberOfApplications,
  496. stage: o.stage,
  497. opening: o.opening,
  498. meta: o.meta,
  499. applicationStake: stakes[key].application,
  500. roleStake: stakes[key].role,
  501. review_end_time: o.stage.review_end_time,
  502. review_end_block: o.stage.review_end_block
  503. };
  504. })
  505. );
  506. }
  507. // Get opening applications for all groups by address
  508. async openingApplicationsByAddress (roleKey: string): Promise<OpeningApplication[]> {
  509. let applications: OpeningApplication[] = [];
  510. for (const group of AvailableGroups) {
  511. applications = applications.concat(await this.openingApplicationsByAddressAndGroup(group, roleKey));
  512. }
  513. return applications;
  514. }
  515. async myRolesByGroup (group: WorkingGroups, roleKeyId: string): Promise<ActiveRole[]> {
  516. const workers = await this.entriesByIds<GroupWorkerId, GroupWorker>(
  517. this.apiMethodByGroup(group, 'workerById')
  518. );
  519. const groupLead = (await this.groupLeadStatus(group)).lead;
  520. return Promise.all(
  521. workers
  522. .filter(([id, worker]) => worker.role_account_id.eq(roleKeyId) && worker.is_active)
  523. .map(async ([id, worker]) => {
  524. let stakeValue: Balance = this.api.createType('u128', 0);
  525. if (worker.role_stake_profile && worker.role_stake_profile.isSome) {
  526. stakeValue = await this.workerStake(worker.role_stake_profile.unwrap());
  527. }
  528. let earnedValue: Balance = this.api.createType('u128', 0);
  529. if (worker.reward_relationship && worker.reward_relationship.isSome) {
  530. earnedValue = await this.workerTotalReward(worker.reward_relationship.unwrap());
  531. }
  532. return {
  533. workerId: id,
  534. name: (groupLead?.workerId && groupLead.workerId.eq(id))
  535. ? _.startCase(group) + ' Lead'
  536. : workerRoleNameByGroup[group],
  537. reward: earnedValue,
  538. stake: stakeValue,
  539. group
  540. };
  541. })
  542. );
  543. }
  544. // All groups roles by key
  545. async myRoles (roleKey: string): Promise<ActiveRole[]> {
  546. let roles: ActiveRole[] = [];
  547. for (const group of AvailableGroups) {
  548. roles = roles.concat(await this.myRolesByGroup(group, roleKey));
  549. }
  550. return roles;
  551. }
  552. protected generateRoleAccount (name: string, password = ''): string | null {
  553. const { address, deriveError, derivePath, isSeedValid, pairType, seed } = generateSeed(null, '', 'bip');
  554. const isValid = !!address && !deriveError && isSeedValid;
  555. if (!isValid) {
  556. return null;
  557. }
  558. const status = createAccount(`${seed}${derivePath}`, pairType, name, password, 'created account');
  559. return status.account as string;
  560. }
  561. applyToOpening (
  562. group: WorkingGroups,
  563. id: number,
  564. roleAccountName: string,
  565. sourceAccount: string,
  566. appStake: Balance,
  567. roleStake: Balance,
  568. applicationText: string): Promise<number> {
  569. return new Promise<number>((resolve, reject) => {
  570. (this.cacheApi.query.members.memberIdsByControllerAccountId(sourceAccount) as Promise<Vec<MemberId>>)
  571. .then((membershipIds) => {
  572. if (membershipIds.length === 0) {
  573. reject(new Error('No membship ID associated with this address'));
  574. }
  575. const roleAccount = this.generateRoleAccount(roleAccountName);
  576. if (!roleAccount) {
  577. reject(new Error('failed to create role account'));
  578. }
  579. const tx = this.apiExtrinsicByGroup(group, 'applyOnOpening')(
  580. membershipIds[0], // Member id
  581. id, // Worker/Curator opening id
  582. roleAccount, // Role account
  583. // TODO: Will need to be adjusted if AtLeast Zero stakes become possible
  584. roleStake.eq(Zero) ? null : roleStake, // Role stake
  585. appStake.eq(Zero) ? null : appStake, // Application stake
  586. applicationText // Human readable text
  587. );
  588. const txFailedCb = () => {
  589. reject(new Error('transaction failed'));
  590. };
  591. const txSuccessCb = () => {
  592. resolve(1);
  593. };
  594. this.queueExtrinsic({
  595. accountId: sourceAccount,
  596. extrinsic: tx,
  597. txFailedCb,
  598. txSuccessCb
  599. });
  600. })
  601. .catch((e) => { reject(e); });
  602. });
  603. }
  604. leaveRole (group: WorkingGroups, sourceAccount: string, id: number, rationale: string, txSuccessCb?: () => void) {
  605. const tx = this.apiExtrinsicByGroup(group, 'leaveRole')(
  606. id,
  607. rationale
  608. );
  609. this.queueExtrinsic({
  610. accountId: sourceAccount,
  611. extrinsic: tx,
  612. txSuccessCb
  613. });
  614. }
  615. withdrawApplication (group: WorkingGroups, sourceAccount: string, id: number, txSuccessCb?: () => void) {
  616. const tx = this.apiExtrinsicByGroup(group, 'withdrawApplication')(
  617. id
  618. );
  619. this.queueExtrinsic({
  620. accountId: sourceAccount,
  621. extrinsic: tx,
  622. txSuccessCb
  623. });
  624. }
  625. }