123456#![recursion_limit = "1024"]7#![cfg_attr(not(feature = "std"), no_std)]8#![allow(9 clippy::too_many_arguments,10 clippy::unnecessary_mut_passed,11 clippy::unused_unit12)]1314extern crate alloc;1516pub use serde::{Serialize, Deserialize};1718pub use frame_support::{19 construct_runtime, decl_module, decl_storage, decl_error,20 dispatch::DispatchResult,21 ensure, fail, parameter_types,22 traits::{23 ExistenceRequirement, Get, Imbalance, KeyOwnerProofSystem, OnUnbalanced, Randomness,24 IsSubType, WithdrawReasons,25 },26 weights::{27 constants::{BlockExecutionWeight, ExtrinsicBaseWeight, RocksDbWeight, WEIGHT_PER_SECOND},28 DispatchInfo, GetDispatchInfo, IdentityFee, Pays, PostDispatchInfo, Weight,29 WeightToFeePolynomial, DispatchClass,30 },31 StorageValue, transactional,32 pallet_prelude::DispatchResultWithPostInfo,33};34use scale_info::TypeInfo;35use frame_system::{self as system, ensure_signed};36use sp_runtime::{sp_std::prelude::Vec};37use nft_data_structs::{38 MAX_DECIMAL_POINTS, MAX_SPONSOR_TIMEOUT, MAX_TOKEN_OWNERSHIP, CUSTOM_DATA_LIMIT,39 VARIABLE_ON_CHAIN_SCHEMA_LIMIT, CONST_ON_CHAIN_SCHEMA_LIMIT, COLLECTION_ADMINS_LIMIT,40 OFFCHAIN_SCHEMA_LIMIT, FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT, REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,41 NFT_SPONSOR_TRANSFER_TIMEOUT, AccessMode, Collection, CreateItemData, CollectionLimits,42 CollectionId, CollectionMode, TokenId, SchemaVersion, SponsorshipState, MetaUpdatePermission,43};44use pallet_common::{45 account::CrossAccountId, CollectionHandle, IsAdmin, Pallet as PalletCommon,46 Error as CommonError, CommonWeightInfo, Allowlist,47};48use pallet_refungible::{Pallet as PalletRefungible, RefungibleHandle};49use pallet_fungible::{Pallet as PalletFungible, FungibleHandle};50use pallet_nonfungible::{Pallet as PalletNonfungible, NonfungibleHandle};5152#[cfg(test)]53mod mock;5455#[cfg(test)]56mod tests;5758mod eth;59mod sponsorship;60pub use sponsorship::NftSponsorshipHandler;61pub use eth::sponsoring::NftEthSponsorshipHandler;6263pub use eth::NftErcSupport;6465pub mod common;66use common::CommonWeights;67pub mod dispatch;68use dispatch::dispatch_call;6970#[cfg(feature = "runtime-benchmarks")]71mod benchmarking;72pub mod weights;73use weights::WeightInfo;7475decl_error! {76 77 pub enum Error for Module<T: Config> {78 79 CollectionDecimalPointLimitExceeded,80 81 ConfirmUnsetSponsorFail,82 83 EmptyArgument,84 85 CollectionLimitBoundsExceeded,86 87 OwnerPermissionsCantBeReverted,88 }89}90pub trait Config:91 system::Config92 + pallet_evm_coder_substrate::Config93 + pallet_common::Config94 + pallet_nonfungible::Config95 + pallet_refungible::Config96 + pallet_fungible::Config97 + Sized98 + TypeInfo99{100 101 type WeightInfo: WeightInfo;102}103104type SelfWeightOf<T> = <T as Config>::WeightInfo;105106107108109110111112113114115116117118119120121122123124125126127128decl_storage! {129 trait Store for Module<T: Config> as Nft {130131 132 133 ChainVersion: u64;134 135136 137 138 139 pub CreateItemBasket get(fn create_item_basket): map hasher(blake2_128_concat) (CollectionId, T::AccountId) => T::BlockNumber;140 141 pub NftTransferBasket get(fn nft_transfer_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => T::BlockNumber;142 143 pub FungibleTransferBasket get(fn fungible_transfer_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(twox_64_concat) T::AccountId => T::BlockNumber;144 145 pub ReFungibleTransferBasket get(fn refungible_transfer_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => T::BlockNumber;146 147148 149 150 pub VariableMetaDataBasket get(fn variable_meta_data_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => Option<T::BlockNumber> = None;151 }152}153154decl_module! {155 pub struct Module<T: Config> for enum Call156 where157 origin: T::Origin158 {159 const CollectionAdminsLimit: u64 = COLLECTION_ADMINS_LIMIT;160 type Error = Error<T>;161162 fn on_initialize(_now: T::BlockNumber) -> Weight {163 0164 }165166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 #[weight = <SelfWeightOf<T>>::create_collection()]183 #[transactional]184 pub fn create_collection(origin,185 collection_name: Vec<u16>,186 collection_description: Vec<u16>,187 token_prefix: Vec<u8>,188 mode: CollectionMode) -> DispatchResult {189190 191 let who = ensure_signed(origin)?;192193 194 let new_collection = Collection::<T> {195 owner: who.clone(),196 name: collection_name,197 mode: mode.clone(),198 mint_mode: false,199 access: AccessMode::Normal,200 description: collection_description,201 token_prefix,202 offchain_schema: Vec::new(),203 schema_version: SchemaVersion::ImageURL,204 sponsorship: SponsorshipState::Disabled,205 variable_on_chain_schema: Vec::new(),206 const_on_chain_schema: Vec::new(),207 limits: Default::default(),208 meta_update_permission: Default::default(),209 };210211 let _id = match mode {212 CollectionMode::NFT => {PalletNonfungible::init_collection(new_collection)?},213 CollectionMode::Fungible(decimal_points) => {214 215 ensure!(decimal_points <= MAX_DECIMAL_POINTS, Error::<T>::CollectionDecimalPointLimitExceeded);216 PalletFungible::init_collection(new_collection)?217 }218 CollectionMode::ReFungible => {219 PalletRefungible::init_collection(new_collection)?220 }221 };222223 Ok(())224 }225226 227 228 229 230 231 232 233 234 235 #[weight = <SelfWeightOf<T>>::destroy_collection()]236 #[transactional]237 pub fn destroy_collection(origin, collection_id: CollectionId) -> DispatchResult {238 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);239240 let collection = <CollectionHandle<T>>::try_get(collection_id)?;241 collection.check_is_owner(&sender)?;242243 244245 match collection.mode {246 CollectionMode::ReFungible => PalletRefungible::destroy_collection(RefungibleHandle::cast(collection), &sender)?,247 CollectionMode::Fungible(_) => PalletFungible::destroy_collection(FungibleHandle::cast(collection), &sender)?,248 CollectionMode::NFT => PalletNonfungible::destroy_collection(NonfungibleHandle::cast(collection), &sender)?,249 }250251 <NftTransferBasket<T>>::remove_prefix(collection_id, None);252 <FungibleTransferBasket<T>>::remove_prefix(collection_id, None);253 <ReFungibleTransferBasket<T>>::remove_prefix(collection_id, None);254255 <VariableMetaDataBasket<T>>::remove_prefix(collection_id, None);256257 Ok(())258 }259260 261 262 263 264 265 266 267 268 269 270 271 272 #[weight = <SelfWeightOf<T>>::add_to_white_list()]273 #[transactional]274 pub fn add_to_white_list(origin, collection_id: CollectionId, address: T::CrossAccountId) -> DispatchResult{275276 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);277 let collection = <CollectionHandle<T>>::try_get(collection_id)?;278279 <PalletCommon<T>>::toggle_allowlist(280 &collection,281 &sender,282 &address,283 true,284 )?;285286 Ok(())287 }288289 290 291 292 293 294 295 296 297 298 299 300 301 #[weight = <SelfWeightOf<T>>::remove_from_white_list()]302 #[transactional]303 pub fn remove_from_white_list(origin, collection_id: CollectionId, address: T::CrossAccountId) -> DispatchResult{304305 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);306 let collection = <CollectionHandle<T>>::try_get(collection_id)?;307308 <PalletCommon<T>>::toggle_allowlist(309 &collection,310 &sender,311 &address,312 false,313 )?;314315 Ok(())316 }317318 319 320 321 322 323 324 325 326 327 328 329 #[weight = <SelfWeightOf<T>>::set_public_access_mode()]330 #[transactional]331 pub fn set_public_access_mode(origin, collection_id: CollectionId, mode: AccessMode) -> DispatchResult332 {333 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);334335 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;336 target_collection.check_is_owner(&sender)?;337338 target_collection.access = mode;339 target_collection.save()340 }341342 343 344 345 346 347 348 349 350 351 352 353 354 355 #[weight = <SelfWeightOf<T>>::set_mint_permission()]356 #[transactional]357 pub fn set_mint_permission(origin, collection_id: CollectionId, mint_permission: bool) -> DispatchResult358 {359 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);360361 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;362 target_collection.check_is_owner(&sender)?;363364 target_collection.mint_mode = mint_permission;365 target_collection.save()366 }367368 369 370 371 372 373 374 375 376 377 378 379 #[weight = <SelfWeightOf<T>>::change_collection_owner()]380 #[transactional]381 pub fn change_collection_owner(origin, collection_id: CollectionId, new_owner: T::AccountId) -> DispatchResult {382383 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);384385 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;386 target_collection.check_is_owner(&sender)?;387388 target_collection.owner = new_owner;389 target_collection.save()390 }391392 393 394 395 396 397 398 399 400 401 402 403 404 405 #[weight = <SelfWeightOf<T>>::add_collection_admin()]406 #[transactional]407 pub fn add_collection_admin(origin, collection_id: CollectionId, new_admin_id: T::CrossAccountId) -> DispatchResult {408 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);409410 let collection = <CollectionHandle<T>>::try_get(collection_id)?;411 collection.check_is_owner_or_admin(&sender)?;412413 <IsAdmin<T>>::insert((collection_id, new_admin_id.as_sub()), true);414 Ok(())415 }416417 418 419 420 421 422 423 424 425 426 427 428 429 #[weight = <SelfWeightOf<T>>::remove_collection_admin()]430 #[transactional]431 pub fn remove_collection_admin(origin, collection_id: CollectionId, account_id: T::CrossAccountId) -> DispatchResult {432 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);433434 let collection = <CollectionHandle<T>>::try_get(collection_id)?;435 collection.check_is_owner_or_admin(&sender)?;436437 <IsAdmin<T>>::remove((collection_id, account_id.as_sub()));438 Ok(())439 }440441 442 443 444 445 446 447 448 449 450 #[weight = <SelfWeightOf<T>>::set_collection_sponsor()]451 #[transactional]452 pub fn set_collection_sponsor(origin, collection_id: CollectionId, new_sponsor: T::AccountId) -> DispatchResult {453 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);454455 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;456 target_collection.check_is_owner_or_admin(&sender)?;457458 target_collection.sponsorship = SponsorshipState::Unconfirmed(new_sponsor);459 target_collection.save()460 }461462 463 464 465 466 467 468 469 #[weight = <SelfWeightOf<T>>::confirm_sponsorship()]470 #[transactional]471 pub fn confirm_sponsorship(origin, collection_id: CollectionId) -> DispatchResult {472 let sender = ensure_signed(origin)?;473474 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;475 ensure!(476 target_collection.sponsorship.pending_sponsor() == Some(&sender),477 Error::<T>::ConfirmUnsetSponsorFail478 );479480 target_collection.sponsorship = SponsorshipState::Confirmed(sender);481 target_collection.save()482 }483484 485 486 487 488 489 490 491 492 493 #[weight = <SelfWeightOf<T>>::remove_collection_sponsor()]494 #[transactional]495 pub fn remove_collection_sponsor(origin, collection_id: CollectionId) -> DispatchResult {496 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);497498 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;499 target_collection.check_is_owner(&sender)?;500501 target_collection.sponsorship = SponsorshipState::Disabled;502 target_collection.save()503 }504505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 #[weight = <CommonWeights<T>>::create_item()]524 #[transactional]525 pub fn create_item(origin, collection_id: CollectionId, owner: T::CrossAccountId, data: CreateItemData) -> DispatchResultWithPostInfo {526 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);527528 dispatch_call::<T, _>(collection_id, |d| d.create_item(sender, owner, data))529 }530531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 #[weight = <CommonWeights<T>>::create_multiple_items(items_data.len() as u32)]550 #[transactional]551 pub fn create_multiple_items(origin, collection_id: CollectionId, owner: T::CrossAccountId, items_data: Vec<CreateItemData>) -> DispatchResultWithPostInfo {552 ensure!(!items_data.is_empty(), Error::<T>::EmptyArgument);553 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);554555 dispatch_call::<T, _>(collection_id, |d| d.create_multiple_items(sender, owner, items_data))556 }557558 559560 561 562 563 564 565 566 567 568 569 570 571 #[weight = <SelfWeightOf<T>>::set_transfers_enabled_flag()]572 #[transactional]573 pub fn set_transfers_enabled_flag(origin, collection_id: CollectionId, value: bool) -> DispatchResult {574 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);575 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;576 target_collection.check_is_owner(&sender)?;577578 579580 target_collection.limits.transfers_enabled = Some(value);581 target_collection.save()582 }583584 585 586 587 588 589 590 591 592 593 594 595 596 597 #[weight = <CommonWeights<T>>::burn_item()]598 #[transactional]599 pub fn burn_item(origin, collection_id: CollectionId, item_id: TokenId, value: u128) -> DispatchResultWithPostInfo {600 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);601602 dispatch_call::<T, _>(collection_id, |d| d.burn_item(sender, item_id, value))603 }604605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 #[weight = <CommonWeights<T>>::burn_from()]622 #[transactional]623 pub fn burn_from(origin, collection_id: CollectionId, from: T::CrossAccountId, item_id: TokenId, value: u128) -> DispatchResultWithPostInfo {624 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);625626 dispatch_call::<T, _>(collection_id, |d| d.burn_from(sender, from, item_id, value))627 }628629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 #[weight = <CommonWeights<T>>::transfer()]653 #[transactional]654 pub fn transfer(origin, recipient: T::CrossAccountId, collection_id: CollectionId, item_id: TokenId, value: u128) -> DispatchResultWithPostInfo {655 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);656657 dispatch_call::<T, _>(collection_id, |d| d.transfer(sender, recipient, item_id, value))658 }659660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 #[weight = <CommonWeights<T>>::approve()]676 #[transactional]677 pub fn approve(origin, spender: T::CrossAccountId, collection_id: CollectionId, item_id: TokenId, amount: u128) -> DispatchResultWithPostInfo {678 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);679680 dispatch_call::<T, _>(collection_id, |d| d.approve(sender, spender, item_id, amount))681 }682683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 #[weight = <CommonWeights<T>>::transfer_from()]703 #[transactional]704 pub fn transfer_from(origin, from: T::CrossAccountId, recipient: T::CrossAccountId, collection_id: CollectionId, item_id: TokenId, value: u128 ) -> DispatchResultWithPostInfo {705 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);706707 dispatch_call::<T, _>(collection_id, |d| d.transfer_from(sender, from, recipient, item_id, value))708 }709710 711 712 713 714 715 716 717 718 719 720 721 722 #[weight = <CommonWeights<T>>::set_variable_metadata(data.len() as u32)]723 #[transactional]724 pub fn set_variable_meta_data (725 origin,726 collection_id: CollectionId,727 item_id: TokenId,728 data: Vec<u8>729 ) -> DispatchResultWithPostInfo {730 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);731732 dispatch_call::<T, _>(collection_id, |d| d.set_variable_metadata(sender, item_id, data))733 }734735 736 737 738 739 740 741 742 743 744 745 746 #[weight = <SelfWeightOf<T>>::set_meta_update_permission_flag()]747 #[transactional]748 pub fn set_meta_update_permission_flag(origin, collection_id: CollectionId, value: MetaUpdatePermission) -> DispatchResult {749 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);750 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;751752 ensure!(753 target_collection.meta_update_permission != MetaUpdatePermission::None,754 <CommonError<T>>::MetadataFlagFrozen,755 );756 target_collection.check_is_owner(&sender)?;757758 target_collection.meta_update_permission = value;759760 target_collection.save()761 }762763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 #[weight = <SelfWeightOf<T>>::set_schema_version()]778 #[transactional]779 pub fn set_schema_version(780 origin,781 collection_id: CollectionId,782 version: SchemaVersion783 ) -> DispatchResult {784 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);785 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;786 target_collection.check_is_owner_or_admin(&sender)?;787 target_collection.schema_version = version;788 target_collection.save()789 }790791 792 793 794 795 796 797 798 799 800 801 802 803 #[weight = <SelfWeightOf<T>>::set_offchain_schema(schema.len() as u32)]804 #[transactional]805 pub fn set_offchain_schema(806 origin,807 collection_id: CollectionId,808 schema: Vec<u8>809 ) -> DispatchResult {810 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);811 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;812 target_collection.check_is_owner_or_admin(&sender)?;813814 815 ensure!(schema.len() as u32 <= OFFCHAIN_SCHEMA_LIMIT, "");816817 target_collection.offchain_schema = schema;818 target_collection.save()819 }820821 822 823 824 825 826 827 828 829 830 831 832 833 #[weight = <SelfWeightOf<T>>::set_const_on_chain_schema(schema.len() as u32)]834 #[transactional]835 pub fn set_const_on_chain_schema (836 origin,837 collection_id: CollectionId,838 schema: Vec<u8>839 ) -> DispatchResult {840 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);841 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;842 target_collection.check_is_owner_or_admin(&sender)?;843844 845 ensure!(schema.len() as u32 <= CONST_ON_CHAIN_SCHEMA_LIMIT, "");846847 target_collection.const_on_chain_schema = schema;848 target_collection.save()849 }850851 852 853 854 855 856 857 858 859 860 861 862 863 #[weight = <SelfWeightOf<T>>::set_const_on_chain_schema(schema.len() as u32)]864 #[transactional]865 pub fn set_variable_on_chain_schema (866 origin,867 collection_id: CollectionId,868 schema: Vec<u8>869 ) -> DispatchResult {870 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);871 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;872 target_collection.check_is_owner_or_admin(&sender)?;873874 875 ensure!(schema.len() as u32 <= VARIABLE_ON_CHAIN_SCHEMA_LIMIT, "");876877 target_collection.variable_on_chain_schema = schema;878 target_collection.save()879 }880881 #[weight = <SelfWeightOf<T>>::set_collection_limits()]882 #[transactional]883 pub fn set_collection_limits(884 origin,885 collection_id: CollectionId,886 new_limit: CollectionLimits,887 ) -> DispatchResult {888 let mut new_limit = new_limit;889 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);890 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;891 target_collection.check_is_owner(&sender)?;892 let old_limit = &target_collection.limits;893894 macro_rules! limit_default {895 ($old:ident, $new:ident, $($field:ident $(($arg:expr))? => $check:expr),* $(,)?) => {{896 $(897 if let Some($new) = $new.$field {898 let $old = $old.$field($($arg)?);899 let _ = $new;900 let _ = $old;901 $check902 } else {903 $new.$field = $old.$field904 }905 )*906 }};907 }908909 limit_default!(old_limit, new_limit,910 account_token_ownership_limit => ensure!(911 new_limit <= MAX_TOKEN_OWNERSHIP,912 <Error<T>>::CollectionLimitBoundsExceeded,913 ),914 sponsor_transfer_timeout(match target_collection.mode {915 CollectionMode::NFT => NFT_SPONSOR_TRANSFER_TIMEOUT,916 CollectionMode::Fungible(_) => FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,917 CollectionMode::ReFungible => REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,918 }) => ensure!(919 new_limit <= MAX_SPONSOR_TIMEOUT,920 <Error<T>>::CollectionLimitBoundsExceeded,921 ),922 sponsored_data_size => ensure!(923 new_limit <= CUSTOM_DATA_LIMIT,924 <Error<T>>::CollectionLimitBoundsExceeded,925 ),926 token_limit => ensure!(927 old_limit >= new_limit && new_limit > 0,928 <CommonError<T>>::CollectionTokenLimitExceeded929 ),930 owner_can_transfer => ensure!(931 old_limit || !new_limit,932 <Error<T>>::OwnerPermissionsCantBeReverted,933 ),934 owner_can_destroy => ensure!(935 old_limit || !new_limit,936 <Error<T>>::OwnerPermissionsCantBeReverted,937 ),938 sponsored_data_rate_limit => {},939 transfers_enabled => {},940 );941942 target_collection.limits = new_limit;943944 target_collection.save()945 }946 }947}948949950impl<T: Config> Pallet<T> {951 pub fn adminlist(collection: CollectionId) -> Vec<T::AccountId> {952 <IsAdmin<T>>::iter_prefix((collection,))953 .map(|(a, _)| a)954 .collect()955 }956 pub fn allowlist(collection: CollectionId) -> Vec<T::AccountId> {957 <Allowlist<T>>::iter_prefix((collection,))958 .map(|(a, _)| a)959 .collect()960 }961}