mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-04-28 22:37:57 +00:00
Pay tx fee with assets by using the asset conversion pallet (#14340)
* Pay tx by swapping the assets * Change liquidity structure * Uncomment the event * Update frame/transaction-payment/asset-tx-payment/src/payment.rs Co-authored-by: Squirrel <gilescope@gmail.com> * New approach * Fix bounds * Clearer version * Change IsType with Into and From * Enable event * Check ED + fix the logic * Add temp comments * Rework the refund * Clean up * Improve readability * Getting closer * fix * Use fungible instead of Currency * Test account without ed * Final push * Fixed * Rename to pallet-asset-conversion-tx-payment * Bring back the old pallet * Update versions * Update docs * Update readme * Wrong readme updated * Revert back doc change * Fix import * Fix kitchensink * Fix * One more time.. * Wait pls * Update frame/asset-conversion/src/lib.rs Co-authored-by: Squirrel <gilescope@gmail.com> * Update frame/support/src/traits/tokens/fungibles/regular.rs Co-authored-by: joe petrowski <25483142+joepetrowski@users.noreply.github.com> * Update docs/comments * Docs improvement * Update frame/transaction-payment/asset-conversion-tx-payment/src/lib.rs Co-authored-by: joe petrowski <25483142+joepetrowski@users.noreply.github.com> * Update frame/transaction-payment/asset-conversion-tx-payment/src/lib.rs Co-authored-by: joe petrowski <25483142+joepetrowski@users.noreply.github.com> * Update frame/transaction-payment/asset-conversion-tx-payment/src/lib.rs Co-authored-by: joe petrowski <25483142+joepetrowski@users.noreply.github.com> * Update frame/transaction-payment/asset-conversion-tx-payment/src/lib.rs Co-authored-by: joe petrowski <25483142+joepetrowski@users.noreply.github.com> * Update frame/transaction-payment/asset-conversion-tx-payment/src/lib.rs Co-authored-by: joe petrowski <25483142+joepetrowski@users.noreply.github.com> * Payed -> paid * Docs * Update frame/transaction-payment/asset-conversion-tx-payment/README.md Co-authored-by: Muharem Ismailov <ismailov.m.h@gmail.com> * Rewrite docs * Try to clean the deps * Add debug assert * Return back frame-benchmarking * Update cargo * Update frame/transaction-payment/asset-conversion-tx-payment/src/mock.rs Co-authored-by: joe petrowski <25483142+joepetrowski@users.noreply.github.com> * Rename * clearer error message * Docs for Pay by Swap (#14445) * docs * better error name * more comments * more docs on swap trait * Fix compile errors * Another fix * Refactoring * Update frame/transaction-payment/asset-conversion-tx-payment/src/payment.rs Co-authored-by: joe petrowski <25483142+joepetrowski@users.noreply.github.com> * Emit an error if we fail to swap the refund back * Add integrity_test * Update frame/asset-conversion/src/lib.rs Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com> * Fmt * Use defensive_ok_or * child PR: Tidy swap event (#14441) * Dedup raising swap event * use expect rather than unwrap * Additional checks for future defence. * cargo fmt * Update frame/asset-conversion/src/lib.rs Co-authored-by: Jegor Sidorenko <5252494+jsidorenko@users.noreply.github.com> --------- Co-authored-by: Jegor Sidorenko <5252494+jsidorenko@users.noreply.github.com> --------- Co-authored-by: Squirrel <gilescope@gmail.com> Co-authored-by: joe petrowski <25483142+joepetrowski@users.noreply.github.com> Co-authored-by: Muharem Ismailov <ismailov.m.h@gmail.com> Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,351 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! # Asset Conversion Transaction Payment Pallet
|
||||
//!
|
||||
//! This pallet allows runtimes that include it to pay for transactions in assets other than the
|
||||
//! chain's native asset.
|
||||
//!
|
||||
//! ## Overview
|
||||
//!
|
||||
//! This pallet provides a `SignedExtension` with an optional `AssetId` that specifies the asset
|
||||
//! to be used for payment (defaulting to the native token on `None`). It expects an
|
||||
//! [`OnChargeAssetTransaction`] implementation analogous to [`pallet-transaction-payment`]. The
|
||||
//! included [`AssetConversionAdapter`] (implementing [`OnChargeAssetTransaction`]) determines the
|
||||
//! fee amount by converting the fee calculated by [`pallet-transaction-payment`] in the native
|
||||
//! asset into the amount required of the specified asset.
|
||||
//!
|
||||
//! ## Pallet API
|
||||
//!
|
||||
//! This pallet does not have any dispatchable calls or storage. It wraps FRAME's Transaction
|
||||
//! Payment pallet and functions as a replacement. This means you should include both pallets in
|
||||
//! your `construct_runtime` macro, but only include this pallet's [`SignedExtension`]
|
||||
//! ([`ChargeAssetTxPayment`]).
|
||||
//!
|
||||
//! ## Terminology
|
||||
//!
|
||||
//! - Native Asset or Native Currency: The asset that a chain considers native, as in its default
|
||||
//! for transaction fee payment, deposits, inflation, etc.
|
||||
//! - Other assets: Other assets that may exist on chain, for example under the Assets pallet.
|
||||
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
use sp_std::prelude::*;
|
||||
|
||||
use codec::{Decode, Encode};
|
||||
use frame_support::{
|
||||
dispatch::{DispatchInfo, DispatchResult, PostDispatchInfo},
|
||||
traits::{
|
||||
tokens::fungibles::{Balanced, Inspect},
|
||||
IsType,
|
||||
},
|
||||
DefaultNoBound,
|
||||
};
|
||||
use pallet_transaction_payment::OnChargeTransaction;
|
||||
use scale_info::TypeInfo;
|
||||
use sp_runtime::{
|
||||
traits::{DispatchInfoOf, Dispatchable, PostDispatchInfoOf, SignedExtension, Zero},
|
||||
transaction_validity::{
|
||||
InvalidTransaction, TransactionValidity, TransactionValidityError, ValidTransaction,
|
||||
},
|
||||
FixedPointOperand,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
mod mock;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
mod payment;
|
||||
pub use payment::*;
|
||||
|
||||
/// Type aliases used for interaction with `OnChargeTransaction`.
|
||||
pub(crate) type OnChargeTransactionOf<T> =
|
||||
<T as pallet_transaction_payment::Config>::OnChargeTransaction;
|
||||
/// Balance type alias for balances of the chain's native asset.
|
||||
pub(crate) type BalanceOf<T> = <OnChargeTransactionOf<T> as OnChargeTransaction<T>>::Balance;
|
||||
/// Liquidity info type alias.
|
||||
pub(crate) type LiquidityInfoOf<T> =
|
||||
<OnChargeTransactionOf<T> as OnChargeTransaction<T>>::LiquidityInfo;
|
||||
|
||||
/// Balance type alias for balances of assets that implement the `fungibles` trait.
|
||||
pub(crate) type AssetBalanceOf<T> =
|
||||
<<T as Config>::Fungibles as Inspect<<T as frame_system::Config>::AccountId>>::Balance;
|
||||
/// Type alias for Asset IDs.
|
||||
pub(crate) type AssetIdOf<T> =
|
||||
<<T as Config>::Fungibles as Inspect<<T as frame_system::Config>::AccountId>>::AssetId;
|
||||
|
||||
/// Type alias for the interaction of balances with `OnChargeAssetTransaction`.
|
||||
pub(crate) type ChargeAssetBalanceOf<T> =
|
||||
<<T as Config>::OnChargeAssetTransaction as OnChargeAssetTransaction<T>>::Balance;
|
||||
/// Type alias for Asset IDs in their interaction with `OnChargeAssetTransaction`.
|
||||
pub(crate) type ChargeAssetIdOf<T> =
|
||||
<<T as Config>::OnChargeAssetTransaction as OnChargeAssetTransaction<T>>::AssetId;
|
||||
/// Liquidity info type alias for interaction with `OnChargeAssetTransaction`.
|
||||
pub(crate) type ChargeAssetLiquidityOf<T> =
|
||||
<<T as Config>::OnChargeAssetTransaction as OnChargeAssetTransaction<T>>::LiquidityInfo;
|
||||
|
||||
/// Used to pass the initial payment info from pre- to post-dispatch.
|
||||
#[derive(Encode, Decode, DefaultNoBound, TypeInfo)]
|
||||
pub enum InitialPayment<T: Config> {
|
||||
/// No initial fee was paid.
|
||||
#[default]
|
||||
Nothing,
|
||||
/// The initial fee was paid in the native currency.
|
||||
Native(LiquidityInfoOf<T>),
|
||||
/// The initial fee was paid in an asset.
|
||||
Asset((LiquidityInfoOf<T>, BalanceOf<T>, AssetBalanceOf<T>)),
|
||||
}
|
||||
|
||||
pub use pallet::*;
|
||||
|
||||
#[frame_support::pallet]
|
||||
pub mod pallet {
|
||||
use super::*;
|
||||
|
||||
#[pallet::config]
|
||||
pub trait Config: frame_system::Config + pallet_transaction_payment::Config {
|
||||
/// The overarching event type.
|
||||
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
|
||||
/// The fungibles instance used to pay for transactions in assets.
|
||||
type Fungibles: Balanced<Self::AccountId>;
|
||||
/// The actual transaction charging logic that charges the fees.
|
||||
type OnChargeAssetTransaction: OnChargeAssetTransaction<Self>;
|
||||
}
|
||||
|
||||
#[pallet::pallet]
|
||||
pub struct Pallet<T>(_);
|
||||
|
||||
#[pallet::event]
|
||||
#[pallet::generate_deposit(pub(super) fn deposit_event)]
|
||||
pub enum Event<T: Config> {
|
||||
/// A transaction fee `actual_fee`, of which `tip` was added to the minimum inclusion fee,
|
||||
/// has been paid by `who` in an asset `asset_id`.
|
||||
AssetTxFeePaid {
|
||||
who: T::AccountId,
|
||||
actual_fee: AssetBalanceOf<T>,
|
||||
tip: BalanceOf<T>,
|
||||
asset_id: ChargeAssetIdOf<T>,
|
||||
},
|
||||
/// A swap of the refund in native currency back to asset failed.
|
||||
AssetRefundFailed { native_amount_kept: BalanceOf<T> },
|
||||
}
|
||||
}
|
||||
|
||||
/// Require payment for transaction inclusion and optionally include a tip to gain additional
|
||||
/// priority in the queue. Allows paying via both `Currency` as well as `fungibles::Balanced`.
|
||||
///
|
||||
/// Wraps the transaction logic in [`pallet_transaction_payment`] and extends it with assets.
|
||||
/// An asset ID of `None` falls back to the underlying transaction payment logic via the native
|
||||
/// currency.
|
||||
#[derive(Encode, Decode, Clone, Eq, PartialEq, TypeInfo)]
|
||||
#[scale_info(skip_type_params(T))]
|
||||
pub struct ChargeAssetTxPayment<T: Config> {
|
||||
#[codec(compact)]
|
||||
tip: BalanceOf<T>,
|
||||
asset_id: Option<ChargeAssetIdOf<T>>,
|
||||
}
|
||||
|
||||
impl<T: Config> ChargeAssetTxPayment<T>
|
||||
where
|
||||
T::RuntimeCall: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>,
|
||||
AssetBalanceOf<T>: Send + Sync + FixedPointOperand,
|
||||
BalanceOf<T>: Send
|
||||
+ Sync
|
||||
+ FixedPointOperand
|
||||
+ Into<ChargeAssetBalanceOf<T>>
|
||||
+ From<ChargeAssetLiquidityOf<T>>,
|
||||
ChargeAssetIdOf<T>: Send + Sync,
|
||||
{
|
||||
/// Utility constructor. Used only in client/factory code.
|
||||
pub fn from(tip: BalanceOf<T>, asset_id: Option<ChargeAssetIdOf<T>>) -> Self {
|
||||
Self { tip, asset_id }
|
||||
}
|
||||
|
||||
/// Fee withdrawal logic that dispatches to either `OnChargeAssetTransaction` or
|
||||
/// `OnChargeTransaction`.
|
||||
fn withdraw_fee(
|
||||
&self,
|
||||
who: &T::AccountId,
|
||||
call: &T::RuntimeCall,
|
||||
info: &DispatchInfoOf<T::RuntimeCall>,
|
||||
len: usize,
|
||||
) -> Result<(BalanceOf<T>, InitialPayment<T>), TransactionValidityError> {
|
||||
let fee = pallet_transaction_payment::Pallet::<T>::compute_fee(len as u32, info, self.tip);
|
||||
debug_assert!(self.tip <= fee, "tip should be included in the computed fee");
|
||||
if fee.is_zero() {
|
||||
Ok((fee, InitialPayment::Nothing))
|
||||
} else if let Some(asset_id) = self.asset_id {
|
||||
T::OnChargeAssetTransaction::withdraw_fee(
|
||||
who,
|
||||
call,
|
||||
info,
|
||||
asset_id,
|
||||
fee.into(),
|
||||
self.tip.into(),
|
||||
)
|
||||
.map(|(used_for_fee, received_exchanged, asset_consumed)| {
|
||||
(
|
||||
fee,
|
||||
InitialPayment::Asset((
|
||||
used_for_fee.into(),
|
||||
received_exchanged.into(),
|
||||
asset_consumed.into(),
|
||||
)),
|
||||
)
|
||||
})
|
||||
} else {
|
||||
<OnChargeTransactionOf<T> as OnChargeTransaction<T>>::withdraw_fee(
|
||||
who, call, info, fee, self.tip,
|
||||
)
|
||||
.map(|i| (fee, InitialPayment::Native(i)))
|
||||
.map_err(|_| -> TransactionValidityError { InvalidTransaction::Payment.into() })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> sp_std::fmt::Debug for ChargeAssetTxPayment<T> {
|
||||
#[cfg(feature = "std")]
|
||||
fn fmt(&self, f: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result {
|
||||
write!(f, "ChargeAssetTxPayment<{:?}, {:?}>", self.tip, self.asset_id.encode())
|
||||
}
|
||||
#[cfg(not(feature = "std"))]
|
||||
fn fmt(&self, _: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> SignedExtension for ChargeAssetTxPayment<T>
|
||||
where
|
||||
T::RuntimeCall: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>,
|
||||
AssetBalanceOf<T>: Send + Sync + FixedPointOperand,
|
||||
BalanceOf<T>: Send
|
||||
+ Sync
|
||||
+ From<u64>
|
||||
+ FixedPointOperand
|
||||
+ Into<ChargeAssetBalanceOf<T>>
|
||||
+ Into<ChargeAssetLiquidityOf<T>>
|
||||
+ From<ChargeAssetLiquidityOf<T>>,
|
||||
ChargeAssetIdOf<T>: Send + Sync,
|
||||
{
|
||||
const IDENTIFIER: &'static str = "ChargeAssetTxPayment";
|
||||
type AccountId = T::AccountId;
|
||||
type Call = T::RuntimeCall;
|
||||
type AdditionalSigned = ();
|
||||
type Pre = (
|
||||
// tip
|
||||
BalanceOf<T>,
|
||||
// who paid the fee
|
||||
Self::AccountId,
|
||||
// imbalance resulting from withdrawing the fee
|
||||
InitialPayment<T>,
|
||||
// asset_id for the transaction payment
|
||||
Option<ChargeAssetIdOf<T>>,
|
||||
);
|
||||
|
||||
fn additional_signed(&self) -> sp_std::result::Result<(), TransactionValidityError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate(
|
||||
&self,
|
||||
who: &Self::AccountId,
|
||||
call: &Self::Call,
|
||||
info: &DispatchInfoOf<Self::Call>,
|
||||
len: usize,
|
||||
) -> TransactionValidity {
|
||||
use pallet_transaction_payment::ChargeTransactionPayment;
|
||||
let (fee, _) = self.withdraw_fee(who, call, info, len)?;
|
||||
let priority = ChargeTransactionPayment::<T>::get_priority(info, len, self.tip, fee);
|
||||
Ok(ValidTransaction { priority, ..Default::default() })
|
||||
}
|
||||
|
||||
fn pre_dispatch(
|
||||
self,
|
||||
who: &Self::AccountId,
|
||||
call: &Self::Call,
|
||||
info: &DispatchInfoOf<Self::Call>,
|
||||
len: usize,
|
||||
) -> Result<Self::Pre, TransactionValidityError> {
|
||||
let (_fee, initial_payment) = self.withdraw_fee(who, call, info, len)?;
|
||||
Ok((self.tip, who.clone(), initial_payment, self.asset_id))
|
||||
}
|
||||
|
||||
fn post_dispatch(
|
||||
pre: Option<Self::Pre>,
|
||||
info: &DispatchInfoOf<Self::Call>,
|
||||
post_info: &PostDispatchInfoOf<Self::Call>,
|
||||
len: usize,
|
||||
result: &DispatchResult,
|
||||
) -> Result<(), TransactionValidityError> {
|
||||
if let Some((tip, who, initial_payment, asset_id)) = pre {
|
||||
match initial_payment {
|
||||
InitialPayment::Native(already_withdrawn) => {
|
||||
debug_assert!(
|
||||
asset_id.is_none(),
|
||||
"For that payment type the `asset_id` should be None"
|
||||
);
|
||||
pallet_transaction_payment::ChargeTransactionPayment::<T>::post_dispatch(
|
||||
Some((tip, who, already_withdrawn)),
|
||||
info,
|
||||
post_info,
|
||||
len,
|
||||
result,
|
||||
)?;
|
||||
},
|
||||
InitialPayment::Asset(already_withdrawn) => {
|
||||
debug_assert!(
|
||||
asset_id.is_some(),
|
||||
"For that payment type the `asset_id` should be set"
|
||||
);
|
||||
let actual_fee = pallet_transaction_payment::Pallet::<T>::compute_actual_fee(
|
||||
len as u32, info, post_info, tip,
|
||||
);
|
||||
|
||||
if let Some(asset_id) = asset_id {
|
||||
let (used_for_fee, received_exchanged, asset_consumed) = already_withdrawn;
|
||||
let converted_fee = T::OnChargeAssetTransaction::correct_and_deposit_fee(
|
||||
&who,
|
||||
info,
|
||||
post_info,
|
||||
actual_fee.into(),
|
||||
tip.into(),
|
||||
used_for_fee.into(),
|
||||
received_exchanged.into(),
|
||||
asset_id,
|
||||
asset_consumed.into(),
|
||||
)?;
|
||||
|
||||
Pallet::<T>::deposit_event(Event::<T>::AssetTxFeePaid {
|
||||
who,
|
||||
actual_fee: converted_fee,
|
||||
tip,
|
||||
asset_id,
|
||||
});
|
||||
}
|
||||
},
|
||||
InitialPayment::Nothing => {
|
||||
// `actual_fee` should be zero here for any signed extrinsic. It would be
|
||||
// non-zero here in case of unsigned extrinsics as they don't pay fees but
|
||||
// `compute_actual_fee` is not aware of them. In both cases it's fine to just
|
||||
// move ahead without adjusting the fee, though, so we do nothing.
|
||||
debug_assert!(tip.is_zero(), "tip should be zero if initial fee was zero.");
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use super::*;
|
||||
use crate as pallet_asset_conversion_tx_payment;
|
||||
|
||||
use codec;
|
||||
use frame_support::{
|
||||
dispatch::DispatchClass,
|
||||
instances::Instance2,
|
||||
ord_parameter_types,
|
||||
pallet_prelude::*,
|
||||
parameter_types,
|
||||
traits::{AsEnsureOriginWithArg, ConstU32, ConstU64, ConstU8, Imbalance, OnUnbalanced},
|
||||
weights::{Weight, WeightToFee as WeightToFeeT},
|
||||
PalletId,
|
||||
};
|
||||
use frame_system as system;
|
||||
use frame_system::{EnsureRoot, EnsureSignedBy};
|
||||
use pallet_asset_conversion::{NativeOrAssetId, NativeOrAssetIdConverter};
|
||||
use pallet_transaction_payment::CurrencyAdapter;
|
||||
use sp_core::H256;
|
||||
use sp_runtime::{
|
||||
testing::Header,
|
||||
traits::{AccountIdConversion, BlakeTwo256, IdentityLookup, SaturatedConversion},
|
||||
Permill,
|
||||
};
|
||||
|
||||
type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<Runtime>;
|
||||
type Block = frame_system::mocking::MockBlock<Runtime>;
|
||||
type Balance = u64;
|
||||
type AccountId = u64;
|
||||
|
||||
frame_support::construct_runtime!(
|
||||
pub enum Runtime where
|
||||
Block = Block,
|
||||
NodeBlock = Block,
|
||||
UncheckedExtrinsic = UncheckedExtrinsic,
|
||||
{
|
||||
System: system,
|
||||
Balances: pallet_balances,
|
||||
TransactionPayment: pallet_transaction_payment,
|
||||
Assets: pallet_assets,
|
||||
PoolAssets: pallet_assets::<Instance2>,
|
||||
AssetConversion: pallet_asset_conversion,
|
||||
AssetTxPayment: pallet_asset_conversion_tx_payment,
|
||||
}
|
||||
);
|
||||
|
||||
parameter_types! {
|
||||
pub(crate) static ExtrinsicBaseWeight: Weight = Weight::zero();
|
||||
}
|
||||
|
||||
pub struct BlockWeights;
|
||||
impl Get<frame_system::limits::BlockWeights> for BlockWeights {
|
||||
fn get() -> frame_system::limits::BlockWeights {
|
||||
frame_system::limits::BlockWeights::builder()
|
||||
.base_block(Weight::zero())
|
||||
.for_class(DispatchClass::all(), |weights| {
|
||||
weights.base_extrinsic = ExtrinsicBaseWeight::get().into();
|
||||
})
|
||||
.for_class(DispatchClass::non_mandatory(), |weights| {
|
||||
weights.max_total = Weight::from_parts(1024, u64::MAX).into();
|
||||
})
|
||||
.build_or_panic()
|
||||
}
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub static WeightToFee: u64 = 1;
|
||||
pub static TransactionByteFee: u64 = 1;
|
||||
}
|
||||
|
||||
impl frame_system::Config for Runtime {
|
||||
type BaseCallFilter = frame_support::traits::Everything;
|
||||
type BlockWeights = BlockWeights;
|
||||
type BlockLength = ();
|
||||
type DbWeight = ();
|
||||
type RuntimeOrigin = RuntimeOrigin;
|
||||
type Index = u64;
|
||||
type BlockNumber = u64;
|
||||
type RuntimeCall = RuntimeCall;
|
||||
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<u64>;
|
||||
type OnNewAccount = ();
|
||||
type OnKilledAccount = ();
|
||||
type SystemWeightInfo = ();
|
||||
type SS58Prefix = ();
|
||||
type OnSetCode = ();
|
||||
type MaxConsumers = ConstU32<16>;
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const ExistentialDeposit: u64 = 10;
|
||||
}
|
||||
|
||||
impl pallet_balances::Config for Runtime {
|
||||
type Balance = Balance;
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type DustRemoval = ();
|
||||
type ExistentialDeposit = ConstU64<10>;
|
||||
type AccountStore = System;
|
||||
type MaxLocks = ();
|
||||
type WeightInfo = ();
|
||||
type MaxReserves = ConstU32<50>;
|
||||
type ReserveIdentifier = [u8; 8];
|
||||
type FreezeIdentifier = ();
|
||||
type MaxFreezes = ();
|
||||
type RuntimeHoldReason = ();
|
||||
type MaxHolds = ();
|
||||
}
|
||||
|
||||
impl WeightToFeeT for WeightToFee {
|
||||
type Balance = u64;
|
||||
|
||||
fn weight_to_fee(weight: &Weight) -> Self::Balance {
|
||||
Self::Balance::saturated_from(weight.ref_time())
|
||||
.saturating_mul(WEIGHT_TO_FEE.with(|v| *v.borrow()))
|
||||
}
|
||||
}
|
||||
|
||||
impl WeightToFeeT for TransactionByteFee {
|
||||
type Balance = u64;
|
||||
|
||||
fn weight_to_fee(weight: &Weight) -> Self::Balance {
|
||||
Self::Balance::saturated_from(weight.ref_time())
|
||||
.saturating_mul(TRANSACTION_BYTE_FEE.with(|v| *v.borrow()))
|
||||
}
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub(crate) static TipUnbalancedAmount: u64 = 0;
|
||||
pub(crate) static FeeUnbalancedAmount: u64 = 0;
|
||||
}
|
||||
|
||||
pub struct DealWithFees;
|
||||
impl OnUnbalanced<pallet_balances::NegativeImbalance<Runtime>> for DealWithFees {
|
||||
fn on_unbalanceds<B>(
|
||||
mut fees_then_tips: impl Iterator<Item = pallet_balances::NegativeImbalance<Runtime>>,
|
||||
) {
|
||||
if let Some(fees) = fees_then_tips.next() {
|
||||
FeeUnbalancedAmount::mutate(|a| *a += fees.peek());
|
||||
if let Some(tips) = fees_then_tips.next() {
|
||||
TipUnbalancedAmount::mutate(|a| *a += tips.peek());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl pallet_transaction_payment::Config for Runtime {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type OnChargeTransaction = CurrencyAdapter<Balances, DealWithFees>;
|
||||
type WeightToFee = WeightToFee;
|
||||
type LengthToFee = TransactionByteFee;
|
||||
type FeeMultiplierUpdate = ();
|
||||
type OperationalFeeMultiplier = ConstU8<5>;
|
||||
}
|
||||
|
||||
type AssetId = u32;
|
||||
|
||||
impl pallet_assets::Config for Runtime {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type Balance = Balance;
|
||||
type AssetId = AssetId;
|
||||
type AssetIdParameter = codec::Compact<AssetId>;
|
||||
type Currency = Balances;
|
||||
type CreateOrigin = AsEnsureOriginWithArg<frame_system::EnsureSigned<AccountId>>;
|
||||
type ForceOrigin = EnsureRoot<AccountId>;
|
||||
type AssetDeposit = ConstU64<2>;
|
||||
type AssetAccountDeposit = ConstU64<2>;
|
||||
type MetadataDepositBase = ConstU64<0>;
|
||||
type MetadataDepositPerByte = ConstU64<0>;
|
||||
type ApprovalDeposit = ConstU64<0>;
|
||||
type StringLimit = ConstU32<20>;
|
||||
type Freezer = ();
|
||||
type Extra = ();
|
||||
type CallbackHandle = ();
|
||||
type WeightInfo = ();
|
||||
type RemoveItemsLimit = ConstU32<1000>;
|
||||
pallet_assets::runtime_benchmarks_enabled! {
|
||||
type BenchmarkHelper = ();
|
||||
}
|
||||
}
|
||||
|
||||
impl pallet_assets::Config<Instance2> for Runtime {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type Balance = u64;
|
||||
type RemoveItemsLimit = ConstU32<1000>;
|
||||
type AssetId = u32;
|
||||
type AssetIdParameter = u32;
|
||||
type Currency = Balances;
|
||||
type CreateOrigin = AsEnsureOriginWithArg<EnsureSignedBy<AssetConversionOrigin, u64>>;
|
||||
type ForceOrigin = frame_system::EnsureRoot<u64>;
|
||||
type AssetDeposit = ConstU64<0>;
|
||||
type AssetAccountDeposit = ConstU64<0>;
|
||||
type MetadataDepositBase = ConstU64<0>;
|
||||
type MetadataDepositPerByte = ConstU64<0>;
|
||||
type ApprovalDeposit = ConstU64<0>;
|
||||
type StringLimit = ConstU32<50>;
|
||||
type Freezer = ();
|
||||
type Extra = ();
|
||||
type WeightInfo = ();
|
||||
type CallbackHandle = ();
|
||||
pallet_assets::runtime_benchmarks_enabled! {
|
||||
type BenchmarkHelper = ();
|
||||
}
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const AssetConversionPalletId: PalletId = PalletId(*b"py/ascon");
|
||||
pub storage AllowMultiAssetPools: bool = false;
|
||||
// should be non-zero if AllowMultiAssetPools is true, otherwise can be zero
|
||||
pub storage LiquidityWithdrawalFee: Permill = Permill::from_percent(0);
|
||||
pub const MaxSwapPathLength: u32 = 4;
|
||||
}
|
||||
|
||||
ord_parameter_types! {
|
||||
pub const AssetConversionOrigin: u64 = AccountIdConversion::<u64>::into_account_truncating(&AssetConversionPalletId::get());
|
||||
}
|
||||
|
||||
impl pallet_asset_conversion::Config for Runtime {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type Currency = Balances;
|
||||
type AssetBalance = <Self as pallet_balances::Config>::Balance;
|
||||
type AssetId = u32;
|
||||
type PoolAssetId = u32;
|
||||
type Assets = Assets;
|
||||
type PoolAssets = PoolAssets;
|
||||
type PalletId = AssetConversionPalletId;
|
||||
type WeightInfo = ();
|
||||
type LPFee = ConstU32<3>; // means 0.3%
|
||||
type PoolSetupFee = ConstU64<100>; // should be more or equal to the existential deposit
|
||||
type PoolSetupFeeReceiver = AssetConversionOrigin;
|
||||
type LiquidityWithdrawalFee = LiquidityWithdrawalFee;
|
||||
type AllowMultiAssetPools = AllowMultiAssetPools;
|
||||
type MaxSwapPathLength = MaxSwapPathLength;
|
||||
type MintMinLiquidity = ConstU64<100>; // 100 is good enough when the main currency has 12 decimals.
|
||||
|
||||
type Balance = u64;
|
||||
type HigherPrecisionBalance = u128;
|
||||
|
||||
type MultiAssetId = NativeOrAssetId<u32>;
|
||||
type MultiAssetIdConverter = NativeOrAssetIdConverter<u32>;
|
||||
|
||||
pallet_asset_conversion::runtime_benchmarks_enabled! {
|
||||
type BenchmarkHelper = ();
|
||||
}
|
||||
}
|
||||
|
||||
impl Config for Runtime {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type Fungibles = Assets;
|
||||
type OnChargeAssetTransaction = AssetConversionAdapter<Balances, AssetConversion>;
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
///! Traits and default implementation for paying transaction fees in assets.
|
||||
use super::*;
|
||||
use crate::Config;
|
||||
|
||||
use codec::FullCodec;
|
||||
use frame_support::{
|
||||
ensure,
|
||||
traits::{fungible::Inspect, fungibles::SwapNative, tokens::Balance},
|
||||
unsigned::TransactionValidityError,
|
||||
};
|
||||
use scale_info::TypeInfo;
|
||||
use sp_runtime::{
|
||||
traits::{DispatchInfoOf, MaybeSerializeDeserialize, PostDispatchInfoOf, Zero},
|
||||
transaction_validity::InvalidTransaction,
|
||||
Saturating,
|
||||
};
|
||||
use sp_std::{fmt::Debug, marker::PhantomData};
|
||||
|
||||
/// Handle withdrawing, refunding and depositing of transaction fees.
|
||||
pub trait OnChargeAssetTransaction<T: Config> {
|
||||
/// The underlying integer type in which fees are calculated.
|
||||
type Balance: Balance;
|
||||
/// The type used to identify the assets used for transaction payment.
|
||||
type AssetId: FullCodec + Copy + MaybeSerializeDeserialize + Debug + Default + Eq + TypeInfo;
|
||||
/// The type used to store the intermediate values between pre- and post-dispatch.
|
||||
type LiquidityInfo;
|
||||
|
||||
/// Secure the payment of the transaction fees before the transaction is executed.
|
||||
///
|
||||
/// Note: The `fee` already includes the `tip`.
|
||||
fn withdraw_fee(
|
||||
who: &T::AccountId,
|
||||
call: &T::RuntimeCall,
|
||||
dispatch_info: &DispatchInfoOf<T::RuntimeCall>,
|
||||
asset_id: Self::AssetId,
|
||||
fee: Self::Balance,
|
||||
tip: Self::Balance,
|
||||
) -> Result<
|
||||
(LiquidityInfoOf<T>, Self::LiquidityInfo, AssetBalanceOf<T>),
|
||||
TransactionValidityError,
|
||||
>;
|
||||
|
||||
/// Refund any overpaid fees and deposit the corrected amount.
|
||||
/// The actual fee gets calculated once the transaction is executed.
|
||||
///
|
||||
/// Note: The `fee` already includes the `tip`.
|
||||
///
|
||||
/// Returns the fee and tip in the asset used for payment as (fee, tip).
|
||||
fn correct_and_deposit_fee(
|
||||
who: &T::AccountId,
|
||||
dispatch_info: &DispatchInfoOf<T::RuntimeCall>,
|
||||
post_info: &PostDispatchInfoOf<T::RuntimeCall>,
|
||||
corrected_fee: Self::Balance,
|
||||
tip: Self::Balance,
|
||||
fee_paid: LiquidityInfoOf<T>,
|
||||
received_exchanged: Self::LiquidityInfo,
|
||||
asset_id: Self::AssetId,
|
||||
initial_asset_consumed: AssetBalanceOf<T>,
|
||||
) -> Result<AssetBalanceOf<T>, TransactionValidityError>;
|
||||
}
|
||||
|
||||
/// Implements the asset transaction for a balance to asset converter (implementing
|
||||
/// [`SwapNative`]).
|
||||
///
|
||||
/// The converter is given the complete fee in terms of the asset used for the transaction.
|
||||
pub struct AssetConversionAdapter<C, CON>(PhantomData<(C, CON)>);
|
||||
|
||||
/// Default implementation for a runtime instantiating this pallet, an asset to native swapper.
|
||||
impl<T, C, CON> OnChargeAssetTransaction<T> for AssetConversionAdapter<C, CON>
|
||||
where
|
||||
T: Config,
|
||||
C: Inspect<<T as frame_system::Config>::AccountId>,
|
||||
CON: SwapNative<T::RuntimeOrigin, T::AccountId, BalanceOf<T>, AssetBalanceOf<T>, AssetIdOf<T>>,
|
||||
AssetIdOf<T>: FullCodec + Copy + MaybeSerializeDeserialize + Debug + Default + Eq + TypeInfo,
|
||||
BalanceOf<T>: IsType<<C as Inspect<<T as frame_system::Config>::AccountId>>::Balance>,
|
||||
{
|
||||
type Balance = BalanceOf<T>;
|
||||
type AssetId = AssetIdOf<T>;
|
||||
type LiquidityInfo = BalanceOf<T>;
|
||||
|
||||
/// Swap & withdraw the predicted fee from the transaction origin.
|
||||
///
|
||||
/// Note: The `fee` already includes the `tip`.
|
||||
///
|
||||
/// Returns the total amount in native currency received by exchanging the `asset_id` and the
|
||||
/// amount in native currency used to pay the fee.
|
||||
fn withdraw_fee(
|
||||
who: &T::AccountId,
|
||||
call: &T::RuntimeCall,
|
||||
info: &DispatchInfoOf<T::RuntimeCall>,
|
||||
asset_id: Self::AssetId,
|
||||
fee: BalanceOf<T>,
|
||||
tip: BalanceOf<T>,
|
||||
) -> Result<
|
||||
(LiquidityInfoOf<T>, Self::LiquidityInfo, AssetBalanceOf<T>),
|
||||
TransactionValidityError,
|
||||
> {
|
||||
// convert the asset into native currency
|
||||
let ed = C::minimum_balance();
|
||||
let native_asset_required =
|
||||
if C::balance(&who) >= ed.saturating_add(fee.into()) { fee } else { fee + ed.into() };
|
||||
|
||||
let asset_consumed = CON::swap_tokens_for_exact_native(
|
||||
who.clone(),
|
||||
asset_id,
|
||||
native_asset_required,
|
||||
None,
|
||||
who.clone(),
|
||||
true,
|
||||
)
|
||||
.map_err(|_| TransactionValidityError::from(InvalidTransaction::Payment))?;
|
||||
|
||||
ensure!(asset_consumed > Zero::zero(), InvalidTransaction::Payment);
|
||||
|
||||
// charge the fee in native currency
|
||||
<T::OnChargeTransaction>::withdraw_fee(who, call, info, fee, tip)
|
||||
.map(|r| (r, native_asset_required, asset_consumed))
|
||||
}
|
||||
|
||||
/// Correct the fee and swap the refund back to asset.
|
||||
///
|
||||
/// Note: The `corrected_fee` already includes the `tip`.
|
||||
/// Note: Is the ED wasn't needed, the `received_exchanged` will be equal to `fee_paid`, or
|
||||
/// `fee_paid + ed` otherwise.
|
||||
fn correct_and_deposit_fee(
|
||||
who: &T::AccountId,
|
||||
dispatch_info: &DispatchInfoOf<T::RuntimeCall>,
|
||||
post_info: &PostDispatchInfoOf<T::RuntimeCall>,
|
||||
corrected_fee: BalanceOf<T>,
|
||||
tip: BalanceOf<T>,
|
||||
fee_paid: LiquidityInfoOf<T>,
|
||||
received_exchanged: Self::LiquidityInfo,
|
||||
asset_id: Self::AssetId,
|
||||
initial_asset_consumed: AssetBalanceOf<T>,
|
||||
) -> Result<AssetBalanceOf<T>, TransactionValidityError> {
|
||||
// Refund the native asset to the account that paid the fees (`who`).
|
||||
// The `who` account will receive the "fee_paid - corrected_fee" refund.
|
||||
<T::OnChargeTransaction>::correct_and_deposit_fee(
|
||||
who,
|
||||
dispatch_info,
|
||||
post_info,
|
||||
corrected_fee,
|
||||
tip,
|
||||
fee_paid,
|
||||
)?;
|
||||
|
||||
// calculate the refund in native asset, to swap back to the desired `asset_id`
|
||||
let swap_back = received_exchanged.saturating_sub(corrected_fee);
|
||||
let mut asset_refund = Zero::zero();
|
||||
if !swap_back.is_zero() {
|
||||
// If this fails, the account might have dropped below the existential balance or there
|
||||
// is not enough liquidity left in the pool. In that case we don't throw an error and
|
||||
// the account will keep the native currency.
|
||||
match CON::swap_exact_native_for_tokens(
|
||||
who.clone(), // we already deposited the native to `who`
|
||||
asset_id, // we want asset_id back
|
||||
swap_back, // amount of the native asset to convert to `asset_id`
|
||||
None, // no minimum amount back
|
||||
who.clone(), // we will refund to `who`
|
||||
false, // no need to keep alive
|
||||
)
|
||||
.ok()
|
||||
{
|
||||
Some(acquired) => asset_refund = acquired,
|
||||
None => {
|
||||
Pallet::<T>::deposit_event(Event::<T>::AssetRefundFailed {
|
||||
native_amount_kept: swap_back,
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let actual_paid = initial_asset_consumed.saturating_sub(asset_refund);
|
||||
Ok(actual_paid)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,708 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use super::*;
|
||||
|
||||
use frame_support::{
|
||||
assert_ok,
|
||||
dispatch::{DispatchInfo, PostDispatchInfo},
|
||||
pallet_prelude::*,
|
||||
traits::{fungible::Inspect, fungibles::Mutate},
|
||||
weights::Weight,
|
||||
};
|
||||
use frame_system as system;
|
||||
use mock::{ExtrinsicBaseWeight, *};
|
||||
use pallet_asset_conversion::NativeOrAssetId;
|
||||
use pallet_balances::Call as BalancesCall;
|
||||
use sp_runtime::traits::StaticLookup;
|
||||
|
||||
const CALL: &<Runtime as frame_system::Config>::RuntimeCall =
|
||||
&RuntimeCall::Balances(BalancesCall::transfer_allow_death { dest: 2, value: 69 });
|
||||
|
||||
pub struct ExtBuilder {
|
||||
balance_factor: u64,
|
||||
base_weight: Weight,
|
||||
byte_fee: u64,
|
||||
weight_to_fee: u64,
|
||||
}
|
||||
|
||||
impl Default for ExtBuilder {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
balance_factor: 1,
|
||||
base_weight: Weight::from_parts(0, 0),
|
||||
byte_fee: 1,
|
||||
weight_to_fee: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtBuilder {
|
||||
pub fn base_weight(mut self, base_weight: Weight) -> Self {
|
||||
self.base_weight = base_weight;
|
||||
self
|
||||
}
|
||||
pub fn balance_factor(mut self, factor: u64) -> Self {
|
||||
self.balance_factor = factor;
|
||||
self
|
||||
}
|
||||
fn set_constants(&self) {
|
||||
ExtrinsicBaseWeight::mutate(|v| *v = self.base_weight);
|
||||
TRANSACTION_BYTE_FEE.with(|v| *v.borrow_mut() = self.byte_fee);
|
||||
WEIGHT_TO_FEE.with(|v| *v.borrow_mut() = self.weight_to_fee);
|
||||
}
|
||||
pub fn build(self) -> sp_io::TestExternalities {
|
||||
self.set_constants();
|
||||
let mut t = frame_system::GenesisConfig::default().build_storage::<Runtime>().unwrap();
|
||||
pallet_balances::GenesisConfig::<Runtime> {
|
||||
balances: if self.balance_factor > 0 {
|
||||
vec![
|
||||
(1, 10 * self.balance_factor),
|
||||
(2, 20 * self.balance_factor),
|
||||
(3, 30 * self.balance_factor),
|
||||
(4, 40 * self.balance_factor),
|
||||
(5, 50 * self.balance_factor),
|
||||
(6, 60 * self.balance_factor),
|
||||
]
|
||||
} else {
|
||||
vec![]
|
||||
},
|
||||
}
|
||||
.assimilate_storage(&mut t)
|
||||
.unwrap();
|
||||
t.into()
|
||||
}
|
||||
}
|
||||
|
||||
/// create a transaction info struct from weight. Handy to avoid building the whole struct.
|
||||
pub fn info_from_weight(w: Weight) -> DispatchInfo {
|
||||
// pays_fee: Pays::Yes -- class: DispatchClass::Normal
|
||||
DispatchInfo { weight: w, ..Default::default() }
|
||||
}
|
||||
|
||||
fn post_info_from_weight(w: Weight) -> PostDispatchInfo {
|
||||
PostDispatchInfo { actual_weight: Some(w), pays_fee: Default::default() }
|
||||
}
|
||||
|
||||
fn info_from_pays(p: Pays) -> DispatchInfo {
|
||||
DispatchInfo { pays_fee: p, ..Default::default() }
|
||||
}
|
||||
|
||||
fn post_info_from_pays(p: Pays) -> PostDispatchInfo {
|
||||
PostDispatchInfo { actual_weight: None, pays_fee: p }
|
||||
}
|
||||
|
||||
fn default_post_info() -> PostDispatchInfo {
|
||||
PostDispatchInfo { actual_weight: None, pays_fee: Default::default() }
|
||||
}
|
||||
|
||||
fn setup_lp(asset_id: u32, balance_factor: u64) {
|
||||
let lp_provider = 5;
|
||||
assert_ok!(Balances::force_set_balance(
|
||||
RuntimeOrigin::root(),
|
||||
lp_provider,
|
||||
10_000 * balance_factor
|
||||
));
|
||||
let lp_provider_account = <Runtime as system::Config>::Lookup::unlookup(lp_provider);
|
||||
assert_ok!(Assets::mint_into(asset_id.into(), &lp_provider_account, 10_000 * balance_factor));
|
||||
|
||||
let token_1 = NativeOrAssetId::Native;
|
||||
let token_2 = NativeOrAssetId::Asset(asset_id);
|
||||
assert_ok!(AssetConversion::create_pool(RuntimeOrigin::signed(lp_provider), token_1, token_2));
|
||||
|
||||
assert_ok!(AssetConversion::add_liquidity(
|
||||
RuntimeOrigin::signed(lp_provider),
|
||||
token_1,
|
||||
token_2,
|
||||
1_000 * balance_factor, // 1 desired
|
||||
10_000 * balance_factor, // 2 desired
|
||||
1, // 1 min
|
||||
1, // 2 min
|
||||
lp_provider_account,
|
||||
));
|
||||
}
|
||||
|
||||
const WEIGHT_5: Weight = Weight::from_parts(5, 0);
|
||||
const WEIGHT_50: Weight = Weight::from_parts(50, 0);
|
||||
const WEIGHT_100: Weight = Weight::from_parts(100, 0);
|
||||
|
||||
#[test]
|
||||
fn transaction_payment_in_native_possible() {
|
||||
let base_weight = 5;
|
||||
let balance_factor = 100;
|
||||
ExtBuilder::default()
|
||||
.balance_factor(balance_factor)
|
||||
.base_weight(Weight::from_parts(base_weight, 0))
|
||||
.build()
|
||||
.execute_with(|| {
|
||||
let len = 10;
|
||||
let pre = ChargeAssetTxPayment::<Runtime>::from(0, None)
|
||||
.pre_dispatch(&1, CALL, &info_from_weight(WEIGHT_5), len)
|
||||
.unwrap();
|
||||
let initial_balance = 10 * balance_factor;
|
||||
assert_eq!(Balances::free_balance(1), initial_balance - 5 - 5 - 10);
|
||||
|
||||
assert_ok!(ChargeAssetTxPayment::<Runtime>::post_dispatch(
|
||||
Some(pre),
|
||||
&info_from_weight(WEIGHT_5),
|
||||
&default_post_info(),
|
||||
len,
|
||||
&Ok(())
|
||||
));
|
||||
assert_eq!(Balances::free_balance(1), initial_balance - 5 - 5 - 10);
|
||||
|
||||
let pre = ChargeAssetTxPayment::<Runtime>::from(5 /* tipped */, None)
|
||||
.pre_dispatch(&2, CALL, &info_from_weight(WEIGHT_100), len)
|
||||
.unwrap();
|
||||
let initial_balance_for_2 = 20 * balance_factor;
|
||||
|
||||
assert_eq!(Balances::free_balance(2), initial_balance_for_2 - 5 - 10 - 100 - 5);
|
||||
assert_ok!(ChargeAssetTxPayment::<Runtime>::post_dispatch(
|
||||
Some(pre),
|
||||
&info_from_weight(WEIGHT_100),
|
||||
&post_info_from_weight(WEIGHT_50),
|
||||
len,
|
||||
&Ok(())
|
||||
));
|
||||
assert_eq!(Balances::free_balance(2), initial_balance_for_2 - 5 - 10 - 50 - 5);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transaction_payment_in_asset_possible() {
|
||||
let base_weight = 5;
|
||||
let balance_factor = 100;
|
||||
ExtBuilder::default()
|
||||
.balance_factor(balance_factor)
|
||||
.base_weight(Weight::from_parts(base_weight, 0))
|
||||
.build()
|
||||
.execute_with(|| {
|
||||
// create the asset
|
||||
let asset_id = 1;
|
||||
let min_balance = 2;
|
||||
assert_ok!(Assets::force_create(
|
||||
RuntimeOrigin::root(),
|
||||
asset_id.into(),
|
||||
42, /* owner */
|
||||
true, /* is_sufficient */
|
||||
min_balance
|
||||
));
|
||||
|
||||
// mint into the caller account
|
||||
let caller = 1;
|
||||
let beneficiary = <Runtime as system::Config>::Lookup::unlookup(caller);
|
||||
let balance = 1000;
|
||||
|
||||
assert_ok!(Assets::mint_into(asset_id.into(), &beneficiary, balance));
|
||||
assert_eq!(Assets::balance(asset_id, caller), balance);
|
||||
|
||||
let len = 10;
|
||||
let tx_weight = 5;
|
||||
|
||||
setup_lp(asset_id, balance_factor);
|
||||
|
||||
let fee_in_native = base_weight + tx_weight + len as u64;
|
||||
let input_quote = AssetConversion::quote_price_tokens_for_exact_tokens(
|
||||
NativeOrAssetId::Asset(asset_id),
|
||||
NativeOrAssetId::Native,
|
||||
fee_in_native,
|
||||
true,
|
||||
);
|
||||
assert_eq!(input_quote, Some(201));
|
||||
|
||||
let fee_in_asset = input_quote.unwrap();
|
||||
assert_eq!(Assets::balance(asset_id, caller), balance);
|
||||
|
||||
let pre = ChargeAssetTxPayment::<Runtime>::from(0, Some(asset_id))
|
||||
.pre_dispatch(&caller, CALL, &info_from_weight(WEIGHT_5), len)
|
||||
.unwrap();
|
||||
// assert that native balance is not used
|
||||
assert_eq!(Balances::free_balance(caller), 10 * balance_factor);
|
||||
|
||||
// check that fee was charged in the given asset
|
||||
assert_eq!(Assets::balance(asset_id, caller), balance - fee_in_asset);
|
||||
|
||||
assert_ok!(ChargeAssetTxPayment::<Runtime>::post_dispatch(
|
||||
Some(pre),
|
||||
&info_from_weight(WEIGHT_5), // estimated tx weight
|
||||
&default_post_info(), // weight actually used == estimated
|
||||
len,
|
||||
&Ok(())
|
||||
));
|
||||
|
||||
assert_eq!(Assets::balance(asset_id, caller), balance - fee_in_asset);
|
||||
assert_eq!(TipUnbalancedAmount::get(), 0);
|
||||
assert_eq!(FeeUnbalancedAmount::get(), fee_in_native);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transaction_payment_in_asset_fails_if_no_pool_for_that_asset() {
|
||||
let base_weight = 5;
|
||||
let balance_factor = 100;
|
||||
ExtBuilder::default()
|
||||
.balance_factor(balance_factor)
|
||||
.base_weight(Weight::from_parts(base_weight, 0))
|
||||
.build()
|
||||
.execute_with(|| {
|
||||
// create the asset
|
||||
let asset_id = 1;
|
||||
let min_balance = 2;
|
||||
assert_ok!(Assets::force_create(
|
||||
RuntimeOrigin::root(),
|
||||
asset_id.into(),
|
||||
42, /* owner */
|
||||
true, /* is_sufficient */
|
||||
min_balance
|
||||
));
|
||||
|
||||
// mint into the caller account
|
||||
let caller = 1;
|
||||
let beneficiary = <Runtime as system::Config>::Lookup::unlookup(caller);
|
||||
let balance = 1000;
|
||||
|
||||
assert_ok!(Assets::mint_into(asset_id.into(), &beneficiary, balance));
|
||||
assert_eq!(Assets::balance(asset_id, caller), balance);
|
||||
|
||||
let len = 10;
|
||||
let pre = ChargeAssetTxPayment::<Runtime>::from(0, Some(asset_id)).pre_dispatch(
|
||||
&caller,
|
||||
CALL,
|
||||
&info_from_weight(WEIGHT_5),
|
||||
len,
|
||||
);
|
||||
|
||||
// As there is no pool in the dex set up for this asset, conversion should fail.
|
||||
assert!(pre.is_err());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transaction_payment_without_fee() {
|
||||
let base_weight = 5;
|
||||
let balance_factor = 100;
|
||||
ExtBuilder::default()
|
||||
.balance_factor(balance_factor)
|
||||
.base_weight(Weight::from_parts(base_weight, 0))
|
||||
.build()
|
||||
.execute_with(|| {
|
||||
let caller = 1;
|
||||
|
||||
// create the asset
|
||||
let asset_id = 1;
|
||||
let balance = 1000;
|
||||
let min_balance = 2;
|
||||
|
||||
assert_ok!(Assets::force_create(
|
||||
RuntimeOrigin::root(),
|
||||
asset_id.into(),
|
||||
42, /* owner */
|
||||
true, /* is_sufficient */
|
||||
min_balance,
|
||||
));
|
||||
|
||||
setup_lp(asset_id, balance_factor);
|
||||
|
||||
// mint into the caller account
|
||||
let beneficiary = <Runtime as system::Config>::Lookup::unlookup(caller);
|
||||
assert_ok!(Assets::mint_into(asset_id.into(), &beneficiary, balance));
|
||||
assert_eq!(Assets::balance(asset_id, caller), balance);
|
||||
|
||||
let weight = 5;
|
||||
let len = 10;
|
||||
let fee_in_native = base_weight + weight + len as u64;
|
||||
let input_quote = AssetConversion::quote_price_tokens_for_exact_tokens(
|
||||
NativeOrAssetId::Asset(asset_id),
|
||||
NativeOrAssetId::Native,
|
||||
fee_in_native,
|
||||
true,
|
||||
);
|
||||
assert_eq!(input_quote, Some(201));
|
||||
|
||||
let fee_in_asset = input_quote.unwrap();
|
||||
let pre = ChargeAssetTxPayment::<Runtime>::from(0, Some(asset_id))
|
||||
.pre_dispatch(&caller, CALL, &info_from_weight(WEIGHT_5), len)
|
||||
.unwrap();
|
||||
|
||||
// assert that native balance is not used
|
||||
assert_eq!(Balances::free_balance(caller), 10 * balance_factor);
|
||||
// check that fee was charged in the given asset
|
||||
assert_eq!(Assets::balance(asset_id, caller), balance - fee_in_asset);
|
||||
|
||||
let refund = AssetConversion::quote_price_exact_tokens_for_tokens(
|
||||
NativeOrAssetId::Native,
|
||||
NativeOrAssetId::Asset(asset_id),
|
||||
fee_in_native,
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(refund, 199);
|
||||
|
||||
assert_ok!(ChargeAssetTxPayment::<Runtime>::post_dispatch(
|
||||
Some(pre),
|
||||
&info_from_weight(WEIGHT_5),
|
||||
&post_info_from_pays(Pays::No),
|
||||
len,
|
||||
&Ok(())
|
||||
));
|
||||
|
||||
// caller should get refunded
|
||||
assert_eq!(Assets::balance(asset_id, caller), balance - fee_in_asset + refund);
|
||||
assert_eq!(Balances::free_balance(caller), 10 * balance_factor);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn asset_transaction_payment_with_tip_and_refund() {
|
||||
let base_weight = 5;
|
||||
let balance_factor = 100;
|
||||
ExtBuilder::default()
|
||||
.balance_factor(balance_factor)
|
||||
.base_weight(Weight::from_parts(base_weight, 0))
|
||||
.build()
|
||||
.execute_with(|| {
|
||||
// create the asset
|
||||
let asset_id = 1;
|
||||
let min_balance = 2;
|
||||
assert_ok!(Assets::force_create(
|
||||
RuntimeOrigin::root(),
|
||||
asset_id.into(),
|
||||
42, /* owner */
|
||||
true, /* is_sufficient */
|
||||
min_balance,
|
||||
));
|
||||
|
||||
setup_lp(asset_id, balance_factor);
|
||||
|
||||
// mint into the caller account
|
||||
let caller = 2;
|
||||
let beneficiary = <Runtime as system::Config>::Lookup::unlookup(caller);
|
||||
let balance = 10000;
|
||||
|
||||
assert_ok!(Assets::mint_into(asset_id.into(), &beneficiary, balance));
|
||||
assert_eq!(Assets::balance(asset_id, caller), balance);
|
||||
|
||||
let weight = 100;
|
||||
let tip = 5;
|
||||
let len = 10;
|
||||
let fee_in_native = base_weight + weight + len as u64 + tip;
|
||||
let input_quote = AssetConversion::quote_price_tokens_for_exact_tokens(
|
||||
NativeOrAssetId::Asset(asset_id),
|
||||
NativeOrAssetId::Native,
|
||||
fee_in_native,
|
||||
true,
|
||||
);
|
||||
assert_eq!(input_quote, Some(1206));
|
||||
|
||||
let fee_in_asset = input_quote.unwrap();
|
||||
let pre = ChargeAssetTxPayment::<Runtime>::from(tip, Some(asset_id))
|
||||
.pre_dispatch(&caller, CALL, &info_from_weight(WEIGHT_100), len)
|
||||
.unwrap();
|
||||
assert_eq!(Assets::balance(asset_id, caller), balance - fee_in_asset);
|
||||
|
||||
let final_weight = 50;
|
||||
let expected_fee = fee_in_native - final_weight - tip;
|
||||
let expected_token_refund = AssetConversion::quote_price_exact_tokens_for_tokens(
|
||||
NativeOrAssetId::Native,
|
||||
NativeOrAssetId::Asset(asset_id),
|
||||
fee_in_native - expected_fee - tip,
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_ok!(ChargeAssetTxPayment::<Runtime>::post_dispatch(
|
||||
Some(pre),
|
||||
&info_from_weight(WEIGHT_100),
|
||||
&post_info_from_weight(WEIGHT_50),
|
||||
len,
|
||||
&Ok(())
|
||||
));
|
||||
|
||||
assert_eq!(TipUnbalancedAmount::get(), tip);
|
||||
assert_eq!(FeeUnbalancedAmount::get(), expected_fee);
|
||||
|
||||
// caller should get refunded
|
||||
assert_eq!(
|
||||
Assets::balance(asset_id, caller),
|
||||
balance - fee_in_asset + expected_token_refund
|
||||
);
|
||||
assert_eq!(Balances::free_balance(caller), 20 * balance_factor);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn payment_from_account_with_only_assets() {
|
||||
let base_weight = 5;
|
||||
let balance_factor = 100;
|
||||
ExtBuilder::default()
|
||||
.balance_factor(balance_factor)
|
||||
.base_weight(Weight::from_parts(base_weight, 0))
|
||||
.build()
|
||||
.execute_with(|| {
|
||||
// create the asset
|
||||
let asset_id = 1;
|
||||
let min_balance = 2;
|
||||
assert_ok!(Assets::force_create(
|
||||
RuntimeOrigin::root(),
|
||||
asset_id.into(),
|
||||
42, /* owner */
|
||||
true, /* is_sufficient */
|
||||
min_balance,
|
||||
));
|
||||
|
||||
setup_lp(asset_id, balance_factor);
|
||||
|
||||
// mint into the caller account
|
||||
let caller = 333;
|
||||
let beneficiary = <Runtime as system::Config>::Lookup::unlookup(caller);
|
||||
let balance = 1000;
|
||||
|
||||
assert_ok!(Assets::mint_into(asset_id.into(), &beneficiary, balance));
|
||||
assert_eq!(Assets::balance(asset_id, caller), balance);
|
||||
|
||||
// assert that native balance is not necessary
|
||||
assert_eq!(Balances::free_balance(caller), 0);
|
||||
let weight = 5;
|
||||
let len = 10;
|
||||
|
||||
let fee_in_native = base_weight + weight + len as u64;
|
||||
let ed = Balances::minimum_balance();
|
||||
let fee_in_asset = AssetConversion::quote_price_tokens_for_exact_tokens(
|
||||
NativeOrAssetId::Asset(asset_id),
|
||||
NativeOrAssetId::Native,
|
||||
fee_in_native + ed,
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(fee_in_asset, 301);
|
||||
|
||||
let pre = ChargeAssetTxPayment::<Runtime>::from(0, Some(asset_id))
|
||||
.pre_dispatch(&caller, CALL, &info_from_weight(WEIGHT_5), len)
|
||||
.unwrap();
|
||||
assert_eq!(Balances::free_balance(caller), ed);
|
||||
// check that fee was charged in the given asset
|
||||
assert_eq!(Assets::balance(asset_id, caller), balance - fee_in_asset);
|
||||
|
||||
let refund = AssetConversion::quote_price_exact_tokens_for_tokens(
|
||||
NativeOrAssetId::Native,
|
||||
NativeOrAssetId::Asset(asset_id),
|
||||
ed,
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_ok!(ChargeAssetTxPayment::<Runtime>::post_dispatch(
|
||||
Some(pre),
|
||||
&info_from_weight(WEIGHT_5),
|
||||
&default_post_info(),
|
||||
len,
|
||||
&Ok(())
|
||||
));
|
||||
assert_eq!(Assets::balance(asset_id, caller), balance - fee_in_asset + refund);
|
||||
assert_eq!(Balances::free_balance(caller), 0);
|
||||
|
||||
assert_eq!(TipUnbalancedAmount::get(), 0);
|
||||
assert_eq!(FeeUnbalancedAmount::get(), fee_in_native);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converted_fee_is_never_zero_if_input_fee_is_not() {
|
||||
let base_weight = 1;
|
||||
let balance_factor = 100;
|
||||
ExtBuilder::default()
|
||||
.balance_factor(balance_factor)
|
||||
.base_weight(Weight::from_parts(base_weight, 0))
|
||||
.build()
|
||||
.execute_with(|| {
|
||||
// create the asset
|
||||
let asset_id = 1;
|
||||
let min_balance = 1;
|
||||
assert_ok!(Assets::force_create(
|
||||
RuntimeOrigin::root(),
|
||||
asset_id.into(),
|
||||
42, /* owner */
|
||||
true, /* is_sufficient */
|
||||
min_balance
|
||||
));
|
||||
|
||||
setup_lp(asset_id, balance_factor);
|
||||
|
||||
// mint into the caller account
|
||||
let caller = 2;
|
||||
let beneficiary = <Runtime as system::Config>::Lookup::unlookup(caller);
|
||||
let balance = 1000;
|
||||
|
||||
assert_ok!(Assets::mint_into(asset_id.into(), &beneficiary, balance));
|
||||
assert_eq!(Assets::balance(asset_id, caller), balance);
|
||||
|
||||
let weight = 1;
|
||||
let len = 1;
|
||||
|
||||
// there will be no conversion when the fee is zero
|
||||
{
|
||||
let pre = ChargeAssetTxPayment::<Runtime>::from(0, Some(asset_id))
|
||||
.pre_dispatch(&caller, CALL, &info_from_pays(Pays::No), len)
|
||||
.unwrap();
|
||||
// `Pays::No` implies there are no fees
|
||||
assert_eq!(Assets::balance(asset_id, caller), balance);
|
||||
|
||||
assert_ok!(ChargeAssetTxPayment::<Runtime>::post_dispatch(
|
||||
Some(pre),
|
||||
&info_from_pays(Pays::No),
|
||||
&post_info_from_pays(Pays::No),
|
||||
len,
|
||||
&Ok(())
|
||||
));
|
||||
assert_eq!(Assets::balance(asset_id, caller), balance);
|
||||
}
|
||||
|
||||
// validate even a small fee gets converted to asset.
|
||||
let fee_in_native = base_weight + weight + len as u64;
|
||||
let fee_in_asset = AssetConversion::quote_price_tokens_for_exact_tokens(
|
||||
NativeOrAssetId::Asset(asset_id),
|
||||
NativeOrAssetId::Native,
|
||||
fee_in_native,
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let pre = ChargeAssetTxPayment::<Runtime>::from(0, Some(asset_id))
|
||||
.pre_dispatch(&caller, CALL, &info_from_weight(Weight::from_parts(weight, 0)), len)
|
||||
.unwrap();
|
||||
assert_eq!(Assets::balance(asset_id, caller), balance - fee_in_asset);
|
||||
|
||||
assert_ok!(ChargeAssetTxPayment::<Runtime>::post_dispatch(
|
||||
Some(pre),
|
||||
&info_from_weight(Weight::from_parts(weight, 0)),
|
||||
&default_post_info(),
|
||||
len,
|
||||
&Ok(())
|
||||
));
|
||||
assert_eq!(Assets::balance(asset_id, caller), balance - fee_in_asset);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn post_dispatch_fee_is_zero_if_pre_dispatch_fee_is_zero() {
|
||||
let base_weight = 1;
|
||||
ExtBuilder::default()
|
||||
.balance_factor(100)
|
||||
.base_weight(Weight::from_parts(base_weight, 0))
|
||||
.build()
|
||||
.execute_with(|| {
|
||||
// create the asset
|
||||
let asset_id = 1;
|
||||
let min_balance = 100;
|
||||
assert_ok!(Assets::force_create(
|
||||
RuntimeOrigin::root(),
|
||||
asset_id.into(),
|
||||
42, /* owner */
|
||||
true, /* is_sufficient */
|
||||
min_balance
|
||||
));
|
||||
|
||||
// mint into the caller account
|
||||
let caller = 333;
|
||||
let beneficiary = <Runtime as system::Config>::Lookup::unlookup(caller);
|
||||
let balance = 1000;
|
||||
|
||||
assert_ok!(Assets::mint_into(asset_id.into(), &beneficiary, balance));
|
||||
assert_eq!(Assets::balance(asset_id, caller), balance);
|
||||
|
||||
let weight = 1;
|
||||
let len = 1;
|
||||
let fee = base_weight + weight + len as u64;
|
||||
|
||||
// calculated fee is greater than 0
|
||||
assert!(fee > 0);
|
||||
|
||||
let pre = ChargeAssetTxPayment::<Runtime>::from(0, Some(asset_id))
|
||||
.pre_dispatch(&caller, CALL, &info_from_pays(Pays::No), len)
|
||||
.unwrap();
|
||||
// `Pays::No` implies no pre-dispatch fees
|
||||
|
||||
assert_eq!(Assets::balance(asset_id, caller), balance);
|
||||
|
||||
let (_tip, _who, initial_payment, _asset_id) = ⪯
|
||||
let not_paying = match initial_payment {
|
||||
&InitialPayment::Nothing => true,
|
||||
_ => false,
|
||||
};
|
||||
assert!(not_paying, "initial payment should be Nothing if we pass Pays::No");
|
||||
|
||||
// `Pays::Yes` on post-dispatch does not mean we pay (we never charge more than the
|
||||
// initial fee)
|
||||
assert_ok!(ChargeAssetTxPayment::<Runtime>::post_dispatch(
|
||||
Some(pre),
|
||||
&info_from_pays(Pays::No),
|
||||
&post_info_from_pays(Pays::Yes),
|
||||
len,
|
||||
&Ok(())
|
||||
));
|
||||
assert_eq!(Assets::balance(asset_id, caller), balance);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn post_dispatch_fee_is_zero_if_unsigned_pre_dispatch_fee_is_zero() {
|
||||
let base_weight = 1;
|
||||
ExtBuilder::default()
|
||||
.balance_factor(100)
|
||||
.base_weight(Weight::from_parts(base_weight, 0))
|
||||
.build()
|
||||
.execute_with(|| {
|
||||
// create the asset
|
||||
let asset_id = 1;
|
||||
let min_balance = 100;
|
||||
assert_ok!(Assets::force_create(
|
||||
RuntimeOrigin::root(),
|
||||
asset_id.into(),
|
||||
42, /* owner */
|
||||
true, /* is_sufficient */
|
||||
min_balance
|
||||
));
|
||||
|
||||
// mint into the caller account
|
||||
let caller = 333;
|
||||
let beneficiary = <Runtime as system::Config>::Lookup::unlookup(caller);
|
||||
let balance = 1000;
|
||||
|
||||
assert_ok!(Assets::mint_into(asset_id.into(), &beneficiary, balance));
|
||||
assert_eq!(Assets::balance(asset_id, caller), balance);
|
||||
|
||||
let weight = 1;
|
||||
let len = 1;
|
||||
ChargeAssetTxPayment::<Runtime>::pre_dispatch_unsigned(
|
||||
CALL,
|
||||
&info_from_weight(Weight::from_parts(weight, 0)),
|
||||
len,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(Assets::balance(asset_id, caller), balance);
|
||||
|
||||
// `Pays::Yes` on post-dispatch does not mean we pay (we never charge more than the
|
||||
// initial fee)
|
||||
assert_ok!(ChargeAssetTxPayment::<Runtime>::post_dispatch(
|
||||
None,
|
||||
&info_from_weight(Weight::from_parts(weight, 0)),
|
||||
&post_info_from_pays(Pays::Yes),
|
||||
len,
|
||||
&Ok(())
|
||||
));
|
||||
assert_eq!(Assets::balance(asset_id, caller), balance);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user