From e2ff7e8645b34d1f14b6bbf91ac7bafdf93fc08f Mon Sep 17 00:00:00 2001 From: nanocryk <6422796+nanocryk@users.noreply.github.com> Date: Wed, 3 Jan 2024 11:52:53 +0100 Subject: [PATCH 01/40] pallet layout --- Cargo.lock | 23 ++++ pallets/stream-payment/Cargo.toml | 34 ++++++ pallets/stream-payment/src/lib.rs | 178 ++++++++++++++++++++++++++++++ 3 files changed, 235 insertions(+) create mode 100644 pallets/stream-payment/Cargo.toml create mode 100644 pallets/stream-payment/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 953645777..ba3532b19 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8939,6 +8939,29 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-stream-payment" +version = "0.1.0" +dependencies = [ + "dp-core", + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "num-traits", + "pallet-balances", + "parity-scale-codec", + "scale-info", + "serde", + "similar-asserts", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", + "tp-maths", + "tp-traits", +] + [[package]] name = "pallet-sudo" version = "4.0.0-dev" diff --git a/pallets/stream-payment/Cargo.toml b/pallets/stream-payment/Cargo.toml new file mode 100644 index 000000000..b4bf77aa5 --- /dev/null +++ b/pallets/stream-payment/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "pallet-stream-payment" +authors = { workspace = true } +description = "Stream payment pallet" +edition = "2021" +license = "GPL-3.0-only" +version = "0.1.0" + +[package.metadata.docs.rs] +targets = [ "x86_64-unknown-linux-gnu" ] + +[dependencies] +log = { workspace = true } +serde = { workspace = true, optional = true } + +dp-core = { workspace = true } +tp-maths = { workspace = true } +tp-traits = { workspace = true } + +# Substrate +frame-benchmarking = { workspace = true, optional = true } +frame-support = { workspace = true } +frame-system = { workspace = true } +parity-scale-codec = { workspace = true } +scale-info = { workspace = true } +sp-core = { workspace = true } +sp-runtime = { workspace = true } +sp-std = { workspace = true } + +[dev-dependencies] +num-traits = { workspace = true } +pallet-balances = { workspace = true, features = [ "std" ] } +similar-asserts = { workspace = true } +sp-io = { workspace = true, features = [ "std" ] } diff --git a/pallets/stream-payment/src/lib.rs b/pallets/stream-payment/src/lib.rs new file mode 100644 index 000000000..2d16ae066 --- /dev/null +++ b/pallets/stream-payment/src/lib.rs @@ -0,0 +1,178 @@ +// Copyright (C) Moondance Labs Ltd. +// This file is part of Tanssi. + +// Tanssi is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Tanssi 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Tanssi. If not, see + +#![cfg_attr(not(feature = "std"), no_std)] + +use { + frame_support::{ + pallet, + pallet_prelude::*, + storage::types::{StorageDoubleMap, StorageMap}, + traits::{fungibles, tokens::Balance}, + Blake2_128Concat, + }, + frame_system::pallet_prelude::*, + parity_scale_codec::{FullCodec, MaxEncodedLen}, + scale_info::TypeInfo, + sp_std::{fmt::Debug, marker::PhantomData}, +}; + +/// Type able to provide the current time for given unit. +pub trait TimeProvider { + fn now(unit: Unit) -> Option; +} + +#[pallet(dev_mode)] +pub mod pallet { + use super::*; + + /// Pooled Staking pallet. + #[pallet::pallet] + #[pallet::without_storage_info] + pub struct Pallet(PhantomData); + + #[pallet::config] + pub trait Config: frame_system::Config { + /// Represents which units of time can be used. Designed to be an enum + /// with a variant for each kind of time source/scale supported. + type TimeUnit: Debug + Clone + FullCodec + TypeInfo + MaxEncodedLen + Eq; + + /// The balance type, which is also the type representing time (as this + /// pallet will do math with both time and balances to compute how + /// much should be paid). + type Balance: Balance; + + /// The currencies type, supporting multiple currencies. + type Currencies: fungibles::Inspect; + + /// Provide the current time in given unit. + type TimeProvider: TimeProvider; + } + + pub type StreamId = u64; + type AccountIdOf = ::AccountId; + type AssetIdOf = <::Currencies as fungibles::Inspect>>::AssetId; + + /// A stream payment from source to target. + /// Stores the last time the stream was updated, which allows to compute + /// elapsed time and perform payment. + #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] + #[derive(RuntimeDebug, PartialEq, Eq, Encode, Decode, Clone, TypeInfo)] + pub struct Stream { + source: AccountId, + target: AccountId, + time_unit: Unit, + asset_id: AssetId, + rate_per_time_unit: Balance, + locked_funds: Balance, + last_time_updated: Balance, + } + + pub type StreamOf = + Stream, ::TimeUnit, AssetIdOf, ::Balance>; + + /// Store the next available stream id. + #[pallet::storage] + pub type NextStreamId = StorageValue; + + /// Store each stream indexed by an Id. + #[pallet::storage] + pub type Streams = StorageMap< + Hasher = Blake2_128Concat, + Key = StreamId, + Value = StreamOf, + QueryKind = OptionQuery, + >; + + /// Lookup for all streams with given source. + /// To avoid maintaining a growing list of stream ids, they are stored in + /// the form of an entry (AccountId, StreamId). If such entry exists then + /// this AccountId is a source in StreamId. One can iterate over all storage + /// keys starting with the AccountId to find all StreamIds. + #[pallet::storage] + pub type LookupStreamsWithSource = StorageDoubleMap< + Key1 = AccountIdOf, + Hasher1 = Blake2_128Concat, + Key2 = StreamId, + Hasher2 = Blake2_128Concat, + Value = (), + QueryKind = OptionQuery, + >; + + /// Lookup for all streams with given target. + /// To avoid maintaining a growing list of stream ids, they are stored in + /// the form of an entry (AccountId, StreamId). If such entry exists then + /// this AccountId is a target in StreamId. One can iterate over all storage + /// keys starting with the AccountId to find all StreamIds. + #[pallet::storage] + pub type LookupStreamsWithTarget = StorageDoubleMap< + Key1 = AccountIdOf, + Hasher1 = Blake2_128Concat, + Key2 = StreamId, + Hasher2 = Blake2_128Concat, + Value = (), + QueryKind = OptionQuery, + >; + + #[pallet::call] + impl Pallet { + #[pallet::call_index(0)] + pub fn open_stream( + _origin: OriginFor, + _target: AccountIdOf, + _time_unit: T::TimeUnit, + _asset_id: AssetIdOf, + _rate_per_time_unit: T::Balance, + _initial_deposit: T::Balance, + ) -> DispatchResultWithPostInfo { + todo!() + } + + #[pallet::call_index(1)] + pub fn close_stream( + _origin: OriginFor, + _stream: StreamId, + ) -> DispatchResultWithPostInfo { + todo!() + } + + #[pallet::call_index(2)] + pub fn update_stream( + _origin: OriginFor, + _stream: StreamId, + ) -> DispatchResultWithPostInfo { + todo!() + } + + #[pallet::call_index(3)] + pub fn refill_stream( + _origin: OriginFor, + _stream: StreamId, + _new_deposit: T::Balance, + ) -> DispatchResultWithPostInfo { + todo!() + } + + #[pallet::call_index(4)] + pub fn change_stream_rate( + _origin: OriginFor, + _stream: StreamId, + _new_rate_per_time_unit: T::Balance, + ) -> DispatchResultWithPostInfo { + todo!() + } + } +} From 6c4faadfa0762f438f9a0100b0adc0cd9c06e23e Mon Sep 17 00:00:00 2001 From: nanocryk <6422796+nanocryk@users.noreply.github.com> Date: Thu, 4 Jan 2024 11:04:49 +0100 Subject: [PATCH 02/40] open stream + update logic --- pallets/stream-payment/src/lib.rs | 178 ++++++++++++++++++++++++++---- 1 file changed, 157 insertions(+), 21 deletions(-) diff --git a/pallets/stream-payment/src/lib.rs b/pallets/stream-payment/src/lib.rs index 2d16ae066..312b7a713 100644 --- a/pallets/stream-payment/src/lib.rs +++ b/pallets/stream-payment/src/lib.rs @@ -21,18 +21,24 @@ use { pallet, pallet_prelude::*, storage::types::{StorageDoubleMap, StorageMap}, - traits::{fungibles, tokens::Balance}, + traits::{ + fungibles::{self, Mutate as _, MutateFreeze as _}, + tokens::{Balance, Preservation}, + }, Blake2_128Concat, }, frame_system::pallet_prelude::*, parity_scale_codec::{FullCodec, MaxEncodedLen}, scale_info::TypeInfo, + sp_runtime::traits::{AtLeast32BitUnsigned, CheckedAdd, CheckedMul, CheckedSub, One, Zero}, sp_std::{fmt::Debug, marker::PhantomData}, }; /// Type able to provide the current time for given unit. +/// For each unit the returned number should monotonically increase and not +/// overflow. pub trait TimeProvider { - fn now(unit: Unit) -> Option; + fn now(unit: &Unit) -> Option; } #[pallet(dev_mode)] @@ -46,6 +52,16 @@ pub mod pallet { #[pallet::config] pub trait Config: frame_system::Config { + /// Type used to represent stream ids. Should be large enough to not overflow. + type StreamId: AtLeast32BitUnsigned + + Default + + Debug + + Copy + + Clone + + FullCodec + + TypeInfo + + MaxEncodedLen; + /// Represents which units of time can be used. Designed to be an enum /// with a variant for each kind of time source/scale supported. type TimeUnit: Debug + Clone + FullCodec + TypeInfo + MaxEncodedLen + Eq; @@ -55,14 +71,19 @@ pub mod pallet { /// much should be paid). type Balance: Balance; + /// LockId type used by `Currencies`. + type LockId: From; + /// The currencies type, supporting multiple currencies. - type Currencies: fungibles::Inspect; + type Currencies: fungibles::Inspect + + fungibles::InspectFreeze + + fungibles::Mutate + + fungibles::MutateFreeze; /// Provide the current time in given unit. type TimeProvider: TimeProvider; } - pub type StreamId = u64; type AccountIdOf = ::AccountId; type AssetIdOf = <::Currencies as fungibles::Inspect>>::AssetId; @@ -86,13 +107,13 @@ pub mod pallet { /// Store the next available stream id. #[pallet::storage] - pub type NextStreamId = StorageValue; + pub type NextStreamId = StorageValue; /// Store each stream indexed by an Id. #[pallet::storage] pub type Streams = StorageMap< Hasher = Blake2_128Concat, - Key = StreamId, + Key = T::StreamId, Value = StreamOf, QueryKind = OptionQuery, >; @@ -106,7 +127,7 @@ pub mod pallet { pub type LookupStreamsWithSource = StorageDoubleMap< Key1 = AccountIdOf, Hasher1 = Blake2_128Concat, - Key2 = StreamId, + Key2 = T::StreamId, Hasher2 = Blake2_128Concat, Value = (), QueryKind = OptionQuery, @@ -121,46 +142,101 @@ pub mod pallet { pub type LookupStreamsWithTarget = StorageDoubleMap< Key1 = AccountIdOf, Hasher1 = Blake2_128Concat, - Key2 = StreamId, + Key2 = T::StreamId, Hasher2 = Blake2_128Concat, Value = (), QueryKind = OptionQuery, >; + #[pallet::error] + pub enum Error { + UnknownStreamId, + StreamIdOverflow, + CantBeBothSourceAndTarget, + CantFetchCurrentTime, + TimeOverflow, + CurrencyOverflow, + } + + #[pallet::composite_enum] + pub enum LockId { + StreamPayment, + } + #[pallet::call] impl Pallet { #[pallet::call_index(0)] pub fn open_stream( - _origin: OriginFor, - _target: AccountIdOf, - _time_unit: T::TimeUnit, - _asset_id: AssetIdOf, - _rate_per_time_unit: T::Balance, - _initial_deposit: T::Balance, + origin: OriginFor, + target: AccountIdOf, + time_unit: T::TimeUnit, + asset_id: AssetIdOf, + rate_per_time_unit: T::Balance, + initial_deposit: T::Balance, ) -> DispatchResultWithPostInfo { - todo!() + let origin = ensure_signed(origin)?; + ensure!(origin != target, Error::::CantBeBothSourceAndTarget); + + let stream_id = NextStreamId::::get(); + let next_stream_id = stream_id + .checked_add(&One::one()) + .ok_or(Error::::StreamIdOverflow)?; + NextStreamId::::set(next_stream_id); + + T::Currencies::increase_frozen( + asset_id.clone(), + &LockId::StreamPayment.into(), + &origin, + initial_deposit, + )?; + + let now = T::TimeProvider::now(&time_unit).ok_or(Error::::CantFetchCurrentTime)?; + let stream = Stream { + source: origin.clone(), + target: target.clone(), + time_unit, + asset_id, + rate_per_time_unit, + locked_funds: initial_deposit, + last_time_updated: now, + }; + + Streams::::insert(stream_id, stream); + LookupStreamsWithSource::::insert(origin, stream_id, ()); + LookupStreamsWithTarget::::insert(target, stream_id, ()); + + Ok(().into()) } #[pallet::call_index(1)] pub fn close_stream( _origin: OriginFor, - _stream: StreamId, + _stream_id: T::StreamId, ) -> DispatchResultWithPostInfo { todo!() } #[pallet::call_index(2)] pub fn update_stream( - _origin: OriginFor, - _stream: StreamId, + origin: OriginFor, + stream_id: T::StreamId, ) -> DispatchResultWithPostInfo { - todo!() + // No problem with anyone updating any stream. + let _ = ensure_signed(origin)?; + + let mut stream = Streams::::get(stream_id).ok_or(Error::::UnknownStreamId)?; + Self::perform_stream_payment(&mut stream)?; + Streams::::insert(stream_id, stream); + + // TODO: Event here or in do_update_stream? + + Ok(().into()) } #[pallet::call_index(3)] pub fn refill_stream( _origin: OriginFor, - _stream: StreamId, + _stream_id: T::StreamId, _new_deposit: T::Balance, ) -> DispatchResultWithPostInfo { todo!() @@ -169,10 +245,70 @@ pub mod pallet { #[pallet::call_index(4)] pub fn change_stream_rate( _origin: OriginFor, - _stream: StreamId, + _stream_id: T::StreamId, _new_rate_per_time_unit: T::Balance, ) -> DispatchResultWithPostInfo { todo!() } } + + impl Pallet { + /// Behavior: + /// A stream payment consist of a locked deposit, a rate per unit of time and the + /// last time the stream was updated. When updating the stream, **at most** + /// `elapsed_time * rate` is unlocked from the source account and transfered to the target + /// account. If this amount is greater than the left deposit, the stream is considered + /// drained **but not closed**. The source can come back later and refill the stream, + /// however there will be no retroactive payment for the time spent as drained. + /// If the stream payment is used to rent a service, the target should pause the service + /// while the stream is drained, and resume it once it is refilled. + fn perform_stream_payment(stream: &mut StreamOf) -> DispatchResultWithPostInfo { + let now = + T::TimeProvider::now(&stream.time_unit).ok_or(Error::::CantFetchCurrentTime)?; + + if stream.locked_funds.is_zero() { + stream.last_time_updated = now; + return Ok(().into()); + } + + let delta = now + .checked_sub(&stream.last_time_updated) + .ok_or(Error::::TimeOverflow)?; + let mut payment = delta + .checked_mul(&stream.rate_per_time_unit) + .ok_or(Error::::CurrencyOverflow)?; + + // We compute the new amount of locked funds. If it underflows it + // means that there is more to pay that what is left, in which case + // we pay all that is left. + let new_locked = match stream.locked_funds.checked_sub(&payment) { + Some(v) => v, + None => { + payment = stream.locked_funds; + Zero::zero() + } + }; + + T::Currencies::decrease_frozen( + stream.asset_id.clone(), + &LockId::StreamPayment.into(), + &stream.source, + payment, + )?; + T::Currencies::transfer( + stream.asset_id.clone(), + &stream.source, + &stream.target, + payment, + Preservation::Preserve, + )?; + + stream.last_time_updated = now; + stream.locked_funds = new_locked; + + // TODO: Emit event here? + + Ok(().into()) + } + } } From 56dd91403da9f4334fcb68a68dde5250b87718b1 Mon Sep 17 00:00:00 2001 From: nanocryk <6422796+nanocryk@users.noreply.github.com> Date: Thu, 4 Jan 2024 12:01:40 +0100 Subject: [PATCH 03/40] close stream + events --- pallets/stream-payment/src/lib.rs | 109 +++++++++++++++++++++++++----- 1 file changed, 92 insertions(+), 17 deletions(-) diff --git a/pallets/stream-payment/src/lib.rs b/pallets/stream-payment/src/lib.rs index 312b7a713..1f6cf2da3 100644 --- a/pallets/stream-payment/src/lib.rs +++ b/pallets/stream-payment/src/lib.rs @@ -52,6 +52,9 @@ pub mod pallet { #[pallet::config] pub trait Config: frame_system::Config { + /// Overarching event type + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + /// Type used to represent stream ids. Should be large enough to not overflow. type StreamId: AtLeast32BitUnsigned + Default @@ -98,7 +101,7 @@ pub mod pallet { time_unit: Unit, asset_id: AssetId, rate_per_time_unit: Balance, - locked_funds: Balance, + deposit: Balance, last_time_updated: Balance, } @@ -152,12 +155,32 @@ pub mod pallet { pub enum Error { UnknownStreamId, StreamIdOverflow, + UnauthorizedOrigin, CantBeBothSourceAndTarget, CantFetchCurrentTime, TimeOverflow, CurrencyOverflow, } + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + StreamOpened { + stream_id: T::StreamId, + }, + StreamClosed { + stream_id: T::StreamId, + refunded: T::Balance, + }, + StreamPayment { + stream_id: T::StreamId, + source: AccountIdOf, + target: AccountIdOf, + amount: T::Balance, + drained: bool, + }, + } + #[pallet::composite_enum] pub enum LockId { StreamPayment, @@ -177,12 +200,14 @@ pub mod pallet { let origin = ensure_signed(origin)?; ensure!(origin != target, Error::::CantBeBothSourceAndTarget); + // Generate a new stream id. let stream_id = NextStreamId::::get(); let next_stream_id = stream_id .checked_add(&One::one()) .ok_or(Error::::StreamIdOverflow)?; NextStreamId::::set(next_stream_id); + // Freeze initial deposit. T::Currencies::increase_frozen( asset_id.clone(), &LockId::StreamPayment.into(), @@ -190,6 +215,7 @@ pub mod pallet { initial_deposit, )?; + // Create stream data. let now = T::TimeProvider::now(&time_unit).ok_or(Error::::CantFetchCurrentTime)?; let stream = Stream { source: origin.clone(), @@ -197,23 +223,58 @@ pub mod pallet { time_unit, asset_id, rate_per_time_unit, - locked_funds: initial_deposit, + deposit: initial_deposit, last_time_updated: now, }; + // Insert stream in storage. Streams::::insert(stream_id, stream); LookupStreamsWithSource::::insert(origin, stream_id, ()); LookupStreamsWithTarget::::insert(target, stream_id, ()); + // Emit event. + Pallet::::deposit_event(Event::::StreamOpened { stream_id }); + Ok(().into()) } #[pallet::call_index(1)] pub fn close_stream( - _origin: OriginFor, - _stream_id: T::StreamId, + origin: OriginFor, + stream_id: T::StreamId, ) -> DispatchResultWithPostInfo { - todo!() + let origin = ensure_signed(origin)?; + let mut stream = Streams::::get(stream_id).ok_or(Error::::UnknownStreamId)?; + + // Only source or target can close a stream. + ensure!( + origin == stream.source || origin == stream.target, + Error::::UnauthorizedOrigin + ); + + // Update stream before closing it to ensure fair payment. + Self::perform_stream_payment(stream_id, &mut stream)?; + + // Unfreeze funds left in the stream. + T::Currencies::decrease_frozen( + stream.asset_id.clone(), + &LockId::StreamPayment.into(), + &stream.source, + stream.deposit, + )?; + + // Remove stream from storage. + Streams::::remove(stream_id); + LookupStreamsWithSource::::remove(stream.source, stream_id); + LookupStreamsWithTarget::::remove(stream.target, stream_id); + + // Emit event. + Pallet::::deposit_event(Event::::StreamClosed { + stream_id, + refunded: stream.deposit, + }); + + Ok(().into()) } #[pallet::call_index(2)] @@ -225,11 +286,9 @@ pub mod pallet { let _ = ensure_signed(origin)?; let mut stream = Streams::::get(stream_id).ok_or(Error::::UnknownStreamId)?; - Self::perform_stream_payment(&mut stream)?; + Self::perform_stream_payment(stream_id, &mut stream)?; Streams::::insert(stream_id, stream); - // TODO: Event here or in do_update_stream? - Ok(().into()) } @@ -262,11 +321,15 @@ pub mod pallet { /// however there will be no retroactive payment for the time spent as drained. /// If the stream payment is used to rent a service, the target should pause the service /// while the stream is drained, and resume it once it is refilled. - fn perform_stream_payment(stream: &mut StreamOf) -> DispatchResultWithPostInfo { + fn perform_stream_payment( + stream_id: T::StreamId, + stream: &mut StreamOf, + ) -> DispatchResultWithPostInfo { let now = T::TimeProvider::now(&stream.time_unit).ok_or(Error::::CantFetchCurrentTime)?; - if stream.locked_funds.is_zero() { + // If deposit is zero the stream is fully drained and there is nothing to transfer. + if stream.deposit.is_zero() { stream.last_time_updated = now; return Ok(().into()); } @@ -274,6 +337,9 @@ pub mod pallet { let delta = now .checked_sub(&stream.last_time_updated) .ok_or(Error::::TimeOverflow)?; + + // We compute the amount due to the target according to the rate, which may be + // lowered if the stream deposit is lower. let mut payment = delta .checked_mul(&stream.rate_per_time_unit) .ok_or(Error::::CurrencyOverflow)?; @@ -281,14 +347,15 @@ pub mod pallet { // We compute the new amount of locked funds. If it underflows it // means that there is more to pay that what is left, in which case // we pay all that is left. - let new_locked = match stream.locked_funds.checked_sub(&payment) { - Some(v) => v, + let (new_locked, drained) = match stream.deposit.checked_sub(&payment) { + Some(v) => (v, false), None => { - payment = stream.locked_funds; - Zero::zero() + payment = stream.deposit; + (Zero::zero(), true) } }; + // Transfer from the source to target. T::Currencies::decrease_frozen( stream.asset_id.clone(), &LockId::StreamPayment.into(), @@ -303,10 +370,18 @@ pub mod pallet { Preservation::Preserve, )?; + // Update stream info. stream.last_time_updated = now; - stream.locked_funds = new_locked; - - // TODO: Emit event here? + stream.deposit = new_locked; + + // Emit event. + Pallet::::deposit_event(Event::::StreamPayment { + stream_id, + source: stream.source.clone(), + target: stream.target.clone(), + amount: payment, + drained, + }); Ok(().into()) } From 0f75ed05f27852cc4dcd3d09a9edd0f3cef6da61 Mon Sep 17 00:00:00 2001 From: nanocryk <6422796+nanocryk@users.noreply.github.com> Date: Fri, 5 Jan 2024 11:05:17 +0100 Subject: [PATCH 04/40] refill + change rate --- pallets/stream-payment/src/lib.rs | 85 ++++++++++++++++++++++++++++--- 1 file changed, 77 insertions(+), 8 deletions(-) diff --git a/pallets/stream-payment/src/lib.rs b/pallets/stream-payment/src/lib.rs index 1f6cf2da3..4171a9f7a 100644 --- a/pallets/stream-payment/src/lib.rs +++ b/pallets/stream-payment/src/lib.rs @@ -160,6 +160,8 @@ pub mod pallet { CantFetchCurrentTime, TimeOverflow, CurrencyOverflow, + SourceCantDecreaseRate, + TargetCantIncreaseRate, } #[pallet::event] @@ -179,6 +181,11 @@ pub mod pallet { amount: T::Balance, drained: bool, }, + StreamRateChanged { + stream_id: T::StreamId, + old_rate: T::Balance, + new_rate: T::Balance, + }, } #[pallet::composite_enum] @@ -294,20 +301,82 @@ pub mod pallet { #[pallet::call_index(3)] pub fn refill_stream( - _origin: OriginFor, - _stream_id: T::StreamId, - _new_deposit: T::Balance, + origin: OriginFor, + stream_id: T::StreamId, + new_deposit: T::Balance, ) -> DispatchResultWithPostInfo { - todo!() + let origin = ensure_signed(origin)?; + let mut stream = Streams::::get(stream_id).ok_or(Error::::UnknownStreamId)?; + + // Only source can refill stream + ensure!(origin == stream.source, Error::::UnauthorizedOrigin); + + // Source will not pay for drained stream retroactively, so we perform payment with + // what is left first. + Self::perform_stream_payment(stream_id, &mut stream)?; + + // Increase deposit. + T::Currencies::increase_frozen( + stream.asset_id.clone(), + &LockId::StreamPayment.into(), + &origin, + new_deposit, + )?; + stream.deposit = stream + .deposit + .checked_add(&new_deposit) + .ok_or(Error::::CurrencyOverflow)?; + + // Update stream info in storage. + Streams::::insert(stream_id, stream); + + Ok(().into()) } #[pallet::call_index(4)] pub fn change_stream_rate( - _origin: OriginFor, - _stream_id: T::StreamId, - _new_rate_per_time_unit: T::Balance, + origin: OriginFor, + stream_id: T::StreamId, + new_rate_per_time_unit: T::Balance, ) -> DispatchResultWithPostInfo { - todo!() + let origin = ensure_signed(origin)?; + let mut stream = Streams::::get(stream_id).ok_or(Error::::UnknownStreamId)?; + + // Only source or target can update the rate. + ensure!( + origin == stream.source || origin == stream.target, + Error::::UnauthorizedOrigin + ); + + // Noop + if new_rate_per_time_unit == stream.rate_per_time_unit { + return Ok(().into()); + } + + // Ensure rate change is fair. + if origin == stream.source && new_rate_per_time_unit < stream.rate_per_time_unit { + return Err(Error::::SourceCantDecreaseRate.into()); + } + + if origin == stream.target && new_rate_per_time_unit > stream.rate_per_time_unit { + return Err(Error::::TargetCantIncreaseRate.into()); + } + + // Perform pending payment before changing rate. + Self::perform_stream_payment(stream_id, &mut stream)?; + + // Emit event. + Pallet::::deposit_event(Event::::StreamRateChanged { + stream_id, + old_rate: stream.rate_per_time_unit, + new_rate: new_rate_per_time_unit, + }); + + // Update rate + stream.rate_per_time_unit = new_rate_per_time_unit; + Streams::::insert(stream_id, stream); + + Ok(().into()) } } From 594ca74dadf5013becb1373da909e8e73445f36e Mon Sep 17 00:00:00 2001 From: nanocryk <6422796+nanocryk@users.noreply.github.com> Date: Mon, 8 Jan 2024 15:04:32 +0100 Subject: [PATCH 05/40] adapter for fungible impl --- Cargo.toml | 1 + pallets/stream-payment/src/as_fungibles.rs | 388 +++++++++++++++++++++ pallets/stream-payment/src/lib.rs | 2 + 3 files changed, 391 insertions(+) create mode 100644 pallets/stream-payment/src/as_fungibles.rs diff --git a/Cargo.toml b/Cargo.toml index a78147705..349875964 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,7 @@ tc-consensus = { path = "client/consensus" } tp-author-noting-inherent = { path = "primitives/author-noting-inherent", default-features = false } tp-consensus = { path = "primitives/consensus", default-features = false } tp-container-chain-genesis-data = { path = "primitives/container-chain-genesis-data", default-features = false } +tp-fungibles-ext = { path = "primitives/fungibles-ext", default-features = false } tp-maths = { path = "primitives/maths", default-features = false } tp-traits = { path = "primitives/traits", default-features = false } diff --git a/pallets/stream-payment/src/as_fungibles.rs b/pallets/stream-payment/src/as_fungibles.rs new file mode 100644 index 000000000..5a30e9799 --- /dev/null +++ b/pallets/stream-payment/src/as_fungibles.rs @@ -0,0 +1,388 @@ +// Copyright (C) Moondance Labs Ltd. +// This file is part of Tanssi. + +// Tanssi is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Tanssi 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Tanssi. If not, see . + +//! Provides a wrapper to adapat a `fungible` into `fungibles`. + +// TODO: PR Substrate to include this convertion, which is the opposite of the +// currently present `ItemOf`. Lacking some redirects due to unaccessible +// imbalances type conversions. +// https://github.com/paritytech/polkadot-sdk/pull/2858 + +use { + core::marker::PhantomData, + frame_support::{ + sp_runtime::{DispatchError, DispatchResult}, + traits::tokens::{ + fungible, fungibles, DepositConsequence, Fortitude, Precision, Preservation, + Provenance, Restriction, WithdrawConsequence, + }, + }, +}; + +/// Redirects `fungibles` function to the `fungible` equivalent without +/// the AssetId argument. +macro_rules! redirect { + ( $( + fn $fn_name:ident ( + $( + $arg_name:ident : $arg_ty:ty + ),* $(,)? + ) $(-> $fn_out:ty)?; + )+) => { + $( + fn $fn_name((): Self::AssetId, $($arg_name:$arg_ty),*) $(-> $fn_out)? { + F::$fn_name($($arg_name),*) + } + )+ + }; +} + +pub struct ConvertHandleImbalanceDrop(PhantomData); + +impl> fungibles::HandleImbalanceDrop<(), B> + for ConvertHandleImbalanceDrop +{ + fn handle((): (), amount: B) { + H::handle(amount) + } +} + +/// A wrapper to use a `fungible` as a `fungibles` with a single asset represented by `()`. +pub struct AsFungibles(PhantomData<(F, AccountId)>); + +impl> fungibles::Inspect + for AsFungibles +{ + type AssetId = (); + type Balance = F::Balance; + + redirect!( + fn total_issuance() -> Self::Balance; + fn minimum_balance() -> Self::Balance; + fn total_balance(who: &AccountId) -> Self::Balance; + fn balance(who: &AccountId) -> Self::Balance; + fn reducible_balance( + who: &AccountId, + preservation: Preservation, + force: Fortitude, + ) -> Self::Balance; + fn can_deposit( + who: &AccountId, + amount: Self::Balance, + provenance: Provenance, + ) -> DepositConsequence; + fn can_withdraw( + who: &AccountId, + amount: Self::Balance, + ) -> WithdrawConsequence; + fn active_issuance() -> Self::Balance; + ); + + fn asset_exists((): Self::AssetId) -> bool { + true + } +} + +impl> fungibles::Unbalanced + for AsFungibles +{ + redirect!( + fn write_balance( + who: &AccountId, + amount: Self::Balance, + ) -> Result, DispatchError>; + fn set_total_issuance(amount: Self::Balance); + fn handle_raw_dust(amount: Self::Balance); + fn decrease_balance( + who: &AccountId, + amount: Self::Balance, + precision: Precision, + preservation: Preservation, + force: Fortitude, + ) -> Result; + fn increase_balance( + who: &AccountId, + amount: Self::Balance, + precision: Precision, + ) -> Result; + fn deactivate(amount: Self::Balance); + fn reactivate(amount: Self::Balance); + ); + + fn handle_dust(fungibles::Dust((), dust): fungibles::Dust) { + F::handle_dust(fungible::Dust(dust)) + } +} + +impl> fungibles::Mutate + for AsFungibles +{ + redirect!( + fn mint_into( + who: &AccountId, + amount: Self::Balance, + ) -> Result; + fn burn_from( + who: &AccountId, + amount: Self::Balance, + precision: Precision, + force: Fortitude, + ) -> Result; + fn shelve(who: &AccountId, amount: Self::Balance) -> Result; + fn restore(who: &AccountId, amount: Self::Balance) -> Result; + fn transfer( + source: &AccountId, + dest: &AccountId, + amount: Self::Balance, + preservation: Preservation, + ) -> Result; + fn set_balance(who: &AccountId, amount: Self::Balance) -> Self::Balance; + fn done_mint_into(who: &AccountId, amount: Self::Balance); + fn done_burn_from(who: &AccountId, amount: Self::Balance); + fn done_shelve(who: &AccountId, amount: Self::Balance); + fn done_restore(who: &AccountId, amount: Self::Balance); + fn done_transfer(source: &AccountId, dest: &AccountId, amount: Self::Balance); + ); +} + +impl> fungibles::Balanced + for AsFungibles +{ + type OnDropDebt = ConvertHandleImbalanceDrop; + type OnDropCredit = ConvertHandleImbalanceDrop; + + // fn rescind((): Self::AssetId, amount: Self::Balance) -> fungibles::Debt { + // let dept = F::rescind(amount); + // from_fungible(dept, ()) + // } + + // fn issue((): Self::AssetId, amount: Self::Balance) -> fungibles::Credit { + // let credit = F::issue(amount); + // from_fungible(credit, ()) + // } + + // fn pair( + // (): Self::AssetId, + // amount: Self::Balance, + // ) -> (fungibles::Debt, fungibles::Credit) { + // let (dept, credit) = F::pair(amount); + // (from_fungible(dept, ()), from_fungible(credit, ())) + // } + + // fn deposit( + // (): Self::AssetId, + // who: &AccountId, + // value: Self::Balance, + // precision: Precision, + // ) -> Result, DispatchError> { + // F::deposit(who, value, precision).map(|dept| from_fungible(dept, ())) + // } + + // fn withdraw( + // (): Self::AssetId, + // who: &AccountId, + // value: Self::Balance, + // precision: Precision, + // preservation: Preservation, + // force: Fortitude, + // ) -> Result, DispatchError> { + // F::withdraw(who, value, precision, preservation, force) + // .map(|credit| from_fungible(credit, ())) + // } + + // fn resolve( + // who: &AccountId, + // credit: fungibles::Credit, + // ) -> Result<(), fungibles::Credit> { + // F::resolve(who, from_fungibles(credit)).map_err(|credit| from_fungible(credit, ())) + // } + + // fn settle( + // who: &AccountId, + // debt: fungibles::Debt, + // preservation: Preservation, + // ) -> Result, fungibles::Debt> { + // F::settle(who, from_fungibles(debt), preservation) + // .map(|credit| from_fungible(credit, ())) + // .map_err(|dept| from_fungible(dept, ())) + // } + + redirect!( + fn done_rescind(amount: Self::Balance); + fn done_issue(amount: Self::Balance); + fn done_deposit(who: &AccountId, amount: Self::Balance); + fn done_withdraw(who: &AccountId, amount: Self::Balance); + ); +} + +impl> fungibles::hold::Inspect + for AsFungibles +{ + type Reason = F::Reason; + + redirect!( + fn total_balance_on_hold(who: &AccountId) -> Self::Balance; + fn balance_on_hold(reason: &Self::Reason, who: &AccountId) -> Self::Balance; + fn reducible_total_balance_on_hold(who: &AccountId, force: Fortitude) -> Self::Balance; + fn hold_available(reason: &Self::Reason, who: &AccountId) -> bool; + fn ensure_can_hold( + reason: &Self::Reason, + who: &AccountId, + amount: Self::Balance, + ) -> DispatchResult; + fn can_hold(reason: &Self::Reason, who: &AccountId, amount: Self::Balance) -> bool; + ); +} + +impl> fungibles::hold::Unbalanced + for AsFungibles +{ + redirect!( + fn set_balance_on_hold( + reason: &Self::Reason, + who: &AccountId, + amount: Self::Balance, + ) -> DispatchResult; + fn decrease_balance_on_hold( + reason: &Self::Reason, + who: &AccountId, + amount: Self::Balance, + precision: Precision, + ) -> Result; + fn increase_balance_on_hold( + reason: &Self::Reason, + who: &AccountId, + amount: Self::Balance, + precision: Precision, + ) -> Result; + ); +} + +impl> fungibles::hold::Mutate + for AsFungibles +{ + redirect!( + fn hold(reason: &Self::Reason, who: &AccountId, amount: Self::Balance) -> DispatchResult; + fn release( + reason: &Self::Reason, + who: &AccountId, + amount: Self::Balance, + precision: Precision, + ) -> Result; + fn burn_held( + reason: &Self::Reason, + who: &AccountId, + amount: Self::Balance, + precision: Precision, + force: Fortitude, + ) -> Result; + fn burn_all_held( + reason: &Self::Reason, + who: &AccountId, + precision: Precision, + force: Fortitude, + ) -> Result; + fn transfer_on_hold( + reason: &Self::Reason, + source: &AccountId, + dest: &AccountId, + amount: Self::Balance, + precision: Precision, + mode: Restriction, + force: Fortitude, + ) -> Result; + fn transfer_and_hold( + reason: &Self::Reason, + source: &AccountId, + dest: &AccountId, + amount: Self::Balance, + precision: Precision, + expendability: Preservation, + force: Fortitude, + ) -> Result; + fn done_hold(reason: &Self::Reason, who: &AccountId, amount: Self::Balance); + fn done_release(reason: &Self::Reason, who: &AccountId, amount: Self::Balance); + fn done_burn_held(reason: &Self::Reason, who: &AccountId, amount: Self::Balance); + fn done_transfer_on_hold( + reason: &Self::Reason, + source: &AccountId, + dest: &AccountId, + amount: Self::Balance, + ); + fn done_transfer_and_hold( + reason: &Self::Reason, + source: &AccountId, + dest: &AccountId, + transferred: Self::Balance, + ); + ); +} + +impl> fungibles::hold::Balanced + for AsFungibles +{ + // fn slash( + // (): Self::AssetId, + // reason: &Self::Reason, + // who: &AccountId, + // amount: Self::Balance, + // ) -> (fungibles::Credit, Self::Balance) { + // let (credit, balance) = F::slash(reason, who, amount); + // (from_fungible(credit, ()), balance) + // } + + redirect!( + fn done_slash(reason: &Self::Reason, who: &AccountId, amount: Self::Balance); + ); +} + +impl> fungibles::freeze::Inspect + for AsFungibles +{ + type Id = F::Id; + + redirect!( + fn balance_frozen(id: &Self::Id, who: &AccountId) -> Self::Balance; + fn can_freeze(id: &Self::Id, who: &AccountId) -> bool; + fn balance_freezable(who: &AccountId) -> Self::Balance; + ); +} + +impl> fungibles::freeze::Mutate + for AsFungibles +{ + redirect!( + fn set_freeze(id: &Self::Id, who: &AccountId, amount: Self::Balance) -> DispatchResult; + fn extend_freeze(id: &Self::Id, who: &AccountId, amount: Self::Balance) -> DispatchResult; + fn thaw(id: &Self::Id, who: &AccountId) -> DispatchResult; + fn set_frozen( + id: &Self::Id, + who: &AccountId, + amount: Self::Balance, + fortitude: Fortitude, + ) -> DispatchResult; + fn ensure_frozen( + id: &Self::Id, + who: &AccountId, + amount: Self::Balance, + fortitude: Fortitude, + ) -> DispatchResult; + fn decrease_frozen(id: &Self::Id, who: &AccountId, amount: Self::Balance) + -> DispatchResult; + fn increase_frozen(id: &Self::Id, who: &AccountId, amount: Self::Balance) + -> DispatchResult; + ); +} diff --git a/pallets/stream-payment/src/lib.rs b/pallets/stream-payment/src/lib.rs index 4171a9f7a..b8a435e96 100644 --- a/pallets/stream-payment/src/lib.rs +++ b/pallets/stream-payment/src/lib.rs @@ -16,6 +16,8 @@ #![cfg_attr(not(feature = "std"), no_std)] +pub mod as_fungibles; + use { frame_support::{ pallet, From ce3780512f0937b86ba08029c55b0a26f12c1a90 Mon Sep 17 00:00:00 2001 From: nanocryk <6422796+nanocryk@users.noreply.github.com> Date: Mon, 15 Jan 2024 12:05:54 +0100 Subject: [PATCH 06/40] rework pallet to not use fungibles traits --- pallets/stream-payment/Cargo.toml | 21 ++ pallets/stream-payment/src/as_fungibles.rs | 388 --------------------- pallets/stream-payment/src/lib.rs | 103 +++--- 3 files changed, 73 insertions(+), 439 deletions(-) delete mode 100644 pallets/stream-payment/src/as_fungibles.rs diff --git a/pallets/stream-payment/Cargo.toml b/pallets/stream-payment/Cargo.toml index b4bf77aa5..228ae6a9b 100644 --- a/pallets/stream-payment/Cargo.toml +++ b/pallets/stream-payment/Cargo.toml @@ -32,3 +32,24 @@ num-traits = { workspace = true } pallet-balances = { workspace = true, features = [ "std" ] } similar-asserts = { workspace = true } sp-io = { workspace = true, features = [ "std" ] } + +[features] +default = [ "std" ] +std = [ + "dp-core/std", + "frame-benchmarking/std", + "frame-support/std", + "frame-system/std", + "log/std", + "pallet-balances/std", + "parity-scale-codec/std", + "scale-info/std", + "serde", + "serde?/std", + "sp-core/std", + "sp-io/std", + "sp-runtime/std", + "sp-std/std", + "tp-maths/std", + "tp-traits/std", +] diff --git a/pallets/stream-payment/src/as_fungibles.rs b/pallets/stream-payment/src/as_fungibles.rs deleted file mode 100644 index 5a30e9799..000000000 --- a/pallets/stream-payment/src/as_fungibles.rs +++ /dev/null @@ -1,388 +0,0 @@ -// Copyright (C) Moondance Labs Ltd. -// This file is part of Tanssi. - -// Tanssi is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// Tanssi 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 General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with Tanssi. If not, see . - -//! Provides a wrapper to adapat a `fungible` into `fungibles`. - -// TODO: PR Substrate to include this convertion, which is the opposite of the -// currently present `ItemOf`. Lacking some redirects due to unaccessible -// imbalances type conversions. -// https://github.com/paritytech/polkadot-sdk/pull/2858 - -use { - core::marker::PhantomData, - frame_support::{ - sp_runtime::{DispatchError, DispatchResult}, - traits::tokens::{ - fungible, fungibles, DepositConsequence, Fortitude, Precision, Preservation, - Provenance, Restriction, WithdrawConsequence, - }, - }, -}; - -/// Redirects `fungibles` function to the `fungible` equivalent without -/// the AssetId argument. -macro_rules! redirect { - ( $( - fn $fn_name:ident ( - $( - $arg_name:ident : $arg_ty:ty - ),* $(,)? - ) $(-> $fn_out:ty)?; - )+) => { - $( - fn $fn_name((): Self::AssetId, $($arg_name:$arg_ty),*) $(-> $fn_out)? { - F::$fn_name($($arg_name),*) - } - )+ - }; -} - -pub struct ConvertHandleImbalanceDrop(PhantomData); - -impl> fungibles::HandleImbalanceDrop<(), B> - for ConvertHandleImbalanceDrop -{ - fn handle((): (), amount: B) { - H::handle(amount) - } -} - -/// A wrapper to use a `fungible` as a `fungibles` with a single asset represented by `()`. -pub struct AsFungibles(PhantomData<(F, AccountId)>); - -impl> fungibles::Inspect - for AsFungibles -{ - type AssetId = (); - type Balance = F::Balance; - - redirect!( - fn total_issuance() -> Self::Balance; - fn minimum_balance() -> Self::Balance; - fn total_balance(who: &AccountId) -> Self::Balance; - fn balance(who: &AccountId) -> Self::Balance; - fn reducible_balance( - who: &AccountId, - preservation: Preservation, - force: Fortitude, - ) -> Self::Balance; - fn can_deposit( - who: &AccountId, - amount: Self::Balance, - provenance: Provenance, - ) -> DepositConsequence; - fn can_withdraw( - who: &AccountId, - amount: Self::Balance, - ) -> WithdrawConsequence; - fn active_issuance() -> Self::Balance; - ); - - fn asset_exists((): Self::AssetId) -> bool { - true - } -} - -impl> fungibles::Unbalanced - for AsFungibles -{ - redirect!( - fn write_balance( - who: &AccountId, - amount: Self::Balance, - ) -> Result, DispatchError>; - fn set_total_issuance(amount: Self::Balance); - fn handle_raw_dust(amount: Self::Balance); - fn decrease_balance( - who: &AccountId, - amount: Self::Balance, - precision: Precision, - preservation: Preservation, - force: Fortitude, - ) -> Result; - fn increase_balance( - who: &AccountId, - amount: Self::Balance, - precision: Precision, - ) -> Result; - fn deactivate(amount: Self::Balance); - fn reactivate(amount: Self::Balance); - ); - - fn handle_dust(fungibles::Dust((), dust): fungibles::Dust) { - F::handle_dust(fungible::Dust(dust)) - } -} - -impl> fungibles::Mutate - for AsFungibles -{ - redirect!( - fn mint_into( - who: &AccountId, - amount: Self::Balance, - ) -> Result; - fn burn_from( - who: &AccountId, - amount: Self::Balance, - precision: Precision, - force: Fortitude, - ) -> Result; - fn shelve(who: &AccountId, amount: Self::Balance) -> Result; - fn restore(who: &AccountId, amount: Self::Balance) -> Result; - fn transfer( - source: &AccountId, - dest: &AccountId, - amount: Self::Balance, - preservation: Preservation, - ) -> Result; - fn set_balance(who: &AccountId, amount: Self::Balance) -> Self::Balance; - fn done_mint_into(who: &AccountId, amount: Self::Balance); - fn done_burn_from(who: &AccountId, amount: Self::Balance); - fn done_shelve(who: &AccountId, amount: Self::Balance); - fn done_restore(who: &AccountId, amount: Self::Balance); - fn done_transfer(source: &AccountId, dest: &AccountId, amount: Self::Balance); - ); -} - -impl> fungibles::Balanced - for AsFungibles -{ - type OnDropDebt = ConvertHandleImbalanceDrop; - type OnDropCredit = ConvertHandleImbalanceDrop; - - // fn rescind((): Self::AssetId, amount: Self::Balance) -> fungibles::Debt { - // let dept = F::rescind(amount); - // from_fungible(dept, ()) - // } - - // fn issue((): Self::AssetId, amount: Self::Balance) -> fungibles::Credit { - // let credit = F::issue(amount); - // from_fungible(credit, ()) - // } - - // fn pair( - // (): Self::AssetId, - // amount: Self::Balance, - // ) -> (fungibles::Debt, fungibles::Credit) { - // let (dept, credit) = F::pair(amount); - // (from_fungible(dept, ()), from_fungible(credit, ())) - // } - - // fn deposit( - // (): Self::AssetId, - // who: &AccountId, - // value: Self::Balance, - // precision: Precision, - // ) -> Result, DispatchError> { - // F::deposit(who, value, precision).map(|dept| from_fungible(dept, ())) - // } - - // fn withdraw( - // (): Self::AssetId, - // who: &AccountId, - // value: Self::Balance, - // precision: Precision, - // preservation: Preservation, - // force: Fortitude, - // ) -> Result, DispatchError> { - // F::withdraw(who, value, precision, preservation, force) - // .map(|credit| from_fungible(credit, ())) - // } - - // fn resolve( - // who: &AccountId, - // credit: fungibles::Credit, - // ) -> Result<(), fungibles::Credit> { - // F::resolve(who, from_fungibles(credit)).map_err(|credit| from_fungible(credit, ())) - // } - - // fn settle( - // who: &AccountId, - // debt: fungibles::Debt, - // preservation: Preservation, - // ) -> Result, fungibles::Debt> { - // F::settle(who, from_fungibles(debt), preservation) - // .map(|credit| from_fungible(credit, ())) - // .map_err(|dept| from_fungible(dept, ())) - // } - - redirect!( - fn done_rescind(amount: Self::Balance); - fn done_issue(amount: Self::Balance); - fn done_deposit(who: &AccountId, amount: Self::Balance); - fn done_withdraw(who: &AccountId, amount: Self::Balance); - ); -} - -impl> fungibles::hold::Inspect - for AsFungibles -{ - type Reason = F::Reason; - - redirect!( - fn total_balance_on_hold(who: &AccountId) -> Self::Balance; - fn balance_on_hold(reason: &Self::Reason, who: &AccountId) -> Self::Balance; - fn reducible_total_balance_on_hold(who: &AccountId, force: Fortitude) -> Self::Balance; - fn hold_available(reason: &Self::Reason, who: &AccountId) -> bool; - fn ensure_can_hold( - reason: &Self::Reason, - who: &AccountId, - amount: Self::Balance, - ) -> DispatchResult; - fn can_hold(reason: &Self::Reason, who: &AccountId, amount: Self::Balance) -> bool; - ); -} - -impl> fungibles::hold::Unbalanced - for AsFungibles -{ - redirect!( - fn set_balance_on_hold( - reason: &Self::Reason, - who: &AccountId, - amount: Self::Balance, - ) -> DispatchResult; - fn decrease_balance_on_hold( - reason: &Self::Reason, - who: &AccountId, - amount: Self::Balance, - precision: Precision, - ) -> Result; - fn increase_balance_on_hold( - reason: &Self::Reason, - who: &AccountId, - amount: Self::Balance, - precision: Precision, - ) -> Result; - ); -} - -impl> fungibles::hold::Mutate - for AsFungibles -{ - redirect!( - fn hold(reason: &Self::Reason, who: &AccountId, amount: Self::Balance) -> DispatchResult; - fn release( - reason: &Self::Reason, - who: &AccountId, - amount: Self::Balance, - precision: Precision, - ) -> Result; - fn burn_held( - reason: &Self::Reason, - who: &AccountId, - amount: Self::Balance, - precision: Precision, - force: Fortitude, - ) -> Result; - fn burn_all_held( - reason: &Self::Reason, - who: &AccountId, - precision: Precision, - force: Fortitude, - ) -> Result; - fn transfer_on_hold( - reason: &Self::Reason, - source: &AccountId, - dest: &AccountId, - amount: Self::Balance, - precision: Precision, - mode: Restriction, - force: Fortitude, - ) -> Result; - fn transfer_and_hold( - reason: &Self::Reason, - source: &AccountId, - dest: &AccountId, - amount: Self::Balance, - precision: Precision, - expendability: Preservation, - force: Fortitude, - ) -> Result; - fn done_hold(reason: &Self::Reason, who: &AccountId, amount: Self::Balance); - fn done_release(reason: &Self::Reason, who: &AccountId, amount: Self::Balance); - fn done_burn_held(reason: &Self::Reason, who: &AccountId, amount: Self::Balance); - fn done_transfer_on_hold( - reason: &Self::Reason, - source: &AccountId, - dest: &AccountId, - amount: Self::Balance, - ); - fn done_transfer_and_hold( - reason: &Self::Reason, - source: &AccountId, - dest: &AccountId, - transferred: Self::Balance, - ); - ); -} - -impl> fungibles::hold::Balanced - for AsFungibles -{ - // fn slash( - // (): Self::AssetId, - // reason: &Self::Reason, - // who: &AccountId, - // amount: Self::Balance, - // ) -> (fungibles::Credit, Self::Balance) { - // let (credit, balance) = F::slash(reason, who, amount); - // (from_fungible(credit, ()), balance) - // } - - redirect!( - fn done_slash(reason: &Self::Reason, who: &AccountId, amount: Self::Balance); - ); -} - -impl> fungibles::freeze::Inspect - for AsFungibles -{ - type Id = F::Id; - - redirect!( - fn balance_frozen(id: &Self::Id, who: &AccountId) -> Self::Balance; - fn can_freeze(id: &Self::Id, who: &AccountId) -> bool; - fn balance_freezable(who: &AccountId) -> Self::Balance; - ); -} - -impl> fungibles::freeze::Mutate - for AsFungibles -{ - redirect!( - fn set_freeze(id: &Self::Id, who: &AccountId, amount: Self::Balance) -> DispatchResult; - fn extend_freeze(id: &Self::Id, who: &AccountId, amount: Self::Balance) -> DispatchResult; - fn thaw(id: &Self::Id, who: &AccountId) -> DispatchResult; - fn set_frozen( - id: &Self::Id, - who: &AccountId, - amount: Self::Balance, - fortitude: Fortitude, - ) -> DispatchResult; - fn ensure_frozen( - id: &Self::Id, - who: &AccountId, - amount: Self::Balance, - fortitude: Fortitude, - ) -> DispatchResult; - fn decrease_frozen(id: &Self::Id, who: &AccountId, amount: Self::Balance) - -> DispatchResult; - fn increase_frozen(id: &Self::Id, who: &AccountId, amount: Self::Balance) - -> DispatchResult; - ); -} diff --git a/pallets/stream-payment/src/lib.rs b/pallets/stream-payment/src/lib.rs index b8a435e96..30d23363c 100644 --- a/pallets/stream-payment/src/lib.rs +++ b/pallets/stream-payment/src/lib.rs @@ -16,17 +16,17 @@ #![cfg_attr(not(feature = "std"), no_std)] -pub mod as_fungibles; + + +#[cfg(feature = "std")] +use serde::{Deserialize, Serialize}; use { frame_support::{ pallet, pallet_prelude::*, storage::types::{StorageDoubleMap, StorageMap}, - traits::{ - fungibles::{self, Mutate as _, MutateFreeze as _}, - tokens::{Balance, Preservation}, - }, + traits::tokens::Balance, Blake2_128Concat, }, frame_system::pallet_prelude::*, @@ -36,6 +36,8 @@ use { sp_std::{fmt::Debug, marker::PhantomData}, }; +pub use pallet::*; + /// Type able to provide the current time for given unit. /// For each unit the returned number should monotonically increase and not /// overflow. @@ -43,6 +45,25 @@ pub trait TimeProvider { fn now(unit: &Unit) -> Option; } +/// Interactions the pallet needs with assets. +pub trait Assets { + /// Transfer assets deposited by an account to another account. + /// Those assets should not be considered deposited in the target account. + fn transfer_deposit( + asset_id: AssetId, + from: &AccountId, + to: &AccountId, + amount: Balance, + ) -> DispatchResult; + + /// Increase the deposit for an account and asset id. Should fail if account doesn't have + /// enough of that asset. Funds should be safe and not slashable. + fn increase_deposit(asset_id: AssetId, account: &AccountId, amount: Balance) -> DispatchResult; + + /// Decrease the deposit for an account and asset id. Should fail on underflow. + fn decrease_deposit(asset_id: AssetId, account: &AccountId, amount: Balance) -> DispatchResult; +} + #[pallet(dev_mode)] pub mod pallet { use super::*; @@ -67,30 +88,27 @@ pub mod pallet { + TypeInfo + MaxEncodedLen; - /// Represents which units of time can be used. Designed to be an enum - /// with a variant for each kind of time source/scale supported. - type TimeUnit: Debug + Clone + FullCodec + TypeInfo + MaxEncodedLen + Eq; - /// The balance type, which is also the type representing time (as this /// pallet will do math with both time and balances to compute how /// much should be paid). type Balance: Balance; - /// LockId type used by `Currencies`. - type LockId: From; + /// Type representing an asset id, a identifier allowing distinguishing assets. + type AssetId: Debug + Clone + FullCodec + TypeInfo + MaxEncodedLen + PartialEq + Eq; - /// The currencies type, supporting multiple currencies. - type Currencies: fungibles::Inspect - + fungibles::InspectFreeze - + fungibles::Mutate - + fungibles::MutateFreeze; + /// Provide interaction with assets. + type Assets: Assets; + + /// Represents which units of time can be used. Designed to be an enum + /// with a variant for each kind of time source/scale supported. + type TimeUnit: Debug + Clone + FullCodec + TypeInfo + MaxEncodedLen + Eq; /// Provide the current time in given unit. type TimeProvider: TimeProvider; } type AccountIdOf = ::AccountId; - type AssetIdOf = <::Currencies as fungibles::Inspect>>::AssetId; + type AssetIdOf = ::AssetId; /// A stream payment from source to target. /// Stores the last time the stream was updated, which allows to compute @@ -98,13 +116,13 @@ pub mod pallet { #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] #[derive(RuntimeDebug, PartialEq, Eq, Encode, Decode, Clone, TypeInfo)] pub struct Stream { - source: AccountId, - target: AccountId, - time_unit: Unit, - asset_id: AssetId, - rate_per_time_unit: Balance, - deposit: Balance, - last_time_updated: Balance, + pub source: AccountId, + pub target: AccountId, + pub time_unit: Unit, + pub asset_id: AssetId, + pub rate_per_time_unit: Balance, + pub deposit: Balance, + pub last_time_updated: Balance, } pub type StreamOf = @@ -190,8 +208,9 @@ pub mod pallet { }, } + /// Freeze reason to use if needed. #[pallet::composite_enum] - pub enum LockId { + pub enum FreezeReason { StreamPayment, } @@ -217,12 +236,7 @@ pub mod pallet { NextStreamId::::set(next_stream_id); // Freeze initial deposit. - T::Currencies::increase_frozen( - asset_id.clone(), - &LockId::StreamPayment.into(), - &origin, - initial_deposit, - )?; + T::Assets::increase_deposit(asset_id.clone(), &origin, initial_deposit)?; // Create stream data. let now = T::TimeProvider::now(&time_unit).ok_or(Error::::CantFetchCurrentTime)?; @@ -265,12 +279,7 @@ pub mod pallet { Self::perform_stream_payment(stream_id, &mut stream)?; // Unfreeze funds left in the stream. - T::Currencies::decrease_frozen( - stream.asset_id.clone(), - &LockId::StreamPayment.into(), - &stream.source, - stream.deposit, - )?; + T::Assets::decrease_deposit(stream.asset_id.clone(), &stream.source, stream.deposit)?; // Remove stream from storage. Streams::::remove(stream_id); @@ -318,12 +327,7 @@ pub mod pallet { Self::perform_stream_payment(stream_id, &mut stream)?; // Increase deposit. - T::Currencies::increase_frozen( - stream.asset_id.clone(), - &LockId::StreamPayment.into(), - &origin, - new_deposit, - )?; + T::Assets::increase_deposit(stream.asset_id.clone(), &origin, new_deposit)?; stream.deposit = stream .deposit .checked_add(&new_deposit) @@ -399,6 +403,10 @@ pub mod pallet { let now = T::TimeProvider::now(&stream.time_unit).ok_or(Error::::CantFetchCurrentTime)?; + if now == stream.last_time_updated { + return Ok(().into()); + } + // If deposit is zero the stream is fully drained and there is nothing to transfer. if stream.deposit.is_zero() { stream.last_time_updated = now; @@ -427,18 +435,11 @@ pub mod pallet { }; // Transfer from the source to target. - T::Currencies::decrease_frozen( - stream.asset_id.clone(), - &LockId::StreamPayment.into(), - &stream.source, - payment, - )?; - T::Currencies::transfer( + T::Assets::transfer_deposit( stream.asset_id.clone(), &stream.source, &stream.target, payment, - Preservation::Preserve, )?; // Update stream info. From ebe7a44e518bbff455d2d5d0edf57a73270b3239 Mon Sep 17 00:00:00 2001 From: nanocryk <6422796+nanocryk@users.noreply.github.com> Date: Mon, 15 Jan 2024 12:06:13 +0100 Subject: [PATCH 07/40] mock + some tests --- pallets/stream-payment/src/lib.rs | 4 + pallets/stream-payment/src/mock.rs | 422 +++++++++++++++++++++++++++ pallets/stream-payment/src/tests.rs | 425 ++++++++++++++++++++++++++++ 3 files changed, 851 insertions(+) create mode 100644 pallets/stream-payment/src/mock.rs create mode 100644 pallets/stream-payment/src/tests.rs diff --git a/pallets/stream-payment/src/lib.rs b/pallets/stream-payment/src/lib.rs index 30d23363c..80c972f45 100644 --- a/pallets/stream-payment/src/lib.rs +++ b/pallets/stream-payment/src/lib.rs @@ -16,7 +16,11 @@ #![cfg_attr(not(feature = "std"), no_std)] +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; #[cfg(feature = "std")] use serde::{Deserialize, Serialize}; diff --git a/pallets/stream-payment/src/mock.rs b/pallets/stream-payment/src/mock.rs new file mode 100644 index 000000000..4962fdf17 --- /dev/null +++ b/pallets/stream-payment/src/mock.rs @@ -0,0 +1,422 @@ +// Copyright (C) Moondance Labs Ltd. +// This file is part of Tanssi. + +// Tanssi is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Tanssi 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Tanssi. If not, see + +use { + crate as pallet_stream_payment, + frame_support::{ + parameter_types, + traits::{ + tokens::{ + fungible::{Mutate, MutateFreeze}, + Preservation, + }, + Everything, OnFinalize, OnInitialize, + }, + }, + parity_scale_codec::{Decode, Encode, MaxEncodedLen}, + scale_info::TypeInfo, + sp_core::{ConstU32, ConstU64, RuntimeDebug, H256}, + sp_runtime::{ + traits::{BlakeTwo256, IdentityLookup}, + BuildStorage, + }, +}; + +type Block = frame_system::mocking::MockBlock; +pub type AccountId = u64; +pub type Balance = u128; + +pub const ALICE: u64 = 0; +pub const BOB: u64 = 1; +pub const CHARLIE: u64 = 2; + +pub const KILO: u128 = 1000; +pub const MEGA: u128 = 1000 * KILO; +pub const GIGA: u128 = 1000 * MEGA; +pub const TERA: u128 = 1000 * GIGA; +pub const PETA: u128 = 1000 * TERA; +pub const DEFAULT_BALANCE: u128 = PETA; + +// Configure a mock runtime to test the pallet. +frame_support::construct_runtime!( + pub enum Runtime + { + System: frame_system, + Balances: pallet_balances, + StreamPayment: pallet_stream_payment, + } +); + +impl frame_system::Config for Runtime { + type BaseCallFilter = Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Nonce = u64; + type Block = Block; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = ConstU64<250>; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = ConstU32<16>; +} + +parameter_types! { + pub ExistentialDeposit: u128 = 1; +} + +impl pallet_balances::Config for Runtime { + type MaxReserves = (); + type ReserveIdentifier = [u8; 4]; + type MaxLocks = (); + type Balance = Balance; + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type FreezeIdentifier = RuntimeFreezeReason; + type MaxFreezes = ConstU32<1>; + type RuntimeHoldReason = RuntimeHoldReason; + type RuntimeFreezeReason = RuntimeFreezeReason; + type MaxHolds = ConstU32<5>; + type WeightInfo = (); +} + +#[derive(RuntimeDebug, PartialEq, Eq, Encode, Decode, Clone, TypeInfo, MaxEncodedLen)] +pub enum StreamPaymentAssetId { + Native, + Dummy, +} + +pub struct StreamPaymentAssets; +impl pallet_stream_payment::Assets + for StreamPaymentAssets +{ + fn transfer_deposit( + asset_id: StreamPaymentAssetId, + from: &AccountId, + to: &AccountId, + amount: Balance, + ) -> frame_support::pallet_prelude::DispatchResult { + Self::decrease_deposit(asset_id.clone(), from, amount)?; + match asset_id { + StreamPaymentAssetId::Native => { + Balances::transfer(from, to, amount, Preservation::Preserve).map(|_| ()) + } + StreamPaymentAssetId::Dummy => Ok(()), + } + } + + fn increase_deposit( + asset_id: StreamPaymentAssetId, + account: &AccountId, + amount: Balance, + ) -> frame_support::pallet_prelude::DispatchResult { + match asset_id { + StreamPaymentAssetId::Native => Balances::increase_frozen( + &pallet_stream_payment::FreezeReason::StreamPayment.into(), + account, + amount, + ), + StreamPaymentAssetId::Dummy => Ok(()), + } + } + + fn decrease_deposit( + asset_id: StreamPaymentAssetId, + account: &AccountId, + amount: Balance, + ) -> frame_support::pallet_prelude::DispatchResult { + match asset_id { + StreamPaymentAssetId::Native => Balances::decrease_frozen( + &pallet_stream_payment::FreezeReason::StreamPayment.into(), + account, + amount, + ), + StreamPaymentAssetId::Dummy => Ok(()), + } + } +} + +#[derive(RuntimeDebug, PartialEq, Eq, Encode, Decode, Clone, TypeInfo, MaxEncodedLen)] +pub enum TimeUnit { + BlockNumber, + Timestamp, + Never, +} + +pub struct TimeProvider; +impl pallet_stream_payment::TimeProvider for TimeProvider { + fn now(unit: &TimeUnit) -> Option { + match unit { + &TimeUnit::BlockNumber => Some(System::block_number().into()), + &TimeUnit::Timestamp => todo!(), + &TimeUnit::Never => None, + } + } +} + +impl pallet_stream_payment::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type StreamId = u64; + type TimeUnit = TimeUnit; + type Balance = Balance; + type AssetId = StreamPaymentAssetId; + type Assets = StreamPaymentAssets; + type TimeProvider = TimeProvider; +} + +pub(crate) struct ExtBuilder { + // endowed accounts with balances + balances: Vec<(AccountId, Balance)>, +} + +impl Default for ExtBuilder { + fn default() -> ExtBuilder { + ExtBuilder { + balances: vec![ + (ALICE, 1 * DEFAULT_BALANCE), + (BOB, 1 * DEFAULT_BALANCE), + (CHARLIE, 1 * DEFAULT_BALANCE), + ], + } + } +} + +impl ExtBuilder { + #[allow(dead_code)] + pub(crate) fn with_balances(mut self, balances: Vec<(AccountId, Balance)>) -> Self { + self.balances = balances; + self + } + + pub(crate) fn build(self) -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::::default() + .build_storage() + .expect("Frame system builds valid default genesis config"); + + pallet_balances::GenesisConfig:: { + balances: self.balances, + } + .assimilate_storage(&mut t) + .expect("Pallet balances storage can be assimilated"); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext + } +} + +/// Rolls forward one block. Returns the new block number. +#[allow(dead_code)] +pub(crate) fn roll_one_block() -> u64 { + Balances::on_finalize(System::block_number()); + System::on_finalize(System::block_number()); + System::set_block_number(System::block_number() + 1); + System::on_initialize(System::block_number()); + Balances::on_initialize(System::block_number()); + System::block_number() +} + +/// Rolls to the desired block. Returns the number of blocks played. +#[allow(dead_code)] +pub(crate) fn roll_to(n: u64) -> u64 { + let mut num_blocks = 0; + let mut block = System::block_number(); + while block < n { + block = roll_one_block(); + num_blocks += 1; + } + num_blocks +} + +#[allow(dead_code)] +pub(crate) fn last_event() -> RuntimeEvent { + System::events().pop().expect("Event expected").event +} + +#[allow(dead_code)] +pub(crate) fn events() -> Vec> { + System::events() + .into_iter() + .map(|r| r.event) + .filter_map(|e| { + if let RuntimeEvent::StreamPayment(inner) = e { + Some(inner) + } else { + None + } + }) + .collect::>() +} + +/// Assert input equal to the last event emitted +#[macro_export] +macro_rules! assert_last_event { + ($event:expr) => { + match &$event { + e => assert_eq!(*e, $crate::mock::last_event()), + } + }; +} + +/// Compares the system events with passed in events +/// Prints highlighted diff iff assert_eq fails +#[macro_export] +macro_rules! assert_eq_events { + ($events:expr) => { + match &$events { + e => similar_asserts::assert_eq!(*e, $crate::mock::events()), + } + }; +} + +/// Compares the last N system events with passed in events, where N is the length of events passed +/// in. +/// +/// Prints highlighted diff iff assert_eq fails. +/// The last events from frame_system will be taken in order to match the number passed to this +/// macro. If there are insufficient events from frame_system, they will still be compared; the +/// output may or may not be helpful. +/// +/// Examples: +/// If frame_system has events [A, B, C, D, E] and events [C, D, E] are passed in, the result would +/// be a successful match ([C, D, E] == [C, D, E]). +/// +/// If frame_system has events [A, B, C, D] and events [B, C] are passed in, the result would be an +/// error and a hopefully-useful diff will be printed between [C, D] and [B, C]. +/// +/// Note that events are filtered to only match parachain-staking (see events()). +#[macro_export] +macro_rules! assert_eq_last_events { + ($events:expr) => { + $crate::assert_tail_eq!($events, $crate::mock::events()) + }; +} + +/// Assert that one array is equal to the tail of the other. A more generic and testable version of +/// assert_eq_last_events. +#[macro_export] +macro_rules! assert_tail_eq { + ($tail:expr, $arr:expr) => { + if $tail.len() != 0 { + // 0-length always passes + + if $tail.len() > $arr.len() { + similar_asserts::assert_eq!($tail, $arr); // will fail + } + + let len_diff = $arr.len() - $tail.len(); + similar_asserts::assert_eq!($tail, $arr[len_diff..]); + } + }; +} + +/// Panics if an event is not found in the system log of events +#[macro_export] +macro_rules! assert_event_emitted { + ($event:expr) => { + match &$event { + e => { + assert!( + $crate::mock::events().iter().find(|x| *x == e).is_some(), + "Event {:?} was not found in events: \n {:?}", + e, + $crate::mock::events() + ); + } + } + }; +} + +/// Panics if an event is found in the system log of events +#[macro_export] +macro_rules! assert_event_not_emitted { + ($event:expr) => { + match &$event { + e => { + assert!( + $crate::mock::events().iter().find(|x| *x == e).is_none(), + "Event {:?} was found in events: \n {:?}", + e, + $crate::mock::events() + ); + } + } + }; +} + +#[macro_export] +macro_rules! assert_fields_eq { + ($left:expr, $right:expr, $field:ident) => { + assert_eq!($left.$field, $right.$field); + }; + ($left:expr, $right:expr, [$( $field:ident ),+ $(,)?] ) => { + $( + assert_eq!($left.$field, $right.$field); + )+ + }; +} + +#[test] +fn assert_tail_eq_works() { + assert_tail_eq!(vec![1, 2], vec![0, 1, 2]); + + assert_tail_eq!(vec![1], vec![1]); + + assert_tail_eq!( + vec![0u32; 0], // 0 length array + vec![0u32; 1] // 1-length array + ); + + assert_tail_eq!(vec![0u32, 0], vec![0u32, 0]); +} + +#[test] +#[should_panic] +fn assert_tail_eq_panics_on_non_equal_tail() { + assert_tail_eq!(vec![2, 2], vec![0, 1, 2]); +} + +#[test] +#[should_panic] +fn assert_tail_eq_panics_on_empty_arr() { + assert_tail_eq!(vec![2, 2], vec![0u32; 0]); +} + +#[test] +#[should_panic] +fn assert_tail_eq_panics_on_longer_tail() { + assert_tail_eq!(vec![1, 2, 3], vec![1, 2]); +} + +#[test] +#[should_panic] +fn assert_tail_eq_panics_on_unequal_elements_same_length_array() { + assert_tail_eq!(vec![1, 2, 3], vec![0, 1, 2]); +} diff --git a/pallets/stream-payment/src/tests.rs b/pallets/stream-payment/src/tests.rs new file mode 100644 index 000000000..1a07e86c7 --- /dev/null +++ b/pallets/stream-payment/src/tests.rs @@ -0,0 +1,425 @@ +// Copyright (C) Moondance Labs Ltd. +// This file is part of Tanssi. + +// Tanssi is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Tanssi 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Tanssi. If not, see + +use { + crate::{ + assert_event_emitted, + mock::{ + roll_to, Balances, ExtBuilder, Runtime, RuntimeOrigin, StreamPayment, + StreamPaymentAssetId, TimeUnit, ALICE, BOB, CHARLIE, DEFAULT_BALANCE, MEGA, + }, + Error, Event, FreezeReason, LookupStreamsWithSource, LookupStreamsWithTarget, NextStreamId, + Stream, Streams, + }, + frame_support::{assert_err, assert_ok, traits::fungible::InspectFreeze}, + sp_runtime::TokenError, +}; + +mod open_stream { + + use super::*; + + #[test] + fn cant_be_both_source_and_target() { + ExtBuilder::default().build().execute_with(|| { + assert_err!( + StreamPayment::open_stream( + RuntimeOrigin::signed(ALICE), + ALICE, + TimeUnit::BlockNumber, + StreamPaymentAssetId::Native, + 100, + 0 + ), + Error::::CantBeBothSourceAndTarget + ); + }) + } + + #[test] + fn stream_id_cannot_overflow() { + ExtBuilder::default().build().execute_with(|| { + NextStreamId::::set(u64::MAX); + + assert_err!( + StreamPayment::open_stream( + RuntimeOrigin::signed(ALICE), + BOB, + TimeUnit::BlockNumber, + StreamPaymentAssetId::Native, + 100, + 0 + ), + Error::::StreamIdOverflow + ); + }) + } + + #[test] + fn balance_too_low_for_deposit() { + ExtBuilder::default() + .with_balances(vec![(ALICE, 1_000_000)]) + .build() + .execute_with(|| { + assert_err!( + StreamPayment::open_stream( + RuntimeOrigin::signed(ALICE), + BOB, + TimeUnit::BlockNumber, + StreamPaymentAssetId::Native, + 100, + 1_000_001 + ), + TokenError::FundsUnavailable, + ); + }) + } + + #[test] + fn time_can_be_fetched() { + ExtBuilder::default().build().execute_with(|| { + assert_err!( + StreamPayment::open_stream( + RuntimeOrigin::signed(ALICE), + BOB, + TimeUnit::Never, + StreamPaymentAssetId::Native, + 100, + 1 * MEGA + ), + Error::::CantFetchCurrentTime, + ); + }) + } + + #[test] + fn stream_opened() { + ExtBuilder::default().build().execute_with(|| { + assert!(Streams::::get(0).is_none()); + + assert_ok!(StreamPayment::open_stream( + RuntimeOrigin::signed(ALICE), + BOB, + TimeUnit::BlockNumber, + StreamPaymentAssetId::Native, + 100, + 1 * MEGA + )); + + assert_event_emitted!(Event::::StreamOpened { stream_id: 0 }); + assert!(Streams::::get(0).is_some()); + assert_eq!( + LookupStreamsWithSource::::iter_key_prefix(ALICE).collect::>(), + &[0] + ); + assert!(LookupStreamsWithSource::::iter_key_prefix(BOB) + .collect::>() + .is_empty()); + assert!(LookupStreamsWithTarget::::iter_key_prefix(ALICE) + .collect::>() + .is_empty()); + assert_eq!( + LookupStreamsWithTarget::::iter_key_prefix(BOB).collect::>(), + &[0] + ); + + assert_eq!( + Balances::balance_frozen(&FreezeReason::StreamPayment.into(), &ALICE), + 1 * MEGA + ); + assert_eq!( + Balances::balance_frozen(&FreezeReason::StreamPayment.into(), &BOB), + 0 + ); + }) + } +} + +mod update_stream { + + use super::*; + + #[test] + fn cannot_update_unknown_stream() { + ExtBuilder::default().build().execute_with(|| { + assert_err!( + StreamPayment::update_stream(RuntimeOrigin::signed(ALICE), 0), + Error::::UnknownStreamId + ); + }) + } + + #[test] + fn update_stream_works() { + ExtBuilder::default().build().execute_with(|| { + let rate = 100; + let initial_deposit = 1 * MEGA; + + assert_ok!(StreamPayment::open_stream( + RuntimeOrigin::signed(ALICE), + BOB, + TimeUnit::BlockNumber, + StreamPaymentAssetId::Native, + rate, + initial_deposit + )); + + assert_eq!(Balances::free_balance(&ALICE), DEFAULT_BALANCE); + assert_eq!( + Balances::balance_frozen(&FreezeReason::StreamPayment.into(), &ALICE), + initial_deposit + ); + + let delta = roll_to(10) as u128; + let payment = delta * rate; + let deposit_left = initial_deposit - payment; + + assert_ok!(StreamPayment::update_stream( + // Anyone can dispatch an update. + RuntimeOrigin::signed(CHARLIE), + 0 + )); + + assert_event_emitted!(Event::::StreamPayment { + stream_id: 0, + source: ALICE, + target: BOB, + amount: payment, + drained: false + }); + assert_eq!( + Streams::::get(0), + Some(Stream { + source: ALICE, + target: BOB, + time_unit: TimeUnit::BlockNumber, + asset_id: StreamPaymentAssetId::Native, + rate_per_time_unit: rate, + deposit: deposit_left, + last_time_updated: 10 + }) + ); + + assert_eq!( + Balances::balance_frozen(&FreezeReason::StreamPayment.into(), &ALICE), + deposit_left + ); + + assert_eq!(Balances::free_balance(ALICE), DEFAULT_BALANCE - payment); + assert_eq!(Balances::free_balance(BOB), DEFAULT_BALANCE + payment); + }) + } +} + +mod close_stream { + use super::*; + + #[test] + fn cannot_close_unknown_stream() { + ExtBuilder::default().build().execute_with(|| { + assert_err!( + StreamPayment::close_stream(RuntimeOrigin::signed(ALICE), 0), + Error::::UnknownStreamId + ); + }) + } + + #[test] + fn stream_cant_be_closed_by_third_party() { + ExtBuilder::default().build().execute_with(|| { + let rate = 100; + let initial_deposit = 1 * MEGA; + + assert_ok!(StreamPayment::open_stream( + RuntimeOrigin::signed(ALICE), + BOB, + TimeUnit::BlockNumber, + StreamPaymentAssetId::Native, + rate, + initial_deposit + )); + + assert_eq!(Balances::free_balance(&ALICE), DEFAULT_BALANCE); + assert_eq!( + Balances::balance_frozen(&FreezeReason::StreamPayment.into(), &ALICE), + initial_deposit + ); + + assert_err!( + StreamPayment::close_stream(RuntimeOrigin::signed(CHARLIE), 0), + Error::::UnauthorizedOrigin + ); + }) + } + + #[test] + fn stream_can_be_closed_by_source() { + ExtBuilder::default().build().execute_with(|| { + let rate = 100; + let initial_deposit = 1 * MEGA; + + assert_ok!(StreamPayment::open_stream( + RuntimeOrigin::signed(ALICE), + BOB, + TimeUnit::BlockNumber, + StreamPaymentAssetId::Native, + rate, + initial_deposit + )); + + assert_eq!(Balances::free_balance(&ALICE), DEFAULT_BALANCE); + assert_eq!( + Balances::balance_frozen(&FreezeReason::StreamPayment.into(), &ALICE), + initial_deposit + ); + + assert_ok!(StreamPayment::close_stream(RuntimeOrigin::signed(ALICE), 0),); + assert_event_emitted!(Event::::StreamClosed { + stream_id: 0, + refunded: initial_deposit + }); + assert_eq!(Streams::::get(0), None); + }) + } + + #[test] + fn stream_can_be_closed_by_target() { + ExtBuilder::default().build().execute_with(|| { + let rate = 100; + let initial_deposit = 1 * MEGA; + + assert_ok!(StreamPayment::open_stream( + RuntimeOrigin::signed(ALICE), + BOB, + TimeUnit::BlockNumber, + StreamPaymentAssetId::Native, + rate, + initial_deposit + )); + + assert_eq!(Balances::free_balance(&ALICE), DEFAULT_BALANCE); + assert_eq!( + Balances::balance_frozen(&FreezeReason::StreamPayment.into(), &ALICE), + initial_deposit + ); + + assert_ok!(StreamPayment::close_stream(RuntimeOrigin::signed(BOB), 0),); + assert_event_emitted!(Event::::StreamClosed { + stream_id: 0, + refunded: initial_deposit + }); + assert_eq!(Streams::::get(0), None); + }) + } + + #[test] + fn close_stream_with_payment() { + ExtBuilder::default().build().execute_with(|| { + let rate = 100; + let initial_deposit = 1 * MEGA; + + assert_ok!(StreamPayment::open_stream( + RuntimeOrigin::signed(ALICE), + BOB, + TimeUnit::BlockNumber, + StreamPaymentAssetId::Native, + rate, + initial_deposit + )); + + assert_eq!(Balances::free_balance(&ALICE), DEFAULT_BALANCE); + assert_eq!( + Balances::balance_frozen(&FreezeReason::StreamPayment.into(), &ALICE), + initial_deposit + ); + + let delta = roll_to(10) as u128; + let payment = delta * rate; + let deposit_left = initial_deposit - payment; + + assert_ok!(StreamPayment::close_stream(RuntimeOrigin::signed(ALICE), 0)); + + assert_event_emitted!(Event::::StreamPayment { + stream_id: 0, + source: ALICE, + target: BOB, + amount: payment, + drained: false + }); + assert_event_emitted!(Event::::StreamClosed { + stream_id: 0, + refunded: deposit_left + }); + assert_eq!(Streams::::get(0), None); + + assert_eq!( + Balances::balance_frozen(&FreezeReason::StreamPayment.into(), &ALICE), + 0 + ); + + assert_eq!(Balances::free_balance(ALICE), DEFAULT_BALANCE - payment); + assert_eq!(Balances::free_balance(BOB), DEFAULT_BALANCE + payment); + }) + } +} + +mod refill_stream { + use super::*; + + #[test] + fn third_party_cannot_refill() { + ExtBuilder::default().build().execute_with(|| { + let rate = 100; + let initial_deposit = 1 * MEGA; + + assert_ok!(StreamPayment::open_stream( + RuntimeOrigin::signed(ALICE), + BOB, + TimeUnit::BlockNumber, + StreamPaymentAssetId::Native, + rate, + initial_deposit + )); + + assert_err!( + StreamPayment::refill_stream(RuntimeOrigin::signed(CHARLIE), 0, initial_deposit), + Error::::UnauthorizedOrigin + ); + }) + } + + #[test] + fn target_cannot_refill() { + ExtBuilder::default().build().execute_with(|| { + let rate = 100; + let initial_deposit = 1 * MEGA; + + assert_ok!(StreamPayment::open_stream( + RuntimeOrigin::signed(ALICE), + BOB, + TimeUnit::BlockNumber, + StreamPaymentAssetId::Native, + rate, + initial_deposit + )); + + assert_err!( + StreamPayment::refill_stream(RuntimeOrigin::signed(BOB), 0, initial_deposit), + Error::::UnauthorizedOrigin + ); + }) + } +} From ffc09366cf081271691b084d9955c574b8823006 Mon Sep 17 00:00:00 2001 From: nanocryk <6422796+nanocryk@users.noreply.github.com> Date: Wed, 17 Jan 2024 17:12:49 +0100 Subject: [PATCH 08/40] more tests --- pallets/stream-payment/src/lib.rs | 18 +- pallets/stream-payment/src/tests.rs | 349 +++++++++++++++++++++++++++- 2 files changed, 363 insertions(+), 4 deletions(-) diff --git a/pallets/stream-payment/src/lib.rs b/pallets/stream-payment/src/lib.rs index 80c972f45..b3db5d942 100644 --- a/pallets/stream-payment/src/lib.rs +++ b/pallets/stream-payment/src/lib.rs @@ -205,6 +205,11 @@ pub mod pallet { amount: T::Balance, drained: bool, }, + StreamRefilled { + stream_id: T::StreamId, + increase: T::Balance, + new_deposit: T::Balance, + }, StreamRateChanged { stream_id: T::StreamId, old_rate: T::Balance, @@ -318,7 +323,7 @@ pub mod pallet { pub fn refill_stream( origin: OriginFor, stream_id: T::StreamId, - new_deposit: T::Balance, + increase: T::Balance, ) -> DispatchResultWithPostInfo { let origin = ensure_signed(origin)?; let mut stream = Streams::::get(stream_id).ok_or(Error::::UnknownStreamId)?; @@ -331,12 +336,19 @@ pub mod pallet { Self::perform_stream_payment(stream_id, &mut stream)?; // Increase deposit. - T::Assets::increase_deposit(stream.asset_id.clone(), &origin, new_deposit)?; + T::Assets::increase_deposit(stream.asset_id.clone(), &origin, increase)?; stream.deposit = stream .deposit - .checked_add(&new_deposit) + .checked_add(&increase) .ok_or(Error::::CurrencyOverflow)?; + // Emit event. + Pallet::::deposit_event(Event::::StreamRefilled { + stream_id, + increase, + new_deposit: stream.deposit, + }); + // Update stream info in storage. Streams::::insert(stream_id, stream); diff --git a/pallets/stream-payment/src/tests.rs b/pallets/stream-payment/src/tests.rs index 1a07e86c7..b1839b193 100644 --- a/pallets/stream-payment/src/tests.rs +++ b/pallets/stream-payment/src/tests.rs @@ -19,7 +19,7 @@ use { assert_event_emitted, mock::{ roll_to, Balances, ExtBuilder, Runtime, RuntimeOrigin, StreamPayment, - StreamPaymentAssetId, TimeUnit, ALICE, BOB, CHARLIE, DEFAULT_BALANCE, MEGA, + StreamPaymentAssetId, TimeUnit, ALICE, BOB, CHARLIE, DEFAULT_BALANCE, KILO, MEGA, }, Error, Event, FreezeReason, LookupStreamsWithSource, LookupStreamsWithTarget, NextStreamId, Stream, Streams, @@ -379,6 +379,16 @@ mod close_stream { mod refill_stream { use super::*; + #[test] + fn cannot_refill_unknown_stream() { + ExtBuilder::default().build().execute_with(|| { + assert_err!( + StreamPayment::refill_stream(RuntimeOrigin::signed(ALICE), 0, 500), + Error::::UnknownStreamId + ); + }) + } + #[test] fn third_party_cannot_refill() { ExtBuilder::default().build().execute_with(|| { @@ -422,4 +432,341 @@ mod refill_stream { ); }) } + + #[test] + fn source_can_refill_without_payment() { + ExtBuilder::default().build().execute_with(|| { + let rate = 100; + let initial_deposit = 1 * MEGA; + + assert_ok!(StreamPayment::open_stream( + RuntimeOrigin::signed(ALICE), + BOB, + TimeUnit::BlockNumber, + StreamPaymentAssetId::Native, + rate, + initial_deposit + )); + + assert_ok!(StreamPayment::refill_stream( + RuntimeOrigin::signed(ALICE), + 0, + initial_deposit + )); + + assert_event_emitted!(Event::::StreamRefilled { + stream_id: 0, + increase: initial_deposit, + new_deposit: 2 * initial_deposit + }); + + assert_eq!( + Balances::balance_frozen(&FreezeReason::StreamPayment.into(), &ALICE), + 2 * initial_deposit + ); + }) + } + + #[test] + fn source_can_refill_with_payment() { + ExtBuilder::default().build().execute_with(|| { + let rate = 100; + let initial_deposit = 1 * MEGA; + + assert_ok!(StreamPayment::open_stream( + RuntimeOrigin::signed(ALICE), + BOB, + TimeUnit::BlockNumber, + StreamPaymentAssetId::Native, + rate, + initial_deposit + )); + + let delta = roll_to(10) as u128; + let payment = delta * rate; + + assert_ok!(StreamPayment::refill_stream( + RuntimeOrigin::signed(ALICE), + 0, + initial_deposit + )); + + assert_event_emitted!(Event::::StreamPayment { + stream_id: 0, + source: ALICE, + target: BOB, + amount: payment, + drained: false + }); + assert_event_emitted!(Event::::StreamRefilled { + stream_id: 0, + increase: initial_deposit, + new_deposit: 2 * initial_deposit - payment + }); + + assert_eq!( + Balances::balance_frozen(&FreezeReason::StreamPayment.into(), &ALICE), + 2 * initial_deposit - payment + ); + + assert_eq!(Balances::free_balance(ALICE), DEFAULT_BALANCE - payment); + assert_eq!(Balances::free_balance(BOB), DEFAULT_BALANCE + payment); + }) + } + + #[test] + fn source_can_refill_with_payment_not_retroactive() { + ExtBuilder::default().build().execute_with(|| { + let rate = 100 * KILO; + let initial_deposit = 1 * MEGA; + + assert_ok!(StreamPayment::open_stream( + RuntimeOrigin::signed(ALICE), + BOB, + TimeUnit::BlockNumber, + StreamPaymentAssetId::Native, + rate, + initial_deposit + )); + + let delta = roll_to(15) as u128; + let payment = delta * rate; + assert!(payment > initial_deposit); + + let new_deposit = 500 * KILO; + + assert_ok!(StreamPayment::refill_stream( + RuntimeOrigin::signed(ALICE), + 0, + new_deposit + )); + + assert_event_emitted!(Event::::StreamPayment { + stream_id: 0, + source: ALICE, + target: BOB, + amount: initial_deposit, + drained: true + }); + assert_event_emitted!(Event::::StreamRefilled { + stream_id: 0, + increase: new_deposit, + new_deposit: new_deposit + }); + + assert_eq!( + Balances::balance_frozen(&FreezeReason::StreamPayment.into(), &ALICE), + new_deposit + ); + + assert_eq!( + Balances::free_balance(ALICE), + DEFAULT_BALANCE - initial_deposit + ); + assert_eq!( + Balances::free_balance(BOB), + DEFAULT_BALANCE + initial_deposit + ); + }) + } +} + +mod change_stream_rate { + use super::*; + + #[test] + fn cannot_change_rate_of_unknown_stream() { + ExtBuilder::default().build().execute_with(|| { + assert_err!( + StreamPayment::change_stream_rate(RuntimeOrigin::signed(ALICE), 0, 500), + Error::::UnknownStreamId + ); + }) + } + + #[test] + fn third_party_cannot_change_rate() { + ExtBuilder::default().build().execute_with(|| { + let rate = 100; + let initial_deposit = 1 * MEGA; + + assert_ok!(StreamPayment::open_stream( + RuntimeOrigin::signed(ALICE), + BOB, + TimeUnit::BlockNumber, + StreamPaymentAssetId::Native, + rate, + initial_deposit + )); + + assert_err!( + StreamPayment::change_stream_rate(RuntimeOrigin::signed(CHARLIE), 0, rate), + Error::::UnauthorizedOrigin + ); + }) + } + + #[test] + fn source_must_increase_rate() { + ExtBuilder::default().build().execute_with(|| { + let rate = 100; + let initial_deposit = 1 * MEGA; + + assert_ok!(StreamPayment::open_stream( + RuntimeOrigin::signed(ALICE), + BOB, + TimeUnit::BlockNumber, + StreamPaymentAssetId::Native, + rate, + initial_deposit + )); + + assert_err!( + StreamPayment::change_stream_rate(RuntimeOrigin::signed(ALICE), 0, rate - 1), + Error::::SourceCantDecreaseRate + ); + }) + } + + #[test] + fn target_must_decrease_rate() { + ExtBuilder::default().build().execute_with(|| { + let rate = 100; + let initial_deposit = 1 * MEGA; + + assert_ok!(StreamPayment::open_stream( + RuntimeOrigin::signed(ALICE), + BOB, + TimeUnit::BlockNumber, + StreamPaymentAssetId::Native, + rate, + initial_deposit + )); + + assert_err!( + StreamPayment::change_stream_rate(RuntimeOrigin::signed(BOB), 0, rate + 1), + Error::::TargetCantIncreaseRate + ); + }) + } + + #[test] + fn source_can_increase_rate() { + ExtBuilder::default().build().execute_with(|| { + let rate = 100; + let initial_deposit = 1 * MEGA; + + assert_ok!(StreamPayment::open_stream( + RuntimeOrigin::signed(ALICE), + BOB, + TimeUnit::BlockNumber, + StreamPaymentAssetId::Native, + rate, + initial_deposit + )); + + let delta = roll_to(10) as u128; + let payment = delta * rate; + + assert_ok!(StreamPayment::change_stream_rate( + RuntimeOrigin::signed(ALICE), + 0, + rate * 2 + ),); + + assert_event_emitted!(Event::::StreamPayment { + stream_id: 0, + source: ALICE, + target: BOB, + amount: payment, + drained: false + }); + assert_event_emitted!(Event::::StreamRateChanged { + stream_id: 0, + old_rate: rate, + new_rate: rate * 2 + }); + + assert_eq!( + Balances::balance_frozen(&FreezeReason::StreamPayment.into(), &ALICE), + initial_deposit - payment + ); + + assert_eq!(Balances::free_balance(ALICE), DEFAULT_BALANCE - payment); + assert_eq!(Balances::free_balance(BOB), DEFAULT_BALANCE + payment); + + assert_eq!( + Streams::::get(0), + Some(Stream { + source: ALICE, + target: BOB, + time_unit: TimeUnit::BlockNumber, + asset_id: StreamPaymentAssetId::Native, + rate_per_time_unit: rate * 2, + deposit: initial_deposit - payment, + last_time_updated: 10 + }) + ); + }) + } + + #[test] + fn target_can_decrease_rate() { + ExtBuilder::default().build().execute_with(|| { + let rate = 100; + let initial_deposit = 1 * MEGA; + + assert_ok!(StreamPayment::open_stream( + RuntimeOrigin::signed(ALICE), + BOB, + TimeUnit::BlockNumber, + StreamPaymentAssetId::Native, + rate, + initial_deposit + )); + + let delta = roll_to(10) as u128; + let payment = delta * rate; + + assert_ok!(StreamPayment::change_stream_rate( + RuntimeOrigin::signed(BOB), + 0, + rate / 2 + ),); + + assert_event_emitted!(Event::::StreamPayment { + stream_id: 0, + source: ALICE, + target: BOB, + amount: payment, + drained: false + }); + assert_event_emitted!(Event::::StreamRateChanged { + stream_id: 0, + old_rate: rate, + new_rate: rate / 2 + }); + + assert_eq!( + Balances::balance_frozen(&FreezeReason::StreamPayment.into(), &ALICE), + initial_deposit - payment + ); + + assert_eq!(Balances::free_balance(ALICE), DEFAULT_BALANCE - payment); + assert_eq!(Balances::free_balance(BOB), DEFAULT_BALANCE + payment); + + assert_eq!( + Streams::::get(0), + Some(Stream { + source: ALICE, + target: BOB, + time_unit: TimeUnit::BlockNumber, + asset_id: StreamPaymentAssetId::Native, + rate_per_time_unit: rate / 2, + deposit: initial_deposit - payment, + last_time_updated: 10 + }) + ); + }) + } } From 61fb7df196dd4c6253cb9a8bc20456b3d44e8331 Mon Sep 17 00:00:00 2001 From: nanocryk <6422796+nanocryk@users.noreply.github.com> Date: Mon, 22 Jan 2024 16:55:20 +0100 Subject: [PATCH 09/40] refactor stream to prepare improved change requests --- pallets/stream-payment/src/lib.rs | 104 ++++++++++++--- pallets/stream-payment/src/tests.rs | 200 ++++++++++++++++++---------- 2 files changed, 209 insertions(+), 95 deletions(-) diff --git a/pallets/stream-payment/src/lib.rs b/pallets/stream-payment/src/lib.rs index b3db5d942..ed58b48ce 100644 --- a/pallets/stream-payment/src/lib.rs +++ b/pallets/stream-payment/src/lib.rs @@ -120,18 +120,75 @@ pub mod pallet { #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] #[derive(RuntimeDebug, PartialEq, Eq, Encode, Decode, Clone, TypeInfo)] pub struct Stream { + /// Payer, source of the stream. pub source: AccountId, + /// Payee, target of the stream. pub target: AccountId, - pub time_unit: Unit, - pub asset_id: AssetId, - pub rate_per_time_unit: Balance, + /// Steam config (time unit, asset id, rate) + pub config: StreamConfig, + /// How much is deposited to fund this stream. pub deposit: Balance, + /// Last time the stream was updated in `config.time_unit`. pub last_time_updated: Balance, + /// Nonce for requests. This prevents a request to make a first request + /// then change it to another request to frontrun the other party + /// accepting. + pub request_nonce: u32, + /// A pending change request if any. + pub pending_request: Option>, + } + + /// Stream configuration. + #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] + #[derive(RuntimeDebug, PartialEq, Eq, Encode, Decode, Clone, TypeInfo)] + pub struct StreamConfig { + /// Unit in which time is measured using a `TimeProvider`. + pub time_unit: Unit, + /// Asset used for payment. + pub asset_id: AssetId, + /// Amount of asset / unit. + pub rate: Balance, + } + + /// Origin of a change request. + #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] + #[derive(RuntimeDebug, PartialEq, Eq, Encode, Decode, Copy, Clone, TypeInfo)] + pub enum ChangeRequester { + Source, + Target, + } + + /// Kind of change requested. + #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] + #[derive(RuntimeDebug, PartialEq, Eq, Encode, Decode, Copy, Clone, TypeInfo)] + pub enum ChangeKind