Skip to content

Commit

Permalink
Multiple token roles (#12)
Browse files Browse the repository at this point in the history
* Make owner BTreeMap<RoleKey, AccountId>

* version bump

* Update roles list

* add multiple roles test

* explicit enum indexes and no default role set test

* owner -> roles

* use weights from 1000 repeated benchmark

* add multiple role happy path test
  • Loading branch information
jonmattgray authored Dec 9, 2021
1 parent 53d837d commit e2a55cf
Show file tree
Hide file tree
Showing 9 changed files with 237 additions and 72 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 11 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ Then you can run the benchmark tool with for example
./target/release/vitalam-node benchmark \
--pallet 'pallet_simple_nft' \
--extrinsic '*' \
--repeat 1000 \
--output ./weights/
```

Expand All @@ -104,11 +105,12 @@ In order to use the API within `polkadot.js` you'll need to configure the follow
"PeerId": "Vec<u8>",
"Key": "Vec<u8>",
"TokenId": "u128",
"RoleKey": "Role",
"TokenMetadataKey": "[u8; 32]",
"TokenMetadataValue": "MetadataValue",
"Token": {
"id": "TokenId",
"owner": "AccountId",
"roles": "BTreeMap<RoleKey, AccountId>",
"creator": "AccountId",
"created_at": "BlockNumber",
"destroyed_at": "Option<BlockNumber>",
Expand All @@ -120,29 +122,32 @@ In order to use the API within `polkadot.js` you'll need to configure the follow
"_enum": {
"File": "Hash",
"Literal": "[u8; 32]",
"None": "null",
},
"None": "null"
}
},
"Role": {
"_enum": ["Admin", "ManufacturingEngineer", "ProcurementBuyer", "ProcurementPlanner", "Supplier"]
}
}
```

### SimpleNFT pallet

The `SimpleNFT` pallet exposes an extrinsic for minting/burning tokens and a storage format that allows their retrieval. All of the additional types listed above, apart from `PeerId`, are for the `SimpleNFT` pallet.
The `SimpleNFT` pallet exposes an extrinsic for minting/burning tokens and a storage format that allows their retrieval.

Note: The json object with types, described above, has been upgraded from `"Address": "AccountId", "LookupSource": "AccountId"` to `"Address": "MultiAddress", "LookupSource": "MultiAddress"` and it also needs to be used in conjunction with the new version of _PolkaDot JS_, **v4.7.2** or higher.

Two storage endpoints are then exposed under `SimpleNFT` for the id of the last token issued (`LastToken`) and a mapping of tokens by id (`TokensById`):

```rust
LastToken get(fn last_token): T::TokenId;
TokensById get(fn tokens_by_id): map T::TokenId => Token<T::AccountId, T::TokenId, T::BlockNumber, T::TokenMetadataKey, T::TokenMetadataValue>;
TokensById get(fn tokens_by_id): map T::TokenId => Token<T::AccountId, T::RoleKey, T::TokenId, T::BlockNumber, T::TokenMetadataKey, T::TokenMetadataValue>;
```

Tokens can be minted/burnt by calling the following extrinsic under `SimpleNFT`:

```rust
pub fn run_process(origin, inputs: Vec<T::TokenId>, outputs: Vec<(T::AccountId, BTreeMap<T::TokenMetadataKey, T::TokenMetadataValue>)> -> dispatch::DispatchResult { ... }
pub fn run_process(origin, inputs: Vec<T::TokenId>, outputs: Vec<(BTreeMap<T::RoleKey, T::AccountId>, BTreeMap<T::TokenMetadataKey, T::TokenMetadataValue>)> -> dispatch::DispatchResult { ... }
```

All of this functionality can be easily accessed using [https://polkadot.js.org/apps](https://polkadot.js.org/apps) against a running `dev` node. You will need to add a network endpoint of `ws://localhost:9944` under `Settings` and apply the above type configurations in the `Settings/Developer` tab.
Expand Down
2 changes: 1 addition & 1 deletion node/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ edition = '2018'
license = 'Apache-2.0'
repository = 'https://github.com/digicatapult/vitalam-node/'
name = 'vitalam-node'
version = '2.3.0'
version = '2.4.0'

[[bin]]
name = 'vitalam-node'
Expand Down
22 changes: 16 additions & 6 deletions pallets/simple-nft/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ use crate::Module as SimpleNFT;
const SEED: u32 = 0;

fn add_nfts<T: Config>(r: u32) -> Result<(), &'static str> {
let owner: T::AccountId = account("owner", 0, SEED);
let account_id: T::AccountId = account("owner", 0, SEED);
let mut roles = BTreeMap::new();
let mut metadata = BTreeMap::new();
roles.insert(T::RoleKey::default(), account_id.clone());
metadata.insert(T::TokenMetadataKey::default(), T::TokenMetadataValue::default());
// let _ = T::Currency::make_free_balance_be(&owner, BalanceOf::<T>::max_value());

let outputs: Vec<_> = (0..r).map(|_| (owner.clone(), metadata.clone())).collect();
SimpleNFT::<T>::run_process(RawOrigin::Signed(owner.clone()).into(), Vec::new(), outputs)?;
let outputs: Vec<_> = (0..r).map(|_| (roles.clone(), metadata.clone())).collect();
SimpleNFT::<T>::run_process(RawOrigin::Signed(account_id.clone()).into(), Vec::new(), outputs)?;

let expected_last_token = nth_token_id::<T>(r)?;

Expand All @@ -38,11 +40,19 @@ fn mk_inputs<T: Config>(i: u32) -> Result<Vec<T::TokenId>, &'static str> {

fn mk_outputs<T: Config>(
o: u32,
) -> Result<Vec<(T::AccountId, BTreeMap<T::TokenMetadataKey, T::TokenMetadataValue>)>, &'static str> {
let owner: T::AccountId = account("owner", 0, SEED);
) -> Result<
Vec<(
BTreeMap<T::RoleKey, T::AccountId>,
BTreeMap<T::TokenMetadataKey, T::TokenMetadataValue>,
)>,
&'static str,
> {
let account_id: T::AccountId = account("owner", 0, SEED);
let mut roles = BTreeMap::new();
let mut metadata = BTreeMap::new();
roles.insert(T::RoleKey::default(), account_id.clone());
metadata.insert(T::TokenMetadataKey::default(), T::TokenMetadataValue::default());
let outputs = (0..o).map(|_| (owner.clone(), metadata.clone())).collect::<Vec<_>>();
let outputs = (0..o).map(|_| (roles.clone(), metadata.clone())).collect::<Vec<_>>();

Ok(outputs)
}
Expand Down
28 changes: 19 additions & 9 deletions pallets/simple-nft/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ mod benchmarking;

#[derive(Encode, Decode, Default, Clone, PartialEq)]
#[cfg_attr(feature = "std", derive(Debug))]
pub struct Token<AccountId, TokenId, BlockNumber, TokenMetadataKey: Ord, TokenMetadataValue> {
pub struct Token<AccountId, RoleKey, TokenId, BlockNumber, TokenMetadataKey: Ord, TokenMetadataValue> {
id: TokenId,
owner: AccountId,
roles: BTreeMap<RoleKey, AccountId>,
creator: AccountId,
created_at: BlockNumber,
destroyed_at: Option<BlockNumber>,
Expand All @@ -49,6 +49,8 @@ pub mod pallet {
type Event: From<Event<Self>> + IsType<<Self as frame_system::Config>::Event>;

type TokenId: Parameter + AtLeast32Bit + Default + Copy + Codec;
type RoleKey: Parameter + Default + Ord;

type TokenMetadataKey: Parameter + Default + Ord;
type TokenMetadataValue: Parameter + Default;

Expand Down Expand Up @@ -78,7 +80,7 @@ pub mod pallet {
_,
Blake2_128Concat,
T::TokenId,
Token<T::AccountId, T::TokenId, T::BlockNumber, T::TokenMetadataKey, T::TokenMetadataValue>,
Token<T::AccountId, T::RoleKey, T::TokenId, T::BlockNumber, T::TokenMetadataKey, T::TokenMetadataValue>,
ValueQuery, /*, DefaultForExampleStorage*/
>;

Expand All @@ -98,8 +100,10 @@ pub mod pallet {
NotOwned,
/// Mutation was attempted on token that has already been burnt
AlreadyBurnt,
/// Mutation was attempted with too many metadata items
/// Token mint was attempted with too many metadata items
TooManyMetadataItems,
/// Token mint was attempted without setting a default role
NoDefaultRole,
}

// The pallet's dispatchable functions.
Expand All @@ -111,7 +115,10 @@ pub mod pallet {
pub(super) fn run_process(
origin: OriginFor<T>,
inputs: Vec<T::TokenId>,
outputs: Vec<(T::AccountId, BTreeMap<T::TokenMetadataKey, T::TokenMetadataValue>)>,
outputs: Vec<(
BTreeMap<T::RoleKey, T::AccountId>,
BTreeMap<T::TokenMetadataKey, T::TokenMetadataValue>,
)>,
) -> DispatchResultWithPostInfo {
// Check it was signed and get the signer
let sender = ensure_signed(origin)?;
Expand All @@ -124,8 +131,11 @@ pub mod pallet {

// INPUT VALIDATION

// check metadata count
for output in outputs.iter() {
// check at least a default role has been set
ensure!(output.0.contains_key(&T::RoleKey::default()), Error::<T>::NoDefaultRole);

// check metadata count
ensure!(
output.1.len() <= T::MaxMetadataCount::get() as usize,
Error::<T>::TooManyMetadataItems
Expand All @@ -135,7 +145,7 @@ pub mod pallet {
// check origin owns inputs and that inputs have not been burnt
for id in inputs.iter() {
let token = <TokensById<T>>::get(id);
ensure!(token.owner == sender, Error::<T>::NotOwned);
ensure!(token.roles[&T::RoleKey::default()] == sender, Error::<T>::NotOwned);
ensure!(token.children == None, Error::<T>::AlreadyBurnt);
}

Expand All @@ -147,13 +157,13 @@ pub mod pallet {
// Create new tokens getting a tuple of the last token created and the complete Vec of tokens created
let (last, children) = outputs
.iter()
.fold((last, Vec::new()), |(last, children), (owner, metadata)| {
.fold((last, Vec::new()), |(last, children), (roles, metadata)| {
let next = _next_token(last);
<TokensById<T>>::insert(
next,
Token {
id: next,
owner: owner.clone(),
roles: roles.clone(),
creator: sender.clone(),
created_at: now,
destroyed_at: None,
Expand Down
13 changes: 13 additions & 0 deletions pallets/simple-nft/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,18 @@ parameter_types! {
pub const MaxMetadataCount: u32 = 3;
}

#[derive(Encode, Decode, Clone, PartialEq, Debug, Eq, Ord, PartialOrd)]
pub enum Role {
Admin,
NotAdmin,
}

impl Default for Role {
fn default() -> Self {
Role::Admin
}
}

#[derive(Encode, Decode, Clone, PartialEq, Debug, Eq)]
pub enum MetadataValue {
File(Hash),
Expand All @@ -82,6 +94,7 @@ impl pallet_simple_nft::Config for Test {
type Event = Event;

type TokenId = u64;
type RoleKey = Role;
type TokenMetadataKey = u64;
type TokenMetadataValue = MetadataValue;

Expand Down
Loading

0 comments on commit e2a55cf

Please sign in to comment.