1use std::path::Path;
2
3use crate::command::tx::FeeTier;
4use crate::App;
5use anyhow::Result;
6use anyhow::{anyhow, bail, Context};
7use clap::Subcommand;
8use comfy_table::presets;
9use dialoguer::Confirm;
10use penumbra_sdk_asset::{asset::Cache, Value};
11use penumbra_sdk_auction::auction::{
12 dutch::DutchAuction, dutch::DutchAuctionDescription, AuctionId,
13};
14use penumbra_sdk_keys::keys::AddressIndex;
15use penumbra_sdk_num::Amount;
16use penumbra_sdk_proto::{view::v1::GasPricesRequest, DomainType};
17use penumbra_sdk_view::ViewClient;
18use penumbra_sdk_wallet::plan::Planner;
19use rand::RngCore;
20use rand_core::OsRng;
21use serde_json;
22
23mod debug;
24pub mod gda;
25
26#[derive(Debug, Subcommand)]
28pub enum DutchCmd {
29 #[clap(display_order = 1000, name = "gradual")]
31 DutchAuctionGradualSchedule {
32 #[clap(long, display_order = 100, default_value = "0")]
34 source: u32,
35 #[clap(long, display_order = 200)]
37 input: String,
38 #[clap(long, display_order = 400)]
42 max_output: String,
43 #[clap(long, display_order = 500)]
47 min_output: String,
48 #[clap(arg_enum, long, display_order = 600, name = "duration")]
50 recipe: gda::GdaRecipe,
51 #[clap(long, display_order = 700)]
53 yes: bool,
54 #[clap(short, long, default_value_t, display_order = 1000)]
56 fee_tier: FeeTier,
57 #[clap(long, hide = true)]
58 debug: bool,
60 },
61 #[clap(display_order = 100, name = "schedule")]
63 DutchAuctionSchedule {
64 #[clap(long, display_order = 100, default_value = "0")]
66 source: u32,
67 #[clap(long, display_order = 200)]
69 input: String,
70 #[clap(long, display_order = 400)]
74 max_output: String,
75 #[clap(long, display_order = 500)]
79 min_output: String,
80 #[clap(long, display_order = 600)]
84 start_height: u64,
85 #[clap(long, display_order = 700)]
90 end_height: u64,
91 #[clap(long, display_order = 800)]
95 step_count: u64,
96 #[clap(short, long, default_value_t, display_order = 1000)]
98 fee_tier: FeeTier,
99 },
100 #[clap(display_order = 300, name = "end")]
102 DutchAuctionEnd {
103 #[clap(long, display_order = 100, default_value = "0")]
105 source: u32,
106 #[clap(long, display_order = 150)]
108 all: bool,
109 #[clap(display_order = 200)]
111 auction_id: Option<String>,
112 #[clap(short, long, default_value_t, display_order = 300)]
114 fee_tier: FeeTier,
115 },
116 #[clap(display_order = 200, name = "withdraw")]
118 DutchAuctionWithdraw {
119 #[clap(long, display_order = 100, default_value = "0")]
121 source: u32,
122 #[clap(long, display_order = 150)]
124 all: bool,
125 #[clap(display_order = 200)]
127 auction_id: Option<String>,
128 #[clap(short, long, default_value_t, display_order = 600)]
130 fee_tier: FeeTier,
131 },
132}
133
134impl DutchCmd {
135 pub async fn exec(&self, app: &mut App) -> anyhow::Result<()> {
137 let gas_prices = app
138 .view
139 .as_mut()
140 .context("view service must be initialized")?
141 .gas_prices(GasPricesRequest {})
142 .await?
143 .into_inner()
144 .gas_prices
145 .expect("gas prices must be available")
146 .try_into()?;
147
148 match self {
149 DutchCmd::DutchAuctionSchedule {
150 source,
151 input,
152 max_output,
153 min_output,
154 start_height,
155 end_height,
156 step_count,
157 fee_tier,
158 } => {
159 let mut nonce = [0u8; 32];
160 OsRng.fill_bytes(&mut nonce);
161
162 let input = input.parse::<Value>()?;
163 let max_output = max_output.parse::<Value>()?;
164 let min_output = min_output.parse::<Value>()?;
165 let output_id = max_output.asset_id;
166
167 let plan = Planner::new(OsRng)
168 .set_gas_prices(gas_prices)
169 .set_fee_tier((*fee_tier).into())
170 .dutch_auction_schedule(DutchAuctionDescription {
171 input,
172 output_id,
173 max_output: max_output.amount,
174 min_output: min_output.amount,
175 start_height: *start_height,
176 end_height: *end_height,
177 step_count: *step_count,
178 nonce,
179 })
180 .plan(
181 app.view
182 .as_mut()
183 .context("view service must be initialized")?,
184 AddressIndex::new(*source),
185 )
186 .await
187 .context("can't build auction schedule transaction")?;
188 app.build_and_submit_transaction(plan).await?;
189 Ok(())
190 }
191 DutchCmd::DutchAuctionEnd {
192 all,
193 auction_id,
194 source,
195 fee_tier,
196 } => {
197 let auction_ids = match (all, auction_id) {
198 (true, _) => auctions_to_end(app.view(), *source).await?,
199 (false, Some(auction_id)) => {
200 let auction_id = auction_id.parse::<AuctionId>()?;
201 vec![auction_id]
202 }
203 (false, None) => {
204 bail!("auction_id is required when --all is not set")
205 }
206 };
207
208 let mut planner = Planner::new(OsRng);
209
210 planner
211 .set_gas_prices(gas_prices)
212 .set_fee_tier((*fee_tier).into());
213
214 for auction_id in auction_ids {
215 planner.dutch_auction_end(auction_id);
216 }
217
218 let plan = planner
219 .plan(
220 app.view
221 .as_mut()
222 .context("view service must be initialized")?,
223 AddressIndex::new(*source),
224 )
225 .await
226 .context("can't build auction end transaction")?;
227 app.build_and_submit_transaction(plan).await?;
228 Ok(())
229 }
230 DutchCmd::DutchAuctionWithdraw {
231 all,
232 source,
233 auction_id,
234 fee_tier,
235 } => {
236 let auctions = match (all, auction_id) {
237 (true, _) => auctions_to_withdraw(app.view(), *source).await?,
238 (false, Some(auction_id)) => {
239 let auction_id = auction_id.parse::<AuctionId>()?;
240
241 let all = auctions_to_withdraw(app.view(), *source).await?;
242 vec![all
243 .into_iter()
244 .find(|a| a.description.id() == auction_id)
245 .ok_or_else(|| {
246 anyhow!("the auction id is unknown from the view service!")
247 })?]
248 }
249 (false, None) => {
250 bail!("auction_id is required when --all is not set")
251 }
252 };
253
254 let mut planner = Planner::new(OsRng);
255
256 planner
257 .set_gas_prices(gas_prices)
258 .set_fee_tier((*fee_tier).into());
259
260 for auction in &auctions {
261 planner.dutch_auction_withdraw(auction);
262 }
263
264 let plan = planner
265 .plan(
266 app.view
267 .as_mut()
268 .context("view service must be initialized")?,
269 AddressIndex::new(*source),
270 )
271 .await
272 .context("can't build auction withdrawal transaction")?;
273 app.build_and_submit_transaction(plan).await?;
274 Ok(())
275 }
276 DutchCmd::DutchAuctionGradualSchedule {
277 source,
278 input: input_str,
279 max_output: max_output_str,
280 min_output: min_output_str,
281 recipe: duration,
282 yes,
283 fee_tier,
284 debug,
285 } => {
286 println!("Gradual dutch auction prototype");
287
288 let input = input_str.parse::<Value>()?;
289 let max_output = max_output_str.parse::<Value>()?;
290 let min_output = min_output_str.parse::<Value>()?;
291
292 let asset_cache = app.view().assets().await?;
293 let current_height = app.view().status().await?.full_sync_height;
294
295 let gda = gda::GradualAuction::new(
296 input,
297 max_output,
298 min_output,
299 duration.clone(),
300 current_height,
301 );
302
303 let auction_descriptions = gda.generate_auctions();
304
305 let input_fmt = input.format(&asset_cache);
306 let max_output_fmt = max_output.format(&asset_cache);
307 let min_output_fmt = min_output.format(&asset_cache);
308
309 println!("total to auction: {input_fmt}");
310 println!("start price: {max_output_fmt}");
311 println!("end price: {min_output_fmt}");
312 display_auction_description(&asset_cache, auction_descriptions.clone());
313
314 let mut planner = Planner::new(OsRng);
315 planner
316 .set_gas_prices(gas_prices)
317 .set_fee_tier((*fee_tier).into());
318
319 for description in &auction_descriptions {
320 planner.dutch_auction_schedule(description.clone());
321 }
322
323 if *debug {
324 let debug_data_path = Path::new("gda-debug-definition-data.json");
325 let auction_data_path = Path::new("gda-debug-auction-data.json");
326
327 let gda_debug_data = serde_json::to_string(&gda)?;
328 std::fs::write(debug_data_path, gda_debug_data)?;
329
330 let gda_auction_data = serde_json::to_string(
331 &auction_descriptions
332 .clone()
333 .into_iter()
334 .map(Into::<debug::DebugDescription>::into)
335 .collect::<Vec<_>>(),
336 )?;
337 std::fs::write(auction_data_path, gda_auction_data)?;
338 tracing::debug!(?debug_data_path, ?auction_data_path, "wrote debug data");
339 return Ok(());
340 }
341
342 let plan = planner
343 .plan(
344 app.view
345 .as_mut()
346 .context("view service must be initialized")?,
347 AddressIndex::new(*source),
348 )
349 .await
350 .context("can't build send transaction")?;
351
352 let tx = app.build_transaction(plan.clone()).await?;
353 let fee_fmt = tx
354 .transaction_body
355 .transaction_parameters
356 .fee
357 .0
358 .format(&asset_cache);
359
360 println!("Total fee: {fee_fmt}");
361
362 if !yes {
363 Confirm::new()
364 .with_prompt("Do you wish to proceed")
365 .interact()?;
366 }
367 app.build_and_submit_transaction(plan).await?;
368
369 Ok(())
370 }
371 }
372 }
373}
374
375async fn all_dutch_auction_states(
376 view_client: &mut impl ViewClient,
377 source: impl Into<AddressIndex>,
378) -> Result<Vec<(AuctionId, DutchAuction, u64)>> {
379 fetch_dutch_auction_states(view_client, source, true).await
380}
381
382async fn active_dutch_auction_states(
383 view_client: &mut impl ViewClient,
384 source: impl Into<AddressIndex>,
385) -> Result<Vec<(AuctionId, DutchAuction, u64)>> {
386 fetch_dutch_auction_states(view_client, source, false).await
387}
388
389async fn fetch_dutch_auction_states(
390 view_client: &mut impl ViewClient,
391 source: impl Into<AddressIndex>,
392 include_inactive: bool,
393) -> Result<Vec<(AuctionId, DutchAuction, u64)>> {
394 let auctions = view_client
395 .auctions(Some(source.into()), include_inactive, true)
396 .await?
397 .into_iter()
398 .filter_map(|(id, _, local_seq, state, _)| {
399 if let Some(state) = state {
400 if let Ok(da) = DutchAuction::decode(state.value) {
401 Some((id, da, local_seq))
402 } else {
403 None
404 }
405 } else {
406 None
407 }
408 })
409 .collect();
410 Ok(auctions)
411}
412async fn auctions_to_end(view_client: &mut impl ViewClient, source: u32) -> Result<Vec<AuctionId>> {
414 let auctions = active_dutch_auction_states(view_client, source).await?;
415
416 let auction_ids = auctions
417 .into_iter()
418 .filter_map(|(id, _auction, local_seq)| {
419 if local_seq == 0 {
422 Some(id)
423 } else {
424 None
425 }
426 })
427 .collect();
428
429 Ok(auction_ids)
430}
431
432async fn auctions_to_withdraw(
433 view_client: &mut impl ViewClient,
434 source: u32,
435) -> Result<Vec<DutchAuction>> {
436 let auctions = all_dutch_auction_states(view_client, source).await?;
437
438 let auction_ids = auctions
439 .into_iter()
440 .filter_map(|(_, auction, local_seq)| {
441 if local_seq == 1 {
444 Some(auction)
445 } else {
446 None
447 }
448 })
449 .collect();
450
451 Ok(auction_ids)
452}
453
454fn display_auction_description(asset_cache: &Cache, auctions: Vec<DutchAuctionDescription>) {
455 let mut tally_max_output = Amount::zero();
456 let mut tally_min_output = Amount::zero();
457 let mut tally_input = Amount::zero();
458 let input_id = auctions[0].input.asset_id;
459 let output_id = auctions[0].output_id;
460
461 let mut table = comfy_table::Table::new();
462 table.load_preset(presets::NOTHING);
463
464 table.set_header(vec![
465 "start",
466 "",
467 "end",
468 "lot",
469 "start price for the lot",
470 "reserve price for the lot",
471 ]);
472
473 for auction in auctions {
474 let start_height = auction.start_height;
475 let end_height = auction.end_height;
476 let input_chunk = Value {
477 asset_id: auction.input.asset_id,
478 amount: Amount::from(auction.input.amount.value()),
479 };
480
481 let max_price = Value {
482 asset_id: auction.output_id,
483 amount: Amount::from(auction.max_output.value()),
484 };
485
486 let min_price = Value {
487 asset_id: auction.output_id,
488 amount: Amount::from(auction.min_output.value()),
489 };
490
491 let max_price_fmt = max_price.format(&asset_cache);
492 let min_price_fmt = min_price.format(&asset_cache);
493
494 let input_chunk_fmt = input_chunk.format(&asset_cache);
495
496 tally_input += input_chunk.amount;
497 tally_max_output += max_price.amount;
498 tally_min_output += min_price.amount;
499
500 table.add_row(vec![
501 format!("{start_height}"),
502 "--------->".to_string(),
503 format!("{end_height}"),
504 input_chunk_fmt,
505 max_price_fmt,
506 min_price_fmt,
507 ]);
508 }
509
510 println!("{}", table);
511
512 let tally_input_fmt = Value {
513 asset_id: input_id,
514 amount: tally_input,
515 }
516 .format(&asset_cache);
517
518 let tally_output_max_fmt = Value {
519 asset_id: output_id,
520 amount: tally_max_output,
521 }
522 .format(&asset_cache);
523
524 let tally_output_min_fmt = Value {
525 asset_id: output_id,
526 amount: tally_min_output,
527 }
528 .format(&asset_cache);
529
530 println!("Total auctioned: {tally_input_fmt}");
531 println!("Total max output: {tally_output_max_fmt}");
532 println!("Total min output: {tally_output_min_fmt}");
533}