From 940093d3c53bfa3bbc5497d28a1919cfaa744c1a Mon Sep 17 00:00:00 2001 From: tuxmain <tuxmain@zettascript.org> Date: Thu, 4 Aug 2022 17:13:39 +0200 Subject: [PATCH] feat(oneshot-account): Pallet oneshot-account --- Cargo.lock | 21 + Cargo.toml | 1 + .../cucumber-features/oneshot_account.feature | 21 + end2end-tests/tests/common/mod.rs | 1 + end2end-tests/tests/common/oneshot.rs | 116 ++++++ end2end-tests/tests/cucumber_tests.rs | 105 ++++- pallets/oneshot-account/Cargo.toml | 84 ++++ pallets/oneshot-account/src/benchmarking.rs | 138 +++++++ pallets/oneshot-account/src/check_nonce.rs | 86 ++++ pallets/oneshot-account/src/lib.rs | 377 ++++++++++++++++++ pallets/oneshot-account/src/mock.rs | 123 ++++++ pallets/oneshot-account/src/types.rs | 24 ++ pallets/oneshot-account/src/weights.rs | 52 +++ resources/metadata.scale | Bin 122124 -> 125548 bytes runtime/common/Cargo.toml | 2 + runtime/common/src/pallets_config.rs | 8 +- .../src/weights/pallet_oneshot_account.rs | 70 ++++ runtime/g1/Cargo.toml | 2 + runtime/g1/src/lib.rs | 3 +- runtime/gdev/Cargo.toml | 2 + runtime/gdev/src/lib.rs | 5 +- runtime/gdev/tests/integration_tests.rs | 78 ++++ runtime/gtest/Cargo.toml | 2 + runtime/gtest/src/lib.rs | 1 + 24 files changed, 1317 insertions(+), 5 deletions(-) create mode 100644 end2end-tests/cucumber-features/oneshot_account.feature create mode 100644 end2end-tests/tests/common/oneshot.rs create mode 100644 pallets/oneshot-account/Cargo.toml create mode 100644 pallets/oneshot-account/src/benchmarking.rs create mode 100644 pallets/oneshot-account/src/check_nonce.rs create mode 100644 pallets/oneshot-account/src/lib.rs create mode 100644 pallets/oneshot-account/src/mock.rs create mode 100644 pallets/oneshot-account/src/types.rs create mode 100644 pallets/oneshot-account/src/weights.rs create mode 100644 runtime/common/src/weights/pallet_oneshot_account.rs diff --git a/Cargo.lock b/Cargo.lock index 0029d6e7f..427dd1171 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,23 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-oneshot-account" +version = "3.0.0" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "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 d6ddf72f3..63920b577 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 000000000..f3d74b790 --- /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 56317abf6..fb464d5a7 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 000000000..b8827fe88 --- /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 265b78295..303787361 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 000000000..bba1f932d --- /dev/null +++ b/pallets/oneshot-account/Cargo.toml @@ -0,0 +1,84 @@ +[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'] +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"] } + +# 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' diff --git a/pallets/oneshot-account/src/benchmarking.rs b/pallets/oneshot-account/src/benchmarking.rs new file mode 100644 index 000000000..fab093943 --- /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>::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>::Normal(recipient1_lookup), + Account::<T>::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 000000000..056f616f2 --- /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 = "OneshotAccountCheckNonce"; + + 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 000000000..8622faef3 --- /dev/null +++ b/pallets/oneshot-account/src/lib.rs @@ -0,0 +1,377 @@ +// 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 check_nonce; +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 000000000..07ea86c70 --- /dev/null +++ b/pallets/oneshot-account/src/mock.rs @@ -0,0 +1,123 @@ +// 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, +}; + +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 TransactionByteFee = (); + type OperationalFeeMultiplier = frame_support::traits::ConstU8<5>; + type WeightToFee = 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 000000000..753448465 --- /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 000000000..ff5e2930e --- /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_two_dests() -> 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..6550da100926ebbbe9df51ce1996f7a69c682a6b 100644 GIT binary patch delta 10278 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 delta 7300 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 diff --git a/runtime/common/Cargo.toml b/runtime/common/Cargo.toml index 523f68404..660fa3223 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/pallets_config.rs b/runtime/common/src/pallets_config.rs index f9e7531c7..10f5feb5c 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 000000000..c10f189ad --- /dev/null +++ b/runtime/common/src/weights/pallet_oneshot_account.rs @@ -0,0 +1,70 @@ +// 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-06-28, STEPS: `50`, REPEAT: 20, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 + +// Executed Command: +// ./target/release/duniter +// benchmark +// --chain=dev +// --steps=50 +// --repeat=20 +// --pallet=pallet_oneshot-account +// --extrinsic=* +// --execution=wasm +// --wasm-execution=compiled +// --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 { + (45_690_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 { + (50_060_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 { + (69_346_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 ee9d7049b..2ce176950 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 ec455d7fa..a944041c7 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 bfdc5a59a..c28e43875 100644 --- a/runtime/gdev/Cargo.toml +++ b/runtime/gdev/Cargo.toml @@ -68,6 +68,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 +140,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 666469fa6..9f6f5ddcd 100644 --- a/runtime/gdev/src/lib.rs +++ b/runtime/gdev/src/lib.rs @@ -115,9 +115,9 @@ 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>, + //pallet_transaction_payment::ChargeTransactionPayment<Runtime>, ); /// Executive: handles dispatch to the various modules. @@ -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 9a1ca4833..c3c43dc8b 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 30be9e5eb..4d40f3b25 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 fd21f986d..efb4fb2f4 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, -- GitLab