From 23e7e864f1133167a8f7fd2208c4f8d8aabb20ef Mon Sep 17 00:00:00 2001 From: kozyrevdev <73348153+kozyrevdev@users.noreply.github.com> Date: Thu, 21 Apr 2022 15:02:09 +0000 Subject: [PATCH] Merge pull request #337 from UniqueNetwork/CORE-178 feature/core-178 --- --- a/client/rpc/src/lib.rs +++ b/client/rpc/src/lib.rs @@ -119,6 +119,15 @@ ) -> Result>>; #[rpc(name = "unique_collectionStats")] fn collection_stats(&self, at: Option) -> Result; + + #[rpc(name = "unique_nextSponsored")] + fn next_sponsored( + &self, + collection: CollectionId, + account: CrossAccountId, + token: TokenId, + at: Option, + ) -> Result>; #[rpc(name = "unique_effectiveCollectionLimits")] fn effective_collection_limits( &self, @@ -228,5 +237,6 @@ pass_method!(last_token_id(collection: CollectionId) -> TokenId); pass_method!(collection_by_id(collection: CollectionId) -> Option>); pass_method!(collection_stats() -> CollectionStats); + pass_method!(next_sponsored(collection: CollectionId, account: CrossAccountId, token: TokenId) -> Option); pass_method!(effective_collection_limits(collection_id: CollectionId) -> Option); } --- a/pallets/unique/src/lib.rs +++ b/pallets/unique/src/lib.rs @@ -67,7 +67,7 @@ mod eth; mod sponsorship; -pub use sponsorship::UniqueSponsorshipHandler; +pub use sponsorship::{UniqueSponsorshipHandler, UniqueSponsorshipPredict}; pub use eth::sponsoring::UniqueEthSponsorshipHandler; pub use eth::UniqueErcSupport; @@ -82,6 +82,12 @@ pub mod weights; use weights::WeightInfo; +pub trait SponsorshipPredict { + fn predict(collection: CollectionId, account: T::CrossAccountId, token: TokenId) -> Option + where + u64: From<::BlockNumber>; +} + decl_error! { /// Error for non-fungible-token module. pub enum Error for Module { --- a/pallets/unique/src/sponsorship.rs +++ b/pallets/unique/src/sponsorship.rs @@ -29,6 +29,7 @@ CollectionId, FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT, MetaUpdatePermission, NFT_SPONSOR_TRANSFER_TIMEOUT, REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT, TokenId, }; +use sp_runtime::traits::Saturating; use pallet_common::{CollectionHandle}; use pallet_evm::account::CrossAccountId; @@ -301,3 +302,65 @@ } } } + +use crate::SponsorshipPredict; +use up_data_structs::SponsorshipState; +pub struct UniqueSponsorshipPredict(PhantomData); + +impl SponsorshipPredict for UniqueSponsorshipPredict +where + T: Config, +{ + fn predict(collection_id: CollectionId, who: T::CrossAccountId, token: TokenId) -> Option + where + u64: From<::BlockNumber>, + { + let collection = >::try_get(collection_id).ok()?; + let _ = collection.sponsorship.sponsor()?; + + // sponsor timeout + let block_number = >::block_number() as T::BlockNumber; + let limit = collection + .limits + .sponsor_transfer_timeout(match collection.mode { + CollectionMode::NFT => NFT_SPONSOR_TRANSFER_TIMEOUT, + CollectionMode::Fungible(_) => FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT, + CollectionMode::ReFungible => REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT, + }); + + let last_tx_block = match collection.mode { + CollectionMode::NFT => >::get(collection.id, token), + CollectionMode::Fungible(_) => { + >::get(collection.id, who.as_sub()) + } + CollectionMode::ReFungible => { + >::get((collection.id, token, who.as_sub())) + } + }; + + if let Some(last_tx_block) = last_tx_block { + return Some( + last_tx_block + .saturating_add(limit.into()) + .saturating_sub(block_number) + .into(), + ); + } + + let token_exists = match collection.mode { + CollectionMode::NFT => { + >::contains_key((collection.id, token)) + } + CollectionMode::Fungible(_) => true, + CollectionMode::ReFungible => { + >::contains_key((collection.id, token)) + } + }; + + if token_exists { + Some(0) + } else { + None + } + } +} --- a/primitives/rpc/src/lib.rs +++ b/primitives/rpc/src/lib.rs @@ -59,6 +59,7 @@ fn last_token_id(collection: CollectionId) -> Result; fn collection_by_id(collection: CollectionId) -> Result>>; fn collection_stats() -> Result; + fn next_sponsored(collection: CollectionId, account: CrossAccountId, token: TokenId) -> Result>; fn effective_collection_limits(collection_id: CollectionId) -> Result>; } } --- a/runtime/common/src/runtime_apis.rs +++ b/runtime/common/src/runtime_apis.rs @@ -69,6 +69,13 @@ fn collection_stats() -> Result { Ok(>::collection_stats()) } + fn next_sponsored(collection: CollectionId, account: CrossAccountId, token: TokenId) -> Result, DispatchError> { + Ok( as + pallet_unique::SponsorshipPredict>::predict( + collection, + account, + token)) + } fn effective_collection_limits(collection: CollectionId) -> Result, DispatchError> { Ok(>::effective_collection_limits(collection)) --- a/tests/src/interfaces/augment-api-rpc.ts +++ b/tests/src/interfaces/augment-api-rpc.ts @@ -623,6 +623,10 @@ * Get token variable metadata **/ variableMetadata: AugmentedRpc<(collection: u32 | AnyNumber | Uint8Array, tokenId: u32 | AnyNumber | Uint8Array, at?: Hash | string | Uint8Array) => Observable>; + /** + * nextSponsored transaction + **/ + nextSponsored: AugmentedRpc<(collection: u32 | AnyNumber | Uint8Array, account: AccountId | string | Uint8Array | { Substrate: any } | { Ethereum: any } | string | Uint8Array, tokenId: u32 | AnyNumber | Uint8Array, at?: Hash | string | Uint8Array) => Observable>>; }; web3: { /** --- a/tests/src/interfaces/unique/definitions.ts +++ b/tests/src/interfaces/unique/definitions.ts @@ -55,6 +55,7 @@ collectionById: fun('Get collection by specified id', [collectionParam], 'Option'), collectionStats: fun('Get collection stats', [], 'UpDataStructsCollectionStats'), allowed: fun('Check if user is allowed to use collection', [collectionParam, crossAccountParam()], 'bool'), + nextSponsored: fun('Get number of blocks when sponsored transaction is available', [collectionParam, crossAccountParam(), tokenParam], 'Option'), effectiveCollectionLimits: fun('Get effective collection limits', [collectionParam], 'Option'), }, }; --- /dev/null +++ b/tests/src/nextSponsoring.test.ts @@ -0,0 +1,110 @@ +// Copyright 2019-2022 Unique Network (Gibraltar) Ltd. +// This file is part of Unique Network. + +// Unique Network is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Unique Network is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Unique Network. If not, see . + +import {ApiPromise} from '@polkadot/api'; +import {IKeyringPair} from '@polkadot/types/types'; +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import privateKey from './substrate/privateKey'; +import {default as usingApi} from './substrate/substrate-api'; +import { + createCollectionExpectSuccess, + setCollectionSponsorExpectSuccess, + confirmSponsorshipExpectSuccess, + createItemExpectSuccess, + transferExpectSuccess, + normalizeAccountId, + getNextSponsored, +} from './util/helpers'; + +chai.use(chaiAsPromised); +const expect = chai.expect; + + + +describe('Integration Test getNextSponsored(collection_id, owner, item_id):', () => { + let alice: IKeyringPair; + let bob: IKeyringPair; + + before(async () => { + await usingApi(async () => { + alice = privateKey('//Alice'); + bob = privateKey('//Bob'); + }); + }); + + it('NFT', async () => { + await usingApi(async (api: ApiPromise) => { + + // Not existing collection + expect(await getNextSponsored(api, 0, normalizeAccountId(alice), 0)).to.be.equal(-1); + + const collectionId = await createCollectionExpectSuccess(); + const itemId = await createItemExpectSuccess(alice, collectionId, 'NFT', alice.address); + + // Check with Disabled sponsoring state + expect(await getNextSponsored(api, collectionId, normalizeAccountId(alice), itemId)).to.be.equal(-1); + await setCollectionSponsorExpectSuccess(collectionId, bob.address); + + // Check with Unconfirmed sponsoring state + expect(await getNextSponsored(api, collectionId, normalizeAccountId(alice), itemId)).to.be.equal(-1); + await confirmSponsorshipExpectSuccess(collectionId, '//Bob'); + + // Check with Confirmed sponsoring state + expect(await getNextSponsored(api, collectionId, normalizeAccountId(alice), itemId)).to.be.equal(0); + + // After transfer + await transferExpectSuccess(collectionId, itemId, alice, bob, 1); + expect(await getNextSponsored(api, collectionId, normalizeAccountId(alice), itemId)).to.be.equal(5); + + // Not existing token + expect(await getNextSponsored(api, collectionId, normalizeAccountId(alice), itemId+1)).to.be.equal(-1); + }); + }); + + it('Fungible', async () => { + await usingApi(async (api: ApiPromise) => { + + const createMode = 'Fungible'; + const funCollectionId = await createCollectionExpectSuccess({mode: {type: createMode, decimalPoints: 0}}); + await createItemExpectSuccess(alice, funCollectionId, createMode); + await setCollectionSponsorExpectSuccess(funCollectionId, bob.address); + await confirmSponsorshipExpectSuccess(funCollectionId, '//Bob'); + expect(await getNextSponsored(api, funCollectionId, normalizeAccountId(alice), 0)).to.be.equal(0); + + await transferExpectSuccess(funCollectionId, 0, alice, bob, 10, 'Fungible'); + expect(await getNextSponsored(api, funCollectionId, normalizeAccountId(alice), 0)).to.be.equal(5); + }); + }); + + it('ReFungible', async () => { + await usingApi(async (api: ApiPromise) => { + + const createMode = 'ReFungible'; + const refunCollectionId = await createCollectionExpectSuccess({mode: {type: createMode}}); + const refunItemId = await createItemExpectSuccess(alice, refunCollectionId, createMode); + await setCollectionSponsorExpectSuccess(refunCollectionId, bob.address); + await confirmSponsorshipExpectSuccess(refunCollectionId, '//Bob'); + expect(await getNextSponsored(api, refunCollectionId, normalizeAccountId(alice), refunItemId)).to.be.equal(0); + + await transferExpectSuccess(refunCollectionId, refunItemId, alice, bob, 10, 'ReFungible'); + expect(await getNextSponsored(api, refunCollectionId, normalizeAccountId(alice), refunItemId)).to.be.equal(5); + + // Not existing token + expect(await getNextSponsored(api, refunCollectionId, normalizeAccountId(alice), refunItemId+1)).to.be.equal(-1); + }); + }); +}); --- a/tests/src/util/helpers.ts +++ b/tests/src/util/helpers.ts @@ -608,6 +608,15 @@ }); } +export async function getNextSponsored( + api: ApiPromise, + collectionId: number, + account: string | CrossAccountId, + tokenId: number, +): Promise { + return Number((await api.rpc.unique.nextSponsored(collectionId, account, tokenId)).unwrapOr(-1)); +} + export async function toggleContractAllowlistExpectSuccess(sender: IKeyringPair, contractAddress: AccountId | string, value = true) { await usingApi(async (api) => { const tx = api.tx.unique.toggleContractAllowList(contractAddress, value); -- gitstuff