diff --git a/substrate/frame/asset-conversion/src/fungibles.rs b/substrate/frame/asset-conversion/src/fungibles.rs new file mode 100644 index 000000000000..454e66d4ec02 --- /dev/null +++ b/substrate/frame/asset-conversion/src/fungibles.rs @@ -0,0 +1,113 @@ +// This file is part of Substrate. + +// Copyright (C) 2023 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::*; + +/// TODO +pub type NativeCredit = + CreditFungible<::AccountId, ::Currency>; + +/// TODO +pub type AssetCredit = + CreditFungibles<::AccountId, ::Assets>; + +/// TODO +pub enum Credit { + /// TODO + Native(NativeCredit), + /// TODO + Asset(AssetCredit), +} + +impl From> for Credit { + fn from(value: NativeCredit) -> Self { + Credit::Native(value) + } +} + +impl From> for Credit { + fn from(value: AssetCredit) -> Self { + Credit::Asset(value) + } +} + +impl Credit { + /// TODO + pub fn peek(&self) -> T::AssetBalance { + match self { + Credit::Native(c) => c.peek().into(), + Credit::Asset(c) => c.peek(), + } + } + + /// TODO + pub fn split(self, amount: T::AssetBalance) -> (Self, Self) { + match self { + Credit::Native(c) => { + let (left, right) = c.split(amount.into()); + (left.into(), right.into()) + }, + Credit::Asset(c) => { + let (left, right) = c.split(amount); + (left.into(), right.into()) + }, + } + } + + /// TODO + pub fn asset(&self) -> T::MultiAssetId { + match self { + Credit::Native(_) => T::MultiAssetIdConverter::get_native(), + Credit::Asset(c) => c.asset().into(), + } + } +} + +impl Pallet { + /// TODO + pub fn resolve(who: &T::AccountId, credit: Credit) -> Result<(), Credit> { + match credit { + Credit::Native(c) => T::Currency::resolve(who, c).map_err(|c| c.into()), + Credit::Asset(c) => T::Assets::resolve(who, c).map_err(|c| c.into()), + } + } + + /// TODO + pub fn withdraw( + asset_id: &T::MultiAssetId, + who: &T::AccountId, + amount: T::AssetBalance, + keep_alive: bool, + ) -> Result, DispatchError> { + let preservation = match keep_alive { + true => Preserve, + false => Expendable, + }; + + match T::MultiAssetIdConverter::try_convert(asset_id) { + MultiAssetIdConversionResult::Converted(asset_id) => + T::Assets::withdraw(asset_id, who, amount, Exact, preservation, Polite) + .map(|c| c.into()), + MultiAssetIdConversionResult::Native => { + let amount = Self::convert_asset_balance_to_native_balance(amount)?; + T::Currency::withdraw(who, amount, Exact, preservation, Polite).map(|c| c.into()) + }, + MultiAssetIdConversionResult::Unsupported(_) => + Err(Error::::UnsupportedAsset.into()), + } + } +} diff --git a/substrate/frame/asset-conversion/src/lib.rs b/substrate/frame/asset-conversion/src/lib.rs index 8d811473e861..04bdac4b24b6 100644 --- a/substrate/frame/asset-conversion/src/lib.rs +++ b/substrate/frame/asset-conversion/src/lib.rs @@ -53,13 +53,17 @@ //! (This can be run against the kitchen sync node in the `node` folder of this repo.) #![deny(missing_docs)] #![cfg_attr(not(feature = "std"), no_std)] -use frame_support::traits::{DefensiveOption, Incrementable}; #[cfg(feature = "runtime-benchmarks")] mod benchmarking; +mod fungibles; +mod swap; mod types; pub mod weights; +pub use fungibles::*; +pub use swap::*; +pub use types::*; #[cfg(test)] mod tests; @@ -69,8 +73,23 @@ mod mock; use codec::Codec; use frame_support::{ - ensure, - traits::tokens::{AssetId, Balance}, + ensure, match_err, + pallet_prelude::DispatchResult, + traits::{ + fungible::{ + Balanced as BalancedFungible, Credit as CreditFungible, Inspect as InspectFungible, + Mutate as MutateFungible, + }, + fungibles::{Balanced, Create, Credit as CreditFungibles, Inspect, Mutate}, + tokens::{ + AssetId, Balance, + Fortitude::Polite, + Precision::Exact, + Preservation::{Expendable, Preserve}, + }, + AccountTouch, ContainsPair, DefensiveOption, Imbalance, Incrementable, PrefixedResult, + }, + BoundedBTreeSet, PalletId, }; use frame_system::{ ensure_signed, @@ -80,36 +99,19 @@ pub use pallet::*; use sp_arithmetic::traits::Unsigned; use sp_runtime::{ traits::{ - CheckedAdd, CheckedDiv, CheckedMul, CheckedSub, Ensure, MaybeDisplay, TrailingZeroInput, + CheckedAdd, CheckedDiv, CheckedMul, CheckedSub, Ensure, IntegerSquareRoot, MaybeDisplay, + One, TrailingZeroInput, Zero, }, - DispatchError, + DispatchError, Saturating, }; use sp_std::prelude::*; -pub use types::*; pub use weights::WeightInfo; #[frame_support::pallet] pub mod pallet { use super::*; - use frame_support::{ - pallet_prelude::*, - traits::{ - fungible::{Inspect as InspectFungible, Mutate as MutateFungible}, - fungibles::{Create, Inspect, Mutate}, - tokens::{ - Fortitude::Polite, - Precision::Exact, - Preservation::{Expendable, Preserve}, - }, - AccountTouch, ContainsPair, - }, - BoundedBTreeSet, PalletId, - }; + use frame_support::pallet_prelude::*; use sp_arithmetic::Permill; - use sp_runtime::{ - traits::{IntegerSquareRoot, One, Zero}, - Saturating, - }; #[pallet::pallet] pub struct Pallet(_); @@ -121,13 +123,14 @@ pub mod pallet { /// Currency type that this works on. type Currency: InspectFungible - + MutateFungible; + + MutateFungible + + BalancedFungible; /// The `Currency::Balance` type of the native currency. - type Balance: Balance; + type Balance: Balance + Into; /// The type used to describe the amount of fractions converted into assets. - type AssetBalance: Balance; + type AssetBalance: Balance + Into; /// A type used for conversions between `Balance` and `AssetBalance`. type HigherPrecisionBalance: IntegerSquareRoot @@ -162,7 +165,8 @@ pub mod pallet { type Assets: Inspect + Mutate + AccountTouch - + ContainsPair; + + ContainsPair + + Balanced; /// Registry for the lp tokens. Ideally only this pallet should have create permissions on /// the assets. @@ -285,7 +289,7 @@ pub mod pallet { send_to: T::AccountId, /// The route of asset ids that the swap went through. /// E.g. A -> Dot -> B - path: BoundedVec, + balance_path: BalancePath, /// The amount of the first asset that was swapped. amount_in: T::AssetBalance, /// The amount of the second asset that was received. @@ -355,6 +359,8 @@ pub mod pallet { PoolMustContainNativeCurrency, /// The provided path must consists of 2 assets at least. InvalidPath, + /// The provided balance path must consists of 2 assets at least. + InvalidBalancePath, /// It was not possible to calculate path data. PathError, /// The provided path must consists of unique assets. @@ -365,6 +371,8 @@ pub mod pallet { /// with another. For example, an array of assets constituting a `path` should have a /// corresponding array of `amounts` along the path. CorrespondenceError, + /// error to be replaced by some from above or to be created new. + TODO, } #[pallet::hooks] @@ -476,10 +484,11 @@ pub mod pallet { ) -> DispatchResult { let sender = ensure_signed(origin)?; - let pool_id = Self::get_pool_id(asset1.clone(), asset2.clone()); + let pool_id = Self::get_pool_id(asset1, asset2); + let (asset1, _) = &pool_id; // swap params if needed let (amount1_desired, amount2_desired, amount1_min, amount2_min) = - if pool_id.0 == asset1 { + if &pool_id.0 == asset1 { (amount1_desired, amount2_desired, amount1_min, amount2_min) } else { (amount2_desired, amount1_desired, amount2_min, amount1_min) @@ -587,9 +596,10 @@ pub mod pallet { ) -> DispatchResult { let sender = ensure_signed(origin)?; - let pool_id = Self::get_pool_id(asset1.clone(), asset2.clone()); + let pool_id = Self::get_pool_id(asset1, asset2); + let (asset1, _) = &pool_id; // swap params if needed - let (amount1_min_receive, amount2_min_receive) = if pool_id.0 == asset1 { + let (amount1_min_receive, amount2_min_receive) = if &pool_id.0 == asset1 { (amount1_min_receive, amount2_min_receive) } else { (amount2_min_receive, amount1_min_receive) @@ -657,7 +667,7 @@ pub mod pallet { #[pallet::weight(T::WeightInfo::swap_exact_tokens_for_tokens())] pub fn swap_exact_tokens_for_tokens( origin: OriginFor, - path: BoundedVec, + path: Path, amount_in: T::AssetBalance, amount_out_min: T::AssetBalance, send_to: T::AccountId, @@ -685,7 +695,7 @@ pub mod pallet { #[pallet::weight(T::WeightInfo::swap_tokens_for_exact_tokens())] pub fn swap_tokens_for_exact_tokens( origin: OriginFor, - path: BoundedVec, + path: Path, amount_out: T::AssetBalance, amount_in_max: T::AssetBalance, send_to: T::AccountId, @@ -713,9 +723,9 @@ pub mod pallet { /// respecting `keep_alive`. /// /// If successful, returns the amount of `path[1]` acquired for the `amount_in`. - pub fn do_swap_exact_tokens_for_tokens( + pub(crate) fn do_swap_exact_tokens_for_tokens( sender: T::AccountId, - path: BoundedVec, + path: Path, amount_in: T::AssetBalance, amount_out_min: Option, send_to: T::AccountId, @@ -728,9 +738,11 @@ pub mod pallet { Self::validate_swap_path(&path)?; - let amounts = Self::get_amounts_out(&amount_in, &path)?; - let amount_out = - *amounts.last().defensive_ok_or("get_amounts_out() returned an empty result")?; + let balance_path = Self::balance_path_from_amount_in(amount_in, path.clone())?; + + let (_, amount_out) = *balance_path + .last() + .defensive_ok_or("get_amounts_out() returned an empty result")?; if let Some(amount_out_min) = amount_out_min { ensure!( @@ -739,7 +751,7 @@ pub mod pallet { ); } - Self::do_swap(sender, &amounts, path, send_to, keep_alive)?; + Self::withdraw_and_swap(sender, balance_path, send_to, keep_alive)?; Ok(amount_out) } @@ -751,9 +763,9 @@ pub mod pallet { /// respecting `keep_alive`. /// /// If successful returns the amount of the `path[0]` taken to provide `path[1]`. - pub fn do_swap_tokens_for_exact_tokens( + pub(crate) fn do_swap_tokens_for_exact_tokens( sender: T::AccountId, - path: BoundedVec, + path: Path, amount_out: T::AssetBalance, amount_in_max: Option, send_to: T::AccountId, @@ -766,9 +778,11 @@ pub mod pallet { Self::validate_swap_path(&path)?; - let amounts = Self::get_amounts_in(&amount_out, &path)?; - let amount_in = - *amounts.first().defensive_ok_or("get_amounts_in() returned an empty result")?; + let balance_path = Self::balance_path_from_amount_out(amount_out, path.clone())?; + + let (_, amount_in) = *balance_path + .first() + .defensive_ok_or("get_amounts_in() returned an empty result")?; if let Some(amount_in_max) = amount_in_max { ensure!( @@ -777,7 +791,7 @@ pub mod pallet { ); } - Self::do_swap(sender, &amounts, path, send_to, keep_alive)?; + Self::withdraw_and_swap(sender, balance_path, send_to, keep_alive)?; Ok(amount_in) } @@ -846,62 +860,67 @@ pub mod pallet { } /// Swap assets along a `path`, depositing in `send_to`. - pub(crate) fn do_swap( + pub(crate) fn withdraw_and_swap( sender: T::AccountId, - amounts: &Vec, - path: BoundedVec, + balance_path: BalancePath, send_to: T::AccountId, keep_alive: bool, ) -> Result<(), DispatchError> { - ensure!(amounts.len() > 1, Error::::CorrespondenceError); - if let Some([asset1, asset2]) = &path.get(0..2) { + let first = balance_path.first().ok_or(Error::::CorrespondenceError)?; + let (asset_in, amount_in) = first.clone(); + + let (_, amount_out) = *balance_path.last().ok_or(Error::::PathError)?; + + let withdrawal = Self::withdraw(&asset_in, &sender, amount_in, keep_alive)?; + let credit_out = Self::do_swap(withdrawal, &balance_path).map_err(|(_, err)| err)?; + Self::resolve(&send_to, credit_out).map_err(|_| Error::::Overflow)?; + Self::deposit_event(Event::SwapExecuted { + who: sender, + send_to, + balance_path, + amount_in, + amount_out, + }); + + Ok(()) + } + + // Note: recursive + pub(crate) fn do_swap( + credit_in: Credit, + balance_path: &BalancePath, + ) -> Result, (Credit, DispatchError)> { + let path_len = balance_path.len() as u32; + if path_len == 0 { + return Ok(credit_in) + } + + return if let Some([(asset1, _), (asset2, asset2_amount)]) = &balance_path.get(0..2) { let pool_id = Self::get_pool_id(asset1.clone(), asset2.clone()); let pool_account = Self::get_pool_account(&pool_id); - // amounts should always contain a corresponding element to path. - let first_amount = amounts.first().ok_or(Error::::CorrespondenceError)?; - - Self::transfer(asset1, &sender, &pool_account, *first_amount, keep_alive)?; - - let mut i = 0; - let path_len = path.len() as u32; - for assets_pair in path.windows(2) { - if let [asset1, asset2] = assets_pair { - let pool_id = Self::get_pool_id(asset1.clone(), asset2.clone()); - let pool_account = Self::get_pool_account(&pool_id); - - let amount_out = - amounts.get((i + 1) as usize).ok_or(Error::::CorrespondenceError)?; - - let to = if i < path_len - 2 { - let asset3 = path.get((i + 2) as usize).ok_or(Error::::PathError)?; - Self::get_pool_account(&Self::get_pool_id( - asset2.clone(), - asset3.clone(), - )) - } else { - send_to.clone() - }; - - let reserve = Self::get_balance(&pool_account, asset2)?; - let reserve_left = reserve.saturating_sub(*amount_out); - Self::validate_minimal_amount(reserve_left, asset2) - .map_err(|_| Error::::ReserveLeftLessThanMinimal)?; - - Self::transfer(asset2, &pool_account, &to, *amount_out, true)?; - } - i.saturating_inc(); - } - Self::deposit_event(Event::SwapExecuted { - who: sender, - send_to, - path, - amount_in: *first_amount, - amount_out: *amounts.last().expect("Always has more than 1 element"), - }); + + Self::resolve(&pool_account, credit_in) + .map_err(|e| (e, Error::::Overflow.into()))?; + + // If this fails, credits are balanced, can return zero credit in error + let credit_out = Self::withdraw(&asset2, &pool_account, *asset2_amount, true) + .map_err(|e| (Credit::::Native(Default::default()), e))?; + + let reserve = match_err!( + Self::get_balance(&pool_account, &asset2), + (|e: Error| (credit_out, e.into())) + ); + let reserve_left = reserve.saturating_sub(*asset2_amount); + match_err!( + Self::validate_minimal_amount(reserve_left, &asset2), + (|_| (credit_out, Error::::ReserveLeftLessThanMinimal.into())) + ); + + let remaining = balance_path[2..].to_vec(); + Self::do_swap(credit_out, &remaining) } else { - return Err(Error::::InvalidPath.into()) + Err((credit_in, Error::::PathError.into())) } - Ok(()) } /// The account ID of the pool. @@ -980,7 +999,7 @@ pub mod pallet { /// Leading to an amount at the end of a `path`, get the required amounts in. pub(crate) fn get_amounts_in( amount_out: &T::AssetBalance, - path: &BoundedVec, + path: &Path, ) -> Result, DispatchError> { let mut amounts: Vec = vec![*amount_out]; @@ -1000,7 +1019,7 @@ pub mod pallet { /// Following an amount into a `path`, get the corresponding amounts out. pub(crate) fn get_amounts_out( amount_in: &T::AssetBalance, - path: &BoundedVec, + path: &Path, ) -> Result, DispatchError> { let mut amounts: Vec = vec![*amount_in]; @@ -1078,12 +1097,13 @@ pub mod pallet { ) -> Result> { let amount1 = T::HigherPrecisionBalance::from(*amount1); let amount2 = T::HigherPrecisionBalance::from(*amount2); + let mint_min_liquidity = T::HigherPrecisionBalance::from(T::MintMinLiquidity::get()); let result = amount1 .checked_mul(&amount2) .ok_or(Error::::Overflow)? .integer_sqrt() - .checked_sub(&T::MintMinLiquidity::get().into()) + .checked_sub(&mint_min_liquidity) .ok_or(Error::::InsufficientLiquidityMinted)?; result.try_into().map_err(|_| Error::::Overflow) @@ -1208,9 +1228,7 @@ pub mod pallet { } /// Ensure that a path is valid. - fn validate_swap_path( - path: &BoundedVec, - ) -> Result<(), DispatchError> { + pub(crate) fn validate_swap_path(path: &Path) -> Result<(), DispatchError> { ensure!(path.len() >= 2, Error::::InvalidPath); // validate all the pools in the path are unique @@ -1228,6 +1246,74 @@ pub mod pallet { Ok(()) } + /// Ensure that a balance path is valid. + pub(crate) fn validate_swap_balance_path( + balance_path: &BalancePath, + ) -> Result<(), DispatchError> { + ensure!(balance_path.len() >= 2, Error::::InvalidBalancePath); + + // validate all the pools in the path are unique + let mut pools = BoundedBTreeSet::, T::MaxSwapPathLength>::new(); + for assets_pair in balance_path.windows(2) { + if let [(asset1, _), (asset2, _)] = assets_pair { + let pool_id = Self::get_pool_id(asset1.clone(), asset2.clone()); + let new_element = + pools.try_insert(pool_id).map_err(|_| Error::::Overflow)?; + if !new_element { + return Err(Error::::NonUniquePath.into()) + } + } + } + Ok(()) + } + + pub(crate) fn balance_path_from_amount_in( + amount_in: T::AssetBalance, + path: Path, + ) -> Result, DispatchError> { + let mut path_with_ins: BalancePath = vec![]; + let mut first_amount: T::AssetBalance = amount_in; + + for assets_pair in path.windows(2) { + if let [asset1, asset2] = assets_pair { + path_with_ins.push((asset1.clone(), first_amount)); + let (reserve_in, reserve_out) = Self::get_reserves(asset1, asset2)?; + first_amount = Self::get_amount_out(&first_amount, &reserve_in, &reserve_out)?; + } + } + + match path.first() { + Some(asset) => path_with_ins.push((asset.clone(), first_amount)), + None => return Err(Error::::InvalidBalancePath.into()), + } + + Ok(path_with_ins) + } + + pub(crate) fn balance_path_from_amount_out( + amount_out: T::AssetBalance, + path: Path, + ) -> Result, DispatchError> { + let mut path_with_outs: BalancePath = vec![]; + let mut last_amount: T::AssetBalance = amount_out; + + for assets_pair in path.windows(2).rev() { + if let [asset1, asset2] = assets_pair { + path_with_outs.push((asset1.clone(), last_amount)); + let (reserve_in, reserve_out) = Self::get_reserves(asset1, asset2)?; + last_amount = Self::get_amount_in(&last_amount, &reserve_in, &reserve_out)?; + } + } + + match path.first() { + Some(asset) => path_with_outs.push((asset.clone(), last_amount)), + None => return Err(Error::::InvalidBalancePath.into()), + } + + path_with_outs.reverse(); + Ok(path_with_outs) + } + /// Returns the next pool asset id for benchmark purposes only. #[cfg(any(test, feature = "runtime-benchmarks"))] pub fn get_next_pool_asset_id() -> T::PoolAssetId { @@ -1238,50 +1324,6 @@ pub mod pallet { } } -impl Swap for Pallet { - fn swap_exact_tokens_for_tokens( - sender: T::AccountId, - path: Vec, - amount_in: T::HigherPrecisionBalance, - amount_out_min: Option, - send_to: T::AccountId, - keep_alive: bool, - ) -> Result { - let path = path.try_into().map_err(|_| Error::::PathError)?; - let amount_out_min = amount_out_min.map(Self::convert_hpb_to_asset_balance).transpose()?; - let amount_out = Self::do_swap_exact_tokens_for_tokens( - sender, - path, - Self::convert_hpb_to_asset_balance(amount_in)?, - amount_out_min, - send_to, - keep_alive, - )?; - Ok(amount_out.into()) - } - - fn swap_tokens_for_exact_tokens( - sender: T::AccountId, - path: Vec, - amount_out: T::HigherPrecisionBalance, - amount_in_max: Option, - send_to: T::AccountId, - keep_alive: bool, - ) -> Result { - let path = path.try_into().map_err(|_| Error::::PathError)?; - let amount_in_max = amount_in_max.map(Self::convert_hpb_to_asset_balance).transpose()?; - let amount_in = Self::do_swap_tokens_for_exact_tokens( - sender, - path, - Self::convert_hpb_to_asset_balance(amount_out)?, - amount_in_max, - send_to, - keep_alive, - )?; - Ok(amount_in.into()) - } -} - sp_api::decl_runtime_apis! { /// This runtime api allows people to query the size of the liquidity pools /// and quote prices for swaps. diff --git a/substrate/frame/asset-conversion/src/swap.rs b/substrate/frame/asset-conversion/src/swap.rs new file mode 100644 index 000000000000..8a6f323f8223 --- /dev/null +++ b/substrate/frame/asset-conversion/src/swap.rs @@ -0,0 +1,219 @@ +// This file is part of Substrate. + +// Copyright (C) 2023 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::{match_err, storage::with_transaction}; +use sp_runtime::TransactionOutcome; + +/// Trait for providing methods to swap between the various asset classes. +pub trait Swap { + /// Swap exactly `amount_in` of asset `path[0]` for asset `path[1]`. + /// If an `amount_out_min` is specified, it will return an error if it is unable to acquire + /// the amount desired. + /// + /// Withdraws the `path[0]` asset from `sender`, deposits the `path[1]` asset to `send_to`, + /// respecting `keep_alive`. + /// + /// If successful, returns the amount of `path[1]` acquired for the `amount_in`. + fn swap_exact_tokens_for_tokens( + sender: AccountId, + path: Vec, + amount_in: Balance, + amount_out_min: Option, + send_to: AccountId, + keep_alive: bool, + ) -> Result; + + /// Take the `path[0]` asset and swap some amount for `amount_out` of the `path[1]`. If an + /// `amount_in_max` is specified, it will return an error if acquiring `amount_out` would be + /// too costly. + /// + /// Withdraws `path[0]` asset from `sender`, deposits `path[1]` asset to `send_to`, + /// respecting `keep_alive`. + /// + /// If successful returns the amount of the `path[0]` taken to provide `path[1]`. + fn swap_tokens_for_exact_tokens( + sender: AccountId, + path: Vec, + amount_out: Balance, + amount_in_max: Option, + send_to: AccountId, + keep_alive: bool, + ) -> Result; +} + +/// TODO +pub trait SwapCredit { + /// TODO + type Balance; + + /// TODO + fn swap_exact_tokens_for_tokens( + path: Vec, + credit_in: Credit, + amount_out_min: Option, + ) -> Result; + + /// TODO + fn swap_tokens_for_exact_tokens( + path: Vec, + amount_out: Self::Balance, + credit_in: Credit, + ) -> Result<(Credit, Credit), (Credit, DispatchError)>; +} + +impl Swap for Pallet { + fn swap_exact_tokens_for_tokens( + sender: T::AccountId, + path: Vec, + amount_in: T::AssetBalance, + amount_out_min: Option, + send_to: T::AccountId, + keep_alive: bool, + ) -> Result { + let path = path.try_into().map_err(|_| Error::::PathError)?; + + let transaction = with_transaction(|| { + let swap = Self::do_swap_exact_tokens_for_tokens( + sender, + path, + amount_in, + amount_out_min, + send_to, + keep_alive, + ); + + match &swap { + Ok(_) => TransactionOutcome::Commit(swap), + _ => TransactionOutcome::Rollback(swap), + } + }); + + return match transaction { + Ok(out) => Ok(out), + Err(err) => Err(err), + } + } + + fn swap_tokens_for_exact_tokens( + sender: T::AccountId, + path: Vec, + amount_out: T::AssetBalance, + amount_in_max: Option, + send_to: T::AccountId, + keep_alive: bool, + ) -> Result { + let path = path.try_into().map_err(|_| Error::::PathError)?; + + let transaction = with_transaction(|| { + let swap = Self::do_swap_tokens_for_exact_tokens( + sender, + path, + amount_out, + amount_in_max, + send_to, + keep_alive, + ); + + match &swap { + Ok(_) => TransactionOutcome::Commit(swap), + _ => TransactionOutcome::Rollback(swap), + } + }); + + return match transaction { + Ok(out) => Ok(out), + Err(err) => Err(err), + } + } +} + +impl SwapCredit> for Pallet { + type Balance = T::AssetBalance; + + fn swap_exact_tokens_for_tokens( + path: Vec, + credit_in: Credit, + amount_out_min: Option, + ) -> Result, (Credit, DispatchError)> { + // TODO ready / implementation + + // TODO ready / wrap `with_transaction` + // TODO ready / swap + + Err((credit_in, Error::::PoolNotFound.into())) + } + + fn swap_tokens_for_exact_tokens( + path: Vec, + amount_out: Self::Balance, + credit_in: Credit, + ) -> Result<(Credit, Credit), (Credit, DispatchError)> { + ensure!(amount_out > Zero::zero(), (credit_in, Error::::ZeroAmount.into())); + ensure!(credit_in.peek() > Zero::zero(), (credit_in, Error::::ZeroAmount.into())); + + //let (credit_in, path) = + //path.try_into().map_with_prefix(credit_in, |_| Error::::PathError.into())?; + + // Alternative, using new match_err macro, no need to re-shadow credit_in variable + let path = match_err!(path.try_into(), (|_| (credit_in, Error::::PathError.into()))); + + match_err!(Self::validate_swap_path(&path), (|e| (credit_in, e))); + + let amounts = match_err!( + Self::get_amounts_in(&amount_out, &path), + (|_| (credit_in, Error::::PathError.into())) + ); + + let amount_in = + match_err!(amounts.first().ok_or(Error::::PathError.into()), (|e| (credit_in, e))); + + ensure!( + credit_in.peek() >= *amount_in, + (credit_in, Error::::ProvidedMaximumNotSufficientForSwap.into()) + ); + let (credit_in, credit_change) = credit_in.split(*amount_in); + + let balance_path = + match_err!(Self::balance_path_from_amount_out(amount_out, path), (|e| (credit_in, e))); + + match_err!(Self::validate_swap_balance_path(&balance_path), (|e| (credit_in, e))); + + // Note + // with_transaction forces Error: From, not present in (Credit, + // DispatchError) if we implement From for the tuple, then there exists an + // error tuple that can be returned with the credit on default (::zero()), specifically + // TransactionalError::LimitReached, which is a nested transactional level of 255. + // Temporary workaround is this mutable binding, there is probably a better workaround + let mut credit_error: Credit = Credit::::Native(Default::default()); + + let transaction = with_transaction(|| match Self::do_swap(credit_in, &balance_path) { + Ok(swap) => TransactionOutcome::Commit(Ok(swap)), + Err(err) => { + let (credit, error) = err; + + credit_error = credit; + TransactionOutcome::Rollback(Err(error)) + }, + }); + + return match transaction { + Ok(out) => Ok((out, credit_change)), + Err(err) => Err((credit_error, err)), + } + } +} diff --git a/substrate/frame/asset-conversion/src/types.rs b/substrate/frame/asset-conversion/src/types.rs index ffdc63ce0ce7..8fcdee69b4d6 100644 --- a/substrate/frame/asset-conversion/src/types.rs +++ b/substrate/frame/asset-conversion/src/types.rs @@ -19,6 +19,7 @@ use super::*; use codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; +use sp_runtime::BoundedVec; use sp_std::{cmp::Ordering, marker::PhantomData}; /// Pool ID. @@ -27,6 +28,14 @@ use sp_std::{cmp::Ordering, marker::PhantomData}; /// migration. pub(super) type PoolIdOf = (::MultiAssetId, ::MultiAssetId); +/// TODO +pub type Path = BoundedVec<::MultiAssetId, ::MaxSwapPathLength>; + +/// TODO +/// +/// Example: [(asset1, amount_in), (asset2, amount_out), (asset2, amount_out), (asset3, amount_out)] +pub(super) type BalancePath = Vec<(::MultiAssetId, ::AssetBalance)>; + /// Stores the lp_token asset id a particular pool has been assigned. #[derive(Decode, Encode, Default, PartialEq, Eq, MaxEncodedLen, TypeInfo)] pub struct PoolInfo { @@ -83,43 +92,6 @@ where } } -/// Trait for providing methods to swap between the various asset classes. -pub trait Swap { - /// Swap exactly `amount_in` of asset `path[0]` for asset `path[1]`. - /// If an `amount_out_min` is specified, it will return an error if it is unable to acquire - /// the amount desired. - /// - /// Withdraws the `path[0]` asset from `sender`, deposits the `path[1]` asset to `send_to`, - /// respecting `keep_alive`. - /// - /// If successful, returns the amount of `path[1]` acquired for the `amount_in`. - fn swap_exact_tokens_for_tokens( - sender: AccountId, - path: Vec, - amount_in: Balance, - amount_out_min: Option, - send_to: AccountId, - keep_alive: bool, - ) -> Result; - - /// Take the `path[0]` asset and swap some amount for `amount_out` of the `path[1]`. If an - /// `amount_in_max` is specified, it will return an error if acquiring `amount_out` would be - /// too costly. - /// - /// Withdraws `path[0]` asset from `sender`, deposits `path[1]` asset to `send_to`, - /// respecting `keep_alive`. - /// - /// If successful returns the amount of the `path[0]` taken to provide `path[1]`. - fn swap_tokens_for_exact_tokens( - sender: AccountId, - path: Vec, - amount_out: Balance, - amount_in_max: Option, - send_to: AccountId, - keep_alive: bool, - ) -> Result; -} - /// An implementation of MultiAssetId that can be either Native or an asset. #[derive(Decode, Encode, Default, MaxEncodedLen, TypeInfo, Clone, Copy, Debug)] pub enum NativeOrAssetId diff --git a/substrate/frame/support/src/traits.rs b/substrate/frame/support/src/traits.rs index 40e348b5e373..c016d54efab7 100644 --- a/substrate/frame/support/src/traits.rs +++ b/substrate/frame/support/src/traits.rs @@ -59,8 +59,8 @@ pub use misc::{ ConstU16, ConstU32, ConstU64, ConstU8, DefensiveMax, DefensiveMin, DefensiveSaturating, DefensiveTruncateFrom, EnsureInherentsAreFirst, EqualPrivilegeOnly, EstimateCallFee, ExecuteBlock, ExtrinsicCall, Get, GetBacking, GetDefault, HandleLifetime, IsSubType, IsType, - Len, OffchainWorker, OnKilledAccount, OnNewAccount, PrivilegeCmp, SameOrOther, Time, - TryCollect, TryDrop, TypedGet, UnixTime, WrapperKeepOpaque, WrapperOpaque, + Len, OffchainWorker, OnKilledAccount, OnNewAccount, PrefixedResult, PrivilegeCmp, SameOrOther, + Time, TryCollect, TryDrop, TypedGet, UnixTime, WrapperKeepOpaque, WrapperOpaque, }; #[allow(deprecated)] pub use misc::{PreimageProvider, PreimageRecipient}; diff --git a/substrate/frame/support/src/traits/misc.rs b/substrate/frame/support/src/traits/misc.rs index eb704de4353c..d213c12e430f 100644 --- a/substrate/frame/support/src/traits/misc.rs +++ b/substrate/frame/support/src/traits/misc.rs @@ -1171,6 +1171,113 @@ pub trait AccountTouch { fn touch(asset: AssetId, who: AccountId, depositor: AccountId) -> DispatchResult; } +/// Provides a way to prefix both `Ok` and `Err` variants of a `Result`. +/// +/// This trait allows you to attach a prefix to both the `Ok` and `Err` variants +/// of a `Result`, making it easier to track additional contextual information +/// alongside the original result. It is especially handy when `P` does not implement +/// `Clone` and `Copy`. +pub trait PrefixedResult { + /// Maps this `Result` to a new `Result` with a prefixed value. + /// + /// Takes a value of any type `P` as a prefix and an `FnOnce` closure `O` + /// for mapping the `Err` variant to a new type `F`. + /// + /// This is particularly useful when `P` does not implement `Clone` or `Copy`. + fn map_with_prefix F>(self, p: P, o: O) -> Result<(P, T), (P, F)>; +} + +impl PrefixedResult for Result { + /// Implements the `map_with_prefix` function for the standard `Result` type. + /// + /// This allows the `Result` to be mapped to another `Result` where both `Ok` and + /// `Err` variants are prefixed with a value of type `P`. The original `Err` type + /// is mapped to a new type `F` using the provided `FnOnce` closure `O`. + /// + /// The use of this implementation is particularly beneficial when the prefix type `P` + /// does not implement `Clone` and `Copy`. + /// + /// # Examples + /// + /// ``` + /// use frame_support::traits::PrefixedResult; + /// + /// let x: Result = Ok(42); + /// let y = x.map_with_prefix("prefix", |e| e); + /// + /// assert_eq!(y, Ok(("prefix", 42))); + /// + /// let x: Result = Err("error"); + /// let y = x.map_with_prefix("prefix", |e| e); + /// + /// assert_eq!(y, Err(("prefix", "error"))); + /// ``` + fn map_with_prefix F>(self, p: P, o: O) -> Result<(P, T), (P, F)> { + match self { + Ok(t) => Ok((p, t)), + Err(e) => Err((p, o(e))), + } + } +} + +/// Macro alternative to map_err() closure. +/// +/// This macro instead, does not move captured error values when the matched result is not an Error. +/// Allows to use any variables passed to error after this statement, without cloning or copying +/// them. +/// +/// # Examples +/// +/// ``` +/// type Error = (i32, &'static str); +/// +/// fn vec_first(v: Vec) -> Result { +/// let n = v.first().ok_or("empty vector")?; +/// Ok(n.clone()) +/// } +/// +/// fn main() -> Result<(), Error> { +/// let v = vec![1, 2, 3]; +/// let context_data = 10; +/// let num = frame_support::match_err!(vec_first(v), (|err| (context_data, err))); +/// // context_data usage is allowed here, it was not moved +/// let num2 = num + context_data; +/// Ok(()) +/// } +/// ``` +/// +/// Above example expands to: +/// +/// ``` +/// type Error = (i32, &'static str); +/// +/// fn vec_first(v: Vec) -> Result { +/// let n = v.first().ok_or("empty vector")?; +/// Ok(n.clone()) +/// } +/// +/// fn main() -> Result<(), Error> { +/// let v = vec![1, 2, 3]; +/// let context_data = 10; +/// let num = match vec_first(v) { +/// Ok(out) => out, +/// Err(err) => return Err((context_data, err)), +/// }; +/// // context_data usage is allowed here, it was not moved +/// let num2 = num + context_data; +/// Ok(()) +/// } +/// ``` +#[macro_export] +macro_rules! match_err { + ($matched: expr, $err_closure: tt) => { + match $matched { + Ok(out) => out, + Err(err) => return Err($err_closure(err)), + } + }; +} + #[cfg(test)] mod test { use super::*;