use super::*;
use crate::Config;
use alloc::vec;
use core::marker::PhantomData;
use frame_support::{
defensive, ensure,
traits::{
fungibles,
tokens::{Balance, Fortitude, Precision, Preservation, WithdrawConsequence},
Defensive, OnUnbalanced, SameOrOther,
},
unsigned::TransactionValidityError,
};
use pallet_asset_conversion::{QuotePrice, SwapCredit};
use sp_runtime::{
traits::{DispatchInfoOf, Get, PostDispatchInfoOf, Zero},
transaction_validity::InvalidTransaction,
Saturating,
};
pub trait OnChargeAssetTransaction<T: Config> {
type Balance: Balance;
type AssetId: AssetId;
type LiquidityInfo;
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<Self::LiquidityInfo, TransactionValidityError>;
fn can_withdraw_fee(
who: &T::AccountId,
asset_id: Self::AssetId,
fee: Self::Balance,
) -> Result<(), TransactionValidityError>;
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,
asset_id: Self::AssetId,
already_withdraw: Self::LiquidityInfo,
) -> Result<BalanceOf<T>, TransactionValidityError>;
}
pub struct SwapAssetAdapter<A, F, S, OU>(PhantomData<(A, F, S, OU)>);
impl<A, F, S, OU, T> OnChargeAssetTransaction<T> for SwapAssetAdapter<A, F, S, OU>
where
A: Get<T::AssetId>,
F: fungibles::Balanced<T::AccountId, Balance = BalanceOf<T>, AssetId = T::AssetId>,
S: SwapCredit<
T::AccountId,
Balance = BalanceOf<T>,
AssetKind = T::AssetId,
Credit = fungibles::Credit<T::AccountId, F>,
> + QuotePrice<Balance = BalanceOf<T>, AssetKind = T::AssetId>,
OU: OnUnbalanced<fungibles::Credit<T::AccountId, F>>,
T: Config,
{
type AssetId = T::AssetId;
type Balance = BalanceOf<T>;
type LiquidityInfo = (fungibles::Credit<T::AccountId, F>, BalanceOf<T>);
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<Self::LiquidityInfo, TransactionValidityError> {
if asset_id == A::get() {
let fee_credit = F::withdraw(
asset_id.clone(),
who,
fee,
Precision::Exact,
Preservation::Preserve,
Fortitude::Polite,
)
.map_err(|_| InvalidTransaction::Payment)?;
return Ok((fee_credit, fee));
}
let asset_fee =
S::quote_price_tokens_for_exact_tokens(asset_id.clone(), A::get(), fee, true)
.ok_or(InvalidTransaction::Payment)?;
let asset_fee_credit = F::withdraw(
asset_id.clone(),
who,
asset_fee,
Precision::Exact,
Preservation::Preserve,
Fortitude::Polite,
)
.map_err(|_| InvalidTransaction::Payment)?;
let (fee_credit, change) = match S::swap_tokens_for_exact_tokens(
vec![asset_id, A::get()],
asset_fee_credit,
fee,
) {
Ok((fee_credit, change)) => (fee_credit, change),
Err((credit_in, _)) => {
defensive!("Fee swap should pass for the quoted amount");
let _ = F::resolve(who, credit_in).defensive_proof("Should resolve the credit");
return Err(InvalidTransaction::Payment.into())
},
};
ensure!(change.peek().is_zero(), InvalidTransaction::Payment);
Ok((fee_credit, asset_fee))
}
fn can_withdraw_fee(
who: &T::AccountId,
asset_id: Self::AssetId,
fee: BalanceOf<T>,
) -> Result<(), TransactionValidityError> {
if asset_id == A::get() {
match F::can_withdraw(asset_id.clone(), who, fee) {
WithdrawConsequence::BalanceLow |
WithdrawConsequence::UnknownAsset |
WithdrawConsequence::Underflow |
WithdrawConsequence::Overflow |
WithdrawConsequence::Frozen =>
return Err(TransactionValidityError::from(InvalidTransaction::Payment)),
WithdrawConsequence::Success |
WithdrawConsequence::ReducedToZero(_) |
WithdrawConsequence::WouldDie => return Ok(()),
}
}
let asset_fee =
S::quote_price_tokens_for_exact_tokens(asset_id.clone(), A::get(), fee, true)
.ok_or(InvalidTransaction::Payment)?;
match F::can_withdraw(asset_id.clone(), who, asset_fee) {
WithdrawConsequence::BalanceLow |
WithdrawConsequence::UnknownAsset |
WithdrawConsequence::Underflow |
WithdrawConsequence::Overflow |
WithdrawConsequence::Frozen =>
return Err(TransactionValidityError::from(InvalidTransaction::Payment)),
WithdrawConsequence::Success |
WithdrawConsequence::ReducedToZero(_) |
WithdrawConsequence::WouldDie => {},
};
Ok(())
}
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,
asset_id: Self::AssetId,
already_withdrawn: Self::LiquidityInfo,
) -> Result<BalanceOf<T>, TransactionValidityError> {
let (fee_paid, initial_asset_consumed) = already_withdrawn;
let refund_amount = fee_paid.peek().saturating_sub(corrected_fee);
let (fee_in_asset, adjusted_paid) = if refund_amount.is_zero() ||
F::total_balance(asset_id.clone(), who).is_zero()
{
(initial_asset_consumed, fee_paid)
} else if asset_id == A::get() {
let (refund, fee_paid) = fee_paid.split(refund_amount);
if let Err(refund) = F::resolve(who, refund) {
let fee_paid = fee_paid.merge(refund).map_err(|_| {
defensive!("`fee_paid` and `refund` are credits of the same asset.");
InvalidTransaction::Payment
})?;
(initial_asset_consumed, fee_paid)
} else {
(fee_paid.peek().saturating_sub(refund_amount), fee_paid)
}
} else {
let refund_asset_amount = S::quote_price_exact_tokens_for_tokens(
A::get(),
asset_id.clone(),
refund_amount,
true,
)
.unwrap_or(Zero::zero());
let debt = if refund_asset_amount.is_zero() {
fungibles::Debt::<T::AccountId, F>::zero(asset_id.clone())
} else {
match F::deposit(asset_id.clone(), &who, refund_asset_amount, Precision::BestEffort)
{
Ok(debt) => debt,
Err(_) => fungibles::Debt::<T::AccountId, F>::zero(asset_id.clone()),
}
};
if debt.peek().is_zero() {
(initial_asset_consumed, fee_paid)
} else {
let (refund, adjusted_paid) = fee_paid.split(refund_amount);
match S::swap_exact_tokens_for_tokens(
vec![A::get(), asset_id],
refund,
Some(refund_asset_amount),
) {
Ok(refund_asset) => {
match refund_asset.offset(debt) {
Ok(SameOrOther::None) => {},
_ => {
defensive!("Debt should be equal to the refund credit");
return Err(InvalidTransaction::Payment.into())
},
};
(
initial_asset_consumed.saturating_sub(refund_asset_amount.into()),
adjusted_paid,
)
},
Err((refund, _)) => {
defensive!("Refund swap should pass for the quoted amount");
match F::settle(who, debt, Preservation::Expendable) {
Ok(dust) => ensure!(dust.peek().is_zero(), InvalidTransaction::Payment),
Err(_) => {
defensive!("Should settle the debt");
return Err(InvalidTransaction::Payment.into())
},
};
let adjusted_paid = adjusted_paid.merge(refund).map_err(|_| {
InvalidTransaction::Payment
})?;
(initial_asset_consumed, adjusted_paid)
},
}
}
};
let (tip, fee) = adjusted_paid.split(tip);
OU::on_unbalanceds(Some(fee).into_iter().chain(Some(tip)));
Ok(fee_in_asset)
}
}