workers.js 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  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(this.base.api.query.storageWorkingGroup.workerById.key(id))
  97. if (!value.isNone) {
  98. // no need to read from storage again!
  99. // const worker = (await this.base.api.query.storageWorkingGroup.workerById(id))[0]
  100. value = value.unwrap()
  101. // construct the Worker type from raw data
  102. // const worker = createType('WorkerOf', value)
  103. // const worker = new Worker(value)
  104. ids.push(id)
  105. providers[id] = new Worker(value)
  106. }
  107. }
  108. return { ids, providers }
  109. }
  110. async getLeadRoleAccount() {
  111. const currentLead = await this.base.api.query.storageWorkingGroup.currentLead()
  112. if (currentLead.isSome) {
  113. const leadWorkerId = currentLead.unwrap()
  114. const worker = await this.base.api.query.storageWorkingGroup.workerById(leadWorkerId)
  115. return worker[0].role_account_id
  116. }
  117. return null
  118. }
  119. // Helper methods below don't really belong in the colossus runtime api library.
  120. // They are only used by the dev-init command in the cli to setup a development environment
  121. /*
  122. * Add a new storage group opening using the lead account. Returns the
  123. * new opening id.
  124. */
  125. async devAddStorageOpening() {
  126. const openTx = this.devMakeAddOpeningTx('Worker')
  127. return this.devSubmitAddOpeningTx(openTx, await this.getLeadRoleAccount())
  128. }
  129. /*
  130. * Add a new storage working group lead opening using sudo account. Returns the
  131. * new opening id.
  132. */
  133. async devAddStorageLeadOpening() {
  134. const openTx = this.devMakeAddOpeningTx('Leader')
  135. const sudoTx = this.base.api.tx.sudo.sudo(openTx)
  136. return this.devSubmitAddOpeningTx(sudoTx, await this.base.identities.getSudoAccount())
  137. }
  138. /*
  139. * Constructs an addOpening tx of openingType
  140. */
  141. devMakeAddOpeningTx(openingType) {
  142. return this.base.api.tx.storageWorkingGroup.addOpening(
  143. 'CurrentBlock',
  144. {
  145. application_rationing_policy: {
  146. max_active_applicants: 1,
  147. },
  148. max_review_period_length: 1000,
  149. // default values for everything else..
  150. },
  151. 'dev-opening',
  152. openingType
  153. )
  154. }
  155. /*
  156. * Submits a tx (expecting it to dispatch storageWorkingGroup.addOpening) and returns
  157. * the OpeningId from the resulting event.
  158. */
  159. async devSubmitAddOpeningTx(tx, senderAccount) {
  160. return this.base.signAndSendThenGetEventResult(senderAccount, tx, {
  161. eventModule: 'storageWorkingGroup',
  162. eventName: 'OpeningAdded',
  163. eventProperty: 'OpeningId',
  164. })
  165. }
  166. /*
  167. * Apply on an opening, returns the application id.
  168. */
  169. async devApplyOnOpening(openingId, memberId, memberAccount, roleAccount) {
  170. const applyTx = this.base.api.tx.storageWorkingGroup.applyOnOpening(
  171. memberId,
  172. openingId,
  173. roleAccount,
  174. null,
  175. null,
  176. `colossus-${memberId}`
  177. )
  178. return this.base.signAndSendThenGetEventResult(memberAccount, applyTx, {
  179. eventModule: 'storageWorkingGroup',
  180. eventName: 'AppliedOnOpening',
  181. eventProperty: 'ApplicationId',
  182. })
  183. }
  184. /*
  185. * Move lead opening to review state using sudo account
  186. */
  187. async devBeginLeadOpeningReview(openingId) {
  188. const beginReviewTx = this.devMakeBeginOpeningReviewTx(openingId)
  189. const sudoTx = this.base.api.tx.sudo.sudo(beginReviewTx)
  190. return this.base.signAndSend(await this.base.identities.getSudoAccount(), sudoTx)
  191. }
  192. /*
  193. * Move a storage opening to review state using lead account
  194. */
  195. async devBeginStorageOpeningReview(openingId) {
  196. const beginReviewTx = this.devMakeBeginOpeningReviewTx(openingId)
  197. return this.base.signAndSend(await this.getLeadRoleAccount(), beginReviewTx)
  198. }
  199. /*
  200. * Constructs a beingApplicantReview tx for openingId, which puts an opening into the review state
  201. */
  202. devMakeBeginOpeningReviewTx(openingId) {
  203. return this.base.api.tx.storageWorkingGroup.beginApplicantReview(openingId)
  204. }
  205. /*
  206. * Fill a lead opening, return the assigned worker id, using the sudo account
  207. */
  208. async devFillLeadOpening(openingId, applicationId) {
  209. const fillTx = this.devMakeFillOpeningTx(openingId, applicationId)
  210. const sudoTx = this.base.api.tx.sudo.sudo(fillTx)
  211. const filled = await this.devSubmitFillOpeningTx(await this.base.identities.getSudoAccount(), sudoTx)
  212. return getWorkerIdFromApplicationIdToWorkerIdMap(filled, applicationId)
  213. }
  214. /*
  215. * Fill a storage opening, return the assigned worker id, using the lead account
  216. */
  217. async devFillStorageOpening(openingId, applicationId) {
  218. const fillTx = this.devMakeFillOpeningTx(openingId, applicationId)
  219. const filled = await this.devSubmitFillOpeningTx(await this.getLeadRoleAccount(), fillTx)
  220. return getWorkerIdFromApplicationIdToWorkerIdMap(filled, applicationId)
  221. }
  222. /*
  223. * Constructs a FillOpening transaction
  224. */
  225. devMakeFillOpeningTx(openingId, applicationId) {
  226. return this.base.api.tx.storageWorkingGroup.fillOpening(openingId, [applicationId], null)
  227. }
  228. /*
  229. * Dispatches a fill opening tx and returns a map of the application id to their new assigned worker ids.
  230. */
  231. async devSubmitFillOpeningTx(senderAccount, tx) {
  232. return this.base.signAndSendThenGetEventResult(senderAccount, tx, {
  233. eventModule: 'storageWorkingGroup',
  234. eventName: 'OpeningFilled',
  235. eventProperty: 'ApplicationIdToWorkerIdMap',
  236. })
  237. }
  238. }
  239. /*
  240. * Finds assigned worker id corresponding to the application id from the resulting
  241. * ApplicationIdToWorkerIdMap map in the OpeningFilled event. Expects map to
  242. * contain at least one entry.
  243. */
  244. function getWorkerIdFromApplicationIdToWorkerIdMap(filledMap, applicationId) {
  245. if (filledMap.size === 0) {
  246. throw new Error('Expected opening to be filled!')
  247. }
  248. let ourApplicationIdKey
  249. for (const key of filledMap.keys()) {
  250. if (key.eq(applicationId)) {
  251. ourApplicationIdKey = key
  252. break
  253. }
  254. }
  255. if (!ourApplicationIdKey) {
  256. throw new Error('Expected application id to have been filled!')
  257. }
  258. const workerId = filledMap.get(ourApplicationIdKey)
  259. return workerId
  260. }
  261. module.exports = {
  262. WorkersApi,
  263. }