1use anyhow::{anyhow, Result};
2use penumbra_sdk_asset::{
3 asset::{self, Unit},
4 Value,
5};
6use penumbra_sdk_num::{fixpoint::U128x128, Amount};
7use rand_core::CryptoRngCore;
8use regex::Regex;
9
10use crate::DirectedTradingPair;
11
12use super::position::Position;
13
14#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct BuyOrder {
18 pub desired: Value,
19 pub offered: Value,
20 pub fee: u32,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct SellOrder {
27 pub offered: Value,
28 pub desired: Value,
29 pub fee: u32,
30}
31
32fn parse_parts(input: &str) -> Result<(&str, &str, u32)> {
34 let (trade_part, fee_part) = match input.rsplit_once('/') {
35 Some((trade_part, fee_part)) if fee_part.ends_with("bps") => (trade_part, fee_part),
37 _ => (input, "0bps"),
38 };
39
40 let Some((val1, val2)) = trade_part.split_once('@') else {
41 anyhow::bail!("could not parse trade string {}", input);
42 };
43
44 let fee = match fee_part.strip_suffix("bps") {
45 Some(fee) => fee.parse::<u32>()?,
46 None => anyhow::bail!("could not parse fee string {}", fee_part),
47 };
48
49 Ok((val1, val2, fee))
50}
51
52fn extract_unit(input: &str) -> Result<Unit> {
53 let unit_re = Regex::new(r"[0-9.]+([^0-9.].*+)$")?;
54 if let Some(captures) = unit_re.captures(input) {
55 let unit = captures.get(1).expect("matched regex").as_str();
56 Ok(asset::REGISTRY.parse_unit(unit))
57 } else {
58 Err(anyhow!("could not extract unit from {}", input))
59 }
60}
61
62impl BuyOrder {
63 pub fn parse_str(input: &str) -> Result<Self> {
65 let (desired_str, price_str, fee) = parse_parts(input)?;
66
67 let desired_unit = extract_unit(desired_str)?;
68 let desired = desired_str.parse::<Value>()?;
69 let price = price_str.parse::<Value>()?;
70
71 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)?
82 .round_up()?
83 .try_into()
84 .expect("rounded to integer");
85
86 let offered = Value {
87 amount: offered_amount,
88 asset_id: price.asset_id,
89 };
90
91 Ok(BuyOrder {
92 desired,
93 offered,
94 fee,
95 })
96 }
97
98 pub fn price_str(&self, cache: &asset::Cache) -> Result<String> {
105 let desired_unit = cache
106 .get(&self.desired.asset_id)
107 .map(|d| d.default_unit())
108 .ok_or_else(|| anyhow!("unknown asset {}", self.desired.asset_id))?;
109
110 let offered_amount = U128x128::from(self.offered.amount);
116 let desired_amount = U128x128::from(self.desired.amount);
117 let desired_unit_amount = U128x128::from(desired_unit.unit_amount());
118
119 let price_amount: Amount = ((offered_amount * desired_unit_amount) / desired_amount)?
120 .round_up()?
122 .try_into()
123 .expect("rounded to integer");
124
125 let price_str = Value {
126 amount: price_amount,
127 asset_id: self.offered.asset_id,
128 }
129 .format(cache);
130
131 Ok(price_str)
132 }
133
134 pub fn format(&self, cache: &asset::Cache) -> Result<String> {
136 let price_str = self.price_str(cache)?;
137 let desired_str = self.desired.format(cache);
138
139 if self.fee != 0 {
140 Ok(format!("{}@{}/{}bps", desired_str, price_str, self.fee))
141 } else {
142 Ok(format!("{}@{}", desired_str, price_str))
143 }
144 }
145}
146
147impl SellOrder {
148 pub fn parse_str(input: &str) -> Result<Self> {
150 let (offered_str, price_str, fee) = parse_parts(input)?;
151
152 let offered_unit = extract_unit(offered_str)?;
153 let offered = offered_str.parse::<Value>()?;
154 let price = price_str.parse::<Value>()?;
155
156 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)?
167 .round_up()?
168 .try_into()
169 .expect("rounded to integer");
170
171 let desired = Value {
172 amount: desired_amount,
173 asset_id: price.asset_id,
174 };
175
176 Ok(SellOrder {
177 offered,
178 desired,
179 fee,
180 })
181 }
182
183 pub fn price_str(&self, cache: &asset::Cache) -> Result<String> {
190 let offered_unit = cache
191 .get(&self.offered.asset_id)
192 .map(|d| d.default_unit())
193 .ok_or_else(|| anyhow!("unknown asset {}", self.offered.asset_id))?;
194
195 let offered_amount = U128x128::from(self.offered.amount);
201 let desired_amount = U128x128::from(self.desired.amount);
202 let offered_unit_amount = U128x128::from(offered_unit.unit_amount());
203
204 if offered_amount == 0u64.into() {
205 return Ok("∞".to_string());
206 }
207
208 let price_amount: Amount = ((desired_amount * offered_unit_amount) / offered_amount)?
209 .round_up()?
211 .try_into()
212 .expect("rounded to integer");
213
214 let price_str = Value {
215 amount: price_amount,
216 asset_id: self.desired.asset_id,
217 }
218 .format(cache);
219
220 Ok(price_str)
221 }
222
223 pub fn format(&self, cache: &asset::Cache) -> Result<String> {
225 let price_str = self.price_str(cache)?;
226 let offered_str = self.offered.format(cache);
227
228 if self.fee != 0 {
229 Ok(format!("{}@{}/{}bps", offered_str, price_str, self.fee))
230 } else {
231 Ok(format!("{}@{}", offered_str, price_str))
232 }
233 }
234}
235
236fn into_position_inner<R: CryptoRngCore>(
237 offered: Value,
238 desired: Value,
239 fee: u32,
240 rng: R,
241) -> Position {
242 let p = desired.amount;
247 let q = offered.amount;
248
249 Position::new(
250 rng,
251 DirectedTradingPair {
252 start: offered.asset_id,
253 end: desired.asset_id,
254 },
255 fee,
256 p,
257 q,
258 super::Reserves {
259 r1: offered.amount,
260 r2: 0u64.into(),
261 },
262 )
263}
264
265impl BuyOrder {
266 pub fn into_position<R: CryptoRngCore>(&self, rng: R) -> Position {
267 into_position_inner(self.offered, self.desired, self.fee, rng)
268 }
269}
270
271impl SellOrder {
272 pub fn into_position<R: CryptoRngCore>(&self, rng: R) -> Position {
273 into_position_inner(self.offered, self.desired, self.fee, rng)
274 }
275}
276
277impl Position {
281 fn interpret_inner(&self) -> Option<(Value, Value)> {
282 let offered = if self.reserves.r1 == 0u64.into() {
286 Value {
287 amount: self.reserves.r2,
288 asset_id: self.phi.pair.asset_2(),
289 }
290 } else if self.reserves.r2 == 0u64.into() {
291 Value {
292 amount: self.reserves.r1,
293 asset_id: self.phi.pair.asset_1(),
294 }
295 } else {
296 return None;
297 };
298 let mut feeless_phi = self.phi.clone();
302 feeless_phi.component.fee = 0;
303 let (_new_reserves, desired) = feeless_phi
304 .fill_output(&self.reserves, offered)
305 .expect("asset types match")
306 .expect("supplied exact reserves");
307
308 Some((offered, desired))
309 }
310
311 pub fn interpret_as_buy(&self) -> Option<BuyOrder> {
315 self.interpret_inner().map(|(offered, desired)| BuyOrder {
316 offered,
317 desired,
318 fee: self.phi.component.fee,
319 })
320 }
321
322 pub fn interpret_as_sell(&self) -> Option<SellOrder> {
326 self.interpret_inner().map(|(offered, desired)| SellOrder {
327 offered,
328 desired,
329 fee: self.phi.component.fee,
330 })
331 }
332
333 pub fn interpret_as_mixed(&self) -> Option<(SellOrder, SellOrder)> {
335 let mut split1 = self.clone();
336 let mut split2 = self.clone();
337
338 if split1.reserves.r2 == 0u64.into() {
339 return None;
340 }
341 split1.reserves.r2 = 0u64.into();
342
343 if split2.reserves.r1 == 0u64.into() {
344 return None;
345 }
346 split2.reserves.r1 = 0u64.into();
347
348 Some((
349 split1.interpret_as_sell().expect("r2 is zero"),
350 split2.interpret_as_sell().expect("r1 is zero"),
351 ))
352 }
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358
359 #[test]
360 fn parse_buy_order_basic() {
361 let mut cache = asset::Cache::default();
363 let gm = asset::Cache::with_known_assets().get_unit("gm").unwrap();
364 let gn = asset::Cache::with_known_assets().get_unit("gn").unwrap();
365 cache.extend([gm.base(), gn.base()]);
366
367 let buy_str_1 = "123.444gm@2gn/10bps";
368 let buy_order_1 = BuyOrder::parse_str(buy_str_1).unwrap();
369 assert_eq!(
370 buy_order_1,
371 BuyOrder {
372 desired: Value {
373 amount: 123_444000u64.into(),
374 asset_id: gm.id()
375 },
376 offered: Value {
377 amount: 246_888000u64.into(),
378 asset_id: gn.id()
379 },
380 fee: 10,
381 }
382 );
383
384 let buy_formatted_1 = buy_order_1.format(&cache).unwrap();
385 assert_eq!(buy_formatted_1, buy_str_1);
386
387 let buy_position_1 = buy_order_1.into_position(rand::thread_rng());
388 let buy_position_as_order_1 = buy_position_1.interpret_as_buy().unwrap();
389 let buy_position_formatted_1 = buy_position_as_order_1.format(&cache).unwrap();
390
391 assert_eq!(buy_position_as_order_1, buy_order_1);
392 assert_eq!(buy_position_formatted_1, buy_str_1);
393 }
394
395 #[test]
396 fn parse_sell_order_basic() {
397 let mut cache = asset::Cache::default();
399 let gm = asset::Cache::with_known_assets().get_unit("gm").unwrap();
400 let gn = asset::Cache::with_known_assets().get_unit("gn").unwrap();
401 cache.extend([gm.base(), gn.base()]);
402
403 let sell_str_1 = "123.444gm@2gn/10bps";
404 let sell_order_1 = SellOrder::parse_str(sell_str_1).unwrap();
405 assert_eq!(
406 sell_order_1,
407 SellOrder {
408 desired: Value {
409 amount: 246_888000u64.into(),
410 asset_id: gn.id()
411 },
412 offered: Value {
413 amount: 123_444000u64.into(),
414 asset_id: gm.id()
415 },
416 fee: 10,
417 }
418 );
419
420 let sell_formatted_1 = sell_order_1.format(&cache).unwrap();
421 assert_eq!(sell_formatted_1, sell_str_1);
422
423 let sell_position_1 = sell_order_1.into_position(rand::thread_rng());
424 let sell_position_as_order_1 = sell_position_1.interpret_as_sell().unwrap();
425 let sell_position_formatted_1 = sell_position_as_order_1.format(&cache).unwrap();
426
427 assert_eq!(sell_position_as_order_1, sell_order_1);
428 assert_eq!(sell_position_formatted_1, sell_str_1);
429 }
430}