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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 B{Fnd$ delta 7300 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 Q{!Os;sd6-{D_Ye51NbQKXaE2J 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