difftreelog
Merge pull request #841 from UniqueNetwork/tests/generalization
in: master
Nesting to non-existing tokens
11 files changed
Cargo.lockdiffbeforeafterboth6754 "frame-benchmarking",6754 "frame-benchmarking",6755 "frame-support",6755 "frame-support",6756 "frame-system",6756 "frame-system",6757 "log",6757 "pallet-common",6758 "pallet-common",6758 "pallet-evm",6759 "pallet-evm",6759 "parity-scale-codec 3.2.1",6760 "parity-scale-codec 3.2.1",pallets/nonfungible/src/lib.rsdiffbeforeafterboth1332 let permissive = nesting.permissive;1332 let permissive = nesting.permissive;133313331334 if permissive {1334 if permissive {1335 // Pass1335 ensure!(1336 <TokenData<T>>::contains_key((handle.id, under)),1337 <CommonError<T>>::TokenNotFound1338 );1336 } else if nesting.token_owner1339 } else if nesting.token_owner1337 && <PalletStructure<T>>::check_indirectly_owned(1340 && <PalletStructure<T>>::check_indirectly_owned(1338 sender.clone(),1341 sender.clone(),1341 Some(from),1344 Some(from),1342 nesting_budget,1345 nesting_budget,1343 )? {1346 )? {1344 // Pass1347 // Pass, token existence is checked in `check_indirectly_owned`1345 } else if nesting.collection_admin && handle.is_owner_or_admin(&sender) {1348 } else if nesting.collection_admin && handle.is_owner_or_admin(&sender) {1346 // Pass1349 ensure!(1350 <TokenData<T>>::contains_key((handle.id, under)),1351 <CommonError<T>>::TokenNotFound1352 );1347 } else {1353 } else {1348 fail!(<CommonError<T>>::UserIsNotAllowedToNest);1354 fail!(<CommonError<T>>::UserIsNotAllowedToNest);1349 }1355 }pallets/structure/Cargo.tomldiffbeforeafterboth17] }17] }18up-data-structs = { path = "../../primitives/data-structs", default-features = false }18up-data-structs = { path = "../../primitives/data-structs", default-features = false }19pallet-evm = { default-features = false, git = "https://github.com/uniquenetwork/frontier", branch = "unique-polkadot-v0.9.36" }19pallet-evm = { default-features = false, git = "https://github.com/uniquenetwork/frontier", branch = "unique-polkadot-v0.9.36" }20log = { version = "0.4.17", default-features = false }202121[features]22[features]22default = ["std"]23default = ["std"]pallets/structure/src/lib.rsdiffbeforeafterboth54#![cfg_attr(not(feature = "std"), no_std)]54#![cfg_attr(not(feature = "std"), no_std)]555556use pallet_common::CommonCollectionOperations;56use pallet_common::CommonCollectionOperations;57use pallet_common::{erc::CrossAccountId, eth::is_collection};57use sp_std::collections::btree_set::BTreeSet;58use sp_std::collections::btree_set::BTreeSet;585959use frame_support::dispatch::{DispatchError, DispatchResult, DispatchResultWithPostInfo};60use frame_support::dispatch::{DispatchError, DispatchResult, DispatchResultWithPostInfo};86 BreadthLimit,87 BreadthLimit,87 /// Couldn't find the token owner that is itself a token.88 /// Couldn't find the token owner that is itself a token.88 TokenNotFound,89 TokenNotFound,90 /// Tried to nest token under collection contract address, instead of token address91 CantNestTokenUnderCollection,89 }92 }909391 #[pallet::event]94 #[pallet::event]302 token_id: TokenId,305 token_id: TokenId,303 nesting_budget: &dyn Budget,306 nesting_budget: &dyn Budget,304 ) -> DispatchResult {307 ) -> DispatchResult {305 Self::try_exec_if_owner_is_valid_nft(under, |collection, parent_id| {308 Self::try_exec_if_token(under, |collection, parent_id| {306 collection.check_nesting(from, (collection_id, token_id), parent_id, nesting_budget)309 collection.check_nesting(from, (collection_id, token_id), parent_id, nesting_budget)307 })310 })308 }311 }319 token_id: TokenId,322 token_id: TokenId,320 nesting_budget: &dyn Budget,323 nesting_budget: &dyn Budget,321 ) -> DispatchResult {324 ) -> DispatchResult {322 Self::try_exec_if_owner_is_valid_nft(under, |collection, parent_id| {325 Self::try_exec_if_token(under, |collection, parent_id| {323 collection.check_nesting(from, (collection_id, token_id), parent_id, nesting_budget)?;326 collection.check_nesting(from, (collection_id, token_id), parent_id, nesting_budget)?;324327325 collection.nest(parent_id, (collection_id, token_id));328 collection.nest(parent_id, (collection_id, token_id));336 collection_id: CollectionId,339 collection_id: CollectionId,337 token_id: TokenId,340 token_id: TokenId,338 ) {341 ) {339 Self::exec_if_owner_is_valid_nft(owner, |collection, parent_id| {342 Self::exec_if_token(owner, |collection, parent_id| {340 collection.nest(parent_id, (collection_id, token_id))343 collection.nest(parent_id, (collection_id, token_id))341 });344 });342 }345 }347 collection_id: CollectionId,350 collection_id: CollectionId,348 token_id: TokenId,351 token_id: TokenId,349 ) {352 ) {350 Self::exec_if_owner_is_valid_nft(owner, |collection, parent_id| {353 if let Err(e) = Self::try_exec_if_token(owner, |collection, parent_id| {351 collection.unnest(parent_id, (collection_id, token_id))354 collection.unnest(parent_id, (collection_id, token_id));355 Ok(())352 });356 }) {357 log::warn!("unnest precondition failed: {e:?}")358 }353 }359 }354360361 /// # Panics362 /// If [`Self::try_exec_if_token`] fails355 fn exec_if_owner_is_valid_nft(363 fn exec_if_token(356 account: &T::CrossAccountId,364 account: &T::CrossAccountId,357 action: impl FnOnce(&dyn CommonCollectionOperations<T>, TokenId),365 action: impl FnOnce(&dyn CommonCollectionOperations<T>, TokenId),358 ) {366 ) {359 Self::try_exec_if_owner_is_valid_nft(account, |collection, id| {367 Self::try_exec_if_token(account, |collection, id| {360 action(collection, id);368 action(collection, id);361 Ok(())369 Ok(())362 })370 })363 .unwrap();371 .unwrap();364 }372 }365373374 /// If `account` is a token address, execute `action` providing found collection as an argument375 /// Token may not exist, it is expected it will be checked in the callback.366 fn try_exec_if_owner_is_valid_nft(376 fn try_exec_if_token(367 account: &T::CrossAccountId,377 account: &T::CrossAccountId,368 action: impl FnOnce(&dyn CommonCollectionOperations<T>, TokenId) -> DispatchResult,378 action: impl FnOnce(&dyn CommonCollectionOperations<T>, TokenId) -> DispatchResult,369 ) -> DispatchResult {379 ) -> DispatchResult {380 if is_collection(&account.as_eth()) {381 fail!(<Error<T>>::CantNestTokenUnderCollection);382 }370 let account = T::CrossTokenAddressMapping::address_to_token(account);383 let Some((collection, token)) = T::CrossTokenAddressMapping::address_to_token(account) else {371372 if account.is_none() {373 return Ok(());384 return Ok(())374 }385 };375376 let account = account.unwrap();377386378 let handle = <CollectionHandle<T>>::try_get(account.0);387 let handle = <CollectionHandle<T>>::try_get(collection)?;379380 if handle.is_err() {381 return Ok(());382 }383384 let handle = handle.unwrap();385388386 let dispatch = T::CollectionDispatch::dispatch(handle);389 let dispatch = T::CollectionDispatch::dispatch(handle);387 let dispatch = dispatch.as_dyn();390 let dispatch = dispatch.as_dyn();388391389 action(dispatch, account.1)392 action(dispatch, token)390 }393 }391}394}392395tests/package.jsondiffbeforeafterboth100 "testFT": "mocha --timeout 9999999 -r ts-node/register ./**/fungible.test.ts",100 "testFT": "mocha --timeout 9999999 -r ts-node/register ./**/fungible.test.ts",101 "testEthFT": "mocha --timeout 9999999 -r ts-node/register ./**/eth/fungible.test.ts",101 "testEthFT": "mocha --timeout 9999999 -r ts-node/register ./**/eth/fungible.test.ts",102 "testRPC": "mocha --timeout 9999999 -r ts-node/register ./**/rpc.test.ts",102 "testRPC": "mocha --timeout 9999999 -r ts-node/register ./**/rpc.test.ts",103 "testPromotion": "yarn setup && mocha --timeout 9999999 -r ts-node/register ./**/app-promotion.*test.ts",103 "testPromotion": "yarn setup && mocha --timeout 9999999 -r ts-node/register ./**/appPromotion/*test.ts",104 "testXcmUnique": "RUN_XCM_TESTS=1 mocha --timeout 9999999 -r ts-node/register ./**/xcm/xcmUnique.test.ts",104 "testXcmUnique": "RUN_XCM_TESTS=1 mocha --timeout 9999999 -r ts-node/register ./**/xcm/xcmUnique.test.ts",105 "testXcmQuartz": "RUN_XCM_TESTS=1 mocha --timeout 9999999 -r ts-node/register ./**/xcm/xcmQuartz.test.ts",105 "testXcmQuartz": "RUN_XCM_TESTS=1 mocha --timeout 9999999 -r ts-node/register ./**/xcm/xcmQuartz.test.ts",106 "testXcmOpal": "RUN_XCM_TESTS=1 mocha --timeout 9999999 -r ts-node/register ./**/xcm/xcmOpal.test.ts",106 "testXcmOpal": "RUN_XCM_TESTS=1 mocha --timeout 9999999 -r ts-node/register ./**/xcm/xcmOpal.test.ts",tests/src/app-promotion.seqtest.tsdiffbeforeafterbothno changes
tests/src/app-promotion.test.tsdiffbeforeafterbothno changes
tests/src/sub/appPromotion/appPromotion.seqtest.tsdiffbeforeafterbothno changes
tests/src/sub/appPromotion/appPromotion.test.tsdiffbeforeafterbothno changes
tests/src/sub/nesting/common.test.tsdiffbeforeafterbothno changes
tests/src/util/playgrounds/unique.tsdiffbeforeafterboth1391 return tokenData;1391 return tokenData;1392 }1392 }13931394 /**1395 * Get token's owner1396 * @param collectionId ID of collection1397 * @param tokenId ID of token1398 * @param blockHashAt optionally query the data at the block with this hash1399 * @example getTokenOwner(10, 5);1400 * @returns Address in CrossAccountId format, e.g. {Substrate: "5DnSF6RRjwteE3BrCj..."}1401 */1402 async getTokenOwner(collectionId: number, tokenId: number, blockHashAt?: string): Promise<CrossAccountId> {1403 let owner;1404 if (typeof blockHashAt === 'undefined') {1405 owner = await this.helper.callRpc('api.rpc.unique.tokenOwner', [collectionId, tokenId]);1406 } else {1407 owner = await this.helper.callRpc('api.rpc.unique.tokenOwner', [collectionId, tokenId, blockHashAt]);1408 }1409 return CrossAccountId.fromLowerCaseKeys(owner.toJSON());1410 }14111412 /**1413 * Recursively find the address that owns the token1414 * @param collectionId ID of collection1415 * @param tokenId ID of token1416 * @param blockHashAt1417 * @example getTokenTopmostOwner(10, 5);1418 * @returns address in CrossAccountId format, e.g. {Substrate: "5DyN4Y92vZCjv38fg..."}1419 */1420 async getTokenTopmostOwner(collectionId: number, tokenId: number, blockHashAt?: string): Promise<CrossAccountId | null> {1421 let owner;1422 if (typeof blockHashAt === 'undefined') {1423 owner = await this.helper.callRpc('api.rpc.unique.topmostTokenOwner', [collectionId, tokenId]);1424 } else {1425 owner = await this.helper.callRpc('api.rpc.unique.topmostTokenOwner', [collectionId, tokenId, blockHashAt]);1426 }14271428 if (owner === null) return null;14291430 return owner.toHuman();1431 }14321433 /**1434 * Nest one token into another1435 * @param signer keyring of signer1436 * @param tokenObj token to be nested1437 * @param rootTokenObj token to be parent1438 * @example nestToken(aliceKeyring, {collectionId: 10, tokenId: 5}, {collectionId: 10, tokenId: 4});1439 * @returns ```true``` if extrinsic success, otherwise ```false```1440 */1441 async nestToken(signer: TSigner, tokenObj: IToken, rootTokenObj: IToken): Promise<boolean> {1442 const rootTokenAddress = this.helper.util.getTokenAccount(rootTokenObj);1443 const result = await this.transferToken(signer, tokenObj.collectionId, tokenObj.tokenId, rootTokenAddress);1444 if(!result) {1445 throw Error('Unable to nest token!');1446 }1447 return result;1448 }14491450 /**1451 * Remove token from nested state1452 * @param signer keyring of signer1453 * @param tokenObj token to unnest1454 * @param rootTokenObj parent of a token1455 * @param toAddressObj address of a new token owner1456 * @example unnestToken(aliceKeyring, {collectionId: 10, tokenId: 5}, {collectionId: 10, tokenId: 4}, {Substrate: "5DyN4Y92vZCjv38fg..."});1457 * @returns ```true``` if extrinsic success, otherwise ```false```1458 */1459 async unnestToken(signer: TSigner, tokenObj: IToken, rootTokenObj: IToken, toAddressObj: ICrossAccountId): Promise<boolean> {1460 const rootTokenAddress = this.helper.util.getTokenAccount(rootTokenObj);1461 const result = await this.transferTokenFrom(signer, tokenObj.collectionId, tokenObj.tokenId, rootTokenAddress, toAddressObj);1462 if(!result) {1463 throw Error('Unable to unnest token!');1464 }1465 return result;1466 }139314671394 /**1468 /**1395 * Set permissions to change token properties1469 * Set permissions to change token properties1557 return new UniqueNFToken(tokenId, this.getCollectionObject(collectionId));1631 return new UniqueNFToken(tokenId, this.getCollectionObject(collectionId));1558 }1632 }15591560 /**1561 * Get token's owner1562 * @param collectionId ID of collection1563 * @param tokenId ID of token1564 * @param blockHashAt optionally query the data at the block with this hash1565 * @example getTokenOwner(10, 5);1566 * @returns Address in CrossAccountId format, e.g. {Substrate: "5DnSF6RRjwteE3BrCj..."}1567 */1568 async getTokenOwner(collectionId: number, tokenId: number, blockHashAt?: string): Promise<CrossAccountId> {1569 let owner;1570 if (typeof blockHashAt === 'undefined') {1571 owner = await this.helper.callRpc('api.rpc.unique.tokenOwner', [collectionId, tokenId]);1572 } else {1573 owner = await this.helper.callRpc('api.rpc.unique.tokenOwner', [collectionId, tokenId, blockHashAt]);1574 }1575 return CrossAccountId.fromLowerCaseKeys(owner.toJSON());1576 }157716331578 /**1634 /**1579 * Is token approved to transfer1635 * Is token approved to transfer1616 return await super.transferTokenFrom(signer, collectionId, tokenId, fromAddressObj, toAddressObj, 1n);1672 return await super.transferTokenFrom(signer, collectionId, tokenId, fromAddressObj, toAddressObj, 1n);1617 }1673 }16181619 /**1620 * Recursively find the address that owns the token1621 * @param collectionId ID of collection1622 * @param tokenId ID of token1623 * @param blockHashAt1624 * @example getTokenTopmostOwner(10, 5);1625 * @returns address in CrossAccountId format, e.g. {Substrate: "5DyN4Y92vZCjv38fg..."}1626 */1627 async getTokenTopmostOwner(collectionId: number, tokenId: number, blockHashAt?: string): Promise<CrossAccountId | null> {1628 let owner;1629 if (typeof blockHashAt === 'undefined') {1630 owner = await this.helper.callRpc('api.rpc.unique.topmostTokenOwner', [collectionId, tokenId]);1631 } else {1632 owner = await this.helper.callRpc('api.rpc.unique.topmostTokenOwner', [collectionId, tokenId, blockHashAt]);1633 }16341635 if (owner === null) return null;16361637 return owner.toHuman();1638 }163916741640 /**1675 /**1641 * Get tokens nested in the provided token1676 * Get tokens nested in the provided token1658 });1693 });1659 }1694 }16601661 /**1662 * Nest one token into another1663 * @param signer keyring of signer1664 * @param tokenObj token to be nested1665 * @param rootTokenObj token to be parent1666 * @example nestToken(aliceKeyring, {collectionId: 10, tokenId: 5}, {collectionId: 10, tokenId: 4});1667 * @returns ```true``` if extrinsic success, otherwise ```false```1668 */1669 async nestToken(signer: TSigner, tokenObj: IToken, rootTokenObj: IToken): Promise<boolean> {1670 const rootTokenAddress = this.helper.util.getTokenAccount(rootTokenObj);1671 const result = await this.transferToken(signer, tokenObj.collectionId, tokenObj.tokenId, rootTokenAddress);1672 if(!result) {1673 throw Error('Unable to nest token!');1674 }1675 return result;1676 }16771678 /**1679 * Remove token from nested state1680 * @param signer keyring of signer1681 * @param tokenObj token to unnest1682 * @param rootTokenObj parent of a token1683 * @param toAddressObj address of a new token owner1684 * @example unnestToken(aliceKeyring, {collectionId: 10, tokenId: 5}, {collectionId: 10, tokenId: 4}, {Substrate: "5DyN4Y92vZCjv38fg..."});1685 * @returns ```true``` if extrinsic success, otherwise ```false```1686 */1687 async unnestToken(signer: TSigner, tokenObj: IToken, rootTokenObj: IToken, toAddressObj: ICrossAccountId): Promise<boolean> {1688 const rootTokenAddress = this.helper.util.getTokenAccount(rootTokenObj);1689 const result = await this.transferTokenFrom(signer, tokenObj.collectionId, tokenObj.tokenId, rootTokenAddress, toAddressObj);1690 if(!result) {1691 throw Error('Unable to unnest token!');1692 }1693 return result;1694 }169516951696 /**1696 /**1697 * Mint new collection1697 * Mint new collection3531 return await this.helper.rft.getToken(this.collectionId, tokenId, [], blockHashAt);3531 return await this.helper.rft.getToken(this.collectionId, tokenId, [], blockHashAt);3532 }3532 }35333534 async getTokenOwner(tokenId: number, blockHashAt?: string) {3535 return await this.helper.rft.getTokenOwner(this.collectionId, tokenId, blockHashAt);3536 }353335373534 async getTokensByAddress(addressObj: ICrossAccountId) {3538 async getTokensByAddress(addressObj: ICrossAccountId) {3535 return await this.helper.rft.getTokensByAddress(this.collectionId, addressObj);3539 return await this.helper.rft.getTokensByAddress(this.collectionId, addressObj);3539 return await this.helper.rft.getTokenTop10Owners(this.collectionId, tokenId);3543 return await this.helper.rft.getTokenTop10Owners(this.collectionId, tokenId);3540 }3544 }35453546 async getTokenTopmostOwner(tokenId: number, blockHashAt?: string) {3547 return await this.helper.rft.getTokenTopmostOwner(this.collectionId, tokenId, blockHashAt);3548 }354135493542 async getTokenBalance(tokenId: number, addressObj: ICrossAccountId) {3550 async getTokenBalance(tokenId: number, addressObj: ICrossAccountId) {3543 return await this.helper.rft.getTokenBalance(this.collectionId, tokenId, addressObj);3551 return await this.helper.rft.getTokenBalance(this.collectionId, tokenId, addressObj);3610 return await this.helper.rft.setTokenPropertyPermissions(signer, this.collectionId, permissions);3618 return await this.helper.rft.setTokenPropertyPermissions(signer, this.collectionId, permissions);3611 }3619 }36203621 async nestToken(signer: TSigner, tokenId: number, toTokenObj: IToken) {3622 return await this.helper.rft.nestToken(signer, {collectionId: this.collectionId, tokenId}, toTokenObj);3623 }36243625 async unnestToken(signer: TSigner, tokenId: number, fromTokenObj: IToken, toAddressObj: ICrossAccountId) {3626 return await this.helper.rft.unnestToken(signer, {collectionId: this.collectionId, tokenId}, fromTokenObj, toAddressObj);3627 }361236283613 scheduleAt<T extends UniqueHelper>(3629 scheduleAt<T extends UniqueHelper>(3614 executionBlockNumber: number,3630 executionBlockNumber: number,3849 return await this.collection.getToken(this.tokenId, blockHashAt);3865 return await this.collection.getToken(this.tokenId, blockHashAt);3850 }3866 }38673868 async getOwner(blockHashAt?: string) {3869 return await this.collection.getTokenOwner(this.tokenId, blockHashAt);3870 }385138713852 async getTop10Owners() {3872 async getTop10Owners() {3853 return await this.collection.getTop10TokenOwners(this.tokenId);3873 return await this.collection.getTop10TokenOwners(this.tokenId);3854 }3874 }38753876 async getTopmostOwner(blockHashAt?: string) {3877 return await this.collection.getTokenTopmostOwner(this.tokenId, blockHashAt);3878 }38793880 async nest(signer: TSigner, toTokenObj: IToken) {3881 return await this.collection.nestToken(signer, this.tokenId, toTokenObj);3882 }38833884 async unnest(signer: TSigner, fromTokenObj: IToken, toAddressObj: ICrossAccountId) {3885 return await this.collection.unnestToken(signer, this.tokenId, fromTokenObj, toAddressObj);3886 }385538873856 async getBalance(addressObj: ICrossAccountId) {3888 async getBalance(addressObj: ICrossAccountId) {3857 return await this.collection.getTokenBalance(this.tokenId, addressObj);3889 return await this.collection.getTokenBalance(this.tokenId, addressObj);