Gleb Urvanov 4 lat temu
rodzic
commit
74bdf067cf
85 zmienionych plików z 3097 dodań i 2060 usunięć
  1. 3 3
      Cargo.lock
  2. 4 0
      node/src/chain_spec.rs
  3. 2 1
      package.json
  4. 3 3
      runtime-modules/common/src/origin.rs
  5. 1 1
      runtime-modules/content-working-group/Cargo.toml
  6. 11 1
      runtime-modules/content-working-group/src/lib.rs
  7. 1 1
      runtime-modules/hiring/Cargo.toml
  8. 4 1
      runtime-modules/hiring/src/hiring/mod.rs
  9. 0 43
      runtime-modules/hiring/src/hiring/opening.rs
  10. 0 15
      runtime-modules/hiring/src/hiring/staking_policy.rs
  11. 64 1
      runtime-modules/hiring/src/lib.rs
  12. 33 4
      runtime-modules/hiring/src/test/public_api/add_opening.rs
  13. 45 8
      runtime-modules/proposals/codex/src/lib.rs
  14. 32 4
      runtime-modules/proposals/codex/src/proposal_types/mod.rs
  15. 14 0
      runtime-modules/proposals/codex/src/proposal_types/parameters.rs
  16. 66 3
      runtime-modules/proposals/codex/src/tests/mod.rs
  17. 1 1
      runtime-modules/working-group/Cargo.toml
  18. 25 1
      runtime-modules/working-group/src/errors.rs
  19. 38 2
      runtime-modules/working-group/src/lib.rs
  20. 225 174
      runtime-modules/working-group/src/tests/mod.rs
  21. 69 71
      runtime/src/integration/proposals/proposal_encoder.rs
  22. 6 0
      runtime/src/tests/proposals_integration/mod.rs
  23. 309 30
      runtime/src/tests/proposals_integration/working_group_proposals.rs
  24. 2 0
      storage-node/.gitignore
  25. 38 7
      storage-node/README.md
  26. 0 18
      storage-node/license_header.txt
  27. 1 4
      storage-node/package.json
  28. 36 1
      storage-node/packages/cli/README.md
  29. 136 120
      storage-node/packages/cli/bin/cli.js
  30. 128 0
      storage-node/packages/cli/bin/dev.js
  31. 3 2
      storage-node/packages/cli/package.json
  32. 36 42
      storage-node/packages/colossus/README.md
  33. 166 260
      storage-node/packages/colossus/bin/cli.js
  34. 2 4
      storage-node/packages/colossus/lib/app.js
  35. 1 2
      storage-node/packages/colossus/lib/discovery.js
  36. 23 17
      storage-node/packages/colossus/lib/sync.js
  37. 6 6
      storage-node/packages/colossus/package.json
  38. 13 15
      storage-node/packages/colossus/paths/asset/v0/{id}.js
  39. 12 7
      storage-node/packages/colossus/paths/discover/v0/{id}.js
  40. 0 68
      storage-node/packages/discovery/IpfsResolver.js
  41. 0 28
      storage-node/packages/discovery/JdsResolver.js
  42. 11 21
      storage-node/packages/discovery/README.md
  43. 0 48
      storage-node/packages/discovery/Resolver.js
  44. 241 148
      storage-node/packages/discovery/discover.js
  45. 13 7
      storage-node/packages/discovery/example.js
  46. 4 3
      storage-node/packages/discovery/package.json
  47. 71 37
      storage-node/packages/discovery/publish.js
  48. 1 2
      storage-node/packages/helios/README.md
  49. 105 88
      storage-node/packages/helios/bin/cli.js
  50. 2 1
      storage-node/packages/helios/package.json
  51. 79 89
      storage-node/packages/runtime-api/assets.js
  52. 1 1
      storage-node/packages/runtime-api/balances.js
  53. 42 30
      storage-node/packages/runtime-api/discovery.js
  54. 117 116
      storage-node/packages/runtime-api/identities.js
  55. 128 118
      storage-node/packages/runtime-api/index.js
  56. 3 2
      storage-node/packages/runtime-api/package.json
  57. 0 186
      storage-node/packages/runtime-api/roles.js
  58. 1 2
      storage-node/packages/runtime-api/test/assets.js
  59. 1 4
      storage-node/packages/runtime-api/test/balances.js
  60. 1 8
      storage-node/packages/runtime-api/test/identities.js
  61. 1 1
      storage-node/packages/runtime-api/test/index.js
  62. 0 67
      storage-node/packages/runtime-api/test/roles.js
  63. 298 0
      storage-node/packages/runtime-api/workers.js
  64. 0 3
      storage-node/packages/storage/README.md
  65. 2 1
      storage-node/packages/storage/package.json
  66. 3 0
      storage-node/packages/storage/storage.js
  67. 48 55
      storage-node/packages/storage/test/storage.js
  68. 0 0
      storage-node/packages/storage/test/template/bar
  69. 0 0
      storage-node/packages/storage/test/template/foo/baz
  70. 0 1
      storage-node/packages/storage/test/template/quux
  71. 19 0
      storage-node/packages/util/externalPromise.js
  72. 2 1
      storage-node/packages/util/package.json
  73. 1 1
      storage-node/packages/util/test/fs/resolve.js
  74. 1 1
      storage-node/packages/util/test/fs/walk.js
  75. 1 1
      storage-node/packages/util/test/lru.js
  76. 1 1
      storage-node/packages/util/test/pagination.js
  77. 1 1
      storage-node/packages/util/test/ranges.js
  78. 10 6
      storage-node/scripts/compose/devchain-and-ipfs-node/docker-compose.yaml
  79. 39 0
      storage-node/scripts/run-dev-instance.sh
  80. 7 0
      storage-node/scripts/stop-dev-instance.sh
  81. 10 2
      types/src/common.ts
  82. 3 19
      types/src/content-working-group/index.ts
  83. 129 3
      types/src/proposals.ts
  84. 22 2
      types/src/working-group/index.ts
  85. 119 14
      yarn.lock

+ 3 - 3
Cargo.lock

@@ -4694,7 +4694,7 @@ dependencies = [
 
 
 [[package]]
 [[package]]
 name = "substrate-content-working-group-module"
 name = "substrate-content-working-group-module"
-version = "1.0.0"
+version = "1.0.1"
 dependencies = [
 dependencies = [
  "parity-scale-codec",
  "parity-scale-codec",
  "serde",
  "serde",
@@ -4857,7 +4857,7 @@ dependencies = [
 
 
 [[package]]
 [[package]]
 name = "substrate-hiring-module"
 name = "substrate-hiring-module"
-version = "1.0.1"
+version = "1.0.2"
 dependencies = [
 dependencies = [
  "hex-literal 0.1.4",
  "hex-literal 0.1.4",
  "mockall",
  "mockall",
@@ -5568,7 +5568,7 @@ dependencies = [
 
 
 [[package]]
 [[package]]
 name = "substrate-working-group-module"
 name = "substrate-working-group-module"
-version = "1.0.0"
+version = "1.0.1"
 dependencies = [
 dependencies = [
  "parity-scale-codec",
  "parity-scale-codec",
  "serde",
  "serde",

+ 4 - 0
node/src/chain_spec.rs

@@ -359,6 +359,10 @@ pub fn testnet_genesis(
                 .set_working_group_leader_reward_proposal_voting_period,
                 .set_working_group_leader_reward_proposal_voting_period,
             set_working_group_leader_reward_proposal_grace_period: cpcp
             set_working_group_leader_reward_proposal_grace_period: cpcp
                 .set_working_group_leader_reward_proposal_grace_period,
                 .set_working_group_leader_reward_proposal_grace_period,
+            terminate_working_group_leader_role_proposal_voting_period: cpcp
+                .terminate_working_group_leader_role_proposal_voting_period,
+            terminate_working_group_leader_role_proposal_grace_period: cpcp
+                .terminate_working_group_leader_role_proposal_grace_period,
         }),
         }),
     }
     }
 }
 }

+ 2 - 1
package.json

@@ -1,6 +1,7 @@
 {
 {
 	"private": true,
 	"private": true,
 	"name": "joystream",
 	"name": "joystream",
+	"version": "1.0.0",
 	"license": "GPL-3.0-only",
 	"license": "GPL-3.0-only",
 	"scripts": {
 	"scripts": {
 		"test": "yarn && yarn workspaces run test",
 		"test": "yarn && yarn workspaces run test",
@@ -15,7 +16,7 @@
 		"types",
 		"types",
 		"pioneer",
 		"pioneer",
 		"pioneer/packages/*",
 		"pioneer/packages/*",
-		"storage-node/",
+		"storage-node",
 		"storage-node/packages/*"
 		"storage-node/packages/*"
 	],
 	],
 	"resolutions": {
 	"resolutions": {

+ 3 - 3
runtime-modules/common/src/origin.rs

@@ -6,10 +6,10 @@ pub trait ActorOriginValidator<Origin, ActorId, AccountId> {
     fn ensure_actor_origin(origin: Origin, actor_id: ActorId) -> Result<AccountId, &'static str>;
     fn ensure_actor_origin(origin: Origin, actor_id: ActorId) -> Result<AccountId, &'static str>;
 }
 }
 
 
-// Multiplies the T::Origin.
-// In our current substrate version system::Origin doesn't support clone(),
-// but it will be supported in latest up-to-date substrate version.
 // TODO: delete when T::Origin will support the clone()
 // TODO: delete when T::Origin will support the clone()
+/// Multiplies the T::Origin.
+/// In our current substrate version system::Origin doesn't support clone(),
+/// but it will be supported in latest up-to-date substrate version.
 pub fn double_origin<T: system::Trait>(origin: T::Origin) -> (T::Origin, T::Origin) {
 pub fn double_origin<T: system::Trait>(origin: T::Origin) -> (T::Origin, T::Origin) {
     let coerced_origin = origin.into().ok().unwrap_or(RawOrigin::None);
     let coerced_origin = origin.into().ok().unwrap_or(RawOrigin::None);
 
 

+ 1 - 1
runtime-modules/content-working-group/Cargo.toml

@@ -1,6 +1,6 @@
 [package]
 [package]
 name = 'substrate-content-working-group-module'
 name = 'substrate-content-working-group-module'
-version = '1.0.0'
+version = '1.0.1'
 authors = ['Joystream contributors']
 authors = ['Joystream contributors']
 edition = '2018'
 edition = '2018'
 
 

+ 11 - 1
runtime-modules/content-working-group/src/lib.rs

@@ -239,6 +239,10 @@ pub static MSG_ORIGIN_IS_NIETHER_MEMBER_CONTROLLER_OR_ROOT: &str =
     "Origin must be controller or root account of member";
     "Origin must be controller or root account of member";
 pub static MSG_MEMBER_HAS_ACTIVE_APPLICATION_ON_OPENING: &str =
 pub static MSG_MEMBER_HAS_ACTIVE_APPLICATION_ON_OPENING: &str =
     "Member already has an active application on the opening";
     "Member already has an active application on the opening";
+pub static MSG_ADD_CURATOR_OPENING_ROLE_STAKE_CANNOT_BE_ZERO: &str =
+    "Add curator opening role stake cannot be zero";
+pub static MSG_ADD_CURATOR_OPENING_APPLICATION_STAKE_CANNOT_BE_ZERO: &str =
+    "Add curator opening application stake cannot be zero";
 
 
 /// The exit stage of a lead involvement in the working group.
 /// The exit stage of a lead involvement in the working group.
 #[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
 #[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
@@ -836,7 +840,7 @@ impl rstd::convert::From<WrappedError<hiring::AddOpeningError>> for &str {
             hiring::AddOpeningError::OpeningMustActivateInTheFuture => {
             hiring::AddOpeningError::OpeningMustActivateInTheFuture => {
                 MSG_ADD_CURATOR_OPENING_ACTIVATES_IN_THE_PAST
                 MSG_ADD_CURATOR_OPENING_ACTIVATES_IN_THE_PAST
             }
             }
-            hiring::AddOpeningError::StakeAmountLessThanMinimumCurrencyBalance(purpose) => {
+            hiring::AddOpeningError::StakeAmountLessThanMinimumStakeBalance(purpose) => {
                 match purpose {
                 match purpose {
                     hiring::StakePurpose::Role => {
                     hiring::StakePurpose::Role => {
                         MSG_ADD_CURATOR_OPENING_ROLE_STAKE_LESS_THAN_MINIMUM
                         MSG_ADD_CURATOR_OPENING_ROLE_STAKE_LESS_THAN_MINIMUM
@@ -849,6 +853,12 @@ impl rstd::convert::From<WrappedError<hiring::AddOpeningError>> for &str {
             hiring::AddOpeningError::ApplicationRationingZeroMaxApplicants => {
             hiring::AddOpeningError::ApplicationRationingZeroMaxApplicants => {
                 MSG_ADD_CURATOR_OPENING_ZERO_MAX_APPLICANT_COUNT
                 MSG_ADD_CURATOR_OPENING_ZERO_MAX_APPLICANT_COUNT
             }
             }
+            hiring::AddOpeningError::StakeAmountCannotBeZero(purpose) => match purpose {
+                hiring::StakePurpose::Role => MSG_ADD_CURATOR_OPENING_ROLE_STAKE_CANNOT_BE_ZERO,
+                hiring::StakePurpose::Application => {
+                    MSG_ADD_CURATOR_OPENING_APPLICATION_STAKE_CANNOT_BE_ZERO
+                }
+            },
         }
         }
     }
     }
 }
 }

+ 1 - 1
runtime-modules/hiring/Cargo.toml

@@ -1,6 +1,6 @@
 [package]
 [package]
 name = 'substrate-hiring-module'
 name = 'substrate-hiring-module'
-version = '1.0.1'
+version = '1.0.2'
 authors = ['Joystream contributors']
 authors = ['Joystream contributors']
 edition = '2018'
 edition = '2018'
 
 

+ 4 - 1
runtime-modules/hiring/src/hiring/mod.rs

@@ -149,9 +149,12 @@ pub enum AddOpeningError {
 
 
     /// It is not possible to stake less than the minimum balance defined in the
     /// It is not possible to stake less than the minimum balance defined in the
     /// `Currency` module.
     /// `Currency` module.
-    StakeAmountLessThanMinimumCurrencyBalance(StakePurpose),
+    StakeAmountLessThanMinimumStakeBalance(StakePurpose),
 
 
     /// It is not possible to provide application rationing policy with zero
     /// It is not possible to provide application rationing policy with zero
     /// 'max_active_applicants' parameter.
     /// 'max_active_applicants' parameter.
     ApplicationRationingZeroMaxApplicants,
     ApplicationRationingZeroMaxApplicants,
+
+    /// It is not possible to stake zero.
+    StakeAmountCannotBeZero(StakePurpose),
 }
 }

+ 0 - 43
runtime-modules/hiring/src/hiring/opening.rs

@@ -6,7 +6,6 @@ use rstd::vec::Vec;
 use codec::{Decode, Encode};
 use codec::{Decode, Encode};
 #[cfg(feature = "std")]
 #[cfg(feature = "std")]
 use serde::{Deserialize, Serialize};
 use serde::{Deserialize, Serialize};
-use srml_support::ensure;
 
 
 use crate::hiring;
 use crate::hiring;
 use crate::hiring::*;
 use crate::hiring::*;
@@ -148,48 +147,6 @@ where
             panic!("stage MUST be active")
             panic!("stage MUST be active")
         }
         }
     }
     }
-
-    /// Performs all necessary check before adding an opening
-    pub(crate) fn ensure_can_add_opening(
-        current_block_height: BlockNumber,
-        activate_at: ActivateOpeningAt<BlockNumber>,
-        runtime_minimum_balance: Balance,
-        application_rationing_policy: Option<ApplicationRationingPolicy>,
-        application_staking_policy: Option<StakingPolicy<Balance, BlockNumber>>,
-        role_staking_policy: Option<StakingPolicy<Balance, BlockNumber>>,
-    ) -> Result<(), AddOpeningError> {
-        // Check that exact activation is actually in the future
-        ensure!(
-            match activate_at {
-                ActivateOpeningAt::ExactBlock(block_number) => block_number > current_block_height,
-                _ => true,
-            },
-            AddOpeningError::OpeningMustActivateInTheFuture
-        );
-
-        if let Some(app_rationing_policy) = application_rationing_policy {
-            ensure!(
-                app_rationing_policy.max_active_applicants > 0,
-                AddOpeningError::ApplicationRationingZeroMaxApplicants
-            );
-        }
-
-        // Check that staking amounts clear minimum balance required.
-        StakingPolicy::ensure_amount_valid_in_opt_staking_policy(
-            application_staking_policy,
-            runtime_minimum_balance.clone(),
-            AddOpeningError::StakeAmountLessThanMinimumCurrencyBalance(StakePurpose::Application),
-        )?;
-
-        // Check that staking amounts clear minimum balance required.
-        StakingPolicy::ensure_amount_valid_in_opt_staking_policy(
-            role_staking_policy,
-            runtime_minimum_balance,
-            AddOpeningError::StakeAmountLessThanMinimumCurrencyBalance(StakePurpose::Role),
-        )?;
-
-        Ok(())
-    }
 }
 }
 
 
 /// The stage at which an `Opening` may be at.
 /// The stage at which an `Opening` may be at.

+ 0 - 15
runtime-modules/hiring/src/hiring/staking_policy.rs

@@ -50,21 +50,6 @@ impl<Balance: PartialOrd + Clone, BlockNumber: Clone> StakingPolicy<Balance, Blo
             None
             None
         }
         }
     }
     }
-
-    /// Ensures that optional staking policy prescribes value that clears minimum balance requirement
-    pub(crate) fn ensure_amount_valid_in_opt_staking_policy<Err>(
-        opt_staking_policy: Option<StakingPolicy<Balance, BlockNumber>>,
-        runtime_minimum_balance: Balance,
-        error: Err,
-    ) -> Result<(), Err> {
-        if let Some(ref staking_policy) = opt_staking_policy {
-            if staking_policy.amount < runtime_minimum_balance {
-                return Err(error);
-            }
-        }
-
-        Ok(())
-    }
 }
 }
 
 
 /// Constraints around staking amount
 /// Constraints around staking amount

+ 64 - 1
runtime-modules/hiring/src/lib.rs

@@ -184,7 +184,7 @@ impl<T: Trait> Module<T> {
     ) -> Result<T::OpeningId, AddOpeningError> {
     ) -> Result<T::OpeningId, AddOpeningError> {
         let current_block_height = <system::Module<T>>::block_number();
         let current_block_height = <system::Module<T>>::block_number();
 
 
-        Opening::<BalanceOf<T>, T::BlockNumber, T::ApplicationId>::ensure_can_add_opening(
+        Self::ensure_can_add_opening(
             current_block_height,
             current_block_height,
             activate_at.clone(),
             activate_at.clone(),
             T::Currency::minimum_balance(),
             T::Currency::minimum_balance(),
@@ -1406,6 +1406,69 @@ impl<T: Trait> Module<T> {
             None
             None
         }
         }
     }
     }
+
+    /// Performs all necessary check before adding an opening
+    pub(crate) fn ensure_can_add_opening(
+        current_block_height: T::BlockNumber,
+        activate_at: ActivateOpeningAt<T::BlockNumber>,
+        minimum_stake_balance: BalanceOf<T>,
+        application_rationing_policy: Option<ApplicationRationingPolicy>,
+        application_staking_policy: Option<StakingPolicy<BalanceOf<T>, T::BlockNumber>>,
+        role_staking_policy: Option<StakingPolicy<BalanceOf<T>, T::BlockNumber>>,
+    ) -> Result<(), AddOpeningError> {
+        // Check that exact activation is actually in the future
+        ensure!(
+            match activate_at {
+                ActivateOpeningAt::ExactBlock(block_number) => block_number > current_block_height,
+                _ => true,
+            },
+            AddOpeningError::OpeningMustActivateInTheFuture
+        );
+
+        if let Some(app_rationing_policy) = application_rationing_policy {
+            ensure!(
+                app_rationing_policy.max_active_applicants > 0,
+                AddOpeningError::ApplicationRationingZeroMaxApplicants
+            );
+        }
+
+        // Check that staking amounts clear minimum balance required.
+        Self::ensure_amount_valid_in_opt_staking_policy(
+            application_staking_policy,
+            minimum_stake_balance,
+            StakePurpose::Application,
+        )?;
+
+        // Check that staking amounts clear minimum balance required.
+        Self::ensure_amount_valid_in_opt_staking_policy(
+            role_staking_policy,
+            minimum_stake_balance,
+            StakePurpose::Role,
+        )?;
+
+        Ok(())
+    }
+
+    /// Ensures that optional staking policy prescribes value that clears minimum balance requirement
+    pub(crate) fn ensure_amount_valid_in_opt_staking_policy(
+        opt_staking_policy: Option<StakingPolicy<BalanceOf<T>, T::BlockNumber>>,
+        minimum_stake_balance: BalanceOf<T>,
+        stake_purpose: StakePurpose,
+    ) -> Result<(), AddOpeningError> {
+        if let Some(ref staking_policy) = opt_staking_policy {
+            ensure!(
+                staking_policy.amount > Zero::zero(),
+                AddOpeningError::StakeAmountCannotBeZero(stake_purpose)
+            );
+
+            ensure!(
+                staking_policy.amount >= minimum_stake_balance,
+                AddOpeningError::StakeAmountLessThanMinimumStakeBalance(stake_purpose)
+            );
+        }
+
+        Ok(())
+    }
 }
 }
 
 
 /*
 /*

+ 33 - 4
runtime-modules/hiring/src/test/public_api/add_opening.rs

@@ -1,6 +1,11 @@
-use crate::mock::*;
+use crate::mock::{build_test_externalities, Hiring, Test};
-use crate::test::*;
+use crate::test::{BlockNumber, OpeningId};
 use crate::StakingAmountLimitMode::Exact;
 use crate::StakingAmountLimitMode::Exact;
+use crate::*;
+use crate::{
+    ActivateOpeningAt, ActiveOpeningStage, AddOpeningError, ApplicationRationingPolicy, Opening,
+    OpeningStage, StakePurpose, StakingPolicy,
+};
 use rstd::collections::btree_set::BTreeSet;
 use rstd::collections::btree_set::BTreeSet;
 
 
 static FIRST_BLOCK_HEIGHT: <Test as system::Trait>::BlockNumber = 1;
 static FIRST_BLOCK_HEIGHT: <Test as system::Trait>::BlockNumber = 1;
@@ -143,6 +148,18 @@ fn add_opening_succeeds_or_fails_due_to_application_staking_policy() {
 
 
         opening_data.call_and_assert(Ok(0));
         opening_data.call_and_assert(Ok(0));
 
 
+        //Zero stake amount
+        opening_data.application_staking_policy = Some(StakingPolicy {
+            amount: 0,
+            amount_mode: Exact,
+            crowded_out_unstaking_period_length: None,
+            review_period_expired_unstaking_period_length: None,
+        });
+
+        opening_data.call_and_assert(Err(AddOpeningError::StakeAmountCannotBeZero(
+            StakePurpose::Application,
+        )));
+
         //Invalid stake amount
         //Invalid stake amount
         opening_data.application_staking_policy = Some(StakingPolicy {
         opening_data.application_staking_policy = Some(StakingPolicy {
             amount: 1,
             amount: 1,
@@ -152,7 +169,7 @@ fn add_opening_succeeds_or_fails_due_to_application_staking_policy() {
         });
         });
 
 
         opening_data.call_and_assert(Err(
         opening_data.call_and_assert(Err(
-            AddOpeningError::StakeAmountLessThanMinimumCurrencyBalance(StakePurpose::Application),
+            AddOpeningError::StakeAmountLessThanMinimumStakeBalance(StakePurpose::Application),
         ));
         ));
     });
     });
 }
 }
@@ -171,6 +188,18 @@ fn add_opening_succeeds_or_fails_due_to_role_staking_policy() {
 
 
         opening_data.call_and_assert(Ok(0));
         opening_data.call_and_assert(Ok(0));
 
 
+        //Zero stake amount
+        opening_data.role_staking_policy = Some(StakingPolicy {
+            amount: 0,
+            amount_mode: Exact,
+            crowded_out_unstaking_period_length: None,
+            review_period_expired_unstaking_period_length: None,
+        });
+
+        opening_data.call_and_assert(Err(AddOpeningError::StakeAmountCannotBeZero(
+            StakePurpose::Role,
+        )));
+
         //Invalid stake amount
         //Invalid stake amount
         opening_data.role_staking_policy = Some(StakingPolicy {
         opening_data.role_staking_policy = Some(StakingPolicy {
             amount: 1,
             amount: 1,
@@ -180,7 +209,7 @@ fn add_opening_succeeds_or_fails_due_to_role_staking_policy() {
         });
         });
 
 
         opening_data.call_and_assert(Err(
         opening_data.call_and_assert(Err(
-            AddOpeningError::StakeAmountLessThanMinimumCurrencyBalance(StakePurpose::Role),
+            AddOpeningError::StakeAmountLessThanMinimumStakeBalance(StakePurpose::Role),
         ));
         ));
     });
     });
 }
 }

+ 45 - 8
runtime-modules/proposals/codex/src/lib.rs

@@ -33,6 +33,7 @@
 //! - [create_decrease_working_group_leader_stake_proposal](./struct.Module.html#method.create_decrease_working_group_leader_stake_proposal)
 //! - [create_decrease_working_group_leader_stake_proposal](./struct.Module.html#method.create_decrease_working_group_leader_stake_proposal)
 //! - [create_slash_working_group_leader_stake_proposal](./struct.Module.html#method.create_slash_working_group_leader_stake_proposal)
 //! - [create_slash_working_group_leader_stake_proposal](./struct.Module.html#method.create_slash_working_group_leader_stake_proposal)
 //! - [create_set_working_group_leader_reward_proposal](./struct.Module.html#method.create_set_working_group_leader_reward_proposal)
 //! - [create_set_working_group_leader_reward_proposal](./struct.Module.html#method.create_set_working_group_leader_reward_proposal)
+//! - [create_terminate_working_group_leader_role_proposal](./struct.Module.html#method.create_terminate_working_group_leader_role_proposal)
 //!
 //!
 //! ### Proposal implementations of this module
 //! ### Proposal implementations of this module
 //! - execute_text_proposal - prints the proposal to the log
 //! - execute_text_proposal - prints the proposal to the log
@@ -59,10 +60,6 @@
 // Do not delete! Cannot be uncommented by default, because of Parity decl_module! issue.
 // Do not delete! Cannot be uncommented by default, because of Parity decl_module! issue.
 // #![warn(missing_docs)]
 // #![warn(missing_docs)]
 
 
-// TODO Working group proposals parameters & default
-// TODO Working group proposals validation limits
-// TODO module comments update.
-
 mod proposal_types;
 mod proposal_types;
 
 
 #[cfg(test)]
 #[cfg(test)]
@@ -83,7 +80,7 @@ use srml_support::{decl_error, decl_module, decl_storage, ensure, print};
 use system::ensure_root;
 use system::ensure_root;
 
 
 pub use crate::proposal_types::{
 pub use crate::proposal_types::{
-    AddOpeningParameters, FillOpeningParameters, ProposalsConfigParameters,
+    AddOpeningParameters, FillOpeningParameters, ProposalsConfigParameters, TerminateRoleParameters,
 };
 };
 pub use proposal_types::{ProposalDetails, ProposalDetailsOf, ProposalEncoder};
 pub use proposal_types::{ProposalDetails, ProposalDetailsOf, ProposalEncoder};
 
 
@@ -398,6 +395,14 @@ decl_storage! {
         /// Grace period for the 'set working group leader reward' proposal
         /// Grace period for the 'set working group leader reward' proposal
         pub SetWorkingGroupLeaderRewardProposalGracePeriod get(set_working_group_leader_reward_proposal_grace_period)
         pub SetWorkingGroupLeaderRewardProposalGracePeriod get(set_working_group_leader_reward_proposal_grace_period)
             config(): T::BlockNumber;
             config(): T::BlockNumber;
+
+        /// Voting period for the 'terminate working group leader role' proposal
+        pub TerminateWorkingGroupLeaderRoleProposalVotingPeriod get(terminate_working_group_leader_role_proposal_voting_period)
+            config(): T::BlockNumber;
+
+        /// Grace period for the 'terminate working group leader role' proposal
+        pub TerminateWorkingGroupLeaderRoleProposalGracePeriod get(terminate_working_group_leader_role_proposal_grace_period)
+            config(): T::BlockNumber;
     }
     }
 }
 }
 
 
@@ -689,7 +694,7 @@ decl_module! {
             title: Vec<u8>,
             title: Vec<u8>,
             description: Vec<u8>,
             description: Vec<u8>,
             stake_balance: Option<BalanceOf<T>>,
             stake_balance: Option<BalanceOf<T>>,
-            params: FillOpeningParameters<
+            fill_opening_parameters: FillOpeningParameters<
                 T::BlockNumber,
                 T::BlockNumber,
                 BalanceOfMint<T>,
                 BalanceOfMint<T>,
                 working_group::OpeningId<T>,
                 working_group::OpeningId<T>,
@@ -697,7 +702,7 @@ decl_module! {
             >
             >
         ) {
         ) {
 
 
-            let proposal_details = ProposalDetails::FillWorkingGroupLeaderOpening(params);
+            let proposal_details = ProposalDetails::FillWorkingGroupLeaderOpening(fill_opening_parameters);
             let params = CreateProposalParameters{
             let params = CreateProposalParameters{
                 origin,
                 origin,
                 member_id,
                 member_id,
@@ -846,6 +851,32 @@ decl_module! {
             Self::create_proposal(params)?;
             Self::create_proposal(params)?;
         }
         }
 
 
+        /// Create 'terminate working group leader rolw' proposal type.
+        /// This proposal uses `terminate_role()` extrinsic from the `working-group`  module.
+        pub fn create_terminate_working_group_leader_role_proposal(
+            origin,
+            member_id: MemberId<T>,
+            title: Vec<u8>,
+            description: Vec<u8>,
+            stake_balance: Option<BalanceOf<T>>,
+            terminate_role_parameters: TerminateRoleParameters<working_group::WorkerId<T>>,
+        ) {
+            let proposal_details = ProposalDetails::TerminateWorkingGroupLeaderRole(terminate_role_parameters);
+
+            let params = CreateProposalParameters{
+                origin,
+                member_id,
+                title,
+                description,
+                stake_balance,
+                proposal_details: proposal_details.clone(),
+                proposal_parameters: proposal_types::parameters::terminate_working_group_leader_role_proposal::<T>(),
+                proposal_code: T::ProposalEncoder::encode_proposal(proposal_details)
+            };
+
+            Self::create_proposal(params)?;
+        }
+
 
 
 // *************** Extrinsic to execute
 // *************** Extrinsic to execute
 
 
@@ -1111,5 +1142,11 @@ impl<T: Trait> Module<T> {
         <SetWorkingGroupLeaderRewardProposalGracePeriod<T>>::put(T::BlockNumber::from(
         <SetWorkingGroupLeaderRewardProposalGracePeriod<T>>::put(T::BlockNumber::from(
             p.set_working_group_leader_reward_proposal_grace_period,
             p.set_working_group_leader_reward_proposal_grace_period,
         ));
         ));
-    } //TODO set defaults for new proposals
+        <TerminateWorkingGroupLeaderRoleProposalVotingPeriod<T>>::put(T::BlockNumber::from(
+            p.terminate_working_group_leader_role_proposal_voting_period,
+        ));
+        <TerminateWorkingGroupLeaderRoleProposalGracePeriod<T>>::put(T::BlockNumber::from(
+            p.terminate_working_group_leader_role_proposal_grace_period,
+        ));
+    }
 }
 }

+ 32 - 4
runtime-modules/proposals/codex/src/proposal_types/mod.rs

@@ -85,17 +85,20 @@ pub enum ProposalDetails<
         FillOpeningParameters<BlockNumber, MintedBalance, OpeningId, ApplicationId>,
         FillOpeningParameters<BlockNumber, MintedBalance, OpeningId, ApplicationId>,
     ),
     ),
 
 
-    /// Balance for the `set working group mint capacity` proposal
+    /// Set working group mint capacity.
     SetWorkingGroupMintCapacity(MintedBalance, WorkingGroup),
     SetWorkingGroupMintCapacity(MintedBalance, WorkingGroup),
 
 
-    /// Balance for the `decrease working group leader stake` proposal
+    /// Decrease the working group leader stake.
     DecreaseWorkingGroupLeaderStake(WorkerId, StakeBalance, WorkingGroup),
     DecreaseWorkingGroupLeaderStake(WorkerId, StakeBalance, WorkingGroup),
 
 
-    /// Balance for the `slash working group leader stake` proposal
+    /// Slash the working group leader stake.
     SlashWorkingGroupLeaderStake(WorkerId, StakeBalance, WorkingGroup),
     SlashWorkingGroupLeaderStake(WorkerId, StakeBalance, WorkingGroup),
 
 
-    /// Balance for the `set working group leader reward` proposal
+    /// Set working group leader reward balance.
     SetWorkingGroupLeaderReward(WorkerId, MintedBalance, WorkingGroup),
     SetWorkingGroupLeaderReward(WorkerId, MintedBalance, WorkingGroup),
+
+    /// Fire the working group leader with possible slashing.
+    TerminateWorkingGroupLeaderRole(TerminateRoleParameters<WorkerId>),
 }
 }
 
 
 impl<
 impl<
@@ -126,6 +129,23 @@ impl<
     }
     }
 }
 }
 
 
+/// Parameters for the 'terminate the leader position' proposal.
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Clone, PartialEq, Debug)]
+pub struct TerminateRoleParameters<WorkerId> {
+    /// Leader worker id to fire.
+    pub worker_id: WorkerId,
+
+    /// Terminate role rationale.
+    pub rationale: Vec<u8>,
+
+    /// Slash the leader stake on terminating.
+    pub slash: bool,
+
+    /// Defines working group with the open position.
+    pub working_group: WorkingGroup,
+}
+
 /// Parameters for the 'fill opening for the leader position' proposal.
 /// Parameters for the 'fill opening for the leader position' proposal.
 #[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
 #[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
 #[derive(Encode, Decode, Clone, PartialEq, Debug)]
 #[derive(Encode, Decode, Clone, PartialEq, Debug)]
@@ -284,6 +304,12 @@ pub struct ProposalsConfigParameters {
 
 
     /// 'Set working group leader reward' proposal grace period
     /// 'Set working group leader reward' proposal grace period
     pub set_working_group_leader_reward_proposal_grace_period: u32,
     pub set_working_group_leader_reward_proposal_grace_period: u32,
+
+    /// 'Terminate working group leader role' proposal voting period
+    pub terminate_working_group_leader_role_proposal_voting_period: u32,
+
+    /// 'Terminate working group leader role' proposal grace period
+    pub terminate_working_group_leader_role_proposal_grace_period: u32,
 }
 }
 
 
 impl Default for ProposalsConfigParameters {
 impl Default for ProposalsConfigParameters {
@@ -317,6 +343,8 @@ impl Default for ProposalsConfigParameters {
             slash_working_group_leader_stake_proposal_grace_period: 0u32,
             slash_working_group_leader_stake_proposal_grace_period: 0u32,
             set_working_group_leader_reward_proposal_voting_period: 43200u32,
             set_working_group_leader_reward_proposal_voting_period: 43200u32,
             set_working_group_leader_reward_proposal_grace_period: 0u32,
             set_working_group_leader_reward_proposal_grace_period: 0u32,
+            terminate_working_group_leader_role_proposal_voting_period: 72200u32,
+            terminate_working_group_leader_role_proposal_grace_period: 0u32,
         }
         }
     }
     }
 }
 }

+ 14 - 0
runtime-modules/proposals/codex/src/proposal_types/parameters.rs

@@ -197,3 +197,17 @@ pub(crate) fn set_working_group_leader_reward_proposal<T: crate::Trait>(
         required_stake: Some(<BalanceOf<T>>::from(50000u32)),
         required_stake: Some(<BalanceOf<T>>::from(50000u32)),
     }
     }
 }
 }
+
+// Proposal parameters for the 'Terminate working group leader role' proposal
+pub(crate) fn terminate_working_group_leader_role_proposal<T: crate::Trait>(
+) -> ProposalParameters<T::BlockNumber, BalanceOf<T>> {
+    ProposalParameters {
+        voting_period: <Module<T>>::terminate_working_group_leader_role_proposal_voting_period(),
+        grace_period: <Module<T>>::terminate_working_group_leader_role_proposal_grace_period(),
+        approval_quorum_percentage: 66,
+        approval_threshold_percentage: 80,
+        slashing_quorum_percentage: 60,
+        slashing_threshold_percentage: 80,
+        required_stake: Some(<BalanceOf<T>>::from(100_000_u32)),
+    }
+}

+ 66 - 3
runtime-modules/proposals/codex/src/tests/mod.rs

@@ -70,7 +70,7 @@ where
 
 
     fn check_for_successful_call(&self) {
     fn check_for_successful_call(&self) {
         let account_id = 1;
         let account_id = 1;
-        let _imbalance = <Test as stake::Trait>::Currency::deposit_creating(&account_id, 50000);
+        let _imbalance = <Test as stake::Trait>::Currency::deposit_creating(&account_id, 150000);
 
 
         assert_eq!((self.successful_call)(), Ok(()));
         assert_eq!((self.successful_call)(), Ok(()));
 
 
@@ -1328,8 +1328,6 @@ fn decrease_stake_with_zero_staking_balance_fails() {
 #[test]
 #[test]
 fn create_set_working_group_leader_reward_proposal_common_checks_succeed() {
 fn create_set_working_group_leader_reward_proposal_common_checks_succeed() {
     initial_test_ext().execute_with(|| {
     initial_test_ext().execute_with(|| {
-        increase_total_balance_issuance(500000);
-
         let proposal_fixture = ProposalTestFixture {
         let proposal_fixture = ProposalTestFixture {
             insufficient_rights_call: || {
             insufficient_rights_call: || {
                 ProposalCodex::create_set_working_group_leader_reward_proposal(
                 ProposalCodex::create_set_working_group_leader_reward_proposal(
@@ -1391,3 +1389,68 @@ fn create_set_working_group_leader_reward_proposal_common_checks_succeed() {
         proposal_fixture.check_all();
         proposal_fixture.check_all();
     });
     });
 }
 }
+
+#[test]
+fn create_terminate_working_group_leader_role_proposal_common_checks_succeed() {
+    initial_test_ext().execute_with(|| {
+        increase_total_balance_issuance(500000);
+
+        let terminate_role_parameters = TerminateRoleParameters {
+            worker_id: 10,
+            rationale: Vec::new(),
+            slash: false,
+            working_group: WorkingGroup::Storage,
+        };
+
+        let proposal_fixture = ProposalTestFixture {
+            insufficient_rights_call: || {
+                ProposalCodex::create_terminate_working_group_leader_role_proposal(
+                    RawOrigin::None.into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    None,
+                    terminate_role_parameters.clone(),
+                )
+            },
+            empty_stake_call: || {
+                ProposalCodex::create_terminate_working_group_leader_role_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    None,
+                    terminate_role_parameters.clone(),
+                )
+            },
+            invalid_stake_call: || {
+                ProposalCodex::create_terminate_working_group_leader_role_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    Some(<BalanceOf<Test>>::from(5000u32)),
+                    terminate_role_parameters.clone(),
+                )
+            },
+            successful_call: || {
+                ProposalCodex::create_terminate_working_group_leader_role_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    Some(<BalanceOf<Test>>::from(100_000_u32)),
+                    terminate_role_parameters.clone(),
+                )
+            },
+            proposal_parameters:
+                crate::proposal_types::parameters::terminate_working_group_leader_role_proposal::<
+                    Test,
+                >(),
+            proposal_details: ProposalDetails::TerminateWorkingGroupLeaderRole(
+                terminate_role_parameters.clone(),
+            ),
+        };
+        proposal_fixture.check_all();
+    });
+}

+ 1 - 1
runtime-modules/working-group/Cargo.toml

@@ -1,6 +1,6 @@
 [package]
 [package]
 name = 'substrate-working-group-module'
 name = 'substrate-working-group-module'
-version = '1.0.0'
+version = '1.0.1'
 authors = ['Joystream contributors']
 authors = ['Joystream contributors']
 edition = '2018'
 edition = '2018'
 
 

+ 25 - 1
runtime-modules/working-group/src/errors.rs

@@ -247,6 +247,24 @@ decl_error! {
 
 
         /// Working group size limit exceeded.
         /// Working group size limit exceeded.
         MaxActiveWorkerNumberExceeded,
         MaxActiveWorkerNumberExceeded,
+
+        /// Add worker opening role stake cannot be zero.
+        AddWorkerOpeningRoleStakeCannotBeZero,
+
+        /// Add worker opening application stake cannot be zero.
+        AddWorkerOpeningApplicationStakeCannotBeZero,
+
+        /// Invalid OpeningPolicyCommitment parameter:
+        /// fill_opening_failed_applicant_application_stake_unstaking_period should be non-zero.
+        FillOpeningFailedApplicantApplicationStakeUnstakingPeriodIsZero,
+
+        /// Invalid OpeningPolicyCommitment parameter:
+        /// fill_opening_failed_applicant_role_stake_unstaking_period should be non-zero.
+        FillOpeningFailedApplicantRoleStakeUnstakingPeriodIsZero,
+
+        /// Invalid OpeningPolicyCommitment parameter:
+        /// fill_opening_successful_applicant_application_stake_unstaking_period should be non-zero.
+        FillOpeningSuccessfulApplicantApplicationStakeUnstakingPeriodIsZero,
     }
     }
 }
 }
 
 
@@ -294,7 +312,7 @@ impl rstd::convert::From<WrappedError<hiring::AddOpeningError>> for Error {
             hiring::AddOpeningError::OpeningMustActivateInTheFuture => {
             hiring::AddOpeningError::OpeningMustActivateInTheFuture => {
                 Error::AddWorkerOpeningActivatesInThePast
                 Error::AddWorkerOpeningActivatesInThePast
             }
             }
-            hiring::AddOpeningError::StakeAmountLessThanMinimumCurrencyBalance(purpose) => {
+            hiring::AddOpeningError::StakeAmountLessThanMinimumStakeBalance(purpose) => {
                 match purpose {
                 match purpose {
                     hiring::StakePurpose::Role => Error::AddWorkerOpeningRoleStakeLessThanMinimum,
                     hiring::StakePurpose::Role => Error::AddWorkerOpeningRoleStakeLessThanMinimum,
                     hiring::StakePurpose::Application => {
                     hiring::StakePurpose::Application => {
@@ -305,6 +323,12 @@ impl rstd::convert::From<WrappedError<hiring::AddOpeningError>> for Error {
             hiring::AddOpeningError::ApplicationRationingZeroMaxApplicants => {
             hiring::AddOpeningError::ApplicationRationingZeroMaxApplicants => {
                 Error::AddWorkerOpeningZeroMaxApplicantCount
                 Error::AddWorkerOpeningZeroMaxApplicantCount
             }
             }
+            hiring::AddOpeningError::StakeAmountCannotBeZero(purpose) => match purpose {
+                hiring::StakePurpose::Role => Error::AddWorkerOpeningRoleStakeCannotBeZero,
+                hiring::StakePurpose::Application => {
+                    Error::AddWorkerOpeningApplicationStakeCannotBeZero
+                }
+            },
         }
         }
     }
     }
 }
 }

+ 38 - 2
runtime-modules/working-group/src/lib.rs

@@ -507,8 +507,7 @@ decl_module! {
         pub fn add_opening(
         pub fn add_opening(
             origin,
             origin,
             activate_at: hiring::ActivateOpeningAt<T::BlockNumber>,
             activate_at: hiring::ActivateOpeningAt<T::BlockNumber>,
-            commitment: OpeningPolicyCommitment<T::BlockNumber,
+            commitment: OpeningPolicyCommitment<T::BlockNumber, BalanceOf<T>>,
-            BalanceOf<T>>,
             human_readable_text: Vec<u8>,
             human_readable_text: Vec<u8>,
             opening_type: OpeningType,
             opening_type: OpeningType,
         ){
         ){
@@ -516,6 +515,8 @@ decl_module! {
 
 
             Self::ensure_opening_human_readable_text_is_valid(&human_readable_text)?;
             Self::ensure_opening_human_readable_text_is_valid(&human_readable_text)?;
 
 
+            Self::ensure_opening_policy_commitment_is_valid(&commitment)?;
+
 
 
             // Add opening
             // Add opening
             // NB: This call can in principle fail, because the staking policies
             // NB: This call can in principle fail, because the staking policies
@@ -989,6 +990,41 @@ decl_module! {
 // ****************** Ensures **********************
 // ****************** Ensures **********************
 
 
 impl<T: Trait<I>, I: Instance> Module<T, I> {
 impl<T: Trait<I>, I: Instance> Module<T, I> {
+    fn ensure_opening_policy_commitment_is_valid(
+        policy_commitment: &OpeningPolicyCommitment<T::BlockNumber, BalanceOf<T>>,
+    ) -> Result<(), Error> {
+        // check fill_opening unstaking periods
+
+        if let Some(unstaking_period) =
+            policy_commitment.fill_opening_failed_applicant_application_stake_unstaking_period
+        {
+            ensure!(
+                unstaking_period != Zero::zero(),
+                Error::FillOpeningFailedApplicantApplicationStakeUnstakingPeriodIsZero
+            );
+        }
+
+        if let Some(unstaking_period) =
+            policy_commitment.fill_opening_failed_applicant_role_stake_unstaking_period
+        {
+            ensure!(
+                unstaking_period != Zero::zero(),
+                Error::FillOpeningFailedApplicantRoleStakeUnstakingPeriodIsZero
+            );
+        }
+
+        if let Some(unstaking_period) =
+            policy_commitment.fill_opening_successful_applicant_application_stake_unstaking_period
+        {
+            ensure!(
+                unstaking_period != Zero::zero(),
+                Error::FillOpeningSuccessfulApplicantApplicationStakeUnstakingPeriodIsZero
+            );
+        }
+
+        Ok(())
+    }
+
     fn ensure_origin_for_opening_type(
     fn ensure_origin_for_opening_type(
         origin: T::Origin,
         origin: T::Origin,
         opening_type: OpeningType,
         opening_type: OpeningType,

+ 225 - 174
runtime-modules/working-group/src/tests/mod.rs

@@ -57,13 +57,47 @@ fn hire_lead_fails_multiple_applications() {
 }
 }
 
 
 #[test]
 #[test]
-fn add_worker_opening_succeeds() {
+fn add_opening_fails_with_incorrect_unstaking_periods() {
     build_test_externalities().execute_with(|| {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
         HireLeadFixture::default().hire_lead();
 
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
+        let add_opening_fixture =
+            AddWorkerOpeningFixture::default().with_policy_commitment(OpeningPolicyCommitment {
+                fill_opening_failed_applicant_role_stake_unstaking_period: Some(0),
+                ..OpeningPolicyCommitment::default()
+            });
+        add_opening_fixture.call_and_assert(Err(
+            Error::FillOpeningFailedApplicantRoleStakeUnstakingPeriodIsZero,
+        ));
+
+        let add_opening_fixture =
+            AddWorkerOpeningFixture::default().with_policy_commitment(OpeningPolicyCommitment {
+                fill_opening_failed_applicant_application_stake_unstaking_period: Some(0),
+                ..OpeningPolicyCommitment::default()
+            });
+        add_opening_fixture.call_and_assert(Err(
+            Error::FillOpeningFailedApplicantApplicationStakeUnstakingPeriodIsZero,
+        ));
+
+        let add_opening_fixture =
+            AddWorkerOpeningFixture::default().with_policy_commitment(OpeningPolicyCommitment {
+                fill_opening_successful_applicant_application_stake_unstaking_period: Some(0),
+                ..OpeningPolicyCommitment::default()
+            });
+        add_opening_fixture.call_and_assert(Err(
+            Error::FillOpeningSuccessfulApplicantApplicationStakeUnstakingPeriodIsZero,
+        ));
+    });
+}
+
+#[test]
+fn add_opening_succeeds() {
+    build_test_externalities().execute_with(|| {
+        HireLeadFixture::default().hire_lead();
+
+        let add_opening_fixture = AddWorkerOpeningFixture::default();
 
 
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
 
         EventFixture::assert_last_crate_event(RawEvent::OpeningAdded(opening_id));
         EventFixture::assert_last_crate_event(RawEvent::OpeningAdded(opening_id));
     });
     });
@@ -74,10 +108,10 @@ fn add_leader_opening_succeeds_fails_with_incorrect_origin_for_opening_type() {
     build_test_externalities().execute_with(|| {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
         HireLeadFixture::default().hire_lead();
 
 
-        let add_worker_opening_fixture =
+        let add_opening_fixture =
             AddWorkerOpeningFixture::default().with_opening_type(OpeningType::Leader);
             AddWorkerOpeningFixture::default().with_opening_type(OpeningType::Leader);
 
 
-        add_worker_opening_fixture.call_and_assert(Err(Error::RequireRootOrigin));
+        add_opening_fixture.call_and_assert(Err(Error::RequireRootOrigin));
     });
     });
 }
 }
 
 
@@ -86,25 +120,25 @@ fn add_leader_opening_succeeds() {
     build_test_externalities().execute_with(|| {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
         HireLeadFixture::default().hire_lead();
 
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default()
+        let add_opening_fixture = AddWorkerOpeningFixture::default()
             .with_opening_type(OpeningType::Leader)
             .with_opening_type(OpeningType::Leader)
             .with_origin(RawOrigin::Root);
             .with_origin(RawOrigin::Root);
 
 
-        add_worker_opening_fixture.call_and_assert(Ok(()));
+        add_opening_fixture.call_and_assert(Ok(()));
     });
     });
 }
 }
 
 
 #[test]
 #[test]
-fn add_worker_opening_fails_with_lead_is_not_set() {
+fn add_opening_fails_with_lead_is_not_set() {
     build_test_externalities().execute_with(|| {
     build_test_externalities().execute_with(|| {
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
+        let add_opening_fixture = AddWorkerOpeningFixture::default();
 
 
-        add_worker_opening_fixture.call_and_assert(Err(Error::CurrentLeadNotSet));
+        add_opening_fixture.call_and_assert(Err(Error::CurrentLeadNotSet));
     });
     });
 }
 }
 
 
 #[test]
 #[test]
-fn add_worker_opening_fails_with_invalid_human_readable_text() {
+fn add_opening_fails_with_invalid_human_readable_text() {
     build_test_externalities().execute_with(|| {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
         HireLeadFixture::default().hire_lead();
 
 
@@ -115,119 +149,119 @@ fn add_worker_opening_fails_with_invalid_human_readable_text() {
             },
             },
         );
         );
 
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default().with_text(Vec::new());
+        let add_opening_fixture = AddWorkerOpeningFixture::default().with_text(Vec::new());
 
 
-        add_worker_opening_fixture.call_and_assert(Err(Error::Other("OpeningTextTooShort")));
+        add_opening_fixture.call_and_assert(Err(Error::Other("OpeningTextTooShort")));
 
 
-        let add_worker_opening_fixture =
+        let add_opening_fixture =
             AddWorkerOpeningFixture::default().with_text(b"Long text".to_vec());
             AddWorkerOpeningFixture::default().with_text(b"Long text".to_vec());
 
 
-        add_worker_opening_fixture.call_and_assert(Err(Error::Other("OpeningTextTooLong")));
+        add_opening_fixture.call_and_assert(Err(Error::Other("OpeningTextTooLong")));
     });
     });
 }
 }
 
 
 #[test]
 #[test]
-fn add_worker_opening_fails_with_hiring_error() {
+fn add_opening_fails_with_hiring_error() {
     build_test_externalities().execute_with(|| {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
         HireLeadFixture::default().hire_lead();
 
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default()
+        let add_opening_fixture = AddWorkerOpeningFixture::default()
             .with_activate_at(hiring::ActivateOpeningAt::ExactBlock(0));
             .with_activate_at(hiring::ActivateOpeningAt::ExactBlock(0));
 
 
-        add_worker_opening_fixture.call_and_assert(Err(Error::AddWorkerOpeningActivatesInThePast));
+        add_opening_fixture.call_and_assert(Err(Error::AddWorkerOpeningActivatesInThePast));
     });
     });
 }
 }
 
 
 #[test]
 #[test]
-fn accept_worker_applications_succeeds() {
+fn accept_applications_succeeds() {
     build_test_externalities().execute_with(|| {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
         HireLeadFixture::default().hire_lead();
 
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default()
+        let add_opening_fixture = AddWorkerOpeningFixture::default()
             .with_activate_at(hiring::ActivateOpeningAt::ExactBlock(5));
             .with_activate_at(hiring::ActivateOpeningAt::ExactBlock(5));
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
 
-        let accept_worker_applications_fixture =
+        let accept_applications_fixture =
             AcceptWorkerApplicationsFixture::default_for_opening_id(opening_id);
             AcceptWorkerApplicationsFixture::default_for_opening_id(opening_id);
-        accept_worker_applications_fixture.call_and_assert(Ok(()));
+        accept_applications_fixture.call_and_assert(Ok(()));
 
 
         EventFixture::assert_last_crate_event(RawEvent::AcceptedApplications(opening_id));
         EventFixture::assert_last_crate_event(RawEvent::AcceptedApplications(opening_id));
     });
     });
 }
 }
 
 
 #[test]
 #[test]
-fn accept_worker_applications_fails_for_invalid_opening_type() {
+fn accept_applications_fails_for_invalid_opening_type() {
     build_test_externalities().execute_with(|| {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
         HireLeadFixture::default().hire_lead();
 
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default()
+        let add_opening_fixture = AddWorkerOpeningFixture::default()
             .with_origin(RawOrigin::Root)
             .with_origin(RawOrigin::Root)
             .with_opening_type(OpeningType::Leader)
             .with_opening_type(OpeningType::Leader)
             .with_activate_at(hiring::ActivateOpeningAt::ExactBlock(5));
             .with_activate_at(hiring::ActivateOpeningAt::ExactBlock(5));
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
 
-        let accept_worker_applications_fixture =
+        let accept_applications_fixture =
             AcceptWorkerApplicationsFixture::default_for_opening_id(opening_id);
             AcceptWorkerApplicationsFixture::default_for_opening_id(opening_id);
-        accept_worker_applications_fixture.call_and_assert(Err(Error::RequireRootOrigin));
+        accept_applications_fixture.call_and_assert(Err(Error::RequireRootOrigin));
     });
     });
 }
 }
 
 
 #[test]
 #[test]
-fn accept_worker_applications_fails_with_hiring_error() {
+fn accept_applications_fails_with_hiring_error() {
     build_test_externalities().execute_with(|| {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
         HireLeadFixture::default().hire_lead();
 
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
+        let add_opening_fixture = AddWorkerOpeningFixture::default();
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
 
-        let accept_worker_applications_fixture =
+        let accept_applications_fixture =
             AcceptWorkerApplicationsFixture::default_for_opening_id(opening_id);
             AcceptWorkerApplicationsFixture::default_for_opening_id(opening_id);
-        accept_worker_applications_fixture.call_and_assert(Err(
+        accept_applications_fixture.call_and_assert(Err(
             Error::AcceptWorkerApplicationsOpeningIsNotWaitingToBegin,
             Error::AcceptWorkerApplicationsOpeningIsNotWaitingToBegin,
         ));
         ));
     });
     });
 }
 }
 
 
 #[test]
 #[test]
-fn accept_worker_applications_fails_with_not_lead() {
+fn accept_applications_fails_with_not_lead() {
     build_test_externalities().execute_with(|| {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
         HireLeadFixture::default().hire_lead();
 
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
+        let add_opening_fixture = AddWorkerOpeningFixture::default();
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
 
         SetLeadFixture::set_lead_with_ids(2, 2, 2);
         SetLeadFixture::set_lead_with_ids(2, 2, 2);
 
 
-        let accept_worker_applications_fixture =
+        let accept_applications_fixture =
             AcceptWorkerApplicationsFixture::default_for_opening_id(opening_id);
             AcceptWorkerApplicationsFixture::default_for_opening_id(opening_id);
-        accept_worker_applications_fixture.call_and_assert(Err(Error::IsNotLeadAccount));
+        accept_applications_fixture.call_and_assert(Err(Error::IsNotLeadAccount));
     });
     });
 }
 }
 
 
 #[test]
 #[test]
-fn accept_worker_applications_fails_with_no_opening() {
+fn accept_applications_fails_with_no_opening() {
     build_test_externalities().execute_with(|| {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
         HireLeadFixture::default().hire_lead();
 
 
         let opening_id = 55; // random opening id
         let opening_id = 55; // random opening id
 
 
-        let accept_worker_applications_fixture =
+        let accept_applications_fixture =
             AcceptWorkerApplicationsFixture::default_for_opening_id(opening_id);
             AcceptWorkerApplicationsFixture::default_for_opening_id(opening_id);
-        accept_worker_applications_fixture.call_and_assert(Err(Error::OpeningDoesNotExist));
+        accept_applications_fixture.call_and_assert(Err(Error::OpeningDoesNotExist));
     });
     });
 }
 }
 
 
 #[test]
 #[test]
-fn apply_on_worker_opening_succeeds() {
+fn apply_on_opening_succeeds() {
     build_test_externalities().execute_with(|| {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
         HireLeadFixture::default().hire_lead();
 
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
+        let add_opening_fixture = AddWorkerOpeningFixture::default();
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
 
-        let appy_on_worker_opening_fixture =
+        let apply_on_opening_fixture =
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id);
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id);
-        let application_id = appy_on_worker_opening_fixture.call_and_assert(Ok(()));
+        let application_id = apply_on_opening_fixture.call_and_assert(Ok(()));
 
 
         EventFixture::assert_last_crate_event(RawEvent::AppliedOnOpening(
         EventFixture::assert_last_crate_event(RawEvent::AppliedOnOpening(
             opening_id,
             opening_id,
@@ -237,59 +271,58 @@ fn apply_on_worker_opening_succeeds() {
 }
 }
 
 
 #[test]
 #[test]
-fn apply_on_worker_opening_fails_with_no_opening() {
+fn apply_on_opening_fails_with_no_opening() {
     build_test_externalities().execute_with(|| {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
         HireLeadFixture::default().hire_lead();
 
 
         let opening_id = 123; // random opening id
         let opening_id = 123; // random opening id
 
 
-        let appy_on_worker_opening_fixture =
+        let apply_on_opening_fixture =
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id);
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id);
-        appy_on_worker_opening_fixture.call_and_assert(Err(Error::OpeningDoesNotExist));
+        apply_on_opening_fixture.call_and_assert(Err(Error::OpeningDoesNotExist));
     });
     });
 }
 }
 
 
 #[test]
 #[test]
-fn apply_on_worker_opening_fails_with_not_set_members() {
+fn apply_on_opening_fails_with_not_set_members() {
     build_test_externalities().execute_with(|| {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
         HireLeadFixture::default().hire_lead();
 
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
+        let add_opening_fixture = AddWorkerOpeningFixture::default();
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
 
-        let appy_on_worker_opening_fixture =
+        let apply_on_opening_fixture =
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id)
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id)
                 .with_origin(RawOrigin::Signed(55), 55);
                 .with_origin(RawOrigin::Signed(55), 55);
-        appy_on_worker_opening_fixture
+        apply_on_opening_fixture.call_and_assert(Err(Error::OriginIsNeitherMemberControllerOrRoot));
-            .call_and_assert(Err(Error::OriginIsNeitherMemberControllerOrRoot));
     });
     });
 }
 }
 
 
 #[test]
 #[test]
-fn apply_on_worker_opening_fails_with_hiring_error() {
+fn apply_on_opening_fails_with_hiring_error() {
     build_test_externalities().execute_with(|| {
     build_test_externalities().execute_with(|| {
         increase_total_balance_issuance_using_account_id(1, 500000);
         increase_total_balance_issuance_using_account_id(1, 500000);
         HireLeadFixture::default().hire_lead();
         HireLeadFixture::default().hire_lead();
 
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
+        let add_opening_fixture = AddWorkerOpeningFixture::default();
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
 
-        let appy_on_worker_opening_fixture =
+        let apply_on_opening_fixture =
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id)
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id)
                 .with_application_stake(100);
                 .with_application_stake(100);
-        appy_on_worker_opening_fixture
+        apply_on_opening_fixture
             .call_and_assert(Err(Error::AddWorkerOpeningStakeProvidedWhenRedundant));
             .call_and_assert(Err(Error::AddWorkerOpeningStakeProvidedWhenRedundant));
     });
     });
 }
 }
 
 
 #[test]
 #[test]
-fn apply_on_worker_opening_fails_with_invalid_application_stake() {
+fn apply_on_opening_fails_with_invalid_application_stake() {
     build_test_externalities().execute_with(|| {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
         HireLeadFixture::default().hire_lead();
 
 
         let stake = 100;
         let stake = 100;
 
 
-        let add_worker_opening_fixture =
+        let add_opening_fixture =
             AddWorkerOpeningFixture::default().with_policy_commitment(OpeningPolicyCommitment {
             AddWorkerOpeningFixture::default().with_policy_commitment(OpeningPolicyCommitment {
                 application_staking_policy: Some(hiring::StakingPolicy {
                 application_staking_policy: Some(hiring::StakingPolicy {
                     amount: stake,
                     amount: stake,
@@ -297,24 +330,45 @@ fn apply_on_worker_opening_fails_with_invalid_application_stake() {
                 }),
                 }),
                 ..OpeningPolicyCommitment::default()
                 ..OpeningPolicyCommitment::default()
             });
             });
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
 
-        let appy_on_worker_opening_fixture =
+        let apply_on_opening_fixture =
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id)
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id)
                 .with_origin(RawOrigin::Signed(2), 2)
                 .with_origin(RawOrigin::Signed(2), 2)
                 .with_application_stake(stake);
                 .with_application_stake(stake);
-        appy_on_worker_opening_fixture.call_and_assert(Err(Error::InsufficientBalanceToApply));
+        apply_on_opening_fixture.call_and_assert(Err(Error::InsufficientBalanceToApply));
+    });
+}
+
+#[test]
+fn add_opening_fails_with_invalid_zero_application_stake() {
+    build_test_externalities().execute_with(|| {
+        HireLeadFixture::default().hire_lead();
+
+        let zero_stake = 0;
+
+        let add_opening_fixture =
+            AddWorkerOpeningFixture::default().with_policy_commitment(OpeningPolicyCommitment {
+                application_staking_policy: Some(hiring::StakingPolicy {
+                    amount: zero_stake,
+                    amount_mode: hiring::StakingAmountLimitMode::AtLeast,
+                    ..hiring::StakingPolicy::default()
+                }),
+                ..OpeningPolicyCommitment::default()
+            });
+        add_opening_fixture
+            .call_and_assert(Err(Error::AddWorkerOpeningApplicationStakeCannotBeZero));
     });
     });
 }
 }
 
 
 #[test]
 #[test]
-fn apply_on_worker_opening_fails_with_invalid_role_stake() {
+fn apply_on_opening_fails_with_invalid_role_stake() {
     build_test_externalities().execute_with(|| {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
         HireLeadFixture::default().hire_lead();
 
 
         let stake = 100;
         let stake = 100;
 
 
-        let add_worker_opening_fixture =
+        let add_opening_fixture =
             AddWorkerOpeningFixture::default().with_policy_commitment(OpeningPolicyCommitment {
             AddWorkerOpeningFixture::default().with_policy_commitment(OpeningPolicyCommitment {
                 role_staking_policy: Some(hiring::StakingPolicy {
                 role_staking_policy: Some(hiring::StakingPolicy {
                     amount: stake,
                     amount: stake,
@@ -322,23 +376,23 @@ fn apply_on_worker_opening_fails_with_invalid_role_stake() {
                 }),
                 }),
                 ..OpeningPolicyCommitment::default()
                 ..OpeningPolicyCommitment::default()
             });
             });
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
 
-        let appy_on_worker_opening_fixture =
+        let apply_on_opening_fixture =
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id)
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id)
                 .with_role_stake(Some(stake))
                 .with_role_stake(Some(stake))
                 .with_origin(RawOrigin::Signed(2), 2);
                 .with_origin(RawOrigin::Signed(2), 2);
-        appy_on_worker_opening_fixture.call_and_assert(Err(Error::InsufficientBalanceToApply));
+        apply_on_opening_fixture.call_and_assert(Err(Error::InsufficientBalanceToApply));
     });
     });
 }
 }
 
 
 #[test]
 #[test]
-fn apply_on_worker_opening_fails_with_invalid_text() {
+fn apply_on_opening_fails_with_invalid_text() {
     build_test_externalities().execute_with(|| {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
         HireLeadFixture::default().hire_lead();
 
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
+        let add_opening_fixture = AddWorkerOpeningFixture::default();
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
 
         <crate::WorkerApplicationHumanReadableText<TestWorkingGroupInstance>>::put(
         <crate::WorkerApplicationHumanReadableText<TestWorkingGroupInstance>>::put(
             InputValidationLengthConstraint {
             InputValidationLengthConstraint {
@@ -347,33 +401,31 @@ fn apply_on_worker_opening_fails_with_invalid_text() {
             },
             },
         );
         );
 
 
-        let appy_on_worker_opening_fixture =
+        let apply_on_opening_fixture =
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id).with_text(Vec::new());
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id).with_text(Vec::new());
-        appy_on_worker_opening_fixture
+        apply_on_opening_fixture
             .call_and_assert(Err(Error::Other("WorkerApplicationTextTooShort")));
             .call_and_assert(Err(Error::Other("WorkerApplicationTextTooShort")));
 
 
-        let appy_on_worker_opening_fixture =
+        let apply_on_opening_fixture =
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id)
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id)
                 .with_text(b"Long text".to_vec());
                 .with_text(b"Long text".to_vec());
-        appy_on_worker_opening_fixture
+        apply_on_opening_fixture.call_and_assert(Err(Error::Other("WorkerApplicationTextTooLong")));
-            .call_and_assert(Err(Error::Other("WorkerApplicationTextTooLong")));
     });
     });
 }
 }
 
 
 #[test]
 #[test]
-fn apply_on_worker_opening_fails_with_already_active_application() {
+fn apply_on_opening_fails_with_already_active_application() {
     build_test_externalities().execute_with(|| {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
         HireLeadFixture::default().hire_lead();
 
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
+        let add_opening_fixture = AddWorkerOpeningFixture::default();
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
 
-        let appy_on_worker_opening_fixture =
+        let apply_on_opening_fixture =
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id);
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id);
-        appy_on_worker_opening_fixture.call_and_assert(Ok(()));
+        apply_on_opening_fixture.call_and_assert(Ok(()));
 
 
-        appy_on_worker_opening_fixture
+        apply_on_opening_fixture.call_and_assert(Err(Error::MemberHasActiveApplicationOnOpening));
-            .call_and_assert(Err(Error::MemberHasActiveApplicationOnOpening));
     });
     });
 }
 }
 
 
@@ -382,12 +434,12 @@ fn withdraw_worker_application_succeeds() {
     build_test_externalities().execute_with(|| {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
         HireLeadFixture::default().hire_lead();
 
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
+        let add_opening_fixture = AddWorkerOpeningFixture::default();
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
 
-        let appy_on_worker_opening_fixture =
+        let apply_on_opening_fixture =
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id);
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id);
-        let application_id = appy_on_worker_opening_fixture.call_and_assert(Ok(()));
+        let application_id = apply_on_opening_fixture.call_and_assert(Ok(()));
 
 
         let withdraw_application_fixture =
         let withdraw_application_fixture =
             WithdrawApplicationFixture::default_for_application_id(application_id);
             WithdrawApplicationFixture::default_for_application_id(application_id);
@@ -413,12 +465,12 @@ fn withdraw_worker_application_fails_invalid_origin() {
     build_test_externalities().execute_with(|| {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
         HireLeadFixture::default().hire_lead();
 
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
+        let add_opening_fixture = AddWorkerOpeningFixture::default();
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
 
-        let appy_on_worker_opening_fixture =
+        let apply_on_opening_fixture =
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id);
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id);
-        let application_id = appy_on_worker_opening_fixture.call_and_assert(Ok(()));
+        let application_id = apply_on_opening_fixture.call_and_assert(Ok(()));
 
 
         let withdraw_application_fixture =
         let withdraw_application_fixture =
             WithdrawApplicationFixture::default_for_application_id(application_id)
             WithdrawApplicationFixture::default_for_application_id(application_id)
@@ -432,12 +484,12 @@ fn withdraw_worker_application_fails_with_invalid_application_author() {
     build_test_externalities().execute_with(|| {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
         HireLeadFixture::default().hire_lead();
 
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
+        let add_opening_fixture = AddWorkerOpeningFixture::default();
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
 
-        let appy_on_worker_opening_fixture =
+        let apply_on_opening_fixture =
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id);
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id);
-        let application_id = appy_on_worker_opening_fixture.call_and_assert(Ok(()));
+        let application_id = apply_on_opening_fixture.call_and_assert(Ok(()));
 
 
         let invalid_author_account_id = 55;
         let invalid_author_account_id = 55;
         let withdraw_application_fixture =
         let withdraw_application_fixture =
@@ -452,12 +504,12 @@ fn withdraw_worker_application_fails_with_hiring_error() {
     build_test_externalities().execute_with(|| {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
         HireLeadFixture::default().hire_lead();
 
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
+        let add_opening_fixture = AddWorkerOpeningFixture::default();
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
 
-        let appy_on_worker_opening_fixture =
+        let apply_on_opening_fixture =
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id);
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id);
-        let application_id = appy_on_worker_opening_fixture.call_and_assert(Ok(()));
+        let application_id = apply_on_opening_fixture.call_and_assert(Ok(()));
 
 
         let withdraw_application_fixture =
         let withdraw_application_fixture =
             WithdrawApplicationFixture::default_for_application_id(application_id);
             WithdrawApplicationFixture::default_for_application_id(application_id);
@@ -472,12 +524,12 @@ fn terminate_worker_application_succeeds() {
     build_test_externalities().execute_with(|| {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
         HireLeadFixture::default().hire_lead();
 
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
+        let add_opening_fixture = AddWorkerOpeningFixture::default();
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
 
-        let appy_on_worker_opening_fixture =
+        let apply_on_opening_fixture =
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id);
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id);
-        let application_id = appy_on_worker_opening_fixture.call_and_assert(Ok(()));
+        let application_id = apply_on_opening_fixture.call_and_assert(Ok(()));
 
 
         let terminate_application_fixture =
         let terminate_application_fixture =
             TerminateApplicationFixture::default_for_application_id(application_id);
             TerminateApplicationFixture::default_for_application_id(application_id);
@@ -492,12 +544,12 @@ fn terminate_worker_application_fails_with_invalid_application_author() {
     build_test_externalities().execute_with(|| {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
         HireLeadFixture::default().hire_lead();
 
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
+        let add_opening_fixture = AddWorkerOpeningFixture::default();
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
 
-        let appy_on_worker_opening_fixture =
+        let apply_on_opening_fixture =
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id);
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id);
-        let application_id = appy_on_worker_opening_fixture.call_and_assert(Ok(()));
+        let application_id = apply_on_opening_fixture.call_and_assert(Ok(()));
 
 
         let invalid_author_account_id = 55;
         let invalid_author_account_id = 55;
         let terminate_application_fixture =
         let terminate_application_fixture =
@@ -512,12 +564,12 @@ fn terminate_worker_application_fails_invalid_origin() {
     build_test_externalities().execute_with(|| {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
         HireLeadFixture::default().hire_lead();
 
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
+        let add_opening_fixture = AddWorkerOpeningFixture::default();
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
 
-        let appy_on_worker_opening_fixture =
+        let apply_on_opening_fixture =
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id);
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id);
-        let application_id = appy_on_worker_opening_fixture.call_and_assert(Ok(()));
+        let application_id = apply_on_opening_fixture.call_and_assert(Ok(()));
 
 
         let terminate_application_fixture =
         let terminate_application_fixture =
             TerminateApplicationFixture::default_for_application_id(application_id)
             TerminateApplicationFixture::default_for_application_id(application_id)
@@ -544,12 +596,12 @@ fn terminate_worker_application_fails_with_hiring_error() {
     build_test_externalities().execute_with(|| {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
         HireLeadFixture::default().hire_lead();
 
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
+        let add_opening_fixture = AddWorkerOpeningFixture::default();
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
 
-        let appy_on_worker_opening_fixture =
+        let apply_on_opening_fixture =
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id);
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id);
-        let application_id = appy_on_worker_opening_fixture.call_and_assert(Ok(()));
+        let application_id = apply_on_opening_fixture.call_and_assert(Ok(()));
 
 
         let terminate_application_fixture =
         let terminate_application_fixture =
             TerminateApplicationFixture::default_for_application_id(application_id);
             TerminateApplicationFixture::default_for_application_id(application_id);
@@ -564,8 +616,8 @@ fn begin_review_worker_applications_succeeds() {
     build_test_externalities().execute_with(|| {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
         HireLeadFixture::default().hire_lead();
 
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
+        let add_opening_fixture = AddWorkerOpeningFixture::default();
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
 
         let begin_review_worker_applications_fixture =
         let begin_review_worker_applications_fixture =
             BeginReviewWorkerApplicationsFixture::default_for_opening_id(opening_id);
             BeginReviewWorkerApplicationsFixture::default_for_opening_id(opening_id);
@@ -580,10 +632,10 @@ fn begin_review_worker_applications_fails_with_invalid_origin_for_opening_type()
     build_test_externalities().execute_with(|| {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
         HireLeadFixture::default().hire_lead();
 
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default()
+        let add_opening_fixture = AddWorkerOpeningFixture::default()
             .with_origin(RawOrigin::Root)
             .with_origin(RawOrigin::Root)
             .with_opening_type(OpeningType::Leader);
             .with_opening_type(OpeningType::Leader);
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
 
         let begin_review_worker_applications_fixture =
         let begin_review_worker_applications_fixture =
             BeginReviewWorkerApplicationsFixture::default_for_opening_id(opening_id);
             BeginReviewWorkerApplicationsFixture::default_for_opening_id(opening_id);
@@ -596,8 +648,8 @@ fn begin_review_worker_applications_fails_with_not_a_lead() {
     build_test_externalities().execute_with(|| {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
         HireLeadFixture::default().hire_lead();
 
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
+        let add_opening_fixture = AddWorkerOpeningFixture::default();
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
 
         SetLeadFixture::set_lead_with_ids(2, 2, 2);
         SetLeadFixture::set_lead_with_ids(2, 2, 2);
 
 
@@ -625,8 +677,8 @@ fn begin_review_worker_applications_with_hiring_error() {
     build_test_externalities().execute_with(|| {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
         HireLeadFixture::default().hire_lead();
 
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
+        let add_opening_fixture = AddWorkerOpeningFixture::default();
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
 
         let begin_review_worker_applications_fixture =
         let begin_review_worker_applications_fixture =
             BeginReviewWorkerApplicationsFixture::default_for_opening_id(opening_id);
             BeginReviewWorkerApplicationsFixture::default_for_opening_id(opening_id);
@@ -642,8 +694,8 @@ fn begin_review_worker_applications_fails_with_invalid_origin() {
     build_test_externalities().execute_with(|| {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
         HireLeadFixture::default().hire_lead();
 
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
+        let add_opening_fixture = AddWorkerOpeningFixture::default();
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
 
         let begin_review_worker_applications_fixture =
         let begin_review_worker_applications_fixture =
             BeginReviewWorkerApplicationsFixture::default_for_opening_id(opening_id)
             BeginReviewWorkerApplicationsFixture::default_for_opening_id(opening_id)
@@ -653,12 +705,12 @@ fn begin_review_worker_applications_fails_with_invalid_origin() {
 }
 }
 
 
 #[test]
 #[test]
-fn fill_worker_opening_succeeds() {
+fn fill_opening_succeeds() {
     build_test_externalities().execute_with(|| {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
         HireLeadFixture::default().hire_lead();
         increase_total_balance_issuance_using_account_id(1, 10000);
         increase_total_balance_issuance_using_account_id(1, 10000);
 
 
-        let add_worker_opening_fixture =
+        let add_opening_fixture =
             AddWorkerOpeningFixture::default().with_policy_commitment(OpeningPolicyCommitment {
             AddWorkerOpeningFixture::default().with_policy_commitment(OpeningPolicyCommitment {
                 role_staking_policy: Some(hiring::StakingPolicy {
                 role_staking_policy: Some(hiring::StakingPolicy {
                     amount: 10,
                     amount: 10,
@@ -668,12 +720,12 @@ fn fill_worker_opening_succeeds() {
                 }),
                 }),
                 ..OpeningPolicyCommitment::default()
                 ..OpeningPolicyCommitment::default()
             });
             });
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
 
-        let appy_on_worker_opening_fixture =
+        let apply_on_opening_fixture =
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id)
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id)
                 .with_role_stake(Some(10));
                 .with_role_stake(Some(10));
-        let application_id = appy_on_worker_opening_fixture.call_and_assert(Ok(()));
+        let application_id = apply_on_opening_fixture.call_and_assert(Ok(()));
 
 
         let begin_review_worker_applications_fixture =
         let begin_review_worker_applications_fixture =
             BeginReviewWorkerApplicationsFixture::default_for_opening_id(opening_id);
             BeginReviewWorkerApplicationsFixture::default_for_opening_id(opening_id);
@@ -682,14 +734,14 @@ fn fill_worker_opening_succeeds() {
         let mint_id = create_mint();
         let mint_id = create_mint();
         set_mint_id(mint_id);
         set_mint_id(mint_id);
 
 
-        let fill_worker_opening_fixture =
+        let fill_opening_fixture =
             FillWorkerOpeningFixture::default_for_ids(opening_id, vec![application_id])
             FillWorkerOpeningFixture::default_for_ids(opening_id, vec![application_id])
                 .with_reward_policy(RewardPolicy {
                 .with_reward_policy(RewardPolicy {
                     amount_per_payout: 1000,
                     amount_per_payout: 1000,
                     next_payment_at_block: 20,
                     next_payment_at_block: 20,
                     payout_interval: None,
                     payout_interval: None,
                 });
                 });
-        let worker_id = fill_worker_opening_fixture.call_and_assert(Ok(()));
+        let worker_id = fill_opening_fixture.call_and_assert(Ok(()));
 
 
         let mut worker_application_dictionary = BTreeMap::new();
         let mut worker_application_dictionary = BTreeMap::new();
         worker_application_dictionary.insert(application_id, worker_id);
         worker_application_dictionary.insert(application_id, worker_id);
@@ -702,12 +754,12 @@ fn fill_worker_opening_succeeds() {
 }
 }
 
 
 #[test]
 #[test]
-fn fill_worker_opening_fails_with_invalid_origin_for_opening_type() {
+fn fill_opening_fails_with_invalid_origin_for_opening_type() {
     build_test_externalities().execute_with(|| {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
         HireLeadFixture::default().hire_lead();
         increase_total_balance_issuance_using_account_id(1, 10000);
         increase_total_balance_issuance_using_account_id(1, 10000);
 
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default()
+        let add_opening_fixture = AddWorkerOpeningFixture::default()
             .with_policy_commitment(OpeningPolicyCommitment {
             .with_policy_commitment(OpeningPolicyCommitment {
                 role_staking_policy: Some(hiring::StakingPolicy {
                 role_staking_policy: Some(hiring::StakingPolicy {
                     amount: 10,
                     amount: 10,
@@ -719,12 +771,12 @@ fn fill_worker_opening_fails_with_invalid_origin_for_opening_type() {
             })
             })
             .with_opening_type(OpeningType::Leader)
             .with_opening_type(OpeningType::Leader)
             .with_origin(RawOrigin::Root);
             .with_origin(RawOrigin::Root);
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
 
-        let appy_on_worker_opening_fixture =
+        let apply_on_opening_fixture =
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id)
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id)
                 .with_role_stake(Some(10));
                 .with_role_stake(Some(10));
-        let application_id = appy_on_worker_opening_fixture.call_and_assert(Ok(()));
+        let application_id = apply_on_opening_fixture.call_and_assert(Ok(()));
 
 
         let begin_review_worker_applications_fixture =
         let begin_review_worker_applications_fixture =
             BeginReviewWorkerApplicationsFixture::default_for_opening_id(opening_id)
             BeginReviewWorkerApplicationsFixture::default_for_opening_id(opening_id)
@@ -733,119 +785,118 @@ fn fill_worker_opening_fails_with_invalid_origin_for_opening_type() {
 
 
         set_mint_id(create_mint());
         set_mint_id(create_mint());
 
 
-        let fill_worker_opening_fixture =
+        let fill_opening_fixture =
             FillWorkerOpeningFixture::default_for_ids(opening_id, vec![application_id])
             FillWorkerOpeningFixture::default_for_ids(opening_id, vec![application_id])
                 .with_reward_policy(RewardPolicy {
                 .with_reward_policy(RewardPolicy {
                     amount_per_payout: 1000,
                     amount_per_payout: 1000,
                     next_payment_at_block: 20,
                     next_payment_at_block: 20,
                     payout_interval: None,
                     payout_interval: None,
                 });
                 });
-        fill_worker_opening_fixture.call_and_assert(Err(Error::RequireRootOrigin));
+        fill_opening_fixture.call_and_assert(Err(Error::RequireRootOrigin));
     });
     });
 }
 }
 
 
 #[test]
 #[test]
-fn fill_worker_opening_fails_with_invalid_origin() {
+fn fill_opening_fails_with_invalid_origin() {
     build_test_externalities().execute_with(|| {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
         HireLeadFixture::default().hire_lead();
 
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
+        let add_opening_fixture = AddWorkerOpeningFixture::default();
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
 
-        let fill_worker_opening_fixture =
+        let fill_opening_fixture =
             FillWorkerOpeningFixture::default_for_ids(opening_id, Vec::new())
             FillWorkerOpeningFixture::default_for_ids(opening_id, Vec::new())
                 .with_origin(RawOrigin::None);
                 .with_origin(RawOrigin::None);
-        fill_worker_opening_fixture.call_and_assert(Err(Error::RequireSignedOrigin));
+        fill_opening_fixture.call_and_assert(Err(Error::RequireSignedOrigin));
     });
     });
 }
 }
 
 
 #[test]
 #[test]
-fn fill_worker_opening_fails_with_not_a_lead() {
+fn fill_opening_fails_with_not_a_lead() {
     build_test_externalities().execute_with(|| {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
         HireLeadFixture::default().hire_lead();
 
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
+        let add_opening_fixture = AddWorkerOpeningFixture::default();
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
 
         SetLeadFixture::set_lead_with_ids(2, 2, 2);
         SetLeadFixture::set_lead_with_ids(2, 2, 2);
 
 
-        let fill_worker_opening_fixture =
+        let fill_opening_fixture =
             FillWorkerOpeningFixture::default_for_ids(opening_id, Vec::new());
             FillWorkerOpeningFixture::default_for_ids(opening_id, Vec::new());
-        fill_worker_opening_fixture.call_and_assert(Err(Error::IsNotLeadAccount));
+        fill_opening_fixture.call_and_assert(Err(Error::IsNotLeadAccount));
     });
     });
 }
 }
 
 
 #[test]
 #[test]
-fn fill_worker_opening_fails_with_invalid_opening() {
+fn fill_opening_fails_with_invalid_opening() {
     build_test_externalities().execute_with(|| {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
         HireLeadFixture::default().hire_lead();
 
 
         let invalid_opening_id = 6;
         let invalid_opening_id = 6;
 
 
-        let fill_worker_opening_fixture =
+        let fill_opening_fixture =
             FillWorkerOpeningFixture::default_for_ids(invalid_opening_id, Vec::new());
             FillWorkerOpeningFixture::default_for_ids(invalid_opening_id, Vec::new());
-        fill_worker_opening_fixture.call_and_assert(Err(Error::OpeningDoesNotExist));
+        fill_opening_fixture.call_and_assert(Err(Error::OpeningDoesNotExist));
     });
     });
 }
 }
 
 
 #[test]
 #[test]
-fn fill_worker_opening_fails_with_invalid_application_list() {
+fn fill_opening_fails_with_invalid_application_list() {
     build_test_externalities().execute_with(|| {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
         HireLeadFixture::default().hire_lead();
 
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
+        let add_opening_fixture = AddWorkerOpeningFixture::default();
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
 
-        let appy_on_worker_opening_fixture =
+        let apply_on_opening_fixture =
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id);
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id);
-        let application_id = appy_on_worker_opening_fixture.call_and_assert(Ok(()));
+        let application_id = apply_on_opening_fixture.call_and_assert(Ok(()));
 
 
         let begin_review_worker_applications_fixture =
         let begin_review_worker_applications_fixture =
             BeginReviewWorkerApplicationsFixture::default_for_opening_id(opening_id);
             BeginReviewWorkerApplicationsFixture::default_for_opening_id(opening_id);
         begin_review_worker_applications_fixture.call_and_assert(Ok(()));
         begin_review_worker_applications_fixture.call_and_assert(Ok(()));
 
 
         let invalid_application_id = 66;
         let invalid_application_id = 66;
-        let fill_worker_opening_fixture = FillWorkerOpeningFixture::default_for_ids(
+        let fill_opening_fixture = FillWorkerOpeningFixture::default_for_ids(
             opening_id,
             opening_id,
             vec![application_id, invalid_application_id],
             vec![application_id, invalid_application_id],
         );
         );
-        fill_worker_opening_fixture
+        fill_opening_fixture.call_and_assert(Err(Error::SuccessfulWorkerApplicationDoesNotExist));
-            .call_and_assert(Err(Error::SuccessfulWorkerApplicationDoesNotExist));
     });
     });
 }
 }
 
 
 #[test]
 #[test]
-fn fill_worker_opening_fails_with_invalid_application_with_hiring_error() {
+fn fill_opening_fails_with_invalid_application_with_hiring_error() {
     build_test_externalities().execute_with(|| {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
         HireLeadFixture::default().hire_lead();
 
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
+        let add_opening_fixture = AddWorkerOpeningFixture::default();
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
 
-        let fill_worker_opening_fixture =
+        let fill_opening_fixture =
             FillWorkerOpeningFixture::default_for_ids(opening_id, Vec::new());
             FillWorkerOpeningFixture::default_for_ids(opening_id, Vec::new());
-        fill_worker_opening_fixture
+        fill_opening_fixture
             .call_and_assert(Err(Error::FullWorkerOpeningOpeningNotInReviewPeriodStage));
             .call_and_assert(Err(Error::FullWorkerOpeningOpeningNotInReviewPeriodStage));
     });
     });
 }
 }
 
 
 #[test]
 #[test]
-fn fill_worker_opening_fails_with_invalid_reward_policy() {
+fn fill_opening_fails_with_invalid_reward_policy() {
     build_test_externalities().execute_with(|| {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
         HireLeadFixture::default().hire_lead();
 
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
+        let add_opening_fixture = AddWorkerOpeningFixture::default();
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
 
-        let appy_on_worker_opening_fixture =
+        let apply_on_opening_fixture =
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id);
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id);
-        let application_id = appy_on_worker_opening_fixture.call_and_assert(Ok(()));
+        let application_id = apply_on_opening_fixture.call_and_assert(Ok(()));
 
 
         let begin_review_worker_applications_fixture =
         let begin_review_worker_applications_fixture =
             BeginReviewWorkerApplicationsFixture::default_for_opening_id(opening_id);
             BeginReviewWorkerApplicationsFixture::default_for_opening_id(opening_id);
         begin_review_worker_applications_fixture.call_and_assert(Ok(()));
         begin_review_worker_applications_fixture.call_and_assert(Ok(()));
 
 
-        let fill_worker_opening_fixture =
+        let fill_opening_fixture =
             FillWorkerOpeningFixture::default_for_ids(opening_id, vec![application_id])
             FillWorkerOpeningFixture::default_for_ids(opening_id, vec![application_id])
                 .with_reward_policy(RewardPolicy {
                 .with_reward_policy(RewardPolicy {
                     amount_per_payout: 10000,
                     amount_per_payout: 10000,
@@ -853,7 +904,7 @@ fn fill_worker_opening_fails_with_invalid_reward_policy() {
                     next_payment_at_block: 0,
                     next_payment_at_block: 0,
                     payout_interval: None,
                     payout_interval: None,
                 });
                 });
-        fill_worker_opening_fixture
+        fill_opening_fixture
     });
     });
 }
 }
 
 

+ 69 - 71
runtime/src/integration/proposals/proposal_encoder.rs

@@ -9,132 +9,119 @@ use rstd::marker::PhantomData;
 use rstd::vec::Vec;
 use rstd::vec::Vec;
 use srml_support::print;
 use srml_support::print;
 
 
+// The macro binds working group outer-level Call with the provided inner-level working group
+// extrinsic call. Outer-call is defined by the provided WorkingGroup param expression.
+
+//Params:
+// - $working_group: expression returning the 'common::working_group::WorkingGroup' enum
+// - $working_group_instance_call: expression returning the exact working group instance extrinsic call
+macro_rules! wrap_working_group_call {
+    ($working_group:expr, $working_group_instance_call:expr) => {{
+        match $working_group {
+            WorkingGroup::Storage => Call::StorageWorkingGroup($working_group_instance_call),
+        }
+    }};
+}
+
 /// _ProposalEncoder_ implementation. It encodes extrinsics with proposal details parameters
 /// _ProposalEncoder_ implementation. It encodes extrinsics with proposal details parameters
 /// using Runtime Call and parity codec.
 /// using Runtime Call and parity codec.
 pub struct ExtrinsicProposalEncoder;
 pub struct ExtrinsicProposalEncoder;
 impl ProposalEncoder<Runtime> for ExtrinsicProposalEncoder {
 impl ProposalEncoder<Runtime> for ExtrinsicProposalEncoder {
     fn encode_proposal(proposal_details: ProposalDetailsOf<Runtime>) -> Vec<u8> {
     fn encode_proposal(proposal_details: ProposalDetailsOf<Runtime>) -> Vec<u8> {
-        match proposal_details {
+        let call = match proposal_details {
             ProposalDetails::Text(text) => {
             ProposalDetails::Text(text) => {
-                Call::ProposalsCodex(proposals_codex::Call::execute_text_proposal(text)).encode()
+                Call::ProposalsCodex(proposals_codex::Call::execute_text_proposal(text))
             }
             }
             ProposalDetails::SetElectionParameters(election_parameters) => Call::CouncilElection(
             ProposalDetails::SetElectionParameters(election_parameters) => Call::CouncilElection(
                 governance::election::Call::set_election_parameters(election_parameters),
                 governance::election::Call::set_election_parameters(election_parameters),
-            )
+            ),
-            .encode(),
             ProposalDetails::SetContentWorkingGroupMintCapacity(mint_balance) => {
             ProposalDetails::SetContentWorkingGroupMintCapacity(mint_balance) => {
                 Call::ContentWorkingGroup(content_working_group::Call::set_mint_capacity(
                 Call::ContentWorkingGroup(content_working_group::Call::set_mint_capacity(
                     mint_balance,
                     mint_balance,
                 ))
                 ))
-                .encode()
             }
             }
             ProposalDetails::Spending(balance, destination) => Call::Council(
             ProposalDetails::Spending(balance, destination) => Call::Council(
                 governance::council::Call::spend_from_council_mint(balance, destination),
                 governance::council::Call::spend_from_council_mint(balance, destination),
-            )
+            ),
-            .encode(),
             ProposalDetails::SetLead(new_lead) => {
             ProposalDetails::SetLead(new_lead) => {
                 Call::ContentWorkingGroup(content_working_group::Call::replace_lead(new_lead))
                 Call::ContentWorkingGroup(content_working_group::Call::replace_lead(new_lead))
-                    .encode()
             }
             }
             ProposalDetails::SetValidatorCount(new_validator_count) => {
             ProposalDetails::SetValidatorCount(new_validator_count) => {
-                Call::Staking(staking::Call::set_validator_count(new_validator_count)).encode()
+                Call::Staking(staking::Call::set_validator_count(new_validator_count))
             }
             }
             ProposalDetails::RuntimeUpgrade(wasm_code) => Call::ProposalsCodex(
             ProposalDetails::RuntimeUpgrade(wasm_code) => Call::ProposalsCodex(
                 proposals_codex::Call::execute_runtime_upgrade_proposal(wasm_code),
                 proposals_codex::Call::execute_runtime_upgrade_proposal(wasm_code),
-            )
+            ),
-            .encode(),
             // ********** Deprecated during the Nicaea release.
             // ********** Deprecated during the Nicaea release.
             // It is kept only for backward compatibility in the Pioneer. **********
             // It is kept only for backward compatibility in the Pioneer. **********
             ProposalDetails::EvictStorageProvider(_) => {
             ProposalDetails::EvictStorageProvider(_) => {
                 print("Error: Calling deprecated EvictStorageProvider encoding option.");
                 print("Error: Calling deprecated EvictStorageProvider encoding option.");
-                Vec::new()
+                return Vec::new();
             }
             }
             // ********** Deprecated during the Nicaea release.
             // ********** Deprecated during the Nicaea release.
             // It is kept only for backward compatibility in the Pioneer. **********
             // It is kept only for backward compatibility in the Pioneer. **********
             ProposalDetails::SetStorageRoleParameters(_) => {
             ProposalDetails::SetStorageRoleParameters(_) => {
                 print("Error: Calling deprecated SetStorageRoleParameters encoding option.");
                 print("Error: Calling deprecated SetStorageRoleParameters encoding option.");
-                Vec::new()
+                return Vec::new();
             }
             }
             ProposalDetails::AddWorkingGroupLeaderOpening(add_opening_params) => {
             ProposalDetails::AddWorkingGroupLeaderOpening(add_opening_params) => {
-                let call = match add_opening_params.working_group {
+                wrap_working_group_call!(
-                    WorkingGroup::Storage => {
+                    add_opening_params.working_group,
-                        Call::StorageWorkingGroup(Wg::create_add_opening_call(add_opening_params))
+                    Wg::create_add_opening_call(add_opening_params)
-                    }
+                )
-                };
-
-                call.encode()
             }
             }
             ProposalDetails::BeginReviewWorkingGroupLeaderApplications(
             ProposalDetails::BeginReviewWorkingGroupLeaderApplications(
                 opening_id,
                 opening_id,
                 working_group,
                 working_group,
-            ) => {
+            ) => wrap_working_group_call!(
-                let call = match working_group {
+                working_group,
-                    WorkingGroup::Storage => Call::StorageWorkingGroup(
+                Wg::create_begin_review_applications_call(opening_id)
-                        Wg::create_begin_review_applications_call(opening_id),
+            ),
-                    ),
-                };
-
-                call.encode()
-            }
             ProposalDetails::FillWorkingGroupLeaderOpening(fill_opening_params) => {
             ProposalDetails::FillWorkingGroupLeaderOpening(fill_opening_params) => {
-                let call = match fill_opening_params.working_group {
+                wrap_working_group_call!(
-                    WorkingGroup::Storage => {
+                    fill_opening_params.working_group,
-                        Call::StorageWorkingGroup(Wg::create_fill_opening_call(fill_opening_params))
+                    Wg::create_fill_opening_call(fill_opening_params)
-                    }
+                )
-                };
-
-                call.encode()
             }
             }
             ProposalDetails::SetWorkingGroupMintCapacity(mint_balance, working_group) => {
             ProposalDetails::SetWorkingGroupMintCapacity(mint_balance, working_group) => {
-                let call = match working_group {
+                wrap_working_group_call!(
-                    WorkingGroup::Storage => {
+                    working_group,
-                        Call::StorageWorkingGroup(Wg::create_set_mint_capacity_call(mint_balance))
+                    Wg::create_set_mint_capacity_call(mint_balance)
-                    }
+                )
-                };
-
-                call.encode()
             }
             }
             ProposalDetails::DecreaseWorkingGroupLeaderStake(
             ProposalDetails::DecreaseWorkingGroupLeaderStake(
                 worker_id,
                 worker_id,
                 decreasing_stake,
                 decreasing_stake,
                 working_group,
                 working_group,
-            ) => {
+            ) => wrap_working_group_call!(
-                let call = match working_group {
+                working_group,
-                    WorkingGroup::Storage => Call::StorageWorkingGroup(
+                Wg::create_decrease_stake_call(worker_id, decreasing_stake)
-                        Wg::create_decrease_stake_call(worker_id, decreasing_stake),
+            ),
-                    ),
-                };
-
-                call.encode()
-            }
             ProposalDetails::SlashWorkingGroupLeaderStake(
             ProposalDetails::SlashWorkingGroupLeaderStake(
                 worker_id,
                 worker_id,
                 slashing_stake,
                 slashing_stake,
                 working_group,
                 working_group,
-            ) => {
+            ) => wrap_working_group_call!(
-                let call = match working_group {
+                working_group,
-                    WorkingGroup::Storage => Call::StorageWorkingGroup(
+                Wg::create_slash_stake_call(worker_id, slashing_stake,)
-                        Wg::create_slash_stake_call(worker_id, slashing_stake),
+            ),
-                    ),
-                };
-
-                call.encode()
-            }
             ProposalDetails::SetWorkingGroupLeaderReward(
             ProposalDetails::SetWorkingGroupLeaderReward(
                 worker_id,
                 worker_id,
                 reward_amount,
                 reward_amount,
                 working_group,
                 working_group,
-            ) => {
+            ) => wrap_working_group_call!(
-                let call = match working_group {
+                working_group,
-                    WorkingGroup::Storage => Call::StorageWorkingGroup(Wg::create_set_reward_call(
+                Wg::create_set_reward_call(worker_id, reward_amount)
-                        worker_id,
+            ),
-                        reward_amount,
+            ProposalDetails::TerminateWorkingGroupLeaderRole(terminate_role_params) => {
-                    )),
+                wrap_working_group_call!(
-                };
+                    terminate_role_params.working_group,
-
+                    Wg::terminate_role_call(terminate_role_params)
-                call.encode()
+                )
             }
             }
-        }
+        };
+
+        call.encode()
     }
     }
 }
 }
 
 
@@ -220,4 +207,15 @@ where
     ) -> working_group::Call<T, I> {
     ) -> working_group::Call<T, I> {
         working_group::Call::<T, I>::update_reward_amount(worker_id, reward_amount)
         working_group::Call::<T, I>::update_reward_amount(worker_id, reward_amount)
     }
     }
+
+    // Generic call constructor for the working group 'terminate role'.
+    fn terminate_role_call(
+        terminate_role_params: proposals_codex::TerminateRoleParameters<working_group::WorkerId<T>>,
+    ) -> working_group::Call<T, I> {
+        working_group::Call::<T, I>::terminate_role(
+            terminate_role_params.worker_id,
+            terminate_role_params.rationale,
+            terminate_role_params.slash,
+        )
+    }
 }
 }

+ 6 - 0
runtime/src/tests/proposals_integration/mod.rs

@@ -474,6 +474,12 @@ where
             ..self
             ..self
         }
         }
     }
     }
+    fn with_setup_enviroment(self, setup_environment: bool) -> Self {
+        Self {
+            setup_environment,
+            ..self
+        }
+    }
 
 
     fn with_member_id(self, member_id: u64) -> Self {
     fn with_member_id(self, member_id: u64) -> Self {
         Self { member_id, ..self }
         Self { member_id, ..self }

+ 309 - 30
runtime/src/tests/proposals_integration/working_group_proposals.rs

@@ -20,7 +20,11 @@ fn add_opening(
     account_id: [u8; 32],
     account_id: [u8; 32],
     activate_at: hiring::ActivateOpeningAt<BlockNumber>,
     activate_at: hiring::ActivateOpeningAt<BlockNumber>,
     opening_policy_commitment: Option<OpeningPolicyCommitment<BlockNumber, u128>>,
     opening_policy_commitment: Option<OpeningPolicyCommitment<BlockNumber, u128>>,
+    sequence_number: u32, // action sequence number to align with other actions
 ) -> u64 {
 ) -> u64 {
+    let expected_proposal_id = sequence_number;
+    let run_to_block = sequence_number + 1;
+
     let opening_id = StorageWorkingGroup::next_opening_id();
     let opening_id = StorageWorkingGroup::next_opening_id();
 
 
     assert!(!<working_group::OpeningById<
     assert!(!<working_group::OpeningById<
@@ -44,14 +48,24 @@ fn add_opening(
                 working_group: WorkingGroup::Storage,
                 working_group: WorkingGroup::Storage,
             },
             },
         )
         )
-    });
+    })
+    .with_expected_proposal_id(expected_proposal_id)
+    .with_run_to_block(run_to_block);
 
 
     codex_extrinsic_test_fixture.call_extrinsic_and_assert();
     codex_extrinsic_test_fixture.call_extrinsic_and_assert();
 
 
     opening_id
     opening_id
 }
 }
 
 
-fn begin_review_applications(member_id: u8, account_id: [u8; 32], opening_id: u64) {
+fn begin_review_applications(
+    member_id: u8,
+    account_id: [u8; 32],
+    opening_id: u64,
+    sequence_number: u32, // action sequence number to align with other actions
+) {
+    let expected_proposal_id = sequence_number;
+    let run_to_block = sequence_number + 1;
+
     let codex_extrinsic_test_fixture = CodexProposalTestFixture::default_for_call(|| {
     let codex_extrinsic_test_fixture = CodexProposalTestFixture::default_for_call(|| {
         ProposalCodex::create_begin_review_working_group_leader_applications_proposal(
         ProposalCodex::create_begin_review_working_group_leader_applications_proposal(
             RawOrigin::Signed(account_id.clone().into()).into(),
             RawOrigin::Signed(account_id.clone().into()).into(),
@@ -64,8 +78,8 @@ fn begin_review_applications(member_id: u8, account_id: [u8; 32], opening_id: u6
         )
         )
     })
     })
     .disable_setup_enviroment()
     .disable_setup_enviroment()
-    .with_expected_proposal_id(2)
+    .with_expected_proposal_id(expected_proposal_id)
-    .with_run_to_block(3);
+    .with_run_to_block(run_to_block);
 
 
     codex_extrinsic_test_fixture.call_extrinsic_and_assert();
     codex_extrinsic_test_fixture.call_extrinsic_and_assert();
 }
 }
@@ -76,15 +90,10 @@ fn fill_opening(
     opening_id: u64,
     opening_id: u64,
     successful_application_id: u64,
     successful_application_id: u64,
     reward_policy: Option<RewardPolicy<Balance, BlockNumber>>,
     reward_policy: Option<RewardPolicy<Balance, BlockNumber>>,
+    sequence_number: u32, // action sequence number to align with other actions
 ) {
 ) {
-    let mut expected_proposal_id = 3;
+    let expected_proposal_id = sequence_number;
-    let mut run_to_block = 4;
+    let run_to_block = sequence_number + 1;
-    if reward_policy.is_some() {
-        set_mint_capacity(member_id, account_id, 999999);
-
-        expected_proposal_id = 4;
-        run_to_block = 5;
-    }
 
 
     let codex_extrinsic_test_fixture = CodexProposalTestFixture::default_for_call(|| {
     let codex_extrinsic_test_fixture = CodexProposalTestFixture::default_for_call(|| {
         ProposalCodex::create_fill_working_group_leader_opening_proposal(
         ProposalCodex::create_fill_working_group_leader_opening_proposal(
@@ -121,7 +130,11 @@ fn decrease_stake(
     account_id: [u8; 32],
     account_id: [u8; 32],
     leader_worker_id: u64,
     leader_worker_id: u64,
     stake_amount: Balance,
     stake_amount: Balance,
+    sequence_number: u32, // action sequence number to align with other actions
 ) {
 ) {
+    let expected_proposal_id = sequence_number;
+    let run_to_block = sequence_number + 1;
+
     let codex_extrinsic_test_fixture = CodexProposalTestFixture::default_for_call(|| {
     let codex_extrinsic_test_fixture = CodexProposalTestFixture::default_for_call(|| {
         ProposalCodex::create_decrease_working_group_leader_stake_proposal(
         ProposalCodex::create_decrease_working_group_leader_stake_proposal(
             RawOrigin::Signed(account_id.clone().into()).into(),
             RawOrigin::Signed(account_id.clone().into()).into(),
@@ -135,13 +148,22 @@ fn decrease_stake(
         )
         )
     })
     })
     .disable_setup_enviroment()
     .disable_setup_enviroment()
-    .with_expected_proposal_id(4)
+    .with_expected_proposal_id(expected_proposal_id)
-    .with_run_to_block(5);
+    .with_run_to_block(run_to_block);
 
 
     codex_extrinsic_test_fixture.call_extrinsic_and_assert();
     codex_extrinsic_test_fixture.call_extrinsic_and_assert();
 }
 }
 
 
-fn slash_stake(member_id: u8, account_id: [u8; 32], leader_worker_id: u64, stake_amount: Balance) {
+fn slash_stake(
+    member_id: u8,
+    account_id: [u8; 32],
+    leader_worker_id: u64,
+    stake_amount: Balance,
+    sequence_number: u32, // action sequence number to align with other actions
+) {
+    let expected_proposal_id = sequence_number;
+    let run_to_block = sequence_number + 1;
+
     let codex_extrinsic_test_fixture = CodexProposalTestFixture::default_for_call(|| {
     let codex_extrinsic_test_fixture = CodexProposalTestFixture::default_for_call(|| {
         ProposalCodex::create_slash_working_group_leader_stake_proposal(
         ProposalCodex::create_slash_working_group_leader_stake_proposal(
             RawOrigin::Signed(account_id.clone().into()).into(),
             RawOrigin::Signed(account_id.clone().into()).into(),
@@ -155,13 +177,22 @@ fn slash_stake(member_id: u8, account_id: [u8; 32], leader_worker_id: u64, stake
         )
         )
     })
     })
     .disable_setup_enviroment()
     .disable_setup_enviroment()
-    .with_expected_proposal_id(4)
+    .with_expected_proposal_id(expected_proposal_id)
-    .with_run_to_block(5);
+    .with_run_to_block(run_to_block);
 
 
     codex_extrinsic_test_fixture.call_extrinsic_and_assert();
     codex_extrinsic_test_fixture.call_extrinsic_and_assert();
 }
 }
 
 
-fn set_reward(member_id: u8, account_id: [u8; 32], leader_worker_id: u64, reward_amount: Balance) {
+fn set_reward(
+    member_id: u8,
+    account_id: [u8; 32],
+    leader_worker_id: u64,
+    reward_amount: Balance,
+    sequence_number: u32, // action sequence number to align with other actions
+) {
+    let expected_proposal_id = sequence_number;
+    let run_to_block = sequence_number + 1;
+
     let codex_extrinsic_test_fixture = CodexProposalTestFixture::default_for_call(|| {
     let codex_extrinsic_test_fixture = CodexProposalTestFixture::default_for_call(|| {
         ProposalCodex::create_set_working_group_leader_reward_proposal(
         ProposalCodex::create_set_working_group_leader_reward_proposal(
             RawOrigin::Signed(account_id.clone().into()).into(),
             RawOrigin::Signed(account_id.clone().into()).into(),
@@ -175,13 +206,22 @@ fn set_reward(member_id: u8, account_id: [u8; 32], leader_worker_id: u64, reward
         )
         )
     })
     })
     .disable_setup_enviroment()
     .disable_setup_enviroment()
-    .with_expected_proposal_id(5)
+    .with_expected_proposal_id(expected_proposal_id)
-    .with_run_to_block(6);
+    .with_run_to_block(run_to_block);
 
 
     codex_extrinsic_test_fixture.call_extrinsic_and_assert();
     codex_extrinsic_test_fixture.call_extrinsic_and_assert();
 }
 }
 
 
-fn set_mint_capacity(member_id: u8, account_id: [u8; 32], mint_capacity: Balance) {
+fn set_mint_capacity(
+    member_id: u8,
+    account_id: [u8; 32],
+    mint_capacity: Balance,
+    sequence_number: u32, // action sequence number to align with other actions
+    setup_environment: bool,
+) {
+    let expected_proposal_id = sequence_number;
+    let run_to_block = sequence_number + 1;
+
     let mint_id_result = <minting::Module<Runtime>>::add_mint(0, None);
     let mint_id_result = <minting::Module<Runtime>>::add_mint(0, None);
 
 
     if let Ok(mint_id) = mint_id_result {
     if let Ok(mint_id) = mint_id_result {
@@ -199,9 +239,41 @@ fn set_mint_capacity(member_id: u8, account_id: [u8; 32], mint_capacity: Balance
             WorkingGroup::Storage,
             WorkingGroup::Storage,
         )
         )
     })
     })
+    .with_setup_enviroment(setup_environment)
+    .with_expected_proposal_id(expected_proposal_id)
+    .with_run_to_block(run_to_block);
+
+    codex_extrinsic_test_fixture.call_extrinsic_and_assert();
+}
+
+fn terminate_role(
+    member_id: u8,
+    account_id: [u8; 32],
+    leader_worker_id: u64,
+    slash: bool,
+    sequence_number: u32, // action sequence number to align with other actions
+) {
+    let expected_proposal_id = sequence_number;
+    let run_to_block = sequence_number + 1;
+
+    let codex_extrinsic_test_fixture = CodexProposalTestFixture::default_for_call(|| {
+        ProposalCodex::create_terminate_working_group_leader_role_proposal(
+            RawOrigin::Signed(account_id.clone().into()).into(),
+            member_id as u64,
+            b"title".to_vec(),
+            b"body".to_vec(),
+            Some(<BalanceOf<Runtime>>::from(100_000_u32)),
+            proposals_codex::TerminateRoleParameters {
+                worker_id: leader_worker_id,
+                rationale: Vec::new(),
+                slash,
+                working_group: WorkingGroup::Storage,
+            },
+        )
+    })
     .disable_setup_enviroment()
     .disable_setup_enviroment()
-    .with_expected_proposal_id(3)
+    .with_expected_proposal_id(expected_proposal_id)
-    .with_run_to_block(4);
+    .with_run_to_block(run_to_block);
 
 
     codex_extrinsic_test_fixture.call_extrinsic_and_assert();
     codex_extrinsic_test_fixture.call_extrinsic_and_assert();
 }
 }
@@ -219,7 +291,13 @@ fn create_add_working_group_leader_opening_proposal_execution_succeeds() {
             StorageWorkingGroupInstance,
             StorageWorkingGroupInstance,
         >>::exists(next_opening_id));
         >>::exists(next_opening_id));
 
 
-        let opening_id = add_opening(member_id, account_id, ActivateOpeningAt::CurrentBlock, None);
+        let opening_id = add_opening(
+            member_id,
+            account_id,
+            ActivateOpeningAt::CurrentBlock,
+            None,
+            1,
+        );
 
 
         // Check for expected opening id.
         // Check for expected opening id.
         assert_eq!(opening_id, next_opening_id);
         assert_eq!(opening_id, next_opening_id);
@@ -243,6 +321,7 @@ fn create_begin_review_working_group_leader_applications_proposal_execution_succ
             account_id.clone(),
             account_id.clone(),
             ActivateOpeningAt::CurrentBlock,
             ActivateOpeningAt::CurrentBlock,
             None,
             None,
+            1,
         );
         );
 
 
         let opening = StorageWorkingGroup::opening_by_id(opening_id);
         let opening = StorageWorkingGroup::opening_by_id(opening_id);
@@ -261,7 +340,7 @@ fn create_begin_review_working_group_leader_applications_proposal_execution_succ
             }
             }
         );
         );
 
 
-        begin_review_applications(member_id, account_id, opening_id);
+        begin_review_applications(member_id, account_id, opening_id, 2);
 
 
         let hiring_opening = Hiring::opening_by_id(opening.hiring_opening_id);
         let hiring_opening = Hiring::opening_by_id(opening.hiring_opening_id);
         assert_eq!(
         assert_eq!(
@@ -291,6 +370,7 @@ fn create_fill_working_group_leader_opening_proposal_execution_succeeds() {
             account_id.clone(),
             account_id.clone(),
             ActivateOpeningAt::CurrentBlock,
             ActivateOpeningAt::CurrentBlock,
             None,
             None,
+            1,
         );
         );
 
 
         let apply_result = StorageWorkingGroup::apply_on_opening(
         let apply_result = StorageWorkingGroup::apply_on_opening(
@@ -307,7 +387,7 @@ fn create_fill_working_group_leader_opening_proposal_execution_succeeds() {
 
 
         let expected_application_id = 0;
         let expected_application_id = 0;
 
 
-        begin_review_applications(member_id, account_id, opening_id);
+        begin_review_applications(member_id, account_id, opening_id, 2);
 
 
         let lead = StorageWorkingGroup::current_lead();
         let lead = StorageWorkingGroup::current_lead();
         assert!(lead.is_none());
         assert!(lead.is_none());
@@ -318,6 +398,7 @@ fn create_fill_working_group_leader_opening_proposal_execution_succeeds() {
             opening_id,
             opening_id,
             expected_application_id,
             expected_application_id,
             None,
             None,
+            3,
         );
         );
 
 
         let lead = StorageWorkingGroup::current_lead();
         let lead = StorageWorkingGroup::current_lead();
@@ -347,6 +428,7 @@ fn create_decrease_group_leader_stake_proposal_execution_succeeds() {
             account_id.clone(),
             account_id.clone(),
             ActivateOpeningAt::CurrentBlock,
             ActivateOpeningAt::CurrentBlock,
             Some(opening_policy_commitment),
             Some(opening_policy_commitment),
+            1,
         );
         );
 
 
         let apply_result = StorageWorkingGroup::apply_on_opening(
         let apply_result = StorageWorkingGroup::apply_on_opening(
@@ -363,7 +445,7 @@ fn create_decrease_group_leader_stake_proposal_execution_succeeds() {
 
 
         let expected_application_id = 0;
         let expected_application_id = 0;
 
 
-        begin_review_applications(member_id, account_id, opening_id);
+        begin_review_applications(member_id, account_id, opening_id, 2);
 
 
         let lead = StorageWorkingGroup::current_lead();
         let lead = StorageWorkingGroup::current_lead();
         assert!(lead.is_none());
         assert!(lead.is_none());
@@ -374,6 +456,7 @@ fn create_decrease_group_leader_stake_proposal_execution_succeeds() {
             opening_id,
             opening_id,
             expected_application_id,
             expected_application_id,
             None,
             None,
+            3,
         );
         );
 
 
         let leader_worker_id = StorageWorkingGroup::current_lead().unwrap();
         let leader_worker_id = StorageWorkingGroup::current_lead().unwrap();
@@ -390,6 +473,7 @@ fn create_decrease_group_leader_stake_proposal_execution_succeeds() {
             account_id,
             account_id,
             leader_worker_id,
             leader_worker_id,
             decreasing_stake_amount,
             decreasing_stake_amount,
+            4,
         );
         );
 
 
         let new_balance = Balances::free_balance::<&AccountId32>(&account_id.into());
         let new_balance = Balances::free_balance::<&AccountId32>(&account_id.into());
@@ -425,6 +509,7 @@ fn create_slash_group_leader_stake_proposal_execution_succeeds() {
             account_id.clone(),
             account_id.clone(),
             ActivateOpeningAt::CurrentBlock,
             ActivateOpeningAt::CurrentBlock,
             Some(opening_policy_commitment),
             Some(opening_policy_commitment),
+            1,
         );
         );
 
 
         let apply_result = StorageWorkingGroup::apply_on_opening(
         let apply_result = StorageWorkingGroup::apply_on_opening(
@@ -441,7 +526,7 @@ fn create_slash_group_leader_stake_proposal_execution_succeeds() {
 
 
         let expected_application_id = 0;
         let expected_application_id = 0;
 
 
-        begin_review_applications(member_id, account_id, opening_id);
+        begin_review_applications(member_id, account_id, opening_id, 2);
 
 
         let lead = StorageWorkingGroup::current_lead();
         let lead = StorageWorkingGroup::current_lead();
         assert!(lead.is_none());
         assert!(lead.is_none());
@@ -452,6 +537,7 @@ fn create_slash_group_leader_stake_proposal_execution_succeeds() {
             opening_id,
             opening_id,
             expected_application_id,
             expected_application_id,
             None,
             None,
+            3,
         );
         );
 
 
         let leader_worker_id = StorageWorkingGroup::current_lead().unwrap();
         let leader_worker_id = StorageWorkingGroup::current_lead().unwrap();
@@ -468,6 +554,7 @@ fn create_slash_group_leader_stake_proposal_execution_succeeds() {
             account_id,
             account_id,
             leader_worker_id,
             leader_worker_id,
             slashing_stake_amount,
             slashing_stake_amount,
+            4,
         );
         );
 
 
         let new_balance = Balances::free_balance::<&AccountId32>(&account_id.into());
         let new_balance = Balances::free_balance::<&AccountId32>(&account_id.into());
@@ -481,6 +568,24 @@ fn create_slash_group_leader_stake_proposal_execution_succeeds() {
     });
     });
 }
 }
 
 
+#[test]
+fn create_set_working_group_mint_capacity_proposal_execution_succeeds() {
+    initial_test_ext().execute_with(|| {
+        let member_id = 1;
+        let account_id: [u8; 32] = [member_id; 32];
+
+        assert_eq!(StorageWorkingGroup::mint(), 0);
+
+        let mint_capacity = 999999;
+        set_mint_capacity(member_id, account_id, mint_capacity, 1, true);
+
+        let mint_id = StorageWorkingGroup::mint();
+        let mint = <minting::Module<Runtime>>::mints(mint_id);
+
+        assert_eq!(mint.capacity(), mint_capacity);
+    });
+}
+
 #[test]
 #[test]
 fn create_set_group_leader_reward_proposal_execution_succeeds() {
 fn create_set_group_leader_reward_proposal_execution_succeeds() {
     initial_test_ext().execute_with(|| {
     initial_test_ext().execute_with(|| {
@@ -503,6 +608,7 @@ fn create_set_group_leader_reward_proposal_execution_succeeds() {
             account_id.clone(),
             account_id.clone(),
             ActivateOpeningAt::CurrentBlock,
             ActivateOpeningAt::CurrentBlock,
             Some(opening_policy_commitment),
             Some(opening_policy_commitment),
+            1,
         );
         );
 
 
         let apply_result = StorageWorkingGroup::apply_on_opening(
         let apply_result = StorageWorkingGroup::apply_on_opening(
@@ -519,7 +625,7 @@ fn create_set_group_leader_reward_proposal_execution_succeeds() {
 
 
         let expected_application_id = 0;
         let expected_application_id = 0;
 
 
-        begin_review_applications(member_id, account_id, opening_id);
+        begin_review_applications(member_id, account_id, opening_id, 2);
 
 
         let lead = StorageWorkingGroup::current_lead();
         let lead = StorageWorkingGroup::current_lead();
         assert!(lead.is_none());
         assert!(lead.is_none());
@@ -531,12 +637,15 @@ fn create_set_group_leader_reward_proposal_execution_succeeds() {
             payout_interval: None,
             payout_interval: None,
         });
         });
 
 
+        set_mint_capacity(member_id, account_id, 999999, 3, false);
+
         fill_opening(
         fill_opening(
             member_id,
             member_id,
             account_id,
             account_id,
             opening_id,
             opening_id,
             expected_application_id,
             expected_application_id,
             reward_policy,
             reward_policy,
+            4,
         );
         );
 
 
         let leader_worker_id = StorageWorkingGroup::current_lead().unwrap();
         let leader_worker_id = StorageWorkingGroup::current_lead().unwrap();
@@ -548,9 +657,179 @@ fn create_set_group_leader_reward_proposal_execution_succeeds() {
         assert_eq!(relationship.amount_per_payout, old_reward_amount);
         assert_eq!(relationship.amount_per_payout, old_reward_amount);
 
 
         let new_reward_amount = 999;
         let new_reward_amount = 999;
-        set_reward(member_id, account_id, leader_worker_id, new_reward_amount);
+        set_reward(
+            member_id,
+            account_id,
+            leader_worker_id,
+            new_reward_amount,
+            5,
+        );
 
 
         let relationship = recurringrewards::RewardRelationships::<Runtime>::get(relationship_id);
         let relationship = recurringrewards::RewardRelationships::<Runtime>::get(relationship_id);
         assert_eq!(relationship.amount_per_payout, new_reward_amount);
         assert_eq!(relationship.amount_per_payout, new_reward_amount);
     });
     });
 }
 }
+
+#[test]
+fn create_terminate_group_leader_role_proposal_execution_succeeds() {
+    initial_test_ext().execute_with(|| {
+        let member_id = 1;
+        let account_id: [u8; 32] = [member_id; 32];
+        let stake_amount = 100;
+
+        let opening_policy_commitment = OpeningPolicyCommitment {
+            role_staking_policy: Some(hiring::StakingPolicy {
+                amount: 100,
+                amount_mode: hiring::StakingAmountLimitMode::AtLeast,
+                crowded_out_unstaking_period_length: None,
+                review_period_expired_unstaking_period_length: None,
+            }),
+            ..OpeningPolicyCommitment::default()
+        };
+
+        let opening_id = add_opening(
+            member_id,
+            account_id.clone(),
+            ActivateOpeningAt::CurrentBlock,
+            Some(opening_policy_commitment),
+            1,
+        );
+
+        let apply_result = StorageWorkingGroup::apply_on_opening(
+            RawOrigin::Signed(account_id.clone().into()).into(),
+            member_id as u64,
+            opening_id,
+            account_id.clone().into(),
+            Some(stake_amount),
+            None,
+            Vec::new(),
+        );
+
+        assert_eq!(apply_result, Ok(()));
+
+        let expected_application_id = 0;
+
+        begin_review_applications(member_id, account_id, opening_id, 2);
+
+        let lead = StorageWorkingGroup::current_lead();
+        assert!(lead.is_none());
+
+        let old_reward_amount = 100;
+        let reward_policy = Some(RewardPolicy {
+            amount_per_payout: old_reward_amount,
+            next_payment_at_block: 9999,
+            payout_interval: None,
+        });
+
+        set_mint_capacity(member_id, account_id, 999999, 3, false);
+
+        fill_opening(
+            member_id,
+            account_id,
+            opening_id,
+            expected_application_id,
+            reward_policy,
+            4,
+        );
+
+        let leader_worker_id = StorageWorkingGroup::current_lead().unwrap();
+
+        let stake_id = 1;
+        let old_balance = Balances::free_balance::<&AccountId32>(&account_id.into());
+        let old_stake = <stake::Module<Runtime>>::stakes(stake_id);
+
+        assert_eq!(get_stake_balance(old_stake), stake_amount);
+
+        terminate_role(member_id, account_id, leader_worker_id, false, 5);
+
+        assert!(StorageWorkingGroup::current_lead().is_none());
+
+        let new_balance = Balances::free_balance::<&AccountId32>(&account_id.into());
+        let new_stake = <stake::Module<Runtime>>::stakes(stake_id);
+
+        assert_eq!(new_stake.staking_status, stake::StakingStatus::NotStaked);
+        assert_eq!(new_balance, old_balance + stake_amount);
+    });
+}
+
+#[test]
+fn create_terminate_group_leader_role_proposal_with_slashing_execution_succeeds() {
+    initial_test_ext().execute_with(|| {
+        let member_id = 1;
+        let account_id: [u8; 32] = [member_id; 32];
+        let stake_amount = 100;
+
+        let opening_policy_commitment = OpeningPolicyCommitment {
+            role_staking_policy: Some(hiring::StakingPolicy {
+                amount: 100,
+                amount_mode: hiring::StakingAmountLimitMode::AtLeast,
+                crowded_out_unstaking_period_length: None,
+                review_period_expired_unstaking_period_length: None,
+            }),
+            ..OpeningPolicyCommitment::default()
+        };
+
+        let opening_id = add_opening(
+            member_id,
+            account_id.clone(),
+            ActivateOpeningAt::CurrentBlock,
+            Some(opening_policy_commitment),
+            1,
+        );
+
+        let apply_result = StorageWorkingGroup::apply_on_opening(
+            RawOrigin::Signed(account_id.clone().into()).into(),
+            member_id as u64,
+            opening_id,
+            account_id.clone().into(),
+            Some(stake_amount),
+            None,
+            Vec::new(),
+        );
+
+        assert_eq!(apply_result, Ok(()));
+
+        let expected_application_id = 0;
+
+        begin_review_applications(member_id, account_id, opening_id, 2);
+
+        let lead = StorageWorkingGroup::current_lead();
+        assert!(lead.is_none());
+
+        let old_reward_amount = 100;
+        let reward_policy = Some(RewardPolicy {
+            amount_per_payout: old_reward_amount,
+            next_payment_at_block: 9999,
+            payout_interval: None,
+        });
+
+        set_mint_capacity(member_id, account_id, 999999, 3, false);
+
+        fill_opening(
+            member_id,
+            account_id,
+            opening_id,
+            expected_application_id,
+            reward_policy,
+            4,
+        );
+
+        let leader_worker_id = StorageWorkingGroup::current_lead().unwrap();
+
+        let stake_id = 1;
+        let old_balance = Balances::free_balance::<&AccountId32>(&account_id.into());
+        let old_stake = <stake::Module<Runtime>>::stakes(stake_id);
+
+        assert_eq!(get_stake_balance(old_stake), stake_amount);
+
+        terminate_role(member_id, account_id, leader_worker_id, true, 5);
+
+        assert!(StorageWorkingGroup::current_lead().is_none());
+
+        let new_balance = Balances::free_balance::<&AccountId32>(&account_id.into());
+        let new_stake = <stake::Module<Runtime>>::stakes(stake_id);
+
+        assert_eq!(new_stake.staking_status, stake::StakingStatus::NotStaked);
+        assert_eq!(new_balance, old_balance);
+    });
+}

+ 2 - 0
storage-node/.gitignore

@@ -25,3 +25,5 @@ node_modules/
 
 
 # Ignore nvm config file
 # Ignore nvm config file
 .nvmrc
 .nvmrc
+
+yarn.lock

+ 38 - 7
storage-node/README.md

@@ -4,11 +4,11 @@ This repository contains several Node packages, located under the `packages/`
 subdirectory. See each individual package for details:
 subdirectory. See each individual package for details:
 
 
 * [colossus](./packages/colossus/README.md) - the main colossus app.
 * [colossus](./packages/colossus/README.md) - the main colossus app.
-* [storage](./packages/storage/README.md) - abstraction over the storage backend.
+* [storage-node-backend](./packages/storage/README.md) - abstraction over the storage backend.
-* [runtime-api](./packages/runtime-api/README.md) - convenience wrappers for the runtime API.
+* [storage-runtime-api](./packages/runtime-api/README.md) - convenience wrappers for the runtime API.
-* [crypto](./packages/crypto/README.md) - cryptographic utility functions.
+* [storage-utils](./packages/util/README.md) - general utility functions.
-* [util](./packages/util/README.md) - general utility functions.
 * [discovery](./packages/discovery/README.md) - service discovery using IPNS.
 * [discovery](./packages/discovery/README.md) - service discovery using IPNS.
+* [storage-cli](./packages/cli/README.md) - cli for uploading and downloading content from the network
 
 
 Installation
 Installation
 ------------
 ------------
@@ -40,17 +40,48 @@ $ yarn install
 The command will install dependencies, and make a `colossus` executable available:
 The command will install dependencies, and make a `colossus` executable available:
 
 
 ```bash
 ```bash
-$ yarn run colossus --help
+$ yarn colossus --help
 ```
 ```
 
 
 *Testing*
 *Testing*
 
 
-Running tests from the repository root will run tests from all packages:
+Run an ipfs node and a joystream-node development chain (in separate terminals)
 
 
+```sh
+ipfs daemon
 ```
 ```
-$ yarn run test
+
+```sh
+joystream-node --dev
+```
+
+```sh
+$ yarn workspace storage-node test
+```
+
+Running a development environment, after starting the ipfs node and development chain
+
+```sh
+yarn storage-cli dev-init
+```
+
+This will configure the running chain with alice as the storage lead and with a know role key for
+the storage provider.
+
+Run colossus in development mode:
+
+```sh
+yarn colossus --dev
+```
+
+Start pioneer ui:
+``sh
+yarn workspace pioneer start
 ```
 ```
 
 
+Browse pioneer on http://localhost:3000/
+You should find Alice account is the storage working group lead and is a storage provider
+Create a media channel. And upload a file.
 
 
 ## Detailed Setup and Configuration Guide
 ## Detailed Setup and Configuration Guide
 For details on how to setup a storage node on the Joystream network, follow this [step by step guide](https://github.com/Joystream/helpdesk/tree/master/roles/storage-providers).
 For details on how to setup a storage node on the Joystream network, follow this [step by step guide](https://github.com/Joystream/helpdesk/tree/master/roles/storage-providers).

+ 0 - 18
storage-node/license_header.txt

@@ -1,18 +0,0 @@
-/*
- * This file is part of the storage node for the Joystream project.
- * Copyright (C) 2019 Joystream Contributors
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <https://www.gnu.org/licenses/>.
- */
-

+ 1 - 4
storage-node/package.json

@@ -1,6 +1,6 @@
 {
 {
   "private": true,
   "private": true,
-  "name": "@joystream/storage-node",
+  "name": "storage-node",
   "version": "1.0.0",
   "version": "1.0.0",
   "engines": {
   "engines": {
     "node": ">=10.15.3",
     "node": ">=10.15.3",
@@ -30,9 +30,6 @@
     "darwin",
     "darwin",
     "linux"
     "linux"
   ],
   ],
-  "workspaces": [
-    "packages/*"
-  ],
   "scripts": {
   "scripts": {
     "test": "wsrun --serial test",
     "test": "wsrun --serial test",
     "lint": "wsrun --serial lint"
     "lint": "wsrun --serial lint"

+ 36 - 1
storage-node/packages/cli/README.md

@@ -1,5 +1,40 @@
 # A CLI for the Joystream Runtime & Colossus
 # A CLI for the Joystream Runtime & Colossus
 
 
-- CLI access for some functionality from `@joystream/runtime-api`
+- CLI access for some functionality from other packages in the storage-node workspace
 - Colossus/storage node functionality:
 - Colossus/storage node functionality:
   - File uploads
   - File uploads
+  - File downloads
+- Development
+  - Setup development environment
+
+Running the storage cli tool:
+
+```sh
+$ yarn storage-cli --help
+```
+
+```sh
+
+  Joystream tool for uploading and downloading files to the network
+
+  Usage:
+    $ storage-cli command [arguments..] [key_file] [passphrase]
+
+  Some commands require a key file as the last option holding the identity for
+  interacting with the runtime API.
+
+  Commands:
+    upload            Upload a file to a Colossus storage node. Requires a
+                      storage node URL, and a local file name to upload. As
+                      an optional third parameter, you can provide a Data
+                      Object Type ID - this defaults to "1" if not provided.
+    download          Retrieve a file. Requires a storage node URL and a content
+                      ID, as well as an output filename.
+    head              Send a HEAD request for a file, and print headers.
+                      Requires a storage node URL and a content ID.
+
+  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.
+
+```

+ 136 - 120
storage-node/packages/cli/bin/cli.js

@@ -17,37 +17,28 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
  */
 
 
-'use strict';
+'use strict'
 
 
-const path = require('path');
+const fs = require('fs')
-const fs = require('fs');
+const assert = require('assert')
-const assert = require('assert');
+const { RuntimeApi } = require('@joystream/storage-runtime-api')
-
+const meow = require('meow')
-const { RuntimeApi } = require('@joystream/runtime-api');
+const chalk = require('chalk')
-
+const _ = require('lodash')
-const meow = require('meow');
+const debug = require('debug')('joystream:storage-cli')
-const chalk = require('chalk');
+const dev = require('./dev')
-const _ = require('lodash');
-
-const debug = require('debug')('joystream:cli');
-
-// Project root
-const project_root = path.resolve(__dirname, '..');
-
-// Configuration (default)
-const pkg = require(path.resolve(project_root, 'package.json'));
 
 
 // Parse CLI
 // Parse CLI
 const FLAG_DEFINITIONS = {
 const FLAG_DEFINITIONS = {
   // TODO
   // TODO
-};
+}
 
 
 const cli = meow(`
 const cli = meow(`
   Usage:
   Usage:
-    $ joystream key_file command [options]
+    $ storage-cli command [arguments..] [key_file] [passphrase]
 
 
-  All commands require a key file holding the identity for interacting with the
+  Some commands require a key file as the last option holding the identity for
-  runtime API.
+  interacting with the runtime API.
 
 
   Commands:
   Commands:
     upload            Upload a file to a Colossus storage node. Requires a
     upload            Upload a file to a Colossus storage node. Requires a
@@ -58,173 +49,198 @@ const cli = meow(`
                       ID, as well as an output filename.
                       ID, as well as an output filename.
     head              Send a HEAD request for a file, and print headers.
     head              Send a HEAD request for a file, and print headers.
                       Requires a storage node URL and a content ID.
                       Requires a storage node URL and a content ID.
+
+  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.
   `,
   `,
-  { flags: FLAG_DEFINITIONS });
+  { flags: FLAG_DEFINITIONS })
 
 
-function assert_file(name, filename)
+function assert_file (name, filename) {
-{
+  assert(filename, `Need a ${name} parameter to proceed!`)
-  assert(filename, `Need a ${name} parameter to proceed!`);
+  assert(fs.statSync(filename).isFile(), `Path "${filename}" is not a file, aborting!`)
-  assert(fs.statSync(filename).isFile(), `Path "${filename}" is not a file, aborting!`);
+}
+
+function load_identity (api, filename, passphrase) {
+  if (filename) {
+    assert_file('keyfile', filename)
+    api.identities.loadUnlock(filename, passphrase)
+  } else {
+    debug('Loading Alice as identity')
+    api.identities.useKeyPair(dev.aliceKeyPair(api))
+  }
 }
 }
 
 
 const commands = {
 const commands = {
-  'upload': async (runtime_api, url, filename, do_type_id) => {
+  // add Alice well known account as storage provider
+  'dev-init': async (api) => {
+    // dev accounts are automatically loaded, no need to add explicitly to keyring
+    // load_identity(api)
+    let dev = require('./dev')
+    return dev.init(api)
+  },
+  // Checks that the setup done by dev-init command was successful.
+  'dev-check': async (api) => {
+    // dev accounts are automatically loaded, no need to add explicitly to keyring
+    // load_identity(api)
+    let dev = require('./dev')
+    return dev.check(api)
+  },
+  // The upload method is not correctly implemented
+  // needs to get the liaison after creating a data object,
+  // resolve the ipns id to the asset put api url of the storage-node
+  // before uploading..
+  'upload': async (api, url, filename, do_type_id, keyfile, passphrase) => {
+    load_identity(keyfile, passphrase)
     // Check parameters
     // Check parameters
-    assert_file('file', filename);
+    assert_file('file', filename)
 
 
-    const size = fs.statSync(filename).size;
+    const size = fs.statSync(filename).size
-    console.log(`File "${filename}" is ` + chalk.green(size) + ' Bytes.');
+    debug(`File "${filename}" is ${chalk.green(size)} Bytes.`)
 
 
     if (!do_type_id) {
     if (!do_type_id) {
-      do_type_id = 1;
+      do_type_id = 1
     }
     }
-    console.log('Data Object Type ID is: ' + chalk.green(do_type_id));
+
+    debug('Data Object Type ID is: ' + chalk.green(do_type_id))
 
 
     // Generate content ID
     // Generate content ID
     // FIXME this require path is like this because of
     // FIXME this require path is like this because of
     // https://github.com/Joystream/apps/issues/207
     // https://github.com/Joystream/apps/issues/207
-    const { ContentId } = require('@joystream/types/lib/media');
+    const { ContentId } = require('@joystream/types/media')
-    var cid = ContentId.generate();
+    var cid = ContentId.generate()
-    cid = cid.encode().toString();
+    cid = cid.encode().toString()
-    console.log('Generated content ID: ' + chalk.green(cid));
+    debug('Generated content ID: ' + chalk.green(cid))
 
 
     // Create Data Object
     // Create Data Object
-    const data_object = await runtime_api.assets.createDataObject(
+    const data_object = await api.assets.createDataObject(
-      runtime_api.identities.key.address, cid, do_type_id, size);
+      api.identities.key.address, cid, do_type_id, size)
-    console.log('Data object created.');
+    debug('Data object created.')
 
 
     // TODO in future, optionally contact liaison here?
     // TODO in future, optionally contact liaison here?
-    const request = require('request');
+    const request = require('request')
-    url = `${url}asset/v0/${cid}`;
+    url = `${url}asset/v0/${cid}`
-    console.log('Uploading to URL', chalk.green(url));
+    debug('Uploading to URL', chalk.green(url))
 
 
-    const f = fs.createReadStream(filename);
+    const f = fs.createReadStream(filename)
     const opts = {
     const opts = {
       url: url,
       url: url,
       headers: {
       headers: {
         'content-type': '',
         'content-type': '',
-        'content-length': `${size}`,
+        'content-length': `${size}`
       },
       },
-      json: true,
+      json: true
-    };
+    }
     return new Promise((resolve, reject) => {
     return new Promise((resolve, reject) => {
       const r = request.put(opts, (error, response, body) => {
       const r = request.put(opts, (error, response, body) => {
         if (error) {
         if (error) {
-          reject(error);
+          reject(error)
-          return;
+          return
         }
         }
 
 
-        if (response.statusCode / 100 != 2) {
+        if (response.statusCode / 100 !== 2) {
-          reject(new Error(`${response.statusCode}: ${body.message || 'unknown reason'}`));
+          reject(new Error(`${response.statusCode}: ${body.message || 'unknown reason'}`))
-          return;
+          return
         }
         }
-        console.log('Upload successful:', body.message);
+        debug('Upload successful:', body.message)
-        resolve();
+        resolve()
-      });
+      })
-      f.pipe(r);
+      f.pipe(r)
-    });
+    })
   },
   },
-
+  // needs to be updated to take a content id and resolve it a potential set
-  'download': async (runtime_api, url, content_id, filename) => {
+  // of providers that has it, and select one (possibly try more than one provider)
-    const request = require('request');
+  // to fetch it from the get api url of a provider..
-    url = `${url}asset/v0/${content_id}`;
+  'download': async (api, url, content_id, filename) => {
-    console.log('Downloading URL', chalk.green(url), 'to', chalk.green(filename));
+    const request = require('request')
-
+    url = `${url}asset/v0/${content_id}`
-    const f = fs.createWriteStream(filename);
+    debug('Downloading URL', chalk.green(url), 'to', chalk.green(filename))
+
+    const f = fs.createWriteStream(filename)
     const opts = {
     const opts = {
       url: url,
       url: url,
-      json: true,
+      json: true
-    };
+    }
     return new Promise((resolve, reject) => {
     return new Promise((resolve, reject) => {
       const r = request.get(opts, (error, response, body) => {
       const r = request.get(opts, (error, response, body) => {
         if (error) {
         if (error) {
-          reject(error);
+          reject(error)
-          return;
+          return
         }
         }
 
 
-        console.log('Downloading', chalk.green(response.headers['content-type']), 'of size', chalk.green(response.headers['content-length']), '...');
+        debug('Downloading', chalk.green(response.headers['content-type']), 'of size', chalk.green(response.headers['content-length']), '...')
 
 
         f.on('error', (err) => {
         f.on('error', (err) => {
-          reject(err);
+          reject(err)
-        });
+        })
 
 
         f.on('finish', () => {
         f.on('finish', () => {
-          if (response.statusCode / 100 != 2) {
+          if (response.statusCode / 100 !== 2) {
-            reject(new Error(`${response.statusCode}: ${body.message || 'unknown reason'}`));
+            reject(new Error(`${response.statusCode}: ${body.message || 'unknown reason'}`))
-            return;
+            return
           }
           }
-          console.log('Download completed.');
+          debug('Download completed.')
-          resolve();
+          resolve()
-        });
+        })
-      });
+      })
-      r.pipe(f);
+      r.pipe(f)
-    });
+    })
   },
   },
-
+  // similar to 'download' function
-  'head': async (runtime_api, url, content_id) => {
+  'head': async (api, url, content_id) => {
-    const request = require('request');
+    const request = require('request')
-    url = `${url}asset/v0/${content_id}`;
+    url = `${url}asset/v0/${content_id}`
-    console.log('Checking URL', chalk.green(url), '...');
+    debug('Checking URL', chalk.green(url), '...')
 
 
     const opts = {
     const opts = {
       url: url,
       url: url,
-      json: true,
+      json: true
-    };
+    }
     return new Promise((resolve, reject) => {
     return new Promise((resolve, reject) => {
       const r = request.head(opts, (error, response, body) => {
       const r = request.head(opts, (error, response, body) => {
         if (error) {
         if (error) {
-          reject(error);
+          reject(error)
-          return;
+          return
         }
         }
 
 
-        if (response.statusCode / 100 != 2) {
+        if (response.statusCode / 100 !== 2) {
-          reject(new Error(`${response.statusCode}: ${body.message || 'unknown reason'}`));
+          reject(new Error(`${response.statusCode}: ${body.message || 'unknown reason'}`))
-          return;
+          return
         }
         }
 
 
         for (var propname in response.headers) {
         for (var propname in response.headers) {
-          console.log(`  ${chalk.yellow(propname)}: ${response.headers[propname]}`);
+          debug(`  ${chalk.yellow(propname)}: ${response.headers[propname]}`)
         }
         }
 
 
-        resolve();
+        resolve()
-      });
+      })
-    });
+    })
-  },
+  }
-
+}
-};
-
-
-async function main()
-{
-  // Key file is at the first instance.
-  const key_file = cli.input[0];
-  assert_file('key file', key_file);
 
 
-  // Create runtime API.
+async function main () {
-  const runtime_api = await RuntimeApi.create({ account_file: key_file });
+  const api = await RuntimeApi.create()
 
 
   // Simple CLI commands
   // Simple CLI commands
-  const command = cli.input[1];
+  const command = cli.input[0]
   if (!command) {
   if (!command) {
-    throw new Error('Need a command to run!');
+    throw new Error('Need a command to run!')
   }
   }
 
 
   if (commands.hasOwnProperty(command)) {
   if (commands.hasOwnProperty(command)) {
     // Command recognized
     // Command recognized
-    const args = _.clone(cli.input).slice(2);
+    const args = _.clone(cli.input).slice(1)
-    await commands[command](runtime_api, ...args);
+    await commands[command](api, ...args)
-  }
+  } else {
-  else {
+    throw new Error(`Command "${command}" not recognized, aborting!`)
-    throw new Error(`Command "${command}" not recognized, aborting!`);
   }
   }
 }
 }
 
 
 main()
 main()
   .then(() => {
   .then(() => {
-    console.log('Process exiting gracefully.');
+    process.exit(0)
-    process.exit(0);
   })
   })
   .catch((err) => {
   .catch((err) => {
-    console.error(chalk.red(err.stack));
+    console.error(chalk.red(err.stack))
-    process.exit(-1);
+    process.exit(-1)
-  });
+  })

+ 128 - 0
storage-node/packages/cli/bin/dev.js

@@ -0,0 +1,128 @@
+/* eslint-disable no-console */
+
+'use strict'
+
+const debug = require('debug')('joystream:storage-cli:dev')
+const assert = require('assert')
+
+// Derivation path appended to well known development seed used on
+// development chains
+const ALICE_URI = '//Alice'
+const ROLE_ACCOUNT_URI = '//Colossus'
+
+function aliceKeyPair (api) {
+  return api.identities.keyring.addFromUri(ALICE_URI, null, 'sr25519')
+}
+
+function roleKeyPair (api) {
+  return api.identities.keyring.addFromUri(ROLE_ACCOUNT_URI, null, 'sr25519')
+}
+
+function developmentPort () {
+  return 3001
+}
+
+const check = async (api) => {
+  const roleAccountId = roleKeyPair(api).address
+  const providerId = await api.workers.findProviderIdByRoleAccount(roleAccountId)
+
+  if (providerId === null) {
+    throw new Error('Dev storage provider not found on chain!')
+  }
+
+  console.log(`
+  Chain is setup with Dev storage provider:
+    providerId = ${providerId}
+    roleAccountId = ${roleAccountId}
+    roleKey = ${ROLE_ACCOUNT_URI}
+  `)
+
+  return providerId
+}
+
+// Setup Alice account on a developement chain as
+// a member, storage lead, and a storage provider using a deterministic
+// development key for the role account
+const init = async (api) => {
+  try {
+    await check(api)
+    return
+  } catch (err) {
+    // We didn't find a storage provider with expected role account
+  }
+
+  const alice = aliceKeyPair(api).address
+  const roleAccount = roleKeyPair(api).address
+
+  debug(`Ensuring Alice is sudo`)
+
+  // make sure alice is sudo - indirectly checking this is a dev chain
+  const sudo = await api.identities.getSudoAccount()
+
+  if (!sudo.eq(alice)) {
+    throw new Error('Setup requires Alice to be sudo. Are you sure you are running a devchain?')
+  }
+
+  console.log('Running setup')
+
+  // set localhost colossus as discovery provider
+  // assuming pioneer dev server is running on port 3000 we should run
+  // the storage dev server on a different port than the default for colossus which is also
+  // 3000
+  debug('Setting Local development node as bootstrap endpoint')
+  await api.discovery.setBootstrapEndpoints(alice, [`http://localhost:${developmentPort()}/`])
+
+  debug('Transferring tokens to storage role account')
+  // Give role account some tokens to work with
+  api.balances.transfer(alice, roleAccount, 100000)
+
+  debug('Ensuring Alice is as member..')
+  let aliceMemberId = await api.identities.firstMemberIdOf(alice)
+
+  if (aliceMemberId === undefined) {
+    debug('Registering Alice as member..')
+    aliceMemberId = await api.identities.registerMember(alice, {
+      handle: 'alice'
+    })
+  } else {
+    debug('Alice is already a member')
+  }
+
+  // Make alice the storage lead
+  debug('Making Alice the storage Lead')
+  const leadOpeningId = await api.workers.dev_addStorageLeadOpening()
+  const leadApplicationId = await api.workers.dev_applyOnOpening(leadOpeningId, aliceMemberId, alice, alice)
+  api.workers.dev_beginLeadOpeningReview(leadOpeningId)
+  await api.workers.dev_fillLeadOpening(leadOpeningId, leadApplicationId)
+
+  const leadAccount = await api.workers.getLeadRoleAccount()
+  if (!leadAccount.eq(alice)) {
+    throw new Error('Setting alice as lead failed')
+  }
+
+  // Create a storage openinging, apply, start review, and fill opening
+  debug(`Making ${ROLE_ACCOUNT_URI} account a storage provider`)
+
+  const openingId = await api.workers.dev_addStorageOpening()
+  debug(`created new storage opening: ${openingId}`)
+
+  const applicationId = await api.workers.dev_applyOnOpening(openingId, aliceMemberId, alice, roleAccount)
+  debug(`applied with application id: ${applicationId}`)
+
+  api.workers.dev_beginStorageOpeningReview(openingId)
+
+  debug(`filling storage opening`)
+  const providerId = await api.workers.dev_fillStorageOpening(openingId, applicationId)
+
+  debug(`Assigned storage provider id: ${providerId}`)
+
+  return check(api)
+}
+
+module.exports = {
+  init,
+  check,
+  aliceKeyPair,
+  roleKeyPair,
+  developmentPort
+}

+ 3 - 2
storage-node/packages/cli/package.json

@@ -1,5 +1,6 @@
 {
 {
   "name": "@joystream/storage-cli",
   "name": "@joystream/storage-cli",
+  "private": true,
   "version": "0.1.0",
   "version": "0.1.0",
   "description": "Joystream tool for uploading and downloading files to the network",
   "description": "Joystream tool for uploading and downloading files to the network",
   "author": "Joystream",
   "author": "Joystream",
@@ -30,7 +31,7 @@
     "lint": "eslint 'paths/**/*.js' 'lib/**/*.js'"
     "lint": "eslint 'paths/**/*.js' 'lib/**/*.js'"
   },
   },
   "bin": {
   "bin": {
-    "joystream": "bin/cli.js"
+    "storage-cli": "bin/cli.js"
   },
   },
   "devDependencies": {
   "devDependencies": {
     "chai": "^4.2.0",
     "chai": "^4.2.0",
@@ -39,7 +40,7 @@
     "temp": "^0.9.0"
     "temp": "^0.9.0"
   },
   },
   "dependencies": {
   "dependencies": {
-    "@joystream/runtime-api": "^0.1.0",
+    "@joystream/storage-runtime-api": "^0.1.0",
     "chalk": "^2.4.2",
     "chalk": "^2.4.2",
     "lodash": "^4.17.11",
     "lodash": "^4.17.11",
     "meow": "^5.0.0",
     "meow": "^5.0.0",

+ 36 - 42
storage-node/packages/colossus/README.md

@@ -3,59 +3,53 @@
 Development
 Development
 -----------
 -----------
 
 
-Run a development server:
+Run a development server (an ipfs node and development chain should be running on the local machine)
 
 
 ```bash
 ```bash
-$ yarn run dev --config myconfig.json
+$ yarn colossus --dev
 ```
 ```
 
 
-Command-Line
+This will expect the chain to be configured with certain development accounts.
-------------
+The setup can be done by running the dev-init command for the storage-cli:
 
 
-Running a storage server is (almost) as easy as running the bundled `colossus`
+```sh
-executable:
+yarn storage-cli dev-init
-
-```bash
-$ colossus --storage=/path/to/storage/directory
 ```
 ```
 
 
-Run with `--help` to see a list of available CLI options.
-
-You need to stake as a storage provider to run a storage node.
-
-Configuration
--------------
-
-Most common configuration options are available as command-line options
-for the CLI.
 
 
-However, some advanced configuration options are only possible to set
+Command-Line
-via the configuration file.
+------------
-
-* `filter` is a hash of upload filtering options.
-  * `max_size` sets the maximum permissible file upload size. If unset,
-    this defaults to 100 MiB.
-  * `mime` is a hash of...
-    * `accept` is an Array of mime types that are acceptable for uploads,
-      such as `text/plain`, etc. Mime types can also be specified for
-      wildcard matching, such as `video/*`.
-    * `reject` is an Array of mime types that are unacceptable for uploads.
-
-Upload Filtering
-----------------
 
 
-The upload filtering logic first tests whether any of the `accept` mime types
+```sh
-are matched. If none are matched, the upload is rejected. If any is matched,
+$ yarn colossus --help
-then the upload is still rejected if any of the `reject` mime types are
+```
-matched.
 
 
-This allows inclusive and exclusive filtering.
+```
+  Colossus - Joystream Storage Node
+
+  Usage:
+    $ colossus [command] [arguments]
+
+  Commands:
+    server        Runs a production server instance. (discovery and storage services)
+                  This is the default command if not specified.
+    discovery     Run the discovery service only.
+
+  Arguments (required for server. Ignored if running server with --dev option):
+    --provider-id ID, -i ID     StorageProviderId assigned to you in working group.
+    --key-file FILE             JSON key export file to use as the storage provider (role account).
+    --public-url=URL, -u URL    API Public URL to announce.
+
+  Arguments (optional):
+    --dev                   Runs server with developer settings.
+    --passphrase            Optional passphrase to use to decrypt the key-file.
+    --port=PORT, -p PORT    Port number to listen on, defaults to 3000.
+    --ws-provider WS_URL    Joystream-node websocket provider, defaults to ws://localhost:9944
+```
 
 
-* `{ accept: ['text/plain', 'text/html'] }` accepts *only* the two given mime types.
+To run a storage server in production you will need to enroll on the network first to
-* `{ accept: ['text/*'], reject: ['text/plain'] }` accepts any `text/*` that is not
+obtain your provider-id and role account.
-  `text/plain`.
 
 
-More advanced filtering is currently not available.
 
 
 API Packages
 API Packages
 ------------
 ------------
@@ -78,7 +72,7 @@ For reusability across API versions, it's best to keep files in the `paths`
 subfolder very thin, and instead inject implementations via the `dependencies`
 subfolder very thin, and instead inject implementations via the `dependencies`
 configuration value of `express-openapi`.
 configuration value of `express-openapi`.
 
 
-These implementations line to the `./lib` subfolder. Adjust `server.js` as
+These implementations line to the `./lib` subfolder. Adjust `app.js` as
 needed to make them available to API packages.
 needed to make them available to API packages.
 
 
 Streaming Notes
 Streaming Notes

+ 166 - 260
storage-node/packages/colossus/bin/cli.js

@@ -1,299 +1,226 @@
 #!/usr/bin/env node
 #!/usr/bin/env node
-'use strict';
+/* es-lint disable*/
+
+'use strict'
 
 
 // Node requires
 // Node requires
-const path = require('path');
+const path = require('path')
 
 
 // npm requires
 // npm requires
-const meow = require('meow');
+const meow = require('meow')
-const configstore = require('configstore');
+const chalk = require('chalk')
-const chalk = require('chalk');
+const figlet = require('figlet')
-const figlet = require('figlet');
+const _ = require('lodash')
-const _ = require('lodash');
 
 
-const debug = require('debug')('joystream:cli');
+const debug = require('debug')('joystream:colossus')
 
 
 // Project root
 // Project root
-const PROJECT_ROOT = path.resolve(__dirname, '..');
+const PROJECT_ROOT = path.resolve(__dirname, '..')
 
 
-// Configuration (default)
+// Number of milliseconds to wait between synchronization runs.
-const pkg = require(path.resolve(PROJECT_ROOT, 'package.json'));
+const SYNC_PERIOD_MS = 300000 // 5min
-const default_config = new configstore(pkg.name);
 
 
 // Parse CLI
 // Parse CLI
 const FLAG_DEFINITIONS = {
 const FLAG_DEFINITIONS = {
   port: {
   port: {
-    type: 'integer',
+    type: 'number',
     alias: 'p',
     alias: 'p',
-    _default: 3000,
+    default: 3000
-  },
-  'syncPeriod': {
-    type: 'integer',
-    _default: 120000,
   },
   },
   keyFile: {
   keyFile: {
     type: 'string',
     type: 'string',
+    isRequired: (flags, input) => {
+      return !flags.dev
+    }
   },
   },
-  config: {
+  publicUrl: {
-    type: 'string',
-    alias: 'c',
-  },
-  'publicUrl': {
     type: 'string',
     type: 'string',
-    alias: 'u'
+    alias: 'u',
+    isRequired: (flags, input) => {
+      return !flags.dev
+    }
   },
   },
-  'passphrase': {
+  passphrase: {
     type: 'string'
     type: 'string'
   },
   },
-  'wsProvider': {
+  wsProvider: {
     type: 'string',
     type: 'string',
-    _default: 'ws://localhost:9944'
+    default: 'ws://localhost:9944'
+  },
+  providerId: {
+    type: 'number',
+    alias: 'i',
+    isRequired: (flags, input) => {
+      return !flags.dev
+    }
   }
   }
-};
+}
 
 
 const cli = meow(`
 const cli = meow(`
   Usage:
   Usage:
-    $ colossus [command] [options]
+    $ colossus [command] [arguments]
 
 
   Commands:
   Commands:
-    server [default]  Run a server instance with the given configuration.
+    server        Runs a production server instance. (discovery and storage services)
-    signup            Sign up as a storage provider. Requires that you provide
+                  This is the default command if not specified.
-                      a JSON account file of an account that is a member, and has
+    discovery     Run the discovery service only.
-                      sufficient balance for staking as a storage provider.
+
-                      Writes a new account file that should be used to run the
+  Arguments (required for server. Ignored if running server with --dev option):
-                      storage node.
+    --provider-id ID, -i ID     StorageProviderId assigned to you in working group.
-    down              Signal to network that all services are down. Running
+    --key-file FILE             JSON key export file to use as the storage provider (role account).
-                      the server will signal that services as online again.
+    --public-url=URL, -u URL    API Public URL to announce.
-    discovery         Run the discovery service only.
+
-
+  Arguments (optional):
-  Options:
+    --dev                   Runs server with developer settings.
-    --config=PATH, -c PATH  Configuration file path. Defaults to
+    --passphrase            Optional passphrase to use to decrypt the key-file.
-                            "${default_config.path}".
     --port=PORT, -p PORT    Port number to listen on, defaults to 3000.
     --port=PORT, -p PORT    Port number to listen on, defaults to 3000.
-    --sync-period           Number of milliseconds to wait between synchronization
+    --ws-provider WS_URL    Joystream-node websocket provider, defaults to ws://localhost:9944
-                            runs. Defaults to 30,000 (30s).
-    --key-file              JSON key export file to use as the storage provider.
-    --passphrase            Optional passphrase to use to decrypt the key-file (if its encrypted).
-    --public-url            API Public URL to announce. No URL will be announced if not specified.
-    --ws-provider           Joystream Node websocket provider url, eg: "ws://127.0.0.1:9944"
   `,
   `,
-  { flags: FLAG_DEFINITIONS });
+  { flags: FLAG_DEFINITIONS })
-
-// Create configuration
-function create_config(pkgname, flags)
-{
-  // Create defaults from flag definitions
-  const defaults = {};
-  for (var key in FLAG_DEFINITIONS) {
-    const defs = FLAG_DEFINITIONS[key];
-    if (defs._default) {
-      defaults[key] = defs._default;
-    }
-  }
-
-  // Provide flags as defaults. Anything stored in the config overrides.
-  var config = new configstore(pkgname, defaults, { configPath: flags.config });
-
-  // But we want the flags to also override what's stored in the config, so
-  // set them all.
-  for (var key in flags) {
-    // Skip aliases and self-referential config flag
-    if (key.length == 1 || key === 'config') continue;
-    // Skip sensitive flags
-    if (key == 'passphrase') continue;
-    // Skip unset flags
-    if (!flags[key]) continue;
-    // Otherwise set.
-    config.set(key, flags[key]);
-  }
-
-  debug('Configuration at', config.path, config.all);
-  return config;
-}
 
 
 // All-important banner!
 // All-important banner!
-function banner()
+function banner () {
-{
+  console.log(chalk.blue(figlet.textSync('joystream', 'Speed')))
-  console.log(chalk.blue(figlet.textSync('joystream', 'Speed')));
 }
 }
 
 
 function start_express_app(app, port) {
 function start_express_app(app, port) {
-  const http = require('http');
+  const http = require('http')
-  const server = http.createServer(app);
+  const server = http.createServer(app)
 
 
   return new Promise((resolve, reject) => {
   return new Promise((resolve, reject) => {
-    server.on('error', reject);
+    server.on('error', reject)
     server.on('close', (...args) => {
     server.on('close', (...args) => {
-      console.log('Server closed, shutting down...');
+      console.log('Server closed, shutting down...')
-      resolve(...args);
+      resolve(...args)
-    });
+    })
     server.on('listening', () => {
     server.on('listening', () => {
-      console.log('API server started.', server.address());
+      console.log('API server started.', server.address())
-    });
+    })
-    server.listen(port, '::');
+    server.listen(port, '::')
-    console.log('Starting API server...');
+    console.log('Starting API server...')
-  });
+  })
 }
 }
+
 // Start app
 // Start app
-function start_all_services(store, api, config)
+function start_all_services ({ store, api, port }) {
-{
+  const app = require('../lib/app')(PROJECT_ROOT, store, api) // reduce falgs to only needed values
-  const app = require('../lib/app')(PROJECT_ROOT, store, api, config);
+  return start_express_app(app, port)
-  const port = config.get('port');
-  return start_express_app(app, port);
 }
 }
 
 
-// Start discovery service app
+// Start discovery service app only
-function start_discovery_service(api, config)
+function start_discovery_service ({ api, port }) {
-{
+  const app = require('../lib/discovery')(PROJECT_ROOT, api) // reduce flags to only needed values
-  const app = require('../lib/discovery')(PROJECT_ROOT, api, config);
+  return start_express_app(app, port)
-  const port = config.get('port');
-  return start_express_app(app, port);
 }
 }
 
 
 // Get an initialized storage instance
 // Get an initialized storage instance
-function get_storage(runtime_api, config)
+function get_storage (runtime_api) {
-{
   // TODO at some point, we can figure out what backend-specific connection
   // TODO at some point, we can figure out what backend-specific connection
   // options make sense. For now, just don't use any configuration.
   // options make sense. For now, just don't use any configuration.
-  const { Storage } = require('@joystream/storage');
+  const { Storage } = require('@joystream/storage-node-backend')
 
 
   const options = {
   const options = {
     resolve_content_id: async (content_id) => {
     resolve_content_id: async (content_id) => {
       // Resolve via API
       // Resolve via API
-      const obj = await runtime_api.assets.getDataObject(content_id);
+      const obj = await runtime_api.assets.getDataObject(content_id)
       if (!obj || obj.isNone) {
       if (!obj || obj.isNone) {
-        return;
+        return
       }
       }
+      // if obj.liaison_judgement !== Accepted .. throw ?
+      return obj.unwrap().ipfs_content_id.toString()
+    }
+  }
 
 
-      return obj.unwrap().ipfs_content_id.toString();
+  return Storage.create(options)
-    },
-  };
-
-  return Storage.create(options);
 }
 }
 
 
-async function run_signup(account_file, provider_url)
+async function init_api_production ({ wsProvider, providerId, keyFile, passphrase }) {
-{
+  // Load key information
-  if (!account_file) {
+  const { RuntimeApi } = require('@joystream/storage-runtime-api')
-    console.log('Cannot proceed without keyfile');
-    return
-  }
 
 
-  const { RuntimeApi } = require('@joystream/runtime-api');
+  if (!keyFile) {
-  const api = await RuntimeApi.create({account_file, canPromptForPassphrase: true, provider_url});
+    throw new Error('Must specify a --key-file argument for running a storage node.')
+  }
 
 
-  if (!api.identities.key) {
+  if (providerId === undefined) {
-    console.log('Cannot proceed without a member account');
+    throw new Error('Must specify a --provider-id argument for running a storage node')
-    return
   }
   }
 
 
-  // Check there is an opening
+  const api = await RuntimeApi.create({
-  let availableSlots = await api.roles.availableSlotsForRole(api.roles.ROLE_STORAGE);
+    account_file: keyFile,
+    passphrase,
+    provider_url: wsProvider,
+    storageProviderId: providerId
+  })
 
 
-  if (availableSlots == 0) {
+  if (!api.identities.key) {
-    console.log(`
+    throw new Error('Failed to unlock storage provider account')
-      There are no open storage provider slots available at this time.
-      Please try again later.
-    `);
-    return;
-  } else {
-    console.log(`There are still ${availableSlots} slots available, proceeding`);
   }
   }
 
 
-  const member_address = api.identities.key.address;
+  if (!await api.workers.isRoleAccountOfStorageProvider(api.storageProviderId, api.identities.key.address)) {
-
+    throw new Error('storage provider role account and storageProviderId are not associated with a worker')
-  // Check if account works
-  const min = await api.roles.requiredBalanceForRoleStaking(api.roles.ROLE_STORAGE);
-  console.log(`Account needs to be a member and have a minimum balance of ${min.toString()}`);
-  const check = await api.roles.checkAccountForStaking(member_address);
-  if (check) {
-    console.log('Account is working for staking, proceeding.');
   }
   }
 
 
-  // Create a role key
+  return api
-  const role_key = await api.identities.createRoleKey(member_address);
-  const role_address = role_key.address;
-  console.log('Generated', role_address, '- this is going to be exported to a JSON file.\n',
-    ' You can provide an empty passphrase to make starting the server easier,\n',
-    ' but you must keep the file very safe, then.');
-  const filename = await api.identities.writeKeyPairExport(role_address);
-  console.log('Identity stored in', filename);
-
-  // Ok, transfer for staking.
-  await api.roles.transferForStaking(member_address, role_address, api.roles.ROLE_STORAGE);
-  console.log('Funds transferred.');
-
-  // Now apply for the role
-  await api.roles.applyForRole(role_address, api.roles.ROLE_STORAGE, member_address);
-  console.log('Role application sent.\nNow visit Roles > My Requests in the app.');
 }
 }
 
 
-async function wait_for_role(config)
+async function init_api_development () {
-{
   // Load key information
   // Load key information
-  const { RuntimeApi } = require('@joystream/runtime-api');
+  const { RuntimeApi } = require('@joystream/storage-runtime-api')
-  const keyFile = config.get('keyFile');
+
-  if (!keyFile) {
+  const wsProvider = 'ws://localhost:9944'
-    throw new Error("Must specify a key file for running a storage node! Sign up for the role; see `colussus --help' for details.");
-  }
-  const wsProvider = config.get('wsProvider');
 
 
   const api = await RuntimeApi.create({
   const api = await RuntimeApi.create({
-    account_file: keyFile,
+    provider_url: wsProvider
-    passphrase: cli.flags.passphrase,
+  })
-    provider_url: wsProvider,
-  });
 
 
-  if (!api.identities.key) {
+  const dev = require('../../cli/bin/dev')
-    throw new Error('Failed to unlock storage provider account');
+
-  }
+  api.identities.useKeyPair(dev.roleKeyPair(api))
 
 
-  // Wait for the account role to be finalized
+  api.storageProviderId = await dev.check(api)
-  console.log('Waiting for the account to be staked as a storage provider role...');
+
-  const result = await api.roles.waitForRole(api.identities.key.address, api.roles.ROLE_STORAGE);
+  return api
-  return [result, api];
 }
 }
 
 
-function get_service_information(config) {
+function get_service_information (publicUrl) {
   // For now assume we run all services on the same endpoint
   // For now assume we run all services on the same endpoint
   return({
   return({
     asset: {
     asset: {
       version: 1, // spec version
       version: 1, // spec version
-      endpoint: config.get('publicUrl')
+      endpoint: publicUrl
     },
     },
     discover: {
     discover: {
       version: 1, // spec version
       version: 1, // spec version
-      endpoint: config.get('publicUrl')
+      endpoint: publicUrl
     }
     }
   })
   })
 }
 }
 
 
-async function announce_public_url(api, config) {
+async function announce_public_url (api, publicUrl) {
   // re-announce in future
   // re-announce in future
   const reannounce = function (timeoutMs) {
   const reannounce = function (timeoutMs) {
-    setTimeout(announce_public_url, timeoutMs, api, config);
+    setTimeout(announce_public_url, timeoutMs, api, publicUrl)
   }
   }
 
 
   debug('announcing public url')
   debug('announcing public url')
-  const { publish } = require('@joystream/discovery')
+  const { publish } = require('@joystream/service-discovery')
-
-  const accountId = api.identities.key.address
 
 
   try {
   try {
-    const serviceInformation = get_service_information(config)
+    const serviceInformation = get_service_information(publicUrl)
 
 
-    let keyId = await publish.publish(serviceInformation);
+    let keyId = await publish.publish(serviceInformation)
 
 
-    const expiresInBlocks = 600; // ~ 1 hour (6s block interval)
+    await api.discovery.setAccountInfo(keyId)
-    await api.discovery.setAccountInfo(accountId, keyId, expiresInBlocks);
 
 
     debug('publishing complete, scheduling next update')
     debug('publishing complete, scheduling next update')
 
 
 // >> sometimes after tx is finalized.. we are not reaching here!
 // >> sometimes after tx is finalized.. we are not reaching here!
 
 
-    // Reannounce before expiery
+    // Reannounce before expiery. Here we are concerned primarily
-    reannounce(50 * 60 * 1000); // in 50 minutes
+    // with keeping the account information refreshed and 'available' in
-
+    // the ipfs network. our record on chain is valid for 24hr
+    reannounce(50 * 60 * 1000) // in 50 minutes
   } catch (err) {
   } catch (err) {
     debug(`announcing public url failed: ${err.stack}`)
     debug(`announcing public url failed: ${err.stack}`)
 
 
@@ -303,95 +230,74 @@ async function announce_public_url(api, config) {
   }
   }
 }
 }
 
 
-function go_offline(api) {
+function go_offline (api) {
-  return api.discovery.unsetAccountInfo(api.identities.key.address)
+  return api.discovery.unsetAccountInfo()
 }
 }
 
 
 // Simple CLI commands
 // Simple CLI commands
-var command = cli.input[0];
+var command = cli.input[0]
 if (!command) {
 if (!command) {
-  command = 'server';
+  command = 'server'
+}
+
+async function start_colossus ({ api, publicUrl, port, flags }) {
+  // TODO: check valid url, and valid port number
+  const store = get_storage(api)
+  banner()
+  const { start_syncing } = require('../lib/sync')
+  start_syncing(api, { syncPeriod: SYNC_PERIOD_MS }, store)
+  announce_public_url(api, publicUrl)
+  return start_all_services({ store, api, port, flags }) // dont pass all flags only required values
 }
 }
 
 
 const commands = {
 const commands = {
   'server': async () => {
   'server': async () => {
-    const cfg = create_config(pkg.name, cli.flags);
+    let publicUrl, port, api
-
+
-    // Load key information
+    if (cli.flags.dev) {
-    const values = await wait_for_role(cfg);
+      const dev = require('../../cli/bin/dev')
-    const result = values[0]
+      api = await init_api_development()
-    const api = values[1];
+      port = dev.developmentPort()
-    if (!result) {
+      publicUrl = `http://localhost:${port}/`
-      throw new Error(`Not staked as storage role.`);
+    } else {
-    }
+      api = await init_api_production(cli.flags)
-    console.log('Staked, proceeding.');
+      publicUrl = cli.flags.publicUrl
-
+      port = cli.flags.port
-    // Make sure a public URL is configured
-    if (!cfg.get('publicUrl')) {
-      throw new Error('publicUrl not configured')
     }
     }
 
 
-    // Continue with server setup
+    return start_colossus({ api, publicUrl, port })
-    const store = get_storage(api, cfg);
-    banner();
-
-    const { start_syncing } = require('../lib/sync');
-    start_syncing(api, cfg, store);
-
-    announce_public_url(api, cfg);
-    await start_all_services(store, api, cfg);
-  },
-  'signup': async (account_file) => {
-    const cfg = create_config(pkg.name, cli.flags);
-    await run_signup(account_file, cfg.get('wsProvider'));
-  },
-  'down': async () => {
-    const cfg = create_config(pkg.name, cli.flags);
-
-    const values = await wait_for_role(cfg);
-    const result = values[0]
-    const api = values[1];
-    if (!result) {
-      throw new Error(`Not staked as storage role.`);
-    }
-
-    await go_offline(api)
   },
   },
   'discovery': async () => {
   'discovery': async () => {
-    debug("Starting Joystream Discovery Service")
+    debug('Starting Joystream Discovery Service')
-    const { RuntimeApi } = require('@joystream/runtime-api')
+    const { RuntimeApi } = require('@joystream/storage-runtime-api')
-    const cfg = create_config(pkg.name, cli.flags)
+    const wsProvider = cli.flags.wsProvider
-    const wsProvider = cfg.get('wsProvider');
+    const api = await RuntimeApi.create({ provider_url: wsProvider })
-    const api = await RuntimeApi.create({ provider_url: wsProvider });
+    const port = cli.flags.port
-    await start_discovery_service(api, cfg)
+    await start_discovery_service({ api, port })
   }
   }
-};
+}
-
 
 
-async function main()
+async function main () {
-{
   // Simple CLI commands
   // Simple CLI commands
-  var command = cli.input[0];
+  var command = cli.input[0]
   if (!command) {
   if (!command) {
-    command = 'server';
+    command = 'server'
   }
   }
 
 
   if (commands.hasOwnProperty(command)) {
   if (commands.hasOwnProperty(command)) {
     // Command recognized
     // Command recognized
-    const args = _.clone(cli.input).slice(1);
+    const args = _.clone(cli.input).slice(1)
-    await commands[command](...args);
+    await commands[command](...args)
-  }
+  } else {
-  else {
+    throw new Error(`Command '${command}' not recognized, aborting!`)
-    throw new Error(`Command "${command}" not recognized, aborting!`);
   }
   }
 }
 }
 
 
 main()
 main()
   .then(() => {
   .then(() => {
-    console.log('Process exiting gracefully.');
+    process.exit(0)
-    process.exit(0);
   })
   })
   .catch((err) => {
   .catch((err) => {
-    console.error(chalk.red(err.stack));
+    console.error(chalk.red(err.stack))
-    process.exit(-1);
+    process.exit(-1)
-  });
+  })

+ 2 - 4
storage-node/packages/colossus/lib/app.js

@@ -32,11 +32,10 @@ const yaml = require('js-yaml');
 // Project requires
 // Project requires
 const validateResponses = require('./middleware/validate_responses');
 const validateResponses = require('./middleware/validate_responses');
 const fileUploads = require('./middleware/file_uploads');
 const fileUploads = require('./middleware/file_uploads');
-const pagination = require('@joystream/util/pagination');
+const pagination = require('@joystream/storage-utils/pagination');
-const storage = require('@joystream/storage');
 
 
 // Configure app
 // Configure app
-function create_app(project_root, storage, runtime, config)
+function create_app(project_root, storage, runtime)
 {
 {
   const app = express();
   const app = express();
   app.use(cors());
   app.use(cors());
@@ -60,7 +59,6 @@ function create_app(project_root, storage, runtime, config)
       'multipart/form-data': fileUploads
       'multipart/form-data': fileUploads
     },
     },
     dependencies: {
     dependencies: {
-      config: config,
       storage: storage,
       storage: storage,
       runtime: runtime,
       runtime: runtime,
     },
     },

+ 1 - 2
storage-node/packages/colossus/lib/discovery.js

@@ -33,7 +33,7 @@ const path = require('path');
 const validateResponses = require('./middleware/validate_responses');
 const validateResponses = require('./middleware/validate_responses');
 
 
 // Configure app
 // Configure app
-function create_app(project_root, runtime, config)
+function create_app(project_root, runtime)
 {
 {
   const app = express();
   const app = express();
   app.use(cors());
   app.use(cors());
@@ -56,7 +56,6 @@ function create_app(project_root, runtime, config)
     },
     },
     docsPath: '/swagger.json',
     docsPath: '/swagger.json',
     dependencies: {
     dependencies: {
-      config: config,
       runtime: runtime,
       runtime: runtime,
     },
     },
   });
   });

+ 23 - 17
storage-node/packages/colossus/lib/sync.js

@@ -20,20 +20,22 @@
 
 
 const debug = require('debug')('joystream:sync');
 const debug = require('debug')('joystream:sync');
 
 
-async function sync_callback(api, config, storage)
+async function sync_callback(api, storage) {
-{
-  debug('Starting sync run...');
-
   // The first step is to gather all data objects from chain.
   // The first step is to gather all data objects from chain.
   // TODO: in future, limit to a configured tranche
   // TODO: in future, limit to a configured tranche
   // FIXME this isn't actually on chain yet, so we'll fake it.
   // FIXME this isn't actually on chain yet, so we'll fake it.
   const knownContentIds = await api.assets.getKnownContentIds() || [];
   const knownContentIds = await api.assets.getKnownContentIds() || [];
 
 
-  const role_addr = api.identities.key.address;
+  const role_addr = api.identities.key.address
+  const providerId = api.storageProviderId
 
 
   // Iterate over all sync objects, and ensure they're synced.
   // Iterate over all sync objects, and ensure they're synced.
   const allChecks = knownContentIds.map(async (content_id) => {
   const allChecks = knownContentIds.map(async (content_id) => {
-    let { relationship, relationshipId } = await api.assets.getStorageRelationshipAndId(role_addr, content_id);
+    let { relationship, relationshipId } = await api.assets.getStorageRelationshipAndId(providerId, content_id);
+
+    // get the data object
+    // make sure the data object was Accepted by the liaison,
+    // don't just blindly attempt to fetch them
 
 
     let fileLocal;
     let fileLocal;
     try {
     try {
@@ -51,8 +53,11 @@ async function sync_callback(api, config, storage)
       try {
       try {
         await storage.synchronize(content_id);
         await storage.synchronize(content_id);
       } catch (err) {
       } catch (err) {
-        debug(err.message)
+        // duplicate logging
+        // debug(err.message)
+        return
       }
       }
+      // why are we returning, if we synced the file
       return;
       return;
     }
     }
 
 
@@ -60,8 +65,8 @@ async function sync_callback(api, config, storage)
       // create relationship
       // create relationship
       debug(`Creating new storage relationship for ${content_id.encode()}`);
       debug(`Creating new storage relationship for ${content_id.encode()}`);
       try {
       try {
-        relationshipId = await api.assets.createAndReturnStorageRelationship(role_addr, content_id);
+        relationshipId = await api.assets.createAndReturnStorageRelationship(role_addr, providerId, content_id);
-        await api.assets.toggleStorageRelationshipReady(role_addr, relationshipId, true);
+        await api.assets.toggleStorageRelationshipReady(role_addr, providerId, relationshipId, true);
       } catch (err) {
       } catch (err) {
         debug(`Error creating new storage relationship ${content_id.encode()}: ${err.stack}`);
         debug(`Error creating new storage relationship ${content_id.encode()}: ${err.stack}`);
         return;
         return;
@@ -70,7 +75,7 @@ async function sync_callback(api, config, storage)
       debug(`Updating storage relationship to ready for ${content_id.encode()}`);
       debug(`Updating storage relationship to ready for ${content_id.encode()}`);
       // update to ready. (Why would there be a relationship set to ready: false?)
       // update to ready. (Why would there be a relationship set to ready: false?)
       try {
       try {
-        await api.assets.toggleStorageRelationshipReady(role_addr, relationshipId, true);
+        await api.assets.toggleStorageRelationshipReady(role_addr, providerId, relationshipId, true);
       } catch(err) {
       } catch(err) {
         debug(`Error setting relationship ready ${content_id.encode()}: ${err.stack}`);
         debug(`Error setting relationship ready ${content_id.encode()}: ${err.stack}`);
       }
       }
@@ -81,26 +86,27 @@ async function sync_callback(api, config, storage)
   });
   });
 
 
 
 
-  await Promise.all(allChecks);
+  return Promise.all(allChecks);
-  debug('sync run complete');
 }
 }
 
 
 
 
-async function sync_periodic(api, config, storage)
+async function sync_periodic(api, flags, storage)
 {
 {
   try {
   try {
-    await sync_callback(api, config, storage);
+    debug('Starting sync run...')
+    await sync_callback(api, storage)
+    debug('sync run complete')
   } catch (err) {
   } catch (err) {
     debug(`Error in sync_periodic ${err.stack}`);
     debug(`Error in sync_periodic ${err.stack}`);
   }
   }
   // always try again
   // always try again
-  setTimeout(sync_periodic, config.get('syncPeriod'), api, config, storage);
+  setTimeout(sync_periodic, flags.syncPeriod, api, flags, storage);
 }
 }
 
 
 
 
-function start_syncing(api, config, storage)
+function start_syncing(api, flags, storage)
 {
 {
-  sync_periodic(api, config, storage);
+  sync_periodic(api, flags, storage);
 }
 }
 
 
 module.exports = {
 module.exports = {

+ 6 - 6
storage-node/packages/colossus/package.json

@@ -1,6 +1,7 @@
 {
 {
   "name": "@joystream/colossus",
   "name": "@joystream/colossus",
-  "version": "0.1.0",
+  "private": true,
+  "version": "0.2.0",
   "description": "Colossus - Joystream Storage Node",
   "description": "Colossus - Joystream Storage Node",
   "author": "Joystream",
   "author": "Joystream",
   "homepage": "https://github.com/Joystream/joystream",
   "homepage": "https://github.com/Joystream/joystream",
@@ -49,18 +50,17 @@
     "temp": "^0.9.0"
     "temp": "^0.9.0"
   },
   },
   "dependencies": {
   "dependencies": {
-    "@joystream/runtime-api": "^0.1.0",
+    "@joystream/storage-runtime-api": "^0.1.0",
-    "@joystream/storage": "^0.1.0",
+    "@joystream/storage-node-backend": "^0.1.0",
-    "@joystream/util": "^0.1.0",
+    "@joystream/storage-utils": "^0.1.0",
     "body-parser": "^1.19.0",
     "body-parser": "^1.19.0",
     "chalk": "^2.4.2",
     "chalk": "^2.4.2",
-    "configstore": "^4.0.0",
     "cors": "^2.8.5",
     "cors": "^2.8.5",
     "express-openapi": "^4.6.1",
     "express-openapi": "^4.6.1",
     "figlet": "^1.2.1",
     "figlet": "^1.2.1",
     "js-yaml": "^3.13.1",
     "js-yaml": "^3.13.1",
     "lodash": "^4.17.11",
     "lodash": "^4.17.11",
-    "meow": "^5.0.0",
+    "meow": "^7.0.1",
     "multer": "^1.4.1",
     "multer": "^1.4.1",
     "si-prefix": "^0.2.0"
     "si-prefix": "^0.2.0"
   }
   }

+ 13 - 15
storage-node/packages/colossus/paths/asset/v0/{id}.js

@@ -20,13 +20,10 @@
 
 
 const path = require('path');
 const path = require('path');
 
 
-const file_type = require('file-type');
+const debug = require('debug')('joystream:colossus:api:asset');
-const mime_types = require('mime-types');
 
 
-const debug = require('debug')('joystream:api:asset');
+const util_ranges = require('@joystream/storage-utils/ranges');
-
+const filter = require('@joystream/storage-node-backend/filter');
-const util_ranges = require('@joystream/util/ranges');
-const filter = require('@joystream/storage/filter');
 
 
 function error_handler(response, err, code)
 function error_handler(response, err, code)
 {
 {
@@ -35,7 +32,7 @@ function error_handler(response, err, code)
 }
 }
 
 
 
 
-module.exports = function(config, storage, runtime)
+module.exports = function(storage, runtime)
 {
 {
   var doc = {
   var doc = {
     // parameters for all operations in this path
     // parameters for all operations in this path
@@ -83,15 +80,16 @@ module.exports = function(config, storage, runtime)
     // Put for uploads
     // Put for uploads
     put: async function(req, res, _next)
     put: async function(req, res, _next)
     {
     {
-      const id = req.params.id;
+      const id = req.params.id; // content id
 
 
       // First check if we're the liaison for the name, otherwise we can bail
       // First check if we're the liaison for the name, otherwise we can bail
       // out already.
       // out already.
       const role_addr = runtime.identities.key.address;
       const role_addr = runtime.identities.key.address;
+      const providerId = runtime.storageProviderId;
       let dataObject;
       let dataObject;
       try {
       try {
         debug('calling checkLiaisonForDataObject')
         debug('calling checkLiaisonForDataObject')
-        dataObject = await runtime.assets.checkLiaisonForDataObject(role_addr, id);
+        dataObject = await runtime.assets.checkLiaisonForDataObject(providerId, id);
         debug('called checkLiaisonForDataObject')
         debug('called checkLiaisonForDataObject')
       } catch (err) {
       } catch (err) {
         error_handler(res, err, 403);
         error_handler(res, err, 403);
@@ -121,14 +119,14 @@ module.exports = function(config, storage, runtime)
             debug('Detected file info:', info);
             debug('Detected file info:', info);
 
 
             // Filter
             // Filter
-            const filter_result = filter(config, req.headers, info.mime_type);
+            const filter_result = filter({}, req.headers, info.mime_type);
             if (200 != filter_result.code) {
             if (200 != filter_result.code) {
               debug('Rejecting content', filter_result.message);
               debug('Rejecting content', filter_result.message);
               stream.end();
               stream.end();
               res.status(filter_result.code).send({ message: filter_result.message });
               res.status(filter_result.code).send({ message: filter_result.message });
 
 
               // Reject the content
               // Reject the content
-              await runtime.assets.rejectContent(role_addr, id);
+              await runtime.assets.rejectContent(role_addr, providerId, id);
               return;
               return;
             }
             }
             debug('Content accepted.');
             debug('Content accepted.');
@@ -155,20 +153,20 @@ module.exports = function(config, storage, runtime)
           try {
           try {
             if (hash !== dataObject.ipfs_content_id.toString()) {
             if (hash !== dataObject.ipfs_content_id.toString()) {
               debug('Rejecting content. IPFS hash does not match value in objectId');
               debug('Rejecting content. IPFS hash does not match value in objectId');
-              await runtime.assets.rejectContent(role_addr, id);
+              await runtime.assets.rejectContent(role_addr, providerId, id);
               res.status(400).send({ message: "Uploaded content doesn't match IPFS hash" });
               res.status(400).send({ message: "Uploaded content doesn't match IPFS hash" });
               return;
               return;
             }
             }
 
 
             debug('accepting Content')
             debug('accepting Content')
-            await runtime.assets.acceptContent(role_addr, id);
+            await runtime.assets.acceptContent(role_addr, providerId, id);
 
 
             debug('creating storage relationship for newly uploaded content')
             debug('creating storage relationship for newly uploaded content')
             // Create storage relationship and flip it to ready.
             // Create storage relationship and flip it to ready.
-            const dosr_id = await runtime.assets.createAndReturnStorageRelationship(role_addr, id);
+            const dosr_id = await runtime.assets.createAndReturnStorageRelationship(role_addr, providerId, id);
 
 
             debug('toggling storage relationship for newly uploaded content')
             debug('toggling storage relationship for newly uploaded content')
-            await runtime.assets.toggleStorageRelationshipReady(role_addr, dosr_id, true);
+            await runtime.assets.toggleStorageRelationshipReady(role_addr, providerId, dosr_id, true);
 
 
             debug('Sending OK response.');
             debug('Sending OK response.');
             res.status(200).send({ message: 'Asset uploaded.' });
             res.status(200).send({ message: 'Asset uploaded.' });

+ 12 - 7
storage-node/packages/colossus/paths/discover/v0/{id}.js

@@ -1,10 +1,10 @@
-const { discover } = require('@joystream/discovery')
+const { discover } = require('@joystream/service-discovery')
-const debug = require('debug')('joystream:api:discovery');
+const debug = require('debug')('joystream:colossus:api:discovery');
 
 
 const MAX_CACHE_AGE = 30 * 60 * 1000;
 const MAX_CACHE_AGE = 30 * 60 * 1000;
 const USE_CACHE = true;
 const USE_CACHE = true;
 
 
-module.exports = function(config, runtime)
+module.exports = function(runtime)
 {
 {
   var doc = {
   var doc = {
     // parameters for all operations in this path
     // parameters for all operations in this path
@@ -15,7 +15,7 @@ module.exports = function(config, runtime)
         required: true,
         required: true,
         description: 'Actor accouuntId',
         description: 'Actor accouuntId',
         schema: {
         schema: {
-          type: 'string',
+          type: 'string', // integer ?
         },
         },
       },
       },
     ],
     ],
@@ -23,7 +23,13 @@ module.exports = function(config, runtime)
     // Resolve Service Information
     // Resolve Service Information
     get: async function(req, res)
     get: async function(req, res)
     {
     {
-        const id = req.params.id;
+        try {
+          var parsedId = parseInt(req.params.id);
+        } catch (err) {
+          return res.status(400).end();
+        }
+
+        const id = parsedId
         let cacheMaxAge = req.query.max_age;
         let cacheMaxAge = req.query.max_age;
 
 
         if (cacheMaxAge) {
         if (cacheMaxAge) {
@@ -47,10 +53,9 @@ module.exports = function(config, runtime)
           } else {
           } else {
             res.status(200).send(info);
             res.status(200).send(info);
           }
           }
-
         } catch (err) {
         } catch (err) {
           debug(`${err}`);
           debug(`${err}`);
-          res.status(400).end()
+          res.status(404).end()
         }
         }
     }
     }
   };
   };

+ 0 - 68
storage-node/packages/discovery/IpfsResolver.js

@@ -1,68 +0,0 @@
-const IpfsClient = require('ipfs-http-client')
-const axios = require('axios')
-const { Resolver } = require('./Resolver')
-
-class IpfsResolver extends Resolver {
-    constructor({
-        host = 'localhost',
-        port,
-        mode = 'rpc', // rpc or gateway
-        protocol = 'http', // http or https
-        ipfs,
-        runtime
-    }) {
-        super({runtime})
-
-        if (ipfs) {
-            // use an existing ipfs client instance
-            this.ipfs = ipfs
-        } else if (mode == 'rpc') {
-            port = port || '5001'
-            this.ipfs = IpfsClient(host, port, { protocol })
-        } else if (mode === 'gateway') {
-            port = port || '8080'
-            this.gateway = this.constructUrl(protocol, host, port)
-        } else {
-            throw new Error('Invalid IPFS Resolver options')
-        }
-    }
-
-    async _resolveOverRpc(identity) {
-        const ipnsPath = `/ipns/${identity}/`
-
-        const ipfsName = await this.ipfs.name.resolve(ipnsPath, {
-            recursive: false, // there should only be one indirection to service info file
-            nocache: false,
-        })
-
-        const data = await this.ipfs.get(ipfsName)
-
-        // there should only be one file published under the resolved path
-        const content = data[0].content
-
-        return JSON.parse(content)
-    }
-
-    async _resolveOverGateway(identity) {
-        const url = `${this.gateway}/ipns/${identity}`
-
-        // expected JSON object response
-        const response = await axios.get(url)
-
-        return response.data
-    }
-
-    resolve(accountId) {
-        const identity = this.resolveIdentity(accountId)
-
-        if (this.ipfs) {
-            return this._resolveOverRpc(identity)
-        } else {
-            return this._resolveOverGateway(identity)
-        }
-    }
-}
-
-module.exports = {
-    IpfsResolver
-}

+ 0 - 28
storage-node/packages/discovery/JdsResolver.js

@@ -1,28 +0,0 @@
-const axios = require('axios')
-const { Resolver } = require('./Resolver')
-
-class JdsResolver extends Resolver {
-    constructor({
-        protocol = 'http', // http or https
-        host = 'localhost',
-        port,
-        runtime
-    }) {
-        super({runtime})
-
-        this.baseUrl = this.constructUrl(protocol, host, port)
-    }
-
-    async resolve(accountId) {
-        const url = `${this.baseUrl}/discover/v0/${accountId}`
-
-        // expected JSON object response
-        const response = await axios.get(url)
-
-        return response.data
-    }
-}
-
-module.exports = {
-    JdsResolver
-}

+ 11 - 21
storage-node/packages/discovery/README.md

@@ -1,29 +1,23 @@
 # Discovery
 # Discovery
 
 
-The `@joystream/discovery` package provides an API for role services to publish
+The `@joystream/service-discovery` package provides an API for role services to publish
 discovery information about themselves, and for consumers to resolve this
 discovery information about themselves, and for consumers to resolve this
 information.
 information.
 
 
 In the Joystream network, services are provided by having members stake for a
 In the Joystream network, services are provided by having members stake for a
-role. The role is identified by a unique actor key. Resolving service information
+role. The role is identified by a worker id. Resolving service information
-associated with the actor key is the main purpose of this module.
+associated with the worker id is the main purpose of this module.
 
 
 This implementation is based on [IPNS](https://docs.ipfs.io/guides/concepts/ipns/)
 This implementation is based on [IPNS](https://docs.ipfs.io/guides/concepts/ipns/)
 as well as runtime information.
 as well as runtime information.
 
 
 ## Discovery Workflow
 ## Discovery Workflow
 
 
-The discovery workflow provides an actor public key to the `discover()` function, which
+The discovery workflow provides worker id to the `discover()` function, which
 will eventually return structured data.
 will eventually return structured data.
 
 
-Clients can verify that the structured data has been signed by the identifying
+Under the hood, `discover()` the bootstrap nodes from the runtime are
-actor. This is normally done automatically, unless a `verify: false` option is
+used in a browser environment, or the local ipfs node otherwise.
-passed to `discover()`. Then, a separate `verify()` call can be used for
-verification.
-
-Under the hood, `discover()` uses any known participating node in the discovery
-network. If no other nodes are known, the bootstrap nodes from the runtime are
-used.
 
 
 There is a distinction in the discovery workflow:
 There is a distinction in the discovery workflow:
 
 
@@ -31,8 +25,8 @@ There is a distinction in the discovery workflow:
   is performed to discover nodes.
   is performed to discover nodes.
 2. If run in a node.js process, instead:
 2. If run in a node.js process, instead:
   - A trusted (local) IPFS node must be configured.
   - A trusted (local) IPFS node must be configured.
-  - The chain is queried to resolve an actor key to an IPNS peer ID.
+  - The chain is queried to resolve a worker id to an IPNS id.
-  - The trusted IPFS node is used to resolve the IPNS peer ID to an IPFS
+  - The trusted IPFS node is used to resolve the IPNS id to an IPFS
     file.
     file.
   - The IPFS file is fetched; this contains the structured data.
   - The IPFS file is fetched; this contains the structured data.
 
 
@@ -45,11 +39,10 @@ The publishing workflow is a little more involved, and requires more interaction
 with the runtime and the trusted IPFS node.
 with the runtime and the trusted IPFS node.
 
 
 1. A service information file is created.
 1. A service information file is created.
-1. The file is signed with the actor key (see below).
+1. The file is published on IPFS, using the IPNS self key of the local node.
-1. The file is published on IPFS.
 1. The IPNS name of the trusted IPFS node is updated to refer to the published
 1. The IPNS name of the trusted IPFS node is updated to refer to the published
    file.
    file.
-1. The runtime mapping from the actor ID to the IPNS name is updated.
+1. The runtime mapping from the worker ID to the IPNS name is updated.
 
 
 ## Published Information
 ## Published Information
 
 
@@ -57,10 +50,7 @@ Any JSON data can theoretically be published with this system; however, the
 following structure is currently imposed:
 following structure is currently imposed:
 
 
 - The JSON must be an Object at the top-level, not an Array.
 - The JSON must be an Object at the top-level, not an Array.
-- Each key must correspond to a service spec (below).
+- Each key must correspond to a [service spec](../../docs/json-signing/README.md).
-
-The data is signed using the [@joystream/json-signing](../json-signing/README.md)
-package.
 
 
 ## Service Info Specifications
 ## Service Info Specifications
 
 

+ 0 - 48
storage-node/packages/discovery/Resolver.js

@@ -1,48 +0,0 @@
-class Resolver {
-    constructor ({
-        runtime
-    }) {
-        this.runtime = runtime
-    }
-
-    constructUrl (protocol, host, port) {
-        port = port ? `:${port}` : ''
-        return `${protocol}:://${host}${port}`
-    }
-
-    async resolveServiceInformation(accountId) {
-        let isActor = await this.runtime.identities.isActor(accountId)
-
-        if (!isActor) {
-            throw new Error('Cannot discover non actor account service info')
-        }
-
-        const identity = await this.resolveIdentity(accountId)
-
-        if (identity == null) {
-            // dont waste time trying to resolve if no identity was found
-            throw new Error('no identity to resolve');
-        }
-
-        return this.resolve(accountId)
-    }
-
-    // lookup ipns identity from chain corresponding to accountId
-    // return null if no identity found or record is expired
-    async resolveIdentity(accountId) {
-        const info = await this.runtime.discovery.getAccountInfo(accountId)
-        return info ? info.identity.toString() : null
-    }
-}
-
-Resolver.Error = {};
-Resolver.Error.UnrecognizedProtocol = class UnrecognizedProtocol extends Error {
-    constructor(message) {
-        super(message);
-        this.name = 'UnrecognizedProtocol';
-    }
-}
-
-module.exports = {
-    Resolver
-}

+ 241 - 148
storage-node/packages/discovery/discover.js

@@ -1,182 +1,275 @@
 const axios = require('axios')
 const axios = require('axios')
-const debug = require('debug')('discovery::discover')
+const debug = require('debug')('joystream:discovery:discover')
-const stripEndingSlash = require('@joystream/util/stripEndingSlash')
+const stripEndingSlash = require('@joystream/storage-utils/stripEndingSlash')
 
 
 const ipfs = require('ipfs-http-client')('localhost', '5001', { protocol: 'http' })
 const ipfs = require('ipfs-http-client')('localhost', '5001', { protocol: 'http' })
-
+const BN = require('bn.js')
-function inBrowser() {
+const { newExternallyControlledPromise } = require('@joystream/storage-utils/externalPromise')
-    return typeof window !== 'undefined'
+
+/**
+ * Determines if code is running in a browser by testing for the global window object
+ */
+function inBrowser () {
+  return typeof window !== 'undefined'
 }
 }
 
 
-var activeDiscoveries = {};
+/**
-var accountInfoCache = {};
+ * Map storage-provider id to a Promise of a discovery result. The purpose
-const CACHE_TTL = 60 * 60 * 1000;
+ * is to avoid concurrent active discoveries for the same provider.
-
+ */
-async function getIpnsIdentity (actorAccountId, runtimeApi) {
+var activeDiscoveries = {}
-    // lookup ipns identity from chain corresponding to actorAccountId
+
-    const info = await runtimeApi.discovery.getAccountInfo(actorAccountId)
+/**
-
+ * Map of storage provider id to string
-    if (info == null) {
+ * Cache of past discovery lookup results
-        // no identity found on chain for account
+ */
-        return null
+var accountInfoCache = {}
-    } else {
+
-        return info.identity.toString()
+/**
-    }
+ * After what period of time a cached record is considered stale, and would
+ * trigger a re-discovery, but only if a query is made for the same provider.
+ */
+const CACHE_TTL = 60 * 60 * 1000
+
+/**
+ * Queries the ipns id (service key) of the storage provider from the blockchain.
+ * If the storage provider is not registered it will return null.
+ * @param {number | BN | u64} storageProviderId - the provider id to lookup
+ * @param { RuntimeApi } runtimeApi - api instance to query the chain
+ * @returns { Promise<string | null> } - ipns multiformat address
+ */
+async function getIpnsIdentity (storageProviderId, runtimeApi) {
+  storageProviderId = new BN(storageProviderId)
+  // lookup ipns identity from chain corresponding to storageProviderId
+  const info = await runtimeApi.discovery.getAccountInfo(storageProviderId)
+
+  if (info == null) {
+    // no identity found on chain for account
+    return null
+  } else {
+    return info.identity.toString()
+  }
 }
 }
 
 
-async function discover_over_ipfs_http_gateway(actorAccountId, runtimeApi, gateway) {
+/**
-    let isActor = await runtimeApi.identities.isActor(actorAccountId)
+ * Resolves provider id to its service information.
+ * Will use an IPFS HTTP gateway. If caller doesn't provide a url the default gateway on
+ * the local ipfs node will be used.
+ * If the storage provider is not registered it will throw an error
+ * @param {number | BN | u64} storageProviderId - the provider id to lookup
+ * @param {RuntimeApi} runtimeApi - api instance to query the chain
+ * @param {string} gateway - optional ipfs http gateway url to perform ipfs queries
+ * @returns { Promise<object> } - the published service information
+ */
+async function discover_over_ipfs_http_gateway (
+  storageProviderId, runtimeApi, gateway = 'http://localhost:8080') {
 
 
-    if (!isActor) {
+  storageProviderId = new BN(storageProviderId)
-        throw new Error('Cannot discover non actor account service info')
+  let isProvider = await runtimeApi.workers.isStorageProvider(storageProviderId)
-    }
 
 
-    const identity = await getIpnsIdentity(actorAccountId, runtimeApi)
+  if (!isProvider) {
+    throw new Error('Cannot discover non storage providers')
+  }
 
 
-    gateway = gateway || 'http://localhost:8080'
+  const identity = await getIpnsIdentity(storageProviderId, runtimeApi)
 
 
-    const url = `${gateway}/ipns/${identity}`
+  if (identity == null) {
+    // dont waste time trying to resolve if no identity was found
+    throw new Error('no identity to resolve')
+  }
 
 
-    const response = await axios.get(url)
+  gateway = stripEndingSlash(gateway)
 
 
-    return response.data
+  const url = `${gateway}/ipns/${identity}`
-}
 
 
-async function discover_over_joystream_discovery_service(actorAccountId, runtimeApi, discoverApiEndpoint) {
+  const response = await axios.get(url)
-    let isActor = await runtimeApi.identities.isActor(actorAccountId)
 
 
-    if (!isActor) {
+  return response.data
-        throw new Error('Cannot discover non actor account service info')
+}
-    }
-
-    const identity = await getIpnsIdentity(actorAccountId, runtimeApi)
-
-    if (identity == null) {
-        // dont waste time trying to resolve if no identity was found
-        throw new Error('no identity to resolve');
-    }
-
-    if (!discoverApiEndpoint) {
-        // Use bootstrap nodes
-        let discoveryBootstrapNodes = await runtimeApi.discovery.getBootstrapEndpoints()
 
 
-        if (discoveryBootstrapNodes.length) {
+/**
-            discoverApiEndpoint = stripEndingSlash(discoveryBootstrapNodes[0].toString())
+ * Resolves id of provider to its service information.
-        } else {
+ * Will use the provided colossus discovery api endpoint. If no api endpoint
-            throw new Error('No known discovery bootstrap nodes found on network');
+ * is provided it attempts to use the configured endpoints from the chain.
-        }
+ * If the storage provider is not registered it will throw an error
+ * @param {number | BN | u64 } storageProviderId - provider id to lookup
+ * @param {RuntimeApi} runtimeApi - api instance to query the chain
+ * @param {string} discoverApiEndpoint - url for a colossus discovery api endpoint
+ * @returns { Promise<object> } - the published service information
+ */
+async function discover_over_joystream_discovery_service (storageProviderId, runtimeApi, discoverApiEndpoint) {
+  storageProviderId = new BN(storageProviderId)
+  let isProvider = await runtimeApi.workers.isStorageProvider(storageProviderId)
+
+  if (!isProvider) {
+    throw new Error('Cannot discover non storage providers')
+  }
+
+  const identity = await getIpnsIdentity(storageProviderId, runtimeApi)
+
+  // dont waste time trying to resolve if no identity was found
+  if (identity == null) {
+    throw new Error('no identity to resolve')
+  }
+
+  if (!discoverApiEndpoint) {
+    // Use bootstrap nodes
+    let discoveryBootstrapNodes = await runtimeApi.discovery.getBootstrapEndpoints()
+
+    if (discoveryBootstrapNodes.length) {
+      discoverApiEndpoint = stripEndingSlash(discoveryBootstrapNodes[0].toString())
+    } else {
+      throw new Error('No known discovery bootstrap nodes found on network')
     }
     }
+  }
 
 
-    const url = `${discoverApiEndpoint}/discover/v0/${actorAccountId}`
+  const url = `${discoverApiEndpoint}/discover/v0/${storageProviderId.toNumber()}`
 
 
-    // should have parsed if data was json?
+  // should have parsed if data was json?
-    const response = await axios.get(url)
+  const response = await axios.get(url)
 
 
-    return response.data
+  return response.data
 }
 }
 
 
-async function discover_over_local_ipfs_node(actorAccountId, runtimeApi) {
+/**
-    let isActor = await runtimeApi.identities.isActor(actorAccountId)
+ * Resolves id of provider to its service information.
+ * Will use the local IPFS node over RPC interface.
+ * If the storage provider is not registered it will throw an error.
+ * @param {number | BN | u64 } storageProviderId - provider id to lookup
+ * @param {RuntimeApi} runtimeApi - api instance to query the chain
+ * @returns { Promise<object> } - the published service information
+ */
+async function discover_over_local_ipfs_node (storageProviderId, runtimeApi) {
+  storageProviderId = new BN(storageProviderId)
+  let isProvider = await runtimeApi.workers.isStorageProvider(storageProviderId)
+
+  if (!isProvider) {
+    throw new Error('Cannot discover non storage providers')
+  }
+
+  const identity = await getIpnsIdentity(storageProviderId, runtimeApi)
+
+  if (identity == null) {
+    // dont waste time trying to resolve if no identity was found
+    throw new Error('no identity to resolve')
+  }
+
+  const ipns_address = `/ipns/${identity}/`
+
+  debug('resolved ipns to ipfs object')
+  // Can this call hang forever!? can/should we set a timeout?
+  let ipfs_name = await ipfs.name.resolve(ipns_address, {
+    // don't recurse, there should only be one indirection to the service info file
+    recursive: false,
+    nocache: false
+  })
+
+  debug('getting ipfs object', ipfs_name)
+  let data = await ipfs.get(ipfs_name) // this can sometimes hang forever!?! can we set a timeout?
+
+  // there should only be one file published under the resolved path
+  let content = data[0].content
+
+  return JSON.parse(content)
+}
 
 
-    if (!isActor) {
+/**
-        throw new Error('Cannot discover non actor account service info')
+ * Cached discovery of storage provider service information. If useCachedValue is
+ * set to true, will always return the cached result if found. New discovery will be triggered
+ * if record is found to be stale. If a stale record is not desired (CACHE_TTL old) pass a non zero
+ * value for maxCacheAge, which will force a new discovery and return the new resolved value.
+ * This method in turn calls _discovery which handles concurrent discoveries and selects the appropriate
+ * protocol to perform the query.
+ * If the storage provider is not registered it will resolve to null
+ * @param {number | BN | u64} storageProviderId - provider to discover
+ * @param {RuntimeApi} runtimeApi - api instance to query the chain
+ * @param {bool} useCachedValue - optionaly use chached queries
+ * @param {number} maxCacheAge - maximum age of a cached query that triggers automatic re-discovery
+ * @returns { Promise<object | null> } - the published service information
+ */
+async function discover (storageProviderId, runtimeApi, useCachedValue = false, maxCacheAge = 0) {
+  storageProviderId = new BN(storageProviderId)
+  const id = storageProviderId.toNumber()
+  const cached = accountInfoCache[id]
+
+  if (cached && useCachedValue) {
+    if (maxCacheAge > 0) {
+      // get latest value
+      if (Date.now() > (cached.updated + maxCacheAge)) {
+        return _discover(storageProviderId, runtimeApi)
+      }
     }
     }
-
+    // refresh if cache if stale, new value returned on next cached query
-    const identity = await getIpnsIdentity(actorAccountId, runtimeApi)
+    if (Date.now() > (cached.updated + CACHE_TTL)) {
-
+      _discover(storageProviderId, runtimeApi)
-    const ipns_address = `/ipns/${identity}/`
+    }
-
+    // return best known value
-    debug('resolved ipns to ipfs object')
+    return cached.value
-    let ipfs_name = await ipfs.name.resolve(ipns_address, {
+  } else {
-        recursive: false, // there should only be one indirection to service info file
+    return _discover(storageProviderId, runtimeApi)
-        nocache: false,
+  }
-    }) // this can hang forever!? can we set a timeout?
-
-    debug('getting ipfs object', ipfs_name)
-    let data = await ipfs.get(ipfs_name) // this can sometimes hang forever!?! can we set a timeout?
-
-    // there should only be one file published under the resolved path
-    let content = data[0].content
-
-    // verify information and if 'discovery' service found
-    // add it to our list of bootstrap nodes
-
-    // TODO cache result or flag
-    return JSON.parse(content)
 }
 }
 
 
-async function discover (actorAccountId, runtimeApi, useCachedValue = false, maxCacheAge = 0) {
+/**
-    const id = actorAccountId.toString();
+ * Internal method that handles concurrent discoveries and caching of results. Will
-    const cached = accountInfoCache[id];
+ * select the appropriate discovery protocol based on wether we are in a browser environemtn or not.
-
+ * If not in a browser it expects a local ipfs node to be running.
-    if (cached && useCachedValue) {
+ * @param {number | BN | u64} storageProviderId
-        if (maxCacheAge > 0) {
+ * @param {RuntimeApi} runtimeApi - api instance for querying the chain
-            // get latest value
+ * @returns { Promise<object | null> } - the published service information
-            if (Date.now() > (cached.updated + maxCacheAge)) {
+ */
-                return _discover(actorAccountId, runtimeApi);
+async function _discover (storageProviderId, runtimeApi) {
-            }
+  storageProviderId = new BN(storageProviderId)
-        }
+  const id = storageProviderId.toNumber()
-        // refresh if cache is stale, new value returned on next cached query
+
-        if (Date.now() > (cached.updated + CACHE_TTL)) {
+  const discoveryResult = activeDiscoveries[id]
-            _discover(actorAccountId, runtimeApi);
+  if (discoveryResult) {
-        }
+    debug('discovery in progress waiting for result for', id)
-        // return best known value
+    return discoveryResult
-        return cached.value;
+  }
+
+  debug('starting new discovery for', id)
+  const deferredDiscovery = newExternallyControlledPromise()
+  activeDiscoveries[id] = deferredDiscovery.promise
+
+  let result
+  try {
+    if (inBrowser()) {
+      result = await discover_over_joystream_discovery_service(storageProviderId, runtimeApi)
     } else {
     } else {
-        return _discover(actorAccountId, runtimeApi);
+      result = await discover_over_local_ipfs_node(storageProviderId, runtimeApi)
     }
     }
-}
-
-function createExternallyControlledPromise() {
-    let resolve, reject;
-    const promise = new Promise((_resolve, _reject) => {
-        resolve = _resolve;
-        reject = _reject;
-    });
-    return ({ resolve, reject, promise });
-}
 
 
-async function _discover(actorAccountId, runtimeApi) {
+    debug(result)
-    const id = actorAccountId.toString();
+    result = JSON.stringify(result)
-
+    accountInfoCache[id] = {
-    const discoveryResult = activeDiscoveries[id];
+      value: result,
-    if (discoveryResult) {
+      updated: Date.now()
-        debug('discovery in progress waiting for result for',id);
-        return discoveryResult
     }
     }
 
 
-    debug('starting new discovery for', id);
+    deferredDiscovery.resolve(result)
-    const deferredDiscovery = createExternallyControlledPromise();
+    delete activeDiscoveries[id]
-    activeDiscoveries[id] = deferredDiscovery.promise;
+    return result
-
+  } catch (err) {
-    let result;
+    // we catch the error so we can update all callers
-    try {
+    // and throw again to inform the first caller.
-        if (inBrowser()) {
+    debug(err.message)
-            result = await discover_over_joystream_discovery_service(actorAccountId, runtimeApi)
+    delete activeDiscoveries[id]
-        } else {
+    // deferredDiscovery.reject(err)
-            result = await discover_over_local_ipfs_node(actorAccountId, runtimeApi)
+    deferredDiscovery.resolve(null) // resolve to null until we figure out the issue below
-        }
+    // throw err // <-- throwing but this isn't being
-        debug(result)
+    // caught correctly in express server! Is it because there is an uncaught promise somewhere
-        result = JSON.stringify(result)
+    // in the prior .reject() call ?
-        accountInfoCache[id] = {
+    // I've only seen this behaviour when error is from ipfs-client
-            value: result,
+    // ... is this unique to errors thrown from ipfs-client?
-            updated: Date.now()
+    // Problem is its crashing the node so just return null for now
-        };
+    return null
-
+  }
-        deferredDiscovery.resolve(result);
-        delete activeDiscoveries[id];
-        return result;
-    } catch (err) {
-        debug(err.message);
-        deferredDiscovery.reject(err);
-        delete activeDiscoveries[id];
-        throw err;
-    }
 }
 }
 
 
 module.exports = {
 module.exports = {
-    discover,
+  discover,
-    discover_over_joystream_discovery_service,
+  discover_over_joystream_discovery_service,
-    discover_over_ipfs_http_gateway,
+  discover_over_ipfs_http_gateway,
-    discover_over_local_ipfs_node,
+  discover_over_local_ipfs_node
-}
+}

+ 13 - 7
storage-node/packages/discovery/example.js

@@ -1,14 +1,18 @@
-const { RuntimeApi } = require('@joystream/runtime-api')
+const { RuntimeApi } = require('@joystream/storage-runtime-api')
 
 
 const { discover, publish } = require('./')
 const { discover, publish } = require('./')
 
 
 async function main() {
 async function main() {
+    // The assigned storage-provider id
+    const provider_id = 0
+
     const runtimeApi = await RuntimeApi.create({
     const runtimeApi = await RuntimeApi.create({
-        account_file: "/Users/mokhtar/Downloads/5Gn9n7SDJ7VgHqHQWYzkSA4vX6DCmS5TFWdHxikTXp9b4L32.json"
+        // Path to the role account key file of the provider
+        account_file: "/path/to/role_account_key_file.json",
+        storageProviderId: provider_id
     })
     })
 
 
-    let published = await publish.publish(
+    let ipns_id = await publish.publish(
-        "5Gn9n7SDJ7VgHqHQWYzkSA4vX6DCmS5TFWdHxikTXp9b4L32",
         {
         {
             asset: {
             asset: {
                 version: 1,
                 version: 1,
@@ -18,11 +22,13 @@ async function main() {
         runtimeApi
         runtimeApi
     )
     )
 
 
-    console.log(published)
+    console.log(ipns_id)
+
+    // register ipns_id on chain
+    await runtimeApi.setAccountInfo(ipfs_id)
 
 
-    // let serviceInfo = await discover('5Gn9n7SDJ7VgHqHQWYzkSA4vX6DCmS5TFWdHxikTXp9b4L32', { runtimeApi })
     let serviceInfo = await discover.discover(
     let serviceInfo = await discover.discover(
-        '5Gn9n7SDJ7VgHqHQWYzkSA4vX6DCmS5TFWdHxikTXp9b4L32',
+        provider_id,
         runtimeApi
         runtimeApi
     )
     )
 
 

+ 4 - 3
storage-node/packages/discovery/package.json

@@ -1,5 +1,6 @@
 {
 {
-  "name": "@joystream/discovery",
+  "name": "@joystream/service-discovery",
+  "private": true,
   "version": "0.1.0",
   "version": "0.1.0",
   "description": "Service Discovery - Joystream Storage Node",
   "description": "Service Discovery - Joystream Storage Node",
   "author": "Joystream",
   "author": "Joystream",
@@ -43,8 +44,8 @@
     "temp": "^0.9.0"
     "temp": "^0.9.0"
   },
   },
   "dependencies": {
   "dependencies": {
-    "@joystream/runtime-api": "^0.1.0",
+    "@joystream/storage-runtime-api": "^0.1.0",
-    "@joystream/util": "^0.1.0",
+    "@joystream/storage-utils": "^0.1.0",
     "async-lock": "^1.2.0",
     "async-lock": "^1.2.0",
     "axios": "^0.18.0",
     "axios": "^0.18.0",
     "chalk": "^2.4.2",
     "chalk": "^2.4.2",

+ 71 - 37
storage-node/packages/discovery/publish.js

@@ -1,53 +1,87 @@
 const ipfsClient = require('ipfs-http-client')
 const ipfsClient = require('ipfs-http-client')
 const ipfs = ipfsClient('localhost', '5001', { protocol: 'http' })
 const ipfs = ipfsClient('localhost', '5001', { protocol: 'http' })
 
 
-const debug = require('debug')('discovery::publish')
+const debug = require('debug')('joystream:discovery:publish')
 
 
-const PUBLISH_KEY = 'self'; // 'services';
+/**
+ * The name of the key used for publishing. We use same key used by the ipfs node
+ * for the network identitiy, to make it possible to identify the ipfs node of the storage
+ * provider and use `ipfs ping` to check on the uptime of a particular node.
+ */
+const PUBLISH_KEY = 'self'
 
 
-function bufferFrom(data) {
+/**
-    return Buffer.from(JSON.stringify(data), 'utf-8')
+ * Applies JSON serialization on the data object and converts the utf-8
+ * string to a Buffer.
+ * @param {object} data - json object
+ * @returns {Buffer}
+ */
+function bufferFrom (data) {
+  return Buffer.from(JSON.stringify(data), 'utf-8')
 }
 }
 
 
-function encodeServiceInfo(info) {
+/**
-    return bufferFrom({
+ * Encodes the service info into a standard format see. /storage-node/docs/json-signing.md
-        serialized: JSON.stringify(info),
+ * To be able to add a signature over the json data. Signing is not currently implemented.
-        // signature: ''
+ * @param {object} info - json object
-    })
+ * @returns {Buffer}
+ */
+function encodeServiceInfo (info) {
+  return bufferFrom({
+    serialized: JSON.stringify(info)
+  })
 }
 }
 
 
+/**
+ * Publishes the service information, encoded using the standard defined in encodeServiceInfo()
+ * to ipfs, using the local ipfs node's PUBLISH_KEY, and returns the key id used to publish.
+ * What we refer to as the ipns id.
+ * @param {object} service_info - the service information to publish
+ * @returns {string} - the ipns id
+ */
 async function publish (service_info) {
 async function publish (service_info) {
-    const keys = await ipfs.key.list()
+  const keys = await ipfs.key.list()
-    let services_key = keys.find((key) => key.name === PUBLISH_KEY)
+  let services_key = keys.find((key) => key.name === PUBLISH_KEY)
-
+
-    // generate a new services key if not found
+  // An ipfs node will always have the self key.
-    if (PUBLISH_KEY !== 'self' && !services_key) {
+  // If the publish key is specified as anything else and it doesn't exist
-        debug('generating ipns services key')
+  // we create it.
-        services_key = await ipfs.key.gen(PUBLISH_KEY, {
+  if (PUBLISH_KEY !== 'self' && !services_key) {
-          type: 'rsa',
+    debug('generating ipns services key')
-          size: 2048
+    services_key = await ipfs.key.gen(PUBLISH_KEY, {
-        });
+      type: 'rsa',
-    }
+      size: 2048
-
-    if (!services_key) {
-        throw new Error('No IPFS publishing key available!')
-    }
-
-    debug('adding service info file to node')
-    const files = await ipfs.add(encodeServiceInfo(service_info))
-
-    debug('publishing...')
-    const published = await ipfs.name.publish(files[0].hash, {
-        key: PUBLISH_KEY,
-        resolve: false,
-        // lifetime: // string - Time duration of the record. Default: 24h
-        // ttl:      // string - Time duration this record should be cached
     })
     })
+  }
+
+  if (!services_key) {
+    throw new Error('No IPFS publishing key available!')
+  }
+
+  debug('adding service info file to node')
+  const files = await ipfs.add(encodeServiceInfo(service_info))
+
+  debug('publishing...')
+  const published = await ipfs.name.publish(files[0].hash, {
+    key: PUBLISH_KEY,
+    resolve: false
+    // lifetime: // string - Time duration of the record. Default: 24h
+    // ttl:      // string - Time duration this record should be cached
+  })
+
+  // The name and ipfs hash of the published service information file, eg.
+  // {
+  //   name: 'QmUNQCkaU1TRnc1WGixqEP3Q3fazM8guSdFRsdnSJTN36A',
+  //   value: '/ipfs/QmcSjtVMfDSSNYCxNAb9PxNpEigCw7h1UZ77gip3ghfbnA'
+  // }
+  // .. The name is equivalent to the key id that was used.
+  debug(published)
 
 
-    debug(published)
+  // Return the key id under which the content was published. Which is used
-    return services_key.id;
+  // to lookup the actual ipfs content id of the published service information
+  return services_key.id
 }
 }
 
 
 module.exports = {
 module.exports = {
-    publish
+  publish
 }
 }

+ 1 - 2
storage-node/packages/helios/README.md

@@ -6,7 +6,6 @@ A basic tool to scan the joystream storage network to get a birds eye view of th
 ## Scanning
 ## Scanning
 
 
 ```
 ```
-yarn
+yarn helios
-yarn run helios
 ```
 ```
 
 

+ 105 - 88
storage-node/packages/helios/bin/cli.js

@@ -1,125 +1,127 @@
 #!/usr/bin/env node
 #!/usr/bin/env node
 
 
-const { RuntimeApi } = require('@joystream/runtime-api');
+const { RuntimeApi } = require('@joystream/storage-runtime-api')
 const { encodeAddress } = require('@polkadot/keyring')
 const { encodeAddress } = require('@polkadot/keyring')
-const { discover } = require('@joystream/discovery');
+const { discover } = require('@joystream/service-discovery')
-const axios = require('axios');
+const axios = require('axios')
-const stripEndingSlash = require('@joystream/util/stripEndingSlash');
+const stripEndingSlash = require('@joystream/storage-utils/stripEndingSlash')
 
 
-(async function main () {
+async function main () {
-
+  const runtime = await RuntimeApi.create()
-  const runtime = await RuntimeApi.create();
+  const { api } = runtime
-  const api  = runtime.api;
 
 
   // get current blockheight
   // get current blockheight
-  const currentHeader = await api.rpc.chain.getHeader();
+  const currentHeader = await api.rpc.chain.getHeader()
-  const currentHeight = currentHeader.number.toBn();
+  const currentHeight = currentHeader.number.toBn()
 
 
   // get all providers
   // get all providers
-  const storageProviders = await api.query.actors.accountIdsByRole(0);
+  const { ids: storageProviders } = await runtime.workers.getAllProviders()
+  console.log(`Found ${storageProviders.length} staked providers`)
 
 
-  const storageProviderAccountInfos = await Promise.all(storageProviders.map(async (account) => {
+  const storageProviderAccountInfos = await Promise.all(storageProviders.map(async (providerId) => {
     return ({
     return ({
-      account,
+      providerId,
-      info: await runtime.discovery.getAccountInfo(account),
+      info: await runtime.discovery.getAccountInfo(providerId)
-      joined: (await api.query.actors.actorByAccountId(account)).unwrap().joined_at
+    })
-    });
+  }))
-  }));
 
 
-  const liveProviders = storageProviderAccountInfos.filter(({account, info}) => {
+  // providers that have updated their account info and published ipfs id
+  // considered live if the record hasn't expired yet
+  const liveProviders = storageProviderAccountInfos.filter(({info}) => {
     return info && info.expires_at.gte(currentHeight)
     return info && info.expires_at.gte(currentHeight)
-  });
+  })
 
 
-  const downProviders = storageProviderAccountInfos.filter(({account, info}) => {
+  const downProviders = storageProviderAccountInfos.filter(({info}) => {
     return info == null
     return info == null
-  });
+  })
 
 
-  const expiredTtlProviders = storageProviderAccountInfos.filter(({account, info}) => {
+  const expiredTtlProviders = storageProviderAccountInfos.filter(({info}) => {
     return info && currentHeight.gte(info.expires_at)
     return info && currentHeight.gte(info.expires_at)
-  });
+  })
 
 
-  let providersStatuses = mapInfoToStatus(liveProviders, currentHeight);
+  let providersStatuses = mapInfoToStatus(liveProviders, currentHeight)
-  console.log('\n== Live Providers\n', providersStatuses);
+  console.log('\n== Live Providers\n', providersStatuses)
 
 
   let expiredProviderStatuses = mapInfoToStatus(expiredTtlProviders, currentHeight)
   let expiredProviderStatuses = mapInfoToStatus(expiredTtlProviders, currentHeight)
-  console.log('\n== Expired Providers\n', expiredProviderStatuses);
+  console.log('\n== Expired Providers\n', expiredProviderStatuses)
 
 
-  // check when actor account was created consider grace period before removing
   console.log('\n== Down Providers!\n', downProviders.map(provider => {
   console.log('\n== Down Providers!\n', downProviders.map(provider => {
     return ({
     return ({
-      account: provider.account.toString(),
+      providerId: provider.providerId
-      age: currentHeight.sub(provider.joined).toNumber()
     })
     })
-  }));
+  }))
 
 
   // Resolve IPNS identities of providers
   // Resolve IPNS identities of providers
   console.log('\nResolving live provider API Endpoints...')
   console.log('\nResolving live provider API Endpoints...')
-  //providersStatuses = providersStatuses.concat(expiredProviderStatuses);
+  let endpoints = await Promise.all(providersStatuses.map(async ({providerId}) => {
-  let endpoints = await Promise.all(providersStatuses.map(async (status) => {
     try {
     try {
-      let serviceInfo = await discover.discover_over_joystream_discovery_service(status.address, runtime);
+      let serviceInfo = await discover.discover_over_joystream_discovery_service(providerId, runtime)
-      let info = JSON.parse(serviceInfo.serialized);
+
-      console.log(`${status.address} -> ${info.asset.endpoint}`);
+      if (serviceInfo == null) {
-      return { address: status.address, endpoint: info.asset.endpoint};
+        console.log(`provider ${providerId} has not published service information`)
+        return { providerId, endpoint: null }
+      }
+
+      let info = JSON.parse(serviceInfo.serialized)
+      console.log(`${providerId} -> ${info.asset.endpoint}`)
+      return { providerId, endpoint: info.asset.endpoint }
     } catch (err) {
     } catch (err) {
-      console.log('resolve failed', status.address, err.message);
+      console.log('resolve failed for id', providerId, err.message)
-      return { address: status.address, endpoint: null};
+      return { providerId, endpoint: null }
     }
     }
-  }));
+  }))
 
 
-  console.log('\nChecking API Endpoint is online')
+  console.log('\nChecking API Endpoints are online')
   await Promise.all(endpoints.map(async (provider) => {
   await Promise.all(endpoints.map(async (provider) => {
     if (!provider.endpoint) {
     if (!provider.endpoint) {
-      console.log('skipping', provider.address);
+      console.log('skipping', provider.address)
       return
       return
     }
     }
-    const swaggerUrl = `${stripEndingSlash(provider.endpoint)}/swagger.json`;
+    const swaggerUrl = `${stripEndingSlash(provider.endpoint)}/swagger.json`
-    let error;
+    let error
     try {
     try {
       await axios.get(swaggerUrl)
       await axios.get(swaggerUrl)
-    } catch (err) {error = err}
+      // maybe print out api version information to detect which version of colossus is running?
-    console.log(`${provider.endpoint} - ${error ? error.message : 'OK'}`);
+      // or add anothe api endpoint for diagnostics information
-  }));
+    } catch (err) { error = err }
+    console.log(`${provider.endpoint} - ${error ? error.message : 'OK'}`)
+  }))
 
 
-  // after resolving for each resolved provider, HTTP HEAD with axios all known content ids
-  // report available/known
   let knownContentIds = await runtime.assets.getKnownContentIds()
   let knownContentIds = await runtime.assets.getKnownContentIds()
+  console.log(`\nData Directory has ${knownContentIds.length} assets`)
 
 
-  console.log(`\nContent Directory has ${knownContentIds.length} assets`);
+  // Check which providers are reporting a ready relationship for each asset
-
   await Promise.all(knownContentIds.map(async (contentId) => {
   await Promise.all(knownContentIds.map(async (contentId) => {
-    let [relationships, judgement] = await assetRelationshipState(api, contentId, storageProviders);
+    let [relationshipsCount, judgement] = await assetRelationshipState(api, contentId, storageProviders)
-    console.log(`${encodeAddress(contentId)} replication ${relationships}/${storageProviders.length} - ${judgement}`);
+    console.log(`${encodeAddress(contentId)} replication ${relationshipsCount}/${storageProviders.length} - ${judgement}`)
-  }));
+  }))
-
-  console.log('\nChecking available assets on providers...');
-
-  endpoints.map(async ({address, endpoint}) => {
-    if (!endpoint) { return }
-    let { found, content } = await countContentAvailability(knownContentIds, endpoint);
-    console.log(`${address}: has ${found} assets`);
-    return content
-  });
-
 
 
   // interesting disconnect doesn't work unless an explicit provider was created
   // interesting disconnect doesn't work unless an explicit provider was created
   // for underlying api instance
   // for underlying api instance
-  runtime.api.disconnect();
+  // We no longer need a connection to the chain
-})();
+  api.disconnect()
+
+  console.log(`\nChecking available assets on providers (this can take some time)...`)
+  endpoints.forEach(async ({ providerId, endpoint }) => {
+    if (!endpoint) { return }
+    const total = knownContentIds.length
+    let { found } = await countContentAvailability(knownContentIds, endpoint)
+    console.log(`provider ${providerId}: has ${found} out of ${total}`)
+  })
+}
 
 
-function mapInfoToStatus(providers, currentHeight) {
+function mapInfoToStatus (providers, currentHeight) {
-  return providers.map(({account, info, joined}) => {
+  return providers.map(({providerId, info}) => {
     if (info) {
     if (info) {
       return {
       return {
-        address: account.toString(),
+        providerId,
-        age: currentHeight.sub(joined).toNumber(),
         identity: info.identity.toString(),
         identity: info.identity.toString(),
         expiresIn: info.expires_at.sub(currentHeight).toNumber(),
         expiresIn: info.expires_at.sub(currentHeight).toNumber(),
-        expired: currentHeight.gte(info.expires_at),
+        expired: currentHeight.gte(info.expires_at)
       }
       }
     } else {
     } else {
       return {
       return {
-        address: account.toString(),
+        providerId,
         identity: null,
         identity: null,
         status: 'down'
         status: 'down'
       }
       }
@@ -127,40 +129,55 @@ function mapInfoToStatus(providers, currentHeight) {
   })
   })
 }
 }
 
 
-async function countContentAvailability(contentIds, source) {
+// HTTP HEAD with axios all known content ids on each provider
+async function countContentAvailability (contentIds, source) {
   let content = {}
   let content = {}
-  let found = 0;
+  let found = 0
-  for(let i = 0; i < contentIds.length; i++) {
+  let missing = 0
-    const assetUrl = makeAssetUrl(contentIds[i], source);
+  for (let i = 0; i < contentIds.length; i++) {
+    const assetUrl = makeAssetUrl(contentIds[i], source)
     try {
     try {
       let info = await axios.head(assetUrl)
       let info = await axios.head(assetUrl)
       content[encodeAddress(contentIds[i])] = {
       content[encodeAddress(contentIds[i])] = {
         type: info.headers['content-type'],
         type: info.headers['content-type'],
         bytes: info.headers['content-length']
         bytes: info.headers['content-length']
       }
       }
+      // TODO: cross check against dataobject size
       found++
       found++
-    } catch(err) { console.log(`${assetUrl} ${err.message}`); continue; }
+    } catch (err) {
+      missing++
+    }
   }
   }
-  console.log(content);
+
-  return { found, content };
+  return { found, missing, content }
 }
 }
 
 
-function makeAssetUrl(contentId, source) {
+function makeAssetUrl (contentId, source) {
-  source = stripEndingSlash(source);
+  source = stripEndingSlash(source)
   return `${source}/asset/v0/${encodeAddress(contentId)}`
   return `${source}/asset/v0/${encodeAddress(contentId)}`
 }
 }
 
 
-async function assetRelationshipState(api, contentId, providers) {
+async function assetRelationshipState (api, contentId, providers) {
-  let dataObject = await api.query.dataDirectory.dataObjectByContentId(contentId);
+  let dataObject = await api.query.dataDirectory.dataObjectByContentId(contentId)
 
 
-  // how many relationships out of active providers?
+  let relationshipIds = await api.query.dataObjectStorageRegistry.relationshipsByContentId(contentId)
-  let relationshipIds = await api.query.dataObjectStorageRegistry.relationshipsByContentId(contentId);
 
 
+  // how many relationships associated with active providers and in ready state
   let activeRelationships = await Promise.all(relationshipIds.map(async (id) => {
   let activeRelationships = await Promise.all(relationshipIds.map(async (id) => {
-    let relationship = await api.query.dataObjectStorageRegistry.relationships(id);
+    let relationship = await api.query.dataObjectStorageRegistry.relationships(id)
     relationship = relationship.unwrap()
     relationship = relationship.unwrap()
+    // only interested in ready relationships
+    if (!relationship.ready) {
+      return undefined
+    }
+    // Does the relationship belong to an active provider ?
     return providers.find((provider) => relationship.storage_provider.eq(provider))
     return providers.find((provider) => relationship.storage_provider.eq(provider))
-  }));
+  }))
+
+  return ([
+    activeRelationships.filter(active => active).length,
+    dataObject.unwrap().liaison_judgement
+  ])
+}
 
 
-  return [activeRelationships.filter(active => active).length, dataObject.unwrap().liaison_judgement]
+main()
-}

+ 2 - 1
storage-node/packages/helios/package.json

@@ -1,5 +1,6 @@
 {
 {
   "name": "@joystream/helios",
   "name": "@joystream/helios",
+  "private": true,
   "version": "0.1.0",
   "version": "0.1.0",
   "bin": {
   "bin": {
     "helios": "bin/cli.js"
     "helios": "bin/cli.js"
@@ -9,7 +10,7 @@
   },
   },
   "license": "MIT",
   "license": "MIT",
   "dependencies": {
   "dependencies": {
-    "@joystream/runtime-api": "^0.1.0",
+    "@joystream/storage-runtime-api": "^0.1.0",
     "@types/bn.js": "^4.11.5",
     "@types/bn.js": "^4.11.5",
     "axios": "^0.19.0",
     "axios": "^0.19.0",
     "bn.js": "^4.11.8"
     "bn.js": "^4.11.8"

+ 79 - 89
storage-node/packages/runtime-api/assets.js

@@ -1,14 +1,9 @@
-'use strict';
+'use strict'
 
 
-const debug = require('debug')('joystream:runtime:assets');
+const debug = require('debug')('joystream:runtime:assets')
+const { decodeAddress } = require('@polkadot/keyring')
 
 
-const { Null } = require('@polkadot/types/primitive');
+function parseContentId (contentId) {
-
-const { _ } = require('lodash');
-
-const { decodeAddress, encodeAddress } = require('@polkadot/keyring');
-
-function parseContentId(contentId) {
   try {
   try {
     return decodeAddress(contentId)
     return decodeAddress(contentId)
   } catch (err) {
   } catch (err) {
@@ -19,158 +14,153 @@ function parseContentId(contentId) {
 /*
 /*
  * Add asset related functionality to the substrate API.
  * Add asset related functionality to the substrate API.
  */
  */
-class AssetsApi
+class AssetsApi {
-{
+  static async create (base) {
-  static async create(base)
+    const ret = new AssetsApi()
-  {
+    ret.base = base
-    const ret = new AssetsApi();
+    await ret.init()
-    ret.base = base;
+    return ret
-    await ret.init();
-    return ret;
   }
   }
 
 
-  async init(account_file)
+  async init () {
-  {
+    debug('Init')
-    debug('Init');
   }
   }
 
 
   /*
   /*
-   * Create a data object.
+   * Create and return a data object.
    */
    */
-  async createDataObject(accountId, contentId, doTypeId, size)
+  async createDataObject (accountId, memberId, contentId, doTypeId, size, ipfsCid) {
-  {
     contentId = parseContentId(contentId)
     contentId = parseContentId(contentId)
-    const tx = this.base.api.tx.dataDirectory.addContent(contentId, doTypeId, size);
+    const tx = this.base.api.tx.dataDirectory.addContent(memberId, contentId, doTypeId, size, ipfsCid)
-    await this.base.signAndSend(accountId, tx);
+    await this.base.signAndSend(accountId, tx)
 
 
     // If the data object constructed properly, we should now be able to return
     // If the data object constructed properly, we should now be able to return
     // the data object from the state.
     // the data object from the state.
-    return await this.getDataObject(contentId);
+    return this.getDataObject(contentId)
   }
   }
 
 
   /*
   /*
-   * Return the Data Object for a CID
+   * Return the Data Object for a contendId
    */
    */
-  async getDataObject(contentId)
+  async getDataObject (contentId) {
-  {
     contentId = parseContentId(contentId)
     contentId = parseContentId(contentId)
-    const obj = await this.base.api.query.dataDirectory.dataObjectByContentId(contentId);
+    return this.base.api.query.dataDirectory.dataObjectByContentId(contentId)
-    return obj;
   }
   }
 
 
   /*
   /*
-   * Verify the liaison state for a DO:
+   * Verify the liaison state for a DataObject:
-   * - Check the content ID has a DO
+   * - Check the content ID has a DataObject
-   * - Check the account is the liaison
+   * - Check the storageProviderId is the liaison
-   * - Check the liaison state is pending
+   * - Check the liaison state is Pending
    *
    *
    * Each failure errors out, success returns the data object.
    * Each failure errors out, success returns the data object.
    */
    */
-  async checkLiaisonForDataObject(accountId, contentId)
+  async checkLiaisonForDataObject (storageProviderId, contentId) {
-  {
     contentId = parseContentId(contentId)
     contentId = parseContentId(contentId)
 
 
-    let obj = await this.getDataObject(contentId);
+    let obj = await this.getDataObject(contentId)
 
 
     if (obj.isNone) {
     if (obj.isNone) {
-      throw new Error(`No DataObject created for content ID: ${contentId}`);
+      throw new Error(`No DataObject created for content ID: ${contentId}`)
     }
     }
 
 
-    const encoded = encodeAddress(obj.raw.liaison);
+    obj = obj.unwrap()
-    if (encoded != accountId) {
+
-      throw new Error(`This storage node is not liaison for the content ID: ${contentId}`);
+    if (!obj.liaison.eq(storageProviderId)) {
+      throw new Error(`This storage node is not liaison for the content ID: ${contentId}`)
     }
     }
 
 
-    if (obj.raw.liaison_judgement.type != 'Pending') {
+    if (obj.liaison_judgement.type !== 'Pending') {
-      throw new Error(`Expected Pending judgement, but found: ${obj.raw.liaison_judgement.type}`);
+      throw new Error(`Expected Pending judgement, but found: ${obj.liaison_judgement.type}`)
     }
     }
 
 
-    return obj.unwrap();
+    return obj
   }
   }
 
 
   /*
   /*
-   * Changes a data object liaison judgement.
+   * Sets the data object liaison judgement to Accepted
    */
    */
-  async acceptContent(accountId, contentId)
+  async acceptContent (providerAccoundId, storageProviderId, contentId) {
-  {
     contentId = parseContentId(contentId)
     contentId = parseContentId(contentId)
-    const tx = this.base.api.tx.dataDirectory.acceptContent(contentId);
+    const tx = this.base.api.tx.dataDirectory.acceptContent(storageProviderId, contentId)
-    return await this.base.signAndSend(accountId, tx);
+    return this.base.signAndSend(providerAccoundId, tx)
   }
   }
 
 
   /*
   /*
-   * Changes a data object liaison judgement.
+   * Sets the data object liaison judgement to Rejected
    */
    */
-  async rejectContent(accountId, contentId)
+  async rejectContent (providerAccountId, storageProviderId, contentId) {
-  {
     contentId = parseContentId(contentId)
     contentId = parseContentId(contentId)
-    const tx = this.base.api.tx.dataDirectory.rejectContent(contentId);
+    const tx = this.base.api.tx.dataDirectory.rejectContent(storageProviderId, contentId)
-    return await this.base.signAndSend(accountId, tx);
+    return this.base.signAndSend(providerAccountId, tx)
   }
   }
 
 
   /*
   /*
-   * Create storage relationship
+   * Creates storage relationship for a data object and provider
    */
    */
-  async createStorageRelationship(accountId, contentId, callback)
+  async createStorageRelationship (providerAccountId, storageProviderId, contentId, callback) {
-  {
     contentId = parseContentId(contentId)
     contentId = parseContentId(contentId)
-    const tx = this.base.api.tx.dataObjectStorageRegistry.addRelationship(contentId);
+    const tx = this.base.api.tx.dataObjectStorageRegistry.addRelationship(storageProviderId, contentId)
 
 
-    const subscribed = [['dataObjectStorageRegistry', 'DataObjectStorageRelationshipAdded']];
+    const subscribed = [['dataObjectStorageRegistry', 'DataObjectStorageRelationshipAdded']]
-    return await this.base.signAndSend(accountId, tx, 3, subscribed, callback);
+    return this.base.signAndSend(providerAccountId, tx, 3, subscribed, callback)
   }
   }
 
 
   /*
   /*
-   * Get storage relationship for contentId
+   * Gets storage relationship for contentId for the given provider
    */
    */
-  async getStorageRelationshipAndId(accountId, contentId) {
+  async getStorageRelationshipAndId (storageProviderId, contentId) {
     contentId = parseContentId(contentId)
     contentId = parseContentId(contentId)
-    let rids = await this.base.api.query.dataObjectStorageRegistry.relationshipsByContentId(contentId);
+    let rids = await this.base.api.query.dataObjectStorageRegistry.relationshipsByContentId(contentId)
-
+
-    while(rids.length) {
+    while (rids.length) {
-      const relationshipId = rids.shift();
+      const relationshipId = rids.shift()
-      let relationship = await this.base.api.query.dataObjectStorageRegistry.relationships(relationshipId);
+      let relationship = await this.base.api.query.dataObjectStorageRegistry.relationships(relationshipId)
-      relationship = relationship.unwrap();
+      relationship = relationship.unwrap()
-      if (relationship.storage_provider.eq(decodeAddress(accountId))) {
+      if (relationship.storage_provider.eq(storageProviderId)) {
-        return ({ relationship, relationshipId });
+        return ({ relationship, relationshipId })
       }
       }
     }
     }
 
 
-    return {};
+    return {}
   }
   }
 
 
-  async createAndReturnStorageRelationship(accountId, contentId)
+  /*
-  {
+   * Creates storage relationship for a data object and provider and returns the relationship id
+   */
+  async createAndReturnStorageRelationship (providerAccountId, storageProviderId, contentId) {
     contentId = parseContentId(contentId)
     contentId = parseContentId(contentId)
     return new Promise(async (resolve, reject) => {
     return new Promise(async (resolve, reject) => {
       try {
       try {
-        await this.createStorageRelationship(accountId, contentId, (events) => {
+        await this.createStorageRelationship(providerAccountId, storageProviderId, contentId, (events) => {
           events.forEach((event) => {
           events.forEach((event) => {
-            resolve(event[1].DataObjectStorageRelationshipId);
+            resolve(event[1].DataObjectStorageRelationshipId)
-          });
+          })
-        });
+        })
       } catch (err) {
       } catch (err) {
-        reject(err);
+        reject(err)
       }
       }
-    });
+    })
   }
   }
 
 
   /*
   /*
-   * Toggle ready state for DOSR.
+   * Set the ready state for a data object storage relationship to the new value
    */
    */
-  async toggleStorageRelationshipReady(accountId, dosrId, ready)
+  async toggleStorageRelationshipReady (providerAccountId, storageProviderId, dosrId, ready) {
-  {
     var tx = ready
     var tx = ready
-      ? this.base.api.tx.dataObjectStorageRegistry.setRelationshipReady(dosrId)
+      ? this.base.api.tx.dataObjectStorageRegistry.setRelationshipReady(storageProviderId, dosrId)
-      : this.base.api.tx.dataObjectStorageRegistry.unsetRelationshipReady(dosrId);
+      : this.base.api.tx.dataObjectStorageRegistry.unsetRelationshipReady(storageProviderId, dosrId)
-    return await this.base.signAndSend(accountId, tx);
+    return this.base.signAndSend(providerAccountId, tx)
   }
   }
 
 
-  async getKnownContentIds() {
+  /*
-    return this.base.api.query.dataDirectory.knownContentIds();
+   * Returns array of know content ids
+   */
+  async getKnownContentIds () {
+    return this.base.api.query.dataDirectory.knownContentIds()
   }
   }
 }
 }
 
 
 module.exports = {
 module.exports = {
-  AssetsApi: AssetsApi,
+  AssetsApi
 }
 }

+ 1 - 1
storage-node/packages/runtime-api/balances.js

@@ -20,7 +20,7 @@
 
 
 const debug = require('debug')('joystream:runtime:balances');
 const debug = require('debug')('joystream:runtime:balances');
 
 
-const { IdentitiesApi } = require('@joystream/runtime-api/identities');
+const { IdentitiesApi } = require('@joystream/storage-runtime-api/identities');
 
 
 /*
 /*
  * Bundle API calls related to account balances.
  * Bundle API calls related to account balances.

+ 42 - 30
storage-node/packages/runtime-api/discovery.js

@@ -1,64 +1,76 @@
-'use strict';
+'use strict'
 
 
-const debug = require('debug')('joystream:runtime:discovery');
+const debug = require('debug')('joystream:runtime:discovery')
 
 
 /*
 /*
  * Add discovery related functionality to the substrate API.
  * Add discovery related functionality to the substrate API.
  */
  */
-class DiscoveryApi
+class DiscoveryApi {
-{
+  static async create (base) {
-  static async create(base)
+    const ret = new DiscoveryApi()
-  {
+    ret.base = base
-    const ret = new DiscoveryApi();
+    await ret.init()
-    ret.base = base;
+    return ret
-    await ret.init();
-    return ret;
   }
   }
 
 
-  async init(account_file)
+  async init () {
-  {
+    debug('Init')
-    debug('Init');
   }
   }
 
 
   /*
   /*
    * Get Bootstrap endpoints
    * Get Bootstrap endpoints
    */
    */
-  async getBootstrapEndpoints() {
+  async getBootstrapEndpoints () {
     return this.base.api.query.discovery.bootstrapEndpoints()
     return this.base.api.query.discovery.bootstrapEndpoints()
   }
   }
 
 
   /*
   /*
-   * Get AccountInfo of an accountId
+   * Set Bootstrap endpoints, requires the sudo account to be provided and unlocked
    */
    */
-  async getAccountInfo(accountId) {
+  async setBootstrapEndpoints (sudoAccount, endpoints) {
-    const decoded = this.base.identities.keyring.decodeAddress(accountId, true)
+    const tx = this.base.api.tx.discovery.setBootstrapEndpoints(endpoints)
-    const info = await this.base.api.query.discovery.accountInfoByAccountId(decoded)
+    // make sudo call
+    return this.base.signAndSend(
+      sudoAccount,
+      this.base.api.tx.sudo.sudo(tx)
+    )
+  }
+
+  /*
+   * Get AccountInfo of a storage provider
+   */
+  async getAccountInfo (storageProviderId) {
+    const info = await this.base.api.query.discovery.accountInfoByStorageProviderId(storageProviderId)
     // Not an Option so we use default value check to know if info was found
     // Not an Option so we use default value check to know if info was found
     return info.expires_at.eq(0) ? null : info
     return info.expires_at.eq(0) ? null : info
   }
   }
 
 
   /*
   /*
-   * Set AccountInfo of an accountId
+   * Set AccountInfo of our storage provider
    */
    */
-  async setAccountInfo(accountId, ipnsId, ttl) {
+  async setAccountInfo (ipnsId) {
-    const isActor = await this.base.identities.isActor(accountId)
+    const roleAccountId = this.base.identities.key.address
-    if (isActor) {
+    const storageProviderId = this.base.storageProviderId
-      const tx = this.base.api.tx.discovery.setIpnsId(ipnsId, ttl)
+    const isProvider = await this.base.workers.isStorageProvider(storageProviderId)
-      return this.base.signAndSend(accountId, tx)
+    if (isProvider) {
+      const tx = this.base.api.tx.discovery.setIpnsId(storageProviderId, ipnsId)
+      return this.base.signAndSend(roleAccountId, tx)
     } else {
     } else {
-      throw new Error('Cannot set AccountInfo for non actor account')
+      throw new Error('Cannot set AccountInfo, id is not a storage provider')
     }
     }
   }
   }
 
 
   /*
   /*
-   * Clear AccountInfo of an accountId
+   * Clear AccountInfo of our storage provider
    */
    */
-  async unsetAccountInfo(accountId) {
+  async unsetAccountInfo () {
-    var tx = this.base.api.tx.discovery.unsetIpnsId()
+    const roleAccountId = this.base.identities.key.address
-    return this.base.signAndSend(accountId, tx)
+    const storageProviderId = this.base.storageProviderId
+    var tx = this.base.api.tx.discovery.unsetIpnsId(storageProviderId)
+    return this.base.signAndSend(roleAccountId, tx)
   }
   }
 }
 }
 
 
 module.exports = {
 module.exports = {
-  DiscoveryApi: DiscoveryApi,
+  DiscoveryApi
 }
 }

+ 117 - 116
storage-node/packages/runtime-api/identities.js

@@ -8,7 +8,7 @@
  * (at your option) any later version.
  * (at your option) any later version.
  *
  *
  * This program is distributed in the hope that it will be useful,
  * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * but WITHOUT ANY WARRANTY without even the implied warranty of
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  * GNU General Public License for more details.
  * GNU General Public License for more details.
  *
  *
@@ -16,220 +16,221 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
  */
 
 
-'use strict';
+'use strict'
 
 
-const path = require('path');
+const path = require('path')
-const fs = require('fs');
+const fs = require('fs')
-// const readline = require('readline');
+// const readline = require('readline')
 
 
-const debug = require('debug')('joystream:runtime:identities');
+const debug = require('debug')('joystream:runtime:identities')
-
+const { Keyring } = require('@polkadot/keyring')
-const { Keyring } = require('@polkadot/keyring');
+const util_crypto = require('@polkadot/util-crypto')
-// const { Null } = require('@polkadot/types/primitive');
-const util_crypto = require('@polkadot/util-crypto');
-
-// const { _ } = require('lodash');
 
 
 /*
 /*
  * Add identity management to the substrate API.
  * Add identity management to the substrate API.
  *
  *
  * This loosely groups: accounts, key management, and membership.
  * This loosely groups: accounts, key management, and membership.
  */
  */
-class IdentitiesApi
+class IdentitiesApi {
-{
+  static async create (base, {account_file, passphrase, canPromptForPassphrase}) {
-  static async create(base, {account_file, passphrase, canPromptForPassphrase})
+    const ret = new IdentitiesApi()
-  {
+    ret.base = base
-    const ret = new IdentitiesApi();
+    await ret.init(account_file, passphrase, canPromptForPassphrase)
-    ret.base = base;
+    return ret
-    await ret.init(account_file, passphrase, canPromptForPassphrase);
-    return ret;
   }
   }
 
 
-  async init(account_file, passphrase, canPromptForPassphrase)
+  async init (account_file, passphrase, canPromptForPassphrase) {
-  {
+    debug('Init')
-    debug('Init');
 
 
     // Creatre keyring
     // Creatre keyring
-    this.keyring = new Keyring();
+    this.keyring = new Keyring()
 
 
-    this.canPromptForPassphrase = canPromptForPassphrase || false;
+    this.canPromptForPassphrase = canPromptForPassphrase || false
 
 
     // Load account file, if possible.
     // Load account file, if possible.
     try {
     try {
-      this.key = await this.loadUnlock(account_file, passphrase);
+      this.key = await this.loadUnlock(account_file, passphrase)
     } catch (err) {
     } catch (err) {
-      debug('Error loading account file:', err.message);
+      debug('Error loading account file:', err.message)
     }
     }
   }
   }
 
 
   /*
   /*
    * Load a key file and unlock it if necessary.
    * Load a key file and unlock it if necessary.
    */
    */
-  async loadUnlock(account_file, passphrase)
+  async loadUnlock (account_file, passphrase) {
-  {
+    const fullname = path.resolve(account_file)
-    const fullname = path.resolve(account_file);
+    debug('Initializing key from', fullname)
-    debug('Initializing key from', fullname);
+    const key = this.keyring.addFromJson(require(fullname))
-    const key = this.keyring.addFromJson(require(fullname));
+    await this.tryUnlock(key, passphrase)
-    await this.tryUnlock(key, passphrase);
+    debug('Successfully initialized with address', key.address)
-    debug('Successfully initialized with address', key.address);
+    return key
-    return key;
   }
   }
 
 
   /*
   /*
    * Try to unlock a key if it isn't already unlocked.
    * Try to unlock a key if it isn't already unlocked.
    * passphrase should be supplied as argument.
    * passphrase should be supplied as argument.
    */
    */
-  async tryUnlock(key, passphrase)
+  async tryUnlock (key, passphrase) {
-  {
     if (!key.isLocked) {
     if (!key.isLocked) {
       debug('Key is not locked, not attempting to unlock')
       debug('Key is not locked, not attempting to unlock')
-      return;
+      return
     }
     }
 
 
     // First try with an empty passphrase - for convenience
     // First try with an empty passphrase - for convenience
     try {
     try {
-      key.decodePkcs8('');
+      key.decodePkcs8('')
 
 
       if (passphrase) {
       if (passphrase) {
-        debug('Key was not encrypted, supplied passphrase was ignored');
+        debug('Key was not encrypted, supplied passphrase was ignored')
       }
       }
 
 
-      return;
+      return
     } catch (err) {
     } catch (err) {
       // pass
       // pass
     }
     }
 
 
     // Then with supplied passphrase
     // Then with supplied passphrase
     try {
     try {
-      debug('Decrypting with supplied passphrase');
+      debug('Decrypting with supplied passphrase')
-      key.decodePkcs8(passphrase);
+      key.decodePkcs8(passphrase)
-      return;
+      return
     } catch (err) {
     } catch (err) {
       // pass
       // pass
     }
     }
 
 
     // If that didn't work, ask for a passphrase if appropriate
     // If that didn't work, ask for a passphrase if appropriate
     if (this.canPromptForPassphrase) {
     if (this.canPromptForPassphrase) {
-      passphrase = await this.askForPassphrase(key.address);
+      passphrase = await this.askForPassphrase(key.address)
-      key.decodePkcs8(passphrase);
+      key.decodePkcs8(passphrase)
       return
       return
     }
     }
 
 
-    throw new Error('invalid passphrase supplied');
+    throw new Error('invalid passphrase supplied')
   }
   }
 
 
   /*
   /*
    * Ask for a passphrase
    * Ask for a passphrase
    */
    */
-  askForPassphrase(address)
+  askForPassphrase (address) {
-  {
     // Query for passphrase
     // Query for passphrase
-    const prompt = require('password-prompt');
+    const prompt = require('password-prompt')
-    return prompt(`Enter passphrase for ${address}: `, { required: false });
+    return prompt(`Enter passphrase for ${address}: `, { required: false })
   }
   }
 
 
   /*
   /*
-   * Return true if the account is a member
+   * Return true if the account is a root account of a member
    */
    */
-  async isMember(accountId)
+  async isMember (accountId) {
-  {
+    const memberIds = await this.memberIdsOf(accountId) // return array of member ids
-    const memberIds = await this.memberIdsOf(accountId); // return array of member ids
     return memberIds.length > 0 // true if at least one member id exists for the acccount
     return memberIds.length > 0 // true if at least one member id exists for the acccount
   }
   }
 
 
   /*
   /*
-   * Return true if the account is an actor/role account
+   * Return all the member IDs of an account by the root account id
    */
    */
-  async isActor(accountId)
+  async memberIdsOf (accountId) {
-  {
+    const decoded = this.keyring.decodeAddress(accountId)
-    const decoded = this.keyring.decodeAddress(accountId);
+    return this.base.api.query.members.memberIdsByRootAccountId(decoded)
-    const actor = await this.base.api.query.actors.actorByAccountId(decoded)
-    return actor.isSome
   }
   }
 
 
   /*
   /*
-   * Return the member IDs of an account
+   * Return the first member ID of an account, or undefined if not a member root account.
    */
    */
-  async memberIdsOf(accountId)
+  async firstMemberIdOf (accountId) {
-  {
+    const decoded = this.keyring.decodeAddress(accountId)
-    const decoded = this.keyring.decodeAddress(accountId);
+    let ids = await this.base.api.query.members.memberIdsByRootAccountId(decoded)
-    return await this.base.api.query.members.memberIdsByRootAccountId(decoded);
+    return ids[0]
   }
   }
 
 
   /*
   /*
-   * Return the first member ID of an account, or undefined if not a member.
+   * Export a key pair to JSON. Will ask for a passphrase.
    */
    */
-  async firstMemberIdOf(accountId)
+  async exportKeyPair (accountId) {
-  {
+    const passphrase = await this.askForPassphrase(accountId)
-    const decoded = this.keyring.decodeAddress(accountId);
+
-    let ids = await this.base.api.query.members.memberIdsByRootAccountId(decoded);
+    // Produce JSON output
-    return ids[0]
+    return this.keyring.toJson(accountId, passphrase)
   }
   }
 
 
   /*
   /*
-   * Create a new key for the given role *name*. If no name is given,
+   * Export a key pair and write it to a JSON file with the account ID as the
-   * default to 'storage'.
+   * name.
    */
    */
-  async createRoleKey(accountId, role)
+  async writeKeyPairExport (accountId, prefix) {
-  {
+    // Generate JSON
-    role = role || 'storage';
+    const data = await this.exportKeyPair(accountId)
-
-    // Generate new key pair
-    const keyPair = util_crypto.naclKeypairFromRandom();
-
-    // Encode to an address.
-    const addr = this.keyring.encodeAddress(keyPair.publicKey);
-    debug('Generated new key pair with address', addr);
 
 
-    // Add to key wring. We set the meta to identify the account as
+    // Write JSON
-    // a role key.
+    var filename = `${data.address}.json`
-    const meta = {
-      name: `${role} role account for ${accountId}`,
-    };
 
 
-    const createPair = require('@polkadot/keyring/pair').default;
+    if (prefix) {
-    const pair = createPair('ed25519', keyPair, meta);
+      const path = require('path')
+      filename = path.resolve(prefix, filename)
+    }
 
 
-    this.keyring.addPair(pair);
+    fs.writeFileSync(filename, JSON.stringify(data), {
+      encoding: 'utf8',
+      mode: 0o600
+    })
 
 
-    return pair;
+    return filename
   }
   }
 
 
   /*
   /*
-   * Export a key pair to JSON. Will ask for a passphrase.
+   * Register account id with userInfo as a new member
+   * using default policy 0, returns new member id
    */
    */
-  async exportKeyPair(accountId)
+  async registerMember (accountId, userInfo) {
-  {
+    const tx = this.base.api.tx.members.buyMembership(0, userInfo)
-    const passphrase = await this.askForPassphrase(accountId);
+
+    return this.base.signAndSendThenGetEventResult(accountId, tx, {
+      eventModule: 'members',
+      eventName: 'MemberRegistered',
+      eventProperty: 'MemberId'
+    })
+  }
 
 
-    // Produce JSON output
+  /*
-    return this.keyring.toJson(accountId, passphrase);
+   * Injects a keypair and sets it as the default identity
+   */
+  useKeyPair (keyPair) {
+    this.key = this.keyring.addPair(keyPair)
   }
   }
 
 
   /*
   /*
-   * Export a key pair and write it to a JSON file with the account ID as the
+   * Create a new role key. If no name is given,
-   * name.
+   * default to 'storage'.
    */
    */
-  async writeKeyPairExport(accountId, prefix)
+  async createNewRoleKey (name) {
-  {
+    name = name || 'storage-provider'
-    // Generate JSON
-    const data = await this.exportKeyPair(accountId);
 
 
-    // Write JSON
+    // Generate new key pair
-    var filename = `${data.address}.json`;
+    const keyPair = util_crypto.naclKeypairFromRandom()
-    if (prefix) {
+
-      const path = require('path');
+    // Encode to an address.
-      filename = path.resolve(prefix, filename);
+    const addr = this.keyring.encodeAddress(keyPair.publicKey)
+    debug('Generated new key pair with address', addr)
+
+    // Add to key wring. We set the meta to identify the account as
+    // a role key.
+    const meta = {
+      name: `${name} role account`
     }
     }
-    fs.writeFileSync(filename, JSON.stringify(data), {
-      encoding: 'utf8',
-      mode: 0o600,
-    });
 
 
-    return filename;
+    const createPair = require('@polkadot/keyring/pair').default
+    const pair = createPair('ed25519', keyPair, meta)
+
+    this.keyring.addPair(pair)
+
+    return pair
+  }
+
+  getSudoAccount() {
+    return this.base.api.query.sudo.key()
   }
   }
 }
 }
 
 
 module.exports = {
 module.exports = {
-  IdentitiesApi: IdentitiesApi,
+  IdentitiesApi
 }
 }

+ 128 - 118
storage-node/packages/runtime-api/index.js

@@ -8,7 +8,7 @@
  * (at your option) any later version.
  * (at your option) any later version.
  *
  *
  * This program is distributed in the hope that it will be useful,
  * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * but WITHOUT ANY WARRANTY without even the implied warranty of
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  * GNU General Public License for more details.
  * GNU General Public License for more details.
  *
  *
@@ -16,70 +16,70 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
  */
 
 
-'use strict';
+'use strict'
 
 
-const debug = require('debug')('joystream:runtime:base');
+const debug = require('debug')('joystream:runtime:base')
 
 
-const { registerJoystreamTypes } = require('@joystream/types');
+const { registerJoystreamTypes } = require('@joystream/types')
-const { ApiPromise, WsProvider } = require('@polkadot/api');
+const { ApiPromise, WsProvider } = require('@polkadot/api')
 
 
-const { IdentitiesApi } = require('@joystream/runtime-api/identities');
+const { IdentitiesApi } = require('@joystream/storage-runtime-api/identities')
-const { BalancesApi } = require('@joystream/runtime-api/balances');
+const { BalancesApi } = require('@joystream/storage-runtime-api/balances')
-const { RolesApi } = require('@joystream/runtime-api/roles');
+const { WorkersApi } = require('@joystream/storage-runtime-api/workers')
-const { AssetsApi } = require('@joystream/runtime-api/assets');
+const { AssetsApi } = require('@joystream/storage-runtime-api/assets')
-const { DiscoveryApi } = require('@joystream/runtime-api/discovery');
+const { DiscoveryApi } = require('@joystream/storage-runtime-api/discovery')
-const AsyncLock = require('async-lock');
+const AsyncLock = require('async-lock')
+const { newExternallyControlledPromise } = require('@joystream/storage-utils/externalPromise')
 
 
 /*
 /*
  * Initialize runtime (substrate) API and keyring.
  * Initialize runtime (substrate) API and keyring.
  */
  */
-class RuntimeApi
+class RuntimeApi {
-{
+  static async create (options) {
-  static async create(options)
+    const runtime_api = new RuntimeApi()
-  {
+    await runtime_api.init(options || {})
-    const runtime_api = new RuntimeApi();
+    return runtime_api
-    await runtime_api.init(options || {});
-    return runtime_api;
   }
   }
 
 
-  async init(options)
+  async init (options) {
-  {
+    debug('Init')
-    debug('Init');
 
 
-    options = options || {};
+    options = options || {}
 
 
     // Register joystream types
     // Register joystream types
-    registerJoystreamTypes();
+    registerJoystreamTypes()
 
 
-    const provider = new WsProvider(options.provider_url || 'ws://localhost:9944');
+    const provider = new WsProvider(options.provider_url || 'ws://localhost:9944')
 
 
     // Create the API instrance
     // Create the API instrance
-    this.api = await ApiPromise.create({ provider });
+    this.api = await ApiPromise.create({ provider })
 
 
-    this.asyncLock = new AsyncLock();
+    this.asyncLock = new AsyncLock()
 
 
     // Keep track locally of account nonces.
     // Keep track locally of account nonces.
-    this.nonces = {};
+    this.nonces = {}
+
+    // The storage provider id to use
+    this.storageProviderId = parseInt(options.storageProviderId) // u64 instead ?
 
 
     // Ok, create individual APIs
     // Ok, create individual APIs
     this.identities = await IdentitiesApi.create(this, {
     this.identities = await IdentitiesApi.create(this, {
       account_file: options.account_file,
       account_file: options.account_file,
       passphrase: options.passphrase,
       passphrase: options.passphrase,
       canPromptForPassphrase: options.canPromptForPassphrase
       canPromptForPassphrase: options.canPromptForPassphrase
-    });
+    })
-    this.balances = await BalancesApi.create(this);
+    this.balances = await BalancesApi.create(this)
-    this.roles = await RolesApi.create(this);
+    this.workers = await WorkersApi.create(this)
-    this.assets = await AssetsApi.create(this);
+    this.assets = await AssetsApi.create(this)
-    this.discovery = await DiscoveryApi.create(this);
+    this.discovery = await DiscoveryApi.create(this)
   }
   }
 
 
-  disconnect()
+  disconnect () {
-  {
+    this.api.disconnect()
-    this.api.disconnect();
   }
   }
 
 
-  executeWithAccountLock(account_id, func) {
+  executeWithAccountLock (account_id, func) {
-    return this.asyncLock.acquire(`${account_id}`, func);
+    return this.asyncLock.acquire(`${account_id}`, func)
   }
   }
 
 
   /*
   /*
@@ -89,47 +89,45 @@ class RuntimeApi
    * The result of the Promise is an array containing first the full event
    * The result of the Promise is an array containing first the full event
    * name, and then the event fields as an object.
    * name, and then the event fields as an object.
    */
    */
-  async waitForEvent(module, name)
+  async waitForEvent (module, name) {
-  {
+    return this.waitForEvents([[module, name]])
-    return this.waitForEvents([[module, name]]);
   }
   }
 
 
-  _matchingEvents(subscribed, events)
+  _matchingEvents(subscribed, events) {
-  {
+    debug(`Number of events: ${events.length} subscribed to ${subscribed}`)
-    debug(`Number of events: ${events.length}; subscribed to ${subscribed}`);
 
 
     const filtered = events.filter((record) => {
     const filtered = events.filter((record) => {
-      const { event, phase } = record;
+      const { event, phase } = record
 
 
       // Show what we are busy with
       // Show what we are busy with
-      debug(`\t${event.section}:${event.method}:: (phase=${phase.toString()})`);
+      debug(`\t${event.section}:${event.method}:: (phase=${phase.toString()})`)
-      debug(`\t\t${event.meta.documentation.toString()}`);
+      debug(`\t\t${event.meta.documentation.toString()}`)
 
 
       // Skip events we're not interested in.
       // Skip events we're not interested in.
       const matching = subscribed.filter((value) => {
       const matching = subscribed.filter((value) => {
-        return event.section == value[0] && event.method == value[1];
+        return event.section === value[0] && event.method === value[1]
-      });
+      })
-      return matching.length > 0;
+      return matching.length > 0
-    });
+    })
-    debug(`Filtered: ${filtered.length}`);
+    debug(`Filtered: ${filtered.length}`)
 
 
     const mapped = filtered.map((record) => {
     const mapped = filtered.map((record) => {
-      const { event } = record;
+      const { event } = record
-      const types = event.typeDef;
+      const types = event.typeDef
 
 
       // Loop through each of the parameters, displaying the type and data
       // Loop through each of the parameters, displaying the type and data
-      const payload = {};
+      const payload = {}
       event.data.forEach((data, index) => {
       event.data.forEach((data, index) => {
-        debug(`\t\t\t${types[index].type}: ${data.toString()}`);
+        debug(`\t\t\t${types[index].type}: ${data.toString()}`)
-        payload[types[index].type] = data;
+        payload[types[index].type] = data
-      });
+      })
 
 
-      const full_name = `${event.section}.${event.method}`;
+      const full_name = `${event.section}.${event.method}`
-      return [full_name, payload];
+      return [full_name, payload]
-    });
+    })
-    debug('Mapped', mapped);
+    debug('Mapped', mapped)
 
 
-    return mapped;
+    return mapped
   }
   }
 
 
   /*
   /*
@@ -139,16 +137,15 @@ class RuntimeApi
    *
    *
    * Returns the first matched event *only*.
    * Returns the first matched event *only*.
    */
    */
-  async waitForEvents(subscribed)
+  async waitForEvents (subscribed) {
-  {
     return new Promise((resolve, reject) => {
     return new Promise((resolve, reject) => {
       this.api.query.system.events((events) => {
       this.api.query.system.events((events) => {
-        const matches = this._matchingEvents(subscribed, events);
+        const matches = this._matchingEvents(subscribed, events)
         if (matches && matches.length) {
         if (matches && matches.length) {
-          resolve(matches);
+          resolve(matches)
         }
         }
-      });
+      })
-    });
+    })
   }
   }
 
 
   /*
   /*
@@ -159,68 +156,68 @@ class RuntimeApi
    * If the subscribed events are given, and a callback as well, then the
    * If the subscribed events are given, and a callback as well, then the
    * callback is invoked with matching events.
    * callback is invoked with matching events.
    */
    */
-  async signAndSend(accountId, tx, attempts, subscribed, callback)
+  async signAndSend (accountId, tx, attempts, subscribed, callback) {
-  {
+    accountId = this.identities.keyring.encodeAddress(accountId)
-    // Prepare key
-    const from_key = this.identities.keyring.getPair(accountId);
 
 
+    // Key must be unlocked
+    const from_key = this.identities.keyring.getPair(accountId)
     if (from_key.isLocked) {
     if (from_key.isLocked) {
-      throw new Error('Must unlock key before using it to sign!');
+      throw new Error('Must unlock key before using it to sign!')
     }
     }
 
 
-    const finalizedPromise = newExternallyControlledPromise();
+    const finalizedPromise = newExternallyControlledPromise()
 
 
-    let unsubscribe = await this.executeWithAccountLock(accountId,  async () => {
+    let unsubscribe = await this.executeWithAccountLock(accountId, async () => {
       // Try to get the next nonce to use
       // Try to get the next nonce to use
-      let nonce = this.nonces[accountId];
+      let nonce = this.nonces[accountId]
 
 
       let incrementNonce = () => {
       let incrementNonce = () => {
         // only increment once
         // only increment once
-        incrementNonce = () => {}; // turn it into a no-op
+        incrementNonce = () => {} // turn it into a no-op
-        nonce = nonce.addn(1);
+        nonce = nonce.addn(1)
-        this.nonces[accountId] = nonce;
+        this.nonces[accountId] = nonce
       }
       }
 
 
       // If the nonce isn't available, get it from chain.
       // If the nonce isn't available, get it from chain.
       if (!nonce) {
       if (!nonce) {
         // current nonce
         // current nonce
-        nonce = await this.api.query.system.accountNonce(accountId);
+        nonce = await this.api.query.system.accountNonce(accountId)
-        debug(`Got nonce for ${accountId} from chain: ${nonce}`);
+        debug(`Got nonce for ${accountId} from chain: ${nonce}`)
       }
       }
 
 
       return new Promise((resolve, reject) => {
       return new Promise((resolve, reject) => {
-        debug('Signing and sending tx');
+        debug('Signing and sending tx')
         // send(statusUpdates) returns a function for unsubscribing from status updates
         // send(statusUpdates) returns a function for unsubscribing from status updates
         let unsubscribe = tx.sign(from_key, { nonce })
         let unsubscribe = tx.sign(from_key, { nonce })
           .send(({events = [], status}) => {
           .send(({events = [], status}) => {
-            debug(`TX status: ${status.type}`);
+            debug(`TX status: ${status.type}`)
 
 
             // Whatever events we get, process them if there's someone interested.
             // Whatever events we get, process them if there's someone interested.
             // It is critical that this event handling doesn't prevent
             // It is critical that this event handling doesn't prevent
             try {
             try {
               if (subscribed && callback) {
               if (subscribed && callback) {
-                const matched = this._matchingEvents(subscribed, events);
+                const matched = this._matchingEvents(subscribed, events)
-                debug('Matching events:', matched);
+                debug('Matching events:', matched)
                 if (matched.length) {
                 if (matched.length) {
-                  callback(matched);
+                  callback(matched)
                 }
                 }
               }
               }
-            } catch(err) {
+            } catch (err) {
               debug(`Error handling events ${err.stack}`)
               debug(`Error handling events ${err.stack}`)
             }
             }
 
 
             // We want to release lock as early as possible, sometimes Ready status
             // We want to release lock as early as possible, sometimes Ready status
             // doesn't occur, so we do it on Broadcast instead
             // doesn't occur, so we do it on Broadcast instead
             if (status.isReady) {
             if (status.isReady) {
-              debug('TX Ready.');
+              debug('TX Ready.')
-              incrementNonce();
+              incrementNonce()
-              resolve(unsubscribe); //releases lock
+              resolve(unsubscribe) // releases lock
             } else if (status.isBroadcast) {
             } else if (status.isBroadcast) {
-              debug('TX Broadcast.');
+              debug('TX Broadcast.')
-              incrementNonce();
+              incrementNonce()
-              resolve(unsubscribe); //releases lock
+              resolve(unsubscribe) // releases lock
             } else if (status.isFinalized) {
             } else if (status.isFinalized) {
-              debug('TX Finalized.');
+              debug('TX Finalized.')
               finalizedPromise.resolve(status)
               finalizedPromise.resolve(status)
             } else if (status.isFuture) {
             } else if (status.isFuture) {
               // comes before ready.
               // comes before ready.
@@ -228,10 +225,10 @@ class RuntimeApi
               // nonce was set in the future. Treating it as an error for now.
               // nonce was set in the future. Treating it as an error for now.
               debug('TX Future!')
               debug('TX Future!')
               // nonce is likely out of sync, delete it so we reload it from chain on next attempt
               // nonce is likely out of sync, delete it so we reload it from chain on next attempt
-              delete this.nonces[accountId];
+              delete this.nonces[accountId]
-              const err = new Error('transaction nonce set in future');
+              const err = new Error('transaction nonce set in future')
-              finalizedPromise.reject(err);
+              finalizedPromise.reject(err)
-              reject(err);
+              reject(err)
             }
             }
 
 
             /* why don't we see these status updates on local devchain (single node)
             /* why don't we see these status updates on local devchain (single node)
@@ -247,45 +244,58 @@ class RuntimeApi
             // Remember this can also happen if in the past we sent a tx with a future nonce, and the current nonce
             // Remember this can also happen if in the past we sent a tx with a future nonce, and the current nonce
             // now matches it.
             // now matches it.
             if (err) {
             if (err) {
-              const errstr = err.toString();
+              const errstr = err.toString()
               // not the best way to check error code.
               // not the best way to check error code.
               // https://github.com/polkadot-js/api/blob/master/packages/rpc-provider/src/coder/index.ts#L52
               // https://github.com/polkadot-js/api/blob/master/packages/rpc-provider/src/coder/index.ts#L52
               if (errstr.indexOf('Error: 1014:') < 0 && // low priority
               if (errstr.indexOf('Error: 1014:') < 0 && // low priority
                   errstr.indexOf('Error: 1010:') < 0) // bad transaction
                   errstr.indexOf('Error: 1010:') < 0) // bad transaction
               {
               {
                 // Error but not nonce related. (bad arguments maybe)
                 // Error but not nonce related. (bad arguments maybe)
-                debug('TX error', err);
+                debug('TX error', err)
               } else {
               } else {
                 // nonce is likely out of sync, delete it so we reload it from chain on next attempt
                 // nonce is likely out of sync, delete it so we reload it from chain on next attempt
-                delete this.nonces[accountId];
+                delete this.nonces[accountId]
               }
               }
             }
             }
 
 
-            finalizedPromise.reject(err);
+            finalizedPromise.reject(err)
             // releases lock
             // releases lock
-            reject(err);
+            reject(err)
-          });
+          })
-      });
+      })
     })
     })
 
 
     // when does it make sense to manyally unsubscribe?
     // when does it make sense to manyally unsubscribe?
     // at this point unsubscribe.then and unsubscribe.catch have been deleted
     // at this point unsubscribe.then and unsubscribe.catch have been deleted
-    // unsubscribe(); // don't unsubscribe if we want to wait for additional status
+    // unsubscribe() // don't unsubscribe if we want to wait for additional status
     // updates to know when the tx has been finalized
     // updates to know when the tx has been finalized
-    return finalizedPromise.promise;
+    return finalizedPromise.promise
   }
   }
+
+  /*
+   * Sign and send a transaction expect event from
+   * module and return eventProperty from the event.
+   */
+  async signAndSendThenGetEventResult (senderAccountId, tx, { eventModule, eventName, eventProperty }) {
+    // event from a module,
+    const subscribed = [[eventModule, eventName]]
+    return new Promise(async (resolve, reject) => {
+      try {
+        await this.signAndSend(senderAccountId, tx, 1, subscribed, (events) => {
+          events.forEach((event) => {
+            // fix - we may not necessarily want the first event
+            // if there are multiple events emitted,
+            resolve(event[1][eventProperty])
+          })
+        })
+      } catch (err) {
+        reject(err)
+      }
+    })
+  }
+
 }
 }
 
 
 module.exports = {
 module.exports = {
-  RuntimeApi: RuntimeApi,
+  RuntimeApi
 }
 }
-
-function newExternallyControlledPromise () {
-  // externally controller promise
-  let resolve, reject;
-  const promise = new Promise((res, rej) => {
-    resolve = res;
-    reject = rej;
-  });
-  return ({resolve, reject, promise});
-}

+ 3 - 2
storage-node/packages/runtime-api/package.json

@@ -1,5 +1,6 @@
 {
 {
-  "name": "@joystream/runtime-api",
+  "name": "@joystream/storage-runtime-api",
+  "private": true,
   "version": "0.1.0",
   "version": "0.1.0",
   "description": "Runtime API abstraction for Joystream Storage Node",
   "description": "Runtime API abstraction for Joystream Storage Node",
   "author": "Joystream",
   "author": "Joystream",
@@ -44,7 +45,7 @@
     "temp": "^0.9.0"
     "temp": "^0.9.0"
   },
   },
   "dependencies": {
   "dependencies": {
-    "@joystream/types": "^0.10.0",
+    "@joystream/types": "^0.11.0",
     "@polkadot/api": "^0.96.1",
     "@polkadot/api": "^0.96.1",
     "async-lock": "^1.2.0",
     "async-lock": "^1.2.0",
     "lodash": "^4.17.11",
     "lodash": "^4.17.11",

+ 0 - 186
storage-node/packages/runtime-api/roles.js

@@ -1,186 +0,0 @@
-/*
- * This file is part of the storage node for the Joystream project.
- * Copyright (C) 2019 Joystream Contributors
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <https://www.gnu.org/licenses/>.
- */
-
-'use strict';
-
-const debug = require('debug')('joystream:runtime:roles');
-
-const { Null, u64 } = require('@polkadot/types');
-
-const { _ } = require('lodash');
-
-/*
- * Add role related functionality to the substrate API.
- */
-class RolesApi
-{
-  static async create(base)
-  {
-    const ret = new RolesApi();
-    ret.base = base;
-    await ret.init();
-    return ret;
-  }
-
-  async init()
-  {
-    debug('Init');
-
-    // Constants
-    this.ROLE_STORAGE = 'StorageProvider'; // new u64(0x00);
-  }
-
-  /*
-   * Raises errors if the given account ID is not valid for staking as the given
-   * role. The role should be one of the ROLE_* constants above.
-   */
-  async checkAccountForStaking(accountId, role)
-  {
-    role = role || this.ROLE_STORAGE;
-
-    if (!await this.base.identities.isMember(accountId)) {
-      const msg = `Account with id "${accountId}" is not a member!`;
-      debug(msg);
-      throw new Error(msg);
-    }
-
-    if (!await this.hasBalanceForRoleStaking(accountId, role)) {
-      const msg = `Account with id "${accountId}" does not have sufficient free balance for role staking!`;
-      debug(msg);
-      throw new Error(msg);
-    }
-
-    debug(`Account with id "${accountId}" is a member with sufficient free balance, able to proceed.`);
-    return true;
-  }
-
-  /*
-   * Returns the required balance for staking for a role.
-   */
-  async requiredBalanceForRoleStaking(role)
-  {
-    const params = await this.base.api.query.actors.parameters(role);
-    if (params.isNone) {
-      throw new Error(`Role ${role} is not defined!`);
-    }
-    const result = params.raw.min_stake
-      .add(params.raw.entry_request_fee)
-      .add(await this.base.balances.baseTransactionFee());
-    return result;
-  }
-
-  /*
-   * Returns true/false if the given account has the balance required for
-   * staking for the given role.
-   */
-  async hasBalanceForRoleStaking(accountId, role)
-  {
-    const required = await this.requiredBalanceForRoleStaking(role);
-    return await this.base.balances.hasMinimumBalanceOf(accountId, required);
-  }
-
-  /*
-   * Transfer enough funds to allow the recipient to stake for the given role.
-   */
-  async transferForStaking(from, to, role)
-  {
-    const required = await this.requiredBalanceForRoleStaking(role);
-    return await this.base.balances.transfer(from, to, required);
-  }
-
-  /*
-   * Return current accounts holding a role.
-   */
-  async accountIdsByRole(role)
-  {
-    const ids = await this.base.api.query.actors.accountIdsByRole(role);
-    return ids.map(id => id.toString());
-  }
-
-  /*
-   * Returns the number of slots available for a role
-   */
-  async availableSlotsForRole(role)
-  {
-    let params = await this.base.api.query.actors.parameters(role);
-    if (params.isNone) {
-      throw new Error(`Role ${role} is not defined!`);
-    }
-    params = params.unwrap();
-    const slots = params.max_actors;
-    const active = await this.accountIdsByRole(role);
-    return (slots.subn(active.length)).toNumber();
-  }
-
-  /*
-   * Send a role application.
-   * - The role account must not be a member, but have sufficient funds for
-   *   staking.
-   * - The member account must be a member.
-   *
-   * After sending this application, the member account will have role request
-   * in the 'My Requests' tab of the app.
-   */
-  async applyForRole(roleAccountId, role, memberAccountId)
-  {
-    const memberId = await this.base.identities.firstMemberIdOf(memberAccountId);
-    if (memberId == undefined) {
-      throw new Error('Account is not a member!');
-    }
-
-    const tx = this.base.api.tx.actors.roleEntryRequest(role, memberId);
-    return await this.base.signAndSend(roleAccountId, tx);
-  }
-
-  /*
-   * Check whether the given role is occupying the given role.
-   */
-  async checkForRole(roleAccountId, role)
-  {
-    const actor = await this.base.api.query.actors.actorByAccountId(roleAccountId);
-    return !_.isEqual(actor.raw, new Null());
-  }
-
-  /*
-   * Same as checkForRole(), but if the account is not currently occupying the
-   * role, wait for the appropriate `actors.Staked` event to be emitted.
-   */
-  async waitForRole(roleAccountId, role)
-  {
-    if (await this.checkForRole(roleAccountId, role)) {
-      return true;
-    }
-
-    return new Promise((resolve, reject) => {
-      this.base.waitForEvent('actors', 'Staked').then((values) => {
-        const name = values[0][0];
-        const payload = values[0][1];
-
-        if (payload.AccountId == roleAccountId) {
-          resolve(true);
-        } else {
-          // reject() ?
-        }
-      });
-    });
-  }
-}
-
-module.exports = {
-  RolesApi: RolesApi,
-}

+ 1 - 2
storage-node/packages/runtime-api/test/assets.js

@@ -22,7 +22,7 @@ const mocha = require('mocha');
 const expect = require('chai').expect;
 const expect = require('chai').expect;
 const sinon = require('sinon');
 const sinon = require('sinon');
 
 
-const { RuntimeApi } = require('@joystream/runtime-api');
+const { RuntimeApi } = require('@joystream/storage-runtime-api');
 
 
 describe('Assets', () => {
 describe('Assets', () => {
   var api;
   var api;
@@ -47,6 +47,5 @@ describe('Assets', () => {
   it('can accept content');
   it('can accept content');
   it('can reject content');
   it('can reject content');
   it('can create a storage relationship for content');
   it('can create a storage relationship for content');
-  it('can create a storage relationship for content and return it');
   it('can toggle a storage relatsionship to ready state');
   it('can toggle a storage relatsionship to ready state');
 });
 });

+ 1 - 4
storage-node/packages/runtime-api/test/balances.js

@@ -22,7 +22,7 @@ const mocha = require('mocha');
 const expect = require('chai').expect;
 const expect = require('chai').expect;
 const sinon = require('sinon');
 const sinon = require('sinon');
 
 
-const { RuntimeApi } = require('@joystream/runtime-api');
+const { RuntimeApi } = require('@joystream/storage-runtime-api');
 
 
 describe('Balances', () => {
 describe('Balances', () => {
   var api;
   var api;
@@ -49,7 +49,4 @@ describe('Balances', () => {
     // >= 0 comparison works
     // >= 0 comparison works
     expect(fee.cmpn(0)).to.be.at.least(0);
     expect(fee.cmpn(0)).to.be.at.least(0);
   });
   });
-
-  // TODO implemtable only with accounts with balance
-  it('can transfer funds');
 });
 });

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

@@ -23,7 +23,7 @@ const expect = require('chai').expect;
 const sinon = require('sinon');
 const sinon = require('sinon');
 const temp = require('temp').track();
 const temp = require('temp').track();
 
 
-const { RuntimeApi } = require('@joystream/runtime-api');
+const { RuntimeApi } = require('@joystream/storage-runtime-api');
 
 
 describe('Identities', () => {
 describe('Identities', () => {
   var api;
   var api;
@@ -31,13 +31,6 @@ describe('Identities', () => {
     api = await RuntimeApi.create({ canPromptForPassphrase: true });
     api = await RuntimeApi.create({ canPromptForPassphrase: true });
   });
   });
 
 
-  it('creates role keys', async () => {
-    const key = await api.identities.createRoleKey('foo', 'bar');
-    expect(key).to.have.property('type', 'ed25519');
-    expect(key.meta.name).to.include('foo');
-    expect(key.meta.name).to.include('bar');
-  });
-
   it('imports keys', async () => {
   it('imports keys', async () => {
     // Unlocked keys can be imported without asking for a passphrase
     // Unlocked keys can be imported without asking for a passphrase
     await api.identities.loadUnlock('test/data/edwards_unlocked.json');
     await api.identities.loadUnlock('test/data/edwards_unlocked.json');

+ 1 - 1
storage-node/packages/runtime-api/test/index.js

@@ -21,7 +21,7 @@
 const mocha = require('mocha');
 const mocha = require('mocha');
 const expect = require('chai').expect;
 const expect = require('chai').expect;
 
 
-const { RuntimeApi } = require('@joystream/runtime-api');
+const { RuntimeApi } = require('@joystream/storage-runtime-api');
 
 
 describe('RuntimeApi', () => {
 describe('RuntimeApi', () => {
   it('can be created', async () => {
   it('can be created', async () => {

+ 0 - 67
storage-node/packages/runtime-api/test/roles.js

@@ -1,67 +0,0 @@
-/*
- * This file is part of the storage node for the Joystream project.
- * Copyright (C) 2019 Joystream Contributors
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <https://www.gnu.org/licenses/>.
- */
-
-'use strict';
-
-const mocha = require('mocha');
-const expect = require('chai').expect;
-const sinon = require('sinon');
-
-const { RuntimeApi } = require('@joystream/runtime-api');
-
-describe('Roles', () => {
-  var api;
-  var key;
-  before(async () => {
-    api = await RuntimeApi.create();
-    key = await api.identities.loadUnlock('test/data/edwards_unlocked.json');
-  });
-
-  it('returns the required balance for role staking', async () => {
-    const amount = await api.roles.requiredBalanceForRoleStaking(api.roles.ROLE_STORAGE);
-
-    // Effectively checks that the role is at least defined.
-    expect(amount.cmpn(0)).to.be.above(0);
-  });
-
-  it('returns whether an account has funds for role staking', async () => {
-    expect(await api.roles.hasBalanceForRoleStaking(key.address, api.roles.ROLE_STORAGE)).to.be.false;
-  });
-
-  it('returns accounts for a role', async () => {
-    const accounts = await api.roles.accountIdsByRole(api.roles.ROLE_STORAGE);
-    // The chain may have accounts configured, so go for the bare minimum in
-    // expectations.
-    expect(accounts).to.have.lengthOf.above(-1);
-  });
-
-  it('can check whether an account fulfils requirements for role staking', async () => {
-    expect(async _ => {
-      await api.roles.checkAccountForRoleStaking(key.address, api.roles.ROLE_STORAGE);
-    }).to.throw;
-  });
-
-  it('can check for an account to have a role', async () => {
-    expect(await api.roles.checkForRole(key.address, api.roles.ROLE_STORAGE)).to.be.false;
-  });
-
-  // TODO requires complex setup, and may change in the near future.
-  it('transfers funds for staking');
-  it('can apply for a role');
-  it('can wait for an account to have a role');
-});

+ 298 - 0
storage-node/packages/runtime-api/workers.js

@@ -0,0 +1,298 @@
+/*
+ * This file is part of the storage node for the Joystream project.
+ * Copyright (C) 2019 Joystream Contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+'use strict'
+
+const debug = require('debug')('joystream:runtime:roles')
+const BN = require('bn.js')
+const { Worker } = require('@joystream/types/working-group')
+
+/*
+ * Add worker related functionality to the substrate API.
+ */
+class WorkersApi {
+  static async create (base) {
+    const ret = new WorkersApi()
+    ret.base = base
+    await ret.init()
+    return ret
+  }
+
+
+  // eslint-disable-next-line class-methods-use-this, require-await
+  async init () {
+    debug('Init')
+  }
+
+  /*
+   * Check whether the given account and id represent an enrolled storage provider
+   */
+  async isRoleAccountOfStorageProvider (storageProviderId, roleAccountId) {
+    const id = new BN(storageProviderId)
+    const roleAccount = this.base.identities.keyring.decodeAddress(roleAccountId)
+    const providerAccount = await this.storageProviderRoleAccount(id)
+    return providerAccount && providerAccount.eq(roleAccount)
+  }
+
+  /*
+   * Returns true if the provider id is enrolled
+   */
+  async isStorageProvider (storageProviderId) {
+    const worker = await this.storageWorkerByProviderId(storageProviderId)
+    return worker !== null
+  }
+
+  /*
+   * Returns a provider's role account or null if provider doesn't exist
+   */
+  async storageProviderRoleAccount (storageProviderId) {
+    const worker = await this.storageWorkerByProviderId(storageProviderId)
+    return worker ? worker.role_account_id : null
+  }
+
+  /*
+   * Returns a Worker instance or null if provider does not exist
+   */
+  async storageWorkerByProviderId (storageProviderId) {
+    const id = new BN(storageProviderId)
+    const { providers } = await this.getAllProviders()
+    return providers[id.toNumber()] || null
+  }
+
+  /*
+   * Returns the the first found provider id with a role account or null if not found
+   */
+  async findProviderIdByRoleAccount (roleAccount) {
+    const { ids, providers } = await this.getAllProviders()
+
+    for (let i = 0; i < ids.length; i++) {
+      const id = ids[i]
+      if (providers[id].role_account_id.eq(roleAccount)) {
+        return id
+      }
+    }
+
+    return null
+  }
+
+  /*
+   * Returns the set of ids and Worker instances of providers enrolled on the network
+   */
+  async getAllProviders () {
+    // const workerEntries = await this.base.api.query.storageWorkingGroup.workerById()
+    // can't rely on .isEmpty or isNone property to detect empty map
+    // return workerEntries.isNone ? [] : workerEntries[0]
+    // return workerEntries.isEmpty ? [] : workerEntries[0]
+    // So we iterate over possible ids which may or may not exist, by reading directly
+    // from storage value
+    const nextWorkerId = (await this.base.api.query.storageWorkingGroup.nextWorkerId()).toNumber()
+    const ids = []
+    const providers = {}
+    for (let id = 0; id < nextWorkerId; id++) {
+      // We get back an Option. Will be None if value doesn't exist
+      // eslint-disable-next-line no-await-in-loop
+      let value = await this.base.api.rpc.state.getStorage(
+        this.base.api.query.storageWorkingGroup.workerById.key(id)
+      )
+
+      if (!value.isNone) {
+        // no need to read from storage again!
+        // const worker = (await this.base.api.query.storageWorkingGroup.workerById(id))[0]
+        value = value.unwrap()
+        // construct the Worker type from raw data
+        // const worker = createType('WorkerOf', value)
+        // const worker = new Worker(value)
+        ids.push(id)
+        providers[id] = new Worker(value)
+      }
+    }
+
+    return { ids, providers }
+  }
+
+  async getLeadRoleAccount() {
+    const currentLead = await this.base.api.query.storageWorkingGroup.currentLead()
+    if (currentLead.isSome) {
+      const leadWorkerId = currentLead.unwrap()
+      const worker = await this.base.api.query.storageWorkingGroup.workerById(leadWorkerId)
+      return worker[0].role_account_id
+    }
+    return null
+  }
+
+  // Helper methods below don't really belong in the colossus runtime api library.
+  // They are only used by the dev-init command in the cli to setup a development environment
+
+  /*
+   * Add a new storage group opening using the lead account. Returns the
+   * new opening id.
+   */
+  async dev_addStorageOpening() {
+    const openTx = this.dev_makeAddOpeningTx('Worker')
+    return this.dev_submitAddOpeningTx(openTx, await this.getLeadRoleAccount())
+  }
+
+  /*
+   * Add a new storage working group lead opening using sudo account. Returns the
+   * new opening id.
+   */
+  async dev_addStorageLeadOpening() {
+    const openTx = this.dev_makeAddOpeningTx('Leader')
+    const sudoTx = this.base.api.tx.sudo.sudo(openTx)
+    return this.dev_submitAddOpeningTx(sudoTx, await this.base.identities.getSudoAccount())
+  }
+
+  /*
+   * Constructs an addOpening tx of openingType
+   */
+  dev_makeAddOpeningTx(openingType) {
+    return this.base.api.tx.storageWorkingGroup.addOpening(
+      'CurrentBlock',
+      {
+        application_rationing_policy: {
+          'max_active_applicants': 1
+        },
+        max_review_period_length: 1000
+        // default values for everything else..
+      },
+      'dev-opening',
+      openingType
+    )
+  }
+
+  /*
+   * Submits a tx (expecting it to dispatch storageWorkingGroup.addOpening) and returns
+   * the OpeningId from the resulting event.
+   */
+  async dev_submitAddOpeningTx(tx, senderAccount) {
+    return this.base.signAndSendThenGetEventResult(senderAccount, tx, {
+      eventModule: 'storageWorkingGroup',
+      eventName: 'OpeningAdded',
+      eventProperty: 'OpeningId'
+    })
+  }
+
+  /*
+   * Apply on an opening, returns the application id.
+   */
+  async dev_applyOnOpening(openingId, memberId, memberAccount, roleAccount) {
+    const applyTx = this.base.api.tx.storageWorkingGroup.applyOnOpening(
+      memberId, openingId, roleAccount, null, null, `colossus-${memberId}`
+    )
+
+    return this.base.signAndSendThenGetEventResult(memberAccount, applyTx, {
+      eventModule: 'storageWorkingGroup',
+      eventName: 'AppliedOnOpening',
+      eventProperty: 'ApplicationId'
+    })
+  }
+
+  /*
+   * Move lead opening to review state using sudo account
+   */
+  async dev_beginLeadOpeningReview(openingId) {
+    const beginReviewTx = this.dev_makeBeginOpeningReviewTx(openingId)
+    const sudoTx = this.base.api.tx.sudo.sudo(beginReviewTx)
+    return this.base.signAndSend(await this.base.identities.getSudoAccount(), sudoTx)
+  }
+
+  /*
+   * Move a storage opening to review state using lead account
+   */
+  async dev_beginStorageOpeningReview(openingId) {
+    const beginReviewTx = this.dev_makeBeginOpeningReviewTx(openingId)
+    return this.base.signAndSend(await this.getLeadRoleAccount(), beginReviewTx)
+  }
+
+  /*
+   * Constructs a beingApplicantReview tx for openingId, which puts an opening into the review state
+   */
+  dev_makeBeginOpeningReviewTx(openingId) {
+    return this.base.api.tx.storageWorkingGroup.beginApplicantReview(openingId)
+  }
+
+  /*
+   * Fill a lead opening, return the assigned worker id, using the sudo account
+   */
+  async dev_fillLeadOpening(openingId, applicationId) {
+    const fillTx = this.dev_makeFillOpeningTx(openingId, applicationId)
+    const sudoTx = this.base.api.tx.sudo.sudo(fillTx)
+    const filled = await this.dev_submitFillOpeningTx(
+      await this.base.identities.getSudoAccount(), sudoTx)
+    return getWorkerIdFromApplicationIdToWorkerIdMap(filled, applicationId)
+  }
+
+  /*
+   * Fill a storage opening, return the assigned worker id, using the lead account
+   */
+  async dev_fillStorageOpening(openingId, applicationId) {
+    const fillTx = this.dev_makeFillOpeningTx(openingId, applicationId)
+    const filled = await this.dev_submitFillOpeningTx(await this.getLeadRoleAccount(), fillTx)
+    return getWorkerIdFromApplicationIdToWorkerIdMap(filled, applicationId)
+  }
+
+  /*
+   * Constructs a FillOpening transaction
+   */
+  dev_makeFillOpeningTx(openingId, applicationId) {
+    return this.base.api.tx.storageWorkingGroup.fillOpening(openingId, [applicationId], null)
+  }
+
+  /*
+   * Dispatches a fill opening tx and returns a map of the application id to their new assigned worker ids.
+   */
+  async dev_submitFillOpeningTx(senderAccount, tx) {
+    return this.base.signAndSendThenGetEventResult(senderAccount, tx, {
+      eventModule: 'storageWorkingGroup',
+      eventName: 'OpeningFilled',
+      eventProperty: 'ApplicationIdToWorkerIdMap'
+    })
+  }
+}
+
+/*
+ * Finds assigned worker id corresponding to the application id from the resulting
+ * ApplicationIdToWorkerIdMap map in the OpeningFilled event. Expects map to
+ * contain at least one entry.
+ */
+function getWorkerIdFromApplicationIdToWorkerIdMap (filledMap, applicationId) {
+  if (filledMap.size === 0) {
+    throw new Error('Expected opening to be filled!')
+  }
+
+  let ourApplicationIdKey
+
+  for (let key of filledMap.keys()) {
+    if (key.eq(applicationId)) {
+      ourApplicationIdKey = key
+      break
+    }
+  }
+
+  if (!ourApplicationIdKey) {
+    throw new Error('Expected application id to have been filled!')
+  }
+
+  const workerId = filledMap.get(ourApplicationIdKey)
+
+  return workerId
+}
+
+module.exports = {
+  WorkersApi
+}

+ 0 - 3
storage-node/packages/storage/README.md

@@ -2,9 +2,6 @@
 
 
 This package contains an abstraction over the storage backend of colossus.
 This package contains an abstraction over the storage backend of colossus.
 
 
-Its main purpose is to allow testing the storage subsystem without having to
-run a blockchain node.
-
 In the current version, the storage is backed by IPFS. In order to run tests,
 In the current version, the storage is backed by IPFS. In order to run tests,
 you have to also run an [IPFS node](https://dist.ipfs.io/#go-ipfs).
 you have to also run an [IPFS node](https://dist.ipfs.io/#go-ipfs).
 
 

+ 2 - 1
storage-node/packages/storage/package.json

@@ -1,5 +1,6 @@
 {
 {
-  "name": "@joystream/storage",
+  "name": "@joystream/storage-node-backend",
+  "private": true,
   "version": "0.1.0",
   "version": "0.1.0",
   "description": "Storage management code for Joystream Storage Node",
   "description": "Storage management code for Joystream Storage Node",
   "author": "Joystream",
   "author": "Joystream",

+ 3 - 0
storage-node/packages/storage/storage.js

@@ -383,6 +383,8 @@ class Storage
   {
   {
     const resolved = await this._resolve_content_id_with_timeout(this._timeout, content_id);
     const resolved = await this._resolve_content_id_with_timeout(this._timeout, content_id);
 
 
+    // validate resolved id is proper ipfs_cid, not null or empty string
+
     if (this.pins[resolved]) {
     if (this.pins[resolved]) {
       return;
       return;
     }
     }
@@ -396,6 +398,7 @@ class Storage
         delete this.pins[resolved];
         delete this.pins[resolved];
       } else {
       } else {
         debug(`Pinned ${resolved}`);
         debug(`Pinned ${resolved}`);
+        // why aren't we doing this.pins[resolved] = true
       }
       }
     });
     });
   }
   }

+ 48 - 55
storage-node/packages/storage/test/storage.js

@@ -26,7 +26,7 @@ const expect = chai.expect;
 
 
 const fs = require('fs');
 const fs = require('fs');
 
 
-const { Storage } = require('@joystream/storage');
+const { Storage } = require('@joystream/storage-node-backend');
 
 
 const IPFS_CID_REGEX = /^Qm[1-9A-HJ-NP-Za-km-z]{44}$/;
 const IPFS_CID_REGEX = /^Qm[1-9A-HJ-NP-Za-km-z]{44}$/;
 
 
@@ -40,28 +40,27 @@ function write(store, content_id, contents, callback)
       });
       });
       stream.on('committed', callback);
       stream.on('committed', callback);
 
 
-      stream.write(contents);
+      if (!stream.write(contents)) {
-      stream.end();
+        stream.once('drain', () => stream.end())
+      } else {
+        process.nextTick(() => stream.end())
+      }
     })
     })
     .catch((err) => {
     .catch((err) => {
       expect.fail(err);
       expect.fail(err);
     });
     });
 }
 }
 
 
-function read_all(stream)
+function read_all (stream) {
-{
+  return new Promise((resolve, reject) => {
-  const chunks = []
+    const chunks = []
-  let chunk
+    stream.on('data', chunk => chunks.push(chunk))
-  do {
+    stream.on('end', () => resolve(Buffer.concat(chunks)))
-    chunk = stream.read();
+    stream.on('error', err => reject(err))
-    if (chunk) {
+    stream.resume()
-        chunks.push(chunk)
+  })
-    }
-  } while (chunk);
-  return Buffer.concat(chunks);
 }
 }
 
 
-
 function create_known_object(content_id, contents, callback)
 function create_known_object(content_id, contents, callback)
 {
 {
   var hash;
   var hash;
@@ -96,45 +95,43 @@ describe('storage/storage', () => {
 
 
     it('detects the MIME type of a write stream', (done) => {
     it('detects the MIME type of a write stream', (done) => {
       const contents = fs.readFileSync('../../storage-node_new.svg');
       const contents = fs.readFileSync('../../storage-node_new.svg');
+      storage.open('mime-test', 'w')
+        .then((stream) => {
+          var file_info;
+          stream.on('file_info', (info) => {
+            // Could filter & abort here now, but we're just going to set this,
+            // and expect it to be set later...
+            file_info = info;
+          });
 
 
-      create_known_object('foobar', contents, (store, hash) => {
+          stream.on('finish', () => {
-        var file_info;
+            stream.commit();
-        store.open('mime-test', 'w')
+          });
-          .then((stream) => {
+
-
+          stream.on('committed', (hash) => {
-            stream.on('file_info', (info) => {
+            // ... if file_info is not set here, there's an issue.
-              // Could filter & abort here now, but we're just going to set this,
+            expect(file_info).to.have.property('mime_type', 'application/xml');
-              // and expect it to be set later...
+            expect(file_info).to.have.property('ext', 'xml');
-              file_info = info;
+            done();
-            });
-
-            stream.on('finish', () => {
-              stream.commit();
-            });
-            stream.on('committed', (hash) => {
-              // ... if file_info is not set here, there's an issue.
-              expect(file_info).to.have.property('mime_type', 'application/xml');
-              expect(file_info).to.have.property('ext', 'xml');
-
-              done();
-            });
-
-            stream.write(contents);
-            stream.end();
-          })
-          .catch((err) => {
-            expect.fail(err);
           });
           });
-      });
 
 
+          if (!stream.write(contents)) {
+            stream.once('drain', () => stream.end())
+          } else {
+            process.nextTick(() => stream.end())
+          }
+        })
+        .catch((err) => {
+          expect.fail(err);
+        });
     });
     });
 
 
     it('can read a stream', (done) => {
     it('can read a stream', (done) => {
       const contents = 'test-for-reading';
       const contents = 'test-for-reading';
       create_known_object('foobar', contents, (store, hash) => {
       create_known_object('foobar', contents, (store, hash) => {
         store.open('foobar', 'r')
         store.open('foobar', 'r')
-          .then((stream) => {
+          .then(async (stream) => {
-            const data = read_all(stream);
+            const data = await read_all(stream);
             expect(Buffer.compare(data, Buffer.from(contents))).to.equal(0);
             expect(Buffer.compare(data, Buffer.from(contents))).to.equal(0);
             done();
             done();
           })
           })
@@ -144,16 +141,12 @@ describe('storage/storage', () => {
       });
       });
     });
     });
 
 
-    // Problems with this test. reading the stream is stalling, so we are
+    it('detects the MIME type of a read stream', (done) => {
-    // not always able to read the full stream for the test to make sense
-    // Disabling for now. Look at readl_all() implementation.. maybe that
-    // is where the fault is?
-    xit('detects the MIME type of a read stream', (done) => {
       const contents = fs.readFileSync('../../storage-node_new.svg');
       const contents = fs.readFileSync('../../storage-node_new.svg');
       create_known_object('foobar', contents, (store, hash) => {
       create_known_object('foobar', contents, (store, hash) => {
         store.open('foobar', 'r')
         store.open('foobar', 'r')
-          .then((stream) => {
+          .then(async (stream) => {
-            const data = read_all(stream);
+            const data = await read_all(stream);
             expect(contents.length).to.equal(data.length);
             expect(contents.length).to.equal(data.length);
             expect(Buffer.compare(data, contents)).to.equal(0);
             expect(Buffer.compare(data, contents)).to.equal(0);
             expect(stream).to.have.property('file_info');
             expect(stream).to.have.property('file_info');
@@ -173,8 +166,8 @@ describe('storage/storage', () => {
       const contents = 'test-for-reading';
       const contents = 'test-for-reading';
       create_known_object('foobar', contents, (store, hash) => {
       create_known_object('foobar', contents, (store, hash) => {
         store.open('foobar', 'r')
         store.open('foobar', 'r')
-          .then((stream) => {
+          .then(async (stream) => {
-            const data = read_all(stream);
+            const data = await read_all(stream);
             expect(Buffer.compare(data, Buffer.from(contents))).to.equal(0);
             expect(Buffer.compare(data, Buffer.from(contents))).to.equal(0);
 
 
             expect(stream.file_info).to.have.property('mime_type', 'application/octet-stream');
             expect(stream.file_info).to.have.property('mime_type', 'application/octet-stream');
@@ -203,7 +196,7 @@ describe('storage/storage', () => {
     it('returns stats for a known object', (done) => {
     it('returns stats for a known object', (done) => {
       const content = 'stat-test';
       const content = 'stat-test';
       const expected_size = content.length;
       const expected_size = content.length;
-      create_known_object('foobar', 'stat-test', (store, hash) => {
+      create_known_object('foobar', content, (store, hash) => {
         expect(store.stat(hash)).to.eventually.have.property('size', expected_size);
         expect(store.stat(hash)).to.eventually.have.property('size', expected_size);
         done();
         done();
       });
       });

+ 0 - 0
storage-node/packages/storage/test/template/bar


+ 0 - 0
storage-node/packages/storage/test/template/foo/baz


+ 0 - 1
storage-node/packages/storage/test/template/quux

@@ -1 +0,0 @@
-foo/baz

+ 19 - 0
storage-node/packages/util/externalPromise.js

@@ -0,0 +1,19 @@
+/**
+ * Returns an object that contains a Promise and exposes its handlers, ie. resolve and reject methods
+ * so it can be fulfilled 'externally'. This is a bit of a hack, but most useful application is when
+ * concurrent async operations are initiated that are all waiting on the same result value.
+ */
+function newExternallyControlledPromise () {
+    let resolve, reject
+
+    const promise = new Promise((res, rej) => {
+      resolve = res
+      reject = rej
+    })
+
+    return ({ resolve, reject, promise })
+}
+
+module.exports = {
+    newExternallyControlledPromise
+}

+ 2 - 1
storage-node/packages/util/package.json

@@ -1,5 +1,6 @@
 {
 {
-  "name": "@joystream/util",
+  "name": "@joystream/storage-utils",
+  "private": true,
   "version": "0.1.0",
   "version": "0.1.0",
   "description": "Utility code for Joystream Storage Node",
   "description": "Utility code for Joystream Storage Node",
   "author": "Joystream",
   "author": "Joystream",

+ 1 - 1
storage-node/packages/util/test/fs/resolve.js

@@ -22,7 +22,7 @@ const mocha = require('mocha');
 const expect = require('chai').expect;
 const expect = require('chai').expect;
 const path = require('path');
 const path = require('path');
 
 
-const resolve = require('@joystream/util/fs/resolve');
+const resolve = require('@joystream/storage-utils/fs/resolve');
 
 
 function tests(base)
 function tests(base)
 {
 {

+ 1 - 1
storage-node/packages/util/test/fs/walk.js

@@ -25,7 +25,7 @@ const temp = require('temp').track();
 const fs = require('fs');
 const fs = require('fs');
 const path = require('path');
 const path = require('path');
 
 
-const fswalk = require('@joystream/util/fs/walk');
+const fswalk = require('@joystream/storage-utils/fs/walk');
 
 
 function walktest(archive, base, done)
 function walktest(archive, base, done)
 {
 {

+ 1 - 1
storage-node/packages/util/test/lru.js

@@ -21,7 +21,7 @@
 const mocha = require('mocha');
 const mocha = require('mocha');
 const expect = require('chai').expect;
 const expect = require('chai').expect;
 
 
-const lru = require('@joystream/util/lru');
+const lru = require('@joystream/storage-utils/lru');
 
 
 const DEFAULT_SLEEP = 1;
 const DEFAULT_SLEEP = 1;
 function sleep(ms = DEFAULT_SLEEP)
 function sleep(ms = DEFAULT_SLEEP)

+ 1 - 1
storage-node/packages/util/test/pagination.js

@@ -22,7 +22,7 @@ const mocha = require('mocha');
 const expect = require('chai').expect;
 const expect = require('chai').expect;
 const mock_http = require('node-mocks-http');
 const mock_http = require('node-mocks-http');
 
 
-const pagination = require('@joystream/util/pagination');
+const pagination = require('@joystream/storage-utils/pagination');
 
 
 describe('util/pagination', function()
 describe('util/pagination', function()
 {
 {

+ 1 - 1
storage-node/packages/util/test/ranges.js

@@ -23,7 +23,7 @@ const expect = require('chai').expect;
 const mock_http = require('node-mocks-http');
 const mock_http = require('node-mocks-http');
 const stream_buffers = require('stream-buffers');
 const stream_buffers = require('stream-buffers');
 
 
-const ranges = require('@joystream/util/ranges');
+const ranges = require('@joystream/storage-utils/ranges');
 
 
 describe('util/ranges', function()
 describe('util/ranges', function()
 {
 {

+ 10 - 6
storage-node/scripts/compose/devchain-and-ipfs-node/docker-compose.yaml

@@ -3,14 +3,18 @@ services:
   ipfs:
   ipfs:
     image: ipfs/go-ipfs:latest
     image: ipfs/go-ipfs:latest
     ports:
     ports:
-      - "5001:5001"
+      - "127.0.0.1:5001:5001"
     volumes:
     volumes:
-      - storage-node-shared-data:/data/ipfs
+      - ipfs-data:/data/ipfs
   chain:
   chain:
-    image: joystream/node:2.1.2
+    image: joystream/node:latest
     ports:
     ports:
-      - "9944:9944"
+      - "127.0.0.1:9944:9944"
-    command: --dev --ws-external
+    volumes:
+      - chain-data:/data
+    command: --dev --ws-external --base-path /data
 volumes:
 volumes:
-  storage-node-shared-data:
+  ipfs-data:
+    driver: local
+  chain-data:
     driver: local
     driver: local

+ 39 - 0
storage-node/scripts/run-dev-instance.sh

@@ -0,0 +1,39 @@
+#!/usr/bin/env bash
+set -e
+
+# Avoid pulling joystream/node from docker hub. It is most likely
+# not the version that we want to work with. Either you should
+# build it locally or pull it down manually if you that is what you want
+if ! docker inspect joystream/node:latest > /dev/null 2>&1;
+then
+  echo "Didn't find local joystream/node:latest docker image."
+  exit 1
+fi
+
+SCRIPT_PATH="$(dirname "${BASH_SOURCE[0]}")"
+
+# stop prior run and clear volumes
+docker-compose -f ${SCRIPT_PATH}/compose/devchain-and-ipfs-node/docker-compose.yaml down -v
+
+# Run a development joystream-node chain and ipfs daemon in the background
+# Will use latest joystream/node images,
+# and will fetch from dockerhub if not found, so build them locally if
+# you need the version from the current branch
+docker-compose -f ${SCRIPT_PATH}/compose/devchain-and-ipfs-node/docker-compose.yaml up -d
+
+# configure the dev chain
+DEBUG=joystream:storage-cli:dev yarn storage-cli dev-init
+
+# Run the tests
+yarn workspace storage-node test
+
+# Run the server in background
+# DEBUG=joystream:storage* yarn colossus --dev > ${SCRIPT_PATH}/colossus.log 2>&1 &
+# PID= ???
+# echo "Development storage node is running in the background process id: ${PID}""
+# prompt for pressing ctrl-c..
+# kill colossus and docker containers...
+# docker-compose -f ${SCRIPT_PATH}/compose/devchain-and-ipfs-node/docker-compose.yaml down -v
+
+# Run the server
+DEBUG=joystream:* yarn colossus --dev

+ 7 - 0
storage-node/scripts/stop-dev-instance.sh

@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+set -e
+
+script_path="$(dirname "${BASH_SOURCE[0]}")"
+
+# stop prior run and clear volumes
+docker-compose -f ${script_path}/compose/devchain-and-ipfs-node/docker-compose.yaml down -v

+ 10 - 2
types/src/common.ts

@@ -1,4 +1,4 @@
-import { Struct, Option, Text, bool, Vec, u16, u32, u64, getTypeRegistry } from "@polkadot/types";
+import { Struct, Option, Text, bool, Vec, u16, u32, u64, getTypeRegistry, Enum, Null } from "@polkadot/types";
 import { BlockNumber, Moment } from '@polkadot/types/interfaces';
 import { BlockNumber, Moment } from '@polkadot/types/interfaces';
 import { Codec } from "@polkadot/types/types";
 import { Codec } from "@polkadot/types/types";
 // we get 'moment' because it is a dependency of @polkadot/util, via @polkadot/keyring
 // we get 'moment' because it is a dependency of @polkadot/util, via @polkadot/keyring
@@ -107,6 +107,13 @@ export class InputValidationLengthConstraint extends JoyStruct<InputValidationLe
     }
     }
 }
 }
 
 
+// TODO: Replace with JoyEnum
+export const WorkingGroupDef = {
+  Storage: Null
+} as const;
+export type WorkingGroupKeys = keyof typeof WorkingGroupDef;
+export class WorkingGroup extends Enum.with(WorkingGroupDef) { };
+
 export function registerCommonTypes() {
 export function registerCommonTypes() {
     const typeRegistry = getTypeRegistry();
     const typeRegistry = getTypeRegistry();
 
 
@@ -117,6 +124,7 @@ export function registerCommonTypes() {
       ThreadId,
       ThreadId,
       PostId,
       PostId,
       InputValidationLengthConstraint,
       InputValidationLengthConstraint,
-      BTreeSet // Is this even necessary?
+      BTreeSet, // Is this even necessary?
+      WorkingGroup
     });
     });
 }
 }

+ 3 - 19
types/src/content-working-group/index.ts

@@ -1,5 +1,5 @@
-import { getTypeRegistry, BTreeMap, Enum, bool, u8, u32, u128, Text, GenericAccountId, Null , Option, Vec, u16 } from '@polkadot/types';
+import { getTypeRegistry, BTreeMap, Enum, bool, u8, u32, Text, GenericAccountId, Null , Option, Vec, u16 } from '@polkadot/types';
-import { BlockNumber, AccountId, Balance } from '@polkadot/types/interfaces';
+import { BlockNumber, AccountId } from '@polkadot/types/interfaces';
 import { BTreeSet, JoyStruct, OptionText, Credential } from '../common';
 import { BTreeSet, JoyStruct, OptionText, Credential } from '../common';
 import { ActorId, MemberId } from '../members';
 import { ActorId, MemberId } from '../members';
 import { StakeId } from '../stake';
 import { StakeId } from '../stake';
@@ -526,21 +526,6 @@ export class CuratorApplicationIdToCuratorIdMap extends BTreeMap<ApplicationId,
   }
   }
 }
 }
 
 
-export type IRewardPolicy = {
-  amount_per_payout: Balance,
-  next_payment_at_block: BlockNumber,
-  payout_interval: Option<BlockNumber>,
-};
-export class RewardPolicy extends JoyStruct<IRewardPolicy> {
-  constructor (value?: IRewardPolicy) {
-    super({
-      amount_per_payout: u128,
-      next_payment_at_block: u32,
-      payout_interval: Option.with(u32),
-    }, value);
-  }
-};
-
 export function registerContentWorkingGroupTypes () {
 export function registerContentWorkingGroupTypes () {
   try {
   try {
     getTypeRegistry().register({
     getTypeRegistry().register({
@@ -564,8 +549,7 @@ export function registerContentWorkingGroupTypes () {
       Principal,
       Principal,
       WorkingGroupUnstaker,
       WorkingGroupUnstaker,
       CuratorApplicationIdToCuratorIdMap,
       CuratorApplicationIdToCuratorIdMap,
-      CuratorApplicationIdSet: Vec.with(CuratorApplicationId),
+      CuratorApplicationIdSet: Vec.with(CuratorApplicationId)
-      RewardPolicy,
     });
     });
   } catch (err) {
   } catch (err) {
     console.error('Failed to register custom types of content working group module', err);
     console.error('Failed to register custom types of content working group module', err);

+ 129 - 3
types/src/proposals.ts

@@ -1,11 +1,14 @@
 import { Text, u32, Enum, getTypeRegistry, Tuple, GenericAccountId, u8, Vec, Option, Struct, Null, Bytes } from "@polkadot/types";
 import { Text, u32, Enum, getTypeRegistry, Tuple, GenericAccountId, u8, Vec, Option, Struct, Null, Bytes } from "@polkadot/types";
+import { bool } from "@polkadot/types/primitive";
 import { BlockNumber, Balance } from "@polkadot/types/interfaces";
 import { BlockNumber, Balance } from "@polkadot/types/interfaces";
 import AccountId from "@polkadot/types/primitive/Generic/AccountId";
 import AccountId from "@polkadot/types/primitive/Generic/AccountId";
-import { ThreadId, JoyStruct } from "./common";
+import { ThreadId, JoyStruct, WorkingGroup } from "./common";
 import { MemberId } from "./members";
 import { MemberId } from "./members";
 import { RoleParameters } from "./roles";
 import { RoleParameters } from "./roles";
 import { StakeId } from "./stake";
 import { StakeId } from "./stake";
 import { ElectionParameters } from "./council";
 import { ElectionParameters } from "./council";
+import { ActivateOpeningAt, OpeningId, ApplicationId } from "./hiring";
+import { WorkingGroupOpeningPolicyCommitment, WorkerId, RewardPolicy } from "./working-group";
 
 
 export type IVotingResults = {
 export type IVotingResults = {
   abstensions: u32;
   abstensions: u32;
@@ -284,7 +287,15 @@ export class ProposalDetails extends Enum {
         SetContentWorkingGroupMintCapacity: "Balance",
         SetContentWorkingGroupMintCapacity: "Balance",
         EvictStorageProvider: "AccountId",
         EvictStorageProvider: "AccountId",
         SetValidatorCount: "u32",
         SetValidatorCount: "u32",
-        SetStorageRoleParameters: RoleParameters
+        SetStorageRoleParameters: RoleParameters,
+        AddWorkingGroupLeaderOpening: AddOpeningParameters,
+        BeginReviewWorkingGroupLeaderApplication: Tuple.with([OpeningId, WorkingGroup]),
+        FillWorkingGroupLeaderOpening: FillOpeningParameters,
+        SetWorkingGroupMintCapacity: Tuple.with(["Balance", WorkingGroup]),
+        DecreaseWorkingGroupLeaderStake: Tuple.with([WorkerId, "Balance", WorkingGroup]),
+        SlashWorkingGroupLeaderStake: Tuple.with([WorkerId, "Balance", WorkingGroup]),
+        SetWorkingGroupLeaderReward: Tuple.with([WorkerId, "Balance", WorkingGroup]),
+        TerminateWorkingGroupLeaderRole: TerminateRoleParameters,
       },
       },
       value,
       value,
       index
       index
@@ -440,6 +451,117 @@ export class DiscussionPost extends Struct {
   }
   }
 }
 }
 
 
+export type IAddOpeningParameters = {
+  activate_at: ActivateOpeningAt;
+  commitment: WorkingGroupOpeningPolicyCommitment;
+  human_readable_text: Bytes;
+  working_group: WorkingGroup;
+};
+
+export class AddOpeningParameters extends JoyStruct<IAddOpeningParameters> {
+  constructor(value?: IAddOpeningParameters) {
+    super(
+      {
+        activate_at: ActivateOpeningAt,
+        commitment: WorkingGroupOpeningPolicyCommitment,
+        human_readable_text: Bytes,
+        working_group: WorkingGroup
+      },
+      value
+    );
+  }
+
+  get activate_at(): ActivateOpeningAt {
+    return this.getField<ActivateOpeningAt>('activate_at');
+  }
+
+  get commitment(): WorkingGroupOpeningPolicyCommitment {
+    return this.getField<WorkingGroupOpeningPolicyCommitment>('commitment');
+  }
+
+  get human_readable_text(): Bytes {
+    return this.getField<Bytes>('human_readable_text');
+  }
+
+  get working_group(): WorkingGroup {
+    return this.getField<WorkingGroup>('working_group');
+  }
+}
+
+export type IFillOpeningParameters = {
+  opening_id: OpeningId;
+  successful_application_id: ApplicationId;
+  reward_policy: Option<RewardPolicy>;
+  working_group: WorkingGroup;
+}
+
+export class FillOpeningParameters extends JoyStruct<IFillOpeningParameters> {
+  constructor(value?: IFillOpeningParameters) {
+    super(
+      {
+        opening_id: OpeningId,
+        successful_application_id: ApplicationId,
+        reward_policy: Option.with(RewardPolicy),
+        working_group: WorkingGroup,
+      },
+      value
+    );
+  }
+
+  get opening_id(): OpeningId {
+    return this.getField<OpeningId>('opening_id');
+  }
+
+  get successful_application_id(): ApplicationId {
+    return this.getField<ApplicationId>('successful_application_id');
+  }
+
+  get reward_policy(): Option<RewardPolicy> {
+    return this.getField<Option<RewardPolicy>>('reward_policy');
+  }
+
+  get working_group(): WorkingGroup {
+    return this.getField<WorkingGroup>('working_group');
+  }
+}
+
+export type ITerminateRoleParameters = {
+  worker_id: WorkerId;
+  rationale: Bytes;
+  slash: bool;
+  working_group: WorkingGroup;
+}
+
+export class TerminateRoleParameters extends JoyStruct<ITerminateRoleParameters> {
+  constructor(value?: ITerminateRoleParameters) {
+    super(
+      {
+        worker_id: WorkerId,
+        rationale: Bytes,
+        slash: bool,
+        working_group: WorkingGroup,
+      },
+      value
+    );
+  }
+
+  get worker_id(): WorkerId {
+    return this.getField<WorkerId>('worker_id');
+  }
+
+  get rationale(): Bytes {
+    return this.getField<Bytes>('rationale');
+  }
+
+  get slash(): bool {
+    return this.getField<bool>('slash');
+  }
+
+  get working_group(): WorkingGroup {
+    return this.getField<WorkingGroup>('working_group');
+  }
+}
+
 // export default proposalTypes;
 // export default proposalTypes;
 export function registerProposalTypes() {
 export function registerProposalTypes() {
   try {
   try {
@@ -448,12 +570,16 @@ export function registerProposalTypes() {
       ProposalStatus,
       ProposalStatus,
       ProposalOf: Proposal,
       ProposalOf: Proposal,
       ProposalDetails,
       ProposalDetails,
+      ProposalDetailsOf: ProposalDetails, // Runtime alias
       VotingResults,
       VotingResults,
       ProposalParameters,
       ProposalParameters,
       VoteKind,
       VoteKind,
       ThreadCounter,
       ThreadCounter,
       DiscussionThread,
       DiscussionThread,
-      DiscussionPost
+      DiscussionPost,
+      AddOpeningParameters,
+      FillOpeningParameters,
+      TerminateRoleParameters
     });
     });
   } catch (err) {
   } catch (err) {
     console.error("Failed to register custom types of proposals module", err);
     console.error("Failed to register custom types of proposals module", err);

+ 22 - 2
types/src/working-group/index.ts

@@ -1,6 +1,6 @@
 import { getTypeRegistry, Bytes, BTreeMap, Option, Enum } from '@polkadot/types';
 import { getTypeRegistry, Bytes, BTreeMap, Option, Enum } from '@polkadot/types';
 import { u16, Null } from '@polkadot/types/primitive';
 import { u16, Null } from '@polkadot/types/primitive';
-import { AccountId, BlockNumber } from '@polkadot/types/interfaces';
+import { AccountId, BlockNumber, Balance } from '@polkadot/types/interfaces';
 import { BTreeSet, JoyStruct } from '../common';
 import { BTreeSet, JoyStruct } from '../common';
 import { MemberId, ActorId } from '../members';
 import { MemberId, ActorId } from '../members';
 import { RewardRelationshipId } from '../recurring-rewards';
 import { RewardRelationshipId } from '../recurring-rewards';
@@ -297,6 +297,23 @@ export class Opening extends JoyStruct<IOpening> {
   }
   }
 }
 }
 
 
+// Also defined in "content-working-group" runtime module, but those definitions are the consistent
+export type IRewardPolicy = {
+  amount_per_payout: Balance,
+  next_payment_at_block: BlockNumber,
+  payout_interval: Option<BlockNumber>,
+};
+
+export class RewardPolicy extends JoyStruct<IRewardPolicy> {
+  constructor (value?: IRewardPolicy) {
+    super({
+      amount_per_payout: 'Balance',
+      next_payment_at_block: 'BlockNumber',
+      payout_interval: 'Option<BlockNumber>',
+    }, value);
+  }
+};
+
 export function registerWorkingGroupTypes() {
 export function registerWorkingGroupTypes() {
   try {
   try {
     getTypeRegistry().register({
     getTypeRegistry().register({
@@ -310,7 +327,10 @@ export function registerWorkingGroupTypes() {
       StorageProviderId,
       StorageProviderId,
       OpeningType,
       OpeningType,
       /// Alias used by the runtime working-group module
       /// Alias used by the runtime working-group module
-      HiringApplicationId: ApplicationId
+      HiringApplicationId: ApplicationId,
+      RewardPolicy,
+      'working_group::OpeningId': OpeningId,
+      'working_group::WorkerId': WorkerId
     });
     });
   } catch (err) {
   } catch (err) {
     console.error('Failed to register custom types of working-group module', err);
     console.error('Failed to register custom types of working-group module', err);

+ 119 - 14
yarn.lock

@@ -1361,10 +1361,12 @@
   resolved "https://registry.yarnpkg.com/@joystream/types/-/types-0.10.0.tgz#7e98ef221410b26a7d952cfc3d1c03d28395ad69"
   resolved "https://registry.yarnpkg.com/@joystream/types/-/types-0.10.0.tgz#7e98ef221410b26a7d952cfc3d1c03d28395ad69"
   integrity sha512-RDZizqGKWGYpLR5PnUWM4aGa7InpWNh2Txlr7Al3ROFYOHoyQf62/omPfEz29F6scwlFxysOdmEfQaLeVRaUxA==
   integrity sha512-RDZizqGKWGYpLR5PnUWM4aGa7InpWNh2Txlr7Al3ROFYOHoyQf62/omPfEz29F6scwlFxysOdmEfQaLeVRaUxA==
   dependencies:
   dependencies:
+    "@polkadot/keyring" "^1.7.0-beta.5"
     "@polkadot/types" "^0.96.1"
     "@polkadot/types" "^0.96.1"
     "@types/vfile" "^4.0.0"
     "@types/vfile" "^4.0.0"
     ajv "^6.11.0"
     ajv "^6.11.0"
     lodash "^4.17.15"
     lodash "^4.17.15"
+    moment "^2.24.0"
 
 
 "@csstools/convert-colors@^1.4.0":
 "@csstools/convert-colors@^1.4.0":
   version "1.4.0"
   version "1.4.0"
@@ -1737,17 +1739,6 @@
     "@types/istanbul-reports" "^1.1.1"
     "@types/istanbul-reports" "^1.1.1"
     "@types/yargs" "^13.0.0"
     "@types/yargs" "^13.0.0"
 
 
-"@joystream/types@./types", "@nicaea/types@./types":
-  version "0.11.0"
-  dependencies:
-    "@polkadot/keyring" "^1.7.0-beta.5"
-    "@polkadot/types" "^0.96.1"
-    "@types/lodash" "^4.14.157"
-    "@types/vfile" "^4.0.0"
-    ajv "^6.11.0"
-    lodash "^4.17.15"
-    moment "^2.24.0"
-
 "@ledgerhq/devices@^4.78.0":
 "@ledgerhq/devices@^4.78.0":
   version "4.78.0"
   version "4.78.0"
   resolved "https://registry.yarnpkg.com/@ledgerhq/devices/-/devices-4.78.0.tgz#149b572f0616096e2bd5eb14ce14d0061c432be6"
   resolved "https://registry.yarnpkg.com/@ledgerhq/devices/-/devices-4.78.0.tgz#149b572f0616096e2bd5eb14ce14d0061c432be6"
@@ -3845,6 +3836,11 @@
   resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
   resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
   integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==
   integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==
 
 
+"@types/minimist@^1.2.0":
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.0.tgz#69a23a3ad29caf0097f06eda59b361ee2f0639f6"
+  integrity sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY=
+
 "@types/mkdirp@^0.5.2":
 "@types/mkdirp@^0.5.2":
   version "0.5.2"
   version "0.5.2"
   resolved "https://registry.yarnpkg.com/@types/mkdirp/-/mkdirp-0.5.2.tgz#503aacfe5cc2703d5484326b1b27efa67a339c1f"
   resolved "https://registry.yarnpkg.com/@types/mkdirp/-/mkdirp-0.5.2.tgz#503aacfe5cc2703d5484326b1b27efa67a339c1f"
@@ -6587,6 +6583,15 @@ camelcase-keys@^4.0.0:
     map-obj "^2.0.0"
     map-obj "^2.0.0"
     quick-lru "^1.0.0"
     quick-lru "^1.0.0"
 
 
+camelcase-keys@^6.2.2:
+  version "6.2.2"
+  resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-6.2.2.tgz#5e755d6ba51aa223ec7d3d52f25778210f9dc3c0"
+  integrity sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==
+  dependencies:
+    camelcase "^5.3.1"
+    map-obj "^4.0.0"
+    quick-lru "^4.0.1"
+
 camelcase@^2.0.0:
 camelcase@^2.0.0:
   version "2.1.1"
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f"
   resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f"
@@ -6607,6 +6612,11 @@ camelcase@^5.0.0, camelcase@^5.2.0, camelcase@^5.3.1:
   resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
   resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
   integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
   integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
 
 
+camelcase@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.0.0.tgz#5259f7c30e35e278f1bdc2a4d91230b37cad981e"
+  integrity sha512-8KMDF1Vz2gzOq54ONPJS65IvTUaB1cHJ2DMM7MbPmLZljDH1qpzzLsWdiN9pHh6qvkRVDTi/07+eNGch/oLU4w==
+
 camelize@^1.0.0:
 camelize@^1.0.0:
   version "1.0.0"
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.0.tgz#164a5483e630fa4321e5af07020e531831b2609b"
   resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.0.tgz#164a5483e630fa4321e5af07020e531831b2609b"
@@ -8239,7 +8249,7 @@ debuglog@^1.0.1:
   resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492"
   resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492"
   integrity sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI=
   integrity sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI=
 
 
-decamelize-keys@^1.0.0:
+decamelize-keys@^1.0.0, decamelize-keys@^1.1.0:
   version "1.1.0"
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9"
   resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9"
   integrity sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=
   integrity sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=
@@ -10446,7 +10456,7 @@ find-up@^2.0.0, find-up@^2.1.0:
   dependencies:
   dependencies:
     locate-path "^2.0.0"
     locate-path "^2.0.0"
 
 
-find-up@^4.0.0:
+find-up@^4.0.0, find-up@^4.1.0:
   version "4.1.0"
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
   resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
   integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
   integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
@@ -11400,6 +11410,11 @@ har-validator@~5.1.0:
     ajv "^6.5.5"
     ajv "^6.5.5"
     har-schema "^2.0.0"
     har-schema "^2.0.0"
 
 
+hard-rejection@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883"
+  integrity sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==
+
 has-ansi@^2.0.0:
 has-ansi@^2.0.0:
   version "2.0.0"
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
   resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
@@ -14044,6 +14059,11 @@ kind-of@^6.0.0, kind-of@^6.0.2:
   resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051"
   resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051"
   integrity sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==
   integrity sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==
 
 
+kind-of@^6.0.3:
+  version "6.0.3"
+  resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
+  integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
+
 klaw@^1.0.0:
 klaw@^1.0.0:
   version "1.3.1"
   version "1.3.1"
   resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439"
   resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439"
@@ -14885,6 +14905,11 @@ map-obj@^2.0.0:
   resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-2.0.0.tgz#a65cd29087a92598b8791257a523e021222ac1f9"
   resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-2.0.0.tgz#a65cd29087a92598b8791257a523e021222ac1f9"
   integrity sha1-plzSkIepJZi4eRJXpSPgISIqwfk=
   integrity sha1-plzSkIepJZi4eRJXpSPgISIqwfk=
 
 
+map-obj@^4.0.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.1.0.tgz#b91221b542734b9f14256c0132c897c5d7256fd5"
+  integrity sha512-glc9y00wgtwcDmp7GaE/0b0OnxpNJsVf3ael/An6Fe2Q51LLwN1er6sdomLRzz5h0+yMpiYLhWYF5R7HeqVd4g==
+
 map-or-similar@^1.5.0:
 map-or-similar@^1.5.0:
   version "1.5.0"
   version "1.5.0"
   resolved "https://registry.yarnpkg.com/map-or-similar/-/map-or-similar-1.5.0.tgz#6de2653174adfb5d9edc33c69d3e92a1b76faf08"
   resolved "https://registry.yarnpkg.com/map-or-similar/-/map-or-similar-1.5.0.tgz#6de2653174adfb5d9edc33c69d3e92a1b76faf08"
@@ -15118,6 +15143,25 @@ meow@^5.0.0:
     trim-newlines "^2.0.0"
     trim-newlines "^2.0.0"
     yargs-parser "^10.0.0"
     yargs-parser "^10.0.0"
 
 
+meow@^7.0.1:
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/meow/-/meow-7.0.1.tgz#1ed4a0a50b3844b451369c48362eb0515f04c1dc"
+  integrity sha512-tBKIQqVrAHqwit0vfuFPY3LlzJYkEOFyKa3bPgxzNl6q/RtN8KQ+ALYEASYuFayzSAsjlhXj/JZ10rH85Q6TUw==
+  dependencies:
+    "@types/minimist" "^1.2.0"
+    arrify "^2.0.1"
+    camelcase "^6.0.0"
+    camelcase-keys "^6.2.2"
+    decamelize-keys "^1.1.0"
+    hard-rejection "^2.1.0"
+    minimist-options "^4.0.2"
+    normalize-package-data "^2.5.0"
+    read-pkg-up "^7.0.1"
+    redent "^3.0.0"
+    trim-newlines "^3.0.0"
+    type-fest "^0.13.1"
+    yargs-parser "^18.1.3"
+
 merge-anything@^2.2.4:
 merge-anything@^2.2.4:
   version "2.4.3"
   version "2.4.3"
   resolved "https://registry.yarnpkg.com/merge-anything/-/merge-anything-2.4.3.tgz#a5689b823c88d0c712fd2916bd1e1b4c3533cad8"
   resolved "https://registry.yarnpkg.com/merge-anything/-/merge-anything-2.4.3.tgz#a5689b823c88d0c712fd2916bd1e1b4c3533cad8"
@@ -15276,6 +15320,11 @@ min-document@^2.19.0:
   dependencies:
   dependencies:
     dom-walk "^0.1.0"
     dom-walk "^0.1.0"
 
 
+min-indent@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869"
+  integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==
+
 mini-create-react-context@^0.3.0:
 mini-create-react-context@^0.3.0:
   version "0.3.2"
   version "0.3.2"
   resolved "https://registry.yarnpkg.com/mini-create-react-context/-/mini-create-react-context-0.3.2.tgz#79fc598f283dd623da8e088b05db8cddab250189"
   resolved "https://registry.yarnpkg.com/mini-create-react-context/-/mini-create-react-context-0.3.2.tgz#79fc598f283dd623da8e088b05db8cddab250189"
@@ -15348,6 +15397,15 @@ minimist-options@^3.0.1:
     arrify "^1.0.1"
     arrify "^1.0.1"
     is-plain-obj "^1.1.0"
     is-plain-obj "^1.1.0"
 
 
+minimist-options@^4.0.2:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619"
+  integrity sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==
+  dependencies:
+    arrify "^1.0.1"
+    is-plain-obj "^1.1.0"
+    kind-of "^6.0.3"
+
 minimist@0.0.8:
 minimist@0.0.8:
   version "0.0.8"
   version "0.0.8"
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
@@ -18644,6 +18702,11 @@ quick-lru@^1.0.0:
   resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-1.1.0.tgz#4360b17c61136ad38078397ff11416e186dcfbb8"
   resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-1.1.0.tgz#4360b17c61136ad38078397ff11416e186dcfbb8"
   integrity sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g=
   integrity sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g=
 
 
+quick-lru@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f"
+  integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==
+
 rabin@^1.6.0:
 rabin@^1.6.0:
   version "1.6.0"
   version "1.6.0"
   resolved "https://registry.yarnpkg.com/rabin/-/rabin-1.6.0.tgz#e05690b13056f08c80098e3ad71b90530038e355"
   resolved "https://registry.yarnpkg.com/rabin/-/rabin-1.6.0.tgz#e05690b13056f08c80098e3ad71b90530038e355"
@@ -19265,6 +19328,15 @@ read-pkg-up@^6.0.0:
     read-pkg "^5.1.1"
     read-pkg "^5.1.1"
     type-fest "^0.5.0"
     type-fest "^0.5.0"
 
 
+read-pkg-up@^7.0.1:
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507"
+  integrity sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==
+  dependencies:
+    find-up "^4.1.0"
+    read-pkg "^5.2.0"
+    type-fest "^0.8.1"
+
 read-pkg@^1.0.0:
 read-pkg@^1.0.0:
   version "1.1.0"
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28"
   resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28"
@@ -19292,7 +19364,7 @@ read-pkg@^3.0.0:
     normalize-package-data "^2.3.2"
     normalize-package-data "^2.3.2"
     path-type "^3.0.0"
     path-type "^3.0.0"
 
 
-read-pkg@^5.1.1:
+read-pkg@^5.1.1, read-pkg@^5.2.0:
   version "5.2.0"
   version "5.2.0"
   resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc"
   resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc"
   integrity sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==
   integrity sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==
@@ -19450,6 +19522,14 @@ redent@^2.0.0:
     indent-string "^3.0.0"
     indent-string "^3.0.0"
     strip-indent "^2.0.0"
     strip-indent "^2.0.0"
 
 
+redent@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f"
+  integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==
+  dependencies:
+    indent-string "^4.0.0"
+    strip-indent "^3.0.0"
+
 redeyed@~2.1.0:
 redeyed@~2.1.0:
   version "2.1.1"
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/redeyed/-/redeyed-2.1.1.tgz#8984b5815d99cb220469c99eeeffe38913e6cc0b"
   resolved "https://registry.yarnpkg.com/redeyed/-/redeyed-2.1.1.tgz#8984b5815d99cb220469c99eeeffe38913e6cc0b"
@@ -21264,6 +21344,13 @@ strip-indent@^2.0.0:
   resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68"
   resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68"
   integrity sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=
   integrity sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=
 
 
+strip-indent@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001"
+  integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==
+  dependencies:
+    min-indent "^1.0.0"
+
 strip-json-comments@^2.0.1, strip-json-comments@~2.0.1:
 strip-json-comments@^2.0.1, strip-json-comments@~2.0.1:
   version "2.0.1"
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
   resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
@@ -22089,6 +22176,11 @@ trim-newlines@^2.0.0:
   resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-2.0.0.tgz#b403d0b91be50c331dfc4b82eeceb22c3de16d20"
   resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-2.0.0.tgz#b403d0b91be50c331dfc4b82eeceb22c3de16d20"
   integrity sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA=
   integrity sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA=
 
 
+trim-newlines@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.0.tgz#79726304a6a898aa8373427298d54c2ee8b1cb30"
+  integrity sha512-C4+gOpvmxaSMKuEf9Qc134F1ZuOHVXKRbtEflf4NTtuuJDEIJ9p5PXsalL8SkeRw+qit1Mo+yuvMPAKwWg/1hA==
+
 trim-off-newlines@^1.0.0:
 trim-off-newlines@^1.0.0:
   version "1.0.1"
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/trim-off-newlines/-/trim-off-newlines-1.0.1.tgz#9f9ba9d9efa8764c387698bcbfeb2c848f11adb3"
   resolved "https://registry.yarnpkg.com/trim-off-newlines/-/trim-off-newlines-1.0.1.tgz#9f9ba9d9efa8764c387698bcbfeb2c848f11adb3"
@@ -22287,6 +22379,11 @@ type-fest@^0.11.0:
   resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1"
   resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1"
   integrity sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==
   integrity sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==
 
 
+type-fest@^0.13.1:
+  version "0.13.1"
+  resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.13.1.tgz#0172cb5bce80b0bd542ea348db50c7e21834d934"
+  integrity sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==
+
 type-fest@^0.3.0:
 type-fest@^0.3.0:
   version "0.3.1"
   version "0.3.1"
   resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.3.1.tgz#63d00d204e059474fe5e1b7c011112bbd1dc29e1"
   resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.3.1.tgz#63d00d204e059474fe5e1b7c011112bbd1dc29e1"
@@ -23826,6 +23923,14 @@ yargs-parser@^15.0.0:
     camelcase "^5.0.0"
     camelcase "^5.0.0"
     decamelize "^1.2.0"
     decamelize "^1.2.0"
 
 
+yargs-parser@^18.1.3:
+  version "18.1.3"
+  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"
+  integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==
+  dependencies:
+    camelcase "^5.0.0"
+    decamelize "^1.2.0"
+
 yargs-parser@^5.0.0:
 yargs-parser@^5.0.0:
   version "5.0.0"
   version "5.0.0"
   resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-5.0.0.tgz#275ecf0d7ffe05c77e64e7c86e4cd94bf0e1228a"
   resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-5.0.0.tgz#275ecf0d7ffe05c77e64e7c86e4cd94bf0e1228a"