123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354#![cfg_attr(not(feature = "std"), no_std)]5556use pallet_common::CommonCollectionOperations;57use pallet_common::{erc::CrossAccountId, eth::is_collection};58use sp_std::collections::btree_set::BTreeSet;5960use frame_support::dispatch::{DispatchError, DispatchResult, DispatchResultWithPostInfo};61use frame_support::fail;62pub use pallet::*;63use pallet_common::{dispatch::CollectionDispatch};64use up_data_structs::{65 CollectionId, TokenId, mapping::TokenAddressMapping, budget::Budget, TokenOwnerError,66};6768#[cfg(feature = "runtime-benchmarks")]69pub mod benchmarking;70pub mod weights;7172pub type SelfWeightOf<T> = <T as crate::Config>::WeightInfo;7374#[frame_support::pallet]75pub mod pallet {76 use frame_support::Parameter;77 use frame_support::dispatch::{GetDispatchInfo, UnfilteredDispatchable};78 use frame_support::pallet_prelude::*;7980 use super::*;8182 #[pallet::error]83 pub enum Error<T> {84 85 OuroborosDetected,86 87 DepthLimit,88 89 BreadthLimit,90 91 TokenNotFound,92 93 CantNestTokenUnderCollection,94 }9596 #[pallet::event]97 pub enum Event<T> {98 99 Executed(DispatchResult),100 }101102 #[pallet::config]103 pub trait Config: frame_system::Config + pallet_common::Config {104 type WeightInfo: weights::WeightInfo;105 type RuntimeEvent: IsType<<Self as frame_system::Config>::RuntimeEvent> + From<Event<Self>>;106 type RuntimeCall: Parameter107 + UnfilteredDispatchable<RuntimeOrigin = Self::RuntimeOrigin>108 + GetDispatchInfo;109 }110111 #[pallet::pallet]112 pub struct Pallet<T>(_);113114 #[pallet::call]115 impl<T: Config> Pallet<T> {116 117 118119 120 121 122 123 124 125 126 127 128 129 130 131 }132}133134#[derive(PartialEq)]135pub enum Parent<CrossAccountId> {136 137 User(CrossAccountId),138 139 TokenNotFound,140 141 MultipleOwners,142 143 Token(CollectionId, TokenId),144}145146impl<T: Config> Pallet<T> {147 148 149 150 151 152 153 pub fn find_parent(154 collection: CollectionId,155 token: TokenId,156 ) -> Result<Parent<T::CrossAccountId>, DispatchError> {157 158 let handle = match T::CollectionDispatch::dispatch(collection) {159 Ok(v) => v,160 Err(_) => return Ok(Parent::TokenNotFound),161 };162 let handle = handle.as_dyn();163164 Ok(match handle.token_owner(token) {165 Ok(owner) => match T::CrossTokenAddressMapping::address_to_token(&owner) {166 Some((collection, token)) => Parent::Token(collection, token),167 None => Parent::User(owner),168 },169 Err(TokenOwnerError::MultipleOwners) => Parent::MultipleOwners,170 Err(TokenOwnerError::NotFound) => Parent::TokenNotFound,171 })172 }173174 175 176 177 178 179 pub fn parent_chain(180 mut collection: CollectionId,181 mut token: TokenId,182 ) -> impl Iterator<Item = Result<Parent<T::CrossAccountId>, DispatchError>> {183 let mut finished = false;184 let mut visited = BTreeSet::new();185 visited.insert((collection, token));186 core::iter::from_fn(move || {187 if finished {188 return None;189 }190 let parent = Self::find_parent(collection, token);191 match parent {192 Ok(Parent::Token(new_collection, new_token)) => {193 collection = new_collection;194 token = new_token;195 if !visited.insert((new_collection, new_token)) {196 finished = true;197 return Some(Err(<Error<T>>::OuroborosDetected.into()));198 }199 }200 _ => finished = true,201 }202 Some(parent as Result<_, DispatchError>)203 })204 }205206 207 208 209 210 211 212 213 pub fn find_topmost_owner(214 collection: CollectionId,215 token: TokenId,216 budget: &dyn Budget,217 ) -> Result<Option<T::CrossAccountId>, DispatchError> {218 let owner = Self::parent_chain(collection, token)219 .take_while(|_| budget.consume())220 .find(|p| {221 matches!(222 p,223 Ok(Parent::User(_) | Parent::TokenNotFound | Parent::MultipleOwners)224 )225 })226 .ok_or(<Error<T>>::DepthLimit)??;227228 Ok(match owner {229 Parent::User(v) => Some(v),230 Parent::MultipleOwners => None,231 _ => fail!(<Error<T>>::TokenNotFound),232 })233 }234235 236 237 238 239 240 241 pub fn get_checked_topmost_owner(242 collection: CollectionId,243 token: TokenId,244 for_nest: Option<(CollectionId, TokenId)>,245 budget: &dyn Budget,246 ) -> Result<Option<T::CrossAccountId>, DispatchError> {247 248 if Some((collection, token)) == for_nest {249 return Err(<Error<T>>::OuroborosDetected.into());250 }251252 for parent in Self::parent_chain(collection, token).take_while(|_| budget.consume()) {253 match parent? {254 255 Parent::Token(collection, token) if Some((collection, token)) == for_nest => {256 return Err(<Error<T>>::OuroborosDetected.into())257 }258 259 Parent::User(user) => return Ok(Some(user)),260 Parent::TokenNotFound => return Err(<Error<T>>::TokenNotFound.into()),261 Parent::MultipleOwners => return Ok(None),262 263 Parent::Token(_, _) => {}264 }265 }266267 Err(<Error<T>>::DepthLimit.into())268 }269270 271 272 273 274 pub fn burn_item_recursively(275 from: T::CrossAccountId,276 collection: CollectionId,277 token: TokenId,278 self_budget: &dyn Budget,279 breadth_budget: &dyn Budget,280 ) -> DispatchResultWithPostInfo {281 let dispatch = T::CollectionDispatch::dispatch(collection)?;282 let dispatch = dispatch.as_dyn();283 dispatch.burn_item_recursively(from, token, self_budget, breadth_budget)284 }285286 287 288 289 290 291 292 293 pub fn check_indirectly_owned(294 user: T::CrossAccountId,295 collection: CollectionId,296 token: TokenId,297 for_nest: Option<(CollectionId, TokenId)>,298 budget: &dyn Budget,299 ) -> Result<bool, DispatchError> {300 let target_parent = match T::CrossTokenAddressMapping::address_to_token(&user) {301 Some((collection, token)) => match Self::find_topmost_owner(collection, token, budget)?302 {303 Some(topmost_owner) => topmost_owner,304 None => return Ok(false),305 },306 None => user,307 };308309 Self::get_checked_topmost_owner(collection, token, for_nest, budget).map(|indirect_owner| {310 indirect_owner.map_or(false, |indirect_owner| indirect_owner == target_parent)311 })312 }313314 315 316 317 318 319 320 pub fn check_nesting(321 from: T::CrossAccountId,322 under: &T::CrossAccountId,323 collection_id: CollectionId,324 token_id: TokenId,325 nesting_budget: &dyn Budget,326 ) -> DispatchResult {327 Self::try_exec_if_token(under, |collection, parent_id| {328 collection.check_nesting(from, (collection_id, token_id), parent_id, nesting_budget)329 })330 }331332 333 334 335 336 337 pub fn nest_if_sent_to_token(338 from: T::CrossAccountId,339 under: &T::CrossAccountId,340 collection_id: CollectionId,341 token_id: TokenId,342 nesting_budget: &dyn Budget,343 ) -> DispatchResult {344 Self::try_exec_if_token(under, |collection, parent_id| {345 collection.check_nesting(from, (collection_id, token_id), parent_id, nesting_budget)?;346347 collection.nest(parent_id, (collection_id, token_id));348349 Ok(())350 })351 }352353 354 355 356 pub fn nest_if_sent_to_token_unchecked(357 owner: &T::CrossAccountId,358 collection_id: CollectionId,359 token_id: TokenId,360 ) {361 Self::exec_if_token(owner, |collection, parent_id| {362 collection.nest(parent_id, (collection_id, token_id))363 });364 }365366 367 pub fn unnest_if_nested(368 owner: &T::CrossAccountId,369 collection_id: CollectionId,370 token_id: TokenId,371 ) {372 if let Err(e) = Self::try_exec_if_token(owner, |collection, parent_id| {373 collection.unnest(parent_id, (collection_id, token_id));374 Ok(())375 }) {376 log::warn!("unnest precondition failed: {e:?}")377 }378 }379380 381 382 fn exec_if_token(383 account: &T::CrossAccountId,384 action: impl FnOnce(&dyn CommonCollectionOperations<T>, TokenId),385 ) {386 Self::try_exec_if_token(account, |collection, id| {387 action(collection, id);388 Ok(())389 })390 .unwrap();391 }392393 394 395 fn try_exec_if_token(396 account: &T::CrossAccountId,397 action: impl FnOnce(&dyn CommonCollectionOperations<T>, TokenId) -> DispatchResult,398 ) -> DispatchResult {399 if is_collection(account.as_eth()) {400 fail!(<Error<T>>::CantNestTokenUnderCollection);401 }402 let Some((collection, token)) = T::CrossTokenAddressMapping::address_to_token(account)403 else {404 return Ok(());405 };406407 let dispatch = T::CollectionDispatch::dispatch(collection)?;408 let dispatch = dispatch.as_dyn();409410 action(dispatch, token)411 }412}