From c89b598eb60ad6282fcafe82c1aa9fe5f30b6358 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Eng=C3=A9libert?= <tuxmain@zettascript.org> Date: Tue, 9 Aug 2022 02:11:19 +0200 Subject: [PATCH] feat(runtime): add pallet oneshot accounts (!51) * run real benchmarks for pallet oneshot * fix(oneshot-account): use benchmarking * fix: metadata should comply subxt & polkadotjs expectations * feat(oneshot-account): Pallet oneshot-account --- Cargo.lock | 22 + 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 | 94 +++++ pallets/oneshot-account/src/benchmarking.rs | 138 +++++++ pallets/oneshot-account/src/check_nonce.rs | 86 ++++ pallets/oneshot-account/src/lib.rs | 380 ++++++++++++++++++ pallets/oneshot-account/src/mock.rs | 124 ++++++ pallets/oneshot-account/src/types.rs | 24 ++ pallets/oneshot-account/src/weights.rs | 52 +++ resources/metadata.scale | Bin 122124 -> 125644 bytes runtime/common/Cargo.toml | 2 + runtime/common/src/apis.rs | 2 + runtime/common/src/pallets_config.rs | 8 +- .../src/weights/pallet_oneshot_account.rs | 71 ++++ runtime/g1/Cargo.toml | 2 + runtime/g1/src/lib.rs | 3 +- runtime/gdev/Cargo.toml | 3 + runtime/gdev/src/lib.rs | 3 +- runtime/gdev/tests/integration_tests.rs | 78 ++++ runtime/gtest/Cargo.toml | 2 + runtime/gtest/src/lib.rs | 1 + 25 files changed, 1335 insertions(+), 4 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..6e5f707fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -887,6 +887,7 @@ dependencies = [ "pallet-identity", "pallet-membership", "pallet-multisig", + "pallet-oneshot-account", "pallet-provide-randomness", "pallet-proxy", "pallet-scheduler", @@ -2426,6 +2427,7 @@ dependencies = [ "pallet-membership", "pallet-multisig", "pallet-offences", + "pallet-oneshot-account", "pallet-preimage", "pallet-provide-randomness", "pallet-proxy", @@ -2494,6 +2496,7 @@ dependencies = [ "pallet-membership", "pallet-multisig", "pallet-offences", + "pallet-oneshot-account", "pallet-preimage", "pallet-provide-randomness", "pallet-proxy", @@ -2776,6 +2779,7 @@ dependencies = [ "pallet-membership", "pallet-multisig", "pallet-offences", + "pallet-oneshot-account", "pallet-preimage", "pallet-provide-randomness", "pallet-proxy", @@ -5309,6 +5313,24 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-oneshot-account" +version = "3.0.0" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "pallet-balances", + "pallet-transaction-payment", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-preimage" version = "4.0.0-dev" diff --git a/Cargo.toml b/Cargo.toml index 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..cde1128d8 --- /dev/null +++ b/pallets/oneshot-account/Cargo.toml @@ -0,0 +1,94 @@ +[package] +authors = ['librelois <c@elo.tf>'] +description = 'FRAME pallet oneshot account.' +edition = '2018' +homepage = 'https://substrate.dev' +license = 'AGPL-3.0' +name = 'pallet-oneshot-account' +readme = 'README.md' +repository = 'https://git.duniter.org/nodes/rust/duniter-v2s' +version = '3.0.0' + +[features] +default = ['std'] +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "pallet-balances", +] +std = [ + 'codec/std', + 'frame-support/std', + 'frame-system/std', + 'frame-benchmarking/std', + 'sp-core/std', + 'sp-io/std', + 'sp-runtime/std', + 'sp-std/std', +] +try-runtime = ['frame-support/try-runtime'] + +[dependencies] +# crates.io +codec = { package = 'parity-scale-codec', version = "3.1.5", default-features = false, features = ["derive"] } +log = { version = "0.4.14", default-features = false } +scale-info = { version = "2.1.1", default-features = false, features = ["derive"] } + +# benchmarks +pallet-balances = { git = 'https://github.com/duniter/substrate', branch = 'duniter-substrate-v0.9.23', optional = true, default-features = false } + +# substrate +[dependencies.frame-benchmarking] +default-features = false +git = 'https://github.com/duniter/substrate' +optional = true +branch = 'duniter-substrate-v0.9.23' + +[dependencies.frame-support] +default-features = false +git = 'https://github.com/duniter/substrate' +branch = 'duniter-substrate-v0.9.23' + +[dependencies.frame-system] +default-features = false +git = 'https://github.com/duniter/substrate' +branch = 'duniter-substrate-v0.9.23' + +[dependencies.pallet-transaction-payment] +default-features = false +git = 'https://github.com/duniter/substrate' +branch = 'duniter-substrate-v0.9.23' + +[dependencies.sp-core] +default-features = false +git = 'https://github.com/duniter/substrate' +branch = 'duniter-substrate-v0.9.23' + +[dependencies.sp-io] +default-features = false +git = 'https://github.com/duniter/substrate' +branch = 'duniter-substrate-v0.9.23' + +[dependencies.sp-runtime] +default-features = false +git = 'https://github.com/duniter/substrate' +branch = 'duniter-substrate-v0.9.23' + +[dependencies.sp-std] +default-features = false +git = 'https://github.com/duniter/substrate' +branch = 'duniter-substrate-v0.9.23' + +### DOC ### + +[package.metadata.docs.rs] +targets = ['x86_64-unknown-linux-gnu'] + +### DEV ### + +[dev-dependencies.sp-io] +git = 'https://github.com/duniter/substrate' +branch = 'duniter-substrate-v0.9.23' + +[dev-dependencies.pallet-balances] +git = 'https://github.com/duniter/substrate' +branch = 'duniter-substrate-v0.9.23' diff --git a/pallets/oneshot-account/src/benchmarking.rs b/pallets/oneshot-account/src/benchmarking.rs new file mode 100644 index 000000000..a72c65851 --- /dev/null +++ b/pallets/oneshot-account/src/benchmarking.rs @@ -0,0 +1,138 @@ +// Copyright 2021-2022 Axiom-Team +// +// This file is part of Duniter-v2S. +// +// Duniter-v2S is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// Duniter-v2S is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>. + +#![cfg(feature = "runtime-benchmarks")] + +use super::*; + +use frame_benchmarking::{account, benchmarks, whitelisted_caller}; +use frame_support::pallet_prelude::IsType; +use frame_support::traits::Get; +use frame_system::RawOrigin; +use pallet_balances::Pallet as Balances; + +use crate::Pallet; + +const SEED: u32 = 0; + +benchmarks! { + where_clause { where + T: pallet_balances::Config, + T::Balance: From<u64>, + <T::Currency as Currency<T::AccountId>>::Balance: IsType<T::Balance> + } + create_oneshot_account { + let existential_deposit = T::ExistentialDeposit::get(); + let caller = whitelisted_caller(); + + // Give some multiple of the existential deposit + let balance = existential_deposit.saturating_mul((2).into()); + let _ = T::Currency::make_free_balance_be(&caller, balance.into()); + + let recipient: T::AccountId = account("recipient", 0, SEED); + let recipient_lookup: <T::Lookup as StaticLookup>::Source = + T::Lookup::unlookup(recipient.clone()); + let transfer_amount = existential_deposit; + }: _(RawOrigin::Signed(caller.clone()), recipient_lookup, transfer_amount.into()) + verify { + assert_eq!(Balances::<T>::free_balance(&caller), transfer_amount); + assert_eq!(OneshotAccounts::<T>::get(&recipient), Some(transfer_amount.into())); + } + where_clause { where + T: pallet_balances::Config, + T::Balance: From<u64>, + <T::Currency as Currency<T::AccountId>>::Balance: IsType<T::Balance>+From<T::Balance> + } + consume_oneshot_account { + let existential_deposit = T::ExistentialDeposit::get(); + let caller: T::AccountId = whitelisted_caller(); + + // Give some multiple of the existential deposit + let balance = existential_deposit.saturating_mul((2).into()); + OneshotAccounts::<T>::insert( + caller.clone(), + Into::<<T::Currency as Currency<T::AccountId>>::Balance>::into(balance) + ); + + // Deposit into a normal account is more expensive than into a oneshot account + // so we create the recipient account with an existential deposit. + let recipient: T::AccountId = account("recipient", 0, SEED); + let recipient_lookup: <T::Lookup as StaticLookup>::Source = + T::Lookup::unlookup(recipient.clone()); + let _ = T::Currency::make_free_balance_be(&recipient, existential_deposit.into()); + }: _( + RawOrigin::Signed(caller.clone()), + T::BlockNumber::zero(), + Account::<<T::Lookup as StaticLookup>::Source>::Normal(recipient_lookup) + ) + verify { + assert_eq!(OneshotAccounts::<T>::get(&caller), None); + assert_eq!( + Balances::<T>::free_balance(&recipient), + existential_deposit.saturating_mul((3).into()) + ); + } + where_clause { where + T: pallet_balances::Config, + T::Balance: From<u64>, + <T::Currency as Currency<T::AccountId>>::Balance: IsType<T::Balance>+From<T::Balance> + } + consume_oneshot_account_with_remaining { + let existential_deposit = T::ExistentialDeposit::get(); + let caller: T::AccountId = whitelisted_caller(); + + // Give some multiple of the existential deposit + let balance = existential_deposit.saturating_mul((2).into()); + OneshotAccounts::<T>::insert( + caller.clone(), + Into::<<T::Currency as Currency<T::AccountId>>::Balance>::into(balance) + ); + + // Deposit into a normal account is more expensive than into a oneshot account + // so we create the recipient accounts with an existential deposits. + let recipient1: T::AccountId = account("recipient1", 0, SEED); + let recipient1_lookup: <T::Lookup as StaticLookup>::Source = + T::Lookup::unlookup(recipient1.clone()); + let _ = T::Currency::make_free_balance_be(&recipient1, existential_deposit.into()); + let recipient2: T::AccountId = account("recipient2", 1, SEED); + let recipient2_lookup: <T::Lookup as StaticLookup>::Source = + T::Lookup::unlookup(recipient2.clone()); + let _ = T::Currency::make_free_balance_be(&recipient2, existential_deposit.into()); + }: _( + RawOrigin::Signed(caller.clone()), + T::BlockNumber::zero(), + Account::<<T::Lookup as StaticLookup>::Source>::Normal(recipient1_lookup), + Account::<<T::Lookup as StaticLookup>::Source>::Normal(recipient2_lookup), + existential_deposit.into() + ) + verify { + assert_eq!(OneshotAccounts::<T>::get(&caller), None); + assert_eq!( + Balances::<T>::free_balance(&recipient1), + existential_deposit.saturating_mul((2).into()) + ); + assert_eq!( + Balances::<T>::free_balance(&recipient2), + existential_deposit.saturating_mul((2).into()) + ); + } + + impl_benchmark_test_suite!( + Pallet, + crate::mock::new_test_ext(), + crate::mock::Test + ); +} diff --git a/pallets/oneshot-account/src/check_nonce.rs b/pallets/oneshot-account/src/check_nonce.rs new file mode 100644 index 000000000..d6325ab4d --- /dev/null +++ b/pallets/oneshot-account/src/check_nonce.rs @@ -0,0 +1,86 @@ +// Copyright 2021 Axiom-Team +// +// This file is part of Substrate-Libre-Currency. +// +// Substrate-Libre-Currency is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// Substrate-Libre-Currency is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with Substrate-Libre-Currency. If not, see <https://www.gnu.org/licenses/>. + +use crate::Config; + +use codec::{Decode, Encode}; +use frame_support::traits::IsSubType; +use frame_support::weights::DispatchInfo; +//use frame_system::Config; +use scale_info::TypeInfo; +use sp_runtime::{ + traits::{DispatchInfoOf, Dispatchable, SignedExtension}, + transaction_validity::{TransactionValidity, TransactionValidityError}, +}; + +#[derive(Encode, Decode, Clone, Eq, PartialEq, TypeInfo)] +#[scale_info(skip_type_params(Runtime))] +pub struct CheckNonce<T: Config>(pub frame_system::CheckNonce<T>); + +impl<T: Config> sp_std::fmt::Debug for CheckNonce<T> { + #[cfg(feature = "std")] + fn fmt(&self, f: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result { + write!(f, "CheckNonce({})", self.0 .0) + } + + #[cfg(not(feature = "std"))] + fn fmt(&self, _: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result { + Ok(()) + } +} + +impl<T: Config + TypeInfo> SignedExtension for CheckNonce<T> +where + T::Call: Dispatchable<Info = DispatchInfo> + IsSubType<crate::Call<T>>, +{ + type AccountId = <T as frame_system::Config>::AccountId; + type Call = <T as frame_system::Config>::Call; + type AdditionalSigned = (); + type Pre = (); + const IDENTIFIER: &'static str = "CheckNonce"; + + fn additional_signed(&self) -> sp_std::result::Result<(), TransactionValidityError> { + self.0.additional_signed() + } + + fn pre_dispatch( + self, + who: &Self::AccountId, + call: &Self::Call, + info: &DispatchInfoOf<Self::Call>, + len: usize, + ) -> Result<(), TransactionValidityError> { + if let Some( + crate::Call::consume_oneshot_account { .. } + | crate::Call::consume_oneshot_account_with_remaining { .. }, + ) = call.is_sub_type() + { + Ok(()) + } else { + self.0.pre_dispatch(who, call, info, len) + } + } + + fn validate( + &self, + who: &Self::AccountId, + call: &Self::Call, + info: &DispatchInfoOf<Self::Call>, + len: usize, + ) -> TransactionValidity { + self.0.validate(who, call, info, len) + } +} diff --git a/pallets/oneshot-account/src/lib.rs b/pallets/oneshot-account/src/lib.rs new file mode 100644 index 000000000..0a65c42ba --- /dev/null +++ b/pallets/oneshot-account/src/lib.rs @@ -0,0 +1,380 @@ +// Copyright 2021 Axiom-Team +// +// This file is part of Substrate-Libre-Currency. +// +// Substrate-Libre-Currency is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// Substrate-Libre-Currency is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with Substrate-Libre-Currency. If not, see <https://www.gnu.org/licenses/>. + +#![cfg_attr(not(feature = "std"), no_std)] + +mod benchmarking; +mod check_nonce; +#[cfg(test)] +mod mock; +mod types; + +pub use check_nonce::CheckNonce; +pub use pallet::*; +pub use types::*; + +use frame_support::pallet_prelude::*; +use frame_support::traits::{ + Currency, ExistenceRequirement, Imbalance, IsSubType, WithdrawReasons, +}; +use frame_system::pallet_prelude::*; +use pallet_transaction_payment::OnChargeTransaction; +use sp_runtime::traits::{DispatchInfoOf, PostDispatchInfoOf, Saturating, StaticLookup, Zero}; +use sp_std::convert::TryInto; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::traits::StorageVersion; + + /// The current storage version. + const STORAGE_VERSION: StorageVersion = StorageVersion::new(1); + + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + #[pallet::storage_version(STORAGE_VERSION)] + #[pallet::without_storage_info] + pub struct Pallet<T>(_); + + // CONFIG // + + #[pallet::config] + pub trait Config: frame_system::Config + pallet_transaction_payment::Config { + type Currency: Currency<Self::AccountId>; + type Event: From<Event<Self>> + IsType<<Self as frame_system::Config>::Event>; + type InnerOnChargeTransaction: OnChargeTransaction<Self>; + } + + // STORAGE // + + #[pallet::storage] + #[pallet::getter(fn oneshot_account)] + pub type OneshotAccounts<T: Config> = StorageMap< + _, + Blake2_128Concat, + T::AccountId, + <T::Currency as Currency<T::AccountId>>::Balance, + OptionQuery, + >; + + // EVENTS // + + #[allow(clippy::type_complexity)] + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event<T: Config> { + OneshotAccountCreated { + account: T::AccountId, + balance: <T::Currency as Currency<T::AccountId>>::Balance, + creator: T::AccountId, + }, + OneshotAccountConsumed { + account: T::AccountId, + dest1: ( + T::AccountId, + <T::Currency as Currency<T::AccountId>>::Balance, + ), + dest2: Option<( + T::AccountId, + <T::Currency as Currency<T::AccountId>>::Balance, + )>, + }, + Withdraw { + account: T::AccountId, + balance: <T::Currency as Currency<T::AccountId>>::Balance, + }, + } + + // ERRORS // + + #[pallet::error] + pub enum Error<T> { + /// Block height is in the future + BlockHeightInFuture, + /// Block height is too old + BlockHeightTooOld, + /// Destination account does not exist + DestAccountNotExist, + /// Destination account has balance less than existential deposit + ExistentialDeposit, + /// Source account has insufficient balance + InsufficientBalance, + /// Destination oneshot account already exists + OneshotAccountAlreadyCreated, + /// Source oneshot account does not exist + OneshotAccountNotExist, + } + + // CALLS // + #[pallet::call] + impl<T: Config> Pallet<T> { + /// Create an account that can only be consumed once + /// + /// - `dest`: The oneshot account to be created. + /// - `balance`: The balance to be transfered to this oneshot account. + /// + /// Origin account is kept alive. + #[pallet::weight(500_000_000)] + pub fn create_oneshot_account( + origin: OriginFor<T>, + dest: <T::Lookup as StaticLookup>::Source, + #[pallet::compact] value: <T::Currency as Currency<T::AccountId>>::Balance, + ) -> DispatchResult { + let transactor = ensure_signed(origin)?; + let dest = T::Lookup::lookup(dest)?; + + ensure!( + value >= <T::Currency as Currency<T::AccountId>>::minimum_balance(), + Error::<T>::ExistentialDeposit + ); + ensure!( + OneshotAccounts::<T>::get(&dest).is_none(), + Error::<T>::OneshotAccountAlreadyCreated + ); + + <T::Currency as Currency<T::AccountId>>::withdraw( + &transactor, + value, + WithdrawReasons::TRANSFER, + ExistenceRequirement::KeepAlive, + )?; + OneshotAccounts::<T>::insert(&dest, value); + Self::deposit_event(Event::OneshotAccountCreated { + account: dest, + balance: value, + creator: transactor, + }); + + Ok(()) + } + /// Consume a oneshot account and transfer its balance to an account + /// + /// - `block_height`: Must be a recent block number. The limit is `BlockHashCount` in the past. (this is to prevent replay attacks) + /// - `dest`: The destination account. + /// - `dest_is_oneshot`: If set to `true`, then a oneshot account is created at `dest`. Else, `dest` has to be an existing account. + #[pallet::weight(500_000_000)] + pub fn consume_oneshot_account( + origin: OriginFor<T>, + block_height: T::BlockNumber, + dest: Account<<T::Lookup as StaticLookup>::Source>, + ) -> DispatchResult { + let transactor = ensure_signed(origin)?; + + let (dest, dest_is_oneshot) = match dest { + Account::Normal(account) => (account, false), + Account::Oneshot(account) => (account, true), + }; + let dest = T::Lookup::lookup(dest)?; + + let value = OneshotAccounts::<T>::take(&transactor) + .ok_or(Error::<T>::OneshotAccountNotExist)?; + + ensure!( + block_height <= frame_system::Pallet::<T>::block_number(), + Error::<T>::BlockHeightInFuture + ); + ensure!( + frame_system::pallet::BlockHash::<T>::contains_key(block_height), + Error::<T>::BlockHeightTooOld + ); + if dest_is_oneshot { + ensure!( + OneshotAccounts::<T>::get(&dest).is_none(), + Error::<T>::OneshotAccountAlreadyCreated + ); + OneshotAccounts::<T>::insert(&dest, value); + Self::deposit_event(Event::OneshotAccountCreated { + account: dest.clone(), + balance: value, + creator: transactor.clone(), + }); + } else { + <T::Currency as Currency<T::AccountId>>::deposit_into_existing(&dest, value)?; + } + OneshotAccounts::<T>::remove(&transactor); + Self::deposit_event(Event::OneshotAccountConsumed { + account: transactor, + dest1: (dest, value), + dest2: None, + }); + + Ok(()) + } + /// Consume a oneshot account then transfer some amount to an account, + /// and the remaining amount to another account. + /// + /// - `block_height`: Must be a recent block number. + /// The limit is `BlockHashCount` in the past. (this is to prevent replay attacks) + /// - `dest`: The destination account. + /// - `dest_is_oneshot`: If set to `true`, then a oneshot account is created at `dest`. Else, `dest` has to be an existing account. + /// - `dest2`: The second destination account. + /// - `dest2_is_oneshot`: If set to `true`, then a oneshot account is created at `dest2`. Else, `dest2` has to be an existing account. + /// - `balance1`: The amount transfered to `dest`, the leftover being transfered to `dest2`. + #[pallet::weight(500_000_000)] + pub fn consume_oneshot_account_with_remaining( + origin: OriginFor<T>, + block_height: T::BlockNumber, + dest: Account<<T::Lookup as StaticLookup>::Source>, + remaining_to: Account<<T::Lookup as StaticLookup>::Source>, + #[pallet::compact] balance: <T::Currency as Currency<T::AccountId>>::Balance, + ) -> DispatchResult { + let transactor = ensure_signed(origin)?; + + let (dest1, dest1_is_oneshot) = match dest { + Account::Normal(account) => (account, false), + Account::Oneshot(account) => (account, true), + }; + let dest1 = T::Lookup::lookup(dest1)?; + let (dest2, dest2_is_oneshot) = match remaining_to { + Account::Normal(account) => (account, false), + Account::Oneshot(account) => (account, true), + }; + let dest2 = T::Lookup::lookup(dest2)?; + + let value = OneshotAccounts::<T>::take(&transactor) + .ok_or(Error::<T>::OneshotAccountNotExist)?; + + let balance1 = balance; + ensure!(value > balance1, Error::<T>::InsufficientBalance); + let balance2 = value.saturating_sub(balance1); + ensure!( + block_height <= frame_system::Pallet::<T>::block_number(), + Error::<T>::BlockHeightInFuture + ); + ensure!( + frame_system::pallet::BlockHash::<T>::contains_key(block_height), + Error::<T>::BlockHeightTooOld + ); + if dest1_is_oneshot { + ensure!( + OneshotAccounts::<T>::get(&dest1).is_none(), + Error::<T>::OneshotAccountAlreadyCreated + ); + ensure!( + balance1 >= <T::Currency as Currency<T::AccountId>>::minimum_balance(), + Error::<T>::ExistentialDeposit + ); + } else { + ensure!( + !<T::Currency as Currency<T::AccountId>>::free_balance(&dest1).is_zero(), + Error::<T>::DestAccountNotExist + ); + } + if dest2_is_oneshot { + ensure!( + OneshotAccounts::<T>::get(&dest2).is_none(), + Error::<T>::OneshotAccountAlreadyCreated + ); + ensure!( + balance2 >= <T::Currency as Currency<T::AccountId>>::minimum_balance(), + Error::<T>::ExistentialDeposit + ); + OneshotAccounts::<T>::insert(&dest2, balance2); + Self::deposit_event(Event::OneshotAccountCreated { + account: dest2.clone(), + balance: balance2, + creator: transactor.clone(), + }); + } else { + <T::Currency as Currency<T::AccountId>>::deposit_into_existing(&dest2, balance2)?; + } + if dest1_is_oneshot { + OneshotAccounts::<T>::insert(&dest1, balance1); + Self::deposit_event(Event::OneshotAccountCreated { + account: dest1.clone(), + balance: balance1, + creator: transactor.clone(), + }); + } else { + <T::Currency as Currency<T::AccountId>>::deposit_into_existing(&dest1, balance1)?; + } + OneshotAccounts::<T>::remove(&transactor); + Self::deposit_event(Event::OneshotAccountConsumed { + account: transactor, + dest1: (dest1, balance1), + dest2: Some((dest2, balance2)), + }); + + Ok(()) + } + } +} + +impl<T: Config> OnChargeTransaction<T> for Pallet<T> +where + T::Call: IsSubType<Call<T>>, + T::InnerOnChargeTransaction: OnChargeTransaction< + T, + Balance = <T::Currency as Currency<T::AccountId>>::Balance, + LiquidityInfo = Option<<T::Currency as Currency<T::AccountId>>::NegativeImbalance>, + >, +{ + type Balance = <T::Currency as Currency<T::AccountId>>::Balance; + type LiquidityInfo = Option<<T::Currency as Currency<T::AccountId>>::NegativeImbalance>; + fn withdraw_fee( + who: &T::AccountId, + call: &T::Call, + dispatch_info: &DispatchInfoOf<T::Call>, + fee: Self::Balance, + tip: Self::Balance, + ) -> Result<Self::LiquidityInfo, TransactionValidityError> { + if let Some( + Call::consume_oneshot_account { .. } + | Call::consume_oneshot_account_with_remaining { .. }, + ) = call.is_sub_type() + { + if fee.is_zero() { + return Ok(None); + } + + if let Some(balance) = OneshotAccounts::<T>::get(&who) { + if balance >= fee { + OneshotAccounts::<T>::insert(who, balance.saturating_sub(fee)); + Self::deposit_event(Event::Withdraw { + account: who.clone(), + balance: fee, + }); + // TODO + return Ok(Some( + <T::Currency as Currency<T::AccountId>>::NegativeImbalance::zero(), + )); + } + } + Err(TransactionValidityError::Invalid( + InvalidTransaction::Payment, + )) + } else { + T::InnerOnChargeTransaction::withdraw_fee(who, call, dispatch_info, fee, tip) + } + } + fn correct_and_deposit_fee( + who: &T::AccountId, + dispatch_info: &DispatchInfoOf<T::Call>, + post_info: &PostDispatchInfoOf<T::Call>, + corrected_fee: Self::Balance, + tip: Self::Balance, + already_withdrawn: Self::LiquidityInfo, + ) -> Result<(), TransactionValidityError> { + T::InnerOnChargeTransaction::correct_and_deposit_fee( + who, + dispatch_info, + post_info, + corrected_fee, + tip, + already_withdrawn, + ) + } +} diff --git a/pallets/oneshot-account/src/mock.rs b/pallets/oneshot-account/src/mock.rs new file mode 100644 index 000000000..35b69d7ae --- /dev/null +++ b/pallets/oneshot-account/src/mock.rs @@ -0,0 +1,124 @@ +// Copyright 2021 Axiom-Team +// +// This file is part of Substrate-Libre-Currency. +// +// Substrate-Libre-Currency is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// Substrate-Libre-Currency is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with Substrate-Libre-Currency. If not, see <https://www.gnu.org/licenses/>. + +use crate::{self as pallet_oneshot_account}; +use frame_support::{parameter_types, traits::Everything, weights::IdentityFee}; +use frame_system as system; +use pallet_transaction_payment::CurrencyAdapter; +use sp_core::H256; +use sp_runtime::{ + testing::Header, + traits::{BlakeTwo256, IdentityLookup}, + BuildStorage, +}; +use sp_std::convert::{TryFrom, TryInto}; + +type Balance = u64; +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<Test>; +type Block = frame_system::mocking::MockBlock<Test>; + +// Configure a mock runtime to test the pallet. +frame_support::construct_runtime!( + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system::{Pallet, Call, Config, Storage, Event<T>}, + Balances: pallet_balances::{Pallet, Call, Storage, Config<T>, Event<T>}, + TransactionPayment: pallet_transaction_payment::{Pallet, Storage}, + OneshotAccount: pallet_oneshot_account::{Pallet, Call, Storage, Event<T>}, + } +); + +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub const SS58Prefix: u8 = 42; +} + +impl system::Config for Test { + type BaseCallFilter = Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type Origin = Origin; + type Call = Call; + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup<Self::AccountId>; + type Header = Header; + type Event = Event; + type BlockHashCount = BlockHashCount; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData<Balance>; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = SS58Prefix; + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} + +parameter_types! { + pub const ExistentialDeposit: Balance = 10; + pub const MaxLocks: u32 = 50; +} + +impl pallet_balances::Config for Test { + type Balance = Balance; + type DustRemoval = (); + type Event = Event; + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type WeightInfo = pallet_balances::weights::SubstrateWeight<Test>; + type MaxLocks = MaxLocks; + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; +} +impl pallet_transaction_payment::Config for Test { + type OnChargeTransaction = OneshotAccount; + type OperationalFeeMultiplier = frame_support::traits::ConstU8<5>; + type WeightToFee = IdentityFee<u64>; + type LengthToFee = IdentityFee<u64>; + type FeeMultiplierUpdate = (); +} +impl pallet_oneshot_account::Config for Test { + type Currency = Balances; + type Event = Event; + type InnerOnChargeTransaction = CurrencyAdapter<Balances, HandleFees>; +} + +pub struct HandleFees; +type NegativeImbalance = <Balances as frame_support::traits::Currency<u64>>::NegativeImbalance; +impl frame_support::traits::OnUnbalanced<NegativeImbalance> for HandleFees { + fn on_nonzero_unbalanced(_amount: NegativeImbalance) {} +} + +// Build genesis storage according to the mock runtime. +#[allow(dead_code)] +pub fn new_test_ext() -> sp_io::TestExternalities { + GenesisConfig { + system: SystemConfig::default(), + balances: BalancesConfig::default(), + } + .build_storage() + .unwrap() + .into() +} diff --git a/pallets/oneshot-account/src/types.rs b/pallets/oneshot-account/src/types.rs new file mode 100644 index 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..a4cbfbe78 --- /dev/null +++ b/pallets/oneshot-account/src/weights.rs @@ -0,0 +1,52 @@ +// Copyright 2021-2022 Axiom-Team +// +// This file is part of Duniter-v2S. +// +// Duniter-v2S is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// Duniter-v2S is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>. + +#![allow(clippy::unnecessary_cast)] + +use frame_support::weights::{constants::RocksDbWeight, Weight}; + +/// Weight functions needed for pallet_universal_dividend. +pub trait WeightInfo { + fn create_oneshot_account() -> Weight; + fn consume_oneshot_account() -> Weight; + fn consume_oneshot_account_with_remaining() -> Weight; +} + +// Insecure weights implementation, use it for tests only! +impl WeightInfo for () { + // Storage: OneshotAccount OneshotAccounts (r:1 w:1) + fn create_oneshot_account() -> Weight { + (45_690_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(1 as Weight)) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } + // Storage: OneshotAccount OneshotAccounts (r:1 w:1) + // Storage: System BlockHash (r:1 w:0) + // Storage: System Account (r:1 w:1) + fn consume_oneshot_account() -> Weight { + (50_060_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(3 as Weight)) + .saturating_add(RocksDbWeight::get().writes(2 as Weight)) + } + // Storage: OneshotAccount OneshotAccounts (r:1 w:1) + // Storage: System BlockHash (r:1 w:0) + // Storage: System Account (r:2 w:2) + fn consume_oneshot_account_with_remaining() -> Weight { + (69_346_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(4 as Weight)) + .saturating_add(RocksDbWeight::get().writes(3 as Weight)) + } +} diff --git a/resources/metadata.scale b/resources/metadata.scale index 53eb7112a2ef328bbe23045d60c5ef402470ae39..10f4232185e31dcb87b88cd33a9731c8efca1265 100644 GIT binary patch delta 10281 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 Khvn6s^8W!<$o(k* delta 7293 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 aLakO^8Edq>;jvEc`YU?-)T=8S)c*zLFX%S_ 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/apis.rs b/runtime/common/src/apis.rs index e6002a5d1..1aaf5690c 100644 --- a/runtime/common/src/apis.rs +++ b/runtime/common/src/apis.rs @@ -238,6 +238,7 @@ macro_rules! runtime_apis { list_benchmark!(list, extra, frame_system, SystemBench::<Runtime>); list_benchmark!(list, extra, pallet_balances, Balances); list_benchmark!(list, extra, pallet_multisig, Multisig); + list_benchmark!(list, extra, pallet_oneshot_account, OneshotAccount); list_benchmark!(list, extra, pallet_proxy, Proxy); list_benchmark!(list, extra, pallet_scheduler, Scheduler); list_benchmark!(list, extra, pallet_timestamp, Timestamp); @@ -284,6 +285,7 @@ macro_rules! runtime_apis { add_benchmark!(params, batches, frame_system, SystemBench::<Runtime>); add_benchmark!(params, batches, pallet_balances, Balances); add_benchmark!(params, batches, pallet_multisig, Multisig); + add_benchmark!(params, batches, pallet_oneshot_account, OneshotAccount); add_benchmark!(params, batches, pallet_proxy, Proxy); add_benchmark!(params, batches, pallet_scheduler, Scheduler); add_benchmark!(params, batches, pallet_timestamp, Timestamp); diff --git a/runtime/common/src/pallets_config.rs b/runtime/common/src/pallets_config.rs index 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..8a50c7664 --- /dev/null +++ b/runtime/common/src/weights/pallet_oneshot_account.rs @@ -0,0 +1,71 @@ +// Copyright 2021-2022 Axiom-Team +// +// This file is part of Duniter-v2S. +// +// Duniter-v2S is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// Duniter-v2S is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>. + +//! Autogenerated weights for `pallet_oneshot_account` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2022-08-08, STEPS: `50`, REPEAT: 20, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Interpreted, CHAIN: Some("dev"), DB CACHE: 1024 + +// Executed Command: +// ./duniter +// benchmark +// pallet +// --chain=dev +// --steps=50 +// --repeat=20 +// --pallet=pallet_oneshot_account +// --extrinsic=* +// --execution=wasm +// --wasm-execution=interpreted-i-know-what-i-do +// --heap-pages=4096 +// --header=./file_header.txt +// --output=. + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(clippy::unnecessary_cast)] + +use frame_support::{traits::Get, weights::Weight}; +use sp_std::marker::PhantomData; + +/// Weight functions for `pallet_oneshot_account`. +pub struct WeightInfo<T>(PhantomData<T>); +impl<T: frame_system::Config> pallet_oneshot_account::WeightInfo for WeightInfo<T> { + // Storage: OneshotAccount OneshotAccounts (r:1 w:1) + fn create_oneshot_account() -> Weight { + (941_602_000 as Weight) + .saturating_add(T::DbWeight::get().reads(1 as Weight)) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + } + // Storage: OneshotAccount OneshotAccounts (r:1 w:1) + // Storage: System BlockHash (r:1 w:0) + // Storage: System Account (r:1 w:1) + fn consume_oneshot_account() -> Weight { + (971_453_000 as Weight) + .saturating_add(T::DbWeight::get().reads(3 as Weight)) + .saturating_add(T::DbWeight::get().writes(2 as Weight)) + } + // Storage: OneshotAccount OneshotAccounts (r:1 w:1) + // Storage: System BlockHash (r:1 w:0) + // Storage: System Account (r:2 w:2) + fn consume_oneshot_account_with_remaining() -> Weight { + (1_500_781_000 as Weight) + .saturating_add(T::DbWeight::get().reads(4 as Weight)) + .saturating_add(T::DbWeight::get().writes(3 as Weight)) + } +} diff --git a/runtime/g1/Cargo.toml b/runtime/g1/Cargo.toml index 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..77ff1f259 100644 --- a/runtime/gdev/Cargo.toml +++ b/runtime/gdev/Cargo.toml @@ -36,6 +36,7 @@ runtime-benchmarks = [ 'pallet-provide-randomness/runtime-benchmarks', 'pallet-im-online/runtime-benchmarks', 'pallet-multisig/runtime-benchmarks', + 'pallet-oneshot-account/runtime-benchmarks', 'pallet-preimage/runtime-benchmarks', 'pallet-proxy/runtime-benchmarks', 'pallet-scheduler/runtime-benchmarks', @@ -68,6 +69,7 @@ std = [ 'pallet-grandpa/std', 'pallet-identity/std', 'pallet-membership/std', + 'pallet-oneshot-account/std', 'pallet-provide-randomness/std', 'pallet-im-online/std', 'pallet-multisig/std', @@ -139,6 +141,7 @@ pallet-duniter-account = { path = '../../pallets/duniter-account', default-featu pallet-duniter-wot = { path = '../../pallets/duniter-wot', default-features = false } pallet-identity = { path = '../../pallets/identity', default-features = false } pallet-membership = { path = '../../pallets/membership', default-features = false } +pallet-oneshot-account = { path = '../../pallets/oneshot-account', default-features = false } pallet-provide-randomness = { path = '../../pallets/provide-randomness', default-features = false } pallet-universal-dividend = { path = '../../pallets/universal-dividend', default-features = false } pallet-upgrade-origin = { path = '../../pallets/upgrade-origin', default-features = false } diff --git a/runtime/gdev/src/lib.rs b/runtime/gdev/src/lib.rs index 666469fa6..1c494414a 100644 --- a/runtime/gdev/src/lib.rs +++ b/runtime/gdev/src/lib.rs @@ -115,7 +115,7 @@ pub type SignedExtra = ( frame_system::CheckTxVersion<Runtime>, frame_system::CheckGenesis<Runtime>, frame_system::CheckEra<Runtime>, - frame_system::CheckNonce<Runtime>, + pallet_oneshot_account::CheckNonce<Runtime>, frame_system::CheckWeight<Runtime>, pallet_transaction_payment::ChargeTransactionPayment<Runtime>, ); @@ -284,6 +284,7 @@ construct_runtime!( // Money management Balances: pallet_balances::{Pallet, Call, Storage, Config<T>, Event<T>} = 6, TransactionPayment: pallet_transaction_payment::{Pallet, Storage} = 32, + OneshotAccount: pallet_oneshot_account::{Pallet, Call, Storage, Event<T>} = 7, // Consensus support AuthorityMembers: pallet_authority_members::{Pallet, Call, Storage, Config<T>, Event<T>} = 10, diff --git a/runtime/gdev/tests/integration_tests.rs b/runtime/gdev/tests/integration_tests.rs index 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