diff --git a/Cargo.lock b/Cargo.lock index 0029d6e7fe7320d7135d04c34c0b2c8783f572a5..6e5f707fe33d400603fff74b42dead30f4f1b6be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -887,6 +887,7 @@ dependencies = [ "pallet-identity", "pallet-membership", "pallet-multisig", + "pallet-oneshot-account", "pallet-provide-randomness", "pallet-proxy", "pallet-scheduler", @@ -2426,6 +2427,7 @@ dependencies = [ "pallet-membership", "pallet-multisig", "pallet-offences", + "pallet-oneshot-account", "pallet-preimage", "pallet-provide-randomness", "pallet-proxy", @@ -2494,6 +2496,7 @@ dependencies = [ "pallet-membership", "pallet-multisig", "pallet-offences", + "pallet-oneshot-account", "pallet-preimage", "pallet-provide-randomness", "pallet-proxy", @@ -2776,6 +2779,7 @@ dependencies = [ "pallet-membership", "pallet-multisig", "pallet-offences", + "pallet-oneshot-account", "pallet-preimage", "pallet-provide-randomness", "pallet-proxy", @@ -5309,6 +5313,24 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-oneshot-account" +version = "3.0.0" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "pallet-balances", + "pallet-transaction-payment", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-preimage" version = "4.0.0-dev" diff --git a/Cargo.toml b/Cargo.toml index d6ddf72f3984686bf5c16d07f08ce5e3b521f074..63920b57750e58d55e124865b7cc561004d98560 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -133,6 +133,7 @@ members = [ 'pallets/duniter-wot', 'pallets/identity', 'pallets/membership', + 'pallets/oneshot-account', 'pallets/authority-members', 'pallets/universal-dividend', 'pallets/upgrade-origin', diff --git a/end2end-tests/cucumber-features/oneshot_account.feature b/end2end-tests/cucumber-features/oneshot_account.feature new file mode 100644 index 0000000000000000000000000000000000000000..f3d74b7906b9892d0933d86f29dba00c3b9ff6ef --- /dev/null +++ b/end2end-tests/cucumber-features/oneshot_account.feature @@ -0,0 +1,21 @@ +Feature: Oneshot account + + Scenario: Simple oneshot consumption + When alice sends 7 ÄžD to oneshot dave + Then alice should have 3 ÄžD + Then dave should have oneshot 7 ÄžD + When oneshot dave consumes into account bob + Then dave should have oneshot 0 ÄžD + Then bob should have 1699 cÄžD + Then bob should have oneshot 0 ÄžD + + Scenario: Double oneshot consumption + When alice sends 7 ÄžD to oneshot dave + Then alice should have 3 ÄžD + Then dave should have oneshot 7 ÄžD + When oneshot dave consumes 4 ÄžD into account bob and the rest into oneshot charlie + Then dave should have oneshot 0 ÄžD + Then bob should have 14 ÄžD + Then bob should have oneshot 0 ÄžD + Then charlie should have 10 ÄžD + Then charlie should have oneshot 299 cÄžD diff --git a/end2end-tests/tests/common/mod.rs b/end2end-tests/tests/common/mod.rs index 56317abf66a6b6f4e2265204f78dae385bffcf81..fb464d5a71c74687a31ca8fadb8f69f3a2a11979 100644 --- a/end2end-tests/tests/common/mod.rs +++ b/end2end-tests/tests/common/mod.rs @@ -18,6 +18,7 @@ pub mod balances; pub mod cert; +pub mod oneshot; #[subxt::subxt(runtime_metadata_path = "../resources/metadata.scale")] pub mod node_runtime {} diff --git a/end2end-tests/tests/common/oneshot.rs b/end2end-tests/tests/common/oneshot.rs new file mode 100644 index 0000000000000000000000000000000000000000..b8827fe8858dd84edad54142c64e2a0545373577 --- /dev/null +++ b/end2end-tests/tests/common/oneshot.rs @@ -0,0 +1,116 @@ +// Copyright 2021 Axiom-Team +// +// This file is part of Substrate-Libre-Currency. +// +// Substrate-Libre-Currency is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// Substrate-Libre-Currency 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with Substrate-Libre-Currency. If not, see <https://www.gnu.org/licenses/>. + +use super::node_runtime::runtime_types::gdev_runtime; +use super::node_runtime::runtime_types::pallet_balances; +use super::node_runtime::runtime_types::pallet_oneshot_account; +use super::*; +use sp_keyring::AccountKeyring; +use subxt::{ + sp_runtime::{AccountId32, MultiAddress}, + PairSigner, +}; + +pub enum Account { + Normal(AccountKeyring), + Oneshot(AccountKeyring), +} + +impl Account { + fn to_account_id( + &self, + ) -> pallet_oneshot_account::types::Account<MultiAddress<AccountId32, ()>> { + match self { + Account::Normal(account) => { + pallet_oneshot_account::types::Account::Normal(account.to_account_id().into()) + } + Account::Oneshot(account) => { + pallet_oneshot_account::types::Account::Oneshot(account.to_account_id().into()) + } + } + } +} + +pub async fn create_oneshot_account( + api: &Api, + client: &Client, + from: AccountKeyring, + amount: u64, + to: AccountKeyring, +) -> Result<()> { + let from = PairSigner::new(from.pair()); + let to = to.to_account_id(); + + let _events = create_block_with_extrinsic( + client, + api.tx() + .oneshot_account() + .create_oneshot_account(to.into(), amount)? + .create_signed(&from, BaseExtrinsicParamsBuilder::new()) + .await?, + ) + .await?; + + Ok(()) +} + +pub async fn consume_oneshot_account( + api: &Api, + client: &Client, + from: AccountKeyring, + to: Account, +) -> Result<()> { + let from = PairSigner::new(from.pair()); + let to = to.to_account_id(); + + let _events = create_block_with_extrinsic( + client, + api.tx() + .oneshot_account() + .consume_oneshot_account(0, to)? + .create_signed(&from, BaseExtrinsicParamsBuilder::new()) + .await?, + ) + .await?; + + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +pub async fn consume_oneshot_account_with_remaining( + api: &Api, + client: &Client, + from: AccountKeyring, + amount: u64, + to: Account, + remaining_to: Account, +) -> Result<()> { + let from = PairSigner::new(from.pair()); + let to = to.to_account_id(); + let remaining_to = remaining_to.to_account_id(); + + let _events = create_block_with_extrinsic( + client, + api.tx() + .oneshot_account() + .consume_oneshot_account_with_remaining(0, to, remaining_to, amount)? + .create_signed(&from, BaseExtrinsicParamsBuilder::new()) + .await?, + ) + .await?; + + Ok(()) +} diff --git a/end2end-tests/tests/cucumber_tests.rs b/end2end-tests/tests/cucumber_tests.rs index 265b782956af207df08e1768c147c0ff565cb6b8..303787361b6d295853c0458e31989c3ef4dcd912 100644 --- a/end2end-tests/tests/cucumber_tests.rs +++ b/end2end-tests/tests/cucumber_tests.rs @@ -155,7 +155,7 @@ async fn n_blocks_later(world: &mut DuniterWorld, n: usize) -> Result<()> { Ok(()) } -#[when(regex = r"([a-zA-Z]+) sends? (\d+) (ÄžD|cÄžD|UD|mUD) to ([a-zA-Z]+)")] +#[when(regex = r"([a-zA-Z]+) sends? (\d+) (ÄžD|cÄžD|UD|mUD) to ([a-zA-Z]+)$")] async fn transfer( world: &mut DuniterWorld, from: String, @@ -181,6 +181,86 @@ async fn transfer( } } +#[when(regex = r"([a-zA-Z]+) sends? (\d+) (ÄžD|cÄžD) to oneshot ([a-zA-Z]+)")] +async fn create_oneshot_account( + world: &mut DuniterWorld, + from: String, + amount: u64, + unit: String, + to: String, +) -> Result<()> { + // Parse inputs + let from = AccountKeyring::from_str(&from).expect("unknown from"); + let to = AccountKeyring::from_str(&to).expect("unknown to"); + let (amount, is_ud) = parse_amount(amount, &unit); + + assert!(!is_ud); + + common::oneshot::create_oneshot_account(world.api(), world.client(), from, amount, to).await +} + +#[when(regex = r"oneshot ([a-zA-Z]+) consumes? into (oneshot|account) ([a-zA-Z]+)")] +async fn consume_oneshot_account( + world: &mut DuniterWorld, + from: String, + is_dest_oneshot: String, + to: String, +) -> Result<()> { + // Parse inputs + let from = AccountKeyring::from_str(&from).expect("unknown from"); + let to = AccountKeyring::from_str(&to).expect("unknown to"); + let to = match is_dest_oneshot.as_str() { + "oneshot" => common::oneshot::Account::Oneshot(to), + "account" => common::oneshot::Account::Normal(to), + _ => unreachable!(), + }; + + common::oneshot::consume_oneshot_account(world.api(), world.client(), from, to).await +} + +#[when( + regex = r"oneshot ([a-zA-Z]+) consumes? (\d+) (ÄžD|cÄžD) into (oneshot|account) ([a-zA-Z]+) and the rest into (oneshot|account) ([a-zA-Z]+)" +)] +#[allow(clippy::too_many_arguments)] +async fn consume_oneshot_account_with_remaining( + world: &mut DuniterWorld, + from: String, + amount: u64, + unit: String, + is_dest_oneshot: String, + to: String, + is_remaining_to_oneshot: String, + remaining_to: String, +) -> Result<()> { + // Parse inputs + let from = AccountKeyring::from_str(&from).expect("unknown from"); + let to = AccountKeyring::from_str(&to).expect("unknown to"); + let remaining_to = AccountKeyring::from_str(&remaining_to).expect("unknown remaining_to"); + let to = match is_dest_oneshot.as_str() { + "oneshot" => common::oneshot::Account::Oneshot(to), + "account" => common::oneshot::Account::Normal(to), + _ => unreachable!(), + }; + let remaining_to = match is_remaining_to_oneshot.as_str() { + "oneshot" => common::oneshot::Account::Oneshot(remaining_to), + "account" => common::oneshot::Account::Normal(remaining_to), + _ => unreachable!(), + }; + let (amount, is_ud) = parse_amount(amount, &unit); + + assert!(!is_ud); + + common::oneshot::consume_oneshot_account_with_remaining( + world.api(), + world.client(), + from, + amount, + to, + remaining_to, + ) + .await +} + #[when(regex = r"([a-zA-Z]+) sends? all (?:his|her) (?:ÄžDs?|DUs?|UDs?) to ([a-zA-Z]+)")] async fn send_all_to(world: &mut DuniterWorld, from: String, to: String) -> Result<()> { // Parse inputs @@ -219,6 +299,29 @@ async fn should_have( Ok(()) } +#[then(regex = r"([a-zA-Z]+) should have oneshot (\d+) (ÄžD|cÄžD)")] +async fn should_have_oneshot( + world: &mut DuniterWorld, + who: String, + amount: u64, + unit: String, +) -> Result<()> { + // Parse inputs + let who = AccountKeyring::from_str(&who) + .expect("unknown to") + .to_account_id(); + let (amount, _is_ud) = parse_amount(amount, &unit); + + let oneshot_amount = world + .api() + .storage() + .oneshot_account() + .oneshot_accounts(&who, None) + .await?; + assert_eq!(oneshot_amount.unwrap_or(0), amount); + Ok(()) +} + #[then(regex = r"Current UD amount should be (\d+).(\d+)")] async fn current_ud_amount_should_be( world: &mut DuniterWorld, diff --git a/pallets/oneshot-account/Cargo.toml b/pallets/oneshot-account/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..cde1128d8733d39a5a64d90f3af6bd05fa607036 --- /dev/null +++ b/pallets/oneshot-account/Cargo.toml @@ -0,0 +1,94 @@ +[package] +authors = ['librelois <c@elo.tf>'] +description = 'FRAME pallet oneshot account.' +edition = '2018' +homepage = 'https://substrate.dev' +license = 'AGPL-3.0' +name = 'pallet-oneshot-account' +readme = 'README.md' +repository = 'https://git.duniter.org/nodes/rust/duniter-v2s' +version = '3.0.0' + +[features] +default = ['std'] +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "pallet-balances", +] +std = [ + 'codec/std', + 'frame-support/std', + 'frame-system/std', + 'frame-benchmarking/std', + 'sp-core/std', + 'sp-io/std', + 'sp-runtime/std', + 'sp-std/std', +] +try-runtime = ['frame-support/try-runtime'] + +[dependencies] +# crates.io +codec = { package = 'parity-scale-codec', version = "3.1.5", default-features = false, features = ["derive"] } +log = { version = "0.4.14", default-features = false } +scale-info = { version = "2.1.1", default-features = false, features = ["derive"] } + +# benchmarks +pallet-balances = { git = 'https://github.com/duniter/substrate', branch = 'duniter-substrate-v0.9.23', optional = true, default-features = false } + +# substrate +[dependencies.frame-benchmarking] +default-features = false +git = 'https://github.com/duniter/substrate' +optional = true +branch = 'duniter-substrate-v0.9.23' + +[dependencies.frame-support] +default-features = false +git = 'https://github.com/duniter/substrate' +branch = 'duniter-substrate-v0.9.23' + +[dependencies.frame-system] +default-features = false +git = 'https://github.com/duniter/substrate' +branch = 'duniter-substrate-v0.9.23' + +[dependencies.pallet-transaction-payment] +default-features = false +git = 'https://github.com/duniter/substrate' +branch = 'duniter-substrate-v0.9.23' + +[dependencies.sp-core] +default-features = false +git = 'https://github.com/duniter/substrate' +branch = 'duniter-substrate-v0.9.23' + +[dependencies.sp-io] +default-features = false +git = 'https://github.com/duniter/substrate' +branch = 'duniter-substrate-v0.9.23' + +[dependencies.sp-runtime] +default-features = false +git = 'https://github.com/duniter/substrate' +branch = 'duniter-substrate-v0.9.23' + +[dependencies.sp-std] +default-features = false +git = 'https://github.com/duniter/substrate' +branch = 'duniter-substrate-v0.9.23' + +### DOC ### + +[package.metadata.docs.rs] +targets = ['x86_64-unknown-linux-gnu'] + +### DEV ### + +[dev-dependencies.sp-io] +git = 'https://github.com/duniter/substrate' +branch = 'duniter-substrate-v0.9.23' + +[dev-dependencies.pallet-balances] +git = 'https://github.com/duniter/substrate' +branch = 'duniter-substrate-v0.9.23' diff --git a/pallets/oneshot-account/src/benchmarking.rs b/pallets/oneshot-account/src/benchmarking.rs new file mode 100644 index 0000000000000000000000000000000000000000..a72c6585106da25fc5b8e8107cfb3fbbe96276ba --- /dev/null +++ b/pallets/oneshot-account/src/benchmarking.rs @@ -0,0 +1,138 @@ +// Copyright 2021-2022 Axiom-Team +// +// This file is part of Duniter-v2S. +// +// Duniter-v2S is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// Duniter-v2S 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>. + +#![cfg(feature = "runtime-benchmarks")] + +use super::*; + +use frame_benchmarking::{account, benchmarks, whitelisted_caller}; +use frame_support::pallet_prelude::IsType; +use frame_support::traits::Get; +use frame_system::RawOrigin; +use pallet_balances::Pallet as Balances; + +use crate::Pallet; + +const SEED: u32 = 0; + +benchmarks! { + where_clause { where + T: pallet_balances::Config, + T::Balance: From<u64>, + <T::Currency as Currency<T::AccountId>>::Balance: IsType<T::Balance> + } + create_oneshot_account { + let existential_deposit = T::ExistentialDeposit::get(); + let caller = whitelisted_caller(); + + // Give some multiple of the existential deposit + let balance = existential_deposit.saturating_mul((2).into()); + let _ = T::Currency::make_free_balance_be(&caller, balance.into()); + + let recipient: T::AccountId = account("recipient", 0, SEED); + let recipient_lookup: <T::Lookup as StaticLookup>::Source = + T::Lookup::unlookup(recipient.clone()); + let transfer_amount = existential_deposit; + }: _(RawOrigin::Signed(caller.clone()), recipient_lookup, transfer_amount.into()) + verify { + assert_eq!(Balances::<T>::free_balance(&caller), transfer_amount); + assert_eq!(OneshotAccounts::<T>::get(&recipient), Some(transfer_amount.into())); + } + where_clause { where + T: pallet_balances::Config, + T::Balance: From<u64>, + <T::Currency as Currency<T::AccountId>>::Balance: IsType<T::Balance>+From<T::Balance> + } + consume_oneshot_account { + let existential_deposit = T::ExistentialDeposit::get(); + let caller: T::AccountId = whitelisted_caller(); + + // Give some multiple of the existential deposit + let balance = existential_deposit.saturating_mul((2).into()); + OneshotAccounts::<T>::insert( + caller.clone(), + Into::<<T::Currency as Currency<T::AccountId>>::Balance>::into(balance) + ); + + // Deposit into a normal account is more expensive than into a oneshot account + // so we create the recipient account with an existential deposit. + let recipient: T::AccountId = account("recipient", 0, SEED); + let recipient_lookup: <T::Lookup as StaticLookup>::Source = + T::Lookup::unlookup(recipient.clone()); + let _ = T::Currency::make_free_balance_be(&recipient, existential_deposit.into()); + }: _( + RawOrigin::Signed(caller.clone()), + T::BlockNumber::zero(), + Account::<<T::Lookup as StaticLookup>::Source>::Normal(recipient_lookup) + ) + verify { + assert_eq!(OneshotAccounts::<T>::get(&caller), None); + assert_eq!( + Balances::<T>::free_balance(&recipient), + existential_deposit.saturating_mul((3).into()) + ); + } + where_clause { where + T: pallet_balances::Config, + T::Balance: From<u64>, + <T::Currency as Currency<T::AccountId>>::Balance: IsType<T::Balance>+From<T::Balance> + } + consume_oneshot_account_with_remaining { + let existential_deposit = T::ExistentialDeposit::get(); + let caller: T::AccountId = whitelisted_caller(); + + // Give some multiple of the existential deposit + let balance = existential_deposit.saturating_mul((2).into()); + OneshotAccounts::<T>::insert( + caller.clone(), + Into::<<T::Currency as Currency<T::AccountId>>::Balance>::into(balance) + ); + + // Deposit into a normal account is more expensive than into a oneshot account + // so we create the recipient accounts with an existential deposits. + let recipient1: T::AccountId = account("recipient1", 0, SEED); + let recipient1_lookup: <T::Lookup as StaticLookup>::Source = + T::Lookup::unlookup(recipient1.clone()); + let _ = T::Currency::make_free_balance_be(&recipient1, existential_deposit.into()); + let recipient2: T::AccountId = account("recipient2", 1, SEED); + let recipient2_lookup: <T::Lookup as StaticLookup>::Source = + T::Lookup::unlookup(recipient2.clone()); + let _ = T::Currency::make_free_balance_be(&recipient2, existential_deposit.into()); + }: _( + RawOrigin::Signed(caller.clone()), + T::BlockNumber::zero(), + Account::<<T::Lookup as StaticLookup>::Source>::Normal(recipient1_lookup), + Account::<<T::Lookup as StaticLookup>::Source>::Normal(recipient2_lookup), + existential_deposit.into() + ) + verify { + assert_eq!(OneshotAccounts::<T>::get(&caller), None); + assert_eq!( + Balances::<T>::free_balance(&recipient1), + existential_deposit.saturating_mul((2).into()) + ); + assert_eq!( + Balances::<T>::free_balance(&recipient2), + existential_deposit.saturating_mul((2).into()) + ); + } + + impl_benchmark_test_suite!( + Pallet, + crate::mock::new_test_ext(), + crate::mock::Test + ); +} diff --git a/pallets/oneshot-account/src/check_nonce.rs b/pallets/oneshot-account/src/check_nonce.rs new file mode 100644 index 0000000000000000000000000000000000000000..d6325ab4d1146bc2c9f3af54f23f9ebd0626b37d --- /dev/null +++ b/pallets/oneshot-account/src/check_nonce.rs @@ -0,0 +1,86 @@ +// Copyright 2021 Axiom-Team +// +// This file is part of Substrate-Libre-Currency. +// +// Substrate-Libre-Currency is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// Substrate-Libre-Currency 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with Substrate-Libre-Currency. If not, see <https://www.gnu.org/licenses/>. + +use crate::Config; + +use codec::{Decode, Encode}; +use frame_support::traits::IsSubType; +use frame_support::weights::DispatchInfo; +//use frame_system::Config; +use scale_info::TypeInfo; +use sp_runtime::{ + traits::{DispatchInfoOf, Dispatchable, SignedExtension}, + transaction_validity::{TransactionValidity, TransactionValidityError}, +}; + +#[derive(Encode, Decode, Clone, Eq, PartialEq, TypeInfo)] +#[scale_info(skip_type_params(Runtime))] +pub struct CheckNonce<T: Config>(pub frame_system::CheckNonce<T>); + +impl<T: Config> sp_std::fmt::Debug for CheckNonce<T> { + #[cfg(feature = "std")] + fn fmt(&self, f: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result { + write!(f, "CheckNonce({})", self.0 .0) + } + + #[cfg(not(feature = "std"))] + fn fmt(&self, _: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result { + Ok(()) + } +} + +impl<T: Config + TypeInfo> SignedExtension for CheckNonce<T> +where + T::Call: Dispatchable<Info = DispatchInfo> + IsSubType<crate::Call<T>>, +{ + type AccountId = <T as frame_system::Config>::AccountId; + type Call = <T as frame_system::Config>::Call; + type AdditionalSigned = (); + type Pre = (); + const IDENTIFIER: &'static str = "CheckNonce"; + + fn additional_signed(&self) -> sp_std::result::Result<(), TransactionValidityError> { + self.0.additional_signed() + } + + fn pre_dispatch( + self, + who: &Self::AccountId, + call: &Self::Call, + info: &DispatchInfoOf<Self::Call>, + len: usize, + ) -> Result<(), TransactionValidityError> { + if let Some( + crate::Call::consume_oneshot_account { .. } + | crate::Call::consume_oneshot_account_with_remaining { .. }, + ) = call.is_sub_type() + { + Ok(()) + } else { + self.0.pre_dispatch(who, call, info, len) + } + } + + fn validate( + &self, + who: &Self::AccountId, + call: &Self::Call, + info: &DispatchInfoOf<Self::Call>, + len: usize, + ) -> TransactionValidity { + self.0.validate(who, call, info, len) + } +} diff --git a/pallets/oneshot-account/src/lib.rs b/pallets/oneshot-account/src/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..0a65c42ba06563d4f86ca7f8b08526b339b0bdb3 --- /dev/null +++ b/pallets/oneshot-account/src/lib.rs @@ -0,0 +1,380 @@ +// Copyright 2021 Axiom-Team +// +// This file is part of Substrate-Libre-Currency. +// +// Substrate-Libre-Currency is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// Substrate-Libre-Currency 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with Substrate-Libre-Currency. If not, see <https://www.gnu.org/licenses/>. + +#![cfg_attr(not(feature = "std"), no_std)] + +mod benchmarking; +mod check_nonce; +#[cfg(test)] +mod mock; +mod types; + +pub use check_nonce::CheckNonce; +pub use pallet::*; +pub use types::*; + +use frame_support::pallet_prelude::*; +use frame_support::traits::{ + Currency, ExistenceRequirement, Imbalance, IsSubType, WithdrawReasons, +}; +use frame_system::pallet_prelude::*; +use pallet_transaction_payment::OnChargeTransaction; +use sp_runtime::traits::{DispatchInfoOf, PostDispatchInfoOf, Saturating, StaticLookup, Zero}; +use sp_std::convert::TryInto; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::traits::StorageVersion; + + /// The current storage version. + const STORAGE_VERSION: StorageVersion = StorageVersion::new(1); + + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + #[pallet::storage_version(STORAGE_VERSION)] + #[pallet::without_storage_info] + pub struct Pallet<T>(_); + + // CONFIG // + + #[pallet::config] + pub trait Config: frame_system::Config + pallet_transaction_payment::Config { + type Currency: Currency<Self::AccountId>; + type Event: From<Event<Self>> + IsType<<Self as frame_system::Config>::Event>; + type InnerOnChargeTransaction: OnChargeTransaction<Self>; + } + + // STORAGE // + + #[pallet::storage] + #[pallet::getter(fn oneshot_account)] + pub type OneshotAccounts<T: Config> = StorageMap< + _, + Blake2_128Concat, + T::AccountId, + <T::Currency as Currency<T::AccountId>>::Balance, + OptionQuery, + >; + + // EVENTS // + + #[allow(clippy::type_complexity)] + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event<T: Config> { + OneshotAccountCreated { + account: T::AccountId, + balance: <T::Currency as Currency<T::AccountId>>::Balance, + creator: T::AccountId, + }, + OneshotAccountConsumed { + account: T::AccountId, + dest1: ( + T::AccountId, + <T::Currency as Currency<T::AccountId>>::Balance, + ), + dest2: Option<( + T::AccountId, + <T::Currency as Currency<T::AccountId>>::Balance, + )>, + }, + Withdraw { + account: T::AccountId, + balance: <T::Currency as Currency<T::AccountId>>::Balance, + }, + } + + // ERRORS // + + #[pallet::error] + pub enum Error<T> { + /// Block height is in the future + BlockHeightInFuture, + /// Block height is too old + BlockHeightTooOld, + /// Destination account does not exist + DestAccountNotExist, + /// Destination account has balance less than existential deposit + ExistentialDeposit, + /// Source account has insufficient balance + InsufficientBalance, + /// Destination oneshot account already exists + OneshotAccountAlreadyCreated, + /// Source oneshot account does not exist + OneshotAccountNotExist, + } + + // CALLS // + #[pallet::call] + impl<T: Config> Pallet<T> { + /// Create an account that can only be consumed once + /// + /// - `dest`: The oneshot account to be created. + /// - `balance`: The balance to be transfered to this oneshot account. + /// + /// Origin account is kept alive. + #[pallet::weight(500_000_000)] + pub fn create_oneshot_account( + origin: OriginFor<T>, + dest: <T::Lookup as StaticLookup>::Source, + #[pallet::compact] value: <T::Currency as Currency<T::AccountId>>::Balance, + ) -> DispatchResult { + let transactor = ensure_signed(origin)?; + let dest = T::Lookup::lookup(dest)?; + + ensure!( + value >= <T::Currency as Currency<T::AccountId>>::minimum_balance(), + Error::<T>::ExistentialDeposit + ); + ensure!( + OneshotAccounts::<T>::get(&dest).is_none(), + Error::<T>::OneshotAccountAlreadyCreated + ); + + <T::Currency as Currency<T::AccountId>>::withdraw( + &transactor, + value, + WithdrawReasons::TRANSFER, + ExistenceRequirement::KeepAlive, + )?; + OneshotAccounts::<T>::insert(&dest, value); + Self::deposit_event(Event::OneshotAccountCreated { + account: dest, + balance: value, + creator: transactor, + }); + + Ok(()) + } + /// Consume a oneshot account and transfer its balance to an account + /// + /// - `block_height`: Must be a recent block number. The limit is `BlockHashCount` in the past. (this is to prevent replay attacks) + /// - `dest`: The destination account. + /// - `dest_is_oneshot`: If set to `true`, then a oneshot account is created at `dest`. Else, `dest` has to be an existing account. + #[pallet::weight(500_000_000)] + pub fn consume_oneshot_account( + origin: OriginFor<T>, + block_height: T::BlockNumber, + dest: Account<<T::Lookup as StaticLookup>::Source>, + ) -> DispatchResult { + let transactor = ensure_signed(origin)?; + + let (dest, dest_is_oneshot) = match dest { + Account::Normal(account) => (account, false), + Account::Oneshot(account) => (account, true), + }; + let dest = T::Lookup::lookup(dest)?; + + let value = OneshotAccounts::<T>::take(&transactor) + .ok_or(Error::<T>::OneshotAccountNotExist)?; + + ensure!( + block_height <= frame_system::Pallet::<T>::block_number(), + Error::<T>::BlockHeightInFuture + ); + ensure!( + frame_system::pallet::BlockHash::<T>::contains_key(block_height), + Error::<T>::BlockHeightTooOld + ); + if dest_is_oneshot { + ensure!( + OneshotAccounts::<T>::get(&dest).is_none(), + Error::<T>::OneshotAccountAlreadyCreated + ); + OneshotAccounts::<T>::insert(&dest, value); + Self::deposit_event(Event::OneshotAccountCreated { + account: dest.clone(), + balance: value, + creator: transactor.clone(), + }); + } else { + <T::Currency as Currency<T::AccountId>>::deposit_into_existing(&dest, value)?; + } + OneshotAccounts::<T>::remove(&transactor); + Self::deposit_event(Event::OneshotAccountConsumed { + account: transactor, + dest1: (dest, value), + dest2: None, + }); + + Ok(()) + } + /// Consume a oneshot account then transfer some amount to an account, + /// and the remaining amount to another account. + /// + /// - `block_height`: Must be a recent block number. + /// The limit is `BlockHashCount` in the past. (this is to prevent replay attacks) + /// - `dest`: The destination account. + /// - `dest_is_oneshot`: If set to `true`, then a oneshot account is created at `dest`. Else, `dest` has to be an existing account. + /// - `dest2`: The second destination account. + /// - `dest2_is_oneshot`: If set to `true`, then a oneshot account is created at `dest2`. Else, `dest2` has to be an existing account. + /// - `balance1`: The amount transfered to `dest`, the leftover being transfered to `dest2`. + #[pallet::weight(500_000_000)] + pub fn consume_oneshot_account_with_remaining( + origin: OriginFor<T>, + block_height: T::BlockNumber, + dest: Account<<T::Lookup as StaticLookup>::Source>, + remaining_to: Account<<T::Lookup as StaticLookup>::Source>, + #[pallet::compact] balance: <T::Currency as Currency<T::AccountId>>::Balance, + ) -> DispatchResult { + let transactor = ensure_signed(origin)?; + + let (dest1, dest1_is_oneshot) = match dest { + Account::Normal(account) => (account, false), + Account::Oneshot(account) => (account, true), + }; + let dest1 = T::Lookup::lookup(dest1)?; + let (dest2, dest2_is_oneshot) = match remaining_to { + Account::Normal(account) => (account, false), + Account::Oneshot(account) => (account, true), + }; + let dest2 = T::Lookup::lookup(dest2)?; + + let value = OneshotAccounts::<T>::take(&transactor) + .ok_or(Error::<T>::OneshotAccountNotExist)?; + + let balance1 = balance; + ensure!(value > balance1, Error::<T>::InsufficientBalance); + let balance2 = value.saturating_sub(balance1); + ensure!( + block_height <= frame_system::Pallet::<T>::block_number(), + Error::<T>::BlockHeightInFuture + ); + ensure!( + frame_system::pallet::BlockHash::<T>::contains_key(block_height), + Error::<T>::BlockHeightTooOld + ); + if dest1_is_oneshot { + ensure!( + OneshotAccounts::<T>::get(&dest1).is_none(), + Error::<T>::OneshotAccountAlreadyCreated + ); + ensure!( + balance1 >= <T::Currency as Currency<T::AccountId>>::minimum_balance(), + Error::<T>::ExistentialDeposit + ); + } else { + ensure!( + !<T::Currency as Currency<T::AccountId>>::free_balance(&dest1).is_zero(), + Error::<T>::DestAccountNotExist + ); + } + if dest2_is_oneshot { + ensure!( + OneshotAccounts::<T>::get(&dest2).is_none(), + Error::<T>::OneshotAccountAlreadyCreated + ); + ensure!( + balance2 >= <T::Currency as Currency<T::AccountId>>::minimum_balance(), + Error::<T>::ExistentialDeposit + ); + OneshotAccounts::<T>::insert(&dest2, balance2); + Self::deposit_event(Event::OneshotAccountCreated { + account: dest2.clone(), + balance: balance2, + creator: transactor.clone(), + }); + } else { + <T::Currency as Currency<T::AccountId>>::deposit_into_existing(&dest2, balance2)?; + } + if dest1_is_oneshot { + OneshotAccounts::<T>::insert(&dest1, balance1); + Self::deposit_event(Event::OneshotAccountCreated { + account: dest1.clone(), + balance: balance1, + creator: transactor.clone(), + }); + } else { + <T::Currency as Currency<T::AccountId>>::deposit_into_existing(&dest1, balance1)?; + } + OneshotAccounts::<T>::remove(&transactor); + Self::deposit_event(Event::OneshotAccountConsumed { + account: transactor, + dest1: (dest1, balance1), + dest2: Some((dest2, balance2)), + }); + + Ok(()) + } + } +} + +impl<T: Config> OnChargeTransaction<T> for Pallet<T> +where + T::Call: IsSubType<Call<T>>, + T::InnerOnChargeTransaction: OnChargeTransaction< + T, + Balance = <T::Currency as Currency<T::AccountId>>::Balance, + LiquidityInfo = Option<<T::Currency as Currency<T::AccountId>>::NegativeImbalance>, + >, +{ + type Balance = <T::Currency as Currency<T::AccountId>>::Balance; + type LiquidityInfo = Option<<T::Currency as Currency<T::AccountId>>::NegativeImbalance>; + fn withdraw_fee( + who: &T::AccountId, + call: &T::Call, + dispatch_info: &DispatchInfoOf<T::Call>, + fee: Self::Balance, + tip: Self::Balance, + ) -> Result<Self::LiquidityInfo, TransactionValidityError> { + if let Some( + Call::consume_oneshot_account { .. } + | Call::consume_oneshot_account_with_remaining { .. }, + ) = call.is_sub_type() + { + if fee.is_zero() { + return Ok(None); + } + + if let Some(balance) = OneshotAccounts::<T>::get(&who) { + if balance >= fee { + OneshotAccounts::<T>::insert(who, balance.saturating_sub(fee)); + Self::deposit_event(Event::Withdraw { + account: who.clone(), + balance: fee, + }); + // TODO + return Ok(Some( + <T::Currency as Currency<T::AccountId>>::NegativeImbalance::zero(), + )); + } + } + Err(TransactionValidityError::Invalid( + InvalidTransaction::Payment, + )) + } else { + T::InnerOnChargeTransaction::withdraw_fee(who, call, dispatch_info, fee, tip) + } + } + fn correct_and_deposit_fee( + who: &T::AccountId, + dispatch_info: &DispatchInfoOf<T::Call>, + post_info: &PostDispatchInfoOf<T::Call>, + corrected_fee: Self::Balance, + tip: Self::Balance, + already_withdrawn: Self::LiquidityInfo, + ) -> Result<(), TransactionValidityError> { + T::InnerOnChargeTransaction::correct_and_deposit_fee( + who, + dispatch_info, + post_info, + corrected_fee, + tip, + already_withdrawn, + ) + } +} diff --git a/pallets/oneshot-account/src/mock.rs b/pallets/oneshot-account/src/mock.rs new file mode 100644 index 0000000000000000000000000000000000000000..35b69d7ae55d610f3db44d9b5d3b8752f64e6e00 --- /dev/null +++ b/pallets/oneshot-account/src/mock.rs @@ -0,0 +1,124 @@ +// Copyright 2021 Axiom-Team +// +// This file is part of Substrate-Libre-Currency. +// +// Substrate-Libre-Currency is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// Substrate-Libre-Currency 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with Substrate-Libre-Currency. If not, see <https://www.gnu.org/licenses/>. + +use crate::{self as pallet_oneshot_account}; +use frame_support::{parameter_types, traits::Everything, weights::IdentityFee}; +use frame_system as system; +use pallet_transaction_payment::CurrencyAdapter; +use sp_core::H256; +use sp_runtime::{ + testing::Header, + traits::{BlakeTwo256, IdentityLookup}, + BuildStorage, +}; +use sp_std::convert::{TryFrom, TryInto}; + +type Balance = u64; +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<Test>; +type Block = frame_system::mocking::MockBlock<Test>; + +// Configure a mock runtime to test the pallet. +frame_support::construct_runtime!( + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system::{Pallet, Call, Config, Storage, Event<T>}, + Balances: pallet_balances::{Pallet, Call, Storage, Config<T>, Event<T>}, + TransactionPayment: pallet_transaction_payment::{Pallet, Storage}, + OneshotAccount: pallet_oneshot_account::{Pallet, Call, Storage, Event<T>}, + } +); + +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub const SS58Prefix: u8 = 42; +} + +impl system::Config for Test { + type BaseCallFilter = Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type Origin = Origin; + type Call = Call; + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup<Self::AccountId>; + type Header = Header; + type Event = Event; + type BlockHashCount = BlockHashCount; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData<Balance>; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = SS58Prefix; + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} + +parameter_types! { + pub const ExistentialDeposit: Balance = 10; + pub const MaxLocks: u32 = 50; +} + +impl pallet_balances::Config for Test { + type Balance = Balance; + type DustRemoval = (); + type Event = Event; + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type WeightInfo = pallet_balances::weights::SubstrateWeight<Test>; + type MaxLocks = MaxLocks; + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; +} +impl pallet_transaction_payment::Config for Test { + type OnChargeTransaction = OneshotAccount; + type OperationalFeeMultiplier = frame_support::traits::ConstU8<5>; + type WeightToFee = IdentityFee<u64>; + type LengthToFee = IdentityFee<u64>; + type FeeMultiplierUpdate = (); +} +impl pallet_oneshot_account::Config for Test { + type Currency = Balances; + type Event = Event; + type InnerOnChargeTransaction = CurrencyAdapter<Balances, HandleFees>; +} + +pub struct HandleFees; +type NegativeImbalance = <Balances as frame_support::traits::Currency<u64>>::NegativeImbalance; +impl frame_support::traits::OnUnbalanced<NegativeImbalance> for HandleFees { + fn on_nonzero_unbalanced(_amount: NegativeImbalance) {} +} + +// Build genesis storage according to the mock runtime. +#[allow(dead_code)] +pub fn new_test_ext() -> sp_io::TestExternalities { + GenesisConfig { + system: SystemConfig::default(), + balances: BalancesConfig::default(), + } + .build_storage() + .unwrap() + .into() +} diff --git a/pallets/oneshot-account/src/types.rs b/pallets/oneshot-account/src/types.rs new file mode 100644 index 0000000000000000000000000000000000000000..75344846552bc8fb87db7bc250b7b25d1d2e3ff2 --- /dev/null +++ b/pallets/oneshot-account/src/types.rs @@ -0,0 +1,24 @@ +// Copyright 2021 Axiom-Team +// +// This file is part of Substrate-Libre-Currency. +// +// Substrate-Libre-Currency is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// Substrate-Libre-Currency 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with Substrate-Libre-Currency. If not, see <https://www.gnu.org/licenses/>. + +use codec::{Decode, Encode}; +use frame_support::pallet_prelude::*; + +#[derive(Clone, Decode, Encode, PartialEq, RuntimeDebug, TypeInfo)] +pub enum Account<AccountId> { + Normal(AccountId), + Oneshot(AccountId), +} diff --git a/pallets/oneshot-account/src/weights.rs b/pallets/oneshot-account/src/weights.rs new file mode 100644 index 0000000000000000000000000000000000000000..a4cbfbe78a9f76a68879cdc9e675a8518987bb7f --- /dev/null +++ b/pallets/oneshot-account/src/weights.rs @@ -0,0 +1,52 @@ +// Copyright 2021-2022 Axiom-Team +// +// This file is part of Duniter-v2S. +// +// Duniter-v2S is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// Duniter-v2S 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>. + +#![allow(clippy::unnecessary_cast)] + +use frame_support::weights::{constants::RocksDbWeight, Weight}; + +/// Weight functions needed for pallet_universal_dividend. +pub trait WeightInfo { + fn create_oneshot_account() -> Weight; + fn consume_oneshot_account() -> Weight; + fn consume_oneshot_account_with_remaining() -> Weight; +} + +// Insecure weights implementation, use it for tests only! +impl WeightInfo for () { + // Storage: OneshotAccount OneshotAccounts (r:1 w:1) + fn create_oneshot_account() -> Weight { + (45_690_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(1 as Weight)) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } + // Storage: OneshotAccount OneshotAccounts (r:1 w:1) + // Storage: System BlockHash (r:1 w:0) + // Storage: System Account (r:1 w:1) + fn consume_oneshot_account() -> Weight { + (50_060_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(3 as Weight)) + .saturating_add(RocksDbWeight::get().writes(2 as Weight)) + } + // Storage: OneshotAccount OneshotAccounts (r:1 w:1) + // Storage: System BlockHash (r:1 w:0) + // Storage: System Account (r:2 w:2) + fn consume_oneshot_account_with_remaining() -> Weight { + (69_346_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(4 as Weight)) + .saturating_add(RocksDbWeight::get().writes(3 as Weight)) + } +} diff --git a/resources/metadata.scale b/resources/metadata.scale index 53eb7112a2ef328bbe23045d60c5ef402470ae39..10f4232185e31dcb87b88cd33a9731c8efca1265 100644 Binary files a/resources/metadata.scale and b/resources/metadata.scale differ diff --git a/runtime/common/Cargo.toml b/runtime/common/Cargo.toml index 523f684043594cacff62d11844e036fbf33bebac..660fa3223c3eac2d4ee2c656df6ca823b0ed5e83 100644 --- a/runtime/common/Cargo.toml +++ b/runtime/common/Cargo.toml @@ -39,6 +39,7 @@ std = [ 'pallet-identity/std', 'pallet-membership/std', 'pallet-multisig/std', + 'pallet-oneshot-account/std', 'pallet-provide-randomness/std', 'pallet-proxy/std', 'pallet-scheduler/std', @@ -68,6 +69,7 @@ pallet-duniter-account = { path = '../../pallets/duniter-account', default-featu pallet-duniter-wot = { path = '../../pallets/duniter-wot', default-features = false } pallet-identity = { path = '../../pallets/identity', default-features = false } pallet-membership = { path = '../../pallets/membership', default-features = false } +pallet-oneshot-account = { path = '../../pallets/oneshot-account', default-features = false } pallet-provide-randomness = { path = '../../pallets/provide-randomness', default-features = false } pallet-upgrade-origin = { path = '../../pallets/upgrade-origin', default-features = false } pallet-universal-dividend = { path = '../../pallets/universal-dividend', default-features = false } diff --git a/runtime/common/src/apis.rs b/runtime/common/src/apis.rs index e6002a5d1756c2d494750e70af8d08074f3e13d5..1aaf5690c44c70a9cf6fad756da0e1cee6d6971e 100644 --- a/runtime/common/src/apis.rs +++ b/runtime/common/src/apis.rs @@ -238,6 +238,7 @@ macro_rules! runtime_apis { list_benchmark!(list, extra, frame_system, SystemBench::<Runtime>); list_benchmark!(list, extra, pallet_balances, Balances); list_benchmark!(list, extra, pallet_multisig, Multisig); + list_benchmark!(list, extra, pallet_oneshot_account, OneshotAccount); list_benchmark!(list, extra, pallet_proxy, Proxy); list_benchmark!(list, extra, pallet_scheduler, Scheduler); list_benchmark!(list, extra, pallet_timestamp, Timestamp); @@ -284,6 +285,7 @@ macro_rules! runtime_apis { add_benchmark!(params, batches, frame_system, SystemBench::<Runtime>); add_benchmark!(params, batches, pallet_balances, Balances); add_benchmark!(params, batches, pallet_multisig, Multisig); + add_benchmark!(params, batches, pallet_oneshot_account, OneshotAccount); add_benchmark!(params, batches, pallet_proxy, Proxy); add_benchmark!(params, batches, pallet_scheduler, Scheduler); add_benchmark!(params, batches, pallet_timestamp, Timestamp); diff --git a/runtime/common/src/pallets_config.rs b/runtime/common/src/pallets_config.rs index f9e7531c7461bda911827c1a7d7a4e57db549a56..10f5feb5c8acb22c21b1147501cb22d83a6f9185 100644 --- a/runtime/common/src/pallets_config.rs +++ b/runtime/common/src/pallets_config.rs @@ -173,13 +173,19 @@ macro_rules! pallets_config { } } } + pub struct OnChargeTransaction; impl pallet_transaction_payment::Config for Runtime { - type OnChargeTransaction = CurrencyAdapter<Balances, HandleFees>; + type OnChargeTransaction = OneshotAccount; type OperationalFeeMultiplier = frame_support::traits::ConstU8<5>; type WeightToFee = common_runtime::fees::WeightToFeeImpl<Balance>; type LengthToFee = common_runtime::fees::LengthToFeeImpl<Balance>; type FeeMultiplierUpdate = (); } + impl pallet_oneshot_account::Config for Runtime { + type Currency = Balances; + type Event = Event; + type InnerOnChargeTransaction = CurrencyAdapter<Balances, HandleFees>; + } // CONSENSUS // diff --git a/runtime/common/src/weights/pallet_oneshot_account.rs b/runtime/common/src/weights/pallet_oneshot_account.rs new file mode 100644 index 0000000000000000000000000000000000000000..8a50c766419bb3d029d79538aefe8cb54ab40ff4 --- /dev/null +++ b/runtime/common/src/weights/pallet_oneshot_account.rs @@ -0,0 +1,71 @@ +// Copyright 2021-2022 Axiom-Team +// +// This file is part of Duniter-v2S. +// +// Duniter-v2S is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// Duniter-v2S 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>. + +//! Autogenerated weights for `pallet_oneshot_account` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2022-08-08, STEPS: `50`, REPEAT: 20, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Interpreted, CHAIN: Some("dev"), DB CACHE: 1024 + +// Executed Command: +// ./duniter +// benchmark +// pallet +// --chain=dev +// --steps=50 +// --repeat=20 +// --pallet=pallet_oneshot_account +// --extrinsic=* +// --execution=wasm +// --wasm-execution=interpreted-i-know-what-i-do +// --heap-pages=4096 +// --header=./file_header.txt +// --output=. + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(clippy::unnecessary_cast)] + +use frame_support::{traits::Get, weights::Weight}; +use sp_std::marker::PhantomData; + +/// Weight functions for `pallet_oneshot_account`. +pub struct WeightInfo<T>(PhantomData<T>); +impl<T: frame_system::Config> pallet_oneshot_account::WeightInfo for WeightInfo<T> { + // Storage: OneshotAccount OneshotAccounts (r:1 w:1) + fn create_oneshot_account() -> Weight { + (941_602_000 as Weight) + .saturating_add(T::DbWeight::get().reads(1 as Weight)) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + } + // Storage: OneshotAccount OneshotAccounts (r:1 w:1) + // Storage: System BlockHash (r:1 w:0) + // Storage: System Account (r:1 w:1) + fn consume_oneshot_account() -> Weight { + (971_453_000 as Weight) + .saturating_add(T::DbWeight::get().reads(3 as Weight)) + .saturating_add(T::DbWeight::get().writes(2 as Weight)) + } + // Storage: OneshotAccount OneshotAccounts (r:1 w:1) + // Storage: System BlockHash (r:1 w:0) + // Storage: System Account (r:2 w:2) + fn consume_oneshot_account_with_remaining() -> Weight { + (1_500_781_000 as Weight) + .saturating_add(T::DbWeight::get().reads(4 as Weight)) + .saturating_add(T::DbWeight::get().writes(3 as Weight)) + } +} diff --git a/runtime/g1/Cargo.toml b/runtime/g1/Cargo.toml index ee9d7049bb0c306cb081934b5eedac81ba4b3151..2ce176950be39d8c872d6f8aac56e7f9dd0c8f06 100644 --- a/runtime/g1/Cargo.toml +++ b/runtime/g1/Cargo.toml @@ -52,6 +52,7 @@ std = [ 'pallet-provide-randomness/std', 'pallet-im-online/std', 'pallet-multisig/std', + 'pallet-oneshot-account/std', 'pallet-preimage/std', 'pallet-proxy/std', 'pallet-session/std', @@ -117,6 +118,7 @@ pallet-duniter-account = { path = '../../pallets/duniter-account', default-featu pallet-duniter-wot = { path = '../../pallets/duniter-wot', default-features = false } pallet-identity = { path = '../../pallets/identity', default-features = false } pallet-membership = { path = '../../pallets/membership', default-features = false } +pallet-oneshot-account = { path = '../../pallets/oneshot-account', default-features = false } pallet-provide-randomness = { path = '../../pallets/provide-randomness', default-features = false } pallet-universal-dividend = { path = '../../pallets/universal-dividend', default-features = false } pallet-upgrade-origin = { path = '../../pallets/upgrade-origin', default-features = false } diff --git a/runtime/g1/src/lib.rs b/runtime/g1/src/lib.rs index ec455d7fad4eac0e9e16631e69c74c80e48f103d..a944041c72c15722b42f318dd04e663316614b80 100644 --- a/runtime/g1/src/lib.rs +++ b/runtime/g1/src/lib.rs @@ -111,7 +111,7 @@ pub type SignedExtra = ( frame_system::CheckTxVersion<Runtime>, frame_system::CheckGenesis<Runtime>, frame_system::CheckEra<Runtime>, - frame_system::CheckNonce<Runtime>, + pallet_oneshot_account::CheckNonce<Runtime>, frame_system::CheckWeight<Runtime>, pallet_transaction_payment::ChargeTransactionPayment<Runtime>, ); @@ -215,6 +215,7 @@ construct_runtime!( // Money management Balances: pallet_balances::{Pallet, Call, Storage, Config<T>, Event<T>} = 6, TransactionPayment: pallet_transaction_payment::{Pallet, Storage} = 32, + OneshotAccount: pallet_oneshot_account::{Pallet, Call, Storage, Event<T>} = 7, // Consensus support. AuthorityMembers: pallet_authority_members::{Pallet, Call, Storage, Config<T>, Event<T>} = 10, diff --git a/runtime/gdev/Cargo.toml b/runtime/gdev/Cargo.toml index bfdc5a59a1359da72f035967c178603f9dfc0ffe..77ff1f259b8350a6314cc04ae4da8812093863f5 100644 --- a/runtime/gdev/Cargo.toml +++ b/runtime/gdev/Cargo.toml @@ -36,6 +36,7 @@ runtime-benchmarks = [ 'pallet-provide-randomness/runtime-benchmarks', 'pallet-im-online/runtime-benchmarks', 'pallet-multisig/runtime-benchmarks', + 'pallet-oneshot-account/runtime-benchmarks', 'pallet-preimage/runtime-benchmarks', 'pallet-proxy/runtime-benchmarks', 'pallet-scheduler/runtime-benchmarks', @@ -68,6 +69,7 @@ std = [ 'pallet-grandpa/std', 'pallet-identity/std', 'pallet-membership/std', + 'pallet-oneshot-account/std', 'pallet-provide-randomness/std', 'pallet-im-online/std', 'pallet-multisig/std', @@ -139,6 +141,7 @@ pallet-duniter-account = { path = '../../pallets/duniter-account', default-featu pallet-duniter-wot = { path = '../../pallets/duniter-wot', default-features = false } pallet-identity = { path = '../../pallets/identity', default-features = false } pallet-membership = { path = '../../pallets/membership', default-features = false } +pallet-oneshot-account = { path = '../../pallets/oneshot-account', default-features = false } pallet-provide-randomness = { path = '../../pallets/provide-randomness', default-features = false } pallet-universal-dividend = { path = '../../pallets/universal-dividend', default-features = false } pallet-upgrade-origin = { path = '../../pallets/upgrade-origin', default-features = false } diff --git a/runtime/gdev/src/lib.rs b/runtime/gdev/src/lib.rs index 666469fa6295944b9408fadab618d0cbeebcb8e6..1c494414a5154922bec981e47ca84aeadc6a9445 100644 --- a/runtime/gdev/src/lib.rs +++ b/runtime/gdev/src/lib.rs @@ -115,7 +115,7 @@ pub type SignedExtra = ( frame_system::CheckTxVersion<Runtime>, frame_system::CheckGenesis<Runtime>, frame_system::CheckEra<Runtime>, - frame_system::CheckNonce<Runtime>, + pallet_oneshot_account::CheckNonce<Runtime>, frame_system::CheckWeight<Runtime>, pallet_transaction_payment::ChargeTransactionPayment<Runtime>, ); @@ -284,6 +284,7 @@ construct_runtime!( // Money management Balances: pallet_balances::{Pallet, Call, Storage, Config<T>, Event<T>} = 6, TransactionPayment: pallet_transaction_payment::{Pallet, Storage} = 32, + OneshotAccount: pallet_oneshot_account::{Pallet, Call, Storage, Event<T>} = 7, // Consensus support AuthorityMembers: pallet_authority_members::{Pallet, Call, Storage, Config<T>, Event<T>} = 10, diff --git a/runtime/gdev/tests/integration_tests.rs b/runtime/gdev/tests/integration_tests.rs index 9a1ca4833c8cfe78dd4e4a727ca122bb6895dd5d..c3c43dc8b506a9f70e0c04ba5965fc994f44f95b 100644 --- a/runtime/gdev/tests/integration_tests.rs +++ b/runtime/gdev/tests/integration_tests.rs @@ -430,3 +430,81 @@ fn test_create_new_idty_without_founds() { ); }); } + +#[test] +fn test_oneshot_accounts() { + ExtBuilder::new(1, 3, 4) + .with_initial_balances(vec![ + (AccountKeyring::Alice.to_account_id(), 1_000), + (AccountKeyring::Eve.to_account_id(), 1_000), + ]) + .build() + .execute_with(|| { + run_to_block(6); + + assert_ok!(OneshotAccount::create_oneshot_account( + frame_system::RawOrigin::Signed(AccountKeyring::Alice.to_account_id()).into(), + MultiAddress::Id(AccountKeyring::Eve.to_account_id()), + 400 + )); + assert_eq!( + Balances::free_balance(AccountKeyring::Alice.to_account_id()), + 600 + ); + run_to_block(7); + + assert_ok!(OneshotAccount::consume_oneshot_account_with_remaining( + frame_system::RawOrigin::Signed(AccountKeyring::Eve.to_account_id()).into(), + 0, + pallet_oneshot_account::Account::Oneshot(MultiAddress::Id( + AccountKeyring::Ferdie.to_account_id() + )), + pallet_oneshot_account::Account::Normal(MultiAddress::Id( + AccountKeyring::Alice.to_account_id() + )), + 300 + )); + assert_eq!( + Balances::free_balance(AccountKeyring::Alice.to_account_id()), + 700 + ); + assert_noop!( + OneshotAccount::consume_oneshot_account( + frame_system::RawOrigin::Signed(AccountKeyring::Eve.to_account_id()).into(), + 0, + pallet_oneshot_account::Account::Oneshot(MultiAddress::Id( + AccountKeyring::Ferdie.to_account_id() + )), + ), + pallet_oneshot_account::Error::<Runtime>::OneshotAccountNotExist + ); + run_to_block(8); + // Oneshot account consumption should not increment the nonce + assert_eq!( + System::account(AccountKeyring::Eve.to_account_id()).nonce, + 0 + ); + + assert_ok!(OneshotAccount::consume_oneshot_account( + frame_system::RawOrigin::Signed(AccountKeyring::Ferdie.to_account_id()).into(), + 0, + pallet_oneshot_account::Account::Normal(MultiAddress::Id( + AccountKeyring::Alice.to_account_id() + )), + )); + assert_eq!( + Balances::free_balance(AccountKeyring::Alice.to_account_id()), + 1000 + ); + assert_noop!( + OneshotAccount::consume_oneshot_account( + frame_system::RawOrigin::Signed(AccountKeyring::Eve.to_account_id()).into(), + 0, + pallet_oneshot_account::Account::Normal(MultiAddress::Id( + AccountKeyring::Alice.to_account_id() + )), + ), + pallet_oneshot_account::Error::<Runtime>::OneshotAccountNotExist + ); + }); +} diff --git a/runtime/gtest/Cargo.toml b/runtime/gtest/Cargo.toml index 30be9e5ebb1cef2990d976df0576031588775901..4d40f3b25d5cd06ab13eb7cc12853abddfaccab9 100644 --- a/runtime/gtest/Cargo.toml +++ b/runtime/gtest/Cargo.toml @@ -49,6 +49,7 @@ std = [ 'pallet-grandpa/std', 'pallet-identity/std', 'pallet-membership/std', + 'pallet-oneshot-account/std', 'pallet-provide-randomness/std', 'pallet-im-online/std', 'pallet-multisig/std', @@ -117,6 +118,7 @@ pallet-duniter-account = { path = '../../pallets/duniter-account', default-featu pallet-duniter-wot = { path = '../../pallets/duniter-wot', default-features = false } pallet-identity = { path = '../../pallets/identity', default-features = false } pallet-membership = { path = '../../pallets/membership', default-features = false } +pallet-oneshot-account = { path = '../../pallets/oneshot-account', default-features = false } pallet-provide-randomness = { path = '../../pallets/provide-randomness', default-features = false } pallet-universal-dividend = { path = '../../pallets/universal-dividend', default-features = false } pallet-upgrade-origin = { path = '../../pallets/upgrade-origin', default-features = false } diff --git a/runtime/gtest/src/lib.rs b/runtime/gtest/src/lib.rs index fd21f986d2969fe69a03acb45788f81a4d1763de..efb4fb2f48631cea8b5bc0c820e84502275be3f1 100644 --- a/runtime/gtest/src/lib.rs +++ b/runtime/gtest/src/lib.rs @@ -216,6 +216,7 @@ construct_runtime!( // Money management Balances: pallet_balances::{Pallet, Call, Storage, Config<T>, Event<T>} = 6, TransactionPayment: pallet_transaction_payment::{Pallet, Storage} = 32, + OneshotAccount: pallet_oneshot_account::{Pallet, Call, Storage, Event<T>} = 7, // Consensus support. AuthorityMembers: pallet_authority_members::{Pallet, Call, Storage, Config<T>, Event<T>} = 10,