diff --git a/Cargo.lock b/Cargo.lock index 13bedf087b..d52d5d01f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4444,6 +4444,7 @@ dependencies = [ "rand 0.8.5", "rand_chacha 0.3.1", "rand_core 0.6.4", + "rand_distr", "regex", "rpassword", "serde", diff --git a/crates/bin/pcli/Cargo.toml b/crates/bin/pcli/Cargo.toml index c68aecff0d..6bf8e7eda0 100644 --- a/crates/bin/pcli/Cargo.toml +++ b/crates/bin/pcli/Cargo.toml @@ -88,7 +88,7 @@ rand_core = {workspace = true, features = ["getrandom"]} regex = {workspace = true} rpassword = "7" serde = {workspace = true, features = ["derive"]} -serde_json = {workspace = true} +serde_json.workspace = true serde_with = {workspace = true, features = ["hex"]} sha2 = {workspace = true} simple-base64 = "0.23" @@ -103,6 +103,7 @@ termion = {workspace = true} tracing = {workspace = true} tracing-subscriber = {workspace = true, features = ["env-filter", "ansi"]} url = {workspace = true, features = ["serde"]} +rand_distr = "0.4.3" [dev-dependencies] assert_cmd = {workspace = true} diff --git a/crates/bin/pcli/src/command/query/auction.rs b/crates/bin/pcli/src/command/query/auction.rs index 36d4ac6fb4..644be7ba19 100644 --- a/crates/bin/pcli/src/command/query/auction.rs +++ b/crates/bin/pcli/src/command/query/auction.rs @@ -1,9 +1,10 @@ +use crate::command::utils::render_positions; use crate::App; use clap::Subcommand; use comfy_table::{presets, Table}; use comfy_table::{Cell, ContentArrangement}; use penumbra_asset::asset::Cache; -use penumbra_asset::{asset, Value}; +use penumbra_asset::Value; use penumbra_auction::auction::dutch::DutchAuction; use penumbra_auction::auction::AuctionId; use penumbra_dex::lp::position::{self, Position}; @@ -16,6 +17,7 @@ use penumbra_proto::core::component::dex::v1::query_service_client::QueryService use penumbra_proto::core::component::dex::v1::LiquidityPositionByIdRequest; use penumbra_proto::DomainType; use penumbra_proto::Name; +use penumbra_view::ViewClient; #[derive(Debug, Subcommand)] pub enum AuctionCmd { @@ -62,7 +64,9 @@ impl AuctionCmd { None }; - render_dutch_auction(&dutch_auction, position).await?; + let asset_cache = app.view().assets().await?; + + render_dutch_auction(&asset_cache, &dutch_auction, position).await?; } else { unimplemented!("only supporting dutch auctions at the moment, come back later"); } @@ -73,23 +77,17 @@ impl AuctionCmd { } pub async fn render_dutch_auction( + asset_cache: &Cache, dutch_auction: &DutchAuction, position: Option, ) -> anyhow::Result<()> { let auction_id = dutch_auction.description.id(); - println!("dutch auction with id {auction_id:?}"); - if let Some(id) = position.as_ref() { - let position_id = id.id(); - println!("auction has a deployed liquidity position with id: {position_id:?}"); - } + let initial_input = dutch_auction.description.input; let input_id = initial_input.asset_id; let output_id = dutch_auction.description.output_id; - let _input_id_str = truncate_asset_id(&input_id); - let _output_id_str = truncate_asset_id(&output_id); - let initial_input_amount = U128x128::from(initial_input.amount); let min_output = U128x128::from(dutch_auction.description.min_output); let max_output = U128x128::from(dutch_auction.description.max_output); @@ -120,10 +118,8 @@ pub async fn render_dutch_auction( let start_height = dutch_auction.description.start_height; let end_height = dutch_auction.description.end_height; - let basic_cache = Cache::with_known_assets(); - let mut auction_table = Table::new(); - auction_table.load_preset(presets::ASCII_FULL); + auction_table.load_preset(presets::UTF8_FULL); auction_table .set_header(vec![ "Auction id", @@ -144,29 +140,22 @@ pub async fn render_dutch_auction( Cell::new(&dutch_auction.description.step_count.to_string()), Cell::new(&format!("{}", start_price)), Cell::new(&format!("{}", end_price)), - Cell::new(&initial_input.format(&basic_cache)), + Cell::new(&initial_input.format(asset_cache)), Cell::new(format!( "({}, {})", - &auction_input_reserves.format(&basic_cache), - &auction_output_reserves.format(&basic_cache) + &auction_input_reserves.format(asset_cache), + &auction_output_reserves.format(asset_cache) )), Cell::new(format!("{}", render_position_id(&maybe_id))) .set_alignment(comfy_table::CellAlignment::Center), ]); - let mut position_table = Table::new(); - position_table - .load_preset(presets::NOTHING) - .set_content_arrangement(ContentArrangement::Dynamic) - .set_table_width(80) - .set_header(vec![ - "position id", - "state", - "input reserves", - "output reserves", - "quoting price", - ]) - .add_row(vec![Cell::new("nothing for now")]); + if let Some(lp) = position { + auction_table.add_row(vec![Cell::new(format!( + "{}", + render_positions(asset_cache, &[lp]) + ))]); + } println!("{auction_table}"); Ok(()) @@ -182,16 +171,6 @@ fn render_sequence(state: u64) -> String { } } -fn truncate_asset_id(asset_id: &asset::Id) -> String { - let input = format!("{asset_id:?}"); - let prefix_len = 15; - if input.len() > prefix_len { - format!("{}...", &input[..prefix_len]) - } else { - input - } -} - fn truncate_auction_id(asset_id: &AuctionId) -> String { let input = format!("{asset_id:?}"); let prefix_len = 16; diff --git a/crates/bin/pcli/src/command/tx.rs b/crates/bin/pcli/src/command/tx.rs index ff9f70a649..29995488f9 100644 --- a/crates/bin/pcli/src/command/tx.rs +++ b/crates/bin/pcli/src/command/tx.rs @@ -25,7 +25,7 @@ use ibc_types::lightclients::tendermint::client_state::ClientState as Tendermint use rand_core::OsRng; use regex::Regex; -use auction::AuctionCmd; +use crate::command::tx::auction::AuctionCmd; use liquidity_position::PositionCmd; use penumbra_asset::{asset, asset::Metadata, Value, STAKING_TOKEN_ASSET_ID}; use penumbra_dex::{lp::position, swap_claim::SwapClaimPlan}; diff --git a/crates/bin/pcli/src/command/tx/auction.rs b/crates/bin/pcli/src/command/tx/auction.rs index 4b2f3d4f5b..48617dd45c 100644 --- a/crates/bin/pcli/src/command/tx/auction.rs +++ b/crates/bin/pcli/src/command/tx/auction.rs @@ -1,19 +1,7 @@ -use super::FeeTier; -use crate::App; -use anyhow::{anyhow, bail, ensure, Context}; +use crate::command::tx::auction::dutch::DutchCmd; use clap::Subcommand; -use penumbra_asset::Value; -use penumbra_auction::auction::{ - dutch::{actions::ActionDutchAuctionWithdrawPlan, DutchAuction, DutchAuctionDescription}, - AuctionId, -}; -use penumbra_dex::lp::position::Position; -use penumbra_keys::keys::AddressIndex; -use penumbra_proto::{view::v1::GasPricesRequest, DomainType, Name}; -use penumbra_view::SpendableNoteRecord; -use penumbra_wallet::plan::Planner; -use rand::RngCore; -use rand_core::OsRng; + +pub mod dutch; #[derive(Debug, Subcommand)] pub enum AuctionCmd { @@ -21,237 +9,3 @@ pub enum AuctionCmd { #[clap(display_order = 100, subcommand)] Dutch(DutchCmd), } - -/// Commands related to Dutch auctions -#[derive(Debug, Subcommand)] -pub enum DutchCmd { - /// Schedule a Dutch auction, a tool to help accomplish price discovery. - #[clap(display_order = 100, name = "schedule")] - DutchAuctionSchedule { - /// Source account initiating the auction. - #[clap(long, display_order = 100, default_value = "0")] - source: u32, - /// The value the seller wishes to auction. - #[clap(long, display_order = 200)] - input: String, - /// The maximum output the seller can receive. - /// - /// This implicitly defines the starting price for the auction. - #[clap(long, display_order = 400)] - max_output: String, - /// The minimum output the seller is willing to receive. - /// - /// This implicitly defines the ending price for the auction. - #[clap(long, display_order = 500)] - min_output: String, - /// The block height at which the auction begins. - /// - /// This allows the seller to schedule an auction at a future time. - #[clap(long, display_order = 600)] - start_height: u64, - /// The block height at which the auction ends. - /// - /// Together with `start_height`, `max_output`, and `min_output`, - /// this implicitly defines the speed of the auction. - #[clap(long, display_order = 700)] - end_height: u64, - /// The number of discrete price steps to use for the auction. - /// - /// `end_height - start_height` must be a multiple of `step_count`. - #[clap(long, display_order = 800)] - step_count: u64, - /// The selected fee tier to multiply the fee amount by. - #[clap(short, long, value_enum, default_value_t, display_order = 1000)] - fee_tier: FeeTier, - }, - /// Terminate a Dutch auction. - #[clap(display_order = 300, name = "end")] - DutchAuctionEnd { - /// Source account terminating the auction. - #[clap(long, display_order = 100, default_value = "0")] - source: u32, - /// Identifier of the auction. - #[clap(long, display_order = 200)] - auction_id: String, - /// The selected fee tier to multiply the fee amount by. - #[clap(short, long, value_enum, default_value_t, display_order = 300)] - fee_tier: FeeTier, - }, - /// Withdraw a Dutch auction, and claim its reserves. - #[clap(display_order = 200, name = "withdraw")] - DutchAuctionWithdraw { - /// Source account withdrawing from the auction. - #[clap(long, display_order = 100)] - source: u32, - /// The auction to withdraw funds from. - #[clap(long, display_order = 200)] - auction_id: String, - // /// The sequence number of the withdrawal. - // #[clap(long, display_order = 300)] - // seq: u64, - // /// The amount of the input asset directly owned by the auction. - // /// - // /// The auction may also own the input asset indirectly, - // /// via the reserves of `current_position` if it exists. - // #[clap(long, display_order = 400)] - // reserves_input: String, - // /// The amount of the output asset directly owned by the auction. - // /// - // /// The auction may also own the output asset indirectly, - // /// via the reserves of `current_position` if it exists. - // #[clap(long, display_order = 500)] - // reserves_output: String, - /// The selected fee tier to multiply the fee amount by. - #[clap(short, long, value_enum, default_value_t, display_order = 600)] - fee_tier: FeeTier, - }, -} - -impl DutchCmd { - /// Process the command by performing the appropriate action. - pub async fn exec(&self, app: &mut App) -> anyhow::Result<()> { - let gas_prices = app - .view - .as_mut() - .context("view service must be initialized")? - .gas_prices(GasPricesRequest {}) - .await? - .into_inner() - .gas_prices - .expect("gas prices must be available") - .try_into()?; - - match self { - DutchCmd::DutchAuctionSchedule { - source, - input, - max_output, - min_output, - start_height, - end_height, - step_count, - fee_tier, - } => { - let mut nonce = [0u8; 32]; - OsRng.fill_bytes(&mut nonce); - - let input = input.parse::()?; - let max_output = max_output.parse::()?; - let min_output = min_output.parse::()?; - let output_id = max_output.asset_id; - ensure!( - min_output.asset_id == output_id, - "min and max output must be the same asset" - ); - - let plan = Planner::new(OsRng) - .set_gas_prices(gas_prices) - .set_fee_tier((*fee_tier).into()) - .dutch_auction_schedule(DutchAuctionDescription { - input, - output_id, - max_output: max_output.amount, - min_output: min_output.amount, - start_height: *start_height, - end_height: *end_height, - step_count: *step_count, - nonce, - }) - .plan( - app.view - .as_mut() - .context("view service must be initialized")?, - AddressIndex::new(*source), - ) - .await - .context("can't build send transaction")?; - app.build_and_submit_transaction(plan).await?; - Ok(()) - } - DutchCmd::DutchAuctionEnd { - auction_id, - source, - fee_tier, - } => { - let auction_id = auction_id.parse::()?; - - let plan = Planner::new(OsRng) - .set_gas_prices(gas_prices) - .set_fee_tier((*fee_tier).into()) - .dutch_auction_end(auction_id) - .plan( - app.view - .as_mut() - .context("view service must be initialized")?, - AddressIndex::new(*source), - ) - .await - .context("can't build send transaction")?; - app.build_and_submit_transaction(plan).await?; - Ok(()) - } - DutchCmd::DutchAuctionWithdraw { - source, - auction_id, - // seq, - // reserves_input, - // reserves_output, - fee_tier, - } => { - let auction_id = auction_id.parse::()?; - - use pbjson_types::Any; - use penumbra_view::ViewClient; - let view_client = app.view(); - let (auction_id, _, auction_raw, _): ( - AuctionId, - SpendableNoteRecord, - Option, - Vec, - ) = view_client - .auctions(None, true, true) - .await? - .into_iter() - .find(|(id, _, _, _)| &auction_id == id) - .ok_or_else(|| anyhow!("the auction id is unknown from the view service!"))?; - - let Some(raw_da_state) = auction_raw else { - bail!("auction state is missing from view server response") - }; - - use penumbra_proto::core::component::auction::v1alpha1 as pb_auction; - // We're processing a Dutch auction: - assert_eq!(raw_da_state.type_url, pb_auction::DutchAuction::type_url()); - - let dutch_auction = DutchAuction::decode(raw_da_state.value)?; - - let reserves_input = Value { - amount: dutch_auction.state.input_reserves, - asset_id: dutch_auction.description.input.asset_id, - }; - let reserves_output = Value { - amount: dutch_auction.state.output_reserves, - asset_id: dutch_auction.description.output_id, - }; - let seq = dutch_auction.state.sequence + 1; - - let mut planner = Planner::new(OsRng); - - let plan = planner - .set_gas_prices(gas_prices) - .set_fee_tier((*fee_tier).into()) - .dutch_auction_withdraw(ActionDutchAuctionWithdrawPlan { - auction_id, - seq, - reserves_input, - reserves_output, - }) - .plan(app.view(), source.into()) - .await - .context("can't build send transaction")?; - app.build_and_submit_transaction(plan).await?; - Ok(()) - } - } - } -} diff --git a/crates/bin/pcli/src/command/tx/auction/dutch/debug.rs b/crates/bin/pcli/src/command/tx/auction/dutch/debug.rs new file mode 100644 index 0000000000..6fefc84878 --- /dev/null +++ b/crates/bin/pcli/src/command/tx/auction/dutch/debug.rs @@ -0,0 +1,25 @@ +use penumbra_auction::auction::dutch::DutchAuctionDescription; +use serde::Serialize; + +#[derive(Debug, Clone, Serialize)] +pub struct DebugDescription { + pub input: u128, + pub min_output: u128, + pub max_output: u128, + pub start_height: u64, + pub end_height: u64, + pub step_count: u64, +} + +impl From for DebugDescription { + fn from(desc: DutchAuctionDescription) -> Self { + DebugDescription { + input: desc.input.amount.value(), + min_output: desc.min_output.value(), + max_output: desc.max_output.value(), + start_height: desc.start_height, + end_height: desc.end_height, + step_count: desc.step_count, + } + } +} diff --git a/crates/bin/pcli/src/command/tx/auction/dutch/gda.rs b/crates/bin/pcli/src/command/tx/auction/dutch/gda.rs new file mode 100644 index 0000000000..bdd2ef1c7a --- /dev/null +++ b/crates/bin/pcli/src/command/tx/auction/dutch/gda.rs @@ -0,0 +1,191 @@ +use clap::ArgEnum; +use penumbra_asset::Value; +use penumbra_auction::auction::dutch::DutchAuctionDescription; +use rand::RngCore; +use rand_core::OsRng; +use rand_distr::Distribution; +use serde::Serialize; + +#[derive(ArgEnum, Clone, Debug, Serialize)] +pub enum GdaRecipe { + #[clap(name = "10m")] + TenMinutes, + #[clap(name = "30m")] + ThirtyMinutes, + #[clap(name = "1h")] + OneHour, + #[clap(name = "2h")] + TwoHours, + #[clap(name = "6h")] + SixHours, + #[clap(name = "12h")] + TwelveHours, + #[clap(name = "1d")] + OneDay, + #[clap(name = "2d")] + TwoDays, +} + +impl GdaRecipe { + pub fn as_blocks(&self) -> u64 { + match self { + GdaRecipe::TenMinutes => 10 * 12, + GdaRecipe::ThirtyMinutes => 30 * 12, + GdaRecipe::OneHour => 60 * 12, + GdaRecipe::TwoHours => 2 * 60 * 12, + GdaRecipe::SixHours => 6 * 60 * 12, + GdaRecipe::TwelveHours => 12 * 60 * 12, + GdaRecipe::OneDay => 24 * 60 * 12, + GdaRecipe::TwoDays => 2 * 24 * 60 * 12, + } + } + + pub fn poisson_intensity_per_block(&self) -> f64 { + match &self { + GdaRecipe::TenMinutes => 0.064614, + GdaRecipe::ThirtyMinutes => 0.050577, + GdaRecipe::OneHour => 0.042122, + GdaRecipe::TwoHours => 0.022629, + GdaRecipe::SixHours => 0.010741, + GdaRecipe::TwelveHours => 0.00537, + GdaRecipe::OneDay => 0.03469, + GdaRecipe::TwoDays => 0.00735, + } + } + + pub fn poisson_intensity(&self) -> f64 { + self.poisson_intensity_per_block() * self.as_blocks() as f64 + } + + pub fn num_auctions(&self) -> u64 { + match self { + GdaRecipe::TenMinutes => 4, + GdaRecipe::ThirtyMinutes => 12, + GdaRecipe::OneHour => 12, + GdaRecipe::TwoHours => 24, + GdaRecipe::SixHours => 36, + GdaRecipe::TwelveHours => 36, + GdaRecipe::OneDay => 48, + GdaRecipe::TwoDays => 48, + } + } + + pub fn sub_auction_length(&self) -> u64 { + match &self { + GdaRecipe::TenMinutes => 60, + GdaRecipe::ThirtyMinutes => 60, + GdaRecipe::OneHour => 120, + GdaRecipe::TwoHours => 120, + GdaRecipe::SixHours => 240, + GdaRecipe::TwelveHours => 480, + GdaRecipe::OneDay => 720, + GdaRecipe::TwoDays => 1440, + } + } + + pub fn step_count(&self) -> u64 { + 60 + } +} + +#[derive(Debug, Serialize)] +pub struct GradualAuction { + pub input: Value, + pub max_output: Value, + pub min_output: Value, + pub recipe: GdaRecipe, + pub start_height: u64, +} + +impl GradualAuction { + pub fn new( + input: Value, + max_output: Value, + min_output: Value, + recipe: GdaRecipe, + start_height: u64, + ) -> Self { + GradualAuction { + input, + max_output, + min_output, + recipe, + start_height, + } + } + + pub fn generate_start_heights(&self) -> Vec { + use rand_distr::Exp; + let lambda = self.recipe.poisson_intensity(); + let num_auctions = self.recipe.num_auctions(); + let start_height = self.start_height; + let sub_auction_length = self.recipe.sub_auction_length(); + tracing::debug!( + lambda, + num_auctions, + start_height, + sub_auction_length, + num_blocks = self.recipe.as_blocks(), + "generating auction starts" + ); + + let mut rng = rand::thread_rng(); + let exp_dist = Exp::new(1.0 / lambda).unwrap(); + let mut current_height = start_height as f64; + + let mut auction_starts = Vec::with_capacity(num_auctions as usize); + for _ in 0..num_auctions { + let ff_clock = exp_dist.sample(&mut rng); + current_height += ff_clock; + let height = current_height.ceil() as u64; + tracing::debug!(height, arrival_time = ff_clock, "selected auction start"); + auction_starts.push(height) + } + + auction_starts + } + + pub fn generate_auctions(&self) -> Vec { + let start_heights = self.generate_start_heights(); + let sub_auction_length = self.recipe.sub_auction_length(); + let step_count = self.recipe.step_count(); + let num_auctions = self.recipe.num_auctions(); + let mut auctions = Vec::with_capacity(num_auctions as usize); + for start_height in start_heights { + let amount_chunk = self.input.amount.value() / num_auctions as u128; + let input_chunk = Value { + asset_id: self.input.asset_id, + amount: amount_chunk.into(), + }; + + let scaled_min_output = self.min_output.amount.value() / num_auctions as u128; + let scaled_max_output = self.max_output.amount.value() / num_auctions as u128; + + let mut nonce = [0u8; 32]; + OsRng.fill_bytes(&mut nonce); + + let end_height = start_height + sub_auction_length; + tracing::debug!( + start_height, + end_height, + sub_auction_length, + step_count, + ?input_chunk, + "generating auction" + ); + + let auction = DutchAuctionDescription { + input: input_chunk, + output_id: self.max_output.asset_id, + max_output: scaled_max_output.into(), + min_output: scaled_min_output.into(), + start_height, + end_height, + step_count, + nonce, + }; + auctions.push(auction); + } + auctions + } +} diff --git a/crates/bin/pcli/src/command/tx/auction/dutch/mod.rs b/crates/bin/pcli/src/command/tx/auction/dutch/mod.rs new file mode 100644 index 0000000000..3cfd72599b --- /dev/null +++ b/crates/bin/pcli/src/command/tx/auction/dutch/mod.rs @@ -0,0 +1,448 @@ +use std::path::Path; + +use crate::command::tx::FeeTier; +use crate::App; +use anyhow::{anyhow, bail, Context}; +use clap::Subcommand; +use comfy_table::presets; +use dialoguer::Confirm; +use penumbra_asset::{asset::Cache, Value}; +use penumbra_auction::auction::dutch::actions::ActionDutchAuctionWithdrawPlan; +use penumbra_auction::auction::{dutch::DutchAuction, dutch::DutchAuctionDescription, AuctionId}; +use penumbra_dex::lp::position::Position; +use penumbra_keys::keys::AddressIndex; +use penumbra_num::Amount; +use penumbra_proto::{view::v1::GasPricesRequest, DomainType, Name}; +use penumbra_view::SpendableNoteRecord; +use penumbra_view::ViewClient; +use penumbra_wallet::plan::Planner; +use rand::RngCore; +use rand_core::OsRng; +use serde_json; + +mod debug; +pub mod gda; + +/// Commands related to Dutch auctions +#[derive(Debug, Subcommand)] +pub enum DutchCmd { + /// Schedule a gradual dutch auction, a prototype for penumbra developers. + #[clap(display_order = 1000, name = "gradual")] + DutchAuctionGradualSchedule { + /// Source account initiating the auction. + #[clap(long, display_order = 100, default_value = "0")] + source: u32, + /// The value the seller wishes to auction. + #[clap(long, display_order = 200)] + input: String, + /// The maximum output the seller can receive. + /// + /// This implicitly defines the starting price for the auction. + #[clap(long, display_order = 400)] + max_output: String, + /// The minimum output the seller is willing to receive. + /// + /// This implicitly defines the ending price for the auction. + #[clap(long, display_order = 500)] + min_output: String, + /// The duration for the auction + #[clap(arg_enum, long, display_order = 600, name = "duration")] + recipe: gda::GdaRecipe, + /// Skip asking for confirmation, pay any fees, and execute the transaction. + #[clap(long, display_order = 700)] + yes: bool, + /// The selected fee tier to multiply the fee amount by. + #[clap(short, long, value_enum, default_value_t, display_order = 1000)] + fee_tier: FeeTier, + #[clap(long, hide = true)] + // Use to produce a debug file for numerical analysis. + debug: bool, + }, + /// Schedule a Dutch auction, a tool to help accomplish price discovery. + #[clap(display_order = 100, name = "schedule")] + DutchAuctionSchedule { + /// Source account initiating the auction. + #[clap(long, display_order = 100, default_value = "0")] + source: u32, + /// The value the seller wishes to auction. + #[clap(long, display_order = 200)] + input: String, + /// The maximum output the seller can receive. + /// + /// This implicitly defines the starting price for the auction. + #[clap(long, display_order = 400)] + max_output: String, + /// The minimum output the seller is willing to receive. + /// + /// This implicitly defines the ending price for the auction. + #[clap(long, display_order = 500)] + min_output: String, + /// The block height at which the auction begins. + /// + /// This allows the seller to schedule an auction at a future time. + #[clap(long, display_order = 600)] + start_height: u64, + /// The block height at which the auction ends. + /// + /// Together with `start_height`, `max_output`, and `min_output`, + /// this implicitly defines the speed of the auction. + #[clap(long, display_order = 700)] + end_height: u64, + /// The number of discrete price steps to use for the auction. + /// + /// `end_height - start_height` must be a multiple of `step_count`. + #[clap(long, display_order = 800)] + step_count: u64, + /// The selected fee tier to multiply the fee amount by. + #[clap(short, long, value_enum, default_value_t, display_order = 1000)] + fee_tier: FeeTier, + }, + /// Terminate a Dutch auction. + #[clap(display_order = 300, name = "end")] + DutchAuctionEnd { + /// Source account terminating the auction. + #[clap(long, display_order = 100, default_value = "0")] + source: u32, + /// Identifier of the auction. + #[clap(long, display_order = 200)] + auction_id: String, + /// The selected fee tier to multiply the fee amount by. + #[clap(short, long, value_enum, default_value_t, display_order = 300)] + fee_tier: FeeTier, + }, + /// Withdraw a Dutch auction, and claim its reserves. + #[clap(display_order = 200, name = "withdraw")] + DutchAuctionWithdraw { + /// Source account withdrawing from the auction. + #[clap(long, display_order = 100)] + source: u32, + /// The auction to withdraw funds from. + #[clap(long, display_order = 200)] + auction_id: String, + /// The selected fee tier to multiply the fee amount by. + #[clap(short, long, value_enum, default_value_t, display_order = 600)] + fee_tier: FeeTier, + }, +} + +impl DutchCmd { + /// Process the command by performing the appropriate action. + pub async fn exec(&self, app: &mut App) -> anyhow::Result<()> { + let gas_prices = app + .view + .as_mut() + .context("view service must be initialized")? + .gas_prices(GasPricesRequest {}) + .await? + .into_inner() + .gas_prices + .expect("gas prices must be available") + .try_into()?; + + match self { + DutchCmd::DutchAuctionSchedule { + source, + input, + max_output, + min_output, + start_height, + end_height, + step_count, + fee_tier, + } => { + let mut nonce = [0u8; 32]; + OsRng.fill_bytes(&mut nonce); + + let input = input.parse::()?; + let max_output = max_output.parse::()?; + let min_output = min_output.parse::()?; + let output_id = max_output.asset_id; + + let plan = Planner::new(OsRng) + .set_gas_prices(gas_prices) + .set_fee_tier((*fee_tier).into()) + .dutch_auction_schedule(DutchAuctionDescription { + input, + output_id, + max_output: max_output.amount, + min_output: min_output.amount, + start_height: *start_height, + end_height: *end_height, + step_count: *step_count, + nonce, + }) + .plan( + app.view + .as_mut() + .context("view service must be initialized")?, + AddressIndex::new(*source), + ) + .await + .context("can't build send transaction")?; + app.build_and_submit_transaction(plan).await?; + Ok(()) + } + DutchCmd::DutchAuctionEnd { + auction_id, + source, + fee_tier, + } => { + let auction_id = auction_id.parse::()?; + + let plan = Planner::new(OsRng) + .set_gas_prices(gas_prices) + .set_fee_tier((*fee_tier).into()) + .dutch_auction_end(auction_id) + .plan( + app.view + .as_mut() + .context("view service must be initialized")?, + AddressIndex::new(*source), + ) + .await + .context("can't build send transaction")?; + app.build_and_submit_transaction(plan).await?; + Ok(()) + } + DutchCmd::DutchAuctionWithdraw { + source, + auction_id, + fee_tier, + } => { + let auction_id = auction_id.parse::()?; + + use pbjson_types::Any; + let view_client = app.view(); + let (auction_id, _, auction_raw, _): ( + AuctionId, + SpendableNoteRecord, + Option, + Vec, + ) = view_client + .auctions(None, true, true) + .await? + .into_iter() + .find(|(id, _, _, _)| &auction_id == id) + .ok_or_else(|| anyhow!("the auction id is unknown from the view service!"))?; + + let Some(raw_da_state) = auction_raw else { + bail!("auction state is missing from view server response") + }; + + use penumbra_proto::core::component::auction::v1alpha1 as pb_auction; + // We're processing a Dutch auction: + assert_eq!(raw_da_state.type_url, pb_auction::DutchAuction::type_url()); + + let dutch_auction = DutchAuction::decode(raw_da_state.value)?; + + let reserves_input = Value { + amount: dutch_auction.state.input_reserves, + asset_id: dutch_auction.description.input.asset_id, + }; + let reserves_output = Value { + amount: dutch_auction.state.output_reserves, + asset_id: dutch_auction.description.output_id, + }; + let seq = dutch_auction.state.sequence + 1; + + let mut planner = Planner::new(OsRng); + + let plan = planner + .set_gas_prices(gas_prices) + .set_fee_tier((*fee_tier).into()) + .dutch_auction_withdraw(ActionDutchAuctionWithdrawPlan { + auction_id, + seq, + reserves_input, + reserves_output, + }) + .plan( + app.view + .as_mut() + .context("view service must be initialized")?, + AddressIndex::new(*source), + ) + .await + .context("can't build send transaction")?; + app.build_and_submit_transaction(plan).await?; + Ok(()) + } + DutchCmd::DutchAuctionGradualSchedule { + source, + input: input_str, + max_output: max_output_str, + min_output: min_output_str, + recipe: duration, + yes, + fee_tier, + debug, + } => { + println!("Gradual dutch auction prototype"); + + let input = input_str.parse::()?; + let max_output = max_output_str.parse::()?; + let min_output = min_output_str.parse::()?; + + let asset_cache = app.view().assets().await?; + let current_height = app.view().status().await?.full_sync_height; + + let gda = gda::GradualAuction::new( + input, + max_output, + min_output, + duration.clone(), + current_height, + ); + + let auction_descriptions = gda.generate_auctions(); + + let input_fmt = input.format(&asset_cache); + let max_output_fmt = max_output.format(&asset_cache); + let min_output_fmt = min_output.format(&asset_cache); + + println!("total to auction: {input_fmt}"); + println!("start price: {max_output_fmt}"); + println!("end price: {min_output_fmt}"); + display_auction_description(&asset_cache, auction_descriptions.clone()); + + let mut planner = Planner::new(OsRng); + planner + .set_gas_prices(gas_prices) + .set_fee_tier((*fee_tier).into()); + + for description in &auction_descriptions { + planner.dutch_auction_schedule(description.clone()); + } + + if *debug { + let debug_data_path = Path::new("gda-debug-definition-data.json"); + let auction_data_path = Path::new("gda-debug-auction-data.json"); + + let gda_debug_data = serde_json::to_string(&gda)?; + std::fs::write(debug_data_path, gda_debug_data)?; + + let gda_auction_data = serde_json::to_string( + &auction_descriptions + .clone() + .into_iter() + .map(Into::::into) + .collect::>(), + )?; + std::fs::write(auction_data_path, gda_auction_data)?; + tracing::debug!(?debug_data_path, ?auction_data_path, "wrote debug data"); + return Ok(()); + } + + let plan = planner + .plan( + app.view + .as_mut() + .context("view service must be initialized")?, + AddressIndex::new(*source), + ) + .await + .context("can't build send transaction")?; + + let tx = app.build_transaction(plan.clone()).await?; + let fee_fmt = tx + .transaction_body + .transaction_parameters + .fee + .0 + .format(&asset_cache); + + println!("Total fee: {fee_fmt}"); + + if !yes { + Confirm::new() + .with_prompt("Do you wish to proceed") + .interact()?; + } + app.build_and_submit_transaction(plan).await?; + + Ok(()) + } + } + } +} + +fn display_auction_description(asset_cache: &Cache, auctions: Vec) { + let mut tally_max_output = Amount::zero(); + let mut tally_min_output = Amount::zero(); + let mut tally_input = Amount::zero(); + let input_id = auctions[0].input.asset_id; + let output_id = auctions[0].output_id; + + let mut table = comfy_table::Table::new(); + table.load_preset(presets::NOTHING); + + table.set_header(vec![ + "start", + "", + "end", + "lot", + "start price for the lot", + "reserve price for the lot", + ]); + + for auction in auctions { + let start_height = auction.start_height; + let end_height = auction.end_height; + let input_chunk = Value { + asset_id: auction.input.asset_id, + amount: Amount::from(auction.input.amount.value()), + }; + + let max_price = Value { + asset_id: auction.output_id, + amount: Amount::from(auction.max_output.value()), + }; + + let min_price = Value { + asset_id: auction.output_id, + amount: Amount::from(auction.min_output.value()), + }; + + let max_price_fmt = max_price.format(&asset_cache); + let min_price_fmt = min_price.format(&asset_cache); + + let input_chunk_fmt = input_chunk.format(&asset_cache); + + tally_input += input_chunk.amount; + tally_max_output += max_price.amount; + tally_min_output += min_price.amount; + + table.add_row(vec![ + format!("{start_height}"), + "--------->".to_string(), + format!("{end_height}"), + input_chunk_fmt, + max_price_fmt, + min_price_fmt, + ]); + } + + println!("{}", table); + + let tally_input_fmt = Value { + asset_id: input_id, + amount: tally_input, + } + .format(&asset_cache); + + let tally_output_max_fmt = Value { + asset_id: output_id, + amount: tally_max_output, + } + .format(&asset_cache); + + let tally_output_min_fmt = Value { + asset_id: output_id, + amount: tally_min_output, + } + .format(&asset_cache); + + println!("Total auctioned: {tally_input_fmt}"); + println!("Total max output: {tally_output_max_fmt}"); + println!("Total min output: {tally_output_min_fmt}"); +} diff --git a/crates/bin/pcli/src/command/view/auction.rs b/crates/bin/pcli/src/command/view/auction.rs index e3cf5e5f71..16fdfee421 100644 --- a/crates/bin/pcli/src/command/view/auction.rs +++ b/crates/bin/pcli/src/command/view/auction.rs @@ -41,8 +41,8 @@ impl AuctionCmd { if pb_auction_state.type_url == pb_auction::DutchAuction::type_url() { let dutch_auction = DutchAuction::decode(pb_auction_state.value) .expect("no deserialization error"); - let position = positions.get(0).cloned(); - render_dutch_auction(&dutch_auction, position) + let asset_cache = view_client.assets().await?; + render_dutch_auction(&asset_cache, &dutch_auction, positions.get(0).cloned()) .await .expect("no rendering errors"); } else { @@ -51,7 +51,7 @@ impl AuctionCmd { } else { let position_ids: Vec = positions .into_iter() - .map(|lp| format!("{}", lp.id())) + .map(|lp: penumbra_dex::lp::position::Position| format!("{}", lp.id())) .collect(); let mut auction_table = Table::new(); @@ -65,22 +65,7 @@ impl AuctionCmd { .set_alignment(comfy_table::CellAlignment::Center), ]); - let mut position_table = Table::new(); - position_table - .load_preset(presets::NOTHING) - .set_content_arrangement(ContentArrangement::Dynamic) - .set_table_width(80) - .set_header(vec![ - "position id", - "state", - "input reserves", - "output reserves", - "quoting price", - ]) - .add_row(vec![Cell::new("nothing for now")]); - println!("{auction_table}"); - println!("detected auction with") } } Ok(())