use anyhow::{anyhow, Result};
use penumbra_asset::{
asset::{self, Unit},
Value,
};
use penumbra_num::{fixpoint::U128x128, Amount};
use rand_core::CryptoRngCore;
use regex::Regex;
use crate::DirectedTradingPair;
use super::position::Position;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BuyOrder {
pub desired: Value,
pub offered: Value,
pub fee: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SellOrder {
pub offered: Value,
pub desired: Value,
pub fee: u32,
}
fn parse_parts(input: &str) -> Result<(&str, &str, u32)> {
let (trade_part, fee_part) = match input.rsplit_once('/') {
Some((trade_part, fee_part)) if fee_part.ends_with("bps") => (trade_part, fee_part),
_ => (input, "0bps"),
};
let Some((val1, val2)) = trade_part.split_once('@') else {
anyhow::bail!("could not parse trade string {}", input);
};
let fee = match fee_part.strip_suffix("bps") {
Some(fee) => fee.parse::<u32>()?,
None => anyhow::bail!("could not parse fee string {}", fee_part),
};
Ok((val1, val2, fee))
}
fn extract_unit(input: &str) -> Result<Unit> {
let unit_re = Regex::new(r"[0-9.]+([^0-9.].*+)$")?;
if let Some(captures) = unit_re.captures(input) {
let unit = captures.get(1).expect("matched regex").as_str();
Ok(asset::REGISTRY.parse_unit(unit))
} else {
Err(anyhow!("could not extract unit from {}", input))
}
}
impl BuyOrder {
pub fn parse_str(input: &str) -> Result<Self> {
let (desired_str, price_str, fee) = parse_parts(input)?;
let desired_unit = extract_unit(desired_str)?;
let desired = desired_str.parse::<Value>()?;
let price = price_str.parse::<Value>()?;
let price_amount = U128x128::from(price.amount); let desired_amount = U128x128::from(desired.amount); let desired_unit_amount = U128x128::from(desired_unit.unit_amount()); let offered_amount = ((price_amount * desired_amount) / desired_unit_amount)?
.round_up()?
.try_into()
.expect("rounded to integer");
let offered = Value {
amount: offered_amount,
asset_id: price.asset_id,
};
Ok(BuyOrder {
desired,
offered,
fee,
})
}
pub fn price_str(&self, cache: &asset::Cache) -> Result<String> {
let desired_unit = cache
.get(&self.desired.asset_id)
.map(|d| d.default_unit())
.ok_or_else(|| anyhow!("unknown asset {}", self.desired.asset_id))?;
let offered_amount = U128x128::from(self.offered.amount);
let desired_amount = U128x128::from(self.desired.amount);
let desired_unit_amount = U128x128::from(desired_unit.unit_amount());
let price_amount: Amount = ((offered_amount * desired_unit_amount) / desired_amount)?
.round_up()?
.try_into()
.expect("rounded to integer");
let price_str = Value {
amount: price_amount,
asset_id: self.offered.asset_id,
}
.format(cache);
Ok(price_str)
}
pub fn format(&self, cache: &asset::Cache) -> Result<String> {
let price_str = self.price_str(cache)?;
let desired_str = self.desired.format(cache);
if self.fee != 0 {
Ok(format!("{}@{}/{}bps", desired_str, price_str, self.fee))
} else {
Ok(format!("{}@{}", desired_str, price_str))
}
}
}
impl SellOrder {
pub fn parse_str(input: &str) -> Result<Self> {
let (offered_str, price_str, fee) = parse_parts(input)?;
let offered_unit = extract_unit(offered_str)?;
let offered = offered_str.parse::<Value>()?;
let price = price_str.parse::<Value>()?;
let price_amount = U128x128::from(price.amount); let offered_amount = U128x128::from(offered.amount); let offered_unit_amount = U128x128::from(offered_unit.unit_amount()); let desired_amount = ((price_amount * offered_amount) / offered_unit_amount)?
.round_up()?
.try_into()
.expect("rounded to integer");
let desired = Value {
amount: desired_amount,
asset_id: price.asset_id,
};
Ok(SellOrder {
offered,
desired,
fee,
})
}
pub fn price_str(&self, cache: &asset::Cache) -> Result<String> {
let offered_unit = cache
.get(&self.offered.asset_id)
.map(|d| d.default_unit())
.ok_or_else(|| anyhow!("unknown asset {}", self.offered.asset_id))?;
let offered_amount = U128x128::from(self.offered.amount);
let desired_amount = U128x128::from(self.desired.amount);
let offered_unit_amount = U128x128::from(offered_unit.unit_amount());
if offered_amount == 0u64.into() {
return Ok("∞".to_string());
}
let price_amount: Amount = ((desired_amount * offered_unit_amount) / offered_amount)?
.round_up()?
.try_into()
.expect("rounded to integer");
let price_str = Value {
amount: price_amount,
asset_id: self.desired.asset_id,
}
.format(cache);
Ok(price_str)
}
pub fn format(&self, cache: &asset::Cache) -> Result<String> {
let price_str = self.price_str(cache)?;
let offered_str = self.offered.format(cache);
if self.fee != 0 {
Ok(format!("{}@{}/{}bps", offered_str, price_str, self.fee))
} else {
Ok(format!("{}@{}", offered_str, price_str))
}
}
}
fn into_position_inner<R: CryptoRngCore>(
offered: Value,
desired: Value,
fee: u32,
rng: R,
) -> Position {
let p = desired.amount;
let q = offered.amount;
Position::new(
rng,
DirectedTradingPair {
start: offered.asset_id,
end: desired.asset_id,
},
fee,
p,
q,
super::Reserves {
r1: offered.amount,
r2: 0u64.into(),
},
)
}
impl BuyOrder {
pub fn into_position<R: CryptoRngCore>(&self, rng: R) -> Position {
into_position_inner(self.offered, self.desired, self.fee, rng)
}
}
impl SellOrder {
pub fn into_position<R: CryptoRngCore>(&self, rng: R) -> Position {
into_position_inner(self.offered, self.desired, self.fee, rng)
}
}
impl Position {
fn interpret_inner(&self) -> Option<(Value, Value)> {
let offered = if self.reserves.r1 == 0u64.into() {
Value {
amount: self.reserves.r2,
asset_id: self.phi.pair.asset_2(),
}
} else if self.reserves.r2 == 0u64.into() {
Value {
amount: self.reserves.r1,
asset_id: self.phi.pair.asset_1(),
}
} else {
return None;
};
let mut feeless_phi = self.phi.clone();
feeless_phi.component.fee = 0;
let (_new_reserves, desired) = feeless_phi
.fill_output(&self.reserves, offered)
.expect("asset types match")
.expect("supplied exact reserves");
Some((offered, desired))
}
pub fn interpret_as_buy(&self) -> Option<BuyOrder> {
self.interpret_inner().map(|(offered, desired)| BuyOrder {
offered,
desired,
fee: self.phi.component.fee,
})
}
pub fn interpret_as_sell(&self) -> Option<SellOrder> {
self.interpret_inner().map(|(offered, desired)| SellOrder {
offered,
desired,
fee: self.phi.component.fee,
})
}
pub fn interpret_as_mixed(&self) -> Option<(SellOrder, SellOrder)> {
let mut split1 = self.clone();
let mut split2 = self.clone();
if split1.reserves.r2 == 0u64.into() {
return None;
}
split1.reserves.r2 = 0u64.into();
if split2.reserves.r1 == 0u64.into() {
return None;
}
split2.reserves.r1 = 0u64.into();
Some((
split1.interpret_as_sell().expect("r2 is zero"),
split2.interpret_as_sell().expect("r1 is zero"),
))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_buy_order_basic() {
let mut cache = asset::Cache::default();
let gm = asset::Cache::with_known_assets().get_unit("gm").unwrap();
let gn = asset::Cache::with_known_assets().get_unit("gn").unwrap();
cache.extend([gm.base(), gn.base()]);
let buy_str_1 = "123.444gm@2gn/10bps";
let buy_order_1 = BuyOrder::parse_str(buy_str_1).unwrap();
assert_eq!(
buy_order_1,
BuyOrder {
desired: Value {
amount: 123_444000u64.into(),
asset_id: gm.id()
},
offered: Value {
amount: 246_888000u64.into(),
asset_id: gn.id()
},
fee: 10,
}
);
let buy_formatted_1 = buy_order_1.format(&cache).unwrap();
assert_eq!(buy_formatted_1, buy_str_1);
let buy_position_1 = buy_order_1.into_position(rand::thread_rng());
let buy_position_as_order_1 = buy_position_1.interpret_as_buy().unwrap();
let buy_position_formatted_1 = buy_position_as_order_1.format(&cache).unwrap();
assert_eq!(buy_position_as_order_1, buy_order_1);
assert_eq!(buy_position_formatted_1, buy_str_1);
}
#[test]
fn parse_sell_order_basic() {
let mut cache = asset::Cache::default();
let gm = asset::Cache::with_known_assets().get_unit("gm").unwrap();
let gn = asset::Cache::with_known_assets().get_unit("gn").unwrap();
cache.extend([gm.base(), gn.base()]);
let sell_str_1 = "123.444gm@2gn/10bps";
let sell_order_1 = SellOrder::parse_str(sell_str_1).unwrap();
assert_eq!(
sell_order_1,
SellOrder {
desired: Value {
amount: 246_888000u64.into(),
asset_id: gn.id()
},
offered: Value {
amount: 123_444000u64.into(),
asset_id: gm.id()
},
fee: 10,
}
);
let sell_formatted_1 = sell_order_1.format(&cache).unwrap();
assert_eq!(sell_formatted_1, sell_str_1);
let sell_position_1 = sell_order_1.into_position(rand::thread_rng());
let sell_position_as_order_1 = sell_position_1.interpret_as_sell().unwrap();
let sell_position_formatted_1 = sell_position_as_order_1.format(&cache).unwrap();
assert_eq!(sell_position_as_order_1, sell_order_1);
assert_eq!(sell_position_formatted_1, sell_str_1);
}
}