Skip to content

Commit

Permalink
pcli(auction): end or withdraw all auctions in one go (#4385)
Browse files Browse the repository at this point in the history
## Describe your changes

Changes the `pcli tx auction` commands to support "end all", "withdraw
all" rather than requiring specific auction IDs


## Checklist before requesting a review

- [x] If this code contains consensus-breaking changes, I have added the
"consensus-breaking" label. Otherwise, I declare my belief that there
are not consensus-breaking changes, for the following reason:

  > Only client-side changes

---------

Signed-off-by: Erwan Or <[email protected]>
Co-authored-by: Erwan Or <[email protected]>
Co-authored-by: Erwan Or <[email protected]>
  • Loading branch information
3 people authored May 28, 2024
1 parent 8bcbb40 commit 3a8f005
Show file tree
Hide file tree
Showing 7 changed files with 256 additions and 109 deletions.
17 changes: 12 additions & 5 deletions crates/bin/pcli/src/command/query/auction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ impl AuctionCmd {

let asset_cache = app.view().assets().await?;

render_dutch_auction(&asset_cache, &dutch_auction, position).await?;
render_dutch_auction(&asset_cache, &dutch_auction, None, position).await?;
} else {
unimplemented!("only supporting dutch auctions at the moment, come back later");
}
Expand All @@ -79,10 +79,11 @@ impl AuctionCmd {
pub async fn render_dutch_auction(
asset_cache: &Cache,
dutch_auction: &DutchAuction,
local_view: Option<u64>,
position: Option<Position>,
) -> anyhow::Result<()> {
let auction_id = dutch_auction.description.id();
println!("dutch auction with id {auction_id:?}");
println!("dutch auction with id {auction_id:?}:");

let initial_input = dutch_auction.description.input;
let input_id = initial_input.asset_id;
Expand Down Expand Up @@ -137,7 +138,7 @@ pub async fn render_dutch_auction(
.set_content_arrangement(ContentArrangement::DynamicFullWidth)
.add_row(vec![
Cell::new(truncate_auction_id(&auction_id)).set_delimiter('.'),
Cell::new(render_sequence(dutch_auction.state.sequence)),
Cell::new(render_sequence(dutch_auction.state.sequence, local_view)),
Cell::new(format!("{start_height} -> {end_height}")),
Cell::new(dutch_auction.description.step_count.to_string()),
Cell::new(format!("{}", start_price)),
Expand All @@ -163,13 +164,19 @@ pub async fn render_dutch_auction(
Ok(())
}

fn render_sequence(state: u64) -> String {
if state == 0 {
fn render_sequence(state: u64, local_seq: Option<u64>) -> String {
let main = if state == 0 {
format!("Opened")
} else if state == 1 {
format!("Closed")
} else {
format!("Withdrawn (seq={state})")
};

if let Some(local_seq) = local_seq {
format!("{main} (local_seq={local_seq})")
} else {
main
}
}

Expand Down
201 changes: 142 additions & 59 deletions crates/bin/pcli/src/command/tx/auction/dutch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,16 @@ use std::path::Path;

use crate::command::tx::FeeTier;
use crate::App;
use anyhow::Result;
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_proto::{view::v1::GasPricesRequest, DomainType};
use penumbra_view::ViewClient;
use penumbra_wallet::plan::Planner;
use rand::RngCore;
Expand Down Expand Up @@ -103,9 +101,12 @@ pub enum DutchCmd {
/// 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,
/// If set, ends all auctions owned by the specified account.
#[clap(long, display_order = 150)]
all: bool,
/// Identifier of the auction to end, if `--all` is not set.
#[clap(display_order = 200)]
auction_id: Option<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,
Expand All @@ -114,11 +115,14 @@ pub enum DutchCmd {
#[clap(display_order = 200, name = "withdraw")]
DutchAuctionWithdraw {
/// Source account withdrawing from the auction.
#[clap(long, display_order = 100)]
#[clap(long, display_order = 100, default_value = "0")]
source: u32,
/// The auction to withdraw funds from.
#[clap(long, display_order = 200)]
auction_id: String,
/// If set, withdraws all auctions owned by the specified account.
#[clap(long, display_order = 150)]
all: bool,
/// Identifier of the auction to withdraw from, if `--all` is not set.
#[clap(display_order = 200)]
auction_id: Option<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,
Expand Down Expand Up @@ -178,92 +182,92 @@ impl DutchCmd {
AddressIndex::new(*source),
)
.await
.context("can't build send transaction")?;
.context("can't build auction schedule transaction")?;
app.build_and_submit_transaction(plan).await?;
Ok(())
}
DutchCmd::DutchAuctionEnd {
all,
auction_id,
source,
fee_tier,
} => {
let auction_id = auction_id.parse::<AuctionId>()?;
let auction_ids = match (all, auction_id) {
(true, _) => auctions_to_end(app.view(), *source).await?,
(false, Some(auction_id)) => {
let auction_id = auction_id.parse::<AuctionId>()?;
vec![auction_id]
}
(false, None) => {
bail!("auction_id is required when --all is not set")
}
};

let plan = Planner::new(OsRng)
let mut planner = Planner::new(OsRng);

planner
.set_gas_prices(gas_prices)
.set_fee_tier((*fee_tier).into())
.dutch_auction_end(auction_id)
.set_fee_tier((*fee_tier).into());

for auction_id in auction_ids {
planner.dutch_auction_end(auction_id);
}

let plan = planner
.plan(
app.view
.as_mut()
.context("view service must be initialized")?,
AddressIndex::new(*source),
)
.await
.context("can't build send transaction")?;
.context("can't build auction end transaction")?;
app.build_and_submit_transaction(plan).await?;
Ok(())
}
DutchCmd::DutchAuctionWithdraw {
all,
source,
auction_id,
fee_tier,
} => {
let auction_id = auction_id.parse::<AuctionId>()?;

use pbjson_types::Any;
let view_client = app.view();
let (auction_id, _, auction_raw, _): (
AuctionId,
SpendableNoteRecord,
Option<Any>,
Vec<Position>,
) = 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::v1 as pb_auction;
// We're processing a Dutch auction:
assert_eq!(raw_da_state.type_url, pb_auction::DutchAuction::type_url());
let auctions = match (all, auction_id) {
(true, _) => auctions_to_withdraw(app.view(), *source).await?,
(false, Some(auction_id)) => {
let auction_id = auction_id.parse::<AuctionId>()?;

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 all = auctions_to_withdraw(app.view(), *source).await?;
vec![all
.into_iter()
.find(|a| a.description.id() == auction_id)
.ok_or_else(|| {
anyhow!("the auction id is unknown from the view service!")
})?]
}
(false, None) => {
bail!("auction_id is required when --all is not set")
}
};
let seq = dutch_auction.state.sequence + 1;

let mut planner = Planner::new(OsRng);

let plan = planner
planner
.set_gas_prices(gas_prices)
.set_fee_tier((*fee_tier).into())
.dutch_auction_withdraw(ActionDutchAuctionWithdrawPlan {
auction_id,
seq,
reserves_input,
reserves_output,
})
.set_fee_tier((*fee_tier).into());

for auction in &auctions {
planner.dutch_auction_withdraw(auction);
}

let plan = planner
.plan(
app.view
.as_mut()
.context("view service must be initialized")?,
AddressIndex::new(*source),
)
.await
.context("can't build send transaction")?;
.context("can't build auction withdrawal transaction")?;
app.build_and_submit_transaction(plan).await?;
Ok(())
}
Expand Down Expand Up @@ -366,6 +370,85 @@ impl DutchCmd {
}
}

async fn all_dutch_auction_states(
view_client: &mut impl ViewClient,
source: impl Into<AddressIndex>,
) -> Result<Vec<(AuctionId, DutchAuction, u64)>> {
fetch_dutch_auction_states(view_client, source, true).await
}

async fn active_dutch_auction_states(
view_client: &mut impl ViewClient,
source: impl Into<AddressIndex>,
) -> Result<Vec<(AuctionId, DutchAuction, u64)>> {
fetch_dutch_auction_states(view_client, source, false).await
}

async fn fetch_dutch_auction_states(
view_client: &mut impl ViewClient,
source: impl Into<AddressIndex>,
include_inactive: bool,
) -> Result<Vec<(AuctionId, DutchAuction, u64)>> {
let auctions = view_client
.auctions(Some(source.into()), include_inactive, true)
.await?
.into_iter()
.filter_map(|(id, _, local_seq, state, _)| {
if let Some(state) = state {
if let Ok(da) = DutchAuction::decode(state.value) {
Some((id, da, local_seq))
} else {
None
}
} else {
None
}
})
.collect();
Ok(auctions)
}
/// Return all the auctions that need to be ended, based on our local view of the chain state.
async fn auctions_to_end(view_client: &mut impl ViewClient, source: u32) -> Result<Vec<AuctionId>> {
let auctions = active_dutch_auction_states(view_client, source).await?;

let auction_ids = auctions
.into_iter()
.filter_map(|(id, _auction, local_seq)| {
// We want to end auctions that we track as "opened" (local_seq == 0)
// so that we can close them, or catch-up with the chain state if they are already closed.
if local_seq == 0 {
Some(id)
} else {
None
}
})
.collect();

Ok(auction_ids)
}

async fn auctions_to_withdraw(
view_client: &mut impl ViewClient,
source: u32,
) -> Result<Vec<DutchAuction>> {
let auctions = all_dutch_auction_states(view_client, source).await?;

let auction_ids = auctions
.into_iter()
.filter_map(|(_, auction, local_seq)| {
// We want to end auctions that we track as "closed" (local_seq == 1)
// so that we can close them, or catch-up with the chain state if they are already closed.
if local_seq == 1 {
Some(auction)
} else {
None
}
})
.collect();

Ok(auction_ids)
}

fn display_auction_description(asset_cache: &Cache, auctions: Vec<DutchAuctionDescription>) {
let mut tally_max_output = Amount::zero();
let mut tally_min_output = Amount::zero();
Expand Down
14 changes: 10 additions & 4 deletions crates/bin/pcli/src/command/view/auction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,27 @@ impl AuctionCmd {
let auctions: Vec<(
penumbra_auction::auction::AuctionId,
penumbra_view::SpendableNoteRecord,
u64,
Option<pbjson_types::Any>,
Vec<penumbra_dex::lp::position::Position>,
)> = view_client
.auctions(None, self.include_inactive, self.query_latest_state)
.await?;

for (auction_id, _, maybe_auction_state, positions) in auctions.into_iter() {
for (auction_id, _, local_seq, maybe_auction_state, positions) in auctions.into_iter() {
if let Some(pb_auction_state) = maybe_auction_state {
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 asset_cache = view_client.assets().await?;
render_dutch_auction(&asset_cache, &dutch_auction, positions.get(0).cloned())
.await
.expect("no rendering errors");
render_dutch_auction(
&asset_cache,
&dutch_auction,
Some(local_seq),
positions.get(0).cloned(),
)
.await
.expect("no rendering errors");
} else {
unimplemented!("only supporting dutch auctions at the moment, come back later");
}
Expand Down
Loading

0 comments on commit 3a8f005

Please sign in to comment.