-
Notifications
You must be signed in to change notification settings - Fork 63
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Pallet services-payment #77
Changes from 13 commits
f6c9a29
41d23de
5a9e01c
d636c35
3da6f36
2dc5fdb
be55236
a470fec
9f6a6fc
e183963
47ecf05
a6df68c
6962b5a
73f27af
60f5410
a4b0d36
dc751d9
aea18aa
3a91bcf
188e2d2
ec0686a
40ff622
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
[package] | ||
name = "pallet-services-payment" | ||
authors = [] | ||
description = "Services payment pallet" | ||
edition = "2021" | ||
publish = false | ||
version = "0.1.0" | ||
|
||
[package.metadata.docs.rs] | ||
targets = [ "x86_64-unknown-linux-gnu" ] | ||
|
||
[dependencies] | ||
frame-support = { workspace = true } | ||
frame-system = { workspace = true } | ||
log = { workspace = true } | ||
parity-scale-codec = { workspace = true, features = [ "derive", "max-encoded-len" ] } | ||
scale-info = { workspace = true } | ||
serde = { workspace = true, optional = true, features = [ "derive" ] } | ||
|
||
cumulus-primitives-core = { workspace = true } | ||
|
||
[dev-dependencies] | ||
sp-runtime = { workspace = true } | ||
sp-core = { workspace = true } | ||
sp-io = { workspace = true } | ||
pallet-balances = { workspace = true } | ||
|
||
[features] | ||
default = [ "std" ] | ||
std = [ | ||
"cumulus-primitives-core/std", | ||
"frame-support/std", | ||
"frame-system/std", | ||
"pallet-balances/std", | ||
"scale-info/std", | ||
] | ||
try-runtime = [ "frame-support/try-runtime" ] |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,181 @@ | ||||||
//! # Services Payment pallet | ||||||
//! | ||||||
//! This pallet allows for block creation services to be paid for by a | ||||||
//! containerChain. | ||||||
|
||||||
#![cfg_attr(not(feature = "std"), no_std)] | ||||||
|
||||||
use { | ||||||
cumulus_primitives_core::ParaId, | ||||||
frame_support::{pallet_prelude::*, sp_runtime::{Saturating, traits::Zero}, traits::Currency}, | ||||||
frame_system::pallet_prelude::*, | ||||||
}; | ||||||
|
||||||
#[cfg(test)] | ||||||
mod mock; | ||||||
|
||||||
#[cfg(test)] | ||||||
mod test; | ||||||
|
||||||
pub use pallet::*; | ||||||
|
||||||
#[frame_support::pallet] | ||||||
pub mod pallet { | ||||||
use super::*; | ||||||
|
||||||
#[pallet::config] | ||||||
pub trait Config: frame_system::Config { | ||||||
/// The overarching event type. | ||||||
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>; | ||||||
/// Handler for fees | ||||||
type OnChargeForBlockCredit: OnChargeForBlockCredit<Self>; | ||||||
/// Currency type for fee payment | ||||||
type Currency: Currency<Self::AccountId>; | ||||||
/// Provider of a block cost which can adjust from block to block | ||||||
type ProvideBlockProductionCost: ProvideBlockProductionCost<Self>; | ||||||
/// The maximum number of credits that can be accumulated | ||||||
type MaxCreditsStored: Get<Self::BlockNumber>; | ||||||
} | ||||||
|
||||||
#[pallet::error] | ||||||
pub enum Error<T> { | ||||||
TooManyCredits, | ||||||
InsufficientFundsToPurchaseCredits, | ||||||
InsufficientCredits, | ||||||
} | ||||||
|
||||||
#[pallet::pallet] | ||||||
pub struct Pallet<T>(PhantomData<T>); | ||||||
|
||||||
#[pallet::event] | ||||||
#[pallet::generate_deposit(pub(super) fn deposit_event)] | ||||||
pub enum Event<T: Config> { | ||||||
CreditsPurchased { | ||||||
para_id: ParaId, | ||||||
payer: T::AccountId, | ||||||
fee: BalanceOf<T>, | ||||||
credits_purchased: T::BlockNumber, | ||||||
credits_remaining: T::BlockNumber, | ||||||
}, | ||||||
CreditBurned { | ||||||
para_id: ParaId, | ||||||
credits_remaining: T::BlockNumber, | ||||||
}, | ||||||
} | ||||||
|
||||||
#[pallet::storage] | ||||||
#[pallet::getter(fn collator_commission)] | ||||||
pub type BlockProductionCredits<T: Config> = StorageMap< | ||||||
_, | ||||||
Blake2_128Concat, | ||||||
ParaId, | ||||||
T::BlockNumber, | ||||||
OptionQuery, | ||||||
>; | ||||||
|
||||||
#[pallet::call] | ||||||
impl<T: Config> Pallet<T> | ||||||
where | ||||||
BalanceOf<T>: From<BlockNumberFor<T>>, | ||||||
{ | ||||||
#[pallet::call_index(0)] | ||||||
#[pallet::weight(0)] // TODO | ||||||
pub fn purchase_credits( | ||||||
origin: OriginFor<T>, | ||||||
para_id: ParaId, | ||||||
credits: T::BlockNumber | ||||||
) -> DispatchResultWithPostInfo { | ||||||
let account = ensure_signed(origin)?; | ||||||
|
||||||
let existing_credits = BlockProductionCredits::<T>::get(para_id).unwrap_or(T::BlockNumber::zero()); | ||||||
let updated_credits = existing_credits.saturating_add(credits); | ||||||
ensure!( | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can maybe buy as many credit as we can to fill the credit buffer here instead of erroring. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added in 188e2d2. One detail to note: if there are 0 purchases possible (even if 0 were requested) the extrinsic succeeds and a This case is a bit ambiguous to me, but this seemed like the most graceful thing to do. We could bloat the event with one more field for clarity: |
||||||
updated_credits <= T::MaxCreditsStored::get(), | ||||||
Error::<T>::TooManyCredits, | ||||||
); | ||||||
|
||||||
// get the current per-credit cost of a block | ||||||
let (block_cost, _weight) = T::ProvideBlockProductionCost::block_cost(¶_id); | ||||||
let total_fee = block_cost.saturating_mul(credits.into()); | ||||||
|
||||||
T::OnChargeForBlockCredit::charge_credits(&account, ¶_id, credits, total_fee)?; | ||||||
|
||||||
BlockProductionCredits::<T>::insert(para_id, updated_credits); | ||||||
|
||||||
Self::deposit_event(Event::<T>::CreditsPurchased { | ||||||
para_id, | ||||||
payer: account, | ||||||
fee: total_fee, | ||||||
credits_purchased: credits, | ||||||
credits_remaining: updated_credits, | ||||||
}); | ||||||
|
||||||
Ok(().into()) | ||||||
} | ||||||
} | ||||||
|
||||||
impl<T: Config> Pallet<T> { | ||||||
// TODO: make this a regular call? weight? | ||||||
notlesh marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
/// Burn a credit for the given para. Deducts one credit if possible, errors otherwise. | ||||||
pub fn burn_credit_for_para(para_id: &ParaId) -> DispatchResultWithPostInfo { | ||||||
let existing_credits = BlockProductionCredits::<T>::get(para_id).unwrap_or(T::BlockNumber::zero()); | ||||||
|
||||||
ensure!( | ||||||
existing_credits >= 1u32.into(), | ||||||
Error::<T>::InsufficientCredits, | ||||||
); | ||||||
|
||||||
let updated_credits = existing_credits.saturating_sub(1u32.into()); | ||||||
BlockProductionCredits::<T>::insert(para_id, updated_credits); | ||||||
|
||||||
Self::deposit_event(Event::<T>::CreditBurned { | ||||||
para_id: *para_id, | ||||||
credits_remaining: updated_credits, | ||||||
}); | ||||||
|
||||||
Ok(().into()) | ||||||
} | ||||||
} | ||||||
|
||||||
#[pallet::genesis_config] | ||||||
pub struct GenesisConfig<T: Config> { | ||||||
pub initial_credits: Vec<(ParaId, T::BlockNumber)>, | ||||||
} | ||||||
|
||||||
#[cfg(feature = "std")] | ||||||
impl<T: Config> Default for GenesisConfig<T> { | ||||||
fn default() -> Self { | ||||||
Self { | ||||||
initial_credits: Default::default(), | ||||||
} | ||||||
} | ||||||
} | ||||||
|
||||||
#[pallet::genesis_build] | ||||||
impl<T: Config> GenesisBuild<T> for GenesisConfig<T> { | ||||||
fn build(&self) { | ||||||
todo!(); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Won't this panic on genesis? If we don't want to implement it just make it a noop.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks, fixed in a4b0d36 |
||||||
} | ||||||
} | ||||||
} | ||||||
|
||||||
/// Balance used by this pallet | ||||||
pub type BalanceOf<T> = | ||||||
<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance; | ||||||
|
||||||
/// Handler for fee charging. This will be invoked when fees need to be deducted from the fee | ||||||
/// account for a given paraId. | ||||||
pub trait OnChargeForBlockCredit<T: Config> { | ||||||
fn charge_credits( | ||||||
payer: &T::AccountId, | ||||||
para_id: &ParaId, | ||||||
credits: T::BlockNumber, | ||||||
fee: BalanceOf<T>, | ||||||
) -> Result<(), Error<T>>; | ||||||
} | ||||||
|
||||||
/// Returns the cost for a given block credit at the current time. This can be a complex operation, | ||||||
/// so it also returns the weight it consumes. (TODO: or just rely on benchmarking) | ||||||
pub trait ProvideBlockProductionCost<T: Config> { | ||||||
fn block_cost(para_id: &ParaId) -> (BalanceOf<T>, Weight); | ||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
use frame_support::traits::{Currency, WithdrawReasons}; | ||
|
||
use { | ||
crate::{self as payment_services_pallet, OnChargeForBlockCredit, ProvideBlockProductionCost}, | ||
cumulus_primitives_core::ParaId, | ||
frame_support::{ | ||
parameter_types, | ||
pallet_prelude::*, | ||
traits::{ | ||
ConstU32, ConstU64, Everything, | ||
tokens::ExistenceRequirement, | ||
}, | ||
}, | ||
sp_core::H256, | ||
sp_runtime::{ | ||
testing::Header, | ||
traits::{BlakeTwo256, IdentityLookup}, | ||
}, | ||
}; | ||
|
||
type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<Test>; | ||
type Block = frame_system::mocking::MockBlock<Test>; | ||
type AccountId = u64; | ||
type Balance = u128; | ||
|
||
frame_support::construct_runtime!( | ||
pub enum Test where | ||
Block = Block, | ||
NodeBlock = Block, | ||
UncheckedExtrinsic = UncheckedExtrinsic, | ||
{ | ||
System: frame_system::{Pallet, Call, Config, Storage, Event<T>}, | ||
Balances: pallet_balances::{Pallet, Call, Storage, Config<T>, Event<T>}, | ||
PaymentServices: payment_services_pallet::{Pallet, Call, Config<T>, Storage, Event<T>} | ||
} | ||
); | ||
|
||
impl frame_system::Config for Test { | ||
type BaseCallFilter = Everything; | ||
type BlockWeights = (); | ||
type BlockLength = (); | ||
type DbWeight = (); | ||
type RuntimeOrigin = RuntimeOrigin; | ||
type RuntimeCall = RuntimeCall; | ||
type Index = u64; | ||
type BlockNumber = u64; | ||
type Hash = H256; | ||
type Hashing = BlakeTwo256; | ||
type AccountId = AccountId; | ||
type Lookup = IdentityLookup<Self::AccountId>; | ||
type Header = Header; | ||
type RuntimeEvent = RuntimeEvent; | ||
type BlockHashCount = ConstU64<250>; | ||
type Version = (); | ||
type PalletInfo = PalletInfo; | ||
type AccountData = pallet_balances::AccountData<Balance>; | ||
type OnNewAccount = (); | ||
type OnKilledAccount = (); | ||
type SystemWeightInfo = (); | ||
type SS58Prefix = (); | ||
type OnSetCode = (); | ||
type MaxConsumers = ConstU32<16>; | ||
} | ||
|
||
parameter_types! { | ||
pub const ExistentialDeposit: u128 = 1; | ||
} | ||
|
||
impl pallet_balances::Config for Test { | ||
type MaxReserves = (); | ||
type ReserveIdentifier = [u8; 4]; | ||
type MaxLocks = (); | ||
type Balance = Balance; | ||
type RuntimeEvent = RuntimeEvent; | ||
type DustRemoval = (); | ||
type ExistentialDeposit = ExistentialDeposit; | ||
type AccountStore = System; | ||
type WeightInfo = (); | ||
} | ||
|
||
parameter_types! { | ||
pub const MaxCreditsStored: u64 = 5; | ||
} | ||
|
||
impl payment_services_pallet::Config for Test { | ||
type RuntimeEvent = RuntimeEvent; | ||
type OnChargeForBlockCredit = ChargeForBlockCredit<Test>; | ||
type Currency = Balances; | ||
type ProvideBlockProductionCost = BlockProductionCost<Test>; | ||
type MaxCreditsStored = MaxCreditsStored; | ||
} | ||
|
||
pub struct ChargeForBlockCredit<Test>(PhantomData<Test>); | ||
impl OnChargeForBlockCredit<Test> for ChargeForBlockCredit<Test> { | ||
fn charge_credits( | ||
payer: &u64, | ||
_para_id: &ParaId, | ||
_credits: u64, | ||
fee: u128, | ||
) -> Result<(), payment_services_pallet::Error<Test>> { | ||
use frame_support::traits::tokens::imbalance::Imbalance; | ||
|
||
let result = Balances::withdraw( | ||
&*payer, | ||
fee, | ||
WithdrawReasons::FEE, | ||
ExistenceRequirement::AllowDeath | ||
); | ||
let imbalance = result.map_err(|_| payment_services_pallet::Error::InsufficientFundsToPurchaseCredits)?; | ||
|
||
if imbalance.peek() != fee { | ||
panic!("withdrawn balance incorrect"); | ||
} | ||
|
||
Ok(()) | ||
} | ||
} | ||
|
||
pub struct BlockProductionCost<Test>(PhantomData<Test>); | ||
impl ProvideBlockProductionCost<Test> for BlockProductionCost<Test> { | ||
fn block_cost(_para_id: &ParaId) -> (u128, Weight) { | ||
(100, Weight::zero()) | ||
} | ||
} | ||
|
||
#[derive(Default)] | ||
pub struct ExtBuilder { | ||
balances: Vec<(AccountId, Balance)>, | ||
} | ||
|
||
impl ExtBuilder { | ||
pub fn with_balances(mut self, balances: Vec<(AccountId, Balance)>) -> Self { | ||
self.balances = balances; | ||
self | ||
} | ||
|
||
pub fn build(self) -> sp_io::TestExternalities { | ||
let mut t = frame_system::GenesisConfig::default() | ||
.build_storage::<Test>() | ||
.unwrap(); | ||
|
||
pallet_balances::GenesisConfig::<Test> { | ||
balances: self.balances, | ||
} | ||
.assimilate_storage(&mut t) | ||
.unwrap(); | ||
|
||
t.into() | ||
} | ||
} | ||
|
||
pub(crate) fn events() -> Vec<payment_services_pallet::Event<Test>> { | ||
System::events() | ||
.into_iter() | ||
.map(|r| r.event) | ||
.filter_map(|e| { | ||
if let RuntimeEvent::PaymentServices(inner) = e { | ||
Some(inner) | ||
} else { | ||
None | ||
} | ||
}) | ||
.collect::<Vec<_>>() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be desirable to be able to provide a maximum fee the caller is willing to pay per credit here...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I think it might be useful. Can you add that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added in 3a91bcf as an
Option
al limit. LMK what you think.