Skip to content

Commit

Permalink
Token traceability (#13)
Browse files Browse the repository at this point in the history
* add original_id + optional set parent

* version bump

* clean up

* use Output struct for run_process

* benchmarking with Output struct

* add Output to README
  • Loading branch information
jonmattgray authored Jan 4, 2022
1 parent a6ba0b9 commit 2650131
Show file tree
Hide file tree
Showing 7 changed files with 449 additions and 93 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.

13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,11 @@ In order to use the API within `polkadot.js` you'll need to configure the follow
"parents": "Vec<TokenId>",
"children": "Option<Vec<TokenId>>"
},
"Output": {
"roles": "BTreeMap<RoleKey, AccountId>",
"metadata": "BTreeMap<TokenMetadataKey, TokenMetadataValue>",
"parent_index": "Option<u32>"
},
"MetadataValue": {
"_enum": {
"File": "Hash",
Expand Down Expand Up @@ -147,7 +152,13 @@ TokensById get(fn tokens_by_id): map T::TokenId => Token<T::AccountId, T::RoleKe
Tokens can be minted/burnt by calling the following extrinsic under `SimpleNFT`:

```rust
pub fn run_process(origin, inputs: Vec<T::TokenId>, outputs: Vec<(BTreeMap<T::RoleKey, T::AccountId>, BTreeMap<T::TokenMetadataKey, T::TokenMetadataValue>)> -> dispatch::DispatchResult { ... }
pub fn run_process(
origin: OriginFor<T>,
inputs: Vec<T::TokenId>,
outputs: Vec<
Output<T::AccountId, T::RoleKey, 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.4.3'
version = '2.5.0'

[[bin]]
name = 'vitalam-node'
Expand Down
37 changes: 27 additions & 10 deletions pallets/simple-nft/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
use super::*;

use core::convert::TryInto;
use frame_benchmarking::{account, benchmarks, impl_benchmark_test_suite};
use frame_system::RawOrigin;
use sp_std::{boxed::Box, vec, vec::Vec};

#[allow(unused)]
use crate::Module as SimpleNFT;
use crate::Output;

const SEED: u32 = 0;

Expand All @@ -19,7 +21,13 @@ fn add_nfts<T: Config>(r: u32) -> Result<(), &'static str> {
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(|_| (roles.clone(), metadata.clone())).collect();
let outputs: Vec<_> = (0..r)
.map(|_| Output {
roles: roles.clone(),
metadata: metadata.clone(),
parent_index: None,
})
.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 @@ -40,23 +48,32 @@ fn mk_inputs<T: Config>(i: u32) -> Result<Vec<T::TokenId>, &'static str> {

fn mk_outputs<T: Config>(
o: u32,
) -> Result<
Vec<(
BTreeMap<T::RoleKey, T::AccountId>,
BTreeMap<T::TokenMetadataKey, T::TokenMetadataValue>,
)>,
&'static str,
> {
inputs_len: u32,
) -> Result<Vec<Output<T::AccountId, T::RoleKey, 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(|_| (roles.clone(), metadata.clone())).collect::<Vec<_>>();
let outputs = (0..o)
.map(|output_index| Output {
roles: roles.clone(),
metadata: metadata.clone(),
parent_index: valid_parent_index(inputs_len, output_index),
})
.collect::<Vec<_>>();

Ok(outputs)
}

fn valid_parent_index(input_len: u32, output_count: u32) -> Option<u32> {
if input_len > 0 && output_count < input_len {
Some(output_count)
} else {
None
}
}

fn nth_token_id<T: Config>(iteration: u32) -> Result<T::TokenId, &'static str> {
let token_id = (0..iteration).fold(T::TokenId::default(), |acc, _| acc + One::one());
Ok(token_id)
Expand All @@ -69,7 +86,7 @@ benchmarks! {

add_nfts::<T>(i)?;
let inputs = mk_inputs::<T>(i)?;
let outputs = mk_outputs::<T>(o)?;
let outputs = mk_outputs::<T>(o, inputs.len().try_into().unwrap())?;
let caller: T::AccountId = account("owner", 0, SEED);
}: _(RawOrigin::Signed(caller), inputs, outputs)
verify {
Expand Down
82 changes: 55 additions & 27 deletions pallets/simple-nft/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use codec::{Decode, Encode};
pub use pallet::*;
use sp_runtime::traits::{AtLeast32Bit, One};
use sp_std::collections::btree_map::BTreeMap;
use sp_std::collections::btree_set::BTreeSet;

/// A FRAME pallet for handling non-fungible tokens
use sp_std::prelude::*;
Expand All @@ -22,6 +23,7 @@ mod benchmarking;
#[cfg_attr(feature = "std", derive(Debug))]
pub struct Token<AccountId, RoleKey, TokenId, BlockNumber, TokenMetadataKey: Ord, TokenMetadataValue> {
id: TokenId,
original_id: TokenId,
roles: BTreeMap<RoleKey, AccountId>,
creator: AccountId,
created_at: BlockNumber,
Expand All @@ -31,6 +33,14 @@ pub struct Token<AccountId, RoleKey, TokenId, BlockNumber, TokenMetadataKey: Ord
children: Option<Vec<TokenId>>, // children is the only mutable component of the token
}

#[derive(Encode, Decode, Default, Clone, PartialEq)]
#[cfg_attr(feature = "std", derive(Debug))]
pub struct Output<AccountId, RoleKey, TokenMetadataKey: Ord, TokenMetadataValue> {
roles: BTreeMap<RoleKey, AccountId>,
metadata: BTreeMap<TokenMetadataKey, TokenMetadataValue>,
parent_index: Option<u32>,
}

pub mod weights;

pub use weights::WeightInfo;
Expand Down Expand Up @@ -104,6 +114,10 @@ pub mod pallet {
TooManyMetadataItems,
/// Token mint was attempted without setting a default role
NoDefaultRole,
/// Index for the consumed token to set as parent is out of bounds
OutOfBoundsParent,
/// Attempted to set the same parent on multiple tokens to mint
DuplicateParents,
}

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

// INPUT VALIDATION

// check multiple tokens are not trying to have the same parent
let mut parent_indices = BTreeSet::new();

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);
ensure!(
output.roles.contains_key(&T::RoleKey::default()),
Error::<T>::NoDefaultRole
);

// check metadata count
ensure!(
output.1.len() <= T::MaxMetadataCount::get() as usize,
output.metadata.len() <= T::MaxMetadataCount::get() as usize,
Error::<T>::TooManyMetadataItems
);

// check parent index
if output.parent_index.is_some() {
let index = output.parent_index.unwrap() as usize;
ensure!(inputs.get(index).is_some(), Error::<T>::OutOfBoundsParent);
ensure!(parent_indices.insert(index), Error::<T>::DuplicateParents);
}
}

// check origin owns inputs and that inputs have not been burnt
Expand All @@ -155,27 +179,31 @@ pub mod pallet {
let last = LastToken::<T>::get();

// 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), (roles, metadata)| {
let next = _next_token(last);
<TokensById<T>>::insert(
next,
Token {
id: next,
roles: roles.clone(),
creator: sender.clone(),
created_at: now,
destroyed_at: None,
metadata: metadata.clone(),
parents: inputs.clone(),
children: None,
},
);
let mut next_children = children.clone();
next_children.push(next);
(next, next_children)
});
let (last, children) = outputs.iter().fold((last, Vec::new()), |(last, children), output| {
let next = _next_token(last);
let original_id = if output.parent_index.is_some() {
inputs.get(output.parent_index.unwrap() as usize).unwrap().clone()
} else {
next
};
<TokensById<T>>::insert(
next,
Token {
id: next,
original_id: original_id,
roles: output.roles.clone(),
creator: sender.clone(),
created_at: now,
destroyed_at: None,
metadata: output.metadata.clone(),
parents: inputs.clone(),
children: None,
},
);
let mut next_children = children.clone();
next_children.push(next);
(next, next_children)
});

// Burn inputs
inputs.iter().for_each(|id| {
Expand Down
Loading

0 comments on commit 2650131

Please sign in to comment.