--- a/pallets/nft-charge-transaction/src/lib.rs +++ b/pallets/nft-charge-transaction/src/lib.rs @@ -135,6 +135,13 @@ .map(|i| (fee, i)); } + // check errors + let error = >::check_error(who, call); + match error { + Err(error) => return Err(error), + Ok(error) => {} + }; + // Determine who is paying transaction fee based on ecnomic model // Parse call to extract collection ID and access collection sponsor let sponsor = >::withdraw_type(who, call); --- /dev/null +++ b/pallets/nft-transaction-payment/README.md @@ -0,0 +1,13 @@ +# Nft Transaction Payment + +## Overview + +A module containing the sponsoring logic for paying for sponsored collections + +**NOTE:** The scheduled calls will be dispatched with the default filter +for the origin: namely `frame_system::Config::BaseCallFilter` for all origin +except root which will get no filter. And not the filter contained in origin +use to call `fn schedule`. + +If a call is scheduled using proxy or whatever mecanism which adds filter, +then those filter will not be used when dispatching the schedule call. --- a/pallets/nft-transaction-payment/src/lib.rs +++ b/pallets/nft-transaction-payment/src/lib.rs @@ -31,6 +31,9 @@ traits::{ Hash, Dispatchable, }, + transaction_validity::{ + InvalidTransaction, TransactionValidityError, + }, }; use pallet_contracts::chain_extension::UncheckedFrom; use sp_std::prelude::*; @@ -85,6 +88,36 @@ impl Module { + pub fn check_error( + who: &T::AccountId, + call: &T::Call + ) -> Result where + T::Call: Dispatchable, + T::Call: IsSubType>, + T::Call: IsSubType>, + T::AccountId: AsRef<[u8]>, + T::AccountId: UncheckedFrom + { + + match IsSubType::>::is_sub_type(call) { + Some(pallet_contracts::Call::call(dest, _value, _gas_limit, _data)) => { + + let called_contract: T::AccountId = T::Lookup::lookup((*dest).clone()).unwrap_or(T::AccountId::default()); + + let owned_contract = pallet_nft::ContractOwner::::get(called_contract.clone()).as_ref() == Some(who); + let white_list_enabled = pallet_nft::ContractWhiteListEnabled::::contains_key(called_contract.clone()); + + if !owned_contract && white_list_enabled { + if !pallet_nft::ContractWhiteList::::contains_key(called_contract.clone(), who) { + return Err(InvalidTransaction::Call.into()); + } + } + Ok(true) + }, + _ => { Ok(true) }, + } + } + pub fn withdraw_type( who: &T::AccountId, call: &T::Call @@ -366,160 +399,4 @@ None } -} - -// type BalanceOf = <::OnChargeTransaction as pallet_transaction_payment::OnChargeTransaction>::Balance; - -// /// Require the transactor pay for themselves and maybe include a tip to gain additional priority -// /// in the queue. -// #[derive(Encode, Decode, Clone, Eq, PartialEq)] -// pub struct ChargeTransactionPayment(#[codec(compact)] BalanceOf); - -// impl sp_std::fmt::Debug -// for ChargeTransactionPayment -// { -// #[cfg(feature = "std")] -// fn fmt(&self, f: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result { -// write!(f, "ChargeTransactionPayment<{:?}>", self.0) -// } -// #[cfg(not(feature = "std"))] -// fn fmt(&self, _: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result { -// Ok(()) -// } -// } - -// impl ChargeTransactionPayment -// where -// T::Call: Dispatchable + IsSubType> + IsSubType>, -// BalanceOf: Send + Sync + From + FixedPointOperand, -// T::AccountId: AsRef<[u8]>, -// T::AccountId: UncheckedFrom, -// { -// fn traditional_fee( -// len: usize, -// info: &DispatchInfoOf, -// tip: BalanceOf, -// ) -> BalanceOf -// where -// T::Call: Dispatchable, -// { -// >::compute_fee(len as u32, info, tip) -// } - -// fn get_priority(len: usize, info: &DispatchInfoOf, final_fee: BalanceOf) -> TransactionPriority { -// let weight_saturation = T::BlockWeights::get().max_block / info.weight.max(1); -// let max_block_length = *T::BlockLength::get().max.get(DispatchClass::Normal); -// let len_saturation = max_block_length as u64 / (len as u64).max(1); -// let coefficient: BalanceOf = weight_saturation -// .min(len_saturation) -// .saturated_into::>(); -// final_fee -// .saturating_mul(coefficient) -// .saturated_into::() -// } - -// fn withdraw_fee( -// &self, -// who: &T::AccountId, -// call: &T::Call, -// info: &DispatchInfoOf, -// len: usize, -// ) -> Result< -// ( -// BalanceOf, -// <::OnChargeTransaction as pallet_transaction_payment::OnChargeTransaction>::LiquidityInfo, -// ), -// TransactionValidityError, -// > { -// let tip = self.0; - -// let fee = Self::traditional_fee(len, info, tip); - -// // Only mess with balances if fee is not zero. -// if fee.is_zero() { -// return <::OnChargeTransaction as pallet_transaction_payment::OnChargeTransaction>::withdraw_fee(who, call, info, fee, tip) -// .map(|i| (fee, i)); -// } - -// // Determine who is paying transaction fee based on ecnomic model -// // Parse call to extract collection ID and access collection sponsor - -// let sponsor = pallet_nft_transaction_payment::::withdraw_type(who, call); -// //let sponsor = Self::Module::::withdraw_type(who, call);; -// // >::withdraw_type(who, call); - -// let who_pays_fee = sponsor.unwrap_or_else(|| who.clone()); - -// <::OnChargeTransaction as pallet_transaction_payment::OnChargeTransaction>::withdraw_fee(&who_pays_fee, call, info, fee, tip) -// .map(|i| (fee, i)) -// } -// } - - -// impl SignedExtension -// for ChargeTransactionPayment -// where -// BalanceOf: Send + Sync + From + FixedPointOperand, -// T::Call: Dispatchable + IsSubType> + IsSubType>, -// T::AccountId: AsRef<[u8]>, -// T::AccountId: UncheckedFrom, -// { -// const IDENTIFIER: &'static str = "ChargeTransactionPayment"; -// type AccountId = T::AccountId; -// type Call = T::Call; -// type AdditionalSigned = (); -// type Pre = ( -// // tip -// BalanceOf, -// // who pays fee -// Self::AccountId, -// // imbalance resulting from withdrawing the fee -// <::OnChargeTransaction as pallet_transaction_payment::OnChargeTransaction>::LiquidityInfo, -// ); -// fn additional_signed(&self) -> sp_std::result::Result<(), TransactionValidityError> { -// Ok(()) -// } - -// fn validate( -// &self, -// who: &Self::AccountId, -// call: &Self::Call, -// info: &DispatchInfoOf, -// len: usize, -// ) -> TransactionValidity { -// let (fee, _) = self.withdraw_fee(who, call, info, len)?; -// Ok(ValidTransaction { -// priority: Self::get_priority(len, info, fee), -// ..Default::default() -// }) -// } - -// fn pre_dispatch( -// self, -// who: &Self::AccountId, -// call: &Self::Call, -// info: &DispatchInfoOf, -// len: usize, -// ) -> Result { -// let (_fee, imbalance) = self.withdraw_fee(who, call, info, len)?; -// Ok((self.0, who.clone(), imbalance)) -// } - -// fn post_dispatch( -// pre: Self::Pre, -// info: &DispatchInfoOf, -// post_info: &PostDispatchInfoOf, -// len: usize, -// _result: &DispatchResult, -// ) -> Result<(), TransactionValidityError> { -// let (tip, who, imbalance) = pre; -// let actual_fee = pallet_transaction_payment::Module::::compute_actual_fee( -// len as u32, -// info, -// post_info, -// tip, -// ); -// ::OnChargeTransaction::correct_and_deposit_fee(&who, info, post_info, actual_fee, tip, imbalance)?; -// Ok(()) -// } -// } \ No newline at end of file +} \ No newline at end of file --- a/pallets/nft/src/lib.rs +++ b/pallets/nft/src/lib.rs @@ -32,10 +32,11 @@ use frame_system::{self as system, ensure_signed, ensure_root}; use sp_runtime::sp_std::prelude::Vec; +use core::ops::{Deref, DerefMut}; use nft_data_structs::{ MAX_DECIMAL_POINTS, MAX_SPONSOR_TIMEOUT, MAX_TOKEN_OWNERSHIP, MAX_REFUNGIBLE_PIECES, AccessMode, ChainLimits, Collection, CreateItemData, CollectionLimits, - CollectionId, CollectionMode, CollectionHandle, TokenId, + CollectionId, CollectionMode, TokenId, SchemaVersion, SponsorshipState, Ownership, NftItemType, FungibleItemType, ReFungibleItemType }; @@ -164,7 +165,25 @@ } } -// + pallet_transaction_payment::Config + pallet_nft_transaction_payment::Config + pallet_contracts::Config +pub struct CollectionHandle { + pub id: CollectionId, + pub collection: Collection, +} + +impl Deref for CollectionHandle { + type Target = Collection; + + fn deref(&self) -> &Self::Target { + &self.collection + } +} + +impl DerefMut for CollectionHandle { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.collection + } +} + pub trait Config: system::Config + Sized { --- a/pallets/scheduler/src/lib.rs +++ b/pallets/scheduler/src/lib.rs @@ -102,7 +102,7 @@ /// Not strictly enforced, but used for weight estimation. type MaxScheduledPerBlock: Get; - /// TODO + /// Sponsoring function type Sponsoring: SponsoringResolve::Call>; /// Weight information for extrinsics in this pallet. @@ -230,8 +230,7 @@ /// - Write: Agenda /// - Will use base weight of 25 which should be good for up to 30 scheduled calls /// # - // #[weight = ::WeightInfo::schedule(T::MaxScheduledPerBlock::get())] - #[weight = 0] + #[weight = ::WeightInfo::schedule(T::MaxScheduledPerBlock::get())] fn schedule(origin, when: T::BlockNumber, maybe_periodic: Option>, @@ -367,7 +366,7 @@ } queued.sort_by_key(|(_, s)| s.priority); let base_weight: Weight = T::DbWeight::get().reads_writes(1, 2); // Agenda + Agenda(next) - let mut total_weight: Weight = ::WeightInfo::schedule(T::MaxScheduledPerBlock::get()); + let mut total_weight: Weight = 0; queued.into_iter() .enumerate() .scan(base_weight, |cumulative_weight, (order, (index, s))| { @@ -407,7 +406,7 @@ ).into(); let sender = ensure_signed(origin).unwrap_or(T::AccountId::default()); let who_will_pay = T::Sponsoring::resolve(&sender, &s.call.clone()).unwrap_or( - T::AccountId::default()); + sender); let sponsor = T::PalletsOrigin::from(system::RawOrigin::Signed(who_will_pay)); let r = s.call.clone().dispatch(sponsor.into()); let maybe_id = s.maybe_id.clone(); --- a/tests/src/pallet-presence.test.ts +++ b/tests/src/pallet-presence.test.ts @@ -13,7 +13,8 @@ // Pallets that must always be present const requiredPallets = [ - 'nft', 'inflation', 'balances', 'contracts', 'randomnesscollectiveflip', 'system', 'timestamp', 'transactionpayment', 'treasury', 'vesting' + 'nft', 'inflation', 'balances', 'contracts', 'randomnesscollectiveflip', 'system', 'timestamp', 'transactionpayment', 'treasury', 'vesting', + 'scheduler', 'nftpayment', 'charging' ]; // Pallets that depend on consensus and governance configuration --- /dev/null +++ b/tests/src/scheduler.test.ts @@ -0,0 +1,94 @@ +// +// 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 chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import privateKey from './substrate/privateKey'; +import usingApi from './substrate/substrate-api'; +import { + createItemExpectSuccess, + createCollectionExpectSuccess, + destroyCollectionExpectSuccess, + findNotExistingCollection, + queryCollectionExpectSuccess, + setOffchainSchemaExpectFailure, + setOffchainSchemaExpectSuccess, + transferExpectSuccess, + scheduleTransferExpectSuccess, + setCollectionSponsorExpectSuccess, + confirmSponsorshipExpectSuccess +} from './util/helpers'; + + +chai.use(chaiAsPromised); +const expect = chai.expect; + +const DATA = [1, 2, 3, 4]; + +describe('Integration Test scheduler base transaction', () => { + let alice: IKeyringPair; + + before(async () => { + await usingApi(async () => { + alice = privateKey('//Alice'); + }); + }); + + it('User can transfer owned token with delay (scheduler)', async () => { + await usingApi(async (api) => { + const Alice = privateKey('//Alice'); + const Bob = privateKey('//Bob'); + // nft + const nftCollectionId = await createCollectionExpectSuccess(); + const newNftTokenId = await createItemExpectSuccess(Alice, nftCollectionId, 'NFT'); + await setCollectionSponsorExpectSuccess(nftCollectionId, Alice.address); + await confirmSponsorshipExpectSuccess(nftCollectionId); + + await scheduleTransferExpectSuccess(nftCollectionId, newNftTokenId, Alice, Bob, 1, 'NFT'); + }); + }); + + +}); + +// describe('Negative Integration Test setOffchainSchema', () => { +// let alice: IKeyringPair; +// let bob: IKeyringPair; + +// let validCollectionId: number; + +// before(async () => { +// await usingApi(async () => { +// alice = privateKey('//Alice'); +// bob = privateKey('//Bob'); + +// validCollectionId = await createCollectionExpectSuccess({ mode: { type: 'NFT' } }); +// }); +// }); + +// it('fails on not existing collection id', async () => { +// const nonExistingCollectionId = await usingApi(findNotExistingCollection); + +// await setOffchainSchemaExpectFailure(alice, nonExistingCollectionId, DATA); +// }); + +// it('fails on destroyed collection id', async () => { +// const destroyedCollectionId = await createCollectionExpectSuccess({ mode: { type: 'NFT' } }); +// await destroyCollectionExpectSuccess(destroyedCollectionId); + +// await setOffchainSchemaExpectFailure(alice, destroyedCollectionId, DATA); +// }); + +// it('fails on too long data', async () => { +// const tooLongData = new Array(4097).fill(0xff); + +// await setOffchainSchemaExpectFailure(alice, validCollectionId, tooLongData); +// }); + +// it('fails on execution by non-owner', async () => { +// await setOffchainSchemaExpectFailure(bob, validCollectionId, DATA); +// }); +// }); --- a/tests/src/util/helpers.ts +++ b/tests/src/util/helpers.ts @@ -5,7 +5,7 @@ import { ApiPromise, Keyring } from '@polkadot/api'; import { Enum, Struct } from '@polkadot/types/codec'; -import type { AccountId, EventRecord } from '@polkadot/types/interfaces'; +import type { AccountId, BlockNumber, Call, EventRecord } from '@polkadot/types/interfaces'; import { u128 } from '@polkadot/types/primitive'; import { IKeyringPair } from '@polkadot/types/types'; import { BigNumber } from 'bignumber.js'; @@ -17,6 +17,8 @@ import { default as usingApi, submitTransactionAsync, submitTransactionExpectFailAsync } from '../substrate/substrate-api'; import { ICollectionInterface } from '../types'; import { hexToStr, strToUTF16, utf16ToStr } from './util'; +import { Compact, Option, Raw, Vec } from '@polkadot/types/codec'; +// import { AccountId, AccountIdOf, AccountIndex, Address, AssetId, Balance, BalanceOf, Block, BlockNumber, Call, ChangesTrieConfiguration, Consensus, ConsensusEngineId, Digest, DigestItem, DispatchClass, DispatchInfo, DispatchInfoTo190, EcdsaSignature, Ed25519Signature, Extrinsic, ExtrinsicEra, ExtrinsicPayload, ExtrinsicPayloadUnknown, ExtrinsicPayloadV1, ExtrinsicPayloadV2, ExtrinsicPayloadV3, ExtrinsicPayloadV4, ExtrinsicSignatureV1, ExtrinsicSignatureV2, ExtrinsicSignatureV3, ExtrinsicSignatureV4, ExtrinsicUnknown, ExtrinsicV1, ExtrinsicV2, ExtrinsicV3, ExtrinsicV4, Hash, Header, ImmortalEra, Index, Justification, KeyTypeId, KeyValue, LockIdentifier, LookupSource, LookupTarget, Moment, MortalEra, MultiSignature, Origin, Perbill, Percent, Permill, Perquintill, Phantom, PhantomData, PreRuntime, Seal, SealV0, Signature, SignedBlock, SignerPayload, Sr25519Signature, ValidatorId, Weight, WeightMultiplier } from '@polkadot/types/interfaces/runtime'; chai.use(chaiAsPromised); const expect = chai.expect; @@ -702,6 +704,54 @@ }); } +async function getBlockNumber(api: ApiPromise): Promise { + return new Promise(async (resolve, reject) => { + const unsubscribe = await api.rpc.chain.subscribeNewHeads((head) => { + unsubscribe(); + resolve(head.number.toNumber()); + }); + }); +} + +export async function +scheduleTransferExpectSuccess(collectionId: number, + tokenId: number, + sender: IKeyringPair, + recipient: IKeyringPair, + value: number | bigint = 1, + type: string = 'NFT') { + await usingApi(async (api: ApiPromise) => { + let balanceBefore = new BN(0); + + + let blockNumber: number | undefined = await getBlockNumber(api); + let expectedBlockNumber = blockNumber + 2; + + expect(blockNumber).to.be.greaterThan(0); + const transferTx = await api.tx.nft.transfer(recipient.address, collectionId, tokenId, value); + const scheduleTx = await api.tx.scheduler.schedule(expectedBlockNumber, null, 0, transferTx); + + const events = await submitTransactionAsync(sender, scheduleTx); + + const sponsorBalanceBefore = new BigNumber((await api.query.system.account(sender.address)).data.free.toString()); + const recipientBalanceBefore = new BigNumber((await api.query.system.account(recipient.address)).data.free.toString()); + + const nftItemDataBefore = (await api.query.nft.nftItemList(collectionId, tokenId)).toJSON() as unknown as ITokenDataType; + expect(nftItemDataBefore.Owner.toString()).to.be.equal(sender.address); + + // sleep for 2 blocks + await new Promise(resolve => setTimeout(resolve, 6000 * 2)); + + const sponsorBalanceAfter = new BigNumber((await api.query.system.account(sender.address)).data.free.toString()); + const recipientBalanceAfter = new BigNumber((await api.query.system.account(recipient.address)).data.free.toString()); + + const nftItemData = (await api.query.nft.nftItemList(collectionId, tokenId)).toJSON() as unknown as ITokenDataType; + expect(nftItemData.Owner.toString()).to.be.equal(recipient.address); + expect(recipientBalanceAfter.toNumber()).to.be.equal(recipientBalanceBefore.toNumber()); + }); +} + + export async function transferExpectSuccess(collectionId: number, tokenId: number,