proposals.ts 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. import {
  2. ParsedProposal,
  3. ProposalType,
  4. ProposalTypes,
  5. ProposalVote,
  6. ProposalVotes,
  7. ParsedPost,
  8. ParsedDiscussion,
  9. DiscussionContraints
  10. } from '../types/proposals';
  11. import { ParsedMember } from '../types/members';
  12. import BaseTransport from './base';
  13. import { ThreadId, PostId } from '@joystream/types/common';
  14. import { Proposal, ProposalId, VoteKind, DiscussionThread, DiscussionPost, ProposalDetails } from '@joystream/types/proposals';
  15. import { MemberId } from '@joystream/types/members';
  16. import { u32, u64, Bytes, Vec } from '@polkadot/types/';
  17. import { BalanceOf } from '@polkadot/types/interfaces';
  18. import { bytesToString } from '../functions/misc';
  19. import _ from 'lodash';
  20. import { metadata as proposalsConsts, apiMethods as proposalsApiMethods } from '../consts/proposals';
  21. import { FIRST_MEMBER_ID } from '../consts/members';
  22. import { ApiPromise } from '@polkadot/api';
  23. import MembersTransport from './members';
  24. import ChainTransport from './chain';
  25. import CouncilTransport from './council';
  26. import { blake2AsHex } from '@polkadot/util-crypto';
  27. import { APIQueryCache } from '../APIQueryCache';
  28. type ProposalDetailsCacheEntry = {
  29. type: ProposalType;
  30. details: any[];
  31. }
  32. type ProposalDetailsCache = {
  33. [id: number]: ProposalDetailsCacheEntry | undefined;
  34. }
  35. export default class ProposalsTransport extends BaseTransport {
  36. private membersT: MembersTransport;
  37. private chainT: ChainTransport;
  38. private councilT: CouncilTransport;
  39. private proposalDetailsCache: ProposalDetailsCache = {};
  40. constructor (
  41. api: ApiPromise,
  42. cacheApi: APIQueryCache,
  43. membersTransport: MembersTransport,
  44. chainTransport: ChainTransport,
  45. councilTransport: CouncilTransport
  46. ) {
  47. super(api, cacheApi);
  48. this.membersT = membersTransport;
  49. this.chainT = chainTransport;
  50. this.councilT = councilTransport;
  51. }
  52. proposalCount () {
  53. return this.proposalsEngine.proposalCount() as Promise<u32>;
  54. }
  55. rawProposalById (id: ProposalId) {
  56. return this.proposalsEngine.proposals(id) as Promise<Proposal>;
  57. }
  58. rawProposalDetails (id: ProposalId) {
  59. return this.proposalsCodex.proposalDetailsByProposalId(id) as Promise<ProposalDetails>;
  60. }
  61. cancellationFee (): number {
  62. return (this.api.consts.proposalsEngine.cancellationFee as BalanceOf).toNumber();
  63. }
  64. async typeAndDetails (id: ProposalId) {
  65. const cachedProposalDetails = this.proposalDetailsCache[id.toNumber()];
  66. // Avoid fetching runtime upgrade proposal details if we already have them cached
  67. if (cachedProposalDetails) {
  68. return cachedProposalDetails;
  69. } else {
  70. // TODO: The right typesafe handling with JoyEnum would be very useful here
  71. const rawDetails = await this.rawProposalDetails(id);
  72. const type = rawDetails.type as ProposalType;
  73. let details: any[];
  74. if (type === 'RuntimeUpgrade') {
  75. // In case of RuntimeUpgrade proposal we override details to just contain the hash and filesize
  76. // (instead of full WASM bytecode)
  77. const wasm = rawDetails.value as Bytes;
  78. details = [blake2AsHex(wasm, 256), wasm.length];
  79. } else {
  80. const detailsJSON = rawDetails.value.toJSON();
  81. details = Array.isArray(detailsJSON) ? detailsJSON : [detailsJSON];
  82. }
  83. // Save entry in cache
  84. this.proposalDetailsCache[id.toNumber()] = { type, details };
  85. return { type, details };
  86. }
  87. }
  88. async proposalById (id: ProposalId): Promise<ParsedProposal> {
  89. const { type, details } = await this.typeAndDetails(id);
  90. const rawProposal = await this.rawProposalById(id);
  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) => new ProposalId(i + 1));
  117. }
  118. async proposals () {
  119. const ids = await this.proposalsIds();
  120. return Promise.all(ids.map(id => this.proposalById(id)));
  121. }
  122. async activeProposals () {
  123. const activeProposalIds = (await this.proposalsEngine.activeProposalIds()) as Vec<ProposalId>;
  124. return Promise.all(activeProposalIds.map(id => this.proposalById(id)));
  125. }
  126. async proposedBy (member: MemberId) {
  127. const proposals = await this.proposals();
  128. return proposals.filter(({ proposerId }) => member.eq(proposerId));
  129. }
  130. async voteByProposalAndMember (proposalId: ProposalId, voterId: MemberId): Promise<VoteKind | null> {
  131. const vote = (await this.proposalsEngine.voteExistsByProposalByVoter(proposalId, voterId)) as VoteKind;
  132. const hasVoted = (await this.api.query.proposalsEngine.voteExistsByProposalByVoter.size(proposalId, voterId)).toNumber();
  133. return hasVoted ? vote : null;
  134. }
  135. async votes (proposalId: ProposalId): Promise<ProposalVotes> {
  136. const voteEntries = await this.doubleMapEntries(
  137. 'proposalsEngine.voteExistsByProposalByVoter', // Double map of intrest
  138. proposalId, // First double-map key value
  139. (v) => new VoteKind(v), // Converter from hex
  140. async () => (await this.membersT.nextMemberId()), // A function that returns the number of iterations to go through when chekcing possible values for the second double-map key (memberId)
  141. FIRST_MEMBER_ID.toNumber() // Min. possible value for second double-map key (memberId)
  142. );
  143. const votesWithMembers: ProposalVote[] = [];
  144. for (const voteEntry of voteEntries) {
  145. const memberId = voteEntry.secondKey;
  146. const vote = voteEntry.value;
  147. const parsedMember = (await this.membersT.expectedMembership(memberId)).toJSON() as ParsedMember;
  148. votesWithMembers.push({
  149. vote,
  150. member: {
  151. memberId: new MemberId(memberId),
  152. ...parsedMember
  153. }
  154. });
  155. }
  156. const proposal = await this.rawProposalById(proposalId);
  157. return {
  158. councilMembersLength: await this.councilT.councilMembersLength(proposal.createdAt.toNumber()),
  159. votes: votesWithMembers
  160. };
  161. }
  162. async parametersFromProposalType (type: ProposalType) {
  163. const methods = proposalsApiMethods[type];
  164. let votingPeriod = 0;
  165. let gracePeriod = 0;
  166. if (methods) {
  167. votingPeriod = ((await this.proposalsCodex[methods.votingPeriod]()) as u32).toNumber();
  168. gracePeriod = ((await this.proposalsCodex[methods.gracePeriod]()) as u32).toNumber();
  169. }
  170. // Currently it's same for all types, but this will change soon (?)
  171. const cancellationFee = this.cancellationFee();
  172. return {
  173. type,
  174. votingPeriod,
  175. gracePeriod,
  176. cancellationFee,
  177. ...proposalsConsts[type]
  178. };
  179. }
  180. async proposalsTypesParameters () {
  181. return Promise.all(ProposalTypes.map(type => this.parametersFromProposalType(type)));
  182. }
  183. async subscribeProposal (id: number|ProposalId, callback: () => void) {
  184. return this.api.query.proposalsEngine.proposals(id, callback);
  185. }
  186. async discussion (id: number|ProposalId): Promise<ParsedDiscussion | null> {
  187. const threadId = (await this.proposalsCodex.threadIdByProposalId(id)) as ThreadId;
  188. if (!threadId.toNumber()) {
  189. return null;
  190. }
  191. const thread = (await this.proposalsDiscussion.threadById(threadId)) as DiscussionThread;
  192. const postEntries = await this.doubleMapEntries(
  193. 'proposalsDiscussion.postThreadIdByPostId',
  194. threadId,
  195. (v) => new DiscussionPost(v),
  196. async () => ((await this.proposalsDiscussion.postCount()) as u64).toNumber()
  197. );
  198. const parsedPosts: ParsedPost[] = [];
  199. for (const { secondKey: postId, value: post } of postEntries) {
  200. parsedPosts.push({
  201. postId: new PostId(postId),
  202. threadId: post.thread_id,
  203. text: bytesToString(post.text),
  204. createdAt: await this.chainT.blockTimestamp(post.created_at.toNumber()),
  205. createdAtBlock: post.created_at.toNumber(),
  206. updatedAt: await this.chainT.blockTimestamp(post.updated_at.toNumber()),
  207. updatedAtBlock: post.updated_at.toNumber(),
  208. authorId: post.author_id,
  209. author: (await this.membersT.expectedMembership(post.author_id)),
  210. editsCount: post.edition_number.toNumber()
  211. });
  212. }
  213. // Sort by creation block asc
  214. parsedPosts.sort((a, b) => a.createdAtBlock - b.createdAtBlock);
  215. return {
  216. title: bytesToString(thread.title),
  217. threadId: threadId,
  218. posts: parsedPosts
  219. };
  220. }
  221. discussionContraints (): DiscussionContraints {
  222. return {
  223. maxPostEdits: (this.api.consts.proposalsDiscussion.maxPostEditionNumber as u32).toNumber(),
  224. maxPostLength: (this.api.consts.proposalsDiscussion.postLengthLimit as u32).toNumber()
  225. };
  226. }
  227. }