--- a/Cargo.lock +++ b/Cargo.lock @@ -3085,10 +3085,12 @@ dependencies = [ "frame-support", "frame-system", + "pallet-transaction-payment", "parity-scale-codec", "sp-core", "sp-io", "sp-runtime", + "sp-std", ] [[package]] --- a/doc/application_development.md +++ b/doc/application_development.md @@ -16,6 +16,54 @@ ![](serverless_architecture.png) +## Custom Types for JS API + +``` +{ + "Schedule": { + "version": "u32", + "put_code_per_byte_cost": "Gas", + "grow_mem_cost": "Gas", + "regular_op_cost": "Gas", + "return_data_per_byte_cost": "Gas", + "event_data_per_byte_cost": "Gas", + "event_per_topic_cost": "Gas", + "event_base_cost": "Gas", + "call_base_cost": "Gas", + "instantiate_base_cost": "Gas", + "dispatch_base_cost": "Gas", + "sandbox_data_read_cost": "Gas", + "sandbox_data_write_cost": "Gas", + "transfer_cost": "Gas", + "instantiate_cost": "Gas", + "max_event_topics": "u32", + "max_stack_height": "u32", + "max_memory_pages": "u32", + "max_table_size": "u32", + "enable_println": "bool", + "max_subject_len": "u32" + }, + "NftItemType": { + "Collection": "u64", + "Owner": "AccountId", + "Data": "Vec" + }, + "CollectionType": { + "Owner": "AccountId", + "NextItemId": "u64", + "Name": "Vec", + "Description": "Vec", + "TokenPrefix": "Vec", + "CustomDataSize": "u32", + "Sponsor": "AccountId", + "UnconfirmedSponsor": "AccountId" + }, + "Address": "AccountId", + "LookupSource": "AccountId", + "Weight": "u64" +} +``` + ## NFT Palette Methods ### Collection Management --- a/pallets/nft/Cargo.toml +++ b/pallets/nft/Cargo.toml @@ -33,6 +33,19 @@ branch = 'rc4_ext_dispatch_reenabled' version = '2.0.0-rc4' +[dependencies.sp-std] +default-features = false +git = 'https://github.com/usetech-llc/substrate.git' +branch = 'rc4_ext_dispatch_reenabled' +version = '2.0.0-rc4' + +[dependencies.transaction-payment] +default-features = false +git = 'https://github.com/usetech-llc/substrate.git' +package = 'pallet-transaction-payment' +branch = 'rc4_ext_dispatch_reenabled' +version = '2.0.0-rc4' + [package] authors = ['Substrate DevHub '] description = 'FRAME pallet nft' @@ -52,4 +65,5 @@ 'frame-support/std', 'frame-system/std', 'sp-runtime/std', + 'sp-std/std', ] --- a/pallets/nft/src/lib.rs +++ b/pallets/nft/src/lib.rs @@ -9,9 +9,32 @@ /// For more guidance on Substrate FRAME, see the example pallet /// https://github.com/paritytech/substrate/blob/master/frame/example/src/lib.rs -use frame_support::{decl_event, decl_module, decl_storage, dispatch::DispatchResult, ensure}; +pub use frame_support::{ + decl_event, decl_module, decl_storage, + construct_runtime, parameter_types, + traits::{Currency, Get, ExistenceRequirement, KeyOwnerProofSystem, OnUnbalanced, Randomness, WithdrawReason, Imbalance}, + weights::{ + DispatchInfo, PostDispatchInfo, constants::{BlockExecutionWeight, ExtrinsicBaseWeight, RocksDbWeight, WEIGHT_PER_SECOND}, + IdentityFee, Weight, WeightToFeePolynomial, GetDispatchInfo, Pays, + }, + StorageValue, + dispatch::DispatchResult, + IsSubType, + ensure +}; + use frame_system::{self as system, ensure_signed}; use sp_runtime::sp_std::prelude::Vec; +use sp_std::prelude::*; +use sp_runtime::{ + FixedU128, FixedPointOperand, + transaction_validity::{ + TransactionPriority, ValidTransaction, InvalidTransaction, TransactionValidityError, TransactionValidity + }, + traits::{ + Saturating, Dispatchable, DispatchInfoOf, PostDispatchInfoOf, SignedExtension, Zero, SaturatedConversion, + }, +}; #[cfg(test)] mod mock; @@ -28,6 +51,8 @@ pub description: Vec, // 256 include null escape char pub token_prefix: Vec, // 16 include null escape char pub custom_data_size: u32, + pub sponsor: AccountId, // Who pays fees. If set to default address, the fees are applied to the transaction sender + pub unconfirmed_sponsor: AccountId, // Sponsor address that has not yet confirmed sponsorship } #[derive(Encode, Decode, Default, Clone, PartialEq)] @@ -51,6 +76,7 @@ /// The overarching event type. type Event: From> + Into<::Event>; + } // This pallet's storage items. @@ -138,6 +164,8 @@ token_prefix: prefix, next_item_id: next_id, custom_data_size: custom_data_sz, + sponsor: T::AccountId::default(), + unconfirmed_sponsor: T::AccountId::default(), }; // Add new collection to map @@ -237,6 +265,54 @@ } #[weight = 0] + pub fn set_collection_sponsor(origin, collection_id: u64, new_sponsor: T::AccountId) -> DispatchResult { + + let sender = ensure_signed(origin)?; + ensure!(>::contains_key(collection_id), "This collection does not exist"); + + let mut target_collection = >::get(collection_id); + ensure!(sender == target_collection.owner, "You do not own this collection"); + + target_collection.unconfirmed_sponsor = new_sponsor; + >::insert(collection_id, target_collection); + + Ok(()) + } + + #[weight = 0] + pub fn confirm_sponsorship(origin, collection_id: u64) -> DispatchResult { + + let sender = ensure_signed(origin)?; + ensure!(>::contains_key(collection_id), "This collection does not exist"); + + let mut target_collection = >::get(collection_id); + ensure!(sender == target_collection.unconfirmed_sponsor, "This address is not set as sponsor, use setCollectionSponsor first"); + + target_collection.sponsor = target_collection.unconfirmed_sponsor; + target_collection.unconfirmed_sponsor = T::AccountId::default(); + >::insert(collection_id, target_collection); + + Ok(()) + } + + #[weight = 0] + pub fn remove_collection_sponsor(origin, collection_id: u64) -> DispatchResult { + + let sender = ensure_signed(origin)?; + ensure!(>::contains_key(collection_id), "This collection does not exist"); + + let mut target_collection = >::get(collection_id); + ensure!(sender == target_collection.owner, "You do not own this collection"); + + target_collection.sponsor = T::AccountId::default(); + >::insert(collection_id, target_collection); + + Ok(()) + } + + + + #[weight = 0] pub fn create_item(origin, collection_id: u64, properties: Vec) -> DispatchResult { let sender = ensure_signed(origin)?; @@ -490,3 +566,185 @@ Ok(()) } } + + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// Economic models + +/// Fee multiplier. +pub type Multiplier = FixedU128; + +type BalanceOf = + <::Currency as Currency<::AccountId>>::Balance; +type NegativeImbalanceOf = <::Currency as Currency< + ::AccountId,>>::NegativeImbalance; + + + +/// 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, T>, + BalanceOf: Send + Sync + FixedPointOperand, +{ + /// utility constructor. Used only in client/factory code. + pub fn from(fee: BalanceOf) -> Self { + Self(fee) + } + + pub fn traditional_fee( + len: usize, + info: &DispatchInfoOf, + tip: BalanceOf, + ) -> BalanceOf where + T::Call: Dispatchable, + { + >::compute_fee(len as u32, info, tip) + } + + fn withdraw_fee( + &self, + who: &T::AccountId, + call: &T::Call, + info: &DispatchInfoOf, + len: usize, + ) -> Result<(BalanceOf, Option>), TransactionValidityError> { + let tip = self.0; + + // Set fee based on call type. Creating collection costs 1 Unique. + // All other transactions have traditional fees so far + let fee = match call.is_sub_type() { + Some(Call::create_collection(..)) => >::from(1_000_000_000), + _ => Self::traditional_fee(len, info, tip) + + // Flat fee model, use only for testing purposes + // _ => >::from(100) + }; + + // Determine who is paying transaction fee based on ecnomic model + // Parse call to extract collection ID and access collection sponsor + let sponsor: T::AccountId = match call.is_sub_type() { + Some(Call::create_item(collection_id, _properties)) => { + >::get(collection_id).sponsor + }, + Some(Call::transfer(collection_id, _item_id, _new_owner)) => { + >::get(collection_id).sponsor + }, + + _ => T::AccountId::default() + }; + + let mut who_pays_fee: T::AccountId = sponsor.clone(); + if sponsor == T::AccountId::default() { + who_pays_fee = who.clone(); + } + + // Only mess with balances if fee is not zero. + if fee.is_zero() { + return Ok((fee, None)); + } + + match ::Currency::withdraw( + &who_pays_fee, + fee, + if tip.is_zero() { + WithdrawReason::TransactionPayment.into() + } else { + WithdrawReason::TransactionPayment | WithdrawReason::Tip + }, + ExistenceRequirement::KeepAlive, + ) { + Ok(imbalance) => Ok((fee, Some(imbalance))), + Err(_) => Err(InvalidTransaction::Payment.into()), + } + } +} + +impl SignedExtension for ChargeTransactionPayment where + BalanceOf: Send + Sync + From + FixedPointOperand, + T::Call: Dispatchable + IsSubType, T>, +{ + const IDENTIFIER: &'static str = "ChargeTransactionPayment"; + type AccountId = T::AccountId; + type Call = T::Call; + type AdditionalSigned = (); + type Pre = (BalanceOf, Self::AccountId, Option>, BalanceOf); + 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)?; + + let mut r = ValidTransaction::default(); + // NOTE: we probably want to maximize the _fee (of any type) per weight unit_ here, which + // will be a bit more than setting the priority to tip. For now, this is enough. + r.priority = fee.saturated_into::(); + Ok(r) + } + + 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, fee)) + } + + fn post_dispatch( + pre: Self::Pre, + info: &DispatchInfoOf, + post_info: &PostDispatchInfoOf, + len: usize, + _result: &DispatchResult, + ) -> Result<(), TransactionValidityError> { + let (tip, who, imbalance, fee) = pre; + if let Some(payed) = imbalance { + let actual_fee = >::compute_actual_fee( + len as u32, + info, + post_info, + tip, + ); + let refund = fee.saturating_sub(actual_fee); + let actual_payment = match ::Currency::deposit_into_existing(&who, refund) { + Ok(refund_imbalance) => { + // The refund cannot be larger than the up front payed max weight. + // `PostDispatchInfo::calc_unspent` guards against such a case. + match payed.offset(refund_imbalance) { + Ok(actual_payment) => actual_payment, + Err(_) => return Err(InvalidTransaction::Payment.into()), + } + } + // We do not recreate the account using the refund. The up front payment + // is gone in that case. + Err(_) => payed, + }; + let imbalances = actual_payment.split(tip); + ::OnTransactionPayment::on_unbalanceds(Some(imbalances.0).into_iter() + .chain(Some(imbalances.1))); + } + Ok(()) + } +} --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -14,13 +14,13 @@ use sp_api::impl_runtime_apis; use sp_consensus_aura::sr25519::AuthorityId as AuraId; use sp_core::{crypto::KeyTypeId, OpaqueMetadata}; -use sp_runtime::traits::{ - BlakeTwo256, Block as BlockT, IdentifyAccount, IdentityLookup, NumberFor, Saturating, Verify, -}; use sp_runtime::{ create_runtime_str, generic, impl_opaque_keys, transaction_validity::{TransactionSource, TransactionValidity}, ApplyExtrinsicResult, MultiSignature, + traits::{ + BlakeTwo256, Block as BlockT, IdentifyAccount, IdentityLookup, NumberFor, Saturating, Verify, + }, }; use sp_std::prelude::*; #[cfg(feature = "std")] @@ -32,16 +32,22 @@ pub use contracts::Schedule as ContractsSchedule; pub use frame_support::{ construct_runtime, parameter_types, - traits::{KeyOwnerProofSystem, Randomness}, + traits::{Currency, Get, ExistenceRequirement, KeyOwnerProofSystem, OnUnbalanced, Randomness, WithdrawReason}, weights::{ - constants::{BlockExecutionWeight, ExtrinsicBaseWeight, RocksDbWeight, WEIGHT_PER_SECOND}, - IdentityFee, Weight, + DispatchInfo, PostDispatchInfo, constants::{BlockExecutionWeight, ExtrinsicBaseWeight, RocksDbWeight, WEIGHT_PER_SECOND}, + IdentityFee, Weight, WeightToFeePolynomial, GetDispatchInfo, Pays, }, StorageValue, + dispatch::DispatchResult, }; +use system::{self as system}; #[cfg(any(feature = "std", test))] pub use sp_runtime::BuildStorage; -pub use sp_runtime::{Perbill, Permill}; +use sp_runtime::{ + Perbill, +}; + + pub use timestamp::Call as TimestampCall; /// Importing a nft pallet @@ -228,7 +234,8 @@ } parameter_types! { - pub const ExistentialDeposit: u128 = 500; + // pub const ExistentialDeposit: u128 = 500; + pub const ExistentialDeposit: u128 = 0; } impl balances::Trait for Runtime { @@ -331,7 +338,7 @@ system::CheckEra, system::CheckNonce, system::CheckWeight, - transaction_payment::ChargeTransactionPayment, + nft::ChargeTransactionPayment, ); /// Unchecked extrinsic type as expected by this runtime. pub type UncheckedExtrinsic = generic::UncheckedExtrinsic; @@ -486,3 +493,4 @@ } } } +