proposals.ts 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. import {
  2. ParsedProposal,
  3. ProposalType,
  4. ProposalTypes,
  5. ProposalVote,
  6. ProposalVotes,
  7. ParsedPost,
  8. ParsedDiscussion,
  9. DiscussionContraints,
  10. ProposalStatusFilter,
  11. ProposalsBatch,
  12. ParsedProposalDetails
  13. } from '../types/proposals';
  14. import { ParsedMember } from '../types/members';
  15. import BaseTransport from './base';
  16. import { ThreadId, PostId } from '@joystream/types/common';
  17. import { Proposal, ProposalId, VoteKind, DiscussionThread, DiscussionPost, ProposalDetails, Finalized, ProposalDecisionStatus } from '@joystream/types/proposals';
  18. import { MemberId } from '@joystream/types/members';
  19. import { u32, Bytes, Null } from '@polkadot/types/';
  20. import { BalanceOf } from '@polkadot/types/interfaces';
  21. import { bytesToString } from '../functions/misc';
  22. import _ from 'lodash';
  23. import { metadata as proposalsConsts, apiMethods as proposalsApiMethods } from '../consts/proposals';
  24. import { ApiPromise } from '@polkadot/api';
  25. import MembersTransport from './members';
  26. import ChainTransport from './chain';
  27. import CouncilTransport from './council';
  28. import { blake2AsHex } from '@polkadot/util-crypto';
  29. import { APIQueryCache } from './APIQueryCache';
  30. type ProposalDetailsCacheEntry = {
  31. type: ProposalType;
  32. details: ParsedProposalDetails;
  33. }
  34. type ProposalDetailsCache = {
  35. [id: number]: ProposalDetailsCacheEntry | undefined;
  36. }
  37. export default class ProposalsTransport extends BaseTransport {
  38. private membersT: MembersTransport;
  39. private chainT: ChainTransport;
  40. private councilT: CouncilTransport;
  41. private proposalDetailsCache: ProposalDetailsCache = {};
  42. constructor (
  43. api: ApiPromise,
  44. cacheApi: APIQueryCache,
  45. membersTransport: MembersTransport,
  46. chainTransport: ChainTransport,
  47. councilTransport: CouncilTransport
  48. ) {
  49. super(api, cacheApi);
  50. this.membersT = membersTransport;
  51. this.chainT = chainTransport;
  52. this.councilT = councilTransport;
  53. }
  54. proposalCount () {
  55. return this.proposalsEngine.proposalCount() as Promise<u32>;
  56. }
  57. rawProposalById (id: ProposalId) {
  58. return this.proposalsEngine.proposals(id) as Promise<Proposal>;
  59. }
  60. rawProposalDetails (id: ProposalId) {
  61. return this.proposalsCodex.proposalDetailsByProposalId(id) as Promise<ProposalDetails>;
  62. }
  63. cancellationFee (): number {
  64. return (this.api.consts.proposalsEngine.cancellationFee as BalanceOf).toNumber();
  65. }
  66. async typeAndDetails (id: ProposalId) {
  67. const cachedProposalDetails = this.proposalDetailsCache[id.toNumber()];
  68. // Avoid fetching runtime upgrade proposal details if we already have them cached
  69. if (cachedProposalDetails) {
  70. return cachedProposalDetails;
  71. } else {
  72. const rawDetails = await this.rawProposalDetails(id);
  73. const type = rawDetails.type;
  74. let details: ParsedProposalDetails = rawDetails;
  75. if (type === 'RuntimeUpgrade') {
  76. // In case of RuntimeUpgrade proposal we override details to just contain the hash and filesize
  77. // (instead of full WASM bytecode)
  78. const wasm = rawDetails.value as Bytes;
  79. details = [blake2AsHex(wasm, 256), wasm.length];
  80. }
  81. // Save entry in cache
  82. this.proposalDetailsCache[id.toNumber()] = { type, details };
  83. return { type, details };
  84. }
  85. }
  86. async proposalById (id: ProposalId, rawProposal?: Proposal): Promise<ParsedProposal> {
  87. const { type, details } = await this.typeAndDetails(id);
  88. if (!rawProposal) {
  89. rawProposal = await this.rawProposalById(id);
  90. }
  91. const proposer = (await this.membersT.expectedMembership(rawProposal.proposerId)).toJSON() as ParsedMember;
  92. const proposal = rawProposal.toJSON() as {
  93. title: string;
  94. description: string;
  95. parameters: any;
  96. votingResults: any;
  97. proposerId: number;
  98. status: any;
  99. };
  100. const createdAtBlock = rawProposal.createdAt;
  101. const createdAt = await this.chainT.blockTimestamp(createdAtBlock.toNumber());
  102. const cancellationFee = this.cancellationFee();
  103. return {
  104. id,
  105. ...proposal,
  106. details,
  107. type,
  108. proposer,
  109. createdAtBlock: createdAtBlock.toJSON(),
  110. createdAt,
  111. cancellationFee
  112. };
  113. }
  114. async proposalsIds () {
  115. const total: number = (await this.proposalCount()).toNumber();
  116. return Array.from({ length: total }, (_, i) => this.api.createType('ProposalId', i + 1));
  117. }
  118. async activeProposalsIds () {
  119. const result = await this.entriesByIds<ProposalId, Null>(
  120. this.api.query.proposalsEngine.activeProposalIds
  121. )
  122. return result.map(([proposalId]) => proposalId);
  123. }
  124. async proposalsBatch (status: ProposalStatusFilter, batchNumber = 1, batchSize = 5): Promise<ProposalsBatch> {
  125. const ids = (status === 'Active' ? await this.activeProposalsIds() : await this.proposalsIds())
  126. .sort((id1, id2) => id2.cmp(id1)); // Sort by newest
  127. let rawProposalsWithIds = (await Promise.all(ids.map(id => this.rawProposalById(id))))
  128. .map((proposal, index) => ({ id: ids[index], proposal }));
  129. if (status !== 'All' && status !== 'Active') {
  130. rawProposalsWithIds = rawProposalsWithIds.filter(({ proposal }) => {
  131. if (!proposal.status.isOfType('Finalized')) {
  132. return false;
  133. }
  134. return proposal.status.asType('Finalized').proposalStatus.type === status;
  135. });
  136. }
  137. const totalBatches = Math.ceil(rawProposalsWithIds.length / batchSize);
  138. rawProposalsWithIds = rawProposalsWithIds.slice((batchNumber - 1) * batchSize, batchNumber * batchSize);
  139. const proposals = await Promise.all(rawProposalsWithIds.map(({ proposal, id }) => this.proposalById(id, proposal)));
  140. return {
  141. batchNumber,
  142. batchSize: rawProposalsWithIds.length,
  143. totalBatches,
  144. proposals
  145. };
  146. }
  147. async voteByProposalAndMember (proposalId: ProposalId, voterId: MemberId): Promise<VoteKind | null> {
  148. const vote = (await this.proposalsEngine.voteExistsByProposalByVoter(proposalId, voterId)) as VoteKind;
  149. const hasVoted = (await this.api.query.proposalsEngine.voteExistsByProposalByVoter.size(proposalId, voterId)).toNumber();
  150. return hasVoted ? vote : null;
  151. }
  152. async votes (proposalId: ProposalId): Promise<ProposalVotes> {
  153. const voteEntries = await this.entriesByIds<MemberId, VoteKind>(
  154. this.api.query.proposalsEngine.voteExistsByProposalByVoter,
  155. proposalId
  156. );
  157. const votesWithMembers: ProposalVote[] = [];
  158. for (const [memberId, vote] of voteEntries) {
  159. const parsedMember = (await this.membersT.expectedMembership(memberId)).toJSON() as ParsedMember;
  160. votesWithMembers.push({
  161. vote,
  162. member: {
  163. memberId,
  164. ...parsedMember
  165. }
  166. });
  167. }
  168. const proposal = await this.rawProposalById(proposalId);
  169. return {
  170. councilMembersLength: await this.councilT.councilMembersLength(proposal.createdAt.toNumber()),
  171. votes: votesWithMembers
  172. };
  173. }
  174. async parametersFromProposalType (type: ProposalType) {
  175. const methods = proposalsApiMethods[type];
  176. let votingPeriod = 0;
  177. let gracePeriod = 0;
  178. if (methods) {
  179. votingPeriod = ((await this.proposalsCodex[methods.votingPeriod]()) as u32).toNumber();
  180. gracePeriod = ((await this.proposalsCodex[methods.gracePeriod]()) as u32).toNumber();
  181. }
  182. // Currently it's same for all types, but this will change soon (?)
  183. const cancellationFee = this.cancellationFee();
  184. return {
  185. type,
  186. votingPeriod,
  187. gracePeriod,
  188. cancellationFee,
  189. ...proposalsConsts[type]
  190. };
  191. }
  192. async proposalsTypesParameters () {
  193. return Promise.all(ProposalTypes.map(type => this.parametersFromProposalType(type)));
  194. }
  195. async subscribeProposal (id: number|ProposalId, callback: () => void) {
  196. return this.api.query.proposalsEngine.proposals(id, callback);
  197. }
  198. async discussion (id: number|ProposalId): Promise<ParsedDiscussion | null> {
  199. const threadId = (await this.proposalsCodex.threadIdByProposalId(id)) as ThreadId;
  200. if (!threadId.toNumber()) {
  201. return null;
  202. }
  203. const thread = (await this.proposalsDiscussion.threadById(threadId)) as DiscussionThread;
  204. const postEntries = await this.entriesByIds<PostId, DiscussionPost>(
  205. this.api.query.proposalsDiscussion.postThreadIdByPostId,
  206. threadId
  207. );
  208. const parsedPosts: ParsedPost[] = [];
  209. for (const [postId, post] of postEntries) {
  210. parsedPosts.push({
  211. postId: postId,
  212. threadId: post.thread_id,
  213. text: bytesToString(post.text),
  214. createdAt: await this.chainT.blockTimestamp(post.created_at.toNumber()),
  215. createdAtBlock: post.created_at.toNumber(),
  216. updatedAt: await this.chainT.blockTimestamp(post.updated_at.toNumber()),
  217. updatedAtBlock: post.updated_at.toNumber(),
  218. authorId: post.author_id,
  219. author: (await this.membersT.expectedMembership(post.author_id)),
  220. editsCount: post.edition_number.toNumber()
  221. });
  222. }
  223. // Sort by creation block asc
  224. parsedPosts.sort((a, b) => a.createdAtBlock - b.createdAtBlock);
  225. return {
  226. title: bytesToString(thread.title),
  227. threadId: threadId,
  228. posts: parsedPosts
  229. };
  230. }
  231. discussionContraints (): DiscussionContraints {
  232. return {
  233. maxPostEdits: (this.api.consts.proposalsDiscussion.maxPostEditionNumber as u32).toNumber(),
  234. maxPostLength: (this.api.consts.proposalsDiscussion.postLengthLimit as u32).toNumber()
  235. };
  236. }
  237. }