--- a/.github/workflows/notify.yml +++ b/.github/workflows/notify.yml @@ -6,8 +6,8 @@ build: runs-on: ubuntu-latest steps: - - uses: avkviring/telegram-github-action@v0.0.8 + - uses: avkviring/telegram-github-action@v0.0.13 env: telegram_to: ${{ secrets.TELEGRAM_TO }} telegram_token: ${{ secrets.TELEGRAM_TOKEN }} - event: ${{ toJson(github.event) }} \ No newline at end of file + event: ${{ toJson(github.event) }} --- a/pallets/nft/src/lib.rs +++ b/pallets/nft/src/lib.rs @@ -1098,9 +1098,20 @@ // Transfer permissions check let target_collection = >::get(collection_id); - ensure!(Self::is_item_owner(sender.clone(), collection_id, item_id) || - Self::is_owner_or_admin_permissions(collection_id, sender.clone()), - Error::::NoPermission); + let allowance_limit = if Self::is_owner_or_admin_permissions( + collection_id, + sender.clone(), + ) { + None + } else if let Some(amount) = Self::owned_amount( + sender.clone(), + collection_id, + item_id, + ) { + Some(amount) + } else { + fail!(Error::::NoPermission); + }; if target_collection.access == AccessMode::WhiteList { Self::check_white_list(collection_id, &sender)?; @@ -1112,6 +1123,9 @@ if allowance_exists { allowance += >::get(collection_id, (item_id, &sender, &spender)); } + if let Some(limit) = allowance_limit { + ensure!(limit >= allowance, Error::::TokenValueTooLow); + } >::insert(collection_id, (item_id, sender.clone(), spender.clone()), allowance); Ok(()) @@ -1883,6 +1897,36 @@ Ok(()) } + fn owned_amount( + subject: T::AccountId, + collection_id: CollectionId, + item_id: TokenId, + ) -> Option { + let target_collection = >::get(collection_id); + + match target_collection.mode { + CollectionMode::NFT => { + if >::get(collection_id, item_id).owner == subject { + return Some(1) + } + None + }, + CollectionMode::Fungible(_) => { + if >::contains_key(collection_id, &subject) { + return Some(>::get(collection_id, &subject) + .value); + } + None + }, + CollectionMode::ReFungible => >::get(collection_id, item_id) + .owner + .iter() + .find(|i| i.owner == subject) + .map(|i| i.fraction), + CollectionMode::Invalid => None, + } + } + fn is_item_owner(subject: T::AccountId, collection_id: CollectionId, item_id: TokenId) -> bool { let target_collection = >::get(collection_id); --- a/tests/src/approve.test.ts +++ b/tests/src/approve.test.ts @@ -2,8 +2,8 @@ // This file is subject to the terms and conditions defined in // file 'LICENSE', which is part of this source code package. // +import { IKeyringPair } from '@polkadot/types/types'; import { ApiPromise } from '@polkadot/api'; -import BN from 'bn.js'; import chai from 'chai'; import chaiAsPromised from 'chai-as-promised'; import privateKey from './substrate/privateKey'; @@ -12,66 +12,70 @@ approveExpectFail, approveExpectSuccess, createCollectionExpectSuccess, - createFungibleItemExpectSuccess, createItemExpectSuccess, destroyCollectionExpectSuccess, - transferFromExpectSuccess, - U128_MAX, + transferExpectSuccess, } from './util/helpers'; chai.use(chaiAsPromised); -const expect = chai.expect; -describe('Integration Test approve(spender, collection_id, item_id, amount):', () => { - it('Execute the extrinsic and check approvedList', async () => { - await usingApi(async (api: ApiPromise) => { - const Alice = privateKey('//Alice'); - const Bob = privateKey('//Bob'); - const nftCollectionId = await createCollectionExpectSuccess(); - // nft - const newNftTokenId = await createItemExpectSuccess(Alice, nftCollectionId, 'NFT'); - await approveExpectSuccess(nftCollectionId, newNftTokenId, Alice, Bob); - // fungible - const fungibleCollectionId = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}}); - const newFungibleTokenId = await createItemExpectSuccess(Alice, fungibleCollectionId, 'Fungible'); - await approveExpectSuccess(fungibleCollectionId, newFungibleTokenId, Alice, Bob); - // reFungible - const reFungibleCollectionId = - await createCollectionExpectSuccess({mode: {type: 'ReFungible'}}); - const newReFungibleTokenId = await createItemExpectSuccess(Alice, reFungibleCollectionId, 'ReFungible'); - await approveExpectSuccess(reFungibleCollectionId, newReFungibleTokenId, Alice, Bob); +let Alice: IKeyringPair; +let Bob: IKeyringPair; + +describe.only('Integration Test approve(spender, collection_id, item_id, amount):', () => { + before(async () => { + await usingApi(async () => { + Alice = privateKey('//Alice'); + Bob = privateKey('//Bob'); }); }); + it('Execute the extrinsic and check approvedList', async () => { + const nftCollectionId = await createCollectionExpectSuccess(); + // nft + const newNftTokenId = await createItemExpectSuccess(Alice, nftCollectionId, 'NFT'); + await approveExpectSuccess(nftCollectionId, newNftTokenId, Alice, Bob); + // fungible + const fungibleCollectionId = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}}); + const newFungibleTokenId = await createItemExpectSuccess(Alice, fungibleCollectionId, 'Fungible'); + await approveExpectSuccess(fungibleCollectionId, newFungibleTokenId, Alice, Bob); + // reFungible + const reFungibleCollectionId = + await createCollectionExpectSuccess({mode: {type: 'ReFungible'}}); + const newReFungibleTokenId = await createItemExpectSuccess(Alice, reFungibleCollectionId, 'ReFungible'); + await approveExpectSuccess(reFungibleCollectionId, newReFungibleTokenId, Alice, Bob); + }); + it('Remove approval by using 0 amount', async () => { - await usingApi(async (api: ApiPromise) => { - const Alice = privateKey('//Alice'); - const Bob = privateKey('//Bob'); - const nftCollectionId = await createCollectionExpectSuccess(); - // nft - const newNftTokenId = await createItemExpectSuccess(Alice, nftCollectionId, 'NFT'); - await approveExpectSuccess(nftCollectionId, newNftTokenId, Alice, Bob, 1); - await approveExpectSuccess(nftCollectionId, newNftTokenId, Alice, Bob, 0); - // fungible - const fungibleCollectionId = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}}); - const newFungibleTokenId = await createItemExpectSuccess(Alice, fungibleCollectionId, 'Fungible'); - await approveExpectSuccess(fungibleCollectionId, newFungibleTokenId, Alice, Bob, 1); - await approveExpectSuccess(fungibleCollectionId, newFungibleTokenId, Alice, Bob, 0); - // reFungible - const reFungibleCollectionId = - await createCollectionExpectSuccess({mode: {type: 'ReFungible'}}); - const newReFungibleTokenId = await createItemExpectSuccess(Alice, reFungibleCollectionId, 'ReFungible'); - await approveExpectSuccess(reFungibleCollectionId, newReFungibleTokenId, Alice, Bob, 1); - await approveExpectSuccess(reFungibleCollectionId, newReFungibleTokenId, Alice, Bob, 0); - }); + const nftCollectionId = await createCollectionExpectSuccess(); + // nft + const newNftTokenId = await createItemExpectSuccess(Alice, nftCollectionId, 'NFT'); + await approveExpectSuccess(nftCollectionId, newNftTokenId, Alice, Bob, 1); + await approveExpectSuccess(nftCollectionId, newNftTokenId, Alice, Bob, 0); + // fungible + const fungibleCollectionId = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}}); + const newFungibleTokenId = await createItemExpectSuccess(Alice, fungibleCollectionId, 'Fungible'); + await approveExpectSuccess(fungibleCollectionId, newFungibleTokenId, Alice, Bob, 1); + await approveExpectSuccess(fungibleCollectionId, newFungibleTokenId, Alice, Bob, 0); + // reFungible + const reFungibleCollectionId = + await createCollectionExpectSuccess({mode: {type: 'ReFungible'}}); + const newReFungibleTokenId = await createItemExpectSuccess(Alice, reFungibleCollectionId, 'ReFungible'); + await approveExpectSuccess(reFungibleCollectionId, newReFungibleTokenId, Alice, Bob, 1); + await approveExpectSuccess(reFungibleCollectionId, newReFungibleTokenId, Alice, Bob, 0); }); }); -describe('Negative Integration Test approve(spender, collection_id, item_id, amount):', () => { +describe.only('Negative Integration Test approve(spender, collection_id, item_id, amount):', () => { + before(async () => { + await usingApi(async (api) => { + Alice = privateKey('//Alice'); + Bob = privateKey('//Bob'); + }); + }); + it('Approve for a collection that does not exist', async () => { await usingApi(async (api: ApiPromise) => { - const Alice = privateKey('//Alice'); - const Bob = privateKey('//Bob'); // nft const nftCollectionCount = await api.query.nft.createdCollectionCount() as unknown as number; await approveExpectFail(nftCollectionCount + 1, 1, Alice, Bob); @@ -85,59 +89,71 @@ }); it('Approve for a collection that was destroyed', async () => { - await usingApi(async (api: ApiPromise) => { - const Alice = privateKey('//Alice'); - const Bob = privateKey('//Bob'); - // nft - const nftCollectionId = await createCollectionExpectSuccess(); - await destroyCollectionExpectSuccess(nftCollectionId); - await approveExpectFail(nftCollectionId, 1, Alice, Bob); - // fungible - const fungibleCollectionId = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}}); - await destroyCollectionExpectSuccess(fungibleCollectionId); - await approveExpectFail(fungibleCollectionId, 1, Alice, Bob); - // reFungible - const reFungibleCollectionId = - await createCollectionExpectSuccess({mode: {type: 'ReFungible'}}); - await destroyCollectionExpectSuccess(reFungibleCollectionId); - await approveExpectFail(reFungibleCollectionId, 1, Alice, Bob); - }); + // nft + const nftCollectionId = await createCollectionExpectSuccess(); + await destroyCollectionExpectSuccess(nftCollectionId); + await approveExpectFail(nftCollectionId, 1, Alice, Bob); + // fungible + const fungibleCollectionId = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}}); + await destroyCollectionExpectSuccess(fungibleCollectionId); + await approveExpectFail(fungibleCollectionId, 1, Alice, Bob); + // reFungible + const reFungibleCollectionId = + await createCollectionExpectSuccess({mode: {type: 'ReFungible'}}); + await destroyCollectionExpectSuccess(reFungibleCollectionId); + await approveExpectFail(reFungibleCollectionId, 1, Alice, Bob); }); it('Approve transfer of a token that does not exist', async () => { - await usingApi(async (api: ApiPromise) => { - const Alice = privateKey('//Alice'); - const Bob = privateKey('//Bob'); - // nft - const nftCollectionId = await createCollectionExpectSuccess(); - await approveExpectFail(nftCollectionId, 2, Alice, Bob); - // fungible - const fungibleCollectionId = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}}); - await approveExpectFail(fungibleCollectionId, 2, Alice, Bob); - // reFungible - const reFungibleCollectionId = - await createCollectionExpectSuccess({mode: {type: 'ReFungible'}}); - await approveExpectFail(reFungibleCollectionId, 2, Alice, Bob); - }); + // nft + const nftCollectionId = await createCollectionExpectSuccess(); + await approveExpectFail(nftCollectionId, 2, Alice, Bob); + // fungible + const fungibleCollectionId = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}}); + await approveExpectFail(fungibleCollectionId, 2, Alice, Bob); + // reFungible + const reFungibleCollectionId = + await createCollectionExpectSuccess({mode: {type: 'ReFungible'}}); + await approveExpectFail(reFungibleCollectionId, 2, Alice, Bob); }); it('Approve using the address that does not own the approved token', async () => { - await usingApi(async (api: ApiPromise) => { - const Alice = privateKey('//Alice'); - const Bob = privateKey('//Bob'); - const nftCollectionId = await createCollectionExpectSuccess(); - // nft - const newNftTokenId = await createItemExpectSuccess(Alice, nftCollectionId, 'NFT'); - await approveExpectFail(nftCollectionId, newNftTokenId, Bob, Alice); - // fungible - const fungibleCollectionId = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}}); - const newFungibleTokenId = await createItemExpectSuccess(Alice, fungibleCollectionId, 'Fungible'); - await approveExpectFail(fungibleCollectionId, newFungibleTokenId, Bob, Alice); - // reFungible - const reFungibleCollectionId = - await createCollectionExpectSuccess({mode: {type: 'ReFungible'}}); - const newReFungibleTokenId = await createItemExpectSuccess(Alice, reFungibleCollectionId, 'ReFungible'); - await approveExpectFail(reFungibleCollectionId, newReFungibleTokenId, Bob, Alice); - }); + const nftCollectionId = await createCollectionExpectSuccess(); + // nft + const newNftTokenId = await createItemExpectSuccess(Alice, nftCollectionId, 'NFT'); + await approveExpectFail(nftCollectionId, newNftTokenId, Bob, Alice); + // fungible + const fungibleCollectionId = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}}); + const newFungibleTokenId = await createItemExpectSuccess(Alice, fungibleCollectionId, 'Fungible'); + await approveExpectFail(fungibleCollectionId, newFungibleTokenId, Bob, Alice); + // reFungible + const reFungibleCollectionId = + await createCollectionExpectSuccess({mode: {type: 'ReFungible'}}); + const newReFungibleTokenId = await createItemExpectSuccess(Alice, reFungibleCollectionId, 'ReFungible'); + await approveExpectFail(reFungibleCollectionId, newReFungibleTokenId, Bob, Alice); + }); + + it('should fail if approved more NFTs than owned', async () => { + const nftCollectionId = await createCollectionExpectSuccess({ mode: { type: 'NFT' } }); + const newNftTokenId = await createItemExpectSuccess(Alice, nftCollectionId, 'NFT'); + await transferExpectSuccess(nftCollectionId, newNftTokenId, Alice, Bob, 1, 'NFT'); + await approveExpectSuccess(nftCollectionId, newNftTokenId, Bob, Alice); + await approveExpectFail(nftCollectionId, newNftTokenId, Bob, Alice); + }); + + it('should fail if approved more ReFungibles than owned', async () => { + const nftCollectionId = await createCollectionExpectSuccess({ mode: { type: 'ReFungible' } }); + const newNftTokenId = await createItemExpectSuccess(Alice, nftCollectionId, 'ReFungible'); + await transferExpectSuccess(nftCollectionId, newNftTokenId, Alice, Bob, 100, 'ReFungible'); + await approveExpectSuccess(nftCollectionId, newNftTokenId, Bob, Alice, 100); + await approveExpectFail(nftCollectionId, newNftTokenId, Bob, Alice, 1); + }); + + it('should fail if approved more Fungibles than owned', async () => { + const nftCollectionId = await createCollectionExpectSuccess({ mode: { type: 'Fungible', decimalPoints: 0 } }); + const newNftTokenId = await createItemExpectSuccess(Alice, nftCollectionId, 'Fungible'); + await transferExpectSuccess(nftCollectionId, newNftTokenId, Alice, Bob, 10, 'Fungible'); + await approveExpectSuccess(nftCollectionId, newNftTokenId, Bob, Alice, 10); + await approveExpectFail(nftCollectionId, newNftTokenId, Bob, Alice, 1); }); });