Browse Source

Merge remote-tracking branch 'joystream/giza' into giza_distribution_bucket_changes

# Conflicts:
#	chain-metadata.json
Shamil Gadelshin 3 years ago
parent
commit
6ada3de455

+ 1 - 1
README.md

@@ -89,7 +89,7 @@ You can also run your our own joystream-node:
 
 ```sh
 git checkout master
-WASM_BUILD_TOOLCHAIN=nightly-2021-03-24 cargo build --release
+WASM_BUILD_TOOLCHAIN=nightly-2021-02-20 cargo +nightly-2021-02-20 build --release
 ./target/release/joystream-node -- --pruning archive --chain testnets/joy-testnet-5.json
 ```
 

File diff suppressed because it is too large
+ 0 - 0
chain-metadata.json


+ 3 - 3
devops/git-hooks/pre-push

@@ -1,13 +1,13 @@
 #!/bin/sh
 set -e
 
-export WASM_BUILD_TOOLCHAIN=nightly-2021-03-24
+export WASM_BUILD_TOOLCHAIN=nightly-2021-02-20
 
 echo 'running clippy (rust linter)'
 # When custom build.rs triggers wasm-build-runner-impl to build we get error:
 # "Rust WASM toolchain not installed, please install it!"
 # So we skip building the WASM binary by setting BUILD_DUMMY_WASM_BINARY=1
-BUILD_DUMMY_WASM_BINARY=1 cargo clippy --release --all -- -D warnings
+BUILD_DUMMY_WASM_BINARY=1 cargo +nightly-2021-02-20 clippy --release --all -- -D warnings
 
 echo 'running cargo unit tests'
-cargo test --release --all
+cargo +nightly-2021-02-20 test --release --all

+ 5 - 5
joystream-node-armv7.Dockerfile

@@ -1,9 +1,9 @@
 FROM rust:1.52.1-buster AS rust
 RUN rustup self update
-RUN rustup install nightly-2021-03-24 --force
-RUN rustup default nightly-2021-03-24
-RUN rustup target add wasm32-unknown-unknown --toolchain nightly-2021-03-24
-RUN rustup component add --toolchain nightly-2021-03-24 clippy
+RUN rustup install nightly-2021-02-20 --force
+RUN rustup default nightly-2021-02-20
+RUN rustup target add wasm32-unknown-unknown --toolchain nightly-2021-02-20
+RUN rustup component add --toolchain nightly-2021-02-20 clippy
 RUN apt-get update && \
   apt-get install -y curl git gcc xz-utils sudo pkg-config unzip clang llvm libc6-dev
 
@@ -14,7 +14,7 @@ COPY . /joystream
 
 # Build all cargo crates
 # Ensure our tests and linter pass before actual build
-ENV WASM_BUILD_TOOLCHAIN=nightly-2021-03-24
+ENV WASM_BUILD_TOOLCHAIN=nightly-2021-02-20
 RUN apt-get install -y libprotobuf-dev protobuf-compiler
 RUN BUILD_DUMMY_WASM_BINARY=1 cargo clippy --release --all -- -D warnings && \
     cargo test --release --all && \

+ 5 - 5
joystream-node.Dockerfile

@@ -1,9 +1,9 @@
 FROM rust:1.52.1-buster AS rust
 RUN rustup self update
-RUN rustup install nightly-2021-03-24 --force
-RUN rustup default nightly-2021-03-24
-RUN rustup target add wasm32-unknown-unknown --toolchain nightly-2021-03-24
-RUN rustup component add --toolchain nightly-2021-03-24 clippy
+RUN rustup install nightly-2021-02-20 --force
+RUN rustup default nightly-2021-02-20
+RUN rustup target add wasm32-unknown-unknown --toolchain nightly-2021-02-20
+RUN rustup component add --toolchain nightly-2021-02-20 clippy
 RUN apt-get update && \
   apt-get install -y curl git gcc xz-utils sudo pkg-config unzip clang llvm libc6-dev
 
@@ -14,7 +14,7 @@ COPY . /joystream
 
 # Build all cargo crates
 # Ensure our tests and linter pass before actual build
-ENV WASM_BUILD_TOOLCHAIN=nightly-2021-03-24
+ENV WASM_BUILD_TOOLCHAIN=nightly-2021-02-20
 RUN BUILD_DUMMY_WASM_BINARY=1 cargo clippy --release --all -- -D warnings && \
     cargo test --release --all && \
     cargo build --release

+ 3 - 3
node/README.md

@@ -26,7 +26,7 @@ cd joystream/
 Compile the node and runtime:
 
 ```bash
-WASM_BUILD_TOOLCHAIN=nightly-2021-03-24 cargo build --release
+WASM_BUILD_TOOLCHAIN=nightly-2021-02-20 cargo +nightly-2021-02-20 build --release
 ```
 
 This produces the binary in `./target/release/joystream-node`
@@ -57,7 +57,7 @@ Use the `--chain` argument, and specify the path to the genesis `chain.json` fil
 Running unit tests:
 
 ```bash
-cargo test --release --all
+cargo +nightly-2021-02-20 test --release --all
 ```
 
 Running full suite of checks, tests, formatting and linting:
@@ -79,7 +79,7 @@ If you are building a tagged release from `master` branch and want to install th
 This will install the executable `joystream-node` to your `~/.cargo/bin` folder, which you would normally have in your `$PATH` environment.
 
 ```bash
-WASM_BUILD_TOOLCHAIN=nightly-2021-03-24 cargo install joystream-node --path node/ --locked
+WASM_BUILD_TOOLCHAIN=nightly-2021-02-20 cargo +nightly-2021-02-20 install joystream-node --path node/ --locked
 ```
 
 Now you can run and connect to the testnet:

+ 5 - 0
runtime-modules/content/src/errors.rs

@@ -37,6 +37,9 @@ decl_error! {
         /// Member authentication failed
         MemberAuthFailed,
 
+        /// Member id not valid
+        CollaboratorIsNotValidMember,
+
         /// Curator authentication failed
         CuratorAuthFailed,
 
@@ -80,5 +83,7 @@ decl_error! {
         InvalidBagSizeSpecified,
 
 
+        /// Actor not channel owner
+        ActorNotChannelOwner,
     }
 }

+ 125 - 79
runtime-modules/content/src/lib.rs

@@ -158,7 +158,7 @@ pub struct ChannelCategoryUpdateParameters {
 /// Type representing an owned channel which videos, playlists, and series can belong to.
 #[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
 #[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)]
-pub struct ChannelRecord<MemberId, CuratorGroupId, AccountId> {
+pub struct ChannelRecord<MemberId: Ord, CuratorGroupId, AccountId> {
     /// The owner of a channel
     owner: ChannelOwner<MemberId, CuratorGroupId>,
     /// The videos under this channel
@@ -167,8 +167,8 @@ pub struct ChannelRecord<MemberId, CuratorGroupId, AccountId> {
     is_censored: bool,
     /// Reward account where revenue is sent if set.
     reward_account: Option<AccountId>,
-    /// Account for withdrawing deletion prize funds
-    deletion_prize_source_account_id: AccountId,
+    /// collaborator set
+    collaborators: BTreeSet<MemberId>,
 }
 
 // Channel alias type for simplification.
@@ -206,22 +206,28 @@ pub type ChannelOwnershipTransferRequest<T> = ChannelOwnershipTransferRequestRec
 /// Information about channel being created.
 #[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
 #[derive(Encode, Decode, Clone, PartialEq, Eq, Debug)]
-pub struct ChannelCreationParametersRecord<StorageAssets, AccountId> {
-    /// Asset collection for the channel, referenced by metadata
+pub struct ChannelCreationParametersRecord<StorageAssets, AccountId, MemberId: Ord> {
+    /// Assets referenced by metadata
     assets: Option<StorageAssets>,
     /// Metadata about the channel.
     meta: Option<Vec<u8>>,
     /// optional reward account
     reward_account: Option<AccountId>,
+    /// initial collaborator set
+    collaborators: BTreeSet<MemberId>,
 }
 
-type ChannelCreationParameters<T> =
-    ChannelCreationParametersRecord<StorageAssets<T>, <T as frame_system::Trait>::AccountId>;
+type ChannelCreationParameters<T> = ChannelCreationParametersRecord<
+    StorageAssets<T>,
+    <T as frame_system::Trait>::AccountId,
+    <T as common::MembershipTypes>::MemberId,
+>;
 
 /// Information about channel being updated.
 #[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
 #[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)]
-pub struct ChannelUpdateParametersRecord<StorageAssets, AccountId, DataObjectId: Ord> {
+pub struct ChannelUpdateParametersRecord<StorageAssets, AccountId, DataObjectId: Ord, MemberId: Ord>
+{
     /// Asset collection for the channel, referenced by metadata    
     assets_to_upload: Option<StorageAssets>,
     /// If set, metadata update for the channel.
@@ -230,12 +236,15 @@ pub struct ChannelUpdateParametersRecord<StorageAssets, AccountId, DataObjectId:
     reward_account: Option<Option<AccountId>>,
     /// assets to be removed from channel
     assets_to_remove: BTreeSet<DataObjectId>,
+    /// collaborator set
+    collaborators: Option<BTreeSet<MemberId>>,
 }
 
 type ChannelUpdateParameters<T> = ChannelUpdateParametersRecord<
     StorageAssets<T>,
     <T as frame_system::Trait>::AccountId,
     DataObjectId<T>,
+    <T as common::MembershipTypes>::MemberId,
 >;
 
 /// A category that videos can belong to.
@@ -514,8 +523,9 @@ decl_module! {
             origin,
         ) {
 
+        let sender = ensure_signed(origin)?;
             // Ensure given origin is lead
-            ensure_is_lead::<T>(origin)?;
+            ensure_lead_auth_success::<T>(&sender)?;
 
             //
             // == MUTATION SAFE ==
@@ -542,7 +552,10 @@ decl_module! {
         ) {
 
             // Ensure given origin is lead
-            ensure_is_lead::<T>(origin)?;
+        let sender = ensure_signed(origin)?;
+            // Ensure given origin is lead
+            ensure_lead_auth_success::<T>(&sender)?;
+
 
             // Ensure curator group under provided curator_group_id already exist
             Self::ensure_curator_group_under_given_id_exists(&curator_group_id)?;
@@ -569,7 +582,10 @@ decl_module! {
         ) {
 
             // Ensure given origin is lead
-            ensure_is_lead::<T>(origin)?;
+        let sender = ensure_signed(origin)?;
+            // Ensure given origin is lead
+            ensure_lead_auth_success::<T>(&sender)?;
+
 
             // Ensure curator group under provided curator_group_id already exist, retrieve corresponding one
             let curator_group = Self::ensure_curator_group_exists(&curator_group_id)?;
@@ -605,7 +621,9 @@ decl_module! {
         ) {
 
             // Ensure given origin is lead
-            ensure_is_lead::<T>(origin)?;
+        let sender = ensure_signed(origin)?;
+            // Ensure given origin is lead
+            ensure_lead_auth_success::<T>(&sender)?;
 
             // Ensure curator group under provided curator_group_id already exist, retrieve corresponding one
             let curator_group = Self::ensure_curator_group_exists(&curator_group_id)?;
@@ -633,21 +651,28 @@ decl_module! {
             actor: ContentActor<T::CuratorGroupId, T::CuratorId, T::MemberId>,
             params: ChannelCreationParameters<T>,
         ) {
+            // channel creator account
+            let sender = ensure_signed(origin)?;
+
             ensure_actor_authorized_to_create_channel::<T>(
-                origin.clone(),
+                &sender,
                 &actor,
             )?;
 
-            // channel creator account
-            let sender = ensure_signed(origin)?;
-
             // The channel owner will be..
             let channel_owner = Self::actor_to_channel_owner(&actor)?;
 
             // next channel id
             let channel_id = NextChannelId::<T>::get();
 
-            // atomically upload to storage and return the # of uploaded assets
+            // ensure collaborator member ids are valid
+            Self::validate_collaborator_set(&params.collaborators)?;
+
+            //
+            // == MUTATION SAFE ==
+            //
+
+            // upload to storage
             if let Some(upload_assets) = params.assets.as_ref() {
                 Self::upload_assets_to_storage(
                     upload_assets,
@@ -656,13 +681,10 @@ decl_module! {
                 )?;
             }
 
-            //
-            // == MUTATION SAFE ==
-            //
-
             // Only increment next channel id if adding content was successful
             NextChannelId::<T>::mutate(|id| *id += T::ChannelId::one());
 
+
             // channel creation
             let channel: Channel<T> = ChannelRecord {
                 owner: channel_owner,
@@ -670,8 +692,7 @@ decl_module! {
                 num_videos: 0u64,
                 is_censored: false,
                 reward_account: params.reward_account.clone(),
-                // setting the channel owner account as the prize funds account
-                deletion_prize_source_account_id: sender,
+                collaborators: params.collaborators.clone(),
             };
 
             // add channel to onchain state
@@ -688,42 +709,53 @@ decl_module! {
             channel_id: T::ChannelId,
             params: ChannelUpdateParameters<T>,
         ) {
+            let sender = ensure_signed(origin)?;
+
             // check that channel exists
-            let channel = Self::ensure_channel_exists(&channel_id)?;
+            let mut channel = Self::ensure_channel_exists(&channel_id)?;
 
-            ensure_actor_authorized_to_update_channel::<T>(
-                origin,
+            ensure_actor_authorized_to_update_channel_assets::<T>(
+                &sender,
                 &actor,
-                &channel.owner,
+                &channel,
             )?;
 
-            Self::remove_assets_from_storage(&params.assets_to_remove, &channel_id, &channel.deletion_prize_source_account_id)?;
+            // maybe update the reward account if actor is not a collaborator
+            if let Some(reward_account) = params.reward_account.as_ref() {
+                ensure_actor_can_manage_reward_account::<T>(&sender, &channel.owner, &actor)?;
+                channel.reward_account = reward_account.clone();
+            }
 
-            // atomically upload to storage and return the # of uploaded assets
-            if let Some(upload_assets) = params.assets_to_upload.as_ref() {
-                Self::upload_assets_to_storage(
-                    upload_assets,
-                    &channel_id,
-                    &channel.deletion_prize_source_account_id
-                )?;
+            // update collaborator set if actor is not a collaborator
+            if let Some(new_collabs) = params.collaborators.as_ref() {
+                ensure_actor_can_manage_collaborators::<T>(&sender, &channel.owner, &actor)?;
+                // ensure collaborator member ids are valid
+                Self::validate_collaborator_set(new_collabs)?;
+
+                channel.collaborators = new_collabs.clone();
             }
 
             //
             // == MUTATION SAFE ==
             //
 
-            let mut channel = channel;
-
-            // Maybe update the reward account
-            if let Some(reward_account) = &params.reward_account {
-                channel.reward_account = reward_account.clone();
+            // upload assets to storage
+            if let Some(upload_assets) = params.assets_to_upload.as_ref() {
+                Self::upload_assets_to_storage(
+                    upload_assets,
+                    &channel_id,
+                    &sender,
+                )?;
             }
 
+            // remove eassets from storage
+            Self::remove_assets_from_storage(&params.assets_to_remove, &channel_id, &sender)?;
+
             // Update the channel
             ChannelById::<T>::insert(channel_id, channel.clone());
 
             Self::deposit_event(RawEvent::ChannelUpdated(actor, channel_id, channel, params));
-        }
+}
 
         // extrinsics for channel deletion
         #[weight = 10_000_000] // TODO: adjust weight
@@ -733,12 +765,13 @@ decl_module! {
             channel_id: T::ChannelId,
             num_objects_to_delete: u64,
         ) -> DispatchResult {
+
+            let sender = ensure_signed(origin)?;
             // check that channel exists
             let channel = Self::ensure_channel_exists(&channel_id)?;
 
-            // ensure permissions
-            ensure_actor_authorized_to_update_channel::<T>(
-                origin,
+            ensure_actor_authorized_to_delete_channel::<T>(
+                &sender,
                 &actor,
                 &channel.owner,
             )?;
@@ -758,6 +791,10 @@ decl_module! {
                     Error::<T>::InvalidBagSizeSpecified
                 );
 
+                //
+                // == MUTATION SAFE ==
+                //
+
                 // construct collection of assets to be removed
                 let assets_to_remove = T::DataObjectStorage::get_data_objects_id(&bag_id);
 
@@ -765,20 +802,16 @@ decl_module! {
                 Self::remove_assets_from_storage(
                     &assets_to_remove,
                     &channel_id,
-                    &channel.deletion_prize_source_account_id
+                    &sender,
                 )?;
 
                 // delete channel dynamic bag
                 Storage::<T>::delete_dynamic_bag(
-                    channel.deletion_prize_source_account_id,
-                    dyn_bag
+                    sender,
+                    dyn_bag,
                 )?;
             }
 
-            //
-            // == MUTATION SAFE ==
-            //
-
             // remove channel from on chain state
             ChannelById::<T>::remove(channel_id);
 
@@ -917,32 +950,33 @@ decl_module! {
             channel_id: T::ChannelId,
             params: VideoCreationParameters<T>,
         ) {
+            let sender = ensure_signed(origin.clone())?;
 
             // check that channel exists
             let channel = Self::ensure_channel_exists(&channel_id)?;
 
-            ensure_actor_authorized_to_update_channel::<T>(
-                origin,
+            ensure_actor_authorized_to_update_channel_assets::<T>(
+                &sender,
                 &actor,
-                &channel.owner,
+                &channel,
             )?;
 
             // next video id
             let video_id = NextVideoId::<T>::get();
 
-            // atomically upload to storage and return the # of uploaded assets
+            //
+            // == MUTATION SAFE ==
+            //
+
+            // upload to storage
             if let Some(upload_assets) = params.assets.as_ref() {
                 Self::upload_assets_to_storage(
                     upload_assets,
                     &channel_id,
-                    &channel.deletion_prize_source_account_id
+                    &sender,
                 )?;
             }
 
-            //
-            // == MUTATION SAFE ==
-            //
-
             // create the video struct
             let video: Video<T> = VideoRecord {
                 in_channel: channel_id,
@@ -975,34 +1009,37 @@ decl_module! {
             video_id: T::VideoId,
             params: VideoUpdateParameters<T>,
         ) {
+
+            let sender = ensure_signed(origin.clone())?;
             // check that video exists, retrieve corresponding channel id.
             let video = Self::ensure_video_exists(&video_id)?;
 
             let channel_id = video.in_channel;
             let channel = ChannelById::<T>::get(&channel_id);
 
-            ensure_actor_authorized_to_update_channel::<T>(
-                origin,
+            // Check for permission to update channel assets
+            ensure_actor_authorized_to_update_channel_assets::<T>(
+                &sender,
                 &actor,
-                &channel.owner,
+                &channel,
             )?;
 
+            //
+            // == MUTATION SAFE ==
+            //
+
             // remove specified assets from channel bag in storage
-            Self::remove_assets_from_storage(&params.assets_to_remove, &channel_id, &channel.deletion_prize_source_account_id)?;
+            Self::remove_assets_from_storage(&params.assets_to_remove, &channel_id, &sender)?;
 
             // atomically upload to storage and return the # of uploaded assets
             if let Some(upload_assets) = params.assets_to_upload.as_ref() {
                 Self::upload_assets_to_storage(
                     upload_assets,
                     &channel_id,
-                    &channel.deletion_prize_source_account_id
+                    &sender,
                 )?;
             }
 
-            //
-            // == MUTATION SAFE ==
-            //
-
             Self::deposit_event(RawEvent::VideoUpdated(actor, video_id, params));
         }
 
@@ -1014,6 +1051,8 @@ decl_module! {
             assets_to_remove: BTreeSet<DataObjectId<T>>,
         ) {
 
+           let sender = ensure_signed(origin.clone())?;
+
             // check that video exists
             let video = Self::ensure_video_exists(&video_id)?;
 
@@ -1021,24 +1060,22 @@ decl_module! {
             let channel_id = video.in_channel;
             let channel = ChannelById::<T>::get(channel_id);
 
-
-            ensure_actor_authorized_to_update_channel::<T>(
-                origin,
+            ensure_actor_authorized_to_update_channel_assets::<T>(
+                &sender,
                 &actor,
-                // The channel owner will be..
-                &channel.owner,
+                &channel,
             )?;
 
             // ensure video can be removed
             Self::ensure_video_can_be_removed(&video)?;
 
-            // remove specified assets from channel bag in storage
-            Self::remove_assets_from_storage(&assets_to_remove, &channel_id, &channel.deletion_prize_source_account_id)?;
-
             //
             // == MUTATION SAFE ==
             //
 
+            // remove specified assets from channel bag in storage
+            Self::remove_assets_from_storage(&assets_to_remove, &channel_id, &sender)?;
+
             // Remove video
             VideoById::<T>::remove(video_id);
 
@@ -1358,12 +1395,12 @@ impl<T: Trait> Module<T> {
         actor: &ContentActor<T::CuratorGroupId, T::CuratorId, T::MemberId>,
     ) -> ActorToChannelOwnerResult<T> {
         match actor {
-            // Lead should use their member or curator role to create channels
-            ContentActor::Lead => Err(Error::<T>::ActorCannotOwnChannel),
             ContentActor::Curator(curator_group_id, _curator_id) => {
                 Ok(ChannelOwner::CuratorGroup(*curator_group_id))
             }
             ContentActor::Member(member_id) => Ok(ChannelOwner::Member(*member_id)),
+            // Lead & collaborators should use their member or curator role to create channels
+            _ => Err(Error::<T>::ActorCannotOwnChannel),
         }
     }
 
@@ -1407,6 +1444,15 @@ impl<T: Trait> Module<T> {
         }
         Ok(())
     }
+
+    fn validate_collaborator_set(collaborators: &BTreeSet<T::MemberId>) -> DispatchResult {
+        // check if all members are valid
+        let res = collaborators
+            .iter()
+            .all(|member_id| <T as ContentActorAuthenticator>::validate_member_id(member_id));
+        ensure!(res, Error::<T>::CollaboratorIsNotValidMember);
+        Ok(())
+    }
 }
 
 decl_event!(

+ 194 - 48
runtime-modules/content/src/permissions/mod.rs

@@ -1,3 +1,19 @@
+// The following table summarizes the permissions in the content subsystem.
+// - Actor role as columns, controller account is Tx sender.
+// - operations on a given channel (=channel=) are rows, which are basically the guards to be
+//   implemented
+// - Entries are conditions to be verified / assertions
+//
+// |                       | *Lead*                   | *Curator*                | *Member*                | *Collaborator*            |
+// |-----------------------+--------------------------+--------------------------+-------------------------+---------------------------|
+// | *assets mgmt*         | channel.owner is curator | curator is channel.owner | member is channel.owner | collab in channel.collabs |
+// | *censorship mgmt*     | true                     | channel.owner is member  | false                   | false                     |
+// | *category mgmt*       | true                     | true                     | false                   | false                     |
+// | *collab. set mgmt*    | channel.owner is curator | curator is channel.owner | member is channel.owner | false                     |
+// | *reward account mgmt* | channel.owner is curator | curator is channel.owner | member is channel.owner | false                     |
+// | *create channel*      | false                    | true                     | true                    | false                     |
+// | *delete channel*      | channel.owner is curator | curator is channel.owner | member is channel.owner | false                     |
+
 mod curator_group;
 
 pub use curator_group::*;
@@ -51,6 +67,9 @@ pub trait ContentActorAuthenticator: frame_system::Trait + membership::Trait {
 
     /// Authorize actor as member
     fn is_member(member_id: &Self::MemberId, account_id: &Self::AccountId) -> bool;
+
+    /// Ensure member id is valid
+    fn validate_member_id(member_id: &Self::MemberId) -> bool;
 }
 
 pub fn ensure_is_valid_curator_id<T: Trait>(curator_id: &T::CuratorId) -> DispatchResult {
@@ -75,8 +94,8 @@ pub fn ensure_curator_auth_success<T: Trait>(
 
 /// Ensure member authorization performed succesfully
 pub fn ensure_member_auth_success<T: Trait>(
-    member_id: &T::MemberId,
     account_id: &T::AccountId,
+    member_id: &T::MemberId,
 ) -> DispatchResult {
     ensure!(
         T::is_member(member_id, account_id),
@@ -91,86 +110,210 @@ pub fn ensure_lead_auth_success<T: Trait>(account_id: &T::AccountId) -> Dispatch
     Ok(())
 }
 
-/// Ensure given `Origin` is lead
-pub fn ensure_is_lead<T: Trait>(origin: T::Origin) -> DispatchResult {
-    let account_id = ensure_signed(origin)?;
-    ensure_lead_auth_success::<T>(&account_id)
-}
-
+/// Ensure actor is authorized to create a channel
 pub fn ensure_actor_authorized_to_create_channel<T: Trait>(
-    origin: T::Origin,
+    sender: &T::AccountId,
     actor: &ContentActor<T::CuratorGroupId, T::CuratorId, T::MemberId>,
 ) -> DispatchResult {
     match actor {
-        // Lead should use their member or curator role to create or update channel assets.
-        ContentActor::Lead => Err(Error::<T>::ActorCannotOwnChannel.into()),
         ContentActor::Curator(curator_group_id, curator_id) => {
-            let sender = ensure_signed(origin)?;
-
             // Authorize curator, performing all checks to ensure curator can act
             CuratorGroup::<T>::perform_curator_in_group_auth(curator_id, curator_group_id, &sender)
         }
-        ContentActor::Member(member_id) => {
-            let sender = ensure_signed(origin)?;
+        ContentActor::Member(member_id) => ensure_member_auth_success::<T>(sender, member_id),
+        // Lead & collaborators should use their member or curator role to create channels.
+        _ => Err(Error::<T>::ActorCannotOwnChannel.into()),
+    }
+}
 
-            ensure_member_auth_success::<T>(member_id, &sender)
+/// Ensure actor is authorized to delete channel
+pub fn ensure_actor_authorized_to_delete_channel<T: Trait>(
+    sender: &T::AccountId,
+    actor: &ContentActor<T::CuratorGroupId, T::CuratorId, T::MemberId>,
+    channel_owner: &ChannelOwner<T::MemberId, T::CuratorGroupId>,
+) -> DispatchResult {
+    match actor {
+        ContentActor::Lead => {
+            // ensure lead is valid
+            ensure_lead_auth_success::<T>(sender)?;
+            // ensure curator
+            ensure_channel_is_owned_by_curators::<T>(channel_owner)?;
+            Ok(())
+        }
+        ContentActor::Curator(curator_group_id, curator_id) => {
+            // ensure curator group is valid
+            CuratorGroup::<T>::perform_curator_in_group_auth(
+                curator_id,
+                curator_group_id,
+                &sender,
+            )?;
+            // ensure group is channel owner
+            ensure_curator_group_is_channel_owner::<T>(channel_owner, curator_group_id)?;
+            Ok(())
         }
+        ContentActor::Member(member_id) => {
+            // ensure valid member
+            ensure_member_auth_success::<T>(&sender, member_id)?;
+            // ensure member is channel owner
+            ensure_member_is_channel_owner::<T>(channel_owner, member_id)?;
+            Ok(())
+        }
+        // collaborators should use their member or curator role to delete channel
+        _ => Err(Error::<T>::ActorNotAuthorized.into()),
     }
 }
 
-// Enure actor can update channels and videos in the channel
-pub fn ensure_actor_authorized_to_update_channel<T: Trait>(
-    origin: T::Origin,
+/// Ensure actor is authorized to manage collaborator set for a channel
+pub fn ensure_actor_can_manage_collaborators<T: Trait>(
+    sender: &T::AccountId,
+    channel_owner: &ChannelOwner<T::MemberId, T::CuratorGroupId>,
     actor: &ContentActor<T::CuratorGroupId, T::CuratorId, T::MemberId>,
-    owner: &ChannelOwner<T::MemberId, T::CuratorGroupId>,
 ) -> DispatchResult {
-    // Only owner of a channel can update and delete channel assets.
-    // Lead can update and delete curator group owned channel assets.
     match actor {
         ContentActor::Lead => {
-            let sender = ensure_signed(origin)?;
-            ensure_lead_auth_success::<T>(&sender)?;
-            if let ChannelOwner::CuratorGroup(_) = owner {
-                Ok(())
-            } else {
-                Err(Error::<T>::ActorNotAuthorized.into())
-            }
+            // ensure lead is valid
+            ensure_lead_auth_success::<T>(sender)?;
+            // ensure curator
+            ensure_channel_is_owned_by_curators::<T>(channel_owner)?;
+            Ok(())
         }
         ContentActor::Curator(curator_group_id, curator_id) => {
-            let sender = ensure_signed(origin)?;
-
-            // Authorize curator, performing all checks to ensure curator can act
+            // ensure curator group is valid
             CuratorGroup::<T>::perform_curator_in_group_auth(
                 curator_id,
                 curator_group_id,
                 &sender,
             )?;
-
-            // Ensure curator group is the channel owner.
-            ensure!(
-                *owner == ChannelOwner::CuratorGroup(*curator_group_id),
-                Error::<T>::ActorNotAuthorized
-            );
-
+            // ensure group is channel owner
+            ensure_curator_group_is_channel_owner::<T>(channel_owner, curator_group_id)?;
             Ok(())
         }
         ContentActor::Member(member_id) => {
-            let sender = ensure_signed(origin)?;
+            // ensure valid member
+            ensure_member_auth_success::<T>(&sender, member_id)?;
+            // ensure member is channel owner
+            ensure_member_is_channel_owner::<T>(channel_owner, member_id)?;
+            Ok(())
+        }
+        // Collaborators should use their member or curator role in order to update reward account.
+        ContentActor::Collaborator(_) => Err(Error::<T>::ActorNotAuthorized.into()),
+    }
+}
 
-            ensure_member_auth_success::<T>(member_id, &sender)?;
+/// Ensure actor is authorized to manage reward account for a channel
+pub fn ensure_actor_can_manage_reward_account<T: Trait>(
+    sender: &T::AccountId,
+    channel_owner: &ChannelOwner<T::MemberId, T::CuratorGroupId>,
+    actor: &ContentActor<T::CuratorGroupId, T::CuratorId, T::MemberId>,
+) -> DispatchResult {
+    match actor {
+        ContentActor::Lead => {
+            // ensure lead is valid
+            ensure_lead_auth_success::<T>(sender)?;
+            // ensure curator
+            ensure_channel_is_owned_by_curators::<T>(channel_owner)?;
+            Ok(())
+        }
+        ContentActor::Curator(curator_group_id, curator_id) => {
+            // ensure curator group is valid
+            CuratorGroup::<T>::perform_curator_in_group_auth(
+                curator_id,
+                curator_group_id,
+                &sender,
+            )?;
+            // ensure group is channel owner
+            ensure_curator_group_is_channel_owner::<T>(channel_owner, curator_group_id)?;
+            Ok(())
+        }
+        ContentActor::Member(member_id) => {
+            // ensure valid member
+            ensure_member_auth_success::<T>(&sender, member_id)?;
+            // ensure member is channel owner
+            ensure_member_is_channel_owner::<T>(channel_owner, member_id)?;
+            Ok(())
+        }
+        // collaborators should use their member or curator role in order to update reward account.
+        _ => Err(Error::<T>::ActorNotAuthorized.into()),
+    }
+}
 
-            // Ensure the member is the channel owner.
+/// Ensure actor is authorized to manage channel assets, video also qualify as assets
+pub fn ensure_actor_authorized_to_update_channel_assets<T: Trait>(
+    sender: &T::AccountId,
+    actor: &ContentActor<T::CuratorGroupId, T::CuratorId, T::MemberId>,
+    channel: &Channel<T>,
+) -> DispatchResult {
+    // Only owner of a channel can update and delete channel assets.
+    // Lead can update and delete curator group owned channel assets.
+    match actor {
+        ContentActor::Lead => {
+            // ensure lead is valid
+            ensure_lead_auth_success::<T>(sender)?;
+            // ensure curator
+            ensure_channel_is_owned_by_curators::<T>(&channel.owner)?;
+            Ok(())
+        }
+        ContentActor::Curator(curator_group_id, curator_id) => {
+            // ensure curator group is valid
+            CuratorGroup::<T>::perform_curator_in_group_auth(curator_id, curator_group_id, sender)?;
+            // ensure group is channel owner
+            ensure_curator_group_is_channel_owner::<T>(&channel.owner, curator_group_id)?;
+            Ok(())
+        }
+        ContentActor::Member(member_id) => {
+            // ensure valid member
+            ensure_member_auth_success::<T>(sender, member_id)?;
+            // ensure member is channel owner
+            ensure_member_is_channel_owner::<T>(&channel.owner, member_id)?;
+            Ok(())
+        }
+        ContentActor::Collaborator(member_id) => {
             ensure!(
-                *owner == ChannelOwner::Member(*member_id),
+                channel.collaborators.contains(member_id),
                 Error::<T>::ActorNotAuthorized
             );
-
             Ok(())
         }
     }
 }
 
-// Enure actor can update or delete channels and videos
+/// Ensure channel is owned by some curators
+pub fn ensure_channel_is_owned_by_curators<T: Trait>(
+    channel_owner: &ChannelOwner<T::MemberId, T::CuratorGroupId>,
+) -> DispatchResult {
+    match channel_owner {
+        ChannelOwner::CuratorGroup(_) => Ok(()),
+        _ => Err(Error::<T>::ActorNotAuthorized.into()),
+    }
+}
+
+/// Ensure specified valid curator group is channel owner
+pub fn ensure_curator_group_is_channel_owner<T: Trait>(
+    channel_owner: &ChannelOwner<T::MemberId, T::CuratorGroupId>,
+    group_id: &T::CuratorGroupId,
+) -> DispatchResult {
+    // Ensure curator group is channel owner
+    ensure!(
+        *channel_owner == ChannelOwner::CuratorGroup(*group_id),
+        Error::<T>::ActorNotAuthorized
+    );
+    Ok(())
+}
+
+/// Ensure specified valid member is channel owner
+pub fn ensure_member_is_channel_owner<T: Trait>(
+    channel_owner: &ChannelOwner<T::MemberId, T::CuratorGroupId>,
+    member_id: &T::MemberId,
+) -> DispatchResult {
+    // Ensure member is channel owner.
+    ensure!(
+        *channel_owner == ChannelOwner::Member(*member_id),
+        Error::<T>::ActorNotAuthorized
+    );
+    Ok(())
+}
+
+/// Ensure actor can set featured videos
 pub fn ensure_actor_authorized_to_set_featured_videos<T: Trait>(
     origin: T::Origin,
     actor: &ContentActor<T::CuratorGroupId, T::CuratorId, T::MemberId>,
@@ -184,6 +327,7 @@ pub fn ensure_actor_authorized_to_set_featured_videos<T: Trait>(
     }
 }
 
+/// Ensure actor can censor
 pub fn ensure_actor_authorized_to_censor<T: Trait>(
     origin: T::Origin,
     actor: &ContentActor<T::CuratorGroupId, T::CuratorId, T::MemberId>,
@@ -213,13 +357,14 @@ pub fn ensure_actor_authorized_to_censor<T: Trait>(
                 Ok(())
             }
         }
-        ContentActor::Member(_) => {
-            // Members cannot censore channels!
+        _ => {
+            // Members & collaborators cannot censore channels!
             Err(Error::<T>::ActorNotAuthorized.into())
         }
     }
 }
 
+/// Ensure actor can manage categories
 pub fn ensure_actor_authorized_to_manage_categories<T: Trait>(
     origin: T::Origin,
     actor: &ContentActor<T::CuratorGroupId, T::CuratorId, T::MemberId>,
@@ -236,8 +381,8 @@ pub fn ensure_actor_authorized_to_manage_categories<T: Trait>(
             // Authorize curator, performing all checks to ensure curator can act
             CuratorGroup::<T>::perform_curator_in_group_auth(curator_id, curator_group_id, &sender)
         }
-        ContentActor::Member(_) => {
-            // Members cannot censore channels!
+        _ => {
+            // Members & collaborators cannot manage categories!
             Err(Error::<T>::ActorNotAuthorized.into())
         }
     }
@@ -254,6 +399,7 @@ pub enum ContentActor<
     Curator(CuratorGroupId, CuratorId),
     Member(MemberId),
     Lead,
+    Collaborator(MemberId),
 }
 
 impl<

+ 195 - 118
runtime-modules/content/src/tests/channels.rs

@@ -2,8 +2,8 @@
 
 use super::curators;
 use super::mock::*;
-use crate::sp_api_hidden_includes_decl_storage::hidden_include::traits::Currency;
 use crate::*;
+use frame_support::traits::Currency;
 use frame_support::{assert_err, assert_ok};
 
 #[test]
@@ -46,6 +46,7 @@ fn successful_channel_deletion() {
                 assets: Some(assets),
                 meta: None,
                 reward_account: None,
+                collaborators: BTreeSet::new(),
             },
             Ok(()),
         );
@@ -78,6 +79,7 @@ fn successful_channel_deletion() {
                 assets: None,
                 meta: None,
                 reward_account: None,
+                collaborators: BTreeSet::new(),
             },
             Ok(()),
         );
@@ -129,8 +131,9 @@ fn successful_channel_assets_deletion() {
             ContentActor::Member(FIRST_MEMBER_ID),
             ChannelCreationParametersRecord {
                 assets: Some(assets),
-                meta: Some(vec![]),
+                meta: None,
                 reward_account: None,
+                collaborators: BTreeSet::new(),
             },
             Ok(()),
         );
@@ -148,6 +151,7 @@ fn successful_channel_assets_deletion() {
                 new_meta: None,
                 reward_account: None,
                 assets_to_remove: assets_to_remove,
+                collaborators: None,
             },
         ));
     })
@@ -205,8 +209,9 @@ fn succesful_channel_update() {
             ContentActor::Member(FIRST_MEMBER_ID),
             ChannelCreationParametersRecord {
                 assets: Some(first_batch),
-                meta: Some(vec![]),
+                meta: None,
                 reward_account: None,
+                collaborators: BTreeSet::new(),
             },
             Ok(()),
         );
@@ -218,9 +223,10 @@ fn succesful_channel_update() {
             channel_id,
             ChannelUpdateParametersRecord {
                 assets_to_upload: Some(second_batch),
-                new_meta: Some(vec![]),
+                new_meta: None,
                 reward_account: None,
                 assets_to_remove: BTreeSet::new(),
+                collaborators: None,
             },
             Ok(()),
         );
@@ -235,6 +241,7 @@ fn succesful_channel_update() {
                 new_meta: None,
                 reward_account: None,
                 assets_to_remove: first_batch_ids,
+                collaborators: None,
             },
             Ok(()),
         );
@@ -278,8 +285,9 @@ fn succesful_channel_creation() {
             ContentActor::Member(FIRST_MEMBER_ID),
             ChannelCreationParametersRecord {
                 assets: Some(assets),
-                meta: Some(vec![]),
+                meta: None,
                 reward_account: None,
+                collaborators: BTreeSet::new(),
             },
             Ok(()),
         );
@@ -295,8 +303,9 @@ fn lead_cannot_create_channel() {
                 ContentActor::Lead,
                 ChannelCreationParametersRecord {
                     assets: None,
-                    meta: Some(vec![]),
+                    meta: None,
                     reward_account: None,
+                    collaborators: BTreeSet::new(),
                 }
             ),
             Error::<Test>::ActorCannotOwnChannel
@@ -317,8 +326,9 @@ fn curator_owned_channels() {
                 ContentActor::Curator(FIRST_CURATOR_GROUP_ID, FIRST_CURATOR_ID),
                 ChannelCreationParametersRecord {
                     assets: None,
-                    meta: Some(vec![]),
+                    meta: None,
                     reward_account: None,
+                    collaborators: BTreeSet::new(),
                 }
             ),
             Error::<Test>::CuratorGroupIsNotActive
@@ -334,8 +344,9 @@ fn curator_owned_channels() {
                 ContentActor::Curator(FIRST_CURATOR_GROUP_ID, SECOND_CURATOR_ID),
                 ChannelCreationParametersRecord {
                     assets: None,
-                    meta: Some(vec![]),
+                    meta: None,
                     reward_account: None,
+                    collaborators: BTreeSet::new(),
                 }
             ),
             Error::<Test>::CuratorIsNotAMemberOfGivenCuratorGroup
@@ -348,8 +359,9 @@ fn curator_owned_channels() {
                 ContentActor::Curator(FIRST_CURATOR_GROUP_ID, FIRST_CURATOR_ID),
                 ChannelCreationParametersRecord {
                     assets: None,
-                    meta: Some(vec![]),
+                    meta: None,
                     reward_account: None,
+                    collaborators: BTreeSet::new(),
                 }
             ),
             Error::<Test>::CuratorAuthFailed
@@ -363,8 +375,9 @@ fn curator_owned_channels() {
             ContentActor::Curator(FIRST_CURATOR_GROUP_ID, FIRST_CURATOR_ID),
             ChannelCreationParametersRecord {
                 assets: None,
-                meta: Some(vec![]),
+                meta: None,
                 reward_account: None,
+                collaborators: BTreeSet::new(),
             }
         ));
 
@@ -377,13 +390,14 @@ fn curator_owned_channels() {
                     owner: ChannelOwner::CuratorGroup(FIRST_CURATOR_GROUP_ID),
                     is_censored: false,
                     reward_account: None,
-                    deletion_prize_source_account_id: FIRST_CURATOR_ORIGIN,
                     num_videos: 0,
+                    collaborators: BTreeSet::new(),
                 },
                 ChannelCreationParametersRecord {
                     assets: None,
-                    meta: Some(vec![]),
+                    meta: None,
                     reward_account: None,
+                    collaborators: BTreeSet::new(),
                 }
             ))
         );
@@ -398,7 +412,8 @@ fn curator_owned_channels() {
                 new_meta: None,
                 reward_account: None,
                 assets_to_remove: BTreeSet::new(),
-            },
+                collaborators: None,
+            }
         ));
 
         // Lead can update curator owned channels
@@ -411,148 +426,208 @@ fn curator_owned_channels() {
                 new_meta: None,
                 reward_account: None,
                 assets_to_remove: BTreeSet::new(),
-            },
+                collaborators: None,
+            }
         ));
     })
 }
 
 #[test]
-fn member_owned_channels() {
+fn invalid_member_cannot_create_channel() {
     with_default_mock_builder(|| {
         // Run to block one to see emitted events
         run_to_block(1);
 
         // Not a member
-        assert_err!(
-            Content::create_channel(
-                Origin::signed(UNKNOWN_ORIGIN),
-                ContentActor::Member(MEMBERS_COUNT + 1),
-                ChannelCreationParametersRecord {
-                    assets: None,
-                    meta: Some(vec![]),
-                    reward_account: None,
-                }
-            ),
-            Error::<Test>::MemberAuthFailed
+        create_channel_mock(
+            FIRST_MEMBER_ORIGIN,
+            ContentActor::Member(UNKNOWN_MEMBER_ID),
+            ChannelCreationParametersRecord {
+                assets: None,
+                meta: None,
+                reward_account: None,
+                collaborators: BTreeSet::new(),
+            },
+            Err(Error::<Test>::MemberAuthFailed.into()),
         );
+    })
+}
 
-        let channel_id_1 = Content::next_channel_id();
+#[test]
+fn invalid_member_cannot_update_channel() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
 
-        // Member can create the channel
-        assert_ok!(Content::create_channel(
-            Origin::signed(FIRST_MEMBER_ORIGIN),
+        create_channel_mock(
+            FIRST_MEMBER_ORIGIN,
             ContentActor::Member(FIRST_MEMBER_ID),
             ChannelCreationParametersRecord {
                 assets: None,
-                meta: Some(vec![]),
+                meta: None,
                 reward_account: None,
-            }
-        ));
-
-        assert_eq!(
-            System::events().last().unwrap().event,
-            MetaEvent::content(RawEvent::ChannelCreated(
-                ContentActor::Member(FIRST_MEMBER_ID),
-                channel_id_1,
-                ChannelRecord {
-                    owner: ChannelOwner::Member(FIRST_MEMBER_ID),
-                    is_censored: false,
-                    reward_account: None,
-                    deletion_prize_source_account_id: FIRST_MEMBER_ORIGIN,
+                collaborators: BTreeSet::new(),
+            },
+            Ok(()),
+        );
 
-                    num_videos: 0,
-                },
-                ChannelCreationParametersRecord {
-                    assets: None,
-                    meta: Some(vec![]),
-                    reward_account: None,
-                }
-            ))
+        update_channel_mock(
+            FIRST_MEMBER_ORIGIN,
+            ContentActor::Member(UNKNOWN_MEMBER_ID),
+            <Test as storage::Trait>::ChannelId::one(),
+            ChannelUpdateParametersRecord {
+                assets_to_upload: None,
+                new_meta: None,
+                reward_account: None,
+                collaborators: None,
+                assets_to_remove: BTreeSet::new(),
+            },
+            Err(Error::<Test>::MemberAuthFailed.into()),
         );
+    })
+}
 
-        let channel_id_2 = Content::next_channel_id();
+#[test]
+fn invalid_member_cannot_delete_channel() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
 
-        // Member can create the channel
-        assert_ok!(Content::create_channel(
-            Origin::signed(SECOND_MEMBER_ORIGIN),
-            ContentActor::Member(SECOND_MEMBER_ID),
+        create_channel_mock(
+            FIRST_MEMBER_ORIGIN,
+            ContentActor::Member(FIRST_MEMBER_ID),
             ChannelCreationParametersRecord {
                 assets: None,
-                meta: Some(vec![]),
+                meta: None,
                 reward_account: None,
-            }
-        ));
+                collaborators: BTreeSet::new(),
+            },
+            Ok(()),
+        );
 
-        assert_eq!(
-            System::events().last().unwrap().event,
-            MetaEvent::content(RawEvent::ChannelCreated(
-                ContentActor::Member(SECOND_MEMBER_ID),
-                channel_id_2,
-                ChannelRecord {
-                    owner: ChannelOwner::Member(SECOND_MEMBER_ID),
-                    is_censored: false,
-                    reward_account: None,
-                    deletion_prize_source_account_id: SECOND_MEMBER_ORIGIN,
+        delete_channel_mock(
+            FIRST_MEMBER_ORIGIN,
+            ContentActor::Member(UNKNOWN_MEMBER_ID),
+            <Test as storage::Trait>::ChannelId::one(),
+            0u64,
+            Err(Error::<Test>::MemberAuthFailed.into()),
+        );
+    })
+}
 
-                    num_videos: 0,
-                },
-                ChannelCreationParametersRecord {
-                    assets: None,
-                    meta: Some(vec![]),
-                    reward_account: None,
-                }
-            ))
+#[test]
+fn non_authorized_collaborators_cannot_update_channel() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        helper_init_accounts(vec![FIRST_MEMBER_ORIGIN, COLLABORATOR_MEMBER_ORIGIN]);
+
+        // create channel
+        create_channel_mock(
+            FIRST_MEMBER_ORIGIN,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            ChannelCreationParametersRecord {
+                assets: Some(helper_generate_storage_assets(vec![2, 3])),
+                meta: None,
+                reward_account: None,
+                collaborators: BTreeSet::new(),
+            },
+            Ok(()),
         );
 
-        // Update channel
-        assert_ok!(Content::update_channel(
-            Origin::signed(FIRST_MEMBER_ORIGIN),
+        // attempt for an non auth. collaborator to update channel assets
+        update_channel_mock(
+            COLLABORATOR_MEMBER_ORIGIN,
+            ContentActor::Collaborator(COLLABORATOR_MEMBER_ID),
+            <Test as storage::Trait>::ChannelId::one(),
+            ChannelUpdateParametersRecord {
+                assets_to_upload: Some(helper_generate_storage_assets(vec![5])),
+                new_meta: None,
+                reward_account: None,
+                assets_to_remove: vec![DataObjectId::<Test>::one()]
+                    .into_iter()
+                    .collect::<BTreeSet<_>>(),
+                collaborators: None,
+            },
+            Err(Error::<Test>::ActorNotAuthorized.into()),
+        );
+
+        // add collaborators
+        update_channel_mock(
+            FIRST_MEMBER_ORIGIN,
             ContentActor::Member(FIRST_MEMBER_ID),
-            channel_id_1,
+            <Test as storage::Trait>::ChannelId::one(),
             ChannelUpdateParametersRecord {
                 assets_to_upload: None,
                 new_meta: None,
                 reward_account: None,
                 assets_to_remove: BTreeSet::new(),
+                collaborators: Some(
+                    vec![COLLABORATOR_MEMBER_ID]
+                        .into_iter()
+                        .collect::<BTreeSet<_>>(),
+                ),
             },
-        ));
+            Ok(()),
+        );
 
-        assert_eq!(
-            System::events().last().unwrap().event,
-            MetaEvent::content(RawEvent::ChannelUpdated(
-                ContentActor::Member(FIRST_MEMBER_ID),
-                channel_id_1,
-                ChannelRecord {
-                    owner: ChannelOwner::Member(FIRST_MEMBER_ID),
-                    is_censored: false,
-                    reward_account: None,
-                    deletion_prize_source_account_id: FIRST_MEMBER_ORIGIN,
+        // attempt for a valid collaborator to update channel fields outside
+        // of his scope
+        update_channel_mock(
+            COLLABORATOR_MEMBER_ORIGIN,
+            ContentActor::Collaborator(COLLABORATOR_MEMBER_ID),
+            <Test as storage::Trait>::ChannelId::one(),
+            ChannelUpdateParametersRecord {
+                assets_to_upload: None,
+                new_meta: None,
+                reward_account: Some(Some(COLLABORATOR_MEMBER_ORIGIN)),
+                assets_to_remove: BTreeSet::new(),
+                collaborators: None,
+            },
+            Err(Error::<Test>::ActorNotAuthorized.into()),
+        );
+    })
+}
 
-                    num_videos: 0,
-                },
-                ChannelUpdateParametersRecord {
-                    assets_to_upload: None,
-                    new_meta: None,
-                    reward_account: None,
-                    assets_to_remove: BTreeSet::new(),
-                }
-            ))
+#[test]
+fn authorized_collaborators_can_update_channel() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        helper_init_accounts(vec![FIRST_MEMBER_ORIGIN, COLLABORATOR_MEMBER_ORIGIN]);
+
+        // create channel
+        create_channel_mock(
+            FIRST_MEMBER_ORIGIN,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            ChannelCreationParametersRecord {
+                assets: Some(helper_generate_storage_assets(vec![2, 3])),
+                meta: None,
+                reward_account: None,
+                collaborators: vec![COLLABORATOR_MEMBER_ID]
+                    .into_iter()
+                    .collect::<BTreeSet<_>>(),
+            },
+            Ok(()),
         );
 
-        // Member cannot update a channel they do not own
-        assert_err!(
-            Content::update_channel(
-                Origin::signed(FIRST_MEMBER_ORIGIN),
-                ContentActor::Member(FIRST_MEMBER_ID),
-                channel_id_2,
-                ChannelUpdateParametersRecord {
-                    assets_to_upload: None,
-                    new_meta: None,
-                    reward_account: None,
-                    assets_to_remove: BTreeSet::new(),
-                },
-            ),
-            Error::<Test>::ActorNotAuthorized
+        // attempt for an auth. collaborator to update channel assets
+        update_channel_mock(
+            COLLABORATOR_MEMBER_ORIGIN,
+            ContentActor::Collaborator(COLLABORATOR_MEMBER_ID),
+            <Test as storage::Trait>::ChannelId::one(),
+            ChannelUpdateParametersRecord {
+                assets_to_upload: Some(helper_generate_storage_assets(vec![5])),
+                new_meta: None,
+                reward_account: None,
+                assets_to_remove: vec![DataObjectId::<Test>::one()]
+                    .into_iter()
+                    .collect::<BTreeSet<_>>(),
+                collaborators: None,
+            },
+            Ok(()),
         );
     })
 }
@@ -569,8 +644,9 @@ fn channel_censoring() {
             ContentActor::Member(FIRST_MEMBER_ID),
             ChannelCreationParametersRecord {
                 assets: None,
-                meta: Some(vec![]),
+                meta: None,
                 reward_account: None,
+                collaborators: BTreeSet::new(),
             }
         ));
 
@@ -645,8 +721,9 @@ fn channel_censoring() {
             ContentActor::Curator(group_id, FIRST_CURATOR_ID),
             ChannelCreationParametersRecord {
                 assets: None,
-                meta: Some(vec![]),
+                meta: None,
                 reward_account: None,
+                collaborators: BTreeSet::new(),
             }
         ));
 

+ 75 - 8
runtime-modules/content/src/tests/mock.rs

@@ -1,9 +1,8 @@
 #![cfg(test)]
 
 use crate::*;
-
 use frame_support::dispatch::{DispatchError, DispatchResult};
-use frame_support::traits::{OnFinalize, OnInitialize};
+use frame_support::traits::{Currency, OnFinalize, OnInitialize};
 use frame_support::{impl_outer_event, impl_outer_origin, parameter_types};
 use sp_core::H256;
 use sp_runtime::{
@@ -31,6 +30,7 @@ pub const SECOND_CURATOR_ORIGIN: u64 = 3;
 pub const FIRST_MEMBER_ORIGIN: u64 = 4;
 pub const SECOND_MEMBER_ORIGIN: u64 = 5;
 pub const UNKNOWN_ORIGIN: u64 = 7777;
+pub const UNKNOWN_MEMBER_ID: u64 = 7777;
 
 // Members range from MemberId 1 to 10
 pub const MEMBERS_COUNT: MemberId = 10;
@@ -46,6 +46,14 @@ pub const FIRST_CURATOR_GROUP_ID: CuratorGroupId = 1;
 pub const FIRST_MEMBER_ID: MemberId = 1;
 pub const SECOND_MEMBER_ID: MemberId = 2;
 
+// members that act as collaborators
+pub const COLLABORATOR_MEMBER_ORIGIN: MemberId = 8;
+pub const COLLABORATOR_MEMBER_ID: MemberId = 9;
+
+/// Constants
+// initial balancer for an account
+pub const INIT_BALANCE: u32 = 500;
+
 impl_outer_origin! {
     pub enum Origin for Test {}
 }
@@ -162,6 +170,10 @@ impl ContentActorAuthenticator for Test {
     type CuratorId = u64;
     type CuratorGroupId = u64;
 
+    fn validate_member_id(member_id: &Self::MemberId) -> bool {
+        *member_id < MEMBERS_COUNT
+    }
+
     fn is_lead(account_id: &Self::AccountId) -> bool {
         let lead_account_id = ensure_signed(Origin::signed(LEAD_ORIGIN)).unwrap();
         *account_id == lead_account_id
@@ -455,11 +467,12 @@ pub fn create_channel_mock(
                 ChannelRecord {
                     owner: owner,
                     is_censored: false,
-                    reward_account: params.reward_account,
-                    deletion_prize_source_account_id: sender,
+                    reward_account: params.reward_account.clone(),
+
+                    collaborators: params.collaborators.clone(),
                     num_videos: 0,
                 },
-                params.clone(),
+                params,
             ))
         );
     }
@@ -493,11 +506,16 @@ pub fn update_channel_mock(
                 ChannelRecord {
                     owner: channel_pre.owner.clone(),
                     is_censored: channel_pre.is_censored,
-                    reward_account: channel_pre.reward_account.clone(),
-                    deletion_prize_source_account_id: sender,
+                    reward_account: params
+                        .reward_account
+                        .map_or_else(|| channel_pre.reward_account.clone(), |account| account),
+                    collaborators: params
+                        .collaborators
+                        .clone()
+                        .unwrap_or(channel_pre.collaborators),
                     num_videos: channel_pre.num_videos,
                 },
-                params.clone(),
+                params,
             ))
         );
     }
@@ -595,3 +613,52 @@ pub fn update_video_mock(
         );
     }
 }
+
+pub fn delete_video_mock(
+    sender: u64,
+    actor: ContentActor<CuratorGroupId, CuratorId, MemberId>,
+    video_id: <Test as Trait>::VideoId,
+    assets_to_remove: BTreeSet<DataObjectId<Test>>,
+    result: DispatchResult,
+) {
+    assert_eq!(
+        Content::delete_video(
+            Origin::signed(sender),
+            actor.clone(),
+            video_id.clone(),
+            assets_to_remove.clone(),
+        ),
+        result.clone(),
+    );
+
+    if result.is_ok() {
+        assert_eq!(
+            System::events().last().unwrap().event,
+            MetaEvent::content(RawEvent::VideoDeleted(actor.clone(), video_id))
+        );
+    }
+}
+
+// helper functions
+pub fn helper_generate_storage_assets(sizes: Vec<u64>) -> StorageAssets<Test> {
+    StorageAssetsRecord {
+        object_creation_list: sizes
+            .into_iter()
+            .map(|s| DataObjectCreationParameters {
+                size: s,
+                ipfs_content_id: s.encode(),
+            })
+            .collect::<Vec<_>>(),
+        expected_data_size_fee: storage::DataObjectPerMegabyteFee::<Test>::get(),
+    }
+}
+
+pub fn helper_init_accounts(accounts: Vec<u64>) {
+    // give channel owner funds to permit collaborators to update assets
+    for acc in accounts.iter() {
+        let _ = balances::Module::<Test>::deposit_creating(
+            acc,
+            <Test as balances::Trait>::Balance::from(INIT_BALANCE),
+        );
+    }
+}

+ 275 - 5
runtime-modules/content/src/tests/videos.rs

@@ -1,9 +1,8 @@
 #![cfg(test)]
-
 use super::curators;
 use super::mock::*;
-use crate::sp_api_hidden_includes_decl_storage::hidden_include::traits::Currency;
 use crate::*;
+use frame_support::traits::Currency;
 use frame_support::{assert_err, assert_ok};
 
 fn create_member_channel() -> ChannelId {
@@ -15,8 +14,9 @@ fn create_member_channel() -> ChannelId {
         ContentActor::Member(FIRST_MEMBER_ID),
         ChannelCreationParametersRecord {
             assets: None,
-            meta: Some(vec![]),
+            meta: None,
             reward_account: None,
+            collaborators: BTreeSet::<MemberId>::new(),
         }
     ));
 
@@ -41,8 +41,9 @@ fn video_creation_successful() {
             ContentActor::Member(FIRST_MEMBER_ID),
             ChannelCreationParametersRecord {
                 assets: None,
-                meta: Some(vec![]),
+                meta: None,
                 reward_account: None,
+                collaborators: BTreeSet::new(),
             },
             Ok(()),
         );
@@ -95,8 +96,9 @@ fn video_update_successful() {
             ContentActor::Member(FIRST_MEMBER_ID),
             ChannelCreationParametersRecord {
                 assets: None,
-                meta: Some(vec![]),
+                meta: None,
                 reward_account: None,
+                collaborators: BTreeSet::new(),
             },
             Ok(()),
         );
@@ -404,3 +406,271 @@ fn featured_videos() {
         );
     })
 }
+
+#[test]
+fn non_authorized_collaborators_cannot_add_video() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        helper_init_accounts(vec![FIRST_MEMBER_ORIGIN, COLLABORATOR_MEMBER_ORIGIN]);
+
+        // create channel
+        create_channel_mock(
+            FIRST_MEMBER_ORIGIN,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            ChannelCreationParametersRecord {
+                assets: Some(helper_generate_storage_assets(vec![2, 3])),
+                meta: None,
+                reward_account: None,
+                collaborators: BTreeSet::new(),
+            },
+            Ok(()),
+        );
+
+        create_video_mock(
+            COLLABORATOR_MEMBER_ORIGIN,
+            ContentActor::Collaborator(COLLABORATOR_MEMBER_ID),
+            <Test as storage::Trait>::ChannelId::one(),
+            VideoCreationParametersRecord {
+                assets: Some(helper_generate_storage_assets(vec![1, 2])),
+                meta: None,
+            },
+            Err(Error::<Test>::ActorNotAuthorized.into()),
+        );
+    })
+}
+
+#[test]
+fn non_authorized_collaborators_cannot_update_video() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        helper_init_accounts(vec![FIRST_MEMBER_ORIGIN, COLLABORATOR_MEMBER_ORIGIN]);
+
+        // create channel
+        create_channel_mock(
+            FIRST_MEMBER_ORIGIN,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            ChannelCreationParametersRecord {
+                assets: Some(helper_generate_storage_assets(vec![2, 3])),
+                meta: None,
+                reward_account: None,
+                collaborators: BTreeSet::new(),
+            },
+            Ok(()),
+        );
+
+        // create video
+        create_video_mock(
+            FIRST_MEMBER_ORIGIN,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            <Test as storage::Trait>::ChannelId::one(),
+            VideoCreationParametersRecord {
+                assets: Some(helper_generate_storage_assets(vec![1, 2])),
+                meta: None,
+            },
+            Ok(()),
+        );
+
+        update_video_mock(
+            COLLABORATOR_MEMBER_ORIGIN,
+            ContentActor::Collaborator(COLLABORATOR_MEMBER_ID),
+            <Test as Trait>::VideoId::one(),
+            VideoUpdateParametersRecord {
+                assets_to_upload: Some(helper_generate_storage_assets(vec![5])),
+                new_meta: None,
+                assets_to_remove: vec![DataObjectId::<Test>::one()]
+                    .into_iter()
+                    .collect::<BTreeSet<_>>(),
+            },
+            Err(Error::<Test>::ActorNotAuthorized.into()),
+        );
+    })
+}
+
+#[test]
+fn non_authorized_collaborators_cannot_delete_video() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        helper_init_accounts(vec![FIRST_MEMBER_ORIGIN, COLLABORATOR_MEMBER_ORIGIN]);
+
+        // create channel
+        create_channel_mock(
+            FIRST_MEMBER_ORIGIN,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            ChannelCreationParametersRecord {
+                assets: Some(helper_generate_storage_assets(vec![2, 3])),
+                meta: None,
+                reward_account: None,
+                collaborators: BTreeSet::new(),
+            },
+            Ok(()),
+        );
+
+        // create video
+        create_video_mock(
+            FIRST_MEMBER_ORIGIN,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            <Test as storage::Trait>::ChannelId::one(),
+            VideoCreationParametersRecord {
+                assets: Some(helper_generate_storage_assets(vec![1, 2])),
+                meta: None,
+            },
+            Ok(()),
+        );
+
+        delete_video_mock(
+            COLLABORATOR_MEMBER_ORIGIN,
+            ContentActor::Collaborator(COLLABORATOR_MEMBER_ID),
+            <Test as Trait>::VideoId::one(),
+            vec![
+                DataObjectId::<Test>::one(),
+                DataObjectId::<Test>::from(2u64),
+            ]
+            .into_iter()
+            .collect::<BTreeSet<_>>(),
+            Err(Error::<Test>::ActorNotAuthorized.into()),
+        );
+    })
+}
+
+#[test]
+fn authorized_collaborators_can_add_video() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        helper_init_accounts(vec![FIRST_MEMBER_ORIGIN, COLLABORATOR_MEMBER_ORIGIN]);
+
+        // create channel
+        create_channel_mock(
+            FIRST_MEMBER_ORIGIN,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            ChannelCreationParametersRecord {
+                assets: Some(helper_generate_storage_assets(vec![2, 3])),
+                meta: None,
+                reward_account: None,
+                collaborators: vec![COLLABORATOR_MEMBER_ID]
+                    .into_iter()
+                    .collect::<BTreeSet<_>>(),
+            },
+            Ok(()),
+        );
+
+        create_video_mock(
+            COLLABORATOR_MEMBER_ORIGIN,
+            ContentActor::Collaborator(COLLABORATOR_MEMBER_ID),
+            <Test as storage::Trait>::ChannelId::one(),
+            VideoCreationParametersRecord {
+                assets: Some(helper_generate_storage_assets(vec![1, 2])),
+                meta: None,
+            },
+            Ok(()),
+        );
+    })
+}
+
+#[test]
+fn authorized_collaborators_can_update_video() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        helper_init_accounts(vec![FIRST_MEMBER_ORIGIN, COLLABORATOR_MEMBER_ORIGIN]);
+
+        // create channel
+        create_channel_mock(
+            FIRST_MEMBER_ORIGIN,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            ChannelCreationParametersRecord {
+                assets: Some(helper_generate_storage_assets(vec![2, 3])),
+                meta: None,
+                reward_account: None,
+                collaborators: vec![COLLABORATOR_MEMBER_ID]
+                    .into_iter()
+                    .collect::<BTreeSet<_>>(),
+            },
+            Ok(()),
+        );
+
+        // create video
+        create_video_mock(
+            COLLABORATOR_MEMBER_ORIGIN,
+            ContentActor::Collaborator(COLLABORATOR_MEMBER_ID),
+            <Test as storage::Trait>::ChannelId::one(),
+            VideoCreationParametersRecord {
+                assets: Some(helper_generate_storage_assets(vec![1, 2])),
+                meta: None,
+            },
+            Ok(()),
+        );
+
+        update_video_mock(
+            COLLABORATOR_MEMBER_ORIGIN,
+            ContentActor::Collaborator(COLLABORATOR_MEMBER_ID),
+            <Test as Trait>::VideoId::one(),
+            VideoUpdateParametersRecord {
+                assets_to_upload: Some(helper_generate_storage_assets(vec![5])),
+                new_meta: None,
+                assets_to_remove: vec![DataObjectId::<Test>::one()]
+                    .into_iter()
+                    .collect::<BTreeSet<_>>(),
+            },
+            Ok(()),
+        );
+    })
+}
+
+#[test]
+fn authorized_collaborators_can_delete_video() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        helper_init_accounts(vec![FIRST_MEMBER_ORIGIN, COLLABORATOR_MEMBER_ORIGIN]);
+
+        // create channel
+        create_channel_mock(
+            FIRST_MEMBER_ORIGIN,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            ChannelCreationParametersRecord {
+                assets: Some(helper_generate_storage_assets(vec![2, 3])),
+                meta: None,
+                reward_account: None,
+                collaborators: vec![COLLABORATOR_MEMBER_ID]
+                    .into_iter()
+                    .collect::<BTreeSet<_>>(),
+            },
+            Ok(()),
+        );
+
+        // create video
+        create_video_mock(
+            COLLABORATOR_MEMBER_ORIGIN,
+            ContentActor::Collaborator(COLLABORATOR_MEMBER_ID),
+            <Test as storage::Trait>::ChannelId::one(),
+            VideoCreationParametersRecord {
+                assets: Some(helper_generate_storage_assets(vec![1, 2])),
+                meta: None,
+            },
+            Ok(()),
+        );
+
+        delete_video_mock(
+            COLLABORATOR_MEMBER_ORIGIN,
+            ContentActor::Collaborator(COLLABORATOR_MEMBER_ID),
+            <Test as Trait>::VideoId::one(),
+            vec![
+                DataObjectId::<Test>::one(),
+                DataObjectId::<Test>::from(2u64),
+            ]
+            .into_iter()
+            .collect::<BTreeSet<_>>(),
+            Ok(()),
+        );
+    })
+}

+ 3 - 0
runtime/src/integration/content_directory.rs

@@ -36,4 +36,7 @@ impl content::ContentActorAuthenticator for Runtime {
     fn is_valid_curator_id(curator_id: &Self::CuratorId) -> bool {
         ContentWorkingGroup::ensure_worker_exists(curator_id).is_ok()
     }
+    fn validate_member_id(member_id: &Self::MemberId) -> bool {
+        membership::Module::<Runtime>::ensure_membership(*member_id).is_ok()
+    }
 }

+ 2 - 2
scripts/cargo-build.sh

@@ -1,5 +1,5 @@
 #!/usr/bin/env bash
 
-export WASM_BUILD_TOOLCHAIN=nightly-2021-03-24
+export WASM_BUILD_TOOLCHAIN=nightly-2021-02-20
 
-cargo build --release
+cargo +nightly-2021-02-20 build --release

+ 2 - 2
scripts/cargo-tests-with-networking.sh

@@ -1,7 +1,7 @@
 #!/bin/sh
 set -e
 
-export WASM_BUILD_TOOLCHAIN=nightly-2021-03-24
+export WASM_BUILD_TOOLCHAIN=nightly-2021-02-20
 
 echo 'running all cargo tests'
-cargo test --release --all -- --ignored
+cargo +nightly-2021-02-20 test --release --all -- --ignored

+ 1 - 1
scripts/raspberry-cross-build.sh

@@ -9,7 +9,7 @@
 export WORKSPACE_ROOT=`cargo metadata --offline --no-deps --format-version 1 | jq .workspace_root -r`
 
 docker run \
-    -e WASM_BUILD_TOOLCHAIN=nightly-2021-03-24 \
+    -e WASM_BUILD_TOOLCHAIN=nightly-2021-02-20 \
     --volume ${WORKSPACE_ROOT}/:/home/cross/project \
     --volume ${HOME}/.cargo/registry:/home/cross/.cargo/registry \
     joystream/rust-raspberry \

+ 4 - 4
scripts/run-dev-chain.sh

@@ -1,13 +1,13 @@
 #!/usr/bin/env bash
 
-export WASM_BUILD_TOOLCHAIN=nightly-2021-03-24
+export WASM_BUILD_TOOLCHAIN=nightly-2021-02-20
 
 # Build release binary
-cargo build --release
+cargo +nightly-2021-02-20 build --release
 
 # Purge existing local chain
-yes | cargo run --release -- purge-chain --dev
+yes | cargo +nightly-2021-02-20 run --release -- purge-chain --dev
 
 # Run local development chain -
 # No need to specify `-p joystream-node` it is the default bin crate in the cargo workspace
-cargo run --release -- --dev
+cargo +nightly-2021-02-20 run --release -- --dev

+ 2 - 4
setup.sh

@@ -27,10 +27,8 @@ curl https://getsubstrate.io -sSf | bash -s -- --fast
 
 source ~/.cargo/env
 
-rustup install nightly-2021-03-24
-rustup target add wasm32-unknown-unknown --toolchain nightly-2021-03-24
-
-rustup default nightly-2021-03-24
+rustup install nightly-2021-02-20
+rustup target add wasm32-unknown-unknown --toolchain nightly-2021-02-20
 
 rustup component add rustfmt clippy
 

+ 8 - 6
types/augment/all/defs.json

@@ -529,8 +529,7 @@
     "StorageBucket": {
         "operator_status": "StorageBucketOperatorStatus",
         "accepting_new_bags": "bool",
-        "voucher": "Voucher",
-        "metadata": "Bytes"
+        "voucher": "Voucher"
     },
     "StaticBagId": {
         "_enum": {
@@ -765,7 +764,8 @@
         "_enum": {
             "Curator": "(CuratorGroupId,CuratorId)",
             "Member": "MemberId",
-            "Lead": "Null"
+            "Lead": "Null",
+            "Collaborator": "MemberId"
         }
     },
     "StorageAssets": {
@@ -777,7 +777,7 @@
         "num_videos": "u64",
         "is_censored": "bool",
         "reward_account": "Option<GenericAccountId>",
-        "deletion_prize_source_account_id": "GenericAccountId"
+        "collaborators": "BTreeSet<MemberId>"
     },
     "ChannelOwner": {
         "_enum": {
@@ -796,13 +796,15 @@
     "ChannelCreationParameters": {
         "assets": "Option<StorageAssets>",
         "meta": "Option<Bytes>",
-        "reward_account": "Option<GenericAccountId>"
+        "reward_account": "Option<GenericAccountId>",
+        "collaborators": "BTreeSet<MemberId>"
     },
     "ChannelUpdateParameters": {
         "assets_to_upload": "Option<StorageAssets>",
         "new_meta": "Option<Bytes>",
         "reward_account": "Option<Option<GenericAccountId>>",
-        "assets_to_remove": "BTreeSet<DataObjectId>"
+        "assets_to_remove": "BTreeSet<DataObjectId>",
+        "collaborators": "Option<BTreeSet<MemberId>>"
     },
     "ChannelOwnershipTransferRequestId": "u64",
     "ChannelOwnershipTransferRequest": {

+ 5 - 2
types/augment/all/types.ts

@@ -210,7 +210,7 @@ export interface Channel extends Struct {
   readonly num_videos: u64;
   readonly is_censored: bool;
   readonly reward_account: Option<GenericAccountId>;
-  readonly deletion_prize_source_account_id: GenericAccountId;
+  readonly collaborators: BTreeSet<MemberId>;
 }
 
 /** @name ChannelCategory */
@@ -237,6 +237,7 @@ export interface ChannelCreationParameters extends Struct {
   readonly assets: Option<StorageAssets>;
   readonly meta: Option<Bytes>;
   readonly reward_account: Option<GenericAccountId>;
+  readonly collaborators: BTreeSet<MemberId>;
 }
 
 /** @name ChannelCurationStatus */
@@ -273,6 +274,7 @@ export interface ChannelUpdateParameters extends Struct {
   readonly new_meta: Option<Bytes>;
   readonly reward_account: Option<Option<GenericAccountId>>;
   readonly assets_to_remove: BTreeSet<DataObjectId>;
+  readonly collaborators: Option<BTreeSet<MemberId>>;
 }
 
 /** @name ChildPositionInParentCategory */
@@ -309,6 +311,8 @@ export interface ContentActor extends Enum {
   readonly isMember: boolean;
   readonly asMember: MemberId;
   readonly isLead: boolean;
+  readonly isCollaborator: boolean;
+  readonly asCollaborator: MemberId;
 }
 
 /** @name ContentIdSet */
@@ -1211,7 +1215,6 @@ export interface StorageBucket extends Struct {
   readonly operator_status: StorageBucketOperatorStatus;
   readonly accepting_new_bags: bool;
   readonly voucher: Voucher;
-  readonly metadata: Bytes;
 }
 
 /** @name StorageBucketId */

+ 5 - 1
types/src/content/index.ts

@@ -2,6 +2,7 @@ import { Vec, Option, Tuple, BTreeSet } from '@polkadot/types'
 import { bool, u64, u32, u128, Null, Bytes } from '@polkadot/types/primitive'
 import { MemberId } from '../members'
 import { JoyStructDecorated, JoyEnum, ChannelId } from '../common'
+
 import { GenericAccountId as AccountId } from '@polkadot/types/generic/AccountId'
 import { DataObjectId, DataObjectCreationParameters } from '../storage'
 
@@ -31,6 +32,7 @@ export class ContentActor extends JoyEnum({
   Curator: Tuple.with([CuratorGroupId, CuratorId]),
   Member: MemberId,
   Lead: Null,
+  Collaborator: MemberId,
 }) {}
 
 export class ChannelOwner extends JoyEnum({
@@ -43,13 +45,14 @@ export class Channel extends JoyStructDecorated({
   num_videos: u64,
   is_censored: bool,
   reward_account: Option.with(AccountId),
-  deletion_prize_source_account_id: AccountId,
+  collaborators: BTreeSet.with(MemberId),
 }) {}
 
 export class ChannelCreationParameters extends JoyStructDecorated({
   assets: Option.with(StorageAssets),
   meta: Option.with(Bytes),
   reward_account: Option.with(AccountId),
+  collaborators: BTreeSet.with(MemberId),
 }) {}
 
 export class ChannelUpdateParameters extends JoyStructDecorated({
@@ -57,6 +60,7 @@ export class ChannelUpdateParameters extends JoyStructDecorated({
   new_meta: Option.with(Bytes),
   reward_account: Option.with(Option.with(AccountId)),
   assets_to_remove: BTreeSet.with(DataObjectId),
+  collaborators: Option.with(BTreeSet.with(MemberId)),
 }) {}
 
 export class ChannelOwnershipTransferRequest extends JoyStructDecorated({

+ 0 - 2
types/src/storage.ts

@@ -171,7 +171,6 @@ export type IStorageBucket = {
   operator_status: StorageBucketOperatorStatus
   accepting_new_bags: bool
   voucher: Voucher
-  metadata: Bytes
 }
 
 export class StorageBucket
@@ -179,7 +178,6 @@ export class StorageBucket
     operator_status: StorageBucketOperatorStatus,
     accepting_new_bags: bool,
     voucher: Voucher,
-    metadata: Bytes,
   })
   implements IStorageBucket {}
 

Some files were not shown because too many files changed in this diff