--- /dev/null +++ b/pallets/structure/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "pallet-structure" +version = "0.1.0" +edition = "2021" + +[dependencies] +frame-support = { default-features = false, git = 'https://github.com/paritytech/substrate.git', branch = 'polkadot-v0.9.17' } +frame-system = { default-features = false, git = 'https://github.com/paritytech/substrate.git', branch = 'polkadot-v0.9.17' } +sp-std = { default-features = false, git = 'https://github.com/paritytech/substrate.git', branch = 'polkadot-v0.9.17' } +pallet-common = { path = "../common", default-features = false } +parity-scale-codec = { version = "2.0.0", default-features = false, features = [ + "derive", +] } +scale-info = { version = "1.0.0", default-features = false, features = [ + "derive", +] } +up-data-structs = { path = "../../primitives/data-structs", default-features = false } + +[features] +default = ["std"] +std = [ + "frame-support/std", + "frame-system/std", + "sp-std/std", + "pallet-common/std", + "scale-info/std", + "parity-scale-codec/std", + "up-data-structs/std", +] --- /dev/null +++ b/pallets/structure/src/lib.rs @@ -0,0 +1,159 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +use sp_std::collections::btree_set::BTreeSet; + +use frame_support::dispatch::DispatchError; +use frame_support::fail; +pub use pallet::*; +use pallet_common::{dispatch::CollectionDispatch, CollectionHandle}; +use up_data_structs::{CollectionId, TokenId, mapping::TokenAddressMapping}; + +#[frame_support::pallet] +pub mod pallet { + use frame_support::Parameter; + use frame_support::dispatch::{GetDispatchInfo, UnfilteredDispatchable}; + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; + + use super::*; + + #[pallet::error] + pub enum Error { + /// While searched for owner, got already checked account + OuroborosDetected, + /// While searched for owner, encountered depth limit + DepthLimit, + /// While searched for owner, found token owner by not-yet-existing token + TokenNotFound, + } + + #[pallet::event] + pub enum Event { + /// Executed call on behalf of token + Executed(DispatchResult), + } + + #[pallet::config] + pub trait Config: frame_system::Config + pallet_common::Config { + type Event: IsType<::Event> + From>; + type Call: Parameter + UnfilteredDispatchable + GetDispatchInfo; + } + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::call] + impl Pallet { + // #[pallet::weight({ + // let dispatch_info = call.get_dispatch_info(); + + // ( + // dispatch_info.weight + // // Cost of dereferencing parent + // .saturating_add(T::DbWeight::get().reads(2 * *max_depth as Weight)) + // .saturating_add(4000 * *max_depth as Weight), + // dispatch_info.class) + // })] + // pub fn execute( + // origin: OriginFor, + // call: Box<::Call>, + // max_depth: u32, + // ) -> DispatchResult { + } +} + +#[derive(PartialEq)] +pub enum Parent { + /// Token owned by normal account + Normal(CrossAccountId), + /// Passed token not found + TokenNotFound, + /// Token owner is another token (target token still may not exist) + Token(CollectionId, TokenId), +} + +impl Pallet { + pub fn find_parent( + collection: CollectionId, + token: TokenId, + ) -> Result, DispatchError> { + // TODO: Reduce cost by not reading collection config + let handle = match CollectionHandle::try_get(collection) { + Ok(v) => v, + Err(_) => return Ok(Parent::TokenNotFound), + }; + let handle = T::CollectionDispatch::dispatch(handle); + let handle = handle.as_dyn(); + + Ok(match handle.token_owner(token) { + Some(owner) => match T::CrossTokenAddressMapping::address_to_token(&owner) { + Some((collection, token)) => Parent::Token(collection, token), + None => Parent::Normal(owner), + }, + None => Parent::TokenNotFound, + }) + } + + pub fn parent_chain( + mut collection: CollectionId, + mut token: TokenId, + ) -> impl Iterator, DispatchError>> { + let mut finished = false; + let mut visited = BTreeSet::new(); + visited.insert((collection, token)); + core::iter::from_fn(move || { + if finished { + return None; + } + let parent = Self::find_parent(collection, token); + match parent { + Ok(Parent::Token(new_collection, new_token)) => { + collection = new_collection; + token = new_token; + if !visited.insert((new_collection, new_token)) { + finished = true; + return Some(Err(>::OuroborosDetected.into())); + } + } + _ => finished = true, + } + Some(parent as Result<_, DispatchError>) + }) + } + + /// Try to dereference address, until finding top level owner + /// + /// May return token address if parent token not yet exists + pub fn find_topmost_owner( + collection: CollectionId, + token: TokenId, + max_depth: u32, + ) -> Result { + let owner = Self::parent_chain(collection, token) + .take(max_depth as usize) + .find(|p| matches!(p, Ok(Parent::Normal(_) | Parent::TokenNotFound))) + .ok_or(>::DepthLimit)??; + + Ok(match owner { + Parent::Normal(v) => v, + _ => fail!(>::TokenNotFound), + }) + } + + /// Check if token indirectly owned by specified user + pub fn indirectly_owned( + user: T::CrossAccountId, + collection: CollectionId, + token: TokenId, + max_depth: u32, + ) -> Result { + let target_parent = match T::CrossTokenAddressMapping::address_to_token(&user) { + Some((collection, token)) => Parent::Token(collection, token), + None => Parent::Normal(user), + }; + + Ok(Self::parent_chain(collection, token) + .take(max_depth as usize) + .any(|parent| Ok(&target_parent) == parent.as_ref())) + } +} --- a/runtime/opal/Cargo.toml +++ b/runtime/opal/Cargo.toml @@ -399,6 +399,7 @@ pallet-inflation = { path = '../../pallets/inflation', default-features = false } up-data-structs = { path = '../../primitives/data-structs', default-features = false } pallet-common = { default-features = false, path = "../../pallets/common" } +pallet-structure = { default-features = false, path = "../../pallets/structure" } pallet-fungible = { default-features = false, path = "../../pallets/fungible" } pallet-refungible = { default-features = false, path = "../../pallets/refungible" } pallet-nonfungible = { default-features = false, path = "../../pallets/nonfungible" } --- a/runtime/opal/src/lib.rs +++ b/runtime/opal/src/lib.rs @@ -886,7 +886,6 @@ impl pallet_structure::Config for Runtime { type Event = Event; type Call = Call; - type WeightInfo = pallet_structure::weights::SubstrateWeight; } impl pallet_fungible::Config for Runtime { @@ -1009,6 +1008,7 @@ Fungible: pallet_fungible::{Pallet, Storage} = 67, Refungible: pallet_refungible::{Pallet, Storage} = 68, Nonfungible: pallet_nonfungible::{Pallet, Storage} = 69, + Structure: pallet_structure::{Pallet, Call, Storage, Event} = 70, // Frontier EVM: pallet_evm::{Pallet, Config, Call, Storage, Event} = 100, --- a/runtime/quartz/Cargo.toml +++ b/runtime/quartz/Cargo.toml @@ -84,6 +84,7 @@ 'serde', 'pallet-inflation/std', 'pallet-common/std', + 'pallet-structure/std', 'pallet-fungible/std', 'pallet-refungible/std', 'pallet-nonfungible/std', @@ -399,6 +400,7 @@ pallet-inflation = { path = '../../pallets/inflation', default-features = false } up-data-structs = { path = '../../primitives/data-structs', default-features = false } pallet-common = { default-features = false, path = "../../pallets/common" } +pallet-structure = { default-features = false, path = "../../pallets/structure" } pallet-fungible = { default-features = false, path = "../../pallets/fungible" } pallet-refungible = { default-features = false, path = "../../pallets/refungible" } pallet-nonfungible = { default-features = false, path = "../../pallets/nonfungible" } --- a/runtime/unique/Cargo.toml +++ b/runtime/unique/Cargo.toml @@ -84,6 +84,7 @@ 'serde', 'pallet-inflation/std', 'pallet-common/std', + 'pallet-structure/std', 'pallet-fungible/std', 'pallet-refungible/std', 'pallet-nonfungible/std', @@ -398,6 +399,7 @@ pallet-inflation = { path = '../../pallets/inflation', default-features = false } up-data-structs = { path = '../../primitives/data-structs', default-features = false } pallet-common = { default-features = false, path = "../../pallets/common" } +pallet-structure = { default-features = false, path = "../../pallets/structure" } pallet-fungible = { default-features = false, path = "../../pallets/fungible" } pallet-refungible = { default-features = false, path = "../../pallets/refungible" } pallet-nonfungible = { default-features = false, path = "../../pallets/nonfungible" }