diff --git a/end2end-tests/cucumber-features/oneshot_account.feature b/end2end-tests/cucumber-features/oneshot_account.feature new file mode 100644 index 0000000000000000000000000000000000000000..f3a92de27bf2d6f405682361bbbb2d0efbd5ab9d --- /dev/null +++ b/end2end-tests/cucumber-features/oneshot_account.feature @@ -0,0 +1,21 @@ +Feature: Oneshot account + + Scenario: Simple oneshot consumption + When alice sends 8 ÄžD to oneshot dave + Then alice should have 2 ÄžD + Then dave should have oneshot 8 ÄžD + When oneshot dave consumes into account bob + Then dave should have oneshot 0 ÄžD + Then bob should have 18 ÄžD + Then bob should have oneshot 0 ÄžD + + Scenario: Double oneshot consumption + When alice sends 8 ÄžD to oneshot dave + Then alice should have 2 ÄžD + Then dave should have oneshot 8 ÄžD + When oneshot dave consumes 6 ÄžD into account bob and the rest into oneshot charlie + Then dave should have oneshot 0 ÄžD + Then bob should have 16 ÄžD + Then bob should have oneshot 0 ÄžD + Then charlie should have 10 ÄžD + Then charlie should have oneshot 2 ÄžD diff --git a/end2end-tests/tests/common/mod.rs b/end2end-tests/tests/common/mod.rs index 078abd747617b2e2c04a6392e796d5e5ccd75507..8ee8431042481dc3f208d32f7d9764e330423c01 100644 --- a/end2end-tests/tests/common/mod.rs +++ b/end2end-tests/tests/common/mod.rs @@ -17,6 +17,7 @@ #![allow(clippy::enum_variant_names, dead_code, unused_imports)] pub mod balances; +pub mod oneshot; #[subxt::subxt(runtime_metadata_path = "../resources/metadata.scale")] pub mod node_runtime {} @@ -28,7 +29,7 @@ use std::path::PathBuf; use std::process::Command; use std::str::FromStr; use subxt::rpc::{rpc_params, ClientT, SubscriptionClientT}; -use subxt::{ClientBuilder, DefaultConfig, DefaultExtra}; +use subxt::{ClientBuilder, DefaultConfig, DefaultExtra, PairSigner}; pub type Api = node_runtime::RuntimeApi<DefaultConfig, DefaultExtra<DefaultConfig>>; pub type Client = subxt::Client<DefaultConfig>; diff --git a/end2end-tests/tests/common/oneshot.rs b/end2end-tests/tests/common/oneshot.rs new file mode 100644 index 0000000000000000000000000000000000000000..acb8a99f33e986d501a182aed3bdf108b450ec85 --- /dev/null +++ b/end2end-tests/tests/common/oneshot.rs @@ -0,0 +1,101 @@ +// 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::*; +use sp_keyring::AccountKeyring; +use subxt::{sp_runtime::MultiAddress, PairSigner}; + +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() + .account() + .create_oneshot_account(to.clone().into(), amount) + .create_signed(&from, ()) + .await?, + ) + .await?; + + Ok(()) +} + +pub async fn consume_oneshot_account( + api: &Api, + client: &Client, + from: AccountKeyring, + to: AccountKeyring, + is_dest_oneshot: bool, +) -> Result<()> { + let from = PairSigner::new(from.pair()); + let to = to.to_account_id(); + + let _events = create_block_with_extrinsic( + client, + api.tx() + .account() + .consume_oneshot_account(0, to.clone().into(), is_dest_oneshot) + .create_signed(&from, ()) + .await?, + ) + .await?; + + Ok(()) +} + +pub async fn consume_oneshot_account_two_dests( + api: &Api, + client: &Client, + from: AccountKeyring, + amount: u64, + to1: AccountKeyring, + is_dest1_oneshot: bool, + to2: AccountKeyring, + is_dest2_oneshot: bool, +) -> Result<()> { + let from = PairSigner::new(from.pair()); + let to1 = to1.to_account_id(); + let to2 = to2.to_account_id(); + + let _events = create_block_with_extrinsic( + client, + api.tx() + .account() + .consume_oneshot_account_two_dests( + 0, + to1.into(), + is_dest1_oneshot, + to2.into(), + is_dest2_oneshot, + amount, + ) + .create_signed(&from, ()) + .await?, + ) + .await?; + + Ok(()) +} diff --git a/end2end-tests/tests/cucumber_tests.rs b/end2end-tests/tests/cucumber_tests.rs index ffe54030a61315e4f916611d0b95c6003594f379..5ea462fffbe920c9b6c97de22da4970fafb81f9c 100644 --- a/end2end-tests/tests/cucumber_tests.rs +++ b/end2end-tests/tests/cucumber_tests.rs @@ -155,6 +155,94 @@ 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 is_dest_oneshot = match is_dest_oneshot.as_str() { + "oneshot" => true, + "account" => false, + _ => unreachable!(), + }; + + common::oneshot::consume_oneshot_account( + &world.api(), + &world.client(), + from, + to, + is_dest_oneshot, + ) + .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]+)" +)] +async fn consume_oneshot_account_two_dests( + world: &mut DuniterWorld, + from: String, + amount: u64, + unit: String, + is_dest1_oneshot: String, + to1: String, + is_dest2_oneshot: String, + to2: String, +) -> Result<()> { + // Parse inputs + let from = AccountKeyring::from_str(&from).expect("unknown from"); + let to1 = AccountKeyring::from_str(&to1).expect("unknown to1"); + let to2 = AccountKeyring::from_str(&to2).expect("unknown to2"); + let is_dest1_oneshot = match is_dest1_oneshot.as_str() { + "oneshot" => true, + "account" => false, + _ => unreachable!(), + }; + let is_dest2_oneshot = match is_dest2_oneshot.as_str() { + "oneshot" => true, + "account" => false, + _ => unreachable!(), + }; + let (amount, is_ud) = parse_amount(amount, &unit); + + assert!(!is_ud); + + common::oneshot::consume_oneshot_account_two_dests( + &world.api(), + &world.client(), + from, + amount, + to1, + is_dest1_oneshot, + to2, + is_dest2_oneshot, + ) + .await +} + #[when(regex = r"([a-zA-Z]+) sends all (?:his|her) (?:ÄžDs?|DUs?) to ([a-zA-Z]+)")] async fn send_all_to(world: &mut DuniterWorld, from: String, to: String) -> Result<()> { // Parse inputs @@ -177,6 +265,24 @@ async fn should_have(world: &mut DuniterWorld, who: String, amount: u64) -> Resu Ok(()) } +#[then(regex = r"([a-zA-Z]+) should have oneshot (\d+) ÄžD")] +async fn should_have_oneshot(world: &mut DuniterWorld, who: String, amount: u64) -> Result<()> { + // Parse inputs + let who = AccountKeyring::from_str(&who) + .expect("unknown to") + .to_account_id(); + let amount = amount * 100; + + let oneshot_amount = world + .api() + .storage() + .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/duniter-account/src/lib.rs b/pallets/duniter-account/src/lib.rs index 1b7f59f8a19933cae41c313f5600e9663060c4d5..b848a4b51d2f7a1e0c56769ab5344ca11c986c7c 100644 --- a/pallets/duniter-account/src/lib.rs +++ b/pallets/duniter-account/src/lib.rs @@ -148,8 +148,8 @@ pub mod pallet { }, OneshotAccountConsumed { account: T::AccountId, - balance: T::Balance, - dest: T::AccountId, + dest1: (T::AccountId, T::Balance), + dest2: Option<(T::AccountId, T::Balance)>, }, /// Random id assigned /// [account_id, random_id] @@ -160,15 +160,19 @@ pub mod pallet { #[pallet::error] pub enum Error<T> { - /// DestAccountNotExist + /// Block height is in the future + BlockHeightInFuture, + /// Block height is too old + BlockHeightTooOld, + /// Destination account does not exist DestAccountNotExist, - /// ExistentialDeposit + /// Destination account has balance less than existential deposit ExistentialDeposit, - /// InsufficientBalance + /// Source account has insufficient balance InsufficientBalance, - /// OneshotAccouncAlreadyCreated + /// Destination oneshot account already exists OneshotAccountAlreadyCreated, - /// OneshotAccountNotExist + /// Source oneshot account does not exist OneshotAccountNotExist, } @@ -254,6 +258,10 @@ pub mod pallet { // 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. #[pallet::weight(500_000_000)] pub fn create_oneshot_account( origin: OriginFor<T>, @@ -292,43 +300,142 @@ pub mod pallet { 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>, - dest: (bool, <T::Lookup as StaticLookup>::Source), + block_height: T::BlockNumber, + dest: <T::Lookup as StaticLookup>::Source, + dest_is_oneshot: bool, ) -> DispatchResult { let transactor = ensure_signed(origin)?; - let dest_is_oneshot = dest.0; - let dest = T::Lookup::lookup(dest.1)?; + 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::OneshotAccountConsumed { - account: transactor.clone(), - balance: value, - dest: dest.clone(), - }); Self::deposit_event(Event::OneshotAccountCreated { - account: dest, + account: dest.clone(), balance: value, - creator: transactor, + creator: transactor.clone(), }); } else { let dest_data = frame_system::Account::<T>::get(&dest).data; ensure!(dest_data.was_providing(), Error::<T>::DestAccountNotExist); - Self::deposit_event(Event::OneshotAccountConsumed { - account: transactor, - balance: value, - dest, + frame_system::Account::<T>::mutate(&dest, |a| a.data.add_free(value)); + } + OneshotAccounts::<T>::remove(&transactor); + Self::deposit_event(Event::OneshotAccountConsumed { + account: transactor, + dest1: (dest, value), + dest2: None, + }); + + Ok(()) + } + /// Consume a oneshot account and transfer its balance to two accounts + /// + /// - `block_height`: Must be a recent block number. The limit is `BlockHashCount` in the past. (this is to prevent replay attacks) + /// - `dest`: The first 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_two_dests( + origin: OriginFor<T>, + block_height: T::BlockNumber, + dest: <T::Lookup as StaticLookup>::Source, + dest_is_oneshot: bool, + dest2: <T::Lookup as StaticLookup>::Source, + dest2_is_oneshot: bool, + #[pallet::compact] balance: T::Balance, + ) -> DispatchResult { + let transactor = ensure_signed(origin)?; + let dest1 = T::Lookup::lookup(dest)?; + 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 dest_is_oneshot { + ensure!( + OneshotAccounts::<T>::get(&dest1).is_none(), + Error::<T>::OneshotAccountAlreadyCreated + ); + ensure!( + balance1 >= T::ExistentialDeposit::get(), + Error::<T>::ExistentialDeposit + ); + } else { + let dest1_data = frame_system::Account::<T>::get(&dest1).data; + ensure!(dest1_data.was_providing(), Error::<T>::DestAccountNotExist); + } + if dest2_is_oneshot { + ensure!( + OneshotAccounts::<T>::get(&dest2).is_none(), + Error::<T>::OneshotAccountAlreadyCreated + ); + ensure!( + balance2 >= T::ExistentialDeposit::get(), + Error::<T>::ExistentialDeposit + ); + OneshotAccounts::<T>::insert(&dest2, balance2); + Self::deposit_event(Event::OneshotAccountCreated { + account: dest2.clone(), + balance: balance2, + creator: transactor.clone(), + }); + } else { + let dest2_data = frame_system::Account::<T>::get(&dest2).data; + ensure!(dest2_data.was_providing(), Error::<T>::DestAccountNotExist); + frame_system::Account::<T>::mutate(&dest2, |a| a.data.add_free(balance2)); + } + if dest_is_oneshot { + OneshotAccounts::<T>::insert(&dest1, balance1); + Self::deposit_event(Event::OneshotAccountCreated { + account: dest1.clone(), + balance: balance1, + creator: transactor.clone(), }); + } else { + frame_system::Account::<T>::mutate(&dest1, |a| a.data.add_free(balance1)); } + OneshotAccounts::<T>::remove(&transactor); + Self::deposit_event(Event::OneshotAccountConsumed { + account: transactor, + dest1: (dest1, balance1), + dest2: Some((dest2, balance2)), + }); Ok(()) } diff --git a/pallets/duniter-account/src/mock.rs b/pallets/duniter-account/src/mock.rs new file mode 100644 index 0000000000000000000000000000000000000000..aaffbb4cb9056f15a0a2d63cedfdb25be1c29856 --- /dev/null +++ b/pallets/duniter-account/src/mock.rs @@ -0,0 +1,142 @@ +// 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::*; +use crate::{self as pallet_duniter_account}; +use frame_support::{parameter_types, traits::Everything, PalletId}; +use frame_system as system; +use sp_core::H256; +use sp_runtime::{ + testing::{Header, TestSignature, UintAuthorityId}, + traits::{BlakeTwo256, IdentityLookup}, + {Perbill, Permill}, +}; +use std::collections::BTreeMap; + +type AccountId = u64; +type Balance = u64; +type Block = frame_system::mocking::MockBlock<Test>; +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<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>}, + Account: pallet_duniter_account::{Pallet, Storage, Config<T>, Event<T>}, + Balances: pallet_balances::{Pallet, Call, Storage, Config<T>, Event<T>}, + ProvideRandomness: pallet_provide_randomness::{Pallet, Call, Storage, Event}, + } +); + +// System +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 = AccountId; + type Lookup = IdentityLookup<Self::AccountId>; + type Header = Header; + type Event = Event; + type BlockHashCount = BlockHashCount; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = types::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 = 1; + 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]; +} + +parameter_types! { + pub const Burn: Permill = Permill::zero(); + pub const ProposalBond: Permill = Permill::from_percent(1); + pub const ProposalBondMaximum: Option<Balance> = None; + pub const SpendPeriod: <Test as system::Config>::BlockNumber = 10; + // Treasury account address: + // gdev/gtest: 5EYCAe5ijiYfyeZ2JJCGq56LmPyNRAKzpG4QkoQkkQNB5e6Z + pub const TreasuryPalletId: PalletId = PalletId(*b"py/trsry"); +} +impl pallet_treasury::Config for Test { + type ApproveOrigin = (); + type Burn = Burn; + type BurnDestination = (); + type Currency = Balances; + type Event = Event; + type OnSlash = pallet_treasury::Treasury; + type ProposalBond = ProposalBond; + type ProposalBondMinimum = frame_support::traits::ConstU64<10_000>; + type ProposalBondMaximum = ProposalBondMaximum; + type MaxApprovals = frame_support::traits::ConstU32<100>; + type PalletId = TreasuryPalletId; + type RejectOrigin = (); + type SpendFunds = (); + type SpendPeriod = SpendPeriod; + type WeightInfo = pallet_treasury::weights::SubstrateWeight<Self>; +} + +impl pallet_provide_randomness::Config for Runtime { + type Currency = Balances; + type Event = Event; + type GetCurrentEpochIndex = GetCurrentEpochIndex<Self>; + type MaxRequests = frame_support::traits::ConstU32<100>; + type RequestPrice = frame_support::traits::ConstU64<2_000>; + type OnFilledRandomness = Account; + type OnUnbalanced = Treasury; + type CurrentBlockRandomness = pallet_babe::CurrentBlockRandomness<Self>; + type RandomnessFromOneEpochAgo = pallet_babe::RandomnessFromOneEpochAgo<Self>; +} + +impl pallet_duniter_account::Config for Test { + type AccountIdToSalt = sp_runtime::traits::ConvertInto; + type Event = Event; + type MaxNewAccountsPerBlock = frame_support::pallet_prelude::ConstU32<1>; + type NewAccountPrice = frame_support::traits::ConstU64<300>; +} diff --git a/pallets/duniter-account/src/tests.rs b/pallets/duniter-account/src/tests.rs new file mode 100644 index 0000000000000000000000000000000000000000..86934326a4adfd636207a7b2abba999efebdd401 --- /dev/null +++ b/pallets/duniter-account/src/tests.rs @@ -0,0 +1,15 @@ +// 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/>. diff --git a/pallets/duniter-account/src/types.rs b/pallets/duniter-account/src/types.rs index 8f1a6b16bbba8ac057724c09b5de640a12d70d64..486be537bf5b7b2797f089304e4d37b0f29b21a9 100644 --- a/pallets/duniter-account/src/types.rs +++ b/pallets/duniter-account/src/types.rs @@ -29,6 +29,9 @@ pub struct AccountData<Balance> { } impl<Balance: Copy + Saturating + Zero> AccountData<Balance> { + pub fn add_free(&mut self, amount: Balance) { + self.free = self.free.saturating_add(amount); + } pub fn free_and_reserved(&self) -> Balance { self.free.saturating_add(self.reserved) } diff --git a/resources/metadata.scale b/resources/metadata.scale index 1f01cc4a16551841453d80d1d36da2e23037db3f..098f7c83f194cb5a3a6da8537a6ded2a4ff2e3c1 100644 Binary files a/resources/metadata.scale and b/resources/metadata.scale differ diff --git a/runtime/gdev/src/check_nonce.rs b/runtime/gdev/src/check_nonce.rs new file mode 100644 index 0000000000000000000000000000000000000000..52d24c7ba4a05c76282a5cdc6526a58d096f45e5 --- /dev/null +++ b/runtime/gdev/src/check_nonce.rs @@ -0,0 +1,83 @@ +// 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::Runtime; + +use codec::{Decode, Encode}; +use frame_system::Config; +use scale_info::TypeInfo; +use sp_runtime::{ + traits::{DispatchInfoOf, SignedExtension}, + transaction_validity::{TransactionValidity, TransactionValidityError}, +}; + +#[derive(Encode, Decode, Clone, Eq, PartialEq, TypeInfo)] +#[scale_info(skip_type_params(Runtime))] +pub struct DuniterCheckNonce(pub frame_system::CheckNonce<Runtime>); + +impl sp_std::fmt::Debug for DuniterCheckNonce { + #[cfg(feature = "std")] + fn fmt(&self, f: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result { + write!(f, "DuniterCheckNonce({})", self.0 .0) + } + + #[cfg(not(feature = "std"))] + fn fmt(&self, _: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result { + Ok(()) + } +} + +impl SignedExtension for DuniterCheckNonce { + type AccountId = <Runtime as Config>::AccountId; + type Call = <Runtime as Config>::Call; + type AdditionalSigned = (); + type Pre = (); + const IDENTIFIER: &'static str = "DuniterCheckNonce"; + + 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 matches!( + call, + Self::Call::Account( + pallet_duniter_account::Call::consume_oneshot_account { .. } + | pallet_duniter_account::Call::consume_oneshot_account_two_dests { .. } + ) + ) { + 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/runtime/gdev/src/lib.rs b/runtime/gdev/src/lib.rs index 3b57eda4a73b771865b7ee71b7c4ea50476a1b65..fe26de099f4c1458e9c0740dde0109ed5167be9e 100644 --- a/runtime/gdev/src/lib.rs +++ b/runtime/gdev/src/lib.rs @@ -22,6 +22,7 @@ #[cfg(feature = "std")] include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); +mod check_nonce; pub mod parameters; pub use self::parameters::*; @@ -110,7 +111,7 @@ pub type SignedExtra = ( frame_system::CheckTxVersion<Runtime>, frame_system::CheckGenesis<Runtime>, frame_system::CheckEra<Runtime>, - frame_system::CheckNonce<Runtime>, + check_nonce::DuniterCheckNonce, frame_system::CheckWeight<Runtime>, pallet_transaction_payment::ChargeTransactionPayment<Runtime>, ); @@ -250,7 +251,7 @@ construct_runtime!( { // Basic stuff System: frame_system::{Pallet, Call, Config, Storage, Event<T>} = 0, - Account: pallet_duniter_account::{Pallet, Storage, Config<T>, Event<T>} = 1, + Account: pallet_duniter_account::{Pallet, Call, Storage, Config<T>, Event<T>} = 1, Scheduler: pallet_scheduler::{Pallet, Call, Storage, Event<T>} = 2, // Block creation diff --git a/runtime/gdev/tests/integration_tests.rs b/runtime/gdev/tests/integration_tests.rs index 6ffc2f586ac041a0875188afb00441dcc62a753b..e28b616b3bf911f8b4b31d68796174b513eceec3 100644 --- a/runtime/gdev/tests/integration_tests.rs +++ b/runtime/gdev/tests/integration_tests.rs @@ -344,3 +344,76 @@ fn test_create_new_idty() { ); }); } + +#[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!(Account::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!(Account::consume_oneshot_account_two_dests( + frame_system::RawOrigin::Signed(AccountKeyring::Eve.to_account_id()).into(), + 0, + MultiAddress::Id(AccountKeyring::Ferdie.to_account_id()), + true, + MultiAddress::Id(AccountKeyring::Alice.to_account_id()), + false, + 300 + )); + assert_eq!( + Balances::free_balance(AccountKeyring::Alice.to_account_id()), + 700 + ); + assert_err!( + Account::consume_oneshot_account( + frame_system::RawOrigin::Signed(AccountKeyring::Eve.to_account_id()).into(), + 0, + MultiAddress::Id(AccountKeyring::Ferdie.to_account_id()), + true + ), + pallet_duniter_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!(Account::consume_oneshot_account( + frame_system::RawOrigin::Signed(AccountKeyring::Ferdie.to_account_id()).into(), + 0, + MultiAddress::Id(AccountKeyring::Alice.to_account_id()), + false, + )); + assert_eq!( + Balances::free_balance(AccountKeyring::Alice.to_account_id()), + 1000 + ); + assert_err!( + Account::consume_oneshot_account( + frame_system::RawOrigin::Signed(AccountKeyring::Eve.to_account_id()).into(), + 0, + MultiAddress::Id(AccountKeyring::Alice.to_account_id()), + false + ), + pallet_duniter_account::Error::<Runtime>::OneshotAccountNotExist + ); + }); +}