workers.js 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. /*
  2. * This file is part of the storage node for the Joystream project.
  3. * Copyright (C) 2019 Joystream Contributors
  4. *
  5. * This program is free software: you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation, either version 3 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License
  16. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  17. */
  18. 'use strict'
  19. const debug = require('debug')('joystream:runtime:roles')
  20. const BN = require('bn.js')
  21. const { Worker } = require('@joystream/types/working-group')
  22. /*
  23. * Add worker related functionality to the substrate API.
  24. */
  25. class WorkersApi {
  26. static async create (base) {
  27. const ret = new WorkersApi()
  28. ret.base = base
  29. await ret.init()
  30. return ret
  31. }
  32. // eslint-disable-next-line class-methods-use-this, require-await
  33. async init () {
  34. debug('Init')
  35. }
  36. /*
  37. * Check whether the given account and id represent an enrolled storage provider
  38. */
  39. async isRoleAccountOfStorageProvider (storageProviderId, roleAccountId) {
  40. const id = new BN(storageProviderId)
  41. const roleAccount = this.base.identities.keyring.decodeAddress(roleAccountId)
  42. const providerAccount = await this.storageProviderRoleAccount(id)
  43. return providerAccount && providerAccount.eq(roleAccount)
  44. }
  45. /*
  46. * Returns true if the provider id is enrolled
  47. */
  48. async isStorageProvider (storageProviderId) {
  49. const worker = await this.storageWorkerByProviderId(storageProviderId)
  50. return worker !== null
  51. }
  52. /*
  53. * Returns a provider's role account or null if provider doesn't exist
  54. */
  55. async storageProviderRoleAccount (storageProviderId) {
  56. const worker = await this.storageWorkerByProviderId(storageProviderId)
  57. return worker ? worker.role_account_id : null
  58. }
  59. /*
  60. * Returns a Worker instance or null if provider does not exist
  61. */
  62. async storageWorkerByProviderId (storageProviderId) {
  63. const id = new BN(storageProviderId)
  64. const { providers } = await this.getAllProviders()
  65. return providers[id.toNumber()] || null
  66. }
  67. /*
  68. * Returns the the first found provider id with a role account or null if not found
  69. */
  70. async findProviderIdByRoleAccount (roleAccount) {
  71. const { ids, providers } = await this.getAllProviders()
  72. for (let i = 0; i < ids.length; i++) {
  73. const id = ids[i]
  74. if (providers[id].role_account_id.eq(roleAccount)) {
  75. return id
  76. }
  77. }
  78. return null
  79. }
  80. /*
  81. * Returns the set of ids and Worker instances of providers enrolled on the network
  82. */
  83. async getAllProviders () {
  84. // const workerEntries = await this.base.api.query.storageWorkingGroup.workerById()
  85. // can't rely on .isEmpty or isNone property to detect empty map
  86. // return workerEntries.isNone ? [] : workerEntries[0]
  87. // return workerEntries.isEmpty ? [] : workerEntries[0]
  88. // So we iterate over possible ids which may or may not exist, by reading directly
  89. // from storage value
  90. const nextWorkerId = (await this.base.api.query.storageWorkingGroup.nextWorkerId()).toNumber()
  91. const ids = []
  92. const providers = {}
  93. for (let id = 0; id < nextWorkerId; id++) {
  94. // We get back an Option. Will be None if value doesn't exist
  95. // eslint-disable-next-line no-await-in-loop
  96. let value = await this.base.api.rpc.state.getStorage(
  97. this.base.api.query.storageWorkingGroup.workerById.key(id)
  98. )
  99. if (!value.isNone) {
  100. // no need to read from storage again!
  101. // const worker = (await this.base.api.query.storageWorkingGroup.workerById(id))[0]
  102. value = value.unwrap()
  103. // construct the Worker type from raw data
  104. // const worker = createType('WorkerOf', value)
  105. // const worker = new Worker(value)
  106. ids.push(id)
  107. providers[id] = new Worker(value)
  108. }
  109. }
  110. return { ids, providers }
  111. }
  112. async getLeadRoleAccount() {
  113. const currentLead = await this.base.api.query.storageWorkingGroup.currentLead()
  114. if (currentLead.isSome) {
  115. const leadWorkerId = currentLead.unwrap()
  116. const worker = await this.base.api.query.storageWorkingGroup.workerById(leadWorkerId)
  117. return worker[0].role_account_id
  118. }
  119. return null
  120. }
  121. // Helper methods below don't really belong in the colossus runtime api library.
  122. // They are only used by the dev-init command in the cli to setup a development environment
  123. /*
  124. * Add a new storage group opening using the lead account. Returns the
  125. * new opening id.
  126. */
  127. async dev_addStorageOpening() {
  128. const openTx = this.dev_makeAddOpeningTx('Worker')
  129. return this.dev_submitAddOpeningTx(openTx, await this.getLeadRoleAccount())
  130. }
  131. /*
  132. * Add a new storage working group lead opening using sudo account. Returns the
  133. * new opening id.
  134. */
  135. async dev_addStorageLeadOpening() {
  136. const openTx = this.dev_makeAddOpeningTx('Leader')
  137. const sudoTx = this.base.api.tx.sudo.sudo(openTx)
  138. return this.dev_submitAddOpeningTx(sudoTx, await this.base.identities.getSudoAccount())
  139. }
  140. /*
  141. * Constructs an addOpening tx of openingType
  142. */
  143. dev_makeAddOpeningTx(openingType) {
  144. return this.base.api.tx.storageWorkingGroup.addOpening(
  145. 'CurrentBlock',
  146. {
  147. application_rationing_policy: {
  148. 'max_active_applicants': 1
  149. },
  150. max_review_period_length: 1000
  151. // default values for everything else..
  152. },
  153. 'dev-opening',
  154. openingType
  155. )
  156. }
  157. /*
  158. * Submits a tx (expecting it to dispatch storageWorkingGroup.addOpening) and returns
  159. * the OpeningId from the resulting event.
  160. */
  161. async dev_submitAddOpeningTx(tx, senderAccount) {
  162. return this.base.signAndSendThenGetEventResult(senderAccount, tx, {
  163. eventModule: 'storageWorkingGroup',
  164. eventName: 'OpeningAdded',
  165. eventProperty: 'OpeningId'
  166. })
  167. }
  168. /*
  169. * Apply on an opening, returns the application id.
  170. */
  171. async dev_applyOnOpening(openingId, memberId, memberAccount, roleAccount) {
  172. const applyTx = this.base.api.tx.storageWorkingGroup.applyOnOpening(
  173. memberId, openingId, roleAccount, null, null, `colossus-${memberId}`
  174. )
  175. return this.base.signAndSendThenGetEventResult(memberAccount, applyTx, {
  176. eventModule: 'storageWorkingGroup',
  177. eventName: 'AppliedOnOpening',
  178. eventProperty: 'ApplicationId'
  179. })
  180. }
  181. /*
  182. * Move lead opening to review state using sudo account
  183. */
  184. async dev_beginLeadOpeningReview(openingId) {
  185. const beginReviewTx = this.dev_makeBeginOpeningReviewTx(openingId)
  186. const sudoTx = this.base.api.tx.sudo.sudo(beginReviewTx)
  187. return this.base.signAndSend(await this.base.identities.getSudoAccount(), sudoTx)
  188. }
  189. /*
  190. * Move a storage opening to review state using lead account
  191. */
  192. async dev_beginStorageOpeningReview(openingId) {
  193. const beginReviewTx = this.dev_makeBeginOpeningReviewTx(openingId)
  194. return this.base.signAndSend(await this.getLeadRoleAccount(), beginReviewTx)
  195. }
  196. /*
  197. * Constructs a beingApplicantReview tx for openingId, which puts an opening into the review state
  198. */
  199. dev_makeBeginOpeningReviewTx(openingId) {
  200. return this.base.api.tx.storageWorkingGroup.beginApplicantReview(openingId)
  201. }
  202. /*
  203. * Fill a lead opening, return the assigned worker id, using the sudo account
  204. */
  205. async dev_fillLeadOpening(openingId, applicationId) {
  206. const fillTx = this.dev_makeFillOpeningTx(openingId, applicationId)
  207. const sudoTx = this.base.api.tx.sudo.sudo(fillTx)
  208. const filled = await this.dev_submitFillOpeningTx(
  209. await this.base.identities.getSudoAccount(), sudoTx)
  210. return getWorkerIdFromApplicationIdToWorkerIdMap(filled, applicationId)
  211. }
  212. /*
  213. * Fill a storage opening, return the assigned worker id, using the lead account
  214. */
  215. async dev_fillStorageOpening(openingId, applicationId) {
  216. const fillTx = this.dev_makeFillOpeningTx(openingId, applicationId)
  217. const filled = await this.dev_submitFillOpeningTx(await this.getLeadRoleAccount(), fillTx)
  218. return getWorkerIdFromApplicationIdToWorkerIdMap(filled, applicationId)
  219. }
  220. /*
  221. * Constructs a FillOpening transaction
  222. */
  223. dev_makeFillOpeningTx(openingId, applicationId) {
  224. return this.base.api.tx.storageWorkingGroup.fillOpening(openingId, [applicationId], null)
  225. }
  226. /*
  227. * Dispatches a fill opening tx and returns a map of the application id to their new assigned worker ids.
  228. */
  229. async dev_submitFillOpeningTx(senderAccount, tx) {
  230. return this.base.signAndSendThenGetEventResult(senderAccount, tx, {
  231. eventModule: 'storageWorkingGroup',
  232. eventName: 'OpeningFilled',
  233. eventProperty: 'ApplicationIdToWorkerIdMap'
  234. })
  235. }
  236. }
  237. /*
  238. * Finds assigned worker id corresponding to the application id from the resulting
  239. * ApplicationIdToWorkerIdMap map in the OpeningFilled event. Expects map to
  240. * contain at least one entry.
  241. */
  242. function getWorkerIdFromApplicationIdToWorkerIdMap (filledMap, applicationId) {
  243. if (filledMap.size === 0) {
  244. throw new Error('Expected opening to be filled!')
  245. }
  246. let ourApplicationIdKey
  247. for (let key of filledMap.keys()) {
  248. if (key.eq(applicationId)) {
  249. ourApplicationIdKey = key
  250. break
  251. }
  252. }
  253. if (!ourApplicationIdKey) {
  254. throw new Error('Expected application id to have been filled!')
  255. }
  256. const workerId = filledMap.get(ourApplicationIdKey)
  257. return workerId
  258. }
  259. module.exports = {
  260. WorkersApi
  261. }