Browse Source

Merge branch 'vnft_unit_tests' into vnft_schema_mappings

iorveth 3 years ago
parent
commit
044d663158
50 changed files with 7200 additions and 1233 deletions
  1. 1 0
      .dockerignore
  2. 5 4
      .env
  3. 4 0
      Cargo.lock
  4. 11 3
      apps.Dockerfile
  5. 10 18
      docker-compose.yml
  6. 1 1
      metadata-protobuf/package.json
  7. 1 1
      metadata-protobuf/src/utils.ts
  8. 6 3
      node/src/chain_spec/mod.rs
  9. 0 1
      query-node/build.sh
  10. 17 0
      query-node/kill-img.sh
  11. 4 4
      query-node/package.json
  12. 8 0
      query-node/schemas/storage.graphql
  13. 23 0
      query-node/start-img.sh
  14. 8 2
      query-node/start.sh
  15. 1 11
      runtime-modules/common/src/lib.rs
  16. 8 14
      runtime-modules/common/src/storage.rs
  17. 8 2
      runtime-modules/content/Cargo.toml
  18. 55 28
      runtime-modules/content/src/errors.rs
  19. 464 310
      runtime-modules/content/src/lib.rs
  20. 201 218
      runtime-modules/content/src/nft/mod.rs
  21. 263 99
      runtime-modules/content/src/nft/types.rs
  22. 61 10
      runtime-modules/content/src/permissions/mod.rs
  23. 336 44
      runtime-modules/content/src/tests/channels.rs
  24. 1 1
      runtime-modules/content/src/tests/curators.rs
  25. 557 38
      runtime-modules/content/src/tests/mock.rs
  26. 1 0
      runtime-modules/content/src/tests/mod.rs
  27. 13 0
      runtime-modules/content/src/tests/nft.rs
  28. 287 0
      runtime-modules/content/src/tests/nft/accept_incoming_offer.rs
  29. 344 0
      runtime-modules/content/src/tests/nft/buy_nft.rs
  30. 226 0
      runtime-modules/content/src/tests/nft/cancel_buy_now.rs
  31. 288 0
      runtime-modules/content/src/tests/nft/cancel_nft_auction.rs
  32. 223 0
      runtime-modules/content/src/tests/nft/cancel_offer.rs
  33. 509 0
      runtime-modules/content/src/tests/nft/cancel_open_auction_bid.rs
  34. 444 0
      runtime-modules/content/src/tests/nft/claim_won_english_auction.rs
  35. 198 0
      runtime-modules/content/src/tests/nft/issue_nft.rs
  36. 660 0
      runtime-modules/content/src/tests/nft/make_bid.rs
  37. 222 0
      runtime-modules/content/src/tests/nft/offer_nft.rs
  38. 441 0
      runtime-modules/content/src/tests/nft/pick_open_auction_winner.rs
  39. 226 0
      runtime-modules/content/src/tests/nft/sell_nft.rs
  40. 676 0
      runtime-modules/content/src/tests/nft/start_nft_auction.rs
  41. 148 30
      runtime-modules/content/src/tests/videos.rs
  42. 180 318
      runtime-modules/content/src/types.rs
  43. 5 0
      runtime-modules/governance/src/mock.rs
  44. 18 18
      runtime-modules/membership/src/lib.rs
  45. 25 7
      runtime-modules/storage/src/lib.rs
  46. 1 0
      runtime-modules/storage/src/tests/mocks.rs
  47. 10 6
      runtime-modules/storage/src/tests/mod.rs
  48. 1 34
      runtime/src/lib.rs
  49. 0 4
      types/augment-codec/augment-api-errors.ts
  50. 0 4
      types/augment/augment-api-errors.ts

+ 1 - 0
.dockerignore

@@ -7,3 +7,4 @@ query-node/lib
 cli/
 tests/
 devops/
+metadata-protobuf/lib

+ 5 - 4
.env

@@ -2,14 +2,14 @@ COMPOSE_PROJECT_NAME=joystream
 PROJECT_NAME=query_node
 
 # We will use a single postgres service with multiple databases
-# The env variables below are by default used by all services and should be 
+# The env variables below are by default used by all services and should be
 # overriden in local env files
 # DB config
 INDEXER_DB_NAME=query_node_indexer
 DB_NAME=query_node_processor
 DB_USER=postgres
 DB_PASS=postgres
-DB_HOST=localhost
+DB_HOST=db
 DB_PORT=5432
 DEBUG=index-builder:*
 TYPEORM_LOGGING=error
@@ -30,9 +30,10 @@ BLOCK_HEIGHT=0
 ###############################
 
 GRAPHQL_SERVER_PORT=4002
-GRAPHQL_SERVER_HOST=localhost
+GRAPHQL_SERVER_HOST=graphql-server
+
 WARTHOG_APP_PORT=4002
-WARTHOG_APP_HOST=localhost
+WARTHOG_APP_HOST=hydra-indexer-gateway
 
 # Default configuration is to use the docker container
 WS_PROVIDER_ENDPOINT_URI=ws://joystream-node:9944/

+ 4 - 0
Cargo.lock

@@ -1,5 +1,7 @@
 # This file is automatically @generated by Cargo.
 # It is not intended for manual editing.
+version = 3
+
 [[package]]
 name = "Inflector"
 version = "0.11.4"
@@ -3827,6 +3829,8 @@ dependencies = [
  "pallet-balances",
  "pallet-common",
  "pallet-membership",
+ "pallet-randomness-collective-flip",
+ "pallet-storage",
  "pallet-timestamp",
  "parity-scale-codec",
  "serde",

+ 11 - 3
apps.Dockerfile

@@ -1,17 +1,25 @@
+FROM mikefarah/yq as manifest-maker
+# Change metadata.source in manifest file. It's not possible to override it via flag/env.
+USER root
+ARG WS_PROVIDER_ENDPOINT_URI
+COPY ./query-node/manifest.yml /joystream/qn-manifest.yml
+RUN yq e -i ".typegen.metadata.source = \"$WS_PROVIDER_ENDPOINT_URI\"" /joystream/qn-manifest.yml
+
 FROM --platform=linux/x86-64 node:14 as builder
 
 WORKDIR /joystream
 COPY . /joystream
-RUN  rm -fr /joystream/pioneer
+COPY --from=manifest-maker /joystream/qn-manifest.yml /joystream/query-node/manifest.yml
+
+RUN rm -fr /joystream/pioneer
 
 # Do not set NODE_ENV=production until after running yarn install
 # to ensure dev dependencies are installed.
 RUN yarn --forzen-lockfile
 
 RUN yarn workspace @joystream/types build
-RUN yarn workspace @joystream/content-metadata-protobuf build:ts
+RUN yarn workspace @joystream/metadata-protobuf build
 RUN yarn workspace query-node-root build
-RUN yarn workspace storage-node build
 
 # Second stage to reduce image size, enable it when
 # all packages have correctly identified what is a devDependency and what is not.

+ 10 - 18
docker-compose.yml

@@ -45,8 +45,6 @@ services:
     env_file:
       # relative to working directory where docker-compose was run from
       - .env
-    environment:
-      - WS_PROVIDER_ENDPOINT_URI=${WS_PROVIDER_ENDPOINT_URI}
     ports:
       - '127.0.0.1:3001:3001'
     command: colossus --dev --ws-provider ${WS_PROVIDER_ENDPOINT_URI} --ipfs-host ipfs
@@ -74,12 +72,12 @@ services:
     build:
       context: .
       dockerfile: apps.Dockerfile
+      network: joystream_default
+      args:
+        - WS_PROVIDER_ENDPOINT_URI=${WS_PROVIDER_ENDPOINT_URI}
     env_file:
       # relative to working directory where docker-compose was run from
       - .env
-    environment:
-      - DB_HOST=db
-      - DB_NAME=${DB_NAME}
     ports:
       - "127.0.0.1:8081:${GRAPHQL_SERVER_PORT}"
     depends_on:
@@ -92,9 +90,6 @@ services:
     env_file:
       # relative to working directory where docker-compose was run from
       - .env
-    environment:
-      - DB_HOST=db
-      - DB_NAME=${DB_NAME}
     ports:
       - "127.0.0.1:8081:${GRAPHQL_SERVER_PORT}"
     depends_on:
@@ -112,15 +107,17 @@ services:
     build:
       context: .
       dockerfile: apps.Dockerfile
+      network: joystream_default
+      args:
+        - WS_PROVIDER_ENDPOINT_URI=${WS_PROVIDER_ENDPOINT_URI}
     env_file:
       # relative to working directory where docker-compose was run from
       - .env
     environment:
       - INDEXER_ENDPOINT_URL=http://hydra-indexer-gateway:${WARTHOG_APP_PORT}/graphql
-      - TYPEORM_HOST=db
+      - TYPEORM_HOST=${DB_HOST}
       - TYPEORM_DATABASE=${DB_NAME}
-      - DEBUG=index-builder:*
-      - WS_PROVIDER_ENDPOINT_URI=ws://joystream-node:9944
+      - WS_PROVIDER_ENDPOINT_URI=${WS_PROVIDER_ENDPOINT_URI}
     volumes:
       - ./types/augment/all/defs.json:/joystream/query-node/mappings/lib/generated/types/typedefs.json
     depends_on:
@@ -135,10 +132,8 @@ services:
       - .env
     environment:
       - INDEXER_ENDPOINT_URL=http://hydra-indexer-gateway:${WARTHOG_APP_PORT}/graphql
-      - TYPEORM_HOST=db
+      - TYPEORM_HOST=${DB_HOST}
       - TYPEORM_DATABASE=${DB_NAME}
-      - DEBUG=index-builder:*
-      - WS_PROVIDER_ENDPOINT_URI=ws://joystream-node:9944
     depends_on:
       - hydra-indexer-gateway
     volumes:
@@ -155,12 +150,9 @@ services:
       # relative to working directory where docker-compose was run from
       - .env
     environment:
-      - DB_HOST=db
       - DB_NAME=${INDEXER_DB_NAME}
       - INDEXER_WORKERS=5
       - REDIS_URI=redis://redis:6379/0
-      - DEBUG=index-builder:*
-      - WS_PROVIDER_ENDPOINT_URI=${WS_PROVIDER_ENDPOINT_URI}
       - TYPES_JSON=types.json
     depends_on:
       - db
@@ -187,7 +179,7 @@ services:
       - PORT=${WARTHOG_APP_PORT}
       - DEBUG=*
     ports:
-      - "127.0.0.1:4000:4002"
+      - "127.0.0.1:4000:${WARTHOG_APP_PORT}"
     depends_on:
       - redis
       - db

+ 1 - 1
metadata-protobuf/package.json

@@ -20,7 +20,7 @@
   "license": "MIT",
   "private": false,
   "scripts": {
-    "build": "yarn compile && tsc",
+    "build": "yarn compile && rm -rf lib && tsc",
     "compile": "yarn ts-node ./scripts/compile.ts",
     "generate-doc": "./generate-md-doc.sh",
     "test": "env TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\" }' mocha --inline-diffs -r ts-node/register 'test/**/*.ts'",

+ 1 - 1
metadata-protobuf/src/utils.ts

@@ -5,7 +5,7 @@ export function isSet<T>(v: T | null | undefined): v is T {
   return v !== null && v !== undefined
 }
 
-export function isEmptyObject(object: Record<string, unknown>): boolean {
+export function isEmptyObject<T>(object: T): boolean {
   return Object.keys(object).length === 0
 }
 

+ 6 - 3
node/src/chain_spec/mod.rs

@@ -349,8 +349,10 @@ pub fn testnet_genesis(
                 next_series_id: 1,
                 next_person_id: 1,
                 next_channel_transfer_request_id: 1,
-                min_round_duration: 3,
-                max_round_duration: 20,
+                min_auction_duration: 3,
+                max_auction_duration: 20,
+                min_auction_extension_period: 5,
+                max_auction_extension_period: 30,
                 min_bid_lock_duration: 2,
                 max_bid_lock_duration: 10,
                 min_starting_price: 10,
@@ -359,8 +361,9 @@ pub fn testnet_genesis(
                 max_creator_royalty: Perbill::from_percent(5),
                 min_bid_step: 10,
                 max_bid_step: 100,
-                auction_fee_percentage: Perbill::from_percent(1),
+                platform_fee_percentage: Perbill::from_percent(1),
                 auction_starts_at_max_delta: 90_000,
+                max_auction_whitelist_length: 100,
             }
         }),
         proposals_codex: Some(ProposalsCodexConfig {

+ 0 - 1
query-node/build.sh

@@ -9,7 +9,6 @@ set -a
 . ../.env
 set +a
 
-# only use this when new Hydra releases and contents of `generated/` folder needs to be refreshed
 yarn clean
 yarn codegen:noinstall
 yarn typegen # if this fails try to run this command outside of yarn workspaces

+ 17 - 0
query-node/kill-img.sh

@@ -0,0 +1,17 @@
+#!/usr/bin/env bash
+set -e
+
+SCRIPT_PATH="$(dirname "${BASH_SOURCE[0]}")"
+cd $SCRIPT_PATH
+
+set -a
+. ../.env
+set +a
+
+# Only remove query-node related services
+docker-compose rm -vsf processor
+docker-compose rm -vsf graphql-server
+docker-compose rm -vsf indexer
+docker-compose rm -vsf hydra-indexer-gateway
+docker-compose rm -vsf redis
+docker-compose rm -vsf db

+ 4 - 4
query-node/package.json

@@ -28,10 +28,10 @@
     "typegen:configure": "NODE_URL=${NODE_URL:-ws://localhost:9000} envsub typegen.template.yml typegen.yml",
     "typegen": "rm -rf ./mappings/generated && hydra-typegen typegen manifest.yml --debug",
     "mappings:build": "yarn workspace query-node-mappings build",
-    "docker:build": "docker build . -f docker/Dockerfile.hydra -t hydra-kit:latest",
-    "docker:db:up": "(cd ../ && docker-compose up -d db)",
-    "docker:db:migrate": "docker run --env-file .env --env DB_HOST=db --env TYPEORM_HOST=db --network container:${PWD##*/}_db_1 hydra-kit:latest yarn db:migrate",
-    "docker:up": "docker-compose up -d"
+    "start:dev": "./start.sh",
+    "start": "./start-img.sh",
+    "kill:dev": "./kill.sh",
+    "kill": "./kill-img.sh"
   },
   "author": "",
   "license": "ISC",

+ 8 - 0
query-node/schemas/storage.graphql

@@ -144,6 +144,10 @@ type StorageBagStorageAssignment @entity {
 
   "Storage bucket that should store the bag"
   storageBucket: StorageBucket!
+
+  # Relationship filtering workaround
+  storageBagId: ID
+  storageBucketId: ID
 }
 
 type StorageBagDistributionAssignment @entity {
@@ -155,6 +159,10 @@ type StorageBagDistributionAssignment @entity {
 
   "Distribution bucket that should distribute the bag"
   distributionBucket: DistributionBucket!
+
+  # Relationship filtering workaround
+  storageBagId: ID
+  distributionBucketId: ID
 }
 
 type StorageDataObject @entity {

+ 23 - 0
query-node/start-img.sh

@@ -0,0 +1,23 @@
+#!/usr/bin/env bash
+set -e
+
+SCRIPT_PATH="$(dirname "${BASH_SOURCE[0]}")"
+cd $SCRIPT_PATH
+
+set -a
+. ../.env
+set +a
+
+# Start the joystream-node first to allow fetching Olympia metadata during build (typegen)
+docker-compose up -d joystream-node
+
+# Bring up db
+docker-compose up -d db
+
+# Setup the db
+docker run --rm --env-file ../.env --network joystream_default joystream/apps workspace query-node-root db:prepare
+docker run --rm --env-file ../.env --network joystream_default joystream/apps workspace query-node-root db:migrate
+
+# Start processor and graphql server
+docker-compose up -d processor
+docker-compose up -d graphql-server

+ 8 - 2
query-node/start.sh

@@ -17,6 +17,9 @@ docker-compose up -d joystream-node
 # Bring up db
 docker-compose up -d db
 
+# Override DB_HOST for db setup
+export DB_HOST=localhost
+
 # Make sure we use dev config for db migrations (prevents "Cannot create database..." and some other errors)
 yarn workspace query-node config:dev
 
@@ -24,7 +27,10 @@ yarn workspace query-node config:dev
 yarn workspace query-node-root db:prepare
 yarn workspace query-node-root db:migrate
 
-docker-compose up -d graphql-server-mnt
+# Set DB_HOST back to docker-service one
+export DB_HOST=db
 
-# Starting up processor will bring up all services it depends on
+# Start processor and graphql server
 docker-compose up -d processor-mnt
+docker-compose up -d graphql-server-mnt
+

+ 1 - 11
runtime-modules/common/src/lib.rs

@@ -24,6 +24,7 @@ pub type ActorId<T> = <T as MembershipTypes>::ActorId;
 
 /// HTTP Url string
 pub type Url = Vec<u8>;
+pub type AssetUrls = Vec<Url>;
 
 /// Generic trait for membership dependent pallets.
 pub trait MembershipTypes: frame_system::Trait {
@@ -65,17 +66,6 @@ pub trait StorageOwnership {
         + Ord
         + PartialEq;
 
-    /// DAO id representation.
-    type DAOId: Parameter
-        + Member
-        + BaseArithmetic
-        + Codec
-        + Default
-        + Copy
-        + MaybeSerialize
-        + Ord
-        + PartialEq;
-
     /// Content id representation.
     type ContentId: Parameter + Member + Codec + Default + Copy + MaybeSerialize + Ord + PartialEq;
 

+ 8 - 14
runtime-modules/common/src/storage.rs

@@ -16,42 +16,36 @@ pub struct ContentParameters<ContentId, DataObjectTypeId> {
 // New owner type for storage object struct
 #[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
 #[derive(Clone, Encode, Decode, PartialEq, Eq, Debug)]
-pub enum StorageObjectOwner<MemberId, ChannelId, DAOId> {
+pub enum StorageObjectOwner<MemberId, ChannelId> {
     Member(MemberId),
-    Channel(ChannelId), // acts through content directory module, where again DAOs can own channels for example
+    Channel(ChannelId), // acts through content directory module,
     #[allow(clippy::upper_case_acronyms)]
-    DAO(DAOId), // acts through upcoming `content_finance` module
-    Council,            // acts through proposal frame_system
+    Council, // acts through proposal frame_system
     WorkingGroup(WorkingGroup), // acts through new extrinsic in working group
 }
 
-impl<MemberId, ChannelId, DAOId> Default for StorageObjectOwner<MemberId, ChannelId, DAOId> {
-    fn default() -> Self {
-        Self::Council
-    }
-}
 // To be implemented by current storage data_directory runtime module.
 // Defined in 'common' package
-pub trait StorageSystem<T: crate::StorageOwnership, MemberId> {
+pub trait StorageSystem<T: crate::StorageOwnership + crate::MembershipTypes> {
     fn atomically_add_content(
-        owner: StorageObjectOwner<MemberId, T::ChannelId, T::DAOId>,
+        owner: StorageObjectOwner<T::MemberId, T::ChannelId>,
         content_parameters: Vec<ContentParameters<T::ContentId, T::DataObjectTypeId>>,
     ) -> DispatchResult;
 
     // Checks if given owner can add provided content to the storage frame_system
     fn can_add_content(
-        owner: StorageObjectOwner<MemberId, T::ChannelId, T::DAOId>,
+        owner: StorageObjectOwner<T::MemberId, T::ChannelId>,
         content_parameters: Vec<ContentParameters<T::ContentId, T::DataObjectTypeId>>,
     ) -> DispatchResult;
 
     fn atomically_remove_content(
-        owner: &StorageObjectOwner<MemberId, T::ChannelId, T::DAOId>,
+        owner: &StorageObjectOwner<T::MemberId, T::ChannelId>,
         content_ids: &[T::ContentId],
     ) -> DispatchResult;
 
     // Checks if given owner can remove content under given content ids from the storage frame_system
     fn can_remove_content(
-        owner: &StorageObjectOwner<MemberId, T::ChannelId, T::DAOId>,
+        owner: &StorageObjectOwner<T::MemberId, T::ChannelId>,
         content_ids: &[T::ContentId],
     ) -> DispatchResult;
 }

+ 8 - 2
runtime-modules/content/Cargo.toml

@@ -13,13 +13,17 @@ sp-arithmetic = { package = 'sp-arithmetic', default-features = false, git = 'ht
 codec = { package = 'parity-scale-codec', version = '1.3.4', default-features = false, features = ['derive'] }
 serde = {version = '1.0.101', features = ['derive'], optional = true}
 common = { package = 'pallet-common', default-features = false, path = '../common'}
+storage = { package = 'pallet-storage', default-features = false, path = '../storage'}
 membership = { package = 'pallet-membership', default-features = false, path = '../membership'}
+balances = { package = 'pallet-balances', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = '2cd20966cc09b059817c3ebe12fc130cdd850d62'}
+
 
 [dev-dependencies]
 sp-io = { package = 'sp-io', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = '2cd20966cc09b059817c3ebe12fc130cdd850d62'}
 sp-core = { package = 'sp-core', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = '2cd20966cc09b059817c3ebe12fc130cdd850d62'}
-balances = { package = 'pallet-balances', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = '2cd20966cc09b059817c3ebe12fc130cdd850d62'}
 pallet-timestamp = { package = 'pallet-timestamp', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = '2cd20966cc09b059817c3ebe12fc130cdd850d62'}
+randomness-collective-flip = { package = 'pallet-randomness-collective-flip', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = '2cd20966cc09b059817c3ebe12fc130cdd850d62'}
+
 
 [features]
 default = ['std']
@@ -32,5 +36,7 @@ std = [
 	'codec/std',
 	'serde',
 	'common/std',
-	'membership/std'
+	'storage/std',
+	'balances/std',
+	'membership/std',
 ]

+ 55 - 28
runtime-modules/content/src/errors.rs

@@ -46,9 +46,6 @@ decl_error! {
         /// Operation cannot be perfomed with this Actor
         ActorNotAuthorized,
 
-        /// This content actor cannot own a channel
-        ActorCannotOwnChannel,
-
         /// A Channel or Video Category does not exist.
         CategoryDoesNotExist,
 
@@ -98,11 +95,17 @@ decl_error! {
         /// Royalty Lower Bound Exceeded
         RoyaltyLowerBoundExceeded,
 
-        /// Round time upper bound exceeded
-        RoundTimeUpperBoundExceeded,
+        /// Auction duration upper bound exceeded
+        AuctionDurationUpperBoundExceeded,
+
+        /// Auction duration lower bound exceeded
+        AuctionDurationLowerBoundExceeded,
 
-        /// Round time lower bound exceeded
-        RoundTimeLowerBoundExceeded,
+        /// Auction extension period upper bound exceeded
+        ExtensionPeriodUpperBoundExceeded,
+
+        /// Auction extension period lower bound exceeded
+        ExtensionPeriodLowerBoundExceeded,
 
         /// Bid lock duration upper bound exceeded
         BidLockDurationUpperBoundExceeded,
@@ -132,54 +135,78 @@ decl_error! {
         StartingPriceConstraintViolated,
 
         /// Already active auction cannot be cancelled
-        ActionIsAlreadyActive,
+        ActionHasBidsAlready,
 
         /// Can not create auction for NFT, if auction have been already started or nft is locked for the transfer
         NftIsNotIdle,
 
-        /// No pending transfers for given NFT
-        PendingTransferDoesNotExist,
-
-        // No incoming transfers for given nft origin
-        NoIncomingTransfers,
+        /// No pending offers for given NFT
+        PendingOfferDoesNotExist,
 
-        // Creator royalty requires reward account to be set.
+        /// Creator royalty requires reward account to be set.
         RewardAccountIsNotSet,
 
-        // Actor, which makes an attempt to finish auction is not a winner
-        CallerIsNotAWinner,
+        /// Actor is not a last bidder
+        ActorIsNotALastBidder,
 
-        // Auction cannot be completed
+        /// Auction cannot be completed
         AuctionCannotBeCompleted,
 
-        // Auction does not have bids
+        /// Auction does not have bids
         LastBidDoesNotExist,
 
-        // Auction starts at lower bound exceeded
+        /// Auction starts at lower bound exceeded
         StartsAtLowerBoundExceeded,
 
-        // Auction starts at upper bound exceeded
+        /// Auction starts at upper bound exceeded
         StartsAtUpperBoundExceeded,
 
-        // Nft is not in auction state
+        /// Nft is not in auction state
         NotInAuctionState,
 
-        // Member is not allowed to participate in auction
+        /// Member is not allowed to participate in auction
         MemberIsNotAllowedToParticipate,
 
-        // Member profile not found
+        /// Member profile not found
         MemberProfileNotFound,
 
-        // Given video nft is not in buy now state
+        /// Given video nft is not in buy now state
         NFTNotInBuyNowState,
 
-        // Auction type is not `Open`
+        /// Auction type is not `Open`
         IsNotOpenAuctionType,
 
-        // Bid lock duration is not expired
+        /// Auction type is not `English`
+        IsNotEnglishAuctionType,
+
+        /// Bid lock duration is not expired
         BidLockDurationIsNotExpired,
 
-        // NFT auction is already expired
-        NFTAuctionIsAlreadyExpired
+        /// NFT auction is already expired
+        NFTAuctionIsAlreadyExpired,
+
+        /// Auction buy now is less then starting price
+        BuyNowIsLessThenStartingPrice,
+
+        /// Max auction whitelist length upper bound exceeded
+        MaxAuctionWhiteListLengthUpperBoundExceeded,
+
+        /// Auction whitelist has only one member
+        WhitelistHasOnlyOneMember,
+
+        /// Extension period is greater then auction duration
+        ExtensionPeriodIsGreaterThenAuctionDuration,
+
+        /// No assets to be removed have been specified
+        NoAssetsSpecified,
+
+        /// Channel assets feasibility
+        InvalidAssetsProvided,
+
+        /// Channel Contains Video
+        ChannelContainsVideos,
+
+        /// Channel Contains Assets
+        ChannelContainsAssets,
     }
 }

File diff suppressed because it is too large
+ 464 - 310
runtime-modules/content/src/lib.rs


+ 201 - 218
runtime-modules/content/src/nft/mod.rs

@@ -5,89 +5,25 @@ pub use types::*;
 use crate::*;
 
 impl<T: Trait> Module<T> {
-    /// Authorize nft owner
-    pub(crate) fn authorize_nft_owner(
-        origin: T::Origin,
-        actor: &ContentActor<CuratorGroupId<T>, CuratorId<T>, MemberId<T>>,
-        video: &Video<T>,
-    ) -> DispatchResult {
-        ensure_signed(origin)?;
-
-        // The content owner will be..
-        let content_owner = Self::actor_to_content_owner(&actor)?;
-
-        video.ensure_nft_ownership::<T>(&content_owner)
-    }
-
-    /// Check whether nft auction expired
-    pub(crate) fn is_nft_auction_expired(auction: &Auction<T>) -> bool {
-        match &auction.last_bid {
-            Some(last_bid) => {
-                // Check whether buy now have been triggered.
-                let is_buy_now_triggered =
-                    matches!(&auction.buy_now_price, Some(buy_now) if *buy_now == last_bid.amount);
-                if let AuctionType::English(round_duration) = auction.auction_type {
-                    let now = <frame_system::Module<T>>::block_number();
-
-                    // Check whether auction round time expired.
-                    let is_auction_round_expired = (now - last_bid.time) >= round_duration;
-                    is_auction_round_expired || is_buy_now_triggered
-                } else {
-                    // Open auction expires only if buy now have been triggered
-                    is_buy_now_triggered
-                }
-            }
-            _ => false,
-        }
-    }
-
-    /// Ensure nft auction not expired
-    pub(crate) fn ensure_nft_auction_not_expired(auction: &Auction<T>) -> DispatchResult {
-        ensure!(
-            !Self::is_nft_auction_expired(auction),
-            Error::<T>::NFTAuctionIsAlreadyExpired
-        );
-        Ok(())
-    }
-
     /// Ensure nft auction can be completed
     pub(crate) fn ensure_auction_can_be_completed(auction: &Auction<T>) -> DispatchResult {
-        if let Some(last_bid) = &auction.last_bid {
-            let can_be_completed = if let AuctionType::English(round_duration) =
-                auction.auction_type
-            {
-                let now = <frame_system::Module<T>>::block_number();
-
-                // Check whether auction round time expired.
-                let is_auction_round_expired = (now - last_bid.time) >= round_duration;
-
-                // Check whether buy now have been triggered.
-                let is_buy_now_triggered =
-                    matches!(&auction.buy_now_price, Some(buy_now) if *buy_now == last_bid.amount);
-
-                is_auction_round_expired || is_buy_now_triggered
-            } else {
-                // Open auction can be completed at any time
-                true
-            };
-
-            ensure!(can_be_completed, Error::<T>::AuctionCannotBeCompleted);
-        }
-
-        Ok(())
-    }
+        let can_be_completed = if let AuctionType::English(EnglishAuctionDetails {
+            auction_duration,
+            ..
+        }) = auction.auction_type
+        {
+            let now = <frame_system::Module<T>>::block_number();
 
-    /// Ensure
-    pub(crate) fn ensure_actor_is_last_bidder(
-        origin: T::Origin,
-        member_id: MemberId<T>,
-        auction: &Auction<T>,
-    ) -> DispatchResult {
-        let account_id = ensure_signed(origin)?;
+            // Check whether auction time expired.
+            (now - auction.starts_at) >= auction_duration
+        } else {
+            // Open auction can be completed at any time
+            true
+        };
 
-        ensure_member_auth_success::<T>(&member_id, &account_id)?;
+        ensure!(can_be_completed, Error::<T>::AuctionCannotBeCompleted);
 
-        auction.ensure_caller_is_last_bidder::<T>(member_id)
+        Ok(())
     }
 
     /// Ensure auction participant has sufficient balance to make bid
@@ -104,24 +40,42 @@ impl<T: Trait> Module<T> {
 
     /// Safety/bound checks for auction parameters
     pub(crate) fn validate_auction_params(
-        auction_params: &AuctionParams<T::VideoId, T::BlockNumber, BalanceOf<T>, MemberId<T>>,
+        auction_params: &AuctionParams<T::BlockNumber, BalanceOf<T>, MemberId<T>>,
     ) -> DispatchResult {
         match auction_params.auction_type {
-            AuctionType::English(round_duration) => {
-                Self::ensure_round_duration_bounds_satisfied(round_duration)?;
+            AuctionType::English(EnglishAuctionDetails {
+                extension_period,
+                auction_duration,
+            }) => {
+                Self::ensure_auction_duration_bounds_satisfied(auction_duration)?;
+                Self::ensure_extension_period_bounds_satisfied(extension_period)?;
+
+                // Ensure auction_duration of English auction is >= extension_period
+                ensure!(
+                    auction_duration >= extension_period,
+                    Error::<T>::ExtensionPeriodIsGreaterThenAuctionDuration
+                );
             }
-            AuctionType::Open(lock_duration) => {
-                Self::ensure_bid_lock_duration_bounds_satisfied(lock_duration)?;
+            AuctionType::Open(OpenAuctionDetails { bid_lock_duration }) => {
+                Self::ensure_bid_lock_duration_bounds_satisfied(bid_lock_duration)?;
             }
         }
 
         Self::ensure_starting_price_bounds_satisfied(auction_params.starting_price)?;
         Self::ensure_bid_step_bounds_satisfied(auction_params.minimal_bid_step)?;
+        Self::ensure_whitelist_bounds_satisfied(&auction_params.whitelist)?;
 
         if let Some(starts_at) = auction_params.starts_at {
             Self::ensure_starts_at_delta_bounds_satisfied(starts_at)?;
         }
 
+        if let Some(buy_now_price) = auction_params.buy_now_price {
+            ensure!(
+                buy_now_price > auction_params.starting_price,
+                Error::<T>::BuyNowIsLessThenStartingPrice
+            );
+        }
+
         Ok(())
     }
 
@@ -130,7 +84,7 @@ impl<T: Trait> Module<T> {
         starts_at: T::BlockNumber,
     ) -> DispatchResult {
         ensure!(
-            starts_at > <frame_system::Module<T>>::block_number(),
+            starts_at >= <frame_system::Module<T>>::block_number(),
             Error::<T>::StartsAtLowerBoundExceeded
         );
 
@@ -143,14 +97,6 @@ impl<T: Trait> Module<T> {
         Ok(())
     }
 
-    /// Ensure channel reward_account account is set
-    pub(crate) fn ensure_reward_account_is_set(channel_id: T::ChannelId) -> DispatchResult {
-        Self::channel_by_id(channel_id)
-            .reward_account
-            .ok_or(Error::<T>::RewardAccountIsNotSet)?;
-        Ok(())
-    }
-
     /// Ensure royalty bounds satisfied
     pub(crate) fn ensure_royalty_bounds_satisfied(royalty: Perbill) -> DispatchResult {
         ensure!(
@@ -177,17 +123,52 @@ impl<T: Trait> Module<T> {
         Ok(())
     }
 
+    /// Ensure whitelist bounds satisfied
+    pub(crate) fn ensure_whitelist_bounds_satisfied(
+        whitelist: &BTreeSet<T::MemberId>,
+    ) -> DispatchResult {
+        match whitelist.len() {
+            // whitelist is empty <==> feature is not active.
+            0 => Ok(()),
+            // auctions with one paticipant does not makes sense
+            1 => Err(Error::<T>::WhitelistHasOnlyOneMember.into()),
+            length => {
+                ensure!(
+                    length <= Self::max_auction_whitelist_length() as usize,
+                    Error::<T>::MaxAuctionWhiteListLengthUpperBoundExceeded
+                );
+                Ok(())
+            }
+        }
+    }
+
     /// Ensure auction duration bounds satisfied
-    pub(crate) fn ensure_round_duration_bounds_satisfied(
-        round_duration: T::BlockNumber,
+    pub(crate) fn ensure_auction_duration_bounds_satisfied(
+        duration: T::BlockNumber,
     ) -> DispatchResult {
         ensure!(
-            round_duration <= Self::max_round_duration(),
-            Error::<T>::RoundTimeUpperBoundExceeded
+            duration <= Self::max_auction_duration(),
+            Error::<T>::AuctionDurationUpperBoundExceeded
         );
         ensure!(
-            round_duration >= Self::min_round_duration(),
-            Error::<T>::RoundTimeLowerBoundExceeded
+            duration >= Self::min_auction_duration(),
+            Error::<T>::AuctionDurationLowerBoundExceeded
+        );
+
+        Ok(())
+    }
+
+    /// Ensure auction extension period bounds satisfied
+    pub(crate) fn ensure_extension_period_bounds_satisfied(
+        extension_period: T::BlockNumber,
+    ) -> DispatchResult {
+        ensure!(
+            extension_period <= Self::max_auction_extension_period(),
+            Error::<T>::ExtensionPeriodUpperBoundExceeded
+        );
+        ensure!(
+            extension_period >= Self::min_auction_extension_period(),
+            Error::<T>::ExtensionPeriodLowerBoundExceeded
         );
 
         Ok(())
@@ -213,18 +194,18 @@ impl<T: Trait> Module<T> {
         starting_price: BalanceOf<T>,
     ) -> DispatchResult {
         ensure!(
-            starting_price >= Self::max_starting_price(),
-            Error::<T>::StartingPriceUpperBoundExceeded
+            starting_price >= Self::min_starting_price(),
+            Error::<T>::StartingPriceLowerBoundExceeded
         );
         ensure!(
-            starting_price <= Self::min_starting_price(),
-            Error::<T>::StartingPriceLowerBoundExceeded
+            starting_price <= Self::max_starting_price(),
+            Error::<T>::StartingPriceUpperBoundExceeded
         );
         Ok(())
     }
 
     /// Ensure given participant have sufficient free balance
-    pub fn ensure_sufficient_free_balance(
+    pub(crate) fn ensure_sufficient_free_balance(
         participant_account_id: &T::AccountId,
         balance: BalanceOf<T>,
     ) -> DispatchResult {
@@ -236,15 +217,11 @@ impl<T: Trait> Module<T> {
     }
 
     /// Ensure given participant can buy nft now
-    pub fn ensure_can_buy_now(
-        video: &Video<T>,
+    pub(crate) fn ensure_can_buy_now(
+        nft: &Nft<T>,
         participant_account_id: &T::AccountId,
     ) -> DispatchResult {
-        if let Some(OwnedNFT {
-            transactional_status: TransactionalStatus::BuyNow(price),
-            ..
-        }) = &video.nft_status
-        {
+        if let TransactionalStatus::BuyNow(price) = &nft.transactional_status {
             Self::ensure_sufficient_free_balance(participant_account_id, *price)
         } else {
             Err(Error::<T>::NFTNotInBuyNowState.into())
@@ -252,151 +229,157 @@ impl<T: Trait> Module<T> {
     }
 
     /// Ensure new pending offer for given participant is available to proceed
-    pub fn ensure_new_pending_offer_available_to_proceed(
-        video: &Video<T>,
-        participant: T::MemberId,
+    pub(crate) fn ensure_new_pending_offer_available_to_proceed(
+        nft: &Nft<T>,
         participant_account_id: &T::AccountId,
     ) -> DispatchResult {
-        match &video.nft_status {
-            Some(OwnedNFT {
-                transactional_status: TransactionalStatus::InitiatedOfferToMember(to, price),
-                ..
-            }) if participant == *to => {
-                if let Some(price) = price {
-                    Self::ensure_sufficient_free_balance(participant_account_id, *price)?;
-                }
+        if let TransactionalStatus::InitiatedOfferToMember(member_id, price) =
+            &nft.transactional_status
+        {
+            // Authorize participant under given member id
+            ensure_member_auth_success::<T>(&member_id, &participant_account_id)?;
+
+            if let Some(price) = price {
+                Self::ensure_sufficient_free_balance(participant_account_id, *price)?;
             }
-            _ => return Err(Error::<T>::NoIncomingTransfers.into()),
+            Ok(())
+        } else {
+            Err(Error::<T>::PendingOfferDoesNotExist.into())
         }
-        Ok(())
+    }
+
+    /// Cancel NFT transaction
+    pub fn cancel_transaction(nft: Nft<T>) -> Nft<T> {
+        if let TransactionalStatus::Auction(ref auction) = nft.transactional_status {
+            if let Some(ref last_bid) = auction.last_bid {
+                // Unreserve previous bidder balance
+                T::Currency::unreserve(&last_bid.bidder_account_id, last_bid.amount);
+            }
+        }
+
+        nft.set_idle_transactional_status()
     }
 
     /// Buy nft
-    pub fn buy_now(
-        mut video: Video<T>,
+    pub(crate) fn buy_now(
+        in_channel: T::ChannelId,
+        mut nft: Nft<T>,
         owner_account_id: T::AccountId,
         new_owner_account_id: T::AccountId,
         new_owner: T::MemberId,
-    ) -> Video<T> {
-        if let Some(OwnedNFT {
-            transactional_status: TransactionalStatus::BuyNow(price),
-            ref mut owner,
-            ..
-        }) = &mut video.nft_status
-        {
-            T::Currency::slash(&new_owner_account_id, *price);
-
-            T::Currency::deposit_creating(&owner_account_id, *price);
-
-            *owner = ChannelOwner::Member(new_owner);
+    ) -> Nft<T> {
+        if let TransactionalStatus::BuyNow(price) = &nft.transactional_status {
+            Self::complete_payment(
+                in_channel,
+                nft.creator_royalty,
+                *price,
+                new_owner_account_id,
+                Some(owner_account_id),
+                false,
+            );
+
+            nft.owner = NFTOwner::Member(new_owner);
         }
 
-        video.set_idle_transactional_status()
+        nft.set_idle_transactional_status()
     }
 
     /// Completes nft offer
-    pub fn complete_nft_offer(
-        mut video: Video<T>,
+    pub(crate) fn complete_nft_offer(
+        in_channel: T::ChannelId,
+        mut nft: Nft<T>,
         owner_account_id: T::AccountId,
         new_owner_account_id: T::AccountId,
-    ) -> Video<T> {
-        if let Some(OwnedNFT {
-            transactional_status: TransactionalStatus::InitiatedOfferToMember(to, price),
-            ref mut owner,
-            ..
-        }) = &mut video.nft_status
-        {
+    ) -> Nft<T> {
+        if let TransactionalStatus::InitiatedOfferToMember(to, price) = &nft.transactional_status {
             if let Some(price) = price {
-                T::Currency::slash(&new_owner_account_id, *price);
-
-                T::Currency::deposit_creating(&owner_account_id, *price);
+                Self::complete_payment(
+                    in_channel,
+                    nft.creator_royalty,
+                    *price,
+                    new_owner_account_id,
+                    Some(owner_account_id),
+                    false,
+                );
             }
 
-            *owner = ChannelOwner::Member(*to);
+            nft.owner = NFTOwner::Member(*to);
         }
 
-        video.set_idle_transactional_status()
+        nft.set_idle_transactional_status()
     }
 
-    /// Complete nft transfer
-    pub(crate) fn complete_nft_auction_transfer(
-        video: &mut Video<T>,
-        auction_fee: BalanceOf<T>,
-        last_bidder_account_id: T::AccountId,
-        last_bidder: T::MemberId,
-        owner_account_id: T::AccountId,
-        last_bid_amount: BalanceOf<T>,
+    /// Complete payment, either auction related or buy now/offer
+    pub(crate) fn complete_payment(
+        in_channel: T::ChannelId,
+        creator_royalty: Option<Royalty>,
+        amount: BalanceOf<T>,
+        sender_account_id: T::AccountId,
+        receiver_account_id: Option<T::AccountId>,
+        // for auction related payments
+        is_auction: bool,
     ) {
-        if let Some(OwnedNFT {
-            owner,
-            transactional_status,
-            creator_royalty,
-            ..
-        }) = &mut video.nft_status
-        {
-            if let Some(creator_royalty) = creator_royalty {
-                let royalty = *creator_royalty * last_bid_amount;
+        let auction_fee = Self::platform_fee_percentage() * amount;
 
-                // Slash last bidder bid
-                T::Currency::slash_reserved(&last_bidder_account_id, last_bid_amount);
+        // Slash amount from sender
+        if is_auction {
+            T::Currency::slash_reserved(&sender_account_id, amount);
+        } else {
+            T::Currency::slash(&sender_account_id, amount);
+        }
+
+        if let Some(creator_royalty) = creator_royalty {
+            let royalty = creator_royalty * amount;
 
-                // Deposit bid, exluding royalty amount and auction fee into auctioneer account
-                if last_bid_amount > royalty + auction_fee {
+            // Deposit amount, exluding royalty and platform fee into receiver account
+            match receiver_account_id {
+                Some(receiver_account_id) if amount > royalty + auction_fee => {
                     T::Currency::deposit_creating(
-                        &owner_account_id,
-                        last_bid_amount - royalty - auction_fee,
+                        &receiver_account_id,
+                        amount - royalty - auction_fee,
                     );
-                } else {
-                    T::Currency::deposit_creating(&owner_account_id, last_bid_amount - auction_fee);
                 }
-
-                // Should always be Some(_) at this stage, because of previously made check.
-                if let Some(creator_account_id) =
-                    Self::channel_by_id(video.in_channel).reward_account
-                {
-                    // Deposit royalty into creator account
-                    T::Currency::deposit_creating(&creator_account_id, royalty);
+                Some(receiver_account_id) => {
+                    T::Currency::deposit_creating(&receiver_account_id, amount - auction_fee);
                 }
-            } else {
-                // Slash last bidder bid and deposit it into auctioneer account
-                T::Currency::slash_reserved(&last_bidder_account_id, last_bid_amount);
+                _ => (),
+            };
 
-                // Deposit bid, exluding auction fee into auctioneer account
-                T::Currency::deposit_creating(&owner_account_id, last_bid_amount - auction_fee);
+            // Should always be Some(_) at this stage, because of previously made check.
+            if let Some(creator_account_id) = Self::channel_by_id(in_channel).reward_account {
+                // Deposit royalty into creator account
+                T::Currency::deposit_creating(&creator_account_id, royalty);
+            }
+        } else {
+            if let Some(receiver_account_id) = receiver_account_id {
+                // Deposit amount, exluding auction fee into receiver account
+                T::Currency::deposit_creating(&receiver_account_id, amount - auction_fee);
             }
-
-            *owner = ChannelOwner::Member(last_bidder);
-            *transactional_status = TransactionalStatus::Idle;
         }
     }
 
     /// Complete auction
     pub(crate) fn complete_auction(
-        mut video: Video<T>,
-        last_bidder_account_id: T::AccountId,
-        owner_account_id: T::AccountId,
-    ) -> Video<T> {
-        if let Some(OwnedNFT {
-            transactional_status: TransactionalStatus::Auction(auction),
-            ..
-        }) = &video.nft_status
-        {
-            let auction = auction.to_owned();
-            if let Some(last_bid) = auction.last_bid {
-                let bid = last_bid.amount;
-                let last_bidder = last_bid.bidder;
-                let auction_fee = Self::auction_fee_percentage() * bid;
-
-                Self::complete_nft_auction_transfer(
-                    &mut video,
-                    auction_fee,
-                    last_bidder_account_id,
-                    last_bidder,
-                    owner_account_id,
-                    bid,
-                );
-            }
-        }
-        video
+        in_channel: T::ChannelId,
+        mut nft: Nft<T>,
+        last_bid: Bid<T::MemberId, T::AccountId, T::BlockNumber, BalanceOf<T>>,
+        owner_account_id: Option<T::AccountId>,
+    ) -> Nft<T> {
+        let last_bid_amount = last_bid.amount;
+        let last_bidder = last_bid.bidder;
+        let bidder_account_id = last_bid.bidder_account_id;
+
+        Self::complete_payment(
+            in_channel,
+            nft.creator_royalty,
+            last_bid_amount,
+            bidder_account_id,
+            owner_account_id,
+            true,
+        );
+
+        nft.owner = NFTOwner::Member(last_bidder);
+        nft.transactional_status = TransactionalStatus::Idle;
+        nft
     }
 }

+ 263 - 99
runtime-modules/content/src/nft/types.rs

@@ -14,18 +14,23 @@ pub type Royalty = Perbill;
 #[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
 #[derive(Encode, Decode, Clone, PartialEq, Eq, Debug)]
 pub enum TransactionalStatus<
-    BlockNumber: BaseArithmetic + Copy,
+    BlockNumber: BaseArithmetic + Copy + Default,
     MemberId: Default + Copy + Ord,
-    Balance: Default,
+    AccountId: Default + Clone + Ord,
+    Balance: Default + Clone + BaseArithmetic,
 > {
     Idle,
     InitiatedOfferToMember(MemberId, Option<Balance>),
-    Auction(AuctionRecord<BlockNumber, Balance, MemberId>),
+    Auction(AuctionRecord<BlockNumber, Balance, MemberId, AccountId>),
     BuyNow(Balance),
 }
 
-impl<BlockNumber: BaseArithmetic + Copy, MemberId: Default + Copy + Ord, Balance: Default> Default
-    for TransactionalStatus<BlockNumber, MemberId, Balance>
+impl<
+        BlockNumber: BaseArithmetic + Copy + Default,
+        MemberId: Default + Copy + Ord,
+        AccountId: Default + Clone + Ord,
+        Balance: Default + Clone + BaseArithmetic,
+    > Default for TransactionalStatus<BlockNumber, MemberId, AccountId, Balance>
 {
     fn default() -> Self {
         Self::Idle
@@ -36,58 +41,142 @@ impl<BlockNumber: BaseArithmetic + Copy, MemberId: Default + Copy + Ord, Balance
 #[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
 #[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)]
 pub struct OwnedNFT<
-    BlockNumber: BaseArithmetic + Copy,
+    BlockNumber: BaseArithmetic + Copy + Default,
     MemberId: Default + Copy + Ord,
-    CuratorGroupId: Default + Copy,
-    DAOId: Default + Copy,
-    Balance: Default,
+    AccountId: Default + Clone + Ord,
+    Balance: Default + Clone + BaseArithmetic,
 > {
-    pub owner: ChannelOwner<MemberId, CuratorGroupId, DAOId>,
-    pub transactional_status: TransactionalStatus<BlockNumber, MemberId, Balance>,
+    pub owner: NFTOwner<MemberId>,
+    pub transactional_status: TransactionalStatus<BlockNumber, MemberId, AccountId, Balance>,
     pub creator_royalty: Option<Royalty>,
 }
 
 impl<
-        BlockNumber: BaseArithmetic + Copy,
+        BlockNumber: BaseArithmetic + Copy + Default,
         MemberId: Default + Copy + PartialEq + Ord,
-        CuratorGroupId: Default + Copy + PartialEq,
-        DAOId: Default + Copy + PartialEq,
-        Balance: Default,
-    > OwnedNFT<BlockNumber, MemberId, CuratorGroupId, DAOId, Balance>
+        AccountId: Default + Clone + PartialEq + Ord,
+        Balance: Default + Clone + BaseArithmetic,
+    > OwnedNFT<BlockNumber, MemberId, AccountId, Balance>
 {
-    /// Whether provided owner is nft owner
-    pub fn is_owner(&self, owner: &ChannelOwner<MemberId, CuratorGroupId, DAOId>) -> bool {
-        self.owner.eq(owner)
-    }
-
     /// Create new NFT
-    pub fn new(
-        owner: ChannelOwner<MemberId, CuratorGroupId, DAOId>,
-        creator_royalty: Option<Royalty>,
-    ) -> Self {
+    pub fn new(owner: NFTOwner<MemberId>, creator_royalty: Option<Royalty>) -> Self {
         Self {
             owner,
             transactional_status: TransactionalStatus::Idle,
             creator_royalty,
         }
     }
+
+    /// Get nft auction record
+    pub fn ensure_auction_state<T: Trait>(
+        &self,
+    ) -> Result<AuctionRecord<BlockNumber, Balance, MemberId, AccountId>, Error<T>> {
+        if let TransactionalStatus::Auction(auction) = &self.transactional_status {
+            Ok(auction.to_owned())
+        } else {
+            Err(Error::<T>::NotInAuctionState)
+        }
+    }
+
+    ///  Ensure nft transactional status is set to `Idle`
+    pub fn ensure_nft_transactional_status_is_idle<T: Trait>(&self) -> DispatchResult {
+        if let TransactionalStatus::Idle = self.transactional_status {
+            Ok(())
+        } else {
+            Err(Error::<T>::NftIsNotIdle.into())
+        }
+    }
+
+    /// Sets nft transactional status to `BuyNow`
+    pub fn set_buy_now_transactionl_status(mut self, buy_now_price: Balance) -> Self {
+        self.transactional_status = TransactionalStatus::BuyNow(buy_now_price);
+        self
+    }
+
+    /// Sets nft transactional status to provided `Auction`
+    pub fn set_auction_transactional_status(
+        mut self,
+        auction: AuctionRecord<BlockNumber, Balance, MemberId, AccountId>,
+    ) -> Self {
+        self.transactional_status = TransactionalStatus::Auction(auction);
+        self
+    }
+
+    /// Set nft transactional status to `Idle`
+    pub fn set_idle_transactional_status(mut self) -> Self {
+        self.transactional_status = TransactionalStatus::Idle;
+        self
+    }
+
+    /// Set nft transactional status to `InitiatedOfferToMember`
+    pub fn set_pending_offer_transactional_status(
+        mut self,
+        to: MemberId,
+        balance: Option<Balance>,
+    ) -> Self {
+        self.transactional_status = TransactionalStatus::InitiatedOfferToMember(to, balance);
+        self
+    }
+
+    /// Ensure NFT has pending offer
+    pub fn ensure_pending_offer_state<T: Trait>(&self) -> DispatchResult {
+        ensure!(
+            matches!(
+                self.transactional_status,
+                TransactionalStatus::InitiatedOfferToMember(..),
+            ),
+            Error::<T>::PendingOfferDoesNotExist
+        );
+        Ok(())
+    }
+
+    /// Ensure NFT is in BuyNow state
+    pub fn ensure_buy_now_state<T: Trait>(&self) -> DispatchResult {
+        ensure!(
+            matches!(self.transactional_status, TransactionalStatus::BuyNow(..),),
+            Error::<T>::NFTNotInBuyNowState
+        );
+        Ok(())
+    }
+}
+
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Clone, PartialEq, Eq, Debug)]
+pub enum NFTOwner<MemberId> {
+    ChannelOwner,
+    Member(MemberId),
+}
+
+impl<MemberId> Default for NFTOwner<MemberId> {
+    fn default() -> Self {
+        Self::ChannelOwner
+    }
 }
 
 /// Information on the auction being created.
 #[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
 #[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)]
-pub struct Bid<MemberId, BlockNumber: BaseArithmetic + Copy, Balance> {
+pub struct Bid<MemberId, AccountId, BlockNumber: BaseArithmetic + Copy, Balance> {
     pub bidder: MemberId,
+    pub bidder_account_id: AccountId,
     pub amount: Balance,
-    pub time: BlockNumber,
+    pub made_at_block: BlockNumber,
 }
 
-impl<MemberId, BlockNumber: BaseArithmetic + Copy, Balance> Bid<MemberId, BlockNumber, Balance> {
-    fn new(bidder: MemberId, amount: Balance, time: BlockNumber) -> Self {
+impl<MemberId, AccountId, BlockNumber: BaseArithmetic + Copy, Balance>
+    Bid<MemberId, AccountId, BlockNumber, Balance>
+{
+    fn new(
+        bidder: MemberId,
+        bidder_account_id: AccountId,
+        amount: Balance,
+        made_at_block: BlockNumber,
+    ) -> Self {
         Self {
             bidder,
+            bidder_account_id,
             amount,
-            time,
+            made_at_block,
         }
     }
 }
@@ -95,44 +184,51 @@ impl<MemberId, BlockNumber: BaseArithmetic + Copy, Balance> Bid<MemberId, BlockN
 /// Information on the auction being created.
 #[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
 #[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)]
-pub struct AuctionRecord<BlockNumber: BaseArithmetic + Copy, Balance, MemberId: Ord> {
+pub struct AuctionRecord<
+    BlockNumber: BaseArithmetic + Copy,
+    Balance: Clone,
+    MemberId: Ord + Clone,
+    AccountId: Ord + Clone,
+> {
     pub starting_price: Balance,
     pub buy_now_price: Option<Balance>,
     /// Auction type (either english or open)
     pub auction_type: AuctionType<BlockNumber>,
     pub minimal_bid_step: Balance,
-    pub last_bid: Option<Bid<MemberId, BlockNumber, Balance>>,
-    pub starts_at: Option<BlockNumber>,
-    pub whitelist: Option<BTreeSet<MemberId>>,
+    pub last_bid: Option<Bid<MemberId, AccountId, BlockNumber, Balance>>,
+    pub starts_at: BlockNumber,
+    pub whitelist: BTreeSet<MemberId>,
 }
 
 impl<
-        BlockNumber: BaseArithmetic + Copy + Default,
-        Balance: Default + BaseArithmetic,
-        MemberId: Default + PartialEq + Ord,
-    > AuctionRecord<BlockNumber, Balance, MemberId>
+        BlockNumber: BaseArithmetic + Copy + Default + Clone,
+        Balance: Default + BaseArithmetic + Clone,
+        MemberId: Default + PartialEq + Ord + Clone,
+        AccountId: Default + PartialEq + Ord + Clone,
+    > AuctionRecord<BlockNumber, Balance, MemberId, AccountId>
 {
     /// Create a new auction record with provided parameters
-    pub fn new<VideoId>(
-        auction_params: AuctionParams<VideoId, BlockNumber, Balance, MemberId>,
-    ) -> Self {
-        let AuctionParams {
-            auction_type,
-            starting_price,
-            buy_now_price,
-            minimal_bid_step,
-            starts_at,
-            whitelist,
-            ..
-        } = auction_params;
-        Self {
-            starting_price,
-            buy_now_price,
-            auction_type,
-            minimal_bid_step,
-            last_bid: None,
-            starts_at,
-            whitelist,
+    pub fn new(auction_params: AuctionParams<BlockNumber, Balance, MemberId>) -> Self {
+        if let Some(starts_at) = auction_params.starts_at {
+            Self {
+                starting_price: auction_params.starting_price,
+                buy_now_price: auction_params.buy_now_price,
+                auction_type: auction_params.auction_type,
+                minimal_bid_step: auction_params.minimal_bid_step,
+                last_bid: None,
+                starts_at,
+                whitelist: auction_params.whitelist,
+            }
+        } else {
+            Self {
+                starting_price: auction_params.starting_price,
+                buy_now_price: auction_params.buy_now_price,
+                auction_type: auction_params.auction_type,
+                minimal_bid_step: auction_params.minimal_bid_step,
+                last_bid: None,
+                starts_at: BlockNumber::default(),
+                whitelist: auction_params.whitelist,
+            }
         }
     }
 
@@ -154,10 +250,6 @@ impl<
                         Error::<T>::BidStepConstraintViolated
                     );
                 } else {
-                    ensure!(
-                        self.minimal_bid_step <= new_bid,
-                        Error::<T>::BidStepConstraintViolated
-                    );
                     ensure!(
                         self.starting_price <= new_bid,
                         Error::<T>::StartingPriceConstraintViolated
@@ -170,31 +262,46 @@ impl<
     }
 
     /// Make auction bid
-    pub fn make_bid(&mut self, who: MemberId, bid: Balance, last_bid_block: BlockNumber) {
-        let bid = Bid::new(who, bid, last_bid_block);
-        self.last_bid = Some(bid);
+    pub fn make_bid(
+        mut self,
+        bidder: MemberId,
+        bidder_account_id: AccountId,
+        bid: Balance,
+        last_bid_block: BlockNumber,
+    ) -> (Self, bool, Bid<MemberId, AccountId, BlockNumber, Balance>) {
+        let bid = Bid::new(bidder, bidder_account_id, bid, last_bid_block);
+        let is_extended = match &mut self.auction_type {
+            AuctionType::English(EnglishAuctionDetails {
+                extension_period,
+                auction_duration,
+            }) if last_bid_block - self.starts_at >= *auction_duration - *extension_period => {
+                // bump auction duration when bid is made during extension period.
+                *auction_duration += *extension_period;
+                true
+            }
+            _ => false,
+        };
+
+        self.last_bid = Some(bid.clone());
+        (self, is_extended, bid)
     }
 
     /// Cnacel auction bid
-    pub fn cancel_bid(&mut self) {
+    pub fn cancel_bid(mut self) -> Self {
         self.last_bid = None;
+        self
     }
 
-    /// Check whether auction have any bids
-    fn is_active(&self) -> bool {
-        self.last_bid.is_some()
-    }
-
-    // Ensure auction is not active
-    fn ensure_is_not_active<T: Trait>(&self) -> DispatchResult {
-        ensure!(self.is_active(), Error::<T>::ActionIsAlreadyActive);
+    // Ensure auction has no bids
+    fn ensure_has_no_bids<T: Trait>(&self) -> DispatchResult {
+        ensure!(self.last_bid.is_none(), Error::<T>::ActionHasBidsAlready);
         Ok(())
     }
 
     /// Ensure given auction can be canceled
     pub fn ensure_auction_can_be_canceled<T: Trait>(&self) -> DispatchResult {
         if let AuctionType::English(_) = self.auction_type {
-            self.ensure_is_not_active::<T>()
+            self.ensure_has_no_bids::<T>()
         } else {
             Ok(())
         }
@@ -202,9 +309,36 @@ impl<
 
     /// Ensure auction have been already started
     pub fn ensure_auction_started<T: Trait>(&self, current_block: BlockNumber) -> DispatchResult {
-        if let Some(starts_at) = self.starts_at {
-            ensure!(starts_at <= current_block, Error::<T>::AuctionDidNotStart);
+        ensure!(
+            self.starts_at <= current_block,
+            Error::<T>::AuctionDidNotStart
+        );
+        Ok(())
+    }
+
+    /// Check whether nft auction expired
+    pub fn is_nft_auction_expired(&self, current_block: BlockNumber) -> bool {
+        if let AuctionType::English(EnglishAuctionDetails {
+            auction_duration, ..
+        }) = self.auction_type
+        {
+            // Check whether auction time expired.
+            (current_block - self.starts_at) >= auction_duration
+        } else {
+            // Open auction never expires
+            false
         }
+    }
+
+    /// Ensure nft auction not expired
+    pub fn ensure_nft_auction_not_expired<T: Trait>(
+        &self,
+        current_block: BlockNumber,
+    ) -> DispatchResult {
+        ensure!(
+            !self.is_nft_auction_expired(current_block),
+            Error::<T>::NFTAuctionIsAlreadyExpired
+        );
         Ok(())
     }
 
@@ -215,7 +349,7 @@ impl<
 
     /// Ensure caller is last bidder.
     pub fn ensure_caller_is_last_bidder<T: Trait>(&self, who: MemberId) -> DispatchResult {
-        ensure!(self.is_last_bidder(who), Error::<T>::CallerIsNotAWinner);
+        ensure!(self.is_last_bidder(who), Error::<T>::ActorIsNotALastBidder);
         Ok(())
     }
 
@@ -228,15 +362,24 @@ impl<
         Ok(())
     }
 
+    /// Ensure auction type is `English`
+    pub fn ensure_is_english_auction<T: Trait>(&self) -> DispatchResult {
+        ensure!(
+            matches!(&self.auction_type, AuctionType::English(_)),
+            Error::<T>::IsNotEnglishAuctionType
+        );
+        Ok(())
+    }
+
     /// Ensure bid lock duration expired
     pub fn ensure_bid_lock_duration_expired<T: Trait>(
         &self,
         current_block: BlockNumber,
-        bid: &Bid<MemberId, BlockNumber, Balance>,
+        bid: Bid<MemberId, AccountId, BlockNumber, Balance>,
     ) -> DispatchResult {
-        if let AuctionType::Open(bid_lock_duration) = &self.auction_type {
+        if let AuctionType::Open(OpenAuctionDetails { bid_lock_duration }) = &self.auction_type {
             ensure!(
-                current_block - bid.time >= *bid_lock_duration,
+                current_block - bid.made_at_block >= *bid_lock_duration,
                 Error::<T>::BidLockDurationIsNotExpired
             );
         }
@@ -255,7 +398,7 @@ impl<
         // ensure last bid exists
         let last_bid = self.ensure_last_bid_exists::<T>()?;
 
-        // Ensure caller is last bidder.
+        // ensure caller is last bidder.
         self.ensure_caller_is_last_bidder::<T>(who)?;
 
         // ensure bid lock duration expired
@@ -264,9 +407,9 @@ impl<
 
     /// If whitelist set, ensure provided member is authorized to make bids
     pub fn ensure_whitelisted_participant<T: Trait>(&self, who: MemberId) -> DispatchResult {
-        if let Some(whitelist) = &self.whitelist {
+        if !self.whitelist.is_empty() {
             ensure!(
-                whitelist.contains(&who),
+                self.whitelist.contains(&who),
                 Error::<T>::MemberIsNotAllowedToParticipate
             );
         }
@@ -276,9 +419,9 @@ impl<
     /// Ensure auction has last bid, return corresponding reference
     pub fn ensure_last_bid_exists<T: Trait>(
         &self,
-    ) -> Result<&Bid<MemberId, BlockNumber, Balance>, Error<T>> {
+    ) -> Result<Bid<MemberId, AccountId, BlockNumber, Balance>, Error<T>> {
         if let Some(bid) = &self.last_bid {
-            Ok(bid)
+            Ok(bid.clone())
         } else {
             Err(Error::<T>::LastBidDoesNotExist)
         }
@@ -286,44 +429,65 @@ impl<
 }
 
 /// Auction alias type for simplification.
-pub type Auction<T> =
-    AuctionRecord<<T as frame_system::Trait>::BlockNumber, BalanceOf<T>, MemberId<T>>;
+pub type Auction<T> = AuctionRecord<
+    <T as frame_system::Trait>::BlockNumber,
+    BalanceOf<T>,
+    MemberId<T>,
+    <T as frame_system::Trait>::AccountId,
+>;
 
 /// OwnedNFT alias type for simplification.
 pub type Nft<T> = OwnedNFT<
     <T as frame_system::Trait>::BlockNumber,
     MemberId<T>,
-    CuratorGroupId<T>,
-    DAOId<T>,
+    <T as frame_system::Trait>::AccountId,
     BalanceOf<T>,
 >;
 
 /// Parameters, needed for auction start
 #[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
 #[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)]
-pub struct AuctionParams<VideoId, BlockNumber, Balance, MemberId: Ord> {
-    pub video_id: VideoId,
-    /// Auction type (either english or open)
+pub struct AuctionParams<BlockNumber, Balance, MemberId: Ord> {
+    // Auction type (either english or open)
     pub auction_type: AuctionType<BlockNumber>,
     pub starting_price: Balance,
     pub minimal_bid_step: Balance,
     pub buy_now_price: Option<Balance>,
     pub starts_at: Option<BlockNumber>,
-    pub whitelist: Option<BTreeSet<MemberId>>,
+    pub whitelist: BTreeSet<MemberId>,
 }
 
 /// Auction type
 #[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
 #[derive(Encode, Decode, Clone, PartialEq, Eq, Debug)]
 pub enum AuctionType<BlockNumber> {
-    // Auction round duration
-    English(BlockNumber),
-    // Bid lock duration
-    Open(BlockNumber),
+    // English auction details
+    English(EnglishAuctionDetails<BlockNumber>),
+    // Open auction details
+    Open(OpenAuctionDetails<BlockNumber>),
 }
 
 impl<BlockNumber: Default> Default for AuctionType<BlockNumber> {
     fn default() -> Self {
-        Self::English(BlockNumber::default())
+        Self::English(EnglishAuctionDetails::default())
     }
 }
+
+/// English auction details
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)]
+pub struct EnglishAuctionDetails<BlockNumber> {
+    // the remaining time on a lot will automatically reset to to the preset extension time
+    // if a new bid is placed within that period
+    pub extension_period: BlockNumber,
+    // auction duration
+    pub auction_duration: BlockNumber,
+}
+
+/// Open auction details
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Clone, PartialEq, Eq, Debug)]
+pub struct OpenAuctionDetails<BlockNumber> {
+    // bid lock duration
+    pub bid_lock_duration: BlockNumber,
+}

+ 61 - 10
runtime-modules/content/src/permissions/mod.rs

@@ -105,7 +105,7 @@ pub fn ensure_actor_authorized_to_create_channel<T: Trait>(
     match actor {
         // Lead should use their member or curator role to create or update channel assets.
         ContentActor::Lead => {
-            Err(Error::<T>::ActorCannotOwnChannel.into())
+            Err(Error::<T>::ActorNotAuthorized.into())
         }
         ContentActor::Curator(curator_group_id, curator_id) => {
             let sender = ensure_signed(origin)?;
@@ -131,13 +131,13 @@ pub fn ensure_actor_authorized_to_create_channel<T: Trait>(
 pub fn ensure_actor_authorized_to_update_channel<T: Trait>(
     origin: T::Origin,
     actor: &ContentActor<T::CuratorGroupId, T::CuratorId, T::MemberId>,
-    owner: &ChannelOwner<T::MemberId, T::CuratorGroupId, T::DAOId>,
+    owner: &ChannelOwner<T::MemberId, T::CuratorGroupId>,
 ) -> DispatchResult {
+    let sender = ensure_signed(origin)?;
     // 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(())
@@ -146,8 +146,6 @@ pub fn ensure_actor_authorized_to_update_channel<T: Trait>(
             }
         }
         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,
@@ -164,8 +162,6 @@ pub fn ensure_actor_authorized_to_update_channel<T: Trait>(
             Ok(())
         }
         ContentActor::Member(member_id) => {
-            let sender = ensure_signed(origin)?;
-
             ensure_member_auth_success::<T>(member_id, &sender)?;
 
             // Ensure the member is the channel owner.
@@ -176,11 +172,66 @@ pub fn ensure_actor_authorized_to_update_channel<T: Trait>(
 
             Ok(())
         }
-        // TODO:
-        // ContentActor::Dao(_daoId) => ...,
     }
 }
 
+/// Enure actor can manage nft
+pub fn ensure_actor_authorized_to_manage_nft<T: Trait>(
+    origin: T::Origin,
+    actor: &ContentActor<T::CuratorGroupId, T::CuratorId, T::MemberId>,
+    nft_owner: &NFTOwner<T::MemberId>,
+    in_channel: T::ChannelId,
+) -> DispatchResult {
+    let sender = ensure_signed(origin)?;
+
+    if let NFTOwner::Member(member_id) = nft_owner {
+        ensure_member_auth_success::<T>(member_id, &sender)?;
+
+        ensure!(
+            *actor == ContentActor::Member(*member_id),
+            Error::<T>::ActorNotAuthorized
+        );
+    } else {
+        // Ensure curator group is the channel owner.
+        let channel_owner = Module::<T>::ensure_channel_exists(&in_channel)?.owner;
+
+        match actor {
+            ContentActor::Lead => {
+                ensure_lead_auth_success::<T>(&sender)?;
+                if let ChannelOwner::CuratorGroup(_) = channel_owner {
+                    return Ok(());
+                } else {
+                    return Err(Error::<T>::ActorNotAuthorized.into());
+                }
+            }
+            ContentActor::Curator(curator_group_id, curator_id) => {
+                // Authorize curator, performing all checks to ensure curator can act
+                CuratorGroup::<T>::perform_curator_in_group_auth(
+                    curator_id,
+                    curator_group_id,
+                    &sender,
+                )?;
+
+                // Ensure curator group is the channel owner.
+                ensure!(
+                    channel_owner == ChannelOwner::CuratorGroup(*curator_group_id),
+                    Error::<T>::ActorNotAuthorized
+                );
+            }
+            ContentActor::Member(member_id) => {
+                ensure_member_auth_success::<T>(member_id, &sender)?;
+
+                // Ensure the member is the channel owner.
+                ensure!(
+                    channel_owner == ChannelOwner::Member(*member_id),
+                    Error::<T>::ActorNotAuthorized
+                );
+            }
+        }
+    }
+    Ok(())
+}
+
 // Enure actor can update or delete channels and videos
 pub fn ensure_actor_authorized_to_set_featured_videos<T: Trait>(
     origin: T::Origin,
@@ -198,7 +249,7 @@ pub fn ensure_actor_authorized_to_set_featured_videos<T: Trait>(
 pub fn ensure_actor_authorized_to_censor<T: Trait>(
     origin: T::Origin,
     actor: &ContentActor<T::CuratorGroupId, T::CuratorId, T::MemberId>,
-    owner: &ChannelOwner<T::MemberId, T::CuratorGroupId, T::DAOId>,
+    owner: &ChannelOwner<T::MemberId, T::CuratorGroupId>,
 ) -> DispatchResult {
     // Only lead and curators can censor channels and videos
     // Only lead can censor curator group owned channels and videos

+ 336 - 44
runtime-modules/content/src/tests/channels.rs

@@ -2,9 +2,301 @@
 
 use super::curators;
 use super::mock::*;
+use crate::sp_api_hidden_includes_decl_storage::hidden_include::traits::Currency;
 use crate::*;
 use frame_support::{assert_err, assert_ok};
 
+#[test]
+fn successful_channel_deletion() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        // create an account with enought balance
+        let _ = balances::Module::<Test>::deposit_creating(
+            &FIRST_MEMBER_ORIGIN,
+            <Test as balances::Trait>::Balance::from(100u32),
+        );
+
+        // 3 assets
+        let assets = NewAssets::<Test>::Upload(CreationUploadParameters {
+            object_creation_list: vec![
+                DataObjectCreationParameters {
+                    size: 3,
+                    ipfs_content_id: b"first".to_vec(),
+                },
+                DataObjectCreationParameters {
+                    size: 3,
+                    ipfs_content_id: b"second".to_vec(),
+                },
+                DataObjectCreationParameters {
+                    size: 3,
+                    ipfs_content_id: b"third".to_vec(),
+                },
+            ],
+            expected_data_size_fee: storage::DataObjectPerMegabyteFee::<Test>::get(),
+        });
+
+        let channel_id = NextChannelId::<Test>::get();
+
+        // create channel
+        create_channel_mock(
+            FIRST_MEMBER_ORIGIN,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            ChannelCreationParametersRecord {
+                assets: assets,
+                meta: vec![],
+                reward_account: None,
+            },
+            Ok(()),
+        );
+
+        // attempt to delete channel with non zero assets
+        delete_channel_mock(
+            FIRST_MEMBER_ORIGIN,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            channel_id,
+            Err(Error::<Test>::ChannelContainsAssets.into()),
+        );
+
+        // delete assets
+        let assets_to_delete = [0u64, 1u64, 2u64]
+            .iter()
+            .map(|&x| x)
+            .collect::<BTreeSet<_>>();
+
+        // delete channel assets
+        delete_channel_assets_mock(
+            FIRST_MEMBER_ORIGIN,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            channel_id,
+            assets_to_delete,
+            Ok(()),
+        );
+
+        let params = get_video_creation_parameters();
+
+        let video_id = NextVideoId::<Test>::get();
+
+        // Create simple video using member actor
+        create_video_mock(
+            FIRST_MEMBER_ORIGIN,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            channel_id,
+            params,
+            Ok(()),
+        );
+
+        // attempt to delete channel with non zero videos
+        delete_channel_mock(
+            FIRST_MEMBER_ORIGIN,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            channel_id,
+            Err(Error::<Test>::ChannelContainsVideos.into()),
+        );
+
+        // delete video
+        assert_ok!(Content::delete_video(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id
+        ));
+
+        // successful deletion
+        delete_channel_mock(
+            FIRST_MEMBER_ORIGIN,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            channel_id,
+            Ok(()),
+        );
+    })
+}
+
+#[test]
+fn successful_channel_assets_deletion() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        // create an account with enought balance
+        let _ = balances::Module::<Test>::deposit_creating(
+            &FIRST_MEMBER_ORIGIN,
+            <Test as balances::Trait>::Balance::from(100u32),
+        );
+
+        // 3 assets
+        let assets = NewAssets::<Test>::Upload(CreationUploadParameters {
+            object_creation_list: vec![
+                DataObjectCreationParameters {
+                    size: 3,
+                    ipfs_content_id: b"first".to_vec(),
+                },
+                DataObjectCreationParameters {
+                    size: 3,
+                    ipfs_content_id: b"second".to_vec(),
+                },
+                DataObjectCreationParameters {
+                    size: 3,
+                    ipfs_content_id: b"third".to_vec(),
+                },
+            ],
+            expected_data_size_fee: storage::DataObjectPerMegabyteFee::<Test>::get(),
+        });
+
+        let channel_id = NextChannelId::<Test>::get();
+        // create channel
+        create_channel_mock(
+            FIRST_MEMBER_ORIGIN,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            ChannelCreationParametersRecord {
+                assets: assets,
+                meta: vec![],
+                reward_account: None,
+            },
+            Ok(()),
+        );
+
+        // delete assets
+        let assets_to_delete = [0u64, 1u64].iter().map(|&x| x).collect::<BTreeSet<_>>();
+
+        // delete channel assets
+        delete_channel_assets_mock(
+            FIRST_MEMBER_ORIGIN,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            channel_id,
+            assets_to_delete,
+            Ok(()),
+        );
+    })
+}
+
+#[test]
+fn succesful_channel_update() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        // create an account with enought balance
+        let _ = balances::Module::<Test>::deposit_creating(
+            &FIRST_MEMBER_ORIGIN,
+            <Test as balances::Trait>::Balance::from(100u32),
+        );
+
+        // 2 + 1 assets to be uploaded
+        let assets = NewAssets::<Test>::Upload(CreationUploadParameters {
+            object_creation_list: vec![
+                DataObjectCreationParameters {
+                    size: 3,
+                    ipfs_content_id: b"first".to_vec(),
+                },
+                DataObjectCreationParameters {
+                    size: 3,
+                    ipfs_content_id: b"second".to_vec(),
+                },
+            ],
+            expected_data_size_fee: storage::DataObjectPerMegabyteFee::<Test>::get(),
+        });
+
+        let new_assets = NewAssets::<Test>::Upload(CreationUploadParameters {
+            object_creation_list: vec![
+                DataObjectCreationParameters {
+                    size: 3,
+                    ipfs_content_id: b"first".to_vec(),
+                },
+                DataObjectCreationParameters {
+                    size: 3,
+                    ipfs_content_id: b"second".to_vec(),
+                },
+            ],
+            expected_data_size_fee: storage::DataObjectPerMegabyteFee::<Test>::get(),
+        });
+
+        let channel_id = NextChannelId::<Test>::get();
+        // create channel
+        create_channel_mock(
+            FIRST_MEMBER_ORIGIN,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            ChannelCreationParametersRecord {
+                assets: assets,
+                meta: vec![],
+                reward_account: None,
+            },
+            Ok(()),
+        );
+
+        // update channel
+        update_channel_mock(
+            FIRST_MEMBER_ORIGIN,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            channel_id,
+            ChannelUpdateParametersRecord {
+                assets: Some(new_assets),
+                new_meta: None,
+                reward_account: None,
+            },
+            Ok(()),
+        );
+
+        // update with 0 assets
+        update_channel_mock(
+            FIRST_MEMBER_ORIGIN,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            channel_id,
+            ChannelUpdateParametersRecord {
+                assets: None,
+                new_meta: None,
+                reward_account: None,
+            },
+            Ok(()),
+        );
+    })
+}
+
+#[test]
+fn succesful_channel_creation() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        // create an account with enought balance
+        let _ = balances::Module::<Test>::deposit_creating(
+            &FIRST_MEMBER_ORIGIN,
+            <Test as balances::Trait>::Balance::from(100u32),
+        );
+
+        // 3 assets to be uploaded
+        let assets = NewAssets::<Test>::Upload(CreationUploadParameters {
+            object_creation_list: vec![
+                DataObjectCreationParameters {
+                    size: 3,
+                    ipfs_content_id: b"first".to_vec(),
+                },
+                DataObjectCreationParameters {
+                    size: 3,
+                    ipfs_content_id: b"second".to_vec(),
+                },
+                DataObjectCreationParameters {
+                    size: 3,
+                    ipfs_content_id: b"third".to_vec(),
+                },
+            ],
+            expected_data_size_fee: storage::DataObjectPerMegabyteFee::<Test>::get(),
+        });
+
+        // create channel
+        create_channel_mock(
+            FIRST_MEMBER_ORIGIN,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            ChannelCreationParametersRecord {
+                assets: assets,
+                meta: vec![],
+                reward_account: None,
+            },
+            Ok(()),
+        );
+    })
+}
+
 #[test]
 fn lead_cannot_create_channel() {
     with_default_mock_builder(|| {
@@ -12,13 +304,13 @@ fn lead_cannot_create_channel() {
             Content::create_channel(
                 Origin::signed(LEAD_ORIGIN),
                 ContentActor::Lead,
-                ChannelCreationParameters {
-                    assets: vec![],
+                ChannelCreationParametersRecord {
+                    assets: NewAssets::<Test>::Urls(vec![]),
                     meta: vec![],
                     reward_account: None,
                 }
             ),
-            Error::<Test>::ActorCannotOwnChannel
+            Error::<Test>::ActorNotAuthorized
         );
     })
 }
@@ -34,8 +326,8 @@ fn curator_owned_channels() {
             Content::create_channel(
                 Origin::signed(FIRST_CURATOR_ORIGIN),
                 ContentActor::Curator(FIRST_CURATOR_GROUP_ID, FIRST_CURATOR_ID),
-                ChannelCreationParameters {
-                    assets: vec![],
+                ChannelCreationParametersRecord {
+                    assets: NewAssets::<Test>::Urls(vec![]),
                     meta: vec![],
                     reward_account: None,
                 }
@@ -51,8 +343,8 @@ fn curator_owned_channels() {
             Content::create_channel(
                 Origin::signed(SECOND_CURATOR_ORIGIN),
                 ContentActor::Curator(FIRST_CURATOR_GROUP_ID, SECOND_CURATOR_ID),
-                ChannelCreationParameters {
-                    assets: vec![],
+                ChannelCreationParametersRecord {
+                    assets: NewAssets::<Test>::Urls(vec![]),
                     meta: vec![],
                     reward_account: None,
                 }
@@ -65,8 +357,8 @@ fn curator_owned_channels() {
             Content::create_channel(
                 Origin::signed(SECOND_CURATOR_ORIGIN),
                 ContentActor::Curator(FIRST_CURATOR_GROUP_ID, FIRST_CURATOR_ID),
-                ChannelCreationParameters {
-                    assets: vec![],
+                ChannelCreationParametersRecord {
+                    assets: NewAssets::<Test>::Urls(vec![]),
                     meta: vec![],
                     reward_account: None,
                 }
@@ -80,8 +372,8 @@ fn curator_owned_channels() {
         assert_ok!(Content::create_channel(
             Origin::signed(FIRST_CURATOR_ORIGIN),
             ContentActor::Curator(FIRST_CURATOR_GROUP_ID, FIRST_CURATOR_ID),
-            ChannelCreationParameters {
-                assets: vec![],
+            ChannelCreationParametersRecord {
+                assets: NewAssets::<Test>::Urls(vec![]),
                 meta: vec![],
                 reward_account: None,
             }
@@ -94,14 +386,14 @@ fn curator_owned_channels() {
                 channel_id,
                 ChannelRecord {
                     owner: ChannelOwner::CuratorGroup(FIRST_CURATOR_GROUP_ID),
-                    videos: vec![],
-                    playlists: vec![],
-                    series: vec![],
                     is_censored: false,
                     reward_account: None,
+                    deletion_prize_source_account_id: FIRST_CURATOR_ORIGIN,
+                    num_assets: 0,
+                    num_videos: 0,
                 },
-                ChannelCreationParameters {
-                    assets: vec![],
+                ChannelCreationParametersRecord {
+                    assets: NewAssets::<Test>::Urls(vec![]),
                     meta: vec![],
                     reward_account: None,
                 }
@@ -113,7 +405,7 @@ fn curator_owned_channels() {
             Origin::signed(FIRST_CURATOR_ORIGIN),
             ContentActor::Curator(FIRST_CURATOR_GROUP_ID, FIRST_CURATOR_ID),
             channel_id,
-            ChannelUpdateParameters {
+            ChannelUpdateParametersRecord {
                 assets: None,
                 new_meta: None,
                 reward_account: None,
@@ -125,7 +417,7 @@ fn curator_owned_channels() {
             Origin::signed(LEAD_ORIGIN),
             ContentActor::Lead,
             channel_id,
-            ChannelUpdateParameters {
+            ChannelUpdateParametersRecord {
                 assets: None,
                 new_meta: None,
                 reward_account: None,
@@ -145,8 +437,8 @@ fn member_owned_channels() {
             Content::create_channel(
                 Origin::signed(UNKNOWN_ORIGIN),
                 ContentActor::Member(MEMBERS_COUNT + 1),
-                ChannelCreationParameters {
-                    assets: vec![],
+                ChannelCreationParametersRecord {
+                    assets: NewAssets::<Test>::Urls(vec![]),
                     meta: vec![],
                     reward_account: None,
                 }
@@ -160,8 +452,8 @@ fn member_owned_channels() {
         assert_ok!(Content::create_channel(
             Origin::signed(FIRST_MEMBER_ORIGIN),
             ContentActor::Member(FIRST_MEMBER_ID),
-            ChannelCreationParameters {
-                assets: vec![],
+            ChannelCreationParametersRecord {
+                assets: NewAssets::<Test>::Urls(vec![]),
                 meta: vec![],
                 reward_account: None,
             }
@@ -174,14 +466,14 @@ fn member_owned_channels() {
                 channel_id_1,
                 ChannelRecord {
                     owner: ChannelOwner::Member(FIRST_MEMBER_ID),
-                    videos: vec![],
-                    playlists: vec![],
-                    series: vec![],
                     is_censored: false,
                     reward_account: None,
+                    deletion_prize_source_account_id: FIRST_MEMBER_ORIGIN,
+                    num_assets: 0,
+                    num_videos: 0,
                 },
-                ChannelCreationParameters {
-                    assets: vec![],
+                ChannelCreationParametersRecord {
+                    assets: NewAssets::<Test>::Urls(vec![]),
                     meta: vec![],
                     reward_account: None,
                 }
@@ -194,8 +486,8 @@ fn member_owned_channels() {
         assert_ok!(Content::create_channel(
             Origin::signed(SECOND_MEMBER_ORIGIN),
             ContentActor::Member(SECOND_MEMBER_ID),
-            ChannelCreationParameters {
-                assets: vec![],
+            ChannelCreationParametersRecord {
+                assets: NewAssets::<Test>::Urls(vec![]),
                 meta: vec![],
                 reward_account: None,
             }
@@ -208,14 +500,14 @@ fn member_owned_channels() {
                 channel_id_2,
                 ChannelRecord {
                     owner: ChannelOwner::Member(SECOND_MEMBER_ID),
-                    videos: vec![],
-                    playlists: vec![],
-                    series: vec![],
                     is_censored: false,
                     reward_account: None,
+                    deletion_prize_source_account_id: SECOND_MEMBER_ORIGIN,
+                    num_assets: 0,
+                    num_videos: 0,
                 },
-                ChannelCreationParameters {
-                    assets: vec![],
+                ChannelCreationParametersRecord {
+                    assets: NewAssets::<Test>::Urls(vec![]),
                     meta: vec![],
                     reward_account: None,
                 }
@@ -227,7 +519,7 @@ fn member_owned_channels() {
             Origin::signed(FIRST_MEMBER_ORIGIN),
             ContentActor::Member(FIRST_MEMBER_ID),
             channel_id_1,
-            ChannelUpdateParameters {
+            ChannelUpdateParametersRecord {
                 assets: None,
                 new_meta: None,
                 reward_account: None,
@@ -241,13 +533,13 @@ fn member_owned_channels() {
                 channel_id_1,
                 ChannelRecord {
                     owner: ChannelOwner::Member(FIRST_MEMBER_ID),
-                    videos: vec![],
-                    playlists: vec![],
-                    series: vec![],
                     is_censored: false,
                     reward_account: None,
+                    deletion_prize_source_account_id: FIRST_MEMBER_ORIGIN,
+                    num_assets: 0,
+                    num_videos: 0,
                 },
-                ChannelUpdateParameters {
+                ChannelUpdateParametersRecord {
                     assets: None,
                     new_meta: None,
                     reward_account: None,
@@ -261,7 +553,7 @@ fn member_owned_channels() {
                 Origin::signed(FIRST_MEMBER_ORIGIN),
                 ContentActor::Member(FIRST_MEMBER_ID),
                 channel_id_2,
-                ChannelUpdateParameters {
+                ChannelUpdateParametersRecord {
                     assets: None,
                     new_meta: None,
                     reward_account: None,
@@ -282,8 +574,8 @@ fn channel_censoring() {
         assert_ok!(Content::create_channel(
             Origin::signed(FIRST_MEMBER_ORIGIN),
             ContentActor::Member(FIRST_MEMBER_ID),
-            ChannelCreationParameters {
-                assets: vec![],
+            ChannelCreationParametersRecord {
+                assets: NewAssets::<Test>::Urls(vec![]),
                 meta: vec![],
                 reward_account: None,
             }
@@ -358,8 +650,8 @@ fn channel_censoring() {
         assert_ok!(Content::create_channel(
             Origin::signed(FIRST_CURATOR_ORIGIN),
             ContentActor::Curator(group_id, FIRST_CURATOR_ID),
-            ChannelCreationParameters {
-                assets: vec![],
+            ChannelCreationParametersRecord {
+                assets: NewAssets::<Test>::Urls(vec![]),
                 meta: vec![],
                 reward_account: None,
             }

+ 1 - 1
runtime-modules/content/src/tests/curators.rs

@@ -1,6 +1,6 @@
 #![cfg(test)]
 
-use super::mock::*;
+use super::mock::{CuratorGroupId, CuratorId, *};
 use crate::*;
 use frame_support::{assert_err, assert_ok};
 

+ 557 - 38
runtime-modules/content/src/tests/mock.rs

@@ -2,28 +2,33 @@
 
 use crate::*;
 
+use frame_support::dispatch::{DispatchError, DispatchResult};
 use frame_support::traits::{OnFinalize, OnInitialize};
 use frame_support::{impl_outer_event, impl_outer_origin, parameter_types};
 use sp_core::H256;
 use sp_runtime::{
     testing::Header,
     traits::{BlakeTwo256, IdentityLookup},
-    Perbill,
+    ModuleId, Perbill,
 };
 
 use crate::ContentActorAuthenticator;
 use crate::Trait;
 use common::currency::GovernanceCurrency;
-use common::storage::StorageSystem;
+use frame_support::assert_ok;
 
 pub type CuratorId = <Test as ContentActorAuthenticator>::CuratorId;
 pub type CuratorGroupId = <Test as ContentActorAuthenticator>::CuratorGroupId;
 pub type MemberId = <Test as membership::Trait>::MemberId;
 pub type ChannelId = <Test as StorageOwnership>::ChannelId;
-// pub type DAOId = <Test as StorageOwnership>::DAOId;
+pub type VideoId = <Test as Trait>::VideoId;
+pub type VideoCategoryId = <Test as Trait>::VideoCategoryId;
+pub type ChannelCategoryId = <Test as Trait>::ChannelCategoryId;
+type ChannelOwnershipTransferRequestId = <Test as Trait>::ChannelOwnershipTransferRequestId;
 
-/// Origins
+pub const REWARD_ACCOUNT_ID: u64 = 25;
 
+/// Origins
 pub const LEAD_ORIGIN: u64 = 1;
 
 pub const FIRST_CURATOR_ORIGIN: u64 = 2;
@@ -38,6 +43,8 @@ pub const MEMBERS_COUNT: MemberId = 10;
 
 /// Runtime Id's
 
+pub const UNKNOWN_ID: u64 = 545;
+
 pub const FIRST_CURATOR_ID: CuratorId = 1;
 pub const SECOND_CURATOR_ID: CuratorId = 2;
 
@@ -46,6 +53,8 @@ pub const FIRST_CURATOR_GROUP_ID: CuratorGroupId = 1;
 
 pub const FIRST_MEMBER_ID: MemberId = 1;
 pub const SECOND_MEMBER_ID: MemberId = 2;
+pub const THIRD_MEMBER_ID: MemberId = 7;
+pub const FOURTH_MEMBER_ID: MemberId = 8;
 
 impl_outer_origin! {
     pub enum Origin for Test {}
@@ -55,11 +64,21 @@ mod content {
     pub use crate::Event;
 }
 
+mod storage_mod {
+    pub use storage::Event;
+}
+
+mod membership_mod {
+    pub use membership::Event;
+}
+
 impl_outer_event! {
     pub enum MetaEvent for Test {
         content<T>,
         frame_system<T>,
         balances<T>,
+        membership_mod<T>,
+        storage_mod<T>,
     }
 }
 
@@ -102,16 +121,8 @@ impl frame_system::Trait for Test {
     type SystemWeightInfo = ();
 }
 
-impl pallet_timestamp::Trait for Test {
-    type BlockNumber = u64;
-    type OnTimestampSet = ();
-    type MinimumPeriod = MinimumPeriod;
-    type WeightInfo = ();
-}
-
 impl common::StorageOwnership for Test {
     type ChannelId = u64;
-    type DAOId = u64;
     type ContentId = u64;
     type DataObjectTypeId = u64;
 }
@@ -130,10 +141,30 @@ impl balances::Trait for Test {
     type MaxLocks = ();
 }
 
+impl pallet_timestamp::Trait for Test {
+    type Moment = u64;
+    type OnTimestampSet = ();
+    type MinimumPeriod = MinimumPeriod;
+    type WeightInfo = ();
+}
+
 impl GovernanceCurrency for Test {
     type Currency = balances::Module<Self>;
 }
 
+parameter_types! {
+    pub const ScreenedMemberMaxInitialBalance: u64 = 5000;
+}
+
+impl membership::Trait for Test {
+    type Event = MetaEvent;
+    type MemberId = u64;
+    type PaidTermId = u64;
+    type SubscriptionId = u64;
+    type ActorId = u64;
+    type ScreenedMemberMaxInitialBalance = ();
+}
+
 impl ContentActorAuthenticator for Test {
     type CuratorId = u64;
     type CuratorGroupId = u64;
@@ -161,39 +192,144 @@ impl ContentActorAuthenticator for Test {
     }
 }
 
-pub struct MockStorageSystem {}
+parameter_types! {
+    pub const MaxNumberOfDataObjectsPerBag: u64 = 4;
+    pub const MaxDistributionBucketFamilyNumber: u64 = 4;
+    pub const MaxDistributionBucketNumberPerFamily: u64 = 10;
+    pub const DataObjectDeletionPrize: u64 = 10;
+    pub const StorageModuleId: ModuleId = ModuleId(*b"mstorage"); // module storage
+    pub const BlacklistSizeLimit: u64 = 1;
+    pub const MaxNumberOfPendingInvitationsPerDistributionBucket: u64 = 1;
+    pub const StorageBucketsPerBagValueConstraint: storage::StorageBucketsPerBagValueConstraint =
+        storage::StorageBucketsPerBagValueConstraint {min: 3, max_min_diff: 7};
+    pub const InitialStorageBucketsNumberForDynamicBag: u64 = 3;
+    pub const MaxRandomIterationNumber: u64 = 3;
+    pub const DefaultMemberDynamicBagNumberOfStorageBuckets: u64 = 3;
+    pub const DefaultChannelDynamicBagNumberOfStorageBuckets: u64 = 4;
+    pub const DistributionBucketsPerBagValueConstraint: storage::DistributionBucketsPerBagValueConstraint =
+        storage::StorageBucketsPerBagValueConstraint {min: 3, max_min_diff: 7};
+    pub const MaxDataObjectSize: u64 = 400;
+}
+
+pub const STORAGE_WG_LEADER_ACCOUNT_ID: u64 = 100001;
+pub const DEFAULT_STORAGE_PROVIDER_ACCOUNT_ID: u64 = 100002;
+pub const DEFAULT_DISTRIBUTION_PROVIDER_ACCOUNT_ID: u64 = 100003;
+pub const DISTRIBUTION_WG_LEADER_ACCOUNT_ID: u64 = 100004;
+pub const DEFAULT_STORAGE_PROVIDER_ID: u64 = 10;
+pub const ANOTHER_STORAGE_PROVIDER_ID: u64 = 11;
+pub const DEFAULT_DISTRIBUTION_PROVIDER_ID: u64 = 12;
+pub const ANOTHER_DISTRIBUTION_PROVIDER_ID: u64 = 13;
 
-// Anyone can upload and delete without restriction
-impl StorageSystem<Test> for MockStorageSystem {
-    fn atomically_add_content(
-        _owner: StorageObjectOwner<Test>,
-        _content_parameters: Vec<ContentParameters<Test>>,
-    ) -> DispatchResult {
-        Ok(())
+impl storage::Trait for Test {
+    type Event = MetaEvent;
+    type DataObjectId = u64;
+    type StorageBucketId = u64;
+    type DistributionBucketId = u64;
+    type DistributionBucketFamilyId = u64;
+    type DistributionBucketOperatorId = u64;
+    type ChannelId = u64;
+    type MaxNumberOfDataObjectsPerBag = MaxNumberOfDataObjectsPerBag;
+    type DataObjectDeletionPrize = DataObjectDeletionPrize;
+    type BlacklistSizeLimit = BlacklistSizeLimit;
+    type ModuleId = StorageModuleId;
+    type MemberOriginValidator = ();
+    type StorageBucketsPerBagValueConstraint = StorageBucketsPerBagValueConstraint;
+    type DefaultMemberDynamicBagNumberOfStorageBuckets =
+        DefaultMemberDynamicBagNumberOfStorageBuckets;
+    type DefaultChannelDynamicBagNumberOfStorageBuckets =
+        DefaultChannelDynamicBagNumberOfStorageBuckets;
+    type Randomness = CollectiveFlip;
+    type MaxRandomIterationNumber = MaxRandomIterationNumber;
+    type MaxDistributionBucketFamilyNumber = MaxDistributionBucketFamilyNumber;
+    type MaxDistributionBucketNumberPerFamily = MaxDistributionBucketNumberPerFamily;
+    type DistributionBucketsPerBagValueConstraint = DistributionBucketsPerBagValueConstraint;
+    type MaxNumberOfPendingInvitationsPerDistributionBucket =
+        MaxNumberOfPendingInvitationsPerDistributionBucket;
+    type ContentId = u64;
+    type MaxDataObjectSize = MaxDataObjectSize;
+
+    fn ensure_storage_working_group_leader_origin(origin: Self::Origin) -> DispatchResult {
+        let account_id = ensure_signed(origin)?;
+
+        if account_id != STORAGE_WG_LEADER_ACCOUNT_ID {
+            Err(DispatchError::BadOrigin)
+        } else {
+            Ok(())
+        }
+    }
+
+    fn ensure_storage_worker_origin(origin: Self::Origin, _: u64) -> DispatchResult {
+        let account_id = ensure_signed(origin)?;
+
+        if account_id != DEFAULT_STORAGE_PROVIDER_ACCOUNT_ID {
+            Err(DispatchError::BadOrigin)
+        } else {
+            Ok(())
+        }
+    }
+
+    fn ensure_storage_worker_exists(worker_id: &u64) -> DispatchResult {
+        let allowed_storage_providers =
+            vec![DEFAULT_STORAGE_PROVIDER_ID, ANOTHER_STORAGE_PROVIDER_ID];
+
+        if !allowed_storage_providers.contains(worker_id) {
+            Err(DispatchError::Other("Invalid worker"))
+        } else {
+            Ok(())
+        }
+    }
+
+    fn ensure_distribution_working_group_leader_origin(origin: Self::Origin) -> DispatchResult {
+        let account_id = ensure_signed(origin)?;
+
+        if account_id != DISTRIBUTION_WG_LEADER_ACCOUNT_ID {
+            Err(DispatchError::BadOrigin)
+        } else {
+            Ok(())
+        }
     }
 
-    fn can_add_content(
-        _owner: StorageObjectOwner<Test>,
-        _content_parameters: Vec<ContentParameters<Test>>,
-    ) -> DispatchResult {
-        Ok(())
+    fn ensure_distribution_worker_origin(origin: Self::Origin, _: u64) -> DispatchResult {
+        let account_id = ensure_signed(origin)?;
+
+        if account_id != DEFAULT_DISTRIBUTION_PROVIDER_ACCOUNT_ID {
+            Err(DispatchError::BadOrigin)
+        } else {
+            Ok(())
+        }
     }
 
-    fn atomically_remove_content(
-        _owner: &StorageObjectOwner<Test>,
-        _content_ids: &[u64],
-    ) -> DispatchResult {
-        Ok(())
+    fn ensure_distribution_worker_exists(worker_id: &u64) -> DispatchResult {
+        let allowed_providers = vec![
+            DEFAULT_DISTRIBUTION_PROVIDER_ID,
+            ANOTHER_DISTRIBUTION_PROVIDER_ID,
+        ];
+
+        if !allowed_providers.contains(worker_id) {
+            Err(DispatchError::Other("Invalid worker"))
+        } else {
+            Ok(())
+        }
     }
+}
 
-    fn can_remove_content(
-        _owner: &StorageObjectOwner<Test>,
-        _content_ids: &[u64],
-    ) -> DispatchResult {
-        Ok(())
+pub const DEFAULT_MEMBER_ID: u64 = 100;
+pub const DEFAULT_MEMBER_ACCOUNT_ID: u64 = 101;
+
+impl common::origin::ActorOriginValidator<Origin, u64, u64> for () {
+    fn ensure_actor_origin(origin: Origin, member_id: u64) -> Result<u64, &'static str> {
+        let signed_account_id = frame_system::ensure_signed(origin)?;
+
+        if signed_account_id == DEFAULT_MEMBER_ACCOUNT_ID && member_id == DEFAULT_MEMBER_ID {
+            Ok(signed_account_id)
+        } else {
+            Err(DispatchError::BadOrigin.into())
+        }
     }
 }
 
+// Anyone can upload and delete without restriction
+
 parameter_types! {
     pub const MaxNumberOfCuratorsPerGroup: u32 = 10;
     pub const ChannelOwnershipPaymentEscrowId: [u8; 8] = *b"12345678";
@@ -229,9 +365,6 @@ impl Trait for Test {
 
     /// The maximum number of curators per group constraint
     type MaxNumberOfCuratorsPerGroup = MaxNumberOfCuratorsPerGroup;
-
-    // Type that handles asset uploads to storage frame_system
-    type StorageSystem = MockStorageSystem;
 }
 
 pub type System = frame_system::Module<Test>;
@@ -247,6 +380,21 @@ pub struct ExtBuilder {
     next_series_id: u64,
     next_channel_transfer_request_id: u64,
     next_curator_group_id: u64,
+    min_auction_duration: u64,
+    max_auction_duration: u64,
+    min_auction_extension_period: u64,
+    max_auction_extension_period: u64,
+    min_bid_lock_duration: u64,
+    max_bid_lock_duration: u64,
+    min_starting_price: u64,
+    max_starting_price: u64,
+    min_creator_royalty: Perbill,
+    max_creator_royalty: Perbill,
+    min_bid_step: u64,
+    max_bid_step: u64,
+    platform_fee_percentage: Perbill,
+    auction_starts_at_max_delta: u64,
+    max_auction_whitelist_length: u32,
 }
 
 impl Default for ExtBuilder {
@@ -261,6 +409,21 @@ impl Default for ExtBuilder {
             next_series_id: 1,
             next_channel_transfer_request_id: 1,
             next_curator_group_id: 1,
+            min_auction_duration: 5,
+            max_auction_duration: 20,
+            min_auction_extension_period: 4,
+            max_auction_extension_period: 30,
+            min_bid_lock_duration: 2,
+            max_bid_lock_duration: 10,
+            min_starting_price: 10,
+            max_starting_price: 1000,
+            min_creator_royalty: Perbill::from_percent(1),
+            max_creator_royalty: Perbill::from_percent(5),
+            min_bid_step: 10,
+            max_bid_step: 100,
+            platform_fee_percentage: Perbill::from_percent(1),
+            auction_starts_at_max_delta: 90_000,
+            max_auction_whitelist_length: 4,
         }
     }
 }
@@ -281,6 +444,21 @@ impl ExtBuilder {
             next_series_id: self.next_series_id,
             next_channel_transfer_request_id: self.next_channel_transfer_request_id,
             next_curator_group_id: self.next_curator_group_id,
+            min_auction_duration: self.min_auction_duration,
+            max_auction_duration: self.max_auction_duration,
+            min_auction_extension_period: self.min_auction_extension_period,
+            max_auction_extension_period: self.max_auction_extension_period,
+            min_bid_lock_duration: self.min_bid_lock_duration,
+            max_bid_lock_duration: self.max_bid_lock_duration,
+            min_starting_price: self.min_starting_price,
+            max_starting_price: self.max_starting_price,
+            min_creator_royalty: self.min_creator_royalty,
+            max_creator_royalty: self.max_creator_royalty,
+            min_bid_step: self.min_bid_step,
+            max_bid_step: self.max_bid_step,
+            platform_fee_percentage: self.platform_fee_percentage,
+            auction_starts_at_max_delta: self.auction_starts_at_max_delta,
+            max_auction_whitelist_length: self.max_auction_whitelist_length,
         }
         .assimilate_storage(&mut t)
         .unwrap();
@@ -302,3 +480,344 @@ pub fn run_to_block(n: u64) {
         <System as OnInitialize<u64>>::on_initialize(System::block_number());
     }
 }
+
+// Events
+
+type RawEvent = crate::RawEvent<
+    ContentActor<CuratorGroupId, CuratorId, MemberId>,
+    MemberId,
+    CuratorGroupId,
+    CuratorId,
+    VideoId,
+    VideoCategoryId,
+    ChannelId,
+    ChannelCategoryId,
+    ChannelOwnershipTransferRequestId,
+    u64,
+    u64,
+    u64,
+    ChannelOwnershipTransferRequest<Test>,
+    Series<<Test as StorageOwnership>::ChannelId, VideoId>,
+    Channel<Test>,
+    <Test as storage::Trait>::DataObjectId,
+    bool,
+    AuctionParams<<Test as frame_system::Trait>::BlockNumber, BalanceOf<Test>, MemberId>,
+    BalanceOf<Test>,
+    ChannelCreationParameters<Test>,
+    ChannelUpdateParameters<Test>,
+    VideoCreationParameters<Test>,
+    VideoUpdateParameters<Test>,
+    NewAssets<Test>,
+    bool,
+>;
+
+pub fn get_test_event(raw_event: RawEvent) -> MetaEvent {
+    MetaEvent::content(raw_event)
+}
+
+pub fn assert_event(tested_event: MetaEvent, number_of_events_after_call: usize) {
+    // Ensure  runtime events length is equal to expected number of events after call
+    assert_eq!(System::events().len(), number_of_events_after_call);
+
+    // Ensure  last emitted event is equal to expected one
+    assert_eq!(System::events().iter().last().unwrap().event, tested_event);
+}
+
+pub fn create_member_channel() -> ChannelId {
+    let channel_id = Content::next_channel_id();
+
+    // Member can create the channel
+    assert_ok!(Content::create_channel(
+        Origin::signed(FIRST_MEMBER_ORIGIN),
+        ContentActor::Member(FIRST_MEMBER_ID),
+        ChannelCreationParametersRecord {
+            assets: NewAssets::<Test>::Urls(vec![]),
+            meta: vec![],
+            reward_account: None,
+        }
+    ));
+
+    channel_id
+}
+
+pub fn get_video_creation_parameters() -> VideoCreationParameters<Test> {
+    VideoCreationParametersRecord {
+        assets: NewAssets::<Test>::Upload(CreationUploadParameters {
+            object_creation_list: vec![
+                DataObjectCreationParameters {
+                    size: 3,
+                    ipfs_content_id: b"first".to_vec(),
+                },
+                DataObjectCreationParameters {
+                    size: 3,
+                    ipfs_content_id: b"second".to_vec(),
+                },
+                DataObjectCreationParameters {
+                    size: 3,
+                    ipfs_content_id: b"third".to_vec(),
+                },
+            ],
+            expected_data_size_fee: storage::DataObjectPerMegabyteFee::<Test>::get(),
+        }),
+        meta: b"test".to_vec(),
+    }
+}
+
+/// Get good params for open auction
+pub fn get_open_auction_params(
+) -> AuctionParams<<Test as frame_system::Trait>::BlockNumber, BalanceOf<Test>, MemberId> {
+    AuctionParams {
+        starting_price: Content::min_starting_price(),
+        buy_now_price: None,
+        auction_type: AuctionType::Open(OpenAuctionDetails {
+            bid_lock_duration: Content::min_bid_lock_duration(),
+        }),
+        minimal_bid_step: Content::min_bid_step(),
+        starts_at: None,
+        whitelist: BTreeSet::new(),
+    }
+}
+
+pub type CollectiveFlip = randomness_collective_flip::Module<Test>;
+
+pub fn create_channel_mock(
+    sender: u64,
+    actor: ContentActor<CuratorGroupId, CuratorId, MemberId>,
+    params: ChannelCreationParameters<Test>,
+    result: DispatchResult,
+) {
+    let channel_id = Content::next_channel_id();
+
+    assert_eq!(
+        Content::create_channel(Origin::signed(sender), actor.clone(), params.clone()),
+        result.clone(),
+    );
+
+    if result.is_ok() {
+        let num_assets = match params.assets.clone() {
+            NewAssets::<Test>::Urls(v) => v.len() as u64,
+            NewAssets::<Test>::Upload(c) => c.object_creation_list.len() as u64,
+        };
+        let owner = Content::actor_to_channel_owner(&actor).unwrap();
+
+        assert_eq!(
+            System::events().last().unwrap().event,
+            MetaEvent::content(RawEvent::ChannelCreated(
+                actor.clone(),
+                channel_id,
+                ChannelRecord {
+                    owner: owner,
+                    is_censored: false,
+                    reward_account: params.reward_account,
+                    deletion_prize_source_account_id: sender,
+                    num_assets: num_assets,
+                    num_videos: 0,
+                },
+                params.clone(),
+            ))
+        );
+    }
+}
+
+pub fn update_channel_mock(
+    sender: u64,
+    actor: ContentActor<CuratorGroupId, CuratorId, MemberId>,
+    channel_id: ChannelId,
+    params: ChannelUpdateParameters<Test>,
+    result: DispatchResult,
+) {
+    let channel_pre = ChannelById::<Test>::get(channel_id.clone());
+
+    assert_eq!(
+        Content::update_channel(
+            Origin::signed(sender),
+            actor.clone(),
+            channel_id.clone(),
+            params.clone()
+        ),
+        result.clone(),
+    );
+
+    if result.is_ok() {
+        let maybe_num_assets = params.assets.clone().map_or(None, |assets| match assets {
+            NewAssets::<Test>::Urls(v) => Some(v.len() as u64),
+            NewAssets::<Test>::Upload(c) => Some(c.object_creation_list.len() as u64),
+        });
+        assert_eq!(
+            System::events().last().unwrap().event,
+            MetaEvent::content(RawEvent::ChannelUpdated(
+                actor.clone(),
+                channel_id,
+                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,
+                    num_assets: channel_pre.num_assets + maybe_num_assets.unwrap_or(0),
+                    num_videos: channel_pre.num_videos,
+                },
+                params.clone(),
+            ))
+        );
+    }
+}
+
+pub fn delete_channel_assets_mock(
+    sender: u64,
+    actor: ContentActor<CuratorGroupId, CuratorId, MemberId>,
+    channel_id: ChannelId,
+    assets: BTreeSet<<Test as storage::Trait>::DataObjectId>,
+    result: DispatchResult,
+) {
+    let channel_pre = ChannelById::<Test>::get(channel_id.clone());
+
+    assert_eq!(
+        Content::remove_channel_assets(
+            Origin::signed(sender),
+            actor.clone(),
+            channel_id.clone(),
+            assets.clone(),
+        ),
+        result.clone(),
+    );
+
+    if result.is_ok() {
+        let num_assets_removed = assets.len();
+        assert_eq!(
+            System::events().last().unwrap().event,
+            MetaEvent::content(RawEvent::ChannelAssetsRemoved(
+                actor.clone(),
+                channel_id,
+                assets.clone(),
+                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,
+                    num_assets: channel_pre.num_assets - (num_assets_removed as u64),
+                    num_videos: channel_pre.num_videos,
+                },
+            ))
+        );
+    }
+}
+
+pub fn delete_channel_mock(
+    sender: u64,
+    actor: ContentActor<CuratorGroupId, CuratorId, MemberId>,
+    channel_id: ChannelId,
+    result: DispatchResult,
+) {
+    assert_eq!(
+        Content::delete_channel(Origin::signed(sender), actor.clone(), channel_id.clone()),
+        result.clone(),
+    );
+
+    if result.is_ok() {
+        assert_eq!(
+            System::events().last().unwrap().event,
+            MetaEvent::content(RawEvent::ChannelDeleted(actor.clone(), channel_id))
+        )
+    }
+}
+
+pub fn create_simple_channel_and_video(sender: u64, member_id: u64) {
+    // deposit initial balance
+    let _ = balances::Module::<Test>::deposit_creating(
+        &sender,
+        <Test as balances::Trait>::Balance::from(30u32),
+    );
+
+    let channel_id = NextChannelId::<Test>::get();
+
+    create_channel_mock(
+        sender,
+        ContentActor::Member(member_id),
+        ChannelCreationParametersRecord {
+            assets: NewAssets::<Test>::Urls(vec![]),
+            meta: vec![],
+            reward_account: Some(REWARD_ACCOUNT_ID),
+        },
+        Ok(()),
+    );
+
+    let params = get_video_creation_parameters();
+
+    // Create simple video using member actor
+    create_video_mock(
+        sender,
+        ContentActor::Member(member_id),
+        channel_id,
+        params,
+        Ok(()),
+    );
+}
+
+pub fn create_video_mock(
+    sender: u64,
+    actor: ContentActor<CuratorGroupId, CuratorId, MemberId>,
+    channel_id: ChannelId,
+    params: VideoCreationParameters<Test>,
+    result: DispatchResult,
+) {
+    let video_id = Content::next_video_id();
+    let num_videos_pre = Content::channel_by_id(channel_id).num_videos;
+
+    assert_eq!(
+        Content::create_video(
+            Origin::signed(sender),
+            actor.clone(),
+            channel_id.clone(),
+            params.clone()
+        ),
+        result.clone(),
+    );
+
+    if result.is_ok() {
+        assert_eq!(
+            System::events().last().unwrap().event,
+            MetaEvent::content(RawEvent::VideoCreated(
+                actor.clone(),
+                channel_id,
+                video_id,
+                params.clone(),
+            ))
+        );
+        assert_eq!(
+            num_videos_pre + 1,
+            Content::channel_by_id(channel_id).num_videos,
+        );
+    }
+}
+
+pub fn update_video_mock(
+    sender: u64,
+    actor: ContentActor<CuratorGroupId, CuratorId, MemberId>,
+    video_id: <Test as Trait>::VideoId,
+    params: VideoUpdateParameters<Test>,
+    result: DispatchResult,
+) {
+    // let channel_id = Content::video_by_id(video_id.clone()).in_channel;
+    // let num_videos_pre = Content::channel_by_id(channel_id).num_videos;
+
+    assert_eq!(
+        Content::update_video(
+            Origin::signed(sender),
+            actor.clone(),
+            video_id.clone(),
+            params.clone()
+        ),
+        result.clone(),
+    );
+
+    if result.is_ok() {
+        assert_eq!(
+            System::events().last().unwrap().event,
+            MetaEvent::content(RawEvent::VideoUpdated(
+                actor.clone(),
+                video_id,
+                params.clone(),
+            ))
+        );
+    }
+}

+ 1 - 0
runtime-modules/content/src/tests/mod.rs

@@ -3,4 +3,5 @@
 mod channels;
 mod curators;
 mod mock;
+mod nft;
 mod videos;

+ 13 - 0
runtime-modules/content/src/tests/nft.rs

@@ -0,0 +1,13 @@
+mod accept_incoming_offer;
+mod buy_nft;
+mod cancel_buy_now;
+mod cancel_nft_auction;
+mod cancel_offer;
+mod cancel_open_auction_bid;
+mod claim_won_english_auction;
+mod issue_nft;
+mod make_bid;
+mod offer_nft;
+mod pick_open_auction_winner;
+mod sell_nft;
+mod start_nft_auction;

+ 287 - 0
runtime-modules/content/src/tests/nft/accept_incoming_offer.rs

@@ -0,0 +1,287 @@
+#![cfg(test)]
+
+use crate::tests::mock::*;
+use crate::*;
+use frame_support::{assert_err, assert_ok};
+
+#[test]
+fn accept_incoming_offer() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        // Offer nft
+        assert_ok!(Content::offer_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            video_id,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            SECOND_MEMBER_ID,
+            None,
+        ));
+
+        // Runtime tested state before call
+
+        // Events number before tested calls
+        let number_of_events_before_call = System::events().len();
+
+        // Accept nft offer
+        assert_ok!(Content::accept_incoming_offer(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            video_id,
+        ));
+
+        // Runtime tested state after call
+
+        // Ensure nft offer accepted succesfully
+        assert!(matches!(
+            Content::video_by_id(video_id).nft_status,
+            Some(OwnedNFT {
+                owner: NFTOwner::Member(member_id),
+                transactional_status: TransactionalStatus::Idle,
+                ..
+            }) if member_id == SECOND_MEMBER_ID
+        ));
+
+        let offer_accepted_event = get_test_event(RawEvent::OfferAccepted(video_id));
+
+        // Last event checked
+        assert_event(offer_accepted_event, number_of_events_before_call + 1);
+    })
+}
+
+#[test]
+fn accept_incoming_offer_video_does_not_exist() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        // Make an attempt to accept incoming nft offer if corresponding video does not exist
+        let accept_incoming_offer_result =
+            Content::accept_incoming_offer(Origin::signed(SECOND_MEMBER_ORIGIN), video_id);
+
+        // Failure checked
+        assert_err!(
+            accept_incoming_offer_result,
+            Error::<Test>::VideoDoesNotExist
+        );
+    })
+}
+
+#[test]
+fn accept_incoming_offer_nft_not_issued() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Make an attempt to accept incoming nft offer if corresponding nft is not issued yet
+        let accept_incoming_offer_result =
+            Content::accept_incoming_offer(Origin::signed(SECOND_MEMBER_ORIGIN), video_id);
+
+        // Failure checked
+        assert_err!(accept_incoming_offer_result, Error::<Test>::NFTDoesNotExist);
+    })
+}
+
+#[test]
+fn accept_incoming_offer_auth_failed() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        // Offer nft
+        assert_ok!(Content::offer_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            video_id,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            SECOND_MEMBER_ID,
+            None,
+        ));
+
+        // Make an attempt to accept incoming nft offer providing wrong credentials
+        let accept_incoming_offer_result =
+            Content::accept_incoming_offer(Origin::signed(UNKNOWN_ORIGIN), video_id);
+
+        // Failure checked
+        assert_err!(
+            accept_incoming_offer_result,
+            Error::<Test>::MemberAuthFailed
+        );
+    })
+}
+
+#[test]
+fn accept_incoming_offer_no_incoming_offers() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        // Make an attempt to accept incoming nft offer if there is no incoming transfers
+        let accept_incoming_offer_result =
+            Content::accept_incoming_offer(Origin::signed(SECOND_MEMBER_ORIGIN), video_id);
+
+        // Failure checked
+        assert_err!(
+            accept_incoming_offer_result,
+            Error::<Test>::PendingOfferDoesNotExist
+        );
+    })
+}
+
+#[test]
+fn accept_incoming_offer_reward_account_is_not_set() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        let _ = balances::Module::<Test>::deposit_creating(
+            &FIRST_MEMBER_ORIGIN,
+            <Test as balances::Trait>::Balance::from(100u32),
+        );
+
+        let channel_id = NextChannelId::<Test>::get();
+
+        create_channel_mock(
+            FIRST_MEMBER_ORIGIN,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            ChannelCreationParametersRecord {
+                assets: NewAssets::<Test>::Urls(vec![]),
+                meta: vec![],
+                reward_account: None,
+            },
+            Ok(()),
+        );
+
+        let params = get_video_creation_parameters();
+
+        // Create simple video using member actor
+        create_video_mock(
+            FIRST_MEMBER_ORIGIN,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            channel_id,
+            params,
+            Ok(()),
+        );
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        // Offer nft
+        assert_ok!(Content::offer_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            video_id,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            SECOND_MEMBER_ID,
+            None,
+        ));
+
+        // Make an attempt to accept incoming nft offer if sender is owner and reward account is not set
+        let accept_incoming_offer_result =
+            Content::accept_incoming_offer(Origin::signed(SECOND_MEMBER_ORIGIN), video_id);
+
+        // Failure checked
+        assert_err!(
+            accept_incoming_offer_result,
+            Error::<Test>::RewardAccountIsNotSet
+        );
+    })
+}
+
+#[test]
+fn accept_incoming_offer_insufficient_balance() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        let price = 10000;
+
+        // Offer nft
+        assert_ok!(Content::offer_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            video_id,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            SECOND_MEMBER_ID,
+            Some(price),
+        ));
+
+        // Make an attempt to accept incoming nft offer if there is no incoming transfers
+        let accept_incoming_offer_result =
+            Content::accept_incoming_offer(Origin::signed(SECOND_MEMBER_ORIGIN), video_id);
+
+        // Failure checked
+        assert_err!(
+            accept_incoming_offer_result,
+            Error::<Test>::InsufficientBalance
+        );
+    })
+}

+ 344 - 0
runtime-modules/content/src/tests/nft/buy_nft.rs

@@ -0,0 +1,344 @@
+#![cfg(test)]
+
+use crate::tests::mock::*;
+use crate::*;
+use frame_support::{assert_err, assert_ok};
+
+#[test]
+fn buy_nft() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        let price = 1000u64;
+
+        // deposit balance to second member
+        let _ = balances::Module::<Test>::deposit_creating(
+            &SECOND_MEMBER_ORIGIN,
+            <Test as balances::Trait>::Balance::from(price),
+        );
+
+        // Sell nft
+        assert_ok!(Content::sell_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            video_id,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            price,
+        ));
+
+        // Runtime tested state before call
+
+        // Events number before tested calls
+        let number_of_events_before_call = System::events().len();
+
+        // Buy nft
+        assert_ok!(Content::buy_nft(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            video_id,
+            SECOND_MEMBER_ID,
+        ));
+
+        // Runtime tested state after call
+
+        // Ensure buyer balance was succesfully slashed after nft had been bought
+        assert_eq!(
+            balances::Module::<Test>::free_balance(SECOND_MEMBER_ORIGIN),
+            0
+        );
+
+        // Ensure the price of nft - platform fee was succesfully deposited into seller account (channel reward account id in this case)
+        assert_eq!(
+            balances::Module::<Test>::free_balance(REWARD_ACCOUNT_ID),
+            price - Content::platform_fee_percentage() * price
+        );
+
+        // Ensure nft succesfully bought
+        assert!(matches!(
+            Content::video_by_id(video_id).nft_status,
+            Some(OwnedNFT {
+                owner: NFTOwner::Member(SECOND_MEMBER_ID),
+                transactional_status: TransactionalStatus::Idle,
+                ..
+            })
+        ));
+
+        let nft_bought_event = get_test_event(RawEvent::NFTBought(video_id, SECOND_MEMBER_ID));
+
+        // Last event checked
+        assert_event(nft_bought_event, number_of_events_before_call + 3);
+    })
+}
+
+#[test]
+fn buy_nft_video_does_not_exist() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        let price = 1000u64;
+
+        // deposit balance to second member
+        let _ = balances::Module::<Test>::deposit_creating(
+            &SECOND_MEMBER_ORIGIN,
+            <Test as balances::Trait>::Balance::from(price),
+        );
+
+        // Make an attempt to buy nft which corresponding video does not exist yet
+        let buy_nft_result = Content::buy_nft(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            video_id,
+            SECOND_MEMBER_ID,
+        );
+
+        // Failure checked
+        assert_err!(buy_nft_result, Error::<Test>::VideoDoesNotExist);
+    })
+}
+
+#[test]
+fn buy_nft_not_issued() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        let price = 1000u64;
+
+        // deposit balance to second member
+        let _ = balances::Module::<Test>::deposit_creating(
+            &SECOND_MEMBER_ORIGIN,
+            <Test as balances::Trait>::Balance::from(price),
+        );
+
+        // Make an attempt to buy nft which is not issued yet
+        let buy_nft_result = Content::buy_nft(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            video_id,
+            SECOND_MEMBER_ID,
+        );
+
+        // Failure checked
+        assert_err!(buy_nft_result, Error::<Test>::NFTDoesNotExist);
+    })
+}
+
+#[test]
+fn buy_nft_auth_failed() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        let price = 1000u64;
+
+        // deposit balance to second member
+        let _ = balances::Module::<Test>::deposit_creating(
+            &SECOND_MEMBER_ORIGIN,
+            <Test as balances::Trait>::Balance::from(price),
+        );
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        // Sell nft
+        assert_ok!(Content::sell_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            video_id,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            price,
+        ));
+
+        // Make an attempt to buy nft with wrong credentials
+        let buy_nft_result =
+            Content::buy_nft(Origin::signed(SECOND_MEMBER_ORIGIN), video_id, UNKNOWN_ID);
+
+        // Failure checked
+        assert_err!(buy_nft_result, Error::<Test>::MemberAuthFailed);
+    })
+}
+
+#[test]
+fn buy_nft_not_in_buy_now_state() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        let price = 1000u64;
+
+        // deposit balance to second member
+        let _ = balances::Module::<Test>::deposit_creating(
+            &SECOND_MEMBER_ORIGIN,
+            <Test as balances::Trait>::Balance::from(price),
+        );
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        // Make an attempt to buy nft which is not in BuyNow state
+        let buy_nft_result = Content::buy_nft(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            video_id,
+            SECOND_MEMBER_ID,
+        );
+
+        // Failure checked
+        assert_err!(buy_nft_result, Error::<Test>::NFTNotInBuyNowState);
+    })
+}
+
+#[test]
+fn buy_nft_insufficient_balance() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        let price = 1000u64;
+
+        // Sell nft
+        assert_ok!(Content::sell_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            video_id,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            price,
+        ));
+
+        // Make an attempt to buy nft with wrong credentials
+        let buy_nft_result = Content::buy_nft(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            video_id,
+            SECOND_MEMBER_ID,
+        );
+
+        // Failure checked
+        assert_err!(buy_nft_result, Error::<Test>::InsufficientBalance);
+    })
+}
+
+#[test]
+fn buy_nft_reward_account_is_not_set() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        let _ = balances::Module::<Test>::deposit_creating(
+            &FIRST_MEMBER_ORIGIN,
+            <Test as balances::Trait>::Balance::from(100u32),
+        );
+
+        let channel_id = NextChannelId::<Test>::get();
+
+        create_channel_mock(
+            FIRST_MEMBER_ORIGIN,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            ChannelCreationParametersRecord {
+                assets: NewAssets::<Test>::Urls(vec![]),
+                meta: vec![],
+                reward_account: None,
+            },
+            Ok(()),
+        );
+
+        let params = get_video_creation_parameters();
+
+        // Create simple video using member actor
+        create_video_mock(
+            FIRST_MEMBER_ORIGIN,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            channel_id,
+            params,
+            Ok(()),
+        );
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        let price = 1000u64;
+
+        // deposit balance to second member
+        let _ = balances::Module::<Test>::deposit_creating(
+            &SECOND_MEMBER_ORIGIN,
+            <Test as balances::Trait>::Balance::from(price),
+        );
+
+        // Sell nft
+        assert_ok!(Content::sell_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            video_id,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            price,
+        ));
+
+        // Make an attempt to buy nft when reward account is not set
+        let buy_nft_result = Content::buy_nft(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            video_id,
+            SECOND_MEMBER_ID,
+        );
+
+        // Failure checked
+        assert_err!(buy_nft_result, Error::<Test>::RewardAccountIsNotSet);
+    })
+}

+ 226 - 0
runtime-modules/content/src/tests/nft/cancel_buy_now.rs

@@ -0,0 +1,226 @@
+#![cfg(test)]
+
+use crate::tests::mock::*;
+use crate::*;
+use frame_support::{assert_err, assert_ok};
+
+#[test]
+fn cancel_buy_now() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        let price = 100;
+
+        // Sell nft
+        assert_ok!(Content::sell_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            video_id,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            price,
+        ));
+
+        // Runtime tested state before call
+
+        // Events number before tested calls
+        let number_of_events_before_call = System::events().len();
+
+        // Cancel buy now
+        assert_ok!(Content::cancel_buy_now(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+        ));
+
+        // Runtime tested state after call
+
+        // Ensure nft status changed to given Auction
+        assert!(matches!(
+            Content::video_by_id(video_id).nft_status,
+            Some(OwnedNFT {
+                transactional_status: TransactionalStatus::Idle,
+                ..
+            })
+        ));
+
+        let buy_now_canceled_event = get_test_event(RawEvent::BuyNowCanceled(
+            video_id,
+            ContentActor::Member(FIRST_MEMBER_ID),
+        ));
+
+        // Last event checked
+        assert_event(buy_now_canceled_event, number_of_events_before_call + 1);
+    })
+}
+
+#[test]
+fn cancel_buy_now_video_does_not_exist() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        // Make an attempt to cancel buy now which corresponding video does not exist yet
+        let cancel_buy_now_result = Content::cancel_buy_now(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+        );
+
+        // Failure checked
+        assert_err!(cancel_buy_now_result, Error::<Test>::VideoDoesNotExist);
+    })
+}
+
+#[test]
+fn cancel_buy_now_not_issued() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Make an attempt to cancel buy now for nft which is not issued yet
+        let cancel_buy_now_result = Content::cancel_buy_now(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+        );
+
+        // Failure checked
+        assert_err!(cancel_buy_now_result, Error::<Test>::NFTDoesNotExist);
+    })
+}
+
+#[test]
+fn cancel_buy_now_auth_failed() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        let price = 100;
+
+        // Sell nft
+        assert_ok!(Content::sell_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            video_id,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            price,
+        ));
+
+        // Make an attempt to cancel buy now with wrong credentials
+        let cancel_buy_now_result = Content::cancel_buy_now(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(UNKNOWN_ID),
+            video_id,
+        );
+
+        // Failure checked
+        assert_err!(cancel_buy_now_result, Error::<Test>::MemberAuthFailed);
+    })
+}
+
+#[test]
+fn cancel_buy_now_not_authorized() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        let price = 100;
+
+        // Sell nft
+        assert_ok!(Content::sell_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            video_id,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            price,
+        ));
+
+        // Make an attempt to cancel buy now if actor is not authorized
+        let cancel_buy_now_result = Content::cancel_buy_now(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            ContentActor::Member(SECOND_MEMBER_ID),
+            video_id,
+        );
+
+        // Failure checked
+        assert_err!(cancel_buy_now_result, Error::<Test>::ActorNotAuthorized);
+    })
+}
+
+#[test]
+fn cancel_buy_now_not_in_auction_state() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        // Make an attempt to cancel buy now if there is no pending one
+        let cancel_buy_now_result = Content::cancel_buy_now(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+        );
+
+        // Failure checked
+        assert_err!(cancel_buy_now_result, Error::<Test>::NFTNotInBuyNowState);
+    })
+}

+ 288 - 0
runtime-modules/content/src/tests/nft/cancel_nft_auction.rs

@@ -0,0 +1,288 @@
+#![cfg(test)]
+
+use crate::tests::mock::*;
+use crate::*;
+use frame_support::{assert_err, assert_ok};
+
+#[test]
+fn cancel_nft_auction() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        // Start nft auction
+        assert_ok!(Content::start_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            get_open_auction_params()
+        ));
+
+        // Runtime tested state before call
+
+        // Events number before tested calls
+        let number_of_events_before_call = System::events().len();
+
+        // Cancel nft auction
+        assert_ok!(Content::cancel_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+        ));
+
+        // Runtime tested state after call
+
+        // Ensure nft status changed to given Auction
+        assert!(matches!(
+            Content::video_by_id(video_id).nft_status,
+            Some(OwnedNFT {
+                transactional_status: TransactionalStatus::Idle,
+                ..
+            })
+        ));
+
+        let nft_auction_canceled_event = get_test_event(RawEvent::AuctionCanceled(
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+        ));
+
+        // Last event checked
+        assert_event(nft_auction_canceled_event, number_of_events_before_call + 1);
+    })
+}
+
+#[test]
+fn cancel_nft_auction_video_does_not_exist() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        // Make an attempt to cancel nft auction which corresponding video does not exist yet
+        let cancel_nft_auction_result = Content::cancel_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+        );
+
+        // Failure checked
+        assert_err!(cancel_nft_auction_result, Error::<Test>::VideoDoesNotExist);
+    })
+}
+
+#[test]
+fn cancel_nft_auction_not_issued() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Make an attempt to cancel nft auction for nft which is not issued yet
+        let cancel_nft_auction_result = Content::cancel_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+        );
+
+        // Failure checked
+        assert_err!(cancel_nft_auction_result, Error::<Test>::NFTDoesNotExist);
+    })
+}
+
+#[test]
+fn cancel_nft_auction_auth_failed() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        // Start nft auction
+        assert_ok!(Content::start_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            get_open_auction_params()
+        ));
+
+        // Make an attempt to cancel nft auction with wrong credentials
+        let cancel_nft_auction_result = Content::cancel_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(UNKNOWN_ID),
+            video_id,
+        );
+
+        // Failure checked
+        assert_err!(cancel_nft_auction_result, Error::<Test>::MemberAuthFailed);
+    })
+}
+
+#[test]
+fn cancel_nft_auction_not_authorized() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        // Start nft auction
+        assert_ok!(Content::start_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            get_open_auction_params()
+        ));
+
+        // Make an attempt to cancel nft auction if actor is not authorized
+        let cancel_nft_auction_result = Content::cancel_nft_auction(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            ContentActor::Member(SECOND_MEMBER_ID),
+            video_id,
+        );
+
+        // Failure checked
+        assert_err!(cancel_nft_auction_result, Error::<Test>::ActorNotAuthorized);
+    })
+}
+
+#[test]
+fn cancel_nft_auction_not_in_auction_state() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        // Make an attempt to cancel nft auction if there is no pending one
+        let cancel_nft_auction_result = Content::cancel_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+        );
+
+        // Failure checked
+        assert_err!(cancel_nft_auction_result, Error::<Test>::NotInAuctionState);
+    })
+}
+
+#[test]
+fn cancel_nft_auction_english_auction_with_bids() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        let auction_params = AuctionParams {
+            starting_price: Content::min_starting_price(),
+            buy_now_price: None,
+            auction_type: AuctionType::English(EnglishAuctionDetails {
+                extension_period: Content::min_auction_extension_period(),
+                auction_duration: Content::max_auction_duration(),
+            }),
+            minimal_bid_step: Content::max_bid_step(),
+            starts_at: None,
+            whitelist: BTreeSet::new(),
+        };
+
+        // Start nft auction
+        assert_ok!(Content::start_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            auction_params
+        ));
+
+        // deposit initial balance
+        let bid = Content::min_starting_price();
+
+        let _ = balances::Module::<Test>::deposit_creating(&SECOND_MEMBER_ORIGIN, bid);
+
+        // Make an english auction bid
+        assert_ok!(Content::make_bid(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            SECOND_MEMBER_ID,
+            video_id,
+            bid,
+        ));
+
+        // Make an attempt to cancel an english auction which already contains a bid
+        let cancel_nft_auction_result = Content::cancel_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+        );
+
+        // Failure checked
+        assert_err!(
+            cancel_nft_auction_result,
+            Error::<Test>::ActionHasBidsAlready
+        );
+    })
+}

+ 223 - 0
runtime-modules/content/src/tests/nft/cancel_offer.rs

@@ -0,0 +1,223 @@
+#![cfg(test)]
+
+use crate::tests::mock::*;
+use crate::*;
+use frame_support::{assert_err, assert_ok};
+
+#[test]
+fn cancel_offer() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        // Offer nft
+        assert_ok!(Content::offer_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            video_id,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            SECOND_MEMBER_ID,
+            None,
+        ));
+
+        // Runtime tested state before call
+
+        // Events number before tested calls
+        let number_of_events_before_call = System::events().len();
+
+        // Cancel offer
+        assert_ok!(Content::cancel_offer(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+        ));
+
+        // Runtime tested state after call
+
+        // Ensure nft status changed to given Auction
+        assert!(matches!(
+            Content::video_by_id(video_id).nft_status,
+            Some(OwnedNFT {
+                transactional_status: TransactionalStatus::Idle,
+                ..
+            })
+        ));
+
+        let buy_now_canceled_event = get_test_event(RawEvent::OfferCanceled(
+            video_id,
+            ContentActor::Member(FIRST_MEMBER_ID),
+        ));
+
+        // Last event checked
+        assert_event(buy_now_canceled_event, number_of_events_before_call + 1);
+    })
+}
+
+#[test]
+fn cancel_offer_video_does_not_exist() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        // Make an attempt to cancel offer which corresponding video does not exist yet
+        let cancel_offer_result = Content::cancel_offer(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+        );
+
+        // Failure checked
+        assert_err!(cancel_offer_result, Error::<Test>::VideoDoesNotExist);
+    })
+}
+
+#[test]
+fn cancel_offer_not_issued() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Make an attempt to cancel offer for nft which is not issued yet
+        let cancel_offer_result = Content::cancel_offer(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+        );
+
+        // Failure checked
+        assert_err!(cancel_offer_result, Error::<Test>::NFTDoesNotExist);
+    })
+}
+
+#[test]
+fn cancel_offer_auth_failed() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        // Offer nft
+        assert_ok!(Content::offer_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            video_id,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            SECOND_MEMBER_ID,
+            None,
+        ));
+
+        // Make an attempt to cancel offer with wrong credentials
+        let cancel_offer_result = Content::cancel_offer(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(UNKNOWN_ID),
+            video_id,
+        );
+
+        // Failure checked
+        assert_err!(cancel_offer_result, Error::<Test>::MemberAuthFailed);
+    })
+}
+
+#[test]
+fn cancel_offer_not_authorized() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        // Offer nft
+        assert_ok!(Content::offer_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            video_id,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            SECOND_MEMBER_ID,
+            None,
+        ));
+
+        // Make an attempt to cancel offer if actor is not authorized
+        let cancel_offer_result = Content::cancel_offer(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            ContentActor::Member(SECOND_MEMBER_ID),
+            video_id,
+        );
+
+        // Failure checked
+        assert_err!(cancel_offer_result, Error::<Test>::ActorNotAuthorized);
+    })
+}
+
+#[test]
+fn cancel_offer_not_in_auction_state() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        // Make an attempt to cancel offer if there is no pending one
+        let cancel_offer_result = Content::cancel_offer(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+        );
+
+        // Failure checked
+        assert_err!(cancel_offer_result, Error::<Test>::PendingOfferDoesNotExist);
+    })
+}

+ 509 - 0
runtime-modules/content/src/tests/nft/cancel_open_auction_bid.rs

@@ -0,0 +1,509 @@
+#![cfg(test)]
+
+use crate::tests::mock::*;
+use crate::*;
+use frame_support::{assert_err, assert_ok};
+
+#[test]
+fn cancel_open_auction_bid() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        let bid_lock_duration = Content::min_bid_lock_duration();
+
+        let auction_params = AuctionParams {
+            starting_price: Content::min_starting_price(),
+            buy_now_price: None,
+            auction_type: AuctionType::Open(OpenAuctionDetails { bid_lock_duration }),
+            minimal_bid_step: Content::min_bid_step(),
+            starts_at: None,
+            whitelist: BTreeSet::new(),
+        };
+
+        // Start nft auction
+        assert_ok!(Content::start_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            auction_params.clone(),
+        ));
+
+        // deposit initial balance
+        let bid = Content::min_starting_price();
+
+        let _ = balances::Module::<Test>::deposit_creating(&SECOND_MEMBER_ORIGIN, bid);
+
+        // Make nft auction bid
+        assert_ok!(Content::make_bid(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            SECOND_MEMBER_ID,
+            video_id,
+            bid,
+        ));
+
+        // Runtime tested state before call
+
+        // Events number before tested calls
+        let number_of_events_before_call = System::events().len();
+
+        // Run to the block where bid lock duration expires
+        run_to_block(bid_lock_duration + 1);
+
+        // Cancel auction bid
+        assert_ok!(Content::cancel_open_auction_bid(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            SECOND_MEMBER_ID,
+            video_id,
+        ));
+
+        // Runtime tested state after call
+
+        // Ensure bid on specific auction successfully canceled
+        assert!(matches!(
+            Content::video_by_id(video_id).nft_status,
+            Some(OwnedNFT {
+                transactional_status: TransactionalStatus::Auction(auction_without_bid,),
+                ..
+            }) if auction_without_bid.last_bid.is_none()
+        ));
+
+        let cancel_open_auction_bid_event =
+            get_test_event(RawEvent::AuctionBidCanceled(SECOND_MEMBER_ID, video_id));
+
+        // Last event checked
+        assert_event(
+            cancel_open_auction_bid_event,
+            number_of_events_before_call + 1,
+        );
+    })
+}
+
+#[test]
+fn cancel_open_auction_bid_lock_duration_did_not_expire() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        let bid_lock_duration = Content::min_bid_lock_duration();
+
+        let auction_params = AuctionParams {
+            starting_price: Content::min_starting_price(),
+            buy_now_price: None,
+            auction_type: AuctionType::Open(OpenAuctionDetails { bid_lock_duration }),
+            minimal_bid_step: Content::min_bid_step(),
+            starts_at: None,
+            whitelist: BTreeSet::new(),
+        };
+
+        // Start nft auction
+        assert_ok!(Content::start_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            auction_params.clone(),
+        ));
+
+        // deposit initial balance
+        let bid = Content::min_starting_price();
+
+        let _ = balances::Module::<Test>::deposit_creating(&SECOND_MEMBER_ORIGIN, bid);
+
+        // Make nft auction bid
+        assert_ok!(Content::make_bid(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            SECOND_MEMBER_ID,
+            video_id,
+            bid,
+        ));
+
+        // Make an attempt to cancel open auction bid if lock duration did not expire
+        let cancel_open_auction_bid_result = Content::cancel_open_auction_bid(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            SECOND_MEMBER_ID,
+            video_id,
+        );
+
+        // Failure checked
+        assert_err!(
+            cancel_open_auction_bid_result,
+            Error::<Test>::BidLockDurationIsNotExpired
+        );
+    })
+}
+
+#[test]
+fn cancel_open_auction_bid_auth_failed() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        let bid_lock_duration = Content::min_bid_lock_duration();
+
+        let auction_params = AuctionParams {
+            starting_price: Content::min_starting_price(),
+            buy_now_price: None,
+            auction_type: AuctionType::Open(OpenAuctionDetails { bid_lock_duration }),
+            minimal_bid_step: Content::min_bid_step(),
+            starts_at: None,
+            whitelist: BTreeSet::new(),
+        };
+
+        // Start nft auction
+        assert_ok!(Content::start_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            auction_params.clone(),
+        ));
+
+        // deposit initial balance
+        let bid = Content::min_starting_price();
+
+        let _ = balances::Module::<Test>::deposit_creating(&SECOND_MEMBER_ORIGIN, bid);
+
+        // Make nft auction bid
+        assert_ok!(Content::make_bid(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            SECOND_MEMBER_ID,
+            video_id,
+            bid,
+        ));
+
+        // Run to the block where bid lock duration expires
+        run_to_block(bid_lock_duration + 1);
+
+        // Make an attempt to cancel open auction bid with wrong credentials
+        let cancel_open_auction_bid_result = Content::cancel_open_auction_bid(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            UNKNOWN_ID,
+            video_id,
+        );
+
+        // Failure checked
+        assert_err!(
+            cancel_open_auction_bid_result,
+            Error::<Test>::MemberAuthFailed
+        );
+    })
+}
+
+#[test]
+fn cancel_open_auction_bid_video_does_not_exist() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        // Make an attempt to cancel open auction bid which corresponding video does not exist
+        let cancel_open_auction_bid_result = Content::cancel_open_auction_bid(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            SECOND_MEMBER_ID,
+            video_id,
+        );
+
+        // Failure checked
+        assert_err!(
+            cancel_open_auction_bid_result,
+            Error::<Test>::VideoDoesNotExist
+        );
+    })
+}
+
+#[test]
+fn cancel_open_auction_bid_nft_is_not_issued() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Make an attempt to cancel open auction bid for nft which is not issued yet
+        let cancel_open_auction_bid_result = Content::cancel_open_auction_bid(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            SECOND_MEMBER_ID,
+            video_id,
+        );
+
+        // Failure checked
+        assert_err!(
+            cancel_open_auction_bid_result,
+            Error::<Test>::NFTDoesNotExist
+        );
+    })
+}
+
+#[test]
+fn cancel_open_auction_bid_not_in_auction_state() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        // Make an attempt to cancel open auction bid for nft which is not in auction state
+        let cancel_open_auction_bid_result = Content::cancel_open_auction_bid(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            SECOND_MEMBER_ID,
+            video_id,
+        );
+
+        // Failure checked
+        assert_err!(
+            cancel_open_auction_bid_result,
+            Error::<Test>::NotInAuctionState
+        );
+    })
+}
+
+#[test]
+fn cancel_open_auction_bid_is_not_open_auction_type() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        let auction_params = AuctionParams {
+            starting_price: Content::min_starting_price(),
+            buy_now_price: None,
+            auction_type: AuctionType::English(EnglishAuctionDetails {
+                extension_period: Content::min_auction_extension_period(),
+                auction_duration: Content::max_auction_duration(),
+            }),
+            minimal_bid_step: Content::max_bid_step(),
+            starts_at: None,
+            whitelist: BTreeSet::new(),
+        };
+
+        // Start nft auction
+        assert_ok!(Content::start_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            auction_params.clone(),
+        ));
+
+        // deposit initial balance
+        let bid = Content::min_starting_price();
+
+        let _ = balances::Module::<Test>::deposit_creating(&SECOND_MEMBER_ORIGIN, bid);
+
+        // Make nft auction bid
+        assert_ok!(Content::make_bid(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            SECOND_MEMBER_ID,
+            video_id,
+            bid,
+        ));
+
+        // Make an attempt to cancel open auction bid for nft which is not in open auction state
+        let cancel_open_auction_bid_result = Content::cancel_open_auction_bid(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            SECOND_MEMBER_ID,
+            video_id,
+        );
+
+        // Failure checked
+        assert_err!(
+            cancel_open_auction_bid_result,
+            Error::<Test>::IsNotOpenAuctionType
+        );
+    })
+}
+
+#[test]
+fn cancel_open_auction_bid_last_bid_does_not_exist() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        let bid_lock_duration = Content::min_bid_lock_duration();
+
+        let auction_params = AuctionParams {
+            starting_price: Content::min_starting_price(),
+            buy_now_price: None,
+            auction_type: AuctionType::Open(OpenAuctionDetails { bid_lock_duration }),
+            minimal_bid_step: Content::min_bid_step(),
+            starts_at: None,
+            whitelist: BTreeSet::new(),
+        };
+
+        // Start nft auction
+        assert_ok!(Content::start_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            auction_params.clone(),
+        ));
+
+        // Run to the block where bid lock duration expires
+        run_to_block(bid_lock_duration + 1);
+
+        // Make an attempt to cancel open auction bid if it does not exist
+        let cancel_open_auction_bid_result = Content::cancel_open_auction_bid(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            SECOND_MEMBER_ID,
+            video_id,
+        );
+
+        // Failure checked
+        assert_err!(
+            cancel_open_auction_bid_result,
+            Error::<Test>::LastBidDoesNotExist
+        );
+    })
+}
+
+#[test]
+fn cancel_open_auction_bid_actor_is_not_a_last_bidder() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        let bid_lock_duration = Content::min_bid_lock_duration();
+
+        let auction_params = AuctionParams {
+            starting_price: Content::min_starting_price(),
+            buy_now_price: None,
+            auction_type: AuctionType::Open(OpenAuctionDetails { bid_lock_duration }),
+            minimal_bid_step: Content::min_bid_step(),
+            starts_at: None,
+            whitelist: BTreeSet::new(),
+        };
+
+        // Start nft auction
+        assert_ok!(Content::start_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            auction_params.clone(),
+        ));
+
+        // deposit initial balance
+        let bid = Content::min_starting_price();
+
+        let _ = balances::Module::<Test>::deposit_creating(&SECOND_MEMBER_ORIGIN, bid);
+
+        // Make nft auction bid
+        assert_ok!(Content::make_bid(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            SECOND_MEMBER_ID,
+            video_id,
+            bid,
+        ));
+
+        // Run to the block where bid lock duration expires
+        run_to_block(bid_lock_duration + 1);
+
+        // Make an attempt to cancel open auction bid if actor is not a last bidder
+        let cancel_open_auction_bid_result = Content::cancel_open_auction_bid(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            THIRD_MEMBER_ID,
+            video_id,
+        );
+
+        // Failure checked
+        assert_err!(
+            cancel_open_auction_bid_result,
+            Error::<Test>::ActorIsNotALastBidder
+        );
+    })
+}

+ 444 - 0
runtime-modules/content/src/tests/nft/claim_won_english_auction.rs

@@ -0,0 +1,444 @@
+#![cfg(test)]
+
+use crate::tests::mock::*;
+use crate::*;
+use frame_support::{assert_err, assert_ok};
+
+#[test]
+fn claim_won_english_auction() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        let auction_params = AuctionParams {
+            starting_price: Content::min_starting_price(),
+            buy_now_price: None,
+            auction_type: AuctionType::English(EnglishAuctionDetails {
+                extension_period: Content::min_auction_extension_period(),
+                auction_duration: Content::max_auction_duration(),
+            }),
+            minimal_bid_step: Content::max_bid_step(),
+            starts_at: None,
+            whitelist: BTreeSet::new(),
+        };
+
+        // Start nft auction
+        assert_ok!(Content::start_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            auction_params.clone(),
+        ));
+
+        // deposit initial balance
+        let bid = Content::min_starting_price();
+
+        let _ = balances::Module::<Test>::deposit_creating(&SECOND_MEMBER_ORIGIN, bid);
+
+        // Make nft auction bid
+        assert_ok!(Content::make_bid(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            SECOND_MEMBER_ID,
+            video_id,
+            bid,
+        ));
+
+        // Runtime tested state before call
+
+        // Events number before tested calls
+        let number_of_events_before_call = System::events().len();
+
+        // Run to the block where auction expires
+        run_to_block(Content::max_auction_duration() + 1);
+
+        // Claim won english auction
+        assert_ok!(Content::claim_won_english_auction(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            SECOND_MEMBER_ID,
+            video_id,
+        ));
+
+        // Runtime tested state after call
+
+        // Ensure english auction successfully completed
+        assert!(matches!(
+            Content::video_by_id(video_id).nft_status,
+            Some(OwnedNFT {
+                transactional_status: TransactionalStatus::Idle,
+                ..
+            })
+        ));
+
+        let claim_won_english_auction_event = get_test_event(RawEvent::EnglishAuctionCompleted(
+            SECOND_MEMBER_ID,
+            video_id,
+        ));
+
+        // Last event checked
+        assert_event(
+            claim_won_english_auction_event,
+            number_of_events_before_call + 3,
+        );
+    })
+}
+
+#[test]
+fn claim_won_english_auction_cannot_be_completed() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        let auction_params = AuctionParams {
+            starting_price: Content::min_starting_price(),
+            buy_now_price: None,
+            auction_type: AuctionType::English(EnglishAuctionDetails {
+                extension_period: Content::min_auction_extension_period(),
+                auction_duration: Content::max_auction_duration(),
+            }),
+            minimal_bid_step: Content::max_bid_step(),
+            starts_at: None,
+            whitelist: BTreeSet::new(),
+        };
+
+        // Start nft auction
+        assert_ok!(Content::start_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            auction_params.clone(),
+        ));
+
+        // deposit initial balance
+        let bid = Content::min_starting_price();
+
+        let _ = balances::Module::<Test>::deposit_creating(&SECOND_MEMBER_ORIGIN, bid);
+
+        // Make nft auction bid
+        assert_ok!(Content::make_bid(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            SECOND_MEMBER_ID,
+            video_id,
+            bid,
+        ));
+
+        // Make an attempt to claim won english auction if it did not expire yet
+        let claim_won_english_auction_result = Content::claim_won_english_auction(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            SECOND_MEMBER_ID,
+            video_id,
+        );
+
+        // Failure checked
+        assert_err!(
+            claim_won_english_auction_result,
+            Error::<Test>::AuctionCannotBeCompleted
+        );
+    })
+}
+
+#[test]
+fn claim_won_english_auction_auth_failed() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        let auction_params = AuctionParams {
+            starting_price: Content::min_starting_price(),
+            buy_now_price: None,
+            auction_type: AuctionType::English(EnglishAuctionDetails {
+                extension_period: Content::min_auction_extension_period(),
+                auction_duration: Content::max_auction_duration(),
+            }),
+            minimal_bid_step: Content::max_bid_step(),
+            starts_at: None,
+            whitelist: BTreeSet::new(),
+        };
+
+        // Start nft auction
+        assert_ok!(Content::start_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            auction_params.clone(),
+        ));
+
+        // deposit initial balance
+        let bid = Content::min_starting_price();
+
+        let _ = balances::Module::<Test>::deposit_creating(&SECOND_MEMBER_ORIGIN, bid);
+
+        // Make nft auction bid
+        assert_ok!(Content::make_bid(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            SECOND_MEMBER_ID,
+            video_id,
+            bid,
+        ));
+
+        // Run to the block where auction expires
+        run_to_block(Content::max_auction_duration() + 1);
+
+        // Make an attempt to claim won english auction with wrong credentials
+        let claim_won_english_auction_result = Content::claim_won_english_auction(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            UNKNOWN_ID,
+            video_id,
+        );
+
+        // Failure checked
+        assert_err!(
+            claim_won_english_auction_result,
+            Error::<Test>::MemberAuthFailed
+        );
+    })
+}
+
+#[test]
+fn claim_won_english_auction_video_does_not_exist() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        // Make an attempt to claim won english auction which corresponding video does not exist
+        let claim_won_english_auction_result = Content::claim_won_english_auction(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            SECOND_MEMBER_ID,
+            video_id,
+        );
+
+        // Failure checked
+        assert_err!(
+            claim_won_english_auction_result,
+            Error::<Test>::VideoDoesNotExist
+        );
+    })
+}
+
+#[test]
+fn claim_won_english_auction_nft_is_not_issued() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Make an attempt to claim won english auction for nft which is not issued yet
+        let claim_won_english_auction_result = Content::claim_won_english_auction(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            SECOND_MEMBER_ID,
+            video_id,
+        );
+
+        // Failure checked
+        assert_err!(
+            claim_won_english_auction_result,
+            Error::<Test>::NFTDoesNotExist
+        );
+    })
+}
+
+#[test]
+fn claim_won_english_auction_not_in_auction_state() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        // Make an attempt to claim won english auction for nft which is not in auction state
+        let claim_won_english_auction_result = Content::claim_won_english_auction(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            SECOND_MEMBER_ID,
+            video_id,
+        );
+
+        // Failure checked
+        assert_err!(
+            claim_won_english_auction_result,
+            Error::<Test>::NotInAuctionState
+        );
+    })
+}
+
+#[test]
+fn claim_won_english_auction_is_not_english_auction_type() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        let bid_lock_duration = Content::min_bid_lock_duration();
+
+        let auction_params = AuctionParams {
+            starting_price: Content::min_starting_price(),
+            buy_now_price: None,
+            auction_type: AuctionType::Open(OpenAuctionDetails { bid_lock_duration }),
+            minimal_bid_step: Content::min_bid_step(),
+            starts_at: None,
+            whitelist: BTreeSet::new(),
+        };
+
+        // Start nft auction
+        assert_ok!(Content::start_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            auction_params.clone(),
+        ));
+
+        // deposit initial balance
+        let bid = Content::min_starting_price();
+
+        let _ = balances::Module::<Test>::deposit_creating(&SECOND_MEMBER_ORIGIN, bid);
+
+        // Make nft auction bid
+        assert_ok!(Content::make_bid(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            SECOND_MEMBER_ID,
+            video_id,
+            bid,
+        ));
+
+        // Make an attempt to claim won english auction for nft which is not in english auction state
+        let claim_won_english_auction_result = Content::claim_won_english_auction(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            SECOND_MEMBER_ID,
+            video_id,
+        );
+
+        // Failure checked
+        assert_err!(
+            claim_won_english_auction_result,
+            Error::<Test>::IsNotEnglishAuctionType
+        );
+    })
+}
+
+#[test]
+fn claim_won_english_auction_last_bid_does_not_exist() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        let auction_params = AuctionParams {
+            starting_price: Content::min_starting_price(),
+            buy_now_price: None,
+            auction_type: AuctionType::English(EnglishAuctionDetails {
+                extension_period: Content::min_auction_extension_period(),
+                auction_duration: Content::max_auction_duration(),
+            }),
+            minimal_bid_step: Content::max_bid_step(),
+            starts_at: None,
+            whitelist: BTreeSet::new(),
+        };
+
+        // Start nft auction
+        assert_ok!(Content::start_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            auction_params.clone(),
+        ));
+
+        // Run to the block where auction expires
+        run_to_block(Content::max_auction_duration() + 1);
+
+        // Make an attempt to claim won english auction if last bid does not exist
+        let claim_won_english_auction_result = Content::claim_won_english_auction(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            SECOND_MEMBER_ID,
+            video_id,
+        );
+
+        // Failure checked
+        assert_err!(
+            claim_won_english_auction_result,
+            Error::<Test>::LastBidDoesNotExist
+        );
+    })
+}

+ 198 - 0
runtime-modules/content/src/tests/nft/issue_nft.rs

@@ -0,0 +1,198 @@
+#![cfg(test)]
+
+use crate::tests::mock::*;
+use crate::*;
+use frame_support::{assert_err, assert_ok};
+
+#[test]
+fn issue_nft() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Video does not have an nft
+        assert_eq!(None, Content::video_by_id(video_id).nft_status);
+
+        // Runtime tested state before call
+
+        // Events number before tested calls
+        let number_of_events_before_call = System::events().len();
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        // Runtime tested state after call
+
+        // Ensure nft created succesfully
+        let nft_status = Some(OwnedNFT::new(NFTOwner::ChannelOwner, None));
+        assert_eq!(nft_status, Content::video_by_id(video_id).nft_status);
+
+        let nft_issued_event = get_test_event(RawEvent::NftIssued(
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None,
+        ));
+
+        // Last event checked
+        assert_event(nft_issued_event, number_of_events_before_call + 1);
+    })
+}
+
+#[test]
+fn issue_nft_video_does_not_exist() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        // Make an attempt to issue nft for non existent video
+        let issue_nft_result = Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None,
+        );
+
+        // Failure checked
+        assert_err!(issue_nft_result, Error::<Test>::VideoDoesNotExist);
+    })
+}
+
+#[test]
+fn issue_nft_already_issued() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        // Make an attempt to issue nft once again for the same video
+        let issue_nft_result = Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None,
+        );
+
+        // Failure checked
+        assert_err!(issue_nft_result, Error::<Test>::NFTAlreadyExists);
+    })
+}
+
+#[test]
+fn issue_nft_auth_failed() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Make an attempt to issue nft with wrong credentials
+        let issue_nft_result = Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(UNKNOWN_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None,
+        );
+
+        // Failure checked
+        assert_err!(issue_nft_result, Error::<Test>::MemberAuthFailed);
+    })
+}
+
+#[test]
+fn issue_nft_actor_not_authorized() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Make an attempt to issue nft if actor is not authorized
+        let issue_nft_result = Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(SECOND_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None,
+        );
+
+        // Failure checked
+        assert_err!(issue_nft_result, Error::<Test>::ActorNotAuthorized);
+    })
+}
+
+#[test]
+fn issue_nft_royalty_bounds_violated() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Make an attempt to issue nft with wrong credentials
+        let issue_nft_result = Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            Some(Perbill::one()),
+            b"metablob".to_vec(),
+            None,
+        );
+
+        // Failure checked
+        assert_err!(issue_nft_result, Error::<Test>::RoyaltyUpperBoundExceeded);
+
+        // Make an attempt to issue nft with wrong credentials
+        let issue_nft_result = Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            Some(Perbill::from_perthousand(1)),
+            b"metablob".to_vec(),
+            None,
+        );
+
+        // Failure checked
+        assert_err!(issue_nft_result, Error::<Test>::RoyaltyLowerBoundExceeded);
+    })
+}

+ 660 - 0
runtime-modules/content/src/tests/nft/make_bid.rs

@@ -0,0 +1,660 @@
+#![cfg(test)]
+
+use crate::tests::mock::*;
+use crate::*;
+use frame_support::{assert_err, assert_ok};
+use std::iter::FromIterator;
+
+#[test]
+fn make_bid() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        let auction_params = get_open_auction_params();
+
+        // Start nft auction
+        assert_ok!(Content::start_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            auction_params.clone(),
+        ));
+
+        // Runtime tested state before call
+
+        // Events number before tested calls
+        let number_of_events_before_call = System::events().len();
+
+        // deposit initial balance
+        let bid = Content::min_starting_price();
+
+        let _ = balances::Module::<Test>::deposit_creating(&SECOND_MEMBER_ORIGIN, bid);
+
+        // Make nft auction bid
+        assert_ok!(Content::make_bid(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            SECOND_MEMBER_ID,
+            video_id,
+            bid,
+        ));
+
+        // Runtime tested state after call
+
+        let mut auction: Auction<Test> = AuctionRecord::new(auction_params.clone());
+        let current_block = <frame_system::Module<Test>>::block_number();
+
+        if auction_params.starts_at.is_none() {
+            auction.starts_at = current_block;
+        }
+
+        let (auction, _, _) =
+            auction.make_bid(SECOND_MEMBER_ID, SECOND_MEMBER_ORIGIN, bid, current_block);
+
+        // Ensure nft status changed to given Auction
+        assert!(matches!(
+            Content::video_by_id(video_id).nft_status,
+            Some(OwnedNFT {
+                transactional_status: TransactionalStatus::Auction(auction_with_bid,),
+                ..
+            }) if auction == auction_with_bid
+        ));
+
+        let auction_bid_made_event = get_test_event(RawEvent::AuctionBidMade(
+            SECOND_MEMBER_ID,
+            video_id,
+            bid,
+            false,
+        ));
+
+        // Last event checked
+        assert_event(auction_bid_made_event, number_of_events_before_call + 4);
+    })
+}
+
+#[test]
+fn make_bid_completes_auction() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        let buy_now_price = Content::min_starting_price();
+
+        let auction_params = AuctionParams {
+            starting_price: buy_now_price,
+            buy_now_price: Some(2 * buy_now_price),
+            auction_type: AuctionType::Open(OpenAuctionDetails {
+                bid_lock_duration: Content::min_bid_lock_duration(),
+            }),
+            minimal_bid_step: Content::max_bid_step(),
+            starts_at: None,
+            whitelist: BTreeSet::new(),
+        };
+
+        // Start nft auction
+        assert_ok!(Content::start_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            auction_params.clone(),
+        ));
+
+        // Runtime tested state before call
+
+        // Events number before tested calls
+        let number_of_events_before_call = System::events().len();
+
+        // deposit initial balance
+        let bid = 2 * Content::min_starting_price();
+
+        let _ = balances::Module::<Test>::deposit_creating(&SECOND_MEMBER_ORIGIN, bid);
+
+        // Make nft auction bid
+        assert_ok!(Content::make_bid(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            SECOND_MEMBER_ID,
+            video_id,
+            bid,
+        ));
+
+        // Runtime tested state after call
+
+        // Ensure nft status changed to given Auction
+        assert!(matches!(
+            Content::video_by_id(video_id).nft_status,
+            Some(OwnedNFT {
+                transactional_status: TransactionalStatus::Idle,
+                owner,
+                ..
+            }) if owner == NFTOwner::Member(SECOND_MEMBER_ID)
+        ));
+
+        let nft_auction_started_event = get_test_event(RawEvent::BidMadeCompletingAuction(
+            SECOND_MEMBER_ID,
+            video_id,
+        ));
+
+        // Last event checked
+        assert_event(nft_auction_started_event, number_of_events_before_call + 5);
+    })
+}
+
+#[test]
+fn make_bid_auth_failed() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        let auction_params = get_open_auction_params();
+
+        // Start nft auction
+        assert_ok!(Content::start_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            auction_params.clone(),
+        ));
+
+        // deposit initial balance
+        let bid = Content::min_starting_price();
+
+        let _ = balances::Module::<Test>::deposit_creating(&SECOND_MEMBER_ORIGIN, bid);
+
+        // Make an attempt to make auction bid providing wrong credentials
+        let make_bid_result = Content::make_bid(
+            Origin::signed(UNKNOWN_ORIGIN),
+            SECOND_MEMBER_ID,
+            video_id,
+            bid,
+        );
+
+        // Failure checked
+        assert_err!(make_bid_result, Error::<Test>::MemberAuthFailed);
+    })
+}
+
+#[test]
+fn make_bid_insufficient_balance() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        let auction_params = get_open_auction_params();
+
+        // Start nft auction
+        assert_ok!(Content::start_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            auction_params.clone(),
+        ));
+
+        let bid = Content::min_starting_price();
+
+        // Make an attempt to make auction bid if account has insufficient balance
+        let make_bid_result = Content::make_bid(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            SECOND_MEMBER_ID,
+            video_id,
+            bid,
+        );
+
+        // Failure checked
+        assert_err!(make_bid_result, Error::<Test>::InsufficientBalance);
+    })
+}
+
+#[test]
+fn make_bid_video_does_not_exist() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        // deposit initial balance
+        let bid = Content::min_starting_price();
+
+        let _ = balances::Module::<Test>::deposit_creating(&SECOND_MEMBER_ORIGIN, bid);
+
+        // Make an attempt to make auction bid if corresponding video does not exist
+        let make_bid_result = Content::make_bid(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            SECOND_MEMBER_ID,
+            video_id,
+            bid,
+        );
+
+        // Failure checked
+        assert_err!(make_bid_result, Error::<Test>::VideoDoesNotExist);
+    })
+}
+
+#[test]
+fn make_bid_nft_is_not_issued() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // deposit initial balance
+        let bid = Content::min_starting_price();
+
+        let _ = balances::Module::<Test>::deposit_creating(&SECOND_MEMBER_ORIGIN, bid);
+
+        // Make an attempt to make auction bid if corresponding nft is not issued yet
+        let make_bid_result = Content::make_bid(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            SECOND_MEMBER_ID,
+            video_id,
+            bid,
+        );
+
+        // Failure checked
+        assert_err!(make_bid_result, Error::<Test>::NFTDoesNotExist);
+    })
+}
+
+#[test]
+fn make_bid_nft_is_not_in_auction_state() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        // deposit initial balance
+        let bid = Content::min_starting_price();
+
+        let _ = balances::Module::<Test>::deposit_creating(&SECOND_MEMBER_ORIGIN, bid);
+
+        // Make an attempt to make auction bid if corresponding nft is not in auction state
+        let make_bid_result = Content::make_bid(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            SECOND_MEMBER_ID,
+            video_id,
+            bid,
+        );
+
+        // Failure checked
+        assert_err!(make_bid_result, Error::<Test>::NotInAuctionState);
+    })
+}
+
+#[test]
+fn make_bid_nft_auction_expired() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        let auction_params = AuctionParams {
+            starting_price: Content::min_starting_price(),
+            buy_now_price: None,
+            auction_type: AuctionType::English(EnglishAuctionDetails {
+                extension_period: Content::min_auction_extension_period(),
+                auction_duration: Content::min_auction_duration(),
+            }),
+            minimal_bid_step: Content::max_bid_step(),
+            starts_at: None,
+            whitelist: BTreeSet::new(),
+        };
+
+        // Start nft auction
+        assert_ok!(Content::start_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            auction_params.clone(),
+        ));
+
+        // Run to the block when auction expires
+        run_to_block(Content::min_auction_duration() + 1);
+
+        // deposit initial balance
+        let bid = Content::min_starting_price();
+
+        let _ = balances::Module::<Test>::deposit_creating(&SECOND_MEMBER_ORIGIN, 2 * bid);
+
+        // Make an attempt to make auction bid if corresponding english nft auction is already expired
+        let make_bid_result = Content::make_bid(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            SECOND_MEMBER_ID,
+            video_id,
+            bid,
+        );
+
+        // Failure checked
+        assert_err!(make_bid_result, Error::<Test>::NFTAuctionIsAlreadyExpired);
+    })
+}
+
+#[test]
+fn make_bid_nft_auction_is_not_started() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        let starting_price = Content::min_starting_price();
+
+        let auction_params = AuctionParams {
+            starting_price,
+            buy_now_price: None,
+            auction_type: AuctionType::Open(OpenAuctionDetails {
+                bid_lock_duration: Content::min_bid_lock_duration(),
+            }),
+            minimal_bid_step: Content::min_bid_step(),
+            starts_at: Some(<frame_system::Module<Test>>::block_number() + 1),
+            whitelist: BTreeSet::new(),
+        };
+
+        // Start nft auction
+        assert_ok!(Content::start_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            auction_params.clone(),
+        ));
+
+        let _ = balances::Module::<Test>::deposit_creating(&SECOND_MEMBER_ORIGIN, starting_price);
+
+        // Make an attempt to make auction bid if auction is not started
+        let make_bid_result = Content::make_bid(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            SECOND_MEMBER_ID,
+            video_id,
+            starting_price,
+        );
+
+        // Failure checked
+        assert_err!(make_bid_result, Error::<Test>::AuctionDidNotStart);
+    })
+}
+
+#[test]
+fn make_bid_member_is_not_allowed_to_participate() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        let auction_params = AuctionParams {
+            starting_price: Content::max_starting_price(),
+            buy_now_price: None,
+            auction_type: AuctionType::Open(OpenAuctionDetails {
+                bid_lock_duration: Content::min_bid_lock_duration(),
+            }),
+            minimal_bid_step: Content::min_bid_step(),
+            starts_at: Some(<frame_system::Module<Test>>::block_number() + 1),
+            whitelist: BTreeSet::from_iter(vec![THIRD_MEMBER_ID, FOURTH_MEMBER_ID].into_iter()),
+        };
+
+        // Start nft auction
+        assert_ok!(Content::start_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            auction_params.clone(),
+        ));
+
+        // Run to the block when auction expires
+        run_to_block(Content::min_auction_duration() + 1);
+
+        // deposit initial balance
+        let bid = Content::min_starting_price();
+
+        let _ = balances::Module::<Test>::deposit_creating(&SECOND_MEMBER_ORIGIN, 2 * bid);
+
+        // Make an attempt to make auction bid on auction with whitelist if member is not whitelisted
+        let make_bid_result = Content::make_bid(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            SECOND_MEMBER_ID,
+            video_id,
+            bid,
+        );
+
+        // Failure checked
+        assert_err!(
+            make_bid_result,
+            Error::<Test>::MemberIsNotAllowedToParticipate
+        );
+    })
+}
+
+#[test]
+fn make_bid_starting_price_constraint_violated() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        let auction_params = AuctionParams {
+            starting_price: Content::max_starting_price(),
+            buy_now_price: None,
+            auction_type: AuctionType::Open(OpenAuctionDetails {
+                bid_lock_duration: Content::min_bid_lock_duration(),
+            }),
+            minimal_bid_step: Content::min_bid_step(),
+            starts_at: None,
+            whitelist: BTreeSet::new(),
+        };
+
+        // Start nft auction
+        assert_ok!(Content::start_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            auction_params.clone(),
+        ));
+
+        let bid = Content::min_starting_price();
+
+        let _ = balances::Module::<Test>::deposit_creating(&SECOND_MEMBER_ORIGIN, bid);
+
+        // Make an attempt to make auction bid if bid amount provided is less then auction starting price
+        let make_bid_result = Content::make_bid(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            SECOND_MEMBER_ID,
+            video_id,
+            bid,
+        );
+
+        // Failure checked
+        assert_err!(
+            make_bid_result,
+            Error::<Test>::StartingPriceConstraintViolated
+        );
+    })
+}
+
+#[test]
+fn make_bid_bid_step_constraint_violated() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        let auction_params = AuctionParams {
+            starting_price: Content::min_starting_price(),
+            buy_now_price: None,
+            auction_type: AuctionType::Open(OpenAuctionDetails {
+                bid_lock_duration: Content::min_bid_lock_duration(),
+            }),
+            minimal_bid_step: Content::min_bid_step(),
+            starts_at: None,
+            whitelist: BTreeSet::new(),
+        };
+
+        // Start nft auction
+        assert_ok!(Content::start_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            auction_params.clone(),
+        ));
+
+        let bid = Content::min_starting_price();
+
+        let _ = balances::Module::<Test>::deposit_creating(&SECOND_MEMBER_ORIGIN, bid);
+
+        // Make a successfull bid
+        assert_ok!(Content::make_bid(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            SECOND_MEMBER_ID,
+            video_id,
+            bid,
+        ));
+
+        let new_bid = bid + Content::min_bid_step() - 1;
+
+        let _ = balances::Module::<Test>::deposit_creating(&SECOND_MEMBER_ORIGIN, new_bid);
+
+        // Make an attempt to make auction bid if bid step constraint violated
+        let make_bid_result = Content::make_bid(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            SECOND_MEMBER_ID,
+            video_id,
+            new_bid,
+        );
+
+        // Failure checked
+        assert_err!(make_bid_result, Error::<Test>::BidStepConstraintViolated);
+    })
+}

+ 222 - 0
runtime-modules/content/src/tests/nft/offer_nft.rs

@@ -0,0 +1,222 @@
+#![cfg(test)]
+
+use crate::tests::mock::*;
+use crate::*;
+use frame_support::{assert_err, assert_ok};
+
+#[test]
+fn offer_nft() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        // Runtime tested state before call
+
+        // Events number before tested calls
+        let number_of_events_before_call = System::events().len();
+
+        // Offer nft
+        assert_ok!(Content::offer_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            video_id,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            SECOND_MEMBER_ID,
+            None,
+        ));
+
+        // Runtime tested state after call
+
+        // Ensure nft offered succesfully
+        assert!(matches!(
+            Content::video_by_id(video_id).nft_status,
+            Some(OwnedNFT {
+                transactional_status: TransactionalStatus::InitiatedOfferToMember(
+                    SECOND_MEMBER_ID,
+                    None
+                ),
+                ..
+            })
+        ));
+
+        let offer_started_event = get_test_event(RawEvent::OfferStarted(
+            video_id,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            SECOND_MEMBER_ID,
+            None,
+        ));
+
+        // Last event checked
+        assert_event(offer_started_event, number_of_events_before_call + 1);
+    })
+}
+
+#[test]
+fn offer_nft_video_does_not_exist() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        // Make an attempt to offer nft which corresponding video does not exist
+        let offer_nft_result = Content::offer_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            video_id,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            SECOND_MEMBER_ID,
+            None,
+        );
+
+        // Failure checked
+        assert_err!(offer_nft_result, Error::<Test>::VideoDoesNotExist);
+    })
+}
+
+#[test]
+fn offer_nft_not_issued() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Make an attempt to offer nft which is not issued yet
+        let offer_nft_result = Content::offer_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            video_id,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            SECOND_MEMBER_ID,
+            None,
+        );
+
+        // Failure checked
+        assert_err!(offer_nft_result, Error::<Test>::NFTDoesNotExist);
+    })
+}
+
+#[test]
+fn offer_nft_auth_failed() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        // Make an attempt to offer nft with wrong credentials
+        let offer_nft_result = Content::offer_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            video_id,
+            ContentActor::Member(UNKNOWN_ID),
+            SECOND_MEMBER_ID,
+            None,
+        );
+
+        // Failure checked
+        assert_err!(offer_nft_result, Error::<Test>::MemberAuthFailed);
+    })
+}
+
+#[test]
+fn offer_nft_not_authorized() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        // Make an attempt to offer nft if actor is not authorized
+        let offer_nft_result = Content::offer_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            video_id,
+            ContentActor::Member(SECOND_MEMBER_ID),
+            SECOND_MEMBER_ID,
+            None,
+        );
+
+        // Failure checked
+        assert_err!(offer_nft_result, Error::<Test>::ActorNotAuthorized);
+    })
+}
+
+#[test]
+fn offer_nft_transactional_status_is_not_idle() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        // Offer nft
+        assert_ok!(Content::offer_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            video_id,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            SECOND_MEMBER_ID,
+            None,
+        ));
+
+        // Make an attempt to offer nft when it is already offered
+        let offer_nft_result = Content::offer_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            video_id,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            SECOND_MEMBER_ID,
+            None,
+        );
+
+        // Failure checked
+        assert_err!(offer_nft_result, Error::<Test>::NftIsNotIdle);
+    })
+}

+ 441 - 0
runtime-modules/content/src/tests/nft/pick_open_auction_winner.rs

@@ -0,0 +1,441 @@
+#![cfg(test)]
+
+use crate::tests::mock::*;
+use crate::*;
+use frame_support::{assert_err, assert_ok};
+
+#[test]
+fn pick_open_auction_winner() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        let bid_lock_duration = Content::min_bid_lock_duration();
+
+        let auction_params = AuctionParams {
+            starting_price: Content::min_starting_price(),
+            buy_now_price: None,
+            auction_type: AuctionType::Open(OpenAuctionDetails { bid_lock_duration }),
+            minimal_bid_step: Content::min_bid_step(),
+            starts_at: None,
+            whitelist: BTreeSet::new(),
+        };
+
+        // Start nft auction
+        assert_ok!(Content::start_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            auction_params.clone(),
+        ));
+
+        // deposit initial balance
+        let bid = Content::min_starting_price();
+
+        let _ = balances::Module::<Test>::deposit_creating(&SECOND_MEMBER_ORIGIN, bid);
+
+        // Make nft auction bid
+        assert_ok!(Content::make_bid(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            SECOND_MEMBER_ID,
+            video_id,
+            bid,
+        ));
+
+        // Runtime tested state before call
+
+        // Events number before tested calls
+        let number_of_events_before_call = System::events().len();
+
+        // Pick open auction winner
+        assert_ok!(Content::pick_open_auction_winner(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+        ));
+
+        // Runtime tested state after call
+
+        // Ensure english auction successfully completed
+        assert!(matches!(
+            Content::video_by_id(video_id).nft_status,
+            Some(OwnedNFT {
+                transactional_status: TransactionalStatus::Idle,
+                ..
+            })
+        ));
+
+        let pick_open_auction_winner_event = get_test_event(RawEvent::OpenAuctionBidAccepted(
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+        ));
+
+        // Last event checked
+        assert_event(
+            pick_open_auction_winner_event,
+            number_of_events_before_call + 3,
+        );
+    })
+}
+
+#[test]
+fn pick_open_auction_winner_auth_failed() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        let bid_lock_duration = Content::min_bid_lock_duration();
+
+        let auction_params = AuctionParams {
+            starting_price: Content::min_starting_price(),
+            buy_now_price: None,
+            auction_type: AuctionType::Open(OpenAuctionDetails { bid_lock_duration }),
+            minimal_bid_step: Content::min_bid_step(),
+            starts_at: None,
+            whitelist: BTreeSet::new(),
+        };
+
+        // Start nft auction
+        assert_ok!(Content::start_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            auction_params.clone(),
+        ));
+
+        // deposit initial balance
+        let bid = Content::min_starting_price();
+
+        let _ = balances::Module::<Test>::deposit_creating(&SECOND_MEMBER_ORIGIN, bid);
+
+        // Make nft auction bid
+        assert_ok!(Content::make_bid(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            SECOND_MEMBER_ID,
+            video_id,
+            bid,
+        ));
+
+        // Run to the block where auction expires
+        run_to_block(Content::max_auction_duration() + 1);
+
+        // Make an attempt to pick open auction winner with wrong credentials
+        let pick_open_auction_winner_result = Content::pick_open_auction_winner(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            ContentActor::Member(UNKNOWN_ID),
+            video_id,
+        );
+
+        // Failure checked
+        assert_err!(
+            pick_open_auction_winner_result,
+            Error::<Test>::MemberAuthFailed
+        );
+    })
+}
+
+#[test]
+fn pick_open_auction_winner_actor_not_authorized() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        let bid_lock_duration = Content::min_bid_lock_duration();
+
+        let auction_params = AuctionParams {
+            starting_price: Content::min_starting_price(),
+            buy_now_price: None,
+            auction_type: AuctionType::Open(OpenAuctionDetails { bid_lock_duration }),
+            minimal_bid_step: Content::min_bid_step(),
+            starts_at: None,
+            whitelist: BTreeSet::new(),
+        };
+
+        // Start nft auction
+        assert_ok!(Content::start_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            auction_params.clone(),
+        ));
+
+        // deposit initial balance
+        let bid = Content::min_starting_price();
+
+        let _ = balances::Module::<Test>::deposit_creating(&SECOND_MEMBER_ORIGIN, bid);
+
+        // Make nft auction bid
+        assert_ok!(Content::make_bid(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            SECOND_MEMBER_ID,
+            video_id,
+            bid,
+        ));
+
+        // Run to the block where auction expires
+        run_to_block(Content::max_auction_duration() + 1);
+
+        // Make an attempt to pick open auction winner if actor is not authorized to do this
+        let pick_open_auction_winner_result = Content::pick_open_auction_winner(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            ContentActor::Member(SECOND_MEMBER_ID),
+            video_id,
+        );
+
+        // Failure checked
+        assert_err!(
+            pick_open_auction_winner_result,
+            Error::<Test>::ActorNotAuthorized
+        );
+    })
+}
+
+#[test]
+fn pick_open_auction_winner_video_does_not_exist() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        // Make an attempt to pick open auction winner which corresponding video does not exist
+        let pick_open_auction_winner_result = Content::pick_open_auction_winner(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            ContentActor::Member(SECOND_MEMBER_ID),
+            video_id,
+        );
+
+        // Failure checked
+        assert_err!(
+            pick_open_auction_winner_result,
+            Error::<Test>::VideoDoesNotExist
+        );
+    })
+}
+
+#[test]
+fn pick_open_auction_winner_nft_is_not_issued() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Make an attempt to pick open auction winner for nft which is not issued yet
+        let pick_open_auction_winner_result = Content::pick_open_auction_winner(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            ContentActor::Member(SECOND_MEMBER_ID),
+            video_id,
+        );
+
+        // Failure checked
+        assert_err!(
+            pick_open_auction_winner_result,
+            Error::<Test>::NFTDoesNotExist
+        );
+    })
+}
+
+#[test]
+fn pick_open_auction_winner_not_in_auction_state() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        // Make an attempt to pick open auction winner for nft which is not in auction state
+        let pick_open_auction_winner_result = Content::pick_open_auction_winner(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+        );
+
+        // Failure checked
+        assert_err!(
+            pick_open_auction_winner_result,
+            Error::<Test>::NotInAuctionState
+        );
+    })
+}
+
+#[test]
+fn pick_open_auction_winner_is_not_open_auction_type() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        let auction_params = AuctionParams {
+            starting_price: Content::min_starting_price(),
+            buy_now_price: None,
+            auction_type: AuctionType::English(EnglishAuctionDetails {
+                extension_period: Content::min_auction_extension_period(),
+                auction_duration: Content::max_auction_duration(),
+            }),
+            minimal_bid_step: Content::max_bid_step(),
+            starts_at: None,
+            whitelist: BTreeSet::new(),
+        };
+
+        // Start nft auction
+        assert_ok!(Content::start_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            auction_params.clone(),
+        ));
+
+        // deposit initial balance
+        let bid = Content::min_starting_price();
+
+        let _ = balances::Module::<Test>::deposit_creating(&SECOND_MEMBER_ORIGIN, bid);
+
+        // Make nft auction bid
+        assert_ok!(Content::make_bid(
+            Origin::signed(SECOND_MEMBER_ORIGIN),
+            SECOND_MEMBER_ID,
+            video_id,
+            bid,
+        ));
+
+        // Make an attempt to pick open auction winner for nft which is in english auction state
+        let pick_open_auction_winner_result = Content::pick_open_auction_winner(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+        );
+
+        // Failure checked
+        assert_err!(
+            pick_open_auction_winner_result,
+            Error::<Test>::IsNotOpenAuctionType
+        );
+    })
+}
+
+#[test]
+fn pick_open_auction_winner_last_bid_does_not_exist() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        let bid_lock_duration = Content::min_bid_lock_duration();
+
+        let auction_params = AuctionParams {
+            starting_price: Content::min_starting_price(),
+            buy_now_price: None,
+            auction_type: AuctionType::Open(OpenAuctionDetails { bid_lock_duration }),
+            minimal_bid_step: Content::min_bid_step(),
+            starts_at: None,
+            whitelist: BTreeSet::new(),
+        };
+
+        // Start nft auction
+        assert_ok!(Content::start_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            auction_params.clone(),
+        ));
+
+        // Run to the block where auction expires
+        run_to_block(Content::max_auction_duration() + 1);
+
+        // Make an attempt to pick open auction winner if last bid does not exist
+        let pick_open_auction_winner_result = Content::pick_open_auction_winner(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+        );
+
+        // Failure checked
+        assert_err!(
+            pick_open_auction_winner_result,
+            Error::<Test>::LastBidDoesNotExist
+        );
+    })
+}

+ 226 - 0
runtime-modules/content/src/tests/nft/sell_nft.rs

@@ -0,0 +1,226 @@
+#![cfg(test)]
+
+use crate::tests::mock::*;
+use crate::*;
+use frame_support::{assert_err, assert_ok};
+
+#[test]
+fn sell_nft() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        // Runtime tested state before call
+
+        // Events number before tested calls
+        let number_of_events_before_call = System::events().len();
+
+        let price = 100;
+
+        // Sell nft
+        assert_ok!(Content::sell_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            video_id,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            price,
+        ));
+
+        // Runtime tested state after call
+
+        // Ensure nft offer made succesfully
+        assert!(matches!(
+            Content::video_by_id(video_id).nft_status,
+            Some(OwnedNFT {
+                transactional_status: TransactionalStatus::BuyNow(
+                    cost,
+                ),
+                ..
+            }) if price == cost
+        ));
+
+        let sell_order_made_event = get_test_event(RawEvent::NFTSellOrderMade(
+            video_id,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            price,
+        ));
+
+        // Last event checked
+        assert_event(sell_order_made_event, number_of_events_before_call + 1);
+    })
+}
+
+#[test]
+fn sell_nft_video_does_not_exist() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        let price = 100;
+
+        // Make an attempt to sell nft which corresponding video does not exist yet
+        let sell_nft_result = Content::sell_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            video_id,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            price,
+        );
+
+        // Failure checked
+        assert_err!(sell_nft_result, Error::<Test>::VideoDoesNotExist);
+    })
+}
+
+#[test]
+fn sell_nft_not_issued() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        let price = 100;
+
+        // Make an attempt to sell nft which is not issued yet
+        let sell_nft_result = Content::sell_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            video_id,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            price,
+        );
+
+        // Failure checked
+        assert_err!(sell_nft_result, Error::<Test>::NFTDoesNotExist);
+    })
+}
+
+#[test]
+fn sell_nft_auth_failed() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        let price = 100;
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        // Make an attempt to sell nft with wrong credentials
+        let sell_nft_result = Content::sell_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            video_id,
+            ContentActor::Member(UNKNOWN_ID),
+            price,
+        );
+
+        // Failure checked
+        assert_err!(sell_nft_result, Error::<Test>::MemberAuthFailed);
+    })
+}
+
+#[test]
+fn sell_nft_not_authorized() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        let price = 100;
+
+        // Make an attempt to sell nft if actor is not authorized
+        let sell_nft_result = Content::sell_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            video_id,
+            ContentActor::Member(SECOND_MEMBER_ID),
+            price,
+        );
+
+        // Failure checked
+        assert_err!(sell_nft_result, Error::<Test>::ActorNotAuthorized);
+    })
+}
+
+#[test]
+fn sell_nft_transactional_status_is_not_idle() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        // Offer nft
+        assert_ok!(Content::offer_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            video_id,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            SECOND_MEMBER_ID,
+            None,
+        ));
+
+        let price = 100;
+
+        // Make an attempt to sell nft when it is already offered
+        let sell_nft_result = Content::sell_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            video_id,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            price,
+        );
+
+        // Failure checked
+        assert_err!(sell_nft_result, Error::<Test>::NftIsNotIdle);
+    })
+}

+ 676 - 0
runtime-modules/content/src/tests/nft/start_nft_auction.rs

@@ -0,0 +1,676 @@
+#![cfg(test)]
+
+use crate::tests::mock::*;
+use crate::*;
+use frame_support::{assert_err, assert_ok};
+use std::iter::FromIterator;
+
+#[test]
+fn start_nft_auction() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        // Runtime tested state before call
+
+        // Events number before tested calls
+        let number_of_events_before_call = System::events().len();
+
+        let auction_params = get_open_auction_params();
+
+        // Start nft auction
+        assert_ok!(Content::start_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            auction_params.clone(),
+        ));
+
+        // Runtime tested state after call
+
+        let mut auction: Auction<Test> = AuctionRecord::new(auction_params.clone());
+
+        if auction_params.starts_at.is_none() {
+            auction.starts_at = <frame_system::Module<Test>>::block_number();
+        }
+
+        // Ensure nft status changed to given Auction
+        assert!(matches!(
+            Content::video_by_id(video_id).nft_status,
+            Some(OwnedNFT {
+                transactional_status: TransactionalStatus::Auction(created_auction,),
+                ..
+            }) if auction == created_auction
+        ));
+
+        let nft_auction_started_event = get_test_event(RawEvent::AuctionStarted(
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            auction_params,
+        ));
+
+        // Last event checked
+        assert_event(nft_auction_started_event, number_of_events_before_call + 1);
+    })
+}
+
+#[test]
+fn start_nft_auction_video_does_not_exist() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        let auction_params = get_open_auction_params();
+
+        // Make an attempt to start nft auction which corresponding video does not exist yet
+        let start_nft_auction_result = Content::start_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            auction_params.clone(),
+        );
+
+        // Failure checked
+        assert_err!(start_nft_auction_result, Error::<Test>::VideoDoesNotExist);
+    })
+}
+
+#[test]
+fn start_nft_auction_not_issued() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        let auction_params = get_open_auction_params();
+
+        // Make an attempt to start nft auction for nft which is not issued yet
+        let start_nft_auction_result = Content::start_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            auction_params.clone(),
+        );
+
+        // Failure checked
+        assert_err!(start_nft_auction_result, Error::<Test>::NFTDoesNotExist);
+    })
+}
+
+#[test]
+fn start_nft_auction_auth_failed() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        let auction_params = get_open_auction_params();
+
+        // Make an attempt to start nft auction with wrong credentials
+        let start_nft_auction_result = Content::start_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(UNKNOWN_ID),
+            video_id,
+            auction_params.clone(),
+        );
+
+        // Failure checked
+        assert_err!(start_nft_auction_result, Error::<Test>::MemberAuthFailed);
+    })
+}
+
+#[test]
+fn start_nft_auction_not_authorized() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        let auction_params = get_open_auction_params();
+
+        // Make an attempt to start nft auction if actor is not authorized
+        let start_nft_auction_result = Content::start_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(SECOND_MEMBER_ID),
+            video_id,
+            auction_params.clone(),
+        );
+
+        // Failure checked
+        assert_err!(start_nft_auction_result, Error::<Test>::ActorNotAuthorized);
+    })
+}
+
+#[test]
+fn start_nft_auction_transactional_status_is_not_idle() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        // Offer nft
+        assert_ok!(Content::offer_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            video_id,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            SECOND_MEMBER_ID,
+            None,
+        ));
+
+        let auction_params = get_open_auction_params();
+
+        // Make an attempt to start nft auction if nft transaction status is not idle
+        let start_nft_auction_result = Content::start_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            auction_params.clone(),
+        );
+
+        // Failure checked
+        assert_err!(start_nft_auction_result, Error::<Test>::NftIsNotIdle);
+    })
+}
+
+#[test]
+fn start_nft_auction_invalid_params() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+
+        let video_id = NextVideoId::<Test>::get();
+
+        create_simple_channel_and_video(FIRST_MEMBER_ORIGIN, FIRST_MEMBER_ID);
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        // Make an attempt to start nft auction if starting price provided is less then min starting price
+        let auction_params = AuctionParams {
+            starting_price: Content::min_starting_price() - 1,
+            buy_now_price: None,
+            auction_type: AuctionType::Open(OpenAuctionDetails {
+                bid_lock_duration: Content::min_bid_lock_duration(),
+            }),
+            minimal_bid_step: Content::min_bid_step(),
+            starts_at: None,
+            whitelist: BTreeSet::new(),
+        };
+
+        let start_nft_auction_result = Content::start_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            auction_params.clone(),
+        );
+
+        // Failure checked
+        assert_err!(
+            start_nft_auction_result,
+            Error::<Test>::StartingPriceLowerBoundExceeded
+        );
+
+        // Make an attempt to start nft auction if starting price provided is greater then max starting price
+        let auction_params = AuctionParams {
+            starting_price: Content::max_starting_price() + 1,
+            buy_now_price: None,
+            auction_type: AuctionType::Open(OpenAuctionDetails {
+                bid_lock_duration: Content::min_bid_lock_duration(),
+            }),
+            minimal_bid_step: Content::min_bid_step(),
+            starts_at: None,
+            whitelist: BTreeSet::new(),
+        };
+
+        let start_nft_auction_result = Content::start_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            auction_params.clone(),
+        );
+
+        // Failure checked
+        assert_err!(
+            start_nft_auction_result,
+            Error::<Test>::StartingPriceUpperBoundExceeded
+        );
+
+        // Make an attempt to start nft auction if minimal bid step provided is less then min allowed bid step
+        let auction_params = AuctionParams {
+            starting_price: Content::max_starting_price(),
+            buy_now_price: None,
+            auction_type: AuctionType::Open(OpenAuctionDetails {
+                bid_lock_duration: Content::min_bid_lock_duration(),
+            }),
+            minimal_bid_step: Content::min_bid_step() - 1,
+            starts_at: None,
+            whitelist: BTreeSet::new(),
+        };
+
+        let start_nft_auction_result = Content::start_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            auction_params.clone(),
+        );
+
+        // Failure checked
+        assert_err!(
+            start_nft_auction_result,
+            Error::<Test>::AuctionBidStepLowerBoundExceeded
+        );
+
+        // Make an attempt to start nft auction if minimal bid step provided is greater then max allowed bid step
+        let auction_params = AuctionParams {
+            starting_price: Content::max_starting_price(),
+            buy_now_price: None,
+            auction_type: AuctionType::Open(OpenAuctionDetails {
+                bid_lock_duration: Content::min_bid_lock_duration(),
+            }),
+            minimal_bid_step: Content::max_bid_step() + 1,
+            starts_at: None,
+            whitelist: BTreeSet::new(),
+        };
+
+        let start_nft_auction_result = Content::start_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            auction_params.clone(),
+        );
+
+        // Failure checked
+        assert_err!(
+            start_nft_auction_result,
+            Error::<Test>::AuctionBidStepUpperBoundExceeded
+        );
+
+        // Make an attempt to start open nft auction if minimal bid lock duration
+        // of auction provided is less then min allowed bid lock duration
+        let auction_params = AuctionParams {
+            starting_price: Content::max_starting_price(),
+            buy_now_price: None,
+            auction_type: AuctionType::Open(OpenAuctionDetails {
+                bid_lock_duration: Content::min_bid_lock_duration() - 1,
+            }),
+            minimal_bid_step: Content::max_bid_step(),
+            starts_at: None,
+            whitelist: BTreeSet::new(),
+        };
+
+        let start_nft_auction_result = Content::start_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            auction_params.clone(),
+        );
+
+        // Failure checked
+        assert_err!(
+            start_nft_auction_result,
+            Error::<Test>::BidLockDurationLowerBoundExceeded
+        );
+
+        // Make an attempt to start open nft auction if minimal bid lock duration
+        // of auction provided is greater then max allowed bid lock duration
+        let auction_params = AuctionParams {
+            starting_price: Content::max_starting_price(),
+            buy_now_price: None,
+            auction_type: AuctionType::Open(OpenAuctionDetails {
+                bid_lock_duration: Content::max_bid_lock_duration() + 1,
+            }),
+            minimal_bid_step: Content::max_bid_step(),
+            starts_at: None,
+            whitelist: BTreeSet::new(),
+        };
+
+        let start_nft_auction_result = Content::start_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            auction_params.clone(),
+        );
+
+        // Failure checked
+        assert_err!(
+            start_nft_auction_result,
+            Error::<Test>::BidLockDurationUpperBoundExceeded
+        );
+
+        // Make an attempt to start english nft auction if extension period
+        // of auction provided is less then min allowed extension period
+        let auction_params = AuctionParams {
+            starting_price: Content::max_starting_price(),
+            buy_now_price: None,
+            auction_type: AuctionType::English(EnglishAuctionDetails {
+                extension_period: Content::min_auction_extension_period() - 1,
+                auction_duration: Content::max_auction_duration(),
+            }),
+            minimal_bid_step: Content::max_bid_step(),
+            starts_at: None,
+            whitelist: BTreeSet::new(),
+        };
+
+        let start_nft_auction_result = Content::start_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            auction_params.clone(),
+        );
+
+        // Failure checked
+        assert_err!(
+            start_nft_auction_result,
+            Error::<Test>::ExtensionPeriodLowerBoundExceeded
+        );
+
+        // Make an attempt to start english nft auction if extension period
+        // of auction provided is greater then max allowed extension period
+        let auction_params = AuctionParams {
+            starting_price: Content::max_starting_price(),
+            buy_now_price: None,
+            auction_type: AuctionType::English(EnglishAuctionDetails {
+                extension_period: Content::max_auction_extension_period() + 1,
+                auction_duration: Content::max_auction_duration(),
+            }),
+            minimal_bid_step: Content::max_bid_step(),
+            starts_at: None,
+            whitelist: BTreeSet::new(),
+        };
+
+        let start_nft_auction_result = Content::start_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            auction_params.clone(),
+        );
+
+        // Failure checked
+        assert_err!(
+            start_nft_auction_result,
+            Error::<Test>::ExtensionPeriodUpperBoundExceeded
+        );
+
+        // Make an attempt to start english nft auction if auction duration
+        // of auction provided is less then min allowed auction duration
+        let auction_params = AuctionParams {
+            starting_price: Content::max_starting_price(),
+            buy_now_price: None,
+            auction_type: AuctionType::English(EnglishAuctionDetails {
+                extension_period: Content::min_auction_extension_period(),
+                auction_duration: Content::min_auction_duration() - 1,
+            }),
+            minimal_bid_step: Content::max_bid_step(),
+            starts_at: None,
+            whitelist: BTreeSet::new(),
+        };
+
+        let start_nft_auction_result = Content::start_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            auction_params.clone(),
+        );
+
+        // Failure checked
+        assert_err!(
+            start_nft_auction_result,
+            Error::<Test>::AuctionDurationLowerBoundExceeded
+        );
+
+        // Make an attempt to start english nft auction if auction duration
+        // of auction provided is greater then max allowed auction duration
+        let auction_params = AuctionParams {
+            starting_price: Content::max_starting_price(),
+            buy_now_price: None,
+            auction_type: AuctionType::English(EnglishAuctionDetails {
+                extension_period: Content::max_auction_extension_period(),
+                auction_duration: Content::max_auction_duration() + 1,
+            }),
+            minimal_bid_step: Content::max_bid_step(),
+            starts_at: None,
+            whitelist: BTreeSet::new(),
+        };
+
+        let start_nft_auction_result = Content::start_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            auction_params.clone(),
+        );
+
+        // Failure checked
+        assert_err!(
+            start_nft_auction_result,
+            Error::<Test>::AuctionDurationUpperBoundExceeded
+        );
+
+        // Make an attempt to start english nft auction if extension period
+        // of auction provided is greater auction duration
+        let auction_params = AuctionParams {
+            starting_price: Content::max_starting_price(),
+            buy_now_price: None,
+            auction_type: AuctionType::English(EnglishAuctionDetails {
+                extension_period: Content::max_auction_extension_period(),
+                auction_duration: Content::min_auction_duration(),
+            }),
+            minimal_bid_step: Content::max_bid_step(),
+            starts_at: None,
+            whitelist: BTreeSet::new(),
+        };
+
+        let start_nft_auction_result = Content::start_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            auction_params.clone(),
+        );
+
+        // Failure checked
+        assert_err!(
+            start_nft_auction_result,
+            Error::<Test>::ExtensionPeriodIsGreaterThenAuctionDuration
+        );
+
+        // Make an attempt to start nft auction if starts_at provided is less then now
+        let auction_params = AuctionParams {
+            starting_price: Content::max_starting_price(),
+            buy_now_price: None,
+            auction_type: AuctionType::Open(OpenAuctionDetails {
+                bid_lock_duration: Content::min_bid_lock_duration(),
+            }),
+            minimal_bid_step: Content::min_bid_step(),
+            starts_at: Some(<frame_system::Module<Test>>::block_number() - 1),
+            whitelist: BTreeSet::new(),
+        };
+
+        let start_nft_auction_result = Content::start_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            auction_params.clone(),
+        );
+
+        // Failure checked
+        assert_err!(
+            start_nft_auction_result,
+            Error::<Test>::StartsAtLowerBoundExceeded
+        );
+
+        // Make an attempt to start nft auction if starts_at provided is greater then now + auction_starts_at_max_delta
+        let auction_params = AuctionParams {
+            starting_price: Content::max_starting_price(),
+            buy_now_price: None,
+            auction_type: AuctionType::Open(OpenAuctionDetails {
+                bid_lock_duration: Content::min_bid_lock_duration(),
+            }),
+            minimal_bid_step: Content::max_bid_step(),
+            starts_at: Some(
+                <frame_system::Module<Test>>::block_number()
+                    + Content::auction_starts_at_max_delta()
+                    + 1,
+            ),
+            whitelist: BTreeSet::new(),
+        };
+
+        let start_nft_auction_result = Content::start_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            auction_params.clone(),
+        );
+
+        // Failure checked
+        assert_err!(
+            start_nft_auction_result,
+            Error::<Test>::StartsAtUpperBoundExceeded
+        );
+
+        // Make an attempt to start nft auction if auction related buy now is less then starting price
+        let buy_now_price = Content::min_starting_price();
+
+        let auction_params = AuctionParams {
+            starting_price: buy_now_price + 1,
+            buy_now_price: Some(buy_now_price),
+            auction_type: AuctionType::Open(OpenAuctionDetails {
+                bid_lock_duration: Content::min_bid_lock_duration(),
+            }),
+            minimal_bid_step: Content::max_bid_step(),
+            starts_at: None,
+            whitelist: BTreeSet::new(),
+        };
+
+        let start_nft_auction_result = Content::start_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            auction_params.clone(),
+        );
+
+        // Failure checked
+        assert_err!(
+            start_nft_auction_result,
+            Error::<Test>::BuyNowIsLessThenStartingPrice
+        );
+
+        // Make an attempt to start nft auction if auction whitelist provided consists only 1 member
+        let auction_params = AuctionParams {
+            starting_price: Content::min_starting_price(),
+            buy_now_price: None,
+            auction_type: AuctionType::Open(OpenAuctionDetails {
+                bid_lock_duration: Content::min_bid_lock_duration(),
+            }),
+            minimal_bid_step: Content::max_bid_step(),
+            starts_at: None,
+            whitelist: BTreeSet::from_iter(vec![SECOND_MEMBER_ID].into_iter()),
+        };
+
+        let start_nft_auction_result = Content::start_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            auction_params.clone(),
+        );
+
+        // Failure checked
+        assert_err!(
+            start_nft_auction_result,
+            Error::<Test>::WhitelistHasOnlyOneMember
+        );
+
+        // Make an attempt to start nft auction if length of auction whitelist provided exceeds max allowed length
+        let whitelist: BTreeSet<_> = (0..=Content::max_auction_whitelist_length())
+            .into_iter()
+            .map(|member| member as u64)
+            .collect();
+
+        let auction_params = AuctionParams {
+            starting_price: Content::min_starting_price(),
+            buy_now_price: None,
+            auction_type: AuctionType::Open(OpenAuctionDetails {
+                bid_lock_duration: Content::min_bid_lock_duration(),
+            }),
+            minimal_bid_step: Content::max_bid_step(),
+            starts_at: None,
+            whitelist,
+        };
+
+        let start_nft_auction_result = Content::start_nft_auction(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            auction_params.clone(),
+        );
+
+        // Failure checked
+        assert_err!(
+            start_nft_auction_result,
+            Error::<Test>::MaxAuctionWhiteListLengthUpperBoundExceeded
+        );
+    })
+}

+ 148 - 30
runtime-modules/content/src/tests/videos.rs

@@ -2,24 +2,100 @@
 
 use super::curators;
 use super::mock::*;
+use crate::sp_api_hidden_includes_decl_storage::hidden_include::traits::Currency;
 use crate::*;
 use frame_support::{assert_err, assert_ok};
 
-fn create_member_channel() -> ChannelId {
-    let channel_id = Content::next_channel_id();
-
-    // Member can create the channel
-    assert_ok!(Content::create_channel(
-        Origin::signed(FIRST_MEMBER_ORIGIN),
-        ContentActor::Member(FIRST_MEMBER_ID),
-        ChannelCreationParameters {
-            assets: vec![],
-            meta: vec![],
-            reward_account: None,
-        }
-    ));
-
-    channel_id
+#[test]
+fn video_creation_successful() {
+    with_default_mock_builder(|| {
+        run_to_block(1);
+
+        // depositi initial balance
+        let _ = balances::Module::<Test>::deposit_creating(
+            &FIRST_MEMBER_ORIGIN,
+            <Test as balances::Trait>::Balance::from(100u32),
+        );
+
+        let channel_id = NextChannelId::<Test>::get();
+
+        create_channel_mock(
+            FIRST_MEMBER_ORIGIN,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            ChannelCreationParametersRecord {
+                assets: NewAssets::<Test>::Urls(vec![]),
+                meta: vec![],
+                reward_account: None,
+            },
+            Ok(()),
+        );
+
+        let params = get_video_creation_parameters();
+
+        create_video_mock(
+            FIRST_MEMBER_ORIGIN,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            channel_id,
+            params,
+            Ok(()),
+        )
+    })
+}
+
+#[test]
+fn video_update_successful() {
+    with_default_mock_builder(|| {
+        run_to_block(1);
+
+        let _ = balances::Module::<Test>::deposit_creating(
+            &FIRST_MEMBER_ORIGIN,
+            <Test as balances::Trait>::Balance::from(100u32),
+        );
+
+        let channel_id = NextChannelId::<Test>::get();
+
+        create_channel_mock(
+            FIRST_MEMBER_ORIGIN,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            ChannelCreationParametersRecord {
+                assets: NewAssets::<Test>::Urls(vec![]),
+                meta: vec![],
+                reward_account: None,
+            },
+            Ok(()),
+        );
+
+        let params = get_video_creation_parameters();
+
+        let video_id = Content::next_video_id();
+
+        create_video_mock(
+            FIRST_MEMBER_ORIGIN,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            channel_id,
+            params,
+            Ok(()),
+        );
+
+        let update_params = VideoUpdateParametersRecord {
+            assets: Some(NewAssets::<Test>::Upload(CreationUploadParameters {
+                object_creation_list: vec![DataObjectCreationParameters {
+                    size: 3,
+                    ipfs_content_id: b"first".to_vec(),
+                }],
+                expected_data_size_fee: storage::DataObjectPerMegabyteFee::<Test>::get(),
+            })),
+            new_meta: None,
+        };
+
+        update_video_mock(
+            FIRST_MEMBER_ORIGIN,
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            update_params,
+            Ok(()),
+        );
+    })
 }
 
 #[test]
@@ -34,8 +110,8 @@ fn member_can_create_videos() {
             Origin::signed(FIRST_MEMBER_ORIGIN),
             ContentActor::Member(FIRST_MEMBER_ID),
             channel_id,
-            VideoCreationParameters {
-                assets: vec![NewAsset::Urls(vec![b"https://somewhere.com/".to_vec()])],
+            VideoCreationParametersRecord {
+                assets: NewAssets::<Test>::Urls(vec![vec![b"https://somewhere.com/".to_vec()]]),
                 meta: b"metablob".to_vec(),
             }
         ));
@@ -46,8 +122,8 @@ fn member_can_create_videos() {
                 ContentActor::Member(FIRST_MEMBER_ID),
                 channel_id,
                 video_id,
-                VideoCreationParameters {
-                    assets: vec![NewAsset::Urls(vec![b"https://somewhere.com/".to_vec()])],
+                VideoCreationParametersRecord {
+                    assets: NewAssets::<Test>::Urls(vec![vec![b"https://somewhere.com/".to_vec()]]),
                     meta: b"metablob".to_vec(),
                 }
             ))
@@ -62,10 +138,10 @@ fn member_can_create_videos() {
             Origin::signed(FIRST_MEMBER_ORIGIN),
             ContentActor::Member(FIRST_MEMBER_ID),
             video_id,
-            VideoUpdateParameters {
-                assets: Some(vec![NewAsset::Urls(vec![
+            VideoUpdateParametersRecord {
+                assets: Some(NewAssets::<Test>::Urls(vec![vec![
                     b"https://somewhere-else.com/".to_vec()
-                ])]),
+                ]])),
                 new_meta: Some(b"newmetablob".to_vec()),
             }
         ));
@@ -75,10 +151,10 @@ fn member_can_create_videos() {
             MetaEvent::content(RawEvent::VideoUpdated(
                 ContentActor::Member(FIRST_MEMBER_ID),
                 video_id,
-                VideoUpdateParameters {
-                    assets: Some(vec![NewAsset::Urls(vec![
+                VideoUpdateParametersRecord {
+                    assets: Some(NewAssets::<Test>::Urls(vec![vec![
                         b"https://somewhere-else.com/".to_vec()
-                    ])]),
+                    ]])),
                     new_meta: Some(b"newmetablob".to_vec()),
                 }
             ))
@@ -90,8 +166,8 @@ fn member_can_create_videos() {
                 Origin::signed(SECOND_MEMBER_ORIGIN),
                 ContentActor::Member(SECOND_MEMBER_ID),
                 channel_id,
-                VideoCreationParameters {
-                    assets: vec![],
+                VideoCreationParametersRecord {
+                    assets: NewAssets::<Test>::Urls(vec![]),
                     meta: vec![],
                 }
             ),
@@ -104,7 +180,7 @@ fn member_can_create_videos() {
                 Origin::signed(SECOND_MEMBER_ORIGIN),
                 ContentActor::Member(SECOND_MEMBER_ID),
                 video_id,
-                VideoUpdateParameters {
+                VideoUpdateParametersRecord {
                     assets: None,
                     new_meta: None,
                 }
@@ -139,6 +215,48 @@ fn member_can_create_videos() {
     })
 }
 
+#[test]
+fn delete_video_nft_is_issued() {
+    with_default_mock_builder(|| {
+        // Run to block one to see emitted events
+        run_to_block(1);
+        let channel_id = create_member_channel();
+
+        let video_id = Content::next_video_id();
+
+        // Create a video
+        assert_ok!(Content::create_video(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            channel_id,
+            VideoCreationParametersRecord {
+                assets: NewAssets::<Test>::Urls(vec![vec![b"https://somewhere.com/".to_vec()]]),
+                meta: b"metablob".to_vec(),
+            }
+        ));
+
+        // Issue nft
+        assert_ok!(Content::issue_nft(
+            Origin::signed(FIRST_MEMBER_ORIGIN),
+            ContentActor::Member(FIRST_MEMBER_ID),
+            video_id,
+            None,
+            b"metablob".to_vec(),
+            None
+        ));
+
+        // Make an attempt to delete a video, which has an nft issued already.
+        assert_err!(
+            Content::delete_video(
+                Origin::signed(FIRST_MEMBER_ORIGIN),
+                ContentActor::Member(FIRST_MEMBER_ID),
+                video_id
+            ),
+            Error::<Test>::NFTAlreadyExists
+        );
+    })
+}
+
 #[test]
 fn curators_can_censor_videos() {
     with_default_mock_builder(|| {
@@ -151,8 +269,8 @@ fn curators_can_censor_videos() {
             Origin::signed(FIRST_MEMBER_ORIGIN),
             ContentActor::Member(FIRST_MEMBER_ID),
             channel_id,
-            VideoCreationParameters {
-                assets: vec![NewAsset::Urls(vec![b"https://somewhere.com/".to_vec()])],
+            VideoCreationParametersRecord {
+                assets: NewAssets::<Test>::Urls(vec![vec![b"https://somewhere.com/".to_vec()]]),
                 meta: b"metablob".to_vec(),
             }
         ));

+ 180 - 318
runtime-modules/content/src/types.rs

@@ -1,52 +1,41 @@
 use crate::*;
 
-pub(crate) type ContentId<T> = <T as StorageOwnership>::ContentId;
-
-pub(crate) type DataObjectTypeId<T> = <T as StorageOwnership>::DataObjectTypeId;
-
-pub(crate) type DAOId<T> = <T as StorageOwnership>::DAOId;
-
-pub(crate) type ContentParameters<T> = ContentParametersRecord<ContentId<T>, DataObjectTypeId<T>>;
-
-pub(crate) type StorageObjectOwner<T> =
-    StorageObjectOwnerRecord<MemberId<T>, <T as StorageOwnership>::ChannelId, DAOId<T>>;
-
-/// Type, used in diffrent numeric constraints representations
-pub type MaxNumber = u32;
-
 /// Specifies how a new asset will be provided on creating and updating
 /// Channels, Videos, Series and Person
 #[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
 #[derive(Encode, Decode, Clone, PartialEq, Eq, Debug)]
-pub enum NewAsset<ContentParameters> {
+pub enum NewAssetsRecord<Balance> {
     /// Upload to the storage frame_system
-    Upload(ContentParameters),
+    Upload(CreationUploadParameters<Balance>),
     /// Multiple url strings pointing at an asset
-    Urls(Vec<Url>),
+    Urls(Vec<AssetUrls>),
 }
 
+pub type NewAssets<T> = NewAssetsRecord<<T as balances::Trait>::Balance>;
+
 /// The owner of a channel, is the authorized "actor" that can update
 /// or delete or transfer a channel and its contents.
 #[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
 #[derive(Encode, Decode, Clone, PartialEq, Eq, Debug)]
-pub enum ChannelOwner<MemberId, CuratorGroupId, DAOId> {
+pub enum ChannelOwner<MemberId, CuratorGroupId> {
     /// A Member owns the channel
     Member(MemberId),
     /// A specific curation group owns the channel
     CuratorGroup(CuratorGroupId),
-    // Native DAO owns the channel
-    Dao(DAOId),
 }
 
 // simplification type
-pub(crate) type ActorToChannelOwnerResult<T> =
-    Result<ChannelOwner<MemberId<T>, CuratorGroupId<T>, DAOId<T>>, Error<T>>;
+pub(crate) type ActorToChannelOwnerResult<T> = Result<
+    ChannelOwner<
+        <T as membership::Trait>::MemberId,
+        <T as ContentActorAuthenticator>::CuratorGroupId,
+    >,
+    Error<T>,
+>;
 
 // Default trait implemented only because its used in a Channel which needs to implement a Default trait
 // since it is a StorageValue.
-impl<MemberId: Default, CuratorGroupId, DAOId> Default
-    for ChannelOwner<MemberId, CuratorGroupId, DAOId>
-{
+impl<MemberId: Default, CuratorGroupId> Default for ChannelOwner<MemberId, CuratorGroupId> {
     fn default() -> Self {
         ChannelOwner::Member(MemberId::default())
     }
@@ -77,28 +66,24 @@ pub struct ChannelCategoryUpdateParameters {
 }
 
 /// Type representing an owned channel which videos, playlists, and series can belong to.
-/// If a channel is deleted, all videos, playlists and series will also be deleted.
 #[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
 #[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)]
-pub struct ChannelRecord<MemberId, CuratorGroupId, DAOId, AccountId, VideoId, PlaylistId, SeriesId>
-{
+pub struct ChannelRecord<MemberId, CuratorGroupId, AccountId> {
     /// The owner of a channel
-    pub owner: ChannelOwner<MemberId, CuratorGroupId, DAOId>,
+    pub owner: ChannelOwner<MemberId, CuratorGroupId>,
     /// The videos under this channel
-    pub videos: Vec<VideoId>,
-    /// The playlists under this channel
-    pub playlists: Vec<PlaylistId>,
-    /// The series under this channel
-    pub series: Vec<SeriesId>,
+    pub num_videos: u64,
     /// If curators have censored this channel or not
     pub is_censored: bool,
     /// Reward account where revenue is sent if set.
     pub reward_account: Option<AccountId>,
+    /// Account for withdrawing deletion prize funds
+    pub deletion_prize_source_account_id: AccountId,
+    /// Number of asset held in storage
+    pub num_assets: u64,
 }
 
-impl<MemberId, CuratorGroupId, DAOId, AccountId, VideoId, PlaylistId, SeriesId>
-    ChannelRecord<MemberId, CuratorGroupId, DAOId, AccountId, VideoId, PlaylistId, SeriesId>
-{
+impl<MemberId, CuratorGroupId, AccountId> ChannelRecord<MemberId, CuratorGroupId, AccountId> {
     /// Ensure censorship status have been changed
     pub fn ensure_censorship_status_changed<T: Trait>(&self, is_censored: bool) -> DispatchResult {
         ensure!(
@@ -111,13 +96,9 @@ impl<MemberId, CuratorGroupId, DAOId, AccountId, VideoId, PlaylistId, SeriesId>
 
 // Channel alias type for simplification.
 pub type Channel<T> = ChannelRecord<
-    MemberId<T>,
+    <T as membership::Trait>::MemberId,
     <T as ContentActorAuthenticator>::CuratorGroupId,
-    DAOId<T>,
     <T as frame_system::Trait>::AccountId,
-    <T as Trait>::VideoId,
-    <T as Trait>::PlaylistId,
-    <T as Trait>::SeriesId,
 >;
 
 /// A request to buy a channel by a new ChannelOwner.
@@ -127,22 +108,20 @@ pub struct ChannelOwnershipTransferRequestRecord<
     ChannelId,
     MemberId,
     CuratorGroupId,
-    DAOId,
     Balance,
     AccountId,
 > {
-    pub channel_id: ChannelId,
-    pub new_owner: ChannelOwner<MemberId, CuratorGroupId, DAOId>,
-    pub payment: Balance,
-    pub new_reward_account: Option<AccountId>,
+    channel_id: ChannelId,
+    new_owner: ChannelOwner<MemberId, CuratorGroupId>,
+    payment: Balance,
+    new_reward_account: Option<AccountId>,
 }
 
-/// ChannelOwnershipTransferRequest type alias for simplification.
+// ChannelOwnershipTransferRequest type alias for simplification.
 pub type ChannelOwnershipTransferRequest<T> = ChannelOwnershipTransferRequestRecord<
-    <T as StorageOwnership>::ChannelId,
-    MemberId<T>,
+    <T as storage::Trait>::ChannelId,
+    <T as membership::Trait>::MemberId,
     <T as ContentActorAuthenticator>::CuratorGroupId,
-    DAOId<T>,
     BalanceOf<T>,
     <T as frame_system::Trait>::AccountId,
 >;
@@ -150,41 +129,32 @@ 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 ChannelCreationParameters<ContentParameters, AccountId> {
-    /// Assets referenced by metadata
-    pub assets: Vec<NewAsset<ContentParameters>>,
+pub struct ChannelCreationParametersRecord<NewAssets, AccountId> {
+    /// Asset collection for the channel, referenced by metadata
+    pub assets: NewAssets,
     /// Metadata about the channel.
     pub meta: Vec<u8>,
     /// optional reward account
     pub reward_account: Option<AccountId>,
 }
 
+pub type ChannelCreationParameters<T> =
+    ChannelCreationParametersRecord<NewAssets<T>, <T as frame_system::Trait>::AccountId>;
+
 /// Information about channel being updated.
 #[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
 #[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)]
-pub struct ChannelUpdateParameters<ContentParameters, AccountId> {
-    /// Assets referenced by metadata
-    pub assets: Option<Vec<NewAsset<ContentParameters>>>,
+pub struct ChannelUpdateParametersRecord<NewAssets, AccountId> {
+    /// Asset collection for the channel, referenced by metadata    
+    pub assets: Option<NewAssets>,
     /// If set, metadata update for the channel.
     pub new_meta: Option<Vec<u8>>,
     /// If set, updates the reward account of the channel
     pub reward_account: Option<Option<AccountId>>,
 }
 
-/// A category that videos can belong to.
-#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
-#[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)]
-pub struct VideoCategory {
-    // No runtime information is currently stored for a Category.
-}
-
-/// Information about the video category being created.
-#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
-#[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)]
-pub struct VideoCategoryCreationParameters {
-    /// Metadata about the video category.
-    pub meta: Vec<u8>,
-}
+pub type ChannelUpdateParameters<T> =
+    ChannelUpdateParametersRecord<NewAssets<T>, <T as frame_system::Trait>::AccountId>;
 
 /// Information about the video category being updated.
 #[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
@@ -195,244 +165,40 @@ pub struct VideoCategoryUpdateParameters {
     pub new_meta: Vec<u8>,
 }
 
+/// Information regarding the content being uploaded
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Clone, PartialEq, Eq, Debug)]
+pub struct CreationUploadParameters<Balance> {
+    /// Data object parameters.
+    pub object_creation_list: Vec<DataObjectCreationParameters>,
+
+    /// Expected data size fee value for this extrinsic call.
+    pub expected_data_size_fee: Balance,
+}
+
 /// Information about the video being created.
 #[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
 #[derive(Encode, Decode, Clone, PartialEq, Eq, Debug)]
-pub struct VideoCreationParameters<ContentParameters> {
-    /// Assets referenced by metadata
-    pub assets: Vec<NewAsset<ContentParameters>>,
+pub struct VideoCreationParametersRecord<NewAssets> {
+    /// Asset collection for the video
+    pub assets: NewAssets,
     /// Metadata for the video.
     pub meta: Vec<u8>,
 }
 
+pub type VideoCreationParameters<T> = VideoCreationParametersRecord<NewAssets<T>>;
+
+/// Information about the video being updated
 #[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
 #[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)]
-pub struct VideoUpdateParameters<ContentParameters> {
+pub struct VideoUpdateParametersRecord<NewAssets> {
     /// Assets referenced by metadata
-    pub assets: Option<Vec<NewAsset<ContentParameters>>>,
+    pub assets: Option<NewAssets>,
     /// If set, metadata update for the video.
     pub new_meta: Option<Vec<u8>>,
 }
 
-/// A video which belongs to a channel. A video may be part of a series or playlist.
-#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
-#[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)]
-pub struct VideoRecord<
-    ChannelId,
-    SeriesId,
-    BlockNumber: BaseArithmetic + Copy,
-    MemberId: Default + Copy + Ord,
-    CuratorGroupId: Default + Copy,
-    DAOId: Default + Copy,
-    Balance: Default,
-> {
-    pub in_channel: ChannelId,
-    // keep track of which season the video is in if it is an 'episode'
-    // - prevent removing a video if it is in a season (because order is important)
-    pub in_series: Option<SeriesId>,
-    /// Whether the curators have censored the video or not.
-    pub is_censored: bool,
-    /// Whether nft for this video have been issued.
-    pub nft_status: Option<OwnedNFT<BlockNumber, MemberId, CuratorGroupId, DAOId, Balance>>,
-}
-
-impl<
-        ChannelId: Clone,
-        SeriesId: Clone,
-        BlockNumber: BaseArithmetic + Copy,
-        MemberId: Default + Copy + PartialEq + Ord,
-        CuratorGroupId: Default + Copy + PartialEq,
-        DAOId: Default + Copy + PartialEq,
-        Balance: Clone + Default,
-    > VideoRecord<ChannelId, SeriesId, BlockNumber, MemberId, CuratorGroupId, DAOId, Balance>
-{
-    fn is_issued(&self) -> bool {
-        self.nft_status.is_some()
-    }
-
-    /// Ensure nft status is set to `NoneIssued`
-    pub fn ensure_nft_is_not_issued<T: Trait>(&self) -> DispatchResult {
-        ensure!(!self.is_issued(), Error::<T>::NFTAlreadyExists);
-        Ok(())
-    }
-
-    /// Ensure nft status is set to `Owned`
-    pub fn ensure_nft_is_issued<T: Trait>(&self) -> DispatchResult {
-        ensure!(self.is_issued(), Error::<T>::NFTDoesNotExist);
-        Ok(())
-    }
-
-    /// Ensure given NFTOwner is nft owner
-    pub fn ensure_nft_ownership<T: Trait>(
-        &self,
-        owner: &ChannelOwner<MemberId, CuratorGroupId, DAOId>,
-    ) -> DispatchResult {
-        if let Some(owned_nft) = &self.nft_status {
-            ensure!(owned_nft.is_owner(owner), Error::<T>::DoesNotOwnNFT);
-        }
-        Ok(())
-    }
-
-    /// Check whether nft transactional status is set to `Auction`
-    pub fn is_nft_auction_started(&self) -> bool {
-        matches!(
-            self.nft_status,
-            Some(OwnedNFT {
-                transactional_status: TransactionalStatus::Auction(..),
-                ..
-            })
-        )
-    }
-
-    /// Ensure nft is in auction state
-    pub fn ensure_nft_auction_state<T: Trait>(
-        &self,
-    ) -> Result<AuctionRecord<BlockNumber, Balance, MemberId>, Error<T>> {
-        if let Some(OwnedNFT {
-            transactional_status: TransactionalStatus::Auction(auction),
-            ..
-        }) = &self.nft_status
-        {
-            Ok(auction.clone())
-        } else {
-            Err(Error::<T>::NotInAuctionState)
-        }
-    }
-
-    /// Get nft auction record
-    pub fn get_nft_auction(&self) -> Option<AuctionRecord<BlockNumber, Balance, MemberId>> {
-        if let Some(OwnedNFT {
-            transactional_status: TransactionalStatus::Auction(ref auction),
-            ..
-        }) = self.nft_status
-        {
-            Some(auction.clone())
-        } else {
-            None
-        }
-    }
-
-    /// Get nft auction record by reference
-    pub fn get_nft_auction_ref(&self) -> Option<&AuctionRecord<BlockNumber, Balance, MemberId>> {
-        if let Some(OwnedNFT {
-            transactional_status: TransactionalStatus::Auction(ref auction),
-            ..
-        }) = self.nft_status
-        {
-            Some(auction)
-        } else {
-            None
-        }
-    }
-
-    /// Get nft auction record by mutable reference
-    pub fn get_nft_auction_ref_mut(
-        &mut self,
-    ) -> Option<&mut AuctionRecord<BlockNumber, Balance, MemberId>> {
-        if let Some(OwnedNFT {
-            transactional_status: TransactionalStatus::Auction(ref mut auction),
-            ..
-        }) = self.nft_status
-        {
-            Some(auction)
-        } else {
-            None
-        }
-    }
-
-    ///  Ensure nft transactional status is set to `Idle`
-    pub fn ensure_nft_transactional_status_is_idle<T: Trait>(&self) -> DispatchResult {
-        let is_idle = matches!(
-            self.nft_status,
-            Some(OwnedNFT {
-                transactional_status: TransactionalStatus::Idle,
-                ..
-            })
-        );
-        ensure!(is_idle, Error::<T>::NftIsNotIdle);
-        Ok(())
-    }
-
-    /// Sets nft transactional status to `BuyNow`
-    pub fn set_buy_now_transactionl_status(mut self, buy_now_price: Balance) -> Self {
-        if let Some(owned_nft) = &mut self.nft_status {
-            owned_nft.transactional_status = TransactionalStatus::BuyNow(buy_now_price);
-        }
-        self
-    }
-
-    /// Sets nft transactional status to provided `Auction`
-    pub fn set_auction_transactional_status(
-        mut self,
-        auction: AuctionRecord<BlockNumber, Balance, MemberId>,
-    ) -> Self {
-        if let Some(owned_nft) = &mut self.nft_status {
-            owned_nft.transactional_status = TransactionalStatus::Auction(auction);
-        }
-        self
-    }
-
-    /// Set nft transactional status to `Idle`
-    pub fn set_idle_transactional_status(mut self) -> Self {
-        if let Some(owned_nft) = &mut self.nft_status {
-            owned_nft.transactional_status = TransactionalStatus::Idle;
-        }
-        self
-    }
-
-    /// Set nft transactional status to `InitiatedOfferToMember`
-    pub fn set_pending_offer_transactional_status(
-        mut self,
-        to: MemberId,
-        balance: Option<Balance>,
-    ) -> Self {
-        if let Some(owned_nft) = &mut self.nft_status {
-            owned_nft.transactional_status =
-                TransactionalStatus::InitiatedOfferToMember(to, balance);
-        }
-        self
-    }
-
-    /// Whether pending tansfer exist
-    pub fn is_pending_offer_transactional_status(&self) -> bool {
-        matches!(
-            self.nft_status,
-            Some(OwnedNFT {
-                transactional_status: TransactionalStatus::InitiatedOfferToMember(..),
-                ..
-            })
-        )
-    }
-
-    /// Ensure NFT has pending offer
-    pub fn ensure_pending_offer_exists<T: Trait>(&self) -> DispatchResult {
-        ensure!(
-            self.is_pending_offer_transactional_status(),
-            Error::<T>::PendingTransferDoesNotExist
-        );
-        Ok(())
-    }
-
-    /// Ensure censorship status have been changed
-    pub fn ensure_censorship_status_changed<T: Trait>(&self, is_censored: bool) -> DispatchResult {
-        ensure!(
-            self.is_censored != is_censored,
-            Error::<T>::VideoCensorshipStatusDidNotChange
-        );
-        Ok(())
-    }
-}
-
-/// Video alias type for simplification.
-pub type Video<T> = VideoRecord<
-    <T as StorageOwnership>::ChannelId,
-    <T as Trait>::SeriesId,
-    <T as frame_system::Trait>::BlockNumber,
-    MemberId<T>,
-    CuratorGroupId<T>,
-    DAOId<T>,
-    BalanceOf<T>,
->;
+pub type VideoUpdateParameters<T> = VideoUpdateParametersRecord<NewAssets<T>>;
 
 /// Information about the plyalist being created.
 #[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
@@ -456,15 +222,15 @@ pub struct PlaylistUpdateParameters {
 #[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)]
 pub struct Playlist<ChannelId> {
     /// The channel the playlist belongs to.
-    pub in_channel: ChannelId,
+    in_channel: ChannelId,
 }
 
 /// Information about the episode being created or updated.
 #[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
 #[derive(Encode, Decode, Clone, PartialEq, Eq, Debug)]
-pub enum EpisodeParameters<VideoId, ContentParameters> {
+pub enum EpisodeParameters<VideoId, NewAssets> {
     /// A new video is being added as the episode.
-    NewVideo(VideoCreationParameters<ContentParameters>),
+    NewVideo(VideoCreationParametersRecord<NewAssets>),
     /// An existing video is being made into an episode.
     ExistingVideo(VideoId),
 }
@@ -472,49 +238,49 @@ pub enum EpisodeParameters<VideoId, ContentParameters> {
 /// Information about the season being created or updated.
 #[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
 #[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)]
-pub struct SeasonParameters<VideoId, ContentParameters> {
+pub struct SeasonParameters<VideoId, NewAssets> {
     /// Season assets referenced by metadata
-    pub assets: Option<Vec<NewAsset<ContentParameters>>>,
+    pub assets: Option<NewAssets>,
     // ?? It might just be more straighforward to always provide full list of episodes at cost of larger tx.
     /// If set, updates the episodes of a season. Extends the number of episodes in a season
     /// when length of new_episodes is greater than previously set. Last elements must all be
     /// 'Some' in that case.
     /// Will truncate existing season when length of new_episodes is less than previously set.
-    pub episodes: Option<Vec<Option<EpisodeParameters<VideoId, ContentParameters>>>>,
-    /// If set, Metadata update for season.
+    episodes: Option<Vec<Option<EpisodeParameters<VideoId, NewAssets>>>>,
+
     pub meta: Option<Vec<u8>>,
 }
 
 /// Information about the series being created or updated.
 #[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
 #[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)]
-pub struct SeriesParameters<VideoId, ContentParameters> {
+pub struct SeriesParameters<VideoId, NewAssets> {
     /// Series assets referenced by metadata
-    pub assets: Option<Vec<NewAsset<ContentParameters>>>,
+    pub assets: Option<NewAssets>,
     // ?? It might just be more straighforward to always provide full list of seasons at cost of larger tx.
     /// If set, updates the seasons of a series. Extend a series when length of seasons is
     /// greater than previoulsy set. Last elements must all be 'Some' in that case.
     /// Will truncate existing series when length of seasons is less than previously set.
-    pub seasons: Option<Vec<Option<SeasonParameters<VideoId, ContentParameters>>>>,
-    pub meta: Option<Vec<u8>>,
+    seasons: Option<Vec<Option<SeasonParameters<VideoId, NewAssets>>>>,
+    meta: Option<Vec<u8>>,
 }
 
 /// A season is an ordered list of videos (episodes).
 #[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
 #[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)]
 pub struct Season<VideoId> {
-    pub episodes: Vec<VideoId>,
+    episodes: Vec<VideoId>,
 }
 
 /// A series is an ordered list of seasons that belongs to a channel.
 #[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
 #[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)]
 pub struct Series<ChannelId, VideoId> {
-    pub in_channel: ChannelId,
-    pub seasons: Vec<Season<VideoId>>,
+    in_channel: ChannelId,
+    seasons: Vec<Season<VideoId>>,
 }
 
-// The actor the caller/origin is trying to act as for Person creation and update and delete calls.
+/// The actor the caller/origin is trying to act as for Person creation and update and delete calls.
 #[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
 #[derive(Encode, Decode, Clone, PartialEq, Eq, Debug)]
 pub enum PersonActor<MemberId, CuratorId> {
@@ -532,8 +298,8 @@ pub enum PersonController<MemberId> {
     Curators,
 }
 
-// Default trait implemented only because its used in Person which needs to implement a Default trait
-// since it is a StorageValue.
+/// Default trait implemented only because its used in Person which needs to implement a Default trait
+/// since it is a StorageValue.
 impl<MemberId: Default> Default for PersonController<MemberId> {
     fn default() -> Self {
         PersonController::Member(MemberId::default())
@@ -543,21 +309,21 @@ impl<MemberId: Default> Default for PersonController<MemberId> {
 /// Information for Person being created.
 #[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
 #[derive(Encode, Decode, Clone, PartialEq, Eq, Debug)]
-pub struct PersonCreationParameters<ContentParameters> {
+pub struct PersonCreationParameters<NewAssets> {
     /// Assets referenced by metadata
-    pub assets: Vec<NewAsset<ContentParameters>>,
+    pub assets: NewAssets,
     /// Metadata for person.
-    pub meta: Vec<u8>,
+    meta: Vec<u8>,
 }
 
 /// Information for Persion being updated.
 #[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
 #[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)]
-pub struct PersonUpdateParameters<ContentParameters> {
+pub struct PersonUpdateParameters<NewAssets> {
     /// Assets referenced by metadata
-    pub assets: Option<Vec<NewAsset<ContentParameters>>>,
+    pub assets: Option<NewAssets>,
     /// Metadata to update person.
-    pub new_meta: Option<Vec<u8>>,
+    new_meta: Option<Vec<u8>>,
 }
 
 /// A Person represents a real person that may be associated with a video.
@@ -567,3 +333,99 @@ pub struct Person<MemberId> {
     /// Who can update or delete this person.
     pub controlled_by: PersonController<MemberId>,
 }
+
+/// A category that videos can belong to.
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)]
+pub struct VideoCategory {
+    // No runtime information is currently stored for a Category.
+}
+
+/// Information about the video category being created.
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)]
+pub struct VideoCategoryCreationParameters {
+    /// Metadata about the video category.
+    pub meta: Vec<u8>,
+}
+
+/// A video which belongs to a channel. A video may be part of a series or playlist.
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)]
+pub struct VideoRecord<
+    ChannelId,
+    SeriesId,
+    DataObjectId: Ord,
+    BlockNumber: BaseArithmetic + Copy + Default,
+    MemberId: Default + Copy + Ord,
+    AccountId: Default + Clone + Ord,
+    Balance: Default + Clone + BaseArithmetic,
+> {
+    pub in_channel: ChannelId,
+    // keep track of which season the video is in if it is an 'episode'
+    // - prevent removing a video if it is in a season (because order is important)
+    pub in_series: Option<SeriesId>,
+    /// Whether the curators have censored the video or not.
+    pub is_censored: bool,
+    /// storage parameters used during deletion
+    pub maybe_data_objects_id_set: Option<BTreeSet<DataObjectId>>,
+    /// Whether nft for this video have been issued.
+    pub nft_status: Option<OwnedNFT<BlockNumber, MemberId, AccountId, Balance>>,
+}
+
+impl<
+        ChannelId: Clone,
+        SeriesId: Clone,
+        DataObjectId: Ord,
+        BlockNumber: BaseArithmetic + Copy + Default,
+        MemberId: Default + Copy + PartialEq + Ord,
+        AccountId: Default + Clone + PartialEq + Ord,
+        Balance: Clone + Default + BaseArithmetic,
+    > VideoRecord<ChannelId, SeriesId, DataObjectId, BlockNumber, MemberId, AccountId, Balance>
+{
+    /// Ensure nft is not issued
+    pub fn ensure_nft_is_not_issued<T: Trait>(&self) -> DispatchResult {
+        ensure!(self.nft_status.is_none(), Error::<T>::NFTAlreadyExists);
+        Ok(())
+    }
+
+    /// Ensure nft is issued
+    pub fn ensure_nft_is_issued<T: Trait>(
+        &self,
+    ) -> Result<OwnedNFT<BlockNumber, MemberId, AccountId, Balance>, Error<T>> {
+        if let Some(owned_nft) = &self.nft_status {
+            Ok(owned_nft.to_owned())
+        } else {
+            Err(Error::<T>::NFTDoesNotExist)
+        }
+    }
+
+    /// Set video nft status
+    pub fn set_nft_status(
+        mut self,
+        nft: OwnedNFT<BlockNumber, MemberId, AccountId, Balance>,
+    ) -> Self {
+        self.nft_status = Some(nft);
+        self
+    }
+
+    /// Ensure censorship status have been changed
+    pub fn ensure_censorship_status_changed<T: Trait>(&self, is_censored: bool) -> DispatchResult {
+        ensure!(
+            self.is_censored != is_censored,
+            Error::<T>::VideoCensorshipStatusDidNotChange
+        );
+        Ok(())
+    }
+}
+
+/// Video alias type for simplification.
+pub type Video<T> = VideoRecord<
+    <T as storage::Trait>::ChannelId,
+    <T as Trait>::SeriesId,
+    <T as storage::Trait>::DataObjectId,
+    <T as frame_system::Trait>::BlockNumber,
+    MemberId<T>,
+    <T as frame_system::Trait>::AccountId,
+    BalanceOf<T>,
+>;

+ 5 - 0
runtime-modules/governance/src/mock.rs

@@ -61,11 +61,13 @@ impl pallet_timestamp::Trait for Test {
     type MinimumPeriod = MinimumPeriod;
     type WeightInfo = ();
 }
+
 impl council::Trait for Test {
     type Event = ();
 
     type CouncilTermEnded = (Election,);
 }
+
 impl election::Trait for Test {
     type Event = ();
 
@@ -84,15 +86,18 @@ impl membership::Trait for Test {
     type ActorId = u32;
     type ScreenedMemberMaxInitialBalance = ScreenedMemberMaxInitialBalance;
 }
+
 impl minting::Trait for Test {
     type Currency = Balances;
     type MintId = u64;
 }
+
 impl recurringrewards::Trait for Test {
     type PayoutStatusHandler = ();
     type RecipientId = u64;
     type RewardRelationshipId = u64;
 }
+
 parameter_types! {
     pub const ExistentialDeposit: u32 = 0;
 }

+ 18 - 18
runtime-modules/membership/src/lib.rs

@@ -250,9 +250,9 @@ decl_event! {
       <T as Trait>::PaidTermId,
     {
         MemberRegistered(MemberId, AccountId, EntryMethod<PaidTermId, AccountId>),
-        MemberUpdatedAboutText(MemberId),
-        MemberUpdatedAvatar(MemberId),
-        MemberUpdatedHandle(MemberId),
+        MemberUpdatedAboutText(MemberId, Vec<u8>),
+        MemberUpdatedAvatar(MemberId, Vec<u8>),
+        MemberUpdatedHandle(MemberId, Vec<u8>),
         MemberSetRootAccount(MemberId, AccountId),
         MemberSetControllerAccount(MemberId, AccountId),
     }
@@ -312,7 +312,7 @@ decl_module! {
 
             ensure!(membership.controller_account == sender, Error::<T>::ControllerAccountRequired);
 
-            Self::_change_member_about_text(member_id, &text)?;
+            Self::_change_member_about_text(member_id, text)?;
         }
 
         /// Change member's avatar
@@ -324,7 +324,7 @@ decl_module! {
 
             ensure!(membership.controller_account == sender, Error::<T>::ControllerAccountRequired);
 
-            Self::_change_member_avatar(member_id, &uri)?;
+            Self::_change_member_avatar(member_id, uri)?;
         }
 
         /// Change member's handle. Will ensure new handle is unique and old one will be available
@@ -356,10 +356,10 @@ decl_module! {
             ensure!(membership.controller_account == sender, Error::<T>::ControllerAccountRequired);
 
             if let Some(uri) = avatar_uri {
-                Self::_change_member_avatar(member_id, &uri)?;
+                Self::_change_member_avatar(member_id, uri)?;
             }
             if let Some(about) = about {
-                Self::_change_member_about_text(member_id, &about)?;
+                Self::_change_member_about_text(member_id, about)?;
             }
             if let Some(handle) = handle {
                 Self::_change_member_handle(member_id, handle)?;
@@ -588,8 +588,8 @@ impl<T: Trait> Module<T> {
         Ok(())
     }
 
-    fn validate_text(text: &[u8]) -> Vec<u8> {
-        let mut text = text.to_owned();
+    fn validate_text(text: Vec<u8>) -> Vec<u8> {
+        let mut text = text;
         text.truncate(Self::max_about_text_length() as usize);
         text
     }
@@ -612,7 +612,7 @@ impl<T: Trait> Module<T> {
         let handle = handle.ok_or(Error::<T>::HandleMustBeProvidedDuringRegistration)?;
         Self::validate_handle(&handle)?;
 
-        let about = Self::validate_text(&about.unwrap_or_default());
+        let about = Self::validate_text(about.unwrap_or_default());
         let avatar_uri = avatar_uri.unwrap_or_default();
         Self::validate_avatar(&avatar_uri)?;
 
@@ -662,20 +662,20 @@ impl<T: Trait> Module<T> {
         Ok(new_member_id)
     }
 
-    fn _change_member_about_text(id: T::MemberId, text: &[u8]) -> DispatchResult {
+    fn _change_member_about_text(id: T::MemberId, text: Vec<u8>) -> DispatchResult {
         let mut membership = Self::ensure_membership(id)?;
         let text = Self::validate_text(text);
-        membership.about = text;
-        Self::deposit_event(RawEvent::MemberUpdatedAboutText(id));
+        membership.about = text.clone();
+        Self::deposit_event(RawEvent::MemberUpdatedAboutText(id, text));
         <MembershipById<T>>::insert(id, membership);
         Ok(())
     }
 
-    fn _change_member_avatar(id: T::MemberId, uri: &[u8]) -> DispatchResult {
+    fn _change_member_avatar(id: T::MemberId, uri: Vec<u8>) -> DispatchResult {
         let mut membership = Self::ensure_membership(id)?;
-        Self::validate_avatar(uri)?;
+        Self::validate_avatar(&uri)?;
         membership.avatar_uri = uri.to_owned();
-        Self::deposit_event(RawEvent::MemberUpdatedAvatar(id));
+        Self::deposit_event(RawEvent::MemberUpdatedAvatar(id, uri));
         <MembershipById<T>>::insert(id, membership);
         Ok(())
     }
@@ -686,8 +686,8 @@ impl<T: Trait> Module<T> {
         Self::ensure_unique_handle(&handle)?;
         <MemberIdByHandle<T>>::remove(&membership.handle);
         <MemberIdByHandle<T>>::insert(handle.clone(), id);
-        membership.handle = handle;
-        Self::deposit_event(RawEvent::MemberUpdatedHandle(id));
+        membership.handle = handle.clone();
+        Self::deposit_event(RawEvent::MemberUpdatedHandle(id, handle));
         <MembershipById<T>>::insert(id, membership);
         Ok(())
     }

+ 25 - 7
runtime-modules/storage/src/lib.rs

@@ -118,6 +118,8 @@
 
 // Internal Substrate warning (decl_event).
 #![allow(clippy::unused_unit)]
+// needed for step iteration over DataObjectId range
+#![feature(step_trait)]
 
 #[cfg(test)]
 mod tests;
@@ -216,6 +218,9 @@ pub trait Trait: frame_system::Trait + balances::Trait + membership::Trait {
     /// Storage event type.
     type Event: From<Event<Self>> + Into<<Self as frame_system::Trait>::Event>;
 
+    /// Content id representation.
+    type ContentId: Parameter + Member + Codec + Default + Copy + MaybeSerialize + Ord + PartialEq;
+
     /// Data object ID type.
     type DataObjectId: Parameter
         + Member
@@ -224,7 +229,8 @@ pub trait Trait: frame_system::Trait + balances::Trait + membership::Trait {
         + Default
         + Copy
         + MaybeSerialize
-        + PartialEq;
+        + PartialEq
+        + iter::Step; // needed for iteration
 
     /// Storage bucket ID type.
     type StorageBucketId: Parameter
@@ -462,7 +468,7 @@ pub type BalanceOf<T> = <T as balances::Trait>::Balance;
 #[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
 #[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)]
 pub struct DataObject<Balance> {
-    /// Defines whether the data object was accepted by a liaison.
+    /// Defines whether the data object was accepted by a liason.
     pub accepted: bool,
 
     /// A reward for the data object deletion.
@@ -932,7 +938,7 @@ decl_storage! {
         /// "Max objects size for a storage bucket voucher" number limit.
         pub VoucherMaxObjectsSizeLimit get (fn voucher_max_objects_size_limit): u64;
 
-        /// "Max objects number for a storage bucket voucher" number limit.
+        /// "Max objects number for a storage  bucket voucher" number limit.
         pub VoucherMaxObjectsNumberLimit get (fn voucher_max_objects_number_limit): u64;
 
         /// DynamicBagCreationPolicy by bag type storage map.
@@ -1103,7 +1109,14 @@ decl_event! {
         /// Params
         /// - dynamic bag ID
         /// - optional DynamicBagDeletionPrize instance
-        DynamicBagCreated(DynamicBagId, Option<DynamicBagDeletionPrizeRecord<AccountId, Balance>>),
+        /// - assigned storage buckets' IDs
+        /// - assigned distribution buckets' IDs
+        DynamicBagCreated(
+            DynamicBagId,
+            Option<DynamicBagDeletionPrizeRecord<AccountId, Balance>>,
+            BTreeSet<StorageBucketId>,
+            BTreeSet<DistributionBucketId>,
+        ),
 
         /// Emits on changing the voucher for a storage bucket.
         /// Params
@@ -2716,9 +2729,9 @@ impl<T: Trait> DataObjectStorage<T> for Module<T> {
         let distribution_buckets = Self::pick_distribution_buckets_for_dynamic_bag(bag_type);
 
         let bag = Bag::<T> {
-            stored_by: storage_buckets,
+            stored_by: storage_buckets.clone(),
             deletion_prize: deletion_prize.clone().map(|dp| dp.prize),
-            distributed_by: distribution_buckets,
+            distributed_by: distribution_buckets.clone(),
             ..Default::default()
         };
 
@@ -2726,7 +2739,12 @@ impl<T: Trait> DataObjectStorage<T> for Module<T> {
 
         <Bags<T>>::insert(&bag_id, bag);
 
-        Self::deposit_event(RawEvent::DynamicBagCreated(dynamic_bag_id, deletion_prize));
+        Self::deposit_event(RawEvent::DynamicBagCreated(
+            dynamic_bag_id,
+            deletion_prize,
+            storage_buckets,
+            distribution_buckets,
+        ));
 
         Ok(())
     }

+ 1 - 0
runtime-modules/storage/src/tests/mocks.rs

@@ -103,6 +103,7 @@ impl crate::Trait for Test {
     type MaxNumberOfPendingInvitationsPerDistributionBucket =
         MaxNumberOfPendingInvitationsPerDistributionBucket;
     type MaxDataObjectSize = MaxDataObjectSize;
+    type ContentId = u64;
 
     fn ensure_storage_working_group_leader_origin(origin: Self::Origin) -> DispatchResult {
         let account_id = ensure_signed(origin)?;

+ 10 - 6
runtime-modules/storage/src/tests/mod.rs

@@ -3098,19 +3098,16 @@ fn create_dynamic_bag_succeeded() {
             .with_deletion_prize(deletion_prize.clone())
             .call_and_assert(Ok(()));
 
-        EventFixture::assert_last_crate_event(RawEvent::DynamicBagCreated(
-            dynamic_bag_id.clone(),
-            Some(deletion_prize),
-        ));
-
         let bag = Storage::dynamic_bag(&dynamic_bag_id);
+
         // Check that IDs are within possible range.
         assert!(bag
             .stored_by
             .iter()
             .all(|id| { *id < Storage::next_storage_bucket_id() }));
 
-        let creation_policy = Storage::get_dynamic_bag_creation_policy(dynamic_bag_id.into());
+        let creation_policy =
+            Storage::get_dynamic_bag_creation_policy(dynamic_bag_id.clone().into());
         assert_eq!(
             bag.stored_by.len(),
             creation_policy.number_of_storage_buckets as usize
@@ -3127,6 +3124,13 @@ fn create_dynamic_bag_succeeded() {
             Balances::usable_balance(&<StorageTreasury<Test>>::module_account_id()),
             deletion_prize_value
         );
+
+        EventFixture::assert_last_crate_event(RawEvent::DynamicBagCreated(
+            dynamic_bag_id,
+            Some(deletion_prize),
+            BTreeSet::from_iter(bag.stored_by),
+            BTreeSet::from_iter(bag.distributed_by),
+        ));
     });
 }
 

+ 1 - 34
runtime/src/lib.rs

@@ -75,7 +75,6 @@ pub use pallet_staking::StakerStatus;
 pub use proposals_codex::ProposalsConfigParameters;
 pub use working_group;
 
-use common::storage::{ContentParameters, StorageObjectOwner};
 pub use content;
 pub use content::MaxNumber;
 
@@ -443,38 +442,6 @@ impl content::Trait for Runtime {
     type SeriesId = SeriesId;
     type ChannelOwnershipTransferRequestId = ChannelOwnershipTransferRequestId;
     type MaxNumberOfCuratorsPerGroup = MaxNumberOfCuratorsPerGroup;
-    type StorageSystem = (); // TODO: Add storage integration
-}
-
-// TODO: Remove after the integration with the Content pallet.
-impl common::storage::StorageSystem<Runtime, MemberId> for () {
-    fn atomically_add_content(
-        _: StorageObjectOwner<MemberId, ChannelId, DAOId>,
-        _: Vec<ContentParameters<ContentId, DataObjectTypeId>>,
-    ) -> sp_runtime::DispatchResult {
-        todo!()
-    }
-
-    fn can_add_content(
-        _: StorageObjectOwner<MemberId, ChannelId, DAOId>,
-        _: Vec<ContentParameters<ContentId, DataObjectTypeId>>,
-    ) -> sp_runtime::DispatchResult {
-        todo!()
-    }
-
-    fn atomically_remove_content(
-        _: &StorageObjectOwner<MemberId, ChannelId, DAOId>,
-        _: &[ContentId],
-    ) -> sp_runtime::DispatchResult {
-        todo!()
-    }
-
-    fn can_remove_content(
-        _: &StorageObjectOwner<MemberId, ChannelId, DAOId>,
-        _: &[ContentId],
-    ) -> sp_runtime::DispatchResult {
-        todo!()
-    }
 }
 
 impl hiring::Trait for Runtime {
@@ -531,7 +498,6 @@ impl common::MembershipTypes for Runtime {
 
 impl common::StorageOwnership for Runtime {
     type ChannelId = ChannelId;
-    type DAOId = DAOId;
     type ContentId = ContentId;
     type DataObjectTypeId = DataObjectTypeId;
 }
@@ -723,6 +689,7 @@ impl storage::Trait for Runtime {
     type MaxNumberOfPendingInvitationsPerDistributionBucket =
         MaxNumberOfPendingInvitationsPerDistributionBucket;
     type MaxDataObjectSize = MaxDataObjectSize;
+    type ContentId = ContentId;
 
     fn ensure_storage_working_group_leader_origin(origin: Self::Origin) -> DispatchResult {
         StorageWorkingGroup::ensure_origin_is_active_leader(origin)

+ 0 - 4
types/augment-codec/augment-api-errors.ts

@@ -70,10 +70,6 @@ declare module '@polkadot/api/types/errors' {
       VestingBalance: AugmentedError<ApiType>;
     };
     content: {
-      /**
-       * This content actor cannot own a channel
-       **/
-      ActorCannotOwnChannel: AugmentedError<ApiType>;
       /**
        * Operation cannot be perfomed with this Actor
        **/

+ 0 - 4
types/augment/augment-api-errors.ts

@@ -70,10 +70,6 @@ declare module '@polkadot/api/types/errors' {
       VestingBalance: AugmentedError<ApiType>;
     };
     content: {
-      /**
-       * This content actor cannot own a channel
-       **/
-      ActorCannotOwnChannel: AugmentedError<ApiType>;
       /**
        * Operation cannot be perfomed with this Actor
        **/

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