pcli/command/tx/
liquidity_position.rs

1use anyhow::Result;
2
3use penumbra_sdk_asset::asset;
4use penumbra_sdk_dex::{
5    lp::{
6        position::{self, Position},
7        BuyOrder, SellOrder,
8    },
9    TradingPair,
10};
11use rand_core::CryptoRngCore;
12
13use super::{replicate::ReplicateCmd, FeeTier};
14
15#[derive(Debug, clap::Subcommand)]
16pub enum PositionCmd {
17    /// Open a new liquidity position based on order details and credits an open position NFT.
18    #[clap(display_order = 100, subcommand)]
19    Order(OrderCmd),
20    /// Debits an all opened position NFTs associated with a specific source and credits closed position NFTs.
21    CloseAll {
22        /// Only spend fugdnds originally received by the given address index.
23        #[clap(long, default_value = "0")]
24        source: u32,
25        /// Only close positions for the given trading pair.
26        #[clap(long)]
27        trading_pair: Option<TradingPair>,
28        /// The selected fee tier to multiply the fee amount by.
29        #[clap(short, long, default_value_t)]
30        fee_tier: FeeTier,
31    },
32    /// Debits opened position NFTs and credits closed position NFTs.
33    Close {
34        /// Only spend funds originally received by the given address index.
35        #[clap(long, default_value = "0")]
36        source: u32,
37        /// The list of [`position::Id`] of the positions to close.
38        position_ids: Vec<position::Id>,
39        /// The selected fee tier to multiply the fee amount by.
40        #[clap(short, long, default_value_t)]
41        fee_tier: FeeTier,
42    },
43    /// Debits all closed position NFTs associated with a specific account and credits withdrawn position NFTs and the final reserves.
44    WithdrawAll {
45        /// Only spend funds originally received by the given address index.
46        #[clap(long, default_value = "0")]
47        source: u32,
48        /// Only withdraw positions for the given trading pair.
49        #[clap(long)]
50        trading_pair: Option<TradingPair>,
51        /// The selected fee tier to multiply the fee amount by.
52        #[clap(short, long, default_value_t)]
53        fee_tier: FeeTier,
54    },
55    /// Debits closed position NFTs and credits withdrawn position NFTs and the final reserves.
56    Withdraw {
57        /// Only spend funds originally received by the given address index.
58        #[clap(long, default_value = "0")]
59        source: u32,
60        /// The list of [`position::Id`] of the positions to withdraw.
61        position_ids: Vec<position::Id>,
62        /// The selected fee tier to multiply the fee amount by.
63        #[clap(short, long, default_value_t)]
64        fee_tier: FeeTier,
65    },
66
67    /// Debits a withdrawn position NFT and credits a claimed position NFT and any liquidity incentives.
68    #[clap(hide(true))] // remove when reward claims exist
69    RewardClaim {},
70    /// Replicate a trading function
71    #[clap(subcommand)]
72    Replicate(ReplicateCmd),
73}
74
75impl PositionCmd {
76    pub fn offline(&self) -> bool {
77        match self {
78            PositionCmd::Order(_) => false,
79            PositionCmd::Close { .. } => false,
80            PositionCmd::CloseAll { .. } => false,
81            PositionCmd::Withdraw { .. } => false,
82            PositionCmd::WithdrawAll { .. } => false,
83            PositionCmd::RewardClaim { .. } => false,
84            PositionCmd::Replicate(replicate) => replicate.offline(),
85        }
86    }
87}
88
89#[derive(Debug, clap::Subcommand)]
90pub enum OrderCmd {
91    Buy {
92        /// The desired purchase, formatted as a string, e.g. `100penumbra@1.2gm` would attempt
93        /// to purchase 100 penumbra at a price of 1.2 gm per 1penumbra.
94        ///
95        /// An optional suffix of the form `/10bps` may be added to specify a fee spread for the
96        /// resulting position, though this is less useful for buy/sell orders than passive LPs.
97        buy_order: String,
98        /// Only spend funds originally received by the given address index.
99        #[clap(long, default_value = "0")]
100        source: u32,
101        /// When set, tags the position as an auto-closing buy.
102        #[clap(long)]
103        auto_close: bool,
104        /// The selected fee tier to multiply the fee amount by.
105        #[clap(short, long, default_value_t)]
106        fee_tier: FeeTier,
107        /// Duplicate the order for the given number of times.
108        #[clap(short, long, default_value = "1")]
109        num_copies: u32,
110    },
111    Sell {
112        /// The desired sale, formatted as a string, e.g. `100penumbra@1.2gm` would attempt
113        /// to sell 100 penumbra at a price of 1.2 gm per 1penumbra.
114        ///
115        /// An optional suffix of the form `/10bps` may be added to specify a fee spread for the
116        /// resulting position, though this is less useful for buy/sell orders than passive LPs.
117        sell_order: String,
118        /// Only spend funds originally received by the given address index.
119        #[clap(long, default_value = "0")]
120        source: u32,
121        /// When set, tags the position as an auto-closing sell.
122        #[clap(long)]
123        auto_close: bool,
124        /// The selected fee tier to multiply the fee amount by.
125        #[clap(short, long, default_value_t)]
126        fee_tier: FeeTier,
127        /// Duplicate the order for the given number of times.
128        #[clap(short, long, default_value = "1")]
129        num_copies: u32,
130    },
131}
132
133impl OrderCmd {
134    pub fn source(&self) -> u32 {
135        match self {
136            OrderCmd::Buy { source, .. } => *source,
137            OrderCmd::Sell { source, .. } => *source,
138        }
139    }
140
141    pub fn fee_tier(&self) -> FeeTier {
142        match self {
143            OrderCmd::Buy { fee_tier, .. } => *fee_tier,
144            OrderCmd::Sell { fee_tier, .. } => *fee_tier,
145        }
146    }
147
148    pub fn is_auto_closing(&self) -> bool {
149        match self {
150            OrderCmd::Buy { auto_close, .. } => *auto_close,
151            OrderCmd::Sell { auto_close, .. } => *auto_close,
152        }
153    }
154
155    pub fn num_copies(&self) -> u32 {
156        match self {
157            OrderCmd::Buy { num_copies, .. } => *num_copies,
158            OrderCmd::Sell { num_copies, .. } => *num_copies,
159        }
160    }
161
162    pub fn as_position(
163        &self,
164        // Preserved since we'll need it after denom metadata refactor
165        _asset_cache: &asset::Cache,
166        mut rng: impl CryptoRngCore,
167    ) -> Result<Vec<Position>> {
168        let positions = match self {
169            OrderCmd::Buy { buy_order, .. } => {
170                tracing::info!(?buy_order, "parsing buy order");
171                let order = BuyOrder::parse_str(buy_order)?;
172                let mut positions = Vec::new();
173                for _ in 0..self.num_copies() {
174                    let mut position = order.into_position(&mut rng);
175                    if self.is_auto_closing() {
176                        position.close_on_fill = true;
177                    }
178                    positions.push(position);
179                }
180                positions
181            }
182            OrderCmd::Sell { sell_order, .. } => {
183                tracing::info!(?sell_order, "parsing sell order");
184                let order = SellOrder::parse_str(sell_order)?;
185                let mut positions = Vec::new();
186
187                for _ in 0..self.num_copies() {
188                    let mut position = order.into_position(&mut rng);
189                    if self.is_auto_closing() {
190                        position.close_on_fill = true;
191                    }
192                    positions.push(position);
193                }
194                positions
195            }
196        };
197
198        Ok(positions)
199    }
200}