123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354#![cfg_attr(not(feature = "std"), no_std)]5556use frame_support::{57 dispatch::{DispatchResult, DispatchResultWithPostInfo},58 fail,59 pallet_prelude::*,60};61use pallet_common::{62 dispatch::CollectionDispatch, erc::CrossAccountId, eth::is_collection,63 CommonCollectionOperations,64};65use sp_std::collections::btree_set::BTreeSet;66use up_data_structs::{67 budget::Budget, mapping::TokenAddressMapping, CollectionId, TokenId, TokenOwnerError,68};6970#[cfg(feature = "runtime-benchmarks")]71pub mod benchmarking;72pub mod weights;7374pub use pallet::*;7576pub type SelfWeightOf<T> = <T as crate::Config>::WeightInfo;7778#[frame_support::pallet]79pub mod pallet {80 use frame_support::{dispatch::GetDispatchInfo, traits::UnfilteredDispatchable, Parameter};8182 use super::*;8384 #[pallet::error]85 pub enum Error<T> {86 87 OuroborosDetected,88 89 DepthLimit,90 91 BreadthLimit,92 93 TokenNotFound,94 95 CantNestTokenUnderCollection,96 }9798 #[pallet::event]99 pub enum Event<T> {100 101 Executed(DispatchResult),102 }103104 #[pallet::config]105 pub trait Config: frame_system::Config + pallet_common::Config {106 type WeightInfo: weights::WeightInfo;107 type RuntimeEvent: IsType<<Self as frame_system::Config>::RuntimeEvent> + From<Event<Self>>;108 type RuntimeCall: Parameter109 + UnfilteredDispatchable<RuntimeOrigin = Self::RuntimeOrigin>110 + GetDispatchInfo;111 }112113 #[pallet::pallet]114 pub struct Pallet<T>(_);115116 #[pallet::call]117 impl<T: Config> Pallet<T> {118 119 120121 122 123 124 125 126 127 128 129 130 131 132 133 }134}135136#[derive(PartialEq)]137pub enum Parent<CrossAccountId> {138 139 User(CrossAccountId),140 141 TokenNotFound,142 143 MultipleOwners,144 145 Token(CollectionId, TokenId),146}147148impl<T: Config> Pallet<T> {149 150 151 152 153 154 155 pub fn find_parent(156 collection: CollectionId,157 token: TokenId,158 ) -> Result<Parent<T::CrossAccountId>, DispatchError> {159 160 let handle = match T::CollectionDispatch::dispatch(collection) {161 Ok(v) => v,162 Err(_) => return Ok(Parent::TokenNotFound),163 };164 let handle = handle.as_dyn();165166 Ok(match handle.token_owner(token) {167 Ok(owner) => match T::CrossTokenAddressMapping::address_to_token(&owner) {168 Some((collection, token)) => Parent::Token(collection, token),169 None => Parent::User(owner),170 },171 Err(TokenOwnerError::MultipleOwners) => Parent::MultipleOwners,172 Err(TokenOwnerError::NotFound) => Parent::TokenNotFound,173 })174 }175176 177 178 179 180 181 pub fn parent_chain(182 mut collection: CollectionId,183 mut token: TokenId,184 ) -> impl Iterator<Item = Result<Parent<T::CrossAccountId>, DispatchError>> {185 let mut finished = false;186 let mut visited = BTreeSet::new();187 visited.insert((collection, token));188 core::iter::from_fn(move || {189 if finished {190 return None;191 }192 let parent = Self::find_parent(collection, token);193 match parent {194 Ok(Parent::Token(new_collection, new_token)) => {195 collection = new_collection;196 token = new_token;197 if !visited.insert((new_collection, new_token)) {198 finished = true;199 return Some(Err(<Error<T>>::OuroborosDetected.into()));200 }201 }202 _ => finished = true,203 }204 Some(parent as Result<_, DispatchError>)205 })206 }207208 209 210 211 212 213 214 215 pub fn find_topmost_owner(216 collection: CollectionId,217 token: TokenId,218 budget: &dyn Budget,219 ) -> Result<Option<T::CrossAccountId>, DispatchError> {220 let owner = Self::parent_chain(collection, token)221 .take_while(|_| budget.consume())222 .find(|p| {223 matches!(224 p,225 Ok(Parent::User(_) | Parent::TokenNotFound | Parent::MultipleOwners)226 )227 })228 .ok_or(<Error<T>>::DepthLimit)??;229230 Ok(match owner {231 Parent::User(v) => Some(v),232 Parent::MultipleOwners => None,233 _ => fail!(<Error<T>>::TokenNotFound),234 })235 }236237 238 239 240 241 242 243 pub fn get_checked_topmost_owner(244 collection: CollectionId,245 token: TokenId,246 for_nest: Option<(CollectionId, TokenId)>,247 budget: &dyn Budget,248 ) -> Result<Option<T::CrossAccountId>, DispatchError> {249 250 if Some((collection, token)) == for_nest {251 return Err(<Error<T>>::OuroborosDetected.into());252 }253254 for parent in Self::parent_chain(collection, token).take_while(|_| budget.consume()) {255 match parent? {256 257 Parent::Token(collection, token) if Some((collection, token)) == for_nest => {258 return Err(<Error<T>>::OuroborosDetected.into())259 }260 261 Parent::User(user) => return Ok(Some(user)),262 Parent::TokenNotFound => return Err(<Error<T>>::TokenNotFound.into()),263 Parent::MultipleOwners => return Ok(None),264 265 Parent::Token(_, _) => {}266 }267 }268269 Err(<Error<T>>::DepthLimit.into())270 }271272 273 274 275 276 pub fn burn_item_recursively(277 from: T::CrossAccountId,278 collection: CollectionId,279 token: TokenId,280 self_budget: &dyn Budget,281 breadth_budget: &dyn Budget,282 ) -> DispatchResultWithPostInfo {283 let dispatch = T::CollectionDispatch::dispatch(collection)?;284 let dispatch = dispatch.as_dyn();285 dispatch.burn_item_recursively(from, token, self_budget, breadth_budget)286 }287288 289 290 291 292 293 294 295 pub fn check_indirectly_owned(296 user: T::CrossAccountId,297 collection: CollectionId,298 token: TokenId,299 for_nest: Option<(CollectionId, TokenId)>,300 budget: &dyn Budget,301 ) -> Result<bool, DispatchError> {302 let target_parent = match T::CrossTokenAddressMapping::address_to_token(&user) {303 Some((collection, token)) => match Self::find_topmost_owner(collection, token, budget)?304 {305 Some(topmost_owner) => topmost_owner,306 None => return Ok(false),307 },308 None => user,309 };310311 Self::get_checked_topmost_owner(collection, token, for_nest, budget).map(|indirect_owner| {312 indirect_owner.map_or(false, |indirect_owner| indirect_owner == target_parent)313 })314 }315316 317 318 319 320 321 322 pub fn check_nesting(323 from: T::CrossAccountId,324 under: &T::CrossAccountId,325 collection_id: CollectionId,326 token_id: TokenId,327 nesting_budget: &dyn Budget,328 ) -> DispatchResult {329 Self::try_exec_if_token(under, |collection, parent_id| {330 collection.check_nesting(from, (collection_id, token_id), parent_id, nesting_budget)331 })332 }333334 335 336 337 338 339 pub fn nest_if_sent_to_token(340 from: T::CrossAccountId,341 under: &T::CrossAccountId,342 collection_id: CollectionId,343 token_id: TokenId,344 nesting_budget: &dyn Budget,345 ) -> DispatchResult {346 Self::try_exec_if_token(under, |collection, parent_id| {347 collection.check_nesting(from, (collection_id, token_id), parent_id, nesting_budget)?;348349 collection.nest(parent_id, (collection_id, token_id));350351 Ok(())352 })353 }354355 356 357 358 pub fn nest_if_sent_to_token_unchecked(359 owner: &T::CrossAccountId,360 collection_id: CollectionId,361 token_id: TokenId,362 ) {363 Self::exec_if_token(owner, |collection, parent_id| {364 collection.nest(parent_id, (collection_id, token_id))365 });366 }367368 369 pub fn unnest_if_nested(370 owner: &T::CrossAccountId,371 collection_id: CollectionId,372 token_id: TokenId,373 ) {374 if let Err(e) = Self::try_exec_if_token(owner, |collection, parent_id| {375 collection.unnest(parent_id, (collection_id, token_id));376 Ok(())377 }) {378 log::warn!("unnest precondition failed: {e:?}")379 }380 }381382 383 384 fn exec_if_token(385 account: &T::CrossAccountId,386 action: impl FnOnce(&dyn CommonCollectionOperations<T>, TokenId),387 ) {388 Self::try_exec_if_token(account, |collection, id| {389 action(collection, id);390 Ok(())391 })392 .unwrap();393 }394395 396 397 fn try_exec_if_token(398 account: &T::CrossAccountId,399 action: impl FnOnce(&dyn CommonCollectionOperations<T>, TokenId) -> DispatchResult,400 ) -> DispatchResult {401 if is_collection(account.as_eth()) {402 fail!(<Error<T>>::CantNestTokenUnderCollection);403 }404 let Some((collection, token)) = T::CrossTokenAddressMapping::address_to_token(account)405 else {406 return Ok(());407 };408409 let dispatch = T::CollectionDispatch::dispatch(collection)?;410 let dispatch = dispatch.as_dyn();411412 action(dispatch, token)413 }414}