From 0f8e122afb91de469c179bfec5297e3733d75c5e Mon Sep 17 00:00:00 2001 From: William Wolff Date: Fri, 10 Mar 2023 12:50:11 +0100 Subject: [PATCH] feat: add support for liquidity from muesliswap liquidity pools feat: add pool_id field for sundaeswap liquidity pools; required for order tx construction; adjusted readme fix: move get_asset_count into utils + tests --- .../liquidity_by_token_pair/README.md | 18 ++- .../liquidity_by_token_pair/minswap.rs | 21 +++- src/reducers/liquidity_by_token_pair/mod.rs | 43 ++----- src/reducers/liquidity_by_token_pair/model.rs | 10 +- .../liquidity_by_token_pair/muesliswap.rs | 44 +++++++ .../liquidity_by_token_pair/sundaeswap.rs | 8 +- src/reducers/liquidity_by_token_pair/utils.rs | 108 ++++++++++++++++-- .../liquidity_by_token_pair/wingriders.rs | 8 +- .../mainnet/daemon.toml | 10 ++ .../preprod/daemon.toml | 1 + 10 files changed, 211 insertions(+), 60 deletions(-) create mode 100644 src/reducers/liquidity_by_token_pair/muesliswap.rs diff --git a/src/reducers/liquidity_by_token_pair/README.md b/src/reducers/liquidity_by_token_pair/README.md index 39bc3043..debde73b 100644 --- a/src/reducers/liquidity_by_token_pair/README.md +++ b/src/reducers/liquidity_by_token_pair/README.md @@ -5,10 +5,13 @@ This reducer intends to aggregate changes across different AMM DEXs (decentralized exchanges). It currently supports the most popular ones which includes: - MinSwap +- Muesliswap - SundaeSwap - Wingriders -Note, that Muesliswap is not a pure AMM DEX and therefore, its liquidity for different token pairs cannot be indexed in a similar way. +### Note + +> Muesliswap is considered a hybrid DEX, offering orderbook liquidity and liquidity via pool in form of an AMM DEX. This reducer currently only observes its liquidity pools. ## Configuration @@ -37,16 +40,17 @@ Since ADA has an empty currency symbol and token name a corresponding Redis key Example ADA/WRT liquidity key with `pool` prefix: https://preprod.cardanoscan.io/token/659ab0b5658687c2e74cd10dba8244015b713bf503b90557769d77a757696e67526964657273 -`pool.659ab0b5658687c2e74cd10dba8244015b713bf503b90557769d77a.757696e67526964657273` +`pool.659ab0b5658687c2e74cd10dba8244015b713bf503b90557769d77a7.57696e67526964657273` ### Redis Value Schema -The reducer's value is a set. Each entry is a single liquidity source that is json encoded. A single member can contain up to four fields: +The reducer's value is a set. Each entry is a single liquidity source that is json encoded. A single member can contain up to five fields: - dex specific prefix to identify the origin of the liquidity source - amount of token a - amount of token b -- a decimal number defining the fee of the liquidity source that's paid to liquidity providers +- a decimal number defining the fee of the liquidity source that's paid to liquidity providers _(optional)_ +- a pool_id encoded base16 \*(optional)\_ only available for Sundaeswap liquidity pools Below you can find the general schema for a JSON encoded member: @@ -55,7 +59,8 @@ Below you can find the general schema for a JSON encoded member: "token_a": string, "token_b": string, "dex": string, - "fee": number + "fee": number, + "pool_id": string } ``` @@ -66,7 +71,8 @@ Example ADA/MIN liquidity source from SundaeSwap DEX: "token_a": "31249392392", "token_b": "1323123231221", "dex": "sun", - "fee": 0.003 + "fee": 0.003, + "pool_id": "2d01" } ``` diff --git a/src/reducers/liquidity_by_token_pair/minswap.rs b/src/reducers/liquidity_by_token_pair/minswap.rs index b3b29d5c..fa3bab33 100644 --- a/src/reducers/liquidity_by_token_pair/minswap.rs +++ b/src/reducers/liquidity_by_token_pair/minswap.rs @@ -1,6 +1,23 @@ -use super::model::TokenPair; +use pallas::ledger::primitives::babbage::PlutusData; -pub type MinSwapPoolDatum = TokenPair; +use super::model::{PoolAsset, TokenPair}; + +pub struct MinSwapPoolDatum { + pub a: PoolAsset, + pub b: PoolAsset, +} + +impl TryFrom<&PlutusData> for MinSwapPoolDatum { + type Error = (); + + fn try_from(value: &PlutusData) -> Result { + if let Some(TokenPair { a, b }) = TokenPair::try_from(value).ok() { + return Ok(Self { a, b }); + } + + Err(()) + } +} #[cfg(test)] mod test { diff --git a/src/reducers/liquidity_by_token_pair/mod.rs b/src/reducers/liquidity_by_token_pair/mod.rs index 413f0f36..15a1dccf 100644 --- a/src/reducers/liquidity_by_token_pair/mod.rs +++ b/src/reducers/liquidity_by_token_pair/mod.rs @@ -1,4 +1,3 @@ -use lazy_static::__Deref; use pallas::ledger::{ primitives::babbage::PlutusData, traverse::{Asset, MultiEraBlock, MultiEraOutput, MultiEraTx}, @@ -7,6 +6,7 @@ use serde::Deserialize; pub mod minswap; pub mod model; +pub mod muesliswap; pub mod sundaeswap; pub mod utils; pub mod wingriders; @@ -14,9 +14,11 @@ pub mod wingriders; use crate::{crosscut, prelude::*}; use self::{ - model::{LiquidityPoolDatum, PoolAsset, TokenPair}, + minswap::MinSwapPoolDatum, + model::{LiquidityPoolDatum, TokenPair}, + muesliswap::MuesliSwapPoolDatum, sundaeswap::SundaePoolDatum, - utils::{build_key_value_pair, contains_currency_symbol, resolve_datum}, + utils::{build_key_value_pair, contains_currency_symbol, get_asset_amount, resolve_datum}, wingriders::WingriderPoolDatum, }; @@ -32,34 +34,6 @@ pub struct Reducer { policy: crosscut::policies::RuntimePolicy, } -fn get_asset_amount(asset: &PoolAsset, assets: &Vec) -> Option { - match asset { - PoolAsset::Ada => { - for asset in assets { - if let Asset::Ada(lovelace_amount) = asset { - return Some(*lovelace_amount); - } - } - } - PoolAsset::AssetClass(matched_currency_symbol_hash, matched_token_name_bytes) => { - let currency_symbol: String = - hex::encode(matched_currency_symbol_hash.deref().to_vec()); - let token_name: String = hex::encode(matched_token_name_bytes.deref()); - for asset in assets { - if let Asset::NativeAsset(currency_symbol_hash, token_name_vector, amount) = asset { - if hex::encode(currency_symbol_hash.deref()).eq(¤cy_symbol) - && hex::encode(token_name_vector).eq(&token_name) - { - return Some(*amount); - } - } - } - } - } - - None -} - impl Reducer { fn get_key_value_pair( &self, @@ -76,7 +50,8 @@ impl Reducer { let pool_datum = LiquidityPoolDatum::try_from(&plutus_data)?; let assets: Vec = utxo.assets(); match pool_datum { - LiquidityPoolDatum::Minswap(TokenPair { a, b }) + LiquidityPoolDatum::MuesliSwapPoolDatum(MuesliSwapPoolDatum { a, b }) + | LiquidityPoolDatum::Minswap(MinSwapPoolDatum { a, b }) | LiquidityPoolDatum::Wingriders(WingriderPoolDatum { a, b }) => { let a_amount_opt: Option = get_asset_amount(&a, &assets); let b_amount_opt: Option = get_asset_amount(&b, &assets); @@ -86,10 +61,11 @@ impl Reducer { a_amount_opt, b_amount_opt, None, + None, ) .ok_or(()); } - LiquidityPoolDatum::Sundaeswap(SundaePoolDatum { a, b, fee }) => { + LiquidityPoolDatum::Sundaeswap(SundaePoolDatum { a, b, fee, pool_id }) => { let a_amount_opt: Option = get_asset_amount(&a, &assets); let b_amount_opt: Option = get_asset_amount(&b, &assets); return build_key_value_pair( @@ -98,6 +74,7 @@ impl Reducer { a_amount_opt, b_amount_opt, Some(fee), + Some(pool_id), ) .ok_or(()); } diff --git a/src/reducers/liquidity_by_token_pair/model.rs b/src/reducers/liquidity_by_token_pair/model.rs index 8aeb828b..5fcee493 100644 --- a/src/reducers/liquidity_by_token_pair/model.rs +++ b/src/reducers/liquidity_by_token_pair/model.rs @@ -6,10 +6,12 @@ use pallas::{ use std::{fmt, str::FromStr}; use super::{ - minswap::MinSwapPoolDatum, sundaeswap::SundaePoolDatum, wingriders::WingriderPoolDatum, + minswap::MinSwapPoolDatum, muesliswap::MuesliSwapPoolDatum, sundaeswap::SundaePoolDatum, + wingriders::WingriderPoolDatum, }; pub enum LiquidityPoolDatum { + MuesliSwapPoolDatum(MuesliSwapPoolDatum), Minswap(MinSwapPoolDatum), Sundaeswap(SundaePoolDatum), Wingriders(WingriderPoolDatum), @@ -19,7 +21,11 @@ impl TryFrom<&PlutusData> for LiquidityPoolDatum { type Error = (); fn try_from(value: &PlutusData) -> Result { - if let Some(minswap_token_pair) = MinSwapPoolDatum::try_from(value).ok() { + if let Some(muesliswap_token_pair) = MuesliSwapPoolDatum::try_from(value).ok() { + return Ok(LiquidityPoolDatum::MuesliSwapPoolDatum( + muesliswap_token_pair, + )); + } else if let Some(minswap_token_pair) = MinSwapPoolDatum::try_from(value).ok() { return Ok(LiquidityPoolDatum::Minswap(minswap_token_pair)); } else if let Some(sundae_token_pair) = SundaePoolDatum::try_from(value).ok() { return Ok(LiquidityPoolDatum::Sundaeswap(sundae_token_pair)); diff --git a/src/reducers/liquidity_by_token_pair/muesliswap.rs b/src/reducers/liquidity_by_token_pair/muesliswap.rs new file mode 100644 index 00000000..0b7a30fe --- /dev/null +++ b/src/reducers/liquidity_by_token_pair/muesliswap.rs @@ -0,0 +1,44 @@ +use pallas::ledger::primitives::babbage::PlutusData; + +use super::model::{PoolAsset, TokenPair}; + +pub struct MuesliSwapPoolDatum { + pub a: PoolAsset, + pub b: PoolAsset, +} + +impl TryFrom<&PlutusData> for MuesliSwapPoolDatum { + type Error = (); + + fn try_from(value: &PlutusData) -> Result { + if let Some(TokenPair { a, b }) = TokenPair::try_from(value).ok() { + return Ok(Self { a, b }); + } + + Err(()) + } +} + +#[cfg(test)] +mod test { + use pallas::ledger::primitives::{babbage::PlutusData, Fragment}; + + use crate::reducers::liquidity_by_token_pair::{ + model::PoolAsset, muesliswap::MuesliSwapPoolDatum, utils::pool_asset_from, + }; + + #[test] + fn test_decoding_pool_datum_ada_min() { + let hex_pool_datum = "d8799fd8799f4040ffd8799f581c29d222ce763455e3d7a09a665ce554f00ac89d2e99a1a83d267170c6434d494eff1a9041264e181eff"; + let data = hex::decode(hex_pool_datum).unwrap(); + let plutus_data = PlutusData::decode_fragment(&data).unwrap(); + let pool_datum = MuesliSwapPoolDatum::try_from(&plutus_data).unwrap(); + assert_eq!(PoolAsset::Ada, pool_datum.a); + let minswap_token = pool_asset_from( + &String::from("29d222ce763455e3d7a09a665ce554f00ac89d2e99a1a83d267170c6"), + &String::from("4d494e"), + ) + .unwrap(); + assert_eq!(minswap_token, pool_datum.b); + } +} diff --git a/src/reducers/liquidity_by_token_pair/sundaeswap.rs b/src/reducers/liquidity_by_token_pair/sundaeswap.rs index 58a61b10..f25a0a4c 100644 --- a/src/reducers/liquidity_by_token_pair/sundaeswap.rs +++ b/src/reducers/liquidity_by_token_pair/sundaeswap.rs @@ -7,6 +7,7 @@ pub struct SundaePoolDatum { pub a: PoolAsset, pub b: PoolAsset, pub fee: f64, + pub pool_id: String, } impl TryFrom<&PlutusData> for SundaePoolDatum { @@ -17,7 +18,11 @@ impl TryFrom<&PlutusData> for SundaePoolDatum { let token_pair_pd = pd.fields.get(0).ok_or(())?; let token_pair = TokenPair::try_from(token_pair_pd)?; - if let Some(PlutusData::Constr(fee_pd)) = pd.fields.get(3) { + if let ( + Some(PlutusData::BoundedBytes(pool_id_bytes)), + Some(PlutusData::Constr(fee_pd)), + ) = (pd.fields.get(1), pd.fields.get(3)) + { return match (fee_pd.fields.get(0), fee_pd.fields.get(1)) { ( Some(PlutusData::BigInt(pallas::ledger::primitives::babbage::BigInt::Int( @@ -33,6 +38,7 @@ impl TryFrom<&PlutusData> for SundaePoolDatum { a: token_pair.a, b: token_pair.b, fee: (n as f64) / (d as f64), + pool_id: hex::encode(pool_id_bytes.clone().to_vec()), }) } _ => Err(()), diff --git a/src/reducers/liquidity_by_token_pair/utils.rs b/src/reducers/liquidity_by_token_pair/utils.rs index 4ec69e2c..56917fc8 100644 --- a/src/reducers/liquidity_by_token_pair/utils.rs +++ b/src/reducers/liquidity_by_token_pair/utils.rs @@ -1,3 +1,4 @@ +use lazy_static::__Deref; use pallas::{ codec::utils::CborWrap, ledger::{ @@ -22,7 +23,7 @@ pub fn contains_currency_symbol(currency_symbol: &String, assets: &Vec) - pub fn pool_asset_from(hex_currency_symbol: &String, hex_asset_name: &String) -> Option { if hex_currency_symbol.len() == 0 && hex_asset_name.len() == 0 { - Some(PoolAsset::Ada); + return Some(PoolAsset::Ada); } if let (Some(pid), Some(tkn)) = ( @@ -30,7 +31,7 @@ pub fn pool_asset_from(hex_currency_symbol: &String, hex_asset_name: &String) -> hex::decode(hex_asset_name).ok(), ) { if let Some(cs) = currency_symbol_from(&pid) { - Some(PoolAsset::AssetClass(cs, AssetName::from(tkn))); + return Some(PoolAsset::AssetClass(cs, AssetName::from(tkn))); } } @@ -58,6 +59,7 @@ pub fn serialize_value( a_amount_opt: Option, b_amount_opt: Option, fee_opt: Option, + pool_id_opt: Option, ) -> Option { let a_amount: u64 = a_amount_opt?; let b_amount: u64 = b_amount_opt?; @@ -77,6 +79,10 @@ pub fn serialize_value( } } + if let Some(pool_id) = pool_id_opt { + result["pool_id"] = serde_json::Value::String(String::from(pool_id.as_str())); + } + Some(result.to_string()) } @@ -86,10 +92,11 @@ pub fn build_key_value_pair( a_amount_opt: Option, b_amount_opt: Option, fee_opt: Option, + pool_id_opt: Option, ) -> Option<(String, String)> { let value: Option = match (&token_pair.a, &token_pair.b) { (PoolAsset::Ada, PoolAsset::AssetClass(_, _)) => { - serialize_value(dex_prefix, a_amount_opt, b_amount_opt, fee_opt) + serialize_value(dex_prefix, a_amount_opt, b_amount_opt, fee_opt, pool_id_opt) } (PoolAsset::AssetClass(_, _), PoolAsset::Ada) => { serialize_value( @@ -97,6 +104,7 @@ pub fn build_key_value_pair( b_amount_opt, // swapped a_amount_opt, // swapped fee_opt, + pool_id_opt, ) } ( @@ -115,13 +123,14 @@ pub fn build_key_value_pair( ); match asset_id_1.cmp(&asset_id_2) { std::cmp::Ordering::Less => { - serialize_value(dex_prefix, a_amount_opt, b_amount_opt, fee_opt) + serialize_value(dex_prefix, a_amount_opt, b_amount_opt, fee_opt, pool_id_opt) } std::cmp::Ordering::Greater => serialize_value( dex_prefix, b_amount_opt, // swapped a_amount_opt, // swapped fee_opt, + pool_id_opt, ), _ => None, } @@ -135,6 +144,34 @@ pub fn build_key_value_pair( None } +pub fn get_asset_amount(asset: &PoolAsset, assets: &Vec) -> Option { + match asset { + PoolAsset::Ada => { + for asset in assets { + if let Asset::Ada(lovelace_amount) = asset { + return Some(*lovelace_amount); + } + } + } + PoolAsset::AssetClass(matched_currency_symbol_hash, matched_token_name_bytes) => { + let currency_symbol: String = + hex::encode(matched_currency_symbol_hash.deref().to_vec()); + let token_name: String = hex::encode(matched_token_name_bytes.deref()); + for asset in assets { + if let Asset::NativeAsset(currency_symbol_hash, token_name_vector, amount) = asset { + if hex::encode(currency_symbol_hash.deref().to_vec()).eq(¤cy_symbol) + && hex::encode(token_name_vector).eq(&token_name) + { + return Some(*amount); + } + } + } + } + } + + None +} + #[cfg(test)] mod test { use std::str::FromStr; @@ -146,7 +183,10 @@ mod test { use crate::reducers::liquidity_by_token_pair::{ model::{CurrencySymbol, PoolAsset, TokenPair}, - utils::{build_key_value_pair, contains_currency_symbol, serialize_value}, + utils::{ + build_key_value_pair, contains_currency_symbol, get_asset_amount, pool_asset_from, + serialize_value, + }, }; static CURRENCY_SYMBOL_1: &str = "93744265ed9762d8fa52c4aacacc703aa8c81de9f6d1a59f2299235b"; @@ -260,9 +300,18 @@ mod test { token_pair.b.to_string() ); - let member = serialize_value(&Some(String::from("min")), Some(10), Some(20), Some(0.005)); + let member = serialize_value( + &Some(String::from("min")), + Some(10), + Some(20), + Some(0.005), + Some(String::from("08")), + ); assert_eq!(true, member.is_some()); - assert_eq!("min:10:20:0.005", member.unwrap()); + assert_eq!( + "{\"dex\":\"min\",\"fee\":0.005,\"pool_id\":\"08\",\"token_a\":\"10\",\"token_b\":\"20\"}", + member.unwrap() + ); let swapped_token_pair = TokenPair { a: token_pair.b.clone(), @@ -271,12 +320,40 @@ mod test { assert_eq!(token_pair.key(), swapped_token_pair.key()); assert_eq!( - build_key_value_pair(&token_pair, &None, Some(10), Some(20), Some(0.005)), - build_key_value_pair(&swapped_token_pair, &None, Some(20), Some(10), Some(0.005),), + build_key_value_pair( + &token_pair, + &None, + Some(10), + Some(20), + Some(0.005), + Some(String::from("08")) + ), + build_key_value_pair( + &swapped_token_pair, + &None, + Some(20), + Some(10), + Some(0.005), + Some(String::from("08")) + ), ); assert_eq!( - build_key_value_pair(&token_pair, &None, Some(10), Some(20), Some(0.005)), - build_key_value_pair(&swapped_token_pair, &None, Some(20), Some(10), Some(0.005),), + build_key_value_pair( + &token_pair, + &None, + Some(10), + Some(20), + Some(0.005), + Some(String::from("08")) + ), + build_key_value_pair( + &swapped_token_pair, + &None, + Some(20), + Some(10), + Some(0.005), + Some(String::from("08")) + ), ); } @@ -289,4 +366,13 @@ mod test { let key = token_pair.key(); assert_eq!(true, key.is_none()); } + + #[test] + fn test_get_asset() { + assert_eq!(None, get_asset_amount(&PoolAsset::Ada, &mock_assets())); + + let asset = + pool_asset_from(&String::from(CURRENCY_SYMBOL_1), &hex::encode("Tkn2")).unwrap(); + assert_eq!(Some(2), get_asset_amount(&asset, &mock_assets())); + } } diff --git a/src/reducers/liquidity_by_token_pair/wingriders.rs b/src/reducers/liquidity_by_token_pair/wingriders.rs index 04c3d437..25e0dafb 100644 --- a/src/reducers/liquidity_by_token_pair/wingriders.rs +++ b/src/reducers/liquidity_by_token_pair/wingriders.rs @@ -14,11 +14,9 @@ impl TryFrom<&PlutusData> for WingriderPoolDatum { if let PlutusData::Constr(pd) = value { if let Some(PlutusData::Constr(nested_pd)) = pd.fields.get(1) { let token_pair_pd = nested_pd.fields.get(0).ok_or(())?; - let token_pair = TokenPair::try_from(token_pair_pd)?; - return Ok(Self { - a: token_pair.a, - b: token_pair.b, - }); + if let Some(TokenPair { a, b }) = TokenPair::try_from(token_pair_pd).ok() { + return Ok(Self { a, b }); + } } } diff --git a/testdrive/liquidity_by_token_pair/mainnet/daemon.toml b/testdrive/liquidity_by_token_pair/mainnet/daemon.toml index 88c75d18..a44dfbdd 100644 --- a/testdrive/liquidity_by_token_pair/mainnet/daemon.toml +++ b/testdrive/liquidity_by_token_pair/mainnet/daemon.toml @@ -1,5 +1,6 @@ [source] type = "N2N" +min_depth = 3 address = "relays-new.cardano-mainnet.iohk.io:3001" [chain] @@ -18,6 +19,15 @@ dex_prefix = "min" # mandatory native asset policy id that marks valid liquidity pools pool_currency_symbol = "0be55d262b29f564998ff81efe21bdc0022621c12f15af08d0f2ddb1" +[[reducers]] +type = "LiquidityByTokenPair" +# optional redis key prefix +pool_prefix = "pool" +# optional redis member prefix +dex_prefix = "mue" +# mandatory native asset policy id that marks valid liquidity pools +pool_currency_symbol = "de9b756719341e79785aa13c164e7fe68c189ed04d61c9876b2fe53f" + [[reducers]] type = "LiquidityByTokenPair" # optional redis key prefix diff --git a/testdrive/liquidity_by_token_pair/preprod/daemon.toml b/testdrive/liquidity_by_token_pair/preprod/daemon.toml index 3fde1ba1..b03f6a29 100644 --- a/testdrive/liquidity_by_token_pair/preprod/daemon.toml +++ b/testdrive/liquidity_by_token_pair/preprod/daemon.toml @@ -1,5 +1,6 @@ [source] type = "N2N" +min_depth = 3 address = "preprod-node.world.dev.cardano.org:30000" [chain]