Browse Source

Merge pull request #1910 from mnaamani/ops/sudo-init-working-group-leads

Sudo init working group leads
Mokhtar Naamani 4 years ago
parent
commit
45cdd79fdf

+ 2 - 2
content-directory-schemas/package.json

@@ -14,9 +14,9 @@
     "generate:types": "ts-node --files ./scripts/schemasToTS.ts",
     "generate:entity-schemas": "ts-node ./scripts/inputSchemasToEntitySchemas.ts",
     "generate:all": "yarn generate:entity-schemas && yarn generate:types",
-    "initialize:alice-as-lead": "ts-node ./scripts/devInitAliceLead.ts",
+    "initialize:lead": "ts-node ./scripts/devInitContentLead.ts",
     "initialize:content-dir": "ts-node ./scripts/initializeContentDir.ts",
-    "initialize:dev": "yarn initialize:alice-as-lead && yarn initialize:content-dir",
+    "initialize:dev": "yarn initialize:lead && yarn initialize:content-dir",
     "example:createChannel": "ts-node ./examples/createChannel.ts",
     "example:createVideo": "ts-node ./examples/createVideo.ts",
     "example:updateChannelHandle": "ts-node ./examples/updateChannelHandle.ts",

+ 0 - 91
content-directory-schemas/scripts/devInitAliceLead.ts

@@ -1,91 +0,0 @@
-import { types } from '@joystream/types'
-import { ApiPromise, WsProvider } from '@polkadot/api'
-import { SubmittableExtrinsic } from '@polkadot/api/types'
-import { ExtrinsicsHelper, getAlicePair } from '../src/helpers/extrinsics'
-
-async function main() {
-  // Init api
-  const WS_URI = process.env.WS_URI || 'ws://127.0.0.1:9944'
-  console.log(`Initializing the api (${WS_URI})...`)
-  const provider = new WsProvider(WS_URI)
-  const api = await ApiPromise.create({ provider, types })
-
-  const ALICE = getAlicePair()
-
-  const txHelper = new ExtrinsicsHelper(api)
-
-  const sudo = (tx: SubmittableExtrinsic<'promise'>) => api.tx.sudo.sudo(tx)
-  const extrinsics: SubmittableExtrinsic<'promise'>[] = []
-
-  // Create membership if not already created
-  let aliceMemberId: number | undefined = (await api.query.members.memberIdsByControllerAccountId(ALICE.address))
-    .toArray()[0]
-    ?.toNumber()
-
-  if (aliceMemberId === undefined) {
-    console.log('Perparing Alice member account creation extrinsic...')
-    aliceMemberId = (await api.query.members.nextMemberId()).toNumber()
-    extrinsics.push(api.tx.members.buyMembership(0, 'alice', null, null))
-  } else {
-    console.log(`Alice member id found: ${aliceMemberId}...`)
-  }
-
-  // Set Alice as lead if lead not already set
-  if ((await api.query.contentDirectoryWorkingGroup.currentLead()).isNone) {
-    const newOpeningId = (await api.query.contentDirectoryWorkingGroup.nextOpeningId()).toNumber()
-    const newApplicationId = (await api.query.contentDirectoryWorkingGroup.nextApplicationId()).toNumber()
-    // Create curator lead opening
-    console.log('Perparing Create Curator Lead Opening extrinsic...')
-    extrinsics.push(
-      sudo(
-        api.tx.contentDirectoryWorkingGroup.addOpening(
-          { CurrentBlock: null }, // activate_at
-          { max_review_period_length: 9999 }, // OpeningPolicyCommitment
-          'api-examples curator opening', // human_readable_text
-          'Leader' // opening_type
-        )
-      )
-    )
-
-    // Apply to lead opening
-    console.log('Perparing Apply to Curator Lead Opening as Alice extrinsic...')
-    extrinsics.push(
-      api.tx.contentDirectoryWorkingGroup.applyOnOpening(
-        aliceMemberId, // member id
-        newOpeningId, // opening id
-        ALICE.address, // address
-        null, // opt role stake
-        null, // opt appl. stake
-        'api-examples curator opening appl.' // human_readable_text
-      )
-    )
-
-    // Begin review period
-    console.log('Perparing Begin Applicant Review extrinsic...')
-    extrinsics.push(sudo(api.tx.contentDirectoryWorkingGroup.beginApplicantReview(newOpeningId)))
-
-    // Fill opening
-    console.log('Perparing Fill Opening extrinsic...')
-    extrinsics.push(
-      sudo(
-        api.tx.contentDirectoryWorkingGroup.fillOpening(
-          newOpeningId, // opening id
-          api.createType('ApplicationIdSet', [newApplicationId]), // succesful applicants
-          null // reward policy
-        )
-      )
-    )
-
-    console.log('Sending extrinsics...')
-    await txHelper.sendAndCheck(ALICE, extrinsics, 'Failed to initialize Alice as Content Curators Lead!')
-  } else {
-    console.log('Curators lead already exists, skipping...')
-  }
-}
-
-main()
-  .then(() => process.exit())
-  .catch((e) => {
-    console.error(e)
-    process.exit(-1)
-  })

+ 105 - 0
content-directory-schemas/scripts/devInitContentLead.ts

@@ -0,0 +1,105 @@
+import { types } from '@joystream/types'
+import { ApiPromise, WsProvider } from '@polkadot/api'
+import { SubmittableExtrinsic } from '@polkadot/api/types'
+import { ExtrinsicsHelper, getAlicePair, getKeyFromSuri } from '../src/helpers/extrinsics'
+
+async function main() {
+  // Init api
+  const WS_URI = process.env.WS_URI || 'ws://127.0.0.1:9944'
+  console.log(`Initializing the api (${WS_URI})...`)
+  const provider = new WsProvider(WS_URI)
+  const api = await ApiPromise.create({ provider, types })
+
+  const LeadKeyPair = process.env.LEAD_URI ? getKeyFromSuri(process.env.LEAD_URI) : getAlicePair()
+  const SudoKeyPair = process.env.SUDO_URI ? getKeyFromSuri(process.env.SUDO_URI) : getAlicePair()
+
+  const txHelper = new ExtrinsicsHelper(api)
+
+  const sudo = (tx: SubmittableExtrinsic<'promise'>) => api.tx.sudo.sudo(tx)
+
+  // Create membership if not already created
+  let memberId: number | undefined = (await api.query.members.memberIdsByControllerAccountId(LeadKeyPair.address))
+    .toArray()[0]
+    ?.toNumber()
+
+  // Only buy membership if LEAD_URI is not provided
+  if (memberId === undefined && process.env.LEAD_URI) {
+    throw new Error('Make sure Controller key LEAD_URI is for a member')
+  }
+
+  if (memberId === undefined) {
+    console.log('Perparing member account creation extrinsic...')
+    memberId = (await api.query.members.nextMemberId()).toNumber()
+    await txHelper.sendAndCheck(
+      LeadKeyPair,
+      [api.tx.members.buyMembership(0, 'alice', null, null)],
+      'Failed to setup member account'
+    )
+  }
+
+  console.log(`Making member id: ${memberId} the content lead.`)
+
+  // Create a new lead opening
+  if ((await api.query.contentDirectoryWorkingGroup.currentLead()).isNone) {
+    const newOpeningId = (await api.query.contentDirectoryWorkingGroup.nextOpeningId()).toNumber()
+    const newApplicationId = (await api.query.contentDirectoryWorkingGroup.nextApplicationId()).toNumber()
+    // Create curator lead opening
+    console.log('Perparing Create Curator Lead Opening extrinsic...')
+    await txHelper.sendAndCheck(
+      SudoKeyPair,
+      [
+        sudo(
+          api.tx.contentDirectoryWorkingGroup.addOpening(
+            { CurrentBlock: null }, // activate_at
+            { max_review_period_length: 9999 }, // OpeningPolicyCommitment
+            'bootstrap curator opening', // human_readable_text
+            'Leader' // opening_type
+          )
+        ),
+      ],
+      'Failed to create Content Curators Lead opening!'
+    )
+
+    // Apply to lead opening
+    console.log('Perparing Apply to Curator Lead Opening as extrinsic...')
+    await txHelper.sendAndCheck(
+      LeadKeyPair,
+      [
+        api.tx.contentDirectoryWorkingGroup.applyOnOpening(
+          memberId, // member id
+          newOpeningId, // opening id
+          LeadKeyPair.address, // address
+          null, // opt role stake
+          null, // opt appl. stake
+          'bootstrap curator opening' // human_readable_text
+        ),
+      ],
+      'Failed to apply on lead opening!'
+    )
+
+    const extrinsics: SubmittableExtrinsic<'promise'>[] = []
+    // Begin review period
+    console.log('Perparing Begin Applicant Review extrinsic...')
+    extrinsics.push(sudo(api.tx.contentDirectoryWorkingGroup.beginApplicantReview(newOpeningId)))
+
+    // Fill opening
+    console.log('Perparing Fill Opening extrinsic...')
+    extrinsics.push(
+      sudo(
+        api.tx.contentDirectoryWorkingGroup.fillOpening(
+          newOpeningId, // opening id
+          api.createType('ApplicationIdSet', [newApplicationId]), // succesful applicants
+          null // reward policy
+        )
+      )
+    )
+
+    await txHelper.sendAndCheck(SudoKeyPair, extrinsics, 'Failed to initialize Content Curators Lead!')
+  } else {
+    console.log('Curators lead already exists, skipping...')
+  }
+}
+
+main()
+  .then(() => process.exit())
+  .catch((e) => console.error(e))

+ 5 - 5
content-directory-schemas/scripts/initializeContentDir.ts

@@ -4,7 +4,7 @@ import { getInitializationInputs } from '../src/helpers/inputs'
 import fs from 'fs'
 import path from 'path'
 import { InputParser } from '../src/helpers/InputParser'
-import { ExtrinsicsHelper, getAlicePair } from '../src/helpers/extrinsics'
+import { ExtrinsicsHelper, getAlicePair, getKeyFromSuri } from '../src/helpers/extrinsics'
 
 // Save entity operations output here for easier debugging
 const ENTITY_OPERATIONS_OUTPUT_PATH = path.join(__dirname, '../operations.json')
@@ -18,7 +18,7 @@ async function main() {
   const provider = new WsProvider(WS_URI)
   const api = await ApiPromise.create({ provider, types })
 
-  const ALICE = getAlicePair()
+  const LeadKeyPair = process.env.LEAD_URI ? getKeyFromSuri(process.env.LEAD_URI) : getAlicePair()
 
   // Emptiness check
   if ((await api.query.contentDirectory.classById.keys()).length > 0) {
@@ -31,11 +31,11 @@ async function main() {
 
   console.log(`Initializing classes (${classInputs.length} input files found)...\n`)
   const classExtrinsics = parser.getCreateClassExntrinsics()
-  await txHelper.sendAndCheck(ALICE, classExtrinsics, 'Class initialization failed!')
+  await txHelper.sendAndCheck(LeadKeyPair, classExtrinsics, 'Class initialization failed!')
 
   console.log(`Initializing schemas (${schemaInputs.length} input files found)...\n`)
   const schemaExtrinsics = await parser.getAddSchemaExtrinsics()
-  await txHelper.sendAndCheck(ALICE, schemaExtrinsics, 'Schemas initialization failed!')
+  await txHelper.sendAndCheck(LeadKeyPair, schemaExtrinsics, 'Schemas initialization failed!')
 
   console.log(`Initializing entities (${entityBatchInputs.length} input files found)`)
   const entityOperations = await parser.getEntityBatchOperations()
@@ -51,7 +51,7 @@ async function main() {
   )
   console.log(`Sending Transaction extrinsic (${entityOperations.length} operations)...`)
   await txHelper.sendAndCheck(
-    ALICE,
+    LeadKeyPair,
     [api.tx.contentDirectory.transaction({ Lead: null }, entityOperations)],
     'Entity initialization failed!'
   )

+ 8 - 1
content-directory-schemas/src/helpers/extrinsics.ts

@@ -7,7 +7,7 @@ import { TypeRegistry } from '@polkadot/types'
 
 // TODO: Move to @joystream/js soon
 
-export function getAlicePair() {
+export function getAlicePair(): KeyringPair {
   const keyring = new Keyring({ type: 'sr25519' })
   keyring.addFromUri('//Alice', { name: 'Alice' })
   const ALICE = keyring.getPairs()[0]
@@ -15,6 +15,13 @@ export function getAlicePair() {
   return ALICE
 }
 
+export function getKeyFromSuri(suri: string): KeyringPair {
+  const keyring = new Keyring({ type: 'sr25519' })
+
+  // Assume a SURI, add to keyring and return keypair
+  return keyring.addFromUri(suri)
+}
+
 export class ExtrinsicsHelper {
   api: ApiPromise
   noncesByAddress: Map<string, number>

+ 4 - 1
storage-node/packages/cli/src/cli.ts

@@ -49,7 +49,7 @@ const usage = `
   Dev Commands:       Commands to run on a development chain.
     dev-init          Setup chain with Alice as lead and storage provider.
     dev-check         Check the chain is setup with Alice as lead and storage provider.
-    vstore-init      Initialize versioned store, Requires SURI of ContentWorking Lead.
+    sudo-create-sp    Initialize the chain with a lead storage provider.
     
   Type 'storage-cli command' for the exact command usage examples.
   `
@@ -72,6 +72,9 @@ const commands = {
   'dev-check': async (api) => {
     return dev.check(api)
   },
+  'sudo-create-sp': async (api) => {
+    return dev.makeMemberInitialLeadAndStorageProvider(api)
+  },
   // Uploads the file to the system. Registers new data object in the runtime, obtains proper colossus instance URL.
   upload: async (
     api: any,

+ 128 - 1
storage-node/packages/cli/src/commands/dev.ts

@@ -3,6 +3,8 @@
 import dbug from 'debug'
 import { KeyringPair } from '@polkadot/keyring/types'
 import { RuntimeApi } from '@joystream/storage-runtime-api'
+import { GenericJoyStreamRoleSchema as HRTJson } from '@joystream/types/hiring/schemas/role.schema.typings'
+
 const debug = dbug('joystream:storage-cli:dev')
 
 // Derivation path appended to well known development seed used on
@@ -142,4 +144,129 @@ const init = async (api: RuntimeApi): Promise<any> => {
   return check(api)
 }
 
-export { init, check, aliceKeyPair, roleKeyPair, developmentPort }
+// Using sudo to create initial storage lead and worker with given keys taken from env variables.
+// Used to quickly setup a storage provider on a new chain before a council is ready.
+const makeMemberInitialLeadAndStorageProvider = async (api: RuntimeApi): Promise<any> => {
+  if (api.workers.getLeadRoleAccount()) {
+    throw new Error('The Storage Lead is already set!')
+  }
+
+  if (!process.env.SUDO_URI) {
+    throw new Error('required SUDO_URI env variable was not set')
+  }
+
+  if (!process.env.MEMBER_ID) {
+    throw new Error('required MEMBER_ID env variable was not set')
+  }
+
+  if (!process.env.MEMBER_CONTROLLER_URI) {
+    throw new Error('required MEMBER_CONTROLLER_URI env variable was not set')
+  }
+
+  if (!process.env.STORAGE_WORKER_ADDRESS) {
+    throw new Error('required STORAGE_WORKER_ADDRESS env variable was not set')
+  }
+
+  const sudoKey = getKeyFromAddressOrSuri(api, process.env.SUDO_URI)
+  const memberId = parseInt(process.env.MEMBER_ID)
+  const memberController = getKeyFromAddressOrSuri(api, process.env.MEMBER_CONTROLLER_URI).address
+  const leadAccount = memberController
+  const workerAccount = process.env.STORAGE_WORKER_ADDRESS
+
+  const sudo = await api.identities.getSudoAccount()
+
+  // Ensure correct sudo key was provided
+  if (!sudo.eq(sudoKey.address)) {
+    throw new Error('Provided SUDO_URI is not the chain sudo')
+  }
+
+  // Ensure MEMBER_ID and MEMBER_CONTROLLER_URI are valid
+  const memberIds = await api.identities.memberIdsOfController(memberController)
+  if (memberIds.find((id) => id.eq(memberId)) === undefined) {
+    throw new Error(
+      'MEMBER_ID and MEMBER_CONTROLLER_URI do not correspond to a registered member and their controller account'
+    )
+  }
+
+  // Ensure STORAGE_WORKER_ADDRESS is a valid Address
+  api.identities.keyring.decodeAddress(workerAccount)
+
+  debug(`Creating Leader with role key: ${leadAccount}`)
+  debug('Creating Lead Opening')
+  const leadOpeningId = await api.workers.devAddStorageLeadOpening(JSON.stringify(getLeadOpeningInfo()))
+  debug('Applying')
+  const leadApplicationId = await api.workers.devApplyOnOpening(leadOpeningId, memberId, memberController, leadAccount)
+  debug('Starting Review')
+  api.workers.devBeginLeadOpeningReview(leadOpeningId)
+  debug('Filling Opening')
+  await api.workers.devFillLeadOpening(leadOpeningId, leadApplicationId)
+
+  const setLeadAccount = await api.workers.getLeadRoleAccount()
+  if (!setLeadAccount.eq(leadAccount)) {
+    throw new Error('Setting Lead failed!')
+  }
+
+  // Create a storage openinging, apply, start review, and fill opening
+  debug(`Making ${workerAccount} account a storage provider.`)
+
+  const openingId = await api.workers.devAddStorageOpening(JSON.stringify(getWorkerOpeningInfo()))
+  debug(`Created new storage opening: ${openingId}`)
+
+  const applicationId = await api.workers.devApplyOnOpening(openingId, memberId, memberController, workerAccount)
+  debug(`Applied with application id: ${applicationId}`)
+
+  api.workers.devBeginStorageOpeningReview(openingId)
+
+  debug(`Filling storage opening.`)
+  const providerId = await api.workers.devFillStorageOpening(openingId, applicationId)
+
+  debug(`Assigned storage provider id: ${providerId}`)
+}
+
+function getLeadOpeningInfo(): HRTJson {
+  return {
+    'version': 1,
+    'headline': 'Initial Storage Lead',
+    'job': {
+      'title': 'Bootstrap Lead',
+      'description': 'Starting opportunity to bootstrap the network',
+    },
+    'application': {
+      'sections': [],
+    },
+    'reward': 'None',
+    'creator': {
+      'membership': {
+        'handle': 'mokhtar',
+      },
+    },
+    'process': {
+      'details': ['automated'],
+    },
+  }
+}
+
+function getWorkerOpeningInfo(): HRTJson {
+  return {
+    'version': 1,
+    'headline': 'Initial Storage Worker',
+    'job': {
+      'title': 'Bootstrap Worker',
+      'description': 'Starting opportunity to bootstrap the network',
+    },
+    'application': {
+      'sections': [],
+    },
+    'reward': 'None',
+    'creator': {
+      'membership': {
+        'handle': 'mokhtar',
+      },
+    },
+    'process': {
+      'details': ['automated'],
+    },
+  }
+}
+
+export { init, check, aliceKeyPair, roleKeyPair, developmentPort, makeMemberInitialLeadAndStorageProvider }

+ 8 - 0
storage-node/packages/runtime-api/identities.js

@@ -135,6 +135,14 @@ class IdentitiesApi {
     return this.base.api.query.members.memberIdsByRootAccountId(decoded)
   }
 
+  /*
+   * Return all the member IDs of an account by the controller account id
+   */
+  async memberIdsOfController(accountId) {
+    const decoded = this.keyring.decodeAddress(accountId)
+    return this.base.api.query.members.memberIdsByControllerAccountId(decoded)
+  }
+
   /*
    * Return the first member ID of an account, or undefined if not a member root account.
    */

+ 7 - 7
storage-node/packages/runtime-api/workers.js

@@ -150,8 +150,8 @@ class WorkersApi {
    * Add a new storage group opening using the lead account. Returns the
    * new opening id.
    */
-  async devAddStorageOpening() {
-    const openTx = this.devMakeAddOpeningTx('Worker')
+  async devAddStorageOpening(info) {
+    const openTx = this.devMakeAddOpeningTx('Worker', info)
     return this.devSubmitAddOpeningTx(openTx, await this.getLeadRoleAccount())
   }
 
@@ -159,8 +159,8 @@ class WorkersApi {
    * Add a new storage working group lead opening using sudo account. Returns the
    * new opening id.
    */
-  async devAddStorageLeadOpening() {
-    const openTx = this.devMakeAddOpeningTx('Leader')
+  async devAddStorageLeadOpening(info) {
+    const openTx = this.devMakeAddOpeningTx('Leader', info)
     const sudoTx = this.base.api.tx.sudo.sudo(openTx)
     return this.devSubmitAddOpeningTx(sudoTx, await this.base.identities.getSudoAccount())
   }
@@ -168,17 +168,17 @@ class WorkersApi {
   /*
    * Constructs an addOpening tx of openingType
    */
-  devMakeAddOpeningTx(openingType) {
+  devMakeAddOpeningTx(openingType, info) {
     return this.base.api.tx.storageWorkingGroup.addOpening(
       'CurrentBlock',
       {
         application_rationing_policy: {
           max_active_applicants: 1,
         },
-        max_review_period_length: 1000,
+        max_review_period_length: 10,
         // default values for everything else..
       },
-      'dev-opening',
+      info || 'dev-opening',
       openingType
     )
   }