tokenomics.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. import BaseTransport from './base';
  2. import { ApiPromise } from '@polkadot/api';
  3. import CouncilTransport from './council';
  4. import WorkingGroupsTransport from './workingGroups';
  5. import { APIQueryCache } from './APIQueryCache';
  6. import { Seats } from '@joystream/types/council';
  7. import { Option } from '@polkadot/types';
  8. import { BlockNumber, BalanceOf, Exposure } from '@polkadot/types/interfaces';
  9. import { WorkerId } from '@joystream/types/working-group';
  10. import { RewardRelationshipId, RewardRelationship } from '@joystream/types/recurring-rewards';
  11. import { StakeId, Stake } from '@joystream/types/stake';
  12. import { CuratorId, Curator, LeadId } from '@joystream/types/content-working-group';
  13. import { TokenomicsData } from '@polkadot/joy-utils/src/types/tokenomics';
  14. import { calculateValidatorsRewardsPerEra } from '../functions/staking';
  15. export default class TokenomicsTransport extends BaseTransport {
  16. private councilT: CouncilTransport;
  17. private workingGroupT: WorkingGroupsTransport;
  18. constructor (api: ApiPromise, cacheApi: APIQueryCache, councilTransport: CouncilTransport, workingGroups: WorkingGroupsTransport) {
  19. super(api, cacheApi);
  20. this.councilT = councilTransport;
  21. this.workingGroupT = workingGroups;
  22. }
  23. async councilSizeAndStake () {
  24. let totalCouncilStake = 0;
  25. const activeCouncil = await this.council.activeCouncil() as Seats;
  26. activeCouncil.map((member) => {
  27. let stakeAmount = 0;
  28. stakeAmount += member.stake.toNumber();
  29. member.backers.forEach((backer) => {
  30. stakeAmount += backer.stake.toNumber();
  31. });
  32. totalCouncilStake += stakeAmount;
  33. });
  34. return {
  35. numberOfCouncilMembers: activeCouncil.length,
  36. totalCouncilStake
  37. };
  38. }
  39. private async councilRewardsPerWeek (numberOfCouncilMembers: number) {
  40. const payoutInterval = Number((await this.api.query.council.payoutInterval() as Option<BlockNumber>).unwrapOr(0));
  41. const amountPerPayout = (await this.api.query.council.amountPerPayout() as BalanceOf).toNumber();
  42. const totalCouncilRewardsPerBlock = (amountPerPayout && payoutInterval)
  43. ? (amountPerPayout * numberOfCouncilMembers) / payoutInterval
  44. : 0;
  45. const { new_term_duration, voting_period, revealing_period, announcing_period } = await this.councilT.electionParameters();
  46. const termDuration = new_term_duration.toNumber();
  47. const votingPeriod = voting_period.toNumber();
  48. const revealingPeriod = revealing_period.toNumber();
  49. const announcingPeriod = announcing_period.toNumber();
  50. const weekInBlocks = 100800;
  51. const councilTermDurationRatio = termDuration / (termDuration + votingPeriod + revealingPeriod + announcingPeriod);
  52. const avgCouncilRewardPerBlock = councilTermDurationRatio * totalCouncilRewardsPerBlock;
  53. const avgCouncilRewardPerWeek = avgCouncilRewardPerBlock * weekInBlocks;
  54. return avgCouncilRewardPerWeek;
  55. }
  56. async getCouncilData () {
  57. const { numberOfCouncilMembers, totalCouncilStake } = await this.councilSizeAndStake();
  58. const totalCouncilRewardsInOneWeek = await this.councilRewardsPerWeek(numberOfCouncilMembers);
  59. return {
  60. numberOfCouncilMembers,
  61. totalCouncilRewardsInOneWeek,
  62. totalCouncilStake
  63. };
  64. }
  65. private async storageProviderSizeAndIds () {
  66. const stakeIds: StakeId[] = [];
  67. const rewardIds: RewardRelationshipId[] = [];
  68. let leadStakeId: StakeId | null = null;
  69. let leadRewardId: RewardRelationshipId | null = null;
  70. let numberOfStorageProviders = 0;
  71. let leadNumber = 0;
  72. const allWorkers = await this.workingGroupT.allWorkers('Storage');
  73. const currentLeadId = (await this.api.query.storageWorkingGroup.currentLead() as Option<WorkerId>).unwrapOr(null)?.toNumber();
  74. allWorkers.forEach(([workerId, worker]) => {
  75. const stakeId = worker.role_stake_profile.isSome ? worker.role_stake_profile.unwrap().stake_id : null;
  76. const rewardId = worker.reward_relationship.unwrapOr(null);
  77. if (currentLeadId !== undefined && currentLeadId === workerId.toNumber()) {
  78. leadStakeId = stakeId;
  79. leadRewardId = rewardId;
  80. leadNumber += 1;
  81. } else {
  82. numberOfStorageProviders += 1;
  83. if (stakeId) {
  84. stakeIds.push(stakeId);
  85. }
  86. if (rewardId) {
  87. rewardIds.push(rewardId);
  88. }
  89. }
  90. });
  91. return {
  92. numberOfStorageProviders,
  93. stakeIds,
  94. rewardIds,
  95. leadNumber,
  96. leadRewardId,
  97. leadStakeId
  98. };
  99. }
  100. private async storageProviderStakeAndRewards (
  101. stakeIds: StakeId[],
  102. leadStakeId: StakeId | null,
  103. rewardIds: RewardRelationshipId[],
  104. leadRewardId: RewardRelationshipId | null
  105. ) {
  106. let totalStorageProviderStake = 0;
  107. let leadStake = 0;
  108. let storageProviderRewardsPerBlock = 0;
  109. let storageLeadRewardsPerBlock = 0;
  110. (await this.api.query.stake.stakes.multi<Stake>(stakeIds)).forEach((stake) => {
  111. totalStorageProviderStake += stake.value.toNumber();
  112. });
  113. (await this.api.query.recurringRewards.rewardRelationships.multi<RewardRelationship>(rewardIds)).map((rewardRelationship) => {
  114. const amount = rewardRelationship.amount_per_payout.toNumber();
  115. const payoutInterval = rewardRelationship.payout_interval.isSome
  116. ? rewardRelationship.payout_interval.unwrap().toNumber()
  117. : null;
  118. if (amount && payoutInterval) {
  119. storageProviderRewardsPerBlock += amount / payoutInterval;
  120. }
  121. });
  122. if (leadStakeId !== null) {
  123. leadStake += (await this.api.query.stake.stakes(leadStakeId) as Stake).value.toNumber();
  124. }
  125. if (leadRewardId !== null) {
  126. const leadRewardData = (await this.api.query.recurringRewards.rewardRelationships(leadRewardId) as RewardRelationship);
  127. const leadAmount = leadRewardData.amount_per_payout.toNumber();
  128. const leadRewardInterval = leadRewardData.payout_interval.isSome ? leadRewardData.payout_interval.unwrap().toNumber() : null;
  129. if (leadAmount && leadRewardInterval) {
  130. storageLeadRewardsPerBlock += leadAmount / leadRewardInterval;
  131. }
  132. }
  133. return {
  134. totalStorageProviderStake,
  135. leadStake,
  136. storageProviderRewardsPerWeek: storageProviderRewardsPerBlock * 100800,
  137. storageProviderLeadRewardsPerWeek: storageLeadRewardsPerBlock * 100800
  138. };
  139. }
  140. async getStorageProviderData () {
  141. const { numberOfStorageProviders, leadNumber, stakeIds, rewardIds, leadRewardId, leadStakeId } = await this.storageProviderSizeAndIds();
  142. const { totalStorageProviderStake, leadStake, storageProviderRewardsPerWeek, storageProviderLeadRewardsPerWeek } =
  143. await this.storageProviderStakeAndRewards(stakeIds, leadStakeId, rewardIds, leadRewardId);
  144. return {
  145. numberOfStorageProviders,
  146. storageProviderLeadNumber: leadNumber,
  147. totalStorageProviderStake,
  148. totalStorageProviderLeadStake: leadStake,
  149. storageProviderRewardsPerWeek,
  150. storageProviderLeadRewardsPerWeek
  151. };
  152. }
  153. private async contentCuratorSizeAndIds () {
  154. const stakeIds: StakeId[] = []; const rewardIds: RewardRelationshipId[] = []; let numberOfContentCurators = 0;
  155. const contentCurators = await this.entriesByIds<CuratorId, Curator>(this.api.query.contentWorkingGroup.curatorById);
  156. const currentLeadId = (await this.api.query.contentWorkingGroup.currentLeadId() as Option<LeadId>).unwrapOr(null)?.toNumber();
  157. contentCurators.forEach(([curatorId, curator]) => {
  158. const stakeId = curator.role_stake_profile.isSome ? curator.role_stake_profile.unwrap().stake_id : null;
  159. const rewardId = curator.reward_relationship.unwrapOr(null);
  160. if (curator.is_active) {
  161. numberOfContentCurators += 1;
  162. if (stakeId) {
  163. stakeIds.push(stakeId);
  164. }
  165. if (rewardId) {
  166. rewardIds.push(rewardId);
  167. }
  168. }
  169. });
  170. return {
  171. stakeIds,
  172. rewardIds,
  173. numberOfContentCurators,
  174. contentCuratorLeadNumber: currentLeadId ? 1 : 0
  175. };
  176. }
  177. private async contentCuratorStakeAndRewards (stakeIds: StakeId[], rewardIds: RewardRelationshipId[]) {
  178. let totalContentCuratorStake = 0;
  179. let contentCuratorRewardsPerBlock = 0;
  180. (await this.api.query.stake.stakes.multi<Stake>(stakeIds)).forEach((stake) => {
  181. totalContentCuratorStake += stake.value.toNumber();
  182. });
  183. (await this.api.query.recurringRewards.rewardRelationships.multi<RewardRelationship>(rewardIds)).map((rewardRelationship) => {
  184. const amount = rewardRelationship.amount_per_payout.toNumber();
  185. const payoutInterval = rewardRelationship.payout_interval.isSome
  186. ? rewardRelationship.payout_interval.unwrap().toNumber()
  187. : null;
  188. if (amount && payoutInterval) {
  189. contentCuratorRewardsPerBlock += amount / payoutInterval;
  190. }
  191. });
  192. return {
  193. totalContentCuratorStake,
  194. contentCuratorRewardsPerBlock
  195. };
  196. }
  197. async getContentCuratorData () {
  198. const { stakeIds, rewardIds, numberOfContentCurators, contentCuratorLeadNumber } = await this.contentCuratorSizeAndIds();
  199. const { totalContentCuratorStake, contentCuratorRewardsPerBlock } = await this.contentCuratorStakeAndRewards(stakeIds, rewardIds);
  200. return {
  201. numberOfContentCurators,
  202. contentCuratorLeadNumber,
  203. totalContentCuratorStake,
  204. contentCuratorRewardsPerWeek: contentCuratorRewardsPerBlock * 100800
  205. };
  206. }
  207. async validatorSizeAndStake () {
  208. const validatorIds = await this.api.query.session.validators();
  209. const currentEra = (await this.api.query.staking.currentEra()).unwrapOr(null);
  210. let totalValidatorStake = 0; let numberOfNominators = 0;
  211. if (currentEra !== null) {
  212. const validatorStakeData = await this.api.query.staking.erasStakers.multi<Exposure>(
  213. validatorIds.map((validatorId) => [currentEra, validatorId])
  214. );
  215. validatorStakeData.forEach((data) => {
  216. if (!data.total.isEmpty) {
  217. totalValidatorStake += data.total.toNumber();
  218. }
  219. if (!data.others.isEmpty) {
  220. numberOfNominators += data.others.length;
  221. }
  222. });
  223. }
  224. return {
  225. numberOfValidators: validatorIds.length,
  226. numberOfNominators,
  227. totalValidatorStake
  228. };
  229. }
  230. async getValidatorData () {
  231. const totalIssuance = (await this.api.query.balances.totalIssuance()).toNumber();
  232. const { numberOfValidators, numberOfNominators, totalValidatorStake } = await this.validatorSizeAndStake();
  233. const validatorRewardsPerEra = calculateValidatorsRewardsPerEra(totalValidatorStake, totalIssuance);
  234. return {
  235. totalIssuance,
  236. numberOfValidators,
  237. numberOfNominators,
  238. totalValidatorStake,
  239. validatorRewardsPerWeek: validatorRewardsPerEra * 168 // Assuming 1 era = 1h
  240. };
  241. }
  242. async getTokenomicsData (): Promise<TokenomicsData> {
  243. const { numberOfCouncilMembers, totalCouncilRewardsInOneWeek, totalCouncilStake } = await this.getCouncilData();
  244. const { numberOfStorageProviders, storageProviderLeadNumber, totalStorageProviderStake, totalStorageProviderLeadStake, storageProviderLeadRewardsPerWeek, storageProviderRewardsPerWeek } = await this.getStorageProviderData();
  245. const { numberOfContentCurators, contentCuratorLeadNumber, totalContentCuratorStake, contentCuratorRewardsPerWeek } = await this.getContentCuratorData();
  246. const { numberOfValidators, numberOfNominators, totalValidatorStake, validatorRewardsPerWeek, totalIssuance } = await this.getValidatorData();
  247. const currentlyStakedTokens = totalCouncilStake + totalStorageProviderStake + totalStorageProviderLeadStake + totalContentCuratorStake + totalValidatorStake;
  248. const totalWeeklySpending = totalCouncilRewardsInOneWeek + storageProviderRewardsPerWeek + storageProviderLeadRewardsPerWeek + contentCuratorRewardsPerWeek + validatorRewardsPerWeek;
  249. const totalNumberOfActors = numberOfCouncilMembers + numberOfStorageProviders + storageProviderLeadNumber + numberOfContentCurators + contentCuratorLeadNumber + numberOfValidators;
  250. return {
  251. totalIssuance,
  252. currentlyStakedTokens,
  253. totalWeeklySpending,
  254. totalNumberOfActors,
  255. validators: {
  256. number: numberOfValidators,
  257. nominators: {
  258. number: numberOfNominators
  259. },
  260. rewardsPerWeek: validatorRewardsPerWeek,
  261. rewardsShare: validatorRewardsPerWeek / totalWeeklySpending,
  262. totalStake: totalValidatorStake,
  263. stakeShare: totalValidatorStake / currentlyStakedTokens
  264. },
  265. council: {
  266. number: numberOfCouncilMembers,
  267. rewardsPerWeek: totalCouncilRewardsInOneWeek,
  268. rewardsShare: totalCouncilRewardsInOneWeek / totalWeeklySpending,
  269. totalStake: totalCouncilStake,
  270. stakeShare: totalCouncilStake / currentlyStakedTokens
  271. },
  272. storageProviders: {
  273. number: numberOfStorageProviders,
  274. totalStake: totalStorageProviderStake,
  275. stakeShare: totalStorageProviderStake / currentlyStakedTokens,
  276. rewardsPerWeek: storageProviderRewardsPerWeek,
  277. rewardsShare: storageProviderRewardsPerWeek / totalWeeklySpending,
  278. lead: {
  279. number: storageProviderLeadNumber,
  280. totalStake: totalStorageProviderLeadStake,
  281. stakeShare: totalStorageProviderLeadStake / currentlyStakedTokens,
  282. rewardsPerWeek: storageProviderLeadRewardsPerWeek,
  283. rewardsShare: storageProviderLeadRewardsPerWeek / totalWeeklySpending
  284. }
  285. },
  286. contentCurators: {
  287. number: numberOfContentCurators,
  288. contentCuratorLead: contentCuratorLeadNumber,
  289. rewardsPerWeek: contentCuratorRewardsPerWeek,
  290. rewardsShare: contentCuratorRewardsPerWeek / totalWeeklySpending,
  291. totalStake: totalContentCuratorStake,
  292. stakeShare: totalContentCuratorStake / currentlyStakedTokens
  293. }
  294. };
  295. }
  296. }