diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 778053e0..f067bb19 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -6,10 +6,11 @@ "lldb.executable": "/usr/bin/lldb" }, "extensions": [ - "rust-lang.rust", - "bungcip.better-toml", - "vadimcn.vscode-lldb" - ], + "rust-lang.rust", + "bungcip.better-toml", + "vadimcn.vscode-lldb", + "rust-lang.rust-analyzer" +], "forwardPorts": [ 3000, 9944 diff --git a/Cargo.lock b/Cargo.lock index 9907b0fb..1917e6a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2147,6 +2147,7 @@ dependencies = [ "pallet-nbv-storage", "pallet-node-authorization", "pallet-randomness-collective-flip", + "pallet-rbac", "pallet-recovery", "pallet-society", "pallet-sudo", @@ -4045,6 +4046,11 @@ dependencies = [ "frame-support", "frame-system", "log", + "pallet-balances", + "pallet-fruniques", + "pallet-rbac", + "pallet-timestamp", + "pallet-uniques", "parity-scale-codec", "scale-info", "serde", @@ -4175,6 +4181,21 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-rbac" +version = "4.0.0-dev" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", +] + [[package]] name = "pallet-recovery" version = "4.0.0-dev" diff --git a/node/src/chain_spec.rs b/node/src/chain_spec.rs index ab4ac07b..3fdce072 100644 --- a/node/src/chain_spec.rs +++ b/node/src/chain_spec.rs @@ -1,6 +1,6 @@ use hashed_runtime::{ AccountId, AuraConfig, BalancesConfig, CouncilConfig, GenesisConfig, GrandpaConfig, Signature, - SudoConfig, SystemConfig, NodeAuthorizationConfig, NBVStorageConfig ,WASM_BINARY, + SudoConfig, SystemConfig, NodeAuthorizationConfig, NBVStorageConfig, WASM_BINARY, }; use sc_chain_spec::Properties; use sc_service::ChainType; @@ -279,6 +279,6 @@ fn testnet_genesis( transaction_payment: Default::default(), nbv_storage : NBVStorageConfig{ bdk_services_url : BDK_SERVICES_MAINNET_URL.as_bytes().to_vec(), - } + }, } } diff --git a/pallets/fruniques/src/tests.rs b/pallets/fruniques/src/tests.rs index 328e142d..f722cfdc 100644 --- a/pallets/fruniques/src/tests.rs +++ b/pallets/fruniques/src/tests.rs @@ -1,6 +1,6 @@ use crate::{mock::*, Error}; -use frame_support::{assert_err, assert_noop, assert_ok}; +use frame_support::{assert_noop, assert_ok}; use sp_runtime::Permill; pub struct ExtBuilder; diff --git a/pallets/gated-marketplace/Cargo.toml b/pallets/gated-marketplace/Cargo.toml index d4e34fcb..d9ae3cb4 100644 --- a/pallets/gated-marketplace/Cargo.toml +++ b/pallets/gated-marketplace/Cargo.toml @@ -25,6 +25,12 @@ frame-benchmarking = { default-features = false, version = "4.0.0-dev", git = "h sp-runtime = { default-features = false, version = "6.0.0", git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.23" } +pallet-balances = { default-features = false, version = "4.0.0-dev", git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.23" } +pallet-uniques = { default-features = false, version = "4.0.0-dev", git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.23" } +pallet-timestamp = { default-features = false, version = "4.0.0-dev", git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.23" } +pallet-fruniques = {path = "../fruniques", default-features = false, version = "0.1.0-dev"} +pallet-rbac = { default-features = false, version = "4.0.0-dev", path="../rbac/"} + [dev-dependencies] sp-core = { default-features = false, version = "6.0.0", git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.23" } sp-io = { default-features = false, version = "6.0.0", git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.23" } @@ -39,6 +45,11 @@ std = [ "frame-support/std", "frame-system/std", "frame-benchmarking/std", + "pallet-balances/std", + "pallet-uniques/std", + "pallet-fruniques/std", + "pallet-timestamp/std", + "pallet-rbac/std" ] runtime-benchmarks = ["frame-benchmarking/runtime-benchmarks"] diff --git a/pallets/gated-marketplace/README.md b/pallets/gated-marketplace/README.md index 3d885d7b..54f3d7e9 100644 --- a/pallets/gated-marketplace/README.md +++ b/pallets/gated-marketplace/README.md @@ -9,10 +9,11 @@ Create marketplaces that require previous authorization before placing sell and - [Getters](#getters) - [Usage](#usage) - [Polkadot-js CLI](#polkadot-js-cli) + - [Submit initial role setup (needs sudo](#submit-initial-role-setup-needs-sudo) - [Create a marketplace](#create-a-marketplace) - [Get a marketplace](#get-a-marketplace) - - [Get what permissions does an account have on a marketplace](#get-what-permissions-does-an-account-have-on-a-marketplace) - - [Get all the accounts that have a certain permission on a marketplace](#get-all-the-accounts-that-have-a-certain-permission-on-a-marketplace) + - [Get what roles does an account have on a marketplace](#get-what-roles-does-an-account-have-on-a-marketplace) + - [Get all the accounts that have a certain role on a marketplace](#get-all-the-accounts-that-have-a-certain-role-on-a-marketplace) - [Apply to a marketplace (without custodian)](#apply-to-a-marketplace-without-custodian) - [Apply to a marketplace (with custodian)](#apply-to-a-marketplace-with-custodian) - [Get an application](#get-an-application) @@ -23,11 +24,22 @@ Create marketplaces that require previous authorization before placing sell and - [Enroll an applicant (by its application id)](#enroll-an-applicant-by-its-application-id) - [Add authority user to marketplace](#add-authority-user-to-marketplace) - [Remove authority user to marketplace](#remove-authority-user-to-marketplace) + - [Put an asset on sale](#put-an-asset-on-sale) + - [Put a buy offer](#put-a-buy-offer) + - [Get offer details](#get-offer-details) + - [Get offers by item](#get-offers-by-item) + - [Get offers by account](#get-offers-by-account) + - [Get offers by marketplace](#get-offers-by-marketplace) + - [Duplicate offer](#duplicate-offer) + - [Remove offer](#remove-offer) + - [Take sell offer - direct purchase](#take-sell-offer---direct-purchase) + - [Take buy offer](#take-buy-offer) - [Polkadot-js api (javascript library)](#polkadot-js-api-javascript-library) + - [Submit initial role setup (needs sudo)](#submit-initial-role-setup-needs-sudo-1) - [Create a marketplace](#create-a-marketplace-1) - [Get a marketplace](#get-a-marketplace-1) - - [Get what permissions does an account have on a marketplace](#get-what-permissions-does-an-account-have-on-a-marketplace-1) - - [Get all the accounts that have a certain permission on a marketplace](#get-all-the-accounts-that-have-a-certain-permission-on-a-marketplace-1) + - [Get what roles does an account have on a marketplace](#get-what-roles-does-an-account-have-on-a-marketplace-1) + - [Get all the accounts that have a certain permission on a marketplace](#get-all-the-accounts-that-have-a-certain-permission-on-a-marketplace) - [Apply to a marketplace (without custodian)](#apply-to-a-marketplace-without-custodian-1) - [Apply to a marketplace (with custodian)](#apply-to-a-marketplace-with-custodian-1) - [Get an application](#get-an-application-1) @@ -38,6 +50,16 @@ Create marketplaces that require previous authorization before placing sell and - [Enroll an applicant (by its application id)](#enroll-an-applicant-by-its-application-id-1) - [Add authority user to marketplace](#add-authority-user-to-marketplace-1) - [Remove authority user to marketplace](#remove-authority-user-to-marketplace-1) + - [Put an asset on sale](#put-an-asset-on-sale-1) + - [Put a buy offer](#put-a-buy-offer-1) + - [Get offer details](#get-offer-details-1) + - [Get offers by item](#get-offers-by-item-1) + - [Get offers by account](#get-offers-by-account-1) + - [Get offers by marketplace](#get-offers-by-marketplace-1) + - [Duplicate offer in another marketplace](#duplicate-offer-in-another-marketplace) + - [Remove offer](#remove-offer-1) + - [Take sell offer - direct purchase](#take-sell-offer---direct-purchase-1) + - [Take buy offer](#take-buy-offer-1) - [Events](#events) - [Errors](#errors) @@ -49,6 +71,7 @@ This module allows to: - Enroll or reject applicants to your marketplace. - Add or remove users as supported authorities to your marketplace, like administrators and/or asset appraisers - WIP: Assign a rating to assets as an Appraiser. +- Create sell or buy orders. Users can bid on the item if they don't like the sale price. ### Terminology - **Authority**: Refers to any user that has special faculties within the marketplace, like enroll new users or grade assets. @@ -60,28 +83,42 @@ This module allows to: - **Enroll**: When enrolled, the user's application becomes approved, gaining the ability to sell and buy assets. - **Reject**: If the user gets rejected, its application becomes rejected and won't have access to the marketplace. - **Custodian**: When submitting an application, the user can optionally specify a third account that will have access to the confidential documents. A custodian doesn't need to be an authority nor being part of the marketplace. +- **Sell order**: The owner of the item creates sales offer fot the item. +- **Buy order**: Users from the marketplace can bid for the item. ## Interface ### Dispachable functions + +- `initial_setup` enables all the permission related functionality using the `RBAC` pallet, it can only be called by the sudo account or a majority of the Council (60%). It is essential to call this extrinsic before using other extrinsics. - `create_marketplace` creates an on-chain marketplace record, it takes a `label` and an account that will fulfill the role of `administrator`, the extrinsic origin will be set up automatically as the marketplace owner. -- `apply` starts the process to enter the specidied `marketplace`. +- `apply` starts the process to enter the specified `marketplace`. - `reapply` allows the applicant to apply again for the selected marketplace. - `enroll` is only callable by the marketplace owner or administrator, as it finishes the application process. It takes a `marketplace` identification, and `account` or `application` identification to enroll or reject, and an `approved` boolean flag which approves the application if set to `true`. Owner/admin can add a feedback regarding the user's application. - `add_authority` is only callable by the marketplace owner or administrator. As it name implies, adds a new user that will have special permission within the marketplace. It takes the `account` which will have the permissions, the type of `authority` it will have, and the `marketplace` identification in which the permissions will be enforced. - `remove_authority` is only callable by the marketplace owner or administrator. Removes the authority enforcer from the marketplace. The marketplace owner cannot be removed and the administrator cannot remove itself. - `update_label_marketplace` is only callable by the marketplace owner or administrator. Changes the marketplace label. If the new label already exists, the old name won't be changed. - `remove_marketplace` is only callable by the marketplace owner or administrator. This action allows the user to remove a marketplace as well as all the information related to this marketplace. +- `enlist_sell_offer` is only callable by the owner of the item. It allows the user to sell an item in the selected marketplace. +- `take_sell_offer` any user interested to buy the item can call this extrinsic. User must have enough balance to buy it. When the transaction is completed, the item ownership is transferred to the buyer. +- `duplicate_offer` allows the owner of the item to duplicate an sell order in any marketplace. +- `remove_offer` is only callable by the creator of the offer, it deletes any offer type from all the storages. +- `enlist_buy_offer` is callable by any market participant, the owner of the item can't create buy orders for their own items. User must have the enough balance to call it. +- `take_buy_offer` is only callable by the owner of the item. If the owner accepts the offer, the buyer must have enough balance to finalize the transaction. ### Getters -- `marketplaces` -- `marketplaces_by_authority` (double storage map) -- `authorities_by_marketplace` (double storage map) -- `applications` -- `applications_by_account` (double storage map) -- `applicants_by_marketplace` (double storage map) -- `custodians` (double storage map) +|Name| Type | +|--|--| +|`marketplaces`| storage map| +|`applications`| storage map| +|`applications_by_account`|double storage map| +|`applicants_by_marketplace`|double storage map| +|`custodians`|double storage map| +|`offers_info` |storage map| +|`offers_by_item`|double storage map| +|`offers_by_account`|storage map| +|`offers_by_marketplace`|storage map| ## Usage @@ -112,6 +149,11 @@ The following examples will be using these prefunded accounts and testing data: ### Polkadot-js CLI +#### Submit initial role setup (needs sudo +```bash +polkadot-js-api tx.gatedMarketplace.initialSetup --sudo --seed "//Alice" +``` + #### Create a marketplace ```bash # Administrator account and marketplace label @@ -131,30 +173,30 @@ polkadot-js-api query.gatedMarketplace.marketplaces "0xace33a53e2c1a5c7fa2f92033 } ``` -#### Get what permissions does an account have on a marketplace +#### Get what roles does an account have on a marketplace ```bash -# account_id, marketplace_id -polkadot-js-api query.gatedMarketplace.marketplacesByAuthority "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" "0xace33a53e2c1a5c7fa2f920338136d0ddc3aba23eacaf708e3871bc856a34b95" +# account_id, pallet_id, marketplace_id +polkadot-js-api query.rbac.rolesByUser "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" 20 "0xace33a53e2c1a5c7fa2f920338136d0ddc3aba23eacaf708e3871bc856a34b95" ``` ```bash # Output should look like this: { - "marketplacesByAuthority": [ - "Owner" + "rolesByUser": [ + "0xc1237f9841c265fb722178da01a1e088c25fb892d6b7cd9634a20ac84bb3ee01" ] } ``` -#### Get all the accounts that have a certain permission on a marketplace +#### Get all the accounts that have a certain role on a marketplace ```bash -# marketplace_id, type of authoriry (it can be "Owner", "Admin" or "Appraiser") -polkadot-js-api query.gatedMarketplace.authoritiesByMarketplace "0xace33a53e2c1a5c7fa2f920338136d0ddc3aba23eacaf708e3871bc856a34b95" "Admin" +# pallet_id, marketplace_id, role_id +polkadot-js-api query.rbac.usersByScope 20 "0xace33a53e2c1a5c7fa2f920338136d0ddc3aba23eacaf708e3871bc856a34b95" "0x08aef7203969e2467b33b14965dfab62e11b085610c798b3cac150b1d7ea033b" ``` ```bash # Output should look like this: { - "authoritiesByMarketplace": [ - "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" + "usersByScope": [ + "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" ] } ``` @@ -236,15 +278,15 @@ polkadot-js-api query.gatedMarketplace.custodians "5DAAnrj7VHTznn2AWBemMuyBwZWs6 #### Enroll an applicant (by its account) ```bash # It can only be called by the marketplace owner (Alice) or administrator (Bob) -# market_id, accountOrApplicationEnumerator, approve boolean -polkadot-js-api tx.gatedMarketplace.enroll "0xace33a53e2c1a5c7fa2f920338136d0ddc3aba23eacaf708e3871bc856a34b95" '{"Account":"5FLSigC9HGRKVhB9FiEo4Y3koPsNmBmLJbpXg2mp1hXcS59Y"}' true --seed "//Bob" +# market_id, accountOrApplicationEnumerator, feedback, approve boolean +polkadot-js-api tx.gatedMarketplace.enroll "0xace33a53e2c1a5c7fa2f920338136d0ddc3aba23eacaf708e3871bc856a34b95" '{"Account":"5FLSigC9HGRKVhB9FiEo4Y3koPsNmBmLJbpXg2mp1hXcS59Y"}' "feedback" true --seed "//Bob" ``` #### Enroll an applicant (by its application id) ```bash # It can be called by the marketplace owner (Alice) or administrator (Bob) # market_id, accountOrApplicationEnumerator, approve boolean -polkadot-js-api tx.gatedMarketplace.enroll "0xace33a53e2c1a5c7fa2f920338136d0ddc3aba23eacaf708e3871bc856a34b95" '{"Application":"0x9ab75a44b507c0030296dd3660bd77d606807cf3415c3409b88c2cad36fd5483"}' true --seed "//Bob" +polkadot-js-api tx.gatedMarketplace.enroll "0xace33a53e2c1a5c7fa2f920338136d0ddc3aba23eacaf708e3871bc856a34b95" '{"Application":"0x9ab75a44b507c0030296dd3660bd77d606807cf3415c3409b88c2cad36fd5483"}' "feedback" true --seed "//Bob" ``` #### Add authority user to marketplace @@ -261,8 +303,120 @@ polkadot-js-api tx.gatedMarketplace.addAuthority "5DAAnrj7VHTznn2AWBemMuyBwZWs6F polkadot-js-api tx.gatedMarketplace.removeAuthority "5DAAnrj7VHTznn2AWBemMuyBwZWs6FNFjdyVXUeYum3PTXFy" "Appraiser" "0xace33a53e2c1a5c7fa2f920338136d0ddc3aba23eacaf708e3871bc856a34b95" --seed "//Alice" ``` +#### Put an asset on sale +```bash +# marketplace_id, collection_id, item_id, sell price +polkadot-js-api tx.gatedMarketplace.enlistSellOffer "0xace33a53e2c1a5c7fa2f920338136d0ddc3aba23eacaf708e3871bc856a34b95" 0 0 10000 --seed "//Charlie" +``` + +#### Put a buy offer +```bash +# marketplace_id, collection_id, item_id, buy price +polkadot-js-api tx.gatedMarketplace.enlistBuyOffer "0xace33a53e2c1a5c7fa2f920338136d0ddc3aba23eacaf708e3871bc856a34b95" 0 0 10001 --seed "//Dave" +``` + +#### Get offer details +```bash +polkadot-js-api query.gatedMarketplace.offersInfo "0x9abbb3e227dedf26a4a64705ffb924ef8d48dc47de981f4db799790ae2239e6b" +``` + +```bash +# Output should look like this +{ + "offersInfo": { + "marketplaceId": "0xace33a53e2c1a5c7fa2f920338136d0ddc3aba23eacaf708e3871bc856a34b95", + "collectionId": "0", + "itemId": "0", + "creator": "5FLSigC9HGRKVhB9FiEo4Y3koPsNmBmLJbpXg2mp1hXcS59Y", + "price": "10,000", + "status": "Open", + "creationDate": "1,660,778,892,000", + "expirationDate": "1,661,383,692,000", + "offerType": "SellOrder", + "buyer": null + } +} +``` + +#### Get offers by item +```bash +# collection_id, item_id +polkadot-js-api query.gatedMarketplace.offersByItem 0 0 +``` + +```bash +# Output should look similar +{ + "offersByItem": [ + "0x4508b428b15e1a0a0138d36efebe3382739726beca8d67239e02a56c19d378eb", + "0x66fcfadc174a596d8f8dc1b067038ed0056c5c3127d6996bc54fa05148caccf0" + ] +} +``` + +#### Get offers by account +```bash +# account_id +polkadot-js-api query.gatedMarketplace.offersByAccount 5FLSigC9HGRKVhB9FiEo4Y3koPsNmBmLJbpXg2mp1hXcS59Y +``` + +```bash +{ + "offersByAccount": [ + "0x4508b428b15e1a0a0138d36efebe3382739726beca8d67239e02a56c19d378eb" + ] +} +``` + + +#### Get offers by marketplace +```bash +# marketplace_id +polkadot-js-api query.gatedMarketplace.offersByMarketplace 0xace33a53e2c1a5c7fa2f920338136d0ddc3aba23eacaf708e3871bc856a34b95 +``` + +```bash +# Output should llok like this +{ + "offersByMarketplace": [ + "0x4508b428b15e1a0a0138d36efebe3382739726beca8d67239e02a56c19d378eb", + "0x66fcfadc174a596d8f8dc1b067038ed0056c5c3127d6996bc54fa05148caccf0" + ] +} +``` + +#### Duplicate offer + +```bash +polkadot-js-api tx.gatedMarketplace.duplicateOffer "0x65c7f4fa353a2212c2db497a8a1ad073453aad2030be7f756cba42a2f976dc82" "0xace33a53e2c1a5c7fa2f920338136d0ddc3aba23eacaf708e3871bc856a34b95" 0 0 10002 --seed "//Charlie" +``` + +#### Remove offer + +```bash +# offer_id, marketplace_id, collection_id, item_id +polkadot-js-api tx.gatedMarketplace.removeOffer "0x8cb8cc124e19fc58eaf9c6dbd0953a7fd955769e6d3983ce2ea83d64d742a62e" "0xa1c17609528fe2630b3be72d6ac8eafc5e0ef95ce78ddad70e83e5fa77ac7342" 0 0 --seed "//Charlie" +``` + +#### Take sell offer - direct purchase +```bash +polkadot-js-api tx.gatedMarketplace.takeSellOffer "0x65c7f4fa353a2212c2db497a8a1ad073453aad2030be7f756cba42a2f976dc82" "0xace33a53e2c1a5c7fa2f920338136d0ddc3aba23eacaf708e3871bc856a34b95" 0 0 --seed "//Dave" +``` + +#### Take buy offer +```bash +# offer_id, marketplace_id, collection_id, item_id +polkadot-js-api tx.gatedMarketplace.takeBuyOffer "0x66fcfadc174a596d8f8dc1b067038ed0056c5c3127d6996bc54fa05148caccf0" "0xace33a53e2c1a5c7fa2f920338136d0ddc3aba23eacaf708e3871bc856a34b95" 0 0 --seed "//Charlie" +``` + ### Polkadot-js api (javascript library) -While most of the data flow is almost identical to its CLI counter part, the javascript library is much more versatile regarding queries. The API setup will be ommited. +While most of the data flow is almost identical to its CLI counter part, the javascript library is much more versatile regarding queries. + + +#### Submit initial role setup (needs sudo) +```js +const initial_set_up = await api.tx.sudo.sudo(api.tx.gatedMarketplace.initialSetup()).signAndSend(alice); +``` #### Create a marketplace ```js @@ -295,44 +449,58 @@ key marketplace_id: [ marketplace: { label: 'my marketplace' } ``` -#### Get what permissions does an account have on a marketplace +#### Get what roles does an account have on a marketplace ```js -const marketplacesByAuth = await api.query.gatedMarketplace.marketplacesByAuthority("5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY","0xace33a53e2c1a5c7fa2f920338136d0ddc3aba23eacaf708e3871bc856a34b95"); - console.log(marketplacesByAuth.toHuman() ); +// account_id, pallet_id, scope_id +const rolesByUser = await api.query.rbac.rolesByUser(alice.address,20, "0xace33a53e2c1a5c7fa2f920338136d0ddc3aba23eacaf708e3871bc856a34b95"); + console.log(rolesByUser.toHuman()); ``` ```bash # Output should look like this: -[ 'Owner' ] +['0x08aef7203969e2467b33b14965dfab62e11b085610c798b3cac150b1d7ea033b'] ``` ```js // get all users permissions on all marketplaces -const allMarketplacesByAuth = await api.query.gatedMarketplace.marketplacesByAuthority.entries(); -allMarketplacesByAuth.forEach(([key, exposure]) => { - console.log('Authority account and marketplace_id:', key.args.map((k) => k.toHuman())); - console.log(' type of authority:', exposure.toHuman(),"\n"); +const all_roles_by_user = await api.query.rbac.rolesByUser.entries(); +all_roles_by_user.forEach(([key, exposure]) => { + console.log('account_id, pallet_id, scope_id:', key.args.map((k) => k.toHuman())); + console.log(' role_ids', exposure.toHuman()); }); ``` ```bash -Authority account and marketplace_id: [ +account_id, pallet_id, scope_id: [ '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty', + '20', '0xace33a53e2c1a5c7fa2f920338136d0ddc3aba23eacaf708e3871bc856a34b95' ] - type of authority: [ 'Admin' ] - -Authority account and marketplace_id: [ + role_ids [ + '0xc1237f9841c265fb722178da01a1e088c25fb892d6b7cd9634a20ac84bb3ee01' +] +account_id, pallet_id, scope_id: [ + '5FLSigC9HGRKVhB9FiEo4Y3koPsNmBmLJbpXg2mp1hXcS59Y', + '20', + '0xace33a53e2c1a5c7fa2f920338136d0ddc3aba23eacaf708e3871bc856a34b95' +] + role_ids [ + '0xae9e025522f868c39b41b8a5ba513335a2a229690bd44c71c998d5a9ad38162b' +] +account_id, pallet_id, scope_id: [ '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', + '20', '0xace33a53e2c1a5c7fa2f920338136d0ddc3aba23eacaf708e3871bc856a34b95' ] - type of authority: [ 'Owner' ] + role_ids [ + '0x08aef7203969e2467b33b14965dfab62e11b085610c798b3cac150b1d7ea033b' +] ``` #### Get all the accounts that have a certain permission on a marketplace ```js -//marketplace_id, type of authoriry (it can be "Owner", "Admin" or "Appraiser") -const authoritiesByMarketplace = await api.query.gatedMarketplace.authoritiesByMarketplace("0xace33a53e2c1a5c7fa2f920338136d0ddc3aba23eacaf708e3871bc856a34b95","Admin"); - console.log(authoritiesByMarketplace.toHuman()); +//pallet_id, marketplace_id, type of authoriry (it can be "Owner", "Admin" or "Appraiser") +const usersByScope = await api.query.rbac.usersByScope(20, "0xace33a53e2c1a5c7fa2f920338136d0ddc3aba23eacaf708e3871bc856a34b95", "0x08aef7203969e2467b33b14965dfab62e11b085610c798b3cac150b1d7ea033b"); +console.log(usersByScope.toHuman()); ``` ```bash # Output should look like this: @@ -341,25 +509,32 @@ const authoritiesByMarketplace = await api.query.gatedMarketplace.authoritiesByM ```js // get all the accounts in a marketplace -const authoritiesByMarketplace = await api.query.gatedMarketplace.authoritiesByMarketplace.entries(); -authoritiesByMarketplace.forEach(([key, exposure]) => { - console.log('marketplace_id and type of authority:', key.args.map((k) => k.toHuman())); - console.log(' accounts that have the role within the marketplace:', exposure.toHuman(),"\n"); +const scope_users_by_role = await api.query.rbac.usersByScope.entries(20, "0xace33a53e2c1a5c7fa2f920338136d0ddc3aba23eacaf708e3871bc856a34b95"); + scope_users_by_role.forEach(([key, exposure]) => { + console.log('pallet_id, scope_id, role_id:', key.args.map((k) => k.toHuman())); + console.log(' account_id', exposure.toHuman()); }); ``` ```bash # Expected output: -marketplace_id and type of authority: [ +pallet_id, scope_id, role_id: [ + '20', '0xace33a53e2c1a5c7fa2f920338136d0ddc3aba23eacaf708e3871bc856a34b95', - 'Admin' + '0xae9e025522f868c39b41b8a5ba513335a2a229690bd44c71c998d5a9ad38162b' ] - accounts that have the role within the marketplace: [ '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty' ] - -marketplace_id and type of authority: [ + account_id [ '5FLSigC9HGRKVhB9FiEo4Y3koPsNmBmLJbpXg2mp1hXcS59Y' ] +pallet_id, scope_id, role_id: [ + '20', + '0xace33a53e2c1a5c7fa2f920338136d0ddc3aba23eacaf708e3871bc856a34b95', + '0x08aef7203969e2467b33b14965dfab62e11b085610c798b3cac150b1d7ea033b' +] + account_id [ '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY' ] +pallet_id, scope_id, role_id: [ + '20', '0xace33a53e2c1a5c7fa2f920338136d0ddc3aba23eacaf708e3871bc856a34b95', - 'Owner' + '0xc1237f9841c265fb722178da01a1e088c25fb892d6b7cd9634a20ac84bb3ee01' ] - accounts that have the role within the marketplace: [ '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY' ] + account_id [ '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty' ] ``` #### Apply to a marketplace (without custodian) @@ -511,16 +686,16 @@ custodians.forEach(([key, exposure]) => { #### Enroll an applicant (by its account) ```bash # It can only be called by the marketplace owner (Alice) or administrator (Bob) -# market_id, accountOrApplicationEnumerator, approve boolean -const enroll = await api.tx.gatedMarketplace.enroll("0xace33a53e2c1a5c7fa2f920338136d0ddc3aba23eacaf708e3871bc856a34b95", {"Account":charlie.address}, true).signAndSend(alice); +# market_id, accountOrApplicationEnumerator, feedback, approve boolean +const enroll = await api.tx.gatedMarketplace.enroll("0xace33a53e2c1a5c7fa2f920338136d0ddc3aba23eacaf708e3871bc856a34b95", {"Account":charlie.address}, "feedback", true).signAndSend(alice); ``` #### Enroll an applicant (by its application id) ```bash # It can be called by the marketplace owner (Alice) or administrator (Bob) -# market_id, accountOrApplicationEnumerator, approve boolean -const enroll = await api.tx.gatedMarketplace.enroll("0xace33a53e2c1a5c7fa2f920338136d0ddc3aba23eacaf708e3871bc856a34b95", {"Application":"0x9ab75a44b507c0030296dd3660bd77d606807cf3415c3409b88c2cad36fd5483"}, true).signAndSend(alice); +# market_id, accountOrApplicationEnumerator, feedback, approve boolean +const enroll = await api.tx.gatedMarketplace.enroll("0xace33a53e2c1a5c7fa2f920338136d0ddc3aba23eacaf708e3871bc856a34b95", {"Application":"0x9ab75a44b507c0030296dd3660bd77d606807cf3415c3409b88c2cad36fd5483"}, "feedback", true).signAndSend(alice); ``` @@ -540,32 +715,232 @@ const removeAuthority = await api.tx.gatedMarketplace.removeAuthority(dave.addre ``` +#### Put an asset on sale +```js +// marketplace_id, collection_id, item_id, sell price +const sell = await api.tx.gatedMarketplace.enlistSellOffer("0xace33a53e2c1a5c7fa2f920338136d0ddc3aba23eacaf708e3871bc856a34b95",0,0,10000).signAndSend(charlie); +``` + +#### Put a buy offer +```js +// marketplace_id, collection_id, item_id, buy price +const buy = await api.tx.gatedMarketplace.enlistBuyOffer("0xace33a53e2c1a5c7fa2f920338136d0ddc3aba23eacaf708e3871bc856a34b95", 0,0, 10001).signAndSend(dave); +``` + +#### Get offer details +```js +const offer_info = await api.query.gatedMarketplace.offersInfo("0x9abbb3e227dedf26a4a64705ffb924ef8d48dc47de981f4db799790ae2239e6b"); + console.log(offer_info.toHuman()); +``` + +```bash +# Output should look like this +{ + marketplaceId: '0xace33a53e2c1a5c7fa2f920338136d0ddc3aba23eacaf708e3871bc856a34b95', + collectionId: '0', + itemId: '0', + creator: '5FLSigC9HGRKVhB9FiEo4Y3koPsNmBmLJbpXg2mp1hXcS59Y', + price: '10,000', + status: 'Open', + creationDate: '1,660,778,892,000', + expirationDate: '1,661,383,692,000', + offerType: 'SellOrder', + buyer: null +} +``` + +```js +// Get details of all offers +const all_offers = await api.query.gatedMarketplace.offersInfo.entries() +all_offers.forEach(([key, exposure]) => { + console.log('offer_id:', key.args.map((k) => k.toHuman())); + console.log('offer details:', exposure.toHuman()); +}); +``` + +```bash +# Output should look like this +offer_id: [ + '0x9abbb3e227dedf26a4a64705ffb924ef8d48dc47de981f4db799790ae2239e6b' +] +offer details: { + marketplaceId: '0xace33a53e2c1a5c7fa2f920338136d0ddc3aba23eacaf708e3871bc856a34b95', + collectionId: '0', + itemId: '0', + creator: '5FLSigC9HGRKVhB9FiEo4Y3koPsNmBmLJbpXg2mp1hXcS59Y', + price: '10,000', + status: 'Open', + creationDate: '1,660,778,892,000', + expirationDate: '1,661,383,692,000', + offerType: 'SellOrder', + buyer: null +} +# ... +``` + +#### Get offers by item +```js +const offers_by_item = await api.query.gatedMarketplace.offersByItem(0,0); +console.log(offers_by_item.toHuman()); +``` + +```bash +[ + '0x4508b428b15e1a0a0138d36efebe3382739726beca8d67239e02a56c19d378eb', + '0x66fcfadc174a596d8f8dc1b067038ed0056c5c3127d6996bc54fa05148caccf0' +] +``` + +```js +// Get all offers in the whole collection +// collection_id, could get omitted to get all offers from all assets, grouped by collection. +const offers_by_collection = await api.query.gatedMarketplace.offersByItem.entries(0); +offers_by_collection.forEach(([key, exposure]) => { + console.log('collection_id, item_id:', key.args.map((k) => k.toHuman())); + console.log('offer_ids:', exposure.toHuman()); +}); +``` + +```bash +# Output should look like this +collection_id, item_id: [ '0', '0' ] +offer_ids: [ + '0x4508b428b15e1a0a0138d36efebe3382739726beca8d67239e02a56c19d378eb', + '0x66fcfadc174a596d8f8dc1b067038ed0056c5c3127d6996bc54fa05148caccf0' +] +``` + +#### Get offers by account +```js +// account_id +const offers_by_account = await api.query.gatedMarketplace.offersByAccount(charlie.address); +console.log(offers_by_account.toHuman()); +``` + +```bash +['0x4508b428b15e1a0a0138d36efebe3382739726beca8d67239e02a56c19d378eb'] +``` + +```js +const all_offers_by_account = await api.query.gatedMarketplace.offersByAccount.entries(); +all_offers_by_account.forEach(([key, exposure]) => { + console.log('account_id:', key.args.map((k) => k.toHuman())); + console.log('offer_ids:', exposure.toHuman()); +}); +``` + +```bash +account_id: [ '5DAAnrj7VHTznn2AWBemMuyBwZWs6FNFjdyVXUeYum3PTXFy' ] +offer_ids: [ + '0x66fcfadc174a596d8f8dc1b067038ed0056c5c3127d6996bc54fa05148caccf0' +] +account_id: [ '5FLSigC9HGRKVhB9FiEo4Y3koPsNmBmLJbpXg2mp1hXcS59Y' ] +offer_ids: [ + '0x4508b428b15e1a0a0138d36efebe3382739726beca8d67239e02a56c19d378eb' +] +``` + + +#### Get offers by marketplace + +```js +// marketplace_id +const offers_by_market = await api.query.gatedMarketplace.offersByMarketplace("0xace33a53e2c1a5c7fa2f920338136d0ddc3aba23eacaf708e3871bc856a34b95") + console.log(offers_by_market.toHuman()); +``` + +```bash +# output should look like this +[ + '0x4508b428b15e1a0a0138d36efebe3382739726beca8d67239e02a56c19d378eb', + '0x66fcfadc174a596d8f8dc1b067038ed0056c5c3127d6996bc54fa05148caccf0' +] +``` + +```js +// All offers by marketplace +const all_offers_by_market = await api.query.gatedMarketplace.offersByMarketplace.entries(); +all_offers_by_market.forEach(([key, exposure]) => { + console.log('marketplace_id:', key.args.map((k) => k.toHuman())); + console.log('offer_ids:', exposure.toHuman()); +}); +``` + +```bash +# output should look like this +marketplace_id: [ + '0xace33a53e2c1a5c7fa2f920338136d0ddc3aba23eacaf708e3871bc856a34b95' +] +offer_ids: [ + '0x4508b428b15e1a0a0138d36efebe3382739726beca8d67239e02a56c19d378eb', + '0x66fcfadc174a596d8f8dc1b067038ed0056c5c3127d6996bc54fa05148caccf0' +] +``` + +#### Duplicate offer in another marketplace +```js +/// offer_id, marketplace_id, collection_id, item_id, price on that marketplace +const duplicate_offer = await api.tx.gatedMarketplace.duplicateOffer("0x65c7f4fa353a2212c2db497a8a1ad073453aad2030be7f756cba42a2f976dc82","0xa1c17609528fe2630b3be72d6ac8eafc5e0ef95ce78ddad70e83e5fa77ac7342", 0, 0, 10002).signAndSend(charlie) +``` + +#### Remove offer +```js +/// offer_id, marketplace_id, collection_id, item_id +const remove_offer = await api.tx.gatedMarketplace.removeOffer("0x8cb8cc124e19fc58eaf9c6dbd0953a7fd955769e6d3983ce2ea83d64d742a62e", "0xa1c17609528fe2630b3be72d6ac8eafc5e0ef95ce78ddad70e83e5fa77ac7342", 0, 0).signAndSend(charlie) +``` + +#### Take sell offer - direct purchase +```js +/// offer_id, marketplace_id, collection_id, item_id +const take_sell_offer = await api.tx.gatedMarketplace.takeSellOffer("0x65c7f4fa353a2212c2db497a8a1ad073453aad2030be7f756cba42a2f976dc82", "0xace33a53e2c1a5c7fa2f920338136d0ddc3aba23eacaf708e3871bc856a34b95", 0, 0).signAndSend(dave) +``` + +#### Take buy offer + +```js +/// offer_id, marketplace_id, collection_id, item_id +const take_buy_offer = await api.tx.gatedMarketplace.takeBuyOffer("0x66fcfadc174a596d8f8dc1b067038ed0056c5c3127d6996bc54fa05148caccf0", "0xace33a53e2c1a5c7fa2f920338136d0ddc3aba23eacaf708e3871bc856a34b95", 0, 0).signAndSend(charlie) +``` + ## Events ```rust /// Marketplaces stored. [owner, admin, market_id] -MarketplaceStored(T::AccountId, T::AccountId, [u8;32]), +1. MarketplaceStored(T::AccountId, T::AccountId, [u8;32]) + /// Application stored on the specified marketplace. [application_id, market_id] -ApplicationStored([u8;32], [u8;32]), +2. ApplicationStored([u8;32], [u8;32]) + /// An applicant was accepted or rejected on the marketplace. [AccountOrApplication, market_id, status] -ApplicationProcessed(AccountOrApplication,[u8;32], ApplicationStatus), +3. ApplicationProcessed(AccountOrApplication,[u8;32], ApplicationStatus) + /// Add a new authority to the selected marketplace -AuthorityAdded(T::AccountId, MarketplaceAuthority), +4. AuthorityAdded(T::AccountId, MarketplaceAuthority) + /// Remove the selected authority from the selected marketplace -AuthorityRemoved(T::AccountId, MarketplaceAuthority), +5. AuthorityRemoved(T::AccountId, MarketplaceAuthority) + /// The label of the selected marketplace has been updated. [market_id] -MarketplaceLabelUpdated([u8;32]), +6. MarketplaceLabelUpdated([u8;32]) + /// The selected marketplace has been removed. [market_id] -MarketplaceRemoved([u8;32]) +7. MarketplaceRemoved([u8;32]) + +/// Offer stored. [collection_id, item_id] +8. OfferStored(T::CollectionId, T::ItemId) + +/// Offer was accepted [offer_id, account] +9. OfferWasAccepted([u8;32], T::AccountId) + +/// Offer was duplicated. [new_offer_id, new_marketplace_id] +10. OfferDuplicated([u8;32], [u8;32]) ``` ## Errors ```rust -/// Work In Progress -NotYetImplemented, -/// Error names should be descriptive. -NoneValue, +///Limit bounded vector exceeded +LimitExceeded, /// The account supervises too many marketplaces ExceedMaxMarketsPerAuth, /// The account has too many roles in that marketplace @@ -587,7 +962,9 @@ AlreadyApplied, /// The specified marketplace does not exist MarketplaceNotFound, /// You need to be an owner or an admin of the marketplace -CannotEnroll, +NotOwnerOrAdmin, +/// There was no change regarding the application status +AlreadyEnrolled, /// There cannot be more than one owner per marketplace OnlyOneOwnerIsAllowed, /// Cannot remove the owner of the marketplace @@ -608,4 +985,32 @@ ApplicationIdNotFound, ApplicationStatusStillPending, /// The application has already been approved, application status is approved ApplicationHasAlreadyBeenApproved, +/// Collection not found +CollectionNotFound, +/// User who calls the function is not the owner of the collection +NotOwner, +/// Offer already exists +OfferAlreadyExists, +/// Offer not found +OfferNotFound, +/// Offer is not available at the moment +OfferIsNotAvailable, +/// Owner cannnot buy its own offer +CannotTakeOffer, +/// User cannot remove the offer from the marketplace +CannotRemoveOffer, +/// Error related to the timestamp +TimestampError, +/// User does not have enough balance to buy the offer +NotEnoughBalance, +/// User cannot delete the offer because is closed +CannotDeleteOffer, +/// There was a problem storing the offer +OfferStorageError, +/// Price must be greater than zero +PriceMustBeGreaterThanZero, +/// User cannot create buy offers for their own items +CannotCreateOffer, +/// This items is not available for sale +ItemNotForSale, ``` \ No newline at end of file diff --git a/pallets/gated-marketplace/src/functions.rs b/pallets/gated-marketplace/src/functions.rs index 4afa6ffa..6d5ccba4 100644 --- a/pallets/gated-marketplace/src/functions.rs +++ b/pallets/gated-marketplace/src/functions.rs @@ -1,22 +1,45 @@ + use super::*; -use frame_support::pallet_prelude::*; -//use frame_system::pallet_prelude::*; +use frame_support::{pallet_prelude::*}; use frame_support::sp_io::hashing::blake2_256; -use sp_runtime::sp_std::vec::Vec; +use sp_runtime::sp_std::vec::Vec; // vec primitive +use scale_info::prelude::vec; // vec![] macro use crate::types::*; +use pallet_rbac::types::*; + +use frame_support::traits::Time; +use frame_support::traits::{Currency}; +use frame_support::traits::ExistenceRequirement::KeepAlive; impl Pallet { + pub fn do_initial_setup()->DispatchResult{ + let pallet_id = Self::pallet_id(); + let super_roles = vec![MarketplaceRole::Owner.to_vec(), MarketplaceRole::Admin.to_vec()]; + let super_role_ids = T::Rbac::create_and_set_roles(pallet_id.clone(), super_roles)?; + for super_role in super_role_ids{ + T::Rbac::create_and_set_permissions(pallet_id.clone(), super_role, Permission::admin_permissions())?; + } + // participant role and permissions + let participant_role_id = T::Rbac::create_and_set_roles(pallet_id.clone(), [MarketplaceRole::Participant.to_vec()].to_vec())?; + T::Rbac::create_and_set_permissions(pallet_id.clone(), participant_role_id[0], Permission::participant_permissions() )?; + // appraiser role and permissions + let _appraiser_role_id = T::Rbac::create_and_set_roles(pallet_id.clone(), [MarketplaceRole::Appraiser.to_vec()].to_vec())?; + // redemption specialist role and permissions + let _redemption_role_id = T::Rbac::create_and_set_roles(pallet_id, [MarketplaceRole::RedemptionSpecialist.to_vec()].to_vec())?; + Ok(()) + } + pub fn do_create_marketplace(owner: T::AccountId, admin: T::AccountId ,marketplace: Marketplace)->DispatchResult{ // Gen market id let marketplace_id = marketplace.using_encoded(blake2_256); // ensure the generated id is unique ensure!(!>::contains_key(marketplace_id), Error::::MarketplaceAlreadyExists); //Insert on marketplaces and marketplaces by auth - Self::insert_in_auth_market_lists(owner.clone(), MarketplaceAuthority::Owner, marketplace_id)?; - Self::insert_in_auth_market_lists(admin.clone(), MarketplaceAuthority::Admin, marketplace_id)?; + T::Rbac::create_scope(Self::pallet_id(),marketplace_id)?; + Self::insert_in_auth_market_lists(owner.clone(), MarketplaceRole::Owner, marketplace_id)?; + Self::insert_in_auth_market_lists(admin.clone(), MarketplaceRole::Admin, marketplace_id)?; >::insert(marketplace_id, marketplace); - Self::deposit_event(Event::MarketplaceStored(owner, admin, marketplace_id)); Ok(()) } @@ -45,9 +68,10 @@ impl Pallet { Ok(()) } - pub fn do_enroll(authority: T::AccountId,marketplace_id: [u8;32], account_or_application: AccountOrApplication, approved: bool, feedback: BoundedVec,)->DispatchResult{ + pub fn do_enroll(authority: T::AccountId, marketplace_id: [u8;32], account_or_application: AccountOrApplication, approved: bool, feedback: BoundedVec,)->DispatchResult{ // ensure the origin is owner or admin - Self::can_enroll(authority, marketplace_id)?; + //Self::can_enroll(authority, marketplace_id)?; + Self::is_authorized(authority, &marketplace_id,Permission::Enroll)?; let next_status = match approved{ true => ApplicationStatus::Approved, false => ApplicationStatus::Rejected, @@ -64,28 +88,28 @@ impl Pallet { }, }; Self::change_applicant_status(applicant, marketplace_id, next_status, feedback)?; - // TODO: if rejected remove application and files? + Self::deposit_event(Event::ApplicationProcessed(account_or_application, marketplace_id, next_status)); Ok(()) } - pub fn do_authority(authority: T::AccountId, account: T::AccountId, authority_type: MarketplaceAuthority, marketplace_id: [u8;32], ) -> DispatchResult { + pub fn do_authority(authority: T::AccountId, account: T::AccountId, authority_type: MarketplaceRole, marketplace_id: [u8;32], ) -> DispatchResult { //ensure the origin is owner or admin //TODO: implement copy trait for MarketplaceAuthority & T::AccountId - Self::can_enroll(authority, marketplace_id)?; - + //Self::can_enroll(authority, marketplace_id)?; + Self::is_authorized(authority, &marketplace_id,Permission::AddAuth)?; //ensure the account is not already an authority - ensure!(!Self::does_exist_authority(account.clone(), marketplace_id, authority_type), Error::::AlreadyApplied); - + // handled by T::Rbac::assign_role_to_user + //ensure!(!Self::does_exist_authority(account.clone(), marketplace_id, authority_type), Error::::AlreadyApplied); match authority_type{ - MarketplaceAuthority::Owner => { + MarketplaceRole::Owner => { ensure!(!Self::owner_exist(marketplace_id), Error::::OnlyOneOwnerIsAllowed); Self::insert_in_auth_market_lists(account.clone(), authority_type, marketplace_id)?; + }, _ =>{ - - Self::insert_in_auth_market_lists(account.clone(), authority_type, marketplace_id)?; + Self::insert_in_auth_market_lists(account.clone(), authority_type, marketplace_id)?; } } @@ -94,19 +118,20 @@ impl Pallet { } - pub fn do_remove_authority(authority: T::AccountId, account: T::AccountId, authority_type: MarketplaceAuthority, marketplace_id: [u8;32], ) -> DispatchResult { + pub fn do_remove_authority(authority: T::AccountId, account: T::AccountId, authority_type: MarketplaceRole, marketplace_id: [u8;32], ) -> DispatchResult { //ensure the origin is owner or admin - Self::can_enroll(authority.clone(), marketplace_id)?; - + //Self::can_enroll(authority.clone(), marketplace_id)?; + Self::is_authorized(authority.clone(), &marketplace_id,Permission::RemoveAuth)?; //ensure the account has the selected authority before to try to remove - ensure!(Self::does_exist_authority(account.clone(), marketplace_id, authority_type), Error::::AuthorityNotFoundForUser); + // T::Rbac handles the if role doesnt hasnt been asigned to the user + //ensure!(Self::does_exist_authority(account.clone(), marketplace_id, authority_type), Error::::AuthorityNotFoundForUser); match authority_type{ - MarketplaceAuthority::Owner => { + MarketplaceRole::Owner => { ensure!(Self::owner_exist(marketplace_id), Error::::OwnerNotFound); - Err(Error::::CantRemoveOwner)?; + return Err(Error::::CantRemoveOwner.into()); }, - MarketplaceAuthority::Admin => { + MarketplaceRole::Admin => { // Admins can not delete themselves ensure!(authority != account, Error::::AdminCannotRemoveItself); @@ -131,7 +156,8 @@ impl Pallet { //ensure the marketplace exists ensure!(>::contains_key(marketplace_id), Error::::MarketplaceNotFound); //ensure the origin is owner or admin - Self::can_enroll(authority, marketplace_id)?; + //Self::can_enroll(authority, marketplace_id)?; + Self::is_authorized(authority, &marketplace_id, Permission::UpdateLabel)?; //update marketplace Self::update_label(marketplace_id, new_label)?; Self::deposit_event(Event::MarketplaceLabelUpdated(marketplace_id)); @@ -143,21 +169,285 @@ impl Pallet { //ensure the marketplace exists ensure!(>::contains_key(marketplace_id), Error::::MarketplaceNotFound); //ensure the origin is owner or admin - Self::can_enroll(authority, marketplace_id)?; + //Self::can_enroll(authority, marketplace_id)?; + Self::is_authorized(authority, &marketplace_id, Permission::RemoveMarketplace)?; //remove marketplace Self::remove_selected_marketplace(marketplace_id)?; Self::deposit_event(Event::MarketplaceRemoved(marketplace_id)); Ok(()) } + pub fn do_enlist_sell_offer(authority: T::AccountId, marketplace_id: [u8;32], collection_id: T::CollectionId, item_id: T::ItemId, price: BalanceOf,) -> DispatchResult { + //This function is only called by the owner of the marketplace + //ensure the marketplace exists + ensure!(>::contains_key(marketplace_id), Error::::MarketplaceNotFound); + Self::is_authorized(authority.clone(), &marketplace_id,Permission::EnlistSellOffer)?; + //ensure the collection exists + if let Some(a) = pallet_uniques::Pallet::::owner(collection_id, item_id) { + ensure!(a == authority, Error::::NotOwner); + } else { + return Err(Error::::CollectionNotFound.into()); + } + + //ensure the price is valid + Self::is_the_price_valid(price)?; + + //Add timestamp to the offer + let(timestamp, timestamp2) = Self::get_timestamp_in_milliseconds().ok_or(Error::::TimestampError)?; + + //create an offer_id + let offer_id = (marketplace_id, authority.clone(), collection_id, timestamp, timestamp2).using_encoded(blake2_256); + + //create offer structure + let offer_data = OfferData:: { + marketplace_id, + collection_id, + item_id, + creator: authority.clone(), + price, + creation_date: timestamp, + expiration_date: timestamp2, + status: OfferStatus::Open, + offer_type: OfferType::SellOrder, + buyer: None, + }; + + //ensure there is no a previous sell offer for this item + Self::can_this_item_receive_sell_orders(collection_id, item_id, marketplace_id)?; + + //insert in OffersByItem + >::try_mutate(collection_id, item_id, |offers| { + offers.try_push(offer_id) + }).map_err(|_| Error::::OfferStorageError)?; + + //insert in OffersByAccount + >::try_mutate(authority, |offers| { + offers.try_push(offer_id) + }).map_err(|_| Error::::OfferStorageError)?; + + //insert in OffersInfo + // ensure the offer_id doesn't exist + ensure!(!>::contains_key(offer_id), Error::::OfferAlreadyExists); + >::insert(offer_id, offer_data); + + //Insert in OffersByMarketplace + >::try_mutate(marketplace_id, |offers| { + offers.try_push(offer_id) + }).map_err(|_| Error::::OfferStorageError)?; + + Self::deposit_event(Event::OfferStored(collection_id, item_id)); + Ok(()) + } + + pub fn do_enlist_buy_offer(authority: T::AccountId, marketplace_id: [u8;32], collection_id: T::CollectionId, item_id: T::ItemId, price: BalanceOf,) -> DispatchResult { + //ensure the item is for sale, if not, return error + Self::can_this_item_receive_buy_orders(collection_id, item_id, marketplace_id)?; + + //ensure the marketplace exists + ensure!(>::contains_key(marketplace_id), Error::::MarketplaceNotFound); + Self::is_authorized(authority.clone(), &marketplace_id,Permission::EnlistBuyOffer)?; + + //ensure the collection exists + //For this case user doesn't need to be the owner of the collection + //but the owner of the item cannot create a buy offer for their own collection + if let Some(a) = pallet_uniques::Pallet::::owner(collection_id, item_id) { + ensure!(a != authority, Error::::CannotCreateOffer); + } else { + return Err(Error::::CollectionNotFound.into()); + } + + //ensure user has enough balance to create the offer + let total_user_balance = T::Currency::total_balance(&authority); + ensure!(total_user_balance >= price, Error::::NotEnoughBalance); + + //ensure the price is valid + Self::is_the_price_valid(price)?; + + //Add timestamp to the offer + let(timestamp, timestamp2) = Self::get_timestamp_in_milliseconds().ok_or(Error::::TimestampError)?; + + //create an offer_id + let offer_id = (marketplace_id, authority.clone(), collection_id, timestamp, timestamp2).using_encoded(blake2_256); + + //create offer structure + let offer_data = OfferData:: { + marketplace_id, + collection_id, + item_id, + creator: authority.clone(), + price, + creation_date: timestamp, + expiration_date: timestamp2, + status: OfferStatus::Open, + offer_type: OfferType::BuyOrder, + buyer: None, + }; + + //insert in OffersByItem + //An item can receive multiple buy offers + >::try_mutate(collection_id, item_id, |offers| { + offers.try_push(offer_id) + }).map_err(|_| Error::::OfferStorageError)?; + + //insert in OffersByAccount + >::try_mutate(authority, |offers| { + offers.try_push(offer_id) + }).map_err(|_| Error::::OfferStorageError)?; + + //insert in OffersInfo + // ensure the offer_id doesn't exist + ensure!(!>::contains_key(offer_id), Error::::OfferAlreadyExists); + >::insert(offer_id, offer_data); + + //Insert in OffersByMarketplace + >::try_mutate(marketplace_id, |offers| { + offers.try_push(offer_id) + }).map_err(|_| Error::::OfferStorageError)?; + + Self::deposit_event(Event::OfferStored(collection_id, item_id)); + Ok(()) + } + + pub fn do_take_sell_offer(buyer: T::AccountId, offer_id: [u8;32]) -> DispatchResult { + //This extrisicn is called by the user who wants to buy the item + //get offer data + let offer_data = >::get(offer_id).ok_or(Error::::OfferNotFound)?; + + Self::is_authorized(buyer.clone(), &offer_data.marketplace_id,Permission::TakeSellOffer)?; + + //ensure the collection & owner exists + let owner_item = pallet_uniques::Pallet::::owner(offer_data.collection_id, offer_data.item_id).ok_or(Error::::OwnerNotFound)?; + + //ensure owner is not the same as the buyer + ensure!(owner_item != buyer, Error::::CannotTakeOffer); + + //ensure the offer_id exists in OffersByItem + Self::does_exist_offer_id_for_this_item(offer_data.collection_id, offer_data.item_id, offer_id)?; + + //ensure the offer is open and available + ensure!(offer_data.status == OfferStatus::Open, Error::::OfferIsNotAvailable); + + //TODO: Use free_balance instead of total_balance + //Get the buyer's balance + let total_amount_buyer = T::Currency::total_balance(&buyer); + //ensure the buyer has enough balance to buy the item + ensure!(total_amount_buyer > offer_data.price, Error::::NotEnoughBalance); + //Transfer the balance + T::Currency::transfer(&buyer, &owner_item, offer_data.price, KeepAlive)?; + + //Use uniques transfer function to transfer the item to the buyer + pallet_uniques::Pallet::::do_transfer(offer_data.collection_id, offer_data.item_id, buyer.clone(), |_, _|{ + Ok(()) + })?; + + //update offer status from all marketplaces + Self::update_offers_status(buyer.clone(), offer_data.collection_id, offer_data.item_id, offer_data.marketplace_id)?; + + //remove all the offers associated with the item + Self::delete_all_offers_for_this_item(offer_data.collection_id, offer_data.item_id)?; + + Self::deposit_event(Event::OfferWasAccepted(offer_id, buyer)); + Ok(()) + } + + pub fn do_take_buy_offer(authority: T::AccountId, offer_id: [u8;32]) -> DispatchResult { + //This extrinsic is called by the owner of the item who accepts the buy offer created by a marketparticipant + //get offer data + let offer_data = >::get(offer_id).ok_or(Error::::OfferNotFound)?; + + Self::is_authorized(authority.clone(), &offer_data.marketplace_id,Permission::TakeBuyOffer)?; + //ensure the collection & owner exists + let owner_item = pallet_uniques::Pallet::::owner(offer_data.collection_id, offer_data.item_id).ok_or(Error::::OwnerNotFound)?; + //ensure only owner of the item can call the extrinic + ensure!(owner_item == authority, Error::::NotOwner); + //ensure owner is not the same as the buy_offer_creator + ensure!(owner_item != offer_data.creator, Error::::CannotTakeOffer); + + //ensure the offer_id exists in OffersByItem + Self::does_exist_offer_id_for_this_item(offer_data.collection_id, offer_data.item_id, offer_id)?; + + //ensure the offer is open and available + ensure!(offer_data.status == OfferStatus::Open, Error::::OfferIsNotAvailable); + + //TODO: Use free_balance instead of total_balance + //Get the buyer's balance + let total_amount_buyer = T::Currency::total_balance(&offer_data.creator); + //ensure the buy_offer_creator has enough balance to buy the item + ensure!(total_amount_buyer > offer_data.price, Error::::NotEnoughBalance); + //Transfer the balance to the owner of the item + T::Currency::transfer(&offer_data.creator, &owner_item, offer_data.price, KeepAlive)?; + //Use uniques transfer function to transfer the item to the market_participant + pallet_uniques::Pallet::::do_transfer(offer_data.collection_id, offer_data.item_id, offer_data.creator.clone(), |_, _|{ + Ok(()) + })?; + + //update offer status from all marketplaces + Self::update_offers_status(offer_data.creator.clone(), offer_data.collection_id, offer_data.item_id, offer_data.marketplace_id)?; + + //remove all the offers associated with the item + Self::delete_all_offers_for_this_item(offer_data.collection_id, offer_data.item_id )?; + + Self::deposit_event(Event::OfferWasAccepted(offer_id, offer_data.creator)); + Ok(()) + } + + + pub fn do_remove_offer(authority: T::AccountId, offer_id: [u8;32]) -> DispatchResult { + //ensure the offer_id exists + ensure!(>::contains_key(offer_id), Error::::OfferNotFound); + + //get offer data + let offer_data = >::get(offer_id).ok_or(Error::::OfferNotFound)?; + Self::is_authorized(authority.clone(), &offer_data.marketplace_id,Permission::RemoveOffer)?; + + //ensure the offer status is Open + ensure!(offer_data.status == OfferStatus::Open, Error::::CannotDeleteOffer); + + // ensure the authority is the creator of the offer + ensure!(offer_data.creator == authority, Error::::CannotRemoveOffer); + + //ensure the offer_id exists in OffersByItem + Self::does_exist_offer_id_for_this_item(offer_data.collection_id, offer_data.item_id, offer_id)?; + + //remove the offer from OfferInfo + >::remove(offer_id); + + //remove the offer from OffersByMarketplace + >::try_mutate(offer_data.marketplace_id, |offers| { + let offer_index = offers.iter().position(|x| *x == offer_id).ok_or(Error::::OfferNotFound)?; + offers.remove(offer_index); + Ok(()) + }).map_err(|_:Error::| Error::::OfferNotFound)?; + + //remove the offer from OffersByAccount + >::try_mutate(authority, |offers| { + let offer_index = offers.iter().position(|x| *x == offer_id).ok_or(Error::::OfferNotFound)?; + offers.remove(offer_index); + Ok(()) + }).map_err(|_:Error::| Error::::OfferNotFound)?; + + //remove the offer from OffersByItem + >::try_mutate(offer_data.collection_id, offer_data.item_id, |offers| { + let offer_index = offers.iter().position(|x| *x == offer_id).ok_or(Error::::OfferNotFound)?; + offers.remove(offer_index); + Ok(()) + }).map_err(|_:Error::| Error::::OfferNotFound)?; + + Self::deposit_event(Event::OfferRemoved(offer_id, offer_data.marketplace_id)); + + Ok(()) + } + + + /*---- Helper functions ----*/ pub fn set_up_application( - fields : BoundedVec<(BoundedVec >,BoundedVec> ), T::MaxFiles>, - custodian_fields: Option<(T::AccountId, BoundedVec>, T::MaxFiles> )> + fields : Fields, + custodian_fields: Option> )-> (Option, BoundedVec ){ let mut f: Vec= fields.iter().map(|tuple|{ ApplicationField{ @@ -166,8 +456,8 @@ impl Pallet { }).collect(); let custodian = match custodian_fields{ Some(c_fields)=>{ - for i in 0..f.len(){ - f[i].custodian_cid = Some(c_fields.1[i].clone()); + for (i, field) in f.iter_mut().enumerate(){ + field.custodian_cid = Some(c_fields.1[i].clone()); } Some(c_fields.0) @@ -177,15 +467,11 @@ impl Pallet { (custodian, BoundedVec::::try_from(f).unwrap_or_default() ) } - fn insert_in_auth_market_lists(authority: T::AccountId, role: MarketplaceAuthority, marketplace_id: [u8;32])->DispatchResult{ + fn insert_in_auth_market_lists(authority: T::AccountId, role: MarketplaceRole, marketplace_id: [u8;32])->DispatchResult{ - >::try_mutate(authority.clone(), marketplace_id, |account_auths|{ - account_auths.try_push(role) - }).map_err(|_| Error::::ExceedMaxRolesPerAuth)?; - - >::try_mutate(marketplace_id, role, | accounts|{ - accounts.try_push(authority) - }).map_err(|_| Error::::ExceedMaxMarketsPerAuth)?; + T::Rbac::assign_role_to_user(authority, Self::pallet_id(), + &marketplace_id, role.id())?; + Ok(()) } @@ -193,6 +479,7 @@ impl Pallet { >::try_mutate(marketplace_id, status,|applicants|{ applicants.try_push(applicant) }).map_err(|_| Error::::ExceedMaxApplicants)?; + Ok(()) } @@ -200,6 +487,7 @@ impl Pallet { >::try_mutate(custodian, marketplace_id, | applications |{ applications.try_push(applicant) }).map_err(|_| Error::::ExceedMaxApplicationsPerCustodian)?; + Ok(()) } @@ -208,26 +496,16 @@ impl Pallet { let applicant_index = applicants.iter().position(|a| *a==applicant.clone()) .ok_or(Error::::ApplicantNotFound)?; applicants.remove(applicant_index); + Ok(()) }) } - fn remove_from_market_lists(account: T::AccountId, author_type: MarketplaceAuthority , marketplace_id : [u8;32])->DispatchResult{ - >::try_mutate(account.clone(), marketplace_id, |account_auths|{ - let author_index = account_auths.iter().position(|a| *a==author_type) - .ok_or(Error::::UserNotFound)?; - account_auths.remove(author_index); - Ok(()) - }).map_err(|_:Error::| Error::::UserNotFound)?; - - >::try_mutate( marketplace_id, author_type, |accounts|{ - let author_index = accounts.iter().position(|a| *a==account.clone()) - .ok_or(Error::::UserNotFound)?; - accounts.remove(author_index); - Ok(()) - }).map_err(|_:Error::| Error::::UserNotFound)?; + fn remove_from_market_lists(account: T::AccountId, author_type: MarketplaceRole , marketplace_id : [u8;32])->DispatchResult{ + T::Rbac::remove_role_from_user(account, Self::pallet_id(), + &marketplace_id, author_type.id())?; Ok(()) } @@ -246,58 +524,63 @@ impl Pallet { } Ok(()) })?; + ensure!(prev_status != next_status, Error::::AlreadyEnrolled); //remove from previous state list Self::remove_from_applicants_lists(applicant.clone(),prev_status, marketplace_id)?; //insert in current state list - Self::insert_in_applicants_lists(applicant, next_status,marketplace_id)?; + Self::insert_in_applicants_lists(applicant.clone(), next_status,marketplace_id)?; + + if prev_status == ApplicationStatus::Approved{ + T::Rbac::remove_role_from_user(applicant.clone(), Self::pallet_id(), &marketplace_id, MarketplaceRole::Participant.id())?; + } + if next_status == ApplicationStatus::Approved{ + T::Rbac::assign_role_to_user(applicant, Self::pallet_id(), &marketplace_id, MarketplaceRole::Participant.id())? + } + Ok(()) } - fn can_enroll( authority: T::AccountId, marketplace_id: [u8;32] ) -> DispatchResult{ - // to enroll, the account needs to be an owner or an admin - let roles = >::try_get(authority, marketplace_id) - .map_err(|_| Error::::CannotEnroll)?; - // iter().any could be called too but this maps directly to desired error - roles.iter().find(|&role|{ - role.eq(&MarketplaceAuthority::Owner) || role.eq(&MarketplaceAuthority::Admin) - }).ok_or(Error::::CannotEnroll)?; - Ok(()) + fn is_authorized( authority: T::AccountId, marketplace_id: &[u8;32], permission: Permission ) -> DispatchResult{ + T::Rbac::is_authorized( + authority, + Self::pallet_id(), + marketplace_id, + &permission.id(), + ) } ///Lets us know if the selected user is an admin. /// It returns true if the user is an admin, false otherwise. fn is_admin(account: T::AccountId, marketplace_id: [u8;32]) -> bool{ - let roles = match >::try_get(account, marketplace_id){ - Ok(roles) => roles, - Err(_) => return false, - }; - - roles.iter().any(|&authority_type| authority_type == MarketplaceAuthority::Admin) + T::Rbac::has_role(account, Self::pallet_id(), + &marketplace_id, [MarketplaceRole::Admin.id()].to_vec()).is_ok() } /// Let us know if the selected account has the selected authority type. /// It returns true if the account has the authority type, false otherwise - fn does_exist_authority(account: T::AccountId, marketplace_id: [u8;32], authority_type: MarketplaceAuthority) -> bool{ - let roles = match >::try_get(account, marketplace_id){ - Ok(roles) => roles, - Err(_) => return false, - }; + // fn does_exist_authority(account: T::AccountId, marketplace_id: [u8;32], authority_type: MarketplaceRole) -> bool{ + // let roles = match >::try_get(account, marketplace_id){ + // Ok(roles) => roles, + // Err(_) => return false, + // }; - roles.iter().any(|authority| authority == &authority_type) - } + // roles.iter().any(|authority| authority == &authority_type) + // } /// Let us know if there's an owner for the selected marketplace. /// It returns true if there's an owner, false otherwise fn owner_exist(marketplace_id: [u8;32]) -> bool { - let owners = match >::try_get( marketplace_id, MarketplaceAuthority::Owner){ - Ok(owners) => owners, - Err(_) => return false, - }; + // let owners = match >::try_get( marketplace_id, MarketplaceAuthority::Owner){ + // Ok(owners) => owners, + // Err(_) => return false, + // }; - owners.len() == 1 + //owners.len() == 1 + T::Rbac::get_role_users_len(Self::pallet_id(), + &marketplace_id, &MarketplaceRole::Owner.id()) == 1 } /// Let us update the marketplace's label. @@ -315,24 +598,14 @@ impl Pallet { /// If returns ok if the deletion was successful, error otherwise. /// Errors only could happen if the storage sources are corrupted. fn remove_selected_marketplace(marketplace_id: [u8;32]) -> DispatchResult { + //TODO: evaluate use iter_key_prefix ->instead iter() //Before to remove the marketplace, we need to remove all its associated authorities // as well as the applicants/applications. //First we need to get the list of all the authorities for the marketplace. - let _users = >::iter_prefix(marketplace_id) - .map(|(_authority, users)| users).flatten().collect::>(); - - //1. remove from MarketplacesByAuthority - _users.iter().for_each(|user|{ - >::remove(user, marketplace_id); - }); - - //2. remove from authorities by marketplace list - >::remove_prefix(marketplace_id, None); - - //3. remove from Applications lists let mut applications = Vec::new(); - + + // remove from Applications lists for ele in >::iter() { if ele.1 == marketplace_id { applications.push(ele.2); @@ -343,22 +616,24 @@ impl Pallet { >::remove(application); } - //4. remove from ApplicationsByAccount list + // remove from ApplicationsByAccount list >::iter().for_each(|(_k1, _k2, _k3)|{ >::remove(_k1, marketplace_id); }); - //5. remove from ApplicantsByMarketplace list + // remove from ApplicantsByMarketplace list >::remove_prefix(marketplace_id, None); - //6. remove from Custodians list + // remove from Custodians list >::iter().for_each(|(_k1, _k2, _k3)|{ >::remove(_k1, marketplace_id); }); - //7. remove from Marketplaces list + // remove from Marketplaces list >::remove(marketplace_id); + T::Rbac::remove_scope(Self::pallet_id(), marketplace_id)?; + Ok(()) } @@ -375,8 +650,8 @@ impl Pallet { .map_err(|_| Error::::ApplicationNotFound)?; match application.status { - ApplicationStatus::Pending => Err(Error::::ApplicationStatusStillPending)?, - ApplicationStatus::Approved => Err(Error::::ApplicationHasAlreadyBeenApproved)?, + ApplicationStatus::Pending => return Err(Error::::ApplicationStatusStillPending.into()), + ApplicationStatus::Approved => return Err(Error::::ApplicationHasAlreadyBeenApproved.into()), ApplicationStatus::Rejected => { //If status is Rejected, we need to delete the previous application from all the storage sources. >::remove(application_id); @@ -387,4 +662,126 @@ impl Pallet { Ok(()) } + + fn get_timestamp_in_milliseconds() -> Option<(u64, u64)> { + let timestamp:u64 = T::Timestamp::now().into(); + let timestamp2 = timestamp + (7 * 24 * 60 * 60 * 1000); + + Some((timestamp, timestamp2)) + } + + fn _is_offer_status(offer_id: [u8;32], offer_status: OfferStatus,) -> bool{ + //we already know that the offer exists, so we don't need to check it here. + if let Some(offer) = >::get(offer_id) { + offer.status == offer_status + } else { + false + } + } + + + fn does_exist_offer_id_for_this_item(collection_id: T::CollectionId, item_id: T::ItemId, offer_id: [u8;32]) -> DispatchResult { + let offers = >::try_get(collection_id, item_id).map_err(|_| Error::::OfferNotFound)?; + //find the offer_id in the vector of offers_ids + offers.iter().find(|&x| *x == offer_id).ok_or(Error::::OfferNotFound)?; + Ok(()) + } + + + + //sell orders here... + + fn update_offers_status(buyer: T::AccountId, collection_id: T::CollectionId, item_id: T::ItemId, marketplace_id: [u8;32]) -> DispatchResult{ + let offer_ids = >::try_get(collection_id, item_id).map_err(|_| Error::::OfferNotFound)?; + + for offer_id in offer_ids { + >::try_mutate::<_,_,DispatchError,_>(offer_id, |offer|{ + let offer = offer.as_mut().ok_or(Error::::OfferNotFound)?; + offer.status = OfferStatus::Closed; + offer.buyer = Some((buyer.clone(), marketplace_id)); + Ok(()) + })?; + + } + Ok(()) + } + + fn is_the_price_valid(price: BalanceOf,) -> DispatchResult { + let minimun_amount: BalanceOf = 1000u32.into(); + ensure!(price > minimun_amount, Error::::PriceMustBeGreaterThanZero); + Ok(()) + } + + fn can_this_item_receive_sell_orders(collection_id: T::CollectionId, item_id: T::ItemId, marketplace_id: [u8;32]) -> DispatchResult { + let offers = >::get(collection_id, item_id); + + //if len is == 0, it means that there is no offers for this item, maybe it's the first entry + if offers.len() > 0 { + for offer in offers { + let offer_info = >::get(offer).ok_or(Error::::OfferNotFound)?; + //ensure the offer_type is SellOrder, because this vector also contains buy offers. + if offer_info.marketplace_id == marketplace_id && offer_info.offer_type == OfferType::SellOrder { + return Err(Error::::OfferAlreadyExists.into()); + } + } + } + + Ok(()) + } + + fn can_this_item_receive_buy_orders(collection_id: T::CollectionId, item_id: T::ItemId, marketplace_id: [u8;32]) -> DispatchResult { + //First we check if the item has is for sale, if not, return error + ensure!(>::contains_key(collection_id, item_id), Error::::ItemNotForSale); + + //ensure the item can receive buy offers on the selected marketplace + let offers = >::get(collection_id, item_id); + + for offer in offers { + let offer_info = >::get(offer).ok_or(Error::::OfferNotFound)?; + //ensure the offer_type is SellOrder, because this vector also contains buy offers. + if offer_info.marketplace_id == marketplace_id && offer_info.offer_type == OfferType::SellOrder { + return Ok(()) + } + } + + Err(Error::::ItemNotForSale.into()) + } + + + fn _delete_all_sell_orders_for_this_item(collection_id: T::CollectionId, item_id: T::ItemId) -> DispatchResult { + //ensure the item has offers associated with it. + ensure!(>::contains_key(collection_id, item_id), Error::::OfferNotFound); + + let offers_ids = >::take(collection_id, item_id); + //let mut remaining_offer_ids: Vec<[u8;32]> = Vec::new(); + let mut buy_offer_ids: BoundedVec<[u8;32], T::MaxOffersPerMarket> = BoundedVec::default(); + + for offer_id in offers_ids { + let offer_info = >::get(offer_id).ok_or(Error::::OfferNotFound)?; + //ensure the offer_type is SellOrder, because this vector also contains offers of BuyOrder OfferType. + if offer_info.offer_type != OfferType::SellOrder { + buy_offer_ids.try_push(offer_id).map_err(|_| Error::::LimitExceeded)?; + } + } + //ensure we already took the entry from the storage map, so we can insert it again. + ensure!(!>::contains_key(collection_id, item_id), Error::::OfferNotFound); + >::insert(collection_id, item_id, buy_offer_ids); + + Ok(()) + } + + fn delete_all_offers_for_this_item(collection_id: T::CollectionId, item_id: T::ItemId) -> DispatchResult { + >::remove(collection_id, item_id); + Ok(()) + } + + + + + pub fn pallet_id()->IdOrVec{ + IdOrVec::Vec( + Self::module_name().as_bytes().to_vec() + ) + } + } \ No newline at end of file diff --git a/pallets/gated-marketplace/src/lib.rs b/pallets/gated-marketplace/src/lib.rs index 232fdc83..c2e4224b 100644 --- a/pallets/gated-marketplace/src/lib.rs +++ b/pallets/gated-marketplace/src/lib.rs @@ -8,25 +8,40 @@ mod mock; #[cfg(test)] mod tests; -#[cfg(feature = "runtime-benchmarks")] -mod benchmarking; - mod functions; mod types; + + #[frame_support::pallet] pub mod pallet { - use frame_support::{pallet_prelude::{*, OptionQuery}, transactional}; + use frame_support::pallet_prelude::*; + use frame_support::transactional; use frame_system::pallet_prelude::*; - //use sp_runtime::sp_std::vec::Vec; + use sp_runtime::traits::Scale; + use frame_support::traits::{Currency, Time}; + use crate::types::*; + use pallet_rbac::types::RoleBasedAccessControl; + + pub type BalanceOf = <::Currency as Currency<::AccountId>>::Balance; #[pallet::config] - pub trait Config: frame_system::Config { + pub trait Config: frame_system::Config + pallet_fruniques::Config{ + type Event: From> + IsType<::Event>; - type RemoveOrigin: EnsureOrigin; - + type Moment: Parameter + + Default + + Scale + + Copy + + MaxEncodedLen + + scale_info::StaticTypeInfo + + Into; + + type Timestamp: Time; + + type RemoveOrigin: EnsureOrigin; #[pallet::constant] type MaxAuthsPerMarket: Get; #[pallet::constant] @@ -45,6 +60,12 @@ pub mod pallet { type MaxFiles: Get; #[pallet::constant] type MaxApplicationsPerCustodian: Get; + #[pallet::constant] + type MaxMarketsPerItem: Get; + #[pallet::constant] + type MaxOffersPerMarket: Get; + + type Rbac : RoleBasedAccessControl; } #[pallet::pallet] @@ -63,30 +84,6 @@ pub mod pallet { OptionQuery, >; - #[pallet::storage] - #[pallet::getter(fn marketplaces_by_authority)] - pub(super) type MarketplacesByAuthority = StorageDoubleMap< - _, - Blake2_128Concat, - T::AccountId, // K1: Authority - Blake2_128Concat, - [u8;32], // K2: marketplace_id - BoundedVec, // scales with MarketplaceAuthority cardinality - ValueQuery - >; - - #[pallet::storage] - #[pallet::getter(fn authorities_by_marketplace)] - pub(super) type AuthoritiesByMarketplace = StorageDoubleMap< - _, - Identity, - [u8;32], //K1: marketplace_id - Blake2_128Concat, - MarketplaceAuthority, //k2: authority - BoundedVec, - ValueQuery - >; - #[pallet::storage] #[pallet::getter(fn applications)] pub(super) type Applications = StorageMap< @@ -103,7 +100,7 @@ pub mod pallet { _, Blake2_128Concat, T::AccountId, // K1: account_id - Blake2_128Concat, + Identity, [u8;32], // k2: marketplace_id [u8;32], //application_id OptionQuery @@ -120,7 +117,7 @@ pub mod pallet { ApplicationStatus, //K2: application_status BoundedVec, ValueQuery - >; + >; #[pallet::storage] #[pallet::getter(fn custodians)] @@ -128,14 +125,56 @@ pub mod pallet { _, Blake2_128Concat, T::AccountId, //custodians - Blake2_128Concat, + Identity, [u8;32], //marketplace_id BoundedVec, //applicants ValueQuery >; + #[pallet::storage] + #[pallet::getter(fn offers_by_item)] + pub(super) type OffersByItem = StorageDoubleMap< + _, + Blake2_128Concat, + T::CollectionId, //collection_id + Blake2_128Concat, + T::ItemId, //item_id + BoundedVec<[u8;32], T::MaxOffersPerMarket>, // offer_id's + ValueQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn offers_by_account)] + pub(super) type OffersByAccount = StorageMap< + _, + Blake2_128Concat, + T::AccountId, // account_id + BoundedVec<[u8;32], T::MaxOffersPerMarket>, // offer_id's + ValueQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn offers_by_marketplace)] + pub(super) type OffersByMarketplace = StorageMap< + _, + Identity, + [u8; 32], // Marketplace_id + BoundedVec<[u8;32], T::MaxOffersPerMarket>, // offer_id's + ValueQuery, + >; + #[pallet::storage] + #[pallet::getter(fn offers_info)] + pub(super) type OffersInfo = StorageMap< + _, + Identity, + [u8; 32], // offer_id + //StorageDoubleMap -> marketplace_id(?) + OfferData, // offer data + OptionQuery, + >; + #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] @@ -147,22 +186,28 @@ pub mod pallet { /// An applicant was accepted or rejected on the marketplace. [AccountOrApplication, market_id, status] ApplicationProcessed(AccountOrApplication,[u8;32], ApplicationStatus), /// Add a new authority to the selected marketplace [account, authority] - AuthorityAdded(T::AccountId, MarketplaceAuthority), + AuthorityAdded(T::AccountId, MarketplaceRole), /// Remove the selected authority from the selected marketplace [account, authority] - AuthorityRemoved(T::AccountId, MarketplaceAuthority), + AuthorityRemoved(T::AccountId, MarketplaceRole), /// The label of the selected marketplace has been updated. [market_id] MarketplaceLabelUpdated([u8;32]), /// The selected marketplace has been removed. [market_id] MarketplaceRemoved([u8;32]), + /// Offer stored. [collection_id, item_id] + OfferStored(T::CollectionId, T::ItemId), + /// Offer was accepted [offer_id, account] + OfferWasAccepted([u8;32], T::AccountId), + /// Offer was duplicated. [new_offer_id, new_marketplace_id] + OfferDuplicated([u8;32], [u8;32]), + /// Offer was removed. [offer_id], [marketplace_id] + OfferRemoved([u8;32], [u8;32]), } // Errors inform users that something went wrong. #[pallet::error] pub enum Error { - /// Work In Progress - NotYetImplemented, - /// Error names should be descriptive. - NoneValue, + ///Limit bounded vector exceeded + LimitExceeded, /// The account supervises too many marketplaces ExceedMaxMarketsPerAuth, /// The account has too many roles in that marketplace @@ -184,7 +229,9 @@ pub mod pallet { /// The specified marketplace does not exist MarketplaceNotFound, /// You need to be an owner or an admin of the marketplace - CannotEnroll, + NotOwnerOrAdmin, + /// There was no change regarding the application status + AlreadyEnrolled, /// There cannot be more than one owner per marketplace OnlyOneOwnerIsAllowed, /// Cannot remove the owner of the marketplace @@ -205,10 +252,49 @@ pub mod pallet { ApplicationStatusStillPending, /// The application has already been approved, application status is approved ApplicationHasAlreadyBeenApproved, + /// Collection not found + CollectionNotFound, + /// User who calls the function is not the owner of the collection + NotOwner, + /// Offer already exists + OfferAlreadyExists, + /// Offer not found + OfferNotFound, + /// Offer is not available at the moment + OfferIsNotAvailable, + /// Owner cannnot buy its own offer + CannotTakeOffer, + /// User cannot remove the offer from the marketplace + CannotRemoveOffer, + /// Error related to the timestamp + TimestampError, + /// User does not have enough balance to buy the offer + NotEnoughBalance, + /// User cannot delete the offer because is closed + CannotDeleteOffer, + /// There was a problem storing the offer + OfferStorageError, + /// Price must be greater than zero + PriceMustBeGreaterThanZero, + /// User cannot create buy offers for their own items + CannotCreateOffer, + /// This items is not available for sale + ItemNotForSale, } #[pallet::call] - impl Pallet { + impl Pallet + where + T: pallet_uniques::Config, + { + + #[transactional] + #[pallet::weight(10_000 + T::DbWeight::get().writes(10))] + pub fn initial_setup(origin: OriginFor) -> DispatchResult { + T::RemoveOrigin::ensure_origin(origin.clone())?; + Self::do_initial_setup()?; + Ok(()) + } /// Create a new marketplace. /// @@ -249,8 +335,8 @@ pub mod pallet { origin: OriginFor, marketplace_id: [u8;32], // Getting encoding errors from polkadotjs if an object vector have optional fields - fields : BoundedVec<(BoundedVec >,BoundedVec> ), T::MaxFiles>, - custodian_fields: Option<(T::AccountId, BoundedVec>, T::MaxFiles> )> + fields : Fields, + custodian_fields: Option> ) -> DispatchResult { let who = ensure_signed(origin)?; @@ -287,8 +373,8 @@ pub mod pallet { origin: OriginFor, marketplace_id: [u8;32], // Getting encoding errors from polkadotjs if an object vector have optional fields - fields : BoundedVec<(BoundedVec >,BoundedVec> ), T::MaxFiles>, - custodian_fields: Option<(T::AccountId, BoundedVec>, T::MaxFiles> )> + fields : Fields, + custodian_fields: Option> ) -> DispatchResult { let who = ensure_signed(origin)?; @@ -306,9 +392,6 @@ pub mod pallet { Self::do_apply(who, custodian, marketplace_id, application) } - - - /// Accept or reject an application. /// @@ -353,7 +436,7 @@ pub mod pallet { /// authority type, it will throw an error. #[transactional] #[pallet::weight(10_000 + T::DbWeight::get().writes(1))] - pub fn add_authority(origin: OriginFor, account: T::AccountId, authority_type: MarketplaceAuthority, marketplace_id: [u8;32]) -> DispatchResult { + pub fn add_authority(origin: OriginFor, account: T::AccountId, authority_type: MarketplaceRole, marketplace_id: [u8;32]) -> DispatchResult { let who = ensure_signed(origin)?; Self::do_authority(who, account, authority_type, marketplace_id) @@ -375,7 +458,7 @@ pub mod pallet { /// If the user doesn't have the selected authority type, it will throw an error. #[transactional] #[pallet::weight(10_000 + T::DbWeight::get().writes(1))] - pub fn remove_authority(origin: OriginFor, account: T::AccountId, authority_type: MarketplaceAuthority, marketplace_id: [u8;32]) -> DispatchResult { + pub fn remove_authority(origin: OriginFor, account: T::AccountId, authority_type: MarketplaceRole, marketplace_id: [u8;32]) -> DispatchResult { let who = ensure_signed(origin)?; //TOREVIEW: If we're allowing more than one role per user per marketplace, we should // check what role we want to remove instead of removing the user completely from @@ -424,7 +507,127 @@ pub mod pallet { Self::do_remove_marketplace(who, marketplace_id) } + + /// Enlist a sell order. + /// + /// This extrinsic creates a sell order in the selected marketplace. + /// + /// ### Parameters: + /// - `origin`: The user who performs the action. + /// - `marketplace_id`: The id of the marketplace where we want to create the sell order. + /// - `collection_id`: The id of the collection. + /// - `item_id`: The id of the item inside the collection. + /// - `price`: The price of the item. + /// + /// ### Considerations: + /// - You can only create a sell order in the marketplace if you are the owner of the item. + /// - You can create only one sell order for each item per marketplace. + /// - If the selected marketplace doesn't exist, it will throw an error. + /// - If the selected collection doesn't exist, it will throw an error. + #[transactional] + #[pallet::weight(10_000 + T::DbWeight::get().writes(1))] + pub fn enlist_sell_offer(origin: OriginFor, marketplace_id: [u8;32], collection_id: T::CollectionId, item_id: T::ItemId, price: BalanceOf,) -> DispatchResult { + let who = ensure_signed(origin)?; + + Self::do_enlist_sell_offer(who, marketplace_id, collection_id, item_id, price) + } + + /// Accepts a sell order. + /// + /// This extrisicn is called by the user who wants to buy the item. + /// Aaccepts a sell order in the selected marketplace. + /// + /// ### Parameters: + /// - `origin`: The user who performs the action. + /// - 'offer_id`: The id of the sell order to be accepted. + /// - `marketplace_id`: The id of the marketplace where we want to accept the sell order. + /// + /// ### Considerations: + /// - You don't need to be the owner of the item to accept the sell order. + /// - Once the sell order is accepted, the ownership of the item is transferred to the buyer. + /// - If you don't have the enough balance to accept the sell order, it will throw an error. + #[transactional] + #[pallet::weight(10_000 + T::DbWeight::get().writes(1))] + pub fn take_sell_offer(origin: OriginFor, offer_id: [u8;32]) -> DispatchResult { + let who = ensure_signed(origin.clone())?; + + Self::do_take_sell_offer(who, offer_id) + } + + /// Delete an offer. + /// + /// This extrinsic deletes an offer in the selected marketplace. + /// + /// ### Parameters: + /// - `origin`: The user who performs the action. + /// - `offer_id`: The id of the offer to be deleted. + /// + /// ### Considerations: + /// - You can delete sell orders or buy orders. + /// - You can only delete an offer if you are the creator of the offer. + /// - Only open offers can be deleted. + /// - If you need to delete multiple offers for the same item, you need to + /// delete them one by one. + #[transactional] + #[pallet::weight(10_000 + T::DbWeight::get().writes(1))] + pub fn remove_offer(origin: OriginFor, offer_id: [u8;32]) -> DispatchResult { + //Currently, we can only remove one offer at a time. + //TODO: Add support for removing multiple offers at a time. + let who = ensure_signed(origin.clone())?; + + Self::do_remove_offer(who, offer_id) + } + + /// Enlist a buy order. + /// + /// This extrinsic creates a buy order in the selected marketplace. + /// + /// ### Parameters: + /// - `origin`: The user who performs the action. + /// - `marketplace_id`: The id of the marketplace where we want to create the buy order. + /// - `collection_id`: The id of the collection. + /// - `item_id`: The id of the item inside the collection. + /// - `price`: The price of the item. + /// + /// ### Considerations: + /// - Any user can create a buy order in the marketplace. + /// - An item can receive multiple buy orders at a time. + /// - You need to have the enough balance to create the buy order. + #[transactional] + #[pallet::weight(10_000 + T::DbWeight::get().writes(1))] + pub fn enlist_buy_offer(origin: OriginFor, marketplace_id: [u8;32], collection_id: T::CollectionId, item_id: T::ItemId, price: BalanceOf,) -> DispatchResult { + let who = ensure_signed(origin)?; + + Self::do_enlist_buy_offer(who, marketplace_id, collection_id, item_id, price) + } + + + /// Accepts a buy order. + /// + /// This extrinsic is called by the owner of the item who accepts the buy offer created by a marketparticipant. + /// Accepts a buy order in the selected marketplace. + /// + /// ### Parameters: + /// - `origin`: The user who performs the action. + /// - `offer_id`: The id of the buy order to be accepted. + /// - `marketplace_id`: The id of the marketplace where we accept the buy order. + /// + /// ### Considerations: + /// - You need to be the owner of the item to accept a buy order. + /// - Owner of the item can accept only one buy order at a time. + /// - When an offer is accepted, all the other offers for this item are closed. + /// - The buyer needs to have the enough balance to accept the buy order. + /// - Once the buy order is accepted, the ownership of the item is transferred to the buyer. + #[transactional] + #[pallet::weight(10_000 + T::DbWeight::get().writes(1))] + pub fn take_buy_offer(origin: OriginFor, offer_id: [u8;32]) -> DispatchResult { + let who = ensure_signed(origin.clone())?; + + Self::do_take_buy_offer(who, offer_id) + } + + //TODO: Add CRUD operations for the offers /// Kill all the stored data. /// @@ -443,12 +646,11 @@ pub mod pallet { ) -> DispatchResult{ T::RemoveOrigin::ensure_origin(origin.clone())?; >::remove_all(None); - >::remove_all(None); - >::remove_all(None); >::remove_all(None); >::remove_all(None); >::remove_all(None); >::remove_all(None); + T::Rbac::remove_pallet_storage(Self::pallet_id())?; Ok(()) } diff --git a/pallets/gated-marketplace/src/mock.rs b/pallets/gated-marketplace/src/mock.rs index 03b022fb..91e481d1 100644 --- a/pallets/gated-marketplace/src/mock.rs +++ b/pallets/gated-marketplace/src/mock.rs @@ -1,5 +1,5 @@ use crate as pallet_gated_marketplace; -use frame_support::parameter_types; +use frame_support::{parameter_types, traits::AsEnsureOriginWithArg}; use frame_system as system; use sp_core::H256; use sp_runtime::{ @@ -10,6 +10,7 @@ use sp_runtime::{ type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; type Block = frame_system::mocking::MockBlock; use frame_system::EnsureRoot; +use system::EnsureSigned; // Configure a mock runtime to test the pallet. frame_support::construct_runtime!( pub enum Test where @@ -19,6 +20,11 @@ frame_support::construct_runtime!( { System: frame_system::{Pallet, Call, Config, Storage, Event}, GatedMarketplace: pallet_gated_marketplace::{Pallet, Call, Storage, Event}, + Uniques: pallet_uniques::{Pallet, Call, Storage, Event}, + Fruniques: pallet_fruniques::{Pallet, Call, Storage, Event}, + Balances: pallet_balances::{Pallet, Call, Storage, Config, Event}, + Timestamp: pallet_timestamp::{Pallet, Call, Storage, Inherent}, + RBAC: pallet_rbac::{Pallet, Call, Storage, Event}, } ); @@ -45,7 +51,7 @@ impl system::Config for Test { type BlockHashCount = BlockHashCount; type Version = (); type PalletInfo = PalletInfo; - type AccountData = (); + type AccountData = pallet_balances::AccountData; type OnNewAccount = (); type OnKilledAccount = (); type SystemWeightInfo = (); @@ -63,7 +69,9 @@ parameter_types! { pub const MaxFeedbackLen: u32 = 256; pub const NameMaxLen: u32 = 100; pub const MaxFiles: u32 = 10; - pub const MaxApplicationsPerCustodian: u32 = 2; + pub const MaxApplicationsPerCustodian: u32 = 2; + pub const MaxMarketsPerItem: u32 = 10; + pub const MaxOffersPerMarket: u32 = 100; } impl pallet_gated_marketplace::Config for Test { @@ -78,9 +86,100 @@ impl pallet_gated_marketplace::Config for Test { type NameMaxLen = NameMaxLen; type MaxFiles = MaxFiles; type MaxApplicationsPerCustodian = MaxApplicationsPerCustodian; + type MaxOffersPerMarket = MaxOffersPerMarket; + type MaxMarketsPerItem = MaxMarketsPerItem; + type Timestamp = Timestamp; + type Moment = u64; + //type LocalCurrency = Balances; + type Rbac = RBAC; +} + +impl pallet_fruniques::Config for Test { + type Event = Event; +} + +parameter_types! { + pub const ClassDeposit: u64 = 2; + pub const InstanceDeposit: u64 = 1; + pub const KeyLimit: u32 = 50; + pub const ValueLimit: u32 = 50; + pub const StringLimit: u32 = 50; + pub const MetadataDepositBase: u64 = 1; + pub const AttributeDepositBase: u64 = 1; + pub const MetadataDepositPerByte: u64 = 1; +} + +impl pallet_uniques::Config for Test { + type Event = Event; + type CollectionId = u32; + type ItemId = u32; + type Currency = Balances; + type ForceOrigin = frame_system::EnsureRoot; + type CollectionDeposit = ClassDeposit; + type ItemDeposit = InstanceDeposit; + type MetadataDepositBase = MetadataDepositBase; + type AttributeDepositBase = MetadataDepositBase; + type DepositPerByte = MetadataDepositPerByte; + type StringLimit = StringLimit; + type KeyLimit = KeyLimit; + type ValueLimit = ValueLimit; + type WeightInfo = (); + #[cfg(feature = "runtime-benchmarks")] + type Helper = (); + type CreateOrigin = AsEnsureOriginWithArg>; + type Locker = (); + +} + +parameter_types! { + pub const ExistentialDeposit: u64 = 1; + pub const MaxReserves: u32 = 50; +} + +impl pallet_balances::Config for Test { + type Balance = u64; + type DustRemoval = (); + type Event = Event; + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type WeightInfo = (); + type MaxLocks = (); + type MaxReserves = MaxReserves; + type ReserveIdentifier = [u8; 8]; +} + + +parameter_types! { + pub const MaxScopesPerPallet: u32 = 2; + pub const MaxRolesPerPallet: u32 = 6; + pub const RoleMaxLen: u32 = 25; + pub const PermissionMaxLen: u32 = 25; + pub const MaxPermissionsPerRole: u32 = 11; + pub const MaxRolesPerUser: u32 = 2; + pub const MaxUsersPerRole: u32 = 2; +} +impl pallet_rbac::Config for Test { + type Event = Event; + type MaxScopesPerPallet = MaxScopesPerPallet; + type MaxRolesPerPallet = MaxRolesPerPallet; + type RoleMaxLen = RoleMaxLen; + type PermissionMaxLen = PermissionMaxLen; + type MaxPermissionsPerRole = MaxPermissionsPerRole; + type MaxRolesPerUser = MaxRolesPerUser; + type MaxUsersPerRole = MaxUsersPerRole; } // Build genesis storage according to the mock runtime. pub fn new_test_ext() -> sp_io::TestExternalities { - frame_system::GenesisConfig::default().build_storage::().unwrap().into() + // TODO: get initial conf? + let mut t: sp_io::TestExternalities = frame_system::GenesisConfig::default().build_storage::().unwrap().into(); + t.execute_with(|| GatedMarketplace::do_initial_setup().expect("Error on configuring initial setup")); + t } + +impl pallet_timestamp::Config for Test { + type Moment = u64; + type OnTimestampSet = (); + type MinimumPeriod = (); + type WeightInfo = (); +} \ No newline at end of file diff --git a/pallets/gated-marketplace/src/tests.rs b/pallets/gated-marketplace/src/tests.rs index c80837b3..2e252042 100644 --- a/pallets/gated-marketplace/src/tests.rs +++ b/pallets/gated-marketplace/src/tests.rs @@ -1,10 +1,17 @@ -use crate::{mock::*, Error, types::*, Custodians}; +use crate::{mock::*, Error, types::*, Config}; use std::vec; use sp_runtime::sp_std::vec::Vec; use codec::Encode; -use frame_support::{assert_ok, BoundedVec, traits::{Len, ConstU32}, assert_noop}; +use frame_support::{assert_ok, BoundedVec, traits::{Len, ConstU32, Currency}, assert_noop}; +use pallet_rbac::types::RoleBasedAccessControl; use sp_io::hashing::blake2_256; +type RbacErr = pallet_rbac::Error; + + +fn pallet_id() ->[u8;32]{ + GatedMarketplace::pallet_id().to_id() +} fn create_label( label : &str ) -> BoundedVec { let s: Vec = label.as_bytes().into(); s.try_into().unwrap_or_default() @@ -28,6 +35,10 @@ fn boundedvec_to_string(boundedvec: &BoundedVec) -> String { s } +fn _find_id(vec_tor: BoundedVec<[u8;32], ConstU32<100>>, id:[u8;32]) -> bool { + vec_tor.iter().find(|&x| *x == id).ok_or(Error::::OfferNotFound).is_ok() +} + fn _create_file(name: &str, cid: &str, create_custodian_file: bool) -> ApplicationField { let display_name_vec: Vec = name.as_bytes().into(); let display_name: BoundedVec> = display_name_vec.try_into().unwrap_or_default(); @@ -42,7 +53,7 @@ fn _create_file(name: &str, cid: &str, create_custodian_file: bool) -> Applicati // due to encoding problems with polkadot-js, the custodians_cid generation will be done in another function fn create_application_fields( n_files: u32) -> - BoundedVec<(BoundedVec >,BoundedVec> ), MaxFiles> { + BoundedVec<(BoundedVec >,BoundedVec> ), MaxFiles> { let mut files = Vec::<(BoundedVec >,BoundedVec> )>::default(); for i in 0..n_files{ let file_name = format!("file{}",i.to_string()); @@ -86,12 +97,16 @@ fn duplicate_marketplaces_shouldnt_work() { } #[test] -fn exceeding_max_markets_per_auth_shouldnt_work() { +fn exceeding_max_roles_per_auth_shouldnt_work() { new_test_ext().execute_with(|| { - assert_ok!(GatedMarketplace::create_marketplace(Origin::signed(1),2, create_label("my marketplace") )); - assert_noop!(GatedMarketplace::create_marketplace(Origin::signed(3),3, create_label("my marketplace 2")), Error::::ExceedMaxRolesPerAuth ); + let m_label = create_label("my marketplace"); + assert_ok!(GatedMarketplace::create_marketplace(Origin::signed(1),2, m_label.clone() )); + assert_ok!(GatedMarketplace::add_authority(Origin::signed(1), 2, MarketplaceRole::Appraiser, m_label.using_encoded(blake2_256))); + assert_noop!( + GatedMarketplace::add_authority(Origin::signed(1), 2, MarketplaceRole::RedemptionSpecialist, m_label.using_encoded(blake2_256) ), + RbacErr::ExceedMaxRolesPerUser + ); - // TODO: test ExceedMaxMarketsPerAuth when its possible to add new authorities }); } @@ -197,7 +212,7 @@ fn enroll_works() { // enroll with account assert_ok!(GatedMarketplace::enroll(Origin::signed(1), m_id , AccountOrApplication::Account(3), true, default_feedback())); // enroll with application - assert_ok!(GatedMarketplace::enroll(Origin::signed(1), m_id , AccountOrApplication::Application(app_id), true, default_feedback())); + assert_ok!(GatedMarketplace::enroll(Origin::signed(1), m_id , AccountOrApplication::Application(app_id), false, default_feedback())); }); } @@ -209,7 +224,7 @@ fn enroll_rejected_works() { let m_id = create_label("my marketplace").using_encoded(blake2_256); assert_ok!(GatedMarketplace::apply(Origin::signed(3),m_id, create_application_fields(2), None )); assert_ok!(GatedMarketplace::apply(Origin::signed(4),m_id, create_application_fields(1), None )); - let app_id = GatedMarketplace::applications_by_account(3,m_id).unwrap(); + let app_id = GatedMarketplace::applications_by_account(4,m_id).unwrap(); // reject with account assert_ok!(GatedMarketplace::enroll(Origin::signed(1), m_id , AccountOrApplication::Account(3), false, default_feedback())); // reject with application @@ -227,7 +242,7 @@ fn enroll_rejected_has_feedback_works() { assert_ok!(GatedMarketplace::apply(Origin::signed(4),m_id, create_application_fields(1), None )); let app_id = GatedMarketplace::applications_by_account(3,m_id).unwrap(); // reject with account - assert_ok!(GatedMarketplace::enroll(Origin::signed(1), m_id , AccountOrApplication::Account(3), false, feedback("We need to reject this application"))); + assert_ok!(GatedMarketplace::enroll(Origin::signed(1), m_id , AccountOrApplication::Account(3), true, feedback("We need to accept this application"))); // reject with application assert_ok!(GatedMarketplace::enroll(Origin::signed(1), m_id , AccountOrApplication::Application(app_id), false, feedback("We need to reject this application"))); @@ -243,13 +258,13 @@ fn enroll_approved_has_feedback_works() { let m_id = create_label("my marketplace").using_encoded(blake2_256); assert_ok!(GatedMarketplace::apply(Origin::signed(3),m_id, create_application_fields(2), None )); assert_ok!(GatedMarketplace::apply(Origin::signed(4),m_id, create_application_fields(1), None )); - let app_id = GatedMarketplace::applications_by_account(3,m_id).unwrap(); + let app_id = GatedMarketplace::applications_by_account(4,m_id).unwrap(); // reject with account - assert_ok!(GatedMarketplace::enroll(Origin::signed(1), m_id , AccountOrApplication::Account(3), true, feedback("We've accepted your publication"))); + assert_ok!(GatedMarketplace::enroll(Origin::signed(1), m_id , AccountOrApplication::Account(3), true, feedback("We've rejected your publication"))); // reject with application - assert_ok!(GatedMarketplace::enroll(Origin::signed(1), m_id , AccountOrApplication::Application(app_id), true, feedback("We've accepted your publication"))); + assert_ok!(GatedMarketplace::enroll(Origin::signed(1), m_id , AccountOrApplication::Application(app_id), true, feedback("We've rejected your publication"))); - assert_eq!(boundedvec_to_string(&GatedMarketplace::applications(app_id).unwrap().feedback), String::from("We've accepted your publication")); + assert_eq!(boundedvec_to_string(&GatedMarketplace::applications(app_id).unwrap().feedback), String::from("We've rejected your publication")); }); } @@ -277,7 +292,7 @@ fn non_authorized_user_enroll_shouldnt_work() { assert_ok!(GatedMarketplace::apply(Origin::signed(3),m_id, create_application_fields(2), None )); // external user tries to enroll someone - assert_noop!(GatedMarketplace::enroll(Origin::signed(4), m_id , AccountOrApplication::Account(3), true, default_feedback()), Error::::CannotEnroll); + assert_noop!(GatedMarketplace::enroll(Origin::signed(4), m_id , AccountOrApplication::Account(3), true, default_feedback()), RbacErr::NotAuthorized); }); } @@ -301,8 +316,9 @@ fn add_authority_appraiser_works() { new_test_ext().execute_with(|| { assert_ok!(GatedMarketplace::create_marketplace(Origin::signed(1),2, create_label("my marketplace") )); let m_id = create_label("my marketplace").using_encoded(blake2_256); - assert_ok!(GatedMarketplace::add_authority(Origin::signed(1), 3, MarketplaceAuthority::Appraiser, m_id)); - assert!(GatedMarketplace::marketplaces_by_authority(3, m_id) == vec![MarketplaceAuthority::Appraiser]); + assert_ok!(GatedMarketplace::add_authority(Origin::signed(1), 3, MarketplaceRole::Appraiser, m_id)); + //assert!(GatedMarketplace::marketplaces_by_authority(3, m_id) == vec![MarketplaceRole::Appraiser]); + assert!(RBAC::roles_by_user((3, pallet_id(), m_id)).contains(&MarketplaceRole::Appraiser.id())); }); } @@ -311,8 +327,9 @@ fn add_authority_admin_works() { new_test_ext().execute_with(|| { assert_ok!(GatedMarketplace::create_marketplace(Origin::signed(1),2, create_label("my marketplace") )); let m_id = create_label("my marketplace").using_encoded(blake2_256); - assert_ok!(GatedMarketplace::add_authority(Origin::signed(1), 3, MarketplaceAuthority::Admin, m_id)); - assert!(GatedMarketplace::marketplaces_by_authority(3, m_id) == vec![MarketplaceAuthority::Admin]); + assert_ok!(GatedMarketplace::add_authority(Origin::signed(1), 3, MarketplaceRole::Admin, m_id)); + //assert!(GatedMarketplace::marketplaces_by_authority(3, m_id) == vec![MarketplaceRole::Admin]); + assert!(RBAC::roles_by_user((3, pallet_id(), m_id)).contains(&MarketplaceRole::Admin.id())); }); } @@ -321,8 +338,9 @@ fn add_authority_redenmption_specialist_works() { new_test_ext().execute_with(|| { assert_ok!(GatedMarketplace::create_marketplace(Origin::signed(1),2, create_label("my marketplace") )); let m_id = create_label("my marketplace").using_encoded(blake2_256); - assert_ok!(GatedMarketplace::add_authority(Origin::signed(1), 3, MarketplaceAuthority::RedemptionSpecialist, m_id)); - assert!(GatedMarketplace::marketplaces_by_authority(3, m_id) == vec![MarketplaceAuthority::RedemptionSpecialist]); + assert_ok!(GatedMarketplace::add_authority(Origin::signed(1), 3, MarketplaceRole::RedemptionSpecialist, m_id)); + //assert!(GatedMarketplace::marketplaces_by_authority(3, m_id) == vec![MarketplaceRole::RedemptionSpecialist]); + assert!(RBAC::roles_by_user((3, pallet_id(), m_id)).contains(&MarketplaceRole::RedemptionSpecialist.id())); }); } @@ -331,7 +349,9 @@ fn add_authority_owner_shouldnt_work() { new_test_ext().execute_with(|| { assert_ok!(GatedMarketplace::create_marketplace(Origin::signed(1),2, create_label("my marketplace") )); let m_id = create_label("my marketplace").using_encoded(blake2_256); - assert_noop!(GatedMarketplace::add_authority(Origin::signed(1), 3, MarketplaceAuthority::Owner, m_id), Error::::OnlyOneOwnerIsAllowed); + assert_noop!(GatedMarketplace::add_authority(Origin::signed(1), 3, MarketplaceRole::Owner, m_id), Error::::OnlyOneOwnerIsAllowed); + let n_owners = ::Rbac::get_role_users_len(GatedMarketplace::pallet_id(), &m_id, &MarketplaceRole::Owner.id()); + assert_eq!(n_owners, 1); }); } @@ -340,8 +360,8 @@ fn add_authority_cant_apply_twice_shouldnt_work(){ new_test_ext().execute_with(|| { assert_ok!(GatedMarketplace::create_marketplace(Origin::signed(1),2, create_label("my marketplace") )); let m_id = create_label("my marketplace").using_encoded(blake2_256); - assert_ok!(GatedMarketplace::add_authority(Origin::signed(1), 3, MarketplaceAuthority::Appraiser, m_id)); - assert_noop!(GatedMarketplace::add_authority(Origin::signed(1), 3, MarketplaceAuthority::Appraiser, m_id), Error::::AlreadyApplied); + assert_ok!(GatedMarketplace::add_authority(Origin::signed(1), 3, MarketplaceRole::Appraiser, m_id)); + assert_noop!(GatedMarketplace::add_authority(Origin::signed(1), 3, MarketplaceRole::Appraiser, m_id), RbacErr::UserAlreadyHasRole); }); } @@ -353,9 +373,10 @@ fn remove_authority_appraiser_works() { new_test_ext().execute_with(|| { assert_ok!(GatedMarketplace::create_marketplace(Origin::signed(1),2, create_label("my marketplace") )); let m_id = create_label("my marketplace").using_encoded(blake2_256); - assert_ok!(GatedMarketplace::add_authority(Origin::signed(1), 3, MarketplaceAuthority::Appraiser, m_id)); - assert_ok!(GatedMarketplace::remove_authority(Origin::signed(1), 3, MarketplaceAuthority::Appraiser, m_id)); - assert!(GatedMarketplace::marketplaces_by_authority(3, m_id) == vec![]); + assert_ok!(GatedMarketplace::add_authority(Origin::signed(1), 3, MarketplaceRole::Appraiser, m_id)); + assert_ok!(GatedMarketplace::remove_authority(Origin::signed(1), 3, MarketplaceRole::Appraiser, m_id)); + //assert!(GatedMarketplace::marketplaces_by_authority(3, m_id) == vec![]); + assert!(RBAC::roles_by_user((3, pallet_id(), m_id)).is_empty()); }); } @@ -364,9 +385,10 @@ fn remove_authority_admin_works() { new_test_ext().execute_with(|| { assert_ok!(GatedMarketplace::create_marketplace(Origin::signed(1),2, create_label("my marketplace") )); let m_id = create_label("my marketplace").using_encoded(blake2_256); - assert_ok!(GatedMarketplace::add_authority(Origin::signed(1), 3, MarketplaceAuthority::Admin, m_id)); - assert_ok!(GatedMarketplace::remove_authority(Origin::signed(1), 3, MarketplaceAuthority::Admin, m_id)); - assert!(GatedMarketplace::marketplaces_by_authority(3, m_id) == vec![]); + assert_ok!(GatedMarketplace::add_authority(Origin::signed(1), 3, MarketplaceRole::Admin, m_id)); + assert_ok!(GatedMarketplace::remove_authority(Origin::signed(1), 3, MarketplaceRole::Admin, m_id)); + //assert!(GatedMarketplace::marketplaces_by_authority(3, m_id) == vec![]); + assert!(RBAC::roles_by_user((3, pallet_id(), m_id)).is_empty()); }); } @@ -375,9 +397,10 @@ fn remove_authority_redemption_specialist_work() { new_test_ext().execute_with(|| { assert_ok!(GatedMarketplace::create_marketplace(Origin::signed(1),2, create_label("my marketplace") )); let m_id = create_label("my marketplace").using_encoded(blake2_256); - assert_ok!(GatedMarketplace::add_authority(Origin::signed(1), 3, MarketplaceAuthority::RedemptionSpecialist, m_id)); - assert_ok!(GatedMarketplace::remove_authority(Origin::signed(1), 3, MarketplaceAuthority::RedemptionSpecialist, m_id)); - assert!(GatedMarketplace::marketplaces_by_authority(3, m_id) == vec![]); + assert_ok!(GatedMarketplace::add_authority(Origin::signed(1), 3, MarketplaceRole::RedemptionSpecialist, m_id)); + assert_ok!(GatedMarketplace::remove_authority(Origin::signed(1), 3, MarketplaceRole::RedemptionSpecialist, m_id)); + //assert!(GatedMarketplace::marketplaces_by_authority(3, m_id) == vec![]); + assert!(RBAC::roles_by_user((3, pallet_id(), m_id)).is_empty()); }); } @@ -386,7 +409,7 @@ fn remove_authority_owner_shouldnt_work(){ new_test_ext().execute_with(|| { assert_ok!(GatedMarketplace::create_marketplace(Origin::signed(1),2, create_label("my marketplace") )); let m_id = create_label("my marketplace").using_encoded(blake2_256); - assert_noop!(GatedMarketplace::remove_authority(Origin::signed(1), 1 , MarketplaceAuthority::Owner, m_id), Error::::CantRemoveOwner); + assert_noop!(GatedMarketplace::remove_authority(Origin::signed(1), 1 , MarketplaceRole::Owner, m_id), Error::::CantRemoveOwner); }); } @@ -397,8 +420,8 @@ fn remove_authority_admin_by_admin_shouldnt_work(){ new_test_ext().execute_with(|| { assert_ok!(GatedMarketplace::create_marketplace(Origin::signed(1),2, create_label("my marketplace") )); let m_id = create_label("my marketplace").using_encoded(blake2_256); - assert_ok!(GatedMarketplace::add_authority(Origin::signed(1), 3, MarketplaceAuthority::Admin, m_id)); - assert_noop!(GatedMarketplace::remove_authority(Origin::signed(3), 3, MarketplaceAuthority::Admin, m_id), Error::::AdminCannotRemoveItself); + assert_ok!(GatedMarketplace::add_authority(Origin::signed(1), 3, MarketplaceRole::Admin, m_id)); + assert_noop!(GatedMarketplace::remove_authority(Origin::signed(3), 3, MarketplaceRole::Admin, m_id), Error::::AdminCannotRemoveItself); }); } @@ -407,8 +430,8 @@ fn remove_authority_user_tries_to_remove_non_existent_role_shouldnt_work(){ new_test_ext().execute_with(|| { assert_ok!(GatedMarketplace::create_marketplace(Origin::signed(1),2, create_label("my marketplace") )); let m_id = create_label("my marketplace").using_encoded(blake2_256); - assert_ok!(GatedMarketplace::add_authority(Origin::signed(1), 3, MarketplaceAuthority::Appraiser, m_id)); - assert_noop!(GatedMarketplace::remove_authority(Origin::signed(1), 3, MarketplaceAuthority::Admin, m_id), Error::::AuthorityNotFoundForUser); + assert_ok!(GatedMarketplace::add_authority(Origin::signed(1), 3, MarketplaceRole::Appraiser, m_id)); + assert_noop!(GatedMarketplace::remove_authority(Origin::signed(1), 3, MarketplaceRole::Admin, m_id), RbacErr::RoleNotFound); }); } @@ -417,9 +440,9 @@ fn remove_authority_user_is_not_admin_or_owner_shouldnt_work(){ new_test_ext().execute_with(|| { assert_ok!(GatedMarketplace::create_marketplace(Origin::signed(1),2, create_label("my marketplace") )); let m_id = create_label("my marketplace").using_encoded(blake2_256); - assert_ok!(GatedMarketplace::add_authority(Origin::signed(1), 3, MarketplaceAuthority::Appraiser, m_id)); - assert_ok!(GatedMarketplace::add_authority(Origin::signed(1), 4, MarketplaceAuthority::Admin, m_id)); - assert_noop!(GatedMarketplace::remove_authority(Origin::signed(3), 4,MarketplaceAuthority::Appraiser, m_id), Error::::CannotEnroll); + assert_ok!(GatedMarketplace::add_authority(Origin::signed(1), 3, MarketplaceRole::Appraiser, m_id)); + assert_ok!(GatedMarketplace::add_authority(Origin::signed(1), 4, MarketplaceRole::Admin, m_id)); + assert_noop!(GatedMarketplace::remove_authority(Origin::signed(3), 4,MarketplaceRole::Appraiser, m_id), RbacErr::NotAuthorized); }); } @@ -428,8 +451,8 @@ fn remove_authority_only_owner_can_remove_admins_works(){ new_test_ext().execute_with(|| { assert_ok!(GatedMarketplace::create_marketplace(Origin::signed(1),2, create_label("my marketplace") )); let m_id = create_label("my marketplace").using_encoded(blake2_256); - assert_ok!(GatedMarketplace::add_authority(Origin::signed(1), 3, MarketplaceAuthority::Admin, m_id)); - assert_ok!(GatedMarketplace::remove_authority(Origin::signed(1), 3, MarketplaceAuthority::Admin, m_id)); + assert_ok!(GatedMarketplace::add_authority(Origin::signed(1), 3, MarketplaceRole::Admin, m_id)); + assert_ok!(GatedMarketplace::remove_authority(Origin::signed(1), 3, MarketplaceRole::Admin, m_id)); }); } @@ -453,8 +476,8 @@ fn update_marketplace_user_without_permission_shouldnt_work(){ //user should be an admin or owner assert_ok!(GatedMarketplace::create_marketplace(Origin::signed(1),2, create_label("my marketplace") )); let m_id = create_label("my marketplace").using_encoded(blake2_256); - assert_ok!(GatedMarketplace::add_authority(Origin::signed(1), 3, MarketplaceAuthority::Appraiser, m_id)); - assert_noop!(GatedMarketplace::update_label_marketplace(Origin::signed(3), m_id, create_label("my marketplace2")), Error::::CannotEnroll); + assert_ok!(GatedMarketplace::add_authority(Origin::signed(1), 3, MarketplaceRole::Appraiser, m_id)); + assert_noop!(GatedMarketplace::update_label_marketplace(Origin::signed(3), m_id, create_label("my marketplace2")), RbacErr::NotAuthorized); }); } @@ -490,8 +513,8 @@ fn remove_marketplace_user_without_permission_shouldnt_work(){ new_test_ext().execute_with(|| { assert_ok!(GatedMarketplace::create_marketplace(Origin::signed(1),2, create_label("my marketplace") )); let m_id = create_label("my marketplace").using_encoded(blake2_256); - assert_ok!(GatedMarketplace::add_authority(Origin::signed(1), 3, MarketplaceAuthority::Appraiser, m_id)); - assert_noop!(GatedMarketplace::remove_marketplace(Origin::signed(3), m_id), Error::::CannotEnroll); + assert_ok!(GatedMarketplace::add_authority(Origin::signed(1), 3, MarketplaceRole::Appraiser, m_id)); + assert_noop!(GatedMarketplace::remove_marketplace(Origin::signed(3), m_id), RbacErr::NotAuthorized); }); } @@ -513,20 +536,28 @@ fn remove_marketplace_deletes_storage_from_marketplaces_by_authority_works(){ let m_id = create_label("my marketplace").using_encoded(blake2_256); assert!(GatedMarketplace::marketplaces(m_id).is_some()); - assert_ok!(GatedMarketplace::add_authority(Origin::signed(1), 3, MarketplaceAuthority::Appraiser, m_id)); - assert_ok!(GatedMarketplace::add_authority(Origin::signed(1), 4, MarketplaceAuthority::RedemptionSpecialist, m_id)); + assert_ok!(GatedMarketplace::add_authority(Origin::signed(1), 3, MarketplaceRole::Appraiser, m_id)); + assert_ok!(GatedMarketplace::add_authority(Origin::signed(1), 4, MarketplaceRole::RedemptionSpecialist, m_id)); - assert!(GatedMarketplace::marketplaces_by_authority(1, m_id) == vec![MarketplaceAuthority::Owner]); - assert!(GatedMarketplace::marketplaces_by_authority(2, m_id) == vec![MarketplaceAuthority::Admin]); - assert!(GatedMarketplace::marketplaces_by_authority(3, m_id) == vec![MarketplaceAuthority::Appraiser]); - assert!(GatedMarketplace::marketplaces_by_authority(4, m_id) == vec![MarketplaceAuthority::RedemptionSpecialist]); - + //assert!(GatedMarketplace::marketplaces_by_authority(1, m_id) == vec![MarketplaceRole::Owner]); + assert_ok!(::Rbac::has_role(1, GatedMarketplace::pallet_id(), &m_id, vec![MarketplaceRole::Owner.id()])); + //assert!(GatedMarketplace::marketplaces_by_authority(2, m_id) == vec![MarketplaceRole::Admin]); + assert_ok!(::Rbac::has_role(2, GatedMarketplace::pallet_id(), &m_id, vec![MarketplaceRole::Admin.id()])); + //assert!(GatedMarketplace::marketplaces_by_authority(3, m_id) == vec![MarketplaceRole::Appraiser]); + assert_ok!(::Rbac::has_role(3, GatedMarketplace::pallet_id(), &m_id, vec![MarketplaceRole::Appraiser.id()])); + //assert!(GatedMarketplace::marketplaces_by_authority(4, m_id) == vec![MarketplaceRole::RedemptionSpecialist]); + assert_ok!(::Rbac::has_role(4, GatedMarketplace::pallet_id(), &m_id, vec![MarketplaceRole::RedemptionSpecialist.id()])); assert_ok!(GatedMarketplace::remove_marketplace(Origin::signed(1), m_id)); - assert!(GatedMarketplace::marketplaces_by_authority(1, m_id) == vec![]); - assert!(GatedMarketplace::marketplaces_by_authority(2, m_id) == vec![]); - assert!(GatedMarketplace::marketplaces_by_authority(3, m_id) == vec![]); - assert!(GatedMarketplace::marketplaces_by_authority(4, m_id) == vec![]); + //assert!(GatedMarketplace::marketplaces_by_authority(1, m_id) == vec![]); + assert!(RBAC::roles_by_user((1, pallet_id(), m_id)).is_empty()); + //assert!(GatedMarketplace::marketplaces_by_authority(2, m_id) == vec![]); + assert!(RBAC::roles_by_user((2, pallet_id(), m_id)).is_empty()); + //assert!(GatedMarketplace::marketplaces_by_authority(3, m_id) == vec![]); + assert!(RBAC::roles_by_user((3, pallet_id(), m_id)).is_empty()); + //assert!(GatedMarketplace::marketplaces_by_authority(4, m_id) == vec![]); + assert!(RBAC::roles_by_user((4, pallet_id(), m_id)).is_empty()); + assert!(GatedMarketplace::marketplaces(m_id).is_none()); }); } @@ -538,20 +569,28 @@ fn remove_marketplace_deletes_storage_from_authorities_by_marketplace_works(){ let m_id = create_label("my marketplace").using_encoded(blake2_256); assert!(GatedMarketplace::marketplaces(m_id).is_some()); - assert_ok!(GatedMarketplace::add_authority(Origin::signed(1), 3, MarketplaceAuthority::Appraiser, m_id)); - assert_ok!(GatedMarketplace::add_authority(Origin::signed(1), 4, MarketplaceAuthority::RedemptionSpecialist, m_id)); + assert_ok!(GatedMarketplace::add_authority(Origin::signed(1), 3, MarketplaceRole::Appraiser, m_id)); + assert_ok!(GatedMarketplace::add_authority(Origin::signed(1), 4, MarketplaceRole::RedemptionSpecialist, m_id)); - assert!(GatedMarketplace::authorities_by_marketplace(m_id, MarketplaceAuthority::Owner) == vec![1]); - assert!(GatedMarketplace::authorities_by_marketplace(m_id, MarketplaceAuthority::Admin) == vec![2]); - assert!(GatedMarketplace::authorities_by_marketplace(m_id, MarketplaceAuthority::Appraiser) == vec![3]); - assert!(GatedMarketplace::authorities_by_marketplace(m_id, MarketplaceAuthority::RedemptionSpecialist) == vec![4]); + //assert!(GatedMarketplace::authorities_by_marketplace(m_id, MarketplaceRole::Owner) == vec![1]); + assert!(RBAC::users_by_scope((pallet_id(), m_id, MarketplaceRole::Owner.id())).contains(&1)); + //assert!(GatedMarketplace::authorities_by_marketplace(m_id, MarketplaceRole::Admin) == vec![2]); + assert!(RBAC::users_by_scope((pallet_id(), m_id, MarketplaceRole::Admin.id())).contains(&2)); + //assert!(GatedMarketplace::authorities_by_marketplace(m_id, MarketplaceRole::Appraiser) == vec![3]); + assert!(RBAC::users_by_scope((pallet_id(), m_id, MarketplaceRole::Appraiser.id())).contains(&3)); + //assert!(GatedMarketplace::authorities_by_marketplace(m_id, MarketplaceRole::RedemptionSpecialist) == vec![4]); + assert!(RBAC::users_by_scope((pallet_id(), m_id, MarketplaceRole::RedemptionSpecialist.id())).contains(&4)); assert_ok!(GatedMarketplace::remove_marketplace(Origin::signed(1), m_id)); - assert!(GatedMarketplace::authorities_by_marketplace(m_id, MarketplaceAuthority::Owner) == vec![]); - assert!(GatedMarketplace::authorities_by_marketplace(m_id, MarketplaceAuthority::Admin) == vec![]); - assert!(GatedMarketplace::authorities_by_marketplace(m_id, MarketplaceAuthority::Appraiser) == vec![]); - assert!(GatedMarketplace::authorities_by_marketplace(m_id, MarketplaceAuthority::RedemptionSpecialist) == vec![]); + //assert!(GatedMarketplace::authorities_by_marketplace(m_id, MarketplaceRole::Owner) == vec![]); + assert!(RBAC::users_by_scope((pallet_id(), m_id, MarketplaceRole::Owner.id())).is_empty()); + //assert!(GatedMarketplace::authorities_by_marketplace(m_id, MarketplaceRole::Admin) == vec![]); + assert!(RBAC::users_by_scope((pallet_id(), m_id, MarketplaceRole::Admin.id())).is_empty()); + //assert!(GatedMarketplace::authorities_by_marketplace(m_id, MarketplaceRole::Appraiser) == vec![]); + assert!(RBAC::users_by_scope((pallet_id(), m_id, MarketplaceRole::Appraiser.id())).is_empty()); + //assert!(GatedMarketplace::authorities_by_marketplace(m_id, MarketplaceRole::RedemptionSpecialist) == vec![]); + assert!(RBAC::users_by_scope((pallet_id(), m_id, MarketplaceRole::RedemptionSpecialist.id())).is_empty()); assert!(GatedMarketplace::marketplaces(m_id).is_none()); }); } @@ -653,7 +692,7 @@ fn remove_marketplace_deletes_storage_from_applicantions_by_account_works(){ let app_id = GatedMarketplace::applications_by_account(3,m_id).unwrap(); assert!(GatedMarketplace::applicants_by_marketplace(m_id, ApplicationStatus::Pending) == vec![3]); assert_ok!(GatedMarketplace::enroll(Origin::signed(1), m_id , AccountOrApplication::Account(3), true, default_feedback())); - assert_ok!(GatedMarketplace::enroll(Origin::signed(1), m_id , AccountOrApplication::Application(app_id), true, default_feedback())); + assert_ok!(GatedMarketplace::enroll(Origin::signed(1), m_id , AccountOrApplication::Application(app_id), false, default_feedback())); assert!(GatedMarketplace::applications_by_account(3, m_id).is_some()); assert_ok!(GatedMarketplace::remove_marketplace(Origin::signed(1), m_id)); @@ -676,7 +715,7 @@ fn remove_marketplace_deletes_storage_from_applications_works(){ assert!(GatedMarketplace::applicants_by_marketplace(m_id, ApplicationStatus::Pending) == vec![3]); assert_ok!(GatedMarketplace::enroll(Origin::signed(1), m_id , AccountOrApplication::Account(3), true,default_feedback())); - assert_ok!(GatedMarketplace::enroll(Origin::signed(1), m_id , AccountOrApplication::Application(app_id), true, default_feedback())); + assert_ok!(GatedMarketplace::enroll(Origin::signed(1), m_id , AccountOrApplication::Application(app_id), false, default_feedback())); assert!(GatedMarketplace::applications(app_id).is_some()); assert_ok!(GatedMarketplace::remove_marketplace(Origin::signed(1), m_id)); @@ -757,3 +796,622 @@ fn reapply_works() { }); } +//Offers +#[test] +fn enlist_sell_offer_works() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + + assert_ok!(GatedMarketplace::create_marketplace(Origin::signed(1), 2, create_label("my marketplace"))); + let m_id = create_label("my marketplace").using_encoded(blake2_256); + + assert_ok!(Uniques::create(Origin::signed(1), 0, 1)); + assert_ok!(Uniques::mint(Origin::signed(1), 0, 0, 1)); + assert_eq!(Uniques::owner(0, 0).unwrap(), 1); + + assert_ok!(GatedMarketplace::enlist_sell_offer(Origin::signed(1), m_id, 0, 0, 10000)); + let offer_id = GatedMarketplace::offers_by_item(0, 0).iter().next().unwrap().clone(); + assert!(GatedMarketplace::offers_info(offer_id).is_some()); + assert_eq!(GatedMarketplace::offers_info(offer_id).unwrap().offer_type, OfferType::SellOrder); + + }); +} + + +#[test] +fn enlist_sell_offer_item_does_not_exist_shouldnt_work() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + + assert_ok!(GatedMarketplace::create_marketplace(Origin::signed(1), 2, create_label("my marketplace"))); + let m_id = create_label("my marketplace").using_encoded(blake2_256); + + assert_ok!(Uniques::create(Origin::signed(1), 0, 1)); + assert_ok!(Uniques::mint(Origin::signed(1), 0, 0, 1)); + assert_eq!(Uniques::owner(0, 0).unwrap(), 1); + + assert_noop!(GatedMarketplace::enlist_sell_offer(Origin::signed(1), m_id, 0, 1, 10000), Error::::CollectionNotFound); + }); +} + + +#[test] +fn enlist_sell_offer_item_already_enlisted_shouldnt_work() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + + assert_ok!(GatedMarketplace::create_marketplace(Origin::signed(1), 2, create_label("my marketplace"))); + let m_id = create_label("my marketplace").using_encoded(blake2_256); + + assert_ok!(Uniques::create(Origin::signed(1), 0, 1)); + assert_ok!(Uniques::mint(Origin::signed(1), 0, 0, 1)); + assert_eq!(Uniques::owner(0, 0).unwrap(), 1); + + assert_ok!(GatedMarketplace::enlist_sell_offer(Origin::signed(1), m_id, 0, 0, 10000)); + let offer_id = GatedMarketplace::offers_by_item(0, 0).iter().next().unwrap().clone(); + assert!(GatedMarketplace::offers_info(offer_id).is_some()); + assert_noop!(GatedMarketplace::enlist_sell_offer(Origin::signed(1), m_id, 0, 0, 10000), Error::::OfferAlreadyExists); + }); +} + +#[test] +fn enlist_sell_offer_not_owner_tries_to_enlist_shouldnt_work() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&2, 100); + + assert_ok!(GatedMarketplace::create_marketplace(Origin::signed(1), 2, create_label("my marketplace"))); + let m_id = create_label("my marketplace").using_encoded(blake2_256); + + assert_ok!(Uniques::create(Origin::signed(1), 0, 1)); + assert_ok!(Uniques::mint(Origin::signed(1), 0, 0, 1)); + assert_eq!(Uniques::owner(0, 0).unwrap(), 1); + + assert_noop!(GatedMarketplace::enlist_sell_offer(Origin::signed(2), m_id, 0, 0, 10000), Error::::NotOwner); + }); +} + +#[test] +fn enlist_sell_offer_price_must_greater_than_zero_shouldnt_work() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + + assert_ok!(GatedMarketplace::create_marketplace(Origin::signed(1), 2, create_label("my marketplace"))); + let m_id = create_label("my marketplace").using_encoded(blake2_256); + + assert_ok!(Uniques::create(Origin::signed(1), 0, 1)); + assert_ok!(Uniques::mint(Origin::signed(1), 0, 0, 1)); + assert_eq!(Uniques::owner(0, 0).unwrap(), 1); + + assert_noop!(GatedMarketplace::enlist_sell_offer(Origin::signed(1), m_id, 0, 0, 0), Error::::PriceMustBeGreaterThanZero); + }); +} + +#[test] +fn enlist_sell_offer_price_must_greater_than_minimun_amount_works() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + + assert_ok!(GatedMarketplace::create_marketplace(Origin::signed(1), 2, create_label("my marketplace"))); + let m_id = create_label("my marketplace").using_encoded(blake2_256); + + assert_ok!(Uniques::create(Origin::signed(1), 0, 1)); + assert_ok!(Uniques::mint(Origin::signed(1), 0, 0, 1)); + assert_eq!(Uniques::owner(0, 0).unwrap(), 1); + + let minimum_amount = 1001; + assert_ok!(GatedMarketplace::enlist_sell_offer(Origin::signed(1), m_id, 0, 0, minimum_amount)); + let offer_id = GatedMarketplace::offers_by_item(0, 0).iter().next().unwrap().clone(); + assert!(GatedMarketplace::offers_info(offer_id).is_some()); + }); +} + +#[test] +fn enlist_sell_offer_is_properly_stored_works() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + + assert_ok!(GatedMarketplace::create_marketplace(Origin::signed(1), 2, create_label("my marketplace"))); + let m_id = create_label("my marketplace").using_encoded(blake2_256); + + assert_ok!(Uniques::create(Origin::signed(1), 0, 1)); + assert_ok!(Uniques::mint(Origin::signed(1), 0, 0, 1)); + assert_eq!(Uniques::owner(0, 0).unwrap(), 1); + + assert_ok!(GatedMarketplace::enlist_sell_offer(Origin::signed(1), m_id, 0, 0, 10000)); + let offer_id = GatedMarketplace::offers_by_item(0, 0).iter().next().unwrap().clone(); + assert!(GatedMarketplace::offers_info(offer_id).is_some()); + assert_eq!(GatedMarketplace::offers_by_account(1).iter().next().unwrap().clone(), offer_id); + assert_eq!(GatedMarketplace::offers_by_marketplace(m_id).iter().next().unwrap().clone(), offer_id); + + }); +} + + +#[test] +fn enlist_sell_offer_two_marketplaces(){ + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + + assert_ok!(GatedMarketplace::create_marketplace(Origin::signed(1), 2, create_label("my marketplace"))); + let m_id = create_label("my marketplace").using_encoded(blake2_256); + + assert_ok!(GatedMarketplace::create_marketplace(Origin::signed(1), 2, create_label("my marketplace2"))); + let m_id2 = create_label("my marketplace2").using_encoded(blake2_256); + + assert_ok!(Uniques::create(Origin::signed(1), 0, 1)); + assert_ok!(Uniques::mint(Origin::signed(1), 0, 0, 1)); + assert_eq!(Uniques::owner(0, 0).unwrap(), 1); + + assert_ok!(GatedMarketplace::enlist_sell_offer(Origin::signed(1), m_id, 0, 0, 10000)); + assert_ok!(GatedMarketplace::enlist_sell_offer(Origin::signed(1), m_id2, 0, 0, 11000)); + + assert_eq!(GatedMarketplace::offers_by_item(0, 0).len(), 2); + assert_eq!(GatedMarketplace::offers_by_account(1).len(), 2); + assert_eq!(GatedMarketplace::offers_by_marketplace(m_id).len(), 1); + assert_eq!(GatedMarketplace::offers_by_marketplace(m_id2).len(), 1); + + }); +} + +#[test] +fn enlist_buy_offer_works() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&2, 1100); + + assert_ok!(GatedMarketplace::create_marketplace(Origin::signed(1), 2, create_label("my marketplace"))); + let m_id = create_label("my marketplace").using_encoded(blake2_256); + + assert_ok!(Uniques::create(Origin::signed(1), 0, 1)); + assert_ok!(Uniques::mint(Origin::signed(1), 0, 0, 1)); + assert_eq!(Uniques::owner(0, 0).unwrap(), 1); + + assert_ok!(GatedMarketplace::enlist_sell_offer(Origin::signed(1), m_id, 0, 0, 1001)); + let offer_id = GatedMarketplace::offers_by_account(1).iter().next().unwrap().clone(); + assert!(GatedMarketplace::offers_info(offer_id).is_some()); + + assert_ok!(GatedMarketplace::enlist_buy_offer(Origin::signed(2), m_id, 0, 0, 1100)); + let offer_id2 = GatedMarketplace::offers_by_account(2).iter().next().unwrap().clone(); + assert!(GatedMarketplace::offers_info(offer_id2).is_some()); + assert_eq!(GatedMarketplace::offers_info(offer_id2).unwrap().offer_type, OfferType::BuyOrder); + }); +} + +#[test] +fn enlist_buy_offer_item_is_not_for_sale_shouldnt_work() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&2, 1100); + + assert_ok!(GatedMarketplace::create_marketplace(Origin::signed(1), 2, create_label("my marketplace"))); + let m_id = create_label("my marketplace").using_encoded(blake2_256); + + assert_ok!(Uniques::create(Origin::signed(1), 0, 1)); + assert_ok!(Uniques::mint(Origin::signed(1), 0, 0, 1)); + assert_eq!(Uniques::owner(0, 0).unwrap(), 1); + + assert_ok!(GatedMarketplace::enlist_sell_offer(Origin::signed(1), m_id, 0, 0, 1001)); + let offer_id = GatedMarketplace::offers_by_account(1).iter().next().unwrap().clone(); + assert!(GatedMarketplace::offers_info(offer_id).is_some()); + + assert_noop!(GatedMarketplace::enlist_buy_offer(Origin::signed(2), m_id, 0, 1, 1100), Error::::ItemNotForSale); + }); +} + + +#[test] +fn enlist_buy_offer_owner_cannnot_create_buy_offers_for_their_own_items_shouldnt_work() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&2, 1100); + + assert_ok!(GatedMarketplace::create_marketplace(Origin::signed(1), 2, create_label("my marketplace"))); + let m_id = create_label("my marketplace").using_encoded(blake2_256); + + assert_ok!(Uniques::create(Origin::signed(1), 0, 1)); + assert_ok!(Uniques::mint(Origin::signed(1), 0, 0, 1)); + assert_eq!(Uniques::owner(0, 0).unwrap(), 1); + + assert_ok!(GatedMarketplace::enlist_sell_offer(Origin::signed(1), m_id, 0, 0, 1001)); + let offer_id = GatedMarketplace::offers_by_account(1).iter().next().unwrap().clone(); + assert!(GatedMarketplace::offers_info(offer_id).is_some()); + + assert_noop!(GatedMarketplace::enlist_buy_offer(Origin::signed(1), m_id, 0, 0, 1100), Error::::CannotCreateOffer); + }); +} + +#[test] +fn enlist_buy_offer_user_does_not_have_enough_balance_shouldnt_work() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&2, 100); + + + assert_ok!(GatedMarketplace::create_marketplace(Origin::signed(1), 2, create_label("my marketplace"))); + let m_id = create_label("my marketplace").using_encoded(blake2_256); + + assert_ok!(Uniques::create(Origin::signed(1), 0, 1)); + assert_ok!(Uniques::mint(Origin::signed(1), 0, 0, 1)); + assert_eq!(Uniques::owner(0, 0).unwrap(), 1); + + assert_ok!(GatedMarketplace::enlist_sell_offer(Origin::signed(1), m_id, 0, 0, 10000)); + let offer_id = GatedMarketplace::offers_by_account(1).iter().next().unwrap().clone(); + assert!(GatedMarketplace::offers_info(offer_id).is_some()); + + assert_noop!(GatedMarketplace::enlist_buy_offer(Origin::signed(2), m_id, 0, 0, 10000), Error::::NotEnoughBalance); + + }); +} + +#[test] +fn enlist_buy_offer_price_must_greater_than_zero_shouldnt_work() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&2, 1100); + + assert_ok!(GatedMarketplace::create_marketplace(Origin::signed(1), 2, create_label("my marketplace"))); + let m_id = create_label("my marketplace").using_encoded(blake2_256); + + assert_ok!(Uniques::create(Origin::signed(1), 0, 1)); + assert_ok!(Uniques::mint(Origin::signed(1), 0, 0, 1)); + assert_eq!(Uniques::owner(0, 0).unwrap(), 1); + + assert_ok!(GatedMarketplace::enlist_sell_offer(Origin::signed(1), m_id, 0, 0, 10000)); + let offer_id = GatedMarketplace::offers_by_account(1).iter().next().unwrap().clone(); + assert!(GatedMarketplace::offers_info(offer_id).is_some()); + + assert_noop!(GatedMarketplace::enlist_buy_offer(Origin::signed(2), m_id, 0, 0, 0), Error::::PriceMustBeGreaterThanZero); + }); +} + + + +#[test] +fn enlist_buy_offer_an_item_can_receive_multiple_buy_offers(){ + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&2, 1101); + Balances::make_free_balance_be(&3, 1201); + + assert_ok!(GatedMarketplace::create_marketplace(Origin::signed(1), 2, create_label("my marketplace"))); + let m_id = create_label("my marketplace").using_encoded(blake2_256); + + assert_ok!(Uniques::create(Origin::signed(1), 0, 1)); + assert_ok!(Uniques::mint(Origin::signed(1), 0, 0, 1)); + assert_eq!(Uniques::owner(0, 0).unwrap(), 1); + + assert_ok!(GatedMarketplace::enlist_sell_offer(Origin::signed(1), m_id, 0, 0, 10000)); + let offer_id = GatedMarketplace::offers_by_account(1).iter().next().unwrap().clone(); + assert!(GatedMarketplace::offers_info(offer_id).is_some()); + + assert_ok!(GatedMarketplace::enlist_buy_offer(Origin::signed(2), m_id, 0, 0, 1100)); + let offer_id2 = GatedMarketplace::offers_by_account(2).iter().next().unwrap().clone(); + assert!(GatedMarketplace::offers_info(offer_id2).is_some()); + + // User 3 will buy the asset so it'll have to enter the marketplace first + assert_ok!(GatedMarketplace::apply(Origin::signed(3),m_id,create_application_fields(2), None )); + assert_ok!(GatedMarketplace::enroll(Origin::signed(1), m_id , AccountOrApplication::Account(3), true, default_feedback())); + assert_ok!(GatedMarketplace::enlist_buy_offer(Origin::signed(3), m_id, 0, 0, 1200)); + let offer_id3 = GatedMarketplace::offers_by_account(3).iter().next().unwrap().clone(); + assert!(GatedMarketplace::offers_info(offer_id3).is_some()); + + assert_eq!(GatedMarketplace::offers_by_item(0, 0).len(), 3); + + }); + +} + +#[test] +fn take_sell_offer_works(){ + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&2, 1300); + + assert_ok!(GatedMarketplace::create_marketplace(Origin::signed(1), 2, create_label("my marketplace"))); + let m_id = create_label("my marketplace").using_encoded(blake2_256); + + assert_ok!(Uniques::create(Origin::signed(1), 0, 1)); + assert_ok!(Uniques::mint(Origin::signed(1), 0, 0, 1)); + assert_eq!(Uniques::owner(0, 0).unwrap(), 1); + + assert_ok!(GatedMarketplace::enlist_sell_offer(Origin::signed(1), m_id, 0, 0, 1200)); + let offer_id = GatedMarketplace::offers_by_item(0, 0).iter().next().unwrap().clone(); + + assert_ok!(GatedMarketplace::take_sell_offer(Origin::signed(2), offer_id)); + assert_eq!(GatedMarketplace::offers_by_item(0, 0).len(), 0); + assert_eq!(GatedMarketplace::offers_info(offer_id).unwrap().status, OfferStatus::Closed); + + }); +} + +#[test] +fn take_sell_offer_owner_cannnot_be_the_buyer_shouldnt_work() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&2, 1300); + + assert_ok!(GatedMarketplace::create_marketplace(Origin::signed(1), 2, create_label("my marketplace"))); + let m_id = create_label("my marketplace").using_encoded(blake2_256); + + assert_ok!(Uniques::create(Origin::signed(1), 0, 1)); + assert_ok!(Uniques::mint(Origin::signed(1), 0, 0, 1)); + assert_eq!(Uniques::owner(0, 0).unwrap(), 1); + + assert_ok!(GatedMarketplace::enlist_sell_offer(Origin::signed(1), m_id, 0, 0, 1200)); + let offer_id = GatedMarketplace::offers_by_item(0, 0).iter().next().unwrap().clone(); + + assert_noop!(GatedMarketplace::take_sell_offer(Origin::signed(1), offer_id), Error::::CannotTakeOffer); + }); +} + +#[test] +fn take_sell_offer_id_does_not_exist_shouldnt_work(){ + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&2, 1300); + + assert_ok!(GatedMarketplace::create_marketplace(Origin::signed(1), 2, create_label("my marketplace"))); + let m_id = create_label("my marketplace").using_encoded(blake2_256); + + assert_ok!(Uniques::create(Origin::signed(1), 0, 1)); + assert_ok!(Uniques::mint(Origin::signed(1), 0, 0, 1)); + assert_eq!(Uniques::owner(0, 0).unwrap(), 1); + + assert_ok!(GatedMarketplace::enlist_sell_offer(Origin::signed(1), m_id, 0, 0, 1200)); + let offer_id = GatedMarketplace::offers_by_item(0, 0).iter().next().unwrap().clone(); + let offer_id2 = offer_id.using_encoded(blake2_256); + + assert_noop!(GatedMarketplace::take_sell_offer(Origin::signed(2), offer_id2), Error::::OfferNotFound); + }); +} + + +#[test] +fn take_sell_offer_buyer_does_not_have_enough_balance_shouldnt_work(){ + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&2, 1100); + + assert_ok!(GatedMarketplace::create_marketplace(Origin::signed(1), 2, create_label("my marketplace"))); + let m_id = create_label("my marketplace").using_encoded(blake2_256); + + assert_ok!(Uniques::create(Origin::signed(1), 0, 1)); + assert_ok!(Uniques::mint(Origin::signed(1), 0, 0, 1)); + assert_eq!(Uniques::owner(0, 0).unwrap(), 1); + + assert_ok!(GatedMarketplace::enlist_sell_offer(Origin::signed(1), m_id, 0, 0, 1200)); + let offer_id = GatedMarketplace::offers_by_item(0, 0).iter().next().unwrap().clone(); + + assert_noop!(GatedMarketplace::take_sell_offer(Origin::signed(2), offer_id), Error::::NotEnoughBalance); + }); +} + +#[test] +fn take_buy_offer_works(){ + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&2, 1300); + + assert_ok!(GatedMarketplace::create_marketplace(Origin::signed(1), 2, create_label("my marketplace"))); + let m_id = create_label("my marketplace").using_encoded(blake2_256); + + assert_ok!(Uniques::create(Origin::signed(1), 0, 1)); + assert_ok!(Uniques::mint(Origin::signed(1), 0, 0, 1)); + assert_eq!(Uniques::owner(0, 0).unwrap(), 1); + + assert_ok!(GatedMarketplace::enlist_sell_offer(Origin::signed(1), m_id, 0, 0, 1001)); + let offer_id = GatedMarketplace::offers_by_account(1).iter().next().unwrap().clone(); + assert!(GatedMarketplace::offers_info(offer_id).is_some()); + + assert_ok!(GatedMarketplace::enlist_buy_offer(Origin::signed(2), m_id, 0, 0, 1200)); + let offer_id2 = GatedMarketplace::offers_by_account(2).iter().next().unwrap().clone(); + assert_eq!(GatedMarketplace::offers_info(offer_id2).unwrap().offer_type, OfferType::BuyOrder); + + assert_ok!(GatedMarketplace::take_buy_offer(Origin::signed(1), offer_id2)); + assert_eq!(GatedMarketplace::offers_by_item(0, 0).len(), 0); + assert_eq!(GatedMarketplace::offers_info(offer_id).unwrap().status, OfferStatus::Closed); + }); +} + +#[test] +fn take_buy_offer_only_owner_can_accept_buy_offers_shouldnt_work(){ + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&2, 1300); + + assert_ok!(GatedMarketplace::create_marketplace(Origin::signed(1), 2, create_label("my marketplace"))); + let m_id = create_label("my marketplace").using_encoded(blake2_256); + + assert_ok!(Uniques::create(Origin::signed(1), 0, 1)); + assert_ok!(Uniques::mint(Origin::signed(1), 0, 0, 1)); + assert_eq!(Uniques::owner(0, 0).unwrap(), 1); + + + assert_ok!(GatedMarketplace::enlist_sell_offer(Origin::signed(1), m_id, 0, 0, 1001)); + let offer_id = GatedMarketplace::offers_by_account(1).iter().next().unwrap().clone(); + assert!(GatedMarketplace::offers_info(offer_id).is_some()); + + assert_ok!(GatedMarketplace::enlist_buy_offer(Origin::signed(2), m_id, 0, 0, 1200)); + let offer_id2 = GatedMarketplace::offers_by_account(2).iter().next().unwrap().clone(); + assert_eq!(GatedMarketplace::offers_info(offer_id2).unwrap().offer_type, OfferType::BuyOrder); + + assert_noop!(GatedMarketplace::take_buy_offer(Origin::signed(2), offer_id2), Error::::NotOwner); + }); +} + +#[test] +fn take_buy_offer_id_does_not_exist_shouldnt_work(){ + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&2, 1300); + + assert_ok!(GatedMarketplace::create_marketplace(Origin::signed(1), 2, create_label("my marketplace"))); + let m_id = create_label("my marketplace").using_encoded(blake2_256); + + assert_ok!(Uniques::create(Origin::signed(1), 0, 1)); + assert_ok!(Uniques::mint(Origin::signed(1), 0, 0, 1)); + assert_eq!(Uniques::owner(0, 0).unwrap(), 1); + + assert_ok!(GatedMarketplace::enlist_sell_offer(Origin::signed(1), m_id, 0, 0, 1001)); + let offer_id = GatedMarketplace::offers_by_account(1).iter().next().unwrap().clone(); + assert!(GatedMarketplace::offers_info(offer_id).is_some()); + + assert_ok!(GatedMarketplace::enlist_buy_offer(Origin::signed(2), m_id, 0, 0, 1200)); + let offer_id2 = GatedMarketplace::offers_by_account(2).iter().next().unwrap().clone(); + assert_eq!(GatedMarketplace::offers_info(offer_id2).unwrap().offer_type, OfferType::BuyOrder); + + let offer_id3 = offer_id2.using_encoded(blake2_256); + assert_noop!(GatedMarketplace::take_buy_offer(Origin::signed(1), offer_id3), Error::::OfferNotFound); + + }); +} + +#[test] +fn take_buy_offer_user_does_not_have_enough_balance_shouldnt_work(){ + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&2, 1300); + + assert_ok!(GatedMarketplace::create_marketplace(Origin::signed(1), 2, create_label("my marketplace"))); + let m_id = create_label("my marketplace").using_encoded(blake2_256); + + assert_ok!(Uniques::create(Origin::signed(1), 0, 1)); + assert_ok!(Uniques::mint(Origin::signed(1), 0, 0, 1)); + assert_eq!(Uniques::owner(0, 0).unwrap(), 1); + + assert_ok!(GatedMarketplace::enlist_sell_offer(Origin::signed(1), m_id, 0, 0, 1001)); + let offer_id = GatedMarketplace::offers_by_account(1).iter().next().unwrap().clone(); + assert!(GatedMarketplace::offers_info(offer_id).is_some()); + + assert_ok!(GatedMarketplace::enlist_buy_offer(Origin::signed(2), m_id, 0, 0, 1200)); + let offer_id2 = GatedMarketplace::offers_by_account(2).iter().next().unwrap().clone(); + assert_eq!(GatedMarketplace::offers_info(offer_id2).unwrap().offer_type, OfferType::BuyOrder); + + Balances::make_free_balance_be(&2, 0); + assert_noop!(GatedMarketplace::take_buy_offer(Origin::signed(1), offer_id2), Error::::NotEnoughBalance); + }); +} + +#[test] +fn remove_sell_offer_works(){ + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&2, 1300); + + assert_ok!(GatedMarketplace::create_marketplace(Origin::signed(1), 2, create_label("my marketplace"))); + let m_id = create_label("my marketplace").using_encoded(blake2_256); + + assert_ok!(Uniques::create(Origin::signed(1), 0, 1)); + assert_ok!(Uniques::mint(Origin::signed(1), 0, 0, 1)); + assert_eq!(Uniques::owner(0, 0).unwrap(), 1); + + assert_ok!(GatedMarketplace::enlist_sell_offer(Origin::signed(1), m_id, 0, 0, 1001)); + let offer_id = GatedMarketplace::offers_by_account(1).iter().next().unwrap().clone(); + assert!(GatedMarketplace::offers_info(offer_id).is_some()); + + assert_ok!(GatedMarketplace::remove_offer(Origin::signed(1), offer_id)); + assert_eq!(GatedMarketplace::offers_by_account(1).len(), 0); + assert!(GatedMarketplace::offers_info(offer_id).is_none()); + }); +} + +#[test] +fn remove_buy_offer_works(){ + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&2, 1300); + + assert_ok!(GatedMarketplace::create_marketplace(Origin::signed(1), 2, create_label("my marketplace"))); + let m_id = create_label("my marketplace").using_encoded(blake2_256); + + assert_ok!(Uniques::create(Origin::signed(1), 0, 1)); + assert_ok!(Uniques::mint(Origin::signed(1), 0, 0, 1)); + assert_eq!(Uniques::owner(0, 0).unwrap(), 1); + + assert_ok!(GatedMarketplace::enlist_sell_offer(Origin::signed(1), m_id, 0, 0, 1001)); + let offer_id = GatedMarketplace::offers_by_account(1).iter().next().unwrap().clone(); + assert!(GatedMarketplace::offers_info(offer_id).is_some()); + + assert_ok!(GatedMarketplace::enlist_buy_offer(Origin::signed(2), m_id, 0, 0, 1001)); + let offer_id2 = GatedMarketplace::offers_by_account(2).iter().next().unwrap().clone(); + assert!(GatedMarketplace::offers_info(offer_id2).is_some()); + + assert_ok!(GatedMarketplace::remove_offer(Origin::signed(2), offer_id2)); + assert_eq!(GatedMarketplace::offers_by_account(2).len(), 0); + assert!(GatedMarketplace::offers_info(offer_id2).is_none()); + }); +} + +#[test] +fn remove_offer_id_does_not_exist_sholdnt_work(){ + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&2, 1300); + + assert_ok!(GatedMarketplace::create_marketplace(Origin::signed(1), 2, create_label("my marketplace"))); + let m_id = create_label("my marketplace").using_encoded(blake2_256); + + assert_ok!(Uniques::create(Origin::signed(1), 0, 1)); + assert_ok!(Uniques::mint(Origin::signed(1), 0, 0, 1)); + assert_eq!(Uniques::owner(0, 0).unwrap(), 1); + + assert_ok!(GatedMarketplace::enlist_sell_offer(Origin::signed(1), m_id, 0, 0, 1001)); + let offer_id = GatedMarketplace::offers_by_account(1).iter().next().unwrap().clone(); + assert!(GatedMarketplace::offers_info(offer_id).is_some()); + + let offer_id2 = offer_id.using_encoded(blake2_256); + assert_noop!(GatedMarketplace::remove_offer(Origin::signed(2), offer_id2), Error::::OfferNotFound); + }); +} + +#[test] +fn remove_offer_creator_doesnt_match_sholdnt_work(){ + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&2, 1300); + + assert_ok!(GatedMarketplace::create_marketplace(Origin::signed(1), 2, create_label("my marketplace"))); + let m_id = create_label("my marketplace").using_encoded(blake2_256); + + assert_ok!(Uniques::create(Origin::signed(1), 0, 1)); + assert_ok!(Uniques::mint(Origin::signed(1), 0, 0, 1)); + assert_eq!(Uniques::owner(0, 0).unwrap(), 1); + + assert_ok!(GatedMarketplace::enlist_sell_offer(Origin::signed(1), m_id, 0, 0, 1001)); + let offer_id = GatedMarketplace::offers_by_account(1).iter().next().unwrap().clone(); + assert!(GatedMarketplace::offers_info(offer_id).is_some()); + + assert_noop!(GatedMarketplace::remove_offer(Origin::signed(2), offer_id), Error::::CannotRemoveOffer); + }); +} + +#[test] +fn remove_offer_status_is_closed_shouldnt_work(){ + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&2, 1300); + + assert_ok!(GatedMarketplace::create_marketplace(Origin::signed(1), 2, create_label("my marketplace"))); + let m_id = create_label("my marketplace").using_encoded(blake2_256); + + assert_ok!(Uniques::create(Origin::signed(1), 0, 1)); + assert_ok!(Uniques::mint(Origin::signed(1), 0, 0, 1)); + assert_eq!(Uniques::owner(0, 0).unwrap(), 1); + + assert_ok!(GatedMarketplace::enlist_sell_offer(Origin::signed(1), m_id, 0, 0, 1001)); + let offer_id = GatedMarketplace::offers_by_account(1).iter().next().unwrap().clone(); + assert!(GatedMarketplace::offers_info(offer_id).is_some()); + + assert_ok!(GatedMarketplace::enlist_buy_offer(Origin::signed(2), m_id, 0, 0, 1200)); + let offer_id2 = GatedMarketplace::offers_by_account(2).iter().next().unwrap().clone(); + assert_eq!(GatedMarketplace::offers_info(offer_id2).unwrap().offer_type, OfferType::BuyOrder); + + assert_ok!(GatedMarketplace::take_buy_offer(Origin::signed(1), offer_id2)); + assert_eq!(GatedMarketplace::offers_by_item(0, 0).len(), 0); + assert_eq!(GatedMarketplace::offers_info(offer_id).unwrap().status, OfferStatus::Closed); + + assert_noop!(GatedMarketplace::remove_offer(Origin::signed(2), offer_id2), Error::::CannotDeleteOffer); + }); + +} \ No newline at end of file diff --git a/pallets/gated-marketplace/src/types.rs b/pallets/gated-marketplace/src/types.rs index 4ff5f046..875d4c44 100644 --- a/pallets/gated-marketplace/src/types.rs +++ b/pallets/gated-marketplace/src/types.rs @@ -2,7 +2,15 @@ use super::*; use frame_support::pallet_prelude::*; //use frame_system::pallet_prelude::*; +use sp_runtime::sp_std::vec::Vec; +use frame_support::sp_io::hashing::blake2_256; +pub type Fields = BoundedVec<(FieldName,Cid), ::MaxFiles>; +type AccountIdOf = ::AccountId; +pub type FieldName = BoundedVec>; +pub type Cid = BoundedVec>; +pub type Cids = BoundedVec; +pub type CustodianFields = ( AccountIdOf, Cids<::MaxFiles>); #[derive(CloneNoBound,Encode, Decode, RuntimeDebugNoBound, Default, TypeInfo, MaxEncodedLen,)] #[scale_info(skip_type_params(T))] @@ -20,20 +28,103 @@ pub enum AccountOrApplication{ } #[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy)] -pub enum MarketplaceAuthority{ +pub enum MarketplaceRole{ Owner, Admin, Appraiser, RedemptionSpecialist, + Participant, } -impl Default for MarketplaceAuthority{ +impl Default for MarketplaceRole{ fn default() -> Self { - MarketplaceAuthority::Appraiser + MarketplaceRole::Participant } } -#[derive(CloneNoBound,Encode, Decode, Eq, PartialEq, RuntimeDebugNoBound, Default, TypeInfo, MaxEncodedLen,)] +impl MarketplaceRole{ + pub fn to_vec(self) -> Vec{ + match self{ + Self::Owner => "Owner".as_bytes().to_vec(), + Self::Admin => "Admin".as_bytes().to_vec(), + Self::Appraiser => "Appraiser".as_bytes().to_vec(), + Self::RedemptionSpecialist => "Redemption_specialist".as_bytes().to_vec(), + Self::Participant => "Participant".as_bytes().to_vec(), + } + } + + pub fn id(&self) -> [u8;32]{ + self.to_vec().using_encoded(blake2_256) + } + + pub fn enum_to_vec() -> Vec>{ + use crate::types::MarketplaceRole::*; + [Owner.to_vec(), Admin.to_vec(), Appraiser.to_vec(), RedemptionSpecialist.to_vec(), Participant.to_vec()].to_vec() + } +} + +/// Extrinsics which require previous authorization to call them +#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy)] +pub enum Permission{ + Enroll, + AddAuth, + RemoveAuth, + UpdateLabel, + RemoveMarketplace, + EnlistSellOffer, + TakeSellOffer, + DuplicateOffer, + RemoveOffer, + EnlistBuyOffer, + TakeBuyOffer, +} + +impl Permission{ + pub fn to_vec(self) -> Vec{ + match self{ + Self::Enroll => "Enroll".as_bytes().to_vec(), + Self::AddAuth => "AddAuth".as_bytes().to_vec(), + Self::RemoveAuth => "RemoveAuth".as_bytes().to_vec(), + Self::UpdateLabel => "UpdateLabel".as_bytes().to_vec(), + Self::RemoveMarketplace => "RemoveMarketplace".as_bytes().to_vec(), + Self::EnlistSellOffer=>"EnlistSellOffer".as_bytes().to_vec(), + Self::TakeSellOffer=>"TakeSellOffer".as_bytes().to_vec(), + Self::DuplicateOffer=>"DuplicateOffer".as_bytes().to_vec(), + Self::RemoveOffer=>"RemoveOffer".as_bytes().to_vec(), + Self::EnlistBuyOffer=>"EnlistBuyOffer".as_bytes().to_vec(), + Self::TakeBuyOffer=>"TakeBuyOffer".as_bytes().to_vec(), + } + } + + pub fn id(&self) -> [u8;32]{ + self.to_vec().using_encoded(blake2_256) + } + + pub fn admin_permissions()-> Vec>{ + use crate::types::Permission::*; + let mut admin_permissions = [Enroll.to_vec(), + AddAuth.to_vec(), + RemoveAuth.to_vec(), + UpdateLabel.to_vec(), + RemoveMarketplace.to_vec()].to_vec(); + admin_permissions.append(&mut Permission::participant_permissions()); + admin_permissions + } + + pub fn participant_permissions()->Vec>{ + use crate::types::Permission::*; + [ + EnlistSellOffer.to_vec(), + TakeSellOffer.to_vec(), + DuplicateOffer.to_vec(), + RemoveOffer.to_vec(), + EnlistBuyOffer.to_vec(), + TakeBuyOffer.to_vec(), + ].to_vec() + } +} + +#[derive(CloneNoBound, Encode, Decode, Eq, PartialEq, RuntimeDebugNoBound, Default, TypeInfo, MaxEncodedLen,)] #[scale_info(skip_type_params(T))] #[codec(mel_bound())] pub struct Application< T: Config >{ @@ -59,8 +150,8 @@ impl Default for ApplicationStatus{ #[derive(CloneNoBound, Encode ,Decode, Eq, RuntimeDebugNoBound, Default, TypeInfo, MaxEncodedLen)] pub struct ApplicationField{ pub display_name: BoundedVec >, - pub cid: BoundedVec >, - pub custodian_cid: Option > >, + pub cid: Cid, + pub custodian_cid: Option, } // Eq macro didnt work (binary operation `==` cannot be applied to type...) impl PartialEq for ApplicationField{ @@ -68,3 +159,39 @@ impl PartialEq for ApplicationField{ self.cid == other.cid && self.display_name == other.display_name } } + + +//offers +#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy)] +pub enum OfferStatus{ + Open, + Closed, +} + +impl Default for OfferStatus{ + fn default() -> Self { + OfferStatus::Open + } +} + +#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy)] +pub enum OfferType{ + SellOrder, + BuyOrder, +} + +#[derive(CloneNoBound, Encode, Decode, RuntimeDebugNoBound, TypeInfo, MaxEncodedLen,)] +#[scale_info(skip_type_params(T))] +#[codec(mel_bound())] +pub struct OfferData{ + pub marketplace_id: [u8;32], + pub collection_id: T::CollectionId, + pub item_id: T::ItemId, + pub creator: T::AccountId, + pub price: BalanceOf, + pub status: OfferStatus, + pub creation_date: u64, + pub expiration_date: u64, + pub offer_type: OfferType, + pub buyer: Option<(T::AccountId, [u8;32])>, +} diff --git a/pallets/rbac/Cargo.toml b/pallets/rbac/Cargo.toml new file mode 100644 index 00000000..bfa1b790 --- /dev/null +++ b/pallets/rbac/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "pallet-rbac" +version = "4.0.0-dev" +description = "FRAME pallet template for defining custom runtime logic." +authors = ["Substrate DevHub "] +homepage = "https://substrate.io/" +edition = "2021" +license = "Unlicense" +publish = false +repository = "https://github.com/substrate-developer-hub/substrate-node-template/" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +log = "0.4" +codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = [ + "derive", +] } +scale-info = { version = "2.0.1", default-features = false, features = ["derive"] } +frame-support = { default-features = false, version = "4.0.0-dev", git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.23"} +frame-system = { default-features = false, version = "4.0.0-dev", git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.23" } +frame-benchmarking = { default-features = false, version = "4.0.0-dev", git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.23", optional = true } +sp-runtime = { default-features = false, version = "6.0.0", git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.23" } + +[dev-dependencies] +sp-core = { default-features = false, version = "6.0.0", git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.23" } +sp-io = { default-features = false, version = "6.0.0", git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.23" } + +[features] +default = ["std"] +std = [ + "codec/std", + "scale-info/std", + "frame-support/std", + "frame-system/std", + "frame-benchmarking/std", +] + +runtime-benchmarks = ["frame-benchmarking/runtime-benchmarks"] +try-runtime = ["frame-support/try-runtime"] diff --git a/pallets/rbac/README.md b/pallets/rbac/README.md new file mode 100644 index 00000000..a986be4c --- /dev/null +++ b/pallets/rbac/README.md @@ -0,0 +1,537 @@ +# Role-Based Access Control (RBAC) +Restrict access to custom pallets by coupling this pallet. Create roles and assign permissions. + +- [Role-Based Access Control (RBAC)](#role-based-access-control-rbac) + - [Overview](#overview) + - [Terminology](#terminology) + - [Interface](#interface) + - [Helper functions](#helper-functions) + - [Getters](#getters) + - [Constants](#constants) + - [Usage](#usage) + - [Loosely coupling RBAC with another pallet](#loosely-coupling-rbac-with-another-pallet) + - [Querying with Polkadot-js CLI](#querying-with-polkadot-js-cli) + - [Get pallet scopes](#get-pallet-scopes) + - [Get role by its id](#get-role-by-its-id) + - [Get all role ids linked to a pallet](#get-all-role-ids-linked-to-a-pallet) + - [Get a permission by pallet and id](#get-a-permission-by-pallet-and-id) + - [Get permissions linked to a role within a pallet](#get-permissions-linked-to-a-role-within-a-pallet) + - [Get which roles the user has in a pallet scope](#get-which-roles-the-user-has-in-a-pallet-scope) + - [Get which users have the role in a pallet scope](#get-which-users-have-the-role-in-a-pallet-scope) + - [Querying with Polkadot-js API (js library)](#querying-with-polkadot-js-api-js-library) + - [Get pallet scopes](#get-pallet-scopes-1) + - [Get all pallet scopes](#get-all-pallet-scopes) + - [Get role by its id](#get-role-by-its-id-1) + - [Get all stored roles](#get-all-stored-roles) + - [Get all role ids linked to a pallet](#get-all-role-ids-linked-to-a-pallet-1) + - [Get all role ids linked to all pallets](#get-all-role-ids-linked-to-all-pallets) + - [Get a permission by pallet and id](#get-a-permission-by-pallet-and-id-1) + - [Get all permissions from a pallet](#get-all-permissions-from-a-pallet) + - [Get permissions linked to a role within a pallet](#get-permissions-linked-to-a-role-within-a-pallet-1) + - [Get all role permissions from a pallet](#get-all-role-permissions-from-a-pallet) + - [Get which roles the user has in a pallet scope](#get-which-roles-the-user-has-in-a-pallet-scope-1) + - [Get which roles the user has in a pallet scope](#get-which-roles-the-user-has-in-a-pallet-scope-2) + - [Get which roles the user has in all scopes from a pallet](#get-which-roles-the-user-has-in-all-scopes-from-a-pallet) + - [Get which users have the role in a pallet scope](#get-which-users-have-the-role-in-a-pallet-scope-1) + - [Get scope users by role](#get-scope-users-by-role) + - [Errors](#errors) + +## Overview +This module allows to +- Define roles grouping them by the runtime pallet index. +- Assign permissions to roles. +- Create scopes, each of them will have an independent list of users. +- Assign roles to users within defined scopes. +- Ask if a user has certain permission, the pallet will search which roles the user has and will determine if its authorized. +- Remove roles from users, scopes and the entire storage assigned to an external pallet. + + +## Terminology +- **Scope**: A group of users with one or more roles, scopes are delimited and categorized by the pallet that created it. +- **Role**: A group of permissions, the RBAC pallet has a global list of roles to avoid data redundancy, however, only the selected roles will be assigned (or created if they don't exist) to the pallet. +- **Permission**: The bottom level filter, permissions are stored and categorized by pallet, and it is highly recommended each restricted extrinsic have its own permission. +- **Pallet index**: a unique number that serves as an identifier, as it is assigned automatically to a pallet when its instantiated in the runtime. The term is interchangeable with pallet id. + +## Interface + +### Helper functions +This module is intended to be used in conjunction with a pallet which loosely couples it, due to that, the pallet doesn't expose any extrinsic. However, the implementation of the `RoleBasedAccessControl` trait has numerous helper functions that allow a flexible roles management. + +- `create_scope` inserts a scope within a external pallet context using its index. +- `remove_scope` deletes all role lists linked to that scope. +- `remove_pallet_storage` deletes all role lists and permissions associated with the pallet. +- `create_and_set_roles` is the recommended first step for setting up the role access for the pallet, as it takes the pallet index and a list of roles to be created (and assigned) in encoded string format. +- `create_role` inserts a role in the global role list and return a generated `role_id`, if its already in the list, it won't perform the id generation and will return the previously stored one instead. It is important to mention that this function won't assign the role to any pallet. +- `set_role_to_pallet` assigns a previously created role to a pallet. +- `set_multiple_pallet_roles` assigns multiple, previously created roles to a pallet. +- `assign_role_to_user` assigns a role to a user in a scope context. The role needs to be previously created and assigned to that pallet. After this function is executed, the specified user will have additional capabilities according to the role. +- `remove_role_from_user` removes a specified role from a user in a scope context. After this function is executed, the user will no longer be able to enforce the removed role and its permissions. +- `create_and_set_permissions` a good second step for enabling role access to the coupled pallet, as it creates and assigns a list of permissions to a role in a pallet context. +- `create_permission` inserts a permission in a pallet context, after this function is executed, the permission is not yet assigned to any role. +- `set_permission_to_role` assigns a previously created permission to a role in a pallet context. +- `set_multiple_permissions_to_role` assigns multiple, previously created permissions to a role in a pallet context. +- `is_authorized` is the suggested authorization mechanism, as it takes the pallet index, scope and the requested permission to be enforced. This function will search the users permissions and will validate if there's a role that has the permission enabled. + - `has_role` a secondary authorization mechanism that takes the pallet index, scope, and a set of roles that the user tentatively has. This method is specially useful when its unclear which roles the user has and any of the specified roles will suffice the authorization. + - `scope_exists` a validation function used internally by other methods, ensure the requested scope is registered in the specified pallet. + - `permission_exists` is a validation function used internally, as it confirms if the permission is stored in the specified pallet. + - `is_role_linked_to_pallet` validates if a role is registered in the pallet. This method doesn't validates if the role has been previously created and assumes it is. + - `is_permission_linked_to_role` ensures the specified permission is linked to the role in a pallet context. This method assumes both the role and permission exists. + - `get_role_users_len` returns the number of users that have the specified role, useful when implementing restrictions on the number of users that can have that role. + +### Getters +- `scopes` +- `roles` +- `pallet_roles` +- `permissions` (storage double map) +- `permissions_by_role` (storage double map) +- `roles_by_user` (storage N map with 3 keys) +- `users_by_scope` (storage N map with 3 keys) + + +### Constants +- `MaxScopesPerPallet: Get` +- `MaxRolesPerPallet: Get` +- `RoleMaxLen: Get` +- `PermissionMaxLen: Get` +- `MaxPermissionsPerRole: Get` +- `MaxRolesPerUser: Get` +- `MaxUsersPerRole: Get` + +## Usage + +### Loosely coupling RBAC with another pallet +Once the RBAC pallet is imported and configured in the runtime, the first step is to import the `RoleBasedAccessControl` trait from the rbac types into the custom pallet, and declare a type within the pallet configuration: +```rust +use pallet_rbac::types::RoleBasedAccessControl; + + #[pallet::config] + pub trait Config: frame_system::Config { + type Event: From> + IsType<::Event>; + // ... + type Rbac : RoleBasedAccessControl; + } +``` + +Then the RBAC pallet can safely be imported as a parameter within another pallet, for example, `gated_marketplaces`: + +```rust +impl pallet_gated_marketplace::Config for Runtime { + type Event = Event; + // ... + type Rbac = RBAC; +} +``` + +Now all the previously mentioned functions are accessible within the custom pallet: +```rust +let create_scope_result : DispatchResult = T::Rbac::create_scope(pallet_id,marketplace_id); +``` + +### Querying with Polkadot-js CLI +As previously stated, this pallet doesn't expose any extrinsics, but rather expose a collection of helper functions that are accessible by any custom pallet that couples it. Therefore, the following section assumes theres a basic RBAC configuration stored on chain. + +#### Get pallet scopes +```bash +# pallet_id +polkadot-js-api query.rbac.scopes 20 +``` +```bash +# Expected output +{ + "scopes": [ + "0x112a94197eb935a48b13ac5e6d37d316a143dd3dcf725c9d9d27d64dbba62890" + ] +} +``` + +#### Get role by its id +```bash +# role_id +polkadot-js-api query.rbac.roles 0x08aef7203969e2467b33b14965dfab62e11b085610c798b3cac150b1d7ea033b +``` +```bash +# Expected output: +{ + "roles": "Owner" +} +``` + +#### Get all role ids linked to a pallet + +```bash +# pallet_id +polkadot-js-api query.rbac.palletRoles 20 +``` +```bash +# Expected output +{ + "palletRoles": [ + "0x08aef7203969e2467b33b14965dfab62e11b085610c798b3cac150b1d7ea033b", + "0xc1237f9841c265fb722178da01a1e088c25fb892d6b7cd9634a20ac84bb3ee01", + "0xae9e025522f868c39b41b8a5ba513335a2a229690bd44c71c998d5a9ad38162b" + ] +} +``` + +#### Get a permission by pallet and id +```bash +# pallet_id, permission_id +polkadot-js-api query.rbac.permissions 20 0xdd2f4fc1f525a38ab2f18b2ef4ff4559ddc344d04aa2ceaec1f5d0c6b4f67674 +``` +```bash +# Expected output +{ + "permissions": "Enroll" +} +``` + +#### Get permissions linked to a role within a pallet +```bash +# pallet_id, role_id +polkadot-js-api query.rbac.permissionsByRole 20 0x08aef7203969e2467b33b14965dfab62e11b085610c798b3cac150b1d7ea033b +``` +```bash +# Expected output +{ + "permissionsByRole": [ + "0xdd2f4fc1f525a38ab2f18b2ef4ff4559ddc344d04aa2ceaec1f5d0c6b4f67674", + "0x2c40feed7853568ca1cb5f852636359f8cc8dc82108191397cb7b8ad90a1d0a1", + "0x78dcd6644c3f21fd1872659dcb32c58af797c5c06963fb2ea0937b8d24479815", + "0xbe1f77a2f9266a2dbaa4858ec7aa3933da37346e96a7968c99870d15552d51a5", + "0x599314a6cceabfd08491d4847fe78ad0e932340ff1877704376890aa6ddb045c" + ] +} +``` + +#### Get which roles the user has in a pallet scope +```bash +# account_id, pallet_id, scope_id +polkadot-js-api query.rbac.rolesByUser 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY 20 0x112a94197eb935a48b13ac5e6d37d316a143dd3dcf725c9d9d27d64dbba62890 +``` +```bash +# Expected output +{ + "rolesByUser": [ + "0x08aef7203969e2467b33b14965dfab62e11b085610c798b3cac150b1d7ea033b" + ] +} +``` + +#### Get which users have the role in a pallet scope +```bash +# pallet_id, scope_id, role_id +polkadot-js-api query.rbac.usersByScope 20 0x112a94197eb935a48b13ac5e6d37d316a143dd3dcf725c9d9d27d64dbba62890 0x08aef7203969e2467b33b14965dfab62e11b085610c798b3cac150b1d7ea033b +``` +```bash +# Expected output +{ + "usersByScope": [ + "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" + ] +} +``` + +### Querying with Polkadot-js API (js library) +The javascript version of the API offers more versatile queries. Again, this section assumes the RBAC pallet has been previously pre-populated with data from another custom pallet. + +#### Get pallet scopes +```js +// pallet_id +const scopes = await api.query.rbac.scopes(20); +console.log(scopes.toHuman()); +``` +```bash +# Expected output +[ + '0xace33a53e2c1a5c7fa2f920338136d0ddc3aba23eacaf708e3871bc856a34b95' +] +``` + +#### Get all pallet scopes +```js +const all_scopes = await api.query.rbac.scopes.entries(); +all_scopes.forEach(([key, exposure]) => { + console.log('key pallet_id:', key.args.map((k) => k.toHuman())); + console.log(' scopes:', exposure.toHuman()); +}); +``` +```bash +# Expected output: +key pallet_id: [ '20' ] + scopes: ['0xace33a53e2c1a5c7fa2f920338136d0ddc3aba23eacaf708e3871bc856a34b95'] +``` + +#### Get role by its id +```js +// role_id +const roles = await api.query.rbac.roles("0x08aef7203969e2467b33b14965dfab62e11b085610c798b3cac150b1d7ea033b"); +console.log(roles.toHuman()); +``` +```bash +# Expected output +Owner +``` + +#### Get all stored roles +```js +const all_roles = await api.query.rbac.roles.entries(); +all_roles.forEach(([key, exposure]) => { + console.log('role_id:', key.args.map((k) => k.toHuman())); + console.log(' name:', exposure.toHuman()); +}); +``` +```bash +role_id: ['0x08aef7203969e2467b33b14965dfab62e11b085610c798b3cac150b1d7ea033b'] + name: Owner +role_id: ['0xae9e025522f868c39b41b8a5ba513335a2a229690bd44c71c998d5a9ad38162b'] + name: Participant +role_id: ['0xc1237f9841c265fb722178da01a1e088c25fb892d6b7cd9634a20ac84bb3ee01'] + name: Admin +``` + +#### Get all role ids linked to a pallet +```js +// pallet_id +const pallet_roles = await api.query.rbac.palletRoles(20); +console.log(pallet_roles.toHuman()); +``` +```bash +# Expected output +[ + '0x08aef7203969e2467b33b14965dfab62e11b085610c798b3cac150b1d7ea033b', + '0xc1237f9841c265fb722178da01a1e088c25fb892d6b7cd9634a20ac84bb3ee01', + '0xae9e025522f868c39b41b8a5ba513335a2a229690bd44c71c998d5a9ad38162b' +] +``` + +#### Get all role ids linked to all pallets +```js +const all_pallet_roles = await api.query.rbac.palletRoles.entries(); +all_pallet_roles.forEach(([key, exposure]) => { + console.log('pallet_id:', key.args.map((k) => k.toHuman())); + console.log(' role_ids:', exposure.toHuman()); +}); +``` +```bash +# Expected output +pallet_id: [ '20' ] + role_ids: [ + '0x08aef7203969e2467b33b14965dfab62e11b085610c798b3cac150b1d7ea033b', + '0xc1237f9841c265fb722178da01a1e088c25fb892d6b7cd9634a20ac84bb3ee01', + '0xae9e025522f868c39b41b8a5ba513335a2a229690bd44c71c998d5a9ad38162b' +] +``` + +#### Get a permission by pallet and id +```js +// pallet_id, permission_id +const permission = await api.query.rbac.permissions(20, "0xdd2f4fc1f525a38ab2f18b2ef4ff4559ddc344d04aa2ceaec1f5d0c6b4f67674"); +console.log(permission.toHuman()); +``` +```bash +# Expected output +Enroll +``` + +#### Get all permissions from a pallet +```js +// the pallet_id can be omitted to get all permissions from all pallets +const all_pallet_permissions = await api.query.rbac.permissions.entries(20); +all_pallet_permissions.forEach(([key, exposure]) => { + console.log('pallet_id and permission_id:', key.args.map((k) => k.toHuman())); + console.log(' permission:', exposure.toHuman()); +}); +``` +```bash +# Expected output +pallet_id and permission_id: [ + '20', + '0x2c40feed7853568ca1cb5f852636359f8cc8dc82108191397cb7b8ad90a1d0a1' +] + permission: AddAuth +pallet_id and permission_id: [ + '20', + '0xbe1f77a2f9266a2dbaa4858ec7aa3933da37346e96a7968c99870d15552d51a5' +] + permission: UpdateLabel +pallet_id and permission_id: [ + '20', + '0xdd2f4fc1f525a38ab2f18b2ef4ff4559ddc344d04aa2ceaec1f5d0c6b4f67674' +] + permission: Enroll +``` + +#### Get permissions linked to a role within a pallet +```js +// pallet_id, role_id +const permissionsByRole = await api.query.rbac.permissionsByRole(20, "0x08aef7203969e2467b33b14965dfab62e11b085610c798b3cac150b1d7ea033b"); +console.log(permissionsByRole.toHuman()); +``` +```bash +# Expected output +[ + '0xdd2f4fc1f525a38ab2f18b2ef4ff4559ddc344d04aa2ceaec1f5d0c6b4f67674', + '0x2c40feed7853568ca1cb5f852636359f8cc8dc82108191397cb7b8ad90a1d0a1', + '0x78dcd6644c3f21fd1872659dcb32c58af797c5c06963fb2ea0937b8d24479815', + '0xbe1f77a2f9266a2dbaa4858ec7aa3933da37346e96a7968c99870d15552d51a5', + '0x599314a6cceabfd08491d4847fe78ad0e932340ff1877704376890aa6ddb045c' +] +``` + +#### Get all role permissions from a pallet +```js +// the pallet_id can be omitted to get all permissions from all pallets by role +const all_pallet_permissions_by_role = await api.query.rbac.permissionsByRole.entries(20); +all_pallet_permissions_by_role.forEach(([key, exposure]) => { + console.log('pallet_id:', key.args.map((k) => k.toHuman())); + console.log(' permission_ids', exposure.toHuman()); +}); +``` +```bash +# Output should look like this +pallet_id: [ + '20', + '0xae9e025522f868c39b41b8a5ba513335a2a229690bd44c71c998d5a9ad38162b' +] + permission_ids: [ + '0xf010b3ffd94e992da28d394e7c065514710383a75508decaaead76e99d6ec4fc', + '0x70ff830f1d86d3f63ebf39fb1270fcab37abab1668a8fc7a5e18c9b1f0b793c2' +] +pallet_id: [ + '20', + '0x08aef7203969e2467b33b14965dfab62e11b085610c798b3cac150b1d7ea033b' +] + permission_ids: [ + '0xdd2f4fc1f525a38ab2f18b2ef4ff4559ddc344d04aa2ceaec1f5d0c6b4f67674', + '0x2c40feed7853568ca1cb5f852636359f8cc8dc82108191397cb7b8ad90a1d0a1' +] +``` + +#### Get which roles the user has in a pallet scope +```js +// account_id, pallet_id, scope_id +const rolesByUser = await api.query.rbac.rolesByUser(alice.address,20, "0xace33a53e2c1a5c7fa2f920338136d0ddc3aba23eacaf708e3871bc856a34b95"); +console.log(rolesByUser.toHuman()); +``` +```bash +# Output should look like this +['0x08aef7203969e2467b33b14965dfab62e11b085610c798b3cac150b1d7ea033b'] +``` + +#### Get which roles the user has in a pallet scope +```js +// account_id, pallet_id, scope_id +const rolesByUser = await api.query.rbac.rolesByUser(alice.address,20, "0xace33a53e2c1a5c7fa2f920338136d0ddc3aba23eacaf708e3871bc856a34b95"); +console.log(rolesByUser.toHuman()); +``` +```bash +# Output should look like this +['0x08aef7203969e2467b33b14965dfab62e11b085610c798b3cac150b1d7ea033b'] +``` + +#### Get which roles the user has in all scopes from a pallet +```js +// The pallet_id can be omitted to get all roles the user has from all pallets. +// Both account_id and pallet_id can be omitted all roles from all users, categorized by pallet_id +const all_roles_by_user = await api.query.rbac.rolesByUser.entries(alice.address, 20); +all_roles_by_user.forEach(([key, exposure]) => { + console.log('account_id, pallet_id, scope_id:', key.args.map((k) => k.toHuman())); + console.log(' role_ids', exposure.toHuman()); +}); +``` +```bash +# Output should look like this +account_id, pallet_id, scope_id: [ + '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', + '20', + '0xace33a53e2c1a5c7fa2f920338136d0ddc3aba23eacaf708e3871bc856a34b95' +] + role_ids [ + '0x08aef7203969e2467b33b14965dfab62e11b085610c798b3cac150b1d7ea033b' +] +``` + + +#### Get which users have the role in a pallet scope +```js +// pallet_id, scope_id, role_id +const usersByScope = await api.query.rbac.usersByScope(20, "0xace33a53e2c1a5c7fa2f920338136d0ddc3aba23eacaf708e3871bc856a34b95", "0x08aef7203969e2467b33b14965dfab62e11b085610c798b3cac150b1d7ea033b"); +console.log(usersByScope.toHuman()); +``` +```bash +# Expected output +[ '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY' ] +``` + +#### Get scope users by role +```js +// The scope_id could be omitted to get all users by role of all pallet scopes. +// The pallet_id and scope_id could be omitted to get a global list of users categorized by pallet, scope, and role. +const scope_users_by_role = await api.query.rbac.usersByScope.entries(20, "0xace33a53e2c1a5c7fa2f920338136d0ddc3aba23eacaf708e3871bc856a34b95"); +scope_users_by_role.forEach(([key, exposure]) => { + console.log('pallet_id, scope_id, role_id:', key.args.map((k) => k.toHuman())); + console.log(' account_id', exposure.toHuman()); +}); +``` +```bash +# Output should look like this +pallet_id, scope_id, role_id: [ + '20', + '0xace33a53e2c1a5c7fa2f920338136d0ddc3aba23eacaf708e3871bc856a34b95', + '0x08aef7203969e2467b33b14965dfab62e11b085610c798b3cac150b1d7ea033b' +] + account_id [ '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY' ] +pallet_id, scope_id, role_id: [ + '20', + '0xace33a53e2c1a5c7fa2f920338136d0ddc3aba23eacaf708e3871bc856a34b95', + '0xc1237f9841c265fb722178da01a1e088c25fb892d6b7cd9634a20ac84bb3ee01' +] + account_id [ '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty' ] +``` + +## Errors + +```rust +/// Error names should be descriptive. +NoneValue, +/// The specified scope doesn't exists +ScopeNotFound, +/// The scope is already linked with the pallet +ScopeAlreadyExists, +/// The specified role doesn't exist +RoleNotFound, +/// The permission doesn't exist in the pallet +PermissionNotFound, +/// The specified user hasn't been asigned to this scope +UserNotFound, +/// The role is already linked in the pallet +DuplicateRole, +/// The permission is already linked to that role in that scope +DuplicatePermission, +/// The user has that role asigned in that scope +UserAlreadyHasRole, +/// The role exists but it hasn't been linked to the pallet +RoleNotLinkedToPallet, +/// The permission wasn't found in the roles capabilities +PermissionNotLinkedToRole, +/// The user doesn't have any roles in this pallet +UserHasNoRoles, +/// The role doesn't have any users assigned to it +RoleHasNoUsers, +/// The pallet has too many scopes +ExceedMaxScopesPerPallet, +/// The pallet cannot have more roles +ExceedMaxRolesPerPallet, +/// The specified role cannot have more permission in this scope +ExceedMaxPermissionsPerRole, +/// The user cannot have more roles in this scope +ExceedMaxRolesPerUser, +/// This role cannot have assigned to more users in this scope +ExceedMaxUsersPerRole, +/// The role string is too long +ExceedRoleMaxLen, +/// The permission string is too long +ExceedPermissionMaxLen, +/// The user does not have the specified role +NotAuthorized, +``` \ No newline at end of file diff --git a/pallets/gated-marketplace/src/benchmarking.rs b/pallets/rbac/src/benchmarking.rs similarity index 100% rename from pallets/gated-marketplace/src/benchmarking.rs rename to pallets/rbac/src/benchmarking.rs diff --git a/pallets/rbac/src/functions.rs b/pallets/rbac/src/functions.rs new file mode 100644 index 00000000..95bbcd55 --- /dev/null +++ b/pallets/rbac/src/functions.rs @@ -0,0 +1,434 @@ +use super::*; +use frame_support::{pallet_prelude::*}; +//use frame_system::pallet_prelude::*; +use frame_support::sp_io::hashing::blake2_256; +use frame_support::sp_std::borrow::ToOwned; +use sp_runtime::sp_std::vec::Vec; + +use crate::types::*; + +impl RoleBasedAccessControl for Pallet{ + /*---- Basic Insertion of individual storage maps ---*/ + /// Scope creation + /// + /// Creates a scope within a external pallet using the pallet index. + /// ### Parameters: + /// - `pallet_id`: The unique pallet identifier. + /// - `scope_id`: The newly generated scope identifier. + fn create_scope(pallet: IdOrVec, scope_id: ScopeId)-> DispatchResult{ + let pallet_id = pallet.to_id(); + >::try_mutate(pallet_id, |scopes|{ + ensure!(!scopes.contains(&scope_id), Error::::ScopeAlreadyExists); + scopes.try_push(scope_id).map_err(|_| Error::::ExceedMaxScopesPerPallet)?; + Ok(()) + }) + } + + /// Scope removal + /// + /// Removes a scope within a external pallet using the pallet index. + /// Executing this function will delete all registered role users. + /// ### Parameters: + /// - `pallet_id`: The unique pallet identifier. + /// - `scope_id`: The scope identifier to remove. + fn remove_scope(pallet: IdOrVec, scope_id: ScopeId) -> DispatchResult{ + let pallet_id = pallet.to_id(); + // remove on scopes + >::try_mutate_exists::<_,(),DispatchError,_>(pallet_id, |scopes_option|{ + let scopes = scopes_option.as_mut().ok_or(Error::::ScopeNotFound)?; + let s_pos = scopes.iter().position(|&s| s==scope_id).ok_or(Error::::ScopeNotFound)?; + scopes.remove(s_pos); + if scopes.is_empty(){ + scopes_option.clone_from(&None); + } + Ok(()) + })?; + let mut scope_users = >::iter_prefix((pallet_id, scope_id)) + .flat_map(|(_role, users)|users).collect::>(); + // exclude duplicate users + scope_users.sort(); scope_users.dedup(); + // remove on RolesByUser + scope_users.iter().for_each(|user|{ + >::remove((user, pallet_id, scope_id)); + }); + // remove on users by scope + >::remove_prefix((pallet_id, scope_id), None); + + Ok(()) + } + + /// External pallet storage removal + /// + /// Removes all storage associated to a external pallet. + /// + /// Executing this function will delete all role lists and permissions linked + /// to that pallet. + /// ### Parameters: + /// - `pallet_id`: The unique pallet identifier. + fn remove_pallet_storage(pallet: IdOrVec) -> DispatchResult{ + let pallet_id_enum = pallet.to_id_enum(); + let pallet_id = pallet_id_enum.to_id(); + //remove all scopes + let scopes = >::get(pallet_id); + for scope in scopes{ + Self::remove_scope(pallet_id_enum.clone(), scope)?; + } + // remove all roles + let pallet_roles = >::take(pallet_id); + //check if there's other pallet that uses the roles, if not, remove them + let all_pallet_roles = >::iter().map(| p| p.1.to_vec()) + .collect::>>(); + let flatten_all_pallet_roles = all_pallet_roles.iter().flatten().collect::>(); + let filtered_roles = pallet_roles.iter().filter(|pallet_role| !flatten_all_pallet_roles.contains(pallet_role)); + filtered_roles.for_each(|role|{ + >::remove(role); + }); + //remove all permissions + >::remove_prefix(pallet_id, None); + >::remove_prefix(pallet_id, None); + Ok(()) + } + + /// Role creation and coupling with pallet. + /// + /// Creates the specified roles if needed and adds them to the pallet. + /// Recommended first step to enable RBAC on a external pallet. + /// ### Parameters: + /// - `pallet_id`: The unique pallet identifier. + /// - `roles`: A list of roles to create, encoded in bytes. + fn create_and_set_roles(pallet: IdOrVec, roles: Vec>) -> + Result, DispatchError>{ + let mut role_ids= Vec::<[u8;32]>::new(); + for role in roles{ + role_ids.push( Self::create_role(role.to_owned())? ); + } + Self::set_multiple_pallet_roles(pallet.to_id_enum(), role_ids.clone())?; + let bounded_ids = Self::bound(role_ids, Error::::ExceedMaxRolesPerPallet)?; + Self::deposit_event(Event::RolesStored(pallet.to_id())); + Ok(bounded_ids) + } + + /// Role creation. + /// + /// Creates a role and returns its identifier, if its already created, + /// the function will return the preexisting one. + /// ### Parameters: + /// - `role`: A role to create, encoded in bytes. + fn create_role(role: Vec)-> Result{ + let role_id = role.using_encoded(blake2_256); + // no "get_or_insert" method found + let b_role = Self::bound::<_,T::RoleMaxLen>(role, Error::::ExceedRoleMaxLen)?; + ensure!(role_id == b_role.using_encoded(blake2_256), Error::::NoneValue); + if !>::contains_key(role_id) {>::insert(role_id, b_role)}; + Ok(role_id) + } + + /// Role coupling with pallet. + /// + /// Assigns a previously created role to a pallet. + /// + /// ### Parameters: + /// - `pallet_id`: The unique pallet identifier. + /// - `role_id`: The unique role identifier. + fn set_role_to_pallet(pallet: IdOrVec, role_id: RoleId )-> DispatchResult{ + ensure!(>::contains_key(role_id), Error::::RoleNotFound); + >::try_mutate(pallet.to_id(), |roles|{ + ensure!(!roles.contains(&role_id), Error::::RoleAlreadyLinkedToPallet ); + roles.try_push(role_id).map_err(|_| Error::::ExceedMaxRolesPerPallet) + })?; + Ok(()) + } + + /// Multiple role coupling with pallet. + /// + /// Assigns multiple, previously created roles to a pallet. + /// ### Parameters: + /// - `pallet_id`: The unique pallet identifier. + /// - `roles`: A list of unique role identifiers. + fn set_multiple_pallet_roles(pallet: IdOrVec, roles: Vec)->DispatchResult{ + let pallet_id = pallet.to_id(); + // checks for duplicates: + ensure!(Self::has_unique_elements(roles.clone()), Error::::DuplicateRole); + let pallet_roles = >::get(&pallet_id); + for id in roles.clone(){ + ensure!(!pallet_roles.contains(&id), Error::::RoleAlreadyLinkedToPallet ); + } + >::try_mutate(pallet_id, |pallet_roles|{ + pallet_roles.try_extend(roles.into_iter()) + }).map_err(|_| Error::::ExceedMaxRolesPerPallet)?; + + Ok(()) + } + + /// Role assignation to a user + /// + /// Assigns a role to a user in a scope context. + /// ### Parameters: + /// - `user`: The account which the role will be granted. + /// - `pallet_id`: The unique pallet identifier. + /// - `scope_id`: The scope in which the role will be granted. + /// - `role_id`: The role identifier to grant for the user. + fn assign_role_to_user(user: T::AccountId, pallet: IdOrVec , scope_id: &ScopeId, role_id: RoleId) -> DispatchResult{ + let pallet_id_enum = pallet.to_id_enum(); + let pallet_id = pallet_id_enum.to_id(); + Self::scope_exists(pallet_id_enum.clone(), scope_id)?; + Self::is_role_linked_to_pallet(pallet_id_enum, &role_id)?; + >::try_mutate((&user, pallet_id, scope_id), | roles |{ + ensure!(!roles.contains(&role_id), Error::::UserAlreadyHasRole); + roles.try_push(role_id).map_err(|_| Error::::ExceedMaxRolesPerUser) + })?; + + >::try_mutate((pallet_id, scope_id, role_id), | users|{ + ensure!(!users.contains(&user), Error::::UserAlreadyHasRole); + users.try_push(user).map_err(|_| Error::::ExceedMaxUsersPerRole) + })?; + Ok(()) + } + + /// Role removal from the user. + /// + /// Removes the specified role from a user in a scope context. the user will no longer + /// be able to enforce the removed role and its permissions. + /// ### Parameters: + /// - `user`: The account which the role will be removed. + /// - `pallet_id`: The unique pallet identifier. + /// - `scope_id`: The scope in which the role will be removed. + /// - `role_id`: The role identifier to remove from the user. + fn remove_role_from_user(user: T::AccountId, pallet: IdOrVec, scope_id: &ScopeId, role_id: RoleId) -> DispatchResult{ + let pallet_id = pallet.to_id(); + >::try_mutate_exists::<_,(),DispatchError,_>((user.clone(), pallet_id, scope_id), |user_roles_option|{ + let user_roles = user_roles_option.as_mut().ok_or(Error::::UserHasNoRoles)?; + let r_pos = user_roles.iter().position(|&r| r==role_id).ok_or(Error::::RoleNotFound)?; + user_roles.remove(r_pos); + if user_roles.is_empty(){ + user_roles_option.clone_from(&None) + } + Ok(()) + })?; + >::try_mutate_exists::<_,(),DispatchError,_>((pallet_id, scope_id, role_id), |auth_users_option|{ + let auth_users = auth_users_option.as_mut().ok_or(Error::::RoleHasNoUsers)?; + let u_pos = auth_users.iter().position(|u| *u==user).ok_or(Error::::UserNotFound)?; + auth_users.remove(u_pos); + if auth_users.is_empty(){ + auth_users_option.clone_from(&None); + } + Ok(()) + })?; + Ok(()) + } + + /// Permission creation and coupling with a role. + /// + /// Creates the specified permissions if needed and assigns them to a role. + /// ### Parameters: + /// - `pallet_id`: The unique pallet identifier. + /// - `role_id`: The role identifier to which the permissions will + /// be linked to. + /// - `permissions`: A list of permissions to create and link, + /// encoded in bytes. + fn create_and_set_permissions(pallet: IdOrVec, role_id: RoleId, permissions: Vec>)-> + Result, DispatchError> { + ensure!(Self::has_unique_elements(permissions.clone()), Error::::DuplicatePermission); + let pallet_id_enum = pallet.to_id_enum(); + Self::is_role_linked_to_pallet(pallet_id_enum.clone(), &role_id )?; + let mut permission_ids = Vec::<[u8;32]>::new(); + for permission in permissions{ + permission_ids.push( Self::create_permission(pallet_id_enum.clone(), permission.to_owned())? ); + } + Self::set_multiple_permissions_to_role(pallet_id_enum, role_id, permission_ids.clone())?; + let b_permissions = Self::bound(permission_ids, Error::::ExceedMaxPermissionsPerRole)?; + Ok(b_permissions) + } + + /// Permission creation + /// + /// Creates the specified permission in the specified pallet.. + /// ### Parameters: + /// - `pallet_id`: The unique pallet identifier. + /// - `permission`: The permission to insert, encoded in bytes. + fn create_permission(pallet: IdOrVec, permission: Vec) -> Result{ + let permission_id = Self::to_id(permission.clone()); + let pallet_id = pallet.to_id(); + + let b_permission = Self::bound:: + <_,T::PermissionMaxLen>(permission, Error::::ExceedPermissionMaxLen)?; + + if !>::contains_key(pallet_id, permission_id){ + >::insert(pallet_id, permission_id, b_permission); + } + Ok(permission_id) + } + + /// Permission linking to role. + /// + /// Assigns a previously created permission to a role. + /// ### Parameters: + /// - `pallet_id`: The unique pallet identifier. + /// - `role_id`: The role identifier to which the permission will be added. + /// - `permission_id`: The permission to assign to the role. + fn set_permission_to_role( pallet: IdOrVec, role_id: RoleId, permission_id: PermissionId ) -> DispatchResult{ + let pallet_id_enum = pallet.to_id_enum(); + let pallet_id = pallet_id_enum.to_id(); + Self::is_role_linked_to_pallet(pallet_id_enum, &role_id)?; + + ensure!(>::contains_key(pallet_id, permission_id), Error::::PermissionNotFound); + + >::try_mutate(pallet_id, role_id, | role_permissions|{ + ensure!(!role_permissions.contains(&permission_id), Error::::DuplicatePermission); + role_permissions.try_push(permission_id).map_err(|_| Error::::ExceedMaxPermissionsPerRole) + })?; + Ok(()) + } + + /// Multiple permissions assignation to a role + /// + /// Assigns multiple, previously created permissions + /// to a role in a pallet context. + /// ### Parameters: + /// - `pallet_id`: The unique pallet identifier. + /// - `role_id`: The role identifier to which the permissions will be added. + /// - `permissions`: A list of permission identifiers to assign to the role. + fn set_multiple_permissions_to_role( pallet: IdOrVec, role_id: RoleId, permissions: Vec )-> DispatchResult{ + // checks for duplicates: + ensure!(Self::has_unique_elements(permissions.clone()), Error::::DuplicatePermission); + let pallet_id_enum = pallet.to_id_enum(); + let pallet_id = pallet_id_enum.to_id(); + Self::is_role_linked_to_pallet(pallet_id_enum, &role_id )?; + + let role_permissions = >::get(&pallet_id, role_id); + for id in permissions.clone(){ + ensure!(!role_permissions.contains(&id), Error::::PermissionAlreadyLinkedToRole ); + } + >::try_mutate(pallet_id, role_id, |role_permissions|{ + role_permissions.try_extend(permissions.into_iter()) + }).map_err(|_| Error::::ExceedMaxPermissionsPerRole)?; + Ok(()) + } + + /*---- Helper functions ----*/ + + /// Authorization function + /// + /// Checks if the user has a role that includes the specified permission. + /// ### Parameters: + /// - `user`: The account to validate. + /// - `pallet_id`: The unique pallet identifier. + /// - `scope_id`: The scope context in which the permission will be validated. + /// - `permission_id`: The permission the user must have. + fn is_authorized(user: T::AccountId, pallet: IdOrVec, scope_id: &ScopeId, permission_id: &PermissionId) -> DispatchResult{ + let pallet_id_enum = pallet.to_id_enum(); + let pallet_id = pallet_id_enum.to_id(); + Self::scope_exists(pallet_id_enum.clone(), scope_id)?; + Self::permission_exists(pallet_id_enum, permission_id)?; + // get roles the user has in this scope + let user_roles = >::get((user, pallet_id, scope_id)); + // determine if one of the roles has the requested permission + let has_permission = user_roles.iter().any(|r_id| >::get(pallet_id, r_id).contains(permission_id)); + ensure!(has_permission, Error::::NotAuthorized); + Ok(()) + } + + /// User role validation function + /// + /// Checks if the user has at least one of the specified roles. + /// ### Parameters: + /// - `user`: The account to validate. + /// - `pallet_id`: The unique pallet identifier. + /// - `scope_id`: The scope context in which the permission will be validated. + /// - `role_ids`: A list of roles to validate. + fn has_role(user: T::AccountId, pallet: IdOrVec, scope_id: &ScopeId, role_ids: Vec)->DispatchResult { + let pallet_id_enum = pallet.to_id_enum(); + Self::scope_exists(pallet_id_enum.clone(), scope_id)?; + let user_roles = >::get((user, pallet_id_enum.to_id(), scope_id)); + ensure!( + user_roles.iter().any(|r| role_ids.contains(r) ), + Error::::NotAuthorized + ); + Ok(()) + } + + /// Scope validation + /// + /// Checks if the scope exists in that pallet. + /// ### Parameters: + /// - `pallet_id`: The unique pallet identifier. + /// - `scope_id`: The scope to validate. + fn scope_exists(pallet: IdOrVec, scope_id:&ScopeId) -> DispatchResult{ + ensure!(>::get(pallet.to_id()).contains(scope_id), Error::::ScopeNotFound); + Ok(()) + } + + /// Permission validation. + /// + /// Checks if the permission exists in a pallet context. + /// ### Parameters: + /// - `pallet_id`: The unique pallet identifier. + /// - `permission_id`: The permission to validate. + fn permission_exists(pallet: IdOrVec, permission_id: &PermissionId)->DispatchResult{ + ensure!(>::contains_key(pallet.to_id(), permission_id), Error::::PermissionNotFound); + Ok(()) + } + + /// Role validation + /// + /// Checks if the role is linked to the pallet. + /// ### Parameters: + /// - `pallet_id`: The unique pallet identifier. + /// - `role_id`: The role to validate + fn is_role_linked_to_pallet(pallet: IdOrVec, role_id: &RoleId)-> DispatchResult{ + // The role exists, now check if the role is assigned to that pallet + >::get(pallet.to_id()).iter().find(|pallet_role| *pallet_role==role_id ) + .ok_or(Error::::RoleNotLinkedToPallet)?; + Ok(()) + } + + /// Permission linking validation + /// + /// Checks if the permission is linked to the role in the pallet context. + /// ### Parameters: + /// - `pallet_id`: The unique pallet identifier. + /// - `role_id`: The role which should have the permission. + /// - `permission_id`: The permission which the role should have. + fn is_permission_linked_to_role(pallet: IdOrVec, role_id: &RoleId, permission_id: &PermissionId)-> DispatchResult{ + let role_permissions = >::get(pallet.to_id(), role_id); + ensure!(role_permissions.contains(permission_id), Error::::PermissionNotLinkedToRole ); + Ok(()) + } + + /// Role list length + /// + /// Returns the number of user that have the specified role in a scope context. + /// ### Parameters: + /// - `pallet_id`: The unique pallet identifier. + /// - `scope_id`: The scope in which the users will be retrieved. + /// - `role_id`: The role in which the number of users will be counted. + fn get_role_users_len(pallet: IdOrVec, scope_id:&ScopeId, role_id: &RoleId) ->usize{ + >::get((pallet.to_id(), scope_id, role_id)).len() + } + + fn to_id(v: Vec)->[u8;32]{ + v.using_encoded(blake2_256) + } + + type MaxRolesPerPallet = T::MaxRolesPerPallet; + + type MaxPermissionsPerRole = T::MaxPermissionsPerRole; + + type PermissionMaxLen = T::PermissionMaxLen; + + type RoleMaxLen = T::RoleMaxLen; + +} + +impl Pallet{ + fn bound>(vec: Vec, err : Error )->Result, Error>{ + BoundedVec::::try_from(vec).map_err(|_| err) + } + + fn has_unique_elements(vec: Vec) -> bool{ + let mut filtered_vec = vec.clone(); + filtered_vec.sort(); + filtered_vec.dedup(); + vec.len() == filtered_vec.len() + } +} \ No newline at end of file diff --git a/pallets/rbac/src/lib.rs b/pallets/rbac/src/lib.rs new file mode 100644 index 00000000..db325af0 --- /dev/null +++ b/pallets/rbac/src/lib.rs @@ -0,0 +1,199 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +/// Edit this file to define custom logic or remove it if it is not needed. +/// Learn more about FRAME and the core library of Substrate FRAME pallets: +/// +pub use pallet::*; + +#[cfg(test)] +mod mock; + +#[cfg(test)] +mod tests; + +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; + + +mod functions; +pub mod types; + +#[frame_support::pallet] +pub mod pallet { + use frame_support::pallet_prelude::{*, ValueQuery}; + use crate::types::*; + + #[pallet::config] + pub trait Config: frame_system::Config { + type Event: From> + IsType<::Event>; + #[pallet::constant] + type MaxScopesPerPallet: Get; + #[pallet::constant] + type MaxRolesPerPallet: Get; + #[pallet::constant] + type RoleMaxLen: Get; + #[pallet::constant] + type PermissionMaxLen: Get; + #[pallet::constant] + type MaxPermissionsPerRole: Get; + #[pallet::constant] + type MaxRolesPerUser: Get; + #[pallet::constant] + type MaxUsersPerRole: Get; + } + + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + pub struct Pallet(_); + + /*--- Onchain storage section ---*/ + + #[pallet::storage] + #[pallet::getter(fn scopes)] + pub(super) type Scopes = StorageMap< + _, + Identity, + PalletId, // pallet_id + BoundedVec, // scopes_id + ValueQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn roles)] + pub(super) type Roles = StorageMap< + _, + Identity, + RoleId, // role_id + BoundedVec, // role + OptionQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn pallet_roles)] + pub(super) type PalletRoles = StorageMap< + _, + Identity, + PalletId, // pallet_id + BoundedVec, // role_id + ValueQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn permissions)] + pub(super) type Permissions = StorageDoubleMap< + _, + Identity, + PalletId, // pallet_id + Identity, + PermissionId, // permission_id + BoundedVec, // permission str + ValueQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn permissions_by_role)] + pub(super) type PermissionsByRole = StorageDoubleMap< + _, + Identity, + PalletId, // pallet_id + Identity, + RoleId, // role_id + BoundedVec, // permission_ids + ValueQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn roles_by_user)] + pub(super) type RolesByUser = StorageNMap< + _, + ( + NMapKey,// user + NMapKey, // pallet_id + NMapKey, // scope_id + ), + BoundedVec, // roles (ids) + ValueQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn users_by_scope)] + pub(super) type UsersByScope = StorageNMap< + _, + ( + // getting "the trait bound `usize: scale_info::TypeInfo` is not satisfied" errors + // on a 32 bit target, this is 4 bytes and on a 64 bit target, this is 8 bytes. + NMapKey, // pallet_id + NMapKey, // scope_id + NMapKey, // role_id + ), + BoundedVec, // users + ValueQuery, + >; + + + + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// An initial roles config was stored [pallet_id] + RolesStored(PalletId), + } + + // Errors inform users that something went wrong. + #[pallet::error] + pub enum Error { + /// Error names should be descriptive. + NoneValue, + /// The specified scope doesn't exists + ScopeNotFound, + /// The scope is already linked with the pallet + ScopeAlreadyExists, + /// The specified role doesn't exist or it hasn't been set to the user + RoleNotFound, + /// The permission doesn't exist in the pallet + PermissionNotFound, + /// The specified user hasn't been asigned to this scope + UserNotFound, + /// The provided role list must have unique elements + DuplicateRole, + /// The provided permission list must have unique elements + DuplicatePermission, + /// The user has that role asigned in that scope + UserAlreadyHasRole, + /// The role is already linked in the pallet + RoleAlreadyLinkedToPallet, + /// The role exists but it hasn't been linked to the pallet + RoleNotLinkedToPallet, + /// The permission is already linked to that role in that scope + PermissionAlreadyLinkedToRole, + /// The permission wasn't found in the roles capabilities + PermissionNotLinkedToRole, + /// The user doesn't have any roles in this pallet + UserHasNoRoles, + /// The role doesn't have any users assigned to it + RoleHasNoUsers, + /// The pallet name is too long + ExceedPalletNameMaxLen, + /// The pallet has too many scopes + ExceedMaxScopesPerPallet, + /// The pallet cannot have more roles + ExceedMaxRolesPerPallet, + /// The specified role cannot have more permission in this scope + ExceedMaxPermissionsPerRole, + /// The user cannot have more roles in this scope + ExceedMaxRolesPerUser, + /// This role cannot have assigned to more users in this scope + ExceedMaxUsersPerRole, + /// The role string is too long + ExceedRoleMaxLen, + /// The permission string is too long + ExceedPermissionMaxLen, + /// The user does not have the specified role + NotAuthorized, + } + + #[pallet::call] + impl Pallet { + } +} \ No newline at end of file diff --git a/pallets/rbac/src/mock.rs b/pallets/rbac/src/mock.rs new file mode 100644 index 00000000..621662b9 --- /dev/null +++ b/pallets/rbac/src/mock.rs @@ -0,0 +1,79 @@ +use crate as pallet_rbac; +use frame_support::parameter_types; +use frame_system as system; +use sp_core::H256; +use sp_runtime::{ + testing::Header, + traits::{BlakeTwo256, IdentityLookup}, +}; + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +// Configure a mock runtime to test the pallet. +frame_support::construct_runtime!( + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system::{Pallet, Call, Config, Storage, Event}, + RBAC: pallet_rbac::{Pallet, Call, Storage, Event}, + } +); + +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub const SS58Prefix: u8 = 42; +} + +impl system::Config for Test { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type Origin = Origin; + type Call = Call; + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Header = Header; + type Event = Event; + type BlockHashCount = BlockHashCount; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = (); + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = SS58Prefix; + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} + +parameter_types! { + pub const MaxScopesPerPallet: u32 = 2; + pub const MaxRolesPerPallet: u32 = 3; + pub const RoleMaxLen: u32 = 10; + pub const PermissionMaxLen: u32 = 15; + pub const MaxPermissionsPerRole: u32 = 3; + pub const MaxRolesPerUser: u32 = 2; + pub const MaxUsersPerRole: u32 = 2; +} +impl pallet_rbac::Config for Test { + type Event = Event; + type MaxScopesPerPallet = MaxScopesPerPallet; + type MaxRolesPerPallet = MaxRolesPerPallet; + type RoleMaxLen = RoleMaxLen; + type PermissionMaxLen = PermissionMaxLen; + type MaxPermissionsPerRole = MaxPermissionsPerRole; + type MaxRolesPerUser = MaxRolesPerUser; + type MaxUsersPerRole = MaxUsersPerRole; +} +// Build genesis storage according to the mock runtime. +pub fn new_test_ext() -> sp_io::TestExternalities { + system::GenesisConfig::default().build_storage::().unwrap().into() +} diff --git a/pallets/rbac/src/tests.rs b/pallets/rbac/src/tests.rs new file mode 100644 index 00000000..f69b7c68 --- /dev/null +++ b/pallets/rbac/src/tests.rs @@ -0,0 +1,829 @@ +use crate::{mock::*, Error, types::{RoleBasedAccessControl, RoleId, ScopeId, PermissionId, IdOrVec}, Config, PermissionsByRole, Permissions}; +use frame_support::{assert_noop, assert_ok, assert_err, BoundedVec, pallet_prelude::DispatchResult}; + +type AccountId = ::AccountId; + +fn pallet_name()->IdOrVec{ + IdOrVec::Vec( + "pallet_test".as_bytes().to_vec() + ) +} + +fn pallet_id()->[u8;32]{ + pallet_name().to_id() +} + +fn create_scope(n: u8)->ScopeId{ + let scope_id = [n;32]; + assert_ok!(RBAC::create_scope(pallet_name(), scope_id)); + assert!(RBAC::scopes(pallet_id()).contains(&scope_id)); + scope_id +} + +fn gen_roles(n_roles: u32)-> Vec>{ + let mut v = Vec::new(); + for i in 0..n_roles{ + v.push(format!("role{}",i).into_bytes().to_vec()); + } + v +} + +fn gen_permissions(n_permissions: u32)-> Vec>{ + let mut v = Vec::new(); + for i in 0..n_permissions{ + v.push(format!("permission{}",i).into_bytes().to_vec()); + } + v +} + +fn create_role(role: Vec)->RoleId{ + let r_id = RBAC::create_role(role.clone()).unwrap(); + assert_eq!(RBAC::roles(r_id).unwrap().to_vec(), role); + r_id +} + +fn create_and_set_roles(roles: Vec>)->BoundedVec::MaxRolesPerPallet >{ + let role_ids = RBAC::create_and_set_roles(pallet_name(), roles).unwrap(); + let inserted_roles_list = RBAC::pallet_roles(pallet_id()); + assert!( + role_ids.iter().all(|r_id| inserted_roles_list.contains(r_id)) + ); + role_ids +} + +fn set_role_to_pallet(role_id: RoleId){ + assert_ok!(RBAC::set_role_to_pallet(pallet_name(), role_id)); +} + +fn set_multiple_pallet_roles(roles: Vec){ + assert_ok!(RBAC::set_multiple_pallet_roles(pallet_name(), roles)); +} + +fn remove_scope(n: u8){ + assert_ok!(RBAC::remove_scope(pallet_name(), [n;32])); + assert!(RBAC::scope_exists(pallet_name(),&[n;32]).is_err()); +} + +fn remove_role_from_user(user: AccountId, scope_id: &ScopeId, role_id: RoleId){ + assert_ok!(RBAC::remove_role_from_user(user, pallet_name(), scope_id, role_id)); + let user_roles = RBAC::roles_by_user((user, pallet_id(), scope_id)); + assert!(!user_roles.contains(&role_id)); + let role_users = RBAC::users_by_scope((pallet_id(), scope_id, role_id)); + assert!(!role_users.contains(&user)); +} + +fn remove_pallet_storage(){ + assert_ok!(RBAC::remove_pallet_storage(pallet_name())); + assert!(RBAC::scopes(pallet_id()).is_empty()); + assert!(RBAC::pallet_roles(pallet_id()).is_empty()); + assert_eq!(>::iter_prefix(pallet_id()).count(), 0); + assert_eq!(>::iter_prefix(pallet_id()).count(), 0); +} + +fn assign_role_to_user(user: AccountId, scope_id : &ScopeId, role_id: RoleId){ + assert_ok!( + RBAC::assign_role_to_user(user, pallet_name(), scope_id, role_id) + ); + let user_roles = RBAC::roles_by_user((user,pallet_id(), scope_id)); + assert!(user_roles.contains(&role_id)); + let role_users = RBAC::users_by_scope((pallet_id(), scope_id, role_id)); + assert!(role_users.contains(&user)); +} + +fn create_permission(permission: Vec)-> PermissionId{ + let permission_id = RBAC::create_permission(pallet_name(), permission.clone()).unwrap(); + assert_eq!( + RBAC::permissions(pallet_id(), permission_id).to_vec(), + permission + ); + permission_id +} + +fn set_permission_to_role(role_id: RoleId, permission_id: PermissionId){ + assert_ok!(RBAC::set_permission_to_role(pallet_name(), role_id, permission_id)); + assert!(RBAC::permissions_by_role(pallet_id(), role_id).contains(&permission_id)); +} + +fn set_multiple_permissions_to_role(role_id: RoleId, permissions: Vec){ + assert_ok!( + RBAC::set_multiple_permissions_to_role(pallet_name(), role_id, permissions.clone()) + ); + let role_permissions = RBAC::permissions_by_role(pallet_id(), role_id); + assert!( + permissions.iter().all(|p|{role_permissions.contains(p)}), + ); +} + +fn create_and_set_permissions(role_id: RoleId, permissions: Vec>)->BoundedVec::MaxPermissionsPerRole>{ + let permission_ids = RBAC::create_and_set_permissions(pallet_name(), role_id,permissions).unwrap(); + let role_permissions = RBAC::permissions_by_role(pallet_id(), role_id); + assert!( + permission_ids.iter().all(|p|{role_permissions.contains(p)}), + ); + permission_ids +} + +fn is_authorized(user: AccountId, scope_id : &ScopeId, permission_id: &PermissionId) -> DispatchResult{ + RBAC::is_authorized(user, pallet_name(), scope_id, permission_id) +} + +fn has_role(user: AccountId, scope_id : &ScopeId, role_ids: Vec) -> DispatchResult{ + RBAC::has_role(user, pallet_name(), scope_id, role_ids) +} + +fn scope_exists(scope_id : &ScopeId) -> DispatchResult{ + RBAC::scope_exists(pallet_name(), scope_id) +} + +fn permission_exists(permission_id: &PermissionId) -> DispatchResult { + RBAC::permission_exists(pallet_name(), permission_id) +} + +fn is_role_linked_to_pallet(role_id: &RoleId) -> DispatchResult { + RBAC::is_role_linked_to_pallet(pallet_name(), role_id) +} + +fn is_permission_linked_to_role(role_id: &RoleId, permission_id: &PermissionId) -> DispatchResult { + RBAC::is_permission_linked_to_role(pallet_name(), role_id, permission_id) +} + +fn get_role_users_len(scope_id : &ScopeId, role_id: &RoleId)-> usize{ + RBAC::get_role_users_len(pallet_name(), scope_id, role_id) +} + +#[test] +fn create_scope_works() { + new_test_ext().execute_with(|| { + create_scope(0); + }); +} + +#[test] +fn create_scope_twice_should_fail() { + new_test_ext().execute_with(|| { + create_scope(0); + assert_noop!(RBAC::create_scope(pallet_name(), [0;32]), Error::::ScopeAlreadyExists); + }); +} + +#[test] +fn exceeding_max_scopes_per_pallet_should_fail() { + new_test_ext().execute_with(|| { + for n in 0..::MaxScopesPerPallet::get(){ + create_scope(n.try_into().unwrap()); + } + assert_noop!(RBAC::create_scope(pallet_name(), [255;32]), Error::::ExceedMaxScopesPerPallet); + }); +} + +#[test] +fn remove_scope_works() { + new_test_ext().execute_with(|| { + let n_roles = ::MaxRolesPerPallet::get(); + let scope_id = create_scope(0); + let role_ids = create_and_set_roles(gen_roles(n_roles)); + role_ids.iter().enumerate().for_each(|(i,role_id)|{ + assign_role_to_user(i.try_into().unwrap(), &scope_id, *role_id); + }); + remove_scope(0); + }); +} + +#[test] +fn remove_non_existent_scope_should_fail() { + new_test_ext().execute_with(|| { + let n_roles = ::MaxRolesPerPallet::get(); + create_and_set_roles(gen_roles(n_roles)); + assert_noop!( + RBAC::remove_scope(pallet_name(), [0;32]), + Error::::ScopeNotFound + ); + }); +} + +#[test] +fn remove_pallet_storage_works() { + new_test_ext().execute_with(|| { + create_scope(0); + remove_pallet_storage(); + }); +} + +#[test] +fn create_role_should_work() { + new_test_ext().execute_with(|| { + create_role("owner".as_bytes().to_vec()); + }); +} + +#[test] +fn exceeding_role_max_len_should_fail() { + new_test_ext().execute_with(|| { + assert_noop!( + RBAC::create_role("0123456789A".as_bytes().to_vec()), + Error::::ExceedRoleMaxLen + ); + }); +} + +#[test] +fn set_role_to_pallet_should_work() { + new_test_ext().execute_with(|| { + let role_id = create_role("owner".as_bytes().to_vec()); + set_role_to_pallet(role_id); + }); +} + +#[test] +fn set_nonexistent_role_to_pallet_should_fail() { + new_test_ext().execute_with(|| { + assert_noop!( + RBAC::set_role_to_pallet(pallet_name(), [0;32]), + Error::::RoleNotFound + ); + }); +} + +#[test] +fn set_role_to_pallet_twice_should_fail() { + new_test_ext().execute_with(|| { + let role_id = create_role("owner".as_bytes().to_vec()); + set_role_to_pallet(role_id); + assert_noop!( + RBAC::set_role_to_pallet(pallet_name(), role_id), + Error::::RoleAlreadyLinkedToPallet + ); + }); +} + + +#[test] +fn exceeding_max_roles_per_pallet_should_fail() { + new_test_ext().execute_with(|| { + let role_max_len = ::MaxRolesPerPallet::get(); + gen_roles(role_max_len).iter().for_each(|role| { + let role_id = create_role(role.clone()); + set_role_to_pallet(role_id); + }); + let role_id = create_role("admin".as_bytes().to_vec()); + assert_noop!( + RBAC::set_role_to_pallet(pallet_name(), role_id), + Error::::ExceedMaxRolesPerPallet + ); + }); +} + +#[test] +fn set_multiple_pallet_roles_should_work() { + new_test_ext().execute_with(|| { + let n_roles = ::MaxRolesPerPallet::get()-1; + let role_ids: Vec = gen_roles(n_roles).iter().map(|role|{ + create_role(role.clone()) + }).collect(); + set_multiple_pallet_roles(role_ids); + }); +} + +#[test] +fn set_multiple_duplicate_pallet_roles_should_fail() { + new_test_ext().execute_with(|| { + let n_roles = ::MaxRolesPerPallet::get()-1; + let mut roles = gen_roles(n_roles); + roles.push("role0".as_bytes().to_vec()); + let role_ids: Vec = roles.iter().map(|role|{ + create_role(role.clone()) + }).collect(); + assert_noop!( + RBAC::set_multiple_pallet_roles(pallet_name(), role_ids), + Error::::DuplicateRole + ); + }); +} + +#[test] +fn set_multiple_pallet_roles_twice_should_fail() { + new_test_ext().execute_with(|| { + let n_roles = ::MaxRolesPerPallet::get(); + let roles = gen_roles(n_roles); + let role_ids: Vec = roles.iter().map(|role|{ + create_role(role.clone()) + }).collect(); + set_multiple_pallet_roles(role_ids.clone()); + assert_noop!( + RBAC::set_multiple_pallet_roles(pallet_name(), role_ids), + Error::::RoleAlreadyLinkedToPallet + ); + }); +} + +#[test] +fn create_and_set_role_should_work() { + new_test_ext().execute_with(|| { + create_and_set_roles(gen_roles(::MaxRolesPerPallet::get())); + }); +} + +#[test] +fn create_and_set_duplicate_role_should_fail() { + new_test_ext().execute_with(|| { + let mut roles = gen_roles(::MaxRolesPerPallet::get()-1); + roles.push("role0".as_bytes().to_vec()); + assert_err!( + RBAC::create_and_set_roles(pallet_name(), roles), + Error::::DuplicateRole + ); + }); +} + +#[test] +fn exceeding_max_roles_per_pallet_from_create_and_set_role_should_fail() { + new_test_ext().execute_with(|| { + let exceed = ::MaxRolesPerPallet::get() + 1; + assert_err!( + RBAC::create_and_set_roles(pallet_name(), gen_roles(exceed)), + Error::::ExceedMaxRolesPerPallet + ); + }); +} + +#[test] +fn assign_role_to_user_should_work() { + new_test_ext().execute_with(|| { + let scope_id = create_scope(0); + let role_id = create_role("owner".as_bytes().to_vec()); + set_role_to_pallet(role_id); + assign_role_to_user(0, &scope_id, role_id); + }); +} + +#[test] +fn assign_role_to_user_twice_should_fail() { + new_test_ext().execute_with(|| { + let scope_id = create_scope(0); + let role_id = create_role("owner".as_bytes().to_vec()); + set_role_to_pallet(role_id); + assign_role_to_user(0, &scope_id, role_id); + assert_noop!( + RBAC::assign_role_to_user(0, pallet_name(), &scope_id, role_id), + Error::::UserAlreadyHasRole + ); + }); +} + +#[test] +fn assign_role_to_user_without_scope_should_fail() { + new_test_ext().execute_with(|| { + let role_id = create_role("owner".as_bytes().to_vec()); + set_role_to_pallet(role_id); + assert_noop!( + RBAC::assign_role_to_user(0, pallet_name(), &[0;32], role_id), + Error::::ScopeNotFound + ); + }); +} + +#[test] +fn exceeding_max_roles_per_user_should_fail() { + new_test_ext().execute_with(|| { + let scope_id = create_scope(0); + let n_roles = ::MaxRolesPerUser::get(); + let roles = gen_roles(n_roles); + let role_ids: Vec = roles.iter().map(|role|{ + create_role(role.clone()) + }).collect(); + set_multiple_pallet_roles(role_ids.clone()); + role_ids.iter().for_each(|role_id|{ + assign_role_to_user(0, &scope_id, *role_id); + }); + let last_role_id = create_role("owner".as_bytes().to_vec()); + set_role_to_pallet(last_role_id); + assert_noop!( + RBAC::assign_role_to_user(0, pallet_name(), &scope_id, last_role_id), + Error::::ExceedMaxRolesPerUser + ); + }); +} + +#[test] +fn exceeding_max_users_per_role_should_fail() { + new_test_ext().execute_with(|| { + let scope_id = create_scope(0); + let role_id = create_role("owner".as_bytes().to_vec()); + let max_users_per_role = ::MaxUsersPerRole::get(); + set_role_to_pallet(role_id); + for i in 0..max_users_per_role{ + assign_role_to_user(i.into(), &scope_id, role_id) + } + // avoiding assert_noop because it checks if the storage mutated + assert_err!( + RBAC::assign_role_to_user((max_users_per_role+1).into(), pallet_name(), &scope_id, role_id), + Error::::ExceedMaxUsersPerRole + ); + }); +} + +#[test] +fn remove_role_from_user_should_work() { + new_test_ext().execute_with(|| { + let scope_id = create_scope(0); + let role_id = create_role("owner".as_bytes().to_vec()); + set_role_to_pallet(role_id); + assign_role_to_user(0, &scope_id, role_id); + remove_role_from_user(0, &scope_id, role_id); + }); +} + +#[test] +fn remove_non_assigned_role_from_user_should_fail() { + new_test_ext().execute_with(|| { + let scope_id = create_scope(0); + assert_noop!( + RBAC::remove_role_from_user(0, pallet_name(), &scope_id, [0;32]), + Error::::UserHasNoRoles + ); + }); +} + +#[test] +fn remove_non_existent_role_from_user_should_fail() { + new_test_ext().execute_with(|| { + let scope_id = create_scope(0); + let role_id = create_role("owner".as_bytes().to_vec()); + set_role_to_pallet(role_id); + assign_role_to_user(0, &scope_id, role_id); + assert_noop!( + RBAC::remove_role_from_user(0, pallet_name(), &scope_id, [0;32]), + Error::::RoleNotFound + ); + }); +} + +#[test] +fn create_permission_should_work() { + new_test_ext().execute_with(|| { + create_permission("enroll".as_bytes().to_vec()); + }); +} + +#[test] +fn exceeding_permission_max_len_should_fail() { + new_test_ext().execute_with(|| { + assert_noop!( + RBAC::create_permission(pallet_name(), "0123456789ABCDFG".as_bytes().to_vec()), + Error::::ExceedPermissionMaxLen + ); + }); +} + +#[test] +fn set_permission_to_role_should_work() { + new_test_ext().execute_with(|| { + let role_id = create_role("admin".as_bytes().to_vec()); + set_role_to_pallet(role_id); + let permission_id = create_permission("enroll".as_bytes().to_vec()); + set_permission_to_role(role_id, permission_id); + }); +} + +#[test] +fn set_non_existent_permission_to_role_should_fail() { + new_test_ext().execute_with(|| { + let role_id = create_role("admin".as_bytes().to_vec()); + set_role_to_pallet(role_id); + assert_noop!( + RBAC::set_permission_to_role(pallet_name(), role_id, [0;32]), + Error::::PermissionNotFound + ); + }); +} + + +#[test] +fn set_permission_to_role_twice_should_fail() { + new_test_ext().execute_with(|| { + let role_id = create_role("admin".as_bytes().to_vec()); + set_role_to_pallet(role_id); + let permission_id = create_permission("enroll".as_bytes().to_vec()); + set_permission_to_role(role_id, permission_id); + assert_noop!( + RBAC::set_permission_to_role(pallet_name(), role_id, permission_id), + Error::::DuplicatePermission + ); + }); +} + +#[test] +fn exceeding_max_permissions_per_role_should_fail() { + new_test_ext().execute_with(|| { + let role_id = create_role("owner".as_bytes().to_vec()); + let max_permissions_per_role = ::MaxPermissionsPerRole::get(); + set_role_to_pallet(role_id); + gen_permissions(max_permissions_per_role).iter() + .for_each(|permission|{ + let permission_id = create_permission(permission.clone()); + set_permission_to_role(role_id, permission_id); + }); + let last_permission_id = create_permission("enroll".as_bytes().to_vec()); + assert_noop!( + RBAC::set_permission_to_role(pallet_name(),role_id, last_permission_id), + Error::::ExceedMaxPermissionsPerRole + ); + }); +} + +#[test] +fn set_multiple_permissions_to_role_should_work() { + new_test_ext().execute_with(|| { + let role_id = create_role("admin".as_bytes().to_vec()); + set_role_to_pallet(role_id); + let permissions = gen_permissions(::MaxPermissionsPerRole::get()); + let permission_ids: Vec = permissions.iter().map(|permission|{ + create_permission(permission.to_vec()) + }).collect(); + set_multiple_permissions_to_role(role_id, permission_ids); + }); +} + +#[test] +fn set_multiple_duplicate_permissions_to_role_should_fail() { + new_test_ext().execute_with(|| { + let role_id = create_role("admin".as_bytes().to_vec()); + set_role_to_pallet(role_id); + let mut permissions = gen_permissions(::MaxPermissionsPerRole::get()-1); + permissions.push("permission0".as_bytes().to_vec()); + let permission_ids: Vec = permissions.iter().map(|permission|{ + create_permission(permission.to_vec()) + }).collect(); + assert_noop!( + RBAC::set_multiple_permissions_to_role(pallet_name(), role_id, permission_ids), + Error::::DuplicatePermission + ); + }); +} + +#[test] +fn set_multiple_permissions_to_unlinked_role_should_fail() { + new_test_ext().execute_with(|| { + let role_id = create_role("admin".as_bytes().to_vec()); + let permissions = gen_permissions(::MaxPermissionsPerRole::get()); + let permission_ids: Vec = permissions.iter().map(|permission|{ + create_permission(permission.to_vec()) + }).collect(); + assert_noop!( + RBAC::set_multiple_permissions_to_role(pallet_name(), role_id, permission_ids), + Error::::RoleNotLinkedToPallet + ); + }); +} + +#[test] +fn set_multiple_permissions_to_role_twice_should_fail() { + new_test_ext().execute_with(|| { + let role_id = create_role("admin".as_bytes().to_vec()); + let permissions = gen_permissions(::MaxPermissionsPerRole::get()); + set_role_to_pallet(role_id); + let permission_ids: Vec = permissions.iter().map(|permission|{ + create_permission(permission.to_vec()) + }).collect(); + set_multiple_permissions_to_role(role_id, permission_ids.clone()); + assert_noop!( + RBAC::set_multiple_permissions_to_role(pallet_name(), role_id, permission_ids), + Error::::PermissionAlreadyLinkedToRole + ); + }); +} + +#[test] +fn exceeding_max_permissions_per_role_from_set_multiple_permissions_to_role_should_fail() { + new_test_ext().execute_with(|| { + let role_id = create_role("admin".as_bytes().to_vec()); + let permissions = gen_permissions(::MaxPermissionsPerRole::get()+1); + set_role_to_pallet(role_id); + let permission_ids: Vec = permissions.iter().map(|permission|{ + create_permission(permission.to_vec()) + }).collect(); + assert_noop!( + RBAC::set_multiple_permissions_to_role(pallet_name(), role_id, permission_ids), + Error::::ExceedMaxPermissionsPerRole + ); + }); +} + +#[test] +fn create_and_set_permissions_should_work() { + new_test_ext().execute_with(|| { + let role_id = create_role("admin".as_bytes().to_vec()); + set_role_to_pallet(role_id); + let permissions = gen_permissions(::MaxPermissionsPerRole::get()); + create_and_set_permissions(role_id, permissions); + }); +} + +#[test] +fn create_set_duplicate_permissions_to_role_should_fail() { + new_test_ext().execute_with(|| { + let role_id = create_role("admin".as_bytes().to_vec()); + set_role_to_pallet(role_id); + let mut permissions = gen_permissions(::MaxPermissionsPerRole::get()-1); + permissions.push("permission0".as_bytes().to_vec()); + assert_noop!( + RBAC::create_and_set_permissions(pallet_name(), role_id, permissions), + Error::::DuplicatePermission + ); + }); +} + +#[test] +fn create_and_set_permissions_to_unlinked_role_should_fail() { + new_test_ext().execute_with(|| { + let role_id = create_role("admin".as_bytes().to_vec()); + let permissions = gen_permissions(::MaxPermissionsPerRole::get()); + assert_noop!( + RBAC::create_and_set_permissions(pallet_name(), role_id, permissions), + Error::::RoleNotLinkedToPallet + ); + }); +} + +#[test] +fn create_and_set_multiple_permissions_to_role_twice_should_fail() { + new_test_ext().execute_with(|| { + let role_id = create_role("admin".as_bytes().to_vec()); + let permissions = gen_permissions(::MaxPermissionsPerRole::get()); + set_role_to_pallet(role_id); + create_and_set_permissions(role_id, permissions.clone()); + assert_noop!( + RBAC::create_and_set_permissions(pallet_name(), role_id, permissions), + Error::::PermissionAlreadyLinkedToRole + ); + }); +} + +#[test] +fn exceeding_max_permissions_per_role_from_create_and_set_permissions_should_fail() { + new_test_ext().execute_with(|| { + let role_id = create_role("admin".as_bytes().to_vec()); + let permissions = gen_permissions(::MaxPermissionsPerRole::get()+1); + set_role_to_pallet(role_id); + assert_err!( + RBAC::create_and_set_permissions(pallet_name(), role_id, permissions), + Error::::ExceedMaxPermissionsPerRole + ); + }); +} + +#[test] +fn is_authorized_should_work() { + new_test_ext().execute_with(|| { + let scope_id = create_scope(0); + let role_ids = create_and_set_roles(["admin".as_bytes().to_vec()].to_vec()); + let mut permission_ids = create_and_set_permissions(*role_ids.get(0).unwrap(), ["enroll".as_bytes().to_vec()].to_vec()); + assign_role_to_user(0, &scope_id, *role_ids.get(0).unwrap()); + assert_ok!( + is_authorized(0, &scope_id, &permission_ids.pop().unwrap()) + ); + }); +} + +#[test] +fn unauthorized_user_should_fail() { + new_test_ext().execute_with(|| { + let scope_id = create_scope(0); + let role_ids = create_and_set_roles(["admin".as_bytes().to_vec()].to_vec()); + let mut permission_ids = create_and_set_permissions(*role_ids.get(0).unwrap(), ["enroll".as_bytes().to_vec()].to_vec()); + assert_noop!( + is_authorized(0, &scope_id, &permission_ids.pop().unwrap()), + Error::::NotAuthorized + ); + }); +} + +#[test] +fn has_role_should_work() { + new_test_ext().execute_with(|| { + let scope_id = create_scope(0); + let role_ids = create_and_set_roles(gen_roles(2)); + assign_role_to_user(0, &scope_id, *role_ids.get(0).unwrap()); + assert_ok!( + has_role(0, &scope_id, role_ids.to_vec()) + ); + }); +} + +#[test] +fn user_that_doesnt_have_role_should_fail() { + new_test_ext().execute_with(|| { + let scope_id = create_scope(0); + let role_ids = create_and_set_roles(gen_roles(2)); + assert_noop!( + has_role(0, &scope_id, role_ids.to_vec()), + Error::::NotAuthorized + ); + }); +} + +#[test] +fn scope_exists_should_work() { + new_test_ext().execute_with(|| { + let scope_id = create_scope(0); + assert_ok!( + scope_exists(&scope_id) + ); + }); +} + +#[test] +fn nonexistent_scope_should_fail() { + new_test_ext().execute_with(|| { + create_scope(0); + assert_noop!( + scope_exists(&[1;32]), + Error::::ScopeNotFound + ); + }); +} + +#[test] +fn permission_exists_should_work() { + new_test_ext().execute_with(|| { + let permission_id = create_permission("enroll".as_bytes().to_vec()); + assert_ok!( + permission_exists(&permission_id) + ); + }); +} + +#[test] +fn nonexistent_permission_should_fail() { + new_test_ext().execute_with(|| { + create_permission("enroll".as_bytes().to_vec()); + assert_noop!( + permission_exists(&[0;32]), + Error::::PermissionNotFound + ); + }); +} + +#[test] +fn is_role_linked_to_pallet_should_work() { + new_test_ext().execute_with(|| { + let role_id = create_role("owner".as_bytes().to_vec()); + set_role_to_pallet(role_id); + assert_ok!( + is_role_linked_to_pallet(&role_id) + ); + }); +} + +#[test] +fn unlinked_role_should_fail() { + new_test_ext().execute_with(|| { + let role_id = create_role("owner".as_bytes().to_vec()); + assert_noop!( + is_role_linked_to_pallet(&role_id), + Error::::RoleNotLinkedToPallet + ); + }); +} + +#[test] +fn is_permission_linked_to_role_should_work() { + new_test_ext().execute_with(|| { + let role_id = create_role("owner".as_bytes().to_vec()); + set_role_to_pallet(role_id); + let permission_id = create_permission("enroll".as_bytes().to_vec()); + set_permission_to_role(role_id, permission_id); + assert_ok!( + is_permission_linked_to_role(&role_id, &permission_id) + ); + }); +} + +#[test] +fn unlinked_permission_should_fail() { + new_test_ext().execute_with(|| { + let role_id = create_role("owner".as_bytes().to_vec()); + set_role_to_pallet(role_id); + let permission_id = create_permission("enroll".as_bytes().to_vec()); + assert_noop!( + is_permission_linked_to_role(&role_id, &permission_id), + Error::::PermissionNotLinkedToRole + ); + }); +} + +#[test] +fn get_role_users_len_should_work() { + new_test_ext().execute_with(|| { + let scope_id = create_scope(0); + let role_id = create_role("owner".as_bytes().to_vec()); + set_role_to_pallet(role_id); + + assert_eq!(get_role_users_len(&scope_id, &role_id), 0); + + assign_role_to_user(0, &scope_id, role_id); + assign_role_to_user(1, &scope_id, role_id); + + assert_eq!(get_role_users_len(&scope_id, &role_id), 2); + }); +} diff --git a/pallets/rbac/src/types.rs b/pallets/rbac/src/types.rs new file mode 100644 index 00000000..1fc7891a --- /dev/null +++ b/pallets/rbac/src/types.rs @@ -0,0 +1,70 @@ +//use super::*; +use frame_support::pallet_prelude::*; +use sp_runtime::sp_std::vec::Vec; +use frame_support::sp_io::hashing::blake2_256; + + +pub type PalletId = [u8;32]; +pub type RoleId = [u8;32]; +pub type ScopeId = [u8;32]; +pub type PermissionId = [u8;32]; + +#[derive(Encode, Decode, Clone, Eq, PartialEq,)] +pub enum IdOrVec{ + Id([u8;32]), + Vec(Vec) +} + +impl IdOrVec{ + pub fn to_id_enum(&self)->Self{ + match self{ + Self::Id(_) => self.clone(), + Self::Vec(_) => Self::Id(Self::to_id(self)) + } + } + + pub fn to_id(&self)->[u8;32]{ + match self{ + Self::Id(id) => *id, + Self::Vec(v) => v.clone().using_encoded(blake2_256) + } + } +} + +pub trait RoleBasedAccessControl{ + type MaxRolesPerPallet: Get; + type MaxPermissionsPerRole: Get; + type RoleMaxLen: Get; + type PermissionMaxLen: Get; + // scopes + fn create_scope(pallet: IdOrVec, scope_id: ScopeId) -> DispatchResult; + // scope removal + fn remove_scope(pallet: IdOrVec, scope_id: ScopeId) -> DispatchResult; + // removes all from one pallet/application + fn remove_pallet_storage(pallet: IdOrVec) -> DispatchResult; + // roles creation and setting + fn create_and_set_roles(pallet: IdOrVec, roles: Vec>) -> + Result, DispatchError>; + fn create_role(role: Vec)-> Result; + fn set_role_to_pallet(pallet: IdOrVec, role_id: RoleId )-> DispatchResult; + fn set_multiple_pallet_roles(pallet: IdOrVec, roles: Vec)->DispatchResult; + fn assign_role_to_user(user: AccountId, pallet: IdOrVec, scope_id: &ScopeId, role_id: RoleId) -> DispatchResult; + // role removal + fn remove_role_from_user(user: AccountId, pallet: IdOrVec, scope_id: &ScopeId, role_id: RoleId) -> DispatchResult; + // permissions + fn create_and_set_permissions(pallet: IdOrVec, role: RoleId, permissions: Vec>)-> + Result, DispatchError>; + fn create_permission(pallet: IdOrVec, permissions: Vec) -> Result; + fn set_permission_to_role( pallet: IdOrVec, role: RoleId, permission: PermissionId ) -> DispatchResult; + fn set_multiple_permissions_to_role( pallet: IdOrVec, role: RoleId, permission: Vec )-> DispatchResult; + // helpers + fn is_authorized(user: AccountId, pallet: IdOrVec, scope_id: &ScopeId, permission_id: &PermissionId ) -> DispatchResult; + fn has_role(user: AccountId, pallet: IdOrVec, scope_id: &ScopeId, role_ids: Vec)->DispatchResult; + fn scope_exists(pallet: IdOrVec, scope_id:&ScopeId) -> DispatchResult; + fn permission_exists(pallet: IdOrVec, permission_id: &PermissionId)->DispatchResult; + fn is_role_linked_to_pallet(pallet: IdOrVec, role_id: &RoleId)-> DispatchResult; + fn is_permission_linked_to_role(pallet: IdOrVec, role_id: &RoleId, permission_id: &PermissionId)-> DispatchResult; + fn get_role_users_len(pallet: IdOrVec, scope_id:&ScopeId, role_id: &RoleId) -> usize; + fn to_id(v: Vec)->[u8;32]; + +} \ No newline at end of file diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 50282264..2b5d78f9 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -65,6 +65,7 @@ pallet-template = { version = "4.0.0-dev", default-features = false, path = "../ pallet-fruniques = { version = "0.1.0-dev", default-features = false, path = "../pallets/fruniques" } pallet-nbv-storage = { version = "4.0.0-dev", default-features = false, path = "../pallets/nbv-storage" } pallet-gated-marketplace = { version = "4.0.0-dev", default-features = false, path = "../pallets/gated-marketplace" } +pallet-rbac = { version = "4.0.0-dev", default-features = false, path = "../pallets/rbac" } pallet-confidential-docs = { version = "4.0.0-dev", default-features = false, path = "../pallets/confidential-docs" } [build-dependencies] @@ -102,6 +103,7 @@ std = [ "pallet-node-authorization/std", "pallet-nbv-storage/std", "pallet-gated-marketplace/std", + "pallet-rbac/std", "pallet-confidential-docs/std", "sp-api/std", "sp-block-builder/std", @@ -115,6 +117,7 @@ std = [ "sp-transaction-pool/std", "sp-version/std", ] + runtime-benchmarks = [ "frame-benchmarking/runtime-benchmarks", "frame-support/runtime-benchmarks", diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index ec70a728..37b5bca8 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -68,6 +68,8 @@ pub type Index = u32; /// A hash of some data used by the chain. pub type Hash = sp_core::H256; +pub type Moment = u64; + /// Opaque types. These are used by the CLI to instantiate machinery that don't need to know /// the specifics of the runtime. They can then be made to be agnostic over specific formats /// of data like extrinsics, allowing for them to continue syncing the network through upgrades @@ -255,7 +257,7 @@ impl pallet_balances::Config for Runtime { type MaxReserves = (); type ReserveIdentifier = [u8; 8]; /// The type for recording an account's balance. - type Balance = Balance; + type Balance = u128; /// The ubiquitous event type. type Event = Event; type DustRemoval = (); @@ -552,6 +554,8 @@ parameter_types! { pub const NameMaxLen: u32 = 100; pub const MaxFiles: u32 = 10; pub const MaxApplicationsPerCustodian: u32 = 10; + pub const MaxMarketsPerItem: u32 = 10; + pub const MaxOffersPerMarket: u32 = 100; } impl pallet_gated_marketplace::Config for Runtime { type Event = Event; @@ -568,8 +572,12 @@ impl pallet_gated_marketplace::Config for Runtime { type NameMaxLen= NameMaxLen; type MaxFiles = MaxFiles; type MaxApplicationsPerCustodian = MaxApplicationsPerCustodian; + type MaxMarketsPerItem = MaxMarketsPerItem; + type MaxOffersPerMarket = MaxOffersPerMarket; + type Timestamp = Timestamp; + type Moment = Moment; + type Rbac = RBAC; } - parameter_types! { pub const XPubLen: u32 = XPUB_LEN; pub const PSBTMaxLen: u32 = 2048; @@ -622,6 +630,26 @@ impl pallet_confidential_docs::Config for Runtime { } +parameter_types! { + pub const MaxScopesPerPallet: u32 = 1000; + pub const MaxRolesPerPallet: u32 = 20; + pub const RoleMaxLen: u32 = 30; + pub const PermissionMaxLen: u32 = 30; + pub const MaxPermissionsPerRole: u32 = 12; + pub const MaxRolesPerUser: u32 = 10; + pub const MaxUsersPerRole: u32 = 10; +} +impl pallet_rbac::Config for Runtime { + type Event = Event; + type MaxScopesPerPallet = MaxScopesPerPallet; + type MaxRolesPerPallet = MaxRolesPerPallet; + type RoleMaxLen = RoleMaxLen; + type PermissionMaxLen = PermissionMaxLen; + type MaxPermissionsPerRole = MaxPermissionsPerRole; + type MaxRolesPerUser = MaxRolesPerUser; + type MaxUsersPerRole = MaxUsersPerRole; +} + parameter_types! { pub const MaxRecursions: u32 = 10; pub const ResourceSymbolLimit: u32 = 10; @@ -713,6 +741,7 @@ construct_runtime!( GatedMarketplace: pallet_gated_marketplace, Assets: pallet_assets, NBVStorage: pallet_nbv_storage, + RBAC: pallet_rbac, ConfidentialDocs: pallet_confidential_docs, } );