pcli/command/query/
auction.rs

1use crate::command::utils::render_positions;
2use crate::App;
3use clap::Subcommand;
4use comfy_table::{presets, Table};
5use comfy_table::{Cell, ContentArrangement};
6use penumbra_sdk_asset::asset::Cache;
7use penumbra_sdk_asset::Value;
8use penumbra_sdk_auction::auction::dutch::DutchAuction;
9use penumbra_sdk_auction::auction::AuctionId;
10use penumbra_sdk_dex::lp::position::{self, Position};
11use penumbra_sdk_num::fixpoint::U128x128;
12use penumbra_sdk_num::Amount;
13use penumbra_sdk_proto::core::component::auction::v1 as pb_auction;
14use penumbra_sdk_proto::core::component::auction::v1::query_service_client::QueryServiceClient as AuctionQueryServiceClient;
15use penumbra_sdk_proto::core::component::auction::v1::AuctionStateByIdRequest;
16use penumbra_sdk_proto::core::component::dex::v1::query_service_client::QueryServiceClient as DexQueryServiceClient;
17use penumbra_sdk_proto::core::component::dex::v1::LiquidityPositionByIdRequest;
18use penumbra_sdk_proto::DomainType;
19use penumbra_sdk_proto::Name;
20use penumbra_sdk_view::ViewClient;
21
22#[derive(Debug, Subcommand)]
23pub enum AuctionCmd {
24    /// Commands related to Dutch auctions
25    Dutch {
26        #[clap(index = 1)]
27        auction_id: AuctionId,
28    },
29}
30
31impl AuctionCmd {
32    pub async fn exec(&self, app: &mut App) -> anyhow::Result<()> {
33        match self {
34            AuctionCmd::Dutch { auction_id } => {
35                let auction_id = auction_id.clone();
36                let mut auction_client = AuctionQueryServiceClient::new(app.pd_channel().await?);
37                let rsp = auction_client
38                    .auction_state_by_id(AuctionStateByIdRequest {
39                        id: Some(auction_id.into()),
40                    })
41                    .await?
42                    .into_inner();
43
44                let pb_auction_state = rsp
45                    .auction
46                    .ok_or_else(|| anyhow::anyhow!("auction state is missing!"))?;
47
48                if pb_auction_state.type_url == pb_auction::DutchAuction::type_url() {
49                    let dutch_auction = DutchAuction::decode(pb_auction_state.value)?;
50                    let position = if let Some(position_id) = dutch_auction.state.current_position {
51                        let mut dex_client = DexQueryServiceClient::new(app.pd_channel().await?);
52                        let position: Position = dex_client
53                            .liquidity_position_by_id(LiquidityPositionByIdRequest {
54                                position_id: Some(position_id.into()),
55                            })
56                            .await?
57                            .into_inner()
58                            .data
59                            .expect("a position should exist")
60                            .try_into()
61                            .expect("no decoding error");
62                        Some(position)
63                    } else {
64                        None
65                    };
66
67                    let asset_cache = app.view().assets().await?;
68
69                    render_dutch_auction(&asset_cache, &dutch_auction, None, position).await?;
70                } else {
71                    unimplemented!("only supporting dutch auctions at the moment, come back later");
72                }
73            }
74        }
75        Ok(())
76    }
77}
78
79pub async fn render_dutch_auction(
80    asset_cache: &Cache,
81    dutch_auction: &DutchAuction,
82    local_view: Option<u64>,
83    position: Option<Position>,
84) -> anyhow::Result<()> {
85    let auction_id = dutch_auction.description.id();
86    println!("dutch auction with id {auction_id:?}:");
87
88    let initial_input = dutch_auction.description.input;
89    let input_id = initial_input.asset_id;
90    let output_id = dutch_auction.description.output_id;
91
92    let initial_input_amount = U128x128::from(initial_input.amount);
93    let min_output = U128x128::from(dutch_auction.description.min_output);
94    let max_output = U128x128::from(dutch_auction.description.max_output);
95    let start_price = (max_output / initial_input_amount).expect("the input is always nonzero");
96    let end_price = (min_output / initial_input_amount).expect("the input is always nonzero");
97
98    let maybe_id = dutch_auction.state.current_position;
99
100    let (position_input_reserve, position_output_reserve) = position.as_ref().map_or_else(
101        || (Amount::zero(), Amount::zero()),
102        |lp| {
103            (
104                lp.reserves_for(input_id)
105                    .expect("lp doesn't have reserves for input asset"),
106                lp.reserves_for(output_id)
107                    .expect("lp doesn't have reserves for output asset"),
108            )
109        },
110    );
111
112    let auction_input_reserves = Value {
113        amount: position_input_reserve + dutch_auction.state.input_reserves,
114        asset_id: input_id,
115    };
116    let auction_output_reserves = Value {
117        amount: position_output_reserve + dutch_auction.state.output_reserves,
118        asset_id: output_id,
119    };
120
121    let start_height = dutch_auction.description.start_height;
122    let end_height = dutch_auction.description.end_height;
123
124    let mut auction_table = Table::new();
125    auction_table.load_preset(presets::UTF8_FULL);
126    auction_table
127        .set_header(vec![
128            "Auction id",
129            "State",
130            "Height range",
131            "# steps",
132            "Start price",
133            "End price",
134            "Input",
135            "Balance",
136            "Has lp?",
137        ])
138        .set_content_arrangement(ContentArrangement::DynamicFullWidth)
139        .add_row(vec![
140            Cell::new(truncate_auction_id(&auction_id)).set_delimiter('.'),
141            Cell::new(render_sequence(dutch_auction.state.sequence, local_view)),
142            Cell::new(format!("{start_height} -> {end_height}")),
143            Cell::new(dutch_auction.description.step_count.to_string()),
144            Cell::new(format!("{}", start_price)),
145            Cell::new(format!("{}", end_price)),
146            Cell::new(initial_input.format(asset_cache)),
147            Cell::new(format!(
148                "({}, {})",
149                &auction_input_reserves.format(asset_cache),
150                &auction_output_reserves.format(asset_cache)
151            )),
152            Cell::new(format!("{}", render_position_id(&maybe_id)))
153                .set_alignment(comfy_table::CellAlignment::Center),
154        ]);
155
156    if let Some(lp) = position {
157        auction_table.add_row(vec![Cell::new(format!(
158            "{}",
159            render_positions(asset_cache, &[lp])
160        ))]);
161    }
162
163    println!("{auction_table}");
164    Ok(())
165}
166
167fn render_sequence(state: u64, local_seq: Option<u64>) -> String {
168    let main = if state == 0 {
169        format!("Opened")
170    } else if state == 1 {
171        format!("Closed")
172    } else {
173        format!("Withdrawn (seq={state})")
174    };
175
176    if let Some(local_seq) = local_seq {
177        format!("{main} (local_seq={local_seq})")
178    } else {
179        main
180    }
181}
182
183fn truncate_auction_id(asset_id: &AuctionId) -> String {
184    let input = format!("{asset_id:?}");
185    let prefix_len = 16;
186    if input.len() > prefix_len {
187        format!("{}...", &input[..prefix_len])
188    } else {
189        input
190    }
191}
192
193fn render_position_id(maybe_id: &Option<position::Id>) -> String {
194    let input = maybe_id.map_or_else(|| format!("x"), |_| format!("✓"));
195    let prefix_len = 6;
196    if input.len() > prefix_len {
197        format!("{}..", &input[..prefix_len])
198    } else {
199        input
200    }
201}