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}