pcli/command/tx/
replicate.rs

1use crate::App;
2
3pub mod linear;
4pub mod xyk;
5
6use linear::Linear;
7use penumbra_sdk_dex::DirectedUnitPair;
8use penumbra_sdk_proto::core::component::dex::v1::{
9    query_service_client::QueryServiceClient as DexQueryServiceClient, SpreadRequest,
10};
11use xyk::ConstantProduct;
12
13/// Queries the chain for a transaction by hash.
14#[derive(Debug, clap::Subcommand)]
15pub enum ReplicateCmd {
16    /// Create a set of positions that attempt to replicate an xy=k (UniV2) AMM.
17    // Hidden pending further testing & review
18    #[clap(visible_alias = "xyk", hide = true)]
19    ConstantProduct(ConstantProduct),
20    /// Create a set of positions that allocate liquidity linearly across a price range.
21    Linear(Linear),
22}
23
24impl ReplicateCmd {
25    pub async fn exec(&self, app: &mut App) -> anyhow::Result<()> {
26        match self {
27            ReplicateCmd::ConstantProduct(xyk_cmd) => xyk_cmd.exec(app).await?,
28            ReplicateCmd::Linear(linear_cmd) => linear_cmd.exec(app).await?,
29        };
30        Ok(())
31    }
32
33    pub fn offline(&self) -> bool {
34        false
35    }
36}
37
38pub async fn process_price_or_fetch_spread(
39    app: &mut App,
40    user_price: Option<f64>,
41    directed_pair: DirectedUnitPair,
42) -> anyhow::Result<f64> {
43    let canonical_pair = directed_pair.into_directed_trading_pair().to_canonical();
44
45    if let Some(user_price) = user_price {
46        let adjusted = adjust_price_by_exponents(user_price, &directed_pair);
47        tracing::debug!(?user_price, ?adjusted, "adjusted price by units");
48        return Ok(adjusted);
49    } else {
50        tracing::debug!("price not provided, fetching spread");
51
52        let mut client = DexQueryServiceClient::new(app.pd_channel().await?);
53        let spread_data = client
54            .spread(SpreadRequest {
55                trading_pair: Some(canonical_pair.into()),
56            })
57            .await?
58            .into_inner();
59
60        tracing::debug!(
61            ?spread_data,
62            pair = canonical_pair.to_string(),
63            "fetched spread for pair"
64        );
65
66        if spread_data.best_1_to_2_position.is_none() || spread_data.best_2_to_1_position.is_none()
67        {
68            anyhow::bail!("couldn't find a market price for the specified assets, you can manually specify a price using --current-price <price>")
69        }
70
71        // The price is the amount of asset 2 required to buy 1 unit of asset 1
72
73        if directed_pair.start.id() == canonical_pair.asset_1() {
74            Ok(spread_data.approx_effective_price_2_to_1)
75        } else if directed_pair.start.id() == canonical_pair.asset_2() {
76            Ok(spread_data.approx_effective_price_1_to_2)
77        } else {
78            anyhow::bail!("the supplied liquidity must be on the pair")
79        }
80    }
81}
82
83fn adjust_price_by_exponents(price: f64, pair: &DirectedUnitPair) -> f64 {
84    let start_exponent = pair.start.exponent() as i32;
85    let end_exponent = pair.end.exponent() as i32;
86
87    price * 10f64.powi(start_exponent - end_exponent)
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn test_adjust_price_by_exponents() {
96        let pair1: DirectedUnitPair = "penumbra:gm".parse().unwrap();
97        let pair2: DirectedUnitPair = "upenumbra:gm".parse().unwrap();
98        let pair3: DirectedUnitPair = "penumbra:ugm".parse().unwrap();
99        let pair4: DirectedUnitPair = "upenumbra:ugm".parse().unwrap();
100
101        let base_price = 1.2;
102
103        assert_eq!(adjust_price_by_exponents(base_price, &pair1), 1.2);
104        assert_eq!(adjust_price_by_exponents(base_price, &pair2), 0.0000012);
105        assert_eq!(adjust_price_by_exponents(base_price, &pair3), 1200000.0);
106        assert_eq!(adjust_price_by_exponents(base_price, &pair4), 1.2);
107    }
108}