1use cosmwasm_std::{
13 to_json_binary, Addr, Decimal, Deps, QuerierWrapper, QueryRequest, StdError, Uint128, WasmQuery,
14};
15use cw_asset::{Asset, AssetInfo};
16use schemars::JsonSchema;
17use serde::{Deserialize, Serialize};
18
19use super::{
20 ans_host::AnsHost, AnsEntryConvertor, AssetEntry, DexAssetPairing, PoolAddress, PoolReference,
21};
22use crate::{error::AbstractError, AbstractResult};
23
24#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
28pub struct AssetConversion {
29 into: AssetInfo,
30 ratio: Decimal,
31}
32
33impl AssetConversion {
34 pub fn new(asset: impl Into<AssetInfo>, price: Decimal) -> Self {
35 Self {
36 into: asset.into(),
37 ratio: price,
38 }
39 }
40 pub fn convert(rates: &[Self], amount: Uint128) -> Vec<Asset> {
42 rates
43 .iter()
44 .map(|rate| Asset::new(rate.into.clone(), amount * rate.ratio))
45 .collect()
46 }
47}
48
49#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
51#[non_exhaustive]
52pub enum UncheckedPriceSource {
53 Pair(DexAssetPairing),
56 LiquidityToken {},
58 ValueAs {
61 asset: AssetEntry,
62 multiplier: Decimal,
63 },
64 None,
65}
66
67impl UncheckedPriceSource {
68 pub fn check(
69 self,
70 deps: Deps,
71 ans_host: &AnsHost,
72 entry: &AssetEntry,
73 ) -> AbstractResult<PriceSource> {
74 match self {
75 UncheckedPriceSource::Pair(pair_info) => {
76 let PoolReference {
77 pool_address,
78 unique_id,
79 } = ans_host
80 .query_asset_pairing(&deps.querier, &pair_info)?
81 .pop()
82 .unwrap();
83 let pool_assets = ans_host
84 .query_pool_metadata(&deps.querier, unique_id)?
85 .assets;
86 let assets = ans_host.query_assets(&deps.querier, &pool_assets)?;
87 assert_eq!(assets.len(), 2);
89 pool_address.expect_contract()?;
91 Ok(PriceSource::Pool {
92 address: pool_address,
93 pair: assets,
94 })
95 }
96 UncheckedPriceSource::LiquidityToken {} => {
97 let lp_token = AnsEntryConvertor::new(entry.clone()).lp_token()?;
98 let pairing = AnsEntryConvertor::new(lp_token.clone()).dex_asset_pairing()?;
99
100 let pool_assets = ans_host.query_assets(&deps.querier, &lp_token.assets)?;
101 let pool_address = ans_host
104 .query_asset_pairing(&deps.querier, &pairing)?
105 .pop()
106 .unwrap()
107 .pool_address;
108 Ok(PriceSource::LiquidityToken {
109 pool_assets,
110 pool_address,
111 })
112 }
113 UncheckedPriceSource::ValueAs { asset, multiplier } => {
114 let asset_info = ans_host.query_asset(&deps.querier, &asset)?;
115 Ok(PriceSource::ValueAs {
116 asset: asset_info,
117 multiplier,
118 })
119 }
120 UncheckedPriceSource::None => Ok(PriceSource::None),
121 }
122 }
123}
124
125#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, JsonSchema)]
127#[non_exhaustive]
128pub enum PriceSource {
129 None,
131 Pool {
134 address: PoolAddress,
135 pair: Vec<AssetInfo>,
137 },
138 LiquidityToken {
140 pool_assets: Vec<AssetInfo>,
141 pool_address: PoolAddress,
142 },
143 ValueAs {
145 asset: AssetInfo,
146 multiplier: Decimal,
147 },
148}
149
150impl PriceSource {
151 pub fn dependencies(&self, asset: &AssetInfo) -> Vec<AssetInfo> {
154 match self {
155 PriceSource::Pool { pair, .. } => {
157 pair.iter().filter(|a| *a != asset).cloned().collect()
158 }
159 PriceSource::LiquidityToken { pool_assets, .. } => pool_assets.clone(),
160 PriceSource::ValueAs { asset, .. } => vec![asset.clone()],
161 PriceSource::None => vec![],
162 }
163 }
164
165 pub fn conversion_rates(
167 &self,
168 deps: Deps,
169 asset: &AssetInfo,
170 ) -> AbstractResult<Vec<AssetConversion>> {
171 match self {
174 PriceSource::Pool { address, pair } => self
176 .trade_pair_price(deps, asset, &address.expect_contract()?, pair)
177 .map(|e| vec![e]),
178 PriceSource::LiquidityToken {
180 pool_address,
181 pool_assets,
182 } => self.lp_conversion(deps, asset, &pool_address.expect_contract()?, pool_assets),
183 PriceSource::ValueAs { asset, multiplier } => {
185 Ok(vec![AssetConversion::new(asset.clone(), *multiplier)])
186 }
187 PriceSource::None => Ok(vec![]),
189 }
190 }
191
192 fn trade_pair_price(
194 &self,
195 deps: Deps,
196 priced_asset: &AssetInfo,
197 address: &Addr,
198 pair: &[AssetInfo],
199 ) -> AbstractResult<AssetConversion> {
200 let other_asset_info = pair.iter().find(|a| a != &priced_asset).unwrap();
201 let pool_info = (
203 other_asset_info.query_balance(&deps.querier, address)?,
204 priced_asset.query_balance(&deps.querier, address)?,
205 );
206 let ratio = Decimal::from_ratio(pool_info.0.u128(), pool_info.1.u128());
208 Ok(AssetConversion::new(other_asset_info.clone(), ratio))
211 }
212
213 fn lp_conversion(
217 &self,
218 deps: Deps,
219 lp_asset: &AssetInfo,
220 pool_addr: &Addr,
221 pool_assets: &[AssetInfo],
222 ) -> AbstractResult<Vec<AssetConversion>> {
223 let supply: Uint128;
224 if let AssetInfo::Cw20(addr) = lp_asset {
225 supply = query_cw20_supply(&deps.querier, addr)?;
226 } else {
227 return Err(StdError::generic_err("Can't have a native LP token").into());
228 }
229 pool_assets
230 .iter()
231 .map(|asset| {
232 let pool_balance = asset
233 .query_balance(&deps.querier, pool_addr.clone())
234 .map_err(AbstractError::from)?;
235 Ok(AssetConversion::new(
236 asset.clone(),
237 Decimal::from_ratio(pool_balance.u128(), supply.u128()),
238 ))
239 })
240 .collect::<AbstractResult<Vec<AssetConversion>>>()
241 }
242}
243
244fn query_cw20_supply(querier: &QuerierWrapper, contract_addr: &Addr) -> AbstractResult<Uint128> {
245 let response: cw20::TokenInfoResponse =
246 querier.query(&QueryRequest::Wasm(WasmQuery::Smart {
247 contract_addr: contract_addr.into(),
248 msg: to_json_binary(&cw20::Cw20QueryMsg::TokenInfo {})?,
249 }))?;
250 Ok(response.total_supply)
251}
252
253#[cfg(test)]
254mod tests {
255 use abstract_testing::prelude::*;
256 use cosmwasm_std::testing::mock_dependencies;
257 use speculoos::prelude::*;
258
259 use super::*;
260
261 mod check {
263 use cosmwasm_std::testing::mock_dependencies;
264
265 use super::*;
266 use crate::{
267 ans_host,
268 objects::{ans_host::AnsHostError, pool_id::PoolAddressBase},
269 };
270
271 #[test]
272 fn liquidity_token() -> AbstractResult<()> {
273 let mut deps = mock_dependencies();
274 deps.querier = MockQuerierBuilder::default()
276 .with_contract_map_entries(
277 TEST_ANS_HOST,
278 ans_host::state::ASSET_ADDRESSES,
279 vec![
280 (
281 &AssetEntry::from(TEST_ASSET_1),
282 AssetInfo::native(TEST_ASSET_1),
283 ),
284 (
285 &AssetEntry::from(TEST_ASSET_2),
286 AssetInfo::native(TEST_ASSET_2),
287 ),
288 ],
289 )
290 .with_contract_map_entries(
291 TEST_ANS_HOST,
292 ans_host::state::ASSET_PAIRINGS,
293 vec![(
294 &DexAssetPairing::new(TEST_ASSET_1.into(), TEST_ASSET_2.into(), TEST_DEX),
295 vec![PoolReference::new(
296 TEST_UNIQUE_ID.into(),
297 PoolAddressBase::Contract(Addr::unchecked(TEST_POOL_ADDR)),
298 )],
299 )],
300 )
301 .build();
302
303 let price_source = UncheckedPriceSource::LiquidityToken {};
304
305 let actual_source_res = price_source.check(
306 deps.as_ref(),
307 &AnsHost::new(Addr::unchecked(TEST_ANS_HOST)),
308 &AssetEntry::new(TEST_LP_TOKEN_NAME),
309 );
310
311 assert_that!(actual_source_res)
312 .is_ok()
313 .is_equal_to(PriceSource::LiquidityToken {
314 pool_address: PoolAddress::contract(Addr::unchecked(TEST_POOL_ADDR)),
315 pool_assets: vec![
316 AssetInfo::native(TEST_ASSET_1),
317 AssetInfo::native(TEST_ASSET_2),
318 ],
319 });
320 Ok(())
321 }
322
323 #[test]
324 fn liquidity_token_missing_asset() -> AbstractResult<()> {
325 let mut deps = mock_dependencies();
326 deps.querier = MockQuerierBuilder::default()
327 .with_contract_map_key(
328 TEST_ANS_HOST,
329 ans_host::state::ASSET_ADDRESSES,
330 &TEST_ASSET_1.into(),
331 )
332 .with_contract_map_key(
333 TEST_ANS_HOST,
334 ans_host::state::ASSET_ADDRESSES,
335 &TEST_ASSET_2.into(),
336 )
337 .with_contract_map_entries(TEST_ANS_HOST, ans_host::state::ASSET_PAIRINGS, vec![])
338 .build();
339
340 let price_source = UncheckedPriceSource::LiquidityToken {};
341
342 let actual_source_res = price_source.check(
343 deps.as_ref(),
344 &AnsHost::new(Addr::unchecked(TEST_ANS_HOST)),
345 &AssetEntry::new(TEST_LP_TOKEN_NAME),
346 );
347
348 assert_that!(actual_source_res)
349 .is_err()
350 .is_equal_to(AbstractError::AnsHostError(AnsHostError::AssetNotFound {
351 asset: AssetEntry::new(TEST_ASSET_1),
352 ans_host: Addr::unchecked(TEST_ANS_HOST),
353 }));
354 Ok(())
355 }
356 }
357
358 mod lp_conversion {
359 use super::*;
360
361 #[test]
362 fn fail_with_native_token() -> AbstractResult<()> {
363 let deps = mock_dependencies();
364 let price_source = PriceSource::LiquidityToken {
365 pool_address: PoolAddress::contract(Addr::unchecked(TEST_POOL_ADDR)),
366 pool_assets: vec![AssetInfo::native(TEST_ASSET_1)],
367 };
368 let actual_res = price_source.lp_conversion(
369 deps.as_ref(),
370 &AssetInfo::native("aoeu"),
371 &Addr::unchecked(TEST_POOL_ADDR),
372 &[],
373 );
374 assert_that!(actual_res)
375 .is_err()
376 .is_equal_to(AbstractError::Std(StdError::generic_err(
377 "Can't have a native LP token",
378 )));
379 Ok(())
380 }
381
382 #[test]
383 fn gets_cw20_supply() -> AbstractResult<()> {
384 let mut deps = mock_dependencies();
385 deps.querier = MockQuerierBuilder::default()
386 .with_contract_item(
387 TEST_LP_TOKEN_ADDR,
388 cw20_base::state::TOKEN_INFO,
389 &cw20_base::state::TokenInfo {
390 name: "test".to_string(),
391 symbol: "test".to_string(),
392 decimals: 0,
393 total_supply: Uint128::from(100u128),
394 mint: None,
395 },
396 )
397 .with_smart_handler(TEST_LP_TOKEN_ADDR, |msg| {
398 let res = match from_json::<cw20::Cw20QueryMsg>(msg).unwrap() {
399 cw20::Cw20QueryMsg::TokenInfo {} => cw20::TokenInfoResponse {
400 name: "test".to_string(),
401 symbol: "test".to_string(),
402 decimals: 0,
403 total_supply: Uint128::from(100u128),
404 },
405 _ => panic!("unexpected message"),
406 };
407
408 Ok(to_json_binary(&res).unwrap())
409 })
410 .build();
411
412 let target_asset = AssetInfo::native(TEST_ASSET_1);
413 let price_source = PriceSource::LiquidityToken {
414 pool_address: PoolAddress::contract(Addr::unchecked(TEST_POOL_ADDR)),
415 pool_assets: vec![target_asset.clone()],
416 };
417 let actual_res = price_source.lp_conversion(
418 deps.as_ref(),
419 &AssetInfo::cw20(Addr::unchecked(TEST_LP_TOKEN_ADDR)),
420 &Addr::unchecked(TEST_POOL_ADDR),
421 &[target_asset.clone()],
422 )?;
423
424 assert_that!(actual_res).has_length(1);
425 assert_that!(actual_res[0]).is_equal_to(AssetConversion {
426 into: target_asset,
427 ratio: Decimal::zero(),
428 });
429
430 Ok(())
431 }
432 }
433}